add hr_timesheet_sheet_no_review, hr_timesheet_sheet_holidays, hr_timesheet_sheet_delete_cascade
This commit is contained in:
parent
b07082f70d
commit
8b0d0bb6cc
53 changed files with 3953 additions and 0 deletions
1
hr_timesheet_sheet_no_review/tests/__init__.py
Normal file
1
hr_timesheet_sheet_no_review/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_hr_timesheet_sheet_no_review
|
||||
|
|
@ -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({}))
|
||||
Loading…
Add table
Add a link
Reference in a new issue