Files
tradon/modules/purchase_trade/sale.py
2026-03-02 10:11:37 +01:00

1024 lines
42 KiB
Python
Executable File

# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If, PYSONEncoder
from trytond.model import (ModelSQL, ModelView)
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
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
import datetime
import logging
import json
from trytond.exceptions import UserWarning, UserError
from trytond.modules.purchase_trade.numbers_to_words import quantity_to_words, amount_to_currency_words, format_date_en
logger = logging.getLogger(__name__)
VALTYPE = [
('priced', 'Price'),
('fee', 'Fee'),
('market', 'Market'),
('derivative', 'Derivative'),
]
class ContractDocumentType(metaclass=PoolMeta):
"Contract - Document Type"
__name__ = 'contract.document.type'
# lc_out = fields.Many2One('lc.letter.outgoing', 'LC out')
# lc_in = fields.Many2One('lc.letter.incoming', 'LC in')
sale = fields.Many2One('sale.sale', "Sale")
class AnalyticDimensionAssignment(metaclass=PoolMeta):
'Analytic Dimension Assignment'
__name__ = 'analytic.dimension.assignment'
sale = fields.Many2One('sale.sale', "Sale")
class Estimated(metaclass=PoolMeta):
"Estimated date"
__name__ = 'pricing.estimated'
sale_line = fields.Many2One('sale.line',"Line")
class FeeLots(metaclass=PoolMeta):
"Fee lots"
__name__ = 'fee.lots'
sale_line = fields.Many2One('sale.line',"Line")
class Backtoback(metaclass=PoolMeta):
'Back To Back'
__name__ = 'back.to.back'
sale = fields.One2Many('sale.sale','btb', "Sale")
class OpenPosition(metaclass=PoolMeta):
"Open position"
__name__ = 'open.position'
sale = fields.Many2One('sale.sale',"Sale")
sale_line = fields.Many2One('sale.line',"Sale Line")
client = fields.Many2One('party.party',"Client")
class SaleStrategy(ModelSQL):
"Sale - Document Type"
__name__ = 'sale.strategy'
sale_line = fields.Many2One('sale.line', 'Sale Line')
strategy = fields.Many2One('mtm.strategy', "Strategy")
class Component(metaclass=PoolMeta):
"Component"
__name__ = 'pricing.component'
sale_line = fields.Many2One('sale.line',"Line")
quota_sale = fields.Function(fields.Numeric("Quota",digits='unit'),'get_quota_sale')
unit_sale = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_sale')
def getDelMonthDateSale(self):
PM = Pool().get('product.month')
if self.sale_line and hasattr(self.sale_line, 'del_period') and self.sale_line.del_period:
pm = PM(self.sale_line.del_period)
if pm:
logger.info("DELMONTHDATE:%s",pm.beg_date)
return pm.beg_date
def getEstimatedTriggerSale(self,t):
logger.info("GETTRIGGER:%s",t)
if t == 'delmonth':
return self.getDelMonthDateSale()
PE = Pool().get('pricing.estimated')
Date = Pool().get('ir.date')
pe = PE.search([('sale_line','=',self.sale_line),('trigger','=',t)])
if pe:
return pe[0].estimated_date
else:
return Date.today()
def get_unit_sale(self, name):
if self.sale_line:
return self.sale_line.unit
def get_quota_sale(self, name):
if self.sale_line:
if self.sale_line.quantity:
return round(self.sale_line.quantity_theorical / (self.nbdays if self.nbdays > 0 else 1),4)
class Pricing(metaclass=PoolMeta):
"Pricing"
__name__ = 'pricing.pricing'
sale_line = fields.Many2One('sale.line',"Lines")
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_sale')
def get_unit_sale(self,name):
if self.sale_line:
return self.sale_line.unit
def get_eod_price_sale(self):
if self.sale_line:
return round((self.fixed_qt * self.fixed_qt_price + self.unfixed_qt * self.unfixed_qt_price)/Decimal(self.sale_line.quantity),4)
return Decimal(0)
class Summary(ModelSQL,ModelView):
"Pricing summary"
__name__ = 'sale.pricing.summary'
sale_line = fields.Many2One('sale.line',"Lines")
price_component = fields.Many2One('pricing.component',"Component")
quantity = fields.Numeric("Qt",digits=(1,4))
fixed_qt = fields.Numeric("Fixed qt",digits=(1,4))
unfixed_qt = fields.Numeric("Unfixed qt",digits=(1,4))
price = fields.Numeric("Price",digits=(1,4))
progress = fields.Float("Fix. progress")
ratio = fields.Numeric("Ratio")
def get_name(self):
if self.price_component:
return self.price_component.get_rec_name()
return ""
def get_last_price(self):
Date = Pool().get('ir.date')
if self.price_component:
pc = Pool().get('pricing.component')(self.price_component)
if pc.price_index:
PI = Pool().get('price.price')
pi = PI(pc.price_index)
return pi.get_price(Date.today(),self.sale_line.unit,self.sale_line.sale.currency,True)
@classmethod
def table_query(cls):
SalePricing = Pool().get('pricing.pricing')
sp = SalePricing.__table__()
SaleComponent = Pool().get('pricing.component')
sc = SaleComponent.__table__()
#wh = Literal(True)
context = Transaction().context
group_pnl = context.get('group_pnl')
if group_pnl:
return None
query = sp.join(sc,'LEFT',condition=sp.price_component == sc.id).select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
Max(sp.id).as_('id'),
sp.sale_line.as_('sale_line'),
sp.price_component.as_('price_component'),
Max(sp.fixed_qt+sp.unfixed_qt).as_('quantity'),
Max(sp.fixed_qt).as_('fixed_qt'),
(Min(sp.unfixed_qt)).as_('unfixed_qt'),
Max(Case((sp.last, sp.eod_price),else_=0)).as_('price'),
(Max(sp.fixed_qt)/Max(sp.fixed_qt+sp.unfixed_qt)).as_('progress'),
Max(sc.ratio).as_('ratio'),
#where=wh,
group_by=[sp.sale_line,sp.price_component])
return query
class Lot(metaclass=PoolMeta):
__name__ = 'lot.lot'
sale_line = fields.Many2One('sale.line',"Sale",ondelete='CASCADE')
lot_quantity_sale = fields.Function(fields.Numeric("Net weight",digits='lot_unit'),'get_qt')
lot_gross_quantity_sale = fields.Function(fields.Numeric("Gross weight",digits='lot_unit'),'get_gross_qt')
def get_qt(self, name):
quantity = self.lot_quantity
if self.lot_hist:
for h in self.lot_hist:
if h.quantity_type.id == 3:
quantity = h.quantity
return quantity
def get_gross_qt(self, name):
quantity = self.lot_quantity
if self.lot_hist:
for h in self.lot_hist:
if h.quantity_type.id == 3:
quantity = h.quantity
return quantity
def getClient(self):
if self.sale_line:
return Pool().get('sale.sale')(self.sale_line.sale).party.id
def getSale(self):
if self.sale_line:
return self.sale_line.sale.id
class Sale(metaclass=PoolMeta):
__name__ = 'sale.sale'
btb = fields.Many2One('back.to.back',"Back to back")
from_location = fields.Many2One('stock.location', 'From location',domain=[('type', "!=", 'customer')])
to_location = fields.Many2One('stock.location', 'To location',domain=[('type', "!=", 'supplier')])
shipment_out = fields.Many2One('stock.shipment.out','Sales')
#pnl = fields.One2Many('valuation.valuation', 'sale', 'Pnl')
pnl = fields.One2Many('valuation.valuation.dyn', 'r_sale', 'Pnl',states={'invisible': ~Eval('group_pnl'),})
pnl_ = fields.One2Many('valuation.valuation.line', 'sale', 'Pnl',states={'invisible': Eval('group_pnl'),})
group_pnl = fields.Boolean("Group Pnl")
derivatives = fields.One2Many('derivative.derivative', 'sale', 'Derivative')
#plans = fields.One2Many('workflow.plan','sale',"Execution plans")
forex = fields.One2Many('forex.cover.physical.sale','contract',"Forex",readonly=True)
broker = fields.Many2One('party.party',"Broker",domain=[('categories.parent', 'child_of', [4])])
tol_min = fields.Numeric("Tol - in %")
tol_max = fields.Numeric("Tol + in %")
tol_min_qt = fields.Numeric("Tol -")
tol_max_qt = fields.Numeric("Tol +")
# certification = fields.Selection([
# (None, ''),
# ('bci', 'BCI'),
# ],"Certification")
# weight_basis = fields.Selection([
# (None, ''),
# ('ncsw', 'NCSW'),
# ('nlw', 'NLW'),
# ], 'Weight basis')
certif = fields.Many2One('purchase.certification',"Certification")
wb = fields.Many2One('purchase.weight.basis',"Weight basis")
association = fields.Many2One('purchase.association',"Association")
crop = fields.Many2One('purchase.crop',"Crop")
viewer = fields.Function(fields.Text(""),'get_viewer')
doc_template = fields.Many2One('doc.template',"Template")
required_documents = fields.Many2Many(
'contract.document.type', 'sale', 'doc_type', 'Required Documents')
analytic_dimensions = fields.One2Many(
'analytic.dimension.assignment',
'sale',
'Analytic Dimensions'
)
trader = fields.Many2One('party.party',"Trader")
operator = fields.Many2One('party.party',"Operator")
our_reference = fields.Char("Our Reference")
@property
def report_terms(self):
if self.lines:
return self.lines[0].note
else:
return ''
@property
def report_qt(self):
if self.lines:
return quantity_to_words(self.lines[0].quantity)
else:
return ''
@property
def report_price(self):
if self.lines:
return amount_to_currency_words(self.lines[0].unit_price)
else:
return ''
@property
def report_delivery(self):
del_date = 'PROMPT'
if self.lines:
if self.lines[0].estimated_date:
delivery_date = [dd.estimated_date for dd in self.lines[0].estimated_date if dd.trigger=='deldate']
if delivery_date:
del_date = delivery_date[0]
if del_date:
del_date = format_date_en(del_date)
return del_date
@property
def report_payment_date(self):
if self.lines:
Date = Pool().get('ir.date')
payment_date = self.lines[0].sale.payment_term.lines[0].get_date(Date.today(),self.lines[0])
if payment_date:
payment_date = format_date_en(payment_date)
return payment_date
@property
def report_shipment(self):
if self.lines:
if len(self.lines[0].lots)>1:
shipment = self.lines[0].lots[1].lot_shipment_in
lot = self.lines[0].lots[1].lot_name
if shipment:
info = 'B/L ' + shipment.bl_number
if shipment.container and shipment.container[0].container_no:
id = 1
for cont in shipment.container:
if id == 1:
info += ' Container(s)'
if cont.container_no:
info += ' ' + cont.container_no
else:
info += ' unnamed'
id += 1
info += ' (LOT ' + lot + ')'
if shipment.note:
info += ' ' + shipment.note
return info
else:
return ''
@classmethod
def default_viewer(cls):
country_start = "Zobiland"
data = {
"highlightedCountryName": country_start
}
return "d3:" + json.dumps(data)
@fields.depends('doc_template','required_documents')
def on_change_with_required_documents(self):
if self.doc_template:
return self.doc_template.type
def get_viewer(self, name=None):
country_start = ''
dep_name = ''
arr_name = ''
departure = ''
arrival = ''
if self.party and self.party.addresses:
if self.party.addresses[0].country:
country_start = self.party.addresses[0].country.name
if self.from_location:
lat_from = self.from_location.lat
lon_from = self.from_location.lon
dep_name = self.from_location.name
departure = { "name":dep_name,"lat": str(lat_from), "lon": str(lon_from) }
if self.to_location:
lat_to = self.to_location.lat
lon_to = self.to_location.lon
arr_name = self.to_location.name
arrival = { "name":arr_name,"lat": str(lat_to), "lon": str(lon_to) }
data = {
"highlightedCountryNames": [{"name":country_start}],
"routePoints": [
{ "lon": -46.3, "lat": -23.9 },
{ "lon": -30.0, "lat": -20.0 },
{ "lon": -30.0, "lat": 0.0 },
{ "lon": -6.0, "lat": 35.9 },
{ "lon": 15.0, "lat": 38.0 },
{ "lon": 29.0, "lat": 41.0 }
],
"boats": [
# {"name": "CARIBBEAN 1",
# "imo": "1234567",
# "lon": -30.0,
# "lat": 0.0,
# "status": "En route",
# "links": [
# { "text": "Voir sur VesselFinder", "url": "https://www.vesselfinder.com" },
# { "text": "Détails techniques", "url": "https://example.com/tech" }
# ],
# "actions": [
# { "type": "track", "id": "123", "label": "Suivre ce bateau" },
# { "type": "details", "id": "123", "label": "Voir détails" }
# ]}
],
"cottonStocks": [
# { "name":"Mali","lat": 12.65, "lon": -8.0, "amount": 300 },
# { "name":"Egypte","lat": 30.05, "lon": 31.25, "amount": 500 },
# { "name":"Irak","lat": 33.0, "lon": 44.0, "amount": 150 }
],
"departures": [departure],
"arrivals": [arrival]
}
return "d3:" + json.dumps(data)
@fields.depends('party','from_location','to_location')
def on_change_with_viewer(self):
return self.get_viewer()
def getLots(self):
if self.lines:
if self.lines.lots:
return [l for l in self.lines.lots]
@classmethod
def validate(cls, sales):
super(Sale, cls).validate(sales)
Line = Pool().get('sale.line')
Date = Pool().get('ir.date')
for sale in sales:
for line in sale.lines:
if not line.quantity_theorical and line.quantity > 0:
line.quantity_theorical = Decimal(str(line.quantity)).quantize(Decimal("0.00001"))
Line.save([line])
if line.lots:
line_p = line.get_matched_lines()#line.lots[0].line
if line_p:
for l in line_p:
#compute pnl
Pnl = Pool().get('valuation.valuation')
Pnl.generate(l.lot_p.line)
# if line.quantity_theorical:
# OpenPosition = Pool().get('open.position')
# OpenPosition.create_from_sale_line(line)
if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and
unit_price = line.get_basis_price()
if unit_price != line.unit_price:
Line = Pool().get('sale.line')
line.unit_price = unit_price
Line.save([line])
if line.price_type == 'efp':
if line.derivatives:
for d in line.derivatives:
line.unit_price = d.price_index.get_price(Date.today(),line.unit,line.currency,True)
Line.save([line])
class SaleLine(metaclass=PoolMeta):
__name__ = 'sale.line'
del_period = fields.Many2One('product.month',"Delivery Period")
lots = fields.One2Many('lot.lot','sale_line',"Lots",readonly=True)
fees = fields.One2Many('fee.fee', 'sale_line', 'Fees')
quantity_theorical = fields.Numeric("Th. quantity", digits='unit', readonly=True)
premium = fields.Numeric("Premium/Discount",digits='unit')
price_type = fields.Selection([
('cash', 'Cash Price'),
('priced', 'Priced'),
('basis', 'Basis'),
('efp', 'EFP'),
], 'Price type')
progress = fields.Function(fields.Float("Fix. progress",
states={
'invisible': Eval('price_type') != 'basis',
}),'get_progress')
from_del = fields.Date("From")
to_del = fields.Date("To")
period_at = fields.Selection([
(None, ''),
('laycan', 'Laycan'),
('loading', 'Loading'),
('discharge', 'Discharge'),
('crossing_border', 'Crossing Border'),
('title_transfer', 'Title transfer'),
('arrival', 'Arrival'),
],"Period at")
concentration = fields.Numeric("Concentration")
price_components = fields.One2Many('pricing.component','sale_line',"Components")
mtm = fields.Many2Many('sale.strategy', 'sale_line', 'strategy', 'Mtm Strategy')
derivatives = fields.One2Many('derivative.derivative','sale_line',"Derivatives")
price_pricing = fields.One2Many('pricing.pricing','sale_line',"Pricing")
price_summary = fields.One2Many('sale.pricing.summary','sale_line',"Summary")
estimated_date = fields.One2Many('pricing.estimated','sale_line',"Estimated date")
tol_min = fields.Numeric("Tol - in %",states={
'readonly': (Eval('inherit_tol')),
})
tol_max = fields.Numeric("Tol + in %",states={
'readonly': (Eval('inherit_tol')),
})
tol_min_qt = fields.Numeric("Tol -",states={
'readonly': (Eval('inherit_tol')),
})
tol_max_qt = fields.Numeric("Tol +",states={
'readonly': (Eval('inherit_tol')),
})
inherit_tol = fields.Boolean("Inherit tolerance")
tol_min_v = fields.Function(fields.Numeric("Qt min"),'get_tol_min')
tol_max_v = fields.Function(fields.Numeric("Qt max"),'get_tol_max')
certif = fields.Many2One('purchase.certification',"Certification",states={'readonly': (Eval('inherit_cer')),})
# certification = fields.Selection([
# (None, ''),
# ('bci', 'BCI'),
# ],"Certification",states={'readonly': (Eval('inherit_cer')),})
inherit_cer = fields.Boolean("Inherit certification")
enable_linked_currency = fields.Boolean("Linked currencies")
linked_price = fields.Numeric("Price", digits='unit',states={
'invisible': (~Eval('enable_linked_currency')),
})
linked_currency = fields.Many2One('currency.linked',"Currency",states={
'invisible': (~Eval('enable_linked_currency')),
})
linked_unit = fields.Many2One('product.uom', 'Unit',states={
'invisible': (~Eval('enable_linked_currency')),
})
premium = fields.Numeric("Premium/Discount",digits='unit')
fee_ = fields.Many2One('fee.fee',"Fee")
@classmethod
def default_price_type(cls):
return 'priced'
@classmethod
def default_inherit_tol(cls):
return True
@classmethod
def default_enable_linked_currency(cls):
return False
@classmethod
def default_inherit_cer(cls):
return True
def get_matched_lines(self):
if self.lots:
LotQt = Pool().get('lot.qt')
return LotQt.search([('lot_s','=',self.lots[0].id),('lot_p','>',0)])
def get_date(self,trigger_event):
trigger_date = None
if self.estimated_date:
logger.info("ESTIMATED_DATE:%s",self.estimated_date)
trigger_date = [d.estimated_date for d in self.estimated_date if d.trigger == trigger_event]
logger.info("TRIGGER_DATE:%s",trigger_date)
logger.info("TRIGGER_EVENT:%s",trigger_event)
trigger_date = trigger_date[0] if trigger_date else None
return trigger_date
def get_tol_min(self,name):
if self.inherit_tol:
if self.sale.tol_min and self.quantity_theorical:
return round((1-(self.sale.tol_min/100))*Decimal(self.quantity_theorical),3)
else:
if self.tol_min and self.quantity_theorical:
return round((1-(self.tol_min/100))*Decimal(self.quantity_theorical),3)
def get_tol_max(self,name):
if self.inherit_tol:
if self.sale.tol_max and self.quantity_theorical:
return round((1+(self.sale.tol_max/100))*Decimal(self.quantity_theorical),3)
else:
if self.tol_max and self.quantity_theorical:
return round((1+(self.tol_max/100))*Decimal(self.quantity_theorical),3)
def get_progress(self,name):
PS = Pool().get('sale.pricing.summary')
ps = PS.search(['sale_line','=',self.id])
if ps:
return sum((e.progress if e.progress else 0) * (e.ratio if e.ratio else 0) / 100 for e in ps)
def getVirtualLot(self):
if self.lots:
return [l for l in self.lots if l.lot_type=='virtual'][0]
def get_price(self,lot_premium=0):
return (self.unit_price + Decimal(lot_premium) + (self.premium if self.premium else Decimal(0))) if self.unit_price else Decimal(0)
def get_basis_price(self):
price = Decimal(0)
for pc in self.price_components:
PP = Pool().get('sale.pricing.summary')
pp = PP.search([('price_component','=',pc.id),('sale_line','=',self.id)])
if pp:
price += pp[0].price * (pc.ratio / 100)
return round(price,4)
def get_price_linked_currency(self,lot_premium=0):
if self.linked_unit:
Uom = Pool().get('product.uom')
qt = Uom.compute_qty(self.unit, float(1), self.linked_unit)
return round(((self.linked_price + Decimal(lot_premium) + (self.premium if self.premium else Decimal(0))) * self.linked_currency.factor) * Decimal(qt), 4)
else:
return round((self.linked_price + Decimal(lot_premium) + (self.premium if self.premium else Decimal(0))) * self.linked_currency.factor, 4)
@fields.depends('id','unit','quantity','unit_price','price_pricing','price_type','price_components','premium','estimated_date','lots','fees','enable_linked_currency','linked_price','linked_currency','linked_unit')
def on_change_with_unit_price(self, name=None):
Date = Pool().get('ir.date')
logger.info("ONCHANGEUNITPRICE:%s",self.unit_price)
if self.price_type == 'basis' and self.lots: #self.price_pricing and self.price_components and
logger.info("ONCHANGEUNITPRICE_IN:%s",self.get_basis_price())
return self.get_basis_price()
if self.enable_linked_currency and self.linked_price and self.linked_currency and self.price_type == 'priced':
return self.get_price_linked_currency()
if self.price_type == 'efp':
if hasattr(self, 'derivatives') and self.derivatives:
for d in self.derivatives:
return d.price_index.get_price(Date.today(),self.unit,self.sale.currency,True)
return self.get_price()
def check_from_to(self,tr):
if tr.pricing_period:
date_from,date_to, d, include, dates = tr.getDateWithEstTrigger(1)
if date_from:
tr.from_p = date_from.date()
if date_to:
tr.to_p = date_to.date()
if tr.application_period:
date_from,date_to, d, include, dates = tr.getDateWithEstTrigger(2)
if date_from:
tr.from_a = date_from.date()
if date_to:
tr.to_a = date_to.date()
TR = Pool().get('pricing.trigger')
TR.save([tr])
def check_pricing(self):
if self.price_components:
for pc in self.price_components:
if not pc.auto:
Pricing = Pool().get('pricing.pricing')
pricings = Pricing.search(['price_component','=',pc.id],order=[('pricing_date', 'ASC')])
if pricings:
cumul_qt = Decimal(0)
index = 0
for pr in pricings:
cumul_qt += pr.quantity
pr.fixed_qt = cumul_qt
pr.fixed_qt_price = pr.get_fixed_price()
pr.unfixed_qt = Decimal(pr.sale_line.quantity_theorical) - pr.fixed_qt
pr.unfixed_qt_price = pr.fixed_qt_price
pr.eod_price = pr.get_eod_price_sale()
if index == len(pricings) - 1:
pr.last = True
index += 1
Pricing.save([pr])
if pc.triggers and pc.auto:
prDate = []
prPrice = []
apDate = []
apPrice = []
for t in pc.triggers:
prD, prP = t.getPricingListDates(pc.calendar)
apD, apP = t.getApplicationListDates(pc.calendar)
prDate.extend(prD)
prPrice.extend(prP)
apDate.extend(apD)
apPrice.extend(apP)
if pc.quota_sale:
prPrice = self.get_avg(prPrice)
self.generate_pricing(pc,apDate,prPrice)
def get_avg(self,lprice):
l = len(lprice)
if l > 0 :
cumulprice = float(0)
i = 1
for p in lprice:
if i > 1:
p['avg_minus_1'] = cumulprice / (i-1)
cumulprice += p['price']
p['avg'] = cumulprice / i
i += 1
return lprice
def getnearprice(self,pl,d,t,max_date=None):
if pl:
pl_sorted = sorted(pl, key=lambda x: x['date'])
pminus = pl_sorted[0]
if not max_date:
max_date = d.date()
for p in pl_sorted:
if p['date'].date() == d.date():
if p['isAvg'] and t == 'avg':
return p[t]
if not p['isAvg'] and t == 'avg':
return p['price']
elif p['date'].date() > d.date():
if pminus != p:
return pminus[t]
else:
return Decimal(0)
pminus = p
return pl_sorted[len(pl)-1][t]
return Decimal(0)
def generate_pricing(self,pc,dl,pl):
Pricing = Pool().get('pricing.pricing')
pricing = Pricing.search(['price_component','=',pc.id])
if pricing:
Pricing.delete(pricing)
cumul_qt = 0
index = 0
dl_sorted = sorted(dl)
for d in dl_sorted:
if pc.pricing_date and d.date() > pc.pricing_date:
break
p = Pricing()
p.sale_line = self.id
logger.info("GENEDATE:%s",d)
logger.info("TYPEDATE:%s",type(d))
p.pricing_date = d.date()
p.price_component = pc.id
p.quantity = round(Decimal(pc.quota_sale),4)
price = round(Decimal(self.getnearprice(pl,d,'price',pc.pricing_date)),4)
p.settl_price = price
if price > 0:
cumul_qt += pc.quota_sale
p.fixed_qt = round(Decimal(cumul_qt),4)
p.fixed_qt_price = round(Decimal(self.getnearprice(pl,d,'avg',pc.pricing_date)),4)
#p.fixed_qt_price = p.get_fixed_price()
if p.fixed_qt_price == 0:
p.fixed_qt_price = round(Decimal(self.getnearprice(pl,d,'avg_minus_1',pc.pricing_date)),4)
p.unfixed_qt = round(Decimal(self.quantity_theorical) - Decimal(cumul_qt),4)
if p.unfixed_qt < 0.001:
p.unfixed_qt = Decimal(0)
p.fixed_qt = Decimal(self.quantity_theorical)
if price > 0:
logger.info("GENERATE_1:%s",price)
p.unfixed_qt_price = price
else:
pr = Decimal(pc.price_index.get_price(p.pricing_date,self.unit,self.sale.currency,True))
pr = round(pr,4)
logger.info("GENERATE_2:%s",pr)
p.unfixed_qt_price = pr
p.eod_price = p.get_eod_price_sale()
if (index == len(dl)-1) or (pc.pricing_date and (index < len(dl)-1 and dl_sorted[index+1].date() > pc.pricing_date)):
p.last = True
logger.info("GENERATE_3:%s",p.unfixed_qt_price)
Pricing.save([p])
index += 1
# @classmethod
# def write(cls, records, values):
# if 'quantity' in values:
# for record in records:
# old_qt = record.quantity
# new_qt = values['quantity']
# logger.info("WRITE_OLD_QT:%s",old_qt)
# logger.info("WRITE_NEW_QT:%s",new_qt)
# if old_qt != new_qt:
# LotQt = Pool().get('lot.qt')
# lqts = LotQt.search(['lot_s','=',record.lots[0]])
# if len(lqts)>1:
# raise UserError("You cannot changed quantity with open quantities defined !")
# return
# elif len(lqts)==1:
# if lqts[0].lot_p or lqts[0].lot_shipment_origin:
# raise UserError("You cannot changed quantity with open quantities defined !")
# return
# lqts[0].lot_quantity = new_qt
# LotQt.save(lqts)
# super().write(records, values)
@classmethod
def delete(cls, lines):
pool = Pool()
LotQt = pool.get('lot.qt')
Valuation = pool.get('valuation.valuation')
OpenPosition = pool.get('open.position')
for line in lines:
if line.lots:
vlot_s = line.lots[0].getVlot_s()
lqts = LotQt.search([('lot_s','=',vlot_s.id),('lot_p','!=',None),('lot_quantity','>',0)])
if lqts:
raise UserError("You cannot delete matched sale")
return
lqts = LotQt.search([('lot_s','=',vlot_s.id)])
LotQt.delete(lqts)
valuations = Valuation.search([('lot','in',line.lots)])
if valuations:
Valuation.delete(valuations)
# op = OpenPosition.search(['sale_line','=',line.id])
# if op:
# OpenPosition.delete(op)
super(SaleLine, cls).delete(lines)
@classmethod
def copy(cls, lines, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('lots', None)
default.setdefault('quantity', Decimal(0))
default.setdefault('quantity_theorical', None)
default.setdefault('price_pricing', None)
return super().copy(lines, default=default)
@classmethod
def validate(cls, salelines):
LotQtHist = Pool().get('lot.qt.hist')
LotQtType = Pool().get('lot.qt.type')
super(SaleLine, cls).validate(salelines)
for line in salelines:
if line.price_components:
for pc in line.price_components:
if pc.triggers:
for tr in pc.triggers:
line.check_from_to(tr)
line.check_pricing()
#no lot need to create one with line quantity
logger.info("FROM_VALIDATE_LINE:%s",line.created_by_code)
if not line.created_by_code:
if not line.lots and line.product.type != 'service' and line.quantity != Decimal(0):
Lot = Pool().get('lot.lot')
lot = Lot()
lot.sale_line = line.id
lot.lot_qt = line.quantity
lot.lot_unit_line = line.unit
lot.lot_quantity = Decimal(str(line.quantity)).quantize(Decimal("0.00001"))#round(line.quantity,5)
lot.lot_status = 'forecast'
lot.lot_type = 'virtual'
lot.lot_product = line.product
lqtt = LotQtType.search([('sequence','=',1)])
if lqtt:
lqh = LotQtHist()
lqh.quantity_type = lqtt[0]
lqh.quantity = lot.lot_quantity
lqh.gross_quantity = lot.lot_quantity
lot.lot_hist = [lqh]
if line.quantity > 0:
Lot.save([lot])
#check if fees need to be updated
if line.fees:
for fee in line.fees:
fl_check = FeeLots.search([('fee','=',fee.id),('lot','=',lot.id),('sale_line','=',line.id)])
if not fl_check:
fl = FeeLots()
fl.fee = fee.id
fl.lot = lot.id
fl.sale_line = line.id
FeeLots.save([fl])
#generate valuation for purchase and sale
LotQt = Pool().get('lot.qt')
if line.lots:
for lot in line.lots:
lqts = LotQt.search([('lot_s','=',lot.id),('lot_p','>',0)])
logger.info("VALIDATE_SL:%s",lqts)
if lqts:
purchase_lines = [e.lot_p.line for e in lqts]
if purchase_lines:
for pl in purchase_lines:
Pnl = Pool().get('valuation.valuation')
Pnl.generate(pl)
class SaleCreatePurchase(Wizard):
"Create mirror purchase"
__name__ = "sale.create.mirror"
start = StateTransition()
purchase = StateView(
'sale.create.input',
'purchase_trade.create_purchase_view_form', [
Button("Cancel", 'end', 'tryton-cancel'),
Button("Create", 'creating', 'tryton-ok', default=True),
])
creating = StateTransition()
def transition_start(self):
return 'purchase'
def transition_creating(self):
Purchase = Pool().get('purchase.purchase')
PL = Pool().get('purchase.line')
LotQt = Pool().get('lot.qt')
p = None
pl = None
for r in self.records:
if r.lines:
p = Purchase()
p.party = self.purchase.party
p.incoterm = self.purchase.incoterm
p.payment_term = self.purchase.payment_term
p.from_location = self.purchase.from_location
p.to_location = self.purchase.to_location
Purchase.save([p])
pl = PL()
pl.quantity = r.lines[0].quantity
pl.unit = r.lines[0].unit
pl.product = r.lines[0].product
pl.unit_price = self.purchase.unit_price
pl.currency = self.purchase.currency
pl.purchase = p.id
PL.save([pl])
#Match if requested
if self.purchase.match:
#Increase forecasted virtual part matched
if pl:
if pl.lots:
qt = Decimal(pl.quantity)
vlot_p = pl.getVirtualLot()
vlot_s = self.records[0].lines[0].getVirtualLot()
if not vlot_p.updateVirtualPart(None,qt,vlot_p,None,vlot_s):
vlot_p.createVirtualPart(qt,pl.unit,vlot_p,None,vlot_s)
#Decrease forecasted virtual part non matched
lqts = LotQt.search([('lot_p','=',vlot_p)])
if lqts:
vlot_p.updateVirtualPart(lqts[0],-qt)
lqts = LotQt.search([('lot_s','=',vlot_s)])
if lqts:
vlot_p.updateVirtualPart(lqts[0],-qt)
return 'end'
def end(self):
return 'reload'
class SaleCreatePurchaseInput(ModelView):
"Create purchase mirror"
__name__ = "sale.create.input"
party = fields.Many2One('party.party',"Supplier")
incoterm = fields.Many2One('incoterm.incoterm',"Incoterm",domain=[('location', '=', False)])
payment_term = fields.Many2One('account.invoice.payment_term', "Payment Term")
from_location = fields.Many2One('stock.location',"From location")
to_location = fields.Many2One('stock.location',"To location")
unit_price = fields.Numeric("Price")
currency = fields.Many2One('currency.currency',"Currency")
match = fields.Boolean("Match open quantity")
class Derivative(metaclass=PoolMeta):
"Derivative"
__name__ = 'derivative.derivative'
sale = fields.Many2One('sale.sale',"Sale")
sale_line = fields.Many2One('sale.line',"Line")
class Valuation(metaclass=PoolMeta):
"Valuation"
__name__ = 'valuation.valuation'
sale = fields.Many2One('sale.sale',"Sale")
sale_line = fields.Many2One('sale.line',"Line")
class ValuationLine(metaclass=PoolMeta):
"Last Valuation"
__name__ = 'valuation.valuation.line'
sale = fields.Many2One('sale.sale',"Sale")
sale_line = fields.Many2One('sale.line',"Line")
class ValuationReport(metaclass=PoolMeta):
"Valuation Report"
__name__ = 'valuation.report'
sale = fields.Many2One('sale.sale',"Sale")
sale_line = fields.Many2One('sale.line',"Line")
class ValuationDyn(metaclass=PoolMeta):
"Valuation"
__name__ = 'valuation.valuation.dyn'
r_sale = fields.Many2One('sale.sale',"Sale")
r_sale_line = fields.Many2One('sale.line',"Line")
@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'),
Max(val.id).as_('id'),
Max(val.purchase).as_('r_purchase'),
Max(val.sale).as_('r_sale'),
Max(val.line).as_('r_line'),
Max(val.date).as_('r_date'),
Literal(None).as_('r_type'),
Max(val.reference).as_('r_reference'),
Literal(None).as_('r_counterparty'),
Max(val.product).as_('r_product'),
Literal(None).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.strategy).as_('r_strategy'),
Max(val.lot).as_('r_lot'),
Max(val.sale_line).as_('r_sale_line'),
where=wh,
group_by=[val.purchase,val.sale])
return query
class Fee(metaclass=PoolMeta):
"Fee"
__name__ = 'fee.fee'
sale_line = fields.Many2One('sale.line',"Line")
class SaleAllocationsWizard(Wizard):
'Open Allocations report from Sale without modal'
__name__ = 'sale.allocations.wizard'
start_state = 'open_report'
open_report = StateAction('purchase_trade.act_lot_report_form')
def do_open_report(self, action):
sale_id = Transaction().context.get('active_id')
if not sale_id:
raise ValueError("No active sale ID in context")
action['context_model'] = 'lot.context'
action['pyson_context'] = PYSONEncoder().encode({
'sale': sale_id,
})
return action, {}