diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 6d05ec478fd..b10898abbdc 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -432,7 +432,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe update_stock = cint(me.frm.doc.update_stock); show_batch_dialog = update_stock; - } else if((this.frm.doc.doctype === 'Purchase Receipt' && me.frm.doc.is_return) || + } else if((this.frm.doc.doctype === 'Purchase Receipt') || this.frm.doc.doctype === 'Delivery Note') { show_batch_dialog = 1; } @@ -538,7 +538,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }); }, () => { - if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) { + if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog && !frappe.flags.dialog_set) { var d = locals[cdt][cdn]; $.each(r.message, function(k, v) { if(!d[k]) d[k] = v; @@ -548,12 +548,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe d.batch_no = undefined; } + frappe.flags.dialog_set = true; erpnext.show_serial_batch_selector(me.frm, d, (item) => { me.frm.script_manager.trigger('qty', item.doctype, item.name); if (!me.frm.doc.set_warehouse) me.frm.script_manager.trigger('warehouse', item.doctype, item.name); me.apply_price_list(item, true); }, undefined, !frappe.flags.hide_serial_batch_dialog); + } else { + frappe.flags.dialog_set = false; } }, () => me.conversion_factor(doc, cdt, cdn, true), @@ -2287,6 +2290,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }; erpnext.show_serial_batch_selector = function (frm, item_row, callback, on_close, show_dialog) { + debugger let warehouse, receiving_stock, existing_stock; if (frm.doc.is_return) { if (["Purchase Receipt", "Purchase Invoice"].includes(frm.doc.doctype)) { diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 8c7b2f2bb02..0e0ef338377 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -29,10 +29,6 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { primary_action: () => this.update_ledgers() }); - if (this.item?.outward) { - this.prepare_for_auto_fetch(); - } - this.dialog.show(); } @@ -76,6 +72,13 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { fieldname: 'scan_batch_no', label: __('Scan Batch No'), options: 'Batch', + get_query: () => { + return { + filters: { + 'item': this.item.item_code + } + }; + }, onchange: () => this.update_serial_batch_no() }); } @@ -97,7 +100,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { } if (this.item?.outward) { - fields = [...fields, ...this.get_filter_fields()]; + fields = [...this.get_filter_fields(), ...fields]; } fields.push({ @@ -126,6 +129,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { fieldname: 'qty', default: this.item.qty || 0, label: __('Qty to Fetch'), + onchange: () => this.get_auto_data() }, { fieldtype: 'Column Break', @@ -135,16 +139,11 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { options: ['FIFO', 'LIFO', 'Expiry'], default: 'FIFO', fieldname: 'based_on', - label: __('Fetch Based On') + label: __('Fetch Based On'), + onchange: () => this.get_auto_data() }, { - fieldtype: 'Column Break', - }, - { - fieldtype: 'Button', - fieldname: 'get_auto_data', - label: __('Fetch {0}', - [this.item?.has_serial_no ? 'Serial Nos' : 'Batch Nos']), + fieldtype: 'Section Break', }, ] @@ -177,6 +176,13 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { fieldname: 'batch_no', label: __('Batch No'), in_list_view: 1, + get_query: () => { + return { + filters: { + 'item': this.item.item_code + } + }; + }, } ] @@ -202,12 +208,6 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { return fields; } - prepare_for_auto_fetch() { - this.dialog.fields_dict.get_auto_data.$input.on('click', () => { - this.get_auto_data(); - }); - } - get_auto_data() { const { qty, based_on } = this.dialog.get_values(); @@ -215,6 +215,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { frappe.throw(__('Please enter Qty to Fetch')); } + if (!based_on) { + based_on = 'FIFO'; + } + frappe.call({ method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_auto_data', args: { diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 1e1d8fdeca9..9e15015aa56 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -17,13 +17,9 @@ class DeprecatedSerialNoValuation: stock_value_change = 0 if actual_qty < 0: - # In case of delivery/stock issue, get average purchase rate - # of serial nos of current entry if not self.sle.is_cancelled: outgoing_value = self.get_incoming_value_for_serial_nos(serial_nos) stock_value_change = -1 * outgoing_value - else: - stock_value_change = actual_qty * self.sle.outgoing_rate self.stock_value_change += stock_value_change diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index b613f20d450..18d8a72e158 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -139,7 +139,7 @@ { "collapsible": 1, "fieldname": "quantity_and_rate_section", - "fieldtype": "Section Break", + "fieldtype": "Tab Break", "label": "Quantity and Rate" }, { @@ -243,7 +243,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-03-24 13:39:17.843812", + "modified": "2023-04-03 16:22:30.767805", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", 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 9f26b40aa7c..4fe59bd0ec3 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 @@ -154,7 +154,10 @@ class SerialandBatchBundle(Document): 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)) + flt(d.qty) + available_qty = flt(sn_obj.available_qty.get(d.batch_no)) + if self.docstatus == 1: + available_qty += flt(d.qty) + self.validate_negative_batch(d.batch_no, available_qty) d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) @@ -553,6 +556,38 @@ class SerialandBatchBundle(Document): def on_submit(self): self.validate_serial_nos_inventory() + def validate_serial_and_batch_inventory(self): + self.check_future_entries_exists() + self.validate_batch_inventory() + + def validate_batch_inventory(self): + if not self.has_batch_no: + return + + batches = [d.batch_no for d in self.entries if d.batch_no] + if not batches: + return + + available_batches = get_auto_batch_nos( + frappe._dict( + { + "item_code": self.item_code, + "warehouse": self.warehouse, + "batch_no": batches, + } + ) + ) + + if not available_batches: + return + + available_batches = get_availabel_batches_qty(available_batches) + for batch_no in batches: + if batch_no not in available_batches or available_batches[batch_no] < 0: + self.throw_error_message( + f"Batch {bold(batch_no)} is not available in the selected warehouse {self.warehouse}" + ) + def on_cancel(self): self.validate_voucher_no_docstatus() @@ -599,6 +634,7 @@ def get_serial_batch_ledgers(item_code, docstatus=None, voucher_no=None, name=No "`tabSerial and Batch Entry`.`serial_no`", ], filters=filters, + order_by="`tabSerial and Batch Entry`.`idx`", ) @@ -762,6 +798,14 @@ def get_auto_data(**kwargs): return get_auto_batch_nos(kwargs) +def get_availabel_batches_qty(available_batches): + available_batches_qty = defaultdict(float) + for batch in available_batches: + available_batches_qty[batch.batch_no] += batch.qty + + return available_batches_qty + + def get_available_serial_nos(kwargs): fields = ["name as serial_no", "warehouse"] if kwargs.has_batch_no: @@ -778,6 +822,7 @@ def get_available_serial_nos(kwargs): if kwargs.warehouse: filters["warehouse"] = kwargs.warehouse + # Since SLEs are not present against POS invoices, need to ignore serial nos present in the POS invoice ignore_serial_nos = get_reserved_serial_nos_for_pos(kwargs) # To ignore serial nos in the same record for the draft state @@ -792,6 +837,13 @@ def get_available_serial_nos(kwargs): elif ignore_serial_nos: filters["name"] = ("not in", ignore_serial_nos) + if kwargs.get("batches"): + batches = get_non_expired_batches(kwargs.get("batches")) + if not batches: + return [] + + filters["batch_no"] = ("in", batches) + return frappe.get_all( "Serial No", fields=fields, @@ -801,6 +853,23 @@ def get_available_serial_nos(kwargs): ) +def get_non_expired_batches(batches): + filters = {} + if isinstance(batches, list): + filters["name"] = ("in", batches) + else: + filters["name"] = batches + + data = frappe.get_all( + "Batch", + filters=filters, + or_filters=[["expiry_date", ">=", today()], ["expiry_date", "is", "not set"]], + fields=["name"], + ) + + return [d.name for d in data] if data else [] + + def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index 4a0abb6dd0c..5a5c403a43a 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -6,7 +6,9 @@ import frappe +from frappe import _, _dict from frappe.tests.utils import FrappeTestCase +from frappe.utils import today from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.item.test_item import make_item @@ -49,26 +51,22 @@ class TestSerialNo(FrappeTestCase): def test_inter_company_transfer(self): se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") - serial_nos = get_serial_nos(se.get("items")[0].serial_no) + serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle) dn = create_delivery_note( - item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0] + item_code="_Test Serialized Item With Series", qty=1, serial_no=[serial_nos[0]] ) serial_no = frappe.get_doc("Serial No", serial_nos[0]) # check Serial No details after delivery - self.assertEqual(serial_no.status, "Delivered") self.assertEqual(serial_no.warehouse, None) - self.assertEqual(serial_no.company, "_Test Company") - self.assertEqual(serial_no.delivery_document_type, "Delivery Note") - self.assertEqual(serial_no.delivery_document_no, dn.name) wh = create_warehouse("_Test Warehouse", company="_Test Company 1") pr = make_purchase_receipt( item_code="_Test Serialized Item With Series", qty=1, - serial_no=serial_nos[0], + serial_no=[serial_nos[0]], company="_Test Company 1", warehouse=wh, ) @@ -76,11 +74,7 @@ class TestSerialNo(FrappeTestCase): serial_no.reload() # check Serial No details after purchase in second company - self.assertEqual(serial_no.status, "Active") self.assertEqual(serial_no.warehouse, wh) - self.assertEqual(serial_no.company, "_Test Company 1") - self.assertEqual(serial_no.purchase_document_type, "Purchase Receipt") - self.assertEqual(serial_no.purchase_document_no, pr.name) def test_inter_company_transfer_intermediate_cancellation(self): """ @@ -89,25 +83,19 @@ class TestSerialNo(FrappeTestCase): Try to cancel intermediate receipts/deliveries to test if it is blocked. """ se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") - serial_nos = get_serial_nos(se.get("items")[0].serial_no) + serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle) sn_doc = frappe.get_doc("Serial No", serial_nos[0]) # check Serial No details after purchase in first company - self.assertEqual(sn_doc.status, "Active") - self.assertEqual(sn_doc.company, "_Test Company") self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC") - self.assertEqual(sn_doc.purchase_document_no, se.name) dn = create_delivery_note( - item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0] + item_code="_Test Serialized Item With Series", qty=1, serial_no=[serial_nos[0]] ) sn_doc.reload() # check Serial No details after delivery from **first** company - self.assertEqual(sn_doc.status, "Delivered") - self.assertEqual(sn_doc.company, "_Test Company") self.assertEqual(sn_doc.warehouse, None) - self.assertEqual(sn_doc.delivery_document_no, dn.name) # try cancelling the first Serial No Receipt, even though it is delivered # block cancellation is Serial No is out of the warehouse @@ -118,7 +106,7 @@ class TestSerialNo(FrappeTestCase): pr = make_purchase_receipt( item_code="_Test Serialized Item With Series", qty=1, - serial_no=serial_nos[0], + serial_no=[serial_nos[0]], company="_Test Company 1", warehouse=wh, ) @@ -133,17 +121,14 @@ class TestSerialNo(FrappeTestCase): dn_2 = create_delivery_note( item_code="_Test Serialized Item With Series", qty=1, - serial_no=serial_nos[0], + serial_no=[serial_nos[0]], company="_Test Company 1", warehouse=wh, ) sn_doc.reload() # check Serial No details after delivery from **second** company - self.assertEqual(sn_doc.status, "Delivered") - self.assertEqual(sn_doc.company, "_Test Company 1") self.assertEqual(sn_doc.warehouse, None) - self.assertEqual(sn_doc.delivery_document_no, dn_2.name) # cannot cancel any intermediate document before last Delivery Note self.assertRaises(frappe.ValidationError, se.cancel) @@ -158,12 +143,12 @@ class TestSerialNo(FrappeTestCase): """ # Receipt in **first** company se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") - serial_nos = get_serial_nos(se.get("items")[0].serial_no) + serial_nos = get_serial_nos_from_bundle(se.get("items")[0].serial_and_batch_bundle) sn_doc = frappe.get_doc("Serial No", serial_nos[0]) # Delivery from first company dn = create_delivery_note( - item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0] + item_code="_Test Serialized Item With Series", qty=1, serial_no=[serial_nos[0]] ) # Receipt in **second** company @@ -171,7 +156,7 @@ class TestSerialNo(FrappeTestCase): pr = make_purchase_receipt( item_code="_Test Serialized Item With Series", qty=1, - serial_no=serial_nos[0], + serial_no=[serial_nos[0]], company="_Test Company 1", warehouse=wh, ) @@ -180,55 +165,29 @@ class TestSerialNo(FrappeTestCase): dn_2 = create_delivery_note( item_code="_Test Serialized Item With Series", qty=1, - serial_no=serial_nos[0], + serial_no=[serial_nos[0]], company="_Test Company 1", warehouse=wh, ) sn_doc.reload() - self.assertEqual(sn_doc.status, "Delivered") - self.assertEqual(sn_doc.company, "_Test Company 1") - self.assertEqual(sn_doc.delivery_document_no, dn_2.name) + self.assertEqual(sn_doc.warehouse, None) dn_2.cancel() sn_doc.reload() # Fallback on Purchase Receipt if Delivery is cancelled - self.assertEqual(sn_doc.status, "Active") - self.assertEqual(sn_doc.company, "_Test Company 1") self.assertEqual(sn_doc.warehouse, wh) - self.assertEqual(sn_doc.purchase_document_no, pr.name) pr.cancel() sn_doc.reload() # Inactive in same company if Receipt cancelled - self.assertEqual(sn_doc.status, "Inactive") - self.assertEqual(sn_doc.company, "_Test Company 1") self.assertEqual(sn_doc.warehouse, None) dn.cancel() sn_doc.reload() # Fallback on Purchase Receipt in FIRST company if # Delivery from FIRST company is cancelled - self.assertEqual(sn_doc.status, "Active") - self.assertEqual(sn_doc.company, "_Test Company") self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC") - self.assertEqual(sn_doc.purchase_document_no, se.name) - - def test_serial_no_sanitation(self): - "Test if Serial No input is sanitised before entering the DB." - item_code = "_Test Serialized Item" - test_records = frappe.get_test_records("Stock Entry") - - se = frappe.copy_doc(test_records[0]) - se.get("items")[0].item_code = item_code - se.get("items")[0].qty = 4 - se.get("items")[0].serial_no = " _TS1, _TS2 , _TS3 , _TS4 - 2021" - se.get("items")[0].transfer_qty = 4 - se.set_stock_entry_type() - se.insert() - se.submit() - - self.assertEqual(se.get("items")[0].serial_no, "_TS1\n_TS2\n_TS3\n_TS4 - 2021") def test_correct_serial_no_incoming_rate(self): """Check correct consumption rate based on serial no record.""" @@ -236,19 +195,28 @@ class TestSerialNo(FrappeTestCase): warehouse = "_Test Warehouse - _TC" serial_nos = ["LOWVALUATION", "HIGHVALUATION"] + for serial_no in serial_nos: + if not frappe.db.exists("Serial No", serial_no): + frappe.get_doc( + {"doctype": "Serial No", "item_code": item_code, "serial_no": serial_no} + ).insert() + in1 = make_stock_entry( - item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, serial_no=serial_nos[0] + item_code=item_code, to_warehouse=warehouse, qty=1, rate=42, serial_no=[serial_nos[0]] ) in2 = make_stock_entry( - item_code=item_code, to_warehouse=warehouse, qty=1, rate=113, serial_no=serial_nos[1] + item_code=item_code, to_warehouse=warehouse, qty=1, rate=113, serial_no=[serial_nos[1]] ) out = create_delivery_note( - item_code=item_code, qty=1, serial_no=serial_nos[0], do_not_submit=True + item_code=item_code, qty=1, serial_no=[serial_nos[0]], do_not_submit=True ) - # change serial no - out.items[0].serial_no = serial_nos[1] + bundle = out.items[0].serial_and_batch_bundle + doc = frappe.get_doc("Serial and Batch Bundle", bundle) + doc.entries[0].serial_no = serial_nos[1] + doc.save() + out.save() out.submit() @@ -285,40 +253,90 @@ class TestSerialNo(FrappeTestCase): } # Test FIFO - first_fetch = auto_fetch_serial_number(5, item_code, warehouse) + first_fetch = get_auto_serial_nos( + _dict( + { + "qty": 5, + "item_code": item_code, + "warehouse": warehouse, + } + ) + ) + self.assertEqual(first_fetch, batch_wise_serials[batch1]) # partial FIFO - partial_fetch = auto_fetch_serial_number(2, item_code, warehouse) + partial_fetch = get_auto_serial_nos( + _dict( + { + "qty": 2, + "item_code": item_code, + "warehouse": warehouse, + } + ) + ) + self.assertTrue( set(partial_fetch).issubset(set(first_fetch)), msg=f"{partial_fetch} should be subset of {first_fetch}", ) # exclusion - remaining = auto_fetch_serial_number( - 3, item_code, warehouse, exclude_sr_nos=json.dumps(partial_fetch) + remaining = get_auto_serial_nos( + _dict( + { + "qty": 3, + "item_code": item_code, + "warehouse": warehouse, + "ignore_serial_nos": partial_fetch, + } + ) ) + self.assertEqual(sorted(remaining + partial_fetch), first_fetch) # batchwise for batch, expected_serials in batch_wise_serials.items(): - fetched_sr = auto_fetch_serial_number(5, item_code, warehouse, batch_nos=batch) + fetched_sr = get_auto_serial_nos( + _dict({"qty": 5, "item_code": item_code, "warehouse": warehouse, "batches": [batch]}) + ) + self.assertEqual(fetched_sr, sorted(expected_serials)) # non existing warehouse - self.assertEqual(auto_fetch_serial_number(10, item_code, warehouse="Nonexisting"), []) + self.assertFalse( + get_auto_serial_nos( + _dict({"qty": 10, "item_code": item_code, "warehouse": "Non Existing Warehouse"}) + ) + ) # multi batch all_serials = [sr for sr_list in batch_wise_serials.values() for sr in sr_list] - fetched_serials = auto_fetch_serial_number( - 10, item_code, warehouse, batch_nos=list(batch_wise_serials.keys()) + fetched_serials = get_auto_serial_nos( + _dict( + { + "qty": 10, + "item_code": item_code, + "warehouse": warehouse, + "batches": list(batch_wise_serials.keys()), + } + ) ) self.assertEqual(sorted(all_serials), fetched_serials) # expiry date frappe.db.set_value("Batch", batch1, "expiry_date", "1980-01-01") - non_expired_serials = auto_fetch_serial_number( - 5, item_code, warehouse, posting_date="2021-01-01", batch_nos=batch1 + non_expired_serials = get_auto_serial_nos( + _dict({"qty": 5, "item_code": item_code, "warehouse": warehouse, "batches": [batch1]}) ) + self.assertEqual(non_expired_serials, []) + + +def get_auto_serial_nos(kwargs): + from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + get_available_serial_nos, + ) + + serial_nos = get_available_serial_nos(kwargs) + return sorted([d.serial_no for d in serial_nos]) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index 4ad6b267235..569f58a69ff 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -15,9 +15,10 @@ "voucher_type", "voucher_no", "voucher_detail_no", + "serial_and_batch_bundle", "dependant_sle_voucher_detail_no", - "recalculate_rate", "section_break_11", + "recalculate_rate", "actual_qty", "qty_after_transaction", "incoming_rate", @@ -31,15 +32,14 @@ "company", "stock_uom", "project", - "serial_and_batch_bundle", - "has_batch_no", - "batch_no", "column_break_26", "fiscal_year", + "has_batch_no", "has_serial_no", - "serial_no", "is_cancelled", - "to_rename" + "to_rename", + "serial_no", + "batch_no" ], "fields": [ { @@ -341,7 +341,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-12-28 14:50:56.359348", + "modified": "2023-04-03 16:33:16.270722", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 33dd9607f4b..9cae66d495e 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -177,6 +177,11 @@ class SerialBatchBundle: {"is_cancelled": 1, "voucher_no": ""}, ) + if self.sle.serial_and_batch_bundle: + frappe.get_cached_doc( + "Serial and Batch Bundle", self.sle.serial_and_batch_bundle + ).validate_serial_and_batch_inventory() + def post_process(self): if not self.sle.serial_and_batch_bundle: return