Files
famlaw/activeblue_familylaw/models/fl_child.py
Carlos Garcia 1d52d85a78 Phase 1: core models, security, seed data, and backend views
Implements full Phase 1 of the activeblue_familylaw Odoo 18 module:
- 17 Python models (fl.case, fl.party, fl.child, fl.support.calculation,
  fl.fee.waiver, fl.income.withholding, fl.deadline, fl.hearing,
  fl.deposition, fl.discovery, fl.document, fl.caselaw, fl.analysis,
  fl.ai.engine, fl.argument, fl.statute, fl.issue.tag) + hr.expense extension
- 3 wizard stubs (intake, analysis, generate-packet)
- Security: 4 groups (admin/paralegal/portal-petitioner/portal-respondent)
  + record rules scoping portal users to their own cases
- Seed data: issue tags, FL statutes, FL DCF support schedule, ir.sequence
- 13 backend view XML files with FL 61.30 worksheet, fee waiver
  eligibility banner, DV safety resources, emancipation alerts
- Static CSS/JS stubs for Phase 6 portal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 18:52:04 -04:00

167 lines
6.8 KiB
Python

from odoo import api, fields, models
from dateutil.relativedelta import relativedelta
class FlChild(models.Model):
_name = 'fl.child'
_description = 'Child on Case'
_order = 'date_of_birth'
case_id = fields.Many2one(
'fl.case', string='Case',
required=True, ondelete='cascade', index=True
)
name = fields.Char(string='Full Name', required=True)
date_of_birth = fields.Date(string='Date of Birth', required=True)
age = fields.Integer(
string='Age', compute='_compute_age', store=True
)
gender = fields.Selection([
('m', 'Male'),
('f', 'Female'),
('x', 'Non-binary / Other'),
], string='Gender')
school = fields.Char(string='School')
medical_provider = fields.Char(string='Primary Care Provider')
# ── Support Per Child ──────────────────────────────────────────────────
support_amount = fields.Float(
string='Support Amount This Child ($)',
help='Per-child support amount from the calculation'
)
health_insurance_premium = fields.Float(
string='Health Insurance Premium ($)',
help='Monthly cost of health insurance for this child only'
)
childcare_cost = fields.Float(
string='Work-Related Childcare ($)',
help='FL 61.30(7): Only work or job-search related childcare costs qualify'
)
extraordinary_expenses = fields.Float(
string='Extraordinary Expenses ($)',
help='FL 61.30(9): Medical or educational expenses beyond ordinary child-rearing costs'
)
# ── Emancipation Tracking ──────────────────────────────────────────────
emancipation_date = fields.Date(
string='Emancipation Date',
compute='_compute_emancipation',
store=True,
help='18th birthday (or high school graduation, whichever is later). '
'Support terminates on this date.'
)
approaching_emancipation = fields.Boolean(
string='Approaching Emancipation',
compute='_compute_approaching_emancipation',
store=True,
help='True when emancipation is within 90 days'
)
emancipation_alert_sent = fields.Boolean(
string='Emancipation Alert Sent',
default=False
)
emancipated = fields.Boolean(
string='Emancipated',
compute='_compute_emancipated',
store=True
)
days_until_emancipation = fields.Integer(
string='Days Until Emancipation',
compute='_compute_approaching_emancipation'
)
# ── Early Emancipation Factors ─────────────────────────────────────────
married = fields.Boolean(
string='Child is Married',
help='Marriage constitutes emancipation under FL law'
)
active_military = fields.Boolean(
string='Active Military Service',
help='Active duty military service constitutes emancipation under FL law'
)
declared_emancipated = fields.Boolean(
string='Court-Declared Emancipated',
help='Court has entered an order declaring this child emancipated'
)
emancipation_notes = fields.Text(
string='Emancipation Notes',
help='Document basis for any early emancipation determination'
)
# ── Computed ──────────────────────────────────────────────────────────
@api.depends('date_of_birth')
def _compute_age(self):
today = fields.Date.today()
for rec in self:
if rec.date_of_birth:
delta = relativedelta(today, rec.date_of_birth)
rec.age = delta.years
else:
rec.age = 0
@api.depends('date_of_birth')
def _compute_emancipation(self):
for rec in self:
if rec.date_of_birth:
rec.emancipation_date = rec.date_of_birth + relativedelta(years=18)
else:
rec.emancipation_date = False
@api.depends('emancipation_date', 'married', 'active_military', 'declared_emancipated')
def _compute_approaching_emancipation(self):
today = fields.Date.today()
for rec in self:
# Early emancipation factors
if rec.married or rec.active_military or rec.declared_emancipated:
rec.approaching_emancipation = False
rec.days_until_emancipation = 0
continue
if rec.emancipation_date:
days_remaining = (rec.emancipation_date - today).days
rec.days_until_emancipation = max(days_remaining, 0)
rec.approaching_emancipation = 0 < days_remaining <= 90
else:
rec.approaching_emancipation = False
rec.days_until_emancipation = 0
@api.depends('emancipation_date', 'married', 'active_military', 'declared_emancipated')
def _compute_emancipated(self):
today = fields.Date.today()
for rec in self:
if rec.married or rec.active_military or rec.declared_emancipated:
rec.emancipated = True
elif rec.emancipation_date:
rec.emancipated = today >= rec.emancipation_date
else:
rec.emancipated = False
# ── Cron ──────────────────────────────────────────────────────────────
def _cron_emancipation_alerts(self):
"""
Run daily via ir.cron.
Sends chatter alert on cases where a child is approaching emancipation
and no alert has been sent yet.
"""
approaching = self.search([
('approaching_emancipation', '=', True),
('emancipation_alert_sent', '=', False),
])
for child in approaching:
days = child.days_until_emancipation
child.case_id.message_post(
body=(
f'<strong>⚠️ EMANCIPATION ALERT</strong><br/>'
f'<b>{child.name}</b> will turn 18 on '
f'<b>{child.emancipation_date}</b> '
f'({days} days from today).<br/>'
f'Child support for this child terminates on that date.<br/>'
f'<b>Action required:</b> A Motion to Modify should be filed '
f'at least 60 days before emancipation to address the change '
f'in support obligation.'
),
subtype_xmlid='mail.mt_note',
)
child.emancipation_alert_sent = True