Merge pull request #21724 from rohitwaghchaure/production-forecasting
feat: production forecasting using exponential smoothing method
This commit is contained in:
commit
df78e29957
@ -19,7 +19,7 @@ from six import itervalues
|
|||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions, get_dimension_with_children
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions, get_dimension_with_children
|
||||||
|
|
||||||
def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_end_date, filter_based_on, periodicity, accumulated_values=False,
|
def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_end_date, filter_based_on, periodicity, accumulated_values=False,
|
||||||
company=None, reset_period_on_fy_change=True):
|
company=None, reset_period_on_fy_change=True, ignore_fiscal_year=False):
|
||||||
"""Get a list of dict {"from_date": from_date, "to_date": to_date, "key": key, "label": label}
|
"""Get a list of dict {"from_date": from_date, "to_date": to_date, "key": key, "label": label}
|
||||||
Periodicity can be (Yearly, Quarterly, Monthly)"""
|
Periodicity can be (Yearly, Quarterly, Monthly)"""
|
||||||
|
|
||||||
@ -67,8 +67,9 @@ def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_
|
|||||||
# if a fiscal year ends before a 12 month period
|
# if a fiscal year ends before a 12 month period
|
||||||
period.to_date = year_end_date
|
period.to_date = year_end_date
|
||||||
|
|
||||||
period.to_date_fiscal_year = get_fiscal_year(period.to_date, company=company)[0]
|
if not ignore_fiscal_year:
|
||||||
period.from_date_fiscal_year_start_date = get_fiscal_year(period.from_date, company=company)[1]
|
period.to_date_fiscal_year = get_fiscal_year(period.to_date, company=company)[0]
|
||||||
|
period.from_date_fiscal_year_start_date = get_fiscal_year(period.from_date, company=company)[1]
|
||||||
|
|
||||||
period_list.append(period)
|
period_list.append(period)
|
||||||
|
|
||||||
|
@ -43,10 +43,11 @@
|
|||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Desk Page",
|
"doctype": "Desk Page",
|
||||||
"extends_another_page": 0,
|
"extends_another_page": 0,
|
||||||
|
"hide_custom": 0,
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"label": "Manufacturing",
|
"label": "Manufacturing",
|
||||||
"modified": "2020-05-19 14:05:59.100891",
|
"modified": "2020-05-20 11:50:20.029056",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Manufacturing",
|
"name": "Manufacturing",
|
||||||
@ -88,6 +89,17 @@
|
|||||||
"stats_filter": "{ \n \"status\": [\"not in\", [\"Completed\"]]\n}",
|
"stats_filter": "{ \n \"status\": [\"not in\", [\"Completed\"]]\n}",
|
||||||
"type": "DocType"
|
"type": "DocType"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "Dashboard",
|
||||||
|
"link_to": "Manufacturing",
|
||||||
|
"restrict_to_domain": "Manufacturing",
|
||||||
|
"type": "Dashboard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Forecasting",
|
||||||
|
"link_to": "Exponential Smoothing Forecasting",
|
||||||
|
"type": "Report"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Work Order Summary",
|
"label": "Work Order Summary",
|
||||||
"link_to": "Work Order Summary",
|
"link_to": "Work Order Summary",
|
||||||
@ -95,10 +107,14 @@
|
|||||||
"type": "Report"
|
"type": "Report"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Dashboard",
|
"label": "BOM Stock Report",
|
||||||
"link_to": "Manufacturing",
|
"link_to": "BOM Stock Report",
|
||||||
"restrict_to_domain": "Manufacturing",
|
"type": "Report"
|
||||||
"type": "Dashboard"
|
},
|
||||||
|
{
|
||||||
|
"label": "Production Planning Report",
|
||||||
|
"link_to": "Production Planning Report",
|
||||||
|
"type": "Report"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
frappe.query_reports["Exponential Smoothing Forecasting"] = {
|
||||||
|
"filters": [
|
||||||
|
{
|
||||||
|
"fieldname":"company",
|
||||||
|
"label": __("Company"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Company",
|
||||||
|
"reqd": 1,
|
||||||
|
"default": frappe.defaults.get_user_default("Company")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"from_date",
|
||||||
|
"label": __("From Date"),
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"default": frappe.datetime.get_today(),
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"to_date",
|
||||||
|
"label": __("To Date"),
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"default": frappe.datetime.add_months(frappe.datetime.get_today(), 12),
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"based_on_document",
|
||||||
|
"label": __("Based On Document"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": ["Sales Order", "Delivery Note", "Quotation"],
|
||||||
|
"default": "Sales Order",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"based_on_field",
|
||||||
|
"label": __("Based On"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": ["Qty", "Amount"],
|
||||||
|
"default": "Qty",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"no_of_years",
|
||||||
|
"label": __("Based On Data ( in years )"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": [3, 6, 9],
|
||||||
|
"default": 3,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "periodicity",
|
||||||
|
"label": __("Periodicity"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": [
|
||||||
|
{ "value": "Monthly", "label": __("Monthly") },
|
||||||
|
{ "value": "Quarterly", "label": __("Quarterly") },
|
||||||
|
{ "value": "Half-Yearly", "label": __("Half-Yearly") },
|
||||||
|
{ "value": "Yearly", "label": __("Yearly") }
|
||||||
|
],
|
||||||
|
"default": "Yearly",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"smoothing_constant",
|
||||||
|
"label": __("Smoothing Constant"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
|
||||||
|
"reqd": 1,
|
||||||
|
"default": 0.3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"item_code",
|
||||||
|
"label": __("Item Code"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Item"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"warehouse",
|
||||||
|
"label": __("Warehouse"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Warehouse",
|
||||||
|
get_query: () => {
|
||||||
|
var company = frappe.query_report.get_filter_value('company');
|
||||||
|
if (company) {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
'company': company
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"add_total_row": 0,
|
||||||
|
"creation": "2020-05-15 05:18:55.838030",
|
||||||
|
"disable_prepared_report": 0,
|
||||||
|
"disabled": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Report",
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": "Yes",
|
||||||
|
"letter_head": "",
|
||||||
|
"modified": "2020-05-15 05:18:55.838030",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Manufacturing",
|
||||||
|
"name": "Exponential Smoothing Forecasting",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"prepared_report": 0,
|
||||||
|
"ref_doctype": "Sales Order",
|
||||||
|
"report_name": "Exponential Smoothing Forecasting",
|
||||||
|
"report_type": "Script Report",
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "Manufacturing User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Stock User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Manufacturing Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Stock Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Sales Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Sales User"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,222 @@
|
|||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe, erpnext
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import flt, nowdate, add_years, cint, getdate
|
||||||
|
from erpnext.accounts.report.financial_statements import get_period_list
|
||||||
|
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
|
||||||
|
|
||||||
|
def execute(filters=None):
|
||||||
|
return ForecastingReport(filters).execute_report()
|
||||||
|
|
||||||
|
class ExponentialSmoothingForecast(object):
|
||||||
|
def forecast_future_data(self):
|
||||||
|
for key, value in self.period_wise_data.items():
|
||||||
|
forecast_data = []
|
||||||
|
for period in self.period_list:
|
||||||
|
forecast_key = "forecast_" + period.key
|
||||||
|
|
||||||
|
if value.get(period.key) and not forecast_data:
|
||||||
|
value[forecast_key] = flt(value.get("avg", 0)) or flt(value.get(period.key))
|
||||||
|
|
||||||
|
# will be use to forecaset next period
|
||||||
|
forecast_data.append([value.get(period.key), value.get(forecast_key)])
|
||||||
|
elif forecast_data:
|
||||||
|
previous_period_data = forecast_data[-1]
|
||||||
|
value[forecast_key] = (previous_period_data[1] +
|
||||||
|
flt(self.filters.smoothing_constant) * (
|
||||||
|
flt(previous_period_data[0]) - flt(previous_period_data[1])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
class ForecastingReport(ExponentialSmoothingForecast):
|
||||||
|
def __init__(self, filters=None):
|
||||||
|
self.filters = frappe._dict(filters or {})
|
||||||
|
self.data = []
|
||||||
|
self.doctype = self.filters.based_on_document
|
||||||
|
self.child_doctype = self.doctype + " Item"
|
||||||
|
self.based_on_field = ("qty"
|
||||||
|
if self.filters.based_on_field == "Qty" else "amount")
|
||||||
|
self.fieldtype = "Float" if self.based_on_field == "qty" else "Currency"
|
||||||
|
self.company_currency = erpnext.get_company_currency(self.filters.company)
|
||||||
|
|
||||||
|
def execute_report(self):
|
||||||
|
self.prepare_periodical_data()
|
||||||
|
self.forecast_future_data()
|
||||||
|
self.data = self.period_wise_data.values()
|
||||||
|
self.add_total()
|
||||||
|
|
||||||
|
columns = self.get_columns()
|
||||||
|
charts = self.get_chart_data()
|
||||||
|
summary_data = self.get_summary_data()
|
||||||
|
|
||||||
|
return columns, self.data, None, charts, summary_data
|
||||||
|
|
||||||
|
def prepare_periodical_data(self):
|
||||||
|
self.period_wise_data = {}
|
||||||
|
|
||||||
|
from_date = add_years(self.filters.from_date, cint(self.filters.no_of_years) * -1)
|
||||||
|
self.period_list = get_period_list(from_date, self.filters.to_date,
|
||||||
|
from_date, self.filters.to_date, None, self.filters.periodicity, ignore_fiscal_year=True)
|
||||||
|
|
||||||
|
order_data = self.get_data_for_forecast() or []
|
||||||
|
|
||||||
|
for entry in order_data:
|
||||||
|
key = (entry.item_code, entry.warehouse)
|
||||||
|
if key not in self.period_wise_data:
|
||||||
|
self.period_wise_data[key] = entry
|
||||||
|
|
||||||
|
period_data = self.period_wise_data[key]
|
||||||
|
for period in self.period_list:
|
||||||
|
# check if posting date is within the period
|
||||||
|
if (entry.posting_date >= period.from_date and entry.posting_date <= period.to_date):
|
||||||
|
period_data[period.key] = period_data.get(period.key, 0.0) + flt(entry.get(self.based_on_field))
|
||||||
|
|
||||||
|
for key, value in self.period_wise_data.items():
|
||||||
|
list_of_period_value = [value.get(p.key, 0) for p in self.period_list]
|
||||||
|
|
||||||
|
if list_of_period_value:
|
||||||
|
value["avg"] = sum(list_of_period_value) / len(list_of_period_value)
|
||||||
|
|
||||||
|
def get_data_for_forecast(self):
|
||||||
|
cond = ""
|
||||||
|
if self.filters.item_code:
|
||||||
|
cond = " AND soi.item_code = %s" %(frappe.db.escape(self.filters.item_code))
|
||||||
|
|
||||||
|
warehouses = []
|
||||||
|
if self.filters.warehouse:
|
||||||
|
warehouses = get_child_warehouses(self.filters.warehouse)
|
||||||
|
cond += " AND soi.warehouse in ({})".format(','.join(['%s'] * len(warehouses)))
|
||||||
|
|
||||||
|
input_data = [self.filters.from_date, self.filters.company]
|
||||||
|
if warehouses:
|
||||||
|
input_data.extend(warehouses)
|
||||||
|
|
||||||
|
date_field = "posting_date" if self.doctype == "Delivery Note" else "transaction_date"
|
||||||
|
|
||||||
|
return frappe.db.sql("""
|
||||||
|
SELECT
|
||||||
|
so.{date_field} as posting_date, soi.item_code, soi.warehouse,
|
||||||
|
soi.item_name, soi.stock_qty as qty, soi.base_amount as amount
|
||||||
|
FROM
|
||||||
|
`tab{doc}` so, `tab{child_doc}` soi
|
||||||
|
WHERE
|
||||||
|
so.docstatus = 1 AND so.name = soi.parent AND
|
||||||
|
so.{date_field} < %s AND so.company = %s {cond}
|
||||||
|
""".format(doc=self.doctype, child_doc=self.child_doctype, date_field=date_field, cond=cond),
|
||||||
|
tuple(input_data), as_dict=1)
|
||||||
|
|
||||||
|
def add_total(self):
|
||||||
|
total_row = {
|
||||||
|
"item_code": _(frappe.bold("Total Quantity"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for value in self.data:
|
||||||
|
for period in self.period_list:
|
||||||
|
forecast_key = "forecast_" + period.key
|
||||||
|
if forecast_key not in total_row:
|
||||||
|
total_row.setdefault(forecast_key, 0.0)
|
||||||
|
|
||||||
|
if period.key not in total_row:
|
||||||
|
total_row.setdefault(period.key, 0.0)
|
||||||
|
|
||||||
|
total_row[forecast_key] += value.get(forecast_key, 0.0)
|
||||||
|
total_row[period.key] += value.get(period.key, 0.0)
|
||||||
|
|
||||||
|
self.data.append(total_row)
|
||||||
|
|
||||||
|
def get_columns(self):
|
||||||
|
columns = [{
|
||||||
|
"label": _("Item Code"),
|
||||||
|
"options": "Item",
|
||||||
|
"fieldname": "item_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"width": 130
|
||||||
|
}, {
|
||||||
|
"label": _("Warehouse"),
|
||||||
|
"options": "Warehouse",
|
||||||
|
"fieldname": "warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"width": 130
|
||||||
|
}]
|
||||||
|
|
||||||
|
width = 150 if self.filters.periodicity in ['Yearly', "Half-Yearly", "Quarterly"] else 100
|
||||||
|
for period in self.period_list:
|
||||||
|
if (self.filters.periodicity in ['Yearly', "Half-Yearly", "Quarterly"]
|
||||||
|
or period.from_date >= getdate(self.filters.from_date)):
|
||||||
|
|
||||||
|
forecast_key = 'forecast_' + period.key
|
||||||
|
|
||||||
|
columns.append({
|
||||||
|
"label": _(period.label),
|
||||||
|
"fieldname": forecast_key,
|
||||||
|
"fieldtype": self.fieldtype,
|
||||||
|
"width": width,
|
||||||
|
"default": 0.0
|
||||||
|
})
|
||||||
|
|
||||||
|
return columns
|
||||||
|
|
||||||
|
def get_chart_data(self):
|
||||||
|
if not self.data: return
|
||||||
|
|
||||||
|
labels = []
|
||||||
|
self.total_demand = []
|
||||||
|
self.total_forecast = []
|
||||||
|
self.total_history_forecast = []
|
||||||
|
self.total_future_forecast = []
|
||||||
|
|
||||||
|
for period in self.period_list:
|
||||||
|
forecast_key = "forecast_" + period.key
|
||||||
|
|
||||||
|
labels.append(_(period.label))
|
||||||
|
|
||||||
|
if period.from_date < getdate(self.filters.from_date):
|
||||||
|
self.total_demand.append(self.data[-1].get(period.key, 0))
|
||||||
|
self.total_history_forecast.append(self.data[-1].get(forecast_key, 0))
|
||||||
|
else:
|
||||||
|
self.total_future_forecast.append(self.data[-1].get(forecast_key, 0))
|
||||||
|
|
||||||
|
self.total_forecast.append(self.data[-1].get(forecast_key, 0))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"data": {
|
||||||
|
"labels": labels,
|
||||||
|
"datasets": [
|
||||||
|
{
|
||||||
|
"name": "Demand",
|
||||||
|
"values": self.total_demand
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Forecast",
|
||||||
|
"values": self.total_forecast
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "line"
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_summary_data(self):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"value": sum(self.total_demand),
|
||||||
|
"label": _("Total Demand (Past Data)"),
|
||||||
|
"currency": self.company_currency,
|
||||||
|
"datatype": self.fieldtype
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": sum(self.total_history_forecast),
|
||||||
|
"label": _("Total Forecast (Past Data)"),
|
||||||
|
"currency": self.company_currency,
|
||||||
|
"datatype": self.fieldtype
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": sum(self.total_future_forecast),
|
||||||
|
"indicator": "Green",
|
||||||
|
"label": _("Total Forecast (Future Data)"),
|
||||||
|
"currency": self.company_currency,
|
||||||
|
"datatype": self.fieldtype
|
||||||
|
}
|
||||||
|
]
|
Loading…
x
Reference in New Issue
Block a user