Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions pos_offline/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
===========
POS Offline
===========

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:732c78e96f912225bc1f778ec70b06625fbf8789566a16235bd5d78f3c272652
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpos-lightgray.png?logo=github
:target: https://github.com/OCA/pos/tree/18.0/pos_offline
:alt: OCA/pos
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/pos-18-0/pos-18-0-pos_offline
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/pos&target_branch=18.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

Full offline capability for the Odoo Point of Sale. Depends on
``pos_pwa``.

When the POS is opened online, all data is cached in IndexedDB. On
subsequent visits without internet, the POS loads entirely from cache
and allows creating orders, processing cash payments, and printing
receipts. Orders are automatically synced when connectivity is restored.

Key features:

- **Offline startup**: Caches ``load_data`` response in IndexedDB, falls
back to cache on network error with automatic IndexedDB readiness wait
- **Pending order persistence**: Pending order IDs are persisted to
IndexedDB so they survive tab/browser closure
- **Exponential backoff retry**: Failed syncs are retried with backoff
(1s to 60s), plus a 10s periodic safety-net check
- **Offline payments**: Payment terminal methods are hidden when
offline; cash and manual methods remain available
- **Barcode fallback**: Barcode nomenclature fetch errors are caught
gracefully (scanning disabled, POS still loads)
- **Rescue sessions**: Backend automatically creates rescue sessions for
orders arriving after the original session was closed
- **Idempotent sync**: Duplicate orders (by UUID) are detected and
skipped

**Table of contents**

.. contents::
:local:

Configuration
=============

1. Install the module (it depends on ``pos_pwa`` which will be
auto-installed)
2. In **Point of Sale > Configuration > Settings**, the "Offline Mode"
toggle is enabled by default
3. Open the POS at least once while online to populate the cache
4. The POS will now load and operate offline on subsequent visits

**Important**: The Odoo instance must have ``DB_FILTER`` configured
(e.g. ``^mydb$``) for the PWA manifest and Service Worker routes to work
correctly with ``auth='public'``.

Known issues / Roadmap
======================

- Delta sync on reconnection (only fetch changed records since last
cache)
- Multi-currency cash register support for offline sessions
- Cache barcode nomenclature in IndexedDB for offline scanning
- Offline indicator banner in the POS UI
- Block session close when pending orders exist

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/pos/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/pos/issues/new?body=module:%20pos_offline%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* KMEE

Contributors
------------

- `KMEE <https://kmee.com.br>`__:

- Luis Felipe Mileo <mileo@kmee.com.br>

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/pos <https://github.com/OCA/pos/tree/18.0/pos_offline>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions pos_offline/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
27 changes: 27 additions & 0 deletions pos_offline/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "POS Offline",
"summary": "Full offline capability for Point of Sale",
"version": "18.0.1.0.0",
"development_status": "Beta",
"category": "Point of Sale",
"website": "https://github.com/OCA/pos",
"author": "KMEE, Odoo Community Association (OCA)",
"license": "LGPL-3",
"application": False,
"installable": True,
"depends": [
"pos_pwa",
],
"data": [
"views/pos_config_views.xml",
],
"assets": {
"point_of_sale._assets_pos": [
"pos_offline/static/src/js/data_service_patch.esm.js",
"pos_offline/static/src/js/data_service_options_patch.esm.js",
"pos_offline/static/src/js/pos_store_patch.esm.js",
"pos_offline/static/src/js/payment_screen_patch.esm.js",
"pos_offline/static/src/js/barcode_reader_patch.esm.js",
],
},
}
3 changes: 3 additions & 0 deletions pos_offline/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import pos_config
from . import pos_session
from . import pos_order
13 changes: 13 additions & 0 deletions pos_offline/models/pos_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from odoo import fields, models


class PosConfig(models.Model):
_inherit = "pos.config"

offline_enabled = fields.Boolean(
string="Offline Mode",
default=True,
help="Enable offline mode for this POS. When enabled, the POS will "
"cache data locally and continue operating when the network is "
"unavailable.",
)
101 changes: 101 additions & 0 deletions pos_offline/models/pos_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import logging

from odoo import _, api, models
from odoo.exceptions import UserError

_logger = logging.getLogger(__name__)


class PosOrder(models.Model):
_inherit = "pos.order"

def _get_valid_session(self, order):
"""Override to create rescue session automatically for offline orders.

When a POS operates offline, orders may arrive after the original
session has been closed. Instead of raising an error, we create
a rescue session automatically.

Note: this method is only called by _process_order when the
session is already closed/closing_control.
"""
PosSession = self.env["pos.session"]
closed_session = PosSession.browse(order["session_id"])

if not closed_session.exists():
raise UserError(
_(
"Cannot process offline order %(order)s: "
"the original session (ID %(session)s) no longer exists.",
order=order.get("name", "Unknown"),
session=order["session_id"],
)
)

_logger.warning(
"Session %s (ID: %s) was closed but received offline order %s "
"(total: %s)",
closed_session.name,
closed_session.id,
order.get("name", "Unknown"),
order.get("amount_total", 0),
)

# Try to find an existing open session for this config
open_session = PosSession.search(
[
("state", "not in", ("closed", "closing_control")),
("config_id", "=", closed_session.config_id.id),
],
limit=1,
)

if open_session:
_logger.info(
"Using open session %s for saving offline order %s",
open_session.name,
order.get("name", "Unknown"),
)
return open_session

# No open session found — create rescue session automatically
return PosSession._create_rescue_session(closed_session)

@api.model
def sync_from_ui(self, orders):
"""Override to handle idempotent sync for offline orders.

When orders are synced from offline, the same order may be sent
multiple times. We detect duplicates by UUID and skip them.
"""
filtered_orders = []
for order in orders:
order_uuid = order.get("uuid")
if order_uuid:
existing = self.search(
[("uuid", "=", order_uuid), ("state", "!=", "draft")],
limit=1,
)
if existing:
_logger.info(
"Skipping duplicate offline order %s (UUID: %s, "
"existing ID: %s)",
order.get("name", "Unknown"),
order_uuid,
existing.id,
)
continue
filtered_orders.append(order)

if not filtered_orders:
# Return empty result in the expected format
return {
"pos.order": [],
"pos.session": [],
"pos.payment": [],
"pos.order.line": [],
"pos.pack.operation.lot": [],
"product.attribute.custom.value": [],
}

return super().sync_from_ui(filtered_orders)
43 changes: 43 additions & 0 deletions pos_offline/models/pos_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import logging

from odoo import models

_logger = logging.getLogger(__name__)


class PosSession(models.Model):
_inherit = "pos.session"

def _get_rescue_session_values(self, closed_session):
"""Return values for creating a rescue session from a closed one.

This ensures the rescue session inherits proper starting balances
and sequence numbers from the original session.
"""
return {
"config_id": closed_session.config_id.id,
"user_id": closed_session.user_id.id,
"cash_register_balance_start": closed_session.cash_register_balance_end,
"sequence_number": closed_session.sequence_number,
}

def _create_rescue_session(self, closed_session):
"""Create a rescue session when orders arrive for a closed session.

This is called during sync_from_ui when a session has been closed
but pending offline orders still reference it.
"""
values = self._get_rescue_session_values(closed_session)
rescue_session = self.sudo().create(values)

# Auto-open the rescue session
rescue_session.action_pos_session_open()

_logger.info(
"Created rescue session %s (ID: %s) for closed session %s (ID: %s)",
rescue_session.name,
rescue_session.id,
closed_session.name,
closed_session.id,
)
return rescue_session
3 changes: 3 additions & 0 deletions pos_offline/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
9 changes: 9 additions & 0 deletions pos_offline/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
1. Install the module (it depends on `pos_pwa` which will be auto-installed)
2. In **Point of Sale > Configuration > Settings**, the "Offline Mode"
toggle is enabled by default
3. Open the POS at least once while online to populate the cache
4. The POS will now load and operate offline on subsequent visits

**Important**: The Odoo instance must have `DB_FILTER` configured
(e.g. `^mydb$`) for the PWA manifest and Service Worker routes to
work correctly with `auth='public'`.
2 changes: 2 additions & 0 deletions pos_offline/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- [KMEE](https://kmee.com.br):
- Luis Felipe Mileo \<<mileo@kmee.com.br>\>
22 changes: 22 additions & 0 deletions pos_offline/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Full offline capability for the Odoo Point of Sale. Depends on `pos_pwa`.

When the POS is opened online, all data is cached in IndexedDB. On
subsequent visits without internet, the POS loads entirely from cache
and allows creating orders, processing cash payments, and printing
receipts. Orders are automatically synced when connectivity is restored.

Key features:

- **Offline startup**: Caches `load_data` response in IndexedDB, falls back
to cache on network error with automatic IndexedDB readiness wait
- **Pending order persistence**: Pending order IDs are persisted to IndexedDB
so they survive tab/browser closure
- **Exponential backoff retry**: Failed syncs are retried with backoff
(1s to 60s), plus a 10s periodic safety-net check
- **Offline payments**: Payment terminal methods are hidden when offline;
cash and manual methods remain available
- **Barcode fallback**: Barcode nomenclature fetch errors are caught
gracefully (scanning disabled, POS still loads)
- **Rescue sessions**: Backend automatically creates rescue sessions for
orders arriving after the original session was closed
- **Idempotent sync**: Duplicate orders (by UUID) are detected and skipped
5 changes: 5 additions & 0 deletions pos_offline/readme/ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- Delta sync on reconnection (only fetch changed records since last cache)
- Multi-currency cash register support for offline sessions
- Cache barcode nomenclature in IndexedDB for offline scanning
- Offline indicator banner in the POS UI
- Block session close when pending orders exist
6 changes: 6 additions & 0 deletions pos_offline/static/description/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading