Files
orion/app/modules/orders/templates/invoices/invoice.html
Samir Boulahtit d7a383f3d7 test: add tests for merchant dashboard metrics and fix invoice template location
Move invoice PDF template from app/templates/invoices/ to
app/modules/orders/templates/invoices/ where InvoicePDFService expects it.
Expand invoice PDF tests to validate template path and existence.

Add unit tests for get_merchant_metrics() in tenancy, billing, and
customer metrics providers. Add unit tests for StatsAggregatorService
merchant methods. Add integration tests for the merchant dashboard
stats endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 21:46:34 +01:00

471 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Invoice {{ invoice.invoice_number }}</title>
<style>
/* Reset and base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 10pt;
line-height: 1.4;
color: #333;
background: #fff;
}
/* Page setup for A4 */
@page {
size: A4;
margin: 20mm 15mm 20mm 15mm;
}
.invoice-container {
max-width: 100%;
padding: 0;
}
/* Header */
.header {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #2563eb;
}
.merchant-info {
max-width: 50%;
}
.merchant-name {
font-size: 18pt;
font-weight: bold;
color: #1e40af;
margin-bottom: 8px;
}
.merchant-details {
font-size: 9pt;
color: #666;
line-height: 1.5;
}
.invoice-title {
text-align: right;
}
.invoice-title h1 {
font-size: 24pt;
color: #1e40af;
margin-bottom: 10px;
}
.invoice-meta {
font-size: 10pt;
color: #666;
}
.invoice-meta strong {
color: #333;
}
/* Addresses section */
.addresses {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
gap: 40px;
}
.address-block {
flex: 1;
}
.address-label {
font-size: 8pt;
text-transform: uppercase;
letter-spacing: 1px;
color: #888;
margin-bottom: 8px;
font-weight: 600;
}
.address-content {
background: #f8fafc;
padding: 15px;
border-radius: 6px;
border-left: 3px solid #2563eb;
}
.address-name {
font-weight: bold;
font-size: 11pt;
margin-bottom: 5px;
}
.address-details {
font-size: 9pt;
color: #555;
line-height: 1.6;
}
/* VAT info badge */
.vat-badge {
display: inline-block;
background: #dbeafe;
color: #1e40af;
padding: 3px 8px;
border-radius: 4px;
font-size: 8pt;
margin-top: 8px;
}
/* Items table */
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
}
.items-table thead {
background: #1e40af;
color: #fff;
}
.items-table th {
padding: 12px 10px;
text-align: left;
font-size: 9pt;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.items-table th.number {
text-align: right;
}
.items-table td {
padding: 12px 10px;
border-bottom: 1px solid #e5e7eb;
font-size: 9pt;
}
.items-table td.number {
text-align: right;
font-family: 'Courier New', monospace;
}
.items-table tbody tr:hover {
background: #f8fafc;
}
.item-sku {
color: #888;
font-size: 8pt;
display: block;
margin-top: 2px;
}
/* Totals */
.totals-section {
display: flex;
justify-content: flex-end;
margin-bottom: 30px;
}
.totals-table {
width: 280px;
}
.totals-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #e5e7eb;
}
.totals-row.total {
border-bottom: none;
border-top: 2px solid #1e40af;
margin-top: 5px;
padding-top: 12px;
}
.totals-label {
color: #666;
font-size: 9pt;
}
.totals-value {
font-family: 'Courier New', monospace;
font-size: 10pt;
}
.totals-row.total .totals-label,
.totals-row.total .totals-value {
font-weight: bold;
font-size: 12pt;
color: #1e40af;
}
/* VAT regime note */
.vat-note {
background: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 6px;
padding: 10px 15px;
font-size: 8pt;
color: #92400e;
margin-bottom: 20px;
}
/* Payment info */
.payment-section {
background: #f0fdf4;
border: 1px solid #22c55e;
border-radius: 6px;
padding: 15px;
margin-bottom: 20px;
}
.payment-title {
font-weight: bold;
color: #166534;
margin-bottom: 10px;
font-size: 10pt;
}
.payment-details {
font-size: 9pt;
color: #333;
}
.payment-details .label {
color: #666;
display: inline-block;
width: 60px;
}
/* Footer */
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
font-size: 8pt;
color: #888;
text-align: center;
}
.footer p {
margin-bottom: 5px;
}
/* Status badge */
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 9pt;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-draft {
background: #fef3c7;
color: #92400e;
}
.status-issued {
background: #dbeafe;
color: #1e40af;
}
.status-paid {
background: #dcfce7;
color: #166534;
}
.status-cancelled {
background: #fee2e2;
color: #991b1b;
}
/* Print styles */
@media print {
body {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
</style>
</head>
<body>
<div class="invoice-container">
<!-- Header -->
<div class="header">
<div class="merchant-info">
<div class="merchant-name">{{ seller.merchant_name }}</div>
<div class="merchant-details">
{% if seller.address %}{{ seller.address }}<br>{% endif %}
{% if seller.postal_code or seller.city %}
{{ seller.postal_code }} {{ seller.city }}<br>
{% endif %}
{{ seller.country }}
{% if seller.vat_number %}
<br>VAT: {{ seller.vat_number }}
{% endif %}
</div>
</div>
<div class="invoice-title">
<h1>INVOICE</h1>
<div class="invoice-meta">
<strong>{{ invoice.invoice_number }}</strong><br>
Date: {{ invoice.invoice_date.strftime('%d/%m/%Y') }}<br>
<span class="status-badge status-{{ invoice.status }}">{{ invoice.status }}</span>
</div>
</div>
</div>
<!-- Addresses -->
<div class="addresses">
<div class="address-block">
<div class="address-label">Bill To</div>
<div class="address-content">
<div class="address-name">{{ buyer.name }}</div>
<div class="address-details">
{% if buyer.get('merchant') %}{{ buyer.merchant }}<br>{% endif %}
{% if buyer.address %}{{ buyer.address }}<br>{% endif %}
{% if buyer.postal_code or buyer.city %}
{{ buyer.postal_code }} {{ buyer.city }}<br>
{% endif %}
{{ buyer.country }}
{% if buyer.email %}<br>{{ buyer.email }}{% endif %}
</div>
{% if buyer.vat_number %}
<div class="vat-badge">VAT: {{ buyer.vat_number }}</div>
{% endif %}
</div>
</div>
{% if invoice.order_id %}
<div class="address-block">
<div class="address-label">Order Reference</div>
<div class="address-content">
<div class="address-details">
Order #{{ invoice.order_id }}<br>
Currency: {{ invoice.currency }}
</div>
</div>
</div>
{% endif %}
</div>
<!-- VAT Regime Note -->
{% if invoice.vat_regime == 'reverse_charge' %}
<div class="vat-note">
<strong>Reverse Charge:</strong> VAT to be accounted for by the recipient pursuant to Article 196 of Council Directive 2006/112/EC.
</div>
{% elif invoice.vat_regime == 'oss' %}
<div class="vat-note">
<strong>OSS Invoice:</strong> VAT charged at {{ invoice.vat_rate }}% ({{ invoice.destination_country }} rate) under One-Stop-Shop scheme.
</div>
{% elif invoice.vat_regime == 'exempt' %}
<div class="vat-note">
<strong>VAT Exempt:</strong> Export outside EU - VAT not applicable.
</div>
{% endif %}
<!-- Items Table -->
<table class="items-table">
<thead>
<tr>
<th style="width: 50%">Description</th>
<th class="number" style="width: 10%">Qty</th>
<th class="number" style="width: 20%">Unit Price</th>
<th class="number" style="width: 20%">Total</th>
</tr>
</thead>
<tbody>
{% for item in line_items %}
<tr>
<td>
{{ item.description }}
{% if item.sku or item.ean %}
<span class="item-sku">
{% if item.sku %}SKU: {{ item.sku }}{% endif %}
{% if item.sku and item.ean %} | {% endif %}
{% if item.ean %}EAN: {{ item.ean }}{% endif %}
</span>
{% endif %}
</td>
<td class="number">{{ item.quantity }}</td>
<td class="number">{{ "%.2f"|format(item.unit_price_cents / 100) }} {{ invoice.currency }}</td>
<td class="number">{{ "%.2f"|format(item.total_cents / 100) }} {{ invoice.currency }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Totals -->
<div class="totals-section">
<div class="totals-table">
<div class="totals-row">
<span class="totals-label">Subtotal</span>
<span class="totals-value">{{ "%.2f"|format(invoice.subtotal_cents / 100) }} {{ invoice.currency }}</span>
</div>
<div class="totals-row">
<span class="totals-label">
VAT ({{ invoice.vat_rate }}%)
{% if invoice.vat_rate_label %}<br><small>{{ invoice.vat_rate_label }}</small>{% endif %}
</span>
<span class="totals-value">{{ "%.2f"|format(invoice.vat_amount_cents / 100) }} {{ invoice.currency }}</span>
</div>
<div class="totals-row total">
<span class="totals-label">Total</span>
<span class="totals-value">{{ "%.2f"|format(invoice.total_cents / 100) }} {{ invoice.currency }}</span>
</div>
</div>
</div>
<!-- Payment Information -->
{% if bank_details or payment_terms %}
<div class="payment-section">
<div class="payment-title">Payment Information</div>
<div class="payment-details">
{% if payment_terms %}
<p style="margin-bottom: 10px;">{{ payment_terms }}</p>
{% endif %}
{% if bank_details %}
{% if bank_details.bank_name %}
<span class="label">Bank:</span> {{ bank_details.bank_name }}<br>
{% endif %}
{% if bank_details.iban %}
<span class="label">IBAN:</span> {{ bank_details.iban }}<br>
{% endif %}
{% if bank_details.bic %}
<span class="label">BIC:</span> {{ bank_details.bic }}
{% endif %}
{% endif %}
</div>
</div>
{% endif %}
<!-- Footer -->
<div class="footer">
{% if footer_text %}
<p>{{ footer_text }}</p>
{% endif %}
<p>Invoice {{ invoice.invoice_number }} | Generated on {{ now.strftime('%d/%m/%Y %H:%M') }}</p>
</div>
</div>
</body>
</html>