From 7eb944c83ccaff1dae357f4024a2134b088ac075 Mon Sep 17 00:00:00 2001 From: tocmo0nlord Date: Tue, 2 Jun 2026 04:19:23 +0000 Subject: [PATCH] Step 9: child-support modification workflow (end to end) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../activeblue_familylaw/__manifest__.py | 3 +- .../activeblue_familylaw/models/__init__.py | 2 + .../models/familylaw_modification.py | 161 ++++++++++++++++++ .../models/familylaw_modification_wizard.py | 34 ++++ .../security/ir.model.access.csv | 3 + .../activeblue_familylaw/tests/__init__.py | 1 + .../activeblue_familylaw/tests/test_step9.py | 133 +++++++++++++++ .../views/familylaw_case_views.xml | 5 + .../views/familylaw_menus.xml | 7 + .../views/familylaw_modification_views.xml | 122 +++++++++++++ 10 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_modification.py create mode 100644 activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_modification_wizard.py create mode 100644 activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/test_step9.py create mode 100644 activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_modification_views.xml diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/__manifest__.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/__manifest__.py index fdb3a37..27bbcd3 100644 --- a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/__manifest__.py +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/__manifest__.py @@ -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", diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/__init__.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/__init__.py index 54028e1..ec42ca5 100644 --- a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/__init__.py +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/__init__.py @@ -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 diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_modification.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_modification.py new file mode 100644 index 0000000..3d7f814 --- /dev/null +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_modification.py @@ -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 diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_modification_wizard.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_modification_wizard.py new file mode 100644 index 0000000..fb19b8e --- /dev/null +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_modification_wizard.py @@ -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", + } diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/security/ir.model.access.csv b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/security/ir.model.access.csv index b9f10f3..661f796 100644 --- a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/security/ir.model.access.csv +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/security/ir.model.access.csv @@ -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 diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/__init__.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/__init__.py index 4f9d5d0..1f34c63 100644 --- a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/__init__.py +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/__init__.py @@ -6,3 +6,4 @@ from . import test_step5 from . import test_step6 from . import test_step7 from . import test_step8 +from . import test_step9 diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/test_step9.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/test_step9.py new file mode 100644 index 0000000..12a0f90 --- /dev/null +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/test_step9.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +"""STEP 9 tests — child-support modification workflow. + + odoo -d -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") diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_case_views.xml b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_case_views.xml index bb087ce..7f1c989 100644 --- a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_case_views.xml +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_case_views.xml @@ -50,6 +50,11 @@ string="Start Mediation" invisible="state != 'discovery'"/>