# Facturation & Paiements — Handoff dev
> Référence unique pour toutes les fonctionnalités facture / paiement construites sur
> la stack `erp.gigafibre.ca` + `client.gigafibre.ca` + `ops`. Lisez
> [../architecture/overview.md](../architecture/overview.md) d'abord pour le contexte
> réseau/services.
Dernière MàJ : 2026-04-17
---
## Table des matières
1. [Importation du système legacy vers ERPNext](#1-importation-du-système-legacy-vers-erpnext)
2. [App Frappe custom — `gigafibre_utils`](#2-app-frappe-custom--gigafibre_utils)
3. [Print Format « Facture TARGO »](#3-print-format--facture-targo-)
4. [Flux de paiement public (sans Authentik)](#4-flux-de-paiement-public-sans-authentik)
5. [Infrastructure Docker](#5-infrastructure-docker)
6. [UI Ops — aperçu client](#6-ui-ops--aperçu-client)
7. [Configuration & secrets](#7-configuration--secrets)
8. [Points connus / TODO](#8-points-connus--todo)
---
## 1. Importation du système legacy vers ERPNext
### Particularités
- **Source** : MariaDB legacy `gestionclient` (conteneur `legacy-db`, 10.100.80.100).
- **Cible** : PostgreSQL ERPNext v16 (patches GROUP BY/HAVING/quotes appliqués — cf.
`feedback_erpnext_postgres.md`).
- **Correspondance des clés** :
- `customer.id` legacy → `Customer.legacy_id` (custom field).
- `service.delivery_id` legacy → `Service Location.legacy_delivery_id` (custom field).
- `invoice_item.service_id` legacy → chaîné via `service.delivery_id` pour rattacher
un item à son `Service Location` ERPNext.
- **Prix** : conservés tels quels (pas de re-calcul des taxes); TPS/TVQ sont imports
inclus dans `taxes[]` de chaque `Sales Invoice`.
- **Volumes finaux** : 6 667 customers · 21 K subscriptions · 115 K invoices ·
1,6 M invoice items · 242 K tickets.
### Fonctionnalités — scripts `scripts/migration/*`
| Script | Rôle |
|---|---|
| `import_invoices.py` | Import complet des factures legacy + JOIN sur `service` pour récupérer `delivery_id` et renseigner `service_location` sur chaque item. |
| `backfill_service_location.py` | Script one-shot pour remplir `service_location` sur les 1,6 M items déjà importés (batches `execute_values` de 20 K lignes, 227 s). |
| `import_payments.py` / `reimport_payments.py` / `import_expro_payments.py` | Import des paiements legacy en `Payment Entry` réconciliés. |
| `import_payment_methods.py` | Carte + ACH + chèque — créés en tant que `Mode of Payment`. |
| `fix_invoice_outstanding.py` | Réconciliation après import — recalcule `outstanding_amount` via Frappe. |
| `fix_invoice_customer_names.py` | Renomme customers pour refléter le nom légal courant (`Subscription.bill_to_name`). |
| `import_payment_arrangements.py` | Ententes de paiement (échelonnement) → `Payment Request` + scheduler. |
| `geocode_locations.py` | Géocode toutes les `Service Location` avec Mapbox. |
### Restauration Item ↔ Service Location (particularité majeure)
Sans ce lien le PDF ne peut pas grouper les lignes par adresse.
```
legacy.invoice_item.service_id
└─→ legacy.service.delivery_id
└─→ ERPNext Service Location.legacy_delivery_id (custom field)
```
1. Custom field `Sales Invoice Item.service_location` (Link → Service Location) créé
via `add_missing_custom_fields.py`.
2. `import_invoices.py` mis à jour : JOIN sur `service` + lookup
`Service Location.legacy_delivery_id` → écrit `service_location` lors de l'INSERT.
3. Backfill ponctuel : `backfill_service_location.py`
(`UPDATE ... FROM (VALUES %s)` via `psycopg2.extras.execute_values`).
### Points d'attention
- **Auth portail** = passwordless magic-link (depuis 2026-04-22) —
`POST /portal/request-link` envoie un JWT 24h par email + SMS.
Les mots de passe legacy MD5 ne sont **plus** utilisés ; le formulaire
ERPNext `/login` est bloqué au niveau Traefik sur `client.gigafibre.ca`
(redirect vers `/#/login`). Aucune migration de hash requise.
- **Devices** rattachés aux **addresses** (Service Location), pas aux customers
(`feedback_device_hierarchy.md`).
- **Serials TPLG** ERPNext ≠ serials réels — matching via MAC
(`feedback_device_serial_mapping.md`).
---
## 2. App Frappe custom — `gigafibre_utils`
### Particularités
- **Chemin source** (serveur ERP) : `/opt/erpnext/custom/gigafibre_utils/`
- **Cuite dans l'image Docker** — `/opt/erpnext/custom/Dockerfile` copie l'app dans
`apps/gigafibre_utils` et la pip-installe en editable.
- **Raison d'exister** : contourner le sandbox Frappe `safe_exec` (qui bloque
`__import__` et certaines opérations) en exposant des `@frappe.whitelist()` Python
natifs appelables depuis Jinja et côté client.
### Arborescence
```
gigafibre_utils/
├── Dockerfile # FROM frappe/erpnext:v16.10.1 + chromium + cairosvg
└── gigafibre_utils/
├── __init__.py
├── hooks.py
├── modules.txt
├── patches.txt
├── api.py # 823 lignes, toutes les méthodes whitelisted
└── www/
└── pay-public.html # Page publique /pay-public (sans Authentik)
```
### Fonctionnalités — `api.py`
#### 2.1 Rendu visuel (QR, logo, descriptions)
| Méthode | `allow_guest` | Rôle |
|---|:-:|---|
| `invoice_qr(invoice)` | ✅ | PNG binaire du QR code (payload = URL `/pay-public` signée, TTL 60 j). |
| `invoice_qr_base64(invoice)` | ✅ | Base64 brut (pour embed direct dans Jinja). |
| `invoice_qr_datauri(invoice)` | ✅ | `data:image/png;base64,…` prêt à injecter en `
`. |
| `logo_base64(height)` | ✅ | Logo TARGO rasterisé via cairosvg (SVG → PNG 96 px). |
| `short_item_name(name, max_len)` | ✅ | Nettoie/tronque les noms d'item (retire caractères parasites, limite longueur). |
**Particularité QR** : la fonction `_sign_pay_token` génère un token HMAC-SHA256
(clé `gigafibre_pay_secret`) → `{invoice, expires_at}` encodé base64-url. Le QR
pointe vers `https://client.gigafibre.ca/pay-public?inv=…&t=…`.
**Particularité logo** : wkhtmltopdf (QtWebKit 2013) ignore `