Files
tradon/modules/purchase_trade/purchase.py
2026-03-09 15:17:34 +01:00

1145 lines
46 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 functools import wraps
from trytond.model import ModelSingleton, ModelSQL, ModelView, fields
from trytond.i18n import gettext
from trytond.report import Report
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Bool, Eval, Id, If, PYSONEncoder
from trytond.model import (ModelSQL, ModelView)
from trytond.tools import (cursor_dict, 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, Abs
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
from itertools import chain, groupby
from operator import itemgetter
import datetime
import logging
import json
import jwt
from collections import defaultdict
from trytond.exceptions import UserWarning, UserError
logger = logging.getLogger(__name__)
TRIGGERS = [
('bldate', 'BL date'),
('invdate', 'Invoice date'),
('ctdate', 'Ct. date'),
('prdate', 'Pur. date'),
('cod', 'COD date'),
('border', 'Border crossing date'),
('pump', 'Pump date'),
('discharge', 'Discharge NOR'),
('arrival', 'Arrival date'),
('delmonth', 'Delivery month'),
('deldate', 'Delivery date'),
]
class DocType(ModelSQL,ModelView):
"Document Type"
__name__ = 'document.type'
name = fields.Char('Name')
class ContractDocumentType(ModelSQL):
"Contract - Document Type"
__name__ = 'contract.document.type'
doc_type = fields.Many2One('document.type', 'Document Type')
purchase = fields.Many2One('purchase.purchase', "Purchase")
class DocTemplate(ModelSQL,ModelView):
"Documents Template"
__name__ = 'doc.template'
name = fields.Char('Name')
type = fields.Many2Many('doc.type.template','template','type',"Document Type")
class DocTypeTemplate(ModelSQL):
"Template - Document Type"
__name__ = 'doc.type.template'
template = fields.Many2One('doc.template')
type = fields.Many2One('document.type')
class PurchaseStrategy(ModelSQL):
"Purchase - Document Type"
__name__ = 'purchase.strategy'
line = fields.Many2One('purchase.line', 'Purchase Line')
strategy = fields.Many2One('mtm.strategy', "Strategy")
class Estimated(metaclass=PoolMeta):
"Estimated date"
__name__ = 'pricing.estimated'
shipment_in = fields.Many2One('stock.shipment.in')
shipment_out = fields.Many2One('stock.shipment.out')
shipment_internal = fields.Many2One('stock.shipment.internal')
purchase = fields.Many2One('purchase.purchase',"Purchase")
line = fields.Many2One('purchase.line',"Line")
class Currency(metaclass=PoolMeta):
"Currency"
__name__ = 'currency.currency'
concatenate = fields.Boolean("Concatenate")
@classmethod
def default_concatenate(cls):
return False
class Unit(metaclass=PoolMeta):
"Unit"
__name__ = 'product.uom'
concatenate = fields.Boolean("Concatenate")
@classmethod
def default_concatenate(cls):
return False
class FeeLots(metaclass=PoolMeta):
"Fee lots"
__name__ = 'fee.lots'
line = fields.Many2One('purchase.line',"Line")
class Component(metaclass=PoolMeta):
"Component"
__name__ = 'pricing.component'
line = fields.Many2One('purchase.line',"Line")
quota = fields.Function(fields.Numeric("Quota",digits='unit'),'get_quota_purchase')
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_purchase')
def getDelMonthDatePurchase(self):
PM = Pool().get('product.month')
if self.line.del_period:
pm = PM(self.line.del_period)
if pm:
logger.info("DELMONTHDATE:%s",pm.beg_date)
return pm.beg_date
def getEstimatedTriggerPurchase(self,t):
logger.info("GETTRIGGER:%s",t)
if t == 'delmonth':
return self.getDelMonthDatePurchase()
PE = Pool().get('pricing.estimated')
Date = Pool().get('ir.date')
pe = PE.search([('line','=',self.line),('trigger','=',t)])
if pe:
return pe[0].estimated_date
else:
return Date.today()
def get_unit_purchase(self, name):
if self.line:
return self.line.unit
def get_quota_purchase(self, name):
if self.line:
if self.line.quantity:
return round(self.line.quantity_theorical / (self.nbdays if self.nbdays > 0 else 1),5)
class Pricing(metaclass=PoolMeta):
"Pricing"
__name__ = 'pricing.pricing'
line = fields.Many2One('purchase.line',"Lines")
unit = fields.Function(fields.Many2One('product.uom',"Unit"),'get_unit_purchase')
def get_unit_purchase(self,name):
if self.line:
return self.line.unit
def get_eod_price_purchase(self):
if self.line:
return round((self.fixed_qt * self.fixed_qt_price + self.unfixed_qt * self.unfixed_qt_price)/Decimal(self.line.quantity_theorical),4)
return Decimal(0)
class Summary(ModelSQL,ModelView):
"Pricing summary"
__name__ = 'purchase.pricing.summary'
line = fields.Many2One('purchase.line',"Lines")
price_component = fields.Many2One('pricing.component',"Component")
quantity = fields.Numeric("Qt",digits=(1,5))
fixed_qt = fields.Numeric("Fixed qt",digits=(1,5))
unfixed_qt = fields.Numeric("Unfixed qt",digits=(1,5))
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)
logger.info("PURCHASECURRENCY:%s",self.line.purchase.currency)
return pi.get_price(Date.today(),self.line.unit,self.line.purchase.currency,True)
@classmethod
def table_query(cls):
PurchasePricing = Pool().get('pricing.pricing')
pp = PurchasePricing.__table__()
PurchaseComponent = Pool().get('pricing.component')
pc = PurchaseComponent.__table__()
#wh = Literal(True)
context = Transaction().context
group_pnl = context.get('group_pnl')
if group_pnl:
return None
query = pp.join(pc,'LEFT',condition=pp.price_component == pc.id).select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(None).as_('write_uid'),
Literal(None).as_('write_date'),
Literal(None).as_('sale_line'),
Max(pp.id).as_('id'),
pp.line.as_('line'),
pp.price_component.as_('price_component'),
Max(pp.fixed_qt+pp.unfixed_qt).as_('quantity'),
Max(pp.fixed_qt).as_('fixed_qt'),
(Min(pp.unfixed_qt)).as_('unfixed_qt'),
Max(Case((pp.last, pp.eod_price),else_=0)).as_('price'),
(Max(pp.fixed_qt)/Max(pp.fixed_qt+pp.unfixed_qt)).as_('progress'),
Max(pc.ratio).as_('ratio'),
#where=wh,
group_by=[pp.line,pp.price_component])
return query
class StockLocation(metaclass=PoolMeta):
__name__ = 'stock.location'
lat = fields.Numeric("Latitude")
lon = fields.Numeric("Longitude")
class PurchaseCertification(ModelSQL,ModelView):
"Certification"
__name__ = 'purchase.certification'
name = fields.Char("Name")
class PurchaseCertificationWeightBasis(ModelSQL,ModelView):
"Weight basis"
__name__ = 'purchase.weight.basis'
name = fields.Char("Name")
qt_type = fields.Many2One('lot.qt.type',"Associated type to final invoice")
description = fields.Char("Description")
class PurchaseAssociation(ModelSQL,ModelView):
"Association"
__name__ = 'purchase.association'
name = fields.Char("Name")
party = fields.Many2One('party.party',"Party")
description = fields.Char("Description")
class PurchaseCrop(ModelSQL,ModelView):
"Crop"
__name__ = 'purchase.crop'
name = fields.Char("Name")
class Purchase(metaclass=PoolMeta):
__name__ = 'purchase.purchase'
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_in = fields.Many2One('stock.shipment.in','Purchases')
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 +")
certif = fields.Many2One('purchase.certification',"Certification",states={'invisible': Eval('company_visible'),})
wb = fields.Many2One('purchase.weight.basis',"Weight basis")
association = fields.Many2One('purchase.association',"Association",states={'invisible': Eval('company_visible'),})
crop = fields.Many2One('purchase.crop',"Crop",states={'invisible': Eval('company_visible'),})
pnl = fields.One2Many('valuation.valuation.dyn', 'r_purchase', 'Pnl',states={'invisible': ~Eval('group_pnl'),})
pnl_ = fields.One2Many('valuation.valuation.line', 'purchase', 'Pnl',states={'invisible': Eval('group_pnl'),})
derivatives = fields.One2Many('derivative.derivative', 'purchase', 'Derivative')
plans = fields.One2Many('workflow.plan','purchase',"Execution plans")
forex = fields.One2Many('forex.cover.physical.contract','contract',"Forex",readonly=True)
plan = fields.Many2One('workflow.plan',"Name")
estimated_date = fields.One2Many('pricing.estimated','purchase',"Estimated date")
group_pnl = fields.Boolean("Group Pnl")
viewer = fields.Function(fields.Text(""),'get_viewer')
doc_template = fields.Many2One('doc.template',"Template")
required_documents = fields.Many2Many(
'contract.document.type', 'purchase', 'doc_type', 'Required Documents')
analytic_dimensions = fields.One2Many(
'analytic.dimension.assignment',
'purchase',
'Analytic Dimensions'
)
trader = fields.Many2One('party.party',"Trader")
operator = fields.Many2One('party.party',"Operator")
our_reference = fields.Char("Our Reference")
company_visible = fields.Function(fields.Boolean("Visible"),'get_company_info')
lc_date = fields.Date("LC date")
def get_company_info(self,name):
return (self.company.party.name == 'MELYA')
@classmethod
def default_viewer(cls):
country_start = "Zobiland"
data = {
"highlightedCountryName": country_start
}
return "d3:" + json.dumps(data)
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)
def getLots(self):
if self.lines:
if self.lines.lots:
return [l for l in self.lines.lots]
@fields.depends('party','from_location','to_location')
def on_change_with_viewer(self):
return self.get_viewer()
@fields.depends('doc_template','required_documents')
def on_change_with_required_documents(self):
if self.doc_template:
return self.doc_template.type
@classmethod
def copy(cls, purchases, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.setdefault('pnl', None)
default.setdefault('derivatives', None)
default.setdefault('pnl_', None)
default.setdefault('plans', None)
return super().copy(purchases, default=default)
@classmethod
def validate(cls, purchases):
super(Purchase, cls).validate(purchases)
Line = Pool().get('purchase.line')
Date = Pool().get('ir.date')
for purchase in purchases:
for line in purchase.lines:
if not line.quantity_theorical and line.quantity > 0:
line.quantity_theorical = (
Decimal(str(line.quantity))
.quantize(Decimal("0.00001"))
)
Line.save([line])
#compute pnl
Pnl = Pool().get('valuation.valuation')
Pnl.generate(line)
if line.quantity_theorical:
OpenPosition = Pool().get('open.position')
# OpenPosition.create_from_purchase_line(line)
#line unit_price calculation
if line.price_type == 'basis' and line.lots: #line.price_pricing and line.price_components and
unit_price = line.get_basis_price()
logger.info("VALIDATEPURCHASE:%s",unit_price)
if unit_price != line.unit_price:
line.unit_price = unit_price
logger.info("VALIDATEPURCHASE2:%s",line.unit_price)
Line.save([line])
if line.price_type == 'efp':
if line.derivatives:
for d in line.derivatives:
line.unit_price = round(Decimal(d.price_index.get_price(Date.today(),line.unit,line.currency,True)),4)
logger.info("EFP_PRICE:%s",line.unit_price)
Line.save([line])
class Line(metaclass=PoolMeta):
__name__ = 'purchase.line'
quantity_theorical = fields.Numeric("Th. quantity", digits='unit', readonly=True)
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')
del_period = fields.Many2One('product.month',"Delivery Period")
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','line',"Components")
price_pricing = fields.One2Many('pricing.pricing','line',"Pricing")
price_summary = fields.One2Many('purchase.pricing.summary','line',"Summary")
estimated_date = fields.One2Many('pricing.estimated','line',"Estimated date")
optional = fields.One2Many('optional.scenario','line',"Optionals Scenarios")
lots = fields.One2Many('lot.lot','line',"Lots",readonly=True)
purchase_line = fields.Many2One('purchase.line',"Lines")
fees = fields.One2Many('fee.fee', 'line', 'Fees')#, filter=[('product.type', '=', 'service')])
derivatives = fields.One2Many('derivative.derivative','line',"Derivatives")
mtm = fields.Many2Many('purchase.strategy', 'line', 'strategy', 'Mtm Strategy')
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')
# certification = fields.Selection([
# (None, ''),
# ('bci', 'BCI'),
# ],"Certification",states={'readonly': (Eval('inherit_cer')),})
certif = fields.Many2One('purchase.certification',"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")
attributes = fields.Dict(
'product.attribute', 'Attributes',
domain=[
('sets', '=', Eval('attribute_set')),
],
states={
'readonly': ~Eval('attribute_set'),
},
depends=['product', 'attribute_set'],
help="Add attributes to the variant."
)
attribute_set = fields.Function(
fields.Many2One('product.attribute.set', "Attribute Set"),
'on_change_with_attribute_set'
)
attributes_name = fields.Function(
fields.Char("Attributes Name"),
'on_change_with_attributes_name'
)
@fields.depends('product')
def on_change_with_attribute_set(self, name=None):
if self.product and self.product.template and self.product.template.attribute_set:
return self.product.template.attribute_set.id
@fields.depends('product', 'attributes')
def on_change_with_attributes_name(self, name=None):
if not self.product or not self.product.attribute_set or not self.attributes:
return
def key(attribute):
return getattr(attribute, 'sequence', attribute.name)
values = []
for attribute in sorted(self.product.attribute_set.attributes, key=key):
if attribute.name in self.attributes:
value = self.attributes[attribute.name]
values.append(gettext(
'product_attribute.msg_label_value',
label=attribute.string,
value=attribute.format(value)
))
return " | ".join(filter(None, values))
@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_p','=',self.lots[0].id),('lot_s','>',0)])
def get_date(self,trigger_event):
trigger_date = None
if self.estimated_date:
trigger_date = [d.estimated_date for d in self.estimated_date if d.trigger == 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.purchase.tol_min and self.quantity_theorical:
return round((1-(self.purchase.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.purchase.tol_max and self.quantity_theorical:
return round((1+(self.purchase.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('purchase.pricing.summary')
ps = PS.search(['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_basis_price(self):
price = Decimal(0)
for pc in self.price_components:
PP = Pool().get('purchase.pricing.summary')
pp = PP.search([('price_component','=',pc.id),('line','=',self.id)])
if pp:
price += pp[0].price * (pc.ratio / 100)
return round(price,4)
def get_price(self,lot_premium=0):
return (self.unit_price + Decimal(lot_premium)) if self.unit_price else Decimal(0) + (self.premium if self.premium else Decimal(0))
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 round(d.price_index.get_price(Date.today(),self.unit,self.purchase.currency,True),4)
return self.unit_price
@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 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_p = line.lots[0].getVlot_p()
lqts = LotQt.search([('lot_p','=',vlot_p.id),('lot_s','!=',None),('lot_quantity','>',0)])
if lqts:
raise UserError("You cannot delete matched sale")
return
lqts = LotQt.search([('lot_p','=',vlot_p.id)])
LotQt.delete(lqts)
valuations = Valuation.search([('lot','in',line.lots)])
if valuations:
Valuation.delete(valuations)
# op = OpenPosition.search(['line','=',line.id])
# if op:
# OpenPosition.delete(op)
super(Line, cls).delete(lines)
@classmethod
def validate(cls, lines):
super(Line, cls).validate(lines)
for line in lines:
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
if not line.created_by_code:
if not line.lots and line.product.type != 'service' and line.quantity != Decimal(0):
FeeLots = Pool().get('fee.lots')
LotQtHist = Pool().get('lot.qt.hist')
LotQtType = Pool().get('lot.qt.type')
Lot = Pool().get('lot.lot')
lot = Lot()
lot.line = line.id
lot.lot_qt = None
lot.lot_unit = None
lot.lot_unit_line = line.unit
lot.lot_quantity = round(Decimal(line.quantity),5)
lot.lot_gross_quantity = None
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
logger.info("PURCHASE_VALIDATE:%s",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),('line','=',line.id)])
if not fl_check:
fl = FeeLots()
fl.fee = fee.id
fl.lot = lot.id
fl.line = line.id
FeeLots.save([fl])
#update inherit fee qt
# if line.fees:
# Fee = Pool().get('fee.fee')
# for f in line.fees:
# if f.inherit_qt:
# f.quantity = round(line.quantity_theorical,4)
# f.unit = line.unit
# Fee.save([f])
#check if fee purchase is filled on fee
if line.fee_:
if not line.fee_.purchase:
Fee = Pool().get('fee.fee')
f = Fee(line.fee_)
f.purchase = line.purchase
Fee.save([f])
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.line.quantity_theorical) - pr.fixed_qt
pr.unfixed_qt_price = pr.fixed_qt_price
pr.eod_price = pr.get_eod_price_purchase()
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:
logger.info("CHECK_PRICING:%s",t)
prD, prP = t.getPricingListDates(pc.calendar)
logger.info("CHECK_PRICING2:%s",prP)
apD, apP = t.getApplicationListDates(pc.calendar)
prDate.extend(prD)
prPrice.extend(prP)
apDate.extend(apD)
apPrice.extend(apP)
if pc.quota:
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.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),5)
price = round(Decimal(self.getnearprice(pl,d,'price')),4)
p.settl_price = price
if price > 0:
cumul_qt += pc.quota
p.fixed_qt = round(Decimal(cumul_qt),5)
p.fixed_qt_price = round(Decimal(self.getnearprice(pl,d,'avg')),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')),4)
p.unfixed_qt = round(Decimal(self.quantity_theorical) - Decimal(cumul_qt),5)
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.purchase.currency,True))
pr = round(pr,4)
logger.info("GENERATE_2:%s",pr)
p.unfixed_qt_price = pr
p.eod_price = p.get_eod_price_purchase()
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 view_attributes(cls):
# return super().view_attributes() + [
# ('/tree/field[@name="quantity"]', 'visual',
# If(Eval('quantity') & (Eval('quantity', 0) > 0),'success','danger')),
# ]
class GoToBi(Wizard):
__name__ = 'purchase.bi'
start_state = 'bi'
bi = StateAction('purchase_trade.url_bi')
def do_bi(self, action):
Configuration = Pool().get('gr.configuration')
config = Configuration.search(['id','>',0])[0]
ct_number = self.records[0].number
action['url'] = config.bi + '/dashboard/6-pnl?lot=&product=&purchase='+ ct_number + '&sale='
return action, {}
class PurchaseAllocationsWizard(Wizard):
'Open Allocations report from Purchase without modal'
__name__ = 'purchase.allocations.wizard'
start_state = 'open_report'
open_report = StateAction('purchase_trade.act_lot_report_form')
def do_open_report(self, action):
purchase_id = Transaction().context.get('active_id')
if not purchase_id:
raise ValueError("No active purchase ID in context")
action['context_model'] = 'lot.context'
action['pyson_context'] = PYSONEncoder().encode({
'purchase': purchase_id,
})
return action, {}
class PurchaseInvoiceReport(
ModelSQL, ModelView):
"Purchase invoices"
__name__ = 'purchase.invoice.report'
r_supplier = fields.Many2One('party.party',"Supplier")
r_purchase = fields.Many2One('purchase.purchase', "Purchase")
r_line = fields.Many2One('purchase.line',"Line")
r_lot = fields.Many2One('lot.lot',"Lot")
r_product = fields.Many2One('product.product', "Product")
r_pur_invoice = fields.Many2One('account.invoice',"Invoice")
r_inv_date = fields.Date("Inv. date")
r_pur_payment = fields.Many2Many('account.invoice-account.move.line','invoice', 'line', string='Payment')
r_invoice_amount = fields.Numeric("Amount",digits=(1,2))
r_payment_amount = fields.Numeric("Paid",digits=(1,2))
r_left_amount = fields.Numeric("Left",digits=(1,2))
r_curr = fields.Many2One('currency.currency',"Curr")
r_reconciliation = fields.Integer("Reconciliation")
r_move = fields.Many2One('account.move',"Move")
r_status = fields.Selection([
('not', 'Not'),
('paid', 'Paid'),
('partial', 'Partially'),
], 'Status')
@classmethod
def table_query(cls):
pool = Pool()
Invoice = pool.get('account.invoice')
InvoiceLine = pool.get('account.invoice.line')
PurchaseLine = pool.get('purchase.line')
Purchase = pool.get('purchase.purchase')
Party = pool.get('party.party')
InvoicePayment = pool.get('account.invoice-account.move.line')
MoveLine = pool.get('account.move.line')
Move = pool.get('account.move')
Currency = pool.get('currency.currency')
Lot = pool.get('lot.lot')
ai = Invoice.__table__()
ail = InvoiceLine.__table__()
pl = PurchaseLine.__table__()
pu = Purchase.__table__()
pa = Party.__table__()
aiaml = InvoicePayment.__table__()
aml = MoveLine.__table__()
lot = Lot.__table__()
cu = Currency.__table__()
mo = Move.__table__()
context = Transaction().context
supplier = context.get('supplier')
purchase = context.get('purchase')
asof = context.get('asof')
todate = context.get('todate')
state = context.get('state')
wh = Literal(True)
wh &= lot.lot_type == 'physic'
wh &= ai.type == 'in'
if supplier:
wh &= (ai.party == supplier)
if purchase:
wh &= (pu.id == purchase)
if asof and todate:
wh &= (ai.invoice_date >= asof) & (ai.invoice_date <= todate)
query = (
lot
.join(ail, 'LEFT', condition=ail.lot == lot.id)
.join(ai, 'LEFT', condition=ai.id == ail.invoice)
.join(pl, 'LEFT', condition=pl.id == lot.line)
.join(pu, 'LEFT', condition=pl.purchase == pu.id)
.join(pa, 'LEFT', condition=ai.party == pa.id)
.join(cu, 'LEFT', condition=cu.id == ail.currency)
.select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(0).as_('write_uid'),
Literal(0).as_('write_date'),
Max(ail.id).as_('id'),
Max(pa.id).as_('r_supplier'),
pu.id.as_('r_purchase'),
pl.id.as_('r_line'),
Max(ail.product).as_('r_product'),
Max(lot.id).as_('r_lot'),
ai.id.as_('r_pur_invoice'),
Max(ai.invoice_date).as_('r_inv_date'),
Sum(ail.quantity*ail.unit_price).as_('r_invoice_amount'),
Max(cu.id).as_('r_curr'),
where=wh,
group_by=[pu.id,pl.id,ai.id]
)
)
query_alias = query
left = Case((Abs(Sum(aml.amount_second_currency))>0,(Max(query_alias.r_invoice_amount)-Sum(aml.amount_second_currency))),else_=(Max(query_alias.r_invoice_amount)-(Sum(aml.debit)-Sum(aml.credit))))
status = Case((left==0, 'paid'),else_=Case((left<Max(query_alias.r_invoice_amount),'partial'),else_='not'))
wh = Literal(True)
if state != 'all':
wh &= status == state
query2 = (
query_alias
.join(aiaml, 'LEFT', condition=(aiaml.invoice == query_alias.r_pur_invoice))
.join(aml, 'LEFT', condition=aml.id == aiaml.line)
.join(mo, 'LEFT', condition=mo.id == aml.move)
.select(
Literal(0).as_('create_uid'),
CurrentTimestamp().as_('create_date'),
Literal(0).as_('write_uid'),
Literal(0).as_('write_date'),
Max(query_alias.id).as_('id'),
Max(query_alias.r_supplier).as_('r_supplier'),
query_alias.r_purchase.as_('r_purchase'),
query_alias.r_line.as_('r_line'),
Max(query_alias.r_product).as_('r_product'),
Max(query_alias.r_lot).as_('r_lot'),
query_alias.r_pur_invoice.as_('r_pur_invoice'),
Max(query_alias.r_inv_date).as_('r_inv_date'),
Max(query_alias.r_invoice_amount).as_('r_invoice_amount'),
Sum(aml.amount_second_currency).as_('r_payment_amount'),
left.as_('r_left_amount'),
status.as_('r_status'),
Max(query_alias.r_curr).as_('r_curr'),
Max(aml.reconciliation).as_('r_reconciliation'),
Max(mo.id).as_('r_move'),
Max(aml.id).as_('r_pur_payment'),
where=wh,
group_by=[query_alias.r_purchase,query_alias.r_line,query_alias.r_pur_invoice]
))
return query2
class PurchaseInvoiceContext(ModelView):
"Purchase Invoice Context"
__name__ = 'purchase.invoice.context'
asof = fields.Date("As of")
todate = fields.Date("To")
supplier = fields.Many2One('party.party',"Supplier")
purchase = fields.Many2One('purchase.purchase', "Purchase")
state = fields.Selection([
('all', 'All'),
('not', 'Not'),
('paid', 'Paid'),
('partial', 'Partially')
], 'State')
@classmethod
def default_asof(cls):
pool = Pool()
Date = pool.get('ir.date')
return Date.today().replace(day=1,month=1,year=1999)
@classmethod
def default_todate(cls):
pool = Pool()
Date = pool.get('ir.date')
return Date.today()
@classmethod
def default_state(cls):
return 'all'
class InvoicePayment(Wizard):
'Payments'
__name__ = 'purchase.invoice.payment'
start_state = 'open_'
open_ = StateAction('account.act_move_line_form')
def do_open_(self, action):
if self.record.r_pur_invoice:
Payment = Pool().get('account.invoice-account.move.line')
payments = Payment.search(['invoice','=',self.record.r_pur_invoice.id])
if payments:
lines = [e.line.id for e in payments]
return action, {'res_id': lines}
class PnlReport(Wizard):
'Pnl report'
__name__ = 'pnl.report'
start = StateAction('purchase_trade.act_pnl_bi')
def do_start(self, action):
pool = Pool()
# action['views'].reverse()
return action, {'res_id': [1]}
class PnlBI(ModelSingleton,ModelSQL, ModelView):
'Pnl BI'
__name__ = 'pnl.bi'
input = fields.Text("BI")
metabase = fields.Function(fields.Text(""),'get_bi')
def get_bi(self,name=None):
Configuration = Pool().get('gr.configuration')
config = Configuration.search(['id','>',0])[0]
payload = {
"resource": {"dashboard": config.pnl_id},
"params": {},
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
}
token = jwt.encode(payload, config.payload, algorithm="HS256")
logger.info("TOKEN:%s",token)
if config.dark:
url = f"metabase:{config.bi}/embed/dashboard/{token}#theme=night&bordered=true&titled=true"
else:
url = f"metabase:{config.bi}/embed/dashboard/{token}#bordered=true&titled=true"
return url
class PositionReport(Wizard):
'Position report'
__name__ = 'position.report'
start = StateAction('purchase_trade.act_position_bi')
def do_start(self, action):
pool = Pool()
# action['views'].reverse()
return action, {'res_id': [1]}
class PositionBI(ModelSingleton,ModelSQL, ModelView):
'Position BI'
__name__ = 'position.bi'
input = fields.Text("BI")
metabase = fields.Function(fields.Text(""),'get_bi')
def get_bi(self,name=None):
Configuration = Pool().get('gr.configuration')
config = Configuration.search(['id','>',0])[0]
payload = {
"resource": {"dashboard": config.position_id},
"params": {},
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
}
token = jwt.encode(payload, config.payload, algorithm="HS256")
logger.info("TOKEN:%s",token)
if config.dark:
url = f"metabase:{config.bi}/embed/dashboard/{token}#theme=night&bordered=true&titled=true"
else:
url = f"metabase:{config.bi}/embed/dashboard/{token}#bordered=true&titled=true"
return url