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 @@
-
+
-
+