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:
parent
4c7c97f772
commit
3bacb55590
@ -425,9 +425,14 @@ def customers_export():
|
||||
items = Customer.query.order_by(Customer.name.asc()).all()
|
||||
buf = StringIO()
|
||||
writer = csv.writer(buf)
|
||||
writer.writerow(["name", "active"])
|
||||
writer.writerow(["name", "active", "autotask_company_id", "autotask_company_name"])
|
||||
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")
|
||||
return Response(
|
||||
@ -475,6 +480,14 @@ def customers_import():
|
||||
header = [c.strip().lower() for c in (rows[0] or [])]
|
||||
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:]:
|
||||
if not r:
|
||||
continue
|
||||
@ -491,15 +504,39 @@ def customers_import():
|
||||
elif a in ("0", "false", "no", "n", "inactive"):
|
||||
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()
|
||||
if existing:
|
||||
if active_val is not None:
|
||||
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
|
||||
else:
|
||||
skipped += 1
|
||||
else:
|
||||
c = Customer(name=name, active=True if active_val is None else active_val)
|
||||
c = Customer(
|
||||
name=name,
|
||||
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)
|
||||
created += 1
|
||||
|
||||
|
||||
@ -219,14 +219,22 @@ def settings_jobs_export():
|
||||
customer_by_id = {}
|
||||
for job in jobs:
|
||||
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:
|
||||
customer = customer_by_id.get(job.customer_id)
|
||||
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),
|
||||
"backup_software": job.backup_software,
|
||||
"backup_type": job.backup_type,
|
||||
@ -283,10 +291,46 @@ def settings_jobs_import():
|
||||
return redirect(url_for("main.settings", section="general"))
|
||||
|
||||
created_customers = 0
|
||||
updated_customers = 0
|
||||
created_jobs = 0
|
||||
updated_jobs = 0
|
||||
|
||||
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:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
@ -388,7 +432,7 @@ def settings_jobs_import():
|
||||
|
||||
db.session.commit()
|
||||
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",
|
||||
)
|
||||
except Exception as exc:
|
||||
|
||||
@ -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
|
||||
- Eliminates need to manually click "Refresh reference data" before selecting defaults
|
||||
- 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
|
||||
- Renamed "Refresh" button to "Search" in Link existing Autotask ticket modal for better clarity (the button performs a search operation)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user