Initial import from Docker volume
This commit is contained in:
307
modules/purchase_trade/lot_split_merge.py
Normal file
307
modules/purchase_trade/lot_split_merge.py
Normal file
@@ -0,0 +1,307 @@
|
||||
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'
|
||||
Reference in New Issue
Block a user