mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-21 18:29:57 +00:00
refactor(stock-balance): reduce alt UOM to single column, fix i18n, add tests
- Reduce from 2 alternate UOM columns to 1 (first alt UOM by idx)
- Fix broken translation strings: replace _(f"...{slot}") with
_("...") — f-strings inside _() are never extracted by bench
get-untranslated, breaking non-English installations
- Simplify fieldnames: alt_uom_1/alt_uom_1_bal_qty → alt_uom/alt_uom_bal_qty
- Add 4 test cases covering: single alt UOM, no alt UOM, disabled filter,
and multiple alt UOMs (first-wins behaviour)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 021b807057)
This commit is contained in:
@@ -605,7 +605,7 @@ class StockBalanceReport:
|
||||
add_additional_uom_columns(self.columns, self.data, self.filters.include_uom, conversion_factors)
|
||||
|
||||
def add_alt_uom_columns(self) -> None:
|
||||
"""Add up to 2 alternate UOM balance columns per item after the Balance Qty column."""
|
||||
"""Add an alternate UOM balance column after the Balance Qty column."""
|
||||
if not self.filters.get("show_alt_uom_balance"):
|
||||
return
|
||||
|
||||
@@ -613,7 +613,6 @@ class StockBalanceReport:
|
||||
if not item_alt_uom_map:
|
||||
return
|
||||
|
||||
# Insert columns right after bal_qty (in reverse order so slot 1 comes first)
|
||||
bal_qty_idx = next(
|
||||
(i for i, col in enumerate(self.columns) if isinstance(col, dict) and col.get("fieldname") == "bal_qty"),
|
||||
None,
|
||||
@@ -621,37 +620,35 @@ class StockBalanceReport:
|
||||
if bal_qty_idx is None:
|
||||
return
|
||||
|
||||
for slot in (2, 1):
|
||||
self.columns.insert(
|
||||
bal_qty_idx + 1,
|
||||
{
|
||||
"label": _(f"Balance Qty (Alt UOM {slot})"),
|
||||
"fieldname": f"alt_uom_{slot}_bal_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 140,
|
||||
},
|
||||
)
|
||||
self.columns.insert(
|
||||
bal_qty_idx + 1,
|
||||
{
|
||||
"label": _(f"Alt UOM {slot}"),
|
||||
"fieldname": f"alt_uom_{slot}",
|
||||
"fieldtype": "Data",
|
||||
"width": 90,
|
||||
},
|
||||
)
|
||||
# Insert in reverse so "Alt UOM" name column appears before qty column
|
||||
self.columns.insert(
|
||||
bal_qty_idx + 1,
|
||||
{
|
||||
"label": _("Balance Qty (Alt UOM)"),
|
||||
"fieldname": "alt_uom_bal_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 140,
|
||||
},
|
||||
)
|
||||
self.columns.insert(
|
||||
bal_qty_idx + 1,
|
||||
{
|
||||
"label": _("Alt UOM"),
|
||||
"fieldname": "alt_uom",
|
||||
"fieldtype": "Data",
|
||||
"width": 90,
|
||||
},
|
||||
)
|
||||
|
||||
for row in self.data:
|
||||
alt_uoms = item_alt_uom_map.get(row.item_code, [])
|
||||
for slot in (1, 2):
|
||||
idx = slot - 1
|
||||
if idx < len(alt_uoms):
|
||||
uom, factor = alt_uoms[idx]["uom"], flt(alt_uoms[idx]["conversion_factor"])
|
||||
row[f"alt_uom_{slot}"] = uom
|
||||
row[f"alt_uom_{slot}_bal_qty"] = flt(row.get("bal_qty", 0)) / factor if factor else 0.0
|
||||
else:
|
||||
row[f"alt_uom_{slot}"] = ""
|
||||
row[f"alt_uom_{slot}_bal_qty"] = 0.0
|
||||
if alt_uoms:
|
||||
uom, factor = alt_uoms[0]["uom"], flt(alt_uoms[0]["conversion_factor"])
|
||||
row["alt_uom"] = uom
|
||||
row["alt_uom_bal_qty"] = flt(row.get("bal_qty", 0)) / factor if factor else 0.0
|
||||
else:
|
||||
row["alt_uom"] = ""
|
||||
row["alt_uom_bal_qty"] = 0.0
|
||||
|
||||
def get_item_alt_uom_map(self) -> dict:
|
||||
"""Return {item_code: [{uom, conversion_factor}, ...]} for alternate UOMs (excluding stock UOM)."""
|
||||
@@ -679,7 +676,7 @@ class StockBalanceReport:
|
||||
result: dict = {}
|
||||
for row in rows:
|
||||
result.setdefault(row.parent, [])
|
||||
if len(result[row.parent]) < 2: # keep up to 2 alternate UOMs
|
||||
if not result[row.parent]: # keep only the first alternate UOM (lowest idx)
|
||||
result[row.parent].append({"uom": row.uom, "conversion_factor": row.conversion_factor})
|
||||
|
||||
return result
|
||||
|
||||
@@ -167,6 +167,52 @@ class TestStockBalance(ERPNextTestSuite):
|
||||
self.assertPartialDictEq(attributes, rows[0])
|
||||
self.assertInvariants(rows)
|
||||
|
||||
def test_alt_uom_balance_single_uom(self):
|
||||
"""Alt UOM columns show correct name and converted qty for an item with one alternate UOM."""
|
||||
self.item.append("uoms", {"conversion_factor": 12, "uom": "Box"})
|
||||
self.item.save()
|
||||
|
||||
self.generate_stock_ledger(self.item.name, [_dict(qty=24, rate=10)])
|
||||
|
||||
rows = stock_balance(self.filters.update({"show_alt_uom_balance": 1}))
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertEqual(rows[0].get("alt_uom"), "Box")
|
||||
self.assertAlmostEqual(rows[0].get("alt_uom_bal_qty"), 2.0) # 24 / 12
|
||||
|
||||
def test_alt_uom_balance_no_alternate_uom(self):
|
||||
"""Alt UOM columns are not added when no items in the report have alt UOMs."""
|
||||
self.generate_stock_ledger(self.item.name, [_dict(qty=5, rate=10)])
|
||||
|
||||
columns, _ = execute(self.filters.update({"show_alt_uom_balance": 1}))
|
||||
col_fieldnames = [c.get("fieldname") for c in columns if isinstance(c, dict)]
|
||||
self.assertNotIn("alt_uom", col_fieldnames)
|
||||
self.assertNotIn("alt_uom_bal_qty", col_fieldnames)
|
||||
|
||||
def test_alt_uom_balance_filter_disabled(self):
|
||||
"""No alt UOM columns are injected when show_alt_uom_balance is not set."""
|
||||
self.item.append("uoms", {"conversion_factor": 12, "uom": "Box"})
|
||||
self.item.save()
|
||||
|
||||
self.generate_stock_ledger(self.item.name, [_dict(qty=24, rate=10)])
|
||||
|
||||
columns, _ = execute(self.filters)
|
||||
col_fieldnames = [c.get("fieldname") for c in columns if isinstance(c, dict)]
|
||||
self.assertNotIn("alt_uom", col_fieldnames)
|
||||
self.assertNotIn("alt_uom_bal_qty", col_fieldnames)
|
||||
|
||||
def test_alt_uom_balance_uses_first_alternate_uom(self):
|
||||
"""When an item has multiple alt UOMs, only the first (lowest idx) is shown."""
|
||||
self.item.append("uoms", {"conversion_factor": 12, "uom": "Box"})
|
||||
self.item.append("uoms", {"conversion_factor": 144, "uom": "Carton"})
|
||||
self.item.save()
|
||||
|
||||
self.generate_stock_ledger(self.item.name, [_dict(qty=144, rate=10)])
|
||||
|
||||
rows = stock_balance(self.filters.update({"show_alt_uom_balance": 1}))
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertEqual(rows[0].get("alt_uom"), "Box")
|
||||
self.assertAlmostEqual(rows[0].get("alt_uom_bal_qty"), 12.0) # 144 / 12, not 144 / 144
|
||||
|
||||
def test_stock_ageing_data_accepts_batchwise_valuation_slots(self):
|
||||
fifo_queue = [
|
||||
["SA-BATCH-NEWER", 1, 2.0, "2021-12-05", 20.0],
|
||||
|
||||
Reference in New Issue
Block a user