From 22767410d56602dc14077844538fbd2ca904b26d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 28 Oct 2025 13:46:07 +0530 Subject: [PATCH] fix: provision to find and fix incorrect serial and batch bundles (cherry picked from commit 10ad56060cc632899a2d254139038d2ba1024b5d) --- .../incorrect_serial_and_batch_bundle.js | 38 +++++----- .../incorrect_serial_and_batch_bundle.py | 76 ++++++++++++++++--- 2 files changed, 86 insertions(+), 28 deletions(-) diff --git a/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.js b/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.js index dccb543115e..eb146a7b447 100644 --- a/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.js +++ b/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.js @@ -24,24 +24,26 @@ frappe.query_reports["Incorrect Serial and Batch Bundle"] = { }, onload(report) { - report.page.add_inner_button(__("Remove SABB Entry"), () => { - let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); - let selected_rows = indexes.map((i) => frappe.query_report.data[i]); + report.page + .add_inner_button(__("Fix SABB Entry"), () => { + let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); + let selected_rows = indexes.map((i) => frappe.query_report.data[i]); - if (!selected_rows.length) { - frappe.throw(__("Please select a row to create a Reposting Entry")); - } else { - frappe.call({ - method: "erpnext.stock.report.incorrect_serial_and_batch_bundle.incorrect_serial_and_batch_bundle.remove_sabb_entry", - freeze: true, - args: { - selected_rows: selected_rows, - }, - callback: function (r) { - frappe.query_report.refresh(); - }, - }); - } - }); + if (!selected_rows.length) { + frappe.throw(__("Please select at least one row to fix")); + } else { + frappe.call({ + method: "erpnext.stock.report.incorrect_serial_and_batch_bundle.incorrect_serial_and_batch_bundle.fix_sabb_entries", + freeze: true, + args: { + selected_rows: selected_rows, + }, + callback: function (r) { + frappe.query_report.refresh(); + }, + }); + } + }) + .addClass("btn-primary"); }, }; diff --git a/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py b/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py index e65725f3c3d..0b27d697a4d 100644 --- a/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py +++ b/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.py @@ -13,7 +13,10 @@ def execute(filters: dict | None = None): every time the report is refreshed or a filter is updated. """ columns = get_columns() - data = get_data(filters) + unlinked_bundles = get_unlinked_serial_batch_bundles(filters) or [] + linked_cancelled_bundles = get_linked_cancelled_sabb(filters) or [] + + data = unlinked_bundles + linked_cancelled_bundles return columns, data @@ -50,14 +53,17 @@ def get_columns() -> list[dict]: "fieldtype": "Data", "width": 200, }, + { + "label": _("Is Cancelled"), + "fieldname": "is_cancelled", + "fieldtype": "Check", + "width": 200, + }, ] -def get_data(filters) -> list[list]: - """Return data for the report. - - The report data is a list of rows, with each row being a list of cell values. - """ +def get_unlinked_serial_batch_bundles(filters) -> list[list]: + # SABB has not been linked to any SLE SABB = frappe.qb.DocType("Serial and Batch Bundle") SLE = frappe.qb.DocType("Stock Ledger Entry") @@ -77,6 +83,7 @@ def get_data(filters) -> list[list]: SABB.voucher_type, SABB.voucher_no, SABB.voucher_detail_no, + SABB.is_cancelled, ) .where( (SLE.serial_and_batch_bundle.isnull()) @@ -94,14 +101,63 @@ def get_data(filters) -> list[list]: return data +def get_linked_cancelled_sabb(filters): + # SABB has cancelled but voucher is not cancelled + + SABB = frappe.qb.DocType("Serial and Batch Bundle") + SLE = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(SABB) + .inner_join(SLE) + .on(SABB.name == SLE.serial_and_batch_bundle) + .select( + SABB.name, + SABB.voucher_type, + SABB.voucher_no, + SABB.voucher_detail_no, + SABB.is_cancelled, + ) + .where( + (SLE.serial_and_batch_bundle.isnotnull()) + & (SABB.docstatus == 2) + & (SABB.is_cancelled == 1) + & (SLE.is_cancelled == 0) + ) + ) + + for field in filters: + query = query.where(SABB[field] == filters[field]) + + data = query.run(as_dict=1) + return data + + @frappe.whitelist() -def remove_sabb_entry(selected_rows): +def fix_sabb_entries(selected_rows): if isinstance(selected_rows, str): selected_rows = frappe.parse_json(selected_rows) for row in selected_rows: doc = frappe.get_doc("Serial and Batch Bundle", row.get("name")) - doc.cancel() - doc.delete() + if doc.is_cancelled == 0 and not frappe.db.get_value( + "Stock Ledger Entry", + {"serial_and_batch_bundle": doc.name, "is_cancelled": 0}, + "name", + ): + doc.db_set({"is_cancelled": 1, "docstatus": 2}) - frappe.msgprint(_("Selected Serial and Batch Bundle entries have been removed.")) + for row in doc.entries: + row.db_set("docstatus", 2) + + elif doc.is_cancelled == 1 and frappe.db.get_value( + "Stock Ledger Entry", + {"serial_and_batch_bundle": doc.name, "is_cancelled": 0}, + "name", + ): + doc.db_set({"is_cancelled": 0, "docstatus": 1}) + + for row in doc.entries: + row.db_set("docstatus", 1) + + frappe.msgprint(_("Selected Serial and Batch Bundle entries have been fixed."))