diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 434e00194d6e712893a28e6666f98b12cbcbefb8..069b61be8d0b2cb321097d2561bbc22046cf154c 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -472,6 +472,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 a27497d9197ef1b17bac12334b86dd7a3f2cea38..c526f3eaf025927aaeac36b1e4fa3eee27c6b7ac 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,25 +488,58 @@ class SerialandBatchBundle(Document): frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransactionError) - def validate_quantity(self, row, qty_field=None): - if not qty_field: - qty_field = "qty" + 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 - 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" + 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" + + if row.get("doctype") == "Subcontracting Receipt Supplied Item": + qty_field = "consumed_qty" + elif row.get("doctype") == "Stock Entry Detail": + qty_field = "transfer_qty" + + return qty_field + def set_is_outward(self): for row in self.entries: if self.type_of_transaction == "Outward" and row.qty > 0: