fix: use serial/batch field for rejected items (#40327)
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 72c50ed..2ef0275 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -2108,6 +2108,92 @@
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(
@@ -2215,7 +2301,7 @@
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 if args.qty is not None else 5
item_code = args.item or args.item_code or "_Test Item"
@@ -2262,6 +2348,9 @@
"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 2f05ec3..a3fbdda 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -159,9 +159,6 @@
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 @@
"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):
+ 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
+
# Since qty field is different for different doctypes
qty = row.get("qty")
warehouse = row.get("warehouse")
@@ -242,15 +229,37 @@
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 eac35b0..e66fe8b 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -859,6 +859,7 @@
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 aa17ab4..fa2c21f 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -2477,6 +2477,88 @@
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 2b62baf..60053aa 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -893,6 +893,9 @@
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 4738a70..3e74ac9 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 @@
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 f9e0a0b..ca14f09 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",