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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
{
|
{
|
||||||
"name": "Active Blue Family Law",
|
"name": "Active Blue Family Law",
|
||||||
"version": "18.0.3.0.0",
|
"version": "18.0.4.0.0",
|
||||||
"category": "Services/Legal",
|
"category": "Services/Legal",
|
||||||
"summary": "Florida family law case management (Miami-Dade / 11th Judicial Circuit)",
|
"summary": "Florida family law case management (Miami-Dade / 11th Judicial Circuit)",
|
||||||
"description": """
|
"description": """
|
||||||
@@ -9,7 +9,8 @@ Active Blue Family Law
|
|||||||
======================
|
======================
|
||||||
Case-management platform for a Florida family-law practice, built in verifiable
|
Case-management platform for a Florida family-law practice, built in verifiable
|
||||||
steps. Step 1: case spine. Step 2: parties/children/issues/proceedings, conflict
|
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.
|
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": [
|
"depends": [
|
||||||
"base",
|
"base",
|
||||||
"mail",
|
"mail",
|
||||||
|
"calendar",
|
||||||
],
|
],
|
||||||
"data": [
|
"data": [
|
||||||
"security/familylaw_security.xml",
|
"security/familylaw_security.xml",
|
||||||
"security/ir.model.access.csv",
|
"security/ir.model.access.csv",
|
||||||
|
"data/familylaw_cron.xml",
|
||||||
"views/familylaw_party_views.xml",
|
"views/familylaw_party_views.xml",
|
||||||
"views/familylaw_child_views.xml",
|
"views/familylaw_child_views.xml",
|
||||||
"views/familylaw_issue_views.xml",
|
"views/familylaw_issue_views.xml",
|
||||||
"views/familylaw_proceeding_views.xml",
|
"views/familylaw_proceeding_views.xml",
|
||||||
"views/familylaw_document_views.xml",
|
"views/familylaw_document_views.xml",
|
||||||
|
"views/familylaw_deadline_views.xml",
|
||||||
"views/familylaw_intake_views.xml",
|
"views/familylaw_intake_views.xml",
|
||||||
"views/familylaw_case_views.xml",
|
"views/familylaw_case_views.xml",
|
||||||
"views/familylaw_menus.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_conflict
|
||||||
from . import familylaw_intake
|
from . import familylaw_intake
|
||||||
from . import familylaw_document
|
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(
|
document_ids = fields.One2many(
|
||||||
"familylaw.document", "proceeding_id", string="Documents",
|
"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):
|
def action_close_proceeding(self):
|
||||||
for proc in 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_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_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_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_case_lifecycle
|
||||||
from . import test_step2
|
from . import test_step2
|
||||||
from . import test_step3
|
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"
|
action="action_familylaw_document"
|
||||||
sequence="20"/>
|
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) -->
|
<!-- Configuration placeholder (populated in later steps) -->
|
||||||
<menuitem id="menu_familylaw_config"
|
<menuitem id="menu_familylaw_config"
|
||||||
name="Configuration"
|
name="Configuration"
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="Proceeding">
|
<form string="Proceeding">
|
||||||
<header>
|
<header>
|
||||||
|
<button name="action_seed_standard_deadlines" type="object"
|
||||||
|
string="Seed Standard Deadlines"/>
|
||||||
<button name="action_close_proceeding" type="object"
|
<button name="action_close_proceeding" type="object"
|
||||||
string="Close Proceeding"
|
string="Close Proceeding"
|
||||||
invisible="state == 'closed'"/>
|
invisible="state == 'closed'"/>
|
||||||
@@ -65,6 +67,21 @@
|
|||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</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>
|
</notebook>
|
||||||
</sheet>
|
</sheet>
|
||||||
<div class="oe_chatter">
|
<div class="oe_chatter">
|
||||||
|
|||||||
Reference in New Issue
Block a user