Phase G of permissions overhaul.
G2: sale.order.action_confirm now requires group_fp_sales_manager
(spec Section 2.B). Sales Reps can save drafts but cannot move SOs
to 'sale' state. UserError raised with clear message if attempted.
G3: Fixed audit-finding-11 typo bug in 2 files. The original code
checked has_group('fusion_plating.group_fusion_plating_administrator'),
an xmlid that has NEVER existed - so the gate always returned False
and only the Manager-side check actually fired. Fixed both:
- fusion_plating_invoicing/models/res_partner.py:34
- fusion_plating_configurator/wizard/fp_direct_order_wizard.py:467
Both now check has_group('fusion_plating.group_fp_manager') which
transitively includes Owner via implied_ids.
G4: Swept all Python has_group() calls to reference new group xmlids.
Backward-compat keeps old refs working today (Phase A's implied_ids),
but the sweep ensures correctness after the 30-day rollback window
deletes old groups. Replacements:
group_fusion_plating_operator -> group_fp_technician
group_fusion_plating_supervisor -> group_fp_shop_manager_v2
group_fusion_plating_manager -> group_fp_manager
group_fusion_plating_admin -> group_fp_owner
group_fusion_plating_cgp_officer -> group_fp_quality_manager
group_fusion_plating_cgp_designated_official -> group_fp_owner
group_fp_estimator -> group_fp_sales_rep
group_fp_accounting -> group_fp_manager
group_fp_receiving -> group_fp_shop_manager_v2
group_fp_shop_manager (legacy) -> group_fp_manager
G1: test_sales_manager_gate.py covers the new confirm gate (SR
blocked, SMg allowed, Manager allowed via diamond implication).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
3.9 KiB
Python
87 lines
3.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
# Part of the Fusion Plating product family.
|
|
|
|
from odoo import api, fields, models
|
|
|
|
|
|
class ResPartner(models.Model):
|
|
_inherit = 'res.partner'
|
|
|
|
# ===== Account hold (existing) ============================================
|
|
x_fc_account_hold = fields.Boolean(
|
|
string='Account Hold', tracking=True,
|
|
help='When active, blocks SO confirmation, invoicing, and shipping.',
|
|
)
|
|
|
|
@api.model
|
|
def _fp_user_can_override_account_hold(self):
|
|
"""True when the current user is allowed to override an account hold.
|
|
|
|
Plating Manager OR Plating Administrator qualifies. Administrator
|
|
is checked explicitly (in addition to the implied chain) because
|
|
Odoo's ``implied_ids`` cascade does NOT reliably propagate to
|
|
existing users on module upgrade — admin (uid 1) typically lands
|
|
in Administrator only, with no Manager membership. Without this
|
|
defensive check, the highest-privileged user can't bypass holds.
|
|
|
|
See CLAUDE.md "Implied group cascade" rule.
|
|
"""
|
|
user = self.env.user
|
|
# Phase G: fixed audit-finding-11 — old code referenced
|
|
# 'fusion_plating.group_fusion_plating_administrator', an xmlid
|
|
# that never existed, so the gate always returned False. Replaced
|
|
# with group_fp_manager which transitively implies Owner via
|
|
# implied_ids in Phase A's diamond hierarchy.
|
|
return user.has_group('fusion_plating.group_fp_manager')
|
|
x_fc_account_hold_reason = fields.Text(string='Hold Reason')
|
|
x_fc_account_hold_date = fields.Datetime(
|
|
string='Hold Date', help='When the hold was placed.',
|
|
)
|
|
x_fc_account_hold_by_id = fields.Many2one(
|
|
'res.users', string='Hold Placed By',
|
|
)
|
|
|
|
# ===== Plating Defaults (cascade onto every new SO for this customer) =====
|
|
# The estimator sets these once on the customer record; they pre-fill
|
|
# invoice strategy, delivery method, and deadlines on every new SO so
|
|
# repeat customers don't need re-typing the same values each order.
|
|
# Tax type lives on `property_account_position_id` (Odoo native fiscal
|
|
# position) and payment terms on `property_payment_term_id` — both are
|
|
# surfaced on the same Plating Defaults tab in the partner form.
|
|
|
|
x_fc_default_invoice_strategy = fields.Selection(
|
|
[('deposit', 'Deposit'),
|
|
('progress', 'Progress Billing'),
|
|
('net_terms', 'Net Terms'),
|
|
('cod_prepay', 'COD / Prepay')],
|
|
string='Default Invoice Strategy',
|
|
help='Pre-fills the SO invoice strategy when this customer is selected. '
|
|
'The estimator can still override per order.',
|
|
)
|
|
x_fc_default_deposit_percent = fields.Float(
|
|
string='Default Deposit %',
|
|
help='Used when invoice strategy is "Deposit". e.g. 50.0 for 50%.',
|
|
)
|
|
x_fc_default_delivery_method = fields.Selection(
|
|
[('local_delivery', 'Local Delivery'),
|
|
('shipping_partner', 'Shipping Partner'),
|
|
('customer_pickup', 'Customer Pickup')],
|
|
string='Default Delivery Method',
|
|
help='Pre-fills the SO delivery method when this customer is selected.',
|
|
)
|
|
# Lead-time defaults are expressed as offsets FROM the SO's planned-start
|
|
# date so they track real production schedules, not just "today + N".
|
|
# If planned_start is unset on the SO, the cascade falls back to today.
|
|
x_fc_default_internal_deadline_days = fields.Integer(
|
|
string='Internal Deadline (+ days from start)',
|
|
help='Pre-fills SO internal deadline as planned_start_date + this '
|
|
'many days. e.g. 5 means "ship five days after we start".',
|
|
)
|
|
x_fc_default_customer_deadline_days = fields.Integer(
|
|
string='Customer Deadline (+ days from start)',
|
|
help='Pre-fills the customer-facing commitment date as '
|
|
'planned_start_date + this many days.',
|
|
)
|