mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-01 15:47:06 +00:00
feat: production plan visualizer page and summary report fix (#58541)
* fix: production plan summary report tree structure and quantities * feat: production plan visualizer page * feat: single screen production plan visualizer with material readiness * fix: show live stock and received status for production plan materials * fix: remove duplicate border under production plan visualizer header * fix: drop page head border on production plan visualizer * fix: add horizontal margin to production plan visualizer * fix: apply record level permissions and resolve shared material owners * fix: list shared raw materials under every finished good that needs them * feat: open linked documents in a side panel from the visualizer * fix: never fall back to stored qty when warehouse stock is not readable * fix: include directly consuming finished goods in material ownership * fix: show each finished good's own share of shared material demand * fix: match production plan quantities and labels in the visualizer * fix: resolve nested sub assembly owners when parent link is missing * fix: keep every matching owner when resolving sub assemblies by item code * feat: flat work order list in place of the items to manufacture tree * fix: flatten items to manufacture rows without changing the table design * fix: align table numbers, units and progress cells * fix: scope nested owner resolution to the same sales order * fix: keep quantity columns numeric and move uom to the item line * fix: recover all finished goods for consolidated sub assembly rows * fix: scope raw material owners to the same sales order
This commit is contained in:
@@ -151,6 +151,15 @@ frappe.ui.form.on("Production Plan", {
|
||||
__("View")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Plan Visualizer"),
|
||||
() => {
|
||||
frappe.route_options = { production_plan: frm.doc.name };
|
||||
frappe.set_route("production-plan-visualizer");
|
||||
},
|
||||
__("View")
|
||||
);
|
||||
|
||||
if (!["Completed", "Closed"].includes(frm.doc.status)) {
|
||||
frm.add_custom_button(__("Schedule Items"), () => {
|
||||
frm.events.show_schedule_dialog(frm);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"content": null,
|
||||
"creation": "2026-08-28 10:00:00",
|
||||
"docstatus": 0,
|
||||
"doctype": "Page",
|
||||
"idx": 0,
|
||||
"modified": "2026-08-28 10:00:00",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "production-plan-visualizer",
|
||||
"owner": "Administrator",
|
||||
"page_name": "production-plan-visualizer",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Manufacturing User"
|
||||
},
|
||||
{
|
||||
"role": "Manufacturing Manager"
|
||||
},
|
||||
{
|
||||
"role": "System Manager"
|
||||
}
|
||||
],
|
||||
"script": null,
|
||||
"standard": "Yes",
|
||||
"style": null,
|
||||
"system_page": 0,
|
||||
"title": "Production Plan Visualizer"
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
import frappe
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_plan_overview(production_plan: str):
|
||||
plan = frappe.get_doc("Production Plan", production_plan)
|
||||
plan.check_permission("read")
|
||||
|
||||
work_orders = get_work_orders(production_plan)
|
||||
purchase_orders = get_purchase_orders(production_plan)
|
||||
schedule = get_schedule(production_plan)
|
||||
stock, warehouses = get_stock_levels(plan)
|
||||
|
||||
return {
|
||||
"plan": get_plan_details(plan),
|
||||
"finished_goods": get_finished_goods(plan, work_orders),
|
||||
"sub_assemblies": get_sub_assemblies(plan, work_orders, purchase_orders, stock, warehouses),
|
||||
"material_requests": get_material_requests(production_plan),
|
||||
"materials": get_materials(plan, production_plan, stock, warehouses),
|
||||
"schedule": schedule,
|
||||
"row_materials": get_row_materials(plan, schedule),
|
||||
}
|
||||
|
||||
|
||||
def get_materials(plan, production_plan, stock, warehouses):
|
||||
raised = {}
|
||||
for row in get_raised_material_request_items(production_plan):
|
||||
raised.setdefault(row.material_request_plan_item, []).append(row)
|
||||
|
||||
return [build_material(row, raised.get(row.name) or [], stock, warehouses) for row in plan.mr_items]
|
||||
|
||||
|
||||
def get_permitted_names(doctype, child_doctype, production_plan):
|
||||
if not frappe.has_permission(doctype):
|
||||
return []
|
||||
|
||||
return frappe.get_list(
|
||||
doctype,
|
||||
filters=[
|
||||
[child_doctype, "production_plan", "=", production_plan],
|
||||
[child_doctype, "docstatus", "<", 2],
|
||||
],
|
||||
pluck="name",
|
||||
distinct=True,
|
||||
limit_page_length=0,
|
||||
)
|
||||
|
||||
|
||||
def get_stock_levels(plan):
|
||||
pairs = [(row.item_code, row.warehouse) for row in plan.mr_items if row.warehouse]
|
||||
pairs += [(row.production_item, row.fg_warehouse) for row in plan.sub_assembly_items if row.fg_warehouse]
|
||||
items = {item for item, _ in pairs}
|
||||
warehouses = {warehouse for _, warehouse in pairs}
|
||||
if not items or not warehouses or not frappe.has_permission("Bin"):
|
||||
return {}, set()
|
||||
|
||||
permitted = set(
|
||||
frappe.get_list(
|
||||
"Warehouse",
|
||||
filters={"name": ("in", warehouses)},
|
||||
pluck="name",
|
||||
limit_page_length=0,
|
||||
)
|
||||
)
|
||||
if not permitted:
|
||||
return {}, permitted
|
||||
|
||||
bins = frappe.get_list(
|
||||
"Bin",
|
||||
filters={"item_code": ("in", items), "warehouse": ("in", permitted)},
|
||||
fields=["item_code", "warehouse", "actual_qty", "projected_qty"],
|
||||
limit_page_length=0,
|
||||
)
|
||||
|
||||
return {(d.item_code, d.warehouse): d for d in bins}, permitted
|
||||
|
||||
|
||||
def build_material(row, raised, stock, warehouses):
|
||||
documents = {
|
||||
entry.name: {"doctype": "Material Request", "name": entry.name, "status": entry.status}
|
||||
for entry in raised
|
||||
}
|
||||
stock_known = row.warehouse in warehouses
|
||||
level = stock.get((row.item_code, row.warehouse)) or frappe._dict()
|
||||
|
||||
return {
|
||||
"row_name": row.name,
|
||||
"item_code": row.item_code,
|
||||
"item_name": row.item_name,
|
||||
"uom": row.uom,
|
||||
"warehouse": row.warehouse,
|
||||
"material_request_type": row.material_request_type,
|
||||
"required_qty": flt(row.required_bom_qty) or flt(row.quantity),
|
||||
"to_procure_qty": flt(row.quantity),
|
||||
"available_qty": flt(level.actual_qty) if stock_known else 0.0,
|
||||
"projected_qty": flt(level.projected_qty) if stock_known else 0.0,
|
||||
"stock_known": stock_known,
|
||||
"requested_qty": flt(row.requested_qty),
|
||||
"ordered_qty": sum(flt(entry.ordered_qty) for entry in raised),
|
||||
"received_qty": sum(flt(entry.received_qty) for entry in raised),
|
||||
"schedule_date": row.schedule_date,
|
||||
"sales_order": row.get("sales_order"),
|
||||
"consumer": row.get("sub_assembly_item_reference"),
|
||||
"main_item_code": row.get("main_item_code"),
|
||||
"from_bom": row.get("from_bom"),
|
||||
"documents": list(documents.values()),
|
||||
}
|
||||
|
||||
|
||||
def get_raised_material_request_items(production_plan):
|
||||
names = get_permitted_names("Material Request", "Material Request Item", production_plan)
|
||||
if not names:
|
||||
return []
|
||||
|
||||
mr_item = frappe.qb.DocType("Material Request Item")
|
||||
material_request = frappe.qb.DocType("Material Request")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(mr_item)
|
||||
.inner_join(material_request)
|
||||
.on(mr_item.parent == material_request.name)
|
||||
.select(
|
||||
mr_item.parent.as_("name"),
|
||||
mr_item.material_request_plan_item,
|
||||
mr_item.item_code,
|
||||
mr_item.qty,
|
||||
mr_item.ordered_qty,
|
||||
mr_item.received_qty,
|
||||
material_request.status,
|
||||
)
|
||||
.where(
|
||||
(mr_item.production_plan == production_plan)
|
||||
& (mr_item.docstatus < 2)
|
||||
& mr_item.parent.isin(names)
|
||||
)
|
||||
.orderby(material_request.transaction_date)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
|
||||
def get_row_materials(plan, schedule):
|
||||
material_items = {d.item_code for d in schedule if d.row_type == "Raw Material"}
|
||||
material_items.update(row.item_code for row in plan.mr_items)
|
||||
material_items.update(row.production_item for row in plan.sub_assembly_items)
|
||||
rows = [(d.name, d.bom_no) for d in plan.po_items + plan.sub_assembly_items if d.bom_no]
|
||||
if not material_items or not rows:
|
||||
return {}
|
||||
|
||||
boms = frappe.get_list(
|
||||
"BOM",
|
||||
filters={"name": ("in", {bom_no for _, bom_no in rows})},
|
||||
pluck="name",
|
||||
limit_page_length=0,
|
||||
)
|
||||
if not boms:
|
||||
return {}
|
||||
|
||||
bom_items = frappe.get_all(
|
||||
"BOM Item",
|
||||
filters={"parent": ("in", boms), "parenttype": "BOM", "item_code": ("in", material_items)},
|
||||
fields=["parent", "item_code"],
|
||||
)
|
||||
|
||||
by_bom = {}
|
||||
for d in bom_items:
|
||||
by_bom.setdefault(d.parent, []).append(d.item_code)
|
||||
|
||||
return {name: by_bom[bom_no] for name, bom_no in rows if by_bom.get(bom_no)}
|
||||
|
||||
|
||||
def get_plan_details(plan):
|
||||
total_planned = flt(plan.total_planned_qty)
|
||||
total_produced = flt(plan.total_produced_qty)
|
||||
return {
|
||||
"name": plan.name,
|
||||
"status": plan.status,
|
||||
"docstatus": plan.docstatus,
|
||||
"company": plan.company,
|
||||
"posting_date": plan.posting_date,
|
||||
"combine_sub_items": plan.combine_sub_items,
|
||||
"total_planned_qty": total_planned,
|
||||
"total_produced_qty": total_produced,
|
||||
"completion": flt(total_produced / total_planned * 100 if total_planned else 0, 1),
|
||||
}
|
||||
|
||||
|
||||
def get_finished_goods(plan, work_orders):
|
||||
rows = []
|
||||
for row in plan.po_items:
|
||||
documents = [d for d in work_orders if d.production_plan_item == row.name]
|
||||
produced_qty = sum(flt(d.produced_qty) for d in documents)
|
||||
rows.append(
|
||||
{
|
||||
"row_name": row.name,
|
||||
"item_code": row.item_code,
|
||||
"item_name": frappe.get_cached_value("Item", row.item_code, "item_name"),
|
||||
"sales_order": row.get("sales_order"),
|
||||
"warehouse": row.warehouse,
|
||||
"planned_start_date": row.planned_start_date,
|
||||
"planned_end_date": row.get("planned_end_date"),
|
||||
"qty": flt(row.planned_qty),
|
||||
"produced_qty": produced_qty,
|
||||
"pending_qty": flt(row.planned_qty) - produced_qty,
|
||||
"stock_uom": row.stock_uom,
|
||||
"documents": documents,
|
||||
}
|
||||
)
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
def get_sub_assemblies(plan, work_orders, purchase_orders, stock, warehouses):
|
||||
rows = []
|
||||
for item in plan.sub_assembly_items:
|
||||
if item.type_of_manufacturing == "Subcontract":
|
||||
documents = [d for d in purchase_orders if d.production_plan_sub_assembly_item == item.name]
|
||||
else:
|
||||
documents = [d for d in work_orders if d.production_plan_sub_assembly_item == item.name]
|
||||
|
||||
produced_qty = sum(flt(d.produced_qty) for d in documents)
|
||||
stock_known = item.fg_warehouse in warehouses
|
||||
level = stock.get((item.production_item, item.fg_warehouse)) or frappe._dict()
|
||||
rows.append(
|
||||
{
|
||||
"row_name": item.name,
|
||||
"production_plan_item": item.production_plan_item,
|
||||
"parent_item_code": item.parent_item_code,
|
||||
"sales_order": item.get("sales_order"),
|
||||
"item_code": item.production_item,
|
||||
"item_name": item.item_name,
|
||||
"qty": flt(item.qty),
|
||||
"produced_qty": produced_qty,
|
||||
"pending_qty": flt(item.qty) - produced_qty,
|
||||
"available_qty": flt(level.actual_qty) if stock_known else 0.0,
|
||||
"stock_known": stock_known,
|
||||
"bom_no": item.bom_no,
|
||||
"bom_level": item.bom_level,
|
||||
"indent": item.indent or 0,
|
||||
"type_of_manufacturing": item.type_of_manufacturing,
|
||||
"supplier": item.get("supplier"),
|
||||
"schedule_date": item.schedule_date,
|
||||
"uom": item.stock_uom or item.uom,
|
||||
"documents": documents,
|
||||
}
|
||||
)
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
def get_work_orders(production_plan):
|
||||
if not frappe.has_permission("Work Order"):
|
||||
return []
|
||||
|
||||
work_orders = frappe.get_list(
|
||||
"Work Order",
|
||||
filters={"production_plan": production_plan, "docstatus": ("<", 2)},
|
||||
fields=[
|
||||
"name",
|
||||
"qty",
|
||||
"produced_qty",
|
||||
"material_transferred_for_manufacturing",
|
||||
"status",
|
||||
"docstatus",
|
||||
"planned_start_date",
|
||||
"production_item as item_code",
|
||||
"item_name",
|
||||
"production_plan_item",
|
||||
"production_plan_sub_assembly_item",
|
||||
],
|
||||
order_by="creation",
|
||||
limit_page_length=0,
|
||||
)
|
||||
|
||||
for row in work_orders:
|
||||
row.doctype = "Work Order"
|
||||
|
||||
return work_orders
|
||||
|
||||
|
||||
def get_purchase_orders(production_plan):
|
||||
names = get_permitted_names("Purchase Order", "Purchase Order Item", production_plan)
|
||||
if not names:
|
||||
return []
|
||||
|
||||
po_item = frappe.qb.DocType("Purchase Order Item")
|
||||
purchase_order = frappe.qb.DocType("Purchase Order")
|
||||
|
||||
purchase_orders = (
|
||||
frappe.qb.from_(po_item)
|
||||
.inner_join(purchase_order)
|
||||
.on(po_item.parent == purchase_order.name)
|
||||
.select(
|
||||
po_item.parent.as_("name"),
|
||||
po_item.qty.as_("order_qty"),
|
||||
po_item.received_qty,
|
||||
po_item.fg_item,
|
||||
po_item.fg_item_qty,
|
||||
po_item.production_plan_sub_assembly_item,
|
||||
purchase_order.status,
|
||||
purchase_order.docstatus,
|
||||
purchase_order.supplier,
|
||||
)
|
||||
.where(
|
||||
(po_item.production_plan == production_plan)
|
||||
& (po_item.docstatus < 2)
|
||||
& po_item.parent.isin(names)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for row in purchase_orders:
|
||||
row.doctype = "Purchase Order"
|
||||
row.qty = flt(row.fg_item_qty) if row.fg_item else flt(row.order_qty)
|
||||
row.produced_qty = flt(row.received_qty)
|
||||
if row.fg_item:
|
||||
row.produced_qty = flt(row.received_qty) / (flt(row.order_qty) / flt(row.fg_item_qty) or 1)
|
||||
|
||||
return purchase_orders
|
||||
|
||||
|
||||
def get_material_requests(production_plan):
|
||||
names = get_permitted_names("Material Request", "Material Request Item", production_plan)
|
||||
if not names:
|
||||
return []
|
||||
|
||||
mr_item = frappe.qb.DocType("Material Request Item")
|
||||
material_request = frappe.qb.DocType("Material Request")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(mr_item)
|
||||
.inner_join(material_request)
|
||||
.on(mr_item.parent == material_request.name)
|
||||
.select(
|
||||
mr_item.parent.as_("name"),
|
||||
material_request.status,
|
||||
material_request.material_request_type,
|
||||
material_request.transaction_date,
|
||||
material_request.per_ordered,
|
||||
material_request.per_received,
|
||||
Sum(mr_item.qty).as_("qty"),
|
||||
)
|
||||
.where(
|
||||
(mr_item.production_plan == production_plan)
|
||||
& (mr_item.docstatus < 2)
|
||||
& mr_item.parent.isin(names)
|
||||
)
|
||||
.groupby(
|
||||
mr_item.parent,
|
||||
material_request.status,
|
||||
material_request.material_request_type,
|
||||
material_request.transaction_date,
|
||||
material_request.per_ordered,
|
||||
material_request.per_received,
|
||||
)
|
||||
.orderby(material_request.transaction_date)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
|
||||
def get_schedule(production_plan):
|
||||
if not frappe.has_permission("Production Plan Schedule"):
|
||||
return []
|
||||
|
||||
return frappe.get_list(
|
||||
"Production Plan Schedule",
|
||||
filters={"production_plan": production_plan},
|
||||
fields=[
|
||||
"name",
|
||||
"subject",
|
||||
"row_type",
|
||||
"plan_row",
|
||||
"item_code",
|
||||
"item_name",
|
||||
"operation",
|
||||
"workstation",
|
||||
"supplier",
|
||||
"from_time",
|
||||
"to_time",
|
||||
"duration_mins",
|
||||
],
|
||||
order_by="from_time",
|
||||
limit_page_length=0,
|
||||
)
|
||||
@@ -21,14 +21,32 @@ frappe.query_reports["Production Plan Summary"] = {
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (column.fieldname == "item_code") {
|
||||
var color = data.pending_qty > 0 ? "red" : "green";
|
||||
if (column.fieldname == "item_code" && !data.document_type) {
|
||||
var color = data.pending_qty > 0 ? "var(--red-500)" : "var(--green-600)";
|
||||
value = `<a style='color:${color}' href="${frappe.utils.get_form_link(
|
||||
"Item",
|
||||
data["item_code"]
|
||||
)}" data-doctype="Item">${frappe.utils.escape_html(data["item_code"])}</a>`;
|
||||
}
|
||||
|
||||
if (column.fieldname == "status" && data.status && frappe.ui.badge) {
|
||||
const themes = {
|
||||
Completed: "green",
|
||||
"In Process": "blue",
|
||||
"Not Started": "amber",
|
||||
Submitted: "blue",
|
||||
Stopped: "red",
|
||||
Closed: "gray",
|
||||
"To Receive and Bill": "amber",
|
||||
"To Receive": "amber",
|
||||
"To Bill": "amber",
|
||||
};
|
||||
value = frappe.ui.badge.html({
|
||||
label: __(data.status),
|
||||
theme: themes[data.status] || "gray",
|
||||
});
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,163 +8,173 @@ from frappe.utils import flt
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
data = get_data(filters)
|
||||
columns = get_column(filters)
|
||||
|
||||
return columns, data
|
||||
return get_column(filters), get_data(filters)
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
data = []
|
||||
plan = frappe.get_cached_doc("Production Plan", filters.get("production_plan"))
|
||||
work_orders = get_work_orders(filters)
|
||||
purchase_orders = get_purchase_orders(filters)
|
||||
|
||||
order_details = {}
|
||||
get_work_order_details(filters, order_details)
|
||||
get_purchase_order_details(filters, order_details)
|
||||
get_production_plan_item_details(filters, data, order_details)
|
||||
data = []
|
||||
for row in plan.po_items:
|
||||
fg_work_orders = [d for d in work_orders if d.production_plan_item == row.name]
|
||||
data.append(get_finished_good_row(row, fg_work_orders))
|
||||
data.extend(get_document_row(d, indent=1) for d in fg_work_orders)
|
||||
sub_items = [d for d in plan.sub_assembly_items if d.production_plan_item == row.name]
|
||||
add_sub_assembly_rows(sub_items, data, work_orders, purchase_orders)
|
||||
|
||||
po_row_names = {row.name for row in plan.po_items}
|
||||
orphan_items = [d for d in plan.sub_assembly_items if d.production_plan_item not in po_row_names]
|
||||
add_sub_assembly_rows(orphan_items, data, work_orders, purchase_orders)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_production_plan_item_details(filters, data, order_details):
|
||||
production_plan_doc = frappe.get_cached_doc("Production Plan", filters.get("production_plan"))
|
||||
for row in production_plan_doc.po_items:
|
||||
work_orders = frappe.get_all(
|
||||
"Work Order",
|
||||
filters={
|
||||
"production_plan_item": row.name,
|
||||
"bom_no": row.bom_no,
|
||||
"production_item": row.item_code,
|
||||
"docstatus": 1,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
def get_finished_good_row(row, fg_work_orders):
|
||||
produced_qty = sum(flt(d.produced_qty) for d in fg_work_orders)
|
||||
return {
|
||||
"indent": 0,
|
||||
"item_code": row.item_code,
|
||||
"item_name": frappe.get_cached_value("Item", row.item_code, "item_name"),
|
||||
"sales_order": row.get("sales_order"),
|
||||
"bom_level": 0,
|
||||
"qty": flt(row.planned_qty),
|
||||
"produced_qty": produced_qty,
|
||||
"pending_qty": flt(row.planned_qty) - produced_qty,
|
||||
}
|
||||
|
||||
order_qty = row.planned_qty
|
||||
total_produced_qty = 0.0
|
||||
# default to the full planned qty so a plan without any work order still
|
||||
# reports everything as pending rather than a misleading zero
|
||||
pending_qty = flt(order_qty)
|
||||
for work_order in work_orders:
|
||||
produced_qty = flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0))
|
||||
pending_qty = flt(order_qty) - produced_qty
|
||||
|
||||
total_produced_qty += produced_qty
|
||||
|
||||
data.append(
|
||||
{
|
||||
"indent": 0,
|
||||
"item_code": row.item_code,
|
||||
"sales_order": row.get("sales_order"),
|
||||
"item_name": frappe.get_cached_value("Item", row.item_code, "item_name"),
|
||||
"qty": order_qty,
|
||||
"document_type": "Work Order",
|
||||
"document_name": work_order or "",
|
||||
"bom_level": 0,
|
||||
"produced_qty": produced_qty,
|
||||
"pending_qty": pending_qty,
|
||||
}
|
||||
)
|
||||
|
||||
order_qty = pending_qty
|
||||
def add_sub_assembly_rows(items, data, work_orders, purchase_orders):
|
||||
for item in items:
|
||||
if item.type_of_manufacturing == "Subcontract":
|
||||
documents = [d for d in purchase_orders if d.production_plan_sub_assembly_item == item.name]
|
||||
else:
|
||||
documents = [d for d in work_orders if d.production_plan_sub_assembly_item == item.name]
|
||||
|
||||
indent = 1 + (item.indent or 0)
|
||||
produced_qty = sum(flt(d.produced_qty) for d in documents)
|
||||
data.append(
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"indent": 0,
|
||||
"qty": row.planned_qty,
|
||||
"produced_qty": total_produced_qty,
|
||||
"pending_qty": pending_qty,
|
||||
"indent": indent,
|
||||
"item_code": item.production_item,
|
||||
"item_name": item.item_name,
|
||||
"bom_level": item.bom_level,
|
||||
"qty": flt(item.qty),
|
||||
"produced_qty": produced_qty,
|
||||
"pending_qty": flt(item.qty) - produced_qty,
|
||||
}
|
||||
)
|
||||
|
||||
get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details)
|
||||
data.extend(get_document_row(d, indent=indent + 1) for d in documents)
|
||||
|
||||
|
||||
def get_production_plan_sub_assembly_item_details(filters, row, production_plan_doc, data, order_details):
|
||||
for item in production_plan_doc.sub_assembly_items:
|
||||
if row.name == item.production_plan_item:
|
||||
subcontracted_item = item.type_of_manufacturing == "Subcontract"
|
||||
|
||||
if subcontracted_item:
|
||||
docnames = frappe.get_all(
|
||||
"Purchase Order Item",
|
||||
filters={"production_plan_sub_assembly_item": item.name, "docstatus": 1},
|
||||
fields=["parent"],
|
||||
order_by="creation",
|
||||
pluck="parent",
|
||||
)
|
||||
else:
|
||||
docnames = frappe.get_all(
|
||||
"Work Order",
|
||||
filters={"production_plan_sub_assembly_item": item.name, "docstatus": 1},
|
||||
fields=["name"],
|
||||
order_by="creation",
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for docname in docnames:
|
||||
data_to_append = {
|
||||
"indent": 1 + item.indent,
|
||||
"item_code": item.production_item,
|
||||
"item_name": item.item_name,
|
||||
"qty": item.qty,
|
||||
"document_type": "Work Order" if not subcontracted_item else "Purchase Order",
|
||||
"document_name": docname or "",
|
||||
"bom_level": item.bom_level,
|
||||
"produced_qty": order_details.get((docname, item.production_item), {}).get(
|
||||
"produced_qty", 0
|
||||
),
|
||||
"pending_qty": flt(item.qty)
|
||||
- flt(order_details.get((docname, item.production_item), {}).get("produced_qty", 0)),
|
||||
}
|
||||
if data[-1] and data[-1]["item_code"] == item.production_item:
|
||||
data_to_append["pending_qty"] = data[-1]["pending_qty"] - data_to_append["produced_qty"]
|
||||
data.append(data_to_append)
|
||||
def get_document_row(doc, indent):
|
||||
return {
|
||||
"indent": indent,
|
||||
"item_code": doc.item_code,
|
||||
"item_name": doc.item_name,
|
||||
"sales_order": doc.get("sales_order"),
|
||||
"document_type": doc.document_type,
|
||||
"document_name": doc.document_name,
|
||||
"status": doc.status,
|
||||
"qty": flt(doc.qty),
|
||||
"produced_qty": flt(doc.produced_qty),
|
||||
"pending_qty": flt(doc.qty) - flt(doc.produced_qty),
|
||||
}
|
||||
|
||||
|
||||
def get_work_order_details(filters, order_details):
|
||||
for row in frappe.get_all(
|
||||
def get_work_orders(filters):
|
||||
work_orders = frappe.get_all(
|
||||
"Work Order",
|
||||
filters={"production_plan": filters.get("production_plan"), "docstatus": 1},
|
||||
fields=["name", "produced_qty", "production_plan", "production_item", "sales_order"],
|
||||
):
|
||||
order_details.setdefault((row.name, row.production_item), row)
|
||||
fields=[
|
||||
"name",
|
||||
"qty",
|
||||
"produced_qty",
|
||||
"status",
|
||||
"sales_order",
|
||||
"production_item as item_code",
|
||||
"item_name",
|
||||
"production_plan_item",
|
||||
"production_plan_sub_assembly_item",
|
||||
],
|
||||
)
|
||||
|
||||
for row in work_orders:
|
||||
row.document_type = "Work Order"
|
||||
row.document_name = row.name
|
||||
|
||||
return work_orders
|
||||
|
||||
|
||||
def get_purchase_order_details(filters, order_details):
|
||||
for row in frappe.get_all(
|
||||
"Purchase Order Item",
|
||||
filters={"production_plan": filters.get("production_plan"), "docstatus": 1},
|
||||
fields=["parent", "qty", "received_qty as produced_qty", "item_code", "fg_item", "fg_item_qty"],
|
||||
):
|
||||
if row.fg_item:
|
||||
row.produced_qty /= row.qty / row.fg_item_qty or 1
|
||||
order_details.setdefault((row.parent, row.fg_item or row.item_code), row)
|
||||
def get_purchase_orders(filters):
|
||||
po_item = frappe.qb.DocType("Purchase Order Item")
|
||||
purchase_order = frappe.qb.DocType("Purchase Order")
|
||||
|
||||
purchase_orders = (
|
||||
frappe.qb.from_(po_item)
|
||||
.inner_join(purchase_order)
|
||||
.on(po_item.parent == purchase_order.name)
|
||||
.select(
|
||||
po_item.parent.as_("document_name"),
|
||||
po_item.qty.as_("order_qty"),
|
||||
po_item.received_qty,
|
||||
po_item.item_code.as_("po_item_code"),
|
||||
po_item.item_name.as_("po_item_name"),
|
||||
po_item.fg_item,
|
||||
po_item.fg_item_qty,
|
||||
po_item.production_plan_sub_assembly_item,
|
||||
purchase_order.status,
|
||||
)
|
||||
.where((po_item.production_plan == filters.get("production_plan")) & (po_item.docstatus == 1))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
return [get_purchase_order_row(row) for row in purchase_orders]
|
||||
|
||||
|
||||
def get_purchase_order_row(row):
|
||||
produced_qty = flt(row.received_qty)
|
||||
if row.fg_item:
|
||||
produced_qty = flt(row.received_qty) / (flt(row.order_qty) / flt(row.fg_item_qty) or 1)
|
||||
|
||||
item_code = row.fg_item or row.po_item_code
|
||||
return frappe._dict(
|
||||
{
|
||||
"document_type": "Purchase Order",
|
||||
"document_name": row.document_name,
|
||||
"status": row.status,
|
||||
"item_code": item_code,
|
||||
"item_name": frappe.get_cached_value("Item", item_code, "item_name"),
|
||||
"qty": flt(row.fg_item_qty) if row.fg_item else flt(row.order_qty),
|
||||
"produced_qty": produced_qty,
|
||||
"production_plan_sub_assembly_item": row.production_plan_sub_assembly_item,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_column(filters):
|
||||
return [
|
||||
{
|
||||
"label": _("Finished Good"),
|
||||
"label": _("Item Code"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "item_code",
|
||||
"width": 240,
|
||||
"options": "Item",
|
||||
},
|
||||
{"label": _("Item Name"), "fieldtype": "data", "fieldname": "item_name", "width": 150},
|
||||
{"label": _("Item Name"), "fieldtype": "Data", "fieldname": "item_name", "width": 180},
|
||||
{
|
||||
"label": _("Sales Order"),
|
||||
"options": "Sales Order",
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "sales_order",
|
||||
"width": 100,
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Document Type"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "document_type",
|
||||
"width": 150,
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Document Name"),
|
||||
@@ -173,6 +183,7 @@ def get_column(filters):
|
||||
"options": "document_type",
|
||||
"width": 180,
|
||||
},
|
||||
{"label": _("Status"), "fieldtype": "Data", "fieldname": "status", "width": 110},
|
||||
{"label": _("BOM Level"), "fieldtype": "Int", "fieldname": "bom_level", "width": 100},
|
||||
{"label": _("Order Qty"), "fieldtype": "Float", "fieldname": "qty", "width": 120},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user