diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py index 2a2241f..782f22a 100755 --- a/modules/purchase_trade/__init__.py +++ b/modules/purchase_trade/__init__.py @@ -72,6 +72,8 @@ def register(): ctrm_reporting.CTRMLongShortCommodityDirection, ctrm_reporting.CTRMLongShortPeriodProduct, ctrm_reporting.CTRMLongShortPricingStatus, + ctrm_reporting.CTRMLongShortDetail, + ctrm_reporting.CTRMLongShortDetailContext, 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 611450d..4d51f5d 100644 --- a/modules/purchase_trade/ctrm_reporting.py +++ b/modules/purchase_trade/ctrm_reporting.py @@ -3,7 +3,7 @@ import datetime from sql import Literal, Null, Union, Window from sql.aggregate import Count, Max, Min, Sum from sql.conditionals import Case, Coalesce -from sql.functions import CurrentTimestamp, RowNumber +from sql.functions import Abs, CurrentTimestamp, RowNumber from trytond.model import ModelSQL, ModelView, fields from trytond.pool import Pool @@ -818,6 +818,481 @@ class CTRMLongShortPricingStatus( line_pricing=True) +class CTRMLongShortDetailContext(CTRMOpenPositionContext): + "CTRM Long & Short Detail Context" + __name__ = 'ctrm.reporting.position.long_short.detail.context' + + asof = fields.Date("From Date") + todate = fields.Date("To Date") + purchase = fields.Many2One('purchase.purchase', "Purchase") + sale = fields.Many2One('sale.sale', "Sale") + supplier = fields.Many2One('party.party', "Supplier") + client = fields.Many2One('party.party', "Client") + reference = fields.Char("Reference") + shipment = fields.Many2One('stock.shipment.in', "Shipment") + vessel = fields.Many2One('trade.vessel', "Vessel") + location = fields.Many2One('stock.location', "Location") + dimension = fields.Many2One('analytic.dimension.value', "Dimension") + matching_status = fields.Selection([ + ('all', 'All'), + ('matched', 'Matched'), + ('not matched', 'Not Matched'), + ], "Matching") + shipping_status = fields.Selection([ + ('all', 'All'), + ('unshipped', 'Unshipped'), + ('planned', 'Planned'), + ('scheduled', 'Scheduled'), + ('shipped', 'Shipped'), + ('received', 'Received'), + ], "Shipping Status") + lot_status = fields.Selection([ + ('all', 'All'), + ('forecast', 'Forecast'), + ('loading', 'Loading'), + ('destination', 'Destination'), + ('stock', 'Stock'), + ('delivered', 'Delivered'), + ], "Where") + lot_origin = fields.Selection([ + ('all', 'All'), + ('open', 'Open'), + ('physic', 'Physic'), + ], "Open/Physic") + availability = fields.Selection([ + ('all', 'All'), + ('available', 'Available'), + ('reserved', 'Reserved'), + ('locked', 'Locked'), + ], "Availability") + display_finished = fields.Boolean("Finished") + + @classmethod + def default_matching_status(cls): + return 'all' + + @classmethod + def default_shipping_status(cls): + return 'all' + + @classmethod + def default_lot_status(cls): + return 'all' + + @classmethod + def default_lot_origin(cls): + return 'all' + + @classmethod + def default_availability(cls): + return 'all' + + +class CTRMLongShortDetail(ModelSQL, ModelView): + "CTRM Long & Short Detail" + __name__ = 'ctrm.reporting.position.long_short.detail' + + contract_type = fields.Selection([ + ('purchase', 'Purchase'), + ('sale', 'Sale'), + ], "Contract Type", readonly=True) + contract_reference = fields.Char("Contract No.", readonly=True) + purchase = fields.Many2One( + 'purchase.purchase', "Purchase", readonly=True) + purchase_line = fields.Many2One( + 'purchase.line', "Purchase Line", readonly=True) + sale = fields.Many2One('sale.sale', "Sale", readonly=True) + sale_line = fields.Many2One('sale.line', "Sale Line", readonly=True) + counterparty = fields.Many2One( + 'party.party', "Counterparty", readonly=True) + lot = fields.Many2One('lot.lot', "Lot", readonly=True) + lot_name = fields.Char("Lot Name", readonly=True) + lot_type = fields.Selection([ + ('virtual', 'Open'), + ('physic', 'Physic'), + ('loss', 'Loss'), + ], "Qt Type", readonly=True) + shipping_status = fields.Selection([ + ('unshipped', 'Unshipped'), + ('planned', 'Planned'), + ('scheduled', 'Scheduled'), + ('shipped', 'Shipped'), + ('received', 'Received'), + ], "Shipping Status", readonly=True) + lot_status = fields.Selection([ + (None, ''), + ('forecast', 'Forecast'), + ('loading', 'Loading'), + ('transit', 'Transit'), + ('destination', 'Destination'), + ('stock', 'Stock'), + ('delivered', 'Delivered'), + ], "Where", readonly=True) + availability = fields.Selection([ + ('available', 'Available'), + ('reserved', 'Reserved'), + ('locked', 'Locked'), + ], "Availability", readonly=True) + product = fields.Many2One( + 'product.product', "Product", readonly=True) + commodity = fields.Many2One( + 'product.category', "Commodity", readonly=True) + attributes_name = fields.Function( + fields.Char("Attributes", readonly=True), 'get_line_display') + specification = fields.Function( + fields.Char("Specification", readonly=True), 'get_line_display') + incoterm = fields.Many2One( + 'incoterm.incoterm', "Incoterm", readonly=True) + incoterm_location = fields.Many2One( + 'party.address', "Incoterm Place", readonly=True) + from_location = fields.Many2One( + 'stock.location', "From", readonly=True) + to_location = fields.Many2One( + 'stock.location', "To", readonly=True) + delivery_period = fields.Many2One( + 'product.month', "Shipment Pd", readonly=True) + from_date = fields.Date("From Date", readonly=True) + to_date = fields.Date("To Date", readonly=True) + shipment_in = fields.Many2One( + 'stock.shipment.in', "Shipment", readonly=True) + vessel = fields.Many2One('trade.vessel', "Vessel", readonly=True) + bl_date = fields.Date("BL Date", readonly=True) + bl_number = fields.Char("BL Number", readonly=True) + etd = fields.Date("ETD", readonly=True) + eta = fields.Date("ETA", readonly=True) + actual_date = fields.Date("Actual Date", readonly=True) + strategy = fields.Many2One( + 'mtm.strategy', "Strategy", readonly=True) + pricing = fields.Selection([ + ('fixed', 'Fixed'), + ('unfixed', 'Unfixed'), + ], "Pricing", readonly=True) + price_type = fields.Selection([ + ('cash', 'Cash Price'), + ('priced', 'Priced'), + ('basis', 'Basis'), + ('efp', 'EFP'), + ], "Price Type", readonly=True) + unit_price = fields.Numeric( + "Unit Price", digits=(16, 4), readonly=True) + currency = fields.Many2One( + 'currency.currency', "Currency", readonly=True) + currency_symbol = fields.Function( + fields.Char("Currency Symbol"), 'get_currency_symbol') + quantity = fields.Numeric("Quantity", digits=(16, 3), readonly=True) + unit = fields.Many2One('product.uom', "Unit", readonly=True) + amount = fields.Numeric("Value", digits=(16, 2), readonly=True) + direction = fields.Selection([ + ('long', 'LONG'), + ('short', 'SHORT'), + ], "Direction", readonly=True) + + def _line(self): + if self.contract_type == 'purchase': + return self.purchase_line + return self.sale_line + + def get_line_display(self, name=None): + line = self._line() + if not line: + return None + return getattr(line, name, None) + + 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 _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 _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): + return Case( + (price_type.in_(['cash', 'priced']), 'fixed'), + else_='unfixed') + + @classmethod + def _detail_source(cls): + context = Transaction().context + LotQt = Pool().get('lot.qt') + return LotQt.getQuery( + purchase=context.get('purchase'), + sale=context.get('sale'), + shipment=context.get('shipment'), + type=context.get('matching_status'), + state=context.get('availability'), + qttype=None, + supplier=context.get('supplier'), + client=context.get('client'), + ps='all', + lot_status=context.get('lot_status'), + group='all', + product=context.get('product'), + location=context.get('location'), + origin=context.get('lot_origin'), + finished=context.get('display_finished'), + shipping_status=context.get('shipping_status'), + asof=context.get('asof'), + todate=context.get('todate'), + dimension=context.get('dimension'), + strategy=context.get('strategy'), + vessel=context.get('vessel'), + reference=context.get('reference')) + + @classmethod + def _apply_common_filters(cls, where, product, commodity, pricing, + direction, period, prefix, counterparty, side): + context = Transaction().context + if context.get('commodity'): + where &= commodity == context['commodity'] + if context.get('pricing_status'): + where &= pricing == context['pricing_status'] + if context.get('direction'): + if context['direction'] == 'flat': + where &= Literal(False) + elif context['direction'] != direction: + where &= Literal(False) + if context.get('shipment_period'): + where &= period == context['shipment_period'] + where = CTRMLongShortMixin._apply_period_filter( + where, period, prefix) + if context.get('counterparty'): + where &= counterparty == context['counterparty'] + return where + + @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') + ShipmentIn = pool.get('stock.shipment.in') + PurchaseStrategy = pool.get('purchase.strategy') + SaleStrategy = pool.get('sale.strategy') + + purchase_line = PurchaseLine.__table__() + purchase = Purchase.__table__() + sale_line = SaleLine.__table__() + sale = Sale.__table__() + shipment = ShipmentIn.__table__() + 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) + purchase_category = cls._category_query() + sale_category = cls._category_query() + purchase_source = cls._detail_source() + sale_source = cls._detail_source() + + purchase_quantity = Abs(Coalesce(purchase_source.r_tot, 0)) + purchase_pricing = cls._line_pricing(purchase_line.price_type) + purchase_where = ( + (purchase_source.r_line != Null) + & (purchase_line.product != Null)) + purchase_where = cls._apply_common_filters( + purchase_where, purchase_line.product, purchase_category.commodity, + purchase_pricing, 'long', purchase_line.del_period, 'purchase', + purchase.party, 'purchase') + purchase_query = ( + purchase_source + .join(purchase_line, + condition=purchase_source.r_line == purchase_line.id) + .join(purchase, condition=purchase_line.purchase == purchase.id) + .join(purchase_category, 'LEFT', + condition=purchase_category.product == purchase_line.product) + .join(shipment, 'LEFT', + condition=purchase_source.r_lot_shipment_in == shipment.id) + .join(purchase_strategy, 'LEFT', + condition=purchase_strategy.line == purchase_line.id) + .select( + (purchase_source.id * 2).as_('id'), + Literal('purchase').as_('contract_type'), + Coalesce(purchase.reference, purchase.our_reference).as_( + 'contract_reference'), + purchase.id.as_('purchase'), + purchase_line.id.as_('purchase_line'), + Literal(None).as_('sale'), + Literal(None).as_('sale_line'), + purchase.party.as_('counterparty'), + purchase_source.r_lot_p.as_('lot'), + purchase_source.r_lot_name.as_('lot_name'), + purchase_source.r_lot_type.as_('lot_type'), + purchase_source.r_shipping_status.as_('shipping_status'), + purchase_source.r_lot_status.as_('lot_status'), + purchase_source.r_lot_av.as_('availability'), + purchase_line.product.as_('product'), + purchase_category.commodity.as_('commodity'), + purchase.incoterm.as_('incoterm'), + purchase.incoterm_location.as_('incoterm_location'), + purchase_source.r_from_l.as_('from_location'), + purchase_source.r_from_t.as_('to_location'), + purchase_line.del_period.as_('delivery_period'), + purchase_line.from_del.as_('from_date'), + purchase_line.to_del.as_('to_date'), + purchase_source.r_lot_shipment_in.as_('shipment_in'), + purchase_source.r_vessel.as_('vessel'), + purchase_source.r_bl_date.as_('bl_date'), + purchase_source.r_bl_number.as_('bl_number'), + shipment.etd.as_('etd'), + Coalesce(shipment.etad, shipment.eta).as_('eta'), + shipment.effective_date.as_('actual_date'), + purchase_strategy.strategy.as_('strategy'), + purchase_pricing.as_('pricing'), + purchase_line.price_type.as_('price_type'), + purchase_line.unit_price.as_('unit_price'), + purchase.currency.as_('currency'), + purchase_quantity.as_('quantity'), + purchase_line.unit.as_('unit'), + (purchase_quantity * Coalesce( + purchase_line.unit_price, 0)).as_('amount'), + Literal('long').as_('direction'), + where=purchase_where)) + + sale_quantity = Abs(Coalesce(sale_source.r_tot, 0)) + sale_pricing = cls._line_pricing(sale_line.price_type) + sale_where = ( + (sale_source.r_sale_line != Null) + & (sale_line.product != Null)) + sale_where = cls._apply_common_filters( + sale_where, sale_line.product, sale_category.commodity, + sale_pricing, 'short', sale_line.del_period, 'sale', + sale.party, 'sale') + sale_query = ( + sale_source + .join(sale_line, + condition=sale_source.r_sale_line == sale_line.id) + .join(sale, condition=sale_line.sale == sale.id) + .join(sale_category, 'LEFT', + condition=sale_category.product == sale_line.product) + .join(shipment, 'LEFT', + condition=sale_source.r_lot_shipment_in == shipment.id) + .join(sale_strategy, 'LEFT', + condition=sale_strategy.line == sale_line.id) + .select( + (sale_source.id * 2 + 1).as_('id'), + Literal('sale').as_('contract_type'), + Coalesce(sale.reference, sale.our_reference).as_( + 'contract_reference'), + Literal(None).as_('purchase'), + Literal(None).as_('purchase_line'), + sale.id.as_('sale'), + sale_line.id.as_('sale_line'), + sale.party.as_('counterparty'), + sale_source.r_lot_p.as_('lot'), + sale_source.r_lot_name.as_('lot_name'), + sale_source.r_lot_type.as_('lot_type'), + sale_source.r_shipping_status.as_('shipping_status'), + sale_source.r_lot_status.as_('lot_status'), + sale_source.r_lot_av.as_('availability'), + sale_line.product.as_('product'), + sale_category.commodity.as_('commodity'), + sale.incoterm.as_('incoterm'), + sale.incoterm_location.as_('incoterm_location'), + sale_source.r_from_l.as_('from_location'), + sale_source.r_from_t.as_('to_location'), + sale_line.del_period.as_('delivery_period'), + sale_line.from_del.as_('from_date'), + sale_line.to_del.as_('to_date'), + sale_source.r_lot_shipment_in.as_('shipment_in'), + sale_source.r_vessel.as_('vessel'), + sale_source.r_bl_date.as_('bl_date'), + sale_source.r_bl_number.as_('bl_number'), + shipment.etd.as_('etd'), + Coalesce(shipment.etad, shipment.eta).as_('eta'), + shipment.effective_date.as_('actual_date'), + sale_strategy.strategy.as_('strategy'), + sale_pricing.as_('pricing'), + sale_line.price_type.as_('price_type'), + sale_line.unit_price.as_('unit_price'), + sale.currency.as_('currency'), + sale_quantity.as_('quantity'), + sale_line.unit.as_('unit'), + (sale_quantity * Coalesce( + sale_line.unit_price, 0)).as_('amount'), + Literal('short').as_('direction'), + where=sale_where)) + + detail = Union(purchase_query, sale_query, all_=True) + return detail.select( + Literal(0).as_('create_uid'), + CurrentTimestamp().as_('create_date'), + Literal(None).as_('write_uid'), + Literal(None).as_('write_date'), + detail.id.as_('id'), + detail.contract_type.as_('contract_type'), + detail.contract_reference.as_('contract_reference'), + detail.purchase.as_('purchase'), + detail.purchase_line.as_('purchase_line'), + detail.sale.as_('sale'), + detail.sale_line.as_('sale_line'), + detail.counterparty.as_('counterparty'), + detail.lot.as_('lot'), + detail.lot_name.as_('lot_name'), + detail.lot_type.as_('lot_type'), + detail.shipping_status.as_('shipping_status'), + detail.lot_status.as_('lot_status'), + detail.availability.as_('availability'), + detail.product.as_('product'), + detail.commodity.as_('commodity'), + detail.incoterm.as_('incoterm'), + detail.incoterm_location.as_('incoterm_location'), + detail.from_location.as_('from_location'), + detail.to_location.as_('to_location'), + detail.delivery_period.as_('delivery_period'), + detail.from_date.as_('from_date'), + detail.to_date.as_('to_date'), + detail.shipment_in.as_('shipment_in'), + detail.vessel.as_('vessel'), + detail.bl_date.as_('bl_date'), + detail.bl_number.as_('bl_number'), + detail.etd.as_('etd'), + detail.eta.as_('eta'), + detail.actual_date.as_('actual_date'), + detail.strategy.as_('strategy'), + detail.pricing.as_('pricing'), + detail.price_type.as_('price_type'), + detail.unit_price.as_('unit_price'), + detail.currency.as_('currency'), + detail.quantity.as_('quantity'), + detail.unit.as_('unit'), + detail.amount.as_('amount'), + detail.direction.as_('direction'), + order_by=[ + detail.delivery_period, detail.product, detail.contract_type, + detail.contract_reference, detail.lot_name]) + + class CTRMPnlContextMixin: date = fields.Date("Valuation Date") product = fields.Many2One('product.product', "Product") diff --git a/modules/purchase_trade/global_reporting.xml b/modules/purchase_trade/global_reporting.xml index 4e3decf..635c36e 100644 --- a/modules/purchase_trade/global_reporting.xml +++ b/modules/purchase_trade/global_reporting.xml @@ -55,6 +55,16 @@ tree ctrm_open_position_list + + ctrm.reporting.position.long_short.detail.context + form + ctrm_long_short_detail_context_form + + + ctrm.reporting.position.long_short.detail + tree + ctrm_long_short_detail_list + ctrm.reporting.position.long_short.product_strategy tree @@ -86,6 +96,16 @@ + + Long & Short Detail + ctrm.reporting.position.long_short.detail + ctrm.reporting.position.long_short.detail.context + + + + + + Long & Short by Product & Strategy ctrm.reporting.position.long_short.product_strategy @@ -162,6 +182,12 @@ action="act_ctrm_open_position" sequence="20" id="menu_ctrm_positions_long_short"/> + + + + + + + + + + + + + + + + diff --git a/modules/purchase_trade/view/ctrm_long_short_detail_list.xml b/modules/purchase_trade/view/ctrm_long_short_detail_list.xml new file mode 100644 index 0000000..b8adda8 --- /dev/null +++ b/modules/purchase_trade/view/ctrm_long_short_detail_list.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +