diff --git a/addons/mrp/__manifest__.py b/addons/mrp/__manifest__.py index 235c0e80736..ce6d450810d 100644 --- a/addons/mrp/__manifest__.py +++ b/addons/mrp/__manifest__.py @@ -15,10 +15,11 @@ 'security/mrp_security.xml', 'security/ir.model.access.csv', 'data/mrp_data.xml', - 'wizard/mrp_product_produce_views.xml', 'wizard/change_production_qty_views.xml', 'wizard/mrp_workcenter_block_view.xml', 'wizard/stock_warn_insufficient_qty_views.xml', + 'wizard/mrp_production_backorder.xml', + 'wizard/mrp_consumption_warning_views.xml', 'views/mrp_views_menus.xml', 'views/stock_move_views.xml', 'views/mrp_workorder_views.xml', diff --git a/addons/mrp/data/mrp_data.xml b/addons/mrp/data/mrp_data.xml index 64041d0b39b..c41f0576a30 100644 --- a/addons/mrp/data/mrp_data.xml +++ b/addons/mrp/data/mrp_data.xml @@ -2,15 +2,6 @@ - - Routing - mrp.routing - RO/ - 5 - 1 - 1 - - - - - Primary Assembly - - - - Secondary Assembly - - - - Manual Component's Assembly - - - - Assemble Furniture - - - - - - - - - Manual Assembly - 60 - 5 - - - - - - - Long time assembly - 180 - 15 - - - - - - - Testing - 60 - 10 - - - - - - - Packing - 30 - 5 - - - - - - - 120 - 5 - Manual Assembly - - - - - - - 120 - 10 - Assembly - - - @@ -114,7 +37,14 @@ 1 - + + + + + Manual Assembly + 60 + 5 + @@ -296,7 +226,14 @@ 3 flexible - + + + + + 120 + 10 + Assembly + @@ -350,8 +287,16 @@ 1 - + + + + Manual Assembly + 60 + 5 + + + 2 @@ -371,7 +316,32 @@ 1 - + + + + + Long time assembly + 180 + 15 + + + + + + + Testing + 60 + 10 + + + + + + + Packing + 30 + 5 + @@ -596,9 +566,34 @@ 2 - SEC-ASSEM + + + + Long time assembly + 180 + 15 + + + + + + + Testing + 60 + 10 + + + + + + + Packing + 30 + 5 + + 1 @@ -630,6 +625,10 @@ + + + + @@ -677,6 +676,10 @@ + + + + @@ -689,32 +692,21 @@ + + + + + - - + + + - - - - - - - - - + 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_abstract_workorder.py b/addons/mrp/models/mrp_abstract_workorder.py deleted file mode 100644 index 13d560f2320..00000000000 --- a/addons/mrp/models/mrp_abstract_workorder.py +++ /dev/null @@ -1,561 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from collections import defaultdict - -from odoo import api, fields, models, _ -from odoo.exceptions import UserError -from odoo.tools import float_compare, float_round, float_is_zero - - -class MrpAbstractWorkorder(models.AbstractModel): - _name = "mrp.abstract.workorder" - _description = "Common code between produce wizards and workorders." - _check_company_auto = True - - production_id = fields.Many2one('mrp.production', 'Manufacturing Order', required=True, check_company=True) - product_id = fields.Many2one(related='production_id.product_id', readonly=True, store=True, check_company=True) - qty_producing = fields.Float(string='Currently Produced Quantity', digits='Product Unit of Measure') - product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True, readonly=True) - finished_lot_id = fields.Many2one( - 'stock.production.lot', string='Lot/Serial Number', - domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True) - product_tracking = fields.Selection(related="product_id.tracking") - consumption = fields.Selection([ - ('strict', 'Strict'), - ('flexible', 'Flexible')], - required=True, - ) - use_create_components_lots = fields.Boolean(related="production_id.picking_type_id.use_create_components_lots") - company_id = fields.Many2one(related='production_id.company_id') - - @api.model - 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 - ) - - def _workorder_line_ids(self): - self.ensure_one() - return self.raw_workorder_line_ids | self.finished_workorder_line_ids - - @api.onchange('qty_producing') - def _onchange_qty_producing(self): - """ Modify the qty currently producing will modify the existing - workorder line in order to match the new quantity to consume for each - component and their reserved quantity. - """ - if self.qty_producing <= 0: - raise UserError(_('You have to produce at least one %s.') % self.product_uom_id.name) - line_values = self._update_workorder_lines() - for values in line_values['to_create']: - self.env[self._workorder_line_ids()._name].new(values) - for line in line_values['to_delete']: - if line in self.raw_workorder_line_ids: - self.raw_workorder_line_ids -= line - else: - self.finished_workorder_line_ids -= line - for line, vals in line_values['to_update'].items(): - line.update(vals) - - def _update_workorder_lines(self): - """ Update workorder lines, according to the new qty currently - produced. It returns a dict with line to create, update or delete. - It do not directly write or unlink the line because this function is - used in onchange and request that write on db (e.g. workorder creation). - """ - line_values = {'to_create': [], 'to_delete': [], 'to_update': {}} - # moves are actual records - move_finished_ids = self.move_finished_ids._origin.filtered(lambda move: move.product_id != self.product_id and move.state not in ('done', 'cancel')) - move_raw_ids = self.move_raw_ids._origin.filtered(lambda move: move.state not in ('done', 'cancel')) - for move in move_raw_ids | move_finished_ids: - move_workorder_lines = self._workorder_line_ids().filtered(lambda w: w.move_id == move) - - # Compute the new quantity for the current component - rounding = move.product_uom.rounding - new_qty = self._prepare_component_quantity(move, self.qty_producing) - - # In case the production uom is different than the workorder uom - # it means the product is serial and production uom is not the reference - new_qty = self.product_uom_id._compute_quantity( - new_qty, - self.production_id.product_uom_id, - round=False - ) - qty_todo = float_round(new_qty - sum(move_workorder_lines.mapped('qty_to_consume')), precision_rounding=rounding) - - # Remove or lower quantity on exisiting workorder lines - if float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0: - qty_todo = abs(qty_todo) - # Try to decrease or remove lines that are not reserved and - # partialy reserved first. A different decrease strategy could - # be define in _unreserve_order method. - for workorder_line in move_workorder_lines.sorted(key=lambda wl: wl._unreserve_order()): - if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0: - break - # If the quantity to consume on the line is lower than the - # quantity to remove, the line could be remove. - if float_compare(workorder_line.qty_to_consume, qty_todo, precision_rounding=rounding) <= 0: - qty_todo = float_round(qty_todo - workorder_line.qty_to_consume, precision_rounding=rounding) - if line_values['to_delete']: - line_values['to_delete'] |= workorder_line - else: - line_values['to_delete'] = workorder_line - # decrease the quantity on the line - else: - new_val = workorder_line.qty_to_consume - qty_todo - # avoid to write a negative reserved quantity - new_reserved = max(0, workorder_line.qty_reserved - qty_todo) - line_values['to_update'][workorder_line] = { - 'qty_to_consume': new_val, - 'qty_done': new_val, - 'qty_reserved': new_reserved, - } - qty_todo = 0 - else: - # Search among wo lines which one could be updated - qty_reserved_wl = defaultdict(float) - # Try to update the line with the greater reservation first in - # order to promote bigger batch. - for workorder_line in move_workorder_lines.sorted(key=lambda wl: wl.qty_reserved, reverse=True): - rounding = workorder_line.product_uom_id.rounding - if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0: - break - move_lines = workorder_line._get_move_lines() - qty_reserved_wl[workorder_line.lot_id] += workorder_line.qty_reserved - # The reserved quantity according to exisiting move line - # already produced (with qty_done set) and other production - # lines with the same lot that are currently on production. - qty_reserved_remaining = sum(move_lines.mapped('product_uom_qty')) - sum(move_lines.mapped('qty_done')) - qty_reserved_wl[workorder_line.lot_id] - if float_compare(qty_reserved_remaining, 0, precision_rounding=rounding) > 0: - qty_to_add = min(qty_reserved_remaining, qty_todo) - line_values['to_update'][workorder_line] = { - 'qty_done': workorder_line.qty_to_consume + qty_to_add, - 'qty_to_consume': workorder_line.qty_to_consume + qty_to_add, - 'qty_reserved': workorder_line.qty_reserved + qty_to_add, - } - qty_todo -= qty_to_add - qty_reserved_wl[workorder_line.lot_id] += qty_to_add - - # If a line exists without reservation and without lot. It - # means that previous operations could not find any reserved - # quantity and created a line without lot prefilled. In this - # case, the system will not find an existing move line with - # available reservation anymore and will increase this line - # instead of creating a new line without lot and reserved - # quantities. - if not workorder_line.qty_reserved and not workorder_line.lot_id and workorder_line.product_tracking != 'serial': - line_values['to_update'][workorder_line] = { - 'qty_done': workorder_line.qty_to_consume + qty_todo, - 'qty_to_consume': workorder_line.qty_to_consume + qty_todo, - } - qty_todo = 0 - - # if there are still qty_todo, create new wo lines - if float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0: - for values in self._generate_lines_values(move, qty_todo): - line_values['to_create'].append(values) - return line_values - - @api.model - def _generate_lines_values(self, move, qty_to_consume): - """ Create workorder line. First generate line based on the reservation, - in order to prefill reserved quantity, lot and serial number. - If the quantity to consume is greater than the reservation quantity then - create line with the correct quantity to consume but without lot or - serial number. - """ - lines = [] - is_tracked = move.product_id.tracking == 'serial' - if move in self.move_raw_ids._origin: - # Get the inverse_name (many2one on line) of raw_workorder_line_ids - initial_line_values = {self.raw_workorder_line_ids._get_raw_workorder_inverse_name(): self.id} - else: - # Get the inverse_name (many2one on line) of finished_workorder_line_ids - initial_line_values = {self.finished_workorder_line_ids._get_finished_workoder_inverse_name(): self.id} - for move_line in move.move_line_ids: - line = dict(initial_line_values) - if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) <= 0: - break - # move line already 'used' in workorder (from its lot for instance) - if move_line.lot_produced_ids or float_compare(move_line.product_uom_qty, move_line.qty_done, precision_rounding=move.product_uom.rounding) <= 0: - continue - # search wo line on which the lot is not fully consumed or other reserved lot - linked_wo_line = self._workorder_line_ids().filtered( - lambda line: line.move_id == move and - line.lot_id == move_line.lot_id - ) - if linked_wo_line: - if float_compare(sum(linked_wo_line.mapped('qty_to_consume')), move_line.product_uom_qty - move_line.qty_done, precision_rounding=move.product_uom.rounding) < 0: - to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty - move_line.qty_done - sum(linked_wo_line.mapped('qty_to_consume'))) - else: - continue - else: - to_consume_in_line = min(qty_to_consume, move_line.product_uom_qty - move_line.qty_done) - line.update({ - 'move_id': move.id, - 'product_id': move.product_id.id, - 'product_uom_id': is_tracked and move.product_id.uom_id.id or move.product_uom.id, - 'qty_to_consume': to_consume_in_line, - 'qty_reserved': to_consume_in_line, - 'lot_id': move_line.lot_id.id, - 'qty_done': to_consume_in_line, - }) - lines.append(line) - qty_to_consume -= to_consume_in_line - # The move has not reserved the whole quantity so we create new wo lines - if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0: - line = dict(initial_line_values) - if move.product_id.tracking == 'serial': - while float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) > 0: - line.update({ - 'move_id': move.id, - 'product_id': move.product_id.id, - 'product_uom_id': move.product_id.uom_id.id, - 'qty_to_consume': 1, - 'qty_done': 1, - }) - lines.append(line) - qty_to_consume -= 1 - else: - line.update({ - 'move_id': move.id, - 'product_id': move.product_id.id, - 'product_uom_id': move.product_uom.id, - 'qty_to_consume': qty_to_consume, - 'qty_done': qty_to_consume, - }) - lines.append(line) - return lines - - 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 _update_moves(self): - """ Once the production is done. Modify the workorder lines into - stock move line with the registered lot and quantity done. - """ - # add missing move for extra component/byproduct - for line in self._workorder_line_ids(): - if not line.move_id: - line._set_move_id() - # Before writting produce quantities, we ensure they respect the bom strictness - self._strict_consumption_check() - vals_list = [] - workorder_lines_to_process = self._workorder_line_ids().filtered(lambda line: line.product_id != self.product_id and line.qty_done > 0) - for line in workorder_lines_to_process: - line._update_move_lines() - if float_compare(line.qty_done, 0, precision_rounding=line.product_uom_id.rounding) > 0: - vals_list += line._create_extra_move_lines() - - self._workorder_line_ids().filtered(lambda line: line.product_id != self.product_id).unlink() - self.env['stock.move.line'].create(vals_list) - - def _strict_consumption_check(self): - if self.consumption == 'strict': - for move in self.move_raw_ids: - lines = self._workorder_line_ids().filtered(lambda l: l.move_id == move) - qty_done = 0.0 - qty_to_consume = 0.0 - for line in lines: - qty_done += line.product_uom_id._compute_quantity(line.qty_done, line.product_id.uom_id) - qty_to_consume += line.product_uom_id._compute_quantity(line.qty_to_consume, line.product_id.uom_id) - rounding = self.product_uom_id.rounding - if float_compare(qty_done, qty_to_consume, 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.') % lines[0].product_id.name) - - 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) - - -class MrpAbstractWorkorderLine(models.AbstractModel): - _name = "mrp.abstract.workorder.line" - _description = "Abstract model to implement product_produce_line as well as\ - workorder_line" - _check_company_auto = True - - move_id = fields.Many2one('stock.move', check_company=True) - product_id = fields.Many2one('product.product', 'Product', required=True, check_company=True) - product_tracking = fields.Selection(related="product_id.tracking") - lot_id = fields.Many2one( - 'stock.production.lot', 'Lot/Serial Number', - check_company=True, - domain="[('product_id', '=', product_id), '|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]") - qty_to_consume = fields.Float('To Consume', digits='Product Unit of Measure') - product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure') - qty_done = fields.Float('Consumed', digits='Product Unit of Measure') - qty_reserved = fields.Float('Reserved', digits='Product Unit of Measure') - company_id = fields.Many2one('res.company', compute='_compute_company_id') - - @api.onchange('lot_id') - def _onchange_lot_id(self): - """ When the user is encoding a produce line for a tracked product, we apply some logic to - help him. This onchange will automatically switch `qty_done` to 1.0. - """ - if self.product_id.tracking == 'serial': - self.qty_done = 1 - - @api.onchange('product_id') - def _onchange_product_id(self): - if self.product_id and not self.move_id: - self.product_uom_id = self.product_id.uom_id - - @api.onchange('qty_done') - def _onchange_qty_done(self): - """ When the user is encoding a produce line for a tracked product, we apply some logic to - help him. This onchange will warn him if he set `qty_done` to a non-supported value. - """ - res = {} - if self.product_id.tracking == 'serial' and not float_is_zero(self.qty_done, self.product_uom_id.rounding): - if float_compare(self.qty_done, 1.0, precision_rounding=self.product_uom_id.rounding) != 0: - message = _('You can only process 1.0 %s of products with unique serial number.') % self.product_id.uom_id.name - res['warning'] = {'title': _('Warning'), 'message': message} - return res - - def _compute_company_id(self): - for line in self: - line.company_id = line._get_production().company_id - - def _update_move_lines(self): - """ update a move line to save the workorder line data""" - self.ensure_one() - if self.lot_id: - move_lines = self.move_id.move_line_ids.filtered(lambda ml: ml.lot_id == self.lot_id and not ml.lot_produced_ids) - else: - move_lines = self.move_id.move_line_ids.filtered(lambda ml: not ml.lot_id and not ml.lot_produced_ids) - - # Sanity check: if the product is a serial number and `lot` is already present in the other - # consumed move lines, raise. - if self.product_id.tracking != 'none' and not self.lot_id: - raise UserError(_('Please enter a lot or serial number for %s !' % self.product_id.display_name)) - - # Update reservation and quantity done - for ml in move_lines: - rounding = ml.product_uom_id.rounding - if float_compare(self.qty_done, 0, precision_rounding=rounding) <= 0: - break - quantity_to_process = min(self.qty_done, ml.product_uom_qty - ml.qty_done) - self.qty_done -= quantity_to_process - - new_quantity_done = (ml.qty_done + quantity_to_process) - # if we produce less than the reserved quantity to produce the finished products - # in different lots, - # we create different component_move_lines to record which one was used - # on which lot of finished product - if float_compare(new_quantity_done, ml.product_uom_qty, precision_rounding=rounding) >= 0: - ml.write({ - 'qty_done': new_quantity_done, - 'lot_produced_ids': self._get_produced_lots(), - }) - else: - new_qty_reserved = ml.product_uom_qty - new_quantity_done - default = { - 'product_uom_qty': new_quantity_done, - 'qty_done': new_quantity_done, - 'lot_produced_ids': self._get_produced_lots(), - } - ml.copy(default=default) - ml.with_context(bypass_reservation_update=True).write({ - 'product_uom_qty': new_qty_reserved, - 'qty_done': 0 - }) - - def _create_extra_move_lines(self): - """Create new sml if quantity produced is bigger than the reserved one""" - vals_list = [] - # apply putaway - location_dest_id = self.move_id.location_dest_id._get_putaway_strategy(self.product_id) or self.move_id.location_dest_id - quants = self.env['stock.quant']._gather(self.product_id, self.move_id.location_id, lot_id=self.lot_id, strict=False) - # Search for a sub-locations where the product is available. - # Loop on the quants to get the locations. If there is not enough - # quantity into stock, we take the move location. Anyway, no - # reservation is made, so it is still possible to change it afterwards. - for quant in quants: - quantity = quant.quantity - quant.reserved_quantity - quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom_id, rounding_method='HALF-UP') - rounding = quant.product_uom_id.rounding - if (float_compare(quant.quantity, 0, precision_rounding=rounding) <= 0 or - float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) <= 0): - continue - vals = { - 'move_id': self.move_id.id, - 'product_id': self.product_id.id, - 'location_id': quant.location_id.id, - 'location_dest_id': location_dest_id.id, - 'product_uom_qty': 0, - 'product_uom_id': self.product_uom_id.id, - 'qty_done': min(quantity, self.qty_done), - 'lot_produced_ids': self._get_produced_lots(), - } - if self.lot_id: - vals.update({'lot_id': self.lot_id.id}) - - vals_list.append(vals) - self.qty_done -= vals['qty_done'] - # If all the qty_done is distributed, we can close the loop - if float_compare(self.qty_done, 0, precision_rounding=self.product_id.uom_id.rounding) <= 0: - break - - if float_compare(self.qty_done, 0, precision_rounding=self.product_id.uom_id.rounding) > 0: - vals = { - 'move_id': self.move_id.id, - 'product_id': self.product_id.id, - 'location_id': self.move_id.location_id.id, - 'location_dest_id': location_dest_id.id, - 'product_uom_qty': 0, - 'product_uom_id': self.product_uom_id.id, - 'qty_done': self.qty_done, - 'lot_produced_ids': self._get_produced_lots(), - } - if self.lot_id: - vals.update({'lot_id': self.lot_id.id}) - - vals_list.append(vals) - - return vals_list - - def _check_line_sn_uniqueness(self): - """ Alert the user if the line serial number as already been consumed/produced """ - self.ensure_one() - if self.product_tracking == 'serial' and self.lot_id: - domain = [ - ('lot_id', '=', self.lot_id.id), - ('qty_done', '=', 1), - ('state', '=', 'done') - ] - # Adapt domain and error message in case of component or byproduct - if self[self._get_raw_workorder_inverse_name()]: - message = _('The serial number %s used for component %s has already been consumed') % (self.lot_id.name, self.product_id.name) - co_prod_move_lines = self._get_production().move_raw_ids.move_line_ids - co_prod_wo_lines = self[self._get_raw_workorder_inverse_name()].raw_workorder_line_ids - domain_unbuild = domain + [ - ('production_id', '=', False), - ('location_id.usage', '=', 'production') - ] - domain.append(('location_dest_id.usage', '=', 'production')) - else: - message = _('The serial number %s used for byproduct %s has already been produced') % (self.lot_id.name, self.product_id.name) - co_prod_move_lines = self._get_production().move_finished_ids.move_line_ids.filtered(lambda ml: ml.product_id != self._get_production().product_id) - co_prod_wo_lines = self[self._get_finished_workoder_inverse_name()].finished_workorder_line_ids - domain_unbuild = domain + [ - ('production_id', '=', False), - ('location_dest_id.usage', '=', 'production') - ] - domain.append(('location_id.usage', '=', 'production')) - - # Check presence of same sn in previous productions - duplicates = self.env['stock.move.line'].search_count(domain) - if duplicates: - # Maybe some move lines have been compensated by unbuild - duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild) - if not (duplicates_unbuild and duplicates - duplicates_unbuild == 0): - raise UserError(message) - # Check presence of same sn in current production - duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == self.lot_id) - if duplicates: - raise UserError(message) - # Check presence of same sn in current wizard/workorder - duplicates = co_prod_wo_lines.filtered(lambda wol: wol.lot_id == self.lot_id) - self - if duplicates: - raise UserError(message) - - def _set_move_id(self): - """ Check the line has a stock_move on which on the quantity will be - transfered at the end of the production """ - self.ensure_one() - # Find move_id that would match - mo = self._get_production() - if self[self._get_raw_workorder_inverse_name()]: - moves = mo.move_raw_ids - else: - moves = mo.move_finished_ids - move_id = moves.filtered(lambda m: m.product_id == self.product_id and m.state not in ('done', 'cancel')) - if not move_id: - # create a move to assign it to the line - if self[self._get_raw_workorder_inverse_name()]: - values = mo._get_move_raw_values(self.product_id, self.qty_done, self.product_uom_id) - elif self.product_id != mo.product_id: - values = mo._get_finished_move_value(self.product_id.id, self.qty_done, self.product_uom_id.id) - else: - # The line is neither a component nor a byproduct - return - move_id = self.env['stock.move'].create(values) - self.move_id = move_id.id - - def _unreserve_order(self): - """ Unreserve line with lower reserved quantity first """ - self.ensure_one() - return (self.qty_reserved,) - - def _get_move_lines(self): - return self.move_id.move_line_ids.filtered(lambda ml: - ml.lot_id == self.lot_id and ml.product_id == self.product_id) - - def _get_produced_lots(self): - return self.move_id in self._get_production().move_raw_ids and self._get_final_lots() and [(4, lot.id) for lot in self._get_final_lots()] - - @api.model - def _get_raw_workorder_inverse_name(self): - raise NotImplementedError('Method _get_raw_workorder_inverse_name() undefined on %s' % self) - - @api.model - def _get_finished_workoder_inverse_name(self): - raise NotImplementedError('Method _get_finished_workoder_inverse_name() undefined on %s' % self) - - # To be implemented in specific model - def _get_final_lots(self): - raise NotImplementedError('Method _get_final_lots() undefined on %s' % self) - - def _get_production(self): - raise NotImplementedError('Method _get_production() undefined on %s' % self) diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py index 362b63e5ee8..6f0603c028f 100644 --- a/addons/mrp/models/mrp_bom.py +++ b/addons/mrp/models/mrp_bom.py @@ -48,10 +48,7 @@ class MrpBom(models.Model): help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control", domain="[('category_id', '=', product_uom_category_id)]") product_uom_category_id = fields.Many2one(related='product_tmpl_id.uom_id.category_id') sequence = fields.Integer('Sequence', help="Gives the sequence order when displaying a list of bills of material.") - routing_id = fields.Many2one( - 'mrp.routing', 'Routing', check_company=True, - help="The operations for producing this BoM. When a routing is specified, the production orders will " - " be executed through work orders, otherwise everything is processed in the production order itself. ") + operation_ids = fields.One2many('mrp.routing.workcenter', 'bom_id', 'Operations', copy=True) ready_to_produce = fields.Selection([ ('all_available', ' When all components are available'), ('asap', 'When components for 1st operation are available')], string='Manufacturing Readiness', @@ -66,11 +63,15 @@ class MrpBom(models.Model): 'res.company', 'Company', index=True, default=lambda self: self.env.company) consumption = fields.Selection([ - ('strict', 'Strict'), - ('flexible', 'Flexible')], - help="Defines if you can consume more or less components than the quantity defined on the BoM.", - default='strict', - string='Consumption', + ('flexible', 'Allowed'), + ('warning', 'Allowed with warning'), + ('strict', 'Blocked')], + help="Defines if you can consume more or less components than the quantity defined on the BoM:\n" + " * Allowed: allowed for all manufacturing users.\n" + " * Allowed with warning: allowed for all manufacturing users with summary of consumption differences when closing the manufacturing order.\n" + " * Blocked: only a manager can close a manufacturing order when the BoM consumption is not respected.", + default='warning', + string='Flexible Consumption', required=True ) @@ -119,11 +120,6 @@ class MrpBom(models.Model): for line in self.bom_line_ids: line.bom_product_template_attribute_value_ids = False - @api.onchange('routing_id') - def onchange_routing_id(self): - for line in self.bom_line_ids: - line.operation_id = False - @api.model def name_create(self, name): # prevent to use string as product_tmpl_id @@ -261,12 +257,6 @@ class MrpBomLine(models.Model): sequence = fields.Integer( 'Sequence', default=1, help="Gives the sequence order when displaying.") - routing_id = fields.Many2one( - 'mrp.routing', 'Routing', - related='bom_id.routing_id', store=True, readonly=False, - help="The list of operations to produce the finished product. The routing is mainly used to " - "compute work center costs during operations and to plan future loads on work centers " - "based on production planning.") bom_id = fields.Many2one( 'mrp.bom', 'Parent BoM', index=True, ondelete='cascade', required=True) @@ -276,9 +266,10 @@ class MrpBomLine(models.Model): 'product.template.attribute.value', string="Apply on Variants", ondelete='restrict', domain="[('id', 'in', possible_bom_product_template_attribute_value_ids)]", help="BOM Product Variants needed to apply this line.") + allowed_operation_ids = fields.Many2many('mrp.routing.workcenter', compute='_compute_allowed_operation_ids') operation_id = fields.Many2one( 'mrp.routing.workcenter', 'Consumed in Operation', check_company=True, - domain="[('routing_id', '=', routing_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]", + domain="[('id', 'in', allowed_operation_ids)]", help="The operation where the components are consumed, or the finished products created.") child_bom_id = fields.Many2one( 'mrp.bom', 'Sub BoM', compute='_compute_child_bom_id') @@ -327,6 +318,20 @@ class MrpBomLine(models.Model): for line in self: line.child_line_ids = line.child_bom_id.bom_line_ids.ids or False + @api.depends('bom_id') + def _compute_allowed_operation_ids(self): + for bom_line in self: + if not bom_line.bom_id.operation_ids: + bom_line.allowed_operation_ids = self.env['mrp.routing.workcenter'] + else: + operation_domain = [ + ('id', 'in', bom_line.bom_id.operation_ids.ids), + '|', + ('company_id', '=', bom_line.company_id.id), + ('company_id', '=', False) + ] + bom_line.allowed_operation_ids = self.env['mrp.routing.workcenter'].search(operation_domain) + @api.onchange('product_uom_id') def onchange_product_uom_id(self): res = {} @@ -403,11 +408,24 @@ class MrpByProduct(models.Model): default=1.0, digits='Product Unit of Measure', required=True) product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True) bom_id = fields.Many2one('mrp.bom', 'BoM', ondelete='cascade') - routing_id = fields.Many2one( - 'mrp.routing', 'Routing', store=True, related='bom_id.routing_id') + allowed_operation_ids = fields.Many2many('mrp.routing.workcenter', compute='_compute_allowed_operation_ids') operation_id = fields.Many2one( 'mrp.routing.workcenter', 'Produced in Operation', check_company=True, - domain="[('routing_id', '=', routing_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]") + domain="[('id', 'in', allowed_operation_ids)]") + + @api.depends('bom_id') + def _compute_allowed_operation_ids(self): + for byproduct in self: + if not byproduct.bom_id.operation_ids: + byproduct.allowed_operation_ids = self.env['mrp.routing.workcenter'] + else: + operation_domain = [ + ('id', 'in', byproduct.bom_id.operation_ids.ids), + '|', + ('company_id', '=', byproduct.company_id.id), + ('company_id', '=', False) + ] + byproduct.allowed_operation_ids = self.env['mrp.routing.workcenter'].search(operation_domain) @api.onchange('product_id') def onchange_product_id(self): diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index f6529bff906..ba8d0d98e22 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -3,13 +3,18 @@ import json import datetime +import math +import re + from collections import defaultdict from dateutil.relativedelta import relativedelta from itertools import groupby from odoo import api, fields, models, _ from odoo.exceptions import AccessError, UserError -from odoo.tools import date_utils, float_compare, float_round, float_is_zero, format_datetime +from odoo.tools import float_compare, float_round, float_is_zero, format_datetime + +SIZE_BACK_ORDER_NUMERING = 3 class MrpProduction(models.Model): @@ -54,8 +59,19 @@ class MrpProduction(models.Model): return fields.Datetime.to_datetime(self.env.context.get('default_date_planned_start')) + datetime.timedelta(hours=1) return datetime.datetime.now() + datetime.timedelta(hours=1) + @api.model + def _get_default_date_planned_start(self): + if self.env.context.get('default_date_deadline'): + return fields.Datetime.to_datetime(self.env.context.get('default_date_deadline')) + return datetime.datetime.now() + + @api.model + def _get_default_is_locked(self): + return self.user_has_groups('mrp.group_locked_by_default') + name = fields.Char( 'Reference', copy=False, readonly=True, default=lambda x: _('New')) + backorder_sequence = fields.Integer("Backorder Sequence", default=0, copy=False, help="Backorder sequence, if equals to 0 means there is not related backorder") origin = fields.Char( 'Source', copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, @@ -66,6 +82,7 @@ class MrpProduction(models.Model): domain="[('id', 'in', allowed_product_ids)]", readonly=True, required=True, check_company=True, states={'draft': [('readonly', False)]}) + product_tracking = fields.Selection(related='product_id.tracking') allowed_product_ids = fields.Many2many('product.product', compute='_compute_allowed_product_ids') product_tmpl_id = fields.Many2one('product.template', 'Product Template', related='product_id.product_tmpl_id') product_qty = fields.Float( @@ -77,6 +94,10 @@ class MrpProduction(models.Model): 'uom.uom', 'Product Unit of Measure', readonly=True, required=True, states={'draft': [('readonly', False)]}, domain="[('category_id', '=', product_uom_category_id)]") + lot_producing_id = fields.Many2one( + 'stock.production.lot', string='Lot/Serial Number', copy=False, + domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True) + qty_producing = fields.Float(string="Quantity Producing", copy=False) product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id') product_uom_qty = fields.Float(string='Total Quantity', compute='_compute_product_uom_qty', store=True) picking_type_id = fields.Many2one( @@ -98,12 +119,12 @@ class MrpProduction(models.Model): states={'draft': [('readonly', False)]}, check_company=True, help="Location where the system will stock the finished products.") date_planned_start = fields.Datetime( - 'Planned Date', copy=False, default=fields.Datetime.now, + 'Scheduled Date', copy=False, default=_get_default_date_planned_start, compute='_compute_dates_planned', inverse='_set_date_planned_start', help="Date at which you plan to start the production.", index=True, required=True, store=True) date_planned_finished = fields.Datetime( - 'Planned End Date', + 'Scheduled End Date', default=_get_default_date_planned_finished, compute='_compute_dates_planned', inverse='_set_date_planned_finished', help="Date at which you plan to finish the production.", @@ -130,17 +151,10 @@ class MrpProduction(models.Model): ('type', '=', 'normal')]""", check_company=True, help="Bill of Materials allow you to define the list of required components to make a finished product.") - routing_id = fields.Many2one( - 'mrp.routing', 'Routing', - readonly=True, compute='_compute_routing', store=True, - help="The list of operations (list of work centers) to produce the finished product. The routing " - "is mainly used to compute work center costs during operations and to plan future loads on " - "work centers based on production planning.") state = fields.Selection([ ('draft', 'Draft'), ('confirmed', 'Confirmed'), - ('planned', 'Planned'), ('progress', 'In Progress'), ('to_close', 'To Close'), ('done', 'Done'), @@ -149,7 +163,6 @@ class MrpProduction(models.Model): store=True, tracking=True, help=" * Draft: The MO is not confirmed yet.\n" " * Confirmed: The MO is confirmed, the stock rules and the reordering of the components are trigerred.\n" - " * Planned: The WO are planned.\n" " * In Progress: The production has started (on the MO or on the WO).\n" " * To Close: The production is done, the MO has to be closed.\n" " * Done: The MO is closed, the stock moves are posted. \n" @@ -174,13 +187,12 @@ class MrpProduction(models.Model): 'stock.move', 'production_id', 'Finished Products', copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, domain=[('scrapped', '=', False)]) + move_byproduct_ids = fields.One2many('stock.move', compute='_compute_move_byproduct_ids', inverse='_set_move_byproduct_ids') finished_move_line_ids = fields.One2many( 'stock.move.line', compute='_compute_lines', inverse='_inverse_lines', string="Finished Product" ) workorder_ids = fields.One2many( - 'mrp.workorder', 'production_id', 'Work Orders', - copy=False, readonly=True) - workorder_count = fields.Integer('# Work Orders', compute='_compute_workorder_count') + 'mrp.workorder', 'production_id', 'Work Orders', copy=True) workorder_done_count = fields.Integer('# Done Work Orders', compute='_compute_workorder_done_count') move_dest_ids = fields.One2many('stock.move', 'created_production_id', string="Stock Movements of Produced Goods") @@ -191,9 +203,6 @@ class MrpProduction(models.Model): reserve_visible = fields.Boolean( 'Allowed to Reserve Production', compute='_compute_unreserve_visible', help='Technical field to check when we can reserve quantities') - post_visible = fields.Boolean( - 'Allowed to Post Inventory', compute='_compute_post_visible', - help='Technical field to check when we can post') user_id = fields.Many2one( 'res.users', 'Responsible', default=lambda self: self.env.user, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, @@ -222,22 +231,28 @@ class MrpProduction(models.Model): scrap_count = fields.Integer(compute='_compute_scrap_move_count', string='Scrap Move') priority = fields.Selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority', readonly=True, states={'draft': [('readonly', False)]}, default='1') - is_locked = fields.Boolean('Is Locked', default=True, copy=False) + is_locked = fields.Boolean('Is Locked', default=_get_default_is_locked, copy=False) + is_planned = fields.Boolean('Its Operations are Planned', compute="_compute_is_planned") + is_partially_planned = fields.Boolean('One operation is Planned', compute="_compute_is_planned") + show_final_lots = fields.Boolean('Show Final Lots', compute='_compute_show_lots') - production_location_id = fields.Many2one('stock.location', "Production Location", related='product_id.property_stock_production', readonly=False, related_sudo=False) + production_location_id = fields.Many2one('stock.location', "Production Location", compute="_compute_production_location", store=True) picking_ids = fields.Many2many('stock.picking', compute='_compute_picking_ids', string='Picking associated to this manufacturing order') delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids') confirm_cancel = fields.Boolean(compute='_compute_confirm_cancel') consumption = fields.Selection([ - ('strict', 'Strict'), - ('flexible', 'Flexible')], + ('flexible', 'Allowed'), + ('warning', 'Allowed with warning'), + ('strict', 'Blocked')], required=True, readonly=True, - default='strict', + default='flexible', ) mrp_production_child_count = fields.Integer("Number of generated MO", compute='_compute_mrp_production_child_count') mrp_production_source_count = fields.Integer("Number of source MO", compute='_compute_mrp_production_source_count') + mrp_production_backorder_count = fields.Integer("Count of linked backorder", compute='_compute_mrp_production_backorder') + show_lock = fields.Boolean('Show Lock/unlock buttons', compute='_compute_show_lock') @api.depends('product_id', 'bom_id', 'company_id') def _compute_allowed_product_ids(self): @@ -255,27 +270,45 @@ class MrpProduction(models.Model): product_domain += [('id', 'in', production.bom_id.product_tmpl_id.product_variant_ids.ids)] production.allowed_product_ids = self.env['product.product'].search(product_domain) - @api.depends('procurement_group_id.stock_move_ids.created_production_id') + @api.depends('procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids') def _compute_mrp_production_child_count(self): for production in self: - production.mrp_production_child_count = len(production.procurement_group_id.stock_move_ids.created_production_id) + production.mrp_production_child_count = len(production.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids) - @api.depends('move_dest_ids.group_id.mrp_production_id') + @api.depends('move_dest_ids.group_id.mrp_production_ids') def _compute_mrp_production_source_count(self): for production in self: - production.mrp_production_source_count = len(production.move_dest_ids.group_id.mrp_production_id) + production.mrp_production_source_count = len(production.procurement_group_id.mrp_production_ids.move_dest_ids.group_id.mrp_production_ids) + + @api.depends('procurement_group_id.mrp_production_ids') + def _compute_mrp_production_backorder(self): + for production in self: + production.mrp_production_backorder_count = len(production.procurement_group_id.mrp_production_ids) @api.depends('move_raw_ids.date_expected', 'move_finished_ids.date_expected') def _compute_dates_planned(self): for production in self: - production.date_planned_start = max(production.mapped('move_raw_ids.date_expected') or [fields.Datetime.now()]) - production.date_planned_finished = max(production.mapped('move_finished_ids.date_expected') or [production.date_deadline or fields.Datetime.now()]) + if production.state != 'done': + production.date_planned_start = max(production.mapped('move_raw_ids.date_expected') or [fields.Datetime.now()]) + if production.move_finished_ids: + production.date_planned_finished = max(production.mapped('move_finished_ids.date_expected')) def _set_date_planned_start(self): - self.move_raw_ids.write({'date_expected': self.date_planned_start}) + if self.date_planned_start: + self.move_raw_ids.write({'date_expected': self.date_planned_start}) def _set_date_planned_finished(self): - self.move_finished_ids.write({'date_expected': self.date_planned_finished}) + if self.date_planned_finished: + self.move_finished_ids.write({'date_expected': self.date_planned_finished}) + + def _compute_is_planned(self): + for production in self: + if production.workorder_ids: + production.is_planned = all(wo.date_planned_start and wo.date_planned_finished for wo in production.workorder_ids) + production.is_partially_planned = any(wo.date_planned_start and wo.date_planned_finished for wo in production.workorder_ids if production.state != 'draft') + else: + production.is_planned = False + production.is_partially_planned = False @api.depends('move_raw_ids.delay_alert_date') def _compute_delay_alert_date(self): @@ -353,6 +386,19 @@ class MrpProduction(models.Model): else: production.product_uom_qty = production.product_qty + @api.depends('product_id', 'company_id') + def _compute_production_location(self): + location_by_company = self.env['stock.location'].read_group([ + ('company_id', 'in', self.company_id.ids), + ('usage', '=', 'production') + ], ['company_id', 'ids:array_agg(id)'], ['company_id']) + location_by_company = {lbc['company_id'][0]: lbc['ids'] for lbc in location_by_company} + for production in self: + if production.product_id: + production.production_location_id = production.product_id.with_company(production.company_id).property_stock_production + else: + production.production_location_id = location_by_company.get(production.company_id.id)[0] + @api.depends('product_id.tracking') def _compute_show_lots(self): for production in self: @@ -367,21 +413,6 @@ class MrpProduction(models.Model): for production in self: production.finished_move_line_ids = production.move_finished_ids.mapped('move_line_ids') - @api.depends('bom_id.routing_id', 'bom_id.routing_id.operation_ids') - def _compute_routing(self): - for production in self: - if production.bom_id.routing_id.operation_ids: - production.routing_id = production.bom_id.routing_id.id - else: - production.routing_id = False - - @api.depends('workorder_ids') - def _compute_workorder_count(self): - data = self.env['mrp.workorder'].read_group([('production_id', 'in', self.ids)], ['production_id'], ['production_id']) - count_data = dict((item['production_id'][0], item['production_id_count']) for item in data) - for production in self: - production.workorder_count = count_data.get(production.id, 0) - @api.depends('workorder_ids.state') def _compute_workorder_done_count(self): data = self.env['mrp.workorder'].read_group([ @@ -391,12 +422,13 @@ class MrpProduction(models.Model): for production in self: production.workorder_done_count = count_data.get(production.id, 0) - @api.depends('move_raw_ids.state', 'move_finished_ids.state', 'workorder_ids', 'workorder_ids.state', 'qty_produced', 'move_raw_ids.quantity_done', 'product_qty') + @api.depends( + 'move_raw_ids.state', 'move_raw_ids.quantity_done', 'move_finished_ids.state', + 'workorder_ids', 'workorder_ids.state', 'product_qty', 'qty_producing') def _compute_state(self): """ Compute the production state. It use the same process than stock picking. It exists 3 extra steps for production: - - planned: Workorder has been launched (workorders only) - - progress: At least one item is produced. + - progress: At least one item is produced or consumed. - to_close: The quantity produced is greater than the quantity to produce and all work orders has been finished. """ @@ -408,23 +440,16 @@ class MrpProduction(models.Model): production.state = 'draft' elif all(move.state == 'cancel' for move in production.move_raw_ids): production.state = 'cancel' - elif all(move.state in ['cancel', 'done'] for move in production.move_raw_ids): - if ( - production.bom_id.consumption == 'flexible' - and float_compare(production.qty_produced, production.product_qty, precision_rounding=production.product_uom_id.rounding) == -1 - ): - production.state = 'progress' - else: - production.state = 'done' - elif production.move_finished_ids.filtered(lambda m: m.state not in ('cancel', 'done') and m.product_id.id == production.product_id.id)\ - and (production.qty_produced >= production.product_qty)\ - and (not production.routing_id or all(wo_state in ('cancel', 'done') for wo_state in production.workorder_ids.mapped('state'))): + elif all(move.state in ('cancel', 'done') for move in production.move_raw_ids): + production.state = 'done' + elif production.qty_producing >= production.product_qty: production.state = 'to_close' - elif production.workorder_ids and any(wo_state in ('progress') for wo_state in production.workorder_ids.mapped('state'))\ - or production.qty_produced > 0 and production.qty_produced < production.product_qty: + elif any(wo_state in ('progress', 'done') for wo_state in production.workorder_ids.mapped('state')): + production.state = 'progress' + elif not float_is_zero(production.qty_producing, precision_rounding=production.product_uom_id.rounding): + production.state = 'progress' + elif any(not float_is_zero(move.quantity_done, precision_rounding=move.product_uom.rounding) for move in production.move_raw_ids) : production.state = 'progress' - elif production.workorder_ids: - production.state = 'planned' else: production.state = 'confirmed' @@ -435,27 +460,23 @@ class MrpProduction(models.Model): if production.state not in ('draft', 'done', 'cancel'): relevant_move_state = production.move_raw_ids._get_relevant_state_among_moves() if relevant_move_state == 'partially_available': - if production.routing_id and production.routing_id.operation_ids and production.bom_id.ready_to_produce == 'asap': + if production.bom_id.operation_ids and production.bom_id.ready_to_produce == 'asap': production.reservation_state = production._get_ready_to_produce_state() else: production.reservation_state = 'confirmed' elif relevant_move_state != 'draft': production.reservation_state = relevant_move_state - @api.depends('move_raw_ids', 'is_locked', 'state', 'move_raw_ids.quantity_done') + @api.depends('move_raw_ids', 'state', 'move_raw_ids.product_uom_qty') def _compute_unreserve_visible(self): for order in self: - already_reserved = order.is_locked and order.state not in ('done', 'cancel') and order.mapped('move_raw_ids.move_line_ids') + already_reserved = order.state not in ('done', 'cancel') and order.mapped('move_raw_ids.move_line_ids') any_quantity_done = any([m.quantity_done > 0 for m in order.move_raw_ids]) + order.unreserve_visible = not any_quantity_done and already_reserved - order.reserve_visible = order.state in ('confirmed', 'planned') and any(move.state in ['confirmed', 'partially_available'] for move in order.move_raw_ids) + order.reserve_visible = (order.is_planned or order.state in ('confirmed', 'progress', 'to_close')) and any(move.state in ['confirmed', 'partially_available'] for move in order.move_raw_ids.filtered(lambda m: m.product_uom_qty)) - @api.depends('move_finished_ids.quantity_done', 'move_finished_ids.state', 'is_locked') - def _compute_post_visible(self): - for order in self: - order.post_visible = order.is_locked and any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_finished_ids) - - @api.depends('workorder_ids.state', 'move_finished_ids', 'move_finished_ids.quantity_done', 'is_locked') + @api.depends('workorder_ids.state', 'move_finished_ids', 'move_finished_ids.quantity_done') def _get_produced_qty(self): for production in self: done_moves = production.move_finished_ids.filtered(lambda x: x.state != 'cancel' and x.product_id.id == production.product_id.id) @@ -469,6 +490,20 @@ class MrpProduction(models.Model): for production in self: production.scrap_count = count_data.get(production.id, 0) + @api.depends('move_finished_ids') + def _compute_move_byproduct_ids(self): + for order in self: + order.move_byproduct_ids = order.move_finished_ids.filtered(lambda m: m.product_id != order.product_id) + + def _set_move_byproduct_ids(self): + move_finished_ids = self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id) + self.move_finished_ids = move_finished_ids | self.move_byproduct_ids + + @api.depends('state') + def _compute_show_lock(self): + for order in self: + order.show_lock = self.env.user.has_group('mrp.group_locked_by_default') and order.id is not False and order.state not in {'cancel', 'draft'} + _sql_constraints = [ ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'), ('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'), @@ -495,7 +530,7 @@ class MrpProduction(models.Model): """ Finds UoM of changed product. """ if not self.product_id: self.bom_id = False - else: + elif not self.bom_id or self.bom_id.product_tmpl_id != self.product_tmpl_id or (self.bom_id.product_id and self.bom_id.product_id != self.product_id): bom = self.env['mrp.bom']._bom_find(product=self.product_id, picking_type=self.picking_type_id, company_id=self.company_id.id, bom_type='normal') if bom: self.bom_id = bom.id @@ -505,6 +540,14 @@ class MrpProduction(models.Model): self.bom_id = False self.product_uom_id = self.product_id.uom_id.id + @api.onchange('product_qty', 'product_uom_id') + def _onchange_product_qty(self): + for workorder in self.workorder_ids: + workorder.product_uom_id = self.product_uom_id + workorder.duration_expected = workorder._get_duration_expected() + if workorder.date_planned_start and workorder.duration_expected: + workorder.date_planned_finished = workorder.date_planned_start + relativedelta(minutes=workorder.duration_expected) + @api.onchange('bom_id') def _onchange_bom_id(self): if not self.product_id and self.bom_id: @@ -516,8 +559,14 @@ class MrpProduction(models.Model): @api.onchange('date_planned_start') def _onchange_date_planned_start(self): - if not self.routing_id: - self.date_planned_finished = self.date_planned_start + datetime.timedelta(hours=1) + if not self.is_planned: + date_planned_finished = self.date_planned_start + relativedelta(days=self.product_id.produce_delay) + date_planned_finished = date_planned_finished + relativedelta(days=self.company_id.manufacturing_lead) + if date_planned_finished == self.date_planned_start: + date_planned_finished = date_planned_finished + relativedelta(hours=1) + self.date_planned_finished = date_planned_finished + self.move_raw_ids = [(1, m.id, {'date_expected': self.date_planned_start, 'date': self.date_planned_start}) for m in self.move_raw_ids] + self.move_finished_ids = [(1, m.id, {'date_expected': date_planned_finished, 'date': date_planned_finished}) for m in self.move_finished_ids] @api.onchange('bom_id', 'product_id', 'product_qty', 'product_uom_id') def _onchange_move_raw(self): @@ -537,7 +586,29 @@ class MrpProduction(models.Model): else: self.move_raw_ids = [(2, move.id) for move in self.move_raw_ids.filtered(lambda m: m.bom_line_id)] - @api.onchange('location_src_id', 'move_raw_ids', 'routing_id') + @api.onchange('bom_id', 'product_id', 'product_qty', 'product_uom_id') + def _onchange_move_finished(self): + if self.product_id and self.product_qty > 0: + # keep manual entries + list_move_finished = [(4, move.id) for move in self.move_finished_ids.filtered( + lambda m: not m.byproduct_id and m.product_id != self.product_id)] + moves_finished_values = self._get_moves_finished_values() + moves_byproduct_dict = {move.byproduct_id.id: move for move in self.move_finished_ids.filtered(lambda m: m.byproduct_id)} + move_finished = self.move_finished_ids.filtered(lambda m: m.product_id == self.product_id) + for move_finished_values in moves_finished_values: + if move_finished_values.get('byproduct_id') in moves_byproduct_dict: + # update existing entries + list_move_finished += [(1, moves_byproduct_dict[move_finished_values['byproduct_id']].id, move_finished_values)] + elif move_finished_values.get('product_id') == self.product_id.id and move_finished: + list_move_finished += [(1, move_finished.id, move_finished_values)] + else: + # add new entries + list_move_finished += [(0, 0, move_finished_values)] + self.move_finished_ids = list_move_finished + else: + self.move_finished_ids = [(2, move.id) for move in self.move_finished_ids.filtered(lambda m: m.bom_line_id)] + + @api.onchange('location_src_id', 'move_raw_ids', 'bom_id') def _onchange_location(self): source_location = self.location_src_id self.move_raw_ids.update({ @@ -545,6 +616,17 @@ class MrpProduction(models.Model): 'location_id': source_location.id, }) + @api.onchange('location_dest_id', 'move_finished_ids', 'bom_id') + def _onchange_location_dest(self): + destination_location = self.location_dest_id + update_value_list = [] + for move in self.move_finished_ids: + update_value_list += [(1, move.id, ({ + 'warehouse_id': destination_location.get_warehouse().id, + 'location_dest_id': destination_location.id, + }))] + self.move_finished_ids = update_value_list + @api.onchange('picking_type_id') def onchange_picking_type(self): location = self.env.ref('stock.stock_location_stock') @@ -553,21 +635,55 @@ class MrpProduction(models.Model): except (AttributeError, AccessError): location = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1).lot_stock_id self.move_raw_ids.update({'picking_type_id': self.picking_type_id}) + self.move_finished_ids.update({'picking_type_id': self.picking_type_id}) self.location_src_id = self.picking_type_id.default_location_src_id.id or location.id self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id + @api.onchange('qty_producing', 'lot_producing_id') + def _onchange_producing(self): + self._set_qty_producing() + + @api.onchange('bom_id') + def _onchange_workorder_ids(self): + if self.bom_id: + self._create_workorder() + def write(self, vals): + if 'workorder_ids' in self: + production_to_replan = self.filtered(lambda p: p.is_planned) res = super(MrpProduction, self).write(vals) for production in self: - if 'date_planned_start' in vals: + if 'date_planned_start' in vals and not self.env.context.get('force_date', False): if production.state in ['done', 'cancel']: raise UserError(_('You cannot move a manufacturing order once it is cancelled or done.')) - if production.workorder_ids and not self.env.context.get('force_date', False): - raise UserError(_('You cannot move a planned manufacturing order.')) - if 'move_raw_ids' in vals and production.state != 'draft': + if production.is_partially_planned: + raise UserError(_('You cannot move a manufacturing order once it has a planned workorder, move related workorder(s) instead.')) + if any(field in ['move_raw_ids', 'move_finished_ids', 'workorder_ids'] for field in vals) and production.state != 'draft': + if production.state == 'done': + # for some reason moves added after state = 'done' won't save group_id, reference if added in + # "stock_move.default_get()" + production.move_raw_ids.filtered(lambda move: move.additional and move.date_expected > production.date_planned_start).write({ + 'group_id': production.procurement_group_id.id, + 'reference': production.name, + 'date_expected': production.date_planned_start, + }) + production.move_finished_ids.filtered(lambda move: move.additional and move.date_expected > production.date_planned_finished).write({ + 'reference': production.name, + 'date_expected': production.date_planned_finished, + }) production._autoconfirm_production() - if not production.routing_id and vals.get('date_planned_start') and not vals.get('date_planned_finished'): + if production in production_to_replan: + production._plan_workorders(replan=True) + if production.state == 'done' and ('lot_producing_id' in vals or 'qty_producing' in vals): + finished_move_lines = production.move_finished_ids.filtered( + lambda move: move.product_id == self.product_id and move.state == 'done').mapped('move_line_ids') + if 'lot_producing_id' in vals: + finished_move_lines.write({'lot_id': vals.get('lot_producing_id')}) + if 'qty_producing' in vals: + finished_move_lines.write({'qty_done': vals.get('qty_producing')}) + + if not production.bom_id.operation_ids and vals.get('date_planned_start') and not vals.get('date_planned_finished'): new_date_planned_start = fields.Datetime.to_datetime(vals.get('date_planned_start')) if not production.date_planned_finished or new_date_planned_start >= production.date_planned_finished: production.date_planned_finished = new_date_planned_start + datetime.timedelta(hours=1) @@ -586,13 +702,13 @@ class MrpProduction(models.Model): procurement_group_vals = self._prepare_procurement_group_vals(values) values['procurement_group_id'] = self.env["procurement.group"].create(procurement_group_vals).id production = super(MrpProduction, self).create(values) - production.move_raw_ids.write({ + (production.move_raw_ids | production.move_finished_ids).write({ 'group_id': production.procurement_group_id.id, - 'reference': production.name, # set reference when MO name is different than 'New' }) # Trigger move_raw creation when importing a file if 'import_file' in self.env.context: production._onchange_move_raw() + production._onchange_move_finished() return production def unlink(self): @@ -614,7 +730,38 @@ class MrpProduction(models.Model): self.is_locked = not self.is_locked return True - def _get_finished_move_value(self, product_id, product_uom_qty, product_uom, operation_id=False, byproduct_id=False): + def _create_workorder(self): + for production in self: + if not production.bom_id: + continue + workorders_values = [] + + product_qty = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id) + exploded_boms, dummy = production.bom_id.explode(production.product_id, product_qty / production.bom_id.product_qty, picking_type=production.bom_id.picking_type_id) + + for bom, bom_data in exploded_boms: + # If the operations of the parent BoM and phantom BoM are the same, don't recreate work orders. + if not (bom.operation_ids and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.operation_ids != bom.operation_ids)): + continue + for operation in bom.operation_ids: + workorders_values += [{ + 'name': operation.name, + 'production_id': production.id, + 'workcenter_id': operation.workcenter_id.id, + 'product_uom_id': production.product_uom_id.id, + 'operation_id': operation.id, + 'state': 'pending', + 'consumption': production.consumption, + }] + production.workorder_ids = [(5, 0)] + [(0, 0, value) for value in workorders_values] + for workorder in production.workorder_ids: + workorder.duration_expected = workorder._get_duration_expected() + + def _get_move_finished_values(self, product_id, product_uom_qty, product_uom, operation_id=False, byproduct_id=False): + group_orders = self.procurement_group_id.mrp_production_ids + move_dest_ids = self.move_dest_ids + if len(group_orders) > 1: + move_dest_ids |= group_orders[0].move_finished_ids.filtered(lambda m: m.product_id == self.product_id).move_dest_ids date_planned_finished = self.date_planned_start + relativedelta(days=self.product_id.produce_delay) date_planned_finished = date_planned_finished + relativedelta(days=self.company_id.manufacturing_lead) if date_planned_finished == self.date_planned_start: @@ -626,7 +773,7 @@ class MrpProduction(models.Model): 'operation_id': operation_id, 'byproduct_id': byproduct_id, 'name': self.name, - 'date': self.date_planned_start, + 'date': date_planned_finished, 'date_expected': date_planned_finished, 'picking_type_id': self.picking_type_id.id, 'location_id': self.product_id.with_company(self.company_id).property_stock_production.id, @@ -640,21 +787,21 @@ class MrpProduction(models.Model): 'propagate_date': self.propagate_date, 'delay_alert': self.delay_alert, 'propagate_date_minimum_delta': self.propagate_date_minimum_delta, - 'move_dest_ids': [(4, x.id) for x in self.move_dest_ids], + 'move_dest_ids': [(4, x.id) for x in move_dest_ids], } - def _generate_finished_moves(self): - if self.product_id in self.bom_id.byproduct_ids.mapped('product_id'): - raise UserError(_("You cannot have %s as the finished product and in the Byproducts") % self.product_id.name) - moves_values = [self._get_finished_move_value(self.product_id.id, self.product_qty, self.product_uom_id.id)] - for byproduct in self.bom_id.byproduct_ids: - product_uom_factor = self.product_uom_id._compute_quantity(self.product_qty, self.bom_id.product_uom_id) - qty = byproduct.product_qty * (product_uom_factor / self.bom_id.product_qty) - move_values = self._get_finished_move_value(byproduct.product_id.id, - qty, byproduct.product_uom_id.id, byproduct.operation_id.id, - byproduct.id) - moves_values.append(move_values) - moves = self.env['stock.move'].create(moves_values) + def _get_moves_finished_values(self): + moves = [] + for production in self: + if production.product_id in production.bom_id.byproduct_ids.mapped('product_id'): + raise UserError(_("You cannot have %s as the finished product and in the Byproducts") % self.product_id.name) + moves = [production._get_move_finished_values(production.product_id.id, production.product_qty, production.product_uom_id.id)] + for byproduct in production.bom_id.byproduct_ids: + product_uom_factor = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id) + qty = byproduct.product_qty * (product_uom_factor / production.bom_id.product_qty) + moves.append(production._get_move_finished_values( + byproduct.product_id.id, qty, byproduct.product_uom_id.id, + byproduct.operation_id.id, byproduct.id)) return moves def _get_moves_raw_values(self): @@ -681,7 +828,6 @@ class MrpProduction(models.Model): data = { 'sequence': bom_line.sequence if bom_line else 10, 'name': self.name, - 'reference': self.name, 'date': self.date_planned_start, 'date_expected': self.date_planned_start, 'bom_line_id': bom_line.id if bom_line else False, @@ -707,6 +853,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'] @@ -736,16 +895,33 @@ class MrpProduction(models.Model): move = self.env['stock.move'].create(move_values) return move, 0, quantity + def _update_raw_moves(self, factor): + self.ensure_one() + update_info = [] + move_to_unlink = self.env['stock.move'] + for move in self.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel')): + old_qty = move.product_uom_qty + new_qty = old_qty * factor + if new_qty > 0: + move.write({'product_uom_qty': new_qty}) + move._action_assign() + update_info.append((move, old_qty, new_qty)) + else: + if move.quantity_done > 0: + raise UserError(_('Lines need to be deleted, but can not as you still have some quantities to consume in them. ')) + move._action_cancel() + move_to_unlink |= move + move_to_unlink.unlink() + return update_info + def _get_ready_to_produce_state(self): """ returns 'assigned' if enough components are reserved in order to complete - the first operation in the routing. If not returns 'waiting' + the first operation of the bom. If not returns 'waiting' """ self.ensure_one() - first_operation = self.routing_id.operation_ids[0] - # Get BoM line related to first opeation in rounting. If there is only - # one opeation in the routing then it will need all BoM lines. + first_operation = self.bom_id.operation_ids[0] bom_line_ids = self.env['mrp.bom.line'] - if len(self.routing_id.operation_ids) == 1: + if len(self.bom_id.operation_ids) == 1: moves_in_first_operation = self.move_raw_ids else: moves_in_first_operation = self.move_raw_ids.filtered(lambda move: move.operation_id == first_operation) @@ -773,19 +949,22 @@ class MrpProduction(models.Model): ) additional_moves.write({ 'group_id': production.procurement_group_id.id, - 'reference': production.name, # set reference when MO name is different than 'New' }) additional_moves._adjust_procure_method() moves_to_confirm |= additional_moves - moves_to_confirm |= production.move_finished_ids.filtered( + additional_byproducts = production.move_finished_ids.filtered( lambda move: move.state == 'draft' and move.additional ) + moves_to_confirm |= additional_byproducts + if moves_to_confirm: moves_to_confirm._action_confirm() + self.workorder_ids.filtered(lambda w: w.state not in ['done', 'cancel'])._action_confirm() + def action_view_mrp_production_childs(self): self.ensure_one() - mrp_production_ids = self.procurement_group_id.stock_move_ids.created_production_id.ids + mrp_production_ids = self.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids.ids action = { 'res_model': 'mrp.production', 'type': 'ir.actions.act_window', @@ -797,7 +976,7 @@ class MrpProduction(models.Model): }) else: action.update({ - 'name': _("MO Source of %s" % self.name), + 'name': _("%s Child MO's") % self.name, 'domain': [('id', 'in', mrp_production_ids)], 'view_mode': 'tree,form', }) @@ -805,7 +984,7 @@ class MrpProduction(models.Model): def action_view_mrp_production_sources(self): self.ensure_one() - mrp_production_ids = self.move_dest_ids.group_id.mrp_production_id.ids + mrp_production_ids = self.procurement_group_id.mrp_production_ids.move_dest_ids.group_id.mrp_production_ids.ids action = { 'res_model': 'mrp.production', 'type': 'ir.actions.act_window', @@ -817,52 +996,60 @@ class MrpProduction(models.Model): }) else: action.update({ - 'name': _("MO Generated by %s" % self.name), + 'name': _("MO Generated by %s") % self.name, 'domain': [('id', 'in', mrp_production_ids)], 'view_mode': 'tree,form', }) return action + def action_view_mrp_production_backorders(self): + backorder_ids = self.procurement_group_id.mrp_production_ids.ids + return { + 'res_model': 'mrp.production', + 'type': 'ir.actions.act_window', + 'name': _("Backorder MO's"), + 'domain': [('id', 'in', backorder_ids)], + 'view_mode': 'tree,form', + } + + def action_generate_serial(self): + self.ensure_one() + self.lot_producing_id = self.env['stock.production.lot'].create({ + 'product_id': self.product_id.id, + 'company_id': self.company_id.id + }) + 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': + self._set_qty_producing() + def action_confirm(self): self._check_company() for production in self: - if not production.bom_id: - production.consumption = 'flexible' - else: + if production.bom_id: production.consumption = production.bom_id.consumption if not production.move_raw_ids: raise UserError(_("Add some materials to consume before marking this MO as to do.")) - production._generate_finished_moves() production.move_raw_ids._adjust_procure_method() (production.move_raw_ids | production.move_finished_ids)._action_confirm() + production.workorder_ids._action_confirm() return True def action_assign(self): for production in self: production.move_raw_ids._action_assign() - production.workorder_ids._refresh_wo_lines() return True - def open_produce_product(self): - self.ensure_one() - if self.bom_id.type == 'phantom': - raise UserError(_('You cannot produce a MO with a bom kit product.')) - action = self.env.ref('mrp.act_mrp_product_produce').read()[0] - return action - def button_plan(self): """ Create work orders. And probably do stuff, like things. """ - orders_to_plan = self.filtered(lambda order: order.routing_id and order.state == 'confirmed') + orders_to_plan = self.filtered(lambda order: not order.is_planned) for order in orders_to_plan: - order.move_raw_ids.filtered(lambda m: m.state == 'draft')._action_confirm() + (order.move_raw_ids | order.move_finished_ids).filtered(lambda m: m.state == 'draft')._action_confirm() # `propagate_date` enables the automatic rescheduling which could lead to hard to # understand behavior if a manufacturing order is planned, i.e. if the work orders do # have their leaves booked in the workcenter calendar. We thus disable the # automatic rescheduling in this scenario. order.move_raw_ids.write({'propagate_date': False}) - quantity = order.product_uom_id._compute_quantity(order.product_qty, order.bom_id.product_uom_id) / order.bom_id.product_qty - boms, lines = order.bom_id.explode(order.product_id, quantity, picking_type=order.bom_id.picking_type_id) - order._generate_workorders(boms) order._plan_workorders() return True @@ -878,15 +1065,14 @@ class MrpProduction(models.Model): # Schedule all work orders (new ones and those already created) qty_to_produce = max(self.product_qty - self.qty_produced, 0) qty_to_produce = self.product_uom_id._compute_quantity(qty_to_produce, self.product_id.uom_id) - start_date = self.date_planned_start + start_date = max(self.date_planned_start, datetime.datetime.now()) if replan: workorder_ids = self.workorder_ids.filtered(lambda wo: wo.state in ['ready', 'pending']) # We plan the manufacturing order according to its `date_planned_start`, but if # `date_planned_start` is in the past, we plan it as soon as possible. - start_date = max(start_date, datetime.datetime.now()) workorder_ids.leave_id.unlink() else: - workorder_ids = self.workorder_ids + workorder_ids = self.workorder_ids.filtered(lambda wo: not wo.date_planned_start) for workorder in workorder_ids: workcenters = workorder.workcenter_id | workorder.workcenter_id.alternative_workcenter_ids @@ -894,9 +1080,10 @@ class MrpProduction(models.Model): vals = {} for workcenter in workcenters: # compute theoretical duration - time_cycle = workorder.operation_id.time_cycle - cycle_number = float_round(qty_to_produce / workcenter.capacity, precision_digits=0, rounding_method='UP') - duration_expected = workcenter.time_start + workcenter.time_stop + cycle_number * time_cycle * 100.0 / workcenter.time_efficiency + if workorder.workcenter_id == workcenter: + duration_expected = workorder.duration_expected + else: + duration_expected = workorder._get_duration_expected(alternative_workcenter=workcenter) from_date, to_date = workcenter._get_first_available_slot(start_date, duration_expected) # If the workcenter is unavailable, try planning on the next one @@ -927,7 +1114,7 @@ class MrpProduction(models.Model): # Create leave on chosen workcenter calendar leave = self.env['resource.calendar.leaves'].create({ - 'name': self.name + ' - ' + workorder.name, + 'name': workorder.display_name, 'calendar_id': best_workcenter.resource_calendar_id.id, 'date_from': best_start_date, 'date_to': best_finished_date, @@ -946,92 +1133,89 @@ class MrpProduction(models.Model): raise UserError(_("Some work orders are already done, you cannot unplan this manufacturing order.")) elif any(wo.state == 'progress' for wo in self.workorder_ids): raise UserError(_("Some work orders have already started, you cannot unplan this manufacturing order.")) - self.workorder_ids.unlink() - def _generate_workorders(self, exploded_boms): - workorders = self.env['mrp.workorder'] - original_one = False - for bom, bom_data in exploded_boms: - # If the routing of the parent BoM and phantom BoM are the same, don't recreate work orders, but use one master routing - if bom.routing_id.id and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.routing_id.id != bom.routing_id.id): - temp_workorders = self._workorders_create(bom, bom_data) - workorders += temp_workorders - if temp_workorders: # In order to avoid two "ending work orders" - if original_one: - temp_workorders[-1].next_work_order_id = original_one - original_one = temp_workorders[0] - return workorders + self.workorder_ids.leave_id.unlink() + self.workorder_ids.write({ + 'date_planned_start': False, + 'date_planned_finished': False, + }) - def _workorders_create(self, bom, bom_data): + def _get_consumption_issues(self): + """Compare the quantity consumed of the components, the expected quantity + on the BoM and the consumption parameter on the order. + + :return: list of tuples (order_id, product_id, consumed_qty, expected_qty) where the + consumption isn't honored. order_id and product_id are recordset of mrp.production + and product.product respectively + :rtype: list """ - :param bom: in case of recursive boms: we could create work orders for child - BoMs - """ - workorders = self.env['mrp.workorder'] + issues = [] + if self.env.context.get('skip_consumption', False): + return issues + for order in self: + if order.consumption == 'flexible' or not order.bom_id or not order.bom_id.bom_line_ids: + continue + expected_move_values = order._get_moves_raw_values() + expected_qty_by_product = defaultdict(float) + for move_values in expected_move_values: + move_product = self.env['product.product'].browse(move_values['product_id']) + move_uom = self.env['uom.uom'].browse(move_values['product_uom']) + move_product_qty = move_uom._compute_quantity(move_values['product_uom_qty'], move_product.uom_id) + expected_qty_by_product[move_product] += move_product_qty * order.qty_producing / order.product_qty - # Initial qty producing - quantity = max(self.product_qty - sum(self.move_finished_ids.filtered(lambda move: move.product_id == self.product_id).mapped('quantity_done')), 0) - quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom_id) - if self.product_id.tracking == 'serial': - quantity = 1.0 + done_qty_by_product = defaultdict(float) + for move in order.move_raw_ids: + qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id) + if move.product_id not in expected_qty_by_product: + issues.append((order, move.product_id, qty_done, 0.0)) + continue + done_qty_by_product[move.product_id] += qty_done - for operation in bom.routing_id.operation_ids: - workorder = workorders.create({ - 'name': operation.name, - 'production_id': self.id, - 'workcenter_id': operation.workcenter_id.id, - 'product_uom_id': self.product_id.uom_id.id, - 'operation_id': operation.id, - 'state': len(workorders) == 0 and 'ready' or 'pending', - 'qty_producing': quantity, - 'consumption': self.bom_id.consumption, - }) - if workorders: - workorders[-1].next_work_order_id = workorder.id - workorders[-1]._start_nextworkorder() - workorders += workorder + for product, qty_to_consume in expected_qty_by_product.items(): + qty_done = done_qty_by_product.get(product, 0.0) + if float_compare(qty_to_consume, qty_done, precision_rounding=product.uom_id.rounding) != 0: + issues.append((order, product, qty_done, qty_to_consume)) - # get the raw moves to attach to this operation - moves_raw = self.env['stock.move'] - for move in self.move_raw_ids: - if move.operation_id == operation and move.bom_line_id.bom_id.routing_id == bom.routing_id: - moves_raw |= move - if move.operation_id == operation and not move.bom_line_id: - moves_raw |= move - moves_finished = self.move_finished_ids.filtered(lambda move: move.operation_id == operation) + return issues - # - Raw moves from a BoM where a routing was set but no operation was precised should - # be consumed at the last workorder of the linked routing. - # - Raw moves from a BoM where no rounting was set should be consumed at the last - # workorder of the main routing. - if len(workorders) == len(bom.routing_id.operation_ids): - moves_raw |= self.move_raw_ids.filtered(lambda move: not move.operation_id and move.bom_line_id.bom_id.routing_id == bom.routing_id) - moves_raw |= self.move_raw_ids.filtered(lambda move: not move.workorder_id and not move.bom_line_id.bom_id.routing_id) + def _action_generate_consumption_wizard(self, consumption_issues): + ctx = self.env.context.copy() + lines = [] + for order, product_id, consumed_qty, expected_qty in consumption_issues: + lines.append((0, 0, { + 'mrp_production_id': order.id, + 'product_id': product_id.id, + 'consumption': order.consumption, + 'product_uom_id': product_id.uom_id.id, + 'product_consumed_qty_uom': consumed_qty, + 'product_expected_qty_uom': expected_qty + })) + ctx.update({'default_mrp_production_ids': self.ids, 'default_mrp_consumption_warning_line_ids': lines}) + action = self.env.ref('mrp.action_mrp_consumption_warning').read()[0] + action['context'] = ctx + return action - moves_finished |= self.move_finished_ids.filtered(lambda move: move.product_id != self.product_id and not move.operation_id) + def _get_quantity_produced_issues(self): + quantity_issues = [] + if self.env.context.get('skip_backorder', False): + return quantity_issues + for order in self: + if not float_is_zero(order._get_quantity_to_backorder(), precision_rounding=order.product_uom_id.rounding): + quantity_issues.append(order) + return quantity_issues - moves_raw.mapped('move_line_ids').write({'workorder_id': workorder.id}) - (moves_finished | moves_raw).write({'workorder_id': workorder.id}) - - workorder._generate_wo_lines() - return workorders - - def _check_lots(self): - # Check that the components were consumed for lots that we have produced. - if self.product_id.tracking != 'none': - finished_lots = self.finished_move_line_ids.mapped('lot_id') - raw_finished_lots = self.move_raw_ids.mapped('move_line_ids.lot_produced_ids') - if (raw_finished_lots - finished_lots): - lots_short = raw_finished_lots - finished_lots - error_msg = _( - 'Some components have been consumed for a lot/serial number that has not been produced. ' - 'Unlock the MO and click on the components lines to correct it.\n' - 'List of the components:\n' - ) - move_lines = self.move_raw_ids.mapped('move_line_ids').filtered(lambda ml: lots_short & ml.lot_produced_ids) - for ml in move_lines: - error_msg += ml.product_id.display_name + ' (' + ', '.join((lots_short & ml.lot_produced_ids).mapped('name')) + ')\n' - raise UserError(error_msg) + def _action_generate_backorder_wizard(self, quantity_issues): + ctx = self.env.context.copy() + lines = [] + for order in quantity_issues: + lines.append((0, 0, { + 'mrp_production_id': order.id, + 'to_backorder': True + })) + ctx.update({'default_mrp_production_ids': self.ids, 'default_mrp_production_backorder_line_ids': lines}) + action = self.env.ref('mrp.action_mrp_production_backorder').read()[0] + action['context'] = ctx + return action def action_cancel(self): """ Cancels production order, unfinished stock moves and set procurement @@ -1083,7 +1267,7 @@ class MrpProduction(models.Model): self.ensure_one() return True - def post_inventory(self): + def _post_inventory(self, cancel_backorder=False): for order in self: moves_not_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done') moves_to_do = order.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) @@ -1093,42 +1277,178 @@ class MrpProduction(models.Model): # to get extra moves. moves_to_do = moves_to_do._action_done() moves_to_do = order.move_raw_ids.filtered(lambda x: x.state == 'done') - moves_not_to_do + + finish_moves = order.move_finished_ids.filtered(lambda m: m.product_id == order.product_id and m.state not in ('done', 'cancel')) + # the finish move can already be completed by the workorder. + if not finish_moves.quantity_done: + if order.product_tracking == 'serial': + uom = order.product_id.uom_id + finish_moves.quantity_done = order.product_uom_id._compute_quantity(order.qty_producing, uom, round='HALF-UP') + finish_moves.move_line_ids.product_uom_id = uom + else: + finish_moves.quantity_done = float_round(order.qty_producing - order.qty_produced, precision_rounding=order.product_uom_id.rounding, rounding_method='HALF-UP') + finish_moves.move_line_ids.lot_id = order.lot_producing_id order._cal_price(moves_to_do) + moves_to_finish = order.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel')) - moves_to_finish = moves_to_finish._action_done() - order.workorder_ids.mapped('raw_workorder_line_ids').unlink() - order.workorder_ids.mapped('finished_workorder_line_ids').unlink() + moves_to_finish = moves_to_finish._action_done(cancel_backorder=cancel_backorder) order.action_assign() consume_move_lines = moves_to_do.mapped('move_line_ids') - for moveline in moves_to_finish.mapped('move_line_ids'): - if moveline.move_id.has_tracking != 'none' and moveline.product_id == order.product_id or moveline.lot_id in consume_move_lines.mapped('lot_produced_ids'): - if any([not ml.lot_produced_ids for ml in consume_move_lines]): - raise UserError(_('You can not consume without telling for which lot you consumed it')) - # Link all movelines in the consumed with same lot_produced_ids false or the correct lot_produced_ids - filtered_lines = consume_move_lines.filtered(lambda ml: moveline.lot_id in ml.lot_produced_ids) - moveline.write({'consume_line_ids': [(6, 0, [x for x in filtered_lines.ids])]}) - else: - # Link with everything - moveline.write({'consume_line_ids': [(6, 0, [x for x in consume_move_lines.ids])]}) + order.move_finished_ids.move_line_ids.consume_line_ids = [(6, 0, consume_move_lines.ids)] return True - def button_mark_done(self): - self.ensure_one() - self._check_company() - for wo in self.workorder_ids: - if wo.time_ids.filtered(lambda x: (not x.date_end) and (x.loss_type in ('productive', 'performance'))): - raise UserError(_('Work order %s is still running') % wo.name) - self._check_lots() + @api.model + def _get_name_backorder(self, name, sequence): + if not sequence: + return name + seq_back = "-" + "0" * (SIZE_BACK_ORDER_NUMERING - 1 - int(math.log10(sequence))) + str(sequence) + if re.search("-\\d{%d}$" % SIZE_BACK_ORDER_NUMERING, name): + return name[:-SIZE_BACK_ORDER_NUMERING-1] + seq_back + return name + seq_back + + def _get_backorder_mo_vals(self, mo_source): + next_seq = max(mo_source.procurement_group_id.mrp_production_ids.mapped("backorder_sequence")) + return { + 'name': self._get_name_backorder(mo_source.name, next_seq + 1), + 'backorder_sequence': next_seq + 1, + 'procurement_group_id': mo_source.procurement_group_id.id, + 'move_raw_ids': None, + 'move_finished_ids': None, + 'product_qty': self._get_quantity_to_backorder(), + 'lot_producing_id': False, + 'origin': mo_source.origin + } + + 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 + production.backorder_sequence = 1 + backorder_mo = production.copy(default=self._get_backorder_mo_vals(production)) + 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.product_uom_qty - 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 + (move | new_move)._do_unreserve() + (move | new_move)._action_assign() + backorders |= backorder_mo + for wo in backorder_mo.workorder_ids: + wo.qty_produced = 0 + if wo.product_tracking == 'serial': + wo.qty_producing = 1 + else: + wo.qty_producing = wo.qty_remaining + + production.name = self._get_name_backorder(production.name, production.backorder_sequence) + + # We need to adapt `duration_expected` on both the original workorders and their + # backordered workorders. To do that, we use the original `duration_expected` and the + # ratio of the quantity really produced and the quantity to produce. + ratio = production.qty_producing / production.product_qty + for workorder in production.workorder_ids: + workorder.duration_expected = workorder.duration_expected * ratio + for workorder in backorder_mo.workorder_ids: + workorder.duration_expected = workorder.duration_expected * (1 - ratio) + backorders.action_confirm() + for wo in backorders.workorder_ids: + if wo.component_id: + wo._update_component_quantity() + # Remove the serial move line without reserved quantity. Post inventory will assigned all the non done moves + # So those move lines are duplicated. + backorders.move_raw_ids.move_line_ids.filtered(lambda ml: ml.product_id.tracking == 'serial' and ml.product_qty == 0).unlink() + backorders.move_raw_ids._recompute_state() + + return backorders + + def button_mark_done(self): + self._check_company() + for order in self: + # TODO : multi _check_sn_uniqueness + error message with MO name + order._check_sn_uniqueness() + + res = self._pre_button_mark_done() + if res is not True: + return res + + if self.env.context.get('mo_ids_to_backorder'): + productions_to_backorder = self.browse(self.env.context['mo_ids_to_backorder']) + productions_not_to_backorder = self - productions_to_backorder + else: + productions_not_to_backorder = self + productions_to_backorder = self.env['mrp.production'] + + self.workorder_ids.button_finish() + + productions_not_to_backorder._post_inventory(cancel_backorder=True) + productions_to_backorder._post_inventory(cancel_backorder=False) + backorders = productions_to_backorder._generate_backorder_productions() - self.post_inventory() # Moves without quantity done are not posted => set them as done instead of canceling. In # case the user edits the MO later on and sets some consumed quantity on those, we do not # want the move lines to be canceled. - (self.move_raw_ids | self.move_finished_ids).filtered(lambda x: x.state not in ('done', 'cancel')).write({ + (productions_not_to_backorder.move_raw_ids | productions_not_to_backorder.move_finished_ids).filtered(lambda x: x.state not in ('done', 'cancel')).write({ 'state': 'done', 'product_uom_qty': 0.0, }) - return self.write({'date_finished': fields.Datetime.now()}) + + for production in self: + production.write({'date_finished': fields.Datetime.now(), 'product_qty': production.qty_produced}) + + for workorder in self.workorder_ids.filtered(lambda w: w.state not in ('done', 'cancel')): + workorder.duration_expected = workorder._get_duration_expected() + + if not backorders: + if self.env.context.get('from_workorder'): + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mrp.production', + 'views': [[self.env.ref('mrp.mrp_production_form_view').id, 'form']], + 'res_id': self.id, + 'target': 'main', + } + return True + context = self.env.context.copy() + context = {k: v for k, v in context.items() if not k.startswith('default_')} + for k, v in context.items(): + if k.startswith('skip_'): + context[k] = False + action = { + 'res_model': 'mrp.production', + 'type': 'ir.actions.act_window', + 'context': dict(context, mo_ids_to_backorder=None) + } + if len(backorders) == 1: + action.update({ + 'view_mode': 'form', + 'res_id': backorders[0].id, + }) + else: + action.update({ + 'name': _("Backorder MO"), + 'domain': [('id', 'in', backorders.ids)], + 'view_mode': 'tree,form', + }) + return action + + def _pre_button_mark_done(self): + consumption_issues = self._get_consumption_issues() + if consumption_issues: + return self._action_generate_consumption_wizard(consumption_issues) + + quantity_issues = self._get_quantity_produced_issues() + if quantity_issues: + return self._action_generate_backorder_wizard(quantity_issues) + return True def do_unreserve(self): for production in self: @@ -1228,7 +1548,7 @@ class MrpProduction(models.Model): 'res_model': 'mrp.unbuild', 'view_id': self.env.ref('mrp.mrp_unbuild_form_view_simplified').id, 'type': 'ir.actions.act_window', - 'context': {'default_mo_id': self.id, + 'context': {'default_mo_id': self.id, 'default_company_id': self.company_id.id, 'default_location_id': self.location_dest_id.id, 'default_location_dest_id': self.location_src_id.id, @@ -1239,3 +1559,81 @@ class MrpProduction(models.Model): @api.model def _prepare_procurement_group_vals(self, values): return {'name': values['name']} + + def _get_quantity_to_backorder(self): + self.ensure_one() + return max(self.product_qty - self.qty_producing, 0) + + def _check_sn_uniqueness(self): + """ Alert the user if the serial number as already been consumed/produced """ + if self.product_tracking == 'serial' and self.lot_producing_id: + sml = self.env['stock.move.line'].search_count([ + ('lot_id', '=', self.lot_producing_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) + + for move in self.move_finished_ids: + if move.has_tracking != 'serial' or move.product_id == self.product_id: + continue + for move_line in move.move_line_ids: + domain = [ + ('lot_id', '=', move_line.lot_id.id), + ('qty_done', '=', 1), + ('state', '=', 'done') + ] + message = _('The serial number %s used for byproduct %s has already been produced') % (move_line.lot_id.name, move_line.product_id.name) + co_prod_move_lines = self.move_finished_ids.move_line_ids.filtered(lambda ml: ml.product_id != self.product_id) + domain_unbuild = domain + [ + ('production_id', '=', False), + ('location_dest_id.usage', '=', 'production') + ] + + # Check presence of same sn in previous productions + duplicates = self.env['stock.move.line'].search_count(domain + [ + ('location_id.usage', '=', 'production') + ]) + if duplicates: + # Maybe some move lines have been compensated by unbuild + duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild) + if not (duplicates_unbuild and duplicates - duplicates_unbuild == 0): + raise UserError(message) + # Check presence of same sn in current production + duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == move_line.lot_id) - move_line + if duplicates: + raise UserError(message) + + for move in self.move_raw_ids: + if move.has_tracking != 'serial': + continue + for move_line in move.move_line_ids: + if float_is_zero(move_line.qty_done, precision_rounding=move_line.product_uom_id.rounding): + continue + domain = [ + ('lot_id', '=', move_line.lot_id.id), + ('qty_done', '=', 1), + ('state', '=', 'done') + ] + message = _('The serial number %s used for component %s has already been consumed') % (move_line.lot_id.name, move_line.product_id.name) + co_prod_move_lines = self.move_raw_ids.move_line_ids + domain_unbuild = domain + [ + ('production_id', '=', False), + ('location_id.usage', '=', 'production') + ] + + # Check presence of same sn in previous productions + duplicates = self.env['stock.move.line'].search_count(domain + [ + ('location_dest_id.usage', '=', 'production') + ]) + if duplicates: + # Maybe some move lines have been compensated by unbuild + duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild) + if not (duplicates_unbuild and duplicates - duplicates_unbuild == 0): + raise UserError(message) + # Check presence of same sn in current production + duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == move_line.lot_id) - move_line + if duplicates: + raise UserError(message) diff --git a/addons/mrp/models/mrp_routing.py b/addons/mrp/models/mrp_routing.py index 7b8f8bfd06b..1a1a33a4fcd 100644 --- a/addons/mrp/models/mrp_routing.py +++ b/addons/mrp/models/mrp_routing.py @@ -4,32 +4,6 @@ from odoo import api, fields, models, _ -class MrpRouting(models.Model): - """ Specifies routings of work centers """ - _name = 'mrp.routing' - _description = 'Routings' - - name = fields.Char('Routing', required=True) - active = fields.Boolean( - 'Active', default=True, - help="If the active field is set to False, it will allow you to hide the routing without removing it.") - code = fields.Char( - 'Reference', - copy=False, default=lambda self: _('New'), readonly=True) - note = fields.Text('Description') - operation_ids = fields.One2many( - 'mrp.routing.workcenter', 'routing_id', 'Operations', - copy=True) - company_id = fields.Many2one( - 'res.company', 'Company', default=lambda self: self.env.company) - - @api.model - def create(self, vals): - if 'code' not in vals or vals['code'] == _('New'): - vals['code'] = self.env['ir.sequence'].next_by_code('mrp.routing') or _('New') - return super(MrpRouting, self).create(vals) - - class MrpRoutingWorkcenter(models.Model): _name = 'mrp.routing.workcenter' _description = 'Work Center Usage' @@ -41,14 +15,14 @@ class MrpRoutingWorkcenter(models.Model): sequence = fields.Integer( 'Sequence', default=100, help="Gives the sequence order when displaying a list of routing Work Centers.") - routing_id = fields.Many2one( - 'mrp.routing', 'Parent Routing', - index=True, ondelete='cascade', required=True, - help="The routing contains all the Work Centers used and for how long. This will create work orders afterwards " - "which alters the execution of the manufacturing order.") + bom_id = fields.Many2one( + 'mrp.bom', 'Bill of Material', + index=True, ondelete='cascade', + help="The Bill of Material this operation is linked to") company_id = fields.Many2one( 'res.company', 'Company', - readonly=True, related='routing_id.company_id', store=True) + readonly=True, store=True, + default=lambda self: self.env.company) worksheet_type = fields.Selection([ ('pdf', 'PDF'), ('google_slide', 'Google Slide'), ('text', 'Text')], string="Work Sheet", default="pdf", diff --git a/addons/mrp/models/mrp_unbuild.py b/addons/mrp/models/mrp_unbuild.py index 40856e8c141..6d345e75763 100644 --- a/addons/mrp/models/mrp_unbuild.py +++ b/addons/mrp/models/mrp_unbuild.py @@ -184,7 +184,7 @@ class MrpUnbuild(models.Model): needed_quantity = move.product_qty moves_lines = original_move.mapped('move_line_ids') if move in produce_moves and self.lot_id: - moves_lines = moves_lines.filtered(lambda ml: self.lot_id in ml.lot_produced_ids) + moves_lines = moves_lines.filtered(lambda ml: self.lot_id in ml.produce_line_ids.lot_id) # FIXME sle: double check with arm for move_line in moves_lines: # Iterate over all move_lines until we unbuilded the correct quantity. taken_quantity = min(needed_quantity, move_line.qty_done) diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index 444976ebaed..0c1407c7a44 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,17 +25,18 @@ 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)]}, + states={'done': [('readonly', True)], 'cancel': [('readonly', True)], 'progress': [('readonly', True)]}, group_expand='_read_group_workcenter_id', check_company=True) 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, @@ -44,12 +45,18 @@ class MrpWorkorder(models.Model): string='Production State', readonly=True, related='production_id.state', help='Technical: used in views only.') + production_bom_id = fields.Many2one('mrp.bom', related='production_id.bom_id') 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, readonly=True, digits='Product Unit of Measure', + copy=False, help="The number of products already handled by this work order") is_produced = fields.Boolean(string="Has Been Produced", compute='_compute_is_produced') @@ -59,34 +66,32 @@ class MrpWorkorder(models.Model): ('progress', 'In Progress'), ('done', 'Finished'), ('cancel', 'Cancelled')], string='Status', - default='pending') + default='pending', copy=False, readonly=True) leave_id = fields.Many2one( 'resource.calendar.leaves', help='Slot into workcenter calendar once planned', - check_company=True) + check_company=True, copy=False) date_planned_start = fields.Datetime( - 'Scheduled Date Start', + 'Scheduled Start Date', compute='_compute_dates_planned', inverse='_set_dates_planned', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, - store=True, - tracking=True) + store=True, tracking=True, copy=False) date_planned_finished = fields.Datetime( - 'Scheduled Date Finished', + 'Scheduled End Date', compute='_compute_dates_planned', inverse='_set_dates_planned', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, - store=True, - tracking=True) + store=True, tracking=True, copy=False) date_start = fields.Datetime( - 'Effective Start Date', + 'Start Date', copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) date_finished = fields.Datetime( - 'Effective End Date', + 'End Date', copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) duration_expected = fields.Float( - 'Expected Duration', digits=(16, 2), + 'Expected Duration', digits=(16, 2), default=60.0, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Expected duration (in minutes)") duration = fields.Float( @@ -101,8 +106,7 @@ class MrpWorkorder(models.Model): progress = fields.Float('Progress Done (%)', digits=(16, 2), compute='_compute_progress') operation_id = fields.Many2one( - 'mrp.routing.workcenter', 'Operation', - check_company=True) + 'mrp.routing.workcenter', 'Operation', check_company=True) # Should be used differently as BoM can change in the meantime worksheet = fields.Binary( 'Worksheet', related='operation_id.worksheet', readonly=True) @@ -121,10 +125,11 @@ 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') + 'mrp.workcenter.productivity', 'workorder_id', copy=False) is_user_working = fields.Boolean( 'Is the Current User Working', compute='_compute_working_users', help="Technical field indicating whether the current user is working. ") @@ -135,15 +140,16 @@ 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): previous_wo_data = self.env['mrp.workorder'].read_group( [('next_work_order_id', 'in', self.ids)], @@ -154,9 +160,14 @@ class MrpWorkorder(models.Model): 'date_planned_start': x['date_planned_start'], 'date_planned_finished': x['date_planned_finished']}) for x in previous_wo_data]) - conflicted_dict = self._get_conflicted_workorder_ids() + if self.ids: + conflicted_dict = self._get_conflicted_workorder_ids() for wo in self: infos = [] + if not wo.date_planned_start or not wo.date_planned_finished or not wo.ids: + wo.show_json_popover = False + wo.json_popover = False + continue if wo.state in ['pending', 'ready']: previous_wo = previous_wo_dict.get(wo.id) prev_start = previous_wo and previous_wo['date_planned_start'] or False @@ -194,6 +205,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 @@ -214,76 +244,6 @@ class MrpWorkorder(models.Model): 'date_to': date_to, }) - @api.depends('state') - def _compute_is_finished_lines_editable(self): - for workorder in self: - if self.user_has_groups('mrp.group_mrp_byproducts') and workorder.state not in ('cancel', 'done'): - workorder.is_finished_lines_editable = True - else: - workorder.is_finished_lines_editable = False - - @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: - diff = self.date_planned_finished - self.date_planned_start - self.duration_expected = diff.total_seconds() / 60 - - @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.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: @@ -297,12 +257,17 @@ class MrpWorkorder(models.Model): # Removes references to workorder to avoid Validation Error (self.mapped('move_raw_ids') | self.mapped('move_finished_ids')).write({'workorder_id': False}) self.mapped('leave_id').unlink() - return super(MrpWorkorder, self).unlink() + mo_dirty = self.production_id.filtered(lambda mo: mo.state in ("confirmed", "progress", "to_close")) + res = super().unlink() + # We need to go through `_action_confirm` for all workorders of the current productions to + # make sure the links between them are correct (`next_work_order_id` could be obsolete now). + mo_dirty.workorder_ids._action_confirm() + return res - @api.depends('production_id.product_qty', 'qty_produced') + @api.depends('production_id.product_qty', 'qty_produced', 'production_id.product_uom_id') def _compute_is_produced(self): self.is_produced = False - for order in self.filtered(lambda p: p.production_id): + for order in self.filtered(lambda p: p.production_id and p.production_id.product_uom_id): rounding = order.production_id.product_uom_id.rounding order.is_produced = float_compare(order.qty_produced, order.production_id.product_qty, precision_rounding=rounding) >= 0 @@ -347,11 +312,27 @@ class MrpWorkorder(models.Model): for workorder in self: workorder.scrap_count = count_data.get(workorder.id, 0) + @api.onchange('date_planned_finished') + def _onchange_date_planned_finished(self): + if self.date_planned_start and self.date_planned_finished: + diff = self.date_planned_finished - self.date_planned_start + self.duration_expected = diff.total_seconds() / 60 + + @api.onchange('operation_id') + def _onchange_operation_id(self): + if self.operation_id: + self.name = self.operation_id.name + self.workcenter_id = self.operation_id.workcenter_id.id + @api.onchange('date_planned_start', 'duration_expected') def _onchange_date_planned_start(self): if self.date_planned_start and self.duration_expected: self.date_planned_finished = self.date_planned_start + relativedelta(minutes=self.duration_expected) + @api.onchange('operation_id', 'workcenter_id', 'qty_production') + def _onchange_expected_duration(self): + self.duration_expected = self._get_duration_expected() + def write(self, values): if 'production_id' in values: raise UserError(_('You cannot link this work order to another manufacturing order.')) @@ -361,7 +342,7 @@ class MrpWorkorder(models.Model): if workorder.state in ('progress', 'done', 'cancel'): raise UserError(_('You cannot change the workcenter of a work order that is in progress or done.')) workorder.leave_id.resource_id = self.env['mrp.workcenter'].browse(values['workcenter_id']).resource_id - if list(values.keys()) != ['time_ids'] and any(workorder.state == 'done' for workorder in self): + if any(k not in ['time_ids', 'duration_expected', 'next_work_order_id'] for k in values.keys()) and any(workorder.state == 'done' for workorder in self): raise UserError(_('You can not change the finished work order.')) if 'date_planned_start' in values or 'date_planned_finished' in values: for workorder in self: @@ -372,196 +353,78 @@ class MrpWorkorder(models.Model): # Update MO dates if the start date of the first WO or the # finished date of the last WO is update. if workorder == workorder.production_id.workorder_ids[0] and 'date_planned_start' in values: - workorder.production_id.with_context(force_date=True).write({ - 'date_planned_start': fields.Datetime.to_datetime(values['date_planned_start']) - }) + if values['date_planned_start']: + workorder.production_id.with_context(force_date=True).write({ + 'date_planned_start': fields.Datetime.to_datetime(values['date_planned_start']) + }) if workorder == workorder.production_id.workorder_ids[-1] and 'date_planned_finished' in values: - workorder.production_id.with_context(force_date=True).write({ - 'date_planned_finished': fields.Datetime.to_datetime(values['date_planned_finished']) - }) + if values['date_planned_finished']: + workorder.production_id.with_context(force_date=True).write({ + 'date_planned_finished': fields.Datetime.to_datetime(values['date_planned_finished']) + }) return super(MrpWorkorder, self).write(values) - 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) + @api.model_create_multi + def create(self, values): + res = super().create(values) + # Auto-confirm manually added workorders. + # We need to go through `_action_confirm` for all workorders of the current productions to + # make sure the links between them are correct. + to_confirm = res.filtered(lambda wo: wo.production_id.state in ("confirmed", "progress", "to_close")) + to_confirm = to_confirm.production_id.workorder_ids + to_confirm._action_confirm() + return res - 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. - """ + def _action_confirm(self): + workorders_by_production = defaultdict(lambda: self.env['mrp.workorder']) 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 + workorders_by_production[workorder.production_id] |= workorder - 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) + for production, workorders in workorders_by_production.items(): + workorders_by_bom = defaultdict(lambda: self.env['mrp.workorder']) + bom = self.env['mrp.bom'] + moves = production.move_raw_ids | production.move_finished_ids - 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, + for workorder in self: + if workorder.operation_id.bom_id: + bom = workorder.operation_id.bom_id + if not bom: + bom = workorder.production_id.bom_id + previous_workorder = workorders_by_bom[bom][-1:] + previous_workorder.next_work_order_id = workorder.id + workorders_by_bom[bom] |= workorder + + moves.filtered(lambda m: m.operation_id == workorder.operation_id).write({ + 'workorder_id': workorder.id }) - 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 + exploded_boms, dummy = production.bom_id.explode(production.product_id, 1, picking_type=production.bom_id.picking_type_id) + exploded_boms = {b[0]: b[1] for b in exploded_boms} + for move in moves: + if move.workorder_id: + continue + bom = move.bom_line_id.bom_id + while bom and bom not in workorders_by_bom: + bom_data = exploded_boms.get(bom, {}) + bom = bom_data.get('parent_line') and bom_data['parent_line'].bom_id or False + if bom in workorders_by_bom: + move.write({ + 'workorder_id': workorders_by_bom[bom][-1:].id + }) + else: + move.write({ + 'workorder_id': workorders_by_bom[production.bom_id][-1:].id + }) - 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 + for workorders in workorders_by_bom.values(): + if workorders[0].state == 'pending': + workorders[0].state = 'ready' + for workorder in workorders: + workorder._start_nextworkorder() 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 ( @@ -570,6 +433,8 @@ class MrpWorkorder(models.Model): (self.operation_id.batch == 'yes' and float_compare(self.operation_id.batch_size, self.qty_produced, precision_rounding=rounding) <= 0)): self.next_work_order_id.state = 'ready' + if self.state == 'done' and self.next_work_order_id.state == 'pending': + self.next_work_order_id.state = 'ready' @api.model def gantt_unavailability(self, start_date, end_date, scale, group_bys=None, rows=None): @@ -615,9 +480,12 @@ class MrpWorkorder(models.Model): if self.state in ('done', 'cancel'): return True + if self.product_tracking == 'serial': + self.qty_producing = 1.0 + # Need a loss in case of the real time exceeding the expected timeline = self.env['mrp.workcenter.productivity'] - if self.duration < self.duration_expected: + if not self.duration_expected or self.duration < self.duration_expected: loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type','=','productive')], limit=1) if not len(loss_id): raise UserError(_("You need to define at least one productivity loss in the category 'Productivity'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses.")) @@ -632,7 +500,7 @@ class MrpWorkorder(models.Model): timeline.create({ 'workorder_id': self.id, 'workcenter_id': self.workcenter_id.id, - 'description': _('Time Tracking: ')+self.env.user.name, + 'description': _('Time Tracking: ') + self.env.user.name, 'loss_id': loss_id[0].id, 'date_start': datetime.now(), 'user_id': self.env.user.id, # FIXME sle: can be inconsistent with company_id @@ -640,26 +508,47 @@ class MrpWorkorder(models.Model): }) if self.state == 'progress': return True + start_date = datetime.now() + vals = { + 'state': 'progress', + 'date_start': start_date, + } + if not self.leave_id: + leave = self.env['resource.calendar.leaves'].create({ + 'name': self.display_name, + 'calendar_id': self.workcenter_id.resource_calendar_id.id, + 'date_from': start_date, + 'date_to': start_date + relativedelta(minutes=self.duration_expected), + 'resource_id': self.workcenter_id.resource_id.id, + 'time_type': 'other' + }) + vals['leave_id'] = leave.id + return self.write(vals) else: - start_date = datetime.now() - vals = { - 'state': 'progress', - 'date_start': start_date, - 'date_planned_start': start_date, - } + vals['date_planned_start'] = start_date if self.date_planned_finished < start_date: vals['date_planned_finished'] = start_date return self.write(vals) def button_finish(self): - self.ensure_one() - self.end_all() end_date = datetime.now() - return self.write({ - 'state': 'done', - 'date_finished': end_date, - 'date_planned_finished': end_date - }) + for workorder in self: + if workorder.state in ('done', 'cancel'): + continue + workorder.end_all() + vals = { + 'state': 'done', + 'date_finished': end_date, + 'date_planned_finished': end_date + } + if not workorder.date_start: + vals['date_start'] = end_date + if not workorder.date_planned_start or end_date < workorder.date_planned_start: + vals['date_planned_start'] = end_date + workorder.write(vals) + + workorder._start_nextworkorder() + return True def end_previous(self, doall=False): """ @@ -751,11 +640,32 @@ class MrpWorkorder(models.Model): action['domain'] = [('workorder_id', '=', self.id)] return action + def action_open_wizard(self): + self.ensure_one() + action = self.env.ref('mrp.mrp_workorder_mrp_production_form').read()[0] + action['res_id'] = self.id + return action + @api.depends('qty_production', 'qty_produced') def _compute_qty_remaining(self): for wo in self: wo.qty_remaining = float_round(wo.qty_production - wo.qty_produced, precision_rounding=wo.production_id.product_uom_id.rounding) + def _get_duration_expected(self, alternative_workcenter=False): + self.ensure_one() + if not self.workcenter_id: + return False + qty_production = self.production_id.product_uom_id._compute_quantity(self.qty_production, self.production_id.product_id.uom_id) + cycle_number = float_round(qty_production / self.workcenter_id.capacity, precision_digits=0, rounding_method='UP') + if alternative_workcenter: + # TODO : find a better alternative : the settings of workcenter can change + duration_expected_working = (self.duration_expected - self.workcenter_id.time_start - self.workcenter_id.time_stop) * self.workcenter_id.time_efficiency / (100.0 * cycle_number) + if duration_expected_working < 0: + duration_expected_working = 0 + return alternative_workcenter.time_start + alternative_workcenter.time_stop + cycle_number * duration_expected_working * 100.0 / alternative_workcenter.time_efficiency + time_cycle = self.operation_id and self.operation_id.time_cycle or 60.0 + return self.workcenter_id.time_start + self.workcenter_id.time_stop + cycle_number * time_cycle * 100.0 / self.workcenter_id.time_efficiency + def _get_conflicted_workorder_ids(self): """Get conlicted workorder(s) with self. @@ -782,46 +692,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/res_config_settings.py b/addons/mrp/models/res_config_settings.py index b84658d9a5b..0ab3a4262bb 100644 --- a/addons/mrp/models/res_config_settings.py +++ b/addons/mrp/models/res_config_settings.py @@ -18,6 +18,7 @@ class ResConfigSettings(models.TransientModel): module_mrp_subcontracting = fields.Boolean("Subcontracting") group_mrp_routings = fields.Boolean("MRP Work Orders", implied_group='mrp.group_mrp_routings') + group_locked_by_default = fields.Boolean("Lock Quantities To Consume", implied_group='mrp.group_locked_by_default') @api.onchange('use_manufacturing_lead') def _onchange_use_manufacturing_lead(self): diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index c60c9f051e6..a3fc758cdfc 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -11,7 +11,6 @@ class StockMoveLine(models.Model): workorder_id = fields.Many2one('mrp.workorder', 'Work Order', check_company=True) production_id = fields.Many2one('mrp.production', 'Production Order', check_company=True) - lot_produced_ids = fields.Many2many('stock.production.lot', string='Finished Lot/Serial Number', check_company=True) done_move = fields.Boolean('Move Done', related='move_id.is_done', readonly=False, store=True) # TDE FIXME: naming @api.model_create_multi @@ -23,8 +22,10 @@ class StockMoveLine(models.Model): # traceability report if line.move_id.raw_material_production_id and line.state == 'done': mo = line.move_id.raw_material_production_id - if line.lot_produced_ids: - produced_move_lines = mo.move_finished_ids.move_line_ids.filtered(lambda sml: sml.lot_id in line.lot_produced_ids) + finished_lots = mo.lot_producing_id + finished_lots |= mo.move_finished_ids.filtered(lambda m: m.product_id != mo.product_id).move_line_ids.lot_id + if finished_lots: + produced_move_lines = mo.move_finished_ids.move_line_ids.filtered(lambda sml: sml.lot_id in finished_lots) line.produce_line_ids = [(6, 0, produced_move_lines.ids)] else: produced_move_lines = mo.move_finished_ids.move_line_ids @@ -45,7 +46,7 @@ class StockMoveLine(models.Model): def _reservation_is_updatable(self, quantity, reserved_quant): self.ensure_one() - if self.lot_produced_ids: + if self.produce_line_ids.lot_id: ml_remaining_qty = self.qty_done - self.product_uom_qty ml_remaining_qty = self.product_uom_id._compute_quantity(ml_remaining_qty, self.product_id.uom_id, rounding_method="HALF-UP") if float_compare(ml_remaining_qty, quantity, precision_rounding=self.product_id.uom_id.rounding) < 0: @@ -54,10 +55,6 @@ class StockMoveLine(models.Model): def write(self, vals): for move_line in self: - if move_line.move_id.production_id and 'lot_id' in vals: - move_line.production_id.move_raw_ids.mapped('move_line_ids')\ - .filtered(lambda r: not r.done_move and move_line.lot_id in r.lot_produced_ids)\ - .write({'lot_produced_ids': [(4, vals['lot_id'])]}) production = move_line.move_id.production_id or move_line.move_id.raw_material_production_id if production and move_line.state == 'done' and any(field in vals for field in ('lot_id', 'location_id', 'qty_done')): move_line._log_message(production, move_line, 'mrp.track_production_move_template', vals) @@ -76,10 +73,10 @@ class StockMove(models.Model): 'mrp.unbuild', 'Disassembly Order', check_company=True) consume_unbuild_id = fields.Many2one( 'mrp.unbuild', 'Consumed Disassembly Order', check_company=True) + allowed_operation_ids = fields.Many2many('mrp.routing.workcenter', compute='_compute_allowed_operation_ids') operation_id = fields.Many2one( 'mrp.routing.workcenter', 'Operation To Consume', check_company=True, - domain="[('routing_id', '=', routing_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]") - routing_id = fields.Many2one(related='raw_material_production_id.routing_id') + domain="[('id', 'in', allowed_operation_ids)]") workorder_id = fields.Many2one( 'mrp.workorder', 'Work Order To Consume', check_company=True) # Quantities to process, in normalized UoMs @@ -95,6 +92,7 @@ class StockMove(models.Model): needs_lots = fields.Boolean('Tracking', compute='_compute_needs_lots') order_finished_lot_ids = fields.Many2many('stock.production.lot', compute='_compute_order_finished_lot_ids') finished_lots_exist = fields.Boolean('Finished Lots Exist', compute='_compute_order_finished_lot_ids') + should_consume_qty = fields.Float('Quantity To Consume', compute='_compute_should_consume_qty') def _unreserve_initial_demand(self, new_move): # If you were already putting stock.move.lots on the next one in the work order, transfer those to the new move @@ -118,39 +116,88 @@ class StockMove(models.Model): move.order_finished_lot_ids = False move.finished_lots_exist = False + @api.depends('raw_material_production_id.bom_id') + def _compute_allowed_operation_ids(self): + for move in self: + if ( + not move.raw_material_production_id or + not move.raw_material_production_id.bom_id or not + move.raw_material_production_id.bom_id.operation_ids + ): + move.allowed_operation_ids = self.env['mrp.routing.workcenter'] + else: + operation_domain = [ + ('id', 'in', move.raw_material_production_id.bom_id.operation_ids.ids), + '|', + ('company_id', '=', self.company_id.id), + ('company_id', '=', False) + ] + move.allowed_operation_ids = self.env['mrp.routing.workcenter'].search(operation_domain) + @api.depends('product_id.tracking') def _compute_needs_lots(self): for move in self: move.needs_lots = move.product_id.tracking != 'none' - @api.depends('raw_material_production_id.is_locked', 'picking_id.is_locked') + @api.depends('raw_material_production_id.is_locked', 'production_id.is_locked') def _compute_is_locked(self): super(StockMove, self)._compute_is_locked() for move in self: if move.raw_material_production_id: move.is_locked = move.raw_material_production_id.is_locked + if move.production_id: + move.is_locked = move.production_id.is_locked @api.depends('state') def _compute_is_done(self): for move in self: move.is_done = (move.state in ('done', 'cancel')) - @api.depends('product_uom_qty') + @api.depends('product_uom_qty', + 'raw_material_production_id', 'raw_material_production_id.product_qty', 'raw_material_production_id.qty_produced', + 'production_id', 'production_id.product_qty', 'production_id.qty_produced') def _compute_unit_factor(self): for move in self: mo = move.raw_material_production_id or move.production_id if mo: - move.unit_factor = (move.product_uom_qty - move.quantity_done) / ((mo.product_qty - mo.qty_produced) or 1) + move.unit_factor = move.product_uom_qty / ((mo.product_qty - mo.qty_produced) or 1) else: move.unit_factor = 1.0 + @api.depends('raw_material_production_id', 'raw_material_production_id.name', 'production_id', 'production_id.name') + def _compute_reference(self): + moves_with_reference = self.env['stock.move'] + for move in self: + if move.raw_material_production_id and move.raw_material_production_id.name: + move.reference = move.raw_material_production_id.name + moves_with_reference |= move + if move.production_id and move.production_id.name: + move.reference = move.production_id.name + moves_with_reference |= move + super(StockMove, self - moves_with_reference)._compute_reference() + + @api.depends('raw_material_production_id.qty_producing', 'product_uom_qty') + def _compute_should_consume_qty(self): + for move in self: + mo = move.raw_material_production_id + if not mo: + move.qty_summary = 0 + continue + move.should_consume_qty = mo.product_uom_id._compute_quantity((mo.qty_producing - mo.qty_produced) * move.unit_factor, mo.product_uom_id, rounding_method='HALF-UP') + + @api.onchange('product_uom_qty') + def _onchange_product_uom_qty(self): + if self.raw_material_production_id and self.has_tracking == 'none': + mo = self.raw_material_production_id + vals = self._update_quantity_done(mo) + @api.model def default_get(self, fields_list): defaults = super(StockMove, self).default_get(fields_list) - if self.env.context.get('default_raw_material_production_id'): - production_id = self.env['mrp.production'].browse(self.env.context['default_raw_material_production_id']) - if production_id.state in ('confirmed', 'done'): - if production_id.state == 'confirmed': + if self.env.context.get('default_raw_material_production_id') or self.env.context.get('default_production_id'): + production_id = self.env['mrp.production'].browse(self.env.context.get('default_raw_material_production_id') or self.env.context.get('default_production_id')) + if production_id.state not in ('draft', 'cancel'): + if production_id.state != 'done': defaults['state'] = 'draft' else: defaults['state'] = 'done' @@ -207,6 +254,17 @@ class StockMove(models.Model): moves_to_return |= phantom_moves.action_explode() return moves_to_return + def action_show_details(self): + self.ensure_one() + action = super().action_show_details() + if self.raw_material_production_id: + action['views'] = [(self.env.ref('mrp.view_stock_move_operations_raw').id, 'form')] + action['context']['show_destination_location'] = False + elif self.production_id: + action['views'] = [(self.env.ref('mrp.view_stock_move_operations_finished').id, 'form')] + action['context']['show_source_location'] = False + return action + def _action_cancel(self): res = super(StockMove, self)._action_cancel() for production in self.mapped('raw_material_production_id'): @@ -215,6 +273,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, @@ -236,10 +299,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() @@ -250,6 +313,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) @@ -310,3 +383,21 @@ class StockMove(models.Model): return min(qty_ratios) // 1 else: return 0.0 + + def _show_details_in_draft(self): + self.ensure_one() + if self.raw_material_production_id and self.state == 'draft': + return True + else: + return super()._show_details_in_draft() + + def _update_quantity_done(self, mo): + self.ensure_one() + ml_values = {} + 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 + 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/models/stock_rule.py b/addons/mrp/models/stock_rule.py index a7dfd5e8961..75d2df4a046 100644 --- a/addons/mrp/models/stock_rule.py +++ b/addons/mrp/models/stock_rule.py @@ -55,6 +55,8 @@ class StockRule(models.Model): # create the MO as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example) productions = self.env['mrp.production'].with_user(SUPERUSER_ID).sudo().with_company(company_id).create(productions_values) self.env['stock.move'].sudo().create(productions._get_moves_raw_values()) + self.env['stock.move'].sudo().create(productions._get_moves_finished_values()) + productions._create_workorder() productions.filtered(lambda p: p.move_raw_ids).action_confirm() for production in productions: @@ -140,7 +142,7 @@ class StockRule(models.Model): class ProcurementGroup(models.Model): _inherit = 'procurement.group' - mrp_production_id = fields.One2many('mrp.production', 'procurement_group_id') + mrp_production_ids = fields.One2many('mrp.production', 'procurement_group_id') @api.model def run(self, procurements, raise_user_error=True): diff --git a/addons/mrp/report/mrp_report_bom_structure.py b/addons/mrp/report/mrp_report_bom_structure.py index 05c3bafbd3d..b9108828b37 100644 --- a/addons/mrp/report/mrp_report_bom_structure.py +++ b/addons/mrp/report/mrp_report_bom_structure.py @@ -55,7 +55,7 @@ class ReportBomStructure(models.AbstractModel): @api.model def get_operations(self, bom_id=False, qty=0, level=0): bom = self.env['mrp.bom'].browse(bom_id) - lines = self._get_operation_line(bom.routing_id, float_round(qty / bom.product_qty, precision_rounding=1, rounding_method='UP'), level) + lines = self._get_operation_line(bom, float_round(qty / bom.product_qty, precision_rounding=1, rounding_method='UP'), level) values = { 'bom_id': bom_id, 'currency': self.env.company.currency_id, @@ -106,7 +106,7 @@ class ReportBomStructure(models.AbstractModel): else: product = bom.product_tmpl_id attachments = self.env['mrp.document'].search([('res_model', '=', 'product.template'), ('res_id', '=', product.id)]) - operations = self._get_operation_line(bom.routing_id, float_round(bom_quantity / bom.product_qty, precision_rounding=1, rounding_method='UP'), 0) + operations = self._get_operation_line(bom, float_round(bom_quantity / bom.product_qty, precision_rounding=1, rounding_method='UP'), 0) company = bom.company_id or self.env.company lines = { 'bom': bom, @@ -163,10 +163,10 @@ class ReportBomStructure(models.AbstractModel): total += sub_total return components, total - def _get_operation_line(self, routing, qty, level): + def _get_operation_line(self, bom, qty, level): operations = [] total = 0.0 - for operation in routing.operation_ids: + for operation in bom.operation_ids: operation_cycle = float_round(qty / operation.workcenter_id.capacity, precision_rounding=1, rounding_method='UP') duration_expected = operation_cycle * operation.time_cycle + operation.workcenter_id.time_stop + operation.workcenter_id.time_start total = ((duration_expected / 60.0) * operation.workcenter_id.costs_hour) @@ -181,7 +181,7 @@ class ReportBomStructure(models.AbstractModel): def _get_price(self, bom, factor, product): price = 0 - if bom.routing_id: + if bom.operation_ids: # routing are defined on a BoM and don't have a concept of quantity. # It means that the operation time are defined for the quantity on # the BoM (the user produces a batch of products). E.g the user @@ -189,7 +189,7 @@ class ReportBomStructure(models.AbstractModel): # will be the 5 for a quantity between 1-10, then doubled for # 11-20,... operation_cycle = float_round(factor, precision_rounding=1, rounding_method='UP') - operations = self._get_operation_line(bom.routing_id, operation_cycle, 0) + operations = self._get_operation_line(bom, operation_cycle, 0) price += sum([op['total'] for op in operations]) for line in bom.bom_line_ids: diff --git a/addons/mrp/security/ir.model.access.csv b/addons/mrp/security/ir.model.access.csv index 3b3ebdf97d2..73d4f9c3888 100644 --- a/addons/mrp/security/ir.model.access.csv +++ b/addons/mrp/security/ir.model.access.csv @@ -4,14 +4,12 @@ access_mrp_workcenter_productivity_loss,mrp.workcenter.productivity.loss,model_m access_mrp_workcenter_productivity_loss_type,mrp.workcenter.productivity.loss.type,model_mrp_workcenter_productivity_loss_type,mrp.group_mrp_user,1,0,0,0 access_mrp_workcenter_productivity,mrp.workcenter.productivity,model_mrp_workcenter_productivity,mrp.group_mrp_user,1,1,1,1 access_mrp_workcenter,mrp.workcenter,model_mrp_workcenter,mrp.group_mrp_user,1,0,0,0 -access_mrp_routing,mrp.routing,model_mrp_routing,mrp.group_mrp_user,1,0,0,0 access_mrp_routing_workcenter,mrp.routing.workcenter,model_mrp_routing_workcenter,mrp.group_mrp_user,1,0,0,0 access_mrp_bom,mrp.bom,model_mrp_bom,group_mrp_user,1,0,0,0 access_mrp_bom_line,mrp.bom.line,model_mrp_bom_line,group_mrp_user,1,0,0,0 access_mrp_bom_byproduct_user,mrp.bom.byproduct,model_mrp_bom_byproduct,mrp.group_mrp_user,1,0,0,0 access_mrp_production,mrp.production user,model_mrp_production,mrp.group_mrp_user,1,1,1,1 access_mrp_workcenter_manager,mrp.workcenter.manager,model_mrp_workcenter,mrp.group_mrp_manager,1,1,1,1 -access_mrp_routing_manager,mrp.routing.manager,model_mrp_routing,mrp.group_mrp_manager,1,1,1,1 access_mrp_routing_workcenter_manager,mrp.routing.workcenter.manager,model_mrp_routing_workcenter,mrp.group_mrp_manager,1,1,1,1 access_mrp_bom_manager,mrp.bom.manager,model_mrp_bom,mrp.group_mrp_manager,1,1,1,1 access_mrp_bom_line_manager,mrp.bom.line.manager,model_mrp_bom_line,mrp.group_mrp_manager,1,1,1,1 @@ -28,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 @@ -59,7 +55,9 @@ access_mrp_unbuild,mrp.unbuild,model_mrp_unbuild,group_mrp_user,1,1,1,1 access_mrp_unbuild_manager,mrp.unbuild manager,model_mrp_unbuild,group_mrp_manager,1,1,1,1 access_mrp_document_mrp_manager,mrp.document group_user,model_mrp_document,group_mrp_manager,1,1,1,1 access_mrp_document_mrp_user,mrp.document group_user,model_mrp_document,group_mrp_user,1,1,1,1 -access_mrp_product_produce,access.mrp.product.produce,model_mrp_product_produce,mrp.group_mrp_user,1,1,1,0 -access_mrp_product_produce_line,access.mrp.product.produce.line,model_mrp_product_produce_line,mrp.group_mrp_user,1,1,1,1 access_change_production_qty,access.change.production.qty,model_change_production_qty,mrp.group_mrp_user,1,1,1,0 access_stock_warn_insufficient_qty_unbuild,access.stock.warn.insufficient.qty.unbuild,model_stock_warn_insufficient_qty_unbuild,mrp.group_mrp_user,1,1,1,0 +access_mrp_production_backorder,access.mrp.production.backorder,model_mrp_production_backorder,mrp.group_mrp_user,1,1,1,0 +access_mrp_production_backorder_line,access.mrp.production.backorder.line,model_mrp_production_backorder_line,mrp.group_mrp_user,1,1,1,0 +access_mrp_consumption_warning,access.mrp.consumption.warning,model_mrp_consumption_warning,mrp.group_mrp_user,1,1,1,0 +access_mrp_consumption_warning_line,access.mrp.consumption.warning.line,model_mrp_consumption_warning_line,mrp.group_mrp_user,1,1,1,0 diff --git a/addons/mrp/security/mrp_security.xml b/addons/mrp/security/mrp_security.xml index c1c350dffd1..795e6c7813b 100644 --- a/addons/mrp/security/mrp_security.xml +++ b/addons/mrp/security/mrp_security.xml @@ -30,6 +30,11 @@ + + Locked by default + + + @@ -78,12 +83,6 @@ ['|',('company_id', 'in', company_ids),('company_id','=',False)] - - mrp_routing multi-company - - ['|',('company_id', 'in', company_ids),('company_id','=',False)] - - mrp_routing_workcenter multi-company diff --git a/addons/mrp/static/src/js/mrp_should_consume.js b/addons/mrp/static/src/js/mrp_should_consume.js new file mode 100644 index 00000000000..50b4f3f893b --- /dev/null +++ b/addons/mrp/static/src/js/mrp_should_consume.js @@ -0,0 +1,81 @@ +odoo.define('mrp.should_consume', function (require) { +"use strict"; + +var BasicFields = require('web.basic_fields'); +var FieldFloat = BasicFields.FieldFloat; +var fieldRegistry = require('web.field_registry'); +var field_utils = require('web.field_utils'); + +/** + * This widget is used to display alongside the total quantity to consume of a production order, + * the exact quantity that the worker should consume depending on the BoM. Ex: + * 2 components to make 1 finished product. + * The production order is created to make 5 finished product and the quantity producing is set to 3. + * The widget will be '3.000 / 5.000'. + */ +var MrpShouldConsume = FieldFloat.extend({ + /** + * @override + */ + init: function (parent, name, params) { + this._super.apply(this, arguments); + this.displayShouldConsume = !['done', 'draft', 'cancel'].includes(params.data.state); + let options = {'digits': [false, 3]}; + this.should_consume_qty = field_utils.format.float(params.data.should_consume_qty, false, options); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Object} [el] jquery input element that will be surrounded by a new span + * @param {float} [value] quantity to display before the input `el` + * @return {jquery element} + */ + _addShouldConsume: function (el, value) { + var $to_consume_container = $(''); + $to_consume_container.text(value + ' / '); + $to_consume_container.append(el); + return $to_consume_container + }, + + /** + * @private + * @override + */ + _renderEdit: function () { + // Keep a reference to the input so $el can become something else + // without losing track of the actual input. + var def = this._super.apply(this, arguments); + if (this.displayShouldConsume) { + var $container = this._addShouldConsume(this.$el, this.should_consume_qty); + $container.addClass('o_row'); + this.$el = $container; + }; + return def; + }, + /** + * Resets the content to the formated value in readonly mode. + * + * @override + * @private + */ + _renderReadonly: function () { + var def = this._super.apply(this, arguments); + if (this.displayShouldConsume) { + var $container = this._addShouldConsume(this.$el, this.should_consume_qty); + this.$el = $container; + }; + return def; + }, +}); + +fieldRegistry.add('mrp_should_consume', MrpShouldConsume); + +return { + MrpShouldConsume: MrpShouldConsume, +}; + +}); diff --git a/addons/mrp/static/src/js/mrp_workorder_popover.js b/addons/mrp/static/src/js/mrp_workorder_popover.js index 6d515aa2340..7e392c09c7a 100644 --- a/addons/mrp/static/src/js/mrp_workorder_popover.js +++ b/addons/mrp/static/src/js/mrp_workorder_popover.js @@ -24,6 +24,9 @@ var MrpWorkorderPopover = PopoverWidget.extend({ _render: function () { this._super.apply(this, arguments); + if (! this.$popover) { + return; + } var self = this; this.$popover.find('.action_replan_button').click(function (e) { self._onReplanClick(e); diff --git a/addons/mrp/static/src/scss/mrp_fields.scss b/addons/mrp/static/src/scss/mrp_fields.scss index 606e32f4d94..da959565fc8 100644 --- a/addons/mrp/static/src/scss/mrp_fields.scss +++ b/addons/mrp/static/src/scss/mrp_fields.scss @@ -5,4 +5,7 @@ height: 30rem; border: none; } -} \ No newline at end of file +} +.o_should_consume{ + padding-left: 0.3em; +} diff --git a/addons/mrp/tests/__init__.py b/addons/mrp/tests/__init__.py index 5d4d1bb767d..2af9d670b87 100644 --- a/addons/mrp/tests/__init__.py +++ b/addons/mrp/tests/__init__.py @@ -6,9 +6,9 @@ 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 from . import test_traceability from . import test_multicompany +from . import test_backorder diff --git a/addons/mrp/tests/common.py b/addons/mrp/tests/common.py index 382bbb849fe..2992c6961d6 100644 --- a/addons/mrp/tests/common.py +++ b/addons/mrp/tests/common.py @@ -7,7 +7,7 @@ from odoo.addons.stock.tests import common2 class TestMrpCommon(common2.TestStockCommon): @classmethod - def generate_mo(self, tracking_final='none', tracking_base_1='none', tracking_base_2='none', qty_final=5, qty_base_1=4, qty_base_2=1): + def generate_mo(self, tracking_final='none', tracking_base_1='none', tracking_base_2='none', qty_final=5, qty_base_1=4, qty_base_2=1, picking_type_id=False, consumption=False): """ This function generate a manufacturing order with one final product and two consumed product. Arguments allows to choose the tracking/qty for each different products. It returns the @@ -34,11 +34,14 @@ class TestMrpCommon(common2.TestStockCommon): 'product_uom_id': self.uom_unit.id, 'product_qty': 1.0, 'type': 'normal', + 'consumption': consumption if consumption else 'flexible', 'bom_line_ids': [ (0, 0, {'product_id': product_to_use_2.id, 'product_qty': qty_base_2}), (0, 0, {'product_id': product_to_use_1.id, 'product_qty': qty_base_1}) ]}) mo_form = Form(self.env['mrp.production']) + if picking_type_id: + mo_form.picking_type_id = picking_type_id mo_form.product_id = product_to_build mo_form.bom_id = bom_1 mo_form.product_qty = qty_final @@ -91,40 +94,15 @@ class TestMrpCommon(common2.TestStockCommon): 'time_stop': 5, 'time_efficiency': 80, }) - cls.routing_1 = cls.env['mrp.routing'].create({ - 'name': 'Simple Line', - }) - cls.routing_2 = cls.env['mrp.routing'].create({ - 'name': 'Complicated Line', - }) - cls.operation_1 = cls.env['mrp.routing.workcenter'].create({ - 'name': 'Gift Wrap Maching', - 'workcenter_id': cls.workcenter_1.id, - 'routing_id': cls.routing_1.id, - 'time_cycle': 15, - 'sequence': 1, - }) - cls.operation_2 = cls.env['mrp.routing.workcenter'].create({ - 'name': 'Cutting Machine', - 'workcenter_id': cls.workcenter_1.id, - 'routing_id': cls.routing_2.id, - 'time_cycle': 12, - 'sequence': 1, - }) - cls.operation_3 = cls.env['mrp.routing.workcenter'].create({ - 'name': 'Weld Machine', - 'workcenter_id': cls.workcenter_1.id, - 'routing_id': cls.routing_2.id, - 'time_cycle': 18, - 'sequence': 2, - }) cls.bom_1 = 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, - 'routing_id': cls.routing_2.id, + 'consumption': 'flexible', + 'operation_ids': [ + ], 'type': 'normal', 'bom_line_ids': [ (0, 0, {'product_id': cls.product_2.id, 'product_qty': 2}), @@ -134,8 +112,11 @@ class TestMrpCommon(common2.TestStockCommon): 'product_id': cls.product_5.id, 'product_tmpl_id': cls.product_5.product_tmpl_id.id, 'product_uom_id': cls.product_5.uom_id.id, + 'consumption': 'flexible', 'product_qty': 1.0, - 'routing_id': cls.routing_1.id, + 'operation_ids': [ + (0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}), + ], 'type': 'phantom', 'sequence': 2, 'bom_line_ids': [ @@ -146,14 +127,19 @@ class TestMrpCommon(common2.TestStockCommon): 'product_id': cls.product_6.id, 'product_tmpl_id': cls.product_6.product_tmpl_id.id, 'product_uom_id': cls.uom_dozen.id, + 'consumption': 'flexible', 'product_qty': 2.0, - 'routing_id': cls.routing_2.id, + 'operation_ids': [ + (0, 0, {'name': 'Cutting Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}), + (0, 0, {'name': 'Weld Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}), + ], 'type': 'normal', 'bom_line_ids': [ (0, 0, {'product_id': cls.product_5.id, 'product_qty': 2}), (0, 0, {'product_id': cls.product_4.id, 'product_qty': 8}), (0, 0, {'product_id': cls.product_2.id, 'product_qty': 12}) ]}) + cls.stock_location_14 = cls.env['stock.location'].create({ 'name': 'Shelf 2', 'location_id': cls.env.ref('stock.warehouse0').lot_stock_id.id, @@ -177,4 +163,4 @@ class TestMrpCommon(common2.TestStockCommon): 'type': 'product', 'tracking': 'none', 'categ_id': cls.env.ref('product.product_category_all').id, - }) \ No newline at end of file + }) diff --git a/addons/mrp/tests/test_backorder.py b/addons/mrp/tests/test_backorder.py new file mode 100644 index 00000000000..ec2e27f1edd --- /dev/null +++ b/addons/mrp/tests/test_backorder.py @@ -0,0 +1,328 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.mrp.tests.common import TestMrpCommon +from odoo.tests import Form +from odoo.tests.common import SavepointCase + + +class TestMrpProductionBackorder(TestMrpCommon): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.stock_location = cls.env.ref('stock.stock_location_stock') + + def setUp(self): + super().setUp() + warehouse_form = Form(self.env['stock.warehouse']) + warehouse_form.name = 'Test Warehouse' + warehouse_form.code = 'TWH' + self.warehouse = warehouse_form.save() + + def test_no_tracking_1(self): + """Create a MO for 4 product. Produce 4. The backorder button should + not appear and hitting mark as done should not open the backorder wizard. + The name of the MO should be MO/001. + """ + mo = self.generate_mo(qty_final=4)[0] + + mo_form = Form(mo) + mo_form.qty_producing = 4 + mo = mo_form.save() + + # No backorder is proposed + self.assertTrue(mo.button_mark_done()) + self.assertEqual(mo._get_quantity_to_backorder(), 0) + self.assertTrue("-001" not in mo.name) + + def test_no_tracking_2(self): + """Create a MO for 4 product. Produce 1. The backorder button should + appear and hitting mark as done should open the backorder wizard. In the backorder + wizard, choose to do the backorder. A new MO for 3 self.untracked_bom should be + created. + The sequence of the first MO should be MO/001-01, the sequence of the second MO + should be MO/001-02. + Check that all MO are reachable through the procurement group. + """ + production, _, _, product_to_use_1, _ = self.generate_mo(qty_final=4, qty_base_1=3) + self.assertEqual(production.state, 'confirmed') + self.assertEqual(production.reserve_visible, True) + + # Make some stock and reserve + for product in production.move_raw_ids.product_id: + self.env['stock.quant'].with_context(inventory_mode=True).create({ + 'product_id': product.id, + 'inventory_quantity': 100, + 'location_id': production.location_src_id.id, + }) + production.action_assign() + self.assertEqual(production.state, 'confirmed') + self.assertEqual(production.reserve_visible, False) + + mo_form = Form(production) + mo_form.qty_producing = 1 + production = mo_form.save() + + action = production.button_mark_done() + backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context'])) + backorder.save().action_backorder() + + # Two related MO to the procurement group + self.assertEqual(len(production.procurement_group_id.mrp_production_ids), 2) + + # Check MO backorder + mo_backorder = production.procurement_group_id.mrp_production_ids[-1] + self.assertEqual(mo_backorder.product_id.id, production.product_id.id) + self.assertEqual(mo_backorder.product_qty, 3) + self.assertEqual(sum(mo_backorder.move_raw_ids.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_uom_qty")), 9) + self.assertEqual(mo_backorder.reserve_visible, False) # the reservation of the first MO should've been moved here + + def test_no_tracking_pbm_1(self): + """Create a MO for 4 product. Produce 1. The backorder button should + appear and hitting mark as done should open the backorder wizard. In the backorder + wizard, choose to do the backorder. A new MO for 3 self.untracked_bom should be + created. + The sequence of the first MO should be MO/001-01, the sequence of the second MO + should be MO/001-02. + Check that all MO are reachable through the procurement group. + """ + with Form(self.warehouse) as warehouse: + warehouse.manufacture_steps = 'pbm' + + production, _, product_to_build, product_to_use_1, product_to_use_2 = self.generate_mo(qty_base_1=4, qty_final=4, picking_type_id=self.warehouse.manu_type_id) + + move_raw_ids = production.move_raw_ids + self.assertEqual(len(move_raw_ids), 2) + self.assertEqual(set(move_raw_ids.mapped("product_id")), {product_to_use_1, product_to_use_2}) + + pbm_move = move_raw_ids.move_orig_ids + self.assertEqual(len(pbm_move), 2) + self.assertEqual(set(pbm_move.mapped("product_id")), {product_to_use_1, product_to_use_2}) + self.assertFalse(pbm_move.move_orig_ids) + + mo_form = Form(production) + mo_form.qty_producing = 1 + production = mo_form.save() + self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16) + self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4) + + action = production.button_mark_done() + backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context'])) + backorder.save().action_backorder() + + mo_backorder = production.procurement_group_id.mrp_production_ids[-1] + self.assertEqual(mo_backorder.delivery_count, 1) + + pbm_move |= mo_backorder.move_raw_ids.move_orig_ids + # Check that quantity is correct + self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16) + self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4) + + self.assertFalse(pbm_move.move_orig_ids) + + def test_no_tracking_pbm_sam_1(self): + """Create a MO for 4 product. Produce 1. The backorder button should + appear and hitting mark as done should open the backorder wizard. In the backorder + wizard, choose to do the backorder. A new MO for 3 self.untracked_bom should be + created. + The sequence of the first MO should be MO/001-01, the sequence of the second MO + should be MO/001-02. + Check that all MO are reachable through the procurement group. + """ + with Form(self.warehouse) as warehouse: + warehouse.manufacture_steps = 'pbm_sam' + production, _, product_to_build, product_to_use_1, product_to_use_2 = self.generate_mo(qty_base_1=4, qty_final=4, picking_type_id=self.warehouse.manu_type_id) + + move_raw_ids = production.move_raw_ids + self.assertEqual(len(move_raw_ids), 2) + self.assertEqual(set(move_raw_ids.mapped("product_id")), {product_to_use_1, product_to_use_2}) + + pbm_move = move_raw_ids.move_orig_ids + self.assertEqual(len(pbm_move), 2) + self.assertEqual(set(pbm_move.mapped("product_id")), {product_to_use_1, product_to_use_2}) + self.assertFalse(pbm_move.move_orig_ids) + self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16) + self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4) + + sam_move = production.move_finished_ids.move_dest_ids + self.assertEqual(len(sam_move), 1) + self.assertEqual(sam_move.product_id.id, product_to_build.id) + self.assertEqual(sum(sam_move.mapped("product_qty")), 4) + + mo_form = Form(production) + mo_form.qty_producing = 1 + production = mo_form.save() + + action = production.button_mark_done() + backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context'])) + backorder.save().action_backorder() + + mo_backorder = production.procurement_group_id.mrp_production_ids[-1] + self.assertEqual(mo_backorder.delivery_count, 2) + + pbm_move |= mo_backorder.move_raw_ids.move_orig_ids + self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_1.id).mapped("product_qty")), 16) + self.assertEqual(sum(pbm_move.filtered(lambda m: m.product_id.id == product_to_use_2.id).mapped("product_qty")), 4) + + sam_move |= mo_backorder.move_finished_ids.move_orig_ids + self.assertEqual(sum(sam_move.mapped("product_qty")), 4) + + def test_tracking_backorder_series_lot_1(self): + """ Create a MO of 4 tracked products. all component is tracked by lots + Produce one by one with one bakorder for each until end. + """ + nb_product_todo = 4 + production, _, p_final, p1, p2 = self.generate_mo(qty_final=nb_product_todo, tracking_final='lot', tracking_base_1='lot', tracking_base_2='lot') + lot_final = self.env['stock.production.lot'].create({ + 'name': 'lot_final', + 'product_id': p_final.id, + 'company_id': self.env.company.id, + }) + lot_1 = self.env['stock.production.lot'].create({ + 'name': 'lot_consumed_1', + 'product_id': p1.id, + 'company_id': self.env.company.id, + }) + lot_2 = self.env['stock.production.lot'].create({ + 'name': 'lot_consumed_2', + 'product_id': p2.id, + 'company_id': self.env.company.id, + }) + + self.env['stock.quant']._update_available_quantity(p1, self.stock_location, nb_product_todo*4, lot_id=lot_1) + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, nb_product_todo, lot_id=lot_2) + + production.action_assign() + active_production = production + for i in range(nb_product_todo): + + details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.product_id == p1), view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.edit(0) as ml: + ml.qty_done = 4 + ml.lot_id = lot_1 + details_operation_form.save() + details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.product_id == p2), view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.edit(0) as ml: + ml.qty_done = 1 + ml.lot_id = lot_2 + details_operation_form.save() + + production_form = Form(active_production) + production_form.qty_producing = 1 + production_form.lot_producing_id = lot_final + active_production = production_form.save() + + active_production.button_mark_done() + if i + 1 != nb_product_todo: # If last MO, don't make a backorder + action = active_production.button_mark_done() + backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context'])) + backorder.save().action_backorder() + active_production = active_production.procurement_group_id.mrp_production_ids[-1] + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), nb_product_todo, f'You should have the {nb_product_todo} final product in stock') + self.assertEqual(len(production.procurement_group_id.mrp_production_ids), nb_product_todo) + + def test_tracking_backorder_series_serial_1(self): + """ Create a MO of 4 tracked products (serial) with pbm_sam. + all component is tracked by serial + Produce one by one with one bakorder for each until end. + """ + nb_product_todo = 4 + production, _, p_final, p1, p2 = self.generate_mo(qty_final=nb_product_todo, tracking_final='serial', tracking_base_1='serial', tracking_base_2='serial', qty_base_1=1) + serials_final, serials_p1, serials_p2 = [], [], [] + for i in range(nb_product_todo): + serials_final.append(self.env['stock.production.lot'].create({ + 'name': f'lot_final_{i}', + 'product_id': p_final.id, + 'company_id': self.env.company.id, + })) + serials_p1.append(self.env['stock.production.lot'].create({ + 'name': f'lot_consumed_1_{i}', + 'product_id': p1.id, + 'company_id': self.env.company.id, + })) + serials_p2.append(self.env['stock.production.lot'].create({ + 'name': f'lot_consumed_2_{i}', + 'product_id': p2.id, + 'company_id': self.env.company.id, + })) + self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 1, lot_id=serials_p1[-1]) + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 1, lot_id=serials_p2[-1]) + + production.action_assign() + active_production = production + for i in range(nb_product_todo): + + details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.product_id == p1), view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.edit(0) as ml: + ml.qty_done = 1 + ml.lot_id = serials_p1[i] + details_operation_form.save() + details_operation_form = Form(active_production.move_raw_ids.filtered(lambda m: m.product_id == p2), view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.edit(0) as ml: + ml.qty_done = 1 + ml.lot_id = serials_p2[i] + details_operation_form.save() + + production_form = Form(active_production) + production_form.qty_producing = 1 + production_form.lot_producing_id = serials_final[i] + active_production = production_form.save() + + active_production.button_mark_done() + if i + 1 != nb_product_todo: # If last MO, don't make a backorder + action = active_production.button_mark_done() + backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context'])) + backorder.save().action_backorder() + active_production = active_production.procurement_group_id.mrp_production_ids[-1] + + self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location), nb_product_todo, f'You should have the {nb_product_todo} final product in stock') + self.assertEqual(len(production.procurement_group_id.mrp_production_ids), nb_product_todo) + + +class TestMrpWorkorderBackorder(SavepointCase): + @classmethod + def setUpClass(cls): + super(TestMrpWorkorderBackorder, 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.finished1 = cls.env['product.product'].create({ + 'name': 'finished1', + 'type': 'product', + }) + cls.compfinished1 = cls.env['product.product'].create({ + 'name': 'compfinished1', + 'type': 'product', + }) + cls.compfinished2 = cls.env['product.product'].create({ + 'name': 'compfinished2', + 'type': 'product', + }) + cls.workcenter1 = cls.env['mrp.workcenter'].create({ + 'name': 'workcenter1', + }) + cls.workcenter2 = 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, + 'product_qty': 1, + 'consumption': 'flexible', + 'type': 'normal', + 'bom_line_ids': [ + (0, 0, {'product_id': cls.compfinished1.id, 'product_qty': 1}), + (0, 0, {'product_id': cls.compfinished2.id, 'product_qty': 1}), + ], + 'operation_ids': [ + (0, 0, {'sequence': 1, 'name': 'finished operation 1', 'workcenter_id': cls.workcenter1.id}), + (0, 0, {'sequence': 2, 'name': 'finished operation 2', 'workcenter_id': cls.workcenter2.id}), + ], + }) + 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 diff --git a/addons/mrp/tests/test_bom.py b/addons/mrp/tests/test_bom.py index d57a3a100cf..8a26d960935 100644 --- a/addons/mrp/tests/test_bom.py +++ b/addons/mrp/tests/test_bom.py @@ -26,9 +26,14 @@ class TestBoM(TestMrpCommon): 'product_tmpl_id': self.product_7_template.id, 'product_uom_id': self.uom_unit.id, 'product_qty': 4.0, - 'routing_id': self.routing_2.id, 'type': 'normal', }) + test_bom.write({ + 'operation_ids': [ + (0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}), + (0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}), + ], + }) test_bom_l1 = self.env['mrp.bom.line'].create({ 'bom_id': test_bom.id, 'product_id': self.product_2.id, @@ -78,9 +83,13 @@ class TestBoM(TestMrpCommon): 'product_tmpl_id': self.product_5.product_tmpl_id.id, 'product_uom_id': self.product_5.uom_id.id, 'product_qty': 1.0, - 'routing_id': self.routing_1.id, 'type': 'phantom' }) + test_bom_1.write({ + 'operation_ids': [ + (0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}), + ], + }) test_bom_1_l1 = self.env['mrp.bom.line'].create({ 'bom_id': test_bom_1.id, 'product_id': self.product_3.id, @@ -92,9 +101,14 @@ class TestBoM(TestMrpCommon): 'product_tmpl_id': self.product_7_template.id, 'product_uom_id': self.uom_unit.id, 'product_qty': 4.0, - 'routing_id': self.routing_2.id, 'type': 'normal', }) + test_bom_2.write({ + 'operation_ids': [ + (0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}), + (0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}), + ] + }) test_bom_2_l1 = self.env['mrp.bom.line'].create({ 'bom_id': test_bom_2.id, 'product_id': self.product_2.id, @@ -143,6 +157,7 @@ class TestBoM(TestMrpCommon): 'product_tmpl_id': self.product_9.product_tmpl_id.id, 'product_uom_id': self.product_9.uom_id.id, 'product_qty': 1.0, + 'consumption': 'flexible', 'type': 'normal' }) test_bom_4 = self.env['mrp.bom'].create({ @@ -150,6 +165,7 @@ class TestBoM(TestMrpCommon): 'product_tmpl_id': self.product_10.product_tmpl_id.id, 'product_uom_id': self.product_10.uom_id.id, 'product_qty': 1.0, + 'consumption': 'flexible', 'type': 'phantom' }) test_bom_3_l1 = self.env['mrp.bom.line'].create({ @@ -278,6 +294,11 @@ class TestBoM(TestMrpCommon): bom_form_crumble.product_uom_id = uom_kg bom_crumble = bom_form_crumble.save() + workcenter = self.env['mrp.workcenter'].create({ + 'costs_hour': 10, + 'name': 'Deserts Table' + }) + with Form(bom_crumble) as bom: with bom.bom_line_ids.new() as line: line.product_id = butter @@ -287,32 +308,19 @@ class TestBoM(TestMrpCommon): line.product_id = biscuit line.product_uom_id = uom_kg line.product_qty = 6 - - workcenter = self.env['mrp.workcenter'].create({ - 'costs_hour': 10, - 'name': 'Deserts Table' - }) - - routing_form = Form(self.env['mrp.routing']) - routing_form.name = "Crumble process" - routing_crumble = routing_form.save() - - with Form(routing_crumble) as routing: - with routing.operation_ids.new() as operation: + with bom.operation_ids.new() as operation: operation.workcenter_id = workcenter operation.name = 'Prepare biscuits' operation.time_cycle_manual = 5 - with routing.operation_ids.new() as operation: + with bom.operation_ids.new() as operation: operation.workcenter_id = workcenter operation.name = 'Prepare butter' operation.time_cycle_manual = 3 - with routing.operation_ids.new() as operation: + with bom.operation_ids.new() as operation: operation.workcenter_id = workcenter operation.name = 'Mix manually' operation.time_cycle_manual = 5 - bom_crumble.routing_id = routing_crumble.id - # TEST BOM STRUCTURE VALUE WITH BOM QUANTITY report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=11, searchVariant=False) # 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes @@ -381,6 +389,13 @@ class TestBoM(TestMrpCommon): bom_form_cheese_cake.product_uom_id = self.uom_unit bom_cheese_cake = bom_form_cheese_cake.save() + workcenter_2 = self.env['mrp.workcenter'].create({ + 'name': 'cake mounting', + 'costs_hour': 20, + 'time_start': 10, + 'time_stop': 15 + }) + with Form(bom_cheese_cake) as bom: with bom.bom_line_ids.new() as line: line.product_id = cream @@ -390,29 +405,15 @@ class TestBoM(TestMrpCommon): line.product_id = crumble line.product_uom_id = uom_kg line.product_qty = 5.4 - - workcenter_2 = self.env['mrp.workcenter'].create({ - 'name': 'cake mounting', - 'costs_hour': 20, - 'time_start': 10, - 'time_stop': 15 - }) - - routing_form = Form(self.env['mrp.routing']) - routing_form.name = "Cheese cake process" - routing_cheese = routing_form.save() - - with Form(routing_cheese) as routing: - with routing.operation_ids.new() as operation: + with bom.operation_ids.new() as operation: operation.workcenter_id = workcenter operation.name = 'Mix cheese and crumble' operation.time_cycle_manual = 10 - with routing.operation_ids.new() as operation: + with bom.operation_ids.new() as operation: operation.workcenter_id = workcenter_2 operation.name = 'Cake mounting' operation.time_cycle_manual = 5 - bom_cheese_cake.routing_id = routing_cheese.id # TEST CHEESE BOM STRUCTURE VALUE WITH BOM QUANTITY report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_cheese_cake.id, searchQty=60, searchVariant=False) diff --git a/addons/mrp/tests/test_byproduct.py b/addons/mrp/tests/test_byproduct.py index 58759c3e0e3..7b0ed3e0926 100644 --- a/addons/mrp/tests/test_byproduct.py +++ b/addons/mrp/tests/test_byproduct.py @@ -58,9 +58,6 @@ class TestMrpByProduct(common.TransactionCase): mnf_product_a = mnf_product_a_form.save() mnf_product_a.action_confirm() - # I compute the data of production order - context = {"active_model": "mrp.production", "active_ids": [mnf_product_a.id], "active_id": mnf_product_a.id} - # I confirm the production order. self.assertEqual(mnf_product_a.state, 'confirmed', 'Production order should be in state confirmed') @@ -71,19 +68,18 @@ class TestMrpByProduct(common.TransactionCase): # I consume and produce the production of products. # I create record for selecting mode and quantity of products to produce. - produce_form = Form(self.env['mrp.product.produce'].with_context(context)) - produce_form.qty_producing = 2.00 - product_consume = produce_form.save() + mo_form = Form(mnf_product_a) + mo_form.qty_producing = 2.00 + mnf_product_a = mo_form.save() # I finish the production order. self.assertEqual(len(mnf_product_a.move_raw_ids), 1, "Wrong consume move on production order.") - product_consume.do_produce() consume_move_c = mnf_product_a.move_raw_ids by_product_move = mnf_product_a.move_finished_ids.filtered(lambda x: x.product_id.id == self.product_b.id) # Check sub production produced quantity... self.assertEqual(consume_move_c.product_uom_qty, 4, "Wrong consumed quantity of product c.") self.assertEqual(by_product_move.product_uom_qty, 2, "Wrong produced quantity of sub product.") - mnf_product_a.post_inventory() + mnf_product_a._post_inventory() # I see that stock moves of External Hard Disk including Headset USB are done now. self.assertFalse(any(move.state != 'done' for move in moves), 'Moves are not done!') diff --git a/addons/mrp/tests/test_cancel_mo.py b/addons/mrp/tests/test_cancel_mo.py index fdbfb50531f..d305571c8b9 100644 --- a/addons/mrp/tests/test_cancel_mo.py +++ b/addons/mrp/tests/test_cancel_mo.py @@ -33,10 +33,9 @@ class TestMrpCancelMO(TestMrpCommon): # Create MO manufacturing_order = self.generate_mo()[0] # Produce some quantity - produce_form = Form(self.env['mrp.product.produce'].with_context(active_id=manufacturing_order.id)) - produce_form.qty_producing = 2 - produce = produce_form.save() - produce.do_produce() + mo_form = Form(manufacturing_order) + mo_form.qty_producing = 2 + manufacturing_order = mo_form.save() # Cancel it manufacturing_order.action_cancel() # Check it's cancelled @@ -53,14 +52,13 @@ class TestMrpCancelMO(TestMrpCommon): after post inventory. """ # Create MO - manufacturing_order = self.generate_mo()[0] + manufacturing_order = self.generate_mo(consumption='strict')[0] # Produce some quantity (not all to avoid to done the MO when post inventory) - produce_form = Form(self.env['mrp.product.produce'].with_context(active_id=manufacturing_order.id)) - produce_form.qty_producing = 2 - produce = produce_form.save() - produce.do_produce() + mo_form = Form(manufacturing_order) + mo_form.qty_producing = 2 + manufacturing_order = mo_form.save() # Post Inventory - manufacturing_order.post_inventory() + manufacturing_order._post_inventory() # Cancel the MO manufacturing_order.action_cancel() # Check MO is marked as done and its SML are done or cancelled @@ -78,44 +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 - mo_data = self.generate_mo() - manufacturing_order = mo_data[0] - bom = mo_data[1] - bom.routing_id = self.routing_1 - - 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 @@ -132,12 +92,11 @@ class TestMrpCancelMO(TestMrpCommon): # it (cannot be deleted) manufacturing_order = self.generate_mo()[0] # Produce some quantity (not all to avoid to done the MO when post inventory) - produce_form = Form(self.env['mrp.product.produce'].with_context(active_id=manufacturing_order.id)) - produce_form.qty_producing = 2 - produce = produce_form.save() - produce.do_produce() + mo_form = Form(manufacturing_order) + mo_form.qty_producing = 2 + manufacturing_order = mo_form.save() # Post Inventory - manufacturing_order.post_inventory() + manufacturing_order._post_inventory() # Unlink the MO must raises an UserError since it cannot be really cancelled self.assertEqual(manufacturing_order.exists().state, 'progress') with self.assertRaises(UserError): diff --git a/addons/mrp/tests/test_multicompany.py b/addons/mrp/tests/test_multicompany.py index 5b43be07224..6b0c3fd3a47 100644 --- a/addons/mrp/tests/test_multicompany.py +++ b/addons/mrp/tests/test_multicompany.py @@ -133,15 +133,10 @@ class TestMrpMulticompany(common.TransactionCase): }) mo_form = Form(self.env['mrp.production'].with_user(self.user_a)) mo_form.product_id = product + mo_form.lot_producing_id = lot_b mo = mo_form.save() - mo.with_user(self.user_b).action_confirm() - produce_form = Form(self.env['mrp.product.produce'].with_user(self.user_b).with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.finished_lot_id = lot_b with self.assertRaises(UserError): - produce_form.save() + mo.with_user(self.user_b).action_confirm() def test_product_produce_2(self): """Check that using a component lot of company b in the produce wizard of a production @@ -168,33 +163,17 @@ class TestMrpMulticompany(common.TransactionCase): mo_form.product_id = product mo = mo_form.save() mo.with_user(self.user_b).action_confirm() - produce_form = Form(self.env['mrp.product.produce'].with_user(self.user_b).with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - with produce_form.raw_workorder_line_ids.edit(0) as line: - line.lot_id = lot_b + mo_form = Form(mo) + mo_form.qty_producing = 1 + mo = mo_form.save() + details_operation_form = Form(mo.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.lot_id = lot_b + ml.qty_done = 1 + details_operation_form.save() with self.assertRaises(UserError): - produce_form.save() + mo.button_mark_done() - def test_workcenter_1(self): - """Check it is not possible to use a routing of Company B in a - workcenter of Company A. """ - - workcenter = self.env['mrp.workcenter'].create({ - 'name': 'WC1', - 'company_id': self.company_a.id, - 'resource_calendar_id': self.company_a.resource_calendar_id.id, - }) - with self.assertRaises(UserError): - self.env['mrp.routing'].create({ - 'name': 'WC1', - 'company_id': self.company_b.id, - 'operation_ids': [(0, 0, { - 'name': 'operation_1', - 'workcenter_id': workcenter.id, - })] - }) def test_partner_1(self): """ On a product without company, as a user of Company B, check it is not possible to use a diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index f13f41d44ae..bae3b44428f 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -8,7 +8,6 @@ from odoo.fields import Datetime as Dt from odoo.exceptions import UserError from odoo.addons.mrp.tests.common import TestMrpCommon - class TestMrpOrder(TestMrpCommon): def test_access_rights_manager(self): @@ -63,7 +62,6 @@ class TestMrpOrder(TestMrpCommon): test_date_planned = Dt.now() - timedelta(days=1) test_quantity = 2.0 - self.bom_1.routing_id = False man_order_form = Form(self.env['mrp.production'].with_user(self.user_mrp_user)) man_order_form.product_id = self.product_4 man_order_form.bom_id = self.bom_1 @@ -80,7 +78,7 @@ class TestMrpOrder(TestMrpCommon): # check production move production_move = man_order.move_finished_ids - self.assertEqual(production_move.date, test_date_planned) + self.assertAlmostEqual(production_move.date, test_date_planned + timedelta(hours=1), delta=timedelta(seconds=10)) self.assertEqual(production_move.product_id, self.product_4) self.assertEqual(production_move.product_uom, man_order.product_uom_id) self.assertEqual(production_move.product_qty, man_order.product_qty) @@ -96,16 +94,18 @@ class TestMrpOrder(TestMrpCommon): self.assertEqual(first_move.product_qty, test_quantity / self.bom_1.product_qty * self.product_4.uom_id.factor_inv * 4) # produce product - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': man_order.id, - 'active_ids': [man_order.id], - })) - produce_form.qty_producing = 1.0 - produce_wizard = produce_form.save() - produce_wizard.do_produce() + mo_form = Form(man_order) + mo_form.qty_producing = 1.0 + man_order = mo_form.save() - man_order.button_mark_done() - self.assertEqual(man_order.state, 'done', "Production order should be in done state.") + action = man_order.button_mark_done() + self.assertEqual(man_order.state, 'progress', "Production order should be open a backorder wizard, then not done yet.") + + quantity_issues = man_order._get_consumption_issues() + action = man_order._action_generate_consumption_wizard(quantity_issues) + backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context'])) + backorder.save().action_close_mo() + self.assertEqual(man_order.state, 'done', "Production order should be done.") def test_production_avialability(self): """ Checks the availability of a production order through mutliple calls to `action_assign`. @@ -149,31 +149,25 @@ class TestMrpOrder(TestMrpCommon): # check sub product availability state is assigned self.assertEqual(production_2.reservation_state, 'assigned', 'Production order should be availability for assigned state') - def test_empty_routing(self): - """ Check what happens when you work with an empty routing""" - routing = self.env['mrp.routing'].create({'name': 'Routing without operations'}) - self.bom_3.routing_id = routing.id - production_form = Form(self.env['mrp.production']) - production_form.product_id = self.product_6 - production = production_form.save() - self.assertEqual(production.routing_id.id, False, 'The routing field should be empty on the mo') - def test_split_move_line(self): """ Consume more component quantity than the initial demand. It should create extra move and share the quantity between the two stock moves """ mo, bom, p_final, p1, p2 = self.generate_mo(qty_base_1=10, qty_final=1, qty_base_2=1) - bom.consumption = 'flexible' mo.action_assign() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - for i in range(len(produce_form.raw_workorder_line_ids)): - with produce_form.raw_workorder_line_ids.edit(i) as line: - line.qty_done += 1 - product_produce = produce_form.save() - product_produce.do_produce() + # check is_quantity_done_editable + mo_form = Form(mo) + mo_form.qty_producing = 1 + mo = mo_form.save() + details_operation_form = Form(mo.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 = 2 + details_operation_form.save() + details_operation_form = Form(mo.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 = 11 + details_operation_form.save() + self.assertEqual(len(mo.move_raw_ids), 2) self.assertEqual(len(mo.move_raw_ids.mapped('move_line_ids')), 2) self.assertEqual(mo.move_raw_ids[0].move_line_ids.mapped('qty_done'), [2]) @@ -186,99 +180,6 @@ class TestMrpOrder(TestMrpCommon): self.assertEqual(mo.move_raw_ids.mapped('quantity_done'), [1, 10, 1, 1]) self.assertEqual(mo.move_raw_ids.mapped('move_line_ids.qty_done'), [1, 10, 1, 1]) - def test_multiple_post_inventory(self): - """ Check the consumed quants of the produced quants when intermediate calls to `post_inventory` during a MO.""" - - # create a bom for `custom_laptop` with components that aren't tracked - unit = self.ref("uom.product_uom_unit") - custom_laptop = self.env['product.product'].create({ - 'name': 'Drawer', - 'type': 'product', - 'uom_id': unit, - 'uom_po_id': unit, - }) - - product_charger = self.env['product.product'].create({ - 'name': 'Charger', - 'type': 'product', - 'uom_id': unit, - 'uom_po_id': unit}) - product_keybord = self.env['product.product'].create({ - 'name': 'Usb Keybord', - 'type': 'product', - 'uom_id': unit, - 'uom_po_id': unit}) - self.env['mrp.bom'].create({ - 'product_tmpl_id': custom_laptop.product_tmpl_id.id, - 'product_qty': 1, - 'product_uom_id': unit, - 'bom_line_ids': [(0, 0, { - 'product_id': product_charger.id, - 'product_qty': 1, - 'product_uom_id': unit - }), (0, 0, { - 'product_id': product_keybord.id, - 'product_qty': 1, - 'product_uom_id': unit - })] - }) - - # put the needed products in stock - source_location_id = self.stock_location_14.id - quant_before = custom_laptop.qty_available - 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': 2, - 'location_id': source_location_id - }), (0, 0, { - 'product_id': product_keybord.id, - 'product_uom_id': product_keybord.uom_id.id, - 'product_qty': 2, - 'location_id': source_location_id - })] - }) - inventory.action_start() - inventory.action_validate() - - # create a mo for this bom - mo_custom_laptop_form = Form(self.env['mrp.production']) - mo_custom_laptop_form.product_id = custom_laptop - mo_custom_laptop_form.product_qty = 2 - mo_custom_laptop = mo_custom_laptop_form.save() - mo_custom_laptop.action_confirm() - mo_custom_laptop.action_assign() - self.assertEqual(mo_custom_laptop.reservation_state, 'assigned') - - # produce one item, call `post_inventory` - context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id} - produce_form = Form(self.env['mrp.product.produce'].with_context(context)) - produce_form.qty_producing = 1.00 - custom_laptop_produce = produce_form.save() - custom_laptop_produce.do_produce() - mo_custom_laptop.post_inventory() - - # check the consumed quants of the produced quant - first_move = mo_custom_laptop.move_finished_ids.filtered(lambda mo: mo.state == 'done') - quant_after1 = custom_laptop.qty_available - self.assertEqual(first_move.quantity_done, 1, "Order already produce 1 product") - self.assertEqual(quant_after1 - quant_before, 1, "1 product available after production") - second_move = mo_custom_laptop.move_finished_ids.filtered(lambda mo: mo.state == 'confirmed') - self.assertEqual(second_move.quantity_done, 0, "There is still one product to pruduce") - - # produce the second item, call `post_inventory` - context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id} - produce_form = Form(self.env['mrp.product.produce'].with_context(context)) - produce_form.qty_producing = 1.00 - custom_laptop_produce = produce_form.save() - custom_laptop_produce.do_produce() - mo_custom_laptop.post_inventory() - self.assertEqual(second_move.quantity_done, 1, "Order produce the second product") - quant_after2 = custom_laptop.qty_available - self.assertEqual(quant_after2 - quant_before, 2, "2 products available after production") - def test_update_quantity_1(self): """ Build 5 final products with different consumed lots, then edit the finished quantity and update the Manufacturing @@ -306,14 +207,15 @@ class TestMrpOrder(TestMrpCommon): self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) mo.action_assign() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_wizard = produce_form.save() - produce_wizard.do_produce() + mo_form = Form(mo) + mo_form.qty_producing = 1 + mo = mo_form.save() - mo.move_finished_ids.move_line_ids.qty_done -= 1 + details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = lot_1 + ml.qty_done = 20 + details_operation_form.save() update_quantity_wizard = self.env['change.production.qty'].create({ 'mo_id': mo.id, 'product_qty': 4, @@ -338,67 +240,25 @@ class TestMrpOrder(TestMrpCommon): self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) mo.action_assign() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 2 - produce_wizard = produce_form.save() - produce_wizard.do_produce() + mo_form = Form(mo) + mo_form.qty_producing = 2 + mo = mo_form.save() + + mo._post_inventory() - mo.post_inventory() update_quantity_wizard = self.env['change.production.qty'].create({ 'mo_id': mo.id, 'product_qty': 5, }) update_quantity_wizard.change_prod_qty() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_wizard = produce_form.save() - produce_wizard.do_produce() + mo_form = Form(mo) + mo_form.qty_producing = 5 + mo = mo_form.save() mo.button_mark_done() self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).mapped('quantity_done')), 20) self.assertEqual(sum(mo.move_finished_ids.mapped('quantity_done')), 5) - def test_update_quantity_3(self): - """ Build 1 final products then update the Manufacturing - order quantity. Check the remaining quantity to produce - take care of the first quantity produced.""" - self.stock_location = self.env.ref('stock.stock_location_stock') - mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=2) - self.assertEqual(len(mo), 1, 'MO should have been created') - - self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 20) - self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) - mo.action_assign() - - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 1 - produce_wizard = produce_form.save() - produce_wizard.do_produce() - - update_quantity_wizard = self.env['change.production.qty'].create({ - 'mo_id': mo.id, - 'product_qty': 3, - }) - update_quantity_wizard.change_prod_qty() - - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_wizard = produce_form.save() - produce_wizard.do_produce() - mo.button_mark_done() - self.assertEqual(sum(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).mapped('quantity_done')), 12) - self.assertEqual(sum(mo.move_finished_ids.mapped('quantity_done')), 3) - def test_rounding(self): """ Checks we round up when bringing goods to produce and round half-up when producing. This implementation allows to implement an efficiency notion (see rev 347f140fe63612ee05e). @@ -427,13 +287,9 @@ class TestMrpOrder(TestMrpCommon): self.assertEqual(production.move_raw_ids[1].product_qty, 84, 'The quantity should be rounded up') # produce product - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': production.id, - 'active_ids': [production.id], - })) - produce_form.qty_producing = 8 - produce_wizard = produce_form.save() - produce_wizard.do_produce() + mo_form = Form(production) + mo_form.qty_producing = 8 + production = mo_form.save() self.assertEqual(production.move_raw_ids[0].quantity_done, 16, 'Should use half-up rounding when producing') self.assertEqual(production.move_raw_ids[1].quantity_done, 34, 'Should use half-up rounding when producing') @@ -448,27 +304,21 @@ class TestMrpOrder(TestMrpCommon): mo.action_assign() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) # change the quantity done in one line - produce_form.raw_workorder_line_ids._records[0]['qty_done'] = 1 + details_operation_form = Form(mo.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 = 1 + details_operation_form.save() # change the quantity producing - produce_form.qty_producing = 3 + mo_form = Form(mo) + mo_form.qty_producing = 3 # check than all quantities are update correctly - line1 = produce_form.raw_workorder_line_ids._records[0] - line2 = produce_form.raw_workorder_line_ids._records[1] - self.assertEqual(line1['qty_to_consume'], 3, "Wrong quantity to consume") - self.assertEqual(line1['qty_done'], 3, "Wrong quantity done") - self.assertEqual(line2['qty_to_consume'], 12, "Wrong quantity to consume") - self.assertEqual(line2['qty_done'], 12, "Wrong quantity done") - - product_produce = produce_form.save() - self.assertEqual(len(product_produce.raw_workorder_line_ids), 2, 'You should have produce lines even the consumed products are not tracked.') - product_produce.do_produce() + self.assertEqual(mo_form.move_raw_ids._records[0]['product_uom_qty'], 5, "Wrong quantity to consume") + self.assertEqual(mo_form.move_raw_ids._records[0]['quantity_done'], 3, "Wrong quantity done") + self.assertEqual(mo_form.move_raw_ids._records[1]['product_uom_qty'], 20, "Wrong quantity to consume") + self.assertEqual(mo_form.move_raw_ids._records[1]['quantity_done'], 12, "Wrong quantity done") def test_product_produce_2(self): """ Checks that, for a BOM where one of the components is tracked by serial number and the @@ -495,42 +345,42 @@ class TestMrpOrder(TestMrpCommon): self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) mo.action_assign() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - self.assertEqual(len(produce_form.raw_workorder_line_ids), 3, 'You should have 3 produce lines. One for each serial to consume and for the untracked product.') - produce_form.qty_producing = 1 + self.assertEqual(len(mo.move_raw_ids.move_line_ids), 3, 'You should have 3 stock move lines. One for each serial to consume and for the untracked product.') + mo_form = Form(mo) + mo_form.qty_producing = 1 + mo = mo_form.save() # get the proposed lot - consumed_lots = self.env['stock.production.lot'] - for workorder_line in produce_form.raw_workorder_line_ids._records: - if workorder_line['product_id'] == p1.id: - consumed_lots |= self.env['stock.production.lot'].browse(workorder_line['lot_id']) - consumed_lots.ensure_one() - product_produce = produce_form.save() - product_produce.do_produce() + details_operation_form = Form(mo.move_raw_ids.filtered(lambda move: move.product_id == p1), view=self.env.ref('stock.view_stock_move_operations')) + self.assertEqual(len(details_operation_form.move_line_ids), 2) + with details_operation_form.move_line_ids.edit(0) as ml: + consumed_lots = ml.lot_id + ml.qty_done = 1 + details_operation_form.save() remaining_lot = (lot_p1_1 | lot_p1_2) - consumed_lots remaining_lot.ensure_one() + action = mo.button_mark_done() + backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context'])) + backorder.save().action_backorder() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - product_produce = produce_form.save() - self.assertEqual(len(product_produce.raw_workorder_line_ids), 2, 'You should have 2 produce lines left.') - for line in product_produce.raw_workorder_line_ids.filtered(lambda x: x.lot_id): - self.assertEqual(line.lot_id, remaining_lot, 'Wrong lot proposed.') + # Check MO backorder + mo_backorder = mo.procurement_group_id.mrp_production_ids[-1] + + mo_form = Form(mo_backorder) + mo_form.qty_producing = 1 + mo_backorder = mo_form.save() + details_operation_form = Form(mo_backorder.move_raw_ids.filtered(lambda move: move.product_id == p1), view=self.env.ref('stock.view_stock_move_operations')) + self.assertEqual(len(details_operation_form.move_line_ids), 1) + with details_operation_form.move_line_ids.edit(0) as ml: + self.assertEqual(ml.lot_id, remaining_lot) def test_product_produce_3(self): """ Checks that, for a BOM where one of the components is tracked by lot and the other is not tracked, when creating a manufacturing order for 1 finished product and reserving, the - produce wizard proposes the corrects lines. Then, checks the generated move lines when over - consuming. + reserved lines are displayed. Then, over-consume by creating new line. """ - # FIXME: some asserts on the quants after overproducing would be nice self.stock_location = self.env.ref('stock.stock_location_stock') self.stock_shelf_1 = self.stock_location_components @@ -538,7 +388,6 @@ class TestMrpOrder(TestMrpCommon): mo, _, p_final, p1, p2 = self.generate_mo(tracking_base_1='lot', qty_base_1=10, qty_final=1) self.assertEqual(len(mo), 1, 'MO should have been created') - mo.bom_id.consumption = 'flexible' # Because we will over consume. first_lot_for_p1 = self.env['stock.production.lot'].create({ 'name': 'lot1', 'product_id': p1.id, @@ -562,22 +411,31 @@ class TestMrpOrder(TestMrpCommon): self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) mo.action_assign() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 1.0 - for i in range(len(produce_form.raw_workorder_line_ids)): - with produce_form.raw_workorder_line_ids.edit(i) as line: - line.qty_done += 1 - product_produce = produce_form.save() - product_produce.finished_lot_id = final_product_lot.id - # product 1 lot 1 shelf1 - # product 1 lot 1 shelf2 - # product 1 lot 2 - self.assertEqual(len(product_produce.raw_workorder_line_ids), 4, 'You should have 4 produce lines. lot 1 shelf_1, lot 1 shelf_2, lot2 and for product which have tracking None') + mo_form = Form(mo) + mo_form.qty_producing = 1.0 + mo_form.lot_producing_id = final_product_lot + mo = mo_form.save() + # p2 + details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.edit(0) as line: + line.qty_done = line.product_uom_qty + with details_operation_form.move_line_ids.new() as line: + line.qty_done = 1 + details_operation_form.save() - product_produce.do_produce() + # p1 + details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations')) + for i in range(len(details_operation_form.move_line_ids)): + # reservation in shelf1: 3 lot1, shelf2: 3 lot1, stock: 4 lot2 + with details_operation_form.move_line_ids.edit(i) as line: + line.qty_done = line.product_uom_qty + with details_operation_form.move_line_ids.new() as line: + line.qty_done = 2 + line.lot_id = first_lot_for_p1 + with details_operation_form.move_line_ids.new() as line: + line.qty_done = 1 + line.lot_id = second_lot_for_p1 + details_operation_form.save() move_1 = mo.move_raw_ids.filtered(lambda m: m.product_id == p1) # qty_done/product_uom_qty lot @@ -618,18 +476,15 @@ class TestMrpOrder(TestMrpCommon): ml_p1[0].qty_done = 1.0 # Produce baby! - product_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - product_produce = product_form.save() - product_produce.do_produce() + mo_form = Form(mo) + mo_form.qty_producing = 1 + mo = mo_form.save() m_p1 = mo.move_raw_ids.filtered(lambda x: x.product_id == p1) ml_p1 = m_p1.mapped('move_line_ids') - self.assertEqual(len(ml_p1), 3) - self.assertEqual(sorted(ml_p1.mapped('qty_done')), [1.0, 2.0, 3.0], 'Quantity done should be 1.0, 2.0 or 3.0') - self.assertEqual(m_p1.quantity_done, 6.0, 'Total qty done should be 6.0') + self.assertEqual(len(ml_p1), 2) + self.assertEqual(sorted(ml_p1.mapped('qty_done')), [2.0, 3.0], 'Quantity done should be 1.0, 2.0 or 3.0') + self.assertEqual(m_p1.quantity_done, 5.0, 'Total qty done should be 6.0') self.assertEqual(sum(ml_p1.mapped('product_uom_qty')), 5.0, 'Total qty reserved should be 5.0') mo.button_mark_done() @@ -638,7 +493,7 @@ class TestMrpOrder(TestMrpCommon): def test_product_produce_6(self): """ Plan 5 finished products, reserve and produce 3. Post the current production. Simulate an unlock and edit and, on the opened moves, set the consumed quantity - to 3. Now, try to update the quantity to produce to 3. It should fail since there + to 3. Now, try to update the quantity to mo2 to 3. It should fail since there are consumed quantities. Unlock and edit, remove the consumed quantities and update the quantity to produce to 3.""" self.stock_location = self.env.ref('stock.stock_location_stock') @@ -650,15 +505,11 @@ class TestMrpOrder(TestMrpCommon): self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) mo.action_assign() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 3 - produce_wizard = produce_form.save() - produce_wizard.do_produce() + mo_form = Form(mo) + mo_form.qty_producing = 3 + mo = mo_form.save() - mo.post_inventory() + mo._post_inventory() self.assertEqual(len(mo.move_raw_ids), 4) mo.move_raw_ids.filtered(lambda m: m.state != 'done')[0].quantity_done = 3 @@ -677,125 +528,118 @@ class TestMrpOrder(TestMrpCommon): self.assertTrue(all(s == 'done' for s in mo.move_raw_ids.mapped('state'))) self.assertEqual(sum(mo.move_raw_ids.mapped('move_line_ids.product_uom_qty')), 0) - def test_product_produce_7(self): - """ Add components in 2 different sub location. Do not reserve the MO - and checks that the move line created takes stock from location that - contains needed raw materials. - """ - mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=2) + def test_consumption_strict_1(self): + """ Checks the constraints of a strict BOM without tracking when playing around + quantities to consume.""" + self.stock_location = self.env.ref('stock.stock_location_stock') + mo, bom, p_final, p1, p2 = self.generate_mo(consumption='strict', qty_final=1) self.assertEqual(len(mo), 1, 'MO should have been created') - self.stock_location = self.env.ref('stock.stock_location_stock') - self.stock_shelf_1 = self.stock_location_components - self.stock_shelf_2 = self.stock_location_14 - - self.env['stock.quant']._update_available_quantity(p1, self.stock_shelf_1, 3) - self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 3) - self.env['stock.quant']._update_available_quantity(p1, self.stock_shelf_2, 2) - - self.env['stock.quant']._update_available_quantity(p2, self.stock_shelf_1, 1) - self.env['stock.quant']._update_available_quantity(p2, self.stock_shelf_2, 1) - - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 1 - produce_wizard = produce_form.save() - - self.assertEqual(len(produce_wizard.raw_workorder_line_ids), 2) - produce_wizard.do_produce() - - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 1 - - produce_wizard = produce_form.save() - - self.assertEqual(len(produce_wizard.raw_workorder_line_ids), 2) - produce_wizard.do_produce() - - mo.button_mark_done() - mo_move_line_p1 = mo.move_raw_ids[1].move_line_ids - self.assertEqual(sum(mo_move_line_p1.filtered(lambda ml: ml.location_id == self.stock_location).mapped('qty_done')), 3) - self.assertEqual(sum(mo_move_line_p1.filtered(lambda ml: ml.location_id == self.stock_shelf_1).mapped('qty_done')), 3) - self.assertEqual(sum(mo_move_line_p1.filtered(lambda ml: ml.location_id == self.stock_shelf_2).mapped('qty_done')), 2) - self.assertEqual(sum(mo.move_finished_ids.move_line_ids.mapped('qty_done')), 2) - - self.assertEqual(self.env['stock.quant']._gather(p1, self.stock_location, strict=True).quantity, 0) - self.assertEqual(self.env['stock.quant']._gather(p1, self.stock_shelf_1, strict=True).quantity, 0) - self.assertEqual(self.env['stock.quant']._gather(p1, self.stock_shelf_2, strict=True).quantity, 0) - - self.assertEqual(self.env['stock.quant']._gather(p2, self.stock_shelf_1, strict=True).quantity, 0) - self.assertEqual(self.env['stock.quant']._gather(p2, self.stock_shelf_2, strict=True).quantity, 0) - self.assertEqual(self.env['stock.quant']._gather(p_final, self.stock_location, strict=True).quantity, 2) - - def test_product_produce_8(self): - """ Produce more than reserved and planned. Check that produce wizard - only propose one line for product not reserved. - """ - mo, bom, p_final, p1, p2 = self.generate_mo(qty_final=2) - self.assertEqual(len(mo), 1, 'MO should have been created') - - self.stock_location = self.env.ref('stock.stock_location_stock') - - self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 5) - self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 2) + self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100) + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) mo.action_assign() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 1 - produce_wizard = produce_form.save() - self.assertEqual(len(produce_wizard.raw_workorder_line_ids), 2) - self.assertEqual(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p1).qty_reserved, 4) - self.assertEqual(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p2).qty_reserved, 1) - produce_wizard.do_produce() - - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 1 - produce_wizard = produce_form.save() - # p1 1 1 1 - # p1 3 0 3 - # p2 1 1 1 - self.assertEqual(len(produce_wizard.raw_workorder_line_ids), 3) - self.assertEqual(sum(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p1).mapped('qty_reserved')), 1) - self.assertEqual(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p1 and l.qty_reserved).qty_to_consume, 1) - self.assertEqual(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p1 and not l.qty_reserved).qty_to_consume, 3) - self.assertEqual(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p2).qty_reserved, 1) - - with Form(produce_wizard) as produce_form: - produce_form.qty_producing = 2 - # p1 1 1 1 - # p1 7 0 7 - # p2 1 1 1 - # p2 1 0 1 - self.assertEqual(len(produce_wizard.raw_workorder_line_ids), 4) - self.assertEqual(sum(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p1).mapped('qty_reserved')), 1) - self.assertEqual(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p1 and l.qty_reserved).qty_to_consume, 1) - self.assertEqual(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p1 and not l.qty_reserved).qty_to_consume, 7) - self.assertEqual(sum(produce_wizard.raw_workorder_line_ids.filtered(lambda l: l.product_id == p2).mapped('qty_reserved')), 1) - - produce_wizard.do_produce() + mo_form = Form(mo) + # try adding another line for a bom product to increase the quantity + mo_form.qty_producing = 1 + with mo_form.move_raw_ids.new() as line: + line.product_id = p1 + mo = mo_form.save() + details_operation_form = Form(mo.move_raw_ids[-1], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.qty_done = 1 + details_operation_form.save() + # Won't accept to be done, instead return a wizard mo.button_mark_done() + self.assertEqual(mo.state, 'to_close') + consumption_issues = mo._get_consumption_issues() + action = mo._action_generate_consumption_wizard(consumption_issues) + warning = Form(self.env['mrp.consumption.warning'].with_context(**action['context'])) + warning = warning.save() - self.assertEqual(self.env['stock.quant']._gather(p1, self.stock_location, strict=True).quantity, -7) - self.assertEqual(self.env['stock.quant']._gather(p2, self.stock_location, strict=True).quantity, -1) - self.assertEqual(self.env['stock.quant']._gather(p_final, self.stock_location, strict=True).quantity, 3) + self.assertEqual(len(warning.mrp_consumption_warning_line_ids), 1) + self.assertEqual(warning.mrp_consumption_warning_line_ids[0].product_consumed_qty_uom, 5) + self.assertEqual(warning.mrp_consumption_warning_line_ids[0].product_expected_qty_uom, 4) + # Force the warning (as a manager) + warning.action_confirm() + self.assertEqual(mo.state, 'done') + + def test_consumption_warning_1(self): + """ Checks the constraints of a strict BOM without tracking when playing around + quantities to consume.""" + self.stock_location = self.env.ref('stock.stock_location_stock') + mo, bom, p_final, p1, p2 = self.generate_mo(consumption='warning', qty_final=1) + self.assertEqual(len(mo), 1, 'MO should have been created') + + self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100) + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) + + mo.action_assign() + + mo_form = Form(mo) + + # try adding another line for a bom product to increase the quantity + mo_form.qty_producing = 1 + with mo_form.move_raw_ids.new() as line: + line.product_id = p1 + mo = mo_form.save() + details_operation_form = Form(mo.move_raw_ids[-1], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.qty_done = 1 + details_operation_form.save() + + # Won't accept to be done, instead return a wizard + mo.button_mark_done() + self.assertEqual(mo.state, 'to_close') + + consumption_issues = mo._get_consumption_issues() + action = mo._action_generate_consumption_wizard(consumption_issues) + warning = Form(self.env['mrp.consumption.warning'].with_context(**action['context'])) + warning = warning.save() + + self.assertEqual(len(warning.mrp_consumption_warning_line_ids), 1) + self.assertEqual(warning.mrp_consumption_warning_line_ids[0].product_consumed_qty_uom, 5) + self.assertEqual(warning.mrp_consumption_warning_line_ids[0].product_expected_qty_uom, 4) + # Force the warning (as a manager or employee) + warning.action_confirm() + self.assertEqual(mo.state, 'done') + + def test_consumption_flexible_1(self): + """ Checks the constraints of a strict BOM without tracking when playing around + quantities to consume.""" + self.stock_location = self.env.ref('stock.stock_location_stock') + mo, bom, p_final, p1, p2 = self.generate_mo(consumption='flexible', qty_final=1) + self.assertEqual(len(mo), 1, 'MO should have been created') + + self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100) + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) + + mo.action_assign() + + mo_form = Form(mo) + + # try adding another line for a bom product to increase the quantity + mo_form.qty_producing = 1 + with mo_form.move_raw_ids.new() as line: + line.product_id = p1 + mo = mo_form.save() + details_operation_form = Form(mo.move_raw_ids[-1], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.qty_done = 1 + details_operation_form.save() + + # Won't accept to be done, instead return a wizard + mo.button_mark_done() + self.assertEqual(mo.state, 'done') def test_product_produce_9(self): - """ Checks the constraints of a strict BOM without tracking when playing around in the - produce wizard. - """ + """ Checks the production wizard contains lines even for untracked products. """ + serial = self.env['product.product'].create({ + 'name': 'S1', + 'tracking': 'serial', + }) self.stock_location = self.env.ref('stock.stock_location_stock') mo, bom, p_final, p1, p2 = self.generate_mo() self.assertEqual(len(mo), 1, 'MO should have been created') @@ -804,53 +648,14 @@ class TestMrpOrder(TestMrpCommon): self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) mo.action_assign() + mo_form = Form(mo) - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - - with self.assertRaises(UserError): - # try adding another line for a bom product to increase the quantity - produce_form.qty_producing = 1 - with produce_form.raw_workorder_line_ids.new() as line: - line.product_id = p1 - line.qty_done = 1 - product_produce = produce_form.save() - product_produce.do_produce() - - with self.assertRaises(UserError): - # Try updating qty_done - product_produce = produce_form.save() - product_produce.raw_workorder_line_ids[0].qty_done += 1 - product_produce.do_produce() - - with self.assertRaises(UserError): - # try adding another product - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 1 - with produce_form.raw_workorder_line_ids.new() as line: - line.product_id = self.product_4 - line.qty_done = 1 - product_produce = produce_form.save() - product_produce.do_produce() - - # try adding another line for a bom product but the total quantity is good - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 1 - - with produce_form.raw_workorder_line_ids.new() as line: - line.product_id = p1 - line.qty_done = 1 - product_produce = produce_form.save() - product_produce.raw_workorder_line_ids[1].qty_done -= 1 - product_produce.do_produce() + # change the quantity done in one line + with self.assertRaises(AssertionError): + with mo_form.move_raw_ids.new() as move: + move.product_id = serial + move.quantity_done = 2 + mo_form.save() def test_product_produce_10(self): """ Produce byproduct with serial, lot and not tracked. @@ -911,8 +716,6 @@ class TestMrpOrder(TestMrpCommon): bp.product_qty = 2.0 bp.product_uom_id = dozen - self.bom_1.routing_id = False - mo_form = Form(self.env['mrp.production']) mo_form.product_id = self.product_4 mo_form.bom_id = self.bom_1 @@ -920,84 +723,95 @@ class TestMrpOrder(TestMrpCommon): mo = mo_form.save() mo.action_confirm() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - self.assertEqual(len(produce_form.finished_workorder_line_ids), 4) - produce_wizard = produce_form.save() - wokorder_lines_byproduct_1 = produce_wizard.finished_workorder_line_ids.filtered(lambda l: l.product_id == self.byproduct1) - self.assertEqual(len(wokorder_lines_byproduct_1), 2) - self.assertEqual(wokorder_lines_byproduct_1.mapped('qty_to_consume'), [1.0, 1.0]) - self.assertEqual(wokorder_lines_byproduct_1.mapped('qty_done'), [1.0, 1.0]) - wokorder_lines_byproduct_2 = produce_wizard.finished_workorder_line_ids.filtered(lambda l: l.product_id == self.byproduct2) - self.assertEqual(len(wokorder_lines_byproduct_2), 1) - self.assertEqual(wokorder_lines_byproduct_2.qty_to_consume, 4.0) - self.assertEqual(wokorder_lines_byproduct_2.qty_done, 4.0) + move_byproduct_1 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct1) + self.assertEqual(len(move_byproduct_1), 1) + self.assertEqual(move_byproduct_1.product_uom_qty, 2.0) + self.assertEqual(move_byproduct_1.quantity_done, 0) + self.assertEqual(len(move_byproduct_1.move_line_ids), 0) - wokorder_lines_byproduct_3 = produce_wizard.finished_workorder_line_ids.filtered(lambda l: l.product_id == self.byproduct3) - self.assertEqual(wokorder_lines_byproduct_3.qty_to_consume, 4.0) - self.assertEqual(wokorder_lines_byproduct_3.qty_done, 4.0) - self.assertEqual(wokorder_lines_byproduct_3.product_uom_id, dozen) + move_byproduct_2 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct2) + self.assertEqual(len(move_byproduct_2), 1) + self.assertEqual(move_byproduct_2.product_uom_qty, 4.0) + self.assertEqual(move_byproduct_2.quantity_done, 0) + self.assertEqual(len(move_byproduct_2.move_line_ids), 0) - produce_form = Form(produce_wizard) - produce_form.qty_producing = 1.0 - self.assertEqual(len(produce_form.finished_workorder_line_ids), 3) - produce_wizard = produce_form.save() - wokorder_lines_byproduct_1 = produce_wizard.finished_workorder_line_ids.filtered(lambda l: l.product_id == self.byproduct1) - self.assertEqual(len(wokorder_lines_byproduct_1), 1) - self.assertEqual(wokorder_lines_byproduct_1.qty_to_consume, 1.0) - self.assertEqual(wokorder_lines_byproduct_1.qty_done, 1.0) - wokorder_lines_byproduct_2 = produce_wizard.finished_workorder_line_ids.filtered(lambda l: l.product_id == self.byproduct2) - self.assertEqual(len(wokorder_lines_byproduct_2), 1) - self.assertEqual(wokorder_lines_byproduct_2.qty_to_consume, 2.0) - self.assertEqual(wokorder_lines_byproduct_2.qty_done, 2.0) + move_byproduct_3 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct3) + self.assertEqual(move_byproduct_3.product_uom_qty, 4.0) + self.assertEqual(move_byproduct_3.quantity_done, 0) + self.assertEqual(move_byproduct_3.product_uom, dozen) + self.assertEqual(len(move_byproduct_3.move_line_ids), 0) - wokorder_lines_byproduct_3 = produce_wizard.finished_workorder_line_ids.filtered(lambda l: l.product_id == self.byproduct3) - self.assertEqual(wokorder_lines_byproduct_3.qty_to_consume, 2.0) - self.assertEqual(wokorder_lines_byproduct_3.qty_done, 2.0) - self.assertEqual(wokorder_lines_byproduct_3.product_uom_id, dozen) + mo_form = Form(mo) + mo_form.qty_producing = 1.0 + mo = mo_form.save() + move_byproduct_1 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct1) + self.assertEqual(len(move_byproduct_1), 1) + self.assertEqual(move_byproduct_1.product_uom_qty, 2.0) + self.assertEqual(move_byproduct_1.quantity_done, 0) - produce_form = Form(produce_wizard) - wokorder_lines_byproduct_1.lot_id = self.serial_1 - wokorder_lines_byproduct_2.lot_id = self.lot_1 - produce_wizard = produce_form.save() - produce_wizard.do_produce() + move_byproduct_2 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct2) + self.assertEqual(len(move_byproduct_2), 1) + self.assertEqual(move_byproduct_2.product_uom_qty, 4.0) + self.assertEqual(move_byproduct_2.quantity_done, 0) - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - self.assertEqual(produce_form.qty_producing, 1.0) - self.assertEqual(len(produce_form.finished_workorder_line_ids), 3) - produce_wizard = produce_form.save() + move_byproduct_3 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct3) + self.assertEqual(move_byproduct_3.product_uom_qty, 4.0) + self.assertEqual(move_byproduct_3.quantity_done, 2.0) + self.assertEqual(move_byproduct_3.product_uom, dozen) - wokorder_lines_byproduct_1 = produce_wizard.finished_workorder_line_ids.filtered(lambda l: l.product_id == self.byproduct1) - self.assertEqual(len(wokorder_lines_byproduct_1), 1) - self.assertEqual(wokorder_lines_byproduct_1.qty_to_consume, 1.0) - self.assertEqual(wokorder_lines_byproduct_1.qty_done, 1.0) - wokorder_lines_byproduct_2 = produce_wizard.finished_workorder_line_ids.filtered(lambda l: l.product_id == self.byproduct2) - self.assertEqual(len(wokorder_lines_byproduct_2), 1) - self.assertEqual(wokorder_lines_byproduct_2.qty_to_consume, 2.0) - self.assertEqual(wokorder_lines_byproduct_2.qty_done, 2.0) + details_operation_form = Form(move_byproduct_1, view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = self.serial_1 + ml.qty_done = 1 + details_operation_form.save() + details_operation_form = Form(move_byproduct_2, view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = self.lot_1 + ml.qty_done = 2 + details_operation_form.save() + action = mo.button_mark_done() + backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context'])) + backorder.save().action_backorder() + mo2 = mo.procurement_group_id.mrp_production_ids[-1] - wokorder_lines_byproduct_3 = produce_wizard.finished_workorder_line_ids.filtered(lambda l: l.product_id == self.byproduct3) - self.assertEqual(wokorder_lines_byproduct_3.qty_to_consume, 2.0) - self.assertEqual(wokorder_lines_byproduct_3.qty_done, 2.0) - self.assertEqual(wokorder_lines_byproduct_3.product_uom_id, dozen) + mo_form = Form(mo2) + mo_form.qty_producing = 1 + mo2 = mo_form.save() - produce_form = Form(produce_wizard) - wokorder_lines_byproduct_1.lot_id = self.serial_2 - wokorder_lines_byproduct_2.lot_id = self.lot_2 - wokorder_lines_byproduct_3.qty_done = 3.0 - produce_wizard = produce_form.save() + move_byproduct_1 = mo2.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct1) + self.assertEqual(len(move_byproduct_1), 1) + self.assertEqual(move_byproduct_1.product_uom_qty, 1.0) + self.assertEqual(move_byproduct_1.quantity_done, 0) - produce_wizard.do_produce() + move_byproduct_2 = mo2.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct2) + self.assertEqual(len(move_byproduct_2), 1) + self.assertEqual(move_byproduct_2.product_uom_qty, 2.0) + self.assertEqual(move_byproduct_2.quantity_done, 0) - mo.button_mark_done() - move_lines_byproduct_1 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct1).mapped('move_line_ids') - move_lines_byproduct_2 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct2).mapped('move_line_ids') - move_lines_byproduct_3 = mo.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct3).mapped('move_line_ids') + move_byproduct_3 = mo2.move_finished_ids.filtered(lambda l: l.product_id == self.byproduct3) + self.assertEqual(move_byproduct_3.product_uom_qty, 2.0) + self.assertEqual(move_byproduct_3.quantity_done, 2.0) + self.assertEqual(move_byproduct_3.product_uom, dozen) + + details_operation_form = Form(move_byproduct_1, view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = self.serial_2 + ml.qty_done = 1 + details_operation_form.save() + details_operation_form = Form(move_byproduct_2, view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = self.lot_2 + ml.qty_done = 2 + details_operation_form.save() + details_operation_form = Form(move_byproduct_3, view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.edit(0) as ml: + ml.qty_done = 3 + details_operation_form.save() + + mo2.button_mark_done() + move_lines_byproduct_1 = (mo | mo2).move_finished_ids.filtered(lambda l: l.product_id == self.byproduct1).mapped('move_line_ids') + move_lines_byproduct_2 = (mo | mo2).move_finished_ids.filtered(lambda l: l.product_id == self.byproduct2).mapped('move_line_ids') + move_lines_byproduct_3 = (mo | mo2).move_finished_ids.filtered(lambda l: l.product_id == self.byproduct3).mapped('move_line_ids') self.assertEqual(move_lines_byproduct_1.filtered(lambda ml: ml.lot_id == self.serial_1).qty_done, 1.0) self.assertEqual(move_lines_byproduct_1.filtered(lambda ml: ml.lot_id == self.serial_2).qty_done, 1.0) self.assertEqual(move_lines_byproduct_2.filtered(lambda ml: ml.lot_id == self.lot_1).qty_done, 2.0) @@ -1020,43 +834,44 @@ class TestMrpOrder(TestMrpCommon): mo.bom_id.consumption = 'flexible' # Because we'll over-consume with a product not defined in the BOM mo.action_assign() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 3 - self.assertEqual(len(produce_form.raw_workorder_line_ids._records), 4, 'Update the produce quantity should change the components quantity.') - self.assertEqual(sum([x['qty_done'] for x in produce_form.raw_workorder_line_ids._records]), 15, 'Update the produce quantity should change the components quantity.') - self.assertEqual(sum([x['qty_reserved'] for x in produce_form.raw_workorder_line_ids._records]), 5, 'Update the produce quantity should not change the components reserved quantity.') - produce_form.qty_producing = 4 - self.assertEqual(len(produce_form.raw_workorder_line_ids._records), 4, 'Update the produce quantity should change the components quantity.') - self.assertEqual(sum([x['qty_done'] for x in produce_form.raw_workorder_line_ids._records]), 20, 'Update the produce quantity should change the components quantity.') - self.assertEqual(sum([x['qty_reserved'] for x in produce_form.raw_workorder_line_ids._records]), 5, 'Update the produce quantity should not change the components reserved quantity.') - - produce_form.qty_producing = 1 - self.assertEqual(len(produce_form.raw_workorder_line_ids._records), 2, 'Update the produce quantity should change the components quantity.') - self.assertEqual(sum([x['qty_done'] for x in produce_form.raw_workorder_line_ids._records]), 5, 'Update the produce quantity should change the components quantity.') - self.assertEqual(sum([x['qty_reserved'] for x in produce_form.raw_workorder_line_ids._records]), 5, 'Update the produce quantity should not change the components reserved quantity.') + mo_form = Form(mo) + mo_form.qty_producing = 3 + self.assertEqual(sum([x['quantity_done'] for x in mo_form.move_raw_ids._records]), 15, 'Update the produce quantity should change the components quantity.') + self.assertEqual(sum([x['reserved_availability'] for x in mo_form.move_raw_ids._records]), 5, 'Update the produce quantity should not change the components reserved quantity.') + mo_form.qty_producing = 4 + self.assertEqual(sum([x['quantity_done'] for x in mo_form.move_raw_ids._records]), 20, 'Update the produce quantity should change the components quantity.') + self.assertEqual(sum([x['reserved_availability'] for x in mo_form.move_raw_ids._records]), 5, 'Update the produce quantity should not change the components reserved quantity.') + mo_form.qty_producing = 1 + self.assertEqual(sum([x['quantity_done'] for x in mo_form.move_raw_ids._records]), 5, 'Update the produce quantity should change the components quantity.') + self.assertEqual(sum([x['reserved_availability'] for x in mo_form.move_raw_ids._records]), 5, 'Update the produce quantity should not change the components reserved quantity.') # try adding another product that doesn't belong to the BoM - with produce_form.raw_workorder_line_ids.new() as line: - line.product_id = self.product_4 - line.qty_done = 1 - produce_wizard = produce_form.save() - produce_wizard.do_produce() + with mo_form.move_raw_ids.new() as move: + move.product_id = self.product_4 + mo = mo_form.save() + details_operation_form = Form(mo.move_raw_ids[-1], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.qty_done = 10 + details_operation_form.save() + # Check that this new product is not updated by qty_producing + mo_form = Form(mo) + mo_form.qty_producing = 2 + for move in mo_form.move_raw_ids._records: + if move['product_id'] == self.product_4.id: + self.assertEqual(move['quantity_done'], 10) + break + mo = mo_form.save() + mo.button_mark_done() def test_product_produce_duplicate_1(self): """ produce a finished product tracked by serial number 2 times with the same SN. Check that an error is raised the second time""" mo1, bom, p_final, p1, p2 = self.generate_mo(tracking_final='serial', qty_final=1, qty_base_1=1,) - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo1.id, - 'active_ids': [mo1.id], - })) - product_produce = produce_form.save() - product_produce.action_generate_serial() - sn = product_produce.finished_lot_id - product_produce.do_produce() + mo_form = Form(mo1) + mo_form.qty_producing = 1 + mo1 = mo_form.save() + mo1.action_generate_serial() + sn = mo1.lot_producing_id mo1.button_mark_done() mo_form = Form(self.env['mrp.production']) @@ -1066,14 +881,11 @@ class TestMrpOrder(TestMrpCommon): mo2 = mo_form.save() mo2.action_confirm() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo2.id, - 'active_ids': [mo2.id], - })) - produce_form.finished_lot_id = sn - product_produce = produce_form.save() + mo_form = Form(mo2) + mo_form.lot_producing_id = sn + mo2 = mo_form.save() with self.assertRaises(UserError): - product_produce.do_produce() + mo2.button_mark_done() def test_product_produce_duplicate_2(self): """ produce a finished product with component tracked by serial number 2 @@ -1084,14 +896,13 @@ class TestMrpOrder(TestMrpCommon): 'product_id': p2.id, 'company_id': self.env.company.id, }) - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo1.id, - 'active_ids': [mo1.id], - })) - with produce_form.raw_workorder_line_ids.edit(0) as line: - line.lot_id = sn - product_produce = produce_form.save() - product_produce.do_produce() + mo_form = Form(mo1) + mo_form.qty_producing = 1 + mo1 = mo_form.save() + details_operation_form = Form(mo1.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = sn + details_operation_form.save() mo1.button_mark_done() mo_form = Form(self.env['mrp.production']) @@ -1101,15 +912,15 @@ class TestMrpOrder(TestMrpCommon): mo2 = mo_form.save() mo2.action_confirm() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo2.id, - 'active_ids': [mo2.id], - })) - with produce_form.raw_workorder_line_ids.edit(0) as line: - line.lot_id = sn - product_produce = produce_form.save() + mo_form = Form(mo2) + mo_form.qty_producing = 1 + mo2 = mo_form.save() + details_operation_form = Form(mo2.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = sn + details_operation_form.save() with self.assertRaises(UserError): - product_produce.do_produce() + mo2.button_mark_done() def test_product_produce_duplicate_3(self): """ produce a finished product with by-product tracked by serial number 2 @@ -1141,15 +952,15 @@ class TestMrpOrder(TestMrpCommon): 'product_id': byproduct.id, 'company_id': self.env.company.id, }) - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - with produce_form.finished_workorder_line_ids.edit(0) as line: - line.lot_id = sn - product_produce = produce_form.save() - product_produce.do_produce() + mo_form = Form(mo) + mo_form.qty_producing = 1 + mo = mo_form.save() + move_byproduct = mo.move_finished_ids.filtered(lambda m: m.product_id != mo.product_id) + details_operation_form = Form(move_byproduct, view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = sn + details_operation_form.save() mo.button_mark_done() mo_form = Form(self.env['mrp.production']) @@ -1159,15 +970,16 @@ class TestMrpOrder(TestMrpCommon): mo2 = mo_form.save() mo2.action_confirm() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo2.id, - 'active_ids': [mo2.id], - })) - with produce_form.finished_workorder_line_ids.edit(0) as line: - line.lot_id = sn - product_produce = produce_form.save() + mo_form = Form(mo2) + mo_form.qty_producing = 1 + mo2 = mo_form.save() + move_byproduct = mo2.move_finished_ids.filtered(lambda m: m.product_id != mo.product_id) + details_operation_form = Form(move_byproduct, view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = sn + details_operation_form.save() with self.assertRaises(UserError): - product_produce.do_produce() + mo2.button_mark_done() def test_product_produce_duplicate_4(self): """ Consuming the same serial number two times should not give an error if @@ -1178,14 +990,13 @@ class TestMrpOrder(TestMrpCommon): 'product_id': p2.id, 'company_id': self.env.company.id, }) - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo1.id, - 'active_ids': [mo1.id], - })) - with produce_form.raw_workorder_line_ids.edit(0) as line: - line.lot_id = sn - product_produce = produce_form.save() - product_produce.do_produce() + mo_form = Form(mo1) + mo_form.qty_producing = 1 + mo1 = mo_form.save() + details_operation_form = Form(mo1.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = sn + details_operation_form.save() mo1.button_mark_done() unbuild_form = Form(self.env['mrp.unbuild']) @@ -1203,15 +1014,15 @@ class TestMrpOrder(TestMrpCommon): mo2 = mo_form.save() mo2.action_confirm() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo2.id, - 'active_ids': [mo2.id], - })) - with produce_form.raw_workorder_line_ids.edit(0) as line: - line.lot_id = sn - product_produce = produce_form.save() - product_produce.do_produce() - + mo_form = Form(mo2) + mo_form.qty_producing = 1 + mo2 = mo_form.save() + details_operation_form = Form(mo2.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = sn + details_operation_form.save() + mo2.button_mark_done() + def test_product_produce_uom(self): """ Produce a finished product tracked by serial number. Set another UoM on the bom. The produce wizard should keep the UoM of the product (unit) @@ -1231,14 +1042,10 @@ class TestMrpOrder(TestMrpCommon): 'uom_id': unit.id, 'uom_po_id': unit.id, }) - routing = self.env['mrp.routing'].create({ - 'name': 'Secondary Assembly', - }) bom = self.env['mrp.bom'].create({ 'product_tmpl_id': plastic_laminate.product_tmpl_id.id, 'product_uom_id': unit.id, 'sequence': 1, - 'routing_id': routing.id, 'bom_line_ids': [(0, 0, { 'product_id': ply_veneer.id, 'product_qty': 1, @@ -1265,86 +1072,20 @@ class TestMrpOrder(TestMrpCommon): self.assertEqual(mo.move_raw_ids.product_qty, 12, '12 units should be reserved.') # produce product - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.finished_lot_id = final_product_lot - product_produce = produce_form.save() - self.assertEqual(product_produce.qty_producing, 1) - self.assertEqual(product_produce.product_uom_id, unit, 'Should be 1 unit since the tracking is serial.') - product_produce.finished_lot_id = final_product_lot.id + mo_form = Form(mo) + mo_form.qty_producing = 1/12.0 + mo_form.lot_producing_id = final_product_lot + mo = mo_form.save() - product_produce.do_produce() move_line_raw = mo.move_raw_ids.mapped('move_line_ids').filtered(lambda m: m.qty_done) self.assertEqual(move_line_raw.qty_done, 1) self.assertEqual(move_line_raw.product_uom_id, unit, 'Should be 1 unit since the tracking is serial.') + mo._post_inventory() move_line_finished = mo.move_finished_ids.mapped('move_line_ids').filtered(lambda m: m.qty_done) self.assertEqual(move_line_finished.qty_done, 1) self.assertEqual(move_line_finished.product_uom_id, unit, 'Should be 1 unit since the tracking is serial.') - def test_product_produce_uom_2(self): - """ Create a bom with a serial tracked component and a pair UoM (2 x unit). - The produce wizard should create 2 line with quantity = 1 and UoM = unit for - this component. """ - - unit = self.env.ref("uom.product_uom_unit") - categ_unit_id = self.env.ref('uom.product_uom_categ_unit') - paire = self.env['uom.uom'].create({ - 'name': 'Paire', - 'factor_inv': 2, - 'uom_type': 'bigger', - 'rounding': 0.001, - 'category_id': categ_unit_id.id - }) - binocular = self.env['product.product'].create({ - 'name': 'Binocular', - 'type': 'product', - 'uom_id': unit.id, - 'uom_po_id': unit.id - }) - nocular = self.env['product.product'].create({ - 'name': 'Nocular', - 'type': 'product', - 'tracking': 'serial', - 'uom_id': unit.id, - 'uom_po_id': unit.id - }) - bom_binocular = self.env['mrp.bom'].create({ - 'product_tmpl_id': binocular.product_tmpl_id.id, - 'product_qty': 1, - 'product_uom_id': unit.id, - 'bom_line_ids': [(0, 0, { - 'product_id': nocular.id, - 'product_qty': 1, - 'product_uom_id': paire.id - })] - }) - mo_form = Form(self.env['mrp.production']) - mo_form.product_id = binocular - mo_form.bom_id = bom_binocular - mo_form.product_uom_id = unit - mo_form.product_qty = 1 - mo = mo_form.save() - - mo.action_confirm() - self.assertEqual(mo.move_raw_ids.product_uom_qty, 1, 'Quantity should be 1.') - self.assertEqual(mo.move_raw_ids.product_uom, paire, 'Move UoM should be "Paire".') - - # produce product - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - product_produce = produce_form.save() - self.assertEqual(product_produce.qty_producing, 1) - self.assertEqual(len(product_produce.raw_workorder_line_ids), 2, 'Should be 2 lines since the component tracking is serial and quantity 2.') - self.assertEqual(product_produce.raw_workorder_line_ids[0].qty_to_consume, 1, 'Should be 1 unit since the tracking is serial and quantity 2.') - self.assertEqual(product_produce.raw_workorder_line_ids[0].product_uom_id, unit, 'Should be the product uom so "unit"') - self.assertEqual(product_produce.raw_workorder_line_ids[1].qty_to_consume, 1, 'Should be 1 unit since the tracking is serial and quantity 2.') - self.assertEqual(product_produce.raw_workorder_line_ids[1].product_uom_id, unit, 'should be the product uom so "unit"') - def test_product_type_service_1(self): # Create finished product finished_product = self.env['product.product'].create({ diff --git a/addons/mrp/tests/test_procurement.py b/addons/mrp/tests/test_procurement.py index cf60399acaa..89be67074bd 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', 'Production order should be for Confirmed state') + self.assertEqual(production_product_6.state, 'confirmed') # Check procurement for product 4 created or not. # Check it created a purchase order @@ -69,14 +69,9 @@ class TestProcurement(TestMrpCommon): # produce product4 # --------------- - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': produce_product_4.id, - 'active_ids': [produce_product_4.id], - })) - produce_form.qty_producing = produce_product_4.product_qty - product_produce = produce_form.save() - product_produce.do_produce() - produce_product_4.post_inventory() + mo_form = Form(produce_product_4) + mo_form.qty_producing = produce_product_4.product_qty + produce_product_4 = mo_form.save() # Check procurement and Production state for product 4. produce_product_4.button_mark_done() self.assertEqual(produce_product_4.state, 'done', 'Production order should be in state done') @@ -95,14 +90,9 @@ class TestProcurement(TestMrpCommon): # ------------------------------------ self.assertEqual(production_product_6.reservation_state, 'assigned', "Consume material not available") - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': production_product_6.id, - 'active_ids': [production_product_6.id], - })) - produce_form.qty_producing = production_product_6.product_qty - product_produce = produce_form.save() - product_produce.do_produce() - production_product_6.post_inventory() + mo_form = Form(production_product_6) + mo_form.qty_producing = production_product_6.product_qty + production_product_6 = mo_form.save() # Check procurement and Production state for product 6. production_product_6.button_mark_done() self.assertEqual(production_product_6.state, 'done', 'Production order should be in state done') @@ -191,13 +181,9 @@ class TestProcurement(TestMrpCommon): self.assertEqual(picking_qc_to_stock.state, 'done') mo.action_assign() self.assertEqual(mo.move_raw_ids.reserved_availability, 3.0) - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) + produce_form = Form(mo) produce_form.qty_producing = 3.0 - produce_wizard = produce_form.save() - produce_wizard.do_produce() + mo = produce_form.save() self.assertEqual(mo.move_raw_ids.quantity_done, 3.0) picking_qc_to_stock.move_line_ids.qty_done = 5.0 self.assertEqual(mo.move_raw_ids.reserved_availability, 5.0) @@ -340,7 +326,10 @@ class TestProcurement(TestMrpCommon): 'product_tmpl_id': parent_product.id, 'product_uom_id': self.uom_unit.id, 'product_qty': 4.0, - 'routing_id': self.routing_2.id, + 'operation_ids': [ + (0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}), + (0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}), + ], 'type': 'normal', }) self.env['mrp.bom.line'].create({ @@ -353,7 +342,10 @@ class TestProcurement(TestMrpCommon): 'product_tmpl_id': child_product.id, 'product_uom_id': self.uom_unit.id, 'product_qty': 4.0, - 'routing_id': self.routing_2.id, + 'operation_ids': [ + (0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}), + (0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}), + ], 'type': 'normal', }) self.env['mrp.bom.line'].create({ @@ -413,12 +405,9 @@ class TestProcurement(TestMrpCommon): self.env['stock.move'].create(move_values) production.action_confirm() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': production.id, - 'active_ids': [production.id], - })) - product_produce = produce_form.save() - product_produce.do_produce() + produce_form = Form(production) + produce_form.qty_producing = production.product_qty + production = produce_form.save() production.button_mark_done() move_dest._action_assign() diff --git a/addons/mrp/tests/test_stock.py b/addons/mrp/tests/test_stock.py index f2c012737c7..64587e1cde9 100644 --- a/addons/mrp/tests/test_stock.py +++ b/addons/mrp/tests/test_stock.py @@ -26,13 +26,6 @@ class TestWarehouse(common.TestMrpCommon): 'name': 'Assembly Line 1', 'resource_calendar_id': self.env.ref('resource.resource_calendar_std').id, }) - mrp_routing = self.env['mrp.routing'].create({ - 'name': 'Primary Assembly', - 'operation_ids': [(0, 0, { - 'workcenter_id': mrp_workcenter.id, - 'name': 'Manual Assembly', - })] - }) inventory = self.env['stock.inventory'].create({ 'name': 'Initial inventory', 'line_ids': [(0, 0, { @@ -49,12 +42,15 @@ class TestWarehouse(common.TestMrpCommon): 'product_tmpl_id': self.laptop.product_tmpl_id.id, 'product_qty': 1, 'product_uom_id': unit.id, + 'consumption': 'flexible', 'bom_line_ids': [(0, 0, { 'product_id': self.graphics_card.id, 'product_qty': 1, 'product_uom_id': unit.id })], - 'routing_id': mrp_routing.id + 'operation_ids': [ + (0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}), + ], }) def new_mo_laptop(self): @@ -162,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 @@ -212,14 +167,10 @@ class TestWarehouse(common.TestMrpCommon): mo_laptop = self.new_mo_laptop() serial = self.env['stock.production.lot'].create({'product_id': self.laptop.id, 'company_id': self.env.company.id}) - product_produce = self.env['mrp.product.produce'].with_context({ - 'active_id': mo_laptop.id, - 'active_ids': [mo_laptop.id], - }).create({ - "qty_producing": 1.0, - "finished_lot_id": serial.id, - }) - product_produce.do_produce() + mo_form = Form(mo_laptop) + mo_form.qty_producing = 1 + mo_form.lot_producing_id = serial + mo_laptop = mo_form.save() mo_laptop.button_mark_done() # We check if the laptop go in the depot and not in the stock diff --git a/addons/mrp/tests/test_traceability.py b/addons/mrp/tests/test_traceability.py index ca93a71c273..ea861c2cefe 100644 --- a/addons/mrp/tests/test_traceability.py +++ b/addons/mrp/tests/test_traceability.py @@ -63,19 +63,22 @@ class TestTraceability(TestMrpCommon): mo.action_assign() # Start MO production - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - - if finished_product.tracking != 'serial': - produce_form.qty_producing = 1 - + mo_form = Form(mo) + mo_form.qty_producing = 1 if finished_product.tracking != 'none': - produce_form.finished_lot_id = self.env['stock.production.lot'].create({'name': 'Serial or Lot finished', 'product_id': finished_product.id, 'company_id': self.env.company.id}) - produce_wizard = produce_form.save() + mo_form.lot_producing_id = self.env['stock.production.lot'].create({'name': 'Serial or Lot finished', 'product_id': finished_product.id, 'company_id': self.env.company.id}) + mo = mo_form.save() + + details_operation_form = Form(mo.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 = 1 + details_operation_form.save() + details_operation_form = Form(mo.move_raw_ids[2], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.edit(0) as ml: + ml.qty_done = 1 + details_operation_form.save() + - produce_wizard.do_produce() mo.button_mark_done() self.assertEqual(mo.state, 'done', "Production order should be in done state.") @@ -144,6 +147,7 @@ class TestTraceability(TestMrpCommon): 'product_tmpl_id': product_final.product_tmpl_id.id, 'product_uom_id': self.uom_unit.id, 'product_qty': 1.0, + 'consumption': 'flexible', 'type': 'normal', 'bom_line_ids': [ (0, 0, {'product_id': product_1.id, 'product_qty': 1}), @@ -160,104 +164,145 @@ class TestTraceability(TestMrpCommon): mo = mo_form.save() mo.action_confirm() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.finished_lot_id = self.env['stock.production.lot'].create({ + mo_form = Form(mo) + mo_form.lot_producing_id = self.env['stock.production.lot'].create({ 'product_id': product_final.id, 'name': 'Final_lot_1', 'company_id': self.env.company.id, }) - with produce_form.raw_workorder_line_ids.edit(0) as line: - line.lot_id = self.env['stock.production.lot'].create({ + mo = mo_form.save() + + details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = self.env['stock.production.lot'].create({ 'product_id': product_1.id, 'name': 'Raw_1_lot_1', 'company_id': self.env.company.id, }) - with produce_form.raw_workorder_line_ids.edit(1) as line: - line.lot_id = self.env['stock.production.lot'].create({ + ml.qty_done = 1 + details_operation_form.save() + details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = self.env['stock.production.lot'].create({ 'product_id': product_2.id, 'name': 'Raw_2_lot_1', 'company_id': self.env.company.id, }) - with produce_form.finished_workorder_line_ids.edit(0) as line: - line.lot_id = self.env['stock.production.lot'].create({ + ml.qty_done = 1 + details_operation_form.save() + details_operation_form = Form( + mo.move_finished_ids.filtered(lambda m: m.product_id == byproduct_1), + view=self.env.ref('stock.view_stock_move_operations') + ) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = self.env['stock.production.lot'].create({ 'product_id': byproduct_1.id, 'name': 'Byproduct_1_lot_1', 'company_id': self.env.company.id, }) - with produce_form.finished_workorder_line_ids.edit(1) as line: - line.lot_id = self.env['stock.production.lot'].create({ + ml.qty_done = 1 + details_operation_form.save() + details_operation_form = Form( + mo.move_finished_ids.filtered(lambda m: m.product_id == byproduct_2), + view=self.env.ref('stock.view_stock_move_operations') + ) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = self.env['stock.production.lot'].create({ 'product_id': byproduct_2.id, 'name': 'Byproduct_2_lot_1', 'company_id': self.env.company.id, }) - produce_wizard = produce_form.save() - produce_wizard.continue_production() + ml.qty_done = 1 + details_operation_form.save() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.finished_lot_id = self.env['stock.production.lot'].create({ + action = mo.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_form = Form(mo_backorder) + mo_form.lot_producing_id = self.env['stock.production.lot'].create({ 'product_id': product_final.id, 'name': 'Final_lot_2', 'company_id': self.env.company.id, }) - with produce_form.raw_workorder_line_ids.edit(0) as line: - line.lot_id = self.env['stock.production.lot'].create({ + mo_form.qty_producing = 1 + mo_backorder = mo_form.save() + + details_operation_form = Form( + mo_backorder.move_raw_ids.filtered(lambda m: m.product_id == product_1), + view=self.env.ref('stock.view_stock_move_operations') + ) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = self.env['stock.production.lot'].create({ 'product_id': product_1.id, 'name': 'Raw_1_lot_2', 'company_id': self.env.company.id, }) - with produce_form.raw_workorder_line_ids.edit(1) as line: - line.lot_id = self.env['stock.production.lot'].create({ + ml.qty_done = 1 + details_operation_form.save() + details_operation_form = Form( + mo_backorder.move_raw_ids.filtered(lambda m: m.product_id == product_2), + view=self.env.ref('stock.view_stock_move_operations') + ) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = self.env['stock.production.lot'].create({ 'product_id': product_2.id, 'name': 'Raw_2_lot_2', 'company_id': self.env.company.id, }) - with produce_form.finished_workorder_line_ids.edit(0) as line: - line.lot_id = self.env['stock.production.lot'].create({ + ml.qty_done = 1 + details_operation_form.save() + details_operation_form = Form( + mo_backorder.move_finished_ids.filtered(lambda m: m.product_id == byproduct_1), + view=self.env.ref('stock.view_stock_move_operations') + ) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = self.env['stock.production.lot'].create({ 'product_id': byproduct_1.id, 'name': 'Byproduct_1_lot_2', 'company_id': self.env.company.id, }) - with produce_form.finished_workorder_line_ids.edit(1) as line: - line.lot_id = self.env['stock.production.lot'].create({ + ml.qty_done = 1 + details_operation_form.save() + details_operation_form = Form( + mo_backorder.move_finished_ids.filtered(lambda m: m.product_id == byproduct_2), + view=self.env.ref('stock.view_stock_move_operations') + ) + with details_operation_form.move_line_ids.new() as ml: + ml.lot_id = self.env['stock.production.lot'].create({ 'product_id': byproduct_2.id, 'name': 'Byproduct_2_lot_2', 'company_id': self.env.company.id, }) - produce_wizard = produce_form.save() - produce_wizard.do_produce() - mo.button_mark_done() + ml.qty_done = 1 + details_operation_form.save() - self.assertEqual(len(mo.move_raw_ids.mapped('move_line_ids')), 4) - self.assertEqual(len(mo.move_finished_ids.mapped('move_line_ids')), 6) + mo_backorder.button_mark_done() + # self.assertEqual(len(mo.move_raw_ids.mapped('move_line_ids')), 4) + # self.assertEqual(len(mo.move_finished_ids.mapped('move_line_ids')), 6) + + mo = mo | mo_backorder raw_move_lines = mo.move_raw_ids.mapped('move_line_ids') raw_line_raw_1_lot_1 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_1') - self.assertEqual(set(raw_line_raw_1_lot_1.lot_produced_ids.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1'])) - raw_line_raw_1_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_2') - self.assertEqual(set(raw_line_raw_1_lot_2.lot_produced_ids.mapped('name')), set(['Final_lot_2', 'Byproduct_1_lot_2', 'Byproduct_2_lot_2'])) + self.assertEqual(set(raw_line_raw_1_lot_1.produce_line_ids.lot_id.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1'])) raw_line_raw_2_lot_1 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_1') - self.assertEqual(set(raw_line_raw_2_lot_1.lot_produced_ids.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1'])) - raw_line_raw_2_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_2') - self.assertEqual(set(raw_line_raw_2_lot_2.lot_produced_ids.mapped('name')), set(['Final_lot_2', 'Byproduct_1_lot_2', 'Byproduct_2_lot_2'])) + self.assertEqual(set(raw_line_raw_2_lot_1.produce_line_ids.lot_id.mapped('name')), set(['Final_lot_1', 'Byproduct_1_lot_1', 'Byproduct_2_lot_1'])) finished_move_lines = mo.move_finished_ids.mapped('move_line_ids') finished_move_line_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Final_lot_1') - self.assertEqual(finished_move_line_lot_1.consume_line_ids, raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1) + self.assertEqual(finished_move_line_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1) finished_move_line_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Final_lot_2') + raw_line_raw_1_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_1_lot_2') + raw_line_raw_2_lot_2 = raw_move_lines.filtered(lambda ml: ml.lot_id.name == 'Raw_2_lot_2') self.assertEqual(finished_move_line_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2) byproduct_move_line_1_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_1_lot_1') - self.assertEqual(byproduct_move_line_1_lot_1.consume_line_ids, raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1) + self.assertEqual(byproduct_move_line_1_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1) byproduct_move_line_1_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_1_lot_2') self.assertEqual(byproduct_move_line_1_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2) byproduct_move_line_2_lot_1 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_2_lot_1') - self.assertEqual(byproduct_move_line_2_lot_1.consume_line_ids, raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1) + self.assertEqual(byproduct_move_line_2_lot_1.consume_line_ids.filtered(lambda l: l.qty_done), raw_line_raw_1_lot_1 | raw_line_raw_2_lot_1) byproduct_move_line_2_lot_2 = finished_move_lines.filtered(lambda ml: ml.lot_id.name == 'Byproduct_2_lot_2') self.assertEqual(byproduct_move_line_2_lot_2.consume_line_ids, raw_line_raw_1_lot_2 | raw_line_raw_2_lot_2) diff --git a/addons/mrp/tests/test_unbuild.py b/addons/mrp/tests/test_unbuild.py index 79aabcd0978..159b4c3df3c 100644 --- a/addons/mrp/tests/test_unbuild.py +++ b/addons/mrp/tests/test_unbuild.py @@ -5,6 +5,7 @@ from odoo.tests import Form from odoo.addons.mrp.tests.common import TestMrpCommon from odoo.exceptions import UserError + class TestUnbuild(TestMrpCommon): def setUp(self): super(TestUnbuild, self).setUp() @@ -26,14 +27,9 @@ class TestUnbuild(TestMrpCommon): self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) mo.action_assign() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 5.0 - produce_wizard = produce_form.save() - produce_wizard.do_produce() - + mo_form = Form(mo) + mo_form.qty_producing = 5.0 + mo = mo_form.save() mo.button_mark_done() self.assertEqual(mo.state, 'done', "Production order should be in done state.") @@ -97,15 +93,10 @@ class TestUnbuild(TestMrpCommon): self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) mo.action_assign() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 5.0 - produce_form.finished_lot_id = lot - produce_wizard = produce_form.save() - - produce_wizard.do_produce() + mo_form = Form(mo) + mo_form.qty_producing = 5.0 + mo_form.lot_producing_id = lot + mo = mo_form.save() mo.button_mark_done() self.assertEqual(mo.state, 'done', "Production order should be in done state.") @@ -182,14 +173,15 @@ class TestUnbuild(TestMrpCommon): if ml.product_id.tracking != 'none': self.assertEqual(ml.lot_id, lot, 'Wrong reserved lot.') - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 5.0 - produce_wizard = produce_form.save() - - produce_wizard.do_produce() + # FIXME sle: behavior change + mo_form = Form(mo) + mo_form.qty_producing = 5.0 + mo = mo_form.save() + details_operation_form = Form(mo.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.lot_id = lot + ml.qty_done = 20 + details_operation_form.save() mo.button_mark_done() self.assertEqual(mo.state, 'done', "Production order should be in done state.") @@ -272,15 +264,19 @@ class TestUnbuild(TestMrpCommon): self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5, lot_id=lot_2) mo.action_assign() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 5.0 - produce_form.finished_lot_id = lot_final - produce_wizard = produce_form.save() - - produce_wizard.do_produce() + # FIXME sle: behavior change + mo_form = Form(mo) + mo_form.qty_producing = 5.0 + mo_form.lot_producing_id = lot_final + mo = mo_form.save() + details_operation_form = Form(mo.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 = 5 + details_operation_form.save() + details_operation_form = Form(mo.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 = 20 + details_operation_form.save() mo.button_mark_done() self.assertEqual(mo.state, 'done', "Production order should be in done state.") @@ -326,7 +322,7 @@ class TestUnbuild(TestMrpCommon): x.save().action_unbuild() self.assertEqual(self.env['stock.quant']._get_available_quantity(p_final, self.stock_location, lot_id=lot_final), 2, 'You should have consumed 3 final product in stock') - self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 92, 'You should have 80 products in stock') + self.assertEqual(self.env['stock.quant']._get_available_quantity(p1, self.stock_location, lot_id=lot_1), 92, 'You should have 92 products in stock') self.assertEqual(self.env['stock.quant']._get_available_quantity(p2, self.stock_location, lot_id=lot_2), 3, 'You should have consumed all the 5 product in stock') x = Form(self.env['mrp.unbuild']) @@ -381,14 +377,18 @@ class TestUnbuild(TestMrpCommon): self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 2, lot_id=lot_3) mo.action_assign() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 5.0 - produce_wizard = produce_form.save() + mo_form = Form(mo) + mo_form.qty_producing = 5.0 + mo = mo_form.save() + details_operation_form = Form(mo.move_raw_ids.filtered(lambda ml: ml.product_id == p2), view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.edit(0) as ml: + ml.qty_done = ml.product_uom_qty + with details_operation_form.move_line_ids.edit(1) as ml: + ml.qty_done = ml.product_uom_qty + with details_operation_form.move_line_ids.edit(2) as ml: + ml.qty_done = ml.product_uom_qty + details_operation_form.save() - produce_wizard.do_produce() mo.button_mark_done() self.assertEqual(mo.state, 'done', "Production order should be in done state.") # Check quantity in stock before unbuild. @@ -415,6 +415,9 @@ class TestUnbuild(TestMrpCommon): """ This test produces an MO in two times and checks that the move lines are linked in a correct way """ mo, bom, p_final, p1, p2 = self.generate_mo(tracking_final='lot', tracking_base_1='none', tracking_base_2='lot') + # Young Tom + # \ Botox - 4 - p1 + # \ Old Tom - 1 - p2 lot_1 = self.env['stock.production.lot'].create({ 'name': 'lot_1', 'product_id': p2.id, @@ -428,15 +431,20 @@ class TestUnbuild(TestMrpCommon): 'company_id': self.env.company.id, }) - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 3.0 - produce_form.finished_lot_id = lot_finished_1 - produce_wizard = produce_form.save() - produce_wizard._workorder_line_ids()[0].lot_id = lot_1 - produce_wizard.do_produce() + self.assertEqual(mo.product_qty, 5) + mo_form = Form(mo) + mo_form.qty_producing = 3.0 + mo_form.lot_producing_id = lot_finished_1 + mo = mo_form.save() + self.assertEqual(mo.move_raw_ids[1].quantity_done, 12) + details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.qty_done = 3 + ml.lot_id = lot_1 + details_operation_form.save() + action = mo.button_mark_done() + backorder = Form(self.env[action['res_model']].with_context(**action['context'])) + backorder.save().action_backorder() lot_2 = self.env['stock.production.lot'].create({ 'name': 'lot_2', @@ -451,21 +459,26 @@ class TestUnbuild(TestMrpCommon): 'company_id': self.env.company.id, }) - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 2.0 - produce_form.finished_lot_id = lot_finished_2 + mo = mo.procurement_group_id.mrp_production_ids[1] + # FIXME sle: issue in backorder? + mo.move_raw_ids.move_line_ids.unlink() + self.assertEqual(mo.product_qty, 2) + mo_form = Form(mo) + mo_form.qty_producing = 2 + mo_form.lot_producing_id = lot_finished_2 + mo = mo_form.save() + details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations')) + with details_operation_form.move_line_ids.new() as ml: + ml.qty_done = 2 + ml.lot_id = lot_2 + details_operation_form.save() + action = mo.button_mark_done() - produce_wizard = produce_form.save() - produce_wizard._workorder_line_ids()[0].lot_id = lot_2 - produce_wizard.do_produce() - mo.button_mark_done() - ml = mo.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and lot_finished_1 in m.lot_produced_ids) - self.assertEqual(ml[0].qty_done, 12.0, 'Should have consumed 12 for the first lot') - ml = mo.finished_move_line_ids[1].consume_line_ids.filtered(lambda m: m.product_id == p1 and lot_finished_2 in m.lot_produced_ids) - self.assertEqual(ml[0].qty_done, 8.0, 'Should have consumed 8 for the second lot') + mo1 = mo.procurement_group_id.mrp_production_ids[0] + ml = mo1.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and lot_finished_1 in m.produce_line_ids.lot_id) + self.assertEqual(sum(ml.mapped('qty_done')), 12.0, 'Should have consumed 12 for the first lot') + ml = mo.finished_move_line_ids[0].consume_line_ids.filtered(lambda m: m.product_id == p1 and lot_finished_2 in m.produce_line_ids.lot_id) + self.assertEqual(sum(ml.mapped('qty_done')), 8.0, 'Should have consumed 8 for the second lot') def test_unbuild_with_routes(self): """ This test creates a MO of a stockable product (Table). A new route for rule QC/Unbuild -> Stock @@ -539,13 +552,9 @@ class TestUnbuild(TestMrpCommon): mo.action_assign() # Produce the final product - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 1.0 - produce_wizard = produce_form.save() - produce_wizard.do_produce() + mo_form = Form(mo) + mo_form.qty_producing = 1.0 + produce_wizard = mo_form.save() mo.button_mark_done() self.assertEqual(mo.state, 'done', "Production order should be in done state.") diff --git a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py index a04f9078acc..408045ae274 100644 --- a/addons/mrp/tests/test_warehouse_multistep_manufacturing.py +++ b/addons/mrp/tests/test_warehouse_multistep_manufacturing.py @@ -184,13 +184,9 @@ class TestMultistepManufacturingWarehouse(TestMrpCommon): self.assertEqual(production_order.reservation_state, 'assigned') self.assertEqual(picking_stock_postprod.state, 'waiting') - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': production_order.id, - 'active_ids': [production_order.id], - })) + produce_form = Form(production_order) produce_form.qty_producing = production_order.product_qty - product_produce = produce_form.save() - product_produce.do_produce() + production_order = produce_form.save() production_order.button_mark_done() self.assertFalse(sum(self.env['stock.quant']._gather(self.raw_product, self.warehouse.pbm_loc_id).mapped('quantity'))) diff --git a/addons/mrp/tests/test_workorder_operation.py b/addons/mrp/tests/test_workorder_operation.py deleted file mode 100644 index de8b40ae1b4..00000000000 --- a/addons/mrp/tests/test_workorder_operation.py +++ /dev/null @@ -1,1815 +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, - 'routing_id': cls.routing_1.id, - '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.routing = cls.env['mrp.routing'].create({ - 'name': 'Assemble Furniture', - 'operation_ids': [(0, 0, { - 'workcenter_id': cls.mrp_workcenter.id, - 'name': 'Manual Assembly', - })] - }) - 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', - 'routing_id': cls.routing.id, - '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.routing.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.routing.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.routing.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.routing.operation_ids.id}), - ] - }) - cls.mrp_workcenter_1 = cls.env['mrp.workcenter'].create({ - 'name': 'Drill Station 1', - 'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id, - }) - cls.mrp_workcenter_3 = cls.env['mrp.workcenter'].create({ - 'name': 'Assembly Line 1', - 'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id, - }) - cls.routing_1 = cls.env['mrp.routing'].create({ - 'name': 'Secondary Assembly', - 'operation_ids': [ - (0, 0, { - 'workcenter_id': cls.mrp_workcenter_1.id, - 'name': 'Packing', - 'time_cycle': 30, - 'sequence': 5}), - (0, 0, { - 'workcenter_id': cls.mrp_workcenter_3.id, - 'name': 'Testing', - 'time_cycle': 60, - 'sequence': 10}), - (0, 0, { - 'workcenter_id': cls.mrp_workcenter_3.id, - 'name': 'Long time assembly', - 'time_cycle': 180, - 'sequence': 15}), - ] - }) - - -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() - production_table.action_confirm() - - # 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" - - # 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 - # --------------------------------------------------------------- - production_table.button_mark_done() - - 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.routing_id = self.routing_1 - - bom.bom_line_ids.filtered(lambda p: p.product_id == product_table_sheet).operation_id = bom.routing_id.operation_ids[0] - bom.bom_line_ids.filtered(lambda p: p.product_id == product_table_leg).operation_id = bom.routing_id.operation_ids[1] - bom.bom_line_ids.filtered(lambda p: p.product_id == product_bolt).operation_id = bom.routing_id.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.operation_1.write({'time_cycle_manual': 20}) - (self.operation_2 | self.operation_3).write({'time_cycle_manual': 10}) - - 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 == self.operation_1) - door_wo_1 = man_order.workorder_ids.filtered(lambda wo: wo.operation_id == self.operation_2) - door_wo_2 = man_order.workorder_ids.filtered(lambda wo: wo.operation_id == self.operation_3) - 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, - '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) - context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id} - product_form = Form(self.env['mrp.product.produce'].with_context(context)) - product_form.qty_producing = 6.00 - laptop_lot_001 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id , 'company_id': self.env.company.id}) - product_form.finished_lot_id = laptop_lot_001 - product_consume = product_form.save() - product_consume._workorder_line_ids()[0].qty_done = 12 - product_consume.do_produce() - - # 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) - mo_custom_laptop.post_inventory() - self.assertEqual(len(mo_custom_laptop.move_raw_ids), 4) - - # 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). - context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id} - produce_form = Form(self.env['mrp.product.produce'].with_context(context)) - produce_form.qty_producing = 4.00 - laptop_lot_002 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id, 'company_id': self.env.company.id}) - produce_form.finished_lot_id = laptop_lot_002 - product_consume = produce_form.save() - self.assertEqual(len(product_consume._workorder_line_ids()), 2) - product_consume._workorder_line_ids()[0].qty_done = 8 - product_consume.do_produce() - 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) - - # Post Inventory of production order. - mo_custom_laptop.post_inventory() - -# raw_moves_state = any(move.state != 'done' for move in mo_custom_laptop.move_raw_ids) -# finsh_moves_state = any(move.state != 'done' for move in mo_custom_laptop.move_finished_ids) -# self.assertFalse(raw_moves_state, "Wrong state in consumed moves of production order.") -# self.assertFalse(finsh_moves_state, "Wrong state in consumed moves of production order.") -# -# # Finished move quants of production order -# -# finshed_quant_lot_001 = mo_custom_laptop.move_finished_ids.filtered(lambda x: x.product_id.id == custom_laptop.id and x.product_uom_qty==6).mapped('quant_ids') -# finshed_quant_lot_002 = mo_custom_laptop.move_finished_ids.filtered(lambda x: x.product_id.id == custom_laptop.id and x.product_uom_qty==4).mapped('quant_ids') -# -# # Check total quantity consumed of charger, keybord -# # -------------------------------------------------- -# charger_quants = mo_custom_laptop.move_raw_ids.filtered(lambda x: x.product_id.id == product_charger.id and x.state == 'done').mapped('quant_ids') -# keybord_moves = mo_custom_laptop.move_raw_ids.filtered(lambda x: x.product_id.id == product_keybord.id and x.state == 'done').mapped('quant_ids') -# self.assertEqual(sum(charger_quants.mapped('qty')), 20) -# self.assertEqual(sum(keybord_moves.mapped('qty')), 20) - - def test_02_different_uom_on_bomlines(self): - """ Testing bill of material with different unit of measure.""" - route_manufacture = self.warehouse.manufacture_pull_id.route_id.id - route_mto = self.warehouse.mto_pull_id.route_id.id - unit = self.ref("uom.product_uom_unit") - dozen = self.ref("uom.product_uom_dozen") - kg = self.ref("uom.product_uom_kgm") - gm = self.ref("uom.product_uom_gram") - # Create Product A, B, C - product_A = self.env['product.product'].create({ - 'name': 'Product A', - 'type': 'product', - 'tracking': 'lot', - 'uom_id': dozen, - 'uom_po_id': dozen, - 'route_ids': [(6, 0, [route_manufacture, route_mto])]}) - product_B = self.env['product.product'].create({ - 'name': 'Product B', - 'type': 'product', - 'tracking': 'lot', - 'uom_id': dozen, - 'uom_po_id': dozen}) - product_C = self.env['product.product'].create({ - 'name': 'Product C', - 'type': 'product', - 'tracking': 'lot', - 'uom_id': kg, - 'uom_po_id': kg}) - - # Bill of materials - # ----------------- - - #=================================== - # Product A 1 Unit - # Product B 4 Unit - # Product C 600 gram - # ----------------------------------- - - bom_a = self.env['mrp.bom'].create({ - 'product_tmpl_id': product_A.product_tmpl_id.id, - 'product_qty': 2, - 'product_uom_id': unit, - 'bom_line_ids': [(0, 0, { - 'product_id': product_B.id, - 'product_qty': 4, - 'product_uom_id': unit - }), (0, 0, { - 'product_id': product_C.id, - 'product_qty': 600, - 'product_uom_id': gm - })] - }) - - # Create production order with product A 10 Unit. - # ----------------------------------------------- - - mo_custom_product_form = Form(self.env['mrp.production']) - mo_custom_product_form.product_id = product_A - mo_custom_product_form.bom_id = bom_a - mo_custom_product_form.product_qty = 10.0 - mo_custom_product_form.product_uom_id = self.env.ref("uom.product_uom_unit") - mo_custom_product = mo_custom_product_form.save() - - move_product_b = mo_custom_product.move_raw_ids.filtered(lambda x: x.product_id == product_B) - move_product_c = mo_custom_product.move_raw_ids.filtered(lambda x: x.product_id == product_C) - - # Check move correctly created or not. - self.assertEqual(move_product_b.product_uom_qty, 20) - self.assertEqual(move_product_b.product_uom.id, unit) - self.assertEqual(move_product_c.product_uom_qty, 3000) - self.assertEqual(move_product_c.product_uom.id, gm) - - # Lot create for product B and product C - # --------------------------------------- - lot_a = self.env['stock.production.lot'].create({'product_id': product_A.id, 'company_id': self.env.company.id}) - lot_b = self.env['stock.production.lot'].create({'product_id': product_B.id, 'company_id': self.env.company.id}) - lot_c = self.env['stock.production.lot'].create({'product_id': product_C.id, 'company_id': self.env.company.id}) - - # Inventory Update - # ---------------- - inventory = self.env['stock.inventory'].create({ - 'name': 'Inventory Product B and C', - 'line_ids': [(0, 0, { - 'product_id': product_B.id, - 'product_uom_id': product_B.uom_id.id, - 'product_qty': 3, - 'prod_lot_id': lot_b.id, - 'location_id': self.source_location_id - }), (0, 0, { - 'product_id': product_C.id, - 'product_uom_id': product_C.uom_id.id, - 'product_qty': 3, - 'prod_lot_id': lot_c.id, - 'location_id': self.source_location_id - })] - }) - inventory.action_start() - inventory.action_validate() - - # Start Production ... - # -------------------- - - mo_custom_product.action_confirm() - mo_custom_product.action_assign() - context = {"active_ids": [mo_custom_product.id], "active_id": mo_custom_product.id} - produce_form = Form(self.env['mrp.product.produce'].with_context(context)) - produce_form.qty_producing = 10.00 - produce_form.finished_lot_id = lot_a - product_consume = produce_form.save() - # laptop_lot_002 = self.env['stock.production.lot'].create({'product_id': custom_laptop.id}) - self.assertEqual(len(product_consume._workorder_line_ids()), 2) - product_consume._workorder_line_ids().filtered(lambda x: x.product_id == product_C).write({'qty_done': 3000}) - product_consume._workorder_line_ids().filtered(lambda x: x.product_id == product_B).write({'qty_done': 20}) - product_consume.do_produce() - mo_custom_product.post_inventory() - - # Check correct quant linked with move or not - # ------------------------------------------- - #TODO: check original quants qtys diminished -# self.assertEqual(len(move_product_b.quant_ids), 1) -# self.assertEqual(len(move_product_c.quant_ids), 1) -# self.assertEqual(move_product_b.quant_ids.qty, move_product_b.product_qty) -# self.assertEqual(move_product_c.quant_ids.qty, 3) -# self.assertEqual(move_product_c.quant_ids.product_uom_id.id, kg) - - 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") - three_step_routing = self.routing_1 - - 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, - 'bom_line_ids': [(0, 0, { - 'product_id': graphics_card.id, - 'product_qty': 1, - 'product_uom_id': unit.id - })], - 'routing_id': three_step_routing.id - }) - - 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, - 'sequence': 2, - 'routing_id': self.routing_1.id, - '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_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(2019, 5, 13, 9, 0) - self.workcenter_1.alternative_workcenter_ids = self.wc_alt_1 | self.wc_alt_2 - workcenters = [self.wc_alt_2, self.wc_alt_1, self.workcenter_1] - for i in range(3): - # 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.routing_id = self.routing_2 - # Allow second workorder to start once the first one is not ended yet - self.operation_2.batch = 'yes' - self.operation_2.batch_size = 1 - 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.operation_2.time_cycle * 100.0 / self.wc_alt_2.time_efficiency + self.wc_alt_2.time_start - duration2 = 2.0 * self.operation_2.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] """ - - 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 = datetime(2019, 5, 13, 14, 0, 0, 0) - 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 - 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 = datetime(2019, 5, 13, 9, 0, 0, 0) - 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, datetime(2019, 5, 13, 9, 0, 0, 0), delta=timedelta(seconds=10), msg="Wrong plannification") - self.assertAlmostEqual(wo1_stop, datetime(2019, 5, 13, 9, 0, 0, 0) + 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.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 = datetime(2019, 5, 13, 9, 0, 0, 0) - mo = mo_form.save() - mo.action_confirm() - mo.button_plan() - wo = mo.workorder_ids - self.assertAlmostEqual(wo.date_planned_start, datetime(2019, 5, 13, 9, 0, 0, 0), delta=timedelta(seconds=10)) - self.assertAlmostEqual(wo.date_planned_finished, datetime(2019, 5, 13, 9, 0, 0, 0) + timedelta(minutes=85.58), 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 = datetime(2019, 5, 13, 9, 0, 0, 0) - 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)) - self.assertAlmostEqual(wo.date_planned_finished, 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.routing_1.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(2019, 5, 13, 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()), False) - 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, - '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, - 'routing_id': self.routing_1.id, - '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 """ - self.routing_1.operation_ids[0].write({'workcenter_id': self.mrp_workcenter_3.id}) - dining_table = self.dining_table - bom = self.mrp_bom_desk - bom.routing_id = self.routing_1 - - bom.bom_line_ids.filtered(lambda p: p.product_id == self.product_table_sheet).operation_id = bom.routing_id.operation_ids[0].id - bom.bom_line_ids.filtered(lambda p: p.product_id == self.product_table_leg).operation_id = bom.routing_id.operation_ids[1].id - bom.bom_line_ids.filtered(lambda p: p.product_id == self.product_bolt).operation_id = bom.routing_id.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.routing_finished1 = cls.env['mrp.routing'].create({ - 'name': 'routing for finished1', - }) - cls.operation_finished1 = cls.env['mrp.routing.workcenter'].create({ - 'sequence': 1, - 'name': 'finished operation 1', - 'workcenter_id': cls.workcenter_finished1.id, - 'routing_id': cls.routing_finished1.id, - }) - cls.operation_finished2 = cls.env['mrp.routing.workcenter'].create({ - 'sequence': 1, - 'name': 'finished operation 2', - 'workcenter_id': cls.workcenter_finished1.id, - 'routing_id': cls.routing_finished1.id, - }) - cls.routing_kit1 = cls.env['mrp.routing'].create({ - 'name': 'routing for kit1', - }) - cls.operation_kit1 = cls.env['mrp.routing.workcenter'].create({ - 'name': 'Kit operation', - 'workcenter_id': cls.workcenter_kit1.id, - 'routing_id': cls.routing_kit1.id, - }) - 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, - 'product_qty': 1, - 'type': 'normal', - 'routing_id': cls.routing_finished1.id, - 'bom_line_ids': [ - (0, 0, {'product_id': cls.compfinished1.id, 'product_qty': 1}), - (0, 0, {'product_id': cls.kit1.id, 'product_qty': 1}), - ]}) - 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', - 'routing_id': cls.routing_kit1.id, - 'bom_line_ids': [ - (0, 0, {'product_id': cls.compkit1.id, 'product_qty': 1}), - ]}) - - 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.operation_kit1 - - 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.operation_kit1 - self.bom_finished1.bom_line_ids[0].operation_id = self.operation_finished1 - self.bom_finished1.bom_line_ids[1].operation_id = self.operation_finished2 - - 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.operation_finished1 - - # 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.routing_id.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) - - def test_6(self): - """ Use the same routing on `self.bom_fnished1` and `self.kit1`. The workorders should not - be duplicated. - """ - self.bom_finished1.bom_line_ids[0].operation_id = self.operation_finished1 - self.bom_finished1.bom_line_ids[1].operation_id = self.operation_finished2 - self.bom_kit1.routing_id = self.routing_finished1 - self.bom_kit1.bom_line_ids.operation_id = self.operation_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), 2) - self.assertEqual(set(mo.workorder_ids[0].raw_workorder_line_ids.product_id.ids), set([self.compfinished1.id, self.compkit1.id])) - self.assertFalse(mo.workorder_ids[1].raw_workorder_line_ids.product_id.id) - - 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.routing_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.routing_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_bom_views.xml b/addons/mrp/views/mrp_bom_views.xml index 768f08f22ee..995f8e2bbea 100644 --- a/addons/mrp/views/mrp_bom_views.xml +++ b/addons/mrp/views/mrp_bom_views.xml @@ -14,7 +14,7 @@
- + @@ -72,10 +76,9 @@ - + - @@ -88,21 +91,31 @@ - + + + + + - + - + @@ -110,8 +123,8 @@ - - + + @@ -142,7 +155,6 @@ - @@ -187,7 +199,6 @@ - @@ -200,7 +211,7 @@ [] tree,kanban,form - {'search_default_group_by_type': True, 'default_company_id': allowed_company_ids[0]} + {'default_company_id': allowed_company_ids[0]}

Create a bill of materials @@ -239,8 +250,8 @@ - + diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index e7b3c222a19..93fbb0a334b 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -14,10 +14,10 @@ + - @@ -39,6 +39,20 @@ records.button_plan() + + Mark as Done + + + list + code + + if records: + res = records.filtered(lambda mo: mo.state in {'to_close', 'progress'}).button_mark_done() + if res is not True: + action = res + + + mrp.production.form mrp.production @@ -46,51 +60,37 @@

+ +
+ + - + + +
- - - - + + + +

@@ -112,68 +128,41 @@ + - -
- - - - - - - - - + + + + + + + + + - - @@ -165,7 +167,7 @@ - +
@@ -186,44 +188,6 @@ - - - - - - - - - - -

Components

- - - - - - - - - - - - - -

Finished Products

- - - - - - - - - - - - -
@@ -485,6 +449,15 @@ + + Work Orders + ir.actions.act_window + mrp.workorder + form + new + + + Work Orders ir.actions.act_window diff --git a/addons/mrp/views/res_config_settings_views.xml b/addons/mrp/views/res_config_settings_views.xml index 229ff502fc6..1ea1782657f 100644 --- a/addons/mrp/views/res_config_settings_views.xml +++ b/addons/mrp/views/res_config_settings_views.xml @@ -20,13 +20,10 @@