# This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import json from trytond.exceptions import UserError from trytond.i18n import gettext from trytond.model import Model, ModelStorage, ModelView, dualmethod, fields from trytond.model.exceptions import ValidationError from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval from trytond.transaction import Transaction from trytond.wizard import Button, StateTransition, StateView, Wizard class ShipmentUnassignMixin: '''Mixin to unassign quantity from assigned shipment moves''' __slots__ = () @dualmethod def unassign(cls, shipments, moves, quantities): ''' Unassign the quantity from the corresponding move of the shipments. ''' pool = Pool() Move = pool.get('stock.move') to_unassign = [] if not all(m.state == 'assigned' for m in moves): raise ValueError("Not assigned move") Move.draft(moves) for move, unassign_quantity in zip(moves, quantities): if not unassign_quantity: continue if unassign_quantity > move.quantity: raise ValueError( "Unassigned quantity greater than move quantity") if unassign_quantity == move.quantity: to_unassign.append(move) else: with Transaction().set_context(_stock_move_split=True): to_unassign.extend(Move.copy( [move], {'quantity': unassign_quantity})) move.quantity -= unassign_quantity Move.save(moves) Move.assign(moves) if to_unassign: cls.wait(shipments, to_unassign) class ShipmentInReturn(ShipmentUnassignMixin, metaclass=PoolMeta): __name__ = 'stock.shipment.in.return' @classmethod def __setup__(cls): super().__setup__() cls._buttons.update({ 'assign_manual_wizard': { 'invisible': Eval('state') != 'waiting', 'depends': ['state'], }, }) @classmethod @ModelView.button_action( 'stock_assign_manual.wizard_shipment_in_return_assign_manual') def assign_manual_wizard(cls, shipments): pass class ShipmentOut(ShipmentUnassignMixin, metaclass=PoolMeta): __name__ = 'stock.shipment.out' @classmethod def __setup__(cls): super().__setup__() cls._buttons.update({ 'assign_manual_wizard': { 'invisible': ((Eval('state') != 'waiting') | (Eval('warehouse_storage') == Eval('warehouse_output'))), 'depends': [ 'state', 'warehouse_storage', 'warehouse_output'], }, }) @classmethod @ModelView.button_action( 'stock_assign_manual.wizard_shipment_out_assign_manual') def assign_manual_wizard(cls, shipments): pass class ShipmentInternal(ShipmentUnassignMixin, metaclass=PoolMeta): __name__ = 'stock.shipment.internal' @classmethod def __setup__(cls): super().__setup__() cls._buttons.update({ 'assign_manual_wizard': { 'invisible': Eval('state') != 'waiting', 'depends': ['state'], }, }) @classmethod @ModelView.button_action( 'stock_assign_manual.wizard_shipment_internal_assign_manual') def assign_manual_wizard(cls, shipments): pass class ShipmentAssignManual(Wizard): __name__ = 'stock.shipment.assign.manual' start_state = 'next_' next_ = StateTransition() show = StateView('stock.shipment.assign.manual.show', 'stock_assign_manual.shipment_assign_manual_show_view_form', [ Button("Cancel", 'end', 'tryton-cancel'), Button("Skip", 'skip', 'tryton-forward', validate=False), Button("Assign", 'assign', 'tryton-ok', default=True), ]) skip = StateTransition() assign = StateTransition() def transition_next_(self): def next_move(): for move in self.record.assign_moves: if move.state == 'draft' and move not in self.show.skipped: self.show.move = move return move if self.show.skipped is None: self.show.skipped = [] if not next_move(): if all(m.state == 'assigned' for m in self.record.assign_moves): self.model.assign([self.record]) return 'end' return 'show' def default_show(self, fields): defaults = {} if 'skipped' in fields: defaults['skipped'] = [m.id for m in self.show.skipped] if 'move' in fields: defaults['move'] = self.show.move.id if 'unit' in fields: defaults['unit'] = self.show.move.unit.id if 'move_quantity' in fields: defaults['move_quantity'] = self.show.move.quantity return defaults def transition_skip(self): moves = list(self.show.skipped) moves.append(self.show.move) self.show.skipped = moves return 'next_' def transition_assign(self): self.show.assign() return 'next_' class ShipmentAssignManualShow(ModelView): __name__ = 'stock.shipment.assign.manual.show' skipped = fields.Many2Many( 'stock.move', None, None, "Skipped", readonly=True) move = fields.Many2One('stock.move', "Move", readonly=True) quantity = fields.Float( "Quantity", digits='unit', domain=['OR', ('quantity', '=', None), [ ('quantity', '>', 0), ('quantity', '<=', Eval('move_quantity', 0)), ], ], help="The maximum quantity to assign from the place.\n" "Leave empty for the full quantity of the move.") unit = fields.Many2One('product.uom', "Unit", readonly=True) move_quantity = fields.Float("Move Quantity", readonly=True) place = fields.Selection('get_places', "Place", required=True, sort=False) place_string = place.translated('place') @fields.depends('move') def get_places(self, with_childs=True, grouping=('product',)): pool = Pool() Date = pool.get('ir.date') Location = pool.get('stock.location') Move = pool.get('stock.move') Product = pool.get('product.product') Lang = pool.get('ir.lang') lang = Lang.get() if not self.move: return [] if with_childs: locations = Location.search([ ('parent', 'child_of', [self.move.from_location.id]), ]) else: locations = [self.move.from_location] location_ids = [loc.id for loc in locations] product_ids = [self.move.product.id] with Transaction().set_context(company=self.move.company.id): stock_date_end = Date.today() with Transaction().set_context( stock_date_end=stock_date_end, stock_assign=True, forecast=False, company=self.move.company.id): pbl = Product.products_by_location( location_ids, with_childs=False, grouping=grouping, grouping_filter=(product_ids,)) def get_key(move, location_id): key = (location_id,) for field in grouping: value = getattr(move, field) if isinstance(value, Model): value = value.id key += (value,) return key def match(key, pattern): for k, p in zip(key, pattern): if p is None or k == p: continue else: return False else: return True def get_name(key): move = Move() parts = [Location(key[0]).rec_name] for field, value in zip(grouping, key[1:]): setattr(move, field, value) value = getattr(move, field) if isinstance(value, ModelStorage): parts.append(value.rec_name) elif value: parts.append(str(value)) return ' - '.join(parts) # Prevent picking from the destination location try: locations.remove(self.move.to_location) except ValueError: pass # Try first to pick from source location locations.remove(self.move.from_location) locations.insert(0, self.move.from_location) places = [(None, '')] quantities = self.move.sort_quantities( pbl.items(), locations, grouping) for key, qty in quantities: move_key = get_key(self.move, key[0]) if qty > 0 and match(key, move_key): uom = self.move.product.default_uom quantity = lang.format_number_symbol( pbl[key], uom, digits=uom.digits) name = '%(name)s (%(quantity)s)' % { 'name': get_name(key), 'quantity': quantity } places.append((json.dumps(key), name)) return places def assign(self, grouping=('product',)): pool = Pool() Move = pool.get('stock.move') Lang = pool.get('ir.lang') lang = Lang.get() if self.quantity is not None: if not (0 <= self.quantity <= self.move.quantity): uom = self.move.product.default_uom raise ValidationError(gettext( 'stock_assign_manual.msg_invalid_quantity', quantity=lang.format_number( self.move_quantity, uom.digits))) quantity = self.move.unit.round(self.quantity) remainder = self.move.unit.round(self.move.quantity - quantity) self.move.quantity = quantity self.move.save() if remainder: Move.copy([self.move], {'quantity': remainder}) key = json.loads(self.place) values = self._apply(key, grouping) quantity = self.move.quantity Move.assign_try([self.move], with_childs=False, grouping=grouping) if self.move.state != 'assigned': # Restore initial values as assign_try may have saved the move for field, value in values.items(): setattr(self.move, field, value) self.move.save() if self.move.quantity == quantity: raise UserError(gettext( 'stock_assign_manual.msg_assign_failed', move=self.move.rec_name, place=self.place_string)) def _apply(self, key, grouping): """Update the move according to the key and return a dictionary with the initial values.""" values = {'from_location': self.move.from_location.id} location_id = key[0] self.move.from_location = location_id for field, value in zip(grouping, key[1:]): if value is not None and '.' not in field: values[field] = getattr(self.move, field) setattr(self.move, field, value) return values class ShipmentUnassignManual(Wizard): __name__ = 'stock.shipment.unassign.manual' start = StateTransition() show = StateView('stock.shipment.unassign.manual.show', 'stock_assign_manual.shipment_unassign_manual_show_view_form', [ Button("Cancel", 'end', 'tryton-cancel'), Button("Unassign", 'unassign', 'tryton-ok', default=True), ]) unassign = StateTransition() def transition_start(self): moves = self.record.assign_moves if any(m.state == 'assigned' for m in moves): return 'show' return 'end' def default_show(self, fields): moves = self.record.assign_moves move_ids = [m.id for m in moves if m.state == 'assigned'] return { 'assigned_moves': move_ids, } def transition_unassign(self): moves = [] quantities = [] for m in self.show.moves: moves.append(m.move) quantities.append(m.unassigned_quantity) self.record.unassign(moves, quantities) return 'end' class ShipmentAssignedMove(ModelView): __name__ = 'stock.shipment.assigned.move' move = fields.Many2One('stock.move', "Move", required=True) unassigned_quantity = fields.Float( "Unassigned Quantity", digits='unit', domain=['OR', ('unassigned_quantity', '=', None), [ ('unassigned_quantity', '>=', 0), ('unassigned_quantity', '<=', Eval('move_quantity', 0)), ], ], help="The quantity to unassign") assigned_quantity = fields.Float( "Assigned Quantity", digits='unit', domain=['OR', ('assigned_quantity', '=', None), [ ('assigned_quantity', '>=', 0), ('assigned_quantity', '<=', Eval('move_quantity', 0)), ], ], help="The quantity left assigned") unit = fields.Function( fields.Many2One('product.uom', "Unit"), 'on_change_with_unit') move_quantity = fields.Function( fields.Float("Move Quantity"), 'on_change_with_move_quantity') @staticmethod def default_unassigned_quantity(): return 0.0 @fields.depends('move', 'unassigned_quantity', 'assigned_quantity') def on_change_move(self, name=None): if self.move: self.assigned_quantity = self.move.quantity self.unassigned_quantity = 0.0 @fields.depends('assigned_quantity', 'move', 'unassigned_quantity', 'unit') def on_change_unassigned_quantity(self, name=None): if self.move and self.unassigned_quantity: self.assigned_quantity = self.unit.round( self.move.quantity - self.unassigned_quantity) @fields.depends('unassigned_quantity', 'move', 'assigned_quantity', 'unit') def on_change_assigned_quantity(self, name=None): if self.move and self.assigned_quantity: self.unassigned_quantity = self.unit.round( self.move.quantity - self.assigned_quantity) @fields.depends('move') def on_change_with_unit(self, name=None): return self.move.unit if self.move else None @fields.depends('move') def on_change_with_move_quantity(self, name=None): if self.move: return self.move.quantity class ShipmentUnassignManualShow(ModelView): __name__ = 'stock.shipment.unassign.manual.show' moves = fields.One2Many( 'stock.shipment.assigned.move', None, "Moves", domain=[('move.id', 'in', Eval('assigned_moves'))], help="The moves to unassign.") assigned_moves = fields.Many2Many( 'stock.move', None, None, "Assigned Moves")