Step 10: emergency workflow (12.941 pick-up/removal) — fork + attachment block

familylaw.emergency.motion (+ familylaw.emergency.attachment):
- have-order-vs-not FORK: action_seed_requirements seeds different required
  attachments (existing-order path = certified copy + proposed order; no-order path =
  verified motion/sworn affidavit + proposed order)
- MISSING-ATTACHMENT BLOCK: _ensure_complete raises if any required attachment is
  unprovided (or no checklist seeded); action_mark_ready / action_file gated on it
- open_for_case() uses the case's open proceeding (fast-path matter from Step 2)

Wizard + views + menu; "Emergency Motion" button on the case form (shown when the
matter is_emergency). ACL added.

Tests (familylaw_step10): fork seeds differ; missing/partial blocks ready; complete
allows ready + file; file requires ready; no-checklist blocks; intake fast-path
(urgency -> is_emergency case) then motion creation; motion uses open proceeding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tocmo0nlord
2026-06-02 04:21:39 +00:00
parent 7eb944c83c
commit 91d4cec0e0
10 changed files with 472 additions and 1 deletions

View File

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

View File

@@ -14,5 +14,7 @@ from . import familylaw_verifier
from . import familylaw_research
from . import familylaw_discovery
from . import familylaw_modification
from . import familylaw_emergency
from . import familylaw_ai_wizard
from . import familylaw_modification_wizard
from . import familylaw_emergency_wizard

View File

@@ -0,0 +1,172 @@
# -*- coding: utf-8 -*-
"""STEP 10 — Emergency workflow (Rule 12.941 pick-up / removal).
An emergency motion hangs off a PROCEEDING (typically the fast-path matter opened by
the intake urgency screen in Step 2). The two structural safety features:
* the HAVE-ORDER-vs-NOT fork — the required attachments differ depending on whether
there is already an order to enforce (certified copy) or the movant must establish
the emergency from scratch (verified motion / sworn affidavit);
* a MISSING-ATTACHMENT BLOCK — the motion cannot be marked ready/filed until every
required attachment for its fork is present.
VERIFY current rule — pick-up / emergency-relief mechanics and required attachments
are volatile; confirm before relying in production. Nothing here decides the motion;
the attorney signs and the court rules.
"""
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class FamilyLawEmergencyMotion(models.Model):
_name = "familylaw.emergency.motion"
_description = "Emergency Motion (12.941)"
_inherit = ["mail.thread"]
_order = "create_date desc"
name = fields.Char(required=True, default="Emergency Motion", 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,
)
child_id = fields.Many2one("familylaw.child", string="Child")
motion_type = fields.Selection(
selection=[
("pickup_order", "Pick-Up Order"),
("temporary_custody", "Emergency Temporary Custody"),
("prevent_removal", "Prevent Removal from Jurisdiction"),
("other", "Other Emergency Relief"),
],
string="Motion Type",
required=True,
default="pickup_order",
tracking=True,
)
have_existing_order = fields.Boolean(
string="Existing Order to Enforce?",
tracking=True,
help="The fork: enforce an existing order (certified copy) vs. establish the "
"emergency from scratch (verified motion / sworn affidavit).",
)
existing_order_reference = fields.Char(string="Existing Order Reference")
basis = fields.Text(string="Sworn Basis (no existing order)")
attachment_ids = fields.One2many(
"familylaw.emergency.attachment", "motion_id", string="Required Attachments",
)
missing_required_count = fields.Integer(
compute="_compute_missing", string="Missing Required",
)
state = fields.Selection(
selection=[
("draft", "Draft"),
("ready", "Ready to File"),
("filed", "Filed"),
],
default="draft",
required=True,
tracking=True,
)
@api.depends("attachment_ids.is_required", "attachment_ids.provided")
def _compute_missing(self):
for m in self:
m.missing_required_count = len(
m.attachment_ids.filtered(lambda a: a.is_required and not a.provided)
)
# --- requirement seeding (the fork) -------------------------------------
def _required_for_fork(self):
self.ensure_one()
if self.have_existing_order:
return [
_("Certified copy of the existing order"),
_("Proposed pick-up / enforcement order"),
]
return [
_("Verified motion / sworn affidavit establishing the emergency"),
_("Proposed pick-up order"),
]
def action_seed_requirements(self):
Att = self.env["familylaw.emergency.attachment"]
for m in self:
m.attachment_ids.unlink()
for label in m._required_for_fork():
Att.create({"motion_id": m.id, "name": label, "is_required": True})
m.message_post(body=_("Required attachments seeded for the %s path.")
% ("existing-order" if m.have_existing_order else "no-order"))
return True
# --- gates --------------------------------------------------------------
def _ensure_complete(self):
for m in self:
missing = m.attachment_ids.filtered(
lambda a: a.is_required and not a.provided)
if not m.attachment_ids:
raise UserError(
_("No attachment checklist yet. Seed the requirements first.")
)
if missing:
raise UserError(
_("Emergency motion '%(name)s' is missing required attachment(s): "
"%(list)s. It cannot be filed until complete.",
name=m.name, list="; ".join(missing.mapped("name")))
)
def action_mark_ready(self):
self._ensure_complete()
for m in self:
m.state = "ready"
m.message_post(body=_("All required attachments present — ready to file."))
return True
def action_file(self):
for m in self:
if m.state != "ready":
raise UserError(
_("Mark the motion ready (all attachments present) before filing.")
)
m.state = "filed"
m.message_post(body=_("Emergency motion filed."))
return True
# --- creation helper (fast-path) ----------------------------------------
@api.model
def open_for_case(self, case, motion_type, have_existing_order, *,
child=None, basis=None, existing_order_reference=None):
case = case if hasattr(case, "id") else self.env["familylaw.case"].browse(case)
proc = case.proceeding_ids.filtered(lambda p: p.state == "open")[:1]
if not proc:
proc = self.env["familylaw.proceeding"].create({
"case_id": case.id,
"name": _("Emergency Proceeding"),
"proceeding_type": "other",
})
motion = self.create({
"proceeding_id": proc.id,
"motion_type": motion_type,
"have_existing_order": have_existing_order,
"child_id": child.id if child else False,
"basis": basis or False,
"existing_order_reference": existing_order_reference or False,
})
motion.action_seed_requirements()
return motion
class FamilyLawEmergencyAttachment(models.Model):
_name = "familylaw.emergency.attachment"
_description = "Emergency Motion Attachment Requirement"
_order = "id"
motion_id = fields.Many2one(
"familylaw.emergency.motion", required=True, ondelete="cascade", index=True,
)
name = fields.Char(required=True)
is_required = fields.Boolean(default=True)
provided = fields.Boolean(default=False)

View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
"""STEP 10 — Wizard to open an emergency motion from a case (fast-path)."""
from odoo import fields, models
class FamilyLawEmergencyWizard(models.TransientModel):
_name = "familylaw.emergency.wizard"
_description = "Open Emergency Motion"
case_id = fields.Many2one("familylaw.case", required=True)
motion_type = fields.Selection(
selection=[
("pickup_order", "Pick-Up Order"),
("temporary_custody", "Emergency Temporary Custody"),
("prevent_removal", "Prevent Removal from Jurisdiction"),
("other", "Other Emergency Relief"),
],
default="pickup_order",
required=True,
)
have_existing_order = fields.Boolean(string="Existing Order to Enforce?")
child_id = fields.Many2one("familylaw.child", string="Child")
existing_order_reference = fields.Char(string="Existing Order Reference")
basis = fields.Text(string="Sworn Basis (no existing order)")
def action_open(self):
self.ensure_one()
motion = self.env["familylaw.emergency.motion"].open_for_case(
self.case_id, self.motion_type, self.have_existing_order,
child=self.child_id or None,
basis=self.basis or None,
existing_order_reference=self.existing_order_reference or None,
)
return {
"type": "ir.actions.act_window",
"res_model": "familylaw.emergency.motion",
"res_id": motion.id,
"view_mode": "form",
"target": "current",
}

View File

@@ -32,3 +32,8 @@ access_familylaw_discovery_attorney,familylaw.discovery.request attorney,model_f
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
access_familylaw_emergency_user,familylaw.emergency.motion staff,model_familylaw_emergency_motion,group_familylaw_user,1,1,1,0
access_familylaw_emergency_attorney,familylaw.emergency.motion attorney,model_familylaw_emergency_motion,group_familylaw_attorney,1,1,1,1
access_familylaw_emergency_att_user,familylaw.emergency.attachment staff,model_familylaw_emergency_attachment,group_familylaw_user,1,1,1,1
access_familylaw_emergency_att_attorney,familylaw.emergency.attachment attorney,model_familylaw_emergency_attachment,group_familylaw_attorney,1,1,1,1
access_familylaw_emergency_wizard_user,familylaw.emergency.wizard user,model_familylaw_emergency_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
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
35 access_familylaw_emergency_user familylaw.emergency.motion staff model_familylaw_emergency_motion group_familylaw_user 1 1 1 0
36 access_familylaw_emergency_attorney familylaw.emergency.motion attorney model_familylaw_emergency_motion group_familylaw_attorney 1 1 1 1
37 access_familylaw_emergency_att_user familylaw.emergency.attachment staff model_familylaw_emergency_attachment group_familylaw_user 1 1 1 1
38 access_familylaw_emergency_att_attorney familylaw.emergency.attachment attorney model_familylaw_emergency_attachment group_familylaw_attorney 1 1 1 1
39 access_familylaw_emergency_wizard_user familylaw.emergency.wizard user model_familylaw_emergency_wizard group_familylaw_user 1 1 1 1

View File

@@ -7,3 +7,4 @@ from . import test_step6
from . import test_step7
from . import test_step8
from . import test_step9
from . import test_step10

View File

@@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
"""STEP 10 tests — emergency workflow (12.941 pick-up/removal).
odoo -d <db> -u activeblue_familylaw --test-enable \
--test-tags familylaw_step10 --stop-after-init
Proves:
* the have-order-vs-not fork seeds different required attachments;
* a missing required attachment BLOCKS marking ready / filing;
* once complete, the motion can be marked ready and filed;
* the intake fast-path opens a matter on minimum facts (is_emergency) and an
emergency motion can be created on it.
"""
from odoo.tests.common import TransactionCase, tagged
from odoo.exceptions import UserError
@tagged("post_install", "-at_install", "familylaw", "familylaw_step10")
class TestStep10Emergency(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner = cls.env["res.partner"].create({"name": "Emerg Client"})
cls.case = cls.env["familylaw.case"].create({
"name": "Emergency Matter", "client_id": cls.partner.id,
"case_type": "enforcement", "is_emergency": True,
})
cls.Motion = cls.env["familylaw.emergency.motion"]
# --- the fork -----------------------------------------------------------
def test_01_no_order_fork_requirements(self):
m = self.Motion.open_for_case(self.case, "pickup_order", False)
names = m.attachment_ids.mapped("name")
self.assertTrue(any("sworn affidavit" in n.lower() or "verified motion" in n.lower()
for n in names))
def test_02_have_order_fork_requirements(self):
m = self.Motion.open_for_case(self.case, "pickup_order", True)
names = m.attachment_ids.mapped("name")
self.assertTrue(any("certified copy" in n.lower() for n in names))
def test_03_forks_differ(self):
m_no = self.Motion.open_for_case(self.case, "pickup_order", False)
m_yes = self.Motion.open_for_case(self.case, "pickup_order", True)
self.assertNotEqual(
set(m_no.attachment_ids.mapped("name")),
set(m_yes.attachment_ids.mapped("name")),
)
# --- missing-attachment block -------------------------------------------
def test_04_missing_blocks_ready(self):
m = self.Motion.open_for_case(self.case, "pickup_order", True)
# nothing provided yet
self.assertGreater(m.missing_required_count, 0)
with self.assertRaises(UserError):
m.action_mark_ready()
self.assertEqual(m.state, "draft")
def test_05_partial_still_blocks(self):
m = self.Motion.open_for_case(self.case, "pickup_order", True)
m.attachment_ids[0].provided = True
if len(m.attachment_ids) > 1:
with self.assertRaises(UserError):
m.action_mark_ready()
def test_06_complete_allows_ready(self):
m = self.Motion.open_for_case(self.case, "pickup_order", True)
m.attachment_ids.write({"provided": True})
m.action_mark_ready()
self.assertEqual(m.state, "ready")
self.assertEqual(m.missing_required_count, 0)
def test_07_file_requires_ready(self):
m = self.Motion.open_for_case(self.case, "pickup_order", True)
with self.assertRaises(UserError):
m.action_file()
m.attachment_ids.write({"provided": True})
m.action_mark_ready()
m.action_file()
self.assertEqual(m.state, "filed")
def test_08_no_checklist_blocks(self):
m = self.Motion.create({
"proceeding_id": self.case.proceeding_ids[0].id,
"motion_type": "pickup_order",
})
# remove any seeded items
m.attachment_ids.unlink()
with self.assertRaises(UserError):
m.action_mark_ready()
# --- fast-path intake ---------------------------------------------------
def test_09_intake_fast_path_then_motion(self):
wiz = self.env["familylaw.intake.wizard"].create({
"caller_name": "Panicked Parent",
"urgency_child_withheld": True,
})
self.assertTrue(wiz.is_emergency)
wiz._create_emergency_case()
case = wiz.created_case_id
self.assertTrue(case.is_emergency)
# emergency motion on the fast-path matter
m = self.Motion.open_for_case(case, "pickup_order", False)
self.assertEqual(m.case_id, case)
self.assertTrue(m.attachment_ids)
def test_10_motion_uses_open_proceeding(self):
m = self.Motion.open_for_case(self.case, "prevent_removal", False)
self.assertEqual(m.proceeding_id.state, "open")
self.assertEqual(m.case_id, self.case)

View File

@@ -55,6 +55,11 @@
string="Open Support Modification"
context="{'default_case_id': id}"
invisible="case_type != 'support_modification'"/>
<!-- Open an emergency motion (fast-path) -->
<button name="%(action_familylaw_emergency_wizard)d" type="action"
string="Emergency Motion" class="btn-danger"
context="{'default_case_id': id}"
invisible="not is_emergency"/>
<!-- Attorney-only: close / reopen -->
<button name="action_close" type="object" string="Close Case"
invisible="state == 'closed'"

View File

@@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_familylaw_emergency_list" model="ir.ui.view">
<field name="name">familylaw.emergency.motion.list</field>
<field name="model">familylaw.emergency.motion</field>
<field name="arch" type="xml">
<list string="Emergency Motions"
decoration-danger="missing_required_count &gt; 0"
decoration-success="state == 'filed'">
<field name="name"/>
<field name="case_id"/>
<field name="motion_type"/>
<field name="have_existing_order"/>
<field name="missing_required_count"/>
<field name="state" widget="badge"/>
</list>
</field>
</record>
<record id="view_familylaw_emergency_form" model="ir.ui.view">
<field name="name">familylaw.emergency.motion.form</field>
<field name="model">familylaw.emergency.motion</field>
<field name="arch" type="xml">
<form string="Emergency Motion">
<header>
<button name="action_seed_requirements" type="object"
string="Seed Requirements"/>
<button name="action_mark_ready" type="object"
string="Mark Ready" class="btn-primary"
invisible="state != 'draft'"/>
<button name="action_file" type="object"
string="File" invisible="state != 'ready'"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,ready,filed"/>
</header>
<sheet>
<div class="alert alert-danger" role="alert"
invisible="missing_required_count == 0">
<field name="missing_required_count" readonly="1" nolabel="1"/>
required attachment(s) missing — the motion is blocked from
filing until complete.
</div>
<div class="oe_title">
<label for="name"/>
<h1><field name="name"/></h1>
</div>
<group>
<group string="Motion">
<field name="motion_type"/>
<field name="have_existing_order"/>
<field name="existing_order_reference"
invisible="not have_existing_order"/>
<field name="child_id"/>
</group>
<group string="Matter">
<field name="proceeding_id"/>
<field name="case_id" readonly="1"/>
</group>
</group>
<group string="Sworn Basis (no existing order)"
invisible="have_existing_order">
<field name="basis" nolabel="1"/>
</group>
<notebook>
<page string="Required Attachments" name="attachments">
<field name="attachment_ids">
<list editable="bottom">
<field name="name"/>
<field name="is_required"/>
<field name="provided" widget="boolean_toggle"/>
</list>
</field>
</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_emergency" model="ir.actions.act_window">
<field name="name">Emergency Motions</field>
<field name="res_model">familylaw.emergency.motion</field>
<field name="view_mode">list,form</field>
</record>
<!-- Wizard -->
<record id="view_familylaw_emergency_wizard_form" model="ir.ui.view">
<field name="name">familylaw.emergency.wizard.form</field>
<field name="model">familylaw.emergency.wizard</field>
<field name="arch" type="xml">
<form string="Open Emergency Motion">
<sheet>
<group>
<field name="case_id"/>
<field name="motion_type"/>
<field name="have_existing_order"/>
<field name="existing_order_reference"
invisible="not have_existing_order"/>
<field name="child_id"/>
<field name="basis" invisible="have_existing_order"/>
</group>
</sheet>
<footer>
<button name="action_open" type="object"
string="Open Motion" class="btn-primary"/>
<button string="Cancel" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_familylaw_emergency_wizard" model="ir.actions.act_window">
<field name="name">Open Emergency Motion</field>
<field name="res_model">familylaw.emergency.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

View File

@@ -41,6 +41,13 @@
action="action_familylaw_affidavit"
sequence="40"/>
<!-- Emergency Motions -->
<menuitem id="menu_familylaw_emergency"
name="Emergency Motions"
parent="menu_familylaw_root"
action="action_familylaw_emergency"
sequence="43"/>
<!-- Support Modifications -->
<menuitem id="menu_familylaw_modifications"
name="Support Modifications"