From 7ca72423b962a80fe7bf3660ed8f9ec4b4a2a60a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 7 Mar 2024 14:36:59 +0530 Subject: [PATCH] fix: use serial/batch field for rejected items (backport #40327) (#40329) fix: use serial/batch field for rejected items (#40327) (cherry picked from commit 01856a6e9d8c849242a897568f8f55afd373311a) Co-authored-by: rohitwaghchaure --- .../purchase_invoice/test_purchase_invoice.py | 91 ++++++++++++++++++- erpnext/controllers/stock_controller.py | 53 ++++++----- .../controllers/subcontracting_controller.py | 1 + .../purchase_receipt/test_purchase_receipt.py | 82 +++++++++++++++++ erpnext/stock/stock_ledger.py | 3 + .../test_subcontracting_receipt.py | 80 ++++++++++++++++ .../subcontracting_receipt_item.json | 5 +- 7 files changed, 289 insertions(+), 26 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index e0fbef4a9b4..8b1e03c7426 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2098,6 +2098,92 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): return_pi.submit() self.assertEqual(return_pi.docstatus, 1) + def test_purchase_invoice_with_use_serial_batch_field_for_rejected_qty(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + batch_item = make_item( + "_Test Purchase Invoice Batch Item For Rejected Qty", + properties={"has_batch_no": 1, "create_new_batch": 1, "is_stock_item": 1}, + ).name + + serial_item = make_item( + "_Test Purchase Invoice Serial Item for Rejected Qty", + properties={"has_serial_no": 1, "is_stock_item": 1}, + ).name + + rej_warehouse = create_warehouse("_Test Purchase INV Warehouse For Rejected Qty") + + batch_no = "BATCH-PI-BNU-TPRBI-0001" + serial_nos = ["SNU-PI-TPRSI-0001", "SNU-PI-TPRSI-0002", "SNU-PI-TPRSI-0003"] + + if not frappe.db.exists("Batch", batch_no): + frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_no, + "item": batch_item, + } + ).insert() + + for serial_no in serial_nos: + if not frappe.db.exists("Serial No", serial_no): + frappe.get_doc( + { + "doctype": "Serial No", + "item_code": serial_item, + "serial_no": serial_no, + } + ).insert() + + pi = make_purchase_invoice( + item_code=batch_item, + received_qty=10, + qty=8, + rejected_qty=2, + update_stock=1, + rejected_warehouse=rej_warehouse, + use_serial_batch_fields=1, + batch_no=batch_no, + rate=100, + do_not_submit=1, + ) + + pi.append( + "items", + { + "item_code": serial_item, + "qty": 2, + "rate": 100, + "base_rate": 100, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "rejected_qty": 1, + "warehouse": pi.items[0].warehouse, + "rejected_warehouse": rej_warehouse, + "use_serial_batch_fields": 1, + "serial_no": "\n".join(serial_nos[:2]), + "rejected_serial_no": serial_nos[2], + }, + ) + + pi.save() + pi.submit() + + pi.reload() + + for row in pi.items: + self.assertTrue(row.serial_and_batch_bundle) + self.assertTrue(row.rejected_serial_and_batch_bundle) + + if row.item_code == batch_item: + self.assertEqual(row.batch_no, batch_no) + else: + self.assertEqual(row.serial_no, "\n".join(serial_nos[:2])) + self.assertEqual(row.rejected_serial_no, serial_nos[2]) + def set_advance_flag(company, flag, default_account): frappe.db.set_value( @@ -2205,7 +2291,7 @@ def make_purchase_invoice(**args): pi.cost_center = args.parent_cost_center bundle_id = None - if args.get("batch_no") or args.get("serial_no"): + if not args.use_serial_batch_fields and ((args.get("batch_no") or args.get("serial_no"))): batches = {} qty = args.qty or 5 item_code = args.item or args.item_code or "_Test Item" @@ -2252,6 +2338,9 @@ def make_purchase_invoice(**args): "rejected_warehouse": args.rejected_warehouse or "", "asset_location": args.location or "", "allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0, + "use_serial_batch_fields": args.get("use_serial_batch_fields") or 0, + "batch_no": args.get("batch_no") if args.get("use_serial_batch_fields") else "", + "serial_no": args.get("serial_no") if args.get("use_serial_batch_fields") else "", }, ) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 63556cddca4..688600774cc 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -159,9 +159,6 @@ class StockController(AccountsController): row.serial_no = clean_serial_no_string(row.serial_no) 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 - if self.get("_action") == "update_after_submit": return @@ -199,31 +196,21 @@ class StockController(AccountsController): "voucher_detail_no": row.name, "company": self.company, "is_rejected": 1 if row.get("rejected_warehouse") else 0, - "serial_nos": get_serial_nos(row.serial_no) if row.serial_no else None, - "batch_no": row.batch_no, "use_serial_batch_fields": row.use_serial_batch_fields, "do_not_submit": True, } - self.update_bundle_details(bundle_details, table_name, row) - sn_doc = SerialBatchCreation(bundle_details).make_serial_and_batch_bundle() + if row.qty: + self.update_bundle_details(bundle_details, table_name, row) + self.create_serial_batch_bundle(bundle_details, row) - if sn_doc.is_rejected: - row.rejected_serial_and_batch_bundle = sn_doc.name - row.db_set( - { - "rejected_serial_and_batch_bundle": sn_doc.name, - } - ) - else: - row.serial_and_batch_bundle = sn_doc.name - row.db_set( - { - "serial_and_batch_bundle": sn_doc.name, - } - ) + if row.get("rejected_qty"): + self.update_bundle_details(bundle_details, table_name, row, is_rejected=True) + self.create_serial_batch_bundle(bundle_details, row) + + def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - def update_bundle_details(self, bundle_details, table_name, row): # Since qty field is different for different doctypes qty = row.get("qty") warehouse = row.get("warehouse") @@ -242,15 +229,37 @@ class StockController(AccountsController): qty = row.transfer_qty warehouse = row.s_warehouse or row.t_warehouse + serial_nos = row.serial_no + if is_rejected: + serial_nos = row.get("rejected_serial_no") + type_of_transaction = "Inward" if not self.is_return else "Outward" + qty = row.get("rejected_qty") + warehouse = row.get("rejected_warehouse") + bundle_details.update( { "qty": qty, + "is_rejected": is_rejected, "type_of_transaction": type_of_transaction, "warehouse": warehouse, "batches": frappe._dict({row.batch_no: qty}) if row.batch_no else None, + "serial_nos": get_serial_nos(serial_nos) if serial_nos else None, + "batch_no": row.batch_no, } ) + def create_serial_batch_bundle(self, bundle_details, row): + from erpnext.stock.serial_batch_bundle import SerialBatchCreation + + sn_doc = SerialBatchCreation(bundle_details).make_serial_and_batch_bundle() + + field = "serial_and_batch_bundle" + if bundle_details.get("is_rejected"): + field = "rejected_serial_and_batch_bundle" + + row.set(field, sn_doc.name) + row.db_set({field: sn_doc.name}) + def validate_serial_nos_and_batches_with_bundle(self, row): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index eac35b0d39f..e66fe8bcdec 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -859,6 +859,7 @@ class SubcontractingController(StockController): item, { "warehouse": item.rejected_warehouse, + "serial_and_batch_bundle": item.get("rejected_serial_and_batch_bundle"), "actual_qty": flt(item.rejected_qty) * flt(item.conversion_factor), "incoming_rate": 0.0, }, diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 053cb1d98bd..8c3c1f750ab 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2440,6 +2440,88 @@ class TestPurchaseReceipt(FrappeTestCase): pr.reload() self.assertEqual(pr.per_billed, 100) + def test_purchase_receipt_with_use_serial_batch_field_for_rejected_qty(self): + batch_item = make_item( + "_Test Purchase Receipt Batch Item For Rejected Qty", + properties={"has_batch_no": 1, "create_new_batch": 1, "is_stock_item": 1}, + ).name + + serial_item = make_item( + "_Test Purchase Receipt Serial Item for Rejected Qty", + properties={"has_serial_no": 1, "is_stock_item": 1}, + ).name + + rej_warehouse = create_warehouse("_Test Purchase Warehouse For Rejected Qty") + + batch_no = "BATCH-BNU-TPRBI-0001" + serial_nos = ["SNU-TPRSI-0001", "SNU-TPRSI-0002", "SNU-TPRSI-0003"] + + if not frappe.db.exists("Batch", batch_no): + frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_no, + "item": batch_item, + } + ).insert() + + for serial_no in serial_nos: + if not frappe.db.exists("Serial No", serial_no): + frappe.get_doc( + { + "doctype": "Serial No", + "item_code": serial_item, + "serial_no": serial_no, + } + ).insert() + + pr = make_purchase_receipt( + item_code=batch_item, + received_qty=10, + qty=8, + rejected_qty=2, + rejected_warehouse=rej_warehouse, + use_serial_batch_fields=1, + batch_no=batch_no, + rate=100, + do_not_submit=1, + ) + + pr.append( + "items", + { + "item_code": serial_item, + "qty": 2, + "rate": 100, + "base_rate": 100, + "item_name": serial_item, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "rejected_qty": 1, + "warehouse": pr.items[0].warehouse, + "rejected_warehouse": rej_warehouse, + "use_serial_batch_fields": 1, + "serial_no": "\n".join(serial_nos[:2]), + "rejected_serial_no": serial_nos[2], + }, + ) + + pr.save() + pr.submit() + + pr.reload() + + for row in pr.items: + self.assertTrue(row.serial_and_batch_bundle) + self.assertTrue(row.rejected_serial_and_batch_bundle) + + if row.item_code == batch_item: + self.assertEqual(row.batch_no, batch_no) + else: + self.assertEqual(row.serial_no, "\n".join(serial_nos[:2])) + self.assertEqual(row.rejected_serial_no, serial_nos[2]) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 90c41f8e3f2..22113d294da 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -893,6 +893,9 @@ class update_entries_after(object): query.run() def calculate_valuation_for_serial_batch_bundle(self, sle): + if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle): + return + doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle) doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 4738a700408..3e74ac91f4b 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -1132,6 +1132,86 @@ class TestSubcontractingReceipt(FrappeTestCase): scr.reload() self.assertTrue(scr.items[0].serial_and_batch_bundle) + def test_use_serial_batch_fields_for_subcontracting_receipt_with_rejected_qty(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + fg_item = make_item( + "Test Subcontracted Item With Batch No for Rejected Qty", + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BATCH-REJ-BNGS-.####", + "is_sub_contracted_item": 1, + }, + ).name + + make_item( + "Test Subcontracted Item With Batch No Service Item 2", + properties={"is_stock_item": 0}, + ) + + make_bom( + item=fg_item, + raw_materials=[ + make_item( + "Test Subcontracted Item With Batch No RM Item 2", + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BATCH-REJ-RM-BNGS-.####", + }, + ).name + ], + ) + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Test Subcontracted Item With Batch No Service Item 2", + "qty": 10, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + batch_no = "BATCH-REJ-BNGS-0001" + if not frappe.db.exists("Batch", batch_no): + frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_no, + "item": fg_item, + } + ).insert() + + rej_warehouse = create_warehouse("_Test Subcontract Warehouse For Rejected Qty") + + scr = make_subcontracting_receipt(sco.name) + self.assertFalse(scr.items[0].serial_and_batch_bundle) + scr.items[0].use_serial_batch_fields = 1 + scr.items[0].batch_no = batch_no + scr.items[0].received_qty = 10 + scr.items[0].rejected_qty = 2 + scr.items[0].qty = 8 + scr.items[0].rejected_warehouse = rej_warehouse + + scr.save() + scr.submit() + scr.reload() + self.assertTrue(scr.items[0].serial_and_batch_bundle) + self.assertTrue(scr.items[0].rejected_serial_and_batch_bundle) + def make_return_subcontracting_receipt(**args): args = frappe._dict(args) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index f9e0a0b591c..ca14f09c9c5 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -335,8 +335,7 @@ "fieldtype": "Small Text", "label": "Rejected Serial No", "no_copy": 1, - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "subcontracting_order_item", @@ -569,7 +568,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2024-02-04 16:23:30.374865", + "modified": "2024-03-07 11:43:38.954262", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item",