diff --git a/locale/es.po b/locale/es.po index f88ed07..4711188 100644 --- a/locale/es.po +++ b/locale/es.po @@ -234,6 +234,10 @@ msgctxt "field:stock.unit_load,warehouse:" msgid "Warehouse" msgstr "Almacén" +msgctxt "field:stock.unit_load,at_warehouse:" +msgid "Warehouse at date" +msgstr "Almacén a fecha" + msgctxt "field:stock.unit_load,warehouse_production:" msgid "Warehouse production" msgstr "Ub. producción almacén" diff --git a/script/set_at_warehouse.py b/script/set_at_warehouse.py new file mode 100644 index 0000000..6cadd83 --- /dev/null +++ b/script/set_at_warehouse.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +import argparse + + +def parse_commandline(): + options = {} + + parser = argparse.ArgumentParser(prog='update_bom_amount') + + parser.add_argument("-c", "--config", dest="configfile", metavar='FILE', + default=None, help="specify config file") + parser.add_argument("-d", "--database", dest="database_name", default=None, + metavar='DATABASE', help="specify the database name") + parser.add_argument("-cp", "--company", dest="company_id", default=None, + metavar='COMPANY', help="specify the company ID") + + options = parser.parse_args() + + if not options.database_name: + parser.error('Missing database option') + if not options.company_id: + parser.error('Missing company option') + return options + + +if __name__ == '__main__': + + options = parse_commandline() + + from trytond.transaction import Transaction + from trytond.pool import Pool + from trytond.config import config as CONFIG + from trytond.tools import grouped_slice, reduce_ids + + CONFIG.update_etc(options.configfile) + + Pool.start() + pool = Pool(options.database_name) + pool.init() + context = {'company': options.company_id} + + with Transaction().start(options.database_name, 1, context=context): + UnitLoad = pool.get('stock.unit_load') + unit_load = UnitLoad.__table__() + cursor = Transaction().connection.cursor() + + records = UnitLoad.search([ + ('at_warehouse', '=', None), + ['OR', + [ + ('production_state', '=', 'running'), + ('warehouse', '!=', None) + ], + ('available', '=', True) + ] + ]) + print('Total records: %s' % len(records)) + values = {} + for grouped_records in grouped_slice(records): + print('> Iterate grouped records') + for record in grouped_records: + wh = record.get_at_warehouse() + if wh: + values.setdefault(wh, []).append(record.id) + + print('Start to update ULs') + for warehouse, uls in values.items(): + print('Warehouse "%s"' % warehouse.rec_name) + cursor.execute(*unit_load.update( + columns=[unit_load.at_warehouse], + values=[warehouse.id], + where=reduce_ids(unit_load.id, uls))) + Transaction().commit() diff --git a/shipment.py b/shipment.py index 754ba02..e292118 100644 --- a/shipment.py +++ b/shipment.py @@ -14,6 +14,7 @@ __all__ = ['ShipmentOut', 'ShipmentInternal', 'ShipmentOutReturn', class ShipmentUnitLoadMixin(object): + unit_loads = fields.Function( fields.One2Many('stock.unit_load', None, 'Unit loads', states={ @@ -57,14 +58,31 @@ class ShipmentUnitLoadMixin(object): return [] -class ShipmentOut(ShipmentUnitLoadMixin, metaclass=PoolMeta): +class AtWarehouseMixin(object): + + @classmethod + def __setup__(cls): + super().__setup__() + add_remove_ = [ + ('at_warehouse', '=', Eval('warehouse', None)), + ('available', '=', True), + ('production_state', '=', 'done'), + ('state', '=', 'done') + ] + if cls.unit_loads.add_remove: + cls.unit_loads.add_remove += add_remove_ + else: + cls.unit_loads.add_remove = add_remove_ + cls.unit_loads.depends.append('warehouse') + + +class ShipmentOut(ShipmentUnitLoadMixin, AtWarehouseMixin, metaclass=PoolMeta): __name__ = 'stock.shipment.out' @classmethod def __setup__(cls): super().__setup__() cls.unit_loads.states['readonly'] |= Not(Bool(Eval('warehouse'))) - cls.unit_loads.depends.append('warehouse') def get_unit_loads(self, name=None): if not self.outgoing_moves: @@ -109,7 +127,8 @@ class ShipmentOut(ShipmentUnitLoadMixin, metaclass=PoolMeta): super()._sync_inventory_to_outgoing(shipments, quantity=quantity) -class ShipmentInternal(ShipmentUnitLoadMixin, metaclass=PoolMeta): +class ShipmentInternal(ShipmentUnitLoadMixin, AtWarehouseMixin, + metaclass=PoolMeta): __name__ = 'stock.shipment.internal' ul_quantity = fields.Function( @@ -239,9 +258,18 @@ class ShipmentOutReturn(ShipmentUnitLoadMixin, metaclass=PoolMeta): return ['inventory_moves', 'incoming_moves'] -class ShipmentInReturn(ShipmentUnitLoadMixin, metaclass=PoolMeta): +class ShipmentInReturn(ShipmentUnitLoadMixin, AtWarehouseMixin, + metaclass=PoolMeta): __name__ = 'stock.shipment.in.return' + warehouse = fields.Function(fields.Many2One('stock.location', 'Warehouse'), + 'on_change_with_warehouse') + + @fields.depends('from_location') + def on_change_with_warehouse(self, name=None): + if self.from_location and self.from_location.warehouse: + return self.from_location.warehouse.id + def get_unit_loads(self, name=None): if not self.moves: return [] diff --git a/stock.py b/stock.py index cceea77..428e9e2 100644 --- a/stock.py +++ b/stock.py @@ -6,6 +6,7 @@ from trytond.model import fields, Workflow from trytond.pyson import Bool from trytond.exceptions import UserError from trytond.i18n import gettext +from trytond.transaction import Transaction from itertools import groupby @@ -20,9 +21,11 @@ def set_unit_load_shipment(func): func(cls, moves) - if uls_to_save: + if uls_to_save and not Transaction().context.get( + 'skip_set_unit_load', False): UnitLoad.set_state(uls_to_save) UnitLoad.set_shipment(uls_to_save) + UnitLoad.set_at_warehouse(uls_to_save) return wrapper @@ -37,8 +40,10 @@ def set_unit_load_state(func): func(cls, moves) - if uls_to_save: + if uls_to_save and not Transaction().context.get( + 'skip_set_unit_load', False): UnitLoad.set_state(uls_to_save) + UnitLoad.set_at_warehouse(uls_to_save) return wrapper diff --git a/stock_lot.py b/stock_lot.py index 228480c..1cc3574 100644 --- a/stock_lot.py +++ b/stock_lot.py @@ -22,19 +22,23 @@ class UnitLoad(metaclass=PoolMeta): return new_move def get_last_moves(self, name=None, product_id=None, location_type=None, - at_date=None, check_start_date=False, **kwargs): + at_date=None, check_start_date=False, move_states=[], + return_ids=True, **kwargs): pool = Pool() Move = pool.get('stock.move') - last_moves = super().get_last_moves(name, product_id, location_type, - at_date, check_start_date, **kwargs) + last_moves = super().get_last_moves( + name=name, product_id=product_id, location_type=location_type, + at_date=at_date, check_start_date=check_start_date, + move_states=move_states, return_ids=return_ids, **kwargs) products_lots = kwargs.get('products_lots', None) if products_lots: - for move_id in last_moves: - move = Move(move_id) + for move in list(last_moves): + if return_ids: + move = Move(move) product_lots = products_lots.get(move.product, None) if product_lots and not product_lots.get(move.lot, 0): - last_moves.remove(move_id) + last_moves.remove(move.id if return_ids else move) return last_moves def _get_quantity_to_move(self, _grouped_moves, product, uom, diff --git a/tests/scenario_batch_drop.rst b/tests/scenario_batch_drop.rst index 2c5c950..7b31e56 100644 --- a/tests/scenario_batch_drop.rst +++ b/tests/scenario_batch_drop.rst @@ -166,7 +166,7 @@ Batch drop wizard:: True >>> drop_ul.form.location = new_prod >>> len(drop_ul.form.warehouse_productions) - 2 + 1 >>> drop_ul.form.start_date = datetime.datetime.now() - relativedelta(hours=2) >>> drop_ul.form.delay_ = datetime.timedelta(minutes=25) >>> drop_ul.form.start_date = datetime.datetime.now() - relativedelta(hours=20) diff --git a/tests/scenario_shipment_in_return.rst b/tests/scenario_shipment_in_return.rst index dd43a37..94cfac2 100644 --- a/tests/scenario_shipment_in_return.rst +++ b/tests/scenario_shipment_in_return.rst @@ -74,7 +74,8 @@ Create an unit load:: >>> unit_load.click('do') >>> len(unit_load.ul_moves) 1 - + >>> unit_load.at_warehouse == storage_loc.warehouse + True Create a shipment in return:: @@ -109,8 +110,14 @@ Add unit load to shipment:: >>> bool(unit_load.available) True >>> shipment_in_return.click('wait') + >>> unit_load.reload() + >>> bool(unit_load.at_warehouse) + True >>> shipment_in_return.click('assign_try') True + >>> unit_load.reload() + >>> bool(unit_load.at_warehouse) + False >>> try: ... shipment_in_return.click('done') ... except MoveFutureWarning as warning: @@ -127,6 +134,8 @@ Add unit load to shipment:: Check unit load:: >>> unit_load.reload() + >>> bool(unit_load.at_warehouse) + False >>> len(unit_load.ul_moves) 2 >>> unit_load.state diff --git a/tests/scenario_shipment_internal.rst b/tests/scenario_shipment_internal.rst index 2fcf7ad..366ff92 100644 --- a/tests/scenario_shipment_internal.rst +++ b/tests/scenario_shipment_internal.rst @@ -13,6 +13,7 @@ Imports:: ... get_company >>> today = datetime.date.today() >>> tomorrow = today + relativedelta(days=1) + >>> yesterday = today - relativedelta(days=1) >>> time_ = datetime.datetime.now().time() >>> time_ = time_.replace(microsecond=0) @@ -58,6 +59,10 @@ Get stock locations:: >>> wh2.name = 'Warehouse 2' >>> wh2.code = 'WH2' >>> wh2.save() + >>> storage_loc2 = wh2.storage_location + >>> storage_loc2.name = '%s 2' % storage_loc2.name + >>> storage_loc2.code = 'STO2' + >>> storage_loc2.save() Create an unit load:: @@ -65,8 +70,7 @@ Create an unit load:: >>> unit_load = UnitLoad() >>> unit_load.company != None True - >>> unit_load.start_date != None - True + >>> unit_load.start_date = datetime.datetime.now() - relativedelta(days=1) >>> unit_load.end_date != None True >>> unit_load.end_date = unit_load.start_date + relativedelta(minutes=5) @@ -84,6 +88,8 @@ Create an unit load:: >>> unit_load.save() >>> unit_load.code != None True + >>> unit_load.at_warehouse == None + True Add moves:: @@ -93,7 +99,7 @@ Add moves:: >>> len(unit_load.production_moves) 1 >>> move = unit_load.production_moves[0] - >>> move.planned_date == today + >>> move.planned_date == yesterday True >>> move.product == unit_load.product True @@ -141,15 +147,17 @@ Check computed fields:: 'Unit' >>> unit_load.save() >>> unit_load.click('do') + >>> unit_load.at_warehouse == warehouse_loc + True Create a shipment internal:: >>> ShipmentInternal = Model.get('stock.shipment.internal') >>> shipment_internal = ShipmentInternal() >>> shipment_internal.company = company - >>> shipment_internal.date_time_ = datetime.datetime.now() + relativedelta(minutes=10) + >>> shipment_internal.date_time_ = datetime.datetime.now() - relativedelta(minutes=10) >>> shipment_internal.from_location = unit_load.location - >>> shipment_internal.to_location = wh2.storage_location + >>> shipment_internal.to_location = storage_loc2 Add Unit load:: @@ -177,4 +185,5 @@ Check unit load state:: 'done' >>> unit_load.location.id == wh2.storage_location.id True - + >>> unit_load.at_warehouse == wh2 + True diff --git a/tests/scenario_stock_unit_load.rst b/tests/scenario_stock_unit_load.rst index a193ea9..0c4bdae 100644 --- a/tests/scenario_stock_unit_load.rst +++ b/tests/scenario_stock_unit_load.rst @@ -83,6 +83,8 @@ Create an unit load:: True >>> unit_load.shipment == None True + >>> unit_load.at_warehouse == None + True Add moves:: @@ -130,6 +132,8 @@ Check computed fields:: >>> len(unit_load.last_moves) == 1 True >>> unit_load.click('assign') + >>> unit_load.at_warehouse == warehouse_loc + True >>> unit_load.state 'assigned' >>> unit_load.moves[0].state @@ -236,6 +240,8 @@ Add Unit load:: >>> unit_load.available True + >>> unit_load.at_warehouse == storage_loc.warehouse + True >>> shipment_out.unit_loads.append(unit_load) >>> len(shipment_out.outgoing_moves) 1 @@ -249,11 +255,26 @@ Add Unit load:: 1 >>> shipment_out.inventory_moves[0].unit_load.id == unit_load.id True + >>> unit_load.reload() + >>> bool(unit_load.at_warehouse) + True >>> shipment_out.click('assign_try') True + >>> unit_load.reload() + >>> bool(unit_load.at_warehouse) + False >>> shipment_out.click('pick') + >>> unit_load.reload() + >>> bool(unit_load.at_warehouse) + False >>> shipment_out.click('pack') + >>> unit_load.reload() + >>> bool(unit_load.at_warehouse) + False >>> shipment_out.click('done') + >>> unit_load.reload() + >>> bool(unit_load.at_warehouse) + False Check unit load state:: @@ -326,4 +347,21 @@ Create another unit load and try move to another warehouse:: >>> unit_load2.click('do') # doctest: +ELLIPSIS Traceback (most recent call last): ... - trytond.exceptions.UserError: Cannot move or drop UL "..." to location "Storage Zone" because it is on Warehouse "Warehouse". - \ No newline at end of file + trytond.exceptions.UserError: Cannot move or drop UL "..." to location "Storage Zone" because it is on Warehouse "Warehouse". - + +Create an unit load in a warehouse and change it later:: + + >>> unit_load = UnitLoad() + >>> unit_load.end_date = unit_load.start_date + relativedelta(minutes=5) + >>> unit_load.warehouse = warehouse_loc + >>> unit_load.production_type = 'location' + >>> unit_load.production_location = warehouse_loc.production_location + >>> unit_load.product = product + >>> unit_load.cases_quantity = 5 + >>> unit_load.save() + >>> unit_load.at_warehouse == warehouse_loc + True + >>> unit_load.warehouse = wh2 + >>> unit_load.save() + >>> unit_load.at_warehouse == wh2 + True \ No newline at end of file diff --git a/unit_load.py b/unit_load.py index 49df404..3eed494 100644 --- a/unit_load.py +++ b/unit_load.py @@ -28,12 +28,6 @@ from .ir import cases_decimal cases_digits = (16, cases_decimal) -__all__ = ['UnitLoad', 'UnitLoadMove', 'MoveUnitLoad', - 'MoveUnitLoadStart', 'UnitLoadLabel', 'DropUnitLoadData', - 'DropUnitLoad', 'DropUnitLoadFailed', 'DropUnitLoadFailedProduct', - 'BatchDropUnitLoad', 'BatchDropUnitLoadData', 'BatchDropUnitLoadConfirm', - 'DropUnitLoadUL', 'DropUnitLoadEndDate', 'CaseLabel'] - MOVE_CHANGES = ['product', 'uom', 'production_type', 'production_location', 'warehouse', 'production_moves', 'moves', 'production_state', 'company', 'quantity', 'start_date', 'end_date', 'cases_quantity'] @@ -228,9 +222,8 @@ class UnitLoad(ModelSQL, ModelView): readonly=True, select=True) production_time = fields.Function(fields.TimeDelta('Production time'), 'get_production_time') - at_warehouse = fields.Function( - fields.Many2One('stock.location', 'Warehouse at date'), - 'get_at_warehouse') + at_warehouse = fields.Many2One('stock.location', 'Warehouse at date', + readonly=True) in_production = fields.Function( fields.Boolean('In production'), 'get_in_production', searcher='search_in_production') @@ -413,6 +406,7 @@ class UnitLoad(ModelSQL, ModelView): 'unit_load_sequence', company=values.get('company', default_company)).get() values['code_length'] = len(values['code']) + values.setdefault('at_warehouse', values.get('warehouse', None)) return super(UnitLoad, cls).create(vlist) @classmethod @@ -423,6 +417,16 @@ class UnitLoad(ModelSQL, ModelView): if values.get('code'): values = values.copy() values['code_length'] = len(values['code']) + if 'warehouse' in values: + wh_records = [r for r in records + if r.production_state == 'running' + and r.state == 'draft' + ] + wh_values = {'at_warehouse': values['warehouse']} + if wh_records == records: + values.update(wh_values) + else: + args.extend((wh_records, wh_values)) _deny_done = cls._deny_modify_done _deny_modify = cls._deny_modify_not_available vals_set = set(values) @@ -728,17 +732,18 @@ class UnitLoad(ModelSQL, ModelView): @classmethod def get_location(cls, records, name=None, product_id=None, type=None, - at_date=None): - pool = Pool() - Move = pool.get('stock.move') - + at_date=None, move_states=[], return_ids=True): value = dict.fromkeys(list(map(int, records)), None) for record in records: moves = record.get_last_moves(product_id=product_id, - location_type=type, at_date=at_date) + location_type=type, at_date=at_date, move_states=move_states, + return_ids=False) if moves: - value[record.id] = Move(moves[0]).to_location.id + location = moves[0].to_location + if return_ids: + location = location.id + value[record.id] = location return value def get_location_type(self, name=None): @@ -791,7 +796,8 @@ class UnitLoad(ModelSQL, ModelView): return max(m.end_date for m in _moves) def get_last_moves(self, name=None, product_id=None, location_type=None, - at_date=None, check_start_date=False, **kwargs): + at_date=None, check_start_date=False, move_states=[], + return_ids=True, **kwargs): if not self.moves: return [] @@ -810,19 +816,25 @@ class UnitLoad(ModelSQL, ModelView): x.end_date or x.start_date or datetime.datetime.min, x.start_date or datetime.datetime.min)[::tup_rev], reverse=True): + if move_states and move.state not in move_states: + continue if move.state == 'cancelled': continue if location_type and location_type != move.to_location.type: continue max_date = checked_date(move) location_id = move.to_location.id - if at_date and at_date < checked_date(move): + if at_date and checked_date(move) and at_date < checked_date(move): continue break - return [m.id for m in self.moves if - (check_start_date and m.start_date or m.end_date) == max_date and - m.to_location.id == location_id] + moves = [m for m in self.moves + if (check_start_date and m.start_date or m.end_date) == max_date + and m.to_location.id == location_id + ] + if return_ids: + moves = list(map(int, moves)) + return moves def get_production_type(self, name=None): if self.production_location: @@ -914,8 +926,7 @@ class UnitLoad(ModelSQL, ModelView): return new_moves def check_to_move(self, from_location, to_location, at_date): - pool = Pool() - Move = pool.get('stock.move') + if not from_location: raise UserError(gettext( 'stock_unit_load.msg_stock_unit_load_missing_location', @@ -935,9 +946,9 @@ class UnitLoad(ModelSQL, ModelView): to_location.type == 'production': # allow overlapped drops if self.drop_moves: - _last_moves = self.get_last_moves(location_type='storage') + _last_moves = self.get_last_moves(location_type='storage', + return_ids=False) if _last_moves: - _last_moves = Move.browse(_last_moves) _max_date = max(m.end_date for m in _last_moves) if _max_date > at_date: lzone = (dateutil.tz.gettz(self.company.timezone @@ -953,9 +964,6 @@ class UnitLoad(ModelSQL, ModelView): def _get_new_moves(self, default_values={}, location_type=None, cases_quantity=None, **kwargs): - pool = Pool() - Move = pool.get('stock.move') - default_values.update({ 'unit_load': self.id, 'origin': None, @@ -964,8 +972,8 @@ class UnitLoad(ModelSQL, ModelView): }) moves = [] - _last_moves = Move.browse(self.get_last_moves( - location_type=location_type, **kwargs)) + _last_moves = self.get_last_moves( + location_type=location_type, return_ids=False, **kwargs) if not default_values.get('from_location'): if not location_type: default_values['from_location'] = self.location.id @@ -1295,8 +1303,9 @@ class UnitLoad(ModelSQL, ModelView): move.unit_load.check_warehouse(move) if moves: - Move.do(moves) - cls.set_drop_state(records) + with Transaction().set_context(skip_set_unit_load=True): + Move.do(moves) + cls.set_drop_state(records, update_warehouse=False) move_changes = [] for record in records: if record.return_moves: @@ -1313,6 +1322,7 @@ class UnitLoad(ModelSQL, ModelView): # set done production state cls.set_production_state(records) cls.set_state(records) + cls.set_at_warehouse(records) @classmethod def set_production_state(cls, records, state='done'): @@ -1326,7 +1336,9 @@ class UnitLoad(ModelSQL, ModelView): }) @classmethod - def set_drop_state(cls, records): + def set_drop_state(cls, records, update_warehouse=True): + """Set dropping state in UL.""" + """update_warehouse: determines if at_warehouse must be computed""" to_drop = [] to_undrop = [] changes = { @@ -1353,6 +1365,8 @@ class UnitLoad(ModelSQL, ModelView): for key, values in changes.items(): if values: cls.write(values, {'dropped': key}) + if update_warehouse: + cls.set_at_warehouse(records) def _get_dropped_quantity(self, product=None, to_uom=None, done=False): if not self.drop_moves: @@ -1752,15 +1766,32 @@ class UnitLoad(ModelSQL, ModelView): to_location=move.to_location.rec_name, warehouse=from_wh[0].rec_name)) - def get_at_warehouse(self, name=None, date=None): - pool = Pool() - Location = pool.get('stock.location') + def get_at_warehouse(self, date=None): + if self.production_state == 'running': + if self.warehouse: + return self.warehouse + elif not self.available: + return None + to_location = self.get_location([self], + at_date=date or datetime.datetime.now(), + type='storage', + move_states=['assigned', 'done'], + return_ids=False)[self.id] + if to_location and to_location.warehouse: + return to_location.warehouse + return None - to_location = self.get_location([self], at_date=date - or datetime.datetime.now(), type='storage') - to_location = Location(to_location[self.id]) - if to_location.warehouse: - return to_location.warehouse.id + @classmethod + def set_at_warehouse(cls, records): + records = cls.browse(records) + to_save = [] + for record in records: + wh = record.get_at_warehouse() + if wh != record.at_warehouse: + record.at_warehouse = wh + to_save.append(record) + if to_save: + cls.save(to_save) @property def cases_digits(self): @@ -1895,8 +1926,8 @@ class MoveUnitLoadStart(ModelView): @fields.depends('planned_date', 'unit_load') def on_change_with_warehouse(self): if self.planned_date and self.unit_load: - return self.unit_load.get_at_warehouse( - date=self.planned_date) + wh = self.unit_load.get_at_warehouse(date=self.planned_date) + return wh.id if wh else None class MoveUnitLoad(Wizard): @@ -1980,12 +2011,8 @@ class DropUnitLoadData(ModelView): def on_change_start_date(self): self.warehouse_production = None if self.start_date and self.unit_load: - pool = Pool() - Location = pool.get('stock.location') - wh = self.unit_load.get_at_warehouse(date=self.start_date) if wh: - wh = Location(wh) self.warehouse_production = wh.production_location @@ -2428,16 +2455,12 @@ class BatchDropUnitLoadData(ModelView): @fields.depends('start_date', 'unit_loads') def on_change_with_warehouse_productions(self): - pool = Pool() - Location = pool.get('stock.location') - - whs = [] + whs = set() if self.start_date and self.unit_loads: for ul in self.unit_loads: wh = ul.get_at_warehouse(date=self.start_date) if wh: - whs.append(wh) - whs = Location.browse(whs) + whs.add(wh) return [wh.production_location.id for wh in whs if wh.production_location] return [] diff --git a/view/unit_load_form.xml b/view/unit_load_form.xml index a00d4ae..c0a8f39 100644 --- a/view/unit_load_form.xml +++ b/view/unit_load_form.xml @@ -52,6 +52,8 @@