Pnl exclude Mark to finished & price on change
This commit is contained in:
@@ -1278,6 +1278,14 @@ class SaleLine(metaclass=PoolMeta):
|
|||||||
+ Decimal(lot_premium or 0)),
|
+ Decimal(lot_premium or 0)),
|
||||||
4)
|
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')
|
@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):
|
def on_change_with_unit_price(self, name=None):
|
||||||
Date = Pool().get('ir.date')
|
Date = Pool().get('ir.date')
|
||||||
|
|||||||
@@ -145,6 +145,80 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
{'type': 'derivative', 'amount': Decimal('30')},
|
{'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):
|
def test_sale_report_crop_name_handles_missing_crop(self):
|
||||||
'sale report crop name returns an empty string when crop is missing'
|
'sale report crop name returns an empty string when crop is missing'
|
||||||
Sale = Pool().get('sale.sale')
|
Sale = Pool().get('sale.sale')
|
||||||
@@ -171,6 +245,19 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
SaleLine.default_pricing_rule(), 'Default pricing rule')
|
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):
|
def test_purchase_line_default_pricing_rule_comes_from_configuration(self):
|
||||||
'purchase line pricing_rule defaults to the purchase_trade singleton value'
|
'purchase line pricing_rule defaults to the purchase_trade singleton value'
|
||||||
PurchaseLine = Pool().get('purchase.line')
|
PurchaseLine = Pool().get('purchase.line')
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ class ValuationBase(ModelSQL):
|
|||||||
}
|
}
|
||||||
return type_map.get(valuation_type, None)
|
return type_map.get(valuation_type, None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _is_finished(cls, record):
|
||||||
|
return bool(getattr(record, 'finished', False))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _filter_values_by_types(cls, values, selected_types):
|
def _filter_values_by_types(cls, values, selected_types):
|
||||||
if selected_types is None:
|
if selected_types is None:
|
||||||
@@ -406,6 +410,8 @@ class ValuationBase(ModelSQL):
|
|||||||
sl_line = sl.sale_line
|
sl_line = sl.sale_line
|
||||||
if not sl_line:
|
if not sl_line:
|
||||||
continue
|
continue
|
||||||
|
if cls._is_finished(sl_line):
|
||||||
|
continue
|
||||||
|
|
||||||
if sl_line.price_type == 'basis':
|
if sl_line.price_type == 'basis':
|
||||||
premium_delta = cls._get_basis_premium_delta(sl_line)
|
premium_delta = cls._get_basis_premium_delta(sl_line)
|
||||||
@@ -641,7 +647,11 @@ class ValuationBase(ModelSQL):
|
|||||||
FeeLots = Pool().get('fee.lots')
|
FeeLots = Pool().get('fee.lots')
|
||||||
#if line is matched with sale_line we should add the open sale side
|
#if line is matched with sale_line we should add the open sale side
|
||||||
sale_lines = line.get_matched_lines() or []
|
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
|
all_lots = (line.lots or ()) + sale_open_lots
|
||||||
for lot in all_lots:
|
for lot in all_lots:
|
||||||
fl = FeeLots.search([('lot', '=', lot.id)])
|
fl = FeeLots.search([('lot', '=', lot.id)])
|
||||||
@@ -821,6 +831,8 @@ class ValuationBase(ModelSQL):
|
|||||||
def generate(cls, line, valuation_type='all'):
|
def generate(cls, line, valuation_type='all'):
|
||||||
selected_types = cls._get_generate_types(valuation_type)
|
selected_types = cls._get_generate_types(valuation_type)
|
||||||
cls._delete_existing(line, selected_types=selected_types)
|
cls._delete_existing(line, selected_types=selected_types)
|
||||||
|
if cls._is_finished(line):
|
||||||
|
return
|
||||||
values = []
|
values = []
|
||||||
values.extend(cls.create_pnl_fee_from_line(line))
|
values.extend(cls.create_pnl_fee_from_line(line))
|
||||||
values.extend(cls.create_pnl_price_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'):
|
def generate_from_sale_line(cls, sale_line, valuation_type='all'):
|
||||||
selected_types = cls._get_generate_types(valuation_type)
|
selected_types = cls._get_generate_types(valuation_type)
|
||||||
cls._delete_existing_sale_line(sale_line, selected_types=selected_types)
|
cls._delete_existing_sale_line(sale_line, selected_types=selected_types)
|
||||||
|
if cls._is_finished(sale_line):
|
||||||
|
return
|
||||||
values = []
|
values = []
|
||||||
values.extend(cls.create_pnl_fee_from_sale_line(sale_line))
|
values.extend(cls.create_pnl_fee_from_sale_line(sale_line))
|
||||||
values.extend(cls.create_pnl_price_from_sale_line(sale_line))
|
values.extend(cls.create_pnl_price_from_sale_line(sale_line))
|
||||||
|
|||||||
Reference in New Issue
Block a user