[MIG] hr_luncheon_voucher: migrate to 18.0

This commit is contained in:
Stéphan Sainléger
2026-05-30 00:34:22 +02:00
parent e4cc346dfe
commit cc719291eb
24 changed files with 1857 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
from . import test_hr_employee
from . import test_hr_lv_allocation
from . import test_generate_lv_allocations_wizard

View File

@@ -0,0 +1,112 @@
from odoo.tests.common import TransactionCase
class TestGenerateLvAllocationsWizard(TransactionCase):
"""Tests for generate.lv.allocation.requests wizard."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.employee_admin = cls.env.ref("hr.employee_admin")
cls.employee_other = cls.env["hr.employee"].create(
{
"name": "Other Employee",
"resource_calendar_id": cls.employee_admin.resource_calendar_id.id,
}
)
# ------------------------------------------------------------------
# generate_lv_allocations
# ------------------------------------------------------------------
def test_generate_for_single_employee(self):
"""Wizard creates one allocation for the selected employee."""
wizard = self._create_wizard()
# Simulate single employee selection
ctx = {"active_ids": [self.employee_admin.id]}
wizard = wizard.with_context(**ctx)
wizard.generate_lv_allocations()
allocations = self.env["hr.lv.allocation"].search(
[("distrib_campaign_name", "=", "March 2026")]
)
self.assertEqual(len(allocations), 1)
self.assertEqual(allocations.employee_id, self.employee_admin)
def test_generate_for_multiple_employees(self):
"""Wizard creates one allocation per selected employee."""
wizard = self._create_wizard()
ctx = {
"active_ids": [
self.employee_admin.id,
self.employee_other.id,
]
}
wizard = wizard.with_context(**ctx)
wizard.generate_lv_allocations()
allocations = self.env["hr.lv.allocation"].search(
[("distrib_campaign_name", "=", "March 2026")]
)
self.assertEqual(len(allocations), 2)
def test_generate_allocation_name_format(self):
"""Allocation name follows 'campaign - employee' pattern."""
wizard = self._create_wizard()
ctx = {"active_ids": [self.employee_admin.id]}
wizard = wizard.with_context(**ctx)
wizard.generate_lv_allocations()
alloc = self.env["hr.lv.allocation"].search(
[("distrib_campaign_name", "=", "March 2026")]
)
expected_name = f"March 2026 - {self.employee_admin.name}"
self.assertEqual(alloc.name, expected_name)
def test_generate_allocations_have_dates(self):
"""Allocations carry the wizard's date range."""
wizard = self._create_wizard()
ctx = {"active_ids": [self.employee_admin.id]}
wizard = wizard.with_context(**ctx)
wizard.generate_lv_allocations()
alloc = self.env["hr.lv.allocation"].search(
[("distrib_campaign_name", "=", "March 2026")]
)
self.assertEqual(alloc.date_from, wizard.date_from)
self.assertEqual(alloc.date_to, wizard.date_to)
def test_generate_allocations_start_in_draft(self):
"""New allocations start in 'draft' state."""
wizard = self._create_wizard()
ctx = {"active_ids": [self.employee_admin.id]}
wizard = wizard.with_context(**ctx)
wizard.generate_lv_allocations()
alloc = self.env["hr.lv.allocation"].search(
[("distrib_campaign_name", "=", "March 2026")]
)
self.assertEqual(alloc.state, "draft")
def test_generate_returns_action(self):
"""The wizard returns an action dict (act_window)."""
wizard = self._create_wizard()
ctx = {"active_ids": [self.employee_admin.id]}
wizard = wizard.with_context(**ctx)
action = wizard.generate_lv_allocations()
self.assertIn("type", action)
self.assertEqual(action["type"], "ir.actions.act_window")
self.assertEqual(action["res_model"], "hr.lv.allocation")
# ------------------------------------------------------------------
# helpers
# ------------------------------------------------------------------
def _create_wizard(self):
return self.env["generate.lv.allocation.requests"].create(
{
"distrib_campaign_name": "March 2026",
"date_from": "2026-03-01",
"date_to": "2026-03-31",
}
)

View File

@@ -0,0 +1,169 @@
from odoo.tests.common import TransactionCase
class TestHrEmployee(TransactionCase):
"""Tests for hr.employee extensions (_compute*, generate_lv_allocation)."""
@classmethod
def setUpClass(cls):
super().setUpClass()
# Use a demo employee (created by hr module)
cls.employee = cls.env.ref("hr.employee_admin")
# Ensure a clean slate
cls.employee.write(
{
"total_acquired_lv": 0,
"distributed_lv": 0,
"dued_lv": 0,
}
)
# ------------------------------------------------------------------
# _compute_total_acquired_lv
# ------------------------------------------------------------------
def test_compute_total_acquired_lv_no_allocations(self):
"""No allocations → total_acquired_lv is 0."""
self.employee.refresh_lv_values()
self.assertEqual(self.employee.total_acquired_lv, 0)
def test_compute_total_acquired_lv_with_allocations(self):
"""Confirmed + distributed allocations are summed."""
self._create_allocation(state="confirmed", number_acquired_lv=5)
self._create_allocation(state="distributed", number_acquired_lv=3)
self.employee.refresh_lv_values()
self.assertEqual(self.employee.total_acquired_lv, 8)
def test_compute_total_acquired_lv_excludes_draft(self):
"""Draft allocations are NOT counted."""
self._create_allocation(state="draft", number_acquired_lv=10)
self.employee.refresh_lv_values()
self.assertEqual(self.employee.total_acquired_lv, 0)
# ------------------------------------------------------------------
# _compute_distributed_lv
# ------------------------------------------------------------------
def test_compute_distributed_lv_no_allocations(self):
"""No allocations → distributed_lv is 0."""
self.employee.refresh_lv_values()
self.assertEqual(self.employee.distributed_lv, 0)
def test_compute_distributed_lv_only_distributed(self):
"""Only 'distributed' state allocations are counted."""
self._create_allocation(state="distributed", number_distributed_lv=4)
self._create_allocation(state="confirmed", number_distributed_lv=2)
self._create_allocation(state="draft", number_distributed_lv=1)
self.employee.refresh_lv_values()
self.assertEqual(self.employee.distributed_lv, 4)
def test_compute_distributed_lv_multiple_distributed(self):
"""Multiple distributed allocations are summed."""
self._create_allocation(state="distributed", number_distributed_lv=3)
self._create_allocation(state="distributed", number_distributed_lv=7)
self.employee.refresh_lv_values()
self.assertEqual(self.employee.distributed_lv, 10)
# ------------------------------------------------------------------
# _compute_dued_lv
# ------------------------------------------------------------------
def test_compute_dued_lv(self):
"""dued_lv = total_acquired_lv - distributed_lv."""
self.employee.write(
{"total_acquired_lv": 20, "distributed_lv": 6}
)
self.employee._compute_dued_lv()
self.assertEqual(self.employee.dued_lv, 14)
def test_compute_dued_lv_zero_when_equal(self):
"""dued_lv is 0 when acquired equals distributed."""
self.employee.write(
{"total_acquired_lv": 10, "distributed_lv": 10}
)
self.employee._compute_dued_lv()
self.assertEqual(self.employee.dued_lv, 0)
# ------------------------------------------------------------------
# generate_lv_allocation
# ------------------------------------------------------------------
def test_generate_lv_allocation_creates_record(self):
"""generate_lv_allocation creates an hr.lv.allocation with correct values."""
campaign_name = "Test Campaign"
values = {
"distrib_campaign_name": campaign_name,
"date_from": "2026-01-01",
"date_to": "2026-01-31",
}
self.employee.generate_lv_allocation(values)
alloc = self.env["hr.lv.allocation"].search(
[
("employee_id", "=", self.employee.id),
("distrib_campaign_name", "=", campaign_name),
]
)
self.assertTrue(alloc)
self.assertEqual(len(alloc), 1)
self.assertEqual(alloc.distrib_campaign_name, campaign_name)
self.assertEqual(alloc.employee_id, self.employee)
self.assertIn(self.employee.name, alloc.name)
def test_generate_lv_allocation_sets_employee_id(self):
"""The passed employee_id in values is overridden by the record's own id."""
other = self.env["hr.employee"].create(
{
"name": "Other Employee",
"resource_calendar_id": self.employee.resource_calendar_id.id,
}
)
values = {
"distrib_campaign_name": "Campaign Override",
"employee_id": other.id,
"date_from": "2026-02-01",
"date_to": "2026-02-28",
}
self.employee.generate_lv_allocation(values)
alloc = self.env["hr.lv.allocation"].search(
[("distrib_campaign_name", "=", "Campaign Override")]
)
self.assertEqual(alloc.employee_id, self.employee)
def test_generate_lv_allocation_updates_counters(self):
"""After creation, acquired and due are computed (triggers create logic)."""
values = {
"distrib_campaign_name": "Counters Check",
"date_from": "2026-03-01",
"date_to": "2026-03-31",
}
self.employee.generate_lv_allocation(values)
alloc = self.env["hr.lv.allocation"].search(
[("distrib_campaign_name", "=", "Counters Check")]
)
# number_acquired_lv should be computed (may be 0 on non-working days)
self.assertIsNotNone(alloc.number_acquired_lv)
self.assertIsNotNone(alloc.number_dued_lv)
# ------------------------------------------------------------------
# helpers
# ------------------------------------------------------------------
def _create_allocation(self, **kwargs):
# Extract numeric fields that create() overrides, to set them after creation
numeric_fields = {
k: kwargs.pop(k)
for k in ["number_acquired_lv", "number_dued_lv", "number_distributed_lv"]
if k in kwargs
}
defaults = {
"employee_id": self.employee.id,
"distrib_campaign_name": "Test",
"date_from": "2026-01-01",
"date_to": "2026-01-31",
"name": "Test - Employee",
}
defaults.update(kwargs)
alloc = self.env["hr.lv.allocation"].create(defaults)
if numeric_fields:
alloc.write(numeric_fields)
return alloc

View File

@@ -0,0 +1,317 @@
from odoo.tests.common import TransactionCase
class TestHrLvAllocation(TransactionCase):
"""Tests for hr.lv.allocation model methods."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.employee = cls.env.ref("hr.employee_admin")
cls.calendar = cls.env.ref("resource.resource_calendar_std")
cls.employee.resource_calendar_id = cls.calendar
# Ensure the calendar has attendances marked as effective periods
# (required by hr_effective_attendance_period for is_working_day etc.)
Attendance = cls.env["resource.calendar.attendance"]
existing = Attendance.search([("calendar_id", "=", cls.calendar.id)])
if existing:
existing.write({"effective_attendance_period": True})
else:
for day_idx, day_name in enumerate(
["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
):
Attendance.create(
{
"calendar_id": cls.calendar.id,
"name": f"{day_name} Morning",
"dayofweek": str(day_idx),
"day_period": "morning",
"hour_from": 8,
"hour_to": 12,
"effective_attendance_period": True,
}
)
Attendance.create(
{
"calendar_id": cls.calendar.id,
"name": f"{day_name} Afternoon",
"dayofweek": str(day_idx),
"day_period": "afternoon",
"hour_from": 13,
"hour_to": 17,
"effective_attendance_period": True,
}
)
cls.allocation = cls._build_allocation(cls)
# Ensure a cancelling event type exists
cls.cancelling_categ = cls.env.ref(
"hr_luncheon_voucher.categ_meet_free_lunch",
raise_if_not_found=False,
)
if not cls.cancelling_categ:
cls.cancelling_categ = cls.env["calendar.event.type"].create(
{"name": "Free Lunch Test", "remove_luncheon_voucher": True}
)
else:
cls.cancelling_categ.remove_luncheon_voucher = True
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _build_allocation(self, **overrides):
vals = {
"employee_id": self.employee.id,
"distrib_campaign_name": "Test Campaign",
"date_from": "2026-01-01",
"date_to": "2026-01-31",
"name": "Test Campaign - Admin",
}
vals.update(overrides)
return self.env["hr.lv.allocation"].create(vals)
# ------------------------------------------------------------------
# _compute_lv_balance
# ------------------------------------------------------------------
def test_lv_balance_positive(self):
"""Balance = number_dued_lv - number_distributed_lv (positive)."""
alloc = self._build_allocation()
alloc.write(
{"number_dued_lv": 10, "number_distributed_lv": 3}
)
alloc._compute_lv_balance()
self.assertEqual(alloc.lv_balance, 7)
def test_lv_balance_zero(self):
"""Balance is 0 when due equals distributed."""
alloc = self._build_allocation()
alloc.write(
{"number_dued_lv": 5, "number_distributed_lv": 5}
)
alloc._compute_lv_balance()
self.assertEqual(alloc.lv_balance, 0)
def test_lv_balance_negative(self):
"""Balance can be negative (over-distribution)."""
alloc = self._build_allocation()
alloc.write(
{"number_dued_lv": 2, "number_distributed_lv": 5}
)
alloc._compute_lv_balance()
self.assertEqual(alloc.lv_balance, -3)
# ------------------------------------------------------------------
# _has_cancelling_voucher_event
# ------------------------------------------------------------------
def test_has_cancelling_event_true(self):
"""Returns True when a cancelling event exists for the employee."""
partner = self.employee.user_id.partner_id
self.env["calendar.event"].create(
{
"name": "Team Lunch",
"start": "2026-01-15 12:00:00",
"stop": "2026-01-15 14:00:00",
"categ_ids": [(6, 0, self.cancelling_categ.ids)],
"partner_ids": [(4, partner.id)],
}
)
day = self.env.cr.now().replace(day=15, month=1, year=2026)
self.assertTrue(self.allocation._has_cancelling_voucher_event(day))
def test_has_cancelling_event_false_no_event(self):
"""Returns False when no cancelling event exists."""
day = self.env.cr.now().replace(day=10, month=1, year=2026)
self.assertFalse(self.allocation._has_cancelling_voucher_event(day))
def test_has_cancelling_event_false_non_cancelling_category(self):
"""Returns False when events exist but without the cancelling flag."""
categ = self.env["calendar.event.type"].create(
{"name": "Regular Meeting", "remove_luncheon_voucher": False}
)
partner = self.employee.user_id.partner_id
self.env["calendar.event"].create(
{
"name": "Stand-up",
"start": "2026-01-15 09:00:00",
"stop": "2026-01-15 09:30:00",
"categ_ids": [(6, 0, categ.ids)],
"partner_ids": [(4, partner.id)],
}
)
day = self.env.cr.now().replace(day=15, month=1, year=2026)
self.assertFalse(self.allocation._has_cancelling_voucher_event(day))
def test_has_cancelling_event_outside_day(self):
"""Returns False when the event is entirely outside the target day."""
partner = self.employee.user_id.partner_id
self.env["calendar.event"].create(
{
"name": "Late Dinner",
"start": "2026-01-15 22:00:00",
"stop": "2026-01-16 00:30:00",
"categ_ids": [(6, 0, self.cancelling_categ.ids)],
"partner_ids": [(4, partner.id)],
}
)
# Check a different day
day = self.env.cr.now().replace(day=14, month=1, year=2026)
self.assertFalse(self.allocation._has_cancelling_voucher_event(day))
# ------------------------------------------------------------------
# _calculate_number_acquired_lv
# ------------------------------------------------------------------
def test_calculate_acquired_on_non_working_day(self):
"""Acquired is 0 for a period of non-working days (e.g. weekend-only)."""
alloc = self._build_allocation(
date_from="2026-01-03", # Saturday
date_to="2026-01-04", # Sunday
)
alloc._calculate_number_acquired_lv()
self.assertEqual(alloc.number_acquired_lv, 0)
def test_calculate_acquired_basic(self):
"""Acquired is > 0 for a normal working period (employee works)."""
alloc = self._build_allocation(
date_from="2026-01-05", # Monday
date_to="2026-01-09", # Friday
)
alloc._calculate_number_acquired_lv()
# Standard 5-day calendar: 5 working days, no leaves, no events
self.assertEqual(alloc.number_acquired_lv, 5)
def test_calculate_acquired_with_leave(self):
"""Acquired excludes days covered by leave."""
# Create a leave covering Wednesday 2026-01-07
self.env["resource.calendar.leaves"].create(
{
"name": "Sick leave",
"date_from": "2026-01-07 00:00:00",
"date_to": "2026-01-07 23:59:59",
"calendar_id": self.calendar.id,
"resource_id": self.employee.resource_id.id,
}
)
alloc = self._build_allocation(
date_from="2026-01-05", # Monday
date_to="2026-01-09", # Friday
)
alloc._calculate_number_acquired_lv()
# 5 working days - 1 leave = 4
self.assertEqual(alloc.number_acquired_lv, 4)
def test_calculate_acquired_with_half_day_cancel(self):
"""When hr_half_day_cancels_voucher is enabled, partial days are excluded."""
self.env.company.hr_half_day_cancels_voucher = True
# Modify calendar to have only morning attendance on Wednesday
wed_attendance = self.env["resource.calendar.attendance"].search(
[
("calendar_id", "=", self.calendar.id),
("dayofweek", "=", "2"), # Wednesday
("day_period", "=", "afternoon"),
]
)
wed_attendance.unlink()
alloc = self._build_allocation(
date_from="2026-01-05", # Monday
date_to="2026-01-09", # Friday
)
alloc._calculate_number_acquired_lv()
# 4 full days + 1 half-day excluded = 4
self.assertEqual(alloc.number_acquired_lv, 4)
# ------------------------------------------------------------------
# _calculate_number_dued_lv
# ------------------------------------------------------------------
def test_calculate_dued_draft(self):
"""In non-distributed state: dued = employee.dued_lv + number_acquired_lv."""
self.employee.write({"dued_lv": 5})
alloc = self._build_allocation(state="draft")
alloc.number_acquired_lv = 10
alloc._calculate_number_dued_lv()
self.assertEqual(alloc.number_dued_lv, 15)
def test_calculate_dued_distributed(self):
"""In distributed: dued = employee.dued_lv only (no addition)."""
self.employee.write({"dued_lv": 8})
alloc = self._build_allocation(state="distributed")
alloc.number_acquired_lv = 20
alloc._calculate_number_dued_lv()
self.assertEqual(alloc.number_dued_lv, 8)
# ------------------------------------------------------------------
# confirm_allocation
# ------------------------------------------------------------------
def test_confirm_draft(self):
"""confirm_allocation transitions draft → confirmed and updates counters."""
self.employee.write({"total_acquired_lv": 0, "dued_lv": 0})
alloc = self._build_allocation(state="draft")
alloc.number_acquired_lv = 6
alloc.confirm_allocation()
self.assertEqual(alloc.state, "confirmed")
self.employee.refresh_lv_values()
self.assertEqual(self.employee.total_acquired_lv, 6)
def test_confirm_already_confirmed_does_not_change(self):
"""confirm_allocation on a confirmed record is a no-op."""
alloc = self._build_allocation(state="confirmed")
alloc.confirm_allocation()
self.assertEqual(alloc.state, "confirmed")
# ------------------------------------------------------------------
# back_to_draft
# ------------------------------------------------------------------
def test_back_to_draft_from_confirmed(self):
"""back_to_draft transitions confirmed → draft."""
alloc = self._build_allocation(state="confirmed")
alloc.back_to_draft()
self.assertEqual(alloc.state, "draft")
def test_back_to_draft_from_distributed(self):
"""back_to_draft transitions distributed → draft."""
alloc = self._build_allocation(state="distributed")
alloc.back_to_draft()
self.assertEqual(alloc.state, "draft")
def test_back_to_draft_draft_is_noop(self):
"""back_to_draft on a draft record is a no-op."""
alloc = self._build_allocation(state="draft")
alloc.back_to_draft()
self.assertEqual(alloc.state, "draft")
# ------------------------------------------------------------------
# distribute_allocation
# ------------------------------------------------------------------
def test_distribute_confirmed(self):
"""distribute_allocation transitions confirmed → distributed."""
alloc = self._build_allocation(state="confirmed")
alloc.distribute_allocation()
self.assertEqual(alloc.state, "distributed")
def test_distribute_draft_is_noop(self):
"""distribute_allocation on draft is a no-op."""
alloc = self._build_allocation(state="draft")
alloc.distribute_allocation()
self.assertEqual(alloc.state, "draft")
# ------------------------------------------------------------------
# adjust_distribution
# ------------------------------------------------------------------
def test_adjust_distribution(self):
"""Adjust distribution sets number_distributed_lv = dued + acquired."""
alloc = self._build_allocation(state="draft")
alloc.number_acquired_lv = 5
alloc.employee_id.dued_lv = 3
alloc.adjust_distribution()
self.assertEqual(alloc.number_distributed_lv, 8)