diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 977864feca7..d90593dff66 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -431,3 +431,4 @@ erpnext.patches.v15_0.repost_gl_entries_with_no_account_subcontracting #2025-08- erpnext.patches.v15_0.patch_missing_buying_price_list_in_material_request erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice erpnext.patches.v15_0.add_company_payment_gateway_account +erpnext.patches.v16_0.update_serial_no_reference_name diff --git a/erpnext/patches/v16_0/__init__.py b/erpnext/patches/v16_0/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/patches/v16_0/update_serial_no_reference_name.py b/erpnext/patches/v16_0/update_serial_no_reference_name.py new file mode 100644 index 00000000000..ab860fedeba --- /dev/null +++ b/erpnext/patches/v16_0/update_serial_no_reference_name.py @@ -0,0 +1,27 @@ +import frappe + + +def execute(): + # Update the reference_name, reference_doctype fields for Serial No where it is null + + sabb = frappe.qb.DocType("Serial and Batch Bundle") + sabb_entry = frappe.qb.DocType("Serial and Batch Entry") + serial_no = frappe.qb.DocType("Serial No").as_("sn") + + query = ( + frappe.qb.update(serial_no) + .join(sabb_entry) + .on(sabb_entry.serial_no == serial_no.name) + .join(sabb) + .on(sabb.name == sabb_entry.parent) + .set(serial_no.reference_name, serial_no.purchase_document_no) + .set(serial_no.reference_doctype, sabb.voucher_type) + .set(serial_no.posting_date, sabb.posting_date) + .where( + (sabb.voucher_no == serial_no.purchase_document_no) + & (sabb.is_cancelled == 0) + & (sabb_entry.docstatus == 1) + ) + ) + + query.run() 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 66546f71125..9700eda297f 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 @@ -1137,12 +1137,12 @@ class SerialandBatchBundle(Document): def before_submit(self): self.validate_serial_and_batch_data() self.validate_serial_and_batch_no_for_returned() - self.set_purchase_document_no() + self.set_source_document_no() def on_submit(self): self.validate_serial_nos_inventory() - def set_purchase_document_no(self): + def set_source_document_no(self): if self.flags.ignore_validate_serial_batch: return @@ -1154,10 +1154,9 @@ class SerialandBatchBundle(Document): sn_table = frappe.qb.DocType("Serial No") ( frappe.qb.update(sn_table) - .set( - sn_table.purchase_document_no, - self.voucher_no if not sn_table.purchase_document_no else self.voucher_no, - ) + .set(sn_table.reference_doctype, self.voucher_type) + .set(sn_table.reference_name, self.voucher_no) + .set(sn_table.posting_date, self.posting_date) .where(sn_table.name.isin(serial_nos)) ).run() diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 550beb67e48..85bc04fe9ba 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -39,7 +39,11 @@ "company", "column_break_2cmm", "work_order", - "purchase_document_no" + "source_document_section", + "reference_doctype", + "posting_date", + "column_break_ctcx", + "reference_name" ], "fields": [ { @@ -262,13 +266,6 @@ "fieldname": "column_break_2cmm", "fieldtype": "Column Break" }, - { - "fieldname": "purchase_document_no", - "fieldtype": "Data", - "label": "Creation Document No", - "no_copy": 1, - "read_only": 1 - }, { "fieldname": "customer", "fieldtype": "Link", @@ -277,12 +274,45 @@ "options": "Customer", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "source_document_section", + "fieldtype": "Section Break", + "label": "Source Document" + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Source Document Type", + "no_copy": 1, + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "column_break_ctcx", + "fieldtype": "Column Break" + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "label": "Source Document Name", + "no_copy": 1, + "options": "reference_doctype", + "read_only": 1 + }, + { + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "no_copy": 1, + "read_only": 1 } ], "icon": "fa fa-barcode", "idx": 1, "links": [], - "modified": "2025-07-15 13:36:21.938700", + "modified": "2025-08-05 17:17:11.328682", "modified_by": "Administrator", "module": "Stock", "name": "Serial No", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 896323d6529..9479a3de2c5 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -48,8 +48,10 @@ class SerialNo(StockController): item_name: DF.Data | None location: DF.Link | None maintenance_status: DF.Literal["", "Under Warranty", "Out of Warranty", "Under AMC", "Out of AMC"] - purchase_document_no: DF.Data | None + posting_date: DF.Date | None purchase_rate: DF.Float + reference_doctype: DF.Link | None + reference_name: DF.DynamicLink | None serial_no: DF.Data status: DF.Literal["", "Active", "Inactive", "Consumed", "Delivered", "Expired"] warehouse: DF.Link | None diff --git a/erpnext/stock/report/serial_/_batch_traceability_report/__init__.py b/erpnext/stock/report/serial_/_batch_traceability_report/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py index 486828af1cc..d089235dbd3 100644 --- a/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py +++ b/erpnext/stock/report/serial_and_batch_summary/serial_and_batch_summary.py @@ -144,7 +144,15 @@ def get_columns(filters, data): ) if not item_details or item_details.get("has_serial_no"): - columns.append({"label": _("Serial No"), "fieldname": "serial_no", "fieldtype": "Data", "width": 120}) + columns.append( + { + "label": _("Serial No"), + "fieldname": "serial_no", + "fieldtype": "Link", + "width": 120, + "options": "Serial No", + } + ) if not item_details or item_details.get("has_batch_no"): columns.extend( diff --git a/erpnext/stock/report/serial_no_and_batch_traceability/__init__.py b/erpnext/stock/report/serial_no_and_batch_traceability/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.js b/erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.js new file mode 100644 index 00000000000..59cb2eb6545 --- /dev/null +++ b/erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.js @@ -0,0 +1,112 @@ +// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Serial No and Batch Traceability"] = { + filters: [ + { + fieldname: "item_code", + label: __("Item Code"), + options: "Item", + fieldtype: "Link", + get_query: () => { + return { + query: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.item_query", + }; + }, + }, + { + fieldname: "batches", + label: __("Batch No"), + fieldtype: "MultiSelectList", + options: "Batch", + get_data: (txt) => { + let filters = { + disabled: 0, + }; + + let item_code = frappe.query_report.get_filter_value("item_code"); + if (item_code?.length) { + filters.item = ["in", item_code]; + } + + return frappe.db.get_link_options("Batch", txt, filters); + }, + }, + { + fieldname: "serial_nos", + label: __("Serial No"), + fieldtype: "MultiSelectList", + options: "Serial No", + get_data: (txt) => { + let filters = {}; + + let item_code = frappe.query_report.get_filter_value("item_code"); + if (item_code?.length) { + filters.item_code = ["in", item_code]; + } + + return frappe.db.get_link_options("Serial No", txt, filters); + }, + }, + { + fieldname: "traceability_direction", + label: __("Tracebility Direction"), + fieldtype: "Select", + options: "Backward\nForward\nBoth", + default: "Backward", + }, + ], + formatter: function (value, row, column, data, default_formatter) { + if (column.fieldname === "qty" && !data.item_code) { + return ""; + } + + return custom_formatter(value, row, column, data, default_formatter); + }, +}; + +function getTraceabilityLink({ type, value, original_value, item_code, data, filter_values }) { + if (!value) return value; + + const base_url = type === "batch_no" ? "/app/batch/" : "/app/serial-no/"; + const filter_list = filter_values[type]; // either batches or serial_nos + + let css_class = "ellipsis"; + + if (filter_list?.length && !filter_list.includes(original_value)) { + // value not in filtered list + css_class = "ellipsis"; + } else if (item_code && data.item_code && data.item_code !== item_code) { + // mismatch in item code + css_class = "ellipsis"; + } else { + // color by direction + css_class = data.direction === "Backward" ? "ellipsis text-success" : "ellipsis text-danger"; + } + + return `${original_value}`; +} + +function custom_formatter(value, row, column, data, default_formatter) { + let original_value = value; + let filter_values = { + batch_no: frappe.query_report.get_filter_value("batches"), + serial_no: frappe.query_report.get_filter_value("serial_nos"), + }; + let item_code = frappe.query_report.get_filter_value("item_code"); + + value = default_formatter(value, row, column, data); + + if (["batch_no", "serial_no"].includes(column.fieldname) && value) { + value = getTraceabilityLink({ + type: column.fieldname, + value, + original_value, + item_code, + data, + filter_values, + }); + } + + return value; +} diff --git a/erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.json b/erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.json new file mode 100644 index 00000000000..ecb0a7b843c --- /dev/null +++ b/erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.json @@ -0,0 +1,52 @@ +{ + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2025-08-05 13:41:28.654684", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2025-08-05 13:41:28.654684", + "modified_by": "Administrator", + "module": "Stock", + "name": "Serial No and Batch Traceability", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Serial and Batch Bundle", + "report_name": "Serial No and Batch Traceability", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Purchase User" + }, + { + "role": "Purchase Manager" + }, + { + "role": "Stock User" + }, + { + "role": "Stock Manager" + }, + { + "role": "Delivery User" + }, + { + "role": "Delivery Manager" + }, + { + "role": "Manufacturing User" + }, + { + "role": "Manufacturing Manager" + } + ], + "timeout": 0 +} diff --git a/erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py b/erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py new file mode 100644 index 00000000000..805e8d3add3 --- /dev/null +++ b/erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py @@ -0,0 +1,462 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.query_builder import Case + + +def execute(filters: dict | None = None): + report = ReportData(filters) + data = report.get_data() + columns = report.get_columns() + + return columns, data + + +class ReportData: + def __init__(self, filters): + self.filters = filters + self.doctype_name = self.get_doctype() + + def get_data(self): + result_data = [] + + if self.filters.get("traceability_direction") in ["Backward", "Both"]: + data = self.get_serial_no_batches() + source_data = self.prepare_source_data(data) + for key in source_data: + sabb_data = source_data[key] + if sabb_data.reference_doctype != "Stock Entry": + continue + + self.set_backward_data(sabb_data) + + self.parse_batch_details(source_data, result_data, "Backward") + + if self.filters.get("traceability_direction") in ["Forward", "Both"]: + data = self.get_serial_no_batches() + batch_details = frappe._dict({}) + for row in data: + value = row.serial_no or row.batch_no + self.set_forward_data(value, batch_details) + + self.parse_batch_details(batch_details, result_data, "Forward") + + return result_data + + def parse_batch_details(self, sabb_data_details, data, direction, indent=0): + for key in sabb_data_details: + sabb = sabb_data_details[key] + row = { + "item_code": sabb.item_code, + "batch_no": sabb.batch_no, + "serial_no": sabb.serial_no, + "warehouse": sabb.warehouse, + "qty": sabb.qty, + "reference_doctype": sabb.reference_doctype, + "reference_name": sabb.reference_name, + "item_name": sabb.item_name, + "posting_date": sabb.posting_date, + "indent": indent, + "direction": direction, + } + + if data and indent == 0: + data.append({}) + + if direction == "Forward" and row["qty"] > 0: + row["direction"] = "Backward" + + if sabb.reference_doctype == "Purchase Receipt": + row["supplier"] = frappe.db.get_value( + "Purchase Receipt", + sabb.reference_name, + "supplier", + ) + elif sabb.reference_doctype == "Stock Entry": + row["work_order"] = frappe.db.get_value( + "Stock Entry", + sabb.reference_name, + "work_order", + ) + + data.append(row) + + raw_materials = sabb.get("raw_materials") + if raw_materials: + self.parse_batch_details(raw_materials, data, direction, indent + 1) + + return data + + def prepare_source_data(self, data): + source_data = frappe._dict({}) + for row in data: + key = (row.item_code, row.reference_name) + if self.doctype_name == "Batch": + sabb_details = self.get_qty_from_sabb(row) + row.update(sabb_details) + else: + row.qty = 1 + + if key not in source_data: + row["raw_materials"] = frappe._dict({}) + source_data[key] = row + + return source_data + + def get_qty_from_sabb(self, row): + sabb = frappe.qb.DocType("Serial and Batch Bundle") + sabb_entry = frappe.qb.DocType("Serial and Batch Entry") + + query = ( + frappe.qb.from_(sabb) + .inner_join(sabb_entry) + .on(sabb.name == sabb_entry.parent) + .select( + sabb_entry.qty, + sabb_entry.warehouse, + ) + .where( + (sabb_entry.batch_no == row.batch_no) + & (sabb.voucher_type == row.reference_doctype) + & (sabb.voucher_no == row.reference_name) + & (sabb.is_cancelled == 0) + & (sabb_entry.docstatus == 1) + ) + ) + + results = query.run(as_dict=True) + + return results[0] if results else {} + + def set_backward_data(self, sabb_data, qty=None): + if qty: + sabb_data.qty = qty + + if "raw_materials" not in sabb_data: + sabb_data.raw_materials = frappe._dict({}) + + materials = self.get_materials(sabb_data) + for material in materials: + key = (material.item_code, material.batch_no) + + # Recursive: batch has sub-components + if material.serial_no or material.batch_no: + value = material.serial_no or material.batch_no + + if key not in sabb_data.raw_materials: + details = self.get_serial_no_batches(value) + if details: + details.update(self.get_qty_from_sabb(details)) + sabb_data.raw_materials[key] = details + + self.set_backward_data(sabb_data.raw_materials[key], material.qty) + else: + sub_key = material.item_code + if sub_key not in sabb_data.raw_materials: + sabb_data.raw_materials[sub_key] = frappe._dict( + { + "item_code": material.item_code, + "item_name": material.item_name, + "qty": material.qty or material.quantity, + "warehouse": material.warehouse, + } + ) + + return sabb_data + + def get_serial_no_batches(self, name=None): + batches = self.filters.get("batches", []) + serial_nos = self.filters.get("serial_nos", []) + + doctype = frappe.qb.DocType(self.doctype_name) + query = frappe.qb.from_(doctype).select( + doctype.reference_doctype, + doctype.reference_name, + doctype.item_name, + ) + + if self.doctype_name == "Batch": + query = query.select( + doctype.item.as_("item_code"), + doctype.name.as_("batch_no"), + doctype.manufacturing_date.as_("posting_date"), + ) + else: + query = query.select( + doctype.item_code, + doctype.name.as_("serial_no"), + doctype.posting_date, + ) + + if name: + query = query.where(doctype.name == name) + data = query.run(as_dict=True) + return data[0] if data else {} + + if batches: + query = query.where(doctype.name.isin(batches)) + elif serial_nos: + query = query.where(doctype.name.isin(serial_nos)) + + if self.filters.get("item_code"): + if self.doctype_name == "Serial No": + query = query.where(doctype.item_code == self.filters.item_code) + else: + query = query.where(doctype.item == self.filters.item_code) + + return query.run(as_dict=True) + + def get_doctype(self): + if self.filters.item_code: + item_details = frappe.get_cached_value( + "Item", + self.filters.item_code, + ["has_batch_no", "has_serial_no"], + as_dict=True, + ) + + if item_details.has_serial_no: + return "Serial No" + elif item_details.has_batch_no: + return "Batch" + + elif self.filters.get("serial_nos"): + return "Serial No" + + return "Batch" + + def get_materials(self, sabb_data): + stock_entry = frappe.qb.DocType("Stock Entry") + stock_entry_detail = frappe.qb.DocType("Stock Entry Detail") + sabb_entry = frappe.qb.DocType("Serial and Batch Entry") + + query = ( + frappe.qb.from_(stock_entry) + .inner_join(stock_entry_detail) + .on(stock_entry.name == stock_entry_detail.parent) + .left_join(sabb_entry) + .on( + (stock_entry_detail.serial_and_batch_bundle == sabb_entry.parent) + & (sabb_entry.docstatus == 1) + ) + .select( + stock_entry_detail.s_warehouse.as_("warehouse"), + stock_entry_detail.item_code, + stock_entry_detail.item_name, + stock_entry_detail.parenttype.as_("reference_doctype"), + stock_entry.name.as_("reference_name"), + ( + ( + stock_entry_detail.qty + / Case().when(stock_entry.fg_completed_qty > 0, stock_entry.fg_completed_qty).else_(1) + ) + * sabb_data.qty + ).as_("qty"), + sabb_entry.batch_no, + sabb_entry.serial_no, + sabb_entry.qty.as_("quantity"), + ) + .where( + (stock_entry.docstatus == 1) + & (stock_entry.purpose.isin(["Manufacture", "Repack"])) + & (stock_entry.name == sabb_data.reference_name) + & (stock_entry_detail.s_warehouse.isnotnull()) + ) + ) + + return query.run(as_dict=True) + + def set_forward_data(self, value, sabb_data): + outward_entries = self.get_outward_sabb_entries(value) + + for row in outward_entries: + if row.reference_doctype == "Stock Entry": + self.process_manufacture_or_repack_entry(row, sabb_data) + else: + self.add_direct_outward_entry(row, sabb_data) + + def add_direct_outward_entry(self, row, batch_details): + key = (row.item_code, row.reference_name) + if key not in batch_details: + row["indent"] = 0 + batch_details[key] = row + + def get_outward_sabb_entries(self, value): + SABB = frappe.qb.DocType("Serial and Batch Bundle") + SABE = frappe.qb.DocType("Serial and Batch Entry") + + query = ( + frappe.qb.from_(SABB) + .inner_join(SABE) + .on(SABB.name == SABE.parent) + .select( + SABB.voucher_type.as_("reference_doctype"), + SABB.voucher_no.as_("reference_name"), + SABE.batch_no, + SABE.serial_no, + SABE.qty, + SABB.item_code, + SABB.item_name, + SABB.posting_date, + SABB.warehouse, + ) + .where((SABB.is_cancelled == 0) & (SABE.docstatus == 1) & (SABB.type_of_transaction == "Outward")) + ) + + if self.doctype_name == "Serial No": + query = query.where(SABE.serial_no == value) + else: + query = query.where(SABE.batch_no == value) + + return query.run(as_dict=True) + + def process_manufacture_or_repack_entry(self, row, batch_details): + ste = frappe.db.get_value("Stock Entry", row.reference_name, ["purpose", "work_order"], as_dict=True) + + if ste and ste.purpose in ["Manufacture", "Repack"]: + fg_item = self.get_finished_item_from_stock_entry(row.reference_name) + if not fg_item: + return + + key = (fg_item.item_code, row.reference_name) + + if key not in batch_details: + serial_no, batch_no = self.get_serial_batch_no(fg_item.serial_and_batch_bundle) + fg_item.update( + { + "work_order": ste.work_order, + "posting_date": row.posting_date, + "serial_no": serial_no, + "batch_no": batch_no, + "indent": 0, + "warehouse": fg_item.warehouse, + "raw_materials": frappe._dict({(row.item_code, row.reference_name): row}), + } + ) + batch_details[key] = fg_item + + def get_finished_item_from_stock_entry(self, reference_name): + return frappe.db.get_value( + "Stock Entry Detail", + {"parent": reference_name, "is_finished_item": 1}, + [ + "item_code", + "item_name", + "serial_and_batch_bundle", + "qty", + "parenttype as reference_doctype", + "parent as reference_name", + "t_warehouse as warehouse", + ], + as_dict=True, + ) + + def get_serial_batch_no(self, serial_and_batch_bundle): + sabb_details = frappe.db.get_value( + "Serial and Batch Entry", + {"parent": serial_and_batch_bundle}, + ["batch_no", "serial_no"], + as_dict=True, + ) + + return (sabb_details.serial_no, sabb_details.batch_no) if sabb_details else (None, None) + + def get_columns(self): + columns = [ + { + "fieldname": "item_code", + "label": _("Item Code"), + "fieldtype": "Link", + "options": "Item", + "width": 180, + }, + { + "fieldname": "item_name", + "label": _("Item Name"), + "fieldtype": "Data", + "width": 120, + }, + ] + + if self.doctype_name == "Serial No": + columns.append( + { + "fieldname": "serial_no", + "label": _("Serial No"), + "fieldtype": "Link", + "options": "Serial No", + "width": 150, + } + ) + else: + columns.append( + { + "fieldname": "batch_no", + "label": _("Batch No"), + "fieldtype": "Link", + "options": "Batch", + "width": 140, + } + ) + + columns.extend( + [ + { + "fieldname": "qty", + "label": _("Quantity"), + "fieldtype": "Float", + "width": 90, + }, + { + "fieldname": "reference_doctype", + "label": _("Voucher Type"), + "fieldtype": "Data", + "width": 130, + }, + { + "fieldname": "reference_name", + "label": _("Source Document No"), + "fieldtype": "Dynamic Link", + "options": "reference_doctype", + "width": 200, + }, + { + "fieldname": "warehouse", + "label": _("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + "width": 120, + }, + { + "fieldname": "posting_date", + "label": _("Posting Date"), + "fieldtype": "Date", + "width": 120, + }, + { + "fieldname": "work_order", + "label": _("Work Order"), + "fieldtype": "Link", + "options": "Work Order", + "width": 160, + }, + { + "fieldname": "supplier", + "label": _("Supplier"), + "fieldtype": "Link", + "options": "Supplier", + "width": 150, + }, + { + "fieldname": "customer", + "label": _("Customer"), + "fieldtype": "Link", + "options": "Customer", + "width": 150, + }, + ] + ) + + return columns diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index bc42fb4f6c6..1ed0021347a 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -423,7 +423,7 @@ class SerialBatchBundle: "Active" if warehouse else status - if (sn_table.purchase_document_no != sle.voucher_no or sle.is_cancelled != 1) + if (sn_table.reference_name != sle.voucher_no or sle.is_cancelled != 1) else "Inactive", ) .set(sn_table.company, sle.company) @@ -1263,6 +1263,10 @@ class SerialBatchCreation: if self.get("voucher_no"): voucher_no = self.get("voucher_no") + voucher_type = "" + if self.get("voucher_type"): + voucher_type = self.get("voucher_type") + for _i in range(abs(cint(self.actual_qty))): serial_no = make_autoname(self.serial_no_series, "Serial No") sr_nos.append(serial_no) @@ -1280,6 +1284,7 @@ class SerialBatchCreation: self.item_name, self.description, "Active", + voucher_type, voucher_no, self.batch_no, ) @@ -1299,7 +1304,8 @@ class SerialBatchCreation: "item_name", "description", "status", - "purchase_document_no", + "reference_doctype", + "reference_name", "batch_no", ] diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json index 2f8435ac702..fe59ac57e4b 100644 --- a/erpnext/stock/workspace/stock/stock.json +++ b/erpnext/stock/workspace/stock/stock.json @@ -1,11 +1,12 @@ { + "app": "erpnext", "charts": [ { "chart_name": "Warehouse wise Stock Value", "label": "Warehouse wise Stock Value" } ], - "content": "[{\"id\":\"BJTnTemGjc\",\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Stock\",\"col\":12}},{\"id\":\"WKeeHLcyXI\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Stock Value\",\"col\":4}},{\"id\":\"6nVoOHuy5w\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Warehouses\",\"col\":4}},{\"id\":\"OUex5VED7d\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Active Items\",\"col\":4}},{\"id\":\"A3svBa974t\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Warehouse wise Stock Value\",\"col\":12}},{\"id\":\"wwAoBx30p3\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"LkqrpJHM9X\",\"type\":\"header\",\"data\":{\"text\":\"Quick Access\",\"col\":12}},{\"id\":\"OR8PYiYspy\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"id\":\"KP1A22WjDl\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Material Request\",\"col\":3}},{\"id\":\"0EYKOrx6U1\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Entry\",\"col\":3}},{\"id\":\"cqotiphmhZ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Receipt\",\"col\":3}},{\"id\":\"Xhjqnm-JxZ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Delivery Note\",\"col\":3}},{\"id\":\"yxCx6Tay4Z\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Ledger\",\"col\":3}},{\"id\":\"o3sdEnNy34\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Balance\",\"col\":3}},{\"id\":\"m9O0HUUDS5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"NwWcNC_xNj\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Inventory Management\",\"col\":3}},{\"id\":\"9AmAh9LnPI\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"3SmmwBbOER\",\"type\":\"header\",\"data\":{\"text\":\"Masters & Reports\",\"col\":12}},{\"id\":\"OAGNH9njt7\",\"type\":\"card\",\"data\":{\"card_name\":\"Items Catalogue\",\"col\":4}},{\"id\":\"jF9eKz0qr0\",\"type\":\"card\",\"data\":{\"card_name\":\"Stock Transactions\",\"col\":4}},{\"id\":\"tyTnQo-MIS\",\"type\":\"card\",\"data\":{\"card_name\":\"Stock Reports\",\"col\":4}},{\"id\":\"dJaJw6YNPU\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"rQf5vK4N_T\",\"type\":\"card\",\"data\":{\"card_name\":\"Serial No and Batch\",\"col\":4}},{\"id\":\"7oM7hFL4v8\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"ve3L6ZifkB\",\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"id\":\"8Kfvu3umw7\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", + "content": "[{\"id\":\"WKeeHLcyXI\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Stock Value\",\"col\":4}},{\"id\":\"6nVoOHuy5w\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Warehouses\",\"col\":4}},{\"id\":\"OUex5VED7d\",\"type\":\"number_card\",\"data\":{\"number_card_name\":\"Total Active Items\",\"col\":4}},{\"id\":\"A3svBa974t\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Warehouse wise Stock Value\",\"col\":12}},{\"id\":\"wwAoBx30p3\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"LkqrpJHM9X\",\"type\":\"header\",\"data\":{\"text\":\"Quick Access\",\"col\":12}},{\"id\":\"OR8PYiYspy\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":3}},{\"id\":\"KP1A22WjDl\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Material Request\",\"col\":3}},{\"id\":\"0EYKOrx6U1\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Entry\",\"col\":3}},{\"id\":\"cqotiphmhZ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Receipt\",\"col\":3}},{\"id\":\"Xhjqnm-JxZ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Delivery Note\",\"col\":3}},{\"id\":\"yxCx6Tay4Z\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Ledger\",\"col\":3}},{\"id\":\"o3sdEnNy34\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Balance\",\"col\":3}},{\"id\":\"m9O0HUUDS5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"NwWcNC_xNj\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Inventory Management\",\"col\":3}},{\"id\":\"9AmAh9LnPI\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"3SmmwBbOER\",\"type\":\"header\",\"data\":{\"text\":\"Masters & Reports\",\"col\":12}},{\"id\":\"OAGNH9njt7\",\"type\":\"card\",\"data\":{\"card_name\":\"Items Catalogue\",\"col\":4}},{\"id\":\"jF9eKz0qr0\",\"type\":\"card\",\"data\":{\"card_name\":\"Stock Transactions\",\"col\":4}},{\"id\":\"tyTnQo-MIS\",\"type\":\"card\",\"data\":{\"card_name\":\"Stock Reports\",\"col\":4}},{\"id\":\"dJaJw6YNPU\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"id\":\"rQf5vK4N_T\",\"type\":\"card\",\"data\":{\"card_name\":\"Serial No and Batch\",\"col\":4}},{\"id\":\"7oM7hFL4v8\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"ve3L6ZifkB\",\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"id\":\"8Kfvu3umw7\",\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]", "creation": "2020-03-02 15:43:10.096528", "custom_blocks": [], "docstatus": 0, @@ -286,80 +287,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Serial No and Batch", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "Item", - "hidden": 0, - "is_query_report": 0, - "label": "Serial No", - "link_count": 0, - "link_to": "Serial No", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "Item", - "hidden": 0, - "is_query_report": 0, - "label": "Batch", - "link_count": 0, - "link_to": "Batch", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "Item", - "hidden": 0, - "is_query_report": 0, - "label": "Installation Note", - "link_count": 0, - "link_to": "Installation Note", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Serial No", - "hidden": 0, - "is_query_report": 0, - "label": "Serial No Service Contract Expiry", - "link_count": 0, - "link_to": "Serial No Service Contract Expiry", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Serial No", - "hidden": 0, - "is_query_report": 0, - "label": "Serial No Status", - "link_count": 0, - "link_to": "Serial No Status", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "Serial No", - "hidden": 0, - "is_query_report": 0, - "label": "Serial No Warranty Expiry", - "link_count": 0, - "link_to": "Serial No Warranty Expiry", - "link_type": "Report", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -601,6 +528,7 @@ "link_to": "Warehouse Wise Stock Balance", "link_type": "Report", "onboard": 0, + "report_ref_doctype": "Stock Ledger Entry", "type": "Link" }, { @@ -761,9 +689,107 @@ "link_type": "Report", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Serial No and Batch", + "link_count": 8, + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "Item", + "hidden": 0, + "is_query_report": 0, + "label": "Serial No", + "link_count": 0, + "link_to": "Serial No", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "Item", + "hidden": 0, + "is_query_report": 0, + "label": "Batch", + "link_count": 0, + "link_to": "Batch", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 1, + "label": "Serial No and Batch Traceability", + "link_count": 0, + "link_to": "Serial No and Batch Traceability", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 1, + "label": "Serial No Ledger", + "link_count": 0, + "link_to": "Serial No Ledger", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Item", + "hidden": 0, + "is_query_report": 0, + "label": "Installation Note", + "link_count": 0, + "link_to": "Installation Note", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "Serial No", + "hidden": 0, + "is_query_report": 0, + "label": "Serial No Service Contract Expiry", + "link_count": 0, + "link_to": "Serial No Service Contract Expiry", + "link_type": "Report", + "onboard": 0, + "report_ref_doctype": "Serial No", + "type": "Link" + }, + { + "dependencies": "Serial No", + "hidden": 0, + "is_query_report": 0, + "label": "Serial No Status", + "link_count": 0, + "link_to": "Serial No Status", + "link_type": "Report", + "onboard": 0, + "report_ref_doctype": "Serial No", + "type": "Link" + }, + { + "dependencies": "Serial No", + "hidden": 0, + "is_query_report": 0, + "label": "Serial No Warranty Expiry", + "link_count": 0, + "link_to": "Serial No Warranty Expiry", + "link_type": "Report", + "onboard": 0, + "report_ref_doctype": "Serial No", + "type": "Link" } ], - "modified": "2023-07-04 14:38:14.988756", + "modified": "2025-08-06 13:22:35.414711", "modified_by": "Administrator", "module": "Stock", "name": "Stock", @@ -836,11 +862,13 @@ { "label": "Stock Ledger", "link_to": "Stock Ledger", + "report_ref_doctype": "Stock Ledger Entry", "type": "Report" }, { "label": "Stock Balance", "link_to": "Stock Balance", + "report_ref_doctype": "Stock Ledger Entry", "type": "Report" }, { @@ -849,5 +877,6 @@ "type": "Dashboard" } ], - "title": "Stock" + "title": "Stock", + "type": "Workspace" }