ITSA points 28.06

This commit is contained in:
2026-06-28 13:31:48 +02:00
parent b7b54c8b7b
commit f45cdb0807
18 changed files with 718 additions and 35 deletions

View File

@@ -9,7 +9,7 @@ from unittest.mock import ANY, Mock, call, patch
from xml.etree import ElementTree
from trytond.pool import Pool
from trytond.pyson import Eval
from trytond.pyson import Eval, PYSONDecoder
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.exceptions import UserError, UserWarning
from trytond.transaction import Transaction
@@ -902,6 +902,48 @@ class PurchaseTradeTestCase(ModuleTestCase):
scenario.get_valuation_date(),
datetime.date(2026, 6, 18))
def test_itsa_mtm_scenario_defaults_calendar(self):
'ITSA mtm scenarios default to the sulphuric acid calendar'
with patch('trytond.modules.purchase_trade.pricing.is_itsa_company',
return_value=True), patch(
'trytond.modules.purchase_trade.pricing.'
'default_itsa_price_calendar',
return_value=17):
self.assertEqual(pricing_module.MtmScenario.default_calendar(), 17)
def test_itsa_mtm_component_defaults(self):
'ITSA mtm components default market price, ratio and USD currency'
with patch('trytond.modules.purchase_trade.pricing.is_itsa_company',
return_value=True), patch(
'trytond.modules.purchase_trade.pricing.'
'default_itsa_price_fix_type',
return_value=21), patch(
'trytond.modules.purchase_trade.pricing.default_itsa_currency',
return_value=840):
self.assertEqual(pricing_module.Mtm.default_fix_type(), 21)
self.assertEqual(
pricing_module.Mtm.default_ratio(), Decimal('100'))
self.assertEqual(pricing_module.Mtm.default_currency(), 840)
def test_itsa_pricing_component_defaults(self):
'ITSA pricing definitions default market price, ratio, USD and calendar'
with patch('trytond.modules.purchase_trade.pricing.is_itsa_company',
return_value=True), patch(
'trytond.modules.purchase_trade.pricing.'
'default_itsa_price_fix_type',
return_value=21), patch(
'trytond.modules.purchase_trade.pricing.'
'default_itsa_price_calendar',
return_value=17), patch(
'trytond.modules.purchase_trade.pricing.default_itsa_currency',
return_value=840):
self.assertEqual(pricing_module.Component.default_fix_type(), 21)
self.assertEqual(
pricing_module.Component.default_ratio(), Decimal('100'))
self.assertEqual(
pricing_module.Component.default_fixed_currency(), 840)
self.assertEqual(pricing_module.Component.default_calendar(), 17)
def test_mtm_scenario_resolves_relative_valuation_dates(self):
'mtm scenario can resolve relative valuation dates'
scenario = pricing_module.MtmScenario()
@@ -1059,7 +1101,8 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(len(target), 3)
self.assertEqual(target[0], values)
self.assertEqual(target[1]['type'], 'mtm')
self.assertEqual(target[0]['pnl'], Decimal('-100.00'))
self.assertEqual(target[1]['type'], 'pur. mtm')
self.assertEqual(target[1]['mtm_curve'], curve_a.id)
self.assertEqual(target[1]['price'], None)
self.assertEqual(target[1]['amount'], Decimal('0'))
@@ -1068,7 +1111,8 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(target[1]['mtm_price_prev'], Decimal('90.0000'))
self.assertEqual(target[1]['amount_prev'], Decimal('-540.00'))
self.assertEqual(target[1]['mtm'], Decimal('-600.00'))
self.assertEqual(target[2]['type'], 'mtm')
self.assertEqual(target[1]['pnl'], Decimal('600.00'))
self.assertEqual(target[2]['type'], 'pur. mtm')
self.assertEqual(target[2]['mtm_curve'], curve_b.id)
self.assertEqual(target[2]['price'], None)
self.assertEqual(target[2]['amount'], Decimal('0'))
@@ -1077,6 +1121,55 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(target[2]['mtm_price_prev'], Decimal('45.0000'))
self.assertEqual(target[2]['amount_prev'], Decimal('-180.00'))
self.assertEqual(target[2]['mtm'], Decimal('-200.00'))
self.assertEqual(target[2]['pnl'], Decimal('200.00'))
@with_transaction()
def test_sale_strategy_mtm_lines_use_sale_type(self):
'sale strategy MTM lines are labelled as sale MTM'
Valuation = Pool().get('valuation.valuation')
line = Mock(unit=Mock())
strategy = Mock(
scenario=Mock(valuation_date=datetime.date(2026, 6, 5)),
currency=None,
components=[],
get_mtm=Mock(return_value=Decimal('250')))
line.mtm = [strategy]
values = {
'type': 'sale priced',
'amount': Decimal('100'),
'base_amount': Decimal('100'),
'quantity': Decimal('10'),
}
target = []
Valuation._append_pnl_values(target, values, line)
self.assertEqual(target[1]['type'], 'sale mtm')
self.assertEqual(target[1]['mtm'], Decimal('250'))
self.assertEqual(target[1]['pnl'], Decimal('-250.00'))
def test_pnl_converts_strategy_mtm_to_base_currency(self):
'valuation PnL compares base amount with MTM in base currency'
Valuation = valuation_module.Valuation
source_currency = Mock(id=1)
base_currency = Mock(id=2)
strategy = Mock(currency=source_currency, scenario=None)
values = {
'base_amount': Decimal('100'),
'base_currency': base_currency,
}
Currency = Mock()
Currency.compute.return_value = Decimal('90')
with patch('trytond.modules.purchase_trade.valuation.Pool') as PoolMock:
PoolMock.return_value.get.return_value = Currency
pnl = Valuation._pnl_amount(
values, strategy=strategy, mtm_amount=Decimal('120'))
Currency.compute.assert_called_once_with(
source_currency, Decimal('120'), base_currency)
self.assertEqual(pnl, Decimal('10.00'))
def test_purchase_pnl_uses_partial_lotqt_match_quantity(self):
'open matched purchase and sale pnl use the matched lot.qt quantity'
@@ -1547,6 +1640,28 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertFalse(
valuation_module.ValuationProcess._sale_line_is_unmatched(sale_line))
def test_regenerate_for_purchase_lines_deduplicates_targets(self):
'targeted valuation regeneration processes each purchase line once'
Valuation = Pool().get('valuation.valuation')
line = Mock(id=10)
with patch.object(Valuation, 'generate') as generate:
Valuation.regenerate_for_purchase_lines([line, line])
generate.assert_called_once_with(line, valuation_type='all')
def test_regenerate_for_sale_lines_deduplicates_targets(self):
'targeted valuation regeneration processes each sale line once'
Valuation = Pool().get('valuation.valuation')
sale_line = Mock(id=20)
with patch.object(
Valuation, 'generate_from_sale_line') as generate_from_sale_line:
Valuation.regenerate_for_sale_lines([sale_line, sale_line])
generate_from_sale_line.assert_called_once_with(
sale_line, valuation_type='all')
def test_parse_numbers_supports_inline_and_legacy_separators(self):
'parse_numbers keeps supporting inline entry and legacy separators'
self.assertEqual(
@@ -1567,6 +1682,8 @@ class PurchaseTradeTestCase(ModuleTestCase):
{'derivative'})
self.assertIn('pur. priced', Valuation._get_generate_types('goods'))
self.assertIn('mtm', Valuation._get_generate_types('goods'))
self.assertIn('pur. mtm', Valuation._get_generate_types('goods'))
self.assertIn('sale mtm', Valuation._get_generate_types('goods'))
def test_filter_values_by_types_keeps_matching_entries_only(self):
'type filtering keeps only the requested valuation entries'
@@ -3596,6 +3713,26 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertTrue(SaleLine.quantity.readonly)
self.assertTrue(PurchaseLine.quantity.readonly)
def test_allow_modification_after_validation_line_readonly_rule(self):
'configuration flag unlocks lines only in quotation state'
Sale = Pool().get('sale.sale')
Purchase = Pool().get('purchase.purchase')
for model in (Sale, Purchase):
expression = model.lines.states['readonly']
self.assertTrue(PYSONDecoder({
'state': 'quotation',
'allow_modification_after_validation': False,
}).decode(expression))
self.assertFalse(PYSONDecoder({
'state': 'quotation',
'allow_modification_after_validation': True,
}).decode(expression))
self.assertTrue(PYSONDecoder({
'state': 'confirmed',
'allow_modification_after_validation': True,
}).decode(expression))
def test_purchase_line_initial_quantity_uses_theoretical_quantity(self):
'purchase line initializes technical quantity from contractual quantity'
PurchaseLine = Pool().get('purchase.line')
@@ -4004,6 +4141,84 @@ class PurchaseTradeTestCase(ModuleTestCase):
'quantity': Decimal('1'),
})
def test_purchase_and_sale_line_create_default_estimated_bl_date(self):
'new lines default missing estimated BL date from delivery period start'
PurchaseLine = Pool().get('purchase.line')
SaleLine = Pool().get('sale.line')
period = Mock(beg_date=datetime.date(2026, 5, 1))
purchase_values = {'del_period': period}
sale_values = {'del_period': period}
PurchaseLine._set_default_estimated_bl_date_values(purchase_values)
SaleLine._set_default_estimated_bl_date_values(sale_values)
self.assertEqual(purchase_values['estimated_date'], [('create', [{
'trigger': 'bldate',
'estimated_date': datetime.date(2026, 5, 1),
}])])
self.assertEqual(sale_values['estimated_date'], [('create', [{
'trigger': 'bldate',
'estimated_date': datetime.date(2026, 5, 1),
}])])
def test_purchase_and_sale_line_keep_explicit_estimated_bl_date(self):
'new lines keep explicit estimated BL date'
PurchaseLine = Pool().get('purchase.line')
SaleLine = Pool().get('sale.line')
period = Mock(beg_date=datetime.date(2026, 5, 1))
explicit = [('create', [{
'trigger': 'bldate',
'estimated_date': datetime.date(2026, 5, 9),
}])]
purchase_values = {'del_period': period, 'estimated_date': list(explicit)}
sale_values = {'del_period': period, 'estimated_date': list(explicit)}
PurchaseLine._set_default_estimated_bl_date_values(purchase_values)
SaleLine._set_default_estimated_bl_date_values(sale_values)
self.assertEqual(purchase_values['estimated_date'], explicit)
self.assertEqual(sale_values['estimated_date'], explicit)
def test_purchase_line_write_creates_missing_estimated_bl_date(self):
'purchase line write creates missing estimated BL date from period'
PurchaseLine = Pool().get('purchase.line')
line = Mock(
id=42,
del_period=Mock(beg_date=datetime.date(2026, 5, 1)),
estimated_date=[],
)
with patch('trytond.modules.purchase_trade.purchase.Pool') as PoolMock:
Estimated = Mock()
PoolMock.return_value.get.return_value = Estimated
PurchaseLine._create_missing_estimated_bl_dates([line])
Estimated.create.assert_called_once_with([{
'line': 42,
'trigger': 'bldate',
'estimated_date': datetime.date(2026, 5, 1),
}])
def test_sale_line_write_creates_missing_estimated_bl_date(self):
'sale line write creates missing estimated BL date from period'
SaleLine = Pool().get('sale.line')
line = Mock(
id=43,
del_period=Mock(beg_date=datetime.date(2026, 5, 1)),
estimated_date=[],
)
with patch('trytond.modules.purchase_trade.sale.Pool') as PoolMock:
Estimated = Mock()
PoolMock.return_value.get.return_value = Estimated
SaleLine._create_missing_estimated_bl_dates([line])
Estimated.create.assert_called_once_with([{
'sale_line': 43,
'trigger': 'bldate',
'estimated_date': datetime.date(2026, 5, 1),
}])
def test_sale_and_purchase_parent_write_check_embedded_line_commands(self):
'sale and purchase writes validate embedded one2many line commands'
Sale = Pool().get('sale.sale')