From 094ecc1f62a97d6580a72c9a9fdf176a6d7dc959 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 12 Feb 2024 17:36:14 +0530 Subject: [PATCH 1/2] fix: validate duplicate SBB --- erpnext/controllers/stock_controller.py | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 8ffbaa1015..dd27200404 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -46,6 +46,9 @@ class BatchExpiredError(frappe.ValidationError): class StockController(AccountsController): def validate(self): super(StockController, self).validate() + + if self.docstatus == 0: + self.validate_duplicate_serial_and_batch_bundle() if not self.get("is_return"): self.validate_inspection() self.validate_serialized_batch() @@ -55,6 +58,32 @@ class StockController(AccountsController): self.validate_internal_transfer() self.validate_putaway_capacity() + def validate_duplicate_serial_and_batch_bundle(self): + if sbb_list := [ + item.get("serial_and_batch_bundle") + for item in self.items + if item.get("serial_and_batch_bundle") + ]: + SLE = frappe.qb.DocType("Stock Ledger Entry") + data = ( + frappe.qb.from_(SLE) + .select(SLE.voucher_type, SLE.voucher_no, SLE.serial_and_batch_bundle) + .where( + (SLE.docstatus == 1) + & (SLE.serial_and_batch_bundle.notnull()) + & (SLE.serial_and_batch_bundle.isin(sbb_list)) + ) + .limit(1) + ).run(as_dict=True) + + if data: + data = data[0] + frappe.throw( + _("Serial and Batch Bundle {0} is already used in {1} {2}.").format( + frappe.bold(data.serial_and_batch_bundle), data.voucher_type, data.voucher_no + ) + ) + def make_gl_entries(self, gl_entries=None, from_repost=False): if self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) From 55e66db315518bda82e4eb77d8fc74f5b2c7d14d Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 12 Feb 2024 18:16:29 +0530 Subject: [PATCH 2/2] test: duplicate SBB --- .../test_serial_and_batch_bundle.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index f430943708..88b262a8c6 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -4,7 +4,7 @@ import json import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, add_to_date, flt, nowdate, nowtime, today from erpnext.stock.doctype.item.test_item import make_item @@ -521,6 +521,24 @@ class TestSerialandBatchBundle(FrappeTestCase): make_serial_nos(item_code, serial_nos) self.assertTrue(frappe.db.exists("Serial No", serial_no_id)) + @change_settings("Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1}) + def test_duplicate_serial_and_batch_bundle(self): + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + + item_code = make_item(properties={"is_stock_item": 1, "has_serial_no": 1}).name + + serial_no = f"{item_code}-001" + serial_nos = [{"serial_no": serial_no, "qty": 1}] + make_serial_nos(item_code, serial_nos) + + pr1 = make_purchase_receipt(item=item_code, qty=1, rate=500, serial_no=[serial_no]) + pr2 = make_purchase_receipt(item=item_code, qty=1, rate=500, do_not_save=True) + + pr1.reload() + pr2.items[0].serial_and_batch_bundle = pr1.items[0].serial_and_batch_bundle + + self.assertRaises(frappe.exceptions.ValidationError, pr2.save) + def get_batch_from_bundle(bundle): from erpnext.stock.serial_batch_bundle import get_batch_nos