diff --git a/apply_polled_quote_template_jobmaterials.py b/apply_polled_quote_template_jobmaterials.py index ac01281..845d71a 100755 --- a/apply_polled_quote_template_jobmaterials.py +++ b/apply_polled_quote_template_jobmaterials.py @@ -33,6 +33,11 @@ DEV_QUOTE_MATERIAL_UUID = "f78b1d23-b9fa-40fe-a806-2425fe09cc0b" QUOTE_INCLUDE_HEADER_MATERIAL_UUID = "1924893b-917f-474a-adaa-2093bd622d4b" QUOTE_EXCLUDE_HEADER_MATERIAL_UUID = "4947bfd7-4875-48f7-9caf-2093b9751b9b" DEV_QUOTE_TAX_RATE_UUID = "84e4dd28-06b3-452b-a796-1f58a20ac49b" +QUOTE_DESCRIPTION_PREFIX = "Thank you for the opportunity to quote to " +QUOTE_DESCRIPTION_SUFFIX = ( + "Please find below a detailed breakdown of the proposed costs included in the quotation. " + "If you have any questions or concerns, please do not hesitate to contact us." +) def utc_now() -> str: @@ -66,6 +71,69 @@ def build_payload(job_uuid: str, row: dict) -> dict: } +def clean_text(value: Any) -> str: + if value is None: + return "" + return str(value).replace("\r\n", "\n").replace("\r", "\n").strip() + + +def first_text(*values: Any) -> str: + for value in values: + text = clean_text(value) + if text: + return text + return "" + + +def format_job_address(job: Dict[str, Any]) -> str: + direct = first_text( + job.get("job_address"), + job.get("site_address"), + job.get("address"), + job.get("location_address"), + job.get("billing_address"), + ) + if direct: + return direct + + parts = [ + first_text(job.get("street"), job.get("street_address"), job.get("address_1"), job.get("address1")), + first_text(job.get("suburb"), job.get("city")), + first_text(job.get("state")), + first_text(job.get("postcode"), job.get("postal_code"), job.get("zip")), + ] + return " ".join(part for part in parts if part) + + +def build_quote_description_text(description: str, job: Dict[str, Any]) -> str: + description = clean_text(description) + if not description: + return "" + address = format_job_address(job) or "the job address" + return f"{QUOTE_DESCRIPTION_PREFIX} {description} at {address}. {QUOTE_DESCRIPTION_SUFFIX}" + + +def build_job_update_payload(description: str, job: Dict[str, Any]) -> dict: + quote_description = build_quote_description_text(description, job) + return {"job_description": quote_description} if quote_description else {} + + +def retrieve_job(session: requests.Session, job_uuid: str) -> Dict[str, Any]: + response = session.get(f"{BASE_URL}/job/{job_uuid}.json", timeout=REQUEST_TIMEOUT) + if not response.ok: + raise RuntimeError(f"Job retrieve failed: HTTP {response.status_code} :: {response.text[:1000]}") + data = response.json() + if not isinstance(data, dict): + raise RuntimeError(f"Job retrieve expected object response, got {type(data).__name__}") + return data + + +def update_job_description(session: requests.Session, job_uuid: str, payload: dict) -> None: + response = session.post(f"{BASE_URL}/job/{job_uuid}.json", json=payload, timeout=REQUEST_TIMEOUT) + if not response.ok: + raise RuntimeError(f"Job description update failed: HTTP {response.status_code} :: {response.text[:1000]}") + + 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: @@ -393,7 +461,40 @@ def main() -> int: } try: + api_key = load_api_key() + session = requests.Session() + session.headers.update({"X-Api-Key": api_key, "Accept": "application/json", "Content-Type": "application/json"}) + + quote_description_source = clean_text(quote["description"]) + job_details = retrieve_job(session, job_uuid) if quote_description_source else {} + job_update_payload = build_job_update_payload(quote_description_source, job_details) + job_update_record_payload = { + "endpoint": f"/job/{job_uuid}.json", + "payload": job_update_payload, + "source_description": quote_description_source, + "job_address": format_job_address(job_details) if job_details else "", + } + job_update_row = { + "kind": "job_description", + "source_question": "Description of Works to be Quoted", + "name": job_update_payload.get("job_description", ""), + } + if not args.apply: + if job_update_payload: + record_apply_row( + conn, + run_id=run_id, + form_response_uuid=form_response_uuid, + job_uuid=job_uuid, + row_index=0, + row=job_update_row, + api_payload=job_update_record_payload, + action="would_update_job_description", + ) + result["job_update"] = {"action": "would_update_job_description", **job_update_record_payload} + else: + result["job_update"] = {"action": "skipped", "reason": "Quote description is empty"} for idx, row in enumerate(desired_rows, start=1): api_payload = build_payload(job_uuid, row) record_apply_row( @@ -416,10 +517,7 @@ def main() -> int: print(json.dumps(result, indent=2 if args.pretty else None, ensure_ascii=False)) return 0 - api_key = load_api_key() init_state_db(STATE_DB_PATH) - session = requests.Session() - session.headers.update({"X-Api-Key": api_key, "Accept": "application/json", "Content-Type": "application/json"}) remote_existing_rows = list_remote_job_materials(session, job_uuid) if remote_existing_rows: @@ -460,6 +558,22 @@ def main() -> int: print(json.dumps(result, indent=2 if args.pretty else None, ensure_ascii=False)) return 0 + if job_update_payload: + update_job_description(session, job_uuid, job_update_payload) + record_apply_row( + conn, + run_id=run_id, + form_response_uuid=form_response_uuid, + job_uuid=job_uuid, + row_index=0, + row=job_update_row, + api_payload=job_update_record_payload, + action="updated_job_description", + ) + result["job_update"] = {"action": "updated_job_description", **job_update_record_payload} + else: + result["job_update"] = {"action": "skipped", "reason": "Quote description is empty"} + created_count = 0 for idx, row in enumerate(desired_rows, start=1): api_payload = build_payload(job_uuid, row) diff --git a/servicem8_inspector.py b/servicem8_inspector.py index 8db8524..a248c7d 100644 --- a/servicem8_inspector.py +++ b/servicem8_inspector.py @@ -93,6 +93,14 @@ def pretty_json(value): return str(value) +def html_value(value): + if value is None: + return "" + if isinstance(value, (dict, list)): + return pretty_json(value) + return str(value) + + def parse_json_field(value, default=None): if value is None: return default @@ -520,10 +528,10 @@ def form_response_detail(row_id: int): for item in sorted(field_data, key=lambda x: x.get("SortOrder", 0)): field_rows.append( f"" - f"{escape(str(item.get('SortOrder', '')))}" - f"{escape(item.get('FieldType', ''))}" - f"{escape(item.get('Question', ''))}" - f"{escape(item.get('Response', ''))}" + f"{escape(html_value(item.get('SortOrder')))}" + f"{escape(html_value(item.get('FieldType')))}" + f"{escape(html_value(item.get('Question')))}" + f"{escape(html_value(item.get('Response')))}" f"" ) @@ -851,8 +859,8 @@ def polled_form_response_detail(form_response_uuid: str): for item in sorted(field_data, key=lambda x: x.get("SortOrder", 0) if isinstance(x, dict) else 0): if isinstance(item, dict): field_rows.append( - f"{escape(str(item.get('SortOrder', '')))}{escape(item.get('FieldType', ''))}" - f"{escape(item.get('Question', ''))}{escape(item.get('Response', ''))}" + f"{escape(html_value(item.get('SortOrder')))}{escape(html_value(item.get('FieldType')))}" + f"{escape(html_value(item.get('Question')))}{escape(html_value(item.get('Response')))}" ) quote_link = f"
Parsed quote
Open parsed Quote Template view
" if quote else ""