From 2eac2ef69fae71d570eb7dceb6edb6e312fbef36 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 28 Mar 2024 14:55:31 +0530 Subject: [PATCH] fix: sales / prchase return validation issue (cherry picked from commit 59dc4a96e18357ee52f0f25137e9c824971cc029) (cherry picked from commit 7a21997701b37ad83540289c22e518e210ab72b9) --- .../delivery_note/test_delivery_note.py | 92 +++++++++++++++++++ .../serial_and_batch_bundle.py | 60 ++++++++++-- 2 files changed, 142 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 24544070d3b..c4852d2cf15 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -461,6 +461,98 @@ class TestDeliveryNote(FrappeTestCase): self.assertEqual(return_dn.items[0].incoming_rate, 150) + def test_sales_return_against_serial_batch_bundle(self): + frappe.db.set_single_value( + "Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle", 1 + ) + + batch_item = make_item( + "Test Sales Return Against Batch Item", + properties={ + "has_batch_no": 1, + "is_stock_item": 1, + "create_new_batch": 1, + "batch_number_series": "BATCH-TSRABII.#####", + }, + ).name + + serial_item = make_item( + "Test Sales Return Against Serial NO Item", + properties={ + "has_serial_no": 1, + "is_stock_item": 1, + "serial_no_series": "SN-TSRABII.#####", + }, + ).name + + make_stock_entry(item_code=batch_item, target="_Test Warehouse - _TC", qty=5, basic_rate=100) + make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=5, basic_rate=100) + + dn = create_delivery_note( + item_code=batch_item, + qty=5, + rate=500, + warehouse="_Test Warehouse - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + use_serial_batch_fields=0, + do_not_submit=1, + ) + + dn.append( + "items", + { + "item_code": serial_item, + "qty": 5, + "rate": 500, + "warehouse": "_Test Warehouse - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "Main - _TC", + "use_serial_batch_fields": 0, + }, + ) + + dn.save() + for row in dn.items: + self.assertFalse(row.use_serial_batch_fields) + + dn.submit() + dn.reload() + for row in dn.items: + self.assertTrue(row.serial_and_batch_bundle) + self.assertFalse(row.use_serial_batch_fields) + self.assertFalse(row.serial_no) + self.assertFalse(row.batch_no) + + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + return_dn = make_return_doc(dn.doctype, dn.name) + for row in return_dn.items: + row.qty = -2 + row.use_serial_batch_fields = 0 + return_dn.save().submit() + + for row in return_dn.items: + total_qty = frappe.db.get_value( + "Serial and Batch Bundle", row.serial_and_batch_bundle, "total_qty" + ) + + self.assertEqual(total_qty, 2) + + doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle) + if doc.has_serial_no: + self.assertEqual(len(doc.entries), 2) + + for entry in doc.entries: + if doc.has_batch_no: + self.assertEqual(entry.qty, 2) + else: + self.assertEqual(entry.qty, 1) + + frappe.db.set_single_value( + "Stock Settings", "do_not_update_serial_batch_on_creation_of_auto_bundle", 0 + ) + def test_return_single_item_from_bundled_items(self): company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") 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 58971e8f19d..11018c27f67 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 @@ -384,6 +384,9 @@ class SerialandBatchBundle(Document): if self.docstatus == 0: self.set_incoming_rate(save=True, row=row) + if self.docstatus == 0 and parent.get("is_return") and parent.is_new(): + self.reset_qty(row, qty_field=qty_field) + self.calculate_qty_and_amount(save=True) self.validate_quantity(row, qty_field=qty_field) self.set_warranty_expiry_date() @@ -417,7 +420,11 @@ class SerialandBatchBundle(Document): if not (self.voucher_type and self.voucher_no): return - if self.voucher_no and not frappe.db.exists(self.voucher_type, self.voucher_no): + if ( + self.docstatus == 1 + and self.voucher_no + and not frappe.db.exists(self.voucher_type, self.voucher_no) + ): self.throw_error_message(f"The {self.voucher_type} # {self.voucher_no} does not exist") if self.flags.ignore_voucher_validation: @@ -481,24 +488,57 @@ class SerialandBatchBundle(Document): frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransactionError) + def reset_qty(self, row, qty_field=None): + qty_field = self.get_qty_field(row, qty_field=qty_field) + qty = abs(row.get(qty_field)) + + idx = None + while qty > 0: + for d in self.entries: + row_qty = abs(d.qty) + if row_qty >= qty: + d.db_set("qty", qty if self.type_of_transaction == "Inward" else qty * -1) + qty = 0 + idx = d.idx + break + else: + qty -= row_qty + idx = d.idx + + if idx and len(self.entries) > idx: + remove_rows = [] + for d in self.entries: + if d.idx > idx: + remove_rows.append(d) + + for d in remove_rows: + self.entries.remove(d) + + self.flags.ignore_links = True + self.save() + def validate_quantity(self, row, qty_field=None): + qty_field = self.get_qty_field(row, qty_field=qty_field) + qty = row.get(qty_field) + if qty_field == "qty" and row.get("stock_qty"): + qty = row.get("stock_qty") + + precision = row.precision + if abs(abs(flt(self.total_qty, precision)) - abs(flt(qty, precision))) > 0.01: + self.throw_error_message( + f"Total quantity {abs(flt(self.total_qty))} in the Serial and Batch Bundle {bold(self.name)} does not match with the quantity {abs(flt(row.get(qty_field)))} for the Item {bold(self.item_code)} in the {self.voucher_type} # {self.voucher_no}" + ) + + def get_qty_field(self, row, qty_field=None) -> str: if not qty_field: qty_field = "qty" - precision = row.precision if row.get("doctype") == "Subcontracting Receipt Supplied Item": qty_field = "consumed_qty" elif row.get("doctype") == "Stock Entry Detail": qty_field = "transfer_qty" - qty = row.get(qty_field) - if qty_field == "qty" and row.get("stock_qty"): - qty = row.get("stock_qty") - - if abs(abs(flt(self.total_qty, precision)) - abs(flt(qty, precision))) > 0.01: - self.throw_error_message( - f"Total quantity {abs(flt(self.total_qty))} in the Serial and Batch Bundle {bold(self.name)} does not match with the quantity {abs(flt(row.get(qty_field)))} for the Item {bold(self.item_code)} in the {self.voucher_type} # {self.voucher_no}" - ) + return qty_field def set_is_outward(self): for row in self.entries: