# -*- coding: utf-8 -*- # Copyright 2024-2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) """Sales rep portal for repair intake. Sales reps marked `is_sales_rep_portal` on their partner can: - /my/repair/new - submit a new service call from their phone - /my/repairs - list of repairs they have submitted - /my/repair/ - read-only detail with status timeline All routes are gated by the is_sales_rep_portal flag and use the SAME shared intake service (`fusion.repair.intake.service`) as the backend wizard - so behaviour stays consistent across surfaces. """ import base64 import logging from odoo import http, fields from odoo.http import request from odoo.addons.portal.controllers.portal import CustomerPortal _logger = logging.getLogger(__name__) class SalesRepRepairPortal(CustomerPortal): # ------------------------------------------------------------------ # ACCESS GATE # ------------------------------------------------------------------ def _check_sales_rep_access(self): partner = request.env.user.partner_id if not getattr(partner, 'is_sales_rep_portal', False): return request.redirect('/my') return None def _staged_attachment_ids_from_files(self, files): """Stage uploaded files as ir.attachment records and return their IDs.""" ids = [] for f in files or []: if not getattr(f, 'filename', None): continue data = f.read() if not data: continue attachment = request.env['ir.attachment'].sudo().create({ 'name': f.filename, 'datas': base64.b64encode(data), 'res_model': 'fusion.repair.intake.session', 'res_id': 0, }) ids.append(attachment.id) return ids # ------------------------------------------------------------------ # NEW SERVICE CALL FORM # ------------------------------------------------------------------ @http.route('/my/repair/new', type='http', auth='user', website=True, sitemap=False) def portal_repair_new(self, **kw): gate = self._check_sales_rep_access() if gate: return gate categories = request.env['fusion.repair.product.category'].sudo().search([ ('active', '=', True), ], order='sequence, name') return request.render('fusion_repairs.portal_sales_rep_repair_form', { 'page_name': 'repair_new', 'categories': categories, 'default_partner': False, 'submitted': False, }) @http.route('/my/repair/lookup_partner', type='jsonrpc', auth='user', website=True) def portal_repair_lookup_partner(self, query=None, **kw): gate = self._check_sales_rep_access() if gate: return {'error': 'access'} if not query or len(query) < 3: return {'matches': []} Partner = request.env['res.partner'].sudo() matches = Partner.search([ '|', '|', ('name', 'ilike', query), ('phone', 'ilike', query), ('email', 'ilike', query), ], limit=8) return { 'matches': [{ 'id': p.id, 'name': p.name or '', 'phone': p.phone or '', 'email': p.email or '', 'street': p.street or '', 'city': p.city or '', 'repair_count': p.x_fc_repair_count, } for p in matches], } @http.route('/my/repair/submit', type='http', auth='user', methods=['POST'], csrf=True, website=True) def portal_repair_submit(self, **post): gate = self._check_sales_rep_access() if gate: return gate partner_id = int(post.get('partner_id') or 0) if not partner_id: return request.redirect('/my/repair/new?error=partner') # Build single-equipment payload from the form. Multi-equipment loop # is supported by adding more equipment_* groups in Phase 2. files = request.httprequest.files.getlist('photos') attachment_ids = self._staged_attachment_ids_from_files(files) equipment = { 'repair_category_id': int(post.get('category_id') or 0) or False, 'product_id': int(post.get('product_id') or 0) or False, 'third_party': post.get('third_party') in ('on', 'true', '1'), 'urgency': post.get('urgency') or 'normal', 'issue_summary': (post.get('issue_summary') or '').strip(), 'issue_category': (post.get('issue_category') or '').strip(), 'internal_notes': (post.get('internal_notes') or '').strip(), 'photo_attachment_ids': attachment_ids, } payload = { 'partner_id': partner_id, 'intake_user_id': request.env.uid, 'equipment_items': [equipment], } try: repairs = request.env['fusion.repair.intake.service'].sudo() \ .create_repair_orders(payload, source='sales_rep_portal') except Exception: _logger.exception('Sales rep portal repair submit failed') return request.redirect('/my/repair/new?error=server') return request.redirect('/my/repair/%d?thanks=1' % repairs[0].id) # ------------------------------------------------------------------ # MY REPAIRS LIST + DETAIL # ------------------------------------------------------------------ @http.route(['/my/repairs', '/my/repairs/page/'], type='http', auth='user', website=True) def portal_repairs_list(self, page=1, **kw): gate = self._check_sales_rep_access() if gate: return gate Repair = request.env['repair.order'].sudo() domain = [('x_fc_intake_user_id', '=', request.env.uid)] total = Repair.search_count(domain) page_size = 20 offset = (page - 1) * page_size repairs = Repair.search(domain, order='create_date desc', limit=page_size, offset=offset) return request.render('fusion_repairs.portal_sales_rep_repair_list', { 'page_name': 'repairs_list', 'repairs': repairs, 'total': total, 'page': page, 'page_size': page_size, }) @http.route('/my/repair/', type='http', auth='user', website=True) def portal_repair_detail(self, repair_id, thanks=None, **kw): gate = self._check_sales_rep_access() if gate: return gate repair = request.env['repair.order'].sudo().browse(repair_id).exists() if not repair or repair.x_fc_intake_user_id.id != request.env.uid: return request.redirect('/my/repairs') return request.render('fusion_repairs.portal_sales_rep_repair_detail', { 'page_name': 'repair_detail', 'repair': repair, 'thanks': bool(thanks), })