Add position exposure report

This commit is contained in:
2026-07-29 21:42:31 +02:00
parent 275008c160
commit 705a1706c3
6 changed files with 637 additions and 2 deletions

View File

@@ -1258,6 +1258,534 @@ class CTRMLongShortDetail(ModelSQL, ModelView):
detail.contract_reference, detail.lot_name])
class CTRMPositionExposureContext(ModelView):
"CTRM Position Exposure Context"
__name__ = 'ctrm.reporting.position.exposure.context'
open_position_date = fields.Date("Open Position Date")
section = fields.Selection([
('all', 'All'),
('physical', 'Physical'),
('derivative', 'Derivatives'),
], "Section")
trade_type = fields.Selection([
('all', 'All'),
('purchase', 'Purchase'),
('sale', 'Sale'),
], "Trade Type")
exposure = fields.Selection([
('both', 'Both'),
('market', 'Market'),
('premium', 'Premium'),
], "Exposure")
pricing_status = fields.Selection([
('all', 'All'),
('fixed', 'Fixed'),
('unfixed', 'Unfixed'),
('partly', 'Partly'),
], "Pricing Status")
commodity = fields.Many2One('product.category', "Commodity")
product = fields.Many2One('product.product', "Product")
counterparty = fields.Many2One('party.party', "Counterparty")
trader = fields.Many2One('party.party', "Trader")
strategy = fields.Many2One('mtm.strategy', "Strategy")
delivery_period_from = fields.Many2One(
'product.month', "Delivery From")
delivery_period_to = fields.Many2One('product.month', "Delivery To")
unit = fields.Many2One('product.uom', "Unit of Measure")
@classmethod
def default_open_position_date(cls):
return Pool().get('ir.date').today()
@classmethod
def default_section(cls):
return 'all'
@classmethod
def default_trade_type(cls):
return 'all'
@classmethod
def default_exposure(cls):
return 'both'
@classmethod
def default_pricing_status(cls):
return 'all'
class CTRMPositionExposure(ModelSQL, ModelView):
"CTRM Position Exposure"
__name__ = 'ctrm.reporting.position.exposure'
open_position_date = fields.Date("Open Position Date", readonly=True)
section = fields.Selection([
('physical', 'Physicals'),
('derivative', 'Derivatives'),
], "Section", readonly=True)
commodity = fields.Many2One(
'product.category', "Commodity", readonly=True)
trade_type = fields.Selection([
('purchase', 'Purchase'),
('sale', 'Sale'),
], "Trade Type", readonly=True)
exposure = fields.Selection([
('market', 'Market Exposure'),
('premium', 'Premium Exposure'),
], "Exposure", readonly=True)
strategy = fields.Many2One(
'mtm.strategy', "Strategy", readonly=True)
contract_reference = fields.Char("Contract No.", readonly=True)
contract_date = fields.Date("Contract Date", readonly=True)
company = fields.Many2One('company.company', "Company", readonly=True)
counterparty = fields.Many2One(
'party.party', "Counterparty", readonly=True)
quantity = fields.Numeric("Quantity", digits=(16, 3), readonly=True)
product = fields.Many2One('product.product', "Product", readonly=True)
incoterm = fields.Many2One(
'incoterm.incoterm', "Incoterm", readonly=True)
incoterm_location = fields.Many2One(
'party.address', "Incoterm Place", readonly=True)
pricing_type = fields.Selection([
('cash', 'Cash Price'),
('priced', 'Priced'),
('basis', 'Basis'),
('efp', 'EFP'),
], "Pricing Type", readonly=True)
delivery_period = fields.Many2One(
'product.month', "Delivery Month", readonly=True)
delivery_start = fields.Date("S/D Start Date", readonly=True)
delivery_end = fields.Date("S/D End Date", readonly=True)
market_index = fields.Many2One(
'price.price', "Market/Index", readonly=True)
prompt_date = fields.Date("Prompt Date", readonly=True)
fixed_percent = fields.Numeric("Fixed %", digits=(16, 2), readonly=True)
unfixed_percent = fields.Numeric(
"Unfixed %", digits=(16, 2), readonly=True)
fixed_price = fields.Numeric("Fixed Price", digits=(16, 4), readonly=True)
unfixed_price = fields.Numeric(
"Unfixed Price", digits=(16, 4), readonly=True)
current_price = fields.Numeric(
"Current Price", digits=(16, 4), readonly=True)
fixed_qty = fields.Numeric("Fixed Qty", digits=(16, 3), readonly=True)
unfixed_qty = fields.Numeric(
"Unfixed Qty", digits=(16, 3), readonly=True)
trader = fields.Many2One('party.party', "Trader", readonly=True)
operator = fields.Many2One('party.party', "Operator", readonly=True)
unit = fields.Many2One('product.uom', "Unit", readonly=True)
amount = fields.Numeric("Amount", digits=(16, 2), readonly=True)
currency = fields.Many2One(
'currency.currency', "Currency", readonly=True)
currency_symbol = fields.Function(
fields.Char("Currency Symbol"), 'get_currency_symbol')
remarks = fields.Char("Remarks", readonly=True)
def get_currency_symbol(self, name=None):
currency = getattr(self, 'currency', None)
return (
getattr(currency, 'symbol', None)
or getattr(currency, 'code', None)
or getattr(currency, 'rec_name', None))
@classmethod
def _context_id(cls, name):
return CTRMLongShortMixin._context_record_id(
Transaction().context.get(name))
@classmethod
def _context_selection(cls, name, values, default=None):
value = Transaction().context.get(name)
return value if value in values else default
@classmethod
def _category_query(cls):
Product = Pool().get('product.product')
Template = Pool().get('product.template')
TemplateCategory = Pool().get('product.template-product.category')
product = Product.__table__()
template = Template.__table__()
template_category = TemplateCategory.__table__()
return (
product
.join(template, condition=product.template == template.id)
.join(template_category, 'LEFT',
condition=template_category.template == template.id)
.select(
product.id.as_('product'),
Max(template_category.category).as_('commodity'),
where=template.type != 'service',
group_by=[product.id]))
@classmethod
def _pricing_summary_query(cls, line_field):
Pricing = Pool().get('pricing.pricing')
Component = Pool().get('pricing.component')
pricing = Pricing.__table__()
component = Component.__table__()
line = getattr(pricing, line_field)
quantity = Max(Coalesce(pricing.fixed_qt, 0)
+ Coalesce(pricing.unfixed_qt, 0))
return (
pricing
.join(component, 'LEFT',
condition=pricing.price_component == component.id)
.select(
line.as_('line'),
Max(pricing.price_component).as_('price_component'),
Max(component.price_index).as_('market_index'),
Max(component.pricing_date).as_('prompt_date'),
quantity.as_('quantity'),
Max(Coalesce(pricing.fixed_qt, 0)).as_('fixed_qty'),
Min(Coalesce(pricing.unfixed_qt, 0)).as_('unfixed_qty'),
Max(Coalesce(pricing.fixed_qt_price, 0)).as_('fixed_price'),
Max(Coalesce(pricing.unfixed_qt_price, 0)).as_(
'unfixed_price'),
Max(Case((pricing.last, pricing.eod_price), else_=0)).as_(
'current_price'),
where=line != Null,
group_by=[line]))
@classmethod
def _strategy_query(cls, strategy_table, line_column):
return strategy_table.select(
line_column.as_('line'),
Max(strategy_table.strategy).as_('strategy'),
group_by=[line_column])
@classmethod
def _line_pricing(cls, price_type, fixed_qty, unfixed_qty):
return Case(
((fixed_qty > 0) & (unfixed_qty > 0), 'partly'),
(unfixed_qty > 0, 'unfixed'),
(price_type.in_(['basis', 'efp']), 'unfixed'),
else_='fixed')
@classmethod
def _exposure_type(cls, price_type):
return Case(
(price_type.in_(['basis', 'efp']), 'market'),
else_='premium')
@classmethod
def _percent(cls, part, total):
return Case(
(total != 0, (part * Literal(100)) / total),
else_=0)
@classmethod
def _apply_context_filters(cls, where, trade_type, exposure, pricing,
product, commodity, counterparty, trader, strategy, period, unit):
trade_type_filter = cls._context_selection(
'trade_type', {'purchase', 'sale'})
section_filter = cls._context_selection(
'section', {'physical', 'derivative'})
exposure_filter = cls._context_selection(
'exposure', {'market', 'premium'})
pricing_filter = cls._context_selection(
'pricing_status', {'fixed', 'unfixed', 'partly'})
product_id = cls._context_id('product')
commodity_id = cls._context_id('commodity')
counterparty_id = cls._context_id('counterparty')
trader_id = cls._context_id('trader')
strategy_id = cls._context_id('strategy')
unit_id = cls._context_id('unit')
period_ids = cls._delivery_period_ids()
if section_filter == 'derivative':
where &= Literal(False)
if trade_type_filter and trade_type_filter != trade_type:
where &= Literal(False)
if exposure_filter:
where &= exposure == exposure_filter
if pricing_filter:
where &= pricing == pricing_filter
if product_id:
where &= product == product_id
if commodity_id:
where &= commodity == commodity_id
if counterparty_id:
where &= counterparty == counterparty_id
if trader_id:
where &= trader == trader_id
if strategy_id:
where &= strategy == strategy_id
if unit_id:
where &= unit == unit_id
if period_ids is not None:
where &= period.in_(period_ids) if period_ids else Literal(False)
return where
@classmethod
def _delivery_period_ids(cls):
period_from = CTRMLongShortMixin._period_record(
cls._context_id('delivery_period_from'))
period_to = CTRMLongShortMixin._period_record(
cls._context_id('delivery_period_to'))
if not period_from and not period_to:
return None
Period = Pool().get('product.month')
domain = []
if period_from and period_from.beg_date:
domain.append(('end_date', '>=', period_from.beg_date))
if period_to and period_to.end_date:
domain.append(('beg_date', '<=', period_to.end_date))
if not domain:
return None
return [p.id for p in Period.search(domain)]
@classmethod
def table_query(cls):
pool = Pool()
PurchaseLine = pool.get('purchase.line')
Purchase = pool.get('purchase.purchase')
SaleLine = pool.get('sale.line')
Sale = pool.get('sale.sale')
PurchaseStrategy = pool.get('purchase.strategy')
SaleStrategy = pool.get('sale.strategy')
purchase_line = PurchaseLine.__table__()
purchase = Purchase.__table__()
sale_line = SaleLine.__table__()
sale = Sale.__table__()
purchase_category = cls._category_query()
sale_category = cls._category_query()
purchase_pricing_summary = cls._pricing_summary_query('line')
sale_pricing_summary = cls._pricing_summary_query('sale_line')
purchase_strategy_table = PurchaseStrategy.__table__()
sale_strategy_table = SaleStrategy.__table__()
purchase_strategy = cls._strategy_query(
purchase_strategy_table, purchase_strategy_table.line)
sale_strategy = cls._strategy_query(
sale_strategy_table, sale_strategy_table.sale_line)
context = Transaction().context
open_position_date = (
context.get('open_position_date')
or Pool().get('ir.date').today())
purchase_quantity = Coalesce(
purchase_line.quantity_theorical, purchase_line.quantity, 0)
purchase_fixed_qty = Coalesce(
purchase_pricing_summary.fixed_qty,
Case((purchase_line.price_type.in_(['cash', 'priced']),
purchase_quantity), else_=0))
purchase_unfixed_qty = Coalesce(
purchase_pricing_summary.unfixed_qty,
Case((purchase_line.price_type.in_(['basis', 'efp']),
purchase_quantity), else_=0))
purchase_pricing = cls._line_pricing(
purchase_line.price_type, purchase_fixed_qty,
purchase_unfixed_qty)
purchase_exposure = cls._exposure_type(purchase_line.price_type)
purchase_where = (
purchase.state.in_(['confirmed', 'processing'])
& (purchase_line.product != Null)
& (purchase_line.type == 'line')
& (purchase_line.finished == False))
purchase_where = cls._apply_context_filters(
purchase_where, 'purchase', purchase_exposure, purchase_pricing,
purchase_line.product, purchase_category.commodity,
purchase.party, purchase.trader, purchase_strategy.strategy,
purchase_line.del_period, purchase_line.unit)
purchase_query = (
purchase_line
.join(purchase, condition=purchase_line.purchase == purchase.id)
.join(purchase_category, 'LEFT',
condition=purchase_category.product == purchase_line.product)
.join(purchase_pricing_summary, 'LEFT',
condition=purchase_pricing_summary.line == purchase_line.id)
.join(purchase_strategy, 'LEFT',
condition=purchase_strategy.line == purchase_line.id)
.select(
purchase_line.id.as_('id'),
Literal(open_position_date).as_('open_position_date'),
Literal('physical').as_('section'),
purchase_category.commodity.as_('commodity'),
Literal('purchase').as_('trade_type'),
purchase_exposure.as_('exposure'),
purchase_strategy.strategy.as_('strategy'),
Coalesce(purchase.reference, purchase.our_reference).as_(
'contract_reference'),
purchase.purchase_date.as_('contract_date'),
purchase.company.as_('company'),
purchase.party.as_('counterparty'),
purchase_quantity.as_('quantity'),
purchase_line.product.as_('product'),
purchase.incoterm.as_('incoterm'),
purchase.incoterm_location.as_('incoterm_location'),
purchase_line.price_type.as_('pricing_type'),
purchase_line.del_period.as_('delivery_period'),
purchase_line.from_del.as_('delivery_start'),
purchase_line.to_del.as_('delivery_end'),
purchase_pricing_summary.market_index.as_('market_index'),
purchase_pricing_summary.prompt_date.as_('prompt_date'),
cls._percent(purchase_fixed_qty, purchase_quantity).as_(
'fixed_percent'),
cls._percent(purchase_unfixed_qty, purchase_quantity).as_(
'unfixed_percent'),
Coalesce(purchase_pricing_summary.fixed_price,
purchase_line.unit_price, 0).as_('fixed_price'),
Coalesce(purchase_pricing_summary.unfixed_price, 0).as_(
'unfixed_price'),
Coalesce(purchase_pricing_summary.current_price,
purchase_line.unit_price, 0).as_('current_price'),
purchase_fixed_qty.as_('fixed_qty'),
purchase_unfixed_qty.as_('unfixed_qty'),
purchase.trader.as_('trader'),
purchase.operator.as_('operator'),
purchase_line.unit.as_('unit'),
(purchase_quantity * Coalesce(
purchase_line.unit_price, 0)).as_('amount'),
purchase.currency.as_('currency'),
Literal(None).as_('remarks'),
where=purchase_where))
sale_quantity = -Coalesce(
sale_line.quantity_theorical, sale_line.quantity, 0)
sale_abs_quantity = Abs(sale_quantity)
sale_fixed_qty = -Coalesce(
sale_pricing_summary.fixed_qty,
Case((sale_line.price_type.in_(['cash', 'priced']),
sale_abs_quantity), else_=0))
sale_unfixed_qty = -Coalesce(
sale_pricing_summary.unfixed_qty,
Case((sale_line.price_type.in_(['basis', 'efp']),
sale_abs_quantity), else_=0))
sale_pricing = cls._line_pricing(
sale_line.price_type, Abs(sale_fixed_qty),
Abs(sale_unfixed_qty))
sale_exposure = cls._exposure_type(sale_line.price_type)
sale_where = (
sale.state.in_(['confirmed', 'processing'])
& (sale_line.product != Null)
& (sale_line.type == 'line')
& (sale_line.finished == False))
sale_where = cls._apply_context_filters(
sale_where, 'sale', sale_exposure, sale_pricing,
sale_line.product, sale_category.commodity,
sale.party, sale.trader, sale_strategy.strategy,
sale_line.del_period, sale_line.unit)
sale_query = (
sale_line
.join(sale, condition=sale_line.sale == sale.id)
.join(sale_category, 'LEFT',
condition=sale_category.product == sale_line.product)
.join(sale_pricing_summary, 'LEFT',
condition=sale_pricing_summary.line == sale_line.id)
.join(sale_strategy, 'LEFT',
condition=sale_strategy.line == sale_line.id)
.select(
(sale_line.id + 100000000).as_('id'),
Literal(open_position_date).as_('open_position_date'),
Literal('physical').as_('section'),
sale_category.commodity.as_('commodity'),
Literal('sale').as_('trade_type'),
sale_exposure.as_('exposure'),
sale_strategy.strategy.as_('strategy'),
Coalesce(sale.reference, sale.our_reference).as_(
'contract_reference'),
sale.sale_date.as_('contract_date'),
sale.company.as_('company'),
sale.party.as_('counterparty'),
sale_quantity.as_('quantity'),
sale_line.product.as_('product'),
sale.incoterm.as_('incoterm'),
sale.incoterm_location.as_('incoterm_location'),
sale_line.price_type.as_('pricing_type'),
sale_line.del_period.as_('delivery_period'),
sale_line.from_del.as_('delivery_start'),
sale_line.to_del.as_('delivery_end'),
sale_pricing_summary.market_index.as_('market_index'),
sale_pricing_summary.prompt_date.as_('prompt_date'),
cls._percent(Abs(sale_fixed_qty), sale_abs_quantity).as_(
'fixed_percent'),
cls._percent(Abs(sale_unfixed_qty), sale_abs_quantity).as_(
'unfixed_percent'),
Coalesce(sale_pricing_summary.fixed_price,
sale_line.unit_price, 0).as_('fixed_price'),
Coalesce(sale_pricing_summary.unfixed_price, 0).as_(
'unfixed_price'),
Coalesce(sale_pricing_summary.current_price,
sale_line.unit_price, 0).as_('current_price'),
sale_fixed_qty.as_('fixed_qty'),
sale_unfixed_qty.as_('unfixed_qty'),
sale.trader.as_('trader'),
sale.operator.as_('operator'),
sale_line.unit.as_('unit'),
(sale_quantity * Coalesce(sale_line.unit_price, 0)).as_(
'amount'),
sale.currency.as_('currency'),
Literal(None).as_('remarks'),
where=sale_where))
section_filter = cls._context_selection(
'section', {'physical', 'derivative'})
trade_type_filter = cls._context_selection(
'trade_type', {'purchase', 'sale'})
if section_filter == 'derivative':
rows = Union(purchase_query, purchase_query, all_=False)
where = rows.id == -1
elif trade_type_filter == 'purchase':
rows = Union(purchase_query, purchase_query, all_=False)
where = Literal(True)
elif trade_type_filter == 'sale':
rows = Union(sale_query, sale_query, all_=False)
where = Literal(True)
else:
rows = Union(purchase_query, sale_query, all_=True)
where = Literal(True)
return rows.select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
rows.id.as_('id'),
rows.open_position_date.as_('open_position_date'),
rows.section.as_('section'),
rows.commodity.as_('commodity'),
rows.trade_type.as_('trade_type'),
rows.exposure.as_('exposure'),
rows.strategy.as_('strategy'),
rows.contract_reference.as_('contract_reference'),
rows.contract_date.as_('contract_date'),
rows.company.as_('company'),
rows.counterparty.as_('counterparty'),
rows.quantity.as_('quantity'),
rows.product.as_('product'),
rows.incoterm.as_('incoterm'),
rows.incoterm_location.as_('incoterm_location'),
rows.pricing_type.as_('pricing_type'),
rows.delivery_period.as_('delivery_period'),
rows.delivery_start.as_('delivery_start'),
rows.delivery_end.as_('delivery_end'),
rows.market_index.as_('market_index'),
rows.prompt_date.as_('prompt_date'),
rows.fixed_percent.as_('fixed_percent'),
rows.unfixed_percent.as_('unfixed_percent'),
rows.fixed_price.as_('fixed_price'),
rows.unfixed_price.as_('unfixed_price'),
rows.current_price.as_('current_price'),
rows.fixed_qty.as_('fixed_qty'),
rows.unfixed_qty.as_('unfixed_qty'),
rows.trader.as_('trader'),
rows.operator.as_('operator'),
rows.unit.as_('unit'),
rows.amount.as_('amount'),
rows.currency.as_('currency'),
rows.remarks.as_('remarks'),
where=where,
order_by=[
rows.delivery_period, rows.commodity, rows.product,
rows.trade_type, rows.contract_reference])
class CTRMPnlContextMixin:
date = fields.Date("Valuation Date")
product = fields.Many2One('product.product', "Product")