[IMP] mrp, sale_mrp: planned date on MO

The purpose of this commit is to simplify the `mrp.production` model in
reducing the number of date field.

The following fields was kept:
    - `date_planned_start`: the planned date for the beginning of the MO
    - `date_planned_finished`: the planned date for the ending of the MO
    - `date_start_wo`: the preferred date to start a Work Order in case
      the MO will plan one.
    - `date_deadline`: is only indicative and is automatically set when
      the manufacturing order is needed for a sale order.
    - `date_start` and `date_finished`: no changes.

Task #1935229

Co-authored-by: Whenrow <whe@odoo.com>
This commit is contained in:
svs-odoo
2019-08-14 08:44:52 +00:00
co-authored by Whenrow
parent 629e8da14f
commit 2a2c27fa45
6 changed files with 125 additions and 57 deletions
+31 -30
View File
@@ -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):
+19 -1
View File
@@ -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):
+1 -1
View File
@@ -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,
@@ -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
+63 -21
View File
@@ -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&lt;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': [
'|',
'&amp;',
('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': [
'|',
'&amp;',
('routing_id', '=', False),
('state', 'in', ['done', 'cancel']),
'&amp;',
('routing_id', '!=', False),
('state', 'not in', ['draft', 'confirmed'])]}"/>
<label for="date_planned_finished" string="to" attrs="{'invisible': [
'|',
('id', '=', False),
'&amp;',
('routing_id', '!=', False),
('state', 'in', ['draft', 'confirmed'])
]}"/>
<field name="date_planned_finished" required="1"
attrs="{'readonly': [
'|',
'&amp;',
('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="['&amp;', ('date_planned_start', '&lt;', current_date), ('state', '=', 'confirmed')]"
<filter string="Late" domain="['&amp;', ('date_deadline', '&lt;', 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>
@@ -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."
)