chore: removed forecasting_method holt winter

This commit is contained in:
Rohit Waghchaure
2026-01-02 15:15:47 +05:30
parent 7bb0ec836f
commit fd5b84fe1a
3 changed files with 3 additions and 148 deletions

View File

@@ -9,7 +9,6 @@
"naming_series",
"company",
"posting_date",
"forecasting_method",
"column_break_xdcy",
"from_date",
"frequency",
@@ -146,13 +145,6 @@
"options": "Planned\nMPS Generated\nCancelled",
"read_only": 1
},
{
"default": "Holt-Winters",
"fieldname": "forecasting_method",
"fieldtype": "Select",
"label": "Forecasting Method",
"options": "Holt-Winters\nManual"
},
{
"default": "Monthly",
"fieldname": "frequency",
@@ -166,7 +158,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-12-24 11:40:44.263700",
"modified": "2026-01-02 15:09:32.165242",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Sales Forecast",

View File

@@ -2,13 +2,10 @@
# For license information, please see license.txt
import frappe
import pandas as pd
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import DateFormat, Sum, YearWeek
from frappe.utils import add_to_date, cint, date_diff, flt
from frappe.utils.nestedset import get_descendants_of
from frappe.utils import add_to_date
class SalesForecast(Document):
@@ -25,7 +22,6 @@ class SalesForecast(Document):
amended_from: DF.Link | None
company: DF.Link
demand_number: DF.Int
forecasting_method: DF.Literal["Holt-Winters", "Manual"]
frequency: DF.Literal["Weekly", "Monthly"]
from_date: DF.Date
items: DF.Table[SalesForecastItem]
@@ -39,46 +35,6 @@ class SalesForecast(Document):
def on_discard(self):
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):
forecast_demand = []
for row in self.selected_items:
@@ -107,99 +63,8 @@ class SalesForecast(Document):
@frappe.whitelist()
def generate_demand(self):
from statsmodels.tsa.holtwinters import ExponentialSmoothing
self.set("items", [])
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
self.generate_manual_demand()
@frappe.whitelist()

View File

@@ -24,8 +24,6 @@ dependencies = [
# MT940 parser for bank statements
"mt-940>=4.26.0",
"pandas~=2.3.3",
"statsmodels~=0.14.6",
]
[build-system]