mirror of
https://github.com/bringout/oca-mrp.git
synced 2026-04-23 05:52:08 +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
48
odoo-bringout-oca-timesheet-hr_timesheet_sheet/README.md
Normal file
48
odoo-bringout-oca-timesheet-hr_timesheet_sheet/README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# HR Timesheet Sheet
|
||||
|
||||
Odoo addon: hr_timesheet_sheet
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-timesheet-hr_timesheet_sheet
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- hr_timesheet
|
||||
- sale_timesheet
|
||||
- web_widget_x2many_2d_matrix
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: HR Timesheet Sheet
|
||||
- **Version**: 16.0.1.2.0
|
||||
- **Category**: Human Resources
|
||||
- **License**: AGPL-3
|
||||
- **Installable**: True
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/timesheet](https://github.com/OCA/timesheet) branch 16.0, addon `hr_timesheet_sheet`.
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original AGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Reports: doc/REPORTS.md
|
||||
- Security: doc/SECURITY.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[Users] -->|HTTP| V[Views and QWeb Templates]
|
||||
V --> C[Controllers]
|
||||
V --> W[Wizards – Transient Models]
|
||||
C --> M[Models and ORM]
|
||||
W --> M
|
||||
M --> R[Reports]
|
||||
DX[Data XML] --> M
|
||||
S[Security – ACLs and Groups] -. enforces .-> M
|
||||
|
||||
subgraph Hr_timesheet_sheet Module - hr_timesheet_sheet
|
||||
direction LR
|
||||
M:::layer
|
||||
W:::layer
|
||||
C:::layer
|
||||
V:::layer
|
||||
R:::layer
|
||||
S:::layer
|
||||
DX:::layer
|
||||
end
|
||||
|
||||
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
|
||||
```
|
||||
|
||||
Notes
|
||||
- Views include tree/form/kanban templates and report templates.
|
||||
- Controllers provide website/portal routes when present.
|
||||
- Wizards are UI flows implemented with `models.TransientModel`.
|
||||
- Data XML loads data/demo records; Security defines groups and access.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for hr_timesheet_sheet. Configure related models, access rights, and options as needed.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Controllers
|
||||
|
||||
This module does not define custom HTTP controllers.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [hr_timesheet](../../odoo-bringout-oca-ocb-hr_timesheet)
|
||||
- [sale_timesheet](../../odoo-bringout-oca-ocb-sale_timesheet)
|
||||
- [web_widget_x2many_2d_matrix](../../odoo-bringout-oca-web-web_widget_x2many_2d_matrix)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# FAQ
|
||||
|
||||
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
|
||||
- Q: How to enable? A: Start server with --addon hr_timesheet_sheet or install in UI.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-timesheet-hr_timesheet_sheet"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-timesheet-hr_timesheet_sheet"
|
||||
```
|
||||
22
odoo-bringout-oca-timesheet-hr_timesheet_sheet/doc/MODELS.md
Normal file
22
odoo-bringout-oca-timesheet-hr_timesheet_sheet/doc/MODELS.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in hr_timesheet_sheet.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class hr_timesheet_sheet
|
||||
class hr_timesheet_sheet_line
|
||||
class hr_timesheet_sheet_line_abstract
|
||||
class hr_timesheet_sheet_new_analytic_line
|
||||
class account_analytic_account
|
||||
class account_analytic_line
|
||||
class hr_department
|
||||
class hr_employee
|
||||
class hr_timesheet_sheet_line_abstract
|
||||
class res_company
|
||||
class res_config_settings
|
||||
```
|
||||
|
||||
Notes
|
||||
- Classes show model technical names; fields omitted for brevity.
|
||||
- Items listed under _inherit are extensions of existing models.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Overview
|
||||
|
||||
Packaged Odoo addon: hr_timesheet_sheet. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon hr_timesheet_sheet
|
||||
- License: LGPL-3
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# Security
|
||||
|
||||
Access control and security definitions in hr_timesheet_sheet.
|
||||
|
||||
## Access Control Lists (ACLs)
|
||||
|
||||
Model access permissions defined in:
|
||||
- **[ir.model.access.csv](../hr_timesheet_sheet/security/ir.model.access.csv)**
|
||||
- 3 model access rules
|
||||
|
||||
## Record Rules
|
||||
|
||||
Row-level security rules defined in:
|
||||
|
||||
## Security Groups & Configuration
|
||||
|
||||
Security groups and permissions defined in:
|
||||
- **[hr_timesheet_sheet_security.xml](../hr_timesheet_sheet/security/hr_timesheet_sheet_security.xml)**
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Security Layers"
|
||||
A[Users] --> B[Groups]
|
||||
B --> C[Access Control Lists]
|
||||
C --> D[Models]
|
||||
B --> E[Record Rules]
|
||||
E --> F[Individual Records]
|
||||
end
|
||||
```
|
||||
|
||||
Security files overview:
|
||||
- **[hr_timesheet_sheet_security.xml](../hr_timesheet_sheet/security/hr_timesheet_sheet_security.xml)**
|
||||
- Security groups, categories, and XML-based rules
|
||||
- **[ir.model.access.csv](../hr_timesheet_sheet/security/ir.model.access.csv)**
|
||||
- Model access permissions (CRUD rights)
|
||||
|
||||
Notes
|
||||
- Access Control Lists define which groups can access which models
|
||||
- Record Rules provide row-level security (filter records by user/group)
|
||||
- Security groups organize users and define permission sets
|
||||
- All security is enforced at the ORM level by Odoo
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Troubleshooting
|
||||
|
||||
- Ensure Python and Odoo environment matches repo guidance.
|
||||
- Check database connectivity and logs if startup fails.
|
||||
- Validate that dependent addons listed in DEPENDENCIES.md are installed.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Usage
|
||||
|
||||
Start Odoo including this addon (from repo root):
|
||||
|
||||
```bash
|
||||
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon hr_timesheet_sheet
|
||||
```
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Wizards
|
||||
|
||||
This module does not include UI wizards.
|
||||
|
|
@ -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
|
||||
)
|
||||
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