Files
famlaw/activeblue_familylaw/models/fl_discovery.py
Carlos Garcia fa0905ddbb Phase 3: Full Discovery + Deposition workflow
fl_deposition.py — Full implementation:
- Calendar event sync for all scheduled depositions
- Notice validation: FL 1.310(b) 10-day minimum; days_notice + notice_valid computed
- Duces tecum document list field with production instructions
- Workflow buttons: Mark Noticed, Confirm, Completed, No-Show, Reschedule, Cancel
- action_no_show: auto-creates Motion to Compel deadline (FL 1.380, 20 days),
  project task with step-by-step instructions, urgent chatter alert
- Court reporter field, transcript tracking, key findings summary

fl_discovery.py — Full implementation:
- action_mark_served: creates fl.deadline for 30-day response window
- action_flag_deficient: creates deficiency notice deadline (good-faith prerequisite)
- action_file_motion_to_compel: FL 1.380 deadline + project task with instructions
- admissions_deemed computed: FL 1.370 auto-deemed-admitted after 30 days (critical)
- _cron_discovery_overdue_alerts: daily check — overdue responses + deemed admissions
  with urgent chatter alerts distinguishing regular overdue vs. deemed-admitted
- sanctions_requested field for FL 1.380(a)(4) expense awards

Enhanced views:
- fl_deposition_views: calendar view, notice validation banners, no-show alert,
  duces tecum section, workflow status bar, results section
- fl_discovery_views: FL 1.370 deemed-admitted critical red banner,
  overdue response warning, subpoena info section, Motion to Compel section,
  tree with inline action buttons, full search with filter presets

ir.cron: added daily discovery overdue alert job to fl_deadline_rules.xml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 23:20:40 -04:00

444 lines
20 KiB
Python

from odoo import api, fields, models
from dateutil.relativedelta import relativedelta
class FlDiscovery(models.Model):
"""
Phase 3 — Full implementation.
Covers all Florida discovery methods with response tracking,
deficiency workflow, and Motion to Compel automation.
Discovery types covered (FL Rules of Civil Procedure):
FL 1.340 — Interrogatories (30 days to respond)
FL 1.350 — Request for Production (30 days to respond)
FL 1.370 — Request for Admissions (30 days to respond; deemed admitted if no response)
FL 1.351 — Subpoena (Third-party document production)
FL 1.310 — Depositions (tracked separately in fl.deposition)
FL 1.280 — General discovery scope
Motion to Compel: FL 1.380
Subpoena to Employer: FL 1.351 (key tool when respondent income unknown)
"""
_name = 'fl.discovery'
_description = 'Discovery Item'
_inherit = ['mail.thread']
_order = 'served_date asc'
case_id = fields.Many2one(
'fl.case', required=True, ondelete='cascade', index=True,
string='Case'
)
discovery_type = fields.Selection([
('interrogatories', 'Interrogatories (FL 1.340)'),
('production', 'Request for Production of Documents (FL 1.350)'),
('admissions', 'Request for Admissions (FL 1.370)'),
('subpoena', 'Subpoena — Third Party (FL 1.351)'),
('deposition', 'Deposition Notice (FL 1.310)'),
], string='Discovery Type', required=True, tracking=True)
directed_to = fields.Selection([
('petitioner', 'Petitioner'),
('respondent', 'Respondent'),
('third_party', 'Third Party'),
], string='Directed To', required=True)
third_party_id = fields.Many2one(
'res.partner', string='Third Party',
help='Employer, bank, or other third party for subpoena'
)
description = fields.Char(
string='Description / Subject',
help='e.g. "Interrogatories regarding income and employment" or '
'"Request for Production — bank statements 2022-2024"'
)
# ── Dates and Deadlines ───────────────────────────────────────────────────
served_date = fields.Date(string='Date Served', tracking=True)
response_due_date = fields.Date(
string='Response Due Date',
compute='_compute_response_due',
store=True,
help='30 days from service date (FL 1.340/1.350/1.370)'
)
response_received_date = fields.Date(
string='Response Date Received', tracking=True
)
# ── Tracking ──────────────────────────────────────────────────────────────
response_complete = fields.Boolean(
string='Response Complete / Adequate',
help='Mark True when you have received a complete, non-deficient response'
)
objections_raised = fields.Boolean(string='Objections Raised by Respondent')
objection_detail = fields.Text(
string='Objection Details',
help='Document specific objections raised: privilege, overbroad, '
'not reasonably calculated to lead to admissible evidence, etc.'
)
deficiency_notice_sent = fields.Boolean(
string='Deficiency Notice Sent',
help='A written notice identifying deficiencies was sent before Motion to Compel'
)
deficiency_notice_date = fields.Date(string='Deficiency Notice Date')
# ── Status ────────────────────────────────────────────────────────────────
state = fields.Selection([
('draft', 'Drafting'),
('served', 'Served — Awaiting Response'),
('responded', 'Response Received'),
('deficient', 'Response Deficient'),
('compelled', 'Motion to Compel Filed'),
('complete', 'Complete'),
], string='Status', default='draft', tracking=True)
# ── Computed Status Fields ────────────────────────────────────────────────
is_overdue = fields.Boolean(
string='Response Overdue',
compute='_compute_overdue_status',
store=True
)
days_until_response = fields.Integer(
string='Days Until Response Due',
compute='_compute_overdue_status'
)
admissions_deemed = fields.Boolean(
string='Admissions Deemed Admitted',
compute='_compute_admissions_deemed',
store=True,
help='FL 1.370: If no response to Request for Admissions within 30 days, '
'each matter is AUTOMATICALLY deemed admitted'
)
# ── Motion to Compel ──────────────────────────────────────────────────────
motion_to_compel_filed = fields.Boolean(
string='Motion to Compel Filed (FL 1.380)'
)
motion_to_compel_date = fields.Date(string='Motion to Compel Date')
sanctions_requested = fields.Boolean(
string='Attorney Fees / Sanctions Requested',
help='FL 1.380(a)(4): If motion is granted, court shall award expenses. '
'If motion is denied, court may award expenses to the opposing party.'
)
# ── Subpoena-Specific ─────────────────────────────────────────────────────
subpoena_employer_flag = fields.Boolean(
string='Employer Subpoena',
compute='_compute_subpoena_employer_flag',
help='True when this is a subpoena directed to an employer for income records'
)
notes = fields.Text(string='Notes')
# ══════════════════════════════════════════════════════════════════════════
# COMPUTED FIELDS
# ══════════════════════════════════════════════════════════════════════════
@api.depends('served_date', 'discovery_type')
def _compute_response_due(self):
for rec in self:
if rec.served_date and rec.discovery_type in (
'interrogatories', 'production', 'admissions'
):
rec.response_due_date = rec.served_date + relativedelta(days=30)
else:
rec.response_due_date = False
@api.depends('response_due_date', 'state', 'response_received_date')
def _compute_overdue_status(self):
today = fields.Date.today()
for rec in self:
terminal_states = ('responded', 'complete')
if rec.state in terminal_states or not rec.response_due_date:
rec.is_overdue = False
rec.days_until_response = 0
else:
rec.days_until_response = (rec.response_due_date - today).days
rec.is_overdue = rec.response_due_date < today
@api.depends('discovery_type', 'response_due_date', 'response_received_date', 'state')
def _compute_admissions_deemed(self):
"""
FL 1.370: Each matter in a Request for Admissions is AUTOMATICALLY
deemed admitted if no response within 30 days.
"""
today = fields.Date.today()
for rec in self:
if (
rec.discovery_type == 'admissions'
and rec.response_due_date
and rec.response_due_date < today
and not rec.response_received_date
and rec.state not in ('responded', 'complete')
):
rec.admissions_deemed = True
else:
rec.admissions_deemed = False
@api.depends('discovery_type', 'directed_to')
def _compute_subpoena_employer_flag(self):
for rec in self:
rec.subpoena_employer_flag = (
rec.discovery_type == 'subpoena'
and rec.third_party_id
)
# ══════════════════════════════════════════════════════════════════════════
# WORKFLOW ACTIONS
# ══════════════════════════════════════════════════════════════════════════
def action_mark_served(self):
"""
Mark discovery as served.
Sets served_date to today and creates a deadline for the response.
"""
if not self.served_date:
self.served_date = fields.Date.today()
self.write({'state': 'served'})
# Create response deadline in fl.deadline
if self.response_due_date:
type_label = dict(self._fields['discovery_type'].selection).get(
self.discovery_type, 'Discovery'
)
to_label = dict(self._fields['directed_to'].selection).get(
self.directed_to, ''
)
deadline_name = 'Response Due: {} to {}{}'.format(
type_label, to_label, self.description or ''
)
self.env['fl.deadline'].create({
'case_id': self.case_id.id,
'name': deadline_name[:200],
'deadline_type': 'compliance',
'due_date': self.response_due_date,
'anchor_date': self.served_date,
'offset_days': 30,
'statute_reference': self._get_statute_ref(),
'notes': (
'Discovery response deadline. If no response received, '
'send deficiency notice, then file Motion to Compel (FL 1.380).'
),
})
self.case_id.message_post(
body=(
'📬 <b>Discovery Served</b><br/>'
'Type: {}<br/>'
'Directed to: {}<br/>'
'Description: {}<br/>'
'Response due: {}'
).format(
dict(self._fields['discovery_type'].selection).get(self.discovery_type, ''),
dict(self._fields['directed_to'].selection).get(self.directed_to, ''),
self.description or 'N/A',
self.response_due_date or 'N/A',
),
subtype_xmlid='mail.mt_note',
)
def action_mark_responded(self):
"""Response received — set date if not already set."""
if not self.response_received_date:
self.response_received_date = fields.Date.today()
self.write({'state': 'responded'})
def action_flag_deficient(self):
"""
Response received but is deficient (incomplete, evasive, objectionable).
Creates a deadline to send deficiency notice.
"""
self.write({'state': 'deficient'})
# Create deadline: send deficiency notice within 10 days
notice_due = fields.Date.today() + relativedelta(days=10)
self.env['fl.deadline'].create({
'case_id': self.case_id.id,
'name': 'Send Deficiency Notice for {} (FL 1.380 prerequisite)'.format(
self.description or dict(self._fields['discovery_type'].selection).get(
self.discovery_type, 'Discovery'
)
),
'deadline_type': 'compliance',
'due_date': notice_due,
'statute_reference': (
'FL 1.380: Must demonstrate good-faith effort to resolve '
'before filing Motion to Compel. Send written deficiency notice.'
),
})
self.case_id.message_post(
body=(
'⚠️ <b>Discovery Response Deficient</b><br/>'
'{} response from {} is incomplete or evasive.<br/><br/>'
'<b>Next Steps:</b><br/>'
'1. Send written deficiency notice ({}) — required before Motion to Compel<br/>'
'2. Allow 10-14 days for supplemental response<br/>'
'3. If no adequate response: file Motion to Compel (FL 1.380)'
).format(
dict(self._fields['discovery_type'].selection).get(self.discovery_type, ''),
dict(self._fields['directed_to'].selection).get(self.directed_to, ''),
notice_due,
),
subtype_xmlid='mail.mt_note',
)
def action_file_motion_to_compel(self):
"""
File Motion to Compel (FL 1.380).
Creates deadline and project task. Updates state to 'compelled'.
"""
self.write({
'state': 'compelled',
'motion_to_compel_filed': True,
'motion_to_compel_date': fields.Date.today(),
})
# Deadline: file motion within 7 days
mtc_due = fields.Date.today() + relativedelta(days=7)
self.env['fl.deadline'].create({
'case_id': self.case_id.id,
'name': 'File Motion to Compel Discovery — {} (FL 1.380)'.format(
self.description or dict(self._fields['discovery_type'].selection).get(
self.discovery_type, 'Discovery'
)
),
'deadline_type': 'compliance',
'due_date': mtc_due,
'statute_reference': (
'FL 1.380 — Motion to Compel Discovery. '
'Include: certificate of good faith, deficiency notice copy, '
'request for sanctions under FL 1.380(a)(4).'
),
})
if self.case_id.project_id:
self.env['project.task'].create({
'name': 'FILE: Motion to Compel — {} (FL 1.380)'.format(
self.description or dict(self._fields['discovery_type'].selection).get(
self.discovery_type, 'Discovery'
)
),
'project_id': self.case_id.project_id.id,
'description': (
'File Motion to Compel for {} directed to {}.\n\n'
'Required elements:\n'
'1. Certificate of Good Faith (prior notice of deficiency)\n'
'2. Copy of original discovery request\n'
'3. Copy of deficiency notice sent\n'
'4. Request for sanctions (attorney fees) under FL 1.380(a)(4)\n\n'
'File with Miami-Dade Clerk and set hearing date.\n'
'Deadline: {}'
).format(
dict(self._fields['discovery_type'].selection).get(
self.discovery_type, 'discovery'
),
dict(self._fields['directed_to'].selection).get(
self.directed_to, ''
),
mtc_due,
),
'date_deadline': mtc_due,
})
self.case_id.message_post(
body=(
'⚖️ <b>Motion to Compel Filed (FL 1.380)</b><br/>'
'Discovery: {}{}<br/>'
'Directed to: {}<br/>'
'Sanctions requested: {}'
).format(
dict(self._fields['discovery_type'].selection).get(self.discovery_type, ''),
self.description or 'N/A',
dict(self._fields['directed_to'].selection).get(self.directed_to, ''),
'✅ Yes' if self.sanctions_requested else 'No',
),
subtype_xmlid='mail.mt_note',
)
def action_mark_complete(self):
"""Mark discovery item fully resolved."""
self.write({'state': 'complete', 'response_complete': True})
# ══════════════════════════════════════════════════════════════════════════
# HELPERS
# ══════════════════════════════════════════════════════════════════════════
def _get_statute_ref(self):
"""Return the applicable statute reference for this discovery type."""
refs = {
'interrogatories': 'FL 1.340 — Interrogatories (30 days to respond)',
'production': 'FL 1.350 — Request for Production (30 days to respond)',
'admissions': (
'FL 1.370 — Request for Admissions (30 days to respond). '
'WARNING: Unanswered admissions are DEEMED ADMITTED automatically.'
),
'subpoena': 'FL 1.351 — Subpoena for Documents from Third Party',
'deposition': 'FL 1.310 — Notice of Taking Deposition',
}
return refs.get(self.discovery_type, 'FL Rules of Civil Procedure')
# ══════════════════════════════════════════════════════════════════════════
# CRON: Daily Discovery Overdue Alerts
# ══════════════════════════════════════════════════════════════════════════
def _cron_discovery_overdue_alerts(self):
"""
Run daily. Check for:
1. Overdue responses → post chatter alert
2. Deemed admissions (FL 1.370) → urgent chatter alert
"""
today = fields.Date.today()
# Overdue responses
overdue = self.search([
('state', 'in', ['served']),
('response_due_date', '<', today),
('is_overdue', '=', True),
])
for item in overdue:
days_overdue = (today - item.response_due_date).days
item.case_id.message_post(
body=(
'🔴 <b>Discovery Response OVERDUE ({} days)</b><br/>'
'Type: {}<br/>'
'Directed to: {}<br/>'
'Description: {}<br/>'
'Was due: {}<br/><br/>'
'<b>Next Step:</b> Send deficiency notice, then '
'consider Motion to Compel (FL 1.380).'
).format(
days_overdue,
dict(item._fields['discovery_type'].selection).get(item.discovery_type, ''),
dict(item._fields['directed_to'].selection).get(item.directed_to, ''),
item.description or 'N/A',
item.response_due_date,
),
subtype_xmlid='mail.mt_note',
)
# Auto-update to served state stays but mark overdue flag is computed
# Deemed admissions (FL 1.370) — extra urgent alert
deemed = self.search([
('discovery_type', '=', 'admissions'),
('admissions_deemed', '=', True),
('state', 'not in', ['responded', 'complete', 'compelled']),
])
for item in deemed:
item.case_id.message_post(
body=(
'<b>🚨 DEEMED ADMITTED — FL 1.370 CRITICAL ALERT</b><br/>'
'A Request for Admissions directed to <b>{}</b> received NO response '
'within 30 days.<br/>'
'<b>Under FL 1.370, each matter is now AUTOMATICALLY DEEMED ADMITTED</b> '
'and may be used against that party at hearing.<br/><br/>'
'If you are the <b>responding party</b>: immediately file a Motion to '
'Withdraw or Amend the Admissions (court has discretion to allow this '
'if no prejudice to the requesting party).<br/><br/>'
'If you are the <b>requesting party</b>: these deemed admissions are '
'now established as facts for trial purposes.'
).format(
dict(item._fields['directed_to'].selection).get(item.directed_to, '')
),
subtype_xmlid='mail.mt_note',
)