diff --git a/README.md b/README.md index 8d3f090..e15978d 100644 --- a/README.md +++ b/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: diff --git a/models/__init__.py b/models/__init__.py index ad0f617..85099a1 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -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 + diff --git a/models/ir_http.py b/models/ir_http.py index 677f7bc..3d98084 100644 --- a/models/ir_http.py +++ b/models/ir_http.py @@ -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()) - valid_cids = [c for c in cids if c in user_cids] - if not valid_cids: - valid_cids = [request.env.user.company_id.id] - 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) + 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 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) diff --git a/models/pos_order.py b/models/pos_order.py new file mode 100644 index 0000000..4e66672 --- /dev/null +++ b/models/pos_order.py @@ -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)