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
143 changes: 143 additions & 0 deletions document_page_distribution_download_log/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

=======================================
Document Page Distribution Download Log
=======================================

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

.. |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
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-NuoBiT%2Fodoo--addons-lightgray.png?logo=github
:target: https://github.com/NuoBiT/odoo-addons/tree/14.0/document_page_distribution_download_log
:alt: NuoBiT/odoo-addons

|badge1| |badge2| |badge3|

This module records **per-recipient download evidence** for distributed
document page versions. It is the download-log counterpart of
``document_page_distribution``: while that module owns the person + version
coverage (``document.page.history.recipient``), this module logs each real
access/download of a version's PDF by a covered recipient, and exposes the
download coverage as quality-audit evidence.

For every ``(version, recipient)`` coverage line it adds a download summary
(downloaded, first/last download date, download count) and a detail of every
real access. A download is only ever recorded against an **existing** recipient
line of the same version and person; this module never creates coverage nor
invents recipients (that remains the responsibility of
``document_page_distribution``).

**How it works**

* Saving a document page stores its content as a new version
(``document.page.history``); the page itself always shows the head version.
* Each time a version's content is saved, the single document link in it is
rewritten from the raw ``/web/content/<attachment>`` form to a version-aware
download route that carries the version id.
* Opening that link checks the user's read access to the page, records the
download against the recipient's coverage line of that exact version, and
redirects to the standard ``/web/content`` file download.

**Audit contract**

The download log is **append-only**. Evidence rows are written only by the
system, through the download controller, on the recipient's behalf; the access
rights are **read-only for every role, the Document Manager included**
(``perm_read`` only). No user can create, edit or delete a download record, so
the trail cannot be tampered with — which is the point of a compliance log.
Visibility follows the document's own Security groups: a user sees the download
evidence of the documents they are allowed to read, and a Document Manager sees
all of it.

**Table of contents**

.. contents::
:local:

Usage
=====

.. important::

Install this module **before** the controlled document versions you want to
track are authored. The version-aware download link is written into a
version's content the moment that content is **saved with this module
installed**. A version whose content was saved *before* the install keeps its
raw ``/web/content`` link and is therefore **not tracked** until its next
revision.

#. Install this module on top of *Document Page Distribution*: it adds the
download evidence on top of the person + version coverage.
#. Make sure your controlled documents have a Security group and have been
distributed at least once (which creates the coverage lines).
#. Edit a document page and insert a single document link (one file) in its
content. Saving content with more than one document link is rejected.
Insert the file with the page editor, which binds it to the page: a file
the recipient cannot read (e.g. uploaded outside the editor and bound to
nothing) answers 404 and no download evidence is recorded.
#. On save, the document link is rewritten to a controlled, version-aware
download URL.
#. When a covered recipient opens the link, the access is logged against their
coverage line for that exact version. Opening it again adds a new evidence
row and updates the last download date, keeping the first one. Opening the
link of an old version is always imputed to that old version.
#. If a user with read access but without a coverage line opens the link, the
file is served but no evidence is recorded (coverage stays owned by the
distribution module).
#. In the *Distribution* tab of the document, each recipient row shows whether
they downloaded the current version, with a button opening the full download
detail (date, user, file), next to the send detail.

.. note::

Uninstalling this module does **not** restore the original ``/web/content``
links in versions saved while it was installed: their stored content keeps
the rewritten tracking links, which stop working once the module (and its
download route) is gone. Re-insert the document link manually on the next
revision if you uninstall.

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

Bugs are tracked on `GitHub Issues <https://github.com/NuoBiT/odoo-addons/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/NuoBiT/odoo-addons/issues/new?body=module:%20document_page_distribution_download_log%0Aversion:%2014.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
~~~~~~~

* NuoBiT Solutions
* S.L.

Contributors
~~~~~~~~~~~~

* `NuoBiT <https://www.nuobit.com>`_:

* Deniz Gallo <dgallo@nuobit.com>

Maintainers
~~~~~~~~~~~

This module is part of the `NuoBiT/odoo-addons <https://github.com/NuoBiT/odoo-addons/tree/14.0/document_page_distribution_download_log>`_ project on GitHub.

You are welcome to contribute.
2 changes: 2 additions & 0 deletions document_page_distribution_download_log/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import controllers
from . import models
21 changes: 21 additions & 0 deletions document_page_distribution_download_log/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2026 NuoBiT Solutions SL - Deniz Gallo <dgallo@nuobit.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

{
"name": "Document Page Distribution Download Log",
"summary": "Per-recipient download evidence log for distributed document "
"page versions.",
"version": "14.0.1.0.0",
"category": "Knowledge Management",
"author": "NuoBiT Solutions, S.L.",
"website": "https://github.com/nuobit/odoo-addons",
"license": "AGPL-3",
"depends": [
"document_page_distribution",
],
"data": [
"security/ir.model.access.csv",
"security/document_page_distribution_download_log_security.xml",
"views/document_page_history_recipient_views.xml",
],
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import main
60 changes: 60 additions & 0 deletions document_page_distribution_download_log/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright 2026 NuoBiT Solutions SL - Deniz Gallo <dgallo@nuobit.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from werkzeug.exceptions import NotFound
from werkzeug.utils import redirect

from odoo import http
from odoo.exceptions import AccessError
from odoo.http import request


class DocumentPageDistributionDownloadLogController(http.Controller):
@http.route(
"/document_page_distribution_download_log/download"
"/<int:history_id>/<int:attachment_id>",
type="http",
auth="user",
methods=["GET"],
)
def download_version(self, history_id, attachment_id, **kwargs):
# resolve-then-authorize: sudo only resolves the version; the access
# gate is the page read check below, run as the requesting user.
# Missing and forbidden both answer 404 so the route does not reveal
# whether a version exists.
history = (
request.env["document.page.history"].sudo().browse(history_id).exists()
)
if not history:
raise NotFound()
page = request.env["document.page"].browse(history.page_id.id)
try:
page.check_access_rights("read")
page.check_access_rule("read")
except AccessError:
raise NotFound() from None
if not history._download_attachment_is_tracked(attachment_id):
raise NotFound()
# the version content may still link an attachment deleted afterwards
attachment = request.env["ir.attachment"].browse(attachment_id).exists()
if not attachment:
raise NotFound()
# mirror /web/content's own access check BEFORE logging: a download
# row is evidence a covered user reached a valid, readable file link
# (never log one the redirect would deny with 403, e.g. an attachment
# not bound to the page); serving the binary and its cache semantics
# stay in /web/content.
try:
attachment.check("read")
except AccessError:
raise NotFound() from None
http_request = request.httprequest
history._log_recipient_download(
attachment_id,
user=request.env.user,
ip_address=http_request.remote_addr,
user_agent=http_request.user_agent.string
if http_request.user_agent
else False,
)
return redirect("/web/content/%s?download=true" % attachment_id, code=303)
Loading
Loading