diff --git a/modules/purchase_trade/stock.py b/modules/purchase_trade/stock.py index 32b7722..8f4c871 100755 --- a/modules/purchase_trade/stock.py +++ b/modules/purchase_trade/stock.py @@ -1699,6 +1699,79 @@ class ShipmentIn(metaclass=PoolMeta): add_fee(fee) return fees + def _get_report_linkage_fee_trade_line(self, fee): + return ( + getattr(fee, 'line', None) + or getattr(fee, 'sale_line', None) + or (self._get_report_linkage_purchase_lines() + or self._get_report_linkage_sale_lines() + or [None])[0]) + + def _get_report_linkage_fee_trade(self, fee): + trade_line = self._get_report_linkage_fee_trade_line(fee) + return ( + getattr(trade_line, 'purchase', None) + or getattr(trade_line, 'sale', None) + or getattr(fee, 'purchase', None) + or getattr(fee, 'sale', None)) + + @classmethod + def _report_linkage_fee_type(cls, fee): + return getattr(fee, 'type', None) or 'budgeted' + + def _get_report_linkage_fee_pairs(self): + groups = {} + order = [] + fallback_trade = ( + self._get_report_linkage_purchase() + or self._get_report_linkage_sale()) + + for fee in self._get_report_linkage_fees(): + trade = self._get_report_linkage_fee_trade(fee) or fallback_trade + key = ( + self._report_record_key(trade), + self._report_record_key(getattr(fee, 'product', None)), + self._report_record_key(getattr(fee, 'supplier', None)), + ) + if key not in groups: + groups[key] = { + 'trade': trade, + 'budgeted': [], + 'ordered': [], + } + order.append(key) + fee_type = self._report_linkage_fee_type(fee) + if fee_type in ('budgeted', 'ordered'): + groups[key][fee_type].append(fee) + + pairs = [] + for key in order: + group = groups[key] + budgeted = group['budgeted'] + if not budgeted: + continue + ordered = group['ordered'] + estimated = sum( + (self._report_fee_amount(fee) for fee in budgeted), + Decimal('0')) + validated = estimated + if self._report_linkage_is_final and ordered: + validated = sum( + (self._report_fee_amount(fee) for fee in ordered), + Decimal('0')) + pairs.append({ + 'fee': budgeted[0], + 'trade': group['trade'], + 'estimated': estimated, + 'validated': validated, + 'ordered': ordered, + }) + return pairs + + @property + def _report_linkage_is_final(self): + return getattr(self, 'report_linkage_status', None) == 'FINAL' + @classmethod def _report_fee_amount(cls, fee): get_amount = getattr(fee, 'get_amount', None) @@ -1740,20 +1813,32 @@ class ShipmentIn(metaclass=PoolMeta): if sale_lines: rows.append(('Sales', '', sale_total, sale_total, 'major')) - fee_total = Decimal('0') + fee_estimated_total = Decimal('0') + fee_validated_total = Decimal('0') fee_rows = {} - for fee in self._get_report_linkage_fees(): + for pair in self._get_report_linkage_fee_pairs(): + fee = pair['fee'] label = self._report_linkage_fee_label(fee) - amount = self._report_fee_amount(fee) - fee_total += amount - fee_rows[label] = fee_rows.get(label, Decimal('0')) + amount - for label, amount in fee_rows.items(): - rows.append(('Costs', label, amount, amount, 'cost')) + estimated = pair['estimated'] + validated = pair['validated'] + fee_estimated_total += estimated + fee_validated_total += validated + current = fee_rows.get(label, (Decimal('0'), Decimal('0'))) + fee_rows[label] = ( + current[0] + estimated, + current[1] + validated, + ) + for label, amounts in fee_rows.items(): + rows.append(( + 'Costs', label, amounts[0], amounts[1], 'cost')) if rows: - rows.append(('Costs', 'Total', fee_total, fee_total, 'cost')) - pnl = purchase_total + sale_total + fee_total - rows.append(('P&L', '', pnl, pnl, 'pnl')) + rows.append(( + 'Costs', 'Total', fee_estimated_total, + fee_validated_total, 'cost')) + estimated_pnl = purchase_total + sale_total + fee_estimated_total + validated_pnl = purchase_total + sale_total + fee_validated_total + rows.append(('P&L', '', estimated_pnl, validated_pnl, 'pnl')) return rows def _get_report_linkage_movement_rows(self): @@ -1844,12 +1929,17 @@ class ShipmentIn(metaclass=PoolMeta): rows.append(self._get_report_linkage_detail_row( 'A', 'Deal expense', purchase, line, -self._report_line_amount(line))) - for fee in self._get_report_linkage_fees(): - rows.append(self._get_report_linkage_fee_detail_row(fee)) - total = sum((row[7] for row in rows), Decimal('0')) + for pair in self._get_report_linkage_fee_pairs(): + rows.append(self._get_report_linkage_fee_detail_row(pair)) + estimated_total = sum((row[6] for row in rows), Decimal('0')) + validated_total = sum((row[7] for row in rows), Decimal('0')) if rows: - rows.append(('A', 'Total', '', '', '', '', total, total, 'total')) - rows.append(('P&L', '', '', '', '', '', total, total, 'pnl')) + rows.append(( + 'A', 'Total', '', '', '', '', estimated_total, + validated_total, 'total')) + rows.append(( + 'P&L', '', '', '', '', '', estimated_total, + validated_total, 'pnl')) return rows def _get_report_linkage_detail_row(self, group, label, trade, line, amount): @@ -1872,20 +1962,13 @@ class ShipmentIn(metaclass=PoolMeta): 'line', ) - def _get_report_linkage_fee_detail_row(self, fee): + def _get_report_linkage_fee_detail_row(self, pair): + fee = pair['fee'] product = getattr(fee, 'product', None) - trade_line = ( - getattr(fee, 'line', None) - or getattr(fee, 'sale_line', None) - or (self._get_report_linkage_purchase_lines() - or self._get_report_linkage_sale_lines() - or [None])[0]) - trade = ( - getattr(trade_line, 'purchase', None) - or getattr(trade_line, 'sale', None)) + trade_line = self._get_report_linkage_fee_trade_line(fee) + trade = pair.get('trade') or self._get_report_linkage_fee_trade(fee) currency = self._report_currency_code(fee) or self._report_currency_code(trade) unit = self._get_report_unit_text(getattr(fee, 'unit', None)) - amount = self._report_fee_amount(fee) quantity = getattr(fee, 'quantity', None) if quantity in (None, '') and trade_line: quantity = self._report_line_quantity(trade_line) @@ -1899,8 +1982,8 @@ class ShipmentIn(metaclass=PoolMeta): '/'.join(part for part in [currency, unit.upper() if unit else ''] if part)] if part), self._format_report_quantity(quantity or 0), - amount, - amount, + pair['estimated'], + pair['validated'], 'fee', ) @@ -4557,7 +4640,33 @@ class LotReportLinkageRecord: def _get_report_linkage_fees(self): shipment = getattr(self.lot, 'lot_shipment_in', None) - return list(getattr(shipment, 'fees', []) or []) + fees = [] + seen = set() + + def add_fee(fee): + if not fee: + return + key = self._report_record_key(fee) + if key in seen: + return + seen.add(key) + fees.append(fee) + + for fee in getattr(shipment, 'fees', []) or []: + add_fee(fee) + for line in ( + self._get_report_linkage_purchase_lines() + + self._get_report_linkage_sale_lines()): + for fee in getattr(line, 'fees', []) or []: + add_fee(fee) + return fees + + _get_report_linkage_fee_trade_line = ( + ShipmentIn._get_report_linkage_fee_trade_line) + _get_report_linkage_fee_trade = ShipmentIn._get_report_linkage_fee_trade + _get_report_linkage_fee_pairs = ShipmentIn._get_report_linkage_fee_pairs + _report_linkage_fee_type = ShipmentIn._report_linkage_fee_type + _report_linkage_is_final = ShipmentIn._report_linkage_is_final _get_report_linkage_summary_rows = ( ShipmentIn._get_report_linkage_summary_rows) diff --git a/modules/purchase_trade/tests/test_module.py b/modules/purchase_trade/tests/test_module.py index 38f8685..0d8447f 100644 --- a/modules/purchase_trade/tests/test_module.py +++ b/modules/purchase_trade/tests/test_module.py @@ -6936,6 +6936,81 @@ description 'linkage report must not alter the lot.qt ORM model' self.assertFalse(hasattr(stock_module, 'LotQt')) + def test_linkage_report_final_validates_ordered_matching_fee(self): + 'linkage final keeps budgeted estimate and validates matching ordered fee' + ShipmentIn = Pool().get('stock.shipment.in') + shipment = ShipmentIn() + shipment.bl_date = datetime.date(2026, 4, 30) + shipment.fees = [] + shipment.moves = [] + shipment.lotqt = [] + + currency = SimpleNamespace(code='USD') + unit = SimpleNamespace(symbol='MT') + product = SimpleNamespace(id=1, rec_name='Sulphuric Acid') + purchase = SimpleNamespace( + id=10, + number='P-10', + reference='26.0001', + currency=currency, + party=SimpleNamespace(id=20, rec_name='SUPPLIER SA'), + to_location=SimpleNamespace(rec_name='Odda'), + incoterm=SimpleNamespace(code='FOB')) + purchase_line = SimpleNamespace( + id=11, + product=product, + purchase=purchase, + quantity_theorical=Decimal('100'), + quantity=Decimal('100'), + unit_price=Decimal('80'), + unit=unit, + fees=[], + mtm=[]) + fee_product = SimpleNamespace(id=30, name='Maritime freight') + supplier = SimpleNamespace(id=40, rec_name='FREIGHT CO') + budgeted_fee = SimpleNamespace( + id=201, + type='budgeted', + line=purchase_line, + sale_line=None, + product=fee_product, + supplier=supplier, + price=Decimal('100'), + quantity=Decimal('1'), + mode='lumpsum', + p_r='pay') + ordered_fee = SimpleNamespace( + id=202, + type='ordered', + line=purchase_line, + sale_line=None, + product=fee_product, + supplier=supplier, + price=Decimal('125'), + quantity=Decimal('1'), + mode='lumpsum', + p_r='pay') + purchase_line.fees = [budgeted_fee, ordered_fee] + lot = SimpleNamespace( + id=101, + lot_type='physic', + line=purchase_line, + sale_line=None) + shipment.incoming_moves = [SimpleNamespace(lot=lot)] + + provisional_fee_row = [ + row for row in shipment._get_report_linkage_summary_rows() + if row[0] == 'Costs' and row[1] == 'Maritime freight'][0] + self.assertEqual(provisional_fee_row[2], Decimal('-100')) + self.assertEqual(provisional_fee_row[3], Decimal('-100')) + + shipment.report_linkage_status = 'FINAL' + final_fee_row = [ + row for row in shipment._get_report_linkage_summary_rows() + if row[0] == 'Costs' and row[1] == 'Maritime freight'][0] + self.assertEqual(final_fee_row[2], Decimal('-100')) + self.assertEqual(final_fee_row[3], Decimal('-125')) + def test_lot_report_linkage_rejects_non_physical_lines(self): 'lot.report linkage report is only available for physical lines' linkage_report = Pool().get('lot.report.linkage', type='report') diff --git a/modules/stock/linkage.fodt b/modules/stock/linkage.fodt index 8ee8c72..5d19b3f 100644 --- a/modules/stock/linkage.fodt +++ b/modules/stock/linkage.fodt @@ -363,7 +363,7 @@ - + @@ -581,10 +581,10 @@ - + - + @@ -1110,8 +1110,8 @@ - - + + @@ -1186,11 +1186,11 @@ - + - +