mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-20 03:47:11 +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 (
|
from erpnext.stock.doctype.delivery_note.services.billing_status import (
|
||||||
update_billed_amount_based_on_so,
|
update_billed_amount_based_on_so,
|
||||||
)
|
)
|
||||||
|
from erpnext.stock.utils import get_bin_qty_map
|
||||||
|
|
||||||
from .services.fixed_assets import FixedAssetService
|
from .services.fixed_assets import FixedAssetService
|
||||||
from .services.inter_company import (
|
from .services.inter_company import (
|
||||||
@@ -991,11 +992,17 @@ class SalesInvoice(SellingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def update_current_stock(self):
|
def update_current_stock(self):
|
||||||
|
bin_qty_map = get_bin_qty_map(self.items + self.packed_items)
|
||||||
|
|
||||||
for item in self.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:
|
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):
|
def update_packing_list(self):
|
||||||
if cint(self.update_stock) == 1:
|
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):
|
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."""
|
"""Set income account for fixed asset item based on company's disposal account and cost center."""
|
||||||
if not self.is_fixed_asset:
|
if not self.is_fixed_asset:
|
||||||
|
|||||||
@@ -296,7 +296,18 @@ def get_batch_qty(
|
|||||||
def get_batches_by_oldest(item_code: str, warehouse: str):
|
def get_batches_by_oldest(item_code: str, warehouse: str):
|
||||||
"""Returns the oldest batch and qty for the given item_code and warehouse"""
|
"""Returns the oldest batch and qty for the given item_code and warehouse"""
|
||||||
batches = get_batch_qty(item_code=item_code, warehouse=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]))
|
batches_dates.sort(key=lambda tup: (tup[1] is None, tup[1]))
|
||||||
return batches_dates
|
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.billing_status import BillingStatusService
|
||||||
from erpnext.stock.doctype.delivery_note.services.packing import PackingService
|
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.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"}
|
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
||||||
|
|
||||||
@@ -258,14 +259,6 @@ class DeliveryNote(SellingController):
|
|||||||
|
|
||||||
super().before_print(settings)
|
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):
|
def so_required(self):
|
||||||
"""check in manage account if sales order required or not"""
|
"""check in manage account if sales order required or not"""
|
||||||
if frappe.get_single_value("Selling Settings", "so_required") == "Yes":
|
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"):
|
if not (self.get("_action") and self._action != "update_after_submit"):
|
||||||
return
|
return
|
||||||
|
|
||||||
warehouse_item_codes = {}
|
bin_qty_map = get_bin_qty_map(self.get("items") + self.get("packed_items"))
|
||||||
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
|
|
||||||
|
|
||||||
for d in self.get("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
|
d.actual_qty = bin_data.actual_qty if bin_data else None
|
||||||
|
|
||||||
for d in self.get("packed_items"):
|
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:
|
if bin_data:
|
||||||
d.actual_qty = flt(bin_data.actual_qty)
|
d.actual_qty = flt(bin_data.actual_qty)
|
||||||
d.projected_qty = flt(bin_data.projected_qty)
|
d.projected_qty = flt(bin_data.projected_qty)
|
||||||
|
|||||||
@@ -56,16 +56,7 @@ class PackedItem(Document):
|
|||||||
warehouse: DF.Link | None
|
warehouse: DF.Link | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def set_actual_and_projected_qty(self):
|
pass
|
||||||
"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
|
|
||||||
|
|
||||||
|
|
||||||
def make_packing_list(doc):
|
def make_packing_list(doc):
|
||||||
|
|||||||
@@ -205,6 +205,32 @@ def get_latest_stock_balance():
|
|||||||
return bin_map
|
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):
|
def get_bin(item_code, warehouse):
|
||||||
bin = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse})
|
bin = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse})
|
||||||
if not bin:
|
if not bin:
|
||||||
|
|||||||
Reference in New Issue
Block a user