diff --git a/fusion_plating/docs/superpowers/specs/2026-05-29-manager-invoice-permission-design.md b/fusion_plating/docs/superpowers/specs/2026-05-29-manager-invoice-permission-design.md new file mode 100644 index 00000000..7c0b2bc3 --- /dev/null +++ b/fusion_plating/docs/superpowers/specs/2026-05-29-manager-invoice-permission-design.md @@ -0,0 +1,71 @@ +# Managers Can Create Invoices (grant Billing group) + +**Date:** 2026-05-29 +**Status:** Approved — implementing directly (single security record; no separate plan) +**Module:** `fusion_plating_invoicing` + +## Goal + +Let the plating **Manager** role (and Quality Manager + Owner, who inherit it) create customer +invoices from a confirmed Sale Order. + +## Background — why managers can't invoice today (verified 2026-05-29) + +- Creating an `account.move` of a customer type requires one of Odoo's accounting groups + (minimum: **Billing** = `account.group_account_invoice`). +- **No plating role grants any Odoo accounting group.** A repo-wide grep for + `account.group_account*` in `fusion_plating*/security/` returns nothing. +- `group_fp_manager` implies only the legacy plating-internal `group_fp_accounting` + (`fusion_plating_invoicing/security/fp_invoicing_security.xml:18-20`), which itself chains + to the deprecated `group_fusion_plating_supervisor` — **not** to any Odoo accounting group. + So it grants no `account.move` rights. +- Result: when a Manager opens a confirmed SO and clicks **Create Invoice**, Odoo's + `_create_invoices()` tries to create an `account.move` and the Manager hits an `AccessError` + for lacking Billing. +- Separately, `fusion_plating_jobs/models/account_move.py::_fp_validate_customer_invoice` + blocks off-SO customer-invoice creation for **all** users (parent-number audit trail). This + is a *workflow* gate, not a permission gate — it is **not** the manager blocker and stays + unchanged. + +## Decision + +Grant `account.group_account_invoice` (**Billing**) to `group_fp_manager` via `implied_ids`. + +- **Cascade:** Quality Manager → implies Manager, Owner → implies Quality Manager, so QM and + Owner inherit Billing automatically. Shop Manager, Sales Manager, Technician, Operator, and + below are **unaffected**. +- **Level — Billing only:** create/edit/post customer invoices + credit notes. Explicitly + **not** Accountant (`account.group_account_user`): no vendor bills, manual journal entries, + bank reconciliation, accounting reports, or period close. +- **Path unchanged:** managers invoice **from the SO** (`Create Invoice` action), which the + `_fp_validate_customer_invoice` workflow gate already permits via `fp_from_so_invoice`. + Standalone off-SO invoices remain blocked for everyone. + +## Implementation + +Extend the existing additive `implied_ids` write on `group_fp_manager` in +`fusion_plating_invoicing/security/fp_invoicing_security.xml`: + +```xml + + + +``` + +`(4, id)` (Command.link) is additive + idempotent across install/`-u`. `fusion_plating_invoicing` +already depends transitively on `account` (it overrides `account.move`), so `ref('account.…')` +resolves. Bump the module `version`. + +## Testing + +`TransactionCase` in `fusion_plating_invoicing/tests/`: create a user holding **only** +`group_fp_manager`, assert `user.has_group('account.group_account_invoice')` is `True` (proves +the implication landed). Then a live entech check: a Manager creates an invoice from a +confirmed SO without an AccessError. + +## Out of scope + +- Relaxing the SO-origin workflow gate (intentional; preserves the parent-number audit trail). +- Accountant-level access (vendor bills, journals, reconciliation, reports). +- Granting invoicing to Shop Manager / Sales Manager / below. diff --git a/fusion_plating/fusion_plating_invoicing/__manifest__.py b/fusion_plating/fusion_plating_invoicing/__manifest__.py index 16bf70aa..01b3d662 100644 --- a/fusion_plating/fusion_plating_invoicing/__manifest__.py +++ b/fusion_plating/fusion_plating_invoicing/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Invoicing', - 'version': '19.0.3.7.0', + 'version': '19.0.3.8.0', 'category': 'Manufacturing/Plating', 'summary': 'Invoice strategy engine with deposit, progress billing, net terms, COD/prepay, and account holds.', 'description': """ diff --git a/fusion_plating/fusion_plating_invoicing/security/fp_invoicing_security.xml b/fusion_plating/fusion_plating_invoicing/security/fp_invoicing_security.xml index 06b0669d..73cd595f 100644 --- a/fusion_plating/fusion_plating_invoicing/security/fp_invoicing_security.xml +++ b/fusion_plating/fusion_plating_invoicing/security/fp_invoicing_security.xml @@ -14,9 +14,15 @@ - + - + diff --git a/fusion_plating/fusion_plating_invoicing/tests/__init__.py b/fusion_plating/fusion_plating_invoicing/tests/__init__.py new file mode 100644 index 00000000..e738dbb1 --- /dev/null +++ b/fusion_plating/fusion_plating_invoicing/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import test_manager_invoice_permission diff --git a/fusion_plating/fusion_plating_invoicing/tests/test_manager_invoice_permission.py b/fusion_plating/fusion_plating_invoicing/tests/test_manager_invoice_permission.py new file mode 100644 index 00000000..1e756e63 --- /dev/null +++ b/fusion_plating/fusion_plating_invoicing/tests/test_manager_invoice_permission.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +"""Manager (+ QM + Owner) imply Odoo Billing so they can invoice. + +Spec: docs/superpowers/specs/2026-05-29-manager-invoice-permission-design.md +""" +from odoo.tests.common import TransactionCase, tagged + + +@tagged('post_install', '-at_install', 'fp_invoice_perm') +class TestManagerInvoicePermission(TransactionCase): + + def _user_with(self, group_xmlid): + return self.env['res.users'].create({ + 'name': 'PermUser', + 'login': 'perm_%s' % group_xmlid.split('.')[-1], + 'group_ids': [(6, 0, [self.env.ref(group_xmlid).id])], + }) + + def test_manager_has_billing_group(self): + manager = self._user_with('fusion_plating.group_fp_manager') + self.assertTrue( + manager.has_group('account.group_account_invoice'), + "Manager should imply Odoo Billing (account.group_account_invoice)", + ) + + def test_owner_inherits_billing(self): + owner = self._user_with('fusion_plating.group_fp_owner') + self.assertTrue(owner.has_group('account.group_account_invoice')) + + def test_shop_manager_does_not_get_billing(self): + # Shop Manager is BELOW Manager — must NOT inherit Billing + # ("managers and ABOVE" scope). + shop = self._user_with('fusion_plating.group_fp_shop_manager_v2') + self.assertFalse(shop.has_group('account.group_account_invoice'))