Roster/booking: doctypes + custom fields ERPNext (idempotent)

- 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) <noreply@anthropic.com>
This commit is contained in:
louispaulb 2026-06-03 16:43:36 -04:00
parent 6fc8a2d37f
commit 7ea546bbbd
2 changed files with 183 additions and 0 deletions

View File

@ -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 techdateshift (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")

View File

@ -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")