# -*- coding: utf-8 -*- from odoo import http, fields, _ from odoo.http import request from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager from odoo.exceptions import AccessError, MissingError, ValidationError import json import base64 import logging from datetime import datetime from markupsafe import Markup _logger = logging.getLogger(__name__) class AssessmentPortal(CustomerPortal): """Portal controller for Assessments""" @http.route(['/my/assessments', '/my/assessments/page/'], type='http', auth='user', website=True) def portal_assessments(self, page=1, search='', state='', sortby='date', **kw): """List of assessments""" partner = request.env.user.partner_id user = request.env.user if not partner.is_authorizer and not partner.is_sales_rep_portal: return request.redirect('/my') Assessment = request.env['fusion.assessment'].sudo() # Build domain based on role domain = [] if partner.is_authorizer and partner.is_sales_rep_portal: domain = ['|', ('authorizer_id', '=', partner.id), ('sales_rep_id', '=', user.id)] elif partner.is_authorizer: domain = [('authorizer_id', '=', partner.id)] elif partner.is_sales_rep_portal: domain = [('sales_rep_id', '=', user.id)] # Add state filter if state: domain.append(('state', '=', state)) # Add search filter if search: domain = domain + [ '|', '|', ('client_name', 'ilike', search), ('reference', 'ilike', search), ('client_email', 'ilike', search), ] # Sorting sortings = { 'date': {'label': _('Date'), 'order': 'assessment_date desc'}, 'name': {'label': _('Client'), 'order': 'client_name'}, 'reference': {'label': _('Reference'), 'order': 'reference'}, 'state': {'label': _('Status'), 'order': 'state'}, } order = sortings.get(sortby, sortings['date'])['order'] # Pager assessment_count = Assessment.search_count(domain) pager = portal_pager( url='/my/assessments', url_args={'search': search, 'state': state, 'sortby': sortby}, total=assessment_count, page=page, step=20, ) # Get assessments assessments = Assessment.search(domain, order=order, limit=20, offset=pager['offset']) # State options for filter state_options = [ ('', _('All')), ('draft', _('In Progress')), ('pending_signature', _('Pending Signatures')), ('completed', _('Completed')), ('cancelled', _('Cancelled')), ] values = { 'assessments': assessments, 'pager': pager, 'search': search, 'state': state, 'state_options': state_options, 'sortby': sortby, 'sortings': sortings, 'page_name': 'assessments', } return request.render('fusion_authorizer_portal.portal_assessments', values) @http.route('/my/assessment/new', type='http', auth='user', website=True) def portal_assessment_new(self, **kw): """Start a new assessment""" partner = request.env.user.partner_id user = request.env.user if not partner.is_authorizer and not partner.is_sales_rep_portal: return request.redirect('/my') # Get list of authorizers for dropdown (if sales rep starting assessment) authorizers = request.env['res.partner'].sudo().search([ ('is_authorizer', '=', True), ]) values = { 'partner': partner, 'user': user, 'authorizers': authorizers, 'countries': request.env['res.country'].sudo().search([]), 'default_country': request.env.ref('base.ca', raise_if_not_found=False), 'page_name': 'assessment_new', } return request.render('fusion_authorizer_portal.portal_assessment_form', values) @http.route('/my/assessment/', type='http', auth='user', website=True) def portal_assessment_view(self, assessment_id, **kw): """View/edit an assessment""" partner = request.env.user.partner_id user = request.env.user if not partner.is_authorizer and not partner.is_sales_rep_portal: return request.redirect('/my') try: assessment = request.env['fusion.assessment'].sudo().browse(assessment_id) if not assessment.exists(): raise MissingError(_('Assessment not found.')) # Check access has_access = ( (partner.is_authorizer and assessment.authorizer_id.id == partner.id) or (partner.is_sales_rep_portal and assessment.sales_rep_id.id == user.id) ) if not has_access: raise AccessError(_('You do not have access to this assessment.')) except (AccessError, MissingError): return request.redirect('/my/assessments') # Get list of authorizers for dropdown authorizers = request.env['res.partner'].sudo().search([ ('is_authorizer', '=', True), ]) # Get assessment photos photos = request.env['ir.attachment'].sudo().search([ ('res_model', '=', 'fusion.assessment'), ('res_id', '=', assessment.id), ('mimetype', 'like', 'image/%'), ]) values = { 'assessment': assessment, 'partner': partner, 'user': user, 'authorizers': authorizers, 'countries': request.env['res.country'].sudo().search([]), 'page_name': 'assessment_edit', 'is_readonly': assessment.state in ['completed', 'cancelled'], 'photos': photos, } return request.render('fusion_authorizer_portal.portal_assessment_form', values) @http.route('/my/assessment/save', type='http', auth='user', website=True, methods=['POST'], csrf=True) def portal_assessment_save(self, assessment_id=None, **kw): """Save assessment data (create or update)""" partner = request.env.user.partner_id user = request.env.user if not partner.is_authorizer and not partner.is_sales_rep_portal: return request.redirect('/my') Assessment = request.env['fusion.assessment'].sudo() # Prepare values vals = { 'client_name': kw.get('client_name', ''), 'client_first_name': kw.get('client_first_name', ''), 'client_last_name': kw.get('client_last_name', ''), 'client_street': kw.get('client_street', ''), 'client_unit': kw.get('client_unit', ''), 'client_city': kw.get('client_city', ''), 'client_state': kw.get('client_state', 'Ontario'), 'client_postal_code': kw.get('client_postal_code', ''), 'client_phone': kw.get('client_phone', ''), 'client_mobile': kw.get('client_mobile', ''), 'client_email': kw.get('client_email', ''), 'client_reference_1': kw.get('client_reference_1', ''), 'client_reference_2': kw.get('client_reference_2', ''), 'assessment_location': kw.get('assessment_location', 'home'), 'assessment_location_notes': kw.get('assessment_location_notes', ''), } # Wheelchair specifications float_fields = [ 'seat_width', 'seat_depth', 'seat_to_floor_height', 'back_height', 'armrest_height', 'footrest_length', 'overall_width', 'overall_length', 'overall_height', 'seat_angle', 'back_angle', 'client_weight', 'client_height' ] for field in float_fields: if kw.get(field): try: vals[field] = float(kw.get(field)) except (ValueError, TypeError): pass # Selection fields selection_fields = ['cushion_type', 'backrest_type', 'frame_type', 'wheel_type'] for field in selection_fields: if kw.get(field): vals[field] = kw.get(field) # Text fields text_fields = ['cushion_notes', 'backrest_notes', 'frame_notes', 'wheel_notes', 'mobility_notes', 'accessibility_notes', 'special_requirements', 'diagnosis'] for field in text_fields: if kw.get(field): vals[field] = kw.get(field) # Authorizer if kw.get('authorizer_id'): try: vals['authorizer_id'] = int(kw.get('authorizer_id')) except (ValueError, TypeError): pass # Country if kw.get('client_country_id'): try: vals['client_country_id'] = int(kw.get('client_country_id')) except (ValueError, TypeError): pass try: if assessment_id and assessment_id != 'None': # Update existing assessment = Assessment.browse(int(assessment_id)) if not assessment.exists(): raise MissingError(_('Assessment not found.')) # Check access has_access = ( (partner.is_authorizer and assessment.authorizer_id.id == partner.id) or (partner.is_sales_rep_portal and assessment.sales_rep_id.id == user.id) ) if not has_access: raise AccessError(_('You do not have access to this assessment.')) if assessment.state not in ['draft', 'pending_signature']: raise ValidationError(_('Cannot modify a completed or cancelled assessment.')) assessment.write(vals) _logger.info(f"Updated assessment {assessment.reference}") else: # Create new vals['sales_rep_id'] = user.id if partner.is_authorizer: vals['authorizer_id'] = partner.id assessment = Assessment.create(vals) _logger.info(f"Created new assessment {assessment.reference}") # Redirect based on action action = kw.get('action', 'save') if action == 'save_signatures': return request.redirect(f'/my/assessment/{assessment.id}/signatures') elif action == 'save_exit': return request.redirect('/my/assessments') else: return request.redirect(f'/my/assessment/{assessment.id}') except Exception as e: _logger.error(f"Error saving assessment: {e}") return request.redirect('/my/assessments') @http.route('/my/assessment//signatures', type='http', auth='user', website=True) def portal_assessment_signatures(self, assessment_id, **kw): """Signature capture page""" partner = request.env.user.partner_id user = request.env.user if not partner.is_authorizer and not partner.is_sales_rep_portal: return request.redirect('/my') try: assessment = request.env['fusion.assessment'].sudo().browse(assessment_id) if not assessment.exists(): raise MissingError(_('Assessment not found.')) # Check access has_access = ( (partner.is_authorizer and assessment.authorizer_id.id == partner.id) or (partner.is_sales_rep_portal and assessment.sales_rep_id.id == user.id) ) if not has_access: raise AccessError(_('You do not have access to this assessment.')) except (AccessError, MissingError): return request.redirect('/my/assessments') values = { 'assessment': assessment, 'partner': partner, 'page_name': 'assessment_signatures', } return request.render('fusion_authorizer_portal.portal_assessment_signatures', values) @http.route('/my/assessment//save_signature', type='jsonrpc', auth='user') def portal_save_signature(self, assessment_id, signature_type='', signature_data='', signer_name='', **kw): """Save a signature (AJAX)""" partner = request.env.user.partner_id user = request.env.user if not partner.is_authorizer and not partner.is_sales_rep_portal: return {'success': False, 'error': 'Access denied'} try: assessment = request.env['fusion.assessment'].sudo().browse(assessment_id) if not assessment.exists(): return {'success': False, 'error': 'Assessment not found'} # Check access has_access = ( (partner.is_authorizer and assessment.authorizer_id.id == partner.id) or (partner.is_sales_rep_portal and assessment.sales_rep_id.id == user.id) ) if not has_access: return {'success': False, 'error': 'Access denied'} if not signature_data: return {'success': False, 'error': 'No signature data provided'} # Remove data URL prefix if present if signature_data.startswith('data:image'): signature_data = signature_data.split(',')[1] vals = {} if signature_type == 'page_11': vals = { 'signature_page_11': signature_data, 'signature_page_11_name': signer_name, 'signature_page_11_date': datetime.now(), } elif signature_type == 'page_12': vals = { 'signature_page_12': signature_data, 'signature_page_12_name': signer_name, 'signature_page_12_date': datetime.now(), } else: return {'success': False, 'error': 'Invalid signature type'} assessment.write(vals) # Update state if needed if assessment.state == 'draft': assessment.state = 'pending_signature' return { 'success': True, 'signatures_complete': assessment.signatures_complete, } except Exception as e: _logger.error(f"Error saving signature: {e}") return {'success': False, 'error': str(e)} @http.route('/my/assessment//complete', type='http', auth='user', website=True, methods=['POST'], csrf=True) def portal_assessment_complete(self, assessment_id, **kw): """Complete the assessment""" partner = request.env.user.partner_id user = request.env.user if not partner.is_authorizer and not partner.is_sales_rep_portal: return request.redirect('/my') try: assessment = request.env['fusion.assessment'].sudo().browse(assessment_id) if not assessment.exists(): raise MissingError(_('Assessment not found.')) # Check access has_access = ( (partner.is_authorizer and assessment.authorizer_id.id == partner.id) or (partner.is_sales_rep_portal and assessment.sales_rep_id.id == user.id) ) if not has_access: raise AccessError(_('You do not have access to this assessment.')) # Complete the assessment result = assessment.action_complete() # Redirect to the created sale order or assessments list if assessment.sale_order_id: return request.redirect('/my/assessments?message=completed') else: return request.redirect('/my/assessments') except ValidationError as e: _logger.warning(f"Validation error completing assessment: {e}") return request.redirect(f'/my/assessment/{assessment_id}/signatures?error=signatures_required') except Exception as e: _logger.error(f"Error completing assessment: {e}") return request.redirect(f'/my/assessment/{assessment_id}?error=1') # ========================================================================== # EXPRESS ASSESSMENT FORM ROUTES # ========================================================================== @http.route('/my/assessment/express', type='http', auth='user', website=True) def portal_assessment_express_new(self, **kw): """Start a new express assessment (Page 1 - Equipment Selection)""" partner = request.env.user.partner_id user = request.env.user if not partner.is_sales_rep_portal: return request.redirect('/my') # Get list of authorizers for dropdown authorizers = request.env['res.partner'].sudo().search([ ('is_authorizer', '=', True), ], order='name') # JSON-safe authorizer list for searchable dropdown (Markup so t-out won't escape) authorizers_json = Markup(json.dumps([ {'id': a.id, 'name': a.name, 'email': a.email or ''} for a in authorizers ])) # Get existing clients for dropdown clients = request.env['res.partner'].sudo().search([ ('customer_rank', '>', 0), ], order='name', limit=500) # Get Google Maps API key ICP = request.env['ir.config_parameter'].sudo() google_maps_api_key = ICP.get_param('fusion_claims.google_maps_api_key', '') values = { 'partner': partner, 'user': user, 'authorizers': authorizers, 'authorizers_json': authorizers_json, 'clients': clients, 'countries': request.env['res.country'].sudo().search([]), 'provinces': self._get_canadian_provinces(), 'default_country': request.env.ref('base.ca', raise_if_not_found=False), 'page_name': 'assessment_express', 'current_page': 1, 'total_pages': 2, 'assessment': None, 'google_maps_api_key': google_maps_api_key, } return request.render('fusion_authorizer_portal.portal_assessment_express', values) @http.route('/my/assessment/express/', type='http', auth='user', website=True) def portal_assessment_express_edit(self, assessment_id, page=1, **kw): """Continue/edit an express assessment""" partner = request.env.user.partner_id user = request.env.user if not partner.is_sales_rep_portal: return request.redirect('/my') try: assessment = request.env['fusion.assessment'].sudo().browse(assessment_id) if not assessment.exists(): raise MissingError(_('Assessment not found.')) # Check access - must be the sales rep who created it if assessment.sales_rep_id.id != user.id: raise AccessError(_('You do not have access to this assessment.')) if assessment.state in ['cancelled']: return request.redirect('/my/assessments') except (AccessError, MissingError): return request.redirect('/my/assessments') # Get list of authorizers for dropdown authorizers = request.env['res.partner'].sudo().search([ ('is_authorizer', '=', True), ], order='name') # JSON-safe authorizer list for searchable dropdown (Markup so t-out won't escape) authorizers_json = Markup(json.dumps([ {'id': a.id, 'name': a.name, 'email': a.email or ''} for a in authorizers ])) # Get existing clients for dropdown clients = request.env['res.partner'].sudo().search([ ('customer_rank', '>', 0), ], order='name', limit=500) try: current_page = int(page) except (ValueError, TypeError): current_page = 1 # Get Google Maps API key ICP = request.env['ir.config_parameter'].sudo() google_maps_api_key = ICP.get_param('fusion_claims.google_maps_api_key', '') values = { 'partner': partner, 'user': user, 'assessment': assessment, 'authorizers': authorizers, 'authorizers_json': authorizers_json, 'clients': clients, 'countries': request.env['res.country'].sudo().search([]), 'provinces': self._get_canadian_provinces(), 'default_country': request.env.ref('base.ca', raise_if_not_found=False), 'page_name': 'assessment_express', 'current_page': current_page, 'total_pages': 2, 'google_maps_api_key': google_maps_api_key, } return request.render('fusion_authorizer_portal.portal_assessment_express', values) @http.route('/my/assessment/express/save', type='http', auth='user', website=True, methods=['POST'], csrf=True) def portal_assessment_express_save(self, **kw): """Save express assessment data (create or update)""" partner = request.env.user.partner_id user = request.env.user if not partner.is_sales_rep_portal: return request.redirect('/my') Assessment = request.env['fusion.assessment'].sudo() assessment_id = kw.get('assessment_id') current_page = int(kw.get('current_page', 1)) action = kw.get('action', 'next') # next, back, save, submit # Build values from form vals = self._build_express_assessment_vals(kw) try: if assessment_id and assessment_id != 'None' and assessment_id != '': # Update existing assessment = Assessment.browse(int(assessment_id)) if not assessment.exists(): raise MissingError(_('Assessment not found.')) if assessment.sales_rep_id.id != user.id: raise AccessError(_('You do not have access to this assessment.')) if assessment.state == 'completed': # Allow updating ONLY consent/signature fields on completed assessments consent_fields = { 'consent_signed_by', 'consent_declaration_accepted', 'consent_date', 'agent_relationship', 'agent_first_name', 'agent_last_name', 'agent_middle_initial', 'agent_unit', 'agent_street_number', 'agent_street_name', 'agent_city', 'agent_province', 'agent_postal_code', 'agent_home_phone', 'agent_business_phone', 'agent_phone_ext', } consent_vals = {k: v for k, v in vals.items() if k in consent_fields} if consent_vals: assessment.write(consent_vals) _logger.info(f"Updated consent fields on completed assessment {assessment.reference}") elif assessment.state == 'cancelled': raise ValidationError(_('Cannot modify a cancelled assessment.')) else: # Draft - allow full update assessment.write(vals) _logger.info(f"Updated express assessment {assessment.reference}") else: # Create new vals['sales_rep_id'] = user.id vals['state'] = 'draft' assessment = Assessment.create(vals) _logger.info(f"Created new express assessment {assessment.reference}") # Handle photo uploads uploaded_photos = request.httprequest.files.getlist('assessment_photos') if uploaded_photos: for photo_file in uploaded_photos: if photo_file and photo_file.filename: try: file_content = photo_file.read() file_base64 = base64.b64encode(file_content) # Create attachment linked to assessment attachment = request.env['ir.attachment'].sudo().create({ 'name': photo_file.filename, 'type': 'binary', 'datas': file_base64, 'res_model': 'fusion.assessment', 'res_id': assessment.id, 'mimetype': photo_file.content_type or 'image/jpeg', }) _logger.info(f"Uploaded assessment photo: {photo_file.filename}") except Exception as e: _logger.error(f"Error uploading photo {photo_file.filename}: {e}") # ===== Handle Page 11 signature capture ===== signature_data = kw.get('signature_page_11_data', '') if signature_data and signature_data.startswith('data:image/'): try: # Strip data URL prefix: "data:image/png;base64,..." sig_base64 = signature_data.split(',', 1)[1] sig_vals = { 'signature_page_11': sig_base64, 'signature_page_11_date': fields.Datetime.now(), } # Set signer name if kw.get('consent_signed_by') == 'agent' and kw.get('agent_first_name'): sig_vals['signature_page_11_name'] = ( f"{kw.get('agent_first_name', '')} {kw.get('agent_last_name', '')}" ).strip() else: sig_vals['signature_page_11_name'] = ( f"{kw.get('client_first_name', '')} {kw.get('client_last_name', '')}" ).strip() assessment.write(sig_vals) _logger.info(f"Saved Page 11 signature for assessment {assessment.reference}") except Exception as e: _logger.error(f"Error saving Page 11 signature: {e}") # Handle navigation if action == 'submit': # If already completed, we just saved consent/signature above -- redirect with success if assessment.state == 'completed': # Generate filled PDF if signature was added 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', }) # Update sale order too # Issue 8 fix: bypass document lock since this is a portal # re-signing on an already-completed assessment (SO may have # progressed past 'submitted' where the lock kicks in) if assessment.sale_order_id: assessment.sale_order_id.with_context( skip_document_lock_validation=True ).write({ 'x_fc_signed_pages_11_12': b64.b64encode(pdf_bytes), 'x_fc_signed_pages_filename': f'ADP_Page11_{assessment.reference}.pdf', }) _logger.info(f"Generated Page 11 PDF for completed assessment {assessment.reference}") except Exception as pdf_e: _logger.warning(f"PDF generation failed (non-blocking): {pdf_e}") # Post consent & signature info to sale order chatter if assessment.sale_order_id and assessment.signature_page_11: try: from markupsafe import Markup signer = assessment.signature_page_11_name or 'Unknown' signed_by = 'Applicant' if assessment.consent_signed_by == 'applicant' else 'Agent' consent_date = str(assessment.consent_date) if assessment.consent_date else 'N/A' # Create signature as attachment sig_att = request.env['ir.attachment'].sudo().create({ 'name': f'Page11_Signature_{assessment.reference}.png', 'type': 'binary', 'datas': assessment.signature_page_11, 'res_model': 'sale.order', 'res_id': assessment.sale_order_id.id, 'mimetype': 'image/png', }) body = Markup( '' ) assessment.sale_order_id.message_post( body=body, message_type='comment', subtype_xmlid='mail.mt_note', attachment_ids=[sig_att.id], ) _logger.info(f"Posted Page 11 consent info to SO {assessment.sale_order_id.name}") except Exception as chat_e: _logger.warning(f"Failed to post consent to chatter: {chat_e}") so_id = assessment.sale_order_id.id if assessment.sale_order_id else '' return request.redirect(f'/my/assessments?message=completed&so={so_id}') # Complete the express assessment try: sale_order = assessment.action_complete_express() # Post assessment photos to sale order chatter photo_attachments = request.env['ir.attachment'].sudo().search([ ('res_model', '=', 'fusion.assessment'), ('res_id', '=', assessment.id), ('mimetype', 'like', 'image/%'), ]) if photo_attachments: # Copy attachments to sale order attachment_ids = [] for att in photo_attachments: new_att = att.copy({ 'res_model': 'sale.order', 'res_id': sale_order.id, }) attachment_ids.append(new_att.id) # Post message to chatter with photos sale_order.message_post( body=f"

Assessment Photos
Photos from assessment {assessment.reference} by {request.env.user.name}

", message_type='comment', subtype_xmlid='mail.mt_comment', attachment_ids=attachment_ids, ) _logger.info(f"Posted {len(attachment_ids)} assessment photos to sale order {sale_order.name}") # Process loaner checkout if loaner data was submitted loaner_product_id = kw.get('loaner_product_id') loaner_checkout_flag = kw.get('loaner_checkout', '0') if loaner_product_id and loaner_checkout_flag == '1': try: loaner_vals = { 'product_id': int(loaner_product_id), 'sale_order_id': sale_order.id, 'partner_id': sale_order.partner_id.id, 'loaner_period_days': int(kw.get('loaner_period_days', 7)), 'checkout_condition': kw.get('loaner_condition', 'good'), 'checkout_notes': kw.get('loaner_notes', ''), 'sales_rep_id': request.env.user.id, } if sale_order.x_fc_authorizer_id: loaner_vals['authorizer_id'] = sale_order.x_fc_authorizer_id.id if sale_order.partner_shipping_id: loaner_vals['delivery_address'] = sale_order.partner_shipping_id.contact_address loaner_lot = kw.get('loaner_lot_id') if loaner_lot: loaner_vals['lot_id'] = int(loaner_lot) checkout = request.env['fusion.loaner.checkout'].sudo().create(loaner_vals) checkout.action_checkout() _logger.info(f"Created loaner checkout {checkout.name} for SO {sale_order.name}") except Exception as le: _logger.error(f"Error creating loaner checkout: {le}") # ===== Generate filled Page 11 PDF if signature exists ===== 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', }) # Also store on sale order # Issue 8 fix: bypass document lock for portal writes sale_order.with_context( skip_document_lock_validation=True ).write({ 'x_fc_signed_pages_11_12': b64.b64encode(pdf_bytes), 'x_fc_signed_pages_filename': f'ADP_Page11_{assessment.reference}.pdf', }) _logger.info(f"Generated Page 11 PDF for assessment {assessment.reference}") except Exception as pdf_e: _logger.warning(f"PDF generation failed (non-blocking): {pdf_e}") return request.redirect(f'/my/assessments?message=completed&so={sale_order.id}') except Exception as e: _logger.error(f"Error completing express assessment: {e}") return request.redirect(f'/my/assessment/express/{assessment.id}?error={str(e)}') elif action == 'start_over': # Cancel and start fresh if assessment_id and assessment_id != 'None': assessment.unlink() return request.redirect('/my/assessment/express') else: # Just save return request.redirect(f'/my/assessment/express/{assessment.id}?page={current_page}') except Exception as e: _logger.error(f"Error saving express assessment: {e}") if assessment_id and assessment_id != 'None': return request.redirect(f'/my/assessment/express/{assessment_id}?page={current_page}&error=1') return request.redirect('/my/assessment/express?error=1') def _build_express_assessment_vals(self, kw): """Build values dict from express form POST data""" vals = {} # Equipment type if kw.get('equipment_type'): vals['equipment_type'] = kw.get('equipment_type') # Equipment sub-types if kw.get('rollator_type'): vals['rollator_type'] = kw.get('rollator_type') if kw.get('wheelchair_type'): vals['wheelchair_type'] = kw.get('wheelchair_type') if kw.get('powerchair_type'): vals['powerchair_type'] = kw.get('powerchair_type') # Float measurements float_fields = [ 'rollator_handle_height', 'rollator_seat_height', 'seat_width', 'seat_depth', 'seat_to_floor_height', 'back_height', 'legrest_length', 'cane_height', 'client_weight', ] for field in float_fields: if kw.get(field): try: vals[field] = float(kw.get(field)) except (ValueError, TypeError): pass # Checkbox options - collect as comma-separated strings # Rollator addons rollator_addons = kw.getlist('rollator_addons') if hasattr(kw, 'getlist') else [] if not rollator_addons and 'rollator_addons' in kw: rollator_addons = [kw.get('rollator_addons')] if kw.get('rollator_addons') else [] if rollator_addons: vals['rollator_addons'] = ', '.join(rollator_addons) # Wheelchair options frame_options = kw.getlist('frame_options') if hasattr(kw, 'getlist') else [] if not frame_options and 'frame_options' in kw: frame_options = [kw.get('frame_options')] if kw.get('frame_options') else [] if frame_options: vals['frame_options'] = ', '.join(frame_options) wheel_options = kw.getlist('wheel_options') if hasattr(kw, 'getlist') else [] if not wheel_options and 'wheel_options' in kw: wheel_options = [kw.get('wheel_options')] if kw.get('wheel_options') else [] if wheel_options: vals['wheel_options'] = ', '.join(wheel_options) legrest_options = kw.getlist('legrest_options') if hasattr(kw, 'getlist') else [] if not legrest_options and 'legrest_options' in kw: legrest_options = [kw.get('legrest_options')] if kw.get('legrest_options') else [] if legrest_options: vals['legrest_options'] = ', '.join(legrest_options) additional_adp_options = kw.getlist('additional_adp_options') if hasattr(kw, 'getlist') else [] if not additional_adp_options and 'additional_adp_options' in kw: additional_adp_options = [kw.get('additional_adp_options')] if kw.get('additional_adp_options') else [] if additional_adp_options: vals['additional_adp_options'] = ', '.join(additional_adp_options) # Powerchair options powerchair_options = kw.getlist('powerchair_options') if hasattr(kw, 'getlist') else [] if not powerchair_options and 'powerchair_options' in kw: powerchair_options = [kw.get('powerchair_options')] if kw.get('powerchair_options') else [] if powerchair_options: vals['powerchair_options'] = ', '.join(powerchair_options) specialty_controls = kw.getlist('specialty_controls') if hasattr(kw, 'getlist') else [] if not specialty_controls and 'specialty_controls' in kw: specialty_controls = [kw.get('specialty_controls')] if kw.get('specialty_controls') else [] if specialty_controls: vals['specialty_controls'] = ', '.join(specialty_controls) # Seatbelt type if kw.get('seatbelt_type'): vals['seatbelt_type'] = kw.get('seatbelt_type') # Additional customization if kw.get('additional_customization'): vals['additional_customization'] = kw.get('additional_customization') # Cushion and backrest if kw.get('cushion_info'): vals['cushion_info'] = kw.get('cushion_info') if kw.get('backrest_info'): vals['backrest_info'] = kw.get('backrest_info') # Client type if kw.get('client_type'): vals['client_type'] = kw.get('client_type') # Client info (Page 2) if kw.get('client_first_name'): vals['client_first_name'] = kw.get('client_first_name') if kw.get('client_middle_name'): vals['client_middle_name'] = kw.get('client_middle_name') if kw.get('client_last_name'): vals['client_last_name'] = kw.get('client_last_name') # Build full client name name_parts = [] if kw.get('client_first_name'): name_parts.append(kw.get('client_first_name')) if kw.get('client_middle_name'): name_parts.append(kw.get('client_middle_name')) if kw.get('client_last_name'): name_parts.append(kw.get('client_last_name')) if name_parts: vals['client_name'] = ' '.join(name_parts) # Health card if kw.get('client_health_card'): vals['client_health_card'] = kw.get('client_health_card') if kw.get('client_health_card_version'): vals['client_health_card_version'] = kw.get('client_health_card_version') # Address if kw.get('client_street'): vals['client_street'] = kw.get('client_street') if kw.get('client_unit'): vals['client_unit'] = kw.get('client_unit') if kw.get('client_city'): vals['client_city'] = kw.get('client_city') if kw.get('client_state'): vals['client_state'] = kw.get('client_state') if kw.get('client_postal_code'): vals['client_postal_code'] = kw.get('client_postal_code') if kw.get('client_country_id'): try: vals['client_country_id'] = int(kw.get('client_country_id')) except (ValueError, TypeError): pass # Contact if kw.get('client_phone'): vals['client_phone'] = kw.get('client_phone') if kw.get('client_email'): vals['client_email'] = kw.get('client_email') # Dates date_fields = ['assessment_start_date', 'assessment_end_date', 'claim_authorization_date', 'previous_funding_date'] for field in date_fields: if kw.get(field): try: vals[field] = kw.get(field) except (ValueError, TypeError): pass # Reason for application if kw.get('reason_for_application'): vals['reason_for_application'] = kw.get('reason_for_application') # Authorizer if kw.get('authorizer_id'): try: vals['authorizer_id'] = int(kw.get('authorizer_id')) except (ValueError, TypeError): pass # Existing partner selection if kw.get('partner_id'): try: partner_id = int(kw.get('partner_id')) if partner_id > 0: vals['partner_id'] = partner_id vals['create_new_partner'] = False else: vals['create_new_partner'] = True except (ValueError, TypeError): vals['create_new_partner'] = True # ===== PAGE 11: Consent & Declaration fields ===== if kw.get('consent_signed_by'): vals['consent_signed_by'] = kw.get('consent_signed_by') if kw.get('consent_declaration_accepted'): vals['consent_declaration_accepted'] = True if kw.get('consent_date'): try: vals['consent_date'] = kw.get('consent_date') except (ValueError, TypeError): pass # Agent fields (only relevant when consent_signed_by == 'agent') agent_fields = [ 'agent_relationship', 'agent_first_name', 'agent_last_name', 'agent_middle_initial', 'agent_unit', 'agent_street_number', 'agent_street_name', 'agent_city', 'agent_province', 'agent_postal_code', 'agent_home_phone', 'agent_business_phone', 'agent_phone_ext', ] for field in agent_fields: if kw.get(field): vals[field] = kw.get(field) return vals def _get_canadian_provinces(self): """Return list of Canadian provinces for dropdown""" return [ ('Ontario', 'Ontario'), ('Quebec', 'Quebec'), ('British Columbia', 'British Columbia'), ('Alberta', 'Alberta'), ('Manitoba', 'Manitoba'), ('Saskatchewan', 'Saskatchewan'), ('Nova Scotia', 'Nova Scotia'), ('New Brunswick', 'New Brunswick'), ('Newfoundland and Labrador', 'Newfoundland and Labrador'), ('Prince Edward Island', 'Prince Edward Island'), ('Northwest Territories', 'Northwest Territories'), ('Yukon', 'Yukon'), ('Nunavut', 'Nunavut'), ] # ========================================================================= # LOANER PORTAL ROUTES # ========================================================================= @http.route('/my/loaner/categories', type='jsonrpc', auth='user', website=True) def portal_loaner_categories(self, **kw): """Return loaner product categories.""" parent = request.env.ref('fusion_claims.product_category_loaner', raise_if_not_found=False) if not parent: return [] categories = request.env['product.category'].sudo().search([ ('parent_id', '=', parent.id), ], order='name') return [{'id': c.id, 'name': c.name} for c in categories] @http.route('/my/loaner/products', type='jsonrpc', auth='user', website=True) def portal_loaner_products(self, **kw): """Return available loaner products and their serial numbers.""" domain = [('x_fc_can_be_loaned', '=', True)] category_id = kw.get('category_id') if category_id: domain.append(('categ_id', '=', int(category_id))) products = request.env['product.product'].sudo().search(domain) loaner_location = request.env.ref('fusion_claims.stock_location_loaner', raise_if_not_found=False) result = [] for p in products: lots = [] if loaner_location: quants = request.env['stock.quant'].sudo().search([ ('product_id', '=', p.id), ('location_id', '=', loaner_location.id), ('quantity', '>', 0), ]) for q in quants: if q.lot_id: lots.append({'id': q.lot_id.id, 'name': q.lot_id.name}) result.append({ 'id': p.id, 'name': p.name, 'category_id': p.categ_id.id, 'period_days': p.product_tmpl_id.x_fc_loaner_period_days or 7, 'lots': lots, }) return result @http.route('/my/loaner/locations', type='jsonrpc', auth='user', website=True) def portal_loaner_locations(self, **kw): """Return internal stock locations for return.""" locations = request.env['stock.location'].sudo().search([ ('usage', '=', 'internal'), ('company_id', '=', request.env.company.id), ]) return [{'id': loc.id, 'name': loc.complete_name} for loc in locations] @http.route('/my/loaner/checkout', type='jsonrpc', auth='user', website=True) def portal_loaner_checkout(self, **kw): """Checkout a loaner from the portal.""" partner = request.env.user.partner_id if not partner.is_sales_rep_portal and not partner.is_authorizer: return {'error': 'Unauthorized'} product_id = int(kw.get('product_id', 0)) lot_id = int(kw.get('lot_id', 0)) if kw.get('lot_id') else False sale_order_id = int(kw.get('sale_order_id', 0)) if kw.get('sale_order_id') else False client_id = int(kw.get('client_id', 0)) if kw.get('client_id') else False loaner_period = int(kw.get('loaner_period_days', 7)) condition = kw.get('checkout_condition', 'good') notes = kw.get('checkout_notes', '') if not product_id: return {'error': 'Product is required'} vals = { 'product_id': product_id, 'loaner_period_days': loaner_period, 'checkout_condition': condition, 'checkout_notes': notes, 'sales_rep_id': request.env.user.id, } if lot_id: vals['lot_id'] = lot_id if sale_order_id: so = request.env['sale.order'].sudo().browse(sale_order_id) if so.exists(): vals['sale_order_id'] = so.id vals['partner_id'] = so.partner_id.id vals['authorizer_id'] = so.x_fc_authorizer_id.id if so.x_fc_authorizer_id else False vals['delivery_address'] = so.partner_shipping_id.contact_address if so.partner_shipping_id else '' if client_id and not vals.get('partner_id'): vals['partner_id'] = client_id if not vals.get('partner_id'): return {'error': 'Client is required'} try: checkout = request.env['fusion.loaner.checkout'].sudo().create(vals) checkout.action_checkout() return { 'success': True, 'checkout_id': checkout.id, 'name': checkout.name, 'message': f'Loaner {checkout.name} checked out successfully', } except Exception as e: _logger.error(f"Loaner checkout error: {e}") return {'error': str(e)} @http.route('/my/loaner/create-product', type='jsonrpc', auth='user', website=True) def portal_loaner_create_product(self, **kw): """Quick-create a loaner product with serial number from the portal.""" partner = request.env.user.partner_id if not partner.is_sales_rep_portal and not partner.is_authorizer: return {'error': 'Unauthorized'} product_name = kw.get('product_name', '').strip() serial_number = kw.get('serial_number', '').strip() if not product_name: return {'error': 'Product name is required'} if not serial_number: return {'error': 'Serial number is required'} try: # Use provided category or default to Loaner Equipment category_id = kw.get('category_id') if category_id: category = request.env['product.category'].sudo().browse(int(category_id)) if not category.exists(): category = None else: category = None if not category: category = request.env.ref('fusion_claims.product_category_loaner', raise_if_not_found=False) if not category: category = request.env['product.category'].sudo().search([ ('name', '=', 'Loaner Equipment'), ], limit=1) if not category: category = request.env['product.category'].sudo().create({ 'name': 'Loaner Equipment', }) # Create product template product_tmpl = request.env['product.template'].sudo().create({ 'name': product_name, 'type': 'consu', 'tracking': 'serial', 'categ_id': category.id, 'x_fc_can_be_loaned': True, 'x_fc_loaner_period_days': 7, 'sale_ok': False, 'purchase_ok': False, }) product = product_tmpl.product_variant_id # Create serial number (lot) lot = request.env['stock.lot'].sudo().create({ 'name': serial_number, 'product_id': product.id, 'company_id': request.env.company.id, }) # Add stock in loaner location loaner_location = request.env.ref('fusion_claims.stock_location_loaner', raise_if_not_found=False) if loaner_location: request.env['stock.quant'].sudo().create({ 'product_id': product.id, 'location_id': loaner_location.id, 'lot_id': lot.id, 'quantity': 1, }) return { 'success': True, 'product_id': product.id, 'product_name': product.name, 'lot_id': lot.id, 'lot_name': lot.name, } except Exception as e: _logger.error(f"Loaner product creation error: {e}") return {'error': str(e)} @http.route('/my/loaner/return', type='jsonrpc', auth='user', website=True) def portal_loaner_return(self, **kw): """Return/pickup a loaner from the portal.""" partner = request.env.user.partner_id if not partner.is_sales_rep_portal and not partner.is_authorizer: return {'error': 'Unauthorized'} checkout_id = int(kw.get('checkout_id', 0)) return_condition = kw.get('return_condition', 'good') return_notes = kw.get('return_notes', '') return_location_id = int(kw.get('return_location_id', 0)) if kw.get('return_location_id') else None if not checkout_id: return {'error': 'Checkout ID is required'} try: checkout = request.env['fusion.loaner.checkout'].sudo().browse(checkout_id) if not checkout.exists(): return {'error': 'Checkout not found'} if checkout.state not in ('checked_out', 'overdue', 'rental_pending'): return {'error': 'This loaner is not currently checked out'} checkout.action_process_return( return_condition=return_condition, return_notes=return_notes, return_location_id=return_location_id, ) return { 'success': True, 'message': f'Loaner {checkout.name} returned successfully', } except Exception as e: _logger.error(f"Loaner return error: {e}") return {'error': str(e)} # ========================================================================== # PUBLIC ASSESSMENT BOOKING # ========================================================================== @http.route('/book-assessment', type='http', auth='public', website=True, sitemap=True) def portal_book_assessment(self, **kw): """Public page for booking an accessibility assessment.""" # Get available sales reps for assignment SalesGroup = request.env.ref('sales_team.group_sale_salesman', raise_if_not_found=False) sales_reps = [] if SalesGroup: sales_reps = request.env['res.users'].sudo().search([ ('groups_id', 'in', [SalesGroup.id]), ('active', '=', True), ]) assessment_types = [ ('stairlift_straight', 'Straight Stair Lift'), ('stairlift_curved', 'Curved Stair Lift'), ('vpl', 'Vertical Platform Lift'), ('ceiling_lift', 'Ceiling Lift'), ('ramp', 'Custom Ramp'), ('bathroom', 'Bathroom Modification'), ('tub_cutout', 'Tub Cutout'), ] values = { 'assessment_types': assessment_types, 'sales_reps': sales_reps, 'success': kw.get('success'), 'error': kw.get('error'), } return request.render('fusion_authorizer_portal.portal_book_assessment', values) @http.route('/book-assessment/submit', type='http', auth='public', website=True, methods=['POST'], csrf=True) def portal_book_assessment_submit(self, **kw): """Process assessment booking form submission.""" try: # Validate required fields if not kw.get('client_name') or not kw.get('client_phone'): return request.redirect('/book-assessment?error=Please+provide+client+name+and+phone+number') if not kw.get('assessment_type'): return request.redirect('/book-assessment?error=Please+select+an+assessment+type') Assessment = request.env['fusion.accessibility.assessment'].sudo() # Determine booking source booking_source = 'portal' if kw.get('booking_source'): booking_source = kw['booking_source'] # Parse date assessment_date = False if kw.get('assessment_date'): try: assessment_date = fields.Date.from_string(kw['assessment_date']) except Exception: assessment_date = False # Determine sales rep sales_rep_id = False if kw.get('sales_rep_id'): try: sales_rep_id = int(kw['sales_rep_id']) except (ValueError, TypeError): pass # Build address string address_parts = [] if kw.get('client_street'): address_parts.append(kw['client_street']) if kw.get('client_city'): address_parts.append(kw['client_city']) if kw.get('client_province'): address_parts.append(kw['client_province']) if kw.get('client_postal'): address_parts.append(kw['client_postal']) vals = { 'assessment_type': kw['assessment_type'], 'client_name': kw['client_name'], 'client_phone': kw.get('client_phone', ''), 'client_email': kw.get('client_email', ''), 'client_address': ', '.join(address_parts) if address_parts else '', 'client_address_street': kw.get('client_street', ''), 'client_address_city': kw.get('client_city', ''), 'client_address_province': kw.get('client_province', ''), 'client_address_postal': kw.get('client_postal', ''), 'assessment_date': assessment_date, 'booking_source': booking_source, 'modification_requested': kw.get('modification_requested', ''), } if sales_rep_id: vals['sales_rep_id'] = sales_rep_id # Link authorizer if provided if kw.get('authorizer_name') and kw.get('authorizer_email'): Partner = request.env['res.partner'].sudo() authorizer = Partner.search([('email', '=', kw['authorizer_email'])], limit=1) if not authorizer: authorizer = Partner.create({ 'name': kw['authorizer_name'], 'email': kw['authorizer_email'], 'phone': kw.get('authorizer_phone', ''), 'is_authorizer': True, }) vals['authorizer_id'] = authorizer.id assessment = Assessment.create(vals) # Create calendar event for the sales rep if assessment_date and sales_rep_id: try: from datetime import datetime as dt, timedelta # Default: 10 AM, 1.5 hour duration start = dt.combine(assessment_date, dt.min.time().replace(hour=10)) stop = start + timedelta(hours=1, minutes=30) event = request.env['calendar.event'].sudo().create({ 'name': f'Assessment: {kw["client_name"]} ({kw.get("client_city", "")})', 'start': fields.Datetime.to_string(start), 'stop': fields.Datetime.to_string(stop), 'user_id': sales_rep_id, 'location': vals.get('client_address', ''), 'description': ( f'Accessibility Assessment Booking\n' f'Client: {kw["client_name"]}\n' f'Phone: {kw.get("client_phone", "")}\n' f'Type: {kw["assessment_type"]}\n' f'Request: {kw.get("modification_requested", "")}' ), 'partner_ids': [(4, request.env['res.users'].sudo().browse(sales_rep_id).partner_id.id)], }) assessment.write({'calendar_event_id': event.id}) except Exception as e: _logger.error(f"Failed to create calendar event: {e}") # Send authorizer notification email if assessment.authorizer_id and assessment.authorizer_id.email: try: company = request.env.company body_html = assessment._email_build( title='Assessment Scheduled', summary=f'An accessibility assessment has been booked for ' f'{kw["client_name"]}.', email_type='info', sections=[('Booking Details', [ ('Client', kw['client_name']), ('Phone', kw.get('client_phone', '')), ('Address', vals.get('client_address', '')), ('Assessment Type', dict(Assessment._fields['assessment_type'].selection).get(kw['assessment_type'], '')), ('Date', str(assessment_date) if assessment_date else 'TBD'), ('Requested', kw.get('modification_requested', '')), ])], note='This booking was made through the online portal.', sender_name=company.name, ) # Replace footer body_html = body_html.replace( 'This is an automated notification from the ADP Claims Management System.', 'This is an automated notification from the Accessibility Case Management System.', ) request.env['mail.mail'].sudo().create({ 'subject': f'Assessment Booked - {kw["client_name"]}', 'body_html': body_html, 'email_to': assessment.authorizer_id.email, 'model': 'fusion.accessibility.assessment', 'res_id': assessment.id, }).send() except Exception as e: _logger.error(f"Failed to send authorizer notification: {e}") # Send Twilio SMS to client if kw.get('client_phone'): try: ICP = request.env['ir.config_parameter'].sudo() if ICP.get_param('fusion_claims.twilio_enabled', 'False').lower() in ('true', '1', 'yes'): import requests as req account_sid = ICP.get_param('fusion_claims.twilio_account_sid', '') auth_token = ICP.get_param('fusion_claims.twilio_auth_token', '') from_number = ICP.get_param('fusion_claims.twilio_phone_number', '') company_phone = request.env.company.phone or '' date_str = str(assessment_date) if assessment_date else 'a date to be confirmed' sms_body = ( f"Hi {kw['client_name']}, your accessibility assessment with " f"Westin Healthcare has been booked for {date_str}. " f"For questions, call {company_phone}." ) if all([account_sid, auth_token, from_number]): url = f'https://api.twilio.com/2010-04-01/Accounts/{account_sid}/Messages.json' req.post(url, data={ 'To': kw['client_phone'], 'From': from_number, 'Body': sms_body, }, auth=(account_sid, auth_token), timeout=10) assessment.write({'sms_confirmation_sent': True}) except Exception as e: _logger.error(f"Failed to send SMS: {e}") return request.redirect('/book-assessment?success=1') except Exception as e: _logger.error(f"Assessment booking error: {e}") return request.redirect(f'/book-assessment?error={str(e)}')