- fusion_claims: separated field service logic, updated controllers/views - fusion_tasks: updated task views and map integration - fusion_authorizer_portal: added page 11 signing, schedule booking, migrations - fusion_shipping: new standalone shipping module (Canada Post, FedEx, DHL, Purolator) - fusion_ltc_management: new standalone LTC management module
1637 lines
69 KiB
Python
1637 lines
69 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',
|
|
'views': [(False, '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.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.armrest_height:
|
|
add_row('Armrest Height', f'{self.armrest_height}"')
|
|
if self.footrest_length:
|
|
add_row('Footrest Length', f'{self.footrest_length}"')
|
|
|
|
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')
|
|
|
|
# Cushion & Backrest (wheelchair/powerchair)
|
|
if self.cushion_info:
|
|
add_row('Cushion', self.cushion_info)
|
|
if self.backrest_info:
|
|
add_row('Backrest', self.backrest_info)
|
|
|
|
# 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',
|
|
'views': [(False, 'list'), (False, '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',
|
|
'views': [(False, '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
|