feat: separate fusion field service and LTC into standalone modules, update core modules
- 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
This commit is contained in:
@@ -4,74 +4,16 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import date, timedelta
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
PREV_FUNDED_FIELDS = {
|
||||
'prev_funded_forearm': 'Forearm Crutches',
|
||||
'prev_funded_wheeled': 'Wheeled Walker',
|
||||
'prev_funded_manual': 'Manual Wheelchair',
|
||||
'prev_funded_power': 'Power Wheelchair',
|
||||
'prev_funded_addon': 'Power Add-On Device',
|
||||
'prev_funded_scooter': 'Power Scooter',
|
||||
'prev_funded_seating': 'Positioning Devices',
|
||||
'prev_funded_tilt': 'Power Tilt System',
|
||||
'prev_funded_recline': 'Power Recline System',
|
||||
'prev_funded_legrests': 'Power Elevating Leg Rests',
|
||||
'prev_funded_frame': 'Paediatric Standing Frame',
|
||||
'prev_funded_stroller': 'Paediatric Specialty Stroller',
|
||||
}
|
||||
|
||||
STATUS_NEXT_STEPS = {
|
||||
'quotation': 'Schedule assessment with the client',
|
||||
'assessment_scheduled': 'Complete the assessment',
|
||||
'assessment_completed': 'Prepare and send ADP application to client',
|
||||
'waiting_for_application': 'Follow up with client to return signed application',
|
||||
'application_received': 'Review application and prepare for submission',
|
||||
'ready_submission': 'Submit application to ADP',
|
||||
'submitted': 'Wait for ADP acceptance (typically within 24 hours)',
|
||||
'accepted': 'Wait for ADP approval decision',
|
||||
'rejected': 'Review rejection reason and correct the application',
|
||||
'resubmitted': 'Wait for ADP acceptance of resubmission',
|
||||
'needs_correction': 'Review and correct the application per ADP feedback',
|
||||
'approved': 'Prepare order for delivery',
|
||||
'approved_deduction': 'Prepare order for delivery (note: approved with deduction)',
|
||||
'ready_delivery': 'Schedule and complete delivery to client',
|
||||
'ready_bill': 'Create and submit ADP invoice',
|
||||
'billed': 'Monitor for ADP payment',
|
||||
'case_closed': 'No further action required',
|
||||
'on_hold': 'Check hold reason and follow up when ready to resume',
|
||||
'denied': 'Review denial reason; consider appeal or alternative funding',
|
||||
'withdrawn': 'No further action unless client wants to reinstate',
|
||||
'cancelled': 'No further action required',
|
||||
'expired': 'Contact client about reapplication if still needed',
|
||||
}
|
||||
|
||||
BASE_DEVICE_LABELS = {
|
||||
'adultWalkertype1': 'Adult Walker Type 1',
|
||||
'adultWalkertype2': 'Adult Walker Type 2',
|
||||
'adultWalkertype3': 'Adult Walker Type 3',
|
||||
'adultlightwtStdwheelchair': 'Lightweight Standard Wheelchair',
|
||||
'adultlightwtPermwheelchair': 'Lightweight Permanent Wheelchair',
|
||||
'adultTiltwheelchair': 'Adult Tilt Wheelchair',
|
||||
'adultStdwheelchair': 'Adult Standard Wheelchair',
|
||||
'adultHighperfwheelchair': 'Adult High Performance Wheelchair',
|
||||
'adultType2': 'Adult Power Type 2',
|
||||
'adultType3': 'Adult Power Type 3',
|
||||
'powerScooter': 'Power Scooter',
|
||||
}
|
||||
|
||||
|
||||
class AIAgentFusionClaims(models.Model):
|
||||
"""Extend ai.agent with Fusion Claims tool methods."""
|
||||
_inherit = 'ai.agent'
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tool 1: Search Client Profiles
|
||||
# ------------------------------------------------------------------
|
||||
def _fc_tool_search_clients(self, search_term=None, city_filter=None, condition_filter=None):
|
||||
"""AI Tool: Search client profiles."""
|
||||
Profile = self.env['fusion.client.profile'].sudo()
|
||||
@@ -104,37 +46,20 @@ class AIAgentFusionClaims(models.Model):
|
||||
})
|
||||
return json.dumps({'count': len(results), 'profiles': results})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tool 2: Get Client Details (enriched with funding history)
|
||||
# ------------------------------------------------------------------
|
||||
def _fc_tool_client_details(self, profile_id):
|
||||
"""AI Tool: Get detailed client information with funding history."""
|
||||
"""AI Tool: Get detailed client information."""
|
||||
Profile = self.env['fusion.client.profile'].sudo()
|
||||
profile = Profile.browse(int(profile_id))
|
||||
if not profile.exists():
|
||||
return json.dumps({'error': 'Profile not found'})
|
||||
|
||||
# Get orders
|
||||
orders = []
|
||||
if profile.partner_id:
|
||||
Invoice = self.env['account.move'].sudo()
|
||||
for o in self.env['sale.order'].sudo().search([
|
||||
('partner_id', '=', profile.partner_id.id),
|
||||
('x_fc_sale_type', '!=', False),
|
||||
], limit=20, order='date_order desc'):
|
||||
invoices = Invoice.search([
|
||||
('x_fc_source_sale_order_id', '=', o.id),
|
||||
('move_type', '=', 'out_invoice'),
|
||||
])
|
||||
inv_summary = []
|
||||
for inv in invoices:
|
||||
inv_summary.append({
|
||||
'number': inv.name or '',
|
||||
'portion': inv.x_fc_adp_invoice_portion or 'full',
|
||||
'amount': float(inv.amount_total),
|
||||
'paid': inv.payment_state in ('paid', 'in_payment'),
|
||||
'date': str(inv.invoice_date) if inv.invoice_date else '',
|
||||
})
|
||||
|
||||
], limit=20):
|
||||
orders.append({
|
||||
'name': o.name,
|
||||
'sale_type': o.x_fc_sale_type,
|
||||
@@ -143,19 +68,11 @@ class AIAgentFusionClaims(models.Model):
|
||||
'client_total': float(o.x_fc_client_portion_total),
|
||||
'total': float(o.amount_total),
|
||||
'date': str(o.date_order.date()) if o.date_order else '',
|
||||
'billing_date': str(o.x_fc_billing_date) if o.x_fc_billing_date else '',
|
||||
'previous_funding_date': str(o.x_fc_previous_funding_date) if o.x_fc_previous_funding_date else '',
|
||||
'funding_warning': o.x_fc_funding_warning_message or '',
|
||||
'funding_warning_level': o.x_fc_funding_warning_level or '',
|
||||
'invoices': inv_summary,
|
||||
})
|
||||
|
||||
# Get applications
|
||||
apps = []
|
||||
for a in profile.application_data_ids[:10]:
|
||||
funded_devices = [
|
||||
label for field, label in PREV_FUNDED_FIELDS.items()
|
||||
if getattr(a, field, False)
|
||||
]
|
||||
apps.append({
|
||||
'date': str(a.application_date) if a.application_date else '',
|
||||
'device': a.base_device or '',
|
||||
@@ -163,17 +80,8 @@ class AIAgentFusionClaims(models.Model):
|
||||
'reason': a.reason_for_application or '',
|
||||
'condition': (a.medical_condition or '')[:100],
|
||||
'authorizer': f'{a.authorizer_first_name or ""} {a.authorizer_last_name or ""}'.strip(),
|
||||
'previously_funded': funded_devices if funded_devices else ['None'],
|
||||
})
|
||||
|
||||
ai_summary = ''
|
||||
ai_risk = ''
|
||||
try:
|
||||
ai_summary = profile.ai_summary or ''
|
||||
ai_risk = profile.ai_risk_flags or ''
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return json.dumps({
|
||||
'profile': {
|
||||
'id': profile.id,
|
||||
@@ -198,18 +106,12 @@ class AIAgentFusionClaims(models.Model):
|
||||
'total_adp': float(profile.total_adp_funded),
|
||||
'total_client': float(profile.total_client_portion),
|
||||
'total_amount': float(profile.total_amount),
|
||||
'applications_count': profile.application_count,
|
||||
'last_assessment': str(profile.last_assessment_date) if profile.last_assessment_date else '',
|
||||
'ai_summary': ai_summary,
|
||||
'ai_risk_flags': ai_risk,
|
||||
},
|
||||
'orders': orders,
|
||||
'applications': apps,
|
||||
})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tool 3: Get Aggregated Stats (migrated from read_group)
|
||||
# ------------------------------------------------------------------
|
||||
def _fc_tool_claims_stats(self):
|
||||
"""AI Tool: Get aggregated claims statistics."""
|
||||
SO = self.env['sale.order'].sudo()
|
||||
@@ -218,46 +120,40 @@ class AIAgentFusionClaims(models.Model):
|
||||
total_profiles = Profile.search_count([])
|
||||
total_orders = SO.search_count([('x_fc_sale_type', '!=', False)])
|
||||
|
||||
# By sale type
|
||||
type_data = SO.read_group(
|
||||
[('x_fc_sale_type', '!=', False)],
|
||||
['x_fc_sale_type', 'amount_total:sum'],
|
||||
['x_fc_sale_type'],
|
||||
)
|
||||
by_type = {}
|
||||
try:
|
||||
type_results = SO._read_group(
|
||||
[('x_fc_sale_type', '!=', False)],
|
||||
groupby=['x_fc_sale_type'],
|
||||
aggregates=['__count', 'amount_total:sum'],
|
||||
)
|
||||
for sale_type, count, total_amount in type_results:
|
||||
by_type[sale_type or 'unknown'] = {
|
||||
'count': count,
|
||||
'total': float(total_amount or 0),
|
||||
}
|
||||
except Exception as e:
|
||||
_logger.warning('Stats by_type failed: %s', e)
|
||||
for r in type_data:
|
||||
by_type[r['x_fc_sale_type']] = {
|
||||
'count': r['x_fc_sale_type_count'],
|
||||
'total': float(r['amount_total'] or 0),
|
||||
}
|
||||
|
||||
# By status
|
||||
status_data = SO.read_group(
|
||||
[('x_fc_sale_type', '!=', False), ('x_fc_adp_application_status', '!=', False)],
|
||||
['x_fc_adp_application_status'],
|
||||
['x_fc_adp_application_status'],
|
||||
)
|
||||
by_status = {}
|
||||
try:
|
||||
status_results = SO._read_group(
|
||||
[('x_fc_sale_type', '!=', False), ('x_fc_adp_application_status', '!=', False)],
|
||||
groupby=['x_fc_adp_application_status'],
|
||||
aggregates=['__count'],
|
||||
)
|
||||
for status, count in status_results:
|
||||
by_status[status or 'unknown'] = count
|
||||
except Exception as e:
|
||||
_logger.warning('Stats by_status failed: %s', e)
|
||||
for r in status_data:
|
||||
by_status[r['x_fc_adp_application_status']] = r['x_fc_adp_application_status_count']
|
||||
|
||||
# By city (top 10)
|
||||
city_data = Profile.read_group(
|
||||
[('city', '!=', False)],
|
||||
['city'],
|
||||
['city'],
|
||||
limit=10,
|
||||
orderby='city_count desc',
|
||||
)
|
||||
by_city = {}
|
||||
try:
|
||||
city_results = Profile._read_group(
|
||||
[('city', '!=', False)],
|
||||
groupby=['city'],
|
||||
aggregates=['__count'],
|
||||
limit=10,
|
||||
order='__count desc',
|
||||
)
|
||||
for city, count in city_results:
|
||||
by_city[city or 'unknown'] = count
|
||||
except Exception as e:
|
||||
_logger.warning('Stats by_city failed: %s', e)
|
||||
for r in city_data:
|
||||
by_city[r['city']] = r['city_count']
|
||||
|
||||
return json.dumps({
|
||||
'total_profiles': total_profiles,
|
||||
@@ -266,405 +162,3 @@ class AIAgentFusionClaims(models.Model):
|
||||
'by_status': by_status,
|
||||
'top_cities': by_city,
|
||||
})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tool 4: Client Status Lookup (by name, not order number)
|
||||
# ------------------------------------------------------------------
|
||||
def _fc_tool_client_status(self, client_name):
|
||||
"""AI Tool: Look up a client's complete status by name."""
|
||||
if not client_name or not client_name.strip():
|
||||
return json.dumps({'error': 'Please provide a client name to search for'})
|
||||
|
||||
client_name = client_name.strip()
|
||||
Profile = self.env['fusion.client.profile'].sudo()
|
||||
SO = self.env['sale.order'].sudo()
|
||||
Invoice = self.env['account.move'].sudo()
|
||||
|
||||
profiles = Profile.search([
|
||||
'|',
|
||||
('first_name', 'ilike', client_name),
|
||||
('last_name', 'ilike', client_name),
|
||||
], limit=5)
|
||||
|
||||
if not profiles:
|
||||
partners = self.env['res.partner'].sudo().search([
|
||||
('name', 'ilike', client_name),
|
||||
], limit=5)
|
||||
if not partners:
|
||||
return json.dumps({'error': f'No client found matching "{client_name}"'})
|
||||
|
||||
results = []
|
||||
for partner in partners:
|
||||
orders = SO.search([
|
||||
('partner_id', '=', partner.id),
|
||||
('x_fc_sale_type', '!=', False),
|
||||
], order='date_order desc', limit=20)
|
||||
if not orders:
|
||||
continue
|
||||
results.append(self._build_client_status_result(
|
||||
partner_name=partner.name,
|
||||
partner_id=partner.id,
|
||||
profile=None,
|
||||
orders=orders,
|
||||
Invoice=Invoice,
|
||||
))
|
||||
if not results:
|
||||
return json.dumps({'error': f'No orders found for "{client_name}"'})
|
||||
return json.dumps({'clients': results})
|
||||
|
||||
results = []
|
||||
for profile in profiles:
|
||||
orders = SO.search([
|
||||
('partner_id', '=', profile.partner_id.id),
|
||||
('x_fc_sale_type', '!=', False),
|
||||
], order='date_order desc', limit=20) if profile.partner_id else SO
|
||||
results.append(self._build_client_status_result(
|
||||
partner_name=profile.display_name,
|
||||
partner_id=profile.partner_id.id if profile.partner_id else None,
|
||||
profile=profile,
|
||||
orders=orders if profile.partner_id else SO.browse(),
|
||||
Invoice=Invoice,
|
||||
))
|
||||
|
||||
return json.dumps({'clients': results})
|
||||
|
||||
def _build_client_status_result(self, partner_name, partner_id, profile, orders, Invoice):
|
||||
"""Build a complete status result for one client."""
|
||||
order_data = []
|
||||
for o in orders:
|
||||
invoices = Invoice.search([
|
||||
('x_fc_source_sale_order_id', '=', o.id),
|
||||
('move_type', '=', 'out_invoice'),
|
||||
])
|
||||
adp_inv = invoices.filtered(lambda i: i.x_fc_adp_invoice_portion == 'adp')
|
||||
client_inv = invoices.filtered(lambda i: i.x_fc_adp_invoice_portion == 'client')
|
||||
|
||||
docs = {
|
||||
'original_application': bool(o.x_fc_original_application),
|
||||
'signed_pages': bool(o.x_fc_signed_pages_11_12),
|
||||
'xml_file': bool(o.x_fc_xml_file),
|
||||
'proof_of_delivery': bool(o.x_fc_proof_of_delivery),
|
||||
}
|
||||
|
||||
status = o.x_fc_adp_application_status or ''
|
||||
next_step = STATUS_NEXT_STEPS.get(status, '')
|
||||
|
||||
order_data.append({
|
||||
'order': o.name,
|
||||
'sale_type': o.x_fc_sale_type,
|
||||
'status': status,
|
||||
'date': str(o.date_order.date()) if o.date_order else '',
|
||||
'total': float(o.amount_total),
|
||||
'adp_total': float(o.x_fc_adp_portion_total),
|
||||
'client_total': float(o.x_fc_client_portion_total),
|
||||
'billing_date': str(o.x_fc_billing_date) if o.x_fc_billing_date else '',
|
||||
'funding_warning': o.x_fc_funding_warning_message or '',
|
||||
'adp_invoice': {
|
||||
'number': adp_inv[0].name if adp_inv else '',
|
||||
'amount': float(adp_inv[0].amount_total) if adp_inv else 0,
|
||||
'paid': adp_inv[0].payment_state in ('paid', 'in_payment') if adp_inv else False,
|
||||
} if adp_inv else None,
|
||||
'client_invoice': {
|
||||
'number': client_inv[0].name if client_inv else '',
|
||||
'amount': float(client_inv[0].amount_total) if client_inv else 0,
|
||||
'paid': client_inv[0].payment_state in ('paid', 'in_payment') if client_inv else False,
|
||||
} if client_inv else None,
|
||||
'documents': docs,
|
||||
'next_step': next_step,
|
||||
})
|
||||
|
||||
result = {
|
||||
'name': partner_name,
|
||||
'partner_id': partner_id,
|
||||
'orders': order_data,
|
||||
'order_count': len(order_data),
|
||||
}
|
||||
|
||||
if profile:
|
||||
result['profile_id'] = profile.id
|
||||
result['health_card'] = profile.health_card_number or ''
|
||||
result['city'] = profile.city or ''
|
||||
result['condition'] = (profile.medical_condition or '')[:100]
|
||||
result['total_adp_funded'] = float(profile.total_adp_funded)
|
||||
result['total_client_funded'] = float(profile.total_client_portion)
|
||||
|
||||
return result
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tool 5: ADP Billing Period Summary
|
||||
# ------------------------------------------------------------------
|
||||
def _fc_tool_adp_billing_period(self, period=None):
|
||||
"""AI Tool: Get ADP billing summary for a posting period."""
|
||||
Mixin = self.env['fusion_claims.adp.posting.schedule.mixin']
|
||||
Invoice = self.env['account.move'].sudo()
|
||||
|
||||
today = date.today()
|
||||
frequency = Mixin._get_adp_posting_frequency()
|
||||
|
||||
if not period or period == 'current':
|
||||
posting_date = Mixin._get_current_posting_date(today)
|
||||
elif period == 'previous':
|
||||
current = Mixin._get_current_posting_date(today)
|
||||
posting_date = current - timedelta(days=frequency)
|
||||
elif period == 'next':
|
||||
posting_date = Mixin._get_next_posting_date(today)
|
||||
else:
|
||||
try:
|
||||
ref_date = date.fromisoformat(period)
|
||||
posting_date = Mixin._get_current_posting_date(ref_date)
|
||||
except (ValueError, TypeError):
|
||||
return json.dumps({'error': f'Invalid period: "{period}". Use "current", "previous", "next", or a date (YYYY-MM-DD).'})
|
||||
|
||||
period_start = posting_date
|
||||
period_end = posting_date + timedelta(days=frequency - 1)
|
||||
submission_deadline = Mixin._get_posting_week_wednesday(posting_date)
|
||||
expected_payment = Mixin._get_expected_payment_date(posting_date)
|
||||
|
||||
adp_invoices = Invoice.search([
|
||||
('x_fc_adp_invoice_portion', '=', 'adp'),
|
||||
('move_type', '=', 'out_invoice'),
|
||||
('invoice_date', '>=', str(period_start)),
|
||||
('invoice_date', '<=', str(period_end)),
|
||||
])
|
||||
|
||||
total_invoiced = sum(adp_invoices.mapped('amount_total'))
|
||||
total_paid = sum(adp_invoices.filtered(
|
||||
lambda i: i.payment_state in ('paid', 'in_payment')
|
||||
).mapped('amount_total'))
|
||||
total_unpaid = total_invoiced - total_paid
|
||||
|
||||
source_orders = adp_invoices.mapped('x_fc_source_sale_order_id')
|
||||
|
||||
invoice_details = []
|
||||
for inv in adp_invoices[:25]:
|
||||
so = inv.x_fc_source_sale_order_id
|
||||
invoice_details.append({
|
||||
'invoice': inv.name or '',
|
||||
'order': so.name if so else '',
|
||||
'client': inv.partner_id.name or '',
|
||||
'amount': float(inv.amount_total),
|
||||
'paid': inv.payment_state in ('paid', 'in_payment'),
|
||||
'date': str(inv.invoice_date) if inv.invoice_date else '',
|
||||
})
|
||||
|
||||
return json.dumps({
|
||||
'period': {
|
||||
'posting_date': str(posting_date),
|
||||
'start': str(period_start),
|
||||
'end': str(period_end),
|
||||
'submission_deadline': f'{submission_deadline.strftime("%A, %B %d, %Y")} 6:00 PM',
|
||||
'expected_payment_date': str(expected_payment),
|
||||
},
|
||||
'summary': {
|
||||
'total_invoices': len(adp_invoices),
|
||||
'total_invoiced': float(total_invoiced),
|
||||
'total_paid': float(total_paid),
|
||||
'total_unpaid': float(total_unpaid),
|
||||
'orders_billed': len(source_orders),
|
||||
},
|
||||
'invoices': invoice_details,
|
||||
})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tool 6: Demographics & Analytics
|
||||
# ------------------------------------------------------------------
|
||||
def _fc_tool_demographics(self, analysis_type=None, city_filter=None, sale_type_filter=None):
|
||||
"""AI Tool: Run demographic and analytical queries on client data."""
|
||||
cr = self.env.cr
|
||||
results = {}
|
||||
|
||||
if not analysis_type:
|
||||
analysis_type = 'full'
|
||||
|
||||
if analysis_type in ('full', 'age_groups'):
|
||||
cr.execute("""
|
||||
SELECT
|
||||
CASE
|
||||
WHEN age < 18 THEN 'Under 18'
|
||||
WHEN age BETWEEN 18 AND 30 THEN '18-30'
|
||||
WHEN age BETWEEN 31 AND 45 THEN '31-45'
|
||||
WHEN age BETWEEN 46 AND 60 THEN '46-60'
|
||||
WHEN age BETWEEN 61 AND 75 THEN '61-75'
|
||||
ELSE '75+'
|
||||
END AS age_group,
|
||||
COUNT(DISTINCT p.id) AS clients,
|
||||
COUNT(app.id) AS applications,
|
||||
ROUND(COUNT(app.id)::numeric / NULLIF(COUNT(DISTINCT p.id), 0), 2) AS avg_applications,
|
||||
COALESCE(ROUND(SUM(p.total_adp_funded) / NULLIF(COUNT(DISTINCT p.id), 0), 2), 0) AS avg_adp_funded,
|
||||
COALESCE(ROUND(SUM(p.total_amount) / NULLIF(COUNT(DISTINCT p.id), 0), 2), 0) AS avg_total
|
||||
FROM fusion_client_profile p
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT EXTRACT(YEAR FROM AGE(CURRENT_DATE, p.date_of_birth))::int AS age
|
||||
) a
|
||||
LEFT JOIN fusion_adp_application_data app ON app.profile_id = p.id
|
||||
WHERE p.date_of_birth IS NOT NULL
|
||||
GROUP BY age_group
|
||||
ORDER BY MIN(age)
|
||||
""")
|
||||
rows = cr.fetchall()
|
||||
results['age_groups'] = [
|
||||
{
|
||||
'age_group': r[0], 'clients': r[1], 'applications': r[2],
|
||||
'avg_applications': float(r[3]), 'avg_adp_funded': float(r[4]),
|
||||
'avg_total': float(r[5]),
|
||||
} for r in rows
|
||||
]
|
||||
|
||||
if analysis_type in ('full', 'devices_by_age'):
|
||||
cr.execute("""
|
||||
SELECT
|
||||
CASE
|
||||
WHEN age < 45 THEN 'Under 45'
|
||||
WHEN age BETWEEN 45 AND 60 THEN '45-60'
|
||||
WHEN age BETWEEN 61 AND 75 THEN '61-75'
|
||||
ELSE '75+'
|
||||
END AS age_group,
|
||||
app.base_device,
|
||||
COUNT(*) AS count
|
||||
FROM fusion_client_profile p
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT EXTRACT(YEAR FROM AGE(CURRENT_DATE, p.date_of_birth))::int AS age
|
||||
) a
|
||||
JOIN fusion_adp_application_data app ON app.profile_id = p.id
|
||||
WHERE p.date_of_birth IS NOT NULL
|
||||
AND app.base_device IS NOT NULL AND app.base_device != ''
|
||||
GROUP BY age_group, app.base_device
|
||||
ORDER BY age_group, count DESC
|
||||
""")
|
||||
rows = cr.fetchall()
|
||||
devices_by_age = {}
|
||||
for age_group, device, count in rows:
|
||||
if age_group not in devices_by_age:
|
||||
devices_by_age[age_group] = []
|
||||
label = BASE_DEVICE_LABELS.get(device, device)
|
||||
devices_by_age[age_group].append({'device': label, 'count': count})
|
||||
results['devices_by_age'] = devices_by_age
|
||||
|
||||
if analysis_type in ('full', 'city_demographics'):
|
||||
city_clause = ""
|
||||
params = []
|
||||
if city_filter:
|
||||
city_clause = "AND LOWER(p.city) = LOWER(%s)"
|
||||
params = [city_filter]
|
||||
|
||||
cr.execute(f"""
|
||||
SELECT
|
||||
p.city,
|
||||
COUNT(DISTINCT p.id) AS clients,
|
||||
COUNT(app.id) AS applications,
|
||||
ROUND(AVG(a.age), 1) AS avg_age,
|
||||
COALESCE(ROUND(SUM(p.total_adp_funded) / NULLIF(COUNT(DISTINCT p.id), 0), 2), 0) AS avg_adp_funded
|
||||
FROM fusion_client_profile p
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT EXTRACT(YEAR FROM AGE(CURRENT_DATE, p.date_of_birth))::int AS age
|
||||
) a
|
||||
LEFT JOIN fusion_adp_application_data app ON app.profile_id = p.id
|
||||
WHERE p.city IS NOT NULL AND p.city != ''
|
||||
AND p.date_of_birth IS NOT NULL
|
||||
{city_clause}
|
||||
GROUP BY p.city
|
||||
ORDER BY clients DESC
|
||||
LIMIT 15
|
||||
""", params)
|
||||
rows = cr.fetchall()
|
||||
results['city_demographics'] = [
|
||||
{
|
||||
'city': r[0], 'clients': r[1], 'applications': r[2],
|
||||
'avg_age': float(r[3]), 'avg_adp_funded': float(r[4]),
|
||||
} for r in rows
|
||||
]
|
||||
|
||||
if analysis_type in ('full', 'benefits'):
|
||||
cr.execute("""
|
||||
SELECT
|
||||
CASE
|
||||
WHEN p.benefit_type = 'odsp' THEN 'ODSP'
|
||||
WHEN p.benefit_type = 'owp' THEN 'Ontario Works'
|
||||
WHEN p.benefit_type = 'acsd' THEN 'ACSD'
|
||||
WHEN p.receives_social_assistance THEN 'Social Assistance (other)'
|
||||
ELSE 'Regular (no assistance)'
|
||||
END AS benefit_category,
|
||||
COUNT(DISTINCT p.id) AS clients,
|
||||
COUNT(app.id) AS applications,
|
||||
ROUND(AVG(a.age), 1) AS avg_age,
|
||||
COALESCE(ROUND(AVG(p.total_adp_funded), 2), 0) AS avg_adp_funded
|
||||
FROM fusion_client_profile p
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT EXTRACT(YEAR FROM AGE(CURRENT_DATE, p.date_of_birth))::int AS age
|
||||
) a
|
||||
LEFT JOIN fusion_adp_application_data app ON app.profile_id = p.id
|
||||
WHERE p.date_of_birth IS NOT NULL
|
||||
GROUP BY benefit_category
|
||||
ORDER BY clients DESC
|
||||
""")
|
||||
rows = cr.fetchall()
|
||||
results['benefits_breakdown'] = [
|
||||
{
|
||||
'category': r[0], 'clients': r[1], 'applications': r[2],
|
||||
'avg_age': float(r[3]), 'avg_adp_funded': float(r[4]),
|
||||
} for r in rows
|
||||
]
|
||||
|
||||
if analysis_type in ('full', 'top_devices'):
|
||||
cr.execute("""
|
||||
SELECT
|
||||
app.base_device,
|
||||
COUNT(*) AS count,
|
||||
COUNT(DISTINCT app.profile_id) AS unique_clients,
|
||||
ROUND(AVG(a.age), 1) AS avg_age
|
||||
FROM fusion_adp_application_data app
|
||||
JOIN fusion_client_profile p ON p.id = app.profile_id
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT EXTRACT(YEAR FROM AGE(CURRENT_DATE, p.date_of_birth))::int AS age
|
||||
) a
|
||||
WHERE app.base_device IS NOT NULL AND app.base_device != '' AND app.base_device != 'none'
|
||||
AND p.date_of_birth IS NOT NULL
|
||||
GROUP BY app.base_device
|
||||
ORDER BY count DESC
|
||||
LIMIT 15
|
||||
""")
|
||||
rows = cr.fetchall()
|
||||
results['top_devices'] = [
|
||||
{
|
||||
'device': BASE_DEVICE_LABELS.get(r[0], r[0]),
|
||||
'device_code': r[0],
|
||||
'applications': r[1],
|
||||
'unique_clients': r[2],
|
||||
'avg_client_age': float(r[3]),
|
||||
} for r in rows
|
||||
]
|
||||
|
||||
if analysis_type in ('full', 'funding_summary'):
|
||||
cr.execute("""
|
||||
SELECT
|
||||
COUNT(*) AS total_profiles,
|
||||
ROUND(AVG(a.age), 1) AS avg_age,
|
||||
ROUND(SUM(p.total_adp_funded), 2) AS total_adp_funded,
|
||||
ROUND(SUM(p.total_client_portion), 2) AS total_client_portion,
|
||||
ROUND(SUM(p.total_amount), 2) AS grand_total,
|
||||
ROUND(AVG(p.total_adp_funded), 2) AS avg_adp_per_client,
|
||||
ROUND(AVG(p.total_client_portion), 2) AS avg_client_per_client,
|
||||
ROUND(AVG(p.claim_count), 2) AS avg_claims_per_client,
|
||||
MIN(a.age) AS youngest,
|
||||
MAX(a.age) AS oldest
|
||||
FROM fusion_client_profile p
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT EXTRACT(YEAR FROM AGE(CURRENT_DATE, p.date_of_birth))::int AS age
|
||||
) a
|
||||
WHERE p.date_of_birth IS NOT NULL
|
||||
""")
|
||||
r = cr.fetchone()
|
||||
results['funding_summary'] = {
|
||||
'total_profiles': r[0],
|
||||
'avg_age': float(r[1]),
|
||||
'total_adp_funded': float(r[2]),
|
||||
'total_client_portion': float(r[3]),
|
||||
'grand_total': float(r[4]),
|
||||
'avg_adp_per_client': float(r[5]),
|
||||
'avg_client_per_client': float(r[6]),
|
||||
'avg_claims_per_client': float(r[7]),
|
||||
'youngest_client_age': r[8],
|
||||
'oldest_client_age': r[9],
|
||||
}
|
||||
|
||||
return json.dumps(results)
|
||||
|
||||
Reference in New Issue
Block a user