feat(fusion_portal): ADP/express->visit wiring, visit entry tile, email consolidation (live on westin 19.0.2.10.0)
- express save captures visit_id; visit-linked submit defers SO creation (saves draft + signature) and returns to the visit for grouping. - portal dashboard 'Start a Visit' tile for sales reps. - fix duplicate-authorizer completion email; visit grouped SOs email once per SO. - define visit._assessment_sale_type (ADP grouping key) - fixes AttributeError. Verified on a westin-v19 clone (load + ADP-grouping + combination-guard smoke test, mail neutralised) then deployed to westin prod 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 -*-
|
||||
{
|
||||
'name': 'Fusion Authorizer & Sales Portal',
|
||||
'version': '19.0.2.9.0',
|
||||
'version': '19.0.2.10.0',
|
||||
'category': 'Sales/Portal',
|
||||
'summary': 'Portal for Authorizers (OTs) and Sales Reps with Assessment Forms',
|
||||
'description': """
|
||||
|
||||
@@ -458,6 +458,7 @@ class AssessmentPortal(CustomerPortal):
|
||||
'current_page': 1,
|
||||
'total_pages': 2,
|
||||
'assessment': None,
|
||||
'visit_id': kw.get('visit_id', ''),
|
||||
'google_maps_api_key': google_maps_api_key,
|
||||
}
|
||||
|
||||
@@ -516,6 +517,7 @@ class AssessmentPortal(CustomerPortal):
|
||||
'partner': partner,
|
||||
'user': user,
|
||||
'assessment': assessment,
|
||||
'visit_id': kw.get('visit_id') or (assessment.visit_id.id if assessment.visit_id else ''),
|
||||
'authorizers': authorizers,
|
||||
'authorizers_json': authorizers_json,
|
||||
'clients': clients,
|
||||
@@ -630,6 +632,30 @@ class AssessmentPortal(CustomerPortal):
|
||||
except Exception as 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
|
||||
if action == 'submit':
|
||||
# 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):
|
||||
"""Build values dict from express form POST data"""
|
||||
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
|
||||
if kw.get('equipment_type'):
|
||||
|
||||
@@ -1486,20 +1486,18 @@ class FusionAssessment(models.Model):
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
# Send to authorizer
|
||||
if self.authorizer_id and self.authorizer_id.email:
|
||||
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
|
||||
|
||||
# Send to client (authorizer/rep/office already emailed by
|
||||
# _send_assessment_completed_email in _create_draft_sale_order)
|
||||
if self.client_email:
|
||||
try:
|
||||
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',
|
||||
)
|
||||
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(
|
||||
"Visit %s created %s sale order %s grouping %d accessibility assessment(s)",
|
||||
self.name, sale_type, sale_order.name, len(accessibility_assessments),
|
||||
@@ -206,6 +216,15 @@ class FusionAssessmentVisit(models.Model):
|
||||
if len(walkers) > 1:
|
||||
raise UserError(_('An ADP order can include only one walker / rollator.'))
|
||||
|
||||
def _assessment_sale_type(self, adp_assessment):
|
||||
"""Funding workflow key for an ADP equipment assessment, mirroring
|
||||
fusion.assessment._create_draft_sale_order: ADP+ODSP when the client
|
||||
type is an ODSP stream, plain ADP otherwise. ADP devices that share a
|
||||
key are grouped onto one sale order."""
|
||||
if adp_assessment.client_type in ('ods', 'acs', 'owp'):
|
||||
return 'adp_odsp'
|
||||
return 'adp'
|
||||
|
||||
def action_complete_visit(self):
|
||||
"""Group the visit's accessibility assessments by funding workflow and
|
||||
create one draft SO per workflow. ADP equipment assessments keep their
|
||||
|
||||
@@ -85,7 +85,19 @@
|
||||
<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="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 -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
@@ -1246,9 +1258,10 @@
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<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>
|
||||
<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
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -50,6 +50,25 @@
|
||||
|
||||
<!-- Main Action Tiles - Professional Grid Layout -->
|
||||
<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 -->
|
||||
<t t-if="request.env.user.partner_id.is_sales_rep_portal">
|
||||
<div class="col-md-6">
|
||||
|
||||
Reference in New Issue
Block a user