feat: escalate POS order processing to superuser and harden company context sanitization in IrHttp
This commit is contained in:
parent
bdf12104c1
commit
bf68e7c40b
14
README.md
14
README.md
@ -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.
|
||||
- Credits the Inter-company Liability account (e.g., `229101 Hubungan RK`).
|
||||
|
||||
### 3. POS Multi-Company Security Bypasses
|
||||
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
|
||||
### 3. Centralized Vendor Payment
|
||||
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).
|
||||
- **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.
|
||||
|
||||
### 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
|
||||
### 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:
|
||||
|
||||
@ -4,3 +4,5 @@ from . import account_payment
|
||||
from . import ir_http
|
||||
from . import pos_payment_method
|
||||
from . import pos_session
|
||||
from . import pos_order
|
||||
|
||||
|
||||
@ -12,29 +12,99 @@ class IrHttp(models.AbstractModel):
|
||||
@classmethod
|
||||
def _pre_dispatch(cls, rule, args):
|
||||
"""
|
||||
Sanitize allowed_company_ids in the request context BEFORE the
|
||||
environment is fully used. This prevents AccessError from
|
||||
environments.py when the browser cookie (cids) contains company IDs
|
||||
that are not in the user's authorized _get_company_ids() 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.
|
||||
Sanitize allowed_company_ids in the request session, request environment,
|
||||
and request parameters contexts BEFORE the environment is fully used.
|
||||
This prevents AccessError from environments.py when any of these contexts
|
||||
contain company IDs that are not in the user's authorized list.
|
||||
"""
|
||||
try:
|
||||
if request.env.uid and request.session.context.get('allowed_company_ids'):
|
||||
cids = request.session.context['allowed_company_ids']
|
||||
user_cids = set(request.env.user._get_company_ids())
|
||||
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 = [request.env.user.company_id.id]
|
||||
valid_cids = [default_cid]
|
||||
if valid_cids != cids:
|
||||
_logger.warning(
|
||||
"IrHttp: sanitizing allowed_company_ids for %s: %s -> %s",
|
||||
request.env.user.login, cids, valid_cids
|
||||
)
|
||||
request.session.context['allowed_company_ids'] = valid_cids
|
||||
request.update_context(allowed_company_ids=valid_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:
|
||||
_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)
|
||||
|
||||
@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
14
models/pos_order.py
Normal 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)
|
||||
Loading…
Reference in New Issue
Block a user