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"
}