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 -*-
|
||||
{
|
||||
"name": "Active Blue Family Law",
|
||||
"version": "18.0.1.0.0",
|
||||
"version": "18.0.2.0.0",
|
||||
"category": "Services/Legal",
|
||||
"summary": "Florida family law case management (Miami-Dade / 11th Judicial Circuit)",
|
||||
"description": """
|
||||
Active Blue Family Law
|
||||
======================
|
||||
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
|
||||
state machine, security groups (attorney / staff), and the case views.
|
||||
steps. Step 1 delivers the case spine; Step 2 adds parties, children, issues,
|
||||
the proceeding layer, automated conflict screening, and the intake questionnaire.
|
||||
|
||||
Each subsequent step adds one vertical, independently testable slice. See
|
||||
BUILD_PLAN.md for the full sequence and the test method.
|
||||
@@ -27,6 +27,11 @@ signing, citation verification, wire map, training, forms & playbook).
|
||||
"data": [
|
||||
"security/familylaw_security.xml",
|
||||
"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_menus.xml",
|
||||
],
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
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 -*-
|
||||
"""STEP 1 — The case spine.
|
||||
|
||||
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.
|
||||
"""STEP 1 — Case spine (updated in Step 2: case_number, county, emergency flag,
|
||||
related records, initial-proceeding creation, conflict screening).
|
||||
"""
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
@@ -17,7 +8,6 @@ from odoo.exceptions import UserError
|
||||
|
||||
ATTORNEY_GROUP = "activeblue_familylaw.group_familylaw_attorney"
|
||||
|
||||
# The case lifecycle. Order matters: it defines the legal "forward" path.
|
||||
STATE_SEQUENCE = [
|
||||
"intake",
|
||||
"engaged",
|
||||
@@ -28,6 +18,12 @@ STATE_SEQUENCE = [
|
||||
"closed",
|
||||
]
|
||||
|
||||
_MODIFICATION_TYPES = {
|
||||
"support_modification",
|
||||
"parenting_modification",
|
||||
"alimony_modification",
|
||||
}
|
||||
|
||||
|
||||
class FamilyLawCase(models.Model):
|
||||
_name = "familylaw.case"
|
||||
@@ -48,6 +44,24 @@ class FamilyLawCase(models.Model):
|
||||
required=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(
|
||||
selection=[
|
||||
("dissolution_no_children", "Dissolution — no children"),
|
||||
@@ -78,7 +92,7 @@ class FamilyLawCase(models.Model):
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
# --- Representation (drives the subpoena workflow in a later step) ------
|
||||
# --- Representation (drives the Step 8 subpoena branch) -----------------
|
||||
representation = fields.Selection(
|
||||
selection=[
|
||||
("attorney", "Attorney of Record"),
|
||||
@@ -89,7 +103,20 @@ class FamilyLawCase(models.Model):
|
||||
required=True,
|
||||
tracking=True,
|
||||
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 -----------------------------------------------------
|
||||
@@ -99,7 +126,7 @@ class FamilyLawCase(models.Model):
|
||||
copy=False,
|
||||
tracking=True,
|
||||
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(
|
||||
selection=[
|
||||
@@ -117,16 +144,158 @@ class FamilyLawCase(models.Model):
|
||||
copy=False,
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
date_opened = fields.Date(
|
||||
string="Date Opened",
|
||||
default=fields.Date.context_today,
|
||||
)
|
||||
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 ---------------------------------------------------
|
||||
def _ensure_attorney(self):
|
||||
"""Raise unless the acting user is in the attorney group."""
|
||||
if not self.env.user.has_group(ATTORNEY_GROUP):
|
||||
raise UserError(
|
||||
_("Only a licensed attorney (Family Law / Attorney group) may "
|
||||
@@ -134,7 +303,6 @@ class FamilyLawCase(models.Model):
|
||||
)
|
||||
|
||||
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)
|
||||
if bad:
|
||||
raise UserError(
|
||||
@@ -145,28 +313,26 @@ class FamilyLawCase(models.Model):
|
||||
)
|
||||
|
||||
def _advance_to(self, target):
|
||||
"""Write the new state and log it on the chatter."""
|
||||
for case in self:
|
||||
case.state = target
|
||||
case.message_post(
|
||||
body=_("Stage changed to: %s") % dict(
|
||||
self._fields["state"].selection).get(target, target)
|
||||
body=_("Stage changed to: %s")
|
||||
% dict(self._fields["state"].selection).get(target, target)
|
||||
)
|
||||
|
||||
# --- Gated transitions --------------------------------------------------
|
||||
def action_mark_conflict_cleared(self):
|
||||
"""Attorney-only: record that the conflict check is clear."""
|
||||
self._ensure_attorney()
|
||||
for case in self:
|
||||
if case.conflict_check_cleared:
|
||||
continue
|
||||
case.conflict_check_cleared = True
|
||||
case.message_post(body=_("Conflict check cleared by %s.")
|
||||
% self.env.user.name)
|
||||
case.message_post(
|
||||
body=_("Conflict check cleared by %s.") % self.env.user.name
|
||||
)
|
||||
return True
|
||||
|
||||
def action_engage(self):
|
||||
"""intake -> engaged. Requires a cleared conflict check."""
|
||||
self._require_state(["intake"])
|
||||
not_cleared = self.filtered(lambda c: not c.conflict_check_cleared)
|
||||
if not_cleared:
|
||||
@@ -178,38 +344,32 @@ class FamilyLawCase(models.Model):
|
||||
return True
|
||||
|
||||
def action_start_disclosure(self):
|
||||
"""engaged -> disclosure (Rule 12.285 phase begins in a later step)."""
|
||||
self._require_state(["engaged"])
|
||||
self._advance_to("disclosure")
|
||||
return True
|
||||
|
||||
def action_start_discovery(self):
|
||||
"""disclosure -> discovery."""
|
||||
self._require_state(["disclosure"])
|
||||
self._advance_to("discovery")
|
||||
return True
|
||||
|
||||
def action_start_mediation(self):
|
||||
"""discovery -> mediation (mandatory in Miami-Dade before final hearing)."""
|
||||
self._require_state(["discovery"])
|
||||
self._advance_to("mediation")
|
||||
return True
|
||||
|
||||
def action_set_hearing(self):
|
||||
"""mediation -> hearing / trial."""
|
||||
self._require_state(["mediation"])
|
||||
self._advance_to("hearing")
|
||||
return True
|
||||
|
||||
def action_close(self):
|
||||
"""Attorney-only: close the matter from any non-closed stage."""
|
||||
self._ensure_attorney()
|
||||
self._require_state([s for s in STATE_SEQUENCE if s != "closed"])
|
||||
self._advance_to("closed")
|
||||
return True
|
||||
|
||||
def action_reopen(self):
|
||||
"""Attorney-only: reopen a closed matter back to engaged."""
|
||||
self._ensure_attorney()
|
||||
self._require_state(["closed"])
|
||||
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
|
||||
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_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_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"?>
|
||||
<odoo>
|
||||
|
||||
<!-- LIST (Odoo 18 uses <list>, not <tree>) -->
|
||||
<!-- LIST view -->
|
||||
<record id="view_familylaw_case_list" model="ir.ui.view">
|
||||
<field name="name">familylaw.case.list</field>
|
||||
<field name="model">familylaw.case</field>
|
||||
@@ -9,10 +9,12 @@
|
||||
<list string="Cases">
|
||||
<field name="name"/>
|
||||
<field name="client_id"/>
|
||||
<field name="case_number"/>
|
||||
<field name="case_type"/>
|
||||
<field name="attorney_id"/>
|
||||
<field name="representation"/>
|
||||
<field name="conflict_check_cleared"/>
|
||||
<field name="is_emergency" widget="boolean_toggle" optional="show"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-success="state == 'closed'"
|
||||
decoration-info="state in ('intake','engaged')"
|
||||
@@ -28,12 +30,16 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Case">
|
||||
<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 -->
|
||||
<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"
|
||||
groups="activeblue_familylaw.group_familylaw_attorney"/>
|
||||
<!-- Forward transitions (visible only from the valid stage) -->
|
||||
<!-- Forward transitions -->
|
||||
<button name="action_engage" type="object" string="Engage"
|
||||
class="btn-primary" invisible="state != 'intake'"/>
|
||||
<button name="action_start_disclosure" type="object"
|
||||
@@ -55,16 +61,34 @@
|
||||
statusbar_visible="intake,engaged,disclosure,discovery,mediation,hearing,closed"/>
|
||||
</header>
|
||||
<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">
|
||||
<label for="name"/>
|
||||
<h1>
|
||||
<field name="name" placeholder="e.g. In re the Marriage of Client A"/>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group string="Matter">
|
||||
<field name="client_id"/>
|
||||
<field name="case_type"/>
|
||||
<field name="case_number"
|
||||
placeholder="e.g. 2024-DR-001234 (court-assigned)"/>
|
||||
<field name="county"/>
|
||||
<field name="representation"/>
|
||||
<field name="date_opened"/>
|
||||
</group>
|
||||
@@ -72,11 +96,46 @@
|
||||
<field name="attorney_id"/>
|
||||
<field name="paralegal_id"/>
|
||||
<field name="conflict_check_cleared" readonly="1"/>
|
||||
<field name="is_emergency"/>
|
||||
</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>
|
||||
<!-- Audit trail. Odoo 18 also offers the shorthand <chatter/> tag;
|
||||
this explicit form is used for maximum cross-release safety. -->
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="activity_ids"/>
|
||||
@@ -86,7 +145,7 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- SEARCH -->
|
||||
<!-- SEARCH — extended to support party name, child name, case number -->
|
||||
<record id="view_familylaw_case_search" model="ir.ui.view">
|
||||
<field name="name">familylaw.case.search</field>
|
||||
<field name="model">familylaw.case</field>
|
||||
@@ -95,11 +154,18 @@
|
||||
<field name="name"/>
|
||||
<field name="client_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"
|
||||
domain="[('attorney_id', '=', uid)]"/>
|
||||
<separator/>
|
||||
<filter name="open_cases" string="Open"
|
||||
domain="[('state', '!=', 'closed')]"/>
|
||||
<filter name="emergency_cases" string="Emergency"
|
||||
domain="[('is_emergency', '=', True)]"/>
|
||||
<filter name="needs_conflict_clearance" string="Awaiting Conflict Clearance"
|
||||
domain="[('conflict_check_cleared', '=', False), ('state', '=', 'intake')]"/>
|
||||
<group expand="0" string="Group By">
|
||||
@@ -123,8 +189,9 @@
|
||||
<field name="context">{'search_default_open_cases': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<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
|
||||
clears the conflict check, then the matter can be engaged.</p>
|
||||
<p>Create a case or use New Intake to start from the triage questionnaire.
|
||||
New matters start in Intake; an attorney clears the conflict check,
|
||||
then the matter can be engaged.</p>
|
||||
</field>
|
||||
</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"
|
||||
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 -->
|
||||
<menuitem id="menu_familylaw_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