Step 12: file archive (SHA-256) + retention lifecycle + calendar layers
familylaw.retention.class — attorney-configured retention periods (years). familylaw.archive — metadata over ir.attachment: - SHA-256 checksum computed from the attachment bytes (kept as PROOF after destruction) - destruction_eligible_date = archived + retention years; is_destruction_eligible - lifecycle retained -> eligible -> destroyed; _cron_flag_eligible daily - action_destroy (attorney-only): logs THAT destruction happened + checksum proof, unlinks the content (not retained), stamps destroyed_by/date familylaw.case (_inherit): archive_ids; action_return_client_file (attorney-only, only at matter close) records client-file return. familylaw.deadline (_inherit): calendar `layer` (statutory/hearing/internal) with per-layer search filter toggles. Archive views + Retention Classes config menu; Return Client File button on closed cases; retention cron; ACL (staff read-only on retention classes). Tests (familylaw_step12): SHA-256 from bytes; eligible-date math; eligible now/not; cron flags eligible; destroy removes content + keeps checksum + audited + attorney- only; client-file return blocked when open / works when closed; deadline layer; archive on case. 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.11.0.0",
|
||||
"version": "18.0.12.0.0",
|
||||
"category": "Services/Legal",
|
||||
"summary": "Florida family law case management (Miami-Dade / 11th Judicial Circuit)",
|
||||
"description": """
|
||||
@@ -38,6 +38,7 @@ Each step adds one vertical, independently testable slice. See BUILD_PLAN.md.
|
||||
"views/familylaw_modification_views.xml",
|
||||
"views/familylaw_emergency_views.xml",
|
||||
"views/familylaw_proceeding_views.xml",
|
||||
"views/familylaw_archive_views.xml",
|
||||
"views/familylaw_intake_views.xml",
|
||||
"views/familylaw_case_views.xml",
|
||||
"views/familylaw_menus.xml",
|
||||
|
||||
@@ -10,4 +10,15 @@
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- Daily: flag archives whose retention period has elapsed. -->
|
||||
<record id="cron_flag_eligible_archives" model="ir.cron">
|
||||
<field name="name">Family Law: Flag Destruction-Eligible Archives</field>
|
||||
<field name="model_id" ref="model_familylaw_archive"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_flag_eligible()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active" eval="True"/>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -16,6 +16,7 @@ from . import familylaw_discovery
|
||||
from . import familylaw_modification
|
||||
from . import familylaw_emergency
|
||||
from . import familylaw_docuseal
|
||||
from . import familylaw_archive
|
||||
from . import familylaw_ai_wizard
|
||||
from . import familylaw_modification_wizard
|
||||
from . import familylaw_emergency_wizard
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""STEP 12 — File archive + retention lifecycle + calendar layers.
|
||||
|
||||
Three concerns:
|
||||
* ARCHIVE — metadata over ir.attachment with a SHA-256 checksum and a retention
|
||||
class. Content lives behind ir.attachment (MinIO/Synology in production).
|
||||
* RETENTION — attorney-configured retention classes drive a closed-matter
|
||||
destruction lifecycle: retained -> eligible -> destroyed. The audit trail records
|
||||
THAT destruction happened (and the checksum as proof) WITHOUT retaining the
|
||||
destroyed content. Plus client-file return at matter close.
|
||||
* CALENDAR LAYERS — a layer tag on deadlines so the calendar can be filtered by
|
||||
category (statutory / hearing / internal).
|
||||
|
||||
VERIFY current rule — Florida file-retention obligations are volatile; retention
|
||||
periods must be confirmed and configured by the attorney, not hardcoded.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
ATTORNEY_GROUP = "activeblue_familylaw.group_familylaw_attorney"
|
||||
|
||||
|
||||
class FamilyLawRetentionClass(models.Model):
|
||||
_name = "familylaw.retention.class"
|
||||
_description = "Retention Class"
|
||||
_order = "name"
|
||||
|
||||
name = fields.Char(required=True)
|
||||
code = fields.Char()
|
||||
retention_years = fields.Integer(
|
||||
string="Retain (years)", required=True, default=6,
|
||||
help="Years to retain after archiving / matter close. Attorney-configured; "
|
||||
"confirm the current Florida obligation.",
|
||||
)
|
||||
description = fields.Text()
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
|
||||
class FamilyLawArchive(models.Model):
|
||||
_name = "familylaw.archive"
|
||||
_description = "Archived File (with retention)"
|
||||
_inherit = ["mail.thread"]
|
||||
_order = "date_archived desc"
|
||||
|
||||
name = fields.Char(required=True, tracking=True)
|
||||
case_id = fields.Many2one("familylaw.case", index=True, tracking=True)
|
||||
proceeding_id = fields.Many2one("familylaw.proceeding", index=True)
|
||||
document_id = fields.Many2one("familylaw.document", index=True)
|
||||
attachment_id = fields.Many2one("ir.attachment", string="File", ondelete="set null")
|
||||
|
||||
checksum = fields.Char(
|
||||
compute="_compute_checksum", store=True, string="SHA-256",
|
||||
help="SHA-256 of the file content; kept as proof even after destruction.",
|
||||
)
|
||||
retention_class_id = fields.Many2one("familylaw.retention.class",
|
||||
string="Retention Class", tracking=True)
|
||||
date_archived = fields.Date(default=fields.Date.context_today, tracking=True)
|
||||
destruction_eligible_date = fields.Date(
|
||||
compute="_compute_eligible_date", store=True,
|
||||
)
|
||||
is_destruction_eligible = fields.Boolean(
|
||||
compute="_compute_is_eligible", string="Eligible Now?",
|
||||
)
|
||||
retention_state = fields.Selection(
|
||||
selection=[
|
||||
("retained", "Retained"),
|
||||
("eligible", "Eligible for Destruction"),
|
||||
("destroyed", "Destroyed"),
|
||||
],
|
||||
default="retained",
|
||||
required=True,
|
||||
tracking=True,
|
||||
)
|
||||
destroyed_date = fields.Datetime(readonly=True, copy=False)
|
||||
destroyed_by_id = fields.Many2one("res.users", readonly=True, copy=False)
|
||||
|
||||
@api.depends("attachment_id", "attachment_id.datas")
|
||||
def _compute_checksum(self):
|
||||
for a in self:
|
||||
if a.attachment_id and a.attachment_id.datas:
|
||||
raw = base64.b64decode(a.attachment_id.datas)
|
||||
a.checksum = hashlib.sha256(raw).hexdigest()
|
||||
elif not a.checksum: # preserve checksum-as-proof after destruction
|
||||
a.checksum = False
|
||||
|
||||
@api.depends("date_archived", "retention_class_id.retention_years")
|
||||
def _compute_eligible_date(self):
|
||||
for a in self:
|
||||
if a.date_archived and a.retention_class_id:
|
||||
a.destruction_eligible_date = a.date_archived + relativedelta(
|
||||
years=a.retention_class_id.retention_years)
|
||||
else:
|
||||
a.destruction_eligible_date = False
|
||||
|
||||
@api.depends("destruction_eligible_date", "retention_state")
|
||||
def _compute_is_eligible(self):
|
||||
today = fields.Date.context_today(self)
|
||||
for a in self:
|
||||
a.is_destruction_eligible = bool(
|
||||
a.retention_state == "retained"
|
||||
and a.destruction_eligible_date
|
||||
and a.destruction_eligible_date <= today
|
||||
)
|
||||
|
||||
def _ensure_attorney(self):
|
||||
if not self.env.user.has_group(ATTORNEY_GROUP):
|
||||
raise UserError(_("Only an attorney may authorize destruction."))
|
||||
|
||||
def action_destroy(self):
|
||||
"""Destroy the archived content. Records THAT destruction happened (with the
|
||||
checksum as proof) but does NOT retain the destroyed content."""
|
||||
self._ensure_attorney()
|
||||
for a in self:
|
||||
if a.retention_state == "destroyed":
|
||||
continue
|
||||
a.message_post(body=_(
|
||||
"Archived content DESTROYED per retention class '%(rc)s'. "
|
||||
"Checksum retained as proof: %(cs)s. Content not retained.",
|
||||
rc=a.retention_class_id.name or _("(none)"),
|
||||
cs=a.checksum or _("(n/a)")))
|
||||
if a.attachment_id:
|
||||
a.attachment_id.unlink()
|
||||
a.write({
|
||||
"retention_state": "destroyed",
|
||||
"destroyed_date": fields.Datetime.now(),
|
||||
"destroyed_by_id": self.env.user.id,
|
||||
})
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def _cron_flag_eligible(self):
|
||||
today = fields.Date.context_today(self)
|
||||
eligible = self.search([
|
||||
("retention_state", "=", "retained"),
|
||||
("destruction_eligible_date", "<=", today),
|
||||
("destruction_eligible_date", "!=", False),
|
||||
])
|
||||
for a in eligible:
|
||||
a.retention_state = "eligible"
|
||||
a.message_post(body=_("Now eligible for destruction (retention period elapsed)."))
|
||||
return len(eligible)
|
||||
|
||||
|
||||
class FamilyLawCaseRetention(models.Model):
|
||||
_inherit = "familylaw.case"
|
||||
|
||||
archive_ids = fields.One2many("familylaw.archive", "case_id", string="Archive")
|
||||
client_file_returned = fields.Boolean(
|
||||
string="Client File Returned", readonly=True, copy=False, tracking=True,
|
||||
)
|
||||
client_file_returned_date = fields.Date(readonly=True, copy=False)
|
||||
|
||||
def action_return_client_file(self):
|
||||
"""Record return of the client file at matter close (attorney-only)."""
|
||||
self._ensure_attorney()
|
||||
for case in self:
|
||||
if case.state != "closed":
|
||||
raise UserError(
|
||||
_("The client file is returned at matter close. Close the matter "
|
||||
"first."))
|
||||
case.client_file_returned = True
|
||||
case.client_file_returned_date = fields.Date.context_today(case)
|
||||
case.message_post(body=_("Client file returned to the client."))
|
||||
return True
|
||||
|
||||
|
||||
class FamilyLawDeadlineLayer(models.Model):
|
||||
_inherit = "familylaw.deadline"
|
||||
|
||||
layer = fields.Selection(
|
||||
selection=[
|
||||
("statutory", "Statutory / Procedural"),
|
||||
("hearing", "Hearing"),
|
||||
("internal", "Internal / Office"),
|
||||
],
|
||||
default="statutory",
|
||||
string="Calendar Layer",
|
||||
help="Categorises the deadline so the calendar can be filtered by layer.",
|
||||
)
|
||||
@@ -37,3 +37,7 @@ access_familylaw_emergency_attorney,familylaw.emergency.motion attorney,model_fa
|
||||
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
|
||||
access_familylaw_retention_class_user,familylaw.retention.class staff,model_familylaw_retention_class,group_familylaw_user,1,0,0,0
|
||||
access_familylaw_retention_class_attorney,familylaw.retention.class attorney,model_familylaw_retention_class,group_familylaw_attorney,1,1,1,1
|
||||
access_familylaw_archive_user,familylaw.archive staff,model_familylaw_archive,group_familylaw_user,1,1,1,0
|
||||
access_familylaw_archive_attorney,familylaw.archive attorney,model_familylaw_archive,group_familylaw_attorney,1,1,1,1
|
||||
|
||||
|
@@ -9,3 +9,4 @@ from . import test_step8
|
||||
from . import test_step9
|
||||
from . import test_step10
|
||||
from . import test_step11
|
||||
from . import test_step12
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""STEP 12 tests — file archive + retention lifecycle + calendar layers.
|
||||
|
||||
odoo -d <db> -u activeblue_familylaw --test-enable \
|
||||
--test-tags familylaw_step12 --stop-after-init
|
||||
|
||||
Proves:
|
||||
* SHA-256 checksum computed from the attachment bytes;
|
||||
* retention class drives the destruction-eligibility date;
|
||||
* eligibility detection + cron flagging;
|
||||
* destroy logs that destruction happened, removes content, keeps checksum proof;
|
||||
* client-file return only at matter close;
|
||||
* deadline calendar layer.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
from datetime import date
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo.tests.common import TransactionCase, new_test_user, tagged
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install", "familylaw", "familylaw_step12")
|
||||
class TestStep12Archive(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.attorney = new_test_user(
|
||||
cls.env, login="fl_atty12", name="Attorney 12",
|
||||
email="atty12@example.com",
|
||||
groups="base.group_user,activeblue_familylaw.group_familylaw_attorney",
|
||||
)
|
||||
cls.partner = cls.env["res.partner"].create({"name": "Arch Client"})
|
||||
cls.case = cls.env["familylaw.case"].create({
|
||||
"name": "Archive Matter", "client_id": cls.partner.id,
|
||||
"case_type": "dissolution_children",
|
||||
})
|
||||
cls.rc = cls.env["familylaw.retention.class"].create({
|
||||
"name": "Standard", "code": "STD", "retention_years": 6,
|
||||
})
|
||||
cls.Arch = cls.env["familylaw.archive"]
|
||||
|
||||
def _attachment(self, content=b"hello world"):
|
||||
return self.env["ir.attachment"].create({
|
||||
"name": "file.pdf",
|
||||
"datas": base64.b64encode(content),
|
||||
})
|
||||
|
||||
# --- checksum -----------------------------------------------------------
|
||||
def test_01_sha256_checksum(self):
|
||||
content = b"some file bytes"
|
||||
att = self._attachment(content)
|
||||
arch = self.Arch.create({
|
||||
"name": "Doc", "case_id": self.case.id, "attachment_id": att.id,
|
||||
})
|
||||
self.assertEqual(arch.checksum, hashlib.sha256(content).hexdigest())
|
||||
|
||||
# --- eligibility date ---------------------------------------------------
|
||||
def test_02_eligible_date_from_retention(self):
|
||||
arch = self.Arch.create({
|
||||
"name": "Doc", "case_id": self.case.id,
|
||||
"retention_class_id": self.rc.id, "date_archived": date(2020, 1, 1),
|
||||
})
|
||||
self.assertEqual(arch.destruction_eligible_date,
|
||||
date(2020, 1, 1) + relativedelta(years=6))
|
||||
|
||||
def test_03_past_eligible_is_eligible(self):
|
||||
arch = self.Arch.create({
|
||||
"name": "Old", "case_id": self.case.id,
|
||||
"retention_class_id": self.rc.id, "date_archived": date(2000, 1, 1),
|
||||
})
|
||||
self.assertTrue(arch.is_destruction_eligible)
|
||||
|
||||
def test_04_future_not_eligible(self):
|
||||
rc = self.env["familylaw.retention.class"].create({
|
||||
"name": "Long", "retention_years": 100})
|
||||
arch = self.Arch.create({
|
||||
"name": "New", "case_id": self.case.id,
|
||||
"retention_class_id": rc.id, "date_archived": date(2020, 1, 1),
|
||||
})
|
||||
self.assertFalse(arch.is_destruction_eligible)
|
||||
|
||||
def test_05_cron_flags_eligible(self):
|
||||
arch = self.Arch.create({
|
||||
"name": "Old", "case_id": self.case.id,
|
||||
"retention_class_id": self.rc.id, "date_archived": date(2000, 1, 1),
|
||||
})
|
||||
self.assertEqual(arch.retention_state, "retained")
|
||||
self.Arch._cron_flag_eligible()
|
||||
self.assertEqual(arch.retention_state, "eligible")
|
||||
|
||||
# --- destruction --------------------------------------------------------
|
||||
def test_06_destroy_removes_content_keeps_checksum(self):
|
||||
content = b"confidential"
|
||||
att = self._attachment(content)
|
||||
arch = self.Arch.create({
|
||||
"name": "Doc", "case_id": self.case.id, "attachment_id": att.id,
|
||||
"retention_class_id": self.rc.id,
|
||||
})
|
||||
cs = arch.checksum
|
||||
self.assertTrue(cs)
|
||||
arch.with_user(self.attorney).action_destroy()
|
||||
self.assertEqual(arch.retention_state, "destroyed")
|
||||
self.assertTrue(arch.destroyed_date)
|
||||
self.assertFalse(arch.attachment_id) # content gone
|
||||
self.assertEqual(arch.checksum, cs) # proof retained
|
||||
|
||||
def test_07_destroy_requires_attorney(self):
|
||||
arch = self.Arch.create({
|
||||
"name": "Doc", "case_id": self.case.id,
|
||||
"retention_class_id": self.rc.id, "date_archived": date(2000, 1, 1),
|
||||
})
|
||||
para = new_test_user(
|
||||
self.env, login="fl_para12", name="Para 12", email="p12@example.com",
|
||||
groups="base.group_user,activeblue_familylaw.group_familylaw_user",
|
||||
)
|
||||
with self.assertRaises(UserError):
|
||||
arch.with_user(para).action_destroy()
|
||||
|
||||
def test_08_destroy_is_audited(self):
|
||||
arch = self.Arch.create({
|
||||
"name": "Doc", "case_id": self.case.id,
|
||||
"retention_class_id": self.rc.id, "date_archived": date(2000, 1, 1),
|
||||
})
|
||||
before = len(arch.message_ids)
|
||||
arch.with_user(self.attorney).action_destroy()
|
||||
self.assertGreater(len(arch.message_ids), before)
|
||||
|
||||
# --- client-file return -------------------------------------------------
|
||||
def test_09_return_blocked_when_open(self):
|
||||
with self.assertRaises(UserError):
|
||||
self.case.with_user(self.attorney).action_return_client_file()
|
||||
|
||||
def test_10_return_on_closed(self):
|
||||
self.case.with_user(self.attorney).action_mark_conflict_cleared()
|
||||
self.case.with_user(self.attorney).action_close()
|
||||
self.case.with_user(self.attorney).action_return_client_file()
|
||||
self.assertTrue(self.case.client_file_returned)
|
||||
self.assertTrue(self.case.client_file_returned_date)
|
||||
|
||||
# --- calendar layer -----------------------------------------------------
|
||||
def test_11_deadline_layer(self):
|
||||
proc = self.case.proceeding_ids[0]
|
||||
dl = self.env["familylaw.deadline"].create({
|
||||
"proceeding_id": proc.id, "name": "Hearing", "deadline_type": "hearing",
|
||||
"trigger_date": date(2025, 1, 1), "days": 1, "layer": "hearing",
|
||||
})
|
||||
self.assertEqual(dl.layer, "hearing")
|
||||
|
||||
def test_12_archive_listed_on_case(self):
|
||||
arch = self.Arch.create({"name": "Doc", "case_id": self.case.id})
|
||||
self.assertIn(arch, self.case.archive_ids)
|
||||
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Retention class -->
|
||||
<record id="view_familylaw_retention_class_list" model="ir.ui.view">
|
||||
<field name="name">familylaw.retention.class.list</field>
|
||||
<field name="model">familylaw.retention.class</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Retention Classes" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="retention_years"/>
|
||||
<field name="description"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_familylaw_retention_class" model="ir.actions.act_window">
|
||||
<field name="name">Retention Classes</field>
|
||||
<field name="res_model">familylaw.retention.class</field>
|
||||
<field name="view_mode">list</field>
|
||||
</record>
|
||||
|
||||
<!-- Archive -->
|
||||
<record id="view_familylaw_archive_list" model="ir.ui.view">
|
||||
<field name="name">familylaw.archive.list</field>
|
||||
<field name="model">familylaw.archive</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Archive"
|
||||
decoration-warning="is_destruction_eligible"
|
||||
decoration-muted="retention_state == 'destroyed'">
|
||||
<field name="name"/>
|
||||
<field name="case_id"/>
|
||||
<field name="retention_class_id"/>
|
||||
<field name="date_archived"/>
|
||||
<field name="destruction_eligible_date"/>
|
||||
<field name="is_destruction_eligible" column_invisible="1"/>
|
||||
<field name="retention_state" widget="badge"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_familylaw_archive_form" model="ir.ui.view">
|
||||
<field name="name">familylaw.archive.form</field>
|
||||
<field name="model">familylaw.archive</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Archived File">
|
||||
<header>
|
||||
<button name="action_destroy" type="object"
|
||||
string="Destroy (logged)" class="btn-danger"
|
||||
confirm="Destroy this archived content? The audit log will
|
||||
record that destruction occurred; the content
|
||||
itself will not be retained."
|
||||
invisible="retention_state == 'destroyed'"
|
||||
groups="activeblue_familylaw.group_familylaw_attorney"/>
|
||||
<field name="retention_state" widget="statusbar"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group string="File">
|
||||
<field name="attachment_id"/>
|
||||
<field name="checksum" readonly="1"/>
|
||||
<field name="document_id"/>
|
||||
</group>
|
||||
<group string="Retention">
|
||||
<field name="case_id"/>
|
||||
<field name="proceeding_id"/>
|
||||
<field name="retention_class_id"/>
|
||||
<field name="date_archived"/>
|
||||
<field name="destruction_eligible_date" readonly="1"/>
|
||||
<field name="is_destruction_eligible" readonly="1"/>
|
||||
<field name="destroyed_date" readonly="1"/>
|
||||
<field name="destroyed_by_id" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_familylaw_archive_search" model="ir.ui.view">
|
||||
<field name="name">familylaw.archive.search</field>
|
||||
<field name="model">familylaw.archive</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Archive">
|
||||
<field name="name"/>
|
||||
<field name="case_id"/>
|
||||
<filter name="eligible" string="Eligible for Destruction"
|
||||
domain="[('is_destruction_eligible','=',True)]"/>
|
||||
<filter name="destroyed" string="Destroyed"
|
||||
domain="[('retention_state','=','destroyed')]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="group_class" string="Retention Class"
|
||||
context="{'group_by':'retention_class_id'}"/>
|
||||
<filter name="group_state" string="State"
|
||||
context="{'group_by':'retention_state'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_familylaw_archive" model="ir.actions.act_window">
|
||||
<field name="name">Archive</field>
|
||||
<field name="res_model">familylaw.archive</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_familylaw_archive_search"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -67,6 +67,10 @@
|
||||
<button name="action_reopen" type="object" string="Reopen"
|
||||
invisible="state != 'closed'"
|
||||
groups="activeblue_familylaw.group_familylaw_attorney"/>
|
||||
<button name="action_return_client_file" type="object"
|
||||
string="Return Client File"
|
||||
invisible="state != 'closed' or client_file_returned"
|
||||
groups="activeblue_familylaw.group_familylaw_attorney"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="intake,engaged,disclosure,discovery,mediation,hearing,closed"/>
|
||||
</header>
|
||||
|
||||
@@ -86,6 +86,14 @@
|
||||
<filter name="pending" string="Pending"
|
||||
domain="[('state','=','pending')]"/>
|
||||
<separator/>
|
||||
<!-- Calendar layer toggles (Step 12) -->
|
||||
<filter name="layer_statutory" string="Statutory layer"
|
||||
domain="[('layer','=','statutory')]"/>
|
||||
<filter name="layer_hearing" string="Hearing layer"
|
||||
domain="[('layer','=','hearing')]"/>
|
||||
<filter name="layer_internal" string="Internal layer"
|
||||
domain="[('layer','=','internal')]"/>
|
||||
<separator/>
|
||||
<filter name="this_week" string="Due Within 7 Days"
|
||||
domain="[('due_date','>=', context_today()),
|
||||
('due_date','<=', context_today() + relativedelta(days=7))]"/>
|
||||
|
||||
@@ -77,11 +77,26 @@
|
||||
sequence="60"
|
||||
groups="activeblue_familylaw.group_familylaw_attorney"/>
|
||||
|
||||
<!-- Configuration placeholder (populated in later steps) -->
|
||||
<!-- Archive -->
|
||||
<menuitem id="menu_familylaw_archive"
|
||||
name="Archive"
|
||||
parent="menu_familylaw_root"
|
||||
action="action_familylaw_archive"
|
||||
sequence="70"/>
|
||||
|
||||
<!-- Configuration -->
|
||||
<menuitem id="menu_familylaw_config"
|
||||
name="Configuration"
|
||||
parent="menu_familylaw_root"
|
||||
sequence="90"
|
||||
groups="activeblue_familylaw.group_familylaw_attorney"/>
|
||||
|
||||
<!-- Configuration > Retention Classes -->
|
||||
<menuitem id="menu_familylaw_retention_class"
|
||||
name="Retention Classes"
|
||||
parent="menu_familylaw_config"
|
||||
action="action_familylaw_retention_class"
|
||||
sequence="10"
|
||||
groups="activeblue_familylaw.group_familylaw_attorney"/>
|
||||
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user