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 ValuationBase(ModelSQL): 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") base_amount = fields.Numeric("Base Amount",digits='unit') rate = fields.Numeric("Rate", digits=(16,6)) @classmethod def _base_pnl(cls, *, line, lot, pnl_type, sale=None): Date = Pool().get('ir.date') values = { 'purchase': line.purchase.id, 'line': line.id, 'type': pnl_type, 'date': Date.today(), 'lot': lot.id, } if sale: values['sale'] = sale.id return values @classmethod def _build_basis_pnl(cls, *, line, lot, sale_line, pc, sign): Currency = Pool().get('currency.currency') Date = Pool().get('ir.date') values = 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() values.update({ 'reference': f"{pc.get_name()} / {pc.ratio}%", 'price': round(pc.price, 4), 'counterparty': sale_line.sale.party.id if sale_line else line.purchase.party.id, 'product': sale_line.product.id if sale_line else line.product.id, }) # State if pc.unfixed_qt == 0: values['state'] = 'fixed' elif pc.fixed_qt == 0: values['state'] = 'unfixed' else: base = sale_line.quantity_theorical if sale_line else line.quantity_theorical values['state'] = f"part. fixed {round(pc.fixed_qt / Decimal(base) * 100, 0)}%" if pc.price and pc.ratio: amount = round(pc.price * qty * Decimal(sign) * pc.ratio / 100, 4) base_amount = amount currency = sale_line.sale.currency.id if sale_line else line.purchase.currency.id rate = Decimal(1) if line.purchase.company.currency != currency: with Transaction().set_context(date=Date.today()): base_amount = Currency.compute(currency,amount, line.purchase.company.currency) rate = round(amount / base_amount,6) last_price = pc.get_last_price() mtm = round(Decimal(last_price) * qty * Decimal(sign), 4) if last_price else Decimal(0) values.update({ 'quantity': round(qty, 5), 'amount': amount, 'base_amount': base_amount, 'rate': rate, 'mtm': round(amount - (mtm * pc.ratio / 100), 4), 'unit': sale_line.unit.id if sale_line else line.unit.id, 'currency': currency, }) return values @classmethod def _build_simple_pnl(cls, *, line, lot, sale_line, price, state, sign, pnl_type): Currency = Pool().get('currency.currency') Date = Pool().get('ir.date') values = 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() amount = round(price * qty * Decimal(sign), 4) base_amount = amount currency = sale_line.sale.currency.id if sale_line else line.purchase.currency.id company_currency = sale_line.sale.company.currency if sale_line else line.purchase.company.currency rate = Decimal(1) if line.purchase.company.currency != currency: with Transaction().set_context(date=Date.today()): base_amount = Currency.compute(currency,amount, company_currency) if base_amount and amount: rate = round(amount / base_amount,6) values.update({ 'price': round(price, 4), 'quantity': round(qty, 5), 'amount': amount, 'base_amount': base_amount, 'rate': rate, 'mtm': Decimal(0), 'state': state, 'unit': sale_line.unit.id if sale_line else line.unit.id, 'currency': currency, 'counterparty': sale_line.sale.party.id if sale_line else line.purchase.party.id, 'product': sale_line.product.id if sale_line else line.product.id, 'reference': ( 'Sale/Physic' if lot.lot_type == 'physic' else 'Sale/Open' if sale_line else 'Purchase/Physic' ), }) return values @classmethod def create_pnl_price_from_line(cls, line): price_lines = [] LotQt = Pool().get('lot.qt') for lot in line.lots: if line.price_type == 'basis': for pc in line.price_summary or []: values = cls._build_basis_pnl(line=line, lot=lot, sale_line=None, pc=pc, sign=-1) if values: price_lines.append(values) elif line.price_type in ('priced', 'efp') and lot.lot_price: price_lines.append( cls._build_simple_pnl( line=line, lot=lot, sale_line=None, price=lot.lot_price, state='fixed' if line.price_type == 'priced' else 'not fixed', sign=-1, pnl_type=f'pur. {line.price_type}' ) ) 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 []: values = cls._build_basis_pnl(line=line, lot=sl, sale_line=sl_line, pc=pc, sign=+1) if values: price_lines.append(values) elif sl_line.price_type in ('priced', 'efp'): price_lines.append( cls._build_simple_pnl( line=line, lot=sl, sale_line=sl_line, price=sl.lot_price_sale, state='fixed' if sl_line.price_type == 'priced' else 'not fixed', 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 = [] Date = Pool().get('ir.date') Currency = Pool().get('currency.currency') FeeLots = Pool().get('fee.lots') for lot in line.lots or []: fl = FeeLots.search([('lot', '=', lot.id)]) if not fl: continue fees = [e.fee for e in fl] for sf in cls.group_fees_by_type_supplier(line, fees): price = Decimal(sf.get_price_per_qt()) if sf.currency != line.purchase.currency: with Transaction().set_context(date=Date.today()): price = Currency.compute(sf.currency, price, line.purchase.currency) sign = -1 if sf.p_r == 'pay' else 1 fee_lines.append({ 'lot': lot.id, 'sale': lot.sale_line.sale.id if lot.sale_line else None, 'purchase': line.purchase.id, 'line': line.id, 'type': ( 'shipment fee' if sf.shipment_in else 'sale fee' if sf.sale_line else 'pur. fee' ), 'date': Date.today(), 'price': price, 'counterparty': sf.supplier.id, 'reference': f"{sf.product.name}/{'Physic' if lot.lot_type == 'physic' else 'Open'}", 'product': sf.product.id, 'state': sf.type, 'quantity': round(lot.get_current_quantity_converted(), 5), 'amount': round(price * lot.get_current_quantity_converted() * sign, 2), 'mtm': Decimal(0), 'unit': sf.unit.id if sf.unit else line.unit.id, 'currency': sf.currency.id, }) return fee_lines @classmethod def create_pnl_der_from_line(cls, line): Date = Pool().get('ir.date') der_lines = [] for d in line.derivatives or []: price = Decimal(d.price_index.get_price_per_qt( d.price, line.unit, line.purchase.currency )) mtm = Decimal(d.price_index.get_price( Date.today(), line.unit, line.purchase.currency, True )) der_lines.append({ 'purchase': line.purchase.id, 'line': line.id, 'type': 'derivative', 'date': Date.today(), 'reference': d.price_index.price_index, 'price': round(price, 4), 'counterparty': d.party.id, 'product': d.product.id, 'state': 'fixed', 'quantity': round(d.quantity, 5), 'amount': round(price * d.quantity * Decimal(-1), 4), 'mtm': round((price * d.quantity * Decimal(-1)) - (mtm * d.quantity * Decimal(-1)), 4), 'unit': line.unit.id, 'currency': line.purchase.currency.id, }) return der_lines @classmethod def generate(cls, line): Date = Pool().get('ir.date') Valuation = Pool().get('valuation.valuation') ValuationLine = Pool().get('valuation.valuation.line') Valuation.delete(Valuation.search([ ('line', '=', line.id), ('date', '=', Date.today()), ])) ValuationLine.delete(ValuationLine.search([ ('line', '=', line.id), ])) values = [] values.extend(cls.create_pnl_fee_from_line(line)) values.extend(cls.create_pnl_price_from_line(line)) values.extend(cls.create_pnl_der_from_line(line)) Valuation.create(values) ValuationLine.create(values) class Valuation(ValuationBase, ModelView): "Valuation" __name__ = 'valuation.valuation' @classmethod def get_totals(cls): cursor = Transaction().connection.cursor() table = cls.__table__() sql = f""" WITH ranked AS ( SELECT CASE WHEN line IS NOT NULL THEN 'P:' || line::text WHEN sale_line IS NOT NULL THEN 'S:' || sale_line::text END AS block_key, date, amount, ROW_NUMBER() OVER ( PARTITION BY CASE WHEN line IS NOT NULL THEN 'P:' || line::text WHEN sale_line IS NOT NULL THEN 'S:' || sale_line::text END ORDER BY date DESC ) AS rn FROM {table} WHERE line IS NOT NULL OR sale_line IS NOT NULL ), current_prev AS ( SELECT block_key, MAX(CASE WHEN rn = 1 THEN amount END) AS amount_t, MAX(CASE WHEN rn = 2 THEN amount END) AS amount_t1 FROM ranked WHERE rn <= 2 GROUP BY block_key ) SELECT COALESCE(SUM(amount_t), 0) AS total_t, COALESCE(SUM(amount_t1), 0) AS total_t1, COALESCE(SUM(amount_t), 0) - COALESCE(SUM(amount_t1), 0) AS variation FROM current_prev """ cursor.execute(sql) total_t, total_t1, variation = cursor.fetchone() return total_t, total_t1, variation class ValuationLine(ValuationBase, ModelView): "Last Valuation" __name__ = 'valuation.valuation.line' 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_base_amount = fields.Numeric("Base Amount",digits='r_unit') r_rate = fields.Numeric("Rate",digits=(16,6)) 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.line') 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'), 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.base_amount).as_('r_base_amount'), Sum(val.rate).as_('r_rate'), Sum(val.mtm).as_('r_mtm'), Max(val.lot).as_('r_lot'), where=wh, group_by=[val.type,val.counterparty,val.state]) return query class ValuationReport(ValuationBase, ModelView): "Valuation Report" __name__ = 'valuation.report' @classmethod def table_query(cls): Valuation = Pool().get('valuation.valuation') val = Valuation.__table__() context = Transaction().context valuation_date = context.get('valuation_date') wh = (val.date == valuation_date) 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_('purchase'), val.sale.as_('sale'), val.sale_line.as_('sale_line'), val.line.as_('line'), val.date.as_('date'), val.type.as_('type'), val.reference.as_('reference'), val.counterparty.as_('counterparty'), val.product.as_('product'), val.state.as_('state'), val.price.as_('price'), val.currency.as_('currency'), val.quantity.as_('quantity'), val.unit.as_('unit'), val.amount.as_('amount'), val.base_amount.as_('base_amount'), val.rate.as_('rate'), val.mtm.as_('mtm'), val.lot.as_('lot'), where=wh) return query class ValuationReportContext(ModelView): "Valuation Report Context" __name__ = 'valuation.report.context' valuation_date = fields.Date("Valuation date") supplier = fields.Many2One('party.party',"Supplier") client = fields.Many2One('party.party',"Client") product = fields.Many2One('product.product',"Product") purchase = fields.Many2One('purchase.purchase', "Purchase") sale = fields.Many2One('sale.sale',"Sale") state = fields.Selection([ ('all', 'All'), ('open', 'Open'), ('fixed', 'Fixed'), ('hedged', 'Hedged') ], 'State') @classmethod def default_valuation_date(cls): pool = Pool() Date = pool.get('ir.date') return Date.today() @classmethod def default_state(cls): return 'all'