Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions hr_timesheet_groupby_task_origin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright Numigi 2025 (tm) and all its contributors
# (https://numigi.com/r/home)
# License LGPL-3.0 or later
# (http://www.gnu.org/licenses/lgpl).

from . import models # noqa: F401
43 changes: 43 additions & 0 deletions hr_timesheet_groupby_task_origin/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright Numigi 2025 (tm) and all its contributors
# (https://numigi.com/r/home)
# License LGPL-3.0 or later
# (http://www.gnu.org/licenses/lgpl).

{

Check notice on line 6 in hr_timesheet_groupby_task_origin/__manifest__.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

hr_timesheet_groupby_task_origin/__manifest__.py#L6

Statement seems to have no effect
'name': 'HR Timesheet Group by Task Origin',
'version': '14.0.1.0.0',
'category': 'Human Resources',
'summary': 'Group timesheet lines by origin task instead of current task',
'description': """
HR Timesheet Group by Task Origin
=================================

This module fixes the inconsistent task grouping behavior between
Accounting and Project applications for analytic lines.

Issue:
--------
- In Accounting app, task grouping uses origin_task_id field
- In Project app (Costs/Revenues), task grouping uses task_id field
- This causes inconsistent grouping results between the two apps

Solution:
---------
- Modifies the task grouping in Project app's Costs/Revenues view to
use origin_task_id instead of task_id
- Aligns the grouping behavior with Accounting app
- Ensures consistent analytic reporting across applications
""",
'author': 'Numigi',
'website': 'https://www.numigi.com',
'depends': [
'hr_timesheet',
'project'
],
'data': [
'views/hr_timesheet_views.xml',
],
'installable': True,
'application': False,
'license': 'LGPL-3',
}
70 changes: 70 additions & 0 deletions hr_timesheet_groupby_task_origin/i18n/fr.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * timesheet_confirm_reset
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-21 18:19+0000\n"
"PO-Revision-Date: 2025-10-21 18:19+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"

#. module: timesheet_confirm_reset
#: model_terms:ir.ui.view,arch_db:timesheet_confirm_reset.view_hr_timesheet_sheet_form_inherit_submit_confirm
msgid "Are you sure you want to submit this timesheet?"
msgstr "Êtes-vous sûr de vouloir soumettre cette feuille de temps ?"

#. module: timesheet_confirm_reset
#: model:ir.model.fields,field_description:timesheet_confirm_reset.field_hr_timesheet_sheet__display_name
msgid "Display Name"
msgstr "Nom affiché"

#. module: timesheet_confirm_reset
#: code:addons/timesheet_confirm_reset/models/hr_timesheet_sheet.py:0
#, python-format
msgid "Error: You cannot reset a timesheet that has already been approved."
msgstr "Erreur : vous ne pouvez pas remettre en brouillon une feuille de temps déjà approuvée."

#. module: timesheet_confirm_reset
#: model:ir.model.fields,field_description:timesheet_confirm_reset.field_hr_timesheet_sheet__id
msgid "ID"
msgstr "ID"

#. module: timesheet_confirm_reset
#: model:ir.model.fields,field_description:timesheet_confirm_reset.field_hr_timesheet_sheet____last_update
msgid "Last Modified on"
msgstr "Dernière modification le"

#. module: timesheet_confirm_reset
#: model_terms:ir.ui.view,arch_db:timesheet_confirm_reset.view_hr_timesheet_sheet_form_inherit_submit_confirm
msgid "Reset to Draft"
msgstr "Remettre en brouillon"

#. module: timesheet_confirm_reset
#: code:addons/timesheet_confirm_reset/models/hr_timesheet_sheet.py:0
#, python-format
msgid "The timesheet is already in draft state."
msgstr "La feuille de temps est déjà en état brouillon."

#. module: timesheet_confirm_reset
#: model:ir.model,name:timesheet_confirm_reset.model_hr_timesheet_sheet
msgid "Timesheet Sheet"
msgstr "Feuille de présence"

#. module: timesheet_confirm_reset
#: code:addons/timesheet_confirm_reset/models/hr_timesheet_sheet.py:0
#, python-format
msgid "Timesheet not found."
msgstr "Feuille de temps introuvable."

#. module: timesheet_confirm_reset
#: code:addons/timesheet_confirm_reset/models/hr_timesheet_sheet.py:0
#, python-format
msgid "You can only reset your own timesheets."
msgstr "Vous ne pouvez remettre en brouillon que vos propres feuilles de temps."
6 changes: 6 additions & 0 deletions hr_timesheet_groupby_task_origin/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright Numigi 2025 (tm) and all its contributors
# (https://numigi.com/r/home)
# License LGPL-3.0 or later
# (http://www.gnu.org/licenses/lgpl).

from . import hr_timesheet_sheet # noqa: F401
42 changes: 42 additions & 0 deletions hr_timesheet_groupby_task_origin/models/hr_timesheet_sheet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright Numigi 2025 (tm) and all its contributors
# (https://numigi.com/r/home)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

from odoo import models, _
from odoo.exceptions import UserError


class HrTimesheetSheet(models.Model):
_inherit = 'hr_timesheet.sheet'

def action_confirm_reset(self):
"""
Allow the owner (employee's user) to reset a submitted sheet back
to draft, provided it is not approved. Raises errors if the sheet
is already approved or modified concurrently.
"""
self.ensure_one()
current_uid = self.env.uid
if (
self.user_id.id != current_uid
and not self.env.user.has_group(
'hr_timesheet.group_hr_timesheet_user'
)
):
raise UserError(_('You can only reset your own timesheets.'))

timesheet_id = self.with_context(
prefetch_fields=False).sudo().browse(self.id)
if not timesheet_id:
raise UserError(_('Timesheet not found.'))

if timesheet_id.state == 'done':
raise UserError(_(
'Error: You cannot reset a timesheet that has already '
'been approved.'
))

if timesheet_id.state in ('draft', False):
raise UserError(_('The timesheet is already in draft state.'))

timesheet_id.sudo().write({'state': 'draft'})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions hr_timesheet_groupby_task_origin/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# © Numigi (tm) and all its contributors (https://numigi.com/r/home)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

from . import test_hr_timesheet_sheet # noqa: F401
80 changes: 80 additions & 0 deletions hr_timesheet_groupby_task_origin/tests/test_hr_timesheet_sheet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# © Numigi 2025 (tm) and all its contributors
# (https://numigi.com/r/home)
# License LGPL-3.0 or later
# (http://www.gnu.org/licenses/lgpl).

from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError


class TestHrTimesheetSheetConfirmReset(TransactionCase):

def setUp(self):
super().setUp()

# Create employee user
self.employee_user = self.env['res.users'].create({
'name': 'Test Employee',
'login': 'employee_test',
})

# Assign hr_timesheet_user group
group = self.env.ref('hr_timesheet.group_hr_timesheet_user')
group.users = [(4, self.employee_user.id)]

# Create employee linked to user
self.employee = self.env['hr.employee'].create({
'name': 'Test Employee',
'user_id': self.employee_user.id,
})

# Create reviewer user
self.reviewer_user = self.env['res.users'].create({
'name': 'Reviewer',
'login': 'reviewer_test',
})

# Assign timesheet approver group
reviewer_group = self.env.ref(
'hr_timesheet.group_hr_timesheet_approver'
)
reviewer_group.users = [(4, self.reviewer_user.id)]

# Create reviewer employee
self.reviewer = self.env['hr.employee'].create({
'name': 'Reviewer',
'user_id': self.reviewer_user.id,
})

# Create timesheet in 'confirm' state with reviewer
self.timesheet = self.env['hr_timesheet.sheet'].sudo().create({
'name': 'Test Timesheet',
'employee_id': self.employee.id,
'reviewer_id': self.reviewer.id,
'state': 'draft',
})

def test_reset_to_draft_success(self):
"""Employee resets their own submitted sheet to draft."""
self.timesheet = self.timesheet.with_user(self.employee_user)
# Submit the sheet
self.timesheet.action_timesheet_confirm()
# Reset to draft
self.timesheet.action_confirm_reset()
self.assertEqual(self.timesheet.state, 'draft')

def test_reset_to_draft_approved_error(self):
"""Resetting an approved timesheet raises an error."""
# Approve the sheet
self.timesheet.reviewer_id = self.reviewer.id
self.timesheet = self.timesheet.with_user(self.reviewer_user)
self.timesheet.action_timesheet_confirm()
self.timesheet.action_timesheet_done()
# Employee tries to reset approved sheet
self.timesheet = self.timesheet.with_user(self.employee_user)
with self.assertRaises(UserError) as e:
self.timesheet.action_confirm_reset()
self.assertIn(
'You cannot reset a timesheet that has already been approved',
str(e.exception)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_hr_timesheet_sheet_form_inherit_submit_confirm" model="ir.ui.view">
<field name="name">hr.timesheet.sheet.form.inherit.submit.confirm</field>
<field name="model">hr_timesheet.sheet</field>
<field name="inherit_id" ref="hr_timesheet_sheet.hr_timesheet_sheet_form"/>
<field name="arch" type="xml">
<!-- Add a confirm message to the action_timesheet_confirm -->
<xpath expr="//button[@name='action_timesheet_confirm']" position="attributes">
<attribute name="confirm">Are you sure you want to submit this timesheet?</attribute>
</xpath>

<!-- Add Reset to Draft button for owner users -->
<xpath expr="//header" position="inside">
<button name="action_confirm_reset"
type="object"
string="Reset to Draft"
groups="hr_timesheet.group_hr_timesheet_user"
attrs="{'invisible': [('state','!=', 'confirm')]}"/>
</xpath>
</field>
</record>
</odoo>
2 changes: 1 addition & 1 deletion timesheet_sheet_confirm_reset/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

{
'name': 'Timesheet Confirm Reset',
'version': '14.0.1.0.0',
'version': '14.0.1.0.1',
'summary': 'Allow user to reset submitted timesheet to draft',
'description': """
This module implements:
Expand Down
44 changes: 22 additions & 22 deletions timesheet_sheet_confirm_reset/i18n/fr.po
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * timesheet_confirm_reset
# * timesheet_sheet_confirm_reset
#
msgid ""
msgstr ""
Expand All @@ -15,56 +15,56 @@ msgstr ""
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"

#. module: timesheet_confirm_reset
#: model_terms:ir.ui.view,arch_db:timesheet_confirm_reset.view_hr_timesheet_sheet_form_inherit_submit_confirm
#. module: timesheet_sheet_confirm_reset
#: model_terms:ir.ui.view,arch_db:timesheet_sheet_confirm_reset.view_hr_timesheet_sheet_form_inherit_submit_confirm
msgid "Are you sure you want to submit this timesheet?"
msgstr "Êtes-vous sûr de vouloir soumettre cette feuille de temps ?"
msgstr "Êtes-vous sûr de vouloir soumettre cette feuille de temps?"

#. module: timesheet_confirm_reset
#: model:ir.model.fields,field_description:timesheet_confirm_reset.field_hr_timesheet_sheet__display_name
#. module: timesheet_sheet_confirm_reset
#: model:ir.model.fields,field_description:timesheet_sheet_confirm_reset.field_hr_timesheet_sheet__display_name
msgid "Display Name"
msgstr "Nom affiché"

#. module: timesheet_confirm_reset
#: code:addons/timesheet_confirm_reset/models/hr_timesheet_sheet.py:0
#. module: timesheet_sheet_confirm_reset
#: code:addons/timesheet_sheet_confirm_reset/models/hr_timesheet_sheet.py:0
#, python-format
msgid "Error: You cannot reset a timesheet that has already been approved."
msgstr "Erreur : vous ne pouvez pas remettre en brouillon une feuille de temps déjà approuvée."

#. module: timesheet_confirm_reset
#: model:ir.model.fields,field_description:timesheet_confirm_reset.field_hr_timesheet_sheet__id
#. module: timesheet_sheet_confirm_reset
#: model:ir.model.fields,field_description:timesheet_sheet_confirm_reset.field_hr_timesheet_sheet__id
msgid "ID"
msgstr "ID"

#. module: timesheet_confirm_reset
#: model:ir.model.fields,field_description:timesheet_confirm_reset.field_hr_timesheet_sheet____last_update
#. module: timesheet_sheet_confirm_reset
#: model:ir.model.fields,field_description:timesheet_sheet_confirm_reset.field_hr_timesheet_sheet____last_update
msgid "Last Modified on"
msgstr "Dernière modification le"

#. module: timesheet_confirm_reset
#: model_terms:ir.ui.view,arch_db:timesheet_confirm_reset.view_hr_timesheet_sheet_form_inherit_submit_confirm
#. module: timesheet_sheet_confirm_reset
#: model_terms:ir.ui.view,arch_db:timesheet_sheet_confirm_reset.view_hr_timesheet_sheet_form_inherit_submit_confirm
msgid "Reset to Draft"
msgstr "Remettre en brouillon"

#. module: timesheet_confirm_reset
#: code:addons/timesheet_confirm_reset/models/hr_timesheet_sheet.py:0
#. module: timesheet_sheet_confirm_reset
#: code:addons/timesheet_sheet_confirm_reset/models/hr_timesheet_sheet.py:0
#, python-format
msgid "The timesheet is already in draft state."
msgstr "La feuille de temps est déjà en état brouillon."

#. module: timesheet_confirm_reset
#: model:ir.model,name:timesheet_confirm_reset.model_hr_timesheet_sheet
#. module: timesheet_sheet_confirm_reset
#: model:ir.model,name:timesheet_sheet_confirm_reset.model_hr_timesheet_sheet
msgid "Timesheet Sheet"
msgstr "Feuille de présence"

#. module: timesheet_confirm_reset
#: code:addons/timesheet_confirm_reset/models/hr_timesheet_sheet.py:0
#. module: timesheet_sheet_confirm_reset
#: code:addons/timesheet_sheet_confirm_reset/models/hr_timesheet_sheet.py:0
#, python-format
msgid "Timesheet not found."
msgstr "Feuille de temps introuvable."

#. module: timesheet_confirm_reset
#: code:addons/timesheet_confirm_reset/models/hr_timesheet_sheet.py:0
#. module: timesheet_sheet_confirm_reset
#: code:addons/timesheet_sheet_confirm_reset/models/hr_timesheet_sheet.py:0
#, python-format
msgid "You can only reset your own timesheets."
msgstr "Vous ne pouvez remettre en brouillon que vos propres feuilles de temps."