Initial commit

This commit is contained in:
gsinghpal
2026-02-22 01:22:18 -05:00
commit 5200d5baf0
2394 changed files with 386834 additions and 0 deletions

View File

@@ -0,0 +1,874 @@
# -*- coding: utf-8 -*-
import logging
import math
from datetime import timedelta
from markupsafe import Markup
from odoo import api, fields, models, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class FusionAccessibilityAssessment(models.Model):
_name = 'fusion.accessibility.assessment'
_description = 'Accessibility Assessment'
_inherit = ['mail.thread', 'mail.activity.mixin', 'fusion.email.builder.mixin']
_order = 'assessment_date desc, id desc'
_rec_name = 'display_name'
# ==========================================================================
# COMMON FIELDS (all assessment types)
# ==========================================================================
reference = fields.Char(
string='Reference',
readonly=True,
copy=False,
default=lambda self: _('New'),
)
display_name = fields.Char(
compute='_compute_display_name',
store=True,
)
assessment_type = fields.Selection(
selection=[
('stairlift_straight', 'Straight Stair Lift'),
('stairlift_curved', 'Curved Stair Lift'),
('vpl', 'Vertical Platform Lift'),
('ceiling_lift', 'Ceiling Lift'),
('ramp', 'Custom Ramp'),
('bathroom', 'Bathroom Modification'),
('tub_cutout', 'Tub Cutout'),
],
string='Assessment Type',
required=True,
tracking=True,
)
state = fields.Selection(
selection=[
('draft', 'Draft'),
('completed', 'Completed'),
('cancelled', 'Cancelled'),
],
string='Status',
default='draft',
tracking=True,
)
# Client Information
client_name = fields.Char(string='Client Name', required=True)
client_address = fields.Char(string='Address')
client_unit = fields.Char(string='Unit/Apt/Suite')
client_address_street = fields.Char(string='Street')
client_address_city = fields.Char(string='City')
client_address_province = fields.Char(string='Province')
client_address_postal = fields.Char(string='Postal Code')
client_phone = fields.Char(string='Phone')
client_email = fields.Char(string='Email')
# Booking fields
booking_source = fields.Selection(
selection=[
('phone_authorizer', 'Phone - Authorizer'),
('phone_client', 'Phone - Client'),
('walk_in', 'Walk-In'),
('portal', 'Online Booking'),
],
string='Booking Source',
default='phone_client',
help='How the assessment was booked',
)
modification_requested = fields.Text(
string='Modification Requested',
help='What the client or authorizer is looking for',
)
sms_confirmation_sent = fields.Boolean(
string='SMS Confirmation Sent',
default=False,
)
calendar_event_id = fields.Many2one(
'calendar.event',
string='Calendar Event',
readonly=True,
copy=False,
)
# Relationships
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 for this assessment',
)
partner_id = fields.Many2one(
'res.partner',
string='Client Partner',
help='Linked partner record (created on completion)',
)
sale_order_id = fields.Many2one(
'sale.order',
string='Created Sale Order',
readonly=True,
copy=False,
)
# Dates
assessment_date = fields.Date(
string='Assessment Date',
default=fields.Date.today,
)
# General Notes
notes = fields.Text(string='General Notes')
# ==========================================================================
# STAIR LIFT - STRAIGHT FIELDS
# ==========================================================================
stair_steps = fields.Integer(string='Number of Steps')
stair_nose_to_nose = fields.Float(string='Nose to Nose Distance (inches)')
stair_side = fields.Selection(
selection=[('left', 'Left'), ('right', 'Right')],
string='Installation Side',
)
stair_style = fields.Selection(
selection=[
('standard', 'Standard Stair Lift'),
('slide_track', 'Slide Track Stair Lift'),
('foldable_hinge', 'Foldable Hinge Stair Lift'),
],
string='Stair Lift Style',
)
stair_power_swivel_upstairs = fields.Boolean(string='Power Swivel (Upstairs)')
stair_power_folding_footrest = fields.Boolean(string='Power Folding Footrest')
stair_calculated_length = fields.Float(
string='Calculated Track Length (inches)',
compute='_compute_stair_straight_length',
store=True,
)
stair_manual_length_override = fields.Float(string='Manual Length Override (inches)')
stair_final_length = fields.Float(
string='Final Track Length (inches)',
compute='_compute_stair_final_length',
store=True,
)
# ==========================================================================
# STAIR LIFT - CURVED FIELDS
# ==========================================================================
stair_curved_steps = fields.Integer(string='Number of Steps (Curved)')
stair_curves_count = fields.Integer(string='Number of Curves')
# Top Landing Options
stair_top_landing_type = fields.Selection(
selection=[
('none', 'Standard (No special landing)'),
('90_exit', '90° Exit'),
('90_parking', '90° Parking'),
('180_parking', '180° Parking'),
('flush_landing', 'Flush Landing'),
('vertical_overrun', 'Vertical Overrun (Custom)'),
],
string='Top Landing Type',
default='none',
help='Type of landing at the top of the staircase',
)
top_overrun_custom_length = fields.Float(
string='Top Overrun Length (inches)',
help='Custom overrun length when Vertical Overrun is selected',
)
# Bottom Landing Options
stair_bottom_landing_type = fields.Selection(
selection=[
('none', 'Standard (No special landing)'),
('90_park', '90° Park'),
('180_park', '180° Park'),
('drop_nose', 'Drop Nose Landing'),
('short_vertical', 'Short Vertical Start'),
('horizontal_overrun', 'Horizontal Overrun (Custom)'),
],
string='Bottom Landing Type',
default='none',
help='Type of landing at the bottom of the staircase',
)
bottom_overrun_custom_length = fields.Float(
string='Bottom Overrun Length (inches)',
help='Custom overrun length when Horizontal Overrun is selected',
)
# Legacy fields kept for backwards compatibility
stair_has_drop_nose = fields.Boolean(string='Has Drop Nose (Legacy)')
stair_parking_type = fields.Selection(
selection=[
('none', 'No Parking'),
('90_degree', '90° Parking (+2 feet)'),
('180_degree', '180° Parking (+4 feet)'),
],
string='Parking Type (Legacy)',
default='none',
)
stair_power_swivel_downstairs = fields.Boolean(string='Power Swivel (Downstairs)')
stair_auto_folding_footrest = fields.Boolean(string='Automatic Folding Footrest')
stair_auto_folding_hinge = fields.Boolean(string='Automatic Folding Hinge')
stair_auto_folding_seat = fields.Boolean(string='Automatic Folding Seat')
stair_custom_color = fields.Boolean(string='Customizable Colored Seat')
stair_additional_charging = fields.Boolean(string='Additional Charging Station')
stair_charging_with_remote = fields.Boolean(string='Charging Station with Remote')
stair_curved_calculated_length = fields.Float(
string='Calculated Track Length (inches)',
compute='_compute_stair_curved_length',
store=True,
)
stair_curved_manual_override = fields.Float(string='Manual Length Override (inches)')
stair_curved_final_length = fields.Float(
string='Final Track Length (inches)',
compute='_compute_stair_curved_final_length',
store=True,
)
# ==========================================================================
# VERTICAL PLATFORM LIFT (VPL) FIELDS
# ==========================================================================
vpl_room_width = fields.Float(string='Room Width (inches)')
vpl_room_depth = fields.Float(string='Room Depth (inches)')
vpl_rise_height = fields.Float(string='Total Rise Height (inches)')
vpl_has_existing_platform = fields.Boolean(string='Existing Platform Available')
vpl_concrete_depth = fields.Float(string='Concrete Depth (inches)', help='Minimum 4 inches required')
vpl_model_type = fields.Selection(
selection=[
('ac', 'AC Model (Dedicated 15-amp breaker required)'),
('dc', 'DC Model (No dedicated breaker required)'),
],
string='Model Type',
)
vpl_has_nearby_plug = fields.Boolean(string='Power Plug Nearby')
vpl_plug_specs = fields.Char(string='Plug Specifications', default='110V / 15-amp')
vpl_needs_plug_install = fields.Boolean(string='Needs Plug Installation')
vpl_needs_certification = fields.Boolean(string='Needs City Certification')
vpl_certification_notes = fields.Text(string='Certification Notes')
# ==========================================================================
# CEILING LIFT FIELDS
# ==========================================================================
ceiling_track_length = fields.Float(string='Total Track Length (feet)')
ceiling_movement_type = fields.Selection(
selection=[
('manual', 'Manual Movement (left-to-right)'),
('powered', 'Powered Movement (left-to-right)'),
],
string='Horizontal Movement Type',
help='All ceiling lifts move up/down with power. This is for left-to-right movement.',
)
ceiling_charging_throughout = fields.Boolean(
string='Charging Throughout Track',
help='Charging available throughout the track instead of one location',
)
ceiling_carry_bar = fields.Boolean(string='Carry Bar')
ceiling_additional_slings = fields.Integer(string='Additional Slings Needed')
# ==========================================================================
# CUSTOM RAMP FIELDS
# ==========================================================================
ramp_height = fields.Float(string='Total Height (inches from ground)')
ramp_ground_incline = fields.Float(string='Ground Incline (degrees)', help='Optional - if ground is inclined')
ramp_at_door = fields.Boolean(string='Ramp at Door', help='Requires 5ft landing at door')
ramp_calculated_length = fields.Float(
string='Calculated Ramp Length (inches)',
compute='_compute_ramp_length',
store=True,
help='Ontario Building Code: 12 inches length per 1 inch height',
)
ramp_landings_needed = fields.Integer(
string='Landings Needed',
compute='_compute_ramp_landings',
store=True,
help='Landing required every 30 feet (minimum 5 feet each)',
)
ramp_total_length = fields.Float(
string='Total Length with Landings (inches)',
compute='_compute_ramp_total_length',
store=True,
)
ramp_handrail_height = fields.Float(
string='Handrail Height (inches)',
default=32.0,
help='Minimum 32 inches required',
)
ramp_manual_override = fields.Float(string='Manual Length Override (inches)')
# ==========================================================================
# BATHROOM MODIFICATION FIELDS
# ==========================================================================
bathroom_description = fields.Text(
string='Modification Description',
help='Describe all bathroom modifications needed',
)
# ==========================================================================
# TUB CUTOUT FIELDS
# ==========================================================================
tub_internal_height = fields.Float(string='Internal Height of Tub (inches)')
tub_external_height = fields.Float(string='External Height of Tub (inches)')
tub_additional_supplies = fields.Text(string='Additional Supplies Needed')
# ==========================================================================
# COMPUTED FIELDS
# ==========================================================================
@api.depends('reference', 'assessment_type', 'client_name')
def _compute_display_name(self):
type_labels = dict(self._fields['assessment_type'].selection)
for rec in self:
type_label = type_labels.get(rec.assessment_type, '')
rec.display_name = f"{rec.reference or 'New'} - {type_label} - {rec.client_name or ''}"
@api.depends('stair_steps', 'stair_nose_to_nose')
def _compute_stair_straight_length(self):
"""Straight stair lift: (steps × nose_to_nose) + 13" top landing"""
for rec in self:
if rec.stair_steps and rec.stair_nose_to_nose:
rec.stair_calculated_length = (rec.stair_steps * rec.stair_nose_to_nose) + 13
else:
rec.stair_calculated_length = 0
@api.depends('stair_calculated_length', 'stair_manual_length_override')
def _compute_stair_final_length(self):
"""Use manual override if provided, otherwise use calculated"""
for rec in self:
if rec.stair_manual_length_override:
rec.stair_final_length = rec.stair_manual_length_override
else:
rec.stair_final_length = rec.stair_calculated_length
@api.depends('stair_curved_steps', 'stair_curves_count',
'stair_top_landing_type', 'stair_bottom_landing_type',
'top_overrun_custom_length', 'bottom_overrun_custom_length')
def _compute_stair_curved_length(self):
"""Curved stair lift calculation:
- 12" per step
- 16" per curve
- Top landing type additions (or custom overrun)
- Bottom landing type additions (or custom overrun)
"""
# Track length additions for each landing type (in inches)
# Note: vertical_overrun and horizontal_overrun use custom lengths
TOP_LANDING_LENGTHS = {
'none': 0,
'90_exit': 24, # 2 feet
'90_parking': 24, # 2 feet
'180_parking': 48, # 4 feet
'flush_landing': 12, # 1 foot
}
BOTTOM_LANDING_LENGTHS = {
'none': 0,
'90_park': 24, # 2 feet
'180_park': 48, # 4 feet
'drop_nose': 12, # 1 foot
'short_vertical': 12, # 1 foot
}
for rec in self:
if rec.stair_curved_steps:
base_length = rec.stair_curved_steps * 12 # 12" per step
curves_length = (rec.stair_curves_count or 0) * 16 # 16" per curve
# Top landing length - use custom if overrun selected
if rec.stair_top_landing_type == 'vertical_overrun':
top_landing = rec.top_overrun_custom_length or 0
else:
top_landing = TOP_LANDING_LENGTHS.get(rec.stair_top_landing_type or 'none', 0)
# Bottom landing length - use custom if overrun selected
if rec.stair_bottom_landing_type == 'horizontal_overrun':
bottom_landing = rec.bottom_overrun_custom_length or 0
else:
bottom_landing = BOTTOM_LANDING_LENGTHS.get(rec.stair_bottom_landing_type or 'none', 0)
rec.stair_curved_calculated_length = (
base_length + curves_length + top_landing + bottom_landing
)
else:
rec.stair_curved_calculated_length = 0
@api.depends('stair_curved_calculated_length', 'stair_curved_manual_override')
def _compute_stair_curved_final_length(self):
"""Use manual override if provided, otherwise use calculated"""
for rec in self:
if rec.stair_curved_manual_override:
rec.stair_curved_final_length = rec.stair_curved_manual_override
else:
rec.stair_curved_final_length = rec.stair_curved_calculated_length
@api.depends('ramp_height')
def _compute_ramp_length(self):
"""Ontario Building Code: 12 inches length per 1 inch height (1:12 ratio)"""
for rec in self:
if rec.ramp_height:
rec.ramp_calculated_length = rec.ramp_height * 12
else:
rec.ramp_calculated_length = 0
@api.depends('ramp_calculated_length')
def _compute_ramp_landings(self):
"""Landing required every 30 feet (360 inches)"""
for rec in self:
if rec.ramp_calculated_length:
# Calculate how many landings are needed (every 30 feet = 360 inches)
rec.ramp_landings_needed = math.ceil(rec.ramp_calculated_length / 360)
else:
rec.ramp_landings_needed = 0
@api.depends('ramp_calculated_length', 'ramp_landings_needed', 'ramp_at_door')
def _compute_ramp_total_length(self):
"""Total length including landings (5 feet = 60 inches each)"""
for rec in self:
base_length = rec.ramp_calculated_length or 0
landings_length = (rec.ramp_landings_needed or 0) * 60 # 5 feet per landing
door_landing = 60 if rec.ramp_at_door else 0 # 5 feet at door
rec.ramp_total_length = base_length + landings_length + door_landing
# ==========================================================================
# CRUD METHODS
# ==========================================================================
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('reference', _('New')) == _('New'):
vals['reference'] = self.env['ir.sequence'].next_by_code(
'fusion.accessibility.assessment'
) or _('New')
return super().create(vals_list)
# ==========================================================================
# BUSINESS LOGIC
# ==========================================================================
def action_complete(self):
"""Complete the assessment and create a Sale Order"""
self.ensure_one()
if not self.client_name:
raise UserError(_('Please enter the client name.'))
# Create or find partner
partner = self._ensure_partner()
# Create draft sale order
sale_order = self._create_draft_sale_order(partner)
# Add tag based on assessment type
self._add_assessment_tag(sale_order)
# Copy photos from assessment to sale order chatter
self._copy_photos_to_sale_order(sale_order)
# Update state
self.write({
'state': 'completed',
'sale_order_id': sale_order.id,
'partner_id': partner.id,
})
# Send email notification to office
self._send_completion_email(sale_order)
# Schedule follow-up activity for sales rep
self._schedule_followup_activity(sale_order)
_logger.info(f"Completed accessibility assessment {self.reference}, created SO {sale_order.name}")
return sale_order
def _add_assessment_tag(self, sale_order):
"""Add a tag to the sale order based on assessment type"""
self.ensure_one()
# Map assessment types to tag names (ALL CAPS)
tag_map = {
'stairlift_straight': 'STRAIGHT STAIR LIFT',
'stairlift_curved': 'CURVED STAIR LIFT',
'vpl': 'VERTICAL PLATFORM LIFT',
'ceiling_lift': 'CEILING LIFT',
'ramp': 'CUSTOM RAMP',
'bathroom': 'BATHROOM MODIFICATION',
'tub_cutout': 'TUB CUTOUT',
}
tag_name = tag_map.get(self.assessment_type)
if not tag_name:
return
# Find or create the tag
Tag = self.env['crm.tag'].sudo()
tag = Tag.search([('name', '=', tag_name)], limit=1)
if not tag:
tag = Tag.create({'name': tag_name})
_logger.info(f"Created new tag: {tag_name}")
# Add tag to sale order
if hasattr(sale_order, 'tag_ids'):
sale_order.write({'tag_ids': [(4, tag.id)]})
_logger.info(f"Added tag '{tag_name}' to SO {sale_order.name}")
def _copy_photos_to_sale_order(self, sale_order):
"""Copy assessment photos to sale order chatter"""
self.ensure_one()
Attachment = self.env['ir.attachment'].sudo()
# Find photos attached to this assessment
photos = Attachment.search([
('res_model', '=', 'fusion.accessibility.assessment'),
('res_id', '=', self.id),
('mimetype', 'like', 'image/%'),
])
if not photos:
return
# Copy attachments to sale order and post in chatter
attachment_ids = []
for photo in photos:
new_attachment = photo.copy({
'res_model': 'sale.order',
'res_id': sale_order.id,
})
attachment_ids.append(new_attachment.id)
if attachment_ids:
type_labels = dict(self._fields['assessment_type'].selection)
type_label = type_labels.get(self.assessment_type, 'Accessibility')
sale_order.message_post(
body=Markup(f'''
<div class="alert alert-secondary">
<strong><i class="fa fa-camera"></i> Assessment Photos</strong><br/>
{len(attachment_ids)} photo(s) from {type_label} Assessment ({self.reference})
</div>
'''),
message_type='comment',
subtype_xmlid='mail.mt_note',
attachment_ids=attachment_ids,
)
_logger.info(f"Copied {len(attachment_ids)} photos to SO {sale_order.name}")
def _send_completion_email(self, sale_order):
"""Send email notification to office about assessment completion"""
self.ensure_one()
ICP = self.env['ir.config_parameter'].sudo()
# Check if email notifications are enabled
if not ICP.get_param('fusion_claims.enable_email_notifications', 'True') == 'True':
return
# Get office notification emails from company
company = self.env.company
office_partners = company.sudo().x_fc_office_notification_ids
email_list = [p.email for p in office_partners if p.email]
office_emails = ', '.join(email_list)
if not office_emails:
_logger.warning("No office notification recipients configured for accessibility assessment completion")
return
type_labels = dict(self._fields['assessment_type'].selection)
type_label = type_labels.get(self.assessment_type, 'Accessibility')
body = self._email_build(
title='Accessibility Assessment Completed',
summary=f'A new {type_label.lower()} assessment has been completed for '
f'<strong>{self.client_name}</strong>. A sale order has been created.',
email_type='info',
sections=[('Assessment Details', [
('Type', type_label),
('Reference', self.reference),
('Client', self.client_name),
('Sales Rep', self.sales_rep_id.name if self.sales_rep_id else 'N/A'),
('Sale Order', sale_order.name),
])],
button_url=f'{sale_order.get_base_url()}/web#id={sale_order.id}&model=sale.order&view_type=form',
button_text='View Sale Order',
)
# Send email
mail_values = {
'subject': f'Accessibility Assessment Completed: {type_label} - {self.client_name}',
'body_html': body,
'email_to': office_emails,
'email_from': self.env.company.email or 'noreply@example.com',
}
try:
mail = self.env['mail.mail'].sudo().create(mail_values)
mail.send()
_logger.info(f"Sent accessibility assessment completion email to {office_emails}")
except Exception as e:
_logger.error(f"Failed to send assessment completion email: {e}")
def _schedule_followup_activity(self, sale_order):
"""Schedule a follow-up activity for the sales rep"""
self.ensure_one()
if not self.sales_rep_id:
return
type_labels = dict(self._fields['assessment_type'].selection)
type_label = type_labels.get(self.assessment_type, 'Accessibility')
# 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
# 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=self.sales_rep_id.id,
summary=f'Follow up on {type_label} Assessment',
note=f'Assessment {self.reference} for {self.client_name} has been completed. Please follow up with the client.',
)
_logger.info(f"Scheduled follow-up activity for {self.sales_rep_id.name} on SO {sale_order.name}")
except Exception as e:
_logger.error(f"Failed to schedule follow-up activity: {e}")
def _ensure_partner(self):
"""Find or create a partner for the client"""
self.ensure_one()
Partner = self.env['res.partner'].sudo()
# First, try to find existing partner by email
if self.client_email:
existing = Partner.search([('email', '=ilike', self.client_email)], limit=1)
if existing:
return existing
# Create new partner
partner_vals = {
'name': self.client_name,
'email': self.client_email,
'phone': self.client_phone,
'street': self.client_address_street or self.client_address,
'street2': self.client_unit or False,
'city': self.client_address_city,
'zip': self.client_address_postal,
'customer_rank': 1,
}
# Set province/state if provided
if self.client_address_province:
state = self.env['res.country.state'].sudo().search([
('code', '=ilike', self.client_address_province),
('country_id.code', '=', 'CA'),
], limit=1)
if state:
partner_vals['state_id'] = state.id
partner_vals['country_id'] = state.country_id.id
else:
# Default to Canada
canada = self.env.ref('base.ca', raise_if_not_found=False)
if canada:
partner_vals['country_id'] = canada.id
partner = Partner.create(partner_vals)
_logger.info(f"Created partner {partner.name} from accessibility 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()
type_labels = dict(self._fields['assessment_type'].selection)
type_label = type_labels.get(self.assessment_type, 'Accessibility')
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'Accessibility: {self.reference} ({type_label})',
'x_fc_sale_type': 'direct_private', # Accessibility items typically private pay
}
sale_order = SaleOrder.create(so_vals)
_logger.info(f"Created draft sale order {sale_order.name} from accessibility assessment {self.reference}")
# Post assessment details to chatter
assessment_html = self._format_assessment_html_table()
sale_order.message_post(
body=Markup(assessment_html),
message_type='comment',
subtype_xmlid='mail.mt_note',
)
return sale_order
def _format_assessment_html_table(self):
"""Format assessment details as HTML for chatter"""
type_labels = dict(self._fields['assessment_type'].selection)
type_label = type_labels.get(self.assessment_type, 'Unknown')
html = f'''
<div class="alert alert-info" role="alert">
<h5 class="alert-heading"><i class="fa fa-wheelchair"></i> Accessibility Assessment: {type_label}</h5>
<p><strong>Reference:</strong> {self.reference}<br/>
<strong>Client:</strong> {self.client_name}<br/>
<strong>Address:</strong> {self.client_address or 'N/A'}<br/>
<strong>Date:</strong> {self.assessment_date}</p>
'''
# Add type-specific details
if self.assessment_type == 'stairlift_straight':
html += f'''
<hr>
<p><strong>Straight Stair Lift Details:</strong></p>
<ul>
<li>Steps: {self.stair_steps or 'N/A'}</li>
<li>Nose to Nose: {self.stair_nose_to_nose or 0}" per step</li>
<li>Installation Side: {self.stair_side or 'N/A'}</li>
<li>Style: {dict(self._fields['stair_style'].selection or {}).get(self.stair_style, 'N/A')}</li>
<li>Calculated Track Length: {self.stair_calculated_length:.1f}"</li>
<li>Final Track Length: {self.stair_final_length:.1f}"</li>
</ul>
<p><strong>Features:</strong></p>
<ul>
{'<li>Power Swivel (Upstairs)</li>' if self.stair_power_swivel_upstairs else ''}
{'<li>Power Folding Footrest</li>' if self.stair_power_folding_footrest else ''}
</ul>
'''
elif self.assessment_type == 'stairlift_curved':
# Format landing types for display
top_landing_display = dict(self._fields['stair_top_landing_type'].selection or {}).get(self.stair_top_landing_type, 'Standard')
bottom_landing_display = dict(self._fields['stair_bottom_landing_type'].selection or {}).get(self.stair_bottom_landing_type, 'Standard')
# Add custom overrun values if applicable
if self.stair_top_landing_type == 'vertical_overrun' and self.top_overrun_custom_length:
top_landing_display += f' ({self.top_overrun_custom_length:.1f}")'
if self.stair_bottom_landing_type == 'horizontal_overrun' and self.bottom_overrun_custom_length:
bottom_landing_display += f' ({self.bottom_overrun_custom_length:.1f}")'
html += f'''
<hr>
<p><strong>Curved Stair Lift Details:</strong></p>
<ul>
<li>Steps: {self.stair_curved_steps or 'N/A'}</li>
<li>Number of Curves: {self.stair_curves_count or 0}</li>
<li>Top Landing: {top_landing_display}</li>
<li>Bottom Landing: {bottom_landing_display}</li>
<li>Calculated Track Length: {self.stair_curved_calculated_length:.1f}"</li>
<li>Final Track Length: {self.stair_curved_final_length:.1f}"</li>
</ul>
<p><strong>Features:</strong></p>
<ul>
{'<li>Power Swivel (Upstairs)</li>' if self.stair_power_swivel_upstairs else ''}
{'<li>Power Swivel (Downstairs)</li>' if self.stair_power_swivel_downstairs else ''}
{'<li>Auto Folding Footrest</li>' if self.stair_auto_folding_footrest else ''}
{'<li>Auto Folding Hinge</li>' if self.stair_auto_folding_hinge else ''}
{'<li>Auto Folding Seat</li>' if self.stair_auto_folding_seat else ''}
{'<li>Customizable Color</li>' if self.stair_custom_color else ''}
{'<li>Additional Charging Station</li>' if self.stair_additional_charging else ''}
{'<li>Charging with Remote</li>' if self.stair_charging_with_remote else ''}
</ul>
'''
elif self.assessment_type == 'vpl':
html += f'''
<hr>
<p><strong>Vertical Platform Lift Details:</strong></p>
<ul>
<li>Room Dimensions: {self.vpl_room_width or 0}" W x {self.vpl_room_depth or 0}" D</li>
<li>Rise Height: {self.vpl_rise_height or 0}"</li>
<li>Existing Platform: {'Yes' if self.vpl_has_existing_platform else 'No'}</li>
<li>Concrete Depth: {self.vpl_concrete_depth or 0}" (min 4" required)</li>
<li>Model Type: {dict(self._fields['vpl_model_type'].selection or {}).get(self.vpl_model_type, 'N/A')}</li>
<li>Power Plug Nearby: {'Yes' if self.vpl_has_nearby_plug else 'No'}</li>
<li>Needs Plug Installation: {'Yes' if self.vpl_needs_plug_install else 'No'}</li>
<li>Needs Certification: {'Yes' if self.vpl_needs_certification else 'No'}</li>
</ul>
'''
elif self.assessment_type == 'ceiling_lift':
html += f'''
<hr>
<p><strong>Ceiling Lift Details:</strong></p>
<ul>
<li>Track Length: {self.ceiling_track_length or 0} feet</li>
<li>Movement Type: {dict(self._fields['ceiling_movement_type'].selection or {}).get(self.ceiling_movement_type, 'N/A')}</li>
<li>Charging Throughout Track: {'Yes' if self.ceiling_charging_throughout else 'No'}</li>
<li>Carry Bar: {'Yes' if self.ceiling_carry_bar else 'No'}</li>
<li>Additional Slings: {self.ceiling_additional_slings or 0}</li>
</ul>
'''
elif self.assessment_type == 'ramp':
html += f'''
<hr>
<p><strong>Custom Ramp Details:</strong></p>
<ul>
<li>Height: {self.ramp_height or 0}" from ground</li>
<li>Ground Incline: {self.ramp_ground_incline or 0}°</li>
<li>At Door: {'Yes (5ft landing required)' if self.ramp_at_door else 'No'}</li>
<li>Calculated Ramp Length: {self.ramp_calculated_length:.1f}" ({self.ramp_calculated_length/12:.1f} ft)</li>
<li>Landings Needed: {self.ramp_landings_needed or 0} (5ft each)</li>
<li>Total Length with Landings: {self.ramp_total_length:.1f}" ({self.ramp_total_length/12:.1f} ft)</li>
<li>Handrail Height: {self.ramp_handrail_height or 32}"</li>
</ul>
'''
elif self.assessment_type == 'bathroom':
html += f'''
<hr>
<p><strong>Bathroom Modification Description:</strong></p>
<p>{self.bathroom_description or 'No description provided.'}</p>
'''
elif self.assessment_type == 'tub_cutout':
html += f'''
<hr>
<p><strong>Tub Cutout Details:</strong></p>
<ul>
<li>Internal Height: {self.tub_internal_height or 0}"</li>
<li>External Height: {self.tub_external_height or 0}"</li>
</ul>
<p><strong>Additional Supplies:</strong></p>
<p>{self.tub_additional_supplies or 'None specified.'}</p>
'''
# Add general notes
if self.notes:
html += f'''
<hr>
<p><strong>Notes:</strong></p>
<p>{self.notes}</p>
'''
html += '</div>'
return html
def action_cancel(self):
"""Cancel the assessment"""
self.ensure_one()
self.write({'state': 'cancelled'})
def action_reset_to_draft(self):
"""Reset to draft state"""
self.ensure_one()
self.write({'state': 'draft'})