Compare commits
27 Commits
14.0-heif
...
18.0-fix-r
| Author | SHA1 | Date | |
|---|---|---|---|
| a48b428b06 | |||
|
|
7996ecc501 | ||
| de80996484 | |||
|
|
cc719291eb | ||
|
|
e4cc346dfe | ||
|
|
ca1a5981b2 | ||
|
|
69bc951b3e | ||
|
|
494f2b4926 | ||
|
|
a2421b89da | ||
| f4d9cd008c | |||
| 3bfe2752b5 | |||
| eecdf0c727 | |||
| 0dc8075a81 | |||
| 6c008cce4d | |||
| dc5fd2897a | |||
| f206606a96 | |||
| e8a819d594 | |||
| 0437a9c9be | |||
| d52b8c424e | |||
| 4a00e642e0 | |||
|
|
beb64d034d | ||
| ec7c402a26 | |||
|
|
fd45cf17a2 | ||
| 091fefc763 | |||
| 583b6e30b2 | |||
| 34abfad364 | |||
| edc8cffc69 |
20
.editorconfig
Normal file
20
.editorconfig
Normal file
@@ -0,0 +1,20 @@
|
||||
# Configuration for known file extensions
|
||||
[*.{css,js,json,less,md,py,rst,sass,scss,xml,yaml,yml}]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{json,yml,yaml,rst,md}]
|
||||
indent_size = 2
|
||||
|
||||
# Do not configure editor for libs and autogenerated content
|
||||
[{*/static/{lib,src/lib}/**,*/static/description/index.html,*/readme/../README.rst}]
|
||||
charset = unset
|
||||
end_of_line = unset
|
||||
indent_size = unset
|
||||
indent_style = unset
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
188
.eslintrc.yml
Normal file
188
.eslintrc.yml
Normal file
@@ -0,0 +1,188 @@
|
||||
env:
|
||||
browser: true
|
||||
es6: true
|
||||
|
||||
# See https://github.com/OCA/odoo-community.org/issues/37#issuecomment-470686449
|
||||
parserOptions:
|
||||
ecmaVersion: 2019
|
||||
|
||||
overrides:
|
||||
- files:
|
||||
- "**/*.esm.js"
|
||||
parserOptions:
|
||||
sourceType: module
|
||||
|
||||
# Globals available in Odoo that shouldn't produce errorings
|
||||
globals:
|
||||
_: readonly
|
||||
$: readonly
|
||||
fuzzy: readonly
|
||||
jQuery: readonly
|
||||
moment: readonly
|
||||
odoo: readonly
|
||||
openerp: readonly
|
||||
owl: readonly
|
||||
luxon: readonly
|
||||
|
||||
# Styling is handled by Prettier, so we only need to enable AST rules;
|
||||
# see https://github.com/OCA/maintainer-quality-tools/pull/618#issuecomment-558576890
|
||||
rules:
|
||||
accessor-pairs: warn
|
||||
array-callback-return: warn
|
||||
callback-return: warn
|
||||
capitalized-comments:
|
||||
- warn
|
||||
- always
|
||||
- ignoreConsecutiveComments: true
|
||||
ignoreInlineComments: true
|
||||
complexity:
|
||||
- warn
|
||||
- 15
|
||||
constructor-super: warn
|
||||
dot-notation: warn
|
||||
eqeqeq: warn
|
||||
global-require: warn
|
||||
handle-callback-err: warn
|
||||
id-blacklist: warn
|
||||
id-match: warn
|
||||
init-declarations: error
|
||||
max-depth: warn
|
||||
max-nested-callbacks: warn
|
||||
max-statements-per-line: warn
|
||||
no-alert: warn
|
||||
no-array-constructor: warn
|
||||
no-caller: warn
|
||||
no-case-declarations: warn
|
||||
no-class-assign: warn
|
||||
no-cond-assign: error
|
||||
no-const-assign: error
|
||||
no-constant-condition: warn
|
||||
no-control-regex: warn
|
||||
no-debugger: error
|
||||
no-delete-var: warn
|
||||
no-div-regex: warn
|
||||
no-dupe-args: error
|
||||
no-dupe-class-members: error
|
||||
no-dupe-keys: error
|
||||
no-duplicate-case: error
|
||||
no-duplicate-imports: error
|
||||
no-else-return: warn
|
||||
no-empty-character-class: warn
|
||||
no-empty-function: error
|
||||
no-empty-pattern: error
|
||||
no-empty: warn
|
||||
no-eq-null: error
|
||||
no-eval: error
|
||||
no-ex-assign: error
|
||||
no-extend-native: warn
|
||||
no-extra-bind: warn
|
||||
no-extra-boolean-cast: warn
|
||||
no-extra-label: warn
|
||||
no-fallthrough: warn
|
||||
no-func-assign: error
|
||||
no-global-assign: error
|
||||
no-implicit-coercion:
|
||||
- warn
|
||||
- allow: ["~"]
|
||||
no-implicit-globals: warn
|
||||
no-implied-eval: warn
|
||||
no-inline-comments: warn
|
||||
no-inner-declarations: warn
|
||||
no-invalid-regexp: warn
|
||||
no-irregular-whitespace: warn
|
||||
no-iterator: warn
|
||||
no-label-var: warn
|
||||
no-labels: warn
|
||||
no-lone-blocks: warn
|
||||
no-lonely-if: error
|
||||
no-mixed-requires: error
|
||||
no-multi-str: warn
|
||||
no-native-reassign: error
|
||||
no-negated-condition: warn
|
||||
no-negated-in-lhs: error
|
||||
no-new-func: warn
|
||||
no-new-object: warn
|
||||
no-new-require: warn
|
||||
no-new-symbol: warn
|
||||
no-new-wrappers: warn
|
||||
no-new: warn
|
||||
no-obj-calls: warn
|
||||
no-octal-escape: warn
|
||||
no-octal: warn
|
||||
no-param-reassign: warn
|
||||
no-path-concat: warn
|
||||
no-process-env: warn
|
||||
no-process-exit: warn
|
||||
no-proto: warn
|
||||
no-prototype-builtins: warn
|
||||
no-redeclare: warn
|
||||
no-regex-spaces: warn
|
||||
no-restricted-globals: warn
|
||||
no-restricted-imports: warn
|
||||
no-restricted-modules: warn
|
||||
no-restricted-syntax: warn
|
||||
no-return-assign: error
|
||||
no-script-url: warn
|
||||
no-self-assign: warn
|
||||
no-self-compare: warn
|
||||
no-sequences: warn
|
||||
no-shadow-restricted-names: warn
|
||||
no-shadow: warn
|
||||
no-sparse-arrays: warn
|
||||
no-sync: warn
|
||||
no-this-before-super: warn
|
||||
no-throw-literal: warn
|
||||
no-undef-init: warn
|
||||
no-undef: error
|
||||
no-unmodified-loop-condition: warn
|
||||
no-unneeded-ternary: error
|
||||
no-unreachable: error
|
||||
no-unsafe-finally: error
|
||||
no-unused-expressions: error
|
||||
no-unused-labels: error
|
||||
no-unused-vars: error
|
||||
no-use-before-define: error
|
||||
no-useless-call: warn
|
||||
no-useless-computed-key: warn
|
||||
no-useless-concat: warn
|
||||
no-useless-constructor: warn
|
||||
no-useless-escape: warn
|
||||
no-useless-rename: warn
|
||||
no-void: warn
|
||||
no-with: warn
|
||||
operator-assignment: [error, always]
|
||||
prefer-const: warn
|
||||
radix: warn
|
||||
require-yield: warn
|
||||
sort-imports: warn
|
||||
spaced-comment: [error, always]
|
||||
strict: [error, function]
|
||||
use-isnan: error
|
||||
valid-jsdoc:
|
||||
- warn
|
||||
- prefer:
|
||||
arg: param
|
||||
argument: param
|
||||
augments: extends
|
||||
constructor: class
|
||||
exception: throws
|
||||
func: function
|
||||
method: function
|
||||
prop: property
|
||||
return: returns
|
||||
virtual: abstract
|
||||
yield: yields
|
||||
preferType:
|
||||
array: Array
|
||||
bool: Boolean
|
||||
boolean: Boolean
|
||||
number: Number
|
||||
object: Object
|
||||
str: String
|
||||
string: String
|
||||
requireParamDescription: false
|
||||
requireReturn: false
|
||||
requireReturnDescription: false
|
||||
requireReturnType: false
|
||||
valid-typeof: warn
|
||||
yoda: warn
|
||||
42
.gitea/workflows/pre-commit.yml
Normal file
42
.gitea/workflows/pre-commit.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
name: pre-commit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "18.0*"
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Get python version
|
||||
run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
|
||||
# - uses: actions/cache@v4
|
||||
# with:
|
||||
# path: ~/.cache/pre-commit
|
||||
# key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- name: Install pre-commit
|
||||
run: pip install pre-commit
|
||||
- name: Run pre-commit
|
||||
run: pre-commit run --all-files --show-diff-on-failure --color=always
|
||||
env:
|
||||
# Consider valid a PR that changes README fragments but doesn't
|
||||
# change the README.rst file itself. It's not really a problem
|
||||
# because the bot will update it anyway after merge. This way, we
|
||||
# lower the barrier for functional contributors that want to fix the
|
||||
# readme fragments, while still letting developers get README
|
||||
# auto-generated (which also helps functionals when using runboat).
|
||||
# DOCS https://pre-commit.com/#temporarily-disabling-hooks
|
||||
SKIP: oca-gen-addon-readme
|
||||
- name: Check that all files generated by pre-commit are in git
|
||||
run: |
|
||||
newfiles="$(git ls-files --others --exclude-from=.gitignore)"
|
||||
if [ "$newfiles" != "" ] ; then
|
||||
echo "Please check-in the following files:"
|
||||
echo "$newfiles"
|
||||
exit 1
|
||||
fi
|
||||
150
.pre-commit-config.yaml
Normal file
150
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,150 @@
|
||||
exclude: |
|
||||
(?x)
|
||||
# NOT INSTALLABLE ADDONS
|
||||
# END NOT INSTALLABLE ADDONS
|
||||
# Files and folders generated by bots, to avoid loops
|
||||
^setup/|/static/description/index\.html$|
|
||||
# We don't want to mess with tool-generated files
|
||||
.svg$|/tests/([^/]+/)?cassettes/|^.copier-answers.yml$|^.github/|^eslint.config.cjs|^prettier.config.cjs|
|
||||
# Maybe reactivate this when all README files include prettier ignore tags?
|
||||
^README\.md$|
|
||||
# Library files can have extraneous formatting (even minimized)
|
||||
/static/(src/)?lib/|
|
||||
# Repos using Sphinx to generate docs don't need prettying
|
||||
^docs/_templates/.*\.html$|
|
||||
# Don't bother non-technical authors with formatting issues in docs
|
||||
readme/.*\.(rst|md)$|
|
||||
# Ignore build and dist directories in addons
|
||||
/build/|/dist/|
|
||||
# Ignore test files in addons
|
||||
/tests/samples/.*|
|
||||
# You don't usually want a bot to modify your legal texts
|
||||
(LICENSE.*|COPYING.*)
|
||||
default_language_version:
|
||||
python: python3
|
||||
node: "16.17.0"
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
# These files are most likely copier diff rejection junks; if found,
|
||||
# review them manually, fix the problem (if needed) and remove them
|
||||
- id: forbidden-files
|
||||
name: forbidden files
|
||||
entry: found forbidden files; remove them
|
||||
language: fail
|
||||
files: "\\.rej$"
|
||||
- id: en-po-files
|
||||
name: en.po files cannot exist
|
||||
entry: found a en.po file
|
||||
language: fail
|
||||
files: '[a-zA-Z0-9_]*/i18n/en\.po$'
|
||||
- repo: https://github.com/oca/maintainer-tools
|
||||
rev: f9b919b9868143135a9c9cb03021089cabba8223
|
||||
hooks:
|
||||
# update the NOT INSTALLABLE ADDONS section above
|
||||
- id: oca-update-pre-commit-excluded-addons
|
||||
- id: oca-fix-manifest-website
|
||||
entry:
|
||||
bash -c 'oca-fix-manifest-website "https://git.elabore.coop/elabore/$(basename
|
||||
$(git rev-parse --show-toplevel))"'
|
||||
- id: oca-gen-addon-readme
|
||||
entry:
|
||||
bash -c 'oca-gen-addon-readme
|
||||
--addons-dir=.
|
||||
--branch=$(git symbolic-ref
|
||||
refs/remotes/origin/HEAD | sed "s@^refs/remotes/origin/@@")
|
||||
--repo-name=$(basename $(git rev-parse --show-toplevel))
|
||||
--org-name="Elabore"
|
||||
--if-source-changed --keep-source-digest'
|
||||
|
||||
- repo: https://github.com/OCA/odoo-pre-commit-hooks
|
||||
rev: v0.1.4
|
||||
hooks:
|
||||
- id: oca-checks-odoo-module
|
||||
- id: oca-checks-po
|
||||
args:
|
||||
- --disable=po-pretty-format
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: prettier
|
||||
name: prettier (with plugin-xml)
|
||||
entry: prettier
|
||||
args:
|
||||
- --write
|
||||
- --list-different
|
||||
- --ignore-unknown
|
||||
types: [text]
|
||||
files: \.(css|htm|html|js|json|jsx|less|md|scss|toml|ts|xml|yaml|yml)$
|
||||
language: node
|
||||
additional_dependencies:
|
||||
- "prettier@2.7.1"
|
||||
- "@prettier/plugin-xml@2.2.0"
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: eslint
|
||||
name: eslint
|
||||
entry: eslint
|
||||
args:
|
||||
- --color
|
||||
- --fix
|
||||
verbose: true
|
||||
types: [javascript]
|
||||
language: node
|
||||
additional_dependencies:
|
||||
- "eslint@8.24.0"
|
||||
- "eslint-plugin-jsdoc@"
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
# exclude autogenerated files
|
||||
exclude: /README\.rst$|\.pot?$
|
||||
- id: end-of-file-fixer
|
||||
# exclude autogenerated files
|
||||
exclude: /README\.rst$|\.pot?$
|
||||
- id: debug-statements
|
||||
- id: fix-encoding-pragma
|
||||
args: ["--remove"]
|
||||
- id: check-case-conflict
|
||||
- id: check-docstring-first
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-merge-conflict
|
||||
# exclude files where underlines are not distinguishable from merge conflicts
|
||||
exclude: /README\.rst$|^docs/.*\.rst$
|
||||
- id: check-symlinks
|
||||
- id: check-xml
|
||||
- id: mixed-line-ending
|
||||
args: ["--fix=lf"]
|
||||
- repo: https://github.com/PyCQA/docformatter
|
||||
rev: v1.7.7
|
||||
hooks:
|
||||
- id: docformatter
|
||||
args: [
|
||||
"--in-place", # modify the files
|
||||
"--recursive", # run on all the files
|
||||
"--wrap-summaries",
|
||||
"88", # max length of 1st line
|
||||
"--wrap-descriptions",
|
||||
"88", # max length of other lines
|
||||
"--pre-summary-newline", # new line before a long summary
|
||||
"--make-summary-multi-line", # force summary on multilines
|
||||
]
|
||||
additional_dependencies: ["tomli"] # if Python <3.11
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.12.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/OCA/pylint-odoo
|
||||
rev: v9.1.3
|
||||
hooks:
|
||||
- id: pylint_odoo
|
||||
name: pylint with optional checks
|
||||
args:
|
||||
- --rcfile=.pylintrc
|
||||
- --exit-zero
|
||||
verbose: true
|
||||
- id: pylint_odoo
|
||||
args:
|
||||
- --rcfile=.pylintrc-mandatory
|
||||
8
.prettierrc.yml
Normal file
8
.prettierrc.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
# Defaults for all prettier-supported languages.
|
||||
# Prettier will complete this with settings from .editorconfig file.
|
||||
bracketSpacing: false
|
||||
printWidth: 88
|
||||
proseWrap: always
|
||||
semi: true
|
||||
trailingComma: "es5"
|
||||
xmlWhitespaceSensitivity: "strict"
|
||||
123
.pylintrc
Normal file
123
.pylintrc
Normal file
@@ -0,0 +1,123 @@
|
||||
|
||||
|
||||
[MASTER]
|
||||
load-plugins=pylint_odoo
|
||||
score=n
|
||||
|
||||
[ODOOLINT]
|
||||
readme-template-url="https://github.com/OCA/maintainer-tools/blob/master/template/module/README.rst"
|
||||
manifest-required-authors=Elabore
|
||||
manifest-required-keys=license
|
||||
manifest-deprecated-keys=description,active
|
||||
license-allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3
|
||||
valid-odoo-versions=18.0
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=all
|
||||
|
||||
# This .pylintrc contains optional AND mandatory checks and is meant to be
|
||||
# loaded in an IDE to have it check everything, in the hope this will make
|
||||
# optional checks more visible to contributors who otherwise never look at a
|
||||
# green travis to see optional checks that failed.
|
||||
# .pylintrc-mandatory containing only mandatory checks is used the pre-commit
|
||||
# config as a blocking check.
|
||||
|
||||
enable=anomalous-backslash-in-string,
|
||||
api-one-deprecated,
|
||||
api-one-multi-together,
|
||||
assignment-from-none,
|
||||
attribute-deprecated,
|
||||
class-camelcase,
|
||||
dangerous-default-value,
|
||||
dangerous-view-replace-wo-priority,
|
||||
development-status-allowed,
|
||||
duplicate-id-csv,
|
||||
duplicate-key,
|
||||
duplicate-xml-fields,
|
||||
duplicate-xml-record-id,
|
||||
eval-referenced,
|
||||
eval-used,
|
||||
incoherent-interpreter-exec-perm,
|
||||
license-allowed,
|
||||
manifest-author-string,
|
||||
manifest-deprecated-key,
|
||||
manifest-required-author,
|
||||
manifest-required-key,
|
||||
manifest-version-format,
|
||||
method-compute,
|
||||
method-inverse,
|
||||
method-required-super,
|
||||
method-search,
|
||||
openerp-exception-warning,
|
||||
pointless-statement,
|
||||
pointless-string-statement,
|
||||
print-used,
|
||||
redundant-keyword-arg,
|
||||
redundant-modulename-xml,
|
||||
reimported,
|
||||
relative-import,
|
||||
return-in-init,
|
||||
rst-syntax-error,
|
||||
sql-injection,
|
||||
too-few-format-args,
|
||||
translation-field,
|
||||
translation-required,
|
||||
unreachable,
|
||||
use-vim-comment,
|
||||
wrong-tabs-instead-of-spaces,
|
||||
xml-syntax-error,
|
||||
attribute-string-redundant,
|
||||
character-not-valid-in-resource-link,
|
||||
consider-merging-classes-inherited,
|
||||
context-overridden,
|
||||
create-user-wo-reset-password,
|
||||
dangerous-filter-wo-user,
|
||||
dangerous-qweb-replace-wo-priority,
|
||||
deprecated-data-xml-node,
|
||||
deprecated-openerp-xml-node,
|
||||
duplicate-po-message-definition,
|
||||
except-pass,
|
||||
file-not-used,
|
||||
invalid-commit,
|
||||
manifest-maintainers-list,
|
||||
missing-newline-extrafiles,
|
||||
missing-readme,
|
||||
missing-return,
|
||||
odoo-addons-relative-import,
|
||||
old-api7-method-defined,
|
||||
po-msgstr-variables,
|
||||
po-syntax-error,
|
||||
renamed-field-parameter,
|
||||
resource-not-exist,
|
||||
str-format-used,
|
||||
test-folder-imported,
|
||||
translation-contains-variable,
|
||||
translation-positional-used,
|
||||
unnecessary-utf8-coding-comment,
|
||||
website-manifest-key-not-valid-uri,
|
||||
xml-attribute-translatable,
|
||||
xml-deprecated-qweb-directive,
|
||||
xml-deprecated-tree-attribute,
|
||||
external-request-timeout,
|
||||
# messages that do not cause the lint step to fail
|
||||
consider-merging-classes-inherited,
|
||||
create-user-wo-reset-password,
|
||||
dangerous-filter-wo-user,
|
||||
deprecated-module,
|
||||
file-not-used,
|
||||
invalid-commit,
|
||||
missing-manifest-dependency,
|
||||
missing-newline-extrafiles,
|
||||
missing-readme,
|
||||
no-utf8-coding-comment,
|
||||
odoo-addons-relative-import,
|
||||
old-api7-method-defined,
|
||||
redefined-builtin,
|
||||
too-complex,
|
||||
unnecessary-utf8-coding-comment
|
||||
|
||||
|
||||
[REPORTS]
|
||||
msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}
|
||||
output-format=colorized
|
||||
reports=no
|
||||
98
.pylintrc-mandatory
Normal file
98
.pylintrc-mandatory
Normal file
@@ -0,0 +1,98 @@
|
||||
|
||||
[MASTER]
|
||||
load-plugins=pylint_odoo
|
||||
score=n
|
||||
|
||||
[ODOOLINT]
|
||||
readme-template-url="https://github.com/OCA/maintainer-tools/blob/master/template/module/README.rst"
|
||||
manifest-required-authors=Elabore
|
||||
manifest-required-keys=license
|
||||
manifest-deprecated-keys=description,active
|
||||
license-allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3
|
||||
valid-odoo-versions=18.0
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=all
|
||||
|
||||
enable=anomalous-backslash-in-string,
|
||||
api-one-deprecated,
|
||||
api-one-multi-together,
|
||||
assignment-from-none,
|
||||
attribute-deprecated,
|
||||
class-camelcase,
|
||||
dangerous-default-value,
|
||||
dangerous-view-replace-wo-priority,
|
||||
development-status-allowed,
|
||||
duplicate-id-csv,
|
||||
duplicate-key,
|
||||
duplicate-xml-fields,
|
||||
duplicate-xml-record-id,
|
||||
eval-referenced,
|
||||
eval-used,
|
||||
incoherent-interpreter-exec-perm,
|
||||
license-allowed,
|
||||
manifest-author-string,
|
||||
manifest-deprecated-key,
|
||||
manifest-required-author,
|
||||
manifest-required-key,
|
||||
manifest-version-format,
|
||||
method-compute,
|
||||
method-inverse,
|
||||
method-required-super,
|
||||
method-search,
|
||||
openerp-exception-warning,
|
||||
pointless-statement,
|
||||
pointless-string-statement,
|
||||
print-used,
|
||||
redundant-keyword-arg,
|
||||
redundant-modulename-xml,
|
||||
reimported,
|
||||
relative-import,
|
||||
return-in-init,
|
||||
rst-syntax-error,
|
||||
sql-injection,
|
||||
too-few-format-args,
|
||||
translation-field,
|
||||
translation-required,
|
||||
unreachable,
|
||||
use-vim-comment,
|
||||
wrong-tabs-instead-of-spaces,
|
||||
xml-syntax-error,
|
||||
attribute-string-redundant,
|
||||
character-not-valid-in-resource-link,
|
||||
consider-merging-classes-inherited,
|
||||
context-overridden,
|
||||
create-user-wo-reset-password,
|
||||
dangerous-filter-wo-user,
|
||||
dangerous-qweb-replace-wo-priority,
|
||||
deprecated-data-xml-node,
|
||||
deprecated-openerp-xml-node,
|
||||
duplicate-po-message-definition,
|
||||
except-pass,
|
||||
file-not-used,
|
||||
invalid-commit,
|
||||
manifest-maintainers-list,
|
||||
missing-newline-extrafiles,
|
||||
missing-readme,
|
||||
missing-return,
|
||||
odoo-addons-relative-import,
|
||||
old-api7-method-defined,
|
||||
po-msgstr-variables,
|
||||
po-syntax-error,
|
||||
renamed-field-parameter,
|
||||
resource-not-exist,
|
||||
str-format-used,
|
||||
test-folder-imported,
|
||||
translation-contains-variable,
|
||||
translation-positional-used,
|
||||
unnecessary-utf8-coding-comment,
|
||||
website-manifest-key-not-valid-uri,
|
||||
xml-attribute-translatable,
|
||||
xml-deprecated-qweb-directive,
|
||||
xml-deprecated-tree-attribute,
|
||||
external-request-timeout
|
||||
|
||||
[REPORTS]
|
||||
msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg}
|
||||
output-format=colorized
|
||||
reports=no
|
||||
31
.ruff.toml
Normal file
31
.ruff.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
target-version = "py310"
|
||||
fix = true
|
||||
|
||||
[lint]
|
||||
extend-select = [
|
||||
"B",
|
||||
"C90",
|
||||
"E501", # line too long (default 88)
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
]
|
||||
extend-safe-fixes = ["UP008"]
|
||||
exclude = ["setup/*"]
|
||||
|
||||
[format]
|
||||
exclude = ["setup/*"]
|
||||
|
||||
[lint.per-file-ignores]
|
||||
"__init__.py" = ["F401", "I001"] # ignore unused and unsorted imports in __init__.py
|
||||
"__manifest__.py" = ["B018"] # useless expression
|
||||
|
||||
[lint.isort]
|
||||
section-order = ["future", "standard-library", "third-party", "odoo", "odoo-addons", "first-party", "local-folder"]
|
||||
|
||||
[lint.isort.sections]
|
||||
"odoo" = ["odoo"]
|
||||
"odoo-addons" = ["odoo.addons"]
|
||||
|
||||
[lint.mccabe]
|
||||
max-complexity = 16
|
||||
@@ -1,60 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * allow_negative_leave_and_allocation
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-01-02 12:11+0000\n"
|
||||
"PO-Revision-Date: 2025-01-02 12:11+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: allow_negative_leave_and_allocation
|
||||
#: model:ir.model.fields,field_description:allow_negative_leave_and_allocation.field_hr_leave_type__allows_negative
|
||||
msgid "Allow Negative Leaves"
|
||||
msgstr ""
|
||||
|
||||
#. module: allow_negative_leave_and_allocation
|
||||
#: model_terms:ir.ui.view,arch_db:allow_negative_leave_and_allocation.hr_leave_type_negative_leave
|
||||
msgid "Allow negative"
|
||||
msgstr ""
|
||||
|
||||
#. module: allow_negative_leave_and_allocation
|
||||
#: model:ir.model.fields,help:allow_negative_leave_and_allocation.field_hr_leave_type__allows_negative
|
||||
msgid ""
|
||||
"If checked, users request can exceed the allocated days and balance can go "
|
||||
"in negative."
|
||||
msgstr ""
|
||||
|
||||
#. module: allow_negative_leave_and_allocation
|
||||
#: model:ir.model.constraint,message:allow_negative_leave_and_allocation.constraint_hr_leave_allocation_duration_check
|
||||
msgid "The duration must be greater than 0."
|
||||
msgstr ""
|
||||
|
||||
#. module: allow_negative_leave_and_allocation
|
||||
#: model:ir.model.constraint,message:allow_negative_leave_and_allocation.constraint_hr_leave_allocation_type_value
|
||||
msgid ""
|
||||
"The employee, department, company or employee category of this request is "
|
||||
"missing. Please make sure that your user login is linked to an employee."
|
||||
msgstr ""
|
||||
|
||||
#. module: allow_negative_leave_and_allocation
|
||||
#: model:ir.model,name:allow_negative_leave_and_allocation.model_hr_leave
|
||||
msgid "Time Off"
|
||||
msgstr ""
|
||||
|
||||
#. module: allow_negative_leave_and_allocation
|
||||
#: model:ir.model,name:allow_negative_leave_and_allocation.model_hr_leave_allocation
|
||||
msgid "Time Off Allocation"
|
||||
msgstr ""
|
||||
|
||||
#. module: allow_negative_leave_and_allocation
|
||||
#: model:ir.model,name:allow_negative_leave_and_allocation.model_hr_leave_type
|
||||
msgid "Time Off Type"
|
||||
msgstr ""
|
||||
@@ -1,60 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * allow_negative_leave_and_allocation
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-01-02 12:11+0000\n"
|
||||
"PO-Revision-Date: 2025-01-02 12:11+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: allow_negative_leave_and_allocation
|
||||
#: model:ir.model.fields,field_description:allow_negative_leave_and_allocation.field_hr_leave_type__allows_negative
|
||||
msgid "Allow Negative Leaves"
|
||||
msgstr "Autoriser les congés négatifs"
|
||||
|
||||
#. module: allow_negative_leave_and_allocation
|
||||
#: model_terms:ir.ui.view,arch_db:allow_negative_leave_and_allocation.hr_leave_type_negative_leave
|
||||
msgid "Allow negative"
|
||||
msgstr "Autoriser les congés négatifs"
|
||||
|
||||
#. module: allow_negative_leave_and_allocation
|
||||
#: model:ir.model.fields,help:allow_negative_leave_and_allocation.field_hr_leave_type__allows_negative
|
||||
msgid ""
|
||||
"If checked, users request can exceed the allocated days and balance can go "
|
||||
"in negative."
|
||||
msgstr ""
|
||||
|
||||
#. module: allow_negative_leave_and_allocation
|
||||
#: model:ir.model.constraint,message:allow_negative_leave_and_allocation.constraint_hr_leave_allocation_duration_check
|
||||
msgid "The duration must be greater than 0."
|
||||
msgstr ""
|
||||
|
||||
#. module: allow_negative_leave_and_allocation
|
||||
#: model:ir.model.constraint,message:allow_negative_leave_and_allocation.constraint_hr_leave_allocation_type_value
|
||||
msgid ""
|
||||
"The employee, department, company or employee category of this request is "
|
||||
"missing. Please make sure that your user login is linked to an employee."
|
||||
msgstr ""
|
||||
|
||||
#. module: allow_negative_leave_and_allocation
|
||||
#: model:ir.model,name:allow_negative_leave_and_allocation.model_hr_leave
|
||||
msgid "Time Off"
|
||||
msgstr "Congés"
|
||||
|
||||
#. module: allow_negative_leave_and_allocation
|
||||
#: model:ir.model,name:allow_negative_leave_and_allocation.model_hr_leave_allocation
|
||||
msgid "Time Off Allocation"
|
||||
msgstr "Allocation de congés"
|
||||
|
||||
#. module: allow_negative_leave_and_allocation
|
||||
#: model:ir.model,name:allow_negative_leave_and_allocation.model_hr_leave_type
|
||||
msgid "Time Off Type"
|
||||
msgstr "Type de congés"
|
||||
@@ -1 +0,0 @@
|
||||
from . import hr_leave_type, hr_leave, hr_leave_allocation
|
||||
@@ -1,16 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
# Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com)
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
class HrLeave(models.Model):
|
||||
_inherit = "hr.leave"
|
||||
|
||||
@api.constrains("state", "number_of_days", "holiday_status_id")
|
||||
def _check_holidays(self):
|
||||
for holiday in self:
|
||||
if holiday.holiday_status_id.allows_negative:
|
||||
continue
|
||||
super()._check_holidays()
|
||||
@@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
# Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com)
|
||||
|
||||
from odoo import models
|
||||
|
||||
class HrLeaveAllocation(models.Model):
|
||||
_inherit = "hr.leave.allocation"
|
||||
|
||||
_sql_constraints = [
|
||||
('type_value',
|
||||
"CHECK( (holiday_type='employee' AND (employee_id IS NOT NULL OR multi_employee IS TRUE)) or "
|
||||
"(holiday_type='category' AND category_id IS NOT NULL) or "
|
||||
"(holiday_type='department' AND department_id IS NOT NULL) or "
|
||||
"(holiday_type='company' AND mode_company_id IS NOT NULL))",
|
||||
"The employee, department, company or employee category of this request is missing. Please make sure that your user login is linked to an employee."),
|
||||
('duration_check', "CHECK((allocation_type != 'regular'))", "The duration must be greater than 0."),
|
||||
]
|
||||
@@ -1,20 +0,0 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class HolidaysType(models.Model):
|
||||
_inherit = "hr.leave.type"
|
||||
|
||||
# negative time off
|
||||
allows_negative = fields.Boolean(string='Allow Negative Leaves',
|
||||
help="If checked, users request can exceed the allocated days and balance can go in negative.")
|
||||
|
||||
@api.depends('validity_start', 'validity_stop')
|
||||
def _compute_valid(self):
|
||||
res = super()._compute_valid()
|
||||
for holiday_type in res:
|
||||
if not holiday_type.valid:
|
||||
holiday_type.valid = holiday_type.allows_negative
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hr_leave_type_negative_leave" model="ir.ui.view">
|
||||
<field name="name">hr.leave.type.negative.leave</field>
|
||||
<field name="model">hr.leave.type</field>
|
||||
<field name="inherit_id" ref="hr_holidays.edit_holiday_status_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name='request_unit' position="after">
|
||||
<field name="allows_negative" />
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,26 +0,0 @@
|
||||
<odoo>
|
||||
<record id="hr_leave_view_negative_leave" model="ir.ui.view">
|
||||
<field name="name">hr.leave.view.negative.leave</field>
|
||||
<field name="model">hr.leave</field>
|
||||
<field name="inherit_id" ref="hr_holidays.hr_leave_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name='holiday_status_id' position="attributes">
|
||||
<attribute name="domain">
|
||||
[
|
||||
'|',
|
||||
('allows_negative', '=', True),
|
||||
'&',
|
||||
('allows_negative', '=', False),
|
||||
'&',
|
||||
('virtual_remaining_leaves', '>', 0),
|
||||
'|',
|
||||
('allocation_type', 'in', ['fixed_allocation', 'no']),
|
||||
'&',
|
||||
('allocation_type', '=', 'fixed'),
|
||||
('max_leaves', '>', '0')
|
||||
]
|
||||
</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -2,7 +2,18 @@
|
||||
hr_effective_attendance_period
|
||||
==============================
|
||||
|
||||
Provide indication that attendance are effectively worked periods or not
|
||||
Provide indication that attendance periods are effectively worked or not.
|
||||
|
||||
Description
|
||||
===========
|
||||
|
||||
This module adds a boolean field on working time attendance lines to mark
|
||||
which periods are effectively worked by employees. It provides utility methods
|
||||
on the resource calendar to determine:
|
||||
|
||||
- Whether a day is a working day
|
||||
- Whether a day is a full working day (both morning and afternoon)
|
||||
- Whether all attendances of a day are effectively worked (no leaves)
|
||||
|
||||
Installation
|
||||
============
|
||||
@@ -10,17 +21,30 @@ Installation
|
||||
Use Odoo normal module installation procedure to install
|
||||
``hr_effective_attendance_period``.
|
||||
|
||||
This module depends on:
|
||||
|
||||
* ``base`` (Odoo core)
|
||||
* ``resource`` (Odoo core)
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Go to ``Configuration > Technical > Resource > Working Times`` and create
|
||||
Working Times for your company. For each attendance line, check
|
||||
``Effective Attendance Period`` to indicate which periods are effectively
|
||||
attended by employees.
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
None yet.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `our issues website <https://github.com/elabore-coop/hr-tools/issues>`_. In case of
|
||||
trouble, please check there if your issue has already been
|
||||
reported. If you spotted it first, help us smashing it by providing a
|
||||
detailed and welcomed feedback.
|
||||
Bugs are tracked on `our issues website <https://github.com/elabore-coop/hr-tools/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us smash it by providing detailed and welcomed feedback.
|
||||
|
||||
Credits
|
||||
=======
|
||||
@@ -34,8 +58,8 @@ Funders
|
||||
-------
|
||||
|
||||
The development of this module has been financially supported by:
|
||||
* Elabore (https://elabore.coop)
|
||||
|
||||
* Elabore (https://elabore.coop)
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
|
||||
@@ -3,35 +3,22 @@
|
||||
|
||||
{
|
||||
"name": "hr_effective_attendance_period",
|
||||
"version": "14.0.2.0.1",
|
||||
"version": "18.0.1.0.0",
|
||||
"author": "Elabore",
|
||||
"website": "https://elabore.coop",
|
||||
"website": "https://git.elabore.coop/elabore/hr-tools",
|
||||
"maintainer": "Stéphan Sainléger",
|
||||
"license": "AGPL-3",
|
||||
"category": "Tools",
|
||||
"summary": "Provide indication that attendance are effectively worked periods or not",
|
||||
"summary": "Indicate whether attendance periods are effectively worked",
|
||||
# any module necessary for this one to work correctly
|
||||
"depends": [
|
||||
"base",
|
||||
"resource",
|
||||
],
|
||||
"qweb": [
|
||||
# "static/src/xml/*.xml",
|
||||
],
|
||||
"external_dependencies": {
|
||||
"python": [],
|
||||
},
|
||||
# always loaded
|
||||
"data": [
|
||||
"views/resource_views.xml",
|
||||
],
|
||||
# only loaded in demonstration mode
|
||||
"demo": [],
|
||||
"js": [],
|
||||
"css": [],
|
||||
"installable": True,
|
||||
# Install this module automatically if all dependency have been previously
|
||||
# and independently installed. Used for synergetic or glue modules.
|
||||
"auto_install": False,
|
||||
"application": False,
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import math
|
||||
from datetime import timedelta
|
||||
|
||||
import pytz
|
||||
from datetime import datetime, timedelta
|
||||
from pytz import timezone
|
||||
from odoo import models, fields
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResourceCalendarAttendance(models.Model):
|
||||
_inherit = "resource.calendar.attendance"
|
||||
|
||||
effective_attendance_period = fields.Boolean('Effective Attendance Period', store=True)
|
||||
effective_attendance_period = fields.Boolean(store=True)
|
||||
|
||||
|
||||
class ResourceCalendar(models.Model):
|
||||
@@ -18,7 +19,7 @@ class ResourceCalendar(models.Model):
|
||||
domain = [
|
||||
("calendar_id", "=", self.id),
|
||||
("dayofweek", "=", day.weekday()),
|
||||
("effective_attendance_period", "=", True)
|
||||
("effective_attendance_period", "=", True),
|
||||
]
|
||||
if self.two_weeks_calendar:
|
||||
# Employee has Even/Odd weekly calendar
|
||||
@@ -38,28 +39,40 @@ class ResourceCalendar(models.Model):
|
||||
|
||||
def is_full_working_day(self, day):
|
||||
day_attendances = self._retrieve_day_matching_attendances(day)
|
||||
morning_worked = len(day_attendances.filtered(lambda x: x.day_period == "morning")) > 0
|
||||
afternoon_worked = len(day_attendances.filtered(lambda x: x.day_period == "afternoon")) > 0
|
||||
morning_worked = (
|
||||
len(day_attendances.filtered(lambda x: x.day_period == "morning")) > 0
|
||||
)
|
||||
afternoon_worked = (
|
||||
len(day_attendances.filtered(lambda x: x.day_period == "afternoon")) > 0
|
||||
)
|
||||
return morning_worked and afternoon_worked
|
||||
|
||||
def _compute_datetime_in_utc_tz(self, date):
|
||||
user_tz = pytz.timezone(self.env.context.get('tz', 'utc') or 'utc')
|
||||
user_tz = pytz.timezone(self.env.context.get("tz", "utc") or "utc")
|
||||
return user_tz.localize(date).astimezone(pytz.utc)
|
||||
|
||||
def _is_worked_attendance(self, resource, day, attendance):
|
||||
attendance_start = fields.Datetime.to_datetime(day.date()) + timedelta(hours=attendance.hour_from)
|
||||
attendance_end = fields.Datetime.to_datetime(day.date()) + timedelta(hours=attendance.hour_to)
|
||||
attendance_start = fields.Datetime.to_datetime(day.date()) + timedelta(
|
||||
hours=attendance.hour_from
|
||||
)
|
||||
attendance_end = fields.Datetime.to_datetime(day.date()) + timedelta(
|
||||
hours=attendance.hour_to
|
||||
)
|
||||
utc_start = self._compute_datetime_in_utc_tz(attendance_start)
|
||||
utc_end = self._compute_datetime_in_utc_tz(attendance_end)
|
||||
resource_leaves = self.env["resource.calendar.leaves"].sudo().search(
|
||||
[
|
||||
("company_id", "=", resource.company_id.id),
|
||||
("date_from", "<=", utc_start),
|
||||
("date_to", ">=", utc_end),
|
||||
"|",
|
||||
("resource_id", "=", resource.id),
|
||||
("resource_id", "=", None),
|
||||
]
|
||||
resource_leaves = (
|
||||
self.env["resource.calendar.leaves"]
|
||||
.sudo()
|
||||
.search(
|
||||
[
|
||||
("company_id", "=", resource.company_id.id),
|
||||
("date_from", "<=", utc_start),
|
||||
("date_to", ">=", utc_end),
|
||||
"|",
|
||||
("resource_id", "=", resource.id),
|
||||
("resource_id", "=", None),
|
||||
]
|
||||
)
|
||||
)
|
||||
if resource_leaves:
|
||||
return False
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_resource_calendar_attendance_tree_inherit_effective_attendance"
|
||||
model="ir.ui.view">
|
||||
<field name="name">resource.calendar.attendance.view.tree.inherit</field>
|
||||
<record
|
||||
id="view_resource_calendar_attendance_tree_inherit_effective_attendance"
|
||||
model="ir.ui.view"
|
||||
>
|
||||
<field name="name">resource.calendar.attendance.view.list.inherit</field>
|
||||
<field name="model">resource.calendar.attendance</field>
|
||||
<field name="inherit_id" ref="resource.view_resource_calendar_attendance_tree" />
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="resource.view_resource_calendar_attendance_tree"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='week_type']" position="after">
|
||||
<field name="effective_attendance_period" />
|
||||
@@ -12,11 +17,16 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_resource_calendar_attendance_form_inherit_effective_attendance"
|
||||
model="ir.ui.view">
|
||||
<field name="name">resource.calendar.attendance.view.tree.inherit</field>
|
||||
<record
|
||||
id="view_resource_calendar_attendance_form_inherit_effective_attendance"
|
||||
model="ir.ui.view"
|
||||
>
|
||||
<field name="name">resource.calendar.attendance.view.list.inherit</field>
|
||||
<field name="model">resource.calendar.attendance</field>
|
||||
<field name="inherit_id" ref="resource.view_resource_calendar_attendance_form" />
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="resource.view_resource_calendar_attendance_form"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='day_period']" position="after">
|
||||
<field name="effective_attendance_period" />
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
==============================
|
||||
hr_expense_analytic_account_preselect_with_project
|
||||
==============================
|
||||
==================================================
|
||||
HR Expense Analytic Account Preselect with Project
|
||||
==================================================
|
||||
|
||||
In expenses, preselect analytic account with project
|
||||
Preselect the analytic distribution on expenses based on the selected project.
|
||||
|
||||
Description
|
||||
===========
|
||||
|
||||
This module adds a ``Project`` field to expense forms. When a project is
|
||||
selected, the analytic distribution is automatically set to the project's
|
||||
analytic account (100%), so the user does not need to manually set the
|
||||
analytic distribution.
|
||||
|
||||
Installation
|
||||
============
|
||||
@@ -10,17 +18,32 @@ Installation
|
||||
Use Odoo normal module installation procedure to install
|
||||
``hr_expense_analytic_account_preselect_with_project``.
|
||||
|
||||
This module depends on:
|
||||
|
||||
* ``base`` (Odoo core)
|
||||
* ``hr_expense`` (Odoo core)
|
||||
* ``project`` (Odoo core)
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
When creating an expense:
|
||||
|
||||
1. Select a ``Project`` in the expense form.
|
||||
2. The analytic distribution is automatically populated with the project's
|
||||
analytic account at 100%.
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
None yet.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `our issues website <https://github.com/elabore-coop/hr-tools/issues>`_. In case of
|
||||
trouble, please check there if your issue has already been
|
||||
reported. If you spotted it first, help us smashing it by providing a
|
||||
detailed and welcomed feedback.
|
||||
Bugs are tracked on `our issues website <https://github.com/elabore-coop/hr-tools/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us smash it by providing detailed and welcomed feedback.
|
||||
|
||||
Credits
|
||||
=======
|
||||
@@ -34,8 +57,8 @@ Funders
|
||||
-------
|
||||
|
||||
The development of this module has been financially supported by:
|
||||
* Elabore (https://elabore.coop)
|
||||
|
||||
* Elabore (https://elabore.coop)
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
|
||||
@@ -3,35 +3,23 @@
|
||||
|
||||
{
|
||||
"name": "hr_expense_analytic_account_preselect_with_project",
|
||||
"version": "14.0.0.0.0",
|
||||
"version": "18.0.1.0.0",
|
||||
"author": "Elabore",
|
||||
"website": "https://elabore.coop",
|
||||
"website": "https://git.elabore.coop/elabore/hr-tools",
|
||||
"maintainer": "Clément Thomas",
|
||||
"license": "AGPL-3",
|
||||
"category": "Tools",
|
||||
"summary": "Provide indication that attendance are effectively worked periods or not",
|
||||
"summary": "Preselect analytic distribution on expenses from project",
|
||||
# any module necessary for this one to work correctly
|
||||
"depends": [
|
||||
"base",
|
||||
"hr_expense",
|
||||
"project",
|
||||
],
|
||||
"qweb": [
|
||||
# "static/src/xml/*.xml",
|
||||
],
|
||||
"external_dependencies": {
|
||||
"python": [],
|
||||
},
|
||||
# always loaded
|
||||
"data": [
|
||||
"views/hr_expense_views.xml",
|
||||
],
|
||||
# only loaded in demonstration mode
|
||||
"demo": [],
|
||||
"js": [],
|
||||
"css": [],
|
||||
"installable": True,
|
||||
# Install this module automatically if all dependency have been previously
|
||||
# and independently installed. Used for synergetic or glue modules.
|
||||
"auto_install": False,
|
||||
"application": False,
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import api, fields, models
|
||||
|
||||
from odoo import fields, models, api
|
||||
|
||||
class HrExpense(models.Model):
|
||||
_inherit = "hr.expense"
|
||||
|
||||
project_id = fields.Many2one('project.project', string="Projet", required=True)
|
||||
project_id = fields.Many2one("project.project", string="Projet", required=True)
|
||||
|
||||
@api.onchange('project_id')
|
||||
@api.onchange("project_id")
|
||||
def set_analytic_account(self):
|
||||
self.analytic_account_id = self.project_id.analytic_account_id
|
||||
|
||||
if self.project_id.account_id:
|
||||
self.analytic_distribution = {str(self.project_id.account_id.id): 100}
|
||||
else:
|
||||
self.analytic_distribution = {}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="hr_expense_view_form_analytic_account_preselect_with_project" model="ir.ui.view">
|
||||
<field name="name">hr.expense.view.form.analytic.account.preselect.with.project</field>
|
||||
<record
|
||||
id="hr_expense_view_form_analytic_account_preselect_with_project"
|
||||
model="ir.ui.view"
|
||||
>
|
||||
<field
|
||||
name="name"
|
||||
>hr.expense.view.form.analytic.account.preselect.with.project</field>
|
||||
<field name="model">hr.expense</field>
|
||||
<field name="inherit_id" ref="hr_expense.hr_expense_view_form"/>
|
||||
<field name="inherit_id" ref="hr_expense.hr_expense_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='analytic_account_id']" position="before">
|
||||
<xpath expr="//field[@name='analytic_distribution']" position="before">
|
||||
<field name="project_id" />
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
==============================
|
||||
hr_expense_report_merge_attachment
|
||||
==============================
|
||||
|
||||
Merge attachments in expense report.
|
||||
|
||||
Managed attachment types :
|
||||
* PDF
|
||||
* Images
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Use Odoo normal module installation procedure to install
|
||||
``hr_expense_report_merge_attachment``.
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
None yet.
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `our issues website <https://github.com/elabore-coop/hr-tools/issues>`_. In case of
|
||||
trouble, please check there if your issue has already been
|
||||
reported. If you spotted it first, help us smashing it by providing a
|
||||
detailed and welcomed feedback.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Clément Thomas
|
||||
|
||||
Funders
|
||||
-------
|
||||
|
||||
The development of this module has been financially supported by:
|
||||
* Elabore (https://elabore.coop)
|
||||
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
This module is maintained by Elabore.
|
||||
@@ -1,3 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import controllers
|
||||
@@ -1,34 +0,0 @@
|
||||
# Copyright 2023 Stéphan Sainléger (Elabore)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "hr_expense_report_merge_attachment",
|
||||
"version": "14.0.0.0.0",
|
||||
"author": "Elabore",
|
||||
"website": "https://elabore.coop",
|
||||
"maintainer": "Clément Thomas",
|
||||
"license": "AGPL-3",
|
||||
"category": "Tools",
|
||||
"summary": "Merge attachments in expense report",
|
||||
# any module necessary for this one to work correctly
|
||||
"depends": [
|
||||
"hr_expense",
|
||||
],
|
||||
"qweb": [
|
||||
],
|
||||
"external_dependencies": {
|
||||
"python": [],
|
||||
},
|
||||
# always loaded
|
||||
"data": [
|
||||
],
|
||||
# only loaded in demonstration mode
|
||||
"demo": [],
|
||||
"js": [],
|
||||
"css": [],
|
||||
"installable": True,
|
||||
# Install this module automatically if all dependency have been previously
|
||||
# and independently installed. Used for synergetic or glue modules.
|
||||
"auto_install": False,
|
||||
"application": False,
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import main
|
||||
@@ -1,85 +0,0 @@
|
||||
import odoo.addons.web.controllers.main as main
|
||||
import ast
|
||||
import base64
|
||||
import io
|
||||
from odoo.tools.pdf import OdooPdfFileReader, OdooPdfFileWriter
|
||||
import PyPDF2
|
||||
import logging
|
||||
from odoo import http
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.lib.pagesizes import letter, A4
|
||||
try:
|
||||
from pillow_heif import register_heif_opener
|
||||
register_heif_opener()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Extension(main.ReportController):
|
||||
|
||||
@http.route(['/report/download'], type='http', auth="user")
|
||||
def report_download(self, data, token, context=None):
|
||||
"""
|
||||
In case of hr expense sheet report : merge PDF with other attachments
|
||||
"""
|
||||
|
||||
res = super(Extension,self).report_download(data, token, context)
|
||||
|
||||
if "hr_expense.report_expense_sheet" in ast.literal_eval(data)[0]: #check if we are generating expense sheet report pdf
|
||||
writer = OdooPdfFileWriter() #Open a file writer to create new PDF
|
||||
|
||||
# Open main pdf and read it and write it page by page in new pdf
|
||||
main_pdf_reader = OdooPdfFileReader(io.BytesIO(res.data), strict=False)
|
||||
for n in range(main_pdf_reader.getNumPages()):
|
||||
writer.addPage(main_pdf_reader.getPage(n))
|
||||
|
||||
# Parse data (last char after "/") to get id of current sheet
|
||||
expense_sheet_id = int(ast.literal_eval(data)[0].split('/')[-1])
|
||||
|
||||
# Get ids of related expenses
|
||||
expense_ids = [e.id for e in http.request.env['hr.expense.sheet'].browse(expense_sheet_id).expense_line_ids]
|
||||
|
||||
# Get related attachments
|
||||
attachments = http.request.env['ir.attachment'].search([('res_id','in',expense_ids),('res_model','=','hr.expense')])
|
||||
|
||||
# Open each attachments, and write it page by page in new pdf
|
||||
for att in attachments:
|
||||
if att.mimetype == "application/pdf":
|
||||
try:
|
||||
attachment_reader = OdooPdfFileReader(io.BytesIO(base64.b64decode(att.datas)), strict=False)
|
||||
for n in range(attachment_reader.getNumPages()):
|
||||
writer.addPage(attachment_reader.getPage(n))
|
||||
except PyPDF2.utils.PdfReadError: # Case of non-pdf attachments
|
||||
logger.info('Attachment %s cannot be merged in expense report'%(att.name,))
|
||||
elif 'image/' in att.mimetype:
|
||||
try:
|
||||
from PIL import Image as PilImage
|
||||
raw = base64.b64decode(att.datas)
|
||||
pil_img = PilImage.open(io.BytesIO(raw))
|
||||
if pil_img.mode not in ('RGB', 'RGBA'):
|
||||
pil_img = pil_img.convert('RGB')
|
||||
jpeg_buffer = io.BytesIO()
|
||||
pil_img.save(jpeg_buffer, format='JPEG')
|
||||
jpeg_buffer.seek(0)
|
||||
packet = io.BytesIO()
|
||||
can = canvas.Canvas(packet)
|
||||
img = ImageReader(jpeg_buffer)
|
||||
can.drawImage(img, 0, 0, A4[0]*0.9, A4[1]*0.9, preserveAspectRatio=True)
|
||||
can.save()
|
||||
packet.seek(0)
|
||||
attachment_reader = OdooPdfFileReader(packet)
|
||||
writer.addPage(attachment_reader.getPage(0))
|
||||
except Exception:
|
||||
logger.info('Attachment %s cannot be merged in expense report', att.name)
|
||||
|
||||
# Write new pdf to res.data
|
||||
buffer = io.BytesIO()
|
||||
writer.write(buffer)
|
||||
pdf_content = buffer.getvalue()
|
||||
|
||||
res.data = pdf_content
|
||||
|
||||
return res
|
||||
@@ -1,3 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
@@ -1,36 +0,0 @@
|
||||
# Copyright 2023 Stéphan Sainléger (Elabore)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "hr_expense_specific_journal",
|
||||
"version": "14.0.1.0.0",
|
||||
"author": "Elabore",
|
||||
"website": "https://elabore.coop",
|
||||
"maintainer": "Clément Thomas",
|
||||
"license": "AGPL-3",
|
||||
"category": "Tools",
|
||||
"summary": "Allow user to only select expense journal in expenses",
|
||||
# any module necessary for this one to work correctly
|
||||
"depends": [
|
||||
"hr_expense","account"
|
||||
],
|
||||
"qweb": [
|
||||
],
|
||||
"external_dependencies": {
|
||||
"python": [],
|
||||
},
|
||||
# always loaded
|
||||
"data": [
|
||||
'views/account_journal_views.xml',
|
||||
'views/hr_expense_sheet_views.xml'
|
||||
],
|
||||
# only loaded in demonstration mode
|
||||
"demo": [],
|
||||
"js": [],
|
||||
"css": [],
|
||||
"installable": True,
|
||||
# Install this module automatically if all dependency have been previously
|
||||
# and independently installed. Used for synergetic or glue modules.
|
||||
"auto_install": False,
|
||||
"application": False,
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * hr_expense_specific_journal
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-10-11 07:29+0000\n"
|
||||
"PO-Revision-Date: 2023-10-11 07:29+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: hr_expense_specific_journal
|
||||
#: model:ir.model.fields,field_description:hr_expense_specific_journal.field_account_journal__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Afficher Nom"
|
||||
|
||||
#. module: hr_expense_specific_journal
|
||||
#: model_terms:ir.ui.view,arch_db:hr_expense_specific_journal.view_account_journal_form_hr_expense_specific_journal_inherit
|
||||
msgid "Expense journal"
|
||||
msgstr "Journal des notes de frais"
|
||||
|
||||
#. module: hr_expense_specific_journal
|
||||
#: model:ir.model.fields,field_description:hr_expense_specific_journal.field_account_journal__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_expense_specific_journal
|
||||
#: model:ir.model.fields,field_description:hr_expense_specific_journal.field_account_journal__is_expense_journal
|
||||
msgid "Is expense journal"
|
||||
msgstr "Est un journal de notes de frais"
|
||||
|
||||
#. module: hr_expense_specific_journal
|
||||
#: model:ir.model,name:hr_expense_specific_journal.model_account_journal
|
||||
msgid "Journal"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_expense_specific_journal
|
||||
#: model:ir.model.fields,field_description:hr_expense_specific_journal.field_account_journal____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "Dernière modification le"
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import account_journal
|
||||
from . import hr_expense
|
||||
@@ -1,6 +0,0 @@
|
||||
from odoo import models, fields
|
||||
|
||||
class AccountJournal(models.Model):
|
||||
_inherit = 'account.journal'
|
||||
|
||||
is_expense_journal = fields.Boolean('Is expense journal')
|
||||
@@ -1,13 +0,0 @@
|
||||
from odoo import models, fields, api
|
||||
|
||||
class HrExpenseSheet(models.Model):
|
||||
_inherit = 'hr.expense.sheet'
|
||||
|
||||
@api.model
|
||||
def _default_journal_id(self):
|
||||
""" The journal is determining the company of the accounting entries generated from expense. We need to force journal company and expense sheet company to be the same. """
|
||||
default_company_id = self.default_get(['company_id'])['company_id']
|
||||
journal = self.env['account.journal'].search([('company_id', '=', default_company_id), ('is_expense_journal','=',True)], limit=1)
|
||||
return journal.id
|
||||
|
||||
journal_id = fields.Many2one(default=_default_journal_id)
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_account_journal_form_hr_expense_specific_journal_inherit" model="ir.ui.view">
|
||||
<field name="name">view.account.journal.form.hr.expense.specific.journal.inherit</field>
|
||||
<field name="model">account.journal</field>
|
||||
<field name="inherit_id" ref="account.view_account_journal_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='group_alias_edit']" position="after">
|
||||
<group string="Expense journal">
|
||||
<field name="is_expense_journal" />
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="hr_expense_sheet_form_view_hr_expense_specific_journal_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.expense.sheet.form.hr.expense.specific.journal.inherit</field>
|
||||
<field name="model">hr.expense.sheet</field>
|
||||
<field name="inherit_id" ref="hr_expense.view_hr_expense_sheet_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="journal_id" position="attributes">
|
||||
<attribute name="domain">[('is_expense_journal','=',True)]</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
95
hr_holidays_timeoff_analysis/README.md
Normal file
95
hr_holidays_timeoff_analysis/README.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# hr_holidays_timeoff_analysis
|
||||
|
||||
Breaks down validated time-off requests into individual day records and provides a pivot table for analyzing leave consumption by employee, leave type, and period.
|
||||
|
||||
## Features
|
||||
|
||||
### Time-off day model (`hr.leave.timeoff.day`)
|
||||
|
||||
One record per actual day of absence, with the following fields:
|
||||
|
||||
* **Date**: the specific day.
|
||||
* **Employee**: the employee on leave.
|
||||
* **Leave request**: link to the original `hr.leave` record.
|
||||
* **Leave type**: related field from the leave request (e.g. Paid leave, Sick leave).
|
||||
* **Duration by day**: the fraction of the day taken off (1.0 for a full day, 0.5 for a half day, or a proportional value for hourly leaves based on `number_of_days_display`).
|
||||
|
||||
### Calendar resolution
|
||||
|
||||
The module determines the employee's working calendar using the following priority:
|
||||
|
||||
1. **Employee calendar planning** (`hr.employee.calendar`): if the `hr_employee_calendar_planning` module is installed, the calendar matching the date range is used. This allows correct handling of employees who switch between full-time and part-time.
|
||||
2. **Employee calendar** (`resource_calendar_id`): the calendar set directly on the employee.
|
||||
3. **Company calendar**: fallback to the company's default calendar.
|
||||
|
||||
Only days where the employee is scheduled to work are recorded. Weekends and non-attendance days are skipped.
|
||||
|
||||
### Public holiday handling
|
||||
|
||||
Public holidays are excluded from time-off day records. The module converts public holiday dates to the employee's timezone before comparison, ensuring correct results for holidays that span midnight UTC.
|
||||
|
||||
### Scheduled action (cron)
|
||||
|
||||
A daily cron job (`cron_manage_timeoff_days`) performs two operations:
|
||||
|
||||
1. **Create**: Iterates over all validated leaves and creates a `hr.leave.timeoff.day` record for each working day within the leave period (skipping weekends, non-attendance days, and public holidays).
|
||||
2. **Delete**: Removes records linked to leaves that are no longer validated (draft, refused) or have been cancelled (`active=False`). Also cleans up orphan records not linked to any leave.
|
||||
|
||||
### Views
|
||||
|
||||
* **List view**: Editable list showing date, employee, leave request, leave type, and duration.
|
||||
* **Pivot view**: Leave days broken down by month (columns) and employee + leave type (rows) — the primary analysis tool.
|
||||
* **Search view**: Date filter defaulting to the current year.
|
||||
|
||||
The menu entry is located under **Time Off > Reporting > Timeoff Days**, accessible to Time Off managers.
|
||||
|
||||
### Access rights
|
||||
|
||||
Read-only access is granted to the Time Off manager group (`hr_holidays.group_hr_holidays_manager`).
|
||||
|
||||
### Tests
|
||||
|
||||
The module includes tests covering:
|
||||
|
||||
* Multi-day leaves spanning two months.
|
||||
* Half-day leaves (duration = 0.5).
|
||||
* Hourly leaves compared against the employee's calendar.
|
||||
* Part-time employee calendars (4-day week).
|
||||
* Public holidays falling within a leave period.
|
||||
* Unvalidated and deleted leaves (cleanup verification).
|
||||
* Cancelled leaves (`active=False`).
|
||||
* Employee calendar planning integration (switching calendars mid-period).
|
||||
|
||||
## Installation
|
||||
|
||||
This module depends on:
|
||||
|
||||
* `base` (Odoo core)
|
||||
* `hr_holidays` (Odoo core)
|
||||
|
||||
Optional: install `hr_employee_calendar_planning` to enable per-period calendar resolution.
|
||||
|
||||
Use the standard Odoo module installation procedure to install `hr_holidays_timeoff_analysis`.
|
||||
|
||||
## Known issues / Roadmap
|
||||
|
||||
None yet.
|
||||
|
||||
## Bug Tracker
|
||||
|
||||
Bugs are tracked on [our issues website](https://git.elabore.coop/Elabore/elabore-addons/issues). In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed feedback.
|
||||
|
||||
## Credits
|
||||
|
||||
### Contributors
|
||||
|
||||
* [Elabore](mailto:contact@elabore.coop)
|
||||
|
||||
### Funders
|
||||
|
||||
The development of this module has been financially supported by:
|
||||
* [Elabore](https://elabore.coop)
|
||||
|
||||
### Maintainer
|
||||
|
||||
This module is maintained by Elabore.
|
||||
@@ -1,14 +1,14 @@
|
||||
===================================
|
||||
allow_negative_leave_and_allocation
|
||||
===================================
|
||||
============================
|
||||
hr_holidays_timeoff_analysis
|
||||
============================
|
||||
|
||||
allow negative leaves and allocations
|
||||
indicate day by day if a date is timeoff or not and generate an analyses pivot by employee
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Use Odoo normal module installation procedure to install
|
||||
``allow_negative_leave_and_allocation``.
|
||||
``hr_holidays_timeoff_analysis``.
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
@@ -18,7 +18,7 @@ None yet.
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `our issues website <https://github.com/elabore-coop/allow_negative_leave_and_allocation/issues>`_. In case of
|
||||
Bugs are tracked on `our issues website <https://github.com/elabore-coop/hr_holidays_timeoff_analysis/issues>`_. In case of
|
||||
trouble, please check there if your issue has already been
|
||||
reported. If you spotted it first, help us smashing it by providing a
|
||||
detailed and welcomed feedback.
|
||||
@@ -29,7 +29,7 @@ Credits
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* `Elabore <mailto:laetitia.dacosta@elabore.coop>`
|
||||
* `Elabore <mailto:contact@elabore.coop>`
|
||||
|
||||
Funders
|
||||
-------
|
||||
30
hr_holidays_timeoff_analysis/__manifest__.py
Normal file
30
hr_holidays_timeoff_analysis/__manifest__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Copyright 2025 Elabore ()
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "hr_holidays_timeoff_analysis",
|
||||
"version": "18.0.1.0.1",
|
||||
"author": "Elabore",
|
||||
"website": "https://git.elabore.coop/elabore/elabore-addons",
|
||||
"maintainer": "Elabore",
|
||||
"license": "AGPL-3",
|
||||
"category": "HR",
|
||||
"summary": (
|
||||
"indicate day by day if a date is timeoff"
|
||||
" or not and generate an analyses pivot by employee"
|
||||
),
|
||||
# any module necessary for this one to work correctly
|
||||
"depends": [
|
||||
"base",
|
||||
"hr_holidays",
|
||||
],
|
||||
# always loaded
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"views/hr_leave_timeoff_day_views.xml",
|
||||
"data/hr_leave_timeoff_day_cron.xml",
|
||||
],
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
"application": False,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<odoo noupdate="1">
|
||||
<record id="ir_cron_create_timeoff_day" model="ir.cron">
|
||||
<field name="name">Create and update Timeoff Days</field>
|
||||
<field name="model_id" ref="model_hr_leave_timeoff_day" />
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.cron_manage_timeoff_days()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="active">True</field>
|
||||
</record>
|
||||
</odoo>
|
||||
104
hr_holidays_timeoff_analysis/i18n/fr.po
Normal file
104
hr_holidays_timeoff_analysis/i18n/fr.po
Normal file
@@ -0,0 +1,104 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * hr_holidays_timeoff_analysis
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-08-21 08:09+0000\n"
|
||||
"PO-Revision-Date: 2025-08-21 08:09+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.actions.server,name:hr_holidays_timeoff_analysis.ir_cron_create_timeoff_day_ir_actions_server
|
||||
#: model:ir.cron,cron_name:hr_holidays_timeoff_analysis.ir_cron_create_timeoff_day
|
||||
msgid "Create and update Timeoff Days"
|
||||
msgstr "Générer et mettre à jour la liste des jours en congés pour analyses"
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__date
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__employee_id
|
||||
msgid "Employee"
|
||||
msgstr "Employé"
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__hr_leave_id
|
||||
msgid "Hr Leave"
|
||||
msgstr "Congés"
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__leave_duration_by_day
|
||||
msgid "Leave Duration By Day"
|
||||
msgstr "Durée d'absence par jour"
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model_terms:ir.ui.view,arch_db:hr_holidays_timeoff_analysis.hr_leave_timeoff_day_view_search
|
||||
msgid "Period"
|
||||
msgstr "Période"
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__hr_leave_type
|
||||
msgid "Time Off Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model,name:hr_holidays_timeoff_analysis.model_hr_leave_timeoff_day
|
||||
msgid "Timeoff Day"
|
||||
msgstr "Jour d'absence"
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.actions.act_window,name:hr_holidays_timeoff_analysis.hr_leave_timeoff_day_action
|
||||
#: model:ir.ui.menu,name:hr_holidays_timeoff_analysis.menu_hr_holidays_timeoff_day_menu
|
||||
#: model_terms:ir.ui.view,arch_db:hr_holidays_timeoff_analysis.hr_leave_timeoff_day_view_search
|
||||
msgid "Timeoff Days"
|
||||
msgstr "Jours d'absence"
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model_terms:ir.ui.view,arch_db:hr_holidays_timeoff_analysis.hr_leave_timeoff_day_view_pivot
|
||||
msgid "Timeoff Days Analysis"
|
||||
msgstr "Analyse des jours d'absence"
|
||||
@@ -0,0 +1,104 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * hr_holidays_timeoff_analysis
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-08-21 08:09+0000\n"
|
||||
"PO-Revision-Date: 2025-08-21 08:09+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.actions.server,name:hr_holidays_timeoff_analysis.ir_cron_create_timeoff_day_ir_actions_server
|
||||
#: model:ir.cron,cron_name:hr_holidays_timeoff_analysis.ir_cron_create_timeoff_day
|
||||
msgid "Create and update Timeoff Days"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__date
|
||||
msgid "Date"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__employee_id
|
||||
msgid "Employee"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__hr_leave_id
|
||||
msgid "Hr Leave"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__leave_duration_by_day
|
||||
msgid "Leave Duration By Day"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model_terms:ir.ui.view,arch_db:hr_holidays_timeoff_analysis.hr_leave_timeoff_day_view_search
|
||||
msgid "Period"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model.fields,field_description:hr_holidays_timeoff_analysis.field_hr_leave_timeoff_day__hr_leave_type
|
||||
msgid "Time Off Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.model,name:hr_holidays_timeoff_analysis.model_hr_leave_timeoff_day
|
||||
msgid "Timeoff Day"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model:ir.actions.act_window,name:hr_holidays_timeoff_analysis.hr_leave_timeoff_day_action
|
||||
#: model:ir.ui.menu,name:hr_holidays_timeoff_analysis.menu_hr_holidays_timeoff_day_menu
|
||||
#: model_terms:ir.ui.view,arch_db:hr_holidays_timeoff_analysis.hr_leave_timeoff_day_view_search
|
||||
msgid "Timeoff Days"
|
||||
msgstr ""
|
||||
|
||||
#. module: hr_holidays_timeoff_analysis
|
||||
#: model_terms:ir.ui.view,arch_db:hr_holidays_timeoff_analysis.hr_leave_timeoff_day_view_pivot
|
||||
msgid "Timeoff Days Analysis"
|
||||
msgstr ""
|
||||
1
hr_holidays_timeoff_analysis/models/__init__.py
Normal file
1
hr_holidays_timeoff_analysis/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import hr_leave_timeoff_day
|
||||
196
hr_holidays_timeoff_analysis/models/hr_leave_timeoff_day.py
Normal file
196
hr_holidays_timeoff_analysis/models/hr_leave_timeoff_day.py
Normal file
@@ -0,0 +1,196 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from datetime import timedelta
|
||||
|
||||
import pytz
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class TimeOffDay(models.Model):
|
||||
_name = "hr.leave.timeoff.day"
|
||||
_description = "Timeoff Day"
|
||||
_order = "date desc"
|
||||
|
||||
date = fields.Date()
|
||||
employee_id = fields.Many2one("hr.employee")
|
||||
hr_leave_id = fields.Many2one("hr.leave")
|
||||
hr_leave_type = fields.Many2one(related="hr_leave_id.holiday_status_id")
|
||||
leave_duration_by_day = fields.Float()
|
||||
|
||||
def employee_is_scheduled_to_work_this_day(self, date, employee, leave):
|
||||
"""
|
||||
Check if the employee is scheduled to work on this day according to his
|
||||
calendar.
|
||||
"""
|
||||
calendar = self.get_calendar(employee, date)
|
||||
if not calendar or not calendar.attendance_ids:
|
||||
return False
|
||||
day_of_week = str(date.weekday())
|
||||
attendances = calendar.attendance_ids.filtered(
|
||||
lambda att: att.dayofweek == day_of_week
|
||||
and not att.display_type
|
||||
)
|
||||
if calendar.two_weeks_calendar:
|
||||
# Only keep attendances of the week matching the target date
|
||||
week_type = str(
|
||||
self.env["resource.calendar.attendance"].get_week_type(date)
|
||||
)
|
||||
attendances = attendances.filtered(
|
||||
lambda att: att.week_type == week_type
|
||||
)
|
||||
return bool(attendances)
|
||||
|
||||
def get_calendar(self, employee, date, leave=None):
|
||||
"""
|
||||
Get the working time calendar of the employee.
|
||||
"""
|
||||
calendar = False
|
||||
# check if hr_employee_calendar model exists
|
||||
# (module hr_employee_calendar_planning is installed)
|
||||
if "hr.employee.calendar" in self.env:
|
||||
hr_employee_calendar = self.env["hr.employee.calendar"].search(
|
||||
[
|
||||
("employee_id", "=", employee.id),
|
||||
("date_start", "<=", date),
|
||||
"|",
|
||||
("date_end", ">=", date),
|
||||
("date_end", "=", False),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
if hr_employee_calendar and hr_employee_calendar.calendar_id:
|
||||
calendar = hr_employee_calendar.calendar_id
|
||||
# If no hr_employee_calendar found or calendar found
|
||||
# in hr_employee_calendar, take the one set on the employee
|
||||
if not calendar and employee.resource_calendar_id:
|
||||
calendar = employee.resource_calendar_id
|
||||
# If no specific calendar is set on the employee, take the one of the company
|
||||
if not calendar and employee.company_id.resource_calendar_id:
|
||||
calendar = employee.company_id.resource_calendar_id
|
||||
return calendar
|
||||
|
||||
def _convert_to_employee_tz(self, date, employee):
|
||||
"""
|
||||
Convert a UTC datetime to the employee's timezone datetime.
|
||||
"""
|
||||
employee_tz = pytz.timezone(employee.tz or "UTC")
|
||||
if date.tzinfo is None:
|
||||
dt = pytz.utc.localize(date)
|
||||
return dt.astimezone(employee_tz)
|
||||
|
||||
def _is_public_holiday_according_to_employe_tz(self, date, employee):
|
||||
if not date or not employee:
|
||||
return False
|
||||
# get public holidays for the employee
|
||||
public_holidays = employee._get_public_holidays(date, date)
|
||||
if not public_holidays:
|
||||
return False
|
||||
if len(public_holidays) > 1:
|
||||
raise UserError(
|
||||
_(
|
||||
"Several holidays have been found on the"
|
||||
" date '%s'. Please correct the anomaly"
|
||||
" before continuing."
|
||||
)
|
||||
% date
|
||||
)
|
||||
ph = public_holidays[0]
|
||||
# Convert public holiday to the employee timezone
|
||||
ph_datetime_from_tz = self._convert_to_employee_tz(ph.date_from, employee)
|
||||
ph_datetime_to_tz = self._convert_to_employee_tz(ph.date_to, employee)
|
||||
# Convert datetime to date
|
||||
ph_date_from = ph_datetime_from_tz.date()
|
||||
ph_date_to = ph_datetime_to_tz.date()
|
||||
# Check if the stat date falls within the public
|
||||
# holiday range after conversion in employee tz
|
||||
if ph_date_from <= date <= ph_date_to:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def compute_leave_duration_by_day(self, leave):
|
||||
"""
|
||||
Compute the leave duration by day based on the leave type.
|
||||
"""
|
||||
leave_duration_by_day = 0.0
|
||||
# Full day case
|
||||
if leave.request_unit_half:
|
||||
leave_duration_by_day = 0.5
|
||||
elif leave.request_unit_hours:
|
||||
leave_duration_by_day = leave.number_of_days
|
||||
else:
|
||||
leave_duration_by_day = 1.0
|
||||
return leave_duration_by_day
|
||||
|
||||
@api.model
|
||||
def cron_manage_timeoff_days(self):
|
||||
self.cron_create_timeoff_days()
|
||||
self.cron_delete_timeoff_days()
|
||||
|
||||
def cron_create_timeoff_days(self):
|
||||
# Browse all validated leaves
|
||||
leaves = self.env["hr.leave"].search(
|
||||
[
|
||||
("state", "=", "validate"),
|
||||
("request_date_from", "!=", False),
|
||||
("request_date_to", "!=", False),
|
||||
("employee_id", "!=", False),
|
||||
]
|
||||
)
|
||||
for leave in leaves:
|
||||
current_date = leave.request_date_from
|
||||
employee = leave.employee_id
|
||||
while current_date <= leave.request_date_to:
|
||||
if self.employee_is_scheduled_to_work_this_day(
|
||||
current_date, employee, leave
|
||||
) and not self._is_public_holiday_according_to_employe_tz(
|
||||
current_date, employee
|
||||
):
|
||||
# The employee is scheduled to work this day
|
||||
# according his calendar and it's not a
|
||||
# public holiday,
|
||||
# so create a timeoff day record if it does not already exist
|
||||
if not self.search(
|
||||
[
|
||||
("date", "=", current_date),
|
||||
("employee_id", "=", employee.id),
|
||||
("hr_leave_id", "=", leave.id),
|
||||
],
|
||||
limit=1,
|
||||
):
|
||||
self.create(
|
||||
{
|
||||
"date": current_date,
|
||||
"employee_id": employee.id,
|
||||
"hr_leave_id": leave.id,
|
||||
"leave_duration_by_day": (
|
||||
self.compute_leave_duration_by_day(leave)
|
||||
),
|
||||
}
|
||||
)
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
def cron_delete_timeoff_days(self):
|
||||
# Browse all unvalidated or canceled leaves
|
||||
leaves = self.env["hr.leave"].search(
|
||||
[
|
||||
("state", "!=", "validate"),
|
||||
("request_date_from", "!=", False),
|
||||
("request_date_to", "!=", False),
|
||||
("employee_id", "!=", False),
|
||||
]
|
||||
)
|
||||
# Delete timeoff days for leaves that are no longer validated
|
||||
for leave in leaves:
|
||||
self.search(
|
||||
[
|
||||
("hr_leave_id", "=", leave.id),
|
||||
]
|
||||
).unlink()
|
||||
# Delete timeoff days that are not linked to any leave
|
||||
self.search(
|
||||
[
|
||||
("hr_leave_id", "=", False),
|
||||
]
|
||||
).unlink()
|
||||
@@ -0,0 +1,3 @@
|
||||
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_hr_leave_timeoff_day_user,access_hr_holidays_timeoff_day_user,model_hr_leave_timeoff_day,hr_holidays.group_hr_holidays_user,1,0,0,0
|
||||
access_hr_leave_timeoff_day_manager,access_hr_holidays_timeoff_day_manager,model_hr_leave_timeoff_day,hr_holidays.group_hr_holidays_manager,1,0,0,0
|
||||
|
1
hr_holidays_timeoff_analysis/tests/__init__.py
Normal file
1
hr_holidays_timeoff_analysis/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_hr_leave_timeoff_day
|
||||
719
hr_holidays_timeoff_analysis/tests/test_hr_leave_timeoff_day.py
Normal file
719
hr_holidays_timeoff_analysis/tests/test_hr_leave_timeoff_day.py
Normal file
@@ -0,0 +1,719 @@
|
||||
from odoo.fields import Date
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestHrLeaveTimeoffDay(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.employee = cls.env["hr.employee"].create(
|
||||
{
|
||||
"name": "Camille",
|
||||
"tz": "Europe/Paris",
|
||||
}
|
||||
)
|
||||
cls.base_calendar = cls.env["resource.calendar"].create(
|
||||
{
|
||||
"name": "default calendar",
|
||||
}
|
||||
)
|
||||
|
||||
cls.time_off_type = cls.env["hr.leave.type"].create(
|
||||
{
|
||||
"name": "Time Off",
|
||||
"requires_allocation": "no",
|
||||
"request_unit": "half_day",
|
||||
}
|
||||
)
|
||||
|
||||
cls.time_off_hour_type = cls.env["hr.leave.type"].create(
|
||||
{
|
||||
"name": "Recovery",
|
||||
"requires_allocation": "no",
|
||||
"request_unit": "hour",
|
||||
}
|
||||
)
|
||||
|
||||
def test_leave_between_2_months_nb_of_days(self):
|
||||
leave = self.env["hr.leave"].create(
|
||||
{
|
||||
"employee_id": self.employee.id,
|
||||
"request_date_from": Date.to_date("2025-06-30"),
|
||||
"request_date_to": Date.to_date("2025-07-06"),
|
||||
"holiday_status_id": self.time_off_type.id,
|
||||
}
|
||||
)
|
||||
leave.state = "validate" # Simulate the leave being validated
|
||||
|
||||
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
|
||||
# Vérifie qu'il existe bien 5 hr.leave.timeoff.day pour ce leave
|
||||
timeoff_days = self.env["hr.leave.timeoff.day"].search(
|
||||
[
|
||||
("employee_id", "=", self.employee.id),
|
||||
("hr_leave_id", "=", leave.id),
|
||||
]
|
||||
)
|
||||
self.assertEqual(
|
||||
len(timeoff_days), 5, "There should be 5 timeoff days for this leave"
|
||||
)
|
||||
# Vérifie que la somme des leave_duration_by_day fait bien 5
|
||||
total_duration = sum(timeoff_days.mapped("leave_duration_by_day"))
|
||||
self.assertEqual(
|
||||
total_duration,
|
||||
5.0,
|
||||
"The sum of leave_duration_by_day should be 5.0 for this leave",
|
||||
)
|
||||
|
||||
def test_leave_duration_by_day_half_day(self):
|
||||
leave = self.env["hr.leave"].create(
|
||||
{
|
||||
"employee_id": self.employee.id,
|
||||
"request_date_from": Date.to_date("2025-07-02"),
|
||||
"request_date_to": Date.to_date("2025-07-02"),
|
||||
"holiday_status_id": self.time_off_type.id,
|
||||
"request_unit_half": True,
|
||||
}
|
||||
)
|
||||
leave.state = "validate" # Simulate the leave being validated
|
||||
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
|
||||
timeoff_days = self.env["hr.leave.timeoff.day"].search(
|
||||
[
|
||||
("employee_id", "=", self.employee.id),
|
||||
("hr_leave_id", "=", leave.id),
|
||||
]
|
||||
)
|
||||
self.assertEqual(
|
||||
len(timeoff_days), 1, "There should be 1 timeoff day for this leave"
|
||||
)
|
||||
|
||||
total_duration = sum(timeoff_days.mapped("leave_duration_by_day"))
|
||||
self.assertEqual(
|
||||
total_duration,
|
||||
0.5,
|
||||
"leave_duration_by_day should be 0.5 for a half day leave",
|
||||
)
|
||||
|
||||
def test_leave_duration_by_day_hour_compare_to_employee_calendar(self):
|
||||
employee_calendar = self.env["resource.calendar"].create(
|
||||
{
|
||||
"name": "Employee Calendar",
|
||||
"attendance_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Monday Morning",
|
||||
"dayofweek": "0",
|
||||
"hour_from": 8,
|
||||
"hour_to": 12,
|
||||
"day_period": "morning",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Monday Afternoon",
|
||||
"dayofweek": "0",
|
||||
"hour_from": 13,
|
||||
"hour_to": 17,
|
||||
"day_period": "afternoon",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Tuesday Morning",
|
||||
"dayofweek": "1",
|
||||
"hour_from": 8,
|
||||
"hour_to": 12,
|
||||
"day_period": "morning",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Tuesday Afternoon",
|
||||
"dayofweek": "1",
|
||||
"hour_from": 13,
|
||||
"hour_to": 17,
|
||||
"day_period": "afternoon",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Wednesday Morning",
|
||||
"dayofweek": "2",
|
||||
"hour_from": 8,
|
||||
"hour_to": 12,
|
||||
"day_period": "morning",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Wednesday Afternoon",
|
||||
"dayofweek": "2",
|
||||
"hour_from": 13,
|
||||
"hour_to": 17,
|
||||
"day_period": "afternoon",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Thursday Morning",
|
||||
"dayofweek": "3",
|
||||
"hour_from": 8,
|
||||
"hour_to": 12,
|
||||
"day_period": "morning",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Thursday Afternoon",
|
||||
"dayofweek": "3",
|
||||
"hour_from": 13,
|
||||
"hour_to": 17,
|
||||
"day_period": "afternoon",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Friday Morning",
|
||||
"dayofweek": "4",
|
||||
"hour_from": 8,
|
||||
"hour_to": 12,
|
||||
"day_period": "morning",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Friday Afternoon",
|
||||
"dayofweek": "4",
|
||||
"hour_from": 13,
|
||||
"hour_to": 17,
|
||||
"day_period": "afternoon",
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
self.employee.resource_calendar_id = employee_calendar
|
||||
|
||||
leave = self.env["hr.leave"].create(
|
||||
{
|
||||
"employee_id": self.employee.id,
|
||||
"request_date_from": Date.to_date("2025-07-03"),
|
||||
"request_date_to": Date.to_date("2025-07-03"),
|
||||
"request_unit_hours": True,
|
||||
"request_hour_from": "8",
|
||||
"request_hour_to": "12",
|
||||
"holiday_status_id": self.time_off_hour_type.id,
|
||||
}
|
||||
)
|
||||
leave._compute_date_from_to()
|
||||
leave._compute_duration()
|
||||
leave.state = "validate" # Simulate the leave being validated
|
||||
|
||||
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
|
||||
timeoff_days = self.env["hr.leave.timeoff.day"].search(
|
||||
[
|
||||
("employee_id", "=", self.employee.id),
|
||||
("hr_leave_id", "=", leave.id),
|
||||
]
|
||||
)
|
||||
self.assertEqual(
|
||||
len(timeoff_days), 1, "There should be 1 timeoff day for this leave"
|
||||
)
|
||||
|
||||
total_duration = sum(timeoff_days.mapped("leave_duration_by_day"))
|
||||
self.assertEqual(
|
||||
total_duration,
|
||||
0.5,
|
||||
"leave_duration_by_day should be 0.5 for 4 hours on an 8h day",
|
||||
)
|
||||
|
||||
def test_leave_duration_by_day_compare_to_time_part_employee_calendar(self):
|
||||
employee_calendar = self.env["resource.calendar"].create(
|
||||
{
|
||||
"name": "Employee Calendar",
|
||||
"attendance_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Monday Morning",
|
||||
"dayofweek": "0",
|
||||
"hour_from": 8,
|
||||
"hour_to": 12,
|
||||
"day_period": "morning",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Monday Afternoon",
|
||||
"dayofweek": "0",
|
||||
"hour_from": 13,
|
||||
"hour_to": 17,
|
||||
"day_period": "afternoon",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Tuesday Morning",
|
||||
"dayofweek": "1",
|
||||
"hour_from": 8,
|
||||
"hour_to": 12,
|
||||
"day_period": "morning",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Tuesday Afternoon",
|
||||
"dayofweek": "1",
|
||||
"hour_from": 13,
|
||||
"hour_to": 17,
|
||||
"day_period": "afternoon",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Wednesday Morning",
|
||||
"dayofweek": "2",
|
||||
"hour_from": 8,
|
||||
"hour_to": 12,
|
||||
"day_period": "morning",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Wednesday Afternoon",
|
||||
"dayofweek": "2",
|
||||
"hour_from": 13,
|
||||
"hour_to": 17,
|
||||
"day_period": "afternoon",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Thursday Morning",
|
||||
"dayofweek": "3",
|
||||
"hour_from": 8,
|
||||
"hour_to": 12,
|
||||
"day_period": "morning",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Thursday Afternoon",
|
||||
"dayofweek": "3",
|
||||
"hour_from": 13,
|
||||
"hour_to": 17,
|
||||
"day_period": "afternoon",
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
self.employee.resource_calendar_id = employee_calendar
|
||||
|
||||
leave = self.env["hr.leave"].create(
|
||||
{
|
||||
"employee_id": self.employee.id,
|
||||
"request_date_from": Date.to_date("2025-07-21"),
|
||||
"request_date_to": Date.to_date("2025-07-27"),
|
||||
"holiday_status_id": self.time_off_type.id,
|
||||
}
|
||||
)
|
||||
leave.state = "validate" # Simulate the leave being validated
|
||||
|
||||
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
|
||||
timeoff_days = self.env["hr.leave.timeoff.day"].search(
|
||||
[
|
||||
("employee_id", "=", self.employee.id),
|
||||
("hr_leave_id", "=", leave.id),
|
||||
]
|
||||
)
|
||||
self.assertEqual(
|
||||
len(timeoff_days), 4, "There should be 4 timeoff day for this leave"
|
||||
)
|
||||
|
||||
total_duration = sum(timeoff_days.mapped("leave_duration_by_day"))
|
||||
self.assertEqual(
|
||||
total_duration,
|
||||
4,
|
||||
"leave_duration_by_day should be 4 day",
|
||||
)
|
||||
|
||||
def test_public_holidays_between_a_leave(self):
|
||||
# Create a public holiday on 8 May 2025,
|
||||
# because of TZ conversion we set it from 7 May 22:00 to 8 May 23:00
|
||||
self.env["resource.calendar.leaves"].create(
|
||||
{
|
||||
"name": "8 mai 2025",
|
||||
"date_from": "2025-05-07 22:00:00",
|
||||
"date_to": "2025-05-08 23:00:00",
|
||||
}
|
||||
)
|
||||
leave = self.env["hr.leave"].create(
|
||||
{
|
||||
"employee_id": self.employee.id,
|
||||
"request_date_from": Date.to_date("2025-05-05"),
|
||||
"request_date_to": Date.to_date(
|
||||
"2025-05-11"
|
||||
), # a public holiday is in between (8 may)
|
||||
"holiday_status_id": self.time_off_type.id,
|
||||
}
|
||||
)
|
||||
leave.state = "validate" # Simulate the leave being validated
|
||||
|
||||
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
|
||||
# Vérifie qu'il existe bien 4 hr.leave.timeoff.day pour ce leave
|
||||
timeoff_days = self.env["hr.leave.timeoff.day"].search(
|
||||
[
|
||||
("employee_id", "=", self.employee.id),
|
||||
("hr_leave_id", "=", leave.id),
|
||||
]
|
||||
)
|
||||
self.assertEqual(
|
||||
len(timeoff_days), 4, "There should be 4 timeoff days for this leave"
|
||||
)
|
||||
# Vérifie que la somme des leave_duration_by_day fait bien 4
|
||||
total_duration = sum(timeoff_days.mapped("leave_duration_by_day"))
|
||||
self.assertEqual(
|
||||
total_duration,
|
||||
4.0,
|
||||
"The sum of leave_duration_by_day should be 4.0 for this leave",
|
||||
)
|
||||
|
||||
def test_unvalidated_leave(self):
|
||||
leave = self.env["hr.leave"].create(
|
||||
{
|
||||
"employee_id": self.employee.id,
|
||||
"request_date_from": Date.to_date("2025-08-04"),
|
||||
"request_date_to": Date.to_date("2025-08-10"),
|
||||
"holiday_status_id": self.time_off_type.id,
|
||||
}
|
||||
)
|
||||
leave.state = "validate" # Simulate the leave being validated
|
||||
|
||||
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
|
||||
# Vérifie qu'il existe bien 5 hr.leave.timeoff.day pour ce leave
|
||||
timeoff_days = self.env["hr.leave.timeoff.day"].search(
|
||||
[
|
||||
("employee_id", "=", self.employee.id),
|
||||
("hr_leave_id", "=", leave.id),
|
||||
]
|
||||
)
|
||||
self.assertEqual(
|
||||
len(timeoff_days), 5, "There should be 5 timeoff days for this leave"
|
||||
)
|
||||
|
||||
leave.state = "confirm"
|
||||
|
||||
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
|
||||
# Vérifie qu'il n'existe plus de hr.leave.timeoff.day pour ce leave
|
||||
timeoff_days = self.env["hr.leave.timeoff.day"].search(
|
||||
[
|
||||
("employee_id", "=", self.employee.id),
|
||||
("hr_leave_id", "=", leave.id),
|
||||
]
|
||||
)
|
||||
self.assertEqual(
|
||||
len(timeoff_days), 0, "There should be no timeoff days for this leave"
|
||||
)
|
||||
|
||||
def test_deleted_leave(self):
|
||||
leave = self.env["hr.leave"].create(
|
||||
{
|
||||
"employee_id": self.employee.id,
|
||||
"request_date_from": Date.to_date("2025-08-18"),
|
||||
"request_date_to": Date.to_date("2025-08-24"),
|
||||
"holiday_status_id": self.time_off_type.id,
|
||||
}
|
||||
)
|
||||
leave.state = "validate" # Simulate the leave being validated
|
||||
|
||||
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
|
||||
# Vérifie qu'il existe bien 5 hr.leave.timeoff.day pour ce leave
|
||||
timeoff_days = self.env["hr.leave.timeoff.day"].search(
|
||||
[
|
||||
("employee_id", "=", self.employee.id),
|
||||
("hr_leave_id", "=", leave.id),
|
||||
]
|
||||
)
|
||||
self.assertEqual(
|
||||
len(timeoff_days), 5, "There should be 5 timeoff days for this leave"
|
||||
)
|
||||
|
||||
leave.state = "confirm"
|
||||
leave.unlink()
|
||||
|
||||
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
|
||||
# Vérifie qu'il n'existe plus de hr.leave.timeoff.day pour ce leave
|
||||
timeoff_days = self.env["hr.leave.timeoff.day"].search(
|
||||
[
|
||||
("employee_id", "=", self.employee.id),
|
||||
("hr_leave_id", "=", leave.id),
|
||||
]
|
||||
)
|
||||
self.assertEqual(
|
||||
len(timeoff_days), 0, "There should be no timeoff days for this leave"
|
||||
)
|
||||
|
||||
def test_unactive_leave(self):
|
||||
"""
|
||||
When a leave is canceled, his status stay 'validate' but the field active is set
|
||||
to False.
|
||||
"""
|
||||
leave = self.env["hr.leave"].create(
|
||||
{
|
||||
"employee_id": self.employee.id,
|
||||
"request_date_from": Date.to_date("2025-12-08"),
|
||||
"request_date_to": Date.to_date("2025-12-14"),
|
||||
"holiday_status_id": self.time_off_type.id,
|
||||
}
|
||||
)
|
||||
leave.state = "validate" # Simulate the leave being validated
|
||||
|
||||
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
|
||||
# Vérifie qu'il existe bien 5 hr.leave.timeoff.day pour ce leave
|
||||
timeoff_days = self.env["hr.leave.timeoff.day"].search(
|
||||
[
|
||||
("employee_id", "=", self.employee.id),
|
||||
("hr_leave_id", "=", leave.id),
|
||||
]
|
||||
)
|
||||
self.assertEqual(
|
||||
len(timeoff_days), 5, "There should be 5 timeoff days for this leave"
|
||||
)
|
||||
|
||||
leave._force_cancel(
|
||||
"reason test", "mail.mt_note"
|
||||
) # Simulate the leave being unactive but status stay 'validate'
|
||||
|
||||
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
|
||||
# Vérifie qu'il n'existe plus de hr.leave.timeoff.day pour ce leave
|
||||
timeoff_days = self.env["hr.leave.timeoff.day"].search(
|
||||
[
|
||||
("employee_id", "=", self.employee.id),
|
||||
("hr_leave_id", "=", leave.id),
|
||||
]
|
||||
)
|
||||
self.assertEqual(
|
||||
len(timeoff_days), 0, "There should be no timeoff days for this leave"
|
||||
)
|
||||
|
||||
def test_hr_employee_calendar(self):
|
||||
if "hr.employee.calendar" not in self.env:
|
||||
return
|
||||
|
||||
employee_calendar_1 = (
|
||||
self.base_calendar
|
||||
) # full time calendar (from monday to friday)
|
||||
employee_calendar_2 = self.env["resource.calendar"].create(
|
||||
{ # part time calendar (from monday to thursday)
|
||||
"name": "Employee Calendar",
|
||||
"attendance_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Monday Morning",
|
||||
"dayofweek": "0",
|
||||
"hour_from": 8,
|
||||
"hour_to": 12,
|
||||
"day_period": "morning",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Monday Afternoon",
|
||||
"dayofweek": "0",
|
||||
"hour_from": 13,
|
||||
"hour_to": 17,
|
||||
"day_period": "afternoon",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Tuesday Morning",
|
||||
"dayofweek": "1",
|
||||
"hour_from": 8,
|
||||
"hour_to": 12,
|
||||
"day_period": "morning",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Tuesday Afternoon",
|
||||
"dayofweek": "1",
|
||||
"hour_from": 13,
|
||||
"hour_to": 17,
|
||||
"day_period": "afternoon",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Wednesday Morning",
|
||||
"dayofweek": "2",
|
||||
"hour_from": 8,
|
||||
"hour_to": 12,
|
||||
"day_period": "morning",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Wednesday Afternoon",
|
||||
"dayofweek": "2",
|
||||
"hour_from": 13,
|
||||
"hour_to": 17,
|
||||
"day_period": "afternoon",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Thursday Morning",
|
||||
"dayofweek": "3",
|
||||
"hour_from": 8,
|
||||
"hour_to": 12,
|
||||
"day_period": "morning",
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "Thursday Afternoon",
|
||||
"dayofweek": "3",
|
||||
"hour_from": 13,
|
||||
"hour_to": 17,
|
||||
"day_period": "afternoon",
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
self.env["hr.employee.calendar"].create(
|
||||
{
|
||||
"employee_id": self.employee.id,
|
||||
"date_start": Date.to_date("2023-07-01"),
|
||||
"date_end": Date.to_date("2025-06-30"),
|
||||
"calendar_id": employee_calendar_1.id,
|
||||
}
|
||||
)
|
||||
self.env["hr.employee.calendar"].create(
|
||||
{
|
||||
"employee_id": self.employee.id,
|
||||
"date_start": Date.to_date("2025-07-01"),
|
||||
"date_end": None,
|
||||
"calendar_id": employee_calendar_2.id,
|
||||
}
|
||||
)
|
||||
self.employee.resource_calendar_id = employee_calendar_2
|
||||
|
||||
# Leave during employee_calendar_1 period when employee is full time
|
||||
leave = self.env["hr.leave"].create(
|
||||
{
|
||||
"employee_id": self.employee.id,
|
||||
"request_date_from": Date.to_date(
|
||||
"2024-07-22"
|
||||
), # this leave occures when employee_calendar_1 is applied
|
||||
"request_date_to": Date.to_date("2024-07-28"),
|
||||
"holiday_status_id": self.time_off_type.id,
|
||||
}
|
||||
)
|
||||
leave.state = "validate" # Simulate the leave being validated
|
||||
|
||||
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
|
||||
timeoff_days = self.env["hr.leave.timeoff.day"].search(
|
||||
[
|
||||
("employee_id", "=", self.employee.id),
|
||||
("hr_leave_id", "=", leave.id),
|
||||
]
|
||||
)
|
||||
self.assertEqual(
|
||||
len(timeoff_days), 5, "There should be 5 timeoff day for this leave"
|
||||
)
|
||||
|
||||
total_duration = sum(timeoff_days.mapped("leave_duration_by_day"))
|
||||
self.assertEqual(
|
||||
total_duration,
|
||||
5,
|
||||
"leave_duration_by_day should be 5 day",
|
||||
)
|
||||
|
||||
leave.state = "confirm"
|
||||
leave.unlink()
|
||||
|
||||
# Leave during employee_calendar_2 period (the employee his part time now)
|
||||
leave_2 = self.env["hr.leave"].create(
|
||||
{
|
||||
"employee_id": self.employee.id,
|
||||
"request_date_from": Date.to_date(
|
||||
"2025-11-24"
|
||||
), # this leave occures when employee_calendar_1 is applied
|
||||
"request_date_to": Date.to_date("2025-11-30"),
|
||||
"holiday_status_id": self.time_off_type.id,
|
||||
}
|
||||
)
|
||||
leave_2.state = "validate" # Simulate the leave being validated
|
||||
|
||||
self.env["hr.leave.timeoff.day"].cron_manage_timeoff_days()
|
||||
timeoff_days_2 = self.env["hr.leave.timeoff.day"].search(
|
||||
[
|
||||
("employee_id", "=", self.employee.id),
|
||||
("hr_leave_id", "=", leave_2.id),
|
||||
]
|
||||
)
|
||||
self.assertEqual(
|
||||
len(timeoff_days_2), 4, "There should be 4 timeoff day for this leave"
|
||||
)
|
||||
|
||||
total_duration_2 = sum(timeoff_days_2.mapped("leave_duration_by_day"))
|
||||
self.assertEqual(
|
||||
total_duration_2,
|
||||
4,
|
||||
"leave_duration_by_day should be 4 day",
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version='1.0' encoding='UTF-8' ?>
|
||||
<odoo>
|
||||
<record id="hr_leave_timeoff_day_view_list" model="ir.ui.view">
|
||||
<field name="model">hr.leave.timeoff.day</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom">
|
||||
<field name="date" />
|
||||
<field name="employee_id" groups="base.group_user" />
|
||||
<field name="hr_leave_id" />
|
||||
<field name="hr_leave_type" />
|
||||
<field name="leave_duration_by_day" />
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_leave_timeoff_day_view_pivot" model="ir.ui.view">
|
||||
<field name="model">hr.leave.timeoff.day</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Timeoff Days Analysis">
|
||||
<field name="date" type="col" interval="month" />
|
||||
<field name="employee_id" type="row" />
|
||||
<field name="hr_leave_type" type="row" />
|
||||
<field name="leave_duration_by_day" type="measure" />
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_leave_timeoff_day_view_search" model="ir.ui.view">
|
||||
<field name="model">hr.leave.timeoff.day</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Timeoff Days">
|
||||
<field name="date" />
|
||||
<field name="employee_id" groups="base.group_user" />
|
||||
<separator />
|
||||
<filter
|
||||
name="filter_date"
|
||||
date="date"
|
||||
default_period="year"
|
||||
string="Period"
|
||||
/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_leave_timeoff_day_action" model="ir.actions.act_window">
|
||||
<field name="name">Timeoff Days</field>
|
||||
<field name="res_model">hr.leave.timeoff.day</field>
|
||||
<field name="view_mode">list,form,pivot</field>
|
||||
<field name="search_view_id" ref="hr_leave_timeoff_day_view_search" />
|
||||
<field name="context">{'search_default_filter_date': True}</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
id="menu_hr_holidays_timeoff_day_menu"
|
||||
name="Timeoff Days"
|
||||
parent="hr_holidays.menu_hr_holidays_report"
|
||||
action="hr_leave_timeoff_day_action"
|
||||
groups="hr_holidays.group_hr_holidays_manager"
|
||||
sequence="6"
|
||||
/>
|
||||
|
||||
</odoo>
|
||||
170
hr_luncheon_voucher/README.md
Normal file
170
hr_luncheon_voucher/README.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# HR Luncheon Voucher
|
||||
|
||||
[](https://odoo-community.org/page/development-status)
|
||||
[](http://www.gnu.org/licenses/agpl-3.0-standalone.html)
|
||||
[](https://github.com/elabore-coop/hr-tools)
|
||||
|
||||
Manage luncheon vouchers credit and distribution for employees.
|
||||
|
||||
## Description
|
||||
|
||||
This module allows the management of luncheon vouchers attribution and
|
||||
distribution. Employees can indicate which days are not concerned by luncheon
|
||||
vouchers. HR managers can adjust the number of luncheon vouchers to distribute
|
||||
and follow each employee's credit.
|
||||
|
||||
### Key Models
|
||||
|
||||
- **Luncheon Voucher Allocation** (`hr.lv.allocation`): tracks each distribution
|
||||
campaign per employee, with a state workflow (Draft → Confirmed → Distributed)
|
||||
and computed counters (acquired, due, distributed, balance).
|
||||
- **Employee extensions** (`hr.employee`): additional fields store running totals
|
||||
of acquired, distributed, and remaining vouchers, plus a default monthly
|
||||
distribution value.
|
||||
- **Meeting type extension** (`calendar.event.type`): a boolean field
|
||||
`remove_luncheon_voucher` lets you mark event categories that cancel the daily
|
||||
voucher (e.g. off-site meetings, free lunch).
|
||||
|
||||
### Attribution Rules
|
||||
|
||||
A luncheon voucher is acquired for a working day if:
|
||||
|
||||
- The employee worked on one or all the attendances of the day (depending on
|
||||
whether the option **Half working days cancel luncheon vouchers** is enabled).
|
||||
- There is no meeting that cancels the voucher during that day (e.g. off-site
|
||||
or free lunch meetings).
|
||||
- An attendance is considered worked as long as there is no leave covering the
|
||||
whole attendance time slot.
|
||||
|
||||
The calculation is fully automated when an allocation request is created: the
|
||||
system iterates through each day of the period, checks the working calendar,
|
||||
validates attendance coverage, verifies leaves, and cross-references calendar
|
||||
events.
|
||||
|
||||
### Allocation Lifecycle
|
||||
|
||||
| State | Description |
|
||||
|-------|-------------|
|
||||
| **Draft** | Allocation request created by the wizard. HR can adjust the number of vouchers to distribute. |
|
||||
| **Confirmed** | HR manager validates the figures. Employee counters are updated. |
|
||||
| **Distributed** | Vouchers have been effectively handed out. Final counters are set. |
|
||||
|
||||
### Employee Dashboard
|
||||
|
||||
In the employee form view, three stat buttons display the current running
|
||||
totals: Acquired, Distributed, and Remaining (Due) vouchers. HR can also
|
||||
refresh these values on demand.
|
||||
|
||||
## Installation
|
||||
|
||||
Use Odoo normal module installation procedure to install `hr_luncheon_voucher`.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `base` (Odoo core)
|
||||
- `calendar` (Odoo core)
|
||||
- `hr` (Odoo core)
|
||||
- `hr_holidays` (Odoo core)
|
||||
- `resource` (Odoo core)
|
||||
- `hr_effective_attendance_period` (this repository)
|
||||
|
||||
### Security / Access Rights
|
||||
|
||||
| Model | HR Users | HR Managers |
|
||||
|-------|----------|-------------|
|
||||
| `hr.lv.allocation` | Read only | Full CRUD |
|
||||
| `generate.lv.allocation.requests` | Read only | Full CRUD |
|
||||
|
||||
## Configuration
|
||||
|
||||
1. **Define cancelling meeting categories.**
|
||||
Go to *Configuration > Technical > Calendar > Meeting Types* and enable the
|
||||
*Remove luncheon voucher* flag on the relevant categories (e.g. Off-site,
|
||||
Free lunch). Two default categories are pre-loaded by the module.
|
||||
|
||||
2. **Toggle the half-day rule.**
|
||||
Go to *Configuration > General Settings > Employees* and check/uncheck
|
||||
*Half working days cancel luncheon vouchers* depending on whether partial
|
||||
attendance should still grant a voucher.
|
||||
|
||||
3. **Set default monthly distribution per employee.**
|
||||
Open each employee's form, go to the *Luncheon Vouchers* section under HR
|
||||
settings, and enter the *Default monthly distribution* value. This value is
|
||||
pre-filled when a new allocation is created.
|
||||
|
||||
4. **Configure effective attendance periods.**
|
||||
Go to *Configuration > Technical > Resource > Working Times* and create
|
||||
Working Time entries for each attendance combination used in your company.
|
||||
Make sure to indicate which periods are effectively attended — the module
|
||||
relies on `hr_effective_attendance_period` to determine daily presence.
|
||||
|
||||
## Usage
|
||||
|
||||
### Calendar Events
|
||||
|
||||
When creating a calendar event that cancels luncheon voucher distribution
|
||||
(off-site meeting, free lunch, etc.), simply assign the corresponding meeting
|
||||
category to the event.
|
||||
|
||||
### Generating Allocations
|
||||
|
||||
For each distribution period, the HR manager should:
|
||||
|
||||
1. Go to the **Employees** list view.
|
||||
2. Select all employees concerned by luncheon vouchers distribution.
|
||||
3. Click the header button **Generate Luncheon Vouchers Allocations**.
|
||||
4. Fill in the wizard: set a campaign name (e.g. "January 2026"), a start
|
||||
date, and an end date.
|
||||
5. Click **Create allocations requests**.
|
||||
|
||||
A voucher allocation request is created for each selected employee in **Draft**
|
||||
state. The number of acquired vouchers is computed automatically based on the
|
||||
attribution rules.
|
||||
|
||||
### Allocation Workflow
|
||||
|
||||
1. **Confirm** an allocation request when the figures are verified. Employee
|
||||
running counters are updated.
|
||||
2. **Distribute** the allocation when the vouchers have been effectively handed
|
||||
out. Click *Distribute Vouchers* to mark the request as distributed.
|
||||
3. **Adjust distribution** at any time in Draft state to override the
|
||||
pre-filled distributed quantity.
|
||||
4. **Back to draft** allows correcting an already confirmed or distributed
|
||||
allocation.
|
||||
5. The **Vouchers balance** column shows the difference between due and
|
||||
distributed vouchers for each allocation.
|
||||
|
||||
### Monitoring
|
||||
|
||||
Each employee form displays three stat buttons — *Acquired*, *Distributed*,
|
||||
and *Due* — giving a real-time summary of the employee's luncheon voucher
|
||||
status. Use the **Refresh Luncheon Vouchers** button in the form header to
|
||||
recompute these values if needed.
|
||||
|
||||
## Known Issues / Roadmap
|
||||
|
||||
None yet.
|
||||
|
||||
## Bug Tracker
|
||||
|
||||
Bugs are tracked on [Gitea Issues](https://git.elabore.coop/Elabore/hr-tools/issues).
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us smash it by providing a detailed and welcomed
|
||||
feedback.
|
||||
|
||||
## Credits
|
||||
|
||||
### Contributors
|
||||
|
||||
- Stéphan Sainléger <https://github.com/stephansainleger>
|
||||
|
||||
### Funders
|
||||
|
||||
The development of this module has been financially supported by:
|
||||
|
||||
- Elabore (https://elabore.coop)
|
||||
- Amaco (https://amaco.org)
|
||||
|
||||
### Maintainer
|
||||
|
||||
This module is maintained by Elabore.
|
||||
@@ -1,80 +0,0 @@
|
||||
===================
|
||||
HR Luncheon Voucher
|
||||
===================
|
||||
|
||||
.. |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%2Faccount--analytic-lightgray.png?logo=github
|
||||
:target: https://github.com/elabore-coop/hr-tools
|
||||
:alt: elabore/hr-tools
|
||||
|
||||
|badge1| |badge2| |badge3|
|
||||
|
||||
This module allows the management of Luncheon Vouchers attribution and distribution.
|
||||
Employees can indicate which days are not concerned by luncheon vouchers.
|
||||
HR managers can ajust the number of luncheon vouchers to distribute, and follow each employee credit.
|
||||
|
||||
Installation
|
||||
============
|
||||
Use Odoo normal module installation procedure to install ``hr_luncheon_voucher``.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
1. Go to ``Configuration > Technical > Calendar > Meeting Types`` and define the meeting categories which cancel the daily luncheon voucher distribution.
|
||||
2. Go to ``Configuration > General Settings > Employees`` and define if employees need to work the whole day to get a luncheon voucher.
|
||||
3. Go to ``Employees`` and define for each employee the default number of luncheon vouchers to distribute in each distribution campaign.
|
||||
4. Go to ``Configuration > Technical > Resource > Working Times`` and create Working Times for each combination of attendance that you have in your company. Do not forget to indicate which period are effectively attended.
|
||||
|
||||
Use
|
||||
===
|
||||
- when an calendar event makes the luncheon voucher distribution cancelled (Off-site or free lunch for instance), add the corresponding category to the event.
|
||||
- for each distribution period, the HR manager should:
|
||||
- go in ``Employees`` tree view
|
||||
- select all the employees concerned by luncheon vouchers distribution
|
||||
- click on header button ``Generate luncheon vouchers allocation``
|
||||
- Fill the wizard form
|
||||
- a voucher allocation request is created for each employee
|
||||
- HR manager confirms an allocation request when the figures are confirmed
|
||||
- HR manager marks the requests as "Distributed" when the vouchers has been effectively distributed
|
||||
- HR manager can correct the allocation requests with ``Back to draft`` button
|
||||
- employees' luncheon voucher counters are updated considering the vouchers acquired, dued and distributed at each campaign.
|
||||
|
||||
**Attribution rules:**
|
||||
|
||||
- a luncheon voucher is acquired for a working day if:
|
||||
- the employee worked on one or all the attendances of the day (depending if option ``Half working days cancel luncheon vouchers`` is True or not)
|
||||
- there is no meeting which cancel the voucher during that day (``Site off`` or ``Free lunch`` meeting for instance)
|
||||
- an attendance is considered as worked as long as there is no leave on the whole attendance time slot
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
None yet.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
Bugs are tracked on `GitHub Issues
|
||||
<https://github.com/elabore-coop/hr-tools/issues>`_. In case of trouble, please
|
||||
check there if your issue has already been reported. If you spotted it first,
|
||||
help us smashing it by providing a detailed and welcomed feedback.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Contributors
|
||||
------------
|
||||
- Stéphan Sainléger <https://github.com/stephansainleger>
|
||||
|
||||
Funders
|
||||
-------
|
||||
The development of this module has been financially supported by:
|
||||
|
||||
- Elabore (https://elabore.coop)
|
||||
- Amaco (https://amaco.org)
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
This module is maintained by ELABORE.
|
||||
7
hr_luncheon_voucher/__manifest__.py
Executable file → Normal file
7
hr_luncheon_voucher/__manifest__.py
Executable file → Normal file
@@ -1,11 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
"name": "HR Luncheon Voucher",
|
||||
"category": "Human Resources",
|
||||
"version": "14.0.2.0.1",
|
||||
"version": "18.0.1.0.0",
|
||||
"summary": "Manage luncheon vouchers credit and distribution",
|
||||
"author": "Elabore",
|
||||
"website": "https://elabore.coop/",
|
||||
"website": "https://git.elabore.coop/elabore/hr-tools",
|
||||
"license": "AGPL-3",
|
||||
"installable": True,
|
||||
"application": True,
|
||||
"auto_install": False,
|
||||
@@ -27,5 +27,4 @@
|
||||
"wizard/generate_lv_allocations_wizard.xml",
|
||||
"data/event_type_data.xml",
|
||||
],
|
||||
"qweb": [],
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="categ_meet_free_lunch" model="calendar.event.type">
|
||||
<field name="name">Free lunch</field>
|
||||
<field name="ref">categ_meet_free_lunch</field>
|
||||
<field name="remove_luncheon_voucher">1</field>
|
||||
</record>
|
||||
<?xml version="1.0" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="categ_meet_free_lunch" model="calendar.event.type">
|
||||
<field name="name">Free lunch</field>
|
||||
<field name="ref">categ_meet_free_lunch</field>
|
||||
<field name="remove_luncheon_voucher">1</field>
|
||||
</record>
|
||||
|
||||
<record id="categ_meet_offsite" model="calendar.event.type">
|
||||
<field name="name">Off-site</field>
|
||||
<field name="ref">categ_meet_offsite</field>
|
||||
<field name="remove_luncheon_voucher">1</field>
|
||||
</record>
|
||||
</data>
|
||||
<record id="categ_meet_offsite" model="calendar.event.type">
|
||||
<field name="name">Off-site</field>
|
||||
<field name="ref">categ_meet_offsite</field>
|
||||
<field name="remove_luncheon_voucher">1</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -2,7 +2,6 @@ from odoo import fields, models
|
||||
|
||||
|
||||
class MeetingType(models.Model):
|
||||
|
||||
_inherit = "calendar.event.type"
|
||||
|
||||
ref = fields.Char(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from xml.dom.minicompat import EmptyNodeList
|
||||
from odoo import fields, models, api
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrEmployeeBase(models.AbstractModel):
|
||||
@@ -30,7 +29,10 @@ class HrEmployeeBase(models.AbstractModel):
|
||||
def _compute_total_acquired_lv(self):
|
||||
for record in self:
|
||||
allocations = self.env["hr.lv.allocation"].search(
|
||||
[("employee_id", "=", record.id), ("state", "=", ["confirmed", "distributed"])]
|
||||
[
|
||||
("employee_id", "=", record.id),
|
||||
("state", "in", ["confirmed", "distributed"]),
|
||||
]
|
||||
)
|
||||
record.total_acquired_lv = sum(allocations.mapped("number_acquired_lv"))
|
||||
|
||||
@@ -56,12 +58,14 @@ class HrEmployeeBase(models.AbstractModel):
|
||||
self.env["hr.lv.allocation"].create(values)
|
||||
|
||||
def action_lv_allocations(self):
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id("hr_luncheon_voucher.act_lv_allocations")
|
||||
action['context'] = {
|
||||
'search_default_employee_id': self.id,
|
||||
'default_employee_id': self.id,
|
||||
action = self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"hr_luncheon_voucher.act_lv_allocations"
|
||||
)
|
||||
action["context"] = {
|
||||
"search_default_employee_id": self.id,
|
||||
"default_employee_id": self.id,
|
||||
}
|
||||
action['domain'] = [('employee_id', '=', self.id)]
|
||||
action["domain"] = [("employee_id", "=", self.id)]
|
||||
return action
|
||||
|
||||
def action_lv_allocations_requests_wizard(self):
|
||||
@@ -73,11 +77,12 @@ class HrEmployeeBase(models.AbstractModel):
|
||||
action["context"] = ctx
|
||||
return action
|
||||
|
||||
@api.model
|
||||
def _name_search(self, name='', args=None, operator='ilike', limit=100, name_get_uid=None):
|
||||
def _search_display_name(self, operator, value):
|
||||
"""
|
||||
On lv allocation, allow employee search on all companies
|
||||
On lv allocation, allow employee search on all companies.
|
||||
"""
|
||||
if self.env.context.get('search_all_campanies'):
|
||||
return super(HrEmployeeBase, self.sudo())._name_search(name, args, operator, limit, name_get_uid)
|
||||
return super(HrEmployeeBase, self)._name_search(name, args, operator, limit, name_get_uid)
|
||||
if self.env.context.get("search_all_campanies"):
|
||||
return super(HrEmployeeBase, self.sudo())._search_display_name(
|
||||
operator, value
|
||||
)
|
||||
return super()._search_display_name(operator, value)
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
from datetime import datetime, timedelta, time
|
||||
from dateutil.rrule import rrule, DAILY
|
||||
from datetime import datetime, time, timedelta
|
||||
|
||||
from dateutil.rrule import DAILY, rrule
|
||||
from pytz import UTC
|
||||
|
||||
from odoo import fields, models, api
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class LuncheonVouchersAllocation(models.Model):
|
||||
_name = "hr.lv.allocation"
|
||||
_description = "Luncheon Vouchers Allocation"
|
||||
|
||||
name = fields.Char('Name')
|
||||
distrib_campaign_name = fields.Char('Distribution campaign')
|
||||
name = fields.Char()
|
||||
distrib_campaign_name = fields.Char("Distribution campaign")
|
||||
state = fields.Selection(
|
||||
[
|
||||
("draft", "Draft"),
|
||||
@@ -21,7 +22,12 @@ class LuncheonVouchersAllocation(models.Model):
|
||||
readonly=True,
|
||||
copy=False,
|
||||
default="draft",
|
||||
help="The status is set to 'Draft', when an allocation request is created. The status is 'Confirmed', when an allocation request is confirmed by HR manager. The status is 'Distributed', when the luncheon vouchers have been distributed.",
|
||||
help=(
|
||||
"The status is set to 'Draft', when an allocation request is created. "
|
||||
"The status is 'Confirmed', when an allocation request is confirmed by "
|
||||
"HR manager. The status is 'Distributed', when the luncheon vouchers "
|
||||
"have been distributed."
|
||||
),
|
||||
)
|
||||
date_from = fields.Datetime(
|
||||
string="Start Date",
|
||||
@@ -83,16 +89,22 @@ class LuncheonVouchersAllocation(models.Model):
|
||||
},
|
||||
)
|
||||
|
||||
lv_balance = fields.Integer('Vouchers balance', compute="_get_lv_balance", help="Vouchers available after distribution. Dued vouchers - Distributed vouchers")
|
||||
lv_balance = fields.Integer(
|
||||
"Vouchers balance",
|
||||
compute="_compute_lv_balance",
|
||||
help="Vouchers available after distribution. Dued vouchers - Distributed.",
|
||||
)
|
||||
|
||||
@api.depends("number_dued_lv", "number_distributed_lv")
|
||||
def _get_lv_balance(self):
|
||||
def _compute_lv_balance(self):
|
||||
for allocation in self:
|
||||
allocation.lv_balance = allocation.number_dued_lv - allocation.number_distributed_lv
|
||||
allocation.lv_balance = (
|
||||
allocation.number_dued_lv - allocation.number_distributed_lv
|
||||
)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, values):
|
||||
res = super(LuncheonVouchersAllocation, self).create(values)
|
||||
res = super().create(values)
|
||||
res._calculate_number_acquired_lv()
|
||||
res._calculate_number_dued_lv()
|
||||
res._default_number_distributed_lv()
|
||||
@@ -104,16 +116,21 @@ class LuncheonVouchersAllocation(models.Model):
|
||||
record.number_distributed_lv = record.employee_id.default_monthly_lv
|
||||
|
||||
def _has_cancelling_voucher_event(self, day):
|
||||
category_no_voucher_ids = self.env["calendar.event.type"].search([("remove_luncheon_voucher", "=", True)])
|
||||
category_no_voucher_ids = self.env["calendar.event.type"].search(
|
||||
[("remove_luncheon_voucher", "=", True)]
|
||||
)
|
||||
events = self.env["calendar.event"].search(
|
||||
[
|
||||
("categ_ids", "in", category_no_voucher_ids.ids),
|
||||
("partner_ids", "in", self.employee_id.user_id.partner_id.id)
|
||||
("partner_ids", "in", self.employee_id.user_id.partner_id.id),
|
||||
]
|
||||
)
|
||||
day_start = fields.Datetime.to_datetime(day.date())
|
||||
day_end = fields.Datetime.to_datetime(day.date()) + timedelta(hours=24)
|
||||
cancelling_events = events.filtered(lambda x: not((x.start < day_start) and (x.stop <= day_start)) and not((x.start >= day_end) and (x.stop > day_end)) )
|
||||
cancelling_events = events.filtered(
|
||||
lambda x: not ((x.start < day_start) and (x.stop <= day_start))
|
||||
and not ((x.start >= day_end) and (x.stop > day_end))
|
||||
)
|
||||
if len(cancelling_events) > 0:
|
||||
return True
|
||||
else:
|
||||
@@ -121,8 +138,12 @@ class LuncheonVouchersAllocation(models.Model):
|
||||
|
||||
def _calculate_number_acquired_lv(self):
|
||||
nb_eligible_days = 0
|
||||
dfrom = datetime.combine(fields.Date.from_string(self.date_from), time.min).replace(tzinfo=UTC)
|
||||
dto = datetime.combine(fields.Date.from_string(self.date_to), time.max).replace(tzinfo=UTC)
|
||||
dfrom = datetime.combine(
|
||||
fields.Date.from_string(self.date_from), time.min
|
||||
).replace(tzinfo=UTC)
|
||||
dto = datetime.combine(fields.Date.from_string(self.date_to), time.max).replace(
|
||||
tzinfo=UTC
|
||||
)
|
||||
period_days = rrule(DAILY, dfrom, until=dto)
|
||||
calendar_resource = self.employee_id.resource_calendar_id
|
||||
for day in period_days:
|
||||
@@ -130,15 +151,23 @@ class LuncheonVouchersAllocation(models.Model):
|
||||
if not calendar_resource.is_working_day(day):
|
||||
continue
|
||||
# The employee should work this day but...
|
||||
if self.env.company.hr_half_day_cancels_voucher and not calendar_resource.is_full_working_day(day):
|
||||
# The luncheon voucher is acquired only if the employee has worked the entire day
|
||||
if (
|
||||
self.env.company.hr_half_day_cancels_voucher
|
||||
and not calendar_resource.is_full_working_day(day)
|
||||
):
|
||||
# Voucher requires full-day attendance
|
||||
continue
|
||||
# Check leaves
|
||||
if not calendar_resource.is_worked_day(self.employee_id, day):
|
||||
continue
|
||||
# The employee has worked this day but...
|
||||
if self.env.company.hr_half_day_cancels_voucher and not calendar_resource.all_attendances_worked(self.employee_id.resource_id, day):
|
||||
# The luncheon voucher is acquired only if the employee has worked the entire day
|
||||
if (
|
||||
self.env.company.hr_half_day_cancels_voucher
|
||||
and not calendar_resource.all_attendances_worked(
|
||||
self.employee_id.resource_id, day
|
||||
)
|
||||
):
|
||||
# Voucher requires all attendances to be worked
|
||||
continue
|
||||
# Check there is no event cancelling the voucher
|
||||
if self._has_cancelling_voucher_event(day):
|
||||
@@ -165,7 +194,7 @@ class LuncheonVouchersAllocation(models.Model):
|
||||
|
||||
def back_to_draft(self):
|
||||
for record in self:
|
||||
if record.state in ["confirmed","distributed"]:
|
||||
if record.state in ["confirmed", "distributed"]:
|
||||
record.state = "draft"
|
||||
record.employee_id._compute_total_acquired_lv()
|
||||
record.employee_id._compute_distributed_lv()
|
||||
@@ -182,5 +211,6 @@ class LuncheonVouchersAllocation(models.Model):
|
||||
for record in self:
|
||||
for record in self:
|
||||
if record.state == "draft":
|
||||
record.number_distributed_lv = record.employee_id.dued_lv + record.number_acquired_lv
|
||||
|
||||
record.number_distributed_lv = (
|
||||
record.employee_id.dued_lv + record.number_acquired_lv
|
||||
)
|
||||
|
||||
@@ -2,6 +2,8 @@ from odoo import fields, models
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
_inherit = 'res.company'
|
||||
_inherit = "res.company"
|
||||
|
||||
hr_half_day_cancels_voucher = fields.Boolean(string="Half working days cancel luncheon vouchers")
|
||||
hr_half_day_cancels_voucher = fields.Boolean(
|
||||
string="Half working days cancel luncheon vouchers"
|
||||
)
|
||||
|
||||
@@ -2,7 +2,10 @@ from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
hr_half_day_cancels_voucher = fields.Boolean(string="Half working days cancel luncheon vouchers", related="company_id.hr_half_day_cancels_voucher", readonly=False)
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
hr_half_day_cancels_voucher = fields.Boolean(
|
||||
string="Half working days cancel luncheon vouchers",
|
||||
related="company_id.hr_half_day_cancels_voucher",
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
3
hr_luncheon_voucher/tests/__init__.py
Normal file
3
hr_luncheon_voucher/tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from . import test_hr_employee
|
||||
from . import test_hr_lv_allocation
|
||||
from . import test_generate_lv_allocations_wizard
|
||||
112
hr_luncheon_voucher/tests/test_generate_lv_allocations_wizard.py
Normal file
112
hr_luncheon_voucher/tests/test_generate_lv_allocations_wizard.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestGenerateLvAllocationsWizard(TransactionCase):
|
||||
"""Tests for generate.lv.allocation.requests wizard."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.employee_admin = cls.env.ref("hr.employee_admin")
|
||||
cls.employee_other = cls.env["hr.employee"].create(
|
||||
{
|
||||
"name": "Other Employee",
|
||||
"resource_calendar_id": cls.employee_admin.resource_calendar_id.id,
|
||||
}
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# generate_lv_allocations
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_generate_for_single_employee(self):
|
||||
"""Wizard creates one allocation for the selected employee."""
|
||||
wizard = self._create_wizard()
|
||||
# Simulate single employee selection
|
||||
ctx = {"active_ids": [self.employee_admin.id]}
|
||||
wizard = wizard.with_context(**ctx)
|
||||
wizard.generate_lv_allocations()
|
||||
|
||||
allocations = self.env["hr.lv.allocation"].search(
|
||||
[("distrib_campaign_name", "=", "March 2026")]
|
||||
)
|
||||
self.assertEqual(len(allocations), 1)
|
||||
self.assertEqual(allocations.employee_id, self.employee_admin)
|
||||
|
||||
def test_generate_for_multiple_employees(self):
|
||||
"""Wizard creates one allocation per selected employee."""
|
||||
wizard = self._create_wizard()
|
||||
ctx = {
|
||||
"active_ids": [
|
||||
self.employee_admin.id,
|
||||
self.employee_other.id,
|
||||
]
|
||||
}
|
||||
wizard = wizard.with_context(**ctx)
|
||||
wizard.generate_lv_allocations()
|
||||
|
||||
allocations = self.env["hr.lv.allocation"].search(
|
||||
[("distrib_campaign_name", "=", "March 2026")]
|
||||
)
|
||||
self.assertEqual(len(allocations), 2)
|
||||
|
||||
def test_generate_allocation_name_format(self):
|
||||
"""Allocation name follows 'campaign - employee' pattern."""
|
||||
wizard = self._create_wizard()
|
||||
ctx = {"active_ids": [self.employee_admin.id]}
|
||||
wizard = wizard.with_context(**ctx)
|
||||
wizard.generate_lv_allocations()
|
||||
|
||||
alloc = self.env["hr.lv.allocation"].search(
|
||||
[("distrib_campaign_name", "=", "March 2026")]
|
||||
)
|
||||
expected_name = f"March 2026 - {self.employee_admin.name}"
|
||||
self.assertEqual(alloc.name, expected_name)
|
||||
|
||||
def test_generate_allocations_have_dates(self):
|
||||
"""Allocations carry the wizard's date range."""
|
||||
wizard = self._create_wizard()
|
||||
ctx = {"active_ids": [self.employee_admin.id]}
|
||||
wizard = wizard.with_context(**ctx)
|
||||
wizard.generate_lv_allocations()
|
||||
|
||||
alloc = self.env["hr.lv.allocation"].search(
|
||||
[("distrib_campaign_name", "=", "March 2026")]
|
||||
)
|
||||
self.assertEqual(alloc.date_from, wizard.date_from)
|
||||
self.assertEqual(alloc.date_to, wizard.date_to)
|
||||
|
||||
def test_generate_allocations_start_in_draft(self):
|
||||
"""New allocations start in 'draft' state."""
|
||||
wizard = self._create_wizard()
|
||||
ctx = {"active_ids": [self.employee_admin.id]}
|
||||
wizard = wizard.with_context(**ctx)
|
||||
wizard.generate_lv_allocations()
|
||||
|
||||
alloc = self.env["hr.lv.allocation"].search(
|
||||
[("distrib_campaign_name", "=", "March 2026")]
|
||||
)
|
||||
self.assertEqual(alloc.state, "draft")
|
||||
|
||||
def test_generate_returns_action(self):
|
||||
"""The wizard returns an action dict (act_window)."""
|
||||
wizard = self._create_wizard()
|
||||
ctx = {"active_ids": [self.employee_admin.id]}
|
||||
wizard = wizard.with_context(**ctx)
|
||||
action = wizard.generate_lv_allocations()
|
||||
self.assertIn("type", action)
|
||||
self.assertEqual(action["type"], "ir.actions.act_window")
|
||||
self.assertEqual(action["res_model"], "hr.lv.allocation")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _create_wizard(self):
|
||||
return self.env["generate.lv.allocation.requests"].create(
|
||||
{
|
||||
"distrib_campaign_name": "March 2026",
|
||||
"date_from": "2026-03-01",
|
||||
"date_to": "2026-03-31",
|
||||
}
|
||||
)
|
||||
169
hr_luncheon_voucher/tests/test_hr_employee.py
Normal file
169
hr_luncheon_voucher/tests/test_hr_employee.py
Normal file
@@ -0,0 +1,169 @@
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestHrEmployee(TransactionCase):
|
||||
"""Tests for hr.employee extensions (_compute*, generate_lv_allocation)."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# Use a demo employee (created by hr module)
|
||||
cls.employee = cls.env.ref("hr.employee_admin")
|
||||
# Ensure a clean slate
|
||||
cls.employee.write(
|
||||
{
|
||||
"total_acquired_lv": 0,
|
||||
"distributed_lv": 0,
|
||||
"dued_lv": 0,
|
||||
}
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# _compute_total_acquired_lv
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_compute_total_acquired_lv_no_allocations(self):
|
||||
"""No allocations → total_acquired_lv is 0."""
|
||||
self.employee.refresh_lv_values()
|
||||
self.assertEqual(self.employee.total_acquired_lv, 0)
|
||||
|
||||
def test_compute_total_acquired_lv_with_allocations(self):
|
||||
"""Confirmed + distributed allocations are summed."""
|
||||
self._create_allocation(state="confirmed", number_acquired_lv=5)
|
||||
self._create_allocation(state="distributed", number_acquired_lv=3)
|
||||
self.employee.refresh_lv_values()
|
||||
self.assertEqual(self.employee.total_acquired_lv, 8)
|
||||
|
||||
def test_compute_total_acquired_lv_excludes_draft(self):
|
||||
"""Draft allocations are NOT counted."""
|
||||
self._create_allocation(state="draft", number_acquired_lv=10)
|
||||
self.employee.refresh_lv_values()
|
||||
self.assertEqual(self.employee.total_acquired_lv, 0)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# _compute_distributed_lv
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_compute_distributed_lv_no_allocations(self):
|
||||
"""No allocations → distributed_lv is 0."""
|
||||
self.employee.refresh_lv_values()
|
||||
self.assertEqual(self.employee.distributed_lv, 0)
|
||||
|
||||
def test_compute_distributed_lv_only_distributed(self):
|
||||
"""Only 'distributed' state allocations are counted."""
|
||||
self._create_allocation(state="distributed", number_distributed_lv=4)
|
||||
self._create_allocation(state="confirmed", number_distributed_lv=2)
|
||||
self._create_allocation(state="draft", number_distributed_lv=1)
|
||||
self.employee.refresh_lv_values()
|
||||
self.assertEqual(self.employee.distributed_lv, 4)
|
||||
|
||||
def test_compute_distributed_lv_multiple_distributed(self):
|
||||
"""Multiple distributed allocations are summed."""
|
||||
self._create_allocation(state="distributed", number_distributed_lv=3)
|
||||
self._create_allocation(state="distributed", number_distributed_lv=7)
|
||||
self.employee.refresh_lv_values()
|
||||
self.assertEqual(self.employee.distributed_lv, 10)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# _compute_dued_lv
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_compute_dued_lv(self):
|
||||
"""dued_lv = total_acquired_lv - distributed_lv."""
|
||||
self.employee.write(
|
||||
{"total_acquired_lv": 20, "distributed_lv": 6}
|
||||
)
|
||||
self.employee._compute_dued_lv()
|
||||
self.assertEqual(self.employee.dued_lv, 14)
|
||||
|
||||
def test_compute_dued_lv_zero_when_equal(self):
|
||||
"""dued_lv is 0 when acquired equals distributed."""
|
||||
self.employee.write(
|
||||
{"total_acquired_lv": 10, "distributed_lv": 10}
|
||||
)
|
||||
self.employee._compute_dued_lv()
|
||||
self.assertEqual(self.employee.dued_lv, 0)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# generate_lv_allocation
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_generate_lv_allocation_creates_record(self):
|
||||
"""generate_lv_allocation creates an hr.lv.allocation with correct values."""
|
||||
campaign_name = "Test Campaign"
|
||||
values = {
|
||||
"distrib_campaign_name": campaign_name,
|
||||
"date_from": "2026-01-01",
|
||||
"date_to": "2026-01-31",
|
||||
}
|
||||
self.employee.generate_lv_allocation(values)
|
||||
alloc = self.env["hr.lv.allocation"].search(
|
||||
[
|
||||
("employee_id", "=", self.employee.id),
|
||||
("distrib_campaign_name", "=", campaign_name),
|
||||
]
|
||||
)
|
||||
self.assertTrue(alloc)
|
||||
self.assertEqual(len(alloc), 1)
|
||||
self.assertEqual(alloc.distrib_campaign_name, campaign_name)
|
||||
self.assertEqual(alloc.employee_id, self.employee)
|
||||
self.assertIn(self.employee.name, alloc.name)
|
||||
|
||||
def test_generate_lv_allocation_sets_employee_id(self):
|
||||
"""The passed employee_id in values is overridden by the record's own id."""
|
||||
other = self.env["hr.employee"].create(
|
||||
{
|
||||
"name": "Other Employee",
|
||||
"resource_calendar_id": self.employee.resource_calendar_id.id,
|
||||
}
|
||||
)
|
||||
values = {
|
||||
"distrib_campaign_name": "Campaign Override",
|
||||
"employee_id": other.id,
|
||||
"date_from": "2026-02-01",
|
||||
"date_to": "2026-02-28",
|
||||
}
|
||||
self.employee.generate_lv_allocation(values)
|
||||
alloc = self.env["hr.lv.allocation"].search(
|
||||
[("distrib_campaign_name", "=", "Campaign Override")]
|
||||
)
|
||||
self.assertEqual(alloc.employee_id, self.employee)
|
||||
|
||||
def test_generate_lv_allocation_updates_counters(self):
|
||||
"""After creation, acquired and due are computed (triggers create logic)."""
|
||||
values = {
|
||||
"distrib_campaign_name": "Counters Check",
|
||||
"date_from": "2026-03-01",
|
||||
"date_to": "2026-03-31",
|
||||
}
|
||||
self.employee.generate_lv_allocation(values)
|
||||
alloc = self.env["hr.lv.allocation"].search(
|
||||
[("distrib_campaign_name", "=", "Counters Check")]
|
||||
)
|
||||
# number_acquired_lv should be computed (may be 0 on non-working days)
|
||||
self.assertIsNotNone(alloc.number_acquired_lv)
|
||||
self.assertIsNotNone(alloc.number_dued_lv)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _create_allocation(self, **kwargs):
|
||||
# Extract numeric fields that create() overrides, to set them after creation
|
||||
numeric_fields = {
|
||||
k: kwargs.pop(k)
|
||||
for k in ["number_acquired_lv", "number_dued_lv", "number_distributed_lv"]
|
||||
if k in kwargs
|
||||
}
|
||||
defaults = {
|
||||
"employee_id": self.employee.id,
|
||||
"distrib_campaign_name": "Test",
|
||||
"date_from": "2026-01-01",
|
||||
"date_to": "2026-01-31",
|
||||
"name": "Test - Employee",
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
alloc = self.env["hr.lv.allocation"].create(defaults)
|
||||
if numeric_fields:
|
||||
alloc.write(numeric_fields)
|
||||
return alloc
|
||||
317
hr_luncheon_voucher/tests/test_hr_lv_allocation.py
Normal file
317
hr_luncheon_voucher/tests/test_hr_lv_allocation.py
Normal file
@@ -0,0 +1,317 @@
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestHrLvAllocation(TransactionCase):
|
||||
"""Tests for hr.lv.allocation model methods."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.employee = cls.env.ref("hr.employee_admin")
|
||||
cls.calendar = cls.env.ref("resource.resource_calendar_std")
|
||||
cls.employee.resource_calendar_id = cls.calendar
|
||||
|
||||
# Ensure the calendar has attendances marked as effective periods
|
||||
# (required by hr_effective_attendance_period for is_working_day etc.)
|
||||
Attendance = cls.env["resource.calendar.attendance"]
|
||||
existing = Attendance.search([("calendar_id", "=", cls.calendar.id)])
|
||||
if existing:
|
||||
existing.write({"effective_attendance_period": True})
|
||||
else:
|
||||
for day_idx, day_name in enumerate(
|
||||
["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
|
||||
):
|
||||
Attendance.create(
|
||||
{
|
||||
"calendar_id": cls.calendar.id,
|
||||
"name": f"{day_name} Morning",
|
||||
"dayofweek": str(day_idx),
|
||||
"day_period": "morning",
|
||||
"hour_from": 8,
|
||||
"hour_to": 12,
|
||||
"effective_attendance_period": True,
|
||||
}
|
||||
)
|
||||
Attendance.create(
|
||||
{
|
||||
"calendar_id": cls.calendar.id,
|
||||
"name": f"{day_name} Afternoon",
|
||||
"dayofweek": str(day_idx),
|
||||
"day_period": "afternoon",
|
||||
"hour_from": 13,
|
||||
"hour_to": 17,
|
||||
"effective_attendance_period": True,
|
||||
}
|
||||
)
|
||||
|
||||
cls.allocation = cls._build_allocation(cls)
|
||||
|
||||
# Ensure a cancelling event type exists
|
||||
cls.cancelling_categ = cls.env.ref(
|
||||
"hr_luncheon_voucher.categ_meet_free_lunch",
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
if not cls.cancelling_categ:
|
||||
cls.cancelling_categ = cls.env["calendar.event.type"].create(
|
||||
{"name": "Free Lunch Test", "remove_luncheon_voucher": True}
|
||||
)
|
||||
else:
|
||||
cls.cancelling_categ.remove_luncheon_voucher = True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_allocation(self, **overrides):
|
||||
vals = {
|
||||
"employee_id": self.employee.id,
|
||||
"distrib_campaign_name": "Test Campaign",
|
||||
"date_from": "2026-01-01",
|
||||
"date_to": "2026-01-31",
|
||||
"name": "Test Campaign - Admin",
|
||||
}
|
||||
vals.update(overrides)
|
||||
return self.env["hr.lv.allocation"].create(vals)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# _compute_lv_balance
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_lv_balance_positive(self):
|
||||
"""Balance = number_dued_lv - number_distributed_lv (positive)."""
|
||||
alloc = self._build_allocation()
|
||||
alloc.write(
|
||||
{"number_dued_lv": 10, "number_distributed_lv": 3}
|
||||
)
|
||||
alloc._compute_lv_balance()
|
||||
self.assertEqual(alloc.lv_balance, 7)
|
||||
|
||||
def test_lv_balance_zero(self):
|
||||
"""Balance is 0 when due equals distributed."""
|
||||
alloc = self._build_allocation()
|
||||
alloc.write(
|
||||
{"number_dued_lv": 5, "number_distributed_lv": 5}
|
||||
)
|
||||
alloc._compute_lv_balance()
|
||||
self.assertEqual(alloc.lv_balance, 0)
|
||||
|
||||
def test_lv_balance_negative(self):
|
||||
"""Balance can be negative (over-distribution)."""
|
||||
alloc = self._build_allocation()
|
||||
alloc.write(
|
||||
{"number_dued_lv": 2, "number_distributed_lv": 5}
|
||||
)
|
||||
alloc._compute_lv_balance()
|
||||
self.assertEqual(alloc.lv_balance, -3)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# _has_cancelling_voucher_event
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_has_cancelling_event_true(self):
|
||||
"""Returns True when a cancelling event exists for the employee."""
|
||||
partner = self.employee.user_id.partner_id
|
||||
self.env["calendar.event"].create(
|
||||
{
|
||||
"name": "Team Lunch",
|
||||
"start": "2026-01-15 12:00:00",
|
||||
"stop": "2026-01-15 14:00:00",
|
||||
"categ_ids": [(6, 0, self.cancelling_categ.ids)],
|
||||
"partner_ids": [(4, partner.id)],
|
||||
}
|
||||
)
|
||||
day = self.env.cr.now().replace(day=15, month=1, year=2026)
|
||||
self.assertTrue(self.allocation._has_cancelling_voucher_event(day))
|
||||
|
||||
def test_has_cancelling_event_false_no_event(self):
|
||||
"""Returns False when no cancelling event exists."""
|
||||
day = self.env.cr.now().replace(day=10, month=1, year=2026)
|
||||
self.assertFalse(self.allocation._has_cancelling_voucher_event(day))
|
||||
|
||||
def test_has_cancelling_event_false_non_cancelling_category(self):
|
||||
"""Returns False when events exist but without the cancelling flag."""
|
||||
categ = self.env["calendar.event.type"].create(
|
||||
{"name": "Regular Meeting", "remove_luncheon_voucher": False}
|
||||
)
|
||||
partner = self.employee.user_id.partner_id
|
||||
self.env["calendar.event"].create(
|
||||
{
|
||||
"name": "Stand-up",
|
||||
"start": "2026-01-15 09:00:00",
|
||||
"stop": "2026-01-15 09:30:00",
|
||||
"categ_ids": [(6, 0, categ.ids)],
|
||||
"partner_ids": [(4, partner.id)],
|
||||
}
|
||||
)
|
||||
day = self.env.cr.now().replace(day=15, month=1, year=2026)
|
||||
self.assertFalse(self.allocation._has_cancelling_voucher_event(day))
|
||||
|
||||
def test_has_cancelling_event_outside_day(self):
|
||||
"""Returns False when the event is entirely outside the target day."""
|
||||
partner = self.employee.user_id.partner_id
|
||||
self.env["calendar.event"].create(
|
||||
{
|
||||
"name": "Late Dinner",
|
||||
"start": "2026-01-15 22:00:00",
|
||||
"stop": "2026-01-16 00:30:00",
|
||||
"categ_ids": [(6, 0, self.cancelling_categ.ids)],
|
||||
"partner_ids": [(4, partner.id)],
|
||||
}
|
||||
)
|
||||
# Check a different day
|
||||
day = self.env.cr.now().replace(day=14, month=1, year=2026)
|
||||
self.assertFalse(self.allocation._has_cancelling_voucher_event(day))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# _calculate_number_acquired_lv
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_calculate_acquired_on_non_working_day(self):
|
||||
"""Acquired is 0 for a period of non-working days (e.g. weekend-only)."""
|
||||
alloc = self._build_allocation(
|
||||
date_from="2026-01-03", # Saturday
|
||||
date_to="2026-01-04", # Sunday
|
||||
)
|
||||
alloc._calculate_number_acquired_lv()
|
||||
self.assertEqual(alloc.number_acquired_lv, 0)
|
||||
|
||||
def test_calculate_acquired_basic(self):
|
||||
"""Acquired is > 0 for a normal working period (employee works)."""
|
||||
alloc = self._build_allocation(
|
||||
date_from="2026-01-05", # Monday
|
||||
date_to="2026-01-09", # Friday
|
||||
)
|
||||
alloc._calculate_number_acquired_lv()
|
||||
# Standard 5-day calendar: 5 working days, no leaves, no events
|
||||
self.assertEqual(alloc.number_acquired_lv, 5)
|
||||
|
||||
def test_calculate_acquired_with_leave(self):
|
||||
"""Acquired excludes days covered by leave."""
|
||||
# Create a leave covering Wednesday 2026-01-07
|
||||
self.env["resource.calendar.leaves"].create(
|
||||
{
|
||||
"name": "Sick leave",
|
||||
"date_from": "2026-01-07 00:00:00",
|
||||
"date_to": "2026-01-07 23:59:59",
|
||||
"calendar_id": self.calendar.id,
|
||||
"resource_id": self.employee.resource_id.id,
|
||||
}
|
||||
)
|
||||
alloc = self._build_allocation(
|
||||
date_from="2026-01-05", # Monday
|
||||
date_to="2026-01-09", # Friday
|
||||
)
|
||||
alloc._calculate_number_acquired_lv()
|
||||
# 5 working days - 1 leave = 4
|
||||
self.assertEqual(alloc.number_acquired_lv, 4)
|
||||
|
||||
def test_calculate_acquired_with_half_day_cancel(self):
|
||||
"""When hr_half_day_cancels_voucher is enabled, partial days are excluded."""
|
||||
self.env.company.hr_half_day_cancels_voucher = True
|
||||
# Modify calendar to have only morning attendance on Wednesday
|
||||
wed_attendance = self.env["resource.calendar.attendance"].search(
|
||||
[
|
||||
("calendar_id", "=", self.calendar.id),
|
||||
("dayofweek", "=", "2"), # Wednesday
|
||||
("day_period", "=", "afternoon"),
|
||||
]
|
||||
)
|
||||
wed_attendance.unlink()
|
||||
|
||||
alloc = self._build_allocation(
|
||||
date_from="2026-01-05", # Monday
|
||||
date_to="2026-01-09", # Friday
|
||||
)
|
||||
alloc._calculate_number_acquired_lv()
|
||||
# 4 full days + 1 half-day excluded = 4
|
||||
self.assertEqual(alloc.number_acquired_lv, 4)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# _calculate_number_dued_lv
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_calculate_dued_draft(self):
|
||||
"""In non-distributed state: dued = employee.dued_lv + number_acquired_lv."""
|
||||
self.employee.write({"dued_lv": 5})
|
||||
alloc = self._build_allocation(state="draft")
|
||||
alloc.number_acquired_lv = 10
|
||||
alloc._calculate_number_dued_lv()
|
||||
self.assertEqual(alloc.number_dued_lv, 15)
|
||||
|
||||
def test_calculate_dued_distributed(self):
|
||||
"""In distributed: dued = employee.dued_lv only (no addition)."""
|
||||
self.employee.write({"dued_lv": 8})
|
||||
alloc = self._build_allocation(state="distributed")
|
||||
alloc.number_acquired_lv = 20
|
||||
alloc._calculate_number_dued_lv()
|
||||
self.assertEqual(alloc.number_dued_lv, 8)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# confirm_allocation
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_confirm_draft(self):
|
||||
"""confirm_allocation transitions draft → confirmed and updates counters."""
|
||||
self.employee.write({"total_acquired_lv": 0, "dued_lv": 0})
|
||||
alloc = self._build_allocation(state="draft")
|
||||
alloc.number_acquired_lv = 6
|
||||
alloc.confirm_allocation()
|
||||
self.assertEqual(alloc.state, "confirmed")
|
||||
self.employee.refresh_lv_values()
|
||||
self.assertEqual(self.employee.total_acquired_lv, 6)
|
||||
|
||||
def test_confirm_already_confirmed_does_not_change(self):
|
||||
"""confirm_allocation on a confirmed record is a no-op."""
|
||||
alloc = self._build_allocation(state="confirmed")
|
||||
alloc.confirm_allocation()
|
||||
self.assertEqual(alloc.state, "confirmed")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# back_to_draft
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_back_to_draft_from_confirmed(self):
|
||||
"""back_to_draft transitions confirmed → draft."""
|
||||
alloc = self._build_allocation(state="confirmed")
|
||||
alloc.back_to_draft()
|
||||
self.assertEqual(alloc.state, "draft")
|
||||
|
||||
def test_back_to_draft_from_distributed(self):
|
||||
"""back_to_draft transitions distributed → draft."""
|
||||
alloc = self._build_allocation(state="distributed")
|
||||
alloc.back_to_draft()
|
||||
self.assertEqual(alloc.state, "draft")
|
||||
|
||||
def test_back_to_draft_draft_is_noop(self):
|
||||
"""back_to_draft on a draft record is a no-op."""
|
||||
alloc = self._build_allocation(state="draft")
|
||||
alloc.back_to_draft()
|
||||
self.assertEqual(alloc.state, "draft")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# distribute_allocation
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_distribute_confirmed(self):
|
||||
"""distribute_allocation transitions confirmed → distributed."""
|
||||
alloc = self._build_allocation(state="confirmed")
|
||||
alloc.distribute_allocation()
|
||||
self.assertEqual(alloc.state, "distributed")
|
||||
|
||||
def test_distribute_draft_is_noop(self):
|
||||
"""distribute_allocation on draft is a no-op."""
|
||||
alloc = self._build_allocation(state="draft")
|
||||
alloc.distribute_allocation()
|
||||
self.assertEqual(alloc.state, "draft")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# adjust_distribution
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_adjust_distribution(self):
|
||||
"""Adjust distribution sets number_distributed_lv = dued + acquired."""
|
||||
alloc = self._build_allocation(state="draft")
|
||||
alloc.number_acquired_lv = 5
|
||||
alloc.employee_id.dued_lv = 3
|
||||
alloc.adjust_distribution()
|
||||
self.assertEqual(alloc.number_distributed_lv, 8)
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="view_event_type_luncheonvoucher_tree" model="ir.ui.view">
|
||||
<field name="name">event.type.luncheonvoucher</field>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_employee_form_lv" model="ir.ui.view">
|
||||
@@ -7,16 +7,44 @@
|
||||
<field name="inherit_id" ref="hr.view_employee_form" />
|
||||
<field name="arch" type="xml">
|
||||
<header position="inside">
|
||||
<button type="object" name="refresh_lv_values" string="Refresh Luncheon Vouchers" class="btn-secundary" />
|
||||
<button
|
||||
type="object"
|
||||
name="refresh_lv_values"
|
||||
string="Refresh Luncheon Vouchers"
|
||||
class="btn-secundary"
|
||||
/>
|
||||
</header>
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button type="object" name="action_lv_allocations" class="oe_stat_button" icon="fa-ticket">
|
||||
<field name="total_acquired_lv" widget="statinfo" string="Acquired" />
|
||||
<button
|
||||
type="object"
|
||||
name="action_lv_allocations"
|
||||
class="oe_stat_button"
|
||||
icon="fa-ticket"
|
||||
>
|
||||
<field
|
||||
name="total_acquired_lv"
|
||||
widget="statinfo"
|
||||
string="Acquired"
|
||||
/>
|
||||
</button>
|
||||
<button type="object" name="action_lv_allocations" class="oe_stat_button" icon="fa-ticket">
|
||||
<field name="distributed_lv" widget="statinfo" string="Distributed" />
|
||||
<button
|
||||
type="object"
|
||||
name="action_lv_allocations"
|
||||
class="oe_stat_button"
|
||||
icon="fa-ticket"
|
||||
>
|
||||
<field
|
||||
name="distributed_lv"
|
||||
widget="statinfo"
|
||||
string="Distributed"
|
||||
/>
|
||||
</button>
|
||||
<button type="object" name="action_lv_allocations" class="oe_stat_button" icon="fa-ticket">
|
||||
<button
|
||||
type="object"
|
||||
name="action_lv_allocations"
|
||||
class="oe_stat_button"
|
||||
icon="fa-ticket"
|
||||
>
|
||||
<field name="dued_lv" widget="statinfo" string="Dued" />
|
||||
</button>
|
||||
</xpath>
|
||||
@@ -34,10 +62,13 @@
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_tree" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//tree" position="inside">
|
||||
<header>
|
||||
<button type="object" name="action_lv_allocations_requests_wizard" string="Generate Luncheon Vouchers Allocations" class="btn-primary" />
|
||||
</header>
|
||||
<xpath expr="//header" position="inside">
|
||||
<button
|
||||
type="object"
|
||||
name="action_lv_allocations_requests_wizard"
|
||||
string="Generate Luncheon Vouchers Allocations"
|
||||
class="btn-primary"
|
||||
/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="hr_lv_allocation_search" model="ir.ui.view">
|
||||
@@ -9,28 +9,55 @@
|
||||
<field name="state" />
|
||||
<field name="employee_id" context="{'search_all_campanies':True}" />
|
||||
<field name="distrib_campaign_name" />
|
||||
<filter name="draft" string="Draft" domain="[('state', '=', 'draft')]" />
|
||||
<filter name="confirmed" string="Confirmed" domain="[('state', '=', 'confirmed')]" />
|
||||
<filter name="distributed" string="Distributed"
|
||||
domain="[('state', '=', 'distributed')]" />
|
||||
<filter
|
||||
name="draft"
|
||||
string="Draft"
|
||||
domain="[('state', '=', 'draft')]"
|
||||
/>
|
||||
<filter
|
||||
name="confirmed"
|
||||
string="Confirmed"
|
||||
domain="[('state', '=', 'confirmed')]"
|
||||
/>
|
||||
<filter
|
||||
name="distributed"
|
||||
string="Distributed"
|
||||
domain="[('state', '=', 'distributed')]"
|
||||
/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_lv_allocation_tree" model="ir.ui.view">
|
||||
<field name="name">hr.lv.allocation.tree</field>
|
||||
<field name="name">hr.lv.allocation.list</field>
|
||||
<field name="model">hr.lv.allocation</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Luncheon vouchers allocations">
|
||||
<list>
|
||||
<header>
|
||||
<button type="object" name="confirm_allocation" string="Confirm"
|
||||
class="btn-primary" />
|
||||
<button type="object" name="distribute_allocation" string="Distribute Vouchers"
|
||||
class="btn-primary" />
|
||||
<button type="object" name="back_to_draft" string="Back to draft"
|
||||
class="btn-secundary" />
|
||||
<button type="object" name="adjust_distribution" string="Adjust distribution"
|
||||
class="btn-secundary" />
|
||||
<button
|
||||
type="object"
|
||||
name="confirm_allocation"
|
||||
string="Confirm"
|
||||
class="btn-primary"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="distribute_allocation"
|
||||
string="Distribute Vouchers"
|
||||
class="btn-primary"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="back_to_draft"
|
||||
string="Back to draft"
|
||||
class="btn-secundary"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="adjust_distribution"
|
||||
string="Adjust distribution"
|
||||
class="btn-secundary"
|
||||
/>
|
||||
</header>
|
||||
<field name="distrib_campaign_name" />
|
||||
<field name="employee_id" />
|
||||
@@ -41,7 +68,7 @@
|
||||
<field name="number_dued_lv" />
|
||||
<field name="number_distributed_lv" />
|
||||
<field name="lv_balance" />
|
||||
</tree>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -51,14 +78,34 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="">
|
||||
<header>
|
||||
<button type="object" name="confirm_allocation" string="Confirm"
|
||||
class="btn-primary" attrs="{'invisible': [('state', '!=', 'draft')]}" />
|
||||
<button type="object" name="distribute_allocation" string="Distribute Vouchers"
|
||||
class="btn-primary" attrs="{'invisible': [('state', '!=', 'confirmed')]}" />
|
||||
<button type="object" name="back_to_draft" string="Back to draft"
|
||||
class="btn-secundary" attrs="{'invisible': [('state', '=', 'draft')]}" />
|
||||
<button type="object" name="adjust_distribution" string="Adjust distribution"
|
||||
class="btn-secundary" attrs="{'invisible': [('state', '!=', 'draft')]}" />
|
||||
<button
|
||||
type="object"
|
||||
name="confirm_allocation"
|
||||
string="Confirm"
|
||||
class="btn-primary"
|
||||
invisible="state != 'draft'"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="distribute_allocation"
|
||||
string="Distribute Vouchers"
|
||||
class="btn-primary"
|
||||
invisible="state != 'confirmed'"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="back_to_draft"
|
||||
string="Back to draft"
|
||||
class="btn-secundary"
|
||||
invisible="state == 'draft'"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="adjust_distribution"
|
||||
string="Adjust distribution"
|
||||
class="btn-secundary"
|
||||
invisible="state != 'draft'"
|
||||
/>
|
||||
</header>
|
||||
<sheet>
|
||||
<h1>
|
||||
@@ -84,6 +131,6 @@
|
||||
<record id="act_lv_allocations" model="ir.actions.act_window">
|
||||
<field name="name">Luncheon vouchers allocations</field>
|
||||
<field name="res_model">hr.lv.allocation</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,6 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<menuitem id="menu_hr_lv_allocations" action="act_lv_allocations" parent="hr.menu_hr_employee_payroll" sequence="1" name="Luncheon vouchers" />
|
||||
<menuitem
|
||||
id="menu_hr_lv_allocations"
|
||||
action="act_lv_allocations"
|
||||
parent="hr.menu_hr_employee_payroll"
|
||||
sequence="1"
|
||||
name="Luncheon vouchers"
|
||||
/>
|
||||
|
||||
</odoo>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_lv_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.lv</field>
|
||||
@@ -6,11 +6,17 @@
|
||||
<field name="priority" eval="99" />
|
||||
<field name="inherit_id" ref="hr.res_config_settings_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='employee_rights_setting_container']/.." position="inside">
|
||||
<xpath
|
||||
expr="//block[@name='employee_rights_setting_container']/.."
|
||||
position="inside"
|
||||
>
|
||||
<h2>Employee Luncheon Vouchers</h2>
|
||||
<div class="row mt16 o_settings_container" name="employee_lv_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="employee_lv_halfday_cancel"
|
||||
title="Luncheon Vouchers Half-day Cancel">
|
||||
<div
|
||||
class="col-12 col-lg-6 o_setting_box"
|
||||
id="employee_lv_halfday_cancel"
|
||||
title="Luncheon Vouchers Half-day Cancel"
|
||||
>
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="hr_half_day_cancels_voucher" />
|
||||
</div>
|
||||
|
||||
@@ -5,15 +5,9 @@ class GenerateLVAllocationRequests(models.TransientModel):
|
||||
_name = "generate.lv.allocation.requests"
|
||||
_description = "Generate Luncheon Vouchers Allocations Requests"
|
||||
|
||||
distrib_campaign_name = fields.Char('Distribution campaign', required=True)
|
||||
date_from = fields.Datetime(
|
||||
string="Start Date",
|
||||
required=True
|
||||
)
|
||||
date_to = fields.Datetime(
|
||||
string="End Date",
|
||||
required=True
|
||||
)
|
||||
distrib_campaign_name = fields.Char("Distribution campaign", required=True)
|
||||
date_from = fields.Datetime(string="Start Date", required=True)
|
||||
date_to = fields.Datetime(string="End Date", required=True)
|
||||
|
||||
def generate_lv_allocations(self):
|
||||
values = {}
|
||||
@@ -27,4 +21,6 @@ class GenerateLVAllocationRequests(models.TransientModel):
|
||||
)
|
||||
employees.generate_mass_lv_allocation(values)
|
||||
# Open lv allocation tree view
|
||||
return self.env["ir.actions.act_window"]._for_xml_id("hr_luncheon_voucher.act_lv_allocations")
|
||||
return self.env["ir.actions.act_window"]._for_xml_id(
|
||||
"hr_luncheon_voucher.act_lv_allocations"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="lv_allocations_requests_wizard" model="ir.ui.view">
|
||||
<record id="lv_allocations_requests_wizard" model="ir.ui.view">
|
||||
<field name="name">lv.allocations.requests.wizard</field>
|
||||
<field name="model">generate.lv.allocation.requests</field>
|
||||
<field name="arch" type="xml">
|
||||
@@ -14,19 +13,30 @@
|
||||
</group>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Create allocations requests" name="generate_lv_allocations" type="object" class="btn-primary" />
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" />
|
||||
<button
|
||||
string="Create allocations requests"
|
||||
name="generate_lv_allocations"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
/>
|
||||
<button
|
||||
string="Cancel"
|
||||
class="btn-secondary"
|
||||
special="cancel"
|
||||
/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="lv_allocations_requests_wizard_action" model="ir.actions.act_window">
|
||||
<record
|
||||
id="lv_allocations_requests_wizard_action"
|
||||
model="ir.actions.act_window"
|
||||
>
|
||||
<field name="name">Create Luncheon Vouchers allocations requests</field>
|
||||
<field name="res_model">generate.lv.allocation.requests</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="lv_allocations_requests_wizard" />
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
0
hr_usability/__init__.py
Normal file
0
hr_usability/__init__.py
Normal file
30
hr_usability/__manifest__.py
Normal file
30
hr_usability/__manifest__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
"name": "hr_usability Elabore",
|
||||
"version": "18.0.1.0.0",
|
||||
"depends": [
|
||||
"base",
|
||||
"hr",
|
||||
"hr_holidays",
|
||||
],
|
||||
"author": "Élabore",
|
||||
"category": "Human Resources/Employees",
|
||||
"summary": "In times off type form view, add 'create_calendar_meeting' field",
|
||||
"description": """
|
||||
|
||||
Go to Times off app > "Setings" > "Type time off"
|
||||
Select a type
|
||||
The 'create_calendar_meeting' is check by default
|
||||
Uncheck it if do not want to display times off in the calendar
|
||||
|
||||
One the checkbox is unchecked for a time off type, the next approuved times off (of that type) won't be created as a meeting and won't appear in the calendar
|
||||
|
||||
""",
|
||||
"data": [
|
||||
"views/hr_leave_type_views.xml",
|
||||
],
|
||||
"demo": [],
|
||||
"application": False,
|
||||
"license": "LGPL-3",
|
||||
}
|
||||
13
hr_usability/views/hr_leave_type_views.xml
Normal file
13
hr_usability/views/hr_leave_type_views.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hr_leave_type_form_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.leave.type.form.inherit</field>
|
||||
<field name="model">hr.leave.type</field>
|
||||
<field name="inherit_id" ref="hr_holidays.edit_holiday_status_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='color']" position="before">
|
||||
<field name="create_calendar_meeting"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
54
timesheet_timesheet_analysis_report/README.md
Normal file
54
timesheet_timesheet_analysis_report/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# timesheet_timesheet_analysis_report
|
||||
|
||||
Add a `timesheet_id` Many2one relation to the `timesheets.analysis.report` model,
|
||||
linking each analysis report line back to its source `account.analytic.line` record.
|
||||
This allows filtering and grouping the Timesheet Analysis Report by individual timesheet
|
||||
entry.
|
||||
|
||||
## Installation
|
||||
|
||||
Use the standard Odoo module installation procedure to install
|
||||
`timesheet_timesheet_analysis_report`.
|
||||
|
||||
**Dependencies:** `base`, `hr_timesheet`
|
||||
|
||||
## Configuration
|
||||
|
||||
No specific configuration is required. The `timesheet_id` field is automatically
|
||||
available on the Timesheet Analysis Report once the module is installed.
|
||||
|
||||
## Usage
|
||||
|
||||
After installation, the Timesheet Analysis Report (`timesheets.analysis.report`)
|
||||
includes a new **Timesheet** field. You can use it to:
|
||||
|
||||
- **Filter** the analysis report by specific timesheet entries.
|
||||
- **Group by** timesheet to aggregate data per individual time entry.
|
||||
- **Search** for report lines linked to a particular timesheet record.
|
||||
|
||||
## Known Issues / Roadmap
|
||||
|
||||
None yet.
|
||||
|
||||
## Bug Tracker
|
||||
|
||||
Bugs are tracked on
|
||||
[our issues website](https://git.elabore.coop/Elabore/hr-tools/issues). In case of
|
||||
trouble, please check there if your issue has already been reported. If you spotted it
|
||||
first, help us smash it by providing detailed feedback.
|
||||
|
||||
## Credits
|
||||
|
||||
### Contributors
|
||||
|
||||
- [Elabore](mailto:laetitia.dacosta@elabore.coop)
|
||||
|
||||
### Funders
|
||||
|
||||
The development of this module has been financially supported by:
|
||||
|
||||
- [Elabore](https://elabore.coop)
|
||||
|
||||
### Maintainer
|
||||
|
||||
This module is maintained by Elabore.
|
||||
1
timesheet_timesheet_analysis_report/__init__.py
Normal file
1
timesheet_timesheet_analysis_report/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
@@ -1,28 +1,26 @@
|
||||
# Copyright 2024 Elabore ()
|
||||
# Copyright 2025 Elabore ()
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "allow_negative_leave_and_allocation",
|
||||
"version": "14.0.1.0.0",
|
||||
"name": "timesheet_timesheet_analysis_report",
|
||||
"version": "18.0.1.0.0",
|
||||
"author": "Elabore",
|
||||
"website": "https://elabore.coop",
|
||||
"website": "https://git.elabore.coop/elabore/hr-tools",
|
||||
"maintainer": "Elabore",
|
||||
"license": "AGPL-3",
|
||||
"category": "hr",
|
||||
"summary": "allow negative leaves and allocations",
|
||||
"category": "HR",
|
||||
"summary": "add a timesheet_id relation to timesheet.analysis.report model",
|
||||
# any module necessary for this one to work correctly
|
||||
"depends": [
|
||||
"base","hr_holidays",
|
||||
"base",
|
||||
"hr_timesheet",
|
||||
],
|
||||
"qweb": [],
|
||||
"external_dependencies": {
|
||||
"python": [],
|
||||
},
|
||||
# always loaded
|
||||
"data": [
|
||||
"views/hr_leave_type_views.xml",
|
||||
"views/hr_leave_views.xml",
|
||||
],
|
||||
"data": [],
|
||||
# only loaded in demonstration mode
|
||||
"demo": [],
|
||||
"js": [],
|
||||
1
timesheet_timesheet_analysis_report/models/__init__.py
Normal file
1
timesheet_timesheet_analysis_report/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import timesheet_analysis_report
|
||||
@@ -0,0 +1,21 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class TimesheetsAnalysisReport(models.Model):
|
||||
_inherit = "timesheets.analysis.report"
|
||||
|
||||
timesheet_id = fields.Many2one(
|
||||
"account.analytic.line",
|
||||
string="Timesheet",
|
||||
readonly=True,
|
||||
help="Feuille de temps",
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _select(self):
|
||||
return (
|
||||
super()._select()
|
||||
+ """,
|
||||
A.id AS timesheet_id
|
||||
"""
|
||||
)
|
||||
Reference in New Issue
Block a user