Accept Merge Request #2269: (feature/新增plm模块 -> develop)

Merge Request: 删除plm模块代码

Created By: @胡尧
Accepted By: @胡尧
URL: https://jikimo-hn.coding.net/p/jikimo_sfs/d/jikimo_sf/git/merge/2269?initial=true
This commit is contained in:
胡尧
2025-07-07 15:00:13 +08:00
committed by Coding
118 changed files with 0 additions and 140103 deletions

View File

@@ -1,5 +0,0 @@
# -*- encoding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import report

View File

@@ -1,41 +0,0 @@
# -*- encoding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Product Lifecycle Management (PLM)',
'version': '1.0',
'category': 'Manufacturing/Product Lifecycle Management (PLM)',
'sequence': 155,
'summary': """Manage engineering change orders on products, bills of material""",
'website': 'https://www.odoo.com/app/plm',
'depends': ['mrp'],
'description': """
Product Life Management
=======================
* Versioning of Bill of Materials and Products
* Different approval flows possible depending on the type of change order
""",
'data': [
'security/mrp_plm.xml',
'security/ir.model.access.csv',
'data/mail_activity_type_data.xml',
'data/mrp_data.xml',
'views/mrp_bom_views.xml',
'views/mrp_document_views.xml',
'views/mrp_eco_views.xml',
'views/product_views.xml',
'views/mrp_production_views.xml',
'report/mrp_report_bom_structure.xml',
],
'application': True,
'license': 'OEEL-1',
'assets': {
'web.assets_backend': [
'mrp_plm/static/src/**/*.js',
'mrp_plm/static/src/**/*.scss',
'mrp_plm/static/src/**/*.xml',
],
}
}

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mail_activity_eco_approval" model="mail.activity.type">
<field name="name">ECO Approval</field>
<field name="res_model">mrp.eco</field>
</record>
</odoo>

View File

@@ -1,44 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Eco type by default -->
<record id="ecotype0" model="mrp.eco.type">
<field name="name">New Product Introduction</field>
</record>
<!-- ECO Stage records -->
<record id="ecostage_new" model="mrp.eco.stage">
<field name="name">New</field>
<field name="type_ids" eval="[(4, ref('ecotype0'))]"/>
<field name="folded" eval="False"/>
</record>
<record id="ecostage_progress" model="mrp.eco.stage">
<field name="name">In Progress</field>
<field name="type_ids" eval="[(4, ref('ecotype0'))]"/>
<field name="folded" eval="False"/>
</record>
<record id="ecostage_validated" model="mrp.eco.stage">
<field name="name">Validated</field>
<field name="type_ids" eval="[(4, ref('ecotype0'))]"/>
<field name="folded" eval="False"/>
<field name="allow_apply_change" eval="True"/>
</record>
<record id="ecostage_effective" model="mrp.eco.stage">
<field name="name">Effective</field>
<field name="type_ids" eval="[(4, ref('ecotype0'))]"/>
<field name="folded" eval="True"/>
<field name="allow_apply_change" eval="True"/>
<field name="final_stage" eval="True"/>
</record>
<!-- ECO sequence -->
<record id="seq_eco" model="ir.sequence">
<field name="name">Engineering Change Order</field>
<field name="code">mrp.eco</field>
<field name="prefix">ECO</field>
<field name="padding">4</field>
<field name="company_id" eval="False"/>
</record>
</data>
</odoo>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mrp_plm
#
# Translators:
# Martin Trigaux <mat@odoo.com>, 2017
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.saas~18+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-10-02 11:50+0000\n"
"PO-Revision-Date: 2017-10-02 11:50+0000\n"
"Last-Translator: Martin Trigaux <mat@odoo.com>, 2017\n"
"Language-Team: French (Canada) (https://www.transifex.com/odoo/teams/41243/"
"fr_CA/)\n"
"Language: fr_CA\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#. module: mrp_plm
#: model:ir.model.fields,field_description:mrp_plm.field_mrp_eco__id
#: model:ir.model.fields,field_description:mrp_plm.field_mrp_eco_approval__id
#: model:ir.model.fields,field_description:mrp_plm.field_mrp_eco_approval_template__id
#: model:ir.model.fields,field_description:mrp_plm.field_mrp_eco_bom_change__id
#: model:ir.model.fields,field_description:mrp_plm.field_mrp_eco_routing_change__id
#: model:ir.model.fields,field_description:mrp_plm.field_mrp_eco_stage__id
#: model:ir.model.fields,field_description:mrp_plm.field_mrp_eco_tag__id
#: model:ir.model.fields,field_description:mrp_plm.field_mrp_eco_type__id
msgid "ID"
msgstr "Identifiant"
#. module: mrp_plm
#: model:ir.model.fields,field_description:mrp_plm.field_mrp_eco__product_tmpl_id
#: model:ir.model.fields,field_description:mrp_plm.field_mrp_eco_bom_change__product_id
#: model_terms:ir.ui.view,arch_db:mrp_plm.mrp_bom_view_kanban
#: model_terms:ir.ui.view,arch_db:mrp_plm.mrp_eco_bom_change_view_form
#: model_terms:ir.ui.view,arch_db:mrp_plm.mrp_eco_search
msgid "Product"
msgstr "Produit"
#. module: mrp_plm
#: model:ir.model,name:mrp_plm.model_product_template
msgid "Product Template"
msgstr "Modèle de produit"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
# -*- encoding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mrp_bom
from . import mrp_document
from . import mrp_eco
from . import mrp_production
from . import product

View File

@@ -1,179 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class MrpBom(models.Model):
_inherit = 'mrp.bom'
version = fields.Integer('Version', default=1, readonly=True)
previous_bom_id = fields.Many2one('mrp.bom', 'Previous BoM')
active = fields.Boolean('Production Ready')
image_128 = fields.Image(related='product_tmpl_id.image_128', readonly=False)
eco_ids = fields.One2many(
'mrp.eco', 'new_bom_id', 'ECO to be applied')
eco_count = fields.Integer('# ECOs', compute='_compute_eco_data')
eco_inprogress_count = fields.Integer("# ECOs in progress", compute='_compute_eco_data')
def _compute_eco_data(self):
self.eco_inprogress_count = 0 # not used
previous_boms_mapping = self._get_previous_boms()
previous_boms_list = list(previous_boms_mapping.keys())
eco_data = self.env['mrp.eco'].read_group([
('bom_id', 'in', previous_boms_list),
('stage_id.folded', '=', False)],
['bom_id'], ['bom_id'])
eco_count = dict((bom.id, 0) for bom in self)
for eco in eco_data:
previous_bom_id = eco['bom_id'][0]
previous_bom_eco_count = eco['bom_id_count']
for bom_id in previous_boms_mapping[previous_bom_id]:
eco_count[bom_id] += previous_bom_eco_count
for bom in self:
bom.eco_count = eco_count[bom.id]
def apply_new_version(self):
""" Put old BoM as deprecated - TODO: Set to stage that is production_ready """
MrpEco = self.env['mrp.eco']
for new_bom in self:
new_bom.write({'active': True})
# Move eco's into rebase state which is in progress state.
ecos = MrpEco.search(['|',
('bom_id', '=', new_bom.previous_bom_id.id),
('current_bom_id', '=', new_bom.previous_bom_id.id),
('new_bom_id', '!=', False),
('new_bom_id', '!=', new_bom.id),
('state', 'not in', ('done', 'new'))])
ecos.write({'state': 'rebase', 'current_bom_id': new_bom.id})
# Change old bom of eco which is in draft state.
draft_ecos = MrpEco.search(['|',
('bom_id', '=', new_bom.previous_bom_id.id),
('current_bom_id', '=', new_bom.previous_bom_id.id),
('new_bom_id', '=', False)])
draft_ecos.write({'bom_id': new_bom.id})
# Deactivate previous revision of BoM
new_bom.previous_bom_id.write({'active': False})
return True
def button_mrp_eco(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("mrp_plm.mrp_eco_action_main")
previous_boms = self._get_previous_boms()
action['domain'] = [('bom_id', 'in', list(previous_boms.keys()))]
action['context'] = {
'default_bom_id': self.id,
'default_product_tmpl_id': self.product_tmpl_id.id,
'default_type': 'bom'
}
return action
def _get_previous_boms(self):
""" Return a dictionary with the keys to be all the previous boms' id and
the value to be a set of ids in self of which the key is their previous boms.
"""
boms_data = self.with_context(active_test=False).search_read(
[('product_tmpl_id', 'in', self.product_tmpl_id.ids)],
fields=['id', 'previous_bom_id'], load=False,
order='id desc, version desc')
previous_boms = dict((bom.id, {bom.id}) for bom in self)
for bom_data in boms_data:
if not bom_data['previous_bom_id']:
continue
bom_id = bom_data['id']
previous_bom_id = bom_data['previous_bom_id']
previous_boms[previous_bom_id] = previous_boms.get(bom_id, set()) | previous_boms.get(previous_bom_id, set())
return previous_boms
def _get_active_version(self):
self.ensure_one()
boms = self.with_context(active_test=False).search([
('product_id', '=', self.product_id.id),
('version', '>', self.version)], order='version')
previous_boms = self
for bom in boms:
if bom.previous_bom_id not in previous_boms:
continue
previous_boms += bom
if bom.active:
return bom
return False
class MrpBomLine(models.Model):
_inherit = 'mrp.bom.line'
def _prepare_rebase_line(self, eco, change_type, product_id, uom_id, operation_id=None, new_qty=0):
self.ensure_one()
return {
'change_type': change_type,
'product_id': product_id,
'rebase_id': eco.id,
'old_uom_id': self.product_uom_id.id,
'new_uom_id': uom_id,
'old_operation_id': self.operation_id.id,
'new_operation_id': operation_id,
'old_product_qty': 0.0 if change_type == 'add' else self.product_qty,
'new_product_qty': new_qty,
}
def _create_or_update_rebase_line(self, ecos, operation, product_id, uom_id, operation_id=None, new_qty=0):
self.ensure_one()
BomChange = self.env['mrp.eco.bom.change']
for eco in ecos:
# When product exist in new bill of material update line otherwise add line in rebase changes.
rebase_line = BomChange.search([
('product_id', '=', product_id),
('rebase_id', '=', eco.id)], limit=1)
if rebase_line:
# Update existing rebase line or unlink it.
if (rebase_line.old_product_qty, rebase_line.old_uom_id.id, rebase_line.old_operation_id.id) != (new_qty, uom_id, operation_id):
if rebase_line.change_type == 'update':
rebase_line.write({'new_product_qty': new_qty, 'new_operation_id': operation_id, 'new_uom_id': uom_id})
else:
rebase_line_vals = self._prepare_rebase_line(eco, 'add', product_id, uom_id, operation_id, new_qty)
rebase_line.write(rebase_line_vals)
else:
rebase_line.unlink()
else:
rebase_line_vals = self._prepare_rebase_line(eco, operation, product_id, uom_id, operation_id, new_qty)
BomChange.create(rebase_line_vals)
eco.state = 'rebase' if eco.bom_rebase_ids or eco.previous_change_ids else 'progress'
return True
def bom_line_change(self, vals, operation='update'):
MrpEco = self.env['mrp.eco']
for line in self:
ecos = MrpEco.search([
('bom_id', '=', line.bom_id.id), ('state', 'in', ('progress', 'rebase')),
('type', 'in', ('bom', 'both'))
])
if ecos:
# Latest bom line (product, uom, operation_id, product_qty)
product_id = vals.get('product_id', line.product_id.id)
uom_id = vals.get('product_uom_id', line.product_uom_id.id)
operation_id = vals.get('operation_id', line.operation_id.id)
product_qty = vals.get('product_qty', line.product_qty)
line._create_or_update_rebase_line(ecos, operation, product_id, uom_id, operation_id, product_qty)
return True
@api.model_create_multi
def create(self, vals_list):
lines = super().create(vals_list)
for line, vals in zip(lines, vals_list):
line.bom_line_change(vals, operation='add')
return lines
def write(self, vals):
operation = 'update'
if vals.get('product_id'):
# It will create update rebase line with negative quantity.
self.bom_line_change({'product_qty': 0.0}, operation)
operation = 'add'
self.bom_line_change(vals, operation)
return super(MrpBomLine, self).write(vals)
def unlink(self):
# It will create update rebase line.
self.bom_line_change({'product_qty': 0.0})
return super(MrpBomLine, self).unlink()

View File

@@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class MrpDocument(models.Model):
_inherit = 'mrp.document'
origin_attachment_id = fields.Many2one('ir.attachment')

View File

@@ -1,834 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from markupsafe import escape
from random import randint
import ast
from odoo import api, fields, models, tools, Command, SUPERUSER_ID, _
from odoo.exceptions import UserError
class MrpEcoType(models.Model):
_name = "mrp.eco.type"
_description = 'ECO Type'
_inherit = ['mail.alias.mixin', 'mail.thread']
_order = "sequence, id"
name = fields.Char('Name', required=True, translate=True)
sequence = fields.Integer('Sequence')
nb_ecos = fields.Integer('ECOs', compute='_compute_nb')
nb_approvals = fields.Integer('Waiting Approvals', compute='_compute_nb')
nb_approvals_my = fields.Integer('Waiting my Approvals', compute='_compute_nb')
nb_validation = fields.Integer('To Apply', compute='_compute_nb')
color = fields.Integer('Color', default=1)
stage_ids = fields.Many2many('mrp.eco.stage', 'mrp_eco_stage_type_rel', 'type_id', 'stage_id', string='Stages')
def _compute_nb(self):
# TDE FIXME: this seems not good for performances, to check (replace by read_group later on)
MrpEco = self.env['mrp.eco']
for eco_type in self:
eco_type.nb_ecos = MrpEco.search_count([
('type_id', '=', eco_type.id), ('state', '!=', 'done')
])
eco_type.nb_validation = MrpEco.search_count([
('type_id', '=', eco_type.id),
('stage_id.allow_apply_change', '=', True),
('state', '=', 'progress')
])
eco_type.nb_approvals = MrpEco.search_count([
('type_id', '=', eco_type.id),
('approval_ids.status', '=', 'none')
])
eco_type.nb_approvals_my = MrpEco.search_count([
('type_id', '=', eco_type.id),
('approval_ids.status', '=', 'none'),
('approval_ids.required_user_ids', '=', self.env.user.id)
])
def _alias_get_creation_values(self):
values = super(MrpEcoType, self)._alias_get_creation_values()
values['alias_model_id'] = self.env['ir.model']._get('mrp.eco').id
if self.id:
values['alias_defaults'] = defaults = ast.literal_eval(self.alias_defaults or "{}")
defaults['type_id'] = self.id
return values
class MrpEcoApprovalTemplate(models.Model):
_name = "mrp.eco.approval.template"
_order = "sequence"
_description = 'ECO Approval Template'
name = fields.Char('Role', required=True)
sequence = fields.Integer('Sequence')
approval_type = fields.Selection([
('optional', 'Approves, but the approval is optional'),
('mandatory', 'Is required to approve'),
('comment', 'Comments only')], 'Approval Type',
default='mandatory', required=True)
user_ids = fields.Many2many('res.users', string='Users', domain=lambda self: [('groups_id', 'in', self.env.ref('mrp_plm.group_plm_user').id)], required=True)
stage_id = fields.Many2one('mrp.eco.stage', 'Stage', required=True)
class MrpEcoApproval(models.Model):
_name = "mrp.eco.approval"
_description = 'ECO Approval'
_order = 'approval_date desc'
eco_id = fields.Many2one(
'mrp.eco', 'ECO',
ondelete='cascade', required=True)
approval_template_id = fields.Many2one(
'mrp.eco.approval.template', 'Template',
ondelete='cascade', required=True)
name = fields.Char('Role', related='approval_template_id.name', store=True, readonly=False)
user_id = fields.Many2one(
'res.users', 'Approved by')
required_user_ids = fields.Many2many(
'res.users', string='Requested Users', related='approval_template_id.user_ids', readonly=False)
template_stage_id = fields.Many2one(
'mrp.eco.stage', 'Approval Stage',
related='approval_template_id.stage_id', store=True, readonly=False)
eco_stage_id = fields.Many2one(
'mrp.eco.stage', 'ECO Stage',
related='eco_id.stage_id', store=True, readonly=False)
status = fields.Selection([
('none', 'Not Yet'),
('comment', 'Commented'),
('approved', 'Approved'),
('rejected', 'Rejected')], string='Status',
default='none', required=True)
approval_date = fields.Datetime('Approval Date')
is_closed = fields.Boolean()
is_approved = fields.Boolean(
compute='_compute_is_approved', store=True)
is_rejected = fields.Boolean(
compute='_compute_is_rejected', store=True)
awaiting_my_validation = fields.Boolean(
compute='_compute_awaiting_my_validation', search='_search_awaiting_my_validation')
@api.depends('status', 'approval_template_id.approval_type')
def _compute_is_approved(self):
for rec in self:
if rec.approval_template_id.approval_type == 'mandatory':
rec.is_approved = rec.status == 'approved'
else:
rec.is_approved = True
@api.depends('status', 'approval_template_id.approval_type')
def _compute_is_rejected(self):
for rec in self:
if rec.approval_template_id.approval_type == 'mandatory':
rec.is_rejected = rec.status == 'rejected'
else:
rec.is_rejected = False
@api.depends('status', 'approval_template_id.approval_type')
def _compute_awaiting_my_validation(self):
# trigger the search method and return a domain where approval ids satisfying the conditions in the search method
awaiting_validation_approval = self.search([('id', 'in', self.ids), ('awaiting_my_validation', '=', True)])
# set awaiting_my_validation values for approvals
awaiting_validation_approval.awaiting_my_validation = True
(self - awaiting_validation_approval).awaiting_my_validation = False
def _search_awaiting_my_validation(self, operator, value):
if (operator, value) not in [('=', True), ('!=', False)]:
raise NotImplementedError(_('Operation not supported'))
return [('required_user_ids', 'in', self.env.uid),
('approval_template_id.approval_type', 'in', ('mandatory', 'optional')),
('status', '!=', 'approved'),
('is_closed', '=', False)]
class MrpEcoStage(models.Model):
_name = 'mrp.eco.stage'
_description = 'ECO Stage'
_order = "sequence, id"
_fold_name = 'folded'
@api.model
def _get_sequence(self):
others = self.search([('sequence','<>',False)], order='sequence desc', limit=1)
if others:
return (others[0].sequence or 0) + 1
return 1
name = fields.Char('Name', required=True, translate=True)
sequence = fields.Integer('Sequence', default=_get_sequence)
folded = fields.Boolean('Folded in kanban view')
allow_apply_change = fields.Boolean(string='Allow to apply changes', help='Allow to apply changes from this stage.')
final_stage = fields.Boolean(string='Final Stage', help='Once the changes are applied, the ECOs will be moved to this stage.')
type_ids = fields.Many2many('mrp.eco.type', 'mrp_eco_stage_type_rel', 'stage_id', 'type_id', string='Types', required=True)
approval_template_ids = fields.One2many('mrp.eco.approval.template', 'stage_id', 'Approvals')
approval_roles = fields.Char('Approval Roles', compute='_compute_approvals', store=True)
is_blocking = fields.Boolean('Blocking Stage', compute='_compute_is_blocking', store=True)
legend_blocked = fields.Char(
'Red Kanban Label', default=lambda s: _('Blocked'), translate=True, required=True,
help='Override the default value displayed for the blocked state for kanban selection, when the ECO is in that stage.')
legend_done = fields.Char(
'Green Kanban Label', default=lambda s: _('Ready'), translate=True, required=True,
help='Override the default value displayed for the done state for kanban selection, when the ECO is in that stage.')
legend_normal = fields.Char(
'Grey Kanban Label', default=lambda s: _('In Progress'), translate=True, required=True,
help='Override the default value displayed for the normal state for kanban selection, when the ECO is in that stage.')
description = fields.Text(help="Description and tooltips of the stage states.")
@api.depends('approval_template_ids.name')
def _compute_approvals(self):
for rec in self:
rec.approval_roles = ', '.join(rec.approval_template_ids.mapped('name'))
@api.depends('approval_template_ids.approval_type')
def _compute_is_blocking(self):
for rec in self:
rec.is_blocking = any(template.approval_type == 'mandatory' for template in rec.approval_template_ids)
class MrpEco(models.Model):
_name = 'mrp.eco'
_description = 'Engineering Change Order (ECO)'
_inherit = ['mail.thread.cc', 'mail.activity.mixin']
@api.model
def _get_type_selection(self):
return [
('bom', _('Bill of Materials')),
('product', _('Product Only'))]
name = fields.Char('Reference', copy=False, required=True)
user_id = fields.Many2one('res.users', 'Responsible', default=lambda self: self.env.user, tracking=True, check_company=True)
type_id = fields.Many2one('mrp.eco.type', 'Type', required=True)
stage_id = fields.Many2one(
'mrp.eco.stage', 'Stage', ondelete='restrict', copy=False, domain="[('type_ids', 'in', type_id)]",
group_expand='_read_group_stage_ids', tracking=True,
default=lambda self: self.env['mrp.eco.stage'].search([('type_ids', 'in', self._context.get('default_type_id'))], limit=1))
company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company)
tag_ids = fields.Many2many('mrp.eco.tag', string='Tags')
priority = fields.Selection([
('0', 'Normal'),
('1', 'High')], string='Priority', tracking=True,
index=True)
note = fields.Html('Note')
effectivity = fields.Selection([
('asap', 'As soon as possible'),
('date', 'At Date')], string='Effective', # Is this English ?
compute='_compute_effectivity', inverse='_set_effectivity', store=True,
help='Date on which the changes should be applied. For reference only.')
effectivity_date = fields.Datetime('Effective Date', tracking=True, help="For reference only.")
approval_ids = fields.One2many('mrp.eco.approval', 'eco_id', 'Approvals', help='Approvals by stage')
state = fields.Selection([
('confirmed', 'To Do'),
('progress', 'In Progress'),
('rebase', 'Rebase'),
('conflict', 'Conflict'),
('done', 'Done')], string='Status',
copy=False, default='confirmed', readonly=True, required=True)
user_can_approve = fields.Boolean(
'Can Approve', compute='_compute_user_approval',
help='Technical field to check if approval by current user is required')
user_can_reject = fields.Boolean(
'Can Reject', compute='_compute_user_approval',
help='Technical field to check if reject by current user is possible')
kanban_state = fields.Selection([
('normal', 'In Progress'),
('done', 'Approved'),
('blocked', 'Blocked')], string='Kanban State',
copy=False, compute='_compute_kanban_state', store=True, readonly=False)
legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', related_sudo=False)
legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', related_sudo=False)
legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', related_sudo=False)
kanban_state_label = fields.Char(compute='_compute_kanban_state_label', string='Kanban State Label', tracking=True)
allow_change_kanban_state = fields.Boolean(
'Allow Change Kanban State', compute='_compute_allow_change_kanban_state')
allow_change_stage = fields.Boolean(
'Allow Change Stage', compute='_compute_allow_change_stage')
allow_apply_change = fields.Boolean(
'Show Apply Change', compute='_compute_allow_apply_change')
product_tmpl_id = fields.Many2one('product.template', "Product", check_company=True)
type = fields.Selection(selection=_get_type_selection, string='Apply on',
default='bom', required=True)
bom_id = fields.Many2one(
'mrp.bom', "Bill of Materials",
domain="[('product_tmpl_id', '=', product_tmpl_id)]", check_company=True) # Should at least have bom or routing on which it is applied?
new_bom_id = fields.Many2one(
'mrp.bom', 'New Bill of Materials',
copy=False)
new_bom_revision = fields.Integer('BoM Revision', related='new_bom_id.version', store=True, readonly=False)
bom_change_ids = fields.One2many(
'mrp.eco.bom.change', 'eco_id', string="ECO BoM Changes",
compute='_compute_bom_change_ids', help='Difference between old BoM and new BoM revision', store=True)
bom_rebase_ids = fields.One2many('mrp.eco.bom.change', 'rebase_id', string="BoM Rebase")
routing_change_ids = fields.One2many(
'mrp.eco.routing.change', 'eco_id', string="ECO Routing Changes",
compute='_compute_routing_change_ids', help='Difference between old operation and new operation revision', store=True)
mrp_document_count = fields.Integer('# Attachments', compute='_compute_attachments')
mrp_document_ids = fields.One2many(
'mrp.document', 'res_id', string='Attachments',
auto_join=True, domain=lambda self: [('res_model', '=', self._name)])
displayed_image_id = fields.Many2one(
'mrp.document', 'Displayed Image',
domain="[('res_model', '=', 'mrp.eco'), ('res_id', '=', id), ('mimetype', 'ilike', 'image')]")
displayed_image_attachment_id = fields.Many2one('ir.attachment', related='displayed_image_id.ir_attachment_id', readonly=False)
color = fields.Integer('Color')
active = fields.Boolean('Active', default=True, help="If the active field is set to False, it will allow you to hide the engineering change order without removing it.")
current_bom_id = fields.Many2one('mrp.bom', string="New Bom")
previous_change_ids = fields.One2many('mrp.eco.bom.change', 'eco_rebase_id', string="Previous ECO Changes", compute='_compute_previous_bom_change', store=True)
def _compute_attachments(self):
for p in self:
p.mrp_document_count = len(p.mrp_document_ids)
@api.depends('effectivity_date')
def _compute_effectivity(self):
for eco in self:
eco.effectivity = 'date' if eco.effectivity_date else 'asap'
def _set_effectivity(self):
for eco in self:
if eco.effectivity == 'asap':
eco.effectivity_date = False
def _is_conflict(self, new_bom_lines, changes=None):
# Find rebase lines having conflict or not.
reb_conflicts = self.env['mrp.eco.bom.change']
for reb_line in changes:
new_line = new_bom_lines.get(reb_line.product_id, None)
if new_line and (reb_line.old_operation_id, reb_line.old_uom_id, reb_line.old_product_qty) != (new_line.operation_id, new_line.product_uom_id, new_line.product_qty):
reb_conflicts |= reb_line
reb_conflicts.write({'conflict': True})
return reb_conflicts
def _get_difference_bom_lines(self, old_bom, new_bom):
# Return difference lines from two bill of material.
new_bom_commands = [(5,)]
old_bom_lines = dict(((line.product_id, tuple(line.bom_product_template_attribute_value_ids.ids), line.operation_id._get_comparison_values()), line) for line in old_bom.bom_line_ids)
if self.new_bom_id:
for line in new_bom.bom_line_ids:
old_line = old_bom_lines.pop((line.product_id, tuple(line.bom_product_template_attribute_value_ids.ids), line.operation_id._get_comparison_values()), None)
if old_line and (line.product_uom_id != old_line.product_uom_id or tools.float_compare(line.product_qty, old_line.product_qty, precision_rounding=line.product_uom_id.rounding)):
new_bom_commands += [(0, 0, {
'change_type': 'update',
'product_id': line.product_id.id,
'old_uom_id': old_line.product_uom_id.id,
'new_uom_id': line.product_uom_id.id,
'old_operation_id': old_line.operation_id.id,
'new_operation_id': line.operation_id.id,
'new_product_qty': line.product_qty,
'old_product_qty': old_line.product_qty})]
elif not old_line:
new_bom_commands += [(0, 0, {
'change_type': 'add',
'product_id': line.product_id.id,
'new_uom_id': line.product_uom_id.id,
'new_operation_id': line.operation_id.id,
'new_product_qty': line.product_qty
})]
for key, old_line in old_bom_lines.items():
new_bom_commands += [(0, 0, {
'change_type': 'remove',
'product_id': old_line.product_id.id,
'old_uom_id': old_line.product_uom_id.id,
'old_operation_id': old_line.operation_id.id,
'old_product_qty': old_line.product_qty,
})]
return new_bom_commands
def rebase(self, old_bom_lines, new_bom_lines, rebase_lines):
"""
This method will apply changes in new revision of BoM
old_bom_lines : Previous BoM or Old BoM version lines.
new_bom_lines : New BoM version lines.
rebase_lines : Changes done in previous version
"""
for reb_line in rebase_lines:
new_bom_line = new_bom_lines.get(reb_line.product_id, None)
if new_bom_line:
if new_bom_line.product_qty + reb_line.upd_product_qty > 0.0:
# Update line if it exist in new bom.
new_bom_line.write({'product_qty': new_bom_line.product_qty + reb_line.upd_product_qty, 'operation_id': reb_line.new_operation_id.id, 'product_uom_id': reb_line.new_uom_id.id})
else:
# Unlink lines if old bom removed lines
new_bom_line.unlink()
else:
# Add bom line in new bom for rebase.
old_line = old_bom_lines.get(reb_line.product_id, None)
if old_line:
old_line.copy({'bom_id': self.new_bom_id.id})
return True
def apply_rebase(self):
""" Apply rebase changes in new version of BoM """
self.ensure_one()
# Rebase logic applied..
vals = {'state': 'progress'}
if self.bom_rebase_ids:
new_bom_lines = dict(((line.product_id), line) for line in self.new_bom_id.bom_line_ids)
if self._is_conflict(new_bom_lines, self.bom_rebase_ids):
return self.write({'state': 'conflict'})
else:
old_bom_lines = dict(((line.product_id), line) for line in self.bom_id.bom_line_ids)
self.rebase(old_bom_lines, new_bom_lines, self.bom_rebase_ids)
# Remove all rebase line of current eco.
self.bom_rebase_ids.unlink()
if self.previous_change_ids:
new_bom_lines = dict(((line.product_id), line) for line in self.new_bom_id.bom_line_ids)
if self._is_conflict(new_bom_lines, self.previous_change_ids):
return self.write({'state': 'conflict'})
else:
new_activated_bom_lines = dict(((line.product_id), line) for line in self.current_bom_id.bom_line_ids)
self.rebase(new_activated_bom_lines, new_bom_lines, self.previous_change_ids)
# Remove all rebase line of current eco.
self.previous_change_ids.unlink()
if self.current_bom_id:
self.new_bom_id.write({'version': self.current_bom_id.version + 1, 'previous_bom_id': self.current_bom_id.id})
vals.update({'bom_id': self.current_bom_id.id, 'current_bom_id': False})
self.message_post(body=_('Successfully Rebased !'))
return self.write(vals)
@api.depends('bom_id.bom_line_ids', 'new_bom_id.bom_line_ids', 'new_bom_id.bom_line_ids.product_qty', 'new_bom_id.bom_line_ids.product_uom_id', 'new_bom_id.bom_line_ids.operation_id')
def _compute_bom_change_ids(self):
# Compute difference between old bom and new bom revision.
for eco in self:
eco.bom_change_ids = eco._get_difference_bom_lines(eco.bom_id, eco.new_bom_id)
@api.depends('bom_id.bom_line_ids', 'current_bom_id.bom_line_ids', 'current_bom_id.bom_line_ids.product_qty', 'current_bom_id.bom_line_ids.product_uom_id', 'current_bom_id.bom_line_ids.operation_id')
def _compute_previous_bom_change(self):
for eco in self:
if eco.current_bom_id:
# Compute difference between old bom and newly activated bom.
eco.previous_change_ids = eco._get_difference_bom_lines(eco.bom_id, eco.current_bom_id)
else:
eco.previous_change_ids = False
@api.depends('bom_id.operation_ids', 'bom_id.operation_ids.active', 'new_bom_id.operation_ids', 'new_bom_id.operation_ids.active')
def _compute_routing_change_ids(self):
for rec in self:
if rec.state == 'confirmed' or rec.type == 'product':
continue
new_routing_commands = [Command.clear()]
old_routing_lines = defaultdict(lambda: self.env['mrp.routing.workcenter'])
# Two operations could have the same values so we save them with the same key
for op in rec.bom_id.operation_ids:
old_routing_lines[op._get_comparison_values()] |= op
if rec.new_bom_id and rec.bom_id:
for operation in rec.new_bom_id.operation_ids:
key = (operation._get_comparison_values())
old_op = old_routing_lines[key][:1]
if old_op:
old_routing_lines[key] -= old_op
if tools.float_compare(old_op.time_cycle_manual, operation.time_cycle_manual, 2) != 0:
new_routing_commands += [Command.create({
'change_type': 'update',
'workcenter_id': operation.workcenter_id.id,
'new_time_cycle_manual': operation.time_cycle_manual,
'old_time_cycle_manual': old_op.time_cycle_manual,
'operation_id': operation.id,
})]
new_routing_commands += self._prepare_detailed_change_commands(operation, old_op)
else:
new_routing_commands += [Command.create({
'change_type': 'add',
'workcenter_id': operation.workcenter_id.id,
'new_time_cycle_manual': operation.time_cycle_manual,
'operation_id': operation.id,
})]
new_routing_commands += self._prepare_detailed_change_commands(operation, None)
for old_ops in old_routing_lines.values():
for old_op in old_ops:
new_routing_commands += [(0, 0, {
'change_type': 'remove',
'workcenter_id': old_op.workcenter_id.id,
'old_time_cycle_manual': old_op.time_cycle_manual,
'operation_id': old_op.id,
})]
rec.routing_change_ids = new_routing_commands
def _prepare_detailed_change_commands(self, new, old):
"""Necessary for overrides to track change of quality checks"""
return []
def _compute_user_approval(self):
for eco in self:
is_required_approval = eco.stage_id.approval_template_ids.filtered(lambda x: x.approval_type in ('mandatory', 'optional') and self.env.user in x.user_ids)
user_approvals = eco.approval_ids.filtered(lambda x: x.template_stage_id == eco.stage_id and x.user_id == self.env.user and not x.is_closed)
last_approval = user_approvals.sorted(lambda a : a.create_date, reverse=True)[:1]
eco.user_can_approve = is_required_approval and not last_approval.is_approved
eco.user_can_reject = is_required_approval and not last_approval.is_rejected
@api.depends('stage_id', 'approval_ids.is_approved', 'approval_ids.is_rejected')
def _compute_kanban_state(self):
""" State of ECO is based on the state of approvals for the current stage. """
for rec in self:
approvals = rec.approval_ids.filtered(lambda app:
app.template_stage_id == rec.stage_id and not app.is_closed)
if not approvals:
rec.kanban_state = 'normal'
elif all(approval.is_approved for approval in approvals):
rec.kanban_state = 'done'
elif any(approval.is_rejected for approval in approvals):
rec.kanban_state = 'blocked'
else:
rec.kanban_state = 'normal'
@api.depends('kanban_state', 'stage_id', 'approval_ids')
def _compute_allow_change_stage(self):
for rec in self:
approvals = rec.approval_ids.filtered(lambda app: app.template_stage_id == rec.stage_id)
if approvals:
rec.allow_change_stage = rec.kanban_state == 'done'
else:
rec.allow_change_stage = rec.kanban_state in ['normal', 'done']
@api.depends('state', 'stage_id.allow_apply_change')
def _compute_allow_apply_change(self):
for rec in self:
rec.allow_apply_change = rec.stage_id.allow_apply_change and rec.state in ('confirmed', 'progress')
@api.depends('stage_id.approval_template_ids')
def _compute_allow_change_kanban_state(self):
for rec in self:
rec.allow_change_kanban_state = False if rec.stage_id.approval_template_ids else True
@api.depends('stage_id', 'kanban_state')
def _compute_kanban_state_label(self):
for eco in self:
if eco.kanban_state == 'normal':
eco.kanban_state_label = eco.legend_normal
elif eco.kanban_state == 'blocked':
eco.kanban_state_label = eco.legend_blocked
else:
eco.kanban_state_label = eco.legend_done
@api.onchange('product_tmpl_id')
def onchange_product_tmpl_id(self):
if self.product_tmpl_id.bom_ids:
self.bom_id = self.product_tmpl_id.bom_ids.ids[0]
@api.onchange('type_id')
def onchange_type_id(self):
self.stage_id = self.env['mrp.eco.stage'].search([('type_ids', 'in', self.type_id.id)], limit=1).id
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
prefix = self.env['ir.sequence'].next_by_code('mrp.eco') or ''
vals['name'] = '%s%s' % (prefix and '%s: ' % prefix or '', vals.get('name', ''))
ecos = super().create(vals_list)
ecos._create_approvals()
return ecos
def write(self, vals):
if vals.get('stage_id'):
newstage = self.env['mrp.eco.stage'].browse(vals['stage_id'])
# raise exception only if we increase the stage, not on decrease
for eco in self:
if eco.stage_id and ((newstage.sequence, newstage.id) > (eco.stage_id.sequence, eco.stage_id.id)):
if not eco.allow_change_stage:
raise UserError(_('You cannot change the stage, as approvals are still required.'))
has_blocking_stages = self.env['mrp.eco.stage'].search_count([
('sequence', '>=', eco.stage_id.sequence),
('sequence', '<=', newstage.sequence),
('type_ids', 'in', eco.type_id.id),
('id', 'not in', [eco.stage_id.id] + [vals['stage_id']]),
('is_blocking', '=', True)])
if has_blocking_stages:
raise UserError(_('You cannot change the stage, as approvals are required in the process.'))
if eco.stage_id != newstage:
eco.approval_ids.filtered(lambda x: x.status != 'none').write({'is_closed': True})
eco.approval_ids.filtered(lambda x: x.status == 'none').unlink()
if 'displayed_image_attachment_id' in vals:
doc = False
if vals['displayed_image_attachment_id']:
doc = self.env['mrp.document'].search([('ir_attachment_id', '=', vals['displayed_image_attachment_id'])])
if not doc:
doc = self.env['mrp.document'].create([{'ir_attachment_id': vals['displayed_image_attachment_id']}])
vals.pop('displayed_image_attachment_id')
vals['displayed_image_id'] = doc
res = super(MrpEco, self).write(vals)
if vals.get('stage_id'):
self._create_approvals()
return res
@api.model
def _read_group_stage_ids(self, stages, domain, order):
""" Read group customization in order to display all the stages of the ECO type
in the Kanban view, even if there is no ECO in that stage
"""
search_domain = []
if self._context.get('default_type_ids'):
search_domain = [('type_ids', 'in', self._context['default_type_ids'])]
stage_ids = stages._search(search_domain, order=order, access_rights_uid=SUPERUSER_ID)
return stages.browse(stage_ids)
@api.returns('mail.message', lambda value: value.id)
def message_post(self, **kwargs):
message = super(MrpEco, self).message_post(**kwargs)
if message.message_type == 'comment' and message.author_id == self.env.user.partner_id: # should use message_values to avoid a read
for eco in self:
for approval in eco.approval_ids.filtered(lambda app: app.template_stage_id == eco.stage_id and app.status == 'none' and app.approval_template_id.approval_type == 'comment'):
if self.env.user in approval.approval_template_id.user_ids:
approval.write({
'status': 'comment',
'user_id': self.env.uid
})
return message
def _create_approvals(self):
approval_vals = []
activity_vals = []
for eco in self:
for approval_template in eco.stage_id.approval_template_ids:
approval = eco.approval_ids.filtered(lambda app: app.approval_template_id == approval_template and not app.is_closed)
if not approval:
approval_vals.append({
'eco_id': eco.id,
'approval_template_id': approval_template.id,
})
for user in approval_template.user_ids:
activity_vals.append({
'activity_type_id': self.env.ref('mrp_plm.mail_activity_eco_approval').id,
'user_id': user.id,
'res_id': eco.id,
'res_model_id': self.env.ref('mrp_plm.model_mrp_eco').id,
})
self.env['mrp.eco.approval'].create(approval_vals)
self.env['mail.activity'].create(activity_vals)
def _create_or_update_approval(self, status):
for eco in self:
for approval_template in eco.stage_id.approval_template_ids.filtered(lambda a: self.env.user in a.user_ids):
approvals = eco.approval_ids.filtered(lambda x: x.approval_template_id == approval_template and not x.is_closed)
none_approvals = approvals.filtered(lambda a: a.status =='none')
confirmed_approvals = approvals - none_approvals
if none_approvals:
none_approvals.write({'status': status, 'user_id': self.env.uid, 'approval_date': fields.Datetime.now()})
confirmed_approvals.write({'is_closed': True})
approval = none_approvals[:1]
else:
approvals.write({'is_closed': True})
approval = self.env['mrp.eco.approval'].create({
'eco_id': eco.id,
'approval_template_id': approval_template.id,
'status': status,
'user_id': self.env.uid,
'approval_date': fields.Datetime.now(),
})
message = escape(_("%(approval_name)s %(approver_name)s %(approval_status)s this ECO")) % {
'approval_name': approval.name,
'approver_name': approval.user_id.name,
'approval_status': approval.status,
}
eco.message_post(body=message, subtype_xmlid='mail.mt_comment')
def approve(self):
self._create_or_update_approval(status='approved')
def reject(self):
self._create_or_update_approval(status='rejected')
def conflict_resolve(self):
self.ensure_one()
vals = {'state': 'progress'}
if self.current_bom_id:
vals.update({'bom_id': self.current_bom_id.id, 'current_bom_id': False})
self.write(vals)
# Set previous BoM on new revision and change version of BoM.
self.new_bom_id.write({'version': self.bom_id.version + 1, 'previous_bom_id': self.bom_id.id})
# Remove all rebase lines.
rebase_lines = self.bom_rebase_ids + self.previous_change_ids
rebase_lines.unlink()
return True
def action_new_revision(self):
IrAttachment = self.env['ir.attachment']
for eco in self:
if eco.type == 'bom':
eco.new_bom_id = eco.bom_id.sudo().copy(default={
'version': eco.bom_id.version + 1,
'active': False,
'previous_bom_id': eco.bom_id.id,
})
attachments = IrAttachment.search([('res_model', '=', 'mrp.bom'),
('res_id', '=', eco.bom_id.id)])
else:
attachments = IrAttachment.search([('res_model', '=', 'product.template'),
('res_id', '=', eco.product_tmpl_id.id)])
for attach in attachments:
new_attach = attach.copy({'res_model': 'mrp.eco', 'res_id': eco.id})
self.env['mrp.document'].create({'ir_attachment_id': new_attach.id, 'origin_attachment_id': attach.id})
self.write({'state': 'progress'})
def action_apply(self):
self._check_company()
eco_need_action = self.env['mrp.eco']
for eco in self:
if eco.state == 'done':
continue
if eco.state == 'rebase':
eco.apply_rebase()
if eco.allow_apply_change:
if eco.type == 'product':
for attach in eco.with_context(active_test=False).mrp_document_ids:
origin = attach.origin_attachment_id
if not attach.active:
origin.unlink()
continue
if origin._compute_checksum(origin.raw) == origin._compute_checksum(attach.raw):
if attach.origin_attachment_id.name != attach.name:
attach.origin_attachment_id.name = attach.name
if attach.origin_attachment_id.company_id != attach.company_id:
attach.origin_attachment_id.company_id = attach.company_id
continue
attach.ir_attachment_id.copy({
'res_model': 'product.template',
'res_id': eco.product_tmpl_id.id,
})
eco.product_tmpl_id.version = eco.product_tmpl_id.version + 1
else:
eco.mapped('new_bom_id').apply_new_version()
for attach in eco.mrp_document_ids:
attach.ir_attachment_id.copy({
'res_model': 'mrp.bom',
'res_id': eco.new_bom_id.id,
})
vals = {'state': 'done'}
stage_id = eco.env['mrp.eco.stage'].search([
('final_stage', '=', True),
('type_ids', 'in', eco.type_id.id)], limit=1).id
if stage_id:
vals['stage_id'] = stage_id
eco.write(vals)
else:
eco_need_action |= eco
if eco_need_action:
return {
'name': _('Eco'),
'type': 'ir.actions.act_window',
'view_mode': 'tree, form',
'views': [[False, 'tree'], [False, 'form']],
'res_model': 'mrp.eco',
'target': 'current',
'domain': [('id', 'in', eco_need_action.ids)],
'context': {'search_default_changetoapply': False},
}
def action_see_attachments(self):
self.ensure_one()
domain = ['&', ('res_model', '=', self._name), ('res_id', '=', self.id)]
attachment_view = self.env.ref('mrp_plm.view_document_file_kanban_mrp_plm')
context = {
'default_res_model': self._name,
'default_res_id': self.id,
'default_company_id': self.company_id.id,
'search_default_all': 1,
'create': self.state != 'done',
'edit': self.state != 'done',
'delete': self.state != 'done',
}
return {
'name': _('Attachments'),
'domain': domain,
'res_model': 'mrp.document',
'type': 'ir.actions.act_window',
'view_id': attachment_view.id,
'views': [(attachment_view.id, 'kanban'), (False, 'form')],
'view_mode': 'kanban,tree,form',
'help': _('''<p class="o_view_nocontent_smiling_face">
Upload files to your ECO, that will be applied to the product later
</p><p>
Use this feature to store any files, like drawings or specifications.
</p>'''),
'limit': 80,
'context': context,
}
def open_new_bom(self):
self.ensure_one()
return {
'name': _('Eco BoM'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'mrp.bom',
'target': 'current',
'res_id': self.new_bom_id.id,
'context': {
'default_product_tmpl_id': self.product_tmpl_id.id,
'default_product_id': self.product_tmpl_id.product_variant_id.id,
'create': self.state != 'done',
'edit': self.state != 'done',
'delete': self.state != 'done',
},
}
class MrpEcoBomChange(models.Model):
_name = 'mrp.eco.bom.change'
_description = 'ECO BoM changes'
eco_id = fields.Many2one('mrp.eco', 'Engineering Change', ondelete='cascade')
eco_rebase_id = fields.Many2one('mrp.eco', 'ECO Rebase', ondelete='cascade')
rebase_id = fields.Many2one('mrp.eco', 'Rebase', ondelete='cascade')
change_type = fields.Selection([('add', 'Add'), ('remove', 'Remove'), ('update', 'Update')], string='Type', required=True)
product_id = fields.Many2one('product.product', 'Product', required=True)
old_uom_id = fields.Many2one('uom.uom', 'Previous Product UoM')
new_uom_id = fields.Many2one('uom.uom', 'New Product UoM')
old_product_qty = fields.Float('Previous revision quantity', default=0)
new_product_qty = fields.Float('New revision quantity', default=0)
old_operation_id = fields.Many2one('mrp.routing.workcenter', 'Previous Consumed in Operation')
new_operation_id = fields.Many2one('mrp.routing.workcenter', 'New Consumed in Operation')
upd_product_qty = fields.Float('Quantity', compute='_compute_change', store=True)
uom_change = fields.Char('Unit of Measure', compute='_compute_change', compute_sudo=True)
operation_change = fields.Char(compute='_compute_change', string='Consumed in Operation', compute_sudo=True)
conflict = fields.Boolean()
@api.depends('new_product_qty', 'old_product_qty', 'old_operation_id', 'new_operation_id', 'old_uom_id', 'new_uom_id')
def _compute_change(self):
for rec in self:
rec.upd_product_qty = rec.new_product_qty - rec.old_product_qty
rec.operation_change = rec.new_operation_id.name if rec.change_type == 'add' else rec.old_operation_id.name
rec.uom_change = False
if (rec.old_uom_id and rec.new_uom_id) and rec.old_uom_id != rec.new_uom_id:
rec.uom_change = rec.old_uom_id.name + ' -> ' + rec.new_uom_id.name
if (rec.old_operation_id._get_comparison_values() != rec.new_operation_id._get_comparison_values()) and rec.change_type == 'update':
rec.operation_change = (rec.old_operation_id.name or '') + ' -> ' + (rec.new_operation_id.name or '')
class MrpEcoRoutingChange(models.Model):
_name = 'mrp.eco.routing.change'
_description = 'Eco Routing changes'
eco_id = fields.Many2one('mrp.eco', 'Engineering Change', ondelete='cascade', required=True)
change_type = fields.Selection([('add', 'Add'), ('remove', 'Remove'), ('update', 'Update')], string='Type', required=True)
workcenter_id = fields.Many2one('mrp.workcenter', 'Work Center')
old_time_cycle_manual = fields.Float('Old manual duration', default=0)
new_time_cycle_manual = fields.Float('New manual duration', default=0)
upd_time_cycle_manual = fields.Float('Manual Duration Change', compute='_compute_upd_time_cycle_manual', store=True)
operation_id = fields.Many2one('mrp.routing.workcenter', 'New or Previous Operation')
operation_name = fields.Char(related='operation_id.name', string='Operation')
@api.depends('new_time_cycle_manual', 'old_time_cycle_manual')
def _compute_upd_time_cycle_manual(self):
for rec in self:
rec.upd_time_cycle_manual = rec.new_time_cycle_manual - rec.old_time_cycle_manual
class MrpEcoTag(models.Model):
_name = "mrp.eco.tag"
_description = "ECO Tags"
def _get_default_color(self):
return randint(1, 11)
name = fields.Char('Tag Name', required=True)
color = fields.Integer('Color Index', default=_get_default_color)
_sql_constraints = [
('name_uniq', 'unique (name)', "Tag name already exists !"),
]

View File

@@ -1,33 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class MrpProduction(models.Model):
_inherit = 'mrp.production'
latest_bom_id = fields.Many2one('mrp.bom', compute="_compute_latest_bom_id")
@api.depends('bom_id', 'bom_id.active')
def _compute_latest_bom_id(self):
self.latest_bom_id = False
# check if the bom has a new version
for mo in self:
if mo.bom_id and not mo.bom_id.active:
mo.latest_bom_id = mo.bom_id._get_active_version()
# check if the components have a new version
mo_to_update = self.search([
('id', 'in', self.filtered(lambda p: not p.latest_bom_id).ids),
('move_raw_ids.bom_line_id.bom_id.active', '=', False)
])
for mo in mo_to_update:
mo.latest_bom_id = mo.bom_id
def action_update_bom(self):
for production in self:
if production.state != 'draft' or not production.latest_bom_id:
continue
latest_bom = production.latest_bom_id
(production.move_finished_ids | production.move_raw_ids).unlink()
production.workorder_ids.unlink()
production.write({'bom_id': latest_bom.id})

View File

@@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
class ProductTemplate(models.Model):
_inherit = 'product.template'
version = fields.Integer('Version', default=1, readonly=True, copy=False, help="The current version of the product.")
eco_count = fields.Integer('# ECOs',compute='_compute_eco_count')
eco_ids = fields.One2many('mrp.eco', 'product_tmpl_id', 'ECOs')
def _compute_eco_count(self):
for p in self:
p.eco_count = len(p.eco_ids)

View File

@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mrp_report_bom_structure

View File

@@ -1,46 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models
class ReportBomStructure(models.AbstractModel):
_inherit = 'report.mrp.report_bom_structure'
def _get_pdf_doc(self, bom_id, data, quantity, product_variant_id=None):
doc = super()._get_pdf_doc(bom_id, data, quantity, product_variant_id)
doc['show_ecos'] = True if data and data.get('show_ecos') == 'true' and self.env.user.user_has_groups('mrp_plm.group_plm_user') else False
return doc
def _get_report_data(self, bom_id, searchQty=0, searchVariant=False):
res = super()._get_report_data(bom_id, searchQty, searchVariant)
res['is_eco_applied'] = self.env.user.user_has_groups('mrp_plm.group_plm_user')
return res
def _get_bom_data(self, bom, warehouse, product=False, line_qty=False, bom_line=False, level=0, parent_bom=False, index=0, product_info=False, ignore_stock=False):
res = super()._get_bom_data(bom, warehouse, product, line_qty, bom_line, level, parent_bom, index, product_info, ignore_stock)
if self.env.user.user_has_groups('mrp_plm.group_plm_user'):
res['version'] = res['bom'] and res['bom'].version or ''
product_tmpl_id = (res['product'] and res['product'].product_tmpl_id.id) or (res['bom'] and res['product'].product_tmpl_id.id)
res['ecos'] = self.env['mrp.eco'].search_count([('product_tmpl_id', '=', product_tmpl_id), ('state', '!=', 'done')]) or ''
return res
def _get_component_data(self, bom, warehouse, bom_line, line_quantity, level, index, product_info, ignore_stock=False):
res = super()._get_component_data(bom, warehouse, bom_line, line_quantity, level, index, product_info, ignore_stock)
if self.env.user.user_has_groups('mrp_plm.group_plm_user'):
res['version'] = False
res['ecos'] = self.env['mrp.eco'].search_count([('product_tmpl_id', '=', res['product'].product_tmpl_id.id), ('state', '!=', 'done')]) or ''
return res
def _get_bom_array_lines(self, data, level, unfolded_ids, unfolded, parent_unfolded):
lines = super()._get_bom_array_lines(data, level, unfolded_ids, unfolded, parent_unfolded)
if not self.env.user.user_has_groups('mrp_plm.group_plm_user'):
return lines
for component in data.get('components', []):
if not component['bom_id']:
continue
bom_line = next(filter(lambda l: l.get('bom_id', None) == component['bom_id'], lines))
if bom_line:
bom_line['version'] = component['version']
bom_line['ecos'] = component['ecos']
return lines

View File

@@ -1,38 +0,0 @@
<odoo>
<template id="report_mrp_bom_inherit_mrp_plm" inherit_id="mrp.report_mrp_bom">
<xpath expr="//th[@name='th_mrp_bom_h']" position="after">
<th t-if="data['show_ecos']">BoM Version</th>
<th t-if="data['show_ecos']">ECOs</th>
</xpath>
<xpath expr="//td[@name='td_mrp_bom']" position="after">
<td t-if="data['show_ecos']">
<span t-esc="data['version']"/>
</td>
<td t-if="data['show_ecos']">
<span t-esc="data['ecos']"/>
</td>
</xpath>
<xpath expr="//td[@name='td_mrp_bom_f']" position="after">
<td t-if="data['show_ecos']"/>
<td t-if="data['show_ecos']"/>
</xpath>
<xpath expr="//td[@name='td_mrp_bom_byproducts_f']" position="after">
<td t-if="data['show_ecos']"/>
<td t-if="data['show_ecos']"/>
</xpath>
</template>
<template id="report_mrp_bom_pdf_line_inherit_mrp_plm" inherit_id="mrp.report_mrp_bom_pdf_line">
<xpath expr="//td[@name='td_mrp_code']" position="after">
<td t-if="data['show_ecos']">
<span t-if="l.get('version')" t-esc="l['version']"/>
</td>
<td t-if="data['show_ecos']">
<span t-if="l.get('ecos')" t-esc="l['ecos']"/>
</td>
</xpath>
</template>
</odoo>

View File

@@ -1,14 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_model_mrp_eco_approval_template_manager,mrp.eco.approval.template,mrp_plm.model_mrp_eco_approval_template,mrp_plm.group_plm_manager,1,1,1,1
access_model_mrp_eco_approval_template_user,mrp.eco.approval.template,mrp_plm.model_mrp_eco_approval_template,mrp_plm.group_plm_user,1,0,0,0
access_model_mrp_eco_approval_user,mrp.eco.approval,mrp_plm.model_mrp_eco_approval,mrp_plm.group_plm_user,1,1,1,1
access_model_mrp_eco_stage_manager,mrp.eco.stage,mrp_plm.model_mrp_eco_stage,mrp_plm.group_plm_manager,1,1,1,1
access_model_mrp_eco_stage_user,mrp.eco.stage,mrp_plm.model_mrp_eco_stage,mrp_plm.group_plm_user,1,0,0,0
access_model_mrp_eco_bom_change_user,mrp.eco.bom.line,mrp_plm.model_mrp_eco_bom_change,mrp_plm.group_plm_user,1,1,1,1
access_model_mrp_eco_routing_change_user,mrp.eco.routing.line,mrp_plm.model_mrp_eco_routing_change,mrp_plm.group_plm_user,1,1,1,1
access_mrp_eco_tag_user,mrp.eco.tag,mrp_plm.model_mrp_eco_tag,mrp_plm.group_plm_user,1,1,1,1
access_mrp_eco_tag_manager,mrp.eco.tag,mrp_plm.model_mrp_eco_tag,mrp_plm.group_plm_manager,1,1,1,1
access_mrp_eco_type_user,mrp.eco.type,mrp_plm.model_mrp_eco_type,mrp_plm.group_plm_user,1,0,0,0
access_mrp_eco_type_manager,mrp.eco.type,mrp_plm.model_mrp_eco_type,mrp_plm.group_plm_manager,1,1,1,1
access_mrp_eco_user,mrp.eco,mrp_plm.model_mrp_eco,mrp_plm.group_plm_user,1,1,1,1
access_product_template_plm_manager,product.template plm_manager,product.model_product_template,mrp_plm.group_plm_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_model_mrp_eco_approval_template_manager mrp.eco.approval.template mrp_plm.model_mrp_eco_approval_template mrp_plm.group_plm_manager 1 1 1 1
3 access_model_mrp_eco_approval_template_user mrp.eco.approval.template mrp_plm.model_mrp_eco_approval_template mrp_plm.group_plm_user 1 0 0 0
4 access_model_mrp_eco_approval_user mrp.eco.approval mrp_plm.model_mrp_eco_approval mrp_plm.group_plm_user 1 1 1 1
5 access_model_mrp_eco_stage_manager mrp.eco.stage mrp_plm.model_mrp_eco_stage mrp_plm.group_plm_manager 1 1 1 1
6 access_model_mrp_eco_stage_user mrp.eco.stage mrp_plm.model_mrp_eco_stage mrp_plm.group_plm_user 1 0 0 0
7 access_model_mrp_eco_bom_change_user mrp.eco.bom.line mrp_plm.model_mrp_eco_bom_change mrp_plm.group_plm_user 1 1 1 1
8 access_model_mrp_eco_routing_change_user mrp.eco.routing.line mrp_plm.model_mrp_eco_routing_change mrp_plm.group_plm_user 1 1 1 1
9 access_mrp_eco_tag_user mrp.eco.tag mrp_plm.model_mrp_eco_tag mrp_plm.group_plm_user 1 1 1 1
10 access_mrp_eco_tag_manager mrp.eco.tag mrp_plm.model_mrp_eco_tag mrp_plm.group_plm_manager 1 1 1 1
11 access_mrp_eco_type_user mrp.eco.type mrp_plm.model_mrp_eco_type mrp_plm.group_plm_user 1 0 0 0
12 access_mrp_eco_type_manager mrp.eco.type mrp_plm.model_mrp_eco_type mrp_plm.group_plm_manager 1 1 1 1
13 access_mrp_eco_user mrp.eco mrp_plm.model_mrp_eco mrp_plm.group_plm_user 1 1 1 1
14 access_product_template_plm_manager product.template plm_manager product.model_product_template mrp_plm.group_plm_manager 1 1 1 1

View File

@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- This group is meant to manage PLM stages -->
<record model="ir.module.category" id="module_category_manufacturing_product_lifecycle_management_(plm)">
<field name="name">PLM</field>
<field name="description">Helps you manage your product's lifecycles.</field>
<field name="sequence">5</field>
</record>
<record id="mrp_plm.group_plm_user" model="res.groups">
<field name="name">User</field>
<field name="category_id" ref="base.module_category_manufacturing_product_lifecycle_management_(plm)"/>
<field name="implied_ids" eval="[(4, ref('mrp.group_mrp_user'))]"/>
<field name="comment">The PLM user uses products lifecycle management</field>
</record>
<record id="mrp_plm.group_plm_manager" model="res.groups">
<field name="name">Administrator</field>
<field name="category_id" ref="base.module_category_manufacturing_product_lifecycle_management_(plm)"/>
<field name="implied_ids" eval="[(4, ref('mrp_plm.group_plm_user'))]"/>
<field name="comment">The PLM manager manages products lifecycle management</field>
</record>
<record id="base.default_user" model="res.users">
<field name="groups_id" eval="[(4, ref('mrp_plm.group_plm_manager'))]"/>
</record>
<record model="res.users" id="base.user_root">
<field eval="[(4,ref('mrp_plm.group_plm_manager'))]" name="groups_id"/>
</record>
<record model="res.users" id="base.user_admin">
<field eval="[(4,ref('mrp_plm.group_plm_manager'))]" name="groups_id"/>
</record>
<data noupdate="1">
<record model="ir.rule" id="mrp_eco_comp_rule">
<field name="name">Manufacturing ECO company rule</field>
<field name="model_id" ref="model_mrp_eco"/>
<field name="domain_force">['|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
</record>
</data>
</odoo>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="100%"><stop offset="0%" stop-color="#7CC098"/><stop offset="100%" stop-color="#5F8A71"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M42.127 69H4c-2 0-4-1-4-4V35l15.895-17.19C25.228 8.478 38 6.334 50 15c8.415 6.077 12.076 13.466 10.984 22.165-.035.285-.1.714-.194 1.29a24.755 24.755 0 0 1-.304 1.352C60.099 41.274 58.936 44.338 57 49L42.127 69z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><path fill="#000" d="M35.722 27L47 30.846v10.385L38.056 47 26 41.23V30.847L35.722 27zm-.381 1L28 31.111 38.045 35 45 31.5 35.34 28zM27 32v8.944L38 46v-9.722L27 32zm-.615-24l-5.9 8.058 9.51 3.036-1.28-3.953a22.208 22.208 0 0 1 7.2-1.203c11.135 0 20.316 8.182 21.904 18.866l3.228-2.94c-2.799-11.282-12.995-19.668-25.132-19.668a25.8 25.8 0 0 0-8.365 1.394L26.385 8zm-8.44 9.452C13.046 22.167 10 28.769 10 36.088c0 7.763 3.434 14.745 8.861 19.496l-2.54 2.807 9.93 1.07-2.12-9.739-2.768 3.074a22.075 22.075 0 0 1-7.6-16.708 22.073 22.073 0 0 1 8.326-17.319l-2.922-.935-1.223-.382zM60.97 32.766l-7.39 6.702 4.068.879c-1.98 10.21-10.933 17.891-21.733 17.891-2.858 0-5.591-.528-8.097-1.508l.916 4.258A25.918 25.918 0 0 0 35.914 62c12.565 0 23.05-8.988 25.4-20.87l3.686.782-4.03-9.146z" opacity=".3"/><path fill="#FFF" d="M35.722 25L47 28.846v10.385L38.056 45 26 39.23V28.847L35.722 25zm-.381 1L28 29.111 38.045 33 45 29.5 35.34 26zM27 30v8.944L38 44v-9.722L27 30zm-.615-24l-5.9 8.058 9.51 3.036-1.28-3.953a22.208 22.208 0 0 1 7.2-1.203c11.135 0 20.316 8.182 21.904 18.866l3.228-2.94C58.248 16.582 48.052 8.196 35.915 8.196A25.8 25.8 0 0 0 27.55 9.59L26.385 6zm-8.44 9.452C13.046 20.167 10 26.769 10 34.088c0 7.763 3.434 14.745 8.861 19.496l-2.54 2.807 9.93 1.07-2.12-9.739-2.768 3.074a22.075 22.075 0 0 1-7.6-16.708 22.073 22.073 0 0 1 8.326-17.319l-2.922-.935-1.223-.382zM60.97 30.766l-7.39 6.702 4.068.879c-1.98 10.21-10.933 17.891-21.733 17.891-2.858 0-5.591-.528-8.097-1.508l.916 4.258A25.918 25.918 0 0 0 35.914 60c12.565 0 23.05-8.988 25.4-20.87l3.686.782-4.03-9.146z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,22 +0,0 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { BomOverviewComponent } from "@mrp/components/bom_overview/mrp_bom_overview";
patch(BomOverviewComponent.prototype, "mrp_plm", {
setup() {
this._super.apply();
this.state.showOptions.ecos = false;
this.state.showOptions.ecoAllowed = false;
},
async getBomData() {
const bomData = await this._super.apply();
this.state.showOptions.ecoAllowed = bomData['is_eco_applied'];
return bomData;
},
getReportName(printAll) {
return this._super.apply(this, arguments) + "&show_ecos=" + (this.state.showOptions.ecoAllowed && this.state.showOptions.ecos);
}
});

View File

@@ -1,25 +0,0 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { BomOverviewDisplayFilter } from "@mrp/components/bom_overview_display_filter/mrp_bom_overview_display_filter";
patch(BomOverviewDisplayFilter.prototype, "mrp_plm", {
setup() {
this._super.apply();
if (this.props.showOptions.ecoAllowed) {
this.displayOptions.ecos = this.env._t('ECOs');
}
},
});
patch(BomOverviewDisplayFilter, "mrp_plm", {
props: {
...BomOverviewDisplayFilter.props,
showOptions: {
...BomOverviewDisplayFilter.showOptions,
ecos: Boolean,
ecoAllowed: Boolean,
},
},
});

View File

@@ -1,30 +0,0 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { BomOverviewLine } from "@mrp/components/bom_overview_line/mrp_bom_overview_line";
patch(BomOverviewLine.prototype, "mrp_plm", {
//---- Handlers ----
async goToEco() {
return this.actionService.doAction({
name: this.env._t("ECOs"),
type: "ir.actions.act_window",
res_model: "mrp.eco",
domain: [["product_tmpl_id.product_variant_ids", "in", [this.data.product_id]]],
views: [[false, "kanban"], [false, "list"], [false, "form"]],
target: "current",
});
}
});
patch(BomOverviewLine, "mrp_plm", {
props: {
...BomOverviewLine.props,
showOptions: {
...BomOverviewLine.showOptions,
ecos: Boolean,
ecoAllowed: Boolean,
},
},
});

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mrp_plm.BomOverviewLine" t-inherit="mrp.BomOverviewLine" t-inherit-mode="extension" owl="1">
<xpath expr="//td[@name='td_mrp_bom']" position="after">
<td t-if="props.showOptions.ecos" class="text-end">
<span t-if="data.version" t-esc="data.version"/>
</td>
<td t-if="props.showOptions.ecos" class="text-end">
<a href="#" t-on-click.prevent="goToEco" t-esc="data.ecos"/>
</td>
</xpath>
</t>
</templates>

Some files were not shown because too many files have changed in this diff Show More