From 7ea546bbbdc47796c872060ad2430482ae26fc31 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Wed, 3 Jun 2026 16:43:36 -0400 Subject: [PATCH] Roster/booking: doctypes + custom fields ERPNext (idempotent) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - create_roster_doctypes.py : Shift Template/Requirement/Assignment, Tech Availability - setup_dispatch_custom_fields.py : efficiency, skills, coût chargé (Dispatch Technician) + booking_prefs/status/token (Dispatch Job) Co-Authored-By: Claude Opus 4.8 (1M context) --- frappe-setup/create_roster_doctypes.py | 130 +++++++++++++++++++ frappe-setup/setup_dispatch_custom_fields.py | 53 ++++++++ 2 files changed, 183 insertions(+) create mode 100644 frappe-setup/create_roster_doctypes.py create mode 100644 frappe-setup/setup_dispatch_custom_fields.py diff --git a/frappe-setup/create_roster_doctypes.py b/frappe-setup/create_roster_doctypes.py new file mode 100644 index 0000000..a1ec84a --- /dev/null +++ b/frappe-setup/create_roster_doctypes.py @@ -0,0 +1,130 @@ +""" +Roster / Planification — création des DocTypes custom dans ERPNext facturation +============================================================================== +À côté de Dispatch Technician / Dispatch Job. Tout en `custom: 1` (stocké en DB, +aucune modif de fichier d'app, aucun `bench migrate`). ADDITIF : ne touche ni la +facturation ni les flux existants. + +DocTypes créés : + - Shift Template : modèles de shifts (heures, couleur, couverture/compétences défaut) + - Shift Requirement : besoin de couverture par date/zone (→ « dispo vs requis ») + - Shift Assignment : assignation tech↔date↔shift (statut Proposé/Publié, source solveur/manuel) + - Tech Availability : congé / pause / indispo (workflow Demandé→Approuvé) + +Exécution (depuis le host) : + docker cp create_roster_doctypes.py erpnext-backend-1:/tmp/create_roster_doctypes.py + docker exec -i erpnext-backend-1 bash -lc \ + "cd /home/frappe/frappe-bench && bench --site erp.gigafibre.ca console" <<'EOF' + exec(open('/tmp/create_roster_doctypes.py').read()) + EOF +""" +import frappe + +# Lancer via : cd /home/frappe/frappe-bench && env/bin/python create_roster_doctypes.py +# (vrai module → portée correcte, contrairement à exec() dans la console IPython). +frappe.init(site="erp.gigafibre.ca", sites_path="sites") +frappe.connect() +frappe.flags.ignore_permissions = True + +MODULE = "Core" + +PERMISSIONS = [ + {"role": "System Manager", "read": 1, "write": 1, "create": 1, "delete": 1}, + {"role": "Administrator", "read": 1, "write": 1, "create": 1, "delete": 1}, +] + +SHIFT_TEMPLATE_FIELDS = [ + {"fieldname": "template_name", "fieldtype": "Data", "label": "Nom du modèle", "reqd": 1, "in_list_view": 1}, + {"fieldname": "cb1", "fieldtype": "Column Break"}, + {"fieldname": "start_time", "fieldtype": "Time", "label": "Heure de début"}, + {"fieldname": "end_time", "fieldtype": "Time", "label": "Heure de fin"}, + {"fieldname": "hours", "fieldtype": "Float", "label": "Durée (h)", "description": "Heures payées (hors pause)"}, + {"fieldname": "sb1", "fieldtype": "Section Break", "label": "Couverture & affichage"}, + {"fieldname": "zone", "fieldtype": "Data", "label": "Zone par défaut"}, + {"fieldname": "default_required", "fieldtype": "Int", "label": "Effectif requis (défaut)", "default": "1"}, + {"fieldname": "required_skills", "fieldtype": "Small Text", "label": "Compétences requises", + "description": "Séparées par des virgules (ex: fibre,cuivre)"}, + {"fieldname": "cb2", "fieldtype": "Column Break"}, + {"fieldname": "color", "fieldtype": "Data", "label": "Couleur (hex)", "default": "#1976d2"}, + {"fieldname": "active", "fieldtype": "Check", "label": "Actif", "default": "1"}, +] + +SHIFT_REQUIREMENT_FIELDS = [ + {"fieldname": "requirement_date", "fieldtype": "Date", "label": "Date", "reqd": 1, "in_list_view": 1}, + {"fieldname": "shift_template", "fieldtype": "Link", "options": "Shift Template", "label": "Modèle de shift", "reqd": 1, "in_list_view": 1}, + {"fieldname": "zone", "fieldtype": "Data", "label": "Zone", "in_list_view": 1}, + {"fieldname": "required_count", "fieldtype": "Int", "label": "Effectif requis", "default": "1", "reqd": 1, "in_list_view": 1}, + {"fieldname": "required_skills", "fieldtype": "Small Text", "label": "Compétences requises"}, +] + +SHIFT_ASSIGNMENT_FIELDS = [ + {"fieldname": "technician", "fieldtype": "Data", "label": "Technicien (ID)", "reqd": 1, "in_list_view": 1}, + {"fieldname": "technician_name", "fieldtype": "Data", "label": "Nom du technicien", "in_list_view": 1}, + {"fieldname": "assignment_date", "fieldtype": "Date", "label": "Date", "reqd": 1, "in_list_view": 1}, + {"fieldname": "cb1", "fieldtype": "Column Break"}, + {"fieldname": "shift_template", "fieldtype": "Link", "options": "Shift Template", "label": "Modèle de shift", "reqd": 1, "in_list_view": 1}, + {"fieldname": "zone", "fieldtype": "Data", "label": "Zone"}, + {"fieldname": "hours", "fieldtype": "Float", "label": "Heures"}, + {"fieldname": "sb1", "fieldtype": "Section Break"}, + {"fieldname": "status", "fieldtype": "Select", "options": "Proposé\nPublié\nAnnulé", "default": "Proposé", "label": "Statut", "in_list_view": 1}, + {"fieldname": "source", "fieldtype": "Select", "options": "solveur\nmanuel", "default": "solveur", "label": "Source"}, +] + +TECH_AVAILABILITY_FIELDS = [ + {"fieldname": "technician", "fieldtype": "Data", "label": "Technicien (ID)", "reqd": 1, "in_list_view": 1}, + {"fieldname": "technician_name", "fieldtype": "Data", "label": "Nom du technicien", "in_list_view": 1}, + {"fieldname": "cb1", "fieldtype": "Column Break"}, + {"fieldname": "availability_type", "fieldtype": "Select", "options": "Congé\nPause\nIndisponible\nMaladie", "default": "Congé", "label": "Type", "in_list_view": 1}, + {"fieldname": "status", "fieldtype": "Select", "options": "Demandé\nApprouvé\nRefusé", "default": "Demandé", "label": "Statut", "in_list_view": 1}, + {"fieldname": "sb1", "fieldtype": "Section Break"}, + {"fieldname": "from_date", "fieldtype": "Date", "label": "Du", "reqd": 1, "in_list_view": 1}, + {"fieldname": "to_date", "fieldtype": "Date", "label": "Au", "reqd": 1, "in_list_view": 1}, + {"fieldname": "cb2", "fieldtype": "Column Break"}, + {"fieldname": "reason", "fieldtype": "Small Text", "label": "Motif"}, + {"fieldname": "approver", "fieldtype": "Data", "label": "Approbateur"}, +] + +DOCTYPES = [ + ("Shift Template", SHIFT_TEMPLATE_FIELDS, "field:template_name"), + ("Shift Requirement", SHIFT_REQUIREMENT_FIELDS, "hash"), + ("Shift Assignment", SHIFT_ASSIGNMENT_FIELDS, "hash"), + ("Tech Availability", TECH_AVAILABILITY_FIELDS, "hash"), +] + +out = [] + + +def log(*a): + out.append(" ".join(str(x) for x in a)) + print(*a) + + +def create_dt(name, fields, autoname): + if frappe.db.exists("DocType", name): + log("EXISTS", name) + return + try: + doc = frappe.new_doc("DocType") + doc.update({ + "name": name, + "module": MODULE, + "custom": 1, + "autoname": autoname, + "naming_rule": "Expression" if autoname.startswith("field:") else "Random", + "track_changes": 1, + "fields": fields, + "permissions": PERMISSIONS, + }) + doc.insert(ignore_permissions=True) + frappe.db.commit() + log("CREATED", name) + except Exception as e: + frappe.db.rollback() + log("ERROR", name, "->", str(e)[:200]) + + +for name, fields, autoname in DOCTYPES: + create_dt(name, fields, autoname) + +open("/tmp/roster_out.txt", "w").write("\n".join(out)) +log("DONE") diff --git a/frappe-setup/setup_dispatch_custom_fields.py b/frappe-setup/setup_dispatch_custom_fields.py new file mode 100644 index 0000000..2c0da95 --- /dev/null +++ b/frappe-setup/setup_dispatch_custom_fields.py @@ -0,0 +1,53 @@ +""" +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", "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", {}), + # ── 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é", "À planifier", "booking_prefs", {}), + ("Dispatch Job", "booking_token", "Jeton RDV client", "Data", None, None, "booking_status", + {"read_only": 1, "no_copy": 1}), +] + +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")