perf: batch per-row bin lookups in sales invoice and delivery note

sales invoice's update_current_stock ran one bin query per item row and one
per packed row. delivery note already batched the same work by warehouse, so
lift that into get_bin_qty_map in stock/utils.py and have both call it.

also batch the per-batch expiry_date lookup in get_batches_by_oldest, and drop
three now-unused per-row setters: delivery note's set_actual_qty (already dead
before this change), sales invoice item's set_actual_qty and packed item's
set_actual_and_projected_qty.
This commit is contained in:
pandiyan
2026-08-11 15:40:23 +05:30
parent b5a3815a64
commit 6bf00d3c8b
6 changed files with 52 additions and 47 deletions

View File

@@ -29,6 +29,7 @@ from erpnext.setup.doctype.company.company import update_company_current_month_s
from erpnext.stock.doctype.delivery_note.services.billing_status import (
update_billed_amount_based_on_so,
)
from erpnext.stock.utils import get_bin_qty_map
from .services.fixed_assets import FixedAssetService
from .services.inter_company import (
@@ -991,11 +992,17 @@ class SalesInvoice(SellingController):
)
def update_current_stock(self):
bin_qty_map = get_bin_qty_map(self.items + self.packed_items)
for item in self.items:
item.set_actual_qty()
if item.item_code and item.warehouse:
bin_data = bin_qty_map.get((item.item_code, item.warehouse))
item.actual_qty = bin_data.actual_qty if bin_data else 0
for packed_item in self.packed_items:
packed_item.set_actual_and_projected_qty()
bin_data = bin_qty_map.get((packed_item.item_code, packed_item.warehouse))
packed_item.actual_qty = bin_data.actual_qty if bin_data else 0
packed_item.projected_qty = bin_data.projected_qty if bin_data else 0
def update_packing_list(self):
if cint(self.update_stock) == 1:

View File

@@ -114,15 +114,6 @@ class SalesInvoiceItem(Document):
)
)
def set_actual_qty(self):
if self.item_code and self.warehouse:
self.actual_qty = (
frappe.db.get_value(
"Bin", {"item_code": self.item_code, "warehouse": self.warehouse}, "actual_qty"
)
or 0
)
def set_income_account_for_fixed_asset(self, company: str):
"""Set income account for fixed asset item based on company's disposal account and cost center."""
if not self.is_fixed_asset:

View File

@@ -296,7 +296,18 @@ def get_batch_qty(
def get_batches_by_oldest(item_code: str, warehouse: str):
"""Returns the oldest batch and qty for the given item_code and warehouse"""
batches = get_batch_qty(item_code=item_code, warehouse=warehouse)
batches_dates = [[batch, frappe.get_value("Batch", batch.batch_no, "expiry_date")] for batch in batches]
if not batches:
return []
expiry_dates = dict(
frappe.get_all(
"Batch",
filters={"name": ["in", {batch.batch_no for batch in batches}]},
fields=["name", "expiry_date"],
as_list=True,
)
)
batches_dates = [[batch, expiry_dates.get(batch.batch_no)] for batch in batches]
batches_dates.sort(key=lambda tup: (tup[1] is None, tup[1]))
return batches_dates

View File

@@ -10,6 +10,7 @@ from erpnext.controllers.selling_controller import SellingController
from erpnext.stock.doctype.delivery_note.services.billing_status import BillingStatusService
from erpnext.stock.doctype.delivery_note.services.packing import PackingService
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
from erpnext.stock.utils import get_bin_qty_map
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -258,14 +259,6 @@ class DeliveryNote(SellingController):
super().before_print(settings)
def set_actual_qty(self):
for d in self.get("items"):
if d.item_code and d.warehouse:
actual_qty = frappe.db.get_value(
"Bin", {"item_code": d.item_code, "warehouse": d.warehouse}, "actual_qty"
)
d.actual_qty = flt(actual_qty) or 0
def so_required(self):
"""check in manage account if sales order required or not"""
if frappe.get_single_value("Selling Settings", "so_required") == "Yes":
@@ -404,28 +397,14 @@ class DeliveryNote(SellingController):
if not (self.get("_action") and self._action != "update_after_submit"):
return
warehouse_item_codes = {}
for d in self.get("items") + self.get("packed_items"):
warehouse_item_codes.setdefault(d.warehouse, set()).add(d.item_code)
if not warehouse_item_codes:
return
bin_map = {}
for warehouse, item_codes in warehouse_item_codes.items():
for b in frappe.get_all(
"Bin",
filters={"item_code": ["in", item_codes], "warehouse": warehouse},
fields=["item_code", "actual_qty", "projected_qty"],
):
bin_map[(b.item_code, warehouse)] = b
bin_qty_map = get_bin_qty_map(self.get("items") + self.get("packed_items"))
for d in self.get("items"):
bin_data = bin_map.get((d.item_code, d.warehouse))
bin_data = bin_qty_map.get((d.item_code, d.warehouse))
d.actual_qty = bin_data.actual_qty if bin_data else None
for d in self.get("packed_items"):
bin_data = bin_map.get((d.item_code, d.warehouse))
bin_data = bin_qty_map.get((d.item_code, d.warehouse))
if bin_data:
d.actual_qty = flt(bin_data.actual_qty)
d.projected_qty = flt(bin_data.projected_qty)

View File

@@ -56,16 +56,7 @@ class PackedItem(Document):
warehouse: DF.Link | None
# end: auto-generated types
def set_actual_and_projected_qty(self):
"Set actual and projected qty based on warehouse and item_code"
_bin = frappe.db.get_value(
"Bin",
{"item_code": self.item_code, "warehouse": self.warehouse},
["actual_qty", "projected_qty"],
as_dict=True,
)
self.actual_qty = _bin.actual_qty if _bin else 0
self.projected_qty = _bin.projected_qty if _bin else 0
pass
def make_packing_list(doc):

View File

@@ -205,6 +205,32 @@ def get_latest_stock_balance():
return bin_map
def get_bin_qty_map(rows) -> dict[tuple[str, str], frappe._dict]:
"""Map ``(item_code, warehouse)`` to its Bin's actual and projected qty.
Fetched in a single query so a document costs one query instead of one per
row. Rows without an item or warehouse are skipped, and item/warehouse pairs
with no Bin are absent from the map.
"""
item_codes = set()
warehouses = set()
for row in rows:
if row.item_code and row.warehouse:
item_codes.add(row.item_code)
warehouses.add(row.warehouse)
if not item_codes:
return {}
bins = frappe.get_all(
"Bin",
filters={"item_code": ["in", item_codes], "warehouse": ["in", warehouses]},
fields=["item_code", "warehouse", "actual_qty", "projected_qty"],
)
return {(bin.item_code, bin.warehouse): bin for bin in bins}
def get_bin(item_code, warehouse):
bin = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse})
if not bin: