Step 5: mandatory disclosure (Rule 12.285) — checklist + financial affidavit

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 <noreply@anthropic.com>
This commit is contained in:
tocmo0nlord
2026-06-02 04:04:57 +00:00
parent a6f8d31316
commit 82f9d3c0e8
10 changed files with 578 additions and 1 deletions

View File

@@ -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",

View File

@@ -7,3 +7,4 @@ from . import familylaw_conflict
from . import familylaw_intake
from . import familylaw_document
from . import familylaw_deadline
from . import familylaw_disclosure

View File

@@ -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).",
)

View File

@@ -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

View File

@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
16 access_familylaw_document_attorney familylaw.document attorney model_familylaw_document group_familylaw_attorney 1 1 1 1
17 access_familylaw_deadline_user familylaw.deadline staff model_familylaw_deadline group_familylaw_user 1 1 1 0
18 access_familylaw_deadline_attorney familylaw.deadline attorney model_familylaw_deadline group_familylaw_attorney 1 1 1 1
19 access_familylaw_disclosure_item_user familylaw.disclosure.item staff model_familylaw_disclosure_item group_familylaw_user 1 1 1 0
20 access_familylaw_disclosure_item_attorney familylaw.disclosure.item attorney model_familylaw_disclosure_item group_familylaw_attorney 1 1 1 1
21 access_familylaw_affidavit_user familylaw.financial.affidavit staff model_familylaw_financial_affidavit group_familylaw_user 1 1 1 0
22 access_familylaw_affidavit_attorney familylaw.financial.affidavit attorney model_familylaw_financial_affidavit group_familylaw_attorney 1 1 1 1
23 access_familylaw_fin_line_user familylaw.fin.line staff model_familylaw_fin_line group_familylaw_user 1 1 1 0
24 access_familylaw_fin_line_attorney familylaw.fin.line attorney model_familylaw_fin_line group_familylaw_attorney 1 1 1 1

View File

@@ -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

View File

@@ -0,0 +1,154 @@
# -*- coding: utf-8 -*-
"""STEP 5 tests — mandatory disclosure (Rule 12.285).
odoo -d <db> -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)

View File

@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ===== Disclosure item ===== -->
<record id="view_familylaw_disclosure_item_list" model="ir.ui.view">
<field name="name">familylaw.disclosure.item.list</field>
<field name="model">familylaw.disclosure.item</field>
<field name="arch" type="xml">
<list string="Disclosure Items"
decoration-success="state == 'provided'"
decoration-muted="state in ('waived','na')">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="case_id"/>
<field name="is_mandatory"/>
<field name="state" widget="badge"
decoration-success="state == 'provided'"
decoration-info="state == 'required'"/>
</list>
</field>
</record>
<record id="view_familylaw_disclosure_item_form" model="ir.ui.view">
<field name="name">familylaw.disclosure.item.form</field>
<field name="model">familylaw.disclosure.item</field>
<field name="arch" type="xml">
<form string="Disclosure Item">
<header>
<button name="action_mark_provided" type="object"
string="Mark Provided" class="btn-primary"
invisible="state == 'provided'"/>
<button name="action_waive" type="object" string="Waive"
invisible="state in ('waived','na') or is_mandatory"/>
</header>
<sheet>
<div class="alert alert-info" role="alert" invisible="not is_mandatory">
Mandatory under Rule 12.285 — cannot be waived by the parties.
</div>
<group>
<group>
<field name="name"/>
<field name="is_mandatory"/>
<field name="state"/>
</group>
<group>
<field name="proceeding_id"/>
<field name="case_id" readonly="1"/>
</group>
</group>
<group string="Notes"><field name="notes" nolabel="1"/></group>
</sheet>
</form>
</field>
</record>
<!-- ===== Financial affidavit ===== -->
<record id="view_familylaw_affidavit_list" model="ir.ui.view">
<field name="name">familylaw.financial.affidavit.list</field>
<field name="model">familylaw.financial.affidavit</field>
<field name="arch" type="xml">
<list string="Financial Affidavits">
<field name="name"/>
<field name="case_id"/>
<field name="gross_annual_income"/>
<field name="form_reference"/>
<field name="due_date"/>
<field name="state" widget="badge"/>
</list>
</field>
</record>
<record id="view_familylaw_affidavit_form" model="ir.ui.view">
<field name="name">familylaw.financial.affidavit.form</field>
<field name="model">familylaw.financial.affidavit</field>
<field name="arch" type="xml">
<form string="Financial Affidavit">
<header>
<button name="action_mark_provided" type="object"
string="Mark Provided" class="btn-primary"
invisible="state != 'draft'"/>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="Affiant name"/></h1>
</div>
<group>
<group string="Form Selection">
<field name="gross_annual_income"/>
<field name="form_reference" readonly="1"/>
<field name="form_type" readonly="1"/>
</group>
<group string="Matter / Timing">
<field name="proceeding_id"/>
<field name="case_id" readonly="1"/>
<field name="party_id"/>
<field name="trigger_date"/>
<field name="due_date" readonly="1"/>
</group>
</group>
<notebook>
<page string="Line Items" name="lines">
<field name="line_ids">
<list editable="bottom">
<field name="category"/>
<field name="name"/>
<field name="amount"/>
<field name="monthly"/>
</list>
</field>
<group class="oe_subtotal_footer">
<field name="total_income"/>
<field name="total_expense"/>
<field name="total_asset"/>
<field name="total_liability"/>
<field name="net_worth"/>
</group>
</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_affidavit" model="ir.actions.act_window">
<field name="name">Financial Affidavits</field>
<field name="res_model">familylaw.financial.affidavit</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@@ -34,6 +34,13 @@
action="action_familylaw_deadline"
sequence="30"/>
<!-- Financial Affidavits -->
<menuitem id="menu_familylaw_affidavits"
name="Financial Affidavits"
parent="menu_familylaw_root"
action="action_familylaw_affidavit"
sequence="40"/>
<!-- Configuration placeholder (populated in later steps) -->
<menuitem id="menu_familylaw_config"
name="Configuration"

View File

@@ -26,6 +26,8 @@
<header>
<button name="action_seed_standard_deadlines" type="object"
string="Seed Standard Deadlines"/>
<button name="action_seed_disclosure_items" type="object"
string="Seed Disclosure Checklist"/>
<button name="action_close_proceeding" type="object"
string="Close Proceeding"
invisible="state == 'closed'"/>
@@ -82,6 +84,31 @@
</list>
</field>
</page>
<page string="Disclosure" name="disclosure">
<field name="disclosure_item_ids"
context="{'default_proceeding_id': id}">
<list editable="bottom"
decoration-success="state == 'provided'"
decoration-muted="state in ('waived','na')">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="is_mandatory"/>
<field name="state" widget="badge"/>
</list>
</field>
</page>
<page string="Financial Affidavits" name="affidavits">
<field name="affidavit_ids"
context="{'default_proceeding_id': id}">
<list>
<field name="name"/>
<field name="gross_annual_income"/>
<field name="form_reference"/>
<field name="due_date"/>
<field name="state" widget="badge"/>
</list>
</field>
</page>
</notebook>
</sheet>
<div class="oe_chatter">