fusion_authorizer_portal: wire accessibility assessments into MOD/ODSP/WSIB workflows (v19.0.2.8.0)
Audit found that fusion.accessibility.assessment._create_draft_sale_order hardcoded x_fc_sale_type='direct_private' for ALL accessibility cases — meaning MOD, ODSP, WSIB, and insurance projects never entered their respective downstream workflows. The MOD workflow rework I shipped in fusion_claims 19.0.8.0.3 was effectively unreachable from the portal. Also: x_fc_authorizer_id never propagated from the assessment to the SO, the new x_fc_mod_accessibility_specialist_id was orphaned, and there was no back-reference from sale.order to the accessibility assessment. Fixes: - New required field x_fc_funding_source on fusion.accessibility.assessment (march_of_dimes / odsp / wsib / insurance / direct_private / other) - _create_draft_sale_order now maps funding_source -> x_fc_sale_type, copies authorizer_id -> x_fc_authorizer_id, sets accessibility_assessment_id back-ref, and for MOD cases pre-populates x_fc_mod_accessibility_specialist_id from sales_rep_id.partner_id - New accessibility_assessment_id field on sale.order so the back-link is queryable both directions (previously only assessment->SO existed) - action_complete now guards against double-completion and missing funding_source: raises UserError instead of silently creating duplicates - Expanded fusion.accessibility.assessment.state from 3 to 6 values (draft/scheduled/in_progress/pending_review/completed/cancelled), added copy=False, added _expand_states group_expand for kanban - Booking form at /book-assessment now collects funding_source (required dropdown) so the path is known before the visit happens - portal_assessment.py book_assessment_submit accepts funding_source with whitelist validation (defaults to direct_private if missing) Deployed to odoo-westin (westin-v19) and odoo-mobility (mobility), both verified at v19.0.2.8.0 with the new columns present.
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.7.0',
|
'version': '19.0.2.8.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': """
|
||||||
|
|||||||
@@ -1097,6 +1097,18 @@ class AssessmentPortal(CustomerPortal):
|
|||||||
if kw.get('client_postal'):
|
if kw.get('client_postal'):
|
||||||
address_parts.append(kw['client_postal'])
|
address_parts.append(kw['client_postal'])
|
||||||
|
|
||||||
|
# 2026-04 portal audit fix — accept funding source from the form
|
||||||
|
# so the generated sale order enters the correct workflow. Defaults
|
||||||
|
# to direct_private for backwards compatibility if the form field
|
||||||
|
# is missing (old cached browsers, legacy API calls).
|
||||||
|
valid_funding_sources = (
|
||||||
|
'march_of_dimes', 'odsp', 'wsib',
|
||||||
|
'insurance', 'direct_private', 'other',
|
||||||
|
)
|
||||||
|
funding_source = kw.get('funding_source', 'direct_private')
|
||||||
|
if funding_source not in valid_funding_sources:
|
||||||
|
funding_source = 'direct_private'
|
||||||
|
|
||||||
vals = {
|
vals = {
|
||||||
'assessment_type': kw['assessment_type'],
|
'assessment_type': kw['assessment_type'],
|
||||||
'client_name': kw['client_name'],
|
'client_name': kw['client_name'],
|
||||||
@@ -1110,6 +1122,7 @@ class AssessmentPortal(CustomerPortal):
|
|||||||
'assessment_date': assessment_date,
|
'assessment_date': assessment_date,
|
||||||
'booking_source': booking_source,
|
'booking_source': booking_source,
|
||||||
'modification_requested': kw.get('modification_requested', ''),
|
'modification_requested': kw.get('modification_requested', ''),
|
||||||
|
'x_fc_funding_source': funding_source,
|
||||||
}
|
}
|
||||||
|
|
||||||
if sales_rep_id:
|
if sales_rep_id:
|
||||||
|
|||||||
@@ -50,13 +50,49 @@ class FusionAccessibilityAssessment(models.Model):
|
|||||||
state = fields.Selection(
|
state = fields.Selection(
|
||||||
selection=[
|
selection=[
|
||||||
('draft', 'Draft'),
|
('draft', 'Draft'),
|
||||||
|
('scheduled', 'Visit Scheduled'),
|
||||||
|
('in_progress', 'Visit In Progress'),
|
||||||
|
('pending_review', 'Pending Review'),
|
||||||
('completed', 'Completed'),
|
('completed', 'Completed'),
|
||||||
('cancelled', 'Cancelled'),
|
('cancelled', 'Cancelled'),
|
||||||
],
|
],
|
||||||
string='Status',
|
string='Status',
|
||||||
default='draft',
|
default='draft',
|
||||||
tracking=True,
|
tracking=True,
|
||||||
|
copy=False,
|
||||||
|
group_expand='_expand_states',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ==========================================================================
|
||||||
|
# FUNDING SOURCE (2026-04 portal audit fix)
|
||||||
|
# Captures which funding stream the accessibility project is for so the
|
||||||
|
# generated sale order enters the right downstream workflow.
|
||||||
|
# ==========================================================================
|
||||||
|
x_fc_funding_source = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
('march_of_dimes', 'March of Dimes'),
|
||||||
|
('odsp', 'ODSP'),
|
||||||
|
('wsib', 'WSIB'),
|
||||||
|
('insurance', 'Private Insurance'),
|
||||||
|
('direct_private', 'Private Pay (Direct)'),
|
||||||
|
('other', 'Other'),
|
||||||
|
],
|
||||||
|
string='Funding Source',
|
||||||
|
required=True,
|
||||||
|
default='direct_private',
|
||||||
|
tracking=True,
|
||||||
|
help='Which funding stream this accessibility project is for. Determines '
|
||||||
|
'the x_fc_sale_type on the generated sale order and which downstream '
|
||||||
|
'workflow (MOD / ODSP / WSIB / direct pay) the case enters.',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _expand_states(self, states, domain):
|
||||||
|
"""Kanban group expansion — always show all 6 workflow states."""
|
||||||
|
return [
|
||||||
|
'draft', 'scheduled', 'in_progress',
|
||||||
|
'pending_review', 'completed', 'cancelled',
|
||||||
|
]
|
||||||
|
|
||||||
# Client Information
|
# Client Information
|
||||||
client_name = fields.Char(string='Client Name', required=True)
|
client_name = fields.Char(string='Client Name', required=True)
|
||||||
@@ -455,15 +491,37 @@ class FusionAccessibilityAssessment(models.Model):
|
|||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
|
|
||||||
def action_complete(self):
|
def action_complete(self):
|
||||||
"""Complete the assessment and create a Sale Order"""
|
"""Complete the assessment and create a Sale Order.
|
||||||
|
|
||||||
|
2026-04 portal audit fix — guards against double-completion
|
||||||
|
(clicking the button twice used to create two sale orders) and
|
||||||
|
requires a funding source to be set so the generated sale order
|
||||||
|
enters the correct downstream workflow.
|
||||||
|
"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
||||||
|
if self.state == 'completed':
|
||||||
|
raise UserError(_('This assessment is already completed.'))
|
||||||
|
if self.state == 'cancelled':
|
||||||
|
raise UserError(_('Cancelled assessments cannot be completed.'))
|
||||||
|
if self.sale_order_id:
|
||||||
|
raise UserError(_(
|
||||||
|
'A sale order has already been created from this assessment (%s). '
|
||||||
|
'Cannot create a second one.'
|
||||||
|
) % self.sale_order_id.name)
|
||||||
|
|
||||||
if not self.client_name:
|
if not self.client_name:
|
||||||
raise UserError(_('Please enter the client name.'))
|
raise UserError(_('Please enter the client name.'))
|
||||||
|
if not self.x_fc_funding_source:
|
||||||
|
raise UserError(_(
|
||||||
|
'Please select a funding source before completing the assessment. '
|
||||||
|
'This determines which workflow the case enters (March of Dimes, '
|
||||||
|
'ODSP, WSIB, Insurance, or Private Pay).'
|
||||||
|
))
|
||||||
|
|
||||||
# Create or find partner
|
# Create or find partner
|
||||||
partner = self._ensure_partner()
|
partner = self._ensure_partner()
|
||||||
|
|
||||||
# Create draft sale order
|
# Create draft sale order
|
||||||
sale_order = self._create_draft_sale_order(partner)
|
sale_order = self._create_draft_sale_order(partner)
|
||||||
|
|
||||||
@@ -691,22 +749,56 @@ class FusionAccessibilityAssessment(models.Model):
|
|||||||
return partner
|
return partner
|
||||||
|
|
||||||
def _create_draft_sale_order(self, partner):
|
def _create_draft_sale_order(self, partner):
|
||||||
"""Create a draft sale order from the assessment"""
|
"""Create a draft sale order from the accessibility assessment.
|
||||||
|
|
||||||
|
2026-04 portal audit fix — previously hardcoded x_fc_sale_type to
|
||||||
|
'direct_private' for ALL accessibility assessments, which meant MOD,
|
||||||
|
ODSP, WSIB, and insurance cases never entered their respective
|
||||||
|
workflows. Now uses x_fc_funding_source to pick the correct sale
|
||||||
|
type, wires the authorizer through, sets the back-reference, and
|
||||||
|
for MOD cases pre-populates x_fc_mod_accessibility_specialist_id
|
||||||
|
from the sales rep's partner record.
|
||||||
|
"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
||||||
SaleOrder = self.env['sale.order'].sudo()
|
SaleOrder = self.env['sale.order'].sudo()
|
||||||
|
|
||||||
type_labels = dict(self._fields['assessment_type'].selection)
|
type_labels = dict(self._fields['assessment_type'].selection)
|
||||||
type_label = type_labels.get(self.assessment_type, 'Accessibility')
|
type_label = type_labels.get(self.assessment_type, 'Accessibility')
|
||||||
|
|
||||||
|
# Map funding source → sale type (currently 1:1 but kept explicit
|
||||||
|
# so the mapping is easy to adjust later without breaking data).
|
||||||
|
sale_type_map = {
|
||||||
|
'march_of_dimes': 'march_of_dimes',
|
||||||
|
'odsp': 'odsp',
|
||||||
|
'wsib': 'wsib',
|
||||||
|
'insurance': 'insurance',
|
||||||
|
'direct_private': 'direct_private',
|
||||||
|
'other': 'other',
|
||||||
|
}
|
||||||
|
sale_type = sale_type_map.get(self.x_fc_funding_source, 'direct_private')
|
||||||
|
|
||||||
so_vals = {
|
so_vals = {
|
||||||
'partner_id': partner.id,
|
'partner_id': partner.id,
|
||||||
'user_id': self.sales_rep_id.id if self.sales_rep_id else self.env.user.id,
|
'user_id': self.sales_rep_id.id if self.sales_rep_id else self.env.user.id,
|
||||||
'state': 'draft',
|
'state': 'draft',
|
||||||
'origin': f'Accessibility: {self.reference} ({type_label})',
|
'origin': f'Accessibility: {self.reference} ({type_label})',
|
||||||
'x_fc_sale_type': 'direct_private', # Accessibility items typically private pay
|
'x_fc_sale_type': sale_type,
|
||||||
|
# Back-reference so the sale order knows which accessibility
|
||||||
|
# assessment spawned it (see field definition in fusion_authorizer_portal/models/sale_order.py).
|
||||||
|
'accessibility_assessment_id': self.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Propagate the authorizer (OT) when one is set on the assessment.
|
||||||
|
if self.authorizer_id:
|
||||||
|
so_vals['x_fc_authorizer_id'] = self.authorizer_id.id
|
||||||
|
|
||||||
|
# For MOD cases: pre-populate the accessibility specialist from the
|
||||||
|
# sales rep who did the visit, so the MOD workflow knows who to
|
||||||
|
# credit on the case without manual data entry.
|
||||||
|
if sale_type == 'march_of_dimes' and self.sales_rep_id and self.sales_rep_id.partner_id:
|
||||||
|
so_vals['x_fc_mod_accessibility_specialist_id'] = self.sales_rep_id.partner_id.id
|
||||||
|
|
||||||
sale_order = SaleOrder.create(so_vals)
|
sale_order = SaleOrder.create(so_vals)
|
||||||
_logger.info(f"Created draft sale order {sale_order.name} from accessibility assessment {self.reference}")
|
_logger.info(f"Created draft sale order {sale_order.name} from accessibility assessment {self.reference}")
|
||||||
|
|
||||||
|
|||||||
@@ -33,12 +33,25 @@ class SaleOrder(models.Model):
|
|||||||
compute='_compute_portal_document_count',
|
compute='_compute_portal_document_count',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Link to assessment
|
# Link to assessment (ADP equipment assessment — rollators, wheelchairs, powerchairs)
|
||||||
assessment_id = fields.Many2one(
|
assessment_id = fields.Many2one(
|
||||||
'fusion.assessment',
|
'fusion.assessment',
|
||||||
string='Source Assessment',
|
string='Source ADP Assessment',
|
||||||
readonly=True,
|
readonly=True,
|
||||||
help='The assessment that created this sale order',
|
help='The ADP equipment assessment that created this sale order',
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2026-04 portal audit fix — link to the accessibility (modification)
|
||||||
|
# assessment if this sale order came from a stair lift / VPL / ceiling lift
|
||||||
|
# / ramp / bathroom modification / tub cutout visit. Previously there
|
||||||
|
# was no way to trace a MOD/ODSP sale order back to its source assessment.
|
||||||
|
accessibility_assessment_id = fields.Many2one(
|
||||||
|
'fusion.accessibility.assessment',
|
||||||
|
string='Source Accessibility Assessment',
|
||||||
|
readonly=True,
|
||||||
|
help='The accessibility (modification) assessment that created this '
|
||||||
|
'sale order — stair lift, VPL, ceiling lift, ramp, bathroom mod, '
|
||||||
|
'or tub cutout visits.',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Authorizer helper field (consolidates multiple possible fields)
|
# Authorizer helper field (consolidates multiple possible fields)
|
||||||
|
|||||||
@@ -45,6 +45,25 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Funding Source (2026-04 portal audit fix —
|
||||||
|
required so the generated sale order enters
|
||||||
|
the correct downstream workflow: MOD, ODSP, etc) -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-bold">How is this project being funded?<span class="text-danger"> *</span></label>
|
||||||
|
<select name="funding_source" class="form-select" required="">
|
||||||
|
<option value="">Select...</option>
|
||||||
|
<option value="march_of_dimes">March of Dimes</option>
|
||||||
|
<option value="odsp">ODSP</option>
|
||||||
|
<option value="wsib">WSIB</option>
|
||||||
|
<option value="insurance">Private Insurance</option>
|
||||||
|
<option value="direct_private">Private Pay (Direct)</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
This determines which workflow the case enters after the visit.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-bold">What are you looking for?</label>
|
<label class="form-label fw-bold">What are you looking for?</label>
|
||||||
<textarea name="modification_requested" class="form-control" rows="2"
|
<textarea name="modification_requested" class="form-control" rows="2"
|
||||||
|
|||||||
Reference in New Issue
Block a user