Files
tradon/modules/purchase_trade/lot_split_merge.py
2025-12-26 13:11:43 +00:00

307 lines
9.4 KiB
Python

from decimal import Decimal
from trytond.wizard import Wizard, StateView, StateTransition, Button
from trytond.model import ModelView, ModelSQL, fields
from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
from trytond.exceptions import UserError
import datetime
class Lot(metaclass=PoolMeta):
"Lot"
__name__ = 'lot.lot'
lot_role = fields.Selection([
('normal', 'Normal'),
('technical', 'Technical'),
], 'Role', required=True)
split_operations = fields.One2Many(
'lot.split.merge', 'source_lot', 'Split/Merge Ops'
)
@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
# ---------------------------------------------------------------------
# SPLIT
# ---------------------------------------------------------------------
def split_by_qt(self, splits):
"""
splits = [
{'lot_qt': 10, 'name': 'Lot A'},
{'lot_qt': 20, 'name': 'Lot B'},
]
"""
self._check_split_allowed()
total_qt = sum([s['lot_qt'] for s in splits])
if total_qt > self.lot_qt:
raise UserError('Split quantity exceeds lot quantity.')
Lot = Pool().get('lot.lot')
Split = Pool().get('lot.split.merge')
parent = self._create_technical_parent()
children = []
for s in splits:
ratio = Decimal(s['lot_qt']) / Decimal(self.lot_qt)
child = Lot(
lot_name=s.get('name'),
lot_parent=parent,
lot_role='normal',
lot_type='physic',
lot_qt=s['lot_qt'],
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,
lot_hist=self._clone_hist_with_ratio(ratio),
)
children.append(child)
Lot.save(children)
# Update original lot
self.lot_qt -= total_qt
self.lot_hist = self._clone_hist_with_ratio(
Decimal(self.lot_qt) / Decimal(self.lot_qt + total_qt)
)
Lot.save([self])
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(),
date=datetime.datetime.now(),
)
)
Split.save(ops)
return children
def split_by_weight(self, splits):
"""
splits = [
{'quantity': Decimal('1200'), 'name': 'Lot A'},
{'quantity': Decimal('800'), 'name': 'Lot B'},
]
"""
self._check_split_allowed()
total_weight = sum([s['quantity'] for s in splits])
if total_weight > self.get_current_quantity():
raise UserError('Split weight exceeds lot weight.')
Lot = Pool().get('lot.lot')
Split = Pool().get('lot.split.merge')
parent = self._create_technical_parent()
children = []
for s in splits:
ratio = s['quantity'] / self.get_current_quantity()
child = Lot(
lot_name=s.get('name'),
lot_parent=parent,
lot_role='normal',
lot_type='physic',
lot_qt=float(self.lot_qt * ratio) if self.lot_qt 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,
lot_hist=self._clone_hist_with_ratio(ratio),
)
children.append(child)
Lot.save(children)
ops = []
for c in children:
ops.append(
Split(
operation='split',
source_lot=self,
target_lot=c,
quantity=c.get_current_quantity(),
date=datetime.datetime.now(),
)
)
Split.save(ops)
self.lot_av = 'locked'
Lot.save([self])
return children
# ---------------------------------------------------------------------
# MERGE
# ---------------------------------------------------------------------
@classmethod
def merge_lots(cls, lots):
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])
merged = Lot(
lot_name='MERGE-' + parent.lot_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])
ops = []
for l in lots:
l.lot_av = 'locked'
ops.append(
Split(
operation='merge',
source_lot=l,
target_lot=merged,
lot_qt=l.lot_qt,
quantity=l.get_current_quantity(),
date=datetime.datetime.now(),
)
)
Lot.save(lots)
Split.save(ops)
return merged
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))
date = fields.DateTime('Date', required=True)
reversed_by = fields.Many2One(
'lot.split.merge', 'Reversed By'
)
class SplitWizardStart(ModelView):
"Split Wizard Start"
__name__ = 'lot.split.wizard.start'
mode = fields.Selection([
('qt', 'By quantity'),
('weight', 'By weight'),
], 'Mode', required=True)
class SplitWizard(Wizard):
"Lot Split Wizard"
__name____ = 'lot.split.wizard'
start = StateView(
'lot.split.wizard.start',
'purchase_trade.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 == 'qt':
lot.split_by_qt([
{'lot_qt': lot.lot_qt / 2, 'name': lot.lot_name + '-1'},
{'lot_qt': lot.lot_qt / 2, 'name': lot.lot_name + '-2'},
])
else:
q = lot.get_current_quantity()
lot.split_by_weight([
{'quantity': q / 2, 'name': lot.lot_name + '-1'},
{'quantity': q / 2, 'name': lot.lot_name + '-2'},
])
return 'end'