Major additions accumulated over 9 days — single commit per request. Flow editor (new): - Generic visual editor for step trees, usable by project wizard + agent flows - PROJECT_KINDS / AGENT_KINDS catalogs decouple UI from domain - Drag-and-drop reorder via vuedraggable with scope isolation per peer group - Chain-aware depends_on rewrite on reorder (sequential only — DAGs preserved) - Variable picker with per-applies_to catalog (Customer / Quotation / Service Contract / Issue / Subscription), insert + copy-clipboard modes - trigger_condition helper with domain-specific JSONLogic examples - Global FlowEditorDialog mounted once in MainLayout, Odoo inline pattern - Server: targo-hub flow-runtime.js, flow-api.js, flow-templates.js - ERPNext: Flow Template/Run doctypes, scheduler, 5 seeded system templates - depends_on chips resolve to step labels instead of opaque "s4" ids QR/OCR scanner (field app): - Camera capture → Gemini Vision via targo-hub with 8s timeout - IndexedDB offline queue retries photos when signal returns - Watcher merges late-arriving scan results into the live UI Dispatch: - Planning mode (draft → publish) with offer pool for unassigned jobs - Shared presets, recurrence selector, suggested-slots dialog - PublishScheduleModal, unassign confirmation Ops app: - ClientDetailPage composables extraction (useClientData, useDeviceStatus, useWifiDiagnostic, useModemDiagnostic) - Project wizard: shared detail sections, wizard catalog/publish composables - Address pricing composable + pricing-mock data - Settings redesign hosting flow templates Targo-hub: - Contract acceptance (JWT residential + DocuSeal commercial tracks) - Referral system - Modem-bridge diagnostic normalizer - Device extractors consolidated Migration scripts: - Invoice/quote print format setup, Jinja rendering - Additional import + fix scripts (reversals, dates, customers, payments) Docs: - Consolidated: old scattered MDs → HANDOFF, ARCHITECTURE, DATA_AND_FLOWS, FLOW_EDITOR_ARCHITECTURE, BILLING_AND_PAYMENTS, CPE_MANAGEMENT, APP_DESIGN_GUIDELINES - Archived legacy wizard PHP for reference - STATUS snapshots for 2026-04-18/19 Cleanup: - Removed ~40 generated PDFs/HTMLs (invoice_preview*, rendered_jinja*) - .gitignore now covers invoice preview output + nested .DS_Store Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
19 KiB
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. LisezARCHITECTURE.mdd'abord pour le contexte réseau/services.
Dernière MàJ : 2026-04-17
Table des matières
- Importation du système legacy vers ERPNext
- App Frappe custom —
gigafibre_utils - Print Format « Facture TARGO »
- Flux de paiement public (sans Authentik)
- Infrastructure Docker
- UI Ops — aperçu client
- Configuration & secrets
- Points connus / TODO
1. Importation du système legacy vers ERPNext
Particularités
- Source : MariaDB legacy
gestionclient(conteneurlegacy-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.idlegacy →Customer.legacy_id(custom field).service.delivery_idlegacy →Service Location.legacy_delivery_id(custom field).invoice_item.service_idlegacy → chaîné viaservice.delivery_idpour rattacher un item à sonService LocationERPNext.
- Prix : conservés tels quels (pas de re-calcul des taxes); TPS/TVQ sont imports
inclus dans
taxes[]de chaqueSales 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)
- Custom field
Sales Invoice Item.service_location(Link → Service Location) créé viaadd_missing_custom_fields.py. import_invoices.pymis à jour : JOIN surservice+ lookupService Location.legacy_delivery_id→ écritservice_locationlors de l'INSERT.- Backfill ponctuel :
backfill_service_location.py(UPDATE ... FROM (VALUES %s)viapsycopg2.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/Dockerfilecopie l'app dansapps/gigafibre_utilset 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 <img src>. |
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 <defs><style> SVG.
Solution retenue : rendu serveur-side via cairosvg → PNG base64 injecté dans
le template. Aucune dépendance côté client.
2.2 Génération PDF (invoice_pdf)
- Contourne complètement la pipeline PDF Frappe (qui ajoute 15 mm de marge top
forcée si pas de
#header-html, et post-process avec pypdf). - Lance
chromium --headless=new --no-pdf-header-footer --print-to-pdfdirectement dans untempfile.TemporaryDirectory. - Réponse HTTP :
Content-Type: application/pdf+Content-Disposition: inline(⚠️ ne pas utiliser"binary"— empêche l'affichage iframe et Acrobat échoue à ouvrir le fichier téléchargé). - Producer : Skia/PDF m147 (identique à Antigravity/Gemini → fidélité pixel-parfait).
Usage :
GET /api/method/gigafibre_utils.api.invoice_pdf?name=SINV-2026-700010
GET /api/method/gigafibre_utils.api.invoice_pdf?name=SINV-xxx&print_format=Facture+TARGO
2.3 Code de parrainage (referral_code)
- Déterministe par Customer — HMAC-SHA256 (clé
gigafibre_pay_secret) surcustomer.name→ 30 bits → 6 caractères Crockford base32 (pas de0/O/1/I/L/Uambigus). - Stable dans le temps (pas stocké en DB).
- ~1,07 milliard de codes possibles → collisions négligeables pour <10 M customers.
3. Print Format « Facture TARGO »
Particularités
- Install :
scripts/migration/setup_invoice_print_format.py(idempotent). - Doctype :
Sales Invoice. - Générateur :
pdf_generator="chrome"— Chromium--print-to-pdfheadless. C'est la meilleure méthode dans ERPNext v16 ; wkhtmltopdf (QtWebKit 2013) est obsolète et donne un rendu dégradé (SVG<defs><style>ignorés, flexbox cassé, polices Unicode QC incomplètes). Avantages :- CSS moderne : flexbox/grid,
@pageaveccounter(page)/counter(pages)pour la pagination (p. ex. « Page 1 de 2 » en@top-right),break-inside: avoid. - Polices système : les accents/caractères spéciaux (é, à, œ, ℃) passent sans mapping manuel.
- Rendu pixel-perfect : même moteur que le navigateur de preview — le preview HTML et le PDF sont identiques.
- Performances : ~1 s pour une facture typique (vs ~3 s wkhtmltopdf).
- Prérequis : Chromium installé dans le container (voir Dockerfile
FROM frappe/erpnext:v16.10.1 + apt-get install chromium), cléchromium_path: /usr/bin/chromiumdanscommon_site_config.json.
- CSS moderne : flexbox/grid,
- Marges : forcées par le Print Format
margin_top=5, bottom=5, left=15, right=15(mm) — Chrome PDF ignore@page { margin: … }dans le CSS (preferCSSPageSize=false), il faut passer par les options ERPNext. - Template Jinja : ~25 Ko, inliné dans le script
setup_invoice_print_format.py(constantehtml_template = r"""…""") — source de vérité unique, versionnée.
Fonctionnalités clés du template
- Prélude de résolution de données — récupère
customer,service_locations,prev_invoice,recent_payments,remaining_balance,account_number, etc. depuis le docSales Invoice. - Position de la fenêtre d'enveloppe — bloc adresse client à 50 mm du haut
(2″) pour s'aligner sur enveloppe #10 à fenêtre (ouverture 2″-3½″).
margin-topdu.cl-blockvarie selon la présence d'unprev_invoice(10 mm si bloc SOMMAIRE DU COMPTE présent en amont, 33 mm sinon).
- Regroupement par Service Location — la section « FRAIS COURANTS » itère
current_charges_locations[], chaque location a ses items (pas de sous-total par location pour réduire la hauteur). - Période de service inline — calculée par item :
- Utilise
service_start_date/service_end_datesi présents (ERPNext deferred). - Sinon déduit la période courante sauf si l'item est one-time (mots-clés :
install,activation,rabais,frais unique,remise, ou montant négatif). - Format :
(1er avril au 30 avril)plein mois,(16 avril au 30 avril)prorata.
- Utilise
- Colonne de droite (meta + QR + parrainage + conditions) :
- Meta-band : N° compte · Date · N° facture
- Montant dû (boîte verte)
- QR code (base64 inline, 60 j TTL)
- Boîte parrainage (fond gris, accent vert) — code 6 car. via
gigafibre_utils.api.referral_code. - Conditions légales (« sera assujettie à des frais de retard… »)
- Contactez-nous : 1867 chemin de la Rivière, Ste-Clotilde, J0L 1W0 · 855 888-2746
- Mini-footer CPRST en bas de page.
Particularités Jinja
- Variables
svc_start/svc_enddoivent être définies avant la boucle items (sinonUndefinedError). - Les appels
frappe.call(...)dans le sandbox Jinja requièrent unfrappe.local.requestvivant → le rendu hors contexte web (ex.bench execute) échoue : toujours tester via HTTP (l'endpointinvoice_pdffait ça correctement).
Fichiers connexes
scripts/migration/invoice_preview.jinja— copie de travail lisible (pas utilisée en prod — seul le Print Format installé compte).scripts/migration/test_jinja_render.py— rend le template localement via Chrome macOS pour comparaison pixel (référence).
4. Flux de paiement public (sans Authentik)
Vue d'ensemble
QR (facture) / SMS / Email Authentik-less
↓ ↑
https://client.gigafibre.ca/pay-public?inv=X&t=HMAC
↓
validate_pay_token → affiche résumé facture
↓
[Payer avec Stripe]
↓
create_checkout_session → redirect Stripe
↓
Stripe Checkout (hors ERP)
↓
stripe_webhook → Payment Entry reconciled
Trois TTL de tokens (signés HMAC-SHA256)
| Token | TTL | Usage |
|---|---|---|
| QR code | 60 jours | Lien permanent sur facture imprimée. |
| Magic link | 15 minutes | SMS/email en cas de token expiré. |
| pay_redirect | 1 heure | Bridge depuis portail authentifié (customer connecté). |
Fonctionnalités — api.py
| Méthode | allow_guest |
Rôle |
|---|---|---|
pay_token(invoice, ttl_days) |
❌ (admin) | Génère une URL signée (debug/test). |
validate_pay_token(invoice, token) |
✅ | Vérifie signature+TTL, retourne résumé facture. |
request_magic_link(invoice, channel, last4) |
✅ | Envoie SMS/email (gated par last4 du téléphone). |
create_checkout_session(invoice, token) |
✅ | Crée Stripe Checkout Session. |
stripe_webhook() |
✅ | Vérifie signature Stripe, crée Payment Entry. |
pay_redirect(invoice) |
⚠️ sous Authentik | Bridge portal → pay-public (bypass staff @targo.ca/@gigafibre.ca). |
Landing page /pay-public
- Servie par Frappe depuis
gigafibre_utils/www/pay-public.html. - Vanilla JS — pas de build step, pas de framework.
- Lit
inv+tdepuisfrappe.form_dict. - Appelle
validate_pay_token→ affiche facture OU formulaire magic-link. - Bouton « Payer avec Stripe » →
create_checkout_session→ redirect.
Carve-out Traefik (contournement Authentik)
Fichier : /opt/traefik/dynamic/pay-public.yml.
Routeurs avec priority ≥ 250 (> priorité 150 des règles /api Authentik) :
client-pay-public—PathPrefix(/pay-public)(landing)client-api-validate-pay-tokenclient-api-magic-linkclient-api-create-checkout-sessionclient-api-stripe-webhook
Tous servis sur client-portal-svc, aucun middleware (pas de ForwardAuth).
Stripe
- Checkout Session API : créée côté serveur (
_stripe_post), redirect 303 verssession.url. - Webhook : vérifie
Stripe-Signature(_stripe_verify_signature) avecgigafibre_stripe_webhook_secret→ créePayment Entryrattaché à la facture. - Clé réutilisée depuis targo-hub (
sk_live_51QCMOI…).
Twilio (magic link SMS)
- Auth basic (
twilio_account_sid:twilio_auth_token). - From
twilio_from_number=+14382313838. - Anti-abuse :
request_magic_linkexigelast4matchant les 4 derniers chiffres du téléphone customer avant d'envoyer.
5. Infrastructure Docker
Image custom ERPNext
/opt/erpnext/custom/Dockerfile :
FROM frappe/erpnext:v16.10.1
USER root
RUN apt-get install -y libcairo2 libpango-1.0-0 libpangocairo-1.0-0 \
libgdk-pixbuf-2.0-0 shared-mime-info \
chromium fonts-liberation fonts-noto-color-emoji
USER frappe
COPY --chown=frappe:frappe gigafibre_utils apps/gigafibre_utils
RUN env/bin/pip install -e apps/gigafibre_utils cairosvg
RUN grep -qx gigafibre_utils apps/apps.txt || echo gigafibre_utils >> apps/apps.txt
Particularités
- Chromium installé à
/usr/bin/chromium(config keychromium_path). - cairosvg (Python) pour rasterisation SVG → PNG.
- App
gigafibre_utilscopiée dans l'image (pas un volume) → toute modif requiertdocker compose build erpnext-backend && docker compose up -d. - ⚠️ Piège :
sites/apps.txtpeut se retrouver avec une ligne parasiteapps.txtqui cassebench(ModuleNotFoundError: No module named 'apps') — nettoyer si ça arrive.
Build & déploiement
cd /opt/erpnext/custom
docker compose build erpnext-backend
docker compose up -d --force-recreate erpnext-backend
6. UI Ops — aperçu client
Fonctionnalité
Onglet « Aperçu client » dans le volet de détail facture
(apps/ops/src/components/shared/detail-sections/InvoiceDetail.vue).
q-tabs: Détails (champs grid) ↔ Aperçu client (iframe PDF).- Iframe pointe sur
gigafibre_utils.api.invoice_pdf?name=…→ rendu Chrome pixel-parfait identique au PDF client.
Particularité Vue scoped
Les styles .modal-field-grid / .mf / .mf-label définis dans DetailModal.vue
avec scoped ne cascadent pas vers les composants enfants montés via
<component :is>. Il faut les dupliquer dans le <style scoped> du composant
enfant (contre-intuitif — documenter pour les nouveaux devs).
7. Configuration & secrets
Toutes les clés vivent dans /opt/erpnext/custom/erpnext/sites/common_site_config.json
(montage bind du volume sites sur le conteneur erpnext-backend).
Clés requises
| Clé | Valeur / format | Utilisation |
|---|---|---|
gigafibre_pay_secret |
64 hex | HMAC signing tokens + referral codes |
gigafibre_pay_host |
https://client.gigafibre.ca/pay-public |
Base URL QR/magic-link |
gigafibre_stripe_secret_key |
sk_live_… |
Stripe Checkout API |
gigafibre_stripe_webhook_secret |
whsec_… |
TODO — webhook signature verify |
twilio_account_sid |
AC… |
SMS magic-link |
twilio_auth_token |
(hex) | SMS auth |
twilio_from_number |
+14382313838 |
SMS from |
chromium_path |
/usr/bin/chromium |
PDF generator |
Commande pour poser une clé
docker exec erpnext-backend-1 bench set-config -g <key> "<value>"
8. Points connus / TODO
⚠️ TODO bloquants
- Webhook Stripe : enregistrer un second endpoint pointant vers
https://client.gigafibre.ca/api/method/gigafibre_utils.api.stripe_webhookdans le dashboard Stripe, récupérer lewhsec_…, poser viabench set-config -g gigafibre_stripe_webhook_secret.
Améliorations suggérées
- Inverse lookup parrainage :
customer_from_referral_code(code)pour appliquer le crédit 50 $ automatiquement à la souscription du parrain. - Rate-limit sur
request_magic_link(actuellement protégé par last4 seulement — OK pour l'abus téléphone, pas pour énumération d'invoices). - Refresh token côté Traccar : le token généré expire le 2031-04-17 ;
prévoir rotation automatique (script cron + API
/api/session/token). - Archivage local des PDFs : stocker dans
File Doctypepour historique et éviter de re-rendre via Chromium à chaque affichage.
Pièges connus
frappe.response.type = "binary"⇒ Acrobat échoue à ouvrir le fichier téléchargé → toujours utiliser"pdf"pour les PDFs.- Postgres v16 + ERPNext : bugs
GROUP BY/HAVING/double-quotes — patches dansfeedback_erpnext_postgres.md. - Script
bench executeéchoue surgigafibre_utils.api.invoice_pdfhors contexte web (Jinja appellefrappe.callqui requiertfrappe.local.request). Tester via HTTP. - Traccar token ≠ FCM token — les
APA91b…sont pour Firebase push, pas pour l'API Traccar (cf. conversation 2026-04-17).
Fichiers à connaître (index rapide)
| Fichier | Rôle |
|---|---|
/opt/erpnext/custom/Dockerfile |
Image ERPNext custom |
/opt/erpnext/custom/gigafibre_utils/gigafibre_utils/api.py |
823 lignes — toutes les méthodes whitelisted |
/opt/erpnext/custom/gigafibre_utils/gigafibre_utils/www/pay-public.html |
Landing publique |
/opt/traefik/dynamic/pay-public.yml |
Carve-out Authentik |
scripts/migration/setup_invoice_print_format.py |
Install/update du Print Format |
scripts/migration/import_invoices.py |
Import legacy → ERPNext |
scripts/migration/backfill_service_location.py |
Backfill item↔location 1,6 M lignes |
scripts/migration/test_jinja_render.py |
Rendu local pour comparaison pixel |
apps/ops/src/components/shared/detail-sections/InvoiceDetail.vue |
Onglet Aperçu client |
services/targo-hub/lib/traccar.js |
Proxy Traccar (Bearer token) |