[IMP] tax_receipt_in_portal: pick report per company, hide missing PDFs
Replace the ``tax_receipt_in_portal.report_uuid`` system parameter by a per-company setting, in Settings > Invoicing > Tax Receipts (``res.company.tax_receipt_report_id``). Each receipt is rendered with the report of its issuing company, not the current one: in the portal, the current company is the website's. The selector only offers PDF Form reports on ``l10n.fr.tax.receipt``; type and model are still checked at rendering time since the report can be edited after selection. The download buttons of the list and detail pages are only shown when the PDF can actually be rendered: a valid report is configured for the issuing company and no mapped field resolves to an empty value. The checks are those of the rendering itself, exposed as a filter (``_get_pdf_available_receipts``) and run in sudo to avoid false negatives on field paths the donor cannot read. Otherwise a discreet "PDF available soon" mention replaces the button, and the download URL redirects to the receipt page. The detail page download link now also carries the share token, so a visitor reaching the page by token can download the PDF.
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user