Skip to content
Open
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
136 changes: 136 additions & 0 deletions hr_work_entry_timesheet/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
==========================
Work Entry with timesheets
==========================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:96eb505415cddcb1b2c8c641f493ff97424f3420d941266623c9a92dddda19c1
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
:target: https://odoo-community.org/page/development-status
:alt: Alpha
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fhr-lightgray.png?logo=github
:target: https://github.com/OCA/hr/tree/18.0/hr_work_entry_timesheet
:alt: OCA/hr
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/hr-18-0/hr-18-0-hr_work_entry_timesheet
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/hr&target_branch=18.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module extends the functionality of hr_work_entry_contract in order
to display corresponding timesheet duration on work entries.

Also a check is made for discrepancy and the work entries are displayed
hatched on calendar view in case :

1. no timesheet has been recorded on some day (assuming that leaves also
create timesheets with native Odoo module project_timesheet_holidays)
2. more hours have been recorded than duration of the work entry

.. IMPORTANT::
This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
`More details on development status <https://odoo-community.org/page/development-status>`_

**Table of contents**

.. contents::
:local:

Use Cases / Context
===================

In France, when your employes have a contract with a specific number of
days worked per year, you need to have every month the count of days
worked per employee. hr_work_entry_contract module from Odoo can be used
to cover this need.

However, the above native Odoo module shows you the work entry per type,
depending on contract and leaves.

From time to time, leaves are not up to date (somebody worked a few
hours one day on which he should be on leave for instance). Based on
timesheets, this module proposes to retrieve existing timesheets and
display those on work entries.

Usage
=====

- Go to *Employees*

- On an "Employee" form view, click on smart-button "Work entries"

- In the calendar view for work entries, you get :

- hatched entries when discrepancy are found
- timesheet duration on card view (when clicking on an entry)

- Timesheet duration has also been added on Work entries form, tree and
pivot views.

Known issues / Roadmap
======================

- In case resource calendar is using half-day attendances you get 2
work entries per day, when timesheets are per day, so we do not know
on which work entry the timesheets should be attributed. So far they
are divided by number of work entries for the day.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/hr/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/hr/issues/new?body=module:%20hr_work_entry_timesheet%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* Le Filament

Contributors
------------

- Rémi remi-filament (https://le-filament.com)

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

.. |maintainer-remi-filament| image:: https://github.com/remi-filament.png?size=40px
:target: https://github.com/remi-filament
:alt: remi-filament

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-remi-filament|

This module is part of the `OCA/hr <https://github.com/OCA/hr/tree/18.0/hr_work_entry_timesheet>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions hr_work_entry_timesheet/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
20 changes: 20 additions & 0 deletions hr_work_entry_timesheet/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "Work Entry with timesheets",
"version": "18.0.1.0.0",
"development_status": "Alpha",
"category": "Human Resources/Employees",
"website": "https://github.com/OCA/hr",
"author": "Le Filament, Odoo Community Association (OCA)",
"maintainers": ["remi-filament"],
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": [
"hr_timesheet",
"hr_work_entry_contract",
"project_timesheet_holidays",
],
"data": [
"views/hr_work_entry_view.xml",
],
}
36 changes: 36 additions & 0 deletions hr_work_entry_timesheet/i18n/fr.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * hr_work_entry_timesheet
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-27 09:58+0000\n"
"PO-Revision-Date: 2025-05-27 09:58+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: hr_work_entry_timesheet
#: model:ir.model,name:hr_work_entry_timesheet.model_account_analytic_line
msgid "Analytic Line"
msgstr "Ligne analytique"

#. module: hr_work_entry_timesheet
#: model:ir.model,name:hr_work_entry_timesheet.model_hr_work_entry
msgid "HR Work Entry"
msgstr "Prestation RH"

#. module: hr_work_entry_timesheet
#: model:ir.model.fields,field_description:hr_work_entry_timesheet.field_hr_work_entry__is_hatched
msgid "Timesheet Conflict"
msgstr "Conflit de feuille de temps"

#. module: hr_work_entry_timesheet
#: model:ir.model.fields,field_description:hr_work_entry_timesheet.field_hr_work_entry__timesheet_duration
msgid "Timesheet Duration"
msgstr "Durée de feuilles de temps"
36 changes: 36 additions & 0 deletions hr_work_entry_timesheet/i18n/hr_work_entry_timesheet.pot
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * hr_work_entry_timesheet
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-27 09:53+0000\n"
"PO-Revision-Date: 2025-05-27 09:53+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: hr_work_entry_timesheet
#: model:ir.model,name:hr_work_entry_timesheet.model_account_analytic_line
msgid "Analytic Line"
msgstr ""

#. module: hr_work_entry_timesheet
#: model:ir.model,name:hr_work_entry_timesheet.model_hr_work_entry
msgid "HR Work Entry"
msgstr ""

#. module: hr_work_entry_timesheet
#: model:ir.model.fields,field_description:hr_work_entry_timesheet.field_hr_work_entry__is_hatched
msgid "Timesheet Conflict"
msgstr ""

#. module: hr_work_entry_timesheet
#: model:ir.model.fields,field_description:hr_work_entry_timesheet.field_hr_work_entry__timesheet_duration
msgid "Timesheet Duration"
msgstr ""
2 changes: 2 additions & 0 deletions hr_work_entry_timesheet/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import hr_work_entry
from . import hr_timesheet
52 changes: 52 additions & 0 deletions hr_work_entry_timesheet/models/hr_timesheet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright 2025- Le Filament (https://le-filament.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from dateutil.relativedelta import relativedelta

from odoo import api, models


class AccountAnalyticLine(models.Model):
_inherit = "account.analytic.line"

def _get_work_entry(self):
work_entries = self.env["hr.work.entry"]
for timesheet in self.filtered(
lambda line: line.project_id and line.employee_id
):
work_entries += work_entries.search(
[
("employee_id", "=", timesheet.employee_id.id),
("date_start", ">=", timesheet.date),
("date_start", "<", timesheet.date + relativedelta(days=1)),
]
)
return work_entries

@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
timesheets = res.filtered("project_id")
if not timesheets:
return res
work_entries = timesheets._get_work_entry()
if work_entries:
work_entries._compute_timesheet_duration()
return res

def write(self, vals):
res = super().write(vals)
if ("unit_amount" in vals or "employee_id" in vals or "date" in vals) and (
self.filtered("project_id") or "project_id" in vals
):
self._get_work_entry()._compute_timesheet_duration()
return res

def unlink(self):
work_entries = self.env["hr.work.entry"]
timesheets = self.filtered("project_id")
if timesheets:
work_entries = timesheets._get_work_entry()
res = super().unlink()
if timesheets:
work_entries._compute_timesheet_duration()
return res
104 changes: 104 additions & 0 deletions hr_work_entry_timesheet/models/hr_work_entry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Copyright 2025- Le Filament (https://le-filament.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

import pytz

from odoo import api, fields, models


class HrWorkEntry(models.Model):
_inherit = "hr.work.entry"

timesheet_duration = fields.Float(compute="_compute_timesheet_duration", store=True)
is_hatched = fields.Boolean(
compute="_compute_timesheet_conflict",
string="Timesheet Conflict",
store=True,
readonly=False,
)

@api.depends("date_start", "employee_id")
def _compute_timesheet_duration(self):
if not self:
return
min_datetime = min(self.mapped("date_start"))
max_datetime = max(self.mapped("date_stop"))
dates = self.mapped(
lambda work_entry: pytz.UTC.localize(work_entry.date_start)
.astimezone(pytz.timezone(work_entry.employee_id.tz))
.replace(tzinfo=None)
.date()
)
min_date = min(dates)
max_date = max(dates)
employee_ids = self.mapped("employee_id").ids
timesheets = (
self.env["account.analytic.line"]
.with_context(tz="UTC")
.read_group(
domain=[
("project_id", "!=", False),
("employee_id", "in", employee_ids),
("date", ">=", min_date),
("date", "<=", max_date),
],
fields=["unit_amount"],
groupby=["employee_id", "date:day"],
lazy=False,
)
)
timesheet_dict = {eid: {} for eid in employee_ids}
for line in timesheets:
date = fields.Date().from_string(line["__range"]["date:day"]["from"])
timesheet_dict[line["employee_id"][0]][date] = line["unit_amount"]
work_entries_dict = {eid: {} for eid in employee_ids}
for employee_id in employee_ids:
employee = self.env["hr.employee"].browse(employee_id)
work_entries = (
self.env["hr.work.entry"]
.with_context(tz=employee.tz)
.read_group(
domain=[
("employee_id", "=", employee_id),
("date_start", ">=", min_datetime),
("date_stop", "<=", max_datetime),
],
fields=[],
groupby=["date_start:day"],
)
)
work_entries_dict[employee_id] = {
pytz.UTC.localize(
fields.Datetime().from_string(
line["__range"]["date_start:day"]["from"]
)
)
.astimezone(pytz.timezone(employee.tz))
.replace(tzinfo=None)
.date(): line["date_start_count"]
for line in work_entries
}
for work_entry in self:
date_start = (
pytz.UTC.localize(work_entry.date_start)
.astimezone(pytz.timezone(work_entry.employee_id.tz))
.replace(tzinfo=None)
.date()
)
timesheet_duration = timesheet_dict[work_entry.employee_id.id].get(
date_start, 0.0
)
work_entry_count = work_entries_dict[work_entry.employee_id.id].get(
date_start, 0
)
work_entry.timesheet_duration = (
timesheet_duration / work_entry_count if work_entry_count else 0.0
)

@api.depends("duration", "timesheet_duration")
def _compute_timesheet_conflict(self):
for entry in self:
entry.is_hatched = entry.duration > 0.0 and (
entry.timesheet_duration == 0.0
or entry.timesheet_duration > entry.duration
)
3 changes: 3 additions & 0 deletions hr_work_entry_timesheet/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
Loading