diff --git a/fusion-plating/fusion_plating_configurator/__init__.py b/fusion-plating/fusion_plating_configurator/__init__.py
index 3c90fa80..2ea9535e 100644
--- a/fusion-plating/fusion_plating_configurator/__init__.py
+++ b/fusion-plating/fusion_plating_configurator/__init__.py
@@ -3,4 +3,5 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
+from . import controllers
from . import models
diff --git a/fusion-plating/fusion_plating_configurator/controllers/__init__.py b/fusion-plating/fusion_plating_configurator/controllers/__init__.py
new file mode 100644
index 00000000..034d6501
--- /dev/null
+++ b/fusion-plating/fusion_plating_configurator/controllers/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+from . import configurator_controller
diff --git a/fusion-plating/fusion_plating_configurator/controllers/configurator_controller.py b/fusion-plating/fusion_plating_configurator/controllers/configurator_controller.py
new file mode 100644
index 00000000..f5044de0
--- /dev/null
+++ b/fusion-plating/fusion_plating_configurator/controllers/configurator_controller.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Nexa Systems Inc.
+# License OPL-1 (Odoo Proprietary License v1.0)
+# Part of the Fusion Plating product family.
+
+import base64
+import io
+import logging
+
+from odoo import http
+from odoo.http import request
+
+_logger = logging.getLogger(__name__)
+
+
+class FpConfiguratorController(http.Controller):
+
+ @http.route('/fp/configurator/calculate_surface_area', type='jsonrpc', auth='user')
+ def calculate_surface_area(self, attachment_id, **kw):
+ """Calculate surface area from an uploaded STL file using trimesh."""
+ attachment = request.env['ir.attachment'].browse(int(attachment_id))
+ if not attachment.exists():
+ return {'error': 'Attachment not found.'}
+
+ try:
+ import trimesh
+ except ImportError:
+ return {'error': 'trimesh library not installed. Run: pip install trimesh'}
+
+ try:
+ raw = base64.b64decode(attachment.datas)
+ mesh = trimesh.load(io.BytesIO(raw), file_type='stl')
+
+ # trimesh returns area in the file's native units (usually mm²)
+ area_mm2 = mesh.area
+ area_sqin = area_mm2 / 645.16 # mm² to sq in
+
+ return {
+ 'surface_area': round(area_sqin, 4),
+ 'surface_area_mm2': round(area_mm2, 2),
+ 'unit': 'sq_in',
+ 'vertex_count': len(mesh.vertices),
+ 'face_count': len(mesh.faces),
+ 'bounding_box': {
+ 'x': round(float(mesh.bounding_box.extents[0]), 2),
+ 'y': round(float(mesh.bounding_box.extents[1]), 2),
+ 'z': round(float(mesh.bounding_box.extents[2]), 2),
+ },
+ }
+ except Exception as e:
+ _logger.warning('STL surface area calculation failed: %s', e)
+ return {'error': str(e)}
diff --git a/fusion-plating/fusion_plating_configurator/models/fp_part_catalog.py b/fusion-plating/fusion_plating_configurator/models/fp_part_catalog.py
index ec62df29..f4e89a27 100644
--- a/fusion-plating/fusion_plating_configurator/models/fp_part_catalog.py
+++ b/fusion-plating/fusion_plating_configurator/models/fp_part_catalog.py
@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
-from odoo import fields, models
+from odoo import api, fields, models, _
class FpPartCatalog(models.Model):
@@ -63,3 +63,39 @@ class FpPartCatalog(models.Model):
('fp_part_catalog_partner_partnum_uniq', 'unique(partner_id, part_number)',
'Part number must be unique per customer.'),
]
+
+ def action_calculate_surface_area(self):
+ """Calculate surface area from the uploaded 3D model file."""
+ self.ensure_one()
+ if not self.model_attachment_id:
+ from odoo.exceptions import UserError
+ raise UserError(_('No 3D model file uploaded.'))
+
+ try:
+ import trimesh
+ except ImportError:
+ from odoo.exceptions import UserError
+ raise UserError(_('trimesh library not installed on the server. Contact your administrator.'))
+
+ import base64
+ import io
+
+ raw = base64.b64decode(self.model_attachment_id.datas)
+ mesh = trimesh.load(io.BytesIO(raw), file_type='stl')
+ area_mm2 = mesh.area
+ area_sqin = area_mm2 / 645.16
+
+ self.surface_area = round(area_sqin, 4)
+ self.surface_area_uom = 'sq_in'
+ self.geometry_source = '3d_model'
+
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': _('Surface Area Calculated'),
+ 'message': _('%.4f sq in (%.2f mm\u00b2) from %d faces') % (area_sqin, area_mm2, len(mesh.faces)),
+ 'type': 'success',
+ 'sticky': False,
+ },
+ }
diff --git a/fusion-plating/fusion_plating_configurator/views/fp_part_catalog_views.xml b/fusion-plating/fusion_plating_configurator/views/fp_part_catalog_views.xml
index 80ac6e2a..f85db347 100644
--- a/fusion-plating/fusion_plating_configurator/views/fp_part_catalog_views.xml
+++ b/fusion-plating/fusion_plating_configurator/views/fp_part_catalog_views.xml
@@ -44,7 +44,14 @@
-
+
+
+
+
+