Merge pull request #210 from Eficent/9.0-add-mrp_request

[9.0][ADD] mrp_production_request
This commit is contained in:
Jordi Ballester Alomar
2017-08-28 14:18:30 +02:00
committed by GitHub
22 changed files with 1172 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
.. 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 module extends the functionality of Manufacturing to allow you to use
Manufacturing Request (MR) as a previous step to Manufacturing Orders (MO).
Configuration
=============
To configure this module to automatically generate Manufacturing Requests
from procurement orders instead of directly create manufacturing orders yo
need to:
#. Go to the products that you want them to trigger manufacturing orders.
#. Go to the *Inventory* tab.
#. Check the route *manufacture* and the box *Manufacturing Request*.
Usage
=====
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 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.
#. 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 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.
.. 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/9.0
Known issues / Roadmap
======================
* Take into account workstations.
* Take into account consumable products.
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.
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* Lois Rilo Antelo <lois.rilo@eficent.com>
* Jordi Ballester <jordi.ballester@eficent.com>
Maintainer
----------
.. 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.

View File

@@ -0,0 +1,6 @@
# -*- 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 . import models
from . import wizards

View File

@@ -0,0 +1,28 @@
# -*- 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).
{
"name": "MRP Production Request",
"summary": "Allows you to use Manufacturing Request as a previous "
"step to Manufacturing Orders for better manufacture "
"planification.",
"version": "9.0.1.0.0",
"category": "Manufacturing",
"website": "https://github.com/OCA/manufacture",
"author": "Eficent,"
"Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": ["mrp", "stock_available_unreserved"],
"data": [
"security/mrp_production_request_security.xml",
"security/ir.model.access.csv",
"data/mrp_production_request_sequence.xml",
"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,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo noupdate="1">
<record id="seq_mrp_production_request" model="ir.sequence">
<field name="name">Manufacturing Request</field>
<field name="code">mrp.production.request</field>
<field name="prefix">MR/%(range_year)s/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
</odoo>

View File

@@ -0,0 +1,9 @@
# -*- 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 . import mrp_production_request
from . import mrp_production
from . import procurement
from . import product
from . import stock_move

View File

@@ -0,0 +1,13 @@
# -*- 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 openerp import fields, models
class MrpProduction(models.Model):
_inherit = "mrp.production"
mrp_production_request_id = fields.Many2one(
comodel_name="mrp.production.request",
string="Manufacturing Request", copy=False, readonly=True)

View File

@@ -0,0 +1,229 @@
# -*- 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 openerp import api, fields, models, _
import openerp.addons.decimal_precision as dp
from openerp.exceptions import UserError
class MrpProductionRequest(models.Model):
_name = "mrp.production.request"
_description = "Manufacturing Request"
_inherit = "mail.thread"
@api.model
def _company_get(self):
company_id = self.env['res.company']._company_default_get(self._name)
return self.env['res.company'].browse(company_id.id)
@api.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 = self.product_id.uom_id
self.bom_id = self.env['mrp.bom']._bom_find(
product_id=self.product_id.id, properties=[])
@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)]})
origin = fields.Char(
string='Source Document',
readonly=True, states={'draft': [('readonly', False)]})
requested_by = fields.Many2one(
comodel_name='res.users', string='Requested by',
default=_get_default_requested_by,
required=True, track_visibility='onchange',
readonly=True, states={'draft': [('readonly', False)]})
assigned_to = fields.Many2one(
comodel_name='res.users', string='Approver',
track_visibility='onchange',
readonly=True, states={'draft': [('readonly', False)]})
description = fields.Text('Description')
date_planned = fields.Date(
string='Planned Date', required=True, track_visibility='onchange',
default=fields.Date.context_today)
company_id = fields.Many2one(
comodel_name='res.company', string='Company',
required=True, default=_company_get)
mrp_production_ids = fields.One2many(
comodel_name="mrp.production", string="Manufacturing Orders",
inverse_name="mrp_production_request_id")
state = fields.Selection(
selection=[("draft", "Draft"),
("to_approve", "To Be Approved"),
("approved", "Approved"),
("done", "Done"),
("cancel", "Cancelled")],
index=True, track_visibility='onchange',
required=True, copy=False, default='draft')
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'])],
track_visibility="onchange",
readonly=True, states={'draft': [('readonly', False)]})
product_tmpl_id = fields.Many2one(
comodel_name='product.template', string='Product Template',
related='product_id.product_tmpl_id')
product_qty = fields.Float(
string="Required Quantity", required=True, track_visibility='onchange',
digits_compute=dp.get_precision('Product Unit of Measure'),
readonly=True, states={'draft': [('readonly', False)]})
product_uom = fields.Many2one(
comodel_name='product.uom', string='Unit of Measure',
readonly=True, states={'draft': [('readonly', False)]},
domain="[('category_id', '=', category_uom_id)]")
category_uom_id = fields.Many2one(related="product_uom.category_id")
manufactured_qty = fields.Float(
string="Quantity in Manufacturing Orders",
compute=_compute_manufactured_qty, store=True, readonly=True,
digits_compute=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,
digits_compute=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,
store=True, digits_compute=dp.get_precision('Product Unit of Measure'),
readonly=True,
help="Quantity pending to add to Manufacturing Orders "
"to fulfill the Manufacturing Request requirement.")
bom_id = fields.Many2one(
comodel_name="mrp.bom", string="Bill of Materials", required=True,
readonly=True, states={'draft': [('readonly', False)]})
routing_id = fields.Many2one(
comodel_name='mrp.routing', string='Routing',
on_delete='setnull', readonly=True,
states={'draft': [('readonly', False)]},
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 plannification.")
location_src_id = fields.Many2one(
comodel_name='stock.location', string='Raw Materials Location',
default=lambda self: self.env['stock.location'].browse(
self.env['mrp.production']._src_id_default()),
required=True, readonly=True, states={'draft': [('readonly', False)]})
location_dest_id = fields.Many2one(
comodel_name='stock.location', string='Finished Products Location',
default=lambda self: self.env['stock.location'].browse(
self.env['mrp.production']._dest_id_default()),
required=True, readonly=True, states={'draft': [('readonly', False)]})
@api.multi
def button_to_approve(self):
self.write({'state': 'to_approve'})
return True
@api.multi
def button_approved(self):
self.write({'state': 'approved'})
return True
@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
def _check_reset_allowed(self):
if any([s in self._get_mo_valid_states() for s in self.mapped(
'mrp_production_ids.state')]):
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):
self._check_reset_allowed()
self.write({'state': 'draft'})
return True
@api.multi
def _check_cancel_allowed(self):
if any([s == 'done' for s in self.mapped(
'procurement_id.state')]):
raise UserError(
_('You cannot reject a manufacturing request related to '
'done procurement orders.'))
@api.multi
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()
return True

View File

@@ -0,0 +1,55 @@
# -*- 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 openerp 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.model
def _prepare_mrp_production_request(self, procurement):
data = self._prepare_mo_vals(procurement)
data['procurement_id'] = procurement.id
data['state'] = 'to_approve'
return data
@api.model
def _run(self, procurement):
if (procurement.rule_id and
procurement.rule_id.action == 'manufacture' and
procurement.product_id.mrp_production_request):
if not procurement.check_bom_exists():
procurement.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(
procurement)
req = self.env['mrp.production.request'].create(request_data)
procurement.message_post(body=_(
"Manufacturing Request created"))
procurement.mrp_production_request_id = req.id
return True
return super(ProcurementOrder, self)._run(procurement)
@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,14 @@
# -*- 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 openerp import fields, models
class ProductTemplate(models.Model):
_inherit = "product.template"
mrp_production_request = fields.Boolean(
string='Manufacturing Request', help="Check this box to generate "
"manufacturing request instead of generating manufacturing orders "
"from procurement.", default=False)

View File

@@ -0,0 +1,18 @@
# -*- 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 openerp import api, models
class StockMove(models.Model):
_inherit = "stock.move"
@api.model
def create(self, vals):
if 'production_id' in vals:
production = self.env['mrp.production'].browse(
vals['production_id'])
if production.mrp_production_request_id:
vals['propagate'] = False
return super(StockMove, self).create(vals)

View File

@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mrp_production_request_user,mrp.request.user,model_mrp_production_request,group_mrp_production_request_user,1,1,1,0
access_mrp_production_request_manager,mrp.request.manager,model_mrp_production_request,group_mrp_production_request_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mrp_production_request_user mrp.request.user model_mrp_production_request group_mrp_production_request_user 1 1 1 0
3 access_mrp_production_request_manager mrp.request.manager model_mrp_production_request group_mrp_production_request_manager 1 1 1 1

View File

@@ -0,0 +1,73 @@
<?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>
<data noupdate="0">
<record model="ir.module.category" id="module_category_mrp_production_request">
<field name="name">Manufacturing Request</field>
<field name="parent_id" ref="base.module_category_manufacturing"/>
<field name="sequence">20</field>
</record>
<record id="group_mrp_production_request_user" model="res.groups">
<field name="name">User</field>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="category_id" ref="module_category_mrp_production_request"/>
</record>
<record id="group_mrp_production_request_manager" model="res.groups">
<field name="name">Manager</field>
<field name="implied_ids"
eval="[(4, ref('mrp_production_request.group_mrp_production_request_user'))]"/>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
<field name="category_id" ref="module_category_mrp_production_request"/>
</record>
</data>
<data noupdate="0">
<record model="ir.rule" id="mrp_production_request_comp_rule">
<field name="name">Manufacturing Request multi-company</field>
<field name="model_id" ref="model_mrp_production_request"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),
('company_id','child_of',[user.company_id.id])]</field>
</record>
<record id="mrp_production_request_followers_rule" model="ir.rule">
<field name="name">Follow Manufacturing Request</field>
<field name="model_id" ref="model_mrp_production_request"/>
<field name="groups" eval="[(6,0, [ref('group_mrp_production_request_user')])]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="domain_force">['|',('requested_by','=',user.id),
('message_partner_ids', 'in', [user.partner_id.id])]</field>
</record>
<record id="mrp_production_request_rule" model="ir.rule">
<field name="name">Manufacturing Request User</field>
<field name="model_id" ref="model_mrp_production_request"/>
<field name="groups" eval="[(6,0, [ref('group_mrp_production_request_user')])]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
<field name="domain_force">[('requested_by','=',user.id)]</field>
</record>
<record id="mpr_production_request_line_manager_rule" model="ir.rule">
<field name="name">Manufacturing Request Line Manager</field>
<field name="model_id" ref="model_mrp_production_request"/>
<field name="groups" eval="[(6,0, [ref('group_mrp_production_request_manager')])]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
</data>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,5 @@
# -*- 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 . import test_mrp_production_request

View File

@@ -0,0 +1,124 @@
# -*- 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 openerp.tests.common import TransactionCase
from openerp import fields
from openerp.exceptions import UserError
class TestMrpProductionRequest(TransactionCase):
def setUp(self, *args, **kwargs):
super(TestMrpProductionRequest, self).setUp(*args, **kwargs)
self.production_model = self.env['mrp.production']
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.product = self.env.ref('product.product_product_3')
self.product.mrp_production_request = True
self.test_product = self.env['product.product'].create({
'name': 'Test Product without BoM',
'mrp_production_request': True,
})
self.test_user = self.env['res.users'].create({
'name': 'John',
'login': 'test',
})
def create_procurement(self, name, product):
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)],
}
return self.env['procurement.order'].create(values)
def test_manufacture_request(self):
"""Tests manufacture request workflow."""
proc = self.create_procurement('TEST/01', self.product)
request = proc.mrp_production_request_id
request.button_to_approve()
request.button_draft()
request.button_to_approve()
request.button_approved()
self.assertEqual(request.pending_qty, 4.0)
wiz = self.wiz_model.create({
'mrp_production_request_id': request.id,
})
wiz.compute_product_line_ids()
wiz.mo_qty = 4.0
wiz.create_mo()
mo = self.production_model.search([
('mrp_production_request_id', '=', request.id)])
self.assertTrue(mo, "No MO created.")
self.assertEqual(request.pending_qty, 0.0)
mo.action_confirm()
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.create({
'mrp_production_request_id': request.id,
})
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):
"""Tests assignation of manufacturing requests."""
randon_bom_id = self.bom_model.search([], limit=1).id
request = self.request_model.create({
'assigned_to': self.test_user.id,
'product_id': self.product.id,
'product_qty': 5.0,
'bom_id': randon_bom_id,
})
request._onchange_product_id()
self.assertEqual(
request.bom_id.product_tmpl_id, self.product.product_tmpl_id,
"Wrong Bill of Materials.")
request.write({
'assigned_to': self.uid,
})
self.assertTrue(request.message_follower_ids,
"Followers not added correctly.")
def test_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()

View File

@@ -0,0 +1,224 @@
<?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="view_mrp_production_request_form">
<field name="name">mrp.production.request.form</field>
<field name="model">mrp.production.request</field>
<field name="arch" type="xml">
<form string="Manufacturing Request">
<header>
<button name="button_draft"
states="to_approve,approved,cancel,done"
string="Reset"
type="object"
groups="mrp_production_request.group_mrp_production_request_manager"/>
<button name="button_to_approve" states="draft"
string="Request approval" type="object"
class="oe_highlight"/>
<button name="button_approved" states="to_approve"
string="Approve" type="object" class="oe_highlight"
groups="mrp_production_request.group_mrp_production_request_manager"/>
<button name="%(mrp_production_request_create_mo_action)d"
context="{'default_mrp_production_request_id':active_id}"
states="approved"
string="Create Manufacturing Order" type="action"/>
<button name="button_done" states="approved"
string="Done" type="object" class="oe_highlight"
groups="mrp_production_request.group_mrp_production_request_manager"/>
<button name="button_cancel" states="to_approve,approved"
string="Reject" type="object"
groups="mrp_production_request.group_mrp_production_request_manager"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,approved,done"/>
</header>
<sheet>
<label string="Manufacturing Request"/>
<h1>
<field name="name"/>
</h1>
<group name="request">
<group>
<field name="product_id"
domain="[('bom_ids','!=',False),('bom_ids.type','!=','phantom')]"/>
<field name="product_tmpl_id" invisible="1"/>
<field name="bom_id"
domain="['&amp;', '|', ('product_id','=',product_id),
'&amp;', ('product_tmpl_id.product_variant_ids','=',product_id),
('product_id','=',False), ('type', '=', 'normal')]"/>
</group>
<group>
<field name="product_qty"/>
<field name="done_qty"/>
<field name="manufactured_qty"/>
<field name="pending_qty"/>
<field name="product_uom" groups="product.group_uom"/>
<field name="category_uom_id" invisible="1"/>
</group>
</group>
<group>
<group name="users">
<field name="requested_by"/>
<field name="assigned_to"/>
</group>
<group>
<field name="description"/>
</group>
</group>
<notebook>
<page name="mrp_production" string="Manufacturing Orders">
<field name="mrp_production_ids"/>
</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="date_planned"/>
</group>
</group>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mrp_production_request_tree">
<field name="name">mrp.production.request.tree</field>
<field name="model">mrp.production.request</field>
<field name="arch" type="xml">
<tree decoration-info="state in ('draft','to_approve')"
decoration-muted="state in ('cancel')"
string="Manufacturing Requests">
<field name="name"/>
<field name="product_id"/>
<field name="requested_by"/>
<field name="assigned_to"/>
<field name="product_qty"/>
<field name="pending_qty"/>
<field name="company_id" groups="base.group_multi_company" widget="selection"/>
<field name="origin"/>
<field name="date_planned"/>
<field name="state"/>
</tree>
</field>
</record>
<record id="view_mrp_production_request_search" model="ir.ui.view">
<field name="name">mrp.production.request.search</field>
<field name="model">mrp.production.request</field>
<field name="arch" type="xml">
<search string="Search Manufacturing Request">
<field name="name" string="Manufacturing Request"/>
<field name="mrp_production_ids" string="Manufacturing Orders"/>
<field name="requested_by"/>
<field name="assigned_to" />
<field name="origin"/>
<field name="state"/>
<field name="pending_qty" invisible="1"/>
<!--Filters:-->
<filter name="unassigned" string="Unassigned"
domain="[('assigned_to','=', False)]"
help="Unassigned Request"/>
<filter domain="[('assigned_to','=', uid)]"
string="Assigned to me"/>
<filter domain="[('requested_by','=', uid)]"
string="My requests" help="Requested by me"/>
<separator/>
<filter domain="[('pending_qty','!=', 0.0)]"
string="Pending Qty" name="pending"
help="Request with pending quantity"/>
<separator/>
<filter name="state_draft" string="Draft"
domain="[('state','=','draft')]"
help="Request is to be approved"/>
<filter name="state_to_approve" string="To Approve"
domain="[('state','=','to_approve')]"
help="Request is to be approved"/>
<filter name="state_approved" string="Approved"
domain="[('state','=','approved')]"
help="Request is approved"/>
<filter name="state_cancel" string="Cancelled"
domain="[('state','=','cancel')]"
help="Request is cancelled"/>
<filter name="state_done" string="Done"
domain="[('state','=','done')]"
help="Request is done"/>
<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="[]"
context="{'group_by':'requested_by'}"/>
<filter string="Assigned to" icon="terp-personal"
domain="[]"
context="{'group_by':'assigned_to'}"/>
<filter string="Source" icon="terp-gtk-jump-to-rtl" domain="[]"
context="{'group_by':'origin'}"/>
</search>
</field>
</record>
<record model="ir.actions.act_window" id="mrp_production_request_action">
<field name="name">Manufacturing Requests</field>
<field name="type">ir.actions.act_window</field>
<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="help" type="html">
<p class="oe_view_nocontent_create">
Click to start a new manufacturing request process.
</p><p>
A Manufacturing Request is an instruction to production to produce
a certain quantity of a given product.
</p>
</field>
</record>
<menuitem
id="menu_mrp_production_request_act"
sequence="10"
parent="mrp.menu_mrp_manufacturing"
action="mrp_production_request_action"
/>
<!--Sever actions-->
<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="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>
</record>
</odoo>

View File

@@ -0,0 +1,18 @@
<?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 id="mrp_production_form_view" model="ir.ui.view">
<field name="name">mrp.production.form - mrp_production_request</field>
<field name="model">mrp.production</field>
<field name="inherit_id"
ref="mrp.mrp_production_form_view"/>
<field name="arch" type="xml">
<field name="move_prod_id" position="after">
<field name="mrp_production_request_id"/>
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,17 @@
<?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

@@ -0,0 +1,18 @@
<?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="view_template_property_form">
<field name="name">product.template.form - mrp_production_request</field>
<field name="model">product.template</field>
<field name="inherit_id"
ref="stock.view_template_property_form"/>
<field name="arch" type="xml">
<field name="route_ids" position="after">
<field name="mrp_production_request"/>
</field>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,5 @@
# -*- 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 . import mrp_production_request_create_mo

View File

@@ -0,0 +1,140 @@
# -*- 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 openerp import api, fields, models
import openerp.addons.decimal_precision as dp
class MrpProductionRequestCreateMo(models.TransientModel):
_name = "mrp.production.request.create.mo"
_description = "Wizard to create Manufacturing Orders"
@api.multi
def compute_product_line_ids(self):
self.product_line_ids.unlink()
res = self._prepare_lines()
product_lines = res[0]
# TODO: expand with workcenter_lines: they are in res[1].
for line in product_lines:
self.env['mrp.production.request.create.mo.line'].create(
self._prepare_product_line(line))
self._get_mo_qty()
return {"type": "ir.actions.do_nothing"}
def _prepare_lines(self):
"""Get the components (product_lines) and Work Centers Utilisation
(workcenter_lines) needed for manufacturing the given a BoM.
:return: product_lines, workcenter_lines
"""
bom_obj = self.env['mrp.bom']
uom_obj = self.env['product.uom']
bom_point = self.bom_id
factor = uom_obj._compute_qty(
self.mrp_production_request_id.product_uom.id, self.pending_qty,
bom_point.product_uom.id)
return bom_obj._bom_explode(
bom_point, self.mrp_production_request_id.product_id,
factor / bom_point.product_qty,
routing_id=self.mrp_production_request_id.routing_id.id)
@api.one
def _get_mo_qty(self):
"""Propose a qty to create a MO available to produce."""
bottle_neck = min(self.product_line_ids.mapped('bottle_neck_factor'))
bottle_neck = max(min(1, bottle_neck), 0)
self.mo_qty = self.pending_qty * bottle_neck
mrp_production_request_id = fields.Many2one(
comodel_name="mrp.production.request", readonly=True)
bom_id = fields.Many2one(
related='mrp_production_request_id.bom_id', readonly=True)
mo_qty = fields.Float(
string="Quantity",
digits_compute=dp.get_precision("Product Unit of Measure"))
pending_qty = fields.Float(
related="mrp_production_request_id.pending_qty",
digits_compute=dp.get_precision("Product Unit of Measure"))
product_uom = fields.Many2one(
related="mrp_production_request_id.product_uom")
product_line_ids = fields.One2many(
comodel_name="mrp.production.request.create.mo.line",
string="Products needed",
inverse_name="mrp_production_request_create_mo_id", readonly=True)
def _prepare_product_line(self, pl):
return {
'product_id': pl['product_id'],
'product_qty': pl['product_qty'],
'product_uom': pl['product_uom'],
'mrp_production_request_create_mo_id': self.id,
'location_id': self.mrp_production_request_id.location_src_id.id,
}
@api.multi
def _prepare_manufacturing_order(self):
self.ensure_one()
request_id = self.mrp_production_request_id
return {
'product_id': request_id.product_id.id,
'bom_id': request_id.bom_id.id,
'product_qty': self.mo_qty,
'product_uom': self.product_uom.id,
'mrp_production_request_id': self.mrp_production_request_id.id,
'origin': request_id.origin,
'location_src_id': request_id.location_src_id.id,
'location_dest_id': request_id.location_dest_id.id,
'routing_id': request_id.routing_id.id,
'move_prod_id': request_id.procurement_id.move_dest_id.id or False,
'date_planned': request_id.date_planned,
'company_id': request_id.company_id.id,
}
@api.multi
def create_mo(self):
self.ensure_one()
vals = self._prepare_manufacturing_order()
mo = self.env['mrp.production'].create(vals)
# Open resulting MO:
action = self.env.ref('mrp.mrp_production_action').read()[0]
res = self.env.ref('mrp.mrp_production_form_view')
action.update({
'res_id': mo and mo.id,
'views': [(res and res.id or False, 'form')],
})
return action
class MrpProductionRequestCreateMoLine(models.TransientModel):
_name = "mrp.production.request.create.mo.line"
@api.one
def _compute_available_qty(self):
product_available = self.product_id.with_context(
location=self.location_id.id)._product_available()[
self.product_id.id]['qty_available_not_res']
res = self.product_uom._compute_qty(
self.product_id.product_tmpl_id.uom_id.id, product_available,
self.product_uom.id)
self.available_qty = res
@api.one
def _compute_bottle_neck_factor(self):
if self.product_qty:
self.bottle_neck_factor = self.available_qty / self.product_qty
product_id = fields.Many2one(
comodel_name='product.product', string='Product', required=True)
product_qty = fields.Float(
string='Quantity Required', required=True,
digits_compute=dp.get_precision('Product Unit of Measure'))
product_uom = fields.Many2one(
comodel_name='product.uom', string='UoM', required=True)
mrp_production_request_create_mo_id = fields.Many2one(
comodel_name='mrp.production.request.create.mo')
available_qty = fields.Float(
string='Quantity Available', compute=_compute_available_qty,
digits_compute=dp.get_precision('Product Unit of Measure'))
bottle_neck_factor = fields.Float(compute=_compute_bottle_neck_factor)
location_id = fields.Many2one(comodel_name='stock.location',
required=True)

View File

@@ -0,0 +1,59 @@
<?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 id="mrp_production_request_create_mo_view" model="ir.ui.view">
<field name="name">mrp.production.request.create.mo.form</field>
<field name="model">mrp.production.request.create.mo</field>
<field name="arch" type="xml">
<form string="Select event to register">
<group name="origin" string="Manufacture Request status"
col="6">
<group colspan="2">
<field name="mrp_production_request_id"
options='{"no_open": True}'/>
<field name="pending_qty"/>
<button name="compute_product_line_ids" type="object"
string="Compute lines" colspan="2" icon="fa-cogs"/>
</group>
<group colspan="4">
<field name="product_line_ids" nolabel="1">
<tree>
<field name="product_id"/>
<field name="product_uom"/>
<field name="product_qty"/>
<field name="available_qty"/>
<field name="bottle_neck_factor"/>
</tree>
</field>
</group>
</group>
<group name="destination" string="Manufacturing Order:" col="4">
<field name="mo_qty"/>
<field name="product_uom" options="{'no_open': True}"
groups="product.group_uom"/>
</group>
<footer>
<button name="create_mo"
type="object"
string="Create MO"
class="oe_highlight"/>
or
<button special="cancel"
string="Cancel"/>
</footer>
</form>
</field>
</record>
<record id="mrp_production_request_create_mo_action" model="ir.actions.act_window">
<field name="name">Create Manufacturing Order</field>
<field name="res_model">mrp.production.request.create.mo</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>