6 Commits

Author SHA1 Message Date
Stéphan Sainléger
1664bb043a [MIG] project_working_time_task_portal: migrate to 18.0 2026-03-13 17:33:14 +01:00
Stéphan Sainléger
f9f1c98580 [MIG] project_task_billable_hours: migrate to 18.0 2026-03-13 17:25:48 +01:00
Stéphan Sainléger
0a7814ad1a [MIG] project_request_data: migrate to 18.0 2026-01-20 17:24:51 +01:00
Stéphan Sainléger
a97bd8ecca [MIG] project_only_current_project_tasks_as_parent: migrate to 18.0
migrates add-on project_parent_task to 18.0 and change the add-on name
for more understanding.
2026-01-20 17:19:47 +01:00
Stéphan Sainléger
8ab14340f1 [MIG] project_average_acceptable_time: migrate to 18.0 2026-01-20 17:09:37 +01:00
Stéphan Sainléger
9192465ca2 [MIG] project_user_default_project: migrate to 18.0 2026-01-20 15:28:22 +01:00
55 changed files with 276 additions and 1399 deletions

View File

@@ -1,42 +0,0 @@
#!/bin/bash
#
# Run the Python tests of the given Odoo modules.
#
# This script is meant to be executed *inside* an Elabore doodba image
# (docker.0k.io/mirror/odoo:rc_<version>-ELABORE-LIGHT), with a
# PostgreSQL service reachable at $PGHOST. The modules under test are
# read from the current working directory (the checked-out repository),
# which is prepended to the addons path so that the PR's code takes
# precedence over the modules already baked in the image.
#
# Usage: odoo-tests.sh <module1,module2,...>
set -euo pipefail
modules="${1:?usage: odoo-tests.sh <module1,module2,...>}"
export PGHOST=postgres
export PGUSER=odoo
export PGPASSWORD=odoopassword
export PGDATABASE="test_$(basename "$PWD" | tr '.-' '__')"
export WITHOUT_DEMO=all
export DB_FILTER='.*'
# Generate /opt/odoo/auto/odoo.conf from the environment and wait for
# the database to accept connections.
/opt/odoo/common/entrypoint
# Odoo test-tags format: "/module1,/module2" (one leading slash).
tags="/$(echo "$modules" | tr ',' ',/')"
exec odoo \
--config=/opt/odoo/auto/odoo.conf \
--max-cron-threads=0 \
--limit-time-real=10000 \
--workers=0 \
--test-enable \
--stop-after-init \
--addons-path="$PWD,/opt/odoo/auto/addons" \
-d "$PGDATABASE" \
-i "$modules" \
--test-tags "$tags"

View File

@@ -2,6 +2,8 @@ name: pre-commit
on:
pull_request:
branches:
- "16.0*"
jobs:
pre-commit:
@@ -13,10 +15,10 @@ jobs:
python-version: "3.11"
- name: Get python version
run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
- uses: https://gitea.com/actions/cache@v3
with:
path: ~/.cache/pre-commit
key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }}
# - 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

View File

@@ -1,79 +0,0 @@
name: tests
on:
pull_request:
branches:
- "18.0*"
workflow_dispatch:
jobs:
test:
runs-on: odoo-ci
container:
image: docker.0k.io/mirror/odoo:rc_18.0-ELABORE-LIGHT
services:
postgres:
image: docker.0k.io/postgres:17.2.0-myc
env:
POSTGRES_USER: odoo
POSTGRES_PASSWORD: odoopassword
POSTGRES_DB: postgres
steps:
# The doodba image ships no Node.js, so actions/checkout@v4 cannot
# run here. The repository is private, so the job token is required.
- name: Checkout
env:
SECRET_GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -eu
token="${GITEA_TOKEN:-${GITHUB_TOKEN:-${SECRET_GITEA_TOKEN:-${SECRET_GITHUB_TOKEN:-}}}}"
if [ -z "$token" ]; then
echo "::error::No job token available to clone the repository"
exit 1
fi
server="${GITHUB_SERVER_URL:-https://git.elabore.coop}"
repo="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is not set}"
ref="${GITHUB_REF:-${GITHUB_SHA:?neither GITHUB_REF nor GITHUB_SHA is set}}"
auth_url="$(printf '%s' "${server%/}/${repo}.git" |
sed -E "s#^https://#https://${token}@#")"
git init -q .
git config --global --add safe.directory "$PWD"
git remote add origin "$auth_url"
git fetch -q --depth=1 origin "$ref"
git checkout -q FETCH_HEAD
- name: Detect changed modules
id: changed
run: |
set -eu
if [ -n "${GITHUB_BASE_REF:-}" ]; then
git fetch -q --depth=1 origin "refs/heads/$GITHUB_BASE_REF"
base_rev="FETCH_HEAD"
scope="diff against $GITHUB_BASE_REF"
else
base_rev=""
scope="all installable modules (manual run)"
fi
if [ -n "$base_rev" ]; then
modules="$(
git diff --name-only "$base_rev" HEAD |
awk -F/ '{print $1}' | sort -u |
while read -r dir; do
[ -f "$dir/__manifest__.py" ] && echo "$dir"
done | paste -sd, -
)"
else
modules="$(
for dir in */; do
dir="${dir%/}"
[ -f "$dir/__manifest__.py" ] && echo "$dir"
done | paste -sd, -
)"
fi
echo "modules=$modules" >> "$GITHUB_OUTPUT"
echo "Modules to test ($scope): ${modules:-<none>}"
- name: Run Odoo tests
if: steps.changed.outputs.modules != ''
run: bash .gitea/scripts/odoo-tests.sh "${{ steps.changed.outputs.modules }}"

90
.gitignore vendored
View File

@@ -1,88 +1,2 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
/.venv
/.pytest_cache
/.ruff_cache
# C extensions
*.so
# Distribution / packaging
.Python
env/
bin/
build/
develop-eggs/
dist/
eggs/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
*.eggs
# Windows installers
*.msi
# Debian packages
*.deb
# Redhat packages
*.rpm
# MacOS packages
*.dmg
*.pkg
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
# Pycharm
.idea
# Eclipse
.settings
# Visual Studio cache/options directory
.vs/
.vscode
# OSX Files
.DS_Store
# Django stuff:
*.log
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Rope
.ropeproject
# Sphinx documentation
docs/_build/
# Backup files
*~
*.swp
# OCA rules
!static/lib/
*.*~
*.pyc

View File

@@ -22,7 +22,7 @@ exclude: |
(LICENSE.*|COPYING.*)
default_language_version:
python: python3
node: "22.9.0"
node: "16.17.0"
repos:
- repo: local
hooks:
@@ -38,12 +38,8 @@ repos:
entry: found a en.po file
language: fail
files: '[a-zA-Z0-9_]*/i18n/en\.po$'
- repo: https://github.com/sbidoul/whool
rev: v1.3
hooks:
- id: whool-init
- repo: https://github.com/oca/maintainer-tools
rev: b89f767503be6ab2b11e4f50a7557cb20066e667
rev: f9b919b9868143135a9c9cb03021089cabba8223
hooks:
# update the NOT INSTALLABLE ADDONS section above
- id: oca-update-pre-commit-excluded-addons
@@ -53,13 +49,16 @@ repos:
$(git rev-parse --show-toplevel))"'
- id: oca-gen-addon-readme
entry:
bash -c 'oca-gen-addon-readme --addons-dir=. --branch=$(git symbolic-ref
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 --convert-fragments-to-markdown'
- id: oca-gen-external-dependencies
--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.0.33
rev: v0.1.4
hooks:
- id: oca-checks-odoo-module
- id: oca-checks-po
@@ -78,8 +77,8 @@ repos:
files: \.(css|htm|html|js|json|jsx|less|md|scss|toml|ts|xml|yaml|yml)$
language: node
additional_dependencies:
- "prettier@3.3.3"
- "@prettier/plugin-xml@3.4.1"
- "prettier@2.7.1"
- "@prettier/plugin-xml@2.2.0"
- repo: local
hooks:
- id: eslint
@@ -92,11 +91,10 @@ repos:
types: [javascript]
language: node
additional_dependencies:
- "eslint@9.12.0"
- "eslint-plugin-jsdoc@50.3.1"
- "globals@16.0.0"
- "eslint@8.24.0"
- "eslint-plugin-jsdoc@"
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: trailing-whitespace
# exclude autogenerated files
@@ -117,8 +115,23 @@ repos:
- 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.6.8
rev: v0.12.0
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]

View File

@@ -10,7 +10,7 @@ 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
valid-odoo-versions=16.0
[MESSAGES CONTROL]
disable=all
@@ -25,25 +25,19 @@ 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,
incoherent-interpreter-exec-perm,
openerp-exception-warning,
redundant-modulename-xml,
relative-import,
rst-syntax-error,
wrong-tabs-instead-of-spaces,
xml-syntax-error,
assignment-from-none,
attribute-deprecated,
dangerous-default-value,
development-status-allowed,
duplicate-key,
eval-used,
incoherent-interpreter-exec-perm,
license-allowed,
manifest-author-string,
manifest-deprecated-key,
@@ -54,68 +48,73 @@ enable=anomalous-backslash-in-string,
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,
file-not-used,
missing-newline-extrafiles,
old-api7-method-defined,
po-msgstr-variables,
po-syntax-error,
str-format-used,
unnecessary-utf8-coding-comment,
xml-attribute-translatable,
xml-deprecated-qweb-directive,
xml-deprecated-tree-attribute,
attribute-string-redundant,
consider-merging-classes-inherited,
context-overridden,
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,
website-manifest-key-not-valid-uri,
external-request-timeout,
missing-manifest-dependency,
too-complex,,
create-user-wo-reset-password,
dangerous-filter-wo-user,
file-not-used,
missing-newline-extrafiles,
no-utf8-coding-comment,
old-api7-method-defined,
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,
manifest-external-assets
too-complex,
unnecessary-utf8-coding-comment
[REPORTS]

View File

@@ -9,7 +9,7 @@ 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
valid-odoo-versions=16.0
[MESSAGES CONTROL]
disable=all
@@ -17,25 +17,19 @@ 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,
incoherent-interpreter-exec-perm,
openerp-exception-warning,
redundant-modulename-xml,
relative-import,
rst-syntax-error,
wrong-tabs-instead-of-spaces,
xml-syntax-error,
assignment-from-none,
attribute-deprecated,
dangerous-default-value,
development-status-allowed,
duplicate-key,
eval-used,
incoherent-interpreter-exec-perm,
license-allowed,
manifest-author-string,
manifest-deprecated-key,
@@ -46,50 +40,56 @@ enable=anomalous-backslash-in-string,
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,
file-not-used,
missing-newline-extrafiles,
old-api7-method-defined,
po-msgstr-variables,
po-syntax-error,
str-format-used,
unnecessary-utf8-coding-comment,
xml-attribute-translatable,
xml-deprecated-qweb-directive,
xml-deprecated-tree-attribute,
attribute-string-redundant,
consider-merging-classes-inherited,
context-overridden,
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]

View File

@@ -1,205 +0,0 @@
var globals = require('globals');
jsdoc = require("eslint-plugin-jsdoc");
const config = [{
plugins: {
jsdoc,
},
languageOptions: {
globals: {
_: "readonly",
$: "readonly",
fuzzy: "readonly",
jQuery: "readonly",
moment: "readonly",
odoo: "readonly",
openerp: "readonly",
owl: "readonly",
luxon: "readonly",
QUnit: "readonly",
...globals.browser,
},
ecmaVersion: 2024,
sourceType: "script",
},
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",
"jsdoc/check-tag-names": "warn",
"jsdoc/check-types": "warn",
"jsdoc/require-param-description": "off",
"jsdoc/require-return": "off",
"jsdoc/require-return-description": "off",
"jsdoc/require-return-type": "off",
"valid-typeof": "warn",
yoda: "warn",
},
settings: {
jsdoc: {
tagNamePreference: {
arg: "param",
argument: "param",
augments: "extends",
constructor: "class",
exception: "throws",
func: "function",
method: "function",
prop: "property",
return: "returns",
virtual: "abstract",
yield: "yields",
},
preferredTypes: {
array: "Array",
bool: "Boolean",
boolean: "Boolean",
number: "Number",
object: "Object",
str: "String",
string: "String",
},
},
},
}, {
files: ["**/*.esm.js", "**/*test.js"],
languageOptions: {
ecmaVersion: 2024,
sourceType: "module",
},
}];
module.exports = config

View File

@@ -1,14 +0,0 @@
/** @type {import('prettier').Config} */
const config = {
// https://github.com/prettier/prettier/issues/15388#issuecomment-1717746872
plugins: [require.resolve("@prettier/plugin-xml")],
bracketSpacing: false,
printWidth: 88,
proseWrap: "always",
semi: true,
trailingComma: "es5",
xmlWhitespaceSensitivity: "preserve",
};
module.exports = config;

View File

@@ -1 +1,2 @@
from . import models, controllers

View File

@@ -3,15 +3,19 @@
{
"name": "project_average_acceptable_time",
"version": "18.0.1.1.0",
"version": "18.0.1.0.0",
"author": "Elabore",
"website": "https://git.elabore.coop/elabore/project-tools",
"website": "https://github.com/elabore-coop/project-tools",
"maintainer": "Clément Thomas",
"license": "AGPL-3",
"category": "Tools",
"summary": "Task validation without customer agreement",
# any module necessary for this one to work correctly
"depends": ["base", "project", "project_user_default_project"],
"depends": [
"base",
"project",
"project_user_default_project"
],
"qweb": [
# "static/src/xml/*.xml",
],
@@ -19,7 +23,10 @@
"python": [],
},
# always loaded
"data": ["views/project_project.xml", "views/portal_home_template.xml"],
"data": [
"views/project_project.xml",
"views/portal_home_template.xml"
],
# only loaded in demonstration mode
"demo": [],
"js": [],

View File

@@ -1 +1,2 @@
from . import custom_portal
from . import custom_portal

View File

@@ -1,29 +1,21 @@
# Copyright 2020 Lokavaluto ()
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.http import request, route
from odoo.addons.portal.controllers.portal import CustomerPortal
class CustomCustomerPortal(CustomerPortal):
@route(["/my/account"], type="http", auth="user", website=True)
def account(self, redirect=None, **post):
self.OPTIONAL_BILLING_FIELDS.append(
"average_acceptable_time"
) # unecessary save in res partner, but necessary to avoid error on form post
response = super().account(redirect, **post)
self.OPTIONAL_BILLING_FIELDS.append("average_acceptable_time") #unecessary save in res partner, but necessary to avoid error on form post
response = super(CustomCustomerPortal, self).account(redirect, **post)
if post and request.httprequest.method == "POST":
error, error_message = self.details_form_validate(post)
if not error:
# sudo: a portal user has no write access at all on
# project.project, and no read access on its default project
# unless it follows it
project_sudo = request.env.user.sudo().default_project_id
if project_sudo and post.get("average_acceptable_time"):
project_sudo.average_acceptable_time = post[
"average_acceptable_time"
]
user = request.env.user
if user.default_project_id and post["average_acceptable_time"]:
user.default_project_id.average_acceptable_time = post["average_acceptable_time"]
return response

View File

@@ -1,2 +1,3 @@
from . import project_project
from . import res_partner
from . import res_partner

View File

@@ -1,7 +1,10 @@
from odoo import fields, models
from odoo import models, fields, _, api
class Project(models.Model):
_inherit = "project.project"
average_acceptable_time = fields.Float("Average acceptable time")
average_acceptable_time = fields.Float('Average acceptable time')

View File

@@ -1,9 +1,10 @@
from odoo import fields, models
from odoo import models, fields, _, api
class ResPartner(models.Model):
_inherit = "res.partner"
average_acceptable_time = fields.Float(
"Average acceptable time"
) # not used, but necessary to post custom field from /my/account
average_acceptable_time = fields.Float('Average acceptable time') # not used, but necessary to post custom field from /my/account

View File

@@ -1,50 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Template /my/home -->
<template
id="portal_my_home_average_acceptable_time"
name="Portal My Home: Average acceptable time"
inherit_id="portal.side_content"
priority="40"
>
<template id="portal_my_home_average_acceptable_time" name="Portal My Home: Average acceptable time" inherit_id="portal.side_content" priority="40">
<xpath expr="//div[@class='o_portal_my_details']" position="inside">
<!-- sudo: a portal user cannot read its own default project
unless it follows it (project.project_project_rule_portal) -->
<t
t-set="default_project_sudo"
t-value="user_id.sudo().default_project_id"
/>
<t t-if="default_project_sudo">
<hr class="mt-1 mb-0" />
<b>Average acceptable time</b>: <t
t-esc="default_project_sudo.average_acceptable_time"
t-options="{'widget': 'float_time'}"
/>
</t>
<hr class="mt-1 mb-0"/>
<b>Average acceptable time</b>: <t t-esc="user_id.default_project_id.average_acceptable_time" t-options="{'widget': 'float_time'}"/>
</xpath>
</template>
<!-- Template /my/account -->
<template
id="portal_my_details_average_acceptable_time"
name="Portal My details: Average acceptable time"
inherit_id="portal.portal_my_details_fields"
>
<template id="portal_my_details_average_acceptable_time" name="Portal My details: Average acceptable time" inherit_id="portal.portal_my_details_fields">
<xpath expr="//input[@name='csrf_token']/.." position="inside">
<div
t-attf-class="form-group #{error.get('average_acceptable_time') and 'o_has_error' or ''} col-xl-6"
>
<label
class="col-form-label"
for="average_acceptable_time"
>Average acceptable time (h)</label>
<div t-attf-class="form-group #{error.get('average_acceptable_time') and 'o_has_error' or ''} col-xl-6">
<label class="col-form-label" for="average_acceptable_time">Average acceptable time (h)</label>
<input
type="text"
name="average_acceptable_time"
t-attf-class="form-control #{error.get('average_acceptable_time') and 'is-invalid' or ''}"
t-att-value="average_acceptable_time or user_id.sudo().default_project_id.average_acceptable_time"
t-att-value="average_acceptable_time or user_id.default_project_id.average_acceptable_time"
t-options="{'widget': 'float_time'}"
/>
/>
</div>
</xpath>
</template>

View File

@@ -1,44 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!--Project KANBAN -->
<record
id="project_project_view_kanban_inherit_average_acceptable_time"
model="ir.ui.view"
>
<field
name="name"
>project.project.kanban.inherit.average.acceptable.time</field>
<record id="project_project_view_kanban_inherit_average_acceptable_time" model="ir.ui.view">
<field name="name">project.project.kanban.inherit.average.acceptable.time</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.view_project_kanban" />
<field name="inherit_id" ref="project.view_project_kanban"/>
<field name="priority">999</field>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('o_project_kanban_main')]" position="inside">
<div class="o_project_kanban_boxes">
<span>Average acceptable time:<![CDATA[&nbsp;]]></span>
<field name="average_acceptable_time" widget="float_time" />
<field name="average_acceptable_time" widget="float_time"/>
</div>
</xpath>
</field>
</record>
<!-- Project FORM -->
<record
id="project_project_form_inherit_average_acceptable_time"
model="ir.ui.view"
>
<record id="project_project_form_inherit_average_acceptable_time" model="ir.ui.view">
<field name="name">project.project.form.inherit.average.acceptable.time</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.edit_project" />
<field name="inherit_id" ref="project.edit_project"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='extra_settings']" position="inside">
<div
class="o_settings_average_acceptable_time"
style="margin-top:10px;"
>
<div class="o_settings_average_acceptable_time" style="margin-top:10px;">
<!-- <span>Average acceptable time:<![CDATA[&nbsp;]]></span> -->
<label for="average_acceptable_time" />
<field name="average_acceptable_time" widget="float_time" />
<label for="average_acceptable_time"/>
<field name="average_acceptable_time" widget="float_time"/>
</div>
</xpath>
</field>

View File

@@ -1,60 +0,0 @@
# Project Link From Invoice
![License: AGPL-3](https://img.shields.io/badge/licence-AGPL--3-blue.svg)
Easily access your projects directly from invoices:
- Displays project name(s) in the invoice list view.
- Adds a smart button on invoice form view to open the related project(s).
## Installation
This module depends on:
- `project` (Odoo core)
- `account` (Odoo core)
- `sale_project` (Odoo core)
Use Odoo's normal procedure to install add-ons.
## Configuration
No specific configuration is required.
## Usage
Open an invoice that is linked to a sale order. If the sale order has tasks
in a project, a "Project(s)" button appears in the header of the invoice form.
Clicking it opens the related project(s) in form or list view.
The invoice list view also displays the project name(s) in a dedicated column.
## Known issues / Roadmap
None yet.
## Bug Tracker
Bugs are tracked on [our issues website](https://github.com/elabore-coop/project-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
### Images
- Elabore: [Icon](https://elabore.coop/web/image/res.company/1/logo)
### 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.

View File

@@ -1,2 +0,0 @@
from . import models

View File

@@ -1,34 +0,0 @@
# Copyright 2022 Stéphan Sainléger (Elabore)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "project_link_from_invoice",
"version": "18.0.1.0.0",
"author": "Elabore",
"website": "https://git.elabore.coop/elabore/project-tools",
"maintainer": "Clément Thomas",
"license": "AGPL-3",
"category": "Tools",
"summary": "Add link from invoice to project",
# any module necessary for this one to work correctly
"depends": [
"base",
"project",
"account",
"sale_project",
],
"external_dependencies": {
"python": [],
},
# always loaded
"data": [
"views/account_move_view.xml",
],
# only loaded in demonstration mode
"demo": [],
"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,
}

View File

@@ -1,58 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * project_link_from_invoice
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-04-27 12:47+0000\n"
"PO-Revision-Date: 2023-04-27 12:47+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: project_link_from_invoice
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move__display_name
msgid "Display Name"
msgstr "Nom affiché"
#. module: project_link_from_invoice
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move__id
msgid "ID"
msgstr ""
#. module: project_link_from_invoice
#: model:ir.model,name:project_link_from_invoice.model_account_move
msgid "Journal Entry"
msgstr "Pièce comptable"
#. module: project_link_from_invoice
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move____last_update
msgid "Last Modified on"
msgstr "Dernière modification le"
#. module: project_link_from_invoice
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_bank_statement_line__project_ids
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move__project_ids
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_payment__project_ids
msgid "Project"
msgstr "Projet"
#. module: project_link_from_invoice
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_bank_statement_line__project_count
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move__project_count
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_payment__project_count
msgid "Project Count"
msgstr "Numbre de projets"
#. module: project_link_from_invoice
#: model_terms:ir.ui.view,arch_db:project_link_from_invoice.view_move_form_project_link_from_invoice_inherit
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_bank_statement_line__projects_name
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move__projects_name
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_payment__projects_name
msgid "Project(s)"
msgstr "Projet(s)"

View File

@@ -1,2 +0,0 @@
from . import account_move

View File

@@ -1,49 +0,0 @@
# Copyright 2022 Stéphan Sainléger (Elabore)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountMove(models.Model):
_inherit = "account.move"
project_ids = fields.Many2many(
"project.project", string="Projects", compute="_get_related_project_ids"
)
project_count = fields.Integer("Project Count", compute="_get_related_project_ids")
projects_name = fields.Char("Project(s)", compute="_get_related_project_ids")
def action_open_projects(self):
"""Open related projects, in form or list view depending on project numbers."""
project_ids = self.project_ids.ids
action = self.env["ir.actions.actions"]._for_xml_id(
"project.open_view_project_all"
)
if self.project_count == 1:
action["res_id"] = project_ids[0]
action["views"] = [[False, "form"]]
else:
action["views"] = [[False, "list"], [False, "form"]]
action["domain"] = [("id", "in", project_ids)]
del action["target"] # to display breadcrumbs
return action
@api.depends("line_ids.sale_line_ids")
def _get_related_project_ids(self):
for move in self:
projects = self.env["project.task"].search(
[
(
"sale_order_id",
"in",
move.line_ids.sale_line_ids.order_id.ids,
)
]
).project_id
move.project_ids = projects.ids
move.projects_name = " ; ".join([p.name for p in projects])
move.project_count = len(projects)

View File

@@ -1,4 +0,0 @@
# Copyright 2022 Stéphan Sainléger (Elabore)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_account_move

View File

@@ -1,193 +0,0 @@
# Copyright 2022 Stéphan Sainléger (Elabore)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import Command
from odoo.tests import tagged
from odoo.addons.sale_project.tests.common import TestSaleProjectCommon
@tagged("post_install", "-at_install")
class TestProjectLinkFromInvoice(TestSaleProjectCommon):
"""Tests for the project_link_from_invoice module.
Verifies that invoices correctly expose related project(s) via
the computed fields project_ids, project_count and projects_name.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
# Service product that creates a task in a new project on SO confirmation
cls.service_product = cls.env["product.product"].create(
{
"name": "Service with Project",
"type": "service",
"invoice_policy": "order",
"service_tracking": "task_in_project",
}
)
# Partner used for orders and invoices
cls.partner = cls.env["res.partner"].create({"name": "Test Partner"})
def _confirm_sale_order(self, product, qty=1):
"""Create and confirm a sale order with one service line."""
so = self.env["sale.order"].create(
{
"partner_id": self.partner.id,
"order_line": [
Command.create(
{
"product_id": product.id,
"product_uom_qty": qty,
}
)
],
}
)
so.action_confirm()
return so
def _invoice_sale_order(self, sale_order):
"""Create and post an invoice from a confirmed sale order."""
invoice_wizard = (
self.env["sale.advance.payment.inv"]
.with_context(
active_model="sale.order",
active_ids=sale_order.ids,
active_id=sale_order.id,
)
.create({"advance_payment_method": "delivered"})
)
invoices = invoice_wizard._create_invoices(sale_order)
invoices.action_post()
return invoices
# ------------------------------------------------------------------
# Tests
# ------------------------------------------------------------------
def test_no_project_on_unrelated_invoice(self):
"""An invoice not linked to any sale order has no related project."""
invoice = self.env["account.move"].create(
{
"move_type": "out_invoice",
"partner_id": self.partner.id,
"invoice_line_ids": [
Command.create(
{
"name": "Manual line",
"quantity": 1,
"price_unit": 100.0,
}
)
],
}
)
self.assertEqual(invoice.project_count, 0)
self.assertFalse(invoice.project_ids)
self.assertEqual(invoice.projects_name, "")
def test_single_project_linked_to_invoice(self):
"""An invoice from a SO that created a task has one related project."""
so = self._confirm_sale_order(self.service_product)
# The SO must have generated a task in a project
task = so.tasks_ids[:1]
self.assertTrue(task, "Sale order confirmation should have created a task")
project = task.project_id
self.assertTrue(project, "Task should belong to a project")
invoice = self._invoice_sale_order(so)
self.assertEqual(len(invoice), 1)
self.assertEqual(invoice.project_count, 1)
self.assertIn(project, invoice.project_ids)
self.assertIn(project.name, invoice.projects_name)
def test_multiple_projects_linked_to_invoice(self):
"""An invoice from two SOs with different projects shows both projects."""
so1 = self._confirm_sale_order(self.service_product)
so2 = self._confirm_sale_order(self.service_product)
project1 = so1.tasks_ids[:1].project_id
project2 = so2.tasks_ids[:1].project_id
self.assertTrue(project1 and project2)
# Ensure the two SOs created different projects
self.assertNotEqual(project1, project2)
# Create a single invoice covering both sale orders
invoice_wizard = (
self.env["sale.advance.payment.inv"]
.with_context(
active_model="sale.order",
active_ids=(so1 + so2).ids,
)
.create({"advance_payment_method": "delivered"})
)
invoices = invoice_wizard._create_invoices(so1 + so2)
invoices.action_post()
# Find the invoice that covers both SO lines
combined_invoice = invoices[:1]
self.assertGreaterEqual(combined_invoice.project_count, 2)
self.assertIn(project1, combined_invoice.project_ids)
self.assertIn(project2, combined_invoice.project_ids)
def test_action_open_projects_single(self):
"""action_open_projects returns a form view when there is one project."""
so = self._confirm_sale_order(self.service_product)
invoice = self._invoice_sale_order(so)
self.assertEqual(invoice.project_count, 1)
action = invoice.action_open_projects()
self.assertEqual(action["res_id"], invoice.project_ids.id)
view_types = [v[1] for v in action["views"]]
self.assertIn("form", view_types)
self.assertNotIn("list", view_types)
def test_action_open_projects_multiple(self):
"""action_open_projects returns a list view when there are multiple projects."""
so1 = self._confirm_sale_order(self.service_product)
so2 = self._confirm_sale_order(self.service_product)
invoice_wizard = (
self.env["sale.advance.payment.inv"]
.with_context(
active_model="sale.order",
active_ids=(so1 + so2).ids,
)
.create({"advance_payment_method": "delivered"})
)
invoices = invoice_wizard._create_invoices(so1 + so2)
invoices.action_post()
combined_invoice = invoices[:1]
if combined_invoice.project_count < 2:
self.skipTest("Combined invoice does not span multiple projects")
action = combined_invoice.action_open_projects()
view_types = [v[1] for v in action["views"]]
self.assertIn("list", view_types)
self.assertIn("form", view_types)
self.assertNotIn("target", action)
def test_projects_name_separator(self):
"""projects_name joins multiple project names with ' ; '."""
so1 = self._confirm_sale_order(self.service_product)
so2 = self._confirm_sale_order(self.service_product)
invoice_wizard = (
self.env["sale.advance.payment.inv"]
.with_context(
active_model="sale.order",
active_ids=(so1 + so2).ids,
)
.create({"advance_payment_method": "delivered"})
)
invoices = invoice_wizard._create_invoices(so1 + so2)
invoices.action_post()
combined_invoice = invoices[:1]
if combined_invoice.project_count >= 2:
self.assertIn(" ; ", combined_invoice.projects_name)

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_move_form_project_link_from_invoice_inherit" model="ir.ui.view">
<field name="name">account.move.form.project.link</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form" />
<field name="priority" eval="99" />
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button
name="action_open_projects"
class="oe_stat_button"
icon="fa-puzzle-piece"
type="object"
invisible="project_count == 0"
>
<field name="project_count" widget="statinfo" string="Project(s)" />
</button>
</xpath>
</field>
</record>
<record id="view_invoice_tree_project_link_from_invoice_inherit" model="ir.ui.view">
<field name="name">account.invoice.list.project.link</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_invoice_tree"/>
<field name="arch" type="xml">
<xpath expr="//list/field[@name='invoice_date']" position="before">
<field name="projects_name" />
</xpath>
</field>
</record>
</odoo>

View File

@@ -5,7 +5,7 @@
"name": "project_only_current_project_tasks_as_parent",
"version": "18.0.1.0.0",
"author": "Elabore",
"website": "https://git.elabore.coop/elabore/project-tools",
"website": "https://elabore.coop",
"maintainer": "Laetitia Da Costa",
"license": "AGPL-3",
"category": "Project",

View File

@@ -1,17 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record model="ir.ui.view" id="view_tasks_form2_parent_project_inherited">
<field name="name">view.tasks.form2.parent.project.inherited</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2" />
<field name="arch" type="xml">
<xpath
expr="//page[@name='extra_info']//field[@name='parent_id']"
position="attributes"
>
<attribute
name="domain"
>[('project_id','=', project_id), ('stage_id.fold', '=', False)]</attribute>
<xpath expr="//page[@name='extra_info']//field[@name='parent_id']" position="attributes">
<attribute name="domain">[('project_id','=', project_id), ('stage_id.fold', '=', False)]</attribute>
</xpath>
</field>
</record>

View File

@@ -5,7 +5,7 @@
"name": "project_request_data",
"version": "18.0.1.0.0",
"author": "Elabore",
"website": "https://git.elabore.coop/elabore/project-tools",
"website": "https://elabore.coop",
"maintainer": "Stéphan Sainléger",
"license": "AGPL-3",
"category": "Project",
@@ -89,4 +89,4 @@ This module is maintained by Elabore.
# and independently installed. Used for synergetic or glue modules.
"auto_install": False,
"application": False,
}
}

View File

@@ -1,3 +1,3 @@
from . import task_service
from . import request_type
from . import project_task
from . import project_task

View File

@@ -1,8 +1,9 @@
from odoo import fields, models
from odoo import models, fields
class Task(models.Model):
_inherit = "project.task"
service_id = fields.Many2one("task.service", string="Service")
request_type_id = fields.Many2one("request.type", string="Request Type")
service_id = fields.Many2one('task.service', string='Service')
request_type_id = fields.Many2one('request.type', string='Request Type')

View File

@@ -1,9 +1,9 @@
from odoo import fields, models
from odoo import models, fields
class RequestType(models.Model):
_name = "request.type"
_description = "Request Type"
name = fields.Char("name", required=True)
sequence = fields.Integer()
name = fields.Char('name', required=True)
sequence = fields.Integer()

View File

@@ -1,9 +1,9 @@
from odoo import fields, models
from odoo import models, fields
class TaskService(models.Model):
_name = "task.service"
_description = "Task service"
name = fields.Char("name", required=True)
sequence = fields.Integer()
name = fields.Char('name', required=True)
sequence = fields.Integer()

View File

@@ -2,4 +2,4 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_task_service_user,project.task_service.user,model_task_service,,1,0,0,0
access_task_service_manager,project.task_service.manager,model_task_service,project.group_project_manager,1,1,1,1
access_request_type_user,project.request_type.user,model_request_type,,1,0,0,0
access_request_type_manager,project.request_type.manager,model_request_type,project.group_project_manager,1,1,1,1
access_request_type_manager,project.request_type.manager,model_request_type,project.group_project_manager,1,1,1,1
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 access_task_service_user project.task_service.user model_task_service 1 0 0 0
3 access_task_service_manager project.task_service.manager model_task_service project.group_project_manager 1 1 1 1
4 access_request_type_user project.request_type.user model_request_type 1 0 0 0
5 access_request_type_manager project.request_type.manager model_request_type project.group_project_manager 1 1 1 1

View File

@@ -1,15 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template
id="portal_my_task_request_data"
name="My Task: Request Data"
inherit_id="project.portal_my_task"
priority="40"
>
<xpath
expr="//div[@id='card_body']/div[hasclass('row','mb-4','container')]"
position="after"
>
<template id="portal_my_task_request_data" name="My Task: Request Data" inherit_id="project.portal_my_task" priority="40">
<xpath expr="//div[@id='card_body']/div[hasclass('row','mb-4','container')]" position="after">
<div id="request_data" class="row mb-4">
<div class="col-12 col-md-6" t-if="task.service_id">
<strong>Service:</strong>
@@ -22,4 +14,4 @@
</div>
</xpath>
</template>
</odoo>
</odoo>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_task_form2_request_data" model="ir.ui.view">
<field name="name">project.task.form.request.data</field>
@@ -18,19 +18,12 @@
<field name="model">project.task</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="before">
<div style="font-size:11px; margin-bottom: 8px;"><field
name="create_date"
widget="date"
/></div>
<div style="font-size:11px; margin-bottom: 8px;"><field name="create_date" widget="date"/></div>
</xpath>
<xpath expr="//field[@name='tag_ids']" position="before">
<span style="background:lightblue; font-size:11px"><field
name="service_id"
/></span>
<span style="background:lightsteelblue; font-size:11px"><field
name="request_type_id"
/></span>
<span style="background:lightblue; font-size:11px"><field name="service_id"/></span>
<span style="background:lightsteelblue; font-size:11px"><field name="request_type_id"/></span>
</xpath>
</field>
</record>
</odoo>
</odoo>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="request_type_view_tree" model="ir.ui.view">
@@ -18,12 +18,6 @@
<field name="view_mode">list</field>
</record>
<menuitem
id="menu_request_types"
action="act_request_types_list"
parent="project.menu_project_config"
sequence="99"
name="Request Types"
/>
<menuitem id="menu_request_types" action="act_request_types_list" parent="project.menu_project_config" sequence="99" name="Request Types" />
</odoo>
</odoo>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="task_service_view_tree" model="ir.ui.view">
@@ -18,12 +18,6 @@
<field name="view_mode">list</field>
</record>
<menuitem
id="menu_task_services"
action="act_task_services_list"
parent="project.menu_project_config"
sequence="99"
name="Task Services"
/>
<menuitem id="menu_task_services" action="act_task_services_list" parent="project.menu_project_config" sequence="99" name="Task Services" />
</odoo>
</odoo>

View File

@@ -5,15 +5,19 @@
"name": "Project task billable hours",
"version": "18.0.1.0.0",
"author": "Elabore",
"website": "https://git.elabore.coop/elabore/project-tools",
"website": "https://github.com/elabore-coop/project-tools",
"maintainer": "Laetitia Da Costa",
"license": "AGPL-3",
"category": "Tools",
"summary": "In task kanban view, display remaining billable hours instead of remaining hours",
"description": "",
"depends": ["project", "hr_timesheet", "project_working_time_task_portal"],
"depends": [
"project",
"hr_timesheet",
"project_working_time_task_portal"
],
"data": [
"views/project_task_views.xml",
"views/project_task_views.xml",
],
"installable": True,
"application": False,

View File

@@ -1,36 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Replace remaining hours by billable remaining hours in kanban view -->
<record id="view_task_kanban_billable_remaining_hours_inherit" model="ir.ui.view">
<field name="name">project.task.kanban.billable.remaining.hours.inherit</field>
<field name="model">project.task</field>
<field
name="inherit_id"
ref="hr_timesheet.view_task_kanban_inherited_progress"
/>
<field name="inherit_id" ref="hr_timesheet.view_task_kanban_inherited_progress"/>
<field name="arch" type="xml">
<xpath expr="//t[@name='allocated_hours']" position="replace">
<t t-set="badge" t-value="" />
<t
t-set="badge"
t-value="'badge-warning'"
t-if="record.billable_progress &gt;= 80 and record.billable_progress &lt;= 100"
/>
<t
t-set="badge"
t-value="'badge-danger'"
t-if="record.billable_remaining_hours.raw_value &lt; 0"
/>
<t
t-set="title"
t-value="'Remaining days'"
t-if="record.encode_uom_in_days.raw_value"
/>
<t t-set="title" t-value="'Billable remaining hours'" t-else="" />
<div
t-attf-class="oe_kanban_align badge {{ badge }}"
t-att-title="title"
>
<t t-set="badge" t-value=""/>
<t t-set="badge" t-value="'badge-warning'" t-if="record.billable_progress &gt;= 80 and record.billable_progress &lt;= 100"/>
<t t-set="badge" t-value="'badge-danger'" t-if="record.billable_remaining_hours.raw_value &lt; 0"/>
<t t-set="title" t-value="'Remaining days'" t-if="record.encode_uom_in_days.raw_value"/>
<t t-set="title" t-value="'Billable remaining hours'" t-else=""/>
<div t-attf-class="oe_kanban_align badge {{ badge }}" t-att-title="title">
<field name="billable_remaining_hours" widget="timesheet_uom" />
</div>
</xpath>

View File

@@ -1 +1,2 @@
from . import models

View File

@@ -5,7 +5,7 @@
"name": "project_user_default_project",
"version": "18.0.1.0.0",
"author": "Elabore",
"website": "https://git.elabore.coop/elabore/project-tools",
"website": "https://elabore.coop",
"maintainer": "Stéphan Sainléger",
"license": "AGPL-3",
"category": "Tools",
@@ -62,8 +62,7 @@ This module is maintained by Elabore.
""",
# any module necessary for this one to work correctly
"depends": [
"base",
"project",
"base", "project",
],
"qweb": [
# "static/src/xml/*.xml",
@@ -84,4 +83,4 @@ This module is maintained by Elabore.
# and independently installed. Used for synergetic or glue modules.
"auto_install": False,
"application": False,
}
}

View File

@@ -1 +1 @@
from . import res_users
from . import res_users

View File

@@ -1,7 +1,6 @@
from odoo import fields, models
from odoo import _, api, fields, models
class Users(models.Model):
_inherit = "res.users"
default_project_id = fields.Many2one("project.project", string="Default Project")
default_project_id = fields.Many2one('project.project', string='Default Project')

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_users_form_inherit_default_project" model="ir.ui.view">
<field name="name">view.users.form.inherit.default.project</field>
@@ -10,4 +10,4 @@
</xpath>
</field>
</record>
</odoo>
</odoo>

View File

@@ -1,2 +1,2 @@
*.*~
*pyc
*pyc

View File

@@ -1 +1 @@
from . import models
from . import models

View File

@@ -3,9 +3,9 @@
{
"name": "project_working_time_task_portal",
"version": "18.0.1.1.0",
"version": "18.0.1.0.0",
"author": "Elabore",
"website": "https://git.elabore.coop/elabore/project-tools",
"website": "https://elabore.coop",
"maintainer": "Boris Gallet",
"license": "AGPL-3",
"category": "tools",
@@ -23,10 +23,9 @@
},
# always loaded
"data": [
# views/hr_timesheet_portal.xml",
#views/hr_timesheet_portal.xml",
"views/hr_timesheet_view_task_form2.xml",
"views/portal_tasks_list.xml",
"views/portal_my_task_with_subtasks.xml"
"views/portal_tasks_list.xml"
],
"assets": {
"web.assets_frontend": [

View File

@@ -4,10 +4,9 @@
from odoo import models
from odoo.osv import expression
class AccountAnalyticLine(models.Model):
_inherit = "account.analytic.line"
_inherit = 'account.analytic.line'
def _timesheet_get_portal_domain(self):
domain = super()._timesheet_get_portal_domain()
return expression.AND([domain, [("exclude_from_sale_order", "=", False)]])
return expression.AND([domain, [('exclude_from_sale_order', '=', False)]])

View File

@@ -1,7 +1,9 @@
/**
* Main
* Main
*/
.oe-project_working_time_task_portal.progressbar {
height: 20px;
background-color: #7c7cac; /* Couleur de la barre de progression */
@@ -9,3 +11,4 @@
color: white; /* Couleur du texte dans la barre de progression */
line-height: 20px; /* Aligner le texte verticalement */
}

View File

@@ -1,41 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template
id="portal_my_task_allocated_hours"
name="My Task: planned hours"
inherit_id="hr_timesheet.portal_my_task"
priority="40"
>
<xpath
expr="//t[@t-call='hr_timesheet.portal_timesheet_table']"
position="before"
>
<template id="portal_my_task_allocated_hours" name="My Task: planned hours" inherit_id="hr_timesheet.portal_my_task" priority="40">
<xpath expr="//t[@t-call='hr_timesheet.portal_timesheet_table']" position="before">
<div class="row">
<div class="col-12 col-md-6 pb-2">
<span style="font-weight:bold;">Planned Hours: </span>
<span
t-field="task.allocated_hours"
t-options="{&quot;widget&quot;: &quot;timesheet_uom_no_toggle&quot;}"
/>
<span t-field="task.allocated_hours" t-options="{&quot;widget&quot;: &quot;timesheet_uom_no_toggle&quot;}"/>
<t t-if="task.allow_subtasks and task.subtask_count > 0">
<div>
<strong>Subtask Planned Hours: </strong>
<span
t-field="task.subtask_allocated_hours"
t-options="{&quot;widget&quot;: &quot;timesheet_uom_no_toggle&quot;}"
/>
<span t-field="task.subtask_allocated_hours" t-options="{&quot;widget&quot;: &quot;timesheet_uom_no_toggle&quot;}"/>
</div>
</t>
</div>
<t t-if="task.billable_progress > 0">
<div class="col-12 col-md-6 pb-2">
<strong>Progress: </strong>
<t t-set="progress" t-value="task.billable_progress" />
<div
class="oe-project_working_time_task_portal progressbar"
t-attf-style="width: #{progress}%;"
>
<t t-esc="progress" />%
<t t-set="progress" t-value="task.billable_progress"/>
<div class="oe-project_working_time_task_portal progressbar" t-attf-style="width: #{progress}%;">
<t t-esc="progress"/>%
</div>
</div>
</t>
@@ -43,25 +26,15 @@
</xpath>
</template>
<template
id="portal_my_task_remaining_hours"
name="My Task: remaining hours"
inherit_id="hr_timesheet.portal_timesheet_table"
priority="40"
>
<template id="portal_my_task_remaining_hours" name="My Task: remaining hours" inherit_id="hr_timesheet.portal_timesheet_table" priority="40">
<xpath expr="//tr[@t-foreach='timesheets']" position="attributes">
<attribute name="t-if">not timesheet.exclude_from_sale_order</attribute>
</xpath>
<xpath expr="//tfoot/tr/th[last()]" position="replace">
<th class="text-end">
<div
t-esc="round(sum(timesheets.filtered(lambda t: not t.exclude_from_sale_order).mapped('unit_amount')), 2)"
t-options="{&quot;widget&quot;: &quot;float_time&quot;}"
/>
<div t-if="task.allocated_hours > 0">Remaining Hours: <span
t-field="task.billable_remaining_hours"
t-options="{&quot;widget&quot;: &quot;float_time&quot;}"
/></div>
<div t-esc="round(sum(timesheets.filtered(lambda t: not t.exclude_from_sale_order).mapped('unit_amount')), 2)"
t-options="{&quot;widget&quot;: &quot;float_time&quot;}"/>
<div t-if="task.allocated_hours > 0">Remaining Hours: <span t-field="task.billable_remaining_hours" t-options="{&quot;widget&quot;: &quot;float_time&quot;}"/></div>
</th>
</xpath>
</template>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="project_working_time_view_task_form" model="ir.ui.view">
@@ -9,165 +9,54 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='effective_hours']" position="after">
<span>
<label
class="font-weight-bold"
for="billable_effective_hours"
string="Billable Effective Hours"
invisible="non_billable_effective_hours == 0.0 or encode_uom_in_days"
/>
<label
class="font-weight-bold"
for="billable_effective_hours"
string="Billable Effective Days"
invisible="non_billable_effective_hours == 0.0 or not encode_uom_in_days"
/>
<label class="font-weight-bold" for="billable_effective_hours" string="Billable Effective Hours" invisible="non_billable_effective_hours == 0.0 or encode_uom_in_days"/>
<label class="font-weight-bold" for="billable_effective_hours" string="Billable Effective Days" invisible="non_billable_effective_hours == 0.0 or not encode_uom_in_days"/>
</span>
<field
name="billable_effective_hours"
widget="timesheet_uom"
class="mt-2"
invisible="non_billable_effective_hours == 0.0"
nolabel="1"
/>
<field name="billable_effective_hours" widget="timesheet_uom" class="mt-2" invisible="non_billable_effective_hours == 0.0" nolabel="1"/>
<span>
<label
class="font-weight-bold"
for="non_billable_effective_hours"
string="Non Billable Effective Hours"
invisible="non_billable_effective_hours == 0.0 or encode_uom_in_days"
/>
<label
class="font-weight-bold"
for="non_billable_effective_hours"
string="Non Billable Effective Days"
invisible="non_billable_effective_hours == 0.0 or not encode_uom_in_days"
/>
<label class="font-weight-bold" for="non_billable_effective_hours" string="Non Billable Effective Hours" invisible="non_billable_effective_hours == 0.0 or encode_uom_in_days"/>
<label class="font-weight-bold" for="non_billable_effective_hours" string="Non Billable Effective Days" invisible="non_billable_effective_hours == 0.0 or not encode_uom_in_days"/>
</span>
<field
name="non_billable_effective_hours"
widget="timesheet_uom"
class="mt-2"
invisible="non_billable_effective_hours == 0.0"
nolabel="1"
/>
<field name="non_billable_effective_hours" widget="timesheet_uom" class="mt-2" invisible="non_billable_effective_hours == 0.0" nolabel="1"/>
</xpath>
<xpath expr="//field[@name='subtask_effective_hours']" position="after">
<span>
<label
class="font-weight-bold"
for="subtask_billable_effective_hours"
string="Subtask Billable Effective Hours"
invisible="subtask_effective_hours == 0.0 or encode_uom_in_days"
/>
<label
class="font-weight-bold"
for="subtask_billable_effective_hours"
string="Subtask Billable Effective Days"
invisible="subtask_effective_hours == 0.0 or not encode_uom_in_days"
/>
<label class="font-weight-bold" for="subtask_billable_effective_hours" string="Subtask Billable Effective Hours" invisible="subtask_effective_hours == 0.0 or encode_uom_in_days"/>
<label class="font-weight-bold" for="subtask_billable_effective_hours" string="Subtask Billable Effective Days" invisible="subtask_effective_hours == 0.0 or not encode_uom_in_days"/>
</span>
<field
name="subtask_billable_effective_hours"
widget="timesheet_uom"
class="mt-2"
invisible="subtask_effective_hours == 0.0"
nolabel="1"
/>
<field name="subtask_billable_effective_hours" widget="timesheet_uom" class="mt-2" invisible="subtask_effective_hours == 0.0" nolabel="1"/>
<span>
<label
class="font-weight-bold"
for="subtask_non_billable_effective_hours"
string="Subtask Non Billable Effective Hours"
invisible="subtask_effective_hours == 0.0 or encode_uom_in_days"
/>
<label
class="font-weight-bold"
for="subtask_non_billable_effective_hours"
string="Subtask Non Billable Effective Days"
invisible="subtask_effective_hours == 0.0 or not encode_uom_in_days"
/>
<label class="font-weight-bold" for="subtask_non_billable_effective_hours" string="Subtask Non Billable Effective Hours" invisible="subtask_effective_hours == 0.0 or encode_uom_in_days"/>
<label class="font-weight-bold" for="subtask_non_billable_effective_hours" string="Subtask Non Billable Effective Days" invisible="subtask_effective_hours == 0.0 or not encode_uom_in_days"/>
</span>
<field
name="subtask_non_billable_effective_hours"
widget="timesheet_uom"
class="mt-2"
invisible="subtask_effective_hours == 0.0"
nolabel="1"
/>
<field name="subtask_non_billable_effective_hours" widget="timesheet_uom" class="mt-2" invisible="subtask_effective_hours == 0.0" nolabel="1"/>
</xpath>
<xpath expr="//field[@name='total_hours_spent']" position="after">
<span invisible="subtask_effective_hours == 0.0">
<label
for="total_billable_hours_spent"
string="Total Billable Hours"
invisible="encode_uom_in_days"
/>
<label
for="total_billable_hours_spent"
string="Total Billable Days"
invisible="not encode_uom_in_days"
/>
<label for="total_billable_hours_spent" string="Total Billable Hours"
invisible="encode_uom_in_days"/>
<label for="total_billable_hours_spent" string="Total Billable Days"
invisible="not encode_uom_in_days"/>
</span>
<field
name="total_billable_hours_spent"
widget="timesheet_uom"
nolabel="1"
invisible="subtask_effective_hours == 0.0"
/>
<field name="total_billable_hours_spent" widget="timesheet_uom" nolabel="1"
invisible="subtask_effective_hours == 0.0"/>
<span invisible="subtask_effective_hours == 0.0">
<label
for="total_non_billable_hours_spent"
string="Total Non Billable Hours"
invisible="encode_uom_in_days"
/>
<label
for="total_non_billable_hours_spent"
string="Total Non Billable Days"
invisible="not encode_uom_in_days"
/>
<label for="total_non_billable_hours_spent" string="Total Non Billable Hours"
invisible="encode_uom_in_days"/>
<label for="total_non_billable_hours_spent" string="Total Non Billable Days"
invisible="not encode_uom_in_days"/>
</span>
<field
name="total_non_billable_hours_spent"
widget="timesheet_uom"
nolabel="1"
invisible="subtask_effective_hours == 0.0"
/>
<field name="total_non_billable_hours_spent" widget="timesheet_uom" nolabel="1"
invisible="subtask_effective_hours == 0.0"/>
</xpath>
<xpath expr="//field[@name='remaining_hours']" position="after">
<span>
<label
class="font-weight-bold text-danger"
for="billable_remaining_hours"
string="Billable Remaining Hours"
invisible="allocated_hours == 0.0 or encode_uom_in_days or billable_remaining_hours &gt;= 0"
/>
<label
class="font-weight-bold"
for="billable_remaining_hours"
string="Billable Remaining Hours"
invisible="allocated_hours == 0.0 or encode_uom_in_days or billable_remaining_hours &lt; 0"
/>
<label
class="font-weight-bold text-danger"
for="billable_remaining_hours"
string="Billable Remaining Days"
invisible="allocated_hours == 0.0 or not encode_uom_in_days or billable_remaining_hours &gt;= 0"
/>
<label
class="font-weight-bold"
for="billable_remaining_hours"
string="Billable Remaining Days"
invisible="allocated_hours == 0.0 or not encode_uom_in_days or billable_remaining_hours &lt; 0"
/>
<label class="font-weight-bold text-danger" for="billable_remaining_hours" string="Billable Remaining Hours" invisible="allocated_hours == 0.0 or encode_uom_in_days or billable_remaining_hours &gt;= 0"/>
<label class="font-weight-bold" for="billable_remaining_hours" string="Billable Remaining Hours" invisible="allocated_hours == 0.0 or encode_uom_in_days or billable_remaining_hours &lt; 0"/>
<label class="font-weight-bold text-danger" for="billable_remaining_hours" string="Billable Remaining Days" invisible="allocated_hours == 0.0 or not encode_uom_in_days or billable_remaining_hours &gt;= 0"/>
<label class="font-weight-bold" for="billable_remaining_hours" string="Billable Remaining Days" invisible="allocated_hours == 0.0 or not encode_uom_in_days or billable_remaining_hours &lt; 0"/>
</span>
<field
name="billable_remaining_hours"
widget="timesheet_uom"
class="oe_subtotal_footer_separator"
invisible="allocated_hours == 0.0"
decoration-danger="billable_remaining_hours &lt; 0"
nolabel="1"
/>
<field name="billable_remaining_hours" widget="timesheet_uom" class="oe_subtotal_footer_separator" invisible="allocated_hours == 0.0" decoration-danger="billable_remaining_hours &lt; 0" nolabel="1"/>
</xpath>
</field>
</record>

View File

@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="hr_timesheet_portal_timesheet_table_inherit"
inherit_id="hr_timesheet.portal_timesheet_table">
<xpath expr="//table[hasclass('o_portal_my_doc_table')]" position="after">
<t t-foreach="task.child_ids" t-as="subtask">
<t t-if="subtask.billable_effective_hours > 0">
<a t-attf-href="/my/tasks/#{subtask.id}">
<span t-att-title="subtask.name" t-field="subtask.name" />
</a>
<t t-set="timesheets" t-value="subtask.timesheet_ids" />
<table class="o_portal_my_doc_table table table-sm table-striped">
<thead>
<tr>
<th>Date</th>
<th>Employee</th>
<th>Description</th>
<th class="text-end" name="t_label">Time Spent</th>
</tr>
</thead>
<tr t-foreach="timesheets" t-as="timesheet" valign="middle">
<td>
<t t-esc="timesheet.date" t-options='{"widget": "date"}' />
</td>
<td t-attf-title="#{timesheet.employee_id.name}">
<t t-esc="timesheet.employee_id.name" />
</td>
<td>
<t t-esc="timesheet.name" />
</td>
<td class="text-end">
<span t-if="is_uom_day" t-esc="timesheet._get_timesheet_time_day()"
t-options='{"widget": "timesheet_uom"}' />
<span t-else="" t-field="timesheet.unit_amount"
t-options='{"widget": "float_time"}' />
</td>
</tr>
</table>
</t>
</t>
</xpath>
</template>
</odoo>

View File

@@ -1,32 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template
id="portal_tasks_list_inherit"
inherit_id="hr_timesheet.portal_tasks_list_inherit"
>
<xpath
expr="//thead//t[@t-if='not project or project.allow_timesheets']/th[@class='text-end']"
position="replace"
>
<th class="text-end">Billable hours</th>
<template id="portal_tasks_list_inherit" inherit_id="project.portal_tasks_list">
<xpath expr="//thead/tr/th[7]" position="replace">
<th t-if="not is_uom_day" class="text-end">Billable Hours</th>
</xpath>
<xpath
expr="//tbody//td[@t-if='not project or project.allow_timesheets']"
position="replace"
>
<td t-if="not project or project.allow_timesheets" class="text-end">
<t t-if="task.allow_timesheets and not is_uom_day">
<span
t-field="task.billable_effective_hours"
t-options='{"widget": "float_time"}'
/>
<t t-if="task.allocated_hours > 0"> / <span
t-field="task.allocated_hours"
t-options='{"widget": "float_time"}'
/>
<xpath expr="//tbody/t/tr/td[7]" position="replace">
<td class="text-end">
<t t-if="not is_uom_day">
<span t-field="task.billable_effective_hours"
t-options='{"widget": "float_time"}' />
<t t-if="task.allocated_hours > 0"> / <span t-field="task.allocated_hours"
t-options='{"widget": "float_time"}' />
</t>
</t>
</td>
</xpath>
</template>
</odoo>
</odoo>