From 82f9d3c0e88cb9c919cdce173bb77d8314b322aa Mon Sep 17 00:00:00 2001 From: tocmo0nlord Date: Tue, 2 Jun 2026 04:04:57 +0000 Subject: [PATCH] =?UTF-8?q?Step=205:=20mandatory=20disclosure=20(Rule=2012?= =?UTF-8?q?.285)=20=E2=80=94=20checklist=20+=20financial=20affidavit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three per-proceeding models: - familylaw.disclosure.item: Rule 12.285 checklist; is_mandatory items refuse waiver in code (action_waive raises); non-mandatory can be waived - familylaw.financial.affidavit: form-by-income selection — short 12.902(b) below the $50,000 gross-annual threshold, long 12.902(c) at/above it; 45-day due date (Rule 12.285(e)) with weekend roll; line totals + net worth - familylaw.fin.line: income/expense/asset/liability line items All thresholds/counts flagged "verify current rule" (volatile FL law). proceeding gets disclosure_item_ids + affidavit_ids, Seed Disclosure Checklist button, and Disclosure / Financial Affidavits notebook tabs. Views + menu + ACL. Tests (familylaw_step5): 15 tests — form selection across the threshold boundary (49999 short / 50000 long / 80000 long), recompute on income change, 45-day due with/without weekend roll (fixed dates), mandatory-cannot-waive, non-mandatory waive, totals + net worth, idempotent seeding, per-proceeding isolation. Co-Authored-By: Claude Opus 4.8 --- .../activeblue_familylaw/__manifest__.py | 3 +- .../activeblue_familylaw/models/__init__.py | 1 + .../models/familylaw_disclosure.py | 209 ++++++++++++++++++ .../models/familylaw_proceeding.py | 34 +++ .../security/ir.model.access.csv | 6 + .../activeblue_familylaw/tests/__init__.py | 1 + .../activeblue_familylaw/tests/test_step5.py | 154 +++++++++++++ .../views/familylaw_disclosure_views.xml | 137 ++++++++++++ .../views/familylaw_menus.xml | 7 + .../views/familylaw_proceeding_views.xml | 27 +++ 10 files changed, 578 insertions(+), 1 deletion(-) create mode 100644 activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_disclosure.py create mode 100644 activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/test_step5.py create mode 100644 activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_disclosure_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 6ba512b..76040ea 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.4.0.0", + "version": "18.0.5.0.0", "category": "Services/Legal", "summary": "Florida family law case management (Miami-Dade / 11th Judicial Circuit)", "description": """ @@ -32,6 +32,7 @@ Each step adds one vertical, independently testable slice. See BUILD_PLAN.md. "views/familylaw_proceeding_views.xml", "views/familylaw_document_views.xml", "views/familylaw_deadline_views.xml", + "views/familylaw_disclosure_views.xml", "views/familylaw_intake_views.xml", "views/familylaw_case_views.xml", "views/familylaw_menus.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 f46b910..5013c24 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 @@ -7,3 +7,4 @@ from . import familylaw_conflict from . import familylaw_intake from . import familylaw_document from . import familylaw_deadline +from . import familylaw_disclosure diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_disclosure.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_disclosure.py new file mode 100644 index 0000000..ded1ba2 --- /dev/null +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_disclosure.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +"""STEP 5 — Mandatory disclosure (Florida Family Law Rule 12.285). + +Three models, all hanging off a PROCEEDING: + + * familylaw.disclosure.item — the checklist of required disclosure documents + * familylaw.financial.affidavit — the financial affidavit, with the form-by-income + selection (12.902(b) short vs 12.902(c) long) + * familylaw.fin.line — line items (income/expense/asset/liability) + +LEGAL NOTES (verify current rule — these values are volatile): + * Mandatory disclosure under Rule 12.285 CANNOT be waived by the parties (with + narrow exceptions, e.g. simplified dissolution). Items flagged is_mandatory + refuse waiver in code. + * The financial affidavit form turns on gross annual income: below the threshold + the short form 12.902(b) applies; at or above it the long form 12.902(c) applies. + The threshold is currently $50,000 — CONFIRM before relying on it. + * The affidavit is due within 45 days of service of the initial pleading + (Rule 12.285(e)) — confirm the trigger and count for the matter at hand. +""" + +from datetime import timedelta + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + +# VERIFY CURRENT RULE — do not treat as permanent. +DISCLOSURE_INCOME_THRESHOLD = 50000.0 # gross annual income, USD +DISCLOSURE_DUE_DAYS = 45 # Rule 12.285(e) + + +def _roll_forward_weekend(due): + while due.weekday() >= 5: + due += timedelta(days=1) + return due + + +class FamilyLawDisclosureItem(models.Model): + _name = "familylaw.disclosure.item" + _description = "Mandatory Disclosure Item" + _inherit = ["mail.thread"] + _order = "sequence, id" + + sequence = fields.Integer(default=10) + 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, + ) + name = fields.Char(required=True, tracking=True) + is_mandatory = fields.Boolean( + string="Mandatory (cannot waive)", + default=True, + help="Rule 12.285 disclosure that the parties cannot waive.", + ) + state = fields.Selection( + selection=[ + ("required", "Required"), + ("provided", "Provided"), + ("waived", "Waived"), + ("na", "Not Applicable"), + ], + default="required", + required=True, + tracking=True, + ) + notes = fields.Text() + + def action_mark_provided(self): + for item in self: + item.state = "provided" + item.message_post(body=_("Disclosure item provided.")) + return True + + def action_waive(self): + for item in self: + if item.is_mandatory: + raise UserError( + _("'%s' is mandatory disclosure under Rule 12.285 and cannot be " + "waived by the parties.") % item.name + ) + item.state = "waived" + item.message_post(body=_("Disclosure item waived (non-mandatory).")) + return True + + +class FamilyLawFinancialAffidavit(models.Model): + _name = "familylaw.financial.affidavit" + _description = "Financial Affidavit (12.902(b)/(c))" + _inherit = ["mail.thread"] + _order = "create_date desc" + + 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, + ) + party_id = fields.Many2one("familylaw.party", string="Affiant (party)") + name = fields.Char(string="Affiant Name", required=True, tracking=True) + + gross_annual_income = fields.Float( + string="Gross Annual Income", + tracking=True, + help="Drives the form selection. Verify the current threshold.", + ) + form_type = fields.Selection( + selection=[ + ("short", "Short Form — 12.902(b)"), + ("long", "Long Form — 12.902(c)"), + ], + compute="_compute_form_type", + store=True, + string="Required Form", + ) + form_reference = fields.Char( + compute="_compute_form_type", store=True, string="Form #", + ) + + trigger_date = fields.Date(string="Trigger (service) Date", tracking=True) + due_date = fields.Date( + compute="_compute_due_date", store=True, tracking=True, + help="45 days from the trigger (Rule 12.285(e)), rolled off weekends.", + ) + state = fields.Selection( + selection=[ + ("draft", "Draft"), + ("provided", "Provided"), + ("filed", "Filed"), + ], + default="draft", + required=True, + tracking=True, + ) + line_ids = fields.One2many( + "familylaw.fin.line", "affidavit_id", string="Line Items", + ) + total_income = fields.Float(compute="_compute_totals", store=True) + total_expense = fields.Float(compute="_compute_totals", store=True) + total_asset = fields.Float(compute="_compute_totals", store=True) + total_liability = fields.Float(compute="_compute_totals", store=True) + net_worth = fields.Float(compute="_compute_totals", store=True) + + @api.depends("gross_annual_income") + def _compute_form_type(self): + for aff in self: + if aff.gross_annual_income >= DISCLOSURE_INCOME_THRESHOLD: + aff.form_type = "long" + aff.form_reference = "12.902(c)" + else: + aff.form_type = "short" + aff.form_reference = "12.902(b)" + + @api.depends("trigger_date") + def _compute_due_date(self): + for aff in self: + if not aff.trigger_date: + aff.due_date = False + continue + raw = aff.trigger_date + timedelta(days=DISCLOSURE_DUE_DAYS) + aff.due_date = _roll_forward_weekend(raw) + + @api.depends("line_ids.amount", "line_ids.category") + def _compute_totals(self): + for aff in self: + sums = {"income": 0.0, "expense": 0.0, "asset": 0.0, "liability": 0.0} + for line in aff.line_ids: + if line.category in sums: + sums[line.category] += line.amount + aff.total_income = sums["income"] + aff.total_expense = sums["expense"] + aff.total_asset = sums["asset"] + aff.total_liability = sums["liability"] + aff.net_worth = sums["asset"] - sums["liability"] + + def action_mark_provided(self): + for aff in self: + aff.state = "provided" + aff.message_post(body=_("Financial affidavit provided.")) + return True + + +class FamilyLawFinLine(models.Model): + _name = "familylaw.fin.line" + _description = "Financial Affidavit Line" + _order = "category, id" + + affidavit_id = fields.Many2one( + "familylaw.financial.affidavit", required=True, ondelete="cascade", index=True, + ) + category = fields.Selection( + selection=[ + ("income", "Income"), + ("expense", "Expense"), + ("asset", "Asset"), + ("liability", "Liability"), + ], + required=True, + default="income", + ) + name = fields.Char(string="Description", required=True) + amount = fields.Float() + monthly = fields.Boolean( + string="Monthly?", + help="If set, this is a recurring monthly figure (informational).", + ) diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_proceeding.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_proceeding.py index 27cfabe..dcf35fe 100644 --- a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_proceeding.py +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_proceeding.py @@ -60,6 +60,40 @@ class FamilyLawProceeding(models.Model): deadline_ids = fields.One2many( "familylaw.deadline", "proceeding_id", string="Deadlines", ) + disclosure_item_ids = fields.One2many( + "familylaw.disclosure.item", "proceeding_id", string="Disclosure Items", + ) + affidavit_ids = fields.One2many( + "familylaw.financial.affidavit", "proceeding_id", string="Financial Affidavits", + ) + + def action_seed_disclosure_items(self): + """Seed the standard Rule 12.285 mandatory disclosure checklist. + VERIFY the current rule's required-document list before relying on it.""" + Item = self.env["familylaw.disclosure.item"] + standard = [ + "Federal income tax returns (last 3 years)", + "IRS forms W-2, 1099, K-1 (last year)", + "Pay stubs (last 3 months)", + "Loan applications / financial statements (last 12 months)", + "Bank / checking / savings statements (last 3 months)", + "Deeds, leases, and mortgages on real property", + "Credit card and other liability statements (last 3 months)", + "Retirement / pension / profit-sharing statements (last 12 months)", + ] + for proc in self: + existing = set(proc.disclosure_item_ids.mapped("name")) + for seq, label in enumerate(standard, start=1): + if label in existing: + continue + Item.create({ + "proceeding_id": proc.id, + "name": label, + "sequence": seq * 10, + "is_mandatory": True, + }) + proc.message_post(body="Mandatory disclosure checklist seeded (Rule 12.285).") + return True def action_seed_standard_deadlines(self): """Create the common procedural deadlines for this proceeding, dated from 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 515902f..c90ea5f 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 @@ -16,3 +16,9 @@ access_familylaw_document_user,familylaw.document staff,model_familylaw_document access_familylaw_document_attorney,familylaw.document attorney,model_familylaw_document,group_familylaw_attorney,1,1,1,1 access_familylaw_deadline_user,familylaw.deadline staff,model_familylaw_deadline,group_familylaw_user,1,1,1,0 access_familylaw_deadline_attorney,familylaw.deadline attorney,model_familylaw_deadline,group_familylaw_attorney,1,1,1,1 +access_familylaw_disclosure_item_user,familylaw.disclosure.item staff,model_familylaw_disclosure_item,group_familylaw_user,1,1,1,0 +access_familylaw_disclosure_item_attorney,familylaw.disclosure.item attorney,model_familylaw_disclosure_item,group_familylaw_attorney,1,1,1,1 +access_familylaw_affidavit_user,familylaw.financial.affidavit staff,model_familylaw_financial_affidavit,group_familylaw_user,1,1,1,0 +access_familylaw_affidavit_attorney,familylaw.financial.affidavit attorney,model_familylaw_financial_affidavit,group_familylaw_attorney,1,1,1,1 +access_familylaw_fin_line_user,familylaw.fin.line staff,model_familylaw_fin_line,group_familylaw_user,1,1,1,0 +access_familylaw_fin_line_attorney,familylaw.fin.line attorney,model_familylaw_fin_line,group_familylaw_attorney,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 afcb0b3..18318b6 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 @@ -2,3 +2,4 @@ from . import test_case_lifecycle from . import test_step2 from . import test_step3 from . import test_step4 +from . import test_step5 diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/test_step5.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/test_step5.py new file mode 100644 index 0000000..4c89d2b --- /dev/null +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/test_step5.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +"""STEP 5 tests — mandatory disclosure (Rule 12.285). + + odoo -d -u activeblue_familylaw --test-enable \ + --test-tags familylaw_step5 --stop-after-init + +Proves: + * form selection 12.902(b) vs 12.902(c) by gross annual income (threshold); + * 45-day affidavit due date with weekend roll (fixed dates); + * mandatory disclosure cannot be waived; non-mandatory can; + * line-item totals and net worth; + * per-proceeding isolation + seeding. +""" + +from datetime import date + +from odoo.tests.common import TransactionCase, tagged +from odoo.exceptions import UserError + + +@tagged("post_install", "-at_install", "familylaw", "familylaw_step5") +class TestStep5Disclosure(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create({"name": "Disc Client"}) + cls.case = cls.env["familylaw.case"].create({ + "name": "Disclosure Matter", + "client_id": cls.partner.id, + "case_type": "dissolution_children", + }) + cls.proc = cls.case.proceeding_ids[0] + cls.Aff = cls.env["familylaw.financial.affidavit"] + cls.Item = cls.env["familylaw.disclosure.item"] + + def _aff(self, income, **kw): + vals = { + "proceeding_id": self.proc.id, + "name": "Affiant", + "gross_annual_income": income, + } + vals.update(kw) + return self.Aff.create(vals) + + # --- form selection by income ------------------------------------------- + def test_01_low_income_short_form(self): + aff = self._aff(30000.0) + self.assertEqual(aff.form_type, "short") + self.assertEqual(aff.form_reference, "12.902(b)") + + def test_02_high_income_long_form(self): + aff = self._aff(80000.0) + self.assertEqual(aff.form_type, "long") + self.assertEqual(aff.form_reference, "12.902(c)") + + def test_03_threshold_is_long_form(self): + # exactly at threshold (>=) -> long form + aff = self._aff(50000.0) + self.assertEqual(aff.form_type, "long") + + def test_04_just_below_threshold_short(self): + aff = self._aff(49999.0) + self.assertEqual(aff.form_type, "short") + + def test_05_form_recomputes_on_income_change(self): + aff = self._aff(10000.0) + self.assertEqual(aff.form_type, "short") + aff.gross_annual_income = 120000.0 + self.assertEqual(aff.form_type, "long") + + # --- 45-day due date ---------------------------------------------------- + def test_06_due_date_45_days(self): + # 2025-01-01 + 45 = 2025-02-15 (Sat) -> 2025-02-17 (Mon) + aff = self._aff(20000.0, trigger_date=date(2025, 1, 1)) + self.assertEqual(aff.due_date, date(2025, 2, 17)) + + def test_07_due_date_no_weekend_roll(self): + # 2025-02-03 (Mon) + 45 = 2025-03-20 (Thu) + aff = self._aff(20000.0, trigger_date=date(2025, 2, 3)) + self.assertEqual(aff.due_date, date(2025, 3, 20)) + + def test_08_no_trigger_no_due(self): + aff = self._aff(20000.0) + self.assertFalse(aff.due_date) + + # --- can't waive mandatory ---------------------------------------------- + def test_09_mandatory_cannot_waive(self): + item = self.Item.create({ + "proceeding_id": self.proc.id, + "name": "Tax returns", + "is_mandatory": True, + }) + with self.assertRaises(UserError): + item.action_waive() + self.assertEqual(item.state, "required") + + def test_10_non_mandatory_can_waive(self): + item = self.Item.create({ + "proceeding_id": self.proc.id, + "name": "Optional extra", + "is_mandatory": False, + }) + item.action_waive() + self.assertEqual(item.state, "waived") + + def test_11_mark_provided(self): + item = self.Item.create({ + "proceeding_id": self.proc.id, + "name": "Pay stubs", + "is_mandatory": True, + }) + item.action_mark_provided() + self.assertEqual(item.state, "provided") + + # --- totals ------------------------------------------------------------- + def test_12_totals_and_net_worth(self): + aff = self._aff(60000.0) + self.env["familylaw.fin.line"].create([ + {"affidavit_id": aff.id, "category": "income", "name": "Salary", "amount": 5000.0}, + {"affidavit_id": aff.id, "category": "expense", "name": "Rent", "amount": 2000.0}, + {"affidavit_id": aff.id, "category": "asset", "name": "Home", "amount": 300000.0}, + {"affidavit_id": aff.id, "category": "liability", "name": "Mortgage", "amount": 180000.0}, + ]) + self.assertEqual(aff.total_income, 5000.0) + self.assertEqual(aff.total_expense, 2000.0) + self.assertEqual(aff.total_asset, 300000.0) + self.assertEqual(aff.total_liability, 180000.0) + self.assertEqual(aff.net_worth, 120000.0) + + # --- seeding + isolation ------------------------------------------------ + def test_13_seed_disclosure_checklist(self): + self.proc.action_seed_disclosure_items() + self.assertTrue(self.proc.disclosure_item_ids) + self.assertTrue(all(self.proc.disclosure_item_ids.mapped("is_mandatory"))) + + def test_14_seed_is_idempotent(self): + self.proc.action_seed_disclosure_items() + n1 = len(self.proc.disclosure_item_ids) + self.proc.action_seed_disclosure_items() + self.assertEqual(len(self.proc.disclosure_item_ids), n1) + + def test_15_affidavit_isolated_per_proceeding(self): + proc2 = self.env["familylaw.proceeding"].create({ + "case_id": self.case.id, + "name": "Second", + "proceeding_type": "modification", + }) + a1 = self._aff(20000.0) + a2 = self.Aff.create({ + "proceeding_id": proc2.id, "name": "Other", "gross_annual_income": 90000.0, + }) + self.assertIn(a1, self.proc.affidavit_ids) + self.assertNotIn(a2, self.proc.affidavit_ids) diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_disclosure_views.xml b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_disclosure_views.xml new file mode 100644 index 0000000..2999d90 --- /dev/null +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_disclosure_views.xml @@ -0,0 +1,137 @@ + + + + + + familylaw.disclosure.item.list + familylaw.disclosure.item + + + + + + + + + + + + + familylaw.disclosure.item.form + familylaw.disclosure.item + +
+
+
+ + + + + + + + + + + + + + + +
+
+
+ + + + familylaw.financial.affidavit.list + familylaw.financial.affidavit + + + + + + + + + + + + + + familylaw.financial.affidavit.form + familylaw.financial.affidavit + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ + + Financial Affidavits + familylaw.financial.affidavit + list,form + + +
diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_menus.xml b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_menus.xml index cc1b6a7..2d282d2 100644 --- a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_menus.xml +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_menus.xml @@ -34,6 +34,13 @@ action="action_familylaw_deadline" sequence="30"/> + + +