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'