Merge pull request #37 from Noviat/14-ebics-batch

14 ebics batch
This commit is contained in:
Luc De Meyer 2022-05-10 22:10:35 +02:00 committed by GitHub
commit 7420d7f19c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 2779 additions and 1350 deletions

13
.copier-answers.yml Normal file
View File

@ -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: []

20
.editorconfig Normal file
View File

@ -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

180
.eslintrc.yml Normal file
View File

@ -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

10
.flake8 Normal file
View File

@ -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

12
.isort.cfg Normal file
View File

@ -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

135
.pre-commit-config.yaml Normal file
View File

@ -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

8
.prettierrc.yml Normal file
View File

@ -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"

88
.pylintrc Normal file
View File

@ -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

64
.pylintrc-mandatory Normal file
View File

@ -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

42
.travis.yml Normal file
View File

@ -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

View File

@ -25,6 +25,7 @@ Remark:
The EBICS 'Test Mode' for uploading orders requires Fintech 4.3.4 or higher. The EBICS 'Test Mode' for uploading orders requires Fintech 4.3.4 or higher.
SWIFT 3SKey support requires Fintech 6.4 or higher. SWIFT 3SKey support requires Fintech 6.4 or higher.
| |
We also recommend to consider the installation of the following modules: We also recommend to consider the installation of the following modules:

View File

@ -2,31 +2,31 @@
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl). # License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
{ {
'name': 'EBICS banking protocol', "name": "EBICS banking protocol",
'version': '14.0.1.0.2', "version": "14.0.1.0.3",
'license': 'LGPL-3', "license": "LGPL-3",
'author': 'Noviat', "author": "Noviat",
'website': 'www.noviat.com', "website": "www.noviat.com",
'category': 'Accounting & Finance', "category": "Accounting & Finance",
'depends': ['account'], "depends": ["account"],
'data': [ "data": [
'security/ebics_security.xml', "security/ebics_security.xml",
'security/ir.model.access.csv', "security/ir.model.access.csv",
'data/ebics_file_format.xml', "data/ebics_file_format.xml",
'views/ebics_config_views.xml', "views/ebics_config_views.xml",
'views/ebics_file_views.xml', "views/ebics_file_views.xml",
'views/ebics_userid_views.xml', "views/ebics_userid_views.xml",
'views/ebics_file_format_views.xml', "views/ebics_file_format_views.xml",
'wizards/ebics_change_passphrase.xml', "wizards/ebics_change_passphrase.xml",
'wizards/ebics_xfer.xml', "wizards/ebics_xfer.xml",
'views/menu.xml', "views/menu.xml",
], ],
'installable': True, "installable": True,
'application': True, "application": True,
'external_dependencies': { "external_dependencies": {
'python': [ "python": [
'fintech', "fintech",
'cryptography', "cryptography",
] ]
}, },
} }

View File

@ -9,7 +9,9 @@
<field name="type">down</field> <field name="type">down</field>
<field name="order_type">C52</field> <field name="order_type">C52</field>
<field name="download_process_method">camt.052</field> <field name="download_process_method">camt.052</field>
<field name="description">bank to customer account report in format camt.052</field> <field
name="description"
>bank to customer account report in format camt.052</field>
<field name="suffix">c52.xml</field> <field name="suffix">c52.xml</field>
</record> </record>
@ -18,7 +20,9 @@
<field name="type">down</field> <field name="type">down</field>
<field name="order_type">Z52</field> <field name="order_type">Z52</field>
<field name="download_process_method">camt.052</field> <field name="download_process_method">camt.052</field>
<field name="description">bank to customer account report in format camt.052</field> <field
name="description"
>bank to customer account report in format camt.052</field>
<field name="suffix">c52.xml</field> <field name="suffix">c52.xml</field>
</record> </record>
@ -27,7 +31,9 @@
<field name="type">down</field> <field name="type">down</field>
<field name="order_type">C53</field> <field name="order_type">C53</field>
<field name="download_process_method">camt.053</field> <field name="download_process_method">camt.053</field>
<field name="description">Bank to customer statement report in format camt.053</field> <field
name="description"
>Bank to customer statement report in format camt.053</field>
<field name="suffix">c53.xml</field> <field name="suffix">c53.xml</field>
</record> </record>
@ -36,7 +42,9 @@
<field name="type">down</field> <field name="type">down</field>
<field name="order_type">Z53</field> <field name="order_type">Z53</field>
<field name="download_process_method">camt.053</field> <field name="download_process_method">camt.053</field>
<field name="description">Bank to customer statement report in format camt.053</field> <field
name="description"
>Bank to customer statement report in format camt.053</field>
<field name="suffix">c53.xml</field> <field name="suffix">c53.xml</field>
</record> </record>
@ -45,7 +53,9 @@
<field name="type">down</field> <field name="type">down</field>
<field name="order_type">C54</field> <field name="order_type">C54</field>
<field name="download_process_method">camt.054</field> <field name="download_process_method">camt.054</field>
<field name="description">Bank to customer debit credit notification in format camt.054</field> <field
name="description"
>Bank to customer debit credit notification in format camt.054</field>
<field name="suffix">c52.xml</field> <field name="suffix">c52.xml</field>
</record> </record>
@ -54,7 +64,9 @@
<field name="type">down</field> <field name="type">down</field>
<field name="order_type">Z54</field> <field name="order_type">Z54</field>
<field name="download_process_method">camt.054</field> <field name="download_process_method">camt.054</field>
<field name="description">Bank to customer debit credit notification in format camt.054</field> <field
name="description"
>Bank to customer debit credit notification in format camt.054</field>
<field name="suffix">c52.xml</field> <field name="suffix">c52.xml</field>
</record> </record>
@ -63,7 +75,9 @@
<field name="type">down</field> <field name="type">down</field>
<field name="order_type">FDL</field> <field name="order_type">FDL</field>
<field name="download_process_method">cfonb120</field> <field name="download_process_method">cfonb120</field>
<field name="description">Bank to customer statement report in format cfonb120</field> <field
name="description"
>Bank to customer statement report in format cfonb120</field>
<field name="suffix">cfonb120.dat</field> <field name="suffix">cfonb120.dat</field>
</record> </record>
@ -71,7 +85,9 @@
<field name="name">pain.002</field> <field name="name">pain.002</field>
<field name="type">down</field> <field name="type">down</field>
<field name="order_type">CDZ</field> <field name="order_type">CDZ</field>
<field name="description">Payment status report for direct debit in format pain.002</field> <field
name="description"
>Payment status report for direct debit in format pain.002</field>
<field name="suffix">psr.xml</field> <field name="suffix">psr.xml</field>
</record> </record>
@ -80,7 +96,9 @@
<field name="type">down</field> <field name="type">down</field>
<field name="order_type">Z01</field> <field name="order_type">Z01</field>
<field name="download_process_method">pain.002</field> <field name="download_process_method">pain.002</field>
<field name="description">Payment status report for direct debit in format pain.002</field> <field
name="description"
>Payment status report for direct debit in format pain.002</field>
<field name="suffix">psr.xml</field> <field name="suffix">psr.xml</field>
</record> </record>
@ -114,7 +132,9 @@
<field name="name">pain.008.001.02.sdd</field> <field name="name">pain.008.001.02.sdd</field>
<field name="type">up</field> <field name="type">up</field>
<field name="order_type">CDD</field> <field name="order_type">CDD</field>
<field name="description">Sepa Core Direct Debit Order in format pain.008.001.02</field> <field
name="description"
>Sepa Core Direct Debit Order in format pain.008.001.02</field>
<field name="suffix">xml</field> <field name="suffix">xml</field>
</record> </record>
@ -122,7 +142,9 @@
<field name="name">pain.008.001.02.sdd</field> <field name="name">pain.008.001.02.sdd</field>
<field name="type">up</field> <field name="type">up</field>
<field name="order_type">XE3</field> <field name="order_type">XE3</field>
<field name="description">Sepa Core Direct Debit Order in format pain.008.001.02</field> <field
name="description"
>Sepa Core Direct Debit Order in format pain.008.001.02</field>
<field name="suffix">xml</field> <field name="suffix">xml</field>
</record> </record>
@ -130,7 +152,9 @@
<field name="name">pain.008.001.02.sbb</field> <field name="name">pain.008.001.02.sbb</field>
<field name="type">up</field> <field name="type">up</field>
<field name="order_type">CDB</field> <field name="order_type">CDB</field>
<field name="description">Sepa Direct Debit (B2B) Order in format pain.008.001.02</field> <field
name="description"
>Sepa Direct Debit (B2B) Order in format pain.008.001.02</field>
<field name="suffix">xml</field> <field name="suffix">xml</field>
</record> </record>
@ -138,7 +162,9 @@
<field name="name">pain.008.001.02.sbb</field> <field name="name">pain.008.001.02.sbb</field>
<field name="type">up</field> <field name="type">up</field>
<field name="order_type">XE4</field> <field name="order_type">XE4</field>
<field name="description">Sepa Direct Debit (B2B) Order in format pain.008.001.02</field> <field
name="description"
>Sepa Direct Debit (B2B) Order in format pain.008.001.02</field>
<field name="suffix">xml</field> <field name="suffix">xml</field>
</record> </record>

View File

@ -5,14 +5,18 @@
<field name="name">EBICS Configuration model company rule</field> <field name="name">EBICS Configuration model company rule</field>
<field name="model_id" ref="model_ebics_config" /> <field name="model_id" ref="model_ebics_config" />
<field eval="True" name="global" /> <field eval="True" name="global" />
<field name="domain_force">['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field> <field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record> </record>
<record id="ebics_file_comp_rule" model="ir.rule"> <record id="ebics_file_comp_rule" model="ir.rule">
<field name="name">EBICS File model company rule</field> <field name="name">EBICS File model company rule</field>
<field name="model_id" ref="model_ebics_file" /> <field name="model_id" ref="model_ebics_file" />
<field eval="True" name="global" /> <field eval="True" name="global" />
<field name="domain_force">['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field> <field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record> </record>
</odoo> </odoo>

View File

@ -1,9 +1,10 @@
# Copyright 2009-2020 Noviat. # Copyright 2009-2020 Noviat.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from openupgradelib import openupgrade # pylint: disable=W7936
import os import os
from openupgradelib import openupgrade # pylint: disable=W7936
@openupgrade.migrate() @openupgrade.migrate()
def migrate(env, version): def migrate(env, version):
@ -16,58 +17,67 @@ def _ebics_config_upgrade(env, version):
env.cr.execute("SELECT * FROM ebics_config") env.cr.execute("SELECT * FROM ebics_config")
cfg_datas = env.cr.dictfetchall() cfg_datas = env.cr.dictfetchall()
for cfg_data in cfg_datas: for cfg_data in cfg_datas:
cfg = env['ebics.config'].browse(cfg_data['id']) cfg = env["ebics.config"].browse(cfg_data["id"])
journal = env['account.journal'].search( journal = env["account.journal"].search(
[('bank_account_id', '=', cfg_data['bank_id'])]) [("bank_account_id", "=", cfg_data["bank_id"])]
keys_fn_old = cfg_data['ebics_keys'] )
keys_fn_old = cfg_data["ebics_keys"]
ebics_keys_root = os.path.dirname(keys_fn_old) ebics_keys_root = os.path.dirname(keys_fn_old)
if os.path.isfile(keys_fn_old): if os.path.isfile(keys_fn_old):
keys_fn = ebics_keys_root + '/' + cfg_data['ebics_user'] + '_keys' keys_fn = ebics_keys_root + "/" + cfg_data["ebics_user"] + "_keys"
os.rename(keys_fn_old, keys_fn) os.rename(keys_fn_old, keys_fn)
state = cfg_data['state'] == 'active' and 'confirm' or 'draft' state = cfg_data["state"] == "active" and "confirm" or "draft"
cfg.write({ cfg.write(
'company_ids': [(6, 0, [cfg_data['company_id']])], {
'journal_ids': [(6, 0, journal.ids)], "company_ids": [(6, 0, [cfg_data["company_id"]])],
'ebics_keys': ebics_keys_root, "journal_ids": [(6, 0, journal.ids)],
'state': state, "ebics_keys": ebics_keys_root,
}) "state": state,
}
)
user_vals = { user_vals = {
'ebics_config_id': cfg_data['id'], "ebics_config_id": cfg_data["id"],
'name': cfg_data['ebics_user'], "name": cfg_data["ebics_user"],
} }
for fld in [ for fld in [
'signature_class', 'ebics_passphrase', "signature_class",
'ebics_ini_letter_fn', 'ebics_public_bank_keys_fn', "ebics_passphrase",
'ebics_key_x509', 'ebics_key_x509_dn_cn', "ebics_ini_letter_fn",
'ebics_key_x509_dn_o', 'ebics_key_x509_dn_ou', "ebics_public_bank_keys_fn",
'ebics_key_x509_dn_c', 'ebics_key_x509_dn_st', "ebics_key_x509",
'ebics_key_x509_dn_l', 'ebics_key_x509_dn_e', "ebics_key_x509_dn_cn",
'ebics_file_format_ids', 'state']: "ebics_key_x509_dn_o",
"ebics_key_x509_dn_ou",
"ebics_key_x509_dn_c",
"ebics_key_x509_dn_st",
"ebics_key_x509_dn_l",
"ebics_key_x509_dn_e",
"ebics_file_format_ids",
"state",
]:
if cfg_data.get(fld): if cfg_data.get(fld):
if fld == 'ebics_file_format_ids': if fld == "ebics_file_format_ids":
user_vals[fld] = [(6, 0, cfg_data[fld])] user_vals[fld] = [(6, 0, cfg_data[fld])]
elif fld == 'state' and cfg_data['state'] == 'active': elif fld == "state" and cfg_data["state"] == "active":
user_vals['state'] = 'active_keys' user_vals["state"] = "active_keys"
else: else:
user_vals[fld] = cfg_data[fld] user_vals[fld] = cfg_data[fld]
ebics_userid = env['ebics.userid'].create(user_vals) ebics_userid = env["ebics.userid"].create(user_vals)
env.cr.execute( env.cr.execute(
""" """
UPDATE ir_attachment UPDATE ir_attachment
SET res_model = 'ebics.userid', res_id = %s SET res_model = 'ebics.userid', res_id = %s
WHERE name in ('ebics_ini_letter', 'ebics_public_bank_keys'); WHERE name in ('ebics_ini_letter', 'ebics_public_bank_keys');
""" """
% ebics_userid.id) % ebics_userid.id
)
if len(cfg_datas) == 1: if len(cfg_datas) == 1:
env.cr.execute( env.cr.execute("UPDATE ebics_file SET ebics_userid_id = %s" % ebics_userid.id)
"UPDATE ebics_file SET ebics_userid_id = %s" % ebics_userid.id)
def _noupdate_changes(env, version): def _noupdate_changes(env, version):
openupgrade.load_data( openupgrade.load_data(
env.cr, env.cr, "account_ebics", "migrations/13.0.1.1/noupdate_changes.xml"
'account_ebics',
'migrations/13.0.1.1/noupdate_changes.xml'
) )

View File

@ -2,16 +2,18 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
_FILE_FORMATS = [ _FILE_FORMATS = [
{'xml_id_name': 'ebics_ff_C52', {
'download_process_method': 'camt.052', "xml_id_name": "ebics_ff_C52",
"download_process_method": "camt.052",
}, },
{'xml_id_name': 'ebics_ff_C53', {
'download_process_method': 'camt.053', "xml_id_name": "ebics_ff_C53",
"download_process_method": "camt.053",
}, },
{'xml_id_name': 'ebics_ff_FDL_camt_xxx_cfonb120_stm', {
'download_process_method': 'cfonb120', "xml_id_name": "ebics_ff_FDL_camt_xxx_cfonb120_stm",
"download_process_method": "cfonb120",
}, },
] ]
@ -21,21 +23,22 @@ def migrate(cr, version):
def _update_file_format(cr, ff): def _update_file_format(cr, ff):
cr.execute( cr.execute( # pylint: disable=E8103
""" """
SELECT res_id FROM ir_model_data SELECT res_id FROM ir_model_data
WHERE module='account_ebics' AND name='{}' WHERE module='account_ebics' AND name='{}'
""".format(ff['xml_id_name']) """.format(
ff["xml_id_name"]
)
) )
res = cr.fetchone() res = cr.fetchone()
if res: if res:
cr.execute( cr.execute( # pylint: disable=E8103
""" """
UPDATE ebics_file_format UPDATE ebics_file_format
SET download_process_method='{download_process_method}' SET download_process_method='{download_process_method}'
WHERE id={ff_id}; WHERE id={ff_id};
""".format( """.format(
download_process_method=ff['download_process_method'], download_process_method=ff["download_process_method"], ff_id=res[0]
ff_id=res[0]
) )
) )

View File

@ -2,35 +2,44 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
_FILE_FORMATS = [ _FILE_FORMATS = [
{'old_xml_id_name': 'ebics_ff_camt_052_001_02_stm', {
'new_xml_id_name': 'ebics_ff_C52', "old_xml_id_name": "ebics_ff_camt_052_001_02_stm",
'new_name': 'camt.052', "new_xml_id_name": "ebics_ff_C52",
"new_name": "camt.052",
}, },
{'old_xml_id_name': 'ebics_ff_camt_053_001_02_stm', {
'new_xml_id_name': 'ebics_ff_C53', "old_xml_id_name": "ebics_ff_camt_053_001_02_stm",
'new_name': 'camt.053', "new_xml_id_name": "ebics_ff_C53",
"new_name": "camt.053",
}, },
{'old_xml_id_name': 'ebics_ff_camt_xxx_cfonb120_stm', {
'new_xml_id_name': 'ebics_ff_FDL_camt_xxx_cfonb120_stm', "old_xml_id_name": "ebics_ff_camt_xxx_cfonb120_stm",
"new_xml_id_name": "ebics_ff_FDL_camt_xxx_cfonb120_stm",
}, },
{'old_xml_id_name': 'ebics_ff_pain_001_001_03_sct', {
'new_xml_id_name': 'ebics_ff_CCT', "old_xml_id_name": "ebics_ff_pain_001_001_03_sct",
"new_xml_id_name": "ebics_ff_CCT",
}, },
{'old_xml_id_name': 'ebics_ff_pain_001', {
'new_xml_id_name': 'ebics_ff_XE2', "old_xml_id_name": "ebics_ff_pain_001",
'new_name': 'pain.001.001.03', "new_xml_id_name": "ebics_ff_XE2",
"new_name": "pain.001.001.03",
}, },
{'old_xml_id_name': 'ebics_ff_pain_008_001_02_sdd', {
'new_xml_id_name': 'ebics_ff_CDD', "old_xml_id_name": "ebics_ff_pain_008_001_02_sdd",
"new_xml_id_name": "ebics_ff_CDD",
}, },
{'old_xml_id_name': 'ebics_ff_pain_008', {
'new_xml_id_name': 'ebics_ff_XE3', "old_xml_id_name": "ebics_ff_pain_008",
"new_xml_id_name": "ebics_ff_XE3",
}, },
{'old_xml_id_name': 'ebics_ff_pain_008_001_02_sbb', {
'new_xml_id_name': 'ebics_ff_CDB', "old_xml_id_name": "ebics_ff_pain_008_001_02_sbb",
"new_xml_id_name": "ebics_ff_CDB",
}, },
{'old_xml_id_name': 'ebics_ff_pain_001_001_02_sct', {
'new_xml_id_name': 'ebics_ff_FUL_pain_001_001_02_sct', "old_xml_id_name": "ebics_ff_pain_001_001_02_sct",
"new_xml_id_name": "ebics_ff_FUL_pain_001_001_02_sct",
}, },
] ]
@ -44,11 +53,13 @@ def migrate(cr, version):
def _update_file_format(cr, ff): def _update_file_format(cr, ff):
cr.execute( cr.execute( # pylint: disable=E8103
""" """
SELECT id, res_id FROM ir_model_data SELECT id, res_id FROM ir_model_data
WHERE module='account_ebics' AND name='{}' WHERE module='account_ebics' AND name='{}'
""".format(ff['old_xml_id_name']) """.format(
ff["old_xml_id_name"]
)
) )
res = cr.fetchone() res = cr.fetchone()
if res: if res:
@ -59,7 +70,7 @@ def _update_file_format(cr, ff):
""".format( """.format(
new_xml_id_name=ff["new_xml_id_name"], xml_id=res[0] new_xml_id_name=ff["new_xml_id_name"], xml_id=res[0]
) )
if ff.get('new_name'): if ff.get("new_name"):
query += """ query += """
UPDATE ebics_file_format UPDATE ebics_file_format
SET name='{new_name}' SET name='{new_name}'
@ -67,4 +78,4 @@ def _update_file_format(cr, ff):
""".format( """.format(
new_name=ff["new_name"], ff_id=res[1] new_name=ff["new_name"], ff_id=res[1]
) )
cr.execute(query) cr.execute(query) # pylint: disable=E8103

View File

@ -5,7 +5,6 @@ from odoo import fields, models
class AccountBankStatement(models.Model): class AccountBankStatement(models.Model):
_inherit = 'account.bank.statement' _inherit = "account.bank.statement"
ebics_file_id = fields.Many2one( ebics_file_id = fields.Many2one(comodel_name="ebics.file", string="EBICS Data File")
comodel_name='ebics.file', string='EBICS Data File')

View File

@ -1,9 +1,9 @@
# Copyright 2009-2020 Noviat. # Copyright 2009-2022 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl). # License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
import logging import logging
import re
import os import os
import re
from odoo import _, api, fields, models from odoo import _, api, fields, models
from odoo.exceptions import UserError from odoo.exceptions import UserError
@ -16,40 +16,55 @@ class EbicsConfig(models.Model):
EBICS configuration is stored in a separate object in order to EBICS configuration is stored in a separate object in order to
allow extra security policies on this object. allow extra security policies on this object.
""" """
_name = 'ebics.config'
_description = 'EBICS Configuration' _name = "ebics.config"
_order = 'name' _description = "EBICS Configuration"
_order = "name"
name = fields.Char( name = fields.Char(
string='Name', string="Name",
readonly=True, states={'draft': [('readonly', False)]}, readonly=True,
required=True) states={"draft": [("readonly", False)]},
required=True,
)
journal_ids = fields.Many2many( journal_ids = fields.Many2many(
comodel_name='account.journal', comodel_name="account.journal",
readonly=True, states={'draft': [('readonly', False)]}, readonly=True,
string='Bank Accounts', states={"draft": [("readonly", False)]},
string="Bank Accounts",
domain="[('type', '=', 'bank')]", domain="[('type', '=', 'bank')]",
required=True) required=True,
)
ebics_host = fields.Char( ebics_host = fields.Char(
string='EBICS HostID', required=True, string="EBICS HostID",
readonly=True, states={'draft': [('readonly', False)]}, required=True,
readonly=True,
states={"draft": [("readonly", False)]},
help="Contact your bank to get the EBICS HostID." help="Contact your bank to get the EBICS HostID."
"\nIn France the BIC is usually allocated to the HostID " "\nIn France the BIC is usually allocated to the HostID "
"whereas in Germany it tends to be an institute specific string " "whereas in Germany it tends to be an institute specific string "
"of 8 characters.") "of 8 characters.",
)
ebics_url = fields.Char( ebics_url = fields.Char(
string='EBICS URL', required=True, string="EBICS URL",
readonly=True, states={'draft': [('readonly', False)]}, required=True,
help="Contact your bank to get the EBICS URL.") readonly=True,
states={"draft": [("readonly", False)]},
help="Contact your bank to get the EBICS URL.",
)
ebics_version = fields.Selection( ebics_version = fields.Selection(
selection=[('H003', 'H003 (2.4)'), selection=[("H003", "H003 (2.4)"), ("H004", "H004 (2.5)")],
('H004', 'H004 (2.5)')], string="EBICS protocol version",
string='EBICS protocol version', readonly=True,
readonly=True, states={'draft': [('readonly', False)]}, states={"draft": [("readonly", False)]},
required=True, default='H004') required=True,
default="H004",
)
ebics_partner = fields.Char( ebics_partner = fields.Char(
string='EBICS PartnerID', required=True, string="EBICS PartnerID",
readonly=True, states={'draft': [('readonly', False)]}, required=True,
readonly=True,
states={"draft": [("readonly", False)]},
help="Organizational unit (company or individual) " help="Organizational unit (company or individual) "
"that concludes a contract with the bank. " "that concludes a contract with the bank. "
"\nIn this contract it will be agreed which order types " "\nIn this contract it will be agreed which order types "
@ -57,77 +72,95 @@ class EbicsConfig(models.Model):
"which of the customer's users (subscribers) " "which of the customer's users (subscribers) "
"communicate with the EBICS bank server and the authorisations " "communicate with the EBICS bank server and the authorisations "
"that these users will possess. " "that these users will possess. "
"\nIt is identified by the PartnerID.") "\nIt is identified by the PartnerID.",
)
ebics_userid_ids = fields.One2many( ebics_userid_ids = fields.One2many(
comodel_name='ebics.userid', comodel_name="ebics.userid",
inverse_name='ebics_config_id', inverse_name="ebics_config_id",
readonly=True, states={'draft': [('readonly', False)]}, readonly=True,
states={"draft": [("readonly", False)]},
help="Human users or a technical system that is/are " help="Human users or a technical system that is/are "
"assigned to a customer. " "assigned to a customer. "
"\nOn the EBICS bank server it is identified " "\nOn the EBICS bank server it is identified "
"by the combination of UserID and PartnerID. " "by the combination of UserID and PartnerID. "
"The technical subscriber serves only for the data exchange " "The technical subscriber serves only for the data exchange "
"between customer and financial institution. " "between customer and financial institution. "
"The human user also can authorise orders.") "The human user also can authorise orders.",
)
ebics_files = fields.Char( ebics_files = fields.Char(
string='EBICS Files Root', required=True, string="EBICS Files Root",
readonly=True, states={'draft': [('readonly', False)]}, required=True,
readonly=True,
states={"draft": [("readonly", False)]},
default=lambda self: self._default_ebics_files(), default=lambda self: self._default_ebics_files(),
help="Root Directory for EBICS File Transfer Folders.") help="Root Directory for EBICS File Transfer Folders.",
)
# We store the EBICS keys in a separate directory in the file system. # We store the EBICS keys in a separate directory in the file system.
# This directory requires special protection to reduce fraude. # This directory requires special protection to reduce fraude.
ebics_keys = fields.Char( ebics_keys = fields.Char(
string='EBICS Keys Root', required=True, string="EBICS Keys Root",
readonly=True, states={'draft': [('readonly', False)]}, required=True,
readonly=True,
states={"draft": [("readonly", False)]},
default=lambda self: self._default_ebics_keys(), default=lambda self: self._default_ebics_keys(),
help="Root Directory for storing the EBICS Keys.") help="Root Directory for storing the EBICS Keys.",
)
ebics_key_version = fields.Selection( ebics_key_version = fields.Selection(
selection=[('A005', 'A005 (RSASSA-PKCS1-v1_5)'), selection=[("A005", "A005 (RSASSA-PKCS1-v1_5)"), ("A006", "A006 (RSASSA-PSS)")],
('A006', 'A006 (RSASSA-PSS)')], string="EBICS key version",
string='EBICS key version', default="A006",
default='A006', readonly=True,
readonly=True, states={'draft': [('readonly', False)]}, states={"draft": [("readonly", False)]},
help="The key version of the electronic signature.") help="The key version of the electronic signature.",
)
ebics_key_bitlength = fields.Integer( ebics_key_bitlength = fields.Integer(
string='EBICS key bitlength', string="EBICS key bitlength",
default=2048, default=2048,
readonly=True, states={'draft': [('readonly', False)]}, readonly=True,
states={"draft": [("readonly", False)]},
help="The bit length of the generated keys. " help="The bit length of the generated keys. "
"\nThe value must be between 1536 and 4096.") "\nThe value must be between 1536 and 4096.",
)
ebics_file_format_ids = fields.Many2many( ebics_file_format_ids = fields.Many2many(
comodel_name='ebics.file.format', comodel_name="ebics.file.format",
column1='config_id', column2='format_id', column1="config_id",
string='EBICS File Formats', column2="format_id",
readonly=True, states={'draft': [('readonly', False)]}, string="EBICS File Formats",
readonly=True,
states={"draft": [("readonly", False)]},
) )
state = fields.Selection( state = fields.Selection(
[('draft', 'Draft'), [("draft", "Draft"), ("confirm", "Confirmed")],
('confirm', 'Confirmed')], string="State",
string='State', default="draft",
default='draft', required=True,
required=True, readonly=True) readonly=True,
)
order_number = fields.Char( order_number = fields.Char(
size=4, readonly=True, states={'draft': [('readonly', False)]}, size=4,
readonly=True,
states={"draft": [("readonly", False)]},
help="Specify the number for the next order." help="Specify the number for the next order."
"\nThis number should match the following pattern : " "\nThis number should match the following pattern : "
"[A-Z]{1}[A-Z0-9]{3}") "[A-Z]{1}[A-Z0-9]{3}",
active = fields.Boolean( )
string='Active', default=True) active = fields.Boolean(string="Active", default=True)
company_ids = fields.Many2many( company_ids = fields.Many2many(
comodel_name='res.company', comodel_name="res.company",
string='Companies', string="Companies",
required=True, required=True,
help="Companies sharing this EBICS contract.") help="Companies sharing this EBICS contract.",
)
@api.model @api.model
def _default_ebics_files(self): def _default_ebics_files(self):
return '/'.join(['/home/odoo/ebics_files', self._cr.dbname]) return "/".join(["/home/odoo/ebics_files", self._cr.dbname])
@api.model @api.model
def _default_ebics_keys(self): def _default_ebics_keys(self):
return '/'.join(['/etc/odoo/ebics_keys', self._cr.dbname]) return "/".join(["/etc/odoo/ebics_keys", self._cr.dbname])
@api.constrains('order_number') @api.constrains("order_number")
def _check_order_number(self): def _check_order_number(self):
for cfg in self: for cfg in self:
nbr = cfg.order_number nbr = cfg.order_number
@ -140,26 +173,28 @@ class EbicsConfig(models.Model):
if not pattern.match(nbr): if not pattern.match(nbr):
ok = False ok = False
if not ok: if not ok:
raise UserError(_( raise UserError(
_(
"Order Number should comply with the following pattern:" "Order Number should comply with the following pattern:"
"\n[A-Z]{1}[A-Z0-9]{3}")) "\n[A-Z]{1}[A-Z0-9]{3}"
)
)
@api.onchange('journal_ids') @api.onchange("journal_ids")
def _onchange_journal_ids(self): def _onchange_journal_ids(self):
self.company_ids = self.journal_ids.mapped('company_id') self.company_ids = self.journal_ids.mapped("company_id")
def unlink(self): def unlink(self):
for ebics_config in self: for ebics_config in self:
if ebics_config.state == 'active': if ebics_config.state == "active":
raise UserError(_( raise UserError(_("You cannot remove active EBICS configurations."))
"You cannot remove active EBICS configurations."))
return super(EbicsConfig, self).unlink() return super(EbicsConfig, self).unlink()
def set_to_draft(self): def set_to_draft(self):
return self.write({'state': 'draft'}) return self.write({"state": "draft"})
def set_to_confirm(self): def set_to_confirm(self):
return self.write({'state': 'confirm'}) return self.write({"state": "confirm"})
def _get_order_number(self): def _get_order_number(self):
return self.order_number return self.order_number
@ -167,31 +202,37 @@ class EbicsConfig(models.Model):
def _update_order_number(self, OrderID): def _update_order_number(self, OrderID):
o_list = list(OrderID) o_list = list(OrderID)
for i, c in enumerate(reversed(o_list), start=1): for i, c in enumerate(reversed(o_list), start=1):
if c == '9': if c == "9":
o_list[-i] = 'A' o_list[-i] = "A"
break break
if c == 'Z': if c == "Z":
continue continue
else: else:
o_list[-i] = chr(ord(c) + 1) o_list[-i] = chr(ord(c) + 1)
break break
next = ''.join(o_list) next_order_number = "".join(o_list)
if next == 'ZZZZ': if next_order_number == "ZZZZ":
next = 'A000' next_order_number = "A000"
self.order_number = next self.order_number = next_order_number
def _check_ebics_keys(self): def _check_ebics_keys(self):
dirname = self.ebics_keys or '' dirname = self.ebics_keys or ""
if not os.path.exists(dirname): if not os.path.exists(dirname):
raise UserError(_( raise UserError(
_(
"EBICS Keys Root Directory %s is not available." "EBICS Keys Root Directory %s is not available."
"\nPlease contact your system administrator.") "\nPlease contact your system administrator."
% dirname) )
% dirname
)
def _check_ebics_files(self): def _check_ebics_files(self):
dirname = self.ebics_files or '' dirname = self.ebics_files or ""
if not os.path.exists(dirname): if not os.path.exists(dirname):
raise UserError(_( raise UserError(
_(
"EBICS Files Root Directory %s is not available." "EBICS Files Root Directory %s is not available."
"\nPlease contact your system administrator.") "\nPlease contact your system administrator."
% dirname) )
% dirname
)

View File

@ -1,4 +1,4 @@
# Copyright 2009-2021 Noviat. # Copyright 2009-2022 Noviat.
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl). # License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
import base64 import base64
@ -6,113 +6,119 @@ import logging
from odoo import _, fields, models from odoo import _, fields, models
from odoo.exceptions import UserError from odoo.exceptions import UserError
from odoo.tools.safe_eval import safe_eval
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class EbicsFile(models.Model): class EbicsFile(models.Model):
_name = 'ebics.file' _name = "ebics.file"
_description = 'Object to store EBICS Data Files' _description = "Object to store EBICS Data Files"
_order = 'date desc' _order = "date desc"
_sql_constraints = [ _sql_constraints = [
('name_uniq', 'unique (name, format_id)', (
'This File has already been down- or uploaded !') "name_uniq",
"unique (name, format_id)",
"This File has already been down- or uploaded !",
)
] ]
name = fields.Char(string='Filename') name = fields.Char(string="Filename")
data = fields.Binary(string='File', readonly=True) data = fields.Binary(string="File", readonly=True)
format_id = fields.Many2one( format_id = fields.Many2one(
comodel_name='ebics.file.format', comodel_name="ebics.file.format", string="EBICS File Formats", readonly=True
string='EBICS File Formats', )
readonly=True) type = fields.Selection(related="format_id.type", readonly=True)
type = fields.Selection(
related='format_id.type',
readonly=True)
date_from = fields.Date( date_from = fields.Date(
readonly=True, readonly=True, help="'Date From' as entered in the download wizard."
help="'Date From' as entered in the download wizard.") )
date_to = fields.Date( date_to = fields.Date(
readonly=True, readonly=True, help="'Date To' as entered in the download wizard."
help="'Date To' as entered in the download wizard.") )
date = fields.Datetime( date = fields.Datetime(
required=True, readonly=True, required=True, readonly=True, help="File Upload/Download date"
help='File Upload/Download date') )
bank_statement_ids = fields.One2many( bank_statement_ids = fields.One2many(
comodel_name='account.bank.statement', comodel_name="account.bank.statement",
inverse_name='ebics_file_id', inverse_name="ebics_file_id",
string='Generated Bank Statements', readonly=True) string="Generated Bank Statements",
readonly=True,
)
state = fields.Selection( state = fields.Selection(
[('draft', 'Draft'), [("draft", "Draft"), ("done", "Done")],
('done', 'Done')], string="State",
string='State', default="draft",
default='draft', required=True,
required=True, readonly=True) readonly=True,
)
user_id = fields.Many2one( user_id = fields.Many2one(
comodel_name='res.users', string='User', comodel_name="res.users",
string="User",
default=lambda self: self.env.user, default=lambda self: self.env.user,
readonly=True) readonly=True,
)
ebics_userid_id = fields.Many2one( ebics_userid_id = fields.Many2one(
comodel_name='ebics.userid', comodel_name="ebics.userid",
string='EBICS UserID', string="EBICS UserID",
ondelete='restrict', ondelete="restrict",
readonly=True) readonly=True,
note = fields.Text(string='Notes') )
note_process = fields.Text(string='Notes') note = fields.Text(string="Notes")
note_process = fields.Text(string="Notes")
company_ids = fields.Many2many( company_ids = fields.Many2many(
comodel_name='res.company', comodel_name="res.company",
string='Companies', string="Companies",
help="Companies sharing this EBICS file.") help="Companies sharing this EBICS file.",
)
def unlink(self): def unlink(self):
ff_methods = self._file_format_methods() ff_methods = self._file_format_methods()
for ebics_file in self: for ebics_file in self:
if ebics_file.state == 'done': if ebics_file.state == "done":
raise UserError(_( raise UserError(_("You can only remove EBICS files in state 'Draft'."))
"You can only remove EBICS files in state 'Draft'."))
# execute format specific actions # execute format specific actions
ff = ebics_file.format_id.download_process_method ff = ebics_file.format_id.download_process_method
if ff in ff_methods: if ff in ff_methods:
if ff_methods[ff].get('unlink'): if ff_methods[ff].get("unlink"):
ff_methods[ff]['unlink'](ebics_file) ff_methods[ff]["unlink"](ebics_file)
# remove bank statements # remove bank statements
ebics_file.bank_statement_ids.unlink() ebics_file.bank_statement_ids.unlink()
return super(EbicsFile, self).unlink() return super(EbicsFile, self).unlink()
def set_to_draft(self): def set_to_draft(self):
return self.write({'state': 'draft'}) return self.write({"state": "draft"})
def set_to_done(self): def set_to_done(self):
return self.write({'state': 'done'}) return self.write({"state": "done"})
def process(self): def process(self):
self.ensure_one() self.ensure_one()
ctx = dict( ctx = dict(self.env.context, allowed_company_ids=self.env.user.company_ids.ids)
self.env.context,
allowed_company_ids=self.env.user.company_ids.ids)
self = self.with_context(ctx) self = self.with_context(ctx)
self.note_process = '' self.note_process = ""
ff_methods = self._file_format_methods() ff_methods = self._file_format_methods()
ff = self.format_id.download_process_method ff = self.format_id.download_process_method
if ff in ff_methods: if ff in ff_methods:
if ff_methods[ff].get('process'): if ff_methods[ff].get("process"):
res = ff_methods[ff]['process'](self) res = ff_methods[ff]["process"](self)
self.state = 'done' self.state = "done"
return res return res
else: else:
return self._process_undefined_format() return self._process_undefined_format()
def action_open_bank_statements(self): def action_open_bank_statements(self):
self.ensure_one() self.ensure_one()
action = self.env['ir.actions.act_window']._for_xml_id( action = self.env["ir.actions.act_window"]._for_xml_id(
'account.action_bank_statement_tree') "account.action_bank_statement_tree"
domain = eval(action.get('domain') or '[]') )
domain += [('id', 'in', self._context.get('statement_ids'))] domain = safe_eval(action.get("domain") or "[]")
action.update({'domain': domain}) domain += [("id", "in", self._context.get("statement_ids"))]
action.update({"domain": domain})
return action return action
def button_close(self): def button_close(self):
self.ensure_one() self.ensure_one()
return {'type': 'ir.actions.act_window_close'} return {"type": "ir.actions.act_window_close"}
def _file_format_methods(self): def _file_format_methods(self):
""" """
@ -120,35 +126,45 @@ class EbicsFile(models.Model):
for extra file formats. for extra file formats.
""" """
res = { res = {
'cfonb120': "cfonb120": {
{'process': self._process_cfonb120, "process": self._process_cfonb120,
'unlink': self._unlink_cfonb120}, "unlink": self._unlink_cfonb120,
'camt.052': },
{'process': self._process_camt052, "camt.052": {
'unlink': self._unlink_camt052}, "process": self._process_camt052,
'camt.053': "unlink": self._unlink_camt052,
{'process': self._process_camt053, },
'unlink': self._unlink_camt053}, "camt.053": {
'camt.054': "process": self._process_camt053,
{'process': self._process_camt054, "unlink": self._unlink_camt053,
'unlink': self._unlink_camt054}, },
'pain.002': "camt.054": {
{'process': self._process_pain002, "process": self._process_camt054,
'unlink': self._unlink_pain002}, "unlink": self._unlink_camt054,
},
"pain.002": {
"process": self._process_pain002,
"unlink": self._unlink_pain002,
},
} }
return res return res
def _check_import_module(self, module, raise_if_not_found=True): def _check_import_module(self, module, raise_if_not_found=True):
mod = self.env['ir.module.module'].sudo().search( mod = (
[('name', '=like', module), self.env["ir.module.module"]
('state', '=', 'installed')]) .sudo()
.search([("name", "=like", module), ("state", "=", "installed")])
)
if not mod: if not mod:
if raise_if_not_found: if raise_if_not_found:
raise UserError(_( raise UserError(
_(
"The module to process the '%s' format is not installed " "The module to process the '%s' format is not installed "
"on your system. " "on your system. "
"\nPlease install module '%s'") "\nPlease install module '%s'"
% (self.format_id.name, module)) )
% (self.format_id.name, module)
)
return False return False
return True return True
@ -156,19 +172,19 @@ class EbicsFile(models.Model):
notifications = [] notifications = []
st_line_ids = [] st_line_ids = []
statement_ids = [] statement_ids = []
if res.get('context'): if res.get("context"):
notifications = res['context'].get('notifications', []) notifications = res["context"].get("notifications", [])
st_line_ids = res['context'].get('statement_line_ids', []) st_line_ids = res["context"].get("statement_line_ids", [])
if notifications: if notifications:
for notif in notifications: for notif in notifications:
parts = [] parts = []
for k in ['type', 'message', 'details']: for k in ["type", "message", "details"]:
if notif.get(k): if notif.get(k):
msg = '%s: %s' % (k, notif[k]) msg = "{}: {}".format(k, notif[k])
parts.append(msg) parts.append(msg)
self.note_process += '\n'.join(parts) self.note_process += "\n".join(parts)
self.note_process += '\n' self.note_process += "\n"
self.note_process += '\n' self.note_process += "\n"
if st_line_ids: if st_line_ids:
self.flush() self.flush()
self.env.cr.execute( self.env.cr.execute(
@ -185,36 +201,37 @@ class EbicsFile(models.Model):
WHERE absl.id IN %s WHERE absl.id IN %s
ORDER BY date, company_id ORDER BY date, company_id
""", """,
(tuple(st_line_ids),) (tuple(st_line_ids),),
) )
sts_data = self.env.cr.dictfetchall() sts_data = self.env.cr.dictfetchall()
else: else:
sts_data = [] sts_data = []
st_cnt = len(sts_data) st_cnt = len(sts_data)
if st_cnt: if st_cnt:
self.note_process += _( self.note_process += _("%s bank statements have been imported: ") % st_cnt
"%s bank statements have been imported: " self.note_process += "\n"
) % st_cnt
self.note_process += '\n'
for st_data in sts_data: for st_data in sts_data:
self.note_process += ("\n%s, %s (%s)") % ( self.note_process += ("\n%s, %s (%s)") % (
st_data['date'], st_data['name'], st_data['company_name']) st_data["date"],
statement_ids = [x['statement_id'] for x in sts_data] st_data["name"],
st_data["company_name"],
)
statement_ids = [x["statement_id"] for x in sts_data]
if statement_ids: if statement_ids:
self.sudo().bank_statement_ids = [(6, 0, statement_ids)] self.sudo().bank_statement_ids = [(6, 0, statement_ids)]
ctx = dict(self.env.context, statement_ids=statement_ids) ctx = dict(self.env.context, statement_ids=statement_ids)
module = __name__.split('addons.')[1].split('.')[0] module = __name__.split("addons.")[1].split(".")[0]
result_view = self.env.ref('%s.ebics_file_view_form_result' % module) result_view = self.env.ref("%s.ebics_file_view_form_result" % module)
return { return {
'name': _('Import EBICS File'), "name": _("Import EBICS File"),
'res_id': self.id, "res_id": self.id,
'view_type': 'form', "view_type": "form",
'view_mode': 'form', "view_mode": "form",
'res_model': self._name, "res_model": self._name,
'view_id': result_view.id, "view_id": result_view.id,
'target': 'new', "target": "new",
'context': ctx, "context": ctx,
'type': 'ir.actions.act_window', "type": "ir.actions.act_window",
} }
@staticmethod @staticmethod
@ -223,60 +240,63 @@ class EbicsFile(models.Model):
We do not support the standard _journal_creation_wizard since a single We do not support the standard _journal_creation_wizard since a single
cfonb120 file may contain statements from different legal entities. cfonb120 file may contain statements from different legal entities.
""" """
import_module = 'account_statement_import_fr_cfonb' import_module = "account_statement_import_fr_cfonb"
self._check_import_module(import_module) self._check_import_module(import_module)
wiz_model = 'account.statement.import' wiz_model = "account.statement.import"
data_file = base64.b64decode(self.data) data_file = base64.b64decode(self.data)
lines = data_file.split(b'\n') lines = data_file.split(b"\n")
wiz_vals_list = [] wiz_vals_list = []
st_lines = b'' st_lines = b""
transactions = False transactions = False
for line in lines: for line in lines:
rec_type = line[0:2] rec_type = line[0:2]
acc_number = line[21:32] acc_number = line[21:32]
st_lines += line + b'\n' st_lines += line + b"\n"
if rec_type == b'04': if rec_type == b"04":
transactions = True transactions = True
if rec_type == b'07': if rec_type == b"07":
if transactions: if transactions:
fn = '_'.join([acc_number.decode(), self.name]) fn = "_".join([acc_number.decode(), self.name])
wiz_vals_list.append({ wiz_vals_list.append(
'statement_filename': fn, {
'statement_file': base64.b64encode(st_lines) "statement_filename": fn,
}) "statement_file": base64.b64encode(st_lines),
st_lines = b'' }
)
st_lines = b""
transactions = False transactions = False
result = { result = {
'type': 'ir.actions.client', "type": "ir.actions.client",
'tag': 'bank_statement_reconciliation_view', "tag": "bank_statement_reconciliation_view",
'context': {'statement_line_ids': [], "context": {
'company_ids': self.env.user.company_ids.ids, "statement_line_ids": [],
'notifications': []}, "company_ids": self.env.user.company_ids.ids,
"notifications": [],
},
} }
wiz_ctx = dict(self.env.context, active_model='ebics.file') wiz_ctx = dict(self.env.context, active_model="ebics.file")
for i, wiz_vals in enumerate(wiz_vals_list, start=1): for i, wiz_vals in enumerate(wiz_vals_list, start=1):
wiz = self.env[wiz_model].with_context(wiz_ctx).create(wiz_vals) wiz = self.env[wiz_model].with_context(wiz_ctx).create(wiz_vals)
res = wiz.import_file_button() res = wiz.import_file_button()
ctx = res.get('context') ctx = res.get("context")
if (res.get('res_model') if res.get("res_model") == "account.bank.statement.import.journal.creation":
== 'account.bank.statement.import.journal.creation'): message = _("Error detected while importing statement number %s.\n") % i
message = _(
"Error detected while importing statement number %s.\n"
) % i
message += _("No financial journal found.") message += _("No financial journal found.")
details = _( details = _("Bank account number: %s") % ctx.get(
'Bank account number: %s' "default_bank_acc_number"
) % ctx.get('default_bank_acc_number') )
result['context']['notifications'].extend([{ result["context"]["notifications"].extend(
'type': 'warning', [
'message': message, {
'details': details, "type": "warning",
}]) "message": message,
"details": details,
}
]
)
continue continue
result['context']['statement_line_ids'].extend( result["context"]["statement_line_ids"].extend(ctx["statement_line_ids"])
ctx['statement_line_ids']) result["context"]["notifications"].extend(ctx["notifications"])
result['context']['notifications'].extend(
ctx['notifications'])
return self._process_result_action(result) return self._process_result_action(result)
@staticmethod @staticmethod
@ -285,11 +305,10 @@ class EbicsFile(models.Model):
Placeholder for cfonb120 specific actions before removing the Placeholder for cfonb120 specific actions before removing the
EBICS data file and its related bank statements. EBICS data file and its related bank statements.
""" """
pass
@staticmethod @staticmethod
def _process_camt052(self): def _process_camt052(self):
import_module = 'account_statement_import_camt' import_module = "account_statement_import_camt"
self._check_import_module(import_module) self._check_import_module(import_module)
return self._process_camt053(self) return self._process_camt053(self)
@ -299,11 +318,10 @@ class EbicsFile(models.Model):
Placeholder for camt052 specific actions before removing the Placeholder for camt052 specific actions before removing the
EBICS data file and its related bank statements. EBICS data file and its related bank statements.
""" """
pass
@staticmethod @staticmethod
def _process_camt054(self): def _process_camt054(self):
import_module = 'account_statement_import_camt' import_module = "account_statement_import_camt"
self._check_import_module(import_module) self._check_import_module(import_module)
return self._process_camt053(self) return self._process_camt053(self)
@ -313,84 +331,89 @@ class EbicsFile(models.Model):
Placeholder for camt054 specific actions before removing the Placeholder for camt054 specific actions before removing the
EBICS data file and its related bank statements. EBICS data file and its related bank statements.
""" """
pass
@staticmethod @staticmethod
def _process_camt053(self): def _process_camt053(self):
modules = [ modules = [
('oca', 'account_statement_import_camt'), ("oca", "account_statement_import_camt"),
('oe', 'account_bank_statement_import_camt'), ("oe", "account_bank_statement_import_camt"),
] ]
found = False found = False
for src, mod in modules: for _src, mod in modules:
if self._check_import_module(mod, raise_if_not_found=False): if self._check_import_module(mod, raise_if_not_found=False):
found = True found = True
break break
if not found: if not found:
raise UserError(_( raise UserError(
_(
"The module to process the '%s' format is not installed " "The module to process the '%s' format is not installed "
"on your system. " "on your system. "
"\nPlease install one of the following modules: \n%s." "\nPlease install one of the following modules: \n%s."
) % (self.format_id.name, ', '.join([x[1] for x in modules]))
) )
if src == 'oca': % (self.format_id.name, ", ".join([x[1] for x in modules]))
)
if _src == "oca":
self._process_camt053_oca() self._process_camt053_oca()
else: else:
self._process_camt053_oe() self._process_camt053_oe()
def _process_camt053_oca(self): def _process_camt053_oca(self):
wiz_model = 'account.statement.import' wiz_model = "account.statement.import"
wiz_vals = { wiz_vals = {
'statement_filename': self.name, "statement_filename": self.name,
'statement_file': self.data, "statement_file": self.data,
} }
result = { result = {
'type': 'ir.actions.client', "type": "ir.actions.client",
'tag': 'bank_statement_reconciliation_view', "tag": "bank_statement_reconciliation_view",
'context': {'statement_line_ids': [], "context": {
'company_ids': self.env.user.company_ids.ids, "statement_line_ids": [],
'notifications': []}, "company_ids": self.env.user.company_ids.ids,
"notifications": [],
},
} }
wiz_ctx = dict(self.env.context, active_model='ebics.file') wiz_ctx = dict(self.env.context, active_model="ebics.file")
wiz = self.env[wiz_model].with_context(wiz_ctx).create(wiz_vals) wiz = self.env[wiz_model].with_context(wiz_ctx).create(wiz_vals)
res = wiz.import_file_button() res = wiz.import_file_button()
ctx = res.get('context') ctx = res.get("context")
if (res.get('res_model') if res.get("res_model") == "account.bank.statement.import.journal.creation":
== 'account.bank.statement.import.journal.creation'): message = _("Error detected while importing statement %s.\n") % self.name
message = _(
"Error detected while importing statement %s.\n"
) % self.name
message += _("No financial journal found.") message += _("No financial journal found.")
details = _( details = _("Bank account number: %s") % ctx.get("default_bank_acc_number")
'Bank account number: %s' result["context"]["notifications"].extend(
) % ctx.get('default_bank_acc_number') [
result['context']['notifications'].extend([{ {
'type': 'warning', "type": "warning",
'message': message, "message": message,
'details': details, "details": details,
}]) }
result['context']['statement_line_ids'].extend( ]
ctx['statement_line_ids']) )
result['context']['notifications'].extend( result["context"]["statement_line_ids"].extend(ctx["statement_line_ids"])
ctx['notifications']) result["context"]["notifications"].extend(ctx["notifications"])
return self._process_result_action(result) return self._process_result_action(result)
def _process_camt053_oe(self): def _process_camt053_oe(self):
wiz_model = 'account.bank.statement.import' wiz_model = "account.bank.statement.import"
wiz_vals = { wiz_vals = {
'attachment_ids': [(0, 0, {'name': self.name, "attachment_ids": [
'datas': self.data, (
'store_fname': self.name})]} 0,
ctx = dict(self.env.context, active_model='ebics.file') 0,
{"name": self.name, "datas": self.data, "store_fname": self.name},
)
]
}
ctx = dict(self.env.context, active_model="ebics.file")
wiz = self.env[wiz_model].with_context(ctx).create(wiz_vals) wiz = self.env[wiz_model].with_context(ctx).create(wiz_vals)
res = wiz.import_file() res = wiz.import_file()
if res.get('res_model') \ if res.get("res_model") == "account.bank.statement.import.journal.creation":
== 'account.bank.statement.import.journal.creation': if res.get("context"):
if res.get('context'): bank_account = res["context"].get("default_bank_acc_number")
bank_account = res['context'].get('default_bank_acc_number') raise UserError(
raise UserError(_( _("No financial journal found for Company Bank Account %s")
"No financial journal found for Company Bank Account %s" % bank_account
) % bank_account) )
return self._process_result_action(res) return self._process_result_action(res)
@staticmethod @staticmethod
@ -399,7 +422,6 @@ class EbicsFile(models.Model):
Placeholder for camt053 specific actions before removing the Placeholder for camt053 specific actions before removing the
EBICS data file and its related bank statements. EBICS data file and its related bank statements.
""" """
pass
@staticmethod @staticmethod
def _process_pain002(self): def _process_pain002(self):
@ -408,7 +430,6 @@ class EbicsFile(models.Model):
TODO: TODO:
add import logic based upon OCA 'account_payment_return_import' add import logic based upon OCA 'account_payment_return_import'
""" """
pass
@staticmethod @staticmethod
def _unlink_pain002(self): def _unlink_pain002(self):
@ -419,8 +440,11 @@ class EbicsFile(models.Model):
raise NotImplementedError raise NotImplementedError
def _process_undefined_format(self): def _process_undefined_format(self):
raise UserError(_( raise UserError(
_(
"The current version of the 'account_ebics' module " "The current version of the 'account_ebics' module "
"has no support to automatically process EBICS files " "has no support to automatically process EBICS files "
"with format %s." "with format %s."
) % self.format_id.name) )
% self.format_id.name
)

View File

@ -5,58 +5,60 @@ from odoo import api, fields, models
class EbicsFileFormat(models.Model): class EbicsFileFormat(models.Model):
_name = 'ebics.file.format' _name = "ebics.file.format"
_description = 'EBICS File Formats' _description = "EBICS File Formats"
_order = 'type,name,order_type' _order = "type,name,order_type"
name = fields.Char( name = fields.Char(
string='Request Type', string="Request Type",
required=True, required=True,
help="E.g. camt.xxx.cfonb120.stm, pain.001.001.03.sct.\n" help="E.g. camt.xxx.cfonb120.stm, pain.001.001.03.sct.\n"
"Specify camt.052, camt.053, camt.054 for camt " "Specify camt.052, camt.053, camt.054 for camt "
"Order Types such as C53, Z53, C54, Z54.\n" "Order Types such as C53, Z53, C54, Z54.\n"
"This name has to match the 'Request Type' in your " "This name has to match the 'Request Type' in your "
"EBICS contract for Order Type 'FDL' or 'FUL'.\n") "EBICS contract for Order Type 'FDL' or 'FUL'.\n",
)
type = fields.Selection( type = fields.Selection(
selection=[('down', 'Download'), selection=[("down", "Download"), ("up", "Upload")], required=True
('up', 'Upload')], )
required=True)
order_type = fields.Char( order_type = fields.Char(
string='Order Type', string="Order Type",
required=True, required=True,
help="E.g. C53 (check your EBICS contract).\n" help="E.g. C53 (check your EBICS contract).\n"
"For most banks in France you should use the " "For most banks in France you should use the "
"format neutral Order Types 'FUL' for upload " "format neutral Order Types 'FUL' for upload "
"and 'FDL' for download.") "and 'FDL' for download.",
)
download_process_method = fields.Selection( download_process_method = fields.Selection(
selection='_selection_download_process_method', selection="_selection_download_process_method",
help="Enable processing within Odoo of the downloaded file " help="Enable processing within Odoo of the downloaded file "
"via the 'Process' button." "via the 'Process' button."
"E.g. specify camt.053 to import a camt.053 file and create " "E.g. specify camt.053 to import a camt.053 file and create "
"a bank statement.") "a bank statement.",
)
# TODO: # TODO:
# move signature_class parameter so that it can be set per EBICS config # move signature_class parameter so that it can be set per EBICS config
signature_class = fields.Selection( signature_class = fields.Selection(
selection=[('E', 'Single signature'), selection=[("E", "Single signature"), ("T", "Transport signature")],
('T', 'Transport signature')], string="Signature Class",
string='Signature Class',
help="Please doublecheck the security of your Odoo " help="Please doublecheck the security of your Odoo "
"ERP system when using class 'E' to prevent unauthorised " "ERP system when using class 'E' to prevent unauthorised "
"users to make supplier payments." "users to make supplier payments."
"\nLeave this field empty to use the default " "\nLeave this field empty to use the default "
"defined for your EBICS UserID.") "defined for your EBICS UserID.",
)
description = fields.Char() description = fields.Char()
suffix = fields.Char( suffix = fields.Char(
required=True, required=True,
help="Specify the filename suffix for this File Format." help="Specify the filename suffix for this File Format." "\nE.g. c53.xml",
"\nE.g. c53.xml") )
@api.model @api.model
def _selection_download_process_method(self): def _selection_download_process_method(self):
methods = self.env['ebics.file']._file_format_methods().keys() methods = self.env["ebics.file"]._file_format_methods().keys()
return [(x, x) for x in methods] return [(x, x) for x in methods]
@api.onchange('type') @api.onchange("type")
def _onchange_type(self): def _onchange_type(self):
if self.type == 'up': if self.type == "up":
self.download_process_method = False self.download_process_method = False

View File

@ -13,224 +13,240 @@ from odoo.exceptions import UserError
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
# logging.basicConfig(
""" # level=logging.DEBUG,
logging.basicConfig( # format='[%(asctime)s] %(levelname)s - %(name)s: %(message)s')
level=logging.DEBUG,
format='[%(asctime)s] %(levelname)s - %(name)s: %(message)s')
"""
try: try:
import fintech import fintech
from fintech.ebics import EbicsKeyRing, EbicsBank, EbicsUser,\ from fintech.ebics import (
EbicsClient, EbicsFunctionalError, EbicsTechnicalError EbicsBank,
fintech.cryptolib = 'cryptography' EbicsClient,
EbicsFunctionalError,
EbicsKeyRing,
EbicsTechnicalError,
EbicsUser,
)
fintech.cryptolib = "cryptography"
except ImportError: except ImportError:
_logger.warning('Failed to import fintech') _logger.warning("Failed to import fintech")
class EbicsBank(EbicsBank): class EbicsBank(EbicsBank):
def _next_order_id(self, partnerid): def _next_order_id(self, partnerid):
""" """
EBICS protocol version H003 requires generation of the OrderID. EBICS protocol version H003 requires generation of the OrderID.
The OrderID must be a string between 'A000' and 'ZZZZ' and The OrderID must be a string between 'A000' and 'ZZZZ' and
unique for each partner id. 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 EbicsUserID(models.Model): class EbicsUserID(models.Model):
_name = 'ebics.userid' _name = "ebics.userid"
_description = 'EBICS UserID' _description = "EBICS UserID"
_order = 'name' _order = "name"
name = fields.Char( name = fields.Char(
string='EBICS UserID', required=True, string="EBICS UserID",
readonly=True, states={'draft': [('readonly', False)]}, required=True,
readonly=True,
states={"draft": [("readonly", False)]},
help="Human users or a technical system that is/are " help="Human users or a technical system that is/are "
"assigned to a customer. " "assigned to a customer. "
"\nOn the EBICS bank server it is identified " "\nOn the EBICS bank server it is identified "
"by the combination of UserID and PartnerID. " "by the combination of UserID and PartnerID. "
"The technical subscriber serves only for the data exchange " "The technical subscriber serves only for the data exchange "
"between customer and financial institution. " "between customer and financial institution. "
"The human user also can authorise orders.") "The human user also can authorise orders.",
)
ebics_config_id = fields.Many2one( ebics_config_id = fields.Many2one(
comodel_name='ebics.config', comodel_name="ebics.config", string="EBICS Configuration", ondelete="cascade"
string='EBICS Configuration', )
ondelete='cascade')
user_ids = fields.Many2many( user_ids = fields.Many2many(
comodel_name='res.users', comodel_name="res.users",
string='Users', string="Users",
required=True, required=True,
help="Users who are allowed to use this EBICS UserID for " help="Users who are allowed to use this EBICS UserID for "
" bank transactions.") " bank transactions.",
)
# Currently only a singe signature class per user is supported # Currently only a singe signature class per user is supported
# Classes A and B are not yet supported. # Classes A and B are not yet supported.
signature_class = fields.Selection( signature_class = fields.Selection(
selection=[('E', 'Single signature'), selection=[("E", "Single signature"), ("T", "Transport signature")],
('T', 'Transport signature')], string="Signature Class",
string='Signature Class', required=True,
required=True, default='T', default="T",
readonly=True, states={'draft': [('readonly', False)]}, readonly=True,
states={"draft": [("readonly", False)]},
help="Default signature class." help="Default signature class."
"This default can be overriden for specific " "This default can be overriden for specific "
"EBICS transactions (cf. File Formats).") "EBICS transactions (cf. File Formats).",
ebics_keys_fn = fields.Char( )
compute='_compute_ebics_keys_fn') ebics_keys_fn = fields.Char(compute="_compute_ebics_keys_fn")
ebics_keys_found = fields.Boolean( ebics_keys_found = fields.Boolean(compute="_compute_ebics_keys_found")
compute='_compute_ebics_keys_found') ebics_passphrase = fields.Char(string="EBICS Passphrase")
ebics_passphrase = fields.Char(
string='EBICS Passphrase')
ebics_ini_letter = fields.Binary( ebics_ini_letter = fields.Binary(
string='EBICS INI Letter', readonly=True, string="EBICS INI Letter",
help="INI-letter PDF document to be sent to your bank.") readonly=True,
ebics_ini_letter_fn = fields.Char( help="INI-letter PDF document to be sent to your bank.",
string='INI-letter Filename', readonly=True) )
ebics_ini_letter_fn = fields.Char(string="INI-letter Filename", readonly=True)
ebics_public_bank_keys = fields.Binary( ebics_public_bank_keys = fields.Binary(
string='EBICS Public Bank Keys', readonly=True, string="EBICS Public Bank Keys",
help="EBICS Public Bank Keys to be checked for consistency.") readonly=True,
help="EBICS Public Bank Keys to be checked for consistency.",
)
ebics_public_bank_keys_fn = fields.Char( ebics_public_bank_keys_fn = fields.Char(
string='EBICS Public Bank Keys Filename', readonly=True) string="EBICS Public Bank Keys Filename", readonly=True
)
swift_3skey = fields.Boolean( swift_3skey = fields.Boolean(
string='Enable 3SKey support', string="Enable 3SKey support",
help="Transactions for this user will be signed " help="Transactions for this user will be signed "
"by means of the SWIFT 3SKey token.") "by means of the SWIFT 3SKey token.",
swift_3skey_certificate = fields.Binary( )
string='3SKey Certficate') swift_3skey_certificate = fields.Binary(string="3SKey Certficate")
swift_3skey_certificate_fn = fields.Char( swift_3skey_certificate_fn = fields.Char(string="EBICS Public Bank Keys Filename")
string='EBICS Public Bank Keys Filename')
# X.509 Distinguished Name attributes used to # X.509 Distinguished Name attributes used to
# create self-signed X.509 certificates # create self-signed X.509 certificates
ebics_key_x509 = fields.Boolean( ebics_key_x509 = fields.Boolean(
string='X509 support', string="X509 support",
help="Set this flag in order to work with " help="Set this flag in order to work with " "self-signed X.509 certificates",
"self-signed X.509 certificates") )
ebics_key_x509_dn_cn = fields.Char( ebics_key_x509_dn_cn = fields.Char(
string='Common Name [CN]', string="Common Name [CN]",
readonly=True, states={'draft': [('readonly', False)]}, readonly=True,
states={"draft": [("readonly", False)]},
) )
ebics_key_x509_dn_o = fields.Char( ebics_key_x509_dn_o = fields.Char(
string='Organization Name [O]', string="Organization Name [O]",
readonly=True, states={'draft': [('readonly', False)]}, readonly=True,
states={"draft": [("readonly", False)]},
) )
ebics_key_x509_dn_ou = fields.Char( ebics_key_x509_dn_ou = fields.Char(
string='Organizational Unit Name [OU]', string="Organizational Unit Name [OU]",
readonly=True, states={'draft': [('readonly', False)]}, readonly=True,
states={"draft": [("readonly", False)]},
) )
ebics_key_x509_dn_c = fields.Char( ebics_key_x509_dn_c = fields.Char(
string='Country Name [C]', string="Country Name [C]",
readonly=True, states={'draft': [('readonly', False)]}, readonly=True,
states={"draft": [("readonly", False)]},
) )
ebics_key_x509_dn_st = fields.Char( ebics_key_x509_dn_st = fields.Char(
string='State Or Province Name [ST]', string="State Or Province Name [ST]",
readonly=True, states={'draft': [('readonly', False)]}, readonly=True,
states={"draft": [("readonly", False)]},
) )
ebics_key_x509_dn_l = fields.Char( ebics_key_x509_dn_l = fields.Char(
string='Locality Name [L]', string="Locality Name [L]",
readonly=True, states={'draft': [('readonly', False)]}, readonly=True,
states={"draft": [("readonly", False)]},
) )
ebics_key_x509_dn_e = fields.Char( ebics_key_x509_dn_e = fields.Char(
string='Email Address', string="Email Address",
readonly=True, states={'draft': [('readonly', False)]}, readonly=True,
states={"draft": [("readonly", False)]},
) )
state = fields.Selection( state = fields.Selection(
[('draft', 'Draft'), [
('init', 'Initialisation'), ("draft", "Draft"),
('get_bank_keys', 'Get Keys from Bank'), ("init", "Initialisation"),
('to_verify', 'Verification'), ("get_bank_keys", "Get Keys from Bank"),
('active_keys', 'Active Keys')], ("to_verify", "Verification"),
string='State', ("active_keys", "Active Keys"),
default='draft', ],
required=True, readonly=True) string="State",
active = fields.Boolean( default="draft",
string='Active', default=True)
company_ids = fields.Many2many(
comodel_name='res.company',
string='Companies',
required=True, required=True,
help="Companies sharing this EBICS contract.") readonly=True,
)
active = fields.Boolean(string="Active", default=True)
company_ids = fields.Many2many(
comodel_name="res.company",
string="Companies",
required=True,
help="Companies sharing this EBICS contract.",
)
@api.depends('name') @api.depends("name")
def _compute_ebics_keys_fn(self): def _compute_ebics_keys_fn(self):
for rec in self: for rec in self:
keys_dir = rec.ebics_config_id.ebics_keys keys_dir = rec.ebics_config_id.ebics_keys
rec.ebics_keys_fn = ( rec.ebics_keys_fn = (
rec.name rec.name and keys_dir and (keys_dir + "/" + rec.name + "_keys")
and keys_dir
and (keys_dir + '/' + rec.name + '_keys'))
@api.depends('ebics_keys_fn')
def _compute_ebics_keys_found(self):
for rec in self:
rec.ebics_keys_found = (
rec.ebics_keys_fn
and os.path.isfile(rec.ebics_keys_fn)
) )
@api.constrains('ebics_passphrase') @api.depends("ebics_keys_fn")
def _compute_ebics_keys_found(self):
for rec in self:
rec.ebics_keys_found = rec.ebics_keys_fn and os.path.isfile(
rec.ebics_keys_fn
)
@api.constrains("ebics_passphrase")
def _check_ebics_passphrase(self): def _check_ebics_passphrase(self):
for rec in self: for rec in self:
if not rec.ebics_passphrase or len(rec.ebics_passphrase) < 8: if not rec.ebics_passphrase or len(rec.ebics_passphrase) < 8:
raise UserError(_( raise UserError(_("The passphrase must be at least 8 characters long"))
"The passphrase must be at least 8 characters long"))
@api.onchange('signature_class') @api.onchange("signature_class")
def _onchange_signature_class(self): def _onchange_signature_class(self):
if self.signature_class == 'T': if self.signature_class == "T":
self.swift_3skey = False self.swift_3skey = False
@api.onchange('swift_3skey') @api.onchange("swift_3skey")
def _onchange_swift_3skey(self): def _onchange_swift_3skey(self):
if self.swift_3skey: if self.swift_3skey:
self.ebics_key_x509 = True self.ebics_key_x509 = True
def set_to_draft(self): def set_to_draft(self):
return self.write({'state': 'draft'}) return self.write({"state": "draft"})
def set_to_active_keys(self): def set_to_active_keys(self):
return self.write({'state': 'active_keys'}) return self.write({"state": "active_keys"})
def set_to_get_bank_keys(self): def set_to_get_bank_keys(self):
return self.write({'state': 'get_bank_keys'}) return self.write({"state": "get_bank_keys"})
def ebics_init_1(self): def ebics_init_1(self): # noqa: C901
""" """
Initialization of bank keys - Step 1: Initialization of bank keys - Step 1:
Create new keys and certificates for this user Create new keys and certificates for this user
""" """
self.ensure_one() self.ensure_one()
self.ebics_config_id._check_ebics_files() self.ebics_config_id._check_ebics_files()
if self.state != 'draft': if self.state != "draft":
raise UserError( raise UserError(
_("Set state to 'draft' before Bank Key (re)initialisation.")) _("Set state to 'draft' before Bank Key (re)initialisation.")
)
if not self.ebics_passphrase: if not self.ebics_passphrase:
raise UserError( raise UserError(_("Set a passphrase."))
_("Set a passphrase."))
if self.swift_3skey and not self.swift_3skey_certificate: if self.swift_3skey and not self.swift_3skey_certificate:
raise UserError( raise UserError(_("3SKey certificate missing."))
_("3SKey certificate missing."))
ebics_version = self.ebics_config_id.ebics_version ebics_version = self.ebics_config_id.ebics_version
try: try:
keyring = EbicsKeyRing( keyring = EbicsKeyRing(
keys=self.ebics_keys_fn, keys=self.ebics_keys_fn, passphrase=self.ebics_passphrase
passphrase=self.ebics_passphrase) )
bank = EbicsBank( bank = EbicsBank(
keyring=keyring, keyring=keyring,
hostid=self.ebics_config_id.ebics_host, hostid=self.ebics_config_id.ebics_host,
url=self.ebics_config_id.ebics_url) url=self.ebics_config_id.ebics_url,
)
user = EbicsUser( user = EbicsUser(
keyring=keyring, keyring=keyring,
partnerid=self.ebics_config_id.ebics_partner, partnerid=self.ebics_config_id.ebics_partner,
userid=self.name) userid=self.name,
)
except Exception: except Exception:
exctype, value = exc_info()[:2] exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:") error = _("EBICS Initialisation Error:")
error += '\n' + str(exctype) + '\n' + str(value) error += "\n" + str(exctype) + "\n" + str(value)
raise UserError(error) raise UserError(error)
self.ebics_config_id._check_ebics_keys() self.ebics_config_id._check_ebics_keys()
@ -240,60 +256,63 @@ class EbicsUserID(models.Model):
# enable import of all type of certicates: A00x, X002, E002 # enable import of all type of certicates: A00x, X002, E002
if self.swift_3skey: if self.swift_3skey:
kwargs = { kwargs = {
self.ebics_config_id.ebics_key_version: self.ebics_config_id.ebics_key_version: base64.decodestring(
base64.decodestring(self.swift_3skey_certificate), self.swift_3skey_certificate
),
} }
user.import_certificates(**kwargs) user.import_certificates(**kwargs)
user.create_keys( user.create_keys(
keyversion=self.ebics_config_id.ebics_key_version, keyversion=self.ebics_config_id.ebics_key_version,
bitlength=self.ebics_config_id.ebics_key_bitlength) bitlength=self.ebics_config_id.ebics_key_bitlength,
)
except Exception: except Exception:
exctype, value = exc_info()[:2] exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:") error = _("EBICS Initialisation Error:")
error += '\n' + str(exctype) + '\n' + str(value) error += "\n" + str(exctype) + "\n" + str(value)
raise UserError(error) raise UserError(error)
if self.swift_3skey and not self.ebics_key_x509: if self.swift_3skey and not self.ebics_key_x509:
raise UserError(_( raise UserError(
_(
"The current version of this module " "The current version of this module "
"requires to X509 support when enabling 3SKey")) "requires to X509 support when enabling 3SKey"
)
)
if self.ebics_key_x509: if self.ebics_key_x509:
dn_attrs = { dn_attrs = {
'commonName': self.ebics_key_x509_dn_cn, "commonName": self.ebics_key_x509_dn_cn,
'organizationName': self.ebics_key_x509_dn_o, "organizationName": self.ebics_key_x509_dn_o,
'organizationalUnitName': self.ebics_key_x509_dn_ou, "organizationalUnitName": self.ebics_key_x509_dn_ou,
'countryName': self.ebics_key_x509_dn_c, "countryName": self.ebics_key_x509_dn_c,
'stateOrProvinceName': self.ebics_key_x509_dn_st, "stateOrProvinceName": self.ebics_key_x509_dn_st,
'localityName': self.ebics_key_x509_dn_l, "localityName": self.ebics_key_x509_dn_l,
'emailAddress': self.ebics_key_x509_dn_e, "emailAddress": self.ebics_key_x509_dn_e,
} }
kwargs = {k: v for k, v in dn_attrs.items() if v} kwargs = {k: v for k, v in dn_attrs.items() if v}
user.create_certificates(**kwargs) user.create_certificates(**kwargs)
client = EbicsClient( client = EbicsClient(bank, user, version=ebics_version)
bank, user, version=ebics_version)
# Send the public electronic signature key to the bank. # Send the public electronic signature key to the bank.
ebics_config_bank = self.ebics_config_id.journal_ids[0].bank_id ebics_config_bank = self.ebics_config_id.journal_ids[0].bank_id
if not ebics_config_bank: if not ebics_config_bank:
raise UserError(_( raise UserError(
"No bank defined for the financial journal " _("No bank defined for the financial journal " "of the EBICS Config")
"of the EBICS Config")) )
try: try:
supported_versions = client.HEV() supported_versions = client.HEV()
if ebics_version not in supported_versions: if ebics_version not in supported_versions:
err_msg = _("EBICS version mismatch.") + "\n" err_msg = _("EBICS version mismatch.") + "\n"
err_msg += _("Versions supported by your bank:") err_msg += _("Versions supported by your bank:")
for k in supported_versions: for k in supported_versions:
err_msg += "\n%s: %s " % (k, supported_versions[k]) err_msg += "\n{}: {} ".format(k, supported_versions[k])
raise UserError(err_msg) raise UserError(err_msg)
if ebics_version == 'H003': if ebics_version == "H003":
bank._order_number = self.ebics_config_id._get_order_number() bank._order_number = self.ebics_config_id._get_order_number()
OrderID = client.INI() OrderID = client.INI()
_logger.info( _logger.info("%s, EBICS INI command, OrderID=%s", self._name, OrderID)
'%s, EBICS INI command, OrderID=%s', self._name, OrderID) if ebics_version == "H003":
if ebics_version == 'H003':
self.ebics_config_id._update_order_number(OrderID) self.ebics_config_id._update_order_number(OrderID)
except URLError: except URLError:
exctype, value = exc_info()[:2] exctype, value = exc_info()[:2]
@ -303,69 +322,68 @@ class EbicsUserID(models.Model):
self.name, self.name,
tb, tb,
) )
raise UserError(_( raise UserError(
"urlopen error:\n url '%s' - %s") _("urlopen error:\n url '%s' - %s")
% (self.ebics_config_id.ebics_url, str(value))) % (self.ebics_config_id.ebics_url, str(value))
)
except EbicsFunctionalError: except EbicsFunctionalError:
e = exc_info() e = exc_info()
error = _("EBICS Functional Error:") error = _("EBICS Functional Error:")
error += '\n' error += "\n"
error += '%s (code: %s)' % (e[1].message, e[1].code) error += "{} (code: {})".format(e[1].message, e[1].code)
raise UserError(error) raise UserError(error)
except EbicsTechnicalError: except EbicsTechnicalError:
e = exc_info() e = exc_info()
error = _("EBICS Technical Error:") error = _("EBICS Technical Error:")
error += '\n' error += "\n"
error += '%s (code: %s)' % (e[1].message, e[1].code) error += "{} (code: {})".format(e[1].message, e[1].code)
raise UserError(error) raise UserError(error)
# Send the public authentication and encryption keys to the bank. # Send the public authentication and encryption keys to the bank.
if ebics_version == 'H003': if ebics_version == "H003":
bank._order_number = self.ebics_config_id._get_order_number() bank._order_number = self.ebics_config_id._get_order_number()
OrderID = client.HIA() OrderID = client.HIA()
_logger.info('%s, EBICS HIA command, OrderID=%s', self._name, OrderID) _logger.info("%s, EBICS HIA command, OrderID=%s", self._name, OrderID)
if ebics_version == 'H003': if ebics_version == "H003":
self.ebics_config_id._update_order_number(OrderID) self.ebics_config_id._update_order_number(OrderID)
# Create an INI-letter which must be printed and sent to the bank. # Create an INI-letter which must be printed and sent to the bank.
ebics_config_bank = self.ebics_config_id.journal_ids[0].bank_id ebics_config_bank = self.ebics_config_id.journal_ids[0].bank_id
cc = ebics_config_bank.country.code cc = ebics_config_bank.country.code
if cc in ['FR', 'DE']: if cc in ["FR", "DE"]:
lang = cc lang = cc
else: else:
lang = self.env.user.lang or \ lang = self.env.user.lang or self.env["res.lang"].search([])[0].code
self.env['res.lang'].search([])[0].code
lang = lang[:2] lang = lang[:2]
tmp_dir = os.path.normpath(self.ebics_config_id.ebics_files + '/tmp') tmp_dir = os.path.normpath(self.ebics_config_id.ebics_files + "/tmp")
if not os.path.isdir(tmp_dir): if not os.path.isdir(tmp_dir):
os.makedirs(tmp_dir, mode=0o700) os.makedirs(tmp_dir, mode=0o700)
fn_date = fields.Date.today().isoformat() fn_date = fields.Date.today().isoformat()
fn = '_'.join( fn = "_".join([self.ebics_config_id.ebics_host, "ini_letter", fn_date]) + ".pdf"
[self.ebics_config_id.ebics_host, 'ini_letter', fn_date]) + '.pdf' full_tmp_fn = os.path.normpath(tmp_dir + "/" + fn)
full_tmp_fn = os.path.normpath(tmp_dir + '/' + fn)
user.create_ini_letter( user.create_ini_letter(
bankname=ebics_config_bank.name, bankname=ebics_config_bank.name, path=full_tmp_fn, lang=lang
path=full_tmp_fn, )
lang=lang) with open(full_tmp_fn, "rb") as f:
with open(full_tmp_fn, 'rb') as f:
letter = f.read() letter = f.read()
self.write({ self.write(
'ebics_ini_letter': base64.encodebytes(letter), {
'ebics_ini_letter_fn': fn, "ebics_ini_letter": base64.encodebytes(letter),
}) "ebics_ini_letter_fn": fn,
}
)
return self.write({'state': 'init'}) return self.write({"state": "init"})
def ebics_init_2(self): def ebics_init_2(self):
""" """
Initialization of bank keys - Step 2: Initialization of bank keys - Step 2:
Activation of the account by the bank. Activation of the account by the bank.
""" """
if self.state != 'init': if self.state != "init":
raise UserError( raise UserError(_("Set state to 'Initialisation'."))
_("Set state to 'Initialisation'."))
self.ensure_one() self.ensure_one()
return self.write({'state': 'get_bank_keys'}) return self.write({"state": "get_bank_keys"})
def ebics_init_3(self): def ebics_init_3(self):
""" """
@ -376,26 +394,27 @@ class EbicsUserID(models.Model):
""" """
self.ensure_one() self.ensure_one()
self.ebics_config_id._check_ebics_files() self.ebics_config_id._check_ebics_files()
if self.state != 'get_bank_keys': if self.state != "get_bank_keys":
raise UserError( raise UserError(_("Set state to 'Get Keys from Bank'."))
_("Set state to 'Get Keys from Bank'."))
try: try:
keyring = EbicsKeyRing( keyring = EbicsKeyRing(
keys=self.ebics_keys_fn, passphrase=self.ebics_passphrase) keys=self.ebics_keys_fn, passphrase=self.ebics_passphrase
)
bank = EbicsBank( bank = EbicsBank(
keyring=keyring, keyring=keyring,
hostid=self.ebics_config_id.ebics_host, hostid=self.ebics_config_id.ebics_host,
url=self.ebics_config_id.ebics_url) url=self.ebics_config_id.ebics_url,
)
user = EbicsUser( user = EbicsUser(
keyring=keyring, keyring=keyring,
partnerid=self.ebics_config_id.ebics_partner, partnerid=self.ebics_config_id.ebics_partner,
userid=self.name) userid=self.name,
client = EbicsClient( )
bank, user, version=self.ebics_config_id.ebics_version) client = EbicsClient(bank, user, version=self.ebics_config_id.ebics_version)
except Exception: except Exception:
exctype, value = exc_info()[:2] exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:") error = _("EBICS Initialisation Error:")
error += '\n' + str(exctype) + '\n' + str(value) error += "\n" + str(exctype) + "\n" + str(value)
raise UserError(error) raise UserError(error)
try: try:
@ -403,28 +422,31 @@ class EbicsUserID(models.Model):
except EbicsFunctionalError: except EbicsFunctionalError:
e = exc_info() e = exc_info()
error = _("EBICS Functional Error:") error = _("EBICS Functional Error:")
error += '\n' error += "\n"
error += '%s (code: %s)' % (e[1].message, e[1].code) error += "{} (code: {})".format(e[1].message, e[1].code)
raise UserError(error) raise UserError(error)
except Exception: except Exception:
exctype, value = exc_info()[:2] exctype, value = exc_info()[:2]
error = _("EBICS Initialisation Error:") error = _("EBICS Initialisation Error:")
error += '\n' + str(exctype) + '\n' + str(value) error += "\n" + str(exctype) + "\n" + str(value)
raise UserError(error) raise UserError(error)
public_bank_keys = public_bank_keys.encode() public_bank_keys = public_bank_keys.encode()
tmp_dir = os.path.normpath(self.ebics_config_id.ebics_files + '/tmp') tmp_dir = os.path.normpath(self.ebics_config_id.ebics_files + "/tmp")
if not os.path.isdir(tmp_dir): if not os.path.isdir(tmp_dir):
os.makedirs(tmp_dir, mode=0o700) os.makedirs(tmp_dir, mode=0o700)
fn_date = fields.Date.today().isoformat() fn_date = fields.Date.today().isoformat()
fn = '_'.join( fn = (
[self.ebics_config_id.ebics_host, 'public_bank_keys', fn_date] "_".join([self.ebics_config_id.ebics_host, "public_bank_keys", fn_date])
) + '.txt' + ".txt"
self.write({ )
'ebics_public_bank_keys': base64.encodestring(public_bank_keys), self.write(
'ebics_public_bank_keys_fn': fn, {
'state': 'to_verify', "ebics_public_bank_keys": base64.encodestring(public_bank_keys),
}) "ebics_public_bank_keys_fn": fn,
"state": "to_verify",
}
)
return True return True
@ -435,32 +457,32 @@ class EbicsUserID(models.Model):
and activate the bank keyu. and activate the bank keyu.
""" """
self.ensure_one() self.ensure_one()
if self.state != 'to_verify': if self.state != "to_verify":
raise UserError( raise UserError(_("Set state to 'Verification'."))
_("Set state to 'Verification'."))
keyring = EbicsKeyRing( keyring = EbicsKeyRing(
keys=self.ebics_keys_fn, passphrase=self.ebics_passphrase) keys=self.ebics_keys_fn, passphrase=self.ebics_passphrase
)
bank = EbicsBank( bank = EbicsBank(
keyring=keyring, keyring=keyring,
hostid=self.ebics_config_id.ebics_host, hostid=self.ebics_config_id.ebics_host,
url=self.ebics_config_id.ebics_url) url=self.ebics_config_id.ebics_url,
)
bank.activate_keys() bank.activate_keys()
return self.write({'state': 'active_keys'}) return self.write({"state": "active_keys"})
def change_passphrase(self): def change_passphrase(self):
self.ensure_one() self.ensure_one()
ctx = dict(self._context, default_ebics_userid_id=self.id) ctx = dict(self._context, default_ebics_userid_id=self.id)
module = __name__.split('addons.')[1].split('.')[0] module = __name__.split("addons.")[1].split(".")[0]
view = self.env.ref( view = self.env.ref("%s.ebics_change_passphrase_view_form" % module)
'%s.ebics_change_passphrase_view_form' % module)
return { return {
'name': _('EBICS keys change passphrase'), "name": _("EBICS keys change passphrase"),
'view_type': 'form', "view_type": "form",
'view_mode': 'form', "view_mode": "form",
'res_model': 'ebics.change.passphrase', "res_model": "ebics.change.passphrase",
'view_id': view.id, "view_id": view.id,
'target': 'new', "target": "new",
'context': ctx, "context": ctx,
'type': 'ir.actions.act_window', "type": "ir.actions.act_window",
} }

View File

@ -13,24 +13,25 @@ try:
import fintech import fintech
except ImportError: except ImportError:
fintech = None fintech = None
_logger.warning('Failed to import fintech') _logger.warning("Failed to import fintech")
fintech_register_name = config.get('fintech_register_name') fintech_register_name = config.get("fintech_register_name")
fintech_register_keycode = config.get('fintech_register_keycode') fintech_register_keycode = config.get("fintech_register_keycode")
fintech_register_users = config.get('fintech_register_users') fintech_register_users = config.get("fintech_register_users")
try: try:
if fintech: if fintech:
fintech_register_users = ( fintech_register_users = (
fintech_register_users fintech_register_users
and [x.strip() for x in fintech_register_users.split(',')] and [x.strip() for x in fintech_register_users.split(",")]
or None or None
) )
fintech.cryptolib = 'cryptography' fintech.cryptolib = "cryptography"
fintech.register( fintech.register(
name=fintech_register_name, name=fintech_register_name,
keycode=fintech_register_keycode, keycode=fintech_register_keycode,
users=fintech_register_users) users=fintech_register_users,
)
except RuntimeError as e: except RuntimeError as e:
if str(e) == "'register' can be called only once": if str(e) == "'register' can be called only once":
pass pass
@ -39,7 +40,7 @@ except RuntimeError as e:
fintech.register() fintech.register()
except Exception: except Exception:
msg = "fintech.register error" msg = "fintech.register error"
tb = ''.join(format_exception(*exc_info())) tb = "".join(format_exception(*exc_info()))
msg += '\n%s' % tb msg += "\n%s" % tb
_logger.error(msg) _logger.error(msg)
fintech.register() fintech.register()

View File

@ -12,21 +12,27 @@
<field name="name">EBICS Configuration model company rule</field> <field name="name">EBICS Configuration model company rule</field>
<field name="model_id" ref="model_ebics_config" /> <field name="model_id" ref="model_ebics_config" />
<field eval="True" name="global" /> <field eval="True" name="global" />
<field name="domain_force">['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field> <field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record> </record>
<record id="ebics_userid_comp_rule" model="ir.rule"> <record id="ebics_userid_comp_rule" model="ir.rule">
<field name="name">EBICS UserID model company rule</field> <field name="name">EBICS UserID model company rule</field>
<field name="model_id" ref="model_ebics_userid" /> <field name="model_id" ref="model_ebics_userid" />
<field eval="True" name="global" /> <field eval="True" name="global" />
<field name="domain_force">['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field> <field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record> </record>
<record id="ebics_file_comp_rule" model="ir.rule"> <record id="ebics_file_comp_rule" model="ir.rule">
<field name="name">EBICS File model company rule</field> <field name="name">EBICS File model company rule</field>
<field name="model_id" ref="model_ebics_file" /> <field name="model_id" ref="model_ebics_file" />
<field eval="True" name="global" /> <field eval="True" name="global" />
<field name="domain_force">['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field> <field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record> </record>
</data> </data>

View File

@ -20,19 +20,32 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="EBICS Configuration"> <form string="EBICS Configuration">
<header> <header>
<button name="set_to_draft" states="confirm" string="Set to Draft" type="object" <button
name="set_to_draft"
states="confirm"
string="Set to Draft"
type="object"
groups="account_ebics.group_ebics_manager" groups="account_ebics.group_ebics_manager"
help="Set to Draft in order to change the EBICS configuration parameters."/> help="Set to Draft in order to change the EBICS configuration parameters."
<button name="set_to_confirm" states="draft" string="Confirm" type="object" class="oe_highlight" />
<button
name="set_to_confirm"
states="draft"
string="Confirm"
type="object"
class="oe_highlight"
groups="account_ebics.group_ebics_manager" groups="account_ebics.group_ebics_manager"
help="The EBICS configuration must be confirmed before it can used for bank transactions."/> help="The EBICS configuration must be confirmed before it can used for bank transactions."
/>
<field name="state" widget="statusbar" /> <field name="state" widget="statusbar" />
</header> </header>
<field name="active" invisible="1" /> <field name="active" invisible="1" />
<widget name="web_ribbon" <widget
name="web_ribbon"
text="Archived" text="Archived"
bg_color="bg-danger" bg_color="bg-danger"
attrs="{'invisible': [('active', '=', True)]}"/> attrs="{'invisible': [('active', '=', True)]}"
/>
<group name="main"> <group name="main">
<group name="main-left"> <group name="main-left">
<field name="name" colspan="2" /> <field name="name" colspan="2" />
@ -43,12 +56,18 @@
<field name="ebics_keys" /> <field name="ebics_keys" />
</group> </group>
<group name="main-right"> <group name="main-right">
<field name="journal_ids" widget="many2many_tags" options="{'no_create': True}"/> <field
name="journal_ids"
widget="many2many_tags"
options="{'no_create': True}"
/>
<field name="ebics_version" /> <field name="ebics_version" />
<field name="ebics_key_version" /> <field name="ebics_key_version" />
<field name="ebics_key_bitlength" /> <field name="ebics_key_bitlength" />
<field name="order_number" <field
attrs="{'invisible': [('ebics_version', '=', 'H004')]}"/> name="order_number"
attrs="{'invisible': [('ebics_version', '=', 'H004')]}"
/>
</group> </group>
<field name="company_ids" invisible="1" /> <field name="company_ids" invisible="1" />
</group> </group>

View File

@ -24,9 +24,11 @@
<group name="main-left"> <group name="main-left">
<field name="type" /> <field name="type" />
<field name="suffix" /> <field name="suffix" />
<field name="download_process_method" <field
name="download_process_method"
attrs="{'invisible': [('type', '=', 'up')]}" attrs="{'invisible': [('type', '=', 'up')]}"
force_save="1"/> force_save="1"
/>
<field name="signature_class" /> <field name="signature_class" />
</group> </group>
<group name="main-right"> <group name="main-right">

View File

@ -12,11 +12,19 @@
<field name="name" /> <field name="name" />
<field name="format_id" /> <field name="format_id" />
<field name="user_id" /> <field name="user_id" />
<field name="company_ids" widget="selection" groups="base.group_multi_company"/> <field
name="company_ids"
widget="selection"
groups="base.group_multi_company"
/>
</group> </group>
<newline /> <newline />
<group expand="0" string="Group By"> <group expand="0" string="Group By">
<filter string="File Format" name="file_format" context="{'group_by':'format_id'}"/> <filter
string="File Format"
name="file_format"
context="{'group_by':'format_id'}"
/>
<filter string="State" name="state" context="{'group_by':'state'}" /> <filter string="State" name="state" context="{'group_by':'state'}" />
<filter string="User" name="user" context="{'group_by':'user_id'}" /> <filter string="User" name="user" context="{'group_by':'user_id'}" />
</group> </group>
@ -38,7 +46,11 @@
<field name="user_id" /> <field name="user_id" />
<field name="state" /> <field name="state" />
<field name="format_id" /> <field name="format_id" />
<field name="company_ids" widget="many2many_tags" groups="base.group_multi_company"/> <field
name="company_ids"
widget="many2many_tags"
groups="base.group_multi_company"
/>
</tree> </tree>
</field> </field>
</record> </record>
@ -50,15 +62,29 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="EBICS File" create="false"> <form string="EBICS File" create="false">
<header> <header>
<button name="set_to_draft" states="done" string="Set to Draft" type="object" groups="account.group_account_manager"/> <button
<button name="process" name="set_to_draft"
states="done"
string="Set to Draft"
type="object"
groups="account.group_account_manager"
/>
<button
name="process"
class="oe_highlight" class="oe_highlight"
states="draft" states="draft"
string="Process" string="Process"
type="object" type="object"
groups="account.group_account_invoice" groups="account.group_account_invoice"
help="Process the EBICS File"/> help="Process the EBICS File"
<button name="set_to_done" states="draft" string="Set to Done" type="object" groups="account.group_account_manager"/> />
<button
name="set_to_done"
states="draft"
string="Set to Done"
type="object"
groups="account.group_account_manager"
/>
<field name="state" widget="statusbar" /> <field name="state" widget="statusbar" />
</header> </header>
<group colspan="4" col="4"> <group colspan="4" col="4">
@ -70,13 +96,20 @@
<field name="date_to" /> <field name="date_to" />
<field name="user_id" /> <field name="user_id" />
<field name="ebics_userid_id" /> <field name="ebics_userid_id" />
<field name="company_ids" widget="many2many_tags" groups="base.group_multi_company"/> <field
name="company_ids"
widget="many2many_tags"
groups="base.group_multi_company"
/>
</group> </group>
<notebook> <notebook>
<page string="Additional Information"> <page string="Additional Information">
<field name="note" nolabel="1" /> <field name="note" nolabel="1" />
</page> </page>
<page string="Bank Statements" attrs="{'invisible':[('bank_statement_ids','=',[])]}"> <page
string="Bank Statements"
attrs="{'invisible':[('bank_statement_ids','=',[])]}"
>
<field name="bank_statement_ids" nolabel="1" /> <field name="bank_statement_ids" nolabel="1" />
</page> </page>
</notebook> </notebook>
@ -93,9 +126,13 @@
<separator colspan="4" string="Results :" /> <separator colspan="4" string="Results :" />
<field name="note_process" colspan="4" nolabel="1" width="850" height="400" /> <field name="note_process" colspan="4" nolabel="1" width="850" height="400" />
<footer> <footer>
<button name="action_open_bank_statements" string="View Bank Statement(s)" <button
type="object" class="oe_highlight" name="action_open_bank_statements"
invisible="not context.get('statement_ids')"/> string="View Bank Statement(s)"
type="object"
class="oe_highlight"
invisible="not context.get('statement_ids')"
/>
<button name="button_close" type="object" string="Close" /> <button name="button_close" type="object" string="Close" />
</footer> </footer>
</form> </form>
@ -138,7 +175,11 @@
<field name="user_id" /> <field name="user_id" />
<field name="state" /> <field name="state" />
<field name="format_id" /> <field name="format_id" />
<field name="company_ids" widget="many2many_tags" groups="base.group_multi_company"/> <field
name="company_ids"
widget="many2many_tags"
groups="base.group_multi_company"
/>
</tree> </tree>
</field> </field>
</record> </record>
@ -150,8 +191,20 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="EBICS File" create="false"> <form string="EBICS File" create="false">
<header> <header>
<button name="set_to_draft" states="done" string="Set to Draft" type="object" groups="account.group_account_manager"/> <button
<button name="set_to_done" states="draft" string="Set to Done" type="object" groups="account.group_account_manager"/> name="set_to_draft"
states="done"
string="Set to Draft"
type="object"
groups="account.group_account_manager"
/>
<button
name="set_to_done"
states="draft"
string="Set to Done"
type="object"
groups="account.group_account_manager"
/>
<field name="state" widget="statusbar" /> <field name="state" widget="statusbar" />
</header> </header>
<group colspan="4" col="4"> <group colspan="4" col="4">
@ -161,7 +214,11 @@
<field name="format_id" /> <field name="format_id" />
<field name="user_id" /> <field name="user_id" />
<field name="ebics_userid_id" /> <field name="ebics_userid_id" />
<field name="company_ids" widget="many2many_tags" groups="base.group_multi_company"/> <field
name="company_ids"
widget="many2many_tags"
groups="base.group_multi_company"
/>
</group> </group>
<notebook> <notebook>
<page string="Additional Information"> <page string="Additional Information">

View File

@ -20,22 +20,66 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="EBICS UserID"> <form string="EBICS UserID">
<header groups="account_ebics.group_ebics_manager"> <header groups="account_ebics.group_ebics_manager">
<button name="ebics_init_1" states="draft" string="EBICS Initialisation" type="object" class="oe_highlight" <button
help="Initialise EBICS Bank Keys"/> name="ebics_init_1"
<button name="ebics_init_2" states="init" string="Account activated" type="object" class="oe_highlight" states="draft"
help="EBICS Initialisation - Push this button when the account has been activated by the bank."/> string="EBICS Initialisation"
<button name="ebics_init_3" states="get_bank_keys" string="Get Bank Keys" type="object" class="oe_highlight" type="object"
help="EBICS Initialisation - After the account has been activated the public bank keys must be downloaded and checked for consistency."/> class="oe_highlight"
<button name="ebics_init_4" states="to_verify" string="Bank Keys Verified" type="object" class="oe_highlight" help="Initialise EBICS Bank Keys"
help="EBICS Initialisation - Push this button when the public have been checked for consistency."/> />
<button name="change_passphrase" string="Change Passphrase" type="object" class="oe_highlight" <button
attrs="{'invisible': [('ebics_keys_found', '=', False)]}"/> name="ebics_init_2"
<button name="set_to_draft" states="active_keys" string="Set to Draft" type="object" states="init"
help="Set to Draft in order to reinitialize your bank connection."/> string="Account activated"
<button name="set_to_get_bank_keys" states="active_keys" string="Renew Bank Keys" type="object" type="object"
help="Use this button to update the EBICS certificates of your bank."/> class="oe_highlight"
<button name="set_to_active_keys" states="draft" string="Force Active Keys" type="object" help="EBICS Initialisation - Push this button when the account has been activated by the bank."
help="Use this button to bypass the EBICS initialization (e.g. in case you have manually transferred active EBICS keys from another system."/> />
<button
name="ebics_init_3"
states="get_bank_keys"
string="Get Bank Keys"
type="object"
class="oe_highlight"
help="EBICS Initialisation - After the account has been activated the public bank keys must be downloaded and checked for consistency."
/>
<button
name="ebics_init_4"
states="to_verify"
string="Bank Keys Verified"
type="object"
class="oe_highlight"
help="EBICS Initialisation - Push this button when the public have been checked for consistency."
/>
<button
name="change_passphrase"
string="Change Passphrase"
type="object"
class="oe_highlight"
attrs="{'invisible': [('ebics_keys_found', '=', False)]}"
/>
<button
name="set_to_draft"
states="active_keys"
string="Set to Draft"
type="object"
help="Set to Draft in order to reinitialize your bank connection."
/>
<button
name="set_to_get_bank_keys"
states="active_keys"
string="Renew Bank Keys"
type="object"
help="Use this button to update the EBICS certificates of your bank."
/>
<button
name="set_to_active_keys"
states="draft"
string="Force Active Keys"
type="object"
help="Use this button to bypass the EBICS initialization (e.g. in case you have manually transferred active EBICS keys from another system."
/>
<field name="state" widget="statusbar" /> <field name="state" widget="statusbar" />
</header> </header>
<group name="main" attrs="{'readonly': [('state', '!=', 'draft')]}"> <group name="main" attrs="{'readonly': [('state', '!=', 'draft')]}">
@ -43,24 +87,41 @@
<field name="ebics_keys_fn" invisible="1" /> <field name="ebics_keys_fn" invisible="1" />
<group name="main-left"> <group name="main-left">
<field name="name" /> <field name="name" />
<field name="ebics_passphrase" password="True" <field
attrs="{'required': [('state', '=', 'draft')]}"/> name="ebics_passphrase"
<field name="swift_3skey" password="True"
attrs="{'invisible': [('signature_class', '=', 'T')]}"/> attrs="{'required': [('state', '=', 'draft')]}"
/>
<field
name="swift_3skey"
attrs="{'invisible': [('signature_class', '=', 'T')]}"
/>
<field name="swift_3skey_certificate_fn" invisible="1" /> <field name="swift_3skey_certificate_fn" invisible="1" />
<field name="swift_3skey_certificate" filename="swift_3skey_certificate_fn" <field
attrs="{'invisible': [('swift_3skey', '=', False)], 'required': [('swift_3skey', '=', True)]}"/> name="swift_3skey_certificate"
filename="swift_3skey_certificate_fn"
attrs="{'invisible': [('swift_3skey', '=', False)], 'required': [('swift_3skey', '=', True)]}"
/>
<field name="active" /> <field name="active" />
</group> </group>
<group name="main-right"> <group name="main-right">
<field name="signature_class" /> <field name="signature_class" />
<field name="user_ids" widget="many2many_tags" options="{'no_create': True}"/> <field
name="user_ids"
widget="many2many_tags"
options="{'no_create': True}"
/>
<field name="ebics_key_x509" /> <field name="ebics_key_x509" />
</group> </group>
</group> </group>
<group col="4" name="dn" attrs="{'invisible': [('ebics_key_x509', '=', False)], 'readonly': [('state', '!=', 'draft')]}"> <group
col="4"
name="dn"
attrs="{'invisible': [('ebics_key_x509', '=', False)], 'readonly': [('state', '!=', 'draft')]}"
>
<group colspan="4" col="1"> <group colspan="4" col="1">
<strong>Distinguished Name attributes used to create self-signed X.509 certificates:</strong> <strong
>Distinguished Name attributes used to create self-signed X.509 certificates:</strong>
</group> </group>
<group name="dn_l" colspan="2"> <group name="dn_l" colspan="2">
<field name="ebics_key_x509_dn_cn" /> <field name="ebics_key_x509_dn_cn" />
@ -74,11 +135,19 @@
<field name="ebics_key_x509_dn_st" /> <field name="ebics_key_x509_dn_st" />
</group> </group>
</group> </group>
<group colspan="4" name="ebics_ini_letter" attrs="{'invisible': [('ebics_ini_letter', '=', False)]}"> <group
colspan="4"
name="ebics_ini_letter"
attrs="{'invisible': [('ebics_ini_letter', '=', False)]}"
>
<field name="ebics_ini_letter_fn" invisible="1" /> <field name="ebics_ini_letter_fn" invisible="1" />
<field name="ebics_ini_letter" filename="ebics_ini_letter_fn" /> <field name="ebics_ini_letter" filename="ebics_ini_letter_fn" />
</group> </group>
<group colspan="4" name="ebics_public_bank_keys" attrs="{'invisible': [('ebics_public_bank_keys', '=', False)]}"> <group
colspan="4"
name="ebics_public_bank_keys"
attrs="{'invisible': [('ebics_public_bank_keys', '=', False)]}"
>
<field name="ebics_public_bank_keys_fn" invisible="1" /> <field name="ebics_public_bank_keys_fn" invisible="1" />
<field name="ebics_public_bank_keys" filename="ebics_public_bank_keys_fn" /> <field name="ebics_public_bank_keys" filename="ebics_public_bank_keys_fn" />
</group> </group>

View File

@ -1,58 +1,76 @@
<?xml version="1.0" ?> <?xml version="1.0" ?>
<odoo> <odoo>
<menuitem id="ebics_processing_menu" <menuitem
id="ebics_processing_menu"
name="EBICS Processing" name="EBICS Processing"
parent="account.menu_finance" parent="account.menu_finance"
sequence="4"/> sequence="4"
/>
<menuitem id="ebics_xfer_menu_download" <menuitem
id="ebics_xfer_menu_download"
name="EBICS Download" name="EBICS Download"
parent="ebics_processing_menu" parent="ebics_processing_menu"
action="ebics_xfer_action_download" action="ebics_xfer_action_download"
sequence="10"/> sequence="10"
/>
<menuitem id="ebics_xfer_menu_upload" <menuitem
id="ebics_xfer_menu_upload"
name="EBICS Upload" name="EBICS Upload"
parent="ebics_processing_menu" parent="ebics_processing_menu"
action="ebics_xfer_action_upload" action="ebics_xfer_action_upload"
sequence="20"/> sequence="20"
/>
<menuitem id="ebics_file_menu" <menuitem
id="ebics_file_menu"
name="EBICS Files" name="EBICS Files"
parent="ebics_processing_menu" parent="ebics_processing_menu"
sequence="30"/> sequence="30"
/>
<menuitem id="ebics_file_menu_download" <menuitem
id="ebics_file_menu_download"
name="Download" name="Download"
parent="ebics_file_menu" parent="ebics_file_menu"
action="ebics_file_action_download" action="ebics_file_action_download"
sequence="10"/> sequence="10"
/>
<menuitem id="ebics_file_menu_upload" <menuitem
id="ebics_file_menu_upload"
name="Upload" name="Upload"
parent="ebics_file_menu" parent="ebics_file_menu"
action="ebics_file_action_upload" action="ebics_file_action_upload"
sequence="20"/> sequence="20"
/>
<menuitem id="ebics_menu" <menuitem
id="ebics_menu"
name="EBICS" name="EBICS"
parent='account.menu_finance_configuration' parent='account.menu_finance_configuration'
groups="account_ebics.group_ebics_manager" groups="account_ebics.group_ebics_manager"
sequence="100"/> sequence="100"
/>
<menuitem id="ebics_config_menu" <menuitem
id="ebics_config_menu"
name="EBICS Configuration" name="EBICS Configuration"
parent="ebics_menu" parent="ebics_menu"
action="ebics_config_action" action="ebics_config_action"
groups="account_ebics.group_ebics_manager" groups="account_ebics.group_ebics_manager"
sequence="10"/> sequence="10"
/>
<menuitem id="ebics_file_format_menu" <menuitem
id="ebics_file_format_menu"
name="EBICS File Formats" name="EBICS File Formats"
parent="ebics_menu" parent="ebics_menu"
action="ebics_file_format_action" action="ebics_file_format_action"
groups="account_ebics.group_ebics_manager" groups="account_ebics.group_ebics_manager"
sequence="20"/> sequence="20"
/>
</odoo> </odoo>

View File

@ -11,65 +11,58 @@ _logger = logging.getLogger(__name__)
try: try:
import fintech import fintech
from fintech.ebics import EbicsKeyRing from fintech.ebics import EbicsKeyRing
fintech.cryptolib = 'cryptography'
fintech.cryptolib = "cryptography"
except ImportError: except ImportError:
_logger.warning('Failed to import fintech') _logger.warning("Failed to import fintech")
class EbicsChangePassphrase(models.TransientModel): class EbicsChangePassphrase(models.TransientModel):
_name = 'ebics.change.passphrase' _name = "ebics.change.passphrase"
_description = 'Change EBICS keys passphrase' _description = "Change EBICS keys passphrase"
ebics_userid_id = fields.Many2one( ebics_userid_id = fields.Many2one(
comodel_name='ebics.userid', comodel_name="ebics.userid", string="EBICS UserID", readonly=True
string='EBICS UserID', )
readonly=True) old_pass = fields.Char(string="Old Passphrase", required=True)
old_pass = fields.Char( new_pass = fields.Char(string="New Passphrase", required=True)
string='Old Passphrase', new_pass_check = fields.Char(string="New Passphrase (verification)", required=True)
required=True) note = fields.Text(string="Notes", readonly=True)
new_pass = fields.Char(
string='New Passphrase',
required=True)
new_pass_check = fields.Char(
string='New Passphrase (verification)',
required=True)
note = fields.Text(string='Notes', readonly=True)
def change_passphrase(self): def change_passphrase(self):
self.ensure_one() self.ensure_one()
if self.old_pass != self.ebics_userid_id.ebics_passphrase: if self.old_pass != self.ebics_userid_id.ebics_passphrase:
raise UserError(_( raise UserError(_("Incorrect old passphrase."))
"Incorrect old passphrase."))
if self.new_pass != self.new_pass_check: if self.new_pass != self.new_pass_check:
raise UserError(_( raise UserError(_("New passphrase verification error."))
"New passphrase verification error."))
if self.new_pass == self.ebics_userid_id.ebics_passphrase: if self.new_pass == self.ebics_userid_id.ebics_passphrase:
raise UserError(_( raise UserError(_("New passphrase equal to old passphrase."))
"New passphrase equal to old passphrase."))
try: try:
keyring = EbicsKeyRing( keyring = EbicsKeyRing(
keys=self.ebics_userid_id.ebics_keys_fn, keys=self.ebics_userid_id.ebics_keys_fn,
passphrase=self.ebics_userid_id.ebics_passphrase) passphrase=self.ebics_userid_id.ebics_passphrase,
)
keyring.change_passphrase(self.new_pass) keyring.change_passphrase(self.new_pass)
except ValueError as e: except ValueError as e:
raise UserError(str(e)) raise UserError(str(e))
self.ebics_userid.ebics_passphrase = self.new_pass self.ebics_userid.ebics_passphrase = self.new_pass
self.note = "The EBICS Passphrase has been changed." self.note = "The EBICS Passphrase has been changed."
module = __name__.split('addons.')[1].split('.')[0] module = __name__.split("addons.")[1].split(".")[0]
result_view = self.env.ref( result_view = self.env.ref(
'%s.ebics_change_passphrase_view_form_result' % module) "%s.ebics_change_passphrase_view_form_result" % module
)
return { return {
'name': _('EBICS Keys Change Passphrase'), "name": _("EBICS Keys Change Passphrase"),
'res_id': self.id, "res_id": self.id,
'view_type': 'form', "view_type": "form",
'view_mode': 'form', "view_mode": "form",
'res_model': 'ebics.change.passphrase', "res_model": "ebics.change.passphrase",
'view_id': result_view.id, "view_id": result_view.id,
'target': 'new', "target": "new",
'type': 'ir.actions.act_window', "type": "ir.actions.act_window",
} }
def button_close(self): def button_close(self):
self.ensure_one() self.ensure_one()
return {'type': 'ir.actions.act_window_close'} return {"type": "ir.actions.act_window_close"}

View File

@ -13,7 +13,12 @@
<field name="new_pass_check" password="True" /> <field name="new_pass_check" password="True" />
</group> </group>
<footer> <footer>
<button name="change_passphrase" string="Change Passphrase" type="object" class="oe_highlight"/> <button
name="change_passphrase"
string="Change Passphrase"
type="object"
class="oe_highlight"
/>
or or
<button string="Cancel" class="oe_link" special="cancel" /> <button string="Cancel" class="oe_link" special="cancel" />
</footer> </footer>

View File

@ -14,125 +14,146 @@ import os
from sys import exc_info from sys import exc_info
from traceback import format_exception from traceback import format_exception
from odoo import api, fields, models, _ from odoo import _, api, fields, models
from odoo.exceptions import UserError from odoo.exceptions import UserError
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
try: try:
import fintech import fintech
from fintech.ebics import EbicsKeyRing, EbicsBank, EbicsUser, EbicsClient,\ from fintech.ebics import (
EbicsFunctionalError, EbicsTechnicalError, EbicsVerificationError EbicsBank,
fintech.cryptolib = 'cryptography' EbicsClient,
EbicsFunctionalError,
EbicsKeyRing,
EbicsTechnicalError,
EbicsUser,
EbicsVerificationError,
)
fintech.cryptolib = "cryptography"
except ImportError: except ImportError:
EbicsBank = object EbicsBank = object
_logger.warning('Failed to import fintech') _logger.warning("Failed to import fintech")
class EbicsBank(EbicsBank): class EbicsBank(EbicsBank):
def _next_order_id(self, partnerid): def _next_order_id(self, partnerid):
""" """
EBICS protocol version H003 requires generation of the OrderID. EBICS protocol version H003 requires generation of the OrderID.
The OrderID must be a string between 'A000' and 'ZZZZ' and The OrderID must be a string between 'A000' and 'ZZZZ' and
unique for each partner id. 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): class EbicsXfer(models.TransientModel):
_name = 'ebics.xfer' _name = "ebics.xfer"
_description = 'EBICS file transfer' _description = "EBICS file transfer"
ebics_config_id = fields.Many2one( ebics_config_id = fields.Many2one(
comodel_name='ebics.config', comodel_name="ebics.config",
string='EBICS Configuration', string="EBICS Configuration",
domain=[('state', '=', 'confirm')], domain=[("state", "=", "confirm")],
default=lambda self: self._default_ebics_config_id()) default=lambda self: self._default_ebics_config_id(),
)
ebics_userid_id = fields.Many2one( ebics_userid_id = fields.Many2one(
comodel_name='ebics.userid', comodel_name="ebics.userid", string="EBICS UserID"
string='EBICS UserID') )
ebics_passphrase = fields.Char( ebics_passphrase = fields.Char(string="EBICS Passphrase")
string='EBICS Passphrase')
date_from = fields.Date() date_from = fields.Date()
date_to = fields.Date() date_to = fields.Date()
upload_data = fields.Binary(string='File to Upload') upload_data = fields.Binary(string="File to Upload")
upload_fname = fields.Char( upload_fname = fields.Char(string="Upload Filename", default="")
string='Upload Filename', default='')
upload_fname_dummy = fields.Char( 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( format_id = fields.Many2one(
comodel_name='ebics.file.format', comodel_name="ebics.file.format",
string='EBICS File Format', string="EBICS File Format",
help="Select EBICS File Format to upload/download." 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( allowed_format_ids = fields.Many2many(
related='ebics_config_id.ebics_file_format_ids', related="ebics_config_id.ebics_file_format_ids",
string='Allowed EBICS File Formats') string="Allowed EBICS File Formats",
)
order_type = fields.Char( order_type = fields.Char(
related='format_id.order_type', related="format_id.order_type",
string='Order Type', string="Order Type",
help="For most banks in France you should use the " help="For most banks in France you should use the "
"format neutral Order Types 'FUL' for upload " "format neutral Order Types 'FUL' for upload "
"and 'FDL' for download.") "and 'FDL' for download.",
)
test_mode = fields.Boolean( test_mode = fields.Boolean(
string='Test Mode', string="Test Mode",
help="Select this option to test if the syntax of " help="Select this option to test if the syntax of "
"the upload file is correct." "the upload file is correct."
"\nThis option is only available for " "\nThis option is only available for "
"Order Type 'FUL'.") "Order Type 'FUL'.",
note = fields.Text(string='EBICS file transfer Log', readonly=True) )
note = fields.Text(string="EBICS file transfer Log", readonly=True)
@api.model @api.model
def _default_ebics_config_id(self): def _default_ebics_config_id(self):
cfg_mod = self.env['ebics.config'] cfg_mod = self.env["ebics.config"]
cfg = cfg_mod.search( 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: if cfg and len(cfg) == 1:
return cfg return cfg
else: else:
return cfg_mod return cfg_mod
@api.onchange('ebics_config_id') @api.onchange("ebics_config_id")
def _onchange_ebics_config_id(self): def _onchange_ebics_config_id(self):
ebics_userids = self.ebics_config_id.ebics_userid_ids ebics_userids = self.ebics_config_id.ebics_userid_ids
if self._context.get('ebics_download'): if self._context.get("ebics_download"):
download_formats = self.ebics_config_id.ebics_file_format_ids\ download_formats = self.ebics_config_id.ebics_file_format_ids.filtered(
.filtered(lambda r: r.type == 'down') lambda r: r.type == "down"
)
if len(download_formats) == 1: if len(download_formats) == 1:
self.format_id = download_formats self.format_id = download_formats
if len(ebics_userids) == 1: if len(ebics_userids) == 1:
self.ebics_userid_id = ebics_userids self.ebics_userid_id = ebics_userids
else: else:
transport_users = ebics_userids.filtered( transport_users = ebics_userids.filtered(
lambda r: r.signature_class == 'T') lambda r: r.signature_class == "T"
)
if len(transport_users) == 1: if len(transport_users) == 1:
self.ebics_userid_id = transport_users self.ebics_userid_id = transport_users
else: else:
upload_formats = self.ebics_config_id.ebics_file_format_ids\ upload_formats = self.ebics_config_id.ebics_file_format_ids.filtered(
.filtered(lambda r: r.type == 'up') lambda r: r.type == "up"
)
if len(upload_formats) == 1: if len(upload_formats) == 1:
self.format_id = upload_formats self.format_id = upload_formats
if len(ebics_userids) == 1: if len(ebics_userids) == 1:
self.ebics_userid_id = ebics_userids self.ebics_userid_id = ebics_userids
@api.onchange('upload_data') @api.onchange("upload_data")
def _onchange_upload_data(self): def _onchange_upload_data(self):
self.upload_fname_dummy = self.upload_fname self.upload_fname_dummy = self.upload_fname
self.format_id = False self.format_id = False
self._detect_upload_format() self._detect_upload_format()
if not self.format_id: 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( 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: if len(upload_formats) > 1:
upload_formats = upload_formats.filtered( 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: if len(upload_formats) == 1:
self.format_id = upload_formats self.format_id = upload_formats
@api.onchange('format_id') @api.onchange("format_id")
def _onchange_format_id(self): def _onchange_format_id(self):
self.order_type = self.format_id.order_type self.order_type = self.format_id.order_type
@ -141,138 +162,152 @@ class EbicsXfer(models.TransientModel):
ctx = self._context.copy() ctx = self._context.copy()
ebics_file = self._ebics_upload() ebics_file = self._ebics_upload()
if ebics_file: if ebics_file:
ctx['ebics_file_id'] = ebics_file.id ctx["ebics_file_id"] = ebics_file.id
module = __name__.split('addons.')[1].split('.')[0] module = __name__.split("addons.")[1].split(".")[0]
result_view = self.env.ref( result_view = self.env.ref("%s.ebics_xfer_view_form_result" % module)
'%s.ebics_xfer_view_form_result' % module)
return { return {
'name': _('EBICS file transfer result'), "name": _("EBICS file transfer result"),
'res_id': self.id, "res_id": self.id,
'view_type': 'form', "view_type": "form",
'view_mode': 'form', "view_mode": "form",
'res_model': 'ebics.xfer', "res_model": "ebics.xfer",
'view_id': result_view.id, "view_id": result_view.id,
'target': 'new', "target": "new",
'context': ctx, "context": ctx,
'type': 'ir.actions.act_window', "type": "ir.actions.act_window",
} }
def ebics_download(self): def ebics_download(self):
self.ensure_one() self.ensure_one()
self.ebics_config_id._check_ebics_files() self.ebics_config_id._check_ebics_files()
ctx = self._context.copy() ctx = self.env.context.copy()
self.note = '' self.note = ""
err_cnt = 0
client = self._setup_client() 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 = ( download_formats = (
self.format_id self.format_id
or self.ebics_config_id.ebics_file_format_ids.filtered( 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_from = self.date_from and self.date_from.isoformat() or None
date_to = self.date_to and self.date_to.isoformat() or None date_to = self.date_to and self.date_to.isoformat() or None
for df in download_formats: for df in download_formats:
try: try:
success = False success = False
if df.order_type == 'FDL': if df.order_type == "FDL":
data = client.FDL(df.name, date_from, date_to) data = client.FDL(df.name, date_from, date_to)
else: else:
params = None params = None
if date_from and date_to: if date_from and date_to:
params = {'DateRange': { params = {
'Start': date_from, "DateRange": {
'End': date_to, "Start": date_from,
}} "End": date_to,
}
}
data = client.download(df.order_type, params=params) data = client.download(df.order_type, params=params)
ebics_files += self._handle_download_data(data, df) ebics_files += self._handle_download_data(data, df)
success = True success = True
except EbicsFunctionalError: except EbicsFunctionalError:
err_cnt += 1
e = exc_info() e = exc_info()
self.note += '\n' self.note += "\n"
self.note += _( self.note += _(
"EBICS Functional Error during download of File Format %s (%s):" "EBICS Functional Error during download of File Format %s (%s):"
) % (df.name, df.order_type) ) % (df.name, df.order_type)
self.note += '\n' self.note += "\n"
self.note += '%s (code: %s)' % (e[1].message, e[1].code) self.note += "{} (code: {})".format(e[1].message, e[1].code)
except EbicsTechnicalError: except EbicsTechnicalError:
err_cnt += 1
e = exc_info() e = exc_info()
self.note += '\n' self.note += "\n"
self.note += _( self.note += _(
"EBICS Technical Error during download of File Format %s (%s):" "EBICS Technical Error during download of File Format %s (%s):"
) % (df.name, df.order_type) ) % (df.name, df.order_type)
self.note += '\n' self.note += "\n"
self.note += '%s (code: %s)' % (e[1].message, e[1].code) self.note += "{} (code: {})".format(e[1].message, e[1].code)
except EbicsVerificationError: except EbicsVerificationError:
self.note += '\n' err_cnt += 1
self.note += "\n"
self.note += _( self.note += _(
"EBICS Verification Error during download of " "EBICS Verification Error during download of "
"File Format %s (%s):" "File Format %s (%s):"
) % (df.name, df.order_type) ) % (df.name, df.order_type)
self.note += '\n' self.note += "\n"
self.note += _("The EBICS response could not be verified.") self.note += _("The EBICS response could not be verified.")
except UserError as e: except UserError as e:
self.note += '\n' self.note += "\n"
self.note += _( self.note += _(
"Warning during download of File Format %s (%s):" "Warning during download of File Format %s (%s):"
) % (df.name, df.order_type) ) % (df.name, df.order_type)
self.note += '\n' self.note += "\n"
self.note += e.name self.note += e.name
except Exception: except Exception:
self.note += '\n' err_cnt += 1
self.note += "\n"
self.note += _( self.note += _(
"Unknown Error during download of File Format %s (%s):" "Unknown Error during download of File Format %s (%s):"
) % (df.name, df.order_type) ) % (df.name, df.order_type)
tb = ''.join(format_exception(*exc_info())) tb = "".join(format_exception(*exc_info()))
self.note += '\n%s' % tb self.note += "\n%s" % tb
else: else:
# mark received data so that it is not included in further # mark received data so that it is not included in further
# downloads # downloads
trans_id = client.last_trans_id trans_id = client.last_trans_id
client.confirm_download(trans_id=trans_id, success=success) 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: if ebics_files:
self.note += '\n' self.note += "\n"
for f in ebics_files: for f in ebics_files:
self.note += _( self.note += (
"EBICS File '%s' is available for further processing." _("EBICS File '%s' is available for further processing.")
) % f.name % f.name
self.note += '\n' )
self.note += "\n"
module = __name__.split('addons.')[1].split('.')[0] ctx["err_cnt"] = err_cnt
result_view = self.env.ref( module = __name__.split("addons.")[1].split(".")[0]
'%s.ebics_xfer_view_form_result' % module) result_view = self.env.ref("%s.ebics_xfer_view_form_result" % module)
return { return {
'name': _('EBICS file transfer result'), "name": _("EBICS file transfer result"),
'res_id': self.id, "res_id": self.id,
'view_type': 'form', "view_type": "form",
'view_mode': 'form', "view_mode": "form",
'res_model': 'ebics.xfer', "res_model": "ebics.xfer",
'view_id': result_view.id, "view_id": result_view.id,
'target': 'new', "target": "new",
'context': ctx, "context": ctx,
'type': 'ir.actions.act_window', "type": "ir.actions.act_window",
} }
def button_close(self): def button_close(self):
self.ensure_one() self.ensure_one()
return {'type': 'ir.actions.act_window_close'} return {"type": "ir.actions.act_window_close"}
def view_ebics_file(self): def view_ebics_file(self):
self.ensure_one() self.ensure_one()
module = __name__.split('addons.')[1].split('.')[0] module = __name__.split("addons.")[1].split(".")[0]
act = self.env['ir.actions.act_window']._for_xml_id( act = self.env["ir.actions.act_window"]._for_xml_id(
'{}.ebics_file_action_download'.format(module)) "{}.ebics_file_action_download".format(module)
act['domain'] = [('id', 'in', self._context['ebics_file_ids'])] )
act["domain"] = [("id", "in", self._context["ebics_file_ids"])]
return act return act
def _ebics_upload(self): def _ebics_upload(self):
self.ensure_one() self.ensure_one()
ebics_file = self.env['ebics.file'] ebics_file = self.env["ebics.file"]
self.note = '' self.note = ""
client = self._setup_client() client = self._setup_client()
if client: if client:
upload_data = base64.decodestring(self.upload_data) upload_data = base64.decodestring(self.upload_data)
@ -280,70 +315,69 @@ class EbicsXfer(models.TransientModel):
OrderID = False OrderID = False
try: try:
order_type = self.order_type order_type = self.order_type
if order_type == 'FUL': if order_type == "FUL":
kwargs = {} kwargs = {}
bank = self.ebics_config_id.journal_ids[0].bank_id bank = self.ebics_config_id.journal_ids[0].bank_id
cc = bank.country.code cc = bank.country.code
if cc: if cc:
kwargs['country'] = cc kwargs["country"] = cc
if self.test_mode: if self.test_mode:
kwargs['TEST'] = 'TRUE' kwargs["TEST"] = "TRUE"
OrderID = client.FUL(ef_format.name, upload_data, **kwargs) OrderID = client.FUL(ef_format.name, upload_data, **kwargs)
else: else:
OrderID = client.upload(order_type, upload_data) OrderID = client.upload(order_type, upload_data)
if OrderID: if OrderID:
self.note += '\n' self.note += "\n"
self.note += _( self.note += (
"EBICS File has been uploaded (OrderID %s)." _("EBICS File has been uploaded (OrderID %s).") % OrderID
) % OrderID )
ef_note = _("EBICS OrderID: %s") % OrderID ef_note = _("EBICS OrderID: %s") % OrderID
if self.env.context.get('origin'): if self.env.context.get("origin"):
ef_note += '\n' + _( ef_note += "\n" + _("Origin: %s") % self._context["origin"]
"Origin: %s") % self._context['origin']
suffix = self.format_id.suffix suffix = self.format_id.suffix
fn = self.upload_fname fn = self.upload_fname
if not fn.endswith(suffix): if not fn.endswith(suffix):
fn = '.'.join([fn, suffix]) fn = ".".join([fn, suffix])
ef_vals = { ef_vals = {
'name': self.upload_fname, "name": self.upload_fname,
'data': self.upload_data, "data": self.upload_data,
'date': fields.Datetime.now(), "date": fields.Datetime.now(),
'format_id': self.format_id.id, "format_id": self.format_id.id,
'state': 'done', "state": "done",
'user_id': self._uid, "user_id": self._uid,
'ebics_userid_id': self.ebics_userid_id.id, "ebics_userid_id": self.ebics_userid_id.id,
'note': ef_note, "note": ef_note,
"company_ids": [ "company_ids": [
self.env.context.get("force_company", self.env.company.id) self.env.context.get("force_company", self.env.company.id)
], ],
} }
self._update_ef_vals(ef_vals) 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: except EbicsFunctionalError:
e = exc_info() e = exc_info()
self.note += '\n' self.note += "\n"
self.note += _("EBICS Functional Error:") self.note += _("EBICS Functional Error:")
self.note += '\n' self.note += "\n"
self.note += '%s (code: %s)' % (e[1].message, e[1].code) self.note += "{} (code: {})".format(e[1].message, e[1].code)
except EbicsTechnicalError: except EbicsTechnicalError:
e = exc_info() e = exc_info()
self.note += '\n' self.note += "\n"
self.note += _("EBICS Technical Error:") self.note += _("EBICS Technical Error:")
self.note += '\n' self.note += "\n"
self.note += '%s (code: %s)' % (e[1].message, e[1].code) self.note += "{} (code: {})".format(e[1].message, e[1].code)
except EbicsVerificationError: except EbicsVerificationError:
self.note += '\n' self.note += "\n"
self.note += _("EBICS Verification Error:") self.note += _("EBICS Verification Error:")
self.note += '\n' self.note += "\n"
self.note += _("The EBICS response could not be verified.") self.note += _("The EBICS response could not be verified.")
except Exception: except Exception:
self.note += '\n' self.note += "\n"
self.note += _("Unknown Error") self.note += _("Unknown Error")
tb = ''.join(format_exception(*exc_info())) tb = "".join(format_exception(*exc_info()))
self.note += '\n%s' % tb 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() OrderID = self.ebics_config_id._get_order_number()
self.ebics_config_id.sudo()._update_order_number(OrderID) self.ebics_config_id.sudo()._update_order_number(OrderID)
@ -353,33 +387,35 @@ class EbicsXfer(models.TransientModel):
self.ebics_config_id._check_ebics_keys() self.ebics_config_id._check_ebics_keys()
passphrase = self._get_passphrase() passphrase = self._get_passphrase()
keyring = EbicsKeyRing( keyring = EbicsKeyRing(
keys=self.ebics_userid_id.ebics_keys_fn, keys=self.ebics_userid_id.ebics_keys_fn, passphrase=passphrase
passphrase=passphrase) )
bank = EbicsBank( bank = EbicsBank(
keyring=keyring, keyring=keyring,
hostid=self.ebics_config_id.ebics_host, hostid=self.ebics_config_id.ebics_host,
url=self.ebics_config_id.ebics_url) url=self.ebics_config_id.ebics_url,
if self.ebics_config_id.ebics_version == 'H003': )
if self.ebics_config_id.ebics_version == "H003":
bank._order_number = self.ebics_config_id._get_order_number() bank._order_number = self.ebics_config_id._get_order_number()
user = EbicsUser( user = EbicsUser(
keyring=keyring, keyring=keyring,
partnerid=self.ebics_config_id.ebics_partner, partnerid=self.ebics_config_id.ebics_partner,
userid=self.ebics_userid_id.name) userid=self.ebics_userid_id.name,
signature_class = self.format_id.signature_class \ )
or self.ebics_userid_id.signature_class signature_class = (
if signature_class == 'T': self.format_id.signature_class or self.ebics_userid_id.signature_class
)
if signature_class == "T":
user.manual_approval = True user.manual_approval = True
try: try:
client = EbicsClient( client = EbicsClient(bank, user, version=self.ebics_config_id.ebics_version)
bank, user, version=self.ebics_config_id.ebics_version)
except Exception: except Exception:
self.note += '\n' self.note += "\n"
self.note += _("Unknown Error") self.note += _("Unknown Error")
tb = ''.join(format_exception(*exc_info())) tb = "".join(format_exception(*exc_info()))
self.note += '\n%s' % tb self.note += "\n%s" % tb
client = False client = False
return client return client
@ -390,19 +426,18 @@ class EbicsXfer(models.TransientModel):
if passphrase: if passphrase:
return passphrase return passphrase
module = __name__.split('addons.')[1].split('.')[0] module = __name__.split("addons.")[1].split(".")[0]
passphrase_view = self.env.ref( passphrase_view = self.env.ref("%s.ebics_xfer_view_form_passphrase" % module)
'%s.ebics_xfer_view_form_passphrase' % module)
return { return {
'name': _('EBICS file transfer'), "name": _("EBICS file transfer"),
'res_id': self.id, "res_id": self.id,
'view_type': 'form', "view_type": "form",
'view_mode': 'form', "view_mode": "form",
'res_model': 'ebics.xfer', "res_model": "ebics.xfer",
'view_id': passphrase_view.id, "view_id": passphrase_view.id,
'target': 'new', "target": "new",
'context': self._context, "context": self._context,
'type': 'ir.actions.act_window', "type": "ir.actions.act_window",
} }
def _file_format_methods(self): def _file_format_methods(self):
@ -411,10 +446,10 @@ class EbicsXfer(models.TransientModel):
for extra file formats. for extra file formats.
""" """
res = { res = {
'camt.xxx.cfonb120.stm': self._handle_cfonb120, "camt.xxx.cfonb120.stm": self._handle_cfonb120,
'camt.xxx.cfonb120.stm.rfi': self._handle_cfonb120, "camt.xxx.cfonb120.stm.rfi": self._handle_cfonb120,
'camt.052.001.02.stm': self._handle_camt052, "camt.052.001.02.stm": self._handle_camt052,
'camt.053.001.02.stm': self._handle_camt053, "camt.053.001.02.stm": self._handle_camt053,
} }
return res return res
@ -422,24 +457,24 @@ class EbicsXfer(models.TransientModel):
""" """
Adapt this method to customize the EBICS File values. Adapt this method to customize the EBICS File values.
""" """
if self.format_id and self.format_id.type == 'up': if self.format_id and self.format_id.type == "up":
fn = ef_vals['name'] fn = ef_vals["name"]
dups = self._check_duplicate_ebics_file( dups = self._check_duplicate_ebics_file(fn, self.format_id)
fn, self.format_id)
if dups: if dups:
n = 1 n = 1
fn = '_'.join([fn, str(n)]) fn = "_".join([fn, str(n)])
while self._check_duplicate_ebics_file(fn, self.format_id): while self._check_duplicate_ebics_file(fn, self.format_id):
n += 1 n += 1
fn = '_'.join([fn, str(n)]) fn = "_".join([fn, str(n)])
ef_vals['name'] = fn ef_vals["name"] = fn
def _handle_download_data(self, data, file_format): def _handle_download_data(self, data, file_format):
ebics_files = self.env['ebics.file'] ebics_files = self.env["ebics.file"]
if isinstance(data, dict): if isinstance(data, dict):
for doc in data: for doc in data:
ebics_files += self._create_ebics_file( ebics_files += self._create_ebics_file(
data[doc], file_format, docname=doc) data[doc], file_format, docname=doc
)
else: else:
ebics_files += self._create_ebics_file(data, file_format) ebics_files += self._create_ebics_file(data, file_format)
return ebics_files return ebics_files
@ -457,59 +492,61 @@ class EbicsXfer(models.TransientModel):
file format specific processing. file format specific processing.
""" """
ebics_files_root = self.ebics_config_id.ebics_files 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): if not os.path.isdir(tmp_dir):
os.makedirs(tmp_dir, mode=0o700) os.makedirs(tmp_dir, mode=0o700)
fn_parts = [self.ebics_config_id.ebics_host, fn_parts = [self.ebics_config_id.ebics_host, self.ebics_config_id.ebics_partner]
self.ebics_config_id.ebics_partner]
if docname: if docname:
fn_parts.append(docname) fn_parts.append(docname)
else: else:
fn_date = self.date_to or fields.Date.today() fn_date = self.date_to or fields.Date.today()
fn_parts.append(fn_date.isoformat()) fn_parts.append(fn_date.isoformat())
base_fn = '_'.join(fn_parts) base_fn = "_".join(fn_parts)
n = 1 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): while os.path.exists(full_tmp_fn):
n += 1 n += 1
tmp_fn = base_fn + '_' + str(n).rjust(3, '0') tmp_fn = base_fn + "_" + str(n).rjust(3, "0")
full_tmp_fn = os.path.normpath(tmp_dir + '/' + tmp_fn) 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) f.write(data)
ff_methods = self._file_format_methods() ff_methods = self._file_format_methods()
if file_format.name in ff_methods: if file_format.name in ff_methods:
data = ff_methods[file_format.name](data) 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) dups = self._check_duplicate_ebics_file(fn, file_format)
if dups: if dups:
raise UserError(_( raise UserError(
_(
"EBICS File with name '%s' has already been downloaded." "EBICS File with name '%s' has already been downloaded."
"\nPlease check this file and rename in case there is " "\nPlease check this file and rename in case there is "
"no risk on duplicate transactions.") "no risk on duplicate transactions."
% fn) )
% fn
)
data = base64.encodebytes(data) data = base64.encodebytes(data)
ef_vals = { ef_vals = {
'name': fn, "name": fn,
'data': data, "data": data,
'date': fields.Datetime.now(), "date": fields.Datetime.now(),
'date_from': self.date_from, "date_from": self.date_from,
'date_to': self.date_to, "date_to": self.date_to,
'format_id': file_format.id, "format_id": file_format.id,
'user_id': self._uid, "user_id": self._uid,
'ebics_userid_id': self.ebics_userid_id.id, "ebics_userid_id": self.ebics_userid_id.id,
'company_ids': self.ebics_config_id.company_ids.ids, "company_ids": self.ebics_config_id.company_ids.ids,
} }
self._update_ef_vals(ef_vals) 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 return ebics_file
def _check_duplicate_ebics_file(self, fn, file_format): def _check_duplicate_ebics_file(self, fn, file_format):
dups = self.env['ebics.file'].search( dups = self.env["ebics.file"].search(
[('name', '=', fn), [("name", "=", fn), ("format_id", "=", file_format.id)]
('format_id', '=', file_format.id)]) )
return dups return dups
def _detect_upload_format(self): def _detect_upload_format(self):
@ -517,31 +554,30 @@ class EbicsXfer(models.TransientModel):
Use this method in order to automatically detect and set the Use this method in order to automatically detect and set the
EBICS upload file format. EBICS upload file format.
""" """
pass
def _update_order_number(self, OrderID): def _update_order_number(self, OrderID):
o_list = list(OrderID) o_list = list(OrderID)
for i, c in enumerate(reversed(o_list), start=1): for i, c in enumerate(reversed(o_list), start=1):
if c == '9': if c == "9":
o_list[-i] = 'A' o_list[-i] = "A"
break break
if c == 'Z': if c == "Z":
continue continue
else: else:
o_list[-i] = chr(ord(c) + 1) o_list[-i] = chr(ord(c) + 1)
break break
next_nr = ''.join(o_list) next_nr = "".join(o_list)
if next_nr == 'ZZZZ': if next_nr == "ZZZZ":
next_nr = 'A000' next_nr = "A000"
self.ebics_config_id.order_number = next_nr self.ebics_config_id.order_number = next_nr
def _insert_line_terminator(self, data_in, line_len): def _insert_line_terminator(self, data_in, line_len):
data_in = data_in.replace(b'\n', b'').replace(b'\r', b'') data_in = data_in.replace(b"\n", b"").replace(b"\r", b"")
data_out = b'' data_out = b""
max_len = len(data_in) max_len = len(data_in)
i = 0 i = 0
while i + line_len <= max_len: 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 i += line_len
return data_out return data_out

View File

@ -9,20 +9,33 @@
<form string="EBICS File Download"> <form string="EBICS File Download">
<group> <group>
<separator string="Select your bank :" colspan="2" /> <separator string="Select your bank :" colspan="2" />
<field name="ebics_config_id" required="1" options="{'no_create': True, 'no_open': True}"/> <field
<field name="ebics_userid_id" name="ebics_config_id"
required="1"
options="{'no_create': True, 'no_open': True}"
/>
<field
name="ebics_userid_id"
domain="[('ebics_config_id', '=', ebics_config_id)]" domain="[('ebics_config_id', '=', ebics_config_id)]"
required="1" required="1"
options="{'no_create': True, 'no_open': True}"/> options="{'no_create': True, 'no_open': True}"
/>
<field name="date_from" /> <field name="date_from" />
<field name="date_to" /> <field name="date_to" />
<field name="format_id" <field
domain="[('type', '=', 'down'), ('id', 'in', allowed_format_ids)]"/> name="format_id"
domain="[('type', '=', 'down'), ('id', 'in', allowed_format_ids)]"
/>
<field name="order_type" /> <field name="order_type" />
<field name="allowed_format_ids" invisible="1" /> <field name="allowed_format_ids" invisible="1" />
</group> </group>
<footer> <footer>
<button name="ebics_download" string="Download Files" type="object" class="oe_highlight"/> <button
name="ebics_download"
string="Download Files"
type="object"
class="oe_highlight"
/>
or or
<button string="Cancel" class="oe_link" special="cancel" /> <button string="Cancel" class="oe_link" special="cancel" />
</footer> </footer>
@ -38,23 +51,40 @@
<form string="EBICS File Upload"> <form string="EBICS File Upload">
<group> <group>
<separator string="Select your bank :" colspan="2" /> <separator string="Select your bank :" colspan="2" />
<field name="ebics_config_id" required="1" options="{'no_create': True, 'no_open': True}"/> <field
<field name="ebics_userid_id" name="ebics_config_id"
required="1"
options="{'no_create': True, 'no_open': True}"
/>
<field
name="ebics_userid_id"
domain="[('ebics_config_id', '=', ebics_config_id)]" domain="[('ebics_config_id', '=', ebics_config_id)]"
required="1" required="1"
options="{'no_create': True, 'no_open': True}"/> options="{'no_create': True, 'no_open': True}"
/>
<separator string="Select your file :" colspan="2" /> <separator string="Select your file :" colspan="2" />
<field name="upload_data" filename="upload_fname" required="1" /> <field name="upload_data" filename="upload_fname" required="1" />
<field name="upload_fname" invisible="1" /> <field name="upload_fname" invisible="1" />
<field name="upload_fname_dummy" /> <field name="upload_fname_dummy" />
<field name="format_id" required="1" <field
domain="[('type', '=', 'up'), ('id', 'in', allowed_format_ids)]"/> name="format_id"
required="1"
domain="[('type', '=', 'up'), ('id', 'in', allowed_format_ids)]"
/>
<field name="order_type" /> <field name="order_type" />
<field name="test_mode" attrs="{'invisible': [('order_type', '!=', 'FUL')]}"/> <field
name="test_mode"
attrs="{'invisible': [('order_type', '!=', 'FUL')]}"
/>
<field name="allowed_format_ids" invisible="1" /> <field name="allowed_format_ids" invisible="1" />
</group> </group>
<footer> <footer>
<button name="ebics_upload" string="Upload File" type="object" class="oe_highlight"/> <button
name="ebics_upload"
string="Upload File"
type="object"
class="oe_highlight"
/>
or or
<button string="Cancel" class="oe_link" special="cancel" /> <button string="Cancel" class="oe_link" special="cancel" />
</footer> </footer>
@ -71,8 +101,13 @@
<separator colspan="4" string="Results :" /> <separator colspan="4" string="Results :" />
<field name="note" colspan="4" nolabel="1" width="850" height="400" /> <field name="note" colspan="4" nolabel="1" width="850" height="400" />
<footer> <footer>
<button name="view_ebics_file" type="object" string="View EBICS File(s)" class="oe_highlight" <button
invisible="not context.get('ebics_file_ids')"/> name="view_ebics_file"
type="object"
string="View EBICS File(s)"
class="oe_highlight"
invisible="not context.get('ebics_file_ids')"
/>
<button name="button_close" type="object" string="Close" /> <button name="button_close" type="object" string="Close" />
</footer> </footer>
</form> </form>

View File

@ -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])

View File

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

View File

@ -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,
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo noupdate="1">
<record id="ir_cron_ebics_batch_import" model="ir.cron">
<field name="name">EBICS Batch Import</field>
<field name="model_id" ref="model_ebics_batch_log" />
<field name="state">code</field>
<field name="code">model._batch_import()</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="active" eval="True" />
<field name="doall" eval="False" />
</record>
</odoo>

View File

@ -0,0 +1 @@
from . import ebics_batch_log

View File

@ -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)

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ebics_batch_log ebics.batch.log model_ebics_batch_log account.group_account_invoice 1 1 1 1
3 access_ebics_batch_log_item ebics.batch.log.item model_ebics_batch_log_item account.group_account_invoice 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,118 @@
<?xml version="1.0" ?>
<odoo>
<record id="ebics_batch_log_view_search" model="ir.ui.view">
<field name="name">ebics.batch.log.search</field>
<field name="model">ebics.batch.log</field>
<field name="arch" type="xml">
<search string="Search EBICS Batch Import Log Files">
<group col="10" colspan="4">
<field name="create_date" />
<field name="state" />
</group>
<newline />
<group expand="0" string="Group By">
<filter name="group_by_state" string="State" context="{'group_by':'state'}" />
</group>
</search>
</field>
</record>
<record id="ebics_batch_log_view_tree" model="ir.ui.view">
<field name="name">ebics.batch.log.tree</field>
<field name="model">ebics.batch.log</field>
<field name="arch" type="xml">
<tree string="EBICS Batch Import Logs" create="false">
<field name="create_date" />
<field name="file_count" />
<field name="state" />
</tree>
</field>
</record>
<record id="ebics_batch_log_view_form" model="ir.ui.view">
<field name="name">ebics.batch.log.form</field>
<field name="model">ebics.batch.log</field>
<field name="arch" type="xml">
<form string="EBICS Batch Import Log" create="false">
<header>
<button
name="button_draft"
states="done,error"
string="Set to Draft"
type="object"
/>
<button
name="reprocess"
string="Reprocess"
help="Reprocess 'draft' EBICS Files"
type="object"
class="oe_highlight"
attrs="{'invisible': ['|', ('state', '=', 'done'),('has_draft_files', '=', False)]}"
/>
<button
name="button_done"
states="draft,error"
string="Mark Done"
type="object"
/>
<field
name="state"
widget="statusbar"
statusbar_visible="draft,done"
statusbar_colors="{'error':'red'}"
/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button
name="view_ebics_files"
type="object"
class="oe_stat_button"
icon="fa-pencil-square-o"
attrs="{'invisible': [('file_count', '=', 0)]}"
>
<field name="file_count" widget="statinfo" string="EBICS Files" />
</button>
</div>
<group colspan="4" col="4">
<field name="create_date" />
<field name="ebics_config_ids" widget="many2many_tags" />
<field name="has_draft_files" invisible="1" />
</group>
<notebook colspan="4">
<page string="Batch Import Logs">
<field name="log_ids" nolabel="1">
<tree string="Log entries">
<field name="create_date" />
<field name="state" />
<field name="error_count" />
</tree>
<form string="Batch Import Log">
<group colspan="4" col="6">
<field name="create_date" />
<field name="error_count" />
</group>
<group attrs="{'invisible':[('note', '=', False)]}">
<separator colspan="4" />
<field name="note" nolabel="1" colspan="4" height="360" />
</group>
</form>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="ebics_batch_log_action" model="ir.actions.act_window">
<field name="name">EBICS Batch Import Logs</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">ebics.batch.log</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="ebics_batch_log_view_tree" />
<field name="search_view_id" ref="ebics_batch_log_view_search" />
</record>
</odoo>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" ?>
<odoo>
<menuitem
id="ebics_batch_log_menu"
name="EBICS Batch Import Logs"
parent="account_ebics.ebics_processing_menu"
action="ebics_batch_log_action"
sequence="100"
/>
</odoo>

View File

@ -2,16 +2,16 @@
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl). # License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
{ {
'name': 'account_ebics with OCA Bank Statement Imoort', "name": "account_ebics with OCA Bank Statement Imoort",
'summary': "Use OCA Bank Statement Import with account_ebics", "summary": "Use OCA Bank Statement Import with account_ebics",
'version': '14.0.1.0.0', "version": "14.0.1.0.0",
'author': 'Noviat', "author": "Noviat",
'category': 'Hidden', "category": "Hidden",
'license': 'LGPL-3', "license": "LGPL-3",
'depends': [ "depends": [
'account_ebics', "account_ebics",
'account_statement_import', "account_statement_import",
], ],
'installable': True, "installable": True,
'auto_install': True, "auto_install": True,
} }

View File

@ -3,32 +3,34 @@
import logging import logging
from odoo import models, _ from odoo import _, models
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class AccountStatementImport(models.TransientModel): class AccountStatementImport(models.TransientModel):
_inherit = 'account.statement.import' _inherit = "account.statement.import"
def _check_parsed_data(self, stmts_vals): def _check_parsed_data(self, stmts_vals):
""" Basic and structural verifications """ """ Basic and structural verifications """
if self.env.context.get('active_model') == 'ebics.file': if self.env.context.get("active_model") == "ebics.file":
message = False message = False
if len(stmts_vals) == 0: if len(stmts_vals) == 0:
message = _("This file doesn't contain any statement.") message = _("This file doesn't contain any statement.")
if not message: if not message:
no_st_line = True no_st_line = True
for vals in stmts_vals: for vals in stmts_vals:
if vals['transactions'] and len(vals['transactions']) > 0: if vals["transactions"] and len(vals["transactions"]) > 0:
no_st_line = False no_st_line = False
break break
if no_st_line: if no_st_line:
message = _('This file doesn\'t contain any transaction.') message = _("This file doesn't contain any transaction.")
if message: if message:
log_msg = _( log_msg = (
"Error detected while processing and EBICS File" _("Error detected while processing and EBICS File")
) + ':\n' + message + ":\n"
+ message
)
_logger.warn(log_msg) _logger.warn(log_msg)
return return
return super()._check_parsed_data(stmts_vals) return super()._check_parsed_data(stmts_vals)
@ -43,20 +45,16 @@ class AccountStatementImport(models.TransientModel):
We could also create empty bank statement (in state done) to clearly We could also create empty bank statement (in state done) to clearly
show days without transactions via the bank statement list view. show days without transactions via the bank statement list view.
""" """
if self.env.context.get('active_model') == 'ebics.file': if self.env.context.get("active_model") == "ebics.file":
transactions = False transactions = False
for st_vals in stmts_vals: for st_vals in stmts_vals:
if st_vals.get('transactions'): if st_vals.get("transactions"):
transactions = True transactions = True
break break
if not transactions: if not transactions:
message = _('This file doesn\'t contain any transaction.') message = _("This file doesn't contain any transaction.")
st_line_ids = [] st_line_ids = []
notifications = { notifications = {"type": "warning", "message": message, "details": ""}
'type': 'warning',
'message': message,
'details': ''
}
return st_line_ids, [notifications] return st_line_ids, [notifications]
return super()._create_bank_statements(stmts_vals, result) return super()._create_bank_statements(stmts_vals, result)

View File

@ -2,19 +2,17 @@
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl). # License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
{ {
'name': 'account_ebics on Odoo Enterprise', "name": "account_ebics on Odoo Enterprise",
'summary': "Deploy account_ebics module on Odoo Enterprise", "summary": "Deploy account_ebics module on Odoo Enterprise",
'version': '14.0.1.0.0', "version": "14.0.1.0.0",
'author': 'Noviat', "author": "Noviat",
'category': 'Hidden', "category": "Hidden",
'license': 'LGPL-3', "license": "LGPL-3",
'depends': [ "depends": [
'account_ebics', "account_ebics",
'account_accountant', "account_accountant",
], ],
'data': [ "data": ["views/account_ebics_menu.xml"],
'views/account_ebics_menu.xml' "installable": True,
], "auto_install": True,
'installable': True,
'auto_install': True,
} }

View File

@ -2,16 +2,16 @@
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl). # License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
{ {
'name': 'account_ebics with Odoo Enterprise Bank Statement Import', "name": "account_ebics with Odoo Enterprise Bank Statement Import",
'summary': "Use Odoo Enterprise Bank Statement Import with account_ebics", "summary": "Use Odoo Enterprise Bank Statement Import with account_ebics",
'version': '14.0.1.0.0', "version": "14.0.1.0.0",
'author': 'Noviat', "author": "Noviat",
'category': 'Hidden', "category": "Hidden",
'license': 'LGPL-3', "license": "LGPL-3",
'depends': [ "depends": [
'account_ebics_oe', "account_ebics_oe",
'account_bank_statement_import', "account_bank_statement_import",
], ],
'installable': True, "installable": True,
'auto_install': True, "auto_install": True,
} }

View File

@ -3,32 +3,34 @@
import logging import logging
from odoo import models, _ from odoo import _, models
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class AccountBankStatementImport(models.TransientModel): class AccountBankStatementImport(models.TransientModel):
_inherit = 'account.bank.statement.import' _inherit = "account.bank.statement.import"
def _check_parsed_data(self, stmts_vals, account_number): def _check_parsed_data(self, stmts_vals, account_number):
""" Basic and structural verifications """ """ Basic and structural verifications """
if self.env.context.get('active_model') == 'ebics.file': if self.env.context.get("active_model") == "ebics.file":
message = False message = False
if len(stmts_vals) == 0: if len(stmts_vals) == 0:
message = _("This file doesn't contain any statement.") message = _("This file doesn't contain any statement.")
if not message: if not message:
no_st_line = True no_st_line = True
for vals in stmts_vals: for vals in stmts_vals:
if vals['transactions'] and len(vals['transactions']) > 0: if vals["transactions"] and len(vals["transactions"]) > 0:
no_st_line = False no_st_line = False
break break
if no_st_line: if no_st_line:
message = _('This file doesn\'t contain any transaction.') message = _("This file doesn't contain any transaction.")
if message: if message:
log_msg = _( log_msg = (
"Error detected while processing and EBICS File" _("Error detected while processing and EBICS File")
) + ':\n' + message + ":\n"
+ message
)
_logger.warn(log_msg) _logger.warn(log_msg)
return return
super()._check_parsed_data(stmts_vals, account_number) super()._check_parsed_data(stmts_vals, account_number)
@ -43,20 +45,16 @@ class AccountBankStatementImport(models.TransientModel):
We could also create empty bank statement (in state done) to clearly We could also create empty bank statement (in state done) to clearly
show days without transactions via the bank statement list view. show days without transactions via the bank statement list view.
""" """
if self.env.context.get('active_model') == 'ebics.file': if self.env.context.get("active_model") == "ebics.file":
transactions = False transactions = False
for st_vals in stmts_vals: for st_vals in stmts_vals:
if st_vals.get('transactions'): if st_vals.get("transactions"):
transactions = True transactions = True
break break
if not transactions: if not transactions:
message = _('This file doesn\'t contain any transaction.') message = _("This file doesn't contain any transaction.")
st_line_ids = [] st_line_ids = []
notifications = { notifications = {"type": "warning", "message": message, "details": ""}
'type': 'warning',
'message': message,
'details': ''
}
return st_line_ids, [notifications] return st_line_ids, [notifications]
return super()._create_bank_statements(stmts_vals) return super()._create_bank_statements(stmts_vals)

View File

@ -2,16 +2,14 @@
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl). # License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
{ {
'name': 'Upload Payment Order via EBICS', "name": "Upload Payment Order via EBICS",
'version': '14.0.1.0.0', "version": "14.0.1.0.0",
'license': 'LGPL-3', "license": "LGPL-3",
'author': 'Noviat', "author": "Noviat",
'category': 'Accounting & Finance', "category": "Accounting & Finance",
'depends': [ "depends": ["account_ebics", "account_payment_order"],
'account_ebics', "data": [
'account_payment_order'], "views/account_payment_order.xml",
'data': [
'views/account_payment_order.xml',
], ],
'installable': True, "installable": True,
} }

View File

@ -6,57 +6,69 @@ from odoo.exceptions import UserError
class AccountPaymentOrder(models.Model): class AccountPaymentOrder(models.Model):
_inherit = 'account.payment.order' _inherit = "account.payment.order"
def ebics_upload(self): def ebics_upload(self):
self.ensure_one() self.ensure_one()
ctx = self._context.copy() ctx = self._context.copy()
attach = self.env['ir.attachment'].search( attach = self.env["ir.attachment"].search(
[('res_model', '=', self._name), [("res_model", "=", self._name), ("res_id", "=", self.id)]
('res_id', '=', self.id)]) )
if not attach: if not attach:
raise UserError(_( raise UserError(
_(
"This payment order doesn't contains attachements." "This payment order doesn't contains attachements."
"\nPlease generate first the Payment Order file first.")) "\nPlease generate first the Payment Order file first."
)
)
elif len(attach) > 1: elif len(attach) > 1:
raise UserError(_( raise UserError(
_(
"This payment order contains multiple attachments." "This payment order contains multiple attachments."
"\nPlease remove the obsolete attachments or upload " "\nPlease remove the obsolete attachments or upload "
"the payment order file via the " "the payment order file via the "
"EBICS Processing > EBICS Upload menu")) "EBICS Processing > EBICS Upload menu"
)
)
else: else:
origin = _("Payment Order") + ': ' + self.name origin = _("Payment Order") + ": " + self.name
ebics_config = self.env['ebics.config'].search([ ebics_config = self.env["ebics.config"].search(
('journal_ids', '=', self.journal_id.id), [
('state', '=', 'confirm'), ("journal_ids", "=", self.journal_id.id),
]) ("state", "=", "confirm"),
]
)
if not ebics_config: if not ebics_config:
raise UserError(_( raise UserError(
_(
"No active EBICS configuration available " "No active EBICS configuration available "
"for the selected bank." "for the selected bank."
)) )
)
if len(ebics_config) == 1: if len(ebics_config) == 1:
ctx["default_ebics_config_id"] = ebics_config.id ctx["default_ebics_config_id"] = ebics_config.id
ctx.update({ ctx.update(
'default_upload_data': attach.datas, {
'default_upload_fname': attach.name, "default_upload_data": attach.datas,
'origin': origin, "default_upload_fname": attach.name,
'force_comany': self.company_id.id, "origin": origin,
}) "force_comany": self.company_id.id,
ebics_xfer = self.env['ebics.xfer'].with_context(ctx).create({}) }
)
ebics_xfer = self.env["ebics.xfer"].with_context(ctx).create({})
ebics_xfer._onchange_ebics_config_id() ebics_xfer._onchange_ebics_config_id()
ebics_xfer._onchange_upload_data() ebics_xfer._onchange_upload_data()
ebics_xfer._onchange_format_id() ebics_xfer._onchange_format_id()
view = self.env.ref('account_ebics.ebics_xfer_view_form_upload') view = self.env.ref("account_ebics.ebics_xfer_view_form_upload")
act = { act = {
'name': _('EBICS Upload'), "name": _("EBICS Upload"),
'view_type': 'form', "view_type": "form",
'view_mode': 'form', "view_mode": "form",
'res_model': 'ebics.xfer', "res_model": "ebics.xfer",
'view_id': view.id, "view_id": view.id,
'res_id': ebics_xfer.id, "res_id": ebics_xfer.id,
'type': 'ir.actions.act_window', "type": "ir.actions.act_window",
'target': 'new', "target": "new",
'context': ctx, "context": ctx,
} }
return act return act

View File

@ -8,8 +8,13 @@
<field name="inherit_id" ref="account_payment_order.account_payment_order_form" /> <field name="inherit_id" ref="account_payment_order.account_payment_order_form" />
<field name="arch" type="xml"> <field name="arch" type="xml">
<button name="open2generated" position="after"> <button name="open2generated" position="after">
<button name="ebics_upload" type="object" states="generated" <button
string="EBICS Upload" class="oe_highlight"/> name="ebics_upload"
type="object"
states="generated"
string="EBICS Upload"
class="oe_highlight"
/>
</button> </button>
</field> </field>
</record> </record>

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
# generated from manifests external_dependencies
cryptography
fintech

View File

@ -0,0 +1,2 @@
# addons listed in this file are ignored by
# setuptools-odoo-make-default (one addon per line)

2
setup/README Normal file
View File

@ -0,0 +1,2 @@
To learn more about this directory, please visit
https://pypi.python.org/pypi/setuptools-odoo

View File

@ -0,0 +1 @@
../../../../account_ebics

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

View File

@ -0,0 +1 @@
../../../../account_ebics_batch

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

View File

@ -0,0 +1 @@
../../../../account_ebics_oca_statement_import

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

View File

@ -0,0 +1 @@
../../../../account_ebics_oe

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

View File

@ -0,0 +1 @@
../../../../account_ebics_oe_statement_import

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)

View File

@ -0,0 +1 @@
../../../../account_ebics_payment_order

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)