import datetime from trytond.model import ModelSQL, ModelView, fields from trytond.pool import Pool from trytond.transaction import Transaction from sql import Literal from sql.functions import CurrentTimestamp from trytond.wizard import Button, StateTransition, StateView, Wizard from trytond.pyson import Bool, Eval, Id, If class DerivativeMatch(ModelSQL, ModelView): "Derivative Match" __name__ = 'derivative.match' long_position = fields.Many2One('derivative.derivative', 'Long Position', required=True) short_position = fields.Many2One('derivative.derivative', 'Short Position', required=True) quantity = fields.Numeric('Matched Quantity', digits='unit', required=True) match_date = fields.Date('Match Date', required=True) method = fields.Selection([ ('fifo', 'FIFO'), ('lifo', 'LIFO'), ('manual', 'Manual'), ], 'Method', required=True) pnl = fields.Function(fields.Numeric('PnL', digits='currency'), 'get_pnl') def get_pnl(self, name): return (self.short_position.price - self.long_position.price) * self.quantity class Derivative(ModelSQL,ModelView): "Derivative" __name__ = 'derivative.derivative' purchase = fields.Many2One('purchase.purchase',"Purchase") line = fields.Many2One('purchase.line',"Purch. Line") efp = fields.Boolean("EFP") price_index = fields.Many2One('price.price',"Curve") nb_ct = fields.Integer("Nb ct") price = fields.Numeric("Entry price",digits='currency') exit_price = fields.Numeric("Exit price",digits='currency') product = fields.Many2One('product.product',"Product") party = fields.Many2One('party.party','Supplier') currency = fields.Function(fields.Many2One('currency.currency',"Cur"),'get_cur') quantity = fields.Function(fields.Numeric("Quantity",digits='unit'),'get_qt') alloc_qty = fields.Function(fields.Numeric("Alloc. Qty",digits='unit'),'get_alloc_qt') unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit') amount = fields.Function(fields.Numeric("Amount",digits='currency'),'get_amount') currency2 = fields.Function(fields.Many2One('currency.currency',"Cur"),'get_cur2') trade_date = fields.Date('Trade Date') maturity_date = fields.Date('Maturity') open_qty = fields.Numeric('Open Quantity', digits='unit') matches_long = fields.One2Many('derivative.match', 'long_position', 'Matches (Long)', states={'invisible': (Eval('direction') == 'long')}) matches_short = fields.One2Many('derivative.match', 'short_position', 'Matches (Short)', states={'invisible': (Eval('direction') == 'short')}) direction = fields.Selection([ ('long', 'Long'), ('short', 'Short'), ], 'Direction', required=True) state = fields.Selection([ ('open', 'Open'), ('closed', 'Closed'), ], 'State', required=True) @classmethod def default_state(cls): return 'open' def get_amount(self,name): if self.price_index and self.line and self.price and self.nb_ct: #pi = Pool().get('price.price')(self.price_index) return self.price_index.get_amount_nb_ct(self.price,self.nb_ct,self.line.unit,self.line.purchase.currency if self.line.purchase else self.sale_line.sale.currency) def get_cur(self,name): if self.price_index: #pi = Pool().get('price.price')(self.price_index) return self.price_index.price_currency def get_cur2(self,name): if self.purchase: return self.purchase.currency def get_unit(self,name): if self.line: return self.line.unit def get_qt(self,name): if self.line: line = self.line else: if self.purchase and self.purchase.lines: line = self.purchase.lines[0] else: line = None if self.price_index and self.nb_ct and line: return self.price_index.get_qt(self.nb_ct,line.unit) def get_alloc_qt(self,name): if self.line: line = self.line else: if self.purchase and self.purchase.lines: line = self.purchase.lines[0] else: line = None if self.price_index and self.nb_ct and line: if line.price_summary: return line.price_summary[0].fixed_qt class MatchWizardStart(ModelView): "Match Selected Derivatives" __name__ = 'derivative.match.start' method = fields.Selection([ ('fifo', 'FIFO'), ('lifo', 'LIFO'), ('manual', 'Manual'), ], 'Method', required=True) quantity = fields.Numeric( 'Quantity to Match', digits='unit', required=True ) class DerivativeMatchWizard(Wizard): "Derivative Match Wizard" __name__ = 'derivative.match.wizard' start = StateView( 'derivative.match.start', 'purchase_trade.derivative_match_start_form_view', [ Button('Cancel', 'end', 'tryton-cancel'), Button('Match', 'match', 'tryton-ok', default=True), ] ) match = StateTransition() def transition_match(self): pool = Pool() Derivative = pool.get('derivative.derivative') Date = pool.get('ir.date') Match = pool.get('derivative.match') active_ids = Transaction().context.get('active_ids', []) if not active_ids: return 'end' positions = Derivative.browse(active_ids) longs = [ p for p in positions if p.direction == 'long' and p.state == 'open' and p.open_qty > 0 ] shorts = [ p for p in positions if p.direction == 'short' and p.state == 'open' and p.open_qty > 0 ] if not longs or not shorts: return 'end' # FIFO / LIFO ordering reverse = self.start.method == 'lifo' longs.sort(key=lambda p: p.trade_date or datetime.date.min, reverse=reverse) shorts.sort(key=lambda p: p.trade_date or datetime.date.min, reverse=reverse) remaining_qty = self.start.quantity for long_pos in longs: if remaining_qty <= 0: break for short_pos in shorts: if remaining_qty <= 0: break match_qty = min( long_pos.open_qty, short_pos.open_qty, remaining_qty ) if match_qty <= 0: continue Match.create([{ 'long_position': long_pos.id, 'short_position': short_pos.id, 'quantity': match_qty, 'method': self.start.method, 'match_date': Date.today(), }]) long_pos.open_qty -= match_qty short_pos.open_qty -= match_qty remaining_qty -= match_qty if long_pos.open_qty == 0: long_pos.state = 'closed' if short_pos.open_qty == 0: short_pos.state = 'closed' long_pos.save() short_pos.save() return 'end' class DerivativeReport(ModelSQL, ModelView): "Derivative Position Report" __name__ = 'derivative.report' r_derivative = fields.Many2One( 'derivative.derivative', "Derivative" ) r_trade_date = fields.Date("Trade Date") r_maturity_date = fields.Date("Maturity") r_product = fields.Many2One( 'product.product', "Product" ) r_party = fields.Many2One( 'party.party', "Counterparty" ) r_direction = fields.Selection([ (None, ''), ('long', 'Long'), ('short', 'Short'), ], 'Direction') r_state = fields.Selection([ ('open', 'Open'), ('closed', 'Closed'), ], 'State') r_quantity = fields.Function(fields.Numeric("Initial Qty",digits='unit'),'get_qt') r_open_qty = fields.Numeric( "Open Qty", digits='unit' ) r_price = fields.Numeric( "Entry Price", digits='currency' ) r_currency = fields.Function(fields.Many2One('currency.currency',"Currency"),'get_cur') def get_cur(self,name): if self.r_derivative: if self.r_derivative.price_index: return self.r_derivative.price_index.price_currency def get_qt(self,name): if self.r_derivative: if self.r_derivative.line: line = self.r_derivative.line else: if self.r_derivative.purchase and self.r_derivative.purchase.lines: line = self.r_derivative.purchase.lines[0] else: line = None if self.r_derivative.price_index and self.r_derivative.nb_ct and line: return self.r_derivative.price_index.get_qt(self.r_derivative.nb_ct,line.unit) # ------------------------------------------------------------ # TABLE QUERY # ------------------------------------------------------------ @classmethod def table_query(cls): pool = Pool() Derivative = pool.get('derivative.derivative') d = Derivative.__table__() context = Transaction().context product = context.get('product') party = context.get('party') direction = context.get('direction') state = context.get('state') trade_from = context.get('trade_from') trade_to = context.get('trade_to') maturity_from = context.get('maturity_from') maturity_to = context.get('maturity_to') open_only = context.get('open_only') wh = Literal(True) if product: wh &= (d.product == product) if party: wh &= (d.party == party) if direction: wh &= (d.direction == direction) if state: wh &= (d.state == state) if open_only: wh &= (d.open_qty > 0) if trade_from: wh &= (d.trade_date >= trade_from) if trade_to: wh &= (d.trade_date <= trade_to) if maturity_from: wh &= (d.maturity_date >= maturity_from) if maturity_to: wh &= (d.maturity_date <= maturity_to) query = d.select( # mandatory technical fields Literal(0).as_('create_uid'), CurrentTimestamp().as_('create_date'), Literal(None).as_('write_uid'), Literal(None).as_('write_date'), d.id.as_('id'), d.id.as_('r_derivative'), d.trade_date.as_('r_trade_date'), d.maturity_date.as_('r_maturity_date'), d.product.as_('r_product'), d.party.as_('r_party'), d.direction.as_('r_direction'), d.state.as_('r_state'), #d.quantity.as_('r_quantity'), d.open_qty.as_('r_open_qty'), d.price.as_('r_price'), where=wh ) return query # ------------------------------------------------------------ # SEARCH NAME # ------------------------------------------------------------ @classmethod def search_rec_name(cls, name, clause): _, operator, operand, *extra = clause return [ 'OR', ('r_product', operator, operand, *extra), ('r_party', operator, operand, *extra), ] class DerivativeReportContext(ModelView): "Derivative Report Context" __name__ = 'derivative.report.context' trade_from = fields.Date("Trade Date From") trade_to = fields.Date("Trade Date To") maturity_from = fields.Date("Maturity From") maturity_to = fields.Date("Maturity To") product = fields.Many2One( 'product.product', "Product" ) party = fields.Many2One( 'party.party', "Counterparty" ) direction = fields.Selection([ ('long', 'Long'), ('short', 'Short'), ], 'Direction') state = fields.Selection([ ('open', 'Open'), ('closed', 'Closed'), ], 'State') open_only = fields.Boolean("Open Positions Only") @classmethod def default_trade_from(cls): return datetime.date(1999, 1, 1) @classmethod def default_trade_to(cls): pool = Pool() Date = pool.get('ir.date') return Date.today()