316 lines
11 KiB
Python
Executable file
316 lines
11 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Minimal mock Synq Desktop Connector for testing the Odoo bridge."""
|
|
|
|
from flask import Flask, request, jsonify
|
|
import random
|
|
from datetime import datetime, timedelta
|
|
|
|
app = Flask(__name__)
|
|
|
|
DEMO_TOKEN = "demo"
|
|
DEMO_SECRET = "demo"
|
|
|
|
|
|
def check_auth():
|
|
token = request.headers.get("X-Synq-Desktop-Token")
|
|
secret = request.headers.get("X-Synq-Desktop-Secret")
|
|
return token == DEMO_TOKEN and secret == DEMO_SECRET
|
|
|
|
|
|
def jsonrpc_wrap(result):
|
|
return {"jsonrpc": "2.0", "id": random.randint(1, 999999), "result": result}
|
|
|
|
|
|
@app.route("/synq/desktop/v1/appointments/upcoming", methods=["POST"])
|
|
def appointments_upcoming():
|
|
if not check_auth():
|
|
return jsonrpc_wrap({"status": "error", "message": "Invalid credentials"}), 401
|
|
now = datetime.now()
|
|
appts = []
|
|
for i in range(4):
|
|
start = now + timedelta(days=i, hours=i % 3)
|
|
appts.append({
|
|
"id": i + 1,
|
|
"name": f"Consultation {i+1}",
|
|
"state": "confirmed",
|
|
"start": start.isoformat(),
|
|
"stop": (start + timedelta(hours=1)).isoformat(),
|
|
"duration": 1.0,
|
|
"appointment_location": "Room A" if i % 2 == 0 else "Room B",
|
|
"is_virtual": False,
|
|
"type_name": "Follow-up",
|
|
"physician_name": "Dr. Qazi",
|
|
"assigned_ma_names": ["MA Sarah"],
|
|
"patient": {"id": 100 + i, "name": f"Patient {i+1}", "email": f"p{i+1}@test.com", "phone": "555-0100"},
|
|
"display_start": start.strftime("%b %d, %Y, %I:%M %p"),
|
|
})
|
|
return jsonrpc_wrap({"status": "ok", "count": len(appts), "appointments": appts})
|
|
|
|
|
|
@app.route("/synq/desktop/v1/patients/search", methods=["POST"])
|
|
def patients_search():
|
|
if not check_auth():
|
|
return jsonrpc_wrap({"status": "error", "message": "Invalid credentials"}), 401
|
|
data = request.get_json(force=True, silent=True) or {}
|
|
params = data.get("params", {})
|
|
query = params.get("q", "")
|
|
patients = [
|
|
{
|
|
"id": 1,
|
|
"name": "Smith, John - 12345 - 1988-12-11",
|
|
"patient_name": "Smith, John - 12345 - 1988-12-11",
|
|
"email": "john.smith@test.com",
|
|
"phone": "555-0101",
|
|
"mobile": "555-0102",
|
|
"date_of_birth": "1988-12-11",
|
|
"sex": "Male",
|
|
"race": "Caucasian",
|
|
"marital_status": "Single",
|
|
"receivable": 1250.0,
|
|
"membership_name": "Gold",
|
|
"loyalty_points": 450,
|
|
"photo_consent_status": "granted",
|
|
"medical_patient_id": 101,
|
|
"partner_id": 1,
|
|
"tag_ids": [1, 2],
|
|
},
|
|
{
|
|
"id": 2,
|
|
"name": "Doe, Jane - 12346 - 1990-05-22",
|
|
"patient_name": "Doe, Jane - 12346 - 1990-05-22",
|
|
"email": "jane.doe@test.com",
|
|
"phone": "555-0201",
|
|
"mobile": None,
|
|
"date_of_birth": "1990-05-22",
|
|
"sex": "Female",
|
|
"race": "Asian",
|
|
"marital_status": "Married",
|
|
"receivable": 0.0,
|
|
"membership_name": "Silver",
|
|
"loyalty_points": 120,
|
|
"photo_consent_status": "pending",
|
|
"medical_patient_id": 102,
|
|
"partner_id": 2,
|
|
"tag_ids": [3],
|
|
},
|
|
]
|
|
if query:
|
|
patients = [p for p in patients if query.lower() in p["name"].lower()]
|
|
return jsonrpc_wrap({"status": "ok", "count": len(patients), "patients": patients})
|
|
|
|
|
|
@app.route("/synq/desktop/v1/treatments", methods=["POST"])
|
|
def treatments_list():
|
|
if not check_auth():
|
|
return jsonrpc_wrap({"status": "error", "message": "Invalid credentials"}), 401
|
|
treatments = [
|
|
{
|
|
"id": 1,
|
|
"name": "Before/After Photo Set",
|
|
"state": "open",
|
|
"date_deadline": (datetime.now() + timedelta(days=3)).isoformat(),
|
|
"patient_name": "Smith, John",
|
|
"patient": {"id": 1, "name": "Smith, John"},
|
|
"appointment": None,
|
|
"is_before_after": True,
|
|
"after_before_project": True,
|
|
"is_internal": False,
|
|
"internal_project": False,
|
|
"description": "Capture photos",
|
|
"provider_name": "Dr. Qazi",
|
|
},
|
|
{
|
|
"id": 2,
|
|
"name": "Internal Review",
|
|
"state": "open",
|
|
"date_deadline": (datetime.now() + timedelta(days=7)).isoformat(),
|
|
"patient_name": None,
|
|
"patient": None,
|
|
"appointment": None,
|
|
"is_before_after": False,
|
|
"after_before_project": False,
|
|
"is_internal": True,
|
|
"internal_project": True,
|
|
"description": "Staff training",
|
|
"provider_name": "Office Manager",
|
|
},
|
|
]
|
|
return jsonrpc_wrap({"status": "ok", "count": len(treatments), "treatments": treatments})
|
|
|
|
|
|
@app.route("/synq/desktop/v1/messages", methods=["POST"])
|
|
def messages_list():
|
|
if not check_auth():
|
|
return jsonrpc_wrap({"status": "error", "message": "Invalid credentials"}), 401
|
|
messages = [
|
|
{
|
|
"id": 1,
|
|
"patient_name": "Smith, John",
|
|
"subject": "Appointment Reminder",
|
|
"content": "Your appointment is tomorrow at 9:00 AM.",
|
|
"date_time": (datetime.now() - timedelta(hours=2)).isoformat(),
|
|
"entered_by": "System",
|
|
"read_status": "unread",
|
|
"has_attachment": False,
|
|
"method": "SMS",
|
|
"method_id": {"id": 1, "name": "SMS"},
|
|
},
|
|
{
|
|
"id": 2,
|
|
"patient_name": "Doe, Jane",
|
|
"subject": "Follow-up",
|
|
"content": "Please complete your post-op survey.",
|
|
"date_time": (datetime.now() - timedelta(days=1)).isoformat(),
|
|
"entered_by": "Dr. Qazi",
|
|
"read_status": "read",
|
|
"has_attachment": False,
|
|
"method": "Email",
|
|
"method_id": {"id": 2, "name": "Email"},
|
|
},
|
|
]
|
|
return jsonrpc_wrap({"status": "ok", "count": len(messages), "messages": messages})
|
|
|
|
|
|
@app.route("/synq/desktop/v1/billing", methods=["POST"])
|
|
def billing_list():
|
|
if not check_auth():
|
|
return jsonrpc_wrap({"status": "error", "message": "Invalid credentials"}), 401
|
|
invoices = [
|
|
{
|
|
"id": 1,
|
|
"name": "INV/2024/0001",
|
|
"partner_name": "Smith, John",
|
|
"amount_total": 2500.0,
|
|
"amount_residual": 1250.0,
|
|
"state": "posted",
|
|
"payment_state": "partial",
|
|
"invoice_date": "2024-01-15",
|
|
"invoice_date_due": "2024-02-15",
|
|
},
|
|
{
|
|
"id": 2,
|
|
"name": "INV/2024/0002",
|
|
"partner_name": "Doe, Jane",
|
|
"amount_total": 1800.0,
|
|
"amount_residual": 0.0,
|
|
"state": "posted",
|
|
"payment_state": "paid",
|
|
"invoice_date": "2024-01-10",
|
|
"invoice_date_due": "2024-02-10",
|
|
},
|
|
]
|
|
return jsonrpc_wrap({"status": "ok", "count": len(invoices), "invoices": invoices})
|
|
|
|
|
|
@app.route("/synq/desktop/v1/sale_orders", methods=["POST"])
|
|
def sale_orders():
|
|
if not check_auth():
|
|
return jsonrpc_wrap({"status": "error", "message": "Invalid credentials"}), 401
|
|
orders = [
|
|
{"id": 1, "name": "SO/2024/0001", "partner_name": "Smith, John", "amount_total": 2500.0, "state": "sale", "date_order": "2024-01-15"},
|
|
{"id": 2, "name": "SO/2024/0002", "partner_name": "Doe, Jane", "amount_total": 1800.0, "state": "sale", "date_order": "2024-01-10"},
|
|
]
|
|
return jsonrpc_wrap({"status": "ok", "count": len(orders), "sale_orders": orders})
|
|
|
|
|
|
@app.route("/synq/desktop/v1/quotations", methods=["POST"])
|
|
def quotations():
|
|
if not check_auth():
|
|
return jsonrpc_wrap({"status": "error", "message": "Invalid credentials"}), 401
|
|
quotes = [
|
|
{"id": 1, "name": "QUO/2024/0001", "partner_name": "Smith, John", "amount_total": 2500.0, "state": "sent", "date_order": "2024-01-15", "validity_date": "2024-02-15"},
|
|
]
|
|
return jsonrpc_wrap({"status": "ok", "count": len(quotes), "quotations": quotes})
|
|
|
|
|
|
@app.route("/synq/desktop/v1/patient/<int:patient_id>", methods=["POST"])
|
|
def patient_detail(patient_id):
|
|
if not check_auth():
|
|
return jsonrpc_wrap({"status": "error", "message": "Invalid credentials"}), 401
|
|
patient = {
|
|
"id": patient_id,
|
|
"name": f"Patient {patient_id}",
|
|
"email": f"p{patient_id}@test.com",
|
|
"phone": "555-0100",
|
|
"mobile": "555-0101",
|
|
"date_of_birth": "1988-12-11",
|
|
"photo_consent_status": "granted",
|
|
"membership_name": "Gold",
|
|
"loyalty_points": 450,
|
|
"receivable": 1250.0,
|
|
"payable": 0.0,
|
|
"dms_directory_id": None,
|
|
"dms_subdirectories": [],
|
|
}
|
|
return jsonrpc_wrap({"status": "ok", "patient": patient})
|
|
|
|
|
|
@app.route("/synq/desktop/v1/authenticate", methods=["POST"])
|
|
def authenticate():
|
|
if not check_auth():
|
|
return jsonrpc_wrap({"status": "error", "message": "Invalid credentials"}), 401
|
|
return jsonrpc_wrap({
|
|
"status": "ok",
|
|
"user": {"id": 2, "name": "Administrator", "login": "admin", "tz": "America/Los_Angeles"},
|
|
"company": {"id": 1, "name": "QCC"},
|
|
})
|
|
|
|
|
|
@app.route("/jsonrpc", methods=["POST"])
|
|
def jsonrpc_handler():
|
|
"""Handle Odoo-style JSON-RPC probes and proxy calls."""
|
|
data = request.get_json(force=True, silent=True) or {}
|
|
method = data.get("method", "")
|
|
params = data.get("params", {})
|
|
service = params.get("service", "")
|
|
method_name = params.get("method", "")
|
|
|
|
# Handle common.version probe used by check_odoo_connection
|
|
if service == "common" and method_name == "version":
|
|
return jsonify({
|
|
"jsonrpc": "2.0",
|
|
"id": data.get("id", 1),
|
|
"result": {
|
|
"server_version": "18.0",
|
|
"server_version_info": [18, 0, 0, "final", 0, ""],
|
|
"server_serie": "18.0",
|
|
"protocol_version": 1,
|
|
}
|
|
})
|
|
|
|
# Handle object.execute_kw for common read/search operations
|
|
if service == "object" and method_name == "execute_kw":
|
|
args = params.get("args", [])
|
|
if len(args) >= 3:
|
|
model = args[0]
|
|
op = args[3] if len(args) > 3 else "search_read"
|
|
if model == "res.partner" and op == "search_read":
|
|
return jsonify({
|
|
"jsonrpc": "2.0",
|
|
"id": data.get("id", 1),
|
|
"result": [
|
|
{"id": 1, "name": "Smith, John", "email": "john.smith@test.com", "phone": "555-0101"},
|
|
{"id": 2, "name": "Doe, Jane", "email": "jane.doe@test.com", "phone": "555-0201"},
|
|
]
|
|
})
|
|
if model == "medical.patient" and op == "search_read":
|
|
return jsonify({
|
|
"jsonrpc": "2.0",
|
|
"id": data.get("id", 1),
|
|
"result": [
|
|
{"id": 101, "name": "Smith, John", "date_of_birth": "1988-12-11", "sex": "Male"},
|
|
{"id": 102, "name": "Doe, Jane", "date_of_birth": "1990-05-22", "sex": "Female"},
|
|
]
|
|
})
|
|
return jsonify({"jsonrpc": "2.0", "id": data.get("id", 1), "result": []})
|
|
|
|
# Default fallback
|
|
return jsonify({
|
|
"jsonrpc": "2.0",
|
|
"id": data.get("id", 1),
|
|
"result": {"status": "ok", "message": "Mock bridge JSON-RPC"}
|
|
})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=8019)
|