Files
odoo_source/addons/mrp/models/mrp_bom.py
T
Josse Colpaert 347f140fe6 [FIX] mrp: rounding issues and efficiency replacement
Before, when you would put a quantity in a BoM line that was not rounded
to its UoM (e.g. use 0.3 piece with rounding 1.0) it would not round this
quantity in the MO also (when exploding the BoM), and the user could also
enter a not rounded quantity himself for the quantity produced or
consumed.

The moves were split with the exact quantities, but the moves were validated
taking into account rounded quantities. This resulted in either moving 2
pieces instead of 1 (entering 0.7 or 1 piece) or raising an error telling
you can not process moves with 0 quantity (0.3 piece).

As product_efficiency and product_rounding was removed from the BoM/BoM
line in v10, if the rounding is correctly handled, the feature could work
like in v9, by e.g. putting 1.03 in the bom line (97% efficiency) and
rounding up when exploding the BoM (e.g. producing 10 pieces would put
it to 11).

When "producing", we round the produced qty to the product uom (it does
not make sense to produce 1.32 if your uom rounding is 1.0). The user
might now change the produced quantity, that's why we round-up when
validating the move: indeed, if the user slightly increase the produced
value (below the uom unit), it actually means he produced more.

This way, instead of not rounding anything and doing rounding behind the
scenes, leading to errors, we round everything and the user sees the
result directly.
2017-01-27 19:22:02 +01:00

279 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_round
class MrpBom(models.Model):
""" Defines bills of material for a product or a product template """
_name = 'mrp.bom'
_description = 'Bill of Material'
_inherit = ['mail.thread']
_rec_name = 'product_tmpl_id'
_order = "sequence"
def _get_default_product_uom_id(self):
return self.env['product.uom'].search([], limit=1, order='id').id
code = fields.Char('Reference')
active = fields.Boolean(
'Active', default=True,
help="If the active field is set to False, it will allow you to hide the bills of material without removing it.")
type = fields.Selection([
('normal', 'Manufacture this product'),
('phantom', 'Ship this product as a set of components (kit)')], 'BoM Type',
default='normal', required=True,
help="Kit (Phantom): When processing a sales order for this product, the delivery order will contain the raw materials, instead of the finished product.")
product_tmpl_id = fields.Many2one(
'product.template', 'Product',
domain="[('type', 'in', ['product', 'consu'])]", required=True)
product_id = fields.Many2one(
'product.product', 'Product Variant',
domain="['&', ('product_tmpl_id', '=', product_tmpl_id), ('type', 'in', ['product', 'consu'])]",
help="If a product variant is defined the BOM is available only for this product.")
bom_line_ids = fields.One2many('mrp.bom.line', 'bom_id', 'BoM Lines', copy=True)
product_qty = fields.Float(
'Quantity', default=1.0,
digits=dp.get_precision('Unit of Measure'), required=True)
product_uom_id = fields.Many2one(
'product.uom', 'Product Unit of Measure',
default=_get_default_product_uom_id, oldname='product_uom', required=True,
help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control")
sequence = fields.Integer('Sequence', help="Gives the sequence order when displaying a list of bills of material.")
routing_id = fields.Many2one(
'mrp.routing', 'Routing',
help="The operations for producing this BoM. When a routing is specified, the production orders will "
" be executed through work orders, otherwise everything is processed in the production order itself. ")
ready_to_produce = fields.Selection([
('all_available', 'All components available'),
('asap', 'The components of 1st operation')], string='Manufacturing Readiness',
default='asap', required=True)
picking_type_id = fields.Many2one(
'stock.picking.type', 'Picking Type', domain=[('code', '=', 'mrp_operation')],
help="When a procurement has a ‘produce’ route with a picking type set, it will try to create "
"a Manufacturing Order for that product using a BoM of the same picking type. That allows "
"to define procurement rules which trigger different manufacturing orders with different BoMs. ")
company_id = fields.Many2one(
'res.company', 'Company',
default=lambda self: self.env['res.company']._company_default_get('mrp.bom'),
required=True)
@api.constrains('product_id', 'product_tmpl_id', 'bom_line_ids')
def _check_product_recursion(self):
for bom in self:
if bom.bom_line_ids.filtered(lambda x: x.product_id.product_tmpl_id == bom.product_tmpl_id):
raise ValidationError(_('BoM line product %s should not be same as BoM product.') % bom.display_name)
@api.onchange('product_uom_id')
def onchange_product_uom_id(self):
res = {}
if not self.product_uom_id or not self.product_tmpl_id:
return
if self.product_uom_id.category_id.id != self.product_tmpl_id.uom_id.category_id.id:
self.product_uom_id = self.product_tmpl_id.uom_id.id
res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
return res
@api.onchange('product_tmpl_id')
def onchange_product_tmpl_id(self):
if self.product_tmpl_id:
self.product_uom_id = self.product_tmpl_id.uom_id.id
@api.multi
def name_get(self):
return [(bom.id, '%s%s' % (bom.code and '%s: ' % bom.code or '', bom.product_tmpl_id.display_name)) for bom in self]
@api.multi
def unlink(self):
if self.env['mrp.production'].search([('bom_id', 'in', self.ids), ('state', 'not in', ['done', 'cancel'])], limit=1):
raise UserError(_('You can not delete a Bill of Material with running manufacturing orders.\nPlease close or cancel it first.'))
return super(MrpBom, self).unlink()
@api.model
def _bom_find(self, product_tmpl=None, product=None, picking_type=None, company_id=False):
""" Finds BoM for particular product, picking and company """
if product:
if not product_tmpl:
product_tmpl = product.product_tmpl_id
domain = ['|', ('product_id', '=', product.id), '&', ('product_id', '=', False), ('product_tmpl_id', '=', product_tmpl.id)]
elif product_tmpl:
domain = [('product_tmpl_id', '=', product_tmpl.id)]
else:
# neither product nor template, makes no sense to search
return False
if picking_type:
domain += ['|', ('picking_type_id', '=', picking_type.id), ('picking_type_id', '=', False)]
if company_id or self.env.context.get('company_id'):
domain = domain + [('company_id', '=', company_id or self.env.context.get('company_id'))]
# order to prioritize bom with product_id over the one without
return self.search(domain, order='sequence, product_id', limit=1)
def explode(self, product, quantity, picking_type=False):
"""
Explodes the BoM and creates two lists with all the information you need: bom_done and line_done
Quantity describes the number of times you need the BoM: so the quantity divided by the number created by the BoM
and converted into its UoM
"""
boms_done = [(self, {'qty': quantity, 'product': product, 'original_qty': quantity, 'parent_line': False})]
lines_done = []
templates_done = product.product_tmpl_id
bom_lines = [(bom_line, product, quantity, False) for bom_line in self.bom_line_ids]
while bom_lines:
current_line, current_product, current_qty, parent_line = bom_lines[0]
bom_lines = bom_lines[1:]
if current_line._skip_bom_line(current_product):
continue
if current_line.product_id.product_tmpl_id in templates_done:
raise UserError(_('Recursion error! A product with a Bill of Material should not have itself in its BoM or child BoMs!'))
line_quantity = current_qty * current_line.product_qty
bom = self._bom_find(product=current_line.product_id, picking_type=picking_type or self.picking_type_id, company_id=self.company_id.id)
if bom.type == 'phantom':
converted_line_quantity = current_line.product_uom_id._compute_quantity(line_quantity / bom.product_qty, bom.product_uom_id)
bom_lines = [(line, current_line.product_id, converted_line_quantity, current_line) for line in bom.bom_line_ids] + bom_lines
templates_done |= current_line.product_id.product_tmpl_id
boms_done.append((bom, {'qty': converted_line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': current_line}))
else:
# We round up here because the user expects that if he has to consume a little more, the whole UOM unit
# should be consumed.
rounding = current_line.product_uom_id.rounding
line_quantity = float_round(line_quantity, precision_rounding=rounding, rounding_method='UP')
lines_done.append((current_line, {'qty': line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': parent_line}))
return boms_done, lines_done
class MrpBomLine(models.Model):
_name = 'mrp.bom.line'
_order = "sequence"
_rec_name = "product_id"
def _get_default_product_uom_id(self):
return self.env['product.uom'].search([], limit=1, order='id').id
product_id = fields.Many2one(
'product.product', 'Product', required=True)
product_qty = fields.Float(
'Product Quantity', default=1.0,
digits=dp.get_precision('Product Unit of Measure'), required=True)
product_uom_id = fields.Many2one(
'product.uom', 'Product Unit of Measure',
default=_get_default_product_uom_id,
oldname='product_uom', required=True,
help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control")
sequence = fields.Integer(
'Sequence', default=1,
help="Gives the sequence order when displaying.")
routing_id = fields.Many2one(
'mrp.routing', 'Routing',
related='bom_id.routing_id', store=True,
help="The list of operations to produce the finished product. The routing is mainly used to "
"compute work center costs during operations and to plan future loads on work centers "
"based on production planning.")
bom_id = fields.Many2one(
'mrp.bom', 'Parent BoM',
index=True, ondelete='cascade', required=True)
attribute_value_ids = fields.Many2many(
'product.attribute.value', string='Variants',
help="BOM Product Variants needed form apply this line.")
operation_id = fields.Many2one(
'mrp.routing.workcenter', 'Consumed in Operation',
help="The operation where the components are consumed, or the finished products created.")
child_bom_id = fields.Many2one(
'mrp.bom', 'Sub BoM', compute='_compute_child_bom_id')
child_line_ids = fields.One2many(
'mrp.bom.line', string="BOM lines of the referred bom",
compute='_compute_child_line_ids')
has_attachments = fields.Boolean('Has Attachments', compute='_compute_has_attachments')
_sql_constraints = [
('bom_qty_zero', 'CHECK (product_qty>0)', 'All product quantities must be greater than 0.\n'
'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
]
@api.one
@api.depends('product_id', 'bom_id')
def _compute_child_bom_id(self):
if not self.product_id:
self.child_bom_id = False
else:
self.child_bom_id = self.env['mrp.bom']._bom_find(
product_tmpl=self.product_id.product_tmpl_id,
product=self.product_id,
picking_type=self.bom_id.picking_type_id)
@api.one
@api.depends('product_id')
def _compute_has_attachments(self):
nbr_attach = self.env['ir.attachment'].search_count([
'|',
'&', ('res_model', '=', 'product.product'), ('res_id', '=', self.product_id.id),
'&', ('res_model', '=', 'product.template'), ('res_id', '=', self.product_id.product_tmpl_id.id)])
self.has_attachments = bool(nbr_attach)
@api.one
@api.depends('child_bom_id')
def _compute_child_line_ids(self):
""" If the BOM line refers to a BOM, return the ids of the child BOM lines """
self.child_line_ids = self.child_bom_id.bom_line_ids.ids
@api.onchange('product_uom_id')
def onchange_product_uom_id(self):
res = {}
if not self.product_uom_id or not self.product_id:
return res
if self.product_uom_id.category_id != self.product_id.uom_id.category_id:
self.product_uom_id = self.product_id.uom_id.id
res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
return res
@api.onchange('product_id')
def onchange_product_id(self):
if self.product_id:
self.product_uom_id = self.product_id.uom_id.id
@api.model
def create(self, values):
if 'product_id' in values and 'product_uom_id' not in values:
values['product_uom_id'] = self.env['product.product'].browse(values['product_id']).uom_id.id
return super(MrpBomLine, self).create(values)
def _skip_bom_line(self, product):
""" Control if a BoM line should be produce, can be inherited for add
custom control. It currently checks that all variant values are in the
product. """
if self.attribute_value_ids:
if not product or self.attribute_value_ids - product.attribute_value_ids:
return True
return False
@api.multi
def action_see_attachments(self):
domain = [
'|',
'&', ('res_model', '=', 'product.product'), ('res_id', '=', self.product_id.id),
'&', ('res_model', '=', 'product.template'), ('res_id', '=', self.product_id.product_tmpl_id.id)]
attachment_view = self.env.ref('mrp.view_document_file_kanban_mrp')
return {
'name': _('Attachments'),
'domain': domain,
'res_model': 'ir.attachment',
'type': 'ir.actions.act_window',
'view_id': attachment_view.id,
'views': [(attachment_view.id, 'kanban'), (False, 'form')],
'view_mode': 'kanban,tree,form',
'view_type': 'form',
'help': _('''<p class="oe_view_nocontent_create">
Click to upload files to your product.
</p><p>
Use this feature to store any files, like drawings or specifications.
</p>'''),
'limit': 80,
'context': "{'default_res_model': '%s','default_res_id': %d}" % ('product.product', self.product_id.id)
}