2026-09-28 11:03:02 +05:30

407 lines
16 KiB
Python

from datetime import date
from calendar import monthrange
from odoo import api, fields, models
from odoo.exceptions import ValidationError
class DanceEnrollment(models.Model):
_inherit = "dance.enrollment"
invoice_count = fields.Integer(compute="_compute_invoice_count")
last_invoice_id = fields.Many2one("account.move", compute="_compute_invoice_count")
discount_type = fields.Selection(
[
("none", "No Discount"),
("sibling", "Sibling"),
("scholarship", "Scholarship"),
("custom", "Custom"),
],
default="none",
required=True,
)
discount_percent = fields.Float(string="Discount %")
apply_prorata = fields.Boolean(string="Apply Pro-rata", default=True)
waiver_state = fields.Selection(
[
("none", "No Waiver"),
("requested", "Requested"),
("approved", "Approved"),
("rejected", "Rejected"),
],
default="none",
required=True,
)
waiver_amount = fields.Monetary(currency_field="currency_id")
waiver_reason = fields.Text()
waiver_requested_by_id = fields.Many2one(
"res.users",
string="Waiver Requested By",
readonly=True,
)
waiver_requested_date = fields.Datetime(string="Waiver Requested On", readonly=True)
waiver_approved_by_id = fields.Many2one(
"res.users",
string="Waiver Approved By",
readonly=True,
)
waiver_approved_date = fields.Datetime(string="Waiver Approved On", readonly=True)
waiver_rejected_by_id = fields.Many2one(
"res.users",
string="Waiver Rejected By",
readonly=True,
)
waiver_rejected_date = fields.Datetime(string="Waiver Rejected On", readonly=True)
currency_id = fields.Many2one(
"res.currency",
related="batch_id.fee_structure_id.currency_id",
readonly=True,
)
base_fee_amount = fields.Monetary(
compute="_compute_fee_preview",
currency_field="currency_id",
)
discount_amount = fields.Monetary(
compute="_compute_fee_preview",
currency_field="currency_id",
)
prorata_amount = fields.Monetary(
compute="_compute_fee_preview",
currency_field="currency_id",
)
approved_waiver_amount = fields.Monetary(
compute="_compute_fee_preview",
currency_field="currency_id",
)
final_fee_amount = fields.Monetary(
compute="_compute_fee_preview",
currency_field="currency_id",
)
@api.depends(
"batch_id.fee_structure_id.amount",
"discount_percent",
"apply_prorata",
"enrollment_date",
"waiver_state",
"waiver_amount",
)
def _compute_fee_preview(self):
for enrollment in self:
if not enrollment.batch_id.fee_structure_id:
enrollment.base_fee_amount = 0.0
enrollment.discount_amount = 0.0
enrollment.prorata_amount = 0.0
enrollment.approved_waiver_amount = 0.0
enrollment.final_fee_amount = 0.0
continue
amounts = enrollment._get_invoice_amounts()
enrollment.base_fee_amount = amounts["base_amount"]
enrollment.discount_amount = amounts["discount_amount"]
enrollment.prorata_amount = amounts["prorata_amount"]
enrollment.approved_waiver_amount = amounts["waiver_amount"]
enrollment.final_fee_amount = amounts["final_amount"]
@api.onchange("discount_type")
def _onchange_discount_type(self):
defaults = {
"none": 0.0,
"sibling": 10.0,
"scholarship": 50.0,
}
if self.discount_type in defaults:
self.discount_percent = defaults[self.discount_type]
@api.constrains("discount_percent", "waiver_amount")
def _check_discount_and_waiver_amounts(self):
for enrollment in self:
if enrollment.discount_percent < 0 or enrollment.discount_percent > 100:
raise ValidationError("Discount percentage must be between 0 and 100.")
if enrollment.waiver_amount < 0:
raise ValidationError("Waiver amount cannot be negative.")
@api.constrains("discount_type", "student_id")
def _check_sibling_discount(self):
for enrollment in self:
if (
enrollment.discount_type == "sibling"
and enrollment.student_id
and not enrollment.student_id.sibling_ids
):
raise ValidationError(
"Sibling discount can only be used for students with linked siblings."
)
def _compute_invoice_count(self):
grouped = self.env["account.move"].read_group(
[
("dance_enrollment_id", "in", self.ids),
("move_type", "=", "out_invoice"),
],
["dance_enrollment_id"],
["dance_enrollment_id"],
)
count_map = {
record["dance_enrollment_id"][0]: record["dance_enrollment_id_count"]
for record in grouped
if record["dance_enrollment_id"]
}
latest_invoice_map = {}
invoices = self.env["account.move"].search(
[
("dance_enrollment_id", "in", self.ids),
("move_type", "=", "out_invoice"),
],
order="invoice_date desc, id desc",
)
for invoice in invoices:
latest_invoice_map.setdefault(invoice.dance_enrollment_id.id, invoice.id)
for enrollment in self:
enrollment.invoice_count = count_map.get(enrollment.id, 0)
enrollment.last_invoice_id = latest_invoice_map.get(enrollment.id)
def action_view_invoices(self):
self.ensure_one()
return {
"name": "Invoices",
"type": "ir.actions.act_window",
"res_model": "account.move",
"view_mode": "tree,form",
"domain": [
("dance_enrollment_id", "=", self.id),
("move_type", "=", "out_invoice"),
],
"context": {
"default_move_type": "out_invoice",
"default_partner_id": self.student_id.id,
"default_dance_batch_id": self.batch_id.id,
"default_dance_student_id": self.student_id.id,
"default_dance_enrollment_id": self.id,
},
}
def action_create_fee_invoice(self):
self.ensure_one()
if self.status != "active":
raise ValidationError("Only active enrollments can generate invoices.")
if not self.batch_id.fee_structure_id:
raise ValidationError("This batch does not have a fee structure.")
self._check_sibling_discount_before_invoice()
if self.waiver_state == "requested":
raise ValidationError("Approve or reject the fee waiver before creating an invoice.")
period_start, period_end = self._get_invoice_period()
existing = self.env["account.move"].search(
[
("move_type", "=", "out_invoice"),
("dance_enrollment_id", "=", self.id),
("dance_period_start", "=", period_start),
("dance_period_end", "=", period_end),
("state", "!=", "cancel"),
],
limit=1,
)
if existing:
raise ValidationError("An invoice already exists for this enrollment and period.")
fee_structure = self.batch_id.fee_structure_id
amounts = self._get_invoice_amounts(period_start, period_end)
income_account = self._get_income_account()
sales_journal = self._get_sales_journal()
invoice = self.env["account.move"].create(
{
"move_type": "out_invoice",
"journal_id": sales_journal.id,
"partner_id": self.student_id.id,
"invoice_date": fields.Date.today(),
"dance_student_id": self.student_id.id,
"dance_batch_id": self.batch_id.id,
"dance_enrollment_id": self.id,
"dance_fee_structure_id": fee_structure.id,
"dance_period_start": period_start,
"dance_period_end": period_end,
"invoice_line_ids": self._get_invoice_line_values(amounts, income_account),
}
)
return {
"name": "Invoice",
"type": "ir.actions.act_window",
"res_model": "account.move",
"res_id": invoice.id,
"view_mode": "form",
}
def action_request_fee_waiver(self):
self.write(
{
"waiver_state": "requested",
"waiver_requested_by_id": self.env.user.id,
"waiver_requested_date": fields.Datetime.now(),
"waiver_approved_by_id": False,
"waiver_approved_date": False,
"waiver_rejected_by_id": False,
"waiver_rejected_date": False,
}
)
return True
def action_approve_fee_waiver(self):
self._check_fee_waiver_approval_access()
for enrollment in self:
if not enrollment.waiver_amount:
raise ValidationError("Set a waiver amount before approving.")
self.write(
{
"waiver_state": "approved",
"waiver_approved_by_id": self.env.user.id,
"waiver_approved_date": fields.Datetime.now(),
"waiver_rejected_by_id": False,
"waiver_rejected_date": False,
}
)
return True
def action_reject_fee_waiver(self):
self._check_fee_waiver_approval_access()
self.write(
{
"waiver_state": "rejected",
"waiver_rejected_by_id": self.env.user.id,
"waiver_rejected_date": fields.Datetime.now(),
"waiver_approved_by_id": False,
"waiver_approved_date": False,
}
)
return True
def _check_fee_waiver_approval_access(self):
if not (
self.env.user.has_group("dance_fees.group_dance_fee_owner")
or self.env.user.has_group("account.group_account_manager")
):
raise ValidationError("Only a fee owner or accounting manager can approve waivers.")
def _check_sibling_discount_before_invoice(self):
self.ensure_one()
if self.discount_type == "sibling" and not self.student_id.sibling_ids:
raise ValidationError(
"Sibling discount can only be used for students with linked siblings."
)
def _get_invoice_period(self):
self.ensure_one()
today = fields.Date.context_today(self)
fee_structure = self.batch_id.fee_structure_id
if fee_structure.duration == "quarterly":
quarter = ((today.month - 1) // 3) * 3 + 1
period_start = date(today.year, quarter, 1)
period_end = date(today.year, quarter + 2, monthrange(today.year, quarter + 2)[1])
return period_start, period_end
period_start = date(today.year, today.month, 1)
period_end = date(today.year, today.month, monthrange(today.year, today.month)[1])
return period_start, period_end
def _get_invoice_amounts(self, period_start=False, period_end=False):
self.ensure_one()
if not period_start or not period_end:
period_start, period_end = self._get_invoice_period()
fee_structure = self.batch_id.fee_structure_id
currency = fee_structure.currency_id or self.env.company.currency_id
base_amount = fee_structure.amount
discount_amount = currency.round(base_amount * (self.discount_percent / 100.0))
after_discount = max(base_amount - discount_amount, 0.0)
prorata_amount = 0.0
prorata_factor = 1.0
if (
self.apply_prorata
and self.enrollment_date
and period_start < self.enrollment_date <= period_end
):
total_days = (period_end - period_start).days + 1
remaining_days = (period_end - self.enrollment_date).days + 1
prorata_factor = remaining_days / total_days if total_days else 1.0
prorated_total = currency.round(after_discount * prorata_factor)
prorata_amount = currency.round(after_discount - prorated_total)
after_discount = prorated_total
waiver_amount = 0.0
if self.waiver_state == "approved":
waiver_amount = min(self.waiver_amount, after_discount)
final_amount = currency.round(max(after_discount - waiver_amount, 0.0))
return {
"base_amount": currency.round(base_amount),
"discount_amount": discount_amount,
"prorata_amount": prorata_amount,
"waiver_amount": currency.round(waiver_amount),
"final_amount": final_amount,
"prorata_factor": prorata_factor,
}
def _get_invoice_line_name(self, amounts):
self.ensure_one()
fee_structure = self.batch_id.fee_structure_id
parts = ["%s - %s fee" % (self.batch_id.name, fee_structure.name)]
if self.discount_amount:
parts.append("%s%% %s discount" % (self.discount_percent, self.discount_type))
if amounts["prorata_amount"]:
parts.append("pro-rata applied")
if amounts["waiver_amount"]:
parts.append("approved waiver applied")
return " | ".join(parts)
def _get_invoice_line_values(self, amounts, income_account):
self.ensure_one()
fee_structure = self.batch_id.fee_structure_id
tax_commands = [(6, 0, fee_structure.tax_ids.ids)] if fee_structure.tax_ids else False
def line_values(name, price_unit):
values = {
"name": name,
"quantity": 1,
"price_unit": price_unit,
"account_id": income_account.id,
}
if tax_commands:
values["tax_ids"] = tax_commands
return (0, 0, values)
lines = [
line_values(
"%s - %s fee" % (self.batch_id.name, fee_structure.name),
amounts["base_amount"],
)
]
if amounts["discount_amount"]:
lines.append(
line_values(
"%s%% %s discount" % (self.discount_percent, self.discount_type),
-amounts["discount_amount"],
)
)
if amounts["prorata_amount"]:
lines.append(line_values("Pro-rata adjustment", -amounts["prorata_amount"]))
if amounts["waiver_amount"]:
lines.append(line_values("Approved fee waiver", -amounts["waiver_amount"]))
return lines
def _get_sales_journal(self):
journal = self.env["account.journal"].search(
[("type", "=", "sale"), ("company_id", "=", self.env.company.id)],
limit=1,
)
if not journal:
raise ValidationError("Create a sales journal before generating invoices.")
return journal
def _get_income_account(self):
account = self.env["account.account"].search(
[
("account_type", "=", "income"),
("deprecated", "=", False),
("company_id", "=", self.env.company.id),
],
limit=1,
)
if not account:
raise ValidationError("Create an income account before generating invoices.")
return account