Files
Odoo-Modules/fusion_authorizer_portal/models/assessment.py
2026-02-22 01:22:18 -05:00

1622 lines
68 KiB
Python

# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
from markupsafe import Markup
from datetime import datetime, timedelta
import base64
import json
import logging
_logger = logging.getLogger(__name__)
class FusionAssessment(models.Model):
_name = 'fusion.assessment'
_description = 'ADP Assessment'
_inherit = ['mail.thread', 'mail.activity.mixin', 'fusion.email.builder.mixin']
_order = 'assessment_date desc, id desc'
_rec_name = 'display_name'
# Status
state = fields.Selection([
('draft', 'In Progress'),
('pending_signature', 'Pending Signatures'),
('completed', 'Completed'),
('cancelled', 'Cancelled'),
], string='Status', default='draft', tracking=True, index=True)
display_name = fields.Char(
string='Display Name',
compute='_compute_display_name',
store=True,
)
# Reference
reference = fields.Char(
string='Reference',
readonly=True,
copy=False,
default=lambda self: _('New'),
)
# ===== EXPRESS FORM: EQUIPMENT TYPE =====
equipment_type = fields.Selection([
('rollator', 'Rollator'),
('wheelchair', 'Wheelchair'),
('powerchair', 'Powerchair'),
], string='Equipment Type', tracking=True, index=True)
# Rollator Types
rollator_type = fields.Selection([
('type_1', 'Type 1'),
('type_2', 'Type 2'),
('type_3', 'Type 3'),
], string='Rollator Type')
# Wheelchair Types (Adult Manual)
wheelchair_type = fields.Selection([
('type_1', 'Type 1 - Adult Manual Standard Wheelchair'),
('type_2', 'Type 2 - Adult Manual Lightweight Wheelchair'),
('type_3', 'Type 3 - Adult Manual Ultra Lightweight Wheelchair'),
('type_4', 'Type 4 - Adult Manual Tilt Wheelchair'),
('type_5', 'Type 5 - Adult Manual Dynamic Tilt Wheelchair'),
], string='Wheelchair Type')
# Powerchair Types
powerchair_type = fields.Selection([
('type_1', 'Adult Power Base Type 1'),
('type_2', 'Adult Power Base Type 2'),
('type_3', 'Adult Power Base Type 3'),
], string='Powerchair Type')
# ===== EXPRESS FORM: ROLLATOR MEASUREMENTS =====
rollator_handle_height = fields.Float(string='Handle Height (inches)', digits=(10, 2))
rollator_seat_height = fields.Float(string='Seat Height (inches)', digits=(10, 2))
# ===== EXPRESS FORM: ROLLATOR ADDONS =====
rollator_addons = fields.Text(
string='Rollator Addons',
help='Comma-separated list of selected rollator addons',
)
# ===== EXPRESS FORM: WHEELCHAIR/POWERCHAIR MEASUREMENTS =====
legrest_length = fields.Float(string='Legrest Length (inches)', digits=(10, 2))
cane_height = fields.Float(string='Cane Height (inches)', digits=(10, 2), help='Ground to Canes')
# ===== EXPRESS FORM: WHEELCHAIR OPTIONS =====
frame_options = fields.Text(
string='Frame Options',
help='Comma-separated list: Recliner Option, Dynamic Tilt Frame, Titanium Frame',
)
wheel_options = fields.Text(
string='Wheel Options/Addons',
help='Comma-separated list of wheel options',
)
legrest_options = fields.Text(
string='Legrest Accessories',
help='Comma-separated list of legrest accessories',
)
additional_adp_options = fields.Text(
string='Additional ADP Funded Options',
help='Comma-separated list of additional ADP options',
)
seatbelt_type = fields.Selection([
('standard', 'Standard Belt - No Padding'),
('padded', 'Padded Belt - Positioning Belts (Modular)'),
('4point', '4 Point Seat Belt - Positioning Belts (Modular)'),
('chest_harness', 'Chest Harness - Positioning Belts (Modular)'),
('additional_pads', 'Additional Pads - Positioning Belts (Custom)'),
], string='Seat Belt Type')
# ===== EXPRESS FORM: POWERCHAIR OPTIONS =====
powerchair_options = fields.Text(
string='Powerchair Additional Options',
help='Comma-separated list of powerchair additional ADP options',
)
specialty_controls = fields.Text(
string='Specialty Components',
help='Comma-separated list of specialty controls (rationale required)',
)
# ===== EXPRESS FORM: ADDITIONAL INFO =====
additional_customization = fields.Text(
string='Additional Information/Customization',
help='Free-form notes for customization requirements',
)
cushion_info = fields.Char(
string='Cushion',
help='Cushion details for wheelchair/powerchair',
)
backrest_info = fields.Char(
string='Backrest',
help='Backrest details for wheelchair/powerchair',
)
# ===== EXPRESS FORM: KEY DATES (map to SO x_fc_ fields) =====
assessment_start_date = fields.Date(
string='Assessment Start Date',
tracking=True,
)
assessment_end_date = fields.Date(
string='Assessment End Date',
tracking=True,
)
claim_authorization_date = fields.Date(
string='Claim Authorization Date',
tracking=True,
)
# ===== EXPRESS FORM: CLIENT TYPE =====
client_type = fields.Selection([
('reg', 'REG - Regular ADP'),
('ods', 'ODS - ODSP'),
('acs', 'ACS - ACSD'),
('owp', 'OWP - Ontario Works'),
], string='Client Type', default='reg',
help='REG = ADP only, ODS/ACS/OWP = ADP + ODSP')
# ===== EXPRESS FORM: REASON FOR APPLICATION =====
reason_for_application = fields.Selection([
('first_access', 'First Time Access - NO previous ADP'),
('additions', 'Additions'),
('mod_non_adp', 'Modification/Upgrade - Original NOT through ADP'),
('mod_adp', 'Modification/Upgrade - Original through ADP'),
('replace_status', 'Replacement - Change in Status'),
('replace_size', 'Replacement - Change in Body Size'),
('replace_worn', 'Replacement - Worn out (past useful life)'),
('replace_lost', 'Replacement - Lost'),
('replace_stolen', 'Replacement - Stolen'),
('replace_damaged', 'Replacement - Damaged beyond repair'),
('replace_no_longer_meets', 'Replacement - No longer meets needs'),
('growth', 'Growth/Change in condition'),
], string='Reason for Application')
previous_funding_date = fields.Date(
string='Previous Funding Date',
help='Date of previous ADP funding (for replacements)',
)
# ===== EXPRESS FORM: CLIENT BIOGRAPHIC (enhanced) =====
client_middle_name = fields.Char(string='Middle Name')
client_health_card_version = fields.Char(string='Health Card Version', help='Version code on health card')
# ===== CLIENT INFORMATION =====
client_name = fields.Char(
string='Client Name',
required=True,
tracking=True,
)
client_first_name = fields.Char(string='First Name')
client_last_name = fields.Char(string='Last Name')
client_address = fields.Text(string='Address')
client_street = fields.Char(string='Street')
client_unit = fields.Char(string='Unit/Apt/Suite')
client_city = fields.Char(string='City')
client_state = fields.Char(string='Province/State', default='Ontario')
client_postal_code = fields.Char(string='Postal Code')
client_country_id = fields.Many2one('res.country', string='Country', default=lambda self: self.env.ref('base.ca', raise_if_not_found=False))
client_phone = fields.Char(string='Phone')
client_mobile = fields.Char(string='Mobile')
client_email = fields.Char(string='Email')
client_dob = fields.Date(string='Date of Birth')
client_health_card = fields.Char(string='Health Card Number')
# Link to existing partner or create new
partner_id = fields.Many2one(
'res.partner',
string='Client Partner',
help='Link to existing customer record',
)
create_new_partner = fields.Boolean(
string='Create New Client',
default=True,
help='If checked, a new customer record will be created on completion',
)
# Client References (for searchability)
client_reference_1 = fields.Char(string='Client Reference 1')
client_reference_2 = fields.Char(string='Client Reference 2')
# ===== ASSESSMENT PARTICIPANTS =====
sales_rep_id = fields.Many2one(
'res.users',
string='Sales Rep',
default=lambda self: self.env.user,
tracking=True,
)
authorizer_id = fields.Many2one(
'res.partner',
string='Authorizer/OT',
tracking=True,
help='The Occupational Therapist or Authorizer conducting the assessment',
)
# ===== ASSESSMENT DATE/LOCATION =====
assessment_date = fields.Datetime(
string='Assessment Date',
default=fields.Datetime.now,
required=True,
tracking=True,
)
assessment_location = fields.Selection([
('home', 'Client Home'),
('clinic', 'Clinic/Office'),
('hospital', 'Hospital'),
('ltc', 'Long Term Care'),
('other', 'Other'),
], string='Location Type', default='home')
assessment_location_notes = fields.Text(string='Location Notes')
# ===== WHEELCHAIR SPECIFICATIONS =====
# Measurements (in inches)
seat_width = fields.Float(string='Seat Width (inches)', digits=(10, 2))
seat_depth = fields.Float(string='Seat Depth (inches)', digits=(10, 2))
seat_to_floor_height = fields.Float(string='Seat to Floor Height (inches)', digits=(10, 2))
back_height = fields.Float(string='Back Height (inches)', digits=(10, 2))
armrest_height = fields.Float(string='Armrest Height (inches)', digits=(10, 2))
footrest_length = fields.Float(string='Footrest Length (inches)', digits=(10, 2))
# Additional Measurements
overall_width = fields.Float(string='Overall Width (inches)', digits=(10, 2))
overall_length = fields.Float(string='Overall Length (inches)', digits=(10, 2))
overall_height = fields.Float(string='Overall Height (inches)', digits=(10, 2))
seat_angle = fields.Float(string='Seat Angle (degrees)', digits=(10, 1))
back_angle = fields.Float(string='Back Angle (degrees)', digits=(10, 1))
# Client Measurements
client_weight = fields.Float(string='Client Weight (lbs)', digits=(10, 1))
client_height = fields.Float(string='Client Height (inches)', digits=(10, 1))
# ===== PRODUCT TYPES =====
cushion_type = fields.Selection([
('foam', 'Foam'),
('gel', 'Gel'),
('air', 'Air'),
('hybrid', 'Hybrid'),
('custom', 'Custom Molded'),
('other', 'Other'),
], string='Cushion Type')
cushion_notes = fields.Text(string='Cushion Notes')
backrest_type = fields.Selection([
('standard', 'Standard'),
('adjustable', 'Adjustable Tension'),
('solid', 'Solid Back'),
('contoured', 'Contoured'),
('custom', 'Custom'),
('other', 'Other'),
], string='Backrest Type')
backrest_notes = fields.Text(string='Backrest Notes')
frame_type = fields.Selection([
('folding', 'Folding'),
('rigid', 'Rigid'),
('tilt', 'Tilt-in-Space'),
('reclining', 'Reclining'),
('standing', 'Standing'),
('power', 'Power'),
('other', 'Other'),
], string='Frame Type')
frame_notes = fields.Text(string='Frame Notes')
wheel_type = fields.Selection([
('pneumatic', 'Pneumatic'),
('solid', 'Solid'),
('flat_free', 'Flat-Free'),
('mag', 'Mag Wheels'),
('spoke', 'Spoke Wheels'),
('other', 'Other'),
], string='Wheel Type')
wheel_notes = fields.Text(string='Wheel Notes')
# ===== ACCESSIBILITY/MOBILITY NEEDS =====
mobility_notes = fields.Text(
string='Mobility Needs',
help='Document the client mobility needs and challenges',
)
accessibility_notes = fields.Text(
string='Accessibility Notes',
help='Document accessibility requirements and home environment',
)
special_requirements = fields.Text(
string='Special Requirements',
help='Any special requirements or customizations needed',
)
diagnosis = fields.Text(
string='Diagnosis/Condition',
help='Relevant medical diagnosis or conditions',
)
# ===== PAGE 11: CONSENT & DECLARATION =====
consent_signed_by = fields.Selection([
('applicant', 'Applicant'),
('agent', 'Agent'),
], string='Consent Signed By', default='applicant',
help='Who signed the ADP consent declaration')
consent_declaration_accepted = fields.Boolean(
string='Consent Declaration Accepted',
help='Whether the consent declaration checkbox was checked',
)
consent_date = fields.Date(
string='Consent Date',
help='Date the consent was signed',
)
# Agent info (only if consent_signed_by == 'agent')
agent_relationship = fields.Selection([
('spouse', 'Spouse'),
('parent', 'Parent(s)'),
('child', 'Child'),
('power_of_attorney', 'Power of Attorney'),
('public_guardian', 'Public Guardian/Trustee'),
], string='Agent Relationship')
agent_first_name = fields.Char(string='Agent First Name')
agent_last_name = fields.Char(string='Agent Last Name')
agent_middle_initial = fields.Char(string='Agent Middle Initial')
agent_unit = fields.Char(string='Agent Unit')
agent_street_number = fields.Char(string='Agent Street Number')
agent_street_name = fields.Char(string='Agent Street Name')
agent_city = fields.Char(string='Agent City')
agent_province = fields.Char(string='Agent Province', default='Ontario')
agent_postal_code = fields.Char(string='Agent Postal Code')
agent_home_phone = fields.Char(string='Agent Home Phone')
agent_business_phone = fields.Char(string='Agent Business Phone')
agent_phone_ext = fields.Char(string='Agent Phone Ext')
# Generated filled PDF from template engine
signed_page_11_pdf = fields.Binary(string='Signed Page 11 PDF')
signed_page_11_pdf_filename = fields.Char(string='Signed Page 11 Filename')
# ===== SIGNATURES =====
signature_page_11 = fields.Binary(
string='Page 11 Signature (Authorizer)',
help='Digital signature for ADP Application Page 11',
)
signature_page_11_name = fields.Char(string='Page 11 Signer Name')
signature_page_11_date = fields.Datetime(string='Page 11 Signed Date')
signature_page_12 = fields.Binary(
string='Page 12 Signature (Client)',
help='Digital signature for ADP Application Page 12',
)
signature_page_12_name = fields.Char(string='Page 12 Signer Name')
signature_page_12_date = fields.Datetime(string='Page 12 Signed Date')
signatures_complete = fields.Boolean(
string='Signatures Complete',
compute='_compute_signatures_complete',
store=True,
)
# ===== RELATIONSHIPS =====
document_ids = fields.One2many(
'fusion.adp.document',
'assessment_id',
string='Documents',
)
comment_ids = fields.One2many(
'fusion.authorizer.comment',
'assessment_id',
string='Comments',
)
sale_order_id = fields.Many2one(
'sale.order',
string='Created Sale Order',
readonly=True,
copy=False,
)
# ===== COMPUTED FIELDS =====
document_count = fields.Integer(
string='Document Count',
compute='_compute_document_count',
)
@api.depends('client_name', 'reference', 'equipment_type')
def _compute_display_name(self):
equipment_labels = {'rollator': 'Rollator', 'wheelchair': 'Wheelchair', 'powerchair': 'Powerchair'}
for assessment in self:
if assessment.reference and assessment.reference != 'New':
equip = equipment_labels.get(assessment.equipment_type, '')
equip_suffix = f' ({equip})' if equip else ''
assessment.display_name = f"{assessment.reference} - {assessment.client_name or 'Unknown'}{equip_suffix}"
else:
assessment.display_name = assessment.client_name or _('New Assessment')
@api.depends('signature_page_11', 'signature_page_12')
def _compute_signatures_complete(self):
for assessment in self:
assessment.signatures_complete = bool(assessment.signature_page_11 and assessment.signature_page_12)
@api.depends('document_ids')
def _compute_document_count(self):
for assessment in self:
assessment.document_count = len(assessment.document_ids)
@api.model_create_multi
def create(self, vals_list):
"""Override create to generate reference number"""
for vals in vals_list:
if vals.get('reference', 'New') == 'New':
vals['reference'] = self.env['ir.sequence'].next_by_code('fusion.assessment') or 'New'
return super().create(vals_list)
def action_mark_pending_signature(self):
"""Move to pending signature state"""
self.ensure_one()
self.state = 'pending_signature'
return True
def action_complete(self):
"""Complete the assessment and create draft sale order"""
self.ensure_one()
if not self.signatures_complete:
raise UserError(_('Please ensure both Page 11 and Page 12 signatures are captured before completing.'))
# Create or link partner
partner = self._ensure_partner()
# Create draft sale order
sale_order = self._create_draft_sale_order(partner)
# Generate signed documents
self._generate_signed_documents()
# Update state
self.write({
'state': 'completed',
'sale_order_id': sale_order.id,
'partner_id': partner.id,
})
# Send notifications
self._send_completion_notifications()
return {
'type': 'ir.actions.act_window',
'name': _('Created Sale Order'),
'res_model': 'sale.order',
'res_id': sale_order.id,
'view_mode': 'form',
'target': 'current',
}
def action_complete_express(self):
"""Complete express assessment and create draft sale order (no signatures required)"""
self.ensure_one()
# Validate required fields
if not self.equipment_type:
raise UserError(_('Please select an equipment type.'))
if not self.authorizer_id:
raise UserError(_('Please select an authorizer.'))
if not self.client_name:
raise UserError(_('Please enter the client name.'))
# Create or link partner
partner = self._ensure_partner()
# Create draft sale order
sale_order = self._create_draft_sale_order(partner)
# Update state
self.write({
'state': 'completed',
'sale_order_id': sale_order.id,
'partner_id': partner.id,
})
_logger.info(f"Completed express assessment {self.reference}, created SO {sale_order.name}")
return sale_order
def action_cancel(self):
"""Cancel the assessment"""
self.ensure_one()
self.state = 'cancelled'
return True
def action_reset_draft(self):
"""Reset to draft state"""
self.ensure_one()
self.state = 'draft'
return True
def _ensure_partner(self):
"""Ensure a partner exists for the client, create if needed"""
self.ensure_one()
if self.partner_id:
return self.partner_id
if not self.create_new_partner:
raise UserError(_('Please select an existing client or check "Create New Client".'))
# Build address
address_parts = []
if self.client_street:
address_parts.append(self.client_street)
if self.client_city:
address_parts.append(self.client_city)
if self.client_state:
address_parts.append(self.client_state)
if self.client_postal_code:
address_parts.append(self.client_postal_code)
partner_vals = {
'name': self.client_name,
'street': self.client_street or '',
'street2': self.client_unit or False,
'city': self.client_city or '',
'zip': self.client_postal_code or '',
'phone': self.client_phone or '',
'email': self.client_email or '',
'customer_rank': 1,
}
if self.client_country_id:
partner_vals['country_id'] = self.client_country_id.id
partner = self.env['res.partner'].sudo().create(partner_vals)
_logger.info(f"Created new partner {partner.name} (ID: {partner.id}) from assessment {self.reference}")
return partner
def _create_draft_sale_order(self, partner):
"""Create a draft sale order from the assessment"""
self.ensure_one()
SaleOrder = self.env['sale.order'].sudo()
CrmTag = self.env['crm.tag'].sudo()
# Determine sale type based on client type
# REG = ADP only, ODS/ACS/OWP = ADP + ODSP
sale_type = 'adp'
if self.client_type in ['ods', 'acs', 'owp']:
sale_type = 'adp_odsp'
# Determine the correct workflow status for the SO.
# The write() auto-transition (assessment_completed -> waiting_for_application)
# does NOT fire during create(), so we set the final target status directly.
if self.assessment_start_date and self.assessment_end_date:
# Both dates filled = assessment completed -> advance to waiting_for_application
target_status = 'waiting_for_application'
elif self.assessment_start_date:
# Only start date = assessment is scheduled
target_status = 'assessment_scheduled'
else:
target_status = 'quotation'
# Prepare values
so_vals = {
'partner_id': partner.id,
'user_id': self.sales_rep_id.id if self.sales_rep_id else self.env.user.id,
'state': 'draft',
'origin': f'Assessment: {self.reference}',
'x_fc_sale_type': sale_type,
# Issue 2 fix: Set assessment back-link for traceability
'assessment_id': self.id,
# Set the correct workflow status directly
'x_fc_adp_application_status': target_status,
}
# Set authorizer if available
if self.authorizer_id:
so_vals['x_fc_authorizer_id'] = self.authorizer_id.id
# Set client references if available
if self.client_reference_1:
so_vals['x_fc_client_ref_1'] = self.client_reference_1
if self.client_reference_2:
so_vals['x_fc_client_ref_2'] = self.client_reference_2
# Map assessment dates to SO x_fc_ fields
if self.assessment_start_date:
so_vals['x_fc_assessment_start_date'] = self.assessment_start_date
if self.assessment_end_date:
so_vals['x_fc_assessment_end_date'] = self.assessment_end_date
if self.claim_authorization_date:
so_vals['x_fc_claim_authorization_date'] = self.claim_authorization_date
# Map reason for application
if self.reason_for_application:
so_vals['x_fc_reason_for_application'] = self.reason_for_application
if self.previous_funding_date:
so_vals['x_fc_previous_funding_date'] = self.previous_funding_date
# Map client type (assessment uses lowercase, sale.order uses uppercase)
if self.client_type:
client_type_map = {
'reg': 'REG',
'ods': 'ODS',
'acs': 'ACS',
'owp': 'OWP',
}
so_vals['x_fc_client_type'] = client_type_map.get(self.client_type, self.client_type.upper())
# =====================================================================
# Issue 3 & 4 fix: Map Page 11 consent & signature tracking to SO
# =====================================================================
if self.consent_signed_by:
# Map consent_signed_by to x_fc_page11_signer_type
if self.consent_signed_by == 'applicant':
so_vals['x_fc_page11_signer_type'] = 'client'
so_vals['x_fc_page11_signer_name'] = self.client_name or ''
elif self.consent_signed_by == 'agent':
# Map agent_relationship to signer_type
agent_type_map = {
'spouse': 'spouse',
'parent': 'parent',
'child': 'legal_guardian',
'power_of_attorney': 'poa',
'public_guardian': 'public_trustee',
}
so_vals['x_fc_page11_signer_type'] = agent_type_map.get(
self.agent_relationship, 'legal_guardian'
)
agent_name = f"{self.agent_first_name or ''} {self.agent_last_name or ''}".strip()
so_vals['x_fc_page11_signer_name'] = agent_name or self.client_name or ''
if self.agent_relationship:
relationship_labels = dict(self._fields['agent_relationship'].selection)
so_vals['x_fc_page11_signer_relationship'] = relationship_labels.get(
self.agent_relationship, self.agent_relationship
)
if self.consent_date:
so_vals['x_fc_page11_signed_date'] = self.consent_date
# Build tags list
tag_ids = []
# Always add ADP tag
adp_tag = CrmTag.search([('name', 'ilike', 'ADP')], limit=1)
if adp_tag:
tag_ids.append(adp_tag.id)
# Add equipment type tag based on selection
equipment_tag_name = None
if self.equipment_type == 'wheelchair' and self.wheelchair_type:
type_map = {
'type_1': 'TYPE 1 WHEELCHAIR',
'type_2': 'TYPE 2 WHEELCHAIR',
'type_3': 'TYPE 3 WHEELCHAIR',
'type_4': 'TYPE 4 WHEELCHAIR',
'type_5': 'TYPE 5 WHEELCHAIR',
}
equipment_tag_name = type_map.get(self.wheelchair_type)
elif self.equipment_type == 'rollator' and self.rollator_type:
type_map = {
'type_1': 'TYPE 1 ROLLATOR',
'type_2': 'TYPE 2 ROLLATOR',
'type_3': 'TYPE 3 ROLLATOR',
}
equipment_tag_name = type_map.get(self.rollator_type)
elif self.equipment_type == 'powerchair' and self.powerchair_type:
type_map = {
'type_1': 'TYPE 1 POWERCHAIR',
'type_2': 'TYPE 2 POWERCHAIR',
'type_3': 'TYPE 3 POWERCHAIR',
}
equipment_tag_name = type_map.get(self.powerchair_type)
# Find or create the equipment tag
if equipment_tag_name:
equipment_tag = CrmTag.search([('name', 'ilike', equipment_tag_name)], limit=1)
if not equipment_tag:
# Create the tag if it doesn't exist
equipment_tag = CrmTag.create({'name': equipment_tag_name})
_logger.info(f"Created new CRM tag: {equipment_tag_name}")
tag_ids.append(equipment_tag.id)
# Add tags to sale order values
if tag_ids:
so_vals['tag_ids'] = [(6, 0, tag_ids)]
sale_order = SaleOrder.create(so_vals)
_logger.info(f"Created draft sale order {sale_order.name} from assessment {self.reference} "
f"with sale_type={sale_type}, status={target_status}")
# =====================================================================
# Issue 6 fix: Post workflow-consistent chatter messages
# =====================================================================
# Post a workflow status message matching what backend wizards would generate
equipment_labels = {
'rollator': 'Rollator', 'wheelchair': 'Wheelchair', 'powerchair': 'Powerchair',
}
equipment_label = equipment_labels.get(self.equipment_type, 'Equipment')
sales_rep_name = self.sales_rep_id.name if self.sales_rep_id else self.env.user.name
authorizer_name = self.authorizer_id.name if self.authorizer_id else 'N/A'
workflow_msg = Markup(
'<div class="alert alert-info" role="alert">'
'<h5 class="alert-heading"><i class="fa fa-clipboard"/> Assessment Completed (Portal)</h5>'
'<ul class="mb-0">'
f'<li><strong>Assessment:</strong> {self.reference}</li>'
f'<li><strong>Equipment:</strong> {equipment_label}</li>'
f'<li><strong>Sales Rep:</strong> {sales_rep_name}</li>'
f'<li><strong>Authorizer:</strong> {authorizer_name}</li>'
f'<li><strong>Assessment Date:</strong> {self.assessment_start_date or "N/A"}'
f' to {self.assessment_end_date or "N/A"}</li>'
f'<li><strong>Status set to:</strong> {target_status.replace("_", " ").title()}</li>'
'</ul>'
'</div>'
)
sale_order.message_post(
body=workflow_msg,
message_type='notification',
subtype_xmlid='mail.mt_note',
)
# Post assessment details to chatter as HTML table
assessment_html = self._format_assessment_html_table()
sale_order.message_post(
body=Markup(assessment_html),
message_type='comment',
subtype_xmlid='mail.mt_note',
)
# =====================================================================
# Issue 4 fix: Post consent & agent details to chatter
# =====================================================================
if self.consent_signed_by:
consent_label = 'Applicant' if self.consent_signed_by == 'applicant' else 'Agent'
consent_parts = [
f'<li><strong>Signed by:</strong> {consent_label}</li>',
]
if self.consent_date:
consent_parts.append(f'<li><strong>Consent date:</strong> {self.consent_date}</li>')
if self.consent_declaration_accepted:
consent_parts.append('<li><strong>Declaration accepted:</strong> Yes</li>')
# Add agent details if signed by agent
if self.consent_signed_by == 'agent':
agent_name = f"{self.agent_first_name or ''} {self.agent_last_name or ''}".strip()
if agent_name:
consent_parts.append(f'<li><strong>Agent name:</strong> {agent_name}</li>')
if self.agent_relationship:
rel_labels = dict(self._fields['agent_relationship'].selection)
consent_parts.append(
f'<li><strong>Relationship:</strong> '
f'{rel_labels.get(self.agent_relationship, self.agent_relationship)}</li>'
)
agent_addr_parts = []
if self.agent_unit:
agent_addr_parts.append(f'Unit {self.agent_unit}')
if self.agent_street_number:
agent_addr_parts.append(self.agent_street_number)
if self.agent_street_name:
agent_addr_parts.append(self.agent_street_name)
if self.agent_city:
agent_addr_parts.append(self.agent_city)
if self.agent_province:
agent_addr_parts.append(self.agent_province)
if self.agent_postal_code:
agent_addr_parts.append(self.agent_postal_code)
if agent_addr_parts:
consent_parts.append(
f'<li><strong>Agent address:</strong> {", ".join(agent_addr_parts)}</li>'
)
phones = []
if self.agent_home_phone:
phones.append(f'Home: {self.agent_home_phone}')
if self.agent_business_phone:
ext = f' ext {self.agent_phone_ext}' if self.agent_phone_ext else ''
phones.append(f'Business: {self.agent_business_phone}{ext}')
if phones:
consent_parts.append(
f'<li><strong>Agent phone:</strong> {", ".join(phones)}</li>'
)
consent_msg = Markup(
'<div class="alert alert-success" role="alert">'
'<h5 class="alert-heading"><i class="fa fa-file-text"/> Page 11 Consent Information</h5>'
'<ul class="mb-0">'
+ ''.join(consent_parts) +
'</ul>'
'</div>'
)
sale_order.message_post(
body=consent_msg,
message_type='notification',
subtype_xmlid='mail.mt_note',
)
# Send email notification to sales person and authorizer
self._send_assessment_completed_email(sale_order)
# Schedule follow-up activity for sales rep
self._schedule_followup_activity(sale_order)
return sale_order
def _schedule_followup_activity(self, sale_order):
"""Schedule a follow-up activity for the sales rep"""
self.ensure_one()
sales_rep = self.sales_rep_id or self.env.user
if not sales_rep:
return
# Get the "To Do" activity type
activity_type = self.env.ref('mail.mail_activity_data_todo', raise_if_not_found=False)
if not activity_type:
_logger.warning("Could not find 'To Do' activity type")
return
# Get equipment type label
equipment_labels = {
'wheelchair': 'Wheelchair',
'rollator': 'Rollator',
'powerchair': 'Powerchair',
}
equipment_label = equipment_labels.get(self.equipment_type, 'Equipment')
# Schedule activity for tomorrow
due_date = fields.Date.today() + timedelta(days=1)
try:
sale_order.activity_schedule(
activity_type_id=activity_type.id,
date_deadline=due_date,
user_id=sales_rep.id,
summary=f'Follow up on {equipment_label} Assessment',
note=f'Assessment {self.reference} for {self.client_name} has been completed. Please follow up with the client and authorizer.',
)
_logger.info(f"Scheduled follow-up activity for {sales_rep.name} on SO {sale_order.name}")
except Exception as e:
_logger.error(f"Failed to schedule follow-up activity: {e}")
def _send_assessment_completed_email(self, sale_order):
"""Send email notification to sales person, authorizer, and office about completed assessment.
Includes the full assessment report so recipients can review without logging in."""
self.ensure_one()
if not self._email_is_enabled():
return
to_emails = []
cc_emails = []
if self.authorizer_id and self.authorizer_id.email:
to_emails.append(self.authorizer_id.email)
if self.sales_rep_id and self.sales_rep_id.email:
cc_emails.append(self.sales_rep_id.email)
company = self.env.company
office_partners = company.sudo().x_fc_office_notification_ids
cc_emails.extend([p.email for p in office_partners if p.email])
if not to_emails and not cc_emails:
return
sales_rep_name = self.sales_rep_id.name if self.sales_rep_id else 'The Sales Team'
assessment_date = self.assessment_end_date.strftime('%B %d, %Y') if self.assessment_end_date else 'Today'
equipment_labels = {'rollator': 'Rollator', 'wheelchair': 'Wheelchair', 'powerchair': 'Powerchair'}
equipment_label = equipment_labels.get(self.equipment_type, 'Equipment')
# Build the detailed assessment report sections for the email
report_sections = self._build_assessment_email_sections(sale_order, equipment_label, assessment_date)
email_body = self._email_build(
title='Assessment Completed',
summary=f'The ADP assessment for <strong>{self.client_name}</strong> has been completed '
f'on {assessment_date}. A draft sales order has been created. '
f'The full assessment report is included below.',
email_type='success',
sections=report_sections,
note='<strong>Next steps:</strong> Please submit the ADP application '
'(including pages 11-12 signed by the client) so we can proceed with the claim submission.',
button_url=f'{sale_order.get_base_url()}/web#id={sale_order.id}&model=sale.order&view_type=form',
button_text='View Sale Order',
sender_name=sales_rep_name,
)
email_to = ', '.join(to_emails) if to_emails else ', '.join(cc_emails[:1])
email_cc = ', '.join(cc_emails) if to_emails else ', '.join(cc_emails[1:])
try:
self.env['mail.mail'].sudo().create({
'subject': f'Assessment Completed - {self.client_name} ({equipment_label}) - {sale_order.name}',
'body_html': email_body,
'email_to': email_to, 'email_cc': email_cc,
'model': 'sale.order', 'res_id': sale_order.id,
'auto_delete': True,
}).send()
chatter_body = Markup(
'<div class="alert alert-info" role="alert">'
f'<strong>Assessment Completed email sent</strong>'
f'<ul class="mb-0 mt-1"><li>To: {email_to}</li>'
f'<li>CC: {email_cc or "None"}</li></ul></div>'
)
sale_order.message_post(body=chatter_body, message_type='notification', subtype_xmlid='mail.mt_note')
_logger.info(f"Sent assessment completed email for {self.reference}")
except Exception as e:
_logger.error(f"Failed to send assessment completed email: {e}")
def _build_assessment_email_sections(self, sale_order, equipment_label, assessment_date):
"""Build comprehensive email sections with all assessment details.
Returns a list of (heading, rows) tuples for _email_build()."""
self.ensure_one()
sections = []
# --- Section 1: Overview ---
overview_rows = [
('Reference', self.reference),
('Sales Order', sale_order.name),
('Client', self.client_name),
('Equipment', equipment_label),
('Assessment Date', assessment_date),
('Authorizer/OT', self.authorizer_id.name if self.authorizer_id else None),
('Sales Rep', self.sales_rep_id.name if self.sales_rep_id else None),
]
if self.client_type:
ct_labels = {
'reg': 'REG - Regular ADP',
'ods': 'ODS - ODSP',
'acs': 'ACS - ACSD',
'owp': 'OWP - Ontario Works',
}
overview_rows.append(('Client Type', ct_labels.get(self.client_type, self.client_type)))
if self.reason_for_application:
reason_labels = dict(self._fields['reason_for_application'].selection)
overview_rows.append(('Reason for Application', reason_labels.get(
self.reason_for_application, self.reason_for_application)))
if self.previous_funding_date:
overview_rows.append(('Previous Funding Date', str(self.previous_funding_date)))
sections.append(('Assessment Overview', overview_rows))
# --- Section 2: Client Information ---
client_rows = [
('Name', self.client_name),
]
if self.client_first_name or self.client_last_name:
full = f"{self.client_first_name or ''} {self.client_middle_name or ''} {self.client_last_name or ''}".strip()
client_rows.append(('Full Name', full))
if self.client_weight:
client_rows.append(('Weight', f'{int(self.client_weight)} lbs'))
if self.client_dob:
client_rows.append(('Date of Birth', str(self.client_dob)))
if self.client_health_card:
version = f' ({self.client_health_card_version})' if self.client_health_card_version else ''
client_rows.append(('Health Card', f'{self.client_health_card}{version}'))
# Address
addr_parts = [p for p in [
self.client_street, self.client_unit, self.client_city,
self.client_state, self.client_postal_code,
] if p]
if addr_parts:
client_rows.append(('Address', ', '.join(addr_parts)))
if self.client_phone:
client_rows.append(('Phone', self.client_phone))
if self.client_mobile:
client_rows.append(('Mobile', self.client_mobile))
if self.client_email:
client_rows.append(('Email', self.client_email))
sections.append(('Client Information', client_rows))
# --- Section 3: Equipment Type & Subtype ---
equip_rows = [('Equipment Type', equipment_label)]
if self.equipment_type == 'rollator':
if self.rollator_type:
type_labels = dict(self._fields['rollator_type'].selection)
equip_rows.append(('Rollator Type', type_labels.get(self.rollator_type, self.rollator_type)))
elif self.equipment_type == 'wheelchair':
if self.wheelchair_type:
type_labels = dict(self._fields['wheelchair_type'].selection)
equip_rows.append(('Wheelchair Type', type_labels.get(self.wheelchair_type, self.wheelchair_type)))
elif self.equipment_type == 'powerchair':
if self.powerchair_type:
type_labels = dict(self._fields['powerchair_type'].selection)
equip_rows.append(('Powerchair Type', type_labels.get(self.powerchair_type, self.powerchair_type)))
sections.append(('Equipment Selection', equip_rows))
# --- Section 4: Measurements ---
meas_rows = []
if self.equipment_type == 'rollator':
if self.rollator_handle_height:
meas_rows.append(('Handle Height', f'{self.rollator_handle_height}"'))
if self.rollator_seat_height:
meas_rows.append(('Seat Height', f'{self.rollator_seat_height}"'))
else:
# Wheelchair / Powerchair measurements
if self.seat_width:
meas_rows.append(('Seat Width', f'{self.seat_width}"'))
if self.seat_depth:
meas_rows.append(('Seat Depth', f'{self.seat_depth}"'))
if self.legrest_length:
meas_rows.append(('Legrest Length', f'{self.legrest_length}"'))
if self.seat_to_floor_height:
sfh_label = f'{self.seat_to_floor_height}" (Including Cushion)' if self.equipment_type == 'wheelchair' else f'{self.seat_to_floor_height}"'
meas_rows.append(('Seat to Floor Height', sfh_label))
if self.cane_height:
meas_rows.append(('Cane Height', f'{self.cane_height}" (Ground to Canes)'))
if self.back_height:
meas_rows.append(('Back Height', f'{self.back_height}"'))
if self.armrest_height:
meas_rows.append(('Armrest Height', f'{self.armrest_height}"'))
if self.footrest_length:
meas_rows.append(('Footrest Length', f'{self.footrest_length}"'))
if meas_rows:
sections.append(('Measurements', meas_rows))
# --- Section 5: Options & Addons ---
options_rows = []
if self.equipment_type == 'rollator' and self.rollator_addons:
for addon in self.rollator_addons.split(','):
addon = addon.strip()
if addon:
options_rows.append((addon, 'Yes'))
elif self.equipment_type == 'wheelchair':
if self.frame_options:
for opt in self.frame_options.split(','):
opt = opt.strip()
if opt:
options_rows.append((f'Frame: {opt}', 'Yes'))
if self.wheel_options:
for opt in self.wheel_options.split(','):
opt = opt.strip()
if opt:
options_rows.append((f'Wheel: {opt}', 'Yes'))
if self.legrest_options:
for opt in self.legrest_options.split(','):
opt = opt.strip()
if opt:
options_rows.append((f'Legrest: {opt}', 'Yes'))
if self.additional_adp_options:
for opt in self.additional_adp_options.split(','):
opt = opt.strip()
if opt:
options_rows.append((f'ADP Option: {opt}', 'Yes'))
elif self.equipment_type == 'powerchair':
if self.powerchair_options:
for opt in self.powerchair_options.split(','):
opt = opt.strip()
if opt:
options_rows.append((f'Powerchair: {opt}', 'Yes'))
if self.specialty_controls:
for opt in self.specialty_controls.split(','):
opt = opt.strip()
if opt:
options_rows.append((f'Specialty: {opt}', 'Yes'))
# Seatbelt (wheelchair & powerchair)
if self.seatbelt_type and self.equipment_type in ('wheelchair', 'powerchair'):
belt_labels = dict(self._fields['seatbelt_type'].selection)
options_rows.append(('Seat Belt', belt_labels.get(self.seatbelt_type, self.seatbelt_type)))
# Cushion & Backrest
if self.cushion_info:
options_rows.append(('Cushion', self.cushion_info))
if self.backrest_info:
options_rows.append(('Backrest', self.backrest_info))
if options_rows:
sections.append(('Options & Accessories', options_rows))
# --- Section 6: Additional Notes ---
notes_rows = []
if self.additional_customization:
notes_rows.append(('Customization', self.additional_customization))
if self.mobility_notes:
notes_rows.append(('Mobility Needs', self.mobility_notes))
if self.accessibility_notes:
notes_rows.append(('Accessibility', self.accessibility_notes))
if self.special_requirements:
notes_rows.append(('Special Requirements', self.special_requirements))
if self.diagnosis:
notes_rows.append(('Diagnosis/Condition', self.diagnosis))
if notes_rows:
sections.append(('Additional Notes', notes_rows))
# --- Section 7: Key Dates ---
date_rows = []
if self.assessment_start_date:
date_rows.append(('Assessment Start', self.assessment_start_date.strftime('%B %d, %Y')))
if self.assessment_end_date:
date_rows.append(('Assessment End', self.assessment_end_date.strftime('%B %d, %Y')))
if self.claim_authorization_date:
date_rows.append(('Claim Authorization', self.claim_authorization_date.strftime('%B %d, %Y')))
if date_rows:
sections.append(('Key Dates', date_rows))
# --- Section 8: Consent Information ---
if self.consent_signed_by:
consent_rows = []
consent_label = 'Applicant' if self.consent_signed_by == 'applicant' else 'Agent'
consent_rows.append(('Signed By', consent_label))
if self.consent_date:
consent_rows.append(('Consent Date', str(self.consent_date)))
if self.consent_declaration_accepted:
consent_rows.append(('Declaration Accepted', 'Yes'))
if self.consent_signed_by == 'agent':
agent_name = f"{self.agent_first_name or ''} {self.agent_last_name or ''}".strip()
if agent_name:
consent_rows.append(('Agent Name', agent_name))
if self.agent_relationship:
rel_labels = dict(self._fields['agent_relationship'].selection)
consent_rows.append(('Relationship', rel_labels.get(
self.agent_relationship, self.agent_relationship)))
sections.append(('Consent & Declaration (Page 11)', consent_rows))
return sections
def _format_assessment_html_table(self):
"""Format assessment data as HTML table for chatter"""
self.ensure_one()
# Get equipment type display name
equipment_labels = {
'rollator': 'Rollator',
'wheelchair': 'Wheelchair',
'powerchair': 'Powerchair',
}
equipment_display = equipment_labels.get(self.equipment_type, 'Unknown')
# Start building HTML
html = f'''
<h3 style="color: #875A7B; margin-bottom: 10px;">ADP Assessment - {equipment_display}</h3>
<table style="border-collapse: collapse; width: 100%; font-size: 13px;">
<tr style="background: #875A7B; color: white;">
<th style="border: 1px solid #ddd; padding: 8px; text-align: left; width: 40%;">Field</th>
<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Value</th>
</tr>
'''
row_num = 0
def add_row(label, value, is_header=False):
nonlocal html, row_num
if is_header:
html += f'''
<tr style="background: #e8f4e8;">
<td colspan="2" style="border: 1px solid #ddd; padding: 8px;"><strong>{label}</strong></td>
</tr>
'''
else:
bg = '#f9f9f9' if row_num % 2 == 0 else '#ffffff'
html += f'''
<tr style="background: {bg};">
<td style="border: 1px solid #ddd; padding: 8px;">{label}</td>
<td style="border: 1px solid #ddd; padding: 8px;">{value}</td>
</tr>
'''
row_num += 1
# Client Type
if self.client_type:
client_type_labels = {
'reg': 'REG - Regular ADP',
'ods': 'ODS - ODSP',
'acs': 'ACS - ACSD',
'owp': 'OWP - Ontario Works',
}
add_row('Client Type', client_type_labels.get(self.client_type, self.client_type))
# Equipment Type
add_row('Equipment Type', equipment_display)
# Client Weight - shown prominently at the top
if self.client_weight:
add_row('Client Weight', f'{int(self.client_weight)} lbs')
# Type-specific info
if self.equipment_type == 'rollator':
if self.rollator_type:
type_labels = dict(self._fields['rollator_type'].selection)
add_row('Rollator Type', type_labels.get(self.rollator_type, self.rollator_type))
add_row('Measurements', '', is_header=True)
if self.rollator_handle_height:
add_row('Handle Height', f'{self.rollator_handle_height}"')
if self.rollator_seat_height:
add_row('Seat Height', f'{self.rollator_seat_height}"')
if self.rollator_addons:
add_row('Addons', '', is_header=True)
for addon in self.rollator_addons.split(','):
addon = addon.strip()
if addon:
add_row(addon, 'Yes')
elif self.equipment_type == 'wheelchair':
if self.wheelchair_type:
type_labels = dict(self._fields['wheelchair_type'].selection)
add_row('Wheelchair Type', type_labels.get(self.wheelchair_type, self.wheelchair_type))
add_row('Measurements', '', is_header=True)
if self.seat_width:
add_row('Seat Width', f'{self.seat_width}"')
if self.seat_depth:
add_row('Seat Depth', f'{self.seat_depth}"')
if self.legrest_length:
add_row('Legrest Length', f'{self.legrest_length}"')
if self.seat_to_floor_height:
add_row('Seat to Floor Height', f'{self.seat_to_floor_height}" (Including Cushion)')
if self.cane_height:
add_row('Cane Height', f'{self.cane_height}" (Ground to Canes)')
if self.back_height:
add_row('Back Height', f'{self.back_height}"')
if self.frame_options:
add_row('Frame Options - ADP Funded', '', is_header=True)
for opt in self.frame_options.split(','):
opt = opt.strip()
if opt:
add_row(opt, 'Yes')
if self.wheel_options:
add_row('Wheel Options/Addons - ADP Funded', '', is_header=True)
for opt in self.wheel_options.split(','):
opt = opt.strip()
if opt:
add_row(opt, 'Yes')
if self.legrest_options:
add_row('Legrest Accessories - ADP Funded', '', is_header=True)
for opt in self.legrest_options.split(','):
opt = opt.strip()
if opt:
add_row(opt, 'Yes')
if self.additional_adp_options:
add_row('Additional ADP Funded Options', '', is_header=True)
for opt in self.additional_adp_options.split(','):
opt = opt.strip()
if opt:
add_row(opt, 'Yes')
if self.seatbelt_type:
belt_labels = dict(self._fields['seatbelt_type'].selection)
add_row('Seat Belt Type', belt_labels.get(self.seatbelt_type, self.seatbelt_type))
elif self.equipment_type == 'powerchair':
if self.powerchair_type:
type_labels = dict(self._fields['powerchair_type'].selection)
add_row('Powerchair Type', type_labels.get(self.powerchair_type, self.powerchair_type))
add_row('Measurements', '', is_header=True)
if self.seat_width:
add_row('Seat Width', f'{self.seat_width}"')
if self.seat_depth:
add_row('Seat Depth', f'{self.seat_depth}"')
if self.legrest_length:
add_row('Legrest Length', f'{self.legrest_length}"')
if self.seat_to_floor_height:
add_row('Seat to Floor Height', f'{self.seat_to_floor_height}"')
if self.back_height:
add_row('Back Height', f'{self.back_height}"')
if self.seatbelt_type:
belt_labels = dict(self._fields['seatbelt_type'].selection)
add_row('Seat Belt Type', belt_labels.get(self.seatbelt_type, self.seatbelt_type))
if self.powerchair_options:
add_row('Powerchair - Additional ADP Funded Options', '', is_header=True)
for opt in self.powerchair_options.split(','):
opt = opt.strip()
if opt:
add_row(opt, 'Yes')
if self.specialty_controls:
add_row('Specialty Components (Rationale Required)', '', is_header=True)
for opt in self.specialty_controls.split(','):
opt = opt.strip()
if opt:
add_row(opt, 'Yes')
# Additional customization notes
if self.additional_customization:
add_row('Additional Information/Customization', self.additional_customization)
# Client info summary
add_row('Client Information', '', is_header=True)
add_row('Client Name', self.client_name or '')
if self.client_weight:
add_row('Client Weight', f'{int(self.client_weight)} lbs')
if self.client_health_card:
version = f' ({self.client_health_card_version})' if self.client_health_card_version else ''
add_row('Health Card', f'{self.client_health_card}{version}')
if self.client_phone:
add_row('Phone', self.client_phone)
if self.client_email:
add_row('Email', self.client_email)
# Dates
add_row('Key Dates', '', is_header=True)
if self.assessment_start_date:
add_row('Assessment Start Date', str(self.assessment_start_date))
if self.assessment_end_date:
add_row('Assessment End Date', str(self.assessment_end_date))
if self.claim_authorization_date:
add_row('Claim Authorization Date', str(self.claim_authorization_date))
if self.reason_for_application:
reason_labels = dict(self._fields['reason_for_application'].selection)
add_row('Reason for Application', reason_labels.get(self.reason_for_application, self.reason_for_application))
html += '</table>'
return html
def _format_specifications_for_order(self):
"""Format wheelchair specifications for the sale order notes (legacy)"""
self.ensure_one()
lines = [
f"=== Assessment Specifications ({self.reference}) ===",
"",
"MEASUREMENTS:",
]
if self.seat_width:
lines.append(f" Seat Width: {self.seat_width}\"")
if self.seat_depth:
lines.append(f" Seat Depth: {self.seat_depth}\"")
if self.seat_to_floor_height:
lines.append(f" Seat to Floor: {self.seat_to_floor_height}\"")
if self.back_height:
lines.append(f" Back Height: {self.back_height}\"")
if self.armrest_height:
lines.append(f" Armrest Height: {self.armrest_height}\"")
if self.footrest_length:
lines.append(f" Footrest Length: {self.footrest_length}\"")
lines.append("")
lines.append("PRODUCT SELECTIONS:")
if self.cushion_type:
lines.append(f" Cushion: {dict(self._fields['cushion_type'].selection).get(self.cushion_type, self.cushion_type)}")
if self.backrest_type:
lines.append(f" Backrest: {dict(self._fields['backrest_type'].selection).get(self.backrest_type, self.backrest_type)}")
if self.frame_type:
lines.append(f" Frame: {dict(self._fields['frame_type'].selection).get(self.frame_type, self.frame_type)}")
if self.wheel_type:
lines.append(f" Wheels: {dict(self._fields['wheel_type'].selection).get(self.wheel_type, self.wheel_type)}")
if self.mobility_notes:
lines.append("")
lines.append("MOBILITY NEEDS:")
lines.append(f" {self.mobility_notes}")
if self.accessibility_notes:
lines.append("")
lines.append("ACCESSIBILITY NOTES:")
lines.append(f" {self.accessibility_notes}")
if self.special_requirements:
lines.append("")
lines.append("SPECIAL REQUIREMENTS:")
lines.append(f" {self.special_requirements}")
return "\n".join(lines)
def _generate_signed_documents(self):
"""Generate document records for signed pages"""
self.ensure_one()
ADPDocument = self.env['fusion.adp.document'].sudo()
# Create Page 11 document if signature exists
if self.signature_page_11:
ADPDocument.create({
'assessment_id': self.id,
'sale_order_id': self.sale_order_id.id if self.sale_order_id else False,
'document_type': 'page_11',
'file': self.signature_page_11,
'filename': f'page_11_signature_{self.reference}.png',
'mimetype': 'image/png',
'source': 'assessment',
})
# Create Page 12 document if signature exists
if self.signature_page_12:
ADPDocument.create({
'assessment_id': self.id,
'sale_order_id': self.sale_order_id.id if self.sale_order_id else False,
'document_type': 'page_12',
'file': self.signature_page_12,
'filename': f'page_12_signature_{self.reference}.png',
'mimetype': 'image/png',
'source': 'assessment',
})
def _send_completion_notifications(self):
"""Send email notifications when assessment is completed"""
self.ensure_one()
# Send to authorizer
if self.authorizer_id and self.authorizer_id.email:
try:
template = self.env.ref('fusion_authorizer_portal.mail_template_assessment_complete_authorizer', raise_if_not_found=False)
if template:
template.send_mail(self.id, force_send=True)
_logger.info(f"Sent assessment completion email to authorizer {self.authorizer_id.email}")
except Exception as e:
_logger.error(f"Failed to send authorizer notification: {e}")
# Send to client
if self.client_email:
try:
template = self.env.ref('fusion_authorizer_portal.mail_template_assessment_complete_client', raise_if_not_found=False)
if template:
template.send_mail(self.id, force_send=True)
_logger.info(f"Sent assessment completion email to client {self.client_email}")
except Exception as e:
_logger.error(f"Failed to send client notification: {e}")
def action_view_documents(self):
"""View related documents"""
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Documents'),
'res_model': 'fusion.adp.document',
'view_mode': 'list,form',
'domain': [('assessment_id', '=', self.id)],
'context': {'default_assessment_id': self.id},
}
def action_view_sale_order(self):
"""View the created sale order"""
self.ensure_one()
if not self.sale_order_id:
raise UserError(_('No sale order has been created yet.'))
return {
'type': 'ir.actions.act_window',
'name': _('Sale Order'),
'res_model': 'sale.order',
'res_id': self.sale_order_id.id,
'view_mode': 'form',
'target': 'current',
}
# ===== PDF TEMPLATE ENGINE INTEGRATION =====
def _get_pdf_context(self):
"""Return a flat dict of all assessment data for PDF template filling.
This is the data source for the generic PDF template engine.
Each key can be mapped to a field on any PDF template via field_key.
"""
self.ensure_one()
ctx = {
# Client biographical
'client_last_name': self.client_last_name or '',
'client_first_name': self.client_first_name or '',
'client_middle_name': self.client_middle_name or '',
'client_name': self.client_name or '',
'client_health_card': self.client_health_card or '',
'client_health_card_version': self.client_health_card_version or '',
'client_street': self.client_street or '',
'client_unit': self.client_unit or '',
'client_city': self.client_city or '',
'client_state': self.client_state or '',
'client_postal_code': self.client_postal_code or '',
'client_phone': self.client_phone or '',
'client_email': self.client_email or '',
'client_weight': str(int(self.client_weight)) if self.client_weight else '',
# Client type
'client_type': self.client_type or '',
'client_type_reg': self.client_type == 'reg',
'client_type_ods': self.client_type == 'ods',
'client_type_acs': self.client_type == 'acs',
'client_type_owp': self.client_type == 'owp',
# Consent & Declaration (Page 11)
'consent_signed_by': self.consent_signed_by or '',
'consent_applicant': self.consent_signed_by == 'applicant',
'consent_agent': self.consent_signed_by == 'agent',
'consent_declaration_accepted': self.consent_declaration_accepted,
'consent_date': str(self.consent_date) if self.consent_date else '',
# Agent info
'agent_relationship': self.agent_relationship or '',
'agent_rel_spouse': self.agent_relationship == 'spouse',
'agent_rel_parent': self.agent_relationship == 'parent',
'agent_rel_child': self.agent_relationship == 'child',
'agent_rel_poa': self.agent_relationship == 'power_of_attorney',
'agent_rel_guardian': self.agent_relationship == 'public_guardian',
'agent_first_name': self.agent_first_name or '',
'agent_last_name': self.agent_last_name or '',
'agent_middle_initial': self.agent_middle_initial or '',
'agent_unit': self.agent_unit or '',
'agent_street_number': self.agent_street_number or '',
'agent_street_name': self.agent_street_name or '',
'agent_city': self.agent_city or '',
'agent_province': self.agent_province or '',
'agent_postal_code': self.agent_postal_code or '',
'agent_home_phone': self.agent_home_phone or '',
'agent_business_phone': self.agent_business_phone or '',
'agent_phone_ext': self.agent_phone_ext or '',
# Equipment
'equipment_type': self.equipment_type or '',
'seat_width': str(self.seat_width) if self.seat_width else '',
'seat_depth': str(self.seat_depth) if self.seat_depth else '',
'seat_to_floor_height': str(self.seat_to_floor_height) if self.seat_to_floor_height else '',
'back_height': str(self.back_height) if self.back_height else '',
'legrest_length': str(self.legrest_length) if self.legrest_length else '',
'cane_height': str(self.cane_height) if self.cane_height else '',
# Dates
'assessment_start_date': str(self.assessment_start_date) if self.assessment_start_date else '',
'assessment_end_date': str(self.assessment_end_date) if self.assessment_end_date else '',
'claim_authorization_date': str(self.claim_authorization_date) if self.claim_authorization_date else '',
# Reason
'reason_for_application': self.reason_for_application or '',
# Reference
'reference': self.reference or '',
}
# Authorizer info
if self.authorizer_id:
ctx['authorizer_name'] = self.authorizer_id.name or ''
ctx['authorizer_phone'] = self.authorizer_id.phone or ''
ctx['authorizer_email'] = self.authorizer_id.email or ''
return ctx
def _get_pdf_signatures(self):
"""Return a dict of signature binaries for PDF template filling."""
self.ensure_one()
sigs = {}
if self.signature_page_11:
sigs['signature_page_11'] = base64.b64decode(self.signature_page_11)
if self.signature_page_12:
sigs['signature_page_12'] = base64.b64decode(self.signature_page_12)
return sigs
def generate_template_pdf(self, template_name='adp_page_11'):
"""Generate a filled PDF using the named template.
Args:
template_name: the name field of the fusion.pdf.template record
Returns:
bytes of the filled PDF, or None if template not found
"""
self.ensure_one()
template = self.env['fusion.pdf.template'].search([
('state', '=', 'active'),
('name', 'ilike', template_name),
], limit=1)
if not template:
_logger.warning("No active PDF template found matching '%s'", template_name)
return None
context_data = self._get_pdf_context()
signatures = self._get_pdf_signatures()
try:
pdf_bytes = template.generate_filled_pdf(context_data, signatures)
_logger.info("Generated filled PDF from template '%s' for assessment %s",
template.name, self.reference)
return pdf_bytes
except Exception as e:
_logger.error("Failed to generate PDF for assessment %s: %s", self.reference, e)
return None