mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-10 16:33:04 +00:00
feat: add item where used report (#55714)
This commit is contained in:
1
erpnext/stock/report/item_where_used/__init__.py
Normal file
1
erpnext/stock/report/item_where_used/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
26
erpnext/stock/report/item_where_used/item_where_used.js
Normal file
26
erpnext/stock/report/item_where_used/item_where_used.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Item Where Used"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "item",
|
||||
label: __("Item"),
|
||||
fieldtype: "Link",
|
||||
options: "Item",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
},
|
||||
{
|
||||
fieldname: "section",
|
||||
label: __("Section"),
|
||||
fieldtype: "Select",
|
||||
options: "\nWhere Used\nReferences",
|
||||
},
|
||||
],
|
||||
};
|
||||
35
erpnext/stock/report/item_where_used/item_where_used.json
Normal file
35
erpnext/stock/report/item_where_used/item_where_used.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2026-06-05 00:00:00.000000",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-06-05 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item Where Used",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Item",
|
||||
"report_name": "Item Where Used",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Stock User"
|
||||
},
|
||||
{
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing User"
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager"
|
||||
},
|
||||
{
|
||||
"role": "Item Manager"
|
||||
}
|
||||
]
|
||||
}
|
||||
487
erpnext/stock/report/item_where_used/item_where_used.py
Normal file
487
erpnext/stock/report/item_where_used/item_where_used.py
Normal file
@@ -0,0 +1,487 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
WHERE_USED_SECTION = "Where Used"
|
||||
REFERENCES_SECTION = "References"
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
filters = frappe._dict(filters or {})
|
||||
columns = get_columns()
|
||||
|
||||
if not filters.get("item"):
|
||||
return columns, []
|
||||
|
||||
return columns, get_data(filters)
|
||||
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
"fieldname": "section",
|
||||
"label": _("Section"),
|
||||
"fieldtype": "Data",
|
||||
"width": 110,
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_type",
|
||||
"label": _("Reference Type"),
|
||||
"fieldtype": "Data",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"fieldname": "document_type",
|
||||
"label": _("Document Type"),
|
||||
"fieldtype": "Data",
|
||||
"width": 160,
|
||||
},
|
||||
{
|
||||
"fieldname": "document_name",
|
||||
"label": _("Document"),
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "document_type",
|
||||
"width": 220,
|
||||
},
|
||||
{
|
||||
"fieldname": "related_item",
|
||||
"label": _("Related Item"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Item",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"fieldname": "matched_field",
|
||||
"label": _("Matched Field"),
|
||||
"fieldtype": "Data",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"fieldname": "row_index",
|
||||
"label": _("Row"),
|
||||
"fieldtype": "Int",
|
||||
"width": 70,
|
||||
},
|
||||
{
|
||||
"fieldname": "quantity",
|
||||
"label": _("Qty"),
|
||||
"fieldtype": "Float",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"fieldname": "uom",
|
||||
"label": _("UOM"),
|
||||
"fieldtype": "Link",
|
||||
"options": "UOM",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_quantity",
|
||||
"label": _("Stock Qty"),
|
||||
"fieldtype": "Float",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_uom",
|
||||
"label": _("Stock UOM"),
|
||||
"fieldtype": "Link",
|
||||
"options": "UOM",
|
||||
"width": 110,
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"label": _("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"width": 160,
|
||||
},
|
||||
{
|
||||
"fieldname": "is_default",
|
||||
"label": _("Default"),
|
||||
"fieldtype": "Check",
|
||||
"width": 80,
|
||||
},
|
||||
{
|
||||
"fieldname": "is_active",
|
||||
"label": _("Active"),
|
||||
"fieldtype": "Check",
|
||||
"width": 80,
|
||||
},
|
||||
{
|
||||
"fieldname": "disabled",
|
||||
"label": _("Disabled"),
|
||||
"fieldtype": "Check",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"fieldname": "details",
|
||||
"label": _("Details"),
|
||||
"fieldtype": "Data",
|
||||
"width": 160,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
data = []
|
||||
|
||||
if not filters.get("section") or filters.section == WHERE_USED_SECTION:
|
||||
data.extend(get_where_used_data(filters))
|
||||
|
||||
if not filters.get("section") or filters.section == REFERENCES_SECTION:
|
||||
data.extend(get_reference_data(filters))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_where_used_data(filters):
|
||||
item = filters.item
|
||||
data = []
|
||||
|
||||
data.extend(get_bom_component_rows(item, filters.get("company")))
|
||||
data.extend(get_product_bundle_component_rows(item))
|
||||
data.extend(get_bom_secondary_item_rows(item, filters.get("company")))
|
||||
data.extend(get_subcontracting_bom_rows(item))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_reference_data(filters):
|
||||
item = filters.item
|
||||
data = []
|
||||
|
||||
data.extend(get_bom_output_rows(item, filters.get("company")))
|
||||
data.extend(get_product_bundle_parent_rows(item))
|
||||
data.extend(get_variant_rows(item))
|
||||
data.extend(get_item_alternative_rows(item))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_bom_component_rows(item, company=None):
|
||||
rows = frappe.get_all(
|
||||
"BOM Item",
|
||||
filters={"item_code": item, "parenttype": "BOM", "docstatus": 1},
|
||||
fields=["parent", "idx", "qty", "uom", "stock_qty", "stock_uom", "bom_no"],
|
||||
order_by="parent asc, idx asc",
|
||||
)
|
||||
bom_map = get_bom_map([row.parent for row in rows], company)
|
||||
|
||||
data = []
|
||||
for row in rows:
|
||||
if bom := bom_map.get(row.parent):
|
||||
data.append(
|
||||
build_row(
|
||||
section=WHERE_USED_SECTION,
|
||||
reference_type=_("BOM Component"),
|
||||
document_type="BOM",
|
||||
document_name=row.parent,
|
||||
related_item=bom.item,
|
||||
matched_field="BOM Item.item_code",
|
||||
row_index=row.idx,
|
||||
quantity=row.qty,
|
||||
uom=row.uom,
|
||||
stock_quantity=row.stock_qty,
|
||||
stock_uom=row.stock_uom,
|
||||
company=bom.company,
|
||||
is_default=bom.is_default,
|
||||
is_active=bom.is_active,
|
||||
details=row.bom_no,
|
||||
)
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_bom_secondary_item_rows(item, company=None):
|
||||
rows = frappe.get_all(
|
||||
"BOM Secondary Item",
|
||||
filters={"item_code": item, "parenttype": "BOM", "docstatus": 1},
|
||||
fields=["parent", "idx", "type", "qty", "uom", "stock_qty", "stock_uom"],
|
||||
order_by="parent asc, idx asc",
|
||||
)
|
||||
bom_map = get_bom_map([row.parent for row in rows], company)
|
||||
|
||||
data = []
|
||||
for row in rows:
|
||||
if bom := bom_map.get(row.parent):
|
||||
data.append(
|
||||
build_row(
|
||||
section=WHERE_USED_SECTION,
|
||||
reference_type=_("BOM Secondary Item"),
|
||||
document_type="BOM",
|
||||
document_name=row.parent,
|
||||
related_item=bom.item,
|
||||
matched_field="BOM Secondary Item.item_code",
|
||||
row_index=row.idx,
|
||||
quantity=row.qty,
|
||||
uom=row.uom,
|
||||
stock_quantity=row.stock_qty,
|
||||
stock_uom=row.stock_uom,
|
||||
company=bom.company,
|
||||
is_default=bom.is_default,
|
||||
is_active=bom.is_active,
|
||||
details=row.type,
|
||||
)
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_bom_output_rows(item, company=None):
|
||||
filters = {"item": item, "docstatus": 1}
|
||||
if company:
|
||||
filters["company"] = company
|
||||
|
||||
rows = frappe.get_all(
|
||||
"BOM",
|
||||
filters=filters,
|
||||
fields=["name", "item", "company", "is_default", "is_active", "quantity", "uom"],
|
||||
order_by="is_default desc, name asc",
|
||||
)
|
||||
|
||||
return [
|
||||
build_row(
|
||||
section=REFERENCES_SECTION,
|
||||
reference_type=_("BOM Output"),
|
||||
document_type="BOM",
|
||||
document_name=row.name,
|
||||
related_item=row.item,
|
||||
matched_field="BOM.item",
|
||||
quantity=row.quantity,
|
||||
uom=row.uom,
|
||||
company=row.company,
|
||||
is_default=row.is_default,
|
||||
is_active=row.is_active,
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
def get_product_bundle_component_rows(item):
|
||||
rows = frappe.get_all(
|
||||
"Product Bundle Item",
|
||||
filters={"item_code": item, "parenttype": "Product Bundle", "docstatus": 0},
|
||||
fields=["parent", "idx", "qty", "uom"],
|
||||
order_by="parent asc, idx asc",
|
||||
)
|
||||
bundle_map = get_product_bundle_map([row.parent for row in rows])
|
||||
|
||||
data = []
|
||||
for row in rows:
|
||||
if bundle := bundle_map.get(row.parent):
|
||||
data.append(
|
||||
build_row(
|
||||
section=WHERE_USED_SECTION,
|
||||
reference_type=_("Product Bundle Component"),
|
||||
document_type="Product Bundle",
|
||||
document_name=row.parent,
|
||||
related_item=bundle.new_item_code,
|
||||
matched_field="Product Bundle Item.item_code",
|
||||
row_index=row.idx,
|
||||
quantity=row.qty,
|
||||
uom=row.uom,
|
||||
is_active=0 if bundle.disabled else 1,
|
||||
disabled=bundle.disabled,
|
||||
)
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_product_bundle_parent_rows(item):
|
||||
rows = frappe.get_all(
|
||||
"Product Bundle",
|
||||
filters={"new_item_code": item, "disabled": 0, "docstatus": 0},
|
||||
fields=["name", "new_item_code", "disabled"],
|
||||
order_by="name asc",
|
||||
)
|
||||
|
||||
return [
|
||||
build_row(
|
||||
section=REFERENCES_SECTION,
|
||||
reference_type=_("Product Bundle Parent"),
|
||||
document_type="Product Bundle",
|
||||
document_name=row.name,
|
||||
related_item=row.new_item_code,
|
||||
matched_field="Product Bundle.new_item_code",
|
||||
is_active=0 if row.disabled else 1,
|
||||
disabled=row.disabled,
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
def get_subcontracting_bom_rows(item):
|
||||
data = []
|
||||
|
||||
for row in frappe.get_all(
|
||||
"Subcontracting BOM",
|
||||
filters={"service_item": item},
|
||||
fields=[
|
||||
"name",
|
||||
"is_active",
|
||||
"finished_good",
|
||||
"service_item",
|
||||
"service_item_qty",
|
||||
"service_item_uom",
|
||||
],
|
||||
order_by="finished_good asc, name asc",
|
||||
):
|
||||
data.append(
|
||||
build_row(
|
||||
section=WHERE_USED_SECTION,
|
||||
reference_type=_("Subcontracting Service Item"),
|
||||
document_type="Subcontracting BOM",
|
||||
document_name=row.name,
|
||||
related_item=row.finished_good,
|
||||
matched_field="Subcontracting BOM.service_item",
|
||||
quantity=row.service_item_qty,
|
||||
uom=row.service_item_uom,
|
||||
is_active=row.is_active,
|
||||
)
|
||||
)
|
||||
|
||||
for row in frappe.get_all(
|
||||
"Subcontracting BOM",
|
||||
filters={"finished_good": item},
|
||||
fields=[
|
||||
"name",
|
||||
"is_active",
|
||||
"finished_good",
|
||||
"finished_good_qty",
|
||||
"finished_good_uom",
|
||||
],
|
||||
order_by="name asc",
|
||||
):
|
||||
data.append(
|
||||
build_row(
|
||||
section=WHERE_USED_SECTION,
|
||||
reference_type=_("Subcontracting Finished Good"),
|
||||
document_type="Subcontracting BOM",
|
||||
document_name=row.name,
|
||||
related_item=row.finished_good,
|
||||
matched_field="Subcontracting BOM.finished_good",
|
||||
quantity=row.finished_good_qty,
|
||||
uom=row.finished_good_uom,
|
||||
is_active=row.is_active,
|
||||
)
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_variant_rows(item):
|
||||
rows = frappe.get_all(
|
||||
"Item",
|
||||
filters={"variant_of": item},
|
||||
fields=["name", "variant_of", "disabled"],
|
||||
order_by="name asc",
|
||||
)
|
||||
|
||||
return [
|
||||
build_row(
|
||||
section=REFERENCES_SECTION,
|
||||
reference_type=_("Item Variant"),
|
||||
document_type="Item",
|
||||
document_name=row.name,
|
||||
related_item=row.name,
|
||||
matched_field="Item.variant_of",
|
||||
disabled=row.disabled,
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
def get_item_alternative_rows(item):
|
||||
data = []
|
||||
|
||||
for row in frappe.get_all(
|
||||
"Item Alternative",
|
||||
filters={"item_code": item},
|
||||
fields=["name", "item_code", "alternative_item_code"],
|
||||
order_by="alternative_item_code asc",
|
||||
):
|
||||
data.append(
|
||||
build_row(
|
||||
section=REFERENCES_SECTION,
|
||||
reference_type=_("Item Alternative"),
|
||||
document_type="Item Alternative",
|
||||
document_name=row.name,
|
||||
related_item=row.alternative_item_code,
|
||||
matched_field="Item Alternative.item_code",
|
||||
)
|
||||
)
|
||||
|
||||
for row in frappe.get_all(
|
||||
"Item Alternative",
|
||||
filters={"alternative_item_code": item},
|
||||
fields=["name", "item_code", "alternative_item_code"],
|
||||
order_by="item_code asc",
|
||||
):
|
||||
data.append(
|
||||
build_row(
|
||||
section=REFERENCES_SECTION,
|
||||
reference_type=_("Alternative For Item"),
|
||||
document_type="Item Alternative",
|
||||
document_name=row.name,
|
||||
related_item=row.item_code,
|
||||
matched_field="Item Alternative.alternative_item_code",
|
||||
)
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_bom_map(bom_names, company=None):
|
||||
bom_names = get_unique_names(bom_names)
|
||||
if not bom_names:
|
||||
return {}
|
||||
|
||||
filters = {"name": ["in", bom_names], "docstatus": 1}
|
||||
if company:
|
||||
filters["company"] = company
|
||||
|
||||
return {
|
||||
row.name: row
|
||||
for row in frappe.get_all(
|
||||
"BOM",
|
||||
filters=filters,
|
||||
fields=["name", "item", "company", "is_default", "is_active"],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def get_product_bundle_map(bundle_names):
|
||||
bundle_names = get_unique_names(bundle_names)
|
||||
if not bundle_names:
|
||||
return {}
|
||||
|
||||
return {
|
||||
row.name: row
|
||||
for row in frappe.get_all(
|
||||
"Product Bundle",
|
||||
filters={"name": ["in", bundle_names], "disabled": 0, "docstatus": 0},
|
||||
fields=["name", "new_item_code", "disabled"],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def build_row(**kwargs):
|
||||
return frappe._dict(kwargs)
|
||||
|
||||
|
||||
def get_unique_names(names):
|
||||
unique_names = []
|
||||
seen = set()
|
||||
|
||||
for name in names:
|
||||
if not name or name in seen:
|
||||
continue
|
||||
|
||||
seen.add(name)
|
||||
unique_names.append(name)
|
||||
|
||||
return unique_names
|
||||
@@ -65,6 +65,7 @@ REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
|
||||
("Incorrect Balance Qty After Transaction", {}),
|
||||
("Item Wise Consumption", {}),
|
||||
("Item Prices", {"items": "Enabled Items only"}),
|
||||
("Item Where Used", {"item": "_Test Item", "_optional": True}),
|
||||
("Delayed Item Report", {"based_on": "Sales Invoice"}),
|
||||
("Delayed Item Report", {"based_on": "Delivery Note"}),
|
||||
("Stock Ageing", {"range": "30, 60, 90", "_optional": True}),
|
||||
|
||||
Reference in New Issue
Block a user