feat(fusion_portal): wire ADP/express into visit + portal entry tile + email consolidation
- express save captures visit_id; when visit-linked, action=submit saves the ADP assessment as a draft (signature + Page 11 PDF still captured) and returns to the visit instead of completing into a standalone SO, so the visit groups the ADP devices into one funding-routed order. Non-visit express flow unchanged. - portal dashboard: featured 'Start a Visit' tile (sales reps) -> /my/visit/new. - fix duplicate-authorizer email: _send_completion_notifications no longer re-emails the authorizer (already emailed with the full report by _send_assessment_completed_email); it now only notifies the client. - visit grouped accessibility SOs now send one office completion email per SO. Bump 19.0.2.10.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
{
|
{
|
||||||
'name': 'Fusion Authorizer & Sales Portal',
|
'name': 'Fusion Authorizer & Sales Portal',
|
||||||
'version': '19.0.2.9.0',
|
'version': '19.0.2.10.0',
|
||||||
'category': 'Sales/Portal',
|
'category': 'Sales/Portal',
|
||||||
'summary': 'Portal for Authorizers (OTs) and Sales Reps with Assessment Forms',
|
'summary': 'Portal for Authorizers (OTs) and Sales Reps with Assessment Forms',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -458,6 +458,7 @@ class AssessmentPortal(CustomerPortal):
|
|||||||
'current_page': 1,
|
'current_page': 1,
|
||||||
'total_pages': 2,
|
'total_pages': 2,
|
||||||
'assessment': None,
|
'assessment': None,
|
||||||
|
'visit_id': kw.get('visit_id', ''),
|
||||||
'google_maps_api_key': google_maps_api_key,
|
'google_maps_api_key': google_maps_api_key,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,6 +517,7 @@ class AssessmentPortal(CustomerPortal):
|
|||||||
'partner': partner,
|
'partner': partner,
|
||||||
'user': user,
|
'user': user,
|
||||||
'assessment': assessment,
|
'assessment': assessment,
|
||||||
|
'visit_id': kw.get('visit_id') or (assessment.visit_id.id if assessment.visit_id else ''),
|
||||||
'authorizers': authorizers,
|
'authorizers': authorizers,
|
||||||
'authorizers_json': authorizers_json,
|
'authorizers_json': authorizers_json,
|
||||||
'clients': clients,
|
'clients': clients,
|
||||||
@@ -630,6 +632,30 @@ class AssessmentPortal(CustomerPortal):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
_logger.error(f"Error saving Page 11 signature: {e}")
|
_logger.error(f"Error saving Page 11 signature: {e}")
|
||||||
|
|
||||||
|
# ===== Visit-linked: defer SO creation to visit completion =====
|
||||||
|
# Started from a visit workspace: do NOT complete into a standalone
|
||||||
|
# sale order. Leave it as a draft linked to the visit so
|
||||||
|
# visit.action_complete_visit() groups the visit's ADP devices
|
||||||
|
# (combination-checked) into ONE ADP order. The Page 11 signature is
|
||||||
|
# already saved above; pre-generate its PDF so it is ready.
|
||||||
|
if assessment.visit_id and action == 'submit':
|
||||||
|
if assessment.signature_page_11 and assessment.consent_declaration_accepted:
|
||||||
|
try:
|
||||||
|
pdf_bytes = assessment.generate_template_pdf('Page 11')
|
||||||
|
if pdf_bytes:
|
||||||
|
import base64 as b64
|
||||||
|
assessment.write({
|
||||||
|
'signed_page_11_pdf': b64.b64encode(pdf_bytes),
|
||||||
|
'signed_page_11_pdf_filename': f'ADP_Page11_{assessment.reference}.pdf',
|
||||||
|
})
|
||||||
|
except Exception as pdf_e:
|
||||||
|
_logger.warning(f"Visit-linked Page 11 PDF generation failed (non-blocking): {pdf_e}")
|
||||||
|
_logger.info(
|
||||||
|
f"Express assessment {assessment.reference} saved to visit "
|
||||||
|
f"{assessment.visit_id.name} (completion deferred to visit)"
|
||||||
|
)
|
||||||
|
return request.redirect(f'/my/visit/{assessment.visit_id.id}')
|
||||||
|
|
||||||
# Handle navigation
|
# Handle navigation
|
||||||
if action == 'submit':
|
if action == 'submit':
|
||||||
# If already completed, we just saved consent/signature above -- redirect with success
|
# If already completed, we just saved consent/signature above -- redirect with success
|
||||||
@@ -803,6 +829,13 @@ class AssessmentPortal(CustomerPortal):
|
|||||||
def _build_express_assessment_vals(self, kw):
|
def _build_express_assessment_vals(self, kw):
|
||||||
"""Build values dict from express form POST data"""
|
"""Build values dict from express form POST data"""
|
||||||
vals = {}
|
vals = {}
|
||||||
|
|
||||||
|
# Visit linkage (assessment started from a visit workspace)
|
||||||
|
if kw.get('visit_id'):
|
||||||
|
try:
|
||||||
|
vals['visit_id'] = int(kw.get('visit_id'))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
# Equipment type
|
# Equipment type
|
||||||
if kw.get('equipment_type'):
|
if kw.get('equipment_type'):
|
||||||
|
|||||||
@@ -1486,20 +1486,18 @@ class FusionAssessment(models.Model):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def _send_completion_notifications(self):
|
def _send_completion_notifications(self):
|
||||||
"""Send email notifications when assessment is completed"""
|
"""Notify the CLIENT that the assessment is complete.
|
||||||
|
|
||||||
|
The authorizer, sales rep and office are already emailed (with the full
|
||||||
|
assessment report) by ``_send_assessment_completed_email`` inside
|
||||||
|
``_create_draft_sale_order``. This method used to ALSO send the
|
||||||
|
authorizer a second, template-only email — that duplicate is removed;
|
||||||
|
here we only notify the client.
|
||||||
|
"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
||||||
# Send to authorizer
|
# Send to client (authorizer/rep/office already emailed by
|
||||||
if self.authorizer_id and self.authorizer_id.email:
|
# _send_assessment_completed_email in _create_draft_sale_order)
|
||||||
try:
|
|
||||||
template = self.env.ref('fusion_portal.mail_template_assessment_complete_authorizer', raise_if_not_found=False)
|
|
||||||
if template:
|
|
||||||
template.send_mail(self.id, force_send=True)
|
|
||||||
_logger.info(f"Sent assessment completion email to authorizer {self.authorizer_id.email}")
|
|
||||||
except Exception as e:
|
|
||||||
_logger.error(f"Failed to send authorizer notification: {e}")
|
|
||||||
|
|
||||||
# Send to client
|
|
||||||
if self.client_email:
|
if self.client_email:
|
||||||
try:
|
try:
|
||||||
template = self.env.ref('fusion_portal.mail_template_assessment_complete_client', raise_if_not_found=False)
|
template = self.env.ref('fusion_portal.mail_template_assessment_complete_client', raise_if_not_found=False)
|
||||||
|
|||||||
@@ -184,6 +184,16 @@ class FusionAssessmentVisit(models.Model):
|
|||||||
subtype_xmlid='mail.mt_note',
|
subtype_xmlid='mail.mt_note',
|
||||||
)
|
)
|
||||||
assessment.write({'state': 'completed'})
|
assessment.write({'state': 'completed'})
|
||||||
|
# One completion notification per SO (not per assessment) — mirrors the
|
||||||
|
# standalone accessibility completion's office email.
|
||||||
|
if accessibility_assessments:
|
||||||
|
try:
|
||||||
|
accessibility_assessments[0]._send_completion_email(sale_order)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning(
|
||||||
|
"Visit %s: completion email failed for %s: %s",
|
||||||
|
self.name, sale_order.name, e,
|
||||||
|
)
|
||||||
_logger.info(
|
_logger.info(
|
||||||
"Visit %s created %s sale order %s grouping %d accessibility assessment(s)",
|
"Visit %s created %s sale order %s grouping %d accessibility assessment(s)",
|
||||||
self.name, sale_type, sale_order.name, len(accessibility_assessments),
|
self.name, sale_type, sale_order.name, len(accessibility_assessments),
|
||||||
|
|||||||
@@ -85,7 +85,19 @@
|
|||||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||||
<input type="hidden" name="assessment_id" t-att-value="assessment.id if assessment else ''"/>
|
<input type="hidden" name="assessment_id" t-att-value="assessment.id if assessment else ''"/>
|
||||||
<input type="hidden" name="current_page" value="1"/>
|
<input type="hidden" name="current_page" value="1"/>
|
||||||
|
<input type="hidden" name="visit_id" id="express_visit_id" t-att-value="visit_id or ''"/>
|
||||||
|
|
||||||
|
<!-- Part of an assessment visit: completing returns to the visit, which groups
|
||||||
|
this device with the rest into one funding-routed ADP order. -->
|
||||||
|
<div t-if="visit_id" class="alert alert-info d-flex align-items-start gap-2 mb-3">
|
||||||
|
<i class="fa fa-clipboard mt-1"/>
|
||||||
|
<div>
|
||||||
|
<strong>Part of an assessment visit.</strong>
|
||||||
|
Completing this device returns you to the visit — it is grouped with the
|
||||||
|
visit's other ADP devices into a single sale order when you complete the visit.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Equipment Selection Section -->
|
<!-- Equipment Selection Section -->
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -1246,9 +1258,10 @@
|
|||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<button type="submit" name="action" value="submit" class="btn btn-success btn-lg px-5">
|
<button type="submit" name="action" value="submit" class="btn btn-success btn-lg px-5">
|
||||||
<i class="fa fa-check me-2"/>Submit Assessment
|
<t t-if="visit_id"><i class="fa fa-clipboard me-2"/>Save to Visit</t>
|
||||||
|
<t t-else=""><i class="fa fa-check me-2"/>Submit Assessment</t>
|
||||||
</button>
|
</button>
|
||||||
<a href="/my/assessments" class="btn btn-outline-secondary btn-lg px-4 ms-3">
|
<a t-att-href="('/my/visit/%s' % visit_id) if visit_id else '/my/assessments'" class="btn btn-outline-secondary btn-lg px-4 ms-3">
|
||||||
Cancel
|
Cancel
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,6 +50,25 @@
|
|||||||
|
|
||||||
<!-- Main Action Tiles - Professional Grid Layout -->
|
<!-- Main Action Tiles - Professional Grid Layout -->
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
|
<!-- 0. Start a Visit (Sales Rep) - bundle multiple assessments from one home visit -->
|
||||||
|
<t t-if="request.env.user.partner_id.is_sales_rep_portal">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<a href="/my/visit/new" class="card h-100 border-0 shadow-sm text-decoration-none" style="border-radius: 12px; min-height: 100px; background: linear-gradient(135deg, #5ba848 0%, #3a8fb7 100%);">
|
||||||
|
<div class="card-body d-flex align-items-center p-4 text-white">
|
||||||
|
<div class="me-3">
|
||||||
|
<div class="icon-circle">
|
||||||
|
<i class="fa fa-clipboard"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-1">Start a Visit</h5>
|
||||||
|
<small>Bundle several assessments from one home visit into funding-routed orders</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
<!-- 1. New Express Assessment (Sales Rep) - Featured tile -->
|
<!-- 1. New Express Assessment (Sales Rep) - Featured tile -->
|
||||||
<t t-if="request.env.user.partner_id.is_sales_rep_portal">
|
<t t-if="request.env.user.partner_id.is_sales_rep_portal">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|||||||
Reference in New Issue
Block a user