OSS-BSS-Field-Dispatch/frappe-setup/setup_dispatch_custom_fields.py
louispaulb 05dfe5aa17 feat: champ Dispatch Job.legacy_ticket_id (idempotence du pont legacy→dispatch)
Renseigné par services/targo-hub/lib/legacy-dispatch-sync.js (repo gigafibre-fsm) :
1 ticket osTicket legacy = 1 Dispatch Job. read_only + search_index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 09:34:18 -04:00

75 lines
5.1 KiB
Python

"""
Custom Fields pour la planification (Roster AI) + prise de RDV — idempotent.
==========================================================================
Consolide tous les champs ajoutés à Dispatch Technician et Dispatch Job pour
le module Roster/Planification et le booking. Remplace les scripts épars.
Exécution :
docker cp setup_dispatch_custom_fields.py erpnext-backend-1:/tmp/
docker exec erpnext-backend-1 sh -lc \
"cd /home/frappe/frappe-bench && env/bin/python /tmp/setup_dispatch_custom_fields.py"
(créer d'abord /home/frappe/logs si frappe.init s'en plaint — cf.
reference_frappe_hr_postgres.md §Pièges scripts)
"""
import frappe
frappe.init(site="erp.gigafibre.ca", sites_path="sites")
frappe.connect()
frappe.flags.ignore_permissions = True
# (dt, fieldname, label, fieldtype, options, default, insert_after, extra)
FIELDS = [
# ── Dispatch Technician : cadence, coût chargé, compétences ──
("Dispatch Technician", "efficiency", "Cadence (facteur temps)", "Float", None, "1.0", "tech_group",
{"description": "1.0 = normal · 1.10 = +10% (plus lent) · 0.90 = -10% (plus rapide)"}),
("Dispatch Technician", "skills", "Compétences", "Data", None, None, "tech_group",
{"description": "Séparées par des virgules, ex: fibre,cuivre,aerien"}),
("Dispatch Technician", "skill_levels", "Niveaux de compétence (JSON)", "Small Text", None, None, "skills",
{"description": "Maîtrise 1-5 par compétence (JSON, ex: {\"installation\":4}). Distinct de l'efficacité (=vitesse). Édité dans la grille Planification."}),
("Dispatch Technician", "skill_eff", "Efficacité par compétence (JSON)", "Small Text", None, None, "skill_levels",
{"description": "Facteur de vitesse PAR compétence (JSON, ex: {\"installation\":0.9}). Défaut = efficacité globale. Édité dans la grille Planification."}),
("Dispatch Technician", "cost_salary_h", "Salaire horaire ($/h)", "Float", None, "0", "efficiency", {}),
("Dispatch Technician", "cost_charges_pct", "Charges sociales (%)", "Float", None, "0", "cost_salary_h", {}),
("Dispatch Technician", "cost_other_h", "Autres coûts/h ($ véhicule, outils, frais)", "Float", None, "0", "cost_charges_pct", {}),
# ── Shift Template : quart de garde (sur appel) ──
("Shift Template", "on_call", "Garde (sur appel — non offert au booking)", "Check", None, "0", "color",
{"description": "Quart de garde/urgence : capacité de réserve. NON offert au booking client et exclu du calcul de capacité offrable. S'affiche en bande hachurée sur la timeline."}),
# ── Tech Availability : absence longue durée (à remplacer vs vacances) ──
("Tech Availability", "long_term", "Longue durée (à remplacer)", "Check", None, "0", "availability_type",
{"description": "Absence longue durée (maternité, invalidité…) : à REMPLACER lors de la réapplication d'un modèle, pas juste à sauter comme des vacances."}),
# ── Dispatch Job : prise de RDV ──
("Dispatch Job", "booking_prefs", "Préférences RDV (JSON)", "Small Text", None, None, "status", {}),
("Dispatch Job", "booking_status", "Statut RDV", "Select", "À planifier\nProposé\nConfirmé\nAnnulé\nÀ reporter", "À planifier", "booking_prefs", {}),
("Dispatch Job", "booking_token", "Jeton RDV client", "Data", None, None, "booking_status",
{"read_only": 1, "no_copy": 1}),
# ── Dispatch Job : pont de synchro avec la DB legacy (osTicket) ──
("Dispatch Job", "legacy_ticket_id", "ID ticket legacy (pont)", "Data", None, None, "ticket_id",
{"read_only": 1, "no_copy": 1, "search_index": 1,
"description": "ID du ticket osTicket legacy (table `ticket`). Renseigné par le pont legacy→dispatch (lib/legacy-dispatch-sync.js) → idempotence : 1 ticket legacy = 1 Dispatch Job."}),
# ── Service Contract : installation financée (conformité CRTC 2026-43, pas de clawback) ──
("Service Contract", "monthly_regular", "Forfait — prix original (barré)", "Currency", None, None, "monthly_rate",
{"description": "Prix mensuel de référence barré (marketing). Le montant facturé reste monthly_rate. Vide/≤ = aucun barré."}),
("Service Contract", "install_fee", "Installation financée ($)", "Currency", None, None, "monthly_regular",
{"description": "Install financée sur la durée (vraie créance, pas une promo). Ex: 240 standard / 120 simple."}),
("Service Contract", "install_regular", "Installation — valeur affichée (barrée)", "Currency", None, None, "install_fee",
{"description": "Prix de référence barré (marketing), ex. 360. Le montant réellement financé/dû reste install_fee (ex. 240)."}),
]
for dt, fn, label, ft, opts, default, after, extra in FIELDS:
cf = f"{dt}-{fn}"
if frappe.db.exists("Custom Field", cf):
print("EXISTS", cf)
continue
doc = {"doctype": "Custom Field", "dt": dt, "fieldname": fn, "label": label,
"fieldtype": ft, "insert_after": after}
if opts:
doc["options"] = opts
if default is not None:
doc["default"] = default
doc.update(extra or {})
frappe.get_doc(doc).insert()
print("CREATED", cf)
frappe.db.commit()
print("DONE")