mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-26 16:34:46 +00:00
Merge pull request #51451 from rohitwaghchaure/fixed-removed-forecasting_method
chore: removed forecasting method holt winter
This commit is contained in:
@@ -9,7 +9,6 @@
|
|||||||
"naming_series",
|
"naming_series",
|
||||||
"company",
|
"company",
|
||||||
"posting_date",
|
"posting_date",
|
||||||
"forecasting_method",
|
|
||||||
"column_break_xdcy",
|
"column_break_xdcy",
|
||||||
"from_date",
|
"from_date",
|
||||||
"frequency",
|
"frequency",
|
||||||
@@ -146,13 +145,6 @@
|
|||||||
"options": "Planned\nMPS Generated\nCancelled",
|
"options": "Planned\nMPS Generated\nCancelled",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "Holt-Winters",
|
|
||||||
"fieldname": "forecasting_method",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"label": "Forecasting Method",
|
|
||||||
"options": "Holt-Winters\nManual"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "Monthly",
|
"default": "Monthly",
|
||||||
"fieldname": "frequency",
|
"fieldname": "frequency",
|
||||||
@@ -166,7 +158,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-12-24 11:40:44.263700",
|
"modified": "2026-01-02 15:09:32.165242",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Sales Forecast",
|
"name": "Sales Forecast",
|
||||||
|
|||||||
@@ -2,13 +2,10 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import pandas as pd
|
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.query_builder.functions import DateFormat, Sum, YearWeek
|
from frappe.utils import add_to_date
|
||||||
from frappe.utils import add_to_date, cint, date_diff, flt
|
|
||||||
from frappe.utils.nestedset import get_descendants_of
|
|
||||||
|
|
||||||
|
|
||||||
class SalesForecast(Document):
|
class SalesForecast(Document):
|
||||||
@@ -25,7 +22,6 @@ class SalesForecast(Document):
|
|||||||
amended_from: DF.Link | None
|
amended_from: DF.Link | None
|
||||||
company: DF.Link
|
company: DF.Link
|
||||||
demand_number: DF.Int
|
demand_number: DF.Int
|
||||||
forecasting_method: DF.Literal["Holt-Winters", "Manual"]
|
|
||||||
frequency: DF.Literal["Weekly", "Monthly"]
|
frequency: DF.Literal["Weekly", "Monthly"]
|
||||||
from_date: DF.Date
|
from_date: DF.Date
|
||||||
items: DF.Table[SalesForecastItem]
|
items: DF.Table[SalesForecastItem]
|
||||||
@@ -39,46 +35,6 @@ class SalesForecast(Document):
|
|||||||
def on_discard(self):
|
def on_discard(self):
|
||||||
self.db_set("status", "Cancelled")
|
self.db_set("status", "Cancelled")
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
self.validate_demand_qty()
|
|
||||||
|
|
||||||
def validate_demand_qty(self):
|
|
||||||
if self.forecasting_method == "Manual":
|
|
||||||
return
|
|
||||||
|
|
||||||
for row in self.items:
|
|
||||||
demand_qty = row.forecast_qty + flt(row.adjust_qty)
|
|
||||||
if row.demand_qty != demand_qty:
|
|
||||||
row.demand_qty = demand_qty
|
|
||||||
|
|
||||||
def get_sales_data(self):
|
|
||||||
to_date = self.from_date
|
|
||||||
from_date = add_to_date(to_date, years=-3)
|
|
||||||
|
|
||||||
doctype = frappe.qb.DocType("Sales Order")
|
|
||||||
child_doctype = frappe.qb.DocType("Sales Order Item")
|
|
||||||
|
|
||||||
query = (
|
|
||||||
frappe.qb.from_(doctype)
|
|
||||||
.inner_join(child_doctype)
|
|
||||||
.on(child_doctype.parent == doctype.name)
|
|
||||||
.select(child_doctype.item_code, Sum(child_doctype.qty).as_("qty"), doctype.transaction_date)
|
|
||||||
.where((doctype.docstatus == 1) & (doctype.transaction_date.between(from_date, to_date)))
|
|
||||||
.groupby(child_doctype.item_code)
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.selected_items:
|
|
||||||
items = [item.item_code for item in self.selected_items]
|
|
||||||
query = query.where(child_doctype.item_code.isin(items))
|
|
||||||
|
|
||||||
if self.parent_warehouse:
|
|
||||||
warehouses = get_descendants_of("Warehouse", self.parent_warehouse)
|
|
||||||
query = query.where(child_doctype.warehouse.isin(warehouses))
|
|
||||||
|
|
||||||
query = query.groupby(doctype.transaction_date)
|
|
||||||
|
|
||||||
return query.run(as_dict=True)
|
|
||||||
|
|
||||||
def generate_manual_demand(self):
|
def generate_manual_demand(self):
|
||||||
forecast_demand = []
|
forecast_demand = []
|
||||||
for row in self.selected_items:
|
for row in self.selected_items:
|
||||||
@@ -107,99 +63,8 @@ class SalesForecast(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def generate_demand(self):
|
def generate_demand(self):
|
||||||
from statsmodels.tsa.holtwinters import ExponentialSmoothing
|
|
||||||
|
|
||||||
self.set("items", [])
|
self.set("items", [])
|
||||||
|
self.generate_manual_demand()
|
||||||
if self.forecasting_method == "Manual":
|
|
||||||
self.generate_manual_demand()
|
|
||||||
return
|
|
||||||
|
|
||||||
sales_data = self.get_sales_data()
|
|
||||||
if not sales_data:
|
|
||||||
frappe.throw(_("No sales data found for the selected items."))
|
|
||||||
|
|
||||||
itemwise_data = self.group_sales_data_by_item(sales_data)
|
|
||||||
|
|
||||||
for item_code, data in itemwise_data.items():
|
|
||||||
seasonal_periods = self.get_seasonal_periods(data)
|
|
||||||
pd_sales_data = pd.DataFrame({"item": data.item, "date": data.date, "qty": data.qty})
|
|
||||||
|
|
||||||
resample_val = "M" if self.frequency == "Monthly" else "W"
|
|
||||||
_sales_data = pd_sales_data.set_index("date").resample(resample_val).sum()["qty"]
|
|
||||||
|
|
||||||
model = ExponentialSmoothing(
|
|
||||||
_sales_data, trend="add", seasonal="add", seasonal_periods=seasonal_periods
|
|
||||||
)
|
|
||||||
|
|
||||||
fit = model.fit()
|
|
||||||
forecast = fit.forecast(self.demand_number)
|
|
||||||
|
|
||||||
forecast_data = forecast.to_dict()
|
|
||||||
if forecast_data:
|
|
||||||
self.add_sales_forecast_item(item_code, forecast_data)
|
|
||||||
|
|
||||||
def add_sales_forecast_item(self, item_code, forecast_data):
|
|
||||||
item_details = frappe.db.get_value(
|
|
||||||
"Item", item_code, ["item_name", "stock_uom as uom", "name as item_code"], as_dict=True
|
|
||||||
)
|
|
||||||
|
|
||||||
uom_whole_number = frappe.get_cached_value("UOM", item_details.uom, "must_be_whole_number")
|
|
||||||
|
|
||||||
for date, qty in forecast_data.items():
|
|
||||||
if uom_whole_number:
|
|
||||||
qty = round(qty)
|
|
||||||
|
|
||||||
item_details.update(
|
|
||||||
{
|
|
||||||
"delivery_date": date,
|
|
||||||
"forecast_qty": qty,
|
|
||||||
"demand_qty": qty,
|
|
||||||
"warehouse": self.parent_warehouse,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.append("items", item_details)
|
|
||||||
|
|
||||||
def get_seasonal_periods(self, data):
|
|
||||||
days = date_diff(data["end_date"], data["start_date"])
|
|
||||||
if self.frequency == "Monthly":
|
|
||||||
months = (days / 365) * 12
|
|
||||||
seasonal_periods = cint(months / 2)
|
|
||||||
if seasonal_periods > 12:
|
|
||||||
seasonal_periods = 12
|
|
||||||
else:
|
|
||||||
weeks = days / 7
|
|
||||||
seasonal_periods = cint(weeks / 2)
|
|
||||||
if seasonal_periods > 52:
|
|
||||||
seasonal_periods = 52
|
|
||||||
|
|
||||||
return seasonal_periods
|
|
||||||
|
|
||||||
def group_sales_data_by_item(self, sales_data):
|
|
||||||
"""
|
|
||||||
Group sales data by item code and calculate total quantity sold.
|
|
||||||
"""
|
|
||||||
itemwise_data = frappe._dict({})
|
|
||||||
for row in sales_data:
|
|
||||||
if row.item_code not in itemwise_data:
|
|
||||||
itemwise_data[row.item_code] = frappe._dict(
|
|
||||||
{
|
|
||||||
"start_date": row.transaction_date,
|
|
||||||
"item": [],
|
|
||||||
"date": [],
|
|
||||||
"qty": [],
|
|
||||||
"end_date": "",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
item_data = itemwise_data[row.item_code]
|
|
||||||
item_data["item"].append(row.item_code)
|
|
||||||
item_data["date"].append(pd.to_datetime(row.transaction_date))
|
|
||||||
item_data["qty"].append(row.qty)
|
|
||||||
item_data["end_date"] = row.transaction_date
|
|
||||||
|
|
||||||
return itemwise_data
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ dependencies = [
|
|||||||
|
|
||||||
# MT940 parser for bank statements
|
# MT940 parser for bank statements
|
||||||
"mt-940>=4.26.0",
|
"mt-940>=4.26.0",
|
||||||
"pandas~=2.3.3",
|
|
||||||
"statsmodels~=0.14.6",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
Reference in New Issue
Block a user