Step 2: parties, children, issues, proceeding layer, conflict screening, intake wizard
Models: - familylaw.party — party to a matter (client/opposing/opposing counsel/other) - familylaw.child — minor child with DOB constraint (rejects future + >25 years) - familylaw.issue — contested issue (time-sharing, support, equitable distribution, etc.) - familylaw.proceeding — unit of legal action; auto-created on every case create() - familylaw.conflict.hit — records of conflict screening hits for attorney review - familylaw.intake.wizard — multi-step intake questionnaire (TransientModel) familylaw.case updates: - case_number (indexed, unique, searchable), county, is_emergency, urgency_notes - One2many to party/child/issue/proceeding/conflict_hit - create() override: auto-opens an initial proceeding typed by case_type - action_run_conflict_screening(): full-DB party + client name search; never auto-clears gate Intake wizard (conditional strictness): - Triage step first; urgency screen selects emergency vs. standard path - Emergency fast-path: create on minimum facts (who + urgency flags), defer rest - Standard strict path: matter name + client + case type + county required - Modification branch (step 3) for support/parenting/alimony modifications - Caller concern logged on case as attorney question; software never answers it - Runs conflict screening on completion Views: party/child/issue/proceeding/intake form+list+inline views; case form now shows emergency banner, conflict warning, notebook tabs for all related records; search extended to find by party name, child name, case_number (filter_domain). Menu: "New Intake" entry launches the wizard. Security: access rules for all 5 new models + intake wizard (base.group_user). Tests (familylaw_step2): 34 tests across 4 classes covering: - Initial proceeding creation and type mapping - Multiple independent proceedings per case - DOB validation (future + implausible age) - Search by party name / child name / case_number - Conflict screening (finds client match, does not auto-clear, hit count, no false positives for unrelated parties) - Standard path strict validation (missing name/type/county/client each rejected) - Emergency path (creates case on caller name alone, sets is_emergency, captures notes) - Caller concern logged on chatter, never answered by software Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,15 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
{
|
{
|
||||||
"name": "Active Blue Family Law",
|
"name": "Active Blue Family Law",
|
||||||
"version": "18.0.1.0.0",
|
"version": "18.0.2.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": """
|
||||||
Active Blue Family Law
|
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 delivers the case spine: the familylaw.case model, its lifecycle
|
steps. Step 1 delivers the case spine; Step 2 adds parties, children, issues,
|
||||||
state machine, security groups (attorney / staff), and the case views.
|
the proceeding layer, automated conflict screening, and the intake questionnaire.
|
||||||
|
|
||||||
Each subsequent step adds one vertical, independently testable slice. See
|
Each subsequent step adds one vertical, independently testable slice. See
|
||||||
BUILD_PLAN.md for the full sequence and the test method.
|
BUILD_PLAN.md for the full sequence and the test method.
|
||||||
@@ -27,6 +27,11 @@ signing, citation verification, wire map, training, forms & playbook).
|
|||||||
"data": [
|
"data": [
|
||||||
"security/familylaw_security.xml",
|
"security/familylaw_security.xml",
|
||||||
"security/ir.model.access.csv",
|
"security/ir.model.access.csv",
|
||||||
|
"views/familylaw_party_views.xml",
|
||||||
|
"views/familylaw_child_views.xml",
|
||||||
|
"views/familylaw_issue_views.xml",
|
||||||
|
"views/familylaw_proceeding_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",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
from . import familylaw_case
|
from . import familylaw_case
|
||||||
|
from . import familylaw_party
|
||||||
|
from . import familylaw_child
|
||||||
|
from . import familylaw_issue
|
||||||
|
from . import familylaw_proceeding
|
||||||
|
from . import familylaw_conflict
|
||||||
|
from . import familylaw_intake
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""STEP 1 — The case spine.
|
"""STEP 1 — Case spine (updated in Step 2: case_number, county, emergency flag,
|
||||||
|
related records, initial-proceeding creation, conflict screening).
|
||||||
familylaw.case is the system of record for a matter. This step implements:
|
|
||||||
* the core identifying fields and the responsible-people links,
|
|
||||||
* the pro-se / attorney representation flag (drives later workflows),
|
|
||||||
* the lifecycle STATE MACHINE with guarded transitions, and
|
|
||||||
* two attorney-only gates (conflict clearance, closing a case).
|
|
||||||
|
|
||||||
The guards are enforced in Python (server-side, testable) AND mirrored in the
|
|
||||||
view via `groups=` on the buttons (defence in depth). Later steps add parties,
|
|
||||||
children, documents, the review gate, deadlines, AI, discovery, etc.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models, _
|
||||||
@@ -17,7 +8,6 @@ from odoo.exceptions import UserError
|
|||||||
|
|
||||||
ATTORNEY_GROUP = "activeblue_familylaw.group_familylaw_attorney"
|
ATTORNEY_GROUP = "activeblue_familylaw.group_familylaw_attorney"
|
||||||
|
|
||||||
# The case lifecycle. Order matters: it defines the legal "forward" path.
|
|
||||||
STATE_SEQUENCE = [
|
STATE_SEQUENCE = [
|
||||||
"intake",
|
"intake",
|
||||||
"engaged",
|
"engaged",
|
||||||
@@ -28,6 +18,12 @@ STATE_SEQUENCE = [
|
|||||||
"closed",
|
"closed",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
_MODIFICATION_TYPES = {
|
||||||
|
"support_modification",
|
||||||
|
"parenting_modification",
|
||||||
|
"alimony_modification",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class FamilyLawCase(models.Model):
|
class FamilyLawCase(models.Model):
|
||||||
_name = "familylaw.case"
|
_name = "familylaw.case"
|
||||||
@@ -48,6 +44,24 @@ class FamilyLawCase(models.Model):
|
|||||||
required=True,
|
required=True,
|
||||||
tracking=True,
|
tracking=True,
|
||||||
)
|
)
|
||||||
|
case_number = fields.Char(
|
||||||
|
string="Court Case Number",
|
||||||
|
index=True,
|
||||||
|
tracking=True,
|
||||||
|
help="Court-assigned case number (e.g. 2024-DR-001234). "
|
||||||
|
"Leave blank until the court assigns one.",
|
||||||
|
)
|
||||||
|
county = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("miami_dade", "Miami-Dade (11th Circuit)"),
|
||||||
|
("broward", "Broward (17th Circuit)"),
|
||||||
|
("palm_beach", "Palm Beach (15th Circuit)"),
|
||||||
|
("other_fl", "Other Florida County"),
|
||||||
|
],
|
||||||
|
string="County / Court",
|
||||||
|
default="miami_dade",
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
case_type = fields.Selection(
|
case_type = fields.Selection(
|
||||||
selection=[
|
selection=[
|
||||||
("dissolution_no_children", "Dissolution — no children"),
|
("dissolution_no_children", "Dissolution — no children"),
|
||||||
@@ -78,7 +92,7 @@ class FamilyLawCase(models.Model):
|
|||||||
tracking=True,
|
tracking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Representation (drives the subpoena workflow in a later step) ------
|
# --- Representation (drives the Step 8 subpoena branch) -----------------
|
||||||
representation = fields.Selection(
|
representation = fields.Selection(
|
||||||
selection=[
|
selection=[
|
||||||
("attorney", "Attorney of Record"),
|
("attorney", "Attorney of Record"),
|
||||||
@@ -89,7 +103,20 @@ class FamilyLawCase(models.Model):
|
|||||||
required=True,
|
required=True,
|
||||||
tracking=True,
|
tracking=True,
|
||||||
help="Pro-se matters route subpoena issuance through the clerk of court; "
|
help="Pro-se matters route subpoena issuance through the clerk of court; "
|
||||||
"attorney-of-record matters issue directly. See Forms & Playbook, Part C.",
|
"attorney-of-record matters issue directly. See Forms & Playbook, Part C.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Emergency flags (Step 2 intake fast-path) --------------------------
|
||||||
|
is_emergency = fields.Boolean(
|
||||||
|
string="Emergency Matter",
|
||||||
|
default=False,
|
||||||
|
tracking=True,
|
||||||
|
help="Set when the intake urgency screen trips. Matter opened on minimum "
|
||||||
|
"facts; staff must complete the record immediately.",
|
||||||
|
)
|
||||||
|
urgency_notes = fields.Text(
|
||||||
|
string="Urgency Notes",
|
||||||
|
help="Emergency description captured at intake.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Gates / status -----------------------------------------------------
|
# --- Gates / status -----------------------------------------------------
|
||||||
@@ -99,7 +126,7 @@ class FamilyLawCase(models.Model):
|
|||||||
copy=False,
|
copy=False,
|
||||||
tracking=True,
|
tracking=True,
|
||||||
help="Set by an attorney once the conflict check is documented and clear. "
|
help="Set by an attorney once the conflict check is documented and clear. "
|
||||||
"Required before the matter can be engaged.",
|
"Required before the matter can be engaged.",
|
||||||
)
|
)
|
||||||
state = fields.Selection(
|
state = fields.Selection(
|
||||||
selection=[
|
selection=[
|
||||||
@@ -117,16 +144,158 @@ class FamilyLawCase(models.Model):
|
|||||||
copy=False,
|
copy=False,
|
||||||
tracking=True,
|
tracking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
date_opened = fields.Date(
|
date_opened = fields.Date(
|
||||||
string="Date Opened",
|
string="Date Opened",
|
||||||
default=fields.Date.context_today,
|
default=fields.Date.context_today,
|
||||||
)
|
)
|
||||||
active = fields.Boolean(default=True)
|
active = fields.Boolean(default=True)
|
||||||
|
|
||||||
|
# --- Related records (Step 2) -------------------------------------------
|
||||||
|
party_ids = fields.One2many(
|
||||||
|
"familylaw.party", "case_id",
|
||||||
|
string="Parties",
|
||||||
|
)
|
||||||
|
child_ids = fields.One2many(
|
||||||
|
"familylaw.child", "case_id",
|
||||||
|
string="Children",
|
||||||
|
)
|
||||||
|
issue_ids = fields.One2many(
|
||||||
|
"familylaw.issue", "case_id",
|
||||||
|
string="Issues",
|
||||||
|
)
|
||||||
|
proceeding_ids = fields.One2many(
|
||||||
|
"familylaw.proceeding", "case_id",
|
||||||
|
string="Proceedings",
|
||||||
|
)
|
||||||
|
conflict_hit_ids = fields.One2many(
|
||||||
|
"familylaw.conflict.hit", "case_id",
|
||||||
|
string="Conflict Screening Hits",
|
||||||
|
)
|
||||||
|
conflict_hit_count = fields.Integer(
|
||||||
|
compute="_compute_conflict_hit_count",
|
||||||
|
string="Conflict Hits",
|
||||||
|
)
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
(
|
||||||
|
"case_number_unique",
|
||||||
|
"UNIQUE(case_number)",
|
||||||
|
"Court case number must be unique. Check the existing cases.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
@api.depends("conflict_hit_ids")
|
||||||
|
def _compute_conflict_hit_count(self):
|
||||||
|
for case in self:
|
||||||
|
case.conflict_hit_count = len(case.conflict_hit_ids)
|
||||||
|
|
||||||
|
# --- Case creation: auto-open an initial proceeding --------------------
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
cases = super().create(vals_list)
|
||||||
|
type_labels = dict(self._fields["case_type"].selection)
|
||||||
|
for case in cases:
|
||||||
|
if case.case_type in _MODIFICATION_TYPES:
|
||||||
|
proc_type = "modification"
|
||||||
|
elif case.case_type == "enforcement":
|
||||||
|
proc_type = "enforcement"
|
||||||
|
else:
|
||||||
|
proc_type = "original"
|
||||||
|
self.env["familylaw.proceeding"].create({
|
||||||
|
"case_id": case.id,
|
||||||
|
"name": _("Initial Proceeding — %s")
|
||||||
|
% type_labels.get(case.case_type, case.case_type),
|
||||||
|
"proceeding_type": proc_type,
|
||||||
|
})
|
||||||
|
return cases
|
||||||
|
|
||||||
|
# --- Conflict screening -------------------------------------------------
|
||||||
|
def action_run_conflict_screening(self):
|
||||||
|
"""Search all party records across all matters for potential conflicts.
|
||||||
|
|
||||||
|
Checks opposing parties on this case against:
|
||||||
|
1. Any party role on any other matter (name match).
|
||||||
|
2. Any client name on any other matter.
|
||||||
|
|
||||||
|
Creates familylaw.conflict.hit records for attorney review.
|
||||||
|
Never auto-clears conflict_check_cleared; that gate stays with the attorney.
|
||||||
|
"""
|
||||||
|
for case in self:
|
||||||
|
case.conflict_hit_ids.unlink()
|
||||||
|
opposing = case.party_ids.filtered(
|
||||||
|
lambda p: p.role == "opposing_party"
|
||||||
|
)
|
||||||
|
if not opposing:
|
||||||
|
case.message_post(
|
||||||
|
body=_(
|
||||||
|
"Conflict screening: no opposing party on record. "
|
||||||
|
"Add an opposing party and re-run."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
role_labels = dict(self.env["familylaw.party"]._fields["role"].selection)
|
||||||
|
hit_count = 0
|
||||||
|
|
||||||
|
for party in opposing:
|
||||||
|
name = (party.name or "").strip()
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 1. Name appears as any party role on another matter
|
||||||
|
for hit in self.env["familylaw.party"].search([
|
||||||
|
("name", "ilike", name),
|
||||||
|
("case_id", "!=", case.id),
|
||||||
|
]):
|
||||||
|
self.env["familylaw.conflict.hit"].create({
|
||||||
|
"case_id": case.id,
|
||||||
|
"party_name": name,
|
||||||
|
"hit_case_id": hit.case_id.id,
|
||||||
|
"hit_role": role_labels.get(hit.role, hit.role),
|
||||||
|
"reason": _(
|
||||||
|
"'%(n)s' appears as '%(r)s' in matter '%(m)s'",
|
||||||
|
n=name,
|
||||||
|
r=role_labels.get(hit.role, hit.role),
|
||||||
|
m=hit.case_id.name,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
hit_count += 1
|
||||||
|
|
||||||
|
# 2. Name matches a client on another matter
|
||||||
|
for cc in self.env["familylaw.case"].search([
|
||||||
|
("client_id.name", "ilike", name),
|
||||||
|
("id", "!=", case.id),
|
||||||
|
]):
|
||||||
|
self.env["familylaw.conflict.hit"].create({
|
||||||
|
"case_id": case.id,
|
||||||
|
"party_name": name,
|
||||||
|
"hit_case_id": cc.id,
|
||||||
|
"hit_role": _("Client"),
|
||||||
|
"reason": _(
|
||||||
|
"'%(n)s' matches client '%(c)s' in matter '%(m)s'",
|
||||||
|
n=name,
|
||||||
|
c=cc.client_id.name,
|
||||||
|
m=cc.name,
|
||||||
|
),
|
||||||
|
})
|
||||||
|
hit_count += 1
|
||||||
|
|
||||||
|
if hit_count:
|
||||||
|
case.message_post(
|
||||||
|
body=_(
|
||||||
|
"⚠️ Conflict screening found %(n)d potential conflict(s). "
|
||||||
|
"Review the Conflict Hits tab — attorney must clear before engaging.",
|
||||||
|
n=hit_count,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
case.message_post(
|
||||||
|
body=_("Conflict screening complete — no potential conflicts detected.")
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
# --- Internal helpers ---------------------------------------------------
|
# --- Internal helpers ---------------------------------------------------
|
||||||
def _ensure_attorney(self):
|
def _ensure_attorney(self):
|
||||||
"""Raise unless the acting user is in the attorney group."""
|
|
||||||
if not self.env.user.has_group(ATTORNEY_GROUP):
|
if not self.env.user.has_group(ATTORNEY_GROUP):
|
||||||
raise UserError(
|
raise UserError(
|
||||||
_("Only a licensed attorney (Family Law / Attorney group) may "
|
_("Only a licensed attorney (Family Law / Attorney group) may "
|
||||||
@@ -134,7 +303,6 @@ class FamilyLawCase(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _require_state(self, allowed):
|
def _require_state(self, allowed):
|
||||||
"""Raise if any record is not in one of the allowed states."""
|
|
||||||
bad = self.filtered(lambda c: c.state not in allowed)
|
bad = self.filtered(lambda c: c.state not in allowed)
|
||||||
if bad:
|
if bad:
|
||||||
raise UserError(
|
raise UserError(
|
||||||
@@ -145,28 +313,26 @@ class FamilyLawCase(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _advance_to(self, target):
|
def _advance_to(self, target):
|
||||||
"""Write the new state and log it on the chatter."""
|
|
||||||
for case in self:
|
for case in self:
|
||||||
case.state = target
|
case.state = target
|
||||||
case.message_post(
|
case.message_post(
|
||||||
body=_("Stage changed to: %s") % dict(
|
body=_("Stage changed to: %s")
|
||||||
self._fields["state"].selection).get(target, target)
|
% dict(self._fields["state"].selection).get(target, target)
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Gated transitions --------------------------------------------------
|
# --- Gated transitions --------------------------------------------------
|
||||||
def action_mark_conflict_cleared(self):
|
def action_mark_conflict_cleared(self):
|
||||||
"""Attorney-only: record that the conflict check is clear."""
|
|
||||||
self._ensure_attorney()
|
self._ensure_attorney()
|
||||||
for case in self:
|
for case in self:
|
||||||
if case.conflict_check_cleared:
|
if case.conflict_check_cleared:
|
||||||
continue
|
continue
|
||||||
case.conflict_check_cleared = True
|
case.conflict_check_cleared = True
|
||||||
case.message_post(body=_("Conflict check cleared by %s.")
|
case.message_post(
|
||||||
% self.env.user.name)
|
body=_("Conflict check cleared by %s.") % self.env.user.name
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def action_engage(self):
|
def action_engage(self):
|
||||||
"""intake -> engaged. Requires a cleared conflict check."""
|
|
||||||
self._require_state(["intake"])
|
self._require_state(["intake"])
|
||||||
not_cleared = self.filtered(lambda c: not c.conflict_check_cleared)
|
not_cleared = self.filtered(lambda c: not c.conflict_check_cleared)
|
||||||
if not_cleared:
|
if not_cleared:
|
||||||
@@ -178,38 +344,32 @@ class FamilyLawCase(models.Model):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def action_start_disclosure(self):
|
def action_start_disclosure(self):
|
||||||
"""engaged -> disclosure (Rule 12.285 phase begins in a later step)."""
|
|
||||||
self._require_state(["engaged"])
|
self._require_state(["engaged"])
|
||||||
self._advance_to("disclosure")
|
self._advance_to("disclosure")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def action_start_discovery(self):
|
def action_start_discovery(self):
|
||||||
"""disclosure -> discovery."""
|
|
||||||
self._require_state(["disclosure"])
|
self._require_state(["disclosure"])
|
||||||
self._advance_to("discovery")
|
self._advance_to("discovery")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def action_start_mediation(self):
|
def action_start_mediation(self):
|
||||||
"""discovery -> mediation (mandatory in Miami-Dade before final hearing)."""
|
|
||||||
self._require_state(["discovery"])
|
self._require_state(["discovery"])
|
||||||
self._advance_to("mediation")
|
self._advance_to("mediation")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def action_set_hearing(self):
|
def action_set_hearing(self):
|
||||||
"""mediation -> hearing / trial."""
|
|
||||||
self._require_state(["mediation"])
|
self._require_state(["mediation"])
|
||||||
self._advance_to("hearing")
|
self._advance_to("hearing")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def action_close(self):
|
def action_close(self):
|
||||||
"""Attorney-only: close the matter from any non-closed stage."""
|
|
||||||
self._ensure_attorney()
|
self._ensure_attorney()
|
||||||
self._require_state([s for s in STATE_SEQUENCE if s != "closed"])
|
self._require_state([s for s in STATE_SEQUENCE if s != "closed"])
|
||||||
self._advance_to("closed")
|
self._advance_to("closed")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def action_reopen(self):
|
def action_reopen(self):
|
||||||
"""Attorney-only: reopen a closed matter back to engaged."""
|
|
||||||
self._ensure_attorney()
|
self._ensure_attorney()
|
||||||
self._require_state(["closed"])
|
self._require_state(["closed"])
|
||||||
self._advance_to("engaged")
|
self._advance_to("engaged")
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class FamilyLawChild(models.Model):
|
||||||
|
_name = "familylaw.child"
|
||||||
|
_description = "Minor Child"
|
||||||
|
_order = "name"
|
||||||
|
|
||||||
|
case_id = fields.Many2one(
|
||||||
|
"familylaw.case",
|
||||||
|
required=True,
|
||||||
|
ondelete="cascade",
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
name = fields.Char(required=True)
|
||||||
|
date_of_birth = fields.Date(string="Date of Birth")
|
||||||
|
age = fields.Integer(
|
||||||
|
compute="_compute_age",
|
||||||
|
string="Age",
|
||||||
|
)
|
||||||
|
notes = fields.Text()
|
||||||
|
|
||||||
|
@api.depends("date_of_birth")
|
||||||
|
def _compute_age(self):
|
||||||
|
today = date.today()
|
||||||
|
for child in self:
|
||||||
|
if not child.date_of_birth:
|
||||||
|
child.age = 0
|
||||||
|
continue
|
||||||
|
dob = child.date_of_birth
|
||||||
|
child.age = (
|
||||||
|
today.year - dob.year
|
||||||
|
- ((today.month, today.day) < (dob.month, dob.day))
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.constrains("date_of_birth")
|
||||||
|
def _check_date_of_birth(self):
|
||||||
|
today = date.today()
|
||||||
|
for child in self:
|
||||||
|
if not child.date_of_birth:
|
||||||
|
continue
|
||||||
|
dob = child.date_of_birth
|
||||||
|
if dob > today:
|
||||||
|
raise ValidationError(
|
||||||
|
"Date of birth cannot be in the future."
|
||||||
|
)
|
||||||
|
age = (
|
||||||
|
today.year - dob.year
|
||||||
|
- ((today.month, today.day) < (dob.month, dob.day))
|
||||||
|
)
|
||||||
|
if age > 25:
|
||||||
|
raise ValidationError(
|
||||||
|
f"Date of birth implies the child is {age} years old. "
|
||||||
|
"Verify the date — this field is for minor children in the matter."
|
||||||
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class FamilyLawConflictHit(models.Model):
|
||||||
|
_name = "familylaw.conflict.hit"
|
||||||
|
_description = "Conflict Screening Hit"
|
||||||
|
_order = "create_date desc"
|
||||||
|
|
||||||
|
case_id = fields.Many2one(
|
||||||
|
"familylaw.case",
|
||||||
|
required=True,
|
||||||
|
ondelete="cascade",
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
party_name = fields.Char(string="Flagged Party Name", required=True)
|
||||||
|
hit_case_id = fields.Many2one(
|
||||||
|
"familylaw.case",
|
||||||
|
string="Found In Matter",
|
||||||
|
)
|
||||||
|
hit_role = fields.Char(string="Role in Matching Matter")
|
||||||
|
reason = fields.Char(string="Reason for Flag")
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""STEP 2 — Intake questionnaire wizard.
|
||||||
|
|
||||||
|
Multi-step TransientModel that:
|
||||||
|
* runs triage FIRST (caller info, opposing party, urgency screen),
|
||||||
|
* branches on urgency: emergency → fast-path on minimum facts;
|
||||||
|
standard → strict (name + client + case_type + county required),
|
||||||
|
* branches into the child-support modification question set,
|
||||||
|
* creates a draft familylaw.case in 'intake' state (and its initial proceeding,
|
||||||
|
via the case model's create() override),
|
||||||
|
* runs conflict screening on the opposing party,
|
||||||
|
* and captures the caller's concern as an attorney question — never answers it.
|
||||||
|
|
||||||
|
No external/network calls. Pure local DB.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
_MODIFICATION_TYPES = {
|
||||||
|
"support_modification",
|
||||||
|
"parenting_modification",
|
||||||
|
"alimony_modification",
|
||||||
|
}
|
||||||
|
|
||||||
|
_CASE_TYPE_SELECTION = [
|
||||||
|
("dissolution_no_children", "Dissolution — no children"),
|
||||||
|
("dissolution_children", "Dissolution — with children"),
|
||||||
|
("paternity", "Paternity"),
|
||||||
|
("support_modification", "Child Support Modification"),
|
||||||
|
("parenting_modification", "Parenting / Time-Sharing Modification"),
|
||||||
|
("alimony_modification", "Alimony Modification"),
|
||||||
|
("enforcement", "Enforcement / Contempt"),
|
||||||
|
("dv_injunction", "Domestic Violence Injunction"),
|
||||||
|
("other", "Other"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class FamilyLawIntakeWizard(models.TransientModel):
|
||||||
|
_name = "familylaw.intake.wizard"
|
||||||
|
_description = "Family Law Intake Questionnaire"
|
||||||
|
|
||||||
|
step = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("triage", "1 — Triage"),
|
||||||
|
("details", "2 — Case Details"),
|
||||||
|
("modification", "3 — Modification Details"),
|
||||||
|
("confirm", "4 — Complete"),
|
||||||
|
],
|
||||||
|
default="triage",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# === TRIAGE (step 1) ====================================================
|
||||||
|
caller_name = fields.Char(string="Caller / Client Name")
|
||||||
|
caller_phone = fields.Char(string="Phone")
|
||||||
|
caller_email = fields.Char(string="Email")
|
||||||
|
county = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("miami_dade", "Miami-Dade (11th Circuit)"),
|
||||||
|
("broward", "Broward (17th Circuit)"),
|
||||||
|
("palm_beach", "Palm Beach (15th Circuit)"),
|
||||||
|
("other_fl", "Other Florida County"),
|
||||||
|
],
|
||||||
|
string="County / Court",
|
||||||
|
default="miami_dade",
|
||||||
|
)
|
||||||
|
case_type = fields.Selection(
|
||||||
|
selection=_CASE_TYPE_SELECTION,
|
||||||
|
string="Case Type",
|
||||||
|
)
|
||||||
|
opposing_party_name = fields.Char(string="Opposing Party Name")
|
||||||
|
has_children = fields.Boolean(string="Minor children involved?")
|
||||||
|
has_existing_order = fields.Boolean(string="Existing court order?")
|
||||||
|
|
||||||
|
# Urgency screen — runs first; triggers the emergency fast-path
|
||||||
|
urgency_child_withheld = fields.Boolean(
|
||||||
|
string="Child being withheld / refused access?"
|
||||||
|
)
|
||||||
|
urgency_removal_threat = fields.Boolean(
|
||||||
|
string="Threat of removal from Florida?"
|
||||||
|
)
|
||||||
|
urgency_violence = fields.Boolean(
|
||||||
|
string="Domestic violence / immediate safety concern?"
|
||||||
|
)
|
||||||
|
urgency_description = fields.Text(
|
||||||
|
string="Describe the Emergency",
|
||||||
|
help="Captured verbatim for the attorney. Do not interpret or advise.",
|
||||||
|
)
|
||||||
|
is_emergency = fields.Boolean(
|
||||||
|
compute="_compute_is_emergency",
|
||||||
|
store=True,
|
||||||
|
string="Emergency",
|
||||||
|
)
|
||||||
|
|
||||||
|
# === STANDARD DETAILS (step 2 — strict path) ============================
|
||||||
|
matter_name = fields.Char(string="Matter Name")
|
||||||
|
client_partner_id = fields.Many2one(
|
||||||
|
"res.partner",
|
||||||
|
string="Client (existing contact)",
|
||||||
|
help="Pick an existing contact or leave blank to create from caller name.",
|
||||||
|
)
|
||||||
|
attorney_id = fields.Many2one("res.users", string="Responsible Attorney")
|
||||||
|
paralegal_id = fields.Many2one("res.users", string="Paralegal")
|
||||||
|
|
||||||
|
# === MODIFICATION DETAILS (step 3 — modification types only) ============
|
||||||
|
existing_order_summary = fields.Text(
|
||||||
|
string="Summary of Existing Order",
|
||||||
|
help="Captured facts from the prior judgment. Not interpreted here — "
|
||||||
|
"prior-judgment interpretation is Step 6+ (attorney-reviewed).",
|
||||||
|
)
|
||||||
|
reason_for_modification = fields.Text(string="Reason for Modification")
|
||||||
|
income_change_details = fields.Text(
|
||||||
|
string="Income / Financial Change Details",
|
||||||
|
help="E.g. 'income dropped from $X to $Y in month/year'. Facts only.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Caller concern — CAPTURED AS ATTORNEY QUESTION, never answered here
|
||||||
|
caller_concern = fields.Text(
|
||||||
|
string="Caller's Question / Concern for the Attorney",
|
||||||
|
help="Record the caller's own words. This is logged on the case as a "
|
||||||
|
"question for the attorney. This software does not answer "
|
||||||
|
"'do I have a good case?' — that determination belongs to the attorney.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# === RESULT (step 4) ====================================================
|
||||||
|
created_case_id = fields.Many2one(
|
||||||
|
"familylaw.case",
|
||||||
|
string="Created Matter",
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
conflict_summary = fields.Text(
|
||||||
|
string="Conflict Screening Results",
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
@api.depends("urgency_child_withheld", "urgency_removal_threat", "urgency_violence")
|
||||||
|
def _compute_is_emergency(self):
|
||||||
|
for w in self:
|
||||||
|
w.is_emergency = bool(
|
||||||
|
w.urgency_child_withheld
|
||||||
|
or w.urgency_removal_threat
|
||||||
|
or w.urgency_violence
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Navigation ---------------------------------------------------------
|
||||||
|
|
||||||
|
def action_next(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if self.step == "triage":
|
||||||
|
if self.is_emergency:
|
||||||
|
self._create_emergency_case()
|
||||||
|
self.step = "confirm"
|
||||||
|
else:
|
||||||
|
self.step = "details"
|
||||||
|
|
||||||
|
elif self.step == "details":
|
||||||
|
self._validate_standard_details()
|
||||||
|
if self.case_type in _MODIFICATION_TYPES:
|
||||||
|
self.step = "modification"
|
||||||
|
else:
|
||||||
|
self._create_standard_case()
|
||||||
|
self.step = "confirm"
|
||||||
|
|
||||||
|
elif self.step == "modification":
|
||||||
|
self._create_standard_case()
|
||||||
|
self.step = "confirm"
|
||||||
|
|
||||||
|
return self._reopen()
|
||||||
|
|
||||||
|
def action_back(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if self.step == "details":
|
||||||
|
self.step = "triage"
|
||||||
|
elif self.step == "modification":
|
||||||
|
self.step = "details"
|
||||||
|
return self._reopen()
|
||||||
|
|
||||||
|
def action_open_case(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": "familylaw.case",
|
||||||
|
"res_id": self.created_case_id.id,
|
||||||
|
"view_mode": "form",
|
||||||
|
"target": "current",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _reopen(self):
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": "familylaw.intake.wizard",
|
||||||
|
"res_id": self.id,
|
||||||
|
"view_mode": "form",
|
||||||
|
"target": "new",
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Validation ---------------------------------------------------------
|
||||||
|
|
||||||
|
def _validate_standard_details(self):
|
||||||
|
errors = []
|
||||||
|
if not self.matter_name:
|
||||||
|
errors.append(_("Matter Name is required."))
|
||||||
|
if not self.client_partner_id and not self.caller_name:
|
||||||
|
errors.append(_("A client contact or caller name is required."))
|
||||||
|
if not self.case_type:
|
||||||
|
errors.append(_("Case Type is required."))
|
||||||
|
if not self.county:
|
||||||
|
errors.append(_("County / Court is required."))
|
||||||
|
if errors:
|
||||||
|
raise UserError("\n".join(errors))
|
||||||
|
|
||||||
|
# --- Case creation ------------------------------------------------------
|
||||||
|
|
||||||
|
def _resolve_or_create_partner(self):
|
||||||
|
if self.client_partner_id:
|
||||||
|
return self.client_partner_id
|
||||||
|
return self.env["res.partner"].create({
|
||||||
|
"name": self.caller_name or _("Unknown — Emergency Intake"),
|
||||||
|
"phone": self.caller_phone or False,
|
||||||
|
"email": self.caller_email or False,
|
||||||
|
})
|
||||||
|
|
||||||
|
def _create_emergency_case(self):
|
||||||
|
"""Fast-path: open on minimum facts — who, which child, what's happening."""
|
||||||
|
partner = self._resolve_or_create_partner()
|
||||||
|
|
||||||
|
flags = []
|
||||||
|
if self.urgency_child_withheld:
|
||||||
|
flags.append(_("Child withheld / refused access"))
|
||||||
|
if self.urgency_removal_threat:
|
||||||
|
flags.append(_("Threat of removal from Florida"))
|
||||||
|
if self.urgency_violence:
|
||||||
|
flags.append(_("Domestic violence / safety concern"))
|
||||||
|
urgency_text = "; ".join(flags)
|
||||||
|
if self.urgency_description:
|
||||||
|
urgency_text += "\n" + self.urgency_description
|
||||||
|
|
||||||
|
case = self.env["familylaw.case"].create({
|
||||||
|
"name": self.matter_name or _("EMERGENCY — %s") % partner.name,
|
||||||
|
"client_id": partner.id,
|
||||||
|
"case_type": self.case_type or "other",
|
||||||
|
"county": self.county or "miami_dade",
|
||||||
|
"is_emergency": True,
|
||||||
|
"urgency_notes": urgency_text,
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.opposing_party_name:
|
||||||
|
self.env["familylaw.party"].create({
|
||||||
|
"case_id": case.id,
|
||||||
|
"name": self.opposing_party_name,
|
||||||
|
"role": "opposing_party",
|
||||||
|
})
|
||||||
|
|
||||||
|
self._post_caller_concern(case)
|
||||||
|
self._run_screening_and_summarise(case)
|
||||||
|
self.created_case_id = case
|
||||||
|
|
||||||
|
def _create_standard_case(self):
|
||||||
|
"""Strict path: all required fields validated before reaching here."""
|
||||||
|
partner = self._resolve_or_create_partner()
|
||||||
|
|
||||||
|
case = self.env["familylaw.case"].create({
|
||||||
|
"name": self.matter_name,
|
||||||
|
"client_id": partner.id,
|
||||||
|
"case_type": self.case_type,
|
||||||
|
"county": self.county or "miami_dade",
|
||||||
|
"attorney_id": self.attorney_id.id if self.attorney_id else False,
|
||||||
|
"paralegal_id": self.paralegal_id.id if self.paralegal_id else False,
|
||||||
|
"is_emergency": False,
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.opposing_party_name:
|
||||||
|
self.env["familylaw.party"].create({
|
||||||
|
"case_id": case.id,
|
||||||
|
"name": self.opposing_party_name,
|
||||||
|
"role": "opposing_party",
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.reason_for_modification:
|
||||||
|
case.message_post(
|
||||||
|
body=_("Modification reason (captured at intake): %s")
|
||||||
|
% self.reason_for_modification
|
||||||
|
)
|
||||||
|
if self.existing_order_summary:
|
||||||
|
case.message_post(
|
||||||
|
body=_("Existing order summary (captured at intake; not legally "
|
||||||
|
"interpreted here — see Step 6+ for prior-judgment extraction): %s")
|
||||||
|
% self.existing_order_summary
|
||||||
|
)
|
||||||
|
|
||||||
|
self._post_caller_concern(case)
|
||||||
|
self._run_screening_and_summarise(case)
|
||||||
|
self.created_case_id = case
|
||||||
|
|
||||||
|
def _post_caller_concern(self, case):
|
||||||
|
if self.caller_concern:
|
||||||
|
case.message_post(
|
||||||
|
body=_(
|
||||||
|
"ATTORNEY QUESTION (captured at intake — this software does "
|
||||||
|
"not answer this; for attorney review only): %s"
|
||||||
|
) % self.caller_concern
|
||||||
|
)
|
||||||
|
|
||||||
|
def _run_screening_and_summarise(self, case):
|
||||||
|
case.action_run_conflict_screening()
|
||||||
|
hits = case.conflict_hit_ids
|
||||||
|
if hits:
|
||||||
|
lines = [
|
||||||
|
_("⚠️ %(n)d potential conflict(s) found — attorney review required "
|
||||||
|
"before engaging:", n=len(hits))
|
||||||
|
]
|
||||||
|
for h in hits:
|
||||||
|
lines.append(" • " + (h.reason or h.party_name))
|
||||||
|
self.conflict_summary = "\n".join(lines)
|
||||||
|
else:
|
||||||
|
self.conflict_summary = _("No potential conflicts detected.")
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class FamilyLawIssue(models.Model):
|
||||||
|
_name = "familylaw.issue"
|
||||||
|
_description = "Contested Issue"
|
||||||
|
_order = "issue_type"
|
||||||
|
|
||||||
|
case_id = fields.Many2one(
|
||||||
|
"familylaw.case",
|
||||||
|
required=True,
|
||||||
|
ondelete="cascade",
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
issue_type = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("time_sharing", "Time-Sharing / Parental Responsibility"),
|
||||||
|
("child_support", "Child Support"),
|
||||||
|
("equitable_distribution", "Equitable Distribution"),
|
||||||
|
("alimony", "Alimony / Spousal Support"),
|
||||||
|
("attorneys_fees", "Attorney's Fees"),
|
||||||
|
("other", "Other"),
|
||||||
|
],
|
||||||
|
string="Issue",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
name = fields.Char(
|
||||||
|
compute="_compute_name",
|
||||||
|
store=True,
|
||||||
|
string="Issue",
|
||||||
|
)
|
||||||
|
description = fields.Text()
|
||||||
|
|
||||||
|
@api.depends("issue_type")
|
||||||
|
def _compute_name(self):
|
||||||
|
labels = dict(self._fields["issue_type"].selection)
|
||||||
|
for issue in self:
|
||||||
|
issue.name = labels.get(issue.issue_type, "")
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class FamilyLawParty(models.Model):
|
||||||
|
_name = "familylaw.party"
|
||||||
|
_description = "Case Party"
|
||||||
|
_inherit = ["mail.thread"]
|
||||||
|
_order = "role, name"
|
||||||
|
|
||||||
|
case_id = fields.Many2one(
|
||||||
|
"familylaw.case",
|
||||||
|
required=True,
|
||||||
|
ondelete="cascade",
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
partner_id = fields.Many2one(
|
||||||
|
"res.partner",
|
||||||
|
string="Contact Record",
|
||||||
|
help="Link to an existing contact. Name is copied to the Name field.",
|
||||||
|
)
|
||||||
|
name = fields.Char(
|
||||||
|
required=True,
|
||||||
|
tracking=True,
|
||||||
|
help="Full legal name of this party.",
|
||||||
|
)
|
||||||
|
role = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("client", "Client"),
|
||||||
|
("opposing_party", "Opposing Party"),
|
||||||
|
("opposing_counsel", "Opposing Counsel"),
|
||||||
|
("other", "Other"),
|
||||||
|
],
|
||||||
|
required=True,
|
||||||
|
default="opposing_party",
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
notes = fields.Text()
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class FamilyLawProceeding(models.Model):
|
||||||
|
_name = "familylaw.proceeding"
|
||||||
|
_description = "Proceeding"
|
||||||
|
_inherit = ["mail.thread"]
|
||||||
|
_order = "date_opened desc"
|
||||||
|
|
||||||
|
case_id = fields.Many2one(
|
||||||
|
"familylaw.case",
|
||||||
|
required=True,
|
||||||
|
ondelete="cascade",
|
||||||
|
index=True,
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
name = fields.Char(
|
||||||
|
string="Proceeding",
|
||||||
|
required=True,
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
proceeding_type = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("original", "Original Action"),
|
||||||
|
("modification", "Modification"),
|
||||||
|
("enforcement", "Enforcement / Contempt"),
|
||||||
|
("appeal", "Appeal"),
|
||||||
|
("other", "Other"),
|
||||||
|
],
|
||||||
|
string="Type",
|
||||||
|
required=True,
|
||||||
|
default="original",
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
state = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("open", "Open"),
|
||||||
|
("closed", "Closed"),
|
||||||
|
],
|
||||||
|
default="open",
|
||||||
|
required=True,
|
||||||
|
tracking=True,
|
||||||
|
)
|
||||||
|
proceeding_number = fields.Char(
|
||||||
|
string="Court-Assigned Number",
|
||||||
|
help="If the court assigns a distinct number for this proceeding.",
|
||||||
|
)
|
||||||
|
date_opened = fields.Date(
|
||||||
|
string="Date Opened",
|
||||||
|
default=fields.Date.context_today,
|
||||||
|
)
|
||||||
|
date_closed = fields.Date(string="Date Closed")
|
||||||
|
notes = fields.Text()
|
||||||
|
|
||||||
|
def action_close_proceeding(self):
|
||||||
|
for proc in self:
|
||||||
|
proc.state = "closed"
|
||||||
|
proc.date_closed = fields.Date.context_today(self)
|
||||||
|
proc.message_post(body="Proceeding closed.")
|
||||||
|
|
||||||
|
def action_reopen_proceeding(self):
|
||||||
|
for proc in self:
|
||||||
|
proc.state = "open"
|
||||||
|
proc.date_closed = False
|
||||||
|
proc.message_post(body="Proceeding reopened.")
|
||||||
@@ -1,3 +1,14 @@
|
|||||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
access_familylaw_case_user,familylaw.case staff,model_familylaw_case,group_familylaw_user,1,1,1,0
|
access_familylaw_case_user,familylaw.case staff,model_familylaw_case,group_familylaw_user,1,1,1,0
|
||||||
access_familylaw_case_attorney,familylaw.case attorney,model_familylaw_case,group_familylaw_attorney,1,1,1,1
|
access_familylaw_case_attorney,familylaw.case attorney,model_familylaw_case,group_familylaw_attorney,1,1,1,1
|
||||||
|
access_familylaw_party_user,familylaw.party staff,model_familylaw_party,group_familylaw_user,1,1,1,0
|
||||||
|
access_familylaw_party_attorney,familylaw.party attorney,model_familylaw_party,group_familylaw_attorney,1,1,1,1
|
||||||
|
access_familylaw_child_user,familylaw.child staff,model_familylaw_child,group_familylaw_user,1,1,1,0
|
||||||
|
access_familylaw_child_attorney,familylaw.child attorney,model_familylaw_child,group_familylaw_attorney,1,1,1,1
|
||||||
|
access_familylaw_issue_user,familylaw.issue staff,model_familylaw_issue,group_familylaw_user,1,1,1,0
|
||||||
|
access_familylaw_issue_attorney,familylaw.issue attorney,model_familylaw_issue,group_familylaw_attorney,1,1,1,1
|
||||||
|
access_familylaw_proceeding_user,familylaw.proceeding staff,model_familylaw_proceeding,group_familylaw_user,1,1,1,0
|
||||||
|
access_familylaw_proceeding_attorney,familylaw.proceeding attorney,model_familylaw_proceeding,group_familylaw_attorney,1,1,1,1
|
||||||
|
access_familylaw_conflict_hit_user,familylaw.conflict.hit staff,model_familylaw_conflict_hit,group_familylaw_user,1,1,1,0
|
||||||
|
access_familylaw_conflict_hit_attorney,familylaw.conflict.hit attorney,model_familylaw_conflict_hit,group_familylaw_attorney,1,1,1,1
|
||||||
|
access_familylaw_intake_wizard_user,familylaw.intake.wizard user,model_familylaw_intake_wizard,base.group_user,1,1,1,1
|
||||||
|
|||||||
|
@@ -1 +1,2 @@
|
|||||||
from . import test_case_lifecycle
|
from . import test_case_lifecycle
|
||||||
|
from . import test_step2
|
||||||
|
|||||||
@@ -0,0 +1,460 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""STEP 2 tests — parties, children, issues, proceedings, conflict screening,
|
||||||
|
intake questionnaire (strict / emergency fast-path).
|
||||||
|
|
||||||
|
Run just this step:
|
||||||
|
odoo -d <db> -u activeblue_familylaw --test-enable \
|
||||||
|
--test-tags familylaw_step2 --stop-after-init
|
||||||
|
|
||||||
|
All tests roll back inside a savepoint. No network calls.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
from odoo.tests.common import TransactionCase, new_test_user, tagged
|
||||||
|
from odoo.exceptions import UserError, ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("post_install", "-at_install", "familylaw", "familylaw_step2")
|
||||||
|
class TestStep2RelationsAndProceeding(TransactionCase):
|
||||||
|
"""Relation integrity, DOB constraint, initial proceeding creation."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.partner_a = cls.env["res.partner"].create({"name": "Client A"})
|
||||||
|
cls.partner_b = cls.env["res.partner"].create({"name": "Client B"})
|
||||||
|
cls.attorney = new_test_user(
|
||||||
|
cls.env,
|
||||||
|
login="fl_atty2",
|
||||||
|
name="Test Attorney 2",
|
||||||
|
email="atty2@example.com",
|
||||||
|
groups="base.group_user,activeblue_familylaw.group_familylaw_attorney",
|
||||||
|
)
|
||||||
|
cls.paralegal = new_test_user(
|
||||||
|
cls.env,
|
||||||
|
login="fl_para2",
|
||||||
|
name="Test Paralegal 2",
|
||||||
|
email="para2@example.com",
|
||||||
|
groups="base.group_user,activeblue_familylaw.group_familylaw_user",
|
||||||
|
)
|
||||||
|
cls.Case = cls.env["familylaw.case"]
|
||||||
|
|
||||||
|
def _make_case(self, case_type="dissolution_children", **kw):
|
||||||
|
vals = {
|
||||||
|
"name": "Test Matter",
|
||||||
|
"client_id": self.partner_a.id,
|
||||||
|
"case_type": case_type,
|
||||||
|
}
|
||||||
|
vals.update(kw)
|
||||||
|
return self.Case.create(vals)
|
||||||
|
|
||||||
|
# --- initial proceeding -------------------------------------------------
|
||||||
|
def test_01_case_create_opens_initial_proceeding(self):
|
||||||
|
case = self._make_case()
|
||||||
|
self.assertEqual(len(case.proceeding_ids), 1)
|
||||||
|
proc = case.proceeding_ids[0]
|
||||||
|
self.assertEqual(proc.state, "open")
|
||||||
|
|
||||||
|
def test_02_modification_case_creates_modification_proceeding(self):
|
||||||
|
case = self._make_case(case_type="support_modification")
|
||||||
|
proc = case.proceeding_ids[0]
|
||||||
|
self.assertEqual(proc.proceeding_type, "modification")
|
||||||
|
|
||||||
|
def test_03_original_case_creates_original_proceeding(self):
|
||||||
|
case = self._make_case(case_type="dissolution_children")
|
||||||
|
proc = case.proceeding_ids[0]
|
||||||
|
self.assertEqual(proc.proceeding_type, "original")
|
||||||
|
|
||||||
|
# --- multiple proceedings -----------------------------------------------
|
||||||
|
def test_04_case_can_hold_multiple_proceedings(self):
|
||||||
|
case = self._make_case()
|
||||||
|
self.env["familylaw.proceeding"].create({
|
||||||
|
"case_id": case.id,
|
||||||
|
"name": "Second Proceeding",
|
||||||
|
"proceeding_type": "modification",
|
||||||
|
})
|
||||||
|
self.assertEqual(len(case.proceeding_ids), 2)
|
||||||
|
states = set(case.proceeding_ids.mapped("state"))
|
||||||
|
self.assertEqual(states, {"open"})
|
||||||
|
|
||||||
|
def test_05_proceeding_states_are_independent(self):
|
||||||
|
case = self._make_case()
|
||||||
|
extra = self.env["familylaw.proceeding"].create({
|
||||||
|
"case_id": case.id,
|
||||||
|
"name": "Extra",
|
||||||
|
"proceeding_type": "enforcement",
|
||||||
|
})
|
||||||
|
extra.action_close_proceeding()
|
||||||
|
self.assertEqual(extra.state, "closed")
|
||||||
|
# Initial proceeding stays open
|
||||||
|
initial = case.proceeding_ids.filtered(lambda p: p != extra)
|
||||||
|
self.assertEqual(initial.state, "open")
|
||||||
|
|
||||||
|
# --- parties ------------------------------------------------------------
|
||||||
|
def test_06_party_links_to_case(self):
|
||||||
|
case = self._make_case()
|
||||||
|
party = self.env["familylaw.party"].create({
|
||||||
|
"case_id": case.id,
|
||||||
|
"name": "Jane Doe",
|
||||||
|
"role": "opposing_party",
|
||||||
|
})
|
||||||
|
self.assertIn(party, case.party_ids)
|
||||||
|
|
||||||
|
# --- children -----------------------------------------------------------
|
||||||
|
def test_07_child_links_to_case(self):
|
||||||
|
case = self._make_case()
|
||||||
|
child = self.env["familylaw.child"].create({
|
||||||
|
"case_id": case.id,
|
||||||
|
"name": "Minor Child A",
|
||||||
|
"date_of_birth": date.today() - timedelta(days=365 * 5),
|
||||||
|
})
|
||||||
|
self.assertIn(child, case.child_ids)
|
||||||
|
|
||||||
|
def test_08_dob_future_rejected(self):
|
||||||
|
case = self._make_case()
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
self.env["familylaw.child"].create({
|
||||||
|
"case_id": case.id,
|
||||||
|
"name": "Future Child",
|
||||||
|
"date_of_birth": date.today() + timedelta(days=1),
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_09_dob_over_25_rejected(self):
|
||||||
|
case = self._make_case()
|
||||||
|
implausible_dob = date.today().replace(year=date.today().year - 26)
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
self.env["familylaw.child"].create({
|
||||||
|
"case_id": case.id,
|
||||||
|
"name": "Old Child",
|
||||||
|
"date_of_birth": implausible_dob,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_10_dob_valid_accepted(self):
|
||||||
|
case = self._make_case()
|
||||||
|
valid_dob = date.today() - timedelta(days=365 * 8)
|
||||||
|
child = self.env["familylaw.child"].create({
|
||||||
|
"case_id": case.id,
|
||||||
|
"name": "Young Child",
|
||||||
|
"date_of_birth": valid_dob,
|
||||||
|
})
|
||||||
|
self.assertTrue(child.id)
|
||||||
|
self.assertEqual(child.age, 8)
|
||||||
|
|
||||||
|
# --- issues -------------------------------------------------------------
|
||||||
|
def test_11_issue_links_to_case(self):
|
||||||
|
case = self._make_case()
|
||||||
|
issue = self.env["familylaw.issue"].create({
|
||||||
|
"case_id": case.id,
|
||||||
|
"issue_type": "child_support",
|
||||||
|
})
|
||||||
|
self.assertIn(issue, case.issue_ids)
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("post_install", "-at_install", "familylaw", "familylaw_step2")
|
||||||
|
class TestStep2SearchByPerson(TransactionCase):
|
||||||
|
"""Find a case by party name, child name, and case_number."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.partner = cls.env["res.partner"].create({"name": "Search Client"})
|
||||||
|
cls.case = cls.env["familylaw.case"].create({
|
||||||
|
"name": "Search Matter",
|
||||||
|
"client_id": cls.partner.id,
|
||||||
|
"case_type": "dissolution_children",
|
||||||
|
"case_number": "2024-DR-999999",
|
||||||
|
})
|
||||||
|
cls.env["familylaw.party"].create({
|
||||||
|
"case_id": cls.case.id,
|
||||||
|
"name": "John Opposing",
|
||||||
|
"role": "opposing_party",
|
||||||
|
})
|
||||||
|
cls.env["familylaw.child"].create({
|
||||||
|
"case_id": cls.case.id,
|
||||||
|
"name": "Little One",
|
||||||
|
"date_of_birth": date.today() - timedelta(days=365 * 6),
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_12_find_by_case_number(self):
|
||||||
|
results = self.env["familylaw.case"].search(
|
||||||
|
[("case_number", "=", "2024-DR-999999")]
|
||||||
|
)
|
||||||
|
self.assertIn(self.case, results)
|
||||||
|
|
||||||
|
def test_13_find_by_party_name(self):
|
||||||
|
results = self.env["familylaw.case"].search(
|
||||||
|
[("party_ids.name", "ilike", "John Opposing")]
|
||||||
|
)
|
||||||
|
self.assertIn(self.case, results)
|
||||||
|
|
||||||
|
def test_14_find_by_child_name(self):
|
||||||
|
results = self.env["familylaw.case"].search(
|
||||||
|
[("child_ids.name", "ilike", "Little One")]
|
||||||
|
)
|
||||||
|
self.assertIn(self.case, results)
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("post_install", "-at_install", "familylaw", "familylaw_step2")
|
||||||
|
class TestStep2ConflictScreening(TransactionCase):
|
||||||
|
"""Conflict screening surfaces a past-client opposing party; does not auto-clear."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.attorney = new_test_user(
|
||||||
|
cls.env,
|
||||||
|
login="fl_atty_conflict",
|
||||||
|
name="Conflict Attorney",
|
||||||
|
email="catty@example.com",
|
||||||
|
groups="base.group_user,activeblue_familylaw.group_familylaw_attorney",
|
||||||
|
)
|
||||||
|
# Case A: "Old Client" is the client
|
||||||
|
cls.old_client = cls.env["res.partner"].create({"name": "Old Client"})
|
||||||
|
cls.case_a = cls.env["familylaw.case"].create({
|
||||||
|
"name": "Old Matter",
|
||||||
|
"client_id": cls.old_client.id,
|
||||||
|
"case_type": "dissolution_children",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Case B: "Old Client" appears as an opposing party
|
||||||
|
cls.new_client = cls.env["res.partner"].create({"name": "New Client"})
|
||||||
|
cls.case_b = cls.env["familylaw.case"].create({
|
||||||
|
"name": "New Matter",
|
||||||
|
"client_id": cls.new_client.id,
|
||||||
|
"case_type": "support_modification",
|
||||||
|
})
|
||||||
|
cls.env["familylaw.party"].create({
|
||||||
|
"case_id": cls.case_b.id,
|
||||||
|
"name": "Old Client",
|
||||||
|
"role": "opposing_party",
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_15_conflict_screening_finds_client_match(self):
|
||||||
|
self.case_b.action_run_conflict_screening()
|
||||||
|
hits = self.case_b.conflict_hit_ids
|
||||||
|
self.assertTrue(hits, "Expected at least one conflict hit for 'Old Client'")
|
||||||
|
|
||||||
|
def test_16_conflict_does_not_auto_clear(self):
|
||||||
|
self.case_b.action_run_conflict_screening()
|
||||||
|
self.assertFalse(
|
||||||
|
self.case_b.conflict_check_cleared,
|
||||||
|
"Conflict screening must never auto-clear the attorney gate.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_17_conflict_hit_count_updates(self):
|
||||||
|
self.case_b.action_run_conflict_screening()
|
||||||
|
self.assertGreater(self.case_b.conflict_hit_count, 0)
|
||||||
|
|
||||||
|
def test_18_engage_blocked_with_uncleared_conflict(self):
|
||||||
|
"""Ensure Step 1's engage gate still works after Step 2 additions."""
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
self.case_b.action_engage()
|
||||||
|
|
||||||
|
def test_19_attorney_can_clear_conflict_after_reviewing_hits(self):
|
||||||
|
self.case_b.action_run_conflict_screening()
|
||||||
|
self.case_b.with_user(self.attorney).action_mark_conflict_cleared()
|
||||||
|
self.assertTrue(self.case_b.conflict_check_cleared)
|
||||||
|
|
||||||
|
def test_20_no_conflict_for_unrelated_opposing_party(self):
|
||||||
|
unique_client = self.env["res.partner"].create({"name": "Completely Unique Person XYZ"})
|
||||||
|
case_c = self.env["familylaw.case"].create({
|
||||||
|
"name": "Unrelated Matter",
|
||||||
|
"client_id": unique_client.id,
|
||||||
|
"case_type": "paternity",
|
||||||
|
})
|
||||||
|
self.env["familylaw.party"].create({
|
||||||
|
"case_id": case_c.id,
|
||||||
|
"name": "Completely Unique Opposing XYZ",
|
||||||
|
"role": "opposing_party",
|
||||||
|
})
|
||||||
|
case_c.action_run_conflict_screening()
|
||||||
|
self.assertEqual(case_c.conflict_hit_count, 0)
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("post_install", "-at_install", "familylaw", "familylaw_step2")
|
||||||
|
class TestStep2IntakeWizard(TransactionCase):
|
||||||
|
"""Intake questionnaire — strict path, emergency fast-path, triage, question capture."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.Wizard = cls.env["familylaw.intake.wizard"]
|
||||||
|
cls.partner = cls.env["res.partner"].create({"name": "Existing Client"})
|
||||||
|
|
||||||
|
def _make_wizard(self, **vals):
|
||||||
|
return self.Wizard.create(vals)
|
||||||
|
|
||||||
|
# --- standard path strict validation ------------------------------------
|
||||||
|
def test_21_standard_path_requires_matter_name(self):
|
||||||
|
wiz = self._make_wizard(
|
||||||
|
step="details",
|
||||||
|
case_type="dissolution_children",
|
||||||
|
county="miami_dade",
|
||||||
|
caller_name="Caller X",
|
||||||
|
)
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
wiz._validate_standard_details()
|
||||||
|
|
||||||
|
def test_22_standard_path_requires_case_type(self):
|
||||||
|
wiz = self._make_wizard(
|
||||||
|
step="details",
|
||||||
|
matter_name="Test Matter",
|
||||||
|
county="miami_dade",
|
||||||
|
caller_name="Caller X",
|
||||||
|
)
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
wiz._validate_standard_details()
|
||||||
|
|
||||||
|
def test_23_standard_path_requires_county(self):
|
||||||
|
wiz = self._make_wizard(
|
||||||
|
step="details",
|
||||||
|
matter_name="Test Matter",
|
||||||
|
case_type="dissolution_children",
|
||||||
|
caller_name="Caller X",
|
||||||
|
county=False,
|
||||||
|
)
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
wiz._validate_standard_details()
|
||||||
|
|
||||||
|
def test_24_standard_path_requires_client_or_caller_name(self):
|
||||||
|
wiz = self._make_wizard(
|
||||||
|
step="details",
|
||||||
|
matter_name="Test Matter",
|
||||||
|
case_type="dissolution_children",
|
||||||
|
county="miami_dade",
|
||||||
|
)
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
wiz._validate_standard_details()
|
||||||
|
|
||||||
|
def test_25_standard_path_creates_case(self):
|
||||||
|
wiz = self._make_wizard(
|
||||||
|
caller_name="John Smith",
|
||||||
|
case_type="dissolution_children",
|
||||||
|
county="miami_dade",
|
||||||
|
)
|
||||||
|
wiz.matter_name = "In re Smith"
|
||||||
|
wiz.client_partner_id = self.partner.id
|
||||||
|
wiz._create_standard_case()
|
||||||
|
case = wiz.created_case_id
|
||||||
|
self.assertTrue(case)
|
||||||
|
self.assertEqual(case.state, "intake")
|
||||||
|
self.assertFalse(case.is_emergency)
|
||||||
|
|
||||||
|
def test_26_standard_case_gets_initial_proceeding(self):
|
||||||
|
wiz = self._make_wizard(
|
||||||
|
caller_name="Jane Doe",
|
||||||
|
case_type="support_modification",
|
||||||
|
county="miami_dade",
|
||||||
|
)
|
||||||
|
wiz.matter_name = "In re Doe"
|
||||||
|
wiz.client_partner_id = self.partner.id
|
||||||
|
wiz._create_standard_case()
|
||||||
|
case = wiz.created_case_id
|
||||||
|
self.assertEqual(len(case.proceeding_ids), 1)
|
||||||
|
self.assertEqual(case.proceeding_ids[0].proceeding_type, "modification")
|
||||||
|
|
||||||
|
# --- triage sets case type ----------------------------------------------
|
||||||
|
def test_27_triage_case_type_propagates_to_case(self):
|
||||||
|
wiz = self._make_wizard(
|
||||||
|
caller_name="Alice",
|
||||||
|
case_type="paternity",
|
||||||
|
county="miami_dade",
|
||||||
|
)
|
||||||
|
wiz.matter_name = "Paternity Matter"
|
||||||
|
wiz.client_partner_id = self.partner.id
|
||||||
|
wiz._create_standard_case()
|
||||||
|
self.assertEqual(wiz.created_case_id.case_type, "paternity")
|
||||||
|
|
||||||
|
# --- emergency fast-path ------------------------------------------------
|
||||||
|
def test_28_emergency_path_creates_case_on_minimum_facts(self):
|
||||||
|
wiz = self._make_wizard(
|
||||||
|
caller_name="Emergency Caller",
|
||||||
|
urgency_child_withheld=True,
|
||||||
|
)
|
||||||
|
self.assertTrue(wiz.is_emergency)
|
||||||
|
wiz._create_emergency_case()
|
||||||
|
case = wiz.created_case_id
|
||||||
|
self.assertTrue(case, "Emergency case should be created")
|
||||||
|
self.assertEqual(case.state, "intake")
|
||||||
|
self.assertTrue(case.is_emergency)
|
||||||
|
|
||||||
|
def test_29_emergency_flag_set_on_case(self):
|
||||||
|
wiz = self._make_wizard(
|
||||||
|
caller_name="Urgent Caller",
|
||||||
|
urgency_violence=True,
|
||||||
|
)
|
||||||
|
wiz._create_emergency_case()
|
||||||
|
self.assertTrue(wiz.created_case_id.is_emergency)
|
||||||
|
|
||||||
|
def test_30_emergency_creates_partner_from_caller_name(self):
|
||||||
|
wiz = self._make_wizard(
|
||||||
|
caller_name="Brand New Person",
|
||||||
|
urgency_removal_threat=True,
|
||||||
|
)
|
||||||
|
wiz._create_emergency_case()
|
||||||
|
case = wiz.created_case_id
|
||||||
|
self.assertEqual(case.client_id.name, "Brand New Person")
|
||||||
|
|
||||||
|
def test_31_emergency_urgency_notes_captured(self):
|
||||||
|
wiz = self._make_wizard(
|
||||||
|
caller_name="Person In Danger",
|
||||||
|
urgency_violence=True,
|
||||||
|
urgency_description="Ex-partner threatened caller this morning.",
|
||||||
|
)
|
||||||
|
wiz._create_emergency_case()
|
||||||
|
self.assertIn("Ex-partner", wiz.created_case_id.urgency_notes)
|
||||||
|
|
||||||
|
# --- caller concern captured as attorney question, never answered -------
|
||||||
|
def test_32_caller_concern_logged_on_case_not_answered(self):
|
||||||
|
wiz = self._make_wizard(
|
||||||
|
caller_name="Concerned Caller",
|
||||||
|
case_type="support_modification",
|
||||||
|
county="miami_dade",
|
||||||
|
caller_concern="Do I have a good case?",
|
||||||
|
)
|
||||||
|
wiz.matter_name = "Concern Matter"
|
||||||
|
wiz.client_partner_id = self.partner.id
|
||||||
|
wiz._create_standard_case()
|
||||||
|
case = wiz.created_case_id
|
||||||
|
chatter_bodies = " ".join(
|
||||||
|
msg.body for msg in case.message_ids if msg.body
|
||||||
|
)
|
||||||
|
self.assertIn("Do I have a good case?", chatter_bodies)
|
||||||
|
self.assertIn("attorney", chatter_bodies.lower())
|
||||||
|
|
||||||
|
def test_33_emergency_concern_also_logged(self):
|
||||||
|
wiz = self._make_wizard(
|
||||||
|
caller_name="Emergency Concern",
|
||||||
|
urgency_child_withheld=True,
|
||||||
|
caller_concern="Am I going to win this?",
|
||||||
|
)
|
||||||
|
wiz._create_emergency_case()
|
||||||
|
case = wiz.created_case_id
|
||||||
|
chatter_bodies = " ".join(msg.body for msg in case.message_ids if msg.body)
|
||||||
|
self.assertIn("Am I going to win this?", chatter_bodies)
|
||||||
|
|
||||||
|
# --- opposing party + conflict screening via wizard ---------------------
|
||||||
|
def test_34_wizard_adds_opposing_party_and_screens(self):
|
||||||
|
# Set up an existing case with this person as a client
|
||||||
|
existing_client = self.env["res.partner"].create({"name": "Opposing Client Match"})
|
||||||
|
self.env["familylaw.case"].create({
|
||||||
|
"name": "Existing Matter",
|
||||||
|
"client_id": existing_client.id,
|
||||||
|
"case_type": "dissolution_children",
|
||||||
|
})
|
||||||
|
|
||||||
|
wiz = self._make_wizard(
|
||||||
|
caller_name="New Caller",
|
||||||
|
case_type="enforcement",
|
||||||
|
county="miami_dade",
|
||||||
|
opposing_party_name="Opposing Client Match",
|
||||||
|
)
|
||||||
|
wiz.matter_name = "Enforcement Matter"
|
||||||
|
wiz.client_partner_id = self.partner.id
|
||||||
|
wiz._create_standard_case()
|
||||||
|
case = wiz.created_case_id
|
||||||
|
# Should have at least one party record
|
||||||
|
self.assertTrue(case.party_ids)
|
||||||
|
# Should have at least one conflict hit
|
||||||
|
self.assertGreater(case.conflict_hit_count, 0)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
|
|
||||||
<!-- LIST (Odoo 18 uses <list>, not <tree>) -->
|
<!-- LIST view -->
|
||||||
<record id="view_familylaw_case_list" model="ir.ui.view">
|
<record id="view_familylaw_case_list" model="ir.ui.view">
|
||||||
<field name="name">familylaw.case.list</field>
|
<field name="name">familylaw.case.list</field>
|
||||||
<field name="model">familylaw.case</field>
|
<field name="model">familylaw.case</field>
|
||||||
@@ -9,10 +9,12 @@
|
|||||||
<list string="Cases">
|
<list string="Cases">
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="client_id"/>
|
<field name="client_id"/>
|
||||||
|
<field name="case_number"/>
|
||||||
<field name="case_type"/>
|
<field name="case_type"/>
|
||||||
<field name="attorney_id"/>
|
<field name="attorney_id"/>
|
||||||
<field name="representation"/>
|
<field name="representation"/>
|
||||||
<field name="conflict_check_cleared"/>
|
<field name="conflict_check_cleared"/>
|
||||||
|
<field name="is_emergency" widget="boolean_toggle" optional="show"/>
|
||||||
<field name="state" widget="badge"
|
<field name="state" widget="badge"
|
||||||
decoration-success="state == 'closed'"
|
decoration-success="state == 'closed'"
|
||||||
decoration-info="state in ('intake','engaged')"
|
decoration-info="state in ('intake','engaged')"
|
||||||
@@ -28,12 +30,16 @@
|
|||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="Case">
|
<form string="Case">
|
||||||
<header>
|
<header>
|
||||||
|
<!-- Conflict screening — available to all users -->
|
||||||
|
<button name="action_run_conflict_screening" type="object"
|
||||||
|
string="Run Conflict Screening"
|
||||||
|
invisible="state != 'intake'"/>
|
||||||
<!-- Attorney-only gate: clear the conflict check -->
|
<!-- Attorney-only gate: clear the conflict check -->
|
||||||
<button name="action_mark_conflict_cleared" type="object"
|
<button name="action_mark_conflict_cleared" type="object"
|
||||||
string="Clear Conflict Check" class="btn-primary"
|
string="Clear Conflict Check" class="btn-warning"
|
||||||
invisible="conflict_check_cleared"
|
invisible="conflict_check_cleared"
|
||||||
groups="activeblue_familylaw.group_familylaw_attorney"/>
|
groups="activeblue_familylaw.group_familylaw_attorney"/>
|
||||||
<!-- Forward transitions (visible only from the valid stage) -->
|
<!-- Forward transitions -->
|
||||||
<button name="action_engage" type="object" string="Engage"
|
<button name="action_engage" type="object" string="Engage"
|
||||||
class="btn-primary" invisible="state != 'intake'"/>
|
class="btn-primary" invisible="state != 'intake'"/>
|
||||||
<button name="action_start_disclosure" type="object"
|
<button name="action_start_disclosure" type="object"
|
||||||
@@ -55,16 +61,34 @@
|
|||||||
statusbar_visible="intake,engaged,disclosure,discovery,mediation,hearing,closed"/>
|
statusbar_visible="intake,engaged,disclosure,discovery,mediation,hearing,closed"/>
|
||||||
</header>
|
</header>
|
||||||
<sheet>
|
<sheet>
|
||||||
|
<!-- Emergency banner -->
|
||||||
|
<div class="alert alert-danger" role="alert"
|
||||||
|
invisible="not is_emergency">
|
||||||
|
⚠️ EMERGENCY MATTER — Opened on minimum facts. Complete this record immediately.
|
||||||
|
<field name="urgency_notes" readonly="1" nolabel="1"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Conflict warning banner -->
|
||||||
|
<div class="alert alert-warning" role="alert"
|
||||||
|
invisible="conflict_hit_count == 0 or conflict_check_cleared">
|
||||||
|
⚠️ <field name="conflict_hit_count" readonly="1" nolabel="1"/> potential conflict(s) detected.
|
||||||
|
See the Conflict Hits tab. Attorney must review and clear before engaging.
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="oe_title">
|
<div class="oe_title">
|
||||||
<label for="name"/>
|
<label for="name"/>
|
||||||
<h1>
|
<h1>
|
||||||
<field name="name" placeholder="e.g. In re the Marriage of Client A"/>
|
<field name="name" placeholder="e.g. In re the Marriage of Client A"/>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<group>
|
<group>
|
||||||
<group string="Matter">
|
<group string="Matter">
|
||||||
<field name="client_id"/>
|
<field name="client_id"/>
|
||||||
<field name="case_type"/>
|
<field name="case_type"/>
|
||||||
|
<field name="case_number"
|
||||||
|
placeholder="e.g. 2024-DR-001234 (court-assigned)"/>
|
||||||
|
<field name="county"/>
|
||||||
<field name="representation"/>
|
<field name="representation"/>
|
||||||
<field name="date_opened"/>
|
<field name="date_opened"/>
|
||||||
</group>
|
</group>
|
||||||
@@ -72,11 +96,46 @@
|
|||||||
<field name="attorney_id"/>
|
<field name="attorney_id"/>
|
||||||
<field name="paralegal_id"/>
|
<field name="paralegal_id"/>
|
||||||
<field name="conflict_check_cleared" readonly="1"/>
|
<field name="conflict_check_cleared" readonly="1"/>
|
||||||
|
<field name="is_emergency"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
|
<notebook>
|
||||||
|
<page string="Parties" name="parties">
|
||||||
|
<field name="party_ids"
|
||||||
|
view_id="view_familylaw_party_inline_list"/>
|
||||||
|
</page>
|
||||||
|
<page string="Children" name="children">
|
||||||
|
<field name="child_ids"
|
||||||
|
view_id="view_familylaw_child_inline_list"/>
|
||||||
|
</page>
|
||||||
|
<page string="Issues" name="issues">
|
||||||
|
<field name="issue_ids"
|
||||||
|
view_id="view_familylaw_issue_inline_list"/>
|
||||||
|
</page>
|
||||||
|
<page string="Proceedings" name="proceedings">
|
||||||
|
<field name="proceeding_ids"
|
||||||
|
view_id="view_familylaw_proceeding_inline_list"/>
|
||||||
|
</page>
|
||||||
|
<page string="Conflict Hits" name="conflict_hits"
|
||||||
|
invisible="conflict_hit_count == 0">
|
||||||
|
<field name="conflict_hit_ids">
|
||||||
|
<list>
|
||||||
|
<field name="party_name"/>
|
||||||
|
<field name="hit_case_id"/>
|
||||||
|
<field name="hit_role"/>
|
||||||
|
<field name="reason"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page string="Emergency Notes" name="emergency"
|
||||||
|
invisible="not is_emergency">
|
||||||
|
<group>
|
||||||
|
<field name="urgency_notes"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
</sheet>
|
</sheet>
|
||||||
<!-- Audit trail. Odoo 18 also offers the shorthand <chatter/> tag;
|
|
||||||
this explicit form is used for maximum cross-release safety. -->
|
|
||||||
<div class="oe_chatter">
|
<div class="oe_chatter">
|
||||||
<field name="message_follower_ids"/>
|
<field name="message_follower_ids"/>
|
||||||
<field name="activity_ids"/>
|
<field name="activity_ids"/>
|
||||||
@@ -86,7 +145,7 @@
|
|||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- SEARCH -->
|
<!-- SEARCH — extended to support party name, child name, case number -->
|
||||||
<record id="view_familylaw_case_search" model="ir.ui.view">
|
<record id="view_familylaw_case_search" model="ir.ui.view">
|
||||||
<field name="name">familylaw.case.search</field>
|
<field name="name">familylaw.case.search</field>
|
||||||
<field name="model">familylaw.case</field>
|
<field name="model">familylaw.case</field>
|
||||||
@@ -95,11 +154,18 @@
|
|||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="client_id"/>
|
<field name="client_id"/>
|
||||||
<field name="attorney_id"/>
|
<field name="attorney_id"/>
|
||||||
|
<field name="case_number" string="Case Number"/>
|
||||||
|
<field name="party_ids" string="Party Name"
|
||||||
|
filter_domain="[('party_ids.name', 'ilike', self)]"/>
|
||||||
|
<field name="child_ids" string="Child Name"
|
||||||
|
filter_domain="[('child_ids.name', 'ilike', self)]"/>
|
||||||
<filter name="my_cases" string="My Cases"
|
<filter name="my_cases" string="My Cases"
|
||||||
domain="[('attorney_id', '=', uid)]"/>
|
domain="[('attorney_id', '=', uid)]"/>
|
||||||
<separator/>
|
<separator/>
|
||||||
<filter name="open_cases" string="Open"
|
<filter name="open_cases" string="Open"
|
||||||
domain="[('state', '!=', 'closed')]"/>
|
domain="[('state', '!=', 'closed')]"/>
|
||||||
|
<filter name="emergency_cases" string="Emergency"
|
||||||
|
domain="[('is_emergency', '=', True)]"/>
|
||||||
<filter name="needs_conflict_clearance" string="Awaiting Conflict Clearance"
|
<filter name="needs_conflict_clearance" string="Awaiting Conflict Clearance"
|
||||||
domain="[('conflict_check_cleared', '=', False), ('state', '=', 'intake')]"/>
|
domain="[('conflict_check_cleared', '=', False), ('state', '=', 'intake')]"/>
|
||||||
<group expand="0" string="Group By">
|
<group expand="0" string="Group By">
|
||||||
@@ -123,8 +189,9 @@
|
|||||||
<field name="context">{'search_default_open_cases': 1}</field>
|
<field name="context">{'search_default_open_cases': 1}</field>
|
||||||
<field name="help" type="html">
|
<field name="help" type="html">
|
||||||
<p class="o_view_nocontent_smiling_face">Open your first matter</p>
|
<p class="o_view_nocontent_smiling_face">Open your first matter</p>
|
||||||
<p>Create a case to begin. New matters start in Intake; an attorney
|
<p>Create a case or use New Intake to start from the triage questionnaire.
|
||||||
clears the conflict check, then the matter can be engaged.</p>
|
New matters start in Intake; an attorney clears the conflict check,
|
||||||
|
then the matter can be engaged.</p>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_familylaw_child_list" model="ir.ui.view">
|
||||||
|
<field name="name">familylaw.child.list</field>
|
||||||
|
<field name="model">familylaw.child</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Children">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="date_of_birth"/>
|
||||||
|
<field name="age"/>
|
||||||
|
<field name="case_id"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_familylaw_child_form" model="ir.ui.view">
|
||||||
|
<field name="name">familylaw.child.form</field>
|
||||||
|
<field name="model">familylaw.child</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Child">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group string="Identity">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="date_of_birth"/>
|
||||||
|
<field name="age" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<group string="Matter">
|
||||||
|
<field name="case_id"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Notes">
|
||||||
|
<field name="notes" nolabel="1"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Inline list used inside the case form notebook -->
|
||||||
|
<record id="view_familylaw_child_inline_list" model="ir.ui.view">
|
||||||
|
<field name="name">familylaw.child.inline.list</field>
|
||||||
|
<field name="model">familylaw.child</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Children" editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="date_of_birth"/>
|
||||||
|
<field name="age" readonly="1"/>
|
||||||
|
<field name="notes"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_familylaw_intake_wizard_form" model="ir.ui.view">
|
||||||
|
<field name="name">familylaw.intake.wizard.form</field>
|
||||||
|
<field name="model">familylaw.intake.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Family Law Intake">
|
||||||
|
<sheet>
|
||||||
|
<!-- Hidden step tracker for conditional visibility -->
|
||||||
|
<field name="step" invisible="1"/>
|
||||||
|
<field name="is_emergency" invisible="1"/>
|
||||||
|
|
||||||
|
<!-- ===== STEP 1 — TRIAGE ===== -->
|
||||||
|
<div invisible="step != 'triage'">
|
||||||
|
<h2>Step 1 — Triage</h2>
|
||||||
|
<group string="Caller Information">
|
||||||
|
<field name="caller_name"/>
|
||||||
|
<field name="caller_phone"/>
|
||||||
|
<field name="caller_email"/>
|
||||||
|
<field name="county"/>
|
||||||
|
<field name="case_type"/>
|
||||||
|
<field name="opposing_party_name"/>
|
||||||
|
<field name="has_children"/>
|
||||||
|
<field name="has_existing_order"/>
|
||||||
|
</group>
|
||||||
|
<group string="Urgency Screen — answer all three">
|
||||||
|
<field name="urgency_child_withheld"/>
|
||||||
|
<field name="urgency_removal_threat"/>
|
||||||
|
<field name="urgency_violence"/>
|
||||||
|
<field name="urgency_description"
|
||||||
|
invisible="not (urgency_child_withheld or urgency_removal_threat or urgency_violence)"
|
||||||
|
placeholder="Describe the emergency — captured for the attorney"/>
|
||||||
|
</group>
|
||||||
|
<div class="alert alert-danger" role="alert"
|
||||||
|
invisible="not (urgency_child_withheld or urgency_removal_threat or urgency_violence)">
|
||||||
|
Emergency flags detected — clicking Next will open the matter immediately
|
||||||
|
on minimum facts. Complete the record in full after the call.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== STEP 2 — CASE DETAILS (standard path) ===== -->
|
||||||
|
<div invisible="step != 'details'">
|
||||||
|
<h2>Step 2 — Case Details</h2>
|
||||||
|
<group string="Matter">
|
||||||
|
<field name="matter_name" required="step == 'details'"/>
|
||||||
|
<field name="client_partner_id"/>
|
||||||
|
<field name="county"/>
|
||||||
|
<field name="case_type" required="step == 'details'"/>
|
||||||
|
</group>
|
||||||
|
<group string="Team">
|
||||||
|
<field name="attorney_id"/>
|
||||||
|
<field name="paralegal_id"/>
|
||||||
|
</group>
|
||||||
|
<group string="Caller's Question for the Attorney">
|
||||||
|
<field name="caller_concern" nolabel="1"
|
||||||
|
placeholder="Record the caller's question verbatim. This software does not answer 'do I have a good case?' — that determination is the attorney's."/>
|
||||||
|
</group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== STEP 3 — MODIFICATION DETAILS ===== -->
|
||||||
|
<div invisible="step != 'modification'">
|
||||||
|
<h2>Step 3 — Modification Details</h2>
|
||||||
|
<group string="Prior Order">
|
||||||
|
<field name="existing_order_summary"
|
||||||
|
placeholder="Brief summary of what the existing order says — facts only. Legal interpretation is Step 6+ (attorney-reviewed)."/>
|
||||||
|
</group>
|
||||||
|
<group string="Reason for Modification">
|
||||||
|
<field name="reason_for_modification"
|
||||||
|
placeholder="What has changed since the last order?"/>
|
||||||
|
<field name="income_change_details"
|
||||||
|
placeholder="Income / financial changes (e.g. job loss, new employment). Facts only — no legal conclusions."/>
|
||||||
|
</group>
|
||||||
|
<group string="Caller's Question for the Attorney">
|
||||||
|
<field name="caller_concern" nolabel="1"
|
||||||
|
placeholder="Record the caller's question verbatim. Do not answer 'do I have a good case?' — that is the attorney's determination."/>
|
||||||
|
</group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== STEP 4 — CONFIRM ===== -->
|
||||||
|
<div invisible="step != 'confirm'">
|
||||||
|
<h2>Step 4 — Intake Complete</h2>
|
||||||
|
<group string="Matter Created">
|
||||||
|
<field name="created_case_id" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<group string="Conflict Screening Results">
|
||||||
|
<field name="conflict_summary" nolabel="1" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<div class="alert alert-warning" role="alert"
|
||||||
|
invisible="not conflict_summary or 'No potential' in (conflict_summary or '')">
|
||||||
|
Potential conflicts detected. The attorney must review the Conflict Hits
|
||||||
|
tab on the case and clear the conflict check before engaging.
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
The matter has been opened in <b>Intake</b> state.
|
||||||
|
An attorney must clear the conflict check before the matter can be engaged.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</sheet>
|
||||||
|
<footer>
|
||||||
|
<button string="← Back" type="object" name="action_back"
|
||||||
|
invisible="step in ('triage', 'confirm')"/>
|
||||||
|
<button string="Next →" type="object" name="action_next"
|
||||||
|
class="btn-primary"
|
||||||
|
invisible="step == 'confirm'"/>
|
||||||
|
<button string="Open Matter" type="object" name="action_open_case"
|
||||||
|
class="btn-primary"
|
||||||
|
invisible="step != 'confirm'"/>
|
||||||
|
<button string="Cancel" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_familylaw_intake_wizard" model="ir.actions.act_window">
|
||||||
|
<field name="name">New Intake</field>
|
||||||
|
<field name="res_model">familylaw.intake.wizard</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- Inline list used inside the case form notebook -->
|
||||||
|
<record id="view_familylaw_issue_inline_list" model="ir.ui.view">
|
||||||
|
<field name="name">familylaw.issue.inline.list</field>
|
||||||
|
<field name="model">familylaw.issue</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Issues" editable="bottom">
|
||||||
|
<field name="issue_type"/>
|
||||||
|
<field name="description"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@@ -6,6 +6,13 @@
|
|||||||
name="Family Law"
|
name="Family Law"
|
||||||
sequence="50"/>
|
sequence="50"/>
|
||||||
|
|
||||||
|
<!-- New Intake — opens the triage wizard -->
|
||||||
|
<menuitem id="menu_familylaw_intake"
|
||||||
|
name="New Intake"
|
||||||
|
parent="menu_familylaw_root"
|
||||||
|
action="action_familylaw_intake_wizard"
|
||||||
|
sequence="5"/>
|
||||||
|
|
||||||
<!-- Cases -->
|
<!-- Cases -->
|
||||||
<menuitem id="menu_familylaw_cases"
|
<menuitem id="menu_familylaw_cases"
|
||||||
name="Cases"
|
name="Cases"
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_familylaw_party_list" model="ir.ui.view">
|
||||||
|
<field name="name">familylaw.party.list</field>
|
||||||
|
<field name="model">familylaw.party</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Parties">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="role" widget="badge"/>
|
||||||
|
<field name="partner_id"/>
|
||||||
|
<field name="case_id"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_familylaw_party_form" model="ir.ui.view">
|
||||||
|
<field name="name">familylaw.party.form</field>
|
||||||
|
<field name="model">familylaw.party</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Party">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group string="Identity">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="role"/>
|
||||||
|
<field name="partner_id"/>
|
||||||
|
</group>
|
||||||
|
<group string="Matter">
|
||||||
|
<field name="case_id"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Notes">
|
||||||
|
<field name="notes" nolabel="1"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Inline list used inside the case form notebook -->
|
||||||
|
<record id="view_familylaw_party_inline_list" model="ir.ui.view">
|
||||||
|
<field name="name">familylaw.party.inline.list</field>
|
||||||
|
<field name="model">familylaw.party</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Parties" editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="role" widget="badge"/>
|
||||||
|
<field name="partner_id"/>
|
||||||
|
<field name="notes"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_familylaw_proceeding_list" model="ir.ui.view">
|
||||||
|
<field name="name">familylaw.proceeding.list</field>
|
||||||
|
<field name="model">familylaw.proceeding</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Proceedings">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="case_id"/>
|
||||||
|
<field name="proceeding_type"/>
|
||||||
|
<field name="state" widget="badge"
|
||||||
|
decoration-success="state == 'closed'"
|
||||||
|
decoration-info="state == 'open'"/>
|
||||||
|
<field name="date_opened"/>
|
||||||
|
<field name="date_closed"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_familylaw_proceeding_form" model="ir.ui.view">
|
||||||
|
<field name="name">familylaw.proceeding.form</field>
|
||||||
|
<field name="model">familylaw.proceeding</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Proceeding">
|
||||||
|
<header>
|
||||||
|
<button name="action_close_proceeding" type="object"
|
||||||
|
string="Close Proceeding"
|
||||||
|
invisible="state == 'closed'"/>
|
||||||
|
<button name="action_reopen_proceeding" type="object"
|
||||||
|
string="Reopen"
|
||||||
|
invisible="state == 'open'"/>
|
||||||
|
<field name="state" widget="statusbar"
|
||||||
|
statusbar_visible="open,closed"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group string="Proceeding">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="proceeding_type"/>
|
||||||
|
<field name="proceeding_number"/>
|
||||||
|
</group>
|
||||||
|
<group string="Matter">
|
||||||
|
<field name="case_id"/>
|
||||||
|
<field name="date_opened"/>
|
||||||
|
<field name="date_closed" 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>
|
||||||
|
|
||||||
|
<!-- Inline list used inside the case form notebook -->
|
||||||
|
<record id="view_familylaw_proceeding_inline_list" model="ir.ui.view">
|
||||||
|
<field name="name">familylaw.proceeding.inline.list</field>
|
||||||
|
<field name="model">familylaw.proceeding</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Proceedings" editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="proceeding_type"/>
|
||||||
|
<field name="proceeding_number"/>
|
||||||
|
<field name="state" widget="badge"
|
||||||
|
decoration-success="state == 'closed'"
|
||||||
|
decoration-info="state == 'open'"/>
|
||||||
|
<field name="date_opened"/>
|
||||||
|
<field name="date_closed" readonly="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user