diff --git a/tax_receipt_in_portal/README.rst b/tax_receipt_in_portal/README.rst index a725c28..35c0200 100644 --- a/tax_receipt_in_portal/README.rst +++ b/tax_receipt_in_portal/README.rst @@ -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. diff --git a/tax_receipt_in_portal/__manifest__.py b/tax_receipt_in_portal/__manifest__.py index e145459..c593219 100644 --- a/tax_receipt_in_portal/__manifest__.py +++ b/tax_receipt_in_portal/__manifest__.py @@ -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, diff --git a/tax_receipt_in_portal/controllers/portal.py b/tax_receipt_in_portal/controllers/portal.py index 9590466..c434b4b 100644 --- a/tax_receipt_in_portal/controllers/portal.py +++ b/tax_receipt_in_portal/controllers/portal.py @@ -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"), diff --git a/tax_receipt_in_portal/i18n/fr.po b/tax_receipt_in_portal/i18n/fr.po index 4a4ef33..d11caaf 100644 --- a/tax_receipt_in_portal/i18n/fr.po +++ b/tax_receipt_in_portal/i18n/fr.po @@ -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 "" "\n" " PDF" msgstr "" +"\n" +" PDF" #. 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" diff --git a/tax_receipt_in_portal/models/__init__.py b/tax_receipt_in_portal/models/__init__.py index c75c93b..a2c5cf8 100644 --- a/tax_receipt_in_portal/models/__init__.py +++ b/tax_receipt_in_portal/models/__init__.py @@ -1 +1,3 @@ from . import l10n_fr_tax_receipt +from . import res_company +from . import res_config_settings diff --git a/tax_receipt_in_portal/models/l10n_fr_tax_receipt.py b/tax_receipt_in_portal/models/l10n_fr_tax_receipt.py index c2bbcb3..ad94606 100644 --- a/tax_receipt_in_portal/models/l10n_fr_tax_receipt.py +++ b/tax_receipt_in_portal/models/l10n_fr_tax_receipt.py @@ -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``. diff --git a/tax_receipt_in_portal/models/res_company.py b/tax_receipt_in_portal/models/res_company.py new file mode 100644 index 0000000..b18955f --- /dev/null +++ b/tax_receipt_in_portal/models/res_company.py @@ -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.", + ) diff --git a/tax_receipt_in_portal/models/res_config_settings.py b/tax_receipt_in_portal/models/res_config_settings.py new file mode 100644 index 0000000..2f63bb5 --- /dev/null +++ b/tax_receipt_in_portal/models/res_config_settings.py @@ -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"), + ) diff --git a/tax_receipt_in_portal/tests/test_portal_tax_receipt.py b/tax_receipt_in_portal/tests/test_portal_tax_receipt.py index aaab84f..654df15 100644 --- a/tax_receipt_in_portal/tests/test_portal_tax_receipt.py +++ b/tax_receipt_in_portal/tests/test_portal_tax_receipt.py @@ -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. diff --git a/tax_receipt_in_portal/views/portal_templates.xml b/tax_receipt_in_portal/views/portal_templates.xml index d7a0ff3..34c61c4 100644 --- a/tax_receipt_in_portal/views/portal_templates.xml +++ b/tax_receipt_in_portal/views/portal_templates.xml @@ -99,6 +99,7 @@