diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 9865388872b..17a27986ffc 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -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: diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py index 25cb508f777..c43bd4df7bd 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py @@ -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: diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 935a37886dd..e9926f61bb9 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -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 diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 8717564fde0..1cc3579003f 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -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) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 7979ac6ff79..ea09440f669 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -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): diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 8ebef95e47f..ffd1d674b10 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -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: