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)