Serves Demo Scene 3: a fee structure (category x amount lines) with a
term-wise installment schedule generates a correct account.move for a
given enrollment + term, a concession recalculates it correctly, and
the invoice is billed to the primary guardian's partner so stock
portal invoice visibility (partner_id-based) works with no new record
rule. Invoices are plain account.move (_inherit adds mc_student_id/
mc_enrollment_id only) - no invoice model was built, per CLAUDE.md
sec 1.3.
Fixed a real modeling mistake before it shipped: the schedule's
"percentages must total 100%" rule was originally a blocking
@api.constrains on every line write, which breaks the normal workflow
of adding one term at a time (every intermediate state before the
last line is, correctly, under 100%) - and would have broken this
module's own demo data loading, since each schedule line is a
separate XML record. Moved the check to where it actually matters:
the invoice-generation wizard now raises a clear UserError if the
resolved schedule doesn't total 100% at the point of use, while a
live constraint still blocks the one thing that's unambiguously wrong
at any point - allocating more than 100%.
Also found, by testing money arithmetic against a live odoo:19.0
container rather than trusting the arithmetic by inspection: every
generated invoice total came back at exactly 1.15x the expected
amount, because the standing "School Fee" product picked up the demo
company's default sales tax. Fixed by explicitly clearing taxes_id on
the product - school fees are correctly untaxed (education services
are GST-exempt in India), not just conveniently untaxed for the test.
Testing this module's access rules surfaced two real bugs in the
security model, not just test bugs, fixed here:
- group_school_staff (from O1) never implied base.group_user, so
any real user holding only this app's custom groups lacked
ordinary internal-user access to core models like res.company -
caught directly via a test user unable to even create an
mc.fee.structure (whose company_id defaults through
self.env.company).
- mc.fee.concession reveals sensitive per-student financial data
(e.g. a need-based hardship discount and its reason). Staff had
read access, and since group_teacher implies group_school_staff,
teachers inherited it too - exposing family financial
circumstances to a role with no legitimate need for it. Removed
the staff access row; only Administrator/Accountant keep it now.
Fee *structure* (per-program pricing, not sensitive) correctly
stays staff/teacher-readable.
CLAUDE.md gets one more Odoo 19 API correction:
res.users.groups_id -> group_ids.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
51 lines
2.2 KiB
Python
51 lines
2.2 KiB
Python
from odoo import api, fields, models
|
|
from odoo.exceptions import ValidationError
|
|
|
|
|
|
class McFeeConcession(models.Model):
|
|
_name = "mc.fee.concession"
|
|
_inherit = ["mail.thread"]
|
|
_description = "Fee Concession"
|
|
_order = "student_id"
|
|
_rec_name = "display_name"
|
|
|
|
student_id = fields.Many2one("mc.student", string="Student", required=True, ondelete="cascade")
|
|
concession_type = fields.Selection(
|
|
[
|
|
("sibling", "Sibling"),
|
|
("merit", "Merit"),
|
|
("staff", "Staff"),
|
|
("need_based", "Need-based"),
|
|
],
|
|
string="Type", required=True, tracking=True,
|
|
)
|
|
computation = fields.Selection(
|
|
[("percent", "Percentage"), ("fixed", "Fixed Amount")],
|
|
string="Computed As", required=True, default="percent", tracking=True,
|
|
)
|
|
value = fields.Float(string="Value", required=True, tracking=True,
|
|
help="A percentage (0-100) if computed as Percentage, otherwise a fixed amount.")
|
|
reason = fields.Char(string="Reason", required=True)
|
|
approver_id = fields.Many2one("res.users", string="Approved By", required=True, tracking=True)
|
|
active = fields.Boolean(string="Active", default=True)
|
|
|
|
@api.depends("student_id.name", "concession_type", "value", "computation")
|
|
def _compute_display_name(self):
|
|
for concession in self:
|
|
value_label = "%.0f%%" % concession.value if concession.computation == "percent" else str(concession.value)
|
|
concession.display_name = "%s - %s (%s)" % (
|
|
concession.student_id.name or "?",
|
|
dict(concession._fields["concession_type"].selection).get(concession.concession_type, "?"),
|
|
value_label,
|
|
)
|
|
|
|
@api.constrains("computation", "value")
|
|
def _check_percent_range(self):
|
|
for concession in self:
|
|
if concession.computation == "percent" and not (0 < concession.value <= 100):
|
|
raise ValidationError(
|
|
"A percentage concession's value must be between 0 and 100."
|
|
)
|
|
if concession.value <= 0:
|
|
raise ValidationError("A concession's value must be greater than zero.")
|