feat(backend): PDF deviz + portal client + SMS + invoice service

- PDF generation with WeasyPrint: deviz and factura templates (A4, branding)
- GET /orders/{id}/pdf/deviz returns PDF with order lines and totals
- Client portal (public, no auth): GET /p/{token}, POST /p/{token}/accept|reject
- SMS service (SMSAPI.ro) - skips in dev when no token configured
- Invoice service: create from validated order, auto-number (F-YYYY-NNNN)
- GET /invoices/{id}/pdf returns factura PDF
- Order status_client field for client accept/reject tracking
- Alembic migration for status_client
- 19 passing tests (auth + sync + orders + pdf + portal + invoices)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 17:34:36 +02:00
parent efc9545ae6
commit 3bdafad22a
17 changed files with 786 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="ro"><head>
<meta charset="UTF-8">
<style>
@page { size: A4; margin: 2cm; }
body { font-family: DejaVu Sans, sans-serif; font-size: 11pt; color: #111; }
.header { display: flex; justify-content: space-between; margin-bottom: 24px; }
h2 { margin: 0; font-size: 16pt; }
h3 { font-size: 11pt; margin: 16px 0 6px; color: #374151; }
table { width: 100%; border-collapse: collapse; }
th { background: #f3f4f6; padding: 6px 8px; text-align: left; font-size: 10pt; }
td { padding: 5px 8px; border-bottom: 1px solid #e5e7eb; font-size: 10pt; }
.totals { margin-top: 20px; text-align: right; }
.totals div { margin-bottom: 4px; }
.total-final { font-weight: bold; font-size: 13pt; border-top: 2px solid #111; padding-top: 6px; }
.info-box { border: 1px solid #d1d5db; padding: 12px; margin-bottom: 16px; }
</style>
</head><body>
<div class="header">
<div>
<strong>FURNIZOR</strong><br>
<strong>{{ tenant.nume }}</strong><br>
{% if tenant.cui %}CUI: {{ tenant.cui }}<br>{% endif %}
{% if tenant.reg_com %}Reg. Com.: {{ tenant.reg_com }}<br>{% endif %}
{% if tenant.adresa %}{{ tenant.adresa }}<br>{% endif %}
{% if tenant.iban %}IBAN: {{ tenant.iban }}<br>{% endif %}
{% if tenant.banca %}Banca: {{ tenant.banca }}{% endif %}
</div>
<div style="text-align:right">
<h2>FACTURA Nr. {{ invoice.nr_factura }}</h2>
<div>Data: {{ invoice.data_factura }}</div>
</div>
</div>
<div class="info-box">
<strong>CLIENT</strong><br>
{{ order.client_nume or 'N/A' }}<br>
{% if order.client_cui %}CUI: {{ order.client_cui }}<br>{% endif %}
{% if order.client_adresa %}{{ order.client_adresa }}{% endif %}
</div>
<div style="margin-bottom: 8px;">
Auto: <strong>{{ order.nr_auto }}</strong> | Deviz: {{ order.id[:8]|upper }}
</div>
<table>
<tr><th>#</th><th>Descriere</th><th>UM</th><th>Cant.</th><th>Pret unit. (RON)</th><th>Total (RON)</th></tr>
{% for l in lines %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ l.descriere }}</td>
<td>{{ l.um or ('ore' if l.tip == 'manopera' else 'buc') }}</td>
<td>{{ l.ore if l.tip == 'manopera' else l.cantitate }}</td>
<td>{{ "%.2f"|format(l.pret_ora if l.tip == 'manopera' else l.pret_unitar) }}</td>
<td>{{ "%.2f"|format(l.total or 0) }}</td>
</tr>
{% endfor %}
</table>
<div class="totals">
<div>Manopera: {{ "%.2f"|format(order.total_manopera or 0) }} RON</div>
<div>Materiale: {{ "%.2f"|format(order.total_materiale or 0) }} RON</div>
<div class="total-final">TOTAL: {{ "%.2f"|format(invoice.total or 0) }} RON</div>
</div>
</body></html>