============================================================
Integration plan for connecting the GP Booking App (Django) with ERPNext (Frappe v15) for invoicing, payments, and insurance claims.
Main page: Accounting App
Users & Roles: /guides/client-admin-guide/users-and-roles
sector_client_{subdomain}). Owns full invoicing, payment, insurance lifecycle.erpnext_web, erpnext_worker, erpnext_scheduler). Database on client VPS via SSH tunnel + SoCat. Custom app: healthcare_accounting.| Decision | Choice |
|---|---|
| Site topology | Per-practice ERPNext site ({practice}.accounts.gp.veripath.co.uk) |
| Invoice ownership | ERPNext is source of truth; GP Booking App stores reference copy |
| Fallback strategy | Async Celery retry with exponential backoff |
| PDF generation | ERPNext generates PDFs, GP Booking App fetches and caches |
| Insurance claims | Two Sales Invoices per claim (insurer + patient) |
| Mapping | PatientCustomerMapping links GP patient IDs to ERPNext Customers |
ERPNext — healthcare_accounting app
| Endpoint | Status |
|---|---|
get_patient_customer(patient_id) |
Tested |
create_patient_customer(...) |
Tested — creates Customer + PatientCustomerMapping |
create_sales_invoice(...) |
Tested — creates + submits Sales Invoice |
get_sales_invoice_status(invoice_name) |
Tested |
create_payment_entry(...) |
Tested — creates Payment Entry, marks invoice Paid |
GP Booking App — Django
| Component | File |
|---|---|
| ERPNextConfig model | integrations/models.py |
| erpnext_client.py | integrations/erpnext_client.py |
| Invoice migration | appointments/migrations/0029_* |
| PaymentTransaction migration | Same migration |
| Celery tasks | integrations/tasks.py |
| Admin registration | integrations/admin.py |
ERPNext v15 has known PostgreSQL compatibility issues. These are handled at runtime by the gunicorn_app.py wrapper (/opt/erpnext/gunicorn_app.py, repo patches/gunicorn_app.py) — the ERPNext source files are not edited:
| Query construct | Fix applied |
|---|---|
GROUP BY missing non-aggregated columns (incl. CTEs) |
Non-grouped bare columns wrapped in MAX(col) AS col (preserves MySQL grouping semantics) |
HAVING referencing a CTE output alias |
Alias resolved to source expression; HAVING → WHERE for ungrouped non-aggregate queries |
IF(numeric, a, b) |
Boolean CASE WHEN (cond IS NOT NULL AND cond <> 0) THEN … |
MySQL "literal" strings |
Converted to 'literal' (backtick-quoted raw SQL only) |
These patches are required for Sales Invoice submission and Payment Entry flows on PostgreSQL, not just report queries. Details: /development/projects/accounting/deployment
| Item | Status |
|---|---|
| Company "Test Client" | Created with UK Standard Chart of Accounts |
| Default income account | Service - TC |
| Default receivable account | Debtors - TC |
| Default bank account | Business Current Account - TC |
| Default cash account | Cash - TC |
| Mode of Payment records | Cash, Bank, Cheque, Card |
| Dev API key/secret | Set on Administrator user |
ERPNext Payment Entry API works. Stripe webhook calls sync_payment_to_erpnext on payment success.
Two-invoice ERPNext flow per claim: insurer invoice + patient shortfall invoice.
GP Booking App changes:
| Component | Detail |
|---|---|
| PrivateInsuranceInvoice model | Added erpnext_insurer_invoice, erpnext_patient_invoice, erpnext_synced, erpnext_last_sync, erpnext_sync_error |
| Migration | insurance_billing/migrations/0003_* |
| sync_insurance_claim_to_erpnext task | Creates 2 Sales Invoices (insurer + patient), stores references |
| ShortfallUpdateView | Triggers sync on insurer payout recording |
ERPNext endpoints:
| Endpoint | Purpose |
|---|---|
| get_insurer_customer(insurer_name) | Look up insurer Customer |
| create_insurer_customer(insurer_name) | Create insurer as Customer (type=Company, group=Commercial) |
Seamless Keycloak SSO from GP Booking App invoice links to ERPNext.
Key changes:
| Component | Detail |
|---|---|
ERPNextConfig.keycloak_realm |
New field — per-practice Keycloak realm for SSO (migration 0011) |
Invoice._erpnext_sso_url() |
Uses config.keycloak_realm instead of hardcoded veripath |
dashboards/context_processors._erpnext_sso_url() |
Accepts realm parameter, reads from config |
healthcare_accounting.api.login.login_via_keycloak |
Added JSON decoder (rauth 0.7.3 default parse_utf8_qsl incompatible with Keycloak) |
ERPNext Social Login Key — base_url set per site to the practice's Keycloak realm (e.g. https://auth.veripath.co.uk/realms/test-client).
Keycloak — erpnext client created per partner realm with:
role protocol mapper (reads user role attribute → OIDC role claim)RECEPTIONIST, ACCOUNTANT, ACCOUNTS_USER, REGISTERED_MANAGER| Trigger | Task | File:Line |
|---|---|---|
| Invoice created (manual) | sync_invoice_to_erpnext | appointments/views.py:1042 |
| Insurance shortfall invoice | sync_invoice_to_erpnext | insurance_billing/views.py:243 |
| Stripe payment succeeded | sync_payment_to_erpnext | appointments/payment.py:84 |
| Insurance shortfall paid | sync_payment_to_erpnext | insurance_billing/views.py:328 |
| Insurer payout recorded | sync_insurance_claim_to_erpnext | insurance_billing/views.py:180 |
All tasks use Celery with exponential backoff retry (60s 120s 240s, max 3).
Appointment types are defined as Items in ERPNext (item group Services) and synced into the GP Booking App as AppointmentType records. ERPNext is the source of truth for pricing (standard_rate → price); the GP Booking App holds scheduling-only fields (duration, delivery method, buffer time, room requirements, staff permissions, online booking toggles).
# Pull Items from ERPNext into GP Booking App (default)
python manage.py sync_appointment_types_to_erpnext
# Push local AppointmentTypes to ERPNext as Items
python manage.py sync_appointment_types_to_erpnext --push
The command iterates all practices with an ERPNextConfig, fetches Items in the Services group from ERPNext, and creates/updates corresponding AppointmentType records keyed by erpnext_item_code. The --push flag does the reverse — creates/updates ERPNext Items from local AppointmentType data.
{practice}.accounts.gp.veripath.co.uk → Accounting → Items → New Item. Set item group to Services and enter the price as standard_rate.sync_appointment_types_to_erpnext — the Item is matched by item_code and an AppointmentType is created/updated with the ERPNext price.AppointmentType detail view in the GP Booking App to set duration, delivery method, buffer time, etc.Practice Manager users are redirected to ERPNext to create/edit appointment types. The GP Booking App detail view is read-only (scheduling fields only).
Minimal check that ERPNext API is alive and basic accounting works.
Step 1.1: Ping
curl -X GET 'https://test-client.accounts.gp.veripath.co.uk/api/method/frappe.ping' \
-H 'Authorization: token dev-gp-booking-api-key:dev-gp-booking-api-secret-2026'
→ {"message":"pong"}
Step 1.2: Create a patient customer
curl -X POST 'https://test-client.accounts.gp.veripath.co.uk/api/method/healthcare_accounting.api.create_patient_customer' \
-H 'Authorization: token dev-gp-booking-api-key:dev-gp-booking-api-secret-2026' \
-H 'Content-Type: application/json' \
-d '{"patient_id":"1","patient_name":"Alice Test","patient_email":"alice@test.com"}'
→ Creates ERPNext Customer + PatientCustomerMapping
Step 1.3: Create a Sales Invoice
curl -X POST 'https://test-client.accounts.gp.veripath.co.uk/api/method/healthcare_accounting.api.create_sales_invoice' \
-H 'Authorization: token dev-gp-booking-api-key:dev-gp-booking-api-secret-2026' \
-H 'Content-Type: application/json' \
-d '{
"customer": "Alice Test - GPB-1",
"company": "Test Client",
"items": [{"item_name": "GP Consultation", "qty": 1, "rate": 75.00, "amount": 75.00}]
}'
→ Returns invoice name, status Unpaid, grand_total 75.00
Step 1.4: Record a payment
curl -X POST 'https://test-client.accounts.gp.veripath.co.uk/api/method/healthcare_accounting.api.create_payment_entry' \
-H 'Authorization: token dev-gp-booking-api-key:dev-gp-booking-api-secret-2026' \
-H 'Content-Type: application/json' \
-d '{"invoice_name":"ACC-SINV-2026-XXXXX","amount":75.00,"payment_type":"Cash","company":"Test Client"}'
→ Payment Entry created, invoice status becomes Paid
Step 1.5: Verify in ERPNext UI
Triggers the Celery integration between apps.
Step 2.1: Staff creates invoice manually
sync_invoice_to_erpnext firesStep 2.2: Verify sync
docker logs gp_booking_app_celery (look for "synced to ERPNext")ACC-SINV-2026-XXXXXStep 2.3: Edge cases to test
Full end-to-end: patient pays → Stripe webhook → ERPNext Payment Entry.
Step 3.1: Create invoice in GP Booking App
(As per Level 2)
Step 3.2: Patient pays online
/appointments/<id>/pay-online//appointments/<id>/create-payment-intent/payment_intent.succeeded webhook to /webhooks/stripe/Step 3.3: Verify ERPNext sync
sync_payment_to_erpnext firesStep 3.4: Verify GP Booking App state
The most complex scenario — demonstrates dual-invoice split.
Prerequisites:
Step 4.1: Export XML
Step 4.2: Record insurer payout
sync_insurance_claim_to_erpnext firesStep 4.3: Verify both ERPNext invoices
ERPNext Sales Invoice #1: Customer=Bupa, amount=100.00 (Unpaid)
ERPNext Sales Invoice #2: Customer=Patient (GPB-ID), amount=50.00 (Unpaid)
Step 4.4: Collect shortfall
sync_payment_to_erpnext fires → Payment Entry against patient invoiceStep 4.5: Final state
ERPNext:
Sales Invoice #1 (Bupa): Unpaid, 100.00 outstanding (paid outside ERPNext)
Sales Invoice #2 (Patient): Paid, 0.00 outstanding
Payment Entry: 50.00 allocated to Patient invoice
GP Booking App:
PrivateInsuranceInvoice: status=PAID, shortfall_status=COLLECTED
Local Invoice: status=PAID
PaymentTransaction: completed
Combines all previous levels in a single patient journey.
{practice}.accounts.gp.veripath.co.ukThe ERPNext installation has been stripped of irrelevant modules to create a focused healthcare accounting experience. Only accounting/finance/healthcare features remain visible.
| Module | Reason |
|---|---|
| Manufacturing | Not relevant for clinics |
| Subcontracting | Not relevant |
| Telephony | Not an accounting feature |
| Quality Management | Quality processes |
| Support | Support ticketing |
| Projects | Project management |
| EDI | Electronic Data Interchange |
| Bulk Transaction | Not needed |
| Maintenance | Equipment maintenance scheduling |
| Social | Social media integration |
| CRM | Patient management handled by GP Booking App |
| Selling | Redundant with invoicing |
| Website | Company website/blog management |
| Stock | Clinics buy supplies as expenses, no inventory tracking |
| Module | Purpose |
|---|---|
| Accounts | Core accounting (invoicing, payments, GL, reports) |
| Healthcare Accounting | Custom healthcare-specific features |
| Buying | Purchase orders for clinic supplies |
| Assets | Equipment/fixed asset tracking |
| Regional | UK tax/VAT rules |
| Workflow | Approval processes (removed from UI but module active) |
| Integrations | Stripe/payment webhooks |
| Sending invoices/statements |
| Visible | Hidden |
|---|---|
| Healthcare Accounting, Accounting, Stock, Buying, Assets, Users, ERPNext Settings, Home | Manufacturing, CRM, Selling, Website, Support, Projects, Quality, Integrations, Tools, Build, ERPNext Integrations |
GP Booking App and its partner orgs authenticate users via Keycloak. Each partner org (e.g., test-client) has its own Keycloak realm. ERPNext SSO uses the same realm as the user's GP Booking App login, so the existing session is reused seamlessly.
| Component | Details |
|---|---|
| Keycloak instance | auth.veripath.co.uk |
| Realm (internal) | veripath — used by non-partner users |
| Realm (per partner org) | e.g. test-client — used by partner org users |
| GP Booking App client | gp-booking-app (created per realm) |
| ERPNext client | erpnext (created per realm) |
| Domain | Both apps under .gp.veripath.co.uk (shared parent domain) |
Key point: Each partner realm has its own erpnext OIDC client with the same configuration (protocol mapper, roles). The ERPNextConfig.keycloak_realm on each practice determines which realm the SSO URL targets.
User logs into GP Booking App via Keycloak
(if partner org → goes to partner realm, e.g. test-client)
│
▼
User clicks ERPNext link (invoice "Open" button)
│
▼
GP Booking App generates SSO URL targeting the practice's Keycloak realm
(e.g. auth.veripath.co.uk/realms/test-client/... for partner orgs)
(e.g. auth.veripath.co.uk/realms/veripath/... for internal users)
│
▼
Keycloak sees existing session in that realm → immediately redirects back
│
▼
ERPNext receives OIDC token with user's role claim
│
▼
ERPNext maps role to appropriate permissions
│
▼
User is logged into ERPNext without re-entering credentials
│
▼
Browser lands on the specific Sales Invoice the user clicked
No login prompt is shown — the user already has a Keycloak session from their GP Booking App login, and the SSO URL targets the same realm.
See the Users & Roles guide for a full description of accounting roles and permissions.
When a user authenticates via Keycloak SSO, ERPNext reads the role claim from the OIDC userinfo response (injected by the erpnext client's protocol mapper, which reads the user's role attribute in Keycloak). ERPNext then applies the corresponding role mapping:
Keycloak role |
ERPNext Role | Access Level |
|---|---|---|
ACCOUNTANT |
Accounts Manager | Full accounting access (invoices, payments, journal entries, reports, settings) |
ACCOUNTS_USER |
Accounts User | Limited invoicing access (view/create invoices, payments, basic reports) |
RECEPTIONIST |
Accounts User | Basic invoice viewing and payment recording |
REGISTERED_MANAGER |
Accounts Manager | Full accounting access |
Default fallback: Users without a mapped role receive the Accounts User role, giving them basic access.
| Role Code | Display Name | Description |
|---|---|---|
ACCOUNTANT |
Accountant | Full access to accounting and financial management |
ACCOUNTS_USER |
Accounts User | Limited access to invoicing and basic transactions |
These are added to the CustomUser.Roles enum and the FormalRole model. They route to /dashboard/accounts/ on login.
Each ERPNext site has its own Social Login Key document, configured with:
https://auth.veripath.co.uk/realms/test-client)erpnexterpnext client secret in that realmhttps://{practice}.accounts.gp.veripath.co.uk/api/method/healthcare_accounting.api.login.login_via_keycloakThe custom endpoint healthcare_accounting.api.login.login_via_keycloak processes the role claim and applies role mapping. It uses get_info_via_oauth("keycloak", code, decoder=_json_decoder) — a JSON decoder is required because rauth 0.7.3 defaults to parse_utf8_qsl which is incompatible with Keycloak's JSON token response.
role set (e.g. RECEPTIONIST)base_urlhttps://test-client.gp.veripath.co.uk/appointments/invoices/role attribute (e.g. RECEPTIONIST → Accounts User)| Test | Accounts Manager | Accounts User |
|---|---|---|
| View Sales Invoice list | ✅ | ✅ |
| Create Sales Invoice | ✅ | ✅ |
| Submit/Save Invoice | ✅ | ✅ |
| Create Payment Entry | ✅ | ✅ |
| View General Ledger | ✅ | ✅ |
| Modify Accounts Settings | ✅ | ❌ |
| Create Journal Entry | ✅ | ❌ |
| Delete/Cancel Invoice | ✅ | ❌ |
============================================================
Page: https://wiki.veripath.co.uk/development/projects/accounting/gp-booking-erpnext-integration