""" 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."}), ("Dispatch Job", "legacy_dept", "Département legacy (pont)", "Data", None, None, "legacy_ticket_id", {"read_only": 1, "no_copy": 1, "description": "Département osTicket legacy (Installation Fibre / Réparation Fibre / Install-Réparation Télé / Téléphonie / Désinstallation…). Sert au coloriage des cartes dispatch « comme legacy »."}), ("Dispatch Job", "legacy_activation_url", "Lien activation TV legacy (pont)", "Small Text", None, None, "legacy_dept", {"read_only": 1, "no_copy": 1, "description": "Lien connect_ministra.php (activation STB/Ministra) extrait du fil du ticket legacy par le pont. Affiché tel quel dans le dispatch — MÊME lien que le tech reçoit (aucune reconstruction)."}), ("Dispatch Job", "legacy_detail", "Détails du ticket legacy (pont)", "Text", None, None, "legacy_activation_url", {"read_only": 1, "no_copy": 1, "description": "Description/contenu du ticket legacy (1er message du fil osTicket, HTML nettoyé) extrait par le pont → visible dans Ops sans ouvrir le legacy."}), # ── 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")