Odoo Production Runbook¶
This page records production-affecting incidents on the Discover Fresh Odoo tenant and the resolutions applied. Read it before touching invoicing, journals, or the delivery-group/trip pipeline.
Production environment¶
| Field | Value |
|---|---|
| URL | https://packetclouds-vendease.odoo.com |
| Database | packetclouds-vendease-discoverfresh-29412875 |
| Tenancy | Odoo Online (SaaS) — modules deployed via Odoo Online's module-upload page, not file copy |
| Active branch | discoverfresh (PR base: main) |
| Runtime | Odoo 17.0 |
Custom module source lives in Odoo/ (the df_* cluster, plus the pct_*, dispatch_vehicle, account_move_remove_constraint, and reporting modules).
Resolved (2026-04-21) — BNPL receivable constraint on customer invoices¶
Symptom¶
sale.advance.payment.inv against new sale orders (e.g. SO33, SO34) raised the Odoo _check_payable_receivable constraint on the payment-term line. This blocked invoice creation for any customer whose payment term stretched maturity past invoice date — i.e. every BNPL invoice on the 15-day term.
Root cause¶
410000 Discover Fresh (BNPL) (account id 6) was being used as the partner receivable — every partner's property_account_receivable_id and the company-level ir.property default both pointed at it — but it was typed account_type = 'income' rather than asset_receivable.
The Odoo constraint _check_payable_receivable fires when a non-receivable account carries a date_maturity, which is exactly what the payment-term line on every BNPL invoice did.
Importantly, revenue itself was always correct: 119/119 product lines in the audit window credited 400000 Discover Fresh Sales. Only the receivable side was mistyped.
Why it had been working¶
Odoo only enforces _check_payable_receivable once date_maturity is non-null on a non-receivable line. The 15-day BNPL term was the first payment term in the database long enough to reliably set a maturity past invoice date and trip the constraint. Earlier "Pay Now" flows posted with maturity equal to invoice date and did not trip it.
Fix applied (idempotent — safe to re-run)¶
- Sale journal
PAYN("Customer Invoices Pay Now", id 1) haddefault_account_id = 410000. Odoo refuses to flip an account to receivable while it remains a sale-journal default. Repointed PAYN's default to400000(id 24) to match its sibling journalBNPL. This was a no-op historically because product lines have always overridden the journal default at line level. - Flipped account
410000fromaccount_type = 'income'→account_type = 'asset_receivable'.reconcilewas alreadyTrueand stayed so.
Trial-balance impact¶
The unpaid AR balance on 410000 moved from Income (P&L) to Receivables (Balance Sheet). Historical entries were already net-debit at invoice / net-credit at payment, so this reclassifies them consistently — no journal entries were rewritten.
Verification¶
sale.advance.payment.inv against SO33 succeeded post-fix. Draft invoice account.move id 304 was created for Beezus Kitchen with due 2026-05-06. Lines:
- product lines credit
400000 [income] - payment-term line debits
410000 [asset_receivable]
Follow-ups (open)¶
- Two duplicate Beezus Kitchen partner records (ids 148 and 149) still exist — not blocking anything, but should be deduped before more invoices accumulate against either.
- PAYN/BNPL journal naming is inverted-sounding. PAYN ("Pay Now") and BNPL now both default to revenue account
400000. The names are misleading; a rename for clarity would help. - SO34 had no invoice at the time of the fix. The constraint is now resolved; the team can create the invoice via the UI when ready.
Important: this incident does not indicate account_move_remove_constraint¶
Several diagnostics initially pointed at the account_move_remove_constraint module. It was unrelated. That module no-ops _check_duplicate_supplier_reference. It does NOT touch _check_payable_receivable or _check_date_maturity. Future investigations should rule out the account-typing root cause before suspecting this module.
Resolved (2026-04-23) — Trip drop "Delivery Fee Charged" stale at 4,000¶
Symptom¶
After the operations team updated zone delivery rates to cluster=₦7,000 and express=₦13,000, Trip Drops in df_logistics continued to display delivery_fee = 4,000 on completed trips. Customer-facing billing was always correct — S00029 was billed ₦13,000 express on INV/2026/00018 — but a parallel logistics pipeline was reporting stale figures.
Root cause¶
Three separate bugs in the delivery-fee propagation chain:
- Empty
zone_idon most groups.df.delivery.group.zone_idwas unset on 20 of 22 groups. With no zone,_compute_delivery_feefell back to defaults fromir.config_parameter(4,000 / 2,000 / 5,000) instead of the configured zone rates. _compute_delivery_typeignored the SO's express flag. The compute ondf.delivery.groupderived its delivery type from order count alone, so a single-order express SO was being classified "solo" in the group, picking up the solo rate instead of express.- Snapshot field on the trip drop.
df.logistics.trip.drop.delivery_feewas a plain stored Float, set once at dispatch time from the group's then-current fee and never recomputed. Subsequent zone-rate updates did not propagate.
Fix applied¶
Source-code changes (committed):
df_discover_fresh/models/delivery_group.py—_compute_delivery_typenow flips toexpresswhen any SO in the group hasx_df_delivery_type = 'express'. Added_backfill_zone_from_orders(), called from bothcreateandwrite, to populatezone_idfrom the first SO when blank.df_logistics/models/logistics_trip_drop.py—delivery_feeis now a stored computed field that mirrors the SO'sproduct_delivery_feeline, so zone changes propagate.df_logistics/wizard/dispatch_wizard_inherit.py— removed the stale manualdelivery_feeassignment on drop create (the new compute owns it).
Production data backfill (one-off, idempotent):
- 16 delivery groups had
zone_idbackfilled from their first SO. - 19 trip drops had
delivery_feerewritten to match their SO's fee line. - S00029 / TRIP-00015 drop: 4,000 → 13,000, now matches
INV/2026/00018.
Follow-ups (open)¶
_sync_delivery_fee_to_ordersstill picksdtypeby order count alone, not by each SO'sx_df_delivery_type. Harmless while solo and express zone rates are equal, but inconsistent with the new_compute_delivery_type. Worth aligning before zone rates diverge again.
Operational policy: data remediation scripts¶
Several XML-RPC remediation scripts exist locally (e.g. for the BNPL fix and the delivery-fee backfill above). Never commit any script that inlines admin credentials.
The standing list of files that must stay local:
- Working notes: any internal
*.mdfiles holding incident or credential detail;DETAILED_WALKTHROUGH.md. - Diagnostic scripts:
diag_*.py,debug_invoice_constraint.py. - One-off fix scripts:
fix_*.py,revert_*.py,create_*_account.py,create_*_journal.py.
These should be kept local for reproducibility but staged out of any commit. Only df_*/, pct_*/, dispatch_vehicle/, account_move_remove_constraint/, purchase_order_inherit/, and similar module source belongs in version control.
When writing a new XML-RPC script:
- Read credentials from
~/.config/odoo-tenant.jsonor environment variables, not literals. - Make every operation idempotent (re-running must be a no-op).
- Add it to the local
.gitignoreif its filename pattern isn't already covered.
Branch and deploy conventions¶
- Active feature branch:
discoverfresh. PR base:main. - The
discoverfreshhistory is a flow of operational fixes (delivery zone audit logs, delivery-fee propagation,oe_chatterwrapper on the delivery-zone form, SKU bulk-upload header handling, native-export header acceptance). Anything that changes one of those flows should reference the relevant commit. - Production deploy is manual via Odoo Online's module-upload page. There is no CI/CD pipeline pushing to packetclouds-vendease.odoo.com; the process is: build a
.zipof the changed module(s), upload through the Apps → Update Apps List → upload flow, then trigger a module update from the Apps menu. - Local development uses the
odoo-17.0checkout in the parent workspace as the runtime, with--addons-path=odoo-17.0/addons,Odoo. Run with--dev=xml,assetswhen iterating on dashboard widgets indf_discover_fresh/static/src/.
Known fragility around module-load ordering¶
df_discover_fresh/__manifest__.py carefully orders its data keys — security, then default data and cron jobs, then views (with actions ahead of menus that reference them), then wizards and reports. Preserve this ordering when adding new XML files. Odoo will fail to load a menu whose action has not yet been registered, with an unhelpful KeyError at install time.
The two reporting modules — comprehensive_stock_report and location_availability_report — declare auto_install = True. They will install automatically on any Odoo database where their dependencies are present. This is intentional today but worth being aware of when bringing up a fresh tenant.
What to escalate¶
If any of the following appear in production logs or operations channels, treat as P1:
_check_payable_receivableor_check_date_maturityconstraint errors during invoice posting — could indicate a regression of the BNPL fix above (account 410000 is the canary).- Trip drop
delivery_feemismatching the corresponding SO'sproduct_delivery_feeinvoice line by more than rounding error — could indicate the compute ondf.logistics.trip.drophas been re-broken. - Any
account.moveposting against410000from a journal other than a sale or bank journal — receivable accounts should never see a journal-entry credit from an unexpected source. - Module install or upgrade failure on packetclouds-vendease.odoo.com referencing a missing menu action — likely the module-load ordering rule above was violated.
Owner of this runbook: whoever currently has the Discover Fresh Operations Manager role in Odoo and the equivalent SaaS-tenant admin access. Update this page on every postmortem.