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:
gsinghpal
2026-04-09 08:24:30 -04:00
parent 5d89e04f82
commit d07159b9b5
5 changed files with 151 additions and 14 deletions

View File

@@ -50,13 +50,49 @@ class FusionAccessibilityAssessment(models.Model):
state = fields.Selection(
selection=[
('draft', 'Draft'),
('scheduled', 'Visit Scheduled'),
('in_progress', 'Visit In Progress'),
('pending_review', 'Pending Review'),
('completed', 'Completed'),
('cancelled', 'Cancelled'),
],
string='Status',
default='draft',
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_name = fields.Char(string='Client Name', required=True)
@@ -455,15 +491,37 @@ class FusionAccessibilityAssessment(models.Model):
# ==========================================================================
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()
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:
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
partner = self._ensure_partner()
# Create draft sale order
sale_order = self._create_draft_sale_order(partner)
@@ -691,22 +749,56 @@ class FusionAccessibilityAssessment(models.Model):
return 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()
SaleOrder = self.env['sale.order'].sudo()
type_labels = dict(self._fields['assessment_type'].selection)
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 = {
'partner_id': partner.id,
'user_id': self.sales_rep_id.id if self.sales_rep_id else self.env.user.id,
'state': 'draft',
'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)
_logger.info(f"Created draft sale order {sale_order.name} from accessibility assessment {self.reference}")

View File

@@ -33,12 +33,25 @@ class SaleOrder(models.Model):
compute='_compute_portal_document_count',
)
# Link to assessment
# Link to assessment (ADP equipment assessment — rollators, wheelchairs, powerchairs)
assessment_id = fields.Many2one(
'fusion.assessment',
string='Source Assessment',
string='Source ADP Assessment',
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)