Add Autotask company mapping to export/import functionality

Extended both customer and jobs export/import to preserve Autotask
company mappings during data migration and system reset workflows.

Changes:
- Customer export (CSV): Added autotask_company_id and autotask_company_name columns
- Customer import (CSV): Reads and applies Autotask mapping fields, resets sync status
- Jobs export (JSON): Includes Autotask fields in customers array
- Jobs import (JSON): Processes customers array with Autotask mappings before jobs
- Backwards compatible: Old exports without Autotask fields continue to work
- Import feedback: Shows both created and updated customer counts

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Ivo Oskamp 2026-02-07 00:03:15 +01:00
parent 4c7c97f772
commit 3bacb55590
3 changed files with 103 additions and 10 deletions

View File

@ -425,9 +425,14 @@ def customers_export():
items = Customer.query.order_by(Customer.name.asc()).all() items = Customer.query.order_by(Customer.name.asc()).all()
buf = StringIO() buf = StringIO()
writer = csv.writer(buf) writer = csv.writer(buf)
writer.writerow(["name", "active"]) writer.writerow(["name", "active", "autotask_company_id", "autotask_company_name"])
for c in items: for c in items:
writer.writerow([c.name, "1" if c.active else "0"]) writer.writerow([
c.name,
"1" if c.active else "0",
c.autotask_company_id if c.autotask_company_id else "",
c.autotask_company_name if c.autotask_company_name else ""
])
out = buf.getvalue().encode("utf-8") out = buf.getvalue().encode("utf-8")
return Response( return Response(
@ -475,6 +480,14 @@ def customers_import():
header = [c.strip().lower() for c in (rows[0] or [])] header = [c.strip().lower() for c in (rows[0] or [])]
start_idx = 1 if ("name" in header or "customer" in header) else 0 start_idx = 1 if ("name" in header or "customer" in header) else 0
# Detect Autotask columns (backwards compatible - these are optional)
autotask_id_idx = None
autotask_name_idx = None
if "autotask_company_id" in header:
autotask_id_idx = header.index("autotask_company_id")
if "autotask_company_name" in header:
autotask_name_idx = header.index("autotask_company_name")
for r in rows[start_idx:]: for r in rows[start_idx:]:
if not r: if not r:
continue continue
@ -491,15 +504,39 @@ def customers_import():
elif a in ("0", "false", "no", "n", "inactive"): elif a in ("0", "false", "no", "n", "inactive"):
active_val = False active_val = False
# Read Autotask fields if present
autotask_company_id = None
autotask_company_name = None
if autotask_id_idx is not None and len(r) > autotask_id_idx:
id_val = (r[autotask_id_idx] or "").strip()
if id_val:
try:
autotask_company_id = int(id_val)
except ValueError:
pass
if autotask_name_idx is not None and len(r) > autotask_name_idx:
name_val = (r[autotask_name_idx] or "").strip()
if name_val:
autotask_company_name = name_val
existing = Customer.query.filter_by(name=name).first() existing = Customer.query.filter_by(name=name).first()
if existing: if existing:
if active_val is not None: if active_val is not None:
existing.active = active_val existing.active = active_val
# Update Autotask mapping if provided in CSV
if autotask_company_id is not None:
existing.autotask_company_id = autotask_company_id
existing.autotask_company_name = autotask_company_name
existing.autotask_mapping_status = None # Will be resynced
existing.autotask_last_sync_at = None
updated += 1 updated += 1
else: else:
skipped += 1 c = Customer(
else: name=name,
c = Customer(name=name, active=True if active_val is None else active_val) active=True if active_val is None else active_val,
autotask_company_id=autotask_company_id,
autotask_company_name=autotask_company_name
)
db.session.add(c) db.session.add(c)
created += 1 created += 1

View File

@ -219,14 +219,22 @@ def settings_jobs_export():
customer_by_id = {} customer_by_id = {}
for job in jobs: for job in jobs:
if job.customer_id and job.customer and job.customer.name: if job.customer_id and job.customer and job.customer.name:
customer_by_id[job.customer_id] = job.customer.name customer_by_id[job.customer_id] = job.customer
payload["customers"] = [{"name": name} for _, name in sorted(customer_by_id.items(), key=lambda x: x[1].lower())] payload["customers"] = [
{
"name": customer.name,
"autotask_company_id": customer.autotask_company_id,
"autotask_company_name": customer.autotask_company_name
}
for _, customer in sorted(customer_by_id.items(), key=lambda x: x[1].name.lower())
]
for job in jobs: for job in jobs:
customer = customer_by_id.get(job.customer_id)
payload["jobs"].append( payload["jobs"].append(
{ {
"customer_name": customer_by_id.get(job.customer_id), "customer_name": customer.name if customer else None,
"from_address": getattr(job, "from_address", None), "from_address": getattr(job, "from_address", None),
"backup_software": job.backup_software, "backup_software": job.backup_software,
"backup_type": job.backup_type, "backup_type": job.backup_type,
@ -283,10 +291,46 @@ def settings_jobs_import():
return redirect(url_for("main.settings", section="general")) return redirect(url_for("main.settings", section="general"))
created_customers = 0 created_customers = 0
updated_customers = 0
created_jobs = 0 created_jobs = 0
updated_jobs = 0 updated_jobs = 0
try: try:
# First, process customers from the payload (if present)
customers_data = payload.get("customers") or []
if isinstance(customers_data, list):
for cust_item in customers_data:
if not isinstance(cust_item, dict):
continue
cust_name = (cust_item.get("name") or "").strip()
if not cust_name:
continue
# Read Autotask fields (backwards compatible - optional)
autotask_company_id = cust_item.get("autotask_company_id")
autotask_company_name = cust_item.get("autotask_company_name")
existing_customer = Customer.query.filter_by(name=cust_name).first()
if existing_customer:
# Update Autotask mapping if provided
if autotask_company_id is not None:
existing_customer.autotask_company_id = autotask_company_id
existing_customer.autotask_company_name = autotask_company_name
existing_customer.autotask_mapping_status = None # Will be resynced
existing_customer.autotask_last_sync_at = None
updated_customers += 1
else:
new_customer = Customer(
name=cust_name,
active=True,
autotask_company_id=autotask_company_id,
autotask_company_name=autotask_company_name
)
db.session.add(new_customer)
created_customers += 1
db.session.flush()
# Now process jobs (customers should already exist from above, or will be created on-the-fly)
for item in jobs: for item in jobs:
if not isinstance(item, dict): if not isinstance(item, dict):
continue continue
@ -388,7 +432,7 @@ def settings_jobs_import():
db.session.commit() db.session.commit()
flash( flash(
f"Import completed. Customers created: {created_customers}. Jobs created: {created_jobs}. Jobs updated: {updated_jobs}.", f"Import completed. Customers created: {created_customers}, updated: {updated_customers}. Jobs created: {created_jobs}, updated: {updated_jobs}.",
"success", "success",
) )
except Exception as exc: except Exception as exc:

View File

@ -32,6 +32,18 @@ This file documents all changes made to this project via Claude Code.
- Only triggers when Autotask is enabled, credentials are configured, and cache is empty - Only triggers when Autotask is enabled, credentials are configured, and cache is empty
- Eliminates need to manually click "Refresh reference data" before selecting defaults - Eliminates need to manually click "Refresh reference data" before selecting defaults
- Displays info message with loaded data counts - Displays info message with loaded data counts
- Extended export/import functionality to include Autotask company mappings:
- **Customer Export/Import** (CSV at /customers/export):
- Export now includes `autotask_company_id` and `autotask_company_name` columns
- Import reads Autotask mapping fields and applies them to existing or new customers
- Backwards compatible - old CSV files without Autotask columns still work
- Import resets `autotask_mapping_status` and `autotask_last_sync_at` to allow resynchronization
- **Jobs Export/Import** (JSON at Settings > Maintenance):
- Export now includes Autotask fields in customers array (`autotask_company_id`, `autotask_company_name`)
- Import processes customers array first, applying Autotask mappings before creating jobs
- Schema remains `approved_jobs_export_v1` for backwards compatibility
- Import message now shows both created and updated customer counts
- Enables preservation of Autotask company mappings during system reset/migration workflows
### Changed ### Changed
- Renamed "Refresh" button to "Search" in Link existing Autotask ticket modal for better clarity (the button performs a search operation) - Renamed "Refresh" button to "Search" in Link existing Autotask ticket modal for better clarity (the button performs a search operation)