from datetime import date, datetime, time from odoo import api, fields, models from dateutil.relativedelta import relativedelta class FlDeadline(models.Model): """ Phase 2 — Full implementation with calendar integration and cron alerts. Phase 1: Core fields and generate_deadlines_for_case stub. """ _name = 'fl.deadline' _description = 'Case Deadline' _inherit = ['mail.thread'] _order = 'due_date asc' case_id = fields.Many2one( 'fl.case', required=True, ondelete='cascade', index=True ) name = fields.Char(string='Deadline', required=True) deadline_type = fields.Selection([ ('filing', 'Filing'), ('service', 'Service of Process'), ('response', 'Response / Answer'), ('discovery_open', 'Discovery Opens'), ('financial_disclosure', 'Financial Disclosure Exchange'), ('deposition_notice', 'Deposition Notice Deadline'), ('deposition', 'Deposition'), ('discovery_cutoff', 'Discovery Cutoff'), ('parenting_class', 'Parenting Class Completion'), ('mediation', 'Mediation'), ('hearing', 'Hearing'), ('compliance', 'Compliance Deadline'), ('emancipation', 'Emancipation'), ('default_motion', 'Motion for Default'), ('custom', 'Custom'), ], required=True) statute_reference = fields.Char( string='Statute / Rule', help='e.g. FL 1.140 — 20 days to answer' ) due_date = fields.Date(string='Due Date', required=True, tracking=True) anchor_date = fields.Date( string='Anchor Date', help='Date this deadline is calculated from' ) offset_days = fields.Integer( string='Offset Days', help='Days from anchor date to due date' ) completed = fields.Boolean(string='Completed', tracking=True) completed_date = fields.Date(string='Completion Date') waived = fields.Boolean(string='Waived') notes = fields.Text(string='Notes') # ── Calendar Integration (Phase 2) ──────────────────────────────────── calendar_event_id = fields.Many2one( 'calendar.event', string='Calendar Event' ) # ── Alerts ──────────────────────────────────────────────────────────── alert_7day_sent = fields.Boolean(default=False) alert_3day_sent = fields.Boolean(default=False) alert_1day_sent = fields.Boolean(default=False) overdue_alert_sent = fields.Boolean(default=False) is_overdue = fields.Boolean( string='Overdue', compute='_compute_is_overdue', store=True ) days_until_due = fields.Integer( string='Days Until Due', compute='_compute_days_until_due' ) @api.depends('due_date', 'completed', 'waived') def _compute_is_overdue(self): today = fields.Date.today() for rec in self: rec.is_overdue = ( not rec.completed and not rec.waived and bool(rec.due_date) and rec.due_date < today ) @api.depends('due_date') def _compute_days_until_due(self): today = fields.Date.today() for rec in self: if rec.due_date: rec.days_until_due = (rec.due_date - today).days else: rec.days_until_due = 0 def action_mark_complete(self): self.completed = True self.completed_date = fields.Date.today() # ── Generation Engine ───────────────────────────────────────────────── @api.model def generate_deadlines_for_case(self, case): """ Auto-generate procedural deadlines from filing_date. Phase 1: Core deadlines only. Phase 2: Full calendar events + alert setup. """ if not case.filing_date: return # Avoid duplicates on re-run existing_types = case.deadline_ids.mapped('deadline_type') rules = [ { 'name': 'Serve Respondent (Target)', 'deadline_type': 'service', 'offset_days': 30, 'anchor': case.filing_date, 'statute_reference': 'FL 1.070 — 120-day maximum', }, ] if case.has_minor_children: rules.append({ 'name': 'Parenting Class — Petitioner (FL 61.21)', 'deadline_type': 'parenting_class', 'offset_days': 45, 'anchor': case.filing_date, 'statute_reference': 'FL 61.21', }) for rule in rules: if rule['deadline_type'] in existing_types: continue due = rule['anchor'] + relativedelta(days=rule['offset_days']) self.create({ 'case_id': case.id, 'name': rule['name'], 'deadline_type': rule['deadline_type'], 'due_date': due, 'anchor_date': rule['anchor'], 'offset_days': rule['offset_days'], 'statute_reference': rule.get('statute_reference', ''), }) @api.model def recalculate_service_deadlines(self, case): """ Recalculate deadlines anchored to service_date when it is set. Called from fl_case.write when service_date changes. """ if not case.service_date: return service_anchored = [ { 'name': 'Respondent Answer Deadline', 'deadline_type': 'response', 'offset_days': 20, 'statute_reference': 'FL 1.140 — 20 days to answer', }, { 'name': 'Mandatory Financial Disclosure Exchange', 'deadline_type': 'financial_disclosure', 'offset_days': 45, 'statute_reference': 'FL 12.285 — 45 days', }, { 'name': 'Discovery Opens (Case at Issue)', 'deadline_type': 'discovery_open', 'offset_days': 20, 'statute_reference': 'FL 12.280', }, ] if case.has_minor_children: service_anchored.append({ 'name': 'Parenting Class — Respondent (FL 61.21)', 'deadline_type': 'parenting_class', 'offset_days': 60, 'statute_reference': 'FL 61.21', }) existing = {d.deadline_type: d for d in case.deadline_ids} for rule in service_anchored: due = case.service_date + relativedelta(days=rule['offset_days']) dl_type = rule['deadline_type'] if dl_type in existing: existing[dl_type].write({ 'due_date': due, 'anchor_date': case.service_date, }) else: self.create({ 'case_id': case.id, 'name': rule['name'], 'deadline_type': dl_type, 'due_date': due, 'anchor_date': case.service_date, 'offset_days': rule['offset_days'], 'statute_reference': rule.get('statute_reference', ''), }) def _cron_deadline_alerts(self): """Run daily — send deadline alerts at 7, 3, 1 days and overdue.""" today = fields.Date.today() upcoming = self.search([ ('completed', '=', False), ('waived', '=', False), ('due_date', '>=', today), ]) for dl in upcoming: days = (dl.due_date - today).days if days == 7 and not dl.alert_7day_sent: dl._send_deadline_alert('7 days') dl.alert_7day_sent = True elif days == 3 and not dl.alert_3day_sent: dl._send_deadline_alert('3 days') dl.alert_3day_sent = True elif days == 1 and not dl.alert_1day_sent: dl._send_deadline_alert('1 day') dl.alert_1day_sent = True overdue = self.search([ ('completed', '=', False), ('waived', '=', False), ('due_date', '<', today), ('overdue_alert_sent', '=', False), ]) for dl in overdue: dl._send_deadline_alert('OVERDUE') dl.overdue_alert_sent = True def _send_deadline_alert(self, timing): icon = '🔴' if timing == 'OVERDUE' else '⏰' self.case_id.message_post( body=( f'{icon} Deadline Alert — {timing}
' f'{self.name} is due on {self.due_date}.
' f'Statute: {self.statute_reference or "N/A"}' ), subtype_xmlid='mail.mt_note', )