This commit is contained in:
2026-06-17 12:25:21 +02:00
parent ee183e23c5
commit a754e15e32
6 changed files with 97 additions and 63 deletions

View File

@@ -8,7 +8,7 @@ from trytond.tools import is_full_text, lstrip_wildcard
from trytond.transaction import Transaction, inactive_records from trytond.transaction import Transaction, inactive_records
from decimal import getcontext, Decimal, ROUND_UP, ROUND_HALF_UP from decimal import getcontext, Decimal, ROUND_UP, ROUND_HALF_UP
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
from sql.conditionals import Case from sql.conditionals import Case, Coalesce
from sql import Column, Literal, Null from sql import Column, Literal, Null
from sql.functions import CurrentTimestamp, DateTrunc from sql.functions import CurrentTimestamp, DateTrunc
from trytond.wizard import Button, StateTransition, StateView, Wizard from trytond.wizard import Button, StateTransition, StateView, Wizard
@@ -1515,10 +1515,10 @@ class FeeLots(ModelSQL,ModelView):
class FeeReport( class FeeReport(
ModelSQL, ModelView): ModelSQL, ModelView):
"Fee Report" "Fee Report"
__name__ = 'fee.report' __name__ = 'fee.report'
r_purchase_line = fields.Many2One('purchase.line', "Purchase line") r_purchase_line = fields.Many2One('purchase.line', "Purchase line")
r_sale_line = fields.Many2One('sale.line', "Sale line") r_sale_line = fields.Many2One('sale.line', "Sale line")
r_shipment_in = fields.Many2One('stock.shipment.in', "Shipment in") r_shipment_in = fields.Many2One('stock.shipment.in', "Shipment in")
r_shipment_out = fields.Many2One('stock.shipment.out', "Shipment out") r_shipment_out = fields.Many2One('stock.shipment.out', "Shipment out")
r_shipment_internal = fields.Many2One('stock.shipment.internal', "Shipment internal") r_shipment_internal = fields.Many2One('stock.shipment.internal', "Shipment internal")
@@ -1540,9 +1540,10 @@ class FeeReport(
('pcost', '% cost price'), ('pcost', '% cost price'),
], 'Mode', required=True) ], 'Mode', required=True)
r_fee_quantity = fields.Function(fields.Numeric("Qt",digits=(1,4)),'get_quantity') r_fee_quantity = fields.Function(fields.Numeric("Qt",digits=(1,4)),'get_quantity')
r_fee_unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit') r_fee_unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit')
r_purchase = fields.Many2One('purchase.purchase',"Purchase", ondelete='CASCADE') r_purchase = fields.Many2One('purchase.purchase',"Purchase", ondelete='CASCADE')
r_sale = fields.Many2One('sale.sale',"Sale", ondelete='CASCADE')
r_fee_amount = fields.Function(fields.Numeric("Amount", digits=(1,4)),'get_amount') r_fee_amount = fields.Function(fields.Numeric("Amount", digits=(1,4)),'get_amount')
r_inv = fields.Function(fields.Many2One('account.invoice',"Invoice"),'get_invoice') r_inv = fields.Function(fields.Many2One('account.invoice',"Invoice"),'get_invoice')
r_dn_cn = fields.Function(fields.Many2One('account.invoice',"DN/CN"), r_dn_cn = fields.Function(fields.Many2One('account.invoice',"DN/CN"),
@@ -1617,16 +1618,26 @@ class FeeReport(
@classmethod @classmethod
def table_query(cls): def table_query(cls):
FeeReport = Pool().get('fee.fee') FeeReport = Pool().get('fee.fee')
fr = FeeReport.__table__() fr = FeeReport.__table__()
Purchase = Pool().get('purchase.purchase') FeeLots = Pool().get('fee.lots')
pu = Purchase.__table__() fl = FeeLots.__table__()
PurchaseLine = Pool().get('purchase.line') PurchaseLine = Pool().get('purchase.line')
pl = PurchaseLine.__table__() pl = PurchaseLine.__table__()
Sale = Pool().get('sale.sale') SaleLine = Pool().get('sale.line')
sa = Sale.__table__() sl = SaleLine.__table__()
SaleLine = Pool().get('sale.line')
sl = SaleLine.__table__() fee_lot_lines = (
fl
.join(pl, 'LEFT', condition=fl.line == pl.id)
.join(sl, 'LEFT', condition=fl.sale_line == sl.id)
.select(
fl.fee.as_('fee'),
Min(fl.line).as_('purchase_line'),
Min(pl.purchase).as_('purchase'),
Min(fl.sale_line).as_('sale_line'),
Min(sl.sale).as_('sale'),
group_by=[fl.fee]))
context = Transaction().context context = Transaction().context
party = context.get('party') party = context.get('party')
@@ -1644,10 +1655,13 @@ class FeeReport(
wh &= (fr.fee_counterparty == party) wh &= (fr.fee_counterparty == party)
if fee_type: if fee_type:
wh &= (fr.fee_type == fee_type) wh &= (fr.fee_type == fee_type)
if purchase: purchase_expr = Coalesce(fee_lot_lines.purchase, fr.purchase)
wh &= (pu.id == purchase) purchase_line_expr = Coalesce(fee_lot_lines.purchase_line, fr.line)
if sale: sale_expr = fee_lot_lines.sale
wh &= (sa.id == sale) if purchase:
wh &= (purchase_expr == purchase)
if sale:
wh &= (sale_expr == sale)
if shipment_in: if shipment_in:
wh &= (fr.shipment_in == shipment_in) wh &= (fr.shipment_in == shipment_in)
if invoice_status == 'not_invoiced': if invoice_status == 'not_invoiced':
@@ -1656,30 +1670,33 @@ class FeeReport(
wh &= ((fr.state == 'invoiced') & (fr.dn_cn == Null)) wh &= ((fr.state == 'invoiced') & (fr.dn_cn == Null))
elif invoice_status == 'invoiced_with_dn_cn': elif invoice_status == 'invoiced_with_dn_cn':
wh &= ((fr.state == 'invoiced') & (fr.dn_cn != Null)) wh &= ((fr.state == 'invoiced') & (fr.dn_cn != Null))
# if shipment_out: # if shipment_out:
# wh &= (fr.shipment_out == shipment_out) # wh &= (fr.shipment_out == shipment_out)
query = fr.join(pl,'LEFT',condition=fr.line == pl.id).join(pu,'LEFT', condition=pl.purchase == pu.id).select( query = fr.join(
Literal(0).as_('create_uid'), fee_lot_lines, 'LEFT', condition=fr.id == fee_lot_lines.fee
CurrentTimestamp().as_('create_date'), ).select(
Literal(None).as_('write_uid'), Literal(0).as_('create_uid'),
Literal(None).as_('write_date'), CurrentTimestamp().as_('create_date'),
fr.id.as_('id'), Literal(None).as_('write_uid'),
fr.line.as_('r_purchase_line'), Literal(None).as_('write_date'),
Literal(None).as_('r_sale_line'), fr.id.as_('id'),
fr.shipment_in.as_('r_shipment_in'), purchase_line_expr.as_('r_purchase_line'),
Literal(None).as_('r_shipment_out'), fee_lot_lines.sale_line.as_('r_sale_line'),
fr.shipment_internal.as_('r_shipment_internal'), fr.shipment_in.as_('r_shipment_in'),
fr.product.as_('r_fee_type'), Literal(None).as_('r_shipment_out'),
fr.supplier.as_('r_fee_counterparty'), fr.shipment_internal.as_('r_shipment_internal'),
fr.product.as_('r_fee_type'),
fr.supplier.as_('r_fee_counterparty'),
fr.type.as_('r_type'), fr.type.as_('r_type'),
fr.p_r.as_('r_fee_paystatus'), fr.p_r.as_('r_fee_paystatus'),
fr.mode.as_('r_mode'), fr.mode.as_('r_mode'),
fr.state.as_('r_state'), fr.state.as_('r_state'),
fr.purchase.as_('r_purchase'), purchase_expr.as_('r_purchase'),
#fr.amount.as_('r_fee_amount'), sale_expr.as_('r_sale'),
fr.price.as_('r_fee_price'), #fr.amount.as_('r_fee_amount'),
fr.currency.as_('r_fee_currency'), fr.price.as_('r_fee_price'),
fr.currency.as_('r_fee_currency'),
#fr.fee_lots.as_('r_fee_lots'), #fr.fee_lots.as_('r_fee_lots'),
#fr.lots.as_('r_lots'), #fr.lots.as_('r_lots'),
where=wh) where=wh)

View File

@@ -159,5 +159,9 @@ this repository contains the full copyright notices and license terms. -->
action="act_fee_report_form" action="act_fee_report_form"
sequence="10" sequence="10"
id="menu_fee_report_form"/> id="menu_fee_report_form"/>
<record model="ir.ui.menu" id="menu_fee_report_form">
<field name="parent" ref="menu_fees"/>
<field name="sequence" eval="10"/>
</record>
</data> </data>
</tryton> </tryton>

View File

@@ -612,24 +612,24 @@ class MtmStrategy(ModelSQL, ModelView):
if comp.price_source_type == 'curve' and comp.price_index: if comp.price_source_type == 'curve' and comp.price_index:
value = Decimal( value = Decimal(
comp.price_index.get_price( comp.price_index.get_price(
dt, dt,
line.unit, line.unit,
self.currency, self.currency,
last=scenario.use_last_price relative_last=scenario.use_last_price
) )
) )
elif comp.price_source_type == 'matrix' and comp.price_matrix: elif comp.price_source_type == 'matrix' and comp.price_matrix:
value = self._get_matrix_price(comp, line, dt) value = self._get_matrix_price(comp, line, dt)
elif comp.price_source_type == 'fixed': elif comp.price_source_type == 'fixed':
value = Decimal(comp.get_price( value = Decimal(comp.get_price(
dt, dt,
line.unit, line.unit,
self.currency, self.currency,
last=scenario.use_last_price relative_last=scenario.use_last_price
)) ))
if comp.ratio: if comp.ratio:
value *= Decimal(comp.ratio) / Decimal(100) value *= Decimal(comp.ratio) / Decimal(100)

View File

@@ -709,6 +709,11 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual( self.assertEqual(
strategy.get_mtm(line, Decimal('10')), strategy.get_mtm(line, Decimal('10')),
Decimal('250.00')) Decimal('250.00'))
strategy.components[0].price_index.get_price.assert_called_once_with(
'2026-03-29',
line.unit,
strategy.currency,
relative_last=True)
@with_transaction() @with_transaction()
def test_get_mtm_uses_fixed_pricing_component(self): def test_get_mtm_uses_fixed_pricing_component(self):
@@ -1205,6 +1210,7 @@ class PurchaseTradeTestCase(ModuleTestCase):
def test_get_strategy_mtm_price_applies_component_ratio(self): def test_get_strategy_mtm_price_applies_component_ratio(self):
'strategy mtm price applies component ratios' 'strategy mtm price applies component ratios'
price_index = Mock(get_price=Mock(return_value=Decimal('100')))
strategy = Mock( strategy = Mock(
scenario=Mock( scenario=Mock(
valuation_date='2026-03-29', valuation_date='2026-03-29',
@@ -1214,7 +1220,7 @@ class PurchaseTradeTestCase(ModuleTestCase):
) )
strategy.components = [Mock( strategy.components = [Mock(
price_source_type='curve', price_source_type='curve',
price_index=Mock(get_price=Mock(return_value=Decimal('100'))), price_index=price_index,
price_matrix=None, price_matrix=None,
ratio=Decimal('25'), ratio=Decimal('25'),
)] )]
@@ -1223,6 +1229,11 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual( self.assertEqual(
valuation_module.Valuation._get_strategy_mtm_price(strategy, line), valuation_module.Valuation._get_strategy_mtm_price(strategy, line),
Decimal('25.0000')) Decimal('25.0000'))
price_index.get_price.assert_called_once_with(
'2026-03-29',
line.unit,
strategy.currency,
relative_last=True)
def test_get_strategy_mtm_price_keeps_signed_component_spread(self): def test_get_strategy_mtm_price_keeps_signed_component_spread(self):
'strategy mtm price keeps signed spread between different components' 'strategy mtm price keeps signed spread between different components'

View File

@@ -522,7 +522,7 @@ class ValuationBase(ModelSQL):
scenario.valuation_date, scenario.valuation_date,
line.unit, line.unit,
strategy.currency, strategy.currency,
last=scenario.use_last_price relative_last=scenario.use_last_price
)) ))
elif comp.price_source_type == 'matrix' and comp.price_matrix: elif comp.price_source_type == 'matrix' and comp.price_matrix:
value = Decimal(strategy._get_matrix_price( value = Decimal(strategy._get_matrix_price(
@@ -533,7 +533,7 @@ class ValuationBase(ModelSQL):
scenario.valuation_date, scenario.valuation_date,
line.unit, line.unit,
strategy.currency, strategy.currency,
last=scenario.use_last_price relative_last=scenario.use_last_price
)) ))
if comp.ratio: if comp.ratio:
@@ -583,7 +583,7 @@ class ValuationBase(ModelSQL):
scenario.valuation_date, scenario.valuation_date,
line.unit, line.unit,
strategy.currency, strategy.currency,
last=scenario.use_last_price)) relative_last=scenario.use_last_price))
previous = cls._previous_curve_price( previous = cls._previous_curve_price(
component.price_index, component.price_index,
scenario.valuation_date, scenario.valuation_date,

View File

@@ -1,6 +1,8 @@
<tree> <tree>
<button name="invoice" string="Action"/> <button name="invoice" string="Action"/>
<field name="r_purchase_line" width="90"/> <field name="r_purchase_line" width="90" tree_invisible="1"/>
<field name="r_purchase" width="100"/>
<field name="r_sale" width="100"/>
<field name="r_shipment_origin" width="120"/> <field name="r_shipment_origin" width="120"/>
<field name="r_fee_type" width="120"/> <field name="r_fee_type" width="120"/>
<field name="r_fee_counterparty" width="100"/> <field name="r_fee_counterparty" width="100"/>