Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Ghostscript
run: apt-get update && apt-get install -y ghostscript
- name: Install addons and dependencies
run: oca_install_addons
- name: Check licenses
Expand Down
152 changes: 152 additions & 0 deletions attachment_pdfa/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
================
Attachment PDF/A
================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:607a7dc56a5708440b64968c78229b78bc02b715336989ba87460c94bd98e2fb
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |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/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github
:target: https://github.com/OCA/storage/tree/18.0/attachment_pdfa
:alt: OCA/storage
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/storage-18-0/storage-18-0-attachment_pdfa
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=18.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module extends Odoo's attachment system to support background
conversion of PDF files to the **PDF/A-3** format.

This module provides a flexible framework to automate PDF to PDF/A-3
conversion via an asynchronous background cron:

- **Opt-in via Mixin**: Rather than blindly converting all system PDFs,
models must opt-in by inheriting ``attachment.pdfa.mixin`` and
implementing conditional rules via
``_attachment_must_be_pdfa(attachment)``.
- **Cron**: Attachments requiring conversion are flagged via the
``is_pdfa_needed`` field on ``ir.attachment`` and processed by a
background cron.
- **Flexible Storage Strategy**: Configurable options allow converted
PDF/A files to either replace the original attachments or be saved
alongside them as new attachments.
- **Pluggable Engine**: Offers conversion via **Ghostscript** or native
**Odoo** PDF writer.

**Table of contents**

.. contents::
:local:

Use Cases / Context
===================

In many legal frameworks and corporate archiving policies, business
documents must be stored in a compliant, long-term preservation format
like **PDF/A-3**.

Installation
============

To install and use Ghostscript conversion (default engine):

1. Ensure Ghostscript is installed on your Odoo server host system.
2. Ensure the ``gs`` binary is available in the system execution PATH of
the user running Odoo.

Configuration
=============

To configure this module:

1. Go to **Settings** > **General Settings**.
2. Scroll down to the **PDF/A Conversion** section under *Document
Settings*.
3. Configure your options:

- **PDF to PDF/A Conversion Method**:

- **Ghostscript** (default).
- **Odoo**.
- **Disable**: Disables automatic conversion globally.

- **PDF/A Storage Strategy**:

- **Replace Original File** (default): Overwrites the original PDF
attachment with the PDF/A binary.
- **Store Beside Original File**: Preserves the original file and
creates a new attachment named ``<Filename>_PDFA.pdf`` alongside
it.

4. Click **Save**.

Usage
=====

To use this module:

1. Create or generate a PDF attachment on a record whose model
implements ``attachment.pdfa.mixin`` (and where
``_attachment_must_be_pdfa(attachment)`` evaluates to ``True``).
2. The attachment automatically flags ``is_pdfa_needed = True``.
3. The scheduled cron processes pending attachments in the background,
updating or creating attachments according to your configured storage
strategy.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/storage/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
`feedback <https://github.com/OCA/storage/issues/new?body=module:%20attachment_pdfa%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* ACSONE SA/NV

Contributors
------------

- Zina Rasoamanana zina.rasoamanana@acsone.eu
(`Acsone <https://acsone.eu>`__)
- Benoit Aimont benoit.aimont@acsone.eu (`Acsone <https://acsone.eu>`__)
- Stéphane Bidoul stephane.bidoul@acsone.eu
(`Acsone <https://acsone.eu>`__)

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/storage <https://github.com/OCA/storage/tree/18.0/attachment_pdfa>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions attachment_pdfa/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
27 changes: 27 additions & 0 deletions attachment_pdfa/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright 2026 ACSONE SA/NV
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).

{
"name": "Attachment PDF/A",
"summary": """Convert PDF attachments to PDF/A""",
"version": "18.0.1.0.0",
"category": "Storage",
"license": "LGPL-3",
"author": "ACSONE SA/NV, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/storage",
"depends": [
"base_setup",
],
"data": [
"data/ir_cron.xml",
"data/config_parameter.xml",
"data/pdfa3_metadata.xml",
"views/res_config_settings.xml",
],
"external_dependencies": {
"bin": [
"gs",
],
},
"installable": True,
}
13 changes: 13 additions & 0 deletions attachment_pdfa/data/config_parameter.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2026 ACSONE SA/NV
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). -->
<odoo noupdate="1">
<record id="function_convert_pdfa" model="ir.config_parameter">
<field name="key">attachment_pdfa.method</field>
<field name="value">ghostscript</field>
</record>
<record id="storage_mode_pdfa" model="ir.config_parameter">
<field name="key">attachment_pdfa.mode</field>
<field name="value">replace</field>
</record>
</odoo>
Binary file added attachment_pdfa/data/iccprofiles_default_rgb.icc
Binary file not shown.
13 changes: 13 additions & 0 deletions attachment_pdfa/data/ir_cron.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2026 ACSONE SA/NV
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). -->
<odoo noupdate="1">
<record id="ir_cron_convert_pdfa" model="ir.cron">
<field name="name">Convert Attachments to PDF/A</field>
<field name="model_id" ref="model_ir_attachment" />
<field name="state">code</field>
<field name="code">model._cron_convert_pdfa()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
</record>
</odoo>
46 changes: 46 additions & 0 deletions attachment_pdfa/data/pdfa3_metadata.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="pdfa3_metadata">
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description
xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/"
rdf:about=""
>
<pdfaid:part>3</pdfaid:part>
<pdfaid:conformance>B</pdfaid:conformance>
</rdf:Description>
<rdf:Description
xmlns:dc="http://purl.org/dc/elements/1.1/"
rdf:about=""
>
<dc:title>
<rdf:Alt>
<rdf:li t-att="{'xml:lang': 'x-default'}" t-out="title" />
</rdf:Alt>
</dc:title>
<dc:creator>
<rdf:Seq>
<rdf:li>Odoo</rdf:li>
</rdf:Seq>
</dc:creator>
<dc:description>
<rdf:Alt>
<rdf:li
t-att="{'xml:lang': 'x-default'}"
>PDF/A3 generated by Odoo</rdf:li>
</rdf:Alt>
</dc:description>
</rdf:Description>
<rdf:Description xmlns:pdf="http://ns.adobe.com/pdf/1.3/" rdf:about="">
<pdf:Producer>Odoo</pdf:Producer>
</rdf:Description>
<rdf:Description xmlns:xmp="http://ns.adobe.com/xap/1.0/" rdf:about="">
<xmp:CreatorTool>Odoo</xmp:CreatorTool>
<xmp:CreateDate t-out="date" />
<xmp:ModifyDate t-out="date" />
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
</template>
</odoo>
Binary file added attachment_pdfa/data/pdfa_def.ps
Binary file not shown.
3 changes: 3 additions & 0 deletions attachment_pdfa/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import attachment_pdfa_mixin
from . import res_config_settings
from . import ir_attachment
16 changes: 16 additions & 0 deletions attachment_pdfa/models/attachment_pdfa_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2026 ACSONE SA/NV
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).

from odoo import models


class AttachmentPdfaMixin(models.AbstractModel):
_name = "attachment.pdfa.mixin"
_description = "Attachment PDF/A Mixin"

def _attachment_must_be_pdfa(self, attachment):
"""Determine whether an attachment requires PDF/A conversion.

:param attachment: ir.attachment record
"""
return False
Loading
Loading