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 @@ + + + Customer + + + + + + + + - - - + + + @@ -131,7 +155,26 @@ - + + + + + + + STEP / STP / STL / IGES / IGS / BREP + + + + + + +