307 lines
9.4 KiB
Python
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' |