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>
444 lines
20 KiB
Python
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',
|
|
)
|