diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 25477ccfcb1a..4a7e02299975 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -447,7 +447,11 @@ def on_submit(self): # Updating stock ledger should always be called after updating prevdoc status, # because updating reserved qty in bin depends upon updated delivered qty in SO if self.update_stock == 1: - self.make_bundle_using_old_serial_batch_fields() + for table_name in ["items", "packed_items"]: + if not self.get(table_name): + continue + + self.make_bundle_using_old_serial_batch_fields(table_name) self.update_stock_ledger() # this sequence because outstanding may get -ve diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index dc4902314930..853f0068a259 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -703,6 +703,9 @@ def set_default_income_account_for_item(obj): def get_serial_and_batch_bundle(child, parent): from erpnext.stock.serial_batch_bundle import SerialBatchCreation + if child.get("use_serial_batch_fields"): + return + if not frappe.db.get_single_value( "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward" ): diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index fdbfd10a5da8..a67fbdca62ca 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -158,7 +158,7 @@ def clean_serial_nos(self): # remove extra whitespace and store one serial no on each line row.serial_no = clean_serial_no_string(row.serial_no) - def make_bundle_using_old_serial_batch_fields(self): + def make_bundle_using_old_serial_batch_fields(self, table_name=None): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.serial_batch_bundle import SerialBatchCreation @@ -169,7 +169,9 @@ def make_bundle_using_old_serial_batch_fields(self): if frappe.flags.in_test and frappe.flags.use_serial_and_batch_fields: return - table_name = "items" + if not table_name: + table_name = "items" + if self.doctype == "Asset Capitalization": table_name = "stock_items" @@ -192,6 +194,12 @@ def make_bundle_using_old_serial_batch_fields(self): qty = row.qty type_of_transaction = "Inward" warehouse = row.warehouse + elif table_name == "packed_items": + qty = row.qty + warehouse = row.warehouse + type_of_transaction = "Outward" + if self.is_return: + type_of_transaction = "Inward" else: qty = row.stock_qty if self.doctype != "Stock Entry" else row.transfer_qty type_of_transaction = get_type_of_transaction(self, row) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 4eacbc154102..a3903a39a981 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -399,7 +399,12 @@ def on_submit(self): elif self.issue_credit_note: self.make_return_invoice() - self.make_bundle_using_old_serial_batch_fields() + for table_name in ["items", "packed_items"]: + if not self.get(table_name): + continue + + self.make_bundle_using_old_serial_batch_fields(table_name) + # Updating stock ledger should always be called after updating prevdoc status, # because updating reserved qty in bin depends upon updated delivered qty in SO self.update_stock_ledger() diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 459e7e7c4f30..293ef9f08570 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1078,6 +1078,8 @@ def test_make_sales_invoice_from_dn_with_returned_qty_duplicate_items(self): self.assertEqual(si2.items[1].qty, 1) def test_delivery_note_bundle_with_batched_item(self): + frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0) + batched_bundle = make_item("_Test Batched bundle", {"is_stock_item": 0}) batched_item = make_item( "_Test Batched Item", @@ -1099,6 +1101,8 @@ def test_delivery_note_bundle_with_batched_item(self): batch_no = get_batch_from_bundle(dn.packed_items[0].serial_and_batch_bundle) self.assertTrue(batch_no) + frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1) + def test_payment_terms_are_fetched_when_creating_sales_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, @@ -1551,6 +1555,53 @@ def test_internal_transfer_for_non_stock_item(self): self.assertEqual(so.items[0].rate, rate) self.assertEqual(dn.items[0].rate, so.items[0].rate) + def test_use_serial_batch_fields_for_packed_items(self): + bundle_item = make_item("Test _Packed Product Bundle Item ", {"is_stock_item": 0}) + serial_item = make_item( + "Test _Packed Serial Item ", + {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-TESTSERIAL-.#####"}, + ) + batch_item = make_item( + "Test _Packed Batch Item ", + { + "is_stock_item": 1, + "has_batch_no": 1, + "batch_no_series": "BATCH-TESTSERIAL-.#####", + "create_new_batch": 1, + }, + ) + make_product_bundle(parent=bundle_item.name, items=[serial_item.name, batch_item.name]) + + item_details = {} + for item in [serial_item, batch_item]: + se = make_stock_entry( + item_code=item.name, target="_Test Warehouse - _TC", qty=5, basic_rate=100 + ) + item_details[item.name] = se.items[0].serial_and_batch_bundle + + dn = create_delivery_note(item_code=bundle_item.name, qty=1, do_not_submit=True) + serial_no = "" + for row in dn.packed_items: + row.use_serial_batch_fields = 1 + + if row.item_code == serial_item.name: + serial_and_batch_bundle = item_details[serial_item.name] + row.serial_no = get_serial_nos_from_bundle(serial_and_batch_bundle)[3] + serial_no = row.serial_no + else: + serial_and_batch_bundle = item_details[batch_item.name] + row.batch_no = get_batch_from_bundle(serial_and_batch_bundle) + + dn.submit() + dn.load_from_db() + + for row in dn.packed_items: + self.assertTrue(row.serial_no or row.batch_no) + self.assertTrue(row.serial_and_batch_bundle) + + if row.serial_no: + self.assertEqual(row.serial_no, serial_no) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index c115e33e1716..c5fed0dcd89c 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -227,6 +227,9 @@ def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data bin = get_packed_item_bin_qty(packing_item.item_code, pi_row.warehouse) pi_row.actual_qty = flt(bin.get("actual_qty")) pi_row.projected_qty = flt(bin.get("projected_qty")) + pi_row.use_serial_batch_fields = frappe.db.get_single_value( + "Stock Settings", "use_serial_batch_fields" + ) def update_packed_item_price_data(pi_row, item_data, doc): 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 eb4df29db829..b6e4d6f40c6a 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 @@ -257,9 +257,9 @@ def set_incoming_rate_for_outward_transaction( if sn_obj.batch_avg_rate.get(d.batch_no): d.incoming_rate = abs(sn_obj.batch_avg_rate.get(d.batch_no)) - available_qty = flt(sn_obj.available_qty.get(d.batch_no)) + available_qty = flt(sn_obj.available_qty.get(d.batch_no), d.precision("qty")) if self.docstatus == 1: - available_qty += flt(d.qty) + available_qty += flt(d.qty, d.precision("qty")) if not allow_negative_stock: self.validate_negative_batch(d.batch_no, available_qty) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 832894b010ed..399e6985548c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -1033,7 +1033,7 @@ def make_serial_and_batch_bundle_for_outward(self): already_picked_serial_nos = [] for row in self.items: - if row.use_serial_batch_fields and (row.serial_no or row.batch_no): + if row.use_serial_batch_fields: continue if not row.s_warehouse: