"""Approval workflow models. Powers the admin "Approval Config" page (`/admin/approval-config`) and the per-record approval requests exposed by `ApprovalWorkflowController`. Prior to this module the controller hand-rolled raw SQL against tables that were never created. These ORM models now own the schema so Odoo can create the tables on install/update and the controller can use a standard API. """ from odoo import api, fields, models STATUS_SELECTION = [ ('draft', 'Draft'), ('active', 'Active'), ('archived', 'Archived'), ] REQUEST_STATE = [ ('draft', 'Draft'), ('in_progress', 'In Progress'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('bypassed', 'Bypassed'), ] STAGE_STATUS = [ ('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('skipped', 'Skipped'), ] class EncoachApprovalWorkflow(models.Model): _name = 'encoach.approval.workflow' _description = 'Approval Workflow' _order = 'create_date desc' name = fields.Char(required=True) type = fields.Char(default='custom', help='Categorical tag e.g. leave, purchase, content, custom.') status = fields.Selection(STATUS_SELECTION, default='draft', required=True, help='Draft = not yet routable. Active = available to new requests.') allow_bypass = fields.Boolean( default=False, help='If true, a bypass reason allows the workflow to be skipped.') entity_id = fields.Many2one('encoach.entity', ondelete='set null') stage_ids = fields.One2many('encoach.approval.stage', 'workflow_id', copy=True) request_ids = fields.One2many('encoach.approval.request', 'workflow_id') stage_count = fields.Integer(compute='_compute_counts') request_count = fields.Integer(compute='_compute_counts') @api.depends('stage_ids', 'request_ids') def _compute_counts(self): for rec in self: rec.stage_count = len(rec.stage_ids) rec.request_count = len(rec.request_ids) class EncoachApprovalStage(models.Model): _name = 'encoach.approval.stage' _description = 'Approval Workflow Stage' _order = 'sequence, id' workflow_id = fields.Many2one('encoach.approval.workflow', required=True, ondelete='cascade', index=True) sequence = fields.Integer(default=10, required=True, help='Stored order; the admin UI speaks `order`.') approver_id = fields.Many2one('res.users', required=True, ondelete='restrict') max_days = fields.Integer(default=3, help='SLA before auto-escalation can fire.') auto_escalate = fields.Boolean(default=False) notification_email = fields.Char( help='Optional CC address; falls back to approver_id.email otherwise.') status = fields.Selection(STAGE_STATUS, default='pending') comment = fields.Text() acted_at = fields.Datetime() class EncoachApprovalRequest(models.Model): _name = 'encoach.approval.request' _description = 'Approval Request' _order = 'create_date desc' workflow_id = fields.Many2one('encoach.approval.workflow', required=True, ondelete='cascade', index=True) res_model = fields.Char(help='Optional target model (e.g. op.student.leave).') res_id = fields.Integer(help='Optional target record id inside res_model.') state = fields.Selection(REQUEST_STATE, default='draft', required=True) requester_id = fields.Many2one('res.users', ondelete='set null') current_stage_id = fields.Many2one('encoach.approval.stage', ondelete='set null') bypass_reason = fields.Text() created_at = fields.Datetime(default=fields.Datetime.now)