Bank reconciliation integration¶
Accountify owns the review workflow; Odoo owns the accounting mutation. The recommended design uses a Studio automated-action webhook and server action to validate and execute one native Odoo reconciliation transaction.
Supported boundary¶
| Component | Models | Operation |
|---|---|---|
| Queue | account.bank.statement.line |
Read unreconciled bank/card transactions |
| Candidate engine | account.move.line |
Read posted open residuals on reconcilable accounts |
| Configuration | account.journal, account.account |
Read suspense account and reconciliation capability |
| Mutation wrapper | base.automation, ir.actions.server |
Validate request and invoke native reconciliation |
| Verification | account.partial.reconcile, account.full.reconcile |
Read native result and residual state |
Safe sequence¶
- Read unreconciled statement lines with explicit company, amount, currency, reference, partner, state, and
write_datefields. - Read candidate move lines that are posted, unreconciled, on a reconcilable account, same-company, and have non-zero residuals.
- Rank suggestions in Accountify and explain amount, partner, reference, date, currency, journal, ecommerce, and historical signals.
- Require user approval; acquire a tenant/statement-line lock and a durable idempotency key.
- Send a signed request to the tenant's versioned Studio webhook.
- Revalidate all state in Odoo immediately before mutation.
- Invoke the native reconciliation method in the wrapper's transaction.
- Read back statement, move-line residual, matching, and full/partial reconciliation state before marking success.
Webhook request contract¶
{
"request_id": "rec_01J...",
"idempotency_key": "tenant:statement:selection:version",
"secret": "redacted",
"statement_line_id": 123,
"selected_move_line_ids": [456, 789],
"action": "exact_match",
"allow_partial": false,
"expected_statement_write_date": "2026-06-29 09:10:11",
"expected_selected_write_dates": {
"456": "2026-06-29 08:00:00",
"789": "2026-06-29 08:05:00"
},
"client_context": {
"tenant_id": "tenant_123",
"actor_id": "staff_456"
}
}
| Field | Required | Validation |
|---|---|---|
request_id |
Yes | Unique audit/correlation identifier |
idempotency_key |
Yes | Same semantic request must return/recover the same outcome |
secret |
Yes | Compare in Odoo; never expose it to the browser or logs |
statement_line_id |
Yes | Exists, same company, still unreconciled, expected write_date |
selected_move_line_ids |
Yes | Non-empty, posted, open, reconcilable, same company/account path |
action |
Yes | One of the approved modes below |
allow_partial |
Yes | Must be explicit; never infer partial approval |
| expected write dates | Yes | Reject stale statement/candidate state |
client_context |
Yes | Audit context only; never trust it for authorization |
Actions¶
| Action | Meaning | MVP |
|---|---|---|
exact_match |
Selected residuals exactly offset the statement suspense amount | Yes |
partial_match |
Approved residual remains open | Yes, explicit only |
write_off |
Approved difference posted to a selected policy account | Later |
bank_fee |
Explicit bank-fee adjustment then reconciliation | Later |
transfer |
Same-entity bank/card transfer handling | Later |
mark_to_check |
No accounting mutation; route to manual review | Yes |
Every later action requires a passed sandbox-feasibility clone test before it is committed to a release: proof that its complete safe_eval call surface executes inside the wrapper. The sandbox permits public methods and mapped field writes only; underscore-prefixed methods are blocked. Feasibility evidence is valid per Odoo version and is re-validated by the scheduled drift check, because Odoo Online upgrades on Odoo's schedule.
Odoo-side validation¶
The wrapper must reject the request before mutation when any of these checks fail:
- Secret, request shape, action, or idempotency validation.
- Statement or candidate does not exist, changed since approval, or is already reconciled.
- Records belong to different companies or violate allowed-company context.
- Parent journal entries are not posted.
- Candidate accounts are not reconcilable or do not form a valid native reconciliation set.
- No unique statement suspense line can be identified.
- Residual/currency amounts do not support the approved exact or partial action.
- Accounting/tax lock date prevents mutation.
- Adjustment action lacks an approved account, journal, explanation, or policy.
Response and readback¶
A webhook HTTP response is not proof of accounting success. The backend should classify the immediate result, then re-read:
- statement line
is_reconciled,move_id, andwrite_date; - selected move-line
reconciled, residuals,matching_number, full/partial reconcile links, andwrite_date; - created adjustment move/lines when applicable;
- native partial/full reconciliation records.
Use operational states: suggested, queued, locked, sent_to_odoo, verified_success, validation_failed, concurrency_conflict, repair_required, and manual_review.
Error taxonomy¶
| Status | Meaning | Required action |
|---|---|---|
validation_failed |
Contract or current Odoo state invalid | Show exact validation; allow correction |
concurrency_conflict |
write_date, residual, or reconcile state changed |
Reload and rescore; do not retry stale payload |
webhook_error |
Wrapper rejected or crashed before confirmed result | Record sanitized error; investigate/version check |
odoo_accounting_error |
Native reconciliation/lock/tax/account rule rejected | Manual accounting review |
timeout_unknown |
Caller does not know whether Odoo committed | Read back by idempotency and record state before retry |
repair_required |
Odoo and Accountify states diverged | Disable action for record/tenant and open repair task |
manual_review |
Valid case outside automated policy | Route with context and suggested next step |
The backend must be able to assign these statuses without human inspection. The Studio webhook response is not guaranteed to carry a structured result, so while the webhook transport is in use the wrapper must expose rejection evidence the backend can read back — for example a rejection code and operation ID written to a Studio field — whenever the HTTP response alone cannot distinguish validation_failed from webhook_error.
Audit record¶
Record tenant and Odoo database, user/role, statement/journal/amount/date/reference/partner, candidate IDs and before residuals, approved action, idempotency key, wrapper version/hash, webhook identifier, timestamps/duration, sanitized error, readback state, and final status. Do not log API keys, webhook secrets, raw bank payloads, or unnecessary personal data.
Bootstrap and drift control¶
- Discover required models and fields using the current runtime exporter.
- Create or update the automation and server action with a reviewed code version.
- Store webhook URL/UUID, wrapper version, and code hash in Accountify secrets/configuration.
- Execute a no-op health payload and verify the expected response.
- Enable the Reconcile control only after capability and permission checks pass.
- Recheck version, hash, URL, model fields, and ACLs on a schedule and after Odoo changes.
All bootstrap, read, and readback calls must use the JSON-2 API. The legacy /xmlrpc, /xmlrpc/2, and /jsonrpc endpoints are scheduled for removal in Odoo Online 21.1 (winter 2027) and Odoo 22 (fall 2028); client databases upgrade on Odoo's schedule, so no new tooling may depend on them. Bootstrap and operating credentials are separate: the operating identity used at runtime must not be able to write ir.actions.server code.
Open verification items¶
The June 26, 2026 smoke test proved wrapper deployability — automation and server-action create/update/read, webhook URL and UUID generation, and cleanup — over legacy JSON-RPC. It did not execute a reconciliation. The following must pass in a representative clone before production enablement:
- Transaction semantics. A payload that validates, mutates, then deliberately raises leaves no committed change; two concurrent deliveries for one statement line produce exactly one reconciliation.
- Sandbox surface.
account.move.line.reconcile()and every required field write execute inside the Execute Code action without a sandbox violation. - Least-privilege identity. The wrapper operates under a restricted integration user; the operating credential cannot write server-action code.
- Transport decision. Evaluate invoking the same versioned server action through JSON-2
ir.actions.server.run()with the payload in context (standard API-key authentication, Odoo access rights, structured error responses) against the webhook trigger; adopt one and record the decision. - JSON-2 bootstrap. The full deployment smoke suite passes over
/json/2endpoints.
Create-type operations (adjustment moves for later actions) additionally require a proven duplicate-suppression pattern before enablement. Candidate techniques — anchor-row serialization and a uniqueness receipt on a core unique-constrained model — are clone-gated and not yet approved.
Test matrix¶
At minimum cover exact, partial, multi-item, over/underpayment, fee/write-off, transfer, refund/chargeback, duplicate import, stale write dates, double-click/retry, wrong company, draft entry, non-reconcilable account, lock date, multi-currency/exchange difference, missing suspense configuration, timeout-after-commit, and wrapper drift. Assert both final Odoo state and Accountify task state.
Design basis¶
This guide is the sanitized, version-controlled implementation contract derived from the restricted Odoo Online Reconciliation Wrapper - Studio Webhook Technical Design dated June 26, 2026. The Drive source folder remains the record for the full design narrative.