From 928568374ec18d2bcd03c5eff4b0e5fa653cee00 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Thu, 7 May 2026 00:50:07 -0500 Subject: [PATCH] Add complexity-driven discovery suggestion wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- activeblue_familylaw/__manifest__.py | 1 + activeblue_familylaw/models/fl_case.py | 6 + .../security/ir.model.access.csv | 6 + .../views/fl_discovery_suggest_views.xml | 129 +++ activeblue_familylaw/wizard/__init__.py | 1 + .../wizard/fl_discovery_suggest_wizard.py | 860 ++++++++++++++++++ 6 files changed, 1003 insertions(+) create mode 100644 activeblue_familylaw/views/fl_discovery_suggest_views.xml create mode 100644 activeblue_familylaw/wizard/fl_discovery_suggest_wizard.py diff --git a/activeblue_familylaw/__manifest__.py b/activeblue_familylaw/__manifest__.py index 4af9932..32a5aee 100644 --- a/activeblue_familylaw/__manifest__.py +++ b/activeblue_familylaw/__manifest__.py @@ -53,6 +53,7 @@ 'views/fl_fee_waiver_views.xml', 'views/fl_statute_views.xml', 'views/fl_wizard_views.xml', + 'views/fl_discovery_suggest_views.xml', 'views/menu_views.xml', # Phase 4 — QWeb PDF Reports 'report/report_financial_affidavit_short.xml', diff --git a/activeblue_familylaw/models/fl_case.py b/activeblue_familylaw/models/fl_case.py index 611819b..0b214ce 100644 --- a/activeblue_familylaw/models/fl_case.py +++ b/activeblue_familylaw/models/fl_case.py @@ -389,6 +389,12 @@ class FlCase(models.Model): # 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( 'fl.caselaw', string='Applicable Case Law' ) diff --git a/activeblue_familylaw/security/ir.model.access.csv b/activeblue_familylaw/security/ir.model.access.csv index 78ed8cd..4a89ecd 100644 --- a/activeblue_familylaw/security/ir.model.access.csv +++ b/activeblue_familylaw/security/ir.model.access.csv @@ -82,3 +82,9 @@ access_fl_analysis_wizard_admin,fl.analysis.wizard admin,model_fl_analysis_wizar # ── 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_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 diff --git a/activeblue_familylaw/views/fl_discovery_suggest_views.xml b/activeblue_familylaw/views/fl_discovery_suggest_views.xml new file mode 100644 index 0000000..d01050c --- /dev/null +++ b/activeblue_familylaw/views/fl_discovery_suggest_views.xml @@ -0,0 +1,129 @@ + + + + + + + + + fl.discovery.suggest.wizard.form + fl.discovery.suggest.wizard + +
+ + +
+

Suggested Discovery Items

+
+ + + + + + + + + + + + + + +
+ Simple case — core financial disclosure items only. + Run an AI analysis to check for additional complexity factors. +
+
+ Moderate complexity — standard income discovery + + targeted items based on case flags. + Items marked "Moderate" minimum are included. +
+
+ Complex case — full discovery suite including employer + subpoenas, bank subpoenas, and deposition. +
+ +
+ Review and deselect any items that don't apply. + Click Create Selected to add them to the case as draft + discovery items. Items are not served until you mark them "Served." +
+ + + + + + + + + + + + + + + +
+
+
+
+
+
+ + + + + Suggest Discovery Items + fl.discovery.suggest.wizard + form + new + + form + + + + + + fl.case.form.issue.tags + fl.case + + + + + + + + + + + +
+
diff --git a/activeblue_familylaw/wizard/__init__.py b/activeblue_familylaw/wizard/__init__.py index 5b5ff74..548b457 100644 --- a/activeblue_familylaw/wizard/__init__.py +++ b/activeblue_familylaw/wizard/__init__.py @@ -1,3 +1,4 @@ from . import fl_intake_wizard from . import fl_analysis_wizard from . import fl_generate_packet_wizard +from . import fl_discovery_suggest_wizard diff --git a/activeblue_familylaw/wizard/fl_discovery_suggest_wizard.py b/activeblue_familylaw/wizard/fl_discovery_suggest_wizard.py new file mode 100644 index 0000000..c60f5f5 --- /dev/null +++ b/activeblue_familylaw/wizard/fl_discovery_suggest_wizard.py @@ -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=( + '🔍 {} Discovery Items Created ' + '(Complexity: {} — {})
' + '{}' + ).format( + len(created), + self.complexity.capitalize(), + self.complexity_source, + '
'.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', + }