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"/>
+