From 1b1dafab022de463feca19d66f62f1da9a80efe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20-=20Le=20Filament?= <remi@le-filament.com> Date: Tue, 13 Feb 2024 18:30:23 +0100 Subject: [PATCH] [MIG] v16.0 --- .editorconfig | 20 ++ .eslintrc.yml | 188 ++++++++++++ .gitignore | 76 +++++ .pre-commit-config.yaml | 117 ++++++++ .pylintrc | 123 ++++++++ .pylintrc-mandatory | 98 ++++++ .ruff.toml | 30 ++ __init__.py | 3 - __manifest__.py | 22 +- controllers/__init__.py | 4 - .../__pycache__/__init__.cpython-36.pyc | Bin 185 -> 0 bytes controllers/__pycache__/main.cpython-36.pyc | Bin 4408 -> 0 bytes controllers/main.py | 125 -------- models/__init__.py | 7 +- models/product_template.py | 13 - models/project_overview.py | 283 ------------------ models/project_update.py | 31 ++ models/res_company.py | 9 +- models/res_config_settings.py | 11 +- models/sale_order.py | 185 +----------- models/sale_order_line.py | 24 ++ views/res_config_settings_view.xml | 23 +- views/sale_view.xml | 21 +- 23 files changed, 756 insertions(+), 657 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintrc.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .pylintrc create mode 100644 .pylintrc-mandatory create mode 100644 .ruff.toml delete mode 100644 controllers/__init__.py delete mode 100644 controllers/__pycache__/__init__.cpython-36.pyc delete mode 100644 controllers/__pycache__/main.cpython-36.pyc delete mode 100644 controllers/main.py delete mode 100644 models/product_template.py delete mode 100644 models/project_overview.py create mode 100644 models/project_update.py create mode 100644 models/sale_order_line.py diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bfd7ac5 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..fed88d7 --- /dev/null +++ b/.eslintrc.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d99361a --- /dev/null +++ b/.gitignore @@ -0,0 +1,76 @@ +# 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 +.copier-answers.yml + +# 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/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3efb4d9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,117 @@ +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/| + # 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/| + # 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: f71041f22b8cd68cf7c77b73a14ca8d8cd190a60 + hooks: + # update the NOT INSTALLABLE ADDONS section above + - id: oca-update-pre-commit-excluded-addons + - id: oca-fix-manifest-website + args: ["https://le-filament.com"] + - id: oca-gen-addon-readme + args: + - --addons-dir=. + - --branch=16.0 + - --org-name=lefilament + - --repo-name=template_module + - --if-source-changed + - --keep-source-digest + - repo: https://github.com/OCA/odoo-pre-commit-hooks + rev: v0.0.25 + hooks: + - id: oca-checks-odoo-module + - id: oca-checks-po + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.7.1 + hooks: + - id: prettier + name: prettier (with plugin-xml) + additional_dependencies: + - "prettier@2.7.1" + - "@prettier/plugin-xml@2.2.0" + args: + - --plugin=@prettier/plugin-xml + files: \.(css|htm|html|js|json|jsx|less|md|scss|toml|ts|xml|yaml|yml)$ + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v8.24.0 + hooks: + - id: eslint + verbose: true + args: + - --color + - --fix + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.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/astral-sh/ruff-pre-commit + rev: v0.1.3 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + - repo: https://github.com/OCA/pylint-odoo + rev: v8.0.19 + hooks: + - id: pylint_odoo + name: pylint with optional checks + args: + - --rcfile=.pylintrc + - --exit-zero + verbose: true + - id: pylint_odoo + args: + - --rcfile=.pylintrc-mandatory diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..71c476d --- /dev/null +++ b/.pylintrc @@ -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=Le Filament +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=16.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 diff --git a/.pylintrc-mandatory b/.pylintrc-mandatory new file mode 100644 index 0000000..9906493 --- /dev/null +++ b/.pylintrc-mandatory @@ -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=Le Filament +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=16.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 diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..0240c75 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,30 @@ + +target-version = "py310" +fix = true + +[lint] +extend-select = [ + "B", + "C90", + "E501", # line too long (default 88) + "I", # isort + "UP", # pyupgrade +] +exclude = ["setup/*"] + +[format] +exclude = ["setup/*"] + +[per-file-ignores] +"__init__.py" = ["F401", "I001"] # ignore unused and unsorted imports in __init__.py +"__manifest__.py" = ["B018"] # useless expression + +[isort] +section-order = ["future", "standard-library", "third-party", "odoo", "odoo-addons", "first-party", "local-folder"] + +[isort.sections] +"odoo" = ["odoo"] +"odoo-addons" = ["odoo.addons"] + +[mccabe] +max-complexity = 16 diff --git a/__init__.py b/__init__.py index db3c96a..0650744 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1 @@ -# Copyright 2019 Le Filament (<http://www.le-filament.com>) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - from . import models diff --git a/__manifest__.py b/__manifest__.py index 1c2bd98..60d1d36 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -1,14 +1,14 @@ { - 'name': "Lien Commandes Projets", - 'summary': """Filament - Lien entre commandes et projets""", - 'author': "Le Filament", - 'website': "https://www.le-filament.com", - 'version': '14.0.1.0.1', - 'license': "AGPL-3", - 'category': 'Sale Management', - 'depends': ['sale_timesheet', 'project'], - 'data': [ - 'views/res_config_settings_view.xml', - 'views/sale_view.xml', + "name": "Lien Commandes Projets", + "summary": """Filament - Lien entre commandes et projets""", + "author": "Le Filament", + "website": "https://le-filament.com", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "category": "Services/Project", + "depends": ["sale_timesheet", "project"], + "data": [ + "views/res_config_settings_view.xml", + "views/sale_view.xml", ], } diff --git a/controllers/__init__.py b/controllers/__init__.py deleted file mode 100644 index 05911a5..0000000 --- a/controllers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright 2019 Le Filament (<http://www.le-filament.com>) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -from . import main diff --git a/controllers/__pycache__/__init__.cpython-36.pyc b/controllers/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index a400b2c9a589cdbb5c342951b0c41e621a8f3ea2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 185 zcmXr!<>ityJR3Kafq~&M0}^0jU|?`yU|=W~U|?WKVMt-jVaR2SVq|1UVG3r@WPZuW zz`&r%c#9=BF*DCklj#;i5lF{Mh9VXQ1_<%XQ$Hg=H&s7BB|jfb7#is(mll`g=jtb> zq~zxn>*r+VWycpM=A_0K6y;~7CYR_Z=jW9a<>%z278UEq$7kkcmc+;F6;$5hu*uC& ODa}c>1DRe7awY&Y2`_>G diff --git a/controllers/__pycache__/main.cpython-36.pyc b/controllers/__pycache__/main.cpython-36.pyc deleted file mode 100644 index e5723210176b859f6ed6e4b034ef3d27ca7a2475..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4408 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 diff --git a/controllers/main.py b/controllers/main.py deleted file mode 100644 index 2cbcad2..0000000 --- a/controllers/main.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright 2019 Le Filament (<http://www.le-filament.com>) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from ast import literal_eval -import babel -from dateutil.relativedelta import relativedelta -import itertools -import json - -from odoo import http, fields, _ -from odoo.http import request -from odoo.tools import float_round -from odoo.addons.sale_timesheet.controllers.main import SaleTimesheetController - - -from odoo.addons.web.controllers.main import clean_action - -DEFAULT_MONTH_RANGE = 3 - - -class CustomSaleTimesheetController(SaleTimesheetController): - - def _table_get_line_values(self, projects): - """ return the header and the rows informations of the table """ - if not projects: - return False - - # useless fields for custom calcul - # uom_hour = request.env.ref('uom.product_uom_hour') - - # build SQL query and fetch raw data - query, query_params = self._table_rows_sql_query(projects) - request.env.cr.execute(query, query_params) - raw_data = request.env.cr.dictfetchall() - rows_employee = self._table_rows_get_employee_lines(projects, raw_data) - default_row_vals = self._table_row_default(projects) - - empty_line_ids, empty_order_ids = self._table_get_empty_so_lines(projects) - print("empty_line_ids, empty_order_ids", str(empty_line_ids)) - - # extract row labels - sale_line_ids = set() - sale_order_ids = set() - for key_tuple, row in rows_employee.items(): - if row[0]['sale_line_id']: - sale_line_ids.add(row[0]['sale_line_id']) - if row[0]['sale_order_id']: - sale_order_ids.add(row[0]['sale_order_id']) - - sale_order_lines = request.env['sale.order.line'].sudo().browse(sale_line_ids | empty_line_ids) - map_so_names = {so.id: so.name for so in request.env['sale.order'].sudo().browse(sale_order_ids | empty_order_ids)} - map_sol = {sol.id: sol for sol in sale_order_lines} - map_sol_names = {sol.id: sol.name.split('\n')[0] if sol.name else _('No Sales Order Line') for sol in sale_order_lines} - map_sol_so = {sol.id: sol.order_id.id for sol in sale_order_lines} - - rows_sale_line = {} # (so, sol) -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted] - for sale_line_id in empty_line_ids: # add service SO line having no timesheet - sale_line_row_key = (map_sol_so.get(sale_line_id), sale_line_id) - sale_line = map_sol.get(sale_line_id) - is_milestone = sale_line.product_id.invoice_policy == 'delivery' and sale_line.product_id.service_type == 'manual' if sale_line else False - rows_sale_line[sale_line_row_key] = [{'label': map_sol_names.get(sale_line_id, _('No Sales Order Line')), 'res_id': sale_line_id, 'res_model': 'sale.order.line', 'type': 'sale_order_line', 'is_milestone': is_milestone}] + default_row_vals[:] - if not is_milestone: - # **** MODIF **** - rows_sale_line[sale_line_row_key][-2] = sale_line.product_uom_qty*sale_line.price_unit/sale_line.order_id.taux_horaire if sale_line else 0.0 - # rows_sale_line[sale_line_row_key][-2] = sale_line.product_uom._compute_quantity(sale_line.product_uom_qty, uom_hour, raise_if_failure=False) if sale_line else 0.0 - - for row_key, row_employee in rows_employee.items(): - sale_line_id = row_key[1] - sale_order_id = row_key[0] - # sale line row - sale_line_row_key = (sale_order_id, sale_line_id) - if sale_line_row_key not in rows_sale_line: - sale_line = map_sol.get(sale_line_id, request.env['sale.order.line']) - is_milestone = sale_line.product_id.invoice_policy == 'delivery' and sale_line.product_id.service_type == 'manual' if sale_line else False - rows_sale_line[sale_line_row_key] = [{'label': map_sol_names.get(sale_line.id) if sale_line else _('No Sales Order Line'), 'res_id': sale_line_id, 'res_model': 'sale.order.line', 'type': 'sale_order_line', 'is_milestone': is_milestone}] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted - if not is_milestone: - # **** MODIF **** - rows_sale_line[sale_line_row_key][-2] = sale_line.product_uom_qty*sale_line.price_unit/sale_line.order_id.taux_horaire if sale_line else 0.0 - # rows_sale_line[sale_line_row_key][-2] = sale_line.product_uom._compute_quantity(sale_line.product_uom_qty, uom_hour, raise_if_failure=False) if sale_line else 0.0 - - for index in range(len(rows_employee[row_key])): - if index != 0: - rows_sale_line[sale_line_row_key][index] += rows_employee[row_key][index] - if not rows_sale_line[sale_line_row_key][0].get('is_milestone'): - rows_sale_line[sale_line_row_key][-1] = rows_sale_line[sale_line_row_key][-2] - rows_sale_line[sale_line_row_key][5] - else: - rows_sale_line[sale_line_row_key][-1] = 0 - - rows_sale_order = {} # so -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted] - rows_sale_order_done_sold = {key : dict(sold=0.0, done=0.0) for key in set(map_sol_so.values()) | set([None])} # SO id -> {'sold':0.0, 'done': 0.0} - for row_key, row_sale_line in rows_sale_line.items(): - sale_order_id = row_key[0] - # sale order row - if sale_order_id not in rows_sale_order: - rows_sale_order[sale_order_id] = [{'label': map_so_names.get(sale_order_id, _('No Sales Order')), 'res_id': sale_order_id, 'res_model': 'sale.order', 'type': 'sale_order'}] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted - - for index in range(len(rows_sale_line[row_key])): - if index != 0: - rows_sale_order[sale_order_id][index] += rows_sale_line[row_key][index] - - # do not sum the milestone SO line for sold and done (for remaining computation) - if not rows_sale_line[row_key][0].get('is_milestone'): - rows_sale_order_done_sold[sale_order_id]['sold'] += rows_sale_line[row_key][-2] - rows_sale_order_done_sold[sale_order_id]['done'] += rows_sale_line[row_key][5] - - # remaining computation of SO row, as Sold - Done (timesheet total) - for sale_order_id, done_sold_vals in rows_sale_order_done_sold.items(): - if sale_order_id in rows_sale_order: - rows_sale_order[sale_order_id][-1] = done_sold_vals['sold'] - done_sold_vals['done'] - - # group rows SO, SOL and their related employee rows. - timesheet_forecast_table_rows = [] - for sale_order_id, sale_order_row in rows_sale_order.items(): - timesheet_forecast_table_rows.append(sale_order_row) - for sale_line_row_key, sale_line_row in rows_sale_line.items(): - if sale_order_id == sale_line_row_key[0]: - timesheet_forecast_table_rows.append(sale_line_row) - for employee_row_key, employee_row in rows_employee.items(): - if sale_order_id == employee_row_key[0] and sale_line_row_key[1] == employee_row_key[1]: - timesheet_forecast_table_rows.append(employee_row) - - # complete table data - return { - 'header': self._table_header(projects), - 'rows': timesheet_forecast_table_rows - } diff --git a/models/__init__.py b/models/__init__.py index eaa1c10..9463a5b 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -1,8 +1,5 @@ -# Copyright 2019 Le Filament (<http://www.le-filament.com>) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - from . import res_company from . import res_config_settings -from . import product_template -from . import project_overview +from . import project_update from . import sale_order +from . import sale_order_line diff --git a/models/product_template.py b/models/product_template.py deleted file mode 100644 index 7ea8631..0000000 --- a/models/product_template.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2019 Le Filament (<http://www.le-filament.com>) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -from odoo import models, fields, api - - -class ProductTemplate(models.Model): - _inherit = "product.template" - - project_linked_stage_id = fields.Many2one('project.task.type', string='Etape', ondelete='restrict', - tracking=True, index=True, - group_expand='_read_group_stage_ids', - domain="[('project_ids', '=', project_id)]", copy=False) diff --git a/models/project_overview.py b/models/project_overview.py deleted file mode 100644 index 66a0351..0000000 --- a/models/project_overview.py +++ /dev/null @@ -1,283 +0,0 @@ -# Copyright 2021 Le Filament (<http://www.le-filament.com>) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -from odoo import fields, models, api, _ -import itertools -from odoo.addons.sale_timesheet.models.project_overview import _to_action_data - - -class Project(models.Model): - _inherit = 'project.project' - - # ------------------------------------------------------ - # Fields declaration - # ------------------------------------------------------ - - # ------------------------------------------------------ - # SQL Constraints - # ------------------------------------------------------ - - # ------------------------------------------------------ - # Default methods - # ------------------------------------------------------ - - # ------------------------------------------------------ - # Computed fields / Search Fields - # ------------------------------------------------------ - - # ------------------------------------------------------ - # Onchange / Constraints - # ------------------------------------------------------ - - # ------------------------------------------------------ - # CRUD methods (ORM overrides) - # ------------------------------------------------------ - def _table_get_line_values(self, employees=None): - """ return the header and the rows informations of the table """ - if not self: - return False - - uom_hour = self.env.ref('uom.product_uom_hour') - company_uom = self.env.company.timesheet_encode_uom_id - is_uom_day = company_uom and company_uom == self.env.ref('uom.product_uom_day') - - # build SQL query and fetch raw data - query, query_params = self._table_rows_sql_query() - self.env.cr.execute(query, query_params) - raw_data = self.env.cr.dictfetchall() - rows_employee = self._table_rows_get_employee_lines(raw_data) - default_row_vals = self._table_row_default() - - empty_line_ids, empty_order_ids = self._table_get_empty_so_lines() - - # extract row labels - sale_line_ids = set() - sale_order_ids = set() - for key_tuple, row in rows_employee.items(): - if row[0]['sale_line_id']: - sale_line_ids.add(row[0]['sale_line_id']) - if row[0]['sale_order_id']: - sale_order_ids.add(row[0]['sale_order_id']) - - sale_orders = self.env['sale.order'].sudo().browse(sale_order_ids | empty_order_ids) - sale_order_lines = self.env['sale.order.line'].sudo().browse(sale_line_ids | empty_line_ids) - map_so_names = {so.id: so.name for so in sale_orders} - map_so_cancel = {so.id: so.state == 'cancel' for so in sale_orders} - map_sol = {sol.id: sol for sol in sale_order_lines} - map_sol_names = {sol.id: sol.name.split('\n')[0] if sol.name else _('No Sales Order Line') for sol in - sale_order_lines} - map_sol_so = {sol.id: sol.order_id.id for sol in sale_order_lines} - - rows_sale_line = {} # (so, sol) -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted] - for sale_line_id in empty_line_ids: # add service SO line having no timesheet - sale_line_row_key = (map_sol_so.get(sale_line_id), sale_line_id) - sale_line = map_sol.get(sale_line_id) - is_milestone = sale_line.product_id.invoice_policy == 'delivery' and sale_line.product_id.service_type == 'manual' if sale_line else False - rows_sale_line[sale_line_row_key] = [{'label': map_sol_names.get(sale_line_id, _('No Sales Order Line')), - 'res_id': sale_line_id, 'res_model': 'sale.order.line', - 'type': 'sale_order_line', - 'is_milestone': is_milestone}] + default_row_vals[:] - if not is_milestone: - # ***** Modif Filament ***** - rows_sale_line[sale_line_row_key][ - -2] = sale_line.product_uom_qty * sale_line.price_unit / sale_line.order_id.taux_horaire if sale_line else 0.0 - - rows_sale_line_all_data = {} - if not employees: - employees = self.env['hr.employee'].sudo().search(self.env['account.analytic.line']._domain_employee_id()) - for row_key, row_employee in rows_employee.items(): - sale_order_id, sale_line_id, employee_id = row_key - # sale line row - sale_line_row_key = (sale_order_id, sale_line_id) - if sale_line_row_key not in rows_sale_line: - sale_line = map_sol.get(sale_line_id, self.env['sale.order.line']) - is_milestone = sale_line.product_id.invoice_policy == 'delivery' and sale_line.product_id.service_type == 'manual' if sale_line else False - rows_sale_line[sale_line_row_key] = [{'label': map_sol_names.get(sale_line.id) if sale_line else _( - 'No Sales Order Line'), 'res_id': sale_line_id, 'res_model': 'sale.order.line', - 'type': 'sale_order_line', - 'is_milestone': is_milestone}] + default_row_vals[ - :] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted - if not is_milestone: - # ***** Modif Filament ***** - rows_sale_line[sale_line_row_key][ - -2] = sale_line.product_uom_qty * sale_line.price_unit / sale_line.order_id.taux_horaire if sale_line else 0.0 - - if sale_line_row_key not in rows_sale_line_all_data: - rows_sale_line_all_data[sale_line_row_key] = [0] * len(row_employee) - for index in range(1, len(row_employee)): - if employee_id in employees.ids: - rows_sale_line[sale_line_row_key][index] += row_employee[index] - rows_sale_line_all_data[sale_line_row_key][index] += row_employee[index] - if not rows_sale_line[sale_line_row_key][0].get('is_milestone'): - rows_sale_line[sale_line_row_key][-1] = rows_sale_line[sale_line_row_key][-2] - \ - rows_sale_line_all_data[sale_line_row_key][5] - else: - rows_sale_line[sale_line_row_key][-1] = 0 - - rows_sale_order = {} # so -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted] - for row_key, row_sale_line in rows_sale_line.items(): - sale_order_id = row_key[0] - # sale order row - if sale_order_id not in rows_sale_order: - rows_sale_order[sale_order_id] = [{'label': map_so_names.get(sale_order_id, _('No Sales Order')), - 'canceled': map_so_cancel.get(sale_order_id, False), - 'res_id': sale_order_id, 'res_model': 'sale.order', - 'type': 'sale_order'}] + default_row_vals[ - :] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted - - for index in range(1, len(row_sale_line)): - rows_sale_order[sale_order_id][index] += row_sale_line[index] - - # group rows SO, SOL and their related employee rows. - timesheet_forecast_table_rows = [] - for sale_order_id, sale_order_row in rows_sale_order.items(): - timesheet_forecast_table_rows.append(sale_order_row) - for sale_line_row_key, sale_line_row in rows_sale_line.items(): - if sale_order_id == sale_line_row_key[0]: - sale_order_row[0]['has_children'] = True - timesheet_forecast_table_rows.append(sale_line_row) - for employee_row_key, employee_row in rows_employee.items(): - if sale_order_id == employee_row_key[0] and sale_line_row_key[1] == employee_row_key[1] and \ - employee_row_key[2] in employees.ids: - sale_line_row[0]['has_children'] = True - timesheet_forecast_table_rows.append(employee_row) - - if is_uom_day: - # convert all values from hours to days - for row in timesheet_forecast_table_rows: - for index in range(1, len(row)): - row[index] = round(uom_hour._compute_quantity(row[index], company_uom, raise_if_failure=False), 2) - # complete table data - return { - 'header': self._table_header(), - 'rows': timesheet_forecast_table_rows - } - - def _plan_get_stat_button(self): - stat_buttons = [] - num_projects = len(self) - if num_projects == 1: - action_data = _to_action_data('project.project', res_id=self.id, - views=[[self.env.ref('project.edit_project').id, 'form']]) - else: - action_data = _to_action_data(action=self.env.ref('project.open_view_project_all_config').sudo(), - domain=[('id', 'in', self.ids)]) - - stat_buttons.append({ - 'name': _('Project') if num_projects == 1 else _('Projects'), - 'count': num_projects, - 'icon': 'fa fa-puzzle-piece', - 'action': action_data - }) - - # if only one project, add it in the context as default value - tasks_domain = [('project_id', 'in', self.ids)] - tasks_context = self.env.context.copy() - tasks_context.pop('search_default_name', False) - late_tasks_domain = [('project_id', 'in', self.ids), ('date_deadline', '<', fields.Date.to_string(fields.Date.today())), ('date_end', '=', False)] - overtime_tasks_domain = [('project_id', 'in', self.ids), ('overtime', '>', 0), ('planned_hours', '>', 0)] - - if len(self) == 1: - tasks_context = {**tasks_context, 'default_project_id': self.id} - elif len(self): - task_projects_ids = self.env['project.task'].read_group([('project_id', 'in', self.ids)], ['project_id'], ['project_id']) - task_projects_ids = [p['project_id'][0] for p in task_projects_ids] - if len(task_projects_ids) == 1: - tasks_context = {**tasks_context, 'default_project_id': task_projects_ids[0]} - - stat_buttons.append({ - 'name': _('Tasks'), - 'count': sum(self.mapped('task_count')), - 'icon': 'fa fa-tasks', - 'action': _to_action_data( - action=self.env.ref('project.action_view_task').sudo(), - domain=tasks_domain, - context=tasks_context - ) - }) - stat_buttons.append({ - 'name': [_("Tasks"), _("Late")], - 'count': self.env['project.task'].search_count(late_tasks_domain), - 'icon': 'fa fa-tasks', - 'action': _to_action_data( - action=self.env.ref('project.action_view_task').sudo(), - domain=late_tasks_domain, - context=tasks_context, - ), - }) - stat_buttons.append({ - 'name': [_("Tasks"), _("in Overtime")], - 'count': self.env['project.task'].search_count(overtime_tasks_domain), - 'icon': 'fa fa-tasks', - 'action': _to_action_data( - action=self.env.ref('project.action_view_task').sudo(), - domain=overtime_tasks_domain, - context=tasks_context, - ), - }) - - if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): - # read all the sale orders linked to the projects' tasks - task_so_ids = self.env['project.task'].search_read([ - ('project_id', 'in', self.ids), ('sale_order_id', '!=', False) - ], ['sale_order_id']) - task_so_ids = [o['sale_order_id'][0] for o in task_so_ids] - - sale_ids = self.env['sale.order'].search([('project_id', 'in', self.ids)]) - sale_orders = self.mapped('sale_line_id.order_id') | self.env['sale.order'].browse(task_so_ids) | sale_ids - if sale_orders: - stat_buttons.append({ - 'name': _('Sales Orders'), - 'count': len(sale_orders), - 'icon': 'fa fa-dollar', - 'action': _to_action_data( - action=self.env.ref('sale.action_orders').sudo(), - domain=[('id', 'in', sale_orders.ids)], - context={'create': False, 'edit': False, 'delete': False} - ) - }) - - invoice_ids = self.env['sale.order'].search_read([('id', 'in', sale_orders.ids)], ['invoice_ids']) - invoice_ids = list(itertools.chain(*[i['invoice_ids'] for i in invoice_ids])) - invoice_ids = self.env['account.move'].search_read([('id', 'in', invoice_ids), ('move_type', '=', 'out_invoice')], ['id']) - invoice_ids = list(map(lambda x: x['id'], invoice_ids)) - - if invoice_ids: - stat_buttons.append({ - 'name': _('Invoices'), - 'count': len(invoice_ids), - 'icon': 'fa fa-pencil-square-o', - 'action': _to_action_data( - action=self.env.ref('account.action_move_out_invoice_type').sudo(), - domain=[('id', 'in', invoice_ids), ('move_type', '=', 'out_invoice')], - context={'create': False, 'delete': False} - ) - }) - - ts_tree = self.env.ref('hr_timesheet.hr_timesheet_line_tree') - ts_form = self.env.ref('hr_timesheet.hr_timesheet_line_form') - if self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day'): - timesheet_label = [_('Days'), _('Recorded')] - else: - timesheet_label = [_('Hours'), _('Recorded')] - - stat_buttons.append({ - 'name': timesheet_label, - 'count': sum(self.mapped('total_timesheet_time')), - 'icon': 'fa fa-calendar', - 'action': _to_action_data( - 'account.analytic.line', - domain=[('project_id', 'in', self.ids)], - views=[(ts_tree.id, 'list'), (ts_form.id, 'form')], - ) - }) - - return stat_buttons - - # ------------------------------------------------------ - # Actions - # ------------------------------------------------------ - - # ------------------------------------------------------ - # Business methods - # ------------------------------------------------------ diff --git a/models/project_update.py b/models/project_update.py new file mode 100644 index 0000000..b2fde5a --- /dev/null +++ b/models/project_update.py @@ -0,0 +1,31 @@ +# Copyright 2021- Le Filament (<https://le-filament.com>) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +from odoo import models + + +class Project(models.Model): + _inherit = "project.update" + + def _get_services_values(self, project): + res = super()._get_services_values(project) + total_sold = 0 + total_effective = 0 + for service_line in res["data"]: + sol = service_line["sol"] + product_uom_qty = sol._convert_qty_company_hours(sol.company_id) + delivered_qty = service_line["effective_value"] + service_line["sold_value"] = product_uom_qty + service_line["remaining_value"] = product_uom_qty - delivered_qty + total_sold += product_uom_qty + total_effective += delivered_qty + total_remaining = total_sold - total_effective + res.update( + { + "total_sold": total_sold, + "total_effective": total_effective, + "total_remaining": total_remaining, + } + ) + return res diff --git a/models/res_company.py b/models/res_company.py index 516fcfe..1e7a8fa 100644 --- a/models/res_company.py +++ b/models/res_company.py @@ -1,11 +1,10 @@ -# Copyright 2019 Le Filament (<http://www.le-filament.com>) +# Copyright 2019-2022 Le Filament (<https://le-filament.com>) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import models, fields, api +from odoo import fields, models + class Company(models.Model): _inherit = "res.company" - taux_horaire = fields.Integer( - required=True, - default=75) + taux_horaire = fields.Integer(required=True, default=100) diff --git a/models/res_config_settings.py b/models/res_config_settings.py index 9305051..d9c77af 100644 --- a/models/res_config_settings.py +++ b/models/res_config_settings.py @@ -1,13 +1,12 @@ -# Copyright 2019 Le Filament (<http://www.le-filament.com>) +# Copyright 2019-2022 Le Filament (<https://le-filament.com>) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import models, fields, api +from odoo import fields, models class ResConfigSettings(models.TransientModel): - _inherit = 'res.config.settings' + _inherit = "res.config.settings" taux_horaire = fields.Integer( - 'Taux horaire', - related='company_id.taux_horaire', - readonly=False) + "Taux horaire", related="company_id.taux_horaire", readonly=False + ) diff --git a/models/sale_order.py b/models/sale_order.py index 16e001b..fc66066 100644 --- a/models/sale_order.py +++ b/models/sale_order.py @@ -1,189 +1,12 @@ -# Copyright 2019 Le Filament (<http://www.le-filament.com>) +# Copyright 2019- Le Filament (<https://le-filament.com>) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import models, fields, api -from odoo.exceptions import ValidationError +from odoo import fields, models class SaleOrder(models.Model): _inherit = "sale.order" - # project_id_linked = fields.Many2one('project.project', string='Projet associé', - # domain=[('allow_timesheets', '=', True), ('active', '=', True)]) - project_name_to_create = fields.Char("Nom du projet") - - project_tracking = fields.Selection([ - ('new', 'Créer un nouveau projet'), - ('link', 'Associer un projet existant'), - ]) - - no_create_task = fields.Boolean(string="Ne pas créer les tâches") - taux_horaire = fields.Integer( - 'Taux horaire', - default=lambda self: self.env.company.taux_horaire) - - @api.onchange("partner_id", "order_line") - def _project_name_to_create(self): - so_line_new_project_with_tasks = self.mapped('order_line').filtered( - lambda sol: sol.is_service and sol.product_id.service_tracking == 'task_in_project') - if not so_line_new_project_with_tasks: - self.project_tracking = False - elif so_line_new_project_with_tasks and self.partner_id: - if not self.project_id: - self.project_tracking = "new" - if not self.project_name_to_create: - self.project_name_to_create = self.partner_id.name + str(' - ') - else: - self.project_tracking = "link" - - @api.onchange("project_tracking") - def _erase_project_tracking(self): - if self.project_tracking == "link": - self.project_name_to_create = None - elif self.project_tracking == "new": - self.project_id = None - if not self.project_name_to_create: - so_line_new_project_with_tasks = self.mapped('order_line').filtered( - lambda sol: sol.is_service and sol.product_id.service_tracking == 'task_in_project') - if so_line_new_project_with_tasks and self.partner_id: - self.project_name_to_create = self.partner_id.name + str(' - ') - - def action_confirm(self): - # on différencie so_line_new_project (dans _timesheet_service_generation) de so_line_new_project_with_tasks - # car on laisse le fonctionnement natif pour les articles où on crée le projet sans les tâches - so_line_new_project_with_tasks = self.mapped('order_line').filtered( - lambda sol: sol.is_service and sol.product_id.service_tracking == 'task_in_project') - if (so_line_new_project_with_tasks and not self.project_id) and ( - so_line_new_project_with_tasks and not self.project_name_to_create): - raise ValidationError( - "Pas de projet associé à ce devis : merci de choisir un projet ou d'en créer un nouveau") - elif so_line_new_project_with_tasks and self.project_id and self.project_name_to_create: - raise ValidationError( - "Vous ne pouvez pas créer un nouveau projet et associer à un projet existant en même temps : merci de " - "vérifier le projet que vous voulez associer à ce devis") # cas normalement géré par le xml - elif not so_line_new_project_with_tasks and (self.project_id or self.project_name_to_create): - raise ValidationError( - "Vous ne pouvez pas associer de projet à ce devis : tous les articles sont déjà liés à des projets " - "existants") # cas normalement géré par le xml - else: - return super(SaleOrder, self).action_confirm() - - -class SaleOrderLine(models.Model): - _inherit = "sale.order.line" - - def _convert_qty_company_hours(self, dest_company): - """ Reprise de la fonction native pour changer le mode de calcul des heures planifiées dans timesheet - """ - company_time_uom_id = dest_company.project_time_mode_id - taux_horaire = self.order_id.taux_horaire - if self.product_uom.id != company_time_uom_id.id \ - and self.product_uom.category_id.id == company_time_uom_id.category_id.id: - planned_hours = super(SaleOrderLine, self)._convert_qty_company_hours(dest_company) - else: - planned_hours = (self.product_uom_qty * self.price_unit) / taux_horaire - return planned_hours - - def _timesheet_create_task(self, project): - """ Pour gérer le stage_id et le nom des tâches pour les projets maintenance et support - """ - task = super(SaleOrderLine, self)._timesheet_create_task(project) - if self.product_id.service_tracking == 'task_global_project': - stage = self.product_id.project_linked_stage_id - client_name = self.order_id.partner_id.name - if stage: - task.stage_id = stage - task_name = str(client_name + " - " + stage.name) - task.write({'name': task_name}) - else: - task.write({'name': client_name}) - return task - - def _timesheet_create_project(self, name_project): - """ Genère le projet de la même manière mais lui donne le nom choisi - """ - project = super(SaleOrderLine, self)._timesheet_create_project() - project.name = name_project # on réécrit le nom du projet - return project - - def _timesheet_service_generation(self): - """ Réécriture de la fonction native de manière quasi-identique - mais qui permet d'associer chaque ligne du devis à un projet - précédemment créé ou choisi - """ - so_line_task_global_project = self.filtered( - lambda sol: sol.is_service and sol.product_id.service_tracking == 'task_global_project') - so_line_new_project = self.filtered( - lambda sol: sol.is_service and sol.product_id.service_tracking in ['project_only', 'task_in_project']) - - # search so lines from SO of current so lines having their project generated, in order to check if the - # current one can create its own project, or reuse the one of its order. - map_so_project = {} - if so_line_new_project: - order_ids = self.mapped('order_id').ids - so_lines_with_project = self.search([('order_id', 'in', order_ids), ('project_id', '!=', False), ( - 'product_id.service_tracking', 'in', ['project_only', 'task_in_project']), - ('product_id.project_template_id', '=', False)]) - map_so_project = {sol.order_id.id: sol.project_id for sol in so_lines_with_project} - so_lines_with_project_templates = self.search([('order_id', 'in', order_ids), ('project_id', '!=', False), ( - 'product_id.service_tracking', 'in', ['project_only', 'task_in_project']), - ('product_id.project_template_id', '!=', False)]) - map_so_project_templates = {(sol.order_id.id, sol.product_id.project_template_id.id): sol.project_id for sol - in so_lines_with_project_templates} - - # search the global project of current SO lines, in which create their task - map_sol_project = {} - if so_line_task_global_project: - map_sol_project = {sol.id: sol.product_id.with_context(force_company=sol.company_id.id).project_id for sol - in so_line_task_global_project} - - def _can_create_project(sol): - if not sol.project_id: - if sol.product_id.project_template_id: - return (sol.order_id.id, sol.product_id.project_template_id.id) not in map_so_project_templates - elif sol.order_id.id not in map_so_project: - return True - return False - - # task_global_project: create task in global project - for so_line in so_line_task_global_project: - no_create_task = so_line.order_id.no_create_task # on récupère la variable de classe créée - if not so_line.task_id and not no_create_task: # rajoute une conditon pour vérifier si on crée les tâches - if map_sol_project.get(so_line.id): - so_line._timesheet_create_task(project=map_sol_project[so_line.id]) - - # project_only, task_in_project: use the project linked to the sale_order. May be create a task too. - for so_line in so_line_new_project: - project = so_line.project_id - # on récupère les variables de classes créées - no_create_task = so_line.order_id.no_create_task - project_linked = so_line.order_id.project_id - new_project_name = so_line.order_id.project_name_to_create - if not project and _can_create_project(so_line): - if project_linked: # si on a choisi un projet .. - project = project_linked # .. on associe la so_line au projet associé au devis - so_line.write({'project_id': project_linked.id}) - elif new_project_name: # si on a choisi un nom de nouveau projet, on crée ce projet - project = so_line._timesheet_create_project(new_project_name) - elif so_line.product_id.service_tracking == 'project_only': # on est dans le cas "project_only" - project = super(SaleOrderLine, self)._timesheet_create_project() - if so_line.product_id.project_template_id: - map_so_project_templates[(so_line.order_id.id, so_line.product_id.project_template_id.id)] = project - else: - map_so_project[so_line.order_id.id] = project - elif not project: - # Attach subsequent SO lines to the created project - so_line.project_id = ( - map_so_project_templates.get((so_line.order_id.id, so_line.product_id.project_template_id.id)) - or map_so_project.get(so_line.order_id.id) - ) - if so_line.product_id.service_tracking == 'task_in_project': - if not project: - if so_line.product_id.project_template_id: - project = map_so_project_templates[ - (so_line.order_id.id, so_line.product_id.project_template_id.id)] - else: - project = map_so_project[so_line.order_id.id] - if not so_line.task_id and not no_create_task: # rajoute conditon pour vérifier si on crée les tâches - so_line._timesheet_create_task(project=project) + "Taux horaire", default=lambda self: self.env.company.taux_horaire + ) diff --git a/models/sale_order_line.py b/models/sale_order_line.py new file mode 100644 index 0000000..7a3f5b8 --- /dev/null +++ b/models/sale_order_line.py @@ -0,0 +1,24 @@ +# Copyright 2019- Le Filament (<https://le-filament.com>) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + def _convert_qty_company_hours(self, dest_company): + """ + Reprise de la fonction native pour changer le mode de calcul des heures + planifiées dans timesheet + """ + company_time_uom_id = dest_company.project_time_mode_id + taux_horaire = self.order_id.taux_horaire + if ( + self.product_uom.id != company_time_uom_id.id + and self.product_uom.category_id.id == company_time_uom_id.category_id.id + ): + planned_hours = super()._convert_qty_company_hours(dest_company) + else: + planned_hours = (self.product_uom_qty * self.price_unit) / taux_horaire + return planned_hours diff --git a/views/res_config_settings_view.xml b/views/res_config_settings_view.xml index 7d70396..870c905 100644 --- a/views/res_config_settings_view.xml +++ b/views/res_config_settings_view.xml @@ -1,21 +1,30 @@ -<?xml version="1.0"?> -<!-- Copyright 2019 Le Filament +<?xml version="1.0" ?> +<!-- Copyright 2019-2022 Le Filament (<https://le-filament.com>) License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> - <odoo> <record id="res_config_settings_view_form" model="ir.ui.view"> - <field name="name">res.config.settings.view.form.inherit.link.sale.project</field> + <field + name="name" + >res.config.settings.view.form.inherit.link.sale.project</field> <field name="model">res.config.settings</field> - <field name="inherit_id" ref="project.res_config_settings_view_form"/> + <field name="inherit_id" ref="project.res_config_settings_view_form" /> <field name="arch" type="xml"> <div class="app_settings_block" data-string="Sales" position="inside"> <h2>Configuration devis-projet</h2> <div class="row mt16 o_settings_container"> <div class="col-12 col-lg-6 o_setting_box"> <div class="o_setting_right_pane"> - <label for="taux_horaire" string="Taux horaire" class="col-3 col-lg-3 o_light_label"/> - <field name="taux_horaire" class="oe_inline" required="1"/> + <label + for="taux_horaire" + string="Taux horaire" + class="col-3 col-lg-3 o_light_label" + /> + <field + name="taux_horaire" + class="oe_inline" + required="1" + /> </div> </div> </div> diff --git a/views/sale_view.xml b/views/sale_view.xml index 519c98b..537debd 100644 --- a/views/sale_view.xml +++ b/views/sale_view.xml @@ -1,26 +1,19 @@ -<?xml version="1.0"?> -<!-- Copyright 2019 Le Filament +<?xml version="1.0" ?> +<!-- Copyright 2021-2022 Le Filament (<https://le-filament.com>) License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> - <odoo> <record model="ir.ui.view" id="view_order_form_link_project"> <field name="name">sale.order.form.link.project</field> <field name="model">sale.order</field> - <field name="inherit_id" ref="sale.view_order_form"/> + <field name="inherit_id" ref="sale.view_order_form" /> <field name="priority">100</field> <field name="arch" type="xml"> - <field name="project_id" position="replace"/> <field name="payment_term_id" position="after"> - <field name="taux_horaire" attrs="{'readonly':[('state','!=','draft')]}"/> - <field name="project_tracking" widget="radio" attrs="{'invisible':['|',('state','=','sale'),('project_tracking','=',False)]}"/> - <field name="project_name_to_create" attrs="{'invisible':['|',('project_tracking','!=','new'),('state','=','sale')]}"/> - <field name="project_id" - attrs="{'invisible':['|',('project_tracking','!=','link'),('state','=','sale')]}" - options="{'no_create': True}"/> - <field name="project_ids" string="Projets associés" attrs="{'invisible':[('state','!=','sale')]}"/> - <field name="no_create_task" attrs="{'invisible':['|',('state','=','sale'),('project_tracking','=',False)]}"/> - + <field + name="taux_horaire" + attrs="{'readonly':[('state','!=','draft')]}" + /> </field> </field> -- GitLab