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 @@ - - - - - - - - - - - - - - - - - - - - -