diff options
| -rw-r--r-- | addons/mrp/models/mrp_production.py | 61 | ||||
| -rw-r--r-- | addons/mrp/models/mrp_workorder.py | 20 | ||||
| -rw-r--r-- | addons/mrp/models/stock_rule.py | 2 | ||||
| -rw-r--r-- | addons/mrp/tests/test_workorder_operation.py | 7 | ||||
| -rw-r--r-- | addons/mrp/views/mrp_production_views.xml | 84 | ||||
| -rw-r--r-- | addons/sale_mrp/tests/test_sale_mrp_lead_time.py | 8 |
6 files changed, 125 insertions, 57 deletions
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 465cf23398d..ff757d69e5e 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -51,6 +51,12 @@ class MrpProduction(models.Model): location = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1).lot_stock_id return location and location.id or False + @api.model + def _get_default_date_planned_finished(self): + if self.env.context.get('default_date_planned_start'): + return fields.Datetime.to_datetime(self.env.context.get('default_date_planned_start')) + datetime.timedelta(hours=1) + return datetime.datetime.now() + datetime.timedelta(hours=1) + name = fields.Char( 'Reference', copy=False, readonly=True, default=lambda x: _('New')) origin = fields.Char( @@ -89,28 +95,24 @@ class MrpProduction(models.Model): states={'draft': [('readonly', False)]}, help="Location where the system will stock the finished products.") date_planned_start = fields.Datetime( - 'Deadline Start', copy=False, default=fields.Datetime.now, - index=True, required=True, - states={'draft': [('readonly', False)]}) + 'Planned Date', copy=False, default=fields.Datetime.now, + help="Date at which you plan to start the production.", + index=True, required=True, store=True) date_planned_finished = fields.Datetime( - 'Deadline End', copy=False, default=fields.Datetime.now, - index=True, - states={'draft': [('readonly', False)]}) - date_planned_start_wo = fields.Datetime( - 'Scheduled Start Date', compute='_compute_date_planned', - copy=False, store=True, - states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) - date_planned_finished_wo = fields.Datetime( - 'Scheduled End Date', compute='_compute_date_planned', - copy=False, store=True, - states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) + 'Planned End Date', + default=_get_default_date_planned_finished, + help="Date at which you plan to finish the production.", + copy=False, store=True) + date_deadline = fields.Datetime( + 'Deadline', copy=False, index=True, + help="Informative date allowing to define when the manufacturing order should be processed at the latest to fulfill delivery on time.") date_start = fields.Datetime('Start Date', copy=False, index=True, readonly=True) date_finished = fields.Datetime('End Date', copy=False, index=True, readonly=True) date_start_wo = fields.Datetime( - 'Planned Start Date', copy=False, readonly=True, - states={'draft': [('readonly', False)], 'confirmed': [('readonly', False)]}, - help="The work orders will be planned based on the availability of the work centers starting from " - "this date. If emtpy, the work orders are planned as soon as possible.") + 'Plan From', copy=False, readonly=True, + help="Work orders will be planned based on the availability of the work centers\ + starting from this date. If empty, the work orders will be planned as soon as possible.", + ) bom_id = fields.Many2one( 'mrp.bom', 'Bill of Material', readonly=True, states={'draft': [('readonly', False)]}, @@ -370,18 +372,6 @@ class MrpProduction(models.Model): for production in self: production.scrap_count = count_data.get(production.id, 0) - @api.depends('workorder_ids.date_planned_start', 'workorder_ids.date_planned_finished') - def _compute_date_planned(self): - for order in self: - date_planned_start_wo = date_planned_finished_wo = False - if order.workorder_ids: - start_dates = order.workorder_ids.filtered(lambda r: r.date_planned_start is not False).sorted(key=lambda r: r.date_planned_start) - date_planned_start_wo = start_dates[0].date_planned_start if start_dates else False - finished_dates = order.workorder_ids.filtered(lambda r: r.date_planned_finished is not False).sorted(key=lambda r: r.date_planned_finished) - date_planned_finished_wo = finished_dates[-1].date_planned_finished if finished_dates else False - order.date_planned_start_wo = date_planned_start_wo - order.date_planned_finished_wo = date_planned_finished_wo - _sql_constraints = [ ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'), ('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'), @@ -414,6 +404,8 @@ class MrpProduction(models.Model): 'date': self.date_planned_start, 'date_expected': self.date_planned_start, }) + if not self.routing_id: + self.date_planned_finished = self.date_planned_start + datetime.timedelta(hours=1) @api.onchange('bom_id', 'product_id', 'product_qty', 'product_uom_id') def _onchange_move_raw(self): @@ -451,6 +443,11 @@ class MrpProduction(models.Model): 'date_expected': vals['date_planned_start'], }) for production in self: + if 'date_planned_start' in vals: + if production.state in ['done', 'cancel']: + raise UserError(_('You cannot move a manufacturing order once it is cancelled or done.')) + if production.workorder_ids and not self.env.context.get('force_date', False): + raise UserError(_('You cannot move a planned manufacturing order.')) if 'move_raw_ids' in vals and production.state != 'draft': production.move_raw_ids.filtered(lambda m: m.state == 'draft')._action_confirm() return res @@ -715,6 +712,10 @@ class MrpProduction(models.Model): }) vals['leave_id'] = leave.id workorder.write(vals) + self.with_context(force_date=True).write({ + 'date_planned_start': self.workorder_ids[0].date_planned_start, + 'date_planned_finished': self.workorder_ids[-1].date_planned_finished + }) def button_unplan(self): if any(wo.state == 'done' for wo in self.workorder_ids): diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index f7be61d8325..2fd03cc7654 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import datetime +from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta from collections import defaultdict @@ -146,6 +146,14 @@ class MrpWorkorder(models.Model): 'date_to': date_to, }) + @api.onchange('date_planned_start') + def _onchange_date_planned_start(self): + if self.duration_expected: + time_delta = timedelta(minutes=self.duration_expected) + else: + time_delta = timedelta(hours=1) + self.update({'date_planned_finished': self.date_planned_start + time_delta}) + @api.onchange('finished_lot_id') def _onchange_finished_lot_id(self): """When the user changes the lot being currently produced, suggest @@ -256,6 +264,16 @@ class MrpWorkorder(models.Model): end_date = fields.Datetime.to_datetime(values.get('date_planned_finished')) or workorder.date_planned_finished if start_date and end_date and start_date > end_date: raise UserError(_('The planned end date of the work order cannot be prior to the planned start date, please correct this to save the work order.')) + # Update MO dates if the start date of the first WO or the + # finished date of the last WO is update. + if workorder == workorder.production_id.workorder_ids[0] and 'date_planned_start' in values: + workorder.production_id.with_context(force_date=True).write({ + 'date_planned_start': values['date_planned_start'] + }) + if workorder == workorder.production_id.workorder_ids[-1] and 'date_planned_finished' in values: + workorder.production_id.with_context(force_date=True).write({ + 'date_planned_finished': values['date_planned_finished'] + }) return super(MrpWorkorder, self).write(values) def _generate_wo_lines(self): diff --git a/addons/mrp/models/stock_rule.py b/addons/mrp/models/stock_rule.py index 5e7e9d1bf81..ff840682e1d 100644 --- a/addons/mrp/models/stock_rule.py +++ b/addons/mrp/models/stock_rule.py @@ -82,7 +82,7 @@ class StockRule(models.Model): 'location_src_id': self.location_src_id.id or self.picking_type_id.default_location_src_id.id or location_id.id, 'location_dest_id': location_id.id, 'bom_id': bom.id, - 'date_planned_start': fields.Datetime.to_string(self._get_date_planned(product_id, company_id, values)), + 'date_deadline': fields.Datetime.to_string(self._get_date_planned(product_id, company_id, values)), 'date_planned_finished': values['date_planned'], 'procurement_group_id': False, 'propagate_cancel': self.propagate_cancel, diff --git a/addons/mrp/tests/test_workorder_operation.py b/addons/mrp/tests/test_workorder_operation.py index 0c711d85302..d4a1f84076d 100644 --- a/addons/mrp/tests/test_workorder_operation.py +++ b/addons/mrp/tests/test_workorder_operation.py @@ -908,6 +908,7 @@ class TestWorkOrderProcess(TestMrpCommon): - calendar wc1 :[mo1][mo4] - calendar wc2 :[mo2 ][mo5 ] - calendar wc3 :[mo3 ][mo6 ]""" + planned_date = datetime(2019, 5, 13, 9, 0) self.workcenter_1.alternative_workcenter_ids = self.wc_alt_1 | self.wc_alt_2 workcenters = [self.wc_alt_2, self.wc_alt_1, self.workcenter_1] for i in range(3): @@ -916,11 +917,14 @@ class TestWorkOrderProcess(TestMrpCommon): mo_form.product_id = self.product_4 mo_form.bom_id = self.planning_bom mo_form.product_qty = 1 + mo_form.date_start_wo = planned_date mo = mo_form.save() mo.action_confirm() mo.button_plan() # Check that workcenters change self.assertEqual(mo.workorder_ids.workcenter_id, workcenters[i], "wrong workcenter %d" % i) + self.assertAlmostEqual(mo.date_planned_start, planned_date, delta=timedelta(seconds=10)) + self.assertAlmostEqual(mo.date_planned_start, mo.workorder_ids.date_planned_start, delta=timedelta(seconds=10)) for i in range(3): # Planning 3 more should choose workcenters in opposite order as @@ -931,11 +935,14 @@ class TestWorkOrderProcess(TestMrpCommon): mo_form.product_id = self.product_4 mo_form.bom_id = self.planning_bom mo_form.product_qty = 1 + mo_form.date_start_wo = planned_date mo = mo_form.save() mo.action_confirm() mo.button_plan() # Check that workcenters change self.assertEqual(mo.workorder_ids.workcenter_id, workcenters[i], "wrong workcenter %d" % i) + self.assertNotEqual(mo.date_planned_start, planned_date) + self.assertAlmostEqual(mo.date_planned_start, mo.workorder_ids.date_planned_start, delta=timedelta(seconds=10)) def test_planning_2(self): """ Plan some manufacturing orders with 2 workorders each diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index 8be9bcbe634..2bf94654de2 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -9,7 +9,6 @@ <tree decoration-bf="message_needaction==True" default_order="date_planned_start desc" decoration-info="state=='confirmed'" decoration-danger="date_planned_start<current_date and state not in ('done','cancel')" decoration-muted="state in ('done','cancel')" string="Manufacturing Orders"> <field name="message_needaction" invisible="1"/> <field name="name"/> - <field name="date_start_wo" optional="hide"/> <field name="date_planned_start" optional="show"/> <field name="product_id" optional="show"/> <field name="product_qty" sum="Total Qty" string="Quantity" optional="show"/> @@ -46,15 +45,15 @@ <button name="button_mark_done" attrs="{'invisible': [('state', '!=', 'to_close')]}" string="Mark as Done" type="object" class="oe_highlight"/> <button name="action_confirm" attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('is_locked', '=', False)]}" string="Mark as Todo" type="object" class="oe_highlight"/> <button name="action_assign" attrs="{'invisible': ['|', '|', ('is_locked', '=', False), ('state', 'in', ('draft', 'done', 'cancel')), ('reservation_state', '=', 'assigned')]}" string="Check availability" type="object" class="oe_highlight"/> - <button name="button_plan" attrs="{'invisible': ['|', ('state', 'not in', ('confirmed', 'planned')), '|', ('routing_id', '=', False), '|', ('date_planned_start_wo', '!=', False), ('date_planned_finished_wo', '!=', False)]}" type="object" string="Plan" class="oe_highlight"/> - <button name="button_unplan" type="object" string="Unplan" attrs="{'invisible': ['|', '|', ('state', '!=', 'planned'), ('date_planned_start_wo', '=', False), ('date_planned_finished_wo', '=', False)]}"/> + <button name="button_plan" attrs="{'invisible': ['|', ('state', '!=', 'confirmed'), ('routing_id', '=', False)]}" type="object" string="Plan" class="oe_highlight"/> + <button name="button_unplan" type="object" string="Unplan" attrs="{'invisible': ['|', '|', ('state', '!=', 'planned'), ('date_planned_start', '=', False), ('date_planned_finished', '=', False)]}"/> <button name="open_produce_product" attrs="{'invisible': ['|', '|', '|', ('state', '=', 'to_close'), ('is_locked', '=', False), ('reservation_state', '!=', 'assigned'), ('routing_id', '!=', False)]}" string="Produce" type="object" class="oe_highlight"/> <button name="open_produce_product" attrs="{'invisible': ['|', '|', '|', ('state', '=', 'to_close'), ('is_locked', '=', False), ('reservation_state', 'not in', ('confirmed', 'waiting')), ('routing_id', '!=', False)]}" string="Produce" type="object"/> <button name="post_inventory" string="Post Inventory" type="object" attrs="{'invisible': [('post_visible', '=', False)]}" groups="base.group_no_one"/> <button name="action_cancel" type="object" string="Cancel" - attrs="{'invisible': ['|', '|', ('is_locked', '=', False), ('state', 'in', ('done','cancel')), ('confirm_cancel', '=', True)]}"/> + attrs="{'invisible': ['|', '|', '|', ('id', '=', False), ('is_locked', '=', False), ('state', 'in', ('done','cancel')), ('confirm_cancel', '=', True)]}"/> <button name="action_cancel" type="object" string="Cancel" - attrs="{'invisible': ['|', '|', ('is_locked', '=', False), ('state', 'in', ('done','cancel')), ('confirm_cancel', '=', False)]}" + attrs="{'invisible': ['|', '|', '|', ('id', '=', False), ('is_locked', '=', False), ('state', 'in', ('done','cancel')), ('confirm_cancel', '=', False)]}" confirm="Some product moves have already been confirmed, this manufacturing order can't be completely cancelled. Are you still sure you want to process ?"/> <button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': ['|', ('state', 'in', ('cancel', 'draft')), ('is_locked', '=', False)]}"/> <button name="button_unreserve" type="object" string="Unreserve" attrs="{'invisible': [('unreserve_visible', '=', False)]}"/> @@ -114,14 +113,56 @@ <field name="routing_id" groups="mrp.group_mrp_routings"/> </group> <group> - <field name="date_planned_start"/> - <field name="date_planned_finished" invisible="1"/> - <field name="date_start_wo" attrs="{'invisible': ['|', ('routing_id', '=', False), ('state', 'not in', ['draft', 'confirmed'])]}"/> - <label for="date_planned_start_wo" string="Scheduled Date" attrs="{'invisible': [('date_planned_start_wo', '=', False)]}"/> - <div class="o_row" attrs="{'invisible': [('date_planned_start_wo', '=', False)]}"> - <field name="date_planned_start_wo" attrs="{'readonly': [('move_raw_ids', '!=', [])]}"/> - <label for="date_planned_finished_wo" string="to" attrs="{'invisible': [('date_planned_finished_wo', '=', False)]}"/> - <field name="date_planned_finished_wo" attrs="{'readonly': [('move_raw_ids', '!=', [])]}"/> + <field name="date_deadline" attrs="{'readonly': [('state', 'in', ['done', 'cancel'])]}"/> + <field name="date_start_wo" + attrs="{'invisible': [ + '|', + '&', + ('routing_id', '!=', False), + ('state', 'not in', ['draft', 'confirmed']), + ('routing_id', '=', False), + ], 'readonly': [ + ('routing_id', '!=', False), + ('state', 'not in', ['draft', 'confirmed']), + ]}"/> + <div class="o_row o_td_label"> + <label for="date_planned_start" string="Planned Date" + attrs="{'invisible': [ + ('routing_id', '!=', False), + ('state', 'in', ['draft', 'confirmed']) + ]}"/> + </div> + <div class="o_row"> + <field name="date_planned_start" + attrs="{'invisible': [ + ('routing_id', '!=', False), + ('state', 'in', ['draft', 'confirmed']), + ], 'readonly': [ + '|', + '&', + ('routing_id', '=', False), + ('state', 'in', ['done', 'cancel']), + '&', + ('routing_id', '!=', False), + ('state', 'not in', ['draft', 'confirmed'])]}"/> + <label for="date_planned_finished" string="to" attrs="{'invisible': [ + '|', + ('id', '=', False), + '&', + ('routing_id', '!=', False), + ('state', 'in', ['draft', 'confirmed']) + ]}"/> + <field name="date_planned_finished" required="1" + attrs="{'readonly': [ + '|', + '&', + ('routing_id', '=', False), + ('state', 'in', ['done', 'cancel']), + ('routing_id', '!=', False) + ], 'invisible': [ + ('routing_id', '!=', False), + ('state', 'in', ['draft', 'confirmed']) + ]}"/> </div> <field name="user_id"/> <field name="origin"/> @@ -204,8 +245,8 @@ <field name="name"/> <field name="product_id"/> <field name="product_qty"/> - <field name="product_uom_id" options="{'no_open':True,'no_create':True}"/> - <field name="date_planned_start"/> + <field name="product_uom_id" options="{'no_open':True,'no_create':True}"/> + <field name="date_deadline"/> <field name="state"/> <field name="activity_state"/> <progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}'/> @@ -220,7 +261,7 @@ </div> <div class="o_kanban_record_bottom"> <div class="oe_kanban_bottom_left text-muted"> - <span><t t-esc="record.name.value"/> <t t-esc="record.date_planned_start.value and record.date_planned_start.value.split(' ')[0] or False"/></span> + <span><t t-esc="record.name.value"/> <t t-esc="record.date_deadline.value and record.date_deadline.value.split(' ')[0] or False"/></span> <field name="activity_ids" widget="kanban_activity"/> </div> <div class="oe_kanban_bottom_right"> @@ -238,7 +279,8 @@ <field name="model">mrp.production</field> <field eval="2" name="priority"/> <field name="arch" type="xml"> - <calendar date_start="date_planned_start" string="Manufacturing Orders" color="routing_id" event_limit="5"> + <calendar date_start="date_planned_start" date_stop="date_planned_finished" + string="Manufacturing Orders" color="routing_id" event_limit="5"> <field name="routing_id"/> <field name="user_id" avatar_field="image_small"/> <field name="product_id"/> @@ -259,7 +301,7 @@ <field name="model">mrp.production</field> <field name="arch" type="xml"> <pivot string="Manufacturing Orders"> - <field name="date_planned_start" type="row"/> + <field name="date_deadline" type="row"/> </pivot> </field> </record> @@ -268,7 +310,7 @@ <field name="model">mrp.production</field> <field name="arch" type="xml"> <graph string="Manufacturing Orders"> - <field name="date_planned_start"/> + <field name="date_deadline"/> </graph> </field> </record> @@ -292,7 +334,7 @@ <filter string="Waiting for Material" name="waiting" domain="[('reservation_state', 'in', ('waiting', 'confirmed'))]"/> <filter string="Material Available" name="filter_ready" domain="[('reservation_state', '=', 'assigned')]"/> <separator/> - <filter string="Late" domain="['&', ('date_planned_start', '<', current_date), ('state', '=', 'confirmed')]" + <filter string="Late" domain="['&', ('date_deadline', '<', current_date), ('state', '=', 'confirmed')]" name="late" help="Production started late"/> <separator/> <field name="product_id"/> @@ -312,7 +354,7 @@ <group expand="0" string="Group By..."> <filter string="Product" name="product" domain="[]" context="{'group_by':'product_id'}"/> <filter string="Routing" name="routing" domain="[]" context="{'group_by':'routing_id'}" groups="mrp.group_mrp_routings"/> - <filter string="Scheduled Date" name="scheduled_date" domain="[]" context="{'group_by':'date_planned_start'}" help="Scheduled Date by Month"/> + <filter string="Scheduled Date" name="scheduled_date" domain="[]" context="{'group_by':'date_deadline'}" help="Scheduled Date by Month"/> <filter string="Status" name="status" domain="[]" context="{'group_by': 'state'}"/> <filter string="Material Availability" name="groupby_reservation_state" domain="[]" context="{'group_by': 'reservation_state'}"/> </group> diff --git a/addons/sale_mrp/tests/test_sale_mrp_lead_time.py b/addons/sale_mrp/tests/test_sale_mrp_lead_time.py index 0f5acd55daf..b019fc0e708 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_lead_time.py +++ b/addons/sale_mrp/tests/test_sale_mrp_lead_time.py @@ -71,9 +71,9 @@ class TestSaleMrpLeadTime(TestStockCommon): # Check schedule date of manufacturing order mo_date = out_date - timedelta(days=self.product_1.produce_delay) - timedelta(days=company.manufacturing_lead) - date_planned_start = fields.Datetime.from_string(manufacturing_order.date_planned_start) + date_deadline = fields.Datetime.from_string(manufacturing_order.date_deadline) self.assertAlmostEqual( - date_planned_start, mo_date, + date_deadline, mo_date, delta=timedelta(seconds=1), msg="Schedule date of manufacturing order should be equal to: Schedule date of picking - product's Manufacturing Lead Time - company's Manufacturing Lead Time." ) @@ -141,9 +141,9 @@ class TestSaleMrpLeadTime(TestStockCommon): # Check schedule date of manufacturing order mo_date = pack_date - timedelta(days=self.product_1.produce_delay) - date_planned_start = fields.Datetime.from_string(manufacturing_order.date_planned_start) + date_deadline = fields.Datetime.from_string(manufacturing_order.date_deadline) self.assertAlmostEqual( - date_planned_start, mo_date, + date_deadline, mo_date, delta=timedelta(seconds=10), msg="Schedule date of manufacturing order should be equal to: Schedule date of pack type picking - product's Manufacturing Lead Time." ) |
