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"/> tree ctrm_long_short_detail_list + + ctrm.reporting.position.exposure.context + form + ctrm_position_exposure_context_form + + + ctrm.reporting.position.exposure + tree + ctrm_position_exposure_list + ctrm.reporting.position.long_short.product_strategy tree @@ -101,6 +111,16 @@ + + Position Exposure + ctrm.reporting.position.exposure + ctrm.reporting.position.exposure.context + + + + + + Long & Short by Product & Strategy ctrm.reporting.position.long_short.product_strategy @@ -171,6 +191,12 @@ sequence="10" id="menu_ctrm_positions_physical_group" icon="tradon-reporting-positions"/> + + + + + + + + + diff --git a/modules/purchase_trade/view/ctrm_position_exposure_list.xml b/modules/purchase_trade/view/ctrm_position_exposure_list.xml new file mode 100644 index 0000000..0c0f031 --- /dev/null +++ b/modules/purchase_trade/view/ctrm_position_exposure_list.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +