changes
This commit is contained in:
@@ -97,14 +97,13 @@ class FpPartCatalog(models.Model):
|
||||
help='False indicates open/broken geometry — review before quoting.',
|
||||
)
|
||||
hole_count = fields.Integer(
|
||||
string='Cylindrical Features',
|
||||
help='Total cylindrical surfaces detected (includes holes, bores, '
|
||||
'and rounded fillets — see breakdown for diameter clustering).',
|
||||
string='Holes',
|
||||
help='Real cylindrical holes/bosses detected (faces summing to ≥350° '
|
||||
'around an axis). Fillets and rounded edges are excluded.',
|
||||
)
|
||||
hole_summary = fields.Char(
|
||||
string='Feature Diameters',
|
||||
help='Cylindrical features grouped by diameter (rounded to 0.5mm). '
|
||||
'Small diameters are usually edge fillets; larger ones are typically holes.',
|
||||
string='Hole Diameters',
|
||||
help='Holes grouped by diameter — e.g. "4× Ø10.2mm, 2× Ø7.9mm".',
|
||||
)
|
||||
masking_area_sqin = fields.Float(
|
||||
string='Masking Area (sq in)', digits=(12, 4),
|
||||
@@ -440,12 +439,13 @@ class FpPartCatalog(models.Model):
|
||||
except Exception:
|
||||
is_manifold = None
|
||||
|
||||
# Cylindrical feature detection — counts holes, fillets, bores.
|
||||
# NOTE: fillets/rounded edges also appear as cylinders. We
|
||||
# cluster by diameter (rounded to 0.5 mm) and report the
|
||||
# breakdown so the estimator can identify real holes vs fillets.
|
||||
# Hole detection: group cylindrical faces by axis LINE + radius,
|
||||
# sum U-parameter ranges. Faces summing to ≥350° form a full
|
||||
# cylinder = real hole or boss. Partial cylinders are fillets/
|
||||
# rounded edges and are excluded.
|
||||
try:
|
||||
diameters = []
|
||||
import math
|
||||
groups = {} # key -> [total_u_radians, radius]
|
||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
while explorer.More():
|
||||
try:
|
||||
@@ -453,22 +453,53 @@ class FpPartCatalog(models.Model):
|
||||
surf = BRepAdaptor_Surface(face)
|
||||
if surf.GetType() == GeomAbs_Cylinder:
|
||||
cyl = surf.Cylinder()
|
||||
diameter_mm = cyl.Radius() * 2.0
|
||||
# Round to 0.5 mm clusters (fillets ~0.5-3mm, holes ~3mm+)
|
||||
diameters.append(round(diameter_mm * 2) / 2)
|
||||
axis = cyl.Axis()
|
||||
loc = axis.Location()
|
||||
d = axis.Direction()
|
||||
u_range = abs(
|
||||
surf.LastUParameter() - surf.FirstUParameter()
|
||||
)
|
||||
r = cyl.Radius()
|
||||
# Canonical axis point: project loc onto plane through origin
|
||||
# perpendicular to direction (unique per axis line)
|
||||
dx, dy, dz = d.X(), d.Y(), d.Z()
|
||||
lx, ly, lz = loc.X(), loc.Y(), loc.Z()
|
||||
t = lx * dx + ly * dy + lz * dz
|
||||
cx = lx - t * dx
|
||||
cy = ly - t * dy
|
||||
cz = lz - t * dz
|
||||
# Normalize direction sign (avoid +/- dedup)
|
||||
if dz < 0 or (dz == 0 and dy < 0) or (
|
||||
dz == 0 and dy == 0 and dx < 0
|
||||
):
|
||||
dx, dy, dz = -dx, -dy, -dz
|
||||
key = (
|
||||
round(r, 1),
|
||||
round(cx, 1), round(cy, 1), round(cz, 1),
|
||||
round(dx, 2), round(dy, 2), round(dz, 2),
|
||||
)
|
||||
if key in groups:
|
||||
groups[key][0] += u_range
|
||||
else:
|
||||
groups[key] = [u_range, r]
|
||||
except Exception:
|
||||
pass
|
||||
explorer.Next()
|
||||
if diameters:
|
||||
from collections import Counter
|
||||
counts = Counter(diameters)
|
||||
|
||||
# Keep only full cylinders (≥ 350° covered by grouped faces)
|
||||
from collections import Counter
|
||||
hole_diameters = []
|
||||
for (total_u, r) in groups.values():
|
||||
if math.degrees(total_u) >= 350:
|
||||
hole_diameters.append(round(r * 2, 1))
|
||||
if hole_diameters:
|
||||
counts = Counter(hole_diameters)
|
||||
hole_count = sum(counts.values())
|
||||
# Sort by diameter ascending, format
|
||||
parts = ['%d× Ø%.1fmm' % (n, d)
|
||||
for d, n in sorted(counts.items())]
|
||||
hole_summary = ', '.join(parts)
|
||||
except Exception as he:
|
||||
_logger.warning('Cylindrical feature detection failed: %s', he)
|
||||
_logger.warning('Hole detection failed: %s', he)
|
||||
|
||||
method = 'occ_brep'
|
||||
finally:
|
||||
|
||||
@@ -45,13 +45,13 @@ class FpQuoteConfigurator(models.Model):
|
||||
string='Drawings',
|
||||
readonly=True,
|
||||
)
|
||||
# -- Auto-extracted geometry from part catalog (read-only on configurator) --
|
||||
# -- Physical part properties (intrinsic, related from part catalog) --
|
||||
bbox_summary_in = fields.Char(
|
||||
related='part_catalog_id.bbox_summary_in', string='Dimensions (in)',
|
||||
readonly=True,
|
||||
)
|
||||
material_weight_kg = fields.Float(
|
||||
related='part_catalog_id.material_weight_kg', string='Weight (kg)',
|
||||
volume_mm3 = fields.Float(
|
||||
related='part_catalog_id.volume_mm3', string='Volume (mm³)',
|
||||
readonly=True,
|
||||
)
|
||||
hole_count = fields.Integer(
|
||||
@@ -66,14 +66,67 @@ class FpQuoteConfigurator(models.Model):
|
||||
related='part_catalog_id.is_manifold', string='Watertight',
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
# -- Quote-editable fields that drive weight / effective area --
|
||||
# These are independent from part catalog (working copy for this quote)
|
||||
masking_area_sqin = fields.Float(
|
||||
related='part_catalog_id.masking_area_sqin', string='Masking Area (sq in)',
|
||||
readonly=False, # allow editing via configurator
|
||||
string='Masking Area (sq in)',
|
||||
digits=(12, 4),
|
||||
help='Surface area excluded from plating (masked surfaces).',
|
||||
)
|
||||
# Computed using CONFIGURATOR's substrate + part catalog's volume
|
||||
# so changing substrate on the quote updates the weight live.
|
||||
material_weight_kg = fields.Float(
|
||||
string='Weight (kg)',
|
||||
digits=(12, 4),
|
||||
compute='_compute_material_weight_kg',
|
||||
store=False,
|
||||
help='Computed from part volume × this quote\'s substrate density. '
|
||||
'Changing substrate on the quote updates weight immediately.',
|
||||
)
|
||||
# Computed using CONFIGURATOR's surface_area and masking_area
|
||||
effective_area_sqin = fields.Float(
|
||||
related='part_catalog_id.effective_area_sqin', string='Effective Area (sq in)',
|
||||
readonly=True,
|
||||
string='Effective Plating Area (sq in)',
|
||||
digits=(12, 4),
|
||||
compute='_compute_effective_area_sqin',
|
||||
store=False,
|
||||
help='Surface area minus masked area, using the values on this quote.',
|
||||
)
|
||||
|
||||
@api.depends('volume_mm3', 'substrate_material')
|
||||
def _compute_material_weight_kg(self):
|
||||
"""Compute weight from part volume × THIS QUOTE'S substrate density."""
|
||||
density_map = {
|
||||
'aluminium': 2.70,
|
||||
'steel': 7.85,
|
||||
'stainless': 8.00,
|
||||
'copper': 8.96,
|
||||
'titanium': 4.51,
|
||||
'other': 7.85,
|
||||
}
|
||||
for rec in self:
|
||||
if not rec.volume_mm3 or not rec.substrate_material:
|
||||
rec.material_weight_kg = 0.0
|
||||
continue
|
||||
density = density_map.get(rec.substrate_material, 7.85)
|
||||
rec.material_weight_kg = round(rec.volume_mm3 * density * 1e-6, 4)
|
||||
|
||||
@api.depends('surface_area', 'surface_area_uom', 'masking_area_sqin')
|
||||
def _compute_effective_area_sqin(self):
|
||||
"""Surface area minus masking area, using THIS QUOTE'S values."""
|
||||
for rec in self:
|
||||
uom = rec.surface_area_uom or 'sq_in'
|
||||
if uom == 'sq_in':
|
||||
area_sqin = rec.surface_area or 0.0
|
||||
elif uom == 'sq_ft':
|
||||
area_sqin = (rec.surface_area or 0.0) * 144.0
|
||||
elif uom == 'sq_cm':
|
||||
area_sqin = (rec.surface_area or 0.0) / 6.4516
|
||||
elif uom == 'sq_m':
|
||||
area_sqin = (rec.surface_area or 0.0) * 1550.0
|
||||
else:
|
||||
area_sqin = rec.surface_area or 0.0
|
||||
rec.effective_area_sqin = max(0.0, area_sqin - (rec.masking_area_sqin or 0.0))
|
||||
drawing_count = fields.Integer(
|
||||
string='Drawings',
|
||||
compute='_compute_drawing_count',
|
||||
@@ -227,6 +280,8 @@ class FpQuoteConfigurator(models.Model):
|
||||
self.complexity = cat.complexity
|
||||
self.masking_zones = cat.masking_zones
|
||||
self.substrate_material = cat.substrate_material
|
||||
# Copy masking area too (for effective-area calculation)
|
||||
self.masking_area_sqin = cat.masking_area_sqin
|
||||
|
||||
@api.onchange('coating_config_id')
|
||||
def _onchange_coating_config_id(self):
|
||||
@@ -714,6 +769,40 @@ class FpQuoteConfigurator(models.Model):
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def action_save_to_catalog(self):
|
||||
"""Push this quote's geometry/material edits back to the master part catalog.
|
||||
|
||||
Writes: substrate_material, surface_area, surface_area_uom,
|
||||
masking_area_sqin, masking_zones, complexity.
|
||||
Only available when a part catalog entry is linked.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self.part_catalog_id:
|
||||
raise UserError(_('No part catalog entry linked to this configurator.'))
|
||||
self.part_catalog_id.write({
|
||||
'substrate_material': self.substrate_material,
|
||||
'surface_area': self.surface_area,
|
||||
'surface_area_uom': self.surface_area_uom,
|
||||
'masking_area_sqin': self.masking_area_sqin,
|
||||
'masking_zones': self.masking_zones,
|
||||
'complexity': self.complexity,
|
||||
})
|
||||
self.message_post(
|
||||
body=_('Geometry and material saved back to part catalog <b>%s</b>.') % self.part_catalog_id.name,
|
||||
message_type='notification',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Saved to Catalog'),
|
||||
'message': _('Part catalog updated with quote geometry and substrate.'),
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
|
||||
def action_view_drawings(self):
|
||||
"""Open the first drawing in the PDF preview dialog (matches RFQ/PO behavior)."""
|
||||
self.ensure_one()
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
string="Recalculate"
|
||||
type="object"
|
||||
class="btn-secondary"/>
|
||||
<button name="action_save_to_catalog"
|
||||
string="Save to Catalog"
|
||||
type="object"
|
||||
class="btn-secondary"
|
||||
confirm="This will overwrite the part catalog's geometry, substrate, masking area, and complexity with values from this quote. Continue?"
|
||||
invisible="not part_catalog_id"/>
|
||||
<button name="action_cancel"
|
||||
string="Cancel"
|
||||
type="object"
|
||||
@@ -162,7 +168,7 @@
|
||||
string="Weight (kg)"
|
||||
readonly="1"/>
|
||||
<field name="hole_count"
|
||||
string="Holes Detected"
|
||||
string="Holes"
|
||||
readonly="1"/>
|
||||
<field name="hole_summary"
|
||||
readonly="1"
|
||||
|
||||
Reference in New Issue
Block a user