From c32108070229fb7f0b69b8f78974de1415ad8b4a Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Thu, 29 Oct 2026 16:50:07 -0300 Subject: [PATCH 1/8] [MIG] hr_timesheet_attendance_ux: Migration to 20.0 --- hr_timesheet_attendance_ux/__manifest__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hr_timesheet_attendance_ux/__manifest__.py b/hr_timesheet_attendance_ux/__manifest__.py index 08a3eb26..7145f6fe 100644 --- a/hr_timesheet_attendance_ux/__manifest__.py +++ b/hr_timesheet_attendance_ux/__manifest__.py @@ -19,7 +19,7 @@ ############################################################################## { "name": "Attendance Timesheet UX", - "version": "19.0.1.1.0", + "version": "20.0.1.0.0", "category": "Human Resources", "sequence": 14, "author": "ADHOC SA", @@ -34,7 +34,7 @@ "views/hr_attendance_views.xml", "views/hr_timesheet_attendance_report_views.xml", ], - "installable": False, + "installable": True, "auto_install": False, "application": False, } From 3fb23d3d8ddc02f914c8a92725965accde749291 Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Thu, 29 Oct 2026 16:59:10 -0300 Subject: [PATCH 2/8] [MIG] hr_timesheet_attendance_ux: adopt native My Attendances in 20.0 Odoo 20.0 adds its own "My Attendances" in the Attendances app, so the module drops its copy, as the product owner decided: - remove the "My Attendances" and "Check In / Check Out" menus, their action and the list view with "Service Hours" (current_worked_hours) - remove the context override on hr_attendance.hr_attendance_action: it asked for search_default_my_attendances, a filter that does not exist (it is called myattendances), and it replaced the whole core context, so the default grouping of the core was lost. The -u of hr_attendance puts the core context back - hours suggestion on a timesheet: compute the attended time inline and subtract the new break_duration of 20.0 - task domain: add ('has_template_ancestor', '=', False). The native domain has it since 19.0 and the module lost it when it replaced the domain, so template tasks were offered - My Timesheet / Attendance pivot: drop search_default_user_id, the report has no user_id field --- hr_timesheet_attendance_ux/__manifest__.py | 1 - hr_timesheet_attendance_ux/models/__init__.py | 1 - .../models/account_analytic_line.py | 9 +++- .../models/hr_attendance.py | 17 -------- .../views/hr_attendance_views.xml | 43 ------------------- .../hr_timesheet_attendance_report_views.xml | 1 - 6 files changed, 7 insertions(+), 65 deletions(-) delete mode 100644 hr_timesheet_attendance_ux/models/hr_attendance.py delete mode 100644 hr_timesheet_attendance_ux/views/hr_attendance_views.xml diff --git a/hr_timesheet_attendance_ux/__manifest__.py b/hr_timesheet_attendance_ux/__manifest__.py index 7145f6fe..8880ad55 100644 --- a/hr_timesheet_attendance_ux/__manifest__.py +++ b/hr_timesheet_attendance_ux/__manifest__.py @@ -31,7 +31,6 @@ ], "data": [ "views/account_analytic_line_views.xml", - "views/hr_attendance_views.xml", "views/hr_timesheet_attendance_report_views.xml", ], "installable": True, diff --git a/hr_timesheet_attendance_ux/models/__init__.py b/hr_timesheet_attendance_ux/models/__init__.py index 50f69399..f69550ae 100644 --- a/hr_timesheet_attendance_ux/models/__init__.py +++ b/hr_timesheet_attendance_ux/models/__init__.py @@ -1,2 +1 @@ -from . import hr_attendance from . import account_analytic_line diff --git a/hr_timesheet_attendance_ux/models/account_analytic_line.py b/hr_timesheet_attendance_ux/models/account_analytic_line.py index 1b3b7a6e..1f2256a6 100644 --- a/hr_timesheet_attendance_ux/models/account_analytic_line.py +++ b/hr_timesheet_attendance_ux/models/account_analytic_line.py @@ -7,13 +7,14 @@ class AccountAnalyticLine(models.Model): _inherit = "account.analytic.line" task_id = fields.Many2one( - domain="[('company_id', '=', company_id), ('project_id.allow_timesheets', '=', True), ('project_id', '=?', project_id), ('stage_id.fold', '=', False)]" + domain="[('company_id', '=', company_id), ('project_id.allow_timesheets', '=', True), ('project_id', '=?', project_id), ('stage_id.fold', '=', False), ('has_template_ancestor', '=', False)]" ) @api.onchange("employee_id", "date") def onchange_compute_hours(self): user_employee = self.default_get(["employee_id", "user_id"]).get("employee_id") hr_attendance = self.env["hr.attendance"] + now = fields.Datetime.now() for rec in self: end = rec.date + timedelta(hours=23, minutes=59, seconds=59) employee_id = rec.employee_id.id or user_employee @@ -32,5 +33,9 @@ def onchange_compute_hours(self): # tenga en cuenta las lineas que son por ausencia total_time_register = self.search(domain) total_time_register = sum(total_time_register.mapped("unit_amount")) - current_worked_hours = sum(attendances.mapped("current_worked_hours")) + # Open attendances count up to now; the unpaid break is not worked time. + current_worked_hours = sum( + ((att.check_out or now) - att.check_in).total_seconds() / 3600.0 - att.break_duration + for att in attendances + ) rec.unit_amount = current_worked_hours - total_time_register diff --git a/hr_timesheet_attendance_ux/models/hr_attendance.py b/hr_timesheet_attendance_ux/models/hr_attendance.py deleted file mode 100644 index 2a16371b..00000000 --- a/hr_timesheet_attendance_ux/models/hr_attendance.py +++ /dev/null @@ -1,17 +0,0 @@ -from odoo import fields, models - - -class HrAttendance(models.Model): - _inherit = "hr.attendance" - - current_worked_hours = fields.Float( - string="Service Hours", - compute="_compute_current_worked_hours", - ) - - def _compute_current_worked_hours(self): - for attendance in self: - check_out_datetime = attendance.check_out or fields.Datetime.now() - check_in_datetime = attendance.check_in - delta = check_out_datetime - check_in_datetime - attendance.current_worked_hours = delta.total_seconds() / 3600.0 diff --git a/hr_timesheet_attendance_ux/views/hr_attendance_views.xml b/hr_timesheet_attendance_ux/views/hr_attendance_views.xml deleted file mode 100644 index 7b0244eb..00000000 --- a/hr_timesheet_attendance_ux/views/hr_attendance_views.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - hr.attendance.list - hr.attendance - - primary - - - - - - - - - {"search_default_my_attendances": 1} - - - - My Attendances - hr.attendance - list,kanban,form - {"search_default_my_attendances": 1} - - - -

Your attendance records will be displayed here.

-

Please make sure you're using the correct filter if you expected to see any.

-
-
- - - - - -
diff --git a/hr_timesheet_attendance_ux/views/hr_timesheet_attendance_report_views.xml b/hr_timesheet_attendance_ux/views/hr_timesheet_attendance_report_views.xml index df2d3e9e..4a5606f5 100644 --- a/hr_timesheet_attendance_ux/views/hr_timesheet_attendance_report_views.xml +++ b/hr_timesheet_attendance_ux/views/hr_timesheet_attendance_report_views.xml @@ -7,7 +7,6 @@ [('employee_id.user_id', '=', uid)] pivot { - 'search_default_user_id': uid, 'pivot_column_groupby': [], 'pivot_measures': ['total_attendance', 'total_timesheet', 'total_difference'], 'pivot_row_groupby': ['date:day'], From 05fc5e167ff5a663ae662e387a13213fd6a3b903 Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Thu, 29 Oct 2026 16:59:10 -0300 Subject: [PATCH 3/8] [ADD] hr_timesheet_attendance_ux: tests for hours suggestion and task domain --- hr_timesheet_attendance_ux/tests/__init__.py | 1 + .../tests/test_hr_timesheet_attendance_ux.py | 92 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 hr_timesheet_attendance_ux/tests/__init__.py create mode 100644 hr_timesheet_attendance_ux/tests/test_hr_timesheet_attendance_ux.py diff --git a/hr_timesheet_attendance_ux/tests/__init__.py b/hr_timesheet_attendance_ux/tests/__init__.py new file mode 100644 index 00000000..f761030b --- /dev/null +++ b/hr_timesheet_attendance_ux/tests/__init__.py @@ -0,0 +1 @@ +from . import test_hr_timesheet_attendance_ux diff --git a/hr_timesheet_attendance_ux/tests/test_hr_timesheet_attendance_ux.py b/hr_timesheet_attendance_ux/tests/test_hr_timesheet_attendance_ux.py new file mode 100644 index 00000000..9e3ca3f0 --- /dev/null +++ b/hr_timesheet_attendance_ux/tests/test_hr_timesheet_attendance_ux.py @@ -0,0 +1,92 @@ +from datetime import datetime, time, timedelta + +from odoo import fields +from odoo.addons.hr_timesheet.tests.test_timesheet import TestCommonTimesheet +from odoo.tests import Form, freeze_time, tagged +from odoo.tools.safe_eval import safe_eval + + +@tagged("post_install", "-at_install") +class TestHrTimesheetAttendanceUx(TestCommonTimesheet): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.today = fields.Date.today() + # A past day, so a closed attendance never ends in the future. + cls.day = cls.today - timedelta(days=1) + + def _new_timesheet_form(self, day): + form = Form( + # The suggestion cuts the day in the user's timezone: fix it. + self.env["account.analytic.line"].with_user(self.user_employee).with_context(tz="UTC"), + view="hr_timesheet.hr_timesheet_line_form", + ) + form.project_id = self.project_customer + form.date = day + return form + + def test_suggested_hours_discount_break_and_logged_time(self): + # The form runs in UTC, so these times stay inside the same day. + self.env["hr.attendance"].create( + { + "employee_id": self.empl_employee.id, + "check_in": datetime.combine(self.day, time(9)), + "check_out": datetime.combine(self.day, time(17)), + "break_duration": 0.5, + } + ) + self.env["account.analytic.line"].create( + { + "name": "Already logged", + "project_id": self.project_customer.id, + "employee_id": self.empl_employee.id, + "date": self.day, + "unit_amount": 2.0, + } + ) + form = self._new_timesheet_form(self.day) + # 8 attended hours - 0.5 break - 2 already logged + self.assertAlmostEqual(form.unit_amount, 5.5) + line = form.save() + self.assertEqual(line.employee_id, self.empl_employee) + self.assertEqual(line.date, self.day) + + with self.subTest("open attendance counts up to now"): + self.env["hr.attendance"].search([("employee_id", "=", self.empl_employee.id)]).unlink() + with freeze_time(datetime.combine(self.today, time(15))): + self.env["hr.attendance"].create( + { + "employee_id": self.empl_employee.id, + "check_in": datetime.combine(self.today, time(12)), + } + ) + form = self._new_timesheet_form(self.today) + self.assertAlmostEqual(form.unit_amount, 3.0) + + def test_task_domain_hides_folded_and_template_tasks(self): + Task = self.env["project.task"] + open_stage = self.env["project.task.type"].create({"name": "Open", "fold": False}) + folded_stage = self.env["project.task.type"].create({"name": "Done", "fold": True}) + self.project_customer.type_ids = [(6, 0, (open_stage | folded_stage).ids)] + open_task = Task.create( + {"name": "Open task", "project_id": self.project_customer.id, "stage_id": open_stage.id} + ) + folded_task = Task.create( + {"name": "Folded task", "project_id": self.project_customer.id, "stage_id": folded_stage.id} + ) + template_task = Task.create( + { + "name": "Template task", + "project_id": self.project_customer.id, + "stage_id": open_stage.id, + "is_template": True, + } + ) + domain = safe_eval( + self.env["account.analytic.line"]._fields["task_id"].domain, + {"company_id": self.project_customer.company_id.id, "project_id": self.project_customer.id}, + ) + available = Task.search(domain) + self.assertIn(open_task, available) + self.assertNotIn(folded_task, available) + self.assertNotIn(template_task, available) From da29c3363f2a89495b7aff6b9552819c32e95f81 Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Thu, 29 Oct 2026 16:59:30 -0300 Subject: [PATCH 4/8] [DOC] hr_timesheet_attendance_ux: README describes the hours suggestion and task filter --- hr_timesheet_attendance_ux/README.rst | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/hr_timesheet_attendance_ux/README.rst b/hr_timesheet_attendance_ux/README.rst index d92cfd45..839201bf 100644 --- a/hr_timesheet_attendance_ux/README.rst +++ b/hr_timesheet_attendance_ux/README.rst @@ -16,6 +16,13 @@ Attendance Timesheet UX #. Add link from timesheets to timesheet vs attendance analysis #. Timesheets/All Timesheets odoo menu is set visible only in developer mode. +#. When you set the employee or the date of a timesheet, the time spent is + suggested: the attendance hours of that day in the user's timezone (open + attendances count up to now), minus the break, minus the time already + logged that day. Timesheets from time off requests are not counted as + already logged. +#. The task selector of a timesheet does not offer tasks in a folded stage or + template tasks. Installation ============ @@ -36,7 +43,9 @@ Usage To use this module, you need to: -#. Go to ... +#. Go to Timesheets > Timesheets > My Timesheet / Attendance to compare your + attendance time with your timesheets, day by day. +#. Create a timesheet and check the suggested time spent. .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas :alt: Try me on Runbot @@ -46,7 +55,7 @@ Bug Tracker =========== Bugs are tracked on `GitHub Issues -`_. In case of trouble, please +`_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed feedback. From 51ee592179e98ff4d5d9edab0beb85b43be8cd9c Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Thu, 29 Oct 2026 17:01:30 -0300 Subject: [PATCH 5/8] [I18N] hr_timesheet_attendance_ux: export terms for 20.0 and translate the module name --- hr_timesheet_attendance_ux/i18n/es.po | 141 ++++++++++++------ .../i18n/hr_timesheet_attendance_ux.pot | 127 +++++++++++----- 2 files changed, 183 insertions(+), 85 deletions(-) diff --git a/hr_timesheet_attendance_ux/i18n/es.po b/hr_timesheet_attendance_ux/i18n/es.po index b7083768..7785a65c 100644 --- a/hr_timesheet_attendance_ux/i18n/es.po +++ b/hr_timesheet_attendance_ux/i18n/es.po @@ -7,84 +7,131 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 19.0+e\n" +"Project-Id-Version: Odoo Server 20.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-10-28 20:55+0000\n" -"PO-Revision-Date: 2025-12-11 18:11+0000\n" +"POT-Creation-Date: 2026-10-29 20:16+0000\n" +"PO-Revision-Date: 2026-10-29 20:16+0000\n" "Last-Translator: BAF \n" -"Language-Team: Spanish \n" +"Language-Team: Spanish \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Plural-Forms: nplurals=3; plural=(n == 1) ? 0 : ((n != 0 && n % 1000000 == 0)" -" ? 1 : 2);\n" +"Plural-Forms: nplurals=3; plural=(n == 1) ? 0 : ((n != 0 && n % 1000000 == 0) ? 1 : 2);\n" "X-Generator: Weblate 5.10.4\n" +#. module: hr_timesheet_attendance_ux +#: model:ir.module.module,description:base.module_hr_timesheet_attendance_ux +msgid "" +".. |company| replace:: ADHOC SA\n" +"\n" +".. |company_logo| image:: https://raw.githubusercontent.com/ingadhoc/maintainer-tools/master/resources/adhoc-logo.png\n" +" :alt: ADHOC SA\n" +" :target: https://www.adhoc.com.ar\n" +"\n" +".. |icon| image:: https://raw.githubusercontent.com/ingadhoc/maintainer-tools/master/resources/adhoc-icon.png\n" +"\n" +".. image:: https://img.shields.io/badge/license-AGPL--3-blue.png\n" +" :target: https://www.gnu.org/licenses/agpl\n" +" :alt: License: AGPL-3\n" +"\n" +"=======================\n" +"Attendance Timesheet UX\n" +"=======================\n" +"\n" +"#. Add link from timesheets to timesheet vs attendance analysis\n" +"#. Timesheets/All Timesheets odoo menu is set visible only in developer mode.\n" +"#. When you set the employee or the date of a timesheet, the time spent is\n" +" suggested: the attendance hours of that day in the user's timezone (open\n" +" attendances count up to now), minus the break, minus the time already\n" +" logged that day. Timesheets from time off requests are not counted as\n" +" already logged.\n" +"#. The task selector of a timesheet does not offer tasks in a folded stage or\n" +" template tasks.\n" +"\n" +"Installation\n" +"============\n" +"\n" +"To install this module, you need to:\n" +"\n" +"#. Only need to install the module\n" +"\n" +"Configuration\n" +"=============\n" +"\n" +"To configure this module, you need to:\n" +"\n" +"#. Nothing to configure\n" +"\n" +"Usage\n" +"=====\n" +"\n" +"To use this module, you need to:\n" +"\n" +"#. Go to Timesheets > Timesheets > My Timesheet / Attendance to compare your\n" +" attendance time with your timesheets, day by day.\n" +"#. Create a timesheet and check the suggested time spent.\n" +"\n" +".. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas\n" +" :alt: Try me on Runbot\n" +" :target: http://runbot.adhoc.com.ar/\n" +"\n" +"Bug Tracker\n" +"===========\n" +"\n" +"Bugs are tracked on `GitHub Issues\n" +"`_. In case of trouble, please\n" +"check there if your issue has already been reported. If you spotted it first,\n" +"help us smashing it by providing a detailed and welcomed feedback.\n" +"\n" +"Credits\n" +"=======\n" +"\n" +"Images\n" +"------\n" +"\n" +"* |company| |icon|\n" +"\n" +"Contributors\n" +"------------\n" +"\n" +"Maintainer\n" +"----------\n" +"\n" +"|company_logo|\n" +"\n" +"This module is maintained by the |company|.\n" +"\n" +"To contribute to this module, please visit https://www.adhoc.com.ar.\n" +msgstr "" + #. module: hr_timesheet_attendance_ux #: model:ir.model,name:hr_timesheet_attendance_ux.model_account_analytic_line msgid "Analytic Line" msgstr "Línea analítica" #. module: hr_timesheet_attendance_ux -#: model:ir.model,name:hr_timesheet_attendance_ux.model_hr_attendance -msgid "Attendance" -msgstr "Asistencia" - -#. module: hr_timesheet_attendance_ux -#: model:ir.ui.menu,name:hr_timesheet_attendance_ux.menu_hr_attendance_my_attendances -msgid "Check In / Check Out" -msgstr "Entrada / Salida" +#: model:ir.module.module,shortdesc:base.module_hr_timesheet_attendance_ux +msgid "Attendance Timesheet UX" +msgstr "Asistencias y parte de horas UX" #. module: hr_timesheet_attendance_ux #: model:ir.model.fields,field_description:hr_timesheet_attendance_ux.field_account_analytic_line__display_name -#: model:ir.model.fields,field_description:hr_timesheet_attendance_ux.field_hr_attendance__display_name msgid "Display Name" -msgstr "Nombre para mostrar" +msgstr "Mostrar nombre" #. module: hr_timesheet_attendance_ux #: model:ir.model.fields,field_description:hr_timesheet_attendance_ux.field_account_analytic_line__id -#: model:ir.model.fields,field_description:hr_timesheet_attendance_ux.field_hr_attendance__id msgid "ID" msgstr "ID" -#. module: hr_timesheet_attendance_ux -#: model:ir.actions.act_window,name:hr_timesheet_attendance_ux.hr_attendance_action -#: model:ir.ui.menu,name:hr_timesheet_attendance_ux.my_hr_attendance -msgid "My Attendances" -msgstr "Mis asistencias" - #. module: hr_timesheet_attendance_ux #: model:ir.actions.act_window,name:hr_timesheet_attendance_ux.action_hr_timesheet_attendance_report_user_week #: model:ir.ui.menu,name:hr_timesheet_attendance_ux.menu_hr_timesheet_attendance_report_user_week msgid "My Timesheet / Attendance" msgstr "Mis Parte de Horas / Asistencias" -#. module: hr_timesheet_attendance_ux -#: model_terms:ir.actions.act_window,help:hr_timesheet_attendance_ux.hr_attendance_action -msgid "" -"Please make sure you're using the correct filter if you expected to see any." -msgstr "" -"Por favor asegúrate de estar usando el filtro correcto si esperabas ver " -"alguno" - -#. module: hr_timesheet_attendance_ux -#: model:ir.model.fields,field_description:hr_timesheet_attendance_ux.field_hr_attendance__current_worked_hours -msgid "Service Hours" -msgstr "Horas de Servicio" - #. module: hr_timesheet_attendance_ux #: model:ir.model.fields,field_description:hr_timesheet_attendance_ux.field_account_analytic_line__task_id msgid "Task" msgstr "Tarea" - -#. module: hr_timesheet_attendance_ux -#: model_terms:ir.ui.view,arch_db:hr_timesheet_attendance_ux.view_attendance_tree -msgid "Total service" -msgstr "Servicio total" - -#. module: hr_timesheet_attendance_ux -#: model_terms:ir.actions.act_window,help:hr_timesheet_attendance_ux.hr_attendance_action -msgid "Your attendance records will be displayed here." -msgstr "Tus registros de asistencias serán mostrados acá" diff --git a/hr_timesheet_attendance_ux/i18n/hr_timesheet_attendance_ux.pot b/hr_timesheet_attendance_ux/i18n/hr_timesheet_attendance_ux.pot index f15ba77f..b6ce1d68 100644 --- a/hr_timesheet_attendance_ux/i18n/hr_timesheet_attendance_ux.pot +++ b/hr_timesheet_attendance_ux/i18n/hr_timesheet_attendance_ux.pot @@ -4,10 +4,10 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 19.0+e\n" +"Project-Id-Version: Odoo Server 20.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-11-22 02:34+0000\n" -"PO-Revision-Date: 2025-11-22 02:34+0000\n" +"POT-Creation-Date: 2026-10-29 20:16+0000\n" +"PO-Revision-Date: 2026-10-29 20:16+0000\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -16,66 +16,117 @@ msgstr "" "Plural-Forms: \n" #. module: hr_timesheet_attendance_ux -#: model:ir.model,name:hr_timesheet_attendance_ux.model_account_analytic_line -msgid "Analytic Line" +#: model:ir.module.module,description:base.module_hr_timesheet_attendance_ux +msgid "" +".. |company| replace:: ADHOC SA\n" +"\n" +".. |company_logo| image:: https://raw.githubusercontent.com/ingadhoc/maintainer-tools/master/resources/adhoc-logo.png\n" +" :alt: ADHOC SA\n" +" :target: https://www.adhoc.com.ar\n" +"\n" +".. |icon| image:: https://raw.githubusercontent.com/ingadhoc/maintainer-tools/master/resources/adhoc-icon.png\n" +"\n" +".. image:: https://img.shields.io/badge/license-AGPL--3-blue.png\n" +" :target: https://www.gnu.org/licenses/agpl\n" +" :alt: License: AGPL-3\n" +"\n" +"=======================\n" +"Attendance Timesheet UX\n" +"=======================\n" +"\n" +"#. Add link from timesheets to timesheet vs attendance analysis\n" +"#. Timesheets/All Timesheets odoo menu is set visible only in developer mode.\n" +"#. When you set the employee or the date of a timesheet, the time spent is\n" +" suggested: the attendance hours of that day in the user's timezone (open\n" +" attendances count up to now), minus the break, minus the time already\n" +" logged that day. Timesheets from time off requests are not counted as\n" +" already logged.\n" +"#. The task selector of a timesheet does not offer tasks in a folded stage or\n" +" template tasks.\n" +"\n" +"Installation\n" +"============\n" +"\n" +"To install this module, you need to:\n" +"\n" +"#. Only need to install the module\n" +"\n" +"Configuration\n" +"=============\n" +"\n" +"To configure this module, you need to:\n" +"\n" +"#. Nothing to configure\n" +"\n" +"Usage\n" +"=====\n" +"\n" +"To use this module, you need to:\n" +"\n" +"#. Go to Timesheets > Timesheets > My Timesheet / Attendance to compare your\n" +" attendance time with your timesheets, day by day.\n" +"#. Create a timesheet and check the suggested time spent.\n" +"\n" +".. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas\n" +" :alt: Try me on Runbot\n" +" :target: http://runbot.adhoc.com.ar/\n" +"\n" +"Bug Tracker\n" +"===========\n" +"\n" +"Bugs are tracked on `GitHub Issues\n" +"`_. In case of trouble, please\n" +"check there if your issue has already been reported. If you spotted it first,\n" +"help us smashing it by providing a detailed and welcomed feedback.\n" +"\n" +"Credits\n" +"=======\n" +"\n" +"Images\n" +"------\n" +"\n" +"* |company| |icon|\n" +"\n" +"Contributors\n" +"------------\n" +"\n" +"Maintainer\n" +"----------\n" +"\n" +"|company_logo|\n" +"\n" +"This module is maintained by the |company|.\n" +"\n" +"To contribute to this module, please visit https://www.adhoc.com.ar.\n" msgstr "" #. module: hr_timesheet_attendance_ux -#: model:ir.model,name:hr_timesheet_attendance_ux.model_hr_attendance -msgid "Attendance" +#: model:ir.model,name:hr_timesheet_attendance_ux.model_account_analytic_line +msgid "Analytic Line" msgstr "" #. module: hr_timesheet_attendance_ux -#: model:ir.ui.menu,name:hr_timesheet_attendance_ux.menu_hr_attendance_my_attendances -msgid "Check In / Check Out" +#: model:ir.module.module,shortdesc:base.module_hr_timesheet_attendance_ux +msgid "Attendance Timesheet UX" msgstr "" #. module: hr_timesheet_attendance_ux #: model:ir.model.fields,field_description:hr_timesheet_attendance_ux.field_account_analytic_line__display_name -#: model:ir.model.fields,field_description:hr_timesheet_attendance_ux.field_hr_attendance__display_name msgid "Display Name" msgstr "" #. module: hr_timesheet_attendance_ux #: model:ir.model.fields,field_description:hr_timesheet_attendance_ux.field_account_analytic_line__id -#: model:ir.model.fields,field_description:hr_timesheet_attendance_ux.field_hr_attendance__id msgid "ID" msgstr "" -#. module: hr_timesheet_attendance_ux -#: model:ir.actions.act_window,name:hr_timesheet_attendance_ux.hr_attendance_action -#: model:ir.ui.menu,name:hr_timesheet_attendance_ux.my_hr_attendance -msgid "My Attendances" -msgstr "" - #. module: hr_timesheet_attendance_ux #: model:ir.actions.act_window,name:hr_timesheet_attendance_ux.action_hr_timesheet_attendance_report_user_week #: model:ir.ui.menu,name:hr_timesheet_attendance_ux.menu_hr_timesheet_attendance_report_user_week msgid "My Timesheet / Attendance" msgstr "" -#. module: hr_timesheet_attendance_ux -#: model_terms:ir.actions.act_window,help:hr_timesheet_attendance_ux.hr_attendance_action -msgid "" -"Please make sure you're using the correct filter if you expected to see any." -msgstr "" - -#. module: hr_timesheet_attendance_ux -#: model:ir.model.fields,field_description:hr_timesheet_attendance_ux.field_hr_attendance__current_worked_hours -msgid "Service Hours" -msgstr "" - #. module: hr_timesheet_attendance_ux #: model:ir.model.fields,field_description:hr_timesheet_attendance_ux.field_account_analytic_line__task_id msgid "Task" msgstr "" - -#. module: hr_timesheet_attendance_ux -#: model_terms:ir.ui.view,arch_db:hr_timesheet_attendance_ux.view_attendance_tree -msgid "Total service" -msgstr "" - -#. module: hr_timesheet_attendance_ux -#: model_terms:ir.actions.act_window,help:hr_timesheet_attendance_ux.hr_attendance_action -msgid "Your attendance records will be displayed here." -msgstr "" From 3e2bac24f34fb2c53589141056f88e09c9df4e55 Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Thu, 29 Oct 2026 17:41:15 -0300 Subject: [PATCH 6/8] [FIX] hr_timesheet_attendance_ux: do not offer the time off task on timesheets The module replaces the task_id domain, and with it the condition ('is_timeoff_task', '=', False) that project_timesheet_holidays adds (auto installed with hr_timesheet and hr_holidays). The module loads after it, so the time off task was offered, and saving a timesheet on it raised a UserError. Already the case in 19.0. The domain is now built by a method, which adds the condition only when project_timesheet_holidays is installed. --- .../models/account_analytic_line.py | 14 +++++++++++--- .../tests/test_hr_timesheet_attendance_ux.py | 17 ++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/hr_timesheet_attendance_ux/models/account_analytic_line.py b/hr_timesheet_attendance_ux/models/account_analytic_line.py index 1f2256a6..03243060 100644 --- a/hr_timesheet_attendance_ux/models/account_analytic_line.py +++ b/hr_timesheet_attendance_ux/models/account_analytic_line.py @@ -6,9 +6,17 @@ class AccountAnalyticLine(models.Model): _inherit = "account.analytic.line" - task_id = fields.Many2one( - domain="[('company_id', '=', company_id), ('project_id.allow_timesheets', '=', True), ('project_id', '=?', project_id), ('stage_id.fold', '=', False), ('has_template_ancestor', '=', False)]" - ) + def _domain_task_id(self): + domain = ( + "[('company_id', '=', company_id), ('project_id.allow_timesheets', '=', True), " + "('project_id', '=?', project_id), ('stage_id.fold', '=', False), ('has_template_ancestor', '=', False)" + ) + # Keep the condition that project_timesheet_holidays adds to the domain this field replaces. + if "is_timeoff_task" in self.env["project.task"]._fields: + domain += ", ('is_timeoff_task', '=', False)" + return domain + "]" + + task_id = fields.Many2one(domain=_domain_task_id) @api.onchange("employee_id", "date") def onchange_compute_hours(self): diff --git a/hr_timesheet_attendance_ux/tests/test_hr_timesheet_attendance_ux.py b/hr_timesheet_attendance_ux/tests/test_hr_timesheet_attendance_ux.py index 9e3ca3f0..7cc74cf0 100644 --- a/hr_timesheet_attendance_ux/tests/test_hr_timesheet_attendance_ux.py +++ b/hr_timesheet_attendance_ux/tests/test_hr_timesheet_attendance_ux.py @@ -82,11 +82,18 @@ def test_task_domain_hides_folded_and_template_tasks(self): "is_template": True, } ) - domain = safe_eval( - self.env["account.analytic.line"]._fields["task_id"].domain, - {"company_id": self.project_customer.company_id.id, "project_id": self.project_customer.id}, - ) - available = Task.search(domain) + domain = self.env["account.analytic.line"]._fields["task_id"]._description_domain(self.env) + company_id = self.project_customer.company_id.id + available = Task.search(safe_eval(domain, {"company_id": company_id, "project_id": self.project_customer.id})) self.assertIn(open_task, available) self.assertNotIn(folded_task, available) self.assertNotIn(template_task, available) + + with self.subTest("time off task is not offered"): + if "is_timeoff_task" not in Task._fields: + self.skipTest("project_timesheet_holidays is not installed") + timeoff_task = self.env.company.leave_timesheet_task_id + self.assertTrue(timeoff_task) + available = Task.search(safe_eval(domain, {"company_id": company_id, "project_id": False})) + self.assertNotIn(timeoff_task, available) + self.assertTrue(available, "the domain still offers other tasks") From 86c09440b911fee28206e46dc7f9e680cb6a655e Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Thu, 29 Oct 2026 18:29:17 -0300 Subject: [PATCH 7/8] [FIX] hr_timesheet_attendance_ux: offer tasks of projects without company The task domain had ('company_id', '=', company_id), so a timesheet did not offer the tasks of a project without company, which is valid. The native domain has no company condition (and task_id has no check_company), so the module now follows it. Already the case in 19.0. --- .../models/account_analytic_line.py | 2 +- .../tests/test_hr_timesheet_attendance_ux.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/hr_timesheet_attendance_ux/models/account_analytic_line.py b/hr_timesheet_attendance_ux/models/account_analytic_line.py index 03243060..0ff13ea7 100644 --- a/hr_timesheet_attendance_ux/models/account_analytic_line.py +++ b/hr_timesheet_attendance_ux/models/account_analytic_line.py @@ -8,7 +8,7 @@ class AccountAnalyticLine(models.Model): def _domain_task_id(self): domain = ( - "[('company_id', '=', company_id), ('project_id.allow_timesheets', '=', True), " + "[('project_id.allow_timesheets', '=', True), " "('project_id', '=?', project_id), ('stage_id.fold', '=', False), ('has_template_ancestor', '=', False)" ) # Keep the condition that project_timesheet_holidays adds to the domain this field replaces. diff --git a/hr_timesheet_attendance_ux/tests/test_hr_timesheet_attendance_ux.py b/hr_timesheet_attendance_ux/tests/test_hr_timesheet_attendance_ux.py index 7cc74cf0..4f747002 100644 --- a/hr_timesheet_attendance_ux/tests/test_hr_timesheet_attendance_ux.py +++ b/hr_timesheet_attendance_ux/tests/test_hr_timesheet_attendance_ux.py @@ -89,6 +89,15 @@ def test_task_domain_hides_folded_and_template_tasks(self): self.assertNotIn(folded_task, available) self.assertNotIn(template_task, available) + with self.subTest("task of a project without company is offered"): + project = self.env["project.project"].create( + {"name": "No company", "allow_timesheets": True, "company_id": False} + ) + task = Task.create({"name": "No company task", "project_id": project.id}) + self.assertFalse(task.company_id) + available = Task.search(safe_eval(domain, {"company_id": company_id, "project_id": project.id})) + self.assertIn(task, available) + with self.subTest("time off task is not offered"): if "is_timeoff_task" not in Task._fields: self.skipTest("project_timesheet_holidays is not installed") From c494e32798bfb808c436e18d5824cbaf30064227 Mon Sep 17 00:00:00 2001 From: les-adhoc Date: Wed, 11 Nov 2026 10:51:07 -0300 Subject: [PATCH 8/8] [IMP] hr_timesheet_attendance_ux: own attendance report and refused attendances Product owner decisions for the migration to 20.0: - In 20.0 hr.timesheet.attendance.report is readable only by timesheet managers, so the "My Timesheet / Attendance" pivot disappeared for timesheet users and approvers. Give timesheet users read access to their own lines again, with the domain of the 19.0 user rule ([('employee_id', '=', user.employee_id.id)]). Approvers and managers get it through their implied groups. - Attendances have a state in 20.0. The hours suggestion of a timesheet leaves the refused ones out, as the core does for the time rules. Tests cover both, and each one was seen failing with the change undone. README and translation terms updated. --- hr_timesheet_attendance_ux/README.rst | 9 ++--- hr_timesheet_attendance_ux/__manifest__.py | 1 + hr_timesheet_attendance_ux/i18n/es.po | 13 +++---- .../i18n/hr_timesheet_attendance_ux.pot | 13 +++---- .../models/account_analytic_line.py | 1 + .../security/ir.access.csv | 2 ++ .../tests/test_hr_timesheet_attendance_ux.py | 34 +++++++++++++++++++ 7 files changed, 57 insertions(+), 16 deletions(-) create mode 100644 hr_timesheet_attendance_ux/security/ir.access.csv diff --git a/hr_timesheet_attendance_ux/README.rst b/hr_timesheet_attendance_ux/README.rst index 839201bf..6cc7ad97 100644 --- a/hr_timesheet_attendance_ux/README.rst +++ b/hr_timesheet_attendance_ux/README.rst @@ -14,13 +14,14 @@ Attendance Timesheet UX ======================= -#. Add link from timesheets to timesheet vs attendance analysis +#. Add link from timesheets to timesheet vs attendance analysis. Timesheet + users can read their own lines of that analysis. #. Timesheets/All Timesheets odoo menu is set visible only in developer mode. #. When you set the employee or the date of a timesheet, the time spent is suggested: the attendance hours of that day in the user's timezone (open - attendances count up to now), minus the break, minus the time already - logged that day. Timesheets from time off requests are not counted as - already logged. + attendances count up to now, refused ones are left out), minus the break, + minus the time already logged that day. Timesheets from time off requests + are not counted as already logged. #. The task selector of a timesheet does not offer tasks in a folded stage or template tasks. diff --git a/hr_timesheet_attendance_ux/__manifest__.py b/hr_timesheet_attendance_ux/__manifest__.py index 8880ad55..a2636b57 100644 --- a/hr_timesheet_attendance_ux/__manifest__.py +++ b/hr_timesheet_attendance_ux/__manifest__.py @@ -30,6 +30,7 @@ "hr_timesheet_attendance", ], "data": [ + "security/ir.access.csv", "views/account_analytic_line_views.xml", "views/hr_timesheet_attendance_report_views.xml", ], diff --git a/hr_timesheet_attendance_ux/i18n/es.po b/hr_timesheet_attendance_ux/i18n/es.po index 7785a65c..623fe432 100644 --- a/hr_timesheet_attendance_ux/i18n/es.po +++ b/hr_timesheet_attendance_ux/i18n/es.po @@ -9,8 +9,8 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 20.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-10-29 20:16+0000\n" -"PO-Revision-Date: 2026-10-29 20:16+0000\n" +"POT-Creation-Date: 2026-11-11 13:47+0000\n" +"PO-Revision-Date: 2026-11-11 13:47+0000\n" "Last-Translator: BAF \n" "Language-Team: Spanish \n" "Language: es\n" @@ -39,13 +39,14 @@ msgid "" "Attendance Timesheet UX\n" "=======================\n" "\n" -"#. Add link from timesheets to timesheet vs attendance analysis\n" +"#. Add link from timesheets to timesheet vs attendance analysis. Timesheet\n" +" users can read their own lines of that analysis.\n" "#. Timesheets/All Timesheets odoo menu is set visible only in developer mode.\n" "#. When you set the employee or the date of a timesheet, the time spent is\n" " suggested: the attendance hours of that day in the user's timezone (open\n" -" attendances count up to now), minus the break, minus the time already\n" -" logged that day. Timesheets from time off requests are not counted as\n" -" already logged.\n" +" attendances count up to now, refused ones are left out), minus the break,\n" +" minus the time already logged that day. Timesheets from time off requests\n" +" are not counted as already logged.\n" "#. The task selector of a timesheet does not offer tasks in a folded stage or\n" " template tasks.\n" "\n" diff --git a/hr_timesheet_attendance_ux/i18n/hr_timesheet_attendance_ux.pot b/hr_timesheet_attendance_ux/i18n/hr_timesheet_attendance_ux.pot index b6ce1d68..627c8753 100644 --- a/hr_timesheet_attendance_ux/i18n/hr_timesheet_attendance_ux.pot +++ b/hr_timesheet_attendance_ux/i18n/hr_timesheet_attendance_ux.pot @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 20.0+e\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-10-29 20:16+0000\n" -"PO-Revision-Date: 2026-10-29 20:16+0000\n" +"POT-Creation-Date: 2026-11-11 13:47+0000\n" +"PO-Revision-Date: 2026-11-11 13:47+0000\n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -34,13 +34,14 @@ msgid "" "Attendance Timesheet UX\n" "=======================\n" "\n" -"#. Add link from timesheets to timesheet vs attendance analysis\n" +"#. Add link from timesheets to timesheet vs attendance analysis. Timesheet\n" +" users can read their own lines of that analysis.\n" "#. Timesheets/All Timesheets odoo menu is set visible only in developer mode.\n" "#. When you set the employee or the date of a timesheet, the time spent is\n" " suggested: the attendance hours of that day in the user's timezone (open\n" -" attendances count up to now), minus the break, minus the time already\n" -" logged that day. Timesheets from time off requests are not counted as\n" -" already logged.\n" +" attendances count up to now, refused ones are left out), minus the break,\n" +" minus the time already logged that day. Timesheets from time off requests\n" +" are not counted as already logged.\n" "#. The task selector of a timesheet does not offer tasks in a folded stage or\n" " template tasks.\n" "\n" diff --git a/hr_timesheet_attendance_ux/models/account_analytic_line.py b/hr_timesheet_attendance_ux/models/account_analytic_line.py index 0ff13ea7..07bb2545 100644 --- a/hr_timesheet_attendance_ux/models/account_analytic_line.py +++ b/hr_timesheet_attendance_ux/models/account_analytic_line.py @@ -31,6 +31,7 @@ def onchange_compute_hours(self): ("employee_id", "=", employee_id), ("check_in", ">=", rec.date), ("check_in", "<=", end), + ("state", "!=", "refused"), ] ) domain = [("employee_id", "=", employee_id), ("date", "=", rec.date)] diff --git a/hr_timesheet_attendance_ux/security/ir.access.csv b/hr_timesheet_attendance_ux/security/ir.access.csv new file mode 100644 index 00000000..8df69573 --- /dev/null +++ b/hr_timesheet_attendance_ux/security/ir.access.csv @@ -0,0 +1,2 @@ +id,name,model_id,group_id/id,operation,domain +access_hr_timesheet_attendance_report_user,Timesheet attendance Report: own,hr.timesheet.attendance.report,hr_timesheet.group_hr_timesheet_user,r,"[('employee_id', '=', user.employee_id.id)]" diff --git a/hr_timesheet_attendance_ux/tests/test_hr_timesheet_attendance_ux.py b/hr_timesheet_attendance_ux/tests/test_hr_timesheet_attendance_ux.py index 4f747002..5cd1cac2 100644 --- a/hr_timesheet_attendance_ux/tests/test_hr_timesheet_attendance_ux.py +++ b/hr_timesheet_attendance_ux/tests/test_hr_timesheet_attendance_ux.py @@ -51,6 +51,20 @@ def test_suggested_hours_discount_break_and_logged_time(self): self.assertEqual(line.employee_id, self.empl_employee) self.assertEqual(line.date, self.day) + with self.subTest("refused attendance is left out"): + line.unlink() + refused = self.env["hr.attendance"].create( + { + "employee_id": self.empl_employee.id, + "check_in": datetime.combine(self.day, time(18)), + "check_out": datetime.combine(self.day, time(20)), + } + ) + refused.action_refuse() + self.assertEqual(refused.state, "refused") + form = self._new_timesheet_form(self.day) + self.assertAlmostEqual(form.unit_amount, 5.5) + with self.subTest("open attendance counts up to now"): self.env["hr.attendance"].search([("employee_id", "=", self.empl_employee.id)]).unlink() with freeze_time(datetime.combine(self.today, time(15))): @@ -106,3 +120,23 @@ def test_task_domain_hides_folded_and_template_tasks(self): available = Task.search(safe_eval(domain, {"company_id": company_id, "project_id": False})) self.assertNotIn(timeoff_task, available) self.assertTrue(available, "the domain still offers other tasks") + + def test_timesheet_user_reads_own_attendance_report(self): + for employee in self.empl_employee | self.empl_employee2: + self.env["hr.attendance"].create( + { + "employee_id": employee.id, + "check_in": datetime.combine(self.day, time(9)), + "check_out": datetime.combine(self.day, time(17)), + } + ) + # The report is an SQL view: write the computed worked hours first. + self.env.flush_all() + Report = self.env["hr.timesheet.attendance.report"] + self.assertEqual( + Report.search([("employee_id", "in", (self.empl_employee | self.empl_employee2).ids)]).employee_id, + self.empl_employee | self.empl_employee2, + ) + own = Report.with_user(self.user_employee).search([]) + self.assertEqual(own.employee_id, self.empl_employee) + self.assertAlmostEqual(sum(own.mapped("total_attendance")), 8.0)