diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py
index 3659672..64d537e 100755
--- a/modules/purchase_trade/__init__.py
+++ b/modules/purchase_trade/__init__.py
@@ -73,6 +73,8 @@ def register():
ctrm_reporting.CTRMLongShortPeriodProduct,
ctrm_reporting.CTRMLongShortPricingStatus,
ctrm_reporting.CTRMLongShortDetail,
+ ctrm_reporting.CTRMPositionExposure,
+ ctrm_reporting.CTRMPositionExposureContext,
ctrm_reporting.CTRMRealizedPnl,
ctrm_reporting.CTRMRealizedPnlContext,
ctrm_reporting.CTRMMtmPnl,
diff --git a/modules/purchase_trade/ctrm_reporting.py b/modules/purchase_trade/ctrm_reporting.py
index eef78e3..995dba6 100644
--- a/modules/purchase_trade/ctrm_reporting.py
+++ b/modules/purchase_trade/ctrm_reporting.py
@@ -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")
diff --git a/modules/purchase_trade/ctrm_reporting.xml b/modules/purchase_trade/ctrm_reporting.xml
index 62b66ea..b324cff 100644
--- a/modules/purchase_trade/ctrm_reporting.xml
+++ b/modules/purchase_trade/ctrm_reporting.xml
@@ -335,8 +335,8 @@
id="menu_ctrm_position_physical"/>