Step 9: child-support modification workflow (end to end)

familylaw.support.modification — wires Steps 1-8 for a support modification:
- open_for_case() opens a NEW modification proceeding under the same case (locked
  decision) and sets a 20-day answer deadline on it
- 15%/$50 substantial-change PRESUMPTION: threshold = max($50, 15% of current);
  meets_threshold computed. Explicitly a deterministic test, NOT a prediction of
  whether the court grants it (no outcome/probability — EXCLUDED capability honored)
- DOR / Title IV-D flag; retroactivity to filing date + note
- prior-judgment handling: action_extract_prior_judgment produces a plain summary +
  FLAGGED interpretation QUESTIONS (carries a not-a-conclusion disclaimer); the model
  has no conclusion/ruling field by design. AI call mocked.

Wizard + views + menu; "Open Support Modification" button on the case form (shown
for support_modification matters). ACL added.

Tests (familylaw_step9): new proceeding created + distinct from original; threshold
15% prong, $50 prong (met/not), change %, no-change; 20-day answer deadline + date
math; DOR flag; retroactivity date; extraction summarizes + flags (not concluded,
no conclusion field); extraction logs a done ai.task.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tocmo0nlord
2026-06-02 04:19:23 +00:00
parent 83f970a031
commit 7eb944c83c
10 changed files with 470 additions and 1 deletions

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
{
"name": "Active Blue Family Law",
"version": "18.0.8.0.0",
"version": "18.0.9.0.0",
"category": "Services/Legal",
"summary": "Florida family law case management (Miami-Dade / 11th Judicial Circuit)",
"description": """
@@ -35,6 +35,7 @@ Each step adds one vertical, independently testable slice. See BUILD_PLAN.md.
"views/familylaw_disclosure_views.xml",
"views/familylaw_ai_views.xml",
"views/familylaw_discovery_views.xml",
"views/familylaw_modification_views.xml",
"views/familylaw_proceeding_views.xml",
"views/familylaw_intake_views.xml",
"views/familylaw_case_views.xml",

View File

@@ -13,4 +13,6 @@ from . import familylaw_citation
from . import familylaw_verifier
from . import familylaw_research
from . import familylaw_discovery
from . import familylaw_modification
from . import familylaw_ai_wizard
from . import familylaw_modification_wizard

View File

@@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
"""STEP 9 — Child-support modification workflow (end to end).
Wires Steps 1-8 for a support modification. Per the locked decision, a modification
does NOT mutate the original action — it opens a NEW `familylaw.proceeding` under the
same case, and deadlines/documents attach to that proceeding.
Captures the deterministic, NON-judgment facts:
* the 15%/$50 substantial-change threshold (presumption of substantial change) —
computed, never an opinion on whether the court will grant it;
* the 20-day answer clock on the supplemental petition;
* the DOR / Title IV-D flag (routes differently);
* retroactivity to the filing date.
Prior-judgment handling is EXTRACTION + PLAIN SUMMARY + FLAGGED interpretation
QUESTIONS — never a conclusion or ruling (locked decision). The AI call is mocked
in tests. Nothing here decides the modification; the attorney does.
VERIFY current rule — the 15%/$50 presumption (§61.30(1)(b)) and the 20-day answer
window are volatile; confirm before relying in production.
"""
from odoo import api, fields, models, _
THRESHOLD_PCT = 0.15 # 15% — VERIFY
THRESHOLD_MIN = 50.0 # or $50, whichever is greater — VERIFY
ANSWER_DAYS = 20 # answer to supplemental petition — VERIFY
class FamilyLawSupportModification(models.Model):
_name = "familylaw.support.modification"
_description = "Child-Support Modification"
_inherit = ["mail.thread"]
_order = "create_date desc"
name = fields.Char(default="Support Modification", tracking=True)
proceeding_id = fields.Many2one(
"familylaw.proceeding", required=True, ondelete="cascade", index=True,
tracking=True,
)
case_id = fields.Many2one(
"familylaw.case", related="proceeding_id.case_id", store=True, index=True,
)
current_support_amount = fields.Float(string="Current Monthly Support", tracking=True)
proposed_support_amount = fields.Float(string="Proposed Monthly Support", tracking=True)
change_amount = fields.Float(compute="_compute_threshold", store=True)
change_pct = fields.Float(compute="_compute_threshold", store=True,
string="Change %")
threshold_amount = fields.Float(compute="_compute_threshold", store=True,
string="Required Min Change")
meets_threshold = fields.Boolean(
compute="_compute_threshold", store=True,
string="Meets 15%/$50 Presumption",
help="Deterministic threshold check ONLY. Whether the court grants the "
"modification is the attorney's/judge's call — not stated here.",
)
is_dor_case = fields.Boolean(
string="DOR / Title IV-D Case",
tracking=True,
help="Department of Revenue Title IV-D case — routes differently.",
)
filing_date = fields.Date(string="Petition Filing Date",
default=fields.Date.context_today, tracking=True)
retroactivity_date = fields.Date(
string="Retroactive To",
help="Modification is generally retroactive to the date the supplemental "
"petition was filed. Verify the current rule for the matter.",
)
retroactivity_note = fields.Text(
default=lambda self: _(
"A granted modification is generally retroactive to the petition filing "
"date. The attorney determines the applicable retroactivity."
),
)
# Prior judgment — extraction + summary + FLAGGED questions (no conclusions)
prior_order_text = fields.Text(string="Prior Order Text")
extracted_summary = fields.Text(string="Plain-Language Summary", readonly=True)
interpretation_flags = fields.Text(
string="Interpretation Questions (for attorney)", readonly=True,
help="Questions surfaced for the attorney — never answered or concluded here.",
)
extraction_task_id = fields.Many2one("familylaw.ai.task", readonly=True)
@api.depends("current_support_amount", "proposed_support_amount")
def _compute_threshold(self):
for rec in self:
change = abs((rec.proposed_support_amount or 0.0)
- (rec.current_support_amount or 0.0))
rec.change_amount = change
base = rec.current_support_amount or 0.0
rec.change_pct = (change / base * 100.0) if base else 0.0
rec.threshold_amount = max(THRESHOLD_MIN, THRESHOLD_PCT * base)
rec.meets_threshold = change >= rec.threshold_amount and change > 0
# --- creation wiring ----------------------------------------------------
@api.model
def open_for_case(self, case, current_amount, proposed_amount, *,
is_dor=False, filing_date=None, prior_order_text=None):
"""Open a NEW modification proceeding under the case and wire its 20-day
answer deadline. Returns the modification record."""
case = case if hasattr(case, "id") else self.env["familylaw.case"].browse(case)
filing_date = filing_date or fields.Date.context_today(self)
proc = self.env["familylaw.proceeding"].create({
"case_id": case.id,
"name": _("Support Modification (%s)") % filing_date,
"proceeding_type": "modification",
})
rec = self.create({
"proceeding_id": proc.id,
"current_support_amount": current_amount,
"proposed_support_amount": proposed_amount,
"is_dor_case": is_dor,
"filing_date": filing_date,
"retroactivity_date": filing_date,
"prior_order_text": prior_order_text or False,
})
# 20-day answer clock on the supplemental petition.
self.env["familylaw.deadline"].create({
"proceeding_id": proc.id,
"name": _("Answer to Supplemental Petition due"),
"deadline_type": "answer",
"trigger_date": filing_date,
"days": ANSWER_DAYS,
})
case.message_post(body=_(
"New support-modification proceeding opened (filing %s). 20-day answer "
"clock set. This is a new proceeding under the same matter.") % filing_date)
return rec
# --- prior-judgment extraction (flagged, not concluded) -----------------
def action_extract_prior_judgment(self):
"""AI extraction + plain summary + FLAGGED interpretation questions.
Never states a legal conclusion. Mocked in tests."""
Client = self.env["familylaw.ai.client"]
for rec in self:
instruction = _(
"Extract the key support terms from this order and write a plain-"
"language summary. Then LIST interpretation QUESTIONS the attorney "
"should resolve. Do NOT answer them or state any legal conclusion.\n\n"
"%s"
) % (rec.prior_order_text or "")
result, task = Client.generate(
"extract_judgment", [{"role": "user", "content": instruction}],
case=rec.case_id, proceeding=rec.proceeding_id,
name=_("Prior-judgment extraction"),
)
text = result.get("text") or ""
rec.extracted_summary = text
rec.interpretation_flags = _(
"FLAGGED FOR ATTORNEY — these are questions, NOT conclusions or "
"rulings. The software does not interpret the order:\n%s"
) % text
rec.extraction_task_id = task.id
rec.message_post(body=_(
"Prior judgment extracted and summarized. Interpretation questions "
"flagged for the attorney — no conclusion drawn by the software."))
return True

View File

@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
"""STEP 9 — Wizard to open a support modification from a case."""
from odoo import fields, models
class FamilyLawModificationWizard(models.TransientModel):
_name = "familylaw.modification.wizard"
_description = "Open Support Modification"
case_id = fields.Many2one("familylaw.case", required=True)
current_support_amount = fields.Float(string="Current Monthly Support")
proposed_support_amount = fields.Float(string="Proposed Monthly Support")
is_dor_case = fields.Boolean(string="DOR / Title IV-D Case")
filing_date = fields.Date(default=fields.Date.context_today)
prior_order_text = fields.Text(string="Prior Order Text (optional)")
def action_open(self):
self.ensure_one()
rec = self.env["familylaw.support.modification"].open_for_case(
self.case_id,
self.current_support_amount,
self.proposed_support_amount,
is_dor=self.is_dor_case,
filing_date=self.filing_date,
prior_order_text=self.prior_order_text or None,
)
return {
"type": "ir.actions.act_window",
"res_model": "familylaw.support.modification",
"res_id": rec.id,
"view_mode": "form",
"target": "current",
}

View File

@@ -29,3 +29,6 @@ access_familylaw_citation_attorney,familylaw.citation attorney,model_familylaw_c
access_familylaw_ai_draft_wizard_user,familylaw.ai.draft.wizard user,model_familylaw_ai_draft_wizard,group_familylaw_user,1,1,1,1
access_familylaw_discovery_user,familylaw.discovery.request staff,model_familylaw_discovery_request,group_familylaw_user,1,1,1,0
access_familylaw_discovery_attorney,familylaw.discovery.request attorney,model_familylaw_discovery_request,group_familylaw_attorney,1,1,1,1
access_familylaw_modification_user,familylaw.support.modification staff,model_familylaw_support_modification,group_familylaw_user,1,1,1,0
access_familylaw_modification_attorney,familylaw.support.modification attorney,model_familylaw_support_modification,group_familylaw_attorney,1,1,1,1
access_familylaw_modification_wizard_user,familylaw.modification.wizard user,model_familylaw_modification_wizard,group_familylaw_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
29 access_familylaw_ai_draft_wizard_user familylaw.ai.draft.wizard user model_familylaw_ai_draft_wizard group_familylaw_user 1 1 1 1
30 access_familylaw_discovery_user familylaw.discovery.request staff model_familylaw_discovery_request group_familylaw_user 1 1 1 0
31 access_familylaw_discovery_attorney familylaw.discovery.request attorney model_familylaw_discovery_request group_familylaw_attorney 1 1 1 1
32 access_familylaw_modification_user familylaw.support.modification staff model_familylaw_support_modification group_familylaw_user 1 1 1 0
33 access_familylaw_modification_attorney familylaw.support.modification attorney model_familylaw_support_modification group_familylaw_attorney 1 1 1 1
34 access_familylaw_modification_wizard_user familylaw.modification.wizard user model_familylaw_modification_wizard group_familylaw_user 1 1 1 1

View File

@@ -6,3 +6,4 @@ from . import test_step5
from . import test_step6
from . import test_step7
from . import test_step8
from . import test_step9

View File

@@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
"""STEP 9 tests — child-support modification workflow.
odoo -d <db> -u activeblue_familylaw --test-enable \
--test-tags familylaw_step9 --stop-after-init
Proves:
* a modification opens a NEW proceeding under the same case;
* the 15%/$50 substantial-change threshold (max of 15% or $50);
* a 20-day answer deadline is created on the new proceeding;
* the DOR flag and retroactivity date are captured;
* prior-judgment handling produces a summary + FLAGGED interpretation questions,
never a conclusion (AI mocked).
"""
from datetime import date
from unittest.mock import patch
from odoo.tests.common import TransactionCase, tagged
CLIENT = "odoo.addons.activeblue_familylaw.models.familylaw_ai.FamilyLawAIClient"
def _ai(text="EXTRACTION OUTPUT"):
return {"text": text, "usage": {"input_tokens": 5, "output_tokens": 5},
"citations": []}
@tagged("post_install", "-at_install", "familylaw", "familylaw_step9")
class TestStep9Modification(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner = cls.env["res.partner"].create({"name": "Mod Client"})
cls.case = cls.env["familylaw.case"].create({
"name": "Mod Matter", "client_id": cls.partner.id,
"case_type": "support_modification",
})
cls.Mod = cls.env["familylaw.support.modification"]
icp = cls.env["ir.config_parameter"].sudo()
icp.set_param("familylaw.anthropic_api_key", "k")
icp.set_param("familylaw.model", "claude-sonnet-4-6")
# --- new proceeding -----------------------------------------------------
def test_01_opens_new_proceeding(self):
before = len(self.case.proceeding_ids)
rec = self.Mod.open_for_case(self.case, 1000.0, 1200.0)
self.assertEqual(len(self.case.proceeding_ids), before + 1)
self.assertEqual(rec.proceeding_id.proceeding_type, "modification")
self.assertEqual(rec.case_id, self.case)
def test_02_separate_from_original_proceeding(self):
original = self.case.proceeding_ids[0]
rec = self.Mod.open_for_case(self.case, 1000.0, 1200.0)
self.assertNotEqual(rec.proceeding_id, original)
# --- threshold ----------------------------------------------------------
def test_03_meets_threshold_15pct(self):
rec = self.Mod.open_for_case(self.case, 1000.0, 1200.0) # +200 vs thr 150
self.assertTrue(rec.meets_threshold)
def test_04_below_threshold(self):
rec = self.Mod.open_for_case(self.case, 1000.0, 1100.0) # +100 vs thr 150
self.assertFalse(rec.meets_threshold)
def test_05_fifty_dollar_prong(self):
# low base: 15% of 200 = 30, but $50 floor applies; +60 meets
rec = self.Mod.open_for_case(self.case, 200.0, 260.0)
self.assertEqual(rec.threshold_amount, 50.0)
self.assertTrue(rec.meets_threshold)
def test_06_fifty_dollar_prong_not_met(self):
rec = self.Mod.open_for_case(self.case, 200.0, 240.0) # +40 < $50
self.assertFalse(rec.meets_threshold)
def test_07_change_pct(self):
rec = self.Mod.open_for_case(self.case, 1000.0, 1200.0)
self.assertAlmostEqual(rec.change_pct, 20.0, places=2)
def test_08_no_change_not_met(self):
rec = self.Mod.open_for_case(self.case, 1000.0, 1000.0)
self.assertFalse(rec.meets_threshold)
# --- 20-day answer deadline ---------------------------------------------
def test_09_answer_deadline_created(self):
rec = self.Mod.open_for_case(self.case, 1000.0, 1200.0,
filing_date=date(2025, 1, 1))
answer = rec.proceeding_id.deadline_ids.filtered(
lambda d: d.deadline_type == "answer")
self.assertTrue(answer)
self.assertEqual(answer.days, 20)
# 2025-01-01 + 20 = 2025-01-21 (Tue)
self.assertEqual(answer.due_date, date(2025, 1, 21))
# --- DOR + retroactivity ------------------------------------------------
def test_10_dor_flag(self):
rec = self.Mod.open_for_case(self.case, 1000.0, 1200.0, is_dor=True)
self.assertTrue(rec.is_dor_case)
def test_11_retroactivity_date_is_filing_date(self):
rec = self.Mod.open_for_case(self.case, 1000.0, 1200.0,
filing_date=date(2025, 3, 5))
self.assertEqual(rec.retroactivity_date, date(2025, 3, 5))
# --- prior-judgment extraction (flagged, not concluded) -----------------
def test_12_extraction_summarizes_and_flags(self):
rec = self.Mod.open_for_case(self.case, 1000.0, 1200.0,
prior_order_text="Order: $1000/mo to Mother.")
with patch(CLIENT + "._call_provider", return_value=_ai("SUMMARY TEXT")):
rec.action_extract_prior_judgment()
self.assertIn("SUMMARY TEXT", rec.extracted_summary)
self.assertTrue(rec.interpretation_flags)
def test_13_interpretation_is_flagged_not_concluded(self):
rec = self.Mod.open_for_case(self.case, 1000.0, 1200.0,
prior_order_text="Order text.")
with patch(CLIENT + "._call_provider", return_value=_ai()):
rec.action_extract_prior_judgment()
# the interpretation field carries the not-a-conclusion disclaimer
self.assertIn("NOT conclusions", rec.interpretation_flags)
# the model structurally has no field that states a legal conclusion/ruling
self.assertNotIn("legal_conclusion", rec._fields)
self.assertNotIn("ruling", rec._fields)
def test_14_extraction_logs_ai_task(self):
rec = self.Mod.open_for_case(self.case, 1000.0, 1200.0,
prior_order_text="Order text.")
with patch(CLIENT + "._call_provider", return_value=_ai()):
rec.action_extract_prior_judgment()
self.assertTrue(rec.extraction_task_id)
self.assertEqual(rec.extraction_task_id.state, "done")

View File

@@ -50,6 +50,11 @@
string="Start Mediation" invisible="state != 'discovery'"/>
<button name="action_set_hearing" type="object"
string="Set for Hearing" invisible="state != 'mediation'"/>
<!-- Open a support modification (new proceeding) -->
<button name="%(action_familylaw_modification_wizard)d" type="action"
string="Open Support Modification"
context="{'default_case_id': id}"
invisible="case_type != 'support_modification'"/>
<!-- Attorney-only: close / reopen -->
<button name="action_close" type="object" string="Close Case"
invisible="state == 'closed'"

View File

@@ -41,6 +41,13 @@
action="action_familylaw_affidavit"
sequence="40"/>
<!-- Support Modifications -->
<menuitem id="menu_familylaw_modifications"
name="Support Modifications"
parent="menu_familylaw_root"
action="action_familylaw_modification"
sequence="44"/>
<!-- Discovery -->
<menuitem id="menu_familylaw_discovery"
name="Discovery"

View File

@@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_familylaw_modification_list" model="ir.ui.view">
<field name="name">familylaw.support.modification.list</field>
<field name="model">familylaw.support.modification</field>
<field name="arch" type="xml">
<list string="Support Modifications">
<field name="name"/>
<field name="case_id"/>
<field name="current_support_amount"/>
<field name="proposed_support_amount"/>
<field name="change_pct"/>
<field name="meets_threshold" widget="boolean_toggle"/>
<field name="is_dor_case"/>
</list>
</field>
</record>
<record id="view_familylaw_modification_form" model="ir.ui.view">
<field name="name">familylaw.support.modification.form</field>
<field name="model">familylaw.support.modification</field>
<field name="arch" type="xml">
<form string="Support Modification">
<header>
<button name="action_extract_prior_judgment" type="object"
string="Extract Prior Judgment"
invisible="not prior_order_text"/>
</header>
<sheet>
<div class="alert alert-info" role="alert">
The 15%/$50 check is a deterministic PRESUMPTION test only.
Whether the court grants the modification is the attorney's
and the court's judgment — the software does not predict it.
</div>
<div class="oe_title">
<label for="name"/>
<h1><field name="name"/></h1>
</div>
<group>
<group string="Support Amounts">
<field name="current_support_amount"/>
<field name="proposed_support_amount"/>
<field name="change_amount" readonly="1"/>
<field name="change_pct" readonly="1"/>
<field name="threshold_amount" readonly="1"/>
<field name="meets_threshold" readonly="1"/>
</group>
<group string="Matter / Timing">
<field name="proceeding_id"/>
<field name="case_id" readonly="1"/>
<field name="is_dor_case"/>
<field name="filing_date"/>
<field name="retroactivity_date"/>
</group>
</group>
<group string="Retroactivity">
<field name="retroactivity_note" nolabel="1" readonly="1"/>
</group>
<notebook>
<page string="Prior Judgment" name="prior">
<field name="prior_order_text"
placeholder="Paste the prior order text to extract + summarize."/>
<separator string="Plain-Language Summary (AI draft)"/>
<field name="extracted_summary" readonly="1"/>
<separator string="Interpretation Questions — FOR ATTORNEY (not conclusions)"/>
<field name="interpretation_flags" readonly="1"/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<record id="action_familylaw_modification" model="ir.actions.act_window">
<field name="name">Support Modifications</field>
<field name="res_model">familylaw.support.modification</field>
<field name="view_mode">list,form</field>
</record>
<!-- Wizard -->
<record id="view_familylaw_modification_wizard_form" model="ir.ui.view">
<field name="name">familylaw.modification.wizard.form</field>
<field name="model">familylaw.modification.wizard</field>
<field name="arch" type="xml">
<form string="Open Support Modification">
<sheet>
<div class="alert alert-info" role="alert">
Opens a NEW proceeding under this matter and sets the 20-day
answer clock. Captures facts only.
</div>
<group>
<field name="case_id"/>
<field name="current_support_amount"/>
<field name="proposed_support_amount"/>
<field name="is_dor_case"/>
<field name="filing_date"/>
<field name="prior_order_text"/>
</group>
</sheet>
<footer>
<button name="action_open" type="object"
string="Open Modification" class="btn-primary"/>
<button string="Cancel" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_familylaw_modification_wizard" model="ir.actions.act_window">
<field name="name">Open Support Modification</field>
<field name="res_model">familylaw.modification.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>