# 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.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 - **Mots de passe legacy** = MD5 non-salé → forcer un reset via OTP email/SMS (`project_portal_auth.md`). - **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 `