From 13fabb0e79a39cd79401806fa68b503876bf965a Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Tue, 2 Jun 2026 08:23:43 -0400 Subject: [PATCH] feat(fusion_portal): assessment-visit redesign - live on westin 19.0.2.9.0 Bundles multiple assessments per home visit; on completion groups them by funding workflow (x_fc_sale_type) into one draft sale order per workflow (March of Dimes / ADP / ODSP / WSIB / private / hardship / insurance). Adds the mobility scooter ADP device type, the power-mobility home-access rule, ADP multi-device combination guard, and the portal visit workspace. Verified on a westin-v19 clone (clean registry load + funding-grouping smoke test) then deployed to westin prod (fusion_portal 19.0.2.9.0). Prod's pre-existing orphaned tax links were preserved (Odoo skips existing FKs), pending a later audit. Co-Authored-By: Claude Opus 4.8 (1M context) --- fusion_portal/__manifest__.py | 5 +- .../controllers/portal_assessment.py | 10 +- fusion_portal/controllers/portal_main.py | 68 ++++- fusion_portal/data/visit_data.xml | 13 + fusion_portal/models/__init__.py | 1 + .../models/accessibility_assessment.py | 9 +- fusion_portal/models/assessment.py | 36 ++- fusion_portal/models/sale_order.py | 11 + fusion_portal/models/visit.py | 280 ++++++++++++++++++ fusion_portal/security/ir.model.access.csv | 2 + .../views/portal_accessibility_templates.xml | 10 + .../views/portal_assessment_express.xml | 63 +++- fusion_portal/views/portal_visit.xml | 115 +++++++ fusion_portal/views/visit_views.xml | 108 +++++++ 14 files changed, 722 insertions(+), 9 deletions(-) create mode 100644 fusion_portal/data/visit_data.xml create mode 100644 fusion_portal/models/visit.py create mode 100644 fusion_portal/views/portal_visit.xml create mode 100644 fusion_portal/views/visit_views.xml diff --git a/fusion_portal/__manifest__.py b/fusion_portal/__manifest__.py index a210e6de..7e7ae2f4 100644 --- a/fusion_portal/__manifest__.py +++ b/fusion_portal/__manifest__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- { 'name': 'Fusion Authorizer & Sales Portal', - 'version': '19.0.2.8.0', + 'version': '19.0.2.9.0', 'category': 'Sales/Portal', 'summary': 'Portal for Authorizers (OTs) and Sales Reps with Assessment Forms', 'description': """ @@ -64,12 +64,14 @@ This module provides external portal access for: 'data/portal_menu_data.xml', 'data/ir_actions_server_data.xml', 'data/welcome_articles.xml', + 'data/visit_data.xml', # Views 'views/res_partner_views.xml', 'views/sale_order_views.xml', 'views/assessment_views.xml', 'views/loaner_checkout_views.xml', 'views/pdf_template_views.xml', + 'views/visit_views.xml', # Portal Templates 'views/portal_templates.xml', 'views/portal_assessment_express.xml', @@ -79,6 +81,7 @@ This module provides external portal access for: 'views/portal_technician_templates.xml', 'views/portal_book_assessment.xml', 'views/portal_page11_sign_templates.xml', + 'views/portal_visit.xml', ], 'assets': { 'web.assets_backend': [ diff --git a/fusion_portal/controllers/portal_assessment.py b/fusion_portal/controllers/portal_assessment.py index 0c242f23..7a69d431 100644 --- a/fusion_portal/controllers/portal_assessment.py +++ b/fusion_portal/controllers/portal_assessment.py @@ -815,7 +815,15 @@ class AssessmentPortal(CustomerPortal): vals['wheelchair_type'] = kw.get('wheelchair_type') if kw.get('powerchair_type'): vals['powerchair_type'] = kw.get('powerchair_type') - + if kw.get('scooter_type'): + vals['scooter_type'] = kw.get('scooter_type') + if kw.get('scooter_max_range'): + vals['scooter_max_range'] = float(kw.get('scooter_max_range') or 0) + if kw.get('x_fc_power_home_accessible'): + vals['x_fc_power_home_accessible'] = kw.get('x_fc_power_home_accessible') + if kw.get('x_fc_power_home_access_notes'): + vals['x_fc_power_home_access_notes'] = kw.get('x_fc_power_home_access_notes') + # Float measurements float_fields = [ 'rollator_handle_height', 'rollator_seat_height', diff --git a/fusion_portal/controllers/portal_main.py b/fusion_portal/controllers/portal_main.py index 0563e099..42c498e4 100644 --- a/fusion_portal/controllers/portal_main.py +++ b/fusion_portal/controllers/portal_main.py @@ -2479,6 +2479,56 @@ class AuthorizerPortal(CustomerPortal): template = template_map.get(assessment_type, 'fusion_portal.portal_accessibility_selector') return request.render(template, values) + # ========================================================================== + # ASSESSMENT VISIT WORKSPACE (Phase 1b/3) + # ========================================================================== + @http.route('/my/visit/new', type='http', auth='user', website=True) + def visit_new(self, **kw): + """Start a new assessment visit and open its workspace.""" + partner = request.env.user.partner_id + if not partner.is_sales_rep_portal and not partner.is_authorizer: + return request.redirect('/my') + visit = request.env['fusion.assessment.visit'].sudo().create({ + 'sales_rep_id': request.env.user.id, + }) + return request.redirect('/my/visit/%s' % visit.id) + + @http.route('/my/visit/', type='http', auth='user', website=True) + def visit_workspace(self, visit_id, **kw): + visit = request.env['fusion.assessment.visit'].sudo().browse(visit_id) + if not visit.exists(): + return request.redirect('/my') + return request.render('fusion_portal.portal_visit_workspace', { + 'visit': visit, + 'page_name': 'visit', + 'error': kw.get('error'), + }) + + @http.route('/my/visit//save', type='http', auth='user', methods=['POST'], website=True, csrf=True) + def visit_save_client(self, visit_id, **post): + visit = request.env['fusion.assessment.visit'].sudo().browse(visit_id) + if visit.exists(): + visit.write({ + 'client_name': (post.get('client_name') or '').strip(), + 'client_phone': (post.get('client_phone') or '').strip(), + 'client_email': (post.get('client_email') or '').strip(), + 'client_address': (post.get('client_address') or '').strip(), + 'x_fc_income_under_mod_threshold': post.get('x_fc_income_under_mod_threshold') or 'unknown', + }) + return request.redirect('/my/visit/%s' % visit_id) + + @http.route('/my/visit//complete', type='http', auth='user', methods=['POST'], website=True, csrf=True) + def visit_complete(self, visit_id, **post): + visit = request.env['fusion.assessment.visit'].sudo().browse(visit_id) + if not visit.exists(): + return request.redirect('/my') + try: + visit.action_complete_visit() + except Exception as e: + _logger.warning("Visit %s completion failed: %s", visit_id, e) + return request.redirect('/my/visit/%s?error=%s' % (visit_id, str(e))) + return request.redirect('/my/visit/%s' % visit_id) + @http.route('/my/accessibility/save', type='json', auth='user', methods=['POST'], csrf=True) def accessibility_assessment_save(self, **post): """Save an accessibility assessment and optionally create a Sale Order""" @@ -2547,6 +2597,13 @@ class AuthorizerPortal(CustomerPortal): if partner.is_authorizer: vals['authorizer_id'] = partner.id + # Link to a visit if this form was launched from the workspace. + if post.get('visit_id'): + try: + vals['visit_id'] = int(post.get('visit_id')) + except (TypeError, ValueError): + pass + # Create the assessment assessment = Assessment.create(vals) _logger.info(f"Created accessibility assessment {assessment.reference} by {request.env.user.name}") @@ -2572,8 +2629,12 @@ class AuthorizerPortal(CustomerPortal): if video_data: self._attach_accessibility_video(assessment, video_data, video_filename) - # Complete the assessment and create Sale Order if requested + # Complete the assessment and create Sale Order if requested. + # When launched from a visit, always save as a draft linked to the + # visit — the VISIT completion creates the grouped sale order(s). create_sale_order = post.get('create_sale_order', True) + if vals.get('visit_id'): + create_sale_order = False if create_sale_order: sale_order = assessment.action_complete() return { @@ -2586,12 +2647,13 @@ class AuthorizerPortal(CustomerPortal): 'redirect_url': f'/my/sales/case/{sale_order.id}', } else: + redirect_url = ('/my/visit/%s' % vals['visit_id']) if vals.get('visit_id') else '/my/accessibility/list' return { 'success': True, 'assessment_id': assessment.id, 'assessment_ref': assessment.reference, - 'message': f'Assessment {assessment.reference} saved as draft.', - 'redirect_url': '/my/accessibility/list', + 'message': f'Assessment {assessment.reference} saved.', + 'redirect_url': redirect_url, } except Exception as e: diff --git a/fusion_portal/data/visit_data.xml b/fusion_portal/data/visit_data.xml new file mode 100644 index 00000000..d5682dca --- /dev/null +++ b/fusion_portal/data/visit_data.xml @@ -0,0 +1,13 @@ + + + + + + Assessment Visit + fusion.assessment.visit + VISIT/%(year)s/ + 4 + + + + diff --git a/fusion_portal/models/__init__.py b/fusion_portal/models/__init__.py index 4109891b..c3ce7f47 100644 --- a/fusion_portal/models/__init__.py +++ b/fusion_portal/models/__init__.py @@ -7,5 +7,6 @@ from . import adp_document from . import assessment from . import accessibility_assessment from . import sale_order +from . import visit from . import loaner_checkout from . import pdf_template \ No newline at end of file diff --git a/fusion_portal/models/accessibility_assessment.py b/fusion_portal/models/accessibility_assessment.py index 10a5534d..a02f6e7c 100644 --- a/fusion_portal/models/accessibility_assessment.py +++ b/fusion_portal/models/accessibility_assessment.py @@ -157,7 +157,14 @@ class FusionAccessibilityAssessment(models.Model): readonly=True, copy=False, ) - + visit_id = fields.Many2one( + 'fusion.assessment.visit', + string='Assessment Visit', + ondelete='set null', + index=True, + help='The home visit this accessibility assessment belongs to.', + ) + # Dates assessment_date = fields.Date( string='Assessment Date', diff --git a/fusion_portal/models/assessment.py b/fusion_portal/models/assessment.py index b3de9c1a..de288bee 100644 --- a/fusion_portal/models/assessment.py +++ b/fusion_portal/models/assessment.py @@ -45,6 +45,7 @@ class FusionAssessment(models.Model): ('rollator', 'Rollator'), ('wheelchair', 'Wheelchair'), ('powerchair', 'Powerchair'), + ('scooter', 'Mobility Scooter'), ], string='Equipment Type', tracking=True, index=True) # Rollator Types @@ -69,6 +70,31 @@ class FusionAssessment(models.Model): ('type_2', 'Adult Power Base Type 2'), ('type_3', 'Adult Power Base Type 3'), ], string='Powerchair Type') + + # ===== MOBILITY SCOOTER (ADP) — 2026-06 Phase 2 ===== + scooter_type = fields.Selection([ + ('travel_3', '3-Wheel Travel/Portable'), + ('travel_4', '4-Wheel Travel/Portable'), + ('standard_3', '3-Wheel Standard'), + ('standard_4', '4-Wheel Standard'), + ('heavy_duty', 'Heavy-Duty / Bariatric'), + ], string='Scooter Type') + scooter_max_range = fields.Float( + string='Maximum Range Needed (km)', digits=(10, 1), + help='Maximum distance the client needs the scooter to travel on a charge.', + ) + + # ===== POWER-MOBILITY HOME ACCESSIBILITY (ADP hard rule) ===== + # Applies to scooter + power wheelchair: ADP funds power mobility only if the + # device can enter and be used at the residence independently, without lifting. + x_fc_power_home_accessible = fields.Selection([ + ('yes', 'Yes — usable inside and outside independently'), + ('no', 'No — home needs accessibility work'), + ], string='Home accessible for power-mobility device?', + help='ADP will not fund a scooter / power wheelchair if the home cannot ' + 'take it (device left outside or in the garage). If No, the home ' + 'needs an accessibility product (ramp / porch lift).') + x_fc_power_home_access_notes = fields.Text(string='Home Access Notes') # ===== EXPRESS FORM: ROLLATOR MEASUREMENTS ===== rollator_handle_height = fields.Float(string='Handle Height (inches)', digits=(10, 2)) @@ -425,7 +451,15 @@ class FusionAssessment(models.Model): readonly=True, copy=False, ) - + visit_id = fields.Many2one( + 'fusion.assessment.visit', + string='Assessment Visit', + ondelete='set null', + index=True, + help='The home visit this ADP assessment belongs to (groups multiple ' + 'assessments / funding workflows from one visit).', + ) + # ===== COMPUTED FIELDS ===== document_count = fields.Integer( string='Document Count', diff --git a/fusion_portal/models/sale_order.py b/fusion_portal/models/sale_order.py index 0d617ab2..404faf81 100644 --- a/fusion_portal/models/sale_order.py +++ b/fusion_portal/models/sale_order.py @@ -53,6 +53,17 @@ class SaleOrder(models.Model): 'sale order — stair lift, VPL, ceiling lift, ramp, bathroom mod, ' 'or tub cutout visits.', ) + + # Link to the assessment visit (one visit -> one SO per funding workflow). + visit_id = fields.Many2one( + 'fusion.assessment.visit', + string='Assessment Visit', + readonly=True, + index=True, + help='The home visit this sale order was generated from. A visit ' + 'produces one sale order per funding workflow (ADP / MOD / ODSP / ' + 'private / ...).', + ) # Authorizer helper field (consolidates multiple possible fields) portal_authorizer_id = fields.Many2one( diff --git a/fusion_portal/models/visit.py b/fusion_portal/models/visit.py new file mode 100644 index 00000000..010b78ee --- /dev/null +++ b/fusion_portal/models/visit.py @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*- +"""Assessment Visit — bundles the assessments done during one home visit. + +A sales rep + occupational therapist visit a client for 30-45 min and may do +several assessments (an ADP wheelchair plus accessibility products like a stair +lift, ramp, or tub cutout). The Visit is the hub that holds the client/context +ONCE and, on completion, groups its assessments by FUNDING WORKFLOW +(x_fc_sale_type) and creates ONE draft sale order per workflow — never one +combined SO, and never a separate SO per item within the same funding. + +See docs/superpowers/specs/2026-06-02-assessment-visit-funding-design.md. +""" +import logging +from markupsafe import Markup +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + +# Accessibility funding source -> sale.order x_fc_sale_type. Mirrors +# fusion.accessibility.assessment._create_draft_sale_order so a grouped Visit +# routes exactly the way a single accessibility assessment would. +ACCESSIBILITY_SALE_TYPE_MAP = { + 'march_of_dimes': 'march_of_dimes', + 'odsp': 'odsp', + 'wsib': 'wsib', + 'hardship': 'hardship', + 'insurance': 'insurance', + 'direct_private': 'direct_private', + 'other': 'other', +} + + +class FusionAssessmentVisit(models.Model): + _name = 'fusion.assessment.visit' + _description = 'Assessment Visit' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = 'visit_date desc, id desc' + + name = fields.Char( + string='Visit Reference', required=True, readonly=True, + copy=False, default=lambda self: _('New'), + ) + state = fields.Selection( + selection=[ + ('measuring', 'Measuring'), + ('client_pending', 'Client Details Pending'), + ('done', 'Completed'), + ('cancelled', 'Cancelled'), + ], + default='measuring', tracking=True, copy=False, + ) + + # --- Shared client + context (entered once for the whole visit) --------- + partner_id = fields.Many2one('res.partner', string='Client', tracking=True) + client_name = fields.Char(string='Client Name', tracking=True) + client_phone = fields.Char(string='Phone') + client_email = fields.Char(string='Email') + client_address = fields.Char(string='Address') + visit_date = fields.Date( + string='Visit Date', default=fields.Date.context_today, tracking=True, + ) + sales_rep_id = fields.Many2one( + 'res.users', string='Sales Rep', + default=lambda self: self.env.user, tracking=True, + ) + authorizer_id = fields.Many2one( + 'res.partner', string='Occupational Therapist', tracking=True, + ) + + # --- March of Dimes funding context (informational; spec §4.1) ---------- + # MOD covers up to $15,000 per person (lifetime), income-gated. + x_fc_income_under_mod_threshold = fields.Selection( + selection=[ + ('yes', 'Yes — under threshold (full $15k available)'), + ('no', 'No — over threshold (may be denied / partial)'), + ('unknown', 'Unknown'), + ], + string='Income under MOD threshold?', default='unknown', + help='March of Dimes funds up to $15,000 per person (lifetime) when the ' + "client's income is under that year's threshold; over it, MOD may " + 'deny or partially approve. Reminder only — no automatic cap ' + 'enforcement in this version.', + ) + + # --- Assessments performed during this visit ---------------------------- + adp_assessment_ids = fields.One2many( + 'fusion.assessment', 'visit_id', string='ADP Assessments', + ) + accessibility_assessment_ids = fields.One2many( + 'fusion.accessibility.assessment', 'visit_id', + string='Accessibility Assessments', + ) + + # --- Sale orders produced — one per funding workflow -------------------- + sale_order_ids = fields.One2many( + 'sale.order', 'visit_id', string='Sale Orders', + ) + + assessment_count = fields.Integer(compute='_compute_counts') + sale_order_count = fields.Integer(compute='_compute_counts') + has_mod_items = fields.Boolean(compute='_compute_has_mod_items') + + @api.depends('adp_assessment_ids', 'accessibility_assessment_ids', 'sale_order_ids') + def _compute_counts(self): + for visit in self: + visit.assessment_count = ( + len(visit.adp_assessment_ids) + + len(visit.accessibility_assessment_ids) + ) + visit.sale_order_count = len(visit.sale_order_ids) + + @api.depends('accessibility_assessment_ids.x_fc_funding_source') + def _compute_has_mod_items(self): + for visit in self: + visit.has_mod_items = any( + a.x_fc_funding_source == 'march_of_dimes' + for a in visit.accessibility_assessment_ids + ) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if not vals.get('name') or vals['name'] == _('New'): + seq = self.env['ir.sequence'].next_by_code('fusion.assessment.visit') + vals['name'] = seq or _('New') + return super().create(vals_list) + + # ------------------------------------------------------------------ + # Completion — group assessments by funding workflow → one SO each + # ------------------------------------------------------------------ + def _ensure_partner(self): + """Resolve the client partner for the visit, reusing an assessment's + partner/_ensure_partner when one is already set.""" + self.ensure_one() + if self.partner_id: + return self.partner_id + # Borrow a child assessment's partner resolution if available. + for assessment in self.accessibility_assessment_ids: + if assessment.partner_id: + return assessment.partner_id + if hasattr(assessment, '_ensure_partner'): + return assessment._ensure_partner() + for assessment in self.adp_assessment_ids: + if assessment.partner_id: + return assessment.partner_id + if self.client_name: + return self.env['res.partner'].sudo().create({ + 'name': self.client_name, + 'phone': self.client_phone or False, + 'email': self.client_email or False, + }) + raise UserError(_('Set a client (or client name) on the visit first.')) + + def _create_grouped_sale_order(self, partner, sale_type, accessibility_assessments): + """Create ONE draft sale order for a set of same-funding accessibility + assessments, link them all to it, and post each one's spec to chatter. + Mirrors fusion.accessibility.assessment._create_draft_sale_order but for + a group sharing one funding workflow.""" + self.ensure_one() + SaleOrder = self.env['sale.order'].sudo() + + 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': _('Visit %s (%s)') % (self.name, sale_type), + 'x_fc_sale_type': sale_type, + 'visit_id': self.id, + } + if self.authorizer_id: + so_vals['x_fc_authorizer_id'] = self.authorizer_id.id + # MOD: pre-fill the accessibility specialist from the sales rep. + 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) + for assessment in accessibility_assessments: + assessment.sale_order_id = sale_order.id + assessment._add_assessment_tag(sale_order) + sale_order.message_post( + body=Markup(assessment._format_assessment_html_table()), + message_type='comment', + subtype_xmlid='mail.mt_note', + ) + assessment.write({'state': 'completed'}) + _logger.info( + "Visit %s created %s sale order %s grouping %d accessibility assessment(s)", + self.name, sale_type, sale_order.name, len(accessibility_assessments), + ) + return sale_order + + def _validate_adp_combination(self, adp_assessments): + """Enforce ADP device-combination rules: at most one seated-mobility + device (manual wheelchair / power wheelchair / scooter), optionally one + walker/rollator, no duplicates.""" + seated_types = {'wheelchair', 'powerchair', 'scooter'} + seated = [a for a in adp_assessments if a.equipment_type in seated_types] + walkers = [a for a in adp_assessments if a.equipment_type == 'rollator'] + labels = dict(self.env['fusion.assessment']._fields['equipment_type'].selection) + if len(seated) > 1: + raise UserError(_( + 'An ADP order can include only one seated-mobility device ' + '(manual wheelchair, power wheelchair, or scooter). This visit has: %s.' + ) % ', '.join(labels.get(a.equipment_type, a.equipment_type) for a in seated)) + if len(walkers) > 1: + raise UserError(_('An ADP order can include only one walker / rollator.')) + + 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 + existing one-assessment-one-SO completion for now (ADP multi-device + grouping arrives in Phase 2).""" + self.ensure_one() + if self.state == 'done': + raise UserError(_('This visit is already completed.')) + if not (self.accessibility_assessment_ids or self.adp_assessment_ids): + raise UserError(_('Add at least one assessment before completing the visit.')) + + partner = self._ensure_partner() + + # Group accessibility assessments by their funding -> sale type. + by_sale_type = {} + for assessment in self.accessibility_assessment_ids: + if assessment.sale_order_id: + continue # already has an SO; don't duplicate + sale_type = ACCESSIBILITY_SALE_TYPE_MAP.get( + assessment.x_fc_funding_source, 'direct_private') + by_sale_type.setdefault(sale_type, []).append(assessment) + + for sale_type, group in by_sale_type.items(): + self._create_grouped_sale_order(partner, sale_type, group) + + # ADP equipment assessments: one ADP order per funding type, with the + # device-combination guard, reusing the existing (prod-tested) express + # completion. The first device creates the SO; the rest attach to it. + adp_by_type = {} + for assessment in self.adp_assessment_ids: + if assessment.sale_order_id: + continue + adp_by_type.setdefault(self._assessment_sale_type(assessment), []).append(assessment) + labels = dict(self.env['fusion.assessment']._fields['equipment_type'].selection) + for sale_type, group in adp_by_type.items(): + self._validate_adp_combination(group) + # Make sure each device carries the visit's client + OT so the + # existing completion logic has what it needs. + for assessment in group: + vals = {} + if not assessment.client_name: + vals['client_name'] = self.client_name or partner.name + if not assessment.authorizer_id and self.authorizer_id: + vals['authorizer_id'] = self.authorizer_id.id + if not assessment.partner_id: + vals['partner_id'] = partner.id + if vals: + assessment.write(vals) + primary = group[0] + sale_order = primary.action_complete_express() + sale_order.write({'visit_id': self.id, 'x_fc_sale_type': sale_type}) + for extra in group[1:]: + extra.write({'state': 'completed', 'sale_order_id': sale_order.id}) + sale_order.message_post( + body=Markup('

Additional ADP device on this order: %s

') + % labels.get(extra.equipment_type, extra.equipment_type or 'device'), + subtype_xmlid='mail.mt_note', + ) + + self.write({'state': 'done', 'partner_id': partner.id}) + return self._action_view_sale_orders() + + def _action_view_sale_orders(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': _('Visit Sale Orders'), + 'res_model': 'sale.order', + 'view_mode': 'list,form', + 'domain': [('visit_id', '=', self.id)], + 'context': {'create': False}, + } diff --git a/fusion_portal/security/ir.model.access.csv b/fusion_portal/security/ir.model.access.csv index 10c6658c..125a2575 100644 --- a/fusion_portal/security/ir.model.access.csv +++ b/fusion_portal/security/ir.model.access.csv @@ -7,6 +7,8 @@ access_fusion_assessment_user,fusion.assessment.user,model_fusion_assessment,bas access_fusion_assessment_portal,fusion.assessment.portal,model_fusion_assessment,base.group_portal,1,1,1,0 access_fusion_accessibility_assessment_user,fusion.accessibility.assessment.user,model_fusion_accessibility_assessment,base.group_user,1,1,1,1 access_fusion_accessibility_assessment_portal,fusion.accessibility.assessment.portal,model_fusion_accessibility_assessment,base.group_portal,1,1,1,0 +access_fusion_assessment_visit_user,fusion.assessment.visit.user,model_fusion_assessment_visit,base.group_user,1,1,1,1 +access_fusion_assessment_visit_portal,fusion.assessment.visit.portal,model_fusion_assessment_visit,base.group_portal,1,1,1,0 access_fusion_pdf_template_user,fusion.pdf.template.user,model_fusion_pdf_template,base.group_user,1,1,1,1 access_fusion_pdf_template_preview_user,fusion.pdf.template.preview.user,model_fusion_pdf_template_preview,base.group_user,1,1,1,1 access_fusion_pdf_template_field_user,fusion.pdf.template.field.user,model_fusion_pdf_template_field,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/fusion_portal/views/portal_accessibility_templates.xml b/fusion_portal/views/portal_accessibility_templates.xml index fcd1d35f..34176ab0 100644 --- a/fusion_portal/views/portal_accessibility_templates.xml +++ b/fusion_portal/views/portal_accessibility_templates.xml @@ -388,6 +388,7 @@ Determines which sale order / funding workflow this case enters. + @@ -647,6 +648,15 @@ // Fallback if Google Maps not loaded window.initAddressAutocomplete = window.initAddressAutocomplete || function() {}; + // Carry visit_id from the workspace launch (?visit_id=) into the form + (function() { + var _vid = new URLSearchParams(window.location.search).get('visit_id'); + if (_vid) { + var f = document.getElementById('acc_visit_id'); + if (f) { f.value = _vid; } + } + })(); + // Form submission function saveAssessment(createSaleOrder) { var form = document.getElementById('accessibility_form'); diff --git a/fusion_portal/views/portal_assessment_express.xml b/fusion_portal/views/portal_assessment_express.xml index d2a625d7..b265e5be 100644 --- a/fusion_portal/views/portal_assessment_express.xml +++ b/fusion_portal/views/portal_assessment_express.xml @@ -98,6 +98,7 @@ + @@ -688,8 +689,62 @@ + + +
+ + +
+ ADP funds power mobility only if the device can enter and be used at the residence independently, without lifting (not left outside / in the garage). If No, add an accessibility assessment (ramp / porch lift) for the home. +
+ +
- + + + + @@ -1278,6 +1333,7 @@ var rollatorForm = document.getElementById('rollator_form'); var wheelchairForm = document.getElementById('wheelchair_form'); var powerchairForm = document.getElementById('powerchair_form'); + var scooterForm = document.getElementById('scooter_form'); var wheelchairTypeSelect = document.querySelector('select[name="wheelchair_type"]'); var reasonSelect = document.getElementById('reason_for_application'); var previousFundingContainer = document.getElementById('previous_funding_date_container'); @@ -1339,13 +1395,16 @@ disableFormInputs(rollatorForm); disableFormInputs(wheelchairForm); disableFormInputs(powerchairForm); - + disableFormInputs(scooterForm); + if (value === 'rollator') { enableFormInputs(rollatorForm); } else if (value === 'wheelchair') { enableFormInputs(wheelchairForm); } else if (value === 'powerchair') { enableFormInputs(powerchairForm); + } else if (value === 'scooter') { + enableFormInputs(scooterForm); } } diff --git a/fusion_portal/views/portal_visit.xml b/fusion_portal/views/portal_visit.xml new file mode 100644 index 00000000..cd615077 --- /dev/null +++ b/fusion_portal/views/portal_visit.xml @@ -0,0 +1,115 @@ + + + + diff --git a/fusion_portal/views/visit_views.xml b/fusion_portal/views/visit_views.xml new file mode 100644 index 00000000..92779ff0 --- /dev/null +++ b/fusion_portal/views/visit_views.xml @@ -0,0 +1,108 @@ + + + + + fusion.assessment.visit.form + fusion.assessment.visit + +
+
+
+ +
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + fusion.assessment.visit.list + fusion.assessment.visit + + + + + + + + + + + + + + + + + Assessment Visits + fusion.assessment.visit + list,form + + + +