changes
This commit is contained in:
@@ -44,6 +44,7 @@ Provides:
|
||||
'views/fp_pricing_rule_views.xml',
|
||||
'views/fp_quote_configurator_views.xml',
|
||||
'views/sale_order_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/fp_configurator_menu.xml',
|
||||
],
|
||||
'assets': {
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
@@ -29,6 +29,13 @@
|
||||
<field name="model">fp.part.catalog</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Part Catalog">
|
||||
<header>
|
||||
<button name="action_create_revision"
|
||||
string="Create New Revision"
|
||||
type="object"
|
||||
class="btn-secondary"
|
||||
icon="fa-code-fork"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_sale_orders"
|
||||
@@ -47,6 +54,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
||||
<widget name="web_ribbon" title="Superseded" bg_color="text-bg-warning" invisible="is_latest_revision"/>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="e.g. Valve Body Housing"/></h1>
|
||||
@@ -56,8 +64,11 @@
|
||||
<group>
|
||||
<field name="partner_id"/>
|
||||
<field name="revision"/>
|
||||
<field name="revision_number"/>
|
||||
<field name="substrate_material"/>
|
||||
<field name="geometry_source"/>
|
||||
<field name="is_latest_revision" invisible="1"/>
|
||||
<field name="parent_part_id" invisible="not parent_part_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<label for="surface_area"/>
|
||||
@@ -101,6 +112,20 @@
|
||||
<field name="model_attachment_id" widget="fp_3d_preview" nolabel="1"/>
|
||||
</div>
|
||||
</page>
|
||||
<page string="Revision History" name="revisions"
|
||||
invisible="not parent_part_id and not revision_ids">
|
||||
<field name="revision_ids" mode="list">
|
||||
<list default_order="revision_number desc">
|
||||
<field name="revision_number" string="Rev #"/>
|
||||
<field name="revision"/>
|
||||
<field name="revision_note"/>
|
||||
<field name="revision_date"/>
|
||||
<field name="surface_area"/>
|
||||
<field name="model_attachment_id" string="3D Model"/>
|
||||
<field name="is_latest_revision" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Notes" name="notes">
|
||||
<field name="notes" placeholder="Additional notes about this part..."/>
|
||||
</page>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_partner_form_fp_parts" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.fp.parts</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_view_parts"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-cube"
|
||||
invisible="x_fc_part_count == 0">
|
||||
<field name="x_fc_part_count" widget="statinfo" string="Parts"/>
|
||||
</button>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//page[@name='sales_purchases']" position="after">
|
||||
<page string="Part Library" name="part_library">
|
||||
<field name="x_fc_part_catalog_ids" mode="list"
|
||||
domain="[('is_latest_revision', '=', True)]">
|
||||
<list default_order="name" >
|
||||
<field name="part_number"/>
|
||||
<field name="name"/>
|
||||
<field name="revision"/>
|
||||
<field name="revision_number" string="Rev #"/>
|
||||
<field name="substrate_material"/>
|
||||
<field name="surface_area"/>
|
||||
<field name="surface_area_uom" string="UoM"/>
|
||||
<field name="complexity"/>
|
||||
<field name="model_attachment_id" string="3D Model"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -123,11 +123,7 @@
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="activity_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
Reference in New Issue
Block a user