4530 lines
186 KiB
Python
Executable File
4530 lines
186 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
|
|
from trytond.model import (ModelSQL, ModelView, dualmethod)
|
|
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, Union, Select
|
|
from sql.conditionals import Coalesce
|
|
from sql.functions import CurrentTimestamp, DateTrunc, Abs
|
|
from sql.operators import Exists
|
|
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
|
|
from itertools import chain, groupby
|
|
from operator import itemgetter
|
|
import datetime
|
|
import json
|
|
import logging
|
|
from trytond.exceptions import UserWarning, UserError
|
|
from trytond.modules.purchase_trade.service import ContractFactory
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class LotAccountingGraph(ModelSQL,ModelView):
|
|
"Lot accounting graph"
|
|
__name__ = 'lot.accounting'
|
|
|
|
lot = fields.Many2One('lot.lot',"Lot")
|
|
name = fields.Char("Name")
|
|
graph = fields.Text("Graph")
|
|
flow_graph = fields.Function(fields.Text("Flux comptable (graph)"),
|
|
'get_flow_graph')
|
|
|
|
def get_flow_graph(self, name=None):
|
|
return self.lot.get_flow_graph()
|
|
|
|
class LotMove(ModelSQL,ModelView):
|
|
"Lots moves"
|
|
__name__ = 'lot.move'
|
|
|
|
lot = fields.Many2One('lot.lot', "Lot", ondelete='CASCADE')
|
|
move = fields.Many2One('stock.move', "Move", ondelete='CASCADE')
|
|
sequence = fields.Integer("Sequence")
|
|
|
|
class Lot(metaclass=PoolMeta):
|
|
__name__ = 'lot.lot'
|
|
|
|
_quantity_consistency_epsilon = Decimal("0.00001")
|
|
|
|
@classmethod
|
|
def _round_consistency_quantity(cls, quantity):
|
|
return Decimal(str(quantity or 0)).quantize(
|
|
cls._quantity_consistency_epsilon,
|
|
rounding=ROUND_HALF_UP)
|
|
|
|
@staticmethod
|
|
def _same_record(left, right):
|
|
return (
|
|
left == right
|
|
or getattr(left, 'id', left) == getattr(right, 'id', right))
|
|
|
|
@classmethod
|
|
def _convert_consistency_quantity(cls, quantity, from_unit, to_unit):
|
|
if not from_unit or not to_unit or cls._same_record(from_unit, to_unit):
|
|
return Decimal(str(quantity or 0))
|
|
Uom = Pool().get('product.uom')
|
|
factor = None
|
|
rate = None
|
|
if from_unit.category.id != to_unit.category.id:
|
|
factor = 1
|
|
rate = 1
|
|
return Decimal(Uom.compute_qty(
|
|
from_unit, float(quantity or 0), to_unit, True, factor, rate))
|
|
|
|
@classmethod
|
|
def _get_line_lots_for_consistency(cls, line):
|
|
lots = list(getattr(line, 'lots', None) or [])
|
|
virtual_lots = [
|
|
lot for lot in lots
|
|
if getattr(lot, 'lot_type', None) == 'virtual']
|
|
physical_lots = [
|
|
lot for lot in lots
|
|
if getattr(lot, 'lot_type', None) == 'physic']
|
|
return virtual_lots, physical_lots
|
|
|
|
@classmethod
|
|
def _get_lot_current_quantity_for_consistency(cls, lot, unit):
|
|
quantity = lot.get_current_quantity_converted(unit=unit)
|
|
return Decimal(str(quantity or 0))
|
|
|
|
@classmethod
|
|
def _get_lot_qt_quantity_for_consistency(cls, lqt, unit):
|
|
quantity = Decimal(str(getattr(lqt, 'lot_quantity', 0) or 0))
|
|
return cls._convert_consistency_quantity(
|
|
quantity, getattr(lqt, 'lot_unit', None), unit)
|
|
|
|
@classmethod
|
|
def _format_consistency_error(cls, line, message, values):
|
|
line_name = (
|
|
getattr(line, 'rec_name', None)
|
|
or getattr(line, 'id', None)
|
|
or line)
|
|
detail = ", ".join(
|
|
"%s=%s" % (key, cls._round_consistency_quantity(value))
|
|
for key, value in values)
|
|
return "Quantity consistency error on line %s: %s (%s)" % (
|
|
line_name, message, detail)
|
|
|
|
@classmethod
|
|
def _assert_line_quantity_consistency(cls, line):
|
|
if not line:
|
|
return
|
|
product = getattr(line, 'product', None)
|
|
if getattr(product, 'type', None) == 'service':
|
|
return
|
|
theorical = getattr(line, 'quantity_theorical', None)
|
|
if theorical is None:
|
|
return
|
|
|
|
unit = getattr(line, 'unit', None)
|
|
virtual_lots, physical_lots = cls._get_line_lots_for_consistency(line)
|
|
if not virtual_lots:
|
|
return
|
|
if len(virtual_lots) > 1:
|
|
raise UserError(
|
|
cls._format_consistency_error(
|
|
line,
|
|
"more than one virtual lot exists",
|
|
[('virtual_lots', len(virtual_lots))]))
|
|
|
|
# Temporarily suspended: repesages successifs are no longer tied
|
|
# reliably to the original lot.qt forecast split. We keep the helper
|
|
# calls in place, but do not enforce the two historical invariants:
|
|
# 1) physical lots + virtual lot must equal quantity_theorical
|
|
# 2) sum(non-zero lot.qt) must cover the positive virtual lot quantity
|
|
#
|
|
# vlot = virtual_lots[0]
|
|
# physical_quantity = sum(
|
|
# cls._get_lot_current_quantity_for_consistency(lot, unit)
|
|
# for lot in physical_lots)
|
|
# virtual_quantity = cls._get_lot_current_quantity_for_consistency(
|
|
# vlot, unit)
|
|
# theorical_quantity = Decimal(str(theorical or 0))
|
|
# expected_diff = (
|
|
# cls._round_consistency_quantity(physical_quantity)
|
|
# + cls._round_consistency_quantity(virtual_quantity)
|
|
# - cls._round_consistency_quantity(theorical_quantity))
|
|
# if expected_diff:
|
|
# raise UserError(
|
|
# cls._format_consistency_error(
|
|
# line,
|
|
# "physical lots + virtual lot must equal theoretical "
|
|
# "quantity",
|
|
# [
|
|
# ('physical', physical_quantity),
|
|
# ('virtual', virtual_quantity),
|
|
# ('theoretical', theorical_quantity),
|
|
# ('diff', expected_diff),
|
|
# ]))
|
|
#
|
|
# cls._assert_virtual_lot_open_quantity_consistency(vlot, line)
|
|
|
|
@classmethod
|
|
def _assert_virtual_lot_open_quantity_consistency(cls, vlot, line=None):
|
|
LotQt = Pool().get('lot.qt')
|
|
line = line or getattr(vlot, 'line', None) or getattr(vlot, 'sale_line', None)
|
|
if not line:
|
|
return
|
|
unit = getattr(line, 'unit', None)
|
|
if getattr(vlot, 'line', None):
|
|
lotqts = LotQt.search([('lot_p', '=', vlot.id)])
|
|
elif getattr(vlot, 'sale_line', None):
|
|
lotqts = LotQt.search([('lot_s', '=', vlot.id)])
|
|
else:
|
|
return
|
|
|
|
open_quantity = sum(
|
|
cls._get_lot_qt_quantity_for_consistency(lqt, unit)
|
|
for lqt in lotqts
|
|
if Decimal(str(getattr(lqt, 'lot_quantity', 0) or 0)) != 0)
|
|
virtual_quantity = cls._get_lot_current_quantity_for_consistency(
|
|
vlot, unit)
|
|
expected_open_quantity = max(virtual_quantity, Decimal(0))
|
|
diff = (
|
|
cls._round_consistency_quantity(open_quantity)
|
|
- cls._round_consistency_quantity(expected_open_quantity))
|
|
if diff < 0:
|
|
raise UserError(
|
|
cls._format_consistency_error(
|
|
line,
|
|
"lot.qt forecast quantity must not be lower than "
|
|
"virtual lot positive current quantity",
|
|
[
|
|
('lot_qt', open_quantity),
|
|
('virtual', virtual_quantity),
|
|
('expected_open', expected_open_quantity),
|
|
('diff', diff),
|
|
]))
|
|
|
|
@classmethod
|
|
def _fresh_consistency_line(cls, line):
|
|
if not line:
|
|
return None
|
|
line_id = getattr(line, 'id', None)
|
|
if not line_id:
|
|
return line
|
|
if getattr(line, 'purchase', None):
|
|
return Pool().get('purchase.line')(line_id)
|
|
if getattr(line, 'sale', None):
|
|
return Pool().get('sale.line')(line_id)
|
|
return line
|
|
|
|
@classmethod
|
|
def assert_lines_quantity_consistency(cls, lines):
|
|
seen = set()
|
|
for line in lines:
|
|
line = cls._fresh_consistency_line(line)
|
|
line_id = (
|
|
getattr(line, '__name__', None),
|
|
getattr(line, 'id', id(line)))
|
|
if line_id in seen:
|
|
continue
|
|
seen.add(line_id)
|
|
cls._assert_line_quantity_consistency(line)
|
|
|
|
@classmethod
|
|
def assert_lots_quantity_consistency(cls, lots):
|
|
lines = []
|
|
for lot in lots:
|
|
if getattr(lot, 'line', None):
|
|
lines.append(lot.line)
|
|
if getattr(lot, 'sale_line', None):
|
|
lines.append(lot.sale_line)
|
|
cls.assert_lines_quantity_consistency(lines)
|
|
|
|
@staticmethod
|
|
def skip_quantity_consistency():
|
|
return Transaction().set_context(
|
|
_purchase_trade_skip_quantity_consistency=True)
|
|
|
|
line = fields.Many2One('purchase.line',"Purchase",ondelete='CASCADE')
|
|
move = fields.Function(fields.Many2One('stock.move',"Move"),'get_current_move')
|
|
lot_move = fields.One2Many('lot.move','lot',"Move")
|
|
invoice_line = fields.Many2One('account.invoice.line',"Purch.Invoice line")
|
|
invoice_line_prov = fields.Many2One('account.invoice.line',"Purch. Invoice line prov")
|
|
sale_invoice_line = fields.Many2One('account.invoice.line',"Sale Invoice line")
|
|
sale_invoice_line_prov = fields.Many2One('account.invoice.line',"Sale Invoice line prov")
|
|
sale_invoice_padding = fields.Numeric(
|
|
"Sale invoice padding", digits='lot_unit_line', readonly=True)
|
|
delta_qt = fields.Numeric("Delta Qt")
|
|
delta_pr = fields.Numeric("Delta Pr")
|
|
delta_amt = fields.Numeric("Delta Amt")
|
|
warrant_nb = fields.Char("Warrant Nb")
|
|
lot_chunk_key = fields.Integer("Chunk key")
|
|
#fees = fields.Many2Many('fee.lots', 'lot', 'fee',"Fees")
|
|
dashboard = fields.Many2One('purchase.dashboard',"Dashboard")
|
|
pivot = fields.Function(
|
|
fields.Text('Pivot'), 'get_pivot'
|
|
)
|
|
vis_data = fields.Function(
|
|
fields.Text('Graphe JSON'), 'get_vis_data'
|
|
)
|
|
|
|
flow_graph = fields.Function(fields.Text("Flux comptable (graph)"),
|
|
'get_flow_graph')
|
|
|
|
def get_pivot(self,name=None):
|
|
AccountMoveLine = Pool().get('account.move.line')
|
|
Account = Pool().get('account.account')
|
|
Stock = Pool().get('stock.move')
|
|
Invoice = Pool().get('account.invoice')
|
|
ForexP = Pool().get('forex.cover.physical.contract')
|
|
ForexS = Pool().get('forex.cover.physical.sale')
|
|
InvoiceLine = Pool().get('account.invoice.line')
|
|
Currency = Pool().get('currency.currency')
|
|
aml = AccountMoveLine.search([('lot','=',self.id)])
|
|
logger.info("GET_PIVOT:%s",aml)
|
|
forexP = None
|
|
forexS = None
|
|
if self.line:
|
|
forexP = ForexP.search(['contract','=',self.line.purchase.id])
|
|
logger.info("GET_PIVOT:%s",forexP)
|
|
if self.sale_line:
|
|
forexS = ForexS.search(['contract','=',self.sale_line.sale.id])
|
|
if forexP:
|
|
forex = AccountMoveLine.search([('origin','=','forex.forex,'+str(forexP[0].forex.id))])
|
|
if forex:
|
|
aml += AccountMoveLine.search(['move','=',forex[0].move.id])
|
|
aml_ = AccountMoveLine.search([('revaluate','=',forex[1].id)])
|
|
if aml_:
|
|
aml += AccountMoveLine.search(['move','=',aml_[0].move.id])
|
|
logger.info("GET_PIVOT:%s",aml_)
|
|
pivot_data = {
|
|
"data": [],
|
|
"options": None
|
|
}
|
|
if aml:
|
|
for l in aml:
|
|
acc= Account(l.account)
|
|
ct_type = 'Purchase'
|
|
pr_type = 'Goods'
|
|
acc_label = 'Account event'
|
|
move_line = str(l.id)
|
|
journal = l.move.journal.id
|
|
product = self.lot_product.name
|
|
ct_number = ''
|
|
amount = l.debit if l.debit != 0 else -l.credit
|
|
second_amount = ''
|
|
rate = 1
|
|
if l.amount_second_currency:
|
|
second_amount = float(abs(l.amount_second_currency))
|
|
rate = round(abs(amount / abs(l.amount_second_currency)),4)
|
|
second_curr = None
|
|
if l.second_currency:
|
|
second_curr = Currency(l.second_currency.id).symbol
|
|
else:
|
|
second_curr = Currency(l.move.company.currency.id).symbol
|
|
|
|
# if l.account.type.payable and l.account.type.statement == 'income':
|
|
# amount = -amount
|
|
ori = None #l.origin
|
|
if not ori:
|
|
ori = l.move.origin
|
|
if ori:
|
|
origin, id = str(ori).split(',')
|
|
if origin == 'stock.move':
|
|
acc_label = '1-Reception'
|
|
sm = Stock(int(id))
|
|
if sm.to_location.type == 'customer':
|
|
ct_type = 'Sale'
|
|
if self.sale_line:
|
|
ct_number = self.sale_line.sale.number
|
|
else:
|
|
ct_type = 'Purchase'
|
|
if self.line:
|
|
ct_number = self.line.purchase.number
|
|
elif origin == 'account.invoice':
|
|
inv = Invoice(int(id))
|
|
product = None
|
|
if inv.lines[0].product:
|
|
product = inv.lines[0].product.name
|
|
if inv.type == 'out':
|
|
ct_type = 'Sale'
|
|
if self.sale_line:
|
|
ct_number = self.sale_line.sale.number
|
|
else:
|
|
ct_type = 'Purchase'
|
|
if self.line:
|
|
ct_number = self.line.purchase.number
|
|
if (journal == 1 and (l.account.id == 824 or l.account.id == 766)):
|
|
acc_label = 'COG Goods'
|
|
elif journal == 3:
|
|
acc_label = '4-Payment'
|
|
else:
|
|
if "Payment of invoice" in (l.move.description if l.move.description else ""):
|
|
acc_label = '4-Payment'
|
|
else:
|
|
if 'Prepayment' in (inv.description if inv.description else ""):
|
|
acc_label = '0-Prepayment'
|
|
else:
|
|
if inv.reference == 'Provisional':
|
|
acc_label = '2-Prov'
|
|
if l.move.description == 'Adjustment':
|
|
acc_label = '2-Adjustment'
|
|
else:
|
|
acc_label = '3-Final'
|
|
else:
|
|
if journal == 6:
|
|
# rate = 1
|
|
if l.move.IsBankMove():
|
|
acc_label = '4-Forex reval'
|
|
else:
|
|
acc_label = '4-Inv reval'
|
|
elif journal == 8:
|
|
acc_label = '4-Payment Fx'
|
|
|
|
pivot_data["data"].append(
|
|
{
|
|
"lot": self.lot_name,
|
|
"ct type": ct_type,
|
|
"pr type": pr_type,
|
|
"ct number": ct_number,
|
|
"event": acc_label,
|
|
"product": product,
|
|
"amount": str(amount),
|
|
"account": str(acc.code),
|
|
"move": l.move.number,
|
|
"move line": move_line,
|
|
"journal": journal,
|
|
"event_date": str(l.move.date),
|
|
"rate": str(rate),
|
|
"tr_amount": str(second_amount),
|
|
"Curr": second_curr
|
|
}
|
|
)
|
|
|
|
pivot_data['options'] = {
|
|
"rows": ["lot","ct type","event_date","event","move","Curr","rate"],
|
|
"cols": ["account"],
|
|
"aggregatorName": "Sum",
|
|
"vals": ["amount"]
|
|
}
|
|
return "pivotjson:" + json.dumps(pivot_data)
|
|
|
|
def get_flow_graph(self, name=None):
|
|
AccountMove = Pool().get('account.move')
|
|
colorBox = '#AABDAF'
|
|
colorBoxB = '#5C665E'
|
|
colorBoxH = '#C2D7C7'
|
|
colorBoxHB = '#7A877D'
|
|
colorCircle = '#C4D9C9'
|
|
colorCircleB = '#617052'
|
|
colorCircleH = '#DFF7E5'
|
|
colorCircleHB = '#8EA67A'
|
|
data = {
|
|
'nodes': [
|
|
{
|
|
'id': 'lot.lot ' + str(self.id),
|
|
'label': 'Lot\n' + self.lot_name,
|
|
'data_model': 'lot.lot',
|
|
'data_id': self.id,
|
|
'x': -300,
|
|
'y': 0,
|
|
'color': {
|
|
'background': colorBox,
|
|
'border': colorBoxB,
|
|
'highlight': {
|
|
'background':colorBoxH,
|
|
'border': colorBoxHB
|
|
}
|
|
},
|
|
'fixed': {
|
|
'x': False,
|
|
'y': True
|
|
}
|
|
}
|
|
],
|
|
'edges': []
|
|
}
|
|
|
|
data['nodes'].append({
|
|
'id': 'purchase.purchase ' + str(self.line.purchase.id),
|
|
'label': 'Purchase\n' + self.line.purchase.number + '\n' + self.line.purchase.party.name,
|
|
'data_model': 'purchase.purchase',
|
|
'data_id': self.line.purchase.id,
|
|
'x': -900,
|
|
'y': 0,
|
|
'color': {
|
|
'background': colorBox,
|
|
'border': colorBoxB,
|
|
'highlight': {
|
|
'background':colorBoxH,
|
|
'border': colorBoxHB
|
|
}
|
|
},
|
|
'fixed': {
|
|
'x': False,
|
|
'y': True
|
|
}
|
|
})
|
|
|
|
data['nodes'].append({
|
|
'id': 'purchase.line ' + str(self.line.id),
|
|
'label': 'Line\n' + self.line.product.name + '\n' + str(self.line.quantity),
|
|
'data_model': 'purchase.line',
|
|
'data_id': self.line.id,
|
|
'x': -600,
|
|
'y': 0,
|
|
'color': {
|
|
'background': colorBox,
|
|
'border': colorBoxB,
|
|
'highlight': {
|
|
'background':colorBoxH,
|
|
'border': colorBoxHB
|
|
}
|
|
},
|
|
'fixed': {
|
|
'x': False,
|
|
'y': True
|
|
}
|
|
})
|
|
|
|
data['edges'].append({
|
|
'from': 'purchase.purchase ' + str(self.line.purchase.id),
|
|
'to': 'purchase.line ' + str(self.line.id),
|
|
'label': '',
|
|
'length': 300,
|
|
'color': {
|
|
'color': '#267F82',
|
|
'highlight': '#1D5F62',
|
|
'hover': '#1D5F62'
|
|
}
|
|
})
|
|
|
|
data['edges'].append({
|
|
'from': 'purchase.line ' + str(self.line.id),
|
|
'to': 'lot.lot ' + str(self.id),
|
|
'label': '',
|
|
'length': 300,
|
|
'color': {
|
|
'color': '#267F82',
|
|
'highlight': '#1D5F62',
|
|
'hover': '#1D5F62'
|
|
}
|
|
})
|
|
|
|
labLines = ''
|
|
if self.lot_move:
|
|
stock_node = {
|
|
"id": "stock.move " + str(self.lot_move[0].move.id),
|
|
"label": "Move\n"
|
|
+ self.lot_move[0].move.from_location.name
|
|
+ "\n"
|
|
+ self.lot_move[0].move.to_location.name,
|
|
"data_model": "stock.move",
|
|
"data_id": self.lot_move[0].move.id,
|
|
"shape": "circle",
|
|
"color": {
|
|
"background": colorCircle,
|
|
"border": colorCircleB,
|
|
'highlight': {
|
|
'background':colorCircleH,
|
|
'border': colorCircleHB
|
|
}
|
|
},
|
|
}
|
|
|
|
data['nodes'].append(stock_node)
|
|
origin = 'stock.move,' + str(self.lot_move[0].move.id)
|
|
am = AccountMove.search([('origin','=',origin)])
|
|
if am:
|
|
for l in am[0].lines:
|
|
labLines += 'Debit ' if l.debit != 0 else 'Credit '
|
|
labLines += str(l.account.code) + "\n"
|
|
data["edges"].append(
|
|
{
|
|
"from": "lot.lot " + str(self.id),
|
|
"to": "stock.move " + str(self.lot_move[0].move.id),
|
|
"label": labLines,
|
|
"data_model": "account.move",
|
|
"data_id": am[0].id,
|
|
"length": 300,
|
|
'color': {
|
|
'color': '#267F82',
|
|
'highlight': '#1D5F62',
|
|
'hover': '#1D5F62'
|
|
}
|
|
}
|
|
)
|
|
|
|
|
|
if self.lot_shipment_in:
|
|
shipment_node = {
|
|
"id": "stock.shipment.in " + str(self.lot_shipment_in.id),
|
|
"label": "Shipment\n" + self.lot_shipment_in.reference,
|
|
"data_model": "stock.shipment.in",
|
|
"data_id": self.lot_shipment_in.id,
|
|
"title": "Vessel in transit",
|
|
"x": 0,
|
|
"y": 0,
|
|
'color': {
|
|
'background': colorBox,
|
|
'border': colorBoxB,
|
|
'highlight': {
|
|
'background':colorBoxH,
|
|
'border': colorBoxHB
|
|
}
|
|
},
|
|
"fixed": {"x": False, "y": True},
|
|
}
|
|
|
|
data['nodes'].append(shipment_node)
|
|
if self.lot_move:
|
|
data["edges"].append(
|
|
{
|
|
"from": "lot.lot " + str(self.id),
|
|
"to": "stock.move " + str(self.lot_move[0].move.id),
|
|
"label": labLines,
|
|
"length": 300,
|
|
'color': {
|
|
'color': '#267F82',
|
|
'highlight': '#1D5F62',
|
|
'hover': '#1D5F62'
|
|
}
|
|
}
|
|
)
|
|
data["edges"].append(
|
|
{
|
|
"from": "stock.move " + str(self.lot_move[0].move.id),
|
|
"to": "stock.shipment.in " + str(self.lot_shipment_in.id),
|
|
"length": 300,
|
|
'color': {
|
|
'color': '#267F82',
|
|
'highlight': '#1D5F62',
|
|
'hover': '#1D5F62'
|
|
}
|
|
}
|
|
)
|
|
|
|
else:
|
|
data["edges"].append(
|
|
{
|
|
"from": "lot.lot " + str(self.id),
|
|
"to": "stock.shipment.in " + str(self.lot_shipment_in.id),
|
|
"length": 300,
|
|
'color': {
|
|
'color': '#267F82',
|
|
'highlight': '#1D5F62',
|
|
'hover': '#1D5F62'
|
|
}
|
|
}
|
|
)
|
|
|
|
|
|
if self.invoice_line_prov:
|
|
prov_node = {
|
|
"id": "account.invoice " + str(self.invoice_line_prov.invoice.id),
|
|
"label": "Prov.\n" + self.invoice_line_prov.invoice.number,
|
|
"data_model": "account.invoice",
|
|
"data_id": self.invoice_line_prov.invoice.id,
|
|
"length": 300,
|
|
"shape": "circle",
|
|
"color": {
|
|
"background": colorCircle,
|
|
"border": colorCircleB,
|
|
'highlight': {
|
|
'background':colorCircleH,
|
|
'border': colorCircleHB
|
|
}
|
|
}
|
|
}
|
|
|
|
data['nodes'].append(prov_node)
|
|
origin = 'account.invoice,' + str(self.invoice_line_prov.invoice.id)
|
|
am = AccountMove.search([('origin','=',origin)])
|
|
if am:
|
|
labLines = ''
|
|
for l in am[0].lines:
|
|
labLines += 'Debit ' if l.debit != 0 else 'Credit '
|
|
labLines += str(l.account.code) + "\n"
|
|
data["edges"].append(
|
|
{
|
|
"from": "account.invoice " + str(self.invoice_line_prov.invoice.id),
|
|
"to": "lot.lot " + str(self.id),
|
|
"label": labLines,
|
|
"data_model": "account.move",
|
|
"data_id": am[0].id,
|
|
"length": 300,
|
|
'color': {
|
|
'color': '#267F82',
|
|
'highlight': '#1D5F62',
|
|
'hover': '#1D5F62'
|
|
}
|
|
}
|
|
)
|
|
|
|
return "visjson:" + json.dumps(data)
|
|
|
|
def get_vis_data(self, name):
|
|
data = {
|
|
"nodes": [{"id": self.id, "label": self.lot_name}],
|
|
"edges": [{"from": self.id, "to": self.id}]
|
|
}
|
|
return 'visjson:' + json.dumps(data)
|
|
|
|
def get_forex_rate(self, amount):
|
|
"""
|
|
Calcule le taux de change moyen applicable à un montant donné,
|
|
en fonction des contrats de couverture Forex liés à l'achat (lot).
|
|
|
|
Retourne :
|
|
rate (float): taux moyen pondéré appliqué
|
|
left_amount (float): montant restant à couvrir (si non totalement couvert)
|
|
"""
|
|
rate = 0.0
|
|
left_amount = amount
|
|
|
|
Forex = Pool().get('forex.forex')
|
|
ForexP = Pool().get('forex.cover.physical.contract')
|
|
|
|
# On récupère les contrats forex physiques liés au contrat d'achat
|
|
forex_contracts = ForexP.search([
|
|
('contract', '=', self.line.purchase.id)
|
|
])
|
|
|
|
if not forex_contracts:
|
|
return None, amount # Aucun contrat forex trouvé
|
|
|
|
total_covered = sum([fc.amount for fc in forex_contracts])
|
|
weighted_sum = Decimal(0)
|
|
|
|
# Cas 1 : Montant totalement couvert ou surcouvert
|
|
if total_covered >= amount:
|
|
remaining_to_cover = amount
|
|
for fc in forex_contracts:
|
|
if remaining_to_cover <= 0:
|
|
break
|
|
|
|
covered_part = min(fc.amount, remaining_to_cover)
|
|
forex = Forex(fc.forex.id)
|
|
rate_part = forex.rate
|
|
|
|
weighted_sum += covered_part * rate_part
|
|
remaining_to_cover -= covered_part
|
|
|
|
rate = weighted_sum / amount
|
|
left_amount = Decimal(0)
|
|
|
|
# Cas 2 : Montant partiellement couvert
|
|
else:
|
|
for fc in forex_contracts:
|
|
forex = Forex(fc.forex)
|
|
weighted_sum += fc.amount * forex.rate
|
|
|
|
rate = weighted_sum / total_covered if total_covered > 0 else None
|
|
left_amount = amount - total_covered
|
|
logger.info("GET_FOREX:%s",rate)
|
|
logger.info("GET_FOREX2:%s",left_amount)
|
|
return rate, left_amount
|
|
|
|
def get_cog(self):
|
|
MoveLine = Pool().get('account.move.line')
|
|
Currency = Pool().get('currency.currency')
|
|
Date = Pool().get('ir.date')
|
|
AccountConfiguration = Pool().get('account.configuration')
|
|
account_configuration = AccountConfiguration(1)
|
|
Uom = Pool().get('product.uom')
|
|
ml = MoveLine.search([
|
|
('lot', '=', self.id),
|
|
('fee', '=', None),
|
|
('account', '=', self.lot_product.account_stock_in_used.id),
|
|
('origin', 'ilike', '%stock.move%'),
|
|
])
|
|
|
|
logger.info("GET_COG:%s",ml)
|
|
if ml:
|
|
return round(Decimal(sum([e.credit-e.debit for e in ml if e.description != 'Delivery'])),2)
|
|
else:
|
|
if self.lot_type == 'physic':
|
|
sup_mov = self.get_last_supplier_move()
|
|
if sup_mov:
|
|
logger.info("GET_COG_MOV:%s",sup_mov)
|
|
unit_price = sup_mov.unit_price
|
|
unit_price = Uom.compute_price(
|
|
self.lot_product.default_uom, unit_price, sup_mov.unit)
|
|
amount = sup_mov.company.currency.round(
|
|
Decimal(str(sup_mov.quantity)) * unit_price)
|
|
if account_configuration.stock_fx_forex == 'forex':
|
|
frate,amt = self.get_forex_rate(amount)
|
|
if frate:
|
|
amount = round(frate * amount,2)
|
|
with Transaction().set_context(date=Date.today()):
|
|
amount += Currency.compute(sup_mov.currency,
|
|
amt, sup_mov.company.currency)
|
|
amount_converted = round(amount,2)
|
|
else:
|
|
with Transaction().set_context(date=sup_mov.effective_date):
|
|
amount_converted = Currency.compute(sup_mov.currency,
|
|
amount, sup_mov.company.currency)
|
|
else:
|
|
with Transaction().set_context(date=sup_mov.effective_date):
|
|
amount_converted = Currency.compute(sup_mov.currency,
|
|
amount, sup_mov.company.currency)
|
|
return round(amount_converted,2)
|
|
return Decimal(0)
|
|
|
|
def IsDelivered(self):
|
|
if self.lot_move:
|
|
lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=True)
|
|
for m in lm:
|
|
if m.move.to_location.type == 'customer' and m.move.state == 'done':
|
|
return True
|
|
return False
|
|
|
|
def get_received_move(self):
|
|
if self.lot_move:
|
|
lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=True)
|
|
for m in lm:
|
|
if m.move.from_location.type == 'supplier' and m.move.state == 'done':
|
|
return m.move
|
|
return None
|
|
|
|
def GetShipment(self,type):
|
|
if type == 'in':
|
|
m = self.get_current_supplier_move()
|
|
if m and m.shipment:
|
|
origin,id = str(m.shipment).split(',')
|
|
if origin == 'stock.shipment.in':
|
|
return id
|
|
|
|
def get_current_customer_move(self):
|
|
if self.lot_move:
|
|
lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=True)
|
|
for m in lm:
|
|
if m.move.to_location.type == 'customer' and m.move.state != 'done' and m.move.from_location.type != 'supplier':
|
|
return m.move
|
|
|
|
def get_current_supplier_move(self):
|
|
if self.lot_move:
|
|
lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=True)
|
|
for m in lm:
|
|
if m.move.from_location.type == 'supplier' and m.move.state != 'done':
|
|
return m.move
|
|
|
|
def get_last_supplier_move(self):
|
|
if self.lot_move:
|
|
lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=True)
|
|
for m in lm:
|
|
if m.move.from_location.type == 'supplier':
|
|
return m.move
|
|
|
|
def get_current_move(self,name):
|
|
return self.get_current_move_by_seq()
|
|
|
|
def get_current_move_by_seq(self,seq=-1):
|
|
if self.lot_move:
|
|
if seq == -1:
|
|
lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=True)
|
|
if seq == 0:
|
|
lm = sorted(self.lot_move, key=lambda x: x.sequence, reverse=False)
|
|
if lm:
|
|
return lm[0].move
|
|
|
|
def getSupplier(self):
|
|
if self.line:
|
|
return Pool().get('purchase.purchase')(self.line.purchase).party.id
|
|
|
|
def getPurchase(self):
|
|
if self.line:
|
|
return self.line.purchase.id
|
|
|
|
def getVlot_p(self):
|
|
if self.line:
|
|
return [l for l in self.line.lots if l.lot_type=='virtual'][0]
|
|
|
|
def getVlot_s(self):
|
|
if self.sale_line:
|
|
return [l for l in self.sale_line.lots if l.lot_type=='virtual'][0]
|
|
|
|
def getLotQt(self):
|
|
LotQt = Pool().get('lot.qt')
|
|
if self.sale_line and not self.line:
|
|
return LotQt.search(['lot_s','=',self.id])
|
|
else:
|
|
return LotQt.search(['lot_p','=',self.id])
|
|
|
|
@classmethod
|
|
def default_dashboard(cls):
|
|
return 1
|
|
|
|
@classmethod
|
|
def _recompute_virtual_lot(cls, line):
|
|
LotHist = Pool().get('lot.qt.hist')
|
|
|
|
if not line:
|
|
return
|
|
|
|
lots = list(line.lots or [])
|
|
physical_lots = [l for l in lots if l.lot_type == 'physic']
|
|
virtual_lots = [l for l in lots if l.lot_type == 'virtual']
|
|
|
|
total_physic = sum(l.get_current_quantity_converted() for l in physical_lots)
|
|
theorical = line.quantity_theorical if hasattr(line, 'quantity_theorical') else line.quantity
|
|
virtual_qty = round(theorical - total_physic, 5)
|
|
logger.info("RECOMPUTE_VIRTUAL_LOT:%s",virtual_qty)
|
|
if virtual_lots:
|
|
vlot = virtual_lots[0]
|
|
lh = vlot.lot_hist[0]
|
|
lh.quantity = virtual_qty
|
|
lh.gross_quantity = virtual_qty
|
|
LotHist.save([lh])
|
|
|
|
@classmethod
|
|
def validate(cls, lots):
|
|
super(Lot, cls).validate(lots)
|
|
LotQt = Pool().get('lot.qt')
|
|
StockMove = Pool().get('stock.move')
|
|
virtual_lots_to_check = []
|
|
for lot in lots:
|
|
if lot.lot_type == 'virtual':
|
|
if lot.line:
|
|
lqt = LotQt.search([('lot_p','=',lot.id)])
|
|
if len(lqt)==0 and not lot.line.created_by_code:
|
|
lot.createVirtualPart(lot.lot_quantity,None,None)
|
|
# Wizard-created matched lines attach/split lot.qt after
|
|
# the virtual lot save; the final workflow check covers
|
|
# the complete state.
|
|
if not lot.line.created_by_code:
|
|
virtual_lots_to_check.append(lot)
|
|
|
|
if lot.sale_line:
|
|
lqt = LotQt.search([('lot_s','=',lot.id)])
|
|
if len(lqt)==0 and not lot.sale_line.created_by_code:
|
|
lot.createVirtualPart(lot.lot_quantity,None,lot.id,'only sale')
|
|
# Same rule as purchase side: do not check the transient
|
|
# save of a virtual lot created by matching code.
|
|
if not lot.sale_line.created_by_code:
|
|
virtual_lots_to_check.append(lot)
|
|
|
|
#Recalculate total line quantity
|
|
if lot.lot_type == 'physic':
|
|
if lot.line:
|
|
cls._recompute_virtual_lot(Pool().get('purchase.line')(lot.line))
|
|
cls._recalc_line_quantity(lot.line,True)
|
|
sup_mov = lot.get_current_supplier_move()
|
|
if sup_mov:
|
|
sup_mov.quantity = lot.get_current_quantity_converted()
|
|
sup_mov.unit_price = lot.get_lot_price()
|
|
StockMove.save([sup_mov])
|
|
|
|
if lot.sale_line:
|
|
cls._recompute_virtual_lot(Pool().get('sale.line')(lot.sale_line))
|
|
cls._recalc_line_quantity(lot.sale_line,False)
|
|
cus_mov = lot.get_current_customer_move()
|
|
if cus_mov:
|
|
cus_mov.quantity = lot.get_current_quantity_converted()
|
|
cus_mov.unit_price = lot.get_lot_sale_price()
|
|
StockMove.save([cus_mov])
|
|
#if matched check that a move exist sale side
|
|
if lot.line:
|
|
if not lot.IsDelivered() and not lot.get_current_customer_move():
|
|
from_ = lot.sale_line.sale.from_location
|
|
to_ = lot.sale_line.sale.to_location
|
|
if from_ and to_ and not (from_.type=='supplier' and to_.type=='customer'):
|
|
Move = Pool().get('stock.move')
|
|
nm = Move()
|
|
nm.from_location = from_
|
|
nm.to_location = to_
|
|
nm.product = lot.lot_product
|
|
nm.unit = lot.sale_line.unit
|
|
nm.quantity = lot.get_current_quantity_converted()
|
|
nm.origin = lot.sale_line
|
|
nm.lot = lot
|
|
nm.shipment = None
|
|
nm.currency = lot.sale_line.currency
|
|
nm.unit_price = lot.get_lot_sale_price()
|
|
Move.save([nm])
|
|
#check that lot with shipment are on this shipment fees
|
|
if lot.lot_shipment_in:
|
|
if lot.lot_shipment_in.fees:
|
|
for f in lot.lot_shipment_in.fees:
|
|
FeeLots = Pool().get('fee.lots')
|
|
fl = FeeLots.search(['fee','=',f.id])
|
|
if not fl:
|
|
fl = FeeLots()
|
|
fl.fee = f.id
|
|
fl.lot = lot.id
|
|
FeeLots.save([fl])
|
|
|
|
if not Transaction().context.get(
|
|
'_purchase_trade_skip_quantity_consistency'):
|
|
cls.assert_lots_quantity_consistency(virtual_lots_to_check)
|
|
|
|
def createVirtualPart(self,qt=0,sh=None,lot_s=None,mode='both'):
|
|
lqt = LotQt()
|
|
if self.lot_type == 'physic':
|
|
lqt.lot_p = self.getVlot_p()
|
|
lqt.lot_s = lot_s
|
|
else:
|
|
if mode =='both':
|
|
lqt.lot_p = self.id
|
|
lqt.lot_s = lot_s
|
|
else:
|
|
lqt.lot_s = lot_s
|
|
lqt.lot_p = None
|
|
if sh:
|
|
logger.info("IDSHIP:%s",sh)
|
|
str_sh = str(sh)
|
|
index = str_sh.rfind(",")
|
|
if index != -1:
|
|
id = int(str_sh[index+1:].strip())
|
|
if 'stock.shipment.in' in str_sh:
|
|
lqt.lot_shipment_in = id
|
|
elif 'stock.shipment.out' in str_sh:
|
|
lqt.lot_shipment_out = id
|
|
elif 'stock.shipment.internal' in str_sh:
|
|
lqt.lot_shipment_internal = id
|
|
lqt.lot_move = None
|
|
lqt.lot_av = 'available'
|
|
lqt.lot_status = 'forecast'
|
|
lqt.lot_quantity = qt
|
|
if self.line:
|
|
lqt.lot_unit = self.line.unit
|
|
if self.sale_line:
|
|
lqt.lot_unit = self.sale_line.unit
|
|
LotQt.save([lqt])
|
|
|
|
def updateVirtualPart(self,qt=0,sh=None,lot_s=None,mode='both'):
|
|
LotQt = Pool().get('lot.qt')
|
|
vlot = self
|
|
if self.lot_type == 'physic':
|
|
vlot = self.getVlot_p()
|
|
|
|
if mode == 'only sale':
|
|
vlot = None
|
|
|
|
lqts = None
|
|
if sh:
|
|
str_sh = str(sh)
|
|
index = str_sh.rfind(",")
|
|
if index != -1:
|
|
id = int(str_sh[index+1:].strip())
|
|
if 'stock.shipment.in' in str_sh:
|
|
lqts = LotQt.search([('lot_p','=',vlot),('lot_shipment_in','=',id),('lot_s','=',lot_s)])
|
|
elif 'stock.shipment.internal' in str_sh:
|
|
lqts = LotQt.search([('lot_p','=',vlot),('lot_shipment_internal','=',id),('lot_s','=',lot_s)])
|
|
elif 'stock.shipment.out' in str_sh:
|
|
lqts = LotQt.search([('lot_p','=',vlot),('lot_shipment_out','=',id),('lot_s','=',lot_s)])
|
|
else:
|
|
lqts = LotQt.search([('lot_p','=',vlot),('lot_s','=',lot_s),('lot_shipment_in','=',None),('lot_shipment_internal','=',None),('lot_shipment_out','=',None)])
|
|
if lqts:
|
|
logger.info("UVP_QT:%s",qt)
|
|
logger.info("UVP_LQ:%s",lqts[0].lot_quantity)
|
|
lqts[0].lot_quantity += qt
|
|
if lqts[0].lot_quantity < 0:
|
|
lqts[0].lot_quantity = 0
|
|
LotQt.save(lqts)
|
|
return True
|
|
return False
|
|
|
|
def createMove(self,r,qt,sh):
|
|
Move = Pool().get('stock.move')
|
|
nm = Move()
|
|
nm.from_location = r.lot_p.line.purchase.from_location
|
|
nm.to_location = r.lot_p.line.purchase.to_location
|
|
nm.product = r.lot_p.lot_product
|
|
nm.unit = r.lot_unit
|
|
nm.quantity = qt
|
|
nm.origin = r.lot_p.line
|
|
nm.shipment = sh
|
|
nm.currency = r.lot_p.line.currency
|
|
nm.unit_price = r.lot_p.line.unit_price
|
|
Move.save([nm])
|
|
return nm.id
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
vlist = [x.copy() for x in vlist]
|
|
L = Pool().get('lot.lot')
|
|
Uom = Pool().get('product.uom')
|
|
for values in vlist:
|
|
parent = values.get('lot_parent')
|
|
if parent:
|
|
#no split for virtual lot
|
|
if values['lot_type']=='virtual':
|
|
return
|
|
l = L(parent)
|
|
balenw = round(Decimal((l.lot_quantity if l.lot_quantity else 0)/Decimal(l.lot_qt)),2)
|
|
balegw = round(Decimal((l.lot_gross_quantity if l.lot_gross_quantity else 0)/Decimal(l.lot_qt)),2)
|
|
nw = Decimal(int(values.get('lot_qt')) * balenw)
|
|
gw = Decimal(int(values.get('lot_qt')) * balegw)
|
|
if l.lot_quantity:
|
|
l.lot_quantity -= nw
|
|
if l.lot_gross_quantity:
|
|
l.lot_gross_quantity -= gw
|
|
if l.lot_qt:
|
|
l.lot_qt -= values.get('lot_qt')
|
|
values['lot_quantity'] = nw
|
|
values['lot_type'] = 'physic'
|
|
values['lot_gross_quantity'] = gw
|
|
values['line'] = l.line
|
|
L.save([l])
|
|
|
|
records = super().create(vlist)
|
|
for record in records:
|
|
if not record.lot_name:
|
|
record.lot_name = str(record.id)
|
|
cls.save(records)
|
|
return records
|
|
|
|
@classmethod
|
|
def delete(cls, lots):
|
|
pool = Pool()
|
|
LL = pool.get('lot.lot')
|
|
FeeLots = pool.get('fee.lots')
|
|
lines_to_update = set()
|
|
sale_lines_to_update = set()
|
|
fees_to_sync = set()
|
|
logger.info("DELETE_FROM_LOTS:%s",lots)
|
|
for lot in lots:
|
|
if lot.lot_type == 'physic':
|
|
if lot.line:
|
|
lines_to_update.add(lot.line.id)
|
|
if lot.sale_line:
|
|
sale_lines_to_update.add(lot.sale_line.id)
|
|
if lot.lot_parent:
|
|
pa = LL(lot.lot_parent)
|
|
if pa:
|
|
pa.lot_qt += lot.lot_qt
|
|
pa.lot_quantity += lot.lot_quantity
|
|
pa.lot_gross_quantity += lot.lot_gross_quantity
|
|
LL.save([pa])
|
|
logger.info("DELETE_FROM_LOTS2:%s",lot)
|
|
fls = FeeLots.search(['lot','=',lot.id])
|
|
if fls:
|
|
fees_to_sync.update(
|
|
fl.fee.id for fl in fls if getattr(fl, 'fee', None))
|
|
FeeLots.delete(fls)
|
|
|
|
super(Lot, cls).delete(lots)
|
|
|
|
for line_id in lines_to_update:
|
|
cls._recompute_virtual_lot(Pool().get('purchase.line')(line_id))
|
|
cls._recalc_line_quantity(line_id,True)
|
|
|
|
for line_id in sale_lines_to_update:
|
|
cls._recompute_virtual_lot(Pool().get('sale.line')(line_id))
|
|
cls._recalc_line_quantity(line_id,False)
|
|
|
|
cls.assert_lines_quantity_consistency(
|
|
[Pool().get('purchase.line')(line_id) for line_id in lines_to_update])
|
|
cls.assert_lines_quantity_consistency(
|
|
[Pool().get('sale.line')(line_id) for line_id in sale_lines_to_update])
|
|
|
|
Fee = Pool().get('fee.fee')
|
|
for fee in Fee.browse(list(fees_to_sync)):
|
|
fee.sync_quantity_from_lots()
|
|
fee.adjust_purchase_values()
|
|
|
|
@classmethod
|
|
def _recalc_line_quantity(cls, line_id,purchase=True):
|
|
PL = Pool().get('purchase.line')
|
|
SL = Pool().get('sale.line')
|
|
if purchase:
|
|
line = PL(line_id)
|
|
else:
|
|
line = SL(line_id)
|
|
qt = Decimal(0)
|
|
lots = list(line.lots or [])
|
|
logger.info("DELETE_RECALC:%s",lots)
|
|
for lot in lots:
|
|
if len(lots) > 1:
|
|
if lot.lot_type == 'physic':
|
|
qt += lot.get_current_quantity_converted()
|
|
elif len(lots) == 1:
|
|
qt = lot.get_current_quantity_converted()
|
|
|
|
line.quantity = round(float(qt), 5)
|
|
if purchase:
|
|
PL.save([line])
|
|
else:
|
|
SL.save([line])
|
|
|
|
# @classmethod
|
|
# def write(cls, *args):
|
|
# Lot = Pool().get('lot.lot')
|
|
# super().write(*args)
|
|
# lots = sum(args[::2], [])
|
|
|
|
class LotQt(
|
|
ModelSQL, ModelView):
|
|
"Lots Qt"
|
|
__name__ = 'lot.qt'
|
|
|
|
lot_p = fields.Many2One('lot.lot',"Pur. lot")
|
|
lot_s = fields.Many2One('lot.lot',"Sale lot")
|
|
lot_quantity = fields.Numeric("Qt",digits=(1,5))
|
|
lot_unit = fields.Many2One('product.uom', "Unit")
|
|
lot_shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
|
|
lot_shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
|
|
lot_shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal")
|
|
lot_move = fields.Many2One('stock.move',"Move")
|
|
lot_av = fields.Selection([
|
|
('available', 'Available'),
|
|
('reserved', 'Reserved'),
|
|
('locked', 'Locked'),
|
|
('prov', 'Prov. inv'),
|
|
('invoiced', 'Invoiced')
|
|
], 'State')
|
|
lot_status = fields.Selection([
|
|
('forecast', 'Forecast'),
|
|
('loading', 'Loading'),
|
|
('transit', 'Transit'),
|
|
('destination', 'Destination'),
|
|
('stock', 'Stock'),
|
|
('delivered', 'Delivered')
|
|
], 'Where')
|
|
lot_visible = fields.Boolean("Visible")
|
|
lot_shipment_origin = fields.Function(
|
|
fields.Reference(
|
|
selection=[
|
|
("stock.shipment.in", "In"),
|
|
("stock.shipment.out", "Out"),
|
|
("stock.shipment.internal", "Internal"),
|
|
],
|
|
string="Shipment",
|
|
),
|
|
"get_shipment_origin",
|
|
)
|
|
|
|
def get_shipment_origin(self, name):
|
|
if self.lot_shipment_in:
|
|
return 'stock.shipment.in,' + str(self.lot_shipment_in.id)
|
|
elif self.lot_shipment_out:
|
|
return 'stock.shipment.out,' + str(self.lot_shipment_out.id)
|
|
elif self.lot_shipment_internal:
|
|
return 'stock.shipment.internal,' + str(self.lot_shipment_internal.id)
|
|
return None
|
|
|
|
@classmethod
|
|
def default_lot_av(cls):
|
|
return 'available'
|
|
|
|
@classmethod
|
|
def default_lot_visible(cls):
|
|
return True
|
|
|
|
@classmethod
|
|
def default_lot_status(cls):
|
|
return 'forecast'
|
|
|
|
def get_rec_name(self, name):
|
|
return self.id
|
|
|
|
def isVirtualP(self):
|
|
if self.lot_p:
|
|
Lot = Pool().get('lot.lot')
|
|
l = Lot(self.lot_p)
|
|
return (l.lot_type == 'virtual')
|
|
return False
|
|
|
|
def isVirtualS(self):
|
|
if self.lot_s:
|
|
Lot = Pool().get('lot.lot')
|
|
l = Lot(self.lot_s)
|
|
return (l.lot_type == 'virtual')
|
|
return False
|
|
|
|
@staticmethod
|
|
def _matching_line(lqt, side):
|
|
if side == 'purchase':
|
|
lot = getattr(lqt, 'lot_p', None)
|
|
return getattr(lot, 'line', None)
|
|
lot = getattr(lqt, 'lot_s', None)
|
|
return getattr(lot, 'sale_line', None)
|
|
|
|
@classmethod
|
|
def _matching_line_tolerance_percent(cls, line, field):
|
|
contract = getattr(line, 'purchase', None) or getattr(line, 'sale', None)
|
|
if getattr(line, 'inherit_tol', False) and contract:
|
|
return Decimal(str(getattr(contract, field, None) or 0))
|
|
return Decimal(str(getattr(line, field, None) or 0))
|
|
|
|
@classmethod
|
|
def _matching_line_tolerance_info(cls, line):
|
|
if not line:
|
|
return {}
|
|
theoretical = Decimal(str(
|
|
getattr(line, 'quantity_theorical', None)
|
|
or getattr(line, 'quantity', None)
|
|
or 0))
|
|
targeted = getattr(line, 'targeted_qt', None)
|
|
if targeted in (None, ''):
|
|
targeted = theoretical
|
|
tol_min = cls._matching_line_tolerance_percent(line, 'tol_min')
|
|
tol_max = cls._matching_line_tolerance_percent(line, 'tol_max')
|
|
return {
|
|
'lot_theoretical_qt': theoretical,
|
|
'lot_targeted_qt': Decimal(str(targeted or 0)),
|
|
'lot_tol_min': tol_min,
|
|
'lot_tol_max': tol_max,
|
|
'lot_tolerance_min': -tol_min,
|
|
'lot_tolerance_max': tol_max,
|
|
'lot_qt_min': round(
|
|
theoretical * (Decimal(1) - tol_min / Decimal(100)), 5),
|
|
'lot_qt_max': round(
|
|
theoretical * (Decimal(1) + tol_max / Decimal(100)), 5),
|
|
}
|
|
|
|
@classmethod
|
|
def _matching_quantity_in_line_unit(cls, lqt, line):
|
|
quantity = abs(Decimal(str(getattr(lqt, 'lot_quantity', 0) or 0)))
|
|
lot_unit = getattr(lqt, 'lot_unit', None)
|
|
line_unit = getattr(line, 'unit', None)
|
|
if not lot_unit or not line_unit or lot_unit == line_unit:
|
|
return quantity
|
|
Uom = Pool().get('product.uom')
|
|
factor = None
|
|
rate = None
|
|
if lot_unit.category.id != line_unit.category.id:
|
|
factor = 1
|
|
rate = 1
|
|
return Decimal(str(Uom.compute_qty(
|
|
lot_unit, float(quantity), line_unit, True, factor, rate)))
|
|
|
|
@classmethod
|
|
def _matching_tolerated_available(cls, lqt, side):
|
|
available = abs(Decimal(str(getattr(lqt, 'lot_quantity', 0) or 0)))
|
|
line = cls._matching_line(lqt, side)
|
|
if not line:
|
|
return available
|
|
info = cls._matching_line_tolerance_info(line)
|
|
qt_max = info.get('lot_qt_max')
|
|
if qt_max is None:
|
|
return available
|
|
domain = [('lot_s', '>', 0)]
|
|
if side == 'purchase':
|
|
domain.append(('lot_p', '=', getattr(lqt.lot_p, 'id', lqt.lot_p)))
|
|
else:
|
|
domain = [('lot_p', '>', 0),
|
|
('lot_s', '=', getattr(lqt.lot_s, 'id', lqt.lot_s))]
|
|
matched = sum(
|
|
cls._matching_quantity_in_line_unit(matched_lqt, line)
|
|
for matched_lqt in cls.search(domain))
|
|
tolerated_available = Decimal(str(qt_max)) - matched
|
|
return max(available, tolerated_available, Decimal(0))
|
|
|
|
@classmethod
|
|
def _matching_already_matched(cls, lqt, side):
|
|
line = cls._matching_line(lqt, side)
|
|
if not line:
|
|
return Decimal(0)
|
|
if side == 'purchase':
|
|
domain = [('lot_p', '=', getattr(lqt.lot_p, 'id', lqt.lot_p)),
|
|
('lot_s', '>', 0)]
|
|
else:
|
|
domain = [('lot_p', '>', 0),
|
|
('lot_s', '=', getattr(lqt.lot_s, 'id', lqt.lot_s))]
|
|
return sum(
|
|
cls._matching_quantity_in_line_unit(matched_lqt, line)
|
|
for matched_lqt in cls.search(domain))
|
|
|
|
@classmethod
|
|
def match_lots(cls,lot_p,lot_s):
|
|
logger.info("MATCH_LOTS:%s",lot_p)
|
|
logger.info("MATCH_LOTS:%s",lot_s)
|
|
LotQt = Pool().get('lot.qt')
|
|
Lot = Pool().get('lot.lot')
|
|
for lp in lot_p:
|
|
qt_p = Decimal(str(lp.lot_matched_qt or 0))
|
|
if qt_p <= 0:
|
|
continue
|
|
lqt = LotQt(lp.lot_r_id) if lp.lot_r_id else None
|
|
if lqt:
|
|
available = cls._matching_tolerated_available(lqt, 'purchase')
|
|
else:
|
|
lot = Lot(lp.lot_id)
|
|
available = Decimal(str(
|
|
lot.get_current_quantity_converted() or 0))
|
|
if qt_p > available:
|
|
raise UserError(
|
|
"Quantity to match exceeds the available purchase "
|
|
"quantity with tolerance.")
|
|
for ls in lot_s:
|
|
qt_s = Decimal(str(ls.lot_matched_qt or 0))
|
|
if qt_s <= 0:
|
|
continue
|
|
lqt = LotQt(ls.lot_r_id)
|
|
available = cls._matching_tolerated_available(lqt, 'sale')
|
|
if qt_s > available:
|
|
raise UserError(
|
|
"Quantity to match exceeds the available sale quantity "
|
|
"with tolerance.")
|
|
for lp in lot_p:
|
|
qt_p = lp.lot_matched_qt
|
|
if qt_p == 0:
|
|
continue
|
|
lqt = LotQt(lp.lot_r_id) if lp.lot_r_id else None
|
|
if lqt and (not lqt.lot_p or lqt.lot_s):
|
|
raise UserError(
|
|
"Please select only unmatched purchase quantities.")
|
|
for ls in lot_s:
|
|
if not ls.lot_matched_qt:
|
|
continue
|
|
l = Lot(lp.lot_id)
|
|
qt = Decimal(0)
|
|
if qt_p >= ls.lot_matched_qt:
|
|
qt = ls.lot_matched_qt
|
|
left_qt = qt_p - qt
|
|
else:
|
|
qt = qt_p
|
|
left_qt = 0
|
|
lqt2 =LotQt(ls.lot_r_id)
|
|
if lqt2.lot_p or not lqt2.lot_s:
|
|
raise UserError(
|
|
"Please select only unmatched sale quantities.")
|
|
if l.lot_type == 'physic':
|
|
ll = lqt2.lot_s
|
|
#Decrease forecasted virtual parts matched
|
|
vlot_p = l.getVlot_p()
|
|
vlot_s = ll.getVlot_s()
|
|
logger.info("SALEVIRTUALLOT:%s",vlot_p)
|
|
logger.info("SALEVIRTUALLOT2:%s",vlot_s)
|
|
#l.updateVirtualPart(-qt,l.lot_shipment_origin,l.getVlot_s())
|
|
#Increase forecasted virtual part non matched
|
|
#if not l.updateVirtualPart(qt,l.lot_shipment_origin,None):
|
|
# l.createVirtualPart(qt,l.lot_shipment_origin,None)
|
|
l.updateVirtualPart(-qt,None,ll,'only sale')
|
|
if lqt:
|
|
lqt.lot_s = ll
|
|
lqt.lot_av = 'reserved'
|
|
LotQt.save([lqt])
|
|
else:
|
|
l.createVirtualPart(qt, None, ll)
|
|
l.sale_line = ll.sale_line
|
|
Lot.save([l])
|
|
#ll.lot_gross_quantity -= qt
|
|
ll.lot_quantity = (
|
|
Decimal(str(ll.lot_quantity or 0)) - qt)
|
|
if ll.lot_qt is not None:
|
|
ll.lot_qt -= float(qt)
|
|
Lot.save([ll])
|
|
else:
|
|
#Increase forecasted virtual part matched
|
|
if not l.updateVirtualPart(qt,lqt.lot_shipment_origin,lqt2.lot_s):
|
|
l.createVirtualPart(qt,lqt.lot_shipment_origin,lqt2.lot_s)
|
|
#Decrease forecasted virtual part non matched
|
|
l.updateVirtualPart(-qt,lqt.lot_shipment_origin,None)
|
|
l.updateVirtualPart(-qt,None,lqt2.lot_s,'only sale')
|
|
if left_qt == 0:
|
|
break
|
|
qt_p = left_qt
|
|
|
|
@classmethod
|
|
def add_physical_lots(cls,lqt,vlots):
|
|
Purchase = Pool().get('purchase.purchase')
|
|
LotQt = Pool().get('lot.qt')
|
|
Lot = Pool().get('lot.lot')
|
|
Uom = Pool().get('product.uom')
|
|
purchase = lqt.lot_p.line.purchase
|
|
lots = []
|
|
tot_qt = 0
|
|
for l in vlots:
|
|
tot_qt += Decimal(str(Uom.compute_qty(
|
|
l.lot_unit_line, float(l.lot_quantity or 0),
|
|
lqt.lot_p.line.unit)))
|
|
tot_qt = round(tot_qt, 5)
|
|
cls._warn_physical_lot_tolerance(lqt, vlots)
|
|
with Lot.skip_quantity_consistency():
|
|
# Consume the forecast first. Physical lot saves recompute the
|
|
# virtual lot, so the matching lot.qt must already carry the
|
|
# target open quantity during the rest of the transaction.
|
|
lqt.lot_quantity -= tot_qt
|
|
if lqt.lot_quantity < 0:
|
|
lqt.lot_quantity = 0
|
|
LotQt.save([lqt])
|
|
|
|
for l in vlots:
|
|
lot,qt = lqt.add_physical_lot(l)
|
|
lots.append(lot)
|
|
|
|
#create move for lots
|
|
Purchase._process_shipment([purchase],lots)
|
|
affected_lines = []
|
|
if lqt.lot_p and lqt.lot_p.line:
|
|
affected_lines.append(lqt.lot_p.line)
|
|
if lqt.lot_s and lqt.lot_s.sale_line:
|
|
affected_lines.append(lqt.lot_s.sale_line)
|
|
Lot.assert_lines_quantity_consistency(affected_lines)
|
|
cls._sync_contract_tolerances_after_physical_lots(affected_lines)
|
|
|
|
if lots:
|
|
return lots[0].id
|
|
|
|
@classmethod
|
|
def _line_contract(cls, line):
|
|
return getattr(line, 'purchase', None) or getattr(line, 'sale', None)
|
|
|
|
@classmethod
|
|
def _line_model_name(cls, line):
|
|
if getattr(line, 'purchase', None):
|
|
return 'purchase.line'
|
|
if getattr(line, 'sale', None):
|
|
return 'sale.line'
|
|
|
|
@classmethod
|
|
def _line_theoretical_quantity(cls, line):
|
|
return Decimal(str(getattr(line, 'quantity_theorical', None) or 0))
|
|
|
|
@classmethod
|
|
def _line_tolerance_percent(cls, line, field):
|
|
contract = cls._line_contract(line)
|
|
if getattr(line, 'inherit_tol', False) and contract:
|
|
value = getattr(contract, field, None)
|
|
else:
|
|
value = getattr(line, field, None)
|
|
if value is None and contract:
|
|
value = getattr(contract, field, None)
|
|
return Decimal(str(value or 0))
|
|
|
|
@classmethod
|
|
def _line_physical_quantity(cls, line):
|
|
quantity = Decimal(0)
|
|
unit = getattr(line, 'unit', None)
|
|
for lot in getattr(line, 'lots', None) or []:
|
|
if getattr(lot, 'lot_type', None) == 'physic':
|
|
quantity += Decimal(str(
|
|
lot.get_current_quantity_converted(unit=unit) or 0))
|
|
return quantity
|
|
|
|
@classmethod
|
|
def _affected_lines_for_lqt(cls, lqt):
|
|
lines = []
|
|
if lqt.lot_p and lqt.lot_p.line:
|
|
lines.append(lqt.lot_p.line)
|
|
if lqt.lot_s and lqt.lot_s.sale_line:
|
|
lines.append(lqt.lot_s.sale_line)
|
|
return lines
|
|
|
|
@classmethod
|
|
def _added_quantity_for_line(cls, line, vlots):
|
|
Uom = Pool().get('product.uom')
|
|
quantity = Decimal(0)
|
|
for lot in vlots:
|
|
quantity += Decimal(str(Uom.compute_qty(
|
|
lot.lot_unit_line, float(lot.lot_quantity or 0), line.unit)))
|
|
return round(quantity, 5)
|
|
|
|
@classmethod
|
|
def _warn_physical_lot_tolerance(cls, lqt, vlots):
|
|
Warning = Pool().get('res.user.warning')
|
|
for line in cls._affected_lines_for_lqt(lqt):
|
|
theoretical = cls._line_theoretical_quantity(line)
|
|
if theoretical <= 0:
|
|
continue
|
|
tolerance = cls._line_tolerance_percent(line, 'tol_max')
|
|
max_quantity = theoretical * (Decimal(1) + tolerance / Decimal(100))
|
|
projected = (
|
|
cls._line_physical_quantity(line)
|
|
+ cls._added_quantity_for_line(line, vlots))
|
|
if projected <= max_quantity:
|
|
continue
|
|
warning_name = Warning.format(
|
|
"Physical lot tolerance exceeded",
|
|
[line, projected, max_quantity])
|
|
if Warning.check(warning_name):
|
|
over_percent = (
|
|
(projected - theoretical) / theoretical * Decimal(100))
|
|
side = 'purchase'
|
|
contract = getattr(line, 'purchase', None)
|
|
if getattr(line, 'sale', None):
|
|
side = 'sale'
|
|
contract = line.sale
|
|
contract_number = getattr(contract, 'number', None) or getattr(
|
|
contract, 'id', None)
|
|
raise LotToleranceWarning(
|
|
warning_name,
|
|
"The physical lot quantity exceeds the current tolerance "
|
|
"on the %s side (contract: %s, line: %s, projected "
|
|
"quantity: %s, maximum with current tolerance: %s, "
|
|
"projected tolerance: %s%%). "
|
|
"Do you want to continue and consume the contract "
|
|
"tolerance envelope?"
|
|
% (
|
|
side,
|
|
contract_number,
|
|
getattr(line, 'id', None),
|
|
round(projected, 5),
|
|
round(max_quantity, 5),
|
|
round(over_percent, 2),
|
|
))
|
|
|
|
@classmethod
|
|
def _contract_lines(cls, contract):
|
|
return [
|
|
line for line in getattr(contract, 'lines', None) or []
|
|
if getattr(line, 'type', 'line') == 'line']
|
|
|
|
@classmethod
|
|
def _sync_contract_tolerances_after_physical_lots(cls, lines):
|
|
contracts = {}
|
|
for line in lines:
|
|
contract = cls._line_contract(line)
|
|
model_name = cls._line_model_name(line)
|
|
if contract and model_name:
|
|
contracts[(model_name, contract.id)] = (model_name, contract)
|
|
|
|
for model_name, contract in contracts.values():
|
|
if model_name == 'purchase.line':
|
|
contract = Pool().get('purchase.purchase')(contract.id)
|
|
elif model_name == 'sale.line':
|
|
contract = Pool().get('sale.sale')(contract.id)
|
|
contract_lines = cls._contract_lines(contract)
|
|
cls._sync_contract_tolerance_lines(contract, contract_lines)
|
|
|
|
@classmethod
|
|
def _sync_contract_tolerance_lines(cls, contract, lines):
|
|
if not lines:
|
|
return
|
|
|
|
header_tol_max = Decimal(str(getattr(contract, 'tol_max', None) or 0))
|
|
header_tol_min = Decimal(str(getattr(contract, 'tol_min', None) or 0))
|
|
theoretical_by_line = {
|
|
line.id: cls._line_theoretical_quantity(line) for line in lines}
|
|
total_theoretical = sum(theoretical_by_line.values(), Decimal(0))
|
|
if total_theoretical <= 0:
|
|
return
|
|
|
|
total_allowed_over = (
|
|
total_theoretical * header_tol_max / Decimal(100))
|
|
over_by_line = {}
|
|
any_over_header = False
|
|
for line in lines:
|
|
theoretical = theoretical_by_line[line.id]
|
|
if theoretical <= 0:
|
|
over_by_line[line.id] = Decimal(0)
|
|
continue
|
|
physical = cls._line_physical_quantity(line)
|
|
over = max(physical - theoretical, Decimal(0))
|
|
over_by_line[line.id] = over
|
|
header_line_over = theoretical * header_tol_max / Decimal(100)
|
|
if over > header_line_over:
|
|
any_over_header = True
|
|
|
|
if not any_over_header:
|
|
return
|
|
|
|
line_models = {}
|
|
for line in lines:
|
|
model_name = cls._line_model_name(line)
|
|
if not model_name:
|
|
continue
|
|
theoretical = theoretical_by_line[line.id]
|
|
if theoretical <= 0:
|
|
continue
|
|
consumed_by_others = sum(
|
|
over for line_id, over in over_by_line.items()
|
|
if line_id != line.id)
|
|
remaining_for_line = max(
|
|
total_allowed_over - consumed_by_others, Decimal(0))
|
|
actual_over = over_by_line[line.id]
|
|
header_line_over = theoretical * header_tol_max / Decimal(100)
|
|
if actual_over > header_line_over:
|
|
allowed_over = actual_over
|
|
else:
|
|
allowed_over = remaining_for_line
|
|
allowed_percent = allowed_over / theoretical * Decimal(100)
|
|
line.inherit_tol = False
|
|
line.tol_min = header_tol_min
|
|
line.tol_max = round(allowed_percent, 5)
|
|
line.tol_min_qt = round(
|
|
theoretical * header_tol_min / Decimal(100), 5)
|
|
line.tol_max_qt = round(allowed_over, 5)
|
|
line_models.setdefault(model_name, []).append(line)
|
|
|
|
for model_name, model_lines in line_models.items():
|
|
if model_lines:
|
|
Pool().get(model_name).save(model_lines)
|
|
|
|
def add_physical_lot(self,l):
|
|
Lot = Pool().get('lot.lot')
|
|
FeeLots = Pool().get('fee.lots')
|
|
Purchase = Pool().get('purchase.purchase')
|
|
newlot = Lot()
|
|
newlot.line = self.lot_p.line
|
|
newlot.lot_shipment_in = self.lot_shipment_in
|
|
newlot.lot_shipment_internal = self.lot_shipment_internal
|
|
newlot.lot_shipment_out = self.lot_shipment_out
|
|
newlot.lot_product = self.lot_p.line.product
|
|
newlot.lot_chunk_key = getattr(l, 'lot_chunk_key', None)
|
|
if self.lot_s:
|
|
newlot.sale_line = self.lot_s.sale_line if self.lot_s.sale_line else None
|
|
newlot.lot_type = 'physic'
|
|
newlot.lot_qt = l.lot_qt
|
|
newlot.lot_unit = l.lot_unit
|
|
newlot.lot_unit_line = l.lot_unit_line
|
|
newlot.lot_premium = getattr(l, 'lot_premium', None) or Decimal(0)
|
|
if newlot.lot_shipment_in or newlot.lot_shipment_internal or newlot.lot_shipment_out:
|
|
newlot.lot_status = 'transit'
|
|
newlot.set_current_quantity(l.lot_quantity,l.lot_gross_quantity,1)
|
|
if (newlot.lot_shipment_in or newlot.lot_shipment_internal or newlot.lot_shipment_out):
|
|
newlot.set_current_quantity(l.lot_quantity,l.lot_gross_quantity,2)
|
|
Lot.save([newlot])
|
|
#add lot to services
|
|
if newlot.line.fees:
|
|
for f in newlot.line.fees:
|
|
exist = FeeLots.search([('fee','=',f.id),('lot','=',newlot.id)])
|
|
if not exist:
|
|
fl =FeeLots()
|
|
fl.fee = f
|
|
fl.lot = newlot
|
|
fl.line = newlot.line
|
|
FeeLots.save([fl])
|
|
#update ordered fee with physical lot quantity
|
|
f.sync_quantity_from_lots()
|
|
f.adjust_purchase_values()
|
|
|
|
if newlot.sale_line and newlot.sale_line.fees:
|
|
for f in newlot.sale_line.fees:
|
|
exist = FeeLots.search([('fee','=',f.id),('lot','=',newlot.id)])
|
|
if not exist:
|
|
fl =FeeLots()
|
|
fl.fee = f
|
|
fl.lot = newlot
|
|
fl.sale_line = newlot.sale_line
|
|
FeeLots.save([fl])
|
|
#update ordered fee with physical lot quantity
|
|
f.sync_quantity_from_lots()
|
|
f.adjust_purchase_values()
|
|
|
|
if newlot.lot_shipment_in and newlot.lot_shipment_in.fees:
|
|
for f in newlot.lot_shipment_in.fees:
|
|
exist = FeeLots.search([('fee','=',f.id),('lot','=',newlot.id)])
|
|
if not exist:
|
|
fl =FeeLots()
|
|
fl.fee = f
|
|
fl.lot = newlot
|
|
FeeLots.save([fl])
|
|
#update ordered fee with physical lot quantity
|
|
f.sync_quantity_from_lots()
|
|
f.adjust_purchase_values()
|
|
|
|
return newlot, newlot.get_current_quantity_converted()
|
|
|
|
def getMove(self,sh):
|
|
if sh:
|
|
if self.lot_p:
|
|
Lot = Pool().get('lot.lot')
|
|
LotQt = Pool().get('lot.qt')
|
|
lp = Lot(self.lot_p)
|
|
vlot_p = lp.getVlot_p()
|
|
if 'stock.shipment.in' in sh:
|
|
lqts = LotQt.search([('lot_p','=',vlot_p),('lot_shipment_in','=',sh),('lot_move','!=',None)])
|
|
elif 'stock.shipment.internal' in sh:
|
|
lqts = LotQt.search([('lot_p','=',vlot_p),('lot_shipment_internal','=',sh),('lot_move','!=',None)])
|
|
elif 'stock.shipment.out' in sh:
|
|
lqts = LotQt.search([('lot_p','=',vlot_p),('lot_shipment_out','=',sh),('lot_move','!=',None)])
|
|
if lqts:
|
|
return lqts[0].lot_move
|
|
else:
|
|
return lp.createMove(self,self.lot_quantity,sh)
|
|
|
|
@classmethod
|
|
def updateMove(cls,move):
|
|
if move:
|
|
Move = Pool().get('stock.move')
|
|
move = Move(move)
|
|
if move:
|
|
move.quantity = round(sum([(e.lot_quantity if e.lot_quantity else 0) for e in move.lotqt]),5)
|
|
Move.save([move])
|
|
|
|
@classmethod
|
|
def validate(cls, lotqts):
|
|
super(LotQt, cls).validate(lotqts)
|
|
Date = Pool().get('ir.date')
|
|
#Update Move
|
|
for lqt in lotqts:
|
|
if (not lqt.lot_p and not lqt.lot_s
|
|
and Decimal(str(lqt.lot_quantity or 0)) != 0):
|
|
raise UserError(
|
|
"Lot quantity forecast must be linked to a purchase "
|
|
"virtual lot or a sale virtual lot.")
|
|
cls.updateMove(lqt.lot_move)
|
|
#generate valuation for purchase and sale
|
|
logger.info("VALIDATE_LQT_LS:%s",lqt.lot_s)
|
|
logger.info("VALIDATE_LQT_LP:%s",lqt.lot_p)
|
|
if lqt.lot_p and lqt.lot_quantity > 0:
|
|
pl = lqt.lot_p.line
|
|
logger.info("VALIDATE_LQT_PL:%s",pl)
|
|
# Pnl = Pool().get('valuation.valuation')
|
|
# pnl = Pnl.search([('line','=',pl.id),('date','=',Date.today())])
|
|
# if pnl:
|
|
# Pnl.delete(pnl)
|
|
# pnl_lines = []
|
|
# pnl_lines.extend(pl.get_pnl_fee_lines())
|
|
# pnl_lines.extend(pl.get_pnl_price_lines())
|
|
# pnl_lines.extend(pl.get_pnl_der_lines())
|
|
# Pnl.save(pnl_lines)
|
|
|
|
#Open position update
|
|
# if pl.quantity_theorical:
|
|
# OpenPosition = Pool().get('open.position')
|
|
# OpenPosition.create_from_purchase_line(pl)
|
|
|
|
@classmethod
|
|
def getQuery(cls, purchase=None, sale=None, shipment=None, type=None,
|
|
state=None, qttype=None, supplier=None, client=None, ps=None,
|
|
lot_status=None, group=None, product=None, location=None,
|
|
origin=None, finished=False, shipping_status=None, asof=None,
|
|
todate=None, dimension=None, strategy=None):
|
|
pool = Pool()
|
|
LotQt = pool.get('lot.qt')
|
|
lqt = LotQt.__table__()
|
|
LotQtHis = pool.get('lot.qt.hist')
|
|
lqh = LotQtHis.__table__()
|
|
LotP = pool.get('lot.lot')
|
|
lp = LotP.__table__()
|
|
LotS = pool.get('lot.lot')
|
|
ls = LotS.__table__()
|
|
Purchase = pool.get('purchase.purchase')
|
|
pu = Purchase.__table__()
|
|
PurchaseLine = pool.get('purchase.line')
|
|
pl = PurchaseLine.__table__()
|
|
Sale = pool.get('sale.sale')
|
|
sa = Sale.__table__()
|
|
SaleLine = pool.get('sale.line')
|
|
sl = SaleLine.__table__()
|
|
ShipmentIn = pool.get('stock.shipment.in')
|
|
si = ShipmentIn.__table__()
|
|
ShipmentInternal = pool.get('stock.shipment.internal')
|
|
sin = ShipmentInternal.__table__()
|
|
Prov = pool.get('account.invoice')
|
|
prov = Prov.__table__()
|
|
ProvLine = pool.get('account.invoice.line')
|
|
prov_line = ProvLine.__table__()
|
|
Final = pool.get('account.invoice')
|
|
final = Final.__table__()
|
|
FinalLine = pool.get('account.invoice.line')
|
|
final_line = FinalLine.__table__()
|
|
Prov_s = pool.get('account.invoice')
|
|
prov_s = Prov_s.__table__()
|
|
ProvLine_s = pool.get('account.invoice.line')
|
|
prov_line_s = ProvLine_s.__table__()
|
|
Final_s = pool.get('account.invoice')
|
|
final_s = Final_s.__table__()
|
|
FinalLine_s = pool.get('account.invoice.line')
|
|
final_line_s = FinalLine_s.__table__()
|
|
ProductUom = pool.get('product.uom')
|
|
puom = ProductUom.__table__()
|
|
ProductUomLine = pool.get('product.uom')
|
|
puoml = ProductUomLine.__table__()
|
|
PurchaseStrategy = pool.get('purchase.strategy')
|
|
pstr = PurchaseStrategy.__table__()
|
|
SaleStrategy = pool.get('sale.strategy')
|
|
sstr = SaleStrategy.__table__()
|
|
AnalyticDimensionAssignment = pool.get('analytic.dimension.assignment')
|
|
ada = AnalyticDimensionAssignment.__table__()
|
|
type = type or 'all'
|
|
state = state or 'all'
|
|
lot_status = lot_status or 'all'
|
|
ps = ps or 'all'
|
|
shipping_status = shipping_status or 'all'
|
|
|
|
def date_filter(purchase_date, sale_date):
|
|
if not asof and not todate:
|
|
return None
|
|
purchase_clause = Literal(True)
|
|
sale_clause = Literal(True)
|
|
if asof:
|
|
purchase_clause &= (purchase_date >= asof)
|
|
sale_clause &= (sale_date >= asof)
|
|
if todate:
|
|
purchase_clause &= (purchase_date <= todate)
|
|
sale_clause &= (sale_date <= todate)
|
|
if ps == 'P':
|
|
return purchase_clause
|
|
elif ps == 'S':
|
|
return sale_clause
|
|
return purchase_clause | sale_clause
|
|
|
|
def dimension_filter(purchase_id, sale_id):
|
|
if not dimension:
|
|
return None
|
|
purchase_dimension = Exists(ada.select(
|
|
ada.id,
|
|
where=((ada.purchase == purchase_id)
|
|
& (ada.value == dimension))))
|
|
sale_dimension = Exists(ada.select(
|
|
ada.id,
|
|
where=((ada.sale == sale_id) & (ada.value == dimension))))
|
|
if ps == 'P':
|
|
return purchase_dimension
|
|
elif ps == 'S':
|
|
return sale_dimension
|
|
return purchase_dimension | sale_dimension
|
|
|
|
wh = ((lqt.lot_quantity > 0) & ((lp.lot_type == 'virtual') | (ls.lot_type == 'virtual')))
|
|
|
|
#wh &= (((lqt.create_date >= asof) & ((lqt.create_date-datetime.timedelta(1)) <= todate)))
|
|
if ps == 'P':
|
|
wh &= (lqt.lot_p != None)
|
|
elif ps == 'S':
|
|
wh &= (lqt.lot_s != None)
|
|
if not finished:
|
|
wh &= (
|
|
((lqt.lot_p == None) | (pl.finished == False))
|
|
& ((lqt.lot_s == None) | (sl.finished == False)))
|
|
if purchase:
|
|
wh &= (pu.id == purchase)
|
|
if sale:
|
|
wh &= (sa.id == sale)
|
|
if state != 'all':
|
|
wh &= (lqt.lot_av == state)
|
|
if shipment:
|
|
wh &= (lqt.lot_shipment_in == shipment)
|
|
if product:
|
|
wh &= (lqt.lot_product == product)
|
|
if location:
|
|
wh &= (Case((lqt.lot_shipment_in > 0, si.to_location),else_=Case((lqt.lot_shipment_internal > 0, sin.to_location),else_=pu.to_location)) == location)
|
|
date_clause = date_filter(pu.purchase_date, sa.sale_date)
|
|
if date_clause is not None:
|
|
wh &= date_clause
|
|
dimension_clause = dimension_filter(pu.id, sa.id)
|
|
if dimension_clause is not None:
|
|
wh &= dimension_clause
|
|
if strategy:
|
|
purchase_strategy = Exists(pstr.select(
|
|
pstr.id,
|
|
where=((pstr.line == pl.id) & (pstr.strategy == strategy))))
|
|
sale_strategy = Exists(sstr.select(
|
|
sstr.id,
|
|
where=((sstr.sale_line == sl.id)
|
|
& (sstr.strategy == strategy))))
|
|
if ps == 'P':
|
|
wh &= purchase_strategy
|
|
elif ps == 'S':
|
|
wh &= sale_strategy
|
|
else:
|
|
wh &= (purchase_strategy | sale_strategy)
|
|
if type=='matched':
|
|
wh &= ((ls.sale_line > 0) & (lp.line > 0))
|
|
elif type=='not matched':
|
|
wh &= (((ls.sale_line > 0) & (lp.line == None)) | ((ls.sale_line == None) & (lp.line > 0)))
|
|
if shipping_status == 'unshipped':
|
|
wh &= (lqt.lot_shipment_in == None)
|
|
elif shipping_status == 'scheduled':
|
|
wh &= ((lqt.lot_shipment_in != None) & (si.state == 'draft'))
|
|
elif shipping_status == 'shipped':
|
|
wh &= ((lqt.lot_shipment_in != None) & (si.state == 'started'))
|
|
elif shipping_status == 'received':
|
|
wh &= ((lqt.lot_shipment_in != None)
|
|
& (si.state.in_(['received', 'done'])))
|
|
if qttype=='virtual':
|
|
wh &= ((lp.lot_type == 'virtual') | (ls.lot_type == 'virtual'))
|
|
elif qttype=='physic':
|
|
wh &= ((lp.lot_type == 'physic') | (ls.lot_type == 'physic'))
|
|
if lot_status:
|
|
if lot_status == 'forecast':
|
|
wh &= (lqt.lot_status == 'forecast')
|
|
elif lot_status == 'loading':
|
|
wh &= (lqt.lot_status == 'loading')
|
|
elif lot_status == 'destination':
|
|
wh &= (lqt.lot_status == 'destination')
|
|
elif lot_status == 'stock':
|
|
wh &= (lqt.lot_status == 'stock')
|
|
elif lot_status == 'delivered':
|
|
wh &= (lqt.lot_status == 'delivered')
|
|
if supplier and pu:
|
|
wh &= (pu.party == supplier)
|
|
if client and sa:
|
|
wh &= (sa.party == client)
|
|
|
|
AvQt = Case(((lqt.lot_p>0) & (lqt.lot_s>0), 0),else_=Case((lqt.lot_p>0,lqt.lot_quantity),else_=-lqt.lot_quantity))
|
|
MaQt = Case(((lqt.lot_p>0) & (lqt.lot_s>0), lqt.lot_quantity),else_=0)
|
|
ShippingStatus = Case(
|
|
(lqt.lot_shipment_in == None, 'unshipped'),
|
|
else_=Case(
|
|
(si.state == 'draft', 'scheduled'),
|
|
else_=Case(
|
|
(si.state == 'started', 'shipped'),
|
|
else_=Case(
|
|
(si.state.in_(['received', 'done']), 'received'),
|
|
else_=None))))
|
|
query = (
|
|
lqt.join(lp,'LEFT',condition=lqt.lot_p == lp.id)
|
|
.join(pl,'LEFT',condition=lp.line == pl.id)
|
|
.join(pu,'LEFT', condition=pl.purchase == pu.id)
|
|
.join(ls,'LEFT',condition=lqt.lot_s == ls.id)
|
|
.join(sl,'LEFT',condition=ls.sale_line == sl.id)
|
|
.join(sa,'LEFT', condition=sl.sale == sa.id)
|
|
.join(si,'LEFT',condition=lqt.lot_shipment_in == si.id)
|
|
.join(sin,'LEFT',condition=lqt.lot_shipment_internal == sin.id)
|
|
.select(
|
|
Literal(0).as_('create_uid'),
|
|
CurrentTimestamp().as_('create_date'),
|
|
Literal(None).as_('write_uid'),
|
|
Literal(None).as_('write_date'),
|
|
(lqt.id + 10000000).as_('id'),
|
|
lqt.lot_p.as_('r_lot_p'),
|
|
lqt.lot_s.as_('r_lot_s'),
|
|
Case((lp.id>0, lp.lot_name),else_=ls.lot_name).as_('r_lot_name'),
|
|
Case((lp.id>0, lp.number),else_=ls.number).as_('r_number'),
|
|
Case((lp.id>0, lp.lot_qt),else_=ls.lot_qt).as_('r_lot_qt'),
|
|
Case((lp.id>0, lp.lot_unit),else_=ls.lot_unit).as_('r_lot_unit'),
|
|
AvQt.as_('r_lot_quantity'),
|
|
MaQt.as_('r_lot_matched'),
|
|
Case((lp.id>0, lp.lot_product),else_=ls.lot_product).as_('r_lot_product'),
|
|
Case((lp.id>0, lp.lot_type),else_=ls.lot_type).as_('r_lot_type'),
|
|
lqt.lot_shipment_in.as_('r_lot_shipment_in'),
|
|
lqt.lot_shipment_out.as_('r_lot_shipment_out'),
|
|
lqt.lot_shipment_internal.as_('r_lot_shipment_internal'),
|
|
si.vessel.as_('r_vessel'),
|
|
si.operator.as_('r_operator'),
|
|
lqt.lot_move.as_('r_lot_move'),
|
|
lqt.lot_status.as_('r_lot_status'),
|
|
Case((lp.lot_type=='physic',lp.lot_av),else_=lqt.lot_av).as_('r_lot_av'),
|
|
Case((lp.id>0, lp.lot_premium),else_=ls.lot_premium).as_('r_lot_premium'),
|
|
Case((lp.id>0, lp.lot_premium_sale),else_=ls.lot_premium_sale).as_('r_lot_premium_sale'),
|
|
Case((lp.id>0, lp.lot_parent),else_=ls.lot_parent).as_('r_lot_parent'),
|
|
Case((lp.id>0, lp.lot_himself),else_=ls.lot_himself).as_('r_lot_himself'),
|
|
Case((lp.id>0, lp.lot_container),else_=ls.lot_container).as_('r_lot_container'),
|
|
Case((lp.id>0, lp.line),else_=None).as_('r_line'),
|
|
Case((pl.id>0, pl.del_period), else_=None).as_('r_del_period'),
|
|
Case((sl.id>0, sl.del_period), else_=None).as_('r_sale_del_period'),
|
|
Case((pu.id>0, pu.id),else_=None).as_('r_purchase'),
|
|
Case((sa.id>0, sa.id),else_=None).as_('r_sale'),
|
|
Case((ls.id>0, ls.sale_line),else_=None).as_('r_sale_line'),
|
|
(MaQt + AvQt).as_('r_tot'),
|
|
pu.party.as_('r_supplier'),
|
|
sa.party.as_('r_client'),
|
|
Case((lqt.lot_shipment_in > 0, si.from_location),else_=Case((lqt.lot_shipment_internal > 0, sin.from_location),else_=pu.from_location)).as_('r_from_l'),
|
|
Case((lqt.lot_shipment_in > 0, si.to_location),else_=Case((lqt.lot_shipment_internal > 0, sin.to_location),else_=pu.to_location)).as_('r_from_t'),
|
|
Literal(None).as_('r_lot_pur_inv_state'),
|
|
Literal(None).as_('r_lot_sale_inv_state'),
|
|
Literal(None).as_('r_lot_pur_inv'),
|
|
Literal(None).as_('r_lot_sal_inv'),
|
|
Literal(None).as_('r_lot_state'),
|
|
ShippingStatus.as_('r_shipping_status'),
|
|
where=wh
|
|
)
|
|
)
|
|
|
|
wh2 = (lp.lot_type == 'physic')
|
|
if ps == 'P':
|
|
wh2 &= (lp.line != None)
|
|
elif ps == 'S':
|
|
wh2 &= (lp.sale_line != None)
|
|
if purchase:
|
|
wh2 &= (pu.id == purchase)
|
|
if sale:
|
|
wh2 &= (sa.id == sale)
|
|
if state != 'all':
|
|
wh2 &= (lp.lot_av == state)
|
|
if shipment:
|
|
wh2 &= (lp.lot_shipment_in == shipment)
|
|
if product:
|
|
wh2 &= (lp.lot_product == product)
|
|
if location:
|
|
wh2 &= (Case((lp.lot_shipment_in > 0, si.to_location), else_=Case((lp.lot_shipment_internal > 0, sin.to_location), else_=pu.to_location)) == location)
|
|
date_clause = date_filter(pu.purchase_date, sa.sale_date)
|
|
if date_clause is not None:
|
|
wh2 &= date_clause
|
|
dimension_clause = dimension_filter(pu.id, sa.id)
|
|
if dimension_clause is not None:
|
|
wh2 &= dimension_clause
|
|
if strategy:
|
|
purchase_strategy = Exists(pstr.select(
|
|
pstr.id,
|
|
where=((pstr.line == pl.id) & (pstr.strategy == strategy))))
|
|
sale_strategy = Exists(sstr.select(
|
|
sstr.id,
|
|
where=((sstr.sale_line == sl.id)
|
|
& (sstr.strategy == strategy))))
|
|
if ps == 'P':
|
|
wh2 &= purchase_strategy
|
|
elif ps == 'S':
|
|
wh2 &= sale_strategy
|
|
else:
|
|
wh2 &= (purchase_strategy | sale_strategy)
|
|
if type=='matched':
|
|
wh2 &= (lp.sale_line != None)
|
|
elif type=='not matched':
|
|
wh2 &= (lp.sale_line == None)
|
|
if shipping_status == 'unshipped':
|
|
wh2 &= (lp.lot_shipment_in == None)
|
|
elif shipping_status == 'scheduled':
|
|
wh2 &= ((lp.lot_shipment_in != None) & (si.state == 'draft'))
|
|
elif shipping_status == 'shipped':
|
|
wh2 &= ((lp.lot_shipment_in != None) & (si.state == 'started'))
|
|
elif shipping_status == 'received':
|
|
wh2 &= ((lp.lot_shipment_in != None)
|
|
& (si.state.in_(['received', 'done'])))
|
|
if qttype=='virtual':
|
|
wh2 &= False
|
|
if lot_status:
|
|
if lot_status == 'forecast':
|
|
wh2 &= (lp.lot_status == 'forecast')
|
|
elif lot_status == 'loading':
|
|
wh2 &= (lp.lot_status == 'loading')
|
|
elif lot_status == 'destination':
|
|
wh2 &= (lp.lot_status == 'destination')
|
|
elif lot_status == 'stock':
|
|
wh2 &= (lp.lot_status == 'stock')
|
|
elif lot_status == 'delivered':
|
|
wh2 &= (lp.lot_status == 'delivered')
|
|
if supplier and pu:
|
|
wh2 &= (pu.party == supplier)
|
|
if client and sa:
|
|
wh2 &= (sa.party == client)
|
|
|
|
AvQt2 = Case((lp.sale_line>0, 0),else_=lqh.quantity * (puom.factor / puoml.factor))
|
|
MaQt2 = Case((lp.sale_line>0, lqh.quantity * (puom.factor / puoml.factor)),else_=0)
|
|
ShippingStatus2 = Case(
|
|
(lp.lot_shipment_in == None, 'unshipped'),
|
|
else_=Case(
|
|
(si.state == 'draft', 'scheduled'),
|
|
else_=Case(
|
|
(si.state == 'started', 'shipped'),
|
|
else_=Case(
|
|
(si.state.in_(['received', 'done']), 'received'),
|
|
else_=None))))
|
|
|
|
query2 = (
|
|
lp.join(lqh, "LEFT", condition=(lp.id == lqh.lot) & (lp.lot_state == lqh.quantity_type))
|
|
.join(pl, "INNER", condition=lp.line == pl.id)
|
|
.join(pu, "INNER", condition=pl.purchase == pu.id)
|
|
.join(sl, "LEFT", condition=lp.sale_line == sl.id)
|
|
.join(sa, "LEFT", condition=sl.sale == sa.id)
|
|
.join(prov_line, "LEFT", condition=lp.invoice_line_prov == prov_line.id)
|
|
.join(prov, "LEFT", condition=prov_line.invoice == prov.id)
|
|
.join(prov_line_s, "LEFT", condition=lp.sale_invoice_line_prov == prov_line_s.id)
|
|
.join(prov_s, "LEFT", condition=prov_line_s.invoice == prov_s.id)
|
|
.join(final_line, "LEFT", condition=lp.invoice_line == final_line.id)
|
|
.join(final, "LEFT", condition=final_line.invoice == final.id)
|
|
.join(final_line_s, "LEFT", condition=lp.sale_invoice_line == final_line_s.id)
|
|
.join(final_s, "LEFT", condition=final_line_s.invoice == final_s.id)
|
|
.join(si,'LEFT',condition=lp.lot_shipment_in == si.id)
|
|
.join(sin,'LEFT',condition=lp.lot_shipment_internal == sin.id)
|
|
.join(puom,'LEFT',condition=lp.lot_unit_line == puom.id)
|
|
.join(puoml,'LEFT',condition=pl.unit == puoml.id)
|
|
.select(
|
|
Literal(0).as_("create_uid"),
|
|
CurrentTimestamp().as_("create_date"),
|
|
Literal(None).as_("write_uid"),
|
|
Literal(None).as_("write_date"),
|
|
(lp.id).as_("id"),
|
|
lp.id.as_("r_lot_p"),
|
|
Literal(None).as_("r_lot_s"),
|
|
lp.lot_name.as_("r_lot_name"),
|
|
lp.number.as_("r_number"),
|
|
lp.lot_qt.as_("r_lot_qt"),
|
|
lp.lot_unit.as_("r_lot_unit"),
|
|
AvQt2.as_("r_lot_quantity"),
|
|
MaQt2.as_("r_lot_matched"),
|
|
lp.lot_product.as_("r_lot_product"),
|
|
lp.lot_type.as_("r_lot_type"),
|
|
lp.lot_shipment_in.as_("r_lot_shipment_in"),
|
|
lp.lot_shipment_out.as_('r_lot_shipment_out'),
|
|
lp.lot_shipment_internal.as_('r_lot_shipment_internal'),
|
|
si.vessel.as_("r_vessel"),
|
|
si.operator.as_("r_operator"),
|
|
lp.move.as_("r_lot_move"),
|
|
lp.lot_status.as_("r_lot_status"),
|
|
lp.lot_av.as_("r_lot_av"),
|
|
lp.lot_premium.as_("r_lot_premium"),
|
|
lp.lot_premium_sale.as_("r_lot_premium_sale"),
|
|
lp.lot_parent.as_("r_lot_parent"),
|
|
lp.lot_himself.as_("r_lot_himself"),
|
|
lp.lot_container.as_("r_lot_container"),
|
|
lp.line.as_("r_line"),
|
|
pl.del_period.as_("r_del_period"),
|
|
sl.del_period.as_("r_sale_del_period"),
|
|
Case((pu.id > 0, pu.id), else_=None).as_("r_purchase"),
|
|
Case((sa.id > 0, sa.id), else_=None).as_("r_sale"),
|
|
lp.sale_line.as_("r_sale_line"),
|
|
(MaQt2 + Abs(AvQt2)).as_("r_tot"),
|
|
pu.party.as_("r_supplier"),
|
|
sa.party.as_("r_client"),
|
|
Case((lp.lot_shipment_in > 0, si.from_location), else_=Case((lp.lot_shipment_internal > 0, sin.from_location), else_=pu.from_location)).as_("r_from_l"),
|
|
Case((lp.lot_shipment_in > 0, si.to_location), else_=Case((lp.lot_shipment_internal > 0, sin.to_location), else_=pu.to_location)).as_("r_from_t"),
|
|
lp.lot_pur_inv_state.as_('r_lot_pur_inv_state'),
|
|
lp.lot_sale_inv_state.as_('r_lot_sale_inv_state'),
|
|
Case((final.id > 0, final.id), else_=prov.id).as_('r_lot_pur_inv'),
|
|
Case((final_s.id > 0, final_s.id), else_=prov_s.id).as_('r_lot_sal_inv'),
|
|
lp.lot_state.as_('r_lot_state'),
|
|
ShippingStatus2.as_('r_shipping_status'),
|
|
where=wh2,
|
|
)
|
|
)
|
|
|
|
if group=='by_physic':
|
|
ship_group = Case((lp.lot_shipment_in>0, lp.lot_shipment_in),else_=Case((lp.lot_shipment_internal>0, lp.lot_shipment_internal),else_=Case((lp.lot_shipment_out>0, lp.lot_shipment_out),else_=None)))
|
|
query2 = (
|
|
lp.join(lqh, "LEFT", condition=(lp.id == lqh.lot) & (lp.lot_state == lqh.quantity_type))
|
|
.join(pl, "INNER", condition=lp.line == pl.id)
|
|
.join(pu, "INNER", condition=pl.purchase == pu.id)
|
|
.join(sl, "LEFT", condition=lp.sale_line == sl.id)
|
|
.join(sa, "LEFT", condition=sl.sale == sa.id)
|
|
.join(prov_line, "LEFT", condition=lp.invoice_line_prov == prov_line.id)
|
|
.join(prov, "LEFT", condition=prov_line.invoice == prov.id)
|
|
.join(prov_line_s, "LEFT", condition=lp.sale_invoice_line_prov == prov_line_s.id)
|
|
.join(prov_s, "LEFT", condition=prov_line_s.invoice == prov_s.id)
|
|
.join(final_line, "LEFT", condition=lp.invoice_line == final_line.id)
|
|
.join(final, "LEFT", condition=final_line.invoice == final.id)
|
|
.join(final_line_s, "LEFT", condition=lp.sale_invoice_line == final_line_s.id)
|
|
.join(final_s, "LEFT", condition=final_line_s.invoice == final_s.id)
|
|
.join(si,'LEFT',condition=lp.lot_shipment_in == si.id)
|
|
.join(sin,'LEFT',condition=lp.lot_shipment_internal == sin.id)
|
|
.join(puom,'LEFT',condition=lp.lot_unit_line == puom.id)
|
|
.join(puoml,'LEFT',condition=pl.unit == puoml.id)
|
|
.select(
|
|
Literal(0).as_("create_uid"),
|
|
CurrentTimestamp().as_("create_date"),
|
|
Literal(None).as_("write_uid"),
|
|
Literal(None).as_("write_date"),
|
|
Max(lp.id).as_("id"),
|
|
Max(lp.id).as_("r_lot_p"),
|
|
Literal(None).as_("r_lot_s"),
|
|
Max(lp.lot_name).as_("r_lot_name"),
|
|
Max(lp.number).as_("r_number"),
|
|
Sum(lp.lot_qt).as_("r_lot_qt"),
|
|
Max(lp.lot_unit).as_("r_lot_unit"),
|
|
Sum(AvQt2).as_("r_lot_quantity"),
|
|
Sum(MaQt2).as_("r_lot_matched"),
|
|
Max(lp.lot_product).as_("r_lot_product"),
|
|
Max(lp.lot_type).as_("r_lot_type"),
|
|
lp.lot_shipment_in.as_("r_lot_shipment_in"),
|
|
lp.lot_shipment_out.as_('r_lot_shipment_out'),
|
|
lp.lot_shipment_internal.as_('r_lot_shipment_internal'),
|
|
Max(si.vessel).as_("r_vessel"),
|
|
Max(si.operator).as_("r_operator"),
|
|
Max(lp.move).as_("r_lot_move"),
|
|
Max(lp.lot_status).as_("r_lot_status"),
|
|
Max(lp.lot_av).as_("r_lot_av"),
|
|
Avg(lp.lot_premium).as_("r_lot_premium"),
|
|
Literal(None).as_("r_lot_premium_sale"),
|
|
Literal(None).as_("r_lot_parent"),
|
|
Literal(None).as_("r_lot_himself"),
|
|
Max(lp.lot_container).as_("r_lot_container"),
|
|
lp.line.as_("r_line"),
|
|
Max(pl.del_period).as_("r_del_period"),
|
|
Max(sl.del_period).as_("r_sale_del_period"),
|
|
Max(Case((pu.id > 0, pu.id), else_=None)).as_("r_purchase"),
|
|
Max(Case((sa.id > 0, sa.id), else_=None)).as_("r_sale"),
|
|
lp.sale_line.as_("r_sale_line"),
|
|
Sum(MaQt2 + Abs(AvQt2)).as_("r_tot"),
|
|
Max(pu.party).as_("r_supplier"),
|
|
Max(sa.party).as_("r_client"),
|
|
Max(Case((lp.lot_shipment_in > 0, si.from_location), else_=Case((lp.lot_shipment_internal > 0, si.from_location), else_=pu.from_location))).as_("r_from_l"),
|
|
Max(Case((lp.lot_shipment_in > 0, si.to_location), else_=Case((lp.lot_shipment_internal > 0, si.to_location), else_=pu.to_location))).as_("r_from_t"),
|
|
Max(lp.lot_pur_inv_state).as_('r_lot_pur_inv_state'),
|
|
Max(lp.lot_sale_inv_state).as_('r_lot_sale_inv_state'),
|
|
Max(Case((final.id > 0, final.id), else_=prov.id)).as_('r_lot_pur_inv'),
|
|
Max(Case((final_s.id > 0, final_s.id), else_=prov_s.id)).as_('r_lot_sal_inv'),
|
|
Max(lp.lot_state).as_('r_lot_state'),
|
|
Max(ShippingStatus2).as_('r_shipping_status'),
|
|
where=wh2,
|
|
group_by=[lp.line,lp.lot_shipment_in,lp.lot_shipment_internal,lp.lot_shipment_out,lp.sale_line]
|
|
)
|
|
)
|
|
|
|
if origin == 'physic':
|
|
union = Union(query2,query2,all_=False)
|
|
elif origin == 'open':
|
|
union = Union(query,query,all_=False)
|
|
else:
|
|
union = Union(query,query2,all_=True)
|
|
|
|
ordered_union = union.select(
|
|
Literal(0).as_("create_uid"),
|
|
CurrentTimestamp().as_("create_date"),
|
|
Literal(None).as_("write_uid"),
|
|
Literal(None).as_("write_date"),
|
|
union.id.as_("id"),
|
|
union.r_lot_p.as_("r_lot_p"),
|
|
Literal(None).as_("r_lot_s"),
|
|
union.r_lot_name.as_("r_lot_name"),
|
|
union.r_number.as_("r_number"),
|
|
union.r_lot_qt.as_("r_lot_qt"),
|
|
union.r_lot_unit.as_("r_lot_unit"),
|
|
union.r_lot_quantity.as_("r_lot_quantity"),
|
|
union.r_lot_matched.as_("r_lot_matched"),
|
|
union.r_lot_product.as_("r_lot_product"),
|
|
union.r_lot_type.as_("r_lot_type"),
|
|
union.r_lot_shipment_in.as_("r_lot_shipment_in"),
|
|
union.r_lot_shipment_out.as_('r_lot_shipment_out'),
|
|
union.r_lot_shipment_internal.as_('r_lot_shipment_internal'),
|
|
union.r_vessel.as_("r_vessel"),
|
|
union.r_operator.as_("r_operator"),
|
|
union.r_lot_move.as_("r_lot_move"),
|
|
union.r_lot_status.as_("r_lot_status"),
|
|
union.r_lot_av.as_("r_lot_av"),
|
|
union.r_lot_premium.as_("r_lot_premium"),
|
|
union.r_lot_premium_sale.as_("r_lot_premium_sale"),
|
|
union.r_lot_parent.as_("r_lot_parent"),
|
|
union.r_lot_himself.as_("r_lot_himself"),
|
|
union.r_lot_container.as_("r_lot_container"),
|
|
union.r_line.as_("r_line"),
|
|
union.r_del_period.as_("r_del_period"),
|
|
union.r_sale_del_period.as_("r_sale_del_period"),
|
|
union.r_purchase.as_("r_purchase"),
|
|
union.r_sale.as_("r_sale"),
|
|
union.r_sale_line.as_("r_sale_line"),
|
|
union.r_tot.as_("r_tot"),
|
|
union.r_supplier.as_("r_supplier"),
|
|
union.r_client.as_("r_client"),
|
|
union.r_from_l.as_("r_from_l"),
|
|
union.r_from_t.as_("r_from_t"),
|
|
union.r_lot_pur_inv_state.as_('r_lot_pur_inv_state'),
|
|
union.r_lot_sale_inv_state.as_('r_lot_sale_inv_state'),
|
|
union.r_lot_pur_inv.as_('r_lot_pur_inv'),
|
|
union.r_lot_sal_inv.as_('r_lot_sal_inv'),
|
|
union.r_lot_state.as_('r_lot_state'),
|
|
union.r_shipping_status.as_('r_shipping_status'),
|
|
order_by=[union.r_line.desc])
|
|
return ordered_union
|
|
|
|
class LotReport(
|
|
ModelSQL, ModelView):
|
|
"Lots management"
|
|
__name__ = 'lot.report'
|
|
r_line = fields.Many2One('purchase.line',"P. line")
|
|
r_purchase = fields.Many2One('purchase.purchase', "Purchase")
|
|
r_lot_p =fields.Many2One('lot.lot',"lot P")
|
|
r_lot_s =fields.Many2One('lot.lot',"lot S")
|
|
r_lot_name = fields.Char("Lot")
|
|
r_number = fields.Char("Number")
|
|
r_lot_qt = fields.Float("Quantity")
|
|
r_lot_unit = fields.Many2One('product.uom', "Unit")
|
|
r_lot_product = fields.Many2One('product.product', "Product")
|
|
r_lot_type = fields.Selection([
|
|
('virtual', 'Open'),
|
|
('physic', 'Physic'),
|
|
('loss', 'Loss'),
|
|
], 'Qt type')
|
|
r_lot_status = fields.Selection([
|
|
(None, ''),
|
|
('forecast', 'Forecast'),
|
|
('loading', 'Loading'),
|
|
('transit', 'Transit'),
|
|
('destination', 'Destination'),
|
|
('stock', 'Stock'),
|
|
('delivered', 'Delivered')
|
|
], 'Where')
|
|
r_lot_av = fields.Selection([
|
|
('available', 'Available'),
|
|
('reserved', 'Reserved'),
|
|
('locked', 'Locked'),
|
|
], 'State')
|
|
r_lot_pur_inv_state = fields.Selection([
|
|
(None, ''),
|
|
('prov', 'Prov'),
|
|
('invoiced', 'Final')
|
|
], 'Pur. inv')
|
|
r_lot_pur_inv = fields.Many2One('account.invoice',"Pur. inv")
|
|
r_lot_sal_inv = fields.Many2One('account.invoice',"Sale inv")
|
|
r_lot_sale_inv_state = fields.Selection([
|
|
(None, ''),
|
|
('prov', 'Prov'),
|
|
('invoiced', 'Final')
|
|
], 'Sale inv')
|
|
r_lot_state = fields.Many2One('lot.qt.type',"Qt state")
|
|
r_lot_quantity = fields.Numeric("Avail. Qt",digits='r_lot_unit_line')
|
|
r_lot_matched = fields.Numeric("Matched Qt",digits='r_lot_unit_line')
|
|
r_lot_premium = fields.Numeric("Prem/Disc", digits='r_lot_unit_line')
|
|
r_lot_premium_sale = fields.Numeric("Prem/Disc", digits='r_lot_unit_line')
|
|
r_lot_shipment_in = fields.Many2One('stock.shipment.in', "Shipment In")
|
|
r_lot_shipment_out = fields.Many2One('stock.shipment.out', "Shipment Out")
|
|
r_lot_shipment_internal = fields.Many2One('stock.shipment.internal', "Shipment Internal")
|
|
r_vessel = fields.Many2One('trade.vessel', "Vessel")
|
|
r_operator = fields.Many2One('party.party', "Operator")
|
|
r_lot_move = fields.Many2One('stock.move', "Move")
|
|
r_lot_parent = fields.Many2One('lot.lot',"Parent")
|
|
r_lot_himself = fields.Many2One('lot.lot',"Lot")
|
|
r_lot_container = fields.Char("Container")
|
|
r_lot_unit_line = fields.Function(fields.Many2One('product.uom', "Unit"),'get_unit')
|
|
r_lot_price = fields.Function(fields.Numeric("Price", digits='r_lot_unit_line'),'get_lot_price')
|
|
r_lot_price_sale = fields.Function(fields.Numeric("Price", digits='r_lot_unit_line'),'get_lot_sale_price')
|
|
r_del_period = fields.Many2One(
|
|
'product.month', "Purchase Delivery Period")
|
|
r_sale_del_period = fields.Many2One(
|
|
'product.month', "Sale Delivery Period")
|
|
r_sale_line = fields.Many2One('sale.line',"S. line")
|
|
r_sale = fields.Many2One('sale.sale',"Sale")
|
|
r_tot = fields.Numeric("Qt tot", digits='r_lot_unit_line')
|
|
r_supplier = fields.Many2One('party.party',"Supplier")
|
|
r_client = fields.Many2One('party.party',"Client")
|
|
r_from_l = fields.Many2One('stock.location', 'From')
|
|
r_from_t = fields.Many2One('stock.location', 'To')
|
|
r_shipping_status = fields.Selection([
|
|
(None, ''),
|
|
('unshipped', 'Unshipped'),
|
|
('scheduled', 'Scheduled'),
|
|
('shipped', 'Shipped'),
|
|
('received', 'Received'),
|
|
], "Shipping status")
|
|
r_shipment_type = fields.Function(fields.Selection([
|
|
(None, ''),
|
|
('dropship', 'Dropship'),
|
|
('inbound', 'Inbound'),
|
|
], "Shipment Type"), 'get_shipment_type')
|
|
sh_icon = fields.Function(fields.Char("Shipment Icon"), 'on_change_with_sh_icon')
|
|
qt_icon = fields.Function(fields.Char("Qt Icon"), 'on_change_with_qt_icon')
|
|
r_shipment_origin = fields.Function(
|
|
fields.Reference(
|
|
selection=[
|
|
("stock.shipment.in", "In"),
|
|
("stock.shipment.out", "Out"),
|
|
("stock.shipment.internal", "Internal"),
|
|
],
|
|
string="Shipment",
|
|
),
|
|
"get_shipment_origin",
|
|
)
|
|
vis_data = fields.Function(
|
|
fields.Text('Graphe JSON'), 'get_vis_data'
|
|
)
|
|
|
|
def get_vis_data(self, name):
|
|
AccountMoveLine = Pool().get('account.move.line')
|
|
Account = Pool().get('account.account')
|
|
Stock = Pool().get('stock.move')
|
|
Lot = Pool().get('lot.lot')
|
|
Invoice = Pool().get('account.invoice')
|
|
InvoiceLine = Pool().get('account.invoice.line')
|
|
aml = AccountMoveLine.search([('lot','>',0)])
|
|
pivot_data = {
|
|
"data": [],
|
|
"options": None
|
|
}
|
|
if aml:
|
|
for l in aml:
|
|
lot = Lot(l.lot)
|
|
acc= Account(l.account)
|
|
ct_type = 'Purchase'
|
|
pr_type = 'Goods'
|
|
acc_label = 'Account event'
|
|
move_line = str(l.id)
|
|
journal = l.move.journal.id
|
|
product = lot.lot_product.name
|
|
ct_number = ''
|
|
amount = l.debit if l.debit != 0 else -l.credit
|
|
# if l.account.type.payable and l.account.type.statement == 'income':
|
|
# amount = -amount
|
|
ori = None #l.origin
|
|
if not ori:
|
|
ori = l.move.origin
|
|
if ori:
|
|
origin, id = str(ori).split(',')
|
|
if origin == 'stock.move':
|
|
acc_label = 'Reception'
|
|
sm = Stock(int(id))
|
|
if sm.to_location.type == 'customer':
|
|
ct_type = 'Sale'
|
|
if lot.sale_line:
|
|
ct_number = lot.sale_line.sale.number
|
|
else:
|
|
ct_type = 'Purchase'
|
|
if lot.line:
|
|
ct_number = lot.line.purchase.number
|
|
elif origin == 'account.invoice':
|
|
inv = Invoice(int(id))
|
|
product = None
|
|
if inv.lines[0].product:
|
|
product = inv.lines[0].product.name
|
|
if inv.type == 'out':
|
|
ct_type = 'Sale'
|
|
if lot.sale_line:
|
|
ct_number = lot.sale_line.sale.number
|
|
else:
|
|
ct_type = 'Purchase'
|
|
if lot.line:
|
|
ct_number = lot.line.purchase.number
|
|
if (journal == 1 and (l.account.id == 824 or l.account.id == 766)):
|
|
acc_label = 'COG Goods'
|
|
else:
|
|
if "Payment of invoice" in (l.move.description if l.move.description else ""):
|
|
acc_label = 'Payment'
|
|
else:
|
|
if 'Prepayment' in (inv.description if inv.description else ""):
|
|
acc_label = 'Prepayment'
|
|
else:
|
|
if inv.reference == 'Provisional':
|
|
acc_label = 'Prov'
|
|
else:
|
|
acc_label = 'Final'
|
|
|
|
pivot_data["data"].append(
|
|
{
|
|
"lot": lot.lot_name,
|
|
"ct type": ct_type,
|
|
"pr type": pr_type,
|
|
"ct number": ct_number,
|
|
"event": acc_label,
|
|
"product": product,
|
|
"amount": str(amount),
|
|
"account": str(acc.code),
|
|
"move": str(l.move),
|
|
"move line": move_line,
|
|
"journal": journal
|
|
}
|
|
)
|
|
|
|
pivot_data['options'] = {
|
|
"rows": ["lot","ct type","event"],
|
|
"cols": ["account"],
|
|
"aggregatorName": "Sum",
|
|
"vals": ["amount"]
|
|
}
|
|
return "pivotjson:" + json.dumps(pivot_data)
|
|
|
|
def get_shipment_origin(self, name):
|
|
if self.r_lot_shipment_in:
|
|
return 'stock.shipment.in,' + str(self.r_lot_shipment_in.id)
|
|
elif self.r_lot_shipment_out:
|
|
return 'stock.shipment.out,' + str(self.r_lot_shipment_out.id)
|
|
elif self.r_lot_shipment_internal:
|
|
return 'stock.shipment.internal,' + str(self.r_lot_shipment_internal.id)
|
|
return None
|
|
|
|
@fields.depends('r_lot_shipment_in', 'r_from_l', 'r_from_t')
|
|
def get_shipment_type(self, name):
|
|
if self.r_lot_shipment_in:
|
|
return self.r_lot_shipment_in.shipment_type
|
|
if (self.r_from_l and self.r_from_t
|
|
and self.r_from_l.type == 'supplier'
|
|
and self.r_from_t.type == 'customer'):
|
|
return 'dropship'
|
|
return 'inbound'
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
cls.r_line.search_unaccented = False
|
|
super(LotReport, cls).__setup__()
|
|
cls._order.insert(0, ('r_line', 'DESC'))
|
|
|
|
@fields.depends('r_lot_type')
|
|
def on_change_with_qt_icon(self, name=None):
|
|
mapping = {
|
|
'virtual': '\U0001F4D1', # 📑
|
|
'physic': '\U0001F4E6', # 📦
|
|
}
|
|
return mapping.get(self.r_lot_type if self.r_lot_type else None, '')
|
|
|
|
|
|
@fields.depends('r_lot_shipment_in')
|
|
def on_change_with_sh_icon(self, name=None):
|
|
mapping = {
|
|
'': '\U0001F69A', #truck
|
|
'vessel': '\U0001F6A2',
|
|
}
|
|
return mapping.get(self.r_lot_shipment_in.transport_type if self.r_lot_shipment_in else None, '')
|
|
|
|
@classmethod
|
|
def _get_origin(cls):
|
|
'Return list of Model names for origin Reference'
|
|
return [cls.__name__]
|
|
|
|
@classmethod
|
|
def get_origin(cls):
|
|
Model = Pool().get('ir.model')
|
|
get_name = Model.get_name
|
|
models = cls._get_origin()
|
|
return [(None, '')] + [(m, get_name(m)) for m in models]
|
|
|
|
def get_unit(self,name):
|
|
if self.r_line:
|
|
return self.r_line.unit
|
|
if self.r_sale_line:
|
|
return self.r_sale_line.unit
|
|
|
|
def get_lot_price(self,name=None):
|
|
price = Decimal(0)
|
|
if self.r_line:
|
|
l = Pool().get('purchase.line')(self.r_line)
|
|
premium = Decimal(0)
|
|
if self.r_lot_premium:
|
|
premium = self.r_lot_premium
|
|
price = premium + l.unit_price
|
|
return round(price,2)
|
|
|
|
def get_lot_sale_price(self,name=None):
|
|
price = Decimal(0)
|
|
# if self.sale_line:
|
|
# l = Pool().get('sale.line')(self.sale_line)
|
|
# premium = Decimal(0)
|
|
# if self.lot_premium_sale:
|
|
# premium = self.lot_premium_sale
|
|
# price = premium + l.unit_price
|
|
return round(price,2)
|
|
|
|
#@classmethod
|
|
#def delete(cls, records):
|
|
# Unmatch = Pool().get('lot.unmatch', type='wizard')
|
|
# session_id, _, _ = Unmatch.create()
|
|
# Unmatch.execute(session_id, {}, 'start')
|
|
|
|
@classmethod
|
|
def table_query(cls, context=None):
|
|
if context is None:
|
|
context = Transaction().context or {}
|
|
LotQT = Pool().get('lot.qt')
|
|
purchase = context.get('purchase')
|
|
sale = context.get('sale')
|
|
shipment = context.get('shipment')
|
|
product = context.get('product')
|
|
location = context.get('location')
|
|
type = context.get('type')
|
|
state = context.get('state')
|
|
wh = context.get('wh')
|
|
mode = context.get('mode')
|
|
group = context.get('group')
|
|
origin = context.get('origin')
|
|
supplier = context.get('supplier')
|
|
client = context.get('client')
|
|
ps = context.get('ps')
|
|
shipping_status = context.get('shipping_status')
|
|
asof = context.get('asof')
|
|
todate = context.get('todate')
|
|
dimension = context.get('dimension')
|
|
strategy = context.get('strategy')
|
|
finished = context.get('finished')
|
|
type = type or 'all'
|
|
state = state or 'all'
|
|
wh = wh or 'all'
|
|
group = group or 'by_physic'
|
|
origin = origin or 'all'
|
|
ps = ps or 'all'
|
|
shipping_status = shipping_status or 'all'
|
|
query = LotQT.getQuery(
|
|
purchase=purchase,
|
|
sale=sale,
|
|
shipment=shipment,
|
|
type=type,
|
|
state=state,
|
|
supplier=supplier,
|
|
client=client,
|
|
ps=ps,
|
|
lot_status=wh,
|
|
group=group,
|
|
product=product,
|
|
location=location,
|
|
origin=origin,
|
|
finished=finished,
|
|
shipping_status=shipping_status,
|
|
asof=asof,
|
|
todate=todate,
|
|
dimension=dimension,
|
|
strategy=strategy)
|
|
return query
|
|
|
|
@classmethod
|
|
def search_count(cls, domain, active_test=True, context=None):
|
|
type = 'all'
|
|
if 'r_lot_matched' in str(domain):
|
|
type = 'matched'
|
|
context = Transaction().context
|
|
context = {'purchase':None,'sale':None,'shipment':None,'type':type,'state':'all','wh':'all','group':'by_physic'}
|
|
query = cls.table_query(context)
|
|
cursor = Transaction().connection.cursor()
|
|
cursor.execute(*query)
|
|
return cursor.rowcount
|
|
|
|
@classmethod
|
|
def view_attributes(cls):
|
|
return super().view_attributes() + [
|
|
('/tree/field[@name="r_lot_shipment_in"]', 'tree_invisible',
|
|
(Eval('mode')=='pnl')),
|
|
('/tree/field[@name="r_lot_state"]', 'tree_invisible',
|
|
(Eval('mode')=='pnl')),
|
|
]
|
|
|
|
# @classmethod
|
|
# def search_rec_name(cls, name, clause):
|
|
# _, operator, operand, *extra = clause
|
|
# if operator.startswith('!') or operator.startswith('not '):
|
|
# bool_op = 'AND'
|
|
# else:
|
|
# bool_op = 'OR'
|
|
# code_value = operand
|
|
# if operator.endswith('like') and is_full_text(operand):
|
|
# code_value = lstrip_wildcard(operand)
|
|
# return [bool_op,
|
|
# ('r_line', operator, operand, *extra),
|
|
# ('r_lot_name', operator, operand, *extra),
|
|
# ('r_lot_product', operator, operand, *extra),
|
|
# ]
|
|
|
|
class LotContext(ModelView):
|
|
"Lot Context"
|
|
__name__ = 'lot.context'
|
|
|
|
asof = fields.Date("As of")
|
|
todate = fields.Date("To")
|
|
purchase = fields.Many2One('purchase.purchase', "Purchase")
|
|
supplier = fields.Many2One('party.party', "Supplier")
|
|
client = fields.Many2One('party.party', "Client")
|
|
sale = fields.Many2One('sale.sale', "Sale")
|
|
shipment = fields.Many2One('stock.shipment.in', "Shipment")
|
|
dimension = fields.Many2One('analytic.dimension.value', "Dimension")
|
|
strategy = fields.Many2One('mtm.strategy', "Strategy")
|
|
product = fields.Many2One('product.product',"Product")
|
|
location = fields.Many2One('stock.location',"Location")
|
|
type = fields.Selection([
|
|
('all', 'All'),
|
|
('matched', 'Matched'),
|
|
('not matched', 'Not Matched'),
|
|
], 'Matching status')
|
|
shipping_status = fields.Selection([
|
|
('all', 'All'),
|
|
('unshipped', 'Unshipped'),
|
|
('scheduled', 'Scheduled'),
|
|
('shipped', 'Shipped'),
|
|
('received', 'Received'),
|
|
], "Shipping status")
|
|
ps = fields.Selection([
|
|
('all', 'All'),
|
|
('P', 'Purchase'),
|
|
('S', 'Sale'),
|
|
], "Side")
|
|
state = fields.Selection([
|
|
('all', 'All'),
|
|
('available', 'Available'),
|
|
('reserved', 'Reserved'),
|
|
('locked', 'Locked')
|
|
], 'State')
|
|
|
|
wh = fields.Selection([
|
|
('all', 'All'),
|
|
('forecast', 'Forecast'),
|
|
('loading', 'Loading'),
|
|
('destination', 'Destination'),
|
|
('stock', 'Stock'),
|
|
('delivered', 'Delivered')
|
|
], 'Where')
|
|
|
|
group = fields.Selection([
|
|
('all', 'All'),
|
|
('by_physic', 'By physic'),
|
|
], 'Group')
|
|
|
|
origin = fields.Selection([
|
|
('all', 'All'),
|
|
('open', 'Open'),
|
|
('physic', 'Physic'),
|
|
], 'Open/Physic')
|
|
|
|
mode = fields.Selection([
|
|
('normal', 'Normal'),
|
|
('pnl', 'Pnl'),
|
|
],'Mode')
|
|
|
|
finished = fields.Boolean("Display finished")
|
|
|
|
@classmethod
|
|
def default_finished(cls):
|
|
return False
|
|
|
|
@classmethod
|
|
def default_type(cls):
|
|
return 'all'
|
|
|
|
@classmethod
|
|
def default_shipping_status(cls):
|
|
return 'all'
|
|
|
|
@classmethod
|
|
def default_ps(cls):
|
|
return 'all'
|
|
|
|
@classmethod
|
|
def default_group(cls):
|
|
return 'all'
|
|
|
|
@classmethod
|
|
def default_wh(cls):
|
|
return 'all'
|
|
|
|
@classmethod
|
|
def default_state(cls):
|
|
return 'all'
|
|
|
|
@classmethod
|
|
def default_purchase(cls):
|
|
return Transaction().context.get('purchase', None)
|
|
|
|
@classmethod
|
|
def default_sale(cls):
|
|
return Transaction().context.get('sale', None)
|
|
|
|
@classmethod
|
|
def get_context(cls, fields_names=None):
|
|
fields_names = fields_names.copy() if fields_names is not None else []
|
|
for field_name in [
|
|
'purchase', 'sale', 'shipment', 'supplier', 'client',
|
|
'dimension', 'strategy', 'product', 'location', 'type',
|
|
'shipping_status', 'ps', 'state', 'wh', 'mode', 'group',
|
|
'origin', 'finished', 'asof', 'todate']:
|
|
if field_name not in fields_names:
|
|
fields_names.append(field_name)
|
|
return super().get_context(fields_names=fields_names)
|
|
|
|
|
|
class LotShipping(Wizard):
|
|
"Link lot to transport"
|
|
__name__ = "lot.shipping"
|
|
|
|
start = StateTransition()
|
|
|
|
ship = StateView(
|
|
'lot.shipping.start',
|
|
'purchase_trade.shipping_view_form', [
|
|
Button("Cancel", 'end', 'tryton-cancel'),
|
|
Button("Link", 'shipping', 'tryton-ok', default=True),
|
|
])
|
|
|
|
shipping = StateTransition()
|
|
|
|
def transition_start(self):
|
|
if self.model.__name__ == 'lot.report':
|
|
return 'ship'
|
|
return 'end'
|
|
|
|
#def default_ship(self, fields):
|
|
#values = []
|
|
# if self.model.__name__ == 'lot.report':
|
|
# return {
|
|
# }
|
|
|
|
def transition_shipping(self):
|
|
Lot = Pool().get('lot.lot')
|
|
LotQt = Pool().get('lot.qt')
|
|
Move = Pool().get('stock.move')
|
|
affected_lines = []
|
|
if not self.ship.shipment:
|
|
pass
|
|
with Lot.skip_quantity_consistency():
|
|
for r in self.records:
|
|
if r.r_lot_shipment_in:
|
|
raise UserError("Please unlink before linking to a new shipment !")
|
|
else:
|
|
shipped_quantity = Decimal(str(r.r_lot_quantity)).quantize(Decimal("0.00001"))
|
|
logger.info("LotShipping:%s",shipped_quantity)
|
|
shipment_origin = None
|
|
if self.ship.quantity:
|
|
shipped_quantity = self.ship.quantity
|
|
if shipped_quantity == 0:
|
|
shipped_quantity = Decimal(str(r.r_lot_matched)).quantize(Decimal("0.00001"))
|
|
if self.ship.shipment == 'in':
|
|
if not self.ship.shipment_in:
|
|
UserError("Shipment not known!")
|
|
shipment_origin = 'stock.shipment.in,'+str(self.ship.shipment_in.id)
|
|
elif self.ship.shipment == 'out':
|
|
if not self.ship.shipment_out:
|
|
UserError("Shipment not known!")
|
|
shipment_origin = 'stock.shipment.out,'+str(self.ship.shipment_out.id)
|
|
elif self.ship.shipment == 'int':
|
|
if not self.ship.shipment_internal:
|
|
UserError("Shipment not known!")
|
|
shipment_origin = 'stock.shipment.internal,'+str(self.ship.shipment_internal.id)
|
|
if r.id < 10000000 :
|
|
l = Lot(r.id)
|
|
logger.info("IN_SHIPPING1:%s",l)
|
|
if self.ship.shipment == 'in':
|
|
l.lot_shipment_in = self.ship.shipment_in
|
|
elif self.ship.shipment == 'out':
|
|
l.lot_shipment_out = self.ship.shipment_out
|
|
elif self.ship.shipment == 'int':
|
|
l.lot_shipment_internal = self.ship.shipment_internal
|
|
logger.info("IN_SHIPPING2:%s",l.move)
|
|
if not l.move:
|
|
continue
|
|
logger.info("IN_SHIPPING3:%s",r)
|
|
move = Move(l.move)
|
|
move.shipment = shipment_origin
|
|
Move.save([move])
|
|
linked_transit_move = move.get_linked_transit_move()
|
|
if linked_transit_move:
|
|
linked_transit_move.shipment = shipment_origin
|
|
Move.save([linked_transit_move])
|
|
#Decrease forecasted virtual part shipped
|
|
vlot_p = l.getVlot_p()
|
|
l.updateVirtualPart(-l.get_current_quantity_converted(),shipment_origin,l.getVlot_s())
|
|
l.lot_av = 'reserved'
|
|
Lot.save([l])
|
|
l.set_current_quantity(l.lot_quantity,l.lot_gross_quantity,2)
|
|
Lot.save([l])
|
|
if l.line:
|
|
affected_lines.append(l.line)
|
|
if l.sale_line:
|
|
affected_lines.append(l.sale_line)
|
|
else:
|
|
lqt = LotQt(r.id - 10000000)
|
|
#Increase forecasted virtual part shipped
|
|
if not lqt.lot_p.updateVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s):
|
|
logger.info("LotShipping2:%s",shipped_quantity)
|
|
lqt.lot_p.createVirtualPart(shipped_quantity,shipment_origin,lqt.lot_s)
|
|
#Decrease forecasted virtual part non shipped
|
|
lqt.lot_p.updateVirtualPart(-shipped_quantity,None,lqt.lot_s)
|
|
if lqt.lot_p and lqt.lot_p.line:
|
|
affected_lines.append(lqt.lot_p.line)
|
|
if lqt.lot_s and lqt.lot_s.sale_line:
|
|
affected_lines.append(lqt.lot_s.sale_line)
|
|
|
|
Lot.assert_lines_quantity_consistency(affected_lines)
|
|
return 'end'
|
|
|
|
def end(self):
|
|
return 'reload'
|
|
|
|
class LotShippingStart(ModelView):
|
|
"Link lot to transport"
|
|
__name__ = "lot.shipping.start"
|
|
shipment = fields.Selection([
|
|
('in', 'Shipment In'),
|
|
('out', 'Shipment Out'),
|
|
('int', 'Internal Shipment'),
|
|
], 'Shipment type', readonly=True)
|
|
shipment_in = fields.Many2One('stock.shipment.in',"Shipment In", states={'invisible': (Eval('shipment') != 'in')})
|
|
shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out", states={'invisible': (Eval('shipment') != 'out')})
|
|
shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal", states={'invisible': (Eval('shipment') != 'int')})
|
|
full = fields.Boolean("Link all the lot")
|
|
quantity = fields.Numeric("Qt", states={'readonly': (Eval('full') == True)})
|
|
|
|
@classmethod
|
|
def default_full(cls):
|
|
return True
|
|
|
|
@classmethod
|
|
def default_shipment(cls):
|
|
return 'in'
|
|
|
|
class QtWarning(UserWarning):
|
|
pass
|
|
|
|
|
|
class LotRemoveWarning(UserWarning):
|
|
pass
|
|
|
|
|
|
class LotToleranceWarning(UserWarning):
|
|
pass
|
|
|
|
|
|
class LotMatching(Wizard):
|
|
"Matching"
|
|
__name__ = "lot.matching"
|
|
|
|
start = StateTransition()
|
|
|
|
match = StateView(
|
|
'lot.matching.start',
|
|
'purchase_trade.matching_view_form', [
|
|
Button("Cancel", 'end', 'tryton-cancel'),
|
|
Button("Match", 'matching', 'tryton-ok', default=True),
|
|
])
|
|
|
|
matching = StateTransition()
|
|
|
|
def transition_start(self):
|
|
return 'match'
|
|
|
|
@staticmethod
|
|
def _record_id(record):
|
|
return record.id if record else None
|
|
|
|
@classmethod
|
|
def _matching_lot_value(cls, lqt, side):
|
|
lot = lqt.lot_p if side == 'purchase' else lqt.lot_s
|
|
line = getattr(lot, 'line', None)
|
|
sale_line = getattr(lot, 'sale_line', None)
|
|
purchase = getattr(line, 'purchase', None)
|
|
sale = getattr(sale_line, 'sale', None)
|
|
party = (
|
|
getattr(purchase, 'party', None) if side == 'purchase'
|
|
else getattr(sale, 'party', None))
|
|
quantity = Decimal(str(lqt.lot_quantity or 0))
|
|
if side == 'sale':
|
|
quantity = abs(quantity)
|
|
values = {
|
|
'lot_id': cls._record_id(lot),
|
|
'lot_r_id': cls._record_id(lqt),
|
|
'lot_purchase': cls._record_id(purchase),
|
|
'lot_sale': cls._record_id(sale),
|
|
'lot_shipment_in': cls._record_id(lqt.lot_shipment_in),
|
|
'lot_shipment_internal': cls._record_id(
|
|
lqt.lot_shipment_internal),
|
|
'lot_shipment_out': cls._record_id(lqt.lot_shipment_out),
|
|
'lot_type': getattr(lot, 'lot_type', None),
|
|
'lot_product': cls._record_id(getattr(lot, 'lot_product', None)),
|
|
'lot_quantity': quantity,
|
|
'lot_matched_qt': 0,
|
|
'lot_already_matched_qt': LotQt._matching_already_matched(
|
|
lqt, side),
|
|
'lot_cp': cls._record_id(party),
|
|
}
|
|
trade_line = line if side == 'purchase' else sale_line
|
|
values.update(LotQt._matching_line_tolerance_info(trade_line))
|
|
return values
|
|
|
|
@classmethod
|
|
def _physical_purchase_matching_value(cls, lot):
|
|
line = getattr(lot, 'line', None)
|
|
purchase = getattr(line, 'purchase', None)
|
|
quantity = Decimal(str(lot.get_current_quantity_converted() or 0))
|
|
values = {
|
|
'lot_id': cls._record_id(lot),
|
|
'lot_r_id': None,
|
|
'lot_purchase': cls._record_id(purchase),
|
|
'lot_sale': None,
|
|
'lot_shipment_in': cls._record_id(
|
|
getattr(lot, 'lot_shipment_in', None)),
|
|
'lot_shipment_internal': cls._record_id(
|
|
getattr(lot, 'lot_shipment_internal', None)),
|
|
'lot_shipment_out': cls._record_id(
|
|
getattr(lot, 'lot_shipment_out', None)),
|
|
'lot_type': getattr(lot, 'lot_type', None),
|
|
'lot_product': cls._record_id(getattr(lot, 'lot_product', None)),
|
|
'lot_quantity': quantity,
|
|
'lot_matched_qt': 0,
|
|
'lot_already_matched_qt': 0,
|
|
'lot_cp': cls._record_id(getattr(purchase, 'party', None)),
|
|
}
|
|
values.update(LotQt._matching_line_tolerance_info(line))
|
|
return values
|
|
|
|
@classmethod
|
|
def _selected_matching_defaults(cls, active_ids):
|
|
LotQt = Pool().get('lot.qt')
|
|
Lot = Pool().get('lot.lot')
|
|
lot_p = []
|
|
lot_s = []
|
|
for active_id in active_ids or []:
|
|
if active_id <= 10000000:
|
|
lot = Lot(active_id)
|
|
if (getattr(lot, 'lot_type', None) == 'physic'
|
|
and getattr(lot, 'line', None)
|
|
and not getattr(lot, 'sale_line', None)):
|
|
lot_p.append(cls._physical_purchase_matching_value(lot))
|
|
continue
|
|
lqt = LotQt(active_id - 10000000)
|
|
if lqt.lot_p and lqt.lot_s:
|
|
continue
|
|
if lqt.lot_p:
|
|
lot_p.append(cls._matching_lot_value(lqt, 'purchase'))
|
|
elif lqt.lot_s:
|
|
lot_s.append(cls._matching_lot_value(lqt, 'sale'))
|
|
return {
|
|
'lot_p': lot_p,
|
|
'lot_s': lot_s,
|
|
}
|
|
|
|
def transition_matching(self):
|
|
Warning = Pool().get('res.user.warning')
|
|
LotQt = Pool().get('lot.qt')
|
|
Lot = Pool().get('lot.lot')
|
|
if self.match.tot_p != self.match.tot_s:
|
|
warning_name = Warning.format("Quantity's issue", [])
|
|
if Warning.check(warning_name):
|
|
raise QtWarning(warning_name,
|
|
"Quantities not compatibles")
|
|
with Lot.skip_quantity_consistency():
|
|
LotQt.match_lots(self.match.lot_p,self.match.lot_s)
|
|
affected_lines = []
|
|
for matched_lot in list(self.match.lot_p or []) + list(self.match.lot_s or []):
|
|
if matched_lot.lot_id:
|
|
if matched_lot.lot_id.line:
|
|
affected_lines.append(matched_lot.lot_id.line)
|
|
if matched_lot.lot_id.sale_line:
|
|
affected_lines.append(matched_lot.lot_id.sale_line)
|
|
if matched_lot.lot_r_id:
|
|
if matched_lot.lot_r_id.lot_p and matched_lot.lot_r_id.lot_p.line:
|
|
affected_lines.append(matched_lot.lot_r_id.lot_p.line)
|
|
if matched_lot.lot_r_id.lot_s and matched_lot.lot_r_id.lot_s.sale_line:
|
|
affected_lines.append(matched_lot.lot_r_id.lot_s.sale_line)
|
|
Lot.assert_lines_quantity_consistency(affected_lines)
|
|
|
|
return 'end'
|
|
|
|
def end(self):
|
|
return 'reload'
|
|
|
|
|
|
class LotGoMatching(Wizard):
|
|
"Go to matching"
|
|
__name__ = "lot.go_matching"
|
|
|
|
start = StateTransition()
|
|
|
|
match = StateView(
|
|
'lot.matching.start',
|
|
'purchase_trade.go_matching_view_form', [
|
|
Button("Cancel", 'end', 'tryton-cancel'),
|
|
Button("Match", 'matching', 'tryton-ok', default=True),
|
|
])
|
|
|
|
matching = StateTransition()
|
|
|
|
def transition_start(self):
|
|
return 'match'
|
|
|
|
def default_match(self, fields):
|
|
context = Transaction().context
|
|
values = LotMatching._selected_matching_defaults(
|
|
context.get('active_ids') or [])
|
|
if not values.get('lot_p') or not values.get('lot_s'):
|
|
raise UserError(
|
|
"Please select at least one purchase line and one sale line "
|
|
"before using Go to matching.")
|
|
values['qt_type'] = 'all'
|
|
return values
|
|
|
|
def transition_matching(self):
|
|
if not self.match.lot_p or not self.match.lot_s:
|
|
raise UserError(
|
|
"Please select at least one purchase line and one sale line "
|
|
"before using Go to matching.")
|
|
return LotMatching.transition_matching(self)
|
|
|
|
def end(self):
|
|
return 'reload'
|
|
|
|
|
|
class LotMatchingStart(ModelView):
|
|
"Matching"
|
|
__name__ = "lot.matching.start"
|
|
lot_p = fields.One2Many('lot.matching.lot','lms',"Purchase")
|
|
tot_p = fields.Numeric("Tot. from P")
|
|
lot_s = fields.One2Many('lot.matching.lot','lms',"Sale")
|
|
tot_s = fields.Numeric("Tot. from S")
|
|
filter_p = fields.Boolean("Purchase filter")
|
|
filter_s = fields.Boolean("Sale filter")
|
|
product_s = fields.Many2Many('product.product',None,None,"Sale product")
|
|
product_p = fields.Many2Many('product.product',None,None,"Purchase product")
|
|
location_p = fields.Many2Many('stock.location',None,None,"Purchase destination")
|
|
location_s = fields.Many2Many('stock.location',None,None,"Sale loading")
|
|
cp_p = fields.Many2Many('party.party',None,None,"Supplier")
|
|
cp_s = fields.Many2Many('party.party',None,None,"Client")
|
|
inc_p = fields.Many2Many('incoterm.incoterm',None,None,"Supplier incoterm")
|
|
inc_s = fields.Many2Many('incoterm.incoterm',None,None,"Client incoterm")
|
|
qt_type = fields.Selection([
|
|
('virtual', 'Open'),
|
|
('physic', 'Physic'),
|
|
('all', 'All'),
|
|
], 'Qt type')
|
|
purchase = fields.Many2One('purchase.purchase',"Purchase")
|
|
sale = fields.Many2One('sale.sale',"Sale")
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(LotMatchingStart, cls).__setup__()
|
|
cls._buttons.update({
|
|
'filters': {
|
|
},
|
|
})
|
|
|
|
@classmethod
|
|
def default_qt_type(cls):
|
|
return 'all'
|
|
|
|
@dualmethod
|
|
@ModelView.button
|
|
def filters(cls, lotmatchingstarts):
|
|
pool = Pool()
|
|
|
|
# @fields.depends('filter_p')
|
|
# def on_change_filter_p(self, name=None):
|
|
# if self.filter_p:
|
|
# ApplyMatching = Pool().get('lot.matching', type='wizard')
|
|
# session_id, _, _ = ApplyMatching.create()
|
|
# ApplyMatching.execute(session_id, {}, 'start')
|
|
|
|
@fields.depends('lot_p')
|
|
def on_change_with_tot_p(self, name=None):
|
|
return sum([e.lot_matched_qt for e in self.lot_p])
|
|
|
|
@fields.depends('lot_s')
|
|
def on_change_with_tot_s(self, name=None):
|
|
return sum([e.lot_matched_qt for e in self.lot_s])
|
|
|
|
@fields.depends('product_s','qt_type','sale','cp_s')
|
|
def on_change_with_lot_s(self, name=None):
|
|
lp = []
|
|
LotQt = Pool().get('lot.qt')
|
|
cursor = Transaction().connection.cursor()
|
|
query = LotQt.getQuery(None,self.sale.id if self.sale else None,None,'not matched','all',self.qt_type,None,self.cp_s.id if self.cp_s else None,'S',None,None,self.product_s,None,'open')
|
|
cursor.execute(*query)
|
|
|
|
for r in cursor_dict(cursor):
|
|
val = {}
|
|
val['lot_id'] = r['r_lot_s']
|
|
val['lot_r_id'] = r['id'] - 10000000
|
|
val['lot_sale'] = r['r_sale']
|
|
val['lot_purchase'] = r['r_purchase']
|
|
val['lot_shipment_in'] = r['r_lot_shipment_in']
|
|
val['lot_shipment_internal'] = r['r_lot_shipment_internal']
|
|
val['lot_shipment_out'] = r['r_lot_shipment_out']
|
|
val['lot_type'] = r['r_lot_type']
|
|
val['lot_product'] = r['r_lot_product']
|
|
val['lot_quantity'] = -r['r_lot_quantity']
|
|
if r['r_sale'] and r['r_purchase']:
|
|
val['lot_quantity'] = r['r_lot_matched']
|
|
val['lot_matched_qt'] = 0
|
|
val['lot_cp'] = r['r_client']
|
|
lqt = LotQt(r['id'] - 10000000)
|
|
val['lot_already_matched_qt'] = (
|
|
LotQt._matching_already_matched(lqt, 'sale'))
|
|
val.update(LotQt._matching_line_tolerance_info(
|
|
LotQt._matching_line(lqt, 'sale')))
|
|
lp.append(val)
|
|
return lp
|
|
|
|
@fields.depends('cp_p','qt_type','purchase','product_p')
|
|
def on_change_with_lot_p(self, name=None):
|
|
lp = []
|
|
LotQt = Pool().get('lot.qt')
|
|
cursor = Transaction().connection.cursor()
|
|
query = LotQt.getQuery(self.purchase.id if self.purchase else None,None,None,'not matched','all',self.qt_type,self.cp_p.id if self.cp_p else None,None,'P',None,None,self.product_p)
|
|
cursor.execute(*query)
|
|
|
|
for r in cursor_dict(cursor):
|
|
val = {}
|
|
val['lot_id'] = r['r_lot_p']
|
|
val['lot_r_id'] = r['id'] - 10000000
|
|
val['lot_purchase'] = r['r_purchase']
|
|
val['lot_sale'] = r['r_sale']
|
|
val['lot_shipment_in'] = r['r_lot_shipment_in']
|
|
val['lot_shipment_internal'] = r['r_lot_shipment_internal']
|
|
val['lot_shipment_out'] = r['r_lot_shipment_out']
|
|
val['lot_type'] = r['r_lot_type']
|
|
val['lot_product'] = r['r_lot_product']
|
|
val['lot_quantity'] = r['r_lot_quantity']
|
|
val['lot_matched_qt'] = 0
|
|
val['lot_cp'] = r['r_supplier']
|
|
lqt = LotQt(r['id'] - 10000000)
|
|
val['lot_already_matched_qt'] = (
|
|
LotQt._matching_already_matched(lqt, 'purchase'))
|
|
val.update(LotQt._matching_line_tolerance_info(
|
|
LotQt._matching_line(lqt, 'purchase')))
|
|
lp.append(val)
|
|
return lp
|
|
|
|
class LotMatchingLot(ModelView):
|
|
"Lots"
|
|
__name__ = "lot.matching.lot"
|
|
|
|
lms = fields.Many2One('lot.matching.start',"Matching")
|
|
lot_id = fields.Many2One('lot.lot',"Lot")
|
|
lot_r_id = fields.Many2One('lot.qt',"Lot Qt")
|
|
lot_purchase = fields.Many2One('purchase.purchase', "Purchase")
|
|
lot_sale = fields.Many2One('sale.sale', "Sale")
|
|
lot_name = fields.Char("Name",readonly=True)
|
|
lot_type = fields.Selection([
|
|
('virtual', 'Open'),
|
|
('physic', 'Physic'),
|
|
('loss', 'Loss'),
|
|
], 'Qt type')
|
|
lot_shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
|
|
lot_shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal")
|
|
lot_shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
|
|
lot_product = fields.Many2One('product.product',"Product",readonly=True)
|
|
lot_quantity = fields.Numeric("Qt",readonly=True)
|
|
lot_theoretical_qt = fields.Numeric("Theoretical Qt", readonly=True)
|
|
lot_targeted_qt = fields.Numeric("Targeted Qt", readonly=True)
|
|
lot_already_matched_qt = fields.Numeric("Already matched", readonly=True)
|
|
lot_qt_min = fields.Numeric("Qt min", readonly=True)
|
|
lot_qt_max = fields.Numeric("Qt max", readonly=True)
|
|
lot_tol_min = fields.Numeric("Tol -", readonly=True)
|
|
lot_tol_max = fields.Numeric("Tol +", readonly=True)
|
|
lot_tolerance_min = fields.Numeric("Tolerance min", readonly=True)
|
|
lot_tolerance_max = fields.Numeric("Tolerance max", readonly=True)
|
|
lot_tolerance_used = fields.Function(
|
|
fields.Numeric("Tolerance used"), 'on_change_with_lot_tolerance_used')
|
|
lot_unit = fields.Many2One('product.uom',"Unit",readonly=True)
|
|
lot_matched_qt = fields.Numeric("Qt to match")
|
|
lot_cp = fields.Many2One('party.party',"Counterparty")
|
|
lot_shipment_origin = fields.Function(
|
|
fields.Reference(
|
|
selection=[
|
|
("stock.shipment.in", "In"),
|
|
("stock.shipment.out", "Out"),
|
|
("stock.shipment.internal", "Internal"),
|
|
],
|
|
string="Shipment",
|
|
),
|
|
"get_shipment_origin",
|
|
)
|
|
|
|
@fields.depends(
|
|
'lot_matched_qt', 'lot_already_matched_qt', 'lot_theoretical_qt')
|
|
def on_change_with_lot_tolerance_used(self, name=None):
|
|
theoretical = Decimal(str(self.lot_theoretical_qt or 0))
|
|
if not theoretical:
|
|
return Decimal(0)
|
|
projected = (
|
|
Decimal(str(self.lot_already_matched_qt or 0))
|
|
+ Decimal(str(self.lot_matched_qt or 0)))
|
|
return round(
|
|
((projected - theoretical) / theoretical) * Decimal(100), 5)
|
|
|
|
def get_shipment_origin(self, name):
|
|
if self.lot_shipment_in:
|
|
return 'stock.shipment.in,' + str(self.lot_shipment_in.id)
|
|
elif self.lot_shipment_out:
|
|
return 'stock.shipment.out,' + str(self.lot_shipment_out.id)
|
|
elif self.lot_shipment_internal:
|
|
return 'stock.shipment.internal,' + str(self.lot_shipment_internal.id)
|
|
return None
|
|
|
|
|
|
class LotUnmatch(Wizard):
|
|
"Unmatch"
|
|
__name__ = "lot.unmatch"
|
|
|
|
start = StateTransition()
|
|
|
|
def transition_start(self):
|
|
Lot = Pool().get('lot.lot')
|
|
LotQt = Pool().get('lot.qt')
|
|
affected_lines = []
|
|
with Lot.skip_quantity_consistency():
|
|
for r in self.records:
|
|
if r.id > 10000000 :
|
|
lqt = LotQt(r.id - 10000000)
|
|
if lqt.lot_p and lqt.lot_s:
|
|
lot = lqt.lot_p
|
|
qt = lqt.lot_quantity
|
|
#Increase forecasted virtual parts non matched
|
|
if not lot.updateVirtualPart(qt,lqt.lot_shipment_origin,None):
|
|
lot.createVirtualPart(qt,lqt.lot_shipment_origin,None)
|
|
if not lot.updateVirtualPart(qt,lqt.lot_shipment_origin,lqt.lot_s,'only sale'):
|
|
lot.createVirtualPart(qt,lqt.lot_shipment_origin,lqt.lot_s,'only sale')
|
|
#Decrease forecasted virtual part matched
|
|
logger.info("UNMATCH_QT:%s",qt)
|
|
lot.updateVirtualPart(-qt,lqt.lot_shipment_origin,lqt.lot_s)
|
|
if lqt.lot_p and lqt.lot_p.line:
|
|
affected_lines.append(lqt.lot_p.line)
|
|
if lqt.lot_s and lqt.lot_s.sale_line:
|
|
affected_lines.append(lqt.lot_s.sale_line)
|
|
else:
|
|
lot = Lot(r.id)
|
|
qt = lot.get_current_quantity_converted()
|
|
# #Increase forecasted virtual part matched
|
|
# if not lot.updateVirtualPart(qt,lot.lot_shipment_origin,lot.getVlot_s()):
|
|
# lot.createVirtualPart(qt,lot.lot_shipment_origin,lot.getVlot_s())
|
|
# #Decrease forecasted virtual part non matched
|
|
# if not lot.updateVirtualPart(-qt,lot.lot_shipment_origin,None):
|
|
# lot.createVirtualPart(-qt,lot.lot_shipment_origin,None)
|
|
ls = lot.getVlot_s()
|
|
ls.lot_quantity += qt
|
|
Lot.save([ls])
|
|
lot.sale_line = None
|
|
Lot.save([lot])
|
|
if lot.line:
|
|
affected_lines.append(lot.line)
|
|
if ls.sale_line:
|
|
affected_lines.append(ls.sale_line)
|
|
Lot.assert_lines_quantity_consistency(affected_lines)
|
|
return 'end'
|
|
|
|
def end(self):
|
|
return 'reload'
|
|
|
|
class LotUnship(Wizard):
|
|
"Unlink transport"
|
|
__name__ = "lot.unship"
|
|
|
|
start = StateTransition()
|
|
|
|
def transition_start(self):
|
|
Lot = Pool().get('lot.lot')
|
|
LotQt = Pool().get('lot.qt')
|
|
Move = Pool().get('stock.move')
|
|
affected_lines = []
|
|
with Lot.skip_quantity_consistency():
|
|
for r in self.records:
|
|
lqt = LotQt(r.id - 10000000)
|
|
lot = Lot(r.id)
|
|
if lqt and r.r_lot_type == 'virtual':
|
|
shipment = None
|
|
qt = lqt.lot_quantity
|
|
mode = 'both'
|
|
if not lqt.lot_p:
|
|
lot = lqt.lot_s
|
|
mode = 'only sale'
|
|
else:
|
|
lot = lqt.lot_p
|
|
if lqt.lot_shipment_in:
|
|
shipment = lqt.lot_shipment_in
|
|
elif lqt.lot_shipment_internal:
|
|
shipment = lqt.lot_shipment_internal
|
|
elif lqt.lot_shipment_out:
|
|
shipment = lqt.lot_shipment_out
|
|
#Decrease forecasted virtual part shipped
|
|
lot.updateVirtualPart(-qt,lqt.lot_shipment_origin,lqt.lot_s,mode)
|
|
#Increase forecasted virtual part non shipped
|
|
if not lot.updateVirtualPart(qt,None,lqt.lot_s,mode):
|
|
lot.createVirtualPart(qt,None,lqt.lot_s,mode)
|
|
if lqt.lot_p and lqt.lot_p.line:
|
|
affected_lines.append(lqt.lot_p.line)
|
|
if lqt.lot_s and lqt.lot_s.sale_line:
|
|
affected_lines.append(lqt.lot_s.sale_line)
|
|
if lot and r.r_lot_type == 'physic':
|
|
lot = Lot(r.r_lot_p)
|
|
if lot.lot_type == 'physic':
|
|
str_sh = str(lot.lot_shipment_origin)
|
|
if 'stock.shipment.in' in str_sh:
|
|
lot.lot_shipment_in = None
|
|
elif 'stock.shipment.out' in str_sh:
|
|
lot.lot_shipment_out = None
|
|
elif 'stock.shipment.internal' in str_sh:
|
|
lot.lot_shipment_internal = None
|
|
|
|
if lot.move:
|
|
move = Move(lot.move)
|
|
move.shipment = None
|
|
Move.save([move])
|
|
# lm = LotMove.search([('lot','=',lot.id),('move','=',lot.move)])
|
|
# if lm:
|
|
# LotMove.delete(lm)
|
|
lot.lot_status = 'forecast'
|
|
Lot.save([lot])
|
|
if lot.line:
|
|
affected_lines.append(lot.line)
|
|
if lot.sale_line:
|
|
affected_lines.append(lot.sale_line)
|
|
Lot.assert_lines_quantity_consistency(affected_lines)
|
|
return 'end'
|
|
|
|
def end(self):
|
|
return 'reload'
|
|
|
|
class LotAdding(Wizard):
|
|
"Adding physical lots"
|
|
__name__ = "lot.add"
|
|
|
|
start = StateTransition()
|
|
|
|
add = StateView(
|
|
'lot.add.lot',
|
|
'purchase_trade.add_lot_view_form', [
|
|
Button("Cancel", 'end', 'tryton-cancel'),
|
|
Button("Add", 'adding', 'tryton-ok', default=True),
|
|
])
|
|
|
|
adding = StateTransition()
|
|
|
|
def transition_start(self):
|
|
return 'add'
|
|
|
|
def default_add(self, fields):
|
|
unit = None
|
|
quantity = None
|
|
sh_in = None
|
|
sh_int = None
|
|
sh_out = None
|
|
Lot = Pool().get('lot.lot')
|
|
LotQt = Pool().get('lot.qt')
|
|
context = Transaction().context
|
|
ids = context.get('active_ids')
|
|
for i in ids:
|
|
val = {}
|
|
if i > 10000000 :
|
|
lqt = LotQt(i - 10000000)
|
|
if not lqt.lot_p:
|
|
raise UserError("To add physical lot to sale, use action 'Apply matching'!")
|
|
lot = lqt.lot_p
|
|
unit = lot.lot_unit_line.id
|
|
quantity = lqt.lot_quantity
|
|
sh_in = lqt.lot_shipment_in.id if lqt.lot_shipment_in else None
|
|
sh_int = lqt.lot_shipment_internal.id if lqt.lot_shipment_internal else None
|
|
sh_out = lqt.lot_shipment_out.id if lqt.lot_shipment_out else None
|
|
else:
|
|
raise UserError("Trying to add physical lots to physical lot!")
|
|
return {
|
|
'unit': unit,
|
|
'quantity': quantity,
|
|
'shipment_in':sh_in,
|
|
'shipment_internal':sh_int,
|
|
'shipment_out':sh_out,
|
|
}
|
|
|
|
def transition_adding(self):
|
|
if self.records:
|
|
LotQt = Pool().get('lot.qt')
|
|
lqt = LotQt(self.records[0].id - 10000000)
|
|
LotQt.add_physical_lots(lqt,self.add.lots)
|
|
return 'end'
|
|
|
|
def end(self):
|
|
return 'reload'
|
|
|
|
class LotAddLot(ModelView):
|
|
"Add physical lots"
|
|
__name__ = "lot.add.lot"
|
|
unit = fields.Many2One('product.uom',"Unit",readonly=True)
|
|
quantity = fields.Numeric("Quantity available",digits='unit',readonly=True)
|
|
shipment_in = fields.Many2One('stock.shipment.in',"Shipment In",states={'invisible': ~Eval('shipment_in')})
|
|
shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal",states={'invisible': ~Eval('shipment_internal')})
|
|
shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out",states={'invisible': ~Eval('shipment_out')})
|
|
lots = fields.One2Many('lot.add.line','lal',"Lots")
|
|
|
|
class LotAddLine(ModelView):
|
|
"Lots"
|
|
__name__ = "lot.add.line"
|
|
|
|
lal = fields.Many2One('lot.add.lot',"Lots")
|
|
lot_qt = fields.Numeric("Packing detail")
|
|
lot_unit = fields.Many2One('product.uom', "Unit",required=True)
|
|
lot_quantity = fields.Numeric("Net weight")
|
|
lot_gross_quantity = fields.Numeric("Gross weight")
|
|
lot_unit_line = fields.Many2One('product.uom', "Unit",required=True)
|
|
lot_premium = fields.Numeric("Premium")
|
|
lot_chunk_key = fields.Integer("Chunk key")
|
|
|
|
# @fields.depends('lot_qt')
|
|
# def on_change_with_lot_quantity(self):
|
|
# if not self.lot_quantity:
|
|
# return self.lot_qt
|
|
|
|
# @fields.depends('lot_qt')
|
|
# def on_change_with_lot_gross_quantity(self):
|
|
# if not self.lot_gross_quantity:
|
|
# return self.lot_qt
|
|
|
|
# @fields.depends('lot_unit')
|
|
# def on_change_with_lot_unit_line(self):
|
|
# if not self.lot_unit_line:
|
|
# return self.lot_unit
|
|
|
|
|
|
class LotImporting(Wizard):
|
|
"Importing physical lots"
|
|
__name__ = "lot.import"
|
|
|
|
start = StateTransition()
|
|
|
|
imp = StateView(
|
|
'lot.import.lot',
|
|
'purchase_trade.import_lot_view_form', [
|
|
Button("Cancel", 'end', 'tryton-cancel'),
|
|
Button("Import", 'importing', 'tryton-ok', default=True),
|
|
])
|
|
|
|
importing = StateTransition()
|
|
|
|
def transition_start(self):
|
|
return 'imp'
|
|
|
|
def transition_importing(self):
|
|
if self.records:
|
|
line = self.records[0].r_line
|
|
Lot = Pool().get('lot.lot')
|
|
LotQt = Pool().get('lot.qt')
|
|
LotQtHist = Pool().get('lot.qt.hist')
|
|
LotQtType = Pool().get('lot.qt.type')
|
|
lqt = LotQt(self.records[0].id - 10000000)
|
|
tot_qt = 0
|
|
affected_lines = [line]
|
|
if self.records[0].r_sale_line:
|
|
affected_lines.append(self.records[0].r_sale_line)
|
|
with Lot.skip_quantity_consistency():
|
|
for l in self.imp.lots:
|
|
l.line = line
|
|
l.lot_shipment_in = self.records[0].r_lot_shipment_in
|
|
l.lot_shipment_internal = self.records[0].r_lot_shipment_internal
|
|
l.lot_shipment_out = self.records[0].r_lot_shipment_out
|
|
l.lot_product = self.records[0].r_lot_product
|
|
l.sale_line = self.records[0].r_sale_line
|
|
l.lot_type = 'physic'
|
|
premium = Decimal(0)
|
|
if l.lot_price:
|
|
premium = l.lot_price - l.line.unit_price
|
|
logger.info("IMPORTLOT:%s",l.line.linked_price)
|
|
if l.line.linked_price:
|
|
premium = l.lot_price - l.line.linked_price
|
|
l.lot_premium = premium
|
|
l.lot_premium_sup = premium
|
|
Uom = Pool().get('product.uom')
|
|
net_qt = round(Decimal(Uom.compute_qty(l.lot_unit_line, float(l.lot_quantity), line.unit)),5)
|
|
gross_qt = round(Decimal(Uom.compute_qty(l.lot_unit_line, float(l.lot_gross_quantity), line.unit)),5)
|
|
lqtt = LotQtType.search([('sequence','=',1)])
|
|
if lqtt:
|
|
lqh = LotQtHist()
|
|
lqh.quantity_type = lqtt[0]
|
|
lqh.quantity = l.lot_quantity
|
|
lqh.gross_quantity = l.lot_gross_quantity
|
|
l.lot_hist = [lqh]
|
|
Lot.save([l])
|
|
tot_qt += net_qt
|
|
logger.info("IMPORTING:%s",net_qt)
|
|
#need to decrease sale virtual lot if matching
|
|
if self.records[0].r_lot_s:
|
|
ls = Lot(self.records[0].r_lot_s)
|
|
ls.lot_quantity -= net_qt
|
|
ls.lot_gross_quantity -= net_qt
|
|
ls.lot_qt -= float(net_qt)
|
|
Lot.save([ls])
|
|
lqt.lot_quantity -= tot_qt
|
|
if lqt.lot_quantity < 0:
|
|
lqt.lot_quantity = 0
|
|
LotQt.save([lqt])
|
|
Lot.assert_lines_quantity_consistency(affected_lines)
|
|
return 'end'
|
|
|
|
def end(self):
|
|
return 'reload'
|
|
|
|
class LotImportLot(ModelView):
|
|
"Import physical lots"
|
|
__name__ = "lot.import.lot"
|
|
|
|
reference = fields.Char("Reference")
|
|
lots = fields.One2Many('lot.lot','',"Lots")
|
|
|
|
@fields.depends('reference')
|
|
def on_change_with_lots(self, name=None):
|
|
lp = []
|
|
LotFCR = Pool().get('lot.fcr')
|
|
if self.reference:
|
|
lfcrs = LotFCR.search([('lot_offer','=',self.reference)])
|
|
for r in lfcrs:
|
|
val = {}
|
|
if r.lot_product:
|
|
val['lot_product'] = r.lot_product.id
|
|
val['lot_qt'] = r.lot_qt
|
|
val['lot_quantity'] = r.lot_quantity
|
|
if r.lot_unit:
|
|
val['lot_unit'] = r.lot_unit.id
|
|
if r.lot_unit_line:
|
|
val['lot_unit_line'] = r.lot_unit_line.id
|
|
val['lot_gross_quantity'] = r.lot_gross_quantity
|
|
val['lot_premium'] = r.lot_premium
|
|
val['lot_price'] = r.lot_price
|
|
lp.append(val)
|
|
return lp
|
|
|
|
class LotRemove(Wizard):
|
|
"Remove lots"
|
|
__name__ = "lot.remove"
|
|
|
|
start = StateTransition()
|
|
|
|
@staticmethod
|
|
def _remove_warning_message(lot):
|
|
messages = []
|
|
if lot.sale_line:
|
|
messages.append("matched")
|
|
if (lot.lot_shipment_in or lot.lot_shipment_internal
|
|
or lot.lot_shipment_out):
|
|
messages.append("shipped")
|
|
if messages:
|
|
return (
|
|
"This physical lot is %s. By clicking yes, the physical lot "
|
|
"and its draft stock move will be deleted and the quantity "
|
|
"will be restored as open quantity."
|
|
% " and ".join(messages))
|
|
return None
|
|
|
|
def transition_start(self):
|
|
Lot = Pool().get('lot.lot')
|
|
Move = Pool().get('stock.move')
|
|
Warning = Pool().get('res.user.warning')
|
|
for r in self.records:
|
|
if r.id > 10000000 :
|
|
raise UserError("Trying to remove open lots!")
|
|
lot = Lot(r.id)
|
|
if lot.move and lot.move.state != 'draft':
|
|
raise UserError(
|
|
"Trying to remove a lot with a stock move that is not "
|
|
"draft!")
|
|
warning_message = self._remove_warning_message(lot)
|
|
if warning_message:
|
|
warning_name = Warning.format("Remove physical lot", [lot])
|
|
if Warning.check(warning_name):
|
|
raise LotRemoveWarning(warning_name, warning_message)
|
|
quantity = lot.get_current_quantity_converted()
|
|
shipment_origin = lot.lot_shipment_origin
|
|
lot_s = lot.getVlot_s() if lot.sale_line else None
|
|
#delete move first if exist
|
|
if lot.move:
|
|
Move.delete([lot.move])
|
|
if not lot.updateVirtualPart(quantity, shipment_origin, lot_s):
|
|
lot.createVirtualPart(quantity, shipment_origin, lot_s)
|
|
Lot.delete([lot])
|
|
return 'end'
|
|
|
|
def end(self):
|
|
return 'reload'
|
|
|
|
class LotInvoice(Wizard):
|
|
"Invoice lots"
|
|
__name__ = "lot.invoice"
|
|
|
|
start = StateTransition()
|
|
|
|
inv = StateView(
|
|
'lot.invoice.start',
|
|
'purchase_trade.invoice_lot_view_form', [
|
|
Button("Cancel", 'end', 'tryton-cancel'),
|
|
Button("Invoice", 'invoicing', 'tryton-ok', default=True),
|
|
])
|
|
|
|
invoicing = StateTransition()
|
|
|
|
message = StateView(
|
|
'purchase.create_prepayment.message',
|
|
'purchase_trade.create_prepayment_message_form',
|
|
[
|
|
Button('OK', 'end', 'tryton-ok'),
|
|
Button('See Invoice', 'see_invoice', 'tryton-go-next'),
|
|
]
|
|
)
|
|
|
|
see_invoice = StateAction('account_invoice.act_invoice_form')
|
|
|
|
def transition_start(self):
|
|
return 'inv'
|
|
|
|
def default_inv(self, fields):
|
|
lot_p = []
|
|
lot_s = []
|
|
fee_pur = []
|
|
pp_pur = []
|
|
fee_sale = []
|
|
pp_sale = []
|
|
act = 'prov'
|
|
line = None
|
|
sale_line = None
|
|
val = {}
|
|
Lot = Pool().get('lot.lot')
|
|
LotQt = Pool().get('lot.qt')
|
|
context = Transaction().context
|
|
ids = context.get('active_ids')
|
|
unit = None
|
|
for i in ids:
|
|
val = {}
|
|
lqts = LotQt.search(['id','=',i - 10000000])
|
|
if lqts:
|
|
lot = lqts[0].lot_p
|
|
else:
|
|
lot = Lot(i)
|
|
line = lot.line
|
|
if lot.line.purchase:
|
|
if lot.line.purchase.wb:
|
|
if lot.line.purchase.wb.qt_type in [e.quantity_type for e in lot.lot_hist]:
|
|
act = 'final'
|
|
val['lot'] = lot.id
|
|
val['lot_name'] = lot.lot_name
|
|
val['lot_state'] = lot.lot_state.id
|
|
val['lot_pur_inv_state'] = lot.lot_pur_inv_state
|
|
val['lot_sale_inv_state'] = lot.lot_sale_inv_state
|
|
val['lot_diff_quantity'] = Decimal(0)
|
|
val['lot_diff_price'] = Decimal(0)
|
|
val['lot_diff_amount'] = Decimal(0)
|
|
val['lot_quantity'] = lot.get_current_quantity_converted()
|
|
val['lot_price'] = lot.lot_price
|
|
val['lot_amount'] = lot.get_current_quantity_converted() * lot.lot_price if lot.lot_price else Decimal(0)
|
|
if lot.invoice_line_prov:
|
|
val['lot_diff_quantity'] = val['lot_quantity'] - Decimal(lot.invoice_line_prov.quantity)
|
|
val['lot_diff_price'] = val['lot_price'] - Decimal(lot.invoice_line_prov.unit_price)
|
|
val['lot_diff_amount'] = val['lot_amount'] - Decimal(lot.invoice_line_prov.amount)
|
|
val['lot_unit'] = line.unit.id #lot.lot_unit_line.id
|
|
unit = val['lot_unit']
|
|
val['lot_currency'] = lot.lot_price_ct_symbol
|
|
lot_p.append(val)
|
|
sale_line = lot.sale_line
|
|
val_s = val.copy() # ou utiliser deepcopy si certains champs sont des objets imbriqués
|
|
val_s['lot_price'] = lot.lot_price_sale
|
|
val_s['lot_amount'] = lot.get_current_quantity_converted() * lot.lot_price_sale if lot.lot_price_sale else Decimal(0)
|
|
val_s['lot_diff_quantity'] = 0
|
|
val_s['lot_diff_price'] = 0
|
|
val_s['lot_diff_amount'] = 0
|
|
if lot.sale_invoice_line_prov:
|
|
provisional_quantity = (
|
|
Decimal(lot.sale_invoice_line_prov.quantity)
|
|
- Decimal(str(lot.sale_invoice_padding or 0)))
|
|
val_s['lot_diff_quantity'] = (
|
|
val_s['lot_quantity'] - provisional_quantity)
|
|
val_s['lot_diff_price'] = val_s['lot_price'] - Decimal(lot.sale_invoice_line_prov.unit_price)
|
|
provisional_amount = (
|
|
provisional_quantity
|
|
* Decimal(lot.sale_invoice_line_prov.unit_price))
|
|
val_s['lot_diff_amount'] = (
|
|
val_s['lot_amount'] - provisional_amount)
|
|
val_s['lot_currency'] = lot.lot_price_ct_symbol_sale
|
|
val_s['lot_unit'] = sale_line.unit.id if sale_line else None
|
|
lot_s.append(val_s)
|
|
if line:
|
|
if line.fees:
|
|
for f in line.fees:
|
|
if f.type == 'ordered':
|
|
val = {}
|
|
val['fee'] = f.id
|
|
val['fee_type'] = f.product.id
|
|
val['fee_quantity'] = f.quantity
|
|
val['fee_price'] = f.price
|
|
val['fee_unit'] = line.unit.id
|
|
val['fee_amount'] = f.amount
|
|
val['fee_landed_cost'] = f.fee_landed_cost
|
|
fee_pur.append(val)
|
|
if line.purchase:
|
|
Prep = Pool().get('account.invoice')
|
|
prep = Prep.search([('description','=','Prepayment'),('party','=',line.purchase.party.id),('state','!=','draft')])
|
|
if prep:
|
|
for p in prep:
|
|
for li in p.lines:
|
|
val = {}
|
|
val['inv'] = li.id
|
|
val['inv_amount'] = li.amount
|
|
pp_pur.append(val)
|
|
if sale_line:
|
|
if sale_line.fees:
|
|
for f in sale_line.fees:
|
|
if f.type == 'ordered':
|
|
val = {}
|
|
val['fee'] = f.id
|
|
val['fee_type'] = f.product.id
|
|
val['fee_quantity'] = f.quantity
|
|
val['fee_price'] = f.price
|
|
val['fee_unit'] = sale_line.unit.id
|
|
val['fee_amount'] = f.amount
|
|
val['fee_landed_cost'] = f.fee_landed_cost
|
|
fee_sale.append(val)
|
|
if sale_line.sale:
|
|
Prep = Pool().get('account.invoice')
|
|
prep = Prep.search([('description','=','Prepayment'),('party','=',sale_line.sale.party.id),('state','!=','draft')])
|
|
if prep:
|
|
for p in prep:
|
|
for li in p.lines:
|
|
val = {}
|
|
val['inv'] = li.id
|
|
val['inv_amount'] = li.amount
|
|
pp_sale.append(val)
|
|
return {
|
|
'lot_p': lot_p,
|
|
'lot_s': lot_s,
|
|
'unit': unit,
|
|
'fee_pur': fee_pur,
|
|
'pp_pur': pp_pur,
|
|
'fee_sale': fee_sale,
|
|
'pp_sale': pp_sale,
|
|
'action': act
|
|
}
|
|
|
|
def transition_invoicing(self):
|
|
Lot = Pool().get('lot.lot')
|
|
Purchase = Pool().get('purchase.purchase')
|
|
Sale = Pool().get('sale.sale')
|
|
lots = []
|
|
purchases = []
|
|
sales = []
|
|
action = self.inv.action
|
|
for r in self.records:
|
|
purchase = r.r_line.purchase if r.r_line else None
|
|
sale = r.r_sale_line.sale if r.r_sale_line else None
|
|
if purchase and purchase not in purchases:
|
|
purchases.append(purchase)
|
|
if sale and sale not in sales:
|
|
sales.append(sale)
|
|
lot = Lot(r.r_lot_p)
|
|
# if lot.move == None:
|
|
# Warning = Pool().get('res.user.warning')
|
|
# warning_name = Warning.format("Lot not confirmed", [])
|
|
# if Warning.check(warning_name):
|
|
# raise QtWarning(warning_name,
|
|
# "Lot not confirmed, click yes to confirm and invoice")
|
|
# continue
|
|
if self.inv.type == 'purchase' and lot.invoice_line:
|
|
continue
|
|
if self.inv.type == 'sale' and lot.sale_invoice_line:
|
|
continue
|
|
lots.append(lot)
|
|
|
|
invoice_line = None
|
|
if self.inv.type == 'purchase':
|
|
Purchase._process_invoice(purchases, lots, action, self.inv.pp_pur)
|
|
for lot in lots:
|
|
lot = Lot(lot.id)
|
|
invoice_line = lot.invoice_line or lot.invoice_line_prov
|
|
if invoice_line:
|
|
break
|
|
else:
|
|
if sales:
|
|
self._apply_sale_invoice_padding(lots)
|
|
Sale._process_invoice(sales, lots, action, self.inv.pp_sale)
|
|
for lot in lots:
|
|
lot = Lot(lot.id)
|
|
invoice_line = lot.sale_invoice_line or lot.sale_invoice_line_prov
|
|
if invoice_line:
|
|
break
|
|
if not invoice_line:
|
|
raise UserError("No invoice line was generated from the selected lots.")
|
|
self.message.invoice = invoice_line.invoice
|
|
|
|
return 'message'
|
|
|
|
@classmethod
|
|
def _split_sale_padding(cls, padding, lots):
|
|
padding = Decimal(str(padding or 0))
|
|
if not padding or not lots:
|
|
return {}
|
|
share = padding / Decimal(len(lots))
|
|
return {lot.id: share for lot in lots}
|
|
|
|
def _apply_sale_invoice_padding(self, lots):
|
|
Lot = Pool().get('lot.lot')
|
|
if self.inv.type != 'sale' or self.inv.action != 'prov':
|
|
return
|
|
padding_by_lot = self._split_sale_padding(self.inv.sale_padding, lots)
|
|
for lot in lots:
|
|
lot.sale_invoice_padding = padding_by_lot.get(lot.id, Decimal(0))
|
|
Lot.save(lots)
|
|
|
|
def default_message(self, fields):
|
|
return {
|
|
'message': 'The invoice has been successfully created.',
|
|
}
|
|
|
|
def do_see_invoice(self, action):
|
|
action['views'].reverse() # pour ouvrir en form directement
|
|
logger.info("*************SEE_INVOICE******************:%s",self.message.invoice)
|
|
return action, {'res_id':self.message.invoice.id}
|
|
|
|
def end(self):
|
|
return 'reload'
|
|
|
|
class LotInvoiceStart(ModelView):
|
|
"Invoice physical lots"
|
|
__name__ = "lot.invoice.start"
|
|
|
|
invoice = fields.Many2One('account.invoice',"Invoice to add lines")
|
|
lot_p = fields.One2Many('lot.invoicing.lot','lis',"Lots",states={'invisible': Eval('type')=='sale'})
|
|
lot_s = fields.One2Many('lot.invoicing.lot','lis',"Lots",states={'invisible': Eval('type')=='purchase'})
|
|
fee_sale = fields.One2Many('lot.invoicing.fee','lfs',"Fees",states={'invisible': Eval('type')=='purchase'})
|
|
pp_sale = fields.One2Many('lot.invoicing.inv','lps',"Prepayments",states={'invisible': Eval('type')=='purchase'})
|
|
fee_pur = fields.One2Many('lot.invoicing.fee','lfs',"Fees",states={'invisible': Eval('type')=='sale'})
|
|
pp_pur = fields.One2Many('lot.invoicing.inv','lps',"Prepayments",states={'invisible': Eval('type')=='sale'})
|
|
unit = fields.Many2One('product.uom',"Unit",readonly=True)
|
|
action = fields.Selection([
|
|
('add', 'Add lines'),
|
|
('final', 'Final'),
|
|
('prov', 'Provisional'),
|
|
], 'Action', required=True)
|
|
|
|
type = fields.Selection([
|
|
('purchase', 'Purchase'),
|
|
('sale', 'Sale'),
|
|
], 'Type', required=True)
|
|
|
|
quantity = fields.Numeric("Quantity to invoice",digits='unit',readonly=True,states={'invisible': Eval('type')=='sale'})
|
|
amount = fields.Numeric("Amount to invoice",digits='unit',readonly=True,states={'invisible': Eval('type')=='sale'})
|
|
quantity_s = fields.Numeric("Quantity to invoice",digits='unit',readonly=True,states={'invisible': Eval('type')=='purchase'})
|
|
amount_s = fields.Numeric("Amount to invoice",digits='unit',readonly=True,states={'invisible': Eval('type')=='purchase'})
|
|
sale_padding = fields.Numeric("Global padding", digits='unit',
|
|
states={
|
|
'invisible': (Eval('type') == 'purchase') | (Eval('action') != 'prov'),
|
|
})
|
|
|
|
@fields.depends('type')
|
|
def on_change_with_action(self, name=None):
|
|
if self.lot_p and self.type == 'purchase':
|
|
purchase = getattr(getattr(self.lot_p[0].lot, 'line', None), 'purchase', None)
|
|
wb = getattr(purchase, 'wb', None)
|
|
if wb and wb.qt_type in [e.quantity_type for e in self.lot_p[0].lot.lot_hist]:
|
|
return 'final'
|
|
else:
|
|
return 'prov'
|
|
if self.lot_s and self.type == 'sale':
|
|
sale = getattr(getattr(self.lot_s[0].lot, 'sale_line', None), 'sale', None)
|
|
wb = getattr(sale, 'wb', None)
|
|
if wb and wb.qt_type in [e.quantity_type for e in self.lot_s[0].lot.lot_hist]:
|
|
return 'final'
|
|
else:
|
|
return 'prov'
|
|
|
|
@fields.depends('lot_p','type','quantity')
|
|
def on_change_with_quantity(self, name=None):
|
|
if self.lot_p and self.type == 'purchase':
|
|
quantity = Decimal(0)
|
|
for l in self.lot_p:
|
|
if l.lot.invoice_line_prov:
|
|
quantity += l.lot_diff_quantity
|
|
else:
|
|
quantity += l.lot_quantity
|
|
return quantity
|
|
|
|
@fields.depends('lot_p','type','amount')
|
|
def on_change_with_amount(self, name=None):
|
|
if self.lot_p and self.type == 'purchase':
|
|
amount = Decimal(0)
|
|
for l in self.lot_p:
|
|
if l.lot.invoice_line_prov:
|
|
amount += l.lot_diff_amount
|
|
else:
|
|
amount += l.lot_amount
|
|
return amount
|
|
|
|
@fields.depends('lot_s','type','quantity_s','sale_padding')
|
|
def on_change_with_quantity_s(self, name=None):
|
|
if self.lot_s and self.type == 'sale':
|
|
quantity = Decimal(0)
|
|
for l in self.lot_s:
|
|
if l.lot.sale_invoice_line_prov:
|
|
quantity += l.lot_diff_quantity
|
|
else:
|
|
quantity += l.lot_quantity
|
|
quantity += Decimal(str(self.sale_padding or 0))
|
|
return quantity
|
|
|
|
@fields.depends('lot_s','type','amount_s','sale_padding')
|
|
def on_change_with_amount_s(self, name=None):
|
|
if self.lot_s and self.type == 'sale':
|
|
amount = Decimal(0)
|
|
padding = Decimal(str(self.sale_padding or 0))
|
|
padding_share = (
|
|
padding / Decimal(len(self.lot_s))
|
|
if padding and self.lot_s else Decimal(0))
|
|
for l in self.lot_s:
|
|
if l.lot.sale_invoice_line_prov:
|
|
amount += l.lot_diff_amount
|
|
else:
|
|
amount += l.lot_amount
|
|
amount += padding_share * Decimal(str(l.lot_price or 0))
|
|
return amount
|
|
|
|
@classmethod
|
|
def default_action(cls):
|
|
return 'prov'
|
|
|
|
@classmethod
|
|
def default_type(cls):
|
|
return 'purchase'
|
|
|
|
class LotInvoicingFee(ModelView):
|
|
"Fees"
|
|
__name__ = "lot.invoicing.fee"
|
|
|
|
lfs = fields.Many2One('lot.invoice.start',"Invoicing")
|
|
fee = fields.Many2One('fee.fee',"Fee")
|
|
fee_type = fields.Many2One('product.product',"Fee type")
|
|
to_invoice = fields.Boolean("To invoice")
|
|
fee_unit = fields.Many2One('product.uom',"Unit",readonly=True)
|
|
fee_quantity = fields.Numeric("Qt",digits=(1,5),readonly=True)
|
|
fee_price = fields.Numeric("Price",digits=(1,4),readonly=True)
|
|
fee_amount = fields.Numeric("Amount",digits=(1,2),readonly=True)
|
|
fee_landed_cost = fields.Boolean("To inventory",readonly=True)
|
|
|
|
class LotInvoicingInv(ModelView):
|
|
"Fees"
|
|
__name__ = "lot.invoicing.inv"
|
|
|
|
lps = fields.Many2One('lot.invoice.start',"Invoicing")
|
|
inv = fields.Many2One('account.invoice.line',"Invoice lines")
|
|
inv_amount = fields.Numeric("Av.amount",digits=(1,2),readonly=True)
|
|
inv_amount_to_use = fields.Numeric("Amount to use",digits=(1,2))
|
|
|
|
class LotInvoicingLot(ModelView):
|
|
"Lots"
|
|
__name__ = "lot.invoicing.lot"
|
|
|
|
lis = fields.Many2One('lot.invoice.start',"Invoicing")
|
|
lot = fields.Many2One('lot.lot',"Lot")
|
|
lot_name = fields.Char("Name",readonly=True)
|
|
lot_state = fields.Many2One('lot.qt.type',"State")
|
|
lot_quantity = fields.Numeric("Qt",digits='unit',readonly=True)
|
|
lot_diff_quantity = fields.Numeric("Diff. qt",digits='unit',readonly=True)
|
|
lot_unit = fields.Many2One('product.uom',"Unit",readonly=True)
|
|
lot_price = fields.Numeric("Price",digits='unit',readonly=True)
|
|
lot_diff_price = fields.Numeric("Diff. price",digits='unit',readonly=True)
|
|
lot_currency = fields.Char("Curr")
|
|
lot_amount = fields.Numeric("Amt",digits='unit',readonly=True)
|
|
lot_diff_amount = fields.Numeric("Diff amt",digits='unit',readonly=True)
|
|
lot_pur_inv_state = fields.Selection([
|
|
(None, ''),
|
|
('prov', 'Prov'),
|
|
('invoiced', 'Final')
|
|
], 'Pur. inv')
|
|
lot_sale_inv_state = fields.Selection([
|
|
(None, ''),
|
|
('prov', 'Prov'),
|
|
('invoiced', 'Final')
|
|
], 'Sale inv')
|
|
|
|
@classmethod
|
|
def view_attributes(cls):
|
|
return super().view_attributes() + [
|
|
('/tree/field[@name="lot_diff_price"]', 'visual',
|
|
If(Eval('lot_diff_price') & (Eval('lot_diff_price', 0) < 0),'danger','success')),
|
|
('/tree/field[@name="lot_diff_quantity"]', 'visual',
|
|
If(Eval('lot_diff_quantity') & (Eval('lot_diff_quantity', 0) < 0),'danger','success')),
|
|
]
|
|
|
|
class LotFCR(ModelSQL, ModelView):
|
|
"Lot FCR"
|
|
__name__ = 'lot.fcr'
|
|
_rec_name = 'lot_name'
|
|
|
|
lot_tare = fields.Numeric("Tare")
|
|
lot_grade = fields.Char("Grade")
|
|
lot_staple = fields.Char("Staple")
|
|
lot_mic = fields.Char("Mic")
|
|
lot_gpt = fields.Char("Gpt")
|
|
lot_uhm = fields.Char("Uhm")
|
|
lot_color = fields.Char("Color")
|
|
lot_counterparty = fields.Many2One('party.party','Supplier')
|
|
lot_offer_date = fields.Date("Offer date")
|
|
lot_offer =fields.Char("Reference")
|
|
lot_product = fields.Many2One('product.product','Product')
|
|
lot_origin = fields.Many2One('country.country','Origin')
|
|
lot_name = fields.Char("Lot")
|
|
lot_qt = fields.Integer("Qt")
|
|
lot_price = fields.Numeric("Price",digits=(1,4))
|
|
lot_amount = fields.Numeric("Amount",digits=(1,2))
|
|
lot_premium = fields.Numeric("Prem/Disc", digits=(1,2))
|
|
lot_quantity = fields.Numeric("Net Qt",digits=(1,5),required=True)
|
|
lot_unit = fields.Many2One('product.uom', "Unit",required=True)
|
|
lot_unit_line = fields.Many2One('product.uom', "Base unit",required=True)
|
|
lot_gross_quantity = fields.Numeric("Gross Qt",digits=(1,5))
|
|
lot_linked = fields.Many2One('lot.lot', "Linked lot")
|
|
lot_create = fields.Boolean("Create")
|
|
|
|
class LotWeighing(Wizard):
|
|
"Weighing"
|
|
__name__ = "lot.weighing"
|
|
|
|
start = StateTransition()
|
|
|
|
w = StateView(
|
|
'lot.weighing.start',
|
|
'purchase_trade.weighing_view_form', [
|
|
Button("Cancel", 'end', 'tryton-cancel'),
|
|
Button("Do", 'weighing', 'tryton-ok', default=True),
|
|
])
|
|
|
|
weighing = StateTransition()
|
|
|
|
def transition_start(self):
|
|
return 'w'
|
|
|
|
def default_w(self, fields):
|
|
lot_p = []
|
|
val = {}
|
|
Lot = Pool().get('lot.lot')
|
|
context = Transaction().context
|
|
ids = context.get('active_ids')
|
|
for i in ids:
|
|
if i > 10000000:
|
|
raise UserError("Trying to do weighing on open quantity!")
|
|
val = {}
|
|
lot = Lot(i)
|
|
val['lot'] = lot.id
|
|
val['lot_name'] = lot.lot_name
|
|
if lot.lot_shipment_in:
|
|
val['lot_shipment_in'] = lot.lot_shipment_in.id
|
|
if lot.lot_shipment_internal:
|
|
val['lot_shipment_internal'] = lot.lot_shipment_internal.id
|
|
if lot.lot_shipment_out:
|
|
val['lot_shipment_out'] = lot.lot_shipment_out.id
|
|
val['lot_product'] = lot.lot_product.id
|
|
val['lot_qt'] = lot.lot_qt
|
|
val['lot_quantity'] = lot.lot_quantity
|
|
val['lot_gross_quantity'] = lot.lot_gross_quantity
|
|
val['lot_unit'] = lot.lot_unit.id
|
|
val['lot_unit_line'] = lot.lot_unit_line.id
|
|
lot_p.append(val)
|
|
return {
|
|
'lot_p': lot_p,
|
|
}
|
|
|
|
def transition_weighing(self):
|
|
Warning = Pool().get('res.user.warning')
|
|
FeeLots = Pool().get('fee.lots')
|
|
Lot = Pool().get('lot.lot')
|
|
LotHist = Pool().get('lot.qt.hist')
|
|
affected_lines = []
|
|
with Lot.skip_quantity_consistency():
|
|
for l in self.w.lot_p:
|
|
quantity = l.lot.get_current_quantity_converted()
|
|
lhs = LotHist.search([('lot',"=",l.lot.id),('quantity_type','=',self.w.lot_state.id)])
|
|
if lhs:
|
|
lh = lhs[0]
|
|
else:
|
|
lh = LotHist()
|
|
lh.lot = l.lot
|
|
lh.quantity_type = self.w.lot_state
|
|
lh.quantity = round(l.lot_quantity_new,5)
|
|
lh.gross_quantity = round(l.lot_gross_quantity_new,5)
|
|
LotHist.save([lh])
|
|
l.lot.lot_qt = l.lot_qt
|
|
|
|
if self.w.lot_update_state :
|
|
l.lot.lot_state = self.w.lot_state
|
|
Lot.save([l.lot])
|
|
diff = round(Decimal(l.lot.get_current_quantity_converted() - quantity),5)
|
|
if diff != 0 :
|
|
#need to update virtual part
|
|
l.lot.updateVirtualPart(-diff,l.lot.lot_shipment_origin,l.lot.getVlot_s())
|
|
if l.lot.line:
|
|
affected_lines.append(l.lot.line)
|
|
if l.lot.sale_line:
|
|
affected_lines.append(l.lot.sale_line)
|
|
#adjuts fee ordered with new quantity
|
|
fees = FeeLots.search(['lot','=',l.lot.id])
|
|
for f in fees:
|
|
f.fee.sync_quantity_from_lots()
|
|
f.fee.adjust_purchase_values()
|
|
|
|
Lot.assert_lines_quantity_consistency(affected_lines)
|
|
return 'end'
|
|
|
|
def end(self):
|
|
return 'reload'
|
|
|
|
class LotWeighingStart(ModelView):
|
|
"Add weighing"
|
|
__name__ = "lot.weighing.start"
|
|
lot_p = fields.One2Many('lot.weighing.lot','lws',"Lots")
|
|
lot_state = fields.Many2One('lot.qt.type',"Weighing type",required=True)
|
|
lot_update_state = fields.Boolean("Update lot state to this weighing")
|
|
|
|
@classmethod
|
|
def default_lot_update_state(cls):
|
|
return True
|
|
|
|
class LotWeighingLot(ModelView):
|
|
"Lots"
|
|
__name__ = "lot.weighing.lot"
|
|
|
|
lws = fields.Many2One('lot.weighing.start',"Weighing")
|
|
lot = fields.Many2One('lot.lot',"Lot")
|
|
lot_name = fields.Char("Name",readonly=True)
|
|
lot_shipment_in = fields.Many2One('stock.shipment.in',"Shipment In")
|
|
lot_shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal")
|
|
lot_shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out")
|
|
lot_product = fields.Many2One('product.product',"Product",readonly=True)
|
|
lot_qt = fields.Integer("Qt")
|
|
lot_quantity = fields.Numeric("Net weight",digits=(1,5),readonly=True)
|
|
lot_gross_quantity = fields.Numeric("Gross weight",digits=(1,5),readonly=True)
|
|
lot_unit = fields.Many2One('product.uom',"Unit",readonly=True)
|
|
lot_unit_line = fields.Many2One('product.uom',"Unit",readonly=True)
|
|
lot_quantity_new = fields.Numeric("New net weight",digits=(1,5))
|
|
lot_gross_quantity_new = fields.Numeric("New gross weight",digits=(1,5))
|
|
lot_shipment_origin = fields.Function(
|
|
fields.Reference(
|
|
selection=[
|
|
("stock.shipment.in", "In"),
|
|
("stock.shipment.out", "Out"),
|
|
("stock.shipment.internal", "Internal"),
|
|
],
|
|
string="Shipment",
|
|
),
|
|
"get_shipment_origin",
|
|
)
|
|
|
|
def get_shipment_origin(self, name):
|
|
if self.lot_shipment_in:
|
|
return 'stock.shipment.in,' + str(self.lot_shipment_in.id)
|
|
elif self.lot_shipment_out:
|
|
return 'stock.shipment.out,' + str(self.lot_shipment_out.id)
|
|
elif self.lot_shipment_internal:
|
|
return 'stock.shipment.internal,' + str(self.lot_shipment_internal.id)
|
|
return None
|
|
|
|
class CreateContracts(Wizard):
|
|
"Create Contracts"
|
|
__name__ = "create.contracts"
|
|
|
|
start = StateTransition()
|
|
|
|
ct = StateView(
|
|
'contracts.start',
|
|
'purchase_trade.contracts_view_form', [
|
|
Button("Cancel", 'end', 'tryton-cancel'),
|
|
Button("Create", 'creating', 'tryton-ok', default=True),
|
|
])
|
|
|
|
creating = StateTransition()
|
|
|
|
#matching = StateAction('purchase_trade.matching_unit')
|
|
|
|
def transition_start(self):
|
|
return 'ct'
|
|
|
|
def default_ct(self, fields):
|
|
LotQt = Pool().get('lot.qt')
|
|
Lot = Pool().get('lot.lot')
|
|
context = Transaction().context
|
|
ids = context.get('active_ids')
|
|
unit = None
|
|
product = None
|
|
sh_in = None
|
|
sh_int = None
|
|
sh_out = None
|
|
lot = None
|
|
qt = Decimal(0)
|
|
type = None
|
|
shipment_in_values = set()
|
|
shipment_internal_values = set()
|
|
shipment_out_values = set()
|
|
for i in ids:
|
|
if i < 10000000:
|
|
raise UserError("You must create contract from an open quantity !")
|
|
l = LotQt(i - 10000000)
|
|
ll = Lot(l.lot_p if l.lot_p else l.lot_s)
|
|
current_type = "Sale" if l.lot_p else "Purchase"
|
|
if type and current_type != type:
|
|
raise UserError("You must select open quantities from the same side.")
|
|
type = current_type
|
|
if product and ll.lot_product.id != product:
|
|
raise UserError("You must select open quantities with the same product.")
|
|
if unit and l.lot_unit.id != unit:
|
|
raise UserError("You must select open quantities with the same unit.")
|
|
unit = l.lot_unit.id
|
|
qt += abs(Decimal(str(l.lot_quantity or 0)))
|
|
product = ll.lot_product.id
|
|
shipment_in_values.add(l.lot_shipment_in.id if l.lot_shipment_in else None)
|
|
shipment_internal_values.add(
|
|
l.lot_shipment_internal.id if l.lot_shipment_internal else None)
|
|
shipment_out_values.add(l.lot_shipment_out.id if l.lot_shipment_out else None)
|
|
if lot is None:
|
|
lot = ll.id
|
|
|
|
if len(shipment_in_values) == 1:
|
|
sh_in = next(iter(shipment_in_values))
|
|
if len(shipment_internal_values) == 1:
|
|
sh_int = next(iter(shipment_internal_values))
|
|
if len(shipment_out_values) == 1:
|
|
sh_out = next(iter(shipment_out_values))
|
|
|
|
return {
|
|
'quantity': qt,
|
|
'unit': unit,
|
|
'product': product,
|
|
'shipment_in': sh_in,
|
|
'shipment_internal': sh_int,
|
|
'shipment_out': sh_out,
|
|
'lot': lot,
|
|
'type': type,
|
|
}
|
|
|
|
def transition_creating(self):
|
|
ContractFactory.create_contracts(
|
|
self.ct.contracts,
|
|
type_=self.ct.type,
|
|
ct=self.ct,
|
|
)
|
|
return 'end'
|
|
|
|
def end(self):
|
|
return 'reload'
|
|
|
|
class ContractsStart(ModelView):
|
|
"Create Contracts"
|
|
__name__ = "contracts.start"
|
|
type = fields.Char("Type of contract to create",readonly=True)
|
|
matched = fields.Boolean("Matched")
|
|
product = fields.Many2One('product.product',"Product",readonly=True)
|
|
unit = fields.Many2One('product.uom',"Unit",readonly=True)
|
|
quantity = fields.Numeric("Current Qt",digits='unit',readonly=True)
|
|
shipment_in = fields.Many2One('stock.shipment.in',"Shipment In",readonly=True,states={'invisible': ~Eval('shipment_in')})
|
|
shipment_internal = fields.Many2One('stock.shipment.internal',"Shipment Internal",readonly=True,states={'invisible': ~Eval('shipment_internal')})
|
|
shipment_out = fields.Many2One('stock.shipment.out',"Shipment Out",readonly=True,states={'invisible': ~Eval('shipment_out')})
|
|
# shipment_origin = fields.Reference(
|
|
# selection=[
|
|
# ("stock.shipment.in", "In"),
|
|
# ("stock.shipment.out", "Out"),
|
|
# ("stock.shipment.internal", "Internal"),
|
|
# ],
|
|
# string="Shipment",
|
|
# )
|
|
lot = fields.Many2One('lot.lot',"Lot",readonly=True)#,states={'invisible': Eval('lqt')})
|
|
contracts = fields.One2Many('contract.detail','cd',"Contracts")
|
|
|
|
@classmethod
|
|
def default_matched(cls):
|
|
return True
|
|
|
|
class ContractDetail(ModelView):
|
|
|
|
"Contract Detail"
|
|
__name__ = "contract.detail"
|
|
|
|
category = fields.Integer("Category")
|
|
cd = fields.Many2One('contracts.start',"Contracts")
|
|
party = fields.Many2One('party.party',"Party", required=True,domain=[('categories.parent', 'child_of', Eval('category'))],depends=['category'])
|
|
currency = fields.Many2One('currency.currency',"Currency", required=True)
|
|
incoterm = fields.Many2One('incoterm.incoterm',"Incoterm", required=True)
|
|
quantity = fields.Numeric("Quantity",digits=(1,5), required=True)
|
|
unit = fields.Many2One('product.uom',"Unit", required=True)
|
|
qt_unit = fields.Many2One('product.uom',"Unit")
|
|
tol_min = fields.Numeric("Tol - in %")
|
|
tol_max = fields.Numeric("Tol + in %")
|
|
crop = fields.Many2One(
|
|
'purchase.crop', "Crop",
|
|
states={'invisible': Eval('company_visible')})
|
|
del_period = fields.Many2One('product.month',"Delivery Period")
|
|
from_del = fields.Date("From")
|
|
to_del = fields.Date("To")
|
|
price = fields.Numeric("Price", required=True,digits=(1,4),states={'invisible': Eval('price_type') != 'priced'})
|
|
price_type = price_type = fields.Selection([
|
|
('cash', 'Cash Price'),
|
|
('priced', 'Priced'),
|
|
('basis', 'Basis'),
|
|
], 'Price type', required=True)
|
|
currency_unit = fields.Selection('get_currency_unit',string="Curr/Unit")
|
|
reference = fields.Char("Reference")
|
|
from_location = fields.Many2One(
|
|
'stock.location', "From location",
|
|
domain=[('type', 'in', ['storage', 'supplier'])])
|
|
to_location = fields.Many2One(
|
|
'stock.location', "To location",
|
|
domain=[('type', 'in', ['storage', 'customer'])])
|
|
payment_term = fields.Many2One('account.invoice.payment_term',"Payment Term", required=True)
|
|
company_visible = fields.Boolean("Visible")
|
|
|
|
@classmethod
|
|
def default_category(cls):
|
|
lqt = cls._get_lqt_from_context()
|
|
if lqt and lqt.lot_p:
|
|
return 1
|
|
else:
|
|
return 2
|
|
|
|
@staticmethod
|
|
def get_currency_unit():
|
|
Currency = Pool().get('currency.currency')
|
|
Uom = Pool().get('product.uom')
|
|
|
|
result = []
|
|
currencies = Currency.search([('concatenate','=',True)])
|
|
units = Uom.search([('concatenate','=',True)])
|
|
result.append(("0_5","USC/lb"))
|
|
result.append(("0_37","USC/mt"))
|
|
for c in currencies:
|
|
for u in units:
|
|
key = "%s_%s" % (c.id, u.id)
|
|
value = "%s / %s" % (c.name, u.symbol or u.name)
|
|
result.append((key, value))
|
|
return result
|
|
|
|
@classmethod
|
|
def default_price_type(cls):
|
|
return 'priced'
|
|
|
|
@classmethod
|
|
def default_company_visible(cls):
|
|
Company = Pool().get('company.company')
|
|
company_id = Transaction().context.get('company')
|
|
if not company_id:
|
|
return False
|
|
company = Company(company_id)
|
|
return bool(company.party and company.party.name == 'MELYA')
|
|
|
|
@classmethod
|
|
def _get_lqt_from_context(cls):
|
|
ctx = Transaction().context
|
|
active_id = ctx.get('active_id') or next(iter(ctx.get('active_ids', [])), None)
|
|
if not active_id:
|
|
return None
|
|
LotQt = Pool().get('lot.qt')
|
|
try:
|
|
return LotQt(active_id - 10000000)
|
|
except Exception:
|
|
return None
|
|
|
|
@classmethod
|
|
def default_reference(cls):
|
|
lqt = cls._get_lqt_from_context()
|
|
if lqt and lqt.lot_p and getattr(lqt.lot_p.line.purchase, 'reference', None):
|
|
return lqt.lot_p.line.purchase.reference
|
|
if lqt and lqt.lot_s and getattr(lqt.lot_s.sale_line.sale, 'reference', None):
|
|
return lqt.lot_s.sale_line.sale.reference
|
|
|
|
@classmethod
|
|
def default_crop(cls):
|
|
lqt = cls._get_lqt_from_context()
|
|
if lqt and lqt.lot_p and getattr(lqt.lot_p.line.purchase, 'crop', None):
|
|
return lqt.lot_p.line.purchase.crop.id
|
|
if lqt and lqt.lot_s and getattr(lqt.lot_s.sale_line.sale, 'crop', None):
|
|
return lqt.lot_s.sale_line.sale.crop.id
|
|
|
|
@classmethod
|
|
def default_currency(cls):
|
|
lqt = cls._get_lqt_from_context()
|
|
if not lqt:
|
|
return
|
|
line = lqt.lot_p.line if lqt.lot_p else lqt.lot_s.sale_line
|
|
if getattr(line, 'enable_linked_currency', False):
|
|
return line.linked_currency.id
|
|
return getattr(line.purchase.currency if lqt.lot_p else line.sale.currency, 'id', None)
|
|
|
|
@classmethod
|
|
def default_unit(cls):
|
|
lqt = cls._get_lqt_from_context()
|
|
if not lqt:
|
|
return
|
|
line = lqt.lot_p.line if lqt.lot_p else lqt.lot_s.sale_line
|
|
if getattr(line, 'enable_linked_currency', False):
|
|
return line.linked_unit.id
|
|
return getattr(line.unit, 'id', None)
|
|
|
|
@classmethod
|
|
def default_currency_unit(cls):
|
|
lqt = cls._get_lqt_from_context()
|
|
if not lqt:
|
|
return
|
|
line = lqt.lot_p.line if lqt.lot_p else lqt.lot_s.sale_line
|
|
if getattr(line, 'enable_linked_currency', False):
|
|
return "0_%s" % line.linked_unit.id
|
|
contract = line.purchase if lqt.lot_p else line.sale
|
|
currency = getattr(contract, 'currency', None)
|
|
unit = getattr(line, 'unit', None)
|
|
if currency and unit:
|
|
return "%s_%s" % (currency.id, unit.id)
|
|
|
|
@classmethod
|
|
def default_tol_min(cls):
|
|
lqt = cls._get_lqt_from_context()
|
|
return getattr(lqt.lot_p.line.purchase if lqt.lot_p else lqt.lot_s.sale_line.sale, 'tol_min', None) if lqt else None
|
|
|
|
@classmethod
|
|
def default_tol_max(cls):
|
|
lqt = cls._get_lqt_from_context()
|
|
return getattr(lqt.lot_p.line.purchase if lqt.lot_p else lqt.lot_s.sale_line.sale, 'tol_max', None) if lqt else None
|
|
|
|
@fields.depends('party', '_parent_party.tol_min', '_parent_party.tol_max')
|
|
def on_change_party(self):
|
|
if self.party:
|
|
self.tol_min = self.party.tol_min
|
|
self.tol_max = self.party.tol_max
|
|
|
|
@classmethod
|
|
def default_del_period(cls):
|
|
lqt = cls._get_lqt_from_context()
|
|
if lqt and getattr(lqt.lot_p.line.purchase if lqt.lot_p else lqt.lot_s.sale_line.sale, 'del_period', None):
|
|
return lqt.lot_p.line.purchase.del_period.id if lqt.lot_p else lqt.lot_s.sale_line.sale.del_period.id
|
|
|
|
@classmethod
|
|
def _get_base_contract(cls):
|
|
lqt = cls._get_lqt_from_context()
|
|
if lqt and lqt.lot_p:
|
|
return lqt.lot_p.line.purchase, 'Sale'
|
|
if lqt and lqt.lot_s:
|
|
return lqt.lot_s.sale_line.sale, 'Purchase'
|
|
return None, None
|
|
|
|
@classmethod
|
|
def default_from_location(cls):
|
|
base, type_ = cls._get_base_contract()
|
|
from_location, _ = ContractFactory._get_mirror_locations(base, type_)
|
|
return getattr(from_location, 'id', None)
|
|
|
|
@classmethod
|
|
def default_to_location(cls):
|
|
base, type_ = cls._get_base_contract()
|
|
_, to_location = ContractFactory._get_mirror_locations(base, type_)
|
|
return getattr(to_location, 'id', None)
|
|
|
|
@fields.depends('del_period')
|
|
def on_change_del_period(self):
|
|
if self.del_period:
|
|
self.from_del = self.del_period.beg_date
|
|
self.to_del = self.del_period.end_date
|
|
|