# -*- 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'}