Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions fs_attachment/README.rst
Original file line number Diff line number Diff line change
@@ -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
============================
Expand All @@ -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
Expand Down Expand Up @@ -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.

Expand Down
14 changes: 8 additions & 6 deletions fs_attachment/models/fs_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions fs_attachment/models/ir_attachment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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):
(
Expand Down
43 changes: 41 additions & 2 deletions fs_attachment/readme/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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`.
Loading
Loading