diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 0000000..36ee1cf --- /dev/null +++ b/.copier-answers.yml @@ -0,0 +1,13 @@ +# Do NOT update manually; changes here will be overwritten by Copier +_commit: v1.1.1 +_src_path: gh:oca/oca-addons-repo-template +dependency_installation_mode: PIP +generate_requirements_txt: true +include_wkhtmltopdf: false +odoo_version: 14.0 +rebel_module_groups: [] +repo_description: "TODO: add repo description." +repo_name: web +repo_slug: web +travis_apt_packages: [] +travis_apt_sources: [] diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bfd7ac5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# Configuration for known file extensions +[*.{css,js,json,less,md,py,rst,sass,scss,xml,yaml,yml}] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{json,yml,yaml,rst,md}] +indent_size = 2 + +# Do not configure editor for libs and autogenerated content +[{*/static/{lib,src/lib}/**,*/static/description/index.html,*/readme/../README.rst}] +charset = unset +end_of_line = unset +indent_size = unset +indent_style = unset +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..88f2881 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,180 @@ +env: + browser: true + +# See https://github.com/OCA/odoo-community.org/issues/37#issuecomment-470686449 +parserOptions: + ecmaVersion: 2017 + +# Globals available in Odoo that shouldn't produce errorings +globals: + _: readonly + $: readonly + fuzzy: readonly + jQuery: readonly + moment: readonly + odoo: readonly + openerp: readonly + Promise: 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 0000000..44ed868 --- /dev/null +++ b/.flake8 @@ -0,0 +1,10 @@ +[flake8] +max-line-length = 80 +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 diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..7683bad --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,12 @@ +[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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6a8a1aa --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,135 @@ +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$| + # 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://github.com/OCA/web"] + - repo: https://github.com/myint/autoflake + rev: v1.4 + hooks: + - id: autoflake + args: ["-i", "--ignore-init-module-imports"] + - 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 + plugin-xml + additional_dependencies: + - "prettier@2.1.2" + - "@prettier/plugin-xml@0.12.0" + args: + - --plugin=@prettier/plugin-xml + - 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 + - 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://github.com/acsone/setuptools-odoo + rev: 2.6.0 + hooks: + - id: setuptools-odoo-make-default + - id: setuptools-odoo-get-requirements + args: + - --output + - requirements.txt + - --header + - "# generated from manifests external_dependencies" + - repo: https://gitlab.com/PyCQA/flake8 + rev: 3.8.3 + hooks: + - id: flake8 + name: flake8 except __init__.py + exclude: /__init__\.py$ + additional_dependencies: ["flake8-bugbear==20.1.4"] + - id: flake8 + name: flake8 only __init__.py + args: ["--extend-ignore=F401"] # ignore unused imports in __init__.py + files: /__init__\.py$ + 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 0000000..5b6d4b3 --- /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 0000000..cdc64ae --- /dev/null +++ b/.pylintrc @@ -0,0 +1,88 @@ +[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=Noviat +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, + # missing-readme, + no-utf8-coding-comment, + odoo-addons-relative-import, + old-api7-method-defined, + redefined-builtin, + too-complex, + unnecessary-utf8-coding-comment + + +[REPORTS] +msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} +output-format=colorized +reports=no diff --git a/.pylintrc-mandatory b/.pylintrc-mandatory new file mode 100644 index 0000000..3c61cdc --- /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=Noviat +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/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5e8e3a6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,42 @@ +language: python +cache: + directories: + - $HOME/.cache/pip + - $HOME/.cache/pre-commit + +python: + - "3.6" + +addons: + postgresql: "9.6" + apt: + packages: + - expect-dev # provides unbuffer utility + +stages: + - test + +jobs: + include: + - stage: test + env: + - TESTS=1 ODOO_REPO="odoo/odoo" MAKEPOT="1" + - stage: test + env: + - TESTS=1 ODOO_REPO="OCA/OCB" +env: + global: + - VERSION="14.0" TESTS="0" LINT_CHECK="0" MAKEPOT="0" + - MQT_DEP=PIP + +install: + - git clone --depth=1 https://github.com/OCA/maintainer-quality-tools.git + ${HOME}/maintainer-quality-tools + - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} + - travis_install_nightly + +script: + - travis_run_tests + +after_success: + - travis_after_tests_success diff --git a/account_ebics/__manifest__.py b/account_ebics/__manifest__.py index b3daf19..2f0ad2f 100644 --- a/account_ebics/__manifest__.py +++ b/account_ebics/__manifest__.py @@ -2,31 +2,31 @@ # License LGPL-3 or later (http://www.gnu.org/licenses/lpgl). { - 'name': 'EBICS banking protocol', - 'version': '14.0.1.0.2', - 'license': 'LGPL-3', - 'author': 'Noviat', - 'website': 'www.noviat.com', - 'category': 'Accounting & Finance', - 'depends': ['account'], - 'data': [ - 'security/ebics_security.xml', - 'security/ir.model.access.csv', - 'data/ebics_file_format.xml', - 'views/ebics_config_views.xml', - 'views/ebics_file_views.xml', - 'views/ebics_userid_views.xml', - 'views/ebics_file_format_views.xml', - 'wizards/ebics_change_passphrase.xml', - 'wizards/ebics_xfer.xml', - 'views/menu.xml', + "name": "EBICS banking protocol", + "version": "14.0.1.0.3", + "license": "LGPL-3", + "author": "Noviat", + "website": "www.noviat.com", + "category": "Accounting & Finance", + "depends": ["account"], + "data": [ + "security/ebics_security.xml", + "security/ir.model.access.csv", + "data/ebics_file_format.xml", + "views/ebics_config_views.xml", + "views/ebics_file_views.xml", + "views/ebics_userid_views.xml", + "views/ebics_file_format_views.xml", + "wizards/ebics_change_passphrase.xml", + "wizards/ebics_xfer.xml", + "views/menu.xml", ], - 'installable': True, - 'application': True, - 'external_dependencies': { - 'python': [ - 'fintech', - 'cryptography', - ] - }, + "installable": True, + "application": True, + "external_dependencies": { + "python": [ + "fintech", + "cryptography", + ] + }, } diff --git a/account_ebics/wizards/ebics_xfer.py b/account_ebics/wizards/ebics_xfer.py index 0ed09f0..5493056 100644 --- a/account_ebics/wizards/ebics_xfer.py +++ b/account_ebics/wizards/ebics_xfer.py @@ -14,125 +14,146 @@ import os from sys import exc_info from traceback import format_exception -from odoo import api, fields, models, _ +from odoo import _, api, fields, models from odoo.exceptions import UserError _logger = logging.getLogger(__name__) try: import fintech - from fintech.ebics import EbicsKeyRing, EbicsBank, EbicsUser, EbicsClient,\ - EbicsFunctionalError, EbicsTechnicalError, EbicsVerificationError - fintech.cryptolib = 'cryptography' + from fintech.ebics import ( + EbicsBank, + EbicsClient, + EbicsFunctionalError, + EbicsKeyRing, + EbicsTechnicalError, + EbicsUser, + EbicsVerificationError, + ) + + fintech.cryptolib = "cryptography" except ImportError: EbicsBank = object - _logger.warning('Failed to import fintech') + _logger.warning("Failed to import fintech") class EbicsBank(EbicsBank): - def _next_order_id(self, partnerid): """ EBICS protocol version H003 requires generation of the OrderID. The OrderID must be a string between 'A000' and 'ZZZZ' and unique for each partner id. """ - return hasattr(self, '_order_number') and self._order_number or 'A000' + return hasattr(self, "_order_number") and self._order_number or "A000" class EbicsXfer(models.TransientModel): - _name = 'ebics.xfer' - _description = 'EBICS file transfer' + _name = "ebics.xfer" + _description = "EBICS file transfer" ebics_config_id = fields.Many2one( - comodel_name='ebics.config', - string='EBICS Configuration', - domain=[('state', '=', 'confirm')], - default=lambda self: self._default_ebics_config_id()) + comodel_name="ebics.config", + string="EBICS Configuration", + domain=[("state", "=", "confirm")], + default=lambda self: self._default_ebics_config_id(), + ) ebics_userid_id = fields.Many2one( - comodel_name='ebics.userid', - string='EBICS UserID') - ebics_passphrase = fields.Char( - string='EBICS Passphrase') + comodel_name="ebics.userid", string="EBICS UserID" + ) + ebics_passphrase = fields.Char(string="EBICS Passphrase") date_from = fields.Date() date_to = fields.Date() - upload_data = fields.Binary(string='File to Upload') - upload_fname = fields.Char( - string='Upload Filename', default='') + upload_data = fields.Binary(string="File to Upload") + upload_fname = fields.Char(string="Upload Filename", default="") upload_fname_dummy = fields.Char( - related='upload_fname', string='Upload Filename', readonly=True) + related="upload_fname", string="Upload Filename", readonly=True + ) format_id = fields.Many2one( - comodel_name='ebics.file.format', - string='EBICS File Format', + comodel_name="ebics.file.format", + string="EBICS File Format", help="Select EBICS File Format to upload/download." - "\nLeave blank to download all available files.") + "\nLeave blank to download all available files.", + ) allowed_format_ids = fields.Many2many( - related='ebics_config_id.ebics_file_format_ids', - string='Allowed EBICS File Formats') + related="ebics_config_id.ebics_file_format_ids", + string="Allowed EBICS File Formats", + ) order_type = fields.Char( - related='format_id.order_type', - string='Order Type', + related="format_id.order_type", + string="Order Type", help="For most banks in France you should use the " - "format neutral Order Types 'FUL' for upload " - "and 'FDL' for download.") + "format neutral Order Types 'FUL' for upload " + "and 'FDL' for download.", + ) test_mode = fields.Boolean( - string='Test Mode', + string="Test Mode", help="Select this option to test if the syntax of " - "the upload file is correct." - "\nThis option is only available for " - "Order Type 'FUL'.") - note = fields.Text(string='EBICS file transfer Log', readonly=True) + "the upload file is correct." + "\nThis option is only available for " + "Order Type 'FUL'.", + ) + note = fields.Text(string="EBICS file transfer Log", readonly=True) @api.model def _default_ebics_config_id(self): - cfg_mod = self.env['ebics.config'] + cfg_mod = self.env["ebics.config"] cfg = cfg_mod.search( - [('company_ids', 'in', self.env.user.company_ids.ids), - ('state', '=', 'confirm')]) + [ + ("company_ids", "in", self.env.user.company_ids.ids), + ("state", "=", "confirm"), + ] + ) if cfg and len(cfg) == 1: return cfg else: return cfg_mod - @api.onchange('ebics_config_id') + @api.onchange("ebics_config_id") def _onchange_ebics_config_id(self): ebics_userids = self.ebics_config_id.ebics_userid_ids - if self._context.get('ebics_download'): - download_formats = self.ebics_config_id.ebics_file_format_ids\ - .filtered(lambda r: r.type == 'down') + if self._context.get("ebics_download"): + download_formats = self.ebics_config_id.ebics_file_format_ids.filtered( + lambda r: r.type == "down" + ) if len(download_formats) == 1: self.format_id = download_formats if len(ebics_userids) == 1: self.ebics_userid_id = ebics_userids else: transport_users = ebics_userids.filtered( - lambda r: r.signature_class == 'T') + lambda r: r.signature_class == "T" + ) if len(transport_users) == 1: self.ebics_userid_id = transport_users else: - upload_formats = self.ebics_config_id.ebics_file_format_ids\ - .filtered(lambda r: r.type == 'up') + upload_formats = self.ebics_config_id.ebics_file_format_ids.filtered( + lambda r: r.type == "up" + ) if len(upload_formats) == 1: self.format_id = upload_formats if len(ebics_userids) == 1: self.ebics_userid_id = ebics_userids - @api.onchange('upload_data') + @api.onchange("upload_data") def _onchange_upload_data(self): self.upload_fname_dummy = self.upload_fname self.format_id = False self._detect_upload_format() if not self.format_id: - upload_formats = self.format_id \ + upload_formats = ( + self.format_id or self.ebics_config_id.ebics_file_format_ids.filtered( - lambda r: r.type == 'up') + lambda r: r.type == "up" + ) + ) if len(upload_formats) > 1: upload_formats = upload_formats.filtered( - lambda r: self.upload_fname.endswith(r.suffix)) + lambda r: self.upload_fname.endswith(r.suffix) + ) if len(upload_formats) == 1: self.format_id = upload_formats - @api.onchange('format_id') + @api.onchange("format_id") def _onchange_format_id(self): self.order_type = self.format_id.order_type @@ -141,138 +162,152 @@ class EbicsXfer(models.TransientModel): ctx = self._context.copy() ebics_file = self._ebics_upload() if ebics_file: - ctx['ebics_file_id'] = ebics_file.id - module = __name__.split('addons.')[1].split('.')[0] - result_view = self.env.ref( - '%s.ebics_xfer_view_form_result' % module) + ctx["ebics_file_id"] = ebics_file.id + module = __name__.split("addons.")[1].split(".")[0] + result_view = self.env.ref("%s.ebics_xfer_view_form_result" % module) return { - 'name': _('EBICS file transfer result'), - 'res_id': self.id, - 'view_type': 'form', - 'view_mode': 'form', - 'res_model': 'ebics.xfer', - 'view_id': result_view.id, - 'target': 'new', - 'context': ctx, - 'type': 'ir.actions.act_window', + "name": _("EBICS file transfer result"), + "res_id": self.id, + "view_type": "form", + "view_mode": "form", + "res_model": "ebics.xfer", + "view_id": result_view.id, + "target": "new", + "context": ctx, + "type": "ir.actions.act_window", } def ebics_download(self): self.ensure_one() self.ebics_config_id._check_ebics_files() - ctx = self._context.copy() - self.note = '' + ctx = self.env.context.copy() + self.note = "" + err_cnt = 0 client = self._setup_client() - if client: + if not client: + err_cnt += 1 + self.note += ( + _("EBICS client setup failed for connection '%s'") + % self.ebics_config_id.name + ) + else: download_formats = ( self.format_id or self.ebics_config_id.ebics_file_format_ids.filtered( - lambda r: r.type == 'down' + lambda r: r.type == "down" ) ) - ebics_files = self.env['ebics.file'] + ebics_files = self.env["ebics.file"] date_from = self.date_from and self.date_from.isoformat() or None date_to = self.date_to and self.date_to.isoformat() or None for df in download_formats: try: success = False - if df.order_type == 'FDL': + if df.order_type == "FDL": data = client.FDL(df.name, date_from, date_to) else: params = None if date_from and date_to: - params = {'DateRange': { - 'Start': date_from, - 'End': date_to, - }} + params = { + "DateRange": { + "Start": date_from, + "End": date_to, + } + } data = client.download(df.order_type, params=params) ebics_files += self._handle_download_data(data, df) success = True except EbicsFunctionalError: + err_cnt += 1 e = exc_info() - self.note += '\n' + self.note += "\n" self.note += _( "EBICS Functional Error during download of File Format %s (%s):" ) % (df.name, df.order_type) - self.note += '\n' - self.note += '%s (code: %s)' % (e[1].message, e[1].code) + self.note += "\n" + self.note += "{} (code: {})".format(e[1].message, e[1].code) except EbicsTechnicalError: + err_cnt += 1 e = exc_info() - self.note += '\n' + self.note += "\n" self.note += _( "EBICS Technical Error during download of File Format %s (%s):" ) % (df.name, df.order_type) - self.note += '\n' - self.note += '%s (code: %s)' % (e[1].message, e[1].code) + self.note += "\n" + self.note += "{} (code: {})".format(e[1].message, e[1].code) except EbicsVerificationError: - self.note += '\n' + err_cnt += 1 + self.note += "\n" self.note += _( "EBICS Verification Error during download of " "File Format %s (%s):" ) % (df.name, df.order_type) - self.note += '\n' + self.note += "\n" self.note += _("The EBICS response could not be verified.") except UserError as e: - self.note += '\n' + self.note += "\n" self.note += _( "Warning during download of File Format %s (%s):" ) % (df.name, df.order_type) - self.note += '\n' + self.note += "\n" self.note += e.name except Exception: - self.note += '\n' + err_cnt += 1 + self.note += "\n" self.note += _( "Unknown Error during download of File Format %s (%s):" ) % (df.name, df.order_type) - tb = ''.join(format_exception(*exc_info())) - self.note += '\n%s' % tb + tb = "".join(format_exception(*exc_info())) + self.note += "\n%s" % tb else: # mark received data so that it is not included in further # downloads trans_id = client.last_trans_id client.confirm_download(trans_id=trans_id, success=success) - ctx['ebics_file_ids'] = ebics_files._ids + ctx["ebics_file_ids"] = ebics_files.ids if ebics_files: - self.note += '\n' + self.note += "\n" for f in ebics_files: - self.note += _( - "EBICS File '%s' is available for further processing." - ) % f.name - self.note += '\n' + self.note += ( + _("EBICS File '%s' is available for further processing.") + % f.name + ) + self.note += "\n" - module = __name__.split('addons.')[1].split('.')[0] - result_view = self.env.ref( - '%s.ebics_xfer_view_form_result' % module) + ctx["err_cnt"] = err_cnt + module = __name__.split("addons.")[1].split(".")[0] + result_view = self.env.ref("%s.ebics_xfer_view_form_result" % module) return { - 'name': _('EBICS file transfer result'), - 'res_id': self.id, - 'view_type': 'form', - 'view_mode': 'form', - 'res_model': 'ebics.xfer', - 'view_id': result_view.id, - 'target': 'new', - 'context': ctx, - 'type': 'ir.actions.act_window', + "name": _("EBICS file transfer result"), + "res_id": self.id, + "view_type": "form", + "view_mode": "form", + "res_model": "ebics.xfer", + "view_id": result_view.id, + "target": "new", + "context": ctx, + "type": "ir.actions.act_window", } def button_close(self): self.ensure_one() - return {'type': 'ir.actions.act_window_close'} + return {"type": "ir.actions.act_window_close"} def view_ebics_file(self): self.ensure_one() - module = __name__.split('addons.')[1].split('.')[0] - act = self.env['ir.actions.act_window']._for_xml_id( - '{}.ebics_file_action_download'.format(module)) - act['domain'] = [('id', 'in', self._context['ebics_file_ids'])] + module = __name__.split("addons.")[1].split(".")[0] + act = self.env["ir.actions.act_window"]._for_xml_id( + "{}.ebics_file_action_download".format(module) + ) + act["domain"] = [("id", "in", self._context["ebics_file_ids"])] return act def _ebics_upload(self): self.ensure_one() - ebics_file = self.env['ebics.file'] - self.note = '' + ebics_file = self.env["ebics.file"] + self.note = "" client = self._setup_client() if client: upload_data = base64.decodestring(self.upload_data) @@ -280,70 +315,69 @@ class EbicsXfer(models.TransientModel): OrderID = False try: order_type = self.order_type - if order_type == 'FUL': + if order_type == "FUL": kwargs = {} bank = self.ebics_config_id.journal_ids[0].bank_id cc = bank.country.code if cc: - kwargs['country'] = cc + kwargs["country"] = cc if self.test_mode: - kwargs['TEST'] = 'TRUE' + kwargs["TEST"] = "TRUE" OrderID = client.FUL(ef_format.name, upload_data, **kwargs) else: OrderID = client.upload(order_type, upload_data) if OrderID: - self.note += '\n' - self.note += _( - "EBICS File has been uploaded (OrderID %s)." - ) % OrderID + self.note += "\n" + self.note += ( + _("EBICS File has been uploaded (OrderID %s).") % OrderID + ) ef_note = _("EBICS OrderID: %s") % OrderID - if self.env.context.get('origin'): - ef_note += '\n' + _( - "Origin: %s") % self._context['origin'] + if self.env.context.get("origin"): + ef_note += "\n" + _("Origin: %s") % self._context["origin"] suffix = self.format_id.suffix fn = self.upload_fname if not fn.endswith(suffix): - fn = '.'.join([fn, suffix]) + fn = ".".join([fn, suffix]) ef_vals = { - 'name': self.upload_fname, - 'data': self.upload_data, - 'date': fields.Datetime.now(), - 'format_id': self.format_id.id, - 'state': 'done', - 'user_id': self._uid, - 'ebics_userid_id': self.ebics_userid_id.id, - 'note': ef_note, + "name": self.upload_fname, + "data": self.upload_data, + "date": fields.Datetime.now(), + "format_id": self.format_id.id, + "state": "done", + "user_id": self._uid, + "ebics_userid_id": self.ebics_userid_id.id, + "note": ef_note, "company_ids": [ self.env.context.get("force_company", self.env.company.id) ], } self._update_ef_vals(ef_vals) - ebics_file = self.env['ebics.file'].create(ef_vals) + ebics_file = self.env["ebics.file"].create(ef_vals) except EbicsFunctionalError: e = exc_info() - self.note += '\n' + self.note += "\n" self.note += _("EBICS Functional Error:") - self.note += '\n' - self.note += '%s (code: %s)' % (e[1].message, e[1].code) + self.note += "\n" + self.note += "{} (code: {})".format(e[1].message, e[1].code) except EbicsTechnicalError: e = exc_info() - self.note += '\n' + self.note += "\n" self.note += _("EBICS Technical Error:") - self.note += '\n' - self.note += '%s (code: %s)' % (e[1].message, e[1].code) + self.note += "\n" + self.note += "{} (code: {})".format(e[1].message, e[1].code) except EbicsVerificationError: - self.note += '\n' + self.note += "\n" self.note += _("EBICS Verification Error:") - self.note += '\n' + self.note += "\n" self.note += _("The EBICS response could not be verified.") except Exception: - self.note += '\n' + self.note += "\n" self.note += _("Unknown Error") - tb = ''.join(format_exception(*exc_info())) - self.note += '\n%s' % tb + tb = "".join(format_exception(*exc_info())) + self.note += "\n%s" % tb - if self.ebics_config_id.ebics_version == 'H003': + if self.ebics_config_id.ebics_version == "H003": OrderID = self.ebics_config_id._get_order_number() self.ebics_config_id.sudo()._update_order_number(OrderID) @@ -353,33 +387,35 @@ class EbicsXfer(models.TransientModel): self.ebics_config_id._check_ebics_keys() passphrase = self._get_passphrase() keyring = EbicsKeyRing( - keys=self.ebics_userid_id.ebics_keys_fn, - passphrase=passphrase) + keys=self.ebics_userid_id.ebics_keys_fn, passphrase=passphrase + ) bank = EbicsBank( keyring=keyring, hostid=self.ebics_config_id.ebics_host, - url=self.ebics_config_id.ebics_url) - if self.ebics_config_id.ebics_version == 'H003': + url=self.ebics_config_id.ebics_url, + ) + if self.ebics_config_id.ebics_version == "H003": bank._order_number = self.ebics_config_id._get_order_number() user = EbicsUser( keyring=keyring, partnerid=self.ebics_config_id.ebics_partner, - userid=self.ebics_userid_id.name) - signature_class = self.format_id.signature_class \ - or self.ebics_userid_id.signature_class - if signature_class == 'T': + userid=self.ebics_userid_id.name, + ) + signature_class = ( + self.format_id.signature_class or self.ebics_userid_id.signature_class + ) + if signature_class == "T": user.manual_approval = True try: - client = EbicsClient( - bank, user, version=self.ebics_config_id.ebics_version) + client = EbicsClient(bank, user, version=self.ebics_config_id.ebics_version) except Exception: - self.note += '\n' + self.note += "\n" self.note += _("Unknown Error") - tb = ''.join(format_exception(*exc_info())) - self.note += '\n%s' % tb + tb = "".join(format_exception(*exc_info())) + self.note += "\n%s" % tb client = False return client @@ -390,19 +426,18 @@ class EbicsXfer(models.TransientModel): if passphrase: return passphrase - module = __name__.split('addons.')[1].split('.')[0] - passphrase_view = self.env.ref( - '%s.ebics_xfer_view_form_passphrase' % module) + module = __name__.split("addons.")[1].split(".")[0] + passphrase_view = self.env.ref("%s.ebics_xfer_view_form_passphrase" % module) return { - 'name': _('EBICS file transfer'), - 'res_id': self.id, - 'view_type': 'form', - 'view_mode': 'form', - 'res_model': 'ebics.xfer', - 'view_id': passphrase_view.id, - 'target': 'new', - 'context': self._context, - 'type': 'ir.actions.act_window', + "name": _("EBICS file transfer"), + "res_id": self.id, + "view_type": "form", + "view_mode": "form", + "res_model": "ebics.xfer", + "view_id": passphrase_view.id, + "target": "new", + "context": self._context, + "type": "ir.actions.act_window", } def _file_format_methods(self): @@ -411,10 +446,10 @@ class EbicsXfer(models.TransientModel): for extra file formats. """ res = { - 'camt.xxx.cfonb120.stm': self._handle_cfonb120, - 'camt.xxx.cfonb120.stm.rfi': self._handle_cfonb120, - 'camt.052.001.02.stm': self._handle_camt052, - 'camt.053.001.02.stm': self._handle_camt053, + "camt.xxx.cfonb120.stm": self._handle_cfonb120, + "camt.xxx.cfonb120.stm.rfi": self._handle_cfonb120, + "camt.052.001.02.stm": self._handle_camt052, + "camt.053.001.02.stm": self._handle_camt053, } return res @@ -422,24 +457,24 @@ class EbicsXfer(models.TransientModel): """ Adapt this method to customize the EBICS File values. """ - if self.format_id and self.format_id.type == 'up': - fn = ef_vals['name'] - dups = self._check_duplicate_ebics_file( - fn, self.format_id) + if self.format_id and self.format_id.type == "up": + fn = ef_vals["name"] + dups = self._check_duplicate_ebics_file(fn, self.format_id) if dups: n = 1 - fn = '_'.join([fn, str(n)]) + fn = "_".join([fn, str(n)]) while self._check_duplicate_ebics_file(fn, self.format_id): n += 1 - fn = '_'.join([fn, str(n)]) - ef_vals['name'] = fn + fn = "_".join([fn, str(n)]) + ef_vals["name"] = fn def _handle_download_data(self, data, file_format): - ebics_files = self.env['ebics.file'] + ebics_files = self.env["ebics.file"] if isinstance(data, dict): for doc in data: ebics_files += self._create_ebics_file( - data[doc], file_format, docname=doc) + data[doc], file_format, docname=doc + ) else: ebics_files += self._create_ebics_file(data, file_format) return ebics_files @@ -457,59 +492,61 @@ class EbicsXfer(models.TransientModel): file format specific processing. """ ebics_files_root = self.ebics_config_id.ebics_files - tmp_dir = os.path.normpath(ebics_files_root + '/tmp') + tmp_dir = os.path.normpath(ebics_files_root + "/tmp") if not os.path.isdir(tmp_dir): os.makedirs(tmp_dir, mode=0o700) - fn_parts = [self.ebics_config_id.ebics_host, - self.ebics_config_id.ebics_partner] + fn_parts = [self.ebics_config_id.ebics_host, self.ebics_config_id.ebics_partner] if docname: fn_parts.append(docname) else: fn_date = self.date_to or fields.Date.today() fn_parts.append(fn_date.isoformat()) - base_fn = '_'.join(fn_parts) + base_fn = "_".join(fn_parts) n = 1 - full_tmp_fn = os.path.normpath(tmp_dir + '/' + base_fn) + full_tmp_fn = os.path.normpath(tmp_dir + "/" + base_fn) while os.path.exists(full_tmp_fn): n += 1 - tmp_fn = base_fn + '_' + str(n).rjust(3, '0') - full_tmp_fn = os.path.normpath(tmp_dir + '/' + tmp_fn) + tmp_fn = base_fn + "_" + str(n).rjust(3, "0") + full_tmp_fn = os.path.normpath(tmp_dir + "/" + tmp_fn) - with open(full_tmp_fn, 'wb') as f: + with open(full_tmp_fn, "wb") as f: f.write(data) ff_methods = self._file_format_methods() if file_format.name in ff_methods: data = ff_methods[file_format.name](data) - fn = '.'.join([base_fn, file_format.suffix]) + fn = ".".join([base_fn, file_format.suffix]) dups = self._check_duplicate_ebics_file(fn, file_format) if dups: - raise UserError(_( - "EBICS File with name '%s' has already been downloaded." - "\nPlease check this file and rename in case there is " - "no risk on duplicate transactions.") - % fn) + raise UserError( + _( + "EBICS File with name '%s' has already been downloaded." + "\nPlease check this file and rename in case there is " + "no risk on duplicate transactions." + ) + % fn + ) data = base64.encodebytes(data) ef_vals = { - 'name': fn, - 'data': data, - 'date': fields.Datetime.now(), - 'date_from': self.date_from, - 'date_to': self.date_to, - 'format_id': file_format.id, - 'user_id': self._uid, - 'ebics_userid_id': self.ebics_userid_id.id, - 'company_ids': self.ebics_config_id.company_ids.ids, + "name": fn, + "data": data, + "date": fields.Datetime.now(), + "date_from": self.date_from, + "date_to": self.date_to, + "format_id": file_format.id, + "user_id": self._uid, + "ebics_userid_id": self.ebics_userid_id.id, + "company_ids": self.ebics_config_id.company_ids.ids, } self._update_ef_vals(ef_vals) - ebics_file = self.env['ebics.file'].create(ef_vals) + ebics_file = self.env["ebics.file"].create(ef_vals) return ebics_file def _check_duplicate_ebics_file(self, fn, file_format): - dups = self.env['ebics.file'].search( - [('name', '=', fn), - ('format_id', '=', file_format.id)]) + dups = self.env["ebics.file"].search( + [("name", "=", fn), ("format_id", "=", file_format.id)] + ) return dups def _detect_upload_format(self): @@ -517,31 +554,30 @@ class EbicsXfer(models.TransientModel): Use this method in order to automatically detect and set the EBICS upload file format. """ - pass def _update_order_number(self, OrderID): o_list = list(OrderID) for i, c in enumerate(reversed(o_list), start=1): - if c == '9': - o_list[-i] = 'A' + if c == "9": + o_list[-i] = "A" break - if c == 'Z': + if c == "Z": continue else: o_list[-i] = chr(ord(c) + 1) break - next_nr = ''.join(o_list) - if next_nr == 'ZZZZ': - next_nr = 'A000' + next_nr = "".join(o_list) + if next_nr == "ZZZZ": + next_nr = "A000" self.ebics_config_id.order_number = next_nr def _insert_line_terminator(self, data_in, line_len): - data_in = data_in.replace(b'\n', b'').replace(b'\r', b'') - data_out = b'' + data_in = data_in.replace(b"\n", b"").replace(b"\r", b"") + data_out = b"" max_len = len(data_in) i = 0 while i + line_len <= max_len: - data_out += data_in[i:i + line_len] + b'\n' + data_out += data_in[i : i + line_len] + b"\n" i += line_len return data_out diff --git a/account_ebics_batch/README.rst b/account_ebics_batch/README.rst new file mode 100644 index 0000000..80f2d09 --- /dev/null +++ b/account_ebics_batch/README.rst @@ -0,0 +1,50 @@ +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +============================================ +Module to enable batch import of EBICS files +============================================ + +This module adds a cron job for the automated import of EBICS files. + +| + +A Log is created during the import in order to document import errors. +If errors have been detected, the Batch Import Log state is set to 'error'. + +When all EBICS Files have been imported correctly, the Batch Import Log state is set to 'done'. + +| + +The user can reprocess the imported EBICS files in status 'draft' via the Log object 'REPROCESS' button until all errors have been cleared. + +As an alternative, the user can force the Batch Import Log state to 'done' +(e.g. when the errors have been circumvented via manual encoding or the reprocessing of a single EBICS file). + +| + +Configuration +============= + +Adapt the 'EBICS Batch Import' ir.cron job created during the module installation. + +The cron job calls the following python method: + +| + +.. code-block:: python + + _batch_import() + + +The EBICS download will be performed on all confirmed EBICS connections. + +You can limit the automated operation to a subset of your EBICS connections via the ebics_config_ids parameter, e.g. + +| + +.. code-block:: python + + _batch_import(ebics_config_ids=[1,3]) + diff --git a/account_ebics_batch/__init__.py b/account_ebics_batch/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/account_ebics_batch/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_ebics_batch/__manifest__.py b/account_ebics_batch/__manifest__.py new file mode 100644 index 0000000..b84f781 --- /dev/null +++ b/account_ebics_batch/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2009-2022 Noviat. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "EBICS Files batch import", + "version": "14.0.1.0.0", + "license": "AGPL-3", + "author": "Noviat", + "website": "http://www.noviat.com", + "category": "Accounting & Finance", + "summary": "EBICS Files automated import and processing", + "depends": ["account_ebics"], + "data": [ + "security/ir.model.access.csv", + "data/ir_cron_data.xml", + "views/ebics_batch_log_views.xml", + "views/menu.xml", + ], + "installable": True, +} diff --git a/account_ebics_batch/data/ir_cron_data.xml b/account_ebics_batch/data/ir_cron_data.xml new file mode 100644 index 0000000..d33d59d --- /dev/null +++ b/account_ebics_batch/data/ir_cron_data.xml @@ -0,0 +1,17 @@ + + + + + EBICS Batch Import + + code + model._batch_import() + + 1 + days + -1 + + + + + diff --git a/account_ebics_batch/models/__init__.py b/account_ebics_batch/models/__init__.py new file mode 100644 index 0000000..740bafa --- /dev/null +++ b/account_ebics_batch/models/__init__.py @@ -0,0 +1 @@ +from . import ebics_batch_log diff --git a/account_ebics_batch/models/ebics_batch_log.py b/account_ebics_batch/models/ebics_batch_log.py new file mode 100644 index 0000000..9dfb064 --- /dev/null +++ b/account_ebics_batch/models/ebics_batch_log.py @@ -0,0 +1,193 @@ +# Copyright 2009-2022 Noviat. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from sys import exc_info +from traceback import format_exception + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class EbicsBatchLog(models.Model): + _name = "ebics.batch.log" + _description = "Object to store EBICS Batch Import Logs" + _order = "create_date desc" + + date_from = fields.Date() + date_to = fields.Date() + ebics_config_ids = fields.Many2many( + comodel_name="ebics.config", string="EBICS Configurations" + ) + log_ids = fields.One2many( + comodel_name="ebics.batch.log.item", + inverse_name="log_id", + string="Batch Import Log Items", + readonly=True, + ) + file_ids = fields.Many2many( + comodel_name="ebics.file", + string="Batch Import EBICS Files", + readonly=True, + ) + file_count = fields.Integer( + string="EBICS Files Count", compute="_compute_ebics_files_fields", readonly=True + ) + has_draft_files = fields.Boolean(compute="_compute_ebics_files_fields") + state = fields.Selection( + selection=[("draft", "Draft"), ("error", "Error"), ("done", "Done")], + string="State", + required=True, + readonly=True, + default="draft", + ) + + @api.depends("file_ids") + def _compute_ebics_files_fields(self): + for rec in self: + rec.has_draft_files = "draft" in rec.file_ids.mapped("state") + rec.file_count = len(rec.file_ids) + + def unlink(self): + for log in self: + if log.state != "draft": + raise UserError(_("Only log objects in state 'draft' can be deleted !")) + return super().unlink() + + def button_draft(self): + self.state = "draft" + + def button_done(self): + self.state = "done" + + def reprocess(self): + import_dict = {"errors": []} + self._ebics_process(import_dict) + self._finalise_processing(import_dict) + + def view_ebics_files(self): + action = self.env["ir.actions.actions"]._for_xml_id( + "account_ebics.ebics_file_action_download" + ) + action["domain"] = [("id", "in", self.file_ids.ids)] + return action + + def _batch_import(self, ebics_config_ids=None, date_from=None, date_to=None): + """ + Call this method from a cron job to automate the EBICS import. + """ + log_model = self.env["ebics.batch.log"] + import_dict = {"errors": []} + configs = self.env["ebics.config"].browse(ebics_config_ids) or self.env[ + "ebics.config" + ].search( + [ + ("company_ids", "in", self.env.user.company_ids.ids), + ("state", "=", "confirm"), + ] + ) + log = log_model.create( + { + "ebics_config_ids": [(6, 0, configs.ids)], + "date_from": date_from, + "date_to": date_to, + } + ) + ebics_file_ids = [] + for config in configs: + err_msg = ( + _("Error while processing EBICS connection '%s' :\n") % config.name + ) + if config.state == "draft": + import_dict["errors"].append( + err_msg + + _( + "Please set state to 'Confirm' and " + "Reprocess this EBICS Import Log." + ) + ) + continue + try: + with self.env.cr.savepoint(): + ebics_file_ids += self._ebics_import( + config, date_from, date_to, import_dict + ) + except UserError as e: + import_dict["errors"].append(err_msg + " ".join(e.args)) + except Exception: + tb = "".join(format_exception(*exc_info())) + import_dict["errors"].append(err_msg + tb) + log.file_ids = [(6, 0, ebics_file_ids)] + try: + with self.env.cr.savepoint(): + log._ebics_process(import_dict) + except UserError as e: + import_dict["errors"].append(err_msg + " ".join(e.args)) + except Exception: + tb = "".join(format_exception(*exc_info())) + import_dict["errors"].append(err_msg + tb) + log._finalise_processing(import_dict) + + def _finalise_processing(self, import_dict): + log_item_model = self.env["ebics.batch.log.item"] + state = self.has_draft_files and "draft" or "done" + note = "" + error_count = 0 + if import_dict["errors"]: + state = "error" + note = "\n\n".join(import_dict["errors"]) + error_count = len(import_dict["errors"]) + log_item_model.create( + { + "log_id": self.id, + "state": state, + "note": note, + "error_count": error_count, + } + ) + self.state = state + + def _ebics_import(self, config, date_from, date_to, import_dict): + ctx = dict(self.env.context, ebics_download=True) + xfer_wiz = ( + self.env["ebics.xfer"] + .with_context(ctx) + .create( + { + "ebics_config_id": config.id, + "date_from": date_from, + "date_to": date_to, + } + ) + ) + xfer_wiz._onchange_ebics_config_id() + res = xfer_wiz.ebics_download() + file_ids = res["context"].get("ebics_file_ids") + if res["context"]["err_cnt"]: + import_dict["errors"].append(xfer_wiz.note) + return file_ids + + def _ebics_process(self, import_dict): + to_process = self.file_ids.filtered(lambda r: r.state == "draft") + for ebics_file in to_process: + ebics_file.process() + + +class EbicsBatchLogItem(models.Model): + _name = "ebics.batch.log.item" + _description = "Object to store EBICS Batch Import Log Items" + _order = "create_date desc" + + log_id = fields.Many2one( + comodel_name="ebics.batch.log", + string="Batch Object", + ondelete="cascade", + readonly=True, + ) + state = fields.Selection( + selection=[("draft", "Draft"), ("error", "Error"), ("done", "Done")], + string="State", + required=True, + readonly=True, + ) + note = fields.Text(string="Batch Import Log", readonly=True) + error_count = fields.Integer(string="Number of Errors", required=True, default=0) diff --git a/account_ebics_batch/security/ir.model.access.csv b/account_ebics_batch/security/ir.model.access.csv new file mode 100644 index 0000000..af76670 --- /dev/null +++ b/account_ebics_batch/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_ebics_batch_log,ebics.batch.log,model_ebics_batch_log,account.group_account_invoice,1,1,1,1 +access_ebics_batch_log_item,ebics.batch.log.item,model_ebics_batch_log_item,account.group_account_invoice,1,1,1,1 diff --git a/account_ebics_batch/static/description/icon.png b/account_ebics_batch/static/description/icon.png new file mode 100644 index 0000000..889d129 Binary files /dev/null and b/account_ebics_batch/static/description/icon.png differ diff --git a/account_ebics_batch/views/ebics_batch_log_views.xml b/account_ebics_batch/views/ebics_batch_log_views.xml new file mode 100644 index 0000000..716882f --- /dev/null +++ b/account_ebics_batch/views/ebics_batch_log_views.xml @@ -0,0 +1,118 @@ + + + + + ebics.batch.log.search + ebics.batch.log + + + + + + + + + + + + + + + + ebics.batch.log.tree + ebics.batch.log + + + + + + + + + + + ebics.batch.log.form + ebics.batch.log + +
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + EBICS Batch Import Logs + ir.actions.act_window + ebics.batch.log + tree,form + + + + +
diff --git a/account_ebics_batch/views/menu.xml b/account_ebics_batch/views/menu.xml new file mode 100644 index 0000000..21343b8 --- /dev/null +++ b/account_ebics_batch/views/menu.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6c420f9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +# generated from manifests external_dependencies +cryptography +fintech diff --git a/setup/.setuptools-odoo-make-default-ignore b/setup/.setuptools-odoo-make-default-ignore new file mode 100644 index 0000000..207e615 --- /dev/null +++ b/setup/.setuptools-odoo-make-default-ignore @@ -0,0 +1,2 @@ +# addons listed in this file are ignored by +# setuptools-odoo-make-default (one addon per line) diff --git a/setup/README b/setup/README new file mode 100644 index 0000000..a63d633 --- /dev/null +++ b/setup/README @@ -0,0 +1,2 @@ +To learn more about this directory, please visit +https://pypi.python.org/pypi/setuptools-odoo diff --git a/setup/account_ebics/odoo/addons/account_ebics b/setup/account_ebics/odoo/addons/account_ebics new file mode 120000 index 0000000..16c1742 --- /dev/null +++ b/setup/account_ebics/odoo/addons/account_ebics @@ -0,0 +1 @@ +../../../../account_ebics \ No newline at end of file diff --git a/setup/account_ebics/setup.py b/setup/account_ebics/setup.py new file mode 100644 index 0000000..28c57bb --- /dev/null +++ b/setup/account_ebics/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/account_ebics_batch/odoo/addons/account_ebics_batch b/setup/account_ebics_batch/odoo/addons/account_ebics_batch new file mode 120000 index 0000000..d1e7e96 --- /dev/null +++ b/setup/account_ebics_batch/odoo/addons/account_ebics_batch @@ -0,0 +1 @@ +../../../../account_ebics_batch \ No newline at end of file diff --git a/setup/account_ebics_batch/setup.py b/setup/account_ebics_batch/setup.py new file mode 100644 index 0000000..28c57bb --- /dev/null +++ b/setup/account_ebics_batch/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/account_ebics_oca_statement_import/odoo/addons/account_ebics_oca_statement_import b/setup/account_ebics_oca_statement_import/odoo/addons/account_ebics_oca_statement_import new file mode 120000 index 0000000..766f43e --- /dev/null +++ b/setup/account_ebics_oca_statement_import/odoo/addons/account_ebics_oca_statement_import @@ -0,0 +1 @@ +../../../../account_ebics_oca_statement_import \ No newline at end of file diff --git a/setup/account_ebics_oca_statement_import/setup.py b/setup/account_ebics_oca_statement_import/setup.py new file mode 100644 index 0000000..28c57bb --- /dev/null +++ b/setup/account_ebics_oca_statement_import/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/account_ebics_oe/odoo/addons/account_ebics_oe b/setup/account_ebics_oe/odoo/addons/account_ebics_oe new file mode 120000 index 0000000..bcdee8e --- /dev/null +++ b/setup/account_ebics_oe/odoo/addons/account_ebics_oe @@ -0,0 +1 @@ +../../../../account_ebics_oe \ No newline at end of file diff --git a/setup/account_ebics_oe/setup.py b/setup/account_ebics_oe/setup.py new file mode 100644 index 0000000..28c57bb --- /dev/null +++ b/setup/account_ebics_oe/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/account_ebics_oe_statement_import/odoo/addons/account_ebics_oe_statement_import b/setup/account_ebics_oe_statement_import/odoo/addons/account_ebics_oe_statement_import new file mode 120000 index 0000000..75aca1f --- /dev/null +++ b/setup/account_ebics_oe_statement_import/odoo/addons/account_ebics_oe_statement_import @@ -0,0 +1 @@ +../../../../account_ebics_oe_statement_import \ No newline at end of file diff --git a/setup/account_ebics_oe_statement_import/setup.py b/setup/account_ebics_oe_statement_import/setup.py new file mode 100644 index 0000000..28c57bb --- /dev/null +++ b/setup/account_ebics_oe_statement_import/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/account_ebics_payment_order/odoo/addons/account_ebics_payment_order b/setup/account_ebics_payment_order/odoo/addons/account_ebics_payment_order new file mode 120000 index 0000000..5ff2d5c --- /dev/null +++ b/setup/account_ebics_payment_order/odoo/addons/account_ebics_payment_order @@ -0,0 +1 @@ +../../../../account_ebics_payment_order \ No newline at end of file diff --git a/setup/account_ebics_payment_order/setup.py b/setup/account_ebics_payment_order/setup.py new file mode 100644 index 0000000..28c57bb --- /dev/null +++ b/setup/account_ebics_payment_order/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)