Add conflict-of-interest check engine; gate stage advancement at Intake

- New fl.conflict.check model: screens petitioner/respondent/party_ids names
  against parties on other open cases (exact partner match + difflib fuzzy
  match at 0.85 threshold); skips folded/closed stages
- Runs automatically as the first action in fl.case.create; logs conflicts to
  chatter with matched-case detail and never silently passes
- fl.case gains conflict_check_passed/conflict_check_id/conflict_check_ids;
  write() blocks advancing stage_id past Intake until the check passes
- Admin-only action_override requires a written justification, stamps user/date,
  and flips conflict_check_passed True with a chatter audit entry
- Add conflict check form/tree/search views, action, Cases sub-menu item,
  case form banner + Run Conflict Check button, and Kanban conflict badge
- ACL entries for fl.conflict.check (admin full, paralegal no-delete)
- Finish Claude migration cleanup in fl_analysis.py (model_used default,
  docstring/help text)
- Add .gitignore for Python artifacts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 18:19:32 +00:00
parent b8ab8494c7
commit 7bc0cc8554
10 changed files with 431 additions and 10 deletions

View File

@@ -1,4 +1,5 @@
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from dateutil.relativedelta import relativedelta
@@ -86,6 +87,23 @@ class FlCase(models.Model):
)
active = fields.Boolean(default=True)
# ══════════════════════════════════════════════════════════════════════
# CONFLICT OF INTEREST
# ══════════════════════════════════════════════════════════════════════
conflict_check_passed = fields.Boolean(
string='Conflict Check Passed',
default=False, tracking=True,
help='Case cannot advance past Intake until the conflict-of-interest '
'check passes (or is overridden by an administrator).'
)
conflict_check_id = fields.Many2one(
'fl.conflict.check', string='Latest Conflict Check', readonly=True
)
conflict_check_ids = fields.One2many(
'fl.conflict.check', 'case_id', string='Conflict Checks'
)
# ══════════════════════════════════════════════════════════════════════
# PARTIES
# ══════════════════════════════════════════════════════════════════════
@@ -794,6 +812,8 @@ class FlCase(models.Model):
) or _('New')
records = super().create(vals_list)
for record in records:
# 0. Conflict-of-interest screen — runs before any other action
self.env['fl.conflict.check'].run_check(record)
# 1. Create Odoo project for task management
record._create_case_project()
# 2. Generate deadlines if filing date already set
@@ -807,6 +827,20 @@ class FlCase(models.Model):
return records
def write(self, vals):
# Block advancing past Intake until conflict check has passed
if vals.get('stage_id'):
new_stage = self.env['fl.case.stage'].browse(vals['stage_id'])
intake = self.env.ref(
'activeblue_familylaw.fl_stage_intake', raise_if_not_found=False
)
if intake and new_stage.sequence > intake.sequence:
for rec in self:
if not rec.conflict_check_passed:
raise UserError(_(
"Cannot advance case %s past Intake: the "
"conflict-of-interest check has not passed. Resolve or "
"override the conflict before changing the stage."
) % rec.name)
result = super().write(vals)
# Recalculate service-anchored deadlines when service_date is set/changed
if 'service_date' in vals:
@@ -1090,3 +1124,15 @@ class FlCase(models.Model):
'target': 'new',
'context': {'default_case_id': self.id},
}
def action_run_conflict_check(self):
self.ensure_one()
check = self.env['fl.conflict.check'].run_check(self)
return {
'type': 'ir.actions.act_window',
'name': 'Conflict Check Result',
'res_model': 'fl.conflict.check',
'res_id': check.id,
'view_mode': 'form',
'target': 'new',
}