169 lines
7.2 KiB
Python

# pyrefly: ignore [missing-import]
import datetime
from odoo import api, fields, models
# pyrefly: ignore [missing-import]
from odoo.exceptions import ValidationError
class EducationFeeAssignment(models.Model):
_name = 'education.fee.assignment'
_description = 'Student Fee Invoice / Assignment'
_order = 'assignment_date desc, id desc'
name = fields.Char(string='Invoice Number', copy=False, readonly=True, default='/', index=True)
student_id = fields.Many2one('education.student', string='Student', required=True, index=True)
academic_year_id = fields.Many2one('education.academic.year', string='Academic Year', required=True, index=True)
academic_term_id = fields.Many2one('education.academic.term', string='Academic Term', index=True)
semester_id = fields.Many2one('education.semester', string='Semester', required=True, index=True)
fee_structure_id = fields.Many2one('education.fee.structure', string='Fee Structure', required=True, index=True)
assignment_date = fields.Date(string='Assignment Date', required=True, default=fields.Date.context_today)
due_date = fields.Date(
string='Due Date',
required=True,
default=fields.Date.context_today,
index=True
)
state = fields.Selection([
('draft', 'Draft'),
('confirmed', 'Confirmed'),
], string='Status', default='draft', required=True, index=True)
currency_id = fields.Many2one('res.currency', related='fee_structure_id.currency_id', store=True)
line_ids = fields.One2many(related='fee_structure_id.line_ids', string='Fee Lines', readonly=True)
payment_ids = fields.One2many('education.student.payment', 'fee_assignment_id', string='Payments')
amount = fields.Monetary(
string='Total Invoiced Amount',
compute='_compute_amounts',
store=True,
currency_field='currency_id'
)
amount_paid = fields.Monetary(
string='Amount Paid',
compute='_compute_amounts',
store=True,
currency_field='currency_id'
)
amount_due = fields.Monetary(
string='Amount Due',
compute='_compute_amounts',
store=True,
currency_field='currency_id'
)
payment_status = fields.Selection([
('unpaid', 'Unpaid'),
('partial', 'Partially Paid'),
('paid', 'Paid'),
('overdue', 'Overdue'),
], string='Payment Status', compute='_compute_amounts', store=True, index=True)
active = fields.Boolean(default=True)
company_id = fields.Many2one(
'res.company',
string='Company',
required=True,
default=lambda self: self.env.company,
index=True,
)
@api.onchange('fee_structure_id', 'assignment_date')
def _onchange_fee_structure(self):
if self.fee_structure_id and self.assignment_date:
days = getattr(self.fee_structure_id, 'due_days', 30) or 30
self.due_date = self.assignment_date + datetime.timedelta(days=days)
@api.constrains('student_id', 'academic_year_id', 'semester_id', 'fee_structure_id', 'company_id')
def _check_unique_assignment(self):
for rec in self:
domain = [
('id', '!=', rec.id),
('student_id', '=', rec.student_id.id),
('academic_year_id', '=', rec.academic_year_id.id),
('semester_id', '=', rec.semester_id.id),
('fee_structure_id', '=', rec.fee_structure_id.id),
('company_id', '=', rec.company_id.id),
('active', '=', True),
]
if self.search_count(domain) > 0:
raise ValidationError("This fee structure is already assigned to this student for this semester.")
@api.depends(
'fee_structure_id.line_ids.amount',
'payment_ids.amount',
'payment_ids.status',
'due_date',
'state'
)
def _compute_amounts(self):
today = fields.Date.today()
for rec in self:
total = sum(line.amount for line in rec.fee_structure_id.line_ids) if rec.fee_structure_id else 0.0
rec.amount = total
confirmed_payments = rec.payment_ids.filtered(lambda p: p.status == 'confirmed')
paid = sum(p.amount for p in confirmed_payments)
# Check if student's overall dues are fully cleared and no specific allocation was attached
if paid == 0 and total > 0 and rec.student_id.outstanding_amount == 0 and rec.student_id.total_paid >= total:
paid = total
rec.amount_paid = paid
rec.amount_due = max(0.0, total - paid)
if rec.state != 'confirmed':
rec.payment_status = 'unpaid'
elif total == 0 or rec.amount_due <= 0:
rec.payment_status = 'paid'
elif rec.due_date and rec.due_date < today:
rec.payment_status = 'overdue'
elif paid > 0:
rec.payment_status = 'partial'
else:
rec.payment_status = 'unpaid'
def action_confirm(self):
for rec in self:
if rec.state == 'confirmed':
continue
if not rec.fee_structure_id.line_ids:
raise ValidationError(f"The selected fee structure '{rec.fee_structure_id.name}' has no fee lines. Please add fee lines to this structure before confirming.")
if not rec.name or rec.name == '/':
seq = self.env['ir.sequence'].next_by_code('education.fee.assignment')
rec.name = seq or f"INV/{rec.id:05d}"
for line in rec.fee_structure_id.line_ids:
self.env['education.student.ledger'].create({
'student_id': rec.student_id.id,
'academic_year_id': rec.academic_year_id.id,
'semester_id': rec.semester_id.id,
'category_id': line.fee_type_id.category_id.id,
'fee_type_id': line.fee_type_id.id,
'date': rec.assignment_date,
'ref': rec.name,
'description': f"{line.fee_type_id.name} (Invoice: {rec.name}, Structure: {rec.fee_structure_id.name})",
'debit': line.amount,
'credit': 0.0,
'status': 'outstanding',
'company_id': rec.company_id.id,
})
rec.state = 'confirmed'
def action_cancel(self):
for rec in self:
if rec.state == 'confirmed':
ledger_entries = self.env['education.student.ledger'].search([
('student_id', '=', rec.student_id.id),
('ref', '=', rec.name),
])
ledger_entries.unlink()
rec.state = 'draft'
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('name') or vals.get('name') == '/':
vals['name'] = self.env['ir.sequence'].next_by_code('education.fee.assignment') or '/'
if not vals.get('due_date') and vals.get('assignment_date'):
vals['due_date'] = fields.Date.from_string(vals['assignment_date']) + datetime.timedelta(days=30)
return super().create(vals_list)