From f94be9dfa92e7f539413e8d32b68829906e3fe2f Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Fri, 17 Apr 2026 19:00:52 -0400 Subject: [PATCH] fix(part-catalog): upload slot + swapped Number/Name + smart buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes on fp.part.catalog form: 1. 3D Model upload actually works now. The old field exposed only a Many2one search dropdown — no way to add a new file. Added a Binary upload slot (model_upload + model_upload_filename) that fires an onchange which wraps the bytes in an ir.attachment and links it to model_attachment_id. The upload slot is hidden once a model is already attached, so the current file stays visible. Accepts STEP/STP/STL/IGES/IGS/BREP. Auto-runs the surface-area calculation after attach, same as before. 2. Part Number is now the big

title, Part Name is the smaller field underneath. Matches how plating shops actually identify parts (by customer part number, not a free-text name). Swapped column order in the list view too — Part Number first, then Name. 3. Four smart buttons now on the part form: - Customer → opens res.partner record - Sale Orders (already existed) - Work Orders → filtered mrp.workorder list across SOs for this part - Quotes (already existed) - Revisions → shown only when 2+ revs exist, opens the revision tree filtered by root part New compute fields workorder_count + revision_count feed the statinfo widgets, with matching action_view_customer, action_view_workorders, action_view_revisions handlers. Verified on demo data: VS-ESMC6H00801P01 → SO=2, WO=18, REV=2 VS-PQR8440 → SO=1, WO=9, REV=3 All counts light up, buttons drill in cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../models/fp_part_catalog.py | 102 ++++++++++++++++++ .../views/fp_part_catalog_views.xml | 53 ++++++++- 2 files changed, 150 insertions(+), 5 deletions(-) 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 2a3e0cf0..ed7ef1c7 100644 --- a/fusion_plating/fusion_plating_configurator/models/fp_part_catalog.py +++ b/fusion_plating/fusion_plating_configurator/models/fp_part_catalog.py @@ -50,6 +50,16 @@ class FpPartCatalog(models.Model): 'ir.attachment', string='3D Model File', help='STEP, STL, or IGES file.', tracking=True, ) + # Binary upload proxy — lets the user drop a file in the form; the + # onchange below wraps it in an ir.attachment and links it to + # model_attachment_id. Without this, the Many2one only offers a + # search dropdown with no upload affordance. + model_upload = fields.Binary( + string='Upload 3D Model', + help='Drop a STEP/STP/STL/IGES/IGS/BREP file here to attach it as ' + 'the 3D model for this part.', + ) + model_upload_filename = fields.Char(string='Upload Filename') drawing_attachment_ids = fields.Many2many( 'ir.attachment', 'fp_part_catalog_drawing_rel', 'part_catalog_id', 'attachment_id', string='PDF Drawings', ) @@ -174,6 +184,12 @@ class FpPartCatalog(models.Model): configurator_count = fields.Integer( string='Quotes', compute='_compute_configurator_count', ) + workorder_count = fields.Integer( + string='Work Orders', compute='_compute_workorder_count', + ) + revision_count = fields.Integer( + string='Revisions', compute='_compute_revision_count', + ) _sql_constraints = [ ('fp_part_catalog_partner_partnum_uniq', 'unique(partner_id, part_number)', @@ -261,6 +277,68 @@ class FpPartCatalog(models.Model): part.configurator_count = self.env['fp.quote.configurator'].search_count( [('part_catalog_id', '=', part.id)]) + def _compute_workorder_count(self): + SaleOrder = self.env['sale.order'] + Production = self.env['mrp.production'] + MrpWO = self.env.get('mrp.workorder') + for part in self: + if MrpWO is None: + part.workorder_count = 0 + continue + so_names = SaleOrder.search( + [('x_fc_part_catalog_id', '=', part.id)] + ).mapped('name') + if not so_names: + part.workorder_count = 0 + continue + mos = Production.search([('origin', 'in', so_names)]) + part.workorder_count = sum(len(m.workorder_ids) for m in mos) + + def _compute_revision_count(self): + for part in self: + root = part.parent_part_id or part + part.revision_count = self.env['fp.part.catalog'].search_count([ + '|', ('id', '=', root.id), ('parent_part_id', '=', root.id), + ]) + + def action_view_customer(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': self.partner_id.display_name, + 'res_model': 'res.partner', + 'res_id': self.partner_id.id, + 'view_mode': 'form', + 'target': 'current', + } + + def action_view_workorders(self): + self.ensure_one() + so_names = self.env['sale.order'].search( + [('x_fc_part_catalog_id', '=', self.id)] + ).mapped('name') + mos = self.env['mrp.production'].search([('origin', 'in', so_names)]) + wo_ids = mos.mapped('workorder_ids').ids + return { + 'type': 'ir.actions.act_window', + 'name': _('Work Orders — %s') % (self.part_number or self.name), + 'res_model': 'mrp.workorder', + 'domain': [('id', 'in', wo_ids)], + 'view_mode': 'list,form,kanban', + } + + def action_view_revisions(self): + self.ensure_one() + root = self.parent_part_id or self + return { + 'type': 'ir.actions.act_window', + 'name': _('Revisions — %s') % (root.part_number or root.name), + 'res_model': 'fp.part.catalog', + 'domain': ['|', ('id', '=', root.id), ('parent_part_id', '=', root.id)], + 'view_mode': 'list,form', + 'context': {'default_parent_part_id': root.id}, + } + def action_view_sale_orders(self): self.ensure_one() orders = self.env['sale.order'].search([('x_fc_part_catalog_id', '=', self.id)]) @@ -336,6 +414,30 @@ class FpPartCatalog(models.Model): if self.model_attachment_id: self._compute_surface_area_from_model() + @api.onchange('model_upload', 'model_upload_filename') + def _onchange_model_upload(self): + """Wrap an uploaded binary file in an ir.attachment and link it. + + Fires as soon as the user drops a file in the "Upload 3D Model" + widget — the attachment is created in-memory (no DB commit) so + saving the part persists both at once. + """ + if not self.model_upload: + return + attachment = self.env['ir.attachment'].create({ + 'name': self.model_upload_filename or 'model.step', + 'datas': self.model_upload, + 'res_model': self._name, + 'res_id': self.id or 0, + }) + self.model_attachment_id = attachment + # Clear the upload buffer so the same widget can accept another file + self.model_upload = False + self.model_upload_filename = False + # If attaching triggered auto-area calc, rerun it + if self.model_attachment_id: + self._compute_surface_area_from_model() + def action_calculate_surface_area(self): """Button: calculate surface area from the uploaded 3D model file.""" self.ensure_one() 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 ce130bbf..331c0159 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 @@ -12,8 +12,9 @@ fp.part.catalog - + + @@ -38,6 +39,15 @@
+ + +
-
@@ -131,7 +155,26 @@ - + +