changes
This commit is contained in:
@@ -10,3 +10,4 @@ from . import fp_pricing_complexity_surcharge
|
||||
from . import fp_pricing_rule
|
||||
from . import fp_quote_configurator
|
||||
from . import sale_order
|
||||
from . import res_partner
|
||||
|
||||
@@ -25,6 +25,18 @@ class FpPartCatalog(models.Model):
|
||||
)
|
||||
part_number = fields.Char(string='Part Number', tracking=True, help="Customer's part number (e.g. VS-R392007E01).")
|
||||
revision = fields.Char(string='Revision', help='Revision letter or number (e.g. Rev: 1B).')
|
||||
revision_number = fields.Integer(string='Rev #', default=1)
|
||||
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(
|
||||
'fp.part.catalog', string='Original Part',
|
||||
ondelete='set null', index=True,
|
||||
help='Links to the original part for revision history.',
|
||||
)
|
||||
is_latest_revision = fields.Boolean(string='Latest Revision', default=True)
|
||||
revision_ids = fields.One2many(
|
||||
'fp.part.catalog', 'parent_part_id', string='Revision History',
|
||||
)
|
||||
substrate_material = fields.Selection(
|
||||
[('aluminium', 'Aluminium'), ('steel', 'Steel'), ('stainless', 'Stainless Steel'),
|
||||
('copper', 'Copper'), ('titanium', 'Titanium'), ('other', 'Other')],
|
||||
@@ -121,6 +133,35 @@ class FpPartCatalog(models.Model):
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def action_create_revision(self):
|
||||
"""Create a new revision of this part. Copies all data, increments revision number."""
|
||||
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_rev = self.copy({
|
||||
'revision_number': max_rev + 1,
|
||||
'revision': f'Rev {max_rev + 1}',
|
||||
'revision_date': fields.Datetime.now(),
|
||||
'revision_note': False,
|
||||
'parent_part_id': root.id,
|
||||
'is_latest_revision': True,
|
||||
'model_attachment_id': self.model_attachment_id.id,
|
||||
})
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'fp.part.catalog',
|
||||
'res_id': new_rev.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
@api.onchange('model_attachment_id')
|
||||
def _onchange_model_attachment_id(self):
|
||||
"""Auto-calculate surface area when a 3D model is attached."""
|
||||
|
||||
@@ -387,16 +387,38 @@ class FpQuoteConfigurator(models.Model):
|
||||
'mimetype': 'application/octet-stream',
|
||||
})
|
||||
|
||||
# Auto-create or update part catalog
|
||||
# Auto-create or update part catalog with revision tracking
|
||||
part_name = os.path.splitext(fname)[0].replace('_', ' ').replace('-', ' ').title()
|
||||
if self.part_catalog_id:
|
||||
# Update existing part
|
||||
if self.part_catalog_id and self.part_catalog_id.model_attachment_id:
|
||||
# Part already has a 3D model — create a new revision
|
||||
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])
|
||||
new_part = old_part.copy({
|
||||
'revision_number': max_rev + 1,
|
||||
'revision': f'Rev {max_rev + 1}',
|
||||
'revision_date': fields.Datetime.now(),
|
||||
'revision_note': f'Updated 3D model: {fname}',
|
||||
'parent_part_id': root.id,
|
||||
'is_latest_revision': True,
|
||||
'model_attachment_id': att.id,
|
||||
})
|
||||
self.part_catalog_id = new_part.id
|
||||
new_part._compute_surface_area_from_model()
|
||||
self.surface_area = new_part.surface_area
|
||||
self.surface_area_uom = new_part.surface_area_uom
|
||||
elif self.part_catalog_id:
|
||||
# Part exists but no 3D model yet — just attach
|
||||
self.part_catalog_id.model_attachment_id = att.id
|
||||
self.part_catalog_id._compute_surface_area_from_model()
|
||||
self.surface_area = self.part_catalog_id.surface_area
|
||||
self.surface_area_uom = self.part_catalog_id.surface_area_uom
|
||||
else:
|
||||
# Create new part catalog entry
|
||||
# No part catalog — create new entry
|
||||
part = self.env['fp.part.catalog'].create({
|
||||
'name': part_name,
|
||||
'partner_id': self.partner_id.id,
|
||||
@@ -404,7 +426,6 @@ class FpQuoteConfigurator(models.Model):
|
||||
'model_attachment_id': att.id,
|
||||
})
|
||||
self.part_catalog_id = part.id
|
||||
# Calculate surface area
|
||||
part._compute_surface_area_from_model()
|
||||
self.surface_area = part.surface_area
|
||||
self.surface_area_uom = part.surface_area_uom
|
||||
@@ -446,13 +467,26 @@ class FpQuoteConfigurator(models.Model):
|
||||
def action_recalculate_price(self):
|
||||
"""Recalculate surface area from 3D model and recompute price."""
|
||||
self.ensure_one()
|
||||
msg = _('No 3D model to calculate from.')
|
||||
# Recalculate surface area from part catalog's 3D model
|
||||
if self.part_catalog_id and self.part_catalog_id.model_attachment_id:
|
||||
result = self.part_catalog_id._compute_surface_area_from_model()
|
||||
if not result.get('error'):
|
||||
self.surface_area = self.part_catalog_id.surface_area
|
||||
self.surface_area_uom = self.part_catalog_id.surface_area_uom
|
||||
# Price recomputes automatically via _compute_price dependency
|
||||
msg = _('Surface area: %.4f %s | Price: %.2f %s') % (
|
||||
self.surface_area, self.surface_area_uom or '',
|
||||
self.calculated_price, self.currency_id.symbol or '$',
|
||||
)
|
||||
else:
|
||||
msg = result['error']
|
||||
# Post result to chatter so user sees it after form reload
|
||||
self.message_post(
|
||||
body=msg,
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return False
|
||||
|
||||
def action_cancel(self):
|
||||
self.write({'state': 'cancelled'})
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
x_fc_part_catalog_ids = fields.One2many(
|
||||
'fp.part.catalog', 'partner_id',
|
||||
string='Part Catalog',
|
||||
)
|
||||
x_fc_part_count = fields.Integer(
|
||||
string='Parts',
|
||||
compute='_compute_part_count',
|
||||
)
|
||||
|
||||
def _compute_part_count(self):
|
||||
for partner in self:
|
||||
partner.x_fc_part_count = self.env['fp.part.catalog'].search_count([
|
||||
('partner_id', '=', partner.id),
|
||||
('is_latest_revision', '=', True),
|
||||
])
|
||||
|
||||
def action_view_parts(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': f'Parts — {self.name}',
|
||||
'res_model': 'fp.part.catalog',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('partner_id', '=', self.id), ('is_latest_revision', '=', True)],
|
||||
'context': {'default_partner_id': self.id},
|
||||
'target': 'current',
|
||||
}
|
||||
Reference in New Issue
Block a user