[ADD] tax_receipt_in_portal: publish tax receipts on the portal
Let donors view and download their own tax receipts from ``/my/tax-receipts``: a paginated list, a detail page with the receipt information, its invoices and the native portal chatter, and a PDF download rendered on demand through the configured ``pdf_form`` report. The PDF rendering is delegated to ``pdf_form_report`` and nothing is archived: a confirmed receipt is frozen by ``l10n_fr_tax_receipt``, so rendering it again yields the same document. Two safety nets compensate third-party behaviors: - report resolution by ``report_uuid`` (system parameter ``tax_receipt_in_portal.report_uuid``) with strict validation of report type and target model; - a blank-field guard which resolves every mapped PDF field value before rendering, because ``pdf_form_report`` swallows resolution errors and would otherwise silently serve a receipt with a blank amount. Access is enforced twice: a portal record rule (own commercial partner, confirmed/sent state only) and an explicit controller domain that also covers internal users browsing ``/my``. Tested with 17 tests (portal isolation, draft exposure, chatter data access, blank-field guard, report misconfiguration, private API contract of ``pdf_form_report``), including a mutation check proving the security tests fail when the record rule is opened.
This commit is contained in:
254
tax_receipt_in_portal/views/portal_templates.xml
Normal file
254
tax_receipt_in_portal/views/portal_templates.xml
Normal file
@@ -0,0 +1,254 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Card on /my/home, using the native portal entry template. -->
|
||||
<template
|
||||
id="portal_my_home_tax_receipt"
|
||||
name="Portal My Home: Tax Receipts"
|
||||
inherit_id="portal.portal_my_home"
|
||||
customize_show="True"
|
||||
priority="40"
|
||||
>
|
||||
<xpath expr="//div[hasclass('o_portal_docs')]" position="before">
|
||||
<t t-set="portal_client_category_enable" t-value="True" />
|
||||
</xpath>
|
||||
<div id="portal_client_category" position="inside">
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Tax Receipts</t>
|
||||
<t t-set="url" t-value="'/my/tax-receipts'" />
|
||||
<t t-set="text">View and download your yearly donation receipts</t>
|
||||
<t t-set="placeholder_count" t-value="'tax_receipt_count'" />
|
||||
</t>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Breadcrumb: list page on top, receipt number on the detail page. -->
|
||||
<template
|
||||
id="portal_breadcrumbs_tax_receipt"
|
||||
name="Portal Breadcrumbs: Tax Receipts"
|
||||
inherit_id="portal.portal_breadcrumbs"
|
||||
priority="40"
|
||||
>
|
||||
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
|
||||
<li
|
||||
t-if="page_name == 'tax_receipt'"
|
||||
t-attf-class="breadcrumb-item #{'active ' if not tax_receipt else ''}"
|
||||
>
|
||||
<a
|
||||
t-if="tax_receipt"
|
||||
t-attf-href="/my/tax-receipts?{{ keep_query() }}"
|
||||
>Tax Receipts</a>
|
||||
<t t-else="">Tax Receipts</t>
|
||||
</li>
|
||||
<!-- page_name is part of the condition for the same reason as
|
||||
account's `portal_invoice` breadcrumb: a leaked
|
||||
`tax_receipt` variable from another portal page body must
|
||||
not activate this item. -->
|
||||
<li
|
||||
t-if="page_name == 'tax_receipt' and tax_receipt"
|
||||
class="breadcrumb-item active"
|
||||
>
|
||||
<t t-out="tax_receipt.name" />
|
||||
</li>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="portal_my_tax_receipts" name="My Tax Receipts">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="breadcrumbs_searchbar" t-value="True" />
|
||||
|
||||
<t t-call="portal.portal_searchbar">
|
||||
<t t-set="title">Tax Receipts</t>
|
||||
</t>
|
||||
|
||||
<t t-if="not tax_receipts">
|
||||
<p class="alert alert-warning">
|
||||
There is currently no tax receipt for your account.
|
||||
</p>
|
||||
</t>
|
||||
|
||||
<t t-if="tax_receipts" t-call="portal.portal_table">
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th name="receipt_number">Receipt #</th>
|
||||
<th name="receipt_year">Year</th>
|
||||
<th name="receipt_amount" class="text-end pe-3">
|
||||
Amount
|
||||
</th>
|
||||
<th name="receipt_download" class="text-end" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="tax_receipts" t-as="receipt">
|
||||
<td>
|
||||
<a
|
||||
t-att-href="receipt.get_portal_url()"
|
||||
t-att-title="receipt.name"
|
||||
>
|
||||
<span t-out="receipt.name" />
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span t-out="receipt.year" />
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
<span
|
||||
t-out="receipt.amount_total"
|
||||
t-options='{"widget": "monetary", "display_currency": receipt.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a
|
||||
class="btn btn-sm btn-secondary"
|
||||
t-attf-href="/my/tax-receipts/#{receipt.id}/download"
|
||||
t-att-title="receipt.name"
|
||||
>
|
||||
<i class="fa fa-download me-1" role="img" />
|
||||
<span>PDF</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="portal_tax_receipt_page" name="My Tax Receipt">
|
||||
<t t-call="portal.portal_layout">
|
||||
<h5 class="mb-0">
|
||||
<span>Tax Receipt - <span t-field="tax_receipt.name" /></span>
|
||||
</h5>
|
||||
<div id="general_information">
|
||||
<div class="row mt-4">
|
||||
<div
|
||||
t-if="tax_receipt.partner_id"
|
||||
class="col-12 col-md-6 mb-4 mb-md-0"
|
||||
>
|
||||
<h6><strong>Donor:</strong></h6>
|
||||
<address
|
||||
t-field="tax_receipt.partner_id"
|
||||
t-options='{"widget": "contact", "fields": ["name", "email", "phone"]}'
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 text-md-end">
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
t-attf-href="/my/tax-receipts/#{tax_receipt.id}/download"
|
||||
>
|
||||
<i class="fa fa-download me-1" role="img" />
|
||||
<span>Download PDF</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-4" id="tax_receipt_information">
|
||||
<div class="col-12 col-md-6 mb-4 mb-md-0">
|
||||
<div t-if="tax_receipt.name" class="row mb-2 mb-sm-1">
|
||||
<div class="col-12 col-sm-4"><strong>Receipt #</strong></div>
|
||||
<div class="col-12 col-sm-8">
|
||||
<span t-field="tax_receipt.name" />
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="tax_receipt.year" class="row mb-2 mb-sm-1">
|
||||
<div class="col-12 col-sm-4"><strong>Year</strong></div>
|
||||
<div class="col-12 col-sm-8">
|
||||
<span t-field="tax_receipt.year" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2 mb-sm-1">
|
||||
<div class="col-12 col-sm-4"><strong>Status</strong></div>
|
||||
<div class="col-12 col-sm-8">
|
||||
<span t-field="tax_receipt.state" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="row mb-2 mb-sm-1">
|
||||
<div class="col-12 col-sm-4"><strong>Amount</strong></div>
|
||||
<div class="col-12 col-sm-8">
|
||||
<span
|
||||
t-field="tax_receipt.amount_total"
|
||||
t-options='{"widget": "monetary", "display_currency": tax_receipt.currency_id}'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
t-if="tax_receipt.amount_total_words"
|
||||
class="row mb-2 mb-sm-1"
|
||||
>
|
||||
<div class="col-12 col-sm-4"><strong>In words</strong></div>
|
||||
<div class="col-12 col-sm-8">
|
||||
<span t-field="tax_receipt.amount_total_words" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
t-if="tax_receipt.last_payment_date"
|
||||
class="row mb-2 mb-sm-1"
|
||||
>
|
||||
<div
|
||||
class="col-12 col-sm-4"
|
||||
><strong>Last Payment Date</strong></div>
|
||||
<div class="col-12 col-sm-8">
|
||||
<span
|
||||
t-field="tax_receipt.last_payment_date"
|
||||
t-options='{"widget": "date"}'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="invoices" id="tax_receipt_invoices" class="mt-4">
|
||||
<h6><strong>Invoices</strong></h6>
|
||||
<table class="table table-sm" id="tax_receipt_invoice_table">
|
||||
<thead class="bg-100">
|
||||
<tr>
|
||||
<th class="text-start">Invoice</th>
|
||||
<th class="text-end">Invoice Date</th>
|
||||
<th class="text-end">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- The loop variable must not be named `invoice`:
|
||||
the body of a t-call is rendered before the
|
||||
layout it calls (ir_qweb _compile_directive_call),
|
||||
so a top-level `invoice` variable would leak
|
||||
into the breadcrumb context, where account's
|
||||
portal_breadcrumbs template tests
|
||||
t-if="invoice". -->
|
||||
<tr t-foreach="invoices" t-as="portal_invoice">
|
||||
<td>
|
||||
<a
|
||||
t-attf-href="/my/invoices/#{portal_invoice.id}"
|
||||
t-att-title="portal_invoice.name"
|
||||
>
|
||||
<t t-out="portal_invoice.name" />
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span
|
||||
t-field="portal_invoice.invoice_date"
|
||||
t-options='{"widget": "date"}'
|
||||
/>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span
|
||||
t-field="portal_invoice.amount_total"
|
||||
t-options='{"widget": "monetary", "display_currency": portal_invoice.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tax_receipt_communication" class="mt-4">
|
||||
<!-- mb-4 is required: the frontend portal chatter paints an
|
||||
opaque sticky bar 15px above its own container
|
||||
(portal/static/src/chatter/core/portal_chatter.xml), which
|
||||
would otherwise cover the bottom of this heading. -->
|
||||
<h6 class="mb-4"><strong>Communication</strong></h6>
|
||||
<t t-call="portal.message_thread" />
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user