hr_timesheet_sheet* fixes: back to list after remove a timesheet. bulk unfreeze confirmed and pending sheets

This commit is contained in:
Luis 2026-10-01 09:38:17 +02:00
parent 8b0d0bb6cc
commit 0323b6672e
11 changed files with 354 additions and 34 deletions

View file

@ -4,11 +4,12 @@
import os
from unittest.mock import patch
from dateutil.relativedelta import relativedelta
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
from odoo.tests.common import Form, TransactionCase
MODULE = "hr_timesheet_sheet_no_review"
REVIEW_ACTIONS = [
@ -285,7 +286,62 @@ class TestHrTimesheetSheetNoReview(TransactionCase):
"%s must only install technical records" % MODULE,
)
def test_wizard_reports_pending_sheets(self):
# The figures reach the interface
def test_wizard_reports_pending_sheets_in_the_interface(self):
"""The path the user walks: default_get() + onchange(), no create().
Opening an action with ``target="new"`` never creates the record: the
client builds a virtual one and the server decides what to compute from
the dependency graph. A compute with no ``@api.depends`` is skipped and
the dialog reports zero on a database full of frozen sheets.
"""
form = Form(self.wizard_model)
self.assertEqual(form.sheet_count, self.baseline["sheets"] + 1)
self.assertEqual(form.timesheet_count, self.baseline["timesheets"] + 1)
self.assertAlmostEqual(
form.timesheet_hours, self.baseline["hours"] + 3.0, places=2
)
self.assertNotIn("Nothing to unfreeze", form.summary_html)
self.assertIn(self.sheet, form.sheet_ids)
def test_wizard_reports_pending_sheets_through_raw_onchange(self):
"""Same flow, called the way the web client calls it.
``Form`` is a faithful client, but it is still test code. This drives
``onchange()`` directly with the field list of the real view, so the
test also fails if the view stops exposing ``sheet_ids`` and
``default_get()`` is therefore never asked for it.
"""
arch = self._arch(
"hr_timesheet_sheet_no_review.hr_timesheet_sheet_unfreeze_wizard_form",
"form",
model=self.wizard_model._name,
)
names = [node.get("name") for node in arch.xpath("//field")]
self.assertIn("sheet_ids", names)
values = self.wizard_model.onchange({}, [], {name: "1" for name in names})[
"value"
]
self.assertEqual(values["sheet_count"], self.baseline["sheets"] + 1)
self.assertEqual(values["timesheet_count"], self.baseline["timesheets"] + 1)
self.assertAlmostEqual(
values["timesheet_hours"], self.baseline["hours"] + 3.0, places=2
)
self.assertNotIn("Nothing to unfreeze", values["summary_html"])
def test_wizard_figures_depend_on_a_real_field(self):
"""Guard: the figures must keep a dependency to be computed at all."""
for name in ("sheet_count", "timesheet_count", "timesheet_hours"):
field = self.wizard_model._fields[name]
self.assertEqual(
self.env.registry.field_depends[field],
("sheet_ids",),
"%s must depend on sheet_ids or it is never computed in the UI" % name,
)
def test_wizard_reports_pending_sheets_from_code(self):
"""``create({})`` is the shell path, used by the diagnosis probe."""
wizard = self.wizard_model.create({})
self.assertEqual(wizard.sheet_count, self.baseline["sheets"] + 1)
self.assertEqual(wizard.timesheet_count, self.baseline["timesheets"] + 1)
@ -296,17 +352,22 @@ class TestHrTimesheetSheetNoReview(TransactionCase):
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)
form = Form(self.wizard_model)
self.assertEqual(form.sheet_count, 0)
self.assertEqual(form.timesheet_count, 0)
self.assertEqual(form.timesheet_hours, 0.0)
self.assertEqual(len(form.sheet_ids), 0)
self.assertIn("Nothing to unfreeze", form.summary_html)
wizard = form.save()
self.assertIn("Nothing to unfreeze", wizard.summary_html)
# Unfreezing
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()
form = Form(self.wizard_model)
expected = form.sheet_count
result = form.save().action_unfreeze()
self.assertEqual(self.sheet.state, "draft")
self.assertFalse(self.sheet.reviewer_id)
self.assertEqual(
@ -314,24 +375,105 @@ class TestHrTimesheetSheetNoReview(TransactionCase):
)
self.assertIn(str(expected), result["params"]["message"])
def test_unfreeze_only_releases_the_sheets_it_announced(self):
"""The confirmation must count what the warning announced.
The wizard is built, and only then another sheet is frozen. That sheet
was not on screen, so it is not released and it is not counted.
"""
wizard = Form(self.wizard_model).save()
announced = wizard.sheet_count
late_sheet = self.sheet_model.create(
{
"employee_id": self.employee.id,
"company_id": self.company.id,
"date_start": self.sheet.date_start + relativedelta(months=1),
"date_end": self.sheet.date_end + relativedelta(months=1),
}
)
late_sheet.action_timesheet_confirm()
result = wizard.action_unfreeze()
self.assertIn(str(announced), result["params"]["message"])
self.assertEqual(self.sheet.state, "draft")
self.assertEqual(late_sheet.state, "confirm")
def test_unfreeze_ignores_sheets_outside_the_domain(self):
"""Operating on a given recordset must not relax the domain.
``sheet_ids`` comes from the client and can be tampered with. The
sheets that ``_unfreeze_domain()`` keeps out stay out, whatever the
wizard is told to release.
"""
draft_sheet = self.sheet_model.create(
{
"employee_id": self.employee.id,
"company_id": self.company.id,
"date_start": self.sheet.date_start + relativedelta(months=2),
"date_end": self.sheet.date_end + relativedelta(months=2),
}
)
self.assertEqual(draft_sheet.state, "draft")
kept_id = self.sheet.id
with patch.object(
type(self.sheet_model),
"_unfreeze_domain",
lambda records: [
("state", "in", ("confirm", "done")),
("id", "!=", kept_id),
],
):
self.assertNotIn(self.sheet, Form(self.wizard_model).sheet_ids)
wizard = self.wizard_model.create(
{"sheet_ids": [(6, 0, (self.sheet + draft_sheet).ids)]}
)
result = wizard.action_unfreeze()
self.assertEqual(self.sheet.state, "confirm")
self.assertEqual(draft_sheet.state, "draft")
self.assertEqual(result["params"]["type"], "info")
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()
result = Form(self.wizard_model).save().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_unfreeze_twice_from_the_interface_is_idempotent(self):
first = Form(self.wizard_model).save()
second = Form(self.wizard_model).save()
self.assertTrue(first.action_unfreeze()["params"]["type"] == "success")
result = second.action_unfreeze()
self.assertEqual(result["params"]["type"], "info")
self.assertEqual(self.sheet.state, "draft")
self.assertFalse(self.sheet.reviewer_id)
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()
Form(self.wizard_model).save().action_unfreeze()
self.line.with_user(self.user).write({"unit_amount": 5.0})
self.assertEqual(self.line.unit_amount, 5.0)
def test_unfreeze_sheets_hook_keeps_its_signature(self):
"""``_unfreeze_sheets()`` is public: calling it with no argument must
keep releasing every frozen sheet."""
self.assertEqual(
self.sheet_model._unfreeze_sheets(), self.baseline["sheets"] + 1
)
self.assertEqual(self.sheet.state, "draft")
def test_unfreeze_sheets_accepts_a_recordset(self):
self.assertEqual(self.sheet_model._unfreeze_sheets(self.sheet), 1)
self.assertEqual(self.sheet.state, "draft")
self.assertEqual(self.sheet_model._unfreeze_sheets(self.sheet), 0)
no_sheet = self.sheet_model.browse()
self.assertEqual(self.sheet_model._unfreeze_sheets(no_sheet), 0)
def test_unfreeze_domain_hook(self):
self.assertEqual(
self.sheet_model._unfreeze_domain(),
@ -355,4 +497,6 @@ class TestHrTimesheetSheetNoReview(TransactionCase):
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({}))
manager_form = Form(self.wizard_model.with_user(self.manager))
self.assertTrue(manager_form.sheet_count)
self.assertTrue(manager_form.save())