169 lines
7.2 KiB
Python
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)
|