From 45d25b9ec6b1da4951dcda906a18c3ee9b03ef26 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 4 Mar 2024 12:04:41 +0530 Subject: [PATCH 1/3] perf: serial and batch bundle valuation (reposting) (#40255) perf: serial and batch bundle valuation (cherry picked from commit 6379238893e94fa19da65b08428000e77fa50bd6) --- erpnext/stock/deprecated_serial_batch.py | 28 +++++++++++---- .../test_landed_cost_voucher.py | 17 --------- .../serial_and_batch_bundle.py | 7 +--- erpnext/stock/serial_batch_bundle.py | 36 ++++++++++--------- erpnext/stock/stock_ledger.py | 19 +++++++++- 5 files changed, 60 insertions(+), 47 deletions(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index ab38c151b641..7be1418823bb 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -13,7 +13,9 @@ def calculate_stock_value_from_deprecarated_ledgers(self): ): return - serial_nos = self.get_serial_nos() + serial_nos = self.get_filterd_serial_nos() + if not serial_nos: + return actual_qty = flt(self.sle.actual_qty) @@ -25,8 +27,21 @@ def calculate_stock_value_from_deprecarated_ledgers(self): self.stock_value_change += stock_value_change + def get_filterd_serial_nos(self): + serial_nos = [] + non_filtered_serial_nos = self.get_serial_nos() + + # If the serial no inwarded using the Serial and Batch Bundle, then the serial no should not be considered + for serial_no in non_filtered_serial_nos: + if serial_no and serial_no not in self.serial_no_incoming_rate: + serial_nos.append(serial_no) + + return serial_nos + @deprecated def get_incoming_value_for_serial_nos(self, serial_nos): + from erpnext.stock.utils import get_combine_datetime + # get rate from serial nos within same company incoming_values = 0.0 for serial_no in serial_nos: @@ -42,18 +57,19 @@ def get_incoming_value_for_serial_nos(self, serial_nos): | (table.serial_no.like("%\n" + serial_no + "\n%")) ) & (table.company == self.sle.company) + & (table.warehouse == self.sle.warehouse) & (table.serial_and_batch_bundle.isnull()) + & (table.actual_qty > 0) & (table.is_cancelled == 0) + & table.posting_datetime + <= get_combine_datetime(self.sle.posting_date, self.sle.posting_time) ) .orderby(table.posting_datetime, order=Order.desc) + .limit(1) ).run(as_dict=1) for sle in stock_ledgers: - self.serial_no_incoming_rate[serial_no] += ( - flt(sle.incoming_rate) - if sle.actual_qty > 0 - else (sle.stock_value_difference / sle.actual_qty) * -1 - ) + self.serial_no_incoming_rate[serial_no] += flt(sle.incoming_rate) incoming_values += self.serial_no_incoming_rate[serial_no] return incoming_values diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 257f263bd22a..4058aa82d7e2 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -415,23 +415,6 @@ def test_serialized_lcv_delivered(self): create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges) new_purchase_rate = serial_no_rate + charges - sn_obj = SerialNoValuation( - sle=frappe._dict( - { - "posting_date": today(), - "posting_time": nowtime(), - "item_code": "_Test Serialized Item", - "warehouse": "Stores - TCP1", - "serial_nos": [serial_no], - } - ) - ) - - new_serial_no_rate = sn_obj.get_incoming_rate_of_serial_no(serial_no) - - # Since the serial no is already delivered the rate must be zero - self.assertFalse(new_serial_no_rate) - stock_value_difference = frappe.db.get_value( "Stock Ledger Entry", filters={ diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 33f0dceba94c..d01dfefc9262 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -332,13 +332,8 @@ def set_incoming_rate_for_inward_transaction(self, row=None, save=False): rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field) for d in self.entries: - if not rate or ( - flt(rate, precision) == flt(d.incoming_rate, precision) and d.stock_value_difference - ): - continue - d.incoming_rate = flt(rate, precision) - if self.has_batch_no: + if d.qty: d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) if save: diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 24dd9d1d2035..1fcc439fda36 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -4,8 +4,9 @@ import frappe from frappe import _, bold from frappe.model.naming import make_autoname -from frappe.query_builder.functions import CombineDatetime, Sum +from frappe.query_builder.functions import CombineDatetime, Sum, Timestamp from frappe.utils import cint, cstr, flt, get_link_to_form, now, nowtime, today +from pypika import Order from erpnext.stock.deprecated_serial_batch import ( DeprecatedBatchNoValuation, @@ -424,19 +425,21 @@ def calculate_stock_value_change(self): ) else: - entries = self.get_serial_no_ledgers() - self.serial_no_incoming_rate = defaultdict(float) self.stock_value_change = 0.0 - for ledger in entries: - self.stock_value_change += ledger.incoming_rate - self.serial_no_incoming_rate[ledger.serial_no] += ledger.incoming_rate + serial_nos = self.get_serial_nos() + for serial_no in serial_nos: + incoming_rate = self.get_incoming_rate_from_bundle(serial_no) + if not incoming_rate: + continue + + self.stock_value_change += incoming_rate + self.serial_no_incoming_rate[serial_no] += incoming_rate self.calculate_stock_value_from_deprecarated_ledgers() - def get_serial_no_ledgers(self): - serial_nos = self.get_serial_nos() + def get_incoming_rate_from_bundle(self, serial_no) -> float: bundle = frappe.qb.DocType("Serial and Batch Bundle") bundle_child = frappe.qb.DocType("Serial and Batch Entry") @@ -444,20 +447,18 @@ def get_serial_no_ledgers(self): frappe.qb.from_(bundle) .inner_join(bundle_child) .on(bundle.name == bundle_child.parent) - .select( - bundle.name, - bundle_child.serial_no, - (bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate"), - ) + .select((bundle_child.incoming_rate * bundle_child.qty).as_("incoming_rate")) .where( (bundle.is_cancelled == 0) & (bundle.docstatus == 1) - & (bundle_child.serial_no.isin(serial_nos)) - & (bundle.type_of_transaction.isin(["Inward", "Outward"])) + & (bundle_child.serial_no == serial_no) + & (bundle.type_of_transaction == "Inward") + & (bundle_child.qty > 0) & (bundle.item_code == self.sle.item_code) & (bundle_child.warehouse == self.sle.warehouse) ) - .orderby(bundle.posting_date, bundle.posting_time, bundle.creation) + .orderby(Timestamp(bundle.posting_date, bundle.posting_time), order=Order.desc) + .limit(1) ) # Important to exclude the current voucher to calculate correct the stock value difference @@ -474,7 +475,8 @@ def get_serial_no_ledgers(self): query = query.where(timestamp_condition) - return query.run(as_dict=True) + incoming_rate = query.run() + return flt(incoming_rate[0][0]) if incoming_rate else 0.0 def get_serial_nos(self): if self.sle.get("serial_nos"): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index fa5938d52267..90c41f8e3f2a 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -952,7 +952,12 @@ def get_incoming_outgoing_rate_from_transaction(self, sle): get_rate_for_return, # don't move this import to top ) - if self.valuation_method == "Moving Average": + if ( + self.valuation_method == "Moving Average" + and not sle.get("serial_no") + and not sle.get("batch_no") + and not sle.get("serial_and_batch_bundle") + ): rate = get_incoming_rate( { "item_code": sle.item_code, @@ -979,6 +984,18 @@ def get_incoming_outgoing_rate_from_transaction(self, sle): voucher_detail_no=sle.voucher_detail_no, sle=sle, ) + + if ( + sle.get("serial_and_batch_bundle") + and rate > 0 + and sle.voucher_type in ["Delivery Note", "Sales Invoice"] + ): + frappe.db.set_value( + sle.voucher_type + " Item", + sle.voucher_detail_no, + "incoming_rate", + rate, + ) elif ( sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and sle.voucher_detail_no From 1834e8efe7852f7ee0088a7e6384e25f8577472c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 4 Mar 2024 12:45:34 +0530 Subject: [PATCH 2/3] chore: fix styles --- erpnext/stock/deprecated_serial_batch.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 7be1418823bb..6767c28733f3 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -61,8 +61,7 @@ def get_incoming_value_for_serial_nos(self, serial_nos): & (table.serial_and_batch_bundle.isnull()) & (table.actual_qty > 0) & (table.is_cancelled == 0) - & table.posting_datetime - <= get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + & (table.posting_datetime <= get_combine_datetime(self.sle.posting_date, self.sle.posting_time)) ) .orderby(table.posting_datetime, order=Order.desc) .limit(1) From 8f346758a679c54938388da16bb94f2f21450fa1 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 4 Mar 2024 12:53:13 +0530 Subject: [PATCH 3/3] chore: fix linters --- erpnext/stock/deprecated_serial_batch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 6767c28733f3..1aa8134b4859 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -61,7 +61,9 @@ def get_incoming_value_for_serial_nos(self, serial_nos): & (table.serial_and_batch_bundle.isnull()) & (table.actual_qty > 0) & (table.is_cancelled == 0) - & (table.posting_datetime <= get_combine_datetime(self.sle.posting_date, self.sle.posting_time)) + & ( + table.posting_datetime <= get_combine_datetime(self.sle.posting_date, self.sle.posting_time) + ) ) .orderby(table.posting_datetime, order=Order.desc) .limit(1)