[11.0][MIG] mrp_production_request

This commit is contained in:
Lois Rilo
2018-09-13 15:55:07 +02:00
committed by Chandresh Thakkar
parent 4269e2771c
commit 423183102f
25 changed files with 490 additions and 299 deletions

View File

@@ -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
<https://github.com/OCA/manufacture/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 <https://github.com/OCA/manufacture/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 <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
* Eficent
Contributors
------------
~~~~~~~~~~~~
* Lois Rilo Antelo <lois.rilo@eficent.com>
* Jordi Ballester <jordi.ballester@eficent.com>
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 <https://odoo-community.org/page/maintainer-role>`_:
|maintainer-lreficent|
This module is part of the `OCA/manufacture <https://github.com/OCA/manufacture/tree/11.0/mrp_production_request>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

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

View File

@@ -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",
],
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
* Lois Rilo Antelo <lois.rilo@eficent.com>
* Jordi Ballester <jordi.ballester@eficent.com>

View File

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

View File

@@ -0,0 +1,4 @@
11.0.1.0.0 (2018-09-13)
~~~~~~~~~~~~~~~~~~~~~~~
* [MIG] Migration to v11. Start of the history.

View File

@@ -0,0 +1,2 @@
* Take into account workstations.
* Take into account consumable products.

View File

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

View File

@@ -1,4 +1 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_mrp_production_request

View File

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

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
<!-- Copyright 2017-18 Eficent Business and IT Consulting Services S.L.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl-3.0) -->
<odoo>
@@ -33,6 +33,16 @@
statusbar_visible="draft,approved,done"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button type="object" name="action_view_mrp_productions"
class="oe_stat_button"
icon="fa-wrench"
attrs="{'invisible': [('mrp_production_count', '=', 0)]}">
<field name="mrp_production_count" widget="statinfo"
string="MOs"/>
</button>
</div>
<label string="Manufacturing Request"/>
<h1>
<field name="name"/>
@@ -61,8 +71,9 @@
<field name="requested_by"/>
<field name="assigned_to"/>
</group>
<group>
<field name="description"/>
<group name="dates">
<field name="date_planned_start"/>
<field name="date_planned_finished" invisible="1"/>
</group>
</group>
<notebook>
@@ -71,20 +82,17 @@
</page>
<page name="extra" string="Extra information">
<group>
<group>
<field name="procurement_id"/>
<field name="procurement_state"/>
<field name="origin"/>
</group>
<group>
<field name="location_src_id"/>
<field name="location_dest_id"/>
<field name="picking_type_id"/>
<field name="date_planned_start"/>
<field name="date_planned_finished" invisible="1"/>/>
</group>
</group>
</group>
<group>
<field name="origin"/>
<field name="description"/>
</group>
</group>
</page>
</notebook>
</sheet>
@@ -142,6 +150,10 @@
string="Pending Qty" name="pending"
help="Request with pending quantity"/>
<separator/>
<filter string="To Do" name="todo"
domain="[('state','in',('draft', 'to_approve','approved'))]"
help="Manufacturing Requests not done or cancelled."/>
<separator/>
<filter name="state_draft" string="Draft"
domain="[('state','=','draft')]"
help="Request is to be approved"/>
@@ -157,11 +169,10 @@
<filter name="state_done" string="Done"
domain="[('state','=','done')]"
help="Request is done"/>
<separator/>
<filter string="Unread Messages"
name="message_needaction"
domain="[('message_needaction','=',True)]"/>
<filter domain="[('requested_by','=', uid)]"
help="My requests"/>
<!--Group by:-->
<filter string="Requested by" icon="terp-personal"
domain="[]"
@@ -182,7 +193,7 @@
<field name="res_model">mrp.production.request</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context">{"search_default_pending":1}</field>
<field name="context">{"search_default_todo":1}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to start a new manufacturing request process.
@@ -204,22 +215,10 @@
<record id="action_server_mrp_production_request_refresh"
model="ir.actions.server">
<field name="name">Refresh Quantities</field>
<field name="condition">True</field>
<field name="type">ir.actions.server</field>
<field name="model_id" ref="model_mrp_production_request" />
<field name="binding_model_id" ref="model_mrp_production_request"/>
<field name="state">code</field>
<field name="code">self._compute_manufactured_qty(cr, uid, context.get('active_ids', []), context=context)</field>
</record>
<record model="ir.values" id="action_mrp_production_request_refresh">
<field name="name">Compute Quantities</field>
<field name="action_id"
ref="action_server_mrp_production_request_refresh" />
<field name="value" eval="'ir.actions.server,' + str(ref('action_server_mrp_production_request_refresh'))" />
<field name="key">action</field>
<field name="model_id" ref="model_mrp_production_request" />
<field name="model">mrp.production.request</field>
<field name="key2">client_action_multi</field>
<field name="code">records._compute_manufactured_qty()</field>
</record>
</odoo>

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl-3.0) -->
<odoo>
<record model="ir.ui.view" id="procurement_form_view">
<field name="name">procurement.order.form - mrp_production_request</field>
<field name="model">procurement.order</field>
<field name="inherit_id" ref="procurement.procurement_form_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='origin']" position="after">
<field name="mrp_production_request_id"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,4 +1 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import mrp_production_request_create_mo

View File

@@ -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).
@@ -98,7 +97,6 @@ class MrpProductionRequestCreateMo(models.TransientModel):
'date_planned_start': request_id.date_planned_start,
'date_planned_finished': request_id.date_planned_finished,
'procurement_group_id': request_id.procurement_group_id.id,
'procurement_ids': [(6, 0, request_id.procurement_ids.ids)],
'propagate': request_id.propagate,
'company_id': request_id.company_id.id,
}