1024 lines
42 KiB
Python
Executable File
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, {}
|