# -*- coding: utf-8 -*- # Copyright 2024-2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) """Backend intake wizard. A simple Phase 1 transient model that captures one-or-many equipment items per call, then delegates to fusion.repair.intake.service to create the repair.order(s). The shared service guarantees identical behaviour to the sales rep portal and the public client portal added in later phases. Multi-equipment per call is supported via the equipment_ids One2many. """ import logging from odoo import _, api, fields, models from odoo.exceptions import UserError _logger = logging.getLogger(__name__) class RepairIntakeWizard(models.TransientModel): _name = 'fusion.repair.intake.wizard' _description = 'Repair Intake Wizard' # ------------------------------------------------------------------ # CALLER / CLIENT # ------------------------------------------------------------------ intake_user_id = fields.Many2one( 'res.users', string='Taken By', default=lambda self: self.env.user, required=True, ) partner_id = fields.Many2one( 'res.partner', string='Client', required=True, help='Existing client. Use the create-and-edit dialog to add a new contact.', ) partner_phone = fields.Char( related='partner_id.phone', string='Phone', readonly=True, ) # ------------------------------------------------------------------ # EQUIPMENT (one-or-many) # ------------------------------------------------------------------ equipment_ids = fields.One2many( 'fusion.repair.intake.wizard.equipment', 'wizard_id', string='Equipment Items', required=True, ) # ------------------------------------------------------------------ # SUBMIT # ------------------------------------------------------------------ def action_submit(self): self.ensure_one() if not self.equipment_ids: raise UserError(_('Please add at least one piece of equipment.')) payload = { 'partner_id': self.partner_id.id, 'intake_user_id': self.intake_user_id.id, 'equipment_items': [self._equipment_payload(eq) for eq in self.equipment_ids], } # sudo() so sub-operations (mail.activity, mail.mail, fusion.technician.task) # never trip on permission checks - x_fc_intake_user_id preserves audit identity. repairs = self.env['fusion.repair.intake.service'].sudo().create_repair_orders( payload, source='backend_wizard', ) if len(repairs) == 1: return { 'type': 'ir.actions.act_window', 'name': repairs.name, 'res_model': 'repair.order', 'view_mode': 'form', 'res_id': repairs.id, } return { 'type': 'ir.actions.act_window', 'name': _('Service Calls Created (%(count)s)', count=len(repairs)), 'res_model': 'repair.order', 'view_mode': 'list,form', 'domain': [('id', 'in', repairs.ids)], } def _equipment_payload(self, eq): """Render an equipment record as a dict the intake service expects.""" return { 'product_id': eq.product_id.id or False, 'lot_id': eq.lot_id.id or False, 'repair_category_id': eq.repair_category_id.id or False, 'intake_template_id': eq.intake_template_id.id or False, 'third_party': eq.third_party, 'urgency': eq.urgency, 'issue_summary': eq.issue_summary or '', 'issue_category': eq.issue_category or '', 'internal_notes': eq.internal_notes or '', 'schedule_date': eq.scheduled_date or False, 'photo_attachment_ids': eq.photo_ids.ids if eq.photo_ids else [], 'answers': [], # Phase 1 wizard doesn't expose per-question answer rows yet } class RepairIntakeWizardEquipment(models.TransientModel): """A single piece of equipment captured in the wizard. Multiple lines = multi-equipment intake (one repair.order per line). """ _name = 'fusion.repair.intake.wizard.equipment' _description = 'Repair Intake Wizard - Equipment Line' _order = 'sequence, id' wizard_id = fields.Many2one( 'fusion.repair.intake.wizard', string='Wizard', required=True, ondelete='cascade', ) sequence = fields.Integer(default=10) # Equipment identification repair_category_id = fields.Many2one( 'fusion.repair.product.category', string='Category', required=True, ) product_id = fields.Many2one( 'product.product', string='Product', help='Specific product if known. Leave blank for generic equipment.', ) lot_id = fields.Many2one( 'stock.lot', string='Serial Number', domain="[('product_id', '=', product_id)]", help='Lot or serial number if known.', ) third_party = fields.Boolean( string='Not Purchased From Us', help='Tick if this equipment was bought elsewhere - we still service it but ' 'warranty is not honoured and a service call-out fee applies.', ) # Intake context intake_template_id = fields.Many2one( 'fusion.repair.intake.template', string='Question Template', help='Defaults to the template configured on the category if left blank.', ) # Triage urgency = fields.Selection( [('normal', 'Normal'), ('urgent', 'Urgent'), ('safety', 'Safety Issue')], string='Urgency', default='normal', required=True, ) scheduled_date = fields.Datetime( string='Preferred Date', default=fields.Datetime.now, ) issue_summary = fields.Char( string='Issue Summary', help='One-line summary of what is wrong (e.g. "stairlift stops halfway up").', ) issue_category = fields.Char( string='Symptom Category', help='Optional symptom tag for catalogue matching (e.g. "battery", "motor").', ) internal_notes = fields.Text(string='Internal Notes') photo_ids = fields.Many2many( 'ir.attachment', 'fusion_repair_intake_wizard_eq_photo_rel', 'eq_id', 'attachment_id', string='Photos / Videos', ) @api.onchange('repair_category_id') def _onchange_repair_category_id(self): """Pre-fill the intake template from the category default.""" if self.repair_category_id and not self.intake_template_id: self.intake_template_id = self.repair_category_id.intake_template_id @api.onchange('product_id') def _onchange_product_id(self): """Pre-fill the category from the product if defined.""" if self.product_id and not self.repair_category_id: cat = self.product_id.product_tmpl_id.x_fc_repair_category_id if cat: self.repair_category_id = cat