test: use unique item and assert exact forecast in Exponential Smoothing test

This commit is contained in:
Nabin Hait
2026-07-02 13:57:07 +05:30
parent 8b7780d494
commit cc9d94efe8

View File

@@ -8,17 +8,24 @@ from erpnext.manufacturing.report.exponential_smoothing_forecasting.exponential_
execute,
)
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.tests.utils import ERPNextTestSuite
TEST_ITEM = "_Test Item"
FROM_DATE = "2026-06-01"
TO_DATE = "2026-08-31"
SMOOTHING_CONSTANT = 0.5
class TestExponentialSmoothingForecasting(ERPNextTestSuite):
"""Drive real submitted Sales Orders and assert the report buckets the ordered
quantities into the correct historical periods and produces a forecast."""
def setUp(self):
# The forecast query has no lower date bound, so it would pick up any committed
# Sales Order for the item. A uniquely-named item keeps the buckets scoped to
# just this test's orders.
self.item = make_item(properties={"is_stock_item": 1}).name
def test_monthly_qty_forecast_from_sales_orders(self):
# Historical demand: distinct calendar months strictly before FROM_DATE.
# Monthly period keys are derived from the period's last day (e.g. "mar_2026").
@@ -45,26 +52,29 @@ class TestExponentialSmoothingForecasting(ERPNextTestSuite):
for key, qty in history.items():
self.assertEqual(flt(row.get(key)), flt(qty), f"bucket {key} mismatch")
# A forecast is produced for the first future period. The first non-zero
# historical period seeds the forecast at the average of non-zero months,
# so the future forecast must be positive.
# The forecast seeds at the average of the non-zero historical months and then
# smooths through them in order: F = F + a*(actual - F). Asserting the exact
# analytical value pins the smoothing formula (Jun 2026 works out to ~7.2083).
expected_avg = sum(history.values()) / len(history)
self.assertGreater(flt(row.get("forecast_jun_2026")), 0.0)
self.assertLessEqual(flt(row.get("forecast_jun_2026")), max(history.values()))
self.assertAlmostEqual(flt(row.get("avg")), expected_avg, places=6)
forecast = expected_avg
for month in ("mar_2026", "apr_2026", "may_2026"):
forecast = forecast + SMOOTHING_CONSTANT * (history[month] - forecast)
self.assertAlmostEqual(flt(row.get("forecast_jun_2026")), forecast, places=6)
def test_ignores_documents_outside_range_and_other_docstatus(self):
self.create_sales_orders({"2026-05-10": 6})
# A draft SO and a future-dated SO must not contribute to historical demand.
make_sales_order(item_code=TEST_ITEM, qty=100, transaction_date="2026-05-20", do_not_submit=True)
make_sales_order(item_code=TEST_ITEM, qty=100, transaction_date=FROM_DATE)
make_sales_order(item_code=self.item, qty=100, transaction_date="2026-05-20", do_not_submit=True)
make_sales_order(item_code=self.item, qty=100, transaction_date=FROM_DATE)
_columns, row = self.run_report()
self.assertEqual(flt(row.get("may_2026")), 6.0)
def create_sales_orders(self, date_to_qty):
for transaction_date, qty in date_to_qty.items():
make_sales_order(item_code=TEST_ITEM, qty=qty, transaction_date=transaction_date)
make_sales_order(item_code=self.item, qty=qty, transaction_date=transaction_date)
def run_report(self, **extra):
filters = frappe._dict(
@@ -76,16 +86,16 @@ class TestExponentialSmoothingForecasting(ERPNextTestSuite):
"periodicity": "Monthly",
"from_date": FROM_DATE,
"to_date": TO_DATE,
"smoothing_constant": 0.5,
"item_code": TEST_ITEM,
"smoothing_constant": SMOOTHING_CONSTANT,
"item_code": self.item,
}
)
filters.update(extra)
columns, data = execute(filters)[:2]
item_row = next(
(r for r in data if r.get("item_code") == TEST_ITEM),
(r for r in data if r.get("item_code") == self.item),
None,
)
self.assertIsNotNone(item_row, f"{TEST_ITEM} row missing from report output")
self.assertIsNotNone(item_row, f"{self.item} row missing from report output")
return columns, item_row