diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..bfd7ac53df9f103f6dc8853738c63fd364445fde --- /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 0000000000000000000000000000000000000000..fed88d70d23ecb3297ea28854b320c4d62ee3c26 --- /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 0000000000000000000000000000000000000000..d99361a24aa02c2d19d8165c3a057b84bc575166 --- /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 0000000000000000000000000000000000000000..3efb4d9157dc36d84e372eec5fc21e7f4a417a58 --- /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 0000000000000000000000000000000000000000..71c476d4f10ac08a7333729b93705c9573d240d5 --- /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 0000000000000000000000000000000000000000..99064933ef82c469ba5fda5b2904447c05c99dbe --- /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 0000000000000000000000000000000000000000..0240c75f6a4ae4550f3473ad0a5faaef022bf6c7 --- /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 db3c96a20d0d825c802d0635aabb49823593e8b8..0650744f6bc69b9f0b865e8c7174c813a5f5995e 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 1c2bd98bf930b9cf2a22888a3adcd1325ccc1106..60d1d36e55e619015eba8d5f62867d5d122158a1 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 05911a59793a247cf64d4df65169af94f40c5e72..0000000000000000000000000000000000000000 --- 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 Binary files a/controllers/__pycache__/__init__.cpython-36.pyc and /dev/null differ diff --git a/controllers/__pycache__/main.cpython-36.pyc b/controllers/__pycache__/main.cpython-36.pyc deleted file mode 100644 index e5723210176b859f6ed6e4b034ef3d27ca7a2475..0000000000000000000000000000000000000000 Binary files a/controllers/__pycache__/main.cpython-36.pyc and /dev/null differ diff --git a/controllers/main.py b/controllers/main.py deleted file mode 100644 index 2cbcad26371edfc8a2bfa17515dc46069dfaa290..0000000000000000000000000000000000000000 --- 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 eaa1c10c07b7acadcdd8d69f4618975156fc1f54..9463a5bb66749640931e09372bc49276daa72c5d 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 7ea8631ba6c7ad5934cdadefaee567520a50b778..0000000000000000000000000000000000000000 --- 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 66a0351ad34696a2ab7bcacda72af1e9924fd9d0..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..b2fde5acee99b4b601c26189f115c4ffdd8a7a9b --- /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 516fcfe04767a259d47cbcb8006ed21104d1167e..1e7a8fa70168fd5dffdbcbbcabc893f00438e66d 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 93050517ddc983de6944c576acb1f89c552fd46a..d9c77af75f201fe23bd42906e43d5d4d989ddf82 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 16e001b8482f79195dd517fce0db3b19aae16158..fc660669a7d4f789a06c8cf2b1c912e790c4e053 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 0000000000000000000000000000000000000000..7a3f5b8f6898c2c6c952912cf236785a1526ed9d --- /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 7d7039611c606df5d8d1b6baecc11832bb722910..870c905467edec3fdf310ae8b937d5a4b341b61c 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 519c98b9e82a9d76fe51d64093d574358a5d9842..537debd3b3cc27927868298919ba797204691c74 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>