Initial import from Docker volume
This commit is contained in:
805
modules/lot/lot.py
Executable file
805
modules/lot/lot.py
Executable file
@@ -0,0 +1,805 @@
|
||||
# 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
|
||||
from trytond.model import (ModelSQL, ModelView)
|
||||
from trytond.exceptions import UserWarning, UserError
|
||||
from trytond.transaction import Transaction, inactive_records
|
||||
from trytond.wizard import Button, StateTransition, StateView, Wizard, StateAction
|
||||
from decimal import getcontext, Decimal, ROUND_HALF_UP
|
||||
from sql.aggregate import Count, Max, Min, Sum, Avg, BoolOr
|
||||
from sql import Column, Literal
|
||||
from sql.functions import CurrentTimestamp, DateTrunc
|
||||
import datetime
|
||||
import logging
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Lot(ModelSQL, ModelView):
|
||||
"Lot"
|
||||
__name__ = 'lot.lot'
|
||||
_rec_name = 'lot_name'
|
||||
|
||||
lot_name = fields.Char("Lot")
|
||||
number = fields.Char("Number", readonly=True)
|
||||
lot_qt = fields.Float("Quantity",required=False)
|
||||
lot_unit = fields.Many2One('product.uom', "Unit",required=False)
|
||||
lot_product = fields.Many2One('product.product', "Product")
|
||||
lot_type = fields.Selection([
|
||||
('virtual', 'Open'),
|
||||
('physic', 'Physic'),
|
||||
('loss', 'Loss'),
|
||||
], 'Qt type')
|
||||
lot_status = fields.Selection([
|
||||
('forecast', 'Forecast'),
|
||||
('loading', 'Loading'),
|
||||
('transit', 'Transit'),
|
||||
('destination', 'Destination'),
|
||||
('stock', 'Stock'),
|
||||
('delivered', 'Delivered')
|
||||
], 'Where')
|
||||
lot_av = fields.Selection([
|
||||
('available', 'Available'),
|
||||
('reserved', 'Reserved'),
|
||||
('locked', 'Locked'),
|
||||
('prov', 'Prov. inv'),
|
||||
('invoiced', 'Invoiced')
|
||||
], 'State')
|
||||
lot_quantity = fields.Function(fields.Numeric("Net weight",digits='line_unit'),'get_current_quantity')
|
||||
lot_premium = fields.Numeric("Premium", digits='line_unit')
|
||||
lot_premium_sup = fields.Numeric("Sup prem", digits='line_unit')
|
||||
lot_premium_tpl = fields.Numeric("Tpl prem", digits='line_unit')
|
||||
lot_premium_sale = fields.Numeric("Prem/Disc", digits='line_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_gross_quantity = fields.Function(fields.Numeric("Gross weight",digits='line_unit'),'get_current_gross_quantity')
|
||||
lot_parent = fields.Many2One('lot.lot',"Parent")
|
||||
lot_childs = fields.One2Many('lot.lot','lot_parent',"Childs")
|
||||
lot_himself = fields.Many2One('lot.lot',"Lot")
|
||||
lot_container = fields.Char("Container")
|
||||
lot_unit_line = fields.Many2One('product.uom', "Unit",required=True)
|
||||
lot_price = fields.Function(fields.Numeric("Price", digits='line_unit'),'get_lot_price')
|
||||
lot_price_sale = fields.Function(fields.Numeric("Price", digits='line_unit'),'get_lot_sale_price')
|
||||
lot_state = fields.Many2One('lot.qt.type',"Qt state")
|
||||
lot_hist = fields.One2Many('lot.qt.hist','lot',"Qt hist")
|
||||
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')
|
||||
lot_price_ct_symbol = fields.Function(fields.Char(""),'get_price_ct_symbol')
|
||||
lot_price_ct_symbol_sale = fields.Function(fields.Char(""),'get_price_ct_symbol_sale')
|
||||
lot_price_ct_symbol_premium = fields.Function(fields.Char(""),'get_price_ct_symbol_premium')
|
||||
line_unit = fields.Function(fields.Many2One('product.uom',"Line unit"),'get_unit_line')
|
||||
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",
|
||||
)
|
||||
|
||||
lot_role = fields.Selection([
|
||||
('normal', 'Normal'),
|
||||
('technical', 'Technical'),
|
||||
], 'Role', required=True)
|
||||
|
||||
split_operations = fields.One2Many(
|
||||
'lot.split.merge', 'source_lot', 'Split/Merge Ops'
|
||||
)
|
||||
|
||||
split_graph = fields.Function(
|
||||
fields.Text('Split Graph'),
|
||||
'get_split_graph'
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def default_lot_role(cls):
|
||||
return 'normal'
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Technical helpers
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
def _check_split_allowed(self):
|
||||
if self.lot_role != 'normal':
|
||||
raise UserError('Only normal lots can be split.')
|
||||
if self.lot_type != 'physic':
|
||||
raise UserError('Only physical lots can be split.')
|
||||
if self.IsDelivered():
|
||||
raise UserError('Delivered lots cannot be split.')
|
||||
|
||||
def _create_technical_parent(self):
|
||||
Lot = Pool().get('lot.lot')
|
||||
parent = Lot(
|
||||
lot_name=f'TECH-{self.lot_name}',
|
||||
lot_role='technical',
|
||||
lot_type='virtual',
|
||||
lot_product=self.lot_product,
|
||||
lot_status=self.lot_status,
|
||||
lot_av='locked',
|
||||
)
|
||||
Lot.save([parent])
|
||||
return parent
|
||||
|
||||
def _clone_hist_with_ratio(self, ratio):
|
||||
LotQtHist = Pool().get('lot.qt.hist')
|
||||
hist = []
|
||||
for h in self.lot_hist:
|
||||
hist.append(
|
||||
LotQtHist(
|
||||
quantity_type=h.quantity_type,
|
||||
quantity=h.quantity * ratio,
|
||||
gross_quantity=h.gross_quantity * ratio,
|
||||
)
|
||||
)
|
||||
return hist
|
||||
|
||||
def split_by_weight(self, splits):
|
||||
Date = Pool().get('ir.date')
|
||||
self._check_split_allowed()
|
||||
|
||||
total_weight = sum(s['quantity'] for s in splits)
|
||||
lot_weight = self.get_current_quantity()
|
||||
|
||||
diff = (lot_weight - total_weight).quantize(Decimal('0.00001'))
|
||||
|
||||
if diff < 0:
|
||||
raise UserError('Split weight exceeds lot weight.')
|
||||
|
||||
if diff != 0:
|
||||
raise UserError(
|
||||
f'Split does not fully consume the lot weight.\n'
|
||||
f'Remaining: {diff}'
|
||||
)
|
||||
|
||||
Lot = Pool().get('lot.lot')
|
||||
Split = Pool().get('lot.split.merge')
|
||||
|
||||
children = []
|
||||
# Parent becomes technical
|
||||
self.lot_av = 'locked'
|
||||
self.lot_role = 'technical'
|
||||
self.lot_name = 'TECH-' + self.lot_name
|
||||
line = self.line
|
||||
self.line = None
|
||||
Lot.save([self])
|
||||
|
||||
for s in splits:
|
||||
ratio = s['quantity'] / lot_weight
|
||||
|
||||
lot_qt = None
|
||||
if self.lot_qt:
|
||||
lot_qt = (Decimal(self.lot_qt) * ratio).quantize(
|
||||
Decimal('0.00001'), rounding=ROUND_HALF_UP
|
||||
)
|
||||
|
||||
child = Lot(
|
||||
lot_name=s.get('name'),
|
||||
lot_role='normal',
|
||||
lot_type='physic',
|
||||
lot_qt=float(lot_qt) if lot_qt is not None else None,
|
||||
lot_unit=self.lot_unit,
|
||||
lot_product=self.lot_product,
|
||||
lot_status=self.lot_status,
|
||||
lot_av=self.lot_av,
|
||||
lot_unit_line=self.lot_unit_line,
|
||||
)
|
||||
|
||||
with Transaction().set_context(_skip_function_fields=True):
|
||||
Lot.save([child])
|
||||
child.set_current_quantity(s['quantity'], s['quantity'], 1)
|
||||
child.lot_parent = self
|
||||
child.line = line
|
||||
Lot.save([child])
|
||||
|
||||
children.append(child)
|
||||
|
||||
ops = []
|
||||
for c in children:
|
||||
ops.append(
|
||||
Split(
|
||||
operation='split',
|
||||
source_lot=self,
|
||||
target_lot=c,
|
||||
lot_qt=c.lot_qt,
|
||||
quantity=c.get_current_quantity(),
|
||||
operation_date=Date.today(),
|
||||
)
|
||||
)
|
||||
Split.save(ops)
|
||||
|
||||
return children
|
||||
|
||||
@classmethod
|
||||
def merge_lots(cls, lots):
|
||||
Date = Pool().get('ir.date')
|
||||
if len(lots) < 2:
|
||||
raise UserError('At least two lots are required for merge.')
|
||||
|
||||
# parent = lots[0].lot_parent
|
||||
# if not parent or parent.lot_role != 'technical':
|
||||
# raise UserError('Lots must share a technical parent.')
|
||||
|
||||
product = lots[0].lot_product
|
||||
for l in lots:
|
||||
if l.lot_product != product:
|
||||
raise UserError('Cannot merge lots of different products.')
|
||||
|
||||
Lot = Pool().get('lot.lot')
|
||||
Split = Pool().get('lot.split.merge')
|
||||
|
||||
total_qt = sum([l.lot_qt or 0 for l in lots])
|
||||
total_weight = sum([l.get_current_quantity() for l in lots])
|
||||
|
||||
ops = []
|
||||
parents_name = ""
|
||||
for l in lots:
|
||||
l.lot_av = 'locked'
|
||||
l.lot_role = 'technical'
|
||||
l.line = None
|
||||
parents_name += l.lot_name + "/"
|
||||
ops.append(
|
||||
Split(
|
||||
operation='merge',
|
||||
source_lot=l,
|
||||
target_lot=merged,
|
||||
lot_qt=l.lot_qt,
|
||||
quantity=l.get_current_quantity(),
|
||||
operation_date=Date.today(),
|
||||
)
|
||||
)
|
||||
|
||||
Lot.save(lots)
|
||||
Split.save(ops)
|
||||
|
||||
merged = Lot(
|
||||
lot_name='MERGE-' + parents_name,
|
||||
lot_role='normal',
|
||||
lot_type='physic',
|
||||
lot_product=product,
|
||||
lot_qt=total_qt,
|
||||
lot_unit=lots[0].lot_unit,
|
||||
lot_unit_line=lots[0].lot_unit_line,
|
||||
)
|
||||
|
||||
merged.set_current_quantity(total_weight, total_weight, 1)
|
||||
Lot.save([merged])
|
||||
|
||||
return merged
|
||||
|
||||
def get_split_graph(self, name=None):
|
||||
Split = Pool().get('lot.split.merge')
|
||||
|
||||
nodes = {}
|
||||
edges = []
|
||||
|
||||
def add_lot_node(lot):
|
||||
if lot.id in nodes:
|
||||
return
|
||||
nodes[lot.id] = {
|
||||
'id': f'lot.lot {lot.id}',
|
||||
'label': f'{lot.lot_name}\n({lot.lot_role})',
|
||||
'data_model': 'lot.lot',
|
||||
'data_id': lot.id,
|
||||
'shape': 'box' if lot.lot_role == 'technical' else 'ellipse',
|
||||
'color': {
|
||||
'background': '#FFD966' if lot.lot_role == 'technical' else '#A7C7E7',
|
||||
'border': '#555',
|
||||
},
|
||||
}
|
||||
|
||||
def walk(lot):
|
||||
add_lot_node(lot)
|
||||
ops = Split.search([('source_lot', '=', lot.id)])
|
||||
for op in ops:
|
||||
tgt = op.target_lot
|
||||
add_lot_node(tgt)
|
||||
edges.append({
|
||||
'from': f'lot.lot {lot.id}',
|
||||
'to': f'lot.lot {tgt.id}',
|
||||
'label': op.operation if op.operation else '',
|
||||
'arrows': 'to',
|
||||
'color': {
|
||||
'color': '#267F82',
|
||||
'highlight': '#1D5F62',
|
||||
'hover': '#1D5F62'
|
||||
}
|
||||
})
|
||||
walk(tgt)
|
||||
|
||||
walk(self)
|
||||
|
||||
# Calcul des relations parent-enfant
|
||||
from collections import defaultdict
|
||||
|
||||
children_map = defaultdict(list)
|
||||
parent_map = {}
|
||||
|
||||
for edge in edges:
|
||||
from_id = edge['from']
|
||||
to_id = edge['to']
|
||||
children_map[from_id].append(to_id)
|
||||
parent_map[to_id] = from_id
|
||||
|
||||
# Trouver la racine (nœud sans parent)
|
||||
all_node_ids = {node['id'] for node in nodes.values()}
|
||||
root_candidates = all_node_ids - set(parent_map.keys())
|
||||
|
||||
if root_candidates:
|
||||
root_id = list(root_candidates)[0]
|
||||
else:
|
||||
root_id = f'lot.lot {self.id}'
|
||||
|
||||
# Assigner les niveaux hiérarchiques
|
||||
levels = {}
|
||||
|
||||
def assign_levels(node_id, level=0):
|
||||
if node_id in levels and levels[node_id] >= level:
|
||||
return
|
||||
levels[node_id] = level
|
||||
for child in children_map.get(node_id, []):
|
||||
assign_levels(child, level + 1)
|
||||
|
||||
assign_levels(root_id)
|
||||
|
||||
# Grouper les nœuds par niveau
|
||||
by_level = defaultdict(list)
|
||||
for node_id, level in levels.items():
|
||||
by_level[level].append(node_id)
|
||||
|
||||
# Calculer les positions - IMPORTANT: utiliser des valeurs positives plus grandes
|
||||
# Calculer les positions pour un graphe HORIZONTAL
|
||||
X_SPACING = 180 # espace entre les niveaux (gauche → droite)
|
||||
Y_SPACING = 100 # espace entre les nœuds d’un même niveau
|
||||
|
||||
max_nodes_in_level = max(len(nodes) for nodes in by_level.values()) if by_level else 1
|
||||
total_height = max_nodes_in_level * Y_SPACING
|
||||
|
||||
for level, node_ids in by_level.items():
|
||||
level_height = len(node_ids) * Y_SPACING
|
||||
start_y = (total_height - level_height) / 2 # Centrer verticalement
|
||||
|
||||
for i, node_id in enumerate(node_ids):
|
||||
node_num = int(node_id.split()[-1])
|
||||
|
||||
# Niveau → X
|
||||
x_pos = level * X_SPACING + 300
|
||||
|
||||
# Répartition verticale
|
||||
y_pos = start_y + (i * Y_SPACING) + 100
|
||||
|
||||
nodes[node_num]['x'] = x_pos
|
||||
nodes[node_num]['y'] = y_pos
|
||||
nodes[node_num]['fixed'] = {
|
||||
'x': True,
|
||||
'y': True
|
||||
}
|
||||
|
||||
|
||||
# S'assurer que tous les nœuds ont des positions
|
||||
for node in nodes.values():
|
||||
if 'x' not in node:
|
||||
node['x'] = 0
|
||||
node['y'] = 0
|
||||
node['fixed'] = {'x': True, 'y': True}
|
||||
|
||||
data = {
|
||||
'nodes': list(nodes.values()),
|
||||
'edges': edges,
|
||||
'physics': {
|
||||
'enabled': False # Désactivé car positions fixes
|
||||
}
|
||||
}
|
||||
|
||||
return 'visjson:' + json.dumps(data)
|
||||
|
||||
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
|
||||
|
||||
def get_unit_line(self,name):
|
||||
if self.line:
|
||||
return self.line.unit
|
||||
return self.lot_unit_line
|
||||
def get_lot_inv(self):
|
||||
if self.invoice_line:
|
||||
return self.invoice_line.invoice.origin
|
||||
if self.invoice_line_prov:
|
||||
return self.invoice_line_prov.invoice.origin
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super(Lot, cls).__setup__()
|
||||
#cls._order.insert(0, ('lot_shipment', 'ASC'))
|
||||
|
||||
@classmethod
|
||||
def default_lot_qt(cls):
|
||||
return Decimal(0)
|
||||
|
||||
@classmethod
|
||||
def default_lot_av(cls):
|
||||
return 'available'
|
||||
|
||||
@classmethod
|
||||
def default_lot_status(cls):
|
||||
return 'forecast'
|
||||
|
||||
@classmethod
|
||||
def default_lot_type(cls):
|
||||
return 'physic'
|
||||
|
||||
@classmethod
|
||||
def default_lot_gross_quantity(cls):
|
||||
return Decimal(0)
|
||||
|
||||
@classmethod
|
||||
def default_lot_type(cls):
|
||||
return 'receive'
|
||||
|
||||
@classmethod
|
||||
def default_lot_state(cls):
|
||||
LotQtType = Pool().get('lot.qt.type')
|
||||
lqt = LotQtType.search([('sequence','=',1)])
|
||||
if lqt:
|
||||
return lqt[0].id
|
||||
|
||||
def get_price_ct_symbol(self,name):
|
||||
if self.line:
|
||||
return str(self.line.currency.symbol) + "/" + str(self.line.unit.symbol)
|
||||
|
||||
def get_price_ct_symbol_sale(self,name):
|
||||
if self.sale_line:
|
||||
return str(self.sale_line.currency.symbol) + "/" + str(self.sale_line.unit.symbol)
|
||||
|
||||
def get_price_ct_symbol_premium(self,name):
|
||||
if self.line:
|
||||
if self.line.enable_linked_currency:
|
||||
return str(self.line.linked_currency.name) + "/" + str(self.line.linked_unit.symbol)
|
||||
else:
|
||||
return str(self.line.currency.symbol) + "/" + str(self.line.unit.symbol)
|
||||
|
||||
def get_hist_quantity(self,seq):
|
||||
qt = Decimal(0)
|
||||
gross_qt = Decimal(0)
|
||||
if self.lot_state:
|
||||
if self.lot_hist:
|
||||
if seq != 0:
|
||||
st = seq
|
||||
else:
|
||||
st = self.lot_state.id
|
||||
lot = [e for e in self.lot_hist if e.quantity_type.id == st][0]
|
||||
qt = round(lot.quantity,5)
|
||||
gross_qt = round(lot.gross_quantity,5)
|
||||
return qt, gross_qt
|
||||
|
||||
def get_virtual_diff(self):
|
||||
Uom = Pool().get('product.uom')
|
||||
line = self.line if self.line else self.sale_line
|
||||
if line:
|
||||
if line.lots:
|
||||
physic_sum = Decimal(0)
|
||||
for l in line.lots:
|
||||
if l.lot_type == 'physic' :
|
||||
physic_sum += round(Decimal(Uom.compute_qty(Uom(l.lot_unit_line),float(l.get_current_quantity()),l.line.unit)),5)
|
||||
return line.quantity_theorical - physic_sum
|
||||
|
||||
def get_current_quantity(self,name=None):
|
||||
# if self.lot_type == 'physic':
|
||||
qt, gross_qt = self.get_hist_quantity(0)
|
||||
return qt
|
||||
# else:
|
||||
# return self.get_virtual_diff()
|
||||
|
||||
def get_current_quantity_converted(self,name=None):
|
||||
Uom = Pool().get('product.uom')
|
||||
unit = self.line.unit if self.line else self.sale_line.unit
|
||||
return round(Decimal(Uom.compute_qty(self.lot_unit_line, float(self.get_current_quantity()), unit)),5)
|
||||
|
||||
def get_current_gross_quantity(self,name=None):
|
||||
if self.lot_type == 'physic':
|
||||
qt, gross_qt = self.get_hist_quantity(0)
|
||||
return gross_qt
|
||||
else:
|
||||
return None
|
||||
|
||||
def add_quantity_to_hist(self,net,gross,qt_type):
|
||||
LotQtHist = Pool().get('lot.qt.hist')
|
||||
lqh = LotQtHist()
|
||||
lqh.quantity_type = qt_type
|
||||
lqh.quantity = net
|
||||
lqh.gross_quantity = gross
|
||||
lqh.lot = self
|
||||
return lqh
|
||||
|
||||
def set_current_quantity(self, net, gross, seq=1):
|
||||
LotQtType = Pool().get('lot.qt.type')
|
||||
lqtt = LotQtType.search([('sequence', '=', seq)])
|
||||
if not lqtt:
|
||||
return
|
||||
|
||||
lot_hist = list(getattr(self, 'lot_hist', []) or [])
|
||||
existing = [e for e in lot_hist if e.quantity_type == lqtt[0]]
|
||||
|
||||
if existing:
|
||||
hist = existing[0]
|
||||
hist.quantity = net
|
||||
hist.gross_quantity = gross
|
||||
else:
|
||||
lot_hist.append(self.add_quantity_to_hist(net, gross, lqtt[0]))
|
||||
|
||||
self.lot_hist = lot_hist
|
||||
self.lot_state = lqtt[0]
|
||||
|
||||
def get_unit(self,name):
|
||||
if self.line:
|
||||
return self.line.unit
|
||||
if self.sale_line:
|
||||
return self.sale_line.unit
|
||||
|
||||
def get_lot_price(self,name=None):
|
||||
price = Decimal(0)
|
||||
if self.line:
|
||||
if self.line.enable_linked_currency and self.line.linked_price and self.line.linked_currency and self.line.price_type == 'priced':
|
||||
return self.line.get_price_linked_currency((self.lot_premium if self.lot_premium else 0))
|
||||
else:
|
||||
return self.line.get_price((self.lot_premium if self.lot_premium else 0))
|
||||
|
||||
def get_lot_sale_price(self,name=None):
|
||||
price = Decimal(0)
|
||||
if self.sale_line:
|
||||
# if self.line.enable_linked_currency and self.line.linked_price and self.line.linked_currency and self.line.price_type == 'priced':
|
||||
# return self.line.get_price_linked_currency((self.lot_premium if self.lot_premium else 0))
|
||||
# else:
|
||||
return self.sale_line.get_price((self.lot_premium if self.lot_premium else 0))
|
||||
|
||||
def get_sale_amount(self,name):
|
||||
round_context = getcontext()
|
||||
round_context.rounding = ROUND_HALF_UP
|
||||
price = self.get_lot_sale_price()
|
||||
return None
|
||||
|
||||
def get_amount(self,name):
|
||||
round_context = getcontext()
|
||||
round_context.rounding = ROUND_HALF_UP
|
||||
price = self.get_lot_price()
|
||||
return None
|
||||
|
||||
class QtType(ModelSQL, ModelView):
|
||||
"Type"
|
||||
__name__ = 'lot.qt.type'
|
||||
name = fields.Char("Name")
|
||||
sequence = fields.Integer("Sequence")
|
||||
|
||||
class QtHist(ModelSQL, ModelView):
|
||||
"Quantities"
|
||||
__name__ = 'lot.qt.hist'
|
||||
|
||||
lot = fields.Many2One(
|
||||
'lot.lot', "Lot", ondelete='CASCADE',
|
||||
)
|
||||
quantity_type = fields.Many2One('lot.qt.type',"Type")
|
||||
quantity = fields.Numeric("Net weight",digits=(1,5))
|
||||
gross_quantity = fields.Numeric("Gross weight",digits=(1,5))
|
||||
|
||||
class LotSplitMerge(ModelSQL, ModelView):
|
||||
"Lot Split Merge"
|
||||
__name__ = 'lot.split.merge'
|
||||
|
||||
operation = fields.Selection([
|
||||
('split', 'Split'),
|
||||
('merge', 'Merge'),
|
||||
], 'Operation', required=True)
|
||||
|
||||
source_lot = fields.Many2One(
|
||||
'lot.lot', 'Source Lot', required=True, ondelete='CASCADE'
|
||||
)
|
||||
target_lot = fields.Many2One(
|
||||
'lot.lot', 'Target Lot', required=True, ondelete='CASCADE'
|
||||
)
|
||||
|
||||
lot_qt = fields.Float('Elements count')
|
||||
quantity = fields.Numeric('Weight', digits=(16, 5))
|
||||
|
||||
operation_date = fields.Date('Date', required=True)
|
||||
reversed_by = fields.Many2One(
|
||||
'lot.split.merge', 'Reversed By'
|
||||
)
|
||||
|
||||
class SplitLine(ModelView):
|
||||
"Split Line"
|
||||
__name__ = 'lot.split.wizard.line'
|
||||
|
||||
name = fields.Char('Lot name')
|
||||
lot_qt = fields.Float('Quantity')
|
||||
weight = fields.Numeric('Weight', digits=(16,5))
|
||||
|
||||
class SplitWizardStart(ModelView):
|
||||
__name__ = 'lot.split.wizard.start'
|
||||
|
||||
mode = fields.Selection([
|
||||
('equal', 'Equal parts'),
|
||||
('manual', 'Manual'),
|
||||
], 'Mode', required=True)
|
||||
|
||||
parts = fields.Integer(
|
||||
'Parts',
|
||||
states={'required': Eval('mode') == 'equal'}
|
||||
)
|
||||
|
||||
create_remainder = fields.Boolean(
|
||||
'Create remainder lot',
|
||||
help='If checked, a remainder lot will be created when the split '
|
||||
'does not exactly match the original weight.'
|
||||
)
|
||||
|
||||
lines = fields.One2Many(
|
||||
'lot.split.wizard.line',
|
||||
None,
|
||||
'Lines',
|
||||
states={'invisible': Eval('mode') != 'manual'}
|
||||
)
|
||||
|
||||
class SplitWizard(Wizard):
|
||||
"Lot Split Wizard"
|
||||
__name__ = 'lot.split.wizard'
|
||||
|
||||
start = StateView(
|
||||
'lot.split.wizard.start',
|
||||
'lot.split_wizard_start_view',
|
||||
[
|
||||
Button('Cancel', 'end', 'tryton-cancel'),
|
||||
Button('Split', 'split', 'tryton-ok', default=True),
|
||||
]
|
||||
)
|
||||
|
||||
split = StateTransition()
|
||||
def transition_split(self):
|
||||
Lot = Pool().get('lot.lot')
|
||||
lot = Lot(Transaction().context['active_id'])
|
||||
|
||||
if self.start.mode == 'equal':
|
||||
part = lot.get_current_quantity() / self.start.parts
|
||||
splits = [
|
||||
{'quantity': part, 'name': f'{lot.lot_name}-{i+1}'}
|
||||
for i in range(self.start.parts)
|
||||
]
|
||||
else:
|
||||
splits = [
|
||||
{'quantity': l.weight, 'lot_qt': l.lot_qt, 'name': l.name}
|
||||
for l in self.start.lines
|
||||
]
|
||||
|
||||
lot.split_by_weight(splits)
|
||||
return 'end'
|
||||
|
||||
class SplitWizard(Wizard):
|
||||
__name__ = 'lot.split.wizard'
|
||||
|
||||
start = StateView(
|
||||
'lot.split.wizard.start',
|
||||
'lot.split_wizard_start_view',
|
||||
[
|
||||
Button('Cancel', 'end', 'tryton-cancel'),
|
||||
Button('Split', 'split', 'tryton-ok', default=True),
|
||||
]
|
||||
)
|
||||
|
||||
split = StateTransition()
|
||||
|
||||
def transition_split(self):
|
||||
Lot = Pool().get('lot.lot')
|
||||
lot = Lot(Transaction().context['active_id'])
|
||||
|
||||
total_weight = lot.get_current_quantity().quantize(
|
||||
Decimal('0.00001'), rounding=ROUND_HALF_UP
|
||||
)
|
||||
|
||||
splits = []
|
||||
allocated = Decimal('0.0')
|
||||
|
||||
# -------------------------------------------------
|
||||
# EQUAL MODE
|
||||
# -------------------------------------------------
|
||||
if self.start.mode == 'equal':
|
||||
if not self.start.parts or self.start.parts <= 0:
|
||||
raise UserError('Number of parts must be greater than zero.')
|
||||
|
||||
base = (total_weight / self.start.parts).quantize(
|
||||
Decimal('0.00001'), rounding=ROUND_HALF_UP
|
||||
)
|
||||
|
||||
for i in range(self.start.parts):
|
||||
qty = base
|
||||
if i == self.start.parts - 1:
|
||||
qty = total_weight - allocated # absorbe le reste
|
||||
|
||||
splits.append({
|
||||
'quantity': qty,
|
||||
'lot_qt': lot.lot_qt / self.start.parts if lot.lot_qt else None,
|
||||
'name': f'{lot.lot_name}-{i + 1}',
|
||||
})
|
||||
allocated += qty
|
||||
|
||||
# -------------------------------------------------
|
||||
# MANUAL MODE
|
||||
# -------------------------------------------------
|
||||
else:
|
||||
for line in self.start.lines:
|
||||
if not line.weight or line.weight <= 0:
|
||||
raise UserError('Each line must have a positive weight.')
|
||||
|
||||
splits.append({
|
||||
'quantity': line.weight,
|
||||
'lot_qt': line.lot_qt,
|
||||
'name': line.name,
|
||||
})
|
||||
allocated += line.weight
|
||||
|
||||
diff = (total_weight - allocated).quantize(
|
||||
Decimal('0.00001'), rounding=ROUND_HALF_UP
|
||||
)
|
||||
|
||||
if diff != 0:
|
||||
if self.start.create_remainder and diff > 0:
|
||||
splits.append({
|
||||
'quantity': diff,
|
||||
'lot_qt': None,
|
||||
'name': f'{lot.lot_name}-REMAINDER',
|
||||
})
|
||||
else:
|
||||
raise UserError(
|
||||
f'Split does not match the lot weight.\n'
|
||||
f'Remaining difference: {diff}'
|
||||
)
|
||||
|
||||
lot.split_by_weight(splits)
|
||||
return 'end'
|
||||
|
||||
class MergeLotsStart(ModelView):
|
||||
"Merge Lots Start"
|
||||
__name__ = 'lot.merge.wizard.start'
|
||||
|
||||
confirm = fields.Boolean('Confirm merge')
|
||||
|
||||
|
||||
class MergeLots(Wizard):
|
||||
"Merge Lots"
|
||||
__name__ = 'lot.merge.wizard'
|
||||
|
||||
start = StateView(
|
||||
'lot.merge.wizard.start',
|
||||
'lot.lot_merge_wizard_start_view',
|
||||
[
|
||||
Button('Cancel', 'end', 'tryton-cancel'),
|
||||
Button('Merge', 'merge', 'tryton-ok', default=True),
|
||||
]
|
||||
)
|
||||
|
||||
merge = StateTransition()
|
||||
|
||||
def transition_merge(self):
|
||||
Lot = Pool().get('lot.lot')
|
||||
ids = Transaction().context['active_ids']
|
||||
lots = Lot.browse(ids)
|
||||
|
||||
Lot.merge_lots(lots)
|
||||
return 'end'
|
||||
Reference in New Issue
Block a user