diff --git a/CHANGELOG b/CHANGELOG index bfde56f94d..113e01c96b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add cron task to reschedule past shipments * Allow lost and found on inventory moves of shipments * Add waste locations on warehouses * Add warning to prevent move in the future diff --git a/doc/index.rst b/doc/index.rst index 6eb140bf4b..0c6de84b2d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -230,6 +230,13 @@ The customer shipment can be in one of this states: cancelled at any time. This also cancels all the moves. +Rescheduling Customer Shipments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is possible to setup a cron task to reschedule customer shipments that are +planned for the past. By default they are rescheduled to today. + + Internal Shipment ----------------- @@ -260,6 +267,12 @@ of moves. It can be in one of these states: cancelled at any time. This also cancels all the moves. +Rescheduling Internal Shipments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is possible to setup a cron task to reschedule internal shipments that are +planned for the past. By default they are rescheduled to today. + Inventory ********* diff --git a/ir.py b/ir.py index 0508a53b35..5238c64248 100644 --- a/ir.py +++ b/ir.py @@ -12,4 +12,10 @@ class Cron(metaclass=PoolMeta): cls.method.selection.extend([ ('product.product|recompute_cost_price_from_moves', "Recompute Cost Price from Moves"), + ('stock.shipment.out|reschedule', + "Reschedule Customer Shipments"), + ('stock.shipment.in.return|reschedule', + "Reschedule Supplier Return Shipments"), + ('stock.shipment.internal|reschedule', + "Reschedule Internal Shipments"), ]) diff --git a/shipment.py b/shipment.py index 62771fe851..eb945b691f 100644 --- a/shipment.py +++ b/shipment.py @@ -834,6 +834,22 @@ class ShipmentInReturn(ShipmentAssignMixin, Workflow, ModelSQL, ModelView): cls.assign(shipments) return success + @classmethod + def _get_reschedule_domain(cls, date): + return [ + ('state', '=', 'waiting'), + ('planned_date', '<', date), + ] + + @classmethod + def reschedule(cls, date=None): + pool = Pool() + Date = pool.get('ir.date') + if date is None: + date = Date.today() + shipments = cls.search(cls._get_reschedule_domain(date)) + cls.write(shipments, {'planned_date': date}) + class ShipmentOut(ShipmentAssignMixin, Workflow, ModelSQL, ModelView): "Customer Shipment" @@ -1407,6 +1423,22 @@ class ShipmentOut(ShipmentAssignMixin, Workflow, ModelSQL, ModelView): else: return False + @classmethod + def _get_reschedule_domain(cls, date): + return [ + ('state', '=', 'waiting'), + ('planned_date', '<', date), + ] + + @classmethod + def reschedule(cls, date=None): + pool = Pool() + Date = pool.get('ir.date') + if date is None: + date = Date.today() + shipments = cls.search(cls._get_reschedule_domain(date)) + cls.write(shipments, {'planned_date': date}) + class ShipmentOutReturn(ShipmentMixin, Workflow, ModelSQL, ModelView): "Customer Return Shipment" @@ -2446,6 +2478,26 @@ class ShipmentInternal(ShipmentAssignMixin, Workflow, ModelSQL, ModelView): if to_write: Move.write(*to_write) + @classmethod + def _get_reschedule_domain(cls, date): + return [ + ('state', '=', 'waiting'), + ('planned_date', '<', date), + ] + + @classmethod + def reschedule(cls, date=None): + pool = Pool() + Date = pool.get('ir.date') + if date is None: + date = Date.today() + shipments = cls.search(cls._get_reschedule_domain(date)) + for shipment in shipments: + shipment.planned_date = date + shipment.planned_start_date = ( + shipment.on_change_with_planned_start_date()) + cls.save(shipments) + class Address(metaclass=PoolMeta): __name__ = 'party.address' diff --git a/tests/scenario_stock_shipment_in_return.rst b/tests/scenario_stock_shipment_in_return.rst new file mode 100644 index 0000000000..46a078544c --- /dev/null +++ b/tests/scenario_stock_shipment_in_return.rst @@ -0,0 +1,81 @@ +================================= +Stock Shipment In Return Scenario +================================= + +Imports:: + + >>> import datetime + >>> from dateutil.relativedelta import relativedelta + >>> from decimal import Decimal + >>> from proteus import Model + >>> from trytond.tests.tools import activate_modules + >>> from trytond.modules.company.tests.tools import create_company, \ + ... get_company + >>> today = datetime.date.today() + >>> yesterday = today - relativedelta(days=1) + +Activate modules:: + + >>> config = activate_modules('stock') + +Create company:: + + >>> _ = create_company() + >>> company = get_company() + +Create supplier:: + + >>> Party = Model.get('party.party') + >>> supplier = Party(name='Supplier') + >>> supplier.save() + +Create product:: + + >>> ProductUom = Model.get('product.uom') + >>> ProductTemplate = Model.get('product.template') + >>> Product = Model.get('product.product') + >>> unit, = ProductUom.find([('name', '=', 'Unit')]) + >>> template = ProductTemplate() + >>> template.name = 'Product' + >>> template.default_uom = unit + >>> template.type = 'goods' + >>> template.list_price = Decimal('20') + >>> template.save() + >>> product, = template.products + +Get stock locations:: + + >>> Location = Model.get('stock.location') + >>> storage_loc, = Location.find([('code', '=', 'STO')]) + >>> supplier_loc, = Location.find([('code', '=', 'SUP')]) + +Create Shipment In:: + + >>> ShipmentInReturn = Model.get('stock.shipment.in.return') + >>> shipment_return = ShipmentInReturn() + >>> shipment_return.planned_date = yesterday + >>> shipment_return.supplier = supplier + >>> shipment_return.from_location = storage_loc + >>> move = shipment_return.moves.new() + >>> move.product = product + >>> move.uom =unit + >>> move.quantity = 1 + >>> move.from_location = storage_loc + >>> move.to_location = supplier_loc + >>> move.company = company + >>> move.unit_price = Decimal('1') + >>> move.currency = company.currency + >>> shipment_return.click('wait') + >>> shipment_return.state + 'waiting' + +Reschedule shipment:: + + >>> Cron = Model.get('ir.cron') + >>> cron = Cron(method='stock.shipment.in.return|reschedule') + >>> cron.interval_number = 1 + >>> cron.interval_type = 'months' + >>> cron.click('run_once') + >>> shipment_return.reload() + >>> shipment_return.planned_date == today + True diff --git a/tests/scenario_stock_shipment_internal.rst b/tests/scenario_stock_shipment_internal.rst index c3cc4d40ad..2649756b8a 100644 --- a/tests/scenario_stock_shipment_internal.rst +++ b/tests/scenario_stock_shipment_internal.rst @@ -197,3 +197,17 @@ Duplicate Internal Shipment:: >>> shipment_copy, = shipment.duplicate() >>> len(shipment_copy.moves) 1 + +Reschedule shipment:: + + >>> shipment_copy.planned_date = yesterday + >>> shipment_copy.click('wait') + >>> set_user(1) + >>> Cron = Model.get('ir.cron') + >>> cron = Cron(method='stock.shipment.internal|reschedule') + >>> cron.interval_number = 1 + >>> cron.interval_type = 'months' + >>> cron.click('run_once') + >>> shipment_copy.reload() + >>> shipment_copy.planned_date == today + True diff --git a/tests/scenario_stock_shipment_out.rst b/tests/scenario_stock_shipment_out.rst index c3f8b25a83..a5389f1930 100644 --- a/tests/scenario_stock_shipment_out.rst +++ b/tests/scenario_stock_shipment_out.rst @@ -256,3 +256,17 @@ Finish the shipment:: >>> inventory_move.effective_date == yesterday True +Reschedule shipment:: + + >>> shipment_copy, = shipment_out.duplicate() + >>> shipment_copy.planned_date = yesterday + >>> shipment_copy.click('wait') + >>> set_user(1) + >>> Cron = Model.get('ir.cron') + >>> cron = Cron(method='stock.shipment.out|reschedule') + >>> cron.interval_number = 1 + >>> cron.interval_type = 'months' + >>> cron.click('run_once') + >>> shipment_copy.reload() + >>> shipment_copy.planned_date == today + True diff --git a/tests/test_stock.py b/tests/test_stock.py index 74a6a9eb4f..59c414140e 100644 --- a/tests/test_stock.py +++ b/tests/test_stock.py @@ -1557,4 +1557,9 @@ def suite(): tearDown=doctest_teardown, encoding='utf-8', checker=doctest_checker, optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) + suite.addTests(doctest.DocFileSuite( + 'scenario_stock_shipment_in_return.rst', + tearDown=doctest_teardown, encoding='utf-8', + checker=doctest_checker, + optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) return suite