diff --git a/addons/mrp/__manifest__.py b/addons/mrp/__manifest__.py index 235c0e807365d5b0fe13769a311b9dec9f0a568d..ce6d450810de29ccd589ff8d86110d563d32f883 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 64041d0b39bd9b28cd2a877c18546e8bc08d6735..c41f0576a3033d67f3bc01f28ccb246aaaac7f9d 100644 --- a/addons/mrp/data/mrp_data.xml +++ b/addons/mrp/data/mrp_data.xml @@ -2,15 +2,6 @@ <odoo> <data noupdate="1"> - <record id="sequence_mrp_route" model="ir.sequence"> - <field name="name">Routing</field> - <field name="code">mrp.routing</field> - <field name="prefix">RO/</field> - <field name="padding">5</field> - <field name="number_next">1</field> - <field name="number_increment">1</field> - <field name="company_id" eval="False"/> - </record> <function model="res.company" name="create_missing_unbuild_sequences" /> <!-- Stock rules and routes diff --git a/addons/mrp/data/mrp_demo.xml b/addons/mrp/data/mrp_demo.xml index 99a0173d83d650d92e3f3eb6a9e10fff0d8f61c4..60c1b1ad768267504a3b131492ef03273bdeb404 100644 --- a/addons/mrp/data/mrp_demo.xml +++ b/addons/mrp/data/mrp_demo.xml @@ -28,83 +28,6 @@ <field name="resource_calendar_id" ref="resource.resource_calendar_std"/> </record> - - - <!-- Resource: mrp.routing --> - - <record id="mrp_routing_0" model="mrp.routing"> - <field name="name">Primary Assembly</field> - </record> - - <record id="mrp_routing_1" model="mrp.routing"> - <field name="name">Secondary Assembly</field> - </record> - - <record id="mrp_routing_2" model="mrp.routing"> - <field name="name">Manual Component's Assembly</field> - </record> - - <record id="mrp_routing_3" model="mrp.routing"> - <field name="name">Assemble Furniture</field> - </record> - - - <!-- Resource: mrp.routing.workcenter --> - - <record id="mrp_routing_workcenter_0" model="mrp.routing.workcenter"> - <field name="routing_id" ref="mrp_routing_0"/> - <field name="workcenter_id" ref="mrp_workcenter_3"/> - <field name="name">Manual Assembly</field> - <field name="time_cycle">60</field> - <field name="sequence">5</field> - <field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/> - </record> - - <record id="mrp_routing_workcenter_1" model="mrp.routing.workcenter"> - <field name="routing_id" ref="mrp_routing_1"/> - <field name="workcenter_id" ref="mrp_workcenter_3"/> - <field name="name">Long time assembly</field> - <field name="time_cycle">180</field> - <field name="sequence">15</field> - <field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/> - </record> - - <record id="mrp_routing_workcenter_3" model="mrp.routing.workcenter"> - <field name="routing_id" ref="mrp_routing_1"/> - <field name="workcenter_id" ref="mrp_workcenter_3"/> - <field name="name">Testing</field> - <field name="time_cycle">60</field> - <field name="sequence">10</field> - <field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/> - </record> - - <record id="mrp_routing_workcenter_4" model="mrp.routing.workcenter"> - <field name="routing_id" ref="mrp_routing_1"/> - <field name="workcenter_id" ref="mrp_workcenter_1"/> - <field name="name">Packing</field> - <field name="time_cycle">30</field> - <field name="sequence">5</field> - <field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/> - </record> - - <record id="mrp_routing_workcenter_2" model="mrp.routing.workcenter"> - <field name="routing_id" ref="mrp_routing_2"/> - <field name="workcenter_id" ref="mrp_workcenter_2"/> - <field name="time_cycle">120</field> - <field name="sequence">5</field> - <field name="name">Manual Assembly</field> - <field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/> - </record> - - <record id="mrp_routing_workcenter_5" model="mrp.routing.workcenter"> - <field name="routing_id" ref="mrp_routing_3"/> - <field name="workcenter_id" ref="mrp_workcenter_3"/> - <field name="time_cycle">120</field> - <field name="sequence">10</field> - <field name="name">Assembly</field> - <field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/> - </record> - <!-- Resource: mrp.bom --> <record id="product.product_product_3_product_template" model="product.template"> @@ -114,7 +37,14 @@ <field name="product_tmpl_id" ref="product.product_product_3_product_template"/> <field name="product_uom_id" ref="uom.product_uom_unit"/> <field name="sequence">1</field> - <field name="routing_id" ref="mrp_routing_0"/> + </record> + <record id="mrp_routing_workcenter_0" model="mrp.routing.workcenter"> + <field name="bom_id" ref="mrp_bom_manufacture"/> + <field name="workcenter_id" ref="mrp_workcenter_3"/> + <field name="name">Manual Assembly</field> + <field name="time_cycle">60</field> + <field name="sequence">5</field> + <field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/> </record> <record id="mrp_bom_manufacture_line_1" model="mrp.bom.line"> @@ -296,7 +226,14 @@ <field name="product_uom_id" ref="uom.product_uom_unit"/> <field name="sequence">3</field> <field name="consumption">flexible</field> - <field name="routing_id" ref="mrp_routing_3"/> + </record> + <record id="mrp_routing_workcenter_5" model="mrp.routing.workcenter"> + <field name="bom_id" ref="mrp_bom_desk"/> + <field name="workcenter_id" ref="mrp_workcenter_3"/> + <field name="time_cycle">120</field> + <field name="sequence">10</field> + <field name="name">Assembly</field> + <field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/> </record> <record id="mrp_bom_desk_line_1" model="mrp.bom.line"> @@ -350,8 +287,16 @@ <field name="product_tmpl_id" ref="product_product_computer_desk_head_product_template"/> <field name="product_uom_id" ref="uom.product_uom_unit"/> <field name="sequence">1</field> - <field name="routing_id" ref="mrp_routing_0"/> </record> + <record id="mrp_routing_workcenter_0" model="mrp.routing.workcenter"> + <field name="bom_id" ref="mrp_bom_table_top"/> + <field name="workcenter_id" ref="mrp_workcenter_3"/> + <field name="name">Manual Assembly</field> + <field name="time_cycle">60</field> + <field name="sequence">5</field> + <field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/> + </record> + <record id="mrp_bom_line_wood_panel" model="mrp.bom.line"> <field name="product_id" ref="product_product_wood_panel"/> <field name="product_qty">2</field> @@ -371,7 +316,32 @@ <field name="product_tmpl_id" ref="product_product_plastic_laminate_product_template"/> <field name="product_uom_id" ref="uom.product_uom_unit"/> <field name="sequence">1</field> - <field name="routing_id" ref="mrp_routing_1"/> + </record> + <record id="mrp_routing_workcenter_1" model="mrp.routing.workcenter"> + <field name="bom_id" ref="mrp_bom_plastic_laminate"/> + <field name="workcenter_id" ref="mrp_workcenter_3"/> + <field name="name">Long time assembly</field> + <field name="time_cycle">180</field> + <field name="sequence">15</field> + <field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/> + </record> + + <record id="mrp_routing_workcenter_3" model="mrp.routing.workcenter"> + <field name="bom_id" ref="mrp_bom_plastic_laminate"/> + <field name="workcenter_id" ref="mrp_workcenter_3"/> + <field name="name">Testing</field> + <field name="time_cycle">60</field> + <field name="sequence">10</field> + <field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/> + </record> + + <record id="mrp_routing_workcenter_4" model="mrp.routing.workcenter"> + <field name="bom_id" ref="mrp_bom_plastic_laminate"/> + <field name="workcenter_id" ref="mrp_workcenter_1"/> + <field name="name">Packing</field> + <field name="time_cycle">30</field> + <field name="sequence">5</field> + <field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/> </record> <record id="mrp_bom_line_plastic_laminate" model="mrp.bom.line"> <field name="product_id" ref="product_product_ply_veneer"/> @@ -596,9 +566,34 @@ <field name="product_tmpl_id" ref="product.product_product_27_product_template"/> <field name="product_uom_id" ref="uom.product_uom_unit"/> <field name="sequence">2</field> - <field name="routing_id" ref="mrp_routing_1"/> <field name="code">SEC-ASSEM</field> </record> + <record id="mrp_routing_workcenter_1" model="mrp.routing.workcenter"> + <field name="bom_id" ref="mrp_bom_laptop_cust_rout"/> + <field name="workcenter_id" ref="mrp_workcenter_3"/> + <field name="name">Long time assembly</field> + <field name="time_cycle">180</field> + <field name="sequence">15</field> + <field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/> + </record> + + <record id="mrp_routing_workcenter_3" model="mrp.routing.workcenter"> + <field name="bom_id" ref="mrp_bom_laptop_cust_rout"/> + <field name="workcenter_id" ref="mrp_workcenter_3"/> + <field name="name">Testing</field> + <field name="time_cycle">60</field> + <field name="sequence">10</field> + <field name="worksheet" type="base64" file="mrp/static/img/assebly-worksheet.pdf"/> + </record> + + <record id="mrp_routing_workcenter_4" model="mrp.routing.workcenter"> + <field name="bom_id" ref="mrp_bom_laptop_cust_rout"/> + <field name="workcenter_id" ref="mrp_workcenter_1"/> + <field name="name">Packing</field> + <field name="time_cycle">30</field> + <field name="sequence">5</field> + <field name="worksheet" type="base64" file="mrp/static/img/cutting-worksheet.pdf"/> + </record> <record id="mrp_bom_laptop_cust_rout_line_1" model="mrp.bom.line"> <field name="product_id" ref="product_product_drawer_drawer"/> <field name="product_qty">1</field> @@ -630,6 +625,10 @@ <value model="stock.move" eval="obj().env.ref('mrp.mrp_production_laptop_cust')._get_moves_raw_values()"/> </function> + <function model="stock.move" name="create"> + <value model="stock.move" eval="obj().env.ref('mrp.mrp_production_laptop_cust')._get_moves_finished_values()"/> + </function> + <!-- Run Scheduler --> <function model="procurement.group" name="run_scheduler"/> @@ -677,6 +676,10 @@ <field name="date_start" eval="(datetime.now() - timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S')"/> </record> + <function model="mrp.production" name="_create_workorder"> + <value eval="[ref('mrp.mrp_production_3')]"/> + </function> + <function model="mrp.production" name="action_confirm"> <value eval="[ref('mrp.mrp_production_3')]"/> </function> @@ -689,32 +692,21 @@ <value eval="[ref('mrp.mrp_production_laptop_cust')]"/> </function> - <function model="mrp.production" name="action_assign"> + <function model="mrp.production" name="write"> <value eval="[ref('mrp.mrp_production_laptop_cust')]"/> + <value eval="{'qty_producing': 5, 'lot_producing_id': ref('mrp.lot_product_27_0')}"/> </function> - <function model="mrp.product.produce" name="create"> - <value model="mrp.product.produce" eval="dict( - obj().with_context(active_id=ref('mrp.mrp_production_laptop_cust')).default_get(list(obj().fields_get())), - **{ - 'qty_producing': obj().env.ref('mrp.mrp_production_laptop_cust').product_qty, - 'finished_lot_id': ref('mrp.lot_product_27_0'), - } - )"/> - </function> - - <function model="mrp.product.produce.line" name="create"> - <value model="mrp.product.produce.line" eval="[dict(item, - **{ - 'raw_product_produce_id': obj().env['mrp.product.produce'].search([('production_id', '=', obj().env.ref('mrp.mrp_production_laptop_cust').id)]).id - }) for item in obj().env['mrp.product.produce'].search([('production_id', '=', obj().env.ref('mrp.mrp_production_laptop_cust').id)])._update_workorder_lines()['to_create']]"/> + <function model="mrp.production" name="action_assign"> + <value eval="[ref('mrp.mrp_production_laptop_cust')]"/> </function> - <function model="mrp.product.produce" name="do_produce"> - <value model="mrp.product.produce" search="[('production_id', '=', obj().env.ref('mrp.mrp_production_laptop_cust').id)]"/> + <function model="stock.move" name="write"> + <value model="stock.move" eval="obj().env['stock.move'].search([('raw_material_production_id', '=', obj().env.ref('mrp.mrp_production_laptop_cust').id)]).ids"/> + <value eval="{'quantity_done': 5}"/> </function> - <function model="mrp.production" name="post_inventory"> + <function model="mrp.production" name="_post_inventory"> <value eval="[ref('mrp.mrp_production_laptop_cust')]"/> </function> diff --git a/addons/mrp/models/__init__.py b/addons/mrp/models/__init__.py index 237e0273151034748bb3293814924f50b99ea2b0..c6b32ff80fcea53e77814c465d71807c4cba903c 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 13d560f2320ef1699a2bd36ba5a648292c18ceec..0000000000000000000000000000000000000000 --- 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 362b63e5ee8cd726047d050c5a1085565136a6a6..6f0603c028f40f8d745d2d79c39dca0445c8eb22 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 f6529bff90629146de849f1444df618ab1c2e2fe..ba8d0d98e22851cf69c0c980932514e69cd1ded4 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) - @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) + order.unreserve_visible = not any_quantity_done and already_reserved + 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('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 - - def _workorders_create(self, bom, bom_data): - """ - :param bom: in case of recursive boms: we could create work orders for child - BoMs + + self.workorder_ids.leave_id.unlink() + self.workorder_ids.write({ + 'date_planned_start': False, + 'date_planned_finished': False, + }) + + 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 """ - 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 + + 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 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)) + + return issues + + 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 - # 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 - - 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 - - # 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) - - # - 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) - - moves_finished |= self.move_finished_ids.filtered(lambda move: move.product_id != self.product_id and not move.operation_id) - - 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 _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 + + 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 + @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.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() + 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 7b8f8bfd06bbc03e603a13500c0b86d7f3c194cb..1a1a33a4fcd7c3500b22fa2453389ef3ff2742a3 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 40856e8c1413c035b49e92b9c7cdb31eda7d5bb3..6d345e757630fe1e5e6541f87262df3ef52d579a 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 444976ebaedaf85af8265a386d62285bf7f30356..0c1407c7a44c4cb7dc1be78a1e0341d043dce489 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) - - 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. - """ + @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 _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 - - qty_already_consumed += wl.qty_done - qty_to_consume = self._prepare_component_quantity(move, workorder.qty_producing) - wl_to_unlink.unlink() - if float_compare(qty_to_consume, qty_already_consumed, precision_rounding=rounding) > 0: - line_values = workorder._generate_lines_values(move, qty_to_consume - qty_already_consumed) - self.env['mrp.workorder.line'].create(line_values) - - def _defaults_from_finished_workorder_line(self, reference_lot_lines): - for r_line in reference_lot_lines: - # see which lot we could suggest and its related qty_producing - if not r_line.lot_id: - continue - candidates = self.finished_workorder_line_ids.filtered(lambda line: line.lot_id == r_line.lot_id) - rounding = self.product_uom_id.rounding - if not candidates: - self.write({ - 'finished_lot_id': r_line.lot_id.id, - 'qty_producing': r_line.qty_done, - }) - return True - elif float_compare(candidates.qty_done, r_line.qty_done, precision_rounding=rounding) < 0: - self.write({ - 'finished_lot_id': r_line.lot_id.id, - 'qty_producing': r_line.qty_done - candidates.qty_done, + workorders_by_production[workorder.production_id] |= workorder + + 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 + + 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 - 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' - - @api.model - def _get_finished_workoder_inverse_name(self): - return 'finished_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 + ) - def _get_final_lots(self): - return (self.raw_workorder_id or self.finished_workorder_id).finished_lot_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_production(self): - return (self.raw_workorder_id or self.finished_workorder_id).production_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 _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 b84658d9a5b5fa7b58c1bfcfbd2ffd06617717b9..0ab3a4262bb0081750ea34008175e49af93b5b09 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 c60c9f051e67ac44a8a6adaab8ff3da157fcd50c..a3fc758cdfc04ac861666a5cd7f6e9536d6cf892 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 a7dfd5e89616b00291032a2dacce089ed9b85f6f..75d2df4a046cc94bd3f43e966008167f4a0e2446 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 05c3bafbd3dea33992456e5d74deace74d53abba..b9108828b3769a2eb06b30dd479890832804276b 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 3b3ebdf97d2d0112d597f053f5c1ba6b43958c44..73d4f9c3888cacccb2eb85faccc73926b54a0bf1 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 c1c350dffd15121ed0fd08ea64271c022ad4dcf2..795e6c7813b14dfd2694773652dc82cf33b48d6e 100644 --- a/addons/mrp/security/mrp_security.xml +++ b/addons/mrp/security/mrp_security.xml @@ -30,6 +30,11 @@ <field name="category_id" ref="base.module_category_hidden"/> </record> + <record id="group_locked_by_default" model="res.groups"> + <field name="name">Locked by default</field> + <field name="category_id" ref="base.module_category_hidden"/> + </record> + </data> <data noupdate="1"> <record id="base.default_user" model="res.users"> @@ -78,12 +83,6 @@ <field name="domain_force">['|',('company_id', 'in', company_ids),('company_id','=',False)]</field> </record> - <record model="ir.rule" id="mrp_routing_rule"> - <field name="name">mrp_routing multi-company</field> - <field name="model_id" search="[('model','=','mrp.routing')]" model="ir.model"/> - <field name="domain_force">['|',('company_id', 'in', company_ids),('company_id','=',False)]</field> - </record> - <record model="ir.rule" id="mrp_routing_workcenter_rule"> <field name="name">mrp_routing_workcenter multi-company</field> <field name="model_id" search="[('model','=','mrp.routing.workcenter')]" model="ir.model"/> 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 0000000000000000000000000000000000000000..50b4f3f893bb13d37ab341e4c96a935b28e9ee99 --- /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 = $('<span class="o_should_consume"/>'); + $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 6d515aa23409c39d28d067282f7d381b4cdf6046..7e392c09c7ace5bec220d804b6a788d39b563d0f 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 606e32f4d944dcde39c5eb0559073b37d9bb1411..da959565fc8ac9e9263b58428aa13e461b379dd4 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 5d4d1bb767dcbd1903e9713f857c97b7fcb05668..2af9d670b87eb926cc323e99807a40ec57950a89 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 382bbb849fe56a9a38575d5aa5f5db6c31df3a13..2992c6961d6c4503115b9030cb616eb3e4fe6298 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 0000000000000000000000000000000000000000..ec2e27f1eddfb52733c11269cab4dff4ca34c40a --- /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 d57a3a100cf06d03766f1628578eba7c05098ca1..8a26d9609356fdfe7e06384ddac4eb9a0e4623a8 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 58759c3e0e3abbb7ae66e19fa1819d159c826fbd..7b0ed3e092657f5189ac8857ef4848232cc90c65 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 fdbfb50531f99612e8676f93ce977a19eda058fc..d305571c8b9950024f7c6a05031bbe0b3a021988 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 5b43be0722495f7aa2b60d62a4b6be8b85296dde..6b0c3fd3a47d88e5b6be879e4c9160e47a776669 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 f13f41d44ae26bcd8f89f5cfb892008005916862..bae3b44428f066688913bea0a17170e44ad70994 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() + + action = man_order.button_mark_done() + self.assertEqual(man_order.state, 'progress', "Production order should be open a backorder wizard, then not done yet.") - man_order.button_mark_done() - self.assertEqual(man_order.state, 'done', "Production order should be in done state.") + 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() + + # Check MO backorder + mo_backorder = mo.procurement_group_id.mrp_production_ids[-1] - 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.') + 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') - - product_produce.do_produce() + 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() + + # 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_location, 100) + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) - 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) + mo.action_assign() - 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) + mo_form = Form(mo) - 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() + # 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) + 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.assertEqual(len(produce_wizard.raw_workorder_line_ids), 2) - produce_wizard.do_produce() + self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100) + self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5) - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - produce_form.qty_producing = 1 + mo.action_assign() - produce_wizard = produce_form.save() + mo_form = Form(mo) - self.assertEqual(len(produce_wizard.raw_workorder_line_ids), 2) - produce_wizard.do_produce() + # 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() - 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.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, 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) - mo.button_mark_done() + # 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() - 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) + # 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) - - 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) - - 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) - - 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) - - 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() - - 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() - - 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) - - 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) - - 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() - - produce_wizard.do_produce() + 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) + + 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) + + 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) + + 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) + + 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) + + 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) + + 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] + + mo_form = Form(mo2) + mo_form.qty_producing = 1 + mo2 = mo_form.save() - 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_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) + + 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) + + 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 - - product_produce.do_produce() + mo_form = Form(mo) + mo_form.qty_producing = 1/12.0 + mo_form.lot_producing_id = final_product_lot + mo = mo_form.save() + 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 cf60399acaa0cbe4663a0de5618905e3cfeae2cd..89be67074bda6adc5b82c40e3ae5993e162b5a2b 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 f2c012737c7a610f66b37b7c349ace4ee5b58be5..64587e1cde98d61419156a37d97de25ede85c271 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 ca93a71c273cc97a17429bd10a28ee9826c27a96..ea861c2cefe1c7f1d1f3600779ef7a29f9e2419f 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], - })) + mo_form = Form(mo) + mo_form.qty_producing = 1 + if finished_product.tracking != 'none': + 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() - if finished_product.tracking != 'serial': - produce_form.qty_producing = 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.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() - 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() - 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() + + 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) + # 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 79aabcd09788be4085ca1b99a46bfc18e6955d09..159b4c3df3c06268703516975a75a5fcb5ebc37f 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 - - 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') + 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() + + 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 a04f9078acc84273f6bace0ae0738bb37d248b47..408045ae274fdcd4cb69985f9a29348df2fd862d 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 de8b40ae1b494bb438e3fdb5ff6b2f86451fb00b..0000000000000000000000000000000000000000 --- 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 768f08f22eed9cba936a124194a9ef02c8b2542d..995f8e2bbeaa65435cc4450aa842bd01f48a6f8d 100644 --- a/addons/mrp/views/mrp_bom_views.xml +++ b/addons/mrp/views/mrp_bom_views.xml @@ -14,7 +14,7 @@ <field name="arch" type="xml"> <form string="Byproduct"> <group> - <field name="routing_id" invisible="1"/> + <field name="allowed_operation_ids" invisible="1"/> <field name="company_id"/> <field name="product_id"/> <label for="product_qty"/> @@ -36,6 +36,11 @@ <form string="Bill of Material"> <sheet> <div class="oe_button_box" name="button_box"> + <button name="%(action_mrp_routing_time)d" type="action" class="oe_stat_button" icon="fa-clock-o" groups="mrp.group_mrp_routings"> + <div class="o_field_widget o_stat_info"> + <span class="o_stat_text">Routing<br/>Performance</span> + </div> + </button> <button name="%(action_report_mrp_bom)d" type="action" class="oe_stat_button" icon="fa-bars" string="Structure & Cost"/> </div> @@ -51,7 +56,6 @@ <field name="product_qty"/> <field name="product_uom_id" options="{'no_open':True,'no_create':True}" groups="uom.group_uom"/> </div> - <field name="routing_id" attrs="{'invisible': [('type','not in',('normal','phantom'))]}" groups="mrp.group_mrp_routings" context="{'default_company_id': company_id}"/> </group> <group> <field name="code"/> @@ -72,10 +76,9 @@ </group> <notebook> <page string="Components" name="components"> - <field name="bom_line_ids" widget="one2many" context="{'default_parent_product_tmpl_id': product_tmpl_id, 'default_product_id': False, 'default_company_id': company_id, 'default_routing_id': routing_id}"> + <field name="bom_line_ids" widget="one2many" context="{'default_parent_product_tmpl_id': product_tmpl_id, 'default_product_id': False, 'default_company_id': company_id, 'default_bom_id': id}"> <tree string="Components" editable="bottom"> <field name="company_id" invisible="1"/> - <field name="routing_id" invisible="1"/> <field name="sequence" widget="handle"/> <field name="product_id" context="{'default_type': 'product'}"/> <field name="product_tmpl_id" invisible="1"/> @@ -88,21 +91,31 @@ <field name="possible_bom_product_template_attribute_value_ids" invisible="1"/> <field name="product_uom_id" options="{'no_open':True,'no_create':True}" groups="uom.group_uom"/> <field name="bom_product_template_attribute_value_ids" widget="many2many_tags" options="{'no_create': True}" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}" groups="product.group_product_variant"/> - <field name="operation_id" groups="mrp.group_mrp_routings" attrs="{'column_invisible': [('parent.type','not in', ('normal', 'phantom'))]}" options="{'no_quick_create':True,'no_create_edit':True}"/> + <field name="allowed_operation_ids" invisible="1"/> + <field name="operation_id" groups="mrp.group_mrp_routings" optional="hidden" attrs="{'column_invisible': [('parent.type','not in', ('normal', 'phantom'))]}" options="{'no_quick_create':True,'no_create_edit':True}"/> </tree> </field> </page> + <page string="Operations" + name="operations" + attrs="{'invisible': [('type','not in',('normal','phantom'))]}" + groups="mrp.group_mrp_routings"> + <field name="operation_ids" + attrs="{'invisible': [('type','not in',('normal','phantom'))]}" + groups="mrp.group_mrp_routings" + context="{'default_company_id': company_id}"/> + </page> <page string="By-products" name="by_products" attrs="{'invisible': [('type','!=','normal')]}" groups="mrp.group_mrp_byproducts"> - <field name="byproduct_ids" context="{'form_view_ref' : 'mrp.mrp_bom_byproduct_form_view', 'default_company_id': company_id, 'default_routing_id': routing_id}"> + <field name="byproduct_ids" context="{'form_view_ref' : 'mrp.mrp_bom_byproduct_form_view', 'default_company_id': company_id, 'default_bom_id': id}"> <tree string="By-products" editable="top"> <field name="company_id" invisible="1"/> - <field name="routing_id" invisible="1"/> <field name="product_id" context="{'default_type': 'product'}"/> <field name="product_qty"/> <field name="product_uom_id" groups="uom.group_uom"/> + <field name="allowed_operation_ids" invisible="1"/> <field name="operation_id" groups="mrp.group_mrp_routings" options="{'no_quick_create':True,'no_create_edit':True}"/> </tree> </field> @@ -110,8 +123,8 @@ <page string="Miscellaneous" name="miscellaneous"> <group> <group> - <field name="ready_to_produce" attrs="{'invisible': [('type','!=','normal')]}" string="Manufacturing Readiness" groups="mrp.group_mrp_routings"/> - <field name="consumption" attrs="{'invisible': [('type','!=','normal')]}"/> + <field name="ready_to_produce" attrs="{'invisible': [('type','!=','normal')]}" string="Manufacturing Readiness" widget="radio" groups="mrp.group_mrp_routings"/> + <field name="consumption" attrs="{'invisible': [('type','!=','normal')]}" widget="radio"/> </group> <group> <field name="picking_type_id" attrs="{'invisible': [('type','!=','normal')]}" string="Operation" groups="stock.group_adv_location"/> @@ -142,7 +155,6 @@ <field name="company_id" groups="base.group_multi_company" optional="show"/> <field name="product_qty" optional="show"/> <field name="product_uom_id" groups="uom.group_uom" optional="show" string="Unit of Measure"/> - <field name="routing_id" groups="mrp.group_mrp_routings" optional="show"/> </tree> </field> </record> @@ -187,7 +199,6 @@ <filter string="Product" name="product" domain="[]" context="{'group_by': 'product_tmpl_id'}"/> <filter string='BoM Type' name="group_by_type" domain="[]" context="{'group_by' : 'type'}"/> <filter string='Unit of Measure' name="default_unit_of_measure" domain="[]" context="{'group_by' : 'product_uom_id'}"/> - <filter string="Routing" name="routings" domain="[]" context="{'group_by': 'routing_id'}"/> </group> </search> </field> @@ -200,7 +211,7 @@ <field name="domain">[]</field> <!-- force empty --> <field name="view_mode">tree,kanban,form</field> <field name="search_view_id" ref="view_mrp_bom_filter"/> - <field name="context">{'search_default_group_by_type': True, 'default_company_id': allowed_company_ids[0]}</field> + <field name="context">{'default_company_id': allowed_company_ids[0]}</field> <field name="help" type="html"> <p class="o_view_nocontent_smiling_face"> Create a bill of materials @@ -239,8 +250,8 @@ </group> <group string="Operation"> <field name="company_id" invisible="1"/> - <field name="routing_id" invisible="1"/> <field name="sequence" groups="base.group_no_one"/> + <field name="allowed_operation_ids" invisible="1"/> <field name="operation_id" groups="mrp.group_mrp_routings"/> </group> </group> diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index e7b3c222a19b7b6bbf00a8d3cb13255301ccb923..93fbb0a334b7aeeec380a73017ef7aa03d1a4bb7 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -14,10 +14,10 @@ <field name="date_deadline" widget="remaining_days" attrs="{'invisible': [('state', 'in', ['done', 'cancel'])]}" optional="hide"/> <field name="product_id" readonly="1" optional="show"/> <field name="product_uom_id" string="Unit of Measure" options="{'no_open':True,'no_create':True}" groups="uom.group_uom" optional="show"/> + <field name="lot_producing_id" optional="hide"/> <field name="bom_id" readonly="1" optional="hide"/> <field name="origin" optional="show"/> <field name="user_id" optional="hide" widget="many2one_avatar_user"/> - <field name="routing_id" groups="mrp.group_mrp_routings" optional="show"/> <field name="reservation_state" optional="show"/> <field name="product_qty" sum="Total Qty" string="Quantity" readonly="1" optional="show"/> <field name="company_id" readonly="1" groups="base.group_multi_company" optional="show"/> @@ -39,6 +39,20 @@ <field name="code">records.button_plan()</field> </record> + <record id="action_production_order_mark_done" model="ir.actions.server"> + <field name="name">Mark as Done</field> + <field name="model_id" ref="mrp.model_mrp_production"/> + <field name="binding_model_id" ref="mrp.model_mrp_production"/> + <field name="binding_view_types">list</field> + <field name="state">code</field> + <field name="code"> + if records: + res = records.filtered(lambda mo: mo.state in {'to_close', 'progress'}).button_mark_done() + if res is not True: + action = res + </field> + </record> + <record id="mrp_production_form_view" model="ir.ui.view"> <field name="name">mrp.production.form</field> <field name="model">mrp.production</field> @@ -46,51 +60,37 @@ <form string="Manufacturing Orders"> <header> <field name="confirm_cancel" invisible="1"/> + <field name="show_lock" invisible="1"/> + <button name="button_mark_done" attrs="{'invisible': ['|', ('state', '!=', 'progress'), ('qty_producing', '=', 0)]}" string="Backorder" type="object" class="oe_highlight"/> <button name="button_mark_done" attrs="{'invisible': [('state', '!=', 'to_close')]}" string="Mark as Done" type="object" class="oe_highlight"/> - <button name="action_confirm" attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('is_locked', '=', False)]}" string="Mark as Todo" type="object" class="oe_highlight"/> - <button name="action_assign" attrs="{'invisible': ['|', '|', ('is_locked', '=', False), ('state', 'in', ('draft', 'done', 'cancel')), ('reserve_visible', '=', False)]}" string="Check availability" type="object" class="oe_highlight"/> - <button name="button_plan" attrs="{'invisible': ['|', ('state', '!=', 'confirmed'), ('routing_id', '=', False)]}" type="object" string="Plan" class="oe_highlight"/> + <button name="action_confirm" attrs="{'invisible': [('state', '!=', 'draft')]}" string="Confirm" type="object" class="oe_highlight"/> + <button name="action_assign" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'done', 'cancel')), ('reserve_visible', '=', False)]}" string="Check availability" type="object"/> + <button name="button_plan" attrs="{'invisible': ['|', '|', ('state', 'not in', ('confirmed', 'progress')), ('workorder_ids', '=', []), ('is_planned', '=', True)]}" type="object" string="Plan" class="oe_highlight"/> <button name="button_unplan" type="object" string="Unplan" attrs="{'invisible': ['|', '|', ('state', '!=', 'planned'), ('date_planned_start', '=', False), ('date_planned_finished', '=', False)]}"/> - <button name="open_produce_product" attrs="{'invisible': ['|', '|', '|', ('state', '=', 'to_close'), ('is_locked', '=', False), ('reservation_state', '!=', 'assigned'), ('routing_id', '!=', False)]}" string="Produce" type="object" class="oe_highlight"/> - <button name="open_produce_product" attrs="{'invisible': ['|', '|', '|', ('state', '=', 'to_close'), ('is_locked', '=', False), ('reservation_state', 'not in', ('confirmed', 'waiting')), ('routing_id', '!=', False)]}" string="Produce" type="object"/> - <button name="post_inventory" string="Post Inventory" type="object" attrs="{'invisible': [('post_visible', '=', False)]}"/> - <button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': ['|', ('state', 'in', ('cancel', 'draft')), ('is_locked', '=', False)]}"/> <button name="button_unreserve" type="object" string="Unreserve" attrs="{'invisible': [('unreserve_visible', '=', False)]}"/> + <button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': [('state', 'in', ('cancel', 'draft'))]}"/> <field name="state" widget="statusbar" statusbar_visible="draft,confirmed,assigned,done"/> - <button name="action_toggle_is_locked" attrs="{'invisible': ['|', '|', ('state', 'in', ('cancel', 'draft')), ('id', '=', False), ('is_locked', '=', False)]}" string="Unlock" groups="mrp.group_mrp_manager" type="object" help="Unlock the manufacturing order to correct what has been consumed or produced."/> - <button name="action_toggle_is_locked" attrs="{'invisible': [('is_locked', '=', True)]}" string="Lock" class="oe_highlight" groups="mrp.group_mrp_manager" type="object"/> + <button name="action_toggle_is_locked" attrs="{'invisible': ['|', ('show_lock', '=', False), ('is_locked', '=', False)]}" string="Unlock" groups="mrp.group_mrp_manager" type="object" help="Unlock the manufacturing order to adjust what has been consumed or produced."/> + <button name="action_toggle_is_locked" attrs="{'invisible': ['|', ('show_lock', '=', False), ('is_locked', '=', True)]}" string="Lock" groups="mrp.group_mrp_manager" type="object" help="Lock the manufacturing order to prevent changes to what has been consumed or produced."/>/> <button name="action_cancel" type="object" string="Cancel" - attrs="{'invisible': ['|', '|', '|', ('id', '=', False), ('is_locked', '=', False), ('state', 'in', ('done','cancel')), ('confirm_cancel', '=', True)]}"/> + attrs="{'invisible': ['|', '|', ('id', '=', False), ('state', 'in', ('done', 'cancel')), ('confirm_cancel', '=', True)]}"/> <button name="action_cancel" type="object" string="Cancel" - attrs="{'invisible': ['|', '|', '|', ('id', '=', False), ('is_locked', '=', False), ('state', 'in', ('done','cancel')), ('confirm_cancel', '=', False)]}" + attrs="{'invisible': ['|', '|', ('id', '=', False), ('state', 'in', ('done', 'cancel')), ('confirm_cancel', '=', False)]}" confirm="Some product moves have already been confirmed, this manufacturing order can't be completely cancelled. Are you still sure you want to process ?"/> <button name="button_unbuild" type="object" string="Unbuild" attrs="{'invisible': [('state', '!=', 'done')]}"/> </header> <sheet> <field name="reservation_state" invisible="1"/> + <field name="is_partially_planned" invisible="1"/> + <field name="date_planned_finished" invisible="1"/> <field name="is_locked" invisible="1"/> - <field name="post_visible" invisible="1"/> + <field name="qty_produced" invisible="1"/> <field name="unreserve_visible" invisible="1"/> <field name="reserve_visible" invisible="1"/> <field name="consumption" invisible="1"/> + <field name="is_planned" invisible="1"/> + <field name="workorder_ids" invisible="1"/> <div class="oe_button_box" name="button_box"> - <button name="%(stock.action_stock_report)d" icon="fa-arrow-up" class="oe_stat_button" string="Traceability" type="action" states="done" groups="stock.group_production_lot"/> - <button name="%(action_mrp_workorder_production_specific)d" type="action" attrs="{'invisible': [('workorder_count', '=', 0)]}" class="oe_stat_button" icon="fa-play-circle-o"> - <div class="o_field_widget o_stat_info"> - <span class="o_stat_value"><field name="workorder_done_count" widget="statinfo" nolabel="1"/> / <field name="workorder_count" widget="statinfo" nolabel="1"/></span> - <span class="o_stat_text">Work Orders</span> - </div> - </button> - <button name="%(action_mrp_production_moves)d" type="action" string="Product Moves" class="oe_stat_button" icon="fa-exchange" attrs="{'invisible': [('state', 'not in', ('progress', 'done'))]}"/> - <button type="object" name="action_view_mo_delivery" class="oe_stat_button" icon="fa-truck" groups="base.group_user" attrs="{'invisible': [('delivery_count', '=', 0)]}"> - <field name="delivery_count" widget="statinfo" string="Transfers"/> - </button> - <button class="oe_stat_button" name="action_see_move_scrap" type="object" icon="fa-arrows-v" attrs="{'invisible': [('scrap_count', '=', 0)]}"> - <div class="o_field_widget o_stat_info"> - <span class="o_stat_value"><field name="scrap_count"/></span> - <span class="o_stat_text">Scraps</span> - </div> - </button> <button class="oe_stat_button" name="action_view_mrp_production_childs" type="object" icon="fa-wrench" attrs="{'invisible': [('mrp_production_child_count', '=', 0)]}"> <div class="o_field_widget o_stat_info"> <span class="o_stat_value"><field name="mrp_production_child_count"/></span> @@ -103,7 +103,23 @@ <span class="o_stat_text">Source MO</span> </div> </button> - <field name="workorder_ids" invisible="1"/> + <button class="oe_stat_button" name="action_view_mrp_production_backorders" type="object" icon="fa-wrench" attrs="{'invisible': [('mrp_production_backorder_count', '<', 2)]}"> + <div class="o_field_widget o_stat_info"> + <span class="o_stat_value"><field name="mrp_production_backorder_count"/></span> + <span class="o_stat_text">Backorder MO's</span> + </div> + </button> + <button class="oe_stat_button" name="action_see_move_scrap" type="object" icon="fa-arrows-v" attrs="{'invisible': [('scrap_count', '=', 0)]}"> + <div class="o_field_widget o_stat_info"> + <span class="o_stat_value"><field name="scrap_count"/></span> + <span class="o_stat_text">Scraps</span> + </div> + </button> + <button type="object" name="action_view_mo_delivery" class="oe_stat_button" icon="fa-truck" groups="base.group_user" attrs="{'invisible': [('delivery_count', '=', 0)]}"> + <field name="delivery_count" widget="statinfo" string="Transfers"/> + </button> + <button name="%(stock.action_stock_report)d" icon="fa-arrow-up" class="oe_stat_button" string="Traceability" type="action" states="done" groups="stock.group_production_lot"/> + <button name="%(action_mrp_production_moves)d" type="action" string="Product Moves" class="oe_stat_button" icon="fa-exchange" attrs="{'invisible': [('state', 'not in', ('progress', 'done'))]}"/> </div> <div class="oe_title"> <h1><field name="name" placeholder="Manufacturing Reference" nolabel="1"/></h1> @@ -112,68 +128,41 @@ <group> <field name="id" invisible="1"/> <field name="allowed_product_ids" invisible="1"/> + <field name="product_tracking" invisible="1"/> <field name="product_id" attrs="{'readonly': [('state', '!=', 'draft')]}"/> <field name="product_tmpl_id" invisible="1"/> - <field name="product_description_variants" attrs="{'invisible': [('product_description_variants', '=', False)], 'readonly': [('state', '!=', 'draft')]}"/> - <label for="product_qty"/> + <field name="product_description_variants" attrs="{'invisible': [('product_description_variants', 'in', (False, ''))], 'readonly': [('state', '!=', 'draft')]}"/> + <label for="product_qty" string="Quantity"/> <div class="o_row no-gutters d-flex"> - <div class="col"> - <field name="product_qty" class="mr-1" attrs="{'readonly': [('state', '!=', 'draft')]}"/> - <field name="product_uom_category_id" invisible="1"/> - <field name="product_uom_id" options="{'no_open':True,'no_create':True}" force_save="1" groups="uom.group_uom" attrs="{'readonly': [('state', '!=', 'draft')]}"/> - <button type="action" - name="%(mrp.action_change_production_qty)d" - context="{'default_mo_id': id}" - string="Update" class="oe_link pt-0" attrs="{'invisible': ['|', '|', ('state', 'in', ('draft', 'done','cancel')), ('id', '=', False), ('bom_id', '=', False)]}"/> + <div attrs="{'invisible': [('state', '=', 'draft')]}" class="o_row"> + <field name="qty_producing" digits="[12,3]" class="text-left" attrs="{'readonly': ['|', ('state', '=', 'cancel'), '&', ('state', '=', 'done'), ('is_locked', '=', True)]}"/> + / </div> + <field name="product_qty" class="oe_inline text-left" attrs="{'readonly': [('state', '!=', 'draft')], 'invisible': [('state', 'not in', ('draft', 'done'))]}"/> + <button type="action" name="%(mrp.action_change_production_qty)d" + context="{'default_mo_id': id}" class="oe_link oe_inline" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'done','cancel')), ('id', '=', False)]}"> + <field name="product_qty" class="oe_inline" attrs="{'readonly': [('state', '!=', 'draft')]}"/> + </button> + <label for="product_uom_id" string="" class="oe_inline"/> + <field name="product_uom_category_id" invisible="1"/> + <field name="product_uom_id" options="{'no_open': True, 'no_create': True}" force_save="1" groups="uom.group_uom" attrs="{'readonly': [('state', '!=', 'draft')]}" class="oe_inline"/> + <span class='text-bf'>To Produce</span> + </div> + <label for="lot_producing_id" attrs="{'invisible': [('product_tracking', 'in', ('none', False))]}"/> + <div class="o_row" attrs="{'invisible': [('product_tracking', 'in', ('none', False))]}"> + <field name="lot_producing_id" + context="{'default_product_id': product_id, 'default_company_id': company_id}" attrs="{'invisible': [('product_tracking', 'in', ('none', False))]}"/> + <button name="action_generate_serial" type="object" class="btn btn-primary fa fa-plus-square-o" aria-label="Creates a new serial/lot number" title="Creates a new serial/lot number" role="img" attrs="{'invisible': ['|', ('product_tracking', 'in', ('none', False)), ('lot_producing_id', '!=', False)]}"/> </div> <field name="bom_id" context="{'default_product_tmpl_id': product_tmpl_id}" attrs="{'readonly': [('state', '!=', 'draft')]}"/> - <field name="routing_id" groups="mrp.group_mrp_routings"/> </group> <group> - <field name="date_deadline" attrs="{'readonly': [('state', 'in', ['done', 'cancel'])]}"/> - <div class="o_row o_td_label"> - <label for="date_planned_start" string="Planned Date" - attrs="{'invisible': [ - ('routing_id', '!=', False), - ('state', 'in', ['draft', 'confirmed']) - ]}"/> - <label for="date_planned_start" string="Plan from" - attrs="{'invisible': [ - '|', - ('routing_id', '=', False), - ('state', 'not in', ['draft', 'confirmed']) - ]}"/> - </div> + <field name="date_deadline"/> + <label for="date_planned_start"/> <div class="o_row"> <field name="date_planned_start" - attrs="{'readonly': [ - '|', - '&', - ('routing_id', '=', False), - ('state', 'in', ['done', 'cancel']), - '&', - ('routing_id', '!=', False), - ('state', 'not in', ['draft', 'confirmed'])]}"/> - <label for="date_planned_finished" string="to" attrs="{'invisible': [ - '|', - ('id', '=', False), - '&', - ('routing_id', '!=', False), - ('state', 'in', ['draft', 'confirmed']) - ]}"/> - <field name="date_planned_finished" required="1" - attrs="{'readonly': [ - '|', - '&', - ('routing_id', '=', False), - ('state', 'in', ['done', 'cancel']), - ('routing_id', '!=', False) - ], 'invisible': [ - ('routing_id', '!=', False), - ('state', 'in', ['draft', 'confirmed']) - ]}"/> + attrs="{'readonly': ['|', ('is_partially_planned', '=', True), ('state', 'in', ['close', 'cancel'])]}"/> <field name="delay_alert_date" invisible="1"/> <field string=" " name="json_popover" widget="stock_rescheduling_popover" attrs="{'invisible': [('delay_alert_date', '=', False)]}"/> </div> @@ -182,31 +171,150 @@ <field name="company_id" groups="base.group_multi_company" options="{'no_create': True}" attrs="{'readonly': [('state', '!=', 'draft')]}" force_save="1"/> <field name="show_final_lots" invisible="1"/> <field name="production_location_id" invisible="1" readonly="1"/> + <field name="move_finished_ids" invisible="1" attrs="{'readonly': ['|', ('state', '=', 'cancel'), '&', ('state', '=', 'done'), ('is_locked', '=', True)]}"> + <tree editable="bottom"> + <field name="product_id"/> + <field name="product_uom_qty"/> + <field name="product_uom"/> + <field name="operation_id"/> + <field name="byproduct_id"/> + <field name="name"/> + <field name="date"/> + <field name="date_expected"/> + <field name="picking_type_id"/> + <field name="location_id"/> + <field name="location_dest_id"/> + <field name="company_id"/> + <field name="warehouse_id"/> + <field name="origin"/> + <field name="group_id"/> + <field name="propagate_date"/> + <field name="propagate_cancel"/> + <field name="delay_alert"/> + <field name="propagate_date_minimum_delta"/> + <field name="move_dest_ids"/> + <field name="state"/> + <!-- Useless as the editable in tree declaration -> For Form Test--> + <field name="product_uom_category_id"/> + <field name="allowed_operation_ids"/> + </tree> + </field> </group> </group> <notebook> <page string="Components" name="components"> - <field name="move_raw_ids" context="{'final_lots': show_final_lots, 'tree_view_ref': 'mrp.view_stock_move_raw_tree', 'form_view_ref': 'mrp.view_stock_move_lots', 'default_location_id': location_src_id, 'default_location_dest_id': production_location_id, 'default_state': 'draft', 'default_raw_material_production_id': id, 'default_picking_type_id': picking_type_id}" attrs="{'readonly': ['&', '&', ('state', '!=', 'draft'), ('is_locked', '=', True), '|', ('state', '!=', 'confirmed'), ('consumption', '=', 'strict')]}"/> + <field name="move_raw_ids" + context="{'default_date_expected': date_planned_start, 'default_location_id': location_src_id, 'default_location_dest_id': production_location_id, 'default_state': 'draft', 'default_raw_material_production_id': id, 'default_picking_type_id': picking_type_id, 'default_company_id': company_id}" + attrs="{'readonly': ['|', ('state', '=', 'cancel'), '&', ('state', '=', 'done'), ('is_locked', '=', True)]}"> + <tree delete="0" default_order="is_done,sequence" editable="bottom"> + <field name="product_id" required="1" attrs="{'readonly': ['|', '|', ('has_move_lines', '=', True), ('state', '=', 'cancel'), '&', ('state', '!=', 'draft'), ('additional', '=', False) ]}"/> + + <field name="move_line_ids" invisible="1"> + <tree> + <field name="lot_id" invisible="1"/> + <field name="owner_id" invisible="1"/> + <field name="package_id" invisible="1"/> + <field name="result_package_id" invisible="1"/> + <field name="location_id" invisible="1"/> + <field name="location_dest_id" invisible="1"/> + <field name="qty_done" invisible="1"/> + <field name="product_id" invisible="1"/> + <field name="product_uom_id" invisible="1"/> + <field name="product_uom_qty" invisible="1"/> + <field name="state" invisible="1"/> + <field name="move_id" invisible="1"/> + <field name="id" invisible="1"/> + </tree> + </field> + + <field name="company_id" invisible="1"/> + <field name="product_uom_category_id" invisible="1"/> + <field name="name" invisible="1"/> + <field name="allowed_operation_ids" invisible="1"/> + <field name="unit_factor" invisible="1"/> + <field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft')]}" options="{'no_open': True, 'no_create': True}" groups="uom.group_uom"/> + <field name="date" invisible="1"/> + <field name="date_expected" invisible="1"/> + <field name="additional" invisible="1"/> + <field name="picking_type_id" invisible="1"/> + <field name="has_tracking" invisible="1"/> + <field name="operation_id" invisible="1"/> + <field name="is_done" invisible="1"/> + <field name="bom_line_id" invisible="1"/> + <field name="sequence" invisible="1"/> + <field name="location_id" invisible="1"/> + <field name="warehouse_id" invisible="1"/> + <field name="is_locked" invisible="1"/> + <field name="has_move_lines" invisible="1"/> + <field name="location_dest_id" domain="[('id', 'child_of', parent.location_dest_id)]" invisible="1"/> + <field name="state" invisible="1" force_save="1"/> + <field name="should_consume_qty" invisible="1"/> + <field name="product_uom_qty" widget="mrp_should_consume" string="To Consume" attrs="{'readonly': ['&', ('parent.state', '!=', 'draft'), '|', ('parent.state', 'not in', ('confirmed', 'planned', 'progress', 'to_close')), ('parent.is_locked', '=', True)]}" width="1"/> + <field name="reserved_availability" attrs="{'invisible': [('is_done', '=', True)], 'column_invisible': [('parent.state', 'in', ('draft', 'done'))]}" string="Reserved" decoration-danger="not is_done and reserved_availability < product_uom_qty and product_uom_qty - reserved_availability > 0.0001"/> + <field name="is_quantity_done_editable" invisible="1"/> + <field name="quantity_done" string="Consumed" + decoration-success="not is_done and (quantity_done - should_consume_qty == 0)" + decoration-warning="not is_done and (quantity_done - should_consume_qty > 0.0001)" + attrs="{'column_invisible': [('parent.state', '=', 'draft')], 'readonly': [('show_details_visible', '=', True)]}"/> + <field name="show_details_visible" invisible="1"/> + <button name="action_show_details" type="object" icon="fa-list" attrs="{'invisible': [('show_details_visible', '=', False)]}" options="{"warn": true}"/> + </tree> + </field> </page> - <page string="Finished Products" name="finished_products"> - <field name="move_finished_ids" invisible="1"/> - <field name="finished_move_line_ids" context="{'form_view_ref': 'mrp.view_finisehd_move_line'}" attrs="{'readonly': [('is_locked', '=', True)], 'invisible': [('finished_move_line_ids', '=', [])]}"> - <tree default_order="done_move" editable="bottom" create="0" delete="0" decoration-muted="state in ('done', 'cancel')"> - <field name="product_id" readonly="1"/> + <page string="By-Products" name="finished_products" groups="mrp.group_mrp_byproducts"> + <field name="move_byproduct_ids" context="{'default_location_id': production_location_id, 'default_location_dest_id': location_src_id, 'default_state': 'draft', 'default_production_id': id, 'default_picking_type_id': picking_type_id, 'default_company_id': company_id}" attrs="{'readonly': ['|', ('state', '=', 'cancel'), '&', ('state', '=', 'done'), ('is_locked', '=', True)]}"> + <tree delete="0" default_order="is_done,sequence" decoration-muted="is_done" editable="bottom"> + <field name="product_id" domain="[('id', '!=', parent.product_id)]" required="1"/> + + <field name="move_line_ids" invisible="1"> + <tree> + <field name="lot_id" invisible="1"/> + <field name="owner_id" invisible="1"/> + <field name="package_id" invisible="1"/> + <field name="result_package_id" invisible="1"/> + <field name="location_id" invisible="1"/> + <field name="location_dest_id" invisible="1"/> + <field name="qty_done" invisible="1"/> + <field name="product_id" invisible="1"/> + <field name="product_uom_id" invisible="1"/> + <field name="product_uom_qty" invisible="1"/> + <field name="state" invisible="1"/> + <field name="move_id" invisible="1"/> + <field name="id" invisible="1"/> + </tree> + </field> + <field name="company_id" invisible="1"/> - <field name="lot_id" groups="stock.group_production_lot" context="{'default_product_id': product_id}" attrs="{'invisible': [('lots_visible', '=', False)]}"/> - <field name="product_uom_id" groups="uom.group_uom"/> - <field name="qty_done" string="Produced"/> - <field name="lots_visible" invisible="1"/> - <field name="done_move" invisible="1"/> - <field name="state" invisible="1"/> <field name="product_uom_category_id" invisible="1"/> + <field name="name" invisible="1"/> + <field name="allowed_operation_ids" invisible="1"/> + <field name="unit_factor" invisible="1"/> + <field name="product_uom" groups="uom.group_uom"/> + <field name="date" invisible="1"/> + <field name="date_expected" invisible="1"/> + <field name="additional" invisible="1"/> + <field name="picking_type_id" invisible="1"/> + <field name="has_tracking" invisible="1"/> + <field name="operation_id" invisible="1"/> + <field name="is_done" invisible="1"/> + <field name="bom_line_id" invisible="1"/> + <field name="sequence" invisible="1"/> + <field name="location_id" invisible="1"/> + <field name="warehouse_id" invisible="1"/> + <field name="is_locked" invisible="1"/> + <field name="has_move_lines" invisible="1"/> + <field name="location_dest_id" domain="[('id', 'child_of', parent.location_dest_id)]" invisible="1"/> + <field name="state" invisible="1" force_save="1"/> + <field name="product_uom_qty" string="To Produce" attrs="{'readonly': ['&', ('parent.state', '!=', 'draft'), '|', ('parent.state', 'not in', ('confirmed', 'planned', 'progress', 'to_close')), ('parent.is_locked', '=', True)]}"/> + <field name="is_quantity_done_editable" invisible="1"/> + <field name="quantity_done" string="Produced" attrs="{'column_invisible': [('parent.state', '=', 'draft')], 'readonly': [('is_quantity_done_editable', '=', False)]}"/> + <field name="show_details_visible" invisible="1"/> + <button name="action_show_details" type="object" icon="fa-list" attrs="{'invisible': [('show_details_visible', '=', False)]}" options="{"warn": true}"/> </tree> </field> - <p attrs="{'invisible': [('finished_move_line_ids', '!=', [])]}"> - Use the Produce button or process the work orders to create some finished products. - </p> </page> + <page string="Operations" name="operations" groups="mrp.group_mrp_routings"> + <field name="workorder_ids" attrs="{'readonly': [('state', 'in', ['cancel', 'done'])]}" context="{'tree_view_ref': 'mrp.mrp_production_workorder_tree_editable_view', 'default_production_id': id, 'default_product_uom_id': product_uom_id, 'default_consumption': consumption, 'default_company_id': company_id}"/> </page> <page string="Miscellaneous" name="miscellaneous" groups="stock.group_stock_multi_locations"> <group> <group> @@ -273,8 +381,7 @@ <field eval="2" name="priority"/> <field name="arch" type="xml"> <calendar date_start="date_planned_start" date_stop="date_planned_finished" - string="Manufacturing Orders" color="routing_id" event_limit="5" quick_add="False"> - <field name="routing_id" filters="1"/> + string="Manufacturing Orders" event_limit="5" quick_add="False"> <field name="user_id" avatar_field="image_128"/> <field name="product_id"/> <field name="product_qty"/> @@ -286,7 +393,7 @@ <field name="name">mrp.production.gantt</field> <field name="model">mrp.production</field> <field name="arch" type="xml"> - <gantt date_stop="date_finished" date_start="date_start" string="Productions" default_group_by="routing_id" create="0"> + <gantt date_stop="date_finished" date_start="date_start" string="Productions" create="0"> </gantt> </field> </record> @@ -319,8 +426,7 @@ <field name="name" string="Manufacturing Order" filter_domain="['|', ('name', 'ilike', self), ('origin', 'ilike', self)]"/> <field name="product_id"/> <field name="move_raw_ids" string="Component" filter_domain="[('move_raw_ids.product_id', 'ilike', self)]"/> - <field name="name" string="Work Center" filter_domain="[('routing_id.operation_ids.workcenter_id', 'ilike', self)]"/> - <field name="routing_id" groups="mrp.group_mrp_routings"/> + <field name="name" string="Work Center" filter_domain="[('bom_id.operation_ids.workcenter_id', 'ilike', self)]"/> <field name="origin"/> <filter string="To Do" name="todo" domain="[('state', 'in', ('draft', 'confirmed', 'planned','progress', 'to_close'))]" help="Manufacturing Orders which are in confirmed state."/> @@ -352,7 +458,6 @@ domain="[('activity_exception_decoration', '!=', False)]"/> <group expand="0" string="Group By..."> <filter string="Product" name="product" domain="[]" context="{'group_by': 'product_id'}"/> - <filter string="Routing" name="routing" domain="[]" context="{'group_by': 'routing_id'}" groups="mrp.group_mrp_routings"/> <filter string="Status" name="status" domain="[]" context="{'group_by': 'state'}"/> <filter string="Material Availability" name="groupby_reservation_state" domain="[]" context="{'group_by': 'reservation_state'}"/> <filter string="Scheduled Date" name="scheduled_date" domain="[]" context="{'group_by': 'date_planned_start'}" help="Scheduled Date by Month"/> diff --git a/addons/mrp/views/mrp_routing_views.xml b/addons/mrp/views/mrp_routing_views.xml index eeb55498856fc3ccec0035262338d54cbd249622..cb0041687617f0121130fd88893ec9014e6be41e 100644 --- a/addons/mrp/views/mrp_routing_views.xml +++ b/addons/mrp/views/mrp_routing_views.xml @@ -10,7 +10,7 @@ <field name="sequence" widget="handle"/> <field name="name"/> <field name="workcenter_id"/> - <field name="time_cycle" widget="float_time" string="Duration (minutes)" sum="Total Duration"/> + <field name="time_cycle" widget="float_time" string="Duration (minutes)" sum="Total Duration" width="1.5"/> </tree> </field> </record> @@ -21,7 +21,6 @@ <field name="arch" type="xml"> <form string="Routing Work Centers"> <sheet> - <field name="routing_id" invisible="1" required="0"/> <group> <group name="description"> <field name="name"/> @@ -63,109 +62,18 @@ </field> </record> - <!-- Routings --> - <record id="mrp_routing_form_view" model="ir.ui.view"> - <field name="name">mrp.routing.form</field> - <field name="model">mrp.routing</field> - <field name="arch" type="xml"> - <form string="Routing"> - <sheet> - <div class="oe_button_box" name="button_box"> - <button name="%(action_mrp_routing_time)d" type="action" class="oe_stat_button" icon="fa-clock-o"> - <div class="o_field_widget o_stat_info"> - <span class="o_stat_text">Time<br/> Analysis</span> - </div> - </button> - </div> - <widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/> - <div class="oe_title"> - <h1> - <field name="code"/> - </h1> - </div> - <group> - <group> - <field name="name"/> - <field name="active" invisible="1"/> - </group> - <group> - <field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/> - </group> - </group> - <notebook> - <page string="Work Center Operations" name="work_center_operations"> - <field name="operation_ids" context="{'default_routing_id': id}"/> - </page> - <page string="Notes" name="notes"> - <field name="note"/> - </page> - </notebook> - </sheet> - </form> - </field> - </record> - - <record id="mrp_routing_tree_view" model="ir.ui.view"> - <field name="name">mrp.routing.tree</field> - <field name="model">mrp.routing</field> - <field name="arch" type="xml"> - <tree string="Routing"> - <field name="code"/> - <field name="name"/> - <field name="active" invisible="1"/> - <field name="company_id" groups="base.group_multi_company"/> - </tree> - </field> - </record> - - <record id="mrp_routing_kanban_view" model="ir.ui.view"> - <field name="name">mrp.routing.kanban</field> - <field name="model">mrp.routing</field> - <field name="arch" type="xml"> - <kanban class="o_kanban_mobile"> - <field name="code"/> - <field name="name"/> - <templates> - <t t-name="kanban-box"> - <div t-attf-class="oe_kanban_card oe_kanban_global_click"> - <div class="o_kanban_record_top"> - <div class="o_kanban_record_headings mt4"> - <strong class="o_kanban_record_title"><span><t t-esc="record.name.value"/></span></strong> - </div> - <span class="badge badge-pill"><field name="code"/></span> - </div> - </div> - </t> - </templates> - </kanban> - </field> - </record> - - <record id="mrp_routing_search_view" model="ir.ui.view"> - <field name="name">mrp.routing.search</field> - <field name="model">mrp.routing</field> - <field name="arch" type="xml"> - <search string="Routing"> - <field name="name" string="Routing" filter_domain="['|', ('name', 'ilike', self), ('code', 'ilike', self)]"/> - <filter name="inactive" string="Archived" domain="[('active', '=', False)]"/> - </search> - </field> - </record> - <record id="mrp_routing_action" model="ir.actions.act_window"> - <field name="name">Routings</field> + <field name="name">Operations</field> <field name="type">ir.actions.act_window</field> - <field name="res_model">mrp.routing</field> - <field name="view_mode">tree,kanban,form</field> - <field name="view_id" ref="mrp_routing_tree_view"/> - <field name="search_view_id" ref="mrp_routing_search_view"/> + <field name="res_model">mrp.routing.workcenter</field> + <field name="view_mode">tree,form</field> + <field name="view_id" ref="mrp_routing_workcenter_tree_view"/> <field name="help" type="html"> <p class="o_view_nocontent_smiling_face"> - Create a new routing + Create a new operation </p><p> - Routings define the successive operations that need to be - done to realize a Manufacturing Order. Each operation from - a Routing is done at a specific Work Center and has a specific duration. + Operation define that need to be done to realize a Work Order. + Each operation is done at a specific Work Center and has a specific duration. </p> </field> </record> @@ -174,6 +82,7 @@ action="mrp_routing_action" parent="menu_mrp_configuration" groups="group_mrp_routings" - sequence="50"/> + sequence="100"/> + </data> </odoo> diff --git a/addons/mrp/views/mrp_templates.xml b/addons/mrp/views/mrp_templates.xml index b5796193dcd07ceec61ffdf2e0126ba435a11a85..713b71cfbc63af9fc8ce0bbdc105a11c81fdda2a 100644 --- a/addons/mrp/views/mrp_templates.xml +++ b/addons/mrp/views/mrp_templates.xml @@ -12,6 +12,7 @@ <script type="text/javascript" src="/mrp/static/src/js/mrp_documents_kanban_record.js"></script> <script type="text/javascript" src="/mrp/static/src/js/mrp_documents_kanban_renderer.js"></script> <script type="text/javascript" src="/mrp/static/src/js/mrp_document_kanban_view.js"></script> + <script type="text/javascript" src="/mrp/static/src/js/mrp_should_consume.js"></script> </xpath> </template> diff --git a/addons/mrp/views/mrp_workcenter_views.xml b/addons/mrp/views/mrp_workcenter_views.xml index 989805a183408c80bafbbb5fadfdb25da880dcd7..ec8a4b7ca438787a34af1a2c9d9cb88d2a5133b1 100644 --- a/addons/mrp/views/mrp_workcenter_views.xml +++ b/addons/mrp/views/mrp_workcenter_views.xml @@ -6,7 +6,7 @@ <field name="name">Manufacturing Orders</field> <field name="res_model">mrp.production</field> <field name="view_mode">tree,kanban,form,gantt</field> - <field name="domain">[('routing_id', '!=', False),('routing_id.operation_ids.workcenter_id','=', active_id)]</field> + <field name="domain">[('bom_id', '!=', False), ('bom_id.operation_ids.workcenter_id', '=', active_id)]</field> </record> <record id="action_work_orders" model="ir.actions.act_window"> @@ -431,14 +431,6 @@ parent="menu_mrp_configuration" sequence="90"/> - <menuitem id="menu_mrp_dashboard" - name="Overview" - action="mrp_workcenter_kanban_action" - groups="group_mrp_routings" - parent="menu_mrp_root" - sequence="5"/> - - <record id="oee_loss_form_view" model="ir.ui.view"> <field name="name">mrp.workcenter.productivity.loss.form</field> <field name="model">mrp.workcenter.productivity.loss</field> diff --git a/addons/mrp/views/mrp_workorder_views.xml b/addons/mrp/views/mrp_workorder_views.xml index b70d7625e210d530cecbb45a4d82774754a29eff..fae484ac451988f69f20164779c86d1059fac054 100644 --- a/addons/mrp/views/mrp_workorder_views.xml +++ b/addons/mrp/views/mrp_workorder_views.xml @@ -32,7 +32,7 @@ <field name="view_mode">graph,pivot,tree,form,gantt,calendar</field> <field name="context">{'search_default_done': True}</field> <field name="search_view_id" ref="view_mrp_production_work_order_search"/> - <field name="domain">[('production_id.routing_id', '=', active_id), ('state', '=', 'done')]</field> + <field name="domain">[('operation_id.bom_id', '=', active_id), ('state', '=', 'done')]</field> <field name="help" type="html"> <p class="o_view_nocontent_empty_folder"> No data to display @@ -60,27 +60,65 @@ </field> </record> - <record model="ir.ui.view" id="mrp_production_workorder_tree_view_inherit"> - <field name="name">mrp.production.work.order.tree</field> + <record model="ir.ui.view" id="mrp_production_workorder_tree_editable_view"> + <field name="name">mrp.production.work.order.tree.editable</field> <field name="model">mrp.workorder</field> + <field name="priority" eval="100"/> <field name="arch" type="xml"> - <tree string="Work Orders" delete="0" create="0"> - <field name="name"/> - <field name="date_planned_start" decoration-danger="date_planned_start<current_date and state in ('ready')"/> - <field name="workcenter_id" widget="selection"/> - <field name="production_id"/> - <field name="product_id"/> - <field name="qty_production"/> - <field name="product_uom_id"/> - <field name="state"/> - <field name="activity_exception_decoration" widget="activity_exception"/> - <field name="company_id" groups="base.group_multi_company"/> + <tree editable="bottom"> + <field name="consumption" invisible="1"/> + <field name="company_id" invisible="1"/> + <field name="is_produced" invisible="1"/> + <field name="is_user_working" invisible="1"/> + <field name="name" invisible="1"/> + <field name="product_uom_id" invisible="1" readonly="0"/> + <field name="production_state" invisible="1"/> + <field name="production_bom_id" invisible="1"/> + <field name="qty_producing" invisible="1"/> + <field name="time_ids" invisible="1"/> + <field name="working_state" invisible="1"/> + <field name="operation_id" invisible="1" domain="['|', ('bom_id', '=', production_bom_id), ('bom_id', '=', False)]" context="{'default_workcenter_id': workcenter_id, 'default_company_id': company_id}"/> + <field name="name" string="Operation"/> + <field name="workcenter_id"/> + <field name="date_planned_start" optional="show"/> + <field name="date_planned_finished" optional="hide"/> + <field name="date_start" optional="hide" readonly="1"/> + <field name="date_finished" optional="hide" readonly="1"/> + <field name="duration_expected" widget="float_time"/> + <field name="duration" widget="mrp_time_counter" + attrs="{'invisible': ['|', ('production_state','=', 'draft'), ('time_ids', '=', [])]}"/> + <button name="action_open_wizard" type="object" icon="fa-external-link" class="oe_edit_only" + context="{'default_workcenter_id': workcenter_id}"/> + <field name="state" widget="badge" decoration-success="state == 'done'" decoration-info="state not in ('done', 'cancel')"/> + <button name="button_start" type="object" string="Start" class="btn-success" + attrs="{'invisible': ['|', '|', '|', ('production_state','in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('state', '=', 'done'), ('is_user_working', '!=', False)]}"/> + <button name="button_pending" type="object" string="Pause" class="btn-warning" + attrs="{'invisible': ['|', '|', ('production_state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('is_user_working', '=', False)]}"/> + <button name="button_finish" type="object" string="Done" class="btn-success" + attrs="{'invisible': ['|', '|', ('production_state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('is_user_working', '=', False)]}"/> + <button name="%(mrp.act_mrp_block_workcenter_wo)d" type="action" string="Block" context="{'default_workcenter_id': workcenter_id}" class="btn-danger" + attrs="{'invisible': ['|', ('production_state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked')]}"/> + <button name="button_unblock" type="object" string="Unblock" context="{'default_workcenter_id': workcenter_id}" class="btn-danger" + attrs="{'invisible': ['|', ('production_state', 'in', ('draft', 'done')), ('working_state', '!=', 'blocked')]}"/> <field name="show_json_popover" invisible="1"/> <field name="json_popover" widget="mrp_workorder_popover" string=" " attrs="{'invisible': [('show_json_popover', '=', False)]}"/> </tree> </field> </record> + <record id="mrp_production_workorder_tree_view" model="ir.ui.view"> + <field name="name">mrp.production.work.order.tree</field> + <field name="model">mrp.workorder</field> + <field name="mode">primary</field> + <field name="priority" eval="10"/> + <field name="inherit_id" ref="mrp.mrp_production_workorder_tree_editable_view"/> + <field name="arch" type="xml"> + <field name="workcenter_id" position="after"> + <field name="production_id"/> + </field> + </field> + </record> + <record model="ir.ui.view" id="mrp_production_workorder_form_view_inherit"> <field name="name">mrp.production.work.order.form</field> <field name="model">mrp.workorder</field> @@ -89,18 +127,7 @@ <field name="is_user_working" invisible="1"/> <field name="working_state" invisible="1"/> <field name="production_state" invisible="1"/> - <field name="allowed_lots_domain" invisible="1"/> - <field name="is_finished_lines_editable" invisible="1"/> <header> - <button name="button_finish" type="object" string="Finish Order" attrs="{'invisible': ['|', ('state', '!=', 'progress'), ('is_produced', '=', False)]}" class="btn-info"/> - <button name="button_start" type="object" string="Start Working" attrs="{'invisible': ['|', ('working_state', '=', 'blocked'), ('state', '!=', 'pending')]}"/> - <button name="button_start" type="object" string="Start Working" attrs="{'invisible': ['|', ('working_state', '=', 'blocked'), ('state', '!=', 'ready')]}" class="btn-success"/> - <button name="record_production" type="object" string="Done" class="btn-success" attrs="{'invisible': ['|', '|', '|', ('is_produced', '=', True), ('working_state', '=', 'blocked'), ('state', '!=', 'progress'), ('is_user_working', '=', False)]}"/> - <button name="button_pending" type="object" string="Pause" class="btn-warning" attrs="{'invisible': ['|', '|', ('working_state', '=', 'blocked'), ('state', 'in', ('done', 'pending', 'ready', 'cancel')), ('is_user_working', '=', False)]}"/> - <button name="%(act_mrp_block_workcenter_wo)d" type="action" context="{'default_workcenter_id': workcenter_id}" string="Block" class="btn-danger" attrs="{'invisible': ['|', '|', ('working_state', '=', 'blocked'), ('state', 'in', ('done', 'pending', 'ready', 'cancel')), ('is_user_working', '=', False)]}"/> - <button name="button_unblock" type="object" string="Unblock" class="btn-danger" attrs="{'invisible': [('working_state', '!=', 'blocked')]}"/> - <button name="button_start" type="object" string="Continue Production" attrs="{'invisible': ['|', '|', '|', ('production_state', '=', 'done'), ('working_state', '=', 'blocked'), ('is_user_working', '=', True), ('state', '!=', 'progress')]}"/> - <button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': [('state', 'in', ('done', 'cancel'))]}"/> <field name="state" widget="statusbar" statusbar_visible="pending,ready,progress,done"/> </header> <sheet> @@ -112,50 +139,25 @@ </div> </button> </div> - <group> - <group> - <field name="product_id" string="To Produce"/> - <label for="qty_produced" string="Quantity Produced"/> - <div class="o_row"> - <field name="qty_produced"/> / - <field name="qty_production"/> - <field name="product_uom_id"/> - <field name="production_availability" nolabel="1" widget="bullet_state" options="{'classes': {'assigned': 'success', 'waiting': 'danger'}}" attrs="{'invisible': [('state', '=', 'done')]}"/> - </div> - <field name="is_produced" invisible="1"/> - </group> - <group> - <field name="workcenter_id"/> - <field name="production_id" readonly="1"/> - <field name="company_id" groups="base.group_multi_company"/> - </group> + <field name="workcenter_id" invisible="1"/> + <field name="company_id" invisible="1"/> + <field name="product_tracking" invisible="1"/> + <field name="production_id" invisible="1"/> + <field name="product_id" invisible="1"/> + <field name="finished_lot_id" invisible="1"/> + <field name="qty_producing" invisible="1"/> + <group attrs="{'invisible': [('date_planned_start', '=', False)]}"> + <label for="date_planned_start" string="Planned Date"/> + <div class="oe_inline"> + <field name="date_planned_start" class="mr8 oe_inline" required="True"/> + <strong class="mr8 oe_inline">to</strong> + <field name="date_planned_finished" class="oe_inline" required="True"/> + <field name="show_json_popover" invisible="1"/> + <field name="json_popover" widget="mrp_workorder_popover" class="oe_inline mx-2" attrs="{'invisible': [('show_json_popover', '=', False)]}"/> + </div> </group> <notebook> <page string="Time Tracking" name="time_tracking" groups="mrp.group_mrp_manager"> - <group> - <label for="date_planned_start" string="Planned Date"/> - <div class="oe_inline"> - <field name="date_planned_start" class="mr8 oe_inline" required="True"/> - <strong class="mr8 oe_inline">to</strong> - <field name="date_planned_finished" class="oe_inline" required="True"/> - <field name="show_json_popover" invisible="1"/> - <field name="json_popover" widget="mrp_workorder_popover" class="oe_inline mx-2" attrs="{'invisible': [('show_json_popover', '=', False)]}"/> - </div> - <label for="date_start" string="Effective Date" attrs="{'invisible': [('date_start', '=', False)]}"/> - <div class="o_row" attrs="{'invisible': [('date_start', '=', False)]}"> - <field name="date_start" readonly="1"/> - <div attrs="{'invisible': [('date_finished', '=', False)]}"> - <strong class="mr8">to</strong> - <field name="date_finished" readonly="1"/> - </div> - </div> - <label for="duration_expected"/> - <div> - <field name="duration_expected" widget="float_time" class="oe_inline"/> - minutes - </div> - <field name="duration" widget="mrp_time_counter" help="Time the currently logged user spent on this workorder."/> - </group> <group> <field name="time_ids" nolabel="1" context="{'default_workcenter_id': workcenter_id, 'default_workorder_id': id}"> <tree> @@ -165,7 +167,7 @@ <field name="user_id"/> <field name="workcenter_id" invisible="1"/> <field name="company_id" invisible="1"/> - <field name="loss_id" string="Productivity"/> + <field name="loss_id" string="Productivity" optional="hide"/> </tree> <form> <group> @@ -186,44 +188,6 @@ </field> </group> </page> - <page string="Current Production" name="current_production"> - <group> - <group> - <field name="qty_producing" string="Quantity in Production" attrs="{'readonly': ['|', ('product_tracking', '=', 'serial'), ('state', 'in', ('done', 'cancel'))]}"/> - <field name="company_id" invisible="1"/> - <field name="finished_lot_id" context="{'default_product_id': product_id, 'default_company_id': company_id}" attrs="{'invisible': [('product_tracking', '=', 'none')]}" groups="stock.group_production_lot"/> - <field name="product_tracking" invisible="1"/> - <field name="consumption" invisible="1"/> - </group> - </group> - <h4 attrs="{'invisible': ['&', ('consumption', '=', 'strict'), ('raw_workorder_line_ids', '=', [])]}">Components</h4> - <field name="raw_workorder_line_ids" attrs="{'invisible': ['&', ('consumption', '=', 'strict'), ('raw_workorder_line_ids', '=', [])], 'readonly': [('state', 'in', ('cancel', 'done'))]}" options="{'create': [('consumption', '=', 'flexible')]}"> - <tree editable="bottom" create="1" delete="0"> - <field name="product_id"/> - <field name="product_tracking" invisible="1"/> - <field name="company_id" invisible="1"/> - <field name="lot_id" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_company_id': company_id, 'default_product_id': product_id, 'active_mo_id': parent.production_id}"/> - <field name="qty_to_consume" attrs="{'readonly': [('parent.consumption', '=', 'strict')]}" force_save="1"/> - <field name="qty_reserved" readonly="1"/> - <field name="qty_done" attrs="{'column_invisible': [('parent.state', 'not in', ('progress', 'done'))]}"/> - <field name="product_uom_id" invisible="1"/> - <field name="move_id" invisible="1"/> - </tree> - </field> - <h4>Finished Products</h4> - <field name="finished_workorder_line_ids" attrs="{'readonly': [('is_finished_lines_editable', '=', False)]}"> - <tree editable="bottom" create="1" delete="0"> - <field name="product_id"/> - <field name="product_tracking" invisible="1"/> - <field name="company_id" invisible="1"/> - <field name="lot_id" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_company_id': company_id, 'default_product_id': product_id}"/> - <field name="qty_to_consume" readonly="1" string="To Produce" force_save="1"/> - <field name="qty_done" string="Produced"/> - <field name="product_uom_id" invisible="1"/> - <field name="move_id" invisible="1"/> - </tree> - </field> - </page> <page string="Work Instruction" name="workorder_page_work_instruction" attrs="{'invisible': [('worksheet', '=', False), ('worksheet_google_slide', '=', False), ('operation_note', '=', False)]}"> <field name="worksheet_type" invisible="1"/> <field name="worksheet" widget="pdf_viewer" attrs="{'invisible': [('worksheet_type', '!=', 'pdf')]}"/> @@ -485,6 +449,15 @@ </field> </record> + <record model="ir.actions.act_window" id="mrp_workorder_mrp_production_form"> + <field name="name">Work Orders</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">mrp.workorder</field> + <field name="view_mode">form</field> + <field name="target">new</field> + <field name="view_id" ref="mrp_production_workorder_form_view_inherit"/> + </record> + <record model="ir.actions.act_window" id="mrp_workorder_todo"> <field name="name">Work Orders</field> <field name="type">ir.actions.act_window</field> diff --git a/addons/mrp/views/res_config_settings_views.xml b/addons/mrp/views/res_config_settings_views.xml index 229ff502fc62ece7ff8a187d4392c19a3acf8a19..1ea1782657fed73722555dec256f9e5e8ecb9400 100644 --- a/addons/mrp/views/res_config_settings_views.xml +++ b/addons/mrp/views/res_config_settings_views.xml @@ -20,13 +20,10 @@ <label for="group_mrp_routings" string="Work Orders"/> <a href="https://www.odoo.com/documentation/user/13.0/manufacturing/management/manufacturing_order.html#manage-manufacturing-with-routings-and-work-centers" title="Documentation" class="o_doc_link" target="_blank"></a> <div class="text-muted"> - Process operations at specific work centers based on the routing + Process operations at specific work centers </div> <div class="content-group" attrs="{'invisible': [('group_mrp_routings','=',False)]}"> <div class="mt8"> - <div> - <button name="%(mrp.mrp_routing_action)d" icon="fa-arrow-right" type="action" string="Routings" class="btn-link"/> - </div> <div> <button name="%(mrp.mrp_workcenter_action)d" icon="fa-arrow-right" type="action" string="Work Centers" class="btn-link"/> </div> @@ -58,6 +55,17 @@ </div> </div> </div> + <div class="col-lg-6 col-12 o_setting_box" id="mrp_lock" title="Makes confirmed manufacturing orders locked rather than unlocked by default. This only applies to new manufacturing orders, not previously created ones."> + <div class="o_setting_left_pane"> + <field name="group_locked_by_default"/> + </div> + <div class="o_setting_right_pane"> + <label for="group_locked_by_default"/> + <div class="text-muted"> + Prevent manufacturing users to modify quantities to consume, unless a manager has unlocked the document + </div> + </div> + </div> </div> <div class="row mt16 o_settings_container"> <div class="col-lg-6 col-12 o_setting_box" id="mrp_byproduct" title="Add by-products to bills of materials. This can be used to get several finished products as well. Without this option you only do: A + B = C. With the option: A + B = C + D."> diff --git a/addons/mrp/views/stock_move_views.xml b/addons/mrp/views/stock_move_views.xml index 7f0273418cdb3591b68b3d5da573082a14aeb700..d5383f86ab3a2e04b036e337884d89a2d9681249 100644 --- a/addons/mrp/views/stock_move_views.xml +++ b/addons/mrp/views/stock_move_views.xml @@ -8,142 +8,35 @@ <field name="domain">['|', ('move_id.raw_material_production_id', '=', active_id), ('move_id.production_id', '=', active_id)]</field> </record> - <record id="view_stock_move_lots" model="ir.ui.view"> - <field name="name">stock.move.lots.form</field> + <record id="view_stock_move_operations_raw" model="ir.ui.view"> + <field name="name">stock.move.operations.raw.form</field> <field name="model">stock.move</field> - <field name="priority">1000</field> + <field name="priority">1</field> + <field name="mode">primary</field> + <field name="inherit_id" ref="stock.view_stock_move_operations" /> <field name="arch" type="xml"> - <form string="Lots"> - <field name="state" invisible="1" force_save="1"/> - <group> - <group> - <field name="product_id" attrs="{'readonly': [('id', '!=', False)]}"/> - <label for="product_uom_qty"/> - <div class="o_row"> - <span><field name="product_uom_qty" attrs="{'readonly': ['&', ('parent.state', '!=', 'draft'), '|', ('parent.state', '!=', 'confirmed'), ('parent.consumption', '=', 'strict')]}" nolabel="1"/></span> - <span> - <field name="product_uom" attrs="{'readonly': [('id', '!=', False)]}" force_save="1" nolabel="1" groups="uom.group_uom"/> - </span> - </div> - <label for="quantity_done" attrs="{'invisible': ['|', ('parent.state', '=', 'draft'), ('id', '=', False)]}"/> - <div class="o_row" attrs="{'invisible': ['|', ('parent.state', '=', 'draft'), ('id', '=', False)]}"> - <span><field name="quantity_done" attrs="{'readonly': ['|', '|', ('move_line_ids', '!=', False), ('is_locked', '=', True), '|', ('finished_lots_exist', '=', True), ('has_tracking', '!=', 'none')]}" nolabel="1"/></span> - <span> / </span> - <span><field name="reserved_availability" nolabel="1"/></span> - </div> - <field name="operation_id" attrs="{'invisible': ['|', ('parent.routing_id', '=', False), ('id', '!=', False)]}" groups="mrp.group_mrp_routings"/> - <field name="routing_id" invisible="1"/> - <field name="unit_factor" invisible="1"/> - <field name="additional" invisible="1"/> - <field name="company_id" invisible="1"/> - <field name="is_done" invisible="1"/> - <field name="workorder_id" invisible="1"/> - <field name="bom_line_id" invisible="1"/> - <field name="location_id" invisible="1"/> - <field name="location_dest_id" invisible="1"/> - <field name="picking_type_id" invisible="1"/> - <field name="operation_id" invisible="1"/> - <field name="warehouse_id" invisible="1"/> - <field name="production_id" invisible="1"/> - <field name="date" invisible="1"/> - <field name="date_expected" invisible="1"/> - <field name="raw_material_production_id" invisible="1"/> - <field name="is_locked" invisible="1"/> - <field name="name" invisible="1"/> - <field name="has_tracking" invisible="1"/> - <field name="order_finished_lot_ids" invisible="1"/> - <field name="finished_lots_exist" invisible="1"/> - <field name="product_uom_category_id" invisible="1"/> - </group> - </group> - <field name="move_line_ids" attrs="{'invisible': ['|', ('parent.state', '=', 'draft'), ('id', '=', False)], 'readonly': ['|', ('is_locked', '=', True), ('state', '=', 'cancel')]}" context="{'default_workorder_id': workorder_id, 'default_product_uom_id': product_uom, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_production_id': production_id or raw_material_production_id}"> - <tree editable="bottom" decoration-success="product_uom_qty==qty_done" decoration-danger="(product_uom_qty > 0) and (qty_done>product_uom_qty)"> - <field name="lot_id" attrs="{'column_invisible': [('parent.has_tracking', '=', 'none')]}" context="{'default_product_id': parent.product_id}"/> - <field name="lot_produced_ids" widget="many2many_tags" options="{'no_open': True, 'no_create': True}" domain="[('id', 'in', parent.order_finished_lot_ids)]" invisible="not context.get('final_lots')"/> - <field name="owner_id" groups="stock.group_tracking_owner" options="{'no_open': True, 'no_create': True}" optional="hide"/> - <field name="package_id" groups="stock.group_tracking_lot" options="{'no_open': True, 'no_create': True}" optional="hide"/> - <field name="product_uom_qty" string="Reserved" readonly="1" optional="show"/> - <field name="qty_done"/> - <field name="workorder_id" invisible="1"/> - <field name="product_id" invisible="1"/> - <field name="product_uom_id" invisible="1"/> - <field name="location_id" invisible="1"/> - <field name="location_dest_id" invisible="1"/> - <field name="production_id" invisible="1"/> - <field name="company_id" invisible="1"/> - <field name="product_uom_category_id" invisible="1"/> - </tree> - </field> - </form> - </field> - </record> - - <record id="view_stock_move_raw_tree" model="ir.ui.view"> - <field name="name">stock.move.raw.tree</field> - <field name="model">stock.move</field> - <field name="priority">1000</field> - <field name="arch" type="xml"> - <tree delete="0" default_order="is_done,sequence" decoration-muted="is_done" decoration-warning="quantity_done - product_uom_qty > 0.0001" decoration-success="not is_done and quantity_done - product_uom_qty < 0.0001" decoration-danger="not is_done and reserved_availability < product_uom_qty and product_uom_qty - reserved_availability > 0.0001"> - <field name="product_id" required="1"/> - <field name="company_id" invisible="1"/> - <field name="product_uom_category_id" invisible="1"/> - <field name="name" invisible="1"/> - <field name="unit_factor" invisible="1"/> - <field name="product_uom" groups="uom.group_uom"/> - <field name="date" invisible="1"/> - <field name="date_expected" invisible="1"/> - <field name="picking_type_id" invisible="1"/> - <field name="has_tracking" invisible="1"/> - <field name="operation_id" invisible="1"/> - <field name="needs_lots" readonly="1" groups="stock.group_production_lot"/> - <field name="is_done" invisible="1"/> - <field name="bom_line_id" invisible="1"/> - <field name="sequence" invisible="1"/> - <field name="location_id" invisible="1"/> - <field name="warehouse_id" invisible="1"/> - <field name="location_dest_id" domain="[('id', 'child_of', parent.location_dest_id)]" invisible="1"/> - <field name="state" invisible="1" force_save="1"/> - <field name="product_uom_qty" string="To Consume"/> - <field name="reserved_availability" attrs="{'invisible': [('is_done', '=', True)], 'column_invisible': [('parent.state', 'in', ('draft', 'done'))]}" string="Reserved"/> - <field name="quantity_done" string="Consumed" attrs="{'column_invisible': [('parent.state', '=', 'draft')]}" readonly="1"/> - </tree> + <xpath expr="//label[@for='product_uom_qty']" position="attributes"> + <attribute name="string">Total To Consume</attribute> + </xpath> + <xpath expr="//label[@for='quantity_done']" position="attributes"> + <attribute name="string">Consumed</attribute> + </xpath> </field> </record> - <record id="view_move_kanban_inherit_mrp" model="ir.ui.view"> - <field name="name">stock.move.kanban.inherit.mrp</field> + <record id="view_stock_move_operations_finished" model="ir.ui.view"> + <field name="name">stock.move.operations.finished.form</field> <field name="model">stock.move</field> - <field name="inherit_id" ref="stock.view_move_kandan"/> + <field name="priority">1</field> + <field name="mode">primary</field> + <field name="inherit_id" ref="stock.view_stock_move_operations" /> <field name="arch" type="xml"> - <xpath expr="//templates" position="before"> - <field name="bom_line_id"/> + <xpath expr="//label[@for='product_uom_qty']" position="attributes"> + <attribute name="string">To Produce</attribute> + </xpath> + <xpath expr="//label[@for='quantity_done']" position="attributes"> + <attribute name="string">Produced</attribute> </xpath> - </field> - </record> - - <record id="view_finisehd_move_line" model="ir.ui.view"> - <field name="name">mrp.finished.move.line.form</field> - <field name="priority">1000</field> - <field name="model">stock.move.line</field> - <field name="arch" type="xml"> - <form string="Finished Product"> - <group> - <group> - <field name="company_id" invisible="1"/> - <field name="product_id" readonly="1"/> - <field name="product_uom_category_id" readonly="1"/> - <label for="qty_done" string ="Quantity"/> - <div class="o_row"> - <span><field name="qty_done" readonly="1" nolabel="1"/></span> - <span>/</span> - <span><field name="product_uom_qty" readonly="1" nolabel="1"/></span> - <span><field name="product_uom_id" attrs="{'readonly': [('id', '!=', False)]}" nolabel="1"/></span> - </div> - <field name="lot_id" string="Lot/Serial number" groups="stock.group_production_lot" readonly="1" attrs="{'invisible': [('lots_visible', '=', False)]}"/> - <field name="lots_visible" invisible="1"/> - </group> - </group> - </form> </field> </record> diff --git a/addons/mrp/wizard/__init__.py b/addons/mrp/wizard/__init__.py index 83ad5a17ef4f4bda2178515c3d39a5c36a51db9e..466142a28a556284accdaf5c700466b7a5e77005 100644 --- a/addons/mrp/wizard/__init__.py +++ b/addons/mrp/wizard/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import mrp_product_produce from . import change_production_qty from . import stock_warn_insufficient_qty +from . import mrp_production_backorder +from . import mrp_consumption_warning diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py index 468681982bbbc6c4cb908b370928a1f2de8cc115..29657577a4ff589ca27c7054451dad42bc2800cc 100644 --- a/addons/mrp/wizard/change_production_qty.py +++ b/addons/mrp/wizard/change_production_qty.py @@ -50,58 +50,40 @@ class ChangeProductionQty(models.TransientModel): format_qty = '%.{precision}f'.format(precision=precision) raise UserError(_("You have already processed %s. Please input a quantity higher than %s ") % (format_qty % produced, format_qty % produced)) old_production_qty = production.product_qty - production.write({'product_qty': wizard.product_qty}) + new_production_qty = wizard.product_qty done_moves = production.move_finished_ids.filtered(lambda x: x.state == 'done' and x.product_id == production.product_id) qty_produced = production.product_id.uom_id._compute_quantity(sum(done_moves.mapped('product_qty')), production.product_uom_id) - factor = production.product_uom_id._compute_quantity(production.product_qty - qty_produced, production.bom_id.product_uom_id) / production.bom_id.product_qty - boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id) + + factor = (new_production_qty - qty_produced) / (old_production_qty - qty_produced) + update_info = production._update_raw_moves(factor) documents = {} - for line, line_data in lines: - if line.child_bom_id and line.child_bom_id.type == 'phantom' or\ - line.product_id.type not in ['product', 'consu']: - continue - move = production.move_raw_ids.filtered(lambda x: x.bom_line_id.id == line.id and x.state not in ('done', 'cancel')) - if move: - move = move[0] - old_qty = move.product_uom_qty - else: - old_qty = 0 + for move, old_qty, new_qty in update_info: iterate_key = production._get_document_iterate_key(move) if iterate_key: - document = self.env['stock.picking']._log_activity_get_documents({move: (line_data['qty'], old_qty)}, iterate_key, 'UP') + document = self.env['stock.picking']._log_activity_get_documents({move: (new_qty, old_qty)}, iterate_key, 'UP') for key, value in document.items(): if documents.get(key): documents[key] += [value] else: documents[key] = [value] - - production._update_raw_move(line, line_data) - production._log_manufacture_exception(documents) - operation_bom_qty = {} - for bom, bom_data in boms: - for operation in bom.routing_id.operation_ids: - operation_bom_qty[operation.id] = bom_data['qty'] - finished_moves_modification = self._update_finished_moves(production, production.product_qty - qty_produced, old_production_qty) - production._log_downside_manufactured_quantity(finished_moves_modification) - moves = production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) - moves._action_assign() + finished_moves_modification = self._update_finished_moves(production, new_production_qty - qty_produced, old_production_qty - qty_produced) + if finished_moves_modification: + production._log_downside_manufactured_quantity(finished_moves_modification) + production.write({'product_qty': new_production_qty}) + for wo in production.workorder_ids: operation = wo.operation_id - if operation_bom_qty.get(operation.id): - cycle_number = float_round(operation_bom_qty[operation.id] / operation.workcenter_id.capacity, precision_digits=0, rounding_method='UP') - wo.duration_expected = (operation.workcenter_id.time_start + - operation.workcenter_id.time_stop + - cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency) + wo._onchange_expected_duration() quantity = wo.qty_production - wo.qty_produced if production.product_id.tracking == 'serial': quantity = 1.0 if not float_is_zero(quantity, precision_digits=precision) else 0.0 else: quantity = quantity if (quantity > 0) else 0 if float_is_zero(quantity, precision_digits=precision): - wo.finished_lot_id = False - wo._workorder_line_ids().unlink() - wo.qty_producing = quantity + wo.check_ids.unlink() + else: + wo.qty_producing = quantity if wo.qty_produced < wo.qty_production and wo.state == 'done': wo.state = 'progress' if wo.qty_produced == wo.qty_production and wo.state == 'progress': @@ -117,11 +99,4 @@ class ChangeProductionQty(models.TransientModel): moves_finished = production.move_finished_ids.filtered(lambda move: move.operation_id == operation) #TODO: code does nothing, unless maybe by_products? moves_raw.mapped('move_line_ids').write({'workorder_id': wo.id}) (moves_finished + moves_raw).write({'workorder_id': wo.id}) - if wo.state not in ('done', 'cancel'): - line_values = wo._update_workorder_lines() - wo._workorder_line_ids().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) return {} diff --git a/addons/mrp/wizard/mrp_consumption_warning.py b/addons/mrp/wizard/mrp_consumption_warning.py new file mode 100644 index 0000000000000000000000000000000000000000..db0404a9eabf22e312fe2f78f1adda0b4a93b045 --- /dev/null +++ b/addons/mrp/wizard/mrp_consumption_warning.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, api + + +class MrpConsumptionWarning(models.TransientModel): + _name = 'mrp.consumption.warning' + _description = "Wizard in case of consumption in warning/strict and more component has been used for a MO (related to the bom)" + + mrp_production_ids = fields.Many2many('mrp.production') + mrp_production_count = fields.Integer(compute="_compute_mrp_production_count") + + consumption = fields.Selection([ + ('flexible', 'Allowed'), + ('warning', 'Allowed with warning'), + ('strict', 'Blocked')], compute="_compute_consumption") + mrp_consumption_warning_line_ids = fields.One2many('mrp.consumption.warning.line', 'mrp_consumption_warning_id') + + @api.depends("mrp_production_ids") + def _compute_mrp_production_count(self): + for wizard in self: + wizard.mrp_production_count = len(wizard.mrp_production_ids) + + @api.depends("mrp_consumption_warning_line_ids.consumption") + def _compute_consumption(self): + for wizard in self: + consumption_map = set(wizard.mrp_consumption_warning_line_ids.mapped("consumption")) + wizard.consumption = "strict" in consumption_map and "strict" or "warning" in consumption_map and "warning" or "flexible" + + def action_confirm(self): + return self.mrp_production_ids.with_context(skip_consumption=True).button_mark_done() + + def action_cancel(self): + if self.env.context.get('from_workorder') and len(self.mrp_production_ids) == 1: + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mrp.production', + 'views': [[self.env.ref('mrp.mrp_production_form_view').id, 'form']], + 'res_id': self.mrp_production_ids.id, + 'target': 'main', + } + +class MrpConsumptionWarningLine(models.TransientModel): + _name = 'mrp.consumption.warning.line' + _description = "Line of issue consumption" + + mrp_consumption_warning_id = fields.Many2one('mrp.consumption.warning', "Parent Wizard", readonly=True, required=True, ondelete="cascade") + mrp_production_id = fields.Many2one('mrp.production', "Manufacturing Order", readonly=True, required=True, ondelete="cascade") + consumption = fields.Selection(related="mrp_production_id.consumption") + + product_id = fields.Many2one('product.product', "Product", readonly=True, required=True) + product_uom_id = fields.Many2one('uom.uom', "Unit of Measure", related="product_id.uom_id", readonly=True) + product_consumed_qty_uom = fields.Float("Consumed", readonly=True) + product_expected_qty_uom = fields.Float("To Consume", readonly=True) diff --git a/addons/mrp/wizard/mrp_consumption_warning_views.xml b/addons/mrp/wizard/mrp_consumption_warning_views.xml new file mode 100644 index 0000000000000000000000000000000000000000..a09d66db320f0aeab42ce2622597620352ae987c --- /dev/null +++ b/addons/mrp/wizard/mrp_consumption_warning_views.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data> + + <!-- MO Consumption Warning --> + <record id="view_mrp_consumption_warning_form" model="ir.ui.view"> + <field name="name">Consumption Warning</field> + <field name="model">mrp.consumption.warning</field> + <field name="arch" type="xml"> + <form string="Consumption Warning"> + <field name="mrp_production_ids" invisible="1"/> + <field name="consumption" invisible="1"/> + <field name="mrp_production_count" invisible="1"/> + <div class="m-2"> + You consumed a different quantity than expected for the following products. + <b attrs="{'invisible': [('consumption', '=', 'strict')]}"> + Please confirm it has been done on purpose. + </b> + <b attrs="{'invisible': [('consumption', '!=', 'strict')]}"> + Please review your component consumption or ask a manager to validate + <span attrs="{'invisible':[('mrp_production_count', '!=', 1)]}">this manufacturing order</span> + <span attrs="{'invisible':[('mrp_production_count', '=', 1)]}">these manufacturing orders</span>. + </b> + </div> + <field name="mrp_consumption_warning_line_ids" nolabel="1"> + <tree create="0" delete="0" editable="top"> + <field name="mrp_production_id" attrs="{'column_invisible':[('parent.mrp_production_count', '=', 1)]}" force_save="1"/> + <field name="consumption" invisible="1" force_save="1"/> + <field name="product_id" force_save="1"/> + <field name="product_uom_id" groups="uom.group_uom" force_save="1"/> + <field name="product_expected_qty_uom" force_save="1"/> + <field name="product_consumed_qty_uom" force_save="1"/> + </tree> + </field> + <footer> + <button name="action_confirm" string="Force" + groups="mrp.group_mrp_manager" attrs="{'invisible': [('consumption', '!=', 'strict')]}" + colspan="1" type="object" class="btn-primary"/> + <button name="action_confirm" string="Confirm" attrs="{'invisible': [('consumption', '=', 'strict')]}" + colspan="1" type="object" class="btn-primary"/> + <button name="action_cancel" string="Review Consumption" + colspan="1" type="object" class="btn-primary"/> + </footer> + </form> + </field> + </record> + + <record id="action_mrp_consumption_warning" model="ir.actions.act_window"> + <field name="name">Consumption Warning</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">mrp.consumption.warning</field> + <field name="view_mode">form</field> + <field name="target">new</field> + </record> + + </data> +</odoo> diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py deleted file mode 100644 index 7e2b47ac8ab595d09c2b5be1f51a31845f40c5d0..0000000000000000000000000000000000000000 --- a/addons/mrp/wizard/mrp_product_produce.py +++ /dev/null @@ -1,154 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from datetime import datetime - -from odoo import api, fields, models, _ -from odoo.exceptions import UserError -from odoo.tools import float_compare - - -class MrpProductProduce(models.TransientModel): - _name = "mrp.product.produce" - _description = "Record Production" - _inherit = ["mrp.abstract.workorder"] - - @api.model - def default_get(self, fields): - res = super(MrpProductProduce, self).default_get(fields) - production = self.env['mrp.production'] - production_id = self.env.context.get('default_production_id') or self.env.context.get('active_id') - if production_id: - production = self.env['mrp.production'].browse(production_id) - if production.exists(): - serial_finished = (production.product_id.tracking == 'serial') - todo_uom = production.product_uom_id.id - todo_quantity = self._get_todo(production) - if serial_finished: - todo_quantity = 1.0 - if production.product_uom_id.uom_type != 'reference': - todo_uom = self.env['uom.uom'].search([('category_id', '=', production.product_uom_id.category_id.id), ('uom_type', '=', 'reference')]).id - if 'production_id' in fields: - res['production_id'] = production.id - if 'product_id' in fields: - res['product_id'] = production.product_id.id - if 'product_uom_id' in fields: - res['product_uom_id'] = todo_uom - if 'serial' in fields: - res['serial'] = bool(serial_finished) - if 'qty_producing' in fields: - res['qty_producing'] = todo_quantity - if 'consumption' in fields: - res['consumption'] = production.bom_id.consumption if production.bom_id else 'flexible' - return res - - serial = fields.Boolean('Requires Serial') - product_tracking = fields.Selection(related="product_id.tracking") - is_pending_production = fields.Boolean(compute='_compute_pending_production') - - move_raw_ids = fields.One2many(related='production_id.move_raw_ids', string="PO Components") - move_finished_ids = fields.One2many(related='production_id.move_finished_ids') - - raw_workorder_line_ids = fields.One2many('mrp.product.produce.line', - 'raw_product_produce_id', string='Components') - finished_workorder_line_ids = fields.One2many('mrp.product.produce.line', - 'finished_product_produce_id', string='By-products') - production_id = fields.Many2one('mrp.production', 'Manufacturing Order', - required=True, ondelete='cascade') - - @api.depends('qty_producing') - def _compute_pending_production(self): - """ Compute if it exits remaining quantity once the quantity on the - current wizard will be processed. The purpose is to display or not - button 'continue'. - """ - for product_produce in self: - remaining_qty = product_produce._get_todo(product_produce.production_id) - product_produce.is_pending_production = remaining_qty - product_produce.qty_producing > 0.0 - - def continue_production(self): - """ Save current wizard and directly opens a new. """ - self.ensure_one() - self._record_production() - action = self.production_id.open_produce_product() - action['context'] = {'default_production_id': self.production_id.id} - return action - - def action_generate_serial(self): - self.ensure_one() - product_produce_wiz = self.env.ref('mrp.view_mrp_product_produce_wizard', False) - self.finished_lot_id = self.env['stock.production.lot'].create({ - 'product_id': self.product_id.id, - 'company_id': self.production_id.company_id.id - }) - return { - 'name': _('Produce'), - 'type': 'ir.actions.act_window', - 'view_mode': 'form', - 'res_model': 'mrp.product.produce', - 'res_id': self.id, - 'view_id': product_produce_wiz.id, - 'target': 'new', - } - - def do_produce(self): - """ Save the current wizard and go back to the MO. """ - self.ensure_one() - self._record_production() - self._check_company() - return {'type': 'ir.actions.act_window_close'} - - def _get_todo(self, production): - """ This method will return remaining todo quantity of production. """ - main_product_moves = production.move_finished_ids.filtered(lambda x: x.product_id.id == production.product_id.id) - todo_quantity = production.product_qty - sum(main_product_moves.mapped('quantity_done')) - todo_quantity = todo_quantity if (todo_quantity > 0) else 0 - return todo_quantity - - def _record_production(self): - # Check all the product_produce line have a move id (the user can add product - # to consume directly in the wizard) - for line in self._workorder_line_ids(): - line._check_line_sn_uniqueness() - # because of an ORM limitation (fields on transient models are not - # recomputed by updates in non-transient models), the related fields on - # this model are not recomputed by the creations above - self.invalidate_cache(['move_raw_ids', 'move_finished_ids']) - - # Save product produce lines data into stock moves/move lines - quantity = self.qty_producing - if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) <= 0: - raise UserError(_("The production order for '%s' has no quantity specified.") % self.product_id.display_name) - - self._check_sn_uniqueness() - self._update_finished_move() - self._update_moves() - if self.production_id.state == 'confirmed': - self.production_id.write({ - 'date_start': datetime.now(), - }) - - -class MrpProductProduceLine(models.TransientModel): - _name = 'mrp.product.produce.line' - _inherit = ["mrp.abstract.workorder.line"] - _description = "Record production line" - - raw_product_produce_id = fields.Many2one('mrp.product.produce', 'Component in Produce wizard') - finished_product_produce_id = fields.Many2one('mrp.product.produce', 'Finished Product in Produce wizard') - - @api.model - def _get_raw_workorder_inverse_name(self): - return 'raw_product_produce_id' - - @api.model - def _get_finished_workoder_inverse_name(self): - return 'finished_product_produce_id' - - def _get_final_lots(self): - product_produce_id = self.raw_product_produce_id or self.finished_product_produce_id - return product_produce_id.finished_lot_id | product_produce_id.finished_workorder_line_ids.mapped('lot_id') - - def _get_production(self): - product_produce_id = self.raw_product_produce_id or self.finished_product_produce_id - return product_produce_id.production_id diff --git a/addons/mrp/wizard/mrp_product_produce_views.xml b/addons/mrp/wizard/mrp_product_produce_views.xml deleted file mode 100644 index a83950c123edcf8dcffcfef86b744bea27ee130c..0000000000000000000000000000000000000000 --- a/addons/mrp/wizard/mrp_product_produce_views.xml +++ /dev/null @@ -1,134 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<odoo> - - <record id="view_mrp_product_produce_wizard" model="ir.ui.view"> - <field name="name">MRP Product Produce</field> - <field name="model">mrp.product.produce</field> - <field name="arch" type="xml"> - <form string="Produce"> - <group> - <group> - <field name="serial" invisible="1"/> - <field name="company_id" invisible="1"/> - <field name="production_id" invisible="1"/> - <field name="product_id" readonly="1"/> - <field name="is_pending_production" invisible="1"/> - <label for="qty_producing" string="Quantity"/> - <div class="o_row"> - <field name="qty_producing" attrs="{'readonly': [('serial', '=', True)]}"/> - <field name="product_uom_id" readonly="1" groups="uom.group_uom"/> - </div> - <field name="product_tracking" invisible="1"/> - <label for="finished_lot_id" attrs="{'invisible': [('product_tracking', '=', 'none')]}"/> - <div class="o_row"> - <field name="finished_lot_id" attrs="{'invisible': [('product_tracking', '=', 'none')], 'required': [('product_tracking', '!=', 'none'), ('finished_lot_id', '!=', False)]}" context="{'default_product_id': product_id, 'default_company_id': company_id}"/> - <button name="action_generate_serial" type="object" class="btn btn-primary fa fa-plus-square-o" aria-label="Creates a new serial/lot number" title="Creates a new serial/lot number" role="img" attrs="{'invisible': ['|', ('product_tracking', '=', 'none'), ('finished_lot_id', '!=', False)]}"/> - </div> - </group> - </group> - <h4 attrs="{'invisible': [('raw_workorder_line_ids', '=', [])]}">Components</h4> - <group> - <field name="raw_workorder_line_ids" attrs="{'invisible': [('raw_workorder_line_ids', '=', [])]}" nolabel="1" context="{'w_production': True, 'active_id': production_id, 'default_finished_lot_id': finished_lot_id}"> - <tree editable="bottom" delete="0" decoration-danger="(qty_to_consume < qty_done)"> - <field name="company_id" invisible="1"/> - <field name="product_id" attrs="{'readonly': [('move_id', '!=', False)]}" required="1" domain="[('id', '!=', parent.product_id), ('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]" force_save="1"/> - <field name="product_tracking" invisible="1"/> - <field name="lot_id" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_product_id': product_id, 'active_mo_id': parent.production_id, 'default_company_id': parent.company_id}" groups="stock.group_production_lot"/> - <field name="qty_to_consume" readonly="1" force_save="1"/> - <field name="qty_reserved" readonly="1" force_save="1" optional="show"/> - <field name="qty_done"/> - <field name="product_uom_id" readonly="1" force_save="1" groups="uom.group_uom"/> - <field name="move_id" invisible="1"/> - </tree> - </field> - </group> - <h4 attrs="{'invisible': [('finished_workorder_line_ids', '=', [])]}">By-products</h4> - <group> - <field name="finished_workorder_line_ids" attrs="{'invisible': [('finished_workorder_line_ids', '=', [])]}" nolabel="1" context="{'w_production': True, 'active_id': production_id, 'default_finished_lot_id': finished_lot_id}"> - <tree editable="bottom" delete="0" decoration-danger="(qty_to_consume < qty_done)"> - <field name="company_id" invisible="1"/> - <field name="product_id" attrs="{'readonly': [('move_id', '!=', False)]}" required="1" domain="[('id', '!=', parent.product_id), ('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', parent.company_id)]" force_save="1"/> - <field name="product_tracking" invisible="1"/> - <field name="lot_id" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_product_id': product_id, 'default_company_id': company_id}" groups="stock.group_production_lot"/> - <field name="qty_to_consume" string="To Produce" readonly="1" force_save="1"/> - <field name="qty_done" string="Produced"/> - <field name="product_uom_id" readonly="1" force_save="1" groups="uom.group_uom"/> - <field name="move_id" invisible="1"/> - </tree> - </field> - </group> - <footer> - <button name="continue_production" attrs="{'invisible':[('is_pending_production', '=', False)]}" type="object" string="Continue" class="btn-primary"/> - <button name="do_produce" type="object" string="Save" class="btn-default btn-secondary" attrs="{'invisible':[('is_pending_production', '=', False)]}"/> - <button name="do_produce" type="object" string="Save" class="btn-primary" attrs="{'invisible':[('is_pending_production', '=', True)]}"/> - <button string="Discard" class="btn-default btn-secondary" special="cancel"/> - </footer> - </form> - </field> - </record> - - <record id="mrp_product_produce_line_form" model="ir.ui.view"> - <field name="name">MRP Product Produce Line</field> - <field name="model">mrp.product.produce.line</field> - <field name="priority">100</field> - <field name="arch" type="xml"> - <form> - <group> - <group> - <field name="company_id" invisible="1"/> - <field name="product_id"/> - <field name="product_tracking" invisible="1"/> - <field name="lot_id" attrs="{'readonly': [('product_tracking', '=', 'none')]}" context="{'default_product_id': product_id}" groups="stock.group_production_lot"/> - <field name="qty_to_consume" readonly="1" force_save="1"/> - <field name="qty_reserved" readonly="1" force_save="1" optional="show"/> - <field name="qty_done"/> - <field name="product_uom_id" readonly="1" force_save="1" groups="uom.group_uom"/> - <field name="move_id" invisible="1"/> - </group> - </group> - </form> - </field> - </record> - - <record id="mrp_product_produce_line_kanban" model="ir.ui.view"> - <field name="name">MRP Product Produce Line</field> - <field name="model">mrp.product.produce.line</field> - <field name="priority">100</field> - <field name="arch" type="xml"> - <kanban class="o_kanban_mobile"> - <field name="product_id"/> - <field name="lot_id"/> - <field name="qty_to_consume"/> - <field name="qty_reserved"/> - <field name="qty_done"/> - <field name="product_uom_id"/> - <templates> - <t t-name="kanban-box"> - <div t-attf-class="oe_kanban_global_click"> - <div class="o_kanban_record_top"> - <div class="o_kanban_record_headings"> - <strong class="o_kanban_record_title"><span><field name="product_id"/></span></strong> - </div> - </div> - <div class="o_kanban_record_body"> - <span>Lot <field name="lot_id"/></span><br/> - <span>Quantity To Consume <field name="qty_to_consume"/></span><br/> - <span>Quantity Reserved <field name="qty_reserved"/></span><br/> - <span>Quantity Done <field name="qty_done"/></span><br/> - </div> - </div> - </t> - </templates> - </kanban> - </field> - </record> - - <record id="act_mrp_product_produce" model="ir.actions.act_window"> - <field name="name">Produce</field> - <field name="type">ir.actions.act_window</field> - <field name="res_model">mrp.product.produce</field> - <field name="view_mode">form</field> - <field name="context">{}</field> - <field name="target">new</field> - </record> -</odoo> diff --git a/addons/mrp/wizard/mrp_production_backorder.py b/addons/mrp/wizard/mrp_production_backorder.py new file mode 100644 index 0000000000000000000000000000000000000000..681b3fffc903ea716cef422f4f20d8fe56c7cf6a --- /dev/null +++ b/addons/mrp/wizard/mrp_production_backorder.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class MrpProductionBackorderLine(models.TransientModel): + _name = 'mrp.production.backorder.line' + _description = "Backorder Confirmation Line" + + mrp_production_backorder_id = fields.Many2one('mrp.production.backorder', 'MO Backorder', required=True, ondelete="cascade") + mrp_production_id = fields.Many2one('mrp.production', 'Manufacturing Order', required=True, ondelete="cascade", readonly=True) + to_backorder = fields.Boolean('To Backorder') + + +class MrpProductionBackorder(models.TransientModel): + _name = 'mrp.production.backorder' + _description = "Wizard to mark as done or create back order" + + mrp_production_ids = fields.Many2many('mrp.production') + + mrp_production_backorder_line_ids = fields.One2many( + 'mrp.production.backorder.line', + 'mrp_production_backorder_id', + string="Backorder Confirmation Lines") + show_backorder_lines = fields.Boolean("Show backorder lines", compute="_compute_show_backorder_lines") + + @api.depends('mrp_production_backorder_line_ids') + def _compute_show_backorder_lines(self): + for wizard in self: + wizard.show_backorder_lines = len(wizard.mrp_production_backorder_line_ids) > 1 + + def action_close_mo(self): + return self.mrp_production_ids.with_context(skip_backorder=True).button_mark_done() + + def action_backorder(self): + mo_ids_to_backorder = self.mrp_production_backorder_line_ids.filtered(lambda l: l.to_backorder).mrp_production_id.ids + return self.mrp_production_ids.with_context(skip_backorder=True, mo_ids_to_backorder=mo_ids_to_backorder).button_mark_done() diff --git a/addons/mrp/wizard/mrp_production_backorder.xml b/addons/mrp/wizard/mrp_production_backorder.xml new file mode 100644 index 0000000000000000000000000000000000000000..269d3f511a156457fab70550ec0e88048ad0b398 --- /dev/null +++ b/addons/mrp/wizard/mrp_production_backorder.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <data> + + <!-- MO Backorder --> + <record id="view_mrp_production_backorder_form" model="ir.ui.view"> + <field name="name">Create Backorder</field> + <field name="model">mrp.production.backorder</field> + <field name="arch" type="xml"> + <form string="Create a Backorder"> + <group> + <p> + You produced less than the initial demand. + </p><p class="text-muted"> + Create a backorder if you expect to process the remaining products later. Do not create a backorder if you will not process the remaining products. + </p> + </group> + <field name="show_backorder_lines" invisible="1"/> + <field name="mrp_production_backorder_line_ids" nolabel="1" attrs="{'invisible': [('show_backorder_lines', '=', False)]}"> + <tree create="0" delete="0" editable="top"> + <field name="mrp_production_id" force_save="1"/> + <field name="to_backorder" widget="boolean_toggle"/> + </tree> + </field> + <footer> + <button name="action_backorder" string="Create backorder" + colspan="1" type="object" class="btn-primary" attrs="{'invisible': [('show_backorder_lines', '!=', False)]}"/> + <button name="action_backorder" string="Validate" + colspan="1" type="object" class="btn-primary" attrs="{'invisible': [('show_backorder_lines', '=', False)]}"/> + <button name="action_close_mo" type="object" string="Mark as Done" attrs="{'invisible': [('show_backorder_lines', '!=', False)]}"/> + <button string="Cancel" class="btn-secondary" special="cancel" /> + </footer> + </form> + </field> + </record> + + <record id="action_mrp_production_backorder" model="ir.actions.act_window"> + <field name="name">Backorder</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">mrp.production.backorder</field> + <field name="view_mode">form</field> + <field name="target">new</field> + </record> + + </data> +</odoo> diff --git a/addons/mrp_account/models/mrp_production.py b/addons/mrp_account/models/mrp_production.py index 6502d71a824c58ae21bc4a493ecc5e7b5b35b0e2..b53b38d5a95814821fb262c1249ff3f362f504ae 100644 --- a/addons/mrp_account/models/mrp_production.py +++ b/addons/mrp_account/models/mrp_production.py @@ -71,9 +71,9 @@ class MrpProduction(models.Model): AccountAnalyticLine.create(vals) def button_mark_done(self): - self.ensure_one() res = super(MrpProduction, self).button_mark_done() - self._costs_generate() + for order in self: + order._costs_generate() return res def action_view_stock_valuation_layers(self): @@ -83,4 +83,5 @@ class MrpProduction(models.Model): context = literal_eval(action['context']) context.update(self.env.context) context['no_at_date'] = True + context['search_default_group_by_product_id'] = False return dict(action, domain=domain, context=context) diff --git a/addons/mrp_account/models/product.py b/addons/mrp_account/models/product.py index 61f067b020a462a2d0c6ae16998139873b5d6437..72f32617d02e93921d411999af86f2765853ca2d 100644 --- a/addons/mrp_account/models/product.py +++ b/addons/mrp_account/models/product.py @@ -47,7 +47,7 @@ class ProductProduct(models.Model): if not boms_to_recompute: boms_to_recompute = [] total = 0 - for opt in bom.routing_id.operation_ids: + for opt in bom.operation_ids: duration_expected = ( opt.workcenter_id.time_start + opt.workcenter_id.time_stop + diff --git a/addons/mrp_account/tests/test_bom_price.py b/addons/mrp_account/tests/test_bom_price.py index b015080a883f56130d1e979064413a67f24521ec..813a8c2b6a8cce2e0e9d68f7b3d0183b4c4d5ea5 100644 --- a/addons/mrp_account/tests/test_bom_price.py +++ b/addons/mrp_account/tests/test_bom_price.py @@ -19,7 +19,7 @@ class TestBom(common.TransactionCase): super(TestBom, self).setUp() self.Product = self.env['product.product'] self.Bom = self.env['mrp.bom'] - self.Routing = self.env['mrp.routing'] + #self.Routing = self.env['mrp.routing'] self.operation = self.env['mrp.routing.workcenter'] # Products. @@ -117,37 +117,63 @@ class TestBom(common.TransactionCase): workcenter_from1.costs_hour = 100 workcenter_1 = workcenter_from1.save() - routing_form1 = Form(self.Routing) - routing_form1.name = 'Assembly Furniture' - routing_1 = routing_form1.save() - - operation_1 = self.operation.create({ - 'name': 'Cutting', - 'workcenter_id': workcenter_1.id, - 'routing_id': routing_1.id, - 'time_mode': 'manual', - 'time_cycle_manual': 20, - 'batch': 'no', - 'sequence': 1, - }) - operation_2 = self.operation.create({ - 'name': 'Drilling', - 'workcenter_id': workcenter_1.id, - 'routing_id': routing_1.id, - 'time_mode': 'manual', - 'time_cycle_manual': 25, - 'batch': 'no', - 'sequence': 2, - }) - operation_3 = self.operation.create({ - 'name': 'Fitting', - 'workcenter_id': workcenter_1.id, - 'routing_id': routing_1.id, - 'time_mode': 'manual', - 'time_cycle_manual': 30, - 'batch': 'no', - 'sequence': 3, - }) + self.bom_1.write({ + 'operation_ids': [ + (0, 0, { + 'name': 'Cutting', + 'workcenter_id': workcenter_1.id, + 'time_mode': 'manual', + 'time_cycle_manual': 20, + 'batch': 'no', + 'sequence': 1, + }), + (0, 0, { + 'name': 'Drilling', + 'workcenter_id': workcenter_1.id, + 'time_mode': 'manual', + 'time_cycle_manual': 25, + 'batch': 'no', + 'sequence': 2, + }), + (0, 0, { + 'name': 'Fitting', + 'workcenter_id': workcenter_1.id, + 'time_mode': 'manual', + 'time_cycle_manual': 30, + 'batch': 'no', + 'sequence': 3, + }), + ], + }), + self.bom_2.write({ + 'operation_ids': [ + (0, 0, { + 'name': 'Cutting', + 'workcenter_id': workcenter_1.id, + 'time_mode': 'manual', + 'time_cycle_manual': 20, + 'batch': 'no', + 'sequence': 1, + }), + (0, 0, { + 'name': 'Drilling', + 'workcenter_id': workcenter_1.id, + 'time_mode': 'manual', + 'time_cycle_manual': 25, + 'batch': 'no', + 'sequence': 2, + }), + (0, 0, { + 'name': 'Fitting', + 'workcenter_id': workcenter_1.id, + 'time_mode': 'manual', + 'time_cycle_manual': 30, + 'batch': 'no', + 'sequence': 3, + }), + ], + }), + # ----------------------------------------------------------------- # Dinning Table Operation Cost(1 Unit) @@ -160,7 +186,6 @@ class TestBom(common.TransactionCase): # Operation Cost 1 unit = 125 # ----------------------------------------------------------------- - self.bom_1.routing_id = routing_1.id # -------------------------------------------------------------------------- # Table Head Operation Cost (1 Dozen) @@ -173,7 +198,6 @@ class TestBom(common.TransactionCase): # Operation Cost 1 dozen (125 per dozen) and 10.42 for 1 Unit # -------------------------------------------------------------------------- - self.bom_2.routing_id = routing_1.id self.assertEqual(self.dining_table.standard_price, 1000, "Initial price of the Product should be 1000") self.dining_table.button_bom_cost() diff --git a/addons/mrp_account/tests/test_mrp_account.py b/addons/mrp_account/tests/test_mrp_account.py index 8bb590c69863ae9d86d70ed4e541e6828e2549fd..f827a0eb02359e8d3f40c0aeb39fd5ba9b47319a 100644 --- a/addons/mrp_account/tests/test_mrp_account.py +++ b/addons/mrp_account/tests/test_mrp_account.py @@ -1,16 +1,120 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.addons.mrp.tests.test_workorder_operation import TestWorkOrderProcessCommon +from odoo.addons.mrp.tests.common import TestMrpCommon from odoo.tests import Form -class TestMrpAccount(TestWorkOrderProcessCommon): +class TestMrpAccount(TestMrpCommon): @classmethod def setUpClass(cls): super(TestMrpAccount, cls).setUpClass() + cls.source_location_id = cls.stock_location_14.id + cls.warehouse = cls.env.ref('stock.warehouse0') + # setting up alternative workcenters + cls.wc_alt_1 = cls.env['mrp.workcenter'].create({ + 'name': 'Nuclear Workcenter bis', + 'capacity': 3, + 'time_start': 9, + 'time_stop': 5, + 'time_efficiency': 80, + }) + cls.wc_alt_2 = cls.env['mrp.workcenter'].create({ + 'name': 'Nuclear Workcenter ter', + 'capacity': 1, + 'time_start': 10, + 'time_stop': 5, + 'time_efficiency': 85, + }) + cls.product_4.uom_id = cls.uom_unit + cls.planning_bom = cls.env['mrp.bom'].create({ + 'product_id': cls.product_4.id, + 'product_tmpl_id': cls.product_4.product_tmpl_id.id, + 'product_uom_id': cls.uom_unit.id, + 'product_qty': 4.0, + 'consumption': 'flexible', + 'operation_ids': [ + (0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}), + ], + 'type': 'normal', + 'bom_line_ids': [ + (0, 0, {'product_id': cls.product_2.id, 'product_qty': 2}), + (0, 0, {'product_id': cls.product_1.id, 'product_qty': 4}) + ]}) + cls.dining_table = cls.env['product.product'].create({ + 'name': 'Table (MTO)', + 'type': 'product', + 'tracking': 'serial', + }) + cls.product_table_sheet = cls.env['product.product'].create({ + 'name': 'Table Top', + 'type': 'product', + 'tracking': 'serial', + }) + cls.product_table_leg = cls.env['product.product'].create({ + 'name': 'Table Leg', + 'type': 'product', + 'tracking': 'lot', + }) + cls.product_bolt = cls.env['product.product'].create({ + 'name': 'Bolt', + 'type': 'product', + }) + cls.product_screw = cls.env['product.product'].create({ + 'name': 'Screw', + 'type': 'product', + }) + cls.mrp_workcenter = cls.env['mrp.workcenter'].create({ + 'name': 'Assembly Line 1', + 'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id, + }) + cls.mrp_bom_desk = cls.env['mrp.bom'].create({ + 'product_tmpl_id': cls.dining_table.product_tmpl_id.id, + 'product_uom_id': cls.env.ref('uom.product_uom_unit').id, + 'sequence': 3, + 'consumption': 'flexible', + 'operation_ids': [ + (0, 0, {'workcenter_id': cls.mrp_workcenter.id, 'name': 'Manual Assembly'}), + ], + }) + cls.mrp_bom_desk.write({ + 'bom_line_ids': [ + (0, 0, { + 'product_id': cls.product_table_sheet.id, + 'product_qty': 1, + 'product_uom_id': cls.env.ref('uom.product_uom_unit').id, + 'sequence': 1, + 'operation_id': cls.mrp_bom_desk.operation_ids.id}), + (0, 0, { + 'product_id': cls.product_table_leg.id, + 'product_qty': 4, + 'product_uom_id': cls.env.ref('uom.product_uom_unit').id, + 'sequence': 2, + 'operation_id': cls.mrp_bom_desk.operation_ids.id}), + (0, 0, { + 'product_id': cls.product_bolt.id, + 'product_qty': 4, + 'product_uom_id': cls.env.ref('uom.product_uom_unit').id, + 'sequence': 3, + 'operation_id': cls.mrp_bom_desk.operation_ids.id}), + (0, 0, { + 'product_id': cls.product_screw.id, + 'product_qty': 10, + 'product_uom_id': cls.env.ref('uom.product_uom_unit').id, + 'sequence': 4, + 'operation_id': cls.mrp_bom_desk.operation_ids.id}), + ] + }) + cls.mrp_workcenter_1 = cls.env['mrp.workcenter'].create({ + 'name': 'Drill Station 1', + 'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id, + }) + cls.mrp_workcenter_3 = cls.env['mrp.workcenter'].create({ + 'name': 'Assembly Line 1', + 'resource_calendar_id': cls.env.ref('resource.resource_calendar_std').id, + }) cls.categ_standard = cls.env['product.category'].create({ 'name': 'STANDARD', 'property_cost_method': 'standard' @@ -66,24 +170,20 @@ class TestMrpAccount(TestWorkOrderProcessCommon): }) inventory.action_validate bom = self.mrp_bom_desk.copy() - bom.routing_id = False # TODO: extend the test later with the necessary operations + bom.operation_ids = False production_table_form = Form(self.env['mrp.production']) production_table_form.product_id = self.dining_table production_table_form.bom_id = bom - production_table_form.product_qty = 5.0 + production_table_form.product_qty = 1 production_table = production_table_form.save() production_table.extra_cost = 20 production_table.action_confirm() - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': production_table.id, - 'active_ids': [production_table.id], - })) - produce_form.qty_producing = 1.0 - produce_wizard = produce_form.save() - produce_wizard.do_produce() - production_table.post_inventory() + mo_form = Form(production_table) + mo_form.qty_producing = 1 + production_table = mo_form.save() + production_table._post_inventory() move_value = production_table.move_finished_ids.filtered(lambda x: x.state == "done").stock_valuation_layer_ids.value # 1 table head at 20 + 4 table leg at 15 + 4 bolt at 10 + 10 screw at 10 + 1*20 (extra cost) diff --git a/addons/mrp_account/tests/test_valuation_layers.py b/addons/mrp_account/tests/test_valuation_layers.py index bf50edf042b96860070a563ee054ae5d2d737b0d..df03cc163de764958e8cbc94efa443ab1829c4e5 100644 --- a/addons/mrp_account/tests/test_valuation_layers.py +++ b/addons/mrp_account/tests/test_valuation_layers.py @@ -39,14 +39,11 @@ class TestMrpValuationCommon(TestStockValuationCommon): return mo def _produce(self, mo, quantity=0): - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - if quantity: - produce_form.qty_producing = quantity - product_produce = produce_form.save() - product_produce.do_produce() + mo_form = Form(mo) + if not quantity: + quantity = mo.product_qty - mo.qty_produced + mo_form.qty_producing += quantity + mo = mo_form.save() class TestMrpValuationStandard(TestMrpValuationCommon): @@ -58,7 +55,10 @@ class TestMrpValuationStandard(TestMrpValuationCommon): self._make_in_move(self.component, 1, 20) mo = self._make_mo(self.bom, 2) self._produce(mo, 1) - mo.post_inventory() + action = mo.button_mark_done() + backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context'])) + backorder.save().action_backorder() + mo = mo.procurement_group_id.mrp_production_ids[-1] self.assertEqual(self.component.value_svl, 20) self.assertEqual(self.product1.value_svl, 10) self.assertEqual(self.component.quantity_svl, 1) @@ -94,7 +94,10 @@ class TestMrpValuationStandard(TestMrpValuationCommon): self._make_in_move(self.component, 1, 20) mo = self._make_mo(self.bom, 2) self._produce(mo, 1) - mo.post_inventory() + action = mo.button_mark_done() + backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context'])) + backorder.save().action_backorder() + mo = mo.procurement_group_id.mrp_production_ids[-1] self.assertEqual(self.component.value_svl, 20) self.assertEqual(self.product1.value_svl, 10) self.assertEqual(self.component.quantity_svl, 1) @@ -131,7 +134,7 @@ class TestMrpValuationStandard(TestMrpValuationCommon): self._make_in_move(self.component, 1, 20) mo = self._make_mo(self.bom, 2) self._produce(mo, 1) - mo.post_inventory() + mo._post_inventory() self.assertEqual(self.component.value_svl, 20) self.assertEqual(self.product1.value_svl, 8.8) self.assertEqual(self.component.quantity_svl, 1) @@ -169,7 +172,7 @@ class TestMrpValuationStandard(TestMrpValuationCommon): self._make_in_move(self.component, 1) mo = self._make_mo(self.bom, 2) self._produce(mo, 1) - mo.post_inventory() + mo._post_inventory() self.assertEqual(self.component.value_svl, 8.8) self.assertEqual(self.product1.value_svl, 8.8) self.assertEqual(self.component.quantity_svl, 1) @@ -208,7 +211,7 @@ class TestMrpValuationStandard(TestMrpValuationCommon): self._make_in_move(self.component, 1) mo = self._make_mo(self.bom, 2) self._produce(mo, 1) - mo.post_inventory() + mo._post_inventory() self.assertEqual(self.component.value_svl, 8.8) self.assertEqual(self.product1.value_svl, 7.2) self.assertEqual(self.component.quantity_svl, 1) @@ -246,7 +249,7 @@ class TestMrpValuationStandard(TestMrpValuationCommon): self._make_in_move(self.component, 1, 20) mo = self._make_mo(self.bom, 2) self._produce(mo, 1) - mo.post_inventory() + mo._post_inventory() self.assertEqual(self.component.value_svl, 15) self.assertEqual(self.product1.value_svl, 15) self.assertEqual(self.component.quantity_svl, 1) diff --git a/addons/mrp_account/views/mrp_production_views.xml b/addons/mrp_account/views/mrp_production_views.xml index 69652e0d26ab80e4350c093565f60740fe6bee77..4b1083c7a41c87462f88e2e109a4a833b5ef69fa 100644 --- a/addons/mrp_account/views/mrp_production_views.xml +++ b/addons/mrp_account/views/mrp_production_views.xml @@ -5,10 +5,10 @@ <field name="inherit_id" ref="mrp.mrp_production_form_view" /> <field name="groups_id" eval="[(4, ref('stock.group_stock_manager'))]"/> <field name="arch" type="xml"> - <xpath expr="//field[@name='finished_move_line_ids']" position="before"> + <xpath expr="//field[@name='product_id']" position="before"> <field name="show_valuation" invisible="1"/> </xpath> - <xpath expr="//button[@name='action_view_mrp_production_sources']" position="after"> + <xpath expr="//div[@name='button_box']" position="inside"> <button string="Valuation" type="object" name="action_view_stock_valuation_layers" class="oe_stat_button" icon="fa-dollar" groups="base.group_no_one" diff --git a/addons/mrp_landed_costs/tests/test_stock_landed_costs_mrp.py b/addons/mrp_landed_costs/tests/test_stock_landed_costs_mrp.py index 97e9863b0299fd3f9880d680808ee27c2ee6051b..6ef491ac4fb5f7487830bbecee392926b3882c12 100644 --- a/addons/mrp_landed_costs/tests/test_stock_landed_costs_mrp.py +++ b/addons/mrp_landed_costs/tests/test_stock_landed_costs_mrp.py @@ -37,16 +37,12 @@ class TestStockLandedCostsMrp(StockAccountTestCommon): 'type': 'product', 'categ_id': cls.categ_all.id }) - cls.routing_1 = cls.env['mrp.routing'].create({ - 'name': 'Simple Line', - }) cls.uom_unit = cls.env.ref('uom.product_uom_unit') cls.bom_refri = cls.env['mrp.bom'].create({ 'product_id': cls.product_refrigerator.id, 'product_tmpl_id': cls.product_refrigerator.product_tmpl_id.id, 'product_uom_id': cls.uom_unit.id, 'product_qty': 1.0, - 'routing_id': cls.routing_1.id, 'type': 'normal', }) cls.bom_refri_line1 = cls.env['mrp.bom.line'].create({ @@ -121,13 +117,10 @@ class TestStockLandedCostsMrp(StockAccountTestCommon): self.assertEqual(first_move.product_qty, 2.0) # produce product - produce_form = Form(self.env['mrp.product.produce'].with_user(self.allow_user).with_context({ - 'active_id': man_order.id, - 'active_ids': [man_order.id], - })) - produce_form.qty_producing = 2.0 - produce_wizard = produce_form.save() - produce_wizard.do_produce() + mo_form = Form(man_order.with_user(self.allow_user)) + mo_form.qty_producing = 2 + man_order = mo_form.save() + man_order.button_mark_done() diff --git a/addons/mrp_product_expiry/models/__init__.py b/addons/mrp_product_expiry/models/__init__.py index f75cb03c89bcdb05e6819ee091cd24385ddad9c5..3728a50a4f0793c483dd69065f3706c1d8c1b929 100644 --- a/addons/mrp_product_expiry/models/__init__.py +++ b/addons/mrp_product_expiry/models/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import mrp_abstract_workorder -from . import mrp_workorder +from . import mrp_production + diff --git a/addons/mrp_product_expiry/models/mrp_abstract_workorder.py b/addons/mrp_product_expiry/models/mrp_production.py similarity index 67% rename from addons/mrp_product_expiry/models/mrp_abstract_workorder.py rename to addons/mrp_product_expiry/models/mrp_production.py index 77bae4eb238d941d714d9e7f79c73e798b4bc688..21522a84f60e830e422bc2054834262e5fda7b62 100644 --- a/addons/mrp_product_expiry/models/mrp_abstract_workorder.py +++ b/addons/mrp_product_expiry/models/mrp_production.py @@ -4,15 +4,21 @@ from odoo import models, _ -class MrpAbstractWorkorder(models.AbstractModel): - _inherit = 'mrp.abstract.workorder' +class MrpWorkorder(models.Model): + _inherit = 'mrp.production' + + def _pre_button_mark_done(self): + confirm_expired_lots = self._check_expired_lots() + if confirm_expired_lots: + return confirm_expired_lots + return super()._pre_button_mark_done() def _check_expired_lots(self): # We use the 'skip_expired' context key to avoid to make the check when # user already confirmed the wizard about using expired lots. if self.env.context.get('skip_expired'): return False - expired_lot_ids = self.raw_workorder_line_ids.filtered(lambda ml: ml.lot_id.product_expiry_alert).lot_id.ids + expired_lot_ids = self.move_raw_ids.move_line_ids.filtered(lambda ml: ml.lot_id.product_expiry_alert).lot_id.ids if expired_lot_ids: return { 'name': _('Confirmation'), @@ -27,5 +33,6 @@ class MrpAbstractWorkorder(models.AbstractModel): context = dict(self.env.context) context.update({ 'default_lot_ids': [(6, 0, expired_lot_ids)], + 'default_production_ids': self.ids, }) return context diff --git a/addons/mrp_product_expiry/models/mrp_workorder.py b/addons/mrp_product_expiry/models/mrp_workorder.py deleted file mode 100644 index 9e7a04372de0c8b2f6b48c220d50740042e6debf..0000000000000000000000000000000000000000 --- a/addons/mrp_product_expiry/models/mrp_workorder.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import models - - -class MrpWorkorder(models.Model): - _inherit = 'mrp.workorder' - - def record_production(self): - confirm_expired_lots = self._check_expired_lots() - if confirm_expired_lots: - return confirm_expired_lots - return super(MrpWorkorder, self).record_production() - - def _get_expired_context(self, expired_lot_ids): - context = super(MrpWorkorder, self)._get_expired_context(expired_lot_ids) - context['default_workorder_id'] = self.id - return context diff --git a/addons/mrp_product_expiry/tests/test_mrp_product_expiry.py b/addons/mrp_product_expiry/tests/test_mrp_product_expiry.py index 0633317f6b12a6c22bda3716bd2f342868f06d57..137877c95a05d49d9a071215e1e1e8667aeb5eca 100644 --- a/addons/mrp_product_expiry/tests/test_mrp_product_expiry.py +++ b/addons/mrp_product_expiry/tests/test_mrp_product_expiry.py @@ -49,6 +49,7 @@ class TestStockProductionLot(TestStockCommon): 'product_tmpl_id': cls.product_apple_pie.product_tmpl_id.id, 'product_uom_id': cls.uom_unit.id, 'product_qty': 1.0, + 'consumption': 'flexible', 'type': 'normal', 'bom_line_ids': [ (0, 0, {'product_id': cls.product_apple.id, 'product_qty': 3}), @@ -64,14 +65,6 @@ class TestStockProductionLot(TestStockCommon): 'time_stop': 5, 'time_efficiency': 80, }) - cls.routing = cls.env['mrp.routing'].create({'name': 'COOK'}) - cls.operation = cls.env['mrp.routing.workcenter'].create({ - 'name': 'Bake in the oven', - 'workcenter_id': cls.workcenter.id, - 'routing_id': cls.routing.id, - 'time_cycle': 15, - 'sequence': 1, - }) def test_01_product_produce(self): """ Checks user doesn't get a confirmation wizard when they produces with @@ -84,16 +77,17 @@ class TestStockProductionLot(TestStockCommon): mo = mo_form.save() mo.action_confirm() # ... and tries to product with a non-expired lot as component. - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - with produce_form.raw_workorder_line_ids.edit(0) as line: - line.lot_id = self.lot_good_apple - product_produce = produce_form.save() - res = product_produce.do_produce() + 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.new() as ml: + ml.qty_done = 3 + ml.lot_id = self.lot_good_apple + details_operation_form.save() + res = mo.button_mark_done() # Producing must not return a wizard in this case. - self.assertEqual(res['type'], 'ir.actions.act_window_close') + self.assertEqual(res, True) def test_02_product_produce_using_expired(self): """ Checks user gets a confirmation wizard when they produces with @@ -106,72 +100,16 @@ class TestStockProductionLot(TestStockCommon): mo = mo_form.save() mo.action_confirm() # ... and tries to product with an expired lot as component. - produce_form = Form(self.env['mrp.product.produce'].with_context({ - 'active_id': mo.id, - 'active_ids': [mo.id], - })) - with produce_form.raw_workorder_line_ids.edit(0) as line: - line.lot_id = self.lot_expired_apple - product_produce = produce_form.save() - res = product_produce.do_produce() + 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.new() as ml: + ml.qty_done = 3 + ml.lot_id = self.lot_expired_apple + details_operation_form.save() + res = mo.button_mark_done() # Producing must return a confirmation wizard. self.assertNotEqual(res, None) self.assertEqual(res['res_model'], 'expiry.picking.confirmation') - def test_03_workorder_without_expired_lot(self): - """ Checks user doesn't get a confirmation wizard when they makes a - workorder without expired components. """ - # Set a routing on the BOM. - self.bom_apple_pie.routing_id = self.routing - # Creates the MO, starts it and plans the Work Order. - mo_form = Form(self.env['mrp.production']) - mo_form.product_id = self.product_apple_pie - mo_form.bom_id = self.bom_apple_pie - mo_form.product_qty = 1 - mo = mo_form.save() - mo.action_confirm() - mo.button_plan() - - wo = mo.workorder_ids[0] - wo.button_start() - # Set a non-expired lot. - wo.raw_workorder_line_ids.write({ - 'qty_done': 3, - 'lot_id': self.lot_good_apple, - }) - - res = wo.record_production() - # Try to record the production using non-expired lot must not return a wizard. - self.assertEqual(res, True) - mo.button_mark_done() - - def test_04_workorder_with_expired_lot(self): - """ Checks user doesn't get a confirmation wizard when they makes a - workorder without expired components. """ - # Set a routing on the BOM. - self.bom_apple_pie.routing_id = self.routing - # Creates the MO, starts it and plans the Work Order. - mo_form = Form(self.env['mrp.production']) - mo_form.product_id = self.product_apple_pie - mo_form.bom_id = self.bom_apple_pie - mo_form.product_qty = 1 - mo = mo_form.save() - mo.action_confirm() - mo.button_plan() - - wo = mo.workorder_ids[0] - wo.button_start() - # Set an expired lot. - wo.raw_workorder_line_ids.write({ - 'qty_done': 3, - 'lot_id': self.lot_expired_apple, - }) - - res = wo.record_production() - # Try to record the production using expired lot must return a - # confirmation wizard. - self.assertNotEqual(res, None) - self.assertEqual(res['res_model'], 'expiry.picking.confirmation') - with self.assertRaises(UserError): - # Cannot finish the MO as the Work Order is still ongoing. - mo.button_mark_done() diff --git a/addons/mrp_product_expiry/wizard/__init__.py b/addons/mrp_product_expiry/wizard/__init__.py index 9d9455305104b44f6f73ea881a1587ae33607eb8..e6eb849c0e831022d8da175703aef31b683e3ae6 100644 --- a/addons/mrp_product_expiry/wizard/__init__.py +++ b/addons/mrp_product_expiry/wizard/__init__.py @@ -2,4 +2,3 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import confirm_expiry -from . import mrp_product_produce diff --git a/addons/mrp_product_expiry/wizard/confirm_expiry.py b/addons/mrp_product_expiry/wizard/confirm_expiry.py index d754c0702536b19858034bb44fa5bb6e97754299..b4ef5d4b768c967b180894d9d94d5538f169e484 100644 --- a/addons/mrp_product_expiry/wizard/confirm_expiry.py +++ b/addons/mrp_product_expiry/wizard/confirm_expiry.py @@ -7,12 +7,12 @@ from odoo import api, fields, models, _ class ConfirmExpiry(models.TransientModel): _inherit = 'expiry.picking.confirmation' - produce_id = fields.Many2one('mrp.product.produce', readonly=True) + production_ids = fields.Many2many('mrp.production', readonly=True) workorder_id = fields.Many2one('mrp.workorder', readonly=True) @api.depends('lot_ids') def _compute_descriptive_fields(self): - if self.produce_id or self.workorder_id: + if self.production_ids or self.workorder_id: # Shows expired lots only if we are more than one expired lot. self.show_lots = len(self.lot_ids) > 1 if self.show_lots: @@ -34,13 +34,8 @@ class ConfirmExpiry(models.TransientModel): super(ConfirmExpiry, self)._compute_descriptive_fields() def confirm_produce(self): - return self.produce_id.with_context(skip_expired=True).do_produce() + return self.production_ids.with_context(skip_expired=True).button_mark_done() def confirm_workorder(self): return self.workorder_id.with_context(skip_expired=True).record_production() - def return_to_produce_wizard(self): - production = self.produce_id.production_id - action = production.open_produce_product() - action['context'] = {'default_production_id': production.id} - return action diff --git a/addons/mrp_product_expiry/wizard/confirm_expiry_view.xml b/addons/mrp_product_expiry/wizard/confirm_expiry_view.xml index d483c624070ebb5272093eca74e3faf268651827..5256ebd2a10539f540527843f25c9fbc43ef2d85 100644 --- a/addons/mrp_product_expiry/wizard/confirm_expiry_view.xml +++ b/addons/mrp_product_expiry/wizard/confirm_expiry_view.xml @@ -7,7 +7,7 @@ <field name="arch" type="xml"> <xpath expr="//field[@name='description']" position="after"> <field name="picking_ids" invisible="1"/> - <field name="produce_id" invisible="1"/> + <field name="production_ids" invisible="1"/> <field name="workorder_id" invisible="1"/> </xpath> <xpath expr="//button[@name='process']" position="attributes"> @@ -17,19 +17,18 @@ <attribute name="attrs">{'invisible': [('picking_ids', '=', [])]}</attribute> </xpath> <xpath expr="//button[@special='cancel']" position="attributes"> - <attribute name="attrs">{'invisible': [('produce_id', '!=', False)]}</attribute> + <attribute name="attrs">{'invisible': [('production_ids', '!=', [])]}</attribute> </xpath> <xpath expr="//button[@name='process']" position="after"> <!-- From Produce Product wizard --> <button name="confirm_produce" string="Confirm" type="object" - attrs="{'invisible': [('produce_id', '=', False)]}" + attrs="{'invisible': [('production_ids', '=', [])]}" class="btn-primary"/> - <button name="return_to_produce_wizard" + <button special="cancel" string="Discard" - type="object" - attrs="{'invisible': [('produce_id', '=', False)]}" + attrs="{'invisible': [('production_ids', '=', [])]}" class="btn-secondary"/> <!-- From a Workorder --> <button name="confirm_workorder" diff --git a/addons/mrp_product_expiry/wizard/mrp_product_produce.py b/addons/mrp_product_expiry/wizard/mrp_product_produce.py deleted file mode 100644 index fd6c6faade07962ce17376452023ff87cf5d96a0..0000000000000000000000000000000000000000 --- a/addons/mrp_product_expiry/wizard/mrp_product_produce.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import models - - -class MrpProductProduce(models.TransientModel): - _inherit = 'mrp.product.produce' - - def do_produce(self): - confirm_expired_lots = self._check_expired_lots() - if confirm_expired_lots: - return confirm_expired_lots - return super(MrpProductProduce, self).do_produce() - - def _get_expired_context(self, expired_lot_ids): - context = super(MrpProductProduce, self)._get_expired_context(expired_lot_ids) - context['default_produce_id'] = self.id - return context diff --git a/addons/mrp_subcontracting/models/stock_move.py b/addons/mrp_subcontracting/models/stock_move.py index 72f50c9374e8c7e6d23e7376afc36f8095b34255..e33f5377c0b53807918371698e7132aa7fc107ca 100644 --- a/addons/mrp_subcontracting/models/stock_move.py +++ b/addons/mrp_subcontracting/models/stock_move.py @@ -126,12 +126,21 @@ class StockMove(models.Model): return res def _action_record_components(self): - action = self.env.ref('mrp.act_mrp_product_produce').read()[0] - action['context'] = dict( - default_production_id=self.move_orig_ids.production_id.id, - default_subcontract_move_id=self.id - ) - return action + self.ensure_one() + production = self.move_orig_ids.production_id + view = self.env.ref('mrp.mrp_production_form_view') + return { + 'name': _('Subcontract'), + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_model': 'mrp.production', + 'views': [(view.id, 'form')], + 'view_id': view.id, + 'target': 'new', + 'res_id': production.id, + 'context': dict(self.env.context, subcontract_move_id=self.id), + } + def _check_overprocessed_subcontract_qty(self): """ If a subcontracted move use tracked components. Do not allow to add @@ -147,8 +156,8 @@ class StockMove(models.Model): if not move._has_tracked_subcontract_components(): continue rounding = move.product_uom.rounding - if float_compare(move.quantity_done, move.move_orig_ids.production_id.qty_produced, precision_rounding=rounding) > 0: - overprocessed_moves |= move +# if float_compare(move.quantity_done, move.move_orig_ids.production_id.qty_produced, precision_rounding=rounding) > 0: +# overprocessed_moves |= move if overprocessed_moves: raise UserError(_(""" You have to use 'Records Components' button in order to register quantity for a diff --git a/addons/mrp_subcontracting/models/stock_picking.py b/addons/mrp_subcontracting/models/stock_picking.py index 29c44aec51b07f8c1f58864da138c89a13d211f0..3af0d45b1ff2479821bb896cb32474d286ac8b5b 100644 --- a/addons/mrp_subcontracting/models/stock_picking.py +++ b/addons/mrp_subcontracting/models/stock_picking.py @@ -4,6 +4,7 @@ from datetime import timedelta from odoo import api, fields, models +from odoo.tools import float_is_zero class StockPicking(models.Model): @@ -43,7 +44,9 @@ class StockPicking(models.Model): for move in picking.move_lines: if not move.is_subcontract: continue - production = move.move_orig_ids.production_id + production = move.move_orig_ids.production_id.filtered(lambda p: p.state not in ('done', 'cancel')) + if len(production) > 1: + production = production[-1] if move._has_tracked_subcontract_components(): move.move_orig_ids.filtered(lambda m: m.state not in ('done', 'cancel')).move_line_ids.unlink() move_finished_ids = move.move_orig_ids.filtered(lambda m: m.state not in ('done', 'cancel')) @@ -58,20 +61,23 @@ class StockPicking(models.Model): 'location_dest_id': move_finished_ids.location_dest_id.id, }) else: - for move_line in move.move_line_ids: - produce = self.env['mrp.product.produce'].with_context(default_production_id=production.id).create({ - 'production_id': production.id, - 'qty_producing': move_line.qty_done, - 'product_uom_id': move_line.product_uom_id.id, - 'finished_lot_id': move_line.lot_id.id, - 'consumption': 'strict', - }) - produce._generate_produce_lines() - produce._record_production() + if not move.move_line_ids.lot_id: + qty_done_production_uom = move.product_uom._compute_quantity(move.quantity_done, production.product_uom_id) + production.qty_producing += qty_done_production_uom + for move in (production.move_raw_ids | production.move_finished_ids.filtered(lambda m: m.product_id != production.product_id)): + if move.state in ('done', 'cancel'): + continue + if float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding): + continue + new_qty = production.product_uom_id._compute_quantity( + (production.qty_producing - production.qty_produced) * move.unit_factor, production.product_uom_id, + rounding_method='HALF-UP') + move.move_line_ids.filtered(lambda ml: ml.state not in ('done', 'cancel')).qty_done = 0 + move.move_line_ids = move._set_quantity_done_prepare_vals(new_qty) productions |= production for subcontracted_production in productions: if subcontracted_production.state == 'progress': - subcontracted_production.post_inventory() + subcontracted_production._post_inventory() else: subcontracted_production.button_mark_done() # For concistency, set the date on production move before the date @@ -132,6 +138,7 @@ class StockPicking(models.Model): for move, bom in subcontract_details: mo = self.env['mrp.production'].with_company(move.company_id).create(self._prepare_subcontract_mo_vals(move, bom)) self.env['stock.move'].create(mo._get_moves_raw_values()) + self.env['stock.move'].create(mo._get_moves_finished_values()) mo.action_confirm() # Link the finished to the receipt move. diff --git a/addons/mrp_subcontracting/tests/test_subcontracting.py b/addons/mrp_subcontracting/tests/test_subcontracting.py index 281e32c96ea15240bb96d053f0ba451fc00ebf6e..c282950ee97331fef4e1e13114d2b0bc811be729 100644 --- a/addons/mrp_subcontracting/tests/test_subcontracting.py +++ b/addons/mrp_subcontracting/tests/test_subcontracting.py @@ -415,7 +415,7 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon): picking_form.partner_id = self.subcontractor_partner1 with picking_form.move_ids_without_package.new() as move: move.product_id = self.finished - move.product_uom_qty = 5 + move.product_uom_qty = 3 # FIXME sle: need to handle the backorder in subcontract picking_receipt = picking_form.save() picking_receipt.action_confirm() mo = picking_receipt.move_lines.move_orig_ids.production_id @@ -442,39 +442,26 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon): 'product_id': self.finished.id, 'company_id': self.env.company.id, }) + mo_form = Form(picking_receipt._get_subcontracted_productions().with_context(subcontract_move_id=picking_receipt.move_lines.id)) + mo_form.qty_producing = 3 + mo_form.lot_producing_id = lot_f1 + 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 = lot_c1 + ml.qty_done = 3 + 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 = lot_c2 + ml.qty_done = 3 + details_operation_form.save() - register_form = Form(self.env['mrp.product.produce'].with_context( - active_id=picking_receipt._get_subcontracted_productions().id, - default_subcontract_move_id=picking_receipt.move_lines.id - )) - register_form.qty_producing = 3.0 - self.assertEqual(len(register_form._values['raw_workorder_line_ids']), 2, - 'Register Components Form should contains one line per component.') - self.assertTrue(all(p[2]['product_id'] in (self.comp1 | self.comp2).ids for p in register_form._values['raw_workorder_line_ids']), - 'Register Components Form should contains component.') - with register_form.raw_workorder_line_ids.edit(0) as pl: - pl.lot_id = lot_c1 - with register_form.raw_workorder_line_ids.edit(1) as pl: - pl.lot_id = lot_c2 - register_form.finished_lot_id = lot_f1 - register_wizard = register_form.save() - action = register_wizard.continue_production() - register_form = Form(self.env['mrp.product.produce'].with_context( - **action['context'] - )) - with register_form.raw_workorder_line_ids.edit(0) as pl: - pl.lot_id = lot_c1 - with register_form.raw_workorder_line_ids.edit(1) as pl: - pl.lot_id = lot_c2 - register_form.finished_lot_id = lot_f1 - register_wizard = register_form.save() - register_wizard.do_produce() - - self.assertEqual(move_comp1.quantity_done, 5.0) + self.assertEqual(move_comp1.quantity_done, 3) self.assertEqual(move_comp1.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_id.name, 'LOT C1') - self.assertEqual(move_comp2.quantity_done, 5.0) + self.assertEqual(move_comp2.quantity_done, 3) self.assertEqual(move_comp2.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_id.name, 'LOT C2') - self.assertEqual(move_finished.quantity_done, 5.0) + self.assertEqual(move_finished.quantity_done, 3) self.assertEqual(move_finished.move_line_ids.filtered(lambda ml: ml.product_uom_qty).lot_id.name, 'LOT F1') corrected_final_lot = self.env['stock.production.lot'].create({ @@ -484,31 +471,17 @@ class TestSubcontractingFlows(TestMrpSubcontractingCommon): }) details_operation_form = Form(picking_receipt.move_lines, view=self.env.ref('stock.view_stock_move_operations')) - for i in range(len(details_operation_form._values['move_line_ids'])): - with details_operation_form.move_line_ids.edit(i) as ml: - if ml._values['qty_done']: - ml.lot_id = corrected_final_lot + with details_operation_form.move_line_ids.edit(0) as ml: + ml.lot_id = corrected_final_lot details_operation_form.save() move_raw_comp_1 = picking_receipt.move_lines.move_orig_ids.production_id.move_raw_ids.filtered(lambda m: m.product_id == self.comp1) move_raw_comp_2 = picking_receipt.move_lines.move_orig_ids.production_id.move_raw_ids.filtered(lambda m: m.product_id == self.comp2) - details_subcontract_moves_form = Form(move_raw_comp_1, view=self.env.ref('mrp_subcontracting.mrp_subcontracting_move_form_view')) - for i in range(len(details_subcontract_moves_form._values['move_line_ids'])): - with details_subcontract_moves_form.move_line_ids.edit(i) as sc: - if sc._values['qty_done']: - sc.lot_produced_ids.remove(index=0) - sc.lot_produced_ids.add(corrected_final_lot) - details_subcontract_moves_form.save() - details_subcontract_moves_form = Form(move_raw_comp_2, view=self.env.ref('mrp_subcontracting.mrp_subcontracting_move_form_view')) - for i in range(len(details_subcontract_moves_form._values['move_line_ids'])): - with details_subcontract_moves_form.move_line_ids.edit(i) as sc: - if sc._values['qty_done']: - sc.lot_produced_ids.remove(index=0) - sc.lot_produced_ids.add(corrected_final_lot) - details_subcontract_moves_form.save() - - self.assertEqual(move_comp1.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_produced_ids.name, 'LOT F2') - self.assertEqual(move_comp2.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).lot_produced_ids.name, 'LOT F2') + picking_receipt.move_lines.move_orig_ids.production_id.lot_producing_id = corrected_final_lot + picking_receipt.move_lines.move_orig_ids.production_id.button_mark_done() + + self.assertEqual(move_comp1.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).produce_line_ids.lot_id.name, 'LOT F2') + self.assertEqual(move_comp2.move_line_ids.filtered(lambda ml: not ml.product_uom_qty).produce_line_ids.lot_id.name, 'LOT F2') def test_flow_8(self): resupply_sub_on_order_route = self.env['stock.location.route'].search([('name', '=', 'Resupply Subcontractor on Order')]) @@ -693,20 +666,20 @@ class TestSubcontractingTracking(TransactionCase): 'product_id': self.comp1_sn.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], - })) - produce_form.finished_lot_id = lot_id - produce_form.raw_workorder_line_ids._records[0]['lot_id'] = serial_id.id - wiz_produce = produce_form.save() - wiz_produce.do_produce() + + mo_form = Form(mo.with_context(subcontract_move_id=picking_receipt.move_lines.id)) + mo_form.qty_producing = 1 + mo_form.lot_producing_id = lot_id + 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 = 1 + ml.lot_id = serial_id + details_operation_form.save() # We should not be able to call the 'record_components' button self.assertFalse(picking_receipt.display_action_record_components) - picking_receipt.move_lines.quantity_done = 1 - picking_receipt.move_lines.move_line_ids.lot_id = lot_id.id picking_receipt.button_validate() self.assertEqual(mo.state, 'done') diff --git a/addons/mrp_subcontracting/views/stock_move_views.xml b/addons/mrp_subcontracting/views/stock_move_views.xml index 63ccc1c0a626c27f2a7c48fa8595a66e4fc1fb81..58159cc81c9c36f7adb3b71ae63bda5fdd63c77e 100644 --- a/addons/mrp_subcontracting/views/stock_move_views.xml +++ b/addons/mrp_subcontracting/views/stock_move_views.xml @@ -17,11 +17,6 @@ <field name="state" invisible="1"/> <field name="tracking" invisible="1"/> <field name="product_id" readonly="1"/> - <field name="lot_produced_ids" - widget="many2many_tags" - context="{'default_product_id': parent.product_id}" - attrs="{'column_invisible': [('parent.finished_lots_exist', '!=', True)]}" - /> <field name="qty_done"/> <field name="lot_id" context="{'default_product_id': product_id}"/> </tree> @@ -30,17 +25,17 @@ </form> </field> </record> - <record id="mrp_subcontracting_move_tree_view" model="ir.ui.view"> - <field name="name">mrp.subcontracting.move.tree.view</field> - <field name="model">stock.move</field> - <field name="priority">1000</field> - <field name="mode">primary</field> - <field name="inherit_id" ref="mrp.view_stock_move_raw_tree"/> - <field name="arch" type="xml"> - <xpath expr="//tree" position="attributes"> - <attribute name="create">0</attribute> - <attribute name="delete">0</attribute> - </xpath> - </field> - </record> + <!-- <record id="mrp_subcontracting_move_tree_view" model="ir.ui.view"> --> + <!-- <field name="name">mrp.subcontracting.move.tree.view</field> --> + <!-- <field name="model">stock.move</field> --> + <!-- <field name="priority">1000</field> --> + <!-- <field name="mode">primary</field> --> + <!-- <field name="inherit_id" ref="mrp.view_stock_move_raw_tree"/> --> + <!-- <field name="arch" type="xml"> --> + <!-- <xpath expr="//tree" position="attributes"> --> + <!-- <attribute name="create">0</attribute> --> + <!-- <attribute name="delete">0</attribute> --> + <!-- </xpath> --> + <!-- </field> --> + <!-- </record> --> </odoo> diff --git a/addons/mrp_subcontracting/wizard/mrp_product_produce.py b/addons/mrp_subcontracting/wizard/mrp_product_produce.py index ceb803858e0c9f886c81c0527f7a51537315d73d..0dd804ad9f4c6da64ec1e4f06b951c1a56747e97 100644 --- a/addons/mrp_subcontracting/wizard/mrp_product_produce.py +++ b/addons/mrp_subcontracting/wizard/mrp_product_produce.py @@ -2,42 +2,36 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import fields, models -from odoo.tools.float_utils import float_is_zero, float_compare +from odoo.tools.float_utils import float_compare, float_is_zero -class MrpProductProduce(models.TransientModel): - _inherit = 'mrp.product.produce' - subcontract_move_id = fields.Many2one('stock.move', 'stock move from the subcontract picking', check_company=True) +class MrpProduction(models.Model): + _inherit = 'mrp.production' - def continue_production(self): - action = super(MrpProductProduce, self).continue_production() - action['context'] = dict(action['context'], default_subcontract_move_id=self.subcontract_move_id.id) - return action + def write(self, vals): + res = super().write(vals) + if self.env.context.get('subcontract_move_id'): + for production in self: + if production.state not in ('cancel', 'done') and \ + ('lot_producing_id' in vals or 'qty_producing' in vals): + production._update_finished_move() - def _generate_produce_lines(self): - """ When the wizard is called in backend, the onchange that create the - produce lines is not trigger. This method generate them and is used with - _record_production to appropriately set the lot_produced_id and - appropriately create raw stock move lines. - """ - 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.product.produce.line'].create(line_values) + def _pre_button_mark_done(self): + if self.env.context.get('subcontract_move_id'): + return True + return super()._pre_button_mark_done() def _update_finished_move(self): """ After producing, set the move line on the subcontract picking. """ - res = super(MrpProductProduce, self)._update_finished_move() - if self.subcontract_move_id: + subcontract_move_id = self.env.context.get('subcontract_move_id') + res = None + if subcontract_move_id: + subcontract_move_id = self.env['stock.move'].browse(subcontract_move_id) quantity = self.qty_producing - if self.finished_lot_id: - move_lines = self.subcontract_move_id.move_line_ids.filtered(lambda ml: ml.lot_id == self.finished_lot_id or not ml.lot_id) + if self.lot_producing_id: + move_lines = subcontract_move_id.move_line_ids.filtered(lambda ml: ml.lot_id == self.lot_producing_id or not ml.lot_id) else: - move_lines = self.subcontract_move_id.move_line_ids.filtered(lambda ml: not ml.lot_id) + move_lines = subcontract_move_id.move_line_ids.filtered(lambda ml: not ml.lot_id) # Update reservation and quantity done for ml in move_lines: rounding = ml.product_uom_id.rounding @@ -52,16 +46,14 @@ class MrpProductProduce(models.TransientModel): if float_compare(new_quantity_done, ml.product_uom_qty, precision_rounding=rounding) >= 0: ml.write({ 'qty_done': new_quantity_done, - 'lot_id': self.finished_lot_id and self.finished_lot_id.id, - 'lot_name': self.finished_lot_id and self.finished_lot_id.name, + 'lot_id': self.lot_producing_id and self.lot_producing_id.id, }) else: new_qty_reserved = ml.product_uom_qty - new_quantity_done default = { 'product_uom_qty': new_quantity_done, 'qty_done': new_quantity_done, - 'lot_id': self.finished_lot_id and self.finished_lot_id.id, - 'lot_name': self.finished_lot_id and self.finished_lot_id.name, + 'lot_id': self.lot_producing_id and self.lot_producing_id.id, } ml.copy(default=default) ml.with_context(bypass_reservation_update=True).write({ @@ -71,24 +63,30 @@ class MrpProductProduce(models.TransientModel): if float_compare(quantity, 0, precision_rounding=self.product_uom_id.rounding) > 0: self.env['stock.move.line'].create({ - 'move_id': self.subcontract_move_id.id, - 'picking_id': self.subcontract_move_id.picking_id.id, + 'move_id': subcontract_move_id.id, + 'picking_id': subcontract_move_id.picking_id.id, 'product_id': self.product_id.id, - 'location_id': self.subcontract_move_id.location_id.id, - 'location_dest_id': self.subcontract_move_id.location_dest_id.id, + 'location_id': subcontract_move_id.location_id.id, + 'location_dest_id': subcontract_move_id.location_dest_id.id, 'product_uom_qty': 0, 'product_uom_id': self.product_uom_id.id, 'qty_done': quantity, - 'lot_id': self.finished_lot_id and self.finished_lot_id.id, - 'lot_name': self.finished_lot_id and self.finished_lot_id.name, + 'lot_id': self.lot_producing_id and self.lot_producing_id.id, }) - if not self._get_todo(self.production_id): - ml_reserved = self.subcontract_move_id.move_line_ids.filtered(lambda ml: + if not self._get_todo(): + ml_reserved = subcontract_move_id.move_line_ids.filtered(lambda ml: float_is_zero(ml.qty_done, precision_rounding=ml.product_uom_id.rounding) and not float_is_zero(ml.product_uom_qty, precision_rounding=ml.product_uom_id.rounding)) ml_reserved.unlink() - for ml in self.subcontract_move_id.move_line_ids: + for ml in subcontract_move_id.move_line_ids: ml.product_uom_qty = ml.qty_done - self.subcontract_move_id._recompute_state() + subcontract_move_id._recompute_state() return res + + def _get_todo(self): + """ This method will return remaining todo quantity of production. """ + main_product_moves = self.move_finished_ids.filtered(lambda x: x.product_id.id == self.product_id.id) + todo_quantity = self.product_qty - sum(main_product_moves.mapped('quantity_done')) + todo_quantity = todo_quantity if (todo_quantity > 0) else 0 + return todo_quantity diff --git a/addons/purchase_mrp/models/purchase.py b/addons/purchase_mrp/models/purchase.py index f1e591372d3bcad9f841437f70419e0adc58d5c4..e2fe9c4195126068aca27bebdfcf0795ea9d4ffc 100644 --- a/addons/purchase_mrp/models/purchase.py +++ b/addons/purchase_mrp/models/purchase.py @@ -12,14 +12,15 @@ class PurchaseOrder(models.Model): compute='_compute_mrp_production_count', groups='mrp.group_mrp_user') - @api.depends('order_line.move_dest_ids.group_id.mrp_production_id') + @api.depends('order_line.move_dest_ids.group_id.mrp_production_ids') def _compute_mrp_production_count(self): for purchase in self: - purchase.mrp_production_count = len(purchase.order_line.move_dest_ids.group_id.mrp_production_id) + purchase.mrp_production_count = len(purchase.order_line.move_dest_ids.group_id.mrp_production_ids | + purchase.order_line.move_ids.move_dest_ids.group_id.mrp_production_ids) def action_view_mrp_productions(self): self.ensure_one() - mrp_production_ids = self.order_line.move_dest_ids.group_id.mrp_production_id.ids + mrp_production_ids = (self.order_line.move_dest_ids.group_id.mrp_production_ids | self.order_line.move_ids.move_dest_ids.group_id.mrp_production_ids).ids action = { 'res_model': 'mrp.production', 'type': 'ir.actions.act_window', diff --git a/addons/purchase_mrp/views/mrp_production_views.xml b/addons/purchase_mrp/views/mrp_production_views.xml index bcfd3b70fe173a43c01167c33f206b4eb10021aa..8aaffd649b2fd39dfc1d6f9ba46be4048a636640 100644 --- a/addons/purchase_mrp/views/mrp_production_views.xml +++ b/addons/purchase_mrp/views/mrp_production_views.xml @@ -3,9 +3,10 @@ <record id="mrp_production_form_view_purchase" model="ir.ui.view"> <field name="name">mrp.production.inherited.form.purchase</field> <field name="model">mrp.production</field> + <field name="priority">32</field> <field name="inherit_id" ref="mrp.mrp_production_form_view"/> <field name="arch" type="xml"> - <xpath expr="//div[@name='button_box']" position="inside"> + <xpath expr="//button[@name='action_view_mrp_production_childs']" position="before"> <button class="oe_stat_button" name="action_view_purchase_orders" type="object" icon="fa-credit-card" groups="purchase.group_purchase_user" attrs="{'invisible': [('purchase_order_count', '=', 0)]}"> <div class="o_field_widget o_stat_info"> <span class="o_stat_value"><field name="purchase_order_count"/></span> diff --git a/addons/sale_mrp/models/mrp_production.py b/addons/sale_mrp/models/mrp_production.py index e05fd996a2bc73e31d3eae0441e7736157e1a3ff..4d6b634ece4cb374e2ee00ae4543b73169c017c9 100644 --- a/addons/sale_mrp/models/mrp_production.py +++ b/addons/sale_mrp/models/mrp_production.py @@ -12,14 +12,14 @@ class MrpProduction(models.Model): compute='_compute_sale_order_count', groups='sales_team.group_sale_salesman') - @api.depends('move_dest_ids.group_id.sale_id') + @api.depends('procurement_group_id.mrp_production_ids.move_dest_ids.group_id.sale_id') def _compute_sale_order_count(self): for production in self: - production.sale_order_count = len(production.move_dest_ids.group_id.sale_id) + production.sale_order_count = len(production.procurement_group_id.mrp_production_ids.move_dest_ids.group_id.sale_id) def action_view_sale_orders(self): self.ensure_one() - sale_order_ids = self.move_dest_ids.group_id.sale_id.ids + sale_order_ids = self.procurement_group_id.mrp_production_ids.move_dest_ids.group_id.sale_id.ids action = { 'res_model': 'sale.order', 'type': 'ir.actions.act_window', diff --git a/addons/sale_mrp/models/sale.py b/addons/sale_mrp/models/sale.py index 927037b954875d64cfb5cdc2600ecc42c8b9d7dc..7bb9d14c43658e5712926691195246bc0f251e35 100644 --- a/addons/sale_mrp/models/sale.py +++ b/addons/sale_mrp/models/sale.py @@ -12,14 +12,14 @@ class SaleOrder(models.Model): compute='_compute_mrp_production_count', groups='mrp.group_mrp_user') - @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_count(self): for sale in self: - sale.mrp_production_count = len(sale.procurement_group_id.stock_move_ids.created_production_id) + sale.mrp_production_count = len(sale.procurement_group_id.stock_move_ids.created_production_id.procurement_group_id.mrp_production_ids) def action_view_mrp_production(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', diff --git a/addons/sale_mrp/tests/test_sale_mrp_flow.py b/addons/sale_mrp/tests/test_sale_mrp_flow.py index e06fe685ce368259c035dae0e7e3265086e8606c..527e992faf771fdd6cb1d494437d403fef126819 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_flow.py +++ b/addons/sale_mrp/tests/test_sale_mrp_flow.py @@ -20,7 +20,6 @@ class TestSaleMrpFlow(AccountTestCommon): cls.MrpProduction = cls.env['mrp.production'] cls.Inventory = cls.env['stock.inventory'] cls.InventoryLine = cls.env['stock.inventory.line'] - cls.ProductProduce = cls.env['mrp.product.produce'] cls.ProductCategory = cls.env['product.category'] cls.categ_unit = cls.env.ref('uom.product_uom_categ_unit') @@ -432,15 +431,10 @@ class TestSaleMrpFlow(AccountTestCommon): # produce product D. # ------------------ - produce_form = Form(self.ProductProduce.with_context({ - 'active_id': mnf_product_d.id, - 'active_ids': [mnf_product_d.id], - })) - produce_form.qty_producing = 20 - produce_d = produce_form.save() - # produce_d.on_change_qty() - produce_d.do_produce() - mnf_product_d.post_inventory() + mo_form = Form(mnf_product_d) + mo_form.qty_producing = 20 + mnf_product_d = mo_form.save() + mnf_product_d._post_inventory() # Check state of manufacturing order. self.assertEqual(mnf_product_d.state, 'done', 'Manufacturing order should still be in progress state.') @@ -487,13 +481,10 @@ class TestSaleMrpFlow(AccountTestCommon): # Produce product A. # ------------------ - produce_form = Form(self.ProductProduce.with_context({ - 'active_id': mnf_product_a.id, - 'active_ids': [mnf_product_a.id], - })) - produce_a = produce_form.save() - produce_a.do_produce() - mnf_product_a.post_inventory() + mo_form = Form(mnf_product_a) + mo_form.qty_producing = mo_form.product_qty + mnf_product_a = mo_form.save() + mnf_product_a._post_inventory() # Check state of manufacturing order product A. self.assertEqual(mnf_product_a.state, 'done', 'Manufacturing order should still be in the progress state.') # Check product A avaialble quantity should be 120. diff --git a/addons/sale_mrp/views/mrp_production_views.xml b/addons/sale_mrp/views/mrp_production_views.xml index c1607cb0e203017254cca99b2db6d0b7ec2eb87f..114b532fd0c355187e4113cdad9c25d4c7e7750a 100644 --- a/addons/sale_mrp/views/mrp_production_views.xml +++ b/addons/sale_mrp/views/mrp_production_views.xml @@ -3,6 +3,7 @@ <record id="mrp_production_form_view_sale" model="ir.ui.view"> <field name="name">mrp.production.inherited.form.sale</field> <field name="model">mrp.production</field> + <field name="priority">64</field> <field name="inherit_id" ref="mrp.mrp_production_form_view"/> <field name="arch" type="xml"> <xpath expr="//button[@name='action_view_mrp_production_childs']" position="before"> diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 15ff6b59c2dca0289ed902cc8f59532396ef76b4..8155141c7cadb68a55675a5d4c2a1b6f388790ea 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -202,7 +202,7 @@ class StockMove(models.Model): else: move.is_locked = False - @api.depends('product_id', 'has_tracking') + @api.depends('product_id', 'has_tracking', 'move_line_ids') def _compute_show_details_visible(self): """ According to this field, the button that calls `action_show_details` will be displayed to work on a move from its picking form view, or not. @@ -216,10 +216,12 @@ class StockMove(models.Model): for move in self: if not move.product_id: move.show_details_visible = False + elif len(move.move_line_ids) > 1: + move.show_details_visible = True else: move.show_details_visible = (((consignment_enabled and move.picking_id.picking_type_id.code != 'incoming') or show_details_visible or move.has_tracking != 'none') and - (move.state != 'draft' or (move.picking_id.immediate_transfer and move.state == 'draft')) and + move._show_details_in_draft() and move.picking_id.picking_type_id.show_operations is False) def _compute_show_reserved_availability(self): @@ -280,9 +282,11 @@ class StockMove(models.Model): """ This will return the move lines to consider when applying _quantity_done_compute on a stock.move. In some context, such as MRP, it is necessary to compute quantity_done on filtered sock.move.line.""" self.ensure_one() - return self.move_line_ids or self.move_line_nosuggest_ids + if self.picking_type_id.show_reserved is False: + return self.move_line_nosuggest_ids + return self.move_line_ids - @api.depends('move_line_ids.qty_done', 'move_line_ids.product_uom_id', 'move_line_nosuggest_ids.qty_done') + @api.depends('move_line_ids.qty_done', 'move_line_ids.product_uom_id', 'move_line_nosuggest_ids.qty_done', 'picking_type_id') def _quantity_done_compute(self): """ This field represents the sum of the move lines `qty_done`. It allows the user to know if there is still work to do. @@ -292,28 +296,36 @@ class StockMove(models.Model): field will be used in `_action_done` in order to know if the move will need a backorder or an extra move. """ - move_lines = self.env['stock.move.line'] - for move in self: - move_lines |= move._get_move_lines() + if not any(self._ids): + # onchange + for move in self: + quantity_done = 0 + for move_line in move._get_move_lines(): + quantity_done += move_line.product_uom_id._compute_quantity( + move_line.qty_done, move.product_uom, round=False) + move.quantity_done = quantity_done + else: + # compute + move_lines = self.env['stock.move.line'] + for move in self: + move_lines |= move._get_move_lines() - data = self.env['stock.move.line'].read_group( - [('id', 'in', move_lines.ids)], - ['move_id', 'product_uom_id', 'qty_done'], ['move_id', 'product_uom_id'], - lazy=False - ) + data = self.env['stock.move.line'].read_group( + [('id', 'in', move_lines.ids)], + ['move_id', 'product_uom_id', 'qty_done'], ['move_id', 'product_uom_id'], + lazy=False + ) - rec = defaultdict(list) - for d in data: - rec[d['move_id'][0]] += [(d['product_uom_id'][0], d['qty_done'])] + rec = defaultdict(list) + for d in data: + rec[d['move_id'][0]] += [(d['product_uom_id'][0], d['qty_done'])] - # In case we are in an onchange, move.id is a NewId, not an integer. Therefore, there is no - # match in the rec dictionary. By using move.ids[0] we get the correct integer value. - for move in self: - uom = move.product_uom - move.quantity_done = sum( - self.env['uom.uom'].browse(line_uom_id)._compute_quantity(qty, uom, round=False) - for line_uom_id, qty in rec.get(move.ids[0] if move.ids else move.id, []) - ) + for move in self: + uom = move.product_uom + move.quantity_done = sum( + self.env['uom.uom'].browse(line_uom_id)._compute_quantity(qty, uom, round=False) + for line_uom_id, qty in rec.get(move.ids[0] if move.ids else move.id, []) + ) def _quantity_done_set(self): quantity_done = self[0].quantity_done # any call to create will invalidate `move.quantity_done` @@ -327,7 +339,12 @@ class StockMove(models.Model): elif len(move_lines) == 1: move_lines[0].qty_done = quantity_done else: - raise UserError(_("Cannot set the done quantity from this stock move, work directly with the move lines.")) + # Bypass the error if we're trying to write the same value. + ml_quantity_done = 0 + for move_line in move_lines: + ml_quantity_done += move_line.product_uom_id._compute_quantity(move_line.qty_done, move.product_uom, round=False) + if float_compare(quantity_done, ml_quantity_done, precision_rounding=move.product_uom.rounding) != 0: + raise UserError(_("Cannot set the done quantity from this stock move, work directly with the move lines.")) def _set_product_qty(self): """ The meaning of product_qty field changed lately and is now a functional field computing the quantity @@ -342,10 +359,19 @@ class StockMove(models.Model): and is represented by the aggregated `product_qty` on the linked move lines. If the move is force assigned, the value will be 0. """ - result = {data['move_id'][0]: data['product_qty'] for data in - self.env['stock.move.line'].read_group([('move_id', 'in', self.ids)], ['move_id','product_qty'], ['move_id'])} - for rec in self: - rec.reserved_availability = rec.product_id.uom_id._compute_quantity(result.get(rec.id, 0.0), rec.product_uom, rounding_method='HALF-UP') + if not any(self._ids): + # onchange + for move in self: + reserved_availability = sum(move.move_line_ids.mapped('product_qty')) + move.reserved_availability = move.product_id.uom_id._compute_quantity( + reserved_availability, move.product_uom, rounding_method='HALF-UP') + else: + # compute + result = {data['move_id'][0]: data['product_qty'] for data in + self.env['stock.move.line'].read_group([('move_id', 'in', self.ids)], ['move_id', 'product_qty'], ['move_id'])} + for move in self: + move.reserved_availability = move.product_id.uom_id._compute_quantity( + result.get(move.id, 0.0), move.product_uom, rounding_method='HALF-UP') @api.depends('state', 'product_id', 'product_qty', 'location_id') def _compute_product_availability(self): @@ -989,6 +1015,8 @@ class StockMove(models.Model): to_assign = {} for move in self: + if move.state != 'draft': + continue # if the move is preceeded, then it's waiting (if preceeding move is done, then action_assign has been called already and its state is already available) if move.move_orig_ids: move_waiting |= move @@ -1525,13 +1553,8 @@ class StockMove(models.Model): else: return [(self.picking_id, self.product_id.responsible_id, visited)] - def _set_quantity_done(self, qty): - """ - Set the given quantity as quantity done on the move through the move lines. The method is - able to handle move lines with a different UoM than the move (but honestly, this would be - looking for trouble...). - @param qty: quantity in the UoM of move.product_uom - """ + def _set_quantity_done_prepare_vals(self, qty): + res = [] for ml in self.move_line_ids: ml_qty = ml.product_uom_qty - ml.qty_done if float_compare(ml_qty, 0, precision_rounding=ml.product_uom_id.rounding) <= 0: @@ -1548,7 +1571,7 @@ class StockMove(models.Model): # Assign qty_done and explicitly round to make sure there is no inconsistency between # ml.qty_done and qty. taken_qty = float_round(taken_qty, precision_rounding=ml.product_uom_id.rounding) - ml.qty_done += taken_qty + res.append((1, ml.id, {'qty_done': ml.qty_done + taken_qty})) if ml.product_uom_id != self.product_uom: taken_qty = ml.product_uom_id._compute_quantity(ml_qty, self.product_uom, round=False) qty -= taken_qty @@ -1556,9 +1579,27 @@ class StockMove(models.Model): if float_compare(qty, 0.0, precision_rounding=self.product_uom.rounding) <= 0: break if float_compare(qty, 0.0, precision_rounding=self.product_uom.rounding) > 0: - vals = self._prepare_move_line_vals(quantity=0) - vals['qty_done'] = qty - ml = self.env['stock.move.line'].create(vals) + if self.product_id.tracking != 'serial': + vals = self._prepare_move_line_vals(quantity=0) + vals['qty_done'] = qty + res.append((0, 0, vals)) + else: + uom_qty = self.product_uom._compute_quantity(qty, self.product_id.uom_id) + for i in range(0, int(uom_qty)): + vals = self._prepare_move_line_vals(quantity=0) + vals['qty_done'] = 1 + vals['product_uom_id'] = self.product_id.uom_id.id + res.append((0, 0, vals)) + return res + + def _set_quantity_done(self, qty): + """ + Set the given quantity as quantity done on the move through the move lines. The method is + able to handle move lines with a different UoM than the move (but honestly, this would be + looking for trouble...). + @param qty: quantity in the UoM of move.product_uom + """ + self.move_line_ids = self._set_quantity_done_prepare_vals(qty) def _adjust_procure_method(self): """ This method will try to apply the procure method MTO on some moves if @@ -1610,3 +1651,7 @@ class StockMove(models.Model): mtso_free_qties_by_loc[move.location_id][move.product_id.id] -= needed_qty else: move.procure_method = 'make_to_order' + + def _show_details_in_draft(self): + self.ensure_one() + return self.state != 'draft' or (self.picking_id.immediate_transfer and self.state == 'draft') diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py index 8abf7a9e55f4a85d49fb62dddd2c8d1b5d3dadf7..7e0f88d6b36128161b891f0e01f616aaa6be05bf 100644 --- a/addons/stock/models/stock_move_line.py +++ b/addons/stock/models/stock_move_line.py @@ -148,14 +148,15 @@ class StockMoveLine(models.Model): res['warning'] = {'title': _('Warning'), 'message': message} return res - @api.onchange('qty_done') + @api.onchange('qty_done', 'product_uom_id') def _onchange_qty_done(self): """ When the user is encoding a move 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.qty_done and self.product_id.tracking == 'serial': - if float_compare(self.qty_done, 1.0, precision_rounding=self.product_id.uom_id.rounding) != 0: + qty_done = self.product_uom_id._compute_quantity(self.qty_done, self.product_id.uom_id) + if float_compare(qty_done, 1.0, precision_rounding=self.product_id.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 diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py index cb0583a4101fd09233d4ca6ff541440d080cffce..389ca4428b52e854c64e325014bf13fc2ce87e55 100644 --- a/addons/stock/tests/test_move.py +++ b/addons/stock/tests/test_move.py @@ -4369,6 +4369,7 @@ class StockMove(SavepointCase): 'product_uom': self.uom_unit.id, 'product_uom_qty': 2.0, 'picking_id': picking.id, + 'picking_type_id': self.env.ref('stock.picking_type_out').id, }) picking.action_confirm() picking.action_assign() @@ -4458,6 +4459,7 @@ class StockMove(SavepointCase): 'product_uom': self.uom_unit.id, 'product_uom_qty': 1.0, 'picking_id': picking.id, + 'picking_type_id': self.env.ref('stock.picking_type_out').id, }) move2 = self.env['stock.move'].create({ 'name': 'test_transit_2', @@ -4467,6 +4469,7 @@ class StockMove(SavepointCase): 'product_uom': self.uom_unit.id, 'product_uom_qty': 2.0, 'picking_id': picking.id, + 'picking_type_id': self.env.ref('stock.picking_type_out').id, }) picking.action_confirm() picking.action_assign() diff --git a/odoo/addons/test_main_flows/static/tests/tours/main_flow.js b/odoo/addons/test_main_flows/static/tests/tours/main_flow.js index c3ff0d07c43e203928127f056520c3fb40fe5928..4c7719b816df80143057a05940fd739861c05676 100644 --- a/odoo/addons/test_main_flows/static/tests/tours/main_flow.js +++ b/odoo/addons/test_main_flows/static/tests/tours/main_flow.js @@ -823,17 +823,15 @@ tour.stepUtils.openBuggerMenu("li.breadcrumb-item.active:contains('Manufacturing position: 'bottom', }, ...tour.stepUtils.statusbarButtonsSteps('Check availability', _t("Check availability")), -...tour.stepUtils.statusbarButtonsSteps('Produce', _t("Produce"), "body.o_web_client:not(.oe_wait)"), { - mobile: false, - trigger: ".modal-footer .btn-primary:nth-child(3)", - content: _t('Record Production'), - position: 'bottom', + trigger: '.o_form_button_edit', + content: _t('Edit the production order'), + extra_trigger: 'body.o_web_client:not(.oe_wait)', }, { - mobile: true, - trigger: '.modal-footer .btn-primary[name="do_produce"]:not(.o_invisible_modifier)', - content: _t('Record Production'), - position: 'bottom', + trigger: ".o_field_widget[name=qty_producing]", + content: _t("Produce"), + run: "text 1", + extra_trigger: 'body.o_web_client:not(.oe_wait)', }, ...tour.stepUtils.statusbarButtonsSteps('Mark as Done', _t("Mark as Done"), ".o_statusbar_status .btn.dropdown-toggle:contains('To Close')"), {