Valuation fee
This commit is contained in:
@@ -35,12 +35,13 @@ class Fee(ModelSQL,ModelView):
|
||||
"Fee"
|
||||
__name__ = 'fee.fee'
|
||||
|
||||
line = fields.Many2One('purchase.line',"Line")
|
||||
line = fields.Many2One('purchase.line',"Line", ondelete='CASCADE')
|
||||
source_rule = fields.Many2One('fee.rule', "Source Rule", readonly=True)
|
||||
generated_by_rule = fields.Boolean("Generated by Rule", readonly=True)
|
||||
shipment_in = fields.Many2One('stock.shipment.in')
|
||||
shipment_out = fields.Many2One('stock.shipment.out')
|
||||
shipment_internal = fields.Many2One('stock.shipment.internal')
|
||||
shipment_in = fields.Many2One('stock.shipment.in', ondelete='CASCADE')
|
||||
shipment_out = fields.Many2One('stock.shipment.out', ondelete='CASCADE')
|
||||
shipment_internal = fields.Many2One(
|
||||
'stock.shipment.internal', ondelete='CASCADE')
|
||||
currency = fields.Many2One('currency.currency',"Currency", required=True)
|
||||
supplier = fields.Many2One('party.party',"Supplier", required=True)
|
||||
type = fields.Selection([
|
||||
|
||||
@@ -81,7 +81,7 @@ class Estimated(metaclass=PoolMeta):
|
||||
shipment_out = fields.Many2One('stock.shipment.out')
|
||||
shipment_internal = fields.Many2One('stock.shipment.internal')
|
||||
purchase = fields.Many2One('purchase.purchase',"Purchase")
|
||||
line = fields.Many2One('purchase.line',"Line")
|
||||
line = fields.Many2One('purchase.line',"Line")
|
||||
|
||||
class Currency(metaclass=PoolMeta):
|
||||
"Currency"
|
||||
@@ -103,12 +103,12 @@ class Unit(metaclass=PoolMeta):
|
||||
def default_concatenate(cls):
|
||||
return False
|
||||
|
||||
class FeeLots(metaclass=PoolMeta):
|
||||
|
||||
"Fee lots"
|
||||
__name__ = 'fee.lots'
|
||||
|
||||
line = fields.Many2One('purchase.line',"Line")
|
||||
class FeeLots(metaclass=PoolMeta):
|
||||
|
||||
"Fee lots"
|
||||
__name__ = 'fee.lots'
|
||||
|
||||
line = fields.Many2One('purchase.line',"Line", ondelete='CASCADE')
|
||||
|
||||
class Component(metaclass=PoolMeta):
|
||||
"Component"
|
||||
|
||||
@@ -51,7 +51,7 @@ class FeeLots(metaclass=PoolMeta):
|
||||
"Fee lots"
|
||||
__name__ = 'fee.lots'
|
||||
|
||||
sale_line = fields.Many2One('sale.line',"Line")
|
||||
sale_line = fields.Many2One('sale.line',"Line", ondelete='CASCADE')
|
||||
|
||||
class Backtoback(metaclass=PoolMeta):
|
||||
'Back To Back'
|
||||
@@ -2802,7 +2802,7 @@ class Fee(metaclass=PoolMeta):
|
||||
"Fee"
|
||||
__name__ = 'fee.fee'
|
||||
|
||||
sale_line = fields.Many2One('sale.line',"Line")
|
||||
sale_line = fields.Many2One('sale.line',"Line", ondelete='CASCADE')
|
||||
|
||||
class SaleAllocationsWizard(Wizard):
|
||||
'Open Allocations report from Sale without modal'
|
||||
|
||||
@@ -1218,6 +1218,8 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
currency=currency,
|
||||
shipment_in=None,
|
||||
sale_line=None,
|
||||
shipment_out=None,
|
||||
shipment_internal=None,
|
||||
unit=unit,
|
||||
)
|
||||
fee.get_amount.return_value = None
|
||||
@@ -1228,6 +1230,44 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
purchase=Mock(id=7, currency=currency),
|
||||
unit=unit,
|
||||
)
|
||||
fee.line = line
|
||||
fee_lots = Mock()
|
||||
fee_lots.search.return_value = [Mock(fee=fee)]
|
||||
|
||||
with patch('trytond.modules.purchase_trade.valuation.Pool') as PoolMock:
|
||||
PoolMock.return_value.get.side_effect = lambda name: {
|
||||
'ir.date': Mock(today=Mock(return_value=datetime.date(2026, 4, 23))),
|
||||
'currency.currency': Mock(),
|
||||
'fee.lots': fee_lots,
|
||||
'lot.qt': Mock(),
|
||||
}[name]
|
||||
|
||||
values = Valuation.create_pnl_fee_from_line(line)
|
||||
|
||||
self.assertEqual(values[0]['amount'], Decimal('0'))
|
||||
self.assertEqual(values[0]['fee'], fee.id)
|
||||
|
||||
def test_create_pnl_fee_from_line_ignores_orphan_fee_lot(self):
|
||||
'purchase fee valuation ignores fees without business attachment'
|
||||
Valuation = Pool().get('valuation.valuation')
|
||||
currency = Mock(id=1)
|
||||
unit = Mock(id=2)
|
||||
lot = Mock(id=5, sale_line=None, lot_type='virtual')
|
||||
lot.get_current_quantity_converted.return_value = Decimal('10')
|
||||
fee = Mock(
|
||||
line=None,
|
||||
sale_line=None,
|
||||
shipment_in=None,
|
||||
shipment_out=None,
|
||||
shipment_internal=None,
|
||||
)
|
||||
line = Mock(
|
||||
id=6,
|
||||
lots=[lot],
|
||||
get_matched_lines=Mock(return_value=[]),
|
||||
purchase=Mock(id=7, currency=currency),
|
||||
unit=unit,
|
||||
)
|
||||
fee_lots = Mock()
|
||||
fee_lots.search.return_value = [Mock(fee=fee)]
|
||||
|
||||
@@ -1241,8 +1281,7 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
|
||||
values = Valuation.create_pnl_fee_from_line(line)
|
||||
|
||||
self.assertEqual(values[0]['amount'], Decimal('0'))
|
||||
self.assertEqual(values[0]['fee'], fee.id)
|
||||
self.assertEqual(values, [])
|
||||
|
||||
def test_fee_quantity_sync_uses_physical_lots_when_present(self):
|
||||
'fee quantity sync ignores virtual lot once physical lots exist'
|
||||
@@ -1356,6 +1395,8 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
currency=currency,
|
||||
shipment_in=None,
|
||||
sale_line=None,
|
||||
shipment_out=None,
|
||||
shipment_internal=None,
|
||||
unit=unit,
|
||||
)
|
||||
fee.is_effective_fee_lot.side_effect = lambda lot: lot is physical
|
||||
@@ -1367,6 +1408,7 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
purchase=Mock(id=8, currency=currency),
|
||||
unit=unit,
|
||||
)
|
||||
fee.line = line
|
||||
fee_lots = Mock()
|
||||
fee_lots.search.return_value = [Mock(fee=fee)]
|
||||
|
||||
@@ -1670,6 +1712,8 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
currency=currency,
|
||||
shipment_in=None,
|
||||
sale_line=None,
|
||||
shipment_out=None,
|
||||
shipment_internal=None,
|
||||
unit=unit,
|
||||
)
|
||||
line = Mock(
|
||||
@@ -1680,6 +1724,7 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
purchase=Mock(id=10, currency=currency),
|
||||
unit=unit,
|
||||
)
|
||||
fee.line = line
|
||||
lot_qt_model = Mock()
|
||||
lot_qt_model.search.return_value = [
|
||||
Mock(
|
||||
@@ -1774,6 +1819,9 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
||||
currency=currency,
|
||||
shipment_in=None,
|
||||
sale_line=sale_line,
|
||||
line=None,
|
||||
shipment_out=None,
|
||||
shipment_internal=None,
|
||||
unit=unit,
|
||||
)
|
||||
fee.is_effective_fee_lot.return_value = True
|
||||
|
||||
@@ -1181,6 +1181,37 @@ class ValuationBase(ModelSQL):
|
||||
if product:
|
||||
raise UserError("Currency is required on fee %s." % product)
|
||||
raise UserError("Currency is required on fee.")
|
||||
|
||||
@classmethod
|
||||
def _fee_has_business_anchor(cls, fee):
|
||||
return any(
|
||||
getattr(fee, field_name, None)
|
||||
for field_name in (
|
||||
'line', 'sale_line', 'shipment_in',
|
||||
'shipment_out', 'shipment_internal'))
|
||||
|
||||
@classmethod
|
||||
def _fee_matches_purchase_context(cls, fee, line, matched_sale_line=None):
|
||||
if not cls._fee_has_business_anchor(fee):
|
||||
return False
|
||||
fee_line = getattr(fee, 'line', None)
|
||||
if fee_line and cls._record_id(fee_line) == cls._record_id(line):
|
||||
return True
|
||||
fee_sale_line = getattr(fee, 'sale_line', None)
|
||||
if (fee_sale_line and matched_sale_line
|
||||
and cls._record_id(fee_sale_line)
|
||||
== cls._record_id(matched_sale_line)):
|
||||
return True
|
||||
return bool(getattr(fee, 'shipment_in', None))
|
||||
|
||||
@classmethod
|
||||
def _fee_matches_sale_context(cls, fee, sale_line):
|
||||
if not cls._fee_has_business_anchor(fee):
|
||||
return False
|
||||
fee_sale_line = getattr(fee, 'sale_line', None)
|
||||
if fee_sale_line:
|
||||
return cls._record_id(fee_sale_line) == cls._record_id(sale_line)
|
||||
return bool(getattr(fee, 'shipment_in', None))
|
||||
|
||||
@classmethod
|
||||
def create_pnl_fee_from_line(cls, line):
|
||||
@@ -1211,7 +1242,10 @@ class ValuationBase(ModelSQL):
|
||||
|
||||
fees = [
|
||||
e.fee for e in fl
|
||||
if e.fee and (
|
||||
if e.fee
|
||||
and cls._fee_matches_purchase_context(
|
||||
e.fee, line, matched_sale_line)
|
||||
and (
|
||||
not hasattr(e.fee, 'is_effective_fee_lot')
|
||||
or e.fee.is_effective_fee_lot(lot))
|
||||
]
|
||||
@@ -1273,7 +1307,8 @@ class ValuationBase(ModelSQL):
|
||||
|
||||
fees = [
|
||||
e.fee for e in fl
|
||||
if e.fee and (not e.fee.sale_line or e.fee.sale_line.id == sale_line.id)
|
||||
if e.fee
|
||||
and cls._fee_matches_sale_context(e.fee, sale_line)
|
||||
and (
|
||||
not hasattr(e.fee, 'is_effective_fee_lot')
|
||||
or e.fee.is_effective_fee_lot(lot))
|
||||
@@ -1525,7 +1560,6 @@ class ValuationDyn(ModelSQL,ModelView):
|
||||
r_strategy = fields.Many2One('mtm.strategy',"Strategy")
|
||||
r_lot = fields.Many2One('lot.lot',"Lot")
|
||||
r_shipment_in = fields.Many2One('stock.shipment.in', "Shipment In")
|
||||
r_fee = fields.Many2One('fee.fee', "Fee")
|
||||
|
||||
@classmethod
|
||||
def table_query(cls):
|
||||
@@ -1564,7 +1598,6 @@ class ValuationDyn(ModelSQL,ModelView):
|
||||
Max(val.strategy).as_('r_strategy'),
|
||||
Max(val.lot).as_('r_lot'),
|
||||
Max(val.shipment_in).as_('r_shipment_in'),
|
||||
Max(val.fee).as_('r_fee'),
|
||||
where=wh,
|
||||
group_by=[val.type, val.counterparty, val.state, val.mtm_curve])
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="r_lot"/>
|
||||
<field name="r_shipment_in" optional="1"/>
|
||||
<field name="r_fee" optional="1"/>
|
||||
<field name="r_type"/>
|
||||
<field name="r_reference"/>
|
||||
<field name="r_counterparty"/>
|
||||
|
||||
Reference in New Issue
Block a user