Step 4: deadline engine (per-proceeding clocks, weekend roll, overdue cron, calendar mirror)
familylaw.deadline — a procedural clock attached to a PROCEEDING: - Deterministic due_date: trigger_date + days, with Rule-2.514-style weekend roll-forward (_roll_forward); _holiday_dates() hook left empty by design (holidays are jurisdiction/year-specific — content-maintenance concern) - STANDARD_OFFSETS (answer 20 / disclosure 45 / discovery 30 / objection 10) as defaults; per-record `days` override. Flagged "verify current rule". - state pending/done/waived/overdue; is_overdue computed + searchable - _cron_flag_overdue daily ir.cron flips pending+past -> overdue, audited - calendar.event mirror auto-created/updated on date changes (allday) - proceeding.deadline_ids + action_seed_standard_deadlines (idempotent) Adds 'calendar' dependency, data/familylaw_cron.xml, deadline views + menu, Deadlines tab + Seed button on proceeding form, security rules. Tests (familylaw_step4): 20 tests with FIXED dates — 20/45/30-day math, weekend roll (Sat/Sun -> Mon), type-default vs explicit days, overdue detect, cron flag, is_overdue search, calendar mirror create+update, per-proceeding isolation, idempotent seeding. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
"name": "Active Blue Family Law",
|
||||
"version": "18.0.3.0.0",
|
||||
"version": "18.0.4.0.0",
|
||||
"category": "Services/Legal",
|
||||
"summary": "Florida family law case management (Miami-Dade / 11th Judicial Circuit)",
|
||||
"description": """
|
||||
@@ -9,7 +9,8 @@ Active Blue Family Law
|
||||
======================
|
||||
Case-management platform for a Florida family-law practice, built in verifiable
|
||||
steps. Step 1: case spine. Step 2: parties/children/issues/proceedings, conflict
|
||||
screening, intake. Step 3: documents + the attorney review gate (Gate 1).
|
||||
screening, intake. Step 3: documents + review gate. Step 4: deadline engine
|
||||
(per-proceeding clocks, weekend roll, overdue cron, calendar mirror).
|
||||
|
||||
Each step adds one vertical, independently testable slice. See BUILD_PLAN.md.
|
||||
""",
|
||||
@@ -19,15 +20,18 @@ Each step adds one vertical, independently testable slice. See BUILD_PLAN.md.
|
||||
"depends": [
|
||||
"base",
|
||||
"mail",
|
||||
"calendar",
|
||||
],
|
||||
"data": [
|
||||
"security/familylaw_security.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"data/familylaw_cron.xml",
|
||||
"views/familylaw_party_views.xml",
|
||||
"views/familylaw_child_views.xml",
|
||||
"views/familylaw_issue_views.xml",
|
||||
"views/familylaw_proceeding_views.xml",
|
||||
"views/familylaw_document_views.xml",
|
||||
"views/familylaw_deadline_views.xml",
|
||||
"views/familylaw_intake_views.xml",
|
||||
"views/familylaw_case_views.xml",
|
||||
"views/familylaw_menus.xml",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Daily: flag pending deadlines whose due date has passed. -->
|
||||
<record id="cron_flag_overdue_deadlines" model="ir.cron">
|
||||
<field name="name">Family Law: Flag Overdue Deadlines</field>
|
||||
<field name="model_id" ref="model_familylaw_deadline"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_flag_overdue()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -6,3 +6,4 @@ from . import familylaw_proceeding
|
||||
from . import familylaw_conflict
|
||||
from . import familylaw_intake
|
||||
from . import familylaw_document
|
||||
from . import familylaw_deadline
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""STEP 4 — Deadline engine.
|
||||
|
||||
A family-law deadline is a statutory/procedural clock. It attaches to a
|
||||
PROCEEDING (locked decision — so the 2024 dissolution's disclosure clock never
|
||||
collides with a 2027 modification's), computes its due date deterministically with
|
||||
Florida-style weekend roll-forward, mirrors itself onto the shared calendar, and is
|
||||
flagged overdue by a daily cron.
|
||||
|
||||
DATE-MATH NOTE (verify current rule at build time):
|
||||
Florida Rule of Judicial Administration 2.514 computes day-periods by excluding
|
||||
the triggering day, counting every following day, and — if the last day falls on a
|
||||
Saturday, Sunday, or legal holiday — rolling forward to the next day that is none
|
||||
of those. We implement the exclude-trigger + weekend roll here. LEGAL HOLIDAYS are
|
||||
jurisdiction- and year-specific and are intentionally NOT hardcoded; see
|
||||
_holiday_dates() for the hook. Standard day-counts (20/45/30/10) are common values
|
||||
and MUST be confirmed against the current rule for each matter type.
|
||||
"""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
# Common Florida day-counts. VERIFY CURRENT RULE per matter type — do not treat as
|
||||
# permanent. Used as defaults; the `days` field can override per record.
|
||||
STANDARD_OFFSETS = {
|
||||
"answer": 20, # Respondent's answer window (Rule 12.140 family practice)
|
||||
"disclosure": 45, # Mandatory disclosure (Rule 12.285)
|
||||
"discovery": 30, # Interrogatory / production responses (Rule 12.340/12.350)
|
||||
"objection": 10, # Subpoena objection window (Step 8)
|
||||
}
|
||||
|
||||
|
||||
class FamilyLawDeadline(models.Model):
|
||||
_name = "familylaw.deadline"
|
||||
_description = "Procedural Deadline"
|
||||
_inherit = ["mail.thread"]
|
||||
_order = "due_date asc"
|
||||
|
||||
name = fields.Char(required=True, tracking=True)
|
||||
proceeding_id = fields.Many2one(
|
||||
"familylaw.proceeding",
|
||||
string="Proceeding",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
index=True,
|
||||
tracking=True,
|
||||
help="Deadlines attach to a proceeding, not directly to the case.",
|
||||
)
|
||||
case_id = fields.Many2one(
|
||||
"familylaw.case",
|
||||
related="proceeding_id.case_id",
|
||||
store=True,
|
||||
index=True,
|
||||
)
|
||||
deadline_type = fields.Selection(
|
||||
selection=[
|
||||
("answer", "Answer / Response"),
|
||||
("disclosure", "Mandatory Disclosure"),
|
||||
("discovery", "Discovery Response"),
|
||||
("objection", "Objection Window"),
|
||||
("hearing", "Hearing"),
|
||||
("custom", "Custom"),
|
||||
],
|
||||
string="Type",
|
||||
required=True,
|
||||
default="custom",
|
||||
tracking=True,
|
||||
)
|
||||
trigger_date = fields.Date(
|
||||
string="Trigger Date",
|
||||
required=True,
|
||||
tracking=True,
|
||||
help="The event that starts the clock (e.g. date of service). Excluded from "
|
||||
"the count per Rule 2.514.",
|
||||
)
|
||||
days = fields.Integer(
|
||||
string="Days",
|
||||
required=True,
|
||||
default=0,
|
||||
tracking=True,
|
||||
help="Number of days in the period. Defaults from the type but can be "
|
||||
"overridden. Verify the current rule.",
|
||||
)
|
||||
due_date = fields.Date(
|
||||
string="Due Date",
|
||||
compute="_compute_due_date",
|
||||
store=True,
|
||||
tracking=True,
|
||||
)
|
||||
state = fields.Selection(
|
||||
selection=[
|
||||
("pending", "Pending"),
|
||||
("done", "Completed"),
|
||||
("waived", "Waived"),
|
||||
("overdue", "Overdue"),
|
||||
],
|
||||
default="pending",
|
||||
required=True,
|
||||
copy=False,
|
||||
tracking=True,
|
||||
)
|
||||
is_overdue = fields.Boolean(
|
||||
compute="_compute_is_overdue",
|
||||
search="_search_is_overdue",
|
||||
string="Overdue?",
|
||||
)
|
||||
calendar_event_id = fields.Many2one(
|
||||
"calendar.event",
|
||||
string="Calendar Entry",
|
||||
copy=False,
|
||||
readonly=True,
|
||||
)
|
||||
notes = fields.Text()
|
||||
|
||||
# --- date math ----------------------------------------------------------
|
||||
def _holiday_dates(self):
|
||||
"""Hook for legal holidays. Intentionally empty — holidays are
|
||||
jurisdiction/year specific and must be supplied by the content-maintenance
|
||||
process (Requirements for Success #7). Return a set of date objects."""
|
||||
return set()
|
||||
|
||||
def _roll_forward(self, due):
|
||||
"""Roll a due date off weekends (and holidays, when supplied)."""
|
||||
holidays = self._holiday_dates()
|
||||
while due.weekday() >= 5 or due in holidays:
|
||||
due += timedelta(days=1)
|
||||
return due
|
||||
|
||||
@api.depends("trigger_date", "days")
|
||||
def _compute_due_date(self):
|
||||
for dl in self:
|
||||
if not dl.trigger_date or not dl.days:
|
||||
dl.due_date = dl.trigger_date or False
|
||||
continue
|
||||
raw = dl.trigger_date + timedelta(days=dl.days)
|
||||
dl.due_date = dl._roll_forward(raw)
|
||||
|
||||
@api.depends("due_date", "state")
|
||||
def _compute_is_overdue(self):
|
||||
today = fields.Date.context_today(self)
|
||||
for dl in self:
|
||||
dl.is_overdue = bool(
|
||||
dl.due_date
|
||||
and dl.state == "pending"
|
||||
and dl.due_date < today
|
||||
)
|
||||
|
||||
def _search_is_overdue(self, operator, value):
|
||||
if operator not in ("=", "!="):
|
||||
raise ValueError(_("Unsupported operator for is_overdue search."))
|
||||
today = fields.Date.context_today(self)
|
||||
want_overdue = (operator == "=" and value) or (operator == "!=" and not value)
|
||||
if want_overdue:
|
||||
return [("state", "=", "pending"), ("due_date", "<", today)]
|
||||
return ["|", ("state", "!=", "pending"), ("due_date", ">=", today)]
|
||||
|
||||
# --- defaults from type -------------------------------------------------
|
||||
@api.onchange("deadline_type")
|
||||
def _onchange_deadline_type(self):
|
||||
if self.deadline_type in STANDARD_OFFSETS and not self.days:
|
||||
self.days = STANDARD_OFFSETS[self.deadline_type]
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if not vals.get("days") and vals.get("deadline_type") in STANDARD_OFFSETS:
|
||||
vals["days"] = STANDARD_OFFSETS[vals["deadline_type"]]
|
||||
records = super().create(vals_list)
|
||||
records._sync_calendar_event()
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if {"name", "due_date", "trigger_date", "days", "state"} & set(vals):
|
||||
self._sync_calendar_event()
|
||||
return res
|
||||
|
||||
# --- calendar mirror ----------------------------------------------------
|
||||
def _sync_calendar_event(self):
|
||||
Cal = self.env["calendar.event"]
|
||||
for dl in self:
|
||||
if not dl.due_date:
|
||||
continue
|
||||
title = _("[Deadline] %(name)s — %(matter)s",
|
||||
name=dl.name, matter=dl.case_id.name or "")
|
||||
vals = {
|
||||
"name": title,
|
||||
"allday": True,
|
||||
"start_date": dl.due_date,
|
||||
"stop_date": dl.due_date,
|
||||
}
|
||||
if dl.calendar_event_id:
|
||||
dl.calendar_event_id.write(vals)
|
||||
else:
|
||||
dl.calendar_event_id = Cal.create(vals).id
|
||||
|
||||
# --- actions ------------------------------------------------------------
|
||||
def action_mark_done(self):
|
||||
for dl in self:
|
||||
dl.state = "done"
|
||||
dl.message_post(body=_("Deadline completed."))
|
||||
return True
|
||||
|
||||
def action_waive(self):
|
||||
for dl in self:
|
||||
dl.state = "waived"
|
||||
dl.message_post(body=_("Deadline waived."))
|
||||
return True
|
||||
|
||||
def action_reset_pending(self):
|
||||
for dl in self:
|
||||
dl.state = "pending"
|
||||
dl.message_post(body=_("Deadline reset to pending."))
|
||||
return True
|
||||
|
||||
# --- cron ---------------------------------------------------------------
|
||||
@api.model
|
||||
def _cron_flag_overdue(self):
|
||||
"""Daily: flag pending deadlines whose due date has passed."""
|
||||
today = fields.Date.context_today(self)
|
||||
overdue = self.search([
|
||||
("state", "=", "pending"),
|
||||
("due_date", "<", today),
|
||||
])
|
||||
for dl in overdue:
|
||||
dl.state = "overdue"
|
||||
dl.message_post(
|
||||
body=_("Deadline is OVERDUE (due %s) — flagged by the deadline engine.")
|
||||
% dl.due_date
|
||||
)
|
||||
return len(overdue)
|
||||
@@ -57,6 +57,34 @@ class FamilyLawProceeding(models.Model):
|
||||
document_ids = fields.One2many(
|
||||
"familylaw.document", "proceeding_id", string="Documents",
|
||||
)
|
||||
deadline_ids = fields.One2many(
|
||||
"familylaw.deadline", "proceeding_id", string="Deadlines",
|
||||
)
|
||||
|
||||
def action_seed_standard_deadlines(self):
|
||||
"""Create the common procedural deadlines for this proceeding, dated from
|
||||
today. The day-counts come from familylaw.deadline.STANDARD_OFFSETS and MUST
|
||||
be confirmed against the current rule — see that module's date-math note."""
|
||||
Deadline = self.env["familylaw.deadline"]
|
||||
today = fields.Date.context_today(self)
|
||||
labels = {
|
||||
"answer": "Answer / Response due",
|
||||
"disclosure": "Mandatory Disclosure due (Rule 12.285)",
|
||||
"discovery": "Discovery responses due",
|
||||
}
|
||||
for proc in self:
|
||||
existing = set(proc.deadline_ids.mapped("deadline_type"))
|
||||
for dtype, label in labels.items():
|
||||
if dtype in existing:
|
||||
continue
|
||||
Deadline.create({
|
||||
"proceeding_id": proc.id,
|
||||
"name": label,
|
||||
"deadline_type": dtype,
|
||||
"trigger_date": today,
|
||||
})
|
||||
proc.message_post(body="Standard deadlines seeded.")
|
||||
return True
|
||||
|
||||
def action_close_proceeding(self):
|
||||
for proc in self:
|
||||
|
||||
@@ -14,3 +14,5 @@ access_familylaw_conflict_hit_attorney,familylaw.conflict.hit attorney,model_fam
|
||||
access_familylaw_intake_wizard_user,familylaw.intake.wizard user,model_familylaw_intake_wizard,base.group_user,1,1,1,1
|
||||
access_familylaw_document_user,familylaw.document staff,model_familylaw_document,group_familylaw_user,1,1,1,0
|
||||
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
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
from . import test_case_lifecycle
|
||||
from . import test_step2
|
||||
from . import test_step3
|
||||
from . import test_step4
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""STEP 4 tests — deadline engine.
|
||||
|
||||
Run just this step:
|
||||
odoo -d <db> -u activeblue_familylaw --test-enable \
|
||||
--test-tags familylaw_step4 --stop-after-init
|
||||
|
||||
Date math is tested with FIXED dates (never today()) for determinism. Overdue
|
||||
behaviour is tested with dates safely in the past/future relative to any run date.
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install", "familylaw", "familylaw_step4")
|
||||
class TestStep4DeadlineEngine(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.partner = cls.env["res.partner"].create({"name": "Deadline Client"})
|
||||
cls.case = cls.env["familylaw.case"].create({
|
||||
"name": "Deadline Matter",
|
||||
"client_id": cls.partner.id,
|
||||
"case_type": "support_modification",
|
||||
})
|
||||
cls.proc = cls.case.proceeding_ids[0]
|
||||
cls.Deadline = cls.env["familylaw.deadline"]
|
||||
|
||||
def _mk(self, **kw):
|
||||
vals = {
|
||||
"proceeding_id": self.proc.id,
|
||||
"name": "Test Deadline",
|
||||
"deadline_type": "custom",
|
||||
"trigger_date": date(2025, 1, 1),
|
||||
"days": 20,
|
||||
}
|
||||
vals.update(kw)
|
||||
return self.Deadline.create(vals)
|
||||
|
||||
# --- basic math (exclude trigger day, add N days) -----------------------
|
||||
def test_01_twenty_day_math(self):
|
||||
# 2025-01-01 (Wed) + 20 days = 2025-01-21 (Tue), no weekend roll needed
|
||||
dl = self._mk(trigger_date=date(2025, 1, 1), days=20)
|
||||
self.assertEqual(dl.due_date, date(2025, 1, 21))
|
||||
|
||||
def test_02_forty_five_day_math(self):
|
||||
# 2025-01-01 + 45 = 2025-02-15 (Sat) -> roll to 2025-02-17 (Mon)
|
||||
dl = self._mk(trigger_date=date(2025, 1, 1), days=45)
|
||||
self.assertEqual(dl.due_date, date(2025, 2, 17))
|
||||
|
||||
def test_03_thirty_day_math(self):
|
||||
# 2025-01-01 + 30 = 2025-01-31 (Fri), no roll
|
||||
dl = self._mk(trigger_date=date(2025, 1, 1), days=30)
|
||||
self.assertEqual(dl.due_date, date(2025, 1, 31))
|
||||
|
||||
# --- weekend roll-forward -----------------------------------------------
|
||||
def test_04_saturday_rolls_to_monday(self):
|
||||
# 2025-03-07 (Fri) + 1 = 2025-03-08 (Sat) -> 2025-03-10 (Mon)
|
||||
dl = self._mk(trigger_date=date(2025, 3, 7), days=1)
|
||||
self.assertEqual(dl.due_date, date(2025, 3, 10))
|
||||
|
||||
def test_05_sunday_rolls_to_monday(self):
|
||||
# 2025-03-07 (Fri) + 2 = 2025-03-09 (Sun) -> 2025-03-10 (Mon)
|
||||
dl = self._mk(trigger_date=date(2025, 3, 7), days=2)
|
||||
self.assertEqual(dl.due_date, date(2025, 3, 10))
|
||||
|
||||
def test_06_weekday_no_roll(self):
|
||||
# 2025-03-07 (Fri) + 4 = 2025-03-11 (Tue), no roll
|
||||
dl = self._mk(trigger_date=date(2025, 3, 7), days=4)
|
||||
self.assertEqual(dl.due_date, date(2025, 3, 11))
|
||||
|
||||
# --- standard offsets default from type ---------------------------------
|
||||
def test_07_answer_default_days(self):
|
||||
dl = self.Deadline.create({
|
||||
"proceeding_id": self.proc.id,
|
||||
"name": "Answer due",
|
||||
"deadline_type": "answer",
|
||||
"trigger_date": date(2025, 1, 1),
|
||||
})
|
||||
self.assertEqual(dl.days, 20)
|
||||
|
||||
def test_08_disclosure_default_days(self):
|
||||
dl = self.Deadline.create({
|
||||
"proceeding_id": self.proc.id,
|
||||
"name": "Disclosure due",
|
||||
"deadline_type": "disclosure",
|
||||
"trigger_date": date(2025, 1, 1),
|
||||
})
|
||||
self.assertEqual(dl.days, 45)
|
||||
|
||||
def test_09_explicit_days_overrides_default(self):
|
||||
dl = self.Deadline.create({
|
||||
"proceeding_id": self.proc.id,
|
||||
"name": "Custom answer",
|
||||
"deadline_type": "answer",
|
||||
"trigger_date": date(2025, 1, 1),
|
||||
"days": 5,
|
||||
})
|
||||
self.assertEqual(dl.days, 5)
|
||||
|
||||
# --- overdue detection --------------------------------------------------
|
||||
def test_10_past_deadline_is_overdue(self):
|
||||
dl = self._mk(trigger_date=date(2000, 1, 1), days=20)
|
||||
self.assertTrue(dl.is_overdue)
|
||||
|
||||
def test_11_future_deadline_not_overdue(self):
|
||||
dl = self._mk(trigger_date=date(2999, 1, 1), days=20)
|
||||
self.assertFalse(dl.is_overdue)
|
||||
|
||||
def test_12_done_deadline_not_overdue(self):
|
||||
dl = self._mk(trigger_date=date(2000, 1, 1), days=20)
|
||||
dl.action_mark_done()
|
||||
self.assertFalse(dl.is_overdue)
|
||||
self.assertEqual(dl.state, "done")
|
||||
|
||||
def test_13_cron_flags_overdue(self):
|
||||
dl = self._mk(trigger_date=date(2000, 1, 1), days=20)
|
||||
self.assertEqual(dl.state, "pending")
|
||||
self.Deadline._cron_flag_overdue()
|
||||
self.assertEqual(dl.state, "overdue")
|
||||
|
||||
def test_14_cron_ignores_future(self):
|
||||
dl = self._mk(trigger_date=date(2999, 1, 1), days=20)
|
||||
self.Deadline._cron_flag_overdue()
|
||||
self.assertEqual(dl.state, "pending")
|
||||
|
||||
def test_15_is_overdue_search(self):
|
||||
past = self._mk(trigger_date=date(2000, 1, 1), days=20)
|
||||
self._mk(trigger_date=date(2999, 1, 1), days=20)
|
||||
found = self.Deadline.search([
|
||||
("is_overdue", "=", True),
|
||||
("proceeding_id", "=", self.proc.id),
|
||||
])
|
||||
self.assertIn(past, found)
|
||||
|
||||
# --- calendar mirror ----------------------------------------------------
|
||||
def test_16_calendar_event_created(self):
|
||||
dl = self._mk()
|
||||
self.assertTrue(dl.calendar_event_id)
|
||||
self.assertEqual(dl.calendar_event_id.start_date, dl.due_date)
|
||||
|
||||
def test_17_calendar_event_updates_on_date_change(self):
|
||||
dl = self._mk(trigger_date=date(2025, 1, 1), days=20)
|
||||
first = dl.calendar_event_id.start_date
|
||||
dl.days = 30
|
||||
self.assertNotEqual(dl.calendar_event_id.start_date, first)
|
||||
self.assertEqual(dl.calendar_event_id.start_date, dl.due_date)
|
||||
|
||||
# --- per-proceeding isolation -------------------------------------------
|
||||
def test_18_deadlines_isolated_per_proceeding(self):
|
||||
proc2 = self.env["familylaw.proceeding"].create({
|
||||
"case_id": self.case.id,
|
||||
"name": "Second Proceeding",
|
||||
"proceeding_type": "modification",
|
||||
})
|
||||
d1 = self._mk()
|
||||
d2 = self.Deadline.create({
|
||||
"proceeding_id": proc2.id,
|
||||
"name": "Other deadline",
|
||||
"deadline_type": "custom",
|
||||
"trigger_date": date(2025, 1, 1),
|
||||
"days": 10,
|
||||
})
|
||||
self.assertIn(d1, self.proc.deadline_ids)
|
||||
self.assertNotIn(d2, self.proc.deadline_ids)
|
||||
self.assertIn(d2, proc2.deadline_ids)
|
||||
|
||||
# --- seeding ------------------------------------------------------------
|
||||
def test_19_seed_standard_deadlines(self):
|
||||
self.proc.action_seed_standard_deadlines()
|
||||
types = set(self.proc.deadline_ids.mapped("deadline_type"))
|
||||
self.assertEqual(types, {"answer", "disclosure", "discovery"})
|
||||
|
||||
def test_20_seed_is_idempotent(self):
|
||||
self.proc.action_seed_standard_deadlines()
|
||||
n1 = len(self.proc.deadline_ids)
|
||||
self.proc.action_seed_standard_deadlines()
|
||||
self.assertEqual(len(self.proc.deadline_ids), n1)
|
||||
@@ -0,0 +1,112 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_familylaw_deadline_list" model="ir.ui.view">
|
||||
<field name="name">familylaw.deadline.list</field>
|
||||
<field name="model">familylaw.deadline</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Deadlines" decoration-danger="is_overdue"
|
||||
decoration-muted="state in ('done','waived')">
|
||||
<field name="name"/>
|
||||
<field name="case_id"/>
|
||||
<field name="proceeding_id"/>
|
||||
<field name="deadline_type"/>
|
||||
<field name="trigger_date"/>
|
||||
<field name="days"/>
|
||||
<field name="due_date"/>
|
||||
<field name="is_overdue" column_invisible="1"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-success="state == 'done'"
|
||||
decoration-danger="state == 'overdue'"
|
||||
decoration-info="state == 'pending'"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_familylaw_deadline_form" model="ir.ui.view">
|
||||
<field name="name">familylaw.deadline.form</field>
|
||||
<field name="model">familylaw.deadline</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Deadline">
|
||||
<header>
|
||||
<button name="action_mark_done" type="object" string="Mark Completed"
|
||||
class="btn-primary" invisible="state in ('done','waived')"/>
|
||||
<button name="action_waive" type="object" string="Waive"
|
||||
invisible="state in ('done','waived')"/>
|
||||
<button name="action_reset_pending" type="object" string="Reset"
|
||||
invisible="state == 'pending'"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="pending,done"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="alert alert-danger" role="alert" invisible="not is_overdue">
|
||||
This deadline is OVERDUE.
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="e.g. Answer to Petition due"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group string="Clock">
|
||||
<field name="deadline_type"/>
|
||||
<field name="trigger_date"/>
|
||||
<field name="days"/>
|
||||
<field name="due_date" readonly="1"/>
|
||||
</group>
|
||||
<group string="Matter">
|
||||
<field name="proceeding_id"/>
|
||||
<field name="case_id" readonly="1"/>
|
||||
<field name="calendar_event_id" readonly="1"/>
|
||||
<field name="is_overdue" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Notes">
|
||||
<field name="notes" nolabel="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="activity_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_familylaw_deadline_search" model="ir.ui.view">
|
||||
<field name="name">familylaw.deadline.search</field>
|
||||
<field name="model">familylaw.deadline</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Deadlines">
|
||||
<field name="name"/>
|
||||
<field name="case_id"/>
|
||||
<field name="proceeding_id"/>
|
||||
<filter name="overdue" string="Overdue"
|
||||
domain="[('is_overdue','=',True)]"/>
|
||||
<filter name="pending" string="Pending"
|
||||
domain="[('state','=','pending')]"/>
|
||||
<separator/>
|
||||
<filter name="this_week" string="Due Within 7 Days"
|
||||
domain="[('due_date','>=', context_today()),
|
||||
('due_date','<=', context_today() + relativedelta(days=7))]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="group_state" string="Status"
|
||||
context="{'group_by':'state'}"/>
|
||||
<filter name="group_type" string="Type"
|
||||
context="{'group_by':'deadline_type'}"/>
|
||||
<filter name="group_case" string="Matter"
|
||||
context="{'group_by':'case_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_familylaw_deadline" model="ir.actions.act_window">
|
||||
<field name="name">Deadlines</field>
|
||||
<field name="res_model">familylaw.deadline</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_familylaw_deadline_search"/>
|
||||
<field name="context">{'search_default_pending': 1}</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -27,6 +27,13 @@
|
||||
action="action_familylaw_document"
|
||||
sequence="20"/>
|
||||
|
||||
<!-- Deadlines -->
|
||||
<menuitem id="menu_familylaw_deadlines"
|
||||
name="Deadlines"
|
||||
parent="menu_familylaw_root"
|
||||
action="action_familylaw_deadline"
|
||||
sequence="30"/>
|
||||
|
||||
<!-- Configuration placeholder (populated in later steps) -->
|
||||
<menuitem id="menu_familylaw_config"
|
||||
name="Configuration"
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Proceeding">
|
||||
<header>
|
||||
<button name="action_seed_standard_deadlines" type="object"
|
||||
string="Seed Standard Deadlines"/>
|
||||
<button name="action_close_proceeding" type="object"
|
||||
string="Close Proceeding"
|
||||
invisible="state == 'closed'"/>
|
||||
@@ -65,6 +67,21 @@
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Deadlines" name="deadlines">
|
||||
<field name="deadline_ids"
|
||||
context="{'default_proceeding_id': id}">
|
||||
<list decoration-danger="is_overdue"
|
||||
decoration-muted="state in ('done','waived')">
|
||||
<field name="name"/>
|
||||
<field name="deadline_type"/>
|
||||
<field name="trigger_date"/>
|
||||
<field name="days"/>
|
||||
<field name="due_date"/>
|
||||
<field name="is_overdue" column_invisible="1"/>
|
||||
<field name="state" widget="badge"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
|
||||
Reference in New Issue
Block a user