Step 14: comms (never auto-send) + AI-assist billing flag + matter-scoped access
familylaw.comms — client communications drafted (optionally AI-assisted) for attorney review: draft -> approved (attorney-only) -> sent. NEVER auto-sends; action_send requires prior approval and an explicit human action. Staff draft/assemble but do not communicate legal positions (Bar Rule 4-5.3). familylaw.time.entry — time with an ai_assisted flag. Case computes total_hours, ai_assisted_hours, ai_assisted_ratio (reportable AI-assisted share). Matter-scoped access (security/familylaw_security.xml): record rule confines staff to matters they are assigned to or created; attorneys see all (OR-combined rules). Bar Rule 4-5.3 supervision + confidentiality. Case gains Communications + Billing/Time tabs; Communications menu; ACL for both models. Hardening (encryption-at-rest/backups) noted as infra, out of module scope. Tests (familylaw_step14): cannot send draft (never auto-send); approve-then-send; approve attorney-only; AI-assist ratio (25% of 8h) + zero case; staff sees assigned / cannot see unassigned (search empty + read AccessError); attorney sees all. This completes the Step 1-14 roadmap. 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.13.0.0",
|
"version": "18.0.14.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": """
|
||||||
@@ -39,6 +39,7 @@ Each step adds one vertical, independently testable slice. See BUILD_PLAN.md.
|
|||||||
"views/familylaw_emergency_views.xml",
|
"views/familylaw_emergency_views.xml",
|
||||||
"views/familylaw_proceeding_views.xml",
|
"views/familylaw_proceeding_views.xml",
|
||||||
"views/familylaw_archive_views.xml",
|
"views/familylaw_archive_views.xml",
|
||||||
|
"views/familylaw_comms_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",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from . import familylaw_emergency
|
|||||||
from . import familylaw_docuseal
|
from . import familylaw_docuseal
|
||||||
from . import familylaw_archive
|
from . import familylaw_archive
|
||||||
from . import familylaw_obligation
|
from . import familylaw_obligation
|
||||||
|
from . import familylaw_comms
|
||||||
from . import familylaw_ai_wizard
|
from . import familylaw_ai_wizard
|
||||||
from . import familylaw_modification_wizard
|
from . import familylaw_modification_wizard
|
||||||
from . import familylaw_emergency_wizard
|
from . import familylaw_emergency_wizard
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""STEP 14 — Comms drafting + AI-assisted billing flag + matter-scoped access.
|
||||||
|
|
||||||
|
Three concerns:
|
||||||
|
* COMMS — plain-language client updates drafted (optionally AI-assisted) for
|
||||||
|
attorney review. They are born draft and NEVER auto-send: there is no code path
|
||||||
|
that transmits a communication without an explicit, post-approval human action.
|
||||||
|
* BILLING FLAG — time entries carry an ai_assisted flag so the AI-assisted share of
|
||||||
|
work is reportable (transparency, fee reasonableness).
|
||||||
|
* MATTER-SCOPED ACCESS — record rules confine staff (non-lawyers) to the matters
|
||||||
|
they are assigned to (Bar Rule 4-5.3 supervision + confidentiality). Attorneys see
|
||||||
|
all. The record rules live in security/familylaw_security.xml.
|
||||||
|
|
||||||
|
Hardening note (infra, not code): encryption-at-rest for case data and backups of the
|
||||||
|
audit trail / DocuSeal host are operational requirements (Requirements for Success
|
||||||
|
#5) — out of scope for the module but called out here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
ATTORNEY_GROUP = "activeblue_familylaw.group_familylaw_attorney"
|
||||||
|
|
||||||
|
|
||||||
|
class FamilyLawComms(models.Model):
|
||||||
|
_name = "familylaw.comms"
|
||||||
|
_description = "Client Communication (review-gated, never auto-send)"
|
||||||
|
_inherit = ["mail.thread"]
|
||||||
|
_order = "create_date desc"
|
||||||
|
|
||||||
|
name = fields.Char(string="Subject", required=True, tracking=True)
|
||||||
|
case_id = fields.Many2one(
|
||||||
|
"familylaw.case", required=True, ondelete="cascade", index=True, tracking=True,
|
||||||
|
)
|
||||||
|
comm_type = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("update", "Status Update"),
|
||||||
|
("email", "Email"),
|
||||||
|
("letter", "Letter"),
|
||||||
|
],
|
||||||
|
default="update",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
body = fields.Html(sanitize=True)
|
||||||
|
source = fields.Selection(
|
||||||
|
selection=[("ai", "AI-Assisted"), ("manual", "Manual")],
|
||||||
|
default="manual",
|
||||||
|
)
|
||||||
|
state = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("draft", "Draft"),
|
||||||
|
("approved", "Approved"),
|
||||||
|
("sent", "Sent"),
|
||||||
|
],
|
||||||
|
default="draft",
|
||||||
|
required=True,
|
||||||
|
copy=False,
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
approved_by_id = fields.Many2one("res.users", readonly=True, copy=False)
|
||||||
|
sent_date = fields.Datetime(readonly=True, copy=False)
|
||||||
|
|
||||||
|
def _ensure_attorney(self):
|
||||||
|
if not self.env.user.has_group(ATTORNEY_GROUP):
|
||||||
|
raise UserError(_("Only an attorney may approve a client communication. "
|
||||||
|
"Staff draft and assemble; they do not communicate legal "
|
||||||
|
"positions to clients."))
|
||||||
|
|
||||||
|
def action_approve(self):
|
||||||
|
self._ensure_attorney()
|
||||||
|
for c in self:
|
||||||
|
if c.state != "draft":
|
||||||
|
raise UserError(_("Only a draft communication can be approved."))
|
||||||
|
c.write({"state": "approved", "approved_by_id": self.env.user.id})
|
||||||
|
c.message_post(body=_("Communication approved by %s.") % self.env.user.name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def action_send(self):
|
||||||
|
"""Explicit, human-initiated send. NEVER auto — requires prior approval."""
|
||||||
|
for c in self:
|
||||||
|
if c.state != "approved":
|
||||||
|
raise UserError(_(
|
||||||
|
"A communication must be attorney-approved before it can be sent. "
|
||||||
|
"Communications never auto-send."))
|
||||||
|
c.write({"state": "sent", "sent_date": fields.Datetime.now()})
|
||||||
|
c.message_post(body=_("Communication sent by %s.") % self.env.user.name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class FamilyLawTimeEntry(models.Model):
|
||||||
|
_name = "familylaw.time.entry"
|
||||||
|
_description = "Time Entry (with AI-assist flag)"
|
||||||
|
_order = "date desc, id desc"
|
||||||
|
|
||||||
|
case_id = fields.Many2one(
|
||||||
|
"familylaw.case", required=True, ondelete="cascade", index=True,
|
||||||
|
)
|
||||||
|
user_id = fields.Many2one(
|
||||||
|
"res.users", default=lambda self: self.env.user, required=True,
|
||||||
|
)
|
||||||
|
date = fields.Date(default=fields.Date.context_today, required=True)
|
||||||
|
description = fields.Char(required=True)
|
||||||
|
hours = fields.Float(required=True)
|
||||||
|
ai_assisted = fields.Boolean(
|
||||||
|
string="AI-Assisted",
|
||||||
|
help="Set when this work was materially assisted by AI — keeps the AI-assisted "
|
||||||
|
"share reportable for fee transparency.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FamilyLawCaseComms(models.Model):
|
||||||
|
_inherit = "familylaw.case"
|
||||||
|
|
||||||
|
comms_ids = fields.One2many("familylaw.comms", "case_id", string="Communications")
|
||||||
|
time_entry_ids = fields.One2many(
|
||||||
|
"familylaw.time.entry", "case_id", string="Time Entries")
|
||||||
|
total_hours = fields.Float(compute="_compute_time", store=True)
|
||||||
|
ai_assisted_hours = fields.Float(compute="_compute_time", store=True)
|
||||||
|
ai_assisted_ratio = fields.Float(
|
||||||
|
compute="_compute_time", store=True, string="AI-Assisted %",
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends("time_entry_ids.hours", "time_entry_ids.ai_assisted")
|
||||||
|
def _compute_time(self):
|
||||||
|
for case in self:
|
||||||
|
total = sum(case.time_entry_ids.mapped("hours"))
|
||||||
|
ai = sum(case.time_entry_ids.filtered("ai_assisted").mapped("hours"))
|
||||||
|
case.total_hours = total
|
||||||
|
case.ai_assisted_hours = ai
|
||||||
|
case.ai_assisted_ratio = (ai / total * 100.0) if total else 0.0
|
||||||
@@ -23,4 +23,35 @@
|
|||||||
<field name="implied_ids" eval="[(4, ref('group_familylaw_user'))]"/>
|
<field name="implied_ids" eval="[(4, ref('group_familylaw_user'))]"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<!-- STEP 14 — matter-scoped access (Bar Rule 4-5.3 supervision).
|
||||||
|
Staff (non-lawyers) see only matters they are assigned to or created;
|
||||||
|
attorneys see all. The two rules are OR-combined for attorneys (who are in
|
||||||
|
both groups), so an attorney is not restricted. -->
|
||||||
|
<record id="rule_case_staff_assigned" model="ir.rule">
|
||||||
|
<field name="name">Family Law: staff see assigned matters only</field>
|
||||||
|
<field name="model_id" ref="model_familylaw_case"/>
|
||||||
|
<field name="groups" eval="[(4, ref('group_familylaw_user'))]"/>
|
||||||
|
<field name="domain_force">
|
||||||
|
['|','|',
|
||||||
|
('attorney_id','=',user.id),
|
||||||
|
('paralegal_id','=',user.id),
|
||||||
|
('create_uid','=',user.id)]
|
||||||
|
</field>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="True"/>
|
||||||
|
<field name="perm_create" eval="True"/>
|
||||||
|
<field name="perm_unlink" eval="True"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_case_attorney_all" model="ir.rule">
|
||||||
|
<field name="name">Family Law: attorneys see all matters</field>
|
||||||
|
<field name="model_id" ref="model_familylaw_case"/>
|
||||||
|
<field name="groups" eval="[(4, ref('group_familylaw_attorney'))]"/>
|
||||||
|
<field name="domain_force">[(1,'=',1)]</field>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="True"/>
|
||||||
|
<field name="perm_create" eval="True"/>
|
||||||
|
<field name="perm_unlink" eval="True"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@@ -43,3 +43,7 @@ access_familylaw_archive_user,familylaw.archive staff,model_familylaw_archive,gr
|
|||||||
access_familylaw_archive_attorney,familylaw.archive attorney,model_familylaw_archive,group_familylaw_attorney,1,1,1,1
|
access_familylaw_archive_attorney,familylaw.archive attorney,model_familylaw_archive,group_familylaw_attorney,1,1,1,1
|
||||||
access_familylaw_obligation_user,familylaw.obligation staff,model_familylaw_obligation,group_familylaw_user,1,1,1,0
|
access_familylaw_obligation_user,familylaw.obligation staff,model_familylaw_obligation,group_familylaw_user,1,1,1,0
|
||||||
access_familylaw_obligation_attorney,familylaw.obligation attorney,model_familylaw_obligation,group_familylaw_attorney,1,1,1,1
|
access_familylaw_obligation_attorney,familylaw.obligation attorney,model_familylaw_obligation,group_familylaw_attorney,1,1,1,1
|
||||||
|
access_familylaw_comms_user,familylaw.comms staff,model_familylaw_comms,group_familylaw_user,1,1,1,0
|
||||||
|
access_familylaw_comms_attorney,familylaw.comms attorney,model_familylaw_comms,group_familylaw_attorney,1,1,1,1
|
||||||
|
access_familylaw_time_entry_user,familylaw.time.entry staff,model_familylaw_time_entry,group_familylaw_user,1,1,1,0
|
||||||
|
access_familylaw_time_entry_attorney,familylaw.time.entry attorney,model_familylaw_time_entry,group_familylaw_attorney,1,1,1,1
|
||||||
|
|||||||
|
@@ -11,3 +11,4 @@ from . import test_step10
|
|||||||
from . import test_step11
|
from . import test_step11
|
||||||
from . import test_step12
|
from . import test_step12
|
||||||
from . import test_step13
|
from . import test_step13
|
||||||
|
from . import test_step14
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""STEP 14 tests — comms (never auto-send) + AI-assist billing + matter-scoped access.
|
||||||
|
|
||||||
|
odoo -d <db> -u activeblue_familylaw --test-enable \
|
||||||
|
--test-tags familylaw_step14 --stop-after-init
|
||||||
|
|
||||||
|
Proves:
|
||||||
|
* a communication cannot be sent before attorney approval (never auto-send);
|
||||||
|
* approval is attorney-only; approved comms can be sent;
|
||||||
|
* the AI-assisted time ratio is computed and reportable;
|
||||||
|
* matter-scoped access: a staff user cannot see unassigned matters; can see
|
||||||
|
assigned ones; an attorney sees all.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from odoo.tests.common import TransactionCase, new_test_user, tagged
|
||||||
|
from odoo.exceptions import UserError, AccessError
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("post_install", "-at_install", "familylaw", "familylaw_step14")
|
||||||
|
class TestStep14Comms(TransactionCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.attorney = new_test_user(
|
||||||
|
cls.env, login="fl_atty14", name="Attorney 14",
|
||||||
|
email="atty14@example.com",
|
||||||
|
groups="base.group_user,activeblue_familylaw.group_familylaw_attorney",
|
||||||
|
)
|
||||||
|
cls.para = new_test_user(
|
||||||
|
cls.env, login="fl_para14", name="Para 14", email="para14@example.com",
|
||||||
|
groups="base.group_user,activeblue_familylaw.group_familylaw_user",
|
||||||
|
)
|
||||||
|
cls.para_other = new_test_user(
|
||||||
|
cls.env, login="fl_para14b", name="Para 14b", email="para14b@example.com",
|
||||||
|
groups="base.group_user,activeblue_familylaw.group_familylaw_user",
|
||||||
|
)
|
||||||
|
cls.partner = cls.env["res.partner"].create({"name": "Comms Client"})
|
||||||
|
cls.case = cls.env["familylaw.case"].create({
|
||||||
|
"name": "Comms Matter", "client_id": cls.partner.id,
|
||||||
|
"case_type": "dissolution_children",
|
||||||
|
"attorney_id": cls.attorney.id, "paralegal_id": cls.para.id,
|
||||||
|
"county": "broward", # avoid AO-13 seeding noise
|
||||||
|
})
|
||||||
|
cls.Comms = cls.env["familylaw.comms"]
|
||||||
|
|
||||||
|
def _comm(self, **kw):
|
||||||
|
vals = {"name": "Status update", "case_id": self.case.id,
|
||||||
|
"comm_type": "update"}
|
||||||
|
vals.update(kw)
|
||||||
|
return self.Comms.create(vals)
|
||||||
|
|
||||||
|
# --- never auto-send ----------------------------------------------------
|
||||||
|
def test_01_cannot_send_draft(self):
|
||||||
|
c = self._comm()
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
c.action_send()
|
||||||
|
self.assertEqual(c.state, "draft")
|
||||||
|
|
||||||
|
def test_02_approve_then_send(self):
|
||||||
|
c = self._comm()
|
||||||
|
c.with_user(self.attorney).action_approve()
|
||||||
|
self.assertEqual(c.state, "approved")
|
||||||
|
c.action_send()
|
||||||
|
self.assertEqual(c.state, "sent")
|
||||||
|
self.assertTrue(c.sent_date)
|
||||||
|
|
||||||
|
def test_03_approve_requires_attorney(self):
|
||||||
|
c = self._comm()
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
c.with_user(self.para).action_approve()
|
||||||
|
|
||||||
|
def test_04_no_auto_send_field_or_cron(self):
|
||||||
|
# structural: there is no method that sends without going through approval
|
||||||
|
c = self._comm()
|
||||||
|
self.assertEqual(c.state, "draft")
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
c.action_send()
|
||||||
|
|
||||||
|
# --- AI-assisted billing ratio ------------------------------------------
|
||||||
|
def test_05_ai_ratio(self):
|
||||||
|
TE = self.env["familylaw.time.entry"]
|
||||||
|
TE.create({"case_id": self.case.id, "description": "AI draft",
|
||||||
|
"hours": 2.0, "ai_assisted": True})
|
||||||
|
TE.create({"case_id": self.case.id, "description": "Manual review",
|
||||||
|
"hours": 6.0, "ai_assisted": False})
|
||||||
|
self.assertEqual(self.case.total_hours, 8.0)
|
||||||
|
self.assertEqual(self.case.ai_assisted_hours, 2.0)
|
||||||
|
self.assertAlmostEqual(self.case.ai_assisted_ratio, 25.0, places=2)
|
||||||
|
|
||||||
|
def test_06_ratio_zero_when_no_time(self):
|
||||||
|
self.assertEqual(self.case.ai_assisted_ratio, 0.0)
|
||||||
|
|
||||||
|
# --- matter-scoped access -----------------------------------------------
|
||||||
|
def test_07_staff_sees_assigned(self):
|
||||||
|
# para is paralegal_id on cls.case -> visible
|
||||||
|
found = self.env["familylaw.case"].with_user(self.para).search(
|
||||||
|
[("id", "=", self.case.id)])
|
||||||
|
self.assertIn(self.case, found)
|
||||||
|
|
||||||
|
def test_08_staff_cannot_see_unassigned(self):
|
||||||
|
other_case = self.env["familylaw.case"].create({
|
||||||
|
"name": "Other Matter", "client_id": self.partner.id,
|
||||||
|
"case_type": "paternity", "attorney_id": self.attorney.id,
|
||||||
|
"paralegal_id": self.para.id, "county": "broward",
|
||||||
|
})
|
||||||
|
# para_other is not assigned and did not create it -> not visible
|
||||||
|
found = self.env["familylaw.case"].with_user(self.para_other).search(
|
||||||
|
[("id", "=", other_case.id)])
|
||||||
|
self.assertNotIn(other_case, found)
|
||||||
|
|
||||||
|
def test_09_attorney_sees_all(self):
|
||||||
|
other_case = self.env["familylaw.case"].create({
|
||||||
|
"name": "Unassigned-to-attorney Matter", "client_id": self.partner.id,
|
||||||
|
"case_type": "paternity", "county": "broward",
|
||||||
|
})
|
||||||
|
found = self.env["familylaw.case"].with_user(self.attorney).search(
|
||||||
|
[("id", "=", other_case.id)])
|
||||||
|
self.assertIn(other_case, found)
|
||||||
|
|
||||||
|
def test_10_staff_read_unassigned_raises(self):
|
||||||
|
other_case = self.env["familylaw.case"].create({
|
||||||
|
"name": "Secret Matter", "client_id": self.partner.id,
|
||||||
|
"case_type": "paternity", "county": "broward",
|
||||||
|
})
|
||||||
|
with self.assertRaises(AccessError):
|
||||||
|
other_case.with_user(self.para_other).read(["name"])
|
||||||
@@ -181,6 +181,33 @@
|
|||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
|
<page string="Communications" name="comms">
|
||||||
|
<field name="comms_ids">
|
||||||
|
<list decoration-success="state == 'sent'"
|
||||||
|
decoration-muted="state == 'draft'">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="comm_type"/>
|
||||||
|
<field name="source"/>
|
||||||
|
<field name="state" widget="badge"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page string="Billing / Time" name="time">
|
||||||
|
<field name="time_entry_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="user_id"/>
|
||||||
|
<field name="description"/>
|
||||||
|
<field name="hours"/>
|
||||||
|
<field name="ai_assisted" widget="boolean_toggle"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
<group class="oe_subtotal_footer">
|
||||||
|
<field name="total_hours"/>
|
||||||
|
<field name="ai_assisted_hours"/>
|
||||||
|
<field name="ai_assisted_ratio"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
<page string="Emergency Notes" name="emergency"
|
<page string="Emergency Notes" name="emergency"
|
||||||
invisible="not is_emergency">
|
invisible="not is_emergency">
|
||||||
<group>
|
<group>
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_familylaw_comms_list" model="ir.ui.view">
|
||||||
|
<field name="name">familylaw.comms.list</field>
|
||||||
|
<field name="model">familylaw.comms</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Communications"
|
||||||
|
decoration-success="state == 'sent'"
|
||||||
|
decoration-info="state == 'approved'"
|
||||||
|
decoration-muted="state == 'draft'">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="case_id"/>
|
||||||
|
<field name="comm_type"/>
|
||||||
|
<field name="source"/>
|
||||||
|
<field name="state" widget="badge"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_familylaw_comms_form" model="ir.ui.view">
|
||||||
|
<field name="name">familylaw.comms.form</field>
|
||||||
|
<field name="model">familylaw.comms</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Communication">
|
||||||
|
<header>
|
||||||
|
<button name="action_approve" type="object" string="Approve"
|
||||||
|
class="btn-primary" invisible="state != 'draft'"
|
||||||
|
groups="activeblue_familylaw.group_familylaw_attorney"/>
|
||||||
|
<button name="action_send" type="object" string="Send"
|
||||||
|
invisible="state != 'approved'"/>
|
||||||
|
<field name="state" widget="statusbar"
|
||||||
|
statusbar_visible="draft,approved,sent"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="alert alert-info" role="alert" invisible="state == 'sent'">
|
||||||
|
Communications are attorney-reviewed and NEVER auto-send. Staff
|
||||||
|
draft and assemble; they do not communicate legal positions.
|
||||||
|
</div>
|
||||||
|
<div class="oe_title">
|
||||||
|
<label for="name"/>
|
||||||
|
<h1><field name="name"/></h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="case_id"/>
|
||||||
|
<field name="comm_type"/>
|
||||||
|
<field name="source"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="approved_by_id" readonly="1"/>
|
||||||
|
<field name="sent_date" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<field name="body"/>
|
||||||
|
</sheet>
|
||||||
|
<div class="oe_chatter">
|
||||||
|
<field name="message_follower_ids"/>
|
||||||
|
<field name="activity_ids"/>
|
||||||
|
<field name="message_ids"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_familylaw_comms" model="ir.actions.act_window">
|
||||||
|
<field name="name">Communications</field>
|
||||||
|
<field name="res_model">familylaw.comms</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@@ -77,6 +77,13 @@
|
|||||||
sequence="60"
|
sequence="60"
|
||||||
groups="activeblue_familylaw.group_familylaw_attorney"/>
|
groups="activeblue_familylaw.group_familylaw_attorney"/>
|
||||||
|
|
||||||
|
<!-- Communications -->
|
||||||
|
<menuitem id="menu_familylaw_comms"
|
||||||
|
name="Communications"
|
||||||
|
parent="menu_familylaw_root"
|
||||||
|
action="action_familylaw_comms"
|
||||||
|
sequence="65"/>
|
||||||
|
|
||||||
<!-- Archive -->
|
<!-- Archive -->
|
||||||
<menuitem id="menu_familylaw_archive"
|
<menuitem id="menu_familylaw_archive"
|
||||||
name="Archive"
|
name="Archive"
|
||||||
|
|||||||
Reference in New Issue
Block a user