INTRODUCTION

Your Best Engineers Are Leaving Because Nobody Told Them Where They Stand.

We see this pattern at mid-size companies running Odoo for everything except HR performance management: quarterly reviews happen in spreadsheets emailed between managers, goals live in a Google Doc nobody updates, and feedback is a once-a-year surprise delivered in a 30-minute meeting. The result is predictable — top performers leave because they have no visibility into their growth trajectory, and underperformers coast because nobody tracks commitments.

Odoo 19 ships a completely reworked Appraisals module that fixes this. The hr_appraisal module now supports structured appraisal plans with recurring schedules, weighted goal tracking tied to employee profiles, true 360-degree feedback with anonymous peer reviews, customizable review templates, and skill gap analysis that feeds directly into the Training module. It's the first version of Odoo's appraisal system that can genuinely replace BambooHR or Lattice for performance management.

This guide walks you through configuring every component — from creating your first appraisal plan to building automated review cycles that run without HR intervention. Every code snippet and XML configuration is tested on Odoo 19 Community and Enterprise.

01

How to Configure Appraisal Plans with Recurring Schedules in Odoo 19

Appraisal plans define who gets reviewed, how often, and by whom. In Odoo 19, plans are department-level objects that automatically generate appraisal records for every employee in the department on a recurring schedule. No more manual creation of 200 appraisal forms every quarter.

Step 1 — Enable the Appraisals Module

Navigate to Apps and install hr_appraisal. On Enterprise, also install hr_appraisal_skills and hr_appraisal_survey for skill assessments and 360 feedback surveys. These modules add critical fields to the appraisal model.

Python — Install via CLI
# Install appraisal modules on an existing database
./odoo-bin -c /etc/odoo/odoo.conf \
  -d production_db \
  -i hr_appraisal,hr_appraisal_skills,hr_appraisal_survey \
  --stop-after-init

Step 2 — Create a Recurring Appraisal Plan

Go to Appraisals → Configuration → Settings. Enable Automatic Appraisal Generation. Then navigate to Appraisals → Configuration → Appraisal Plans and create a new plan:

FieldValueEffect
NameQuarterly Engineering ReviewDisplayed on appraisal forms
DepartmentEngineeringScoped to all employees in Engineering + sub-departments
RecurrenceEvery 3 monthsCron job auto-creates appraisals on the 1st of each quarter
Review Deadline15 days after creationEmployee and manager see a countdown; overdue appraisals flagged in Kanban
TemplateEngineering Performance TemplatePre-fills questions and rating scales (configured in Step 5)

Step 3 — Automate Plan Execution via Cron

Odoo 19 registers a ir.cron record that triggers appraisal generation. You can customize the cron schedule or trigger it manually for testing:

Python — Shell command to trigger appraisal generation
# Trigger appraisal generation manually from odoo shell
env['hr.appraisal'].sudo()._cron_create_appraisals()

# Verify generated appraisals
appraisals = env['hr.appraisal'].search([
    ('state', '=', 'new'),
    ('date_close', '>=', '2026-04-01'),
])
print(f"Generated {{len(appraisals)}} appraisals for Q2 2026")
Appraisal Plan Scoping

Plans inherit department hierarchy. If you create a plan for "Engineering," employees in "Engineering / Backend" and "Engineering / Frontend" sub-departments are automatically included. To exclude a sub-department, create a separate plan for it with a longer recurrence period or mark it inactive.

02

Setting Up Goal Management with Weighted Objectives and Progress Tracking

Goals in Odoo 19 are not just text fields on an appraisal form. They are standalone records linked to employees, with deadlines, weight percentages, progress tracking, and manager sign-off. This means goals persist across appraisal cycles — a Q1 goal that extends into Q2 carries its progress forward instead of being re-created from scratch.

Step 1 — Define Goal Categories

Before creating individual goals, set up categories that align with your performance framework. Navigate to Appraisals → Configuration → Goal Tags:

XML — Goal tag seed data for a custom module
<odoo>
  <data noupdate="1">
    <record id="goal_tag_delivery" model="hr.appraisal.goal.tag">
      <field name="name">Delivery &amp; Execution</field>
      <field name="color">1</field>
    </record>
    <record id="goal_tag_leadership" model="hr.appraisal.goal.tag">
      <field name="name">Leadership &amp; Mentoring</field>
      <field name="color">2</field>
    </record>
    <record id="goal_tag_innovation" model="hr.appraisal.goal.tag">
      <field name="name">Innovation &amp; Learning</field>
      <field name="color">4</field>
    </record>
    <record id="goal_tag_culture" model="hr.appraisal.goal.tag">
      <field name="name">Culture &amp; Collaboration</field>
      <field name="color">5</field>
    </record>
  </data>
</odoo>

Step 2 — Create Weighted Goals per Employee

Each employee's goals should have weights that sum to 100%. The weighted score drives the final appraisal rating. Navigate to Appraisals → Goals or assign goals directly from the employee profile:

Python — Programmatic goal creation
# Create goals for a specific employee
employee = env['hr.employee'].browse(42)

goals = [
    {
        'name': 'Deliver Project Atlas on schedule',
        'employee_id': employee.id,
        'manager_id': employee.parent_id.id,
        'tag_ids': [(4, env.ref('my_module.goal_tag_delivery').id)],
        'deadline': '2026-06-30',
        'weight': 40,  # 40% of final score
        'description': 'All milestones delivered by June 30. '
                       'Measured by PM sign-off on each phase.',
    },
    {
        'name': 'Mentor 2 junior developers through onboarding',
        'employee_id': employee.id,
        'manager_id': employee.parent_id.id,
        'tag_ids': [(4, env.ref('my_module.goal_tag_leadership').id)],
        'deadline': '2026-06-30',
        'weight': 25,
        'description': 'Both mentees pass their 90-day review. '
                       'Weekly 1:1 sessions logged in calendar.',
    },
    {
        'name': 'Complete AWS Solutions Architect certification',
        'employee_id': employee.id,
        'manager_id': employee.parent_id.id,
        'tag_ids': [(4, env.ref('my_module.goal_tag_innovation').id)],
        'deadline': '2026-05-31',
        'weight': 20,
        'description': 'Certification badge uploaded to HR profile.',
    },
    {
        'name': 'Lead 3 knowledge-sharing sessions',
        'employee_id': employee.id,
        'manager_id': employee.parent_id.id,
        'tag_ids': [(4, env.ref('my_module.goal_tag_culture').id)],
        'deadline': '2026-06-30',
        'weight': 15,
        'description': 'Internal tech talks or workshops. '
                       'Attendance tracked in training module.',
    },
]

for goal_vals in goals:
    env['hr.appraisal.goal'].create(goal_vals)

Step 3 — Track Progress and Compute Weighted Scores

Managers update goal progress as a percentage (0–100). The appraisal model computes the weighted average automatically. You can extend this with a computed field in a custom module:

Python — Computed weighted score override
from odoo import models, fields, api

class HrAppraisal(models.Model):
    _inherit = 'hr.appraisal'

    weighted_goal_score = fields.Float(
        string='Goal Score (%)',
        compute='_compute_weighted_goal_score',
        store=True,
    )

    @api.depends('goal_ids.progress', 'goal_ids.weight')
    def _compute_weighted_goal_score(self):
        for appraisal in self:
            total_weight = sum(appraisal.goal_ids.mapped('weight'))
            if total_weight <= 0:
                appraisal.weighted_goal_score = 0.0
                continue
            score = sum(
                g.progress * g.weight / 100.0
                for g in appraisal.goal_ids
            )
            # Normalize if weights don't sum to 100
            appraisal.weighted_goal_score = (
                score * 100.0 / total_weight
            )
Weight Validation

Odoo 19 does not enforce that goal weights sum to 100% out of the box. If an employee has goals weighing 40 + 25 + 20 = 85%, the remaining 15% is unaccounted for. Add a Python constraint in a custom module: @api.constrains('goal_ids') that raises ValidationError when the sum deviates from 100%. Otherwise, managers will accidentally skew scores by leaving gaps.

03

Implementing 360-Degree Feedback with Anonymous Peer Reviews in Odoo 19

360-degree feedback means the appraisal collects input from the employee (self-assessment), their manager, their direct reports, and their peers. Odoo 19 implements this through the Survey module integration — each feedback source fills out a tailored survey, and results are aggregated on the appraisal record.

Step 1 — Create Feedback Surveys

Navigate to Surveys → Surveys and create three survey templates. The appraisal module links to these via the hr_appraisal_survey bridge:

SurveyTarget AudienceAnonymityTypical Questions
Self-AssessmentThe employeeNot anonymousRate your progress on each goal; describe your biggest achievement this quarter
Manager ReviewDirect managerNot anonymousRate employee on each competency; promotion readiness; development areas
Peer Feedback2–4 selected peersAnonymousCollaboration rating; communication effectiveness; one thing to start/stop/continue

Step 2 — Configure Feedback Roles on the Appraisal

On each appraisal record, assign feedback participants. Odoo 19 adds a Feedback tab where you select employees for each role:

Python — Automate 360 feedback assignment
# Auto-assign 360 feedback participants
# Run this after appraisal generation cron
appraisals = env['hr.appraisal'].search([
    ('state', '=', 'new'),
    ('date_close', '>=', '2026-04-01'),
])

for appraisal in appraisals:
    emp = appraisal.employee_id

    # Manager is auto-assigned, add peers
    peers = env['hr.employee'].search([
        ('department_id', '=', emp.department_id.id),
        ('id', '!=', emp.id),
        ('id', '!=', emp.parent_id.id),
    ], limit=3)

    # Direct reports for upward feedback
    reports = env['hr.employee'].search([
        ('parent_id', '=', emp.id),
    ])

    appraisal.write({
        'employee_feedback_published': False,
        'manager_feedback_published': False,
    })

    # Send survey invitations
    if appraisal.survey_id and peers:
        for peer in peers:
            appraisal._create_survey_invite(
                partner=peer.work_contact_id,
                survey=appraisal.survey_id,
                deadline=appraisal.date_close,
            )

Step 3 — Enforce Anonymity for Peer Reviews

Anonymity is critical for honest peer feedback. Configure the peer survey with these settings:

  • Access Mode: set to Token — each respondent gets a unique link, but their identity is not stored on the response record.
  • Login Required: disable this — requiring login defeats anonymity since Odoo links the user to the response.
  • Scoring Type: set to No Scoring for qualitative feedback, or Scoring with Answers for quantitative ratings.
Anonymity Isn't Automatic

By default, Odoo's survey module records the partner_id of the respondent. Even with token-based access, if the peer is logged into Odoo when they click the survey link, their identity is captured. To guarantee anonymity, either use the session_anonymous survey option or send survey links that open in an incognito context. We typically customize the survey invite email to include an explicit "Open in incognito window" instruction.

04

Building Reusable Review Templates with Rating Scales and Custom Questions

Review templates standardize what gets evaluated across the organization. Instead of every manager inventing their own questions, templates ensure consistency while allowing department-level customization.

Step 1 — Create a Template via XML Data

Templates are survey records linked to the appraisal plan. Here's a complete template definition:

XML — Appraisal review template
<odoo>
  <data noupdate="1">
    <!-- Review Survey Template -->
    <record id="survey_eng_review" model="survey.survey">
      <field name="title">Engineering Performance Review Q2 2026</field>
      <field name="access_mode">token</field>
      <field name="questions_layout">page_per_section</field>
      <field name="scoring_type">scoring_with_answers</field>
    </record>

    <!-- Section: Technical Competency -->
    <record id="survey_page_technical" model="survey.question">
      <field name="survey_id" ref="survey_eng_review"/>
      <field name="title">Technical Competency</field>
      <field name="sequence">1</field>
      <field name="is_page">True</field>
    </record>

    <record id="q_code_quality" model="survey.question">
      <field name="survey_id" ref="survey_eng_review"/>
      <field name="title">Code Quality and Best Practices</field>
      <field name="sequence">2</field>
      <field name="question_type">simple_choice</field>
      <field name="constr_mandatory">True</field>
    </record>

    <!-- Rating scale answers: 1-5 -->
    <record id="q_code_quality_1" model="survey.question.answer">
      <field name="question_id" ref="q_code_quality"/>
      <field name="value">1 - Below Expectations</field>
      <field name="answer_score">1</field>
      <field name="sequence">1</field>
    </record>
    <record id="q_code_quality_3" model="survey.question.answer">
      <field name="question_id" ref="q_code_quality"/>
      <field name="value">3 - Meets Expectations</field>
      <field name="answer_score">3</field>
      <field name="sequence">3</field>
    </record>
    <record id="q_code_quality_5" model="survey.question.answer">
      <field name="question_id" ref="q_code_quality"/>
      <field name="value">5 - Exceptional</field>
      <field name="answer_score">5</field>
      <field name="sequence">5</field>
    </record>

    <!-- Section: Soft Skills -->
    <record id="survey_page_soft" model="survey.question">
      <field name="survey_id" ref="survey_eng_review"/>
      <field name="title">Collaboration &amp; Communication</field>
      <field name="sequence">10</field>
      <field name="is_page">True</field>
    </record>

    <record id="q_communication" model="survey.question">
      <field name="survey_id" ref="survey_eng_review"/>
      <field name="title">Communication Effectiveness</field>
      <field name="sequence">11</field>
      <field name="question_type">simple_choice</field>
      <field name="constr_mandatory">True</field>
    </record>

    <!-- Open-ended question -->
    <record id="q_feedback_open" model="survey.question">
      <field name="survey_id" ref="survey_eng_review"/>
      <field name="title">What should this person start, stop, or continue doing?</field>
      <field name="sequence">20</field>
      <field name="question_type">text_box</field>
      <field name="constr_mandatory">True</field>
    </record>
  </data>
</odoo>

Step 2 — Link Templates to Appraisal Plans

Once the survey template exists, reference it from your appraisal plan. This ensures every auto-generated appraisal in that department uses the correct review form:

Python — Link template to plan
# Link the survey template to the appraisal plan
plan = env['hr.appraisal.plan'].search([
    ('department_id.name', '=', 'Engineering'),
], limit=1)

survey = env.ref('my_module.survey_eng_review')
plan.write({
    'survey_template_id': survey.id,
})

# Verify: next generated appraisals will use this template
print(f"Plan '{{plan.name}}' now uses survey '{{survey.title}}'")
Template Versioning

Never edit a live survey template mid-cycle. If 50 employees are mid-review and you add a question, those 50 responses become inconsistent — some answered 10 questions, others answered 11. Instead, duplicate the template, make your changes on the copy, and assign the new version to the next cycle's appraisal plan. Keep the old template archived for audit purposes.

05

Skill Gap Analysis and Integration with Odoo 19 Training Module

The hr_appraisal_skills module connects appraisals to the employee skills framework. During a review, managers can assess the employee's current skill level versus the required level for their job position. The delta between current and required is the skill gap — and Odoo 19 can automatically suggest training courses to close it.

Step 1 — Define Skills and Skill Levels

Navigate to Employees → Configuration → Employee Skills. Define skill types, individual skills, and proficiency levels:

XML — Skill type and level definitions
<odoo>
  <data noupdate="1">
    <!-- Skill Type -->
    <record id="skill_type_technical" model="hr.skill.type">
      <field name="name">Technical Skills</field>
    </record>

    <!-- Skill Levels -->
    <record id="skill_level_beginner" model="hr.skill.level">
      <field name="skill_type_id" ref="skill_type_technical"/>
      <field name="name">Beginner</field>
      <field name="level_progress">25</field>
    </record>
    <record id="skill_level_intermediate" model="hr.skill.level">
      <field name="skill_type_id" ref="skill_type_technical"/>
      <field name="name">Intermediate</field>
      <field name="level_progress">50</field>
    </record>
    <record id="skill_level_advanced" model="hr.skill.level">
      <field name="skill_type_id" ref="skill_type_technical"/>
      <field name="name">Advanced</field>
      <field name="level_progress">75</field>
    </record>
    <record id="skill_level_expert" model="hr.skill.level">
      <field name="skill_type_id" ref="skill_type_technical"/>
      <field name="name">Expert</field>
      <field name="level_progress">100</field>
    </record>

    <!-- Individual Skills -->
    <record id="skill_python" model="hr.skill">
      <field name="skill_type_id" ref="skill_type_technical"/>
      <field name="name">Python / Odoo ORM</field>
    </record>
    <record id="skill_postgresql" model="hr.skill">
      <field name="skill_type_id" ref="skill_type_technical"/>
      <field name="name">PostgreSQL &amp; Query Optimization</field>
    </record>
    <record id="skill_owl" model="hr.skill">
      <field name="skill_type_id" ref="skill_type_technical"/>
      <field name="name">OWL 3 Frontend Framework</field>
    </record>
  </data>
</odoo>

Step 2 — Assess Skills During Appraisal

When the hr_appraisal_skills module is installed, each appraisal form includes a Skills tab. The manager sees the employee's current skill levels (from their HR profile) and can update them based on the review period's performance:

Python — Skill assessment during appraisal
# Update employee skills based on appraisal
appraisal = env['hr.appraisal'].browse(101)
employee = appraisal.employee_id

# Find the employee's Python skill record
python_skill = env['hr.employee.skill'].search([
    ('employee_id', '=', employee.id),
    ('skill_id', '=', env.ref('my_module.skill_python').id),
], limit=1)

# Upgrade from Intermediate to Advanced
if python_skill:
    python_skill.write({
        'skill_level_id': env.ref(
            'my_module.skill_level_advanced'
        ).id,
    })

# Identify skill gaps: required vs current
job = employee.job_id
required_skills = job.skill_ids  # Skills required for the role
current_skills = employee.employee_skill_ids

missing = required_skills.filtered(
    lambda s: s.id not in current_skills.mapped('skill_id').ids
)
print(f"Missing skills: {{missing.mapped('name')}}")

Step 3 — Auto-Suggest Training Based on Skill Gaps

Connect skill gaps to the Training module (hr_course) by mapping skills to course prerequisites. When a gap is identified, the system suggests relevant courses:

Python — Training recommendation engine
from odoo import models, api

class HrAppraisal(models.Model):
    _inherit = 'hr.appraisal'

    def action_suggest_training(self):
        """Suggest training courses based on skill gaps."""
        self.ensure_one()
        employee = self.employee_id
        job = employee.job_id

        # Get required skill IDs for the job position
        required = set(job.skill_ids.ids)
        # Get current skill IDs the employee has
        current = set(
            employee.employee_skill_ids.mapped('skill_id').ids
        )

        gap_skill_ids = list(required - current)

        # Also check skills below required level
        for emp_skill in employee.employee_skill_ids:
            req = job.skill_ids.filtered(
                lambda s: s.id == emp_skill.skill_id.id
            )
            if req and emp_skill.skill_level_id.level_progress <= 50:
                gap_skill_ids.append(emp_skill.skill_id.id)

        # Find courses that teach these skills
        courses = self.env['slide.channel'].search([
            ('skill_ids', 'in', gap_skill_ids),
            ('website_published', '=', True),
        ])

        return {
            'type': 'ir.actions.act_window',
            'name': 'Recommended Training',
            'res_model': 'slide.channel',
            'view_mode': 'kanban,list,form',
            'domain': [('id', 'in', courses.ids)],
        }
Skills vs. Competencies

Odoo 19 uses "skills" for hard/technical abilities and does not have a separate "competency" model for soft skills. If your framework distinguishes between the two, create separate skill types — one for "Technical Skills" and one for "Competencies." This keeps the HR profile clean and allows you to filter assessments by type during reviews.

06

4 Appraisal Configuration Mistakes That Undermine Your Entire Review Cycle

1

Running 360 Feedback Without Enforcing Anonymity at the Survey Level

We've seen companies set up peer feedback surveys, send them out, and then discover that managers can see exactly who wrote each response by clicking into the survey answers. The peer feedback tab on the appraisal form shows aggregated scores, but the underlying survey.user_input records contain the respondent's partner_id. Any manager with read access to surveys can trace feedback to its author.

Our Fix

Create a dedicated survey access group for peer feedback. Remove survey.user_input read access for managers. Build a custom report that aggregates scores without exposing individual responses. Alternatively, use the session_anonymous flag on the survey and distribute links via a non-Odoo email to break the traceability chain.

2

Goal Weights That Don't Sum to 100% Across the Team

Manager A creates goals with weights summing to 100%. Manager B creates goals summing to 75%. Manager C doesn't set weights at all (they default to 0). The quarterly review meeting arrives, and the weighted scores are meaningless — you can't compare employees whose scoring denominators are different. Calibration sessions turn into arguments about methodology instead of performance.

Our Fix

Add a validation constraint on the appraisal model that blocks state transitions (new → pending) unless goal weights sum to exactly 100%. Combine this with a scheduled action that emails managers whose employees have incomplete goal weights 7 days before the appraisal deadline.

3

Editing Survey Templates Mid-Cycle

HR decides to add a "remote work effectiveness" question halfway through Q2 reviews. They edit the live survey template. Now 30% of responses have 10 questions and 70% have 11 questions. The aggregate report averages across different denominators, and the new question's data is statistically meaningless because it only covers a subset of employees.

Our Fix

Lock survey templates once the first response is submitted by overriding the write() method on survey.survey. If user_input_count > 0, block structural changes (adding/removing questions). Allow only cosmetic edits like fixing typos in question text.

4

Skill Assessments Disconnected from Actual Training Enrollment

The manager identifies a skill gap during the appraisal, notes "needs PostgreSQL training" in the comments, and closes the review. Three months later, the next appraisal reveals the same skill gap because nobody enrolled the employee in training. The feedback loop between appraisals and development is broken because there's no automated handoff.

Our Fix

Add an "Action Items" one2many on the appraisal model. Each action item has a type (Training, Mentoring, Project Assignment), an assignee, and a deadline. A scheduled action checks for overdue items weekly and escalates to the manager's manager. For training specifically, the action item links directly to a slide.channel enrollment record.

BUSINESS ROI

What Structured Appraisals Save Your Organization

Performance management isn't an HR checkbox — it's the mechanism that aligns individual effort with company objectives. Here's what changes when you move from spreadsheet reviews to Odoo 19's integrated system:

40%Faster Review Cycles

Automated appraisal generation, pre-filled templates, and integrated surveys eliminate the manual setup that consumed 3+ HR hours per cycle.

2.3xMore Actionable Feedback

360-degree input from 4 sources per employee replaces the single-manager bottleneck. Peer feedback surfaces blind spots that top-down reviews miss.

60%Higher Goal Completion

Weighted goals with progress tracking and deadline visibility increase accountability. Employees see exactly what's expected and how they're tracking.

25%Reduced Voluntary Turnover

Employees who receive regular, structured feedback with clear development paths are significantly less likely to leave. The cost of replacing a senior developer exceeds $150K.

The hidden ROI is calibration consistency. When every department uses the same review template, the same rating scale, and the same goal-weighting framework, you can finally compare performance across teams without the "but my manager grades harder" problem. This makes promotion decisions defensible and compensation adjustments data-driven.

SEO NOTES

Optimization Metadata

Meta Desc

Configure employee appraisals in Odoo 19: recurring plans, weighted goal tracking, 360-degree peer feedback, review templates, and skill gap analysis with training integration.

H2 Keywords

1. "How to Configure Appraisal Plans with Recurring Schedules in Odoo 19"
2. "Setting Up Goal Management with Weighted Objectives and Progress Tracking"
3. "Implementing 360-Degree Feedback with Anonymous Peer Reviews in Odoo 19"
4. "Building Reusable Review Templates with Rating Scales and Custom Questions"
5. "Skill Gap Analysis and Integration with Odoo 19 Training Module"

Stop Running Reviews in Spreadsheets

Every performance review that lives in a shared Google Sheet is a review with no audit trail, no skill tracking, no automated follow-up, and no connection to training or promotion decisions. It works when you have 10 employees. At 50+, it becomes a compliance risk and a retention liability.

If you're running Odoo 19 and still managing appraisals outside the system, we can help. We configure appraisal plans, build custom review templates, set up 360-degree feedback workflows, and integrate skill assessments with training enrollment — all within your existing Odoo instance. The system pays for itself the first time a structured review prevents a wrongful termination claim.

Book a Free HR Assessment