Files
famlaw/activeblue_familylaw/wizard/fl_analysis_wizard.py
Carlos Garcia 26f58952b4 Phase 7: full wizards, auto-generated case tasks, config parameters
fl_intake_wizard.py:
  - Full multi-step intake: parties, income, children, DV flag, fee
    waiver, AI analysis option
  - Creates res.partner → fl.party → fl.case chain (mirrors portal)
  - Triggers fee waiver record creation and Ollama AI analysis
  - Residency warning computed field (FL 61.021 — 6-month check)

fl_generate_packet_wizard.py:
  - Generates selected documents as PDFs via _render_qweb_pdf
  - Handles 4 binding models: fl.case, fl.party, fl.fee.waiver,
    fl.support.calculation, fl.income.withholding
  - Attaches generated PDFs to case chatter with summary note
  - Bound to fl.case form as an action button

fl_analysis_wizard.py:
  - Checks for recent analysis (<24h) before running new one
  - force_reanalysis flag bypasses the lock
  - Shows last analysis age label in form; opens result as dialog
  - Bound to fl.case form as an action button

fl_case.py:
  - _CASE_TASK_TEMPLATES: standard task lists for 6 case types
  - _generate_case_tasks(): creates project.task records from templates
  - Called automatically from _create_case_project on case creation

fl_wizard_views.xml:
  - Form views for all 3 wizards with inline help text
  - Packet wizard bound to fl.case form via binding_model_id

data/case_task_templates.xml:
  - ir.config_parameter records for Ollama URL, model, deadline days,
    mandatory disclosure days, AI lockout hours — all admin-configurable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 23:49:10 -05:00

136 lines
4.7 KiB
Python

import logging
from datetime import datetime, timedelta
from odoo import api, fields, models, _
_logger = logging.getLogger(__name__)
class FlAnalysisWizard(models.TransientModel):
"""
Phase 7 — Full AI analysis trigger wizard.
Checks for a recent analysis (<24 h) before running a new one unless
force_reanalysis is True. Opens the resulting fl.analysis form on success.
"""
_name = 'fl.analysis.wizard'
_description = 'Trigger AI Case Analysis'
case_id = fields.Many2one(
'fl.case', string='Case', required=True,
default=lambda self: self.env.context.get('active_id'),
)
force_reanalysis = fields.Boolean(
string='Force Re-analysis',
help='Run a new analysis even if one was completed in the last 24 hours'
)
# Computed info fields shown in the form
recent_analysis_id = fields.Many2one(
'fl.analysis', string='Most Recent Analysis',
compute='_compute_recent_analysis',
)
recent_analysis_age_label = fields.Char(
string='Last Analysis', compute='_compute_recent_analysis'
)
has_recent = fields.Boolean(compute='_compute_recent_analysis')
@api.depends('case_id')
def _compute_recent_analysis(self):
for rec in self:
if not rec.case_id:
rec.recent_analysis_id = False
rec.recent_analysis_age_label = ''
rec.has_recent = False
continue
latest = self.env['fl.analysis'].search(
[('case_id', '=', rec.case_id.id), ('state', '=', 'complete')],
order='create_date desc',
limit=1,
)
rec.recent_analysis_id = latest
if latest:
age = datetime.now() - latest.create_date
hours = int(age.total_seconds() // 3600)
minutes = int((age.total_seconds() % 3600) // 60)
if hours == 0:
rec.recent_analysis_age_label = f'{minutes} minute(s) ago'
elif hours < 24:
rec.recent_analysis_age_label = f'{hours} hour(s) ago'
else:
days = age.days
rec.recent_analysis_age_label = f'{days} day(s) ago'
rec.has_recent = age < timedelta(hours=24)
else:
rec.recent_analysis_age_label = 'No analysis yet'
rec.has_recent = False
def action_run_analysis(self):
"""
Run the AI analysis pipeline via fl.ai.engine.
If a recent analysis exists and force_reanalysis is False,
just open the existing one.
"""
self.ensure_one()
if self.has_recent and not self.force_reanalysis:
# Surface the existing analysis instead
return {
'type': 'ir.actions.act_window',
'name': _('AI Analysis'),
'res_model': 'fl.analysis',
'res_id': self.recent_analysis_id.id,
'view_mode': 'form',
'target': 'new',
}
# Previous analyses remain in history — new one will appear first
# (fl.analysis is ordered by create_date desc)
# Post start note
self.case_id.message_post(
body='🤖 AI analysis started by %s' % self.env.user.name,
subtype_xmlid='mail.mt_note',
)
try:
analysis = self.env['fl.ai.engine'].analyze_case(self.case_id.id)
except Exception as e:
_logger.error('Analysis wizard: engine error for case %s: %s', self.case_id.id, e)
self.case_id.message_post(
body=f'❌ AI analysis failed: {e}',
subtype_xmlid='mail.mt_note',
)
return {
'type': 'ir.actions.act_window',
'name': _('Case'),
'res_model': 'fl.case',
'res_id': self.case_id.id,
'view_mode': 'form',
'target': 'current',
}
return {
'type': 'ir.actions.act_window',
'name': _('AI Analysis Result'),
'res_model': 'fl.analysis',
'res_id': analysis.id,
'view_mode': 'form',
'target': 'new',
}
def action_view_recent(self):
"""Open the most recent analysis form view."""
self.ensure_one()
if not self.recent_analysis_id:
return False
return {
'type': 'ir.actions.act_window',
'name': _('AI Analysis'),
'res_model': 'fl.analysis',
'res_id': self.recent_analysis_id.id,
'view_mode': 'form',
'target': 'new',
}