folder rename
This commit is contained in:
64
fusion_plating/fusion_plating_bridge_documents/README.md
Normal file
64
fusion_plating/fusion_plating_bridge_documents/README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Fusion Plating — Documents Bridge (Enterprise)
|
||||
|
||||
Part of the Fusion Plating product family by Nexa Systems Inc.
|
||||
|
||||
## What this module does
|
||||
|
||||
When both `fusion_plating_quality` and Odoo Enterprise's `documents` module are
|
||||
installed, this bridge **auto-installs** and wires the two together so that
|
||||
every attachment dropped on a Fusion Plating quality record (NCR, CAPA, FAIR,
|
||||
or Controlled Document) is automatically mirrored into a dedicated Documents
|
||||
workspace with the right tag applied.
|
||||
|
||||
No manual file uploads, no duplicate copies to keep in sync — the bridge takes
|
||||
care of it on `ir.attachment.create()`.
|
||||
|
||||
## What it creates
|
||||
|
||||
* **Workspace**: `Plating — Quality` (a `documents.document` with `type='folder'`)
|
||||
* **Facet**: `Record Type` (a `documents.facet` scoped to the workspace)
|
||||
* **Tags**: `NCR`, `CAPA`, `FAIR`, `Doc Control` under that facet
|
||||
* **Smart button**: a `Documents` stat button on every NCR / CAPA / FAIR /
|
||||
Controlled Document form view, opening the filtered Documents kanban for
|
||||
that record
|
||||
|
||||
## How it works
|
||||
|
||||
1. A user attaches a file to an NCR (or any of the other supported records)
|
||||
via the chatter or a wizard.
|
||||
2. The bridge's `ir.attachment.create()` override inspects `res_model` and,
|
||||
if it matches one of the supported quality models, creates a mirror
|
||||
`documents.document` record:
|
||||
* Placed inside the `Plating — Quality` workspace
|
||||
* Tagged with the corresponding record type tag
|
||||
* Linked back to the original `ir.attachment` via `attachment_id`
|
||||
3. On the quality record form, the smart button reads a computed Many2many
|
||||
(`x_fc_document_ids`) that searches `documents.document` by the underlying
|
||||
attachment's `res_model` + `res_id` — no duplication, no storage overhead.
|
||||
|
||||
## Safety & robustness
|
||||
|
||||
* The bridge never blocks attachment creation. Any exception raised while
|
||||
creating the mirror `documents.document` record is caught and logged — the
|
||||
user's upload always succeeds.
|
||||
* All references to the workspace folder and tags use
|
||||
`env.ref(..., raise_if_not_found=False)`. If the data records are ever
|
||||
removed or renamed, the bridge degrades gracefully (no mirror created,
|
||||
logged warning).
|
||||
* The bridge never modifies `fusion_plating`, `fusion_plating_quality`, or
|
||||
the EE `documents` module. It is purely additive.
|
||||
|
||||
## Dependencies
|
||||
|
||||
* `fusion_plating_quality`
|
||||
* `documents` (Odoo Enterprise)
|
||||
|
||||
## Auto-install
|
||||
|
||||
`auto_install = True` — the bridge installs automatically whenever both
|
||||
dependencies are present in the same database, and stays dormant otherwise.
|
||||
|
||||
## Copyright
|
||||
|
||||
Copyright 2026 Nexa Systems Inc. All rights reserved.
|
||||
Licensed under OPL-1 (Odoo Proprietary License v1.0).
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import models
|
||||
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Documents Bridge (EE)',
|
||||
'version': '19.0.1.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Enterprise bridge: auto-promotes Fusion Plating quality attachments '
|
||||
'(NCR, CAPA, FAIR, Doc Control) into Odoo EE Documents with a tagged '
|
||||
'workspace. Auto-installs when both modules are present.',
|
||||
'description': """
|
||||
Fusion Plating — Documents Bridge (Enterprise)
|
||||
==============================================
|
||||
|
||||
Part of the Fusion Plating product family by Nexa Systems Inc.
|
||||
|
||||
This bridge module connects the native Fusion Plating QMS (`fusion_plating_quality`)
|
||||
with the Odoo Enterprise `documents` module. When both modules are installed the
|
||||
bridge installs automatically and takes care of the plumbing so that every
|
||||
attachment dropped on an NCR, CAPA, FAIR, or Controlled Document record is
|
||||
promoted into a dedicated "Plating — Quality" workspace and tagged by record
|
||||
type for easy retrieval, review, and audit export.
|
||||
|
||||
What it does
|
||||
------------
|
||||
* Creates a dedicated Documents workspace: "Plating — Quality"
|
||||
* Creates a "Record Type" facet with four tags: NCR, CAPA, FAIR, Doc Control
|
||||
* Overrides `ir.attachment.create()` so attachments added to supported quality
|
||||
records are silently mirrored as `documents.document` records in the
|
||||
workspace and tagged with the appropriate record type
|
||||
* Adds a "Documents" smart button on each NCR, CAPA, FAIR, and Doc Control form
|
||||
view that opens the filtered Documents kanban for that record
|
||||
* Ships with `auto_install = True` so no manual install step is required — the
|
||||
bridge activates as soon as both pre-requisite modules are present
|
||||
|
||||
Why this module exists
|
||||
----------------------
|
||||
The Community-Edition-compatible `fusion_plating_quality` module intentionally
|
||||
does NOT depend on the Enterprise `documents` module. On Enterprise deployments
|
||||
this bridge provides the richer Documents-app experience (workspaces, tags,
|
||||
bulk download, preview, sharing) without ever touching the core or quality
|
||||
modules — both stay CE-safe and upgradable.
|
||||
|
||||
Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
""",
|
||||
'author': 'Nexa Systems Inc.',
|
||||
'website': 'https://www.nexasystems.ca',
|
||||
'maintainer': 'Nexa Systems Inc.',
|
||||
'support': 'support@nexasystems.ca',
|
||||
'license': 'OPL-1',
|
||||
'price': 0.00,
|
||||
'currency': 'CAD',
|
||||
'depends': [
|
||||
'fusion_plating_quality',
|
||||
'documents',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/documents_folder_data.xml',
|
||||
'data/documents_tag_data.xml',
|
||||
'views/fp_ncr_views.xml',
|
||||
'views/fp_capa_views.xml',
|
||||
'views/fp_fair_views.xml',
|
||||
'views/fp_doc_control_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
|
||||
Workspace folder for Fusion Plating QMS attachments.
|
||||
|
||||
Odoo 19 EE unified the Documents data model: folders are simply
|
||||
``documents.document`` records with ``type = 'folder'`` and a null
|
||||
``folder_id`` (root) or a parent folder reference. This matches the
|
||||
Odoo 18.x / 19.x behaviour where the Documents kanban is driven by
|
||||
a single model rather than a separate ``documents.folder`` model.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="documents_folder_plating_quality" model="documents.document">
|
||||
<field name="name">Plating — Quality</field>
|
||||
<field name="type">folder</field>
|
||||
<field name="folder_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
|
||||
Tags used by the bridge to categorise mirrored quality attachments.
|
||||
|
||||
Odoo 19 EE organises tags under a two-level hierarchy:
|
||||
documents.facet (a.k.a. "category" — scoped to a folder/workspace)
|
||||
└── documents.tag (individual tags, required to have a facet_id)
|
||||
|
||||
We create one facet called "Record Type" inside the Plating — Quality
|
||||
workspace, then four tags beneath it — one per supported quality
|
||||
record type.
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="documents_facet_record_type" model="documents.facet">
|
||||
<field name="name">Record Type</field>
|
||||
<field name="folder_id" ref="documents_folder_plating_quality"/>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
|
||||
<record id="documents_tag_ncr" model="documents.tag">
|
||||
<field name="name">NCR</field>
|
||||
<field name="facet_id" ref="documents_facet_record_type"/>
|
||||
<field name="sequence">10</field>
|
||||
</record>
|
||||
|
||||
<record id="documents_tag_capa" model="documents.tag">
|
||||
<field name="name">CAPA</field>
|
||||
<field name="facet_id" ref="documents_facet_record_type"/>
|
||||
<field name="sequence">20</field>
|
||||
</record>
|
||||
|
||||
<record id="documents_tag_fair" model="documents.tag">
|
||||
<field name="name">FAIR</field>
|
||||
<field name="facet_id" ref="documents_facet_record_type"/>
|
||||
<field name="sequence">30</field>
|
||||
</record>
|
||||
|
||||
<record id="documents_tag_doc_control" model="documents.tag">
|
||||
<field name="name">Doc Control</field>
|
||||
<field name="facet_id" ref="documents_facet_record_type"/>
|
||||
<field name="sequence">40</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from . import ir_attachment
|
||||
from . import fp_ncr
|
||||
from . import fp_capa
|
||||
from . import fp_fair
|
||||
from . import fp_doc_control
|
||||
@@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
_FOLDER_XMLID = 'fusion_plating_bridge_documents.documents_folder_plating_quality'
|
||||
|
||||
|
||||
class FpCapa(models.Model):
|
||||
"""Bridge extension: expose Documents workspace on CAPAs."""
|
||||
_inherit = 'fusion.plating.capa'
|
||||
|
||||
x_fc_document_ids = fields.Many2many(
|
||||
'documents.document',
|
||||
'fp_bridge_capa_document_rel',
|
||||
'capa_id',
|
||||
'document_id',
|
||||
string='Quality Documents',
|
||||
compute='_compute_x_fc_document_ids',
|
||||
store=False,
|
||||
help='Documents in the Plating — Quality workspace mirrored from '
|
||||
'attachments on this CAPA.',
|
||||
)
|
||||
x_fc_document_count = fields.Integer(
|
||||
string='# Documents',
|
||||
compute='_compute_x_fc_document_ids',
|
||||
store=False,
|
||||
)
|
||||
|
||||
@api.depends('message_attachment_count')
|
||||
def _compute_x_fc_document_ids(self):
|
||||
Document = self.env.get('documents.document') if 'documents.document' in self.env else None
|
||||
for rec in self:
|
||||
if not Document:
|
||||
rec.x_fc_document_ids = False
|
||||
rec.x_fc_document_count = 0
|
||||
continue
|
||||
docs = Document.sudo().search([
|
||||
('attachment_id.res_model', '=', 'fusion.plating.capa'),
|
||||
('attachment_id.res_id', '=', rec.id),
|
||||
])
|
||||
rec.x_fc_document_ids = docs
|
||||
rec.x_fc_document_count = len(docs)
|
||||
|
||||
def action_view_documents(self):
|
||||
self.ensure_one()
|
||||
folder_id = self._get_default_folder_id()
|
||||
ctx = {}
|
||||
if folder_id:
|
||||
ctx['default_folder_id'] = folder_id
|
||||
ctx['searchpanel_default_folder_id'] = folder_id
|
||||
return {
|
||||
'name': _('Quality Documents'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'documents.document',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'domain': [('id', 'in', self.x_fc_document_ids.ids)],
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
def _get_default_folder_id(self):
|
||||
folder = self.env.ref(_FOLDER_XMLID, raise_if_not_found=False)
|
||||
return folder.id if folder else 0
|
||||
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
_FOLDER_XMLID = 'fusion_plating_bridge_documents.documents_folder_plating_quality'
|
||||
|
||||
|
||||
class FpDocControl(models.Model):
|
||||
"""Bridge extension: expose Documents workspace on Controlled Documents.
|
||||
|
||||
Doc Control already carries a native ``attachment_ids`` Many2many; the
|
||||
bridge additionally exposes the `documents.document` mirror so users can
|
||||
jump straight into the Documents app to use its preview, tag, share,
|
||||
and lock features.
|
||||
"""
|
||||
_inherit = 'fusion.plating.doc.control'
|
||||
|
||||
x_fc_document_ids = fields.Many2many(
|
||||
'documents.document',
|
||||
'fp_bridge_doc_control_document_rel',
|
||||
'doc_id',
|
||||
'document_id',
|
||||
string='Quality Documents',
|
||||
compute='_compute_x_fc_document_ids',
|
||||
store=False,
|
||||
help='Documents in the Plating — Quality workspace mirrored from '
|
||||
'attachments on this controlled document record.',
|
||||
)
|
||||
x_fc_document_count = fields.Integer(
|
||||
string='# Documents',
|
||||
compute='_compute_x_fc_document_ids',
|
||||
store=False,
|
||||
)
|
||||
|
||||
@api.depends('attachment_ids', 'message_attachment_count')
|
||||
def _compute_x_fc_document_ids(self):
|
||||
Document = self.env.get('documents.document') if 'documents.document' in self.env else None
|
||||
for rec in self:
|
||||
if not Document:
|
||||
rec.x_fc_document_ids = False
|
||||
rec.x_fc_document_count = 0
|
||||
continue
|
||||
# Pull in both chatter attachments (matched via res_model/res_id) and
|
||||
# any documents whose underlying ir.attachment is in the native
|
||||
# attachment_ids M2m on this controlled document record.
|
||||
native_attachment_ids = rec.attachment_ids.ids
|
||||
if native_attachment_ids:
|
||||
domain = [
|
||||
'|',
|
||||
'&', ('attachment_id.res_model', '=', 'fusion.plating.doc.control'),
|
||||
('attachment_id.res_id', '=', rec.id),
|
||||
('attachment_id', 'in', native_attachment_ids),
|
||||
]
|
||||
else:
|
||||
domain = [
|
||||
('attachment_id.res_model', '=', 'fusion.plating.doc.control'),
|
||||
('attachment_id.res_id', '=', rec.id),
|
||||
]
|
||||
docs = Document.sudo().search(domain)
|
||||
rec.x_fc_document_ids = docs
|
||||
rec.x_fc_document_count = len(docs)
|
||||
|
||||
def action_view_documents(self):
|
||||
self.ensure_one()
|
||||
folder_id = self._get_default_folder_id()
|
||||
ctx = {}
|
||||
if folder_id:
|
||||
ctx['default_folder_id'] = folder_id
|
||||
ctx['searchpanel_default_folder_id'] = folder_id
|
||||
return {
|
||||
'name': _('Quality Documents'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'documents.document',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'domain': [('id', 'in', self.x_fc_document_ids.ids)],
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
def _get_default_folder_id(self):
|
||||
folder = self.env.ref(_FOLDER_XMLID, raise_if_not_found=False)
|
||||
return folder.id if folder else 0
|
||||
@@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
_FOLDER_XMLID = 'fusion_plating_bridge_documents.documents_folder_plating_quality'
|
||||
|
||||
|
||||
class FpFair(models.Model):
|
||||
"""Bridge extension: expose Documents workspace on FAIRs."""
|
||||
_inherit = 'fusion.plating.fair'
|
||||
|
||||
x_fc_document_ids = fields.Many2many(
|
||||
'documents.document',
|
||||
'fp_bridge_fair_document_rel',
|
||||
'fair_id',
|
||||
'document_id',
|
||||
string='Quality Documents',
|
||||
compute='_compute_x_fc_document_ids',
|
||||
store=False,
|
||||
help='Documents in the Plating — Quality workspace mirrored from '
|
||||
'attachments on this FAIR.',
|
||||
)
|
||||
x_fc_document_count = fields.Integer(
|
||||
string='# Documents',
|
||||
compute='_compute_x_fc_document_ids',
|
||||
store=False,
|
||||
)
|
||||
|
||||
@api.depends('message_attachment_count')
|
||||
def _compute_x_fc_document_ids(self):
|
||||
Document = self.env.get('documents.document') if 'documents.document' in self.env else None
|
||||
for rec in self:
|
||||
if not Document:
|
||||
rec.x_fc_document_ids = False
|
||||
rec.x_fc_document_count = 0
|
||||
continue
|
||||
docs = Document.sudo().search([
|
||||
('attachment_id.res_model', '=', 'fusion.plating.fair'),
|
||||
('attachment_id.res_id', '=', rec.id),
|
||||
])
|
||||
rec.x_fc_document_ids = docs
|
||||
rec.x_fc_document_count = len(docs)
|
||||
|
||||
def action_view_documents(self):
|
||||
self.ensure_one()
|
||||
folder_id = self._get_default_folder_id()
|
||||
ctx = {}
|
||||
if folder_id:
|
||||
ctx['default_folder_id'] = folder_id
|
||||
ctx['searchpanel_default_folder_id'] = folder_id
|
||||
return {
|
||||
'name': _('Quality Documents'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'documents.document',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'domain': [('id', 'in', self.x_fc_document_ids.ids)],
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
def _get_default_folder_id(self):
|
||||
folder = self.env.ref(_FOLDER_XMLID, raise_if_not_found=False)
|
||||
return folder.id if folder else 0
|
||||
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
_FOLDER_XMLID = 'fusion_plating_bridge_documents.documents_folder_plating_quality'
|
||||
|
||||
|
||||
class FpNcr(models.Model):
|
||||
"""Bridge extension: expose Documents workspace on NCRs.
|
||||
|
||||
Adds a reverse link to any `documents.document` records that were
|
||||
created by the bridge override on `ir.attachment`, plus a smart
|
||||
button action that opens the filtered Documents kanban for the
|
||||
current NCR.
|
||||
"""
|
||||
_inherit = 'fusion.plating.ncr'
|
||||
|
||||
x_fc_document_ids = fields.Many2many(
|
||||
'documents.document',
|
||||
'fp_bridge_ncr_document_rel',
|
||||
'ncr_id',
|
||||
'document_id',
|
||||
string='Quality Documents',
|
||||
compute='_compute_x_fc_document_ids',
|
||||
store=False,
|
||||
help='Documents in the Plating — Quality workspace mirrored from '
|
||||
'attachments on this NCR.',
|
||||
)
|
||||
x_fc_document_count = fields.Integer(
|
||||
string='# Documents',
|
||||
compute='_compute_x_fc_document_ids',
|
||||
store=False,
|
||||
)
|
||||
|
||||
@api.depends('message_attachment_count')
|
||||
def _compute_x_fc_document_ids(self):
|
||||
Document = self.env.get('documents.document') if 'documents.document' in self.env else None
|
||||
for rec in self:
|
||||
if not Document:
|
||||
rec.x_fc_document_ids = False
|
||||
rec.x_fc_document_count = 0
|
||||
continue
|
||||
docs = Document.sudo().search([
|
||||
('attachment_id.res_model', '=', 'fusion.plating.ncr'),
|
||||
('attachment_id.res_id', '=', rec.id),
|
||||
])
|
||||
rec.x_fc_document_ids = docs
|
||||
rec.x_fc_document_count = len(docs)
|
||||
|
||||
def action_view_documents(self):
|
||||
self.ensure_one()
|
||||
folder_id = self._get_default_folder_id()
|
||||
ctx = {}
|
||||
if folder_id:
|
||||
ctx['default_folder_id'] = folder_id
|
||||
ctx['searchpanel_default_folder_id'] = folder_id
|
||||
return {
|
||||
'name': _('Quality Documents'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'documents.document',
|
||||
'view_mode': 'kanban,list,form',
|
||||
'domain': [('id', 'in', self.x_fc_document_ids.ids)],
|
||||
'context': ctx,
|
||||
}
|
||||
|
||||
def _get_default_folder_id(self):
|
||||
folder = self.env.ref(_FOLDER_XMLID, raise_if_not_found=False)
|
||||
return folder.id if folder else 0
|
||||
@@ -0,0 +1,122 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Map of quality-module res_model -> bridge tag XML id.
|
||||
# Kept module-level so both create() and write() can consult it cheaply.
|
||||
_QUALITY_MODELS_TO_TAG = {
|
||||
'fusion.plating.ncr': 'fusion_plating_bridge_documents.documents_tag_ncr',
|
||||
'fusion.plating.capa': 'fusion_plating_bridge_documents.documents_tag_capa',
|
||||
'fusion.plating.fair': 'fusion_plating_bridge_documents.documents_tag_fair',
|
||||
'fusion.plating.doc.control': 'fusion_plating_bridge_documents.documents_tag_doc_control',
|
||||
}
|
||||
|
||||
_FOLDER_XMLID = 'fusion_plating_bridge_documents.documents_folder_plating_quality'
|
||||
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
"""Bridge ir.attachment with Odoo EE `documents.document`.
|
||||
|
||||
Whenever an attachment is created on one of the Fusion Plating QMS
|
||||
record types (NCR, CAPA, FAIR, Doc Control) we silently mirror it as
|
||||
a `documents.document` record inside the "Plating — Quality"
|
||||
workspace, tagged with the corresponding record type. The original
|
||||
`ir.attachment` record is untouched and continues to live on the
|
||||
quality record as before — the bridge is purely additive.
|
||||
|
||||
Design notes
|
||||
------------
|
||||
* We resolve the folder and tag XML ids via ``env.ref`` with
|
||||
``raise_if_not_found=False`` so that a partial install, a missing
|
||||
demo record, or a future schema change can never break attachment
|
||||
creation on a quality record — the worst case is that the
|
||||
`documents.document` mirror record isn't created and a line goes
|
||||
to the log.
|
||||
* The write is wrapped in a broad try/except for the same reason:
|
||||
user-facing attachment creation must never be blocked by a bridge
|
||||
failure.
|
||||
* We use ``sudo()`` on the `documents.document` create because the
|
||||
user uploading the attachment may not have write access to the
|
||||
Documents app — the bridge is a system-level convenience.
|
||||
"""
|
||||
_inherit = 'ir.attachment'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
attachments = super().create(vals_list)
|
||||
try:
|
||||
self._fusion_plating_bridge_promote_to_documents(attachments)
|
||||
except Exception: # pragma: no cover - defensive only
|
||||
_logger.exception(
|
||||
"Fusion Plating Documents bridge: failed to promote attachments %s",
|
||||
attachments.ids,
|
||||
)
|
||||
return attachments
|
||||
|
||||
def _fusion_plating_bridge_promote_to_documents(self, attachments):
|
||||
"""Create `documents.document` mirror records for quality attachments.
|
||||
|
||||
Silently skips if:
|
||||
- the documents module isn't in the registry (defensive, the
|
||||
manifest already depends on it but this module may be tested
|
||||
on CE)
|
||||
- the target folder hasn't been created yet
|
||||
- the attachment is not attached to a quality record
|
||||
"""
|
||||
if 'documents.document' not in self.env:
|
||||
return
|
||||
|
||||
folder = self.env.ref(_FOLDER_XMLID, raise_if_not_found=False)
|
||||
if not folder:
|
||||
_logger.warning(
|
||||
"Fusion Plating Documents bridge: target folder %s not found",
|
||||
_FOLDER_XMLID,
|
||||
)
|
||||
return
|
||||
|
||||
Document = self.env['documents.document'].sudo()
|
||||
|
||||
# Cache tag lookups across the batch so we don't hit env.ref per attachment.
|
||||
tag_cache = {}
|
||||
|
||||
for att in attachments:
|
||||
if att.res_model not in _QUALITY_MODELS_TO_TAG:
|
||||
continue
|
||||
# Skip attachments linked to a specific field (e.g. image_1920) —
|
||||
# those are UI artefacts, not user-uploaded docs.
|
||||
if att.res_field:
|
||||
continue
|
||||
# Skip records that have no concrete res_id (drafts being built).
|
||||
if not att.res_id:
|
||||
continue
|
||||
|
||||
tag_xmlid = _QUALITY_MODELS_TO_TAG[att.res_model]
|
||||
if tag_xmlid not in tag_cache:
|
||||
tag = self.env.ref(tag_xmlid, raise_if_not_found=False)
|
||||
tag_cache[tag_xmlid] = tag.id if tag else False
|
||||
tag_id = tag_cache[tag_xmlid]
|
||||
|
||||
doc_vals = {
|
||||
'attachment_id': att.id,
|
||||
'folder_id': folder.id,
|
||||
'name': att.name or 'Untitled',
|
||||
}
|
||||
if tag_id:
|
||||
doc_vals['tag_ids'] = [(4, tag_id)]
|
||||
|
||||
try:
|
||||
Document.create(doc_vals)
|
||||
except Exception: # pragma: no cover - defensive only
|
||||
_logger.exception(
|
||||
"Fusion Plating Documents bridge: could not create "
|
||||
"documents.document for attachment id=%s (res_model=%s, res_id=%s)",
|
||||
att.id, att.res_model, att.res_id,
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_documents_document_fp_operator,documents.document.fp.operator,documents.model_documents_document,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_documents_document_fp_supervisor,documents.document.fp.supervisor,documents.model_documents_document,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
|
||||
access_documents_document_fp_manager,documents.document.fp.manager,documents.model_documents_document,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_documents_tag_fp_operator,documents.tag.fp.operator,documents.model_documents_tag,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_documents_tag_fp_manager,documents.tag.fp.manager,documents.model_documents_tag,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_documents_facet_fp_operator,documents.facet.fp.operator,documents.model_documents_facet,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_documents_facet_fp_manager,documents.facet.fp.manager,documents.model_documents_facet,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
|
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
|
||||
The upstream fusion_plating_quality CAPA form does not ship with a
|
||||
button_box. We inject one at the top of the <sheet> so our stat button
|
||||
has somewhere to live. Adding via the sheet xpath keeps the core form
|
||||
untouched.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_capa_form_bridge_documents" model="ir.ui.view">
|
||||
<field name="name">fp.capa.form.bridge.documents</field>
|
||||
<field name="model">fusion.plating.capa</field>
|
||||
<field name="inherit_id" ref="fusion_plating_quality.view_fp_capa_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//sheet/div[hasclass('oe_title')]" position="before">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_documents" type="object"
|
||||
class="oe_stat_button" icon="fa-folder-open">
|
||||
<field name="x_fc_document_count" widget="statinfo" string="Documents"/>
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
|
||||
The upstream fusion_plating_quality Doc Control form does not ship
|
||||
with a button_box. We inject one at the top of the <sheet> so our
|
||||
stat button has somewhere to live.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_doc_control_form_bridge_documents" model="ir.ui.view">
|
||||
<field name="name">fp.doc.control.form.bridge.documents</field>
|
||||
<field name="model">fusion.plating.doc.control</field>
|
||||
<field name="inherit_id" ref="fusion_plating_quality.view_fp_doc_control_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//sheet/div[hasclass('oe_title')]" position="before">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_documents" type="object"
|
||||
class="oe_stat_button" icon="fa-folder-open">
|
||||
<field name="x_fc_document_count" widget="statinfo" string="Documents"/>
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
|
||||
The upstream fusion_plating_quality FAIR form does not ship with a
|
||||
button_box. We inject one at the top of the <sheet> so our stat button
|
||||
has somewhere to live.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_fair_form_bridge_documents" model="ir.ui.view">
|
||||
<field name="name">fp.fair.form.bridge.documents</field>
|
||||
<field name="model">fusion.plating.fair</field>
|
||||
<field name="inherit_id" ref="fusion_plating_quality.view_fp_fair_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//sheet/div[hasclass('oe_title')]" position="before">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_documents" type="object"
|
||||
class="oe_stat_button" icon="fa-folder-open">
|
||||
<field name="x_fc_document_count" widget="statinfo" string="Documents"/>
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_ncr_form_bridge_documents" model="ir.ui.view">
|
||||
<field name="name">fp.ncr.form.bridge.documents</field>
|
||||
<field name="model">fusion.plating.ncr</field>
|
||||
<field name="inherit_id" ref="fusion_plating_quality.view_fp_ncr_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_view_documents" type="object"
|
||||
class="oe_stat_button" icon="fa-folder-open">
|
||||
<field name="x_fc_document_count" widget="statinfo" string="Documents"/>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user