Skip to content

[18.0] pos_pwa, pos_offline: PWA infrastructure and offline mode for POS#1504

Draft
mileo wants to merge 14 commits intoOCA:18.0from
kmee:18.0-pos-pwa-offline
Draft

[18.0] pos_pwa, pos_offline: PWA infrastructure and offline mode for POS#1504
mileo wants to merge 14 commits intoOCA:18.0from
kmee:18.0-pos-pwa-offline

Conversation

@mileo
Copy link
Copy Markdown
Member

@mileo mileo commented Mar 22, 2026

Two new modules that enable the Odoo 18.0 Point of Sale to operate fully offline:

pos_pwa — PWA Infrastructure

  • Service Worker with scope /pos (separate from Odoo's /odoo SW)
  • Cache strategies: stale-while-revalidate for assets, cache-first for fonts/images, network-first with 4s timeout for POS shell, network-only for RPC
  • Web manifest for "Add to Home Screen" / standalone app install
  • Static offline fallback page
  • FIFO cache eviction (max 300 entries)

pos_offline — Full Offline Capability (depends on pos_pwa)

  • Offline startup: caches load_data response in IndexedDB, falls back to cache on ConnectionLostError with automatic IndexedDB readiness wait
  • Pending order persistence: order IDs persisted to IndexedDB, survive tab/browser closure
  • Auto-sync on reconnection: online event listener + 10s periodic check reset the offline flag and trigger syncAllOrders
  • Exponential backoff retry: 1s → 2s → 4s → ... → 60s max
  • Offline payments: terminal-based methods hidden, cash/manual remain; offline finalization skips server sync and goes directly to receipt
  • Barcode fallback: fetchNomenclature errors caught gracefully (scanning disabled, POS still loads)
  • Backend resilience: auto rescue session creation for orders arriving after session close; idempotent sync_from_ui with UUID dedup
  • Configurable: offline_enabled field on pos.config (enabled by default)

How to test

  1. Install pos_pwa + pos_offline
  2. Ensure DB_FILTER is set (e.g. DB_FILTER=^mydb$) — required for auth='public' routes
  3. Open POS online → confirm [POS Offline] Cached load_data for config X in console
  4. DevTools → Network → check "Offline"
  5. Reload → POS loads from IndexedDB cache
  6. Create order → pay with cash → receipt screen works
  7. Uncheck "Offline" → orders sync automatically within ~10s
  8. Verify synced orders in backend

Technical notes

  • Service Worker is vanilla JS (no Workbox), ~250 lines
  • All JS patches use patch() from @web/core/utils/patch for compatibility
  • IndexedDB version is base + 1 offset (not hardcoded) to avoid conflicts
  • Custom IndexedDB stores (_pos_load_data_cache, _pending_orders) are stripped from readAll() before loadData to prevent ORM model processing crashes
  • loadInitialData calls orm.call directly instead of super to intercept network errors before the core's blocking window.alert

mileo added 14 commits March 19, 2026 12:23
Add Service Worker with scope /pos (separate from Odoo's /odoo SW),
web manifest, and offline fallback page. Extends the POS index template
to register the SW and inject PWA meta tags.

Cache strategies:
- Static assets: stale-while-revalidate
- Fonts/images: cache-first
- POS HTML shell: network-first with 4s timeout
- RPC calls: network-only (managed by PosData)
Depends on pos_pwa. Adds:

- Startup offline: patches PosData.loadInitialData() to cache load_data
  response in IndexedDB and fall back to cache on ConnectionLostError
- IndexedDB stores: overrides initIndexedDB() to add _pos_load_data_cache
  and _pending_orders stores (DB version bump to 2)
- Robust sync: patches PosStore to persist pending orders to IndexedDB,
  restore on startup, and retry with exponential backoff (1s→60s max)
- Offline payment: patches PaymentScreen to hide terminal-based methods
  when offline and skip server sync during finalization
- Backend: idempotent sync_from_ui (UUID dedup), auto rescue session
  creation for orders arriving after session close
- Config: offline_enabled field on pos.config
pos_pwa:
- Fix manifest.webmanifest returning 404 by using make_response
  with json.dumps instead of make_json_response
- Replace deprecated apple-mobile-web-app-capable with
  mobile-web-app-capable meta tag

pos_offline:
- Fix TypeError "Cannot read properties of undefined (reading 'id')"
  on POS load. The custom IndexedDB stores (_pos_load_data_cache,
  _pending_orders) were being included in readAll() results and
  passed to missingRecursive/loadData which tried to process them
  as ORM models. Now loadIndexedDBData is overridden to strip
  custom stores before processing.
The /pos/manifest.webmanifest route returns 404 despite the controller
being loaded (service-worker.js works). This may be caused by Odoo's
routing treating .webmanifest as a static file extension.

Add alternative route /pos/pwa_manifest without file extension and
use it in the manifest link tag. Keep the original route as fallback.
pos_pwa:
- Serve offline page as static HTML file instead of QWeb template
  (server may be unreachable when SW needs to serve it)
- Add /pos/pwa_manifest route alongside /pos/manifest.webmanifest
- Remove QWeb pos_offline_page template (replaced by static HTML)

pos_offline:
- Rewrite loadInitialData to call orm.call directly instead of super,
  intercepting network errors BEFORE the core's blocking window.alert.
  On ConnectionLostError or network failure, falls back to IndexedDB
  cache immediately.
- Add _isNetworkError helper for non-ConnectionLostError failures
  (ERR_INTERNET_DISCONNECTED, Failed to fetch, etc.)
- Patch BarcodeParser.fetchNomenclature to catch network errors and
  return empty nomenclature instead of blocking POS startup
…ocessServerData)

- data_service_patch: call orm.call directly instead of super to
  intercept network errors before the core's blocking window.alert.
  Add _waitForIndexedDB to handle race condition where IndexedDB
  is not yet ready when loadInitialData runs offline.
  Add _isNetworkError for ERR_INTERNET_DISCONNECTED etc.
- pos_store_patch: override afterProcessServerData to skip
  readDataFromServer when offline, go directly to markReady/showScreen.
- Add barcode_reader_patch to manifest assets list.
pos_pwa:
- Add /web/image to SW cache-first strategy so category images,
  user avatars, and product images are cached on first online visit

pos_offline:
- Override allowProductCreation to return false when offline instead
  of making an RPC call that throws ConnectionLostError
- Fix "Response body is already used" error in cacheFirst strategy
  by cloning response before passing to cache.put
- Add /web/image URL pattern to cache-first strategy so POS category
  images and user avatars are cached for offline use
The /pos/pwa_manifest route returns 404 despite being registered in
the controller (werkzeug routing issue with underscore URLs).
The /pos/manifest.webmanifest route works correctly, so use that.
Using t-att-href to bypass OCA XML link validation.
data_service_patch (#2):
- Replace super.loadInitialData() fallback with inline alert + return.
  The old approach re-called the RPC (which would fail again) and could
  show duplicate error alerts.

pos_store_patch (#3):
- afterProcessServerData now runs local-only logic when offline:
  adds paid unsynced orders to pending set, marks residual orders
  as cancelled. Previously it skipped all of this.

pos_store_patch (#4):
- Fix exponential backoff retry being effectively dead. The core's
  syncAllOrders catches ConnectionLostError internally without
  re-throwing, so our catch block never triggered. Now we check if
  pendingOrders still exist after sync returns and schedule retry
  if so.
#1  data_service_patch: await _cacheLoadData to ensure persistence
#5  payment_screen_patch: re-check offline status on addNewPaymentLine
    for reactive filtering (not just onMounted)
#6  Remove dead offline_banner.xml template (no component uses it)
#7  data_service_patch: check config.offline_enabled before caching
#8  pos_order: simplify _get_valid_session, remove redundant super call
#9  data_service_options_patch: use base version + offset instead of
    hardcoded DB version 2, call super.initIndexedDB() first
#10 pos_sw: add MAX_CACHE_ENTRIES (300) with FIFO eviction in cacheFirst
Replace dynamic import() of @web/core/l10n/dates with static import.
Dynamic imports fail offline because the module resolver tries to
fetch from the server. Static imports are bundled in the asset.
- Add online event listener in PosStore to reset offline flag and
  trigger syncAllOrders when network comes back
- Periodic sync check (reduced from 30s to 10s) now also resets
  the offline flag when navigator.onLine is true, fixing the case
  where the core's online listener doesn't fire (e.g. DevTools
  network throttle)
- This ensures orders created offline are synced automatically
  when connectivity is restored, without requiring a page reload
Add readme/ directories with DESCRIPTION.md, CONTRIBUTORS.md,
CONFIGURE.md, and ROADMAP.md for both modules following OCA
maintainer-tools template.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant