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:
parent
6fc8a2d37f
commit
7ea546bbbd
130
frappe-setup/create_roster_doctypes.py
Normal file
130
frappe-setup/create_roster_doctypes.py
Normal 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 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")
|
||||
53
frappe-setup/setup_dispatch_custom_fields.py
Normal file
53
frappe-setup/setup_dispatch_custom_fields.py
Normal 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")
|
||||
Loading…
Reference in New Issue
Block a user