[ADD] tax_receipt_in_portal: publish tax receipts on the portal #34

Open
stephansainleger wants to merge 2 commits from 18.0-tax_receipt_in_portal into 18.0
11 changed files with 397 additions and 93 deletions
Showing only changes of commit 5eb3fe766b - Show all commits

View File

@@ -57,24 +57,26 @@ installed automatically.
Configuration
=============
One system parameter is required
(:menuselection:`Settings --> Technical --> System Parameters`):
Select the report rendering tax receipts in
:menuselection:`Settings --> Invoicing --> Tax Receipts --> Tax Receipt
Report`.
==================================== =========================================
Key Value
==================================== =========================================
``tax_receipt_in_portal.report_uuid`` The ``report_uuid`` of the PDF Form
report that renders tax receipts.
==================================== =========================================
The setting is **per company**: each receipt is rendered with the report of
its *issuing* company (``company_id`` of the receipt), not the current one —
in the portal, the current company is the website's.
A UUID is used rather than a database id so the configuration survives a
restore on another database. The report must:
The selector only offers reports that:
* be of type ``PDF Form`` (``report_type = pdf_form``);
* target the model ``l10n.fr.tax.receipt``.
* are of type ``PDF Form`` (``report_type = pdf_form``);
* target the model ``l10n.fr.tax.receipt``;
* belong to the company, or are shared (no company).
Anything else raises an explicit error at download time instead of serving
a wrong document.
These conditions are checked again at rendering time, since a report can be
edited after being selected. Deleting the selected report empties the
setting.
Without a valid report, donors still see their receipts, but no download
button: a discreet "PDF available soon" mention replaces it.
Bind the PDF fields to receipt field paths, for instance:
@@ -94,6 +96,12 @@ Usage
Donors see only their own receipts, and only once **confirmed**. Confirming
a receipt in the back office is therefore what publishes it.
A download button is shown only when the PDF can actually be rendered: a
report is configured for the issuing company and no mapped field resolves
to an empty value. Otherwise the button is replaced by a "PDF available
soon" mention, and a direct call to the download URL redirects to the
receipt page.
Security
========
@@ -142,6 +150,10 @@ Known issues / Roadmap
* **No archiving.** The PDF is rendered at each download and never stored.
A receipt whose rendering becomes impossible (report deleted or
reconfigured) can no longer be served, even though it was already sent.
* **Availability is evaluated at each display.** Deciding whether to show
the download button resolves every mapped PDF field of each displayed
receipt — at most one page of the list. A report with costly Python
expressions slows the list down accordingly.
* **No email notification** and **no portal account invitation**: a donor
without a portal account cannot reach the page. Both are deliberately
out of scope.

View File

@@ -18,6 +18,7 @@
"security/ir.model.access.csv",
"security/ir_rule.xml",
"views/portal_templates.xml",
"views/res_config_settings_views.xml",
],
"installable": True,
"application": False,

View File

@@ -2,6 +2,8 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
"""Portal routes exposing a donor's own tax receipts."""
from werkzeug.urls import url_encode
from odoo import http
from odoo.exceptions import AccessError, MissingError
from odoo.http import content_disposition, request
@@ -97,6 +99,7 @@ class TaxReceiptCustomerPortal(CustomerPortal):
"page_name": "tax_receipt",
"tax_receipt": receipt_sudo,
"invoices": self._tax_receipt_portal_invoices(receipt_sudo),
"pdf_available": bool(receipt_sudo._get_pdf_available_receipts()),
}
return self._get_page_view_values(
receipt_sudo,
@@ -139,6 +142,11 @@ class TaxReceiptCustomerPortal(CustomerPortal):
values.update(
{
"tax_receipts": receipts_page,
# Computed on the current page only: at most one page of
# receipts is checked per request.
"pdf_available_ids": set(
receipts_page._get_pdf_available_receipts().ids
),
"page_name": "tax_receipt",
"pager": pager,
"default_url": "/my/tax-receipts",
@@ -191,11 +199,17 @@ class TaxReceiptCustomerPortal(CustomerPortal):
rule or on the share token — so a draft receipt or another donor's
receipt is refused.
When the PDF cannot be rendered (no report configured for the
issuing company, or a blank mapped field), the visitor is sent back
to the receipt page, which then shows no download button. This
covers a configuration change between page display and click.
:param receipt_id: database id of the requested receipt.
:param access_token: share token allowing access without
ownership.
:return: an ``application/pdf`` response, or a redirect to
``/my`` when the receipt is not accessible.
:return: an ``application/pdf`` response, a redirect to the
receipt page when no PDF is available, or a redirect to ``/my``
when the receipt is not accessible.
"""
try:
receipt_sudo = self._document_check_access(
@@ -203,6 +217,11 @@ class TaxReceiptCustomerPortal(CustomerPortal):
)
except (AccessError, MissingError):
return request.redirect("/my")
if not receipt_sudo._get_pdf_available_receipts():
detail_url = f"/my/tax-receipts/{receipt_sudo.id}"
if access_token:
detail_url += "?" + url_encode({"access_token": access_token})
return request.redirect(detail_url)
pdf_bytes, filename = receipt_sudo._render_tax_receipt_pdf()
headers = [
("Content-Type", "application/pdf"),

View File

@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-10-20 21:59+0000\n"
"PO-Revision-Date: 2026-10-20 21:59+0000\n"
"POT-Creation-Date: 2026-10-23 10:55+0000\n"
"PO-Revision-Date: 2026-10-23 10:55+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -27,6 +27,8 @@ msgid ""
"<i class=\"fa fa-download me-1\" role=\"img\"/>\n"
" <span>PDF</span>"
msgstr ""
"<i class=\"fa fa-download me-1\" role=\"img\"/>\n"
" <span>PDF</span>"
#. module: tax_receipt_in_portal
#: model_terms:ir.ui.view,arch_db:tax_receipt_in_portal.portal_tax_receipt_page
@@ -92,6 +94,16 @@ msgstr "Avertissement d'accès"
msgid "Amount"
msgstr "Montant"
#. module: tax_receipt_in_portal
#: model:ir.model,name:tax_receipt_in_portal.model_res_company
msgid "Companies"
msgstr "Sociétés"
#. module: tax_receipt_in_portal
#: model:ir.model,name:tax_receipt_in_portal.model_res_config_settings
msgid "Config Settings"
msgstr "Paramètres de configuration"
#. module: tax_receipt_in_portal
#: model:ir.model.fields,help:tax_receipt_in_portal.field_l10n_fr_tax_receipt__access_url
msgid "Customer Portal URL"
@@ -110,20 +122,32 @@ msgstr "Date de facture"
#. module: tax_receipt_in_portal
#. odoo-python
#: code:addons/tax_receipt_in_portal/models/l10n_fr_tax_receipt.py:0
msgid "No report found with report_uuid %(uuid)s, configured in %(param)s."
msgid ""
"No tax receipt report configured for company %(company)s. Select one in "
"Settings > Invoicing > Tax Receipts."
msgstr ""
"Aucun rapport trouvé avec le report_uuid %(uuid)s, configuré dans %(param)s."
"Aucun rapport de reçu fiscal configuré pour la société %(company)s. Sélectionnez-en un dans Paramètres > Facturation > Reçus fiscaux."
#. module: tax_receipt_in_portal
#. odoo-python
#: code:addons/tax_receipt_in_portal/models/l10n_fr_tax_receipt.py:0
msgid ""
"No tax receipt report configured. Set the system parameter %(param)s to the "
"report_uuid of the PDF Form report that renders tax receipts."
#: model_terms:ir.ui.view,arch_db:tax_receipt_in_portal.res_config_settings_view_form
msgid "PDF Form report used to render tax receipts downloaded from the portal"
msgstr ""
"Aucun rapport de reçu fiscal configuré. Renseignez le paramètre système "
"%(param)s avec le report_uuid du rapport PDF Form qui génère les reçus "
"fiscaux."
"Rapport PDF Form utilisé pour générer les reçus fiscaux téléchargés depuis le portail"
#. module: tax_receipt_in_portal
#: model:ir.model.fields,help:tax_receipt_in_portal.field_res_company__tax_receipt_report_id
#: model:ir.model.fields,help:tax_receipt_in_portal.field_res_config_settings__tax_receipt_report_id
msgid ""
"PDF Form report used to render the tax receipts issued by this company in "
"the portal. Without it, donors can see their receipts but not download them."
msgstr ""
"Rapport PDF Form utilisé pour générer dans le portail les reçus fiscaux émis par cette société. Sans lui, les donateurs voient leurs reçus mais ne peuvent pas les télécharger."
#. module: tax_receipt_in_portal
#: model_terms:ir.ui.view,arch_db:tax_receipt_in_portal.portal_my_tax_receipts
#: model_terms:ir.ui.view,arch_db:tax_receipt_in_portal.portal_tax_receipt_page
msgid "PDF available soon"
msgstr "PDF bientôt disponible"
#. module: tax_receipt_in_portal
#: model:ir.model.fields,field_description:tax_receipt_in_portal.field_l10n_fr_tax_receipt__access_url
@@ -165,10 +189,18 @@ msgstr "Jeton de sécurité"
msgid "Tax Receipt -"
msgstr "Reçu fiscal -"
#. module: tax_receipt_in_portal
#: model:ir.model.fields,field_description:tax_receipt_in_portal.field_res_company__tax_receipt_report_id
#: model:ir.model.fields,field_description:tax_receipt_in_portal.field_res_config_settings__tax_receipt_report_id
#: model_terms:ir.ui.view,arch_db:tax_receipt_in_portal.res_config_settings_view_form
msgid "Tax Receipt Report"
msgstr "Rapport de reçu fiscal"
#. module: tax_receipt_in_portal
#: model_terms:ir.ui.view,arch_db:tax_receipt_in_portal.portal_breadcrumbs_tax_receipt
#: model_terms:ir.ui.view,arch_db:tax_receipt_in_portal.portal_my_home_tax_receipt
#: model_terms:ir.ui.view,arch_db:tax_receipt_in_portal.portal_my_tax_receipts
#: model_terms:ir.ui.view,arch_db:tax_receipt_in_portal.res_config_settings_view_form
msgid "Tax Receipts"
msgstr "Reçus fiscaux"

View File

@@ -1 +1,3 @@
from . import l10n_fr_tax_receipt
from . import res_company
from . import res_config_settings

View File

@@ -10,12 +10,6 @@ amounts, numbering and the invoice selection belong to
from odoo import _, models
from odoo.exceptions import UserError
#: ``ir.config_parameter`` key holding the ``report_uuid`` of the
#: ``pdf_form`` report used to render a tax receipt. A UUID is used rather
#: than a database id so the configuration survives a restore on another
#: database.
REPORT_UUID_PARAM = "tax_receipt_in_portal.report_uuid"
#: Receipt states a donor is allowed to see and download. A draft receipt
#: carries no legal number yet and must never reach the portal.
PORTAL_VISIBLE_STATES = ("confirmed", "sent")
@@ -53,39 +47,32 @@ class L10nFrTaxReceipt(models.Model):
return result
def _get_tax_receipt_report(self):
"""Return the ``pdf_form`` report configured to render receipts.
"""Return the ``pdf_form`` report rendering this receipt.
The report is looked up by its ``report_uuid`` stored in the
``tax_receipt_in_portal.report_uuid`` system parameter.
The report is the one selected on the receipt's **issuing**
company (Settings > Invoicing > Tax Receipts), not on the current
company: in the portal, the current company is the website's,
which may differ.
The type and target model are checked again here although the
settings field restricts the choice: the report can be edited
after being selected.
:return: a singleton ``ir.actions.report`` recordset, sudoed.
:raises UserError: if the parameter is unset, if no report carries
that UUID, if the report is not a ``pdf_form`` report, or if it
does not target ``l10n.fr.tax.receipt``.
:raises UserError: if the company has no report configured, if the
report is not a ``pdf_form`` report, or if it does not target
``l10n.fr.tax.receipt``.
"""
params = self.env["ir.config_parameter"].sudo()
report_uuid = (params.get_param(REPORT_UUID_PARAM) or "").strip()
if not report_uuid:
raise UserError(
_(
"No tax receipt report configured. Set the system "
"parameter %(param)s to the report_uuid of the PDF Form "
"report that renders tax receipts.",
param=REPORT_UUID_PARAM,
)
)
report = (
self.env["ir.actions.report"]
.sudo()
.search([("report_uuid", "=", report_uuid)], limit=1)
)
self.ensure_one()
company = self.sudo().company_id
report = company.tax_receipt_report_id
if not report:
raise UserError(
_(
"No report found with report_uuid %(uuid)s, configured "
"in %(param)s.",
uuid=report_uuid,
param=REPORT_UUID_PARAM,
"No tax receipt report configured for company "
"%(company)s. Select one in Settings > Invoicing > Tax "
"Receipts.",
company=company.name,
)
)
if report.report_type != PDF_FORM_REPORT_TYPE:
@@ -153,6 +140,43 @@ class L10nFrTaxReceipt(models.Model):
)
)
def _get_pdf_available_receipts(self):
"""Return the receipts of ``self`` whose PDF can be rendered.
Applies exactly the checks of ``_render_tax_receipt_pdf`` — visible
state, configured and valid report, no blank field — but as a
filter instead of raising, so the portal only offers a download
button that works.
The checks run in sudo, like the rendering itself: evaluated with
the donor's rights, a field path reaching data they cannot read
would resolve to nothing and hide a downloadable receipt.
The report is resolved once per issuing company.
:return: a subset of ``self``, in the caller's environment.
"""
reports_by_company = {}
available_ids = []
for receipt in self.sudo():
if receipt.state not in PORTAL_VISIBLE_STATES:
continue
company = receipt.company_id
if company not in reports_by_company:
try:
reports_by_company[company] = receipt._get_tax_receipt_report()
except UserError:
reports_by_company[company] = None
report = reports_by_company[company]
if not report:
continue
try:
receipt._check_report_renderable(report)
except UserError:
continue
available_ids.append(receipt.id)
return self.browse(available_ids)
def _render_tax_receipt_pdf(self):
"""Render this tax receipt as a PDF through ``pdf_form_report``.

View File

@@ -0,0 +1,29 @@
# Copyright 2026 Elabore
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
"""Per-company choice of the report rendering tax receipts."""
from odoo import fields, models
#: Domain restricting the selectable reports to PDF Form reports on tax
#: receipts. Shared by the company field and its settings counterpart.
TAX_RECEIPT_REPORT_DOMAIN = (
"[('report_type', '=', 'pdf_form'),"
" ('model', '=', 'l10n.fr.tax.receipt'),"
" ('company_id', 'in', [False, {company}])]"
)
class ResCompany(models.Model):
"""Store the tax receipt report of each issuing company."""
_inherit = "res.company"
tax_receipt_report_id = fields.Many2one(
"ir.actions.report",
string="Tax Receipt Report",
ondelete="set null",
domain=TAX_RECEIPT_REPORT_DOMAIN.format(company="id"),
help="PDF Form report used to render the tax receipts issued by "
"this company in the portal. Without it, donors can see their "
"receipts but not download them.",
)

View File

@@ -0,0 +1,19 @@
# Copyright 2026 Elabore
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
"""Expose the tax receipt report in the Invoicing settings."""
from odoo import fields, models
from .res_company import TAX_RECEIPT_REPORT_DOMAIN
class ResConfigSettings(models.TransientModel):
"""Add the tax receipt report selector to the settings."""
_inherit = "res.config.settings"
tax_receipt_report_id = fields.Many2one(
related="company_id.tax_receipt_report_id",
readonly=False,
domain=TAX_RECEIPT_REPORT_DOMAIN.format(company="company_id"),
)

View File

@@ -5,16 +5,15 @@
import base64
import inspect
import io
import re
from odoo import Command
from odoo.exceptions import UserError
from odoo.tests import tagged
from odoo.tools import mute_logger
from odoo.tools.safe_eval import safe_eval
from odoo.addons.account.tests.common import AccountTestInvoicingHttpCommon
from odoo.addons.tax_receipt_in_portal.models.l10n_fr_tax_receipt import (
REPORT_UUID_PARAM,
)
#: Logger of the third-party renderer, which downgrades field resolution
#: errors to warnings. Muted where a test triggers one on purpose.
@@ -62,9 +61,7 @@ class TestTaxReceiptPortal(AccountTestInvoicingHttpCommon):
cls._map_field("NUMBER", "name")
cls._map_field("DONOR", "partner_id.name")
cls._map_field("AMOUNT", "amount_total_words")
cls.env["ir.config_parameter"].sudo().set_param(
REPORT_UUID_PARAM, cls.report.report_uuid
)
cls.env.company.tax_receipt_report_id = cls.report
cls.donor_a, cls.user_a = cls._create_donor("donor_a")
cls.donor_b, cls.user_b = cls._create_donor("donor_b")
@@ -80,17 +77,19 @@ class TestTaxReceiptPortal(AccountTestInvoicingHttpCommon):
# ------------------------------------------------------------------
@classmethod
def _create_pdf_form_report(cls, model_name):
def _create_pdf_form_report(cls, model_name, suffix=""):
"""Create a ``pdf_form`` report on ``model_name``.
:param model_name: technical name of the report target model.
:param suffix: distinguishes several reports on the same model,
whose name and ``report_name`` must stay unique.
:return: the created ``ir.actions.report`` record.
"""
report = cls.env["ir.actions.report"].create(
{
"name": f"Tax receipt test ({model_name})",
"name": f"Tax receipt test ({model_name}){suffix}",
"model": model_name,
"report_name": f"tax_receipt_in_portal.test_{model_name}",
"report_name": f"tax_receipt_in_portal.test_{model_name}{suffix}",
"report_type": "pdf_form",
"filename_pattern": "tax_receipt_{object.id}.pdf",
}
@@ -348,6 +347,84 @@ class TestTaxReceiptPortal(AccountTestInvoicingHttpCommon):
)
self.assertEqual(result["tax_receipt_count"], 0)
# ------------------------------------------------------------------
# PDF availability
# ------------------------------------------------------------------
#: CSS class of the "PDF available soon" mention, used instead of the
#: translatable wording.
PENDING_CLASS = "o_tax_receipt_pdf_pending"
def test_no_report_hides_download_buttons(self):
"""Without a configured report, no download button is shown.
The receipt itself stays visible: only its PDF is unavailable.
"""
self.env.company.tax_receipt_report_id = False
self._login(self.user_a)
listing = self.url_open("/my/tax-receipts")
self.assertEqual(listing.status_code, 200)
self.assertIn(self.receipt_a.name, listing.text)
self.assertNotIn("/download", listing.text)
self.assertIn(self.PENDING_CLASS, listing.text)
page = self.url_open(f"/my/tax-receipts/{self.receipt_a.id}")
self.assertEqual(page.status_code, 200)
self.assertNotIn("/download", page.text)
self.assertIn(self.PENDING_CLASS, page.text)
def test_available_pdf_shows_buttons(self):
"""With a valid report, both buttons are shown, no pending mention."""
self._login(self.user_a)
listing = self.url_open("/my/tax-receipts")
self.assertIn(f"/my/tax-receipts/{self.receipt_a.id}/download", listing.text)
self.assertNotIn(self.PENDING_CLASS, listing.text)
page = self.url_open(f"/my/tax-receipts/{self.receipt_a.id}")
self.assertIn(f"/my/tax-receipts/{self.receipt_a.id}/download", page.text)
self.assertNotIn(self.PENDING_CLASS, page.text)
@mute_logger(PDF_FORM_LOGGER)
def test_blank_field_hides_button_for_that_receipt_only(self):
"""A receipt with a blank mapped field loses its button, not others."""
older_receipt = self._create_receipt(self.donor_a, 2023, 50.0)
older_receipt.action_confirm()
self.env["pdf.form.field.line"].create(
{
"report_id": self.report.id,
"pdf_field_name": "ONLY_2025",
"pdf_field_type": "text",
"is_acroform": False,
"page": 1,
"evaluation_type": "equation",
"python_expr": "'ok' if object.year == 2025 else ''",
}
)
self._login(self.user_a)
listing = self.url_open("/my/tax-receipts")
self.assertIn(f"/my/tax-receipts/{self.receipt_a.id}/download", listing.text)
self.assertNotIn(f"/my/tax-receipts/{older_receipt.id}/download", listing.text)
self.assertEqual(listing.text.count(self.PENDING_CLASS), 1)
def test_download_unavailable_redirects_to_receipt(self):
"""Downloading an unavailable PDF goes back to the receipt page."""
self.env.company.tax_receipt_report_id = False
self._login(self.user_a)
response = self.url_open(
f"/my/tax-receipts/{self.receipt_a.id}/download",
allow_redirects=False,
)
self.assertIn(response.status_code, (302, 303))
self.assertTrue(
response.headers["Location"].endswith(
f"/my/tax-receipts/{self.receipt_a.id}"
)
)
# ------------------------------------------------------------------
# Rendering safety net
# ------------------------------------------------------------------
@@ -397,35 +474,34 @@ class TestTaxReceiptPortal(AccountTestInvoicingHttpCommon):
# Configuration safety net
# ------------------------------------------------------------------
def test_report_misconfigured_missing_param(self):
"""An unset report parameter raises instead of rendering blindly."""
self.env["ir.config_parameter"].sudo().set_param(REPORT_UUID_PARAM, "")
with self.assertRaisesRegex(UserError, REPORT_UUID_PARAM):
def test_report_misconfigured_missing(self):
"""No report on the issuing company raises a configuration error."""
self.env.company.tax_receipt_report_id = False
# Asserted on the company name, not on the translatable wording.
with self.assertRaisesRegex(UserError, re.escape(self.env.company.name)):
self.receipt_a._render_tax_receipt_pdf()
def test_report_misconfigured_unknown_uuid(self):
"""An unknown report UUID raises a configuration error."""
self.env["ir.config_parameter"].sudo().set_param(
REPORT_UUID_PARAM, "no-such-uuid"
)
with self.assertRaisesRegex(UserError, "no-such-uuid"):
self.receipt_a._render_tax_receipt_pdf()
def test_report_deleted_unsets_setting(self):
"""Deleting the selected report empties the setting, no crash."""
report = self._create_pdf_form_report("l10n.fr.tax.receipt", " deleted")
self.env.company.tax_receipt_report_id = report
report.unlink()
self.assertFalse(self.env.company.tax_receipt_report_id)
self.assertFalse(self.receipt_a._get_pdf_available_receipts())
def test_report_misconfigured_wrong_model(self):
"""A report targeting another model is refused."""
"""A report retargeted to another model after selection is refused.
The settings domain prevents selecting it, but the report itself
can be edited afterwards; the ORM does not re-check the domain.
"""
other_report = self._create_pdf_form_report("res.partner")
self.env["ir.config_parameter"].sudo().set_param(
REPORT_UUID_PARAM, other_report.report_uuid
)
self.env.company.tax_receipt_report_id = other_report
with self.assertRaisesRegex(UserError, "res.partner"):
self.receipt_a._render_tax_receipt_pdf()
def test_report_misconfigured_wrong_type(self):
"""A report that is not a PDF Form report is refused.
``pdf_form_report`` adds ``report_uuid`` to every report, so a
plain QWeb report can be pointed at by mistake.
"""
"""A report that is not a PDF Form report is refused."""
qweb_report = self.env["ir.actions.report"].create(
{
"name": "Not a PDF Form report",
@@ -434,12 +510,67 @@ class TestTaxReceiptPortal(AccountTestInvoicingHttpCommon):
"report_type": "qweb-pdf",
}
)
self.env["ir.config_parameter"].sudo().set_param(
REPORT_UUID_PARAM, qweb_report.report_uuid
)
self.env.company.tax_receipt_report_id = qweb_report
with self.assertRaisesRegex(UserError, "qweb-pdf"):
self.receipt_a._render_tax_receipt_pdf()
def test_report_of_issuing_company(self):
"""A receipt uses its issuing company's report, not the current one.
In the portal, the current company is the website's; a receipt
issued by another company must still use that company's report.
"""
other_company = self.env["res.company"].create({"name": "Other issuer"})
other_report = self._create_pdf_form_report(
"l10n.fr.tax.receipt", " other company"
)
other_company.tax_receipt_report_id = other_report
other_receipt = self.env["l10n.fr.tax.receipt"].create(
{
"partner_id": self.donor_a.id,
"company_id": other_company.id,
"year": 2025,
}
)
self.assertEqual(self.env.company.tax_receipt_report_id, self.report)
self.assertEqual(other_receipt._get_tax_receipt_report(), other_report)
self.assertEqual(self.receipt_a._get_tax_receipt_report(), self.report)
def test_settings_domain(self):
"""The settings selector only offers PDF Form tax receipt reports."""
wrong_model = self._create_pdf_form_report("res.partner")
qweb_report = self.env["ir.actions.report"].create(
{
"name": "Not a PDF Form report",
"model": "l10n.fr.tax.receipt",
"report_name": "tax_receipt_in_portal.test_qweb_domain",
"report_type": "qweb-pdf",
}
)
field = self.env["res.company"]._fields["tax_receipt_report_id"]
domain = safe_eval(field.domain, {"id": self.env.company.id})
selectable = self.env["ir.actions.report"].search(domain)
self.assertIn(self.report, selectable)
self.assertNotIn(wrong_model, selectable)
self.assertNotIn(qweb_report, selectable)
def test_settings_write_company(self):
"""Saving the settings stores the report on the current company."""
self.env.company.tax_receipt_report_id = False
settings = self.env["res.config.settings"].create(
{"tax_receipt_report_id": self.report.id}
)
settings.execute()
self.assertEqual(self.env.company.tax_receipt_report_id, self.report)
def test_settings_view(self):
"""The Tax Receipts block is part of the Invoicing settings."""
arch = self.env["res.config.settings"].get_views([(False, "form")])["views"][
"form"
]["arch"]
self.assertIn('id="tax_receipt_settings"', arch)
self.assertIn('name="tax_receipt_report_id"', arch)
def test_compute_api_contract(self):
"""Guard the private renderer API this module depends on.

View File

@@ -99,6 +99,7 @@
</td>
<td class="text-end">
<a
t-if="receipt.id in pdf_available_ids"
class="btn btn-sm btn-secondary"
t-attf-href="/my/tax-receipts/#{receipt.id}/download"
t-att-title="receipt.name"
@@ -106,6 +107,10 @@
<i class="fa fa-download me-1" role="img" />
<span>PDF</span>
</a>
<span
t-else=""
class="text-muted small o_tax_receipt_pdf_pending"
>PDF available soon</span>
</td>
</tr>
</tbody>
@@ -132,12 +137,17 @@
</div>
<div class="col-12 col-md-6 text-md-end">
<a
t-if="pdf_available"
class="btn btn-primary"
t-attf-href="/my/tax-receipts/#{tax_receipt.id}/download"
t-attf-href="/my/tax-receipts/#{tax_receipt.id}/download#{'?access_token=%s' % access_token if access_token else ''}"
>
<i class="fa fa-download me-1" role="img" />
<span>Download PDF</span>
</a>
<span
t-else=""
class="text-muted o_tax_receipt_pdf_pending"
>PDF available soon</span>
</div>
</div>
<div class="row mt-4" id="tax_receipt_information">

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.tax.receipt.portal</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="account.res_config_settings_view_form" />
<field name="arch" type="xml">
<xpath expr="//block[@id='invoicing_settings']" position="after">
<block title="Tax Receipts" id="tax_receipt_settings">
<setting
id="tax_receipt_report"
string="Tax Receipt Report"
company_dependent="1"
help="PDF Form report used to render tax receipts downloaded from the portal"
>
<field
name="tax_receipt_report_id"
options="{'no_create': True}"
/>
</setting>
</block>
</xpath>
</field>
</record>
</odoo>