feat(invoicing): managers (+QM+Owner) can create customer invoices
Grant Odoo Billing (account.group_account_invoice) to group_fp_manager via implied_ids; Quality Manager + Owner inherit it. Billing only (not Accountant); the SO-origin workflow gate in fusion_plating_jobs is unchanged, so managers invoice from the Sale Order's Create Invoice action. Tests assert Manager/Owner get Billing and Shop Manager does not. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
|
<record id="fusion_plating.group_fp_manager" model="res.groups">
|
||||||
|
<field name="implied_ids" eval="[(4, ref('fusion_plating_invoicing.group_fp_accounting')),
|
||||||
|
(4, ref('account.group_account_invoice'))]"/>
|
||||||
|
</record>
|
||||||
|
```
|
||||||
|
|
||||||
|
`(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.
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Invoicing',
|
'name': 'Fusion Plating — Invoicing',
|
||||||
'version': '19.0.3.7.0',
|
'version': '19.0.3.8.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Invoice strategy engine with deposit, progress billing, net terms, COD/prepay, and account holds.',
|
'summary': 'Invoice strategy engine with deposit, progress billing, net terms, COD/prepay, and account holds.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -14,9 +14,15 @@
|
|||||||
<field name="user_ids" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
<field name="user_ids" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- Backward-compat: new Manager role implies old Accounting group. -->
|
<!-- Backward-compat: new Manager role implies old Accounting group.
|
||||||
|
2026-05-29: Manager (+ QM + Owner via implication) also gets Odoo's
|
||||||
|
Billing group so they can create customer invoices from a Sale
|
||||||
|
Order. Billing only — not Accountant. The SO-origin workflow gate
|
||||||
|
in fusion_plating_jobs is unchanged (off-SO invoices still blocked).
|
||||||
|
Spec: docs/superpowers/specs/2026-05-29-manager-invoice-permission-design.md -->
|
||||||
<record id="fusion_plating.group_fp_manager" model="res.groups">
|
<record id="fusion_plating.group_fp_manager" model="res.groups">
|
||||||
<field name="implied_ids" eval="[(4, ref('fusion_plating_invoicing.group_fp_accounting'))]"/>
|
<field name="implied_ids" eval="[(4, ref('fusion_plating_invoicing.group_fp_accounting')),
|
||||||
|
(4, ref('account.group_account_invoice'))]"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import test_manager_invoice_permission
|
||||||
@@ -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'))
|
||||||
Reference in New Issue
Block a user