diff --git a/containers/backupchecks/src/backend/app/main/routes_customers.py b/containers/backupchecks/src/backend/app/main/routes_customers.py index e54ab83..cba84e4 100644 --- a/containers/backupchecks/src/backend/app/main/routes_customers.py +++ b/containers/backupchecks/src/backend/app/main/routes_customers.py @@ -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 - updated += 1 - else: - skipped += 1 + # 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: - 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 diff --git a/containers/backupchecks/src/backend/app/main/routes_settings.py b/containers/backupchecks/src/backend/app/main/routes_settings.py index f7e772c..c81e495 100644 --- a/containers/backupchecks/src/backend/app/main/routes_settings.py +++ b/containers/backupchecks/src/backend/app/main/routes_settings.py @@ -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: diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index fe813ec..d21c29c 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -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)