From a5232d9c103c36c92038622da5c1998c0fbdbe60 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 1 Mar 2024 22:26:54 +0530 Subject: [PATCH] fix: serial no valuation rate (#40221) --- erpnext/stock/deprecated_serial_batch.py | 41 +++---- .../serial_and_batch_bundle.py | 2 +- .../test_serial_and_batch_bundle.py | 104 ++++++++++++++++++ 3 files changed, 123 insertions(+), 24 deletions(-) diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 2f1270e958..ab38c151b6 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -8,9 +8,12 @@ from pypika import Order class DeprecatedSerialNoValuation: @deprecated def calculate_stock_value_from_deprecarated_ledgers(self): - serial_nos = list( - filter(lambda x: x not in self.serial_no_incoming_rate and x, self.get_serial_nos()) - ) + if not frappe.db.get_value( + "Stock Ledger Entry", {"serial_no": ("is", "set"), "is_cancelled": 0}, "name" + ): + return + + serial_nos = self.get_serial_nos() actual_qty = flt(self.sle.actual_qty) @@ -25,23 +28,12 @@ class DeprecatedSerialNoValuation: @deprecated def get_incoming_value_for_serial_nos(self, serial_nos): # get rate from serial nos within same company - all_serial_nos = frappe.get_all( - "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)} - ) - incoming_values = 0.0 - for d in all_serial_nos: - if d.company == self.sle.company: - self.serial_no_incoming_rate[d.name] += flt(d.purchase_rate) - incoming_values += flt(d.purchase_rate) - - # Get rate for serial nos which has been transferred to other company - invalid_serial_nos = [d.name for d in all_serial_nos if d.company != self.sle.company] - for serial_no in invalid_serial_nos: + for serial_no in serial_nos: table = frappe.qb.DocType("Stock Ledger Entry") - incoming_rate = ( + stock_ledgers = ( frappe.qb.from_(table) - .select(table.incoming_rate) + .select(table.incoming_rate, table.actual_qty, table.stock_value_difference) .where( ( (table.serial_no == serial_no) @@ -51,15 +43,18 @@ class DeprecatedSerialNoValuation: ) & (table.company == self.sle.company) & (table.serial_and_batch_bundle.isnull()) - & (table.actual_qty > 0) & (table.is_cancelled == 0) ) - .orderby(table.posting_date, order=Order.desc) - .limit(1) - ).run() + .orderby(table.posting_datetime, order=Order.desc) + ).run(as_dict=1) - self.serial_no_incoming_rate[serial_no] += flt(incoming_rate[0][0]) if incoming_rate else 0 - incoming_values += self.serial_no_incoming_rate[serial_no] + 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 + ) + incoming_values += self.serial_no_incoming_rate[serial_no] return incoming_values 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 b6e4d6f40c..33f0dceba9 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 @@ -2118,7 +2118,7 @@ def is_serial_batch_no_exists(item_code, type_of_transaction, serial_no=None, ba make_serial_no(serial_no, item_code) - if batch_no and frappe.db.exists("Batch", batch_no): + if batch_no and not frappe.db.exists("Batch", batch_no): if type_of_transaction != "Inward": frappe.throw(_("Batch No {0} does not exists").format(batch_no)) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index b932c1371d..826ac03b5f 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -540,6 +540,110 @@ class TestSerialandBatchBundle(FrappeTestCase): self.assertRaises(frappe.exceptions.ValidationError, pr2.save) + def test_serial_no_valuation_for_legacy_ledgers(self): + sn_item = make_item( + "Test Serial No Valuation for Legacy Ledgers", + properties={"has_serial_no": 1, "serial_no_series": "SNN-TSNVL.-#####"}, + ).name + + serial_nos = [] + for serial_no in [f"{sn_item}-0001", f"{sn_item}-0002"]: + if not frappe.db.exists("Serial No", serial_no): + sn_doc = frappe.get_doc( + { + "doctype": "Serial No", + "serial_no": serial_no, + "item_code": sn_item, + } + ).insert(ignore_permissions=True) + serial_nos.append(serial_no) + + frappe.flags.ignore_serial_batch_bundle_validation = True + + qty_after_transaction = 0.0 + stock_value = 0.0 + for row in [{"qty": 2, "rate": 100}, {"qty": -2, "rate": 100}, {"qty": 2, "rate": 200}]: + row = frappe._dict(row) + qty_after_transaction += row.qty + stock_value += row.rate * row.qty + + doc = frappe.get_doc( + { + "doctype": "Stock Ledger Entry", + "posting_date": today(), + "posting_time": nowtime(), + "incoming_rate": row.rate if row.qty > 0 else 0, + "qty_after_transaction": qty_after_transaction, + "stock_value_difference": row.rate * row.qty, + "stock_value": stock_value, + "valuation_rate": row.rate, + "actual_qty": row.qty, + "item_code": sn_item, + "warehouse": "_Test Warehouse - _TC", + "serial_no": "\n".join(serial_nos), + "company": "_Test Company", + } + ) + doc.flags.ignore_permissions = True + doc.flags.ignore_mandatory = True + doc.flags.ignore_links = True + doc.flags.ignore_validate = True + doc.submit() + + for sn in serial_nos: + sn_doc = frappe.get_doc("Serial No", sn) + if row.qty > 0: + sn_doc.db_set("warehouse", "_Test Warehouse - _TC") + else: + sn_doc.db_set("warehouse", "") + + frappe.flags.ignore_serial_batch_bundle_validation = False + + se = make_stock_entry( + item_code=sn_item, + qty=2, + source="_Test Warehouse - _TC", + serial_no="\n".join(serial_nos), + use_serial_batch_fields=True, + do_not_submit=True, + ) + + se.save() + se.submit() + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": se.name, "is_cancelled": 0, "voucher_type": "Stock Entry"}, + "stock_value_difference", + ) + + self.assertEqual(flt(stock_value_difference, 2), 400.0 * -1) + + se = make_stock_entry( + item_code=sn_item, + qty=1, + rate=353, + target="_Test Warehouse - _TC", + ) + + serial_no = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)[0] + + se = make_stock_entry( + item_code=sn_item, + qty=1, + source="_Test Warehouse - _TC", + serial_no=serial_no, + use_serial_batch_fields=True, + ) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": se.name, "is_cancelled": 0, "voucher_type": "Stock Entry"}, + "stock_value_difference", + ) + + self.assertEqual(flt(stock_value_difference, 2), 353.0 * -1) + def get_batch_from_bundle(bundle): from erpnext.stock.serial_batch_bundle import get_batch_nos