Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c4248eba76 | |||
| c425f45910 |
@@ -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)
|
||||
|
||||
@@ -7,8 +7,8 @@ BASE_URL = os.getenv('SERVICEM8_BASE_URL', 'https://api.servicem8.com')
|
||||
ENDPOINT = '/webhook_subscriptions/event'
|
||||
ACCESS_TOKEN = os.getenv('SERVICEM8_ACCESS_TOKEN', 'smk-ac525b-99c4b96305a49c7c-fe4dd3e705b647ea')
|
||||
EVENT_NAME = os.getenv('SERVICEM8_EVENT', 'form.response_created')
|
||||
CALLBACK_URL = os.getenv('SERVICEM8_CALLBACK_URL', 'https://nps-dev.coast2cloud.net/webhooks/servicem8/form-response')
|
||||
UNIQUE_ID = os.getenv('SERVICEM8_UNIQUE_ID', 'dev-form-response')
|
||||
CALLBACK_URL = os.getenv('SERVICEM8_CALLBACK_URL', 'https://webhook.naroomaplumbing.au/webhooks/servicem8/form-response')
|
||||
UNIQUE_ID = os.getenv('SERVICEM8_UNIQUE_ID', 'au-dev-form-response')
|
||||
|
||||
|
||||
def pretty_print_json(data):
|
||||
|
||||
@@ -8,8 +8,8 @@ ENDPOINT = '/webhook_subscriptions'
|
||||
ACCESS_TOKEN = os.getenv('SERVICEM8_ACCESS_TOKEN', 'smk-ac525b-99c4b96305a49c7c-fe4dd3e705b647ea')
|
||||
OBJECT_NAME = os.getenv('SERVICEM8_OBJECT', 'job')
|
||||
FIELDS = os.getenv('SERVICEM8_FIELDS', 'uuid,status,date,queue_uuid,queue_expiry_date,work_order_date,active,edit_date,generated_job_id')
|
||||
CALLBACK_URL = os.getenv('SERVICEM8_CALLBACK_URL', 'https://nps-dev.coast2cloud.net/webhooks/servicem8-object')
|
||||
UNIQUE_ID = os.getenv('SERVICEM8_UNIQUE_ID', 'dev-job-object')
|
||||
CALLBACK_URL = os.getenv('SERVICEM8_CALLBACK_URL', 'https://webhook.naroomaplumbing.au/webhooks/servicem8-object')
|
||||
UNIQUE_ID = os.getenv('SERVICEM8_UNIQUE_ID', 'au-dev-job-object')
|
||||
|
||||
|
||||
def pretty_print_json(data):
|
||||
|
||||
@@ -5,9 +5,9 @@ import requests
|
||||
|
||||
BASE_URL = os.getenv('SERVICEM8_BASE_URL', 'https://api.servicem8.com')
|
||||
ENDPOINT = '/webhook_subscriptions/event'
|
||||
ACCESS_TOKEN = os.getenv('SERVICEM8_ACCESS_TOKEN', '')
|
||||
ACCESS_TOKEN = os.getenv('SERVICEM8_ACCESS_TOKEN', 'smk-ac525b-99c4b96305a49c7c-fe4dd3e705b647ea')
|
||||
EVENT_NAME = os.getenv('SERVICEM8_EVENT', 'job.updated')
|
||||
CALLBACK_URL = os.getenv('SERVICEM8_CALLBACK_URL', 'https://nps-dev.coast2cloud.net/webhooks/servicem8-job-updated')
|
||||
CALLBACK_URL = os.getenv('SERVICEM8_CALLBACK_URL', 'https://webhook.naroomaplumbing.au/webhooks/servicem8-job-updated')
|
||||
UNIQUE_ID = os.getenv('SERVICEM8_UNIQUE_ID', 'dev-job-updated')
|
||||
|
||||
|
||||
|
||||
+14
-6
@@ -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"<tr>"
|
||||
f"<td>{escape(str(item.get('SortOrder', '')))}</td>"
|
||||
f"<td>{escape(item.get('FieldType', ''))}</td>"
|
||||
f"<td>{escape(item.get('Question', ''))}</td>"
|
||||
f"<td>{escape(item.get('Response', ''))}</td>"
|
||||
f"<td>{escape(html_value(item.get('SortOrder')))}</td>"
|
||||
f"<td>{escape(html_value(item.get('FieldType')))}</td>"
|
||||
f"<td>{escape(html_value(item.get('Question')))}</td>"
|
||||
f"<td>{escape(html_value(item.get('Response')))}</td>"
|
||||
f"</tr>"
|
||||
)
|
||||
|
||||
@@ -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"<tr><td>{escape(str(item.get('SortOrder', '')))}</td><td>{escape(item.get('FieldType', ''))}</td>"
|
||||
f"<td>{escape(item.get('Question', ''))}</td><td>{escape(item.get('Response', ''))}</td></tr>"
|
||||
f"<tr><td>{escape(html_value(item.get('SortOrder')))}</td><td>{escape(html_value(item.get('FieldType')))}</td>"
|
||||
f"<td>{escape(html_value(item.get('Question')))}</td><td>{escape(html_value(item.get('Response')))}</td></tr>"
|
||||
)
|
||||
quote_link = f"<div><strong>Parsed quote</strong></div><div><a href='/poll/quote-template/{escape(form_response_uuid)}'>Open parsed Quote Template view</a></div>" if quote else ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user