[REF] mrp: onchange to compute

This task will:
- Clean the code (e.g. the production's moves are now created directly)
- Have the same behavior when creating object in the UI or directly in
backend

It's mainly on the `mrp.production` object and most of the computed
stored field are used as default value or act like an onchange on draft
production. Once the production order is validated, it's only modify
by direct write

Task-2709753

closes odoo/odoo#83576

Related: odoo/enterprise#24405
Signed-off-by: Arnold Moyaux <arm@odoo.com>
This commit is contained in:
Arnold Moyaux
2022-04-22 12:04:23 +02:00
parent ab9cb62c74
commit d7716f071d
12 changed files with 284 additions and 354 deletions
-35
View File
@@ -78,13 +78,6 @@
<field name="product_qty">3</field>
<field name="bom_id" ref="mrp_bom_manufacture"/>
</record>
<function model="stock.move" name="create">
<value model="stock.move" eval="
obj().env.ref('mrp.mrp_production_1')._get_moves_raw_values() +
obj().env.ref('mrp.mrp_production_1')._get_moves_finished_values()"/>
</function>
<!-- Table -->
<record id="product_product_computer_desk" model="product.product">
@@ -295,12 +288,6 @@
<field name="bom_id" ref="mrp_bom_desk"/>
</record>
<function model="stock.move" name="create">
<value model="stock.move" eval="
obj().env.ref('mrp.mrp_production_3')._get_moves_raw_values() +
obj().env.ref('mrp.mrp_production_3')._get_moves_finished_values()"/>
</function>
<record id="mrp_bom_table_top" model="mrp.bom">
<field name="product_tmpl_id" ref="product_product_computer_desk_head_product_template"/>
<field name="product_uom_id" ref="uom.product_uom_unit"/>
@@ -402,14 +389,6 @@
<field name="location_dest_id" ref="stock.stock_location_stock"/>
<field name="bom_id" ref="mrp_bom_table_top"/>
</record>
<!-- Generate Table & Table Top MO's moves -->
<function model="stock.move" name="create">
<value model="stock.move" eval="
obj().env.ref('mrp.mrp_production_4')._get_moves_raw_values() +
obj().env.ref('mrp.mrp_production_4')._get_moves_finished_values()"/>
</function>
<!-- Table Kit -->
<record id="product_product_table_kit" model="product.product">
@@ -651,12 +630,6 @@
<field name="bom_id" ref="mrp_bom_drawer"/>
</record>
<function model="stock.move" name="create">
<value model="stock.move" eval="
obj().env.ref('mrp.mrp_production_drawer')._get_moves_raw_values() +
obj().env.ref('mrp.mrp_production_drawer')._get_moves_finished_values()"/>
</function>
<!-- Run Scheduler -->
<function model="procurement.group" name="run_scheduler"/>
@@ -704,10 +677,6 @@
<field name="date_start" eval="(datetime.now() - timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<function model="mrp.production" name="_create_workorder">
<value eval="[ref('mrp.mrp_production_3')]"/>
</function>
<function model="mrp.production" name="action_confirm" eval="[[
ref('mrp.mrp_production_3'),
ref('mrp.mrp_production_4'),
@@ -732,10 +701,6 @@
<value eval="{'quantity_done': 5}"/>
</function>
<function model="mrp.production" name="_post_inventory">
<value eval="[ref('mrp.mrp_production_drawer')]"/>
</function>
<function model="mrp.production" name="button_mark_done">
<value eval="[ref('mrp.mrp_production_drawer')]"/>
</function>
+259 -273
View File
@@ -28,40 +28,6 @@ class MrpProduction(models.Model):
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'priority desc, date_planned_start asc,id'
@api.model
def _get_default_picking_type(self):
company_id = self.env.context.get('default_company_id', self.env.company.id)
return self.env['stock.picking.type'].search([
('code', '=', 'mrp_operation'),
('warehouse_id.company_id', '=', company_id),
], limit=1).id
@api.model
def _get_default_location_src_id(self):
location = False
company_id = self.env.context.get('default_company_id', self.env.company.id)
if self.env.context.get('default_picking_type_id'):
location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_src_id
if not location:
location = self.env['stock.warehouse'].search([('company_id', '=', company_id)], limit=1).lot_stock_id
return location and location.id or False
@api.model
def _get_default_location_dest_id(self):
location = False
company_id = self.env.context.get('default_company_id', self.env.company.id)
if self._context.get('default_picking_type_id'):
location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_dest_id
if not location:
location = self.env['stock.warehouse'].search([('company_id', '=', 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)
@api.model
def _get_default_date_planned_start(self):
if self.env.context.get('default_date_deadline'):
@@ -72,8 +38,7 @@ class MrpProduction(models.Model):
def _get_default_is_locked(self):
return not self.user_has_groups('mrp.group_unlocked_by_default')
name = fields.Char(
'Reference', copy=False, readonly=True, default=lambda x: _('New'))
name = fields.Char('Reference', copy=False, readonly=True)
priority = fields.Selection(
PROCUREMENT_PRIORITIES, string='Priority', default='0',
help="Components will be reserved first for the MO with the highest priorities.")
@@ -92,19 +57,19 @@ class MrpProduction(models.Model):
('company_id', '=', company_id)
]
""",
compute='_compute_product_id', store=True, copy=True,
readonly=True, required=True, check_company=True,
states={'draft': [('readonly', False)]})
product_tracking = fields.Selection(related='product_id.tracking')
product_tmpl_id = fields.Many2one('product.template', 'Product Template', related='product_id.product_tmpl_id')
product_qty = fields.Float(
'Quantity To Produce',
default=1.0, digits='Product Unit of Measure',
readonly=True, required=True, tracking=True,
states={'draft': [('readonly', False)]})
'Quantity To Produce', digits='Product Unit of Measure',
readonly=False, required=True, tracking=True, default=1.0,
compute='_compute_product_qty', store=True, copy=True)
product_uom_id = fields.Many2one(
'uom.uom', 'Product Unit of Measure',
readonly=True, required=True,
states={'draft': [('readonly', False)]}, domain="[('category_id', '=', product_uom_category_id)]")
readonly=False, required=True, compute='_compute_uom_id', store=True, copy=True,
domain="[('category_id', '=', product_uom_category_id)]")
lot_producing_id = fields.Many2one(
'stock.lot', string='Lot/Serial Number', copy=False,
domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True)
@@ -112,23 +77,22 @@ class MrpProduction(models.Model):
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
product_uom_qty = fields.Float(string='Total Quantity', compute='_compute_product_uom_qty', store=True)
picking_type_id = fields.Many2one(
'stock.picking.type', 'Operation Type',
'stock.picking.type', 'Operation Type', copy=True, readonly=False,
compute='_compute_picking_type_id', store=True, precompute=True,
domain="[('code', '=', 'mrp_operation'), ('company_id', '=', company_id)]",
default=_get_default_picking_type, required=True, check_company=True, index=True)
required=True, check_company=True, index=True)
use_create_components_lots = fields.Boolean(related='picking_type_id.use_create_components_lots')
location_src_id = fields.Many2one(
'stock.location', 'Components Location',
default=_get_default_location_src_id,
readonly=True, required=True,
compute='_compute_locations', store=True, check_company=True,
readonly=False, required=True, precompute=True,
domain="[('usage','=','internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
states={'draft': [('readonly', False)]}, check_company=True,
help="Location where the system will look for components.")
location_dest_id = fields.Many2one(
'stock.location', 'Finished Products Location',
default=_get_default_location_dest_id,
readonly=True, required=True,
compute='_compute_locations', store=True, check_company=True,
readonly=False, required=True, precompute=True,
domain="[('usage','=','internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
states={'draft': [('readonly', False)]}, check_company=True,
help="Location where the system will stock the finished products.")
date_planned_start = fields.Datetime(
'Scheduled Date', copy=False, default=_get_default_date_planned_start,
@@ -136,11 +100,11 @@ class MrpProduction(models.Model):
index=True, required=True)
date_planned_finished = fields.Datetime(
'Scheduled End Date',
default=_get_default_date_planned_finished,
compute='_compute_date_planned_finished', store=True,
help="Date at which you plan to finish the production.",
copy=False)
date_deadline = fields.Datetime(
'Deadline', copy=False, store=True, readonly=True, compute='_compute_date_deadline', inverse='_set_date_deadline',
'Deadline', copy=False, store=True, readonly=True, compute='_compute_date_deadline',
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, readonly=True, help="Date of the WO")
date_finished = fields.Datetime('End Date', copy=False, readonly=True, help="Date when the MO has been close")
@@ -149,8 +113,7 @@ class MrpProduction(models.Model):
production_real_duration = fields.Float("Real Duration", help="Total real duration (in minutes)", compute='_compute_production_real_duration')
bom_id = fields.Many2one(
'mrp.bom', 'Bill of Material',
readonly=True, states={'draft': [('readonly', False)]},
'mrp.bom', 'Bill of Material', readonly=False,
domain="""[
'&',
'|',
@@ -163,7 +126,7 @@ class MrpProduction(models.Model):
('product_tmpl_id.product_variant_ids','=',product_id),
('product_id','=',False),
('type', '=', 'normal')]""",
check_company=True,
check_company=True, compute='_compute_bom_id', store=True,
help="Bill of Materials allow you to define the list of required components to make a finished product.")
state = fields.Selection([
@@ -194,18 +157,20 @@ class MrpProduction(models.Model):
move_raw_ids = fields.One2many(
'stock.move', 'raw_material_production_id', 'Components',
compute='_compute_move_raw_ids', store=True, readonly=False,
copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
domain=[('scrapped', '=', False)])
move_finished_ids = fields.One2many(
'stock.move', 'production_id', 'Finished Products',
copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
'stock.move', 'production_id', 'Finished Products', readonly=False,
compute='_compute_move_finished_ids', store=True, copy=False,
domain=[('scrapped', '=', False)])
move_byproduct_ids = fields.One2many('stock.move', compute='_compute_move_byproduct_ids', inverse='_set_move_byproduct_ids')
finished_move_line_ids = fields.One2many(
'stock.move.line', compute='_compute_lines', inverse='_inverse_lines', string="Finished Product"
)
workorder_ids = fields.One2many(
'mrp.workorder', 'production_id', 'Work Orders', copy=True)
'mrp.workorder', 'production_id', 'Work Orders', copy=True,
compute='_compute_workorder_ids', store=True, readonly=False)
move_dest_ids = fields.One2many('stock.move', 'created_production_id',
string="Stock Movements of Produced Goods")
@@ -273,6 +238,11 @@ class MrpProduction(models.Model):
compute='_compute_show_allocation',
help='Technical Field used to decide whether the button "Allocation" should be displayed.')
_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!'),
]
@api.depends('procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids')
def _compute_mrp_production_child_count(self):
for production in self:
@@ -288,6 +258,40 @@ class MrpProduction(models.Model):
for production in self:
production.mrp_production_backorder_count = len(production.procurement_group_id.mrp_production_ids)
@api.depends('company_id')
def _compute_picking_type_id(self):
domain = [
('code', '=', 'mrp_operation'),
('warehouse_id.company_id', 'in', self.company_id.ids),
]
picking_types = self.env['stock.picking.type'].search_read(domain, ['company_id'], load=False, limit=1)
picking_type_by_company = {pt['company_id']: pt['id'] for pt in picking_types}
for mo in self:
if mo.picking_type_id and mo.picking_type_id.company_id == mo.company_id:
continue
mo.picking_type_id = picking_type_by_company.get(mo.company_id.id, False)
@api.depends('bom_id', 'product_id')
def _compute_uom_id(self):
for production in self:
if production.state != 'draft':
continue
if production.bom_id and production._origin.bom_id != production.bom_id:
production.product_uom_id = production.bom_id.product_uom_id
elif production.product_id:
production.product_uom_id = production.product_id.uom_id
else:
production.product_uom_id = False
@api.depends('picking_type_id')
def _compute_locations(self):
for production in self:
if not production.picking_type_id.default_location_src_id or not production.picking_type_id.default_location_dest_id:
company_id = production.company_id.id if (production.company_id and production.company_id in self.env.companies) else self.env.company.id
fallback_loc = self.env['stock.warehouse'].search([('company_id', '=', company_id)], limit=1).lot_stock_id
production.location_src_id = production.picking_type_id.default_location_src_id.id or fallback_loc.id
production.location_dest_id = production.picking_type_id.default_location_dest_id.id or fallback_loc.id
@api.depends('state', 'reservation_state', 'date_planned_start', 'move_raw_ids', 'move_raw_ids.forecast_availability', 'move_raw_ids.forecast_expected_date')
def _compute_components_availability(self):
productions = self.filtered(lambda mo: mo.state not in ('cancel', 'done', 'draft'))
@@ -312,15 +316,45 @@ class MrpProduction(models.Model):
if production.date_planned_start:
production.components_availability_state = 'late' if forecast_date > production.date_planned_start else 'expected'
@api.depends('bom_id')
def _compute_product_id(self):
for production in self:
bom = production.bom_id
if bom and (
not production.product_id or bom.product_tmpl_id != production.product_tmpl_id
or bom.product_id and bom.product_id != production.product_id
):
production.product_id = bom.product_id or bom.product_tmpl_id.product_variant_id
@api.depends('product_id', 'picking_type_id')
def _compute_bom_id(self):
mo_by_picking_type_and_company_id = defaultdict(lambda: self.env['mrp.production'])
for mo in self:
if not mo.product_id and not mo.bom_id:
mo.bom_id = False
continue
mo_by_picking_type_and_company_id[(mo.picking_type_id, mo.company_id.id)] |= mo
for (picking_type, company_id), productions in mo_by_picking_type_and_company_id.items():
boms_by_product = self.env['mrp.bom']._bom_find(productions.product_id, picking_type=picking_type, company_id=company_id, bom_type='normal')
for production in productions:
if not production.bom_id or production.bom_id.product_tmpl_id != production.product_tmpl_id or (production.bom_id.product_id and production.bom_id.product_id != production.product_id):
bom = boms_by_product[production.product_id]
production.bom_id = bom.id or False
@api.depends('bom_id')
def _compute_product_qty(self):
for production in self:
if production.state != 'draft':
continue
if production.bom_id and production._origin.bom_id != production.bom_id:
production.product_qty = production.bom_id.product_qty
@api.depends('move_finished_ids.date_deadline')
def _compute_date_deadline(self):
for production in self:
production.date_deadline = min(production.move_finished_ids.filtered('date_deadline').mapped('date_deadline'), default=production.date_deadline or False)
def _set_date_deadline(self):
for production in self:
production.move_finished_ids.date_deadline = production.date_deadline
@api.depends('workorder_ids.duration_expected')
def _compute_production_duration_expected(self):
for production in self:
@@ -389,26 +423,6 @@ class MrpProduction(models.Model):
])
order.delivery_count = len(order.picking_ids)
def action_view_mo_delivery(self):
""" This function returns an action that display picking related to
manufacturing order orders. It can either be a in a list or in a form
view, if there is only one picking to show.
"""
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("stock.action_picking_tree_all")
pickings = self.mapped('picking_ids')
if len(pickings) > 1:
action['domain'] = [('id', 'in', pickings.ids)]
elif pickings:
form_view = [(self.env.ref('stock.view_picking_form').id, 'form')]
if 'views' in action:
action['views'] = form_view + [(state,view) for state,view in action['views'] if view != 'form']
else:
action['views'] = form_view
action['res_id'] = pickings.id
action['context'] = dict(self._context, default_origin=self.name)
return action
@api.depends('product_uom_id', 'product_qty', 'product_id.uom_id')
def _compute_product_uom_qty(self):
for production in self:
@@ -477,6 +491,49 @@ class MrpProduction(models.Model):
elif any(not float_is_zero(move.quantity_done, precision_rounding=move.product_uom.rounding or move.product_id.uom_id.rounding) for move in production.move_raw_ids):
production.state = 'progress'
@api.depends('bom_id', 'product_id', 'product_qty', 'product_uom_id')
def _compute_workorder_ids(self):
for production in self:
if production.state != 'draft':
continue
workorders_list = [Command.link(wo.id) for wo in production.workorder_ids.filtered(lambda wo: not wo.operation_id)]
if not production.bom_id and not production._origin.product_id:
production.workorder_ids = workorders_list
if production.product_id != production._origin.product_id:
production.workorder_ids = [Command.clear()]
if production.bom_id and production.product_id and production.product_qty > 0:
# keep manual entries
workorders_values = []
product_qty = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id)
exploded_boms, dummy = production.bom_id.explode(production.product_id, product_qty / production.bom_id.product_qty, picking_type=production.bom_id.picking_type_id)
for bom, bom_data in exploded_boms:
# If the operations of the parent BoM and phantom BoM are the same, don't recreate work orders.
if not (bom.operation_ids and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.operation_ids != bom.operation_ids)):
continue
for operation in bom.operation_ids:
if operation._skip_operation_line(bom_data['product']):
continue
workorders_values += [{
'name': operation.name,
'production_id': production.id,
'workcenter_id': operation.workcenter_id.id,
'product_uom_id': production.product_uom_id.id,
'operation_id': operation.id,
'state': 'pending',
}]
workorders_dict = {wo.operation_id.id: wo for wo in production.workorder_ids.filtered(lambda wo: wo.operation_id)}
for workorder_values in workorders_values:
if workorder_values['operation_id'] in workorders_dict:
# update existing entries
workorders_list += [Command.update(workorders_dict[workorder_values['operation_id']].id, workorder_values)]
else:
# add new entries
workorders_list += [Command.create(workorder_values)]
production.workorder_ids = workorders_list
else:
production.workorder_ids = [Command.delete(wo.id) for wo in production.workorder_ids.filtered(lambda wo: wo.operation_id)]
@api.depends('state', 'move_raw_ids.state')
def _compute_reservation_state(self):
self.reservation_state = False
@@ -528,6 +585,7 @@ class MrpProduction(models.Model):
def _set_move_byproduct_ids(self):
move_finished_ids = self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id)
# TODO: Try to create by-product moves here instead of moving them in the `create`.
self.move_finished_ids = move_finished_ids | self.move_byproduct_ids
@api.depends('state')
@@ -577,11 +635,6 @@ class MrpProduction(models.Model):
('move_orig_ids', 'in', lines.ids)], limit=1):
mo.show_allocation = True
_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!'),
]
@api.depends('product_uom_qty', 'date_planned_start')
def _compute_forecasted_issue(self):
for order in self:
@@ -599,127 +652,63 @@ class MrpProduction(models.Model):
late_stock_moves = self.env['stock.move'].search([('delay_alert_date', operator, value)])
return ['|', ('move_raw_ids', 'in', late_stock_moves.ids), ('move_finished_ids', 'in', late_stock_moves.ids)]
@api.onchange('company_id')
def _onchange_company_id(self):
if self.company_id:
if self.move_raw_ids:
self.move_raw_ids.update({'company_id': self.company_id})
if self.picking_type_id and self.picking_type_id.company_id != self.company_id:
self.picking_type_id = self.env['stock.picking.type'].search([
('code', '=', 'mrp_operation'),
('warehouse_id.company_id', '=', self.company_id.id),
], limit=1).id
@api.onchange('product_id', 'picking_type_id', 'company_id')
def _onchange_product_id(self):
""" Finds UoM of changed product. """
if not self.product_id:
self.bom_id = False
elif not self.bom_id or self.bom_id.product_tmpl_id != self.product_tmpl_id or (self.bom_id.product_id and self.bom_id.product_id != self.product_id):
bom = self.env['mrp.bom']._bom_find(self.product_id, picking_type=self.picking_type_id, company_id=self.company_id.id, bom_type='normal')[self.product_id]
if bom:
self.bom_id = bom.id
self.product_qty = self.bom_id.product_qty
self.product_uom_id = self.bom_id.product_uom_id.id
else:
self.bom_id = False
self.product_uom_id = self.product_id.uom_id.id
@api.onchange('product_qty', 'product_uom_id')
def _onchange_product_qty(self):
for workorder in self.workorder_ids:
workorder.product_uom_id = self.product_uom_id
if self._origin.product_qty:
workorder.duration_expected = workorder._get_duration_expected(ratio=self.product_qty / self._origin.product_qty)
else:
workorder.duration_expected = workorder._get_duration_expected()
if workorder.date_planned_start and workorder.duration_expected:
workorder.date_planned_finished = workorder.date_planned_start + relativedelta(minutes=workorder.duration_expected)
@api.onchange('bom_id')
def _onchange_bom_id(self):
if not self.product_id and self.bom_id:
self.product_id = self.bom_id.product_id or self.bom_id.product_tmpl_id.product_variant_ids[:1]
self.product_qty = self.bom_id.product_qty or 1.0
self.product_uom_id = self.bom_id and self.bom_id.product_uom_id.id or self.product_id.uom_id.id
self.move_raw_ids = [(2, move.id) for move in self.move_raw_ids.filtered(lambda m: m.bom_line_id)]
self.move_finished_ids = [(2, move.id) for move in self.move_finished_ids]
self.picking_type_id = self.bom_id.picking_type_id or self.picking_type_id
@api.onchange('date_planned_start', 'product_id')
def _onchange_date_planned_start(self):
if self.date_planned_start and not self.is_planned:
date_planned_finished = self.date_planned_start + relativedelta(days=self.product_id.produce_delay)
date_planned_finished = date_planned_finished + relativedelta(days=self.company_id.manufacturing_lead)
if date_planned_finished == self.date_planned_start:
@api.depends('company_id', 'date_planned_start', 'is_planned', 'product_id')
def _compute_date_planned_finished(self):
for production in self:
if not production.date_planned_start or production.is_planned:
continue
days_delay = production.product_id.produce_delay + production.company_id.manufacturing_lead
date_planned_finished = production.date_planned_start + relativedelta(days=days_delay)
if date_planned_finished == production.date_planned_start:
date_planned_finished = date_planned_finished + relativedelta(hours=1)
self.date_planned_finished = date_planned_finished
self.move_raw_ids = [(1, m.id, {'date': self.date_planned_start}) for m in self.move_raw_ids]
self.move_finished_ids = [(1, m.id, {'date': date_planned_finished}) for m in self.move_finished_ids]
production.date_planned_finished = date_planned_finished
@api.onchange('bom_id', 'product_id', 'product_qty', 'product_uom_id')
def _onchange_move_raw(self):
if not self.bom_id and not self._origin.product_id:
return
# Clear move raws if we are changing the product. In case of creation (self._origin is empty),
# we need to avoid keeping incorrect lines, so clearing is necessary too.
if self.product_id != self._origin.product_id:
self.move_raw_ids = [(5,)]
if self.bom_id and self.product_id and self.product_qty > 0:
# keep manual entries
list_move_raw = [(4, move.id) for move in self.move_raw_ids.filtered(lambda m: not m.bom_line_id)]
moves_raw_values = self._get_moves_raw_values()
move_raw_dict = {move.bom_line_id.id: move for move in self.move_raw_ids.filtered(lambda m: m.bom_line_id)}
for move_raw_values in moves_raw_values:
if move_raw_values['bom_line_id'] in move_raw_dict:
# update existing entries
list_move_raw += [(1, move_raw_dict[move_raw_values['bom_line_id']].id, move_raw_values)]
else:
# add new entries
list_move_raw += [(0, 0, move_raw_values)]
self.move_raw_ids = list_move_raw
else:
self.move_raw_ids = [(2, move.id) for move in self.move_raw_ids.filtered(lambda m: m.bom_line_id)]
@api.depends('company_id', 'bom_id', 'product_id', 'product_qty', 'product_uom_id', 'location_src_id', 'date_planned_start')
def _compute_move_raw_ids(self):
for production in self:
if production.state != 'draft':
continue
list_move_raw = [Command.link(move.id) for move in production.move_raw_ids.filtered(lambda m: not m.bom_line_id)]
if not production.bom_id and not production._origin.product_id:
production.move_raw_ids = list_move_raw
if production.product_id != production._origin.product_id:
production.move_raw_ids = [Command.clear()]
if production.bom_id and production.product_id and production.product_qty > 0:
# keep manual entries
moves_raw_values = production._get_moves_raw_values()
move_raw_dict = {move.bom_line_id.id: move for move in production.move_raw_ids.filtered(lambda m: m.bom_line_id)}
for move_raw_values in moves_raw_values:
if move_raw_values['bom_line_id'] in move_raw_dict:
# update existing entries
list_move_raw += [Command.update(move_raw_dict[move_raw_values['bom_line_id']].id, move_raw_values)]
else:
# add new entries
list_move_raw += [Command.create(move_raw_values)]
production.move_raw_ids = list_move_raw
else:
production.move_raw_ids = [Command.delete(move.id) for move in production.move_raw_ids.filtered(lambda m: m.bom_line_id)]
@api.onchange('product_id')
def _onchange_move_finished_product(self):
self.move_finished_ids = [(5,)]
if self.product_id:
self._create_update_move_finished()
@api.onchange('bom_id', 'product_qty', 'product_uom_id')
def _onchange_move_finished(self):
if self.product_id and self.product_qty > 0:
self._create_update_move_finished()
else:
self.move_finished_ids = [(2, move.id) for move in self.move_finished_ids.filtered(lambda m: m.bom_line_id)]
@api.onchange('location_src_id', 'move_raw_ids', 'bom_id')
def _onchange_location(self):
source_location = self.location_src_id
self.move_raw_ids.update({
'warehouse_id': source_location.warehouse_id.id,
'location_id': source_location.id,
})
@api.onchange('location_dest_id', 'move_finished_ids', 'bom_id')
def _onchange_location_dest(self):
destination_location = self.location_dest_id
update_value_list = []
for move in self.move_finished_ids:
update_value_list += [(1, move.id, ({
'warehouse_id': destination_location.warehouse_id.id,
'location_dest_id': destination_location.id,
}))]
self.move_finished_ids = update_value_list
@api.onchange('picking_type_id')
def _onchange_picking_type(self):
if not self.picking_type_id.default_location_src_id or not self.picking_type_id.default_location_dest_id.id:
company_id = self.company_id.id if (self.company_id and self.company_id in self.env.companies) else self.env.company.id
fallback_loc = self.env['stock.warehouse'].search([('company_id', '=', company_id)], limit=1).lot_stock_id
self.location_src_id = self.picking_type_id.default_location_src_id.id or fallback_loc.id
self.location_dest_id = self.picking_type_id.default_location_dest_id.id or fallback_loc.id
@api.depends('product_id', 'bom_id', 'product_qty', 'product_uom_id', 'location_dest_id', 'date_planned_finished')
def _compute_move_finished_ids(self):
for production in self:
if production.state != 'draft':
updated_values = {}
if production.date_planned_finished:
updated_values['date'] = production.date_planned_finished
if production.date_deadline:
updated_values['date_deadline'] = production.date_deadline
if 'date' in updated_values or 'date_deadline' in updated_values:
production.move_finished_ids = [
Command.update(m.id, updated_values) for m in production.move_finished_ids
]
continue
production.move_finished_ids = [Command.clear()]
if production.product_id:
production._create_update_move_finished()
else:
production.move_finished_ids = [
Command.delete(move.id) for move in production.move_finished_ids if move.bom_line_id
]
@api.onchange('qty_producing', 'lot_producing_id')
def _onchange_producing(self):
@@ -734,12 +723,13 @@ class MrpProduction(models.Model):
if message:
return {'warning': {'title': _('Warning'), 'message': message}}
@api.onchange('bom_id', 'product_id')
def _onchange_workorder_ids(self):
if self.bom_id and self.product_id:
self._create_workorder()
else:
self.workorder_ids = False
@api.onchange('product_id', 'move_raw_ids')
def _onchange_product_id(self):
for move in self.move_raw_ids:
if self.product_id == move.product_id:
message = _("The component %s should not be the same as the product to produce.", self.product_id.display_name)
self.move_raw_ids = self.move_raw_ids - move
return {'warning': {'title': _('Warning'), 'message': message}}
@api.constrains('move_byproduct_ids')
def _check_byproducts(self):
@@ -749,16 +739,24 @@ class MrpProduction(models.Model):
if sum(order.move_byproduct_ids.mapped('cost_share')) > 100:
raise ValidationError(_("The total cost share for a manufacturing order's by-products cannot exceed 100."))
@api.constrains('product_id', 'move_raw_ids')
def _check_production_lines(self):
for production in self:
for move in production.move_raw_ids:
if production.product_id == move.product_id:
raise ValidationError(_("The component %s should not be the same as the product to produce.") % production.product_id.display_name)
def write(self, vals):
if 'move_byproduct_ids' in vals and 'move_finished_ids' not in vals:
vals['move_finished_ids'] = vals.get('move_finished_ids', []) + vals['move_byproduct_ids']
del vals['move_byproduct_ids']
if 'workorder_ids' in self:
production_to_replan = self.filtered(lambda p: p.is_planned)
if 'move_raw_ids' in vals and self.state not in ['draft', 'cancel', 'done']:
# When adding a move raw, it should have the source location's `warehouse_id`.
# Before, it was handle by an onchange, now it's forced if not already in vals.
warehouse_id = self.location_src_id.warehouse_id.id
if vals.get('location_src_id'):
location_source = self.env['stock.location'].browse(vals.get('location_src_id'))
warehouse_id = location_source.warehouse_id.id
for move_vals in vals['move_raw_ids']:
command, _id, field_values = move_vals
if command == Command.CREATE and not field_values.get('warehouse_id', False):
field_values['warehouse_id'] = warehouse_id
res = super(MrpProduction, self).write(vals)
for production in self:
@@ -799,12 +797,11 @@ class MrpProduction(models.Model):
vals['move_finished_ids'] = vals.get('move_finished_ids', []) + vals['move_byproduct_ids']
del vals['move_byproduct_ids']
if not vals.get('name', False) or vals['name'] == _('New'):
picking_type_id = vals.get('picking_type_id') or self._get_default_picking_type()
picking_type_id = self.env['stock.picking.type'].browse(picking_type_id)
if picking_type_id:
vals['name'] = picking_type_id.sequence_id.next_by_id()
else:
vals['name'] = self.env['ir.sequence'].next_by_code('mrp.production') or _('New')
picking_type_id = vals.get('picking_type_id')
if not picking_type_id:
picking_type_id = self._get_default_picking_type_id(vals.get('company_id', self.env.company.id))
vals['picking_type_id'] = picking_type_id
vals['name'] = self.env['stock.picking.type'].browse(picking_type_id).sequence_id.next_by_id()
if not vals.get('procurement_group_id'):
procurement_group_vals = self._prepare_procurement_group_vals(vals)
vals['procurement_group_id'] = self.env["procurement.group"].create(procurement_group_vals).id
@@ -816,15 +813,6 @@ class MrpProduction(models.Model):
production._onchange_move_finished()
return productions
@api.ondelete(at_uninstall=False)
def _unlink_except_done(self):
if any(production.state == 'done' for production in self):
raise UserError(_('Cannot delete a manufacturing order in done state.'))
not_cancel = self.filtered(lambda m: m.state != 'cancel')
if not_cancel:
productions_name = ', '.join([prod.display_name for prod in not_cancel])
raise UserError(_('%s cannot be deleted. Try to cancel them before.', productions_name))
def unlink(self):
self.action_cancel()
workorders_to_delete = self.workorder_ids.filtered(lambda wo: wo.state != 'done')
@@ -845,6 +833,22 @@ class MrpProduction(models.Model):
default['move_raw_ids'] = [(0, 0, move.copy_data()[0]) for move in self.move_raw_ids.filtered(lambda m: m.product_qty != 0.0)]
return super(MrpProduction, self).copy_data(default=default)
def action_view_mo_delivery(self):
""" Returns an action that display picking related to manufacturing order.
It can either be a list view or in a form view (if there is only one picking to show).
"""
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("stock.action_picking_tree_all")
if len(self.picking_ids) > 1:
action['domain'] = [('id', 'in', self.picking_ids.ids)]
elif self.picking_ids:
action['res_id'] = self.picking_ids.id
action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')]
if 'views' in action:
action['views'] += [(state, view) for state, view in action['views'] if view != 'form']
action['context'] = dict(self._context, default_origin=self.name)
return action
def action_toggle_is_locked(self):
self.ensure_one()
self.is_locked = not self.is_locked
@@ -863,33 +867,12 @@ class MrpProduction(models.Model):
action['context']['warehouse'] = warehouse.id
return action
def _create_workorder(self):
for production in self:
if not production.bom_id or not production.product_id:
continue
workorders_values = []
product_qty = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id)
exploded_boms, dummy = production.bom_id.explode(production.product_id, product_qty / production.bom_id.product_qty, picking_type=production.bom_id.picking_type_id)
for bom, bom_data in exploded_boms:
# If the operations of the parent BoM and phantom BoM are the same, don't recreate work orders.
if not (bom.operation_ids and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.operation_ids != bom.operation_ids)):
continue
for operation in bom.operation_ids:
if operation._skip_operation_line(bom_data['product']):
continue
workorders_values += [{
'name': operation.name,
'production_id': production.id,
'workcenter_id': operation.workcenter_id.id,
'product_uom_id': production.product_uom_id.id,
'operation_id': operation.id,
'state': 'pending',
}]
production.workorder_ids = [(5, 0)] + [(0, 0, value) for value in workorders_values]
for workorder in production.workorder_ids:
workorder.duration_expected = workorder._get_duration_expected()
@api.model
def _get_default_picking_type_id(self, company_id):
return self.env['stock.picking.type'].search([
('code', '=', 'mrp_operation'),
('warehouse_id.company_id', '=', company_id),
], limit=1).id
def _get_move_finished_values(self, product_id, product_uom_qty, product_uom, operation_id=False, byproduct_id=False, cost_share=0):
group_orders = self.procurement_group_id.mrp_production_ids
@@ -902,7 +885,7 @@ class MrpProduction(models.Model):
'product_uom': product_uom,
'operation_id': operation_id,
'byproduct_id': byproduct_id,
'name': self.name,
'name': _('New'),
'date': self._get_date_planned_finished(),
'date_deadline': self.date_deadline,
'picking_type_id': self.picking_type_id.id,
@@ -911,7 +894,7 @@ class MrpProduction(models.Model):
'company_id': self.company_id.id,
'production_id': self.id,
'warehouse_id': self.location_dest_id.warehouse_id.id,
'origin': self.name,
'origin': self.product_id.partner_ref,
'group_id': self.procurement_group_id.id,
'propagate_cancel': self.propagate_cancel,
'move_dest_ids': [(4, x.id) for x in self.move_dest_ids if not byproduct_id],
@@ -946,9 +929,6 @@ class MrpProduction(models.Model):
It is important that the special *2Many commands used here remain as long as function
is used within onchanges.
"""
# keep manual entries
list_move_finished = [(4, move.id) for move in self.move_finished_ids.filtered(
lambda m: not m.byproduct_id and m.product_id != self.product_id)]
list_move_finished = []
moves_finished_values = self._get_moves_finished_values()
moves_byproduct_dict = {move.byproduct_id.id: move for move in self.move_finished_ids.filtered(lambda m: m.byproduct_id)}
@@ -956,12 +936,12 @@ class MrpProduction(models.Model):
for move_finished_values in moves_finished_values:
if move_finished_values.get('byproduct_id') in moves_byproduct_dict:
# update existing entries
list_move_finished += [(1, moves_byproduct_dict[move_finished_values['byproduct_id']].id, move_finished_values)]
list_move_finished += [Command.update(moves_byproduct_dict[move_finished_values['byproduct_id']].id, move_finished_values)]
elif move_finished_values.get('product_id') == self.product_id.id and move_finished:
list_move_finished += [(1, move_finished.id, move_finished_values)]
list_move_finished += [Command.update(move_finished.id, move_finished_values)]
else:
# add new entries
list_move_finished += [(0, 0, move_finished_values)]
list_move_finished += [Command.create(move_finished_values)]
self.move_finished_ids = list_move_finished
def _get_moves_raw_values(self):
@@ -993,7 +973,7 @@ class MrpProduction(models.Model):
source_location = self.location_src_id
data = {
'sequence': bom_line.sequence if bom_line else 10,
'name': self.name,
'name': _('New'),
'date': self.date_planned_start,
'date_deadline': self.date_planned_start,
'bom_line_id': bom_line.id if bom_line else False,
@@ -1066,6 +1046,15 @@ class MrpProduction(models.Model):
self.env['procurement.group'].run(procurements)
return update_info
@api.ondelete(at_uninstall=False)
def _unlink_except_done(self):
if any(production.state == 'done' for production in self):
raise UserError(_('Cannot delete a manufacturing order in done state.'))
not_cancel = self.filtered(lambda m: m.state != 'cancel')
if not_cancel:
productions_name = ', '.join([prod.display_name for prod in not_cancel])
raise UserError(_('%s cannot be deleted. Try to cancel them before.', productions_name))
def _get_ready_to_produce_state(self):
""" returns 'assigned' if enough components are reserved in order to complete
the first operation of the bom. If not returns 'waiting'
@@ -1997,15 +1986,12 @@ class MrpProduction(models.Model):
production = self.env['mrp.production'].create({
'product_id': product_id.id,
'bom_id': bom_id.id,
'picking_type_id': bom_id.picking_type_id or self._get_default_picking_type(),
'picking_type_id': bom_id.picking_type_id or self._get_default_picking_type_id(self.env.company.id),
'product_qty': sum(production.product_uom_qty for production in self),
'product_uom_id': product_id.uom_id.id,
'user_id': user_id.id,
'origin': ",".join(sorted([production.name for production in self])),
})
self.env['stock.move'].create(production._get_moves_raw_values())
self.env['stock.move'].create(production._get_moves_finished_values())
production._create_workorder()
for move in production.move_raw_ids:
for field, vals in origs[move.bom_line_id.id].items():
+8 -7
View File
@@ -91,9 +91,9 @@ class MrpWorkorder(models.Model):
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
duration_expected = fields.Float(
'Expected Duration', digits=(16, 2), default=60.0,
'Expected Duration', digits=(16, 2), default=60.0, compute='_compute_duration_expected',
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
help="Expected duration (in minutes)")
readonly=False, store=True, help="Expected duration (in minutes)")
duration = fields.Float(
'Real Duration', compute='_compute_duration', inverse='_set_duration',
readonly=False, store=True, copy=False)
@@ -299,6 +299,11 @@ class MrpWorkorder(models.Model):
rounding = order.production_id.product_uom_id.rounding
order.is_produced = float_compare(order.qty_produced, order.production_id.product_qty, precision_rounding=rounding) >= 0
@api.depends('operation_id', 'workcenter_id', 'qty_production')
def _compute_duration_expected(self):
for workorder in self:
workorder.duration_expected = workorder._get_duration_expected()
@api.depends('time_ids.duration', 'qty_produced')
def _compute_duration(self):
for order in self:
@@ -408,12 +413,8 @@ class MrpWorkorder(models.Model):
compute_leaves=True, domain=[('time_type', 'in', ['leave', 'other'])]
)
@api.onchange('operation_id', 'workcenter_id', 'qty_production')
def _onchange_expected_duration(self):
self.duration_expected = self._get_duration_expected()
def write(self, values):
if 'production_id' in values:
if 'production_id' in values and any(values['production_id'] != w.production_id.id for w in self):
raise UserError(_('You cannot link this work order to another manufacturing order.'))
if 'workcenter_id' in values:
for workorder in self:
-7
View File
@@ -40,7 +40,6 @@ class StockRule(models.Model):
@api.model
def _run_manufacture(self, procurements):
productions_values_by_company = defaultdict(list)
errors = []
for procurement, rule in procurements:
if float_compare(procurement.product_qty, 0, precision_rounding=procurement.product_uom.rounding) <= 0:
# If procurement contains negative quantity, don't create a MO that would be for a negative value.
@@ -49,15 +48,9 @@ class StockRule(models.Model):
productions_values_by_company[procurement.company_id.id].append(rule._prepare_mo_vals(*procurement, bom))
if errors:
raise ProcurementException(errors)
for company_id, productions_values in productions_values_by_company.items():
# create the MO as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example)
productions = self.env['mrp.production'].with_user(SUPERUSER_ID).sudo().with_company(company_id).create(productions_values)
self.env['stock.move'].sudo().create(productions._get_moves_raw_values())
self.env['stock.move'].sudo().create(productions._get_moves_finished_values())
productions._create_workorder()
productions.filtered(lambda p: not p.orderpoint_id or not p.move_raw_ids).action_confirm()
for production in productions:
-11
View File
@@ -271,17 +271,6 @@ class MrpProduction(models.Model):
def _populate(self, size):
productions = super()._populate(size)
def fill_mo_with_bom_info():
productions_with_bom = productions.filtered('bom_id')
_logger.info("Create Raw moves of MO(s) with bom (%d)" % len(productions_with_bom))
self.env['stock.move'].create(productions_with_bom._get_moves_raw_values())
_logger.info("Create Finished moves of MO(s) with bom (%d)" % len(productions_with_bom))
self.env['stock.move'].create(productions_with_bom._get_moves_finished_values())
_logger.info("Create Workorder moves of MO(s) with bom (%d)" % len(productions_with_bom))
productions_with_bom._create_workorder()
fill_mo_with_bom_info()
def confirm_bom_mo(sample_ratio):
# Confirm X % of prototype MO
random = populate.Random('confirm_bom_mo')
+2 -5
View File
@@ -316,13 +316,11 @@ class TestMrpProductionBackorder(TestMrpCommon):
backorder.save().action_backorder()
return mo.procurement_group_id.mrp_production_ids[-1]
default_picking_type_id = self.env['mrp.production']._get_default_picking_type()
default_picking_type_id = self.env['mrp.production']._get_default_picking_type_id(self.env.company.id)
default_picking_type = self.env['stock.picking.type'].browse(default_picking_type_id)
mo_sequence = default_picking_type.sequence_id
mo_sequence.prefix = "WH-MO-"
initial_mo_name = mo_sequence.prefix + str(mo_sequence.number_next_actual).zfill(mo_sequence.padding)
production = self.generate_mo(qty_final=5)[0]
self.assertEqual(production.name, initial_mo_name)
@@ -331,7 +329,6 @@ class TestMrpProductionBackorder(TestMrpCommon):
self.assertEqual(backorder.name, initial_mo_name + "-002")
backorder.backorder_sequence = 998
for seq in [998, 999, 1000]:
new_backorder = produce_one(backorder)
self.assertEqual(backorder.name, initial_mo_name + "-" + str(seq))
@@ -425,7 +422,7 @@ class TestMrpProductionBackorder(TestMrpCommon):
'location_id': self.stock_location.id,
})._apply_inventory()
default_picking_type_id = self.env['mrp.production']._get_default_picking_type()
default_picking_type_id = self.env['mrp.production']._get_default_picking_type_id(self.env.company.id)
default_picking_type = self.env['stock.picking.type'].browse(default_picking_type_id)
# make sure generated MO will auto-assign
+10 -2
View File
@@ -337,6 +337,7 @@ class TestMrpOrder(TestMrpCommon):
self.assertEqual(production.workorder_ids.duration_expected, 165)
def test_update_quantity_4(self):
""" Workcenter 1 has 10' start time and 5' stop time """
bom = self.env['mrp.bom'].create({
'product_id': self.product_6.id,
'product_tmpl_id': self.product_6.product_tmpl_id.id,
@@ -364,6 +365,14 @@ class TestMrpOrder(TestMrpCommon):
mo_form = Form(production)
mo_form.product_qty = 3
production = mo_form.save()
self.assertEqual(production.workorder_ids.duration_expected, 40)
production.action_confirm()
update_quantity_wizard = self.env['change.production.qty'].create({
'mo_id': production.id,
'product_qty': 9,
})
update_quantity_wizard.change_prod_qty()
self.assertEqual(production.workorder_ids.duration_expected, 90)
def test_update_quantity_5(self):
@@ -549,8 +558,7 @@ class TestMrpOrder(TestMrpCommon):
self.assertEqual(mo.move_finished_ids[0].date, datetime(2023, 5, 15, 10, 0))
mo.action_confirm()
mo.button_plan()
with Form(mo) as frm:
frm.date_planned_start = datetime(2024, 5, 15, 9, 0)
mo.date_planned_start = datetime(2024, 5, 15, 9, 0)
self.assertEqual(mo.move_finished_ids[0].date, datetime(2024, 5, 15, 10, 0))
def test_rounding(self):
+2 -4
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from datetime import timedelta
from odoo import fields
from odoo.tests import Form
@@ -250,9 +250,7 @@ class TestProcurement(TestMrpCommon):
self.assertEqual(move_orig.product_qty, 10, 'the quantity to produce is not good relative to the move')
new_sheduled_date = fields.Datetime.to_datetime(mo.date_planned_start) + timedelta(days=5)
mo_form = Form(mo)
mo_form.date_planned_start = new_sheduled_date
mo = mo_form.save()
mo.date_planned_start = new_sheduled_date
self.assertAlmostEqual(mo.move_raw_ids.date, mo.date_planned_start, delta=timedelta(seconds=1))
self.assertAlmostEqual(mo.move_finished_ids.date, mo.date_planned_finished, delta=timedelta(seconds=1))
+1 -1
View File
@@ -211,7 +211,7 @@
</button>
<label for="product_uom_id" string="" class="oe_inline"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="product_uom_id" options="{'no_open': True, 'no_create': True}" force_save="1" groups="uom.group_uom" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="product_uom_id" options="{'no_open': True, 'no_create': True}" groups="uom.group_uom" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<span class='font-weight-bold'>To Produce</span>
<button type="object" name="action_product_forecast_report" title="Forecast Report" icon="fa-area-chart" attrs="{'invisible': [('forecasted_issue', '=', True)]}"/>
<button type="object" name="action_product_forecast_report" title="Forecast Report" icon="fa-area-chart" attrs="{'invisible': [('forecasted_issue', '=', False)]}" class="text-danger"/>
@@ -23,14 +23,6 @@
<field name="date_planned_start" eval="(datetime.now() - relativedelta(weeks=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<function model="stock.move" name="create">
<value model="stock.move" eval="
obj().env.ref('mrp_account.mrp_production_drawer_2')._get_moves_raw_values() +
obj().env.ref('mrp_account.mrp_production_drawer_2')._get_moves_finished_values() +
obj().env.ref('mrp_account.mrp_production_drawer_3')._get_moves_raw_values() +
obj().env.ref('mrp_account.mrp_production_drawer_3')._get_moves_finished_values()"/>
</function>
<function model="mrp.production" name="action_confirm" eval="[[
ref('mrp_account.mrp_production_drawer_2'),
ref('mrp_account.mrp_production_drawer_3'),
@@ -121,6 +121,7 @@ class TestAnalyticAccount(TransactionCase):
self.assertEqual(len(mo.workorder_ids.wc_analytic_account_line_id), 0)
# change duration to 60
mo_form = Form(mo)
with mo_form.workorder_ids.edit(0) as line_edit:
line_edit.duration = 60.0
mo_form.save()
@@ -161,6 +162,7 @@ class TestAnalyticAccount(TransactionCase):
self.assertEqual(len(mo.workorder_ids.wc_analytic_account_line_id), 0)
# change duration to 60
mo_form = Form(mo)
with mo_form.workorder_ids.edit(0) as line_edit:
line_edit.duration = 60.0
mo_form.save()
@@ -506,7 +506,6 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon):
move.product_uom_qty = 1
picking_receipt = picking_form.save()
picking_receipt.action_confirm()
self.assertEqual(picking_receipt.display_action_record_components, 'facultative')
action = picking_receipt.action_record_components()
mo = self.env['mrp.production'].browse(action['res_id'])