diff --git a/fusion_authorizer_portal/__manifest__.py b/fusion_authorizer_portal/__manifest__.py index f2bb5a08..5bca83c0 100644 --- a/fusion_authorizer_portal/__manifest__.py +++ b/fusion_authorizer_portal/__manifest__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- { 'name': 'Fusion Authorizer & Sales Portal', - 'version': '19.0.2.7.0', + 'version': '19.0.2.8.0', 'category': 'Sales/Portal', 'summary': 'Portal for Authorizers (OTs) and Sales Reps with Assessment Forms', 'description': """ diff --git a/fusion_authorizer_portal/controllers/portal_assessment.py b/fusion_authorizer_portal/controllers/portal_assessment.py index 609b5516..d477e0eb 100644 --- a/fusion_authorizer_portal/controllers/portal_assessment.py +++ b/fusion_authorizer_portal/controllers/portal_assessment.py @@ -1097,6 +1097,18 @@ class AssessmentPortal(CustomerPortal): if kw.get('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 = { 'assessment_type': kw['assessment_type'], 'client_name': kw['client_name'], @@ -1110,6 +1122,7 @@ class AssessmentPortal(CustomerPortal): 'assessment_date': assessment_date, 'booking_source': booking_source, 'modification_requested': kw.get('modification_requested', ''), + 'x_fc_funding_source': funding_source, } if sales_rep_id: diff --git a/fusion_authorizer_portal/models/accessibility_assessment.py b/fusion_authorizer_portal/models/accessibility_assessment.py index 07a0fa74..423e394b 100644 --- a/fusion_authorizer_portal/models/accessibility_assessment.py +++ b/fusion_authorizer_portal/models/accessibility_assessment.py @@ -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}") diff --git a/fusion_authorizer_portal/models/sale_order.py b/fusion_authorizer_portal/models/sale_order.py index 38f2c0cd..5022dd66 100644 --- a/fusion_authorizer_portal/models/sale_order.py +++ b/fusion_authorizer_portal/models/sale_order.py @@ -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) diff --git a/fusion_authorizer_portal/views/portal_book_assessment.xml b/fusion_authorizer_portal/views/portal_book_assessment.xml index c3cdef00..13882fee 100644 --- a/fusion_authorizer_portal/views/portal_book_assessment.xml +++ b/fusion_authorizer_portal/views/portal_book_assessment.xml @@ -45,6 +45,25 @@ + +