mirror of
https://github.com/bringout/oca-ocb-mrp.git
synced 2026-04-25 15:32:03 +02:00
19.0 vanilla
This commit is contained in:
parent
accf5918df
commit
6e65e8c877
688 changed files with 225434 additions and 199401 deletions
|
|
@ -1,10 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _, Command
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.osv.expression import AND, OR
|
||||
from odoo.tools import float_round
|
||||
from odoo.fields import Command, Domain
|
||||
from odoo.tools import float_compare
|
||||
from odoo.tools.misc import clean_context, OrderedSet
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ 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']
|
||||
_inherit = ['mail.thread', 'product.catalog.mixin']
|
||||
_rec_name = 'product_tmpl_id'
|
||||
_rec_names_search = ['product_tmpl_id', 'code']
|
||||
_order = "sequence, id"
|
||||
|
|
@ -31,34 +31,38 @@ class MrpBom(models.Model):
|
|||
product_tmpl_id = fields.Many2one(
|
||||
'product.template', 'Product',
|
||||
check_company=True, index=True,
|
||||
domain="[('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", required=True)
|
||||
domain="[('type', '=', 'consu')]", required=True)
|
||||
product_id = fields.Many2one(
|
||||
'product.product', 'Product Variant',
|
||||
check_company=True, index=True,
|
||||
domain="['&', ('product_tmpl_id', '=', product_tmpl_id), ('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
domain="['&', ('product_tmpl_id', '=', product_tmpl_id), ('type', '=', '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)
|
||||
byproduct_ids = fields.One2many('mrp.bom.byproduct', 'bom_id', 'By-products', copy=True)
|
||||
product_qty = fields.Float(
|
||||
'Quantity', default=1.0,
|
||||
digits='Product Unit of Measure', required=True,
|
||||
digits='Product Unit', required=True,
|
||||
help="This should be the smallest quantity that this product can be produced in. If the BOM contains operations, make sure the work center capacity is accurate.")
|
||||
product_uom_id = fields.Many2one(
|
||||
'uom.uom', 'Unit of Measure',
|
||||
'uom.uom', 'Unit',
|
||||
default=_get_default_product_uom_id, required=True,
|
||||
help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control", domain="[('category_id', '=', product_uom_category_id)]")
|
||||
product_uom_category_id = fields.Many2one(related='product_tmpl_id.uom_id.category_id')
|
||||
help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control")
|
||||
sequence = fields.Integer('Sequence')
|
||||
operation_ids = fields.One2many('mrp.routing.workcenter', 'bom_id', 'Operations', copy=True)
|
||||
operation_count = fields.Integer('Operations Count', compute='_compute_operation_count')
|
||||
show_copy_operations_button = fields.Boolean(
|
||||
compute="_compute_show_copy_operations_button",
|
||||
help="Technical field used to control the visibility of the 'Copy Existing Operations' button.")
|
||||
ready_to_produce = fields.Selection([
|
||||
('all_available', ' When all components are available'),
|
||||
('asap', 'When components for 1st operation are available')], string='Manufacturing Readiness',
|
||||
default='all_available', required=True)
|
||||
picking_type_id = fields.Many2one(
|
||||
'stock.picking.type', 'Operation Type', domain="[('code', '=', 'mrp_operation'), ('company_id', '=', company_id)]",
|
||||
'stock.picking.type', 'Operation Type', domain="[('code', '=', 'mrp_operation')]",
|
||||
check_company=True,
|
||||
help=u"When a procurement has a ‘produce’ route with a operation type set, it will try to create "
|
||||
"a Manufacturing Order for that product using a BoM of the same operation type. That allows "
|
||||
"a Manufacturing Order for that product using a BoM of the same operation type.If not,"
|
||||
"the operation type is not taken into account in the BoM search. That allows "
|
||||
"to define stock rules which trigger different manufacturing orders with different BoMs.")
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company', index=True,
|
||||
|
|
@ -70,7 +74,7 @@ class MrpBom(models.Model):
|
|||
help="Defines if you can consume more or less components than the quantity defined on the BoM:\n"
|
||||
" * Allowed: allowed for all manufacturing users.\n"
|
||||
" * Allowed with warning: allowed for all manufacturing users with summary of consumption differences when closing the manufacturing order.\n"
|
||||
" Note that in the case of component Manual Consumption, where consumption is registered manually exclusively, consumption warnings will still be issued when appropriate also.\n"
|
||||
" Note that in the case of component Highlight Consumption, where consumption is registered manually exclusively, consumption warnings will still be issued when appropriate also.\n"
|
||||
" * Blocked: only a manager can close a manufacturing order when the BoM consumption is not respected.",
|
||||
default='warning',
|
||||
string='Flexible Consumption',
|
||||
|
|
@ -82,10 +86,20 @@ class MrpBom(models.Model):
|
|||
allow_operation_dependencies = fields.Boolean('Operation Dependencies',
|
||||
help="Create operation level dependencies that will influence both planning and the status of work orders upon MO confirmation. If this feature is ticked, and nothing is specified, Odoo will assume that all operations can be started simultaneously."
|
||||
)
|
||||
produce_delay = fields.Integer(
|
||||
'Manufacturing Lead Time', default=0,
|
||||
help="Average lead time in days to manufacture this product. In the case of multi-level BOM, the manufacturing lead times of the components will be added. In case the product is subcontracted, this can be used to determine the date at which components should be sent to the subcontractor.")
|
||||
days_to_prepare_mo = fields.Integer(
|
||||
string="Days to prepare Manufacturing Order", default=0,
|
||||
help="Create and confirm Manufacturing Orders this many days in advance, to have enough time to replenish components or manufacture semi-finished products.")
|
||||
show_set_bom_button = fields.Boolean(compute="_compute_show_set_bom_button")
|
||||
batch_size = fields.Float('Batch Size', default=1.0, digits='Product Unit', help="All automatically generated manufacturing orders for this product will be of this size.")
|
||||
enable_batch_size = fields.Boolean(default=False)
|
||||
|
||||
_sql_constraints = [
|
||||
('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'),
|
||||
]
|
||||
_qty_positive = models.Constraint(
|
||||
'check (product_qty > 0)',
|
||||
'The quantity to produce must be positive!',
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
'product_tmpl_id.attribute_line_ids.value_ids',
|
||||
|
|
@ -94,14 +108,26 @@ class MrpBom(models.Model):
|
|||
)
|
||||
def _compute_possible_product_template_attribute_value_ids(self):
|
||||
for bom in self:
|
||||
bom.possible_product_template_attribute_value_ids = bom.product_tmpl_id.valid_product_template_attribute_line_ids._without_no_variant_attributes().product_template_value_ids._only_active()
|
||||
bom.possible_product_template_attribute_value_ids = bom.product_tmpl_id.valid_product_template_attribute_line_ids.product_template_value_ids._only_active()
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
if self.product_id:
|
||||
warning = (
|
||||
self.bom_line_ids.bom_product_template_attribute_value_ids or
|
||||
self.operation_ids.bom_product_template_attribute_value_ids or
|
||||
self.byproduct_ids.bom_product_template_attribute_value_ids
|
||||
) and {
|
||||
'warning': {
|
||||
'title': _("Warning"),
|
||||
'message': _("Changing the product or variant will permanently reset all previously encoded variant-related data."),
|
||||
}
|
||||
}
|
||||
self.bom_line_ids.bom_product_template_attribute_value_ids = False
|
||||
self.operation_ids.bom_product_template_attribute_value_ids = False
|
||||
self.byproduct_ids.bom_product_template_attribute_value_ids = False
|
||||
if warning:
|
||||
return warning
|
||||
|
||||
@api.constrains('active', 'product_id', 'product_tmpl_id', 'bom_line_ids')
|
||||
def _check_bom_cycle(self):
|
||||
|
|
@ -118,8 +144,9 @@ class MrpBom(models.Model):
|
|||
for component in components:
|
||||
if component in finished_products:
|
||||
names = finished_products.mapped('display_name')
|
||||
raise ValidationError(_("The current configuration is incorrect because it would create a cycle "
|
||||
"between these products: %s.") % ', '.join(names))
|
||||
raise ValidationError(_(
|
||||
"The current configuration is incorrect because it would create a cycle between these products: %s.",
|
||||
', '.join(names)))
|
||||
if component not in subcomponents_dict:
|
||||
products_to_find |= component
|
||||
|
||||
|
|
@ -134,11 +161,11 @@ class MrpBom(models.Model):
|
|||
_check_cycle(subcomponents, finished_products | component)
|
||||
|
||||
boms_to_check = self
|
||||
domain = []
|
||||
for product in self.bom_line_ids.product_id:
|
||||
domain = OR([domain, self._bom_find_domain(product)])
|
||||
if domain:
|
||||
boms_to_check |= self.env['mrp.bom'].search(domain)
|
||||
if self.bom_line_ids.product_id:
|
||||
boms_to_check |= self.search(Domain.OR(
|
||||
self._bom_find_domain(product)
|
||||
for product in self.bom_line_ids.product_id
|
||||
))
|
||||
|
||||
for bom in boms_to_check:
|
||||
if not bom.active:
|
||||
|
|
@ -154,12 +181,6 @@ class MrpBom(models.Model):
|
|||
else:
|
||||
_check_cycle(bom.bom_line_ids.product_id, finished_products)
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if 'sequence' in vals and self and self[-1].id == self._prefetch_ids[-1]:
|
||||
self.browse(self._prefetch_ids)._check_bom_cycle()
|
||||
return res
|
||||
|
||||
@api.constrains('product_id', 'product_tmpl_id', 'bom_line_ids', 'byproduct_ids', 'operation_ids')
|
||||
def _check_bom_lines(self):
|
||||
for bom in self:
|
||||
|
|
@ -180,15 +201,17 @@ class MrpBom(models.Model):
|
|||
else:
|
||||
same_product = bom.product_tmpl_id == byproduct.product_id.product_tmpl_id
|
||||
if same_product:
|
||||
raise ValidationError(_("By-product %s should not be the same as BoM product.") % bom.display_name)
|
||||
raise ValidationError(_("By-product %s should not be the same as BoM product.", bom.display_name))
|
||||
if byproduct.cost_share < 0:
|
||||
raise ValidationError(_("By-products cost shares must be positive."))
|
||||
if sum(bom.byproduct_ids.mapped('cost_share')) > 100:
|
||||
raise ValidationError(_("The total cost share for a BoM's by-products cannot exceed 100."))
|
||||
for product in bom.product_tmpl_id.product_variant_ids:
|
||||
total_variant_cost_share = sum(bom.byproduct_ids.filtered(lambda bp: not bp._skip_byproduct_line(product) and not bp.product_uom_id.is_zero(bp.product_qty)).mapped('cost_share'))
|
||||
if float_compare(total_variant_cost_share, 100, precision_digits=2) > 0:
|
||||
raise ValidationError(_("The total cost share for a BoM's by-products cannot exceed 100."))
|
||||
|
||||
@api.onchange('bom_line_ids', 'product_qty', 'product_id', 'product_tmpl_id')
|
||||
def onchange_bom_structure(self):
|
||||
if self.type == 'phantom' and self._origin and self.env['stock.move'].search([('bom_line_id', 'in', self._origin.bom_line_ids.ids)], limit=1):
|
||||
if self.type == 'phantom' and self._origin and self.env['stock.move'].search_count([('bom_line_id', 'in', self._origin.bom_line_ids.ids)], limit=1):
|
||||
return {
|
||||
'warning': {
|
||||
'title': _('Warning'),
|
||||
|
|
@ -198,20 +221,23 @@ class MrpBom(models.Model):
|
|||
}
|
||||
}
|
||||
|
||||
@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
|
||||
warning = (
|
||||
self.bom_line_ids.bom_product_template_attribute_value_ids or
|
||||
self.operation_ids.bom_product_template_attribute_value_ids or
|
||||
self.byproduct_ids.bom_product_template_attribute_value_ids
|
||||
) and {
|
||||
'warning': {
|
||||
'title': _("Warning"),
|
||||
'message': _("Changing the product or variant will permanently reset all previously encoded variant-related data."),
|
||||
}
|
||||
}
|
||||
default_uom_id = self.env.context.get('default_product_uom_id')
|
||||
# Avoids updating the BoM's UoM in case a specific UoM was passed through as a default value.
|
||||
if self.product_uom_id.id != default_uom_id:
|
||||
self.product_uom_id = self.product_tmpl_id.uom_id.id
|
||||
if self.product_id.product_tmpl_id != self.product_tmpl_id:
|
||||
self.product_id = False
|
||||
self.bom_line_ids.bom_product_template_attribute_value_ids = False
|
||||
|
|
@ -223,67 +249,136 @@ class MrpBom(models.Model):
|
|||
domain.append(('id', '!=', self.id.origin))
|
||||
number_of_bom_of_this_product = self.env['mrp.bom'].search_count(domain)
|
||||
if number_of_bom_of_this_product: # add a reference to the bom if there is already a bom for this product
|
||||
self.code = _("%s (new) %s", self.product_tmpl_id.name, number_of_bom_of_this_product)
|
||||
else:
|
||||
self.code = False
|
||||
self.code = _("%(product_name)s (new) %(number_of_boms)s", product_name=self.product_tmpl_id.name, number_of_boms=number_of_bom_of_this_product)
|
||||
if warning:
|
||||
return warning
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
res = super().create(vals_list)
|
||||
# Checks if the BoM was created from a Manufacturing Order (through Generate BoM action).
|
||||
parent_production_id = self.env.context.get('parent_production_id')
|
||||
if parent_production_id: # In this case, assign the newly created BoM to the MO.
|
||||
# Clean context to avoid parasitic default values.
|
||||
env = self.env(context=clean_context(self.env.context))
|
||||
production = env['mrp.production'].browse(parent_production_id)
|
||||
production._link_bom(res[0])
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
relevant_fields = ['bom_line_ids', 'byproduct_ids', 'product_tmpl_id', 'product_id', 'product_qty']
|
||||
if any(field_name in vals for field_name in relevant_fields):
|
||||
self._set_outdated_bom_in_productions()
|
||||
if 'sequence' in vals and self and self[-1].id == list(self._prefetch_ids)[-1]:
|
||||
self.browse(self._prefetch_ids)._check_bom_cycle()
|
||||
return res
|
||||
|
||||
def copy(self, default=None):
|
||||
res = super().copy(default)
|
||||
if self.operation_ids:
|
||||
operations_mapping = {}
|
||||
for original, copied in zip(self.operation_ids, res.operation_ids.sorted()):
|
||||
operations_mapping[original] = copied
|
||||
for bom_line in res.bom_line_ids:
|
||||
if bom_line.operation_id:
|
||||
bom_line.operation_id = operations_mapping[bom_line.operation_id]
|
||||
for byproduct in res.byproduct_ids:
|
||||
if byproduct.operation_id:
|
||||
byproduct.operation_id = operations_mapping[byproduct.operation_id]
|
||||
for operation in self.operation_ids:
|
||||
if operation.blocked_by_operation_ids:
|
||||
copied_operation = operations_mapping[operation]
|
||||
dependencies = []
|
||||
for dependency in operation.blocked_by_operation_ids:
|
||||
dependencies.append(Command.link(operations_mapping[dependency].id))
|
||||
copied_operation.blocked_by_operation_ids = dependencies
|
||||
|
||||
return res
|
||||
new_boms = super().copy(default)
|
||||
for old_bom, new_bom in zip(self, new_boms):
|
||||
if old_bom.operation_ids:
|
||||
operations_mapping = {}
|
||||
for original, copied in zip(old_bom.operation_ids, new_bom.operation_ids.sorted()):
|
||||
operations_mapping[original] = copied
|
||||
for bom_line in new_bom.bom_line_ids:
|
||||
if bom_line.operation_id:
|
||||
bom_line.operation_id = operations_mapping[bom_line.operation_id]
|
||||
for byproduct in new_bom.byproduct_ids:
|
||||
if byproduct.operation_id:
|
||||
byproduct.operation_id = operations_mapping[byproduct.operation_id]
|
||||
for operation in old_bom.operation_ids:
|
||||
if operation.blocked_by_operation_ids:
|
||||
copied_operation = operations_mapping[operation]
|
||||
dependencies = []
|
||||
for dependency in operation.blocked_by_operation_ids:
|
||||
dependencies.append(Command.link(operations_mapping[dependency].id))
|
||||
copied_operation.blocked_by_operation_ids = dependencies
|
||||
return new_boms
|
||||
|
||||
@api.model
|
||||
def name_create(self, name):
|
||||
# prevent to use string as product_tmpl_id
|
||||
if isinstance(name, str):
|
||||
key = 'default_' + self._rec_name
|
||||
if key in self.env.context:
|
||||
result = super().name_create(self.env.context[key])
|
||||
self.browse(result[0]).code = name
|
||||
return result
|
||||
raise UserError(_("You cannot create a new Bill of Material from here."))
|
||||
return super(MrpBom, self).name_create(name)
|
||||
|
||||
def toggle_active(self):
|
||||
self.with_context({'active_test': False}).operation_ids.toggle_active()
|
||||
return super().toggle_active()
|
||||
def action_archive(self):
|
||||
self.with_context(active_test=False).operation_ids.action_archive()
|
||||
return super().action_archive()
|
||||
|
||||
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]
|
||||
def action_unarchive(self):
|
||||
self.with_context(active_test=False).operation_ids.action_unarchive()
|
||||
return super().action_unarchive()
|
||||
|
||||
@api.depends('code')
|
||||
def _compute_display_name(self):
|
||||
for bom in self:
|
||||
display_name = f"{bom.code + ': ' if bom.code else ''}{bom.product_tmpl_id.display_name}"
|
||||
if self.env.context.get('display_bom_uom_qty') and (bom.product_qty > 1 or bom.product_uom_id != bom.product_tmpl_id.uom_id):
|
||||
display_name += f" ({bom.product_qty} {bom.product_uom_id.name})"
|
||||
bom.display_name = _('%(display_name)s', display_name=display_name)
|
||||
|
||||
@api.depends('operation_ids')
|
||||
def _compute_operation_count(self):
|
||||
for bom in self:
|
||||
bom.operation_count = len(bom.operation_ids)
|
||||
|
||||
def _compute_show_copy_operations_button(self):
|
||||
exist_operation = bool(self.env['mrp.routing.workcenter'].search_count([], limit=1))
|
||||
self.show_copy_operations_button = exist_operation
|
||||
|
||||
def action_compute_bom_days(self):
|
||||
company_id = self.env.context.get('default_company_id', self.env.company.id)
|
||||
warehouse = self.env['stock.warehouse'].search([('company_id', '=', company_id)], limit=1)
|
||||
for bom in self:
|
||||
bom_data = self.env['report.mrp.report_bom_structure'].with_context(minimized=True)._get_bom_data(bom, warehouse, bom.product_id, ignore_stock=True)
|
||||
bom.days_to_prepare_mo = self.env['report.mrp.report_bom_structure']._get_max_component_delay(bom_data['components'])
|
||||
if bom_data.get('availability_state') == 'unavailable' and not bom_data.get('components_available', True):
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Cannot compute days to prepare due to missing route info for at least 1 component or for the final product.'),
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
@api.constrains('product_tmpl_id', 'product_id', 'type')
|
||||
def check_kit_has_not_orderpoint(self):
|
||||
product_ids = [pid for bom in self.filtered(lambda bom: bom.type == "phantom")
|
||||
for pid in (bom.product_id.ids or bom.product_tmpl_id.product_variant_ids.ids)]
|
||||
if self.env['stock.warehouse.orderpoint'].search([('product_id', 'in', product_ids)], count=True):
|
||||
if self.env['stock.warehouse.orderpoint'].search_count([('product_id', 'in', product_ids)], limit=1):
|
||||
raise ValidationError(_("You can not create a kit-type bill of materials for products that have at least one reordering rule."))
|
||||
|
||||
@api.constrains('enable_batch_size', 'batch_size')
|
||||
def _check_valid_batch_size(self):
|
||||
if any(bom.enable_batch_size and bom.product_uom_id.compare(bom.batch_size, 0.0) <= 0 for bom in self):
|
||||
raise ValidationError(self.env._("The batch size must be positive!"))
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_running_mo(self):
|
||||
if self.env['mrp.production'].search([('bom_id', 'in', self.ids), ('state', 'not in', ['done', 'cancel'])], limit=1):
|
||||
if self.env['mrp.production'].search_count([('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.'))
|
||||
|
||||
@api.model
|
||||
def _bom_find_domain(self, products, picking_type=None, company_id=False, bom_type=False):
|
||||
domain = ['&', '|', ('product_id', 'in', products.ids), '&', ('product_id', '=', False), ('product_tmpl_id', 'in', products.product_tmpl_id.ids), ('active', '=', True)]
|
||||
domain = (
|
||||
Domain('product_id', 'in', products.ids) | (
|
||||
Domain('product_id', '=', False) & Domain('product_tmpl_id', 'in', products.product_tmpl_id.ids)
|
||||
)
|
||||
) & Domain('active', '=', True)
|
||||
if company_id or self.env.context.get('company_id'):
|
||||
domain = AND([domain, ['|', ('company_id', '=', False), ('company_id', '=', company_id or self.env.context.get('company_id'))]])
|
||||
domain &= Domain('company_id', 'in', [False, company_id or self.env.context.get('company_id')])
|
||||
if picking_type:
|
||||
domain = AND([domain, ['|', ('picking_type_id', '=', picking_type.id), ('picking_type_id', '=', False)]])
|
||||
domain &= Domain('picking_type_id', 'in', [picking_type.id, False])
|
||||
if bom_type:
|
||||
domain = AND([domain, [('type', '=', bom_type)]])
|
||||
domain &= Domain('type', '=', bom_type)
|
||||
return domain
|
||||
|
||||
@api.model
|
||||
|
|
@ -318,29 +413,12 @@ class MrpBom(models.Model):
|
|||
|
||||
return bom_by_product
|
||||
|
||||
def explode(self, product, quantity, picking_type=False):
|
||||
def explode(self, product, quantity, picking_type=False, never_attribute_values=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
|
||||
"""
|
||||
from collections import defaultdict
|
||||
|
||||
graph = defaultdict(list)
|
||||
V = set()
|
||||
|
||||
def check_cycle(v, visited, recStack, graph):
|
||||
visited[v] = True
|
||||
recStack[v] = True
|
||||
for neighbour in graph[v]:
|
||||
if visited[neighbour] == False:
|
||||
if check_cycle(neighbour, visited, recStack, graph) == True:
|
||||
return True
|
||||
elif recStack[neighbour] == True:
|
||||
return True
|
||||
recStack[v] = False
|
||||
return False
|
||||
|
||||
product_ids = set()
|
||||
product_boms = {}
|
||||
def update_product_boms():
|
||||
|
|
@ -351,15 +429,12 @@ class MrpBom(models.Model):
|
|||
for product in products:
|
||||
product_boms.setdefault(product, self.env['mrp.bom'])
|
||||
|
||||
boms_done = [(self, {'qty': quantity, 'product': product, 'original_qty': quantity, 'parent_line': False})]
|
||||
boms_done = [(self, self.env['mrp.bom.line']._prepare_bom_done_values(quantity, product, quantity, []))]
|
||||
lines_done = []
|
||||
V |= set([product.product_tmpl_id.id])
|
||||
|
||||
bom_lines = []
|
||||
for bom_line in self.bom_line_ids:
|
||||
product_id = bom_line.product_id
|
||||
V |= set([product_id.product_tmpl_id.id])
|
||||
graph[product.product_tmpl_id.id].append(product_id.product_tmpl_id.id)
|
||||
bom_lines.append((bom_line, product, quantity, False))
|
||||
product_ids.add(product_id.id)
|
||||
update_product_boms()
|
||||
|
|
@ -368,34 +443,36 @@ class MrpBom(models.Model):
|
|||
current_line, current_product, current_qty, parent_line = bom_lines[0]
|
||||
bom_lines = bom_lines[1:]
|
||||
|
||||
if current_line._skip_bom_line(current_product):
|
||||
if current_line._skip_bom_line(current_product, never_attribute_values):
|
||||
continue
|
||||
|
||||
line_quantity = current_qty * current_line.product_qty
|
||||
if not current_line.product_id in product_boms:
|
||||
if current_line.product_id not in product_boms:
|
||||
update_product_boms()
|
||||
product_ids.clear()
|
||||
bom = product_boms.get(current_line.product_id)
|
||||
if bom:
|
||||
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]
|
||||
converted_line_quantity = current_line.product_uom_id._compute_quantity(
|
||||
line_quantity / bom.product_qty, bom.product_uom_id, round=False
|
||||
)
|
||||
bom_lines = [(line, current_line.product_id, converted_line_quantity, current_line) for line in bom.bom_line_ids] + bom_lines
|
||||
for bom_line in bom.bom_line_ids:
|
||||
graph[current_line.product_id.product_tmpl_id.id].append(bom_line.product_id.product_tmpl_id.id)
|
||||
if bom_line.product_id.product_tmpl_id.id in V and check_cycle(bom_line.product_id.product_tmpl_id.id, {key: False for key in V}, {key: False for key in V}, graph):
|
||||
raise UserError(_('Recursion error! A product with a Bill of Material should not have itself in its BoM or child BoMs!'))
|
||||
V |= set([bom_line.product_id.product_tmpl_id.id])
|
||||
if not bom_line.product_id in product_boms:
|
||||
if bom_line.product_id not in product_boms:
|
||||
product_ids.add(bom_line.product_id.id)
|
||||
boms_done.append((bom, {'qty': converted_line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': current_line}))
|
||||
boms_done.append((bom, current_line._prepare_bom_done_values(converted_line_quantity, current_product, quantity, boms_done)))
|
||||
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}))
|
||||
line_quantity = current_line.product_uom_id.round(line_quantity, rounding_method='UP')
|
||||
lines_done.append((current_line, current_line._prepare_line_done_values(line_quantity, current_product, quantity, parent_line, boms_done)))
|
||||
|
||||
lines_done = self._round_last_line_done(lines_done)
|
||||
return boms_done, lines_done
|
||||
|
||||
@api.model
|
||||
def _round_last_line_done(self, lines_done):
|
||||
return lines_done
|
||||
|
||||
@api.model
|
||||
def get_import_templates(self):
|
||||
return [{
|
||||
|
|
@ -403,6 +480,189 @@ class MrpBom(models.Model):
|
|||
'template': '/mrp/static/xls/mrp_bom.xls'
|
||||
}]
|
||||
|
||||
def _set_outdated_bom_in_productions(self):
|
||||
if not self:
|
||||
return
|
||||
# Searches for MOs using these BoMs to notify them that their BoM has been updated.
|
||||
list_of_domain_by_bom = []
|
||||
for bom in self:
|
||||
if bom.product_id:
|
||||
domain_by_products = Domain('product_id', '=', bom.product_id.id)
|
||||
else:
|
||||
domain_by_products = Domain('product_id', 'in', bom.product_tmpl_id.product_variant_ids.ids)
|
||||
domain_for_confirmed_mo = Domain('state', '=', 'confirmed') & domain_by_products
|
||||
# Avoid confirmed MOs if the BoM's product was changed.
|
||||
domain_by_states = Domain('state', '=', 'draft') | domain_for_confirmed_mo
|
||||
list_of_domain_by_bom.append(Domain('bom_id', '=', bom.id) & domain_by_states)
|
||||
productions = self.env['mrp.production'].search(Domain.OR(list_of_domain_by_bom))
|
||||
if productions:
|
||||
productions.is_outdated_bom = True
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# CATALOG
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _get_action_add_from_catalog_extra_context(self):
|
||||
return {
|
||||
**super()._get_action_add_from_catalog_extra_context(),
|
||||
'product_catalog_currency_id': self.env.company.currency_id.id,
|
||||
}
|
||||
|
||||
def _default_order_line_values(self, child_field=False):
|
||||
default_data = super()._default_order_line_values(child_field)
|
||||
new_default_data = self[child_field]._get_product_catalog_lines_data(default=True)
|
||||
|
||||
return {**default_data, **new_default_data}
|
||||
|
||||
def _get_product_catalog_order_data(self, products, **kwargs):
|
||||
product_catalog = super()._get_product_catalog_order_data(products, **kwargs)
|
||||
for product in products:
|
||||
product_catalog[product.id] |= self._get_product_price_and_data(product)
|
||||
return product_catalog
|
||||
|
||||
def _get_product_price_and_data(self, product):
|
||||
self.ensure_one()
|
||||
return {'price': product.standard_price}
|
||||
|
||||
def _get_product_catalog_record_lines(self, product_ids, *, child_field=False, **kwargs):
|
||||
if not child_field:
|
||||
return {}
|
||||
lines = self[child_field].filtered(lambda line: line.product_id.id in product_ids)
|
||||
return lines.grouped('product_id')
|
||||
|
||||
def _update_order_line_info(self, product_id, quantity, *, child_field=False, **kwargs):
|
||||
if not child_field:
|
||||
return 0
|
||||
entity = self[child_field].filtered(lambda line: line.product_id.id == product_id)
|
||||
if entity:
|
||||
if quantity != 0:
|
||||
entity.product_qty = quantity
|
||||
else:
|
||||
entity.unlink()
|
||||
elif quantity > 0:
|
||||
command = Command.create({
|
||||
'product_qty': quantity,
|
||||
'product_id': product_id,
|
||||
})
|
||||
self.write({child_field: [command]})
|
||||
|
||||
return self.env['product.product'].browse(product_id).standard_price
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# DOCUMENT
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _get_mail_thread_data_attachments(self):
|
||||
res = super()._get_mail_thread_data_attachments()
|
||||
return res | self._get_extra_attachments()
|
||||
|
||||
def _get_extra_attachments(self):
|
||||
is_byproduct = self.env.user.has_group('mrp.group_mrp_byproducts')
|
||||
product_ids, template_ids = OrderedSet(), OrderedSet()
|
||||
for bom in self:
|
||||
product_ids.add(bom.product_id.id)
|
||||
template_ids.add(bom.product_tmpl_id.id)
|
||||
if is_byproduct:
|
||||
product_ids.update(bom.byproduct_ids.product_id.ids)
|
||||
template_ids.update(bom.byproduct_ids.product_id.product_tmpl_id.ids)
|
||||
|
||||
domain = Domain('attached_on_mrp', '=', 'bom') & (
|
||||
(Domain('res_model', '=', 'product.product') & Domain('res_id', 'in', product_ids))
|
||||
| (Domain('res_model', '=', 'product.template') & Domain('res_id', 'in', template_ids))
|
||||
)
|
||||
attachements = self.env['product.document'].search(domain).ir_attachment_id
|
||||
return attachements
|
||||
|
||||
@api.model
|
||||
def _skip_for_no_variant(self, product, bom_attribule_values, never_attribute_values=False):
|
||||
""" Controls if a Component/Operation/Byproduct line should be skipped based on the 'no_variant' attributes
|
||||
Cases:
|
||||
- no_variant:
|
||||
1. attribute present on the line
|
||||
=> need to be at least one attribute value matching between the one passed as args and the ones one the line
|
||||
2. attribute not present on the line
|
||||
=> valid if the line has no attribute value selected for that attribute
|
||||
- always and dynamic: match_all_variant_values()
|
||||
"""
|
||||
no_variant_bom_attributes = bom_attribule_values.filtered(lambda av: av.attribute_id.create_variant == 'no_variant')
|
||||
|
||||
# Attributes create_variant 'always' and 'dynamic'
|
||||
other_attribute_valid = product._match_all_variant_values(bom_attribule_values - no_variant_bom_attributes)
|
||||
|
||||
# If there are no never attribute values on the line => 'always' and 'dynamic'
|
||||
if not no_variant_bom_attributes:
|
||||
return not other_attribute_valid
|
||||
|
||||
# Or if there are never attribute on the line values but no value is passed => impossible to match
|
||||
if not never_attribute_values:
|
||||
return True
|
||||
|
||||
bom_values_by_attribute = no_variant_bom_attributes.grouped('attribute_id')
|
||||
never_values_by_attribute = never_attribute_values.grouped('attribute_id')
|
||||
|
||||
# Or if there is no overlap between given line values attributes and the ones on on the bom
|
||||
if not any(never_att_id in no_variant_bom_attributes.attribute_id.ids for never_att_id in never_attribute_values.attribute_id.ids):
|
||||
return True
|
||||
|
||||
# Check that at least one variant attribute is correct
|
||||
for attribute, values in bom_values_by_attribute.items():
|
||||
if never_values_by_attribute.get(attribute) and any(val.id in never_values_by_attribute[attribute].ids for val in values):
|
||||
return not other_attribute_valid
|
||||
|
||||
# None were found, so we skip the line
|
||||
return True
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# REPLENISHMENT WIZARD
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _compute_show_set_bom_button(self):
|
||||
self.show_set_bom_button = True
|
||||
orderpoint_id = self.env.context.get('orderpoint_id', self.env.context.get('default_orderpoint_id'))
|
||||
if orderpoint_id:
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].browse(orderpoint_id)
|
||||
self.filtered(
|
||||
lambda s: s.id == orderpoint.bom_id.id
|
||||
).show_set_bom_button = False
|
||||
|
||||
def action_set_bom_on_orderpoint(self):
|
||||
self.ensure_one()
|
||||
orderpoint_id = self.env.context.get('orderpoint_id')
|
||||
if not orderpoint_id:
|
||||
return
|
||||
orderpoint = self.env['stock.warehouse.orderpoint'].browse(orderpoint_id)
|
||||
if 'manufacture' not in orderpoint.route_id.rule_ids.mapped('action'):
|
||||
domain = Domain.AND([
|
||||
[('action', '=', 'manufacture')],
|
||||
Domain.OR([
|
||||
[('company_id', '=', orderpoint.company_id.id)],
|
||||
[('company_id', '=', False)],
|
||||
]),
|
||||
])
|
||||
orderpoint.route_id = self.env['stock.rule'].search(domain, limit=1).route_id.id
|
||||
orderpoint.bom_id = self
|
||||
bom_qty = self.product_uom_id._compute_quantity(self.product_qty, orderpoint.product_id.uom_id)
|
||||
if orderpoint.qty_to_order < bom_qty:
|
||||
orderpoint.qty_to_order = bom_qty
|
||||
return orderpoint.action_stock_replenishment_info()
|
||||
|
||||
def action_open_operation_form(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'mrp.routing.workcenter',
|
||||
'context': {
|
||||
'default_bom_id': self.id,
|
||||
'search_default_bom_id': self.id,
|
||||
'bom_id_invisible': True,
|
||||
},
|
||||
}
|
||||
|
||||
def action_copy_existing_operations(self):
|
||||
self.ensure_one()
|
||||
return self.env['mrp.routing.workcenter'].with_context(bom_id=self.id).copy_existing_operations()
|
||||
|
||||
|
||||
class MrpBomLine(models.Model):
|
||||
_name = 'mrp.bom.line'
|
||||
|
|
@ -414,19 +674,16 @@ class MrpBomLine(models.Model):
|
|||
def _get_default_product_uom_id(self):
|
||||
return self.env['uom.uom'].search([], limit=1, order='id').id
|
||||
|
||||
product_id = fields.Many2one('product.product', 'Component', required=True, check_company=True)
|
||||
product_id = fields.Many2one('product.product', 'Component', required=True, check_company=True, index=True)
|
||||
product_tmpl_id = fields.Many2one('product.template', 'Product Template', related='product_id.product_tmpl_id', store=True, index=True)
|
||||
company_id = fields.Many2one(
|
||||
related='bom_id.company_id', store=True, index=True, readonly=True)
|
||||
product_qty = fields.Float(
|
||||
'Quantity', default=1.0,
|
||||
digits='Product Unit of Measure', required=True)
|
||||
digits='Product Unit', required=True)
|
||||
product_uom_id = fields.Many2one(
|
||||
'uom.uom', 'Product Unit of Measure',
|
||||
default=_get_default_product_uom_id,
|
||||
required=True,
|
||||
help="Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control", domain="[('category_id', '=', product_uom_category_id)]")
|
||||
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
|
||||
'uom.uom', 'Unit',
|
||||
default=_get_default_product_uom_id, required=True)
|
||||
sequence = fields.Integer(
|
||||
'Sequence', default=1,
|
||||
help="Gives the sequence order when displaying.")
|
||||
|
|
@ -451,29 +708,11 @@ class MrpBomLine(models.Model):
|
|||
compute='_compute_child_line_ids')
|
||||
attachments_count = fields.Integer('Attachments Count', compute='_compute_attachments_count')
|
||||
tracking = fields.Selection(related='product_id.tracking')
|
||||
manual_consumption = fields.Boolean(
|
||||
'Manual Consumption', default=False, compute='_compute_manual_consumption',
|
||||
readonly=False, store=True, copy=True,
|
||||
help="When activated, then the registration of consumption for that component is recorded manually exclusively.\n"
|
||||
"If not activated, and any of the components consumption is edited manually on the manufacturing order, Odoo assumes manual consumption also.")
|
||||
manual_consumption_readonly = fields.Boolean(
|
||||
'Manual Consumption Readonly', compute='_compute_manual_consumption_readonly')
|
||||
|
||||
_sql_constraints = [
|
||||
('bom_qty_zero', 'CHECK (product_qty>=0)', 'All product quantities must be greater or equal to 0.\n'
|
||||
'Lines with 0 quantities can be used as optional lines. \n'
|
||||
'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
|
||||
]
|
||||
|
||||
@api.depends('product_id', 'tracking', 'operation_id')
|
||||
def _compute_manual_consumption(self):
|
||||
for line in self:
|
||||
line.manual_consumption = (line.tracking != 'none' or line.operation_id)
|
||||
|
||||
@api.depends('tracking', 'operation_id')
|
||||
def _compute_manual_consumption_readonly(self):
|
||||
for line in self:
|
||||
line.manual_consumption_readonly = (line.tracking != 'none' or line.operation_id)
|
||||
_bom_qty_zero = models.Constraint(
|
||||
'CHECK (product_qty>=0)',
|
||||
'All product quantities must be greater or equal to 0.\nLines with 0 quantities can be used as optional lines. \nYou should install the mrp_byproduct module if you want to manage extra products on BoMs!',
|
||||
)
|
||||
|
||||
@api.depends('product_id', 'bom_id')
|
||||
def _compute_child_bom_id(self):
|
||||
|
|
@ -488,10 +727,11 @@ class MrpBomLine(models.Model):
|
|||
@api.depends('product_id')
|
||||
def _compute_attachments_count(self):
|
||||
for line in self:
|
||||
nbr_attach = self.env['mrp.document'].search_count([
|
||||
nbr_attach = self.env['product.document'].search_count([
|
||||
'&', '&', ('attached_on_mrp', '=', 'bom'), ('active', '=', 't'),
|
||||
'|',
|
||||
'&', ('res_model', '=', 'product.product'), ('res_id', '=', line.product_id.id),
|
||||
'&', ('res_model', '=', 'product.template'), ('res_id', '=', line.product_id.product_tmpl_id.id)])
|
||||
'&', ('res_model', '=', 'product.template'), ('res_id', '=', line.product_tmpl_id.id)])
|
||||
line.attachments_count = nbr_attach
|
||||
|
||||
@api.depends('child_bom_id')
|
||||
|
|
@ -500,16 +740,6 @@ class MrpBomLine(models.Model):
|
|||
for line in self:
|
||||
line.child_line_ids = line.child_bom_id.bom_line_ids.ids or False
|
||||
|
||||
@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:
|
||||
|
|
@ -522,40 +752,91 @@ class MrpBomLine(models.Model):
|
|||
values['product_uom_id'] = self.env['product.product'].browse(values['product_id']).uom_id.id
|
||||
return super(MrpBomLine, self).create(vals_list)
|
||||
|
||||
def _skip_bom_line(self, product):
|
||||
""" Control if a BoM line should be produced, can be inherited to add
|
||||
custom control.
|
||||
def _skip_bom_line(self, product, never_attribute_values=False):
|
||||
""" Control if a BoM line should be produced, can be inherited to add custom control.
|
||||
cases:
|
||||
- no_variant:
|
||||
1. attribute present on the line
|
||||
=> need to be at least one attribute value matching between the one passed as args and the ones one the line
|
||||
2. attribute not present on the line
|
||||
=> valid if the line has no attribute value selected for that attribute
|
||||
- always and dynamic: match_all_variant_values()
|
||||
"""
|
||||
self.ensure_one()
|
||||
if product._name == 'product.template':
|
||||
if not product or product._name == 'product.template':
|
||||
return False
|
||||
return not product._match_all_variant_values(self.bom_product_template_attribute_value_ids)
|
||||
|
||||
return self.env['mrp.bom']._skip_for_no_variant(product, self.bom_product_template_attribute_value_ids, never_attribute_values)
|
||||
|
||||
def action_see_attachments(self):
|
||||
domain = [
|
||||
'&', ('attached_on_mrp', '=', 'bom'),
|
||||
'|',
|
||||
'&', ('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')
|
||||
attachments = self.env['product.document'].search(domain)
|
||||
nbr_product_attach = len(attachments.filtered(lambda a: a.res_model == 'product.product'))
|
||||
nbr_template_attach = len(attachments.filtered(lambda a: a.res_model == 'product.template'))
|
||||
context = {'default_res_model': 'product.product',
|
||||
'default_res_id': self.product_id.id,
|
||||
'default_company_id': self.company_id.id,
|
||||
'attached_on_bom': True,
|
||||
'search_default_context_variant': not (nbr_product_attach == 0 and nbr_template_attach > 0) if self.env.user.has_group('product.group_product_variant') else False
|
||||
}
|
||||
|
||||
return {
|
||||
'name': _('Attachments'),
|
||||
'domain': domain,
|
||||
'res_model': 'mrp.document',
|
||||
'res_model': 'product.document',
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_id': attachment_view.id,
|
||||
'views': [(attachment_view.id, 'kanban'), (False, 'form')],
|
||||
'view_mode': 'kanban,tree,form',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'target': 'current',
|
||||
'help': _('''<p class="o_view_nocontent_smiling_face">
|
||||
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, 'default_company_id': %s}" % ('product.product', self.product_id.id, self.company_id.id)
|
||||
'context': context,
|
||||
'search_view_id': self.env.ref('product.product_document_search').ids
|
||||
}
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# CATALOG
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
class MrpByProduct(models.Model):
|
||||
def action_add_from_catalog(self):
|
||||
bom = self.env['mrp.bom'].browse(self.env.context.get('order_id'))
|
||||
return bom.with_context(child_field='bom_line_ids').action_add_from_catalog()
|
||||
|
||||
def _get_product_catalog_lines_data(self, default=False, **kwargs):
|
||||
if self and not default:
|
||||
self.product_id.ensure_one()
|
||||
return {
|
||||
**self[0].bom_id._get_product_price_and_data(self[0].product_id),
|
||||
'quantity': sum(
|
||||
self.mapped(
|
||||
lambda line: line.product_uom_id._compute_quantity(
|
||||
qty=line.product_qty,
|
||||
to_unit=line.product_uom_id,
|
||||
)
|
||||
)
|
||||
),
|
||||
'readOnly': len(self) > 1,
|
||||
'uomDisplayName': len(self) == 1 and self.product_uom_id.display_name or self.product_id.uom_id.display_name,
|
||||
}
|
||||
return {
|
||||
'quantity': 0,
|
||||
}
|
||||
|
||||
def _prepare_bom_done_values(self, quantity, product, original_quantity, boms_done):
|
||||
return {'qty': quantity, 'product': product, 'original_qty': original_quantity, 'parent_line': self}
|
||||
|
||||
def _prepare_line_done_values(self, quantity, product, original_quantity, parent_line, boms_done):
|
||||
return {'qty': quantity, 'product': product, 'original_qty': original_quantity, 'parent_line': parent_line}
|
||||
|
||||
|
||||
class MrpBomByproduct(models.Model):
|
||||
_name = 'mrp.bom.byproduct'
|
||||
_description = 'Byproduct'
|
||||
_rec_name = "product_id"
|
||||
|
|
@ -566,11 +847,9 @@ class MrpByProduct(models.Model):
|
|||
company_id = fields.Many2one(related='bom_id.company_id', store=True, index=True, readonly=True)
|
||||
product_qty = fields.Float(
|
||||
'Quantity',
|
||||
default=1.0, digits='Product Unit of Measure', required=True)
|
||||
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
|
||||
product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True,
|
||||
compute="_compute_product_uom_id", store=True, readonly=False, precompute=True,
|
||||
domain="[('category_id', '=', product_uom_category_id)]")
|
||||
default=1.0, digits='Product Unit', required=True)
|
||||
product_uom_id = fields.Many2one('uom.uom', 'Unit', required=True,
|
||||
compute="_compute_product_uom_id", store=True, readonly=False, precompute=True)
|
||||
bom_id = fields.Many2one('mrp.bom', 'BoM', ondelete='cascade', index=True)
|
||||
allowed_operation_ids = fields.One2many('mrp.routing.workcenter', related='bom_id.operation_ids')
|
||||
operation_id = fields.Many2one(
|
||||
|
|
@ -593,11 +872,40 @@ class MrpByProduct(models.Model):
|
|||
for record in self:
|
||||
record.product_uom_id = record.product_id.uom_id.id
|
||||
|
||||
def _skip_byproduct_line(self, product):
|
||||
def _skip_byproduct_line(self, product, never_attribute_values=False):
|
||||
""" Control if a byproduct line should be produced, can be inherited to add
|
||||
custom control.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if product._name == 'product.template':
|
||||
if not product or product._name == 'product.template':
|
||||
return False
|
||||
return not product._match_all_variant_values(self.bom_product_template_attribute_value_ids)
|
||||
|
||||
return self.env['mrp.bom']._skip_for_no_variant(product, self.bom_product_template_attribute_value_ids, never_attribute_values)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# CATALOG
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def action_add_from_catalog(self):
|
||||
bom = self.env['mrp.bom'].browse(self.env.context.get('order_id'))
|
||||
return bom.with_context(child_field='byproduct_ids').action_add_from_catalog()
|
||||
|
||||
def _get_product_catalog_lines_data(self, default=False, **kwargs):
|
||||
if self and not default:
|
||||
self.product_id.ensure_one()
|
||||
return {
|
||||
**self[0].bom_id._get_product_price_and_data(self[0].product_id),
|
||||
'quantity': sum(
|
||||
self.mapped(
|
||||
lambda line: line.product_uom_id._compute_quantity(
|
||||
qty=line.product_qty,
|
||||
to_unit=line.product_uom_id,
|
||||
)
|
||||
)
|
||||
),
|
||||
'readOnly': len(self) > 1,
|
||||
'uomDisplayName': len(self) == 1 and self.product_uom_id.display_name or self.product_id.uom_id.display_name,
|
||||
}
|
||||
return {
|
||||
'quantity': 0,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue