diff --git a/modules/purchase_trade/__init__.py b/modules/purchase_trade/__init__.py index 118eab8..515cfa3 100755 --- a/modules/purchase_trade/__init__.py +++ b/modules/purchase_trade/__init__.py @@ -3,7 +3,32 @@ from trytond.pool import Pool -from . import purchase,sale,global_reporting,stock,derivative,lot,pricing,workflow,lc,dashboard,fee,payment_term,purchase_prepayment,cron,party,forex,outgoing,incoming,optional,association_tables, document_tracking, open_position, credit_risk +from . import ( + purchase, + sale, + global_reporting, + stock, + derivative, + lot, + pricing, + workflow, + lc, + dashboard, + fee, + payment_term, + purchase_prepayment, + cron, + party, + forex, + outgoing, + incoming, + optional, + association_tables, + document_tracking, + open_position, + credit_risk, + valuation, +) def register(): Pool.register( @@ -69,8 +94,8 @@ def register(): fee.Fee, fee.FeeLots, purchase.FeeLots, - fee.Valuation, - fee.ValuationDyn, + valuation.Valuation, + valuation.ValuationDyn, derivative.Derivative, derivative.DerivativeMatch, derivative.MatchWizardStart, diff --git a/modules/purchase_trade/fee.py b/modules/purchase_trade/fee.py index 02c74f6..62fd50e 100755 --- a/modules/purchase_trade/fee.py +++ b/modules/purchase_trade/fee.py @@ -21,117 +21,6 @@ from trytond.exceptions import UserWarning, UserError logger = logging.getLogger(__name__) -VALTYPE = [ - ('priced', 'Price'), - ('pur. priced', 'Pur. price'), - ('pur. efp', 'Pur. efp'), - ('sale priced', 'Sale price'), - ('sale efp', 'Sale efp'), - ('line fee', 'Line fee'), - ('pur. fee', 'Pur. fee'), - ('sale fee', 'Sale fee'), - ('shipment fee', 'Shipment fee'), - ('market', 'Market'), - ('derivative', 'Derivative'), -] - -class Valuation(ModelSQL,ModelView): - "Valuation" - __name__ = 'valuation.valuation' - - purchase = fields.Many2One('purchase.purchase',"Purchase") - line = fields.Many2One('purchase.line',"Purch. Line") - date = fields.Date("Date") - type = fields.Selection(VALTYPE, "Type") - reference = fields.Char("Reference") - counterparty = fields.Many2One('party.party',"Counterparty") - product = fields.Many2One('product.product',"Product") - state = fields.Char("State") - price = fields.Numeric("Price",digits='unit') - currency = fields.Many2One('currency.currency',"Cur") - quantity = fields.Numeric("Quantity",digits='unit') - unit = fields.Many2One('product.uom',"Unit") - amount = fields.Numeric("Amount",digits='unit') - mtm = fields.Numeric("Mtm",digits='unit') - lot = fields.Many2One('lot.lot',"Lot") - -class ValuationDyn(ModelSQL,ModelView): - "Valuation" - __name__ = 'valuation.valuation.dyn' - - r_purchase = fields.Many2One('purchase.purchase',"Purchase") - r_line = fields.Many2One('purchase.line',"Line") - r_date = fields.Date("Date") - r_type = fields.Selection(VALTYPE, "Type") - r_reference = fields.Char("Reference") - r_counterparty = fields.Many2One('party.party',"Counterparty") - r_product = fields.Many2One('product.product',"Product") - r_state = fields.Char("State") - r_price = fields.Numeric("Price",digits='r_unit') - r_currency = fields.Many2One('currency.currency',"Cur") - r_quantity = fields.Numeric("Quantity",digits='r_unit') - r_unit = fields.Many2One('product.uom',"Unit") - r_amount = fields.Numeric("Amount",digits='r_unit') - r_mtm = fields.Numeric("Mtm",digits='r_unit') - r_lot = fields.Many2One('lot.lot',"Lot") - - @classmethod - def table_query(cls): - Valuation = Pool().get('valuation.valuation') - val = Valuation.__table__() - context = Transaction().context - group_pnl = context.get('group_pnl') - wh = (val.id > 0) - # query = val.select( - # Literal(0).as_('create_uid'), - # CurrentTimestamp().as_('create_date'), - # Literal(None).as_('write_uid'), - # Literal(None).as_('write_date'), - # val.id.as_('id'), - # val.purchase.as_('r_purchase'), - # val.line.as_('r_line'), - # val.date.as_('r_date'), - # val.type.as_('r_type'), - # val.reference.as_('r_reference'), - # val.counterparty.as_('r_counterparty'), - # val.product.as_('r_product'), - # val.state.as_('r_state'), - # val.price.as_('r_price'), - # val.currency.as_('r_currency'), - # val.quantity.as_('r_quantity'), - # val.unit.as_('r_unit'), - # val.amount.as_('r_amount'), - # val.mtm.as_('r_mtm'), - # val.lot.as_('r_lot'), - # where=wh) - - #if group_pnl==True: - query = val.select( - Literal(0).as_('create_uid'), - CurrentTimestamp().as_('create_date'), - Literal(None).as_('write_uid'), - Literal(None).as_('write_date'), - Max(val.id).as_('id'), - Max(val.purchase).as_('r_purchase'), - Max(val.line).as_('r_line'), - Max(val.date).as_('r_date'), - val.type.as_('r_type'), - Max(val.reference).as_('r_reference'), - val.counterparty.as_('r_counterparty'), - Max(val.product).as_('r_product'), - val.state.as_('r_state'), - Avg(val.price).as_('r_price'), - Max(val.currency).as_('r_currency'), - Sum(val.quantity).as_('r_quantity'), - Max(val.unit).as_('r_unit'), - Sum(val.amount).as_('r_amount'), - Sum(val.mtm).as_('r_mtm'), - Max(val.lot).as_('r_lot'), - where=wh, - group_by=[val.type,val.counterparty,val.state]) - - return query - def filter_state(state): def filter(func): @wraps(func) diff --git a/modules/purchase_trade/fee.xml b/modules/purchase_trade/fee.xml index dca08ce..424b39f 100755 --- a/modules/purchase_trade/fee.xml +++ b/modules/purchase_trade/fee.xml @@ -19,26 +19,6 @@ this repository contains the full copyright notices and license terms. --> fee_tree_sequence - - valuation.valuation - tree - valuation_tree_sequence3 - - - valuation.valuation - graph - valuation_graph - - - valuation.valuation - graph - valuation_graph2 - - - valuation.valuation.dyn - tree - valuation_tree_sequence4 - fee.fee tree diff --git a/modules/purchase_trade/purchase.py b/modules/purchase_trade/purchase.py index b81e2cc..24f529d 100755 --- a/modules/purchase_trade/purchase.py +++ b/modules/purchase_trade/purchase.py @@ -380,19 +380,11 @@ class Purchase(metaclass=PoolMeta): Decimal(str(line.quantity)) .quantize(Decimal("0.00001")) ) - Line.save([line]) #compute pnl Pnl = Pool().get('valuation.valuation') - pnl = Pnl.search([('line','=',line.id)]) - if pnl: - Pnl.delete(pnl) - pnl_lines = [] - pnl_lines.extend(line.get_pnl_fee_lines()) - pnl_lines.extend(line.get_pnl_price_lines()) - pnl_lines.extend(line.get_pnl_der_lines()) - Pnl.save(pnl_lines) + Pnl.generate(line) if line.quantity_theorical: OpenPosition = Pool().get('open.position') diff --git a/modules/purchase_trade/tryton.cfg b/modules/purchase_trade/tryton.cfg index 0ff0286..015d586 100755 --- a/modules/purchase_trade/tryton.cfg +++ b/modules/purchase_trade/tryton.cfg @@ -28,4 +28,5 @@ xml: party.xml forex.xml global_reporting.xml - derivative.xml \ No newline at end of file + derivative.xml + valuation.xml \ No newline at end of file diff --git a/modules/purchase_trade/valuation.py b/modules/purchase_trade/valuation.py new file mode 100644 index 0000000..8f3f00c --- /dev/null +++ b/modules/purchase_trade/valuation.py @@ -0,0 +1,383 @@ +from trytond.model import fields +from trytond.report import Report +from trytond.pool import Pool, PoolMeta +from trytond.pyson import Bool, Eval, Id, If +from trytond.model import (ModelSQL, ModelView) +from trytond.tools import is_full_text, lstrip_wildcard +from trytond.transaction import Transaction, inactive_records +from decimal import getcontext, Decimal, ROUND_HALF_UP +from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr +from sql.conditionals import Case +from sql import Column, Literal +from sql.functions import CurrentTimestamp, DateTrunc +from trytond.wizard import Button, StateTransition, StateView, Wizard +from itertools import chain, groupby +from operator import itemgetter +import datetime +import logging +from collections import defaultdict +from trytond.exceptions import UserWarning, UserError + +logger = logging.getLogger(__name__) + +VALTYPE = [ + ('priced', 'Price'), + ('pur. priced', 'Pur. price'), + ('pur. efp', 'Pur. efp'), + ('sale priced', 'Sale price'), + ('sale efp', 'Sale efp'), + ('line fee', 'Line fee'), + ('pur. fee', 'Pur. fee'), + ('sale fee', 'Sale fee'), + ('shipment fee', 'Shipment fee'), + ('market', 'Market'), + ('derivative', 'Derivative'), +] + +class Valuation(ModelSQL,ModelView): + "Valuation" + __name__ = 'valuation.valuation' + + purchase = fields.Many2One('purchase.purchase',"Purchase") + line = fields.Many2One('purchase.line',"Purch. Line") + date = fields.Date("Date") + type = fields.Selection(VALTYPE, "Type") + reference = fields.Char("Reference") + counterparty = fields.Many2One('party.party',"Counterparty") + product = fields.Many2One('product.product',"Product") + state = fields.Char("State") + price = fields.Numeric("Price",digits='unit') + currency = fields.Many2One('currency.currency',"Cur") + quantity = fields.Numeric("Quantity",digits='unit') + unit = fields.Many2One('product.uom',"Unit") + amount = fields.Numeric("Amount",digits='unit') + mtm = fields.Numeric("Mtm",digits='unit') + lot = fields.Many2One('lot.lot',"Lot") + + @classmethod + def _base_pnl(cls, *, line, lot, pnl_type, sale=None): + Date = Pool().get('ir.date') + pnl = Pool().get('valuation.valuation')() + + pnl.purchase = line.purchase.id + pnl.line = line.id + pnl.type = pnl_type + pnl.date = Date.today() + pnl.lot = lot.id + + if sale: + pnl.sale = sale.id + + return pnl + + @classmethod + def _build_basis_pnl(cls, *, line, lot, sale_line, pc, sign): + pnl = cls._base_pnl( + line=line, + lot=lot, + sale=sale_line.sale if sale_line else None, + pnl_type='sale priced' if sale_line else 'pur. priced' + ) + + qty = lot.get_current_quantity_converted() + + pnl.reference = f"{pc.get_name()} / {pc.ratio}%" + pnl.price = round(pc.price, 4) + pnl.counterparty = sale_line.sale.party if sale_line else line.purchase.party + pnl.product = sale_line.product if sale_line else line.product + + # State + if pc.unfixed_qt == 0: + pnl.state = 'fixed' + elif pc.fixed_qt == 0: + pnl.state = 'unfixed' + else: + base = sale_line.quantity_theorical if sale_line else line.quantity_theorical + pnl.state = f"part. fixed {round(pc.fixed_qt / Decimal(base) * 100, 0)}%" + + if pc.price and pc.ratio: + pnl.quantity = round(qty, 5) + pnl.amount = round(pc.price * qty * Decimal(sign) * pc.ratio / 100, 4) + + mtm = Decimal(0) + last_price = pc.get_last_price() + if last_price: + mtm = round(Decimal(last_price) * qty * Decimal(sign), 4) + + pnl.mtm = round(pnl.amount - (mtm * pc.ratio / 100), 4) + pnl.unit = sale_line.unit if sale_line else line.unit + pnl.currency = sale_line.sale.currency if sale_line else line.purchase.currency + + return pnl + + @classmethod + def _build_simple_pnl(cls, *, line, lot, sale_line, price, state, sign, pnl_type): + pnl = cls._base_pnl( + line=line, + lot=lot, + sale=sale_line.sale if sale_line else None, + pnl_type=pnl_type + ) + + qty = lot.get_current_quantity_converted() + pnl.price = round(price, 4) + pnl.quantity = round(qty, 5) + pnl.amount = round(pnl.price * qty * Decimal(sign), 4) + pnl.mtm = Decimal(0) + pnl.state = state + pnl.unit = sale_line.unit if sale_line else line.unit + pnl.currency = sale_line.sale.currency if sale_line else line.purchase.currency + pnl.counterparty = sale_line.sale.party if sale_line else line.purchase.party + pnl.product = sale_line.product if sale_line else line.product + + pnl.reference = 'Sale/Physic' if lot.lot_type == 'physic' else 'Sale/Open' if sale_line else 'Purchase/Physic' + + return pnl + + @classmethod + def create_pnl_price_from_line(cls, line): + price_lines = [] + LotQt = Pool().get('lot.qt') + + for lot in line.lots: + + # --- PURCHASE SIDE --- + if line.price_type == 'basis': + for pc in line.price_summary or []: + pnl = cls._build_basis_pnl(line=line, lot=lot, sale_line=None, pc=pc, sign=-1) + if pnl: + price_lines.append(pnl) + + elif line.price_type in ('priced', 'efp') and lot.lot_price: + state = 'fixed' if line.price_type == 'priced' else 'not fixed' + price_lines.append( + cls._build_simple_pnl( + line=line, + lot=lot, + sale_line=None, + price=lot.lot_price, + state=state, + sign=-1, + pnl_type=f'pur. {line.price_type}' + ) + ) + + # --- SALE SIDE --- + sale_lots = [lot] if lot.sale_line else [ + lqt.lot_s for lqt in LotQt.search([('lot_p','=',lot.id),('lot_s','>',0),('lot_quantity','>',0)]) + ] + + for sl in sale_lots: + sl_line = sl.sale_line + if not sl_line: + continue + + if sl_line.price_type == 'basis': + for pc in sl_line.price_summary or []: + pnl = cls._build_basis_pnl(line=line, lot=sl, sale_line=sl_line, pc=pc, sign=+1) + if pnl: + price_lines.append(pnl) + + elif sl_line.price_type in ('priced', 'efp'): + state = 'fixed' if sl_line.price_type == 'priced' else 'not fixed' + price = sl.lot_price_sale + price_lines.append( + cls._build_simple_pnl( + line=line, + lot=sl, + sale_line=sl_line, + price=price, + state=state, + sign=+1, + pnl_type=f'sale {sl_line.price_type}' + ) + ) + + return price_lines + + @classmethod + def group_fees_by_type_supplier(cls,line,fees): + grouped = defaultdict(list) + + # Regrouper par (type, supplier) + for fee in fees: + key = (fee.product, fee.supplier) + grouped[key].append(fee) + result = [] + for key, fee_list in grouped.items(): + ordered_fees = [f for f in fee_list if f.type == 'ordered'] + if ordered_fees: + result.extend(ordered_fees) + else: + budgeted_fees = [f for f in fee_list if f.type == 'budgeted'] + result.extend(budgeted_fees) + return result + + @classmethod + def create_pnl_fee_from_line(cls,line): + fee_lines = [] + #pnl management + Pnl = Pool().get('valuation.valuation') + Date = Pool().get('ir.date') + Currency = Pool().get('currency.currency') + FeeLots = Pool().get('fee.lots') + if line.lots: + for lot in line.lots: + fl = FeeLots.search(['lot','=',lot.id]) + if fl: + fees = [e.fee for e in fl] + sorted_fees = cls.group_fees_by_type_supplier(line,fees) + if sorted_fees: + for sf in sorted_fees: + pnl = Pnl() + pnl.lot = lot.id + if lot.sale_line: + pnl.sale = lot.sale_line.sale.id + pnl.purchase = line.purchase.id + pnl.line = line.id + if sf.line: + pnl.type = 'pur. fee' + if sf.sale_line: + pnl.type = 'sale fee' + if sf.shipment_in: + pnl.type = 'shipment fee' + pnl.date = Date.today() + pnl.price = Decimal(sf.get_price_per_qt()) + if sf.currency != line.purchase.currency: + with Transaction().set_context(date=Date.today()): + pnl.price = Currency.compute(sf.currency,pnl.price, line.purchase.currency) + pnl.counterparty = sf.supplier + str_op = '' + if lot.lot_type == 'physic': + str_op = '/Physic' + else: + str_op = '/Open' + pnl.reference = sf.product.name + str_op + pnl.product = sf.product + pnl.state = sf.type + if sf.p_r == 'pay': + sign = -1 + pnl.amount = round(pnl.price * lot.get_current_quantity_converted() * sign,2) + pnl.mtm = 0 + pnl.quantity = round(lot.get_current_quantity_converted(),5) + pnl.unit = sf.unit if sf.unit else line.unit + pnl.currency = sf.currency + fee_lines.append(pnl) + + return fee_lines + + @classmethod + def create_pnl_der_from_line(cls,line): + der_lines = [] + if line.derivatives: + Pnl = Pool().get('valuation.valuation') + Date = Pool().get('ir.date') + for d in line.derivatives: + pnl = Pnl() + pnl.purchase = line.purchase.id + pnl.line = line.id + pnl.type = 'derivative' + pnl.date = Date.today() + pnl.reference = d.price_index.price_index + pnl.price = round(Decimal(d.price_index.get_price_per_qt(d.price,line.unit,line.purchase.currency)),4) + pnl.counterparty = d.party + pnl.product = d.product + pnl.state = 'fixed' + pnl.amount = round(pnl.price * (d.quantity) * Decimal(-1),4) + mtm = round(Decimal(d.price_index.get_price(Date.today(),line.unit,line.purchase.currency,True)) * d.quantity * Decimal(-1),4) + pnl.mtm = pnl.amount - mtm + pnl.quantity = round(d.quantity,5) + pnl.unit = line.unit + pnl.currency = line.purchase.currency + der_lines.append(pnl) + return der_lines + + @classmethod + def generate(cls, line): + Date = Pool().get('ir.date') + Pnl = Pool().get('valuation.valuation') + pnl = Pnl.search([('line','=',line.id),('date','=',Date.today())]) + if pnl: + Pnl.delete(pnl) + pnl_lines = [] + pnl_lines.extend(cls.create_pnl_fee_from_line(line)) + pnl_lines.extend(cls.create_pnl_price_from_line(line)) + pnl_lines.extend(cls.create_pnl_der_from_line(line)) + Pnl.save(pnl_lines) + +class ValuationDyn(ModelSQL,ModelView): + "Valuation" + __name__ = 'valuation.valuation.dyn' + + r_purchase = fields.Many2One('purchase.purchase',"Purchase") + r_line = fields.Many2One('purchase.line',"Line") + r_date = fields.Date("Date") + r_type = fields.Selection(VALTYPE, "Type") + r_reference = fields.Char("Reference") + r_counterparty = fields.Many2One('party.party',"Counterparty") + r_product = fields.Many2One('product.product',"Product") + r_state = fields.Char("State") + r_price = fields.Numeric("Price",digits='r_unit') + r_currency = fields.Many2One('currency.currency',"Cur") + r_quantity = fields.Numeric("Quantity",digits='r_unit') + r_unit = fields.Many2One('product.uom',"Unit") + r_amount = fields.Numeric("Amount",digits='r_unit') + r_mtm = fields.Numeric("Mtm",digits='r_unit') + r_lot = fields.Many2One('lot.lot',"Lot") + + @classmethod + def table_query(cls): + Valuation = Pool().get('valuation.valuation') + val = Valuation.__table__() + context = Transaction().context + group_pnl = context.get('group_pnl') + wh = (val.id > 0) + # query = val.select( + # Literal(0).as_('create_uid'), + # CurrentTimestamp().as_('create_date'), + # Literal(None).as_('write_uid'), + # Literal(None).as_('write_date'), + # val.id.as_('id'), + # val.purchase.as_('r_purchase'), + # val.line.as_('r_line'), + # val.date.as_('r_date'), + # val.type.as_('r_type'), + # val.reference.as_('r_reference'), + # val.counterparty.as_('r_counterparty'), + # val.product.as_('r_product'), + # val.state.as_('r_state'), + # val.price.as_('r_price'), + # val.currency.as_('r_currency'), + # val.quantity.as_('r_quantity'), + # val.unit.as_('r_unit'), + # val.amount.as_('r_amount'), + # val.mtm.as_('r_mtm'), + # val.lot.as_('r_lot'), + # where=wh) + + #if group_pnl==True: + query = val.select( + Literal(0).as_('create_uid'), + CurrentTimestamp().as_('create_date'), + Literal(None).as_('write_uid'), + Literal(None).as_('write_date'), + Max(val.id).as_('id'), + Max(val.purchase).as_('r_purchase'), + Max(val.line).as_('r_line'), + Max(val.date).as_('r_date'), + val.type.as_('r_type'), + Max(val.reference).as_('r_reference'), + val.counterparty.as_('r_counterparty'), + Max(val.product).as_('r_product'), + val.state.as_('r_state'), + Avg(val.price).as_('r_price'), + Max(val.currency).as_('r_currency'), + Sum(val.quantity).as_('r_quantity'), + Max(val.unit).as_('r_unit'), + Sum(val.amount).as_('r_amount'), + Sum(val.mtm).as_('r_mtm'), + Max(val.lot).as_('r_lot'), + where=wh, + group_by=[val.type,val.counterparty,val.state]) + + return query diff --git a/modules/purchase_trade/valuation.xml b/modules/purchase_trade/valuation.xml new file mode 100644 index 0000000..63ba7dc --- /dev/null +++ b/modules/purchase_trade/valuation.xml @@ -0,0 +1,24 @@ + + + + valuation.valuation + tree + valuation_tree_sequence3 + + + valuation.valuation + graph + valuation_graph + + + valuation.valuation + graph + valuation_graph2 + + + valuation.valuation.dyn + tree + valuation_tree_sequence4 + + + \ No newline at end of file