Hedging setup
This commit is contained in:
@@ -12,22 +12,42 @@ class PurchaseConfiguration(metaclass=PoolMeta):
|
|||||||
|
|
||||||
allow_modification_after_validation = fields.Boolean(
|
allow_modification_after_validation = fields.Boolean(
|
||||||
"Autorise modification after validation")
|
"Autorise modification after validation")
|
||||||
|
auto_hedging = fields.Boolean("Auto hedge")
|
||||||
|
auto_hedging_over = fields.Boolean("Over hedge")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def default_allow_modification_after_validation(cls):
|
def default_allow_modification_after_validation(cls):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_auto_hedging(cls):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_auto_hedging_over(cls):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class SaleConfiguration(metaclass=PoolMeta):
|
class SaleConfiguration(metaclass=PoolMeta):
|
||||||
__name__ = 'sale.configuration'
|
__name__ = 'sale.configuration'
|
||||||
|
|
||||||
allow_modification_after_validation = fields.Boolean(
|
allow_modification_after_validation = fields.Boolean(
|
||||||
"Autorise modification after validation")
|
"Autorise modification after validation")
|
||||||
|
auto_hedging = fields.Boolean("Auto hedge")
|
||||||
|
auto_hedging_over = fields.Boolean("Over hedge")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def default_allow_modification_after_validation(cls):
|
def default_allow_modification_after_validation(cls):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_auto_hedging(cls):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default_auto_hedging_over(cls):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class AccountConfiguration(metaclass=PoolMeta):
|
class AccountConfiguration(metaclass=PoolMeta):
|
||||||
__name__ = 'account.configuration'
|
__name__ = 'account.configuration'
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from trytond.pyson import Bool, Eval, Id, If, PYSONEncoder
|
|||||||
from trytond.model import (ModelSQL, ModelView)
|
from trytond.model import (ModelSQL, ModelView)
|
||||||
from trytond.tools import (cursor_dict, is_full_text, lstrip_wildcard)
|
from trytond.tools import (cursor_dict, is_full_text, lstrip_wildcard)
|
||||||
from trytond.transaction import Transaction, inactive_records
|
from trytond.transaction import Transaction, inactive_records
|
||||||
from decimal import getcontext, Decimal, ROUND_HALF_UP
|
from decimal import getcontext, Decimal, ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_UP
|
||||||
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
|
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
|
||||||
from sql.conditionals import Case, Coalesce
|
from sql.conditionals import Case, Coalesce
|
||||||
from sql import Column, Literal
|
from sql import Column, Literal
|
||||||
@@ -2903,6 +2903,60 @@ class Line(metaclass=PoolMeta):
|
|||||||
lot.lot_unit = packing_unit
|
lot.lot_unit = packing_unit
|
||||||
Lot.save([lot])
|
Lot.save([lot])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _auto_hedge_configuration(cls):
|
||||||
|
Configuration = Pool().get('purchase.configuration')
|
||||||
|
configurations = Configuration.search([], limit=1)
|
||||||
|
if configurations:
|
||||||
|
return configurations[0]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _auto_hedge_contract_count(cls, line, over_hedge=False):
|
||||||
|
price_index = getattr(line, 'coffee_market_reference', None)
|
||||||
|
if not price_index or not getattr(line, 'unit', None):
|
||||||
|
return 0
|
||||||
|
quantity = Decimal(str(
|
||||||
|
getattr(line, 'quantity_theorical', None)
|
||||||
|
or getattr(line, 'quantity', None)
|
||||||
|
or 0))
|
||||||
|
if quantity <= 0:
|
||||||
|
return 0
|
||||||
|
contract_quantity = Decimal(str(price_index.get_qt(1, line.unit) or 0))
|
||||||
|
if contract_quantity <= 0:
|
||||||
|
return 0
|
||||||
|
rounding = ROUND_CEILING if over_hedge else ROUND_FLOOR
|
||||||
|
return int((quantity / contract_quantity).to_integral_value(
|
||||||
|
rounding=rounding))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _ensure_auto_hedge_derivative(cls, line):
|
||||||
|
config = cls._auto_hedge_configuration()
|
||||||
|
if not config or not getattr(config, 'auto_hedging', False):
|
||||||
|
return
|
||||||
|
if getattr(line, 'derivatives', None):
|
||||||
|
return
|
||||||
|
price_index = getattr(line, 'coffee_market_reference', None)
|
||||||
|
nb_ct = cls._auto_hedge_contract_count(
|
||||||
|
line, over_hedge=getattr(config, 'auto_hedging_over', False))
|
||||||
|
if not price_index or nb_ct <= 0:
|
||||||
|
return
|
||||||
|
Derivative = Pool().get('derivative.derivative')
|
||||||
|
Date = Pool().get('ir.date')
|
||||||
|
quantity = price_index.get_qt(nb_ct, line.unit)
|
||||||
|
Derivative.create([{
|
||||||
|
'purchase': line.purchase.id if line.purchase else None,
|
||||||
|
'line': line.id,
|
||||||
|
'product': line.product.id if line.product else None,
|
||||||
|
'party': line.purchase.party.id
|
||||||
|
if line.purchase and line.purchase.party else None,
|
||||||
|
'price_index': price_index.id,
|
||||||
|
'nb_ct': nb_ct,
|
||||||
|
'price': getattr(line, 'coffee_market_price', None),
|
||||||
|
'direction': 'short',
|
||||||
|
'trade_date': Date.today(),
|
||||||
|
'open_qty': quantity,
|
||||||
|
}])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def write(cls, *args):
|
def write(cls, *args):
|
||||||
actions = iter(args)
|
actions = iter(args)
|
||||||
@@ -3135,6 +3189,7 @@ class Line(metaclass=PoolMeta):
|
|||||||
fl.line = line.id
|
fl.line = line.id
|
||||||
FeeLots.save([fl])
|
FeeLots.save([fl])
|
||||||
cls._sync_virtual_lot_packing(line)
|
cls._sync_virtual_lot_packing(line)
|
||||||
|
cls._ensure_auto_hedge_derivative(line)
|
||||||
|
|
||||||
if line.fee_:
|
if line.fee_:
|
||||||
if not line.fee_.purchase:
|
if not line.fee_.purchase:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from trytond.model import (ModelSQL, ModelView)
|
|||||||
from trytond.i18n import gettext
|
from trytond.i18n import gettext
|
||||||
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
|
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
|
||||||
from trytond.transaction import Transaction, inactive_records
|
from trytond.transaction import Transaction, inactive_records
|
||||||
from decimal import getcontext, Decimal, ROUND_HALF_UP
|
from decimal import getcontext, Decimal, ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_UP
|
||||||
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
|
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
|
||||||
from sql.conditionals import Case
|
from sql.conditionals import Case
|
||||||
from sql import Column, Literal
|
from sql import Column, Literal
|
||||||
@@ -3023,6 +3023,60 @@ class SaleLine(metaclass=PoolMeta):
|
|||||||
lot.lot_qt = packing_count
|
lot.lot_qt = packing_count
|
||||||
lot.lot_unit = packing_unit
|
lot.lot_unit = packing_unit
|
||||||
Lot.save([lot])
|
Lot.save([lot])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _auto_hedge_configuration(cls):
|
||||||
|
Configuration = Pool().get('sale.configuration')
|
||||||
|
configurations = Configuration.search([], limit=1)
|
||||||
|
if configurations:
|
||||||
|
return configurations[0]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _auto_hedge_contract_count(cls, line, over_hedge=False):
|
||||||
|
price_index = getattr(line, 'coffee_market_reference', None)
|
||||||
|
if not price_index or not getattr(line, 'unit', None):
|
||||||
|
return 0
|
||||||
|
quantity = Decimal(str(
|
||||||
|
getattr(line, 'quantity_theorical', None)
|
||||||
|
or getattr(line, 'quantity', None)
|
||||||
|
or 0))
|
||||||
|
if quantity <= 0:
|
||||||
|
return 0
|
||||||
|
contract_quantity = Decimal(str(price_index.get_qt(1, line.unit) or 0))
|
||||||
|
if contract_quantity <= 0:
|
||||||
|
return 0
|
||||||
|
rounding = ROUND_CEILING if over_hedge else ROUND_FLOOR
|
||||||
|
return int((quantity / contract_quantity).to_integral_value(
|
||||||
|
rounding=rounding))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _ensure_auto_hedge_derivative(cls, line):
|
||||||
|
config = cls._auto_hedge_configuration()
|
||||||
|
if not config or not getattr(config, 'auto_hedging', False):
|
||||||
|
return
|
||||||
|
if getattr(line, 'derivatives', None):
|
||||||
|
return
|
||||||
|
price_index = getattr(line, 'coffee_market_reference', None)
|
||||||
|
nb_ct = cls._auto_hedge_contract_count(
|
||||||
|
line, over_hedge=getattr(config, 'auto_hedging_over', False))
|
||||||
|
if not price_index or nb_ct <= 0:
|
||||||
|
return
|
||||||
|
Derivative = Pool().get('derivative.derivative')
|
||||||
|
Date = Pool().get('ir.date')
|
||||||
|
quantity = price_index.get_qt(nb_ct, line.unit)
|
||||||
|
Derivative.create([{
|
||||||
|
'sale': line.sale.id if line.sale else None,
|
||||||
|
'sale_line': line.id,
|
||||||
|
'product': line.product.id if line.product else None,
|
||||||
|
'party': line.sale.party.id
|
||||||
|
if line.sale and line.sale.party else None,
|
||||||
|
'price_index': price_index.id,
|
||||||
|
'nb_ct': nb_ct,
|
||||||
|
'price': getattr(line, 'coffee_market_price', None),
|
||||||
|
'direction': 'long',
|
||||||
|
'trade_date': Date.today(),
|
||||||
|
'open_qty': quantity,
|
||||||
|
}])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(cls, salelines):
|
def validate(cls, salelines):
|
||||||
@@ -3083,6 +3137,7 @@ class SaleLine(metaclass=PoolMeta):
|
|||||||
fl.sale_line = line.id
|
fl.sale_line = line.id
|
||||||
FeeLots.save([fl])
|
FeeLots.save([fl])
|
||||||
cls._sync_virtual_lot_packing(line)
|
cls._sync_virtual_lot_packing(line)
|
||||||
|
cls._ensure_auto_hedge_derivative(line)
|
||||||
|
|
||||||
#generate valuation for purchase and sale
|
#generate valuation for purchase and sale
|
||||||
LotQt = Pool().get('lot.qt')
|
LotQt = Pool().get('lot.qt')
|
||||||
|
|||||||
@@ -266,6 +266,112 @@ class PurchaseTradeTestCase(ModuleTestCase):
|
|||||||
|
|
||||||
lot_model.save.assert_not_called()
|
lot_model.save.assert_not_called()
|
||||||
|
|
||||||
|
def test_purchase_auto_hedge_under_uses_floor_contract_count(self):
|
||||||
|
'purchase auto hedge under creates a short floor contract quantity'
|
||||||
|
unit = Mock()
|
||||||
|
price_index = Mock(id=7)
|
||||||
|
price_index.get_qt.side_effect = [Decimal('17'), Decimal('17')]
|
||||||
|
line = Mock(
|
||||||
|
id=12,
|
||||||
|
derivatives=[],
|
||||||
|
coffee_market_reference=price_index,
|
||||||
|
coffee_market_price=Decimal('300'),
|
||||||
|
quantity_theorical=Decimal('26.4'),
|
||||||
|
quantity=None,
|
||||||
|
unit=unit,
|
||||||
|
product=Mock(id=3),
|
||||||
|
purchase=Mock(id=4, party=Mock(id=5)))
|
||||||
|
config_model = Mock()
|
||||||
|
config_model.search.return_value = [
|
||||||
|
Mock(auto_hedging=True, auto_hedging_over=False)]
|
||||||
|
derivative_model = Mock()
|
||||||
|
date_model = Mock()
|
||||||
|
date_model.today.return_value = datetime.date(2026, 7, 16)
|
||||||
|
pool = Mock()
|
||||||
|
pool.get.side_effect = lambda name: {
|
||||||
|
'purchase.configuration': config_model,
|
||||||
|
'derivative.derivative': derivative_model,
|
||||||
|
'ir.date': date_model,
|
||||||
|
}[name]
|
||||||
|
|
||||||
|
with patch.object(purchase_module, 'Pool', return_value=pool):
|
||||||
|
purchase_module.Line._ensure_auto_hedge_derivative(line)
|
||||||
|
|
||||||
|
derivative_model.create.assert_called_once_with([{
|
||||||
|
'purchase': 4,
|
||||||
|
'line': 12,
|
||||||
|
'product': 3,
|
||||||
|
'party': 5,
|
||||||
|
'price_index': 7,
|
||||||
|
'nb_ct': 1,
|
||||||
|
'price': Decimal('300'),
|
||||||
|
'direction': 'short',
|
||||||
|
'trade_date': datetime.date(2026, 7, 16),
|
||||||
|
'open_qty': Decimal('17'),
|
||||||
|
}])
|
||||||
|
|
||||||
|
def test_sale_auto_hedge_over_uses_ceiling_contract_count(self):
|
||||||
|
'sale auto hedge over creates a long ceiling contract quantity'
|
||||||
|
unit = Mock()
|
||||||
|
price_index = Mock(id=8)
|
||||||
|
price_index.get_qt.side_effect = [Decimal('17'), Decimal('34')]
|
||||||
|
line = Mock(
|
||||||
|
id=22,
|
||||||
|
derivatives=[],
|
||||||
|
coffee_market_reference=price_index,
|
||||||
|
coffee_market_price=Decimal('301'),
|
||||||
|
quantity_theorical=Decimal('26.4'),
|
||||||
|
quantity=None,
|
||||||
|
unit=unit,
|
||||||
|
product=Mock(id=13),
|
||||||
|
sale=Mock(id=14, party=Mock(id=15)))
|
||||||
|
config_model = Mock()
|
||||||
|
config_model.search.return_value = [
|
||||||
|
Mock(auto_hedging=True, auto_hedging_over=True)]
|
||||||
|
derivative_model = Mock()
|
||||||
|
date_model = Mock()
|
||||||
|
date_model.today.return_value = datetime.date(2026, 7, 16)
|
||||||
|
pool = Mock()
|
||||||
|
pool.get.side_effect = lambda name: {
|
||||||
|
'sale.configuration': config_model,
|
||||||
|
'derivative.derivative': derivative_model,
|
||||||
|
'ir.date': date_model,
|
||||||
|
}[name]
|
||||||
|
|
||||||
|
with patch.object(sale_module, 'Pool', return_value=pool):
|
||||||
|
sale_module.SaleLine._ensure_auto_hedge_derivative(line)
|
||||||
|
|
||||||
|
derivative_model.create.assert_called_once_with([{
|
||||||
|
'sale': 14,
|
||||||
|
'sale_line': 22,
|
||||||
|
'product': 13,
|
||||||
|
'party': 15,
|
||||||
|
'price_index': 8,
|
||||||
|
'nb_ct': 2,
|
||||||
|
'price': Decimal('301'),
|
||||||
|
'direction': 'long',
|
||||||
|
'trade_date': datetime.date(2026, 7, 16),
|
||||||
|
'open_qty': Decimal('34'),
|
||||||
|
}])
|
||||||
|
|
||||||
|
def test_auto_hedge_keeps_existing_manual_derivative(self):
|
||||||
|
'auto hedge does not create a duplicate when a derivative exists'
|
||||||
|
line = Mock(derivatives=[Mock()])
|
||||||
|
config_model = Mock()
|
||||||
|
config_model.search.return_value = [
|
||||||
|
Mock(auto_hedging=True, auto_hedging_over=True)]
|
||||||
|
derivative_model = Mock()
|
||||||
|
pool = Mock()
|
||||||
|
pool.get.side_effect = lambda name: {
|
||||||
|
'purchase.configuration': config_model,
|
||||||
|
'derivative.derivative': derivative_model,
|
||||||
|
}[name]
|
||||||
|
|
||||||
|
with patch.object(purchase_module, 'Pool', return_value=pool):
|
||||||
|
purchase_module.Line._ensure_auto_hedge_derivative(line)
|
||||||
|
|
||||||
|
derivative_model.create.assert_not_called()
|
||||||
|
|
||||||
def test_itsa_book_year_suffix_uses_april_fiscal_start(self):
|
def test_itsa_book_year_suffix_uses_april_fiscal_start(self):
|
||||||
'ITSA book year changes on April 1st'
|
'ITSA book year changes on April 1st'
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|||||||
@@ -4,5 +4,10 @@
|
|||||||
<label name="allow_modification_after_validation"/>
|
<label name="allow_modification_after_validation"/>
|
||||||
<field name="allow_modification_after_validation"/>
|
<field name="allow_modification_after_validation"/>
|
||||||
<newline/>
|
<newline/>
|
||||||
|
<label name="auto_hedging"/>
|
||||||
|
<field name="auto_hedging"/>
|
||||||
|
<label name="auto_hedging_over"/>
|
||||||
|
<field name="auto_hedging_over"/>
|
||||||
|
<newline/>
|
||||||
</xpath>
|
</xpath>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -4,5 +4,10 @@
|
|||||||
<label name="allow_modification_after_validation"/>
|
<label name="allow_modification_after_validation"/>
|
||||||
<field name="allow_modification_after_validation"/>
|
<field name="allow_modification_after_validation"/>
|
||||||
<newline/>
|
<newline/>
|
||||||
|
<label name="auto_hedging"/>
|
||||||
|
<field name="auto_hedging"/>
|
||||||
|
<label name="auto_hedging_over"/>
|
||||||
|
<field name="auto_hedging_over"/>
|
||||||
|
<newline/>
|
||||||
</xpath>
|
</xpath>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
Reference in New Issue
Block a user