mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-14 23:18:40 +00:00
Merge pull request #56320 from frappe/chore/stock-report-test-coverage
test: correctness coverage for Stock Ledger and Stock Projected Qty reports
This commit is contained in:
@@ -4,18 +4,86 @@
|
||||
import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
|
||||
make_serial_item_with_serial,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.report.stock_ledger.stock_ledger import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
WAREHOUSE = "_Test Warehouse - _TC"
|
||||
|
||||
class TestStockLedgerReeport(ERPNextTestSuite):
|
||||
def setUp(self) -> None:
|
||||
make_serial_item_with_serial(self, "_Test Stock Report Serial Item")
|
||||
self.filters = frappe._dict(
|
||||
|
||||
class TestStockLedgerReport(ERPNextTestSuite):
|
||||
"""Correctness tests for the Stock Ledger report.
|
||||
|
||||
A shared `make_movements`/`run` pair keeps each test small without persisting
|
||||
any data: movements are created per test and rolled back, while the report runs
|
||||
read-only.
|
||||
"""
|
||||
|
||||
def make_movements(self, item_code, movements):
|
||||
for movement in movements:
|
||||
make_stock_entry(item_code=item_code, **movement)
|
||||
|
||||
def run_report(self, item_code, from_date=None, to_date=None):
|
||||
filters = frappe._dict(
|
||||
company="_Test Company",
|
||||
from_date=today(),
|
||||
to_date=add_days(today(), 30),
|
||||
item_code=["_Test Stock Report Serial Item"],
|
||||
from_date=from_date or add_days(today(), -1),
|
||||
to_date=to_date or today(),
|
||||
item_code=[item_code],
|
||||
warehouse=WAREHOUSE,
|
||||
)
|
||||
return list(execute(filters)[1])
|
||||
|
||||
def test_in_out_quantities_and_running_balance(self):
|
||||
item = make_item().name
|
||||
self.make_movements(
|
||||
item,
|
||||
[
|
||||
{"qty": 10, "to_warehouse": WAREHOUSE, "basic_rate": 100},
|
||||
{"qty": 4, "from_warehouse": WAREHOUSE},
|
||||
],
|
||||
)
|
||||
|
||||
rows = self.run_report(item)
|
||||
receipt = next(row for row in rows if row.get("in_qty"))
|
||||
issue = next(row for row in rows if row.get("out_qty"))
|
||||
|
||||
self.assertEqual(receipt["in_qty"], 10)
|
||||
self.assertEqual(receipt["qty_after_transaction"], 10)
|
||||
self.assertEqual(issue["out_qty"], -4)
|
||||
self.assertEqual(issue["qty_after_transaction"], 6)
|
||||
|
||||
def test_opening_balance_reflects_movements_before_from_date(self):
|
||||
item = make_item().name
|
||||
self.make_movements(
|
||||
item,
|
||||
[
|
||||
{
|
||||
"qty": 10,
|
||||
"to_warehouse": WAREHOUSE,
|
||||
"basic_rate": 100,
|
||||
"posting_date": add_days(today(), -10),
|
||||
},
|
||||
{"qty": 4, "from_warehouse": WAREHOUSE, "posting_date": today()},
|
||||
],
|
||||
)
|
||||
|
||||
rows = self.run_report(item, from_date=add_days(today(), -5), to_date=today())
|
||||
|
||||
# the receipt predates the range, so it surfaces as the opening balance
|
||||
self.assertEqual(rows[0]["item_code"], "'Opening'")
|
||||
self.assertEqual(rows[0]["qty_after_transaction"], 10)
|
||||
|
||||
# the in-range issue draws down from the opening balance
|
||||
issue = next(row for row in rows if row.get("out_qty"))
|
||||
self.assertEqual(issue["qty_after_transaction"], 6)
|
||||
|
||||
def test_filters_to_requested_item_only(self):
|
||||
item_a = make_item().name
|
||||
item_b = make_item().name
|
||||
self.make_movements(item_a, [{"qty": 5, "to_warehouse": WAREHOUSE, "basic_rate": 100}])
|
||||
self.make_movements(item_b, [{"qty": 7, "to_warehouse": WAREHOUSE, "basic_rate": 100}])
|
||||
|
||||
rows = self.run_report(item_a)
|
||||
item_codes = {row["item_code"] for row in rows if row.get("voucher_no")}
|
||||
self.assertEqual(item_codes, {item_a})
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.report.stock_projected_qty.stock_projected_qty import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
WAREHOUSE = "_Test Warehouse - _TC"
|
||||
|
||||
|
||||
class TestStockProjectedQty(ERPNextTestSuite):
|
||||
"""Correctness tests for the Stock Projected Qty report (a current-Bin snapshot)."""
|
||||
|
||||
def run_report(self, item_code):
|
||||
filters = frappe._dict(company="_Test Company", item_code=item_code)
|
||||
columns, data = execute(filters)
|
||||
fields = [column["fieldname"] for column in columns]
|
||||
return [dict(zip(fields, row, strict=False)) for row in data]
|
||||
|
||||
def test_projected_qty_includes_actual_and_ordered(self):
|
||||
item = make_item().name
|
||||
make_stock_entry(item_code=item, qty=10, to_warehouse=WAREHOUSE, basic_rate=100)
|
||||
create_purchase_order(item_code=item, qty=5, rate=100, warehouse=WAREHOUSE)
|
||||
|
||||
row = self.run_report(item)[0]
|
||||
self.assertEqual(row["actual_qty"], 10)
|
||||
self.assertEqual(row["ordered_qty"], 5)
|
||||
self.assertEqual(row["projected_qty"], 15)
|
||||
|
||||
def test_projected_qty_includes_all_quantity_components(self):
|
||||
"""projected_qty = actual + ordered + requested + planned
|
||||
- reserved - reserved_for_production - reserved_for_subcontract - reserved_for_production_plan
|
||||
and every component is surfaced as its own column."""
|
||||
item = make_item().name
|
||||
make_stock_entry(item_code=item, qty=100, to_warehouse=WAREHOUSE, basic_rate=100)
|
||||
|
||||
bin_doc = frappe.get_doc("Bin", {"item_code": item, "warehouse": WAREHOUSE})
|
||||
bin_doc.update(
|
||||
{
|
||||
"actual_qty": 100,
|
||||
"ordered_qty": 50,
|
||||
"indented_qty": 30, # requested
|
||||
"planned_qty": 20,
|
||||
"reserved_qty": 10,
|
||||
"reserved_qty_for_production": 8,
|
||||
"reserved_qty_for_sub_contract": 6,
|
||||
"reserved_qty_for_production_plan": 4,
|
||||
}
|
||||
)
|
||||
bin_doc.set_projected_qty()
|
||||
bin_doc.db_update()
|
||||
|
||||
# 100 + 50 + 30 + 20 - 10 - 8 - 6 - 4
|
||||
self.assertEqual(bin_doc.projected_qty, 172)
|
||||
|
||||
row = self.run_report(item)[0]
|
||||
self.assertEqual(row["actual_qty"], 100)
|
||||
self.assertEqual(row["ordered_qty"], 50)
|
||||
self.assertEqual(row["indented_qty"], 30)
|
||||
self.assertEqual(row["planned_qty"], 20)
|
||||
self.assertEqual(row["reserved_qty"], 10)
|
||||
self.assertEqual(row["reserved_qty_for_production"], 8)
|
||||
self.assertEqual(row["reserved_qty_for_sub_contract"], 6)
|
||||
self.assertEqual(row["reserved_qty_for_production_plan"], 4)
|
||||
self.assertEqual(row["projected_qty"], 172)
|
||||
|
||||
def test_shortage_qty_from_reorder_level(self):
|
||||
item = make_item().name
|
||||
doc = frappe.get_doc("Item", item)
|
||||
doc.append(
|
||||
"reorder_levels",
|
||||
{
|
||||
"warehouse": WAREHOUSE,
|
||||
"warehouse_reorder_level": 20,
|
||||
"warehouse_reorder_qty": 15,
|
||||
"material_request_type": "Purchase",
|
||||
},
|
||||
)
|
||||
doc.save()
|
||||
make_stock_entry(item_code=item, qty=10, to_warehouse=WAREHOUSE, basic_rate=100)
|
||||
|
||||
row = self.run_report(item)[0]
|
||||
self.assertEqual(row["re_order_level"], 20)
|
||||
self.assertEqual(row["projected_qty"], 10)
|
||||
self.assertEqual(row["shortage_qty"], 10) # reorder level 20 - projected 10
|
||||
|
||||
def test_item_filter_returns_only_requested_item(self):
|
||||
item_a = make_item().name
|
||||
item_b = make_item().name
|
||||
make_stock_entry(item_code=item_a, qty=5, to_warehouse=WAREHOUSE, basic_rate=100)
|
||||
make_stock_entry(item_code=item_b, qty=7, to_warehouse=WAREHOUSE, basic_rate=100)
|
||||
|
||||
rows = self.run_report(item_a)
|
||||
self.assertEqual({row["item_code"] for row in rows}, {item_a})
|
||||
Reference in New Issue
Block a user