# 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 functools from trytond.i18n import gettext from trytond.model import ModelSQL, ModelView, Workflow, fields from trytond.modules.stock.exceptions import AssignError from trytond.pool import Pool, PoolMeta from trytond.pyson import Eval, If from trytond.transaction import Transaction class Location(metaclass=PoolMeta): __name__ = 'stock.location' movable = fields.Boolean( "Movable", states={ 'invisible': Eval('type') != 'storage', }) assigned_by = fields.Reference( "Assigned by", 'get_assigned_by', readonly=True) @classmethod def _get_assigned_by(cls): "Return list of Model names for assigned_by Reference" return ['stock.shipment.internal', 'stock.shipment.out'] @classmethod def get_assigned_by(cls): pool = Pool() IrModel = pool.get('ir.model') get_name = IrModel.get_name models = cls._get_assigned_by() return [(None, '')] + [(m, get_name(m)) for m in models] @classmethod def deactivate_empty(cls, locations=None): if locations is None: locations = cls.search([ ('movable', '=', True), ]) to_deactivate = cls.get_empty_locations(locations) if to_deactivate: cls.write(cls.browse(to_deactivate), {'active': False}) @classmethod def forecast_location_move(cls, date): """Move temporary locations planned for the date and returns a method to restore the initial parent.""" pool = Pool() ShipmentInternal = pool.get('stock.shipment.internal') Location = pool.get('stock.location') Date = pool.get('ir.date') today = Date.today() shipments = ShipmentInternal.search([ ('locations', '!=', None), ('state', 'not in', ['cancelled', 'done']), ['OR', [ ('planned_date', '<=', date), ('planned_date', '>=', today), ('effective_date', '=', None), ], [ ('effective_date', '<=', date), ('effective_date', '>=', today), ], ], ], order=[('planned_date', 'ASC'), ('id', 'ASC')]) location_parents = {} locations = [] for shipment in shipments: for location in shipment.locations: location_parents.setdefault(location, location.parent) location.parent = shipment.to_location locations.append(location) Location.save(locations) def restore(): locations = [] for location, parent in location_parents.items(): location.parent = parent locations.append(location) Location.save(locations) return restore def clear_location_assignation(func): @functools.wraps(func) def wrapper(cls, shipments, *args, **kwargs): pool = Pool() Location = pool.get('stock.location') locations = [] for shipment in shipments: for location in shipment.locations: if location.assigned_by == shipment: locations.append(location) Location.write(locations, {'assigned_by': None}) return func(cls, shipments, *args, **kwargs) return wrapper class ShipmentInternal(metaclass=PoolMeta): __name__ = 'stock.shipment.internal' locations = fields.Many2Many( 'stock.shipment.internal-location', 'shipment', 'location', "Locations", domain=[ ('type', '=', 'storage'), ('movable', '=', True), If(Eval('state') == 'assigned', ('parent', 'child_of', Eval('from_location', -1)), ()), ], states={ 'readonly': (~Eval('state').in_(['request', 'draft']) | ~Eval('from_location') | ~Eval('to_location')), }) @classmethod @ModelView.button @Workflow.transition('draft') @clear_location_assignation def draft(cls, shipments): super().draft(shipments) @classmethod @ModelView.button @Workflow.transition('waiting') @clear_location_assignation def wait(cls, shipments): super().wait(shipments) @classmethod @Workflow.transition('assigned') def assign(cls, shipments): pool = Pool() Location = pool.get('stock.location') locations = {} for shipment in shipments: for location in shipment.locations: if not location.assigned_by: location.assigned_by = shipment if location in locations: raise AssignError( gettext('stock_location_move' '.msg_location_already_assigned') % { 'location': location.rec_name, 'assigned_by': locations[location].rec_name, }) locations[location] = location.assigned_by elif location.assigned_by != shipment: raise AssignError( gettext('stock_location_move' '.msg_location_already_assigned') % { 'location': location.rec_name, 'assigned_by': location.assigned_by.rec_name, }) if locations: Location.save(list(locations)) super().assign(shipments) @classmethod @ModelView.button @Workflow.transition('shipped') def ship(cls, shipments): pool = Pool() Location = pool.get('stock.location') to_write = [] for shipment in shipments: if not shipment.transit_location or not shipment.locations: continue to_write.append(list(shipment.locations)) to_write.append({ 'parent': shipment.transit_location.id, }) if to_write: Location.write(*to_write) super().ship(shipments) @classmethod @ModelView.button @Workflow.transition('done') @clear_location_assignation def do(cls, shipments): pool = Pool() Location = pool.get('stock.location') to_write = [] for shipment in shipments: if not shipment.locations: continue to_write.append(list(shipment.locations)) to_write.append({ 'parent': shipment.to_location.id, }) if to_write: Location.write(*to_write) super().do(shipments) @classmethod @ModelView.button @Workflow.transition('cancelled') @clear_location_assignation def cancel(cls, shipments): super().cancel(shipments) class ShipmentInternal_Location(ModelSQL): __name__ = 'stock.shipment.internal-location' shipment = fields.Many2One( 'stock.shipment.internal', "Shipment", required=True) location = fields.Many2One( 'stock.location', "Location", required=True, domain=[ ('type', '=', 'storage'), ('movable', '=', True), ]) def deactivate_empty_location(func): @functools.wraps(func) def wrapper(cls, shipments, *args, **kwargs): pool = Pool() Location = pool.get('stock.location') result = func(cls, shipments, *args, **kwargs) locations = set() for shipment in shipments: locations.update( move.from_location for move in shipment.moves if move.from_location.movable) Location.deactivate_empty(list(locations)) return result return wrapper class ShipmentOut(metaclass=PoolMeta): __name__ = 'stock.shipment.out' @classmethod @ModelView.button @Workflow.transition('done') @deactivate_empty_location def do(cls, shipments): super().do(shipments) class ShipmentInReturn(metaclass=PoolMeta): __name__ = 'stock.shipment.in.return' @classmethod @ModelView.button @Workflow.transition('done') @deactivate_empty_location def do(cls, shipments): super().do(shipments) class Supply(metaclass=PoolMeta): __name__ = 'stock.supply' def transition_create_(self): with Transaction().set_context(forecast_location_move=True): return super().transition_create_()