import argparse import json import os import sys from pathlib import Path import requests from servicem8_quote_template_parser import ( QUOTE_TEMPLATE_FORM_UUID, STATE_DB_PATH, init_state_db, load_input_file, parse_quote_template_form_response, record_generated_job_material, ) BASE_URL = os.getenv("SERVICEM8_BASE_URL", "https://api.servicem8.com/api_1.0") ACCESS_TOKEN = os.getenv("SERVICEM8_ACCESS_TOKEN", "") REQUEST_TIMEOUT = int(os.getenv("SERVICEM8_TIMEOUT", "30")) def build_payload(job_uuid: str, row: dict) -> dict: return { "job_uuid": job_uuid, "material_uuid": row["material_uuid"], "name": row["name"], "quantity": row["quantity"], "price": row["price"], "displayed_amount": row["displayed_amount"], "displayed_amount_is_tax_inclusive": row["displayed_amount_is_tax_inclusive"], "sort_order": row["sort_order"], } def create_job_material(session: requests.Session, payload: dict) -> str: response = session.post(f"{BASE_URL}/jobmaterial.json", json=payload, timeout=REQUEST_TIMEOUT) if not response.ok: raise RuntimeError(f"Create failed: HTTP {response.status_code} :: {response.text}") record_uuid = response.headers.get("x-record-uuid", "") if not record_uuid: raise RuntimeError("Create succeeded but x-record-uuid header was missing") return record_uuid def main(): parser = argparse.ArgumentParser(description="Create ServiceM8 jobMaterials from a Quote Template form response") parser.add_argument("input", help="Path to JSON file containing full form response payload or a data object with field_data") parser.add_argument("--apply", action="store_true", help="Actually create records in ServiceM8. Default is dry-run.") parser.add_argument("--pretty", action="store_true", help="Pretty-print output JSON") args = parser.parse_args() init_state_db(STATE_DB_PATH) payload = load_input_file(args.input) parsed = parse_quote_template_form_response(payload) form_uuid = parsed.get("form_uuid", "") if form_uuid and form_uuid != QUOTE_TEMPLATE_FORM_UUID: raise SystemExit(f"Not a Quote Template form response: {form_uuid}") job_uuid = parsed.get("job_uuid", "") form_response_uuid = parsed.get("form_response_uuid", "") desired_rows = parsed.get("desired_job_materials", []) if not job_uuid: raise SystemExit("Missing job_uuid / regarding_object_uuid in form response") result = { "mode": "apply" if args.apply else "dry-run", "job_uuid": job_uuid, "form_response_uuid": form_response_uuid, "count": len(desired_rows), "rows": [], "state_db_path": str(STATE_DB_PATH), } if not args.apply: for row in desired_rows: result["rows"].append( { "action": "would_create", "kind": row["kind"], "payload": build_payload(job_uuid, row), "source_question": row.get("source_question", ""), "source_field_uuid": row.get("source_field_uuid", ""), } ) print(json.dumps(result, indent=2 if args.pretty else None, ensure_ascii=False)) return if not ACCESS_TOKEN: raise SystemExit("SERVICEM8_ACCESS_TOKEN is required for --apply") session = requests.Session() session.headers.update( { "X-Api-Key": ACCESS_TOKEN, "Accept": "application/json", "Content-Type": "application/json", } ) for row in desired_rows: api_payload = build_payload(job_uuid, row) created_uuid = create_job_material(session, api_payload) record_generated_job_material( job_uuid=job_uuid, form_response_uuid=form_response_uuid, job_material_uuid=created_uuid, kind=row.get("kind", ""), source_field_uuid=row.get("source_field_uuid", ""), source_question=row.get("source_question", ""), source_text=row.get("name", ""), db_path=STATE_DB_PATH, ) result["rows"].append( { "action": "created", "kind": row["kind"], "job_material_uuid": created_uuid, "payload": api_payload, } ) print(json.dumps(result, indent=2 if args.pretty else None, ensure_ascii=False)) if __name__ == "__main__": try: main() except Exception as exc: print(str(exc), file=sys.stderr) sys.exit(1)