- InlineField component + useInlineEdit composable for Odoo-style dblclick editing - Client search by name, account ID, and legacy_customer_id (or_filters) - SMS/Email notification panel on ContactCard via n8n webhooks - Ticket reply thread via Communication docs - All migration scripts (51 files) now tracked - Client portal and field tech app added to monorepo - README rewritten with full feature list, migration summary, architecture - CHANGELOG updated with all recent work - ROADMAP updated with current completion status - Removed hardcoded tokens from docs (use $ERP_SERVICE_TOKEN) - .gitignore updated (docker/, .claude/, exports/, .quasar/) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
146 lines
5.6 KiB
Python
146 lines
5.6 KiB
Python
"""
|
|
Link Employee → Dispatch Technician and populate technicians from active staff.
|
|
Run inside erpnext-backend-1:
|
|
/home/frappe/frappe-bench/env/bin/python /home/frappe/frappe-bench/import_technicians.py
|
|
"""
|
|
import frappe
|
|
import os
|
|
import time
|
|
from datetime import datetime
|
|
|
|
os.chdir("/home/frappe/frappe-bench/sites")
|
|
frappe.init(site="erp.gigafibre.ca", sites_path=".")
|
|
frappe.connect()
|
|
frappe.local.flags.ignore_permissions = True
|
|
print("Connected:", frappe.local.site)
|
|
|
|
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# PHASE 1: Add 'employee' Link field to Dispatch Technician
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\n" + "="*60)
|
|
print("PHASE 1: ADD EMPLOYEE LINK FIELD")
|
|
print("="*60)
|
|
|
|
existing = frappe.db.sql("""
|
|
SELECT fieldname FROM "tabDocField"
|
|
WHERE parent = 'Dispatch Technician' AND fieldname = 'employee'
|
|
""")
|
|
|
|
if not existing:
|
|
# Get max idx
|
|
max_idx = frappe.db.sql("""
|
|
SELECT COALESCE(MAX(idx), 0) FROM "tabDocField"
|
|
WHERE parent = 'Dispatch Technician'
|
|
""")[0][0]
|
|
|
|
frappe.db.sql("""
|
|
INSERT INTO "tabDocField" (
|
|
name, creation, modified, modified_by, owner, docstatus, idx,
|
|
parent, parentfield, parenttype,
|
|
fieldname, label, fieldtype, options, reqd, read_only, hidden
|
|
) VALUES (
|
|
%(name)s, %(now)s, %(now)s, 'Administrator', 'Administrator', 0, %(idx)s,
|
|
'Dispatch Technician', 'fields', 'DocType',
|
|
'employee', 'Employee', 'Link', 'Employee', 0, 0, 0
|
|
)
|
|
""", {
|
|
"name": "dt-employee-link-{}".format(int(time.time())),
|
|
"now": now_str,
|
|
"idx": max_idx + 1,
|
|
})
|
|
|
|
# Add the column to the actual table
|
|
try:
|
|
frappe.db.sql("""
|
|
ALTER TABLE "tabDispatch Technician" ADD COLUMN employee varchar(140)
|
|
""")
|
|
except Exception as e:
|
|
if "already exists" in str(e).lower() or "duplicate" in str(e).lower():
|
|
print("Column 'employee' already exists")
|
|
else:
|
|
raise
|
|
|
|
frappe.db.commit()
|
|
print("Added 'employee' Link field to Dispatch Technician")
|
|
else:
|
|
print("'employee' field already exists on Dispatch Technician")
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# PHASE 2: Clear test technicians and populate from employees
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\n" + "="*60)
|
|
print("PHASE 2: POPULATE TECHNICIANS FROM EMPLOYEES")
|
|
print("="*60)
|
|
|
|
# Delete existing test technicians
|
|
existing_techs = frappe.db.sql('SELECT COUNT(*) FROM "tabDispatch Technician"')[0][0]
|
|
if existing_techs > 0:
|
|
frappe.db.sql('DELETE FROM "tabDispatch Technician"')
|
|
frappe.db.commit()
|
|
print("Deleted {} test technicians".format(existing_techs))
|
|
|
|
# Get active employees in tech/support/sysadmin roles (Operations + Customer Service departments)
|
|
# These are the staff who do field work or dispatch-related tasks
|
|
employees = frappe.db.sql("""
|
|
SELECT name, employee_name, employee_number, cell_number, company_email,
|
|
department, designation, status
|
|
FROM "tabEmployee"
|
|
WHERE status = 'Active'
|
|
AND department IN ('Operations - T', 'Customer Service - T', 'Management - T')
|
|
ORDER BY name
|
|
""", as_dict=True)
|
|
|
|
print("Active dispatch-eligible employees: {}".format(len(employees)))
|
|
|
|
counter = 0
|
|
for emp in employees:
|
|
counter += 1
|
|
tech_id = "TECH-{}".format(emp["employee_number"])
|
|
tech_name = tech_id # document name
|
|
|
|
frappe.db.sql("""
|
|
INSERT INTO "tabDispatch Technician" (
|
|
name, creation, modified, modified_by, owner, docstatus, idx,
|
|
technician_id, full_name, phone, email, status, employee
|
|
) VALUES (
|
|
%(name)s, %(now)s, %(now)s, 'Administrator', 'Administrator', 0, 0,
|
|
%(tech_id)s, %(full_name)s, %(phone)s, %(email)s, 'Disponible', %(employee)s
|
|
)
|
|
""", {
|
|
"name": tech_name,
|
|
"now": now_str,
|
|
"tech_id": tech_id,
|
|
"full_name": emp["employee_name"],
|
|
"phone": emp["cell_number"],
|
|
"email": emp["company_email"],
|
|
"employee": emp["name"],
|
|
})
|
|
|
|
frappe.db.commit()
|
|
print("Created {} Dispatch Technicians".format(counter))
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# PHASE 3: VERIFY
|
|
# ═══════════════════════════════════════════════════════════════
|
|
print("\n" + "="*60)
|
|
print("PHASE 3: VERIFY")
|
|
print("="*60)
|
|
|
|
techs = frappe.db.sql("""
|
|
SELECT name, technician_id, full_name, phone, email, employee
|
|
FROM "tabDispatch Technician"
|
|
ORDER BY name
|
|
""", as_dict=True)
|
|
|
|
print("Total Dispatch Technicians: {}".format(len(techs)))
|
|
for t in techs:
|
|
print(" {} → {} phone={} email={} emp={}".format(
|
|
t["technician_id"], t["full_name"],
|
|
t["phone"] or "-", t["email"] or "-", t["employee"]))
|
|
|
|
# Clear cache
|
|
frappe.clear_cache()
|
|
print("\nCache cleared — Dispatch Technician doctype updated")
|