diff --git a/fusion_portal/__manifest__.py b/fusion_portal/__manifest__.py index 7397db46..e9de3a35 100644 --- a/fusion_portal/__manifest__.py +++ b/fusion_portal/__manifest__.py @@ -81,6 +81,7 @@ This module provides external portal access for: 'views/portal_technician_templates.xml', 'views/portal_book_assessment.xml', 'views/portal_page11_sign_templates.xml', + 'views/portal_visit.xml', ], 'assets': { 'web.assets_backend': [ diff --git a/fusion_portal/controllers/portal_main.py b/fusion_portal/controllers/portal_main.py index 0563e099..42c498e4 100644 --- a/fusion_portal/controllers/portal_main.py +++ b/fusion_portal/controllers/portal_main.py @@ -2479,6 +2479,56 @@ class AuthorizerPortal(CustomerPortal): template = template_map.get(assessment_type, 'fusion_portal.portal_accessibility_selector') return request.render(template, values) + # ========================================================================== + # ASSESSMENT VISIT WORKSPACE (Phase 1b/3) + # ========================================================================== + @http.route('/my/visit/new', type='http', auth='user', website=True) + def visit_new(self, **kw): + """Start a new assessment visit and open its workspace.""" + partner = request.env.user.partner_id + if not partner.is_sales_rep_portal and not partner.is_authorizer: + return request.redirect('/my') + visit = request.env['fusion.assessment.visit'].sudo().create({ + 'sales_rep_id': request.env.user.id, + }) + return request.redirect('/my/visit/%s' % visit.id) + + @http.route('/my/visit/', type='http', auth='user', website=True) + def visit_workspace(self, visit_id, **kw): + visit = request.env['fusion.assessment.visit'].sudo().browse(visit_id) + if not visit.exists(): + return request.redirect('/my') + return request.render('fusion_portal.portal_visit_workspace', { + 'visit': visit, + 'page_name': 'visit', + 'error': kw.get('error'), + }) + + @http.route('/my/visit//save', type='http', auth='user', methods=['POST'], website=True, csrf=True) + def visit_save_client(self, visit_id, **post): + visit = request.env['fusion.assessment.visit'].sudo().browse(visit_id) + if visit.exists(): + visit.write({ + 'client_name': (post.get('client_name') or '').strip(), + 'client_phone': (post.get('client_phone') or '').strip(), + 'client_email': (post.get('client_email') or '').strip(), + 'client_address': (post.get('client_address') or '').strip(), + 'x_fc_income_under_mod_threshold': post.get('x_fc_income_under_mod_threshold') or 'unknown', + }) + return request.redirect('/my/visit/%s' % visit_id) + + @http.route('/my/visit//complete', type='http', auth='user', methods=['POST'], website=True, csrf=True) + def visit_complete(self, visit_id, **post): + visit = request.env['fusion.assessment.visit'].sudo().browse(visit_id) + if not visit.exists(): + return request.redirect('/my') + try: + visit.action_complete_visit() + except Exception as e: + _logger.warning("Visit %s completion failed: %s", visit_id, e) + return request.redirect('/my/visit/%s?error=%s' % (visit_id, str(e))) + return request.redirect('/my/visit/%s' % visit_id) + @http.route('/my/accessibility/save', type='json', auth='user', methods=['POST'], csrf=True) def accessibility_assessment_save(self, **post): """Save an accessibility assessment and optionally create a Sale Order""" @@ -2547,6 +2597,13 @@ class AuthorizerPortal(CustomerPortal): if partner.is_authorizer: vals['authorizer_id'] = partner.id + # Link to a visit if this form was launched from the workspace. + if post.get('visit_id'): + try: + vals['visit_id'] = int(post.get('visit_id')) + except (TypeError, ValueError): + pass + # Create the assessment assessment = Assessment.create(vals) _logger.info(f"Created accessibility assessment {assessment.reference} by {request.env.user.name}") @@ -2572,8 +2629,12 @@ class AuthorizerPortal(CustomerPortal): if video_data: self._attach_accessibility_video(assessment, video_data, video_filename) - # Complete the assessment and create Sale Order if requested + # Complete the assessment and create Sale Order if requested. + # When launched from a visit, always save as a draft linked to the + # visit — the VISIT completion creates the grouped sale order(s). create_sale_order = post.get('create_sale_order', True) + if vals.get('visit_id'): + create_sale_order = False if create_sale_order: sale_order = assessment.action_complete() return { @@ -2586,12 +2647,13 @@ class AuthorizerPortal(CustomerPortal): 'redirect_url': f'/my/sales/case/{sale_order.id}', } else: + redirect_url = ('/my/visit/%s' % vals['visit_id']) if vals.get('visit_id') else '/my/accessibility/list' return { 'success': True, 'assessment_id': assessment.id, 'assessment_ref': assessment.reference, - 'message': f'Assessment {assessment.reference} saved as draft.', - 'redirect_url': '/my/accessibility/list', + 'message': f'Assessment {assessment.reference} saved.', + 'redirect_url': redirect_url, } except Exception as e: diff --git a/fusion_portal/views/portal_accessibility_templates.xml b/fusion_portal/views/portal_accessibility_templates.xml index fcd1d35f..34176ab0 100644 --- a/fusion_portal/views/portal_accessibility_templates.xml +++ b/fusion_portal/views/portal_accessibility_templates.xml @@ -388,6 +388,7 @@ Determines which sale order / funding workflow this case enters. + @@ -647,6 +648,15 @@ // Fallback if Google Maps not loaded window.initAddressAutocomplete = window.initAddressAutocomplete || function() {}; + // Carry visit_id from the workspace launch (?visit_id=) into the form + (function() { + var _vid = new URLSearchParams(window.location.search).get('visit_id'); + if (_vid) { + var f = document.getElementById('acc_visit_id'); + if (f) { f.value = _vid; } + } + })(); + // Form submission function saveAssessment(createSaleOrder) { var form = document.getElementById('accessibility_form'); diff --git a/fusion_portal/views/portal_visit.xml b/fusion_portal/views/portal_visit.xml new file mode 100644 index 00000000..cd615077 --- /dev/null +++ b/fusion_portal/views/portal_visit.xml @@ -0,0 +1,115 @@ + + + +