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_delete_cascade

View file

@ -0,0 +1,744 @@
# Copyright 2026 Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import json
from datetime import date, datetime
from odoo import SUPERUSER_ID
from odoo.api import call_kw
from odoo.exceptions import AccessError, UserError
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
WEEK_START = date(2026, 10, 5)
WEEK_END = date(2026, 10, 11)
@tagged("post_install", "-at_install")
class TestHrTimesheetSheetDeleteCascade(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(
context=dict(
cls.env.context,
tracking_disable=True,
no_reset_password=True,
mail_create_nolog=True,
mail_notrack=True,
)
)
cls.company = cls.env["res.company"].create({"name": "Delete Cascade Co"})
cls.env.user.write({"company_ids": [(4, cls.company.id)]})
cls.company.resource_calendar_id.tz = "UTC"
cls.internal_project = cls.company.internal_project_id
cls.timeoff_task = cls.company.leave_timesheet_task_id
cls.user = cls._new_user("tsdc_employee")
cls.user_other = cls._new_user("tsdc_other")
cls.user_span = cls._new_user("tsdc_span")
cls.user_manager = cls._new_user(
"tsdc_manager", group="hr_timesheet.group_timesheet_manager"
)
cls.user_internal = cls._new_internal_user("tsdc_internal")
cls.employee = cls._new_employee("Cascade Employee", cls.user)
cls.employee_other = cls._new_employee("Other Employee", cls.user_other)
cls.employee_span = cls._new_employee("Span Employee", cls.user_span)
cls._new_employee("Cascade Manager", cls.user_manager)
cls.project = cls.env["project.project"].create(
{
"name": "Delete Cascade Project",
"company_id": cls.company.id,
"allow_timesheets": True,
"is_timesheet_task_required": False,
}
)
cls.leave_type = cls.env["hr.leave.type"].create(
{
"name": "Paid Time Off (cascade test)",
"company_id": cls.company.id,
"requires_allocation": "yes",
"allocation_validation_type": "no",
"leave_validation_type": "no_validation",
"timesheet_generate": True,
"timesheet_project_id": cls.internal_project.id,
"timesheet_task_id": cls.timeoff_task.id,
}
)
cls._allocate(cls.employee)
cls._allocate(cls.employee_span)
cls.leave = cls._request_leave(
cls.employee,
datetime(2026, 10, 6, 6, 0, 0),
datetime(2026, 10, 8, 18, 0, 0),
)
cls.leave_lines = cls.leave.sudo().timesheet_ids
cls.leave_span = cls._request_leave(
cls.employee_span,
datetime(2026, 10, 8, 6, 0, 0),
datetime(2026, 10, 13, 18, 0, 0),
)
cls.leave_span_lines = cls.leave_span.sudo().timesheet_ids
cls.sheet = cls._new_sheet(cls.employee)
cls.regular_line = cls._new_regular_line(cls.employee, 4.0)
cls.sheet._compute_timesheet_ids()
cls.sheet_other = cls._new_sheet(cls.employee_other)
# Fixture helpers
@classmethod
def _new_user(cls, login, group="hr_timesheet.group_hr_timesheet_user"):
return (
cls.env["res.users"]
.with_context(no_reset_password=True, default_role_line_ids=[])
.create(
{
"name": login,
"login": login,
"email": "%s@example.org" % login,
"company_id": cls.company.id,
"company_ids": [(6, 0, [cls.company.id])],
"groups_id": [
(
6,
0,
[
cls.env.ref(group).id,
cls.env.ref("project.group_project_user").id,
# Third party add-ons read project_id.stage_id
# when a timesheet line is written, and that
# field is protected by this group.
cls.env.ref("project.group_project_stages").id,
],
)
],
}
)
)
@classmethod
def _new_internal_user(cls, login):
return (
cls.env["res.users"]
.with_context(no_reset_password=True, default_role_line_ids=[])
.create(
{
"name": login,
"login": login,
"email": "%s@example.org" % login,
"company_id": cls.company.id,
"company_ids": [(6, 0, [cls.company.id])],
"groups_id": [(6, 0, [cls.env.ref("base.group_user").id])],
}
)
)
@classmethod
def _new_employee(cls, name, user):
return cls.env["hr.employee"].create(
{"name": name, "user_id": user.id, "company_id": cls.company.id}
)
@classmethod
def _allocate(cls, employee):
allocation = cls.env["hr.leave.allocation"].create(
{
"name": "Allocation for %s" % employee.name,
"holiday_status_id": cls.leave_type.id,
"employee_id": employee.id,
"number_of_days": 25,
"date_from": date(2026, 1, 1),
}
)
if allocation.state != "validate":
allocation.action_validate()
return allocation
@classmethod
def _request_leave(cls, employee, date_from, date_to):
return cls.env["hr.leave"].create(
{
"name": "Time off for %s" % employee.name,
"holiday_status_id": cls.leave_type.id,
"employee_id": employee.id,
"date_from": date_from,
"date_to": date_to,
}
)
@classmethod
def _new_sheet(cls, employee):
return cls.env["hr_timesheet.sheet"].create(
{
"employee_id": employee.id,
"company_id": cls.company.id,
"date_start": WEEK_START,
"date_end": WEEK_END,
}
)
@classmethod
def _new_regular_line(cls, employee, hours):
return cls.env["account.analytic.line"].create(
{
"name": "Regular work",
"project_id": cls.project.id,
"employee_id": employee.id,
"date": WEEK_START,
"unit_amount": hours,
}
)
# Test helpers
def _set_policy(self, policy):
self.company.timesheet_sheet_delete_leave_policy = policy
def _recompute(self, records):
token = self.env["account.analytic.line"]._sheet_recompute_token()
return records.with_context(sheet_recompute=token)
def _cascade(self, records):
token = self.env["account.analytic.line"]._sheet_delete_cascade_token()
return records.with_context(sheet_delete_cascade=token)
def _wizard(self, sheets, user=None):
return (
self.env["hr_timesheet.sheet.delete.wizard"]
.with_user(user or self.user)
.with_context(
active_model="hr_timesheet.sheet", active_ids=sheets.ids
)
.create({})
)
def _leave_balance(self, employee):
leave_type = self.leave_type.with_context(employee_id=employee.id)
leave_type.invalidate_recordset()
return leave_type.leaves_taken, leave_type.virtual_remaining_leaves
def _span_sheet(self):
sheet = self._new_sheet(self.employee_span)
sheet._compute_timesheet_ids()
return sheet
def _second_company_sheet_with_leave(self, policy):
company = self.env["res.company"].create({"name": "Cascade Co %s" % policy})
company.resource_calendar_id.tz = "UTC"
company.timesheet_sheet_delete_leave_policy = policy
user = (
self.env["res.users"]
.with_context(no_reset_password=True, default_role_line_ids=[])
.create(
{
"name": "tsdc_%s" % policy,
"login": "tsdc_%s" % policy,
"email": "tsdc_%s@example.org" % policy,
"company_id": company.id,
"company_ids": [(6, 0, [company.id])],
"groups_id": [
(
6,
0,
[
self.env.ref(
"hr_timesheet.group_hr_timesheet_user"
).id,
self.env.ref("project.group_project_user").id,
self.env.ref("project.group_project_stages").id,
],
)
],
}
)
)
employee = self.env["hr.employee"].create(
{
"name": "Employee %s" % policy,
"user_id": user.id,
"company_id": company.id,
}
)
leave_type = self.env["hr.leave.type"].create(
{
"name": "Paid Time Off (%s)" % policy,
"company_id": company.id,
"requires_allocation": "yes",
"allocation_validation_type": "no",
"leave_validation_type": "no_validation",
"timesheet_generate": True,
"timesheet_project_id": company.internal_project_id.id,
"timesheet_task_id": company.leave_timesheet_task_id.id,
}
)
allocation = self.env["hr.leave.allocation"].create(
{
"name": "Allocation %s" % policy,
"holiday_status_id": leave_type.id,
"employee_id": employee.id,
"number_of_days": 25,
"date_from": date(2026, 1, 1),
}
)
if allocation.state != "validate":
allocation.action_validate()
leave = self.env["hr.leave"].create(
{
"name": "Time off %s" % policy,
"holiday_status_id": leave_type.id,
"employee_id": employee.id,
"date_from": datetime(2026, 10, 6, 6, 0, 0),
"date_to": datetime(2026, 10, 7, 18, 0, 0),
}
)
sheet = self.env["hr_timesheet.sheet"].create(
{
"employee_id": employee.id,
"company_id": company.id,
"date_start": WEEK_START,
"date_end": WEEK_END,
}
)
sheet._compute_timesheet_ids()
return {
"company": company,
"user": user,
"employee": employee,
"leave": leave,
"sheet": sheet,
}
def _invoice(self):
journal = self.env["account.journal"].create(
{
"name": "Cascade Test Journal",
"code": "CTJ",
"type": "general",
"company_id": self.company.id,
}
)
return self.env["account.move"].create(
{
"move_type": "entry",
"journal_id": journal.id,
"company_id": self.company.id,
}
)
# Fixture sanity
def test_fixture(self):
self.assertEqual(len(self.leave_lines), 3)
self.assertEqual(self.leave.state, "validate")
self.assertEqual(len(self.leave_span_lines), 4)
self.assertEqual(
self.sheet.timesheet_ids, self.leave_lines | self.regular_line
)
# RF-01
def test_wizard_preview_does_not_delete(self):
wizard = self._wizard(self.sheet)
self.assertEqual(wizard.sheet_count, 1)
self.assertEqual(wizard.timesheet_count, len(self.sheet.timesheet_ids))
self.assertEqual(
wizard.timesheet_hours,
sum(self.sheet.timesheet_ids.mapped("unit_amount")),
)
self.assertIn(self.sheet.complete_name, wizard.summary_html)
self.assertTrue(self.sheet.exists())
self.assertEqual(self.sheet.timesheet_ids.exists(), self.sheet.timesheet_ids)
def test_wizard_action_opens_on_selected_sheets(self):
action = self.sheet.with_user(self.user).action_delete_sheet()
self.assertEqual(action["res_model"], "hr_timesheet.sheet.delete.wizard")
self.assertEqual(action["context"]["active_ids"], self.sheet.ids)
# RF-02
def test_cascade_deletes_timesheets(self):
self._set_policy("detach")
sheet = self.sheet_other
line_a = self._new_regular_line(self.employee_other, 2.0)
line_b = self._new_regular_line(self.employee_other, 3.0)
sheet._compute_timesheet_ids()
line_ids = sheet.timesheet_ids.ids
self.assertEqual(sorted(line_ids), sorted((line_a | line_b).ids))
self._wizard(sheet, user=self.user_other).action_confirm_delete()
self.assertFalse(sheet.exists())
self.assertFalse(self.env["account.analytic.line"].browse(line_ids).exists())
def test_bare_unlink_is_refused(self):
with self.assertRaises(UserError):
self.sheet.with_user(self.user).unlink()
self.assertTrue(self.sheet.exists())
# RF-03
def test_user_cannot_delete_other_sheet(self):
other = self.sheet_other.with_user(self.user)
with self.assertRaises(AccessError):
other.check_access_rule("unlink")
with self.assertRaises(AccessError):
other.check_access_rule("write")
def test_user_can_delete_own_sheet(self):
own = self.sheet.with_user(self.user)
own.check_access_rule("unlink")
own.check_access_rule("write")
def test_internal_user_without_timesheet_group_cannot_delete_other_sheet(self):
self.assertFalse(
self.user_internal.has_group("hr_timesheet.group_hr_timesheet_user")
)
sheet = self.sheet.with_user(self.user_internal)
with self.assertRaises(AccessError):
sheet.check_access_rule("unlink")
with self.assertRaises(AccessError):
sheet.check_access_rule("write")
def test_manager_can_delete_any_sheet(self):
sheets = (self.sheet | self.sheet_other).with_user(self.user_manager)
sheets.check_access_rule("unlink")
sheets.check_access_rule("write")
# RF-04
def test_invoiced_timesheets_block_deletion(self):
self.regular_line.timesheet_invoice_id = self._invoice()
wizard = self._wizard(self.sheet)
self.assertEqual(wizard.invoiced_count, 1)
self.assertTrue(wizard.blocking_reason)
self.assertIn(self.sheet.complete_name, wizard.blocking_reason)
self.assertIn("1", wizard.blocking_reason)
with self.assertRaises(UserError):
wizard.action_confirm_delete()
with self.assertRaises(UserError):
self._cascade(self.sheet.with_user(self.user)).unlink()
self.assertTrue(self.sheet.exists())
# RF-05
def test_policy_default_is_detach(self):
company = self.env["res.company"].create({"name": "Fresh Cascade Co"})
self.assertEqual(company.timesheet_sheet_delete_leave_policy, "detach")
self.assertEqual(self.sheet._sheet_leave_policy(), "detach")
def test_policy_detach_keeps_leave(self):
self._set_policy("detach")
leave_line_ids = self.leave_lines.ids
self._wizard(self.sheet).action_confirm_delete()
self.assertFalse(self.sheet.exists())
self.assertFalse(self.regular_line.exists())
lines = self.env["account.analytic.line"].browse(leave_line_ids)
self.assertEqual(lines.exists(), lines)
self.assertFalse(lines.mapped("sheet_id"))
self.assertTrue(self.leave.exists())
self.assertEqual(self.leave.state, "validate")
def test_policy_detach_keeps_balance(self):
self._set_policy("detach")
taken, remaining = self._leave_balance(self.employee)
self._wizard(self.sheet).action_confirm_delete()
self.assertEqual(self._leave_balance(self.employee), (taken, remaining))
# RF-06
def test_cascade_cancels_leaves_and_restores_balance(self):
self._set_policy("cancel")
taken, remaining = self._leave_balance(self.employee)
self.assertEqual(taken, 3.0)
leave_line_ids = self.leave_lines.ids
leave = self.leave
self._wizard(self.sheet).action_confirm_delete()
self.assertFalse(self.sheet.exists())
self.assertFalse(
self.env["account.analytic.line"].browse(leave_line_ids).exists()
)
self.assertFalse(self.regular_line.exists())
self.assertFalse(leave.exists())
new_taken, new_remaining = self._leave_balance(self.employee)
self.assertEqual(new_taken, taken - 3.0)
self.assertEqual(new_remaining, remaining + 3.0)
# RF-07
def test_wizard_lists_leaves(self):
self._set_policy("cancel")
wizard = self._wizard(self.sheet)
self.assertEqual(wizard.leave_policy, "cancel")
self.assertEqual(wizard.leave_count, len(self.leave_lines))
self.assertEqual(wizard.leave_days, 3.0)
self.assertEqual(wizard.leave_ids, self.leave)
self.assertFalse(wizard.partial_leave_ids)
self.assertIn(self.employee.name, wizard.summary_html)
self.assertIn(self.leave_type.name, wizard.summary_html)
def test_wizard_warns_about_leaves_spanning_sheets(self):
self._set_policy("cancel")
sheet = self._span_sheet()
wizard = self._wizard(sheet, user=self.user_span)
self.assertEqual(wizard.partial_leave_ids, self.leave_span)
self.assertIn("Warning", wizard.summary_html)
self.assertIn(self.employee_span.name, wizard.summary_html)
def test_wizard_detach_policy_states_leaves_untouched(self):
self._set_policy("detach")
wizard = self._wizard(self.sheet)
self.assertEqual(wizard.leave_policy, "detach")
self.assertEqual(wizard.leave_count, len(self.leave_lines))
self.assertEqual(wizard.leave_days, 0.0)
self.assertFalse(wizard.leave_ids)
self.assertFalse(wizard.partial_leave_ids)
self.assertIn("balances do not change", wizard.summary_html)
def test_wizard_without_leaves_has_no_leave_section(self):
line = self._new_regular_line(self.employee_other, 2.0)
sheet = self.sheet_other
sheet._compute_timesheet_ids()
self.assertEqual(sheet.timesheet_ids, line)
for policy in ("detach", "cancel"):
self._set_policy(policy)
wizard = self._wizard(sheet, user=self.user_other)
self.assertEqual(wizard.leave_count, 0)
self.assertNotIn("Time off", wizard.summary_html)
def test_uncancellable_leave_blocks_deletion(self):
self._set_policy("cancel")
# A request already refused outside this module keeps its entries:
# action_refuse() would have removed them, a data fix would not.
self.leave.with_user(SUPERUSER_ID).sudo().write({"state": "refuse"})
self.assertEqual(self.sheet._sheet_leaves_to_cancel(), self.env["hr.leave"])
self.assertEqual(self.sheet._sheet_leaves_not_cancellable(), self.leave)
wizard = self._wizard(self.sheet)
self.assertTrue(wizard.blocking_reason)
self.assertIn(self.sheet.complete_name, wizard.blocking_reason)
self.assertIn("cannot be cancelled", wizard.blocking_reason)
with self.assertRaises(UserError):
wizard.action_confirm_delete()
self.env.invalidate_all()
self.assertTrue(self.sheet.exists())
self.assertEqual(self.sheet.timesheet_ids.exists(), self.sheet.timesheet_ids)
def test_multicompany_batch_summary_matches_each_policy(self):
self._set_policy("cancel")
other = self._second_company_sheet_with_leave("detach")
self.env.user.write({"company_ids": [(4, other["company"].id)]})
sheets = self.sheet | other["sheet"]
data = sheets._delete_cascade_prepare()
titles = [section["title"] for section in data["sections"]]
# Compared against the titles the model itself produces, so the test
# does not depend on the language of the user running it.
self.assertIn(other["sheet"]._delete_cascade_detach_section()["title"], titles)
self.assertIn(self.sheet._delete_cascade_cancel_section()[0]["title"], titles)
self.assertEqual(data["leave_days"], 3.0)
self.assertEqual(data["leave_count"], 5)
cancel_title = self.sheet._delete_cascade_cancel_section()[0]["title"]
cancelled = " ".join(
line
for section in data["sections"]
if section["title"] == cancel_title
for line in section["lines"]
)
self.assertIn(self.employee.name, cancelled)
self.assertNotIn(other["employee"].name, cancelled)
wizard = self._wizard(sheets, user=self.env.user)
self.assertEqual(wizard.leave_ids, self.leave)
self.assertEqual(wizard.leave_days, 3.0)
def test_public_holiday_lines_created_while_cancelling_are_deleted(self):
self._set_policy("cancel")
global_leave = (
self.env["resource.calendar.leaves"]
.with_context(allowed_company_ids=[self.company.id])
.create(
{
"name": "Public holiday",
"calendar_id": self.company.resource_calendar_id.id,
"company_id": self.company.id,
"date_from": datetime(2026, 10, 7, 0, 0, 0),
"date_to": datetime(2026, 10, 7, 23, 59, 0),
}
)
)
self.assertFalse(global_leave.timesheet_ids)
sheet_id = self.sheet.id
self._wizard(self.sheet).action_confirm_delete()
self.env.flush_all()
self.env.invalidate_all()
self.assertFalse(self.sheet.exists())
self.assertFalse(global_leave.timesheet_ids)
self.env.cr.execute(
"SELECT COUNT(*) FROM account_analytic_line WHERE sheet_id = %s",
(sheet_id,),
)
self.assertEqual(self.env.cr.fetchone()[0], 0)
# RF-08
def test_bare_unlink_of_leave_line_is_refused(self):
line = self.leave_lines[0].with_user(self.user)
with self.assertRaises(UserError):
line.unlink()
self.env.invalidate_all()
self.assertTrue(self.leave_lines[0].exists())
self.assertTrue(self.leave_lines[0].sheet_id)
def test_unlink_during_sheet_recompute_detaches(self):
line = self.leave_lines[0].with_user(self.user)
self._recompute(line).unlink()
self.env.invalidate_all()
self.assertTrue(self.leave_lines[0].exists())
self.assertFalse(self.leave_lines[0].sheet_id)
self.assertTrue(self.leave.exists())
def test_unlink_during_delete_cascade_cancel_deletes(self):
self._set_policy("cancel")
line = self.leave_lines[0]
self._cascade(line.with_user(self.user).sudo()).unlink()
self.assertFalse(line.exists())
def test_unlink_during_delete_cascade_detach_detaches(self):
self._set_policy("detach")
line = self.leave_lines[0]
self._cascade(line.with_user(self.user).sudo()).unlink()
self.env.invalidate_all()
self.assertTrue(line.exists())
self.assertFalse(line.sheet_id)
self.assertTrue(self.leave.exists())
def test_delete_cascade_flag_without_sudo_is_refused(self):
line = self.leave_lines[0].with_user(self.user)
for policy in ("detach", "cancel"):
self._set_policy(policy)
with self.assertRaises(UserError):
self._cascade(line).unlink()
self.env.invalidate_all()
self.assertTrue(self.leave_lines[0].exists())
self.assertTrue(self.leave_lines[0].sheet_id)
def test_both_flags_delete_cascade_wins(self):
self._set_policy("cancel")
line = self.leave_lines[0]
self._cascade(self._recompute(line.with_user(self.user).sudo())).unlink()
self.assertFalse(line.exists())
def test_both_flags_with_detach_policy_still_detaches(self):
self._set_policy("detach")
line = self.leave_lines[0]
self._cascade(self._recompute(line.with_user(self.user).sudo())).unlink()
self.env.invalidate_all()
self.assertTrue(line.exists())
self.assertFalse(line.sheet_id)
# RF-09
def test_no_cascade_flag_leak_after_deletion(self):
self._set_policy("cancel")
self._wizard(self.sheet).action_confirm_delete()
self.assertIsNone(self.env.context.get("sheet_delete_cascade"))
other_line = self.leave_span_lines[0].with_user(self.user_span)
with self.assertRaises(UserError):
other_line.unlink()
self.env.invalidate_all()
self.assertTrue(self.leave_span_lines[0].exists())
self.assertIsNone(self.env.context.get("sheet_delete_cascade"))
# Published hooks
def test_delete_cascade_prepare_contract(self):
data = self.sheet._delete_cascade_prepare()
self.assertEqual(
set(data),
{
"sheet_count",
"timesheet_count",
"timesheet_hours",
"invoiced_count",
"leave_count",
"leave_days",
"leave_policy",
"blocking",
"sections",
},
)
self.assertEqual(data["blocking"], [])
self.assertTrue(all("title" in s and "lines" in s for s in data["sections"]))
def test_detach_hook_matrix(self):
"""One assertion per row of the RF-08 table, on the same leave line.
The base recordset is bound to a plain employee on purpose: the class
env is superuser, so a line taken straight from the fixture would carry
env.su and the row that must fail without it would pass for the wrong
reason.
"""
line = self.leave_lines[0].with_user(self.user)
self.assertFalse(line.env.su)
line_su = line.sudo()
# Row 1: no flags.
self.assertFalse(line._detach_leave_lines_on_unlink())
# Row 2: sheet recompute, any policy.
self.assertTrue(self._recompute(line)._detach_leave_lines_on_unlink())
self._set_policy("cancel")
# Row 3: deliberate cascade with cancel deletes instead of detaching.
self.assertFalse(self._cascade(line_su)._detach_leave_lines_on_unlink())
# Row 5, cancel: the sentinel alone, without env.su, detaches nothing.
self.assertFalse(self._cascade(line)._detach_leave_lines_on_unlink())
self.assertFalse(self._cascade(line)._in_deliberate_delete_cascade())
# Row 6: the cascade wins over a concurrent recompute.
self.assertFalse(
self._cascade(self._recompute(line_su))._detach_leave_lines_on_unlink()
)
self._set_policy("detach")
# Row 4: deliberate cascade with detach keeps the line alive.
self.assertTrue(self._cascade(line_su)._detach_leave_lines_on_unlink())
# Row 5, detach: still nothing without env.su.
self.assertFalse(self._cascade(line)._detach_leave_lines_on_unlink())
self.assertFalse(self._cascade(line)._in_deliberate_delete_cascade())
def test_context_token_is_not_serializable(self):
token = self.env["account.analytic.line"]._sheet_delete_cascade_token()
with self.assertRaises(TypeError):
json.dumps(token)
def test_forged_cascade_flag_does_not_delete_sheet(self):
model = self.env["hr_timesheet.sheet"].with_user(self.user)
with self.assertRaises(UserError):
call_kw(
model,
"unlink",
[self.sheet.ids],
{"context": {"sheet_delete_cascade": True}},
)
self.env.invalidate_all()
self.assertTrue(self.sheet.exists())
self.assertEqual(self.sheet.timesheet_ids.exists(), self.sheet.timesheet_ids)
self.assertFalse(
model.with_context(sheet_delete_cascade=True)._in_delete_cascade()
)
def test_forged_cascade_flag_does_not_delete_leave_line(self):
model = self.env["account.analytic.line"].with_user(self.user)
line = self.leave_lines[0]
self._set_policy("cancel")
with self.assertRaises(UserError):
call_kw(
model,
"unlink",
[[line.id]],
{"context": {"sheet_delete_cascade": True}},
)
self.env.invalidate_all()
self.assertTrue(line.exists())
self.assertFalse(
model.sudo()
.with_context(sheet_delete_cascade=True)
._in_deliberate_delete_cascade()
)