diff --git a/mrp_production_request/README.rst b/mrp_production_request/README.rst index e63bc8e24..1fb7a6462 100644 --- a/mrp_production_request/README.rst +++ b/mrp_production_request/README.rst @@ -1,24 +1,54 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 - ====================== MRP Production Request ====================== +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png + :target: https://odoo-community.org/page/development-status + :alt: Mature +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmanufacture-lightgray.png?logo=github + :target: https://github.com/OCA/manufacture/tree/11.0/mrp_production_request + :alt: OCA/manufacture +.. |badge4| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/129/11.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| + This module extends the functionality of Manufacturing to allow you to use Manufacturing Request (MR) as a previous step to Manufacturing Orders (MO). +Some of the benefits you can obtain are: + +* Allow managers to review what is going to be manufactured. +* Better control of manufacturing calendar. +* Manage big requirements splitting them in batches. +* Know your bottleneck component in advance and only schedule what you really + can build. + +**Table of contents** + +.. contents:: + :local: + Configuration ============= -To configure this module to automatically generate Manufacturing Requests -from procurement orders instead of directly create manufacturing orders yo +To configure a product to automatically generate Manufacturing Requests +from procurements instead of directly create manufacturing orders you need to: -#. Go to the products that you want them to trigger manufacturing orders. +#. Go to the products that you want them to trigger manufacturing requests. #. Go to the *Inventory* tab. -#. Check the route *manufacture* and the box *Manufacturing Request*. +#. Check the box of a *manufacture* route and the box of + *Manufacturing Request*. Usage ===== @@ -29,12 +59,12 @@ To use this module, you need to: #. Create a manufacturing request or open a existing one (assigned to you or created from a procurement). #. If you click on *Request approval* button the user assigned as approver - will added to the thread. + will be added to the thread. #. If you are the approver you can either click on *Approve* or *Reject* buttons. -#. Rejecting a MR will cancel associated procurements and propagate this - cancellation. -#. Approving a MR will allow to create manufacturing orders. +#. Rejecting a MR will cancel it and propagate this cancellation to + destination moves. +#. Approving a MR will allow you to create manufacturing orders. #. You can manually set to done a request by clicking in the button *Done*. To create MOs from MRs you have to: @@ -43,7 +73,8 @@ To create MOs from MRs you have to: #. Click on the button *Create Manufacturing Order*. #. In the opened wizard, click on *Compute lines* so you will have a quantity proposed for creating a MO. This quantity is the maximum quantity - you can produce with the current stock available in the source location. + you can produce with the current stock available for the components needed + in the source location. #. Use the proposed quantity or change it and click on *Create MO* at the bottom of the wizard. @@ -51,49 +82,64 @@ To create MOs from MRs you have to: from a MR to MOs. It is in hands of the user to decide when a MR is ended and to set it to *Done* state. -.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas - :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/129/10.0 - Known issues / Roadmap ====================== * Take into account workstations. * Take into account consumable products. +Changelog +========= + +11.0.1.0.0 (2018-09-13) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [MIG] Migration to v11. Start of the history. + Bug Tracker =========== -Bugs are tracked on `GitHub Issues -`_. In case of trouble, please -check there if your issue has already been reported. If you spotted it first, -help us smash it by providing detailed and welcomed feedback. +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed feedback. + +Do not contact contributors directly about support or help with technical issues. Credits ======= -Images ------- +Authors +~~~~~~~ -* Odoo Community Association: `Icon `_. +* Eficent Contributors ------------- +~~~~~~~~~~~~ * Lois Rilo Antelo * Jordi Ballester -Maintainer ----------- +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. .. image:: https://odoo-community.org/logo.png :alt: Odoo Community Association :target: https://odoo-community.org -This module is maintained by the OCA. - OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -To contribute to this module, please visit https://odoo-community.org. +.. |maintainer-lreficent| image:: https://github.com/lreficent.png?size=40px + :target: https://github.com/lreficent + :alt: lreficent + +Current `maintainer `_: + +|maintainer-lreficent| + +This module is part of the `OCA/manufacture `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mrp_production_request/__init__.py b/mrp_production_request/__init__.py index 84830dc8e..aee8895e7 100644 --- a/mrp_production_request/__init__.py +++ b/mrp_production_request/__init__.py @@ -1,5 +1,2 @@ -# -*- coding: utf-8 -*- -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - from . import models from . import wizards diff --git a/mrp_production_request/__manifest__.py b/mrp_production_request/__manifest__.py index 8ada6c881..308a562fd 100644 --- a/mrp_production_request/__manifest__.py +++ b/mrp_production_request/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Eficent Business and IT Consulting Services S.L. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { @@ -6,7 +5,9 @@ "summary": "Allows you to use Manufacturing Request as a previous " "step to Manufacturing Orders for better manufacture " "planification.", - "version": "10.0.1.1.0", + "version": "11.0.1.0.0", + "development_status": "Mature", + "maintainers": ['lreficent'], "category": "Manufacturing", "website": "https://github.com/OCA/manufacture", "author": "Eficent," @@ -22,7 +23,6 @@ "wizards/mrp_production_request_create_mo_view.xml", "views/mrp_production_request_view.xml", "views/product_template_view.xml", - "views/procurement_order_view.xml", "views/mrp_production_view.xml", ], } diff --git a/mrp_production_request/migrations/11.0.1.0.0/post-migration.py b/mrp_production_request/migrations/11.0.1.0.0/post-migration.py new file mode 100644 index 000000000..83a115078 --- /dev/null +++ b/mrp_production_request/migrations/11.0.1.0.0/post-migration.py @@ -0,0 +1,19 @@ +# Copyright 2018 Eficent Business and IT Consulting Services S.L. +# (http://www.eficent.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +def migrate(cr, version): + """Copy `move_dest_id` from the old procurement orders to the new field + `move_dest_ids` in the Manufacturing Request.""" + cr.execute(""" + SELECT move_dest_id, mrp_production_request_id + FROM procurement_order + WHERE mrp_production_request_id is not null;""") + for move_dest, mrp_request in cr.fetchall(): + if move_dest: + cr.execute(""" + UPDATE stock_move + SET created_mrp_production_request_id = %s + WHERE id = %s + """, (mrp_request, move_dest,)) diff --git a/mrp_production_request/models/__init__.py b/mrp_production_request/models/__init__.py index ffa417603..d450fbd94 100644 --- a/mrp_production_request/models/__init__.py +++ b/mrp_production_request/models/__init__.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - from . import mrp_production_request from . import mrp_production -from . import procurement +from . import procurement_rule from . import product from . import stock_move +from . import stock_warehouse_orderpoint diff --git a/mrp_production_request/models/mrp_production.py b/mrp_production_request/models/mrp_production.py index f844a19ba..99048b638 100644 --- a/mrp_production_request/models/mrp_production.py +++ b/mrp_production_request/models/mrp_production.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Eficent Business and IT Consulting Services S.L. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -13,7 +12,16 @@ class MrpProduction(models.Model): string="Manufacturing Request", copy=False, readonly=True) def _generate_finished_moves(self): + """`move_dest_ids` is a One2many fields in mrp.production, thus we + cannot indicate the same destination move in several MOs (which most + probably would be the case with MRs). + Storing them on the MR and writing them on the finished moves as it + would happen if they were present in the MO, is the best workaround + without changing the standard data model.""" move = super(MrpProduction, self)._generate_finished_moves() - mr_proc = self.mrp_production_request_id.procurement_id - if mr_proc and mr_proc.move_dest_id: - move.write({"move_dest_id": mr_proc.move_dest_id.id}) + request = self.mrp_production_request_id + if request and request.move_dest_ids: + move.write({ + 'move_dest_ids': [(4, x.id) for x in request.move_dest_ids], + }) + return move diff --git a/mrp_production_request/models/mrp_production_request.py b/mrp_production_request/models/mrp_production_request.py index 93f599de7..5f7a62cdd 100644 --- a/mrp_production_request/models/mrp_production_request.py +++ b/mrp_production_request/models/mrp_production_request.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017-18 Eficent Business and IT Consulting Services S.L. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -11,7 +10,7 @@ class MrpProductionRequest(models.Model): _name = "mrp.production.request" _description = "Manufacturing Request" _inherit = "mail.thread" - _order = "id DESC" + _order = "date_planned_start desc, id desc" @api.model def _company_get(self): @@ -22,59 +21,6 @@ class MrpProductionRequest(models.Model): def _get_default_requested_by(self): return self.env.user - @api.multi - def _subscribe_assigned_user(self, vals): - self.ensure_one() - if vals.get('assigned_to'): - self.message_subscribe_users(user_ids=[self.assigned_to.id]) - - @api.model - def _create_sequence(self, vals): - if not vals.get('name') or vals.get('name') == '/': - vals['name'] = self.env['ir.sequence'].next_by_code( - 'mrp.production.request') or '/' - return vals - - @api.model - def create(self, vals): - """Add sequence if name is not defined and subscribe to the thread - the user assigned to the request.""" - vals = self._create_sequence(vals) - res = super(MrpProductionRequest, self).create(vals) - res._subscribe_assigned_user(vals) - return res - - @api.multi - def write(self, vals): - res = super(MrpProductionRequest, self).write(vals) - for request in self: - request._subscribe_assigned_user(vals) - return res - - @api.onchange('product_id') - def _onchange_product_id(self): - self.product_uom_id = self.product_id.uom_id - self.bom_id = self.env['mrp.bom']._bom_find( - product=self.product_id, company_id=self.company_id.id, - picking_type=self.picking_type_id) - - @api.model - def _get_mo_valid_states(self): - return ['draft', 'confirmed', 'ready', 'in_production', 'done'] - - @api.multi - @api.depends('mrp_production_ids', 'mrp_production_ids.state', 'state') - def _compute_manufactured_qty(self): - valid_states = self._get_mo_valid_states() - for req in self: - done_mo = req.mrp_production_ids.filtered( - lambda mo: mo.state in 'done').mapped('product_qty') - req.done_qty = sum(done_mo) - valid_mo = req.mrp_production_ids.filtered( - lambda mo: mo.state in valid_states).mapped('product_qty') - req.manufactured_qty = sum(valid_mo) - req.pending_qty = max(req.product_qty - req.manufactured_qty, 0.0) - name = fields.Char( default="/", required=True, readonly=True, states={'draft': [('readonly', False)]}) @@ -89,7 +35,10 @@ class MrpProductionRequest(models.Model): assigned_to = fields.Many2one( comodel_name='res.users', string='Approver', track_visibility='onchange', - readonly=True, states={'draft': [('readonly', False)]}) + readonly=True, states={'draft': [('readonly', False)]}, + domain=lambda self: [('groups_id', 'in', self.env.ref( + 'mrp_production_request.' + 'group_mrp_production_request_manager').id)]) description = fields.Text('Description') date_planned_start = fields.Datetime( 'Deadline Start', copy=False, default=fields.Datetime.now, @@ -105,6 +54,10 @@ class MrpProductionRequest(models.Model): mrp_production_ids = fields.One2many( comodel_name="mrp.production", string="Manufacturing Orders", inverse_name="mrp_production_request_id", readonly=True) + mrp_production_count = fields.Integer( + compute="_compute_mrp_production_count", + string="MO's Count", + ) state = fields.Selection( selection=[("draft", "Draft"), ("to_approve", "To Be Approved"), @@ -117,21 +70,11 @@ class MrpProductionRequest(models.Model): string='Procurement Group', comodel_name='procurement.group', copy=False) - procurement_ids = fields.One2many( - string='Related Procurements', - comodel_name='procurement.order', - inverse_name='mrp_production_request_id') propagate = fields.Boolean( 'Propagate cancel and split', help='If checked, when the previous move of the move ' '(which was generated by a next procurement) is cancelled ' 'or split, the move generated by this move will too') - procurement_id = fields.Many2one( - comodel_name="procurement.order", string="Procurement Order", - readonly=True) - procurement_state = fields.Selection( - related='procurement_id.state', - store=True, readonly=True, string="Procurement State") product_id = fields.Many2one( comodel_name="product.product", string="Product", required=True, domain=[('type', 'in', ['product', 'consu'])], @@ -142,7 +85,7 @@ class MrpProductionRequest(models.Model): related='product_id.product_tmpl_id') product_qty = fields.Float( string="Required Quantity", required=True, track_visibility='onchange', - digits=dp.get_precision('Product Unit of Measure'), + digits=dp.get_precision('Product Unit of Measure'), default=1.0, readonly=True, states={'draft': [('readonly', False)]}) product_uom_id = fields.Many2one( comodel_name='product.uom', string='Unit of Measure', @@ -151,16 +94,16 @@ class MrpProductionRequest(models.Model): category_uom_id = fields.Many2one(related="product_uom_id.category_id") manufactured_qty = fields.Float( string="Quantity in Manufacturing Orders", - compute=_compute_manufactured_qty, store=True, readonly=True, + compute="_compute_manufactured_qty", store=True, readonly=True, digits=dp.get_precision('Product Unit of Measure'), help="Sum of the quantities in Manufacturing Orders (in any state).") done_qty = fields.Float( string="Quantity Done", store=True, readonly=True, - compute=_compute_manufactured_qty, + compute="_compute_manufactured_qty", digits=dp.get_precision('Product Unit of Measure'), help="Sum of the quantities in all done Manufacturing Orders.") pending_qty = fields.Float( - string="Pending Quantity", compute=_compute_manufactured_qty, + string="Pending Quantity", compute="_compute_manufactured_qty", store=True, digits=dp.get_precision('Product Unit of Measure'), readonly=True, help="Quantity pending to add to Manufacturing Orders " @@ -191,6 +134,76 @@ class MrpProductionRequest(models.Model): default=lambda self: self.env['stock.picking.type'].browse( self.env['mrp.production']._get_default_picking_type()), required=True, readonly=True, states={'draft': [('readonly', False)]}) + move_dest_ids = fields.One2many( + comodel_name='stock.move', + inverse_name='created_mrp_production_request_id', + string="Stock Movements of Produced Goods") + orderpoint_id = fields.Many2one( + comodel_name='stock.warehouse.orderpoint', + string='Orderpoint') + + _sql_constraints = [ + ('name_uniq', 'unique(name, company_id)', + 'Reference must be unique per Company!'), + ] + + @api.model + def _get_mo_valid_states(self): + return ['planned', 'confirmed', 'progress', 'done'] + + @api.multi + @api.depends('mrp_production_ids', 'mrp_production_ids.state', 'state') + def _compute_manufactured_qty(self): + valid_states = self._get_mo_valid_states() + for req in self: + done_mo = req.mrp_production_ids.filtered( + lambda mo: mo.state in 'done').mapped('product_qty') + req.done_qty = sum(done_mo) + valid_mo = req.mrp_production_ids.filtered( + lambda mo: mo.state in valid_states).mapped('product_qty') + req.manufactured_qty = sum(valid_mo) + req.pending_qty = max(req.product_qty - req.manufactured_qty, 0.0) + + @api.multi + def _compute_mrp_production_count(self): + for rec in self: + rec.mrp_production_count = len(rec.mrp_production_ids) + + @api.onchange('product_id') + def _onchange_product_id(self): + self.product_uom_id = self.product_id.uom_id + self.bom_id = self.env['mrp.bom']._bom_find( + product=self.product_id, company_id=self.company_id.id, + picking_type=self.picking_type_id) + + @api.multi + def _subscribe_assigned_user(self, vals): + self.ensure_one() + if vals.get('assigned_to'): + self.message_subscribe_users(user_ids=[self.assigned_to.id]) + + @api.model + def _create_sequence(self, vals): + if not vals.get('name') or vals.get('name') == '/': + vals['name'] = self.env['ir.sequence'].next_by_code( + 'mrp.production.request') or '/' + return vals + + @api.model + def create(self, vals): + """Add sequence if name is not defined and subscribe to the thread + the user assigned to the request.""" + vals = self._create_sequence(vals) + res = super(MrpProductionRequest, self).create(vals) + res._subscribe_assigned_user(vals) + return res + + @api.multi + def write(self, vals): + res = super(MrpProductionRequest, self).write(vals) + for request in self: + request._subscribe_assigned_user(vals) + return res @api.multi def button_to_approve(self): @@ -205,8 +218,6 @@ class MrpProductionRequest(models.Model): @api.multi def button_done(self): self.write({'state': 'done'}) - if self.mapped('procurement_id'): - self.mapped('procurement_id').write({'state': 'done'}) return True @api.multi @@ -216,11 +227,6 @@ class MrpProductionRequest(models.Model): raise UserError( _("You cannot reset a manufacturing request if the related " "manufacturing orders are not cancelled.")) - if any([s in ['done', 'cancel'] for s in self.mapped( - 'procurement_id.state')]): - raise UserError( - _("You cannot reset a manufacturing request related to " - "done or cancelled procurement orders.")) @api.multi def button_draft(self): @@ -230,8 +236,7 @@ class MrpProductionRequest(models.Model): @api.multi def _check_cancel_allowed(self): - if any([s == 'done' for s in self.mapped( - 'procurement_id.state')]): + if any([s == 'done' for s in self.mapped('state')]): raise UserError( _('You cannot reject a manufacturing request related to ' 'done procurement orders.')) @@ -240,15 +245,21 @@ class MrpProductionRequest(models.Model): def button_cancel(self): self._check_cancel_allowed() self.write({'state': 'cancel'}) - self.mapped('procurement_id').with_context( - from_mrp_production_request=True).cancel() - if not self.env.context.get('cancel_procurement'): - procurements = self.mapped('procurement_id') - procurements.filtered(lambda r: r.state not in ( - 'cancel', 'exception') and not r.rule_id.propagate).write( - {'state': 'exception'}) - moves = procurements.filtered( - lambda r: r.rule_id.propagate).mapped( - 'move_dest_id') - moves.filtered(lambda r: r.state != 'cancel').action_cancel() + self.mapped('move_dest_ids').filtered( + lambda r: r.state != 'cancel')._action_cancel() return True + + @api.multi + def action_view_mrp_productions(self): + action = self.env.ref('mrp.mrp_production_action') + result = action.read()[0] + result['context'] = {} + mos = self.mapped('mrp_production_ids') + # choose the view_mode accordingly + if len(mos) != 1: + result['domain'] = [('id', 'in', mos.ids)] + elif len(mos) == 1: + form = self.env.ref('mrp.mrp_production_form_view', False) + result['views'] = [(form and form.id or False, 'form')] + result['res_id'] = mos[0].id + return result diff --git a/mrp_production_request/models/procurement.py b/mrp_production_request/models/procurement.py deleted file mode 100644 index b1a4114d6..000000000 --- a/mrp_production_request/models/procurement.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2017 Eficent Business and IT Consulting Services S.L. -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import api, fields, models, _ - - -class ProcurementOrder(models.Model): - _inherit = "procurement.order" - - mrp_production_request_id = fields.Many2one( - comodel_name="mrp.production.request", string="Manufacturing Request", - copy=False) - - @api.multi - def _prepare_mrp_production_request(self): - self.ensure_one() - data = self._prepare_mo_vals(self._get_matching_bom()) - data['procurement_id'] = self.id - data['state'] = 'to_approve' - return data - - @api.multi - def _run(self): - self.ensure_one() - if (self.rule_id and - self.rule_id.action == 'manufacture' and - self.product_id.mrp_production_request): - if not self._get_matching_bom(): - self.message_post( - body=_("No BoM exists for this product!")) - return False - if not self.mrp_production_request_id: - request_data = self._prepare_mrp_production_request() - req = self.env['mrp.production.request'].create(request_data) - self.message_post(body=_( - "Manufacturing Request created")) - self.mrp_production_request_id = req.id - return True - return super(ProcurementOrder, self)._run() - - @api.multi - def propagate_cancels(self): - result = super(ProcurementOrder, self).propagate_cancels() - for procurement in self: - mrp_production_requests = \ - self.env['mrp.production.request'].sudo().search([ - ('procurement_id', '=', procurement.id)]) - if mrp_production_requests and not self.env.context.get( - 'from_mrp_production_request'): - mrp_production_requests.sudo().button_cancel() - for mr in mrp_production_requests: - mr.sudo().message_post( - body=_("Related procurement has been cancelled.")) - procurement.write({'mrp_production_request_id': None}) - return result diff --git a/mrp_production_request/models/procurement_rule.py b/mrp_production_request/models/procurement_rule.py new file mode 100644 index 000000000..a02d818c9 --- /dev/null +++ b/mrp_production_request/models/procurement_rule.py @@ -0,0 +1,80 @@ +# Copyright 2018 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models, _ +from odoo.exceptions import UserError + + +class ProcurementOrder(models.Model): + _inherit = "procurement.rule" + + @api.multi + def _prepare_mrp_production_request( + self, product_id, product_qty, product_uom, location_id, name, + origin, values, bom): + self.ensure_one() + data = self._prepare_mo_vals( + product_id, product_qty, product_uom, location_id, name, + origin, values, bom) + data['state'] = 'to_approve' + orderpoint = values.get('orderpoint_id') + if orderpoint: + data['orderpoint_id'] = orderpoint.id + return data + + @api.multi + def _need_production_request(self, product_id): + return self.action == 'manufacture' \ + and product_id.mrp_production_request + + @api.multi + def _run_production_request(self, product_id, product_qty, product_uom, + location_id, name, origin, values): + """Trying to handle this as much similar as possible to Odoo + production orders. See `_run_manufacture` in Odoo standard.""" + request_obj = self.env['mrp.production.request'] + request_obj_sudo = request_obj.sudo().with_context( + force_company=values['company_id'].id) + bom = self._get_matching_bom(product_id, values) + if not bom: + raise UserError(_( + 'There is no Bill of Material found for the product %s. ' + 'Please define a Bill of Material for this product.') % ( + product_id.display_name,)) + + # create the MR as SUPERUSER because the current user may not + # have the rights to do it (mto product launched by a sale for example) + request = request_obj_sudo.create( + self._prepare_mrp_production_request( + product_id, product_qty, product_uom, location_id, name, + origin, values, bom)) + origin_production = values.get('move_dest_ids') and \ + values['move_dest_ids'][0].raw_material_production_id or False + orderpoint = values.get('orderpoint_id') + if orderpoint: + request.message_post_with_view( + 'mail.message_origin_link', + values={'self': request, + 'origin': orderpoint}, + subtype_id=self.env.ref('mail.mt_note').id, + ) + if origin_production: + request.message_post_with_view( + 'mail.message_origin_link', + values={'self': request, + 'origin': origin_production}, + subtype_id=self.env.ref('mail.mt_note').id, + ) + return True + + @api.multi + def _run_manufacture(self, product_id, product_qty, product_uom, + location_id, name, origin, values): + if self._need_production_request(product_id): + return self._run_production_request( + product_id, product_qty, product_uom, + location_id, name, origin, values) + + return super()._run_manufacture( + product_id, product_qty, product_uom, location_id, name, + origin, values) diff --git a/mrp_production_request/models/product.py b/mrp_production_request/models/product.py index d051954c9..047899362 100644 --- a/mrp_production_request/models/product.py +++ b/mrp_production_request/models/product.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Eficent Business and IT Consulting Services S.L. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). diff --git a/mrp_production_request/models/stock_move.py b/mrp_production_request/models/stock_move.py index c963ddc4d..56315628f 100644 --- a/mrp_production_request/models/stock_move.py +++ b/mrp_production_request/models/stock_move.py @@ -1,13 +1,17 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Eficent Business and IT Consulting Services S.L. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, models +from odoo import api, fields, models class StockMove(models.Model): _inherit = "stock.move" + created_mrp_production_request_id = fields.Many2one( + comodel_name='mrp.production.request', + string='Created Production Request', + ) + @api.model def create(self, vals): if 'production_id' in vals: diff --git a/mrp_production_request/models/stock_warehouse_orderpoint.py b/mrp_production_request/models/stock_warehouse_orderpoint.py new file mode 100644 index 000000000..4bc50cac2 --- /dev/null +++ b/mrp_production_request/models/stock_warehouse_orderpoint.py @@ -0,0 +1,19 @@ +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class Orderpoint(models.Model): + _inherit = "stock.warehouse.orderpoint" + + def _quantity_in_progress(self): + res = super(Orderpoint, self)._quantity_in_progress() + mrp_requests = self.env['mrp.production.request'].search([ + ('state', 'not in', ('done', 'cancel')), + ('orderpoint_id', 'in', self.ids), + ]) + for rec in mrp_requests: + res[rec.orderpoint_id.id] += rec.product_uom_id._compute_quantity( + rec.pending_qty, rec.orderpoint_id.product_uom, round=False) + return res diff --git a/mrp_production_request/readme/CONFIGURE.rst b/mrp_production_request/readme/CONFIGURE.rst new file mode 100644 index 000000000..7a23cf78a --- /dev/null +++ b/mrp_production_request/readme/CONFIGURE.rst @@ -0,0 +1,8 @@ +To configure a product to automatically generate Manufacturing Requests +from procurements instead of directly create manufacturing orders you +need to: + +#. Go to the products that you want them to trigger manufacturing requests. +#. Go to the *Inventory* tab. +#. Check the box of a *manufacture* route and the box of + *Manufacturing Request*. diff --git a/mrp_production_request/readme/CONTRIBUTORS.rst b/mrp_production_request/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..f2e2d56d4 --- /dev/null +++ b/mrp_production_request/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Lois Rilo Antelo +* Jordi Ballester diff --git a/mrp_production_request/readme/DESCRIPTION.rst b/mrp_production_request/readme/DESCRIPTION.rst new file mode 100644 index 000000000..99add7589 --- /dev/null +++ b/mrp_production_request/readme/DESCRIPTION.rst @@ -0,0 +1,10 @@ +This module extends the functionality of Manufacturing to allow you to use +Manufacturing Request (MR) as a previous step to Manufacturing Orders (MO). + +Some of the benefits you can obtain are: + +* Allow managers to review what is going to be manufactured. +* Better control of manufacturing calendar. +* Manage big requirements splitting them in batches. +* Know your bottleneck component in advance and only schedule what you really + can build. diff --git a/mrp_production_request/readme/HISTORY.rst b/mrp_production_request/readme/HISTORY.rst new file mode 100644 index 000000000..04b41e032 --- /dev/null +++ b/mrp_production_request/readme/HISTORY.rst @@ -0,0 +1,4 @@ +11.0.1.0.0 (2018-09-13) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [MIG] Migration to v11. Start of the history. diff --git a/mrp_production_request/readme/ROADMAP.rst b/mrp_production_request/readme/ROADMAP.rst new file mode 100644 index 000000000..e4ff9a6f6 --- /dev/null +++ b/mrp_production_request/readme/ROADMAP.rst @@ -0,0 +1,2 @@ +* Take into account workstations. +* Take into account consumable products. diff --git a/mrp_production_request/readme/USAGE.rst b/mrp_production_request/readme/USAGE.rst new file mode 100644 index 000000000..5a620fe35 --- /dev/null +++ b/mrp_production_request/readme/USAGE.rst @@ -0,0 +1,28 @@ +To use this module, you need to: + +#. Go to *Manufacturing > Manufacturing Requests*. +#. Create a manufacturing request or open a existing one (assigned to you or + created from a procurement). +#. If you click on *Request approval* button the user assigned as approver + will be added to the thread. +#. If you are the approver you can either click on *Approve* or *Reject* + buttons. +#. Rejecting a MR will cancel it and propagate this cancellation to + destination moves. +#. Approving a MR will allow you to create manufacturing orders. +#. You can manually set to done a request by clicking in the button *Done*. + +To create MOs from MRs you have to: + +#. Go to approved manufacturing request. +#. Click on the button *Create Manufacturing Order*. +#. In the opened wizard, click on *Compute lines* so you will have a + quantity proposed for creating a MO. This quantity is the maximum quantity + you can produce with the current stock available for the components needed + in the source location. +#. Use the proposed quantity or change it and click on *Create MO* at the + bottom of the wizard. + +**NOTE:** This module does not restrict the quantity that can be converted +from a MR to MOs. It is in hands of the user to decide when a MR is ended and +to set it to *Done* state. diff --git a/mrp_production_request/tests/__init__.py b/mrp_production_request/tests/__init__.py index f544ff207..b2fda7ec9 100644 --- a/mrp_production_request/tests/__init__.py +++ b/mrp_production_request/tests/__init__.py @@ -1,4 +1 @@ -# -*- coding: utf-8 -*- -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - from . import test_mrp_production_request diff --git a/mrp_production_request/tests/test_mrp_production_request.py b/mrp_production_request/tests/test_mrp_production_request.py index 3628888d5..6bf67d50c 100644 --- a/mrp_production_request/tests/test_mrp_production_request.py +++ b/mrp_production_request/tests/test_mrp_production_request.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# Copyright 2017-18 Eficent Business and IT Consulting Services S.L. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo.tests.common import TransactionCase @@ -14,38 +13,90 @@ class TestMrpProductionRequest(TransactionCase): self.request_model = self.env['mrp.production.request'] self.wiz_model = self.env['mrp.production.request.create.mo'] self.bom_model = self.env['mrp.bom'] + self.group_model = self.env['procurement.group'] + self.product_model = self.env['product.product'] + self.bom_model = self.env['mrp.bom'] + self.boml_model = self.env['mrp.bom.line'] + self.warehouse = self.env.ref('stock.warehouse0') + self.stock_loc = self.env.ref('stock.stock_location_stock') + route_manuf = self.env.ref('mrp.route_warehouse0_manufacture') + + # Prepare Products: self.product = self.env.ref('product.product_product_3') self.product.mrp_production_request = True + self.product.route_ids = [(4, route_manuf.id, 0)] - self.test_product = self.env['product.product'].create({ + self.product_no_bom = self.product_model.create({ 'name': 'Test Product without BoM', 'mrp_production_request': True, + 'route_ids': [(6, 0, route_manuf.ids)], + }) + self.product_orderpoint = self.product_model.create({ + 'name': 'Test Product for orderpoint', + 'mrp_production_request': True, + 'route_ids': [(6, 0, route_manuf.ids)], + }) + product_component = self.product_model.create({ + 'name': 'Test component', + 'mrp_production_request': True, + 'route_ids': [(6, 0, route_manuf.ids)], }) + # Create Bill of Materials: + self.test_bom_1 = self.bom_model.create({ + 'product_id': self.product_orderpoint.id, + 'product_tmpl_id': self.product_orderpoint.product_tmpl_id.id, + 'product_uom_id': self.product_orderpoint.uom_id.id, + 'product_qty': 1.0, + 'type': 'normal', + }) + self.boml_model.create({ + 'bom_id': self.test_bom_1.id, + 'product_id': product_component.id, + 'product_qty': 1.0, + }) + + # Create Orderpoint: + self.orderpoint = self.env['stock.warehouse.orderpoint'].create({ + 'warehouse_id': self.warehouse.id, + 'location_id': self.warehouse.lot_stock_id.id, + 'product_id': self.product_orderpoint.id, + 'product_min_qty': 10.0, + 'product_max_qty': 50.0, + 'product_uom': self.product_orderpoint.uom_id.id, + }) + + # Create Procurement Group: + self.test_group = self.group_model.create({ + 'name': 'TEST', + }) + + # Create User: self.test_user = self.env['res.users'].create({ 'name': 'John', 'login': 'test', }) - def create_procurement(self, name, product): + def procure(self, group, product, qty=4.0,): values = { - 'name': name, 'date_planned': fields.Datetime.now(), - 'product_id': product.id, - 'product_qty': 4.0, - 'product_uom': product.uom_id.id, - 'warehouse_id': self.env.ref('stock.warehouse0').id, - 'location_id': self.env.ref('stock.stock_location_stock').id, - 'route_ids': [ - (4, self.env.ref('mrp.route_warehouse0_manufacture').id, 0)], + 'group_id': group, } - return self.env['procurement.order'].create(values) + self.group_model.run( + product, qty, product.uom_id, self.stock_loc, + group.name, group.name, values, + ) + return True - def test_manufacture_request(self): + def test_01_manufacture_request(self): """Tests manufacture request workflow.""" - proc = self.create_procurement('TEST/01', self.product) - request = proc.mrp_production_request_id + self.procure(self.test_group, self.product) + request = self.request_model.search([ + ('product_id', '=', self.product.id), + ('procurement_group_id', '=', self.test_group.id), + ]) + self.assertEqual(len(request), 1) request.button_to_approve() request.button_draft() request.button_to_approve() @@ -61,32 +112,7 @@ class TestMrpProductionRequest(TransactionCase): self.assertEqual(request.pending_qty, 0.0) request.button_done() - def test_cancellation_from_request(self): - """Tests propagation of cancel to procurements from manufacturing - request and not from manufacturing order.""" - proc = self.create_procurement('TEST/02', self.product) - request = proc.mrp_production_request_id - wiz = self.wiz_model.with_context(active_ids=request.ids).create({}) - wiz.mo_qty = 4.0 - wiz.create_mo() - with self.assertRaises(UserError): - request.button_draft() - mo = self.production_model.search([ - ('mrp_production_request_id', '=', request.id)]) - mo.action_cancel() - self.assertNotEqual(proc.state, 'cancel') - request.button_cancel() - self.assertEqual(proc.state, 'cancel') - - def test_cancellation_from_proc(self): - """Tests cancelation from procurement.""" - proc = self.create_procurement('TEST/03', self.product) - request = proc.mrp_production_request_id - self.assertNotEqual(request.state, 'cancel') - proc.cancel() - self.assertEqual(request.state, 'cancel') - - def test_assignation(self): + def test_02_assignation(self): """Tests assignation of manufacturing requests.""" randon_bom_id = self.bom_model.search([], limit=1).id request = self.request_model.create({ @@ -105,15 +131,27 @@ class TestMrpProductionRequest(TransactionCase): self.assertTrue(request.message_follower_ids, "Followers not added correctly.") - def test_raise_errors(self): + def test_03_substract_qty_from_orderpoint(self): + """Quantity in Manufacturing Requests should be considered by + orderpoints.""" + request = self.request_model.search([ + ('product_id', '=', self.product_orderpoint.id), + ]) + self.assertFalse(request) + self.env['procurement.group'].run_scheduler() + request = self.request_model.search([ + ('product_id', '=', self.product_orderpoint.id), + ]) + self.assertEqual(len(request), 1) + # Running again the scheduler should not generate a new MR. + self.env['procurement.group'].run_scheduler() + request = self.request_model.search([ + ('product_id', '=', self.product_orderpoint.id), + ]) + self.assertEqual(len(request), 1) + + def test_04_raise_errors(self): """Tests user errors raising properly.""" - proc_no_bom = self.create_procurement('TEST/05', self.test_product) - self.assertEqual(proc_no_bom.state, 'exception') - proc = self.create_procurement('TEST/05', self.product) - request = proc.mrp_production_request_id - request.button_to_approve() - proc.write({'state': 'done'}) with self.assertRaises(UserError): - request.button_cancel() - with self.assertRaises(UserError): - request.button_draft() + # No Bill of Materials: + self.procure(self.test_group, self.product_no_bom) diff --git a/mrp_production_request/views/mrp_production_request_view.xml b/mrp_production_request/views/mrp_production_request_view.xml index 3d268f22a..b1320542a 100644 --- a/mrp_production_request/views/mrp_production_request_view.xml +++ b/mrp_production_request/views/mrp_production_request_view.xml @@ -1,5 +1,5 @@ - @@ -33,6 +33,16 @@ statusbar_visible="draft,approved,done"/> +
+ +
+