From b86bc6e979355412297194c952dc5cb2bb2f61e0 Mon Sep 17 00:00:00 2001 From: William Henrotin Date: Wed, 16 Oct 2019 14:14:41 +0000 Subject: [PATCH] [FIX] mrp: cancelling and validating impact on planning The commit 0679d0d introduced a new planning mechanism for the workorder. Two issues have been instroduced since this patch. 1. Cancelling a workorder didn't free the time slot in the workcenter calendar 2. Validation a workorder sooner than the planned date didn't update the workcenter calendar to plan another production sooner. This commit update the date_planned_start and date_planned_finished at cancellation and validation to impact the workcenters calendar. To keep the original planning dates, they are now tracked This commit also fix the button_start() method that always rewrited the date_start even if the production had already been started. Task : 2082442 closes odoo/odoo#38947 X-original-commit: 4ca6c773a00320865142e4efb3645f36c54ce9a2 Signed-off-by: Simon Lejeune (sle) --- addons/mrp/models/mrp_workorder.py | 37 +++++++++--- addons/mrp/tests/test_workorder_operation.py | 60 +++++++++++++++++++- 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index 4cec3c8366a..989e7f485b6 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -69,13 +69,15 @@ class MrpWorkorder(models.Model): compute='_compute_dates_planned', inverse='_set_dates_planned', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, - store=True) + store=True, + tracking=True) date_planned_finished = fields.Datetime( 'Scheduled Date Finished', compute='_compute_dates_planned', inverse='_set_dates_planned', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, - store=True) + store=True, + tracking=True) date_start = fields.Datetime( 'Effective Start Date', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) @@ -541,14 +543,28 @@ class MrpWorkorder(models.Model): 'user_id': self.env.user.id, # FIXME sle: can be inconsistent with company_id 'company_id': self.company_id.id, }) - return self.write({'state': 'progress', - 'date_start': datetime.now(), - }) + if self.state == 'progress': + return True + else: + start_date = datetime.now() + vals = { + 'state': 'progress', + 'date_start': start_date, + 'date_planned_start': start_date, + } + if self.date_planned_finished < start_date: + vals['date_planned_finished'] = start_date + return self.write(vals) def button_finish(self): self.ensure_one() self.end_all() - return self.write({'state': 'done', 'date_finished': fields.Datetime.now()}) + end_date = datetime.now() + return self.write({ + 'state': 'done', + 'date_finished': end_date, + 'date_planned_finished': end_date + }) def end_previous(self, doall=False): """ @@ -595,14 +611,19 @@ class MrpWorkorder(models.Model): return True def action_cancel(self): + self.leave_id.unlink() return self.write({'state': 'cancel'}) def button_done(self): if any([x.state in ('done', 'cancel') for x in self]): raise UserError(_('A Manufacturing Order is already done or cancelled.')) self.end_all() - return self.write({'state': 'done', - 'date_finished': datetime.now()}) + end_date = datetime.now() + return self.write({ + 'state': 'done', + 'date_finished': end_date, + 'date_planned_finished': end_date, + }) def button_scrap(self): self.ensure_one() diff --git a/addons/mrp/tests/test_workorder_operation.py b/addons/mrp/tests/test_workorder_operation.py index 35619db2898..ee2c3bff2ad 100644 --- a/addons/mrp/tests/test_workorder_operation.py +++ b/addons/mrp/tests/test_workorder_operation.py @@ -410,6 +410,7 @@ class TestWorkOrderProcess(TestMrpCommon): # first operation of main bom finished_lot = self.env['stock.production.lot'].create({'product_id': man_order.product_id.id, 'company_id': self.env.company.id}) + door_wo_1.button_start() door_wo_1.write({ 'finished_lot_id': finished_lot.id, 'qty_producing': 48 @@ -419,6 +420,7 @@ class TestWorkOrderProcess(TestMrpCommon): # second operation of main bom self.assertEqual(door_wo_2.state, 'ready', "Workorder should be in ready state.") + door_wo_2.button_start() door_wo_2.record_production() self.assertEqual(door_wo_2.state, 'done', "Workorder should be in done state.") @@ -751,6 +753,7 @@ class TestWorkOrderProcess(TestMrpCommon): self.assertEqual(workorders[0].state, 'done') for workorder in workorders - workorders[0]: + workorder.button_start() self.assertEqual(workorder.finished_lot_id, serial_a) workorder.record_production() self.assertEqual(workorder.finished_lot_id, serial_b) @@ -797,12 +800,12 @@ class TestWorkOrderProcess(TestMrpCommon): mo.button_plan() workorder_0 = mo.workorder_ids[0] + workorder_0.button_start() workorder_0.record_production() workorder_0.record_production() - with self.assertRaises(UserError): - workorder_0.record_production() workorder_1 = mo.workorder_ids[1] + workorder_1.button_start() with Form(workorder_1) as wo: wo.finished_lot_id = lot_1 workorder_1.record_production() @@ -1043,6 +1046,59 @@ class TestWorkOrderProcess(TestMrpCommon): with self.assertRaises(UserError): mo.button_plan() + def test_planning_5(self): + """ Cancelling a production with workorders should free all reserved slot + in the related workcenters calendars """ + mo_form = Form(self.env['mrp.production']) + mo_form.product_id = self.product_4 + mo_form.bom_id = self.planning_bom + mo_form.product_qty = 1 + mo = mo_form.save() + mo.action_confirm() + mo.button_plan() + + mo.action_cancel() + self.assertEqual(mo.workorder_ids.mapped('date_start'), [False]) + self.assertEqual(mo.workorder_ids.mapped('date_finished'), [False]) + + def test_planning_6(self): + """ Marking a workorder as done before the theoretical date should update + the reservation slot in the calendar the be able to reserve the next + production sooner """ + self.workcenter_1.alternative_workcenter_ids = self.wc_alt_1 | self.wc_alt_2 + self.env['mrp.workcenter'].search([]).write({'tz': 'UTC'}) # compute all date in UTC + self.planning_bom.routing = self.env.ref("mrp.mrp_routing_0") + mo_form = Form(self.env['mrp.production']) + mo_form.product_id = self.product_4 + mo_form.bom_id = self.planning_bom + mo_form.product_qty = 1 + mo_form.date_start_wo = datetime(2019, 5, 13, 9, 0, 0, 0) + mo = mo_form.save() + mo.action_confirm() + mo.button_plan() + wo = mo.workorder_ids + self.assertAlmostEqual(wo.date_planned_start, datetime(2019, 5, 13, 9, 0, 0, 0), delta=timedelta(seconds=10)) + self.assertAlmostEqual(wo.date_planned_finished, datetime(2019, 5, 13, 9, 0, 0, 0) + timedelta(minutes=85.58), delta=timedelta(seconds=10)) + wo.button_start() + wo.record_production() + # Marking workorder as done should change the finished date + self.assertAlmostEqual(wo.date_finished, datetime.now(), delta=timedelta(seconds=10)) + self.assertAlmostEqual(wo.date_planned_finished, datetime.now(), delta=timedelta(seconds=10)) + + mo_form = Form(self.env['mrp.production']) + mo_form.product_id = self.product_4 + mo_form.bom_id = self.planning_bom + mo_form.product_qty = 1 + mo_form.date_start_wo = datetime(2019, 5, 13, 9, 0, 0, 0) + mo = mo_form.save() + mo.action_confirm() + mo.button_plan() + wo = mo.workorder_ids + wo.button_start() + self.assertAlmostEqual(wo.date_start, datetime.now(), delta=timedelta(seconds=10)) + self.assertAlmostEqual(wo.date_planned_start, datetime.now(), delta=timedelta(seconds=10)) + self.assertAlmostEqual(wo.date_planned_finished, datetime.now(), delta=timedelta(seconds=10)) + def test_plan_unplan_date(self): """ Testing planning a workorder then cancel it and then plan it again. The planned date must be the same the first time and the second time the