============================================================
Implementation plan for integrating the dental app with ERPNext accounting, mirroring the GP booking app's ERPNext integration. Status: Phase A (ERPNext infra) COMPLETE (2026-08-02); Phase B (Django port) COMPLETE (2026-08-18) — verified live on test-client-dental; Phase C verified; Phase D (docs & runbook) PENDING.
Decision (2026-08-02): Mirror the GP approach exactly — per-practice ERPNext sites, full ERPNext install with UI-hidden modules, the healthcare_accounting Frappe app, and the ported Django integration. Billing scope: DentalTransaction invoicing, Stripe → Payment Entry sync, FP17 two-invoice flow. B8 (PracticeSubscription seat invoicing) was REMOVED from the dental plan on 2026-08-18 — seat pricing is VeriPath↔client B2B and belongs in VeriPath's platform infrastructure, not the per-practice app (see Phase B).
test-client.accounts.gp.veripath.co.uk) is the full ERPNext app with non-accounting modules hidden in the UI (apps.txt = frappe + erpnext). There is no schema-level "Accounts only" install.erpnext_web/worker/scheduler, 1 GiB limit each).bench migrate / app upgrades safe.The dental site test-client.accounts.dental.veripath.co.uk is provisioned and the accounting flow verified (create customer → create + submit Sales Invoice → Payment Entry → invoice Paid, GL entries correct).
A1. Client VPS — ERPNext database
bench new-site created erpnext_test_client_dental + a site DB user on the dental client VPS via the postgres superuser on client_db (credentials stored outside the wiki), reached through the SSH tunnel 172.18.0.1:5436 → socat → client VPS:5432. ✅agents/handoff/from-hermes).A2. DNS — *.accounts.dental.veripath.co.uk wildcard already resolved. ✅
A3. Nginx + TLS — vhost → 127.0.0.1:8005 + cert already in place; curl -I → 200. ✅
A4. Provision ERPNext site
bench new-site test-client.accounts.dental.veripath.co.uk \
--db-type postgres --db-name erpnext_test_client_dental \
--db-host 172.18.0.1 --db-port 5436 \
--db-root-username postgres --db-root-password '<client-db-superuser-pw>' \
--admin-password '<generated>' \
--install-app erpnext --install-app healthcare_accounting
--install-app healthcare_accounting fails with "App not in apps.txt" unless healthcare_accounting is listed in the bench sites/apps.txt, and the app's patches.txt must contain a [post_model_sync] section (the [pre_model_sync]-only version crashes the install). Both are fixed in the image.site_config.json: append keycloak_realm: veripath, keycloak_client_secret, plus encryption_key / setup_complete: 1 (bench writes the rest). Restart the web container to load the healthcare_accounting patches. ✅A5. Keycloak
https://*.accounts.dental.veripath.co.uk/* and the explicit https://test-client.accounts.dental.veripath.co.uk/* to the erpnext client's redirectUris. Caveat (learned 2026-08-02): * wildcards do NOT match subdomains — the explicit per-host entry is what actually works.Social Login Key on the site (realm veripath, client erpnext, secret from Keycloak, redirect https://test-client.accounts.dental.veripath.co.uk/api/method/frappe.integrations.oauth2_logins.login_via_keycloak). ✅A6. Bootstrap company
gb_uk_healthcare_clinic).get_doc (not the setup wizard): must seed setup fixtures (install_fixtures('United Kingdom')), create Fiscal Years, Price Lists (install_defaults), and set the Global Defaults currency to GBP (a fresh site defaults to INR, which breaks invoicing with a currency-mismatch error). Set the company's default income/receivable/cash/bank/payable/expense accounts + cost centers.ERPNextConfig. ✅A7. Modules hidden — non-accounting modules blocked globally via the Administrator user's block_modules (cosmetic). ✅
A8. Verified — frappe.ping → 200; full API smoke test passes (customer → invoice → submit → payment → Paid) via the same calls the Django client will use. ✅
PostgreSQL compatibility patches: the accounting flow required fixes to gunicorn_app.py (GROUP BY MAX() wrap, HAVING→WHERE, IF(numeric), double-quoted literals). See /development/projects/accounting/deployment.
Found during Phase B verification; all fixed and verified live:
erpnext_test_client_dental (client VPS): = missed rows that LIKE found on tabDocType/tabDocField → frappe.get_meta failed → API 500s. Fixed via REINDEX DATABASE (all 699 tables) + clear-cache + web restart. (lead_name/income-account resolution failures were consequences of this.)create_insurer_customer → 417 — used a GROUP-type customer group (All Customer Groups). Fixed to Commercial in the healthcare_accounting source + running containers (/root/work/erpnext-deploy/healthcare_accounting/healthcare_accounting/api/__init__.py).
Commercial fix is now baked into the image (healthcare_accounting/.../api/__init__.py:96, commit 6224da6 on Forgejo erpnext-deploy), shipped to VPS3 (docker save|gzip|load), and the containers were recreated (docker compose up -d --force-recreate). Post-recreate verification: all 3 containers healthy, dental site frappe.ping → 200, create_insurer_customer succeeds (scratch + real NHS customer created with customer_group=Commercial, no 417), keycloak_realm/setup_complete config intact. Old image removed. Recreate is now safe.run_method in the request body (/api/resource/Sales Invoice/<name>/ with {"run_method": "submit"}), not the /submit URL suffix — worked around in ERPNextClient.submit_sales_invoice./root/work/dental_booking_app, branch develop) — COMPLETE 2026-08-18All code on develop, committed in 6 logical units. Verified live on test-client-dental (Phase C). One planned deviation: DentalERPNextConfig lives in the dental app, not integrations (see B1).
| Item | Status | Notes / commit |
|---|---|---|
| B1 ERPNextConfig | DONE (deviated) | DentalERPNextConfig in the dental app (table dental_dentalerpnextconfig), NOT integrations. Rationale: dental + GP share the default DB dsp_clinic, which already carries GP's integrations_erpnextconfig (FK→onboarding.Practice); an integrations-app model FK→DentalPractice would be a cross-DB relation (integrations→dsp_clinic, dental→veripath_dental/client DBs). 9c704812 |
| B2 Invoice + InvoiceLineItem | DONE | New dental/invoice_models.py (mirrors GP appointments/invoice_models.py, incl. _erpnext_sso_url); auto INV-YYYY-XXXXX, invoice_type incl. NHS_REIMBURSABLE / PATIENT_COPAY, FP17 link, erpnext sync fields. 86850fa5 |
| B3 erpnext_client.py | DONE | Ported verbatim from GP + practice_name fix (DentalPractice uses practice_name, not name) + submit action via run_method. da50d48a |
| B4 invoice_service.py | DONE | create_invoice_in_erpnext — synchronous create+submit+local Invoice; income account passed per-item (dental items carry no item_code). 73629d33 |
| B5 Celery tasks | DONE | sync_payment_to_erpnext, sync_fp17_claim_to_erpnext (both accept tenant_id + set_current_tenant so the worker routes to the right DB); exponential backoff 60/120/240s, max 3. 7c1f90a9 |
| B6 Hooks | DONE | dental/payment.py — after payment_intent.succeeded and after reconcile_claim_payment, try/except-wrapped enqueues; DentalTransaction erpnext fields + migration. 7c1f90a9 |
| B7 FP17 two-invoice | DONE | NHS reimbursable share → NHS insurer customer; patient co-payment → patient customer; both Invoice rows record erpnext_invoice_name. Verified live (both submitted). 7c1f90a9 |
| B8 Seats subscription | REMOVED 2026-08-18 | Decision (Matthew): seat pricing is the intended basis of VeriPath's own client pricing (amount TBD — not necessarily £125/seat/month) and is strictly between VeriPath and the client. Seat calculation/billing belongs in VeriPath's platform infrastructure (which bills clients), NOT the per-practice dental app. PracticeSubscription untouched; no beat entry, no task — do not implement. |
| B9 Keycloak SSO | DONE | Invoice.erpnext_url, dental/context_processors.py erpnext_site_url, role-gated invoice list page, nav link. cdf0e6a6 |
Provisioning (repeatable): practice 55 (Test Client Dental) configured — python manage.py provision_erpnext_config --tenant test-client-dental --practice 55 … (add --income-account/--receivable-account as needed). API key/secret piped via stdin, never printed.
ACC-SINV-2026-00021) → Payment Entry (ACC-PAY-00009) → invoice PAID.ACC-SINV-2026-00025 after restart).manage.py check clean (1 pre-existing unrelated warning); test data cleaned from ERPNext + local Invoice rows.dental_onboarding, on-boarding-new-clients, developer-plan handshake; document the bench new-site slowness + watcher technique.============================================================