This commit is contained in:
gsinghpal
2026-05-10 10:25:12 -04:00
parent 6c6a59ceef
commit 6b7b44264a
59 changed files with 2461 additions and 324 deletions

View File

@@ -3,11 +3,56 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
import re
from markupsafe import Markup
from odoo import api, fields, models, _
def _bump_revision_label(label):
"""Best-effort next revision after ``label``.
Customers use varied revision schemes (A/B/C, A1/A2, "Rev 1"/"Rev 2",
custom strings). This helper handles the common ones; for unrecognised
formats it returns ``label + '*'`` so the user knows they need to
fix the label manually.
- 'A''B' ... 'Y''Z''AA'
- 'a''b' (case preserved on single letter)
- 'A1''A2', 'B12''B13', 'Rev 1''Rev 2'
- 'AB''AC' (last letter incremented)
- everything else → ``label + '*'``
"""
if not label:
return 'A'
label = label.strip()
# Trailing digits — "Rev 1" → "Rev 2", "A1" → "A2".
# Preserve zero-padding when the original was padded ("014" → "015").
m = re.match(r'^(.*?)(\d+)$', label)
if m:
prefix, digits = m.group(1), m.group(2)
bumped = int(digits) + 1
if digits.startswith('0') and len(str(bumped)) <= len(digits):
return f"{prefix}{str(bumped).zfill(len(digits))}"
return f"{prefix}{bumped}"
# Single letter
if len(label) == 1 and label.isalpha():
if label.upper() == 'Z':
return 'AA' if label.isupper() else 'aa'
return chr(ord(label) + 1)
# Multi-char ending in letter — "AB" → "AC"
m = re.match(r'^(.*?)([A-Za-z])$', label)
if m and m.group(2).upper() != 'Z':
return m.group(1) + chr(ord(m.group(2)) + 1)
# Unknown format — caller must edit
return label + '*'
class FpPartCatalog(models.Model):
"""Customer part library.
@@ -36,8 +81,12 @@ class FpPartCatalog(models.Model):
tracking=True, domain="[('customer_rank', '>', 0)]",
)
part_number = fields.Char(string='Part Number', required=True, tracking=True, help="Customer's part number (e.g. VS-R392007E01).")
revision = fields.Char(string='Revision', required=True, default='A', help='Revision letter or number (e.g. Rev: 1B).')
revision_number = fields.Integer(string='Rev #', default=1)
revision = fields.Char(
string='Revision', required=True, default='A',
help="Customer's drawing revision label. Free-text — accepts any "
"format the customer uses (A, B, C / A1, B2 / Rev 1, Rev 2 / "
"ECO-2024-014 etc.).",
)
revision_note = fields.Char(string='Revision Note', help='What changed in this revision.')
revision_date = fields.Datetime(string='Revision Date', default=fields.Datetime.now)
parent_part_id = fields.Many2one(
@@ -643,21 +692,46 @@ class FpPartCatalog(models.Model):
'target': 'current',
}
def action_create_revision(self):
"""Create a new revision of this part. Copies all data, increments revision number."""
def action_open_revision_wizard(self):
"""Open the interactive Create-New-Revision wizard.
This is what the form-header button calls. The wizard asks
the user for the revision label, note, and optionally a new
drawing/3D file BEFORE the new record is created — which is
what most users want.
For non-interactive callers (auto-rev on 3D upload, direct
order line bump) use ``action_create_revision`` directly.
"""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Create New Revision'),
'res_model': 'fp.part.revision.bump.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_part_id': self.id,
'active_id': self.id,
'active_model': 'fp.part.catalog',
},
}
def action_create_revision(self):
"""Programmatic, non-interactive revision bump.
Copies the part with a best-effort label bump via
``_bump_revision_label``. Used by code paths that don't have
a user prompt (auto-rev when a new 3D model is uploaded on a
quote, direct-order line bump). User-facing flows should call
``action_open_revision_wizard`` instead.
"""
self.ensure_one()
# Mark current as no longer latest
self.is_latest_revision = False
# Determine the root part for the chain
root = self.parent_part_id or self
# Find highest revision number in chain
all_revs = self.env['fp.part.catalog'].search([
'|', ('id', '=', root.id), ('parent_part_id', '=', root.id),
])
max_rev = max(all_revs.mapped('revision_number') or [0])
new_label = _bump_revision_label(self.revision)
new_rev = self.copy({
'revision_number': max_rev + 1,
'revision': f'Rev {max_rev + 1}',
'revision': new_label,
'revision_date': fields.Datetime.now(),
'revision_note': False,
'parent_part_id': root.id,

View File

@@ -697,13 +697,10 @@ class FpQuoteConfigurator(models.Model):
old_part = self.part_catalog_id
old_part.is_latest_revision = False
root = old_part.parent_part_id or old_part
all_revs = self.env['fp.part.catalog'].search([
'|', ('id', '=', root.id), ('parent_part_id', '=', root.id),
])
max_rev = max(all_revs.mapped('revision_number') or [0])
from .fp_part_catalog import _bump_revision_label
new_label = _bump_revision_label(old_part.revision)
new_part = old_part.copy({
'revision_number': max_rev + 1,
'revision': f'Rev {max_rev + 1}',
'revision': new_label,
'revision_date': fields.Datetime.now(),
'revision_note': f'Updated 3D model: {fname}',
'parent_part_id': root.id,