1444 lines
66 KiB
Python
1444 lines
66 KiB
Python
# -*- 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/<int: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/<int:assessment_id>', 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/<int:assessment_id>/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/<int:assessment_id>/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/<int:assessment_id>/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/<int:assessment_id>', 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(
|
|
'<div class="alert alert-success" role="alert">'
|
|
'<h5 class="alert-heading"><i class="fa fa-file-text"/> Page 11 Consent & Signature</h5>'
|
|
'<ul class="mb-0">'
|
|
f'<li><strong>Signed by:</strong> {signer} ({signed_by})</li>'
|
|
f'<li><strong>Consent date:</strong> {consent_date}</li>'
|
|
f'<li><strong>Declaration accepted:</strong> Yes</li>'
|
|
'</ul>'
|
|
'</div>'
|
|
)
|
|
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"<p><strong>Assessment Photos</strong><br/>Photos from assessment {assessment.reference} by {request.env.user.name}</p>",
|
|
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'<strong>{kw["client_name"]}</strong>.',
|
|
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)}')
|