mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-13 06:31:48 +00:00
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:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user