changes
This commit is contained in:
@@ -8,4 +8,5 @@ from . import fp_add_from_so_wizard
|
||||
from . import fp_add_from_quote_wizard
|
||||
from . import fp_quote_promote_wizard
|
||||
from . import fp_part_catalog_import_wizard
|
||||
from . import fp_part_revision_bump_wizard
|
||||
from . import fp_serial_bulk_add_wizard
|
||||
|
||||
@@ -55,7 +55,10 @@ class FpDirectOrderLine(models.Model):
|
||||
coating_config_id = fields.Many2one(
|
||||
'fp.coating.config',
|
||||
string='Primary Treatment',
|
||||
required=True,
|
||||
help='Optional. Some orders are non-coating work (re-inspection, '
|
||||
'rework, masking-only, etc.) and the operator picks the '
|
||||
'workflow downstream — leaving this blank lets that path '
|
||||
'through.',
|
||||
)
|
||||
treatment_ids = fields.Many2many(
|
||||
'fp.treatment',
|
||||
@@ -665,7 +668,7 @@ class FpDirectOrderLine(models.Model):
|
||||
new_rev = self.env['fp.part.catalog'].search([
|
||||
('parent_part_id', '=', (part.parent_part_id or part).id),
|
||||
('is_latest_revision', '=', True),
|
||||
], limit=1, order='revision_number desc')
|
||||
], limit=1, order='revision_date desc')
|
||||
if not new_rev:
|
||||
return part
|
||||
|
||||
|
||||
@@ -189,21 +189,23 @@ class FpDirectOrderWizard(models.Model):
|
||||
rec.total_qty = sum(rec.line_ids.mapped('quantity'))
|
||||
rec.total_line_count = len(rec.line_ids)
|
||||
|
||||
@api.depends('line_ids.part_catalog_id', 'line_ids.coating_config_id',
|
||||
@api.depends('line_ids.part_catalog_id',
|
||||
'line_ids.unit_price', 'line_ids.quantity')
|
||||
def _compute_missing_info_msg(self):
|
||||
for rec in self:
|
||||
has_missing = False
|
||||
for line in rec.line_ids:
|
||||
# coating_config_id intentionally NOT in the gate —
|
||||
# it's optional now (rework / inspection-only / masking
|
||||
# work doesn't need a primary treatment).
|
||||
if (not line.part_catalog_id
|
||||
or not line.coating_config_id
|
||||
or not line.unit_price
|
||||
or not line.quantity):
|
||||
has_missing = True
|
||||
break
|
||||
rec.missing_info_msg = (
|
||||
'Some lines are missing quote information '
|
||||
'(part / treatment / price / qty). '
|
||||
'(part / price / qty). '
|
||||
'Verify before confirming the order.'
|
||||
if has_missing else False
|
||||
)
|
||||
@@ -272,7 +274,10 @@ class FpDirectOrderWizard(models.Model):
|
||||
|
||||
# Account-hold early warning. Hard block lives in action_confirm
|
||||
# but Sarah deserves to know NOW before she builds 5 lines.
|
||||
if getattr(self.partner_id, 'x_fc_account_hold', False):
|
||||
# Resolve via commercial_partner so a hold on the company is
|
||||
# caught even when an Acme-AP child contact is selected.
|
||||
commercial = self.partner_id.commercial_partner_id
|
||||
if getattr(commercial, 'x_fc_account_hold', False):
|
||||
return {
|
||||
'warning': {
|
||||
'title': _('Customer on Account Hold'),
|
||||
@@ -280,7 +285,7 @@ class FpDirectOrderWizard(models.Model):
|
||||
'%s is currently on account hold. You can still '
|
||||
'build the quotation, but it cannot be confirmed '
|
||||
'until the hold is cleared by accounting.'
|
||||
) % self.partner_id.display_name,
|
||||
) % commercial.display_name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,14 +443,24 @@ class FpDirectOrderWizard(models.Model):
|
||||
# Account-hold hard block — same policy as sale.order.action_confirm
|
||||
# but enforced earlier so the wizard doesn't waste Sarah's time.
|
||||
# Manager override allowed via context key fp_skip_account_hold=True.
|
||||
if (getattr(self.partner_id, 'x_fc_account_hold', False)
|
||||
# Resolved through commercial_partner so a hold on the company
|
||||
# blocks every child-contact entry too.
|
||||
commercial = self.partner_id.commercial_partner_id
|
||||
# Bypass: Plating Manager OR Plating Administrator. Both checked
|
||||
# because Odoo's implied_ids cascade (Administrator → Manager)
|
||||
# doesn't always propagate to existing users on upgrade. See
|
||||
# CLAUDE.md "Implied group cascade" rule.
|
||||
can_override = (
|
||||
self.env.user.has_group('fusion_plating.group_fusion_plating_manager')
|
||||
or self.env.user.has_group('fusion_plating.group_fusion_plating_administrator')
|
||||
)
|
||||
if (getattr(commercial, 'x_fc_account_hold', False)
|
||||
and not self.env.context.get('fp_skip_account_hold')
|
||||
and not self.env.user.has_group(
|
||||
'fusion_plating.group_fusion_plating_manager')):
|
||||
and not can_override):
|
||||
raise UserError(_(
|
||||
'Customer %s is on account hold. Have a manager clear the '
|
||||
'hold (or override) before creating the order.'
|
||||
) % self.partner_id.display_name)
|
||||
) % commercial.display_name)
|
||||
|
||||
# Accept EITHER a PO (document + number) OR the PO Pending
|
||||
# flag. Customers who haven't sent paperwork yet use Pending;
|
||||
@@ -535,10 +550,14 @@ class FpDirectOrderWizard(models.Model):
|
||||
for line in self.line_ids:
|
||||
part = line._get_or_bump_revision()
|
||||
resolved_parts[line.id] = part
|
||||
# Build the line header. Primary treatment is optional now;
|
||||
# when missing, drop it from the header rather than printing
|
||||
# "False - PartName Rev A".
|
||||
treatment_label = line.coating_config_id.name or _('No coating')
|
||||
header = '%s - %s Rev %s (x%d)' % (
|
||||
line.coating_config_id.name,
|
||||
treatment_label,
|
||||
part.name,
|
||||
part.revision or part.revision_number,
|
||||
part.revision,
|
||||
line.quantity,
|
||||
)
|
||||
extended = (line.line_description or '').strip()
|
||||
|
||||
@@ -154,7 +154,8 @@
|
||||
optional="hide"/>
|
||||
<field name="internal_description"
|
||||
optional="hide"/>
|
||||
<field name="coating_config_id"/>
|
||||
<field name="coating_config_id"
|
||||
optional="show"/>
|
||||
<field name="process_variant_id"
|
||||
string="Process / Recipe"
|
||||
options="{'no_quick_create': True}"
|
||||
@@ -196,10 +197,12 @@
|
||||
<field name="treatment_ids"
|
||||
widget="many2many_tags"
|
||||
optional="hide"/>
|
||||
<field name="quantity"/>
|
||||
<field name="quantity"
|
||||
optional="show"/>
|
||||
<field name="unit_price"
|
||||
widget="monetary"
|
||||
options="{'currency_field': 'currency_id'}"/>
|
||||
options="{'currency_field': 'currency_id'}"
|
||||
optional="show"/>
|
||||
<field name="tax_ids"
|
||||
widget="many2many_tags"
|
||||
options="{'no_create': True}"
|
||||
|
||||
@@ -22,7 +22,6 @@ CSV_COLUMNS = [
|
||||
'name', # required
|
||||
'customer', # required unless wizard.partner_id set
|
||||
'revision',
|
||||
'revision_number',
|
||||
'substrate_material',
|
||||
'surface_area',
|
||||
'surface_area_uom',
|
||||
@@ -266,7 +265,6 @@ class FpPartCatalogImportWizard(models.TransientModel):
|
||||
'part_number': part_number,
|
||||
'name': name,
|
||||
'revision': (row.get('revision') or '').strip() or False,
|
||||
'revision_number': _to_int(row.get('revision_number'), 1),
|
||||
'substrate_material': substrate,
|
||||
'surface_area': _to_float(row.get('surface_area')),
|
||||
'surface_area_uom': uom,
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
# -*- 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, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
from ..models.fp_part_catalog import _bump_revision_label
|
||||
|
||||
|
||||
class FpPartRevisionBumpWizard(models.TransientModel):
|
||||
"""Interactive wizard for creating a new revision of a part.
|
||||
|
||||
Replaces the old "click Create New Revision and immediately get a
|
||||
half-blank form" UX. Now the user supplies the revision label,
|
||||
revision note, and optionally a new drawing file BEFORE the
|
||||
revision is created. The wizard pre-fills a best-effort label
|
||||
via ``_bump_revision_label`` so the common case (A → B, A1 → A2,
|
||||
Rev 1 → Rev 2, ECO-2024-014 → ECO-2024-015) is one click.
|
||||
"""
|
||||
_name = 'fp.part.revision.bump.wizard'
|
||||
_description = 'Create Part Revision Wizard'
|
||||
|
||||
part_id = fields.Many2one(
|
||||
'fp.part.catalog', string='Source Part', required=True,
|
||||
readonly=True, ondelete='cascade',
|
||||
)
|
||||
current_revision = fields.Char(
|
||||
string='Current Revision', related='part_id.revision', readonly=True,
|
||||
)
|
||||
new_revision = fields.Char(
|
||||
string='New Revision', required=True,
|
||||
help="The revision label for the new copy. Pre-filled with a "
|
||||
"best-effort guess; edit to match the customer's actual "
|
||||
"revision scheme (A/B/C, A1/A2, Rev 2, ECO-2024-015, etc.).",
|
||||
)
|
||||
revision_note = fields.Char(
|
||||
string='Revision Note',
|
||||
help='What changed in this revision? (e.g. "Updated tolerance on '
|
||||
'feature B per ECN-2024-014".)',
|
||||
)
|
||||
revision_date = fields.Datetime(
|
||||
string='Revision Date', default=fields.Datetime.now, required=True,
|
||||
)
|
||||
new_drawing_file = fields.Binary(
|
||||
string='New Drawing (PDF, optional)',
|
||||
help='Drop a PDF drawing here. It will be added to the new '
|
||||
'revision\'s drawing list. Leave empty to inherit the '
|
||||
'existing drawings.',
|
||||
)
|
||||
new_drawing_filename = fields.Char(string='Drawing Filename')
|
||||
new_model_file = fields.Binary(
|
||||
string='New 3D Model (STEP/STL/IGES, optional)',
|
||||
help='Drop a STEP, STP, STL, IGES, IGS, BREP, or BRP file here. '
|
||||
'Replaces the 3D model on the new revision. Leave empty to '
|
||||
'inherit the existing 3D model.',
|
||||
)
|
||||
new_model_filename = fields.Char(string='Model Filename')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Defaults
|
||||
# ------------------------------------------------------------------
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
vals = super().default_get(fields_list)
|
||||
# Resolve part_id from context (button passes active_id).
|
||||
part_id = vals.get('part_id') or self.env.context.get('default_part_id') \
|
||||
or self.env.context.get('active_id')
|
||||
if part_id and self.env.context.get('active_model') in (
|
||||
'fp.part.catalog', None,
|
||||
):
|
||||
part = self.env['fp.part.catalog'].browse(part_id)
|
||||
if part.exists():
|
||||
vals['part_id'] = part.id
|
||||
if 'new_revision' in fields_list and not vals.get('new_revision'):
|
||||
vals['new_revision'] = _bump_revision_label(part.revision or '')
|
||||
return vals
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Validation
|
||||
# ------------------------------------------------------------------
|
||||
@api.constrains('new_revision', 'part_id')
|
||||
def _check_new_revision_unique(self):
|
||||
for wiz in self:
|
||||
label = (wiz.new_revision or '').strip()
|
||||
if not label:
|
||||
raise ValidationError(_('New revision label cannot be empty.'))
|
||||
if not wiz.part_id:
|
||||
continue
|
||||
if label == (wiz.part_id.revision or '').strip():
|
||||
raise ValidationError(_(
|
||||
'New revision label must differ from the current '
|
||||
'revision (%s).'
|
||||
) % wiz.part_id.revision)
|
||||
# Uniqueness within the part chain (same root + same label).
|
||||
root = wiz.part_id.parent_part_id or wiz.part_id
|
||||
sibling = self.env['fp.part.catalog'].search([
|
||||
'|',
|
||||
('id', '=', root.id),
|
||||
('parent_part_id', '=', root.id),
|
||||
('revision', '=', label),
|
||||
], limit=1)
|
||||
if sibling:
|
||||
raise ValidationError(_(
|
||||
'A revision "%(rev)s" already exists for this part '
|
||||
'(part %(pn)s). Pick a different label.'
|
||||
) % {'rev': label, 'pn': wiz.part_id.part_number or ''})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Action
|
||||
# ------------------------------------------------------------------
|
||||
def action_create_revision(self):
|
||||
"""Create the new revision and navigate to it."""
|
||||
self.ensure_one()
|
||||
part = self.part_id
|
||||
if not part:
|
||||
raise UserError(_('No source part selected.'))
|
||||
|
||||
new_label = (self.new_revision or '').strip()
|
||||
part.is_latest_revision = False
|
||||
root = part.parent_part_id or part
|
||||
|
||||
new_part = part.copy({
|
||||
'revision': new_label,
|
||||
'revision_date': self.revision_date or fields.Datetime.now(),
|
||||
'revision_note': self.revision_note or False,
|
||||
'parent_part_id': root.id,
|
||||
'is_latest_revision': True,
|
||||
'model_attachment_id': part.model_attachment_id.id,
|
||||
})
|
||||
|
||||
# Optional new PDF drawing — appended to the drawing list.
|
||||
if self.new_drawing_file:
|
||||
drawing_att = self.env['ir.attachment'].create({
|
||||
'name': self.new_drawing_filename or 'drawing.pdf',
|
||||
'datas': self.new_drawing_file,
|
||||
'res_model': 'fp.part.catalog',
|
||||
'res_id': new_part.id,
|
||||
})
|
||||
new_part.drawing_attachment_ids = [(4, drawing_att.id)]
|
||||
|
||||
# Optional new 3D model — replaces the model attachment.
|
||||
if self.new_model_file:
|
||||
model_att = self.env['ir.attachment'].create({
|
||||
'name': self.new_model_filename or 'model.step',
|
||||
'datas': self.new_model_file,
|
||||
'res_model': 'fp.part.catalog',
|
||||
'res_id': new_part.id,
|
||||
})
|
||||
new_part.model_attachment_id = model_att.id
|
||||
|
||||
new_part.message_post(body=_(
|
||||
'Revision %(new)s created from %(old)s. %(note)s'
|
||||
) % {
|
||||
'new': new_label,
|
||||
'old': part.revision or '',
|
||||
'note': self.revision_note or '',
|
||||
})
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Part Revision'),
|
||||
'res_model': 'fp.part.catalog',
|
||||
'res_id': new_part.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2026 Nexa Systems Inc.
|
||||
License OPL-1 (Odoo Proprietary License v1.0)
|
||||
Part of the Fusion Plating product family.
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_fp_part_revision_bump_wizard_form" model="ir.ui.view">
|
||||
<field name="name">fp.part.revision.bump.wizard.form</field>
|
||||
<field name="model">fp.part.revision.bump.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Create New Revision">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h2>Create New Revision</h2>
|
||||
<p class="text-muted">
|
||||
Bump the revision label for
|
||||
<strong><field name="part_id" readonly="1" nolabel="1" options="{'no_open': True}"/></strong>.
|
||||
The pre-filled label is a best-effort guess —
|
||||
adjust it to match the customer's actual scheme.
|
||||
</p>
|
||||
</div>
|
||||
<group>
|
||||
<group string="Revision">
|
||||
<field name="current_revision"/>
|
||||
<field name="new_revision" placeholder="e.g. B, A2, Rev 2, ECO-2024-015"/>
|
||||
<field name="revision_date"/>
|
||||
</group>
|
||||
<group string="Details">
|
||||
<field name="revision_note"
|
||||
placeholder="What changed? (e.g. tolerance update on feature B)"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Updated Files (optional)">
|
||||
<group string="2D Drawing (PDF)">
|
||||
<field name="new_drawing_filename" invisible="1"/>
|
||||
<field name="new_drawing_file"
|
||||
filename="new_drawing_filename"
|
||||
widget="binary"
|
||||
nolabel="1"/>
|
||||
<div class="text-muted small">
|
||||
Added to the new revision's drawing list.
|
||||
Leave empty to inherit the current drawings.
|
||||
</div>
|
||||
</group>
|
||||
<group string="3D Model (STEP/STL/IGES)">
|
||||
<field name="new_model_filename" invisible="1"/>
|
||||
<field name="new_model_file"
|
||||
filename="new_model_filename"
|
||||
widget="binary"
|
||||
nolabel="1"/>
|
||||
<div class="text-muted small">
|
||||
Replaces the 3D model on the new revision.
|
||||
Leave empty to inherit the current model.
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_create_revision"
|
||||
string="Create Revision"
|
||||
type="object"
|
||||
class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fp_part_revision_bump_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Create New Revision</field>
|
||||
<field name="res_model">fp.part.revision.bump.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user