diff --git a/addons/mrp/models/__init__.py b/addons/mrp/models/__init__.py
index 237e0273151..c6b32ff80fc 100644
--- a/addons/mrp/models/__init__.py
+++ b/addons/mrp/models/__init__.py
@@ -2,7 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mrp_document
-from . import mrp_abstract_workorder
from . import res_config_settings
from . import mrp_bom
from . import mrp_routing
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index f45f5e65373..44d3589b113 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -539,7 +539,6 @@ class MrpProduction(models.Model):
@api.onchange('product_qty', 'product_uom_id')
def _onchange_product_qty(self):
- product_qty = self.product_uom_id._compute_quantity(self.product_qty, self.product_uom_id)
for workorder in self.workorder_ids:
if not workorder.product_uom_id:
workorder.product_uom_id = self.product_uom_id
@@ -601,25 +600,7 @@ class MrpProduction(models.Model):
@api.onchange('qty_producing', 'lot_producing_id')
def _onchange_producing(self):
- if self.product_id.tracking == 'serial':
- qty_producing_uom = self.product_uom_id._compute_quantity(self.qty_producing, self.product_id.uom_id, rounding_method='HALF-UP')
- if qty_producing_uom != 1:
- self.qty_producing = self.product_id.uom_id._compute_quantity(1, self.product_uom_id, rounding_method='HALF-UP')
- for move in (self.move_raw_ids | self.move_finished_ids.filtered(lambda m: m.product_id != self.product_id)):
- if move.state in ('done', 'cancel'):
- continue
- # Do not update extra product quantities
- if float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding):
- continue
- if move.has_tracking != 'none' or move.state == 'done':
- continue
- vals = move._update_quantity_done(self)
- if vals.get('to_create'):
- for res in vals['to_create']:
- move.move_line_ids.new(res)
- if vals.get('to_write'):
- for move_line, res in vals['to_write']:
- move_line.update(res)
+ self._set_qty_producing()
@api.onchange('bom_id')
def _onchange_workorder_ids(self):
@@ -723,7 +704,6 @@ class MrpProduction(models.Model):
'product_uom_id': production.product_id.uom_id.id,
'operation_id': operation.id,
'state': 'pending',
- 'qty_producing': production.product_id.tracking == 'serial' and 1.0 or product_qty,
'consumption': production.consumption,
}]
production.workorder_ids = [(5, 0)] + [(0, 0, value) for value in workorders_values]
@@ -826,6 +806,19 @@ class MrpProduction(models.Model):
}
return data
+ def _set_qty_producing(self):
+ if self.product_id.tracking == 'serial':
+ qty_producing_uom = self.product_uom_id._compute_quantity(self.qty_producing, self.product_id.uom_id, rounding_method='HALF-UP')
+ if qty_producing_uom != 1:
+ self.qty_producing = self.product_id.uom_id._compute_quantity(1, self.product_uom_id, rounding_method='HALF-UP')
+
+ for move in (self.move_raw_ids | self.move_finished_ids.filtered(lambda m: m.product_id != self.product_id)):
+ if move._should_bypass_set_qty_producing():
+ continue
+ new_qty = self.product_uom_id._compute_quantity((self.qty_producing - self.qty_produced) * move.unit_factor, self.product_uom_id, rounding_method='HALF-UP')
+ move.move_line_ids.filtered(lambda ml: ml.state not in ('done', 'cancel')).qty_done = 0
+ move.move_line_ids = move._set_quantity_done_prepare_vals(new_qty)
+
def _update_raw_move(self, bom_line, line_data):
""" :returns update_move, old_quantity, new_quantity """
quantity = line_data['qty']
@@ -981,24 +974,7 @@ class MrpProduction(models.Model):
if self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id).move_line_ids:
self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id).move_line_ids.lot_id = self.lot_producing_id
if self.product_id.tracking == 'serial':
- # copy/paste from _onchange_producing
- qty_producing_uom = self.product_uom_id._compute_quantity(self.qty_producing, self.product_id.uom_id, rounding_method='HALF-UP')
- if qty_producing_uom != 1:
- self.qty_producing = self.product_id.uom_id._compute_quantity(1, self.product_uom_id, rounding_method='HALF-UP')
- for move in (self.move_raw_ids | self.move_finished_ids.filtered(lambda m: m.product_id != self.product_id)):
- if move.state in ('done', 'cancel'):
- continue
- if float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding):
- continue
- if move.has_tracking != 'none':
- continue
- vals = move._update_quantity_done(self)
- if vals.get('to_create'):
- for res in vals['to_create']:
- move.move_line_ids.create(res)
- if vals.get('to_write'):
- for move_line, res in vals['to_write']:
- move_line.write(res)
+ self._set_qty_producing()
def action_confirm(self):
self._check_company()
@@ -1265,8 +1241,6 @@ class MrpProduction(models.Model):
moves_to_finish = order.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
moves_to_finish = moves_to_finish._action_done(cancel_backorder=cancel_backorder)
- order.workorder_ids.mapped('raw_workorder_line_ids').unlink()
- order.workorder_ids.mapped('finished_workorder_line_ids').unlink()
order.action_assign()
consume_move_lines = moves_to_do.mapped('move_line_ids')
order.move_finished_ids.move_line_ids.consume_line_ids = [(6, 0, consume_move_lines.ids)]
@@ -1294,7 +1268,7 @@ class MrpProduction(models.Model):
'origin': mo_source.origin
}
- def _generate_backorder_productions(self):
+ def _generate_backorder_productions(self, close_mo=True):
backorders = self.env['mrp.production']
for production in self:
if production.backorder_sequence == 0: # Activate backorder naming
@@ -1307,17 +1281,26 @@ class MrpProduction(models.Model):
wo.qty_producing = 1
else:
wo.qty_producing = wo.qty_remaining
-
- production.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel')).write({
- 'raw_material_production_id': backorder_mo.id,
- })
+ if close_mo:
+ production.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel')).write({
+ 'raw_material_production_id': backorder_mo.id,
+ })
+ production.move_finished_ids.filtered(lambda m: m.state not in ('done', 'cancel')).write({
+ 'production_id': backorder_mo.id,
+ })
+ else:
+ for move in production.move_raw_ids | production.move_finished_ids:
+ new_move = self.env['stock.move'].browse(move._split(move.unit_factor * production.qty_producing))
+ if move.raw_material_production_id:
+ new_move.raw_material_production_id = backorder_mo.id
+ else:
+ new_move.production_id = backorder_mo.id
backorders |= backorder_mo
production.name = self._get_name_backorder(production.name, production.backorder_sequence)
for wo in backorder_mo.workorder_ids:
- wo.duration_expected = wo._get_duration_expected()
-
+ wo.duration_expected = wo._get_duration_expected(wo.workcenter_id)
backorders.action_confirm()
# Remove the serial move line without reserved quantity. Post inventory will assigned all the non done moves
# So those move lines are duplicated.
diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py
index 1542c480e90..5272acd24b3 100644
--- a/addons/mrp/models/mrp_workorder.py
+++ b/addons/mrp/models/mrp_workorder.py
@@ -14,7 +14,7 @@ from odoo.tools import float_compare, float_round, format_datetime
class MrpWorkorder(models.Model):
_name = 'mrp.workorder'
_description = 'Work Order'
- _inherit = ['mail.thread', 'mail.activity.mixin', 'mrp.abstract.workorder']
+ _inherit = ['mail.thread', 'mail.activity.mixin']
def _read_group_workcenter_id(self, workcenters, domain, order):
workcenter_ids = self.env.context.get('default_workcenter_id')
@@ -25,10 +25,6 @@ class MrpWorkorder(models.Model):
name = fields.Char(
'Work Order', required=True,
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
- company_id = fields.Many2one(
- 'res.company', 'Company',
- default=lambda self: self.env.company,
- required=True, index=True, readonly=True)
workcenter_id = fields.Many2one(
'mrp.workcenter', 'Work Center', required=True,
states={'done': [('readonly', True)], 'cancel': [('readonly', True)], 'progress': [('readonly', True)]},
@@ -36,6 +32,11 @@ class MrpWorkorder(models.Model):
working_state = fields.Selection(
string='Workcenter Status', related='workcenter_id.working_state', readonly=False,
help='Technical: used in views only')
+ product_id = fields.Many2one(related='production_id.product_id', readonly=True, store=True, check_company=True)
+ product_tracking = fields.Selection(related="product_id.tracking")
+ product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True, readonly=True)
+ use_create_components_lots = fields.Boolean(related="production_id.picking_type_id.use_create_components_lots")
+ production_id = fields.Many2one('mrp.production', 'Manufacturing Order', required=True, check_company=True)
production_availability = fields.Selection(
string='Stock Availability', readonly=True,
related='production_id.reservation_state', store=True,
@@ -45,6 +46,10 @@ class MrpWorkorder(models.Model):
related='production_id.state',
help='Technical: used in views only.')
qty_production = fields.Float('Original Production Quantity', readonly=True, related='production_id.product_qty')
+ company_id = fields.Many2one(related='production_id.company_id')
+ qty_producing = fields.Float(
+ compute='_compute_qty_producing', inverse='_set_qty_producing',
+ string='Currently Produced Quantity', digits='Product Unit of Measure')
qty_remaining = fields.Float('Quantity To Be Produced', compute='_compute_qty_remaining', digits='Product Unit of Measure')
qty_produced = fields.Float(
'Quantity', default=0.0,
@@ -118,8 +123,9 @@ class MrpWorkorder(models.Model):
'stock.move.line', 'workorder_id', 'Moves to Track',
help="Inventory moves for which you must scan a lot number at this work order")
finished_lot_id = fields.Many2one(
- 'stock.production.lot', 'Lot/Serial Number', domain="[('id', 'in', allowed_lots_domain)]",
- states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, check_company=True)
+ 'stock.production.lot', string='Lot/Serial Number', compute='_compute_finished_lot_id',
+ inverse='_set_finished_lot_id', domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]",
+ check_company=True)
time_ids = fields.One2many(
'mrp.workcenter.productivity', 'workorder_id', copy=False)
is_user_working = fields.Boolean(
@@ -132,14 +138,15 @@ class MrpWorkorder(models.Model):
scrap_ids = fields.One2many('stock.scrap', 'workorder_id')
scrap_count = fields.Integer(compute='_compute_scrap_move_count', string='Scrap Move')
production_date = fields.Datetime('Production Date', related='production_id.date_planned_start', store=True, readonly=False)
- raw_workorder_line_ids = fields.One2many('mrp.workorder.line',
- 'raw_workorder_id', string='Components')
- finished_workorder_line_ids = fields.One2many('mrp.workorder.line',
- 'finished_workorder_id', string='By-products')
- allowed_lots_domain = fields.One2many(comodel_name='stock.production.lot', compute="_compute_allowed_lots_domain")
is_finished_lines_editable = fields.Boolean(compute='_compute_is_finished_lines_editable')
json_popover = fields.Char('Popover Data JSON', compute='_compute_json_popover')
show_json_popover = fields.Boolean('Show Popover?', compute='_compute_json_popover')
+ consumption = fields.Selection([
+ ('strict', 'Strict'),
+ ('warning', 'Warning'),
+ ('flexible', 'Flexible')],
+ required=True,
+ )
@api.depends('production_state', 'date_planned_start', 'date_planned_finished')
def _compute_json_popover(self):
@@ -197,6 +204,25 @@ class MrpWorkorder(models.Model):
'replan': color_icon not in [False, 'text-primary']
})
+ @api.depends('production_id.lot_producing_id')
+ def _compute_finished_lot_id(self):
+ for workorder in self:
+ workorder.finished_lot_id = workorder.production_id.lot_producing_id
+
+ def _set_finished_lot_id(self):
+ for workorder in self:
+ workorder.production_id.lot_producing_id = workorder.finished_lot_id
+
+ @api.depends('production_id.qty_producing')
+ def _compute_qty_producing(self):
+ for workorder in self:
+ workorder.qty_producing = workorder.production_id.qty_producing
+
+ def _set_qty_producing(self):
+ for workorder in self:
+ workorder.production_id.qty_producing = workorder.qty_producing
+ workorder.production_id._set_qty_producing()
+
# Both `date_planned_start` and `date_planned_finished` are related fields on `leave_id`. Let's say
# we slide a workorder on a gantt view, a single call to write is made with both
# fields Changes. As the ORM doesn't batch the write on related fields and instead
@@ -224,53 +250,6 @@ class MrpWorkorder(models.Model):
workorder.is_finished_lines_editable = True
else:
workorder.is_finished_lines_editable = False
-
- @api.depends('production_id.workorder_ids.finished_workorder_line_ids',
- 'production_id.workorder_ids.finished_workorder_line_ids.qty_done',
- 'production_id.workorder_ids.finished_workorder_line_ids.lot_id')
- def _compute_allowed_lots_domain(self):
- """ Check if all the finished products has been assigned to a serial
- number or a lot in other workorders. If yes, restrict the selectable lot
- to the lot/sn used in other workorders.
- """
- productions = self.mapped('production_id')
- treated = self.browse()
- for production in productions:
- if production.state == 'draft':
- continue
- if production.product_id.tracking == 'none':
- continue
-
- rounding = production.product_uom_id.rounding
- finished_workorder_lines = production.workorder_ids.mapped('finished_workorder_line_ids').filtered(lambda wl: wl.product_id == production.product_id)
- qties_done_per_lot = defaultdict(list)
- for finished_workorder_line in finished_workorder_lines:
- # It is possible to have finished workorder lines without a lot (eg using the dummy
- # test type). Ignore them when computing the allowed lots.
- if finished_workorder_line.lot_id:
- qties_done_per_lot[finished_workorder_line.lot_id.id].append(finished_workorder_line.qty_done)
-
- qty_to_produce = production.product_qty
- allowed_lot_ids = self.env['stock.production.lot']
- qty_produced = sum([max(qty_dones) for qty_dones in qties_done_per_lot.values()])
- if float_compare(qty_produced, qty_to_produce, precision_rounding=rounding) < 0:
- # If we haven't produced enough, all lots are available
- allowed_lot_ids = self.env['stock.production.lot'].search([
- ('product_id', '=', production.product_id.id),
- ('company_id', '=', production.company_id.id),
- ])
- else:
- # If we produced enough, only the already produced lots are available
- allowed_lot_ids = self.env['stock.production.lot'].browse(qties_done_per_lot.keys())
- workorders = production.workorder_ids.filtered(lambda wo: wo.state not in ('done', 'cancel'))
- for workorder in workorders:
- if workorder.product_tracking == 'serial':
- workorder.allowed_lots_domain = allowed_lot_ids - workorder.finished_workorder_line_ids.filtered(lambda wl: wl.product_id == production.product_id).mapped('lot_id')
- else:
- workorder.allowed_lots_domain = allowed_lot_ids
- treated |= workorder
- (self - treated).allowed_lots_domain = False
-
def name_get(self):
res = []
for wo in self:
@@ -339,18 +318,6 @@ class MrpWorkorder(models.Model):
for workorder in self:
workorder.scrap_count = count_data.get(workorder.id, 0)
- @api.onchange('finished_lot_id')
- def _onchange_finished_lot_id(self):
- """When the user changes the lot being currently produced, suggest
- a quantity to produce consistent with the previous workorders. """
- previous_wo = self.env['mrp.workorder'].search([
- ('next_work_order_id', '=', self.id)
- ])
- if previous_wo:
- line = previous_wo.finished_workorder_line_ids.filtered(lambda line: line.product_id == self.product_id and line.lot_id == self.finished_lot_id)
- if line:
- self.qty_producing = line.qty_done
-
@api.onchange('date_planned_finished')
def _onchange_date_planned_finished(self):
if self.date_planned_start and self.date_planned_finished:
@@ -453,196 +420,15 @@ class MrpWorkorder(models.Model):
'workorder_id': workorders_by_bom[production.bom_id][-1:].id
})
- for workorder in workorders:
- workorder._apply_update_workorder_lines()
-
for workorders in workorders_by_bom.values():
if workorders[0].state == 'pending':
workorders[0].state = 'ready'
for workorder in workorders:
workorder._start_nextworkorder()
- def _generate_wo_lines(self):
- """ Generate workorder line """
- self.ensure_one()
- moves = (self.move_raw_ids | self.move_finished_ids).filtered(
- lambda move: move.state not in ('done', 'cancel')
- )
- for move in moves:
- qty_to_consume = self._prepare_component_quantity(move, self.qty_producing)
- line_values = self._generate_lines_values(move, qty_to_consume)
- self.env['mrp.workorder.line'].create(line_values)
-
- def _apply_update_workorder_lines(self):
- """ update existing line on the workorder. It could be trigger manually
- after a modification of qty_producing.
- """
- self.ensure_one()
- line_values = self._update_workorder_lines()
- self.env['mrp.workorder.line'].create(line_values['to_create'])
- if line_values['to_delete']:
- line_values['to_delete'].unlink()
- for line, vals in line_values['to_update'].items():
- line.write(vals)
-
- def _refresh_wo_lines(self):
- """ Modify exisiting workorder line in order to match the reservation on
- stock move line. The strategy is to remove the line that were not
- processed yet then call _generate_lines_values that recreate workorder
- line depending the reservation.
- """
- for workorder in self:
- raw_moves = workorder.move_raw_ids.filtered(
- lambda move: move.state not in ('done', 'cancel')
- )
- wl_to_unlink = self.env['mrp.workorder.line']
- for move in raw_moves:
- rounding = move.product_uom.rounding
- qty_already_consumed = 0.0
- workorder_lines = workorder.raw_workorder_line_ids.filtered(lambda w: w.move_id == move)
- for wl in workorder_lines:
- if not wl.qty_done:
- wl_to_unlink |= wl
- continue
-
- qty_already_consumed += wl.qty_done
- qty_to_consume = self._prepare_component_quantity(move, workorder.qty_producing)
- wl_to_unlink.unlink()
- if float_compare(qty_to_consume, qty_already_consumed, precision_rounding=rounding) > 0:
- line_values = workorder._generate_lines_values(move, qty_to_consume - qty_already_consumed)
- self.env['mrp.workorder.line'].create(line_values)
-
- def _defaults_from_finished_workorder_line(self, reference_lot_lines):
- for r_line in reference_lot_lines:
- # see which lot we could suggest and its related qty_producing
- if not r_line.lot_id:
- continue
- candidates = self.finished_workorder_line_ids.filtered(lambda line: line.lot_id == r_line.lot_id)
- rounding = self.product_uom_id.rounding
- if not candidates:
- self.write({
- 'finished_lot_id': r_line.lot_id.id,
- 'qty_producing': r_line.qty_done,
- })
- return True
- elif float_compare(candidates.qty_done, r_line.qty_done, precision_rounding=rounding) < 0:
- self.write({
- 'finished_lot_id': r_line.lot_id.id,
- 'qty_producing': r_line.qty_done - candidates.qty_done,
- })
- return True
- return False
-
- def record_production(self):
- if not self:
- return True
-
- self.ensure_one()
- self._check_sn_uniqueness()
- self._check_company()
- if float_compare(self.qty_producing, 0, precision_rounding=self.product_uom_id.rounding) <= 0:
- raise UserError(_('Please set the quantity you are currently producing. It should be different from zero.'))
- if self.production_id.product_id.tracking != 'none' and not self.finished_lot_id and self.move_raw_ids:
- raise UserError(_('You should provide a lot for the final product'))
- if 'check_ids' not in self:
- for line in self.raw_workorder_line_ids | self.finished_workorder_line_ids:
- line._check_line_sn_uniqueness()
- # If last work order, then post lots used
- if not self.next_work_order_id:
- self._update_finished_move()
-
- # Transfer quantities from temporary to final move line or make them final
- self._update_moves()
-
- # Transfer lot (if present) and quantity produced to a finished workorder line
- if self.product_tracking != 'none':
- self._create_or_update_finished_line()
-
- # Update workorder quantity produced
- self.qty_produced += self.qty_producing
-
- # Suggest a finished lot on the next workorder
- if self.next_work_order_id and self.product_tracking != 'none' and (not self.next_work_order_id.finished_lot_id or self.next_work_order_id.finished_lot_id == self.finished_lot_id):
- self.next_work_order_id._defaults_from_finished_workorder_line(self.finished_workorder_line_ids)
- # As we may have changed the quantity to produce on the next workorder,
- # make sure to update its wokorder lines
- self.next_work_order_id._apply_update_workorder_lines()
-
- # One a piece is produced, you can launch the next work order
- self._start_nextworkorder()
-
- # Test if the production is done
- rounding = self.production_id.product_uom_id.rounding
- if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) < 0:
- previous_wo = self.env['mrp.workorder']
- if self.product_tracking != 'none':
- previous_wo = self.env['mrp.workorder'].search([
- ('next_work_order_id', '=', self.id)
- ])
- candidate_found_in_previous_wo = False
- if previous_wo:
- candidate_found_in_previous_wo = self._defaults_from_finished_workorder_line(previous_wo.finished_workorder_line_ids)
- if not candidate_found_in_previous_wo:
- # self is the first workorder
- self.qty_producing = self.qty_remaining
- self.finished_lot_id = False
- if self.product_tracking == 'serial':
- self.qty_producing = 1
-
- self._apply_update_workorder_lines()
- else:
- self.qty_producing = 0
- self.button_finish()
- return True
-
def _get_byproduct_move_to_update(self):
return self.production_id.move_finished_ids.filtered(lambda x: (x.product_id.id != self.production_id.product_id.id) and (x.state not in ('done', 'cancel')))
- def _create_or_update_finished_line(self):
- """
- 1. Check that the final lot and the quantity producing is valid regarding
- other workorders of this production
- 2. Save final lot and quantity producing to suggest on next workorder
- """
- self.ensure_one()
- final_lot_quantity = self.qty_production
- rounding = self.product_uom_id.rounding
- # Get the max quantity possible for current lot in other workorders
- for workorder in (self.production_id.workorder_ids - self):
- # We add the remaining quantity to the produced quantity for the
- # current lot. For 5 finished products: if in the first wo it
- # creates 4 lot A and 1 lot B and in the second it create 3 lot A
- # and it remains 2 units to product, it could produce 5 lot A.
- # In this case we select 4 since it would conflict with the first
- # workorder otherwise.
- line = workorder.finished_workorder_line_ids.filtered(lambda line: line.lot_id == self.finished_lot_id)
- line_without_lot = workorder.finished_workorder_line_ids.filtered(lambda line: line.product_id == workorder.product_id and not line.lot_id)
- quantity_remaining = workorder.qty_remaining + line_without_lot.qty_done
- quantity = line.qty_done + quantity_remaining
- if line and float_compare(quantity, final_lot_quantity, precision_rounding=rounding) <= 0:
- final_lot_quantity = quantity
- elif float_compare(quantity_remaining, final_lot_quantity, precision_rounding=rounding) < 0:
- final_lot_quantity = quantity_remaining
-
- # final lot line for this lot on this workorder.
- current_lot_lines = self.finished_workorder_line_ids.filtered(lambda line: line.lot_id == self.finished_lot_id)
-
- # this lot has already been produced
- if float_compare(final_lot_quantity, current_lot_lines.qty_done + self.qty_producing, precision_rounding=rounding) < 0:
- raise UserError(_('You have produced %s %s of lot %s in the previous workorder. You are trying to produce %s in this one') %
- (final_lot_quantity, self.product_id.uom_id.name, self.finished_lot_id.name, current_lot_lines.qty_done + self.qty_producing))
-
- # Update workorder line that regiter final lot created
- if not current_lot_lines:
- current_lot_lines = self.env['mrp.workorder.line'].create({
- 'finished_workorder_id': self.id,
- 'product_id': self.product_id.id,
- 'lot_id': self.finished_lot_id.id,
- 'qty_done': self.qty_producing,
- })
- else:
- current_lot_lines.qty_done += self.qty_producing
-
def _start_nextworkorder(self):
rounding = self.product_id.uom_id.rounding
if self.next_work_order_id.state == 'pending' and (
@@ -910,46 +696,75 @@ class MrpWorkorder(models.Model):
res[wo1].append(wo2)
return res
-
-class MrpWorkorderLine(models.Model):
- _name = 'mrp.workorder.line'
- _inherit = ["mrp.abstract.workorder.line"]
- _description = "Workorder move line"
-
- raw_workorder_id = fields.Many2one('mrp.workorder', 'Component for Workorder',
- ondelete='cascade')
- finished_workorder_id = fields.Many2one('mrp.workorder', 'Finished Product for Workorder',
- ondelete='cascade')
-
- @api.onchange('qty_to_consume')
- def _onchange_qty_to_consume(self):
- # Update qty_done for products added in ready state
- wo = self.raw_workorder_id or self.finished_workorder_id
- if wo.state == 'ready':
- self.qty_done = self.qty_to_consume
-
- @api.model_create_multi
- def create(self, vals_list):
- res = super().create(vals_list)
- for line in res:
- wo = line.raw_workorder_id
- if wo and\
- wo.consumption == 'strict' and\
- wo.state == 'progress' and\
- line.product_id.id not in wo.production_id.bom_id.bom_line_ids.product_id.ids:
- raise UserError(_('You cannot consume additional component as the consumption defined on the Bill of Material is set to "strict"'))
- return res
-
@api.model
- def _get_raw_workorder_inverse_name(self):
- return 'raw_workorder_id'
+ def _prepare_component_quantity(self, move, qty_producing):
+ """ helper that computes quantity to consume (or to create in case of byproduct)
+ depending on the quantity producing and the move's unit factor"""
+ if move.product_id.tracking == 'serial':
+ uom = move.product_id.uom_id
+ else:
+ uom = move.product_uom
+ return move.product_uom._compute_quantity(
+ qty_producing * move.unit_factor,
+ uom,
+ round=False
+ )
- @api.model
- def _get_finished_workoder_inverse_name(self):
- return 'finished_workorder_id'
+ def _update_finished_move(self):
+ """ Update the finished move & move lines in order to set the finished
+ product lot on it as well as the produced quantity. This method get the
+ information either from the last workorder or from the Produce wizard."""
+ production_move = self.production_id.move_finished_ids.filtered(
+ lambda move: move.product_id == self.product_id and
+ move.state not in ('done', 'cancel')
+ )
+ if production_move and production_move.product_id.tracking != 'none':
+ if not self.finished_lot_id:
+ raise UserError(_('You need to provide a lot for the finished product.'))
+ move_line = production_move.move_line_ids.filtered(
+ lambda line: line.lot_id.id == self.finished_lot_id.id
+ )
+ if move_line:
+ if self.product_id.tracking == 'serial':
+ raise UserError(_('You cannot produce the same serial number twice.'))
+ move_line.product_uom_qty += self.qty_producing
+ move_line.qty_done += self.qty_producing
+ else:
+ location_dest_id = production_move.location_dest_id._get_putaway_strategy(self.product_id).id or production_move.location_dest_id.id
+ move_line.create({
+ 'move_id': production_move.id,
+ 'product_id': production_move.product_id.id,
+ 'lot_id': self.finished_lot_id.id,
+ 'product_uom_qty': self.qty_producing,
+ 'product_uom_id': self.product_uom_id.id,
+ 'qty_done': self.qty_producing,
+ 'location_id': production_move.location_id.id,
+ 'location_dest_id': location_dest_id,
+ })
+ else:
+ rounding = production_move.product_uom.rounding
+ production_move._set_quantity_done(
+ float_round(self.qty_producing, precision_rounding=rounding)
+ )
- def _get_final_lots(self):
- return (self.raw_workorder_id or self.finished_workorder_id).finished_lot_id
+ def _strict_consumption_check(self):
+ if self.consumption == 'strict':
+ for move in self.move_raw_ids:
+ qty_done = 0.0
+ for line in move.move_line_ids:
+ qty_done += line.product_uom_id._compute_quantity(line.qty_done, move.product_uom)
+ rounding = move.product_uom_id.rounding
+ if float_compare(qty_done, move.product_uom_qty, precision_rounding=rounding) != 0:
+ raise UserError(_('You should consume the quantity of %s defined in the BoM. If you want to consume more or less components, change the consumption setting on the BoM.') % move.product_id.name)
- def _get_production(self):
- return (self.raw_workorder_id or self.finished_workorder_id).production_id
+ def _check_sn_uniqueness(self):
+ """ Alert the user if the serial number as already been produced """
+ if self.product_tracking == 'serial' and self.finished_lot_id:
+ sml = self.env['stock.move.line'].search_count([
+ ('lot_id', '=', self.finished_lot_id.id),
+ ('location_id.usage', '=', 'production'),
+ ('qty_done', '=', 1),
+ ('state', '=', 'done')
+ ])
+ if sml:
+ raise UserError(_('This serial number for product %s has already been produced') % self.product_id.name)
diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py
index d711b6561c1..808c2aa3ddd 100644
--- a/addons/mrp/models/stock_move.py
+++ b/addons/mrp/models/stock_move.py
@@ -3,7 +3,7 @@
from odoo import api, exceptions, fields, models, _
from odoo.exceptions import UserError
-from odoo.tools import float_compare, float_round, float_repr, float_is_zero
+from odoo.tools import float_compare, float_round, float_is_zero
class StockMoveLine(models.Model):
@@ -188,12 +188,6 @@ class StockMove(models.Model):
if self.raw_material_production_id and self.has_tracking == 'none':
mo = self.raw_material_production_id
vals = self._update_quantity_done(mo)
- if vals.get('to_create'):
- for res in vals['to_create']:
- self.move_line_ids.new(res)
- if vals.get('to_write'):
- for move_line, res in vals['to_write']:
- move_line.update(res)
@api.model
def default_get(self, fields_list):
@@ -277,6 +271,11 @@ class StockMove(models.Model):
production._action_cancel()
return res
+ def _prepare_move_split_vals(self, qty):
+ defaults = super()._prepare_move_split_vals(qty)
+ defaults['workorder_id'] = False
+ return defaults
+
def _prepare_phantom_move_values(self, bom_line, product_qty, quantity_done):
return {
'picking_id': self.picking_id.id if self.picking_id else False,
@@ -298,10 +297,10 @@ class StockMove(models.Model):
return vals
def _get_upstream_documents_and_responsibles(self, visited):
- if self.production_id and self.production_id.state not in ('done', 'cancel'):
- return [(self.production_id, self.production_id.user_id, visited)]
- else:
- return super(StockMove, self)._get_upstream_documents_and_responsibles(visited)
+ if self.production_id and self.production_id.state not in ('done', 'cancel'):
+ return [(self.production_id, self.production_id.user_id, visited)]
+ else:
+ return super(StockMove, self)._get_upstream_documents_and_responsibles(visited)
def _delay_alert_get_documents(self):
res = super(StockMove, self)._delay_alert_get_documents()
@@ -312,6 +311,16 @@ class StockMove(models.Model):
res = super(StockMove, self)._should_be_assigned()
return bool(res and not (self.production_id or self.raw_material_production_id))
+ def _should_bypass_set_qty_producing(self):
+ if self.state in ('done', 'cancel'):
+ return True
+ # Do not update extra product quantities
+ if float_is_zero(self.product_uom_qty, precision_rounding=self.product_uom.rounding):
+ return True
+ if self.has_tracking != 'none' or self.state == 'done':
+ return True
+ return False
+
def _should_bypass_reservation(self):
res = super(StockMove, self)._should_bypass_reservation()
return bool(res and not self.production_id)
@@ -390,7 +399,7 @@ class StockMove(models.Model):
new_qty = mo.product_uom_id._compute_quantity((mo.qty_producing - mo.qty_produced) * self.unit_factor, mo.product_uom_id, rounding_method='HALF-UP')
if not self.is_quantity_done_editable:
self.move_line_ids.filtered(lambda ml: ml.state not in ('done', 'cancel')).qty_done = 0
- ml_values = self._set_quantity_done_prepare_vals(new_qty)
+ self.move_line_ids = self._set_quantity_done_prepare_vals(new_qty)
else:
self.quantity_done = new_qty
return ml_values
diff --git a/addons/mrp/security/ir.model.access.csv b/addons/mrp/security/ir.model.access.csv
index ab13940651c..73d4f9c3888 100644
--- a/addons/mrp/security/ir.model.access.csv
+++ b/addons/mrp/security/ir.model.access.csv
@@ -26,8 +26,6 @@ access_product_supplierinfo_user,product.supplierinfo user,product.model_product
access_res_partner,res.partner,base.model_res_partner,mrp.group_mrp_user,1,0,0,0
access_mrp_workorder_mrp_user,mrp.workorder.user,model_mrp_workorder,mrp.group_mrp_user,1,1,1,1
access_mrp_workorder_mrp_manager,mrp.workorder,model_mrp_workorder,mrp.group_mrp_manager,1,1,1,1
-access_mrp_workorder_line_mrp_user,mrp.workorder.user,model_mrp_workorder_line,mrp.group_mrp_user,1,1,1,1
-access_mrp_workorder_line_mrp_manager,mrp.workorder,model_mrp_workorder_line,mrp.group_mrp_manager,1,1,1,1
access_resource_calendar_leaves_user,mrp.resource.calendar.leaves.user,resource.model_resource_calendar_leaves,mrp.group_mrp_user,1,1,1,1
access_resource_calendar_leaves_manager,mrp.resource.calendar.leaves.manager,resource.model_resource_calendar_leaves,mrp.group_mrp_manager,1,0,0,0
access_resource_calendar_attendance_mrp_user,mrp.resource.calendar.attendance.mrp.user,resource.model_resource_calendar_attendance,mrp.group_mrp_user,1,1,1,1
diff --git a/addons/mrp/tests/__init__.py b/addons/mrp/tests/__init__.py
index ec5322adba3..2af9d670b87 100644
--- a/addons/mrp/tests/__init__.py
+++ b/addons/mrp/tests/__init__.py
@@ -6,7 +6,6 @@ from . import test_cancel_mo
from . import test_order
from . import test_stock
from . import test_warehouse_multistep_manufacturing
-from . import test_workorder_operation
from . import test_procurement
from . import test_unbuild
from . import test_oee
diff --git a/addons/mrp/tests/test_backorder.py b/addons/mrp/tests/test_backorder.py
index 0c3361781ad..a55b957184e 100644
--- a/addons/mrp/tests/test_backorder.py
+++ b/addons/mrp/tests/test_backorder.py
@@ -327,41 +327,3 @@ class TestMrpWorkorderBackorder(SavepointCase):
})
cls.bom_finished1.bom_line_ids[0].operation_id = cls.bom_finished1.operation_ids[0].id
cls.bom_finished1.bom_line_ids[1].operation_id = cls.bom_finished1.operation_ids[1].id
-
- def test_1(self):
- """Operations are set on `self.bom_kit1` but none on `self.bom_finished1`."""
- mo_form = Form(self.env['mrp.production'])
- mo_form.product_id = self.finished1
- mo_form.bom_id = self.bom_finished1
- mo_form.product_qty = 2.0
- mo = mo_form.save()
-
- mo.action_confirm()
- mo.button_plan()
-
- self.assertEqual(len(mo.workorder_ids), 2)
-
- workorder1 = mo.workorder_ids[0]
- workorder2 = mo.workorder_ids[1]
-
- self.assertEqual(workorder1.raw_workorder_line_ids.product_id, self.compfinished1)
- self.assertEqual(workorder1.raw_workorder_line_ids.qty_to_consume, 2)
-
- workorder1.button_start()
- workorder1.button_finish()
-
- self.assertEqual(workorder2.raw_workorder_line_ids.product_id, self.compfinished2)
- self.assertEqual(workorder2.raw_workorder_line_ids.qty_to_consume, 2)
-
- workorder2.button_start()
- workorder2.button_finish()
-
- action = mo.with_context(debug=True).button_mark_done()
- backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
- backorder.save().action_backorder()
- mo_backorder = mo.procurement_group_id.mrp_production_ids[-1]
- mo_backorder.button_plan()
- self.assertEqual(len(mo_backorder.workorder_ids), 2)
-
- self.assertEqual(workorder1.state, 'done')
- self.assertEqual(workorder2.state, 'done')
diff --git a/addons/mrp/tests/test_cancel_mo.py b/addons/mrp/tests/test_cancel_mo.py
index 20f0e9457c7..d305571c8b9 100644
--- a/addons/mrp/tests/test_cancel_mo.py
+++ b/addons/mrp/tests/test_cancel_mo.py
@@ -76,73 +76,6 @@ class TestMrpCancelMO(TestMrpCommon):
self.assertEqual(manufacturing_order.move_finished_ids[1].state, 'cancel',
"The other move finished is cancelled like its MO.")
- def test_cancel_mo_with_routing(self):
- """ Cancel a Manufacturing Order with routing (so generate a Work Order)
- and produce some quantities. When cancelled, the MO must be marked as
- done and the WO must be cancelled.
- """
- # Create MO
- product_to_build = self.env['product.product'].create({
- 'name': 'Young Tom',
- 'type': 'product',
- })
- product_to_use_1 = self.env['product.product'].create({
- 'name': 'Botox',
- 'type': 'product',
- })
- product_to_use_2 = self.env['product.product'].create({
- 'name': 'Old Tom',
- 'type': 'product',
- })
- bom = self.env['mrp.bom'].create({
- 'product_id': product_to_build.id,
- 'product_tmpl_id': product_to_build.product_tmpl_id.id,
- 'product_uom_id': self.uom_unit.id,
- 'product_qty': 1.0,
- 'consumption': 'strict',
- 'type': 'normal',
- 'bom_line_ids': [
- (0, 0, {'product_id': product_to_use_2.id, 'product_qty': 4}),
- (0, 0, {'product_id': product_to_use_1.id, 'product_qty': 1})
- ],
- 'operation_ids': [
- (0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
- ]
- })
- mo_form = Form(self.env['mrp.production'])
- mo_form.product_id = product_to_build
- mo_form.bom_id = bom
- mo_form.product_qty = 5.0
- manufacturing_order = mo_form.save()
- manufacturing_order.action_confirm()
-
- manufacturing_order.button_plan()
- workorder = manufacturing_order.workorder_ids
- # Produce some quantity
- workorder.button_start()
- workorder.qty_producing = 2
- workorder._apply_update_workorder_lines()
- workorder.record_production()
- # Post Inventory
- manufacturing_order._post_inventory()
- # Cancel it
- manufacturing_order.action_cancel()
- # Check MO is done, WO is cancelled and its SML are done or cancelled
- self.assertEqual(manufacturing_order.state, 'done', "MO should be in done state.")
- self.assertEqual(workorder.state, 'cancel', "WO should be cancelled.")
- self.assertEqual(manufacturing_order.move_raw_ids[0].state, 'done',
- "Due to 'post_inventory', some move raw must stay in done state")
- self.assertEqual(manufacturing_order.move_raw_ids[1].state, 'done',
- "Due to 'post_inventory', some move raw must stay in done state")
- self.assertEqual(manufacturing_order.move_raw_ids[2].state, 'cancel',
- "The other move raw are cancelled like their MO.")
- self.assertEqual(manufacturing_order.move_raw_ids[3].state, 'cancel',
- "The other move raw are cancelled like their MO.")
- self.assertEqual(manufacturing_order.move_finished_ids[0].state, 'done',
- "Due to 'post_inventory', a move finished must stay in done state")
- self.assertEqual(manufacturing_order.move_finished_ids[1].state, 'cancel',
- "The other move finished is cancelled like its MO.")
-
def test_unlink_mo(self):
""" Try to unlink a Manufacturing Order, and check it's possible or not
depending of the MO state (must be in cancel state to be unlinked, but
diff --git a/addons/mrp/tests/test_procurement.py b/addons/mrp/tests/test_procurement.py
index 89be67074bd..3d1281f013b 100644
--- a/addons/mrp/tests/test_procurement.py
+++ b/addons/mrp/tests/test_procurement.py
@@ -39,7 +39,7 @@ class TestProcurement(TestMrpCommon):
production_product_6.action_assign()
# check production state is Confirmed
- self.assertEqual(production_product_6.state, 'confirmed')
+ self.assertEqual(production_product_6.state, 'to_close')
# Check procurement for product 4 created or not.
# Check it created a purchase order
diff --git a/addons/mrp/tests/test_stock.py b/addons/mrp/tests/test_stock.py
index 7ca4b2493d3..64587e1cde9 100644
--- a/addons/mrp/tests/test_stock.py
+++ b/addons/mrp/tests/test_stock.py
@@ -158,47 +158,6 @@ class TestWarehouse(common.TestMrpCommon):
# scrap_move = production_3.move_raw_ids.filtered(lambda x: x.product_id == self.product_2 and x.scrapped)
# self.assertTrue(scrap_move, "There are no any scrap move created for production order.")
- def test_putaway_after_manufacturing_1(self):
- """ This test checks a manufactured product without tracking will go to
- location defined in putaway strategy.
- """
- mo_laptop = self.new_mo_laptop()
-
- mo_laptop.button_plan()
- workorder = mo_laptop.workorder_ids[0]
-
- workorder.button_start()
- workorder.record_production()
- mo_laptop.button_mark_done()
-
- # We check if the laptop go in the depot and not in the stock
- move = mo_laptop.move_finished_ids
- location_dest = move.move_line_ids.location_dest_id
- self.assertEqual(location_dest.id, self.depot_location.id)
- self.assertNotEqual(location_dest.id, self.stock_location.id)
-
- def test_putaway_after_manufacturing_2(self):
- """ This test checks a tracked manufactured product will go to location
- defined in putaway strategy.
- """
- self.laptop.tracking = 'serial'
- mo_laptop = self.new_mo_laptop()
-
- mo_laptop.button_plan()
- workorder = mo_laptop.workorder_ids[0]
-
- workorder.button_start()
- serial = self.env['stock.production.lot'].create({'product_id': self.laptop.id, 'company_id': self.env.company.id})
- workorder.finished_lot_id = serial
- workorder.record_production()
- mo_laptop.button_mark_done()
-
- # We check if the laptop go in the depot and not in the stock
- move = mo_laptop.move_finished_ids
- location_dest = move.move_line_ids.location_dest_id
- self.assertEqual(location_dest.id, self.depot_location.id)
- self.assertNotEqual(location_dest.id, self.stock_location.id)
-
def test_putaway_after_manufacturing_3(self):
""" This test checks a tracked manufactured product will go to location
defined in putaway strategy when the production is recorded with
diff --git a/addons/mrp/tests/test_workorder_operation.py b/addons/mrp/tests/test_workorder_operation.py
deleted file mode 100644
index 190768adc9f..00000000000
--- a/addons/mrp/tests/test_workorder_operation.py
+++ /dev/null
@@ -1,1785 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from datetime import datetime, timedelta
-from odoo.tests import Form
-from odoo.tests.common import SavepointCase
-from odoo.addons.mrp.tests.common import TestMrpCommon
-from odoo.exceptions import ValidationError, UserError
-
-
-class TestWorkOrderProcessCommon(TestMrpCommon):
-
- @classmethod
- def setUpClass(cls):
- super(TestWorkOrderProcessCommon, cls).setUpClass()
- cls.source_location_id = cls.stock_location_14.id
- cls.warehouse = cls.env.ref('stock.warehouse0')
- # setting up alternative workcenters
- cls.wc_alt_1 = cls.env['mrp.workcenter'].create({
- 'name': 'Nuclear Workcenter bis',
- 'capacity': 3,
- 'time_start': 9,
- 'time_stop': 5,
- 'time_efficiency': 80,
- })
- cls.wc_alt_2 = cls.env['mrp.workcenter'].create({
- 'name': 'Nuclear Workcenter ter',
- 'capacity': 1,
- 'time_start': 10,
- 'time_stop': 5,
- 'time_efficiency': 85,
- })
- cls.product_4.uom_id = cls.uom_unit
- cls.planning_bom = cls.env['mrp.bom'].create({
- 'product_id': cls.product_4.id,
- 'product_tmpl_id': cls.product_4.product_tmpl_id.id,
- 'product_uom_id': cls.uom_unit.id,
- 'product_qty': 4.0,
- 'consumption': 'flexible',
- 'operation_ids': [
- (0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
- ],
- 'type': 'normal',
- 'bom_line_ids': [
- (0, 0, {'product_id': cls.product_2.id, 'product_qty': 2}),
- (0, 0, {'product_id': cls.product_1.id, 'product_qty': 4})
- ]})
- cls.dining_table = cls.env['product.product'].create({
- 'name': 'Table (MTO)',
- 'type': 'product',
- 'tracking': 'serial',
- })
- cls.product_table_sheet = cls.env['product.product'].create({
- 'name': 'Table Top',
- 'type': 'product',
- 'tracking': 'serial',
- })
- cls.product_table_leg = cls.env['product.product'].create({
- 'name': 'Table Leg',
- 'type': 'product',
- 'tracking': 'lot',
- })
- cls.product_bolt = cls.env['product.product'].create({
- 'name': 'Bolt',
- 'type': 'product',
- })
- cls.product_screw = cls.env['product.product'].create({
- 'name': 'Screw',
- 'type': 'product',
- })
-
- cls.mrp_workcenter = cls.env['mrp.workcenter'].create({
- 'name': 'Assembly Line 1',
- 'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
- })
- cls.mrp_bom_desk = cls.env['mrp.bom'].create({
- 'product_tmpl_id': cls.dining_table.product_tmpl_id.id,
- 'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
- 'sequence': 3,
- 'consumption': 'flexible',
- 'operation_ids': [
- (0, 0, {'workcenter_id': cls.mrp_workcenter.id, 'name': 'Manual Assembly'}),
- ],
- })
- cls.mrp_bom_desk.write({
- 'bom_line_ids': [
- (0, 0, {
- 'product_id': cls.product_table_sheet.id,
- 'product_qty': 1,
- 'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
- 'sequence': 1,
- 'operation_id': cls.mrp_bom_desk.operation_ids.id}),
- (0, 0, {
- 'product_id': cls.product_table_leg.id,
- 'product_qty': 4,
- 'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
- 'sequence': 2,
- 'operation_id': cls.mrp_bom_desk.operation_ids.id}),
- (0, 0, {
- 'product_id': cls.product_bolt.id,
- 'product_qty': 4,
- 'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
- 'sequence': 3,
- 'operation_id': cls.mrp_bom_desk.operation_ids.id}),
- (0, 0, {
- 'product_id': cls.product_screw.id,
- 'product_qty': 10,
- 'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
- 'sequence': 4,
- 'operation_id': cls.mrp_bom_desk.operation_ids.id}),
- ]
- })
- cls.mrp_workcenter_1 = cls.env['mrp.workcenter'].create({
- 'name': 'Drill Station 1',
- 'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
- })
- cls.mrp_workcenter_3 = cls.env['mrp.workcenter'].create({
- 'name': 'Assembly Line 1',
- 'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
- })
-
-
-class TestWorkOrderProcess(TestWorkOrderProcessCommon):
- def full_availability(self):
- """set full availability for all calendars"""
- calendar = self.env['resource.calendar'].search([])
- calendar.write({'attendance_ids': [(5, 0, 0)]})
- calendar.write({'attendance_ids': [
- (0, 0, {'name': 'Monday', 'dayofweek': '0', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
- (0, 0, {'name': 'Tuesday', 'dayofweek': '1', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
- (0, 0, {'name': 'Wednesday', 'dayofweek': '2', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
- (0, 0, {'name': 'Thursday', 'dayofweek': '3', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
- (0, 0, {'name': 'Friday', 'dayofweek': '4', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
- (0, 0, {'name': 'Saturday', 'dayofweek': '5', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
- (0, 0, {'name': 'Sunday', 'dayofweek': '6', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}),
- ]})
-
- def test_00_workorder_process(self):
- """ Testing consume quants and produced quants with workorder """
- dining_table = self.dining_table
- product_table_sheet = self.product_table_sheet
- product_table_leg = self.product_table_leg
- product_bolt = self.product_bolt
- product_screw = self.product_screw
- mrp_bom_desk = self.mrp_bom_desk
-
- self.env['stock.move'].search([('product_id', 'in', [product_bolt.id, product_screw.id])])._do_unreserve()
-
- production_table_form = Form(self.env['mrp.production'])
- production_table_form.product_id = dining_table
- production_table_form.bom_id = mrp_bom_desk
- production_table_form.product_qty = 1.0
- production_table_form.product_uom_id = dining_table.uom_id
- production_table = production_table_form.save()
-
- # Set tracking lot on finish and consume products.
- dining_table.tracking = 'lot'
- product_table_sheet.tracking = 'lot'
- product_table_leg.tracking = 'lot'
- product_bolt.tracking = "lot"
-
- production_table.action_confirm()
- # Initial inventory of product sheet, lags and bolt
- lot_sheet = self.env['stock.production.lot'].create({'product_id': product_table_sheet.id, 'company_id': self.env.company.id})
- lot_leg = self.env['stock.production.lot'].create({'product_id': product_table_leg.id, 'company_id': self.env.company.id})
- lot_bolt = self.env['stock.production.lot'].create({'product_id': product_bolt.id, 'company_id': self.env.company.id})
-
- # Initialize inventory
- # --------------------
- inventory = self.env['stock.inventory'].create({
- 'name': 'Inventory Product Table',
- 'line_ids': [(0, 0, {
- 'product_id': product_table_sheet.id,
- 'product_uom_id': product_table_sheet.uom_id.id,
- 'product_qty': 20,
- 'prod_lot_id': lot_sheet.id,
- 'location_id': self.source_location_id
- }), (0, 0, {
- 'product_id': product_table_leg.id,
- 'product_uom_id': product_table_leg.uom_id.id,
- 'product_qty': 20,
- 'prod_lot_id': lot_leg.id,
- 'location_id': self.source_location_id
- }), (0, 0, {
- 'product_id': product_bolt.id,
- 'product_uom_id': product_bolt.uom_id.id,
- 'product_qty': 20,
- 'prod_lot_id': lot_bolt.id,
- 'location_id': self.source_location_id
- }), (0, 0, {
- 'product_id': product_screw.id,
- 'product_uom_id': product_screw.uom_id.id,
- 'product_qty': 20,
- 'location_id': self.source_location_id
- })]
- })
- inventory.action_start()
- inventory.action_validate()
-
- # Create work order
- production_table.button_plan()
- # Check Work order created or not
- self.assertEqual(len(production_table.workorder_ids), 1)
-
- # ---------------------------------------------------------
- # Process all workorder and check it state.
- # ----------------------------------------------------------
-
- workorder = production_table.workorder_ids[0]
- self.assertEqual(workorder.state, 'ready', "workorder state should be ready.")
-
- # --------------------------------------------------------------
- # Process assembly line
- # ---------------------------------------------------------
- finished_lot =self.env['stock.production.lot'].create({'product_id': production_table.product_id.id, 'company_id': self.env.company.id})
- workorder.write({'finished_lot_id': finished_lot.id})
- workorder.button_start()
- for workorder_line_id in workorder._workorder_line_ids():
- if workorder_line_id.product_id.id == product_bolt.id:
- workorder_line_id.write({'lot_id': lot_bolt.id, 'qty_done': 1})
- if workorder_line_id.product_id.id == product_table_sheet.id:
- workorder_line_id.write({'lot_id': lot_sheet.id, 'qty_done': 1})
- if workorder_line_id.product_id.id == product_table_leg.id:
- workorder_line_id.write({'lot_id': lot_leg.id, 'qty_done': 1})
- self.assertEqual(workorder.state, 'progress')
-
- workorder.record_production()
- self.assertEqual(workorder.state, 'done')
- move_table_sheet = production_table.move_raw_ids.filtered(lambda x : x.product_id == product_table_sheet)
- self.assertEqual(move_table_sheet.quantity_done, 1)
-
- # ---------------------------------------------------------------
- # Check consume quants and produce quants after posting inventory
- # ---------------------------------------------------------------
- action = production_table.button_mark_done()
- backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
- backorder.save().action_close_mo()
-
- self.assertEqual(product_screw.qty_available, 10)
- self.assertEqual(product_bolt.qty_available, 19)
- self.assertEqual(product_table_leg.qty_available, 19)
- self.assertEqual(product_table_sheet.qty_available, 19)
-
- def test_00b_workorder_process(self):
- """ Testing consume quants and produced quants with workorder """
- dining_table = self.dining_table
- product_table_sheet = self.product_table_sheet
- product_table_leg = self.product_table_leg
- product_bolt = self.product_bolt
- product_screw = self.product_screw
- bom = self.mrp_bom_desk
-
- self.env['stock.move'].search([('product_id', '=', product_bolt.id)])._do_unreserve()
-
- bom.operation_ids = False
- bom.write({
- 'operation_ids': [
- (0, 0, {
- 'workcenter_id': self.mrp_workcenter_1.id,
- 'name': 'Packing',
- 'time_cycle': 30,
- 'sequence': 5}),
- (0, 0, {
- 'workcenter_id': self.mrp_workcenter_3.id,
- 'name': 'Testing',
- 'time_cycle': 60,
- 'sequence': 10}),
- (0, 0, {
- 'workcenter_id': self.mrp_workcenter_3.id,
- 'name': 'Long time assembly',
- 'time_cycle': 180,
- 'sequence': 15}),
- ]
- })
-
- bom.bom_line_ids.filtered(lambda p: p.product_id == product_table_sheet).operation_id = bom.operation_ids[0]
- bom.bom_line_ids.filtered(lambda p: p.product_id == product_table_leg).operation_id = bom.operation_ids[1]
- bom.bom_line_ids.filtered(lambda p: p.product_id == product_bolt).operation_id = bom.operation_ids[2]
-
- production_table_form = Form(self.env['mrp.production'])
- production_table_form.product_id = dining_table
- production_table_form.bom_id = bom
- production_table_form.product_qty = 2.0
- production_table_form.product_uom_id = dining_table.uom_id
- production_table = production_table_form.save()
- # Set tracking lot on finish and consume products.
- dining_table.tracking = 'lot'
- product_table_sheet.tracking = 'lot'
- product_table_leg.tracking = 'lot'
- product_bolt.tracking = "lot"
- production_table.action_confirm()
- # Initial inventory of product sheet, lags and bolt
- lot_sheet = self.env['stock.production.lot'].create({'product_id': product_table_sheet.id, 'company_id': self.env.company.id})
- lot_leg = self.env['stock.production.lot'].create({'product_id': product_table_leg.id, 'company_id': self.env.company.id})
- lot_bolt = self.env['stock.production.lot'].create({'product_id': product_bolt.id, 'company_id': self.env.company.id})
-
- # Initialize inventory
- # --------------------
- inventory = self.env['stock.inventory'].create({
- 'name': 'Inventory Product Table',
- 'line_ids': [(0, 0, {
- 'product_id': product_table_sheet.id,
- 'product_uom_id': product_table_sheet.uom_id.id,
- 'product_qty': 20,
- 'prod_lot_id': lot_sheet.id,
- 'location_id': self.source_location_id
- }), (0, 0, {
- 'product_id': product_table_leg.id,
- 'product_uom_id': product_table_leg.uom_id.id,
- 'product_qty': 20,
- 'prod_lot_id': lot_leg.id,
- 'location_id': self.source_location_id
- }), (0, 0, {
- 'product_id': product_bolt.id,
- 'product_uom_id': product_bolt.uom_id.id,
- 'product_qty': 20,
- 'prod_lot_id': lot_bolt.id,
- 'location_id': self.source_location_id
- })]
- })
- inventory.action_start()
- inventory.action_validate()
-
- # Create work order
- production_table.button_plan()
- # Check Work order created or not
- self.assertEqual(len(production_table.workorder_ids), 3)
-
- # ---------------------------------------------------------
- # Process all workorder and check it state.
- # ----------------------------------------------------------
-
- workorders = production_table.workorder_ids
- self.assertEqual(workorders[0].state, 'ready', "First workorder state should be ready.")
- self.assertEqual(workorders[1].state, 'pending')
- self.assertEqual(workorders[2].state, 'pending')
-
- # --------------------------------------------------------------
- # Process cutting operation...
- # ---------------------------------------------------------
- finished_lot = self.env['stock.production.lot'].create({'product_id': production_table.product_id.id, 'company_id': self.env.company.id})
- workorders[0].write({'finished_lot_id': finished_lot.id, 'qty_producing': 1.0})
- workorders[0].button_start()
- workorders[0]._workorder_line_ids()[0].write({'lot_id': lot_sheet.id, 'qty_done': 1})
- self.assertEqual(workorders[0].state, 'progress')
-
- workorders[0].record_production()
-
- move_table_sheet = production_table.move_raw_ids.filtered(lambda p: p.product_id == product_table_sheet)
- self.assertEqual(move_table_sheet.quantity_done, 1)
-
- # --------------------------------------------------------------
- # Process drilling operation ...
- # ---------------------------------------------------------
- workorders[1].button_start()
- workorders[1].qty_producing = 1.0
- workorders[1]._workorder_line_ids()[0].write({'lot_id': lot_leg.id, 'qty_done': 4})
- workorders[1].record_production()
- move_leg = production_table.move_raw_ids.filtered(lambda p: p.product_id == product_table_leg)
- #self.assertEqual(workorders[1].state, 'done')
- self.assertEqual(move_leg.quantity_done, 4)
-
- # --------------------------------------------------------------
- # Process fitting operation ...
- # ---------------------------------------------------------
- workorders[2].button_start()
- workorders[2].qty_producing = 1.0
- move_lot = workorders[2]._workorder_line_ids()[0]
- move_lot.write({'lot_id': lot_bolt.id, 'qty_done': 4})
- move_table_bolt = production_table.move_raw_ids.filtered(lambda p: p.product_id.id == product_bolt.id)
- workorders[2].record_production()
- self.assertEqual(move_table_bolt.quantity_done, 4)
-
- # Change the quantity of the production order to 1
- wiz = self.env['change.production.qty'].create({'mo_id': production_table.id ,
- 'product_qty': 1.0})
- wiz.change_prod_qty()
- # ---------------------------------------------------------------
- # Check consume quants and produce quants after posting inventory
- # ---------------------------------------------------------------
- production_table._post_inventory()
- self.assertEqual(sum(move_table_sheet.mapped('quantity_done')), 1, "Wrong quantity of consumed product %s" % move_table_sheet.product_id.name)
- self.assertEqual(sum(move_leg.mapped('quantity_done')), 4, "Wrong quantity of consumed product %s" % move_leg.product_id.name)
- self.assertEqual(sum(move_table_bolt.mapped('quantity_done')), 4, "Wrong quantity of consumed product %s" % move_table_bolt.product_id.name)
-
- def test_explode_from_order(self):
- # bom3 produces 2 Dozen of Doors (p6), aka 24
- # To produce 24 Units of Doors (p6)
- # - 2 Units of Tools (p5) -> need 4
- # - 8 Dozen of Sticks (p4) -> need 16
- # - 12 Units of Wood (p2) -> need 24
- # bom2 produces 1 Unit of Sticks (p4)
- # To produce 1 Unit of Sticks (p4)
- # - 2 Dozen of Sticks (p4) -> need 8
- # - 3 Dozen of Stones (p3) -> need 12
-
- # Update capacity, start time, stop time, and time efficiency.
- # ------------------------------------------------------------
- self.workcenter_1.write({'capacity': 1, 'time_start': 0, 'time_stop': 0, 'time_efficiency': 100})
-
- # Set manual time cycle 20 and 10.
- # --------------------------------
- self.bom_3.operation_ids[0].time_cycle_manual = 10
- self.bom_3.operation_ids[1].time_cycle_manual = 10
- self.bom_2.operation_ids.time_cycle_manual = 20
-
- man_order_form = Form(self.env['mrp.production'])
- man_order_form.product_id = self.product_6
- man_order_form.bom_id = self.bom_3
- man_order_form.product_qty = 48
- man_order_form.product_uom_id = self.product_6.uom_id
- man_order = man_order_form.save()
- # reset quantities
- self.product_1.type = "product"
- self.env['stock.quant'].with_context(inventory_mode=True).create({
- 'product_id': self.product_1.id,
- 'inventory_quantity': 0.0,
- 'location_id': self.warehouse_1.lot_stock_id.id,
- })
-
- (self.product_2 | self.product_4).write({
- 'tracking': 'none',
- })
- # assign consume material
- man_order.action_confirm()
- man_order.action_assign()
- self.assertEqual(man_order.reservation_state, 'confirmed', "Production order should be in waiting state.")
-
- # check consume materials of manufacturing order
- self.assertEqual(len(man_order.move_raw_ids), 4, "Consume material lines are not generated proper.")
- product_2_consume_moves = man_order.move_raw_ids.filtered(lambda x: x.product_id == self.product_2)
- product_3_consume_moves = man_order.move_raw_ids.filtered(lambda x: x.product_id == self.product_3)
- product_4_consume_moves = man_order.move_raw_ids.filtered(lambda x: x.product_id == self.product_4)
- product_5_consume_moves = man_order.move_raw_ids.filtered(lambda x: x.product_id == self.product_5)
- consume_qty_2 = product_2_consume_moves.product_uom_qty
- self.assertEqual(consume_qty_2, 24.0, "Consume material quantity of Wood should be 24 instead of %s" % str(consume_qty_2))
- consume_qty_3 = product_3_consume_moves.product_uom_qty
- self.assertEqual(consume_qty_3, 12.0, "Consume material quantity of Stone should be 12 instead of %s" % str(consume_qty_3))
- self.assertEqual(len(product_4_consume_moves), 2, "Consume move are not generated proper.")
- for consume_moves in product_4_consume_moves:
- consume_qty_4 = consume_moves.product_uom_qty
- self.assertIn(consume_qty_4, [8.0, 16.0], "Consume material quantity of Stick should be 8 or 16 instead of %s" % str(consume_qty_4))
- self.assertFalse(product_5_consume_moves, "Move should not create for phantom bom")
-
- # create required lots
- lot_product_2 = self.env['stock.production.lot'].create({'product_id': self.product_2.id, 'company_id': self.env.company.id})
- lot_product_4 = self.env['stock.production.lot'].create({'product_id': self.product_4.id, 'company_id': self.env.company.id})
-
- # refuel stock
- inventory = self.env['stock.inventory'].create({
- 'name': 'Inventory For Product C',
- 'line_ids': [(0, 0, {
- 'product_id': self.product_2.id,
- 'product_uom_id': self.product_2.uom_id.id,
- 'product_qty': 30,
- 'prod_lot_id': lot_product_2.id,
- 'location_id': self.stock_location_14.id
- }), (0, 0, {
- 'product_id': self.product_3.id,
- 'product_uom_id': self.product_3.uom_id.id,
- 'product_qty': 60,
- 'location_id': self.stock_location_14.id
- }), (0, 0, {
- 'product_id': self.product_4.id,
- 'product_uom_id': self.product_4.uom_id.id,
- 'product_qty': 60,
- 'prod_lot_id': lot_product_4.id,
- 'location_id': self.stock_location_14.id
- })]
- })
- inventory.action_start()
- inventory.action_validate()
-
- # re-assign consume material
- man_order.action_assign()
-
- # Check production order status after assign.
- self.assertEqual(man_order.reservation_state, 'assigned', "Production order should be in assigned state.")
- # Plan production order.
- man_order.button_plan()
-
- # check workorders
- # - main bom: Door: 2 operations
- # operation 1: Cutting
- # operation 2: Welding, waiting for the previous one
- # - kit bom: Stone Tool: 1 operation
- # operation 1: Gift Wrapping
- workorders = man_order.workorder_ids
- kit_wo = man_order.workorder_ids.filtered(lambda wo: wo.operation_id.name == "Gift Wrap Maching")
- door_wo_1 = man_order.workorder_ids.filtered(lambda wo: wo.operation_id.name == "Cutting Machine")
- door_wo_2 = man_order.workorder_ids.filtered(lambda wo: wo.operation_id.name == "Weld Machine")
-
- for workorder in workorders:
- self.assertEqual(workorder.workcenter_id, self.workcenter_1, "Workcenter does not match.")
- self.assertEqual(kit_wo.state, 'ready', "Workorder should be in ready state.")
- self.assertEqual(door_wo_1.state, 'ready', "Workorder should be in ready state.")
- self.assertEqual(door_wo_2.state, 'pending', "Workorder should be in pending state.")
- self.assertEqual(kit_wo.duration_expected, 960, "Workorder duration should be 960 instead of %s." % str(kit_wo.duration_expected))
- self.assertEqual(door_wo_1.duration_expected, 480, "Workorder duration should be 480 instead of %s." % str(door_wo_1.duration_expected))
- self.assertEqual(door_wo_2.duration_expected, 480, "Workorder duration should be 480 instead of %s." % str(door_wo_2.duration_expected))
-
- # subbom: kit for stone tools
- kit_wo.button_start()
- finished_lot = self.env['stock.production.lot'].create({'product_id': man_order.product_id.id, 'company_id': self.env.company.id})
- kit_wo.write({
- 'finished_lot_id': finished_lot.id,
- 'qty_producing': 48
- })
-
- kit_wo.record_production()
-
- self.assertEqual(kit_wo.state, 'done', "Workorder should be in done state.")
-
- # 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
- })
- door_wo_1.record_production()
- self.assertEqual(door_wo_1.state, 'done', "Workorder should be in done state.")
-
- # 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.")
-
- def test_01_without_workorder(self):
- """ Testing consume quants and produced quants without workorder """
- unit = self.ref("uom.product_uom_unit")
- custom_laptop = self.env['product.product'].create({
- 'name': 'Drawer',
- 'type': 'product',
- 'tracking': 'lot',
- })
-
- # Create new product charger and keybord
- # --------------------------------------
- product_charger = self.env['product.product'].create({
- 'name': 'Charger',
- 'type': 'product',
- 'tracking': 'lot',
- 'uom_id': unit,
- 'uom_po_id': unit})
- product_keybord = self.env['product.product'].create({
- 'name': 'Usb Keybord',
- 'type': 'product',
- 'tracking': 'lot',
- 'uom_id': unit,
- 'uom_po_id': unit})
-
- # Create bill of material for customized laptop.
-
- bom_custom_laptop = self.env['mrp.bom'].create({
- 'product_tmpl_id': custom_laptop.product_tmpl_id.id,
- 'product_qty': 10,
- 'product_uom_id': unit,
- 'consumption': 'flexible',
- 'bom_line_ids': [(0, 0, {
- 'product_id': product_charger.id,
- 'product_qty': 20,
- 'product_uom_id': unit
- }), (0, 0, {
- 'product_id': product_keybord.id,
- 'product_qty': 20,
- 'product_uom_id': unit
- })]
- })
-
- # Create production order for customize laptop.
-
- mo_custom_laptop_form = Form(self.env['mrp.production'])
- mo_custom_laptop_form.product_id = custom_laptop
- mo_custom_laptop_form.bom_id = bom_custom_laptop
- mo_custom_laptop_form.product_qty = 10.0
- mo_custom_laptop_form.product_uom_id = self.env.ref("uom.product_uom_unit")
- mo_custom_laptop = mo_custom_laptop_form.save()
-
- mo_custom_laptop.action_confirm()
- # Assign component to production order.
- mo_custom_laptop.action_assign()
-
- # Check production order status of availablity
-
- self.assertEqual(mo_custom_laptop.reservation_state, 'confirmed')
-
- # --------------------------------------------------
- # Set inventory for rawmaterial charger and keybord
- # --------------------------------------------------
-
- lot_charger = self.env['stock.production.lot'].create({'product_id': product_charger.id, 'company_id': self.env.company.id})
- lot_keybord = self.env['stock.production.lot'].create({'product_id': product_keybord.id, 'company_id': self.env.company.id})
-
- # Initialize Inventory
- # --------------------
- inventory = self.env['stock.inventory'].create({
- 'name': 'Inventory Product Table',
- 'line_ids': [(0, 0, {
- 'product_id': product_charger.id,
- 'product_uom_id': product_charger.uom_id.id,
- 'product_qty': 20,
- 'prod_lot_id': lot_charger.id,
- 'location_id': self.source_location_id
- }), (0, 0, {
- 'product_id': product_keybord.id,
- 'product_uom_id': product_keybord.uom_id.id,
- 'product_qty': 20,
- 'prod_lot_id': lot_keybord.id,
- 'location_id': self.source_location_id
- })]
- })
- inventory.action_start()
- inventory.action_validate()
-
- # Check consumed move status
- mo_custom_laptop.action_assign()
- self.assertEqual(mo_custom_laptop.reservation_state, 'assigned')
-
- # Check current status of raw materials.
- for move in mo_custom_laptop.move_raw_ids:
- self.assertEqual(move.product_uom_qty, 20, "Wrong consume quantity of raw material %s: %s instead of %s" % (move.product_id.name, move.product_uom_qty, 20))
- self.assertEqual(move.quantity_done, 0, "Wrong produced quantity on raw material %s: %s instead of %s" % (move.product_id.name, move.quantity_done, 0))
-
- # -----------------
- # Start production
- # -----------------
-
- # Produce 6 Unit of custom laptop will consume ( 12 Unit of keybord and 12 Unit of charger)
- laptop_lot_001 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id , 'company_id': self.env.company.id})
- mo_form = Form(mo_custom_laptop)
- mo_form.qty_producing = 6
- mo_form.lot_producing_id = laptop_lot_001
- mo_custom_laptop = mo_form.save()
- details_operation_form = Form(mo_custom_laptop.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
- with details_operation_form.move_line_ids.edit(0) as ml:
- ml.qty_done = 12
- details_operation_form.save()
- details_operation_form = Form(mo_custom_laptop.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
- with details_operation_form.move_line_ids.edit(0) as ml:
- ml.qty_done = 12
- details_operation_form.save()
-
- action = mo_custom_laptop.button_mark_done()
- backorder = Form(self.env[action['res_model']].with_context(**action['context']))
- backorder.save().action_backorder()
-
- # Check consumed move after produce 6 quantity of customized laptop.
- for move in mo_custom_laptop.move_raw_ids:
- self.assertEqual(move.quantity_done, 12, "Wrong produced quantity on raw material %s" % (move.product_id.name))
- self.assertEqual(len(mo_custom_laptop.move_raw_ids), 2)
-
- # Check done move and confirmed move quantity.
- charger_done_move = mo_custom_laptop.move_raw_ids.filtered(lambda x: x.product_id.id == product_charger.id and x.state == 'done')
- keybord_done_move = mo_custom_laptop.move_raw_ids.filtered(lambda x: x.product_id.id == product_keybord.id and x.state == 'done')
- self.assertEqual(charger_done_move.product_uom_qty, 12)
- self.assertEqual(keybord_done_move.product_uom_qty, 12)
-
- # Produce remaining 4 quantity
- # ----------------------------
-
- # Produce 4 Unit of custom laptop will consume ( 8 Unit of keybord and 8 Unit of charger).
- laptop_lot_002 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id, 'company_id': self.env.company.id})
- mo_custom_laptop = mo_custom_laptop.procurement_group_id.mrp_production_ids[1]
- mo_form = Form(mo_custom_laptop)
- mo_form.qty_producing = 4
- mo_form.lot_producing_id = laptop_lot_002
- mo_custom_laptop = mo_form.save()
- details_operation_form = Form(mo_custom_laptop.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
- with details_operation_form.move_line_ids.edit(0) as ml:
- ml.qty_done = 8
- details_operation_form.save()
- details_operation_form = Form(mo_custom_laptop.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
- with details_operation_form.move_line_ids.edit(0) as ml:
- ml.qty_done = 8
- details_operation_form.save()
-
- charger_move = mo_custom_laptop.move_raw_ids.filtered(lambda x: x.product_id.id == product_charger.id and x.state != 'done')
- keybord_move = mo_custom_laptop.move_raw_ids.filtered(lambda x: x.product_id.id == product_keybord.id and x.state !='done')
- self.assertEqual(charger_move.quantity_done, 8, "Wrong consumed quantity of %s" % charger_move.product_id.name)
- self.assertEqual(keybord_move.quantity_done, 8, "Wrong consumed quantity of %s" % keybord_move.product_id.name)
-
- def test_03_test_serial_number_defaults(self):
- """ Test that the correct serial number is suggested on consecutive work orders. """
- laptop = self.laptop
- graphics_card = self.graphics_card
- unit = self.env.ref("uom.product_uom_unit")
-
- laptop.tracking = 'serial'
-
- bom_laptop = self.env['mrp.bom'].create({
- 'product_tmpl_id': laptop.product_tmpl_id.id,
- 'product_qty': 1,
- 'product_uom_id': unit.id,
- 'consumption': 'flexible',
- 'bom_line_ids': [(0, 0, {
- 'product_id': graphics_card.id,
- 'product_qty': 1,
- 'product_uom_id': unit.id
- })],
- 'operation_ids': [
- (0, 0, {
- 'workcenter_id': self.mrp_workcenter_1.id,
- 'name': 'Packing',
- 'time_cycle': 30,
- 'sequence': 5}),
- (0, 0, {
- 'workcenter_id': self.mrp_workcenter_3.id,
- 'name': 'Testing',
- 'time_cycle': 60,
- 'sequence': 10}),
- (0, 0, {
- 'workcenter_id': self.mrp_workcenter_3.id,
- 'name': 'Long time assembly',
- 'time_cycle': 180,
- 'sequence': 15}),
- ],
- })
-
- mo_laptop_form = Form(self.env['mrp.production'])
- mo_laptop_form.product_id = laptop
- mo_laptop_form.bom_id = bom_laptop
- mo_laptop_form.product_qty = 3
- mo_laptop = mo_laptop_form.save()
-
- mo_laptop.action_confirm()
- mo_laptop.button_plan()
- workorders = mo_laptop.workorder_ids.sorted()
- self.assertEqual(len(workorders), 3)
-
- workorders[0].button_start()
- serial_a = self.env['stock.production.lot'].create({'product_id': laptop.id, 'company_id': self.env.company.id})
- workorders[0].finished_lot_id = serial_a
- workorders[0].record_production()
- serial_b = self.env['stock.production.lot'].create({'product_id': laptop.id, 'company_id': self.env.company.id})
- workorders[0].finished_lot_id = serial_b
- workorders[0].record_production()
- serial_c = self.env['stock.production.lot'].create({'product_id': laptop.id, 'company_id': self.env.company.id})
- workorders[0].finished_lot_id = serial_c
- workorders[0].record_production()
- 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)
- workorder.record_production()
- self.assertEqual(workorder.finished_lot_id, serial_c)
- workorder.record_production()
- self.assertEqual(workorder.state, 'done')
-
- def test_03b_test_serial_number_defaults(self):
- """ Check the constraint on the workorder final_lot. The first workorder
- produces 2/2 units without serial number (serial is only required when
- you register a component) then the second workorder try to register a
- serial number. It should be allowed since the first workorder did not
- specify a seiral number.
- """
- drawer = self.env['product.product'].create({
- 'name': 'Drawer',
- 'type': 'product',
- 'tracking': 'lot',
- })
- drawer_drawer = self.env['product.product'].create({
- 'name': 'Drawer Black',
- 'type': 'product',
- 'tracking': 'lot',
- })
- drawer_case = self.env['product.product'].create({
- 'name': 'Drawer Case Black',
- 'type': 'product',
- 'tracking': 'lot',
- })
- bom = self.env['mrp.bom'].create({
- 'product_tmpl_id': drawer.product_tmpl_id.id,
- 'product_uom_id': self.env.ref('uom.product_uom_unit').id,
- 'consumption': 'flexible',
- 'sequence': 2,
- 'operation_ids': [
- (0, 0, {
- 'workcenter_id': self.mrp_workcenter_1.id,
- 'name': 'Packing',
- 'time_cycle': 30,
- 'sequence': 5}),
- (0, 0, {
- 'workcenter_id': self.mrp_workcenter_3.id,
- 'name': 'Testing',
- 'time_cycle': 60,
- 'sequence': 10}),
- (0, 0, {
- 'workcenter_id': self.mrp_workcenter_3.id,
- 'name': 'Long time assembly',
- 'time_cycle': 180,
- 'sequence': 15}),
- ],
- 'bom_line_ids': [(0, 0, {
- 'product_id': drawer_drawer.id,
- 'product_qty': 1,
- 'product_uom_id': self.env.ref('uom.product_uom_unit').id,
- 'sequence': 1,
- }), (0, 0, {
- 'product_id': drawer_case.id,
- 'product_qty': 1,
- 'product_uom_id': self.env.ref('uom.product_uom_unit').id,
- 'sequence': 2,
- })]
- })
- inventory = self.env['stock.inventory'].create({
- 'name': 'Initial inventory',
- 'line_ids': [(0, 0, {
- 'product_id': drawer_drawer.id,
- 'product_uom_id': drawer_drawer.uom_id.id,
- 'product_qty': 50.0,
- 'location_id': self.stock_location_14.id,
- }), (0, 0, {
- 'product_id': drawer_case.id,
- 'product_uom_id': drawer_case.uom_id.id,
- 'product_qty': 50.0,
- 'location_id': self.stock_location_14.id,
- })]
- })
- inventory.action_start()
- inventory.action_validate()
-
- product = bom.product_tmpl_id.product_variant_id
- product.tracking = 'serial'
-
- lot_1 = self.env['stock.production.lot'].create({
- 'product_id': product.id,
- 'name': 'LOT000001',
- 'company_id': self.env.company.id,
- })
-
- lot_2 = self.env['stock.production.lot'].create({
- 'product_id': product.id,
- 'name': 'LOT000002',
- 'company_id': self.env.company.id,
- })
- self.env['stock.production.lot'].create({
- 'product_id': product.id,
- 'name': 'LOT000003',
- 'company_id': self.env.company.id,
- })
-
- mo_form = Form(self.env['mrp.production'])
- mo_form.product_id = product
- mo_form.bom_id = bom
- mo_form.product_qty = 2.0
- mo = mo_form.save()
-
- mo.action_confirm()
- mo.button_plan()
-
- workorder_0 = mo.workorder_ids[0]
- workorder_0.button_start()
- workorder_0.record_production()
- 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()
-
- self.assertTrue(len(workorder_1.allowed_lots_domain) > 1)
- with Form(workorder_1) as wo:
- wo.finished_lot_id = lot_2
- workorder_1.record_production()
-
- workorder_2 = mo.workorder_ids[2]
- self.assertEqual(workorder_2.allowed_lots_domain, lot_1 | lot_2)
-
- self.assertEqual(workorder_0.finished_workorder_line_ids.qty_done, 2)
- self.assertFalse(workorder_0.finished_workorder_line_ids.lot_id)
- self.assertEqual(sum(workorder_1.finished_workorder_line_ids.mapped('qty_done')), 2)
- self.assertEqual(workorder_1.finished_workorder_line_ids.mapped('lot_id'), lot_1 | lot_2)
-
- def test_04_test_planning_date(self):
- """ Test that workorder are planned at the correct time. """
- # The workcenter is working 24/7
- self.full_availability()
-
- dining_table = self.dining_table
-
- production_table_form = Form(self.env['mrp.production'])
- production_table_form.product_id = dining_table
- production_table_form.bom_id = self.mrp_bom_desk
- production_table_form.product_qty = 1.0
- production_table_form.product_uom_id = dining_table.uom_id
- production_table = production_table_form.save()
- production_table.action_confirm()
-
- # Create work order
- production_table.button_plan()
- workorder = production_table.workorder_ids[0]
-
- # Check that the workorder is planned now and that it lasts one hour
- self.assertAlmostEqual(workorder.date_planned_start, datetime.now(), delta=timedelta(seconds=10), msg="Workorder should be planned now.")
- self.assertAlmostEqual(workorder.date_planned_finished, datetime.now() + timedelta(hours=1), delta=timedelta(seconds=10), msg="Workorder should be done in an hour.")
-
- def test_04b_test_planning_date(self):
- """ Test that workorder are planned at the correct time when setting a start date """
- # The workcenter is working 24/7
- self.full_availability()
-
- dining_table = self.dining_table
-
- date_start = datetime.now() + timedelta(days=1)
-
- production_table_form = Form(self.env['mrp.production'])
- production_table_form.product_id = dining_table
- production_table_form.bom_id = self.mrp_bom_desk
- production_table_form.product_qty = 1.0
- production_table_form.product_uom_id = dining_table.uom_id
- production_table_form.date_planned_start = date_start
- production_table = production_table_form.save()
- production_table.action_confirm()
-
- # Create work order
- production_table.button_plan()
- workorder = production_table.workorder_ids[0]
-
- # Check that the workorder is planned now and that it lasts one hour
- self.assertAlmostEqual(workorder.date_planned_start, date_start, delta=timedelta(seconds=1), msg="Workorder should be planned tomorrow.")
- self.assertAlmostEqual(workorder.date_planned_finished, date_start + timedelta(hours=1), delta=timedelta(seconds=1), msg="Workorder should be done one hour later.")
-
- def test_planning_overlaps_wo(self):
- """ Test that workorder doesn't overlaps between then when plan the MO """
- self.full_availability()
-
- dining_table = self.dining_table
-
- # Take between +30min -> +90min
- date_start = datetime.now() + timedelta(minutes=30)
-
- production_table_form = Form(self.env['mrp.production'])
- production_table_form.product_id = dining_table
- production_table_form.bom_id = self.mrp_bom_desk
- production_table_form.product_qty = 1.0
- production_table_form.product_uom_id = dining_table.uom_id
- production_table_form.date_planned_start = date_start
- production_table = production_table_form.save()
- production_table.action_confirm()
-
- # Create work order
- production_table.button_plan()
- workorder_prev = production_table.workorder_ids[0]
-
- # Check that the workorder is planned now and that it lasts one hour
- self.assertAlmostEqual(workorder_prev.date_planned_start, date_start, delta=timedelta(seconds=10), msg="Workorder should be planned in +30min")
- self.assertAlmostEqual(workorder_prev.date_planned_finished, date_start + timedelta(hours=1), delta=timedelta(seconds=10), msg="Workorder should be done in +90min")
-
- # As soon as possible, but because of the first one, it will planned only after +90 min
- date_start = datetime.now()
-
- production_table_form = Form(self.env['mrp.production'])
- production_table_form.product_id = dining_table
- production_table_form.bom_id = self.mrp_bom_desk
- production_table_form.product_qty = 1.0
- production_table_form.product_uom_id = dining_table.uom_id
- production_table_form.date_planned_start = date_start
- production_table = production_table_form.save()
- production_table.action_confirm()
-
- # Create work order
- production_table.button_plan()
- workorder = production_table.workorder_ids[0]
-
- # Check that the workorder is planned now and that it lasts one hour
- self.assertAlmostEqual(workorder.date_planned_start, workorder_prev.date_planned_finished, delta=timedelta(seconds=10), msg="Workorder should be planned after the first one")
- self.assertAlmostEqual(workorder.date_planned_finished, workorder_prev.date_planned_finished + timedelta(hours=1), delta=timedelta(seconds=10), msg="Workorder should be done one hour later.")
-
- def test_change_production_1(self):
- """Change the quantity to produce on the MO while workorders are already planned."""
- dining_table = self.dining_table
- dining_table.tracking = 'lot'
- production_table_form = Form(self.env['mrp.production'])
- production_table_form.product_id = dining_table
- production_table_form.bom_id = self.mrp_bom_desk
- production_table_form.product_qty = 1.0
- production_table_form.product_uom_id = dining_table.uom_id
- production_table = production_table_form.save()
- production_table.action_confirm()
-
- # Create work order
- production_table.button_plan()
-
- context = {'active_id': production_table.id, 'active_model': 'mrp.production'}
- change_qty_form = Form(self.env['change.production.qty'].with_context(context))
- change_qty_form.product_qty = 2.00
- change_qty = change_qty_form.save()
- change_qty.change_prod_qty()
-
- self.assertEqual(production_table.workorder_ids[0].qty_producing, 2, "Quantity to produce not updated")
-
- def test_expected_duration_workorder(self):
- """ Test that the expected_duration is editable and change when other
- workcenter is set or product quantity to produce is increased
- Also this expected duration need to be recalculate correctly during the planning
- if it used a alternative workcenter
- """
- planned_date = datetime(2023, 5, 15, 9, 0)
- self.mrp_workcenter_3.write({'capacity': 1, 'time_start': 0, 'time_stop': 2, 'time_efficiency': 50})
- self.workcenter_1.write({'capacity': 2, 'time_start': 2, 'time_stop': 1, 'time_efficiency': 80, 'alternative_workcenter_ids': [(6, 0, [self.mrp_workcenter_3.id])]})
- self.planning_bom.operation_ids[0].write({'time_cycle_manual': 10})
- mo_form = Form(self.env['mrp.production'])
- mo_form.bom_id = self.planning_bom
- mo_form.date_planned_start = planned_date
- mo = mo_form.save()
-
- # TEST Default/onchange duration_expected
- self.assertEqual(len(mo.workorder_ids), 1)
- self.assertEqual(mo.workorder_ids.workcenter_id, self.workcenter_1, "wrong workcenter")
- # time_cycle = 10, product_qty = 4.0, capacity = 2, time_start = 2, time_stop = 1, time_efficiency = 80%
- # => 2 + 1 + (4 / 2 * 10) * (100 / 80) = 28.0
- self.assertEqual(mo.workorder_ids.duration_expected, 28.0, "wrong duration_expected")
- mo_form = Form(mo)
- with mo_form.workorder_ids.edit(0) as wo_form:
- wo_form.workcenter_id = self.mrp_workcenter_3
- mo = mo_form.save()
- # time_cycle = 10, product_qty = 4.0, capacity = 1, time_start = 0, time_stop = 2, time_efficiency = 50%
- # => 0 + 2 + (4 / 1 * 10) * (100 / 50) = 82.0
- self.assertEqual(mo.workorder_ids.duration_expected, 82.0, "wrong duration_expected")
-
- # TEST Manual change of duration_expected
- with mo_form.workorder_ids.edit(0) as wo_form:
- wo_form.workcenter_id = self.workcenter_1
- self.assertEqual(wo_form.duration_expected, 28.0, "wrong duration_expected")
- wo_form.duration_expected = 40.0
- mo = mo_form.save()
-
- self.assertEqual(mo.workorder_ids.duration_expected, 40.0, "wrong duration_expected")
- mo = mo_form.save()
- mo.action_confirm()
- mo.button_plan()
- self.assertEqual(mo.workorder_ids.duration_expected, 40.0, "duration_expected doesn't reset")
-
- self.assertAlmostEqual(mo.date_planned_start, mo.workorder_ids.date_planned_start, delta=timedelta(seconds=10))
- self.assertAlmostEqual(mo.date_planned_finished, mo.workorder_ids.date_planned_finished, delta=timedelta(seconds=10))
- self.assertAlmostEqual(mo.date_planned_start, planned_date, delta=timedelta(seconds=10))
- self.assertAlmostEqual(mo.date_planned_finished, planned_date + timedelta(minutes=40), delta=timedelta(seconds=10))
-
- def test_planning_0(self):
- """ Test alternative conditions
- 1. alternative relation is directionnal
- 2. a workcenter cannot be it's own alternative """
- self.workcenter_1.alternative_workcenter_ids = self.wc_alt_1 | self.wc_alt_2
- self.assertEqual(self.wc_alt_1.alternative_workcenter_ids, self.env['mrp.workcenter'], "Alternative workcenter is not reciprocal")
- self.assertEqual(self.wc_alt_2.alternative_workcenter_ids, self.env['mrp.workcenter'], "Alternative workcenter is not reciprocal")
- with self.assertRaises(ValidationError):
- self.workcenter_1.alternative_workcenter_ids |= self.workcenter_1
-
- def test_planning_1(self):
- """ Testing planning workorder with alternative workcenters
- Plan 6 times the same MO, the workorders should be split accross workcenters
- The 3 workcenters are free, this test plans 3 workorder in a row then three next.
- The workcenters have not exactly the same parameters (efficiency, start time) so the
- the last 3 workorder are not dispatched like the 3 first.
- At the end of the test, the calendars will look like:
- - calendar wc1 :[mo1][mo4]
- - calendar wc2 :[mo2 ][mo5 ]
- - calendar wc3 :[mo3 ][mo6 ]"""
- planned_date = datetime(2023, 5, 15, 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):
- # Create an MO for product4
- 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_planned_start = 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
- # - wc_alt_2 as the best efficiency
- # - wc_alt_1 take a little less start time
- # - workcenter_1 is the worst
- 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_planned_start = 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
- Batch size of the operation will influence start dates of workorders
- The first unit to be produced can go the second workorder before finishing
- to produce the second unit.
- calendar wc1 : [q1][q2]
- calendar wc2 : [q1][q2]"""
- self.workcenter_1.alternative_workcenter_ids = self.wc_alt_1 | self.wc_alt_2
- self.planning_bom.operation_ids = False
- self.planning_bom.write({
- 'operation_ids': [
- (0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1, 'batch': 'yes', 'batch_size': 1}),
- (0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
- ],
- })
- # Allow second workorder to start once the first one is not ended yet
- self.env['mrp.workcenter'].search([]).write({'capacity': 1})
- # workcenters work 24/7
- self.full_availability()
-
- 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 = 2
- mo = mo_form.save()
- mo.action_confirm()
- plan = datetime.now()
- mo.button_plan()
- self.assertEqual(mo.workorder_ids[0].workcenter_id, self.wc_alt_2, "wrong workcenter")
- self.assertEqual(mo.workorder_ids[1].workcenter_id, self.wc_alt_1, "wrong workcenter")
-
- duration1 = self.planning_bom.operation_ids[0].time_cycle * 100.0 / self.wc_alt_2.time_efficiency + self.wc_alt_2.time_start
- duration2 = 2.0 * self.planning_bom.operation_ids[0].time_cycle * 100.0 / self.wc_alt_1.time_efficiency + self.wc_alt_1.time_start + self.wc_alt_1.time_stop
- wo2_start = mo.workorder_ids[1].date_planned_start
- wo2_stop = mo.workorder_ids[1].date_planned_finished
-
- wo2_start_theo = self.wc_alt_2.resource_calendar_id.plan_hours(duration1 / 60.0, plan, compute_leaves=False, resource=self.wc_alt_2.resource_id)
- wo2_stop_theo = self.wc_alt_1.resource_calendar_id.plan_hours(duration2 / 60.0, wo2_start, compute_leaves=False, resource=self.wc_alt_2.resource_id)
-
- self.assertAlmostEqual(wo2_start, wo2_start_theo, delta=timedelta(seconds=10), msg="Wrong plannification")
- self.assertAlmostEqual(wo2_stop, wo2_stop_theo, delta=timedelta(seconds=10), msg="Wrong plannification")
-
- def test_planning_3(self):
- """ Plan some manufacturing orders with 1 workorder on 1 workcenter
- the first workorder will be hard set in the future to see if the second
- one take the free slot before on the calendar
- calendar after first mo : [ ][mo1]
- calendar after second mo: [mo2][mo1] """
- planned_date = datetime(2023, 5, 15, 14, 0)
-
- 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
-
- 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_planned_start = planned_date
- mo = mo_form.save()
- start = mo.date_planned_start
- mo.action_confirm()
- mo.button_plan()
- self.assertEqual(mo.workorder_ids[0].workcenter_id, self.wc_alt_2, "wrong workcenter")
- wo1_start = mo.workorder_ids[0].date_planned_start
- wo1_stop = mo.workorder_ids[0].date_planned_finished
- self.assertAlmostEqual(wo1_start, start, delta=timedelta(seconds=10), msg="Wrong plannification")
- self.assertAlmostEqual(wo1_stop, start + timedelta(minutes=85.58), delta=timedelta(seconds=10), msg="Wrong plannification")
-
- # second MO should be plan before as there is a free slot before
- planned_date = datetime(2023, 5, 15, 9, 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_planned_start = planned_date
- mo = mo_form.save()
- mo.action_confirm()
- mo.button_plan()
- self.assertEqual(mo.workorder_ids[0].workcenter_id, self.wc_alt_2, "wrong workcenter")
- wo1_start = mo.workorder_ids[0].date_planned_start
- wo1_stop = mo.workorder_ids[0].date_planned_finished
- self.assertAlmostEqual(wo1_start, planned_date, delta=timedelta(seconds=10), msg="Wrong plannification")
- self.assertAlmostEqual(wo1_stop, planned_date + timedelta(minutes=85.59), delta=timedelta(seconds=10), msg="Wrong plannification")
-
- def test_planning_4(self):
- """ Plan a manufacturing orders with 1 workorder on 1 workcenter
- the workcenter calendar is empty. which means the workcenter is never
- available. Planning a workorder on it should raise an error"""
-
- self.workcenter_1.alternative_workcenter_ids = self.wc_alt_1 | self.wc_alt_2
- self.env['resource.calendar'].search([]).write({'attendance_ids': [(5, False, False)]})
-
- 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()
- 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.env['mrp.workcenter'].search([]).write({'tz': 'UTC'}) # compute all date in UTC
- mrp_workcenter_3 = self.env['mrp.workcenter'].create({
- 'name': 'assembly line 1',
- 'resource_calendar_id': self.env.ref('resource.resource_calendar_std').id,
- })
- self.planning_bom.operation_ids = False
- self.planning_bom.write({
- 'operation_ids': [(0, 0, {
- 'workcenter_id': mrp_workcenter_3.id,
- 'name': 'Manual Assembly',
- 'time_cycle': 60,
- })]
- })
- planned_date = datetime(2023, 5, 15, 9, 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_planned_start = planned_date
- mo = mo_form.save()
- mo.action_confirm()
- mo.button_plan()
- wo = mo.workorder_ids
- self.assertAlmostEqual(wo.date_planned_start, planned_date, delta=timedelta(seconds=10))
- self.assertAlmostEqual(wo.date_planned_finished, planned_date + timedelta(minutes=60), 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_planned_start = planned_date
- 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))
-
- def test_planning_7(self):
- """ set the workcenter capacity to 10. Produce a dozen of product tracked by
- SN. The production should be done in two batches"""
- self.workcenter_1.capacity = 10
- self.workcenter_1.time_efficiency = 100
- self.workcenter_1.time_start = 0
- self.workcenter_1.time_stop = 0
- self.planning_bom.operation_ids.time_cycle = 60
- self.product_4.tracking = 'serial'
- mo_form = Form(self.env['mrp.production'])
- mo_form.product_id = self.product_4
- mo_form.bom_id = self.planning_bom
- mo_form.product_uom_id = self.uom_dozen
- mo_form.product_qty = 1
- mo = mo_form.save()
- mo.action_confirm()
- mo.button_plan()
- wo = mo.workorder_ids
- self.assertEqual(wo.duration_expected, 120)
-
- 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
- workorder is planned."""
- planned_date = datetime(2023, 5, 15, 9, 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_planned_start = planned_date
- mo = mo_form.save()
- mo.action_confirm()
- # Plans the MO and checks the date.
- mo.button_plan()
- self.assertAlmostEqual(mo.date_planned_start, planned_date, delta=timedelta(seconds=10))
- self.assertEqual(bool(mo.workorder_ids.exists()), True)
- leave = mo.workorder_ids.leave_id
- self.assertEqual(bool(leave.exists()), True)
- # Unplans the MO and checks the workorder and its leave no more exist.
- mo.button_unplan()
- self.assertEqual(bool(mo.workorder_ids.exists()), True)
- self.assertEqual(bool(leave.exists()), False)
- # Plans (again) the MO and checks the date is still the same.
- mo.button_plan()
- 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))
-
- def test_kit_planning(self):
- """ Bom made of component 1 and component 2 which is a kit made of
- component 1 too. Check the workorder lines are well created after reservation
- Main bom :
- - comp1 (qty=1)
- - kit (qty=1)
- - comp1 (qty=4)
- - comp2 (qty=1)
- should give :
- - wo line 1 (comp1, qty=1)
- - wo line 2 (comp1, qty=4)
- - wo line 3 (comp2, qty=1) """
- # Kit bom
- self.env['mrp.bom'].create({
- 'product_id': self.product_4.id,
- 'product_tmpl_id': self.product_4.product_tmpl_id.id,
- 'product_uom_id': self.uom_unit.id,
- 'product_qty': 1.0,
- 'consumption': 'flexible',
- 'type': 'phantom',
- 'bom_line_ids': [
- (0, 0, {'product_id': self.product_2.id, 'product_qty': 1}),
- (0, 0, {'product_id': self.product_1.id, 'product_qty': 4})
- ]})
-
- # Main bom
- main_bom = self.env['mrp.bom'].create({
- 'product_id': self.product_5.id,
- 'product_tmpl_id': self.product_5.product_tmpl_id.id,
- 'product_uom_id': self.uom_unit.id,
- 'product_qty': 1.0,
- 'consumption': 'flexible',
- 'operation_ids': [
- (0, 0, {
- 'workcenter_id': self.mrp_workcenter_1.id,
- 'name': 'Packing',
- 'time_cycle': 30,
- 'sequence': 5}),
- (0, 0, {
- 'workcenter_id': self.mrp_workcenter_3.id,
- 'name': 'Testing',
- 'time_cycle': 60,
- 'sequence': 10}),
- (0, 0, {
- 'workcenter_id': self.mrp_workcenter_3.id,
- 'name': 'Long time assembly',
- 'time_cycle': 180,
- 'sequence': 15}),
- ],
- 'type': 'normal',
- 'bom_line_ids': [
- (0, 0, {'product_id': self.product_1.id, 'product_qty': 1}),
- (0, 0, {'product_id': self.product_4.id, 'product_qty': 1})
- ]})
-
- mo_form = Form(self.env['mrp.production'])
- mo_form.product_id = self.product_5
- mo_form.bom_id = main_bom
- mo_form.product_qty = 1
- mo = mo_form.save()
- mo.action_confirm()
- mo.action_assign()
- mo.button_plan()
-
- self.assertEqual(len(mo.workorder_ids), 3)
- long_time_assembly = mo.workorder_ids[2]
- self.assertEqual(len(long_time_assembly.raw_workorder_line_ids), 3)
- line1 = long_time_assembly.raw_workorder_line_ids[0]
- line2 = long_time_assembly.raw_workorder_line_ids[1]
- line3 = long_time_assembly.raw_workorder_line_ids[2]
- self.assertEqual(line1.product_id, self.product_1)
- self.assertEqual(line1.qty_done, 1)
- self.assertEqual(line2.product_id, self.product_2)
- self.assertEqual(line2.qty_done, 1)
- self.assertEqual(line3.product_id, self.product_1)
- self.assertEqual(line3.qty_done, 4)
-
- def test_conflict_and_replan(self):
- """ TEST Json data conflicted and the replan button of a workorder """
- dining_table = self.dining_table
- bom = self.mrp_bom_desk
- bom.operation_ids = False
- bom.write({
- 'operation_ids': [
- (0, 0, {
- 'workcenter_id': self.mrp_workcenter_3.id,
- 'name': 'Packing',
- 'time_cycle': 30,
- 'sequence': 5}),
- (0, 0, {
- 'workcenter_id': self.mrp_workcenter_3.id,
- 'name': 'Testing',
- 'time_cycle': 60,
- 'sequence': 10}),
- (0, 0, {
- 'workcenter_id': self.mrp_workcenter_3.id,
- 'name': 'Long time assembly',
- 'time_cycle': 180,
- 'sequence': 15}),
- ]})
-
-
- bom.bom_line_ids.filtered(lambda p: p.product_id == self.product_table_sheet).operation_id = bom.operation_ids[0].id
- bom.bom_line_ids.filtered(lambda p: p.product_id == self.product_table_leg).operation_id = bom.operation_ids[1].id
- bom.bom_line_ids.filtered(lambda p: p.product_id == self.product_bolt).operation_id = bom.operation_ids[2].id
-
- production_table_form = Form(self.env['mrp.production'])
- production_table_form.product_id = dining_table
- production_table_form.bom_id = bom
- production_table_form.product_qty = 1.0
- production_table_form.product_uom_id = dining_table.uom_id
- production_table = production_table_form.save()
-
- production_table.action_confirm()
- # Create work order
- production_table.button_plan()
- # Check Work order created or not
- self.assertEqual(len(production_table.workorder_ids), 3)
-
- workorders = production_table.workorder_ids
- wo1, wo2, wo3 = workorders[0], workorders[1], workorders[2]
-
- self.assertEqual(wo1.state, 'ready', "First workorder state should be ready.")
- self.assertEqual(wo1.workcenter_id.id, self.mrp_workcenter_3.id)
- self.assertEqual(wo2.state, 'pending')
- self.assertEqual(wo3.state, 'pending')
-
- self.assertFalse(wo1.id in wo1._get_conflicted_workorder_ids(), "Shouldn't conflict")
- self.assertFalse(wo2.id in wo2._get_conflicted_workorder_ids(), "Shouldn't conflict")
- self.assertFalse(wo3.id in wo3._get_conflicted_workorder_ids(), "Shouldn't conflict")
-
- # Conflicted with wo1
- wo2.write({'date_planned_start': wo1.date_planned_start, 'date_planned_finished': wo1.date_planned_finished})
- # Bad order of workorders (wo3-wo1-wo2) + Late
- wo3.write({'date_planned_start': wo1.date_planned_start - timedelta(weeks=1), 'date_planned_finished': wo1.date_planned_finished - timedelta(weeks=1)})
-
- self.assertTrue(wo2.id in wo2._get_conflicted_workorder_ids(), "Should conflict with wo1")
- self.assertTrue(wo1.id in wo1._get_conflicted_workorder_ids(), "Should conflict with wo2")
-
- self.assertTrue('text-danger' in wo2.json_popover, "Popover should in be in red (due to conflict)")
- self.assertTrue('text-danger' in wo3.json_popover, "Popover should in be in red (due to bad order of wo)")
- self.assertTrue('text-warning' in wo3.json_popover, "Popover contains of warning (late)")
-
- wo1.button_start()
- self.assertEqual(wo1.state, 'progress')
- self.assertEqual(wo2.id in wo2._get_conflicted_workorder_ids(), False, "Shouldn't have a conflict because wo1 is in progress")
-
- wo1_date_planned_start = wo1.date_planned_start
- wo2_date_planned_start = wo2.date_planned_start
- wo3_date_planned_start = wo3.date_planned_start
-
- wo2.action_replan() # Replan all MO of WO
-
- self.assertEqual(wo1.date_planned_start, wo1_date_planned_start, "Planned date of Workorder 1 shouldn't change (because it is in progress)")
- self.assertNotEqual(wo2.date_planned_start, wo2_date_planned_start, "Planned date of Workorder 2 should be updated")
- self.assertNotEqual(wo3.date_planned_start, wo3_date_planned_start, "Planned date of Workorder 3 should be updated")
- self.assertTrue(wo3.date_planned_start > wo2.date_planned_start, "Workorder 2 should be before the 3")
-
-
-class TestRoutingAndKits(SavepointCase):
- @classmethod
- def setUpClass(cls):
- """
- kit1 (consu)
- compkit1
- finished1
- compfinished1
-
- Finished1 (Bom1)
- - compfinished1
- - kit1
- Kit1 (BomKit1)
- - compkit1
-
- Rounting1 (finished1)
- - operation 1
- - operation 2
- Rounting2 (kit1)
- - operation 1
- """
- super(TestRoutingAndKits, cls).setUpClass()
- cls.uom_unit = cls.env['uom.uom'].search([
- ('category_id', '=', cls.env.ref('uom.product_uom_categ_unit').id),
- ('uom_type', '=', 'reference')
- ], limit=1)
- cls.kit1 = cls.env['product.product'].create({
- 'name': 'kit1',
- 'type': 'consu',
- })
- cls.compkit1 = cls.env['product.product'].create({
- 'name': 'compkit1',
- 'type': 'product',
- })
- cls.finished1 = cls.env['product.product'].create({
- 'name': 'finished1',
- 'type': 'product',
- })
- cls.compfinished1 = cls.env['product.product'].create({
- 'name': 'compfinished',
- 'type': 'product',
- })
- cls.workcenter_finished1 = cls.env['mrp.workcenter'].create({
- 'name': 'workcenter1',
- })
- cls.workcenter_kit1 = cls.env['mrp.workcenter'].create({
- 'name': 'workcenter2',
- })
- cls.bom_finished1 = cls.env['mrp.bom'].create({
- 'product_id': cls.finished1.id,
- 'product_tmpl_id': cls.finished1.product_tmpl_id.id,
- 'product_uom_id': cls.uom_unit.id,
- 'consumption': 'flexible',
- 'product_qty': 1,
- 'type': 'normal',
- 'bom_line_ids': [
- (0, 0, {'product_id': cls.compfinished1.id, 'product_qty': 1}),
- (0, 0, {'product_id': cls.kit1.id, 'product_qty': 1}),
- ],
- 'operation_ids': [
- (0, 0, {'sequence': 1, 'name': 'finished operation 1', 'workcenter_id': cls.workcenter_finished1.id}),
- (0, 0, {'sequence': 2, 'name': 'finished operation 2', 'workcenter_id': cls.workcenter_finished1.id}),
- ],
- })
- cls.bom_kit1 = cls.env['mrp.bom'].create({
- 'product_id': cls.kit1.id,
- 'product_tmpl_id': cls.kit1.product_tmpl_id.id,
- 'product_uom_id': cls.uom_unit.id,
- 'product_qty': 1,
- 'type': 'phantom',
- 'bom_line_ids': [
- (0, 0, {'product_id': cls.compkit1.id, 'product_qty': 1}),
- ],
- 'operation_ids': [
- (0, 0, {'name': 'Kit operation', 'workcenter_id': cls.workcenter_kit1.id})
- ]
- })
-
- def test_1(self):
- """Operations are set on `self.bom_kit1` but none on `self.bom_finished1`."""
- self.bom_kit1.bom_line_ids.operation_id = self.bom_kit1.operation_ids[0]
-
- mo_form = Form(self.env['mrp.production'])
- mo_form.product_id = self.finished1
- mo_form.bom_id = self.bom_finished1
- mo_form.product_qty = 1.0
- mo = mo_form.save()
-
- mo.action_confirm()
- mo.button_plan()
-
- self.assertEqual(len(mo.workorder_ids), 3)
- self.assertEqual(len(mo.workorder_ids[0].raw_workorder_line_ids), 0)
- self.assertEqual(mo.workorder_ids[1].raw_workorder_line_ids.product_id, self.compfinished1)
- self.assertEqual(mo.workorder_ids[2].raw_workorder_line_ids.product_id, self.compkit1)
-
- def test_2(self):
- """Operations are not set on `self.bom_kit1` and `self.bom_finished1`."""
- mo_form = Form(self.env['mrp.production'])
- mo_form.product_id = self.finished1
- mo_form.bom_id = self.bom_finished1
- mo_form.product_qty = 1.0
- mo = mo_form.save()
-
- mo.action_confirm()
- mo.button_plan()
-
- self.assertEqual(len(mo.workorder_ids), 3)
- self.assertEqual(len(mo.workorder_ids[0].raw_workorder_line_ids), 0)
- self.assertEqual(mo.workorder_ids[1].raw_workorder_line_ids.product_id, self.compfinished1)
- self.assertEqual(mo.workorder_ids[2].raw_workorder_line_ids.product_id, self.compkit1)
-
- def test_3(self):
- """Operations are set both `self.bom_kit1` and `self.bom_finished1`."""
- self.bom_kit1.bom_line_ids.operation_id = self.bom_kit1.operation_ids
- self.bom_finished1.bom_line_ids[0].operation_id = self.bom_finished1.operation_ids[0]
- self.bom_finished1.bom_line_ids[1].operation_id = self.bom_finished1.operation_ids[1]
-
- mo_form = Form(self.env['mrp.production'])
- mo_form.product_id = self.finished1
- mo_form.bom_id = self.bom_finished1
- mo_form.product_qty = 1.0
- mo = mo_form.save()
-
- mo.action_confirm()
- mo.button_plan()
-
- self.assertEqual(len(mo.workorder_ids), 3)
- self.assertEqual(mo.workorder_ids[0].raw_workorder_line_ids.product_id, self.compfinished1)
- self.assertFalse(mo.workorder_ids[1].raw_workorder_line_ids.product_id.id)
- self.assertEqual(mo.workorder_ids[2].raw_workorder_line_ids.product_id, self.compkit1)
-
- def test_4(self):
- """Operations are set on `self.kit1`, none are set on `self.bom_finished1` and a kit
- without routing was added to `self.bom_finished1`. We expect the component of the kit
- without routing to be consumed at the last workorder of the main BoM.
- """
- kit2 = self.env['product.product'].create({
- 'name': 'kit2',
- 'type': 'consu',
- })
- compkit2 = self.env['product.product'].create({
- 'name': 'compkit2',
- 'type': 'product',
- })
- bom_kit2 = self.env['mrp.bom'].create({
- 'product_id': kit2.id,
- 'product_tmpl_id': kit2.product_tmpl_id.id,
- 'product_uom_id': self.uom_unit.id,
- 'product_qty': 1,
- 'type': 'phantom',
- 'bom_line_ids': [(0, 0, {'product_id': compkit2.id, 'product_qty': 1})]
- })
- self.bom_finished1.write({'bom_line_ids': [(0, 0, {'product_id': kit2.id, 'product_qty': 1})]})
-
- mo_form = Form(self.env['mrp.production'])
- mo_form.product_id = self.finished1
- mo_form.bom_id = self.bom_finished1
- mo_form.product_qty = 1.0
- mo = mo_form.save()
-
- mo.action_confirm()
- mo.button_plan()
-
- self.assertEqual(len(mo.workorder_ids), 3)
-
- self.assertEqual(len(mo.workorder_ids[0].raw_workorder_line_ids), 0)
- self.assertEqual(set(mo.workorder_ids[1].raw_workorder_line_ids.product_id.ids), set([self.compfinished1.id, compkit2.id]))
- self.assertEqual(mo.workorder_ids[2].raw_workorder_line_ids.product_id, self.compkit1)
-
- def test_5(self):
- # Main bom: set the normal component to the first of the two operations of the routing.
- bomline_compfinished = self.bom_finished1.bom_line_ids.filtered(lambda bl: bl.product_id == self.compfinished1)
- bomline_compfinished.operation_id = self.bom_finished1.operation_ids[0]
-
- # Main bom: the kit do not have an operation set but there's one on its bom
- bomline_kit1 = self.bom_finished1.bom_line_ids - bomline_compfinished
- self.assertFalse(bomline_kit1.operation_id.id)
- self.bom_kit1.bom_line_ids.operation_id = self.bom_kit1.operation_ids
-
- # Main bom: add a kit without routing
- kit2 = self.env['product.product'].create({
- 'name': 'kit2',
- 'type': 'consu',
- })
- compkit2 = self.env['product.product'].create({
- 'name': 'compkit2',
- 'type': 'product',
- })
- bom_kit2 = self.env['mrp.bom'].create({
- 'product_id': kit2.id,
- 'product_tmpl_id': kit2.product_tmpl_id.id,
- 'product_uom_id': self.uom_unit.id,
- 'product_qty': 1,
- 'type': 'phantom',
- 'bom_line_ids': [(0, 0, {'product_id': compkit2.id, 'product_qty': 1})]
- })
- self.bom_finished1.write({'bom_line_ids': [(0, 0, {'product_id': kit2.id, 'product_qty': 1})]})
-
- mo_form = Form(self.env['mrp.production'])
- mo_form.product_id = self.finished1
- mo_form.bom_id = self.bom_finished1
- mo_form.product_qty = 1.0
- mo = mo_form.save()
-
- mo.action_confirm()
- mo.button_plan()
-
- self.assertEqual(len(mo.workorder_ids), 3)
- self.assertEqual(mo.workorder_ids[0].raw_workorder_line_ids.product_id, self.compfinished1)
- self.assertEqual(mo.workorder_ids[1].raw_workorder_line_ids.product_id, compkit2)
- self.assertEqual(mo.workorder_ids[2].raw_workorder_line_ids.product_id, self.compkit1)
-
- # -------------------------------------------------------------------------
- # Those 2 next tests aren't related to routing and kit but to flexible
- # consumption.
- # -------------------------------------------------------------------------
- def test_merge_lot(self):
- """ Produce 10 units of product tracked by lot on two workorder. On the
- first one, produce 4 onto lot1 then 6 onto lot1 as well. The second
- workorder should be prefilled with 10 units and lot1"""
- self.finished1.tracking = 'lot'
- lot1 = self.env['stock.production.lot'].create({
- 'product_id': self.finished1.id,
- 'company_id': self.env.company.id,
- })
- mo_form = Form(self.env['mrp.production'])
- mo_form.product_id = self.finished1
- mo_form.bom_id = self.bom_finished1
- mo_form.product_qty = 10.0
- mo = mo_form.save()
-
- mo.action_confirm()
- mo.button_plan()
- wo1 = mo.workorder_ids.filtered(lambda wo: wo.state == 'ready')[0]
- wo1.button_start()
- wo1.qty_producing = 4
- wo1.finished_lot_id = lot1
- wo1.record_production()
- wo1.qty_producing = 6
- wo1.finished_lot_id = lot1
- wo1.record_production()
- wo2 = mo.workorder_ids.filtered(lambda wo: wo.state == 'ready')[0]
- wo2.button_start()
- self.assertEqual(wo2.qty_producing, 10)
- self.assertEqual(wo2.finished_lot_id, lot1)
-
- def test_add_move(self):
- """ Make a production using multi step routing. Add an additional move
- on a specific operation and check that the produce is consumed into the
- right workorder. """
- self.bom_finished1.consumption = 'flexible'
- add_product = self.env['product.product'].create({
- 'name': 'Additional',
- 'type': 'product',
- })
- mo_form = Form(self.env['mrp.production'])
- mo_form.product_id = self.finished1
- mo_form.bom_id = self.bom_finished1
- mo_form.product_qty = 10.0
- mo = mo_form.save()
-
- mo_form = Form(mo)
- with mo_form.move_raw_ids.new() as move:
- move.name = mo.name
- move.product_id = add_product
- move.product_uom = add_product.uom_id
- move.location_id = mo.location_src_id
- move.location_dest_id = mo.production_location_id
- move.product_uom_qty = 2
- move.operation_id = mo.bom_id.operation_ids[0]
- mo = mo_form.save()
- self.assertEqual(len(mo.move_raw_ids), 3)
- mo.action_confirm()
- self.assertEqual(mo.move_raw_ids.mapped('state'), ['confirmed'] * 3)
- mo.button_plan()
- self.assertEqual(len(mo.workorder_ids), 3)
- wo1 = mo.workorder_ids[0]
- lines = wo1.raw_workorder_line_ids
- self.assertEqual(lines.product_id, add_product)
-
- def test_add_move_2(self):
- """ Make a production using multi step routing. Add an additional move
- on a specific operation and check that the produce is consumed into the
- right workorder. """
- self.bom_finished1.consumption = 'flexible'
- add_product = self.env['product.product'].create({
- 'name': 'Additional',
- 'type': 'product',
- })
- mo_form = Form(self.env['mrp.production'])
- mo_form.product_id = self.finished1
- mo_form.bom_id = self.bom_finished1
- mo_form.product_qty = 10.0
- mo = mo_form.save()
- mo.action_confirm()
- mo_form = Form(mo)
- with mo_form.move_raw_ids.new() as move:
- move.name = mo.name
- move.product_id = add_product
- move.product_uom = add_product.uom_id
- move.location_id = mo.location_src_id
- move.location_dest_id = mo.production_location_id
- move.product_uom_qty = 2
- move.operation_id = mo.bom_id.operation_ids[0]
- mo = mo_form.save()
- new_move = mo.move_raw_ids.filtered(lambda move: move.additional)
- self.assertEqual(len(mo.move_raw_ids), 3)
- self.assertEqual(len(new_move), 1)
- self.assertEqual(mo.move_raw_ids.mapped('state'), ['confirmed'] * 3)
- mo.button_plan()
- self.assertEqual(len(mo.workorder_ids), 3)
- wo1 = mo.workorder_ids[0]
- lines = wo1.raw_workorder_line_ids
- self.assertEqual(lines.product_id, add_product)
diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml
index e3ce89b4a53..25a2c2f5dab 100644
--- a/addons/mrp/views/mrp_production_views.xml
+++ b/addons/mrp/views/mrp_production_views.xml
@@ -286,43 +286,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/addons/mrp/views/mrp_workorder_views.xml b/addons/mrp/views/mrp_workorder_views.xml
index 081dc74ab02..9b4bfc5196a 100644
--- a/addons/mrp/views/mrp_workorder_views.xml
+++ b/addons/mrp/views/mrp_workorder_views.xml
@@ -89,7 +89,6 @@
-
@@ -107,6 +106,7 @@
+
diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py
index 72d00e9b348..bacce8e4ca5 100644
--- a/addons/mrp/wizard/change_production_qty.py
+++ b/addons/mrp/wizard/change_production_qty.py
@@ -50,11 +50,11 @@ class ChangeProductionQty(models.TransientModel):
format_qty = '%.{precision}f'.format(precision=precision)
raise UserError(_("You have already processed %s. Please input a quantity higher than %s ") % (format_qty % produced, format_qty % produced))
old_production_qty = production.product_qty
- production.write({'product_qty': wizard.product_qty})
+ new_production_qty = wizard.product_qty
done_moves = production.move_finished_ids.filtered(lambda x: x.state == 'done' and x.product_id == production.product_id)
qty_produced = production.product_id.uom_id._compute_quantity(sum(done_moves.mapped('product_qty')), production.product_uom_id)
- factor = (production.product_qty - qty_produced) / (old_production_qty - qty_produced)
+ factor = (new_production_qty - qty_produced) / (old_production_qty - qty_produced)
update_info = production._update_raw_moves(factor)
documents = {}
for move, old_qty, new_qty in update_info:
@@ -67,9 +67,10 @@ class ChangeProductionQty(models.TransientModel):
else:
documents[key] = [value]
production._log_manufacture_exception(documents)
- finished_moves_modification = self._update_finished_moves(production, production.product_qty - qty_produced, old_production_qty - qty_produced)
+ finished_moves_modification = self._update_finished_moves(production, new_production_qty - qty_produced, old_production_qty - qty_produced)
if finished_moves_modification:
production._log_downside_manufactured_quantity(finished_moves_modification)
+ production.write({'product_qty': new_production_qty})
for wo in production.workorder_ids:
operation = wo.operation_id
@@ -80,9 +81,9 @@ class ChangeProductionQty(models.TransientModel):
else:
quantity = quantity if (quantity > 0) else 0
if float_is_zero(quantity, precision_digits=precision):
- wo.finished_lot_id = False
- wo._workorder_line_ids().unlink()
- wo.qty_producing = quantity
+ wo.check_ids.unlink()
+ else:
+ wo.qty_producing = quantity
if wo.qty_produced < wo.qty_production and wo.state == 'done':
wo.state = 'progress'
if wo.qty_produced == wo.qty_production and wo.state == 'progress':
diff --git a/addons/mrp_account/tests/test_mrp_account.py b/addons/mrp_account/tests/test_mrp_account.py
index f77dbfc3c6e..f827a0eb023 100644
--- a/addons/mrp_account/tests/test_mrp_account.py
+++ b/addons/mrp_account/tests/test_mrp_account.py
@@ -1,16 +1,120 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo.addons.mrp.tests.test_workorder_operation import TestWorkOrderProcessCommon
+from odoo.addons.mrp.tests.common import TestMrpCommon
from odoo.tests import Form
-class TestMrpAccount(TestWorkOrderProcessCommon):
+class TestMrpAccount(TestMrpCommon):
@classmethod
def setUpClass(cls):
super(TestMrpAccount, cls).setUpClass()
+ cls.source_location_id = cls.stock_location_14.id
+ cls.warehouse = cls.env.ref('stock.warehouse0')
+ # setting up alternative workcenters
+ cls.wc_alt_1 = cls.env['mrp.workcenter'].create({
+ 'name': 'Nuclear Workcenter bis',
+ 'capacity': 3,
+ 'time_start': 9,
+ 'time_stop': 5,
+ 'time_efficiency': 80,
+ })
+ cls.wc_alt_2 = cls.env['mrp.workcenter'].create({
+ 'name': 'Nuclear Workcenter ter',
+ 'capacity': 1,
+ 'time_start': 10,
+ 'time_stop': 5,
+ 'time_efficiency': 85,
+ })
+ cls.product_4.uom_id = cls.uom_unit
+ cls.planning_bom = cls.env['mrp.bom'].create({
+ 'product_id': cls.product_4.id,
+ 'product_tmpl_id': cls.product_4.product_tmpl_id.id,
+ 'product_uom_id': cls.uom_unit.id,
+ 'product_qty': 4.0,
+ 'consumption': 'flexible',
+ 'operation_ids': [
+ (0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
+ ],
+ 'type': 'normal',
+ 'bom_line_ids': [
+ (0, 0, {'product_id': cls.product_2.id, 'product_qty': 2}),
+ (0, 0, {'product_id': cls.product_1.id, 'product_qty': 4})
+ ]})
+ cls.dining_table = cls.env['product.product'].create({
+ 'name': 'Table (MTO)',
+ 'type': 'product',
+ 'tracking': 'serial',
+ })
+ cls.product_table_sheet = cls.env['product.product'].create({
+ 'name': 'Table Top',
+ 'type': 'product',
+ 'tracking': 'serial',
+ })
+ cls.product_table_leg = cls.env['product.product'].create({
+ 'name': 'Table Leg',
+ 'type': 'product',
+ 'tracking': 'lot',
+ })
+ cls.product_bolt = cls.env['product.product'].create({
+ 'name': 'Bolt',
+ 'type': 'product',
+ })
+ cls.product_screw = cls.env['product.product'].create({
+ 'name': 'Screw',
+ 'type': 'product',
+ })
+ cls.mrp_workcenter = cls.env['mrp.workcenter'].create({
+ 'name': 'Assembly Line 1',
+ 'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
+ })
+ cls.mrp_bom_desk = cls.env['mrp.bom'].create({
+ 'product_tmpl_id': cls.dining_table.product_tmpl_id.id,
+ 'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
+ 'sequence': 3,
+ 'consumption': 'flexible',
+ 'operation_ids': [
+ (0, 0, {'workcenter_id': cls.mrp_workcenter.id, 'name': 'Manual Assembly'}),
+ ],
+ })
+ cls.mrp_bom_desk.write({
+ 'bom_line_ids': [
+ (0, 0, {
+ 'product_id': cls.product_table_sheet.id,
+ 'product_qty': 1,
+ 'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
+ 'sequence': 1,
+ 'operation_id': cls.mrp_bom_desk.operation_ids.id}),
+ (0, 0, {
+ 'product_id': cls.product_table_leg.id,
+ 'product_qty': 4,
+ 'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
+ 'sequence': 2,
+ 'operation_id': cls.mrp_bom_desk.operation_ids.id}),
+ (0, 0, {
+ 'product_id': cls.product_bolt.id,
+ 'product_qty': 4,
+ 'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
+ 'sequence': 3,
+ 'operation_id': cls.mrp_bom_desk.operation_ids.id}),
+ (0, 0, {
+ 'product_id': cls.product_screw.id,
+ 'product_qty': 10,
+ 'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
+ 'sequence': 4,
+ 'operation_id': cls.mrp_bom_desk.operation_ids.id}),
+ ]
+ })
+ cls.mrp_workcenter_1 = cls.env['mrp.workcenter'].create({
+ 'name': 'Drill Station 1',
+ 'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
+ })
+ cls.mrp_workcenter_3 = cls.env['mrp.workcenter'].create({
+ 'name': 'Assembly Line 1',
+ 'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id,
+ })
cls.categ_standard = cls.env['product.category'].create({
'name': 'STANDARD',
'property_cost_method': 'standard'
@@ -66,10 +170,11 @@ class TestMrpAccount(TestWorkOrderProcessCommon):
})
inventory.action_validate
bom = self.mrp_bom_desk.copy()
+ bom.operation_ids = False
production_table_form = Form(self.env['mrp.production'])
production_table_form.product_id = self.dining_table
production_table_form.bom_id = bom
- production_table_form.product_qty = 5.0
+ production_table_form.product_qty = 1
production_table = production_table_form.save()
production_table.extra_cost = 20
diff --git a/addons/mrp_product_expiry/models/__init__.py b/addons/mrp_product_expiry/models/__init__.py
index 51e07c35917..3728a50a4f0 100644
--- a/addons/mrp_product_expiry/models/__init__.py
+++ b/addons/mrp_product_expiry/models/__init__.py
@@ -2,4 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mrp_production
-from . import mrp_workorder
+
diff --git a/addons/mrp_product_expiry/models/mrp_workorder.py b/addons/mrp_product_expiry/models/mrp_workorder.py
deleted file mode 100644
index 0acc06d829b..00000000000
--- a/addons/mrp_product_expiry/models/mrp_workorder.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from odoo import models, _
-
-
-class MrpWorkorder(models.Model):
- _inherit = 'mrp.workorder'
-
- def record_production(self):
- confirm_expired_lots = self._check_expired_lots()
- if confirm_expired_lots:
- return confirm_expired_lots
- return super().record_production()
-
- def _check_expired_lots(self):
- # We use the 'skip_expired' context key to avoid to make the check when
- # user already confirmed the wizard about using expired lots.
- if self.env.context.get('skip_expired'):
- return False
- expired_lot_ids = self.raw_workorder_line_ids.filtered(lambda ml: ml.lot_id.product_expiry_alert).lot_id.ids
- if expired_lot_ids:
- return {
- 'name': _('Confirmation'),
- 'type': 'ir.actions.act_window',
- 'res_model': 'expiry.picking.confirmation',
- 'view_mode': 'form',
- 'target': 'new',
- 'context': self._get_expired_context(expired_lot_ids),
- }
-
- def _get_expired_context(self, expired_lot_ids):
- context = dict(self.env.context)
- context.update({
- 'default_lot_ids': [(6, 0, expired_lot_ids)],
- 'default_workorder_id': self.id,
- })
- return context
diff --git a/addons/mrp_product_expiry/tests/test_mrp_product_expiry.py b/addons/mrp_product_expiry/tests/test_mrp_product_expiry.py
index 22e9764d2fd..137877c95a0 100644
--- a/addons/mrp_product_expiry/tests/test_mrp_product_expiry.py
+++ b/addons/mrp_product_expiry/tests/test_mrp_product_expiry.py
@@ -113,71 +113,3 @@ class TestStockProductionLot(TestStockCommon):
self.assertNotEqual(res, None)
self.assertEqual(res['res_model'], 'expiry.picking.confirmation')
- def test_03_workorder_without_expired_lot(self):
- """ Checks user doesn't get a confirmation wizard when they makes a
- workorder without expired components. """
- # Set a routing on the BOM.
- self.bom_apple_pie.write({
- 'operation_ids': [(0, 0, {
- 'name': 'Bake in the oven',
- 'workcenter_id': self.workcenter.id,
- 'time_cycle': 15,
- 'sequence': 1,
- })],
- })
- # Creates the MO, starts it and plans the Work Order.
- mo_form = Form(self.env['mrp.production'])
- mo_form.product_id = self.product_apple_pie
- mo_form.bom_id = self.bom_apple_pie
- mo_form.product_qty = 1
- mo = mo_form.save()
- mo.action_confirm()
- mo.button_plan()
-
- wo = mo.workorder_ids[0]
- wo.button_start()
- # Set a non-expired lot.
- wo.raw_workorder_line_ids.write({
- 'qty_done': 3,
- 'lot_id': self.lot_good_apple,
- })
-
- res = wo.record_production()
- # Try to record the production using non-expired lot must not return a wizard.
- self.assertEqual(res, True)
- mo.button_mark_done()
-
- def test_04_workorder_with_expired_lot(self):
- """ Checks user doesn't get a confirmation wizard when they makes a
- workorder without expired components. """
- # Set a routing on the BOM.
- self.bom_apple_pie.write({
- 'operation_ids': [(0, 0, {
- 'name': 'Bake in the oven',
- 'workcenter_id': self.workcenter.id,
- 'time_cycle': 15,
- 'sequence': 1,
- })],
- })
- # Creates the MO, starts it and plans the Work Order.
- mo_form = Form(self.env['mrp.production'])
- mo_form.product_id = self.product_apple_pie
- mo_form.bom_id = self.bom_apple_pie
- mo_form.product_qty = 1
- mo = mo_form.save()
- mo.action_confirm()
- mo.button_plan()
-
- wo = mo.workorder_ids[0]
- wo.button_start()
- # Set an expired lot.
- wo.raw_workorder_line_ids.write({
- 'qty_done': 3,
- 'lot_id': self.lot_expired_apple,
- })
-
- res = wo.record_production()
- # Try to record the production using expired lot must return a
- # confirmation wizard.
- self.assertNotEqual(res, None)
- self.assertEqual(res['res_model'], 'expiry.picking.confirmation')
diff --git a/addons/mrp_subcontracting/models/stock_picking.py b/addons/mrp_subcontracting/models/stock_picking.py
index 43cba77b1a3..137015dc846 100644
--- a/addons/mrp_subcontracting/models/stock_picking.py
+++ b/addons/mrp_subcontracting/models/stock_picking.py
@@ -73,13 +73,7 @@ class StockPicking(models.Model):
(production.qty_producing - production.qty_produced) * move.unit_factor, production.product_uom_id,
rounding_method='HALF-UP')
move.move_line_ids.filtered(lambda ml: ml.state not in ('done', 'cancel')).qty_done = 0
- vals = move._set_quantity_done_prepare_vals(new_qty)
- if vals['to_create']:
- for res in vals['to_create']:
- move.move_line_ids.create(res)
- if vals['to_write']:
- for move_line, res in vals['to_write']:
- move_line.write(res)
+ move.move_line_ids = move._set_quantity_done_prepare_vals(new_qty)
productions |= production
for subcontracted_production in productions:
if subcontracted_production.state == 'progress':
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 0af3341055f..f008e4160e8 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -1552,7 +1552,7 @@ class StockMove(models.Model):
return [(self.picking_id, self.product_id.responsible_id, visited)]
def _set_quantity_done_prepare_vals(self, qty):
- res = {'to_write': [], 'to_create': []}
+ res = []
for ml in self.move_line_ids:
ml_qty = ml.product_uom_qty - ml.qty_done
if float_compare(ml_qty, 0, precision_rounding=ml.product_uom_id.rounding) <= 0:
@@ -1569,7 +1569,7 @@ class StockMove(models.Model):
# Assign qty_done and explicitly round to make sure there is no inconsistency between
# ml.qty_done and qty.
taken_qty = float_round(taken_qty, precision_rounding=ml.product_uom_id.rounding)
- res['to_write'].append((ml, {'qty_done': ml.qty_done + taken_qty}))
+ res.append((1, ml.id, {'qty_done': ml.qty_done + taken_qty}))
if ml.product_uom_id != self.product_uom:
taken_qty = ml.product_uom_id._compute_quantity(ml_qty, self.product_uom, round=False)
qty -= taken_qty
@@ -1580,14 +1580,14 @@ class StockMove(models.Model):
if self.product_id.tracking != 'serial':
vals = self._prepare_move_line_vals(quantity=0)
vals['qty_done'] = qty
- res['to_create'].append(vals)
+ res.append((0, 0, vals))
else:
uom_qty = self.product_uom._compute_quantity(qty, self.product_id.uom_id)
for i in range(0, int(uom_qty)):
vals = self._prepare_move_line_vals(quantity=0)
vals['qty_done'] = 1
vals['product_uom_id'] = self.product_id.uom_id.id
- res['to_create'].append(vals)
+ res.append((0, 0, vals))
return res
def _set_quantity_done(self, qty):
@@ -1597,13 +1597,7 @@ class StockMove(models.Model):
looking for trouble...).
@param qty: quantity in the UoM of move.product_uom
"""
- vals = self._set_quantity_done_prepare_vals(qty)
- if vals['to_create']:
- for res in vals['to_create']:
- self.env['stock.move.line'].create(res)
- if vals['to_write']:
- for move_line, vals in vals['to_write']:
- move_line.write(vals)
+ self.move_line_ids = self._set_quantity_done_prepare_vals(qty)
def _adjust_procure_method(self):
""" This method will try to apply the procure method MTO on some moves if