Add complexity-driven discovery suggestion wizard

fl_discovery_suggest_wizard.py:
  - fl.discovery.suggest.wizard: reads case complexity (AI analysis or
    rule-based fallback), case type, issue_tag_ids, and flags
    (domestic_violence_flag, respondent_has_counsel, income_imputation_concern)
    to build a pre-checked list of relevant discovery items
  - fl.discovery.suggest.line: one row per suggested item with type,
    directed_to, description, rationale, trigger badge, and min complexity
  - 50+ templates across 10 trigger categories: base, modification,
    dissolution, paternity, alimony, custody, imputation (Barner v. Barner),
    self_employment, domestic_violence, respondent_counsel, complex_only
  - action_create_selected: creates fl.discovery records (draft) and posts
    a chatter summary with all created items; bound to fl.case form

fl_case.py:
  - Add issue_tag_ids Many2many(fl.issue.tag) — field referenced by AI
    engine rule-tagging but not previously declared on the model

fl_discovery_suggest_views.xml:
  - Wizard form: complexity badge, alert box explaining level, editable
    suggestion list with trigger/type/description/rationale columns
  - Action bound to fl.case form via binding_model_id
  - Inherits fl.case form to add issue_tag_ids widget to AI tab

ir.model.access.csv: access rows for both new wizard models

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Carlos Garcia
2026-05-07 00:50:07 -05:00
parent 26f58952b4
commit 928568374e
6 changed files with 1003 additions and 0 deletions

View File

@@ -53,6 +53,7 @@
'views/fl_fee_waiver_views.xml', 'views/fl_fee_waiver_views.xml',
'views/fl_statute_views.xml', 'views/fl_statute_views.xml',
'views/fl_wizard_views.xml', 'views/fl_wizard_views.xml',
'views/fl_discovery_suggest_views.xml',
'views/menu_views.xml', 'views/menu_views.xml',
# Phase 4 — QWeb PDF Reports # Phase 4 — QWeb PDF Reports
'report/report_financial_affidavit_short.xml', 'report/report_financial_affidavit_short.xml',

View File

@@ -389,6 +389,12 @@ class FlCase(models.Model):
# CASE LAW & AI ANALYSIS # CASE LAW & AI ANALYSIS
# ══════════════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════════════
issue_tag_ids = fields.Many2many(
'fl.issue.tag',
'fl_case_issue_tag_rel', 'case_id', 'tag_id',
string='Issue Tags',
help='Set automatically by AI rule-tagging engine; can also be set manually'
)
caselaw_ids = fields.Many2many( caselaw_ids = fields.Many2many(
'fl.caselaw', string='Applicable Case Law' 'fl.caselaw', string='Applicable Case Law'
) )

View File

@@ -82,3 +82,9 @@ access_fl_analysis_wizard_admin,fl.analysis.wizard admin,model_fl_analysis_wizar
# ── fl.generate.packet.wizard ──────────────────────────────────────────────── # ── fl.generate.packet.wizard ────────────────────────────────────────────────
access_fl_generate_packet_wizard_admin,fl.generate.packet.wizard admin,model_fl_generate_packet_wizard,group_admin,1,1,1,1 access_fl_generate_packet_wizard_admin,fl.generate.packet.wizard admin,model_fl_generate_packet_wizard,group_admin,1,1,1,1
access_fl_generate_packet_wizard_paralegal,fl.generate.packet.wizard paralegal,model_fl_generate_packet_wizard,group_paralegal,1,1,1,1 access_fl_generate_packet_wizard_paralegal,fl.generate.packet.wizard paralegal,model_fl_generate_packet_wizard,group_paralegal,1,1,1,1
# ── fl.discovery.suggest.wizard ──────────────────────────────────────────────
access_fl_discovery_suggest_wizard_admin,fl.discovery.suggest.wizard admin,model_fl_discovery_suggest_wizard,group_admin,1,1,1,1
access_fl_discovery_suggest_wizard_paralegal,fl.discovery.suggest.wizard paralegal,model_fl_discovery_suggest_wizard,group_paralegal,1,1,1,1
# ── fl.discovery.suggest.line ────────────────────────────────────────────────
access_fl_discovery_suggest_line_admin,fl.discovery.suggest.line admin,model_fl_discovery_suggest_line,group_admin,1,1,1,1
access_fl_discovery_suggest_line_paralegal,fl.discovery.suggest.line paralegal,model_fl_discovery_suggest_line,group_paralegal,1,1,1,1
1 id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
82 # ── fl.generate.packet.wizard ────────────────────────────────────────────────
83 access_fl_generate_packet_wizard_admin,fl.generate.packet.wizard admin,model_fl_generate_packet_wizard,group_admin,1,1,1,1
84 access_fl_generate_packet_wizard_paralegal,fl.generate.packet.wizard paralegal,model_fl_generate_packet_wizard,group_paralegal,1,1,1,1
85 # ── fl.discovery.suggest.wizard ──────────────────────────────────────────────
86 access_fl_discovery_suggest_wizard_admin,fl.discovery.suggest.wizard admin,model_fl_discovery_suggest_wizard,group_admin,1,1,1,1
87 access_fl_discovery_suggest_wizard_paralegal,fl.discovery.suggest.wizard paralegal,model_fl_discovery_suggest_wizard,group_paralegal,1,1,1,1
88 # ── fl.discovery.suggest.line ────────────────────────────────────────────────
89 access_fl_discovery_suggest_line_admin,fl.discovery.suggest.line admin,model_fl_discovery_suggest_line,group_admin,1,1,1,1
90 access_fl_discovery_suggest_line_paralegal,fl.discovery.suggest.line paralegal,model_fl_discovery_suggest_line,group_paralegal,1,1,1,1

View File

@@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Discovery Suggestion Wizard views
Opened from the fl.case form (action button) or Cases menu.
Reads case complexity, issue tags, and flags to pre-populate a
checklist of relevant fl.discovery items for review.
-->
<odoo>
<data>
<!-- ══════════════════════════════════════════════════════════════
Wizard form view
══════════════════════════════════════════════════════════════ -->
<record id="view_fl_discovery_suggest_wizard_form" model="ir.ui.view">
<field name="name">fl.discovery.suggest.wizard.form</field>
<field name="model">fl.discovery.suggest.wizard</field>
<field name="arch" type="xml">
<form string="Discovery Suggestions">
<sheet>
<div class="oe_title">
<h1>Suggested Discovery Items</h1>
</div>
<!-- Case + Complexity header -->
<group>
<group>
<field name="case_id" readonly="1"/>
</group>
<group>
<field name="complexity" readonly="1" widget="badge"
decoration-success="complexity == 'simple'"
decoration-warning="complexity == 'moderate'"
decoration-danger="complexity == 'complex'"/>
<field name="complexity_source" readonly="1" string="Source"/>
<field name="selected_count" readonly="1" string="Items Selected"/>
</group>
</group>
<!-- Complexity explanation -->
<div class="alert alert-success mb-0"
invisible="complexity != 'simple'">
<strong>Simple case</strong> — core financial disclosure items only.
Run an AI analysis to check for additional complexity factors.
</div>
<div class="alert alert-warning mb-0"
invisible="complexity != 'moderate'">
<strong>Moderate complexity</strong> — standard income discovery +
targeted items based on case flags.
Items marked "Moderate" minimum are included.
</div>
<div class="alert alert-danger mb-0"
invisible="complexity != 'complex'">
<strong>Complex case</strong> — full discovery suite including employer
subpoenas, bank subpoenas, and deposition.
</div>
<div class="alert alert-info mt-2">
<strong>Review and deselect</strong> any items that don't apply.
Click <em>Create Selected</em> to add them to the case as draft
discovery items. Items are not served until you mark them "Served."
</div>
<!-- Suggestion lines -->
<field name="line_ids" nolabel="1">
<list editable="bottom" decoration-muted="not selected">
<field name="selected" widget="boolean_toggle" optional="show"/>
<field name="trigger_label" string="Trigger"
widget="badge" optional="show"/>
<field name="discovery_type" optional="show"/>
<field name="directed_to" optional="show"/>
<field name="description"/>
<field name="rationale" optional="hide"/>
<field name="complexity_label" string="Min. Level"
optional="hide"/>
<field name="sequence" optional="hide"/>
</list>
</field>
</sheet>
<footer>
<button name="action_create_selected"
string="Create Selected Discovery Items"
type="object" class="btn-primary"
invisible="selected_count == 0"/>
<button name="action_populate"
string="Re-generate Suggestions"
type="object" class="btn-secondary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- ══════════════════════════════════════════════════════════════
Action — opened from fl.case form
══════════════════════════════════════════════════════════════ -->
<record id="action_fl_discovery_suggest_wizard" model="ir.actions.act_window">
<field name="name">Suggest Discovery Items</field>
<field name="res_model">fl.discovery.suggest.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="binding_model_id" ref="model_fl_case"/>
<field name="binding_view_types">form</field>
</record>
<!-- ══════════════════════════════════════════════════════════════
Add issue_tag_ids widget to fl.case form — AI tab
══════════════════════════════════════════════════════════════ -->
<record id="view_fl_case_issue_tags_inherit" model="ir.ui.view">
<field name="name">fl.case.form.issue.tags</field>
<field name="model">fl.case</field>
<field name="inherit_id" ref="activeblue_familylaw.view_fl_case_form"/>
<field name="arch" type="xml">
<!-- Add issue_tag_ids to the AI Analysis tab -->
<xpath expr="//page[@name='ai']//group[1]" position="after">
<group>
<field name="issue_tag_ids" widget="many2many_tags"
options="{'color_field': 'color'}"
string="Issue Tags (auto-tagged by AI)"/>
</group>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@@ -1,3 +1,4 @@
from . import fl_intake_wizard from . import fl_intake_wizard
from . import fl_analysis_wizard from . import fl_analysis_wizard
from . import fl_generate_packet_wizard from . import fl_generate_packet_wizard
from . import fl_discovery_suggest_wizard

View File

@@ -0,0 +1,860 @@
"""
fl.discovery.suggest.wizard — Complexity-driven discovery suggestions.
Opens from the fl.case form. Reads:
- Case complexity (from latest AI analysis, or rule-based fallback)
- Case type (modification, dissolution, paternity, etc.)
- Active issue tags (income_imputation, self_employment_income, etc.)
- Case flags (domestic_violence_flag, respondent_has_counsel, etc.)
Builds a pre-checked list of relevant fl.discovery items. User reviews,
deselects unwanted items, then clicks "Create Selected" to create
fl.discovery records in draft state.
"""
import logging
from odoo import api, fields, models, _
_logger = logging.getLogger(__name__)
# ── Discovery template data ───────────────────────────────────────────────────
#
# Each entry is a dict:
# discovery_type : 'interrogatories' | 'production' | 'admissions' |
# 'subpoena' | 'deposition'
# directed_to : 'petitioner' | 'respondent' | 'third_party'
# description : Short description shown in the wizard line
# rationale : Why this item is suggested (shown in tooltip/column)
# min_complexity : 'simple' | 'moderate' | 'complex' (minimum to show)
# trigger : Key matching one of the _TRIGGERS below; used for badge
#
# Trigger keys:
# 'base' — always suggested for all cases with financial issues
# 'modification' — case_type = 'modification'
# 'dissolution' — case_type in dissolution_children / dissolution_no_children
# 'paternity' — case_type = 'paternity'
# 'alimony' — case_type = 'alimony_modification'
# 'custody' — case_type = 'custody_modification'
# 'imputation' — income_imputation_concern flag OR imputation tag
# 'self_employment' — self_employment_income tag on analysis
# 'domestic_violence' — domestic_violence_flag
# 'respondent_counsel'— respondent_has_counsel flag
# 'property' — dissolution + assets involved (dissolution_children or dissolution_no_children)
# 'complex_only' — only at 'complex' complexity
_TEMPLATES = [
# ── Base: every case with financial issues ────────────────────────────────
{
'trigger': 'base',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'Current employment — employer name, position, start date, and gross salary',
'rationale': 'Establish current income for FL 61.30 calculation',
'min_complexity': 'simple',
},
{
'trigger': 'base',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'All sources of income — wages, self-employment, rental, investment, alimony received',
'rationale': 'FL 61.30(2)(a): all income sources are includable; prevents underdisclosure',
'min_complexity': 'simple',
},
{
'trigger': 'base',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Federal income tax returns — last 3 years, all pages and schedules',
'rationale': 'FL 12.285 mandatory disclosure; primary income verification document',
'min_complexity': 'simple',
},
{
'trigger': 'base',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Pay stubs — most recent 6 months from all employers',
'rationale': 'FL 12.285 mandatory disclosure; confirms current gross income',
'min_complexity': 'simple',
},
{
'trigger': 'base',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'W-2s and 1099s — last 3 years from all sources',
'rationale': 'Cross-check against tax returns; identifies all income sources',
'min_complexity': 'simple',
},
{
'trigger': 'base',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Bank statements — all accounts, last 6 months',
'rationale': 'Verify income deposits and identify unreported cash income',
'min_complexity': 'simple',
},
{
'trigger': 'base',
'discovery_type': 'admissions',
'directed_to': 'respondent',
'description': 'Admit that respondent is currently employed and earning income',
'rationale': 'FL 1.370: establishes baseline income fact; deemed admitted if ignored',
'min_complexity': 'simple',
},
# ── Modification cases ────────────────────────────────────────────────────
{
'trigger': 'modification',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'Changes in employment or income since the date of the existing order',
'rationale': 'FL 61.30(1)(b): modification requires showing substantial change in circumstances',
'min_complexity': 'simple',
},
{
'trigger': 'modification',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'Any bonuses, commissions, overtime received in last 24 months',
'rationale': 'FL 61.30(2)(a)(3): bonuses and commissions are gross income',
'min_complexity': 'simple',
},
{
'trigger': 'modification',
'discovery_type': 'admissions',
'directed_to': 'respondent',
'description': 'Admit that a substantial change in circumstances has occurred since the last order',
'rationale': 'Establishes the modification threshold prerequisite (FL 61.30(1)(b))',
'min_complexity': 'moderate',
},
# ── Dissolution (with or without children) ────────────────────────────────
{
'trigger': 'dissolution',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'All real property owned — address, assessed value, mortgage balance, title holder',
'rationale': 'FL 61.075: marital assets subject to equitable distribution',
'min_complexity': 'simple',
},
{
'trigger': 'dissolution',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Mortgage statements — all real property, last 12 months',
'rationale': 'Establish equity in marital real estate',
'min_complexity': 'simple',
},
{
'trigger': 'dissolution',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Retirement account statements — 401(k), IRA, pension — last 3 years',
'rationale': 'FL 61.075: retirement accounts accrued during marriage are marital assets',
'min_complexity': 'simple',
},
{
'trigger': 'dissolution',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Investment and brokerage account statements — last 3 years',
'rationale': 'Identify marital investment assets for equitable distribution',
'min_complexity': 'simple',
},
{
'trigger': 'dissolution',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'All marital debts — creditor, balance, monthly payment, whose name',
'rationale': 'FL 61.075: marital liabilities are also subject to equitable distribution',
'min_complexity': 'simple',
},
{
'trigger': 'dissolution',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Life insurance policies with cash surrender value',
'rationale': 'Cash value life insurance is a marital asset subject to distribution',
'min_complexity': 'moderate',
},
{
'trigger': 'dissolution',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'All vehicles, boats, and recreational vehicles — make, model, year, value, loan balance',
'rationale': 'Personal property with significant value is subject to equitable distribution',
'min_complexity': 'moderate',
},
# ── Paternity cases ───────────────────────────────────────────────────────
{
'trigger': 'paternity',
'discovery_type': 'admissions',
'directed_to': 'respondent',
'description': 'Admit that respondent is the biological father of the minor child(ren)',
'rationale': 'Establishes paternity as a fact without requiring DNA testing if admitted',
'min_complexity': 'simple',
},
{
'trigger': 'paternity',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'Respondent\'s relationship with the child(ren) — frequency and nature of contact',
'rationale': 'Relevant to timesharing and parental responsibility determination',
'min_complexity': 'simple',
},
{
'trigger': 'paternity',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Any prior acknowledgment of paternity (written, verbal communications)',
'rationale': 'Prior acknowledgments may establish paternity without court testing',
'min_complexity': 'moderate',
},
# ── Alimony modification ──────────────────────────────────────────────────
{
'trigger': 'alimony',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'Current financial status — income, assets, liabilities',
'rationale': 'FL 61.14: modification requires showing substantial change; need current financials',
'min_complexity': 'simple',
},
{
'trigger': 'alimony',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'Any cohabitation or remarriage since entry of alimony order',
'rationale': 'FL 61.14(1)(b): cohabitation can reduce or terminate alimony',
'min_complexity': 'simple',
},
{
'trigger': 'alimony',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Documentation of cohabitation — lease agreements, utility bills, mail',
'rationale': 'Evidence of cohabitation as grounds for alimony modification',
'min_complexity': 'moderate',
},
# ── Custody modification ──────────────────────────────────────────────────
{
'trigger': 'custody',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'Changes in respondent\'s living situation since last custody order',
'rationale': 'Substantial change in circumstances affecting child\'s welfare required (FL 61.13)',
'min_complexity': 'simple',
},
{
'trigger': 'custody',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Child\'s school records — attendance, grades, disciplinary records',
'rationale': 'Academic performance reflects whether current timesharing serves the child',
'min_complexity': 'simple',
},
{
'trigger': 'custody',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Child\'s medical and counseling records (last 2 years)',
'rationale': 'Medical/mental health records may reflect parenting issues',
'min_complexity': 'moderate',
},
# ── Income imputation concern ─────────────────────────────────────────────
{
'trigger': 'imputation',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'Prior employment history — all employers, positions, and salaries for last 5 years',
'rationale': (
'Barner v. Barner (4th DCA 2006): income may be imputed based on '
'prior earning history and capacity'
),
'min_complexity': 'simple',
},
{
'trigger': 'imputation',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'All education, degrees, professional licenses, and certifications',
'rationale': 'FL 61.30(2)(b): imputed income based on earning capacity and qualifications',
'min_complexity': 'simple',
},
{
'trigger': 'imputation',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'Job search efforts — all applications submitted in last 12 months',
'rationale': 'If claiming unemployed, must show genuine effort to obtain employment',
'min_complexity': 'simple',
},
{
'trigger': 'imputation',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'Monthly living expenses — rent, utilities, food, transportation, recreation',
'rationale': (
'Barner v. Barner: lifestyle inconsistent with claimed income is evidence '
'of imputable income'
),
'min_complexity': 'simple',
},
{
'trigger': 'imputation',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Credit card statements — all cards, last 12 months',
'rationale': 'Spending patterns establish actual lifestyle standard (Barner v. Barner)',
'min_complexity': 'simple',
},
{
'trigger': 'imputation',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Loan applications — all applications last 3 years (state income reported)',
'rationale': 'Income stated on loan applications vs. claimed income discrepancy is powerful evidence',
'min_complexity': 'moderate',
},
{
'trigger': 'imputation',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Unemployment compensation records — claim forms, benefit amounts',
'rationale': 'Cross-check with claimed unemployment status',
'min_complexity': 'moderate',
},
{
'trigger': 'imputation',
'discovery_type': 'admissions',
'directed_to': 'respondent',
'description': 'Admit that respondent is voluntarily unemployed or underemployed',
'rationale': 'Admission avoids need to prove voluntary nature of unemployment at hearing',
'min_complexity': 'moderate',
},
{
'trigger': 'imputation',
'discovery_type': 'subpoena',
'directed_to': 'third_party',
'description': 'Subpoena to prior employers — employment dates, position, salary, reason for separation',
'rationale': 'Establishes earning history from third-party records that cannot be disputed',
'min_complexity': 'moderate',
},
# ── Self-employment income ────────────────────────────────────────────────
{
'trigger': 'self_employment',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'Business structure — entity type, ownership %, officer/member roles',
'rationale': 'FL 61.30(2)(a)(4): self-employment income = gross revenue minus allowable expenses',
'min_complexity': 'simple',
},
{
'trigger': 'self_employment',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'Business gross revenues and net income — last 3 years',
'rationale': 'Court uses net business income (after legitimate deductions) for support calculation',
'min_complexity': 'simple',
},
{
'trigger': 'self_employment',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'Business expenses claimed as deductions — itemized list with amounts',
'rationale': 'FL 61.30(2)(a)(4): only expenses "necessary" to produce income are deductible',
'min_complexity': 'simple',
},
{
'trigger': 'self_employment',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Business federal tax returns — last 3 years (Schedule C, 1120-S, K-1)',
'rationale': 'Primary document for self-employment income; check for non-recurring deductions',
'min_complexity': 'simple',
},
{
'trigger': 'self_employment',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Business bank account statements — all accounts, last 12 months',
'rationale': 'Revenue deposits reveal true income vs. reported income',
'min_complexity': 'simple',
},
{
'trigger': 'self_employment',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'All client contracts and invoices — last 12 months',
'rationale': 'Revenue documentation from contracts supports income calculation',
'min_complexity': 'moderate',
},
{
'trigger': 'self_employment',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Accounts receivable aging report and accounts payable summary',
'rationale': 'Unpaid invoices may reveal income deferred to avoid support obligation',
'min_complexity': 'moderate',
},
{
'trigger': 'self_employment',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'QuickBooks or accounting software export — last 2 years',
'rationale': 'Comprehensive business financial records; harder to manipulate than summary reports',
'min_complexity': 'moderate',
},
{
'trigger': 'self_employment',
'discovery_type': 'subpoena',
'directed_to': 'third_party',
'description': 'Subpoena to CPA / accountant — business financial records and communications',
'rationale': 'Third-party accountant records cannot be tailored; provides independent income picture',
'min_complexity': 'moderate',
},
{
'trigger': 'self_employment',
'discovery_type': 'deposition',
'directed_to': 'respondent',
'description': 'Deposition of respondent regarding business income, expenses, and financial management',
'rationale': 'Lock in testimony on income before hearing; identify inconsistencies',
'min_complexity': 'complex',
},
# ── Domestic violence ─────────────────────────────────────────────────────
{
'trigger': 'domestic_violence',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'All police reports, incident reports, and arrest records involving either party',
'rationale': 'FL 44.102: DV history affects mediation requirements and attorney referral',
'min_complexity': 'simple',
},
{
'trigger': 'domestic_violence',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Existing restraining orders, protective injunctions, or orders of protection',
'rationale': 'Active injunctions affect contact and timesharing arrangements',
'min_complexity': 'simple',
},
{
'trigger': 'domestic_violence',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Medical records for injuries attributed to domestic violence incidents',
'rationale': 'Medical records corroborate DV allegations and establish timeline',
'min_complexity': 'moderate',
},
{
'trigger': 'domestic_violence',
'discovery_type': 'admissions',
'directed_to': 'respondent',
'description': 'Admit that respondent has been subject to a domestic violence injunction',
'rationale': 'Establishes DV history without requiring full evidentiary hearing',
'min_complexity': 'moderate',
},
# ── Respondent has counsel (moderate+) ───────────────────────────────────
{
'trigger': 'respondent_counsel',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'All documents, communications, or evidence respondent intends to use at hearing',
'rationale': 'With counsel, respondent is likely preparing exhibits — pre-identify them via discovery',
'min_complexity': 'moderate',
},
{
'trigger': 'respondent_counsel',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'Identity of any expert witnesses respondent intends to call (FL 1.280(b)(4))',
'rationale': 'Expert disclosure required; income experts are common in modification cases',
'min_complexity': 'moderate',
},
# ── Complex: employer subpoena ────────────────────────────────────────────
{
'trigger': 'complex_only',
'discovery_type': 'subpoena',
'directed_to': 'third_party',
'description': 'Subpoena to respondent\'s employer — payroll records, W-2s, bonuses (last 3 years)',
'rationale': 'FL 1.351: direct employer records are the most reliable income evidence',
'min_complexity': 'moderate',
},
{
'trigger': 'complex_only',
'discovery_type': 'subpoena',
'directed_to': 'third_party',
'description': 'Subpoena to financial institution — respondent\'s deposit accounts (last 12 months)',
'rationale': 'Bank-certified records cannot be altered; reveals true income deposits',
'min_complexity': 'complex',
},
{
'trigger': 'complex_only',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'Cryptocurrency and digital asset wallet records — all transactions last 2 years',
'rationale': 'Complex cases often involve unreported crypto income or hidden assets',
'min_complexity': 'complex',
},
{
'trigger': 'complex_only',
'discovery_type': 'deposition',
'directed_to': 'respondent',
'description': 'Deposition of respondent — full financial picture, income history, and assets',
'rationale': 'FL 1.310: oral deposition locks in testimony; essential in complex income disputes',
'min_complexity': 'complex',
},
{
'trigger': 'complex_only',
'discovery_type': 'interrogatories',
'directed_to': 'respondent',
'description': 'All financial accounts — bank, brokerage, retirement, cryptocurrency — account numbers and institutions',
'rationale': 'Complex asset cases require full account disclosure for subpoena targeting',
'min_complexity': 'complex',
},
{
'trigger': 'complex_only',
'discovery_type': 'production',
'directed_to': 'respondent',
'description': 'IRS Form 4506-T authorization — respondent to authorize IRS tax transcript release',
'rationale': 'IRS transcripts cannot be falsified; definitive income verification',
'min_complexity': 'complex',
},
]
# Which triggers are active for a given case — maps trigger key to a
# function (case) -> bool. These are evaluated at wizard-open time.
_TRIGGER_LABELS = {
'base': 'All Cases',
'modification': 'Modification',
'dissolution': 'Dissolution',
'paternity': 'Paternity',
'alimony': 'Alimony Mod.',
'custody': 'Custody Mod.',
'imputation': 'Income Imputation',
'self_employment': 'Self-Employment',
'domestic_violence': 'Domestic Violence',
'respondent_counsel': 'Respondent Counsel',
'complex_only': 'Complex',
}
_COMPLEXITY_ORDER = {'simple': 0, 'moderate': 1, 'complex': 2}
class FlDiscoverySuggestLine(models.TransientModel):
_name = 'fl.discovery.suggest.line'
_description = 'Suggested Discovery Item'
_order = 'sequence asc'
wizard_id = fields.Many2one(
'fl.discovery.suggest.wizard', ondelete='cascade'
)
selected = fields.Boolean(string='Include', default=True)
sequence = fields.Integer(default=10)
discovery_type = fields.Selection([
('interrogatories', 'Interrogatories (FL 1.340)'),
('production', 'Request for Production (FL 1.350)'),
('admissions', 'Request for Admissions (FL 1.370)'),
('subpoena', 'Subpoena — Third Party (FL 1.351)'),
('deposition', 'Deposition Notice (FL 1.310)'),
], string='Type')
directed_to = fields.Selection([
('petitioner', 'Petitioner'),
('respondent', 'Respondent'),
('third_party', 'Third Party'),
], string='Directed To')
description = fields.Char(string='Description / Subject')
rationale = fields.Char(string='Basis / Why Suggested')
trigger_label = fields.Char(string='Trigger')
complexity_label = fields.Char(string='Min. Complexity')
class FlDiscoverySuggestWizard(models.TransientModel):
_name = 'fl.discovery.suggest.wizard'
_description = 'Discovery Suggestion Wizard'
case_id = fields.Many2one(
'fl.case', string='Case', required=True,
default=lambda self: self.env.context.get('active_id'),
)
complexity = fields.Selection([
('simple', 'Simple'),
('moderate', 'Moderate'),
('complex', 'Complex'),
], string='Case Complexity', compute='_compute_complexity', store=False)
complexity_source = fields.Char(
string='Complexity Source', compute='_compute_complexity', store=False
)
line_ids = fields.One2many(
'fl.discovery.suggest.line', 'wizard_id', string='Suggested Discovery'
)
selected_count = fields.Integer(
string='Selected', compute='_compute_selected_count'
)
@api.depends('case_id')
def _compute_complexity(self):
for rec in self:
if not rec.case_id:
rec.complexity = 'simple'
rec.complexity_source = ''
continue
rec.complexity, rec.complexity_source = rec._assess_complexity()
@api.depends('line_ids.selected')
def _compute_selected_count(self):
for rec in self:
rec.selected_count = sum(1 for l in rec.line_ids if l.selected)
# ── Complexity assessment ─────────────────────────────────────────────────
def _assess_complexity(self):
"""
Determine case complexity.
Priority: latest AI analysis → rule-based fallback.
Returns (complexity_str, source_label).
"""
case = self.case_id
# 1. Latest completed AI analysis
latest = self.env['fl.analysis'].search([
('case_id', '=', case.id),
('state', '=', 'complete'),
], order='create_date desc', limit=1)
if latest and latest.case_complexity:
c = latest.case_complexity
if c not in ('simple', 'moderate', 'complex'):
c = 'moderate'
return c, f'AI Analysis ({latest.create_date.strftime("%Y-%m-%d")})'
# 2. Rule-based fallback
score = 0
if case.domestic_violence_flag:
score += 3
if case.respondent_has_counsel:
score += 2
if case.income_imputation_concern:
score += 2
if case.case_type in ('dissolution_children', 'dissolution_no_children'):
score += 2
# Check issue tags on the case
tag_names = set(case.issue_tag_ids.mapped('name')) if case.issue_tag_ids else set()
if 'self_employment_income' in tag_names:
score += 3
if 'income_imputation' in tag_names:
score += 2
# Check respondent income gap
if (case.petitioner_id and case.respondent_id and
case.respondent_id.monthly_gross_income and
case.petitioner_id.monthly_gross_income):
ratio = max(
case.petitioner_id.monthly_gross_income,
case.respondent_id.monthly_gross_income
) / (min(
case.petitioner_id.monthly_gross_income,
case.respondent_id.monthly_gross_income
) or 1)
if ratio > 3:
score += 2
if score <= 2:
return 'simple', 'Rule-based estimate'
elif score <= 5:
return 'moderate', 'Rule-based estimate'
else:
return 'complex', 'Rule-based estimate'
# ── Trigger detection ─────────────────────────────────────────────────────
def _active_triggers(self):
"""Return set of trigger keys that are active for this case."""
case = self.case_id
active = {'base'}
# Case type
ct = case.case_type
if ct == 'modification':
active.add('modification')
elif ct in ('dissolution_children', 'dissolution_no_children'):
active.add('dissolution')
elif ct == 'paternity':
active.add('paternity')
elif ct == 'alimony_modification':
active.add('alimony')
elif ct == 'custody_modification':
active.add('custody')
# Case flags
if case.domestic_violence_flag:
active.add('domestic_violence')
if case.respondent_has_counsel:
active.add('respondent_counsel')
if case.income_imputation_concern:
active.add('imputation')
# Issue tags stored on the case (written by AI engine + rule tagging)
tag_names = set(case.issue_tag_ids.mapped('name')) if case.issue_tag_ids else set()
if 'income_imputation' in tag_names:
active.add('imputation')
if 'self_employment_income' in tag_names:
active.add('self_employment')
if 'domestic_violence' in tag_names:
active.add('domestic_violence')
# Complex always add complex_only if complexity is complex
if self.complexity == 'complex':
active.add('complex_only')
elif self.complexity == 'moderate':
# Subpoena-to-employer is useful at moderate too
active.add('complex_only')
return active
# ── Populate suggestions ──────────────────────────────────────────────────
@api.onchange('case_id')
def _onchange_case_id(self):
if not self.case_id:
self.line_ids = [(5, 0, 0)]
return
self._populate_suggestions()
def _populate_suggestions(self):
"""Build suggestion lines from templates based on case data."""
if not self.case_id:
return
# Force recompute of complexity
self._compute_complexity()
active_triggers = self._active_triggers()
complexity_val = _COMPLEXITY_ORDER.get(self.complexity, 0)
seen = set() # de-duplicate by (type, directed_to, description)
lines = []
seq = 10
for tmpl in _TEMPLATES:
trigger = tmpl['trigger']
if trigger not in active_triggers:
continue
min_c = _COMPLEXITY_ORDER.get(tmpl['min_complexity'], 0)
if min_c > complexity_val:
continue
key = (tmpl['discovery_type'], tmpl['directed_to'], tmpl['description'])
if key in seen:
continue
seen.add(key)
lines.append((0, 0, {
'selected': True,
'sequence': seq,
'discovery_type': tmpl['discovery_type'],
'directed_to': tmpl['directed_to'],
'description': tmpl['description'],
'rationale': tmpl['rationale'],
'trigger_label': _TRIGGER_LABELS.get(trigger, trigger),
'complexity_label': tmpl['min_complexity'].capitalize(),
}))
seq += 10
self.line_ids = [(5, 0, 0)] + lines
# ── Default: populate on creation ─────────────────────────────────────────
@api.model
def default_get(self, fields_list):
vals = super().default_get(fields_list)
return vals
def action_populate(self):
"""Manual re-populate button (after case_id is set)."""
self.ensure_one()
self._populate_suggestions()
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}
# ── Create selected discovery items ───────────────────────────────────────
def action_create_selected(self):
"""Create fl.discovery records for all selected lines."""
self.ensure_one()
selected = self.line_ids.filtered(lambda l: l.selected)
if not selected:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Nothing selected'),
'message': _('Select at least one discovery item to create.'),
'type': 'warning',
'sticky': False,
},
}
created = self.env['fl.discovery']
for line in selected:
created |= self.env['fl.discovery'].create({
'case_id': self.case_id.id,
'discovery_type': line.discovery_type,
'directed_to': line.directed_to,
'description': line.description,
'notes': f'Rationale: {line.rationale}',
'state': 'draft',
})
self.case_id.message_post(
body=(
'🔍 <b>{} Discovery Items Created</b> '
'(Complexity: {}{})<br/>'
'{}'
).format(
len(created),
self.complexity.capitalize(),
self.complexity_source,
'<br/>'.join(
'• [{}] {}'.format(
dict(self.env['fl.discovery']._fields['discovery_type'].selection)
.get(d.discovery_type, d.discovery_type),
d.description or ''
)
for d in created
),
),
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',
}