Pnl exclude Mark to finished & price on change

This commit is contained in:
2026-04-21 13:22:41 +02:00
parent 897c6f6824
commit 280cff5fdb
3 changed files with 110 additions and 1 deletions

View File

@@ -1278,6 +1278,14 @@ class SaleLine(metaclass=PoolMeta):
+ Decimal(lot_premium or 0)),
4)
@fields.depends('product', 'quantity', 'unit_price',
methods=['_get_context_sale_price'])
def compute_unit_price(self):
unit_price = super().compute_unit_price()
if unit_price is None:
unit_price = self.unit_price
return unit_price
@fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit')
def on_change_with_unit_price(self, name=None):
Date = Pool().get('ir.date')

View File

@@ -145,6 +145,80 @@ class PurchaseTradeTestCase(ModuleTestCase):
{'type': 'derivative', 'amount': Decimal('30')},
])
def test_generate_skips_finished_purchase_line(self):
'valuation generation keeps deleting old rows but skips finished purchase lines'
Valuation = Pool().get('valuation.valuation')
line = Mock(finished=True)
with patch.object(Valuation, '_delete_existing') as delete_existing, patch.object(
Valuation, 'create_pnl_fee_from_line') as create_fees, patch.object(
Valuation, 'create_pnl_price_from_line') as create_prices, patch.object(
Valuation, 'create_pnl_der_from_line') as create_derivatives, patch(
'trytond.modules.purchase_trade.valuation.Pool') as PoolMock:
Valuation.generate(line)
delete_existing.assert_called_once_with(line, selected_types=None)
create_fees.assert_not_called()
create_prices.assert_not_called()
create_derivatives.assert_not_called()
PoolMock.return_value.get.assert_not_called()
def test_generate_skips_finished_sale_line(self):
'sale valuation generation keeps deleting old rows but skips finished sale lines'
Valuation = Pool().get('valuation.valuation')
sale_line = Mock(finished=True)
with patch.object(Valuation, '_delete_existing_sale_line') as delete_existing, patch.object(
Valuation, 'create_pnl_fee_from_sale_line') as create_fees, patch.object(
Valuation, 'create_pnl_price_from_sale_line') as create_prices, patch.object(
Valuation, 'create_pnl_der_from_sale_line') as create_derivatives, patch(
'trytond.modules.purchase_trade.valuation.Pool') as PoolMock:
Valuation.generate_from_sale_line(sale_line)
delete_existing.assert_called_once_with(sale_line, selected_types=None)
create_fees.assert_not_called()
create_prices.assert_not_called()
create_derivatives.assert_not_called()
PoolMock.return_value.get.assert_not_called()
def test_create_pnl_price_from_line_ignores_finished_matched_sale_line(self):
'purchase valuation does not add sale-side pnl when the matched sale line is finished'
Valuation = Pool().get('valuation.valuation')
finished_sale_line = Mock(
finished=True,
price_type='priced',
mtm=[],
)
sale_lot = Mock(sale_line=finished_sale_line)
purchase_lot = Mock(
sale_line=None,
lot_price=Decimal('10'),
lot_type='physic',
)
purchase_lot.get_current_quantity_converted.return_value = Decimal('2')
line = Mock(
finished=False,
lots=[purchase_lot],
price_type='priced',
mtm=[],
purchase=Mock(id=1, currency=Mock(id=1), company=Mock(currency=Mock(id=1)), party=Mock(id=1)),
unit=Mock(id=1),
product=Mock(id=1),
)
lot_qt_model = Mock()
lot_qt_model.search.return_value = [Mock(lot_s=sale_lot)]
with patch(
'trytond.modules.purchase_trade.valuation.Pool') as PoolMock:
PoolMock.return_value.get.return_value = lot_qt_model
values = Valuation.create_pnl_price_from_line(line)
self.assertEqual(len(values), 1)
self.assertEqual(values[0]['type'], 'pur. priced')
self.assertNotIn('sale_line', values[0])
def test_sale_report_crop_name_handles_missing_crop(self):
'sale report crop name returns an empty string when crop is missing'
Sale = Pool().get('sale.sale')
@@ -171,6 +245,19 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(
SaleLine.default_pricing_rule(), 'Default pricing rule')
def test_sale_line_compute_unit_price_keeps_existing_value_when_base_returns_none(self):
'sale line quantity changes keep the manual unit price when no sale price is found'
SaleLine = Pool().get('sale.line')
line = SaleLine()
line.unit_price = Decimal('123.45')
with patch('trytond.modules.purchase_trade.sale.super') as super_mock:
super_mock.return_value.compute_unit_price.return_value = None
self.assertEqual(
line.compute_unit_price(),
Decimal('123.45'))
def test_purchase_line_default_pricing_rule_comes_from_configuration(self):
'purchase line pricing_rule defaults to the purchase_trade singleton value'
PurchaseLine = Pool().get('purchase.line')

View File

@@ -76,6 +76,10 @@ class ValuationBase(ModelSQL):
}
return type_map.get(valuation_type, None)
@classmethod
def _is_finished(cls, record):
return bool(getattr(record, 'finished', False))
@classmethod
def _filter_values_by_types(cls, values, selected_types):
if selected_types is None:
@@ -406,6 +410,8 @@ class ValuationBase(ModelSQL):
sl_line = sl.sale_line
if not sl_line:
continue
if cls._is_finished(sl_line):
continue
if sl_line.price_type == 'basis':
premium_delta = cls._get_basis_premium_delta(sl_line)
@@ -641,7 +647,11 @@ class ValuationBase(ModelSQL):
FeeLots = Pool().get('fee.lots')
#if line is matched with sale_line we should add the open sale side
sale_lines = line.get_matched_lines() or []
sale_open_lots = tuple(s.lot_s for s in sale_lines if s.lot_s)
sale_open_lots = tuple(
s.lot_s for s in sale_lines
if s.lot_s and s.lot_s.sale_line
and not cls._is_finished(s.lot_s.sale_line)
)
all_lots = (line.lots or ()) + sale_open_lots
for lot in all_lots:
fl = FeeLots.search([('lot', '=', lot.id)])
@@ -821,6 +831,8 @@ class ValuationBase(ModelSQL):
def generate(cls, line, valuation_type='all'):
selected_types = cls._get_generate_types(valuation_type)
cls._delete_existing(line, selected_types=selected_types)
if cls._is_finished(line):
return
values = []
values.extend(cls.create_pnl_fee_from_line(line))
values.extend(cls.create_pnl_price_from_line(line))
@@ -837,6 +849,8 @@ class ValuationBase(ModelSQL):
def generate_from_sale_line(cls, sale_line, valuation_type='all'):
selected_types = cls._get_generate_types(valuation_type)
cls._delete_existing_sale_line(sale_line, selected_types=selected_types)
if cls._is_finished(sale_line):
return
values = []
values.extend(cls.create_pnl_fee_from_sale_line(sale_line))
values.extend(cls.create_pnl_price_from_sale_line(sale_line))