diff --git a/activeblue_familylaw/data/fl_deadline_rules.xml b/activeblue_familylaw/data/fl_deadline_rules.xml index 734c382..cc701e2 100644 --- a/activeblue_familylaw/data/fl_deadline_rules.xml +++ b/activeblue_familylaw/data/fl_deadline_rules.xml @@ -45,5 +45,18 @@ + + + FL Family Law: Discovery Overdue Alerts (FL 1.370 / 1.380) + + code + model._cron_discovery_overdue_alerts() + 1 + days + -1 + True + + + diff --git a/activeblue_familylaw/models/fl_deposition.py b/activeblue_familylaw/models/fl_deposition.py index ab439f4..32f868b 100644 --- a/activeblue_familylaw/models/fl_deposition.py +++ b/activeblue_familylaw/models/fl_deposition.py @@ -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=( + '📋 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', + ) diff --git a/activeblue_familylaw/models/fl_discovery.py b/activeblue_familylaw/models/fl_discovery.py index cb6a100..5f0404b 100644 --- a/activeblue_familylaw/models/fl_discovery.py +++ b/activeblue_familylaw/models/fl_discovery.py @@ -5,14 +5,19 @@ from dateutil.relativedelta import relativedelta class FlDiscovery(models.Model): """ Phase 3 — Full implementation. - Phase 1: Stub with core fields. + Covers all Florida discovery methods with response tracking, + deficiency workflow, and Motion to Compel automation. - Covers all FL discovery methods: - - Interrogatories (FL 1.340) — 30 days to respond - - Request for Production (FL 1.350) — 30 days to respond - - Request for Admissions (FL 1.370) — 30 days to respond - - Subpoena — third party documents (FL 1.351) - - Depositions — tracked in fl.deposition + 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' @@ -20,51 +25,419 @@ class FlDiscovery(models.Model): _order = 'served_date asc' case_id = fields.Many2one( - 'fl.case', required=True, ondelete='cascade', index=True + 'fl.case', required=True, ondelete='cascade', index=True, + string='Case' ) discovery_type = fields.Selection([ ('interrogatories', 'Interrogatories (FL 1.340)'), - ('production', 'Request for Production (FL 1.350)'), + ('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 (FL 1.310)'), - ], string='Discovery Type', required=True) + ('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' + 'res.partner', string='Third Party', + help='Employer, bank, or other third party for subpoena' ) - description = fields.Char(string='Description / Subject') - served_date = fields.Date(string='Served Date') + 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='FL 1.340/1.350/1.370: 30 days to respond' + 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 Received Date') - response_complete = fields.Boolean(string='Response Complete') - objections_raised = fields.Boolean(string='Objections Raised') - objection_detail = fields.Text(string='Objection Details') - deficiency_notice_sent = fields.Boolean(string='Deficiency Notice Sent') + 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'), + ('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 != 'deposition': - rec.response_due_date = ( - rec.served_date + relativedelta(days=30) - ) + 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=( + '📬 Discovery Served
' + 'Type: {}
' + 'Directed to: {}
' + 'Description: {}
' + '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=( + '⚠️ Discovery Response Deficient
' + '{} response from {} is incomplete or evasive.

' + 'Next Steps:
' + '1. Send written deficiency notice ({}) — required before Motion to Compel
' + '2. Allow 10-14 days for supplemental response
' + '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=( + '⚖️ Motion to Compel Filed (FL 1.380)
' + 'Discovery: {} — {}
' + 'Directed to: {}
' + '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=( + '🔴 Discovery Response OVERDUE ({} days)
' + 'Type: {}
' + 'Directed to: {}
' + 'Description: {}
' + 'Was due: {}

' + 'Next Step: 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=( + '🚨 DEEMED ADMITTED — FL 1.370 CRITICAL ALERT
' + 'A Request for Admissions directed to {} received NO response ' + 'within 30 days.
' + 'Under FL 1.370, each matter is now AUTOMATICALLY DEEMED ADMITTED ' + 'and may be used against that party at hearing.

' + 'If you are the responding party: immediately file a Motion to ' + 'Withdraw or Amend the Admissions (court has discretion to allow this ' + 'if no prejudice to the requesting party).

' + 'If you are the requesting party: 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', + ) diff --git a/activeblue_familylaw/views/fl_deposition_views.xml b/activeblue_familylaw/views/fl_deposition_views.xml index b4ee6f4..c1a10e4 100644 --- a/activeblue_familylaw/views/fl_deposition_views.xml +++ b/activeblue_familylaw/views/fl_deposition_views.xml @@ -2,67 +2,234 @@ + fl.deposition.tree fl.deposition - + - - - + + + + + + fl.deposition.form fl.deposition
- +
+ + + + + + + + + + + + - + + + - - - + + - - + + + + + + + + + + + + + + + + + - + - + + + + + + + + + + + +
+ + +
+ + + fl.deposition.calendar + fl.deposition + + + + + + + + + + + + + + fl.deposition.search + fl.deposition + + + + + + + + + + + + + + + + + + + + Depositions fl.deposition - tree,form + tree,calendar,form + + [('state', 'not in', ['cancelled'])] + {'search_default_filter_pending': 1}
diff --git a/activeblue_familylaw/views/fl_discovery_views.xml b/activeblue_familylaw/views/fl_discovery_views.xml index 2dacce4..cbee807 100644 --- a/activeblue_familylaw/views/fl_discovery_views.xml +++ b/activeblue_familylaw/views/fl_discovery_views.xml @@ -2,65 +2,242 @@ + fl.discovery.tree fl.discovery - + - + + - - + + + + +