Initial commit
This commit is contained in:
298
fusion_claims/models/client_profile.py
Normal file
298
fusion_claims/models/client_profile.py
Normal file
@@ -0,0 +1,298 @@
|
||||
# -*- 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'}
|
||||
Reference in New Issue
Block a user