From 1349bcc5353777d34928e3defad64961df4fbef1 Mon Sep 17 00:00:00 2001 From: Dipen Gala Date: Sat, 13 Jun 2026 11:34:28 +0530 Subject: [PATCH] refactor(stock-balance): reduce alt UOM to single column, fix i18n, add tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 (cherry picked from commit 021b80705775a12861872a2742f7c6c430538a5a) --- .../report/stock_balance/stock_balance.py | 59 +++++++++---------- .../stock_balance/test_stock_balance.py | 46 +++++++++++++++ 2 files changed, 74 insertions(+), 31 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 2c728ae9d8b..5a3e8911fcf 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -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 diff --git a/erpnext/stock/report/stock_balance/test_stock_balance.py b/erpnext/stock/report/stock_balance/test_stock_balance.py index ac545e03df3..16ecb971620 100644 --- a/erpnext/stock/report/stock_balance/test_stock_balance.py +++ b/erpnext/stock/report/stock_balance/test_stock_balance.py @@ -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],