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

Binary file not shown.

View File

@@ -96,6 +96,12 @@ def register():
pricing.ImportPricesResult, pricing.ImportPricesResult,
duplicate.TradeCustomDuplicateStart, duplicate.TradeCustomDuplicateStart,
module='purchase_trade', type_='model') module='purchase_trade', type_='model')
Pool.register(
configuration.PurchaseConfiguration,
module='purchase', type_='model')
Pool.register(
configuration.SaleConfiguration,
module='sale', type_='model')
Pool.register( Pool.register(
incoming.ImportSwift, incoming.ImportSwift,
incoming.PrepareDocuments, incoming.PrepareDocuments,

View File

@@ -41,6 +41,23 @@ def default_itsa_currency():
return record_id(currency) if currency else None return record_id(currency) if currency else None
def default_itsa_price_fix_type():
fix_type = _search_first('price.fixtype', [
[('name', '=', 'Market Price')],
[('name', '=', 'Market price')],
[('name', 'ilike', 'Market Price')],
])
return record_id(fix_type) if fix_type else None
def default_itsa_price_calendar():
calendar = _search_first('price.calendar', [
[('name', '=', 'Argus Sulphuric Acid')],
[('name', 'ilike', 'Argus Sulphuric Acid')],
])
return record_id(calendar) if calendar else None
def default_itsa_unit(): def default_itsa_unit():
unit = _search_first('product.uom', [ unit = _search_first('product.uom', [
[('symbol', '=', 'Mt')], [('symbol', '=', 'Mt')],

View File

@@ -4,6 +4,28 @@ from trytond.pyson import Eval
from trytond.transaction import Transaction from trytond.transaction import Transaction
class PurchaseConfiguration(metaclass=PoolMeta):
__name__ = 'purchase.configuration'
allow_modification_after_validation = fields.Boolean(
"Autorise modification after validation")
@classmethod
def default_allow_modification_after_validation(cls):
return False
class SaleConfiguration(metaclass=PoolMeta):
__name__ = 'sale.configuration'
allow_modification_after_validation = fields.Boolean(
"Autorise modification after validation")
@classmethod
def default_allow_modification_after_validation(cls):
return False
class AccountConfiguration(metaclass=PoolMeta): class AccountConfiguration(metaclass=PoolMeta):
__name__ = 'account.configuration' __name__ = 'account.configuration'

View File

@@ -10,6 +10,16 @@
<field name="type">form</field> <field name="type">form</field>
<field name="name">template_configuration_form</field> <field name="name">template_configuration_form</field>
</record> </record>
<record model="ir.ui.view" id="purchase_configuration_view_form">
<field name="model">purchase.configuration</field>
<field name="inherit" ref="purchase.configuration_view_form"/>
<field name="name">purchase_configuration_form</field>
</record>
<record model="ir.ui.view" id="sale_configuration_view_form">
<field name="model">sale.configuration</field>
<field name="inherit" ref="sale.configuration_view_form"/>
<field name="name">sale_configuration_form</field>
</record>
<record model="ir.action.act_window" id="act_purchase_trade_configuration_form"> <record model="ir.action.act_window" id="act_purchase_trade_configuration_form">
<field name="name">Pricing Configuration</field> <field name="name">Pricing Configuration</field>

View File

@@ -21,7 +21,7 @@ PHYSICAL_VALUATION_TYPES = [
'market', 'market',
] ]
DERIVATIVE_VALUATION_TYPES = ['derivative'] DERIVATIVE_VALUATION_TYPES = ['derivative']
MTM_VALUATION_TYPES = ['mtm'] MTM_VALUATION_TYPES = ['mtm', 'pur. mtm', 'sale mtm']
ALL_VALUATION_TYPES = ( ALL_VALUATION_TYPES = (
PHYSICAL_VALUATION_TYPES + DERIVATIVE_VALUATION_TYPES PHYSICAL_VALUATION_TYPES + DERIVATIVE_VALUATION_TYPES
+ MTM_VALUATION_TYPES) + MTM_VALUATION_TYPES)
@@ -496,6 +496,8 @@ class CTRMMtmPnl(ModelSQL, ModelView):
('shipment fee', 'Shipment fee'), ('shipment fee', 'Shipment fee'),
('market', 'Market'), ('market', 'Market'),
('mtm', 'Mtm'), ('mtm', 'Mtm'),
('pur. mtm', 'Pur. Mtm'),
('sale mtm', 'Sale Mtm'),
('derivative', 'Derivative'), ('derivative', 'Derivative'),
], "Type") ], "Type")
reference = fields.Char("Reference") reference = fields.Char("Reference")

View File

@@ -25,7 +25,8 @@ from io import BytesIO
from xml.etree import ElementTree from xml.etree import ElementTree
from trytond.modules.purchase_trade.purchase import (TRIGGERS) from trytond.modules.purchase_trade.purchase import (TRIGGERS)
from trytond.modules.purchase_trade.company_defaults import ( from trytond.modules.purchase_trade.company_defaults import (
default_itsa_currency, default_itsa_unit, is_itsa_company) default_itsa_currency, default_itsa_price_calendar,
default_itsa_price_fix_type, default_itsa_unit, is_itsa_company)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -594,6 +595,13 @@ class MtmScenario(ModelSQL, ModelView):
def default_valuation_date_type(): def default_valuation_date_type():
return 'fixed' return 'fixed'
@classmethod
def default_calendar(cls):
if is_itsa_company():
calendar = default_itsa_price_calendar()
if calendar:
return calendar
def get_resolved_valuation_date(self, name=None): def get_resolved_valuation_date(self, name=None):
return self.get_valuation_date() return self.get_valuation_date()
@@ -777,12 +785,14 @@ class Mtm(ModelSQL, ModelView):
currency = fields.Many2One('currency.currency', "Currency") currency = fields.Many2One('currency.currency', "Currency")
def get_cur(self, name=None): def get_cur(self, name=None):
if self.price_index: if self.price_index:
return self.price_index.price_currency return self.price_index.price_currency
if self.price_matrix: if self.price_matrix:
return self.price_matrix.currency return self.price_matrix.currency
return None if self.currency:
return self.currency
return None
@fields.depends('price_index','price_matrix') @fields.depends('price_index','price_matrix')
def on_change_with_currency(self): def on_change_with_currency(self):
@@ -795,6 +805,18 @@ class Mtm(ModelSQL, ModelView):
if currency: if currency:
return currency return currency
@classmethod
def default_fix_type(cls):
if is_itsa_company():
fix_type = default_itsa_price_fix_type()
if fix_type:
return fix_type
@classmethod
def default_ratio(cls):
if is_itsa_company():
return Decimal('100')
class PriceMatrix(ModelSQL, ModelView): class PriceMatrix(ModelSQL, ModelView):
"Price Matrix" "Price Matrix"
__name__ = 'price.matrix' __name__ = 'price.matrix'
@@ -934,26 +956,24 @@ class Component(ModelSQL, ModelView):
else: else:
return '[' + self.fix_type.name + '] ' return '[' + self.fix_type.name + '] '
def get_cur(self, name=None): def get_cur(self, name=None):
if self.price_index: if self.price_index:
PI = Pool().get('price.price') PI = Pool().get('price.price')
pi = PI(self.price_index) pi = PI(self.price_index)
return pi.price_currency return pi.price_currency
if self.price_matrix: if self.price_matrix:
return self.price_matrix.currency return self.price_matrix.currency
if self.price_source_type == 'fixed': return self.fixed_currency
return self.fixed_currency
@fields.depends(
@fields.depends( 'price_source_type', 'price_index', 'price_matrix',
'price_source_type', 'price_index', 'price_matrix', 'fixed_currency')
'fixed_currency')
def on_change_with_currency(self): def on_change_with_currency(self):
if self.price_source_type == 'curve' and self.price_index: if self.price_source_type == 'curve' and self.price_index:
return self.get_cur() return self.get_cur()
if self.price_source_type == 'matrix' and self.price_matrix: if self.price_source_type == 'matrix' and self.price_matrix:
return self.get_cur() return self.get_cur()
if self.price_source_type == 'fixed': return self.fixed_currency
return self.fixed_currency
@classmethod @classmethod
def default_fixed_currency(cls): def default_fixed_currency(cls):
@@ -961,6 +981,25 @@ class Component(ModelSQL, ModelView):
currency = default_itsa_currency() currency = default_itsa_currency()
if currency: if currency:
return currency return currency
@classmethod
def default_fix_type(cls):
if is_itsa_company():
fix_type = default_itsa_price_fix_type()
if fix_type:
return fix_type
@classmethod
def default_ratio(cls):
if is_itsa_company():
return Decimal('100')
@classmethod
def default_calendar(cls):
if is_itsa_company():
calendar = default_itsa_price_calendar()
if calendar:
return calendar
@classmethod @classmethod
def set_cur(cls, components, name, value): def set_cur(cls, components, name, value):

View File

@@ -272,6 +272,10 @@ class Purchase(metaclass=PoolMeta):
def __setup__(cls): def __setup__(cls):
super().__setup__() super().__setup__()
cls._transitions.discard(('confirmed', 'processing')) cls._transitions.discard(('confirmed', 'processing'))
cls.lines.states['readonly'] = (
(Eval('state') != 'draft')
& ((Eval('state') != 'quotation')
| ~Eval('allow_modification_after_validation')))
cls._buttons['process'] = { cls._buttons['process'] = {
'invisible': True, 'invisible': True,
'depends': ['state'], 'depends': ['state'],
@@ -380,11 +384,14 @@ class Purchase(metaclass=PoolMeta):
doc_template = fields.Many2One('doc.template',"Template") doc_template = fields.Many2One('doc.template',"Template")
required_documents = fields.Many2Many( required_documents = fields.Many2Many(
'contract.document.type', 'purchase', 'doc_type', 'Required Documents') 'contract.document.type', 'purchase', 'doc_type', 'Required Documents')
analytic_dimensions = fields.One2Many( analytic_dimensions = fields.One2Many(
'analytic.dimension.assignment', 'analytic.dimension.assignment',
'purchase', 'purchase',
'Analytic Dimensions' 'Analytic Dimensions'
) )
allow_modification_after_validation = fields.Function(
fields.Boolean("Autorise modification after validation"),
'on_change_with_allow_modification_after_validation')
trader = fields.Many2One( trader = fields.Many2One(
'party.party', "Trader", 'party.party', "Trader",
domain=[('categories.name', '=', 'TRADER')]) domain=[('categories.name', '=', 'TRADER')])
@@ -404,6 +411,13 @@ class Purchase(metaclass=PoolMeta):
self.company and self.company.party self.company and self.company.party
and self.company.party.name in {'MELYA', 'ITSA'}) and self.company.party.name in {'MELYA', 'ITSA'})
def on_change_with_allow_modification_after_validation(self, name=None):
Configuration = Pool().get('purchase.configuration')
configurations = Configuration.search([], limit=1)
return bool(
configurations
and configurations[0].allow_modification_after_validation)
def _get_default_bank_account(self): def _get_default_bank_account(self):
if not self.party or not self.party.bank_accounts: if not self.party or not self.party.bank_accounts:
return None return None
@@ -2087,15 +2101,97 @@ class Line(metaclass=PoolMeta):
raise UserError( raise UserError(
"Shipment period From date must be before To date.") "Shipment period From date must be before To date.")
@classmethod
def _estimated_bl_relation_field(cls):
return 'line'
@classmethod
def _has_estimated_bl_date(cls, estimated_dates):
return any(
getattr(estimated, 'trigger', None) == 'bldate'
for estimated in (estimated_dates or []))
@classmethod
def _values_have_estimated_bl_date(cls, values):
for command in values.get('estimated_date') or []:
action = command[0]
if action == 'create':
if any(
estimated.get('trigger') == 'bldate'
for estimated in command[1]):
return True
elif action == 'write':
actions = iter(command[1:])
for _estimated_ids, estimated_values in zip(actions, actions):
if estimated_values.get('trigger') == 'bldate':
return True
return False
@classmethod
def _delivery_period_from_values(cls, values):
period = values.get('del_period')
if period and not hasattr(period, 'beg_date'):
period = Pool().get('product.month')(period)
return period
@classmethod
def _default_estimated_bl_date(cls, values):
period = cls._delivery_period_from_values(values)
return getattr(period, 'beg_date', None)
@classmethod
def _set_default_estimated_bl_date_values(cls, values):
if cls._values_have_estimated_bl_date(values):
return
estimated_date = cls._default_estimated_bl_date(values)
if not estimated_date:
return
if values.get('estimated_date') is None:
values['estimated_date'] = []
values['estimated_date'].append(('create', [{
'trigger': 'bldate',
'estimated_date': estimated_date,
}]))
@classmethod
def _create_missing_estimated_bl_dates(cls, lines):
Estimated = Pool().get('pricing.estimated')
values = []
relation_field = cls._estimated_bl_relation_field()
for line in lines:
if cls._has_estimated_bl_date(getattr(line, 'estimated_date', None)):
continue
del_period = getattr(line, 'del_period', None)
estimated_date = getattr(del_period, 'beg_date', None)
if not estimated_date:
continue
values.append({
relation_field: line.id,
'trigger': 'bldate',
'estimated_date': estimated_date,
})
if values:
Estimated.create(values)
@classmethod @classmethod
def create(cls, vlist): def create(cls, vlist):
regenerate_valuation = any(
cls._should_regenerate_valuation(values) for values in vlist)
for values in vlist: for values in vlist:
cls._check_delivery_period_values([cls()], values) cls._check_delivery_period_values([cls()], values)
cls._set_default_estimated_bl_date_values(values)
cls._set_initial_quantity_values(values) cls._set_initial_quantity_values(values)
lines = super().create(vlist) lines = super().create(vlist)
if not Transaction().context.get('_purchase_trade_skip_fee_rules'): if not Transaction().context.get('_purchase_trade_skip_fee_rules'):
Pool().get('fee.rule').apply_to_lines( Pool().get('fee.rule').apply_to_lines(
lines, 'purchase_line', auto_only=True) lines, 'purchase_line', auto_only=True)
if (regenerate_valuation
and not Transaction().context.get(
'_purchase_trade_skip_valuation_regeneration')):
Valuation = Pool().get('valuation.valuation')
with Transaction().set_context(
_purchase_trade_skip_valuation_regeneration=True):
Valuation.regenerate_for_purchase_lines(lines)
return lines return lines
@classmethod @classmethod
@@ -2689,12 +2785,28 @@ class Line(metaclass=PoolMeta):
def on_change_linked_unit(self): def on_change_linked_unit(self):
self._recompute_trade_price_fields() self._recompute_trade_price_fields()
@classmethod
def _valuation_regeneration_fields(cls):
return {
'quantity', 'quantity_theorical', 'unit', 'unit_price',
'price_type', 'premium', 'linked_price', 'linked_currency',
'linked_unit', 'price_pricing', 'price_components', 'derivatives',
'fees', 'lots', 'product', 'currency',
}
@classmethod
def _should_regenerate_valuation(cls, values):
return bool(cls._valuation_regeneration_fields() & set(values))
@classmethod @classmethod
def write(cls, *args): def write(cls, *args):
actions = iter(args) actions = iter(args)
args = [] args = []
valuation_line_ids = set()
for records, values in zip(actions, actions): for records, values in zip(actions, actions):
cls._check_delivery_period_values(records, values) cls._check_delivery_period_values(records, values)
if cls._should_regenerate_valuation(values):
valuation_line_ids.update(record.id for record in records)
args.extend((records, values)) args.extend((records, values))
# Agents: # Agents:
@@ -2748,9 +2860,18 @@ class Line(metaclass=PoolMeta):
Pool().get('lot.lot').assert_lines_quantity_consistency( Pool().get('lot.lot').assert_lines_quantity_consistency(
cls._fresh_lines_for_quantity_consistency(lines)) cls._fresh_lines_for_quantity_consistency(lines))
cls._create_missing_estimated_bl_dates(lines)
if not Transaction().context.get('_purchase_trade_skip_fee_rules'): if not Transaction().context.get('_purchase_trade_skip_fee_rules'):
Pool().get('fee.rule').apply_to_lines( Pool().get('fee.rule').apply_to_lines(
lines, 'purchase_line', auto_only=True) lines, 'purchase_line', auto_only=True)
if (valuation_line_ids
and not Transaction().context.get(
'_purchase_trade_skip_valuation_regeneration')):
Valuation = Pool().get('valuation.valuation')
with Transaction().set_context(
_purchase_trade_skip_valuation_regeneration=True):
Valuation.regenerate_for_purchase_lines(
cls.browse(list(valuation_line_ids)))
@classmethod @classmethod
def _sync_open_lot_quantity(cls, line, vlot, target_quantity): def _sync_open_lot_quantity(cls, line, vlot, target_quantity):

View File

@@ -237,6 +237,10 @@ class Sale(metaclass=PoolMeta):
def __setup__(cls): def __setup__(cls):
super().__setup__() super().__setup__()
cls._transitions.discard(('confirmed', 'processing')) cls._transitions.discard(('confirmed', 'processing'))
cls.lines.states['readonly'] = (
(Eval('state') != 'draft')
& ((Eval('state') != 'quotation')
| ~Eval('allow_modification_after_validation')))
cls._buttons['process'] = { cls._buttons['process'] = {
'invisible': True, 'invisible': True,
'depends': ['state'], 'depends': ['state'],
@@ -329,6 +333,9 @@ class Sale(metaclass=PoolMeta):
'sale', 'sale',
'Analytic Dimensions' 'Analytic Dimensions'
) )
allow_modification_after_validation = fields.Function(
fields.Boolean("Autorise modification after validation"),
'on_change_with_allow_modification_after_validation')
trader = fields.Many2One( trader = fields.Many2One(
'party.party', "Trader", 'party.party', "Trader",
domain=[('categories.name', '=', 'TRADER')]) domain=[('categories.name', '=', 'TRADER')])
@@ -348,6 +355,13 @@ class Sale(metaclass=PoolMeta):
self.company and self.company.party self.company and self.company.party
and self.company.party.name in {'MELYA', 'ITSA'}) and self.company.party.name in {'MELYA', 'ITSA'})
def on_change_with_allow_modification_after_validation(self, name=None):
Configuration = Pool().get('sale.configuration')
configurations = Configuration.search([], limit=1)
return bool(
configurations
and configurations[0].allow_modification_after_validation)
def _get_default_bank_account(self): def _get_default_bank_account(self):
if not self.party or not self.party.bank_accounts: if not self.party or not self.party.bank_accounts:
return None return None
@@ -1911,15 +1925,97 @@ class SaleLine(metaclass=PoolMeta):
raise UserError( raise UserError(
"Shipment period From date must be before To date.") "Shipment period From date must be before To date.")
@classmethod
def _estimated_bl_relation_field(cls):
return 'sale_line'
@classmethod
def _has_estimated_bl_date(cls, estimated_dates):
return any(
getattr(estimated, 'trigger', None) == 'bldate'
for estimated in (estimated_dates or []))
@classmethod
def _values_have_estimated_bl_date(cls, values):
for command in values.get('estimated_date') or []:
action = command[0]
if action == 'create':
if any(
estimated.get('trigger') == 'bldate'
for estimated in command[1]):
return True
elif action == 'write':
actions = iter(command[1:])
for _estimated_ids, estimated_values in zip(actions, actions):
if estimated_values.get('trigger') == 'bldate':
return True
return False
@classmethod
def _delivery_period_from_values(cls, values):
period = values.get('del_period')
if period and not hasattr(period, 'beg_date'):
period = Pool().get('product.month')(period)
return period
@classmethod
def _default_estimated_bl_date(cls, values):
period = cls._delivery_period_from_values(values)
return getattr(period, 'beg_date', None)
@classmethod
def _set_default_estimated_bl_date_values(cls, values):
if cls._values_have_estimated_bl_date(values):
return
estimated_date = cls._default_estimated_bl_date(values)
if not estimated_date:
return
if values.get('estimated_date') is None:
values['estimated_date'] = []
values['estimated_date'].append(('create', [{
'trigger': 'bldate',
'estimated_date': estimated_date,
}]))
@classmethod
def _create_missing_estimated_bl_dates(cls, lines):
Estimated = Pool().get('pricing.estimated')
values = []
relation_field = cls._estimated_bl_relation_field()
for line in lines:
if cls._has_estimated_bl_date(getattr(line, 'estimated_date', None)):
continue
del_period = getattr(line, 'del_period', None)
estimated_date = getattr(del_period, 'beg_date', None)
if not estimated_date:
continue
values.append({
relation_field: line.id,
'trigger': 'bldate',
'estimated_date': estimated_date,
})
if values:
Estimated.create(values)
@classmethod @classmethod
def create(cls, vlist): def create(cls, vlist):
regenerate_valuation = any(
cls._should_regenerate_valuation(values) for values in vlist)
for values in vlist: for values in vlist:
cls._check_delivery_period_values([cls()], values) cls._check_delivery_period_values([cls()], values)
cls._set_default_estimated_bl_date_values(values)
cls._set_initial_quantity_values(values) cls._set_initial_quantity_values(values)
lines = super().create(vlist) lines = super().create(vlist)
if not Transaction().context.get('_purchase_trade_skip_fee_rules'): if not Transaction().context.get('_purchase_trade_skip_fee_rules'):
Pool().get('fee.rule').apply_to_lines( Pool().get('fee.rule').apply_to_lines(
lines, 'sale_line', auto_only=True) lines, 'sale_line', auto_only=True)
if (regenerate_valuation
and not Transaction().context.get(
'_purchase_trade_skip_valuation_regeneration')):
Valuation = Pool().get('valuation.valuation')
with Transaction().set_context(
_purchase_trade_skip_valuation_regeneration=True):
Valuation.regenerate_for_sale_lines(lines)
return lines return lines
@classmethod @classmethod
@@ -2631,12 +2727,28 @@ class SaleLine(metaclass=PoolMeta):
Pricing.save([p]) Pricing.save([p])
index += 1 index += 1
@classmethod
def _valuation_regeneration_fields(cls):
return {
'quantity', 'quantity_theorical', 'unit', 'unit_price',
'price_type', 'premium', 'linked_price', 'linked_currency',
'linked_unit', 'price_pricing', 'price_components', 'derivatives',
'fees', 'lots', 'product', 'currency',
}
@classmethod
def _should_regenerate_valuation(cls, values):
return bool(cls._valuation_regeneration_fields() & set(values))
@classmethod @classmethod
def write(cls, *args): def write(cls, *args):
actions = iter(args) actions = iter(args)
args = [] args = []
valuation_line_ids = set()
for records, values in zip(actions, actions): for records, values in zip(actions, actions):
cls._check_delivery_period_values(records, values) cls._check_delivery_period_values(records, values)
if cls._should_regenerate_valuation(values):
valuation_line_ids.update(record.id for record in records)
args.extend((records, values)) args.extend((records, values))
old_values = {} old_values = {}
@@ -2684,9 +2796,18 @@ class SaleLine(metaclass=PoolMeta):
Pool().get('lot.lot').assert_lines_quantity_consistency( Pool().get('lot.lot').assert_lines_quantity_consistency(
cls._fresh_lines_for_quantity_consistency(lines)) cls._fresh_lines_for_quantity_consistency(lines))
cls._create_missing_estimated_bl_dates(lines)
if not Transaction().context.get('_purchase_trade_skip_fee_rules'): if not Transaction().context.get('_purchase_trade_skip_fee_rules'):
Pool().get('fee.rule').apply_to_lines( Pool().get('fee.rule').apply_to_lines(
lines, 'sale_line', auto_only=True) lines, 'sale_line', auto_only=True)
if (valuation_line_ids
and not Transaction().context.get(
'_purchase_trade_skip_valuation_regeneration')):
Valuation = Pool().get('valuation.valuation')
with Transaction().set_context(
_purchase_trade_skip_valuation_regeneration=True):
Valuation.regenerate_for_sale_lines(
cls.browse(list(valuation_line_ids)))
@classmethod @classmethod
def _sync_open_lot_quantity(cls, line, vlot, target_quantity): def _sync_open_lot_quantity(cls, line, vlot, target_quantity):
@@ -3003,6 +3124,7 @@ class ValuationDyn(metaclass=PoolMeta):
Sum(val.amount).as_('r_amount'), Sum(val.amount).as_('r_amount'),
Sum(val.amount_prev).as_('r_amount_prev'), Sum(val.amount_prev).as_('r_amount_prev'),
Sum(val.base_amount).as_('r_base_amount'), Sum(val.base_amount).as_('r_base_amount'),
Sum(val.pnl).as_('r_pnl'),
Max(val.base_currency).as_('r_base_currency'), Max(val.base_currency).as_('r_base_currency'),
Sum(val.rate).as_('r_rate'), Sum(val.rate).as_('r_rate'),
Avg(val.mtm_price).as_('r_mtm_price'), Avg(val.mtm_price).as_('r_mtm_price'),

View File

@@ -9,7 +9,7 @@ from unittest.mock import ANY, Mock, call, patch
from xml.etree import ElementTree from xml.etree import ElementTree
from trytond.pool import Pool 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.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.exceptions import UserError, UserWarning from trytond.exceptions import UserError, UserWarning
from trytond.transaction import Transaction from trytond.transaction import Transaction
@@ -902,6 +902,48 @@ class PurchaseTradeTestCase(ModuleTestCase):
scenario.get_valuation_date(), scenario.get_valuation_date(),
datetime.date(2026, 6, 18)) 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): def test_mtm_scenario_resolves_relative_valuation_dates(self):
'mtm scenario can resolve relative valuation dates' 'mtm scenario can resolve relative valuation dates'
scenario = pricing_module.MtmScenario() scenario = pricing_module.MtmScenario()
@@ -1059,7 +1101,8 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertEqual(len(target), 3) self.assertEqual(len(target), 3)
self.assertEqual(target[0], values) 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]['mtm_curve'], curve_a.id)
self.assertEqual(target[1]['price'], None) self.assertEqual(target[1]['price'], None)
self.assertEqual(target[1]['amount'], Decimal('0')) 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]['mtm_price_prev'], Decimal('90.0000'))
self.assertEqual(target[1]['amount_prev'], Decimal('-540.00')) self.assertEqual(target[1]['amount_prev'], Decimal('-540.00'))
self.assertEqual(target[1]['mtm'], Decimal('-600.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]['mtm_curve'], curve_b.id)
self.assertEqual(target[2]['price'], None) self.assertEqual(target[2]['price'], None)
self.assertEqual(target[2]['amount'], Decimal('0')) 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]['mtm_price_prev'], Decimal('45.0000'))
self.assertEqual(target[2]['amount_prev'], Decimal('-180.00')) self.assertEqual(target[2]['amount_prev'], Decimal('-180.00'))
self.assertEqual(target[2]['mtm'], Decimal('-200.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): def test_purchase_pnl_uses_partial_lotqt_match_quantity(self):
'open matched purchase and sale pnl use the matched lot.qt quantity' 'open matched purchase and sale pnl use the matched lot.qt quantity'
@@ -1547,6 +1640,28 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertFalse( self.assertFalse(
valuation_module.ValuationProcess._sale_line_is_unmatched(sale_line)) 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): def test_parse_numbers_supports_inline_and_legacy_separators(self):
'parse_numbers keeps supporting inline entry and legacy separators' 'parse_numbers keeps supporting inline entry and legacy separators'
self.assertEqual( self.assertEqual(
@@ -1567,6 +1682,8 @@ class PurchaseTradeTestCase(ModuleTestCase):
{'derivative'}) {'derivative'})
self.assertIn('pur. priced', Valuation._get_generate_types('goods')) self.assertIn('pur. priced', Valuation._get_generate_types('goods'))
self.assertIn('mtm', 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): def test_filter_values_by_types_keeps_matching_entries_only(self):
'type filtering keeps only the requested valuation entries' 'type filtering keeps only the requested valuation entries'
@@ -3596,6 +3713,26 @@ class PurchaseTradeTestCase(ModuleTestCase):
self.assertTrue(SaleLine.quantity.readonly) self.assertTrue(SaleLine.quantity.readonly)
self.assertTrue(PurchaseLine.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): def test_purchase_line_initial_quantity_uses_theoretical_quantity(self):
'purchase line initializes technical quantity from contractual quantity' 'purchase line initializes technical quantity from contractual quantity'
PurchaseLine = Pool().get('purchase.line') PurchaseLine = Pool().get('purchase.line')
@@ -4004,6 +4141,84 @@ class PurchaseTradeTestCase(ModuleTestCase):
'quantity': Decimal('1'), '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): def test_sale_and_purchase_parent_write_check_embedded_line_commands(self):
'sale and purchase writes validate embedded one2many line commands' 'sale and purchase writes validate embedded one2many line commands'
Sale = Pool().get('sale.sale') Sale = Pool().get('sale.sale')

View File

@@ -33,6 +33,8 @@ VALTYPE = [
('shipment fee', 'Shipment fee'), ('shipment fee', 'Shipment fee'),
('market', 'Market'), ('market', 'Market'),
('mtm', 'Mtm'), ('mtm', 'Mtm'),
('pur. mtm', 'Pur. Mtm'),
('sale mtm', 'Sale Mtm'),
('derivative', 'Derivative'), ('derivative', 'Derivative'),
] ]
@@ -67,6 +69,7 @@ class ValuationBase(ModelSQL):
shipment_in = fields.Many2One('stock.shipment.in', "Shipment In") shipment_in = fields.Many2One('stock.shipment.in', "Shipment In")
fee = fields.Many2One('fee.fee', "Fee") fee = fields.Many2One('fee.fee', "Fee")
base_amount = fields.Numeric("Base Amount",digits=(16,2)) base_amount = fields.Numeric("Base Amount",digits=(16,2))
pnl = fields.Numeric("Pnl", digits=(16, 2))
base_currency = fields.Many2One('currency.currency', "Base Cur") base_currency = fields.Many2One('currency.currency', "Base Cur")
rate = fields.Numeric("Rate", digits=(16,6)) rate = fields.Numeric("Rate", digits=(16,6))
@@ -77,7 +80,8 @@ class ValuationBase(ModelSQL):
'fees': {'line fee', 'pur. fee', 'sale fee', 'shipment fee'}, 'fees': {'line fee', 'pur. fee', 'sale fee', 'shipment fee'},
'goods': { 'goods': {
'priced', 'pur. priced', 'pur. efp', 'priced', 'pur. priced', 'pur. efp',
'sale priced', 'sale efp', 'market', 'mtm', 'sale priced', 'sale efp', 'market',
'mtm', 'pur. mtm', 'sale mtm',
}, },
'derivatives': {'derivative'}, 'derivatives': {'derivative'},
} }
@@ -370,6 +374,12 @@ class ValuationBase(ModelSQL):
seen.add(key) seen.add(key)
return unique return unique
@classmethod
def _prepare_values_for_create(cls, values):
for value in values:
cls._set_pnl(value, strategy=value.get('strategy'))
return values
@classmethod @classmethod
def _snapshot_identity_domain(cls, value): def _snapshot_identity_domain(cls, value):
domain = [ domain = [
@@ -450,6 +460,30 @@ class ValuationBase(ModelSQL):
def _sale_line_is_unmatched(cls, sale_line): def _sale_line_is_unmatched(cls, sale_line):
return not cls._matched_purchase_lines_from_sale_line(sale_line) return not cls._matched_purchase_lines_from_sale_line(sale_line)
@classmethod
def _unique_records(cls, records):
unique = []
seen = set()
for record in records or []:
record_id = cls._record_id(record)
key = record_id if record_id is not None else id(record)
if not record or key in seen:
continue
unique.append(record)
seen.add(key)
return unique
@classmethod
def regenerate_for_purchase_lines(cls, lines, valuation_type='all'):
for line in cls._unique_records(lines):
cls.generate(line, valuation_type=valuation_type)
@classmethod
def regenerate_for_sale_lines(cls, sale_lines, valuation_type='all'):
for sale_line in cls._unique_records(sale_lines):
cls.generate_from_sale_line(
sale_line, valuation_type=valuation_type)
@classmethod @classmethod
def _delete_existing_sale_line(cls, sale_line, selected_types=None): def _delete_existing_sale_line(cls, sale_line, selected_types=None):
Date = Pool().get('ir.date') Date = Pool().get('ir.date')
@@ -569,6 +603,68 @@ class ValuationBase(ModelSQL):
return -abs(mtm) return -abs(mtm)
return abs(mtm) return abs(mtm)
@staticmethod
def _strategy_mtm_type(values):
if values.get('type') in {'pur. priced', 'pur. efp'}:
return 'pur. mtm'
if values.get('type') in {'sale priced', 'sale efp'}:
return 'sale mtm'
return 'mtm'
@staticmethod
def _coerce_decimal(value):
if value in (None, ''):
return Decimal(0)
return Decimal(str(value))
@classmethod
def _mtm_base_amount(cls, mtm_amount, strategy, values):
if mtm_amount in (None, ''):
return Decimal(0)
mtm_amount = Decimal(str(mtm_amount))
source_currency = getattr(strategy, 'currency', None)
base_currency = values.get('base_currency')
if not source_currency or not base_currency:
return mtm_amount
source_currency_id = cls._record_id(source_currency)
base_currency_id = cls._record_id(base_currency)
if source_currency_id == base_currency_id:
return mtm_amount
Currency = Pool().get('currency.currency')
if isinstance(source_currency, int):
source_currency = Currency(source_currency)
if isinstance(base_currency, int):
base_currency = Currency(base_currency)
scenario = getattr(strategy, 'scenario', None)
conversion_date = (
cls._strategy_valuation_date(strategy)
if scenario else values.get('date'))
if conversion_date:
with Transaction().set_context(date=conversion_date):
return Currency.compute(
source_currency, mtm_amount, base_currency)
return Currency.compute(source_currency, mtm_amount, base_currency)
@classmethod
def _pnl_amount(cls, values, strategy=None, mtm_amount=None):
base_amount = cls._coerce_decimal(values.get('base_amount'))
if mtm_amount is None:
mtm_amount = values.get('mtm')
mtm_base_amount = (
cls._mtm_base_amount(mtm_amount, strategy, values)
if strategy else cls._coerce_decimal(mtm_amount))
return round(base_amount - mtm_base_amount, 2)
@classmethod
def _set_pnl(cls, values, strategy=None, mtm_amount=None):
if values is not None:
values['pnl'] = cls._pnl_amount(
values, strategy=strategy, mtm_amount=mtm_amount)
return values
@staticmethod @staticmethod
def _component_weight(component): def _component_weight(component):
ratio = Decimal(component.ratio or 0) ratio = Decimal(component.ratio or 0)
@@ -623,7 +719,7 @@ class ValuationBase(ModelSQL):
mtm_curve=None, mtm_price_prev=None, mtm_amount_prev=None): mtm_curve=None, mtm_price_prev=None, mtm_amount_prev=None):
line_values = dict(values) line_values = dict(values)
line_values.update({ line_values.update({
'type': 'mtm', 'type': cls._strategy_mtm_type(values),
'reference': 'MTM/%s' % (values.get('reference') or ''), 'reference': 'MTM/%s' % (values.get('reference') or ''),
'price': None, 'price': None,
'amount': Decimal(0), 'amount': Decimal(0),
@@ -635,6 +731,7 @@ class ValuationBase(ModelSQL):
'mtm': mtm_amount, 'mtm': mtm_amount,
'strategy': strategy, 'strategy': strategy,
}) })
cls._set_pnl(line_values, strategy=strategy, mtm_amount=mtm_amount)
return line_values return line_values
@classmethod @classmethod
@@ -842,11 +939,13 @@ class ValuationBase(ModelSQL):
def _append_pnl_values(cls, price_lines, values, mtm_source): def _append_pnl_values(cls, price_lines, values, mtm_source):
if (values and getattr(mtm_source, 'mtm', None) if (values and getattr(mtm_source, 'mtm', None)
and cls._supports_strategy_mtm(values)): and cls._supports_strategy_mtm(values)):
cls._set_pnl(values)
price_lines.append(values) price_lines.append(values)
for strat in mtm_source.mtm: for strat in mtm_source.mtm:
cls._append_strategy_mtm_lines( cls._append_strategy_mtm_lines(
price_lines, values, strat, mtm_source) price_lines, values, strat, mtm_source)
elif values: elif values:
cls._set_pnl(values)
price_lines.append(values) price_lines.append(values)
@classmethod @classmethod
@@ -1467,6 +1566,7 @@ class ValuationBase(ModelSQL):
values.extend(cls.create_pnl_der_from_line(line)) values.extend(cls.create_pnl_der_from_line(line))
values = cls._filter_values_by_types(values, selected_types) values = cls._filter_values_by_types(values, selected_types)
values = cls._dedupe_values(values) values = cls._dedupe_values(values)
values = cls._prepare_values_for_create(values)
cls._delete_existing_snapshot_values(values) cls._delete_existing_snapshot_values(values)
if values: if values:
@@ -1492,6 +1592,7 @@ class ValuationBase(ModelSQL):
values.extend(cls.create_pnl_der_from_sale_line(sale_line)) values.extend(cls.create_pnl_der_from_sale_line(sale_line))
values = cls._filter_values_by_types(values, selected_types) values = cls._filter_values_by_types(values, selected_types)
values = cls._dedupe_values(values) values = cls._dedupe_values(values)
values = cls._prepare_values_for_create(values)
cls._delete_existing_snapshot_values(values) cls._delete_existing_snapshot_values(values)
if values: if values:
@@ -1589,6 +1690,7 @@ class ValuationDyn(ModelSQL,ModelView):
r_amount = fields.Numeric("Amount",digits='r_unit') r_amount = fields.Numeric("Amount",digits='r_unit')
r_amount_prev = fields.Numeric("Amount -1", digits='r_unit') r_amount_prev = fields.Numeric("Amount -1", digits='r_unit')
r_base_amount = fields.Numeric("Base Amount",digits='r_unit') r_base_amount = fields.Numeric("Base Amount",digits='r_unit')
r_pnl = fields.Numeric("Pnl", digits='r_unit')
r_base_currency = fields.Many2One('currency.currency', "Base Cur") r_base_currency = fields.Many2One('currency.currency', "Base Cur")
r_rate = fields.Numeric("Rate",digits=(16,6)) r_rate = fields.Numeric("Rate",digits=(16,6))
r_mtm_price = fields.Numeric("Mtm Price",digits='r_unit') r_mtm_price = fields.Numeric("Mtm Price",digits='r_unit')
@@ -1628,6 +1730,7 @@ class ValuationDyn(ModelSQL,ModelView):
Sum(val.amount).as_('r_amount'), Sum(val.amount).as_('r_amount'),
Sum(val.amount_prev).as_('r_amount_prev'), Sum(val.amount_prev).as_('r_amount_prev'),
Sum(val.base_amount).as_('r_base_amount'), Sum(val.base_amount).as_('r_base_amount'),
Sum(val.pnl).as_('r_pnl'),
Max(val.base_currency).as_('r_base_currency'), Max(val.base_currency).as_('r_base_currency'),
Sum(val.rate).as_('r_rate'), Sum(val.rate).as_('r_rate'),
Avg(val.mtm_price).as_('r_mtm_price'), Avg(val.mtm_price).as_('r_mtm_price'),
@@ -1686,6 +1789,7 @@ class ValuationReport(ValuationBase, ModelView):
val.amount.as_('amount'), val.amount.as_('amount'),
val.amount_prev.as_('amount_prev'), val.amount_prev.as_('amount_prev'),
val.base_amount.as_('base_amount'), val.base_amount.as_('base_amount'),
val.pnl.as_('pnl'),
val.base_currency.as_('base_currency'), val.base_currency.as_('base_currency'),
val.rate.as_('rate'), val.rate.as_('rate'),
val.mtm_price.as_('mtm_price'), val.mtm_price.as_('mtm_price'),

View File

@@ -0,0 +1,8 @@
<?xml version="1.0"?>
<data>
<xpath expr="/form/label[@name='purchase_process_after']" position="before">
<label name="allow_modification_after_validation"/>
<field name="allow_modification_after_validation"/>
<newline/>
</xpath>
</data>

View File

@@ -3,6 +3,9 @@
this repository contains the full copyright notices and license terms. --> this repository contains the full copyright notices and license terms. -->
<data> <data>
<xpath expr="/form/group[@id='hd'][2]" position="replace"/> <xpath expr="/form/group[@id='hd'][2]" position="replace"/>
<xpath expr="/form/field[@name='party_lang']" position="before">
<field name="allow_modification_after_validation" invisible="1"/>
</xpath>
<xpath expr="/form/group[@id='hd'][1]" position="replace"> <xpath expr="/form/group[@id='hd'][1]" position="replace">
<group id="purchase_header_summary" colspan="6" col="10" <group id="purchase_header_summary" colspan="6" col="10"
panel="summary" xalign="0" yalign="0" panel="summary" xalign="0" yalign="0"

View File

@@ -0,0 +1,8 @@
<?xml version="1.0"?>
<data>
<xpath expr="/form/label[@name='sale_process_after']" position="before">
<label name="allow_modification_after_validation"/>
<field name="allow_modification_after_validation"/>
<newline/>
</xpath>
</data>

View File

@@ -3,6 +3,9 @@
this repository contains the full copyright notices and license terms. --> this repository contains the full copyright notices and license terms. -->
<data> <data>
<xpath expr="/form/group[@id='hd'][2]" position="replace"/> <xpath expr="/form/group[@id='hd'][2]" position="replace"/>
<xpath expr="/form/field[@name='party_lang']" position="before">
<field name="allow_modification_after_validation" invisible="1"/>
</xpath>
<xpath expr="/form/group[@id='hd'][1]" position="replace"> <xpath expr="/form/group[@id='hd'][1]" position="replace">
<group id="sale_header_summary" colspan="6" col="10" <group id="sale_header_summary" colspan="6" col="10"
panel="summary" xalign="0" yalign="0" panel="summary" xalign="0" yalign="0"

View File

@@ -6,7 +6,7 @@
<field name="fee" optional="1"/> <field name="fee" optional="1"/>
<field name="type" <field name="type"
widget="badge" widget="badge"
badge_colors="priced:#2563eb,Price:#2563eb,pur. priced:#1d4ed8,Pur. price:#1d4ed8,pur. efp:#38bdf8,Pur. efp:#38bdf8,sale priced:#16a34a,Sale price:#16a34a,sale efp:#86efac,Sale efp:#86efac,market:#64748b,Market:#64748b,line fee:#d97706,Line fee:#d97706,pur. fee:#f59e0b,Pur. fee:#f59e0b,sale fee:#fb923c,Sale fee:#fb923c,shipment fee:#0f766e,Shipment fee:#0f766e,mtm:#9f1239,Mtm:#9f1239,derivative:#8b5cf6,Derivative:#8b5cf6,*:#94a3b8"/> badge_colors="priced:#2563eb,Price:#2563eb,pur. priced:#1d4ed8,Pur. price:#1d4ed8,pur. efp:#38bdf8,Pur. efp:#38bdf8,sale priced:#16a34a,Sale price:#16a34a,sale efp:#86efac,Sale efp:#86efac,market:#64748b,Market:#64748b,line fee:#d97706,Line fee:#d97706,pur. fee:#f59e0b,Pur. fee:#f59e0b,sale fee:#fb923c,Sale fee:#fb923c,shipment fee:#0f766e,Shipment fee:#0f766e,mtm:#9f1239,Mtm:#9f1239,pur. mtm:#9f1239,Pur. Mtm:#9f1239,sale mtm:#9f1239,Sale Mtm:#9f1239,derivative:#8b5cf6,Derivative:#8b5cf6,*:#94a3b8"/>
<field name="reference"/> <field name="reference"/>
<field name="counterparty"/> <field name="counterparty"/>
<field name="state"/> <field name="state"/>
@@ -19,6 +19,7 @@
<field name="currency"/> <field name="currency"/>
<field name="amount_prev" optional="1"/> <field name="amount_prev" optional="1"/>
<field name="base_amount" sum="1"/> <field name="base_amount" sum="1"/>
<field name="pnl" sum="1"/>
<field name="base_currency"/> <field name="base_currency"/>
<field name="rate"/> <field name="rate"/>
<field name="strategy"/> <field name="strategy"/>

View File

@@ -7,7 +7,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="fee" optional="1"/> <field name="fee" optional="1"/>
<field name="type" <field name="type"
widget="badge" widget="badge"
badge_colors="priced:#2563eb,Price:#2563eb,pur. priced:#1d4ed8,Pur. price:#1d4ed8,pur. efp:#38bdf8,Pur. efp:#38bdf8,sale priced:#16a34a,Sale price:#16a34a,sale efp:#86efac,Sale efp:#86efac,market:#64748b,Market:#64748b,line fee:#d97706,Line fee:#d97706,pur. fee:#f59e0b,Pur. fee:#f59e0b,sale fee:#fb923c,Sale fee:#fb923c,shipment fee:#0f766e,Shipment fee:#0f766e,mtm:#9f1239,Mtm:#9f1239,derivative:#8b5cf6,Derivative:#8b5cf6,*:#94a3b8"/> badge_colors="priced:#2563eb,Price:#2563eb,pur. priced:#1d4ed8,Pur. price:#1d4ed8,pur. efp:#38bdf8,Pur. efp:#38bdf8,sale priced:#16a34a,Sale price:#16a34a,sale efp:#86efac,Sale efp:#86efac,market:#64748b,Market:#64748b,line fee:#d97706,Line fee:#d97706,pur. fee:#f59e0b,Pur. fee:#f59e0b,sale fee:#fb923c,Sale fee:#fb923c,shipment fee:#0f766e,Shipment fee:#0f766e,mtm:#9f1239,Mtm:#9f1239,pur. mtm:#9f1239,Pur. Mtm:#9f1239,sale mtm:#9f1239,Sale Mtm:#9f1239,derivative:#8b5cf6,Derivative:#8b5cf6,*:#94a3b8"/>
<field name="reference"/> <field name="reference"/>
<field name="counterparty"/> <field name="counterparty"/>
<field name="state"/> <field name="state"/>
@@ -20,6 +20,7 @@ this repository contains the full copyright notices and license terms. -->
<field name="currency"/> <field name="currency"/>
<field name="amount_prev" optional="1"/> <field name="amount_prev" optional="1"/>
<field name="base_amount" sum="1"/> <field name="base_amount" sum="1"/>
<field name="pnl" sum="1"/>
<field name="base_currency"/> <field name="base_currency"/>
<field name="rate"/> <field name="rate"/>
<field name="strategy"/> <field name="strategy"/>

View File

@@ -15,6 +15,7 @@ this repository contains the full copyright notices and license terms. -->
sum_previous="r_amount_prev" sum_previous="r_amount_prev"
sum_variation="1"/> sum_variation="1"/>
<field name="r_amount_prev" optional="1"/> <field name="r_amount_prev" optional="1"/>
<field name="r_pnl" sum="1"/>
<field name="r_strategy"/> <field name="r_strategy"/>
<field name="r_mtm_curve"/> <field name="r_mtm_curve"/>
<field name="r_mtm_price" <field name="r_mtm_price"