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>
136 lines
4.7 KiB
Python
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',
|
|
}
|