Linkage
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -6936,6 +6936,81 @@ description</t></is></c>
|
||||
'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')
|
||||
|
||||
@@ -363,7 +363,7 @@
|
||||
<style:table-row-properties style:min-row-height="0.011cm"/>
|
||||
</style:style>
|
||||
<style:style style:name="LKFinancialSummary.7" style:family="table-row">
|
||||
<style:table-row-properties style:min-row-height="0.191cm"/>
|
||||
<style:table-row-properties style:min-row-height="0.11cm"/>
|
||||
</style:style>
|
||||
<style:style style:name="LKSection_5f_3" style:display-name="LKSection_3" style:family="table">
|
||||
<style:table-properties style:width="28.6cm" table:align="margins"/>
|
||||
@@ -581,10 +581,10 @@
|
||||
<style:table-column-properties style:column-width="4.015cm" style:rel-column-width="9198*"/>
|
||||
</style:style>
|
||||
<style:style style:name="LKFinancialDetails.C" style:family="table-column">
|
||||
<style:table-column-properties style:column-width="5.62cm" style:rel-column-width="12877*"/>
|
||||
<style:table-column-properties style:column-width="6.744cm" style:rel-column-width="15453*"/>
|
||||
</style:style>
|
||||
<style:style style:name="LKFinancialDetails.D" style:family="table-column">
|
||||
<style:table-column-properties style:column-width="4.214cm" style:rel-column-width="9657*"/>
|
||||
<style:table-column-properties style:column-width="3.09cm" style:rel-column-width="7081*"/>
|
||||
</style:style>
|
||||
<style:style style:name="LKFinancialDetails.E" style:family="table-column">
|
||||
<style:table-column-properties style:column-width="3.11cm" style:rel-column-width="7126*"/>
|
||||
@@ -1110,8 +1110,8 @@
|
||||
<style:text-properties style:font-name="Arial" fo:font-size="11pt"/>
|
||||
</style:style>
|
||||
<style:style style:name="P85" style:family="paragraph" style:parent-style-name="Standard">
|
||||
<style:paragraph-properties fo:margin-top="0cm" fo:margin-bottom="0cm" style:contextual-spacing="false" fo:line-height="90%"/>
|
||||
<style:text-properties style:font-name="Arial" fo:font-size="10pt" fo:font-weight="bold"/>
|
||||
<style:paragraph-properties fo:margin-top="0cm" fo:margin-bottom="0cm" style:contextual-spacing="false" fo:line-height="100%"/>
|
||||
<style:text-properties style:font-name="Arial" fo:font-size="11pt" fo:font-weight="bold"/>
|
||||
</style:style>
|
||||
<style:style style:name="P86" style:family="paragraph" style:parent-style-name="Standard">
|
||||
<style:paragraph-properties fo:margin-top="0cm" fo:margin-bottom="0cm" style:contextual-spacing="false" fo:line-height="88%"/>
|
||||
@@ -1186,11 +1186,11 @@
|
||||
<style:text-properties style:font-name="Arial" fo:font-size="8pt" fo:font-weight="bold"/>
|
||||
</style:style>
|
||||
<style:style style:name="P105" style:family="paragraph" style:parent-style-name="Standard">
|
||||
<style:paragraph-properties fo:margin-top="0cm" fo:margin-bottom="0cm" style:contextual-spacing="false" fo:line-height="90%"/>
|
||||
<style:paragraph-properties fo:margin-top="0cm" fo:margin-bottom="0cm" style:contextual-spacing="false" fo:line-height="72%"/>
|
||||
<style:text-properties style:font-name="Arial" fo:font-size="7pt"/>
|
||||
</style:style>
|
||||
<style:style style:name="P106" style:family="paragraph" style:parent-style-name="Standard">
|
||||
<style:paragraph-properties fo:line-height="90%" fo:text-align="end" style:justify-single-word="false"/>
|
||||
<style:paragraph-properties fo:line-height="72%" fo:text-align="end" style:justify-single-word="false"/>
|
||||
<style:text-properties style:font-name="Arial" fo:font-size="7pt"/>
|
||||
</style:style>
|
||||
<style:style style:name="P107" style:family="paragraph" style:parent-style-name="Standard">
|
||||
|
||||
Reference in New Issue
Block a user