feat: escalate POS order processing to superuser and harden company context sanitization in IrHttp

This commit is contained in:
Suherdy Yacob 2026-05-20 00:27:56 +07:00
parent bdf12104c1
commit bf68e7c40b
4 changed files with 114 additions and 28 deletions

View File

@ -21,18 +21,18 @@ When a branch-side POS session is closed, the module automates the inter-company
- Debits the parent bank's **Outstanding Receipt Account**, allowing for seamless reconciliation against incoming bank statement lines in the parent's accounting dashboard. - Debits the parent bank's **Outstanding Receipt Account**, allowing for seamless reconciliation against incoming bank statement lines in the parent's accounting dashboard.
- Credits the Inter-company Liability account (e.g., `229101 Hubungan RK`). - Credits the Inter-company Liability account (e.g., `229101 Hubungan RK`).
### 3. POS Multi-Company Security Bypasses ### 3. Centralized Vendor Payment
To allow POS Cashiers (who only have access to a branch company) to smoothly interact with parent company payment methods and products without encountering restrictive Odoo `AccessError` blocks:
- Overrides Odoo's core `product.product_comp_rule` to grant all internal users global read access `[(1, '=', 1)]` to products across all companies.
- Overrides Odoo's core `base.res_company_rule_employee` to grant all internal users global read access to `res.company` records, ensuring shared journals and payment methods can be read safely during order sync.
- (These specific patches are implemented in the `hr_multi_company_employee` dependency module's `_register_hook`).
### 4. Centralized Vendor Payment
Enables branches to pay vendor bills from a bank account managed by a parent company: Enables branches to pay vendor bills from a bank account managed by a parent company:
- **Branch-Side**: Intercepts the "Register Payment" wizard. Instead of creating a standard payment, it generates a clearing entry that moves the liability from the Vendor (Accounts Payable) to the Parent Company (Inter-company Account). - **Branch-Side**: Intercepts the "Register Payment" wizard. Instead of creating a standard payment, it generates a clearing entry that moves the liability from the Vendor (Accounts Payable) to the Parent Company (Inter-company Account).
- **Parent-Side**: Automatically creates an actual `account.payment` record in the parent company, paying out of the parent bank journal and debiting the inter-company clearing account. - **Parent-Side**: Automatically creates an actual `account.payment` record in the parent company, paying out of the parent bank journal and debiting the inter-company clearing account.
- **Workflow**: Vendor bill remains in the branch, but is marked as **Paid** via the clearing mechanism. The actual cash outflow and bank reconciliation happen in the parent. - **Workflow**: Vendor bill remains in the branch, but is marked as **Paid** via the clearing mechanism. The actual cash outflow and bank reconciliation happen in the parent.
### 4. Multi-Company POS Order and Checkout Bypass
Solves Odoo's restrictive `Access to unauthorized or invalid companies` error/warnings in logs when branch cashiers place orders, checkout, or sync POS sessions:
- **Environment & Session Sanitization**: Overrides `ir.http._pre_dispatch` and `_dispatch` to safely query the cashier's authorized companies using `sudo()`, and dynamically filters out unauthorized or parent company IDs (such as parent company `2`) from `allowed_company_ids` in request parameters (`request.params`), HTTP sessions (`request.session.context`), and environment context (`request.env.context`).
- **Sudo Order Post-Processing**: Inherits `pos.order` to run post-processing (`_process_saved_order`, including picking generation, cost/method computation, and payment posting) under `sudo()`. This allows Odoo to safely read company-dependent properties (like `cost_method` or `property_cost_method`) and generate inventory pickings without triggering security warnings in `environments.py`.
## Configuration ## Configuration
### 1. POS Inter-Company Clearing ### 1. POS Inter-Company Clearing
To enable the automated inter-company clearing, navigate to **Point of Sale > Configuration > Payment Methods** and configure the following in the "Inter-Company Clearing" section: To enable the automated inter-company clearing, navigate to **Point of Sale > Configuration > Payment Methods** and configure the following in the "Inter-Company Clearing" section:

View File

@ -4,3 +4,5 @@ from . import account_payment
from . import ir_http from . import ir_http
from . import pos_payment_method from . import pos_payment_method
from . import pos_session from . import pos_session
from . import pos_order

View File

@ -12,29 +12,99 @@ class IrHttp(models.AbstractModel):
@classmethod @classmethod
def _pre_dispatch(cls, rule, args): def _pre_dispatch(cls, rule, args):
""" """
Sanitize allowed_company_ids in the request context BEFORE the Sanitize allowed_company_ids in the request session, request environment,
environment is fully used. This prevents AccessError from and request parameters contexts BEFORE the environment is fully used.
environments.py when the browser cookie (cids) contains company IDs This prevents AccessError from environments.py when any of these contexts
that are not in the user's authorized _get_company_ids() list. contain company IDs that are not in the user's authorized list.
This commonly happens when a user had a parent company in their
allowed companies list and the stale cids cookie persists in the browser.
""" """
try: try:
if request.env.uid and request.session.context.get('allowed_company_ids'): if request.env.uid:
cids = request.session.context['allowed_company_ids'] # Use sudo() to query the user's companies safely, bypassing company restrictions.
user_cids = set(request.env.user._get_company_ids()) user_cids = set(request.env.user.sudo()._get_company_ids())
valid_cids = [c for c in cids if c in user_cids] default_cid = request.env.user.sudo().company_id.id
if not valid_cids:
valid_cids = [request.env.user.company_id.id] def sanitize_context_cids(context, user_cids, default_cid):
if valid_cids != cids: if not isinstance(context, dict) or 'allowed_company_ids' not in context:
_logger.warning( return False
"IrHttp: sanitizing allowed_company_ids for %s: %s -> %s", cids = context['allowed_company_ids']
request.env.user.login, cids, valid_cids if not isinstance(cids, list):
) return False
request.session.context['allowed_company_ids'] = valid_cids valid_cids = [c for c in cids if c in user_cids]
request.update_context(allowed_company_ids=valid_cids) if not valid_cids:
valid_cids = [default_cid]
if valid_cids != cids:
_logger.warning("Sanitized allowed_company_ids in context from %s to %s for user %s", cids, valid_cids, request.env.uid)
context['allowed_company_ids'] = valid_cids
return True
return False
# Sanitize session context
if getattr(request, 'session', None) and getattr(request.session, 'context', None):
sanitize_context_cids(request.session.context, user_cids, default_cid)
# Sanitize request.env.context
env_context = dict(request.env.context)
if sanitize_context_cids(env_context, user_cids, default_cid):
request.update_context(**env_context)
# Sanitize request params root context
if isinstance(getattr(request, 'params', None), dict):
root_context = request.params.get('context')
if isinstance(root_context, dict):
sanitize_context_cids(root_context, user_cids, default_cid)
# Sanitize request params kwargs context
kwargs = request.params.get('kwargs')
if isinstance(kwargs, dict):
kwargs_context = kwargs.get('context')
if isinstance(kwargs_context, dict):
sanitize_context_cids(kwargs_context, user_cids, default_cid)
except Exception as e: except Exception as e:
_logger.debug("IrHttp: could not sanitize company context: %s", e) _logger.warning("IrHttp: could not sanitize company context in _pre_dispatch: %s", e, exc_info=True)
return super()._pre_dispatch(rule, args) return super()._pre_dispatch(rule, args)
@classmethod
def _dispatch(cls, endpoint):
"""
Sanitize allowed_company_ids in the request parameters (request.params)
context AFTER they have been populated by the JSON-RPC dispatcher but
BEFORE the controller/model method is executed.
"""
try:
if request.env.uid:
# Use sudo() to query the user's companies safely, bypassing company restrictions.
user_cids = set(request.env.user.sudo()._get_company_ids())
default_cid = request.env.user.sudo().company_id.id
def sanitize_context_cids(context, user_cids, default_cid):
if not isinstance(context, dict) or 'allowed_company_ids' not in context:
return False
cids = context['allowed_company_ids']
if not isinstance(cids, list):
return False
valid_cids = [c for c in cids if c in user_cids]
if not valid_cids:
valid_cids = [default_cid]
if valid_cids != cids:
_logger.warning("Sanitized allowed_company_ids in _dispatch from %s to %s for user %s", cids, valid_cids, request.env.uid)
context['allowed_company_ids'] = valid_cids
return True
return False
# Sanitize request params root context
if isinstance(getattr(request, 'params', None), dict):
root_context = request.params.get('context')
if isinstance(root_context, dict):
sanitize_context_cids(root_context, user_cids, default_cid)
# Sanitize request params kwargs context
kwargs = request.params.get('kwargs')
if isinstance(kwargs, dict):
kwargs_context = kwargs.get('context')
if isinstance(kwargs_context, dict):
sanitize_context_cids(kwargs_context, user_cids, default_cid)
except Exception as e:
_logger.warning("IrHttp: could not sanitize company context in _dispatch: %s", e, exc_info=True)
return super()._dispatch(endpoint)

14
models/pos_order.py Normal file
View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from odoo import models
class PosOrder(models.Model):
_inherit = 'pos.order'
def _process_saved_order(self, draft):
"""
Escalate to superuser during saved order post-processing (picking creation,
cost computation, etc.). This avoids multi-company access/environment checks
when POS cashier handles cross-company or company-dependent properties.
"""
return super(PosOrder, self.sudo())._process_saved_order(draft)