mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-16 08:05:00 +00:00
feat: serial no and batch traceability report
This commit is contained in:
@@ -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
|
||||
|
||||
0
erpnext/patches/v16_0/__init__.py
Normal file
0
erpnext/patches/v16_0/__init__.py
Normal file
27
erpnext/patches/v16_0/update_serial_no_reference_name.py
Normal file
27
erpnext/patches/v16_0/update_serial_no_reference_name.py
Normal file
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 `<a class="${css_class}" href="${base_url}${original_value}">${original_value}</a>`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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\":\"<span class=\\\"h4\\\"><b>Quick Access</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Masters & Reports</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Quick Access</b></span>\",\"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\":\"<span class=\\\"h4\\\"><b>Masters & Reports</b></span>\",\"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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user