mirror of
https://github.com/bringout/oca-mrp.git
synced 2026-04-24 09:12:00 +02:00
Initial commit: OCA Mrp packages (117 packages)
This commit is contained in:
commit
277e84fd7a
4403 changed files with 395154 additions and 0 deletions
|
|
@ -0,0 +1,131 @@
|
|||
==================
|
||||
HR Timesheet Sheet
|
||||
==================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:faddd2bab01c77bceef23115853e6806a0b39685cf857967bba202ca08e84895
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |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
|
||||
: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
|
||||
: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 supplies a new screen enabling you to manage your work encoding
|
||||
(timesheet) by period. Timesheet entries are made by employees each day. At the
|
||||
end of the defined period, employees submit their validated sheet and the
|
||||
reviewer must then approve submitted entries. Periods are defined in the
|
||||
company forms and you can set them to run monthly, weekly or daily. By default,
|
||||
policy is configured to have HR Officers as reviewers.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
This module relies on:
|
||||
|
||||
* The OCA module '2D matrix for x2many fields', and can be downloaded from
|
||||
Github: https://github.com/OCA/web/tree/16.0/web_widget_x2many_2d_matrix
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
If you want other default ranges different from weekly, you need to go:
|
||||
|
||||
* In the menu `Configuration` -> `Settings` -> **Timesheet Options**,
|
||||
and select in **Timesheet Sheet Range** the default range you want.
|
||||
* When you have a weekly range you can also specify the **Week Start Day**.
|
||||
|
||||
To change who reviews submitted sheets, go to *Configuration > Settings > Timesheet Options*
|
||||
and configure **Timesheet Sheet Review Policy** accordingly.
|
||||
|
||||
For adding more review policies, look at the *hr_timesheet_sheet_policy_xxx*
|
||||
extra modules.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
If you modify the `Details` tab, automatically the `Summary` tab is updated.
|
||||
But if you modify the `Summary` tab, you need to save in order to have the `Details` tab updated.
|
||||
|
||||
In case you modify the unit amount of both tabs, the `Details` tab will prevail.
|
||||
If you modify the `Summary` tab, and you need to do a change in the `Details` tab, please save before.
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
* The timesheet grid is limited to display a max. of 1M cells, due to a
|
||||
limitation of the tree view limit parameter not being able to dynamically
|
||||
set a limit. Since default value of odoo, 40 records is too small, we decided
|
||||
to set 1M, which should be good enough in the majority of scenarios.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/timesheet/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/timesheet/issues/new?body=module:%20hr_timesheet_sheet%0Aversion:%2016.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
|
||||
~~~~~~~
|
||||
|
||||
* ForgeFlow
|
||||
* Onestein
|
||||
* CorporateHub
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Miquel Raïch <miquel.raich@forgeflow.com>
|
||||
* Andrea Stirpe <a.stirpe@onestein.nl>
|
||||
* Lois Rilo <lois.rilo@forgeflow.com>
|
||||
* `CorporateHub <https://corporatehub.eu/>`__
|
||||
|
||||
* Alexey Pelykh <alexey.pelykh@corphub.eu>
|
||||
|
||||
* Dennis Sluijk <d.sluijk@onestein.nl>
|
||||
* Sunanda Chhatbar <sunanda.chhatbar@initos.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.
|
||||
|
||||
This module is part of the `OCA/timesheet <https://github.com/OCA/timesheet/tree/16.0/hr_timesheet_sheet>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import models
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# Copyright 2018 ForgeFlow (https://www.forgeflow.com)
|
||||
# Copyright 2018-2019 Brainbean Apps (https://brainbeanapps.com)
|
||||
# Copyright 2018-2019 Onestein (<https://www.onestein.eu>)
|
||||
# Copyright 2020 CorporateHub (https://corporatehub.eu)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "HR Timesheet Sheet",
|
||||
"version": "16.0.1.2.0",
|
||||
"category": "Human Resources",
|
||||
"sequence": 80,
|
||||
"summary": "Timesheet Sheets, Activities",
|
||||
"license": "AGPL-3",
|
||||
"author": "ForgeFlow, Onestein, CorporateHub, " "Odoo Community Association (OCA)",
|
||||
"website": "https://github.com/OCA/timesheet",
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
"depends": ["hr_timesheet", "sale_timesheet", "web_widget_x2many_2d_matrix"],
|
||||
"data": [
|
||||
"data/hr_timesheet_sheet_data.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"security/hr_timesheet_sheet_security.xml",
|
||||
"views/hr_timesheet_sheet_views.xml",
|
||||
"views/hr_department_views.xml",
|
||||
"views/hr_employee_views.xml",
|
||||
"views/account_analytic_line_views.xml",
|
||||
"views/res_config_settings_views.xml",
|
||||
],
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
<odoo>
|
||||
<!-- Timesheet sheet related subtypes for messaging / Chatter -->
|
||||
<record id="mt_timesheet_confirmed" model="mail.message.subtype">
|
||||
<field name="name">Waiting Review</field>
|
||||
<field name="res_model">hr_timesheet.sheet</field>
|
||||
<field name="default" eval="True" />
|
||||
<field name="description">Waiting review</field>
|
||||
</record>
|
||||
<record id="mt_timesheet_approved" model="mail.message.subtype">
|
||||
<field name="name">Approved</field>
|
||||
<field name="res_model">hr_timesheet.sheet</field>
|
||||
<field name="default" eval="True" />
|
||||
<field name="description">Approved</field>
|
||||
</record>
|
||||
</odoo>
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,9 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import res_company
|
||||
from . import res_config
|
||||
from . import account_analytic_account
|
||||
from . import account_analytic_line
|
||||
from . import hr_timesheet_sheet
|
||||
from . import hr_department
|
||||
from . import hr_employee
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# Copyright 2019 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class AccountAnalyticAccount(models.Model):
|
||||
_inherit = "account.analytic.account"
|
||||
|
||||
@api.constrains("company_id")
|
||||
def _check_timesheet_sheet_company_id(self):
|
||||
for rec in self.sudo():
|
||||
sheets = rec.line_ids.mapped("sheet_id").filtered(
|
||||
lambda s: s.company_id and s.company_id != rec.company_id
|
||||
)
|
||||
if sheets:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"You cannot change the company, "
|
||||
"as this %(rec_name)s (%(rec_display_name)s) "
|
||||
"is assigned to %(current_name)s (%(current_display_name)s).",
|
||||
rec_name=rec._name,
|
||||
rec_display_name=rec.display_name,
|
||||
current_name=sheets[0]._name,
|
||||
current_display_name=sheets[0].display_name,
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
# Copyright 2018 ForgeFlow, S.L.
|
||||
# Copyright 2018-2019 Brainbean Apps (https://brainbeanapps.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
class AccountAnalyticLine(models.Model):
|
||||
_inherit = "account.analytic.line"
|
||||
|
||||
sheet_id = fields.Many2one(
|
||||
comodel_name="hr_timesheet.sheet", string="Sheet", copy=False
|
||||
)
|
||||
sheet_state = fields.Selection(string="Sheet State", related="sheet_id.state")
|
||||
|
||||
def _get_sheet_domain(self):
|
||||
"""Hook for extensions"""
|
||||
self.ensure_one()
|
||||
return [
|
||||
("date_end", ">=", self.date),
|
||||
("date_start", "<=", self.date),
|
||||
("employee_id", "=", self.employee_id.id),
|
||||
("company_id", "in", [self.company_id.id, False]),
|
||||
("state", "in", ["new", "draft"]),
|
||||
]
|
||||
|
||||
def _determine_sheet(self):
|
||||
"""Hook for extensions"""
|
||||
self.ensure_one()
|
||||
return self.env["hr_timesheet.sheet"].search(self._get_sheet_domain(), limit=1)
|
||||
|
||||
def _compute_sheet(self):
|
||||
"""Links the timesheet line to the corresponding sheet"""
|
||||
for timesheet in self.filtered("project_id"):
|
||||
sheet = timesheet._determine_sheet()
|
||||
if timesheet.sheet_id != sheet:
|
||||
timesheet.sheet_id = sheet
|
||||
|
||||
@api.constrains("company_id", "sheet_id")
|
||||
def _check_company_id_sheet_id(self):
|
||||
for aal in self.sudo():
|
||||
if (
|
||||
aal.company_id
|
||||
and aal.sheet_id.company_id
|
||||
and aal.company_id != aal.sheet_id.company_id
|
||||
):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"You cannot create a timesheet of a different company "
|
||||
"than the one of the timesheet sheet:"
|
||||
"\n - %(sheet_name)s of %(sheet_company)s"
|
||||
"\n - %(name)s of %(company)s",
|
||||
sheet_name=aal.sheet_id.complete_name,
|
||||
sheet_company=aal.sheet_id.company_id.name,
|
||||
name=aal.name,
|
||||
company=aal.company_id.name,
|
||||
)
|
||||
)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, values):
|
||||
if not self.env.context.get("sheet_create") and "sheet_id" in values:
|
||||
del values["sheet_id"]
|
||||
res = super().create(values)
|
||||
res._compute_sheet()
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _sheet_create(self, values):
|
||||
return self.with_context(sheet_create=True).create(values)
|
||||
|
||||
def write(self, values):
|
||||
self._check_state_on_write(values)
|
||||
res = super().write(values)
|
||||
if self._timesheet_should_compute_sheet(values):
|
||||
self._compute_sheet()
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
self._check_state()
|
||||
return super().unlink()
|
||||
|
||||
def _check_state_on_write(self, values):
|
||||
"""Hook for extensions"""
|
||||
if self._timesheet_should_check_write(values):
|
||||
self._check_state()
|
||||
|
||||
@api.model
|
||||
def _timesheet_should_check_write(self, values):
|
||||
"""Hook for extensions"""
|
||||
return bool(set(self._get_timesheet_protected_fields()) & set(values.keys()))
|
||||
|
||||
@api.model
|
||||
def _timesheet_should_compute_sheet(self, values):
|
||||
"""Hook for extensions"""
|
||||
return any(f in self._get_sheet_affecting_fields() for f in values)
|
||||
|
||||
@api.model
|
||||
def _get_timesheet_protected_fields(self):
|
||||
"""Hook for extensions"""
|
||||
return [
|
||||
"name",
|
||||
"date",
|
||||
"unit_amount",
|
||||
"user_id",
|
||||
"employee_id",
|
||||
"department_id",
|
||||
"company_id",
|
||||
"task_id",
|
||||
"project_id",
|
||||
"sheet_id",
|
||||
]
|
||||
|
||||
@api.model
|
||||
def _get_sheet_affecting_fields(self):
|
||||
"""Hook for extensions"""
|
||||
return ["date", "employee_id", "project_id", "company_id"]
|
||||
|
||||
def _check_state(self):
|
||||
if self.env.context.get("skip_check_state"):
|
||||
return
|
||||
for line in self.exists().filtered("sheet_id"):
|
||||
if line.sheet_id.state not in ["new", "draft"]:
|
||||
raise UserError(
|
||||
_(
|
||||
"You cannot modify an entry in a confirmed timesheet sheet"
|
||||
": %(names)s",
|
||||
names=line.sheet_id.complete_name,
|
||||
)
|
||||
)
|
||||
|
||||
def merge_timesheets(self):
|
||||
unit_amount = sum(t.unit_amount for t in self)
|
||||
amount = sum(t.amount for t in self)
|
||||
self[0].write({"unit_amount": unit_amount, "amount": amount})
|
||||
self[1:].unlink()
|
||||
return self[0]
|
||||
|
||||
def _check_can_update_timesheet(self):
|
||||
return super()._check_can_update_timesheet() or not self.filtered("sheet_id")
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
# Copyright 2018 ForgeFlow, S.L.
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class HrDepartment(models.Model):
|
||||
_inherit = "hr.department"
|
||||
|
||||
timesheet_sheet_to_approve_count = fields.Integer(
|
||||
compute="_compute_timesheet_to_approve", string="Timesheet Sheets to Approve"
|
||||
)
|
||||
|
||||
def _compute_timesheet_to_approve(self):
|
||||
timesheet_data = self.env["hr_timesheet.sheet"].read_group(
|
||||
[("department_id", "in", self.ids), ("state", "=", "confirm")],
|
||||
["department_id"],
|
||||
["department_id"],
|
||||
)
|
||||
result = {
|
||||
data["department_id"][0]: data["department_id_count"]
|
||||
for data in timesheet_data
|
||||
}
|
||||
for department in self:
|
||||
department.timesheet_sheet_to_approve_count = result.get(department.id, 0)
|
||||
|
||||
@api.constrains("company_id")
|
||||
def _check_company_id(self):
|
||||
for rec in self.sudo().filtered("company_id"):
|
||||
for field in [
|
||||
rec.env["hr_timesheet.sheet"].search(
|
||||
[
|
||||
("department_id", "=", rec.id),
|
||||
("company_id", "!=", rec.company_id.id),
|
||||
("company_id", "!=", False),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
]:
|
||||
if (
|
||||
rec.company_id
|
||||
and field.company_id
|
||||
and rec.company_id != field.company_id
|
||||
):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"You cannot change the company, "
|
||||
"as this %(rec_name)s (%(rec_display_name)s) "
|
||||
"is assigned to %(current_name)s (%(current_display_name)s).",
|
||||
rec_name=rec._name,
|
||||
rec_display_name=rec.display_name,
|
||||
current_name=field._name,
|
||||
current_display_name=field.display_name,
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
# Copyright 2018 ForgeFlow, S.L.
|
||||
# Copyright 2019 Brainbean Apps (https://brainbeanapps.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = "hr.employee"
|
||||
|
||||
timesheet_sheet_ids = fields.One2many(
|
||||
comodel_name="hr_timesheet.sheet",
|
||||
inverse_name="employee_id",
|
||||
string="Timesheet Sheets",
|
||||
)
|
||||
timesheet_sheet_count = fields.Integer(
|
||||
compute="_compute_timesheet_sheet_count", string="Timesheet Sheets Count"
|
||||
)
|
||||
|
||||
def _compute_timesheet_sheet_count(self):
|
||||
Sheet = self.env["hr_timesheet.sheet"]
|
||||
for employee in self:
|
||||
employee.timesheet_sheet_count = Sheet.search_count(
|
||||
[("employee_id", "=", employee.id)]
|
||||
)
|
||||
|
||||
@api.constrains("company_id")
|
||||
def _check_company_id(self):
|
||||
for rec in self.sudo().filtered("company_id"):
|
||||
for field in [
|
||||
rec.env["hr_timesheet.sheet"].search(
|
||||
[
|
||||
("employee_id", "=", rec.id),
|
||||
("company_id", "!=", rec.company_id.id),
|
||||
("company_id", "!=", False),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
]:
|
||||
if (
|
||||
rec.company_id
|
||||
and field.company_id
|
||||
and rec.company_id != field.company_id
|
||||
):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"You cannot change the company, "
|
||||
"as this %(rec_name)s (%(rec_display_name)s) "
|
||||
"is assigned to %(current_name)s (%(current_display_name)s).",
|
||||
rec_name=rec._name,
|
||||
rec_display_name=rec.display_name,
|
||||
current_name=field._name,
|
||||
current_display_name=field.display_name,
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,922 @@
|
|||
# Copyright 2018-2020 ForgeFlow, S.L.
|
||||
# Copyright 2018-2020 Brainbean Apps (https://brainbeanapps.com)
|
||||
# Copyright 2018-2019 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from datetime import datetime, time
|
||||
|
||||
import babel.dates
|
||||
from dateutil.relativedelta import SU, relativedelta
|
||||
|
||||
from odoo import SUPERUSER_ID, _, api, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
empty_name = "/"
|
||||
|
||||
|
||||
class Sheet(models.Model):
|
||||
_name = "hr_timesheet.sheet"
|
||||
_description = "Timesheet Sheet"
|
||||
_inherit = ["mail.thread", "mail.activity.mixin", "portal.mixin"]
|
||||
_table = "hr_timesheet_sheet"
|
||||
_order = "id desc"
|
||||
_rec_name = "complete_name"
|
||||
|
||||
def _default_date_start(self):
|
||||
return self._get_period_start(
|
||||
self.env.user.company_id, fields.Date.context_today(self)
|
||||
)
|
||||
|
||||
def _default_date_end(self):
|
||||
return self._get_period_end(
|
||||
self.env.user.company_id, fields.Date.context_today(self)
|
||||
)
|
||||
|
||||
def _selection_review_policy(self):
|
||||
ResCompany = self.env["res.company"]
|
||||
return ResCompany._fields["timesheet_sheet_review_policy"].selection
|
||||
|
||||
def _default_review_policy(self):
|
||||
company = self.env.company
|
||||
return company.timesheet_sheet_review_policy
|
||||
|
||||
def _default_employee(self):
|
||||
company = self.env.company
|
||||
return self.env["hr.employee"].search(
|
||||
[("user_id", "=", self.env.uid), ("company_id", "in", [company.id, False])],
|
||||
limit=1,
|
||||
order="company_id ASC",
|
||||
)
|
||||
|
||||
def _default_department_id(self):
|
||||
return self._default_employee().department_id
|
||||
|
||||
name = fields.Char(compute="_compute_name")
|
||||
employee_id = fields.Many2one(
|
||||
comodel_name="hr.employee",
|
||||
string="Employee",
|
||||
default=lambda self: self._default_employee(),
|
||||
required=True,
|
||||
readonly=True,
|
||||
states={"new": [("readonly", False)]},
|
||||
)
|
||||
user_id = fields.Many2one(
|
||||
comodel_name="res.users",
|
||||
related="employee_id.user_id",
|
||||
string="User",
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
date_start = fields.Date(
|
||||
string="Date From",
|
||||
default=lambda self: self._default_date_start(),
|
||||
required=True,
|
||||
index=True,
|
||||
readonly=True,
|
||||
states={"new": [("readonly", False)]},
|
||||
)
|
||||
date_end = fields.Date(
|
||||
string="Date To",
|
||||
default=lambda self: self._default_date_end(),
|
||||
required=True,
|
||||
index=True,
|
||||
readonly=True,
|
||||
states={"new": [("readonly", False)]},
|
||||
)
|
||||
timesheet_ids = fields.One2many(
|
||||
comodel_name="account.analytic.line",
|
||||
inverse_name="sheet_id",
|
||||
string="Timesheets",
|
||||
readonly=True,
|
||||
states={"new": [("readonly", False)], "draft": [("readonly", False)]},
|
||||
)
|
||||
line_ids = fields.One2many(
|
||||
comodel_name="hr_timesheet.sheet.line",
|
||||
compute="_compute_line_ids",
|
||||
string="Timesheet Sheet Lines",
|
||||
readonly=True,
|
||||
states={"new": [("readonly", False)], "draft": [("readonly", False)]},
|
||||
)
|
||||
new_line_ids = fields.One2many(
|
||||
comodel_name="hr_timesheet.sheet.new.analytic.line",
|
||||
inverse_name="sheet_id",
|
||||
string="Temporary Timesheets",
|
||||
readonly=True,
|
||||
states={"new": [("readonly", False)], "draft": [("readonly", False)]},
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
("new", "New"),
|
||||
("draft", "Open"),
|
||||
("confirm", "Waiting Review"),
|
||||
("done", "Approved"),
|
||||
],
|
||||
default="new",
|
||||
tracking=True,
|
||||
string="Status",
|
||||
required=True,
|
||||
readonly=True,
|
||||
index=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company",
|
||||
string="Company",
|
||||
default=lambda self: self.env.company,
|
||||
required=True,
|
||||
readonly=True,
|
||||
)
|
||||
review_policy = fields.Selection(
|
||||
selection=lambda self: self._selection_review_policy(),
|
||||
default=lambda self: self._default_review_policy(),
|
||||
required=True,
|
||||
readonly=True,
|
||||
)
|
||||
department_id = fields.Many2one(
|
||||
comodel_name="hr.department",
|
||||
string="Department",
|
||||
default=lambda self: self._default_department_id(),
|
||||
readonly=True,
|
||||
states={"new": [("readonly", False)]},
|
||||
)
|
||||
reviewer_id = fields.Many2one(
|
||||
comodel_name="hr.employee", string="Reviewer", readonly=True, tracking=True
|
||||
)
|
||||
add_line_project_id = fields.Many2one(
|
||||
comodel_name="project.project",
|
||||
string="Select Project",
|
||||
domain="[('company_id', '=', company_id), ('allow_timesheets', '=', True)]",
|
||||
help="If selected, the associated project is added "
|
||||
"to the timesheet sheet when clicked the button.",
|
||||
)
|
||||
add_line_task_id = fields.Many2one(
|
||||
comodel_name="project.task",
|
||||
string="Select Task",
|
||||
domain="[('id', 'in', available_task_ids)]",
|
||||
help="If selected, the associated task is added "
|
||||
"to the timesheet sheet when clicked the button.",
|
||||
)
|
||||
available_task_ids = fields.Many2many(
|
||||
comodel_name="project.task",
|
||||
string="Available Tasks",
|
||||
compute="_compute_available_task_ids",
|
||||
)
|
||||
total_time = fields.Float(compute="_compute_total_time", store=True)
|
||||
can_review = fields.Boolean(
|
||||
compute="_compute_can_review", search="_search_can_review"
|
||||
)
|
||||
complete_name = fields.Char(compute="_compute_complete_name")
|
||||
|
||||
@api.depends("date_start", "date_end")
|
||||
def _compute_name(self):
|
||||
locale = self.env.context.get("lang") or self.env.user.lang or "en_US"
|
||||
for sheet in self:
|
||||
if sheet.date_start == sheet.date_end:
|
||||
sheet.name = babel.dates.format_skeleton(
|
||||
skeleton="MMMEd",
|
||||
datetime=datetime.combine(sheet.date_start, time.min),
|
||||
locale=locale,
|
||||
)
|
||||
continue
|
||||
|
||||
period_start = sheet.date_start.strftime("%V, %Y")
|
||||
period_end = sheet.date_end.strftime("%V, %Y")
|
||||
|
||||
if sheet.date_end <= sheet.date_start + relativedelta(weekday=SU):
|
||||
sheet.name = _("Week %(end)s", end=period_end)
|
||||
else:
|
||||
sheet.name = _(
|
||||
"Weeks %(start)s - %(end)s", start=period_start, end=period_end
|
||||
)
|
||||
|
||||
@api.depends("timesheet_ids.unit_amount")
|
||||
def _compute_total_time(self):
|
||||
for sheet in self:
|
||||
sheet.total_time = sum(sheet.mapped("timesheet_ids.unit_amount"))
|
||||
|
||||
@api.depends("review_policy")
|
||||
def _compute_can_review(self):
|
||||
for sheet in self:
|
||||
sheet.can_review = self.env.user in sheet._get_possible_reviewers()
|
||||
|
||||
@api.model
|
||||
def _search_can_review(self, operator, value):
|
||||
def check_in(users):
|
||||
return self.env.user in users
|
||||
|
||||
def check_not_in(users):
|
||||
return self.env.user not in users
|
||||
|
||||
if (operator == "=" and value) or (operator in ["<>", "!="] and not value):
|
||||
check = check_in
|
||||
else:
|
||||
check = check_not_in
|
||||
|
||||
sheets = self.search([]).filtered(
|
||||
lambda sheet: check(sheet._get_possible_reviewers())
|
||||
)
|
||||
return [("id", "in", sheets.ids)]
|
||||
|
||||
@api.depends("name", "employee_id")
|
||||
def _compute_complete_name(self):
|
||||
for sheet in self:
|
||||
complete_name = sheet.name
|
||||
complete_name_components = sheet._get_complete_name_components()
|
||||
if complete_name_components:
|
||||
complete_name = "{} ({})".format(
|
||||
complete_name,
|
||||
", ".join(complete_name_components),
|
||||
)
|
||||
sheet.complete_name = complete_name
|
||||
|
||||
@api.constrains("date_start", "date_end")
|
||||
def _check_start_end_dates(self):
|
||||
for sheet in self:
|
||||
if sheet.date_start > sheet.date_end:
|
||||
raise ValidationError(
|
||||
_("The start date cannot be later than the end date.")
|
||||
)
|
||||
|
||||
def _get_complete_name_components(self):
|
||||
"""Hook for extensions"""
|
||||
self.ensure_one()
|
||||
return [self.employee_id.name_get()[0][1]]
|
||||
|
||||
def _get_overlapping_sheet_domain(self):
|
||||
"""Hook for extensions"""
|
||||
self.ensure_one()
|
||||
return [
|
||||
("id", "!=", self.id),
|
||||
("date_start", "<=", self.date_end),
|
||||
("date_end", ">=", self.date_start),
|
||||
("employee_id", "=", self.employee_id.id),
|
||||
("company_id", "=", self._get_timesheet_sheet_company().id),
|
||||
]
|
||||
|
||||
@api.constrains(
|
||||
"date_start", "date_end", "company_id", "employee_id", "review_policy"
|
||||
)
|
||||
def _check_overlapping_sheets(self):
|
||||
for sheet in self:
|
||||
overlapping_sheets = self.search(sheet._get_overlapping_sheet_domain())
|
||||
if overlapping_sheets:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"You cannot have 2 or more sheets that overlap!\n"
|
||||
'Please use the menu "Timesheet Sheet" '
|
||||
"to avoid this problem.\nConflicting sheets:\n - %(names)s",
|
||||
names=(
|
||||
"\n - ".join(overlapping_sheets.mapped("complete_name")),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@api.constrains("company_id", "employee_id")
|
||||
def _check_company_id_employee_id(self):
|
||||
for rec in self.sudo():
|
||||
if (
|
||||
rec.company_id
|
||||
and rec.employee_id.company_id
|
||||
and rec.company_id != rec.employee_id.company_id
|
||||
):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"The Company in the Timesheet Sheet and in "
|
||||
"the Employee must be the same."
|
||||
)
|
||||
)
|
||||
|
||||
@api.constrains("company_id", "department_id")
|
||||
def _check_company_id_department_id(self):
|
||||
for rec in self.sudo():
|
||||
if (
|
||||
rec.company_id
|
||||
and rec.department_id.company_id
|
||||
and rec.company_id != rec.department_id.company_id
|
||||
):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"The Company in the Timesheet Sheet and in "
|
||||
"the Department must be the same."
|
||||
)
|
||||
)
|
||||
|
||||
@api.constrains("company_id", "add_line_project_id")
|
||||
def _check_company_id_add_line_project_id(self):
|
||||
for rec in self.sudo():
|
||||
if (
|
||||
rec.company_id
|
||||
and rec.add_line_project_id.company_id
|
||||
and rec.company_id != rec.add_line_project_id.company_id
|
||||
):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"The Company in the Timesheet Sheet and in "
|
||||
"the Project must be the same."
|
||||
)
|
||||
)
|
||||
|
||||
@api.constrains("company_id", "add_line_task_id")
|
||||
def _check_company_id_add_line_task_id(self):
|
||||
for rec in self.sudo():
|
||||
if (
|
||||
rec.company_id
|
||||
and rec.add_line_task_id.company_id
|
||||
and rec.company_id != rec.add_line_task_id.company_id
|
||||
):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"The Company in the Timesheet Sheet and in "
|
||||
"the Task must be the same."
|
||||
)
|
||||
)
|
||||
|
||||
def _get_possible_reviewers(self):
|
||||
self.ensure_one()
|
||||
res = self.env["res.users"].browse(SUPERUSER_ID)
|
||||
if self.review_policy == "hr":
|
||||
res |= self.env.ref("hr.group_hr_user").users
|
||||
elif self.review_policy == "hr_manager":
|
||||
res |= self.env.ref("hr.group_hr_manager").users
|
||||
elif self.review_policy == "timesheet_manager":
|
||||
res |= self.env.ref("hr_timesheet.group_hr_timesheet_approver").users
|
||||
return res
|
||||
|
||||
def _get_timesheet_sheet_company(self):
|
||||
self.ensure_one()
|
||||
employee = self.employee_id
|
||||
company = employee.company_id or employee.department_id.company_id
|
||||
if not company:
|
||||
company = employee.user_id.company_id
|
||||
return company
|
||||
|
||||
@api.onchange("employee_id")
|
||||
def _onchange_employee_id(self):
|
||||
if self.employee_id:
|
||||
company = self._get_timesheet_sheet_company()
|
||||
self.company_id = company
|
||||
self.review_policy = company.timesheet_sheet_review_policy
|
||||
self.department_id = self.employee_id.department_id
|
||||
|
||||
def _get_timesheet_sheet_lines_domain(self):
|
||||
self.ensure_one()
|
||||
return [
|
||||
("date", "<=", self.date_end),
|
||||
("date", ">=", self.date_start),
|
||||
("employee_id", "=", self.employee_id.id),
|
||||
("company_id", "=", self._get_timesheet_sheet_company().id),
|
||||
("project_id", "!=", False),
|
||||
]
|
||||
|
||||
@api.depends("date_start", "date_end")
|
||||
def _compute_line_ids(self):
|
||||
SheetLine = self.env["hr_timesheet.sheet.line"]
|
||||
for sheet in self:
|
||||
if not all([sheet.date_start, sheet.date_end]):
|
||||
continue
|
||||
matrix = sheet._get_data_matrix()
|
||||
vals_list = []
|
||||
for key in sorted(matrix, key=lambda key: sheet._get_matrix_sortby(key)):
|
||||
vals_list.append(sheet._get_default_sheet_line(matrix, key))
|
||||
if sheet.state in ["new", "draft"] and self.env.context.get(
|
||||
"hr_timesheet_sheet_clean_timesheets", True
|
||||
):
|
||||
sheet.clean_timesheets(matrix[key])
|
||||
sheet.line_ids = [(6, 0, SheetLine.create(vals_list).ids)]
|
||||
|
||||
@api.model
|
||||
def _matrix_key_attributes(self):
|
||||
"""Hook for extensions"""
|
||||
return ["date", "project_id", "task_id"]
|
||||
|
||||
@api.model
|
||||
def _matrix_key(self):
|
||||
return namedtuple("MatrixKey", self._matrix_key_attributes())
|
||||
|
||||
@api.model
|
||||
def _get_matrix_key_values_for_line(self, aal):
|
||||
"""Hook for extensions"""
|
||||
return {"date": aal.date, "project_id": aal.project_id, "task_id": aal.task_id}
|
||||
|
||||
@api.model
|
||||
def _get_matrix_sortby(self, key):
|
||||
res = []
|
||||
for attribute in key:
|
||||
if hasattr(attribute, "name_get"):
|
||||
name = attribute.name_get()
|
||||
value = name[0][1] if name else ""
|
||||
else:
|
||||
value = attribute
|
||||
res.append(value)
|
||||
return res
|
||||
|
||||
def _get_data_matrix(self):
|
||||
self.ensure_one()
|
||||
MatrixKey = self._matrix_key()
|
||||
matrix = {}
|
||||
empty_line = self.env["account.analytic.line"]
|
||||
for line in self.timesheet_ids:
|
||||
key = MatrixKey(**self._get_matrix_key_values_for_line(line))
|
||||
if key not in matrix:
|
||||
matrix[key] = empty_line
|
||||
matrix[key] += line
|
||||
for date in self._get_dates():
|
||||
for key in matrix.copy():
|
||||
key = MatrixKey(**{**key._asdict(), "date": date})
|
||||
if key not in matrix:
|
||||
matrix[key] = empty_line
|
||||
return matrix
|
||||
|
||||
def _compute_timesheet_ids(self):
|
||||
AccountAnalyticLines = self.env["account.analytic.line"]
|
||||
for sheet in self:
|
||||
domain = sheet._get_timesheet_sheet_lines_domain()
|
||||
timesheets = AccountAnalyticLines.search(domain)
|
||||
sheet.link_timesheets_to_sheet(timesheets)
|
||||
sheet.timesheet_ids = [(6, 0, timesheets.ids)]
|
||||
|
||||
@api.onchange("date_start", "date_end", "employee_id")
|
||||
def _onchange_scope(self):
|
||||
self._compute_timesheet_ids()
|
||||
|
||||
@api.onchange("date_start", "date_end")
|
||||
def _onchange_dates(self):
|
||||
if self.date_start > self.date_end:
|
||||
self.date_end = self.date_start
|
||||
|
||||
@api.onchange("timesheet_ids")
|
||||
def _onchange_timesheets(self):
|
||||
self._compute_line_ids()
|
||||
|
||||
@api.depends(
|
||||
"add_line_project_id", "company_id", "timesheet_ids", "timesheet_ids.task_id"
|
||||
)
|
||||
def _compute_available_task_ids(self):
|
||||
project_task_obj = self.env["project.task"]
|
||||
for rec in self:
|
||||
if rec.add_line_project_id:
|
||||
rec.available_task_ids = project_task_obj.search(
|
||||
[
|
||||
("project_id", "=", rec.add_line_project_id.id),
|
||||
("company_id", "=", rec.company_id.id),
|
||||
("id", "not in", rec.timesheet_ids.mapped("task_id").ids),
|
||||
]
|
||||
).ids
|
||||
else:
|
||||
rec.available_task_ids = []
|
||||
|
||||
@api.model
|
||||
def _check_employee_user_link(self, vals):
|
||||
if vals.get("employee_id"):
|
||||
employee = self.env["hr.employee"].sudo().browse(vals["employee_id"])
|
||||
if not employee.user_id:
|
||||
raise UserError(
|
||||
_(
|
||||
"In order to create a sheet for this employee, you must"
|
||||
" link him/her to an user: %s"
|
||||
)
|
||||
% (employee.name,)
|
||||
)
|
||||
return employee.user_id.id
|
||||
return False
|
||||
|
||||
def copy(self, default=None):
|
||||
if not self.env.context.get("allow_copy_timesheet"):
|
||||
raise UserError(_("You cannot duplicate a sheet."))
|
||||
return super().copy(default=default)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
self._check_employee_user_link(vals)
|
||||
res = super().create(vals_list)
|
||||
res.write({"state": "draft"})
|
||||
return res
|
||||
|
||||
def _sheet_write(self, field, recs):
|
||||
self.with_context(sheet_write=True).write({field: [(6, 0, recs.ids)]})
|
||||
|
||||
def write(self, vals):
|
||||
self._check_employee_user_link(vals)
|
||||
res = super().write(vals)
|
||||
for rec in self:
|
||||
if rec.state == "draft" and not self.env.context.get("sheet_write"):
|
||||
rec._update_analytic_lines_from_new_lines(vals)
|
||||
if "add_line_project_id" not in vals:
|
||||
rec.delete_empty_lines(True)
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
for sheet in self:
|
||||
if sheet.state in ("confirm", "done"):
|
||||
raise UserError(
|
||||
_(
|
||||
"You cannot delete a timesheet sheet which is already"
|
||||
" submitted or confirmed: %s"
|
||||
)
|
||||
% (sheet.complete_name,)
|
||||
)
|
||||
return super().unlink()
|
||||
|
||||
def onchange(self, values, field_name, field_onchange):
|
||||
"""
|
||||
Pass a flag for _compute_line_ids not to clean timesheet lines to be (kind of)
|
||||
idempotent during onchange
|
||||
"""
|
||||
return super(
|
||||
Sheet, self.with_context(hr_timesheet_sheet_clean_timesheets=False)
|
||||
).onchange(values, field_name, field_onchange)
|
||||
|
||||
def _get_informables(self):
|
||||
"""Hook for extensions"""
|
||||
self.ensure_one()
|
||||
return self.employee_id.parent_id.user_id.partner_id
|
||||
|
||||
def _get_subscribers(self):
|
||||
"""Hook for extensions"""
|
||||
self.ensure_one()
|
||||
subscribers = self._get_possible_reviewers().mapped("partner_id")
|
||||
subscribers |= self._get_informables()
|
||||
return subscribers
|
||||
|
||||
def _timesheet_subscribe_users(self):
|
||||
for sheet in self.sudo():
|
||||
subscribers = sheet._get_subscribers()
|
||||
if subscribers:
|
||||
sheet.message_subscribe(partner_ids=subscribers.ids)
|
||||
|
||||
def action_timesheet_draft(self):
|
||||
if self.filtered(lambda sheet: sheet.state != "done"):
|
||||
raise UserError(_("Cannot revert to draft a non-approved sheet."))
|
||||
self._check_can_review()
|
||||
self.write({"state": "draft", "reviewer_id": False})
|
||||
|
||||
def action_timesheet_confirm(self):
|
||||
self._timesheet_subscribe_users()
|
||||
self.reset_add_line()
|
||||
self.write({"state": "confirm"})
|
||||
|
||||
def action_timesheet_done(self):
|
||||
if self.filtered(lambda sheet: sheet.state != "confirm"):
|
||||
raise UserError(_("Cannot approve a non-submitted sheet."))
|
||||
self._check_can_review()
|
||||
self.write({"state": "done", "reviewer_id": self._get_current_reviewer().id})
|
||||
|
||||
def action_timesheet_refuse(self):
|
||||
if self.filtered(lambda sheet: sheet.state != "confirm"):
|
||||
raise UserError(_("Cannot reject a non-submitted sheet."))
|
||||
self._check_can_review()
|
||||
self.write({"state": "draft", "reviewer_id": False})
|
||||
|
||||
@api.model
|
||||
def _get_current_reviewer(self):
|
||||
reviewer = self.env["hr.employee"].search(
|
||||
[("user_id", "=", self.env.uid)], limit=1
|
||||
)
|
||||
if not reviewer:
|
||||
raise UserError(
|
||||
_(
|
||||
"In order to review a timesheet sheet, your user needs to be"
|
||||
" linked to an employee."
|
||||
)
|
||||
)
|
||||
return reviewer
|
||||
|
||||
def _check_can_review(self):
|
||||
if self.filtered(lambda x: not x.can_review and x.review_policy == "hr"):
|
||||
raise UserError(_("Only a HR Officer or Manager can review the sheet."))
|
||||
|
||||
def button_add_line(self):
|
||||
for rec in self:
|
||||
if rec.state in ["new", "draft"]:
|
||||
rec.add_line()
|
||||
rec.reset_add_line()
|
||||
|
||||
def reset_add_line(self):
|
||||
self.write({"add_line_project_id": False, "add_line_task_id": False})
|
||||
|
||||
def _get_date_name(self, date):
|
||||
name = babel.dates.format_skeleton(
|
||||
skeleton="MMMEd",
|
||||
datetime=datetime.combine(date, time.min),
|
||||
locale=(self.env.context.get("lang") or self.env.user.lang or "en_US"),
|
||||
)
|
||||
name = re.sub(r"(\s*[^\w\d\s])\s+", r"\1\n", name)
|
||||
name = re.sub(r"([\w\d])\s([\w\d])", "\\1\u00A0\\2", name)
|
||||
return name
|
||||
|
||||
def _get_dates(self):
|
||||
start = self.date_start
|
||||
end = self.date_end
|
||||
if end < start:
|
||||
return []
|
||||
dates = [start]
|
||||
while start != end:
|
||||
start += relativedelta(days=1)
|
||||
dates.append(start)
|
||||
return dates
|
||||
|
||||
def _get_line_name(self, project_id, task_id=None, **kwargs):
|
||||
self.ensure_one()
|
||||
if task_id:
|
||||
return "{} - {}".format(
|
||||
project_id.name_get()[0][1], task_id.name_get()[0][1]
|
||||
)
|
||||
|
||||
return project_id.name_get()[0][1]
|
||||
|
||||
def _get_new_line_unique_id(self):
|
||||
"""Hook for extensions"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
"project_id": self.add_line_project_id,
|
||||
"task_id": self.add_line_task_id,
|
||||
}
|
||||
|
||||
def _get_default_sheet_line(self, matrix, key):
|
||||
self.ensure_one()
|
||||
values = {
|
||||
"value_x": self._get_date_name(key.date),
|
||||
"value_y": self._get_line_name(**key._asdict()),
|
||||
"date": key.date,
|
||||
"project_id": key.project_id.id,
|
||||
"task_id": key.task_id.id,
|
||||
"unit_amount": sum(t.unit_amount for t in matrix[key]),
|
||||
"employee_id": self.employee_id.id,
|
||||
"company_id": self.company_id.id,
|
||||
}
|
||||
if self.id:
|
||||
values.update({"sheet_id": self.id})
|
||||
return values
|
||||
|
||||
@api.model
|
||||
def _prepare_empty_analytic_line(self):
|
||||
return {
|
||||
"name": empty_name,
|
||||
"employee_id": self.employee_id.id,
|
||||
"date": self.date_start,
|
||||
"project_id": self.add_line_project_id.id,
|
||||
"task_id": self.add_line_task_id.id,
|
||||
"sheet_id": self.id,
|
||||
"unit_amount": 0.0,
|
||||
"company_id": self.company_id.id,
|
||||
}
|
||||
|
||||
def add_line(self):
|
||||
if not self.add_line_project_id:
|
||||
return
|
||||
values = self._prepare_empty_analytic_line()
|
||||
new_line_unique_id = self._get_new_line_unique_id()
|
||||
existing_unique_ids = list(
|
||||
{frozenset(line.get_unique_id().items()) for line in self.line_ids}
|
||||
)
|
||||
if existing_unique_ids:
|
||||
self.delete_empty_lines(False)
|
||||
if frozenset(new_line_unique_id.items()) not in existing_unique_ids:
|
||||
new_line = self.env["account.analytic.line"]._sheet_create(values)
|
||||
self.write({"timesheet_ids": [(4, new_line.id)]})
|
||||
|
||||
def link_timesheets_to_sheet(self, timesheets):
|
||||
self.ensure_one()
|
||||
if self.id and self.state in ["new", "draft"]:
|
||||
for aal in timesheets.filtered(lambda a: not a.sheet_id):
|
||||
aal.write({"sheet_id": self.id})
|
||||
|
||||
def clean_timesheets(self, timesheets):
|
||||
repeated = timesheets.filtered(
|
||||
lambda t: t.name == empty_name and not t.timesheet_invoice_id
|
||||
)
|
||||
if len(repeated) > 1 and self.id:
|
||||
return repeated.merge_timesheets()
|
||||
return timesheets
|
||||
|
||||
def _is_add_line(self, row):
|
||||
"""Hook for extensions"""
|
||||
self.ensure_one()
|
||||
return (
|
||||
self.add_line_project_id == row.project_id
|
||||
and self.add_line_task_id == row.task_id
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _is_line_of_row(self, aal, row):
|
||||
"""Hook for extensions"""
|
||||
return (
|
||||
aal.project_id.id == row.project_id.id and aal.task_id.id == row.task_id.id
|
||||
)
|
||||
|
||||
def delete_empty_lines(self, delete_empty_rows=False):
|
||||
self.ensure_one()
|
||||
for name in list(set(self.line_ids.mapped("value_y"))):
|
||||
rows = self.line_ids.filtered(lambda l: l.value_y == name)
|
||||
if not rows:
|
||||
continue
|
||||
row = fields.first(rows)
|
||||
if delete_empty_rows and self._is_add_line(row):
|
||||
check = any([line.unit_amount for line in rows])
|
||||
else:
|
||||
check = not all([line.unit_amount for line in rows])
|
||||
if not check:
|
||||
continue
|
||||
row_lines = self.timesheet_ids.filtered(
|
||||
lambda aal: self._is_line_of_row(aal, row)
|
||||
)
|
||||
row_lines.filtered(
|
||||
lambda t: t.name == empty_name
|
||||
and not t.unit_amount
|
||||
and not t.timesheet_invoice_id
|
||||
).unlink()
|
||||
if self.timesheet_ids != self.timesheet_ids.exists():
|
||||
self._sheet_write("timesheet_ids", self.timesheet_ids.exists())
|
||||
|
||||
def _update_analytic_lines_from_new_lines(self, vals):
|
||||
self.ensure_one()
|
||||
new_line_ids_list = []
|
||||
for line in vals.get("line_ids", []):
|
||||
# Every time we change a value in the grid a new line in line_ids
|
||||
# is created with the proposed changes, even though the line_ids
|
||||
# is a computed field. We capture the value of 'new_line_ids'
|
||||
# in the proposed dict before it disappears.
|
||||
# This field holds the ids of the transient records
|
||||
# of model 'hr_timesheet.sheet.new.analytic.line'.
|
||||
if line[0] == 1 and line[2] and line[2].get("new_line_id"):
|
||||
new_line_ids_list += [line[2].get("new_line_id")]
|
||||
for new_line in self.new_line_ids.exists():
|
||||
if new_line.id in new_line_ids_list:
|
||||
new_line._update_analytic_lines()
|
||||
self.new_line_ids.exists().unlink()
|
||||
self._sheet_write("new_line_ids", self.new_line_ids.exists())
|
||||
|
||||
@api.model
|
||||
def _prepare_new_line(self, line):
|
||||
"""Hook for extensions"""
|
||||
return {
|
||||
"sheet_id": line.sheet_id.id,
|
||||
"date": line.date,
|
||||
"project_id": line.project_id.id,
|
||||
"task_id": line.task_id.id,
|
||||
"unit_amount": line.unit_amount,
|
||||
"company_id": line.company_id.id,
|
||||
"employee_id": line.employee_id.id,
|
||||
}
|
||||
|
||||
def _is_compatible_new_line(self, line_a, line_b):
|
||||
"""Hook for extensions"""
|
||||
self.ensure_one()
|
||||
return (
|
||||
line_a.project_id.id == line_b.project_id.id
|
||||
and line_a.task_id.id == line_b.task_id.id
|
||||
and line_a.date == line_b.date
|
||||
)
|
||||
|
||||
def add_new_line(self, line):
|
||||
self.ensure_one()
|
||||
new_line_model = self.env["hr_timesheet.sheet.new.analytic.line"]
|
||||
new_line = self.new_line_ids.filtered(
|
||||
lambda l: self._is_compatible_new_line(l, line)
|
||||
)
|
||||
if new_line:
|
||||
new_line.write({"unit_amount": line.unit_amount})
|
||||
else:
|
||||
vals = self._prepare_new_line(line)
|
||||
new_line = new_line_model.create(vals)
|
||||
self._sheet_write("new_line_ids", self.new_line_ids | new_line)
|
||||
line.new_line_id = new_line.id
|
||||
|
||||
@api.model
|
||||
def _get_period_start(self, company, date):
|
||||
r = company and company.sheet_range or "WEEKLY"
|
||||
if r == "WEEKLY":
|
||||
if company.timesheet_week_start:
|
||||
delta = relativedelta(weekday=int(company.timesheet_week_start), days=6)
|
||||
else:
|
||||
delta = relativedelta(days=date.weekday())
|
||||
return date - delta
|
||||
elif r == "MONTHLY":
|
||||
return date + relativedelta(day=1)
|
||||
return date
|
||||
|
||||
@api.model
|
||||
def _get_period_end(self, company, date):
|
||||
r = company and company.sheet_range or "WEEKLY"
|
||||
if r == "WEEKLY":
|
||||
if company.timesheet_week_start:
|
||||
delta = relativedelta(
|
||||
weekday=(int(company.timesheet_week_start) + 6) % 7
|
||||
)
|
||||
else:
|
||||
delta = relativedelta(days=6 - date.weekday())
|
||||
return date + delta
|
||||
elif r == "MONTHLY":
|
||||
return date + relativedelta(months=1, day=1, days=-1)
|
||||
return date
|
||||
|
||||
# ------------------------------------------------
|
||||
# OpenChatter methods and notifications
|
||||
# ------------------------------------------------
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
if self:
|
||||
record = self[0]
|
||||
if "state" in init_values and record.state == "confirm":
|
||||
return self.env.ref("hr_timesheet_sheet.mt_timesheet_confirmed")
|
||||
elif "state" in init_values and record.state == "done":
|
||||
return self.env.ref("hr_timesheet_sheet.mt_timesheet_approved")
|
||||
return super()._track_subtype(init_values)
|
||||
|
||||
|
||||
class AbstractSheetLine(models.AbstractModel):
|
||||
_name = "hr_timesheet.sheet.line.abstract"
|
||||
_description = "Abstract Timesheet Sheet Line"
|
||||
|
||||
sheet_id = fields.Many2one(comodel_name="hr_timesheet.sheet", ondelete="cascade")
|
||||
date = fields.Date()
|
||||
project_id = fields.Many2one(comodel_name="project.project", string="Project")
|
||||
task_id = fields.Many2one(comodel_name="project.task", string="Task")
|
||||
unit_amount = fields.Float(string="Quantity", default=0.0)
|
||||
company_id = fields.Many2one(comodel_name="res.company", string="Company")
|
||||
employee_id = fields.Many2one(comodel_name="hr.employee", string="Employee")
|
||||
|
||||
def get_unique_id(self):
|
||||
"""Hook for extensions"""
|
||||
self.ensure_one()
|
||||
return {"project_id": self.project_id, "task_id": self.task_id}
|
||||
|
||||
|
||||
class SheetLine(models.TransientModel):
|
||||
_name = "hr_timesheet.sheet.line"
|
||||
_inherit = "hr_timesheet.sheet.line.abstract"
|
||||
_description = "Timesheet Sheet Line"
|
||||
|
||||
value_x = fields.Char(string="Date Name")
|
||||
value_y = fields.Char(string="Project Name")
|
||||
new_line_id = fields.Integer(default=0)
|
||||
|
||||
@api.onchange("unit_amount")
|
||||
def onchange_unit_amount(self):
|
||||
"""This method is called when filling a cell of the matrix."""
|
||||
self.ensure_one()
|
||||
sheet = self._get_sheet()
|
||||
if not sheet:
|
||||
return {
|
||||
"warning": {
|
||||
"title": _("Warning"),
|
||||
"message": _("Save the Timesheet Sheet first."),
|
||||
}
|
||||
}
|
||||
sheet.add_new_line(self)
|
||||
|
||||
@api.model
|
||||
def _get_sheet(self):
|
||||
sheet = (self._origin or self).sheet_id
|
||||
if not sheet:
|
||||
model = self.env.context.get("params", {}).get("model", "")
|
||||
obj_id = self.env.context.get("params", {}).get("id")
|
||||
if model == "hr_timesheet.sheet" and isinstance(obj_id, int):
|
||||
sheet = self.env["hr_timesheet.sheet"].browse(obj_id)
|
||||
return sheet
|
||||
|
||||
|
||||
class SheetNewAnalyticLine(models.TransientModel):
|
||||
_name = "hr_timesheet.sheet.new.analytic.line"
|
||||
_inherit = "hr_timesheet.sheet.line.abstract"
|
||||
_description = "Timesheet Sheet New Analytic Line"
|
||||
|
||||
@api.model
|
||||
def _is_similar_analytic_line(self, aal):
|
||||
"""Hook for extensions"""
|
||||
return (
|
||||
aal.date == self.date
|
||||
and aal.project_id.id == self.project_id.id
|
||||
and aal.task_id.id == self.task_id.id
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _update_analytic_lines(self):
|
||||
sheet = self.sheet_id
|
||||
timesheets = sheet.timesheet_ids.filtered(
|
||||
lambda aal: self._is_similar_analytic_line(aal)
|
||||
)
|
||||
new_ts = timesheets.filtered(lambda t: t.name == empty_name)
|
||||
amount = sum(t.unit_amount for t in timesheets)
|
||||
diff_amount = self.unit_amount - amount
|
||||
if len(new_ts) > 1:
|
||||
new_ts = new_ts.merge_timesheets()
|
||||
sheet._sheet_write("timesheet_ids", sheet.timesheet_ids.exists())
|
||||
if not diff_amount:
|
||||
return
|
||||
if new_ts:
|
||||
unit_amount = new_ts.unit_amount + diff_amount
|
||||
if unit_amount:
|
||||
new_ts.write({"unit_amount": unit_amount})
|
||||
else:
|
||||
new_ts.unlink()
|
||||
sheet._sheet_write("timesheet_ids", sheet.timesheet_ids.exists())
|
||||
else:
|
||||
new_ts_values = sheet._prepare_new_line(self)
|
||||
new_ts_values.update({"name": empty_name, "unit_amount": diff_amount})
|
||||
self.env["account.analytic.line"]._sheet_create(new_ts_values)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# Copyright 2018 ForgeFlow, S.L.
|
||||
# Copyright 2019 Brainbean Apps (https://brainbeanapps.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from odoo import fields, models
|
||||
|
||||
_WEEKDAYS = [
|
||||
("0", "Monday"),
|
||||
("1", "Tuesday"),
|
||||
("2", "Wednesday"),
|
||||
("3", "Thursday"),
|
||||
("4", "Friday"),
|
||||
("5", "Saturday"),
|
||||
("6", "Sunday"),
|
||||
]
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
sheet_range = fields.Selection(
|
||||
[("MONTHLY", "Month"), ("WEEKLY", "Week"), ("DAILY", "Day")],
|
||||
string="Timesheet Sheet Range",
|
||||
default="WEEKLY",
|
||||
help="The range of your Timesheet Sheet.",
|
||||
)
|
||||
|
||||
timesheet_week_start = fields.Selection(
|
||||
selection=_WEEKDAYS, string="Week start day", default="0"
|
||||
)
|
||||
|
||||
timesheet_sheet_review_policy = fields.Selection(
|
||||
selection=[
|
||||
("hr", "By HR Officers"),
|
||||
("hr_manager", "By HR Managers"),
|
||||
("timesheet_manager", "By Timesheets Managers"),
|
||||
],
|
||||
default="hr",
|
||||
help="How Timesheet Sheets review is performed.",
|
||||
)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright 2018 ForgeFlow, S.L.
|
||||
# Copyright 2019 Brainbean Apps (https://brainbeanapps.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfig(models.TransientModel):
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
sheet_range = fields.Selection(
|
||||
related="company_id.sheet_range",
|
||||
string="Timesheet Sheet Range",
|
||||
help="The range of your Timesheet Sheet.",
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
timesheet_week_start = fields.Selection(
|
||||
related="company_id.timesheet_week_start",
|
||||
string="Week Start Day",
|
||||
help="Starting day for Timesheet Sheets.",
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
timesheet_sheet_review_policy = fields.Selection(
|
||||
related="company_id.timesheet_sheet_review_policy", readonly=False
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
If you want other default ranges different from weekly, you need to go:
|
||||
|
||||
* In the menu `Configuration` -> `Settings` -> **Timesheet Options**,
|
||||
and select in **Timesheet Sheet Range** the default range you want.
|
||||
* When you have a weekly range you can also specify the **Week Start Day**.
|
||||
|
||||
To change who reviews submitted sheets, go to *Configuration > Settings > Timesheet Options*
|
||||
and configure **Timesheet Sheet Review Policy** accordingly.
|
||||
|
||||
For adding more review policies, look at the *hr_timesheet_sheet_policy_xxx*
|
||||
extra modules.
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
* Miquel Raïch <miquel.raich@forgeflow.com>
|
||||
* Andrea Stirpe <a.stirpe@onestein.nl>
|
||||
* Lois Rilo <lois.rilo@forgeflow.com>
|
||||
* `CorporateHub <https://corporatehub.eu/>`__
|
||||
|
||||
* Alexey Pelykh <alexey.pelykh@corphub.eu>
|
||||
|
||||
* Dennis Sluijk <d.sluijk@onestein.nl>
|
||||
* Sunanda Chhatbar <sunanda.chhatbar@initos.com>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
This module supplies a new screen enabling you to manage your work encoding
|
||||
(timesheet) by period. Timesheet entries are made by employees each day. At the
|
||||
end of the defined period, employees submit their validated sheet and the
|
||||
reviewer must then approve submitted entries. Periods are defined in the
|
||||
company forms and you can set them to run monthly, weekly or daily. By default,
|
||||
policy is configured to have HR Officers as reviewers.
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
This module relies on:
|
||||
|
||||
* The OCA module '2D matrix for x2many fields', and can be downloaded from
|
||||
Github: https://github.com/OCA/web/tree/16.0/web_widget_x2many_2d_matrix
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
* The timesheet grid is limited to display a max. of 1M cells, due to a
|
||||
limitation of the tree view limit parameter not being able to dynamically
|
||||
set a limit. Since default value of odoo, 40 records is too small, we decided
|
||||
to set 1M, which should be good enough in the majority of scenarios.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
If you modify the `Details` tab, automatically the `Summary` tab is updated.
|
||||
But if you modify the `Summary` tab, you need to save in order to have the `Details` tab updated.
|
||||
|
||||
In case you modify the unit amount of both tabs, the `Details` tab will prevail.
|
||||
If you modify the `Summary` tab, and you need to do a change in the `Details` tab, please save before.
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
<odoo noupdate="1">
|
||||
<record model="ir.rule" id="timesheet_sheet_comp_rule">
|
||||
<field name="name">Timesheet sheet multi-company</field>
|
||||
<field
|
||||
name="model_id"
|
||||
search="[('model','=','hr_timesheet.sheet')]"
|
||||
model="ir.model"
|
||||
/>
|
||||
<field name="global" eval="True" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|',('company_id','=',False),('company_id','in',company_ids)]</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_hr_timesheet_sheet_user,hr_timesheet.sheet,model_hr_timesheet_sheet,base.group_user,1,1,1,1
|
||||
access_hr_timesheet_sheet_line,hr_timesheet.sheet.line,model_hr_timesheet_sheet_line,hr_timesheet.group_hr_timesheet_user,1,1,1,1
|
||||
access_hr_timesheet_sheet_new_analytic_line,hr_timesheet.sheet.new.analyticline,model_hr_timesheet_sheet_new_analytic_line,hr_timesheet.group_hr_timesheet_user,1,1,1,1
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
|
|
@ -0,0 +1,479 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>HR Timesheet Sheet</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
Despite the name, some widely supported CSS2 features are used.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: gray; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic, pre.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="hr-timesheet-sheet">
|
||||
<h1 class="title">HR Timesheet Sheet</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:faddd2bab01c77bceef23115853e6806a0b39685cf857967bba202ca08e84895
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/timesheet/tree/16.0/hr_timesheet_sheet"><img alt="OCA/timesheet" src="https://img.shields.io/badge/github-OCA%2Ftimesheet-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/timesheet-16-0/timesheet-16-0-hr_timesheet_sheet"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/timesheet&target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
||||
<p>This module supplies a new screen enabling you to manage your work encoding
|
||||
(timesheet) by period. Timesheet entries are made by employees each day. At the
|
||||
end of the defined period, employees submit their validated sheet and the
|
||||
reviewer must then approve submitted entries. Periods are defined in the
|
||||
company forms and you can set them to run monthly, weekly or daily. By default,
|
||||
policy is configured to have HR Officers as reviewers.</p>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#installation" id="toc-entry-1">Installation</a></li>
|
||||
<li><a class="reference internal" href="#configuration" id="toc-entry-2">Configuration</a></li>
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-3">Usage</a></li>
|
||||
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-4">Known issues / Roadmap</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-5">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-6">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-7">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-8">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-9">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="installation">
|
||||
<h1><a class="toc-backref" href="#toc-entry-1">Installation</a></h1>
|
||||
<p>This module relies on:</p>
|
||||
<ul class="simple">
|
||||
<li>The OCA module ‘2D matrix for x2many fields’, and can be downloaded from
|
||||
Github: <a class="reference external" href="https://github.com/OCA/web/tree/16.0/web_widget_x2many_2d_matrix">https://github.com/OCA/web/tree/16.0/web_widget_x2many_2d_matrix</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="configuration">
|
||||
<h1><a class="toc-backref" href="#toc-entry-2">Configuration</a></h1>
|
||||
<p>If you want other default ranges different from weekly, you need to go:</p>
|
||||
<ul class="simple">
|
||||
<li>In the menu <cite>Configuration</cite> -> <cite>Settings</cite> -> <strong>Timesheet Options</strong>,
|
||||
and select in <strong>Timesheet Sheet Range</strong> the default range you want.</li>
|
||||
<li>When you have a weekly range you can also specify the <strong>Week Start Day</strong>.</li>
|
||||
</ul>
|
||||
<p>To change who reviews submitted sheets, go to <em>Configuration > Settings > Timesheet Options</em>
|
||||
and configure <strong>Timesheet Sheet Review Policy</strong> accordingly.</p>
|
||||
<p>For adding more review policies, look at the <em>hr_timesheet_sheet_policy_xxx</em>
|
||||
extra modules.</p>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#toc-entry-3">Usage</a></h1>
|
||||
<p>If you modify the <cite>Details</cite> tab, automatically the <cite>Summary</cite> tab is updated.
|
||||
But if you modify the <cite>Summary</cite> tab, you need to save in order to have the <cite>Details</cite> tab updated.</p>
|
||||
<p>In case you modify the unit amount of both tabs, the <cite>Details</cite> tab will prevail.
|
||||
If you modify the <cite>Summary</cite> tab, and you need to do a change in the <cite>Details</cite> tab, please save before.</p>
|
||||
</div>
|
||||
<div class="section" id="known-issues-roadmap">
|
||||
<h1><a class="toc-backref" href="#toc-entry-4">Known issues / Roadmap</a></h1>
|
||||
<ul class="simple">
|
||||
<li>The timesheet grid is limited to display a max. of 1M cells, due to a
|
||||
limitation of the tree view limit parameter not being able to dynamically
|
||||
set a limit. Since default value of odoo, 40 records is too small, we decided
|
||||
to set 1M, which should be good enough in the majority of scenarios.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#toc-entry-5">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/timesheet/issues">GitHub Issues</a>.
|
||||
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
|
||||
<a class="reference external" href="https://github.com/OCA/timesheet/issues/new?body=module:%20hr_timesheet_sheet%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#toc-entry-6">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-7">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>ForgeFlow</li>
|
||||
<li>Onestein</li>
|
||||
<li>CorporateHub</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-8">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Miquel Raïch <<a class="reference external" href="mailto:miquel.raich@forgeflow.com">miquel.raich@forgeflow.com</a>></li>
|
||||
<li>Andrea Stirpe <<a class="reference external" href="mailto:a.stirpe@onestein.nl">a.stirpe@onestein.nl</a>></li>
|
||||
<li>Lois Rilo <<a class="reference external" href="mailto:lois.rilo@forgeflow.com">lois.rilo@forgeflow.com</a>></li>
|
||||
<li><a class="reference external" href="https://corporatehub.eu/">CorporateHub</a><ul>
|
||||
<li>Alexey Pelykh <<a class="reference external" href="mailto:alexey.pelykh@corphub.eu">alexey.pelykh@corphub.eu</a>></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Dennis Sluijk <<a class="reference external" href="mailto:d.sluijk@onestein.nl">d.sluijk@onestein.nl</a>></li>
|
||||
<li>Sunanda Chhatbar <<a class="reference external" href="mailto:sunanda.chhatbar@initos.com">sunanda.chhatbar@initos.com</a>></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-9">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org">
|
||||
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
|
||||
</a>
|
||||
<p>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.</p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/timesheet/tree/16.0/hr_timesheet_sheet">OCA/timesheet</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
|
|
@ -0,0 +1,3 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import test_hr_timesheet_sheet
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,108 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
Copyright 2018 ForgeFlow, S.L.
|
||||
Copyright 2018-2019 Brainbean Apps (https://brainbeanapps.com)
|
||||
Copyright 2018-2019 Onestein (<https://www.onestein.eu>)
|
||||
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
<odoo>
|
||||
<record id="hr_timesheet_line_form" model="ir.ui.view">
|
||||
<field name="name">account.analytic.line.form</field>
|
||||
<field name="model">account.analytic.line</field>
|
||||
<field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="date" position="after">
|
||||
<field
|
||||
name="sheet_id"
|
||||
attrs="{'invisible': [('sheet_id', '=', False)]}"
|
||||
/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
<record id="hr_timesheet_line_search" model="ir.ui.view">
|
||||
<field name="name">account.analytic.line.search</field>
|
||||
<field name="model">account.analytic.line</field>
|
||||
<field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_search" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="date" position="before">
|
||||
<field name="sheet_id" />
|
||||
</field>
|
||||
<filter name="mine" position="before">
|
||||
<filter
|
||||
name="unsubmitted"
|
||||
string="Not Submitted"
|
||||
domain="['|',('sheet_id','=',False),('sheet_id.state','=','draft')]"
|
||||
/>
|
||||
<separator />
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
<record id="act_hr_timesheet_line_to_submit" model="ir.actions.act_window">
|
||||
<field name="name">Timesheets to Submit</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">account.analytic.line</field>
|
||||
<field name="view_id" eval="False" />
|
||||
<field name="context">
|
||||
{
|
||||
'search_default_groupby_employee': 1,
|
||||
'search_default_groupby_project': 1,
|
||||
'search_default_groupby_date': 1,
|
||||
}
|
||||
</field>
|
||||
<field
|
||||
name="domain"
|
||||
>[('project_id.active','=',True),'|',('sheet_id','=',False),('sheet_id.state','=','draft')]</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Timesheets to submit.
|
||||
</p>
|
||||
<p>
|
||||
Employees must record timesheets every day and confirm at the end
|
||||
of the reporting period. Once the timesheet sheet is confirmed, it should be
|
||||
validated by a reviewer.
|
||||
</p>
|
||||
<p>
|
||||
Timesheets can also be invoiced to customers, depending on the
|
||||
configuration of each project's related contract.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<record id="act_hr_timesheet_line_to_submit_my" model="ir.actions.act_window">
|
||||
<field name="name">My Timesheets to Submit</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">account.analytic.line</field>
|
||||
<field name="view_id" eval="False" />
|
||||
<field name="context">
|
||||
{
|
||||
'search_default_groupby_project': 1,
|
||||
'search_default_groupby_date': 1,
|
||||
}
|
||||
</field>
|
||||
<field
|
||||
name="domain"
|
||||
>[('user_id', '=', uid),('project_id.active','=',True),'|',('sheet_id','=',False),('sheet_id.state','=','draft')]</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
My timesheets to submit.
|
||||
</p>
|
||||
<p>
|
||||
You must record timesheets every day and confirm at the end
|
||||
of the reporting period. Once the timesheet sheet is confirmed, it should be
|
||||
validated by a reviewer.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<menuitem
|
||||
action="act_hr_timesheet_line_to_submit"
|
||||
id="menu_act_hr_timesheet_line_to_submit"
|
||||
parent="hr_timesheet.menu_hr_time_tracking"
|
||||
groups="hr_timesheet.group_timesheet_manager"
|
||||
sequence="6"
|
||||
/>
|
||||
<menuitem
|
||||
action="act_hr_timesheet_line_to_submit_my"
|
||||
id="menu_act_hr_timesheet_line_to_submit_my"
|
||||
parent="hr_timesheet_sheet.menu_hr_my_timesheets"
|
||||
sequence="5"
|
||||
/>
|
||||
</odoo>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue