From b1b75eca3d5010d94b96f2467e8a81e7f3e86a3f Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Fri, 16 Jan 2026 00:01:34 +0530 Subject: [PATCH 1/2] fix(manufacturing): handle None value for actual_end_date (cherry picked from commit 16f09141da954f16b9ea232c75df1530f0f8644a) --- .../production_analytics.py | 160 ++++++++---------- 1 file changed, 70 insertions(+), 90 deletions(-) diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py index 5c84a2dc8d1..f0fe0e87a78 100644 --- a/erpnext/manufacturing/report/production_analytics/production_analytics.py +++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py @@ -8,6 +8,8 @@ from frappe.utils import getdate, today from erpnext.stock.report.stock_analytics.stock_analytics import get_period, get_period_date_ranges +WORK_ORDER_STATUS_LIST = ["Not Started", "Overdue", "Pending", "Completed", "Closed", "Stopped"] + def execute(filters=None): columns = get_columns(filters) @@ -16,119 +18,97 @@ def execute(filters=None): def get_columns(filters): - columns = [{"label": _("Status"), "fieldname": "Status", "fieldtype": "Data", "width": 140}] - + columns = [{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 140}] ranges = get_period_date_ranges(filters) for _dummy, end_date in ranges: period = get_period(end_date, filters) - columns.append({"label": _(period), "fieldname": scrub(period), "fieldtype": "Float", "width": 120}) return columns -def get_periodic_data(filters, entry): - periodic_data = { - "Not Started": {}, - "Overdue": {}, - "Pending": {}, - "Completed": {}, - "Closed": {}, - "Stopped": {}, - } +def get_work_orders(filters): + from_date = filters.get("from_date") + to_date = filters.get("to_date") - ranges = get_period_date_ranges(filters) + WorkOrder = frappe.qb.DocType("Work Order") - for from_date, end_date in ranges: - period = get_period(end_date, filters) - for d in entry: - if getdate(from_date) <= getdate(d.creation) <= getdate(end_date) and d.status not in [ - "Draft", - "Submitted", - "Completed", - "Cancelled", - ]: - if d.status in ["Not Started", "Closed", "Stopped"]: - periodic_data = update_periodic_data(periodic_data, d.status, period) - elif getdate(today()) > getdate(d.planned_end_date): - periodic_data = update_periodic_data(periodic_data, "Overdue", period) - elif getdate(today()) < getdate(d.planned_end_date): - periodic_data = update_periodic_data(periodic_data, "Pending", period) - - if ( - getdate(from_date) <= getdate(d.actual_end_date) <= getdate(end_date) - and d.status == "Completed" - ): - periodic_data = update_periodic_data(periodic_data, "Completed", period) - - return periodic_data - - -def update_periodic_data(periodic_data, status, period): - if periodic_data.get(status).get(period): - periodic_data[status][period] += 1 - else: - periodic_data[status][period] = 1 - - return periodic_data + return ( + frappe.qb.from_(WorkOrder) + .select(WorkOrder.creation, WorkOrder.actual_end_date, WorkOrder.planned_end_date, WorkOrder.status) + .where( + (WorkOrder.docstatus == 1) + & (WorkOrder.company == filters.get("company")) + & ( + (WorkOrder.creation.between(from_date, to_date)) + | (WorkOrder.actual_end_date.between(from_date, to_date)) + ) + ) + .run(as_dict=True) + ) def get_data(filters, columns): - data = [] - entry = frappe.get_all( - "Work Order", - fields=[ - "creation", - "actual_end_date", - "planned_end_date", - "status", - ], - filters={"docstatus": 1, "company": filters["company"]}, - ) + ranges = build_ranges(filters) + period_labels = [pd for _fd, _td, pd in ranges] + periodic_data = {status: {pd: 0 for pd in period_labels} for status in WORK_ORDER_STATUS_LIST} + entries = get_work_orders(filters) - periodic_data = get_periodic_data(filters, entry) + for d in entries: + if d.status == "Completed": + if not d.actual_end_date: + continue - labels = ["Not Started", "Overdue", "Pending", "Completed", "Closed", "Stopped"] - chart_data = get_chart_data(periodic_data, columns) - ranges = get_period_date_ranges(filters) + if period := get_period_for_date(getdate(d.actual_end_date), ranges): + periodic_data["Completed"][period] += 1 + continue - for label in labels: - work = {} - work["Status"] = _(label) - for _dummy, end_date in ranges: - period = get_period(end_date, filters) - if periodic_data.get(label).get(period): - work[scrub(period)] = periodic_data.get(label).get(period) + creation_date = getdate(d.creation) + period = get_period_for_date(creation_date, ranges) + if not period: + continue + + if d.status in ("Not Started", "Closed", "Stopped"): + periodic_data[d.status][period] += 1 + else: + if d.planned_end_date and getdate(today()) > getdate(d.planned_end_date): + periodic_data["Overdue"][period] += 1 else: - work[scrub(period)] = 0.0 - data.append(work) + periodic_data["Pending"][period] += 1 - return data, chart_data + data = [] + for status in WORK_ORDER_STATUS_LIST: + row = {"status": _(status)} + for _fd, _td, pd in ranges: + row[scrub(pd)] = periodic_data[status].get(pd, 0) + data.append(row) + + chart = get_chart_data(periodic_data, columns) + return data, chart + + +def get_period_for_date(date, ranges): + for from_date, to_date, period in ranges: + if from_date <= date <= to_date: + return period + return None + + +def build_ranges(filters): + ranges = [] + for from_date, end_date in get_period_date_ranges(filters): + period = get_period(end_date, filters) + ranges.append((getdate(from_date), getdate(end_date), period)) + return ranges def get_chart_data(periodic_data, columns): labels = [d.get("label") for d in columns[1:]] - not_start, overdue, pending, completed, closed, stopped = [], [], [], [], [], [] datasets = [] + for status in WORK_ORDER_STATUS_LIST: + values = [periodic_data.get(status, {}).get(label, 0) for label in labels] + datasets.append({"name": _(status), "values": values}) - for d in labels: - not_start.append(periodic_data.get("Not Started").get(d)) - overdue.append(periodic_data.get("Overdue").get(d)) - pending.append(periodic_data.get("Pending").get(d)) - completed.append(periodic_data.get("Completed").get(d)) - closed.append(periodic_data.get("Closed").get(d)) - stopped.append(periodic_data.get("Stopped").get(d)) - - datasets.append({"name": _("Not Started"), "values": not_start}) - datasets.append({"name": _("Overdue"), "values": overdue}) - datasets.append({"name": _("Pending"), "values": pending}) - datasets.append({"name": _("Completed"), "values": completed}) - datasets.append({"name": _("Closed"), "values": closed}) - datasets.append({"name": _("Stopped"), "values": stopped}) - - chart = {"data": {"labels": labels, "datasets": datasets}} - chart["type"] = "line" - - return chart + return {"data": {"labels": labels, "datasets": datasets}, "type": "line"} From b99ca486d7bc7fcaea771ea79a508b0be14e5bfd Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Wed, 4 Feb 2026 12:18:03 +0530 Subject: [PATCH 2/2] fix(manufacturing): fix chart period keys (cherry picked from commit 27091e516881c1749cbaf3101ff53664fcd9d086) --- .../production_analytics.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/erpnext/manufacturing/report/production_analytics/production_analytics.py b/erpnext/manufacturing/report/production_analytics/production_analytics.py index f0fe0e87a78..41fd4dd0e82 100644 --- a/erpnext/manufacturing/report/production_analytics/production_analytics.py +++ b/erpnext/manufacturing/report/production_analytics/production_analytics.py @@ -51,7 +51,7 @@ def get_work_orders(filters): def get_data(filters, columns): ranges = build_ranges(filters) - period_labels = [pd for _fd, _td, pd in ranges] + period_labels = [scrub(pd) for _fd, _td, pd in ranges] periodic_data = {status: {pd: 0 for pd in period_labels} for status in WORK_ORDER_STATUS_LIST} entries = get_work_orders(filters) @@ -60,12 +60,12 @@ def get_data(filters, columns): if not d.actual_end_date: continue - if period := get_period_for_date(getdate(d.actual_end_date), ranges): + if period := scrub(get_period_for_date(getdate(d.actual_end_date), ranges)): periodic_data["Completed"][period] += 1 continue creation_date = getdate(d.creation) - period = get_period_for_date(creation_date, ranges) + period = scrub(get_period_for_date(creation_date, ranges)) if not period: continue @@ -80,8 +80,8 @@ def get_data(filters, columns): data = [] for status in WORK_ORDER_STATUS_LIST: row = {"status": _(status)} - for _fd, _td, pd in ranges: - row[scrub(pd)] = periodic_data[status].get(pd, 0) + for _fd, _td, period in ranges: + row[scrub(period)] = periodic_data[status].get(scrub(period), 0) data.append(row) chart = get_chart_data(periodic_data, columns) @@ -104,11 +104,12 @@ def build_ranges(filters): def get_chart_data(periodic_data, columns): - labels = [d.get("label") for d in columns[1:]] + period_labels = [d.get("label") for d in columns[1:]] + period_fieldnames = [d.get("fieldname") for d in columns[1:]] datasets = [] for status in WORK_ORDER_STATUS_LIST: - values = [periodic_data.get(status, {}).get(label, 0) for label in labels] + values = [periodic_data.get(status, {}).get(fieldname, 0) for fieldname in period_fieldnames] datasets.append({"name": _(status), "values": values}) - return {"data": {"labels": labels, "datasets": datasets}, "type": "line"} + return {"data": {"labels": period_labels, "datasets": datasets}, "type": "line"}