From 91d4cec0e0b7dfac5e3f54e590f73e7a49eb6089 Mon Sep 17 00:00:00 2001 From: tocmo0nlord Date: Tue, 2 Jun 2026 04:21:39 +0000 Subject: [PATCH] =?UTF-8?q?Step=2010:=20emergency=20workflow=20(12.941=20p?= =?UTF-8?q?ick-up/removal)=20=E2=80=94=20fork=20+=20attachment=20block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../activeblue_familylaw/__manifest__.py | 3 +- .../activeblue_familylaw/models/__init__.py | 2 + .../models/familylaw_emergency.py | 172 ++++++++++++++++++ .../models/familylaw_emergency_wizard.py | 41 +++++ .../security/ir.model.access.csv | 5 + .../activeblue_familylaw/tests/__init__.py | 1 + .../activeblue_familylaw/tests/test_step10.py | 112 ++++++++++++ .../views/familylaw_case_views.xml | 5 + .../views/familylaw_emergency_views.xml | 125 +++++++++++++ .../views/familylaw_menus.xml | 7 + 10 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_emergency.py create mode 100644 activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_emergency_wizard.py create mode 100644 activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/test_step10.py create mode 100644 activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_emergency_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 27bbcd3..3807c3f 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.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", 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 ec42ca5..55b26cb 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 @@ -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 diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_emergency.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_emergency.py new file mode 100644 index 0000000..c76320c --- /dev/null +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_emergency.py @@ -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) diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_emergency_wizard.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_emergency_wizard.py new file mode 100644 index 0000000..426984a --- /dev/null +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_emergency_wizard.py @@ -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", + } 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 661f796..43329c0 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 @@ -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 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 1f34c63..648482c 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 @@ -7,3 +7,4 @@ from . import test_step6 from . import test_step7 from . import test_step8 from . import test_step9 +from . import test_step10 diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/test_step10.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/test_step10.py new file mode 100644 index 0000000..9bba6bb --- /dev/null +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/test_step10.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +"""STEP 10 tests — emergency workflow (12.941 pick-up/removal). + + odoo -d -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) 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 7f1c989..f5ed775 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 @@ -55,6 +55,11 @@ string="Open Support Modification" context="{'default_case_id': id}" invisible="case_type != 'support_modification'"/> + +