feat: add mcs_invoice_currency_display addon with modern invoice template

- Suppresses CAD company currency block on foreign-currency invoices
- Modern dark navy invoice layout with clean line items table
- Amber tax-exempt notice for pre-HST invoices (under $30k threshold)
- Structured for easy HST enablement after incorporation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
metatroncubeswdev 2026-05-18 15:25:40 -04:00
parent 6ef5b53c75
commit 4b37c198b7
9 changed files with 415 additions and 0 deletions

View File

@ -0,0 +1 @@
from . import models

View File

@ -0,0 +1,14 @@
{
'name': 'MCS Invoice Currency Display Fix',
'version': '17.0.1.0.0',
'summary': 'Hides the company currency (CAD) totals block on foreign currency invoices',
'author': 'Metatroncube Software Solutions',
'depends': ['account'],
'data': [
'views/report_invoice_currency.xml',
'views/report_invoice_template.xml',
],
'installable': True,
'auto_install': False,
'license': 'LGPL-3',
}

View File

@ -0,0 +1 @@
from . import mcs_invoice_currency_display

View File

@ -0,0 +1,10 @@
from odoo import models, fields, api
class McsInvoiceCurrencyDisplay(models.Model):
_name = 'mcs.invoice.currency.display'
_description = 'MCS Invoice Currency Display Fix'
name = fields.Char(string='Name', required=True)
active = fields.Boolean(default=True)
notes = fields.Text(string='Notes')

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mcs_invoice_currency_display_user,access.mcs.invoice.currency.display,model_mcs_invoice_currency_display,base.group_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mcs_invoice_currency_display_user access.mcs.invoice.currency.display model_mcs_invoice_currency_display base.group_user 1 1 1 0

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mcs_invoice_currency_display_view_form" model="ir.ui.view">
<field name="name">mcs_invoice_currency_display.form</field>
<field name="model">mcs.invoice.currency.display</field>
<field name="arch" type="xml">
<form string="MCS Invoice Currency Display Fix">
<sheet>
<group>
<field name="name"/>
<field name="active"/>
</group>
<group string="Notes">
<field name="notes" nolabel="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="mcs_invoice_currency_display_view_list" model="ir.ui.view">
<field name="name">mcs_invoice_currency_display.list</field>
<field name="model">mcs.invoice.currency.display</field>
<field name="arch" type="xml">
<list string="MCS Invoice Currency Display Fix">
<field name="name"/>
<field name="active"/>
</list>
</field>
</record>
</odoo>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="menu_mcs_invoice_currency_display_root" name="MCS Invoice Currency Display Fix" sequence="100"/>
<menuitem id="menu_mcs_invoice_currency_display_main" name="MCS Invoice Currency Display Fix"
parent="menu_mcs_invoice_currency_display_root" sequence="10"
action="action_mcs_invoice_currency_display"/>
<record id="action_mcs_invoice_currency_display" model="ir.actions.act_window">
<field name="name">MCS Invoice Currency Display Fix</field>
<field name="res_model">mcs.invoice.currency.display</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--
Override the company currency block on customer invoices.
When the invoice currency differs from the company currency (e.g. USD invoice
for a CAD company), Odoo normally adds a second "Taxes CAD / Total" block.
This module hides that block so the invoice only shows amounts in the
invoice currency, which is the correct display for international clients.
-->
<template id="document_tax_totals_company_currency_template"
inherit_id="account.document_tax_totals_company_currency_template">
<xpath expr="//t[@t-set='show_company_taxes']" position="replace">
<t t-set="show_company_taxes" t-value="False"/>
</xpath>
</template>
</odoo>

View File

@ -0,0 +1,327 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--
Modern invoice layout — Metatroncube Software Solutions
Overrides the content area of account.report_invoice_document.
Tax handling
────────────
Current: Under the $30k small-supplier threshold — no HST/GST registered.
When no tax lines are present, an amber "Tax Exempt" notice is shown.
Future: Once incorporated and HST-registered, add tax lines to invoices as
normal. The totals block (account.document_tax_totals) will
automatically render each tax group with its registration number.
Remove/disable the amber notice at that point.
-->
<template id="report_invoice_document_mcs_modern"
inherit_id="account.report_invoice_document">
<xpath expr="//div[@class='mt-5 clearfix']" position="replace">
<div class="mt-3">
<div class="page mb-4">
<!-- ━━━ INVOICE TITLE BAR ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
<table style="width:100%;background:#1a2e4a;color:#fff;border-radius:4px;margin-bottom:20px;border-collapse:collapse;">
<tr>
<td style="padding:14px 20px;vertical-align:middle;">
<div style="font-size:22px;font-weight:700;letter-spacing:1.5px;line-height:1.15;">
<t t-if="not proforma">
<t t-if="o.move_type == 'out_invoice' and o.state == 'posted'">INVOICE</t>
<t t-elif="o.move_type == 'out_invoice' and o.state == 'draft'">DRAFT INVOICE</t>
<t t-elif="o.move_type == 'out_invoice' and o.state == 'cancel'">CANCELLED INVOICE</t>
<t t-elif="o.move_type == 'out_refund' and o.state == 'posted'">CREDIT NOTE</t>
<t t-elif="o.move_type == 'out_refund' and o.state == 'draft'">DRAFT CREDIT NOTE</t>
<t t-elif="o.move_type == 'out_refund' and o.state == 'cancel'">CANCELLED CREDIT NOTE</t>
<t t-elif="o.move_type == 'in_refund'">VENDOR CREDIT NOTE</t>
<t t-elif="o.move_type == 'in_invoice'">VENDOR BILL</t>
</t>
<t t-else="">PROFORMA INVOICE</t>
</div>
<div t-if="o.name != '/'" style="font-size:12.5px;opacity:.70;margin-top:3px;letter-spacing:.3px;">
<span t-field="o.name"/>
</div>
</td>
<td style="padding:14px 20px;vertical-align:middle;text-align:right;font-size:12px;white-space:nowrap;">
<div t-if="o.invoice_date">
<span style="opacity:.65;">Invoice Date</span>&#160;
<strong><span t-field="o.invoice_date"/></strong>
</div>
<div t-if="o.invoice_date_due and o.move_type == 'out_invoice' and o.state == 'posted'"
style="margin-top:5px;">
<span style="opacity:.65;">Due Date</span>&#160;
<strong><span t-field="o.invoice_date_due"/></strong>
</div>
<div t-if="o.ref" style="margin-top:5px;">
<span style="opacity:.65;">Reference</span>&#160;
<span t-field="o.ref"/>
</div>
<div t-if="o.invoice_origin" style="margin-top:5px;">
<span style="opacity:.65;">Source</span>&#160;
<span t-field="o.invoice_origin"/>
</div>
<div t-if="o.invoice_incoterm_id" style="margin-top:5px;">
<span style="opacity:.65;">Incoterm</span>&#160;
<span t-field="o.invoice_incoterm_id.code"/>
<t t-if="o.incoterm_location">&#160;<span t-field="o.incoterm_location"/></t>
</div>
<div t-if="o.partner_id.ref" style="margin-top:5px;">
<span style="opacity:.65;">Client Code</span>&#160;
<span t-field="o.partner_id.ref"/>
</div>
</td>
</tr>
</table>
<div class="oe_structure"/>
<!-- ━━━ LINE ITEMS TABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
<t t-set="display_discount" t-value="any(l.discount for l in o.invoice_line_ids)"/>
<t t-set="lines" t-value="o.invoice_line_ids.sorted(key=lambda l: (-l.sequence, l.date, l.move_name, -l.id), reverse=True)"/>
<table style="width:100%;border-collapse:collapse;font-size:12.5px;" name="invoice_line_table">
<thead>
<tr style="background:#243f5c;color:#fff;">
<th name="th_description" style="padding:9px 12px;text-align:left;font-weight:600;border:none;">Description</th>
<th name="th_quantity" style="padding:9px 8px;text-align:right;font-weight:600;border:none;white-space:nowrap;">Qty</th>
<th name="th_priceunit" style="padding:9px 8px;text-align:right;font-weight:600;border:none;white-space:nowrap;">Unit Price</th>
<th t-if="display_discount" name="th_discount"
style="padding:9px 8px;text-align:right;font-weight:600;border:none;">Disc.%</th>
<th name="th_taxes" style="padding:9px 8px;text-align:center;font-weight:600;border:none;white-space:nowrap;">Tax</th>
<th name="th_subtotal" style="padding:9px 12px;text-align:right;font-weight:600;border:none;white-space:nowrap;">Amount</th>
</tr>
</thead>
<tbody class="invoice_tbody">
<t t-set="current_subtotal" t-value="0"/>
<t t-set="current_section" t-value="None"/>
<t t-foreach="lines" t-as="line">
<t t-set="current_subtotal" t-value="current_subtotal + line.price_subtotal"/>
<tr t-att-class="'fw-bold o_line_section' if line.display_type == 'line_section' else 'o_line_note' if line.display_type == 'line_note' else ''"
t-att-style="'background:#f4f7fb;' if line_index % 2 != 0 else 'background:#ffffff;'">
<!-- ── Product line ── -->
<t t-if="line.display_type == 'product'" name="account_invoice_line_accountable">
<td name="account_invoice_line_name"
style="padding:9px 12px;border-top:1px solid #e8ecf0;vertical-align:top;">
<span t-if="line.name" t-field="line.name" t-options="{'widget': 'text'}"/>
</td>
<td name="td_quantity"
style="padding:9px 8px;border-top:1px solid #e8ecf0;text-align:right;vertical-align:top;white-space:nowrap;">
<span t-field="line.quantity"/>
<span t-field="line.product_uom_id" groups="uom.group_uom"
style="font-size:10px;color:#9ca3af;"/>
</td>
<td name="td_price_unit"
style="padding:9px 8px;border-top:1px solid #e8ecf0;text-align:right;vertical-align:top;white-space:nowrap;">
<span t-field="line.price_unit"/>
</td>
<td t-if="display_discount" name="td_discount"
style="padding:9px 8px;border-top:1px solid #e8ecf0;text-align:right;vertical-align:top;">
<span t-field="line.discount"/>
</td>
<t t-set="taxes" t-value="', '.join([(tax.invoice_label or tax.name) for tax in line.tax_ids])"/>
<td name="td_taxes"
style="padding:9px 8px;border-top:1px solid #e8ecf0;text-align:center;vertical-align:top;font-size:11px;color:#6b7280;white-space:nowrap;">
<span t-if="taxes" t-out="taxes" id="line_tax_ids"/>
<span t-else="" style="color:#d1d5db;"></span>
</td>
<td name="td_subtotal"
style="padding:9px 12px;border-top:1px solid #e8ecf0;text-align:right;vertical-align:top;font-weight:500;white-space:nowrap;">
<span t-field="line.price_subtotal"/>
</td>
</t>
<!-- ── Section header ── -->
<t t-elif="line.display_type == 'line_section'">
<td colspan="99"
style="padding:8px 12px;border-top:2px solid #1a2e4a;background:#eef2f7;font-weight:700;color:#1a2e4a;font-size:11px;text-transform:uppercase;letter-spacing:.6px;">
<span t-if="line.name" t-field="line.name" t-options="{'widget': 'text'}"/>
</td>
<t t-set="current_section" t-value="line"/>
<t t-set="current_subtotal" t-value="0"/>
</t>
<!-- ── Note line ── -->
<t t-elif="line.display_type == 'line_note'">
<td colspan="99"
style="padding:4px 12px;border-top:1px solid #f3f4f6;color:#9ca3af;font-size:11.5px;font-style:italic;">
<span t-if="line.name" t-field="line.name" t-options="{'widget': 'text'}"/>
</td>
</t>
</tr>
<!-- Section subtotal row -->
<t t-if="current_section and (line_last or lines[line_index+1].display_type == 'line_section')">
<tr>
<td colspan="99"
style="text-align:right;padding:5px 12px;border-top:1px solid #d1d9e0;font-size:12px;color:#374151;background:#f9fbfc;">
<span style="margin-right:12px;font-style:italic;">Section Subtotal</span>
<strong t-out="current_subtotal"
t-options="{'widget':'monetary','display_currency':o.currency_id}"/>
</td>
</tr>
</t>
</t>
</tbody>
</table>
<!-- ━━━ BOTTOM SECTION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -->
<table style="width:100%;border-collapse:collapse;margin-top:18px;">
<tr>
<!-- ── LEFT: notes, HST notice, payment terms ── -->
<td style="width:54%;vertical-align:top;padding-right:20px;">
<!-- Tax-exempt / HST notice -->
<t t-set="has_any_tax" t-value="any(line.tax_ids for line in o.invoice_line_ids)"/>
<div t-if="not has_any_tax"
style="background:#fffbeb;border:1px solid #fcd34d;border-left:4px solid #f59e0b;padding:9px 12px;border-radius:4px;margin-bottom:12px;font-size:11.5px;color:#78350f;line-height:1.55;">
<!--
FUTURE: Once HST-registered after incorporation, remove this block
and add the appropriate tax to each invoice line instead.
The totals section below will then display the HST breakdown
and registration number automatically.
-->
<strong>Tax Exempt</strong> — Metatroncube Software Solutions is not
currently registered for HST/GST. No sales tax is applicable on this invoice.
</div>
<!-- Fiscal position note -->
<div t-if="not is_html_empty(o.fiscal_position_id.note)"
style="font-size:12px;color:#4b5563;margin-bottom:8px;">
<span t-field="o.fiscal_position_id.note"/>
</div>
<!-- Payment term note -->
<div t-if="o.invoice_payment_term_id.note"
style="font-size:12px;color:#374151;margin-bottom:6px;">
<span t-field="o.invoice_payment_term_id.note" name="payment_term"/>
</div>
<!-- Installment schedule -->
<t t-set="payment_term_details" t-value="o.payment_term_details"/>
<t t-if="o.invoice_payment_term_id.display_on_invoice and payment_term_details">
<div t-if="o.show_payment_term_details"
id="total_payment_term_details_table"
style="font-size:11.5px;color:#374151;margin-bottom:8px;">
<t t-if="o._is_eligible_for_early_payment_discount(o.currency_id, o.invoice_date)">
<div>
<span t-options="{'widget':'monetary','display_currency':o.currency_id}"
t-out="o.invoice_payment_term_id._get_amount_due_after_discount(o.amount_total, o.amount_tax)"/>
due if paid before
<span t-out="o.invoice_payment_term_id._get_last_discount_date_formatted(o.invoice_date)"/>
</div>
</t>
<t t-if="len(payment_term_details) > 1" t-foreach="payment_term_details" t-as="term">
<div>
Instalment <span t-out="term_index + 1"/>:&#160;
<strong t-options="{'widget':'monetary','display_currency':o.currency_id}"
t-out="term.get('amount')"/>
due <span t-out="term.get('date')"/>
</div>
</t>
</div>
</t>
<!-- Payment reference + bank account -->
<div t-if="o.move_type in ('out_invoice','in_refund') and o.payment_reference"
style="font-size:12px;color:#374151;margin-bottom:6px;" name="payment_communication">
<strong>Payment Reference:</strong>&#160;
<span class="fw-bold" t-field="o.payment_reference"/>
<t t-if="o.partner_bank_id">
<br/>Account: <span t-field="o.partner_bank_id" class="fw-bold"/>
</t>
</div>
<!-- Terms &amp; conditions / narration -->
<div t-if="not is_html_empty(o.narration)"
style="font-size:11px;color:#6b7280;margin-top:8px;border-top:1px solid #f0f0f0;padding-top:8px;"
name="comment">
<span t-field="o.narration"/>
</div>
<!-- QR code (EPC / payment) -->
<t t-set="show_qr" t-value="o.display_qr_code and o.amount_residual > 0"/>
<div t-if="not show_qr" name="qr_code_placeholder" class="oe_structure"/>
<div id="qrcode" class="avoid-page-break-inside"
style="display:flex;align-items:center;margin-top:14px;" t-else="">
<div id="qrcode_image" style="margin-right:10px;">
<t t-set="qr_code_url" t-value="o._generate_qr_code(silent_errors=True)"/>
<p t-if="qr_code_url" class="position-relative mb-0">
<img t-att-src="qr_code_url"/>
<img src="/account/static/src/img/Odoo_logo_O.svg"
id="qrcode_odoo_logo"
class="top-50 start-50 position-absolute bg-white border border-white border-3 rounded-circle"/>
</p>
</div>
<div t-if="qr_code_url"
style="font-size:11px;color:#9ca3af;font-style:italic;" id="qrcode_info">
Scan to pay with your mobile
</div>
</div>
</td>
<!-- ── RIGHT: totals box ── -->
<td style="width:46%;vertical-align:top;">
<div style="border:1px solid #dde3ea;border-radius:6px;overflow:hidden;">
<table class="table table-sm table-borderless avoid-page-break-inside"
style="margin:0;font-size:12.5px;width:100%;">
<!-- Subtotal / tax lines / grand total -->
<t t-set="tax_totals" t-value="o.tax_totals or {}"/>
<t t-call="account.document_tax_totals"/>
<!-- Payments already applied (invoice-with-payments variant) -->
<t t-if="print_with_payments">
<t t-if="o.payment_state != 'invoicing_legacy'">
<t t-set="payments_vals"
t-value="o.sudo().invoice_payments_widget and o.sudo().invoice_payments_widget['content'] or []"/>
<t t-foreach="payments_vals" t-as="payment_vals">
<tr t-if="payment_vals['is_exchange'] == 0"
style="font-size:11.5px;color:#6b7280;">
<td style="padding:5px 12px;">
<i class="oe_form_field oe_payment_label">
Paid on
<t t-out="payment_vals['date']"
t-options="{'widget':'date'}"/>
</i>
</td>
<td class="text-end" style="padding:5px 12px;white-space:nowrap;">
<span t-out="payment_vals['amount']"
t-options="{'widget':'monetary','display_currency':o.currency_id}"/>
</td>
</tr>
</t>
<t t-if="len(payments_vals) > 0">
<tr style="background:#1a2e4a;color:#fff;font-weight:700;font-size:13px;"
class="border-black fw-bold">
<td style="padding:10px 12px;">Amount Due</td>
<td class="text-end" style="padding:10px 12px;white-space:nowrap;">
<span t-field="o.amount_residual"/>
</td>
</tr>
</t>
</t>
</t>
</table>
</div>
<!-- Amount in words -->
<div t-if="o.company_id.display_invoice_amount_total_words"
style="font-size:10.5px;color:#9ca3af;text-align:right;margin-top:5px;font-style:italic;">
<span t-field="o.amount_total_words"/>
</div>
<!-- CAD block suppressed by report_invoice_currency.xml override -->
<t t-call="account.document_tax_totals_company_currency_template"/>
</td>
</tr>
</table>
<div class="oe_structure"/>
</div>
</div>
</xpath>
</template>
</odoo>