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>
This commit is contained in:
@@ -1,37 +1,86 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
|
||||
class FlDeposition(models.Model):
|
||||
"""
|
||||
Phase 3 — Full implementation with notice validation, duces tecum, no-show workflow.
|
||||
Phase 1: Stub with core fields.
|
||||
Phase 3 — Full implementation.
|
||||
FL 1.310: Notice of Taking Deposition
|
||||
- Minimum 10 days notice required (FL 1.310(b))
|
||||
- Maximum 7 hours per deponent per day (FL 1.310(d))
|
||||
- Duces tecum: document production requirement
|
||||
- No-show workflow: auto-trigger Motion to Compel (FL 1.380)
|
||||
"""
|
||||
_name = 'fl.deposition'
|
||||
_description = 'Deposition Record'
|
||||
_inherit = ['mail.thread']
|
||||
_order = 'scheduled_date asc'
|
||||
_rec_name = 'deponent_id'
|
||||
|
||||
case_id = fields.Many2one(
|
||||
'fl.case', required=True, ondelete='cascade', index=True
|
||||
'fl.case', required=True, ondelete='cascade', index=True,
|
||||
string='Case'
|
||||
)
|
||||
deponent_id = fields.Many2one(
|
||||
'res.partner', string='Deponent', required=True
|
||||
)
|
||||
deponent_type = fields.Selection([
|
||||
('opposing_party', 'Opposing Party'),
|
||||
('employer', 'Employer'),
|
||||
('employer', "Deponent's Employer"),
|
||||
('accountant_cpa', 'Accountant / CPA'),
|
||||
('business_partner', 'Business Partner'),
|
||||
('vocational_expert', 'Vocational Expert'),
|
||||
('witness', 'Witness'),
|
||||
('other', 'Other'),
|
||||
], string='Deponent Type', required=True)
|
||||
|
||||
# ── Notice ────────────────────────────────────────────────────────────────
|
||||
notice_date = fields.Date(
|
||||
string='Notice Served Date',
|
||||
help='FL 1.310(b): Minimum 10 days notice required before deposition'
|
||||
help='Date the Notice of Taking Deposition was served on the deponent. '
|
||||
'FL 1.310(b): Minimum 10 days notice required.'
|
||||
)
|
||||
scheduled_date = fields.Datetime(string='Deposition Date / Time')
|
||||
location = fields.Char(string='Location / Zoom Link')
|
||||
days_notice = fields.Integer(
|
||||
string='Days of Notice',
|
||||
compute='_compute_notice_info',
|
||||
help='Number of days between notice date and scheduled deposition'
|
||||
)
|
||||
notice_valid = fields.Boolean(
|
||||
string='Notice Valid (≥10 days)',
|
||||
compute='_compute_notice_info',
|
||||
help='FL 1.310(b): At least 10 days notice required'
|
||||
)
|
||||
notice_warning = fields.Char(
|
||||
string='Notice Warning',
|
||||
compute='_compute_notice_info'
|
||||
)
|
||||
|
||||
# ── Schedule ──────────────────────────────────────────────────────────────
|
||||
scheduled_date = fields.Datetime(
|
||||
string='Deposition Date / Time', tracking=True
|
||||
)
|
||||
location = fields.Char(
|
||||
string='Location / Zoom Link',
|
||||
help='Physical address or Zoom link. Include court reporter contact info.'
|
||||
)
|
||||
court_reporter = fields.Char(
|
||||
string='Court Reporter',
|
||||
help='Court reporter name and contact information'
|
||||
)
|
||||
max_duration_hours = fields.Float(
|
||||
string='Max Duration (hours)',
|
||||
default=7.0,
|
||||
help='FL 1.310(d): Maximum 7 hours per deponent per day. '
|
||||
'Parties may agree to extend.'
|
||||
)
|
||||
days_until_deposition = fields.Integer(
|
||||
string='Days Until Deposition',
|
||||
compute='_compute_days_until_deposition'
|
||||
)
|
||||
|
||||
# ── State ─────────────────────────────────────────────────────────────────
|
||||
state = fields.Selection([
|
||||
('draft', 'Drafting Notice'),
|
||||
('noticed', 'Notice Served'),
|
||||
@@ -41,15 +90,305 @@ class FlDeposition(models.Model):
|
||||
('cancelled', 'Cancelled'),
|
||||
('rescheduled', 'Rescheduled'),
|
||||
], string='Status', default='draft', tracking=True)
|
||||
|
||||
# ── Duces Tecum ───────────────────────────────────────────────────────────
|
||||
duces_tecum = fields.Boolean(
|
||||
string='Duces Tecum (Document Production)',
|
||||
help='Deponent is required to bring documents'
|
||||
string='Duces Tecum (Bring Documents)',
|
||||
help='Require deponent to produce documents at deposition'
|
||||
)
|
||||
max_duration_hours = fields.Float(
|
||||
default=7.0,
|
||||
help='FL 1.310(d): Maximum 7 hours per deponent per day'
|
||||
duces_tecum_items = fields.Text(
|
||||
string='Documents Required (Duces Tecum)',
|
||||
help=(
|
||||
'List documents deponent must bring. Examples:\n'
|
||||
'- Last 3 years federal tax returns\n'
|
||||
'- Last 6 months paystubs / payroll records\n'
|
||||
'- Bank statements (all accounts, last 12 months)\n'
|
||||
'- Business financial statements (if self-employed)\n'
|
||||
'- Corporate tax returns (if business owner)\n'
|
||||
'- Quickbooks / accounting records'
|
||||
)
|
||||
)
|
||||
|
||||
# ── Calendar ──────────────────────────────────────────────────────────────
|
||||
calendar_event_id = fields.Many2one(
|
||||
'calendar.event', string='Calendar Event', ondelete='set null'
|
||||
)
|
||||
|
||||
# ── Results ───────────────────────────────────────────────────────────────
|
||||
income_verified = fields.Boolean(string='Income Figures Verified')
|
||||
income_verified_amount = fields.Float(string='Verified Income Amount ($)')
|
||||
key_findings = fields.Text(string='Key Findings')
|
||||
income_verified_amount = fields.Float(
|
||||
string='Verified Monthly Income ($)',
|
||||
help='Net monthly income as established at deposition'
|
||||
)
|
||||
key_findings = fields.Text(
|
||||
string='Key Findings / Testimony Summary',
|
||||
help='Summarize key admissions, income figures, contradictions, '
|
||||
'and relevant testimony obtained'
|
||||
)
|
||||
transcript_received = fields.Boolean(string='Transcript Received')
|
||||
transcript_notes = fields.Text(string='Transcript Notes')
|
||||
|
||||
# ── No-Show / 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='Sanctions Requested')
|
||||
|
||||
notes = fields.Text(string='Notes')
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
# COMPUTED FIELDS
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@api.depends('notice_date', 'scheduled_date')
|
||||
def _compute_notice_info(self):
|
||||
for rec in self:
|
||||
if rec.notice_date and rec.scheduled_date:
|
||||
days = (rec.scheduled_date.date() - rec.notice_date).days
|
||||
rec.days_notice = days
|
||||
rec.notice_valid = days >= 10
|
||||
if days < 10:
|
||||
rec.notice_warning = (
|
||||
'⚠️ FL 1.310(b): Only {} days notice. '
|
||||
'Minimum 10 days required. Deponent may object.'.format(days)
|
||||
)
|
||||
else:
|
||||
rec.notice_warning = (
|
||||
'✅ FL 1.310(b): {} days notice — requirement met.'.format(days)
|
||||
)
|
||||
else:
|
||||
rec.days_notice = 0
|
||||
rec.notice_valid = False
|
||||
rec.notice_warning = '⚪ Set notice date and scheduled date to validate'
|
||||
|
||||
@api.depends('scheduled_date')
|
||||
def _compute_days_until_deposition(self):
|
||||
today = fields.Date.today()
|
||||
for rec in self:
|
||||
if rec.scheduled_date:
|
||||
rec.days_until_deposition = (rec.scheduled_date.date() - today).days
|
||||
else:
|
||||
rec.days_until_deposition = 0
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
# CRUD — calendar event lifecycle
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
for rec in records:
|
||||
if rec.scheduled_date:
|
||||
rec.with_context(_no_dep_sync=True)._create_or_update_calendar_event()
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
result = super().write(vals)
|
||||
if not self.env.context.get('_no_dep_sync'):
|
||||
sync_fields = {'scheduled_date', 'state', 'location', 'deponent_id'}
|
||||
if sync_fields.intersection(vals.keys()):
|
||||
for rec in self:
|
||||
rec.with_context(_no_dep_sync=True)._create_or_update_calendar_event()
|
||||
return result
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
# CALENDAR INTEGRATION
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _create_or_update_calendar_event(self):
|
||||
"""Create or update a calendar.event for this deposition."""
|
||||
if not self.scheduled_date or self.state in ('cancelled',):
|
||||
if self.calendar_event_id:
|
||||
self.calendar_event_id.write({'active': False})
|
||||
return
|
||||
|
||||
partners = self.env['res.partner']
|
||||
if self.case_id.petitioner_id:
|
||||
partners |= self.case_id.petitioner_id
|
||||
if self.env.user.partner_id:
|
||||
partners |= self.env.user.partner_id
|
||||
|
||||
duration = min(self.max_duration_hours or 7.0, 7.0)
|
||||
stop_dt = self.scheduled_date + timedelta(hours=duration)
|
||||
|
||||
type_label = dict(self._fields['deponent_type'].selection).get(
|
||||
self.deponent_type, 'Deponent'
|
||||
)
|
||||
event_name = '[{}] Deposition — {} ({})'.format(
|
||||
self.case_id.name,
|
||||
self.deponent_id.name,
|
||||
type_label,
|
||||
)
|
||||
description = (
|
||||
'FL 1.310 Deposition\n'
|
||||
'Case: {}\n'
|
||||
'Deponent: {} ({})\n'
|
||||
'Location: {}\n'
|
||||
'Court Reporter: {}\n'
|
||||
'Max Duration: {} hours\n'
|
||||
'Duces Tecum: {}\n'
|
||||
'Status: {}'
|
||||
).format(
|
||||
self.case_id.name,
|
||||
self.deponent_id.name,
|
||||
type_label,
|
||||
self.location or 'TBD',
|
||||
self.court_reporter or 'TBD',
|
||||
duration,
|
||||
'YES — document production required' if self.duces_tecum else 'No',
|
||||
dict(self._fields['state'].selection).get(self.state, ''),
|
||||
)
|
||||
|
||||
if self.calendar_event_id:
|
||||
self.calendar_event_id.write({
|
||||
'name': event_name,
|
||||
'start': self.scheduled_date,
|
||||
'stop': stop_dt,
|
||||
'description': description,
|
||||
'active': self.state not in ('cancelled',),
|
||||
'show_as': 'busy',
|
||||
})
|
||||
else:
|
||||
event = self.env['calendar.event'].sudo().create({
|
||||
'name': event_name,
|
||||
'start': self.scheduled_date,
|
||||
'stop': stop_dt,
|
||||
'description': description,
|
||||
'partner_ids': [(6, 0, partners.ids)],
|
||||
'show_as': 'busy',
|
||||
'privacy': 'confidential',
|
||||
})
|
||||
self.write({'calendar_event_id': event.id})
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
# WORKFLOW ACTIONS
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def action_mark_noticed(self):
|
||||
"""Notice of Taking Deposition has been served on deponent."""
|
||||
if not self.notice_date:
|
||||
self.notice_date = fields.Date.today()
|
||||
self.write({'state': 'noticed'})
|
||||
self.case_id.message_post(
|
||||
body=(
|
||||
'📋 <b>Deposition Notice Served</b><br/>'
|
||||
'Deponent: {} ({})<br/>'
|
||||
'Notice date: {}<br/>'
|
||||
'Scheduled: {}<br/>'
|
||||
'Notice valid: {} (FL 1.310(b): 10 days required)'
|
||||
).format(
|
||||
self.deponent_id.name,
|
||||
dict(self._fields['deponent_type'].selection).get(self.deponent_type, ''),
|
||||
self.notice_date,
|
||||
self.scheduled_date,
|
||||
'✅ Yes' if self.notice_valid else '⚠️ INSUFFICIENT NOTICE',
|
||||
),
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
def action_confirm(self):
|
||||
"""Deponent and court reporter confirmed."""
|
||||
self.write({'state': 'confirmed'})
|
||||
|
||||
def action_mark_completed(self):
|
||||
"""Deposition completed successfully."""
|
||||
self.write({'state': 'completed'})
|
||||
if self.calendar_event_id:
|
||||
self.calendar_event_id.write({'active': False})
|
||||
self.case_id.message_post(
|
||||
body=(
|
||||
'✅ <b>Deposition Completed</b><br/>'
|
||||
'Deponent: {} ({})<br/>'
|
||||
'Income verified: {} {}<br/>'
|
||||
'<i>Update Key Findings with testimony summary.</i>'
|
||||
).format(
|
||||
self.deponent_id.name,
|
||||
dict(self._fields['deponent_type'].selection).get(self.deponent_type, ''),
|
||||
'✅ ${}' .format(self.income_verified_amount) if self.income_verified else '❌ Not yet',
|
||||
'/month' if self.income_verified else '',
|
||||
),
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
def action_no_show(self):
|
||||
"""
|
||||
Deponent did not appear at deposition.
|
||||
FL 1.380: Motion to Compel / Motion for Sanctions.
|
||||
"""
|
||||
self.write({'state': 'no_show'})
|
||||
|
||||
# Create Motion to Compel deadline (20 days to file)
|
||||
mtc_due = fields.Date.today() + relativedelta(days=20)
|
||||
self.env['fl.deadline'].create({
|
||||
'case_id': self.case_id.id,
|
||||
'name': 'File Motion to Compel Deposition — {} No-Show (FL 1.380)'.format(
|
||||
self.deponent_id.name
|
||||
),
|
||||
'deadline_type': 'compliance',
|
||||
'due_date': mtc_due,
|
||||
'anchor_date': fields.Date.today(),
|
||||
'offset_days': 20,
|
||||
'statute_reference': (
|
||||
'FL 1.380 — Motion to Compel Attendance at Deposition. '
|
||||
'May also request sanctions under FL 1.380(b)(2).'
|
||||
),
|
||||
})
|
||||
|
||||
# Create project task
|
||||
if self.case_id.project_id:
|
||||
self.env['project.task'].create({
|
||||
'name': 'File Motion to Compel: {} Deposition No-Show'.format(
|
||||
self.deponent_id.name
|
||||
),
|
||||
'project_id': self.case_id.project_id.id,
|
||||
'description': (
|
||||
'{} failed to appear at the scheduled deposition on {}.\n\n'
|
||||
'Steps:\n'
|
||||
'1. Prepare Motion to Compel Attendance (FL 1.380)\n'
|
||||
'2. Include Certificate of Good Faith Effort to Resolve\n'
|
||||
'3. Request sanctions under FL 1.380(b)(2) — costs and attorney fees\n'
|
||||
'4. File with Miami-Dade Clerk and set hearing date\n\n'
|
||||
'Deadline to file: {}'
|
||||
).format(
|
||||
self.deponent_id.name,
|
||||
self.scheduled_date,
|
||||
mtc_due,
|
||||
),
|
||||
'date_deadline': mtc_due,
|
||||
})
|
||||
|
||||
self.case_id.message_post(
|
||||
body=(
|
||||
'<b>⚠️ DEPOSITION NO-SHOW — Motion to Compel Required</b><br/>'
|
||||
'{} did not appear at the deposition scheduled for {}.<br/><br/>'
|
||||
'<b>Action Required:</b> File a Motion to Compel Attendance (FL 1.380) '
|
||||
'within 20 days ({}).<br/>'
|
||||
'You may also request sanctions (attorney fees / costs) '
|
||||
'under FL 1.380(b)(2).'
|
||||
).format(
|
||||
self.deponent_id.name,
|
||||
self.scheduled_date,
|
||||
mtc_due,
|
||||
),
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
def action_cancel(self):
|
||||
"""Cancel this deposition (e.g. case settled, rescheduled)."""
|
||||
self.write({'state': 'cancelled'})
|
||||
if self.calendar_event_id:
|
||||
self.calendar_event_id.write({'active': False})
|
||||
|
||||
def action_reschedule(self):
|
||||
"""Mark as rescheduled — user should create a new deposition record."""
|
||||
self.write({'state': 'rescheduled'})
|
||||
self.case_id.message_post(
|
||||
body=(
|
||||
'🔄 <b>Deposition Rescheduled</b><br/>'
|
||||
'Deposition for {} has been marked rescheduled. '
|
||||
'Create a new deposition record with the new date and '
|
||||
'serve a new Notice of Taking Deposition.'
|
||||
).format(self.deponent_id.name),
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user