diff --git a/erpnext/manufacturing/doctype/sales_forecast/sales_forecast.json b/erpnext/manufacturing/doctype/sales_forecast/sales_forecast.json index 550f62ee112..b55a7a1dba6 100644 --- a/erpnext/manufacturing/doctype/sales_forecast/sales_forecast.json +++ b/erpnext/manufacturing/doctype/sales_forecast/sales_forecast.json @@ -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", diff --git a/erpnext/manufacturing/doctype/sales_forecast/sales_forecast.py b/erpnext/manufacturing/doctype/sales_forecast/sales_forecast.py index dd1427bcd70..b0762c99438 100644 --- a/erpnext/manufacturing/doctype/sales_forecast/sales_forecast.py +++ b/erpnext/manufacturing/doctype/sales_forecast/sales_forecast.py @@ -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() diff --git a/pyproject.toml b/pyproject.toml index e3627770446..e450d675f02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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]