changes
This commit is contained in:
@@ -97,14 +97,13 @@ class FpPartCatalog(models.Model):
|
|||||||
help='False indicates open/broken geometry — review before quoting.',
|
help='False indicates open/broken geometry — review before quoting.',
|
||||||
)
|
)
|
||||||
hole_count = fields.Integer(
|
hole_count = fields.Integer(
|
||||||
string='Cylindrical Features',
|
string='Holes',
|
||||||
help='Total cylindrical surfaces detected (includes holes, bores, '
|
help='Real cylindrical holes/bosses detected (faces summing to ≥350° '
|
||||||
'and rounded fillets — see breakdown for diameter clustering).',
|
'around an axis). Fillets and rounded edges are excluded.',
|
||||||
)
|
)
|
||||||
hole_summary = fields.Char(
|
hole_summary = fields.Char(
|
||||||
string='Feature Diameters',
|
string='Hole Diameters',
|
||||||
help='Cylindrical features grouped by diameter (rounded to 0.5mm). '
|
help='Holes grouped by diameter — e.g. "4× Ø10.2mm, 2× Ø7.9mm".',
|
||||||
'Small diameters are usually edge fillets; larger ones are typically holes.',
|
|
||||||
)
|
)
|
||||||
masking_area_sqin = fields.Float(
|
masking_area_sqin = fields.Float(
|
||||||
string='Masking Area (sq in)', digits=(12, 4),
|
string='Masking Area (sq in)', digits=(12, 4),
|
||||||
@@ -440,12 +439,13 @@ class FpPartCatalog(models.Model):
|
|||||||
except Exception:
|
except Exception:
|
||||||
is_manifold = None
|
is_manifold = None
|
||||||
|
|
||||||
# Cylindrical feature detection — counts holes, fillets, bores.
|
# Hole detection: group cylindrical faces by axis LINE + radius,
|
||||||
# NOTE: fillets/rounded edges also appear as cylinders. We
|
# sum U-parameter ranges. Faces summing to ≥350° form a full
|
||||||
# cluster by diameter (rounded to 0.5 mm) and report the
|
# cylinder = real hole or boss. Partial cylinders are fillets/
|
||||||
# breakdown so the estimator can identify real holes vs fillets.
|
# rounded edges and are excluded.
|
||||||
try:
|
try:
|
||||||
diameters = []
|
import math
|
||||||
|
groups = {} # key -> [total_u_radians, radius]
|
||||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
||||||
while explorer.More():
|
while explorer.More():
|
||||||
try:
|
try:
|
||||||
@@ -453,22 +453,53 @@ class FpPartCatalog(models.Model):
|
|||||||
surf = BRepAdaptor_Surface(face)
|
surf = BRepAdaptor_Surface(face)
|
||||||
if surf.GetType() == GeomAbs_Cylinder:
|
if surf.GetType() == GeomAbs_Cylinder:
|
||||||
cyl = surf.Cylinder()
|
cyl = surf.Cylinder()
|
||||||
diameter_mm = cyl.Radius() * 2.0
|
axis = cyl.Axis()
|
||||||
# Round to 0.5 mm clusters (fillets ~0.5-3mm, holes ~3mm+)
|
loc = axis.Location()
|
||||||
diameters.append(round(diameter_mm * 2) / 2)
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
explorer.Next()
|
explorer.Next()
|
||||||
if diameters:
|
|
||||||
from collections import Counter
|
# Keep only full cylinders (≥ 350° covered by grouped faces)
|
||||||
counts = Counter(diameters)
|
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())
|
hole_count = sum(counts.values())
|
||||||
# Sort by diameter ascending, format
|
|
||||||
parts = ['%d× Ø%.1fmm' % (n, d)
|
parts = ['%d× Ø%.1fmm' % (n, d)
|
||||||
for d, n in sorted(counts.items())]
|
for d, n in sorted(counts.items())]
|
||||||
hole_summary = ', '.join(parts)
|
hole_summary = ', '.join(parts)
|
||||||
except Exception as he:
|
except Exception as he:
|
||||||
_logger.warning('Cylindrical feature detection failed: %s', he)
|
_logger.warning('Hole detection failed: %s', he)
|
||||||
|
|
||||||
method = 'occ_brep'
|
method = 'occ_brep'
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -45,13 +45,13 @@ class FpQuoteConfigurator(models.Model):
|
|||||||
string='Drawings',
|
string='Drawings',
|
||||||
readonly=True,
|
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(
|
bbox_summary_in = fields.Char(
|
||||||
related='part_catalog_id.bbox_summary_in', string='Dimensions (in)',
|
related='part_catalog_id.bbox_summary_in', string='Dimensions (in)',
|
||||||
readonly=True,
|
readonly=True,
|
||||||
)
|
)
|
||||||
material_weight_kg = fields.Float(
|
volume_mm3 = fields.Float(
|
||||||
related='part_catalog_id.material_weight_kg', string='Weight (kg)',
|
related='part_catalog_id.volume_mm3', string='Volume (mm³)',
|
||||||
readonly=True,
|
readonly=True,
|
||||||
)
|
)
|
||||||
hole_count = fields.Integer(
|
hole_count = fields.Integer(
|
||||||
@@ -66,14 +66,67 @@ class FpQuoteConfigurator(models.Model):
|
|||||||
related='part_catalog_id.is_manifold', string='Watertight',
|
related='part_catalog_id.is_manifold', string='Watertight',
|
||||||
readonly=True,
|
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(
|
masking_area_sqin = fields.Float(
|
||||||
related='part_catalog_id.masking_area_sqin', string='Masking Area (sq in)',
|
string='Masking Area (sq in)',
|
||||||
readonly=False, # allow editing via configurator
|
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(
|
effective_area_sqin = fields.Float(
|
||||||
related='part_catalog_id.effective_area_sqin', string='Effective Area (sq in)',
|
string='Effective Plating Area (sq in)',
|
||||||
readonly=True,
|
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(
|
drawing_count = fields.Integer(
|
||||||
string='Drawings',
|
string='Drawings',
|
||||||
compute='_compute_drawing_count',
|
compute='_compute_drawing_count',
|
||||||
@@ -227,6 +280,8 @@ class FpQuoteConfigurator(models.Model):
|
|||||||
self.complexity = cat.complexity
|
self.complexity = cat.complexity
|
||||||
self.masking_zones = cat.masking_zones
|
self.masking_zones = cat.masking_zones
|
||||||
self.substrate_material = cat.substrate_material
|
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')
|
@api.onchange('coating_config_id')
|
||||||
def _onchange_coating_config_id(self):
|
def _onchange_coating_config_id(self):
|
||||||
@@ -714,6 +769,40 @@ class FpQuoteConfigurator(models.Model):
|
|||||||
'target': 'current',
|
'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):
|
def action_view_drawings(self):
|
||||||
"""Open the first drawing in the PDF preview dialog (matches RFQ/PO behavior)."""
|
"""Open the first drawing in the PDF preview dialog (matches RFQ/PO behavior)."""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|||||||
@@ -23,6 +23,12 @@
|
|||||||
string="Recalculate"
|
string="Recalculate"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn-secondary"/>
|
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"
|
<button name="action_cancel"
|
||||||
string="Cancel"
|
string="Cancel"
|
||||||
type="object"
|
type="object"
|
||||||
@@ -162,7 +168,7 @@
|
|||||||
string="Weight (kg)"
|
string="Weight (kg)"
|
||||||
readonly="1"/>
|
readonly="1"/>
|
||||||
<field name="hole_count"
|
<field name="hole_count"
|
||||||
string="Holes Detected"
|
string="Holes"
|
||||||
readonly="1"/>
|
readonly="1"/>
|
||||||
<field name="hole_summary"
|
<field name="hole_summary"
|
||||||
readonly="1"
|
readonly="1"
|
||||||
|
|||||||
Reference in New Issue
Block a user