from datetime import timedelta from odoo import api, fields, models from dateutil.relativedelta import relativedelta class FlDeposition(models.Model): """ 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, string='Case' ) deponent_id = fields.Many2one( 'res.partner', string='Deponent', required=True ) deponent_type = fields.Selection([ ('opposing_party', 'Opposing Party'), ('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='Date the Notice of Taking Deposition was served on the deponent. ' 'FL 1.310(b): Minimum 10 days notice required.' ) 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'), ('confirmed', 'Confirmed'), ('completed', 'Completed'), ('no_show', 'Deponent No-Show'), ('cancelled', 'Cancelled'), ('rescheduled', 'Rescheduled'), ], string='Status', default='draft', tracking=True) # ── Duces Tecum ─────────────────────────────────────────────────────────── duces_tecum = fields.Boolean( string='Duces Tecum (Bring Documents)', help='Require deponent to produce documents at deposition' ) 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 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=( '📋 Deposition Notice Served
' 'Deponent: {} ({})
' 'Notice date: {}
' 'Scheduled: {}
' '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=( '✅ Deposition Completed
' 'Deponent: {} ({})
' 'Income verified: {} {}
' 'Update Key Findings with testimony summary.' ).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=( '⚠️ DEPOSITION NO-SHOW — Motion to Compel Required
' '{} did not appear at the deposition scheduled for {}.

' 'Action Required: File a Motion to Compel Attendance (FL 1.380) ' 'within 20 days ({}).
' '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=( '🔄 Deposition Rescheduled
' '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', )