feat(fusion_portal): Phase 3 - assessment visit workspace (accessibility path)

Adds the portal workspace: /my/visit/new starts a visit; /my/visit/<id> shows the
add-as-you-go workspace (add buttons -> existing forms carrying ?visit_id, a
deferred client+funding form, and a Complete button). Accessibility forms launched
from a visit save as a DRAFT linked to it (JS carries visit_id into the form; the
controller captures it and skips the per-assessment SO) - the VISIT completion then
creates the grouped per-funding sale orders.

NOT YET: express/ADP form visit-linking, email consolidation, polished tablet UI.
Untested locally (Enterprise dep) - clone verification pending.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-02 02:37:08 -04:00
parent 89467432a7
commit 21cfd55419
4 changed files with 191 additions and 3 deletions

View File

@@ -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/<int:visit_id>', 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/<int:visit_id>/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/<int:visit_id>/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: