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..d4cc423ccda9db9691205c9da83307af97b2670f --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,187 @@ +env: + browser: true + es6: true + +# See https://github.com/OCA/odoo-community.org/issues/37#issuecomment-470686449 +parserOptions: + ecmaVersion: 2017 + +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 + +# 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/.flake8 b/.flake8 new file mode 100644 index 0000000000000000000000000000000000000000..e397e8ed4e3e7f7fe7785dd391bb80aa6d85575e --- /dev/null +++ b/.flake8 @@ -0,0 +1,12 @@ +[flake8] +max-line-length = 88 +max-complexity = 16 +# B = bugbear +# B9 = bugbear opinionated (incl line length) +select = C,E,F,W,B,B9 +# E203: whitespace before ':' (black behaviour) +# E501: flake8 line length (covered by bugbear B950) +# W503: line break before binary operator (black behaviour) +ignore = E203,E501,W503 +per-file-ignores= + __init__.py:F401 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..818770fb1bdc0a144e924c9a5940f0b035df8a0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,75 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +/.venv +/.pytest_cache + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg +*.eggs + +# 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/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000000000000000000000000000000000000..0ec187efd1bf802844749f508cda0c8f138970f9 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,13 @@ +[settings] +; see https://github.com/psf/black +multi_line_output=3 +include_trailing_comma=True +force_grid_wrap=0 +combine_as_imports=True +use_parentheses=True +line_length=88 +known_odoo=odoo +known_odoo_addons=odoo.addons +sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER +default_section=THIRDPARTY +ensure_newline_before_comments = True diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8a5999af4419f630dda23d9366b51967b8322ed3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,127 @@ +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$| + # You don't usually want a bot to modify your legal texts + (LICENSE.*|COPYING.*) +default_language_version: + python: python3 + node: "14.13.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$" + - repo: https://github.com/oca/maintainer-tools + rev: ab1d7f6 + 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"] + - repo: https://github.com/myint/autoflake + rev: v1.4 + hooks: + - id: autoflake + args: + - --expand-star-imports + - --ignore-init-module-imports + - --in-place + - --remove-all-unused-imports + - --remove-duplicate-keys + - --remove-unused-variables + - repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.1.2 + hooks: + - id: prettier + name: prettier (with plugin-xml) + additional_dependencies: + - "prettier@2.1.2" + - "@prettier/plugin-xml@0.12.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: v7.8.1 + hooks: + - id: eslint + verbose: true + args: + - --color + - --fix + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.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/asottile/pyupgrade + rev: v2.7.2 + hooks: + - id: pyupgrade + args: ["--keep-percent-format"] + - repo: https://github.com/PyCQA/isort + rev: 5.5.1 + hooks: + - id: isort + name: isort except __init__.py + args: + - --settings=. + exclude: /__init__\.py$ + - repo: https://gitlab.com/PyCQA/flake8 + rev: 3.8.3 + hooks: + - id: flake8 + name: flake8 + additional_dependencies: ["flake8-bugbear==20.1.4"] + - repo: https://github.com/PyCQA/pylint + rev: pylint-2.5.3 + hooks: + - id: pylint + name: pylint with optional checks + args: + - --rcfile=.pylintrc + - --exit-zero + verbose: true + additional_dependencies: &pylint_deps + - pylint-odoo==3.5.0 + - id: pylint + name: pylint with mandatory checks + args: + - --rcfile=.pylintrc-mandatory + additional_dependencies: *pylint_deps diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000000000000000000000000000000000000..5b6d4b361ace92f3877993bf2848fac190d8fab6 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,8 @@ +# Defaults for all prettier-supported languages. +# Prettier will complete this with settings from .editorconfig file. +bracketSpacing: false +printWidth: 88 +proseWrap: always +semi: true +trailingComma: "es5" +xmlWhitespaceSensitivity: "strict" diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000000000000000000000000000000000000..dc6270e15be0c08da00e768a570f27c785d8630e --- /dev/null +++ b/.pylintrc @@ -0,0 +1,87 @@ +[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=14.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, + # 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, + 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..43ea23947166ff8080219007cfae43ec54a28f8e --- /dev/null +++ b/.pylintrc-mandatory @@ -0,0 +1,64 @@ +[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=14.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 + +[REPORTS] +msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} +output-format=colorized +reports=no diff --git a/__init__.py b/__init__.py index 2a6dab500b21763bce93a09050ba2915d9299ad4..10382a19cf8a24fef65783e672f58dd2a0344474 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,4 @@ # © 2021 Le Filament (<http://www.le-filament.com>) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from . import controllers -from . import models -from . import report -from . import wizard +from . import controllers, models, report, wizard diff --git a/__manifest__.py b/__manifest__.py index 6f9b93b3b7d432091beb569b2edeb08f7c4dbad6..c0e6d863903adda0aaddd694947671868ff44333 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -1,8 +1,7 @@ { "name": "CG SCOP - Paiements IDF", "summary": "CG SCOP - Paiements IDF", - "version": "12.0.1.1", - "development_status": "Production/Stable", + "version": "14.0.1.0.0", "author": "Le Filament", "license": "AGPL-3", "application": False, diff --git a/controllers/banner_scop_invoice_idf.py b/controllers/banner_scop_invoice_idf.py index 04ac9290fb0adaff1e5e91f41daa14aca494657f..728de90990d9519426e126992dec8b04dad15e9d 100644 --- a/controllers/banner_scop_invoice_idf.py +++ b/controllers/banner_scop_invoice_idf.py @@ -6,17 +6,16 @@ from odoo.http import request class BannerScopInvoiceIDFController(http.Controller): - - @http.route(['/cgscop_invoice_idf/header'], type="json", auth="user") + @http.route(["/cgscop_invoice_idf/header"], type="json", auth="user") def scop_invoice_idf_header(self): return { - 'html': request.env.ref( - 'cgscop_invoice_idf.header_template').render({}) + "html": request.env.ref("cgscop_invoice_idf.header_template")._render({}) } - @http.route(['/cgscop_cotisation_idf/header'], type="json", auth="user") + @http.route(["/cgscop_cotisation_idf/header"], type="json", auth="user") def scop_cotisation_idf_header(self): return { - 'html': request.env.ref( - 'cgscop_invoice_idf.header_template_cotisations').render({}) + "html": request.env.ref( + "cgscop_invoice_idf.header_template_cotisations" + )._render({}) } diff --git a/models/res_company.py b/models/res_company.py old mode 100755 new mode 100644 index 3ba2d2de33d33b4a3e24d9f125918f7390e5e1a5..57691758139a3716ad456f5ef48abc5402df7a8c --- a/models/res_company.py +++ b/models/res_company.py @@ -5,8 +5,6 @@ from odoo import fields, models class ScopInvoiceIDFCompany(models.Model): - _inherit = 'res.company' + _inherit = "res.company" - is_invoice_idf = fields.Boolean( - string='Gestion des paiements IDF' - ) + is_invoice_idf = fields.Boolean(string="Gestion des paiements IDF") diff --git a/models/res_config_settings.py b/models/res_config_settings.py old mode 100755 new mode 100644 index 94030fb9afe9491542b36c633138cf3dd66b1ed1..75d9f8f60f7cb078d47410d70a94e03ea2e5f4d8 --- a/models/res_config_settings.py +++ b/models/res_config_settings.py @@ -5,11 +5,9 @@ from odoo import fields, models class ScopInvoiceIDFConfigSettings(models.TransientModel): - _inherit = 'res.config.settings' + _inherit = "res.config.settings" - is_invoice_idf = fields.Boolean( - related="company_id.is_invoice_idf", - readonly=False) + is_invoice_idf = fields.Boolean(related="company_id.is_invoice_idf", readonly=False) # TODO: Remove during migration 14.0 def execute(self): @@ -19,8 +17,7 @@ class ScopInvoiceIDFConfigSettings(models.TransientModel): """ res = super(ScopInvoiceIDFConfigSettings, self).execute() - menu_invoice_idf = self.env.ref( - 'cgscop_invoice_idf.menu_scop_invoice_idf') + menu_invoice_idf = self.env.ref("cgscop_invoice_idf.menu_scop_invoice_idf") bool_condition = self.is_invoice_idf diff --git a/models/scop_cotisation_idf.py b/models/scop_cotisation_idf.py index e6ed81f477436ce1ac725ed19fa79930f8c4b16d..7e72558c694499b323a7ff2a3fca59459a914fa9 100644 --- a/models/scop_cotisation_idf.py +++ b/models/scop_cotisation_idf.py @@ -1,7 +1,7 @@ # © 2021 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 +from odoo import api, fields, models class ScopCotisationsIDF(models.Model): @@ -11,79 +11,90 @@ class ScopCotisationsIDF(models.Model): # ------------------------------------------------------ # Fields declaration # ------------------------------------------------------ - name = fields.Char('Libellé') + name = fields.Char("Libellé") company_id = fields.Many2one( - comodel_name='res.company', - string='Société', + comodel_name="res.company", + string="Société", ) partner_id = fields.Many2one( - comodel_name='res.partner', - string='Adhérent', + comodel_name="res.partner", + string="Adhérent", required=True, - domain=[('is_company', '=', True)]) - journal = fields.Char('Journal') - writing_date = fields.Date('Date écriture') - piece = fields.Integer('Numéro de pièce comptable') - year = fields.Char('Année') - type = fields.Selection([ - ('inv', 'Cotisation'), - ('refund', 'Avoir'), - ('reject', 'Rejet'), - ('pay', 'Paiement')], - string='Type', required=True) - lettrage = fields.Char('Référence de lettrage') + domain=[("is_company", "=", True)], + ) + journal = fields.Char("Journal") + writing_date = fields.Date("Date écriture") + piece = fields.Integer("Numéro de pièce comptable") + year = fields.Char("Année") + type = fields.Selection( + [ + ("inv", "Cotisation"), + ("refund", "Avoir"), + ("reject", "Rejet"), + ("pay", "Paiement"), + ], + string="Type", + required=True, + ) + lettrage = fields.Char("Référence de lettrage") currency_id = fields.Many2one( - comodel_name='res.currency', - string='Monnaie', - related='company_id.currency_id') - debit = fields.Monetary(string='Débit', currency_field='currency_id') - credit = fields.Monetary(string='Crédit', currency_field='currency_id') + comodel_name="res.currency", + string="Monnaie", + related="company_id.currency_id", + ) + debit = fields.Monetary(string="Débit", currency_field="currency_id") + credit = fields.Monetary(string="Crédit", currency_field="currency_id") amount_residual = fields.Monetary( - string='Reste à payer', - compute='_compute_amount_residual', - store=True, currency_field='currency_id') + string="Reste à payer", + compute="_compute_amount_residual", + store=True, + currency_field="currency_id", + ) state = fields.Selection( - string='Etat', - selection=[('no_invoice', 'Pas de cotisation associée'), - ('awaiting_payments', 'En attente de paiements'), - ('overpaid', 'La somme des paiements est supérieure au ' - 'montant de la cotisation'), - ('paid', 'La cotisation a bien été réglée !')], + string="Etat", + selection=[ + ("no_invoice", "Pas de cotisation associée"), + ("awaiting_payments", "En attente de paiements"), + ( + "overpaid", + "La somme des paiements est supérieure au " "montant de la cotisation", + ), + ("paid", "La cotisation a bien été réglée !"), + ], required=False, - search='_search_state', - compute='_compute_state') + search="_search_state", + compute="_compute_state", + ) invoice_id = fields.Many2one( - comodel_name='scop.cotisation.idf', - string='Cotisation') + comodel_name="scop.cotisation.idf", string="Cotisation" + ) payments_ids = fields.One2many( - comodel_name='scop.cotisation.idf', - inverse_name='invoice_id', - string='Paiements / Avoirs', - required=False, readonly=True) + comodel_name="scop.cotisation.idf", + inverse_name="invoice_id", + string="Paiements / Avoirs", + required=False, + readonly=True, + ) exoneration = fields.Boolean( - string='Exonération', - compute='_compute_exoneration', - store=True) - cotiz_zero = fields.Boolean(string='Non appelée') + string="Exonération", compute="_compute_exoneration", store=True + ) + cotiz_zero = fields.Boolean(string="Non appelée") # ------------------------------------------------------ # Computed fields # ------------------------------------------------------ - @api.depends('payments_ids', 'debit', - 'payments_ids.debit', 'payments_ids.debit') - @api.multi + @api.depends("payments_ids", "debit", "payments_ids.debit", "payments_ids.debit") def _compute_amount_residual(self): for r in self: - if r.type == 'inv': + if r.type == "inv": if r.payments_ids: - amount_paid = sum(r.payments_ids.mapped('credit')) + amount_paid = sum(r.payments_ids.mapped("credit")) r.amount_residual = r.debit - amount_paid else: r.amount_residual = r.debit - @api.multi def _compute_state(self): """ A line can be in different states : @@ -95,45 +106,41 @@ class ScopCotisationsIDF(models.Model): def which_state(amount_residual): if amount_residual > 0: - return 'awaiting_payments' + return "awaiting_payments" elif amount_residual < 0: - return 'overpaid' + return "overpaid" else: - return 'paid' + return "paid" for r in self: - if r.type != 'inv': + if r.type != "inv": if not r.invoice_id: - r.state = 'no_invoice' + r.state = "no_invoice" else: r.state = which_state(r.invoice_id.amount_residual) else: r.state = which_state(r.amount_residual) - @api.multi def _search_state(self, operator, value): recs = None - if operator == '=': + if operator == "=": recs = self.search([]).filtered(lambda x: x.state == value) - elif operator == 'in': + elif operator == "in": recs = self.search([]).filtered(lambda x: x.state in value) - elif operator == '!=': + elif operator == "!=": recs = self.search([]).filtered(lambda x: x.state != value) - elif operator == 'not in': + elif operator == "not in": recs = self.search([]).filtered(lambda x: x.state not in value) if recs: - return [('id', 'in', [x.id for x in recs])] + return [("id", "in", [x.id for x in recs])] - @api.multi - @api.depends('type', 'invoice_id', - 'payments_ids', 'payments_ids.type') + @api.depends("type", "invoice_id", "payments_ids", "payments_ids.type") def _compute_exoneration(self): for r in self: - if r.type == 'refund': + if r.type == "refund": r.exoneration = True - if r.type == 'inv': - exos = r.payments_ids.filtered( - lambda l: l.exoneration is True) + if r.type == "inv": + exos = r.payments_ids.filtered(lambda l: l.exoneration is True) if exos: r.exoneration = True @@ -145,87 +152,84 @@ class ScopCotisationsIDF(models.Model): Link payments and invoices if same letter for a given year :return: """ - years = set(self.mapped('year')) - partners = self.mapped('partner_id') + years = set(self.mapped("year")) + partners = self.mapped("partner_id") for year in years: for partner in partners: - invoice_lines = self.search([ - ['year', '=', year], - ['partner_id', '=', partner.id], - ['type', '=', 'inv']]) - letters = set(invoice_lines.mapped('lettrage')) - lines_to_lettre = self.search([ - ['year', '=', year], - ['partner_id', '=', partner.id], - ['type', '!=', 'inv']]) + invoice_lines = self.search( + [ + ["year", "=", year], + ["partner_id", "=", partner.id], + ["type", "=", "inv"], + ] + ) + letters = set(invoice_lines.mapped("lettrage")) + lines_to_lettre = self.search( + [ + ["year", "=", year], + ["partner_id", "=", partner.id], + ["type", "!=", "inv"], + ] + ) for line in lines_to_lettre: if line.lettrage in letters: line.invoice_id = invoice_lines.filtered( - lambda l: l.lettrage == line.lettrage)[0] + lambda l: l.lettrage == line.lettrage + )[0] def action_open_payment(self): return { - 'type': 'ir.actions.act_window', - 'name': 'Paiement / Avoir', - 'views': [ - [self.env.ref( - 'cgscop_invoice_idf.view_scop_cotisation_idf_form').id, - "form"] + "type": "ir.actions.act_window", + "name": "Paiement / Avoir", + "views": [ + [ + self.env.ref("cgscop_invoice_idf.view_scop_cotisation_idf_form").id, + "form", + ] ], - 'view_mode': 'form', - 'res_model': 'scop.cotisation.idf', - 'target': 'current', - 'res_id': self.id + "view_mode": "form", + "res_model": "scop.cotisation.idf", + "target": "current", + "res_id": self.id, } def action_free_payment(self): self.invoice_id = None def action_show_payments(self): - form_view = self.env.ref( - 'cgscop_invoice_idf.view_scop_cotisation_idf_form').id - tree_view = self.env.ref( - 'cgscop_invoice_idf.view_scop_cotisation_idf_tree').id + form_view = self.env.ref("cgscop_invoice_idf.view_scop_cotisation_idf_form").id + tree_view = self.env.ref("cgscop_invoice_idf.view_scop_cotisation_idf_tree").id return { - 'type': 'ir.actions.act_window', - 'name': 'Paiement / Avoir', - 'views': [ - [tree_view, "tree"], [form_view, "form"] - ], - 'view_mode': 'form', - 'res_model': 'scop.cotisation.idf', - 'target': 'current', - 'domain': [('id', 'in', self.payments_ids.ids)] + "type": "ir.actions.act_window", + "name": "Paiement / Avoir", + "views": [[tree_view, "tree"], [form_view, "form"]], + "view_mode": "form", + "res_model": "scop.cotisation.idf", + "target": "current", + "domain": [("id", "in", self.payments_ids.ids)], } def action_show_invoice(self): - form_view = self.env.ref( - 'cgscop_invoice_idf.view_scop_cotisation_idf_form').id + form_view = self.env.ref("cgscop_invoice_idf.view_scop_cotisation_idf_form").id return { - 'type': 'ir.actions.act_window', - 'name': 'Cotisation', - 'views': [ - [form_view, "form"] - ], - 'view_mode': 'form', - 'res_model': 'scop.cotisation.idf', - 'target': 'current', - 'res_id': self.invoice_id.id + "type": "ir.actions.act_window", + "name": "Cotisation", + "views": [[form_view, "form"]], + "view_mode": "form", + "res_model": "scop.cotisation.idf", + "target": "current", + "res_id": self.invoice_id.id, } def action_show_partner_contribution_idf(self): - form_view = self.env.ref( - 'cgscop_invoice_idf.view_scop_cotisation_idf_form').id - tree_view = self.env.ref( - 'cgscop_invoice_idf.view_scop_cotisation_idf_tree').id + form_view = self.env.ref("cgscop_invoice_idf.view_scop_cotisation_idf_form").id + tree_view = self.env.ref("cgscop_invoice_idf.view_scop_cotisation_idf_tree").id return { - 'type': 'ir.actions.act_window', - 'name': 'Cotisations IDF - ' + self.partner_id.name, - 'views': [ - [tree_view, "tree"], [form_view, "form"] - ], - 'view_mode': 'form', - 'res_model': 'scop.cotisation.idf', - 'target': 'current', - 'domain': [('partner_id', '=', self.partner_id.id)] + "type": "ir.actions.act_window", + "name": "Cotisations IDF - " + self.partner_id.name, + "views": [[tree_view, "tree"], [form_view, "form"]], + "view_mode": "form", + "res_model": "scop.cotisation.idf", + "target": "current", + "domain": [("partner_id", "=", self.partner_id.id)], } diff --git a/models/scop_invoice_idf.py b/models/scop_invoice_idf.py index a8cc05dfa599e594069ced750823a33f2c7fda14..fd82962afd61307358f4443620c8f380c27b4c6e 100644 --- a/models/scop_invoice_idf.py +++ b/models/scop_invoice_idf.py @@ -1,7 +1,7 @@ # © 2021 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 +from odoo import api, fields, models class ScopInvoiceIDF(models.Model): @@ -11,76 +11,86 @@ class ScopInvoiceIDF(models.Model): # ------------------------------------------------------ # Fields declaration # ------------------------------------------------------ - name = fields.Char('Libellé') + name = fields.Char("Libellé") company_id = fields.Many2one( - comodel_name='res.company', - string='Société', + comodel_name="res.company", + string="Société", ) partner_id = fields.Many2one( - comodel_name='res.partner', - string='Adhérent', + comodel_name="res.partner", + string="Adhérent", required=True, - domain=[('is_company', '=', True)]) - journal = fields.Char('Journal') - writing_date = fields.Date('Date écriture') - acc_doc = fields.Char('Numéro de pièce comptable') - year = fields.Char('Année') - type = fields.Selection([ - ('inv', 'Facture'), - ('refund', 'Avoir'), - ('reject', 'Rejet'), - ('pay', 'Paiement')], - string='Type', required=True) - lettrage = fields.Char('Référence de lettrage') + domain=[("is_company", "=", True)], + ) + journal = fields.Char("Journal") + writing_date = fields.Date("Date écriture") + acc_doc = fields.Char("Numéro de pièce comptable") + year = fields.Char("Année") + type = fields.Selection( + [ + ("inv", "Facture"), + ("refund", "Avoir"), + ("reject", "Rejet"), + ("pay", "Paiement"), + ], + string="Type", + required=True, + ) + lettrage = fields.Char("Référence de lettrage") currency_id = fields.Many2one( - comodel_name='res.currency', - string='Monnaie', - related='company_id.currency_id') - debit = fields.Monetary(string='Débit', currency_field='currency_id') - credit = fields.Monetary(string='Crédit', currency_field='currency_id') + comodel_name="res.currency", + string="Monnaie", + related="company_id.currency_id", + ) + debit = fields.Monetary(string="Débit", currency_field="currency_id") + credit = fields.Monetary(string="Crédit", currency_field="currency_id") amount_residual = fields.Float( - string='Reste à payer', - compute='_compute_amount_residual', - store=True, currency_field='currency_id' + string="Reste à payer", + compute="_compute_amount_residual", + store=True, + currency_field="currency_id", ) state = fields.Selection( - string='Etat', - selection=[('no_invoice', 'Pas de facture associée'), - ('awaiting_payments', 'En attente de paiements'), - ('overpaid', 'La somme des paiements est supérieure au ' - 'montant de la facture'), - ('paid', 'La facture a bien été réglée !')], + string="Etat", + selection=[ + ("no_invoice", "Pas de facture associée"), + ("awaiting_payments", "En attente de paiements"), + ( + "overpaid", + "La somme des paiements est supérieure au " "montant de la facture", + ), + ("paid", "La facture a bien été réglée !"), + ], required=False, - search='_search_state', - compute='_compute_state') + search="_search_state", + compute="_compute_state", + ) invoice_id = fields.Many2one( - comodel_name='scop.invoice.idf', - string='Facture', + comodel_name="scop.invoice.idf", + string="Facture", ) payments_ids = fields.One2many( - comodel_name='scop.invoice.idf', - inverse_name='invoice_id', - string='Paiements / Avoirs', - required=False, readonly=True) + comodel_name="scop.invoice.idf", + inverse_name="invoice_id", + string="Paiements / Avoirs", + required=False, + readonly=True, + ) # ------------------------------------------------------ # Computed fields # ------------------------------------------------------ - @api.depends('payments_ids', 'debit', - 'payments_ids.debit', 'payments_ids.debit') - @api.multi + @api.depends("payments_ids", "debit", "payments_ids.debit", "payments_ids.debit") def _compute_amount_residual(self): for r in self: - if r.type == 'inv': + if r.type == "inv": if r.payments_ids: - amount_paid = sum( - r.payments_ids.mapped('credit')) + amount_paid = sum(r.payments_ids.mapped("credit")) r.amount_residual = r.debit - amount_paid else: r.amount_residual = r.debit - @api.multi def _compute_state(self): """ A line can be in different states : @@ -89,36 +99,36 @@ class ScopInvoiceIDF(models.Model): - Some payments are missing - The invoice has been paid """ + def which_state(amount_residual): if amount_residual > 0: - return 'awaiting_payments' + return "awaiting_payments" elif amount_residual < 0: - return 'overpaid' + return "overpaid" else: - return 'paid' + return "paid" for r in self: - if r.type != 'inv': + if r.type != "inv": if not r.invoice_id: - r.state = 'no_invoice' + r.state = "no_invoice" else: r.state = which_state(r.invoice_id.amount_residual) else: r.state = which_state(r.amount_residual) - @api.multi def _search_state(self, operator, value): recs = None - if operator == '=': + if operator == "=": recs = self.search([]).filtered(lambda x: x.state == value) - elif operator == 'in': + elif operator == "in": recs = self.search([]).filtered(lambda x: x.state in value) - elif operator == '!=': + elif operator == "!=": recs = self.search([]).filtered(lambda x: x.state != value) - elif operator == 'not in': + elif operator == "not in": recs = self.search([]).filtered(lambda x: x.state not in value) if recs: - return [('id', 'in', [x.id for x in recs])] + return [("id", "in", [x.id for x in recs])] # ------------------------------------------------------ # Actions @@ -128,87 +138,84 @@ class ScopInvoiceIDF(models.Model): Link payments and invoices if same letter for a given year :return: """ - years = set(self.mapped('year')) - partners = self.mapped('partner_id') + years = set(self.mapped("year")) + partners = self.mapped("partner_id") for year in years: for partner in partners: - invoice_lines = self.search([ - ['year', '=', year], - ['partner_id', '=', partner.id], - ['type', '=', 'inv']]) - letters = set(invoice_lines.mapped('lettrage')) - lines_to_lettre = self.search([ - ['year', '=', year], - ['partner_id', '=', partner.id], - ['type', '!=', 'inv']]) + invoice_lines = self.search( + [ + ["year", "=", year], + ["partner_id", "=", partner.id], + ["type", "=", "inv"], + ] + ) + letters = set(invoice_lines.mapped("lettrage")) + lines_to_lettre = self.search( + [ + ["year", "=", year], + ["partner_id", "=", partner.id], + ["type", "!=", "inv"], + ] + ) for line in lines_to_lettre: if line.lettrage in letters: line.invoice_id = invoice_lines.filtered( - lambda l: l.lettrage == line.lettrage)[0] + lambda l: l.lettrage == line.lettrage + )[0] def action_open_payment(self): return { - 'type': 'ir.actions.act_window', - 'name': 'Paiement / Avoir', - 'views': [ - [self.env.ref( - 'cgscop_invoice_idf.view_scop_invoice_idf_form').id, - "form"] + "type": "ir.actions.act_window", + "name": "Paiement / Avoir", + "views": [ + [ + self.env.ref("cgscop_invoice_idf.view_scop_invoice_idf_form").id, + "form", + ] ], - 'view_mode': 'form', - 'res_model': 'scop.invoice.idf', - 'target': 'current', - 'res_id': self.id + "view_mode": "form", + "res_model": "scop.invoice.idf", + "target": "current", + "res_id": self.id, } def action_free_payment(self): self.invoice_id = None def action_show_payments(self): - form_view = self.env.ref( - 'cgscop_invoice_idf.view_scop_invoice_idf_form').id - tree_view = self.env.ref( - 'cgscop_invoice_idf.view_scop_invoice_idf_tree').id + form_view = self.env.ref("cgscop_invoice_idf.view_scop_invoice_idf_form").id + tree_view = self.env.ref("cgscop_invoice_idf.view_scop_invoice_idf_tree").id return { - 'type': 'ir.actions.act_window', - 'name': 'Paiement / Avoir', - 'views': [ - [tree_view, "tree"], [form_view, "form"] - ], - 'view_mode': 'form', - 'res_model': 'scop.invoice.idf', - 'target': 'current', - 'domain': [('id', 'in', self.payments_ids.ids)] + "type": "ir.actions.act_window", + "name": "Paiement / Avoir", + "views": [[tree_view, "tree"], [form_view, "form"]], + "view_mode": "form", + "res_model": "scop.invoice.idf", + "target": "current", + "domain": [("id", "in", self.payments_ids.ids)], } def action_show_invoice(self): - form_view = self.env.ref( - 'cgscop_invoice_idf.view_scop_invoice_idf_form').id + form_view = self.env.ref("cgscop_invoice_idf.view_scop_invoice_idf_form").id return { - 'type': 'ir.actions.act_window', - 'name': 'Facture', - 'views': [ - [form_view, "form"] - ], - 'view_mode': 'form', - 'res_model': 'scop.invoice.idf', - 'target': 'current', - 'res_id': self.invoice_id.id + "type": "ir.actions.act_window", + "name": "Facture", + "views": [[form_view, "form"]], + "view_mode": "form", + "res_model": "scop.invoice.idf", + "target": "current", + "res_id": self.invoice_id.id, } def action_show_partner_invoice_idf(self): - form_view = self.env.ref( - 'cgscop_invoice_idf.view_scop_invoice_idf_form').id - tree_view = self.env.ref( - 'cgscop_invoice_idf.view_scop_invoice_idf_tree').id + form_view = self.env.ref("cgscop_invoice_idf.view_scop_invoice_idf_form").id + tree_view = self.env.ref("cgscop_invoice_idf.view_scop_invoice_idf_tree").id return { - 'type': 'ir.actions.act_window', - 'name': 'Factures IDF - ' + self.partner_id.name, - 'views': [ - [tree_view, "tree"], [form_view, "form"] - ], - 'view_mode': 'form', - 'res_model': 'scop.invoice.idf', - 'target': 'current', - 'domain': [('partner_id', '=', self.partner_id.id)] + "type": "ir.actions.act_window", + "name": "Factures IDF - " + self.partner_id.name, + "views": [[tree_view, "tree"], [form_view, "form"]], + "view_mode": "form", + "res_model": "scop.invoice.idf", + "target": "current", + "domain": [("partner_id", "=", self.partner_id.id)], } diff --git a/models/scop_invoice_idf_logs.py b/models/scop_invoice_idf_logs.py index 601d8dd4fbe30ba7a41b45d2ccfac2da02bd77a5..5b1a1411daa6a6acc0147560aeb672e538b49253 100644 --- a/models/scop_invoice_idf_logs.py +++ b/models/scop_invoice_idf_logs.py @@ -1,35 +1,34 @@ # © 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 pytz +from odoo import fields, models + class ScopImportIDFLogs(models.Model): - _name = 'scop.invoice.idf.logs' - _description = 'Logs des imports IDF' + _name = "scop.invoice.idf.logs" + _description = "Logs des imports IDF" # ------------------------------------------------------ # Fields # ------------------------------------------------------ - name = fields.Char(compute='_compute_name', string="Date") + name = fields.Char(compute="_compute_name", string="Date") type = fields.Selection( - string='Type', - selection=[('inv', 'Factures'), - ('cotiz', 'Cotisations')]) - logs = fields.Html( - string='Commentaires', - required=True) - success = fields.Integer('Succès') - failure = fields.Integer('Echecs') + string="Type", + selection=[("inv", "Factures"), ("cotiz", "Cotisations")], + ) + logs = fields.Html(string="Commentaires", required=True) + success = fields.Integer("Succès") + failure = fields.Integer("Echecs") # ------------------------------------------------------ # Compute # ------------------------------------------------------ - @api.multi def _compute_name(self): for r in self: - timezone = self.env.context.get('tz') or self.env.user.tz + timezone = self.env.context.get("tz") or self.env.user.tz user_tz = pytz.timezone(timezone) - r.name = "Import du %s" % pytz.utc.localize(r.create_date).\ - astimezone(user_tz).strftime("%d/%m/%Y - %-H:%M") + r.name = "Import du %s" % pytz.utc.localize(r.create_date).astimezone( + user_tz + ).strftime("%d/%m/%Y - %-H:%M") diff --git a/report/__init__.py b/report/__init__.py old mode 100755 new mode 100644 diff --git a/report/account_invoice_all.py b/report/account_invoice_all.py index bdcb4bff8c5f0a721801a89658a40b4050ff9655..1cb7af1e4b81f080ac560c7c17271d43040bfdd1 100644 --- a/report/account_invoice_all.py +++ b/report/account_invoice_all.py @@ -1,15 +1,15 @@ # © 2022 Le Filament (<http://www.le-filament.com>) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import models, api, fields +from odoo import fields, models class ScopAccountInvoiceIDFReport(models.Model): _inherit = "account.invoice.all" source = fields.Selection( - selection_add=[ - ('idf', 'IDF')]) + selection_add=[("idf", "IDF")], ondelete={"idf": "cascade"} + ) # ------------------------------------------------------ # Query @@ -17,60 +17,77 @@ class ScopAccountInvoiceIDFReport(models.Model): def _subquery(self): select_str = super(ScopAccountInvoiceIDFReport, self)._subquery() select_str += """ - UNION ALL ( + UNION ALL SELECT 'idf' as source, null as invoice_id, - writing_date as date_invoice, - writing_date as date_due, - acc_doc as number, + writing_date as invoice_date, + writing_date as invoice_date_due, + acc_doc as name, company_id, partner_id, - null as user_id, + null as invoice_user_id, debit as amount_untaxed_signed, debit as amount_total_signed, - amount_residual as residual_company_signed, - null as payment_mode_id, - 'out_invoice' as type, + amount_residual as amount_residual_signed, + 'out_invoice' as move_type, + 'posted' as state, CASE WHEN amount_residual = 0 THEN 'paid' - ELSE 'open' - END AS state + WHEN amount_residual = debit THEN 'not_paid' + ELSE 'partial' + END AS payment_state FROM scop_invoice_idf WHERE type = 'inv' - ) """ return select_str # ------------------------------------------------------ # Computed fields # ------------------------------------------------------ - @api.multi def _compute_lines(self): for inv in self: - if inv.source == 'idf': - invoice_idf_id = self.env['scop.invoice.idf'].sudo().search([ - ('partner_id', '=', inv.partner_id.id), - ('writing_date', '=', inv.date_invoice), - ('acc_doc', '=', inv.number), - ('type', '=', 'inv'), - ]) + if inv.source == "idf": + invoice_idf_id = ( + self.env["scop.invoice.idf"] + .sudo() + .search( + [ + ("partner_id", "=", inv.partner_id.id), + ("writing_date", "=", inv.date_invoice), + ("acc_doc", "=", inv.number), + ("type", "=", "inv"), + ] + ) + ) if len(invoice_idf_id) == 1: - inv.lines = "<table class='table table-hover table-striped table-sm'>" \ - "<thead class='thead-light'><tr><th>Description</th><th>Quantité</th><th>P.U</th>" \ - "<th>Total HT</th><th>Total TTC</th></tr></thead><tbody>" + \ - ''.join( - invoice_idf_id.mapped( - lambda l: ("<tr><td>" + l.name.replace( - '\n', '<br/>') + "</td>" + - "<td></td>" + - "<td>" + str( - l.debit) + "</td>" + - "<td>" + str( - l.debit) + "</td>" + - "<td>" + str( - l.debit) + "</td></tr>"))) + "</tbody></table>" + inv.lines = ( + "<table class='table table-hover table-striped table-sm'>" + "<thead class='thead-light'><tr><th>Description</th>" + "<th>Quantité</th><th>P.U</th>" + "<th>Total HT</th><th>Total TTC</th></tr></thead><tbody>" + + "".join( + invoice_idf_id.mapped( + lambda l: ( + "<tr><td>" + + l.name.replace("\n", "<br/>") + + "</td>" + + "<td></td>" + + "<td>" + + str(l.debit) + + "</td>" + + "<td>" + + str(l.debit) + + "</td>" + + "<td>" + + str(l.debit) + + "</td></tr>" + ) + ) + ) + + "</tbody></table>" + ) if not inv.lines: super(ScopAccountInvoiceIDFReport, inv)._compute_lines() diff --git a/report/scop_contribution_report.py b/report/scop_contribution_report.py index 4075c0e547defa08639a436fd2ab914d30eb83c8..f88249eea48c996ba2a84cc64f268f774d2758a4 100644 --- a/report/scop_contribution_report.py +++ b/report/scop_contribution_report.py @@ -1,23 +1,24 @@ # © 2022 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 +from odoo import fields, models class ScopContributionReport(models.Model): _inherit = "scop.contribution.report" source = fields.Selection( - selection_add=[ - ('idf', 'IDF')]) + selection_add=[("idf", "IDF")], ondelete={"idf": "cascade"} + ) # ------------------------------------------------------ # Query # ------------------------------------------------------ def _subquery(self): - contribution_ur_id = self.env.ref('cgscop_partner.riga_14399').id + contribution_ur_id = self.env.ref("cgscop_partner.riga_14399").id query = super(ScopContributionReport, self)._subquery() - query += """ + query += ( + """ UNION ALL ( SELECT 'idf' as source, @@ -32,7 +33,9 @@ class ScopContributionReport(models.Model): WHERE type = 'inv' ) - """ % contribution_ur_id + """ + % contribution_ur_id + ) return query # ------------------------------------------------------ @@ -40,22 +43,32 @@ class ScopContributionReport(models.Model): # ------------------------------------------------------ def _get_payment(self): self.ensure_one() - if self.source == 'idf': - payment_ids = self.env['scop.cotisation.idf'].sudo().search([ - ('year', '=', self.year), - ('partner_id', '=', self.partner_id.id), - ('type', '!=', 'inv'), - ]) + if self.source == "idf": + payment_ids = ( + self.env["scop.cotisation.idf"] + .sudo() + .search( + [ + ("year", "=", self.year), + ("partner_id", "=", self.partner_id.id), + ("type", "!=", "inv"), + ] + ) + ) if payment_ids: - payments = payment_ids.mapped(lambda p: { - 'date': p.writing_date, - 'name': p.name, - 'ref': p.piece, - 'credit': p.credit, - }) + payments = payment_ids.mapped( + lambda p: { + "date": p.writing_date, + "name": p.name, + "ref": p.piece, + "credit": p.credit, + } + ) payments_html = self._get_html_table(payments) else: - payments_html = "<p>Il n'y a pas de paiements associés à cette cotisation</p>" + payments_html = ( + "<p>Il n'y a pas de paiements associés à cette cotisation</p>" + ) return payments_html else: return super(ScopContributionReport, self)._get_payment() diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv index 84855a10dcf39522c3c9da9b0420776799cc5b24..e21212af103d96e5ff871bc355dec5b4c490ff18 100644 --- a/security/ir.model.access.csv +++ b/security/ir.model.access.csv @@ -2,3 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_scop_invoice_idf,access_scop_invoice_idf,model_scop_invoice_idf,cgscop_invoice_idf.group_cg_invoice_idf_admin,1,1,1,1 access_scop_cotisation_idf,access_scop_cotisation_idf,model_scop_cotisation_idf,cgscop_invoice_idf.group_cg_invoice_idf_admin,1,1,1,1 access_scop_invoice_idf_logs,access_scop_invoice_idf_logs,model_scop_invoice_idf_logs,cgscop_invoice_idf.group_cg_invoice_idf_admin,1,1,1,1 +access_scop_invoice_idf_wizard,access_scop_invoice_idf_wizard,model_scop_invoice_idf_wizard,base.group_user,1,1,1,1 diff --git a/security/security_rules.xml b/security/security_rules.xml index 831a881c547bbe7435af16df1876b0e8902c93b2..aa269ac2b3727359083f1a163aa09af78f16472f 100644 --- a/security/security_rules.xml +++ b/security/security_rules.xml @@ -1,54 +1,64 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8" ?> <!-- Copyright 2021 Le Filament License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> - <odoo> - <data noupdate="0"> + <data> <!-- Add Liasse Fiscale category --> <record model="ir.module.category" id="cgscop_invoice_idf_group"> - <field name="name">Gestion des factures et cotisations IDF</field> + <field name="name">Gestion des factures et cotisations IDF</field> + <field name="parent_id" ref="cgscop_partner.module_cgscop_category" /> </record> <!-- Add ARESCOP groups --> <record id="group_cg_invoice_idf_admin" model="res.groups"> <field name="name">Gestionnaire</field> - <field name="category_id" ref="cgscop_invoice_idf_group"/> + <field name="category_id" ref="cgscop_invoice_idf_group" /> </record> <!-- Rules --> <record id="cg_invoice_idf_rule" model="ir.rule"> - <field name="name">Imports des factures consultables que pour sa société</field> - <field name="model_id" ref="cgscop_invoice_idf.model_scop_invoice_idf"/> - <field name="groups" eval="[(6, 0, [ref('group_cg_invoice_idf_admin')])]"/> - <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> - <field name="perm_read" eval="True"/> - <field name="perm_write" eval="True"/> - <field name="perm_create" eval="True"/> - <field name="perm_unlink" eval="True"/> + <field + name="name" + >Imports des factures consultables que pour sa société</field> + <field name="model_id" ref="cgscop_invoice_idf.model_scop_invoice_idf" /> + <field name="groups" eval="[(6, 0, [ref('group_cg_invoice_idf_admin')])]" /> + <field + name="domain_force" + >['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> + <field name="perm_read" eval="True" /> + <field name="perm_write" eval="True" /> + <field name="perm_create" eval="True" /> + <field name="perm_unlink" eval="True" /> </record> <record id="cg_cotisations_idf_rule" model="ir.rule"> - <field name="name">Imports des cotisations consultables que pour sa société</field> - <field name="model_id" ref="cgscop_invoice_idf.model_scop_invoice_idf"/> - <field name="groups" eval="[(6, 0, [ref('group_cg_invoice_idf_admin')])]"/> - <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> - <field name="perm_read" eval="True"/> - <field name="perm_write" eval="True"/> - <field name="perm_create" eval="True"/> - <field name="perm_unlink" eval="True"/> + <field + name="name" + >Imports des cotisations consultables que pour sa société</field> + <field name="model_id" ref="cgscop_invoice_idf.model_scop_invoice_idf" /> + <field name="groups" eval="[(6, 0, [ref('group_cg_invoice_idf_admin')])]" /> + <field + name="domain_force" + >['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> + <field name="perm_read" eval="True" /> + <field name="perm_write" eval="True" /> + <field name="perm_create" eval="True" /> + <field name="perm_unlink" eval="True" /> </record> <record id="cg_invoice_logs_idf_rule" model="ir.rule"> <field name="name">Logs des imports consultables que pour sa société</field> - <field name="model_id" ref="cgscop_invoice_idf.model_scop_invoice_idf"/> - <field name="groups" eval="[(6, 0, [ref('group_cg_invoice_idf_admin')])]"/> - <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> - <field name="perm_read" eval="True"/> - <field name="perm_write" eval="True"/> - <field name="perm_create" eval="True"/> - <field name="perm_unlink" eval="True"/> + <field name="model_id" ref="cgscop_invoice_idf.model_scop_invoice_idf" /> + <field name="groups" eval="[(6, 0, [ref('group_cg_invoice_idf_admin')])]" /> + <field + name="domain_force" + >['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> + <field name="perm_read" eval="True" /> + <field name="perm_write" eval="True" /> + <field name="perm_create" eval="True" /> + <field name="perm_unlink" eval="True" /> </record> </data> -</odoo> \ No newline at end of file +</odoo> diff --git a/static/description/icon.png b/static/description/icon.png index 82ef47760a441cf229b5009f0a18ccf3842fbfa5..499652a2c9c0307f875bafc145b6fbafa133da72 100644 Binary files a/static/description/icon.png and b/static/description/icon.png differ diff --git a/templates/scop_invoice_idf_header_template.xml b/templates/scop_invoice_idf_header_template.xml index 54427bcaa6291de20285411f9edf7781e10b7b7a..caff4c27618b08a7e4fcde0ea5c76c664a4468bd 100644 --- a/templates/scop_invoice_idf_header_template.xml +++ b/templates/scop_invoice_idf_header_template.xml @@ -1,7 +1,6 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8" ?> <!-- Copyright 2021 Le Filament License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> - <odoo> <template id="header_template" name="cgscop_invoice_idf.header_template"> @@ -9,11 +8,15 @@ <div class="container"> <div class="row"> <div class="col-12"> - <span style="font-size: 1.4rem; margin-right: 32px;"><strong>Importer : </strong></span> - <a type="action" - data-method="scop_invoice_idf_wizard_action" - data-model="scop.invoice.idf.wizard" - class="btn btn-secondary" style="margin-right: 32px;"> + <span style="font-size: 1.4rem; margin-right: 32px;"><strong + >Importer : </strong></span> + <a + type="action" + data-method="scop_invoice_idf_wizard_action" + data-model="scop.invoice.idf.wizard" + class="btn btn-secondary" + style="margin-right: 32px;" + > <u>Factures</u> </a> </div> @@ -22,16 +25,23 @@ </div> </template> - <template id="header_template_cotisations" name="cgscop_cotisation_idf.header_template"> + <template + id="header_template_cotisations" + name="cgscop_cotisation_idf.header_template" + > <div id="lm" style="background-color: #fff; padding: 16px 0"> <div class="container"> <div class="row"> <div class="col-12"> - <span style="font-size: 1.4rem; margin-right: 32px;"><strong>Importer : </strong></span> - <a type="action" - data-method="scop_cotisation_idf_wizard_action" - data-model="scop.invoice.idf.wizard" - class="btn btn-secondary" style="margin-right: 32px;"> + <span style="font-size: 1.4rem; margin-right: 32px;"><strong + >Importer : </strong></span> + <a + type="action" + data-method="scop_cotisation_idf_wizard_action" + data-model="scop.invoice.idf.wizard" + class="btn btn-secondary" + style="margin-right: 32px;" + > <u>Cotisations</u> </a> </div> diff --git a/views/res_config_settings.xml b/views/res_config_settings.xml old mode 100755 new mode 100644 index a5eee2f390dd838e45ab8a2f71c676dca2d05151..5c32c89ac420e7fda17df3ad617392f6a93a4120 --- a/views/res_config_settings.xml +++ b/views/res_config_settings.xml @@ -1,25 +1,29 @@ -<?xml version="1.0" encoding="UTF-8"?> +<?xml version="1.0" encoding="UTF-8" ?> <odoo> <data> <record id="res_config_settings_view_form_cotisations" model="ir.ui.view"> <field name="name">res.config.settings.view.form.inherit.cotisations</field> <field name="model">res.config.settings</field> - <field name="priority" eval="90"/> - <field name="inherit_id" ref="base.res_config_settings_view_form"/> + <field name="priority" eval="90" /> + <field name="inherit_id" ref="base.res_config_settings_view_form" /> <field name="arch" type="xml"> <xpath expr="//div[hasclass('settings')]" position="inside"> - <div class="app_settings_block" data-string="Paiements IDF" - string="Paiements IDF" data-key="cgscop_invoice_idf" - icon="cgscop_invoice_idf,static/description/icon.png"> + <div + class="app_settings_block" + data-string="Paiements IDF" + string="Paiements IDF" + data-key="cgscop_invoice_idf" + icon="cgscop_invoice_idf,static/description/icon.png" + > <h2>Paiements IDF</h2> <div class="row mt16 o_settings_container" name="contribution"> <div class="col-xs-12 col-md-6 o_setting_box"> <div class="o_setting_left_pane"> - <field name="is_invoice_idf"/> + <field name="is_invoice_idf" /> </div> <div class="o_setting_right_pane"> - <label for="is_invoice_idf"/> + <label for="is_invoice_idf" /> <div class="text-muted"> Activer la gestion des paiements IDF </div> @@ -32,4 +36,4 @@ </record> </data> -</odoo> \ No newline at end of file +</odoo> diff --git a/views/scop_cotisation_idf.xml b/views/scop_cotisation_idf.xml index 00e52bfd54dfe87dade7fa3bb2cd434024883a12..4792a2ebc9ddc2f8bf752c905065364622c9812c 100644 --- a/views/scop_cotisation_idf.xml +++ b/views/scop_cotisation_idf.xml @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8" ?> <odoo> <data> @@ -6,8 +6,8 @@ <record id="action_server_scop_cotisation_idf" model="ir.actions.server"> <field name="name">Lettrage</field> <field name="type">ir.actions.server</field> - <field name="model_id" ref="model_scop_cotisation_idf"/> - <field name="binding_model_id" ref="model_scop_cotisation_idf"/> + <field name="model_id" ref="model_scop_cotisation_idf" /> + <field name="binding_model_id" ref="model_scop_cotisation_idf" /> <field name="state">code</field> <field name="code"> if records: @@ -20,26 +20,29 @@ <field name="name">scop.cotisation.idf.tree</field> <field name="model">scop.cotisation.idf</field> <field name="arch" type="xml"> - <tree string="Cotisations IDF" - banner_route="/cgscop_cotisation_idf/header" - decoration-danger="state in ['no_invoice', 'overpaid']" - decoration-info="state == 'awaiting_payments'" - import='False' default_order='writing_date desc'> - <field name="state" invisible="True"/> - <field name="writing_date"/> - <field name="name"/> - <field name="partner_id"/> - <field name="year"/> - <field name="type"/> - <field name="currency_id" invisible="1"/> - <field name="debit" widget="monetary"/> - <field name="credit" widget="monetary"/> - <field name='amount_residual' widget="monetary"/> - <field name='invoice_id'/> - <field name='payments_ids'/> - <field name='lettrage'/> - <field name='exoneration'/> - <field name='cotiz_zero'/> + <tree + string="Cotisations IDF" + banner_route="/cgscop_cotisation_idf/header" + decoration-danger="state in ['no_invoice', 'overpaid']" + decoration-info="state == 'awaiting_payments'" + import='False' + default_order='writing_date desc' + > + <field name="state" invisible="True" /> + <field name="writing_date" /> + <field name="name" /> + <field name="partner_id" /> + <field name="year" /> + <field name="type" /> + <field name="currency_id" invisible="1" /> + <field name="debit" widget="monetary" /> + <field name="credit" widget="monetary" /> + <field name='amount_residual' widget="monetary" /> + <field name='invoice_id' /> + <field name='payments_ids' /> + <field name='lettrage' /> + <field name='exoneration' /> + <field name='cotiz_zero' /> </tree> </field> </record> @@ -52,66 +55,129 @@ <form string="Cotisations IDF"> <sheet> <div class="oe_button_box" name="button_box"> - <button name="action_show_partner_contribution_idf" type="object" class="oe_stat_button" icon="fa-puzzle-piece">En cours</button> - <button name="action_show_payments" type="object" class="oe_stat_button" icon="fa-eur" - attrs="{'invisible': ['|', ('type', '!=', 'inv'), ('payments_ids', '=', False)]}">Paiements</button> - <button name="action_show_invoice" type="object" class="oe_stat_button" icon="fa-calculator" - attrs="{'invisible': ['|', ('type', '=', 'inv'), ('invoice_id', '=', False)]}">Cotisation</button> + <button + name="action_show_partner_contribution_idf" + type="object" + class="oe_stat_button" + icon="fa-puzzle-piece" + >En cours</button> + <button + name="action_show_payments" + type="object" + class="oe_stat_button" + icon="fa-eur" + attrs="{'invisible': ['|', ('type', '!=', 'inv'), ('payments_ids', '=', False)]}" + >Paiements</button> + <button + name="action_show_invoice" + type="object" + class="oe_stat_button" + icon="fa-calculator" + attrs="{'invisible': ['|', ('type', '=', 'inv'), ('invoice_id', '=', False)]}" + >Cotisation</button> </div> <div class="oe_title"> <h1> - <field name="type"/> + <field name="type" /> </h1> </div> - <div class="alert alert-danger" role="alert" attrs="{'invisible': [('state', 'not in', ('no_invoice','overpaid'))]}"> - <field class="o_field_header" name="state" readonly="1"/> + <div + class="alert alert-danger" + role="alert" + attrs="{'invisible': [('state', 'not in', ('no_invoice','overpaid'))]}" + > + <field class="o_field_header" name="state" readonly="1" /> </div> - <div class="alert alert-info" role="alert" attrs="{'invisible': [('state', '!=', 'awaiting_payments')]}"> - <field class="o_field_header" name="state" readonly="1"/> + <div + class="alert alert-info" + role="alert" + attrs="{'invisible': [('state', '!=', 'awaiting_payments')]}" + > + <field class="o_field_header" name="state" readonly="1" /> </div> - <div class="alert alert-success" role="alert" attrs="{'invisible': [('state', '!=', 'paid')]}"> - <field class="o_field_header" name="state" readonly="1"/> + <div + class="alert alert-success" + role="alert" + attrs="{'invisible': [('state', '!=', 'paid')]}" + > + <field class="o_field_header" name="state" readonly="1" /> </div> <group col="2"> <group name="partner" string="Adhérent"> - <field name="partner_id" options="{'no_create': True, 'no_edit': True}"/> - <field name="year"/> - <field name="exoneration"/> - <field name="cotiz_zero"/> + <field + name="partner_id" + options="{'no_create': True, 'no_edit': True}" + /> + <field name="year" /> + <field name="exoneration" /> + <field name="cotiz_zero" /> </group> <group name="cotiz" string="Cotisation"> - <field name="name"/> - <field name="currency_id" invisible="1"/> - <field name="credit" widget="monetary" string="Montant" attrs="{'invisible': [('type', '=', 'inv')]}"/> - <field name="debit" widget="monetary" string="Montant" attrs="{'invisible': [('type', '!=', 'inv')]}"/> - <field name="amount_residual" widget="monetary" attrs="{'invisible': [('type', '!=', 'inv')]}"/> - <field name="invoice_id" - domain="[('partner_id', '=', partner_id), ('type', '=', 'inv')]" - options="{'no_create': True, 'no_edit': True}" - attrs="{'invisible': [('type', '=', 'inv')]}" - help="Les cotisations disponibles sont celles liées à la coop."/> - <field name="lettrage"/> + <field name="name" /> + <field name="currency_id" invisible="1" /> + <field + name="credit" + widget="monetary" + string="Montant" + attrs="{'invisible': [('type', '=', 'inv')]}" + /> + <field + name="debit" + widget="monetary" + string="Montant" + attrs="{'invisible': [('type', '!=', 'inv')]}" + /> + <field + name="amount_residual" + widget="monetary" + attrs="{'invisible': [('type', '!=', 'inv')]}" + /> + <field + name="invoice_id" + domain="[('partner_id', '=', partner_id), ('type', '=', 'inv')]" + options="{'no_create': True, 'no_edit': True}" + attrs="{'invisible': [('type', '=', 'inv')]}" + help="Les cotisations disponibles sont celles liées à la coop." + /> + <field name="lettrage" /> </group> </group> <group col="1"> <group> - <field name="payments_ids" - attrs="{'invisible': [('type', '!=', 'inv')]}" - style="pointer-events: none;"> - <tree editable="top" default_order="writing_date desc"> - <field name="name"/> - <field name="writing_date"/> - <field name="type"/> - <field name="currency_id" invisible="1"/> - <field name="credit" widget="monetary" string="Montant"/> - <field name='lettrage'/> - <field name='exoneration'/> - <button name="action_open_payment" type="object" - string="Paiement / Avoir" icon="fa-eye" - style="pointer-events: visible;"/> - <button name="action_free_payment" type="object" - string="Détacher le paiement" icon="fa-times" - style="pointer-events: visible;"/> + <field + name="payments_ids" + attrs="{'invisible': [('type', '!=', 'inv')]}" + style="pointer-events: none;" + > + <tree + editable="top" + default_order="writing_date desc" + > + <field name="name" /> + <field name="writing_date" /> + <field name="type" /> + <field name="currency_id" invisible="1" /> + <field + name="credit" + widget="monetary" + string="Montant" + /> + <field name='lettrage' /> + <field name='exoneration' /> + <button + name="action_open_payment" + type="object" + string="Paiement / Avoir" + icon="fa-eye" + style="pointer-events: visible;" + /> + <button + name="action_free_payment" + type="object" + string="Détacher le paiement" + icon="fa-times" + style="pointer-events: visible;" + /> </tree> </field> </group> @@ -127,8 +193,8 @@ <field name="model">scop.cotisation.idf</field> <field name="arch" type="xml"> <graph string="Cotisations IDF"> - <field name="year" type="row"/> - <field name="credit" type="measure"/> + <field name="year" type="row" /> + <field name="credit" type="measure" /> </graph> </field> </record> @@ -139,9 +205,9 @@ <field name="model">scop.cotisation.idf</field> <field name="arch" type="xml"> <pivot string="Cotisations IDF" display_quantity="True"> - <field name="partner_id" type="row"/> - <field name="currency_id" invisible="1"/> - <field name="amount_residual" type="measure" widget="monetary"/> + <field name="partner_id" type="row" /> + <field name="currency_id" invisible="1" /> + <field name="amount_residual" type="measure" widget="monetary" /> </pivot> </field> </record> @@ -152,26 +218,74 @@ <field name="model">scop.cotisation.idf</field> <field name="arch" type="xml"> <search string="Paiements IDF"> - <field name="partner_id"/> - <field name="name"/> - <field name="year"/> - <filter name="current_year" string="Année en cours" domain="[('year', '=', (context_today()).strftime('%Y'))]"/> - <separator/> - <filter name="with_issue" domain="[('state','in',['no_invoice', 'overpaid'])]" string="Erreur lettrage et/ou montant"/> - <filter name="without_invoice" domain="[('type','!=','inv'),('state','in',['no_invoice'])]" string="Paiements orphelins"/> - <separator/> - <filter name="is_type_invoice" domain="[('type','=','inv')]" string="Cotisations"/> - <filter name="cotiz_paid" domain="[('type','=','inv'),('state','=','paid')]" string="Cotisations payées"/> - <filter name="cotiz_unpaid" domain="[('type','=','inv'),('state','=','awaiting_payments')]" string="Cotisations reste à payées"/> - <filter name="cotiz_with_issue" domain="[('type','=','inv'),('state','=','overpaid')]" string="Cotisations en erreur"/> - <separator/> - <filter name="cotiz_exo" domain="[('exoneration','=',True)]" string="Exonérations"/> - <filter name="cotiz_not_called" domain="[('type','=','inv'),('cotiz_zero','=',True)]" string="Cotisations non appelées"/> + <field name="partner_id" /> + <field name="name" /> + <field name="year" /> + <filter + name="current_year" + string="Année en cours" + domain="[('year', '=', (context_today()).strftime('%Y'))]" + /> + <separator /> + <filter + name="with_issue" + domain="[('state','in',['no_invoice', 'overpaid'])]" + string="Erreur lettrage et/ou montant" + /> + <filter + name="without_invoice" + domain="[('type','!=','inv'),('state','in',['no_invoice'])]" + string="Paiements orphelins" + /> + <separator /> + <filter + name="is_type_invoice" + domain="[('type','=','inv')]" + string="Cotisations" + /> + <filter + name="cotiz_paid" + domain="[('type','=','inv'),('state','=','paid')]" + string="Cotisations payées" + /> + <filter + name="cotiz_unpaid" + domain="[('type','=','inv'),('state','=','awaiting_payments')]" + string="Cotisations reste à payées" + /> + <filter + name="cotiz_with_issue" + domain="[('type','=','inv'),('state','=','overpaid')]" + string="Cotisations en erreur" + /> + <separator /> + <filter + name="cotiz_exo" + domain="[('exoneration','=',True)]" + string="Exonérations" + /> + <filter + name="cotiz_not_called" + domain="[('type','=','inv'),('cotiz_zero','=',True)]" + string="Cotisations non appelées" + /> <group expand="0" string="Group By"> - <filter name="group_by_partner_id" string="Adhérent" context="{'group_by':'partner_id'}"/> - <separator/> - <filter string="Date écriture" name="groupby_writing_date" context="{'group_by':'writing_date'}"/> - <filter string="Type" name="groupby_type" context="{'group_by':'type'}"/> + <filter + name="group_by_partner_id" + string="Adhérent" + context="{'group_by':'partner_id'}" + /> + <separator /> + <filter + string="Date écriture" + name="groupby_writing_date" + context="{'group_by':'writing_date'}" + /> + <filter + string="Type" + name="groupby_type" + context="{'group_by':'type'}" + /> </group> </search> </field> @@ -182,15 +296,19 @@ <field name="name">Cotisations IDF</field> <field name="res_model">scop.cotisation.idf</field> <field name="view_mode">tree,form,pivot,graph</field> - <field name="context">{'search_default_is_type_invoice': True, 'search_default_current_year': True}</field> + <field + name="context" + >{'search_default_is_type_invoice': True, 'search_default_current_year': True}</field> </record> <!-- MENUS --> - <menuitem id="menu_scop_invoice_idf_imports_cotisations" - name="Cotisations" - parent="menu_scop_invoice_idf" - sequence="20" - action="action_scop_cotisation_idf"/> + <menuitem + id="menu_scop_invoice_idf_imports_cotisations" + name="Cotisations" + parent="menu_scop_invoice_idf" + sequence="20" + action="action_scop_cotisation_idf" + /> </data> </odoo> diff --git a/views/scop_invoice_idf.xml b/views/scop_invoice_idf.xml index 1af448bf6f3e0edf0281d8e001a7599d08601575..68468815c4cd47299f3a41a1ecdfdebb8c5ed7a5 100644 --- a/views/scop_invoice_idf.xml +++ b/views/scop_invoice_idf.xml @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8" ?> <odoo> <data> @@ -6,8 +6,8 @@ <record id="action_server_scop_invoice_idf" model="ir.actions.server"> <field name="name">Lettrage</field> <field name="type">ir.actions.server</field> - <field name="model_id" ref="model_scop_invoice_idf"/> - <field name="binding_model_id" ref="model_scop_invoice_idf"/> + <field name="model_id" ref="model_scop_invoice_idf" /> + <field name="binding_model_id" ref="model_scop_invoice_idf" /> <field name="state">code</field> <field name="code"> if records: @@ -20,24 +20,27 @@ <field name="name">scop.invoice.idf.tree</field> <field name="model">scop.invoice.idf</field> <field name="arch" type="xml"> - <tree string="Paiements IDF" - banner_route="/cgscop_invoice_idf/header" - decoration-danger="state in ['no_invoice', 'overpaid']" - decoration-info="state == 'awaiting_payments'" - import='False' default_order='writing_date desc'> - <field name="state" invisible="True"/> - <field name="writing_date"/> - <field name="name"/> - <field name="partner_id"/> - <field name="year"/> - <field name="type"/> - <field name="currency_id" invisible="1"/> - <field name="credit" widget="monetary"/> - <field name="debit" widget="monetary"/> - <field name='amount_residual' widget="monetary"/> - <field name='invoice_id'/> - <field name='payments_ids'/> - <field name='lettrage'/> + <tree + string="Paiements IDF" + banner_route="/cgscop_invoice_idf/header" + decoration-danger="state in ['no_invoice', 'overpaid']" + decoration-info="state == 'awaiting_payments'" + import='False' + default_order='writing_date desc' + > + <field name="state" invisible="True" /> + <field name="writing_date" /> + <field name="name" /> + <field name="partner_id" /> + <field name="year" /> + <field name="type" /> + <field name="currency_id" invisible="1" /> + <field name="credit" widget="monetary" /> + <field name="debit" widget="monetary" /> + <field name='amount_residual' widget="monetary" /> + <field name='invoice_id' /> + <field name='payments_ids' /> + <field name='lettrage' /> </tree> </field> </record> @@ -50,63 +53,126 @@ <form string="Paiements IDF"> <sheet> <div class="oe_button_box" name="button_box"> - <button name="action_show_partner_invoice_idf" type="object" class="oe_stat_button" icon="fa-puzzle-piece">En cours</button> - <button name="action_show_payments" type="object" class="oe_stat_button" icon="fa-eur" - attrs="{'invisible': ['|', ('type', '!=', 'inv'), ('payments_ids', '=', False)]}">Paiements</button> - <button name="action_show_invoice" type="object" class="oe_stat_button" icon="fa-calculator" - attrs="{'invisible': ['|', ('type', '=', 'inv'), ('invoice_id', '=', False)]}">Facture</button> + <button + name="action_show_partner_invoice_idf" + type="object" + class="oe_stat_button" + icon="fa-puzzle-piece" + >En cours</button> + <button + name="action_show_payments" + type="object" + class="oe_stat_button" + icon="fa-eur" + attrs="{'invisible': ['|', ('type', '!=', 'inv'), ('payments_ids', '=', False)]}" + >Paiements</button> + <button + name="action_show_invoice" + type="object" + class="oe_stat_button" + icon="fa-calculator" + attrs="{'invisible': ['|', ('type', '=', 'inv'), ('invoice_id', '=', False)]}" + >Facture</button> </div> <div class="oe_title"> <h1> - <field name="type"/> + <field name="type" /> </h1> </div> - <div class="alert alert-danger" role="alert" attrs="{'invisible': [('state', 'not in', ('no_invoice','overpaid'))]}"> - <field class="o_field_header" name="state" readonly="1"/> + <div + class="alert alert-danger" + role="alert" + attrs="{'invisible': [('state', 'not in', ('no_invoice','overpaid'))]}" + > + <field class="o_field_header" name="state" readonly="1" /> </div> - <div class="alert alert-info" role="alert" attrs="{'invisible': [('state', '!=', 'awaiting_payments')]}"> - <field class="o_field_header" name="state" readonly="1"/> + <div + class="alert alert-info" + role="alert" + attrs="{'invisible': [('state', '!=', 'awaiting_payments')]}" + > + <field class="o_field_header" name="state" readonly="1" /> </div> - <div class="alert alert-success" role="alert" attrs="{'invisible': [('state', '!=', 'paid')]}"> - <field class="o_field_header" name="state" readonly="1"/> + <div + class="alert alert-success" + role="alert" + attrs="{'invisible': [('state', '!=', 'paid')]}" + > + <field class="o_field_header" name="state" readonly="1" /> </div> <group col="2"> <group name="partner" string="Adhérent"> - <field name="partner_id" options="{'no_create': True, 'no_edit': True}"/> - <field name="year"/> + <field + name="partner_id" + options="{'no_create': True, 'no_edit': True}" + /> + <field name="year" /> </group> <group name="invoice" string="Facture"> - <field name="name"/> - <field name="currency_id" invisible="1"/> - <field name="credit" widget="monetary" string="Montant" attrs="{'invisible': [('type', '=', 'inv')]}"/> - <field name="debit" widget="monetary" string="Montant" attrs="{'invisible': [('type', '!=', 'inv')]}"/> - <field name="amount_residual" widget="monetary" attrs="{'invisible': [('type', '!=', 'inv')]}"/> - <field name="invoice_id" - domain="[('partner_id', '=', partner_id), ('type', '=', 'inv')]" - options="{'no_create': True, 'no_edit': True}" - attrs="{'invisible': [('type', '=', 'inv')]}" - help="Les factures disponibles sont celles liées à la coop."/> - <field name="lettrage"/> + <field name="name" /> + <field name="currency_id" invisible="1" /> + <field + name="credit" + widget="monetary" + string="Montant" + attrs="{'invisible': [('type', '=', 'inv')]}" + /> + <field + name="debit" + widget="monetary" + string="Montant" + attrs="{'invisible': [('type', '!=', 'inv')]}" + /> + <field + name="amount_residual" + widget="monetary" + attrs="{'invisible': [('type', '!=', 'inv')]}" + /> + <field + name="invoice_id" + domain="[('partner_id', '=', partner_id), ('type', '=', 'inv')]" + options="{'no_create': True, 'no_edit': True}" + attrs="{'invisible': [('type', '=', 'inv')]}" + help="Les factures disponibles sont celles liées à la coop." + /> + <field name="lettrage" /> </group> </group> <group col="1"> <group> - <field name="payments_ids" - attrs="{'invisible': [('type', '!=', 'inv')]}" - style="pointer-events: none;"> - <tree editable="top" default_order="writing_date desc"> - <field name="name"/> - <field name="writing_date"/> - <field name="type"/> - <field name="currency_id" invisible="1"/> - <field name="credit" string="Montant" widget="monetary"/> - <field name='lettrage'/> - <button name="action_open_payment" type="object" - string="Paiement / Avoir" icon="fa-eye" - style="pointer-events: visible;"/> - <button name="action_free_payment" type="object" - string="Détacher le paiement" icon="fa-times" - style="pointer-events: visible;"/> + <field + name="payments_ids" + attrs="{'invisible': [('type', '!=', 'inv')]}" + style="pointer-events: none;" + > + <tree + editable="top" + default_order="writing_date desc" + > + <field name="name" /> + <field name="writing_date" /> + <field name="type" /> + <field name="currency_id" invisible="1" /> + <field + name="credit" + string="Montant" + widget="monetary" + /> + <field name='lettrage' /> + <button + name="action_open_payment" + type="object" + string="Paiement / Avoir" + icon="fa-eye" + style="pointer-events: visible;" + /> + <button + name="action_free_payment" + type="object" + string="Détacher le paiement" + icon="fa-times" + style="pointer-events: visible;" + /> </tree> </field> </group> @@ -122,8 +188,8 @@ <field name="model">scop.invoice.idf</field> <field name="arch" type="xml"> <graph string="Paiements IDF"> - <field name="year" type="row" interval="month"/> - <field name="credit" type="measure"/> + <field name="year" type="row" interval="month" /> + <field name="credit" type="measure" /> </graph> </field> </record> @@ -134,8 +200,8 @@ <field name="model">scop.invoice.idf</field> <field name="arch" type="xml"> <pivot string="Factures IDF" display_quantity="True"> - <field name="partner_id" type="row"/> - <field name="amount_residual" type="measure"/> + <field name="partner_id" type="row" /> + <field name="amount_residual" type="measure" /> </pivot> </field> </record> @@ -146,24 +212,64 @@ <field name="model">scop.invoice.idf</field> <field name="arch" type="xml"> <search string="Paiements IDF"> - <field name="partner_id"/> - <field name="name"/> - <field name="year"/> - <filter name="current_year" string="Année en cours" domain="[('year', '=', (context_today()).strftime('%Y'))]"/> - <separator/> - <filter name="with_issue" domain="[('state','in',['no_invoice', 'overpaid'])]" string="Erreur lettrage et/ou montant"/> - <filter name="without_invoice" domain="[('type','!=','inv'),('state','in',['no_invoice'])]" string="Paiements orphelins"/> - <separator/> - <filter name="is_type_invoice" domain="[('type','=','inv')]" string="Factures"/> - <filter name="inv_paid" domain="[('type','=','inv'),('state','=','paid')]" string="Factures payées"/> - <filter name="inv_unpaid" domain="[('type','=','inv'),('state','=','awaiting_payments')]" string="Factures reste à payées"/> - <filter name="inv_with_issue" domain="[('type','=','inv'),('state','=','overpaid')]" string="Factures en erreur"/> - <separator/> + <field name="partner_id" /> + <field name="name" /> + <field name="year" /> + <filter + name="current_year" + string="Année en cours" + domain="[('year', '=', (context_today()).strftime('%Y'))]" + /> + <separator /> + <filter + name="with_issue" + domain="[('state','in',['no_invoice', 'overpaid'])]" + string="Erreur lettrage et/ou montant" + /> + <filter + name="without_invoice" + domain="[('type','!=','inv'),('state','in',['no_invoice'])]" + string="Paiements orphelins" + /> + <separator /> + <filter + name="is_type_invoice" + domain="[('type','=','inv')]" + string="Factures" + /> + <filter + name="inv_paid" + domain="[('type','=','inv'),('state','=','paid')]" + string="Factures payées" + /> + <filter + name="inv_unpaid" + domain="[('type','=','inv'),('state','=','awaiting_payments')]" + string="Factures reste à payées" + /> + <filter + name="inv_with_issue" + domain="[('type','=','inv'),('state','=','overpaid')]" + string="Factures en erreur" + /> + <separator /> <group expand="0" string="Group By"> - <filter name="group_by_partner_id" string="Adhérent" context="{'group_by':'partner_id'}"/> - <separator/> - <filter string="Date écriture" name="groupby_writing_date" context="{'group_by':'writing_date'}"/> - <filter string="Type" name="groupby_type" context="{'group_by':'type'}"/> + <filter + name="group_by_partner_id" + string="Adhérent" + context="{'group_by':'partner_id'}" + /> + <separator /> + <filter + string="Date écriture" + name="groupby_writing_date" + context="{'group_by':'writing_date'}" + /> + <filter + string="Type" + name="groupby_type" + context="{'group_by':'type'}" + /> </group> </search> </field> @@ -174,18 +280,24 @@ <field name="name">Factures IDF</field> <field name="res_model">scop.invoice.idf</field> <field name="view_mode">tree,form,pivot,graph</field> - <field name="context">{'search_default_is_type_invoice': True, 'search_default_current_year': True}</field> + <field + name="context" + >{'search_default_is_type_invoice': True, 'search_default_current_year': True}</field> </record> <!-- MENUS --> - <menuitem id="menu_scop_invoice_idf" - name="Paiements IDF" - web_icon="cgscop_invoice_idf,static/description/icon_module.png"/> - <menuitem id="menu_scop_invoice_idf_imports" - name="Factures" - parent="menu_scop_invoice_idf" - sequence="10" - action="action_scop_invoice_idf"/> + <menuitem + id="menu_scop_invoice_idf" + name="Paiements IDF" + web_icon="cgscop_invoice_idf,static/description/icon_module.png" + /> + <menuitem + id="menu_scop_invoice_idf_imports" + name="Factures" + parent="menu_scop_invoice_idf" + sequence="10" + action="action_scop_invoice_idf" + /> </data> </odoo> diff --git a/views/scop_invoice_idf_logs.xml b/views/scop_invoice_idf_logs.xml index d3b407270ee63b3f43d12f312ed05726fa4b9f24..18248cda8cdade5a9c2998e283ec3601ade62ec6 100644 --- a/views/scop_invoice_idf_logs.xml +++ b/views/scop_invoice_idf_logs.xml @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8" ?> <odoo> <data> @@ -7,11 +7,15 @@ <field name="name">scop.invoice.idf.logs.tree</field> <field name="model">scop.invoice.idf.logs</field> <field name="arch" type="xml"> - <tree create="false" string="Logs des imports IDF" default_order='create_date desc'> - <field name="name"/> - <field name="type"/> - <field name="success"/> - <field name="failure"/> + <tree + create="false" + string="Logs des imports IDF" + default_order='create_date desc' + > + <field name="name" /> + <field name="type" /> + <field name="success" /> + <field name="failure" /> </tree> </field> </record> @@ -25,17 +29,17 @@ <sheet> <group col="2"> <group> - <field name="name"/> - <field name="type"/> + <field name="name" /> + <field name="type" /> </group> <group> - <field name="success"/> - <field name="failure"/> + <field name="success" /> + <field name="failure" /> </group> </group> <group col="1"> <group> - <field name="logs"/> + <field name="logs" /> </group> </group> </sheet> @@ -51,11 +55,13 @@ </record> <!-- MENUS --> - <menuitem id="menu_scop_invoice_idf_logs" - name="Logs des imports" - parent="menu_scop_invoice_idf" - sequence="50" - action="action_scop_invoice_idf_logs"/> + <menuitem + id="menu_scop_invoice_idf_logs" + name="Logs des imports" + parent="menu_scop_invoice_idf" + sequence="50" + action="action_scop_invoice_idf_logs" + /> </data> </odoo> diff --git a/wizard/scop_invoice_idf_wizard.py b/wizard/scop_invoice_idf_wizard.py index 519036ca8e34ecfdbde17e71f18339d90ed33331..a941ee9df72f414c33d3981d925a7a3aedd48f11 100644 --- a/wizard/scop_invoice_idf_wizard.py +++ b/wizard/scop_invoice_idf_wizard.py @@ -1,31 +1,33 @@ # © 2021 Le Filament (<http://www.le-filament.com>) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -import csv import base64 +import csv import mimetypes - from io import StringIO -from odoo import fields, models, api +from odoo import _, api, fields, models from odoo.exceptions import UserError from odoo.tools.mimetypes import guess_mimetype class ScopImportIDFWizard(models.TransientModel): - _name = 'scop.invoice.idf.wizard' + _name = "scop.invoice.idf.wizard" _description = "Wizard: Importer les cotisations ou factures IDF" # ------------------------------------------------------ # Fields declaration # ------------------------------------------------------ - file = fields.Binary('Fichier CSV', required=True) - filename = fields.Char('Filename') + file = fields.Binary("Fichier CSV", required=True) + filename = fields.Char("Filename") type = fields.Selection( - string='Type', - selection=[('inv', 'Facture'), - ('cotiz', 'Cotisation'), ], - required=False, ) + string="Type", + selection=[ + ("inv", "Facture"), + ("cotiz", "Cotisation"), + ], + required=False, + ) # ------------------------------------------------------ # Actions @@ -33,35 +35,37 @@ class ScopImportIDFWizard(models.TransientModel): @api.model def scop_invoice_idf_wizard_action(self): return { - 'type': 'ir.actions.act_window', - 'name': 'Import Factures', - 'views': [ - [self.env.ref( - 'cgscop_invoice_idf.scop_invoice_idf_wizard_form').id, - "form"] + "type": "ir.actions.act_window", + "name": "Import Factures", + "views": [ + [ + self.env.ref("cgscop_invoice_idf.scop_invoice_idf_wizard_form").id, + "form", + ] ], - 'context': { - 'default_type': 'inv', + "context": { + "default_type": "inv", }, - 'res_model': 'scop.invoice.idf.wizard', - 'target': 'new', + "res_model": "scop.invoice.idf.wizard", + "target": "new", } @api.model def scop_cotisation_idf_wizard_action(self): return { - 'type': 'ir.actions.act_window', - 'name': 'Import Cotisations', - 'views': [ - [self.env.ref( - 'cgscop_invoice_idf.scop_invoice_idf_wizard_form').id, - "form"] + "type": "ir.actions.act_window", + "name": "Import Cotisations", + "views": [ + [ + self.env.ref("cgscop_invoice_idf.scop_invoice_idf_wizard_form").id, + "form", + ] ], - 'context': { - 'default_type': 'cotiz', + "context": { + "default_type": "cotiz", }, - 'res_model': 'scop.invoice.idf.wizard', - 'target': 'new', + "res_model": "scop.invoice.idf.wizard", + "target": "new", } def load_data(self): @@ -76,59 +80,65 @@ class ScopImportIDFWizard(models.TransientModel): else: content_type = guess_mimetype(str(self.file)) - if content_type != 'text/csv': - raise UserError('This file does not seem to be a CSV file') + if content_type != "text/csv": + raise UserError(_("This file does not seem to be a CSV file")) - file = StringIO(base64.b64decode(self.file).decode('ISO-8859-1')) - reader = csv.DictReader(file, delimiter=';') + file = StringIO(base64.b64decode(self.file).decode("ISO-8859-1")) + reader = csv.DictReader(file, delimiter=";") company_id = self.env.user.company_id line = 1 tried_lines = 0 success = 0 logs = "<ul>" - new_log = self.env['scop.invoice.idf.logs'].create({'logs': logs}) + new_log = self.env["scop.invoice.idf.logs"].create({"logs": logs}) new_lines_years = set() - if self.type == 'inv': - model = 'scop.invoice.idf' + if self.type == "inv": + model = "scop.invoice.idf" else: - model = 'scop.cotisation.idf' + model = "scop.cotisation.idf" for row in reader: line += 1 - journal = row['Journal'] + journal = row["Journal"] if journal in ["VE", "BFC"]: tried_lines += 1 - compte = row['Compte'] - partner_id = self.env['res.partner'].search([ - ['member_number_int', '=', int(compte[2:7])], - ['member_number_int', '!=', 0] - ]) + compte = row["Compte"] + partner_id = self.env["res.partner"].search( + [ + ["member_number_int", "=", int(compte[2:7])], + ["member_number_int", "!=", 0], + ] + ) if len(partner_id) != 1: - logs += "<li> Ligne " \ - + str(line) \ - + ": Impossible de rattacher le compte " \ - + compte \ - + "</li>" + logs += ( + "<li> Ligne " + + str(line) + + ": Impossible de rattacher le compte " + + compte + + "</li>" + ) else: - writing_date = row['Date écriture'] - libelle = row['Libellé écriture'] + writing_date = row["Date écriture"] + libelle = row["Libellé écriture"] existing_import_line = self.env[model].search( [ - ['partner_id', '=', partner_id.id], - ['writing_date', '=', writing_date], - ['journal', '=', journal], - ['name', '=', libelle] - ]) + ["partner_id", "=", partner_id.id], + ["writing_date", "=", writing_date], + ["journal", "=", journal], + ["name", "=", libelle], + ] + ) if len(existing_import_line) > 0: logs += "<li> Ligne " + str(line) + ": Doublon</li>" else: - debit = int(row['Débit euro']) - credit = int(row['Crédit euro']) + debit = int(row["Débit euro"]) + credit = int(row["Crédit euro"]) if debit > 0 and credit > 0: - logs += "<li> Ligne " + str( - line) + ": Montants incorrects</li>" + logs += ( + "<li> Ligne " + str(line) + ": Montants incorrects</li>" + ) else: if journal == "VE" and debit > 0: line_type = "inv" @@ -141,43 +151,46 @@ class ScopImportIDFWizard(models.TransientModel): else: line_type = "refund" - lettrage = row['Lettrage N'] - year = row['Année'] - acc_doc = row['Pièce'] + lettrage = row["Lettrage N"] + year = row["Année"] + acc_doc = row["Pièce"] values = { - 'company_id': company_id.id, - 'partner_id': partner_id.id, - 'journal': journal, - 'writing_date': writing_date, - 'acc_doc': acc_doc, - 'name': libelle, - 'year': year, - 'type': line_type, - 'lettrage': lettrage, - 'debit': debit, - 'credit': credit, + "company_id": company_id.id, + "partner_id": partner_id.id, + "journal": journal, + "writing_date": writing_date, + "acc_doc": acc_doc, + "name": libelle, + "year": year, + "type": line_type, + "lettrage": lettrage, + "debit": debit, + "credit": credit, } new_line = self.env[model].create(values) success += 1 new_lines_years.add(new_line.year) logs += "</ul>" - new_log.write({ - 'type': self.type, - 'logs': logs, - 'success': success, - 'failure': tried_lines - success, - }) + new_log.write( + { + "type": self.type, + "logs": logs, + "success": success, + "failure": tried_lines - success, + } + ) # Lettrage for all line in updated years - recordset_for_lettrage = self.env[model].search([ - ['year', 'in', list(new_lines_years)]]) + recordset_for_lettrage = self.env[model].search( + [["year", "in", list(new_lines_years)]] + ) recordset_for_lettrage.reconcile() return { - 'type': 'ir.actions.act_window', - 'views': [[False, "form"]], - 'view_mode': 'form', - 'res_model': 'scop.invoice.idf.logs', - 'target': 'current', - 'res_id': new_log.id + "type": "ir.actions.act_window", + "views": [[False, "form"]], + "view_mode": "form", + "res_model": "scop.invoice.idf.logs", + "target": "current", + "res_id": new_log.id, } diff --git a/wizard/scop_invoice_idf_wizard.xml b/wizard/scop_invoice_idf_wizard.xml index 7e9327b2b492553b847a59cb4938cec89c58842f..60f5d72121ba9f303c7d067697f43888e2bc0cd4 100644 --- a/wizard/scop_invoice_idf_wizard.xml +++ b/wizard/scop_invoice_idf_wizard.xml @@ -1,7 +1,6 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8" ?> <!-- Copyright 2021 Le Filament License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). --> - <odoo> <data> @@ -12,13 +11,22 @@ <field name="arch" type="xml"> <form string="Load File"> <group> - <field name="filename" invisible="1"/> - <field name="file" filename="filename" required="1"/> - <field name="type" invisible="1"/> + <field name="filename" invisible="1" /> + <field name="file" filename="filename" required="1" /> + <field name="type" invisible="1" /> </group> <footer> - <button class="btn btn-sm btn-primary" name="load_data" string="Importer le fichier" type="object"/> - <button class="btn btn-sm btn-default" special="cancel" string="Fermer"/> + <button + class="btn btn-sm btn-primary" + name="load_data" + string="Importer le fichier" + type="object" + /> + <button + class="btn btn-sm btn-default" + special="cancel" + string="Fermer" + /> </footer> </form> </field>