diff --git a/hr_timesheet_sheet_activity/README.rst b/hr_timesheet_sheet_activity/README.rst new file mode 100644 index 0000000000..6f7b7e9bb6 --- /dev/null +++ b/hr_timesheet_sheet_activity/README.rst @@ -0,0 +1,87 @@ +============================= +HR Timesheet Sheet Activities +============================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:fe04fe07b7e649beb3b78bb931a004609d534e967cd81dbf3756234698c6784a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |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%2Ftimesheet-lightgray.png?logo=github + :target: https://github.com/OCA/timesheet/tree/16.0/hr_timesheet_sheet_activity + :alt: OCA/timesheet +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/timesheet-16-0/timesheet-16-0-hr_timesheet_sheet_activity + :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/timesheet&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds the following activities related to submission and review of +timesheet sheets: + +* Draft and Submit sheet(s) for review 1 business day prior the end of the period; +* Review sheet(s) within 1 business day after submission day (not earlier than the end of the period); +* Re-submit sheet(s) within 1 business day in case those have been rejected. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* CorporateHub + +Contributors +~~~~~~~~~~~~ + +* `CorporateHub `__ + + * Alexey Pelykh + +* `Ooops404 `__: + + * Ilyas + +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. + +This module is part of the `OCA/timesheet `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/hr_timesheet_sheet_activity/__init__.py b/hr_timesheet_sheet_activity/__init__.py new file mode 100644 index 0000000000..4b76c7b2d5 --- /dev/null +++ b/hr_timesheet_sheet_activity/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import models diff --git a/hr_timesheet_sheet_activity/__manifest__.py b/hr_timesheet_sheet_activity/__manifest__.py new file mode 100644 index 0000000000..7935f11328 --- /dev/null +++ b/hr_timesheet_sheet_activity/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright 2020 Brainbean Apps (https://brainbeanapps.com) +# Copyright 2020 CorporateHub (https://corporatehub.eu) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "HR Timesheet Sheet Activities", + "version": "16.0.1.0.0", + "category": "Human Resources", + "website": "https://github.com/OCA/timesheet", + "author": "CorporateHub, " "Odoo Community Association (OCA)", + "license": "AGPL-3", + "installable": True, + "application": False, + "summary": ( + "Automatic activities related to submission and review of timesheet" " sheets" + ), + "depends": [ + "hr_timesheet_sheet", + ], + "data": [ + "data/hr_timesheet_sheet_activity_data.xml", + "views/mail_activity.xml", + ], +} diff --git a/hr_timesheet_sheet_activity/data/hr_timesheet_sheet_activity_data.xml b/hr_timesheet_sheet_activity/data/hr_timesheet_sheet_activity_data.xml new file mode 100644 index 0000000000..0ccdbb51fd --- /dev/null +++ b/hr_timesheet_sheet_activity/data/hr_timesheet_sheet_activity_data.xml @@ -0,0 +1,28 @@ + + + + + + + + Timesheet Sheet Submission + fa-calendar-check-o + hr_timesheet.sheet + + + + Timesheet Sheet Review + fa-calendar-check-o + hr_timesheet.sheet + + + + Timesheet Sheet Re-submission + fa-calendar-check-o + hr_timesheet.sheet + + + diff --git a/hr_timesheet_sheet_activity/i18n/es.po b/hr_timesheet_sheet_activity/i18n/es.po new file mode 100644 index 0000000000..11a2f8a7b2 --- /dev/null +++ b/hr_timesheet_sheet_activity/i18n/es.po @@ -0,0 +1,58 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_timesheet_sheet_activity +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2020-05-18 12:19+0000\n" +"Last-Translator: Josep M \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 3.10\n" + +#. module: hr_timesheet_sheet_activity +#: model:ir.actions.act_window,name:hr_timesheet_sheet_activity.action_view_hr_timesheet_sheet_mail_activity_types +#: model:ir.ui.menu,name:hr_timesheet_sheet_activity.menu_view_hr_timesheet_sheet_mail_activity_types +msgid "Activity Types" +msgstr "Tipos Actividades" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model.fields,field_description:hr_timesheet_sheet_activity.field_hr_timesheet_sheet__display_name +msgid "Display Name" +msgstr "" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model.fields,field_description:hr_timesheet_sheet_activity.field_hr_timesheet_sheet__id +msgid "ID" +msgstr "" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model.fields,field_description:hr_timesheet_sheet_activity.field_hr_timesheet_sheet____last_update +msgid "Last Modified on" +msgstr "" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model,name:hr_timesheet_sheet_activity.model_hr_timesheet_sheet +msgid "Timesheet Sheet" +msgstr "Parte horas" + +#. module: hr_timesheet_sheet_activity +#: model:mail.activity.type,name:hr_timesheet_sheet_activity.activity_sheet_resubmission +msgid "Timesheet Sheet Re-submission" +msgstr "Re-enviar Parte horas" + +#. module: hr_timesheet_sheet_activity +#: model:mail.activity.type,name:hr_timesheet_sheet_activity.activity_sheet_review +msgid "Timesheet Sheet Review" +msgstr "Revisar Parte horas" + +#. module: hr_timesheet_sheet_activity +#: model:mail.activity.type,name:hr_timesheet_sheet_activity.activity_sheet_submission +msgid "Timesheet Sheet Submission" +msgstr "Enviar Parte horas" diff --git a/hr_timesheet_sheet_activity/i18n/hr_timesheet_sheet_activity.pot b/hr_timesheet_sheet_activity/i18n/hr_timesheet_sheet_activity.pot new file mode 100644 index 0000000000..ea25504054 --- /dev/null +++ b/hr_timesheet_sheet_activity/i18n/hr_timesheet_sheet_activity.pot @@ -0,0 +1,55 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_timesheet_sheet_activity +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \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_timesheet_sheet_activity +#: model:ir.actions.act_window,name:hr_timesheet_sheet_activity.action_view_hr_timesheet_sheet_mail_activity_types +#: model:ir.ui.menu,name:hr_timesheet_sheet_activity.menu_view_hr_timesheet_sheet_mail_activity_types +msgid "Activity Types" +msgstr "" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model.fields,field_description:hr_timesheet_sheet_activity.field_hr_timesheet_sheet__display_name +msgid "Display Name" +msgstr "" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model.fields,field_description:hr_timesheet_sheet_activity.field_hr_timesheet_sheet__id +msgid "ID" +msgstr "" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model.fields,field_description:hr_timesheet_sheet_activity.field_hr_timesheet_sheet____last_update +msgid "Last Modified on" +msgstr "" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model,name:hr_timesheet_sheet_activity.model_hr_timesheet_sheet +msgid "Timesheet Sheet" +msgstr "" + +#. module: hr_timesheet_sheet_activity +#: model:mail.activity.type,name:hr_timesheet_sheet_activity.activity_sheet_resubmission +msgid "Timesheet Sheet Re-submission" +msgstr "" + +#. module: hr_timesheet_sheet_activity +#: model:mail.activity.type,name:hr_timesheet_sheet_activity.activity_sheet_review +msgid "Timesheet Sheet Review" +msgstr "" + +#. module: hr_timesheet_sheet_activity +#: model:mail.activity.type,name:hr_timesheet_sheet_activity.activity_sheet_submission +msgid "Timesheet Sheet Submission" +msgstr "" diff --git a/hr_timesheet_sheet_activity/i18n/it.po b/hr_timesheet_sheet_activity/i18n/it.po new file mode 100644 index 0000000000..c25849122e --- /dev/null +++ b/hr_timesheet_sheet_activity/i18n/it.po @@ -0,0 +1,58 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_timesheet_sheet_activity +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-10-11 08:38+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: hr_timesheet_sheet_activity +#: model:ir.actions.act_window,name:hr_timesheet_sheet_activity.action_view_hr_timesheet_sheet_mail_activity_types +#: model:ir.ui.menu,name:hr_timesheet_sheet_activity.menu_view_hr_timesheet_sheet_mail_activity_types +msgid "Activity Types" +msgstr "Tipi attività" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model.fields,field_description:hr_timesheet_sheet_activity.field_hr_timesheet_sheet__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model.fields,field_description:hr_timesheet_sheet_activity.field_hr_timesheet_sheet__id +msgid "ID" +msgstr "ID" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model.fields,field_description:hr_timesheet_sheet_activity.field_hr_timesheet_sheet____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model,name:hr_timesheet_sheet_activity.model_hr_timesheet_sheet +msgid "Timesheet Sheet" +msgstr "Prospetto foglio ore" + +#. module: hr_timesheet_sheet_activity +#: model:mail.activity.type,name:hr_timesheet_sheet_activity.activity_sheet_resubmission +msgid "Timesheet Sheet Re-submission" +msgstr "Re-invio prospetto foglio ore" + +#. module: hr_timesheet_sheet_activity +#: model:mail.activity.type,name:hr_timesheet_sheet_activity.activity_sheet_review +msgid "Timesheet Sheet Review" +msgstr "Revisione prospetto foglio ore" + +#. module: hr_timesheet_sheet_activity +#: model:mail.activity.type,name:hr_timesheet_sheet_activity.activity_sheet_submission +msgid "Timesheet Sheet Submission" +msgstr "Invio prospetto foglio ore" diff --git a/hr_timesheet_sheet_activity/i18n/nl_NL.po b/hr_timesheet_sheet_activity/i18n/nl_NL.po new file mode 100644 index 0000000000..8c4c1b027e --- /dev/null +++ b/hr_timesheet_sheet_activity/i18n/nl_NL.po @@ -0,0 +1,58 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_timesheet_sheet_activity +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2020-04-03 08:19+0000\n" +"Last-Translator: Cas Vissers \n" +"Language-Team: none\n" +"Language: nl_NL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 3.10\n" + +#. module: hr_timesheet_sheet_activity +#: model:ir.actions.act_window,name:hr_timesheet_sheet_activity.action_view_hr_timesheet_sheet_mail_activity_types +#: model:ir.ui.menu,name:hr_timesheet_sheet_activity.menu_view_hr_timesheet_sheet_mail_activity_types +msgid "Activity Types" +msgstr "Activiteit soorten" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model.fields,field_description:hr_timesheet_sheet_activity.field_hr_timesheet_sheet__display_name +msgid "Display Name" +msgstr "" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model.fields,field_description:hr_timesheet_sheet_activity.field_hr_timesheet_sheet__id +msgid "ID" +msgstr "" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model.fields,field_description:hr_timesheet_sheet_activity.field_hr_timesheet_sheet____last_update +msgid "Last Modified on" +msgstr "" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model,name:hr_timesheet_sheet_activity.model_hr_timesheet_sheet +msgid "Timesheet Sheet" +msgstr "Urenstaat formulier" + +#. module: hr_timesheet_sheet_activity +#: model:mail.activity.type,name:hr_timesheet_sheet_activity.activity_sheet_resubmission +msgid "Timesheet Sheet Re-submission" +msgstr "Urenstaat formulier opnieuw ingediend" + +#. module: hr_timesheet_sheet_activity +#: model:mail.activity.type,name:hr_timesheet_sheet_activity.activity_sheet_review +msgid "Timesheet Sheet Review" +msgstr "Urenstaat formulier goedkeuren" + +#. module: hr_timesheet_sheet_activity +#: model:mail.activity.type,name:hr_timesheet_sheet_activity.activity_sheet_submission +msgid "Timesheet Sheet Submission" +msgstr "Urenstaat formulier indienen" diff --git a/hr_timesheet_sheet_activity/i18n/pt.po b/hr_timesheet_sheet_activity/i18n/pt.po new file mode 100644 index 0000000000..caaca95550 --- /dev/null +++ b/hr_timesheet_sheet_activity/i18n/pt.po @@ -0,0 +1,58 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_timesheet_sheet_activity +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2020-05-26 00:19+0000\n" +"Last-Translator: Pedro Castro Silva \n" +"Language-Team: none\n" +"Language: pt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 3.10\n" + +#. module: hr_timesheet_sheet_activity +#: model:ir.actions.act_window,name:hr_timesheet_sheet_activity.action_view_hr_timesheet_sheet_mail_activity_types +#: model:ir.ui.menu,name:hr_timesheet_sheet_activity.menu_view_hr_timesheet_sheet_mail_activity_types +msgid "Activity Types" +msgstr "Tipos de Atividades" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model.fields,field_description:hr_timesheet_sheet_activity.field_hr_timesheet_sheet__display_name +msgid "Display Name" +msgstr "" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model.fields,field_description:hr_timesheet_sheet_activity.field_hr_timesheet_sheet__id +msgid "ID" +msgstr "" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model.fields,field_description:hr_timesheet_sheet_activity.field_hr_timesheet_sheet____last_update +msgid "Last Modified on" +msgstr "" + +#. module: hr_timesheet_sheet_activity +#: model:ir.model,name:hr_timesheet_sheet_activity.model_hr_timesheet_sheet +msgid "Timesheet Sheet" +msgstr "Folha de Horas" + +#. module: hr_timesheet_sheet_activity +#: model:mail.activity.type,name:hr_timesheet_sheet_activity.activity_sheet_resubmission +msgid "Timesheet Sheet Re-submission" +msgstr "Re-submissão de Folha de Horas" + +#. module: hr_timesheet_sheet_activity +#: model:mail.activity.type,name:hr_timesheet_sheet_activity.activity_sheet_review +msgid "Timesheet Sheet Review" +msgstr "Revisão de Folha de Horas" + +#. module: hr_timesheet_sheet_activity +#: model:mail.activity.type,name:hr_timesheet_sheet_activity.activity_sheet_submission +msgid "Timesheet Sheet Submission" +msgstr "Submissão de Folha de Horas" diff --git a/hr_timesheet_sheet_activity/models/__init__.py b/hr_timesheet_sheet_activity/models/__init__.py new file mode 100644 index 0000000000..3bc8e53de4 --- /dev/null +++ b/hr_timesheet_sheet_activity/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import hr_timesheet_sheet diff --git a/hr_timesheet_sheet_activity/models/hr_timesheet_sheet.py b/hr_timesheet_sheet_activity/models/hr_timesheet_sheet.py new file mode 100644 index 0000000000..80dc439a25 --- /dev/null +++ b/hr_timesheet_sheet_activity/models/hr_timesheet_sheet.py @@ -0,0 +1,264 @@ +# Copyright 2020 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime, time + +from dateutil.relativedelta import relativedelta +from dateutil.rrule import MONTHLY, WEEKLY +from pytz import UTC, timezone + +from odoo import fields, models + + +class HrTimesheetSheet(models.Model): + _inherit = "hr_timesheet.sheet" + + def _get_subscribers(self): + """Reviewers are going to be notified using activities""" + res = super()._get_subscribers() + res = res - self._get_possible_reviewers().mapped("partner_id") + return res + + def write(self, vals): + res = super().write(vals) + + for sheet in self.filtered(lambda sheet: sheet.state == "draft"): + # NOTE: user_id is written manually instead of using new_user_id + # in order to update only activities with different user_id + activities = sheet.activity_reschedule( + ["hr_timesheet_sheet_activity.activity_sheet_resubmission"], + ) + for activity in activities: + if activity.user_id == sheet.user_id: + continue + if activity.user_id != self.env.user: + # NOTE: Only assigned user can update the activity + activity = activity.sudo() + activity.write( + { + "user_id": sheet.user_id.id, + } + ) + if activities: + continue + + deadline = sheet._activity_sheet_submission_deadline() + + # NOTE: Instead of updating activities using activity_reschedule, + # manually update only needed fields + activities = sheet.activity_reschedule( + ["hr_timesheet_sheet_activity.activity_sheet_submission"], + ) + for activity in activities: + values = {} + if activity.user_id != sheet.user_id: + values.update( + { + "user_id": sheet.user_id.id, + } + ) + if activity.date_deadline != deadline: + values.update( + { + # NOTE: user_id is set to trigger a notification + "user_id": sheet.user_id.id, + "date_deadline": deadline, + } + ) + if not values: + continue + + if activity.user_id != self.env.user: + # NOTE: Only assigned user can update the activity + activity = activity.sudo() + + # NOTE: to get consistent results, disable automatic notice + # and send one manually + activity.with_context( + mail_activity_quick_update=True, + ).write(values) + activity.action_notify() + if activities: + continue + + sheet.activity_schedule( + "hr_timesheet_sheet_activity.activity_sheet_submission", + date_deadline=deadline, + user_id=sheet.user_id.id, + ) + + return res + + def action_timesheet_draft(self): + for sheet in self: + sheet.activity_schedule( + "hr_timesheet_sheet_activity.activity_sheet_resubmission", + date_deadline=sheet._activity_sheet_resubmission_deadline(), + user_id=sheet.user_id.id, + ) + + return super().action_timesheet_draft() + + def action_timesheet_confirm(self): + res = super().action_timesheet_confirm() + + # NOTE: activity_reschedule is used instead of activity_feedback + # to accomodate non-assigned-user completion + activities = self.activity_reschedule( + [ + "hr_timesheet_sheet_activity.activity_sheet_submission", + "hr_timesheet_sheet_activity.activity_sheet_resubmission", + ] + ) + for activity in activities: + if activity.user_id != self.env.user: + # NOTE: Only assigned user can update the activity + activity = activity.sudo() + activity.action_feedback() + + for sheet in self: + for reviewer in sheet._get_possible_reviewers(): + deadline = sheet._activity_sheet_review_deadline(reviewer) + sheet.activity_schedule( + "hr_timesheet_sheet_activity.activity_sheet_review", + date_deadline=deadline, + user_id=reviewer.id, + ) + return res + + def action_timesheet_done(self): + res = super().action_timesheet_done() + + # NOTE: activity_reschedule is used instead of activity_feedback + # to accomodate non-assigned-user completion + activities = self.activity_reschedule( + [ + "hr_timesheet_sheet_activity.activity_sheet_review", + ] + ) + for activity in activities: + if activity.user_id != self.env.user: + # NOTE: Only assigned user can update the activity + activity = activity.sudo() + activity.action_feedback() + return res + + def action_timesheet_refuse(self): + for sheet in self: + sheet.activity_schedule( + "hr_timesheet_sheet_activity.activity_sheet_resubmission", + date_deadline=sheet._activity_sheet_resubmission_deadline(), + user_id=sheet.user_id.id, + ) + + res = super().action_timesheet_refuse() + + # NOTE: activity_reschedule is used instead of activity_feedback + # to accomodate non-assigned-user completion + activities = self.activity_reschedule( + [ + "hr_timesheet_sheet_activity.activity_sheet_review", + ] + ) + for activity in activities: + if activity.user_id != self.env.user: + # NOTE: Only assigned user can update the activity + activity = activity.sudo() + activity.action_feedback() + return res + + def _activity_sheet_submission_deadline(self): + """Hook for extensions""" + self.ensure_one() + + employee_timezone = timezone(self.employee_id.tz or "UTC") + employee_today = self.env.context.get( + "hr_timesheet_sheet_activity_today", + fields.Datetime.now() + .replace(tzinfo=UTC) + .astimezone(employee_timezone) + .date(), + ) + + # Get last workday of employee or last day of period (in user tz) + datetime_start = datetime.combine( + self.date_start, + time.min, + ).replace(tzinfo=employee_timezone) + datetime_end = datetime.combine( + max(self.date_end, employee_today), time.max + ).replace(tzinfo=employee_timezone) + worktimes = self.employee_id.list_work_time_per_day( + datetime_start, + datetime_end, + ) + worktimes = list(filter(lambda worktime: worktime[1] > 0, worktimes)) + if worktimes: + return worktimes[-1][0] # Last workday of period + return datetime_end.date() + + def _activity_sheet_resubmission_deadline(self): + """Hook for extensions""" + self.ensure_one() + return None + + def _activity_sheet_review_deadline(self, reviewer): + """Hook for extensions""" + self.ensure_one() + + employee_timezone = timezone(self.employee_id.tz or "UTC") + employee_today = self.env.context.get( + "hr_timesheet_sheet_activity_today", + fields.Datetime.now() + .replace(tzinfo=UTC) + .astimezone(employee_timezone) + .date(), + ) + deadline = max(self.date_end + relativedelta(days=1), employee_today) + + reviewer_employee = ( + self.env["hr.employee"] + .with_context( + active_test=False, + ) + .search( + [("user_id", "=", reviewer.id)], + limit=1, + ) + ) + if not reviewer_employee: + return deadline + + worktimes = reviewer_employee.list_work_time_per_day( + datetime.combine(deadline, time.min).replace(tzinfo=employee_timezone), + datetime.combine( + deadline + self._activity_sheet_review_max_period(), time.max + ).replace(tzinfo=employee_timezone), + ) + worktimes = list(filter(lambda worktime: worktime[1] > 0, worktimes)) + if worktimes: + return worktimes[0][0] # First workday of period + return ( + datetime.combine(deadline, time.max) + .replace(tzinfo=employee_timezone) + .astimezone(timezone(reviewer_employee.tz or "UTC")) + .date() + ) + + def _activity_sheet_review_max_period(self): + """Hook for extensions""" + self.ensure_one() + sheet_range = self.company_id.sheet_range + if not sheet_range: + r = WEEKLY + elif sheet_range == "DAILY": + r = 1 + elif sheet_range == "WEEKLY": + r = 2 + elif sheet_range == "MONTHLY": + r = 3 + if r == WEEKLY: + return relativedelta(weeks=1) + elif r == MONTHLY: + return relativedelta(months=1) + return relativedelta(days=1) diff --git a/hr_timesheet_sheet_activity/readme/CONTRIBUTORS.rst b/hr_timesheet_sheet_activity/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..60d0bb2791 --- /dev/null +++ b/hr_timesheet_sheet_activity/readme/CONTRIBUTORS.rst @@ -0,0 +1,7 @@ +* `CorporateHub `__ + + * Alexey Pelykh + +* `Ooops404 `__: + + * Ilyas diff --git a/hr_timesheet_sheet_activity/readme/DESCRIPTION.rst b/hr_timesheet_sheet_activity/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..3e39e055b8 --- /dev/null +++ b/hr_timesheet_sheet_activity/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +This module adds the following activities related to submission and review of +timesheet sheets: + +* Draft and Submit sheet(s) for review 1 business day prior the end of the period; +* Review sheet(s) within 1 business day after submission day (not earlier than the end of the period); +* Re-submit sheet(s) within 1 business day in case those have been rejected. diff --git a/hr_timesheet_sheet_activity/static/description/icon.png b/hr_timesheet_sheet_activity/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/hr_timesheet_sheet_activity/static/description/icon.png differ diff --git a/hr_timesheet_sheet_activity/static/description/index.html b/hr_timesheet_sheet_activity/static/description/index.html new file mode 100644 index 0000000000..90a360fc4a --- /dev/null +++ b/hr_timesheet_sheet_activity/static/description/index.html @@ -0,0 +1,436 @@ + + + + + +HR Timesheet Sheet Activities + + + +
+

HR Timesheet Sheet Activities

+ + +

Beta License: AGPL-3 OCA/timesheet Translate me on Weblate Try me on Runboat

+

This module adds the following activities related to submission and review of +timesheet sheets:

+
    +
  • Draft and Submit sheet(s) for review 1 business day prior the end of the period;
  • +
  • Review sheet(s) within 1 business day after submission day (not earlier than the end of the period);
  • +
  • Re-submit sheet(s) within 1 business day in case those have been rejected.
  • +
+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • CorporateHub
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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.

+

This module is part of the OCA/timesheet project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/hr_timesheet_sheet_activity/tests/__init__.py b/hr_timesheet_sheet_activity/tests/__init__.py new file mode 100644 index 0000000000..f389d26069 --- /dev/null +++ b/hr_timesheet_sheet_activity/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import test_hr_timesheet_sheet_activity diff --git a/hr_timesheet_sheet_activity/tests/test_hr_timesheet_sheet_activity.py b/hr_timesheet_sheet_activity/tests/test_hr_timesheet_sheet_activity.py new file mode 100644 index 0000000000..46f392e7b7 --- /dev/null +++ b/hr_timesheet_sheet_activity/tests/test_hr_timesheet_sheet_activity.py @@ -0,0 +1,519 @@ +# Copyright 2020 Brainbean Apps (https://brainbeanapps.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import date + +from odoo import fields +from odoo.tests import common + + +class TestHrTimesheetSheetActivity(common.TransactionCase): + def setUp(self): + super().setUp() + + self.IrModel = self.env["ir.model"] + self.ResUsers = self.env["res.users"] + self.Company = self.env["res.company"] + self.Project = self.env["project.project"] + self.HrEmployee = self.env["hr.employee"] + self.HrTimesheetSheet = self.env["hr_timesheet.sheet"] + self.AccountAnalyticLine = self.env["account.analytic.line"] + self.MailActivity = self.env["mail.activity"] + self.company_id = self.env.ref("base.main_company") + self.now = fields.Datetime.now() + self.group_hr_user = self.env.ref("hr.group_hr_user") + self.group_hr_timesheet_user = self.env.ref( + "hr_timesheet.group_hr_timesheet_user" + ) + self.group_project_user = self.env.ref("project.group_project_user") + self.hr_timesheet_sheet_modelid = self.IrModel._get( + self.HrTimesheetSheet._name + ).id + self.activity_sheet_submission = self.env.ref( + "hr_timesheet_sheet_activity.activity_sheet_submission" + ) + self.activity_sheet_resubmission = self.env.ref( + "hr_timesheet_sheet_activity.activity_sheet_resubmission" + ) + self.activity_sheet_review = self.env.ref( + "hr_timesheet_sheet_activity.activity_sheet_review" + ) + + def test_activity(self): + user_1 = self.ResUsers.sudo().create( + { + "name": "User 1", + "login": "user_1", + "email": "user-1@example.com", + "company_id": self.company_id.id, + } + ) + user_2 = self.ResUsers.sudo().create( + { + "name": "User 2", + "login": "user_2", + "email": "user-2@example.com", + "company_id": self.company_id.id, + "groups_id": [ + ( + 6, + 0, + [ + self.group_hr_user.id, + self.group_hr_timesheet_user.id, + self.group_project_user.id, + ], + ) + ], + } + ) + employee = self.HrEmployee.create( + { + "name": "Employee", + "user_id": user_1.id, + } + ) + self.HrEmployee.create( + { + "name": "Officer", + "user_id": user_2.id, + } + ) + project = self.Project.create( + { + "name": "Project", + } + ) + + self.AccountAnalyticLine.create( + { + "project_id": project.id, + "employee_id": employee.id, + "name": "Time Entry", + } + ) + + sheet = self.HrTimesheetSheet.with_user(user_1).create( + { + "employee_id": employee.id, + } + ) + + activities = self.MailActivity.search( + [ + ("res_model_id", "=", self.hr_timesheet_sheet_modelid), + ("user_id", "=", user_1.id), + ] + ) + self.assertEqual(len(activities), 1) + self.assertEqual(activities.res_id, sheet.id) + self.assertEqual(activities.activity_type_id, self.activity_sheet_submission) + + sheet.with_user(user_1).action_timesheet_confirm() + + activities = self.MailActivity.search( + [ + ("res_model_id", "=", self.hr_timesheet_sheet_modelid), + ("user_id", "=", user_2.id), + ] + ) + self.assertEqual(len(activities), 1) + self.assertEqual(activities.res_id, sheet.id) + self.assertEqual(activities.activity_type_id, self.activity_sheet_review) + + sheet.with_user(user_2).action_timesheet_done() + + activities = self.MailActivity.search( + [ + ("res_model_id", "=", self.hr_timesheet_sheet_modelid), + ] + ) + self.assertEqual(len(activities), 0) + + sheet.with_user(user_2).action_timesheet_draft() + + activities = self.MailActivity.search( + [ + ("res_model_id", "=", self.hr_timesheet_sheet_modelid), + ("user_id", "=", user_1.id), + ] + ) + self.assertEqual(len(activities), 1) + self.assertEqual(activities.res_id, sheet.id) + self.assertEqual(activities.activity_type_id, self.activity_sheet_resubmission) + + sheet.with_user(user_1).action_timesheet_confirm() + + activities = self.MailActivity.search( + [ + ("res_model_id", "=", self.hr_timesheet_sheet_modelid), + ("user_id", "=", user_2.id), + ] + ) + self.assertEqual(len(activities), 1) + self.assertEqual(activities.res_id, sheet.id) + self.assertEqual(activities.activity_type_id, self.activity_sheet_review) + + sheet.with_user(user_1).action_timesheet_refuse() + + activities = self.MailActivity.search( + [ + ("res_model_id", "=", self.hr_timesheet_sheet_modelid), + ("user_id", "=", user_1.id), + ] + ) + self.assertEqual(len(activities), 1) + self.assertEqual(activities.res_id, sheet.id) + self.assertEqual(activities.activity_type_id, self.activity_sheet_resubmission) + + def test_period_ends_on_workday(self): + user_1 = self.ResUsers.sudo().create( + { + "name": "User 1", + "login": "user_1", + "email": "user-1@example.com", + "company_id": self.company_id.id, + } + ) + user_2 = self.ResUsers.sudo().create( + { + "name": "User 2", + "login": "user_2", + "email": "user-2@example.com", + "company_id": self.company_id.id, + "groups_id": [ + ( + 6, + 0, + [ + self.group_hr_user.id, + self.group_hr_timesheet_user.id, + self.group_project_user.id, + ], + ) + ], + } + ) + employee = self.HrEmployee.create( + { + "name": "Employee", + "user_id": user_1.id, + } + ) + self.HrEmployee.create( + { + "name": "Officer", + "user_id": user_2.id, + } + ) + project = self.Project.create( + { + "name": "Project", + } + ) + + self.AccountAnalyticLine.create( + { + "project_id": project.id, + "employee_id": employee.id, + "date": date(2020, 2, 7), + "name": "Time Entry", + } + ) + + sheet = ( + self.HrTimesheetSheet.with_user(user_1) + .with_context( + hr_timesheet_sheet_activity_today=date(2020, 2, 7), + ) + .create( + { + "employee_id": employee.id, + "date_start": date(2020, 2, 3), + "date_end": date(2020, 2, 7), + } + ) + ) + + activity = self.MailActivity.search( + [ + ("res_id", "=", sheet.id), + ("activity_type_id", "=", self.activity_sheet_submission.id), + ] + ) + self.assertEqual(activity.date_deadline, date(2020, 2, 7)) + + sheet.with_user(user_1).action_timesheet_confirm() + + activity = self.MailActivity.search( + [ + ("res_id", "=", sheet.id), + ("activity_type_id", "=", self.activity_sheet_review.id), + ("user_id", "=", user_2.id), + ] + ) + self.assertEqual(activity.date_deadline, date(2020, 2, 10)) + + def test_period_ends_on_weekend(self): + user_1 = self.ResUsers.sudo().create( + { + "name": "User 1", + "login": "user_1", + "email": "user-1@example.com", + "company_id": self.company_id.id, + } + ) + user_2 = self.ResUsers.sudo().create( + { + "name": "User 2", + "login": "user_2", + "email": "user-2@example.com", + "company_id": self.company_id.id, + "groups_id": [ + ( + 6, + 0, + [ + self.group_hr_user.id, + self.group_hr_timesheet_user.id, + self.group_project_user.id, + ], + ) + ], + } + ) + employee = self.HrEmployee.create( + { + "name": "Employee", + "user_id": user_1.id, + } + ) + self.HrEmployee.create( + { + "name": "Officer", + "user_id": user_2.id, + } + ) + project = self.Project.create( + { + "name": "Project", + } + ) + + self.AccountAnalyticLine.create( + { + "project_id": project.id, + "employee_id": employee.id, + "date": date(2020, 2, 7), + "name": "Time Entry", + } + ) + + sheet = ( + self.HrTimesheetSheet.with_user(user_1) + .with_context( + hr_timesheet_sheet_activity_today=date(2020, 2, 7), + ) + .create( + { + "employee_id": employee.id, + "date_start": date(2020, 2, 3), + "date_end": date(2020, 2, 9), + } + ) + ) + + activity = self.MailActivity.search( + [ + ("res_id", "=", sheet.id), + ("activity_type_id", "=", self.activity_sheet_submission.id), + ] + ) + self.assertEqual(activity.date_deadline, date(2020, 2, 7)) + + sheet.with_user(user_1).action_timesheet_confirm() + + activity = self.MailActivity.search( + [ + ("res_id", "=", sheet.id), + ("activity_type_id", "=", self.activity_sheet_review.id), + ("user_id", "=", user_2.id), + ] + ) + self.assertEqual(activity.date_deadline, date(2020, 2, 10)) + + def test_period_overdue(self): + user_1 = self.ResUsers.sudo().create( + { + "name": "User 1", + "login": "user_1", + "email": "user-1@example.com", + "company_id": self.company_id.id, + } + ) + user_2 = self.ResUsers.sudo().create( + { + "name": "User 2", + "login": "user_2", + "email": "user-2@example.com", + "company_id": self.company_id.id, + "groups_id": [ + ( + 6, + 0, + [ + self.group_hr_user.id, + self.group_hr_timesheet_user.id, + self.group_project_user.id, + ], + ) + ], + } + ) + employee = self.HrEmployee.create( + { + "name": "Employee", + "user_id": user_1.id, + } + ) + self.HrEmployee.create( + { + "name": "Officer", + "user_id": user_2.id, + } + ) + project = self.Project.create( + { + "name": "Project", + } + ) + + self.AccountAnalyticLine.create( + { + "project_id": project.id, + "employee_id": employee.id, + "date": date(2020, 1, 31), + "name": "Time Entry", + } + ) + + sheet = ( + self.HrTimesheetSheet.with_user(user_1) + .with_context( + hr_timesheet_sheet_activity_today=date(2020, 2, 7), + ) + .create( + { + "employee_id": employee.id, + "date_start": date(2020, 1, 27), + "date_end": date(2020, 2, 2), + } + ) + ) + + activity = self.MailActivity.search( + [ + ("res_id", "=", sheet.id), + ("activity_type_id", "=", self.activity_sheet_submission.id), + ] + ) + self.assertEqual(activity.date_deadline, date(2020, 2, 7)) + + sheet.with_user(user_1).action_timesheet_confirm() + + activity = self.MailActivity.search( + [ + ("res_id", "=", sheet.id), + ("activity_type_id", "=", self.activity_sheet_review.id), + ("user_id", "=", user_2.id), + ] + ) + self.assertEqual(activity.date_deadline, date(2020, 2, 7)) + + def test_weekend_period(self): + user_1 = self.ResUsers.sudo().create( + { + "name": "User 1", + "login": "user_1", + "email": "user-1@example.com", + "company_id": self.company_id.id, + } + ) + user_2 = self.ResUsers.sudo().create( + { + "name": "User 2", + "login": "user_2", + "email": "user-2@example.com", + "company_id": self.company_id.id, + "groups_id": [ + ( + 6, + 0, + [ + self.group_hr_user.id, + self.group_hr_timesheet_user.id, + self.group_project_user.id, + ], + ) + ], + } + ) + employee = self.HrEmployee.create( + { + "name": "Employee", + "user_id": user_1.id, + } + ) + self.HrEmployee.create( + { + "name": "Officer", + "user_id": user_2.id, + } + ) + project = self.Project.create( + { + "name": "Project", + } + ) + + self.AccountAnalyticLine.create( + { + "project_id": project.id, + "employee_id": employee.id, + "date": date(2020, 2, 1), + "name": "Time Entry", + } + ) + + sheet = ( + self.HrTimesheetSheet.with_user(user_1) + .with_context( + hr_timesheet_sheet_activity_today=date(2020, 2, 1), + ) + .create( + { + "employee_id": employee.id, + "date_start": date(2020, 2, 1), + "date_end": date(2020, 2, 1), + } + ) + ) + + activity = self.MailActivity.search( + [ + ("res_id", "=", sheet.id), + ("activity_type_id", "=", self.activity_sheet_submission.id), + ] + ) + self.assertEqual(activity.date_deadline, date(2020, 2, 1)) + + sheet.with_user(user_1).action_timesheet_confirm() + + activity = self.MailActivity.search( + [ + ("res_id", "=", sheet.id), + ("activity_type_id", "=", self.activity_sheet_review.id), + ("user_id", "=", user_2.id), + ] + ) + self.assertEqual(activity.date_deadline, date(2020, 2, 3)) diff --git a/hr_timesheet_sheet_activity/views/mail_activity.xml b/hr_timesheet_sheet_activity/views/mail_activity.xml new file mode 100644 index 0000000000..656ddc0114 --- /dev/null +++ b/hr_timesheet_sheet_activity/views/mail_activity.xml @@ -0,0 +1,26 @@ + + + + + + Activity Types + mail.activity.type + tree,form + [('res_model', '=', 'hr_timesheet.sheet')] + {'default_res_model': 'hr_timesheet.sheet'} + + + + + diff --git a/setup/hr_timesheet_sheet_activity/odoo/addons/hr_timesheet_sheet_activity b/setup/hr_timesheet_sheet_activity/odoo/addons/hr_timesheet_sheet_activity new file mode 120000 index 0000000000..c6a5bfbd4b --- /dev/null +++ b/setup/hr_timesheet_sheet_activity/odoo/addons/hr_timesheet_sheet_activity @@ -0,0 +1 @@ +../../../../hr_timesheet_sheet_activity \ No newline at end of file diff --git a/setup/hr_timesheet_sheet_activity/setup.py b/setup/hr_timesheet_sheet_activity/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/hr_timesheet_sheet_activity/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)