add hr_timesheet_sheet_no_review, hr_timesheet_sheet_holidays, hr_timesheet_sheet_delete_cascade

This commit is contained in:
Luis 2026-09-22 10:58:36 +02:00
parent b07082f70d
commit 8b0d0bb6cc
53 changed files with 3953 additions and 0 deletions

View file

@ -0,0 +1 @@
from . import test_hr_timesheet_sheet_no_review

View file

@ -0,0 +1,358 @@
# Copyright 2026 Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import os
from unittest.mock import patch
from lxml import etree
from odoo.exceptions import AccessError, UserError
from odoo.modules.module import get_manifest, get_module_path
from odoo.tests.common import TransactionCase
MODULE = "hr_timesheet_sheet_no_review"
REVIEW_ACTIONS = [
"action_timesheet_confirm",
"action_timesheet_done",
"action_timesheet_draft",
"action_timesheet_refuse",
]
REVIEW_MENUS = [
"hr_timesheet_sheet.menu_hr_to_review",
"hr_timesheet_sheet.menu_act_hr_timesheet_sheet_to_review",
"hr_timesheet_sheet.menu_act_hr_timesheet_line_to_submit",
"hr_timesheet_sheet.menu_act_hr_timesheet_line_to_submit_my",
]
class TestHrTimesheetSheetNoReview(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.sheet_model = cls.env["hr_timesheet.sheet"]
cls.line_model = cls.env["account.analytic.line"]
cls.wizard_model = cls.env["hr_timesheet.sheet.unfreeze.wizard"]
cls.company = cls.env["res.company"].create({"name": "No Review Company"})
cls.env.user.company_ids += cls.company
cls.user = (
cls.env["res.users"]
.with_context(no_reset_password=True, default_role_line_ids=[])
.create(
{
"name": "No Review Employee",
"login": "no_review_employee",
"email": "no_review_employee@example.org",
"company_id": cls.company.id,
"company_ids": [(6, 0, [cls.company.id])],
"groups_id": [
(
6,
0,
[
cls.env.ref("hr_timesheet.group_hr_timesheet_user").id,
cls.env.ref("project.group_project_user").id,
],
)
],
}
)
)
cls.manager = (
cls.env["res.users"]
.with_context(no_reset_password=True, default_role_line_ids=[])
.create(
{
"name": "No Review Manager",
"login": "no_review_manager",
"email": "no_review_manager@example.org",
"company_id": cls.company.id,
"company_ids": [(6, 0, [cls.company.id])],
"groups_id": [
(
6,
0,
[
cls.env.ref("hr_timesheet.group_timesheet_manager").id,
cls.env.ref("project.group_project_manager").id,
],
)
],
}
)
)
cls.reviewer = (
cls.env["res.users"]
.with_context(no_reset_password=True, default_role_line_ids=[])
.create(
{
"name": "No Review Reviewer",
"login": "no_review_reviewer",
"email": "no_review_reviewer@example.org",
"company_id": cls.company.id,
"company_ids": [(6, 0, [cls.company.id])],
"groups_id": [
(
6,
0,
[
cls.env.ref("hr.group_hr_user").id,
cls.env.ref("hr_timesheet.group_hr_timesheet_user").id,
cls.env.ref("project.group_project_user").id,
],
)
],
}
)
)
cls.employee = cls.env["hr.employee"].create(
{
"name": "No Review Employee",
"user_id": cls.user.id,
"company_id": cls.company.id,
}
)
cls.env["hr.employee"].create(
{
"name": "No Review Reviewer",
"user_id": cls.reviewer.id,
"company_id": cls.company.id,
}
)
cls.project = cls.env["project.project"].create(
{
"name": "No Review Project",
"company_id": cls.company.id,
"allow_timesheets": True,
}
)
cls.task = cls.env["project.task"].create(
{
"name": "No Review Task",
"project_id": cls.project.id,
"company_id": cls.company.id,
}
)
cls.baseline = cls._frozen_figures(cls.env)
cls.sheet = cls.sheet_model.create(
{"employee_id": cls.employee.id, "company_id": cls.company.id}
)
cls.line = cls.line_model.create(
{
"name": "Frozen entry",
"date": cls.sheet.date_start,
"project_id": cls.project.id,
"task_id": cls.task.id,
"employee_id": cls.employee.id,
"company_id": cls.company.id,
"unit_amount": 3.0,
}
)
cls.sheet.action_timesheet_confirm()
@staticmethod
def _frozen_figures(env):
sheet_model = env["hr_timesheet.sheet"]
sheets = sheet_model.search(sheet_model._unfreeze_domain())
groups = env["account.analytic.line"].read_group(
[("sheet_id", "in", sheets.ids)], ["unit_amount"], []
)
return {
"sheets": len(sheets),
"timesheets": groups[0]["__count"] if groups else 0,
"hours": (groups[0]["unit_amount"] if groups else 0.0) or 0.0,
}
def _arch(self, xml_id, view_type, model="hr_timesheet.sheet"):
view = self.env.ref(xml_id)
return etree.fromstring(
self.env[model].get_view(view_id=view.id, view_type=view_type)["arch"]
)
# RF-01
def test_review_buttons_are_hidden(self):
for xml_id in (
"hr_timesheet_sheet.hr_timesheet_sheet_form",
"hr_timesheet_sheet.hr_timesheet_sheet_form_my",
):
arch = self._arch(xml_id, "form")
for action in REVIEW_ACTIONS:
self.assertFalse(
arch.xpath("//button[@name='%s']" % action),
"%s still exposes %s" % (xml_id, action),
)
state = arch.xpath("//header/field[@name='state']")
self.assertTrue(state)
self.assertEqual(state[0].get("statusbar_visible"), "draft")
def test_review_buttons_are_hidden_in_kanban(self):
arch = self._arch("hr_timesheet_sheet.view_hr_timesheet_sheet_kanban", "kanban")
for action in REVIEW_ACTIONS:
self.assertFalse(arch.xpath("//a[@name='%s']" % action))
def test_reviewer_is_hidden_in_lists(self):
for xml_id in (
"hr_timesheet_sheet.hr_timesheet_sheet_tree",
"hr_timesheet_sheet.hr_timesheet_sheet_tree_my",
):
arch = self._arch(xml_id, "tree")
self.assertFalse(arch.xpath("//field[@name='reviewer_id']"))
self.assertIsNone(arch.xpath("//tree")[0].get("decoration-muted"))
def test_review_filters_are_hidden_in_search(self):
arch = self._arch("hr_timesheet_sheet.view_hr_timesheet_sheet_filter", "search")
self.assertFalse(arch.xpath("//filter[@name='confirm']"))
self.assertFalse(arch.xpath("//filter[@name='to_review']"))
self.assertFalse(arch.xpath("//search/field[@name='reviewer_id']"))
self.assertFalse(arch.xpath("//filter[@name='groupby_reviewer']"))
def test_submission_filter_is_hidden_in_timesheet_search(self):
arch = self._arch(
"hr_timesheet.hr_timesheet_line_search",
"search",
model="account.analytic.line",
)
self.assertFalse(arch.xpath("//filter[@name='unsubmitted']"))
self.assertFalse(
arch.xpath("//search/separator[not(preceding-sibling::filter)]"),
"removing the filter must not leave its separator behind",
)
def test_department_kanban_has_no_review_counter(self):
arch = self._arch(
"hr.hr_department_view_kanban", "kanban", model="hr.department"
)
rendered = etree.tostring(arch).decode()
self.assertNotIn("timesheet_sheet_to_approve_count", rendered)
action = self.env.ref("hr_timesheet_sheet.hr_timesheet_action_from_department")
self.assertFalse(arch.xpath("//a[@name='%s']" % action.id))
def test_review_menus_are_inactive(self):
for xml_id in REVIEW_MENUS:
self.assertFalse(self.env.ref(xml_id).active, xml_id)
def test_review_methods_are_kept(self):
"""The workflow is gone from the interface but still works from code.
The rest of the family (``..._policy_*``, ``..._autodraft``,
``..._attendance``) calls these methods, so they must stay functional.
"""
sheet = self.sheet.with_user(self.reviewer)
self.assertEqual(sheet.state, "confirm")
sheet.action_timesheet_done()
self.assertEqual(sheet.state, "done")
self.assertEqual(sheet.reviewer_id.user_id, self.reviewer)
sheet.action_timesheet_draft()
self.assertEqual(sheet.state, "draft")
self.assertFalse(sheet.reviewer_id)
self.sheet.with_user(self.user).action_timesheet_confirm()
self.assertEqual(sheet.state, "confirm")
sheet.action_timesheet_refuse()
self.assertEqual(sheet.state, "draft")
self.assertFalse(sheet.reviewer_id)
# RF-02
def test_install_does_not_change_sheet_states(self):
manifest = get_manifest(MODULE)
for key in ("pre_init_hook", "post_init_hook", "uninstall_hook", "post_load"):
self.assertFalse(
manifest.get(key), "%s must not declare a %s" % (MODULE, key)
)
module_path = get_module_path(MODULE)
for data_file in manifest["data"]:
with open(os.path.join(module_path, data_file)) as handle:
self.assertNotRegex(
handle.read(),
r"<\s*function",
"%s must not call a method on install" % data_file,
)
records = self.env["ir.model.data"].search([("module", "=", MODULE)])
self.assertTrue(records)
business_models = records.filtered(
lambda record: not record.model.startswith("ir.")
)
self.assertFalse(
business_models.mapped("model"),
"%s must only install technical records" % MODULE,
)
def test_wizard_reports_pending_sheets(self):
wizard = self.wizard_model.create({})
self.assertEqual(wizard.sheet_count, self.baseline["sheets"] + 1)
self.assertEqual(wizard.timesheet_count, self.baseline["timesheets"] + 1)
self.assertAlmostEqual(
wizard.timesheet_hours, self.baseline["hours"] + 3.0, places=2
)
self.assertNotIn("Nothing to unfreeze", wizard.summary_html)
def test_wizard_reports_nothing_to_unfreeze(self):
self.sheet_model._unfreeze_sheets()
wizard = self.wizard_model.create({})
self.assertEqual(wizard.sheet_count, 0)
self.assertEqual(wizard.timesheet_count, 0)
self.assertEqual(wizard.timesheet_hours, 0.0)
self.assertIn("Nothing to unfreeze", wizard.summary_html)
def test_unfreeze_releases_sheets(self):
self.assertEqual(self.sheet.state, "confirm")
wizard = self.wizard_model.create({})
expected = wizard.sheet_count
result = wizard.action_unfreeze()
self.assertEqual(self.sheet.state, "draft")
self.assertFalse(self.sheet.reviewer_id)
self.assertEqual(
self.sheet_model.search_count([("state", "in", ("confirm", "done"))]), 0
)
self.assertIn(str(expected), result["params"]["message"])
def test_unfreeze_is_idempotent(self):
self.assertTrue(self.sheet_model._unfreeze_sheets())
self.assertEqual(self.sheet_model._unfreeze_sheets(), 0)
wizard = self.wizard_model.create({})
result = wizard.action_unfreeze()
self.assertEqual(
self.sheet_model.search_count([("state", "in", ("confirm", "done"))]), 0
)
self.assertEqual(result["params"]["type"], "info")
self.assertEqual(self.sheet.state, "draft")
def test_unfrozen_lines_are_editable(self):
with self.assertRaises(UserError):
self.line.with_user(self.user).write({"unit_amount": 5.0})
self.wizard_model.create({}).action_unfreeze()
self.line.with_user(self.user).write({"unit_amount": 5.0})
self.assertEqual(self.line.unit_amount, 5.0)
def test_unfreeze_domain_hook(self):
self.assertEqual(
self.sheet_model._unfreeze_domain(),
[("state", "in", ("confirm", "done"))],
)
kept_id = self.sheet.id
with patch.object(
type(self.sheet_model),
"_unfreeze_domain",
lambda records: [
("state", "in", ("confirm", "done")),
("id", "!=", kept_id),
],
):
self.sheet_model._unfreeze_sheets()
self.assertEqual(self.sheet.state, "confirm")
self.assertEqual(
self.sheet_model.search_count([("state", "in", ("confirm", "done"))]), 1
)
def test_wizard_is_restricted_to_managers(self):
with self.assertRaises(AccessError):
self.wizard_model.with_user(self.user).create({})
self.assertTrue(self.wizard_model.with_user(self.manager).create({}))