fix(numbering): post-review fixes — credit notes, SO unlink, multi-part grouping, SQL whitelist
- B1: Add Credit Note wizard path was blocked because invoice_origin has copy=False and the wizard doesn't set fp_from_so_invoice. Now the validator allows reversals when reversed_entry_id points at a customer-facing move that itself went through the validator at original creation time. account.move._fp_parent_sale_order also walks self.reversed_entry_id._fp_parent_sale_order so the credit note inherits the parent number (CN-<parent>). - Bug 1: sale.order.unlink() now blocks deletion when x_fc_parent_number is set (matches spec §6.2). Draft quotes remain freely deletable per Odoo standard. Applies to all users including admins. - Bug 2: out_receipt added to CUSTOMER_TYPES so POS-style receipts hit the same SO-flow gate as out_invoice / out_refund. - C1: WO grouping key changed from recipe.id to (recipe.id, part.id, coating.id). Bundling lines with different parts under one WO put first_line's part_number on the CoC header — silent compliance mis-attestation. Now distinct parts always get distinct WOs even when they share a recipe. - C3: SQL whitelist (_FP_COUNTER_FIELD_RE) on _fp_assign_parent_name's interpolated counter field name. No user input today; defence in depth for future subclasses that might read the name from context. Verified on entech: parent=30017, credit note = CN-30017, multi-part SO produces 2 WOs (one per part), confirmed-SO unlink blocked, out_receipt blocked, whitelist regex enforced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating',
|
||||
'version': '19.0.18.15.14',
|
||||
'version': '19.0.18.15.15',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||
'description': """
|
||||
|
||||
@@ -11,10 +11,19 @@ are impossible. Subclasses implement three small hooks and call
|
||||
See docs/superpowers/specs/2026-05-12-parent-number-hierarchy-design.md
|
||||
for the design rationale.
|
||||
"""
|
||||
import re
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.translate import _
|
||||
|
||||
# Whitelist regex for counter-field names. The mixin interpolates the
|
||||
# returned name into raw SQL, so a future subclass that read this from
|
||||
# a context value or Selection field would otherwise open a SQL-injection
|
||||
# surface. Enforce: must look like one of our x_fc_pn_*_count counters
|
||||
# (lowercase letters / underscores only).
|
||||
_FP_COUNTER_FIELD_RE = re.compile(r'^x_fc_pn_[a-z_]+_count$')
|
||||
|
||||
|
||||
class FpParentNumberedMixin(models.AbstractModel):
|
||||
_name = 'fp.parent.numbered.mixin'
|
||||
@@ -73,6 +82,16 @@ class FpParentNumberedMixin(models.AbstractModel):
|
||||
if not so or not so.x_fc_parent_number:
|
||||
return False
|
||||
counter_field = self._fp_parent_counter_field()
|
||||
# Whitelist check — the field name is interpolated directly into
|
||||
# SQL below, so we never trust an arbitrary string. All current
|
||||
# subclasses return a literal; this guard exists so a future
|
||||
# subclass that reads the field name from context / Selection /
|
||||
# user input can't smuggle a SQL fragment in.
|
||||
if not _FP_COUNTER_FIELD_RE.match(counter_field or ''):
|
||||
raise UserError(_(
|
||||
'Invalid parent-counter field name %r — must match '
|
||||
'pattern x_fc_pn_*_count.'
|
||||
) % counter_field)
|
||||
# SELECT FOR UPDATE - locks the SO row until commit, so a
|
||||
# concurrent create on the same SO blocks here and reads the
|
||||
# updated counter after we release. No race, no drift.
|
||||
|
||||
Reference in New Issue
Block a user