483 lines
17 KiB
Python
483 lines
17 KiB
Python
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
|
|
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)
|
|
|
|
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'
|
|
|
|
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'
|