Maintainers
+Maintainers
This module is maintained by the OCA.
@@ -844,6 +879,5 @@ diff --git a/fs_attachment/README.rst b/fs_attachment/README.rst index b50b5d9d27..8338203c1e 100644 --- a/fs_attachment/README.rst +++ b/fs_attachment/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ============================ Base Attachment Object Store ============================ @@ -17,7 +13,7 @@ Base Attachment Object Store .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github @@ -179,6 +175,47 @@ to different resource fields/models. You can configure it either on the ids provided by Odoo. See the Server Environment section for a concrete example. +Dynamic routing rules +~~~~~~~~~~~~~~~~~~~~~ + +On top of the static ``model_ids``/``field_ids`` mapping described +above, you can define dynamic routing rules from the menu +``Settings > Technical > FS Storage Rules``. Unlike the static mapping, +a rule can look at the actual record the attachment is linked to (its +state, a related field, etc.) and not just its model or field. + +Each rule defines: + +- ``Model``: the resource model the rule applies to. +- ``Field``: optional. Restricts the rule to attachments stored through + this specific binary field. Leave it empty to match any field, + including attachments with no ``res_field`` at all. +- ``Domain``: a domain evaluated against the resource record. Rules are + evaluated in ``sequence`` order; the first one whose domain matches + wins. +- ``Storage``: the storage to use when the domain matches. + +For example, to route attachments of company partners to a dedicated +storage while individuals keep using the default one, create a rule on +``res.partner`` with the domain ``[('is_company', '=', True)]``. + +The storage of an attachment is therefore resolved in this order: + +1. The first matching dynamic ``fs.storage.rule``, if any. +2. The static ``model_ids``/``field_ids`` mapping (or + ``model_xmlids``/``field_xmlids`` from a server environment file). +3. The storage configured as the default for attachments. + +Note + +A rule's domain is only evaluated when the attachment is created or when +its content is rewritten (via ``open()``, where ``new_version=True`` by +default). It is **not** re-evaluated afterwards when the record changes. +For example, an invoice PDF generated while the invoice is ``posted`` +keeps the storage selected at that time, even if the invoice is later +reset to draft. Routing is a permanent decision made when the file is +written, not a live reflection of the record's current state. + Another key feature of this module is the ability to get access to the attachments from URLs. diff --git a/fs_attachment/models/fs_storage.py b/fs_attachment/models/fs_storage.py index b403412cd9..6475f785ca 100644 --- a/fs_attachment/models/fs_storage.py +++ b/fs_attachment/models/fs_storage.py @@ -195,14 +195,16 @@ def get_storage_code_for_attachments_fallback(self): @api.model def get_default_storage_code_for_attachments(self): - """Return the code of the storage to use to store the attachments. - If the resource field is linked to a particular storage, return this one. - Otherwise if the resource model is linked to a particular storage, - return it. - Finally return the code of the storage to use by default.""" + """Return the code of the storage to use to store the attachment. + + Dynamic fs.storage.rule take priority (record-aware, e.g. state), + then the static per-field/per-model mapping, then the global + default storage. + """ res_field = self.env.context.get("attachment_res_field") res_model = self.env.context.get("attachment_res_model") - storage_code = self.get_storage_code_by_model_field(res_model, res_field) + res_id = self.env.context.get("attachment_res_id") + storage_code = self._get_storage_code_for_record(res_model, res_id, res_field) if not storage_code: storage_code = self.get_storage_code_for_attachments_fallback() return storage_code diff --git a/fs_attachment/models/ir_attachment.py b/fs_attachment/models/ir_attachment.py index 72ca189736..755a6cc9b5 100644 --- a/fs_attachment/models/ir_attachment.py +++ b/fs_attachment/models/ir_attachment.py @@ -283,6 +283,7 @@ def create(self, vals_list): self.with_context( attachment_res_model=vals.get("res_model"), attachment_res_field=vals.get("res_field"), + attachment_res_id=vals.get("res_id"), ), ).create(vals) attachments += attachment @@ -332,6 +333,7 @@ def write(self, vals): rec.with_context( attachment_res_model=vals.get("res_model") or rec.res_model, attachment_res_field=vals.get("res_field") or rec.res_field, + attachment_res_id=vals.get("res_id") or rec.res_id, ), ).write(vals) @@ -1084,6 +1086,7 @@ def _file_open(self) -> io.IOBase: new_store_fname = self.attachment.with_context( attachment_res_model=self.attachment.res_model, attachment_res_field=self.attachment.res_field, + attachment_res_id=self.attachment.res_id, )._file_write(content, checksum) if self.attachment._is_file_from_a_storage(new_store_fname): ( diff --git a/fs_attachment/readme/USAGE.md b/fs_attachment/readme/USAGE.md index 6283ddb40d..4023493787 100644 --- a/fs_attachment/readme/USAGE.md +++ b/fs_attachment/readme/USAGE.md @@ -89,6 +89,45 @@ to different resource fields/models. You can configure it either on the ids provided by Odoo. See the Server Environment section for a concrete example. +### Dynamic routing rules + +On top of the static `model_ids`/`field_ids` mapping described above, you can +define dynamic routing rules from the menu `Settings > Technical > FS Storage +Rules`. Unlike the static mapping, a rule can look at the actual record the +attachment is linked to (its state, a related field, etc.) and not just its +model or field. + +Each rule defines: + +- `Model`: the resource model the rule applies to. +- `Field`: optional. Restricts the rule to attachments stored through this + specific binary field. Leave it empty to match any field, including + attachments with no `res_field` at all. +- `Domain`: a domain evaluated against the resource record. Rules are + evaluated in `sequence` order; the first one whose domain matches wins. +- `Storage`: the storage to use when the domain matches. + +For example, to route attachments of company partners to a dedicated +storage while individuals keep using the default one, create a rule on +`res.partner` with the domain `[('is_company', '=', True)]`. + +The storage of an attachment is therefore resolved in this order: + +1. The first matching dynamic `fs.storage.rule`, if any. +2. The static `model_ids`/`field_ids` mapping (or `model_xmlids`/`field_xmlids` + from a server environment file). +3. The storage configured as the default for attachments. + +Note + +A rule's domain is only evaluated when the attachment is created or when its +content is rewritten (via `open()`, where `new_version=True` by default). It is **not** +re-evaluated afterwards when the record changes. For example, an invoice PDF +generated while the invoice is `posted` keeps the storage selected at that +time, even if the invoice is later reset to draft. Routing is a permanent +decision made when the file is written, not a live reflection of the record's +current state. + Another key feature of this module is the ability to get access to the attachments from URLs. @@ -245,7 +284,7 @@ with attachment.open("w", new_version=False) as f: update of modules that are loaded before `fs_attachment` will still be stored in the location defined in the `ir_attachment.location` system parameter (which is `file` by default, meaning the regular on-disk `filestore` - directory). - + directory). + A simple way to work around this issue is to set the `ir_attachment.location` System Parameter record to `db`. diff --git a/fs_attachment/static/description/index.html b/fs_attachment/static/description/index.html index f3194d3593..2303ca2062 100644 --- a/fs_attachment/static/description/index.html +++ b/fs_attachment/static/description/index.html @@ -3,7 +3,7 @@
-In some cases, you need to store attachment in another system that the Odoo’s filestore. For example, when your deployment is based on a multi-server architecture to ensure redundancy and scalability, your @@ -420,42 +415,45 @@
The configuration is done through the creation of a filesytem storage record into odoo. To create a new storage, go to the menu Settings > Technical > FS Storage and click on Create.
@@ -542,6 +540,42 @@On top of the static model_ids/field_ids mapping described +above, you can define dynamic routing rules from the menu +Settings > Technical > FS Storage Rules. Unlike the static mapping, +a rule can look at the actual record the attachment is linked to (its +state, a related field, etc.) and not just its model or field.
+Each rule defines:
+For example, to route attachments of company partners to a dedicated +storage while individuals keep using the default one, create a rule on +res.partner with the domain [('is_company', '=', True)].
+The storage of an attachment is therefore resolved in this order:
+Note
+A rule’s domain is only evaluated when the attachment is created or when +its content is rewritten (via open(), where new_version=True by +default). It is not re-evaluated afterwards when the record changes. +For example, an invoice PDF generated while the invoice is posted +keeps the storage selected at that time, even if the invoice is later +reset to draft. Routing is a permanent decision made when the file is +written, not a live reflection of the record’s current state.
Another key feature of this module is the ability to get access to the attachments from URLs.
When you configure a storage through the use of server environment file, you can provide values for the following keys:
The open method on the attachment can be used to open manipulate the attachment as a file object. The object returned by the call to the method implements methods from io.IOBase. The method can ba called @@ -666,7 +701,7 @@
When working in multi staging environments, the management of the attachments can be tricky. For example, if you have a production @@ -696,11 +731,11 @@
Bugfixes
Bugfixes
Improve performance at creation of an attachment or when the @@ -749,7 +784,7 @@
Bugfixes
Bugfixes
Bugfixes
Bugfixes
Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -800,16 +835,16 @@
Do not contact contributors directly about support or help with technical issues.