299 lines
13 KiB
Python
299 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2024-2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
|
|
from odoo import api, fields, models
|
|
|
|
|
|
class FusionClientProfile(models.Model):
|
|
_name = 'fusion.client.profile'
|
|
_description = 'Client Profile'
|
|
_order = 'last_name, first_name'
|
|
_rec_name = 'display_name'
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
|
|
# ------------------------------------------------------------------
|
|
# PERSONAL INFORMATION (from ADP XML Section 1)
|
|
# ------------------------------------------------------------------
|
|
partner_id = fields.Many2one(
|
|
'res.partner', string='Odoo Contact',
|
|
help='Linked contact record in Odoo',
|
|
)
|
|
first_name = fields.Char(string='First Name', tracking=True)
|
|
last_name = fields.Char(string='Last Name', tracking=True)
|
|
middle_initial = fields.Char(string='Middle Initial')
|
|
display_name = fields.Char(
|
|
string='Name', compute='_compute_display_name', store=True,
|
|
)
|
|
health_card_number = fields.Char(
|
|
string='Health Card Number', index=True, tracking=True,
|
|
help='Ontario Health Card Number (10 digits)',
|
|
)
|
|
health_card_version = fields.Char(string='Health Card Version')
|
|
date_of_birth = fields.Date(string='Date of Birth', tracking=True)
|
|
ltch_name = fields.Char(
|
|
string='Long-Term Care Home',
|
|
help='Name of LTCH if applicable',
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# ADDRESS
|
|
# ------------------------------------------------------------------
|
|
unit_number = fields.Char(string='Unit Number')
|
|
street_number = fields.Char(string='Street Number')
|
|
street_name = fields.Char(string='Street Name')
|
|
rural_route = fields.Char(string='Lot/Concession/Rural Route')
|
|
city = fields.Char(string='City', index=True, tracking=True)
|
|
province = fields.Char(string='Province', default='ON')
|
|
postal_code = fields.Char(string='Postal Code')
|
|
|
|
# ------------------------------------------------------------------
|
|
# CONTACT
|
|
# ------------------------------------------------------------------
|
|
home_phone = fields.Char(string='Home Phone')
|
|
business_phone = fields.Char(string='Business Phone')
|
|
phone_extension = fields.Char(string='Phone Extension')
|
|
|
|
# ------------------------------------------------------------------
|
|
# BENEFITS ELIGIBILITY (from XML confirmationOfBenefit)
|
|
# ------------------------------------------------------------------
|
|
receives_social_assistance = fields.Boolean(
|
|
string='Receives Social Assistance', tracking=True,
|
|
)
|
|
benefit_type = fields.Selection([
|
|
('owp', 'Ontario Works Program (OWP)'),
|
|
('odsp', 'Ontario Disability Support Program (ODSP)'),
|
|
('acsd', 'Assistance to Children with Severe Disabilities (ACSD)'),
|
|
], string='Benefit Type', tracking=True)
|
|
wsib_eligible = fields.Boolean(string='WSIB Eligible', tracking=True)
|
|
vac_eligible = fields.Boolean(
|
|
string='Veterans Affairs Canada Eligible', tracking=True,
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# CURRENT MEDICAL STATUS (updated from latest XML)
|
|
# ------------------------------------------------------------------
|
|
medical_condition = fields.Text(
|
|
string='Medical Condition/Diagnosis', tracking=True,
|
|
help='Current presenting medical condition from latest ADP application',
|
|
)
|
|
mobility_status = fields.Text(
|
|
string='Functional Mobility Status', tracking=True,
|
|
help='Current functional mobility status from latest ADP application',
|
|
)
|
|
last_assessment_date = fields.Date(
|
|
string='Last Assessment Date', tracking=True,
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# RELATIONSHIPS
|
|
# ------------------------------------------------------------------
|
|
application_data_ids = fields.One2many(
|
|
'fusion.adp.application.data', 'profile_id',
|
|
string='ADP Applications',
|
|
)
|
|
# Chat is handled via Odoo's native AI agent (discuss.channel with ai_chat type)
|
|
|
|
# ------------------------------------------------------------------
|
|
# COMPUTED FIELDS
|
|
# ------------------------------------------------------------------
|
|
claim_count = fields.Integer(
|
|
string='Claims', compute='_compute_claim_stats', store=True,
|
|
)
|
|
total_adp_funded = fields.Monetary(
|
|
string='Total ADP Funded', compute='_compute_claim_stats', store=True,
|
|
currency_field='currency_id',
|
|
)
|
|
total_client_portion = fields.Monetary(
|
|
string='Total Client Portion', compute='_compute_claim_stats', store=True,
|
|
currency_field='currency_id',
|
|
)
|
|
total_amount = fields.Monetary(
|
|
string='Total Amount', compute='_compute_claim_stats', store=True,
|
|
currency_field='currency_id',
|
|
)
|
|
currency_id = fields.Many2one(
|
|
'res.currency', string='Currency',
|
|
default=lambda self: self.env.company.currency_id,
|
|
)
|
|
application_count = fields.Integer(
|
|
string='Applications', compute='_compute_application_count',
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# AI ANALYSIS (auto-computed from application data)
|
|
# ------------------------------------------------------------------
|
|
ai_summary = fields.Text(
|
|
string='Summary',
|
|
compute='_compute_ai_analysis',
|
|
)
|
|
ai_risk_flags = fields.Text(
|
|
string='Risk Flags',
|
|
compute='_compute_ai_analysis',
|
|
)
|
|
ai_last_analyzed = fields.Datetime(string='Last AI Analysis')
|
|
|
|
# ------------------------------------------------------------------
|
|
# COMPUTED METHODS
|
|
# ------------------------------------------------------------------
|
|
@api.depends('first_name', 'last_name')
|
|
def _compute_display_name(self):
|
|
for profile in self:
|
|
parts = [profile.last_name or '', profile.first_name or '']
|
|
profile.display_name = ', '.join(p for p in parts if p) or 'New Profile'
|
|
|
|
@api.depends('partner_id', 'partner_id.sale_order_ids',
|
|
'partner_id.sale_order_ids.x_fc_adp_portion_total',
|
|
'partner_id.sale_order_ids.x_fc_client_portion_total',
|
|
'partner_id.sale_order_ids.amount_total')
|
|
def _compute_claim_stats(self):
|
|
for profile in self:
|
|
if profile.partner_id:
|
|
orders = self.env['sale.order'].search([
|
|
('partner_id', '=', profile.partner_id.id),
|
|
('x_fc_sale_type', '!=', False),
|
|
])
|
|
profile.claim_count = len(orders)
|
|
profile.total_adp_funded = sum(orders.mapped('x_fc_adp_portion_total'))
|
|
profile.total_client_portion = sum(orders.mapped('x_fc_client_portion_total'))
|
|
profile.total_amount = sum(orders.mapped('amount_total'))
|
|
else:
|
|
profile.claim_count = 0
|
|
profile.total_adp_funded = 0
|
|
profile.total_client_portion = 0
|
|
profile.total_amount = 0
|
|
|
|
def _compute_application_count(self):
|
|
for profile in self:
|
|
profile.application_count = len(profile.application_data_ids)
|
|
|
|
@api.depends('application_data_ids', 'application_data_ids.application_date',
|
|
'application_data_ids.base_device', 'application_data_ids.reason_for_application')
|
|
def _compute_ai_analysis(self):
|
|
for profile in self:
|
|
apps = profile.application_data_ids.sorted('application_date', reverse=True)
|
|
|
|
# --- SUMMARY ---
|
|
summary_lines = []
|
|
|
|
# Number of applications
|
|
app_count = len(apps)
|
|
summary_lines.append(f"Total Applications: {app_count}")
|
|
|
|
# Last funding history
|
|
if apps:
|
|
latest = apps[0]
|
|
date_str = latest.application_date.strftime('%B %d, %Y') if latest.application_date else 'Unknown date'
|
|
device = latest.base_device or 'Not specified'
|
|
category = dict(latest._fields['device_category'].selection).get(
|
|
latest.device_category, latest.device_category or 'N/A'
|
|
) if latest.device_category else 'N/A'
|
|
summary_lines.append(f"Last Application: {date_str}")
|
|
summary_lines.append(f"Last Device: {device} ({category})")
|
|
|
|
# Reason for last application
|
|
reason = latest.reason_for_application or 'Not specified'
|
|
summary_lines.append(f"Reason: {reason}")
|
|
|
|
# Authorizer
|
|
auth_name = f"{latest.authorizer_first_name or ''} {latest.authorizer_last_name or ''}".strip()
|
|
if auth_name:
|
|
summary_lines.append(f"Authorizer: {auth_name}")
|
|
|
|
# All devices received (unique)
|
|
devices = set()
|
|
for a in apps:
|
|
if a.base_device:
|
|
devices.add(a.base_device)
|
|
if len(devices) > 1:
|
|
summary_lines.append(f"All Devices: {', '.join(sorted(devices))}")
|
|
else:
|
|
summary_lines.append("No applications on file.")
|
|
|
|
profile.ai_summary = '\n'.join(summary_lines)
|
|
|
|
# --- RISK FLAGS ---
|
|
risk_lines = []
|
|
|
|
if app_count >= 2:
|
|
# Calculate frequency
|
|
dated_apps = [a for a in apps if a.application_date]
|
|
if len(dated_apps) >= 2:
|
|
dates = sorted([a.application_date for a in dated_apps])
|
|
total_span = (dates[-1] - dates[0]).days
|
|
if total_span > 0:
|
|
avg_days = total_span / (len(dates) - 1)
|
|
if avg_days < 365:
|
|
risk_lines.append(
|
|
f"High Frequency: {app_count} applications over "
|
|
f"{total_span} days (avg {avg_days:.0f} days apart)"
|
|
)
|
|
elif avg_days < 730:
|
|
risk_lines.append(
|
|
f"Moderate Frequency: {app_count} applications over "
|
|
f"{total_span // 365} year(s) (avg {avg_days:.0f} days apart)"
|
|
)
|
|
else:
|
|
risk_lines.append(
|
|
f"Normal Frequency: {app_count} applications over "
|
|
f"{total_span // 365} year(s) (avg {avg_days:.0f} days apart)"
|
|
)
|
|
|
|
# Check for multiple replacements
|
|
replacements = [a for a in apps if a.reason_for_application and 'replacement' in a.reason_for_application.lower()]
|
|
if len(replacements) >= 2:
|
|
risk_lines.append(f"Multiple Replacements: {len(replacements)} replacement applications")
|
|
|
|
if not risk_lines:
|
|
risk_lines.append("No flags identified.")
|
|
|
|
profile.ai_risk_flags = '\n'.join(risk_lines)
|
|
|
|
# ------------------------------------------------------------------
|
|
# ACTIONS
|
|
# ------------------------------------------------------------------
|
|
def action_view_claims(self):
|
|
"""Open sale orders for this client."""
|
|
self.ensure_one()
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': f'Claims - {self.display_name}',
|
|
'res_model': 'sale.order',
|
|
'view_mode': 'list,form',
|
|
'domain': [('partner_id', '=', self.partner_id.id), ('x_fc_sale_type', '!=', False)],
|
|
}
|
|
|
|
def action_view_applications(self):
|
|
"""Open parsed ADP application data for this client."""
|
|
self.ensure_one()
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': f'Applications - {self.display_name}',
|
|
'res_model': 'fusion.adp.application.data',
|
|
'view_mode': 'list,form',
|
|
'domain': [('profile_id', '=', self.id)],
|
|
}
|
|
|
|
def action_open_ai_chat(self):
|
|
"""Open AI chat about this client using Odoo's native AI agent."""
|
|
self.ensure_one()
|
|
agent = self.env.ref('fusion_claims.ai_agent_fusion_claims', raise_if_not_found=False)
|
|
if agent:
|
|
# Create channel with client context so the AI knows which client
|
|
channel = agent._create_ai_chat_channel()
|
|
# Post an initial context message about this client
|
|
initial_prompt = (
|
|
f"I want to ask about client {self.display_name} "
|
|
f"(Profile ID: {self.id}, Health Card: {self.health_card_number or 'N/A'}). "
|
|
f"Please look up their details."
|
|
)
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'agent_chat_action',
|
|
'params': {
|
|
'channelId': channel.id,
|
|
'user_prompt': initial_prompt,
|
|
},
|
|
}
|
|
return {'type': 'ir.actions.act_window_close'}
|