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.
SWIFT 3SKey support requires Fintech 6.4 or higher.
|
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).
{
'name': 'EBICS banking protocol',
'version': '14.0.1.0.2',
'license': 'LGPL-3',
'author': 'Noviat',
'website': 'www.noviat.com',
'category': 'Accounting & Finance',
'depends': ['account'],
'data': [
'security/ebics_security.xml',
'security/ir.model.access.csv',
'data/ebics_file_format.xml',
'views/ebics_config_views.xml',
'views/ebics_file_views.xml',
'views/ebics_userid_views.xml',
'views/ebics_file_format_views.xml',
'wizards/ebics_change_passphrase.xml',
'wizards/ebics_xfer.xml',
'views/menu.xml',
"name": "EBICS banking protocol",
"version": "14.0.1.0.3",
"license": "LGPL-3",
"author": "Noviat",
"website": "www.noviat.com",
"category": "Accounting & Finance",
"depends": ["account"],
"data": [
"security/ebics_security.xml",
"security/ir.model.access.csv",
"data/ebics_file_format.xml",
"views/ebics_config_views.xml",
"views/ebics_file_views.xml",
"views/ebics_userid_views.xml",
"views/ebics_file_format_views.xml",
"wizards/ebics_change_passphrase.xml",
"wizards/ebics_xfer.xml",
"views/menu.xml",
],
'installable': True,
'application': True,
'external_dependencies': {
'python': [
'fintech',
'cryptography',
"installable": True,
"application": True,
"external_dependencies": {
"python": [
"fintech",
"cryptography",
]
},
}

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data noupdate="1">
@ -9,7 +9,9 @@
<field name="type">down</field>
<field name="order_type">C52</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>
</record>
@ -18,7 +20,9 @@
<field name="type">down</field>
<field name="order_type">Z52</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>
</record>
@ -27,7 +31,9 @@
<field name="type">down</field>
<field name="order_type">C53</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>
</record>
@ -36,7 +42,9 @@
<field name="type">down</field>
<field name="order_type">Z53</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>
</record>
@ -45,7 +53,9 @@
<field name="type">down</field>
<field name="order_type">C54</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>
</record>
@ -54,7 +64,9 @@
<field name="type">down</field>
<field name="order_type">Z54</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>
</record>
@ -63,7 +75,9 @@
<field name="type">down</field>
<field name="order_type">FDL</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>
</record>
@ -71,7 +85,9 @@
<field name="name">pain.002</field>
<field name="type">down</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>
</record>
@ -80,7 +96,9 @@
<field name="type">down</field>
<field name="order_type">Z01</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>
</record>
@ -114,7 +132,9 @@
<field name="name">pain.008.001.02.sdd</field>
<field name="type">up</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>
</record>
@ -122,7 +142,9 @@
<field name="name">pain.008.001.02.sdd</field>
<field name="type">up</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>
</record>
@ -130,7 +152,9 @@
<field name="name">pain.008.001.02.sbb</field>
<field name="type">up</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>
</record>
@ -138,7 +162,9 @@
<field name="name">pain.008.001.02.sbb</field>
<field name="type">up</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>
</record>

View File

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

View File

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

View File

@ -2,35 +2,44 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
_FILE_FORMATS = [
{'old_xml_id_name': 'ebics_ff_camt_052_001_02_stm',
'new_xml_id_name': 'ebics_ff_C52',
'new_name': 'camt.052',
{
"old_xml_id_name": "ebics_ff_camt_052_001_02_stm",
"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',
'new_name': 'camt.053',
{
"old_xml_id_name": "ebics_ff_camt_053_001_02_stm",
"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',
'new_name': 'pain.001.001.03',
{
"old_xml_id_name": "ebics_ff_pain_001",
"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):
cr.execute(
cr.execute( # pylint: disable=E8103
"""
SELECT id, res_id FROM ir_model_data
WHERE module='account_ebics' AND name='{}'
""".format(ff['old_xml_id_name'])
""".format(
ff["old_xml_id_name"]
)
)
res = cr.fetchone()
if res:
@ -59,7 +70,7 @@ def _update_file_format(cr, ff):
""".format(
new_xml_id_name=ff["new_xml_id_name"], xml_id=res[0]
)
if ff.get('new_name'):
if ff.get("new_name"):
query += """
UPDATE ebics_file_format
SET name='{new_name}'
@ -67,4 +78,4 @@ def _update_file_format(cr, ff):
""".format(
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):
_inherit = 'account.bank.statement'
_inherit = "account.bank.statement"
ebics_file_id = fields.Many2one(
comodel_name='ebics.file', string='EBICS Data File')
ebics_file_id = fields.Many2one(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).
import logging
import re
import os
import re
from odoo import _, api, fields, models
from odoo.exceptions import UserError
@ -16,40 +16,55 @@ class EbicsConfig(models.Model):
EBICS configuration is stored in a separate object in order to
allow extra security policies on this object.
"""
_name = 'ebics.config'
_description = 'EBICS Configuration'
_order = 'name'
_name = "ebics.config"
_description = "EBICS Configuration"
_order = "name"
name = fields.Char(
string='Name',
readonly=True, states={'draft': [('readonly', False)]},
required=True)
string="Name",
readonly=True,
states={"draft": [("readonly", False)]},
required=True,
)
journal_ids = fields.Many2many(
comodel_name='account.journal',
readonly=True, states={'draft': [('readonly', False)]},
string='Bank Accounts',
comodel_name="account.journal",
readonly=True,
states={"draft": [("readonly", False)]},
string="Bank Accounts",
domain="[('type', '=', 'bank')]",
required=True)
required=True,
)
ebics_host = fields.Char(
string='EBICS HostID', required=True,
readonly=True, states={'draft': [('readonly', False)]},
string="EBICS HostID",
required=True,
readonly=True,
states={"draft": [("readonly", False)]},
help="Contact your bank to get the EBICS HostID."
"\nIn France the BIC is usually allocated to the HostID "
"whereas in Germany it tends to be an institute specific string "
"of 8 characters.")
"of 8 characters.",
)
ebics_url = fields.Char(
string='EBICS URL', required=True,
readonly=True, states={'draft': [('readonly', False)]},
help="Contact your bank to get the EBICS URL.")
string="EBICS URL",
required=True,
readonly=True,
states={"draft": [("readonly", False)]},
help="Contact your bank to get the EBICS URL.",
)
ebics_version = fields.Selection(
selection=[('H003', 'H003 (2.4)'),
('H004', 'H004 (2.5)')],
string='EBICS protocol version',
readonly=True, states={'draft': [('readonly', False)]},
required=True, default='H004')
selection=[("H003", "H003 (2.4)"), ("H004", "H004 (2.5)")],
string="EBICS protocol version",
readonly=True,
states={"draft": [("readonly", False)]},
required=True,
default="H004",
)
ebics_partner = fields.Char(
string='EBICS PartnerID', required=True,
readonly=True, states={'draft': [('readonly', False)]},
string="EBICS PartnerID",
required=True,
readonly=True,
states={"draft": [("readonly", False)]},
help="Organizational unit (company or individual) "
"that concludes a contract with the bank. "
"\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) "
"communicate with the EBICS bank server and the authorisations "
"that these users will possess. "
"\nIt is identified by the PartnerID.")
"\nIt is identified by the PartnerID.",
)
ebics_userid_ids = fields.One2many(
comodel_name='ebics.userid',
inverse_name='ebics_config_id',
readonly=True, states={'draft': [('readonly', False)]},
comodel_name="ebics.userid",
inverse_name="ebics_config_id",
readonly=True,
states={"draft": [("readonly", False)]},
help="Human users or a technical system that is/are "
"assigned to a customer. "
"\nOn the EBICS bank server it is identified "
"by the combination of UserID and PartnerID. "
"The technical subscriber serves only for the data exchange "
"between customer and financial institution. "
"The human user also can authorise orders.")
"The human user also can authorise orders.",
)
ebics_files = fields.Char(
string='EBICS Files Root', required=True,
readonly=True, states={'draft': [('readonly', False)]},
string="EBICS Files Root",
required=True,
readonly=True,
states={"draft": [("readonly", False)]},
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.
# This directory requires special protection to reduce fraude.
ebics_keys = fields.Char(
string='EBICS Keys Root', required=True,
readonly=True, states={'draft': [('readonly', False)]},
string="EBICS Keys Root",
required=True,
readonly=True,
states={"draft": [("readonly", False)]},
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(
selection=[('A005', 'A005 (RSASSA-PKCS1-v1_5)'),
('A006', 'A006 (RSASSA-PSS)')],
string='EBICS key version',
default='A006',
readonly=True, states={'draft': [('readonly', False)]},
help="The key version of the electronic signature.")
selection=[("A005", "A005 (RSASSA-PKCS1-v1_5)"), ("A006", "A006 (RSASSA-PSS)")],
string="EBICS key version",
default="A006",
readonly=True,
states={"draft": [("readonly", False)]},
help="The key version of the electronic signature.",
)
ebics_key_bitlength = fields.Integer(
string='EBICS key bitlength',
string="EBICS key bitlength",
default=2048,
readonly=True, states={'draft': [('readonly', False)]},
readonly=True,
states={"draft": [("readonly", False)]},
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(
comodel_name='ebics.file.format',
column1='config_id', column2='format_id',
string='EBICS File Formats',
readonly=True, states={'draft': [('readonly', False)]},
comodel_name="ebics.file.format",
column1="config_id",
column2="format_id",
string="EBICS File Formats",
readonly=True,
states={"draft": [("readonly", False)]},
)
state = fields.Selection(
[('draft', 'Draft'),
('confirm', 'Confirmed')],
string='State',
default='draft',
required=True, readonly=True)
[("draft", "Draft"), ("confirm", "Confirmed")],
string="State",
default="draft",
required=True,
readonly=True,
)
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."
"\nThis number should match the following pattern : "
"[A-Z]{1}[A-Z0-9]{3}")
active = fields.Boolean(
string='Active', default=True)
"[A-Z]{1}[A-Z0-9]{3}",
)
active = fields.Boolean(string="Active", default=True)
company_ids = fields.Many2many(
comodel_name='res.company',
string='Companies',
comodel_name="res.company",
string="Companies",
required=True,
help="Companies sharing this EBICS contract.")
help="Companies sharing this EBICS contract.",
)
@api.model
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
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):
for cfg in self:
nbr = cfg.order_number
@ -140,26 +173,28 @@ class EbicsConfig(models.Model):
if not pattern.match(nbr):
ok = False
if not ok:
raise UserError(_(
raise UserError(
_(
"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):
self.company_ids = self.journal_ids.mapped('company_id')
self.company_ids = self.journal_ids.mapped("company_id")
def unlink(self):
for ebics_config in self:
if ebics_config.state == 'active':
raise UserError(_(
"You cannot remove active EBICS configurations."))
if ebics_config.state == "active":
raise UserError(_("You cannot remove active EBICS configurations."))
return super(EbicsConfig, self).unlink()
def set_to_draft(self):
return self.write({'state': 'draft'})
return self.write({"state": "draft"})
def set_to_confirm(self):
return self.write({'state': 'confirm'})
return self.write({"state": "confirm"})
def _get_order_number(self):
return self.order_number
@ -167,31 +202,37 @@ class EbicsConfig(models.Model):
def _update_order_number(self, OrderID):
o_list = list(OrderID)
for i, c in enumerate(reversed(o_list), start=1):
if c == '9':
o_list[-i] = 'A'
if c == "9":
o_list[-i] = "A"
break
if c == 'Z':
if c == "Z":
continue
else:
o_list[-i] = chr(ord(c) + 1)
break
next = ''.join(o_list)
if next == 'ZZZZ':
next = 'A000'
self.order_number = next
next_order_number = "".join(o_list)
if next_order_number == "ZZZZ":
next_order_number = "A000"
self.order_number = next_order_number
def _check_ebics_keys(self):
dirname = self.ebics_keys or ''
dirname = self.ebics_keys or ""
if not os.path.exists(dirname):
raise UserError(_(
raise UserError(
_(
"EBICS Keys Root Directory %s is not available."
"\nPlease contact your system administrator.")
% dirname)
"\nPlease contact your system administrator."
)
% dirname
)
def _check_ebics_files(self):
dirname = self.ebics_files or ''
dirname = self.ebics_files or ""
if not os.path.exists(dirname):
raise UserError(_(
raise UserError(
_(
"EBICS Files Root Directory %s is not available."
"\nPlease contact your system administrator.")
% dirname)
"\nPlease contact your system administrator."
)
% 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).
import base64
@ -6,113 +6,119 @@ import logging
from odoo import _, fields, models
from odoo.exceptions import UserError
from odoo.tools.safe_eval import safe_eval
_logger = logging.getLogger(__name__)
class EbicsFile(models.Model):
_name = 'ebics.file'
_description = 'Object to store EBICS Data Files'
_order = 'date desc'
_name = "ebics.file"
_description = "Object to store EBICS Data Files"
_order = "date desc"
_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')
data = fields.Binary(string='File', readonly=True)
name = fields.Char(string="Filename")
data = fields.Binary(string="File", readonly=True)
format_id = fields.Many2one(
comodel_name='ebics.file.format',
string='EBICS File Formats',
readonly=True)
type = fields.Selection(
related='format_id.type',
readonly=True)
comodel_name="ebics.file.format", string="EBICS File Formats", readonly=True
)
type = fields.Selection(related="format_id.type", readonly=True)
date_from = fields.Date(
readonly=True,
help="'Date From' as entered in the download wizard.")
readonly=True, help="'Date From' as entered in the download wizard."
)
date_to = fields.Date(
readonly=True,
help="'Date To' as entered in the download wizard.")
readonly=True, help="'Date To' as entered in the download wizard."
)
date = fields.Datetime(
required=True, readonly=True,
help='File Upload/Download date')
required=True, readonly=True, help="File Upload/Download date"
)
bank_statement_ids = fields.One2many(
comodel_name='account.bank.statement',
inverse_name='ebics_file_id',
string='Generated Bank Statements', readonly=True)
comodel_name="account.bank.statement",
inverse_name="ebics_file_id",
string="Generated Bank Statements",
readonly=True,
)
state = fields.Selection(
[('draft', 'Draft'),
('done', 'Done')],
string='State',
default='draft',
required=True, readonly=True)
[("draft", "Draft"), ("done", "Done")],
string="State",
default="draft",
required=True,
readonly=True,
)
user_id = fields.Many2one(
comodel_name='res.users', string='User',
comodel_name="res.users",
string="User",
default=lambda self: self.env.user,
readonly=True)
readonly=True,
)
ebics_userid_id = fields.Many2one(
comodel_name='ebics.userid',
string='EBICS UserID',
ondelete='restrict',
readonly=True)
note = fields.Text(string='Notes')
note_process = fields.Text(string='Notes')
comodel_name="ebics.userid",
string="EBICS UserID",
ondelete="restrict",
readonly=True,
)
note = fields.Text(string="Notes")
note_process = fields.Text(string="Notes")
company_ids = fields.Many2many(
comodel_name='res.company',
string='Companies',
help="Companies sharing this EBICS file.")
comodel_name="res.company",
string="Companies",
help="Companies sharing this EBICS file.",
)
def unlink(self):
ff_methods = self._file_format_methods()
for ebics_file in self:
if ebics_file.state == 'done':
raise UserError(_(
"You can only remove EBICS files in state 'Draft'."))
if ebics_file.state == "done":
raise UserError(_("You can only remove EBICS files in state 'Draft'."))
# execute format specific actions
ff = ebics_file.format_id.download_process_method
if ff in ff_methods:
if ff_methods[ff].get('unlink'):
ff_methods[ff]['unlink'](ebics_file)
if ff_methods[ff].get("unlink"):
ff_methods[ff]["unlink"](ebics_file)
# remove bank statements
ebics_file.bank_statement_ids.unlink()
return super(EbicsFile, self).unlink()
def set_to_draft(self):
return self.write({'state': 'draft'})
return self.write({"state": "draft"})
def set_to_done(self):
return self.write({'state': 'done'})
return self.write({"state": "done"})
def process(self):
self.ensure_one()
ctx = dict(
self.env.context,
allowed_company_ids=self.env.user.company_ids.ids)
ctx = dict(self.env.context, allowed_company_ids=self.env.user.company_ids.ids)
self = self.with_context(ctx)
self.note_process = ''
self.note_process = ""
ff_methods = self._file_format_methods()
ff = self.format_id.download_process_method
if ff in ff_methods:
if ff_methods[ff].get('process'):
res = ff_methods[ff]['process'](self)
self.state = 'done'
if ff_methods[ff].get("process"):
res = ff_methods[ff]["process"](self)
self.state = "done"
return res
else:
return self._process_undefined_format()
def action_open_bank_statements(self):
self.ensure_one()
action = self.env['ir.actions.act_window']._for_xml_id(
'account.action_bank_statement_tree')
domain = eval(action.get('domain') or '[]')
domain += [('id', 'in', self._context.get('statement_ids'))]
action.update({'domain': domain})
action = self.env["ir.actions.act_window"]._for_xml_id(
"account.action_bank_statement_tree"
)
domain = safe_eval(action.get("domain") or "[]")
domain += [("id", "in", self._context.get("statement_ids"))]
action.update({"domain": domain})
return action
def button_close(self):
self.ensure_one()
return {'type': 'ir.actions.act_window_close'}
return {"type": "ir.actions.act_window_close"}
def _file_format_methods(self):
"""
@ -120,35 +126,45 @@ class EbicsFile(models.Model):
for extra file formats.
"""
res = {
'cfonb120':
{'process': self._process_cfonb120,
'unlink': self._unlink_cfonb120},
'camt.052':
{'process': self._process_camt052,
'unlink': self._unlink_camt052},
'camt.053':
{'process': self._process_camt053,
'unlink': self._unlink_camt053},
'camt.054':
{'process': self._process_camt054,
'unlink': self._unlink_camt054},
'pain.002':
{'process': self._process_pain002,
'unlink': self._unlink_pain002},
"cfonb120": {
"process": self._process_cfonb120,
"unlink": self._unlink_cfonb120,
},
"camt.052": {
"process": self._process_camt052,
"unlink": self._unlink_camt052,
},
"camt.053": {
"process": self._process_camt053,
"unlink": self._unlink_camt053,
},
"camt.054": {
"process": self._process_camt054,
"unlink": self._unlink_camt054,
},
"pain.002": {
"process": self._process_pain002,
"unlink": self._unlink_pain002,
},
}
return res
def _check_import_module(self, module, raise_if_not_found=True):
mod = self.env['ir.module.module'].sudo().search(
[('name', '=like', module),
('state', '=', 'installed')])
mod = (
self.env["ir.module.module"]
.sudo()
.search([("name", "=like", module), ("state", "=", "installed")])
)
if not mod:
if raise_if_not_found:
raise UserError(_(
raise UserError(
_(
"The module to process the '%s' format is not installed "
"on your system. "
"\nPlease install module '%s'")
% (self.format_id.name, module))
"\nPlease install module '%s'"
)
% (self.format_id.name, module)
)
return False
return True
@ -156,19 +172,19 @@ class EbicsFile(models.Model):
notifications = []
st_line_ids = []
statement_ids = []
if res.get('context'):
notifications = res['context'].get('notifications', [])
st_line_ids = res['context'].get('statement_line_ids', [])
if res.get("context"):
notifications = res["context"].get("notifications", [])
st_line_ids = res["context"].get("statement_line_ids", [])
if notifications:
for notif in notifications:
parts = []
for k in ['type', 'message', 'details']:
for k in ["type", "message", "details"]:
if notif.get(k):
msg = '%s: %s' % (k, notif[k])
msg = "{}: {}".format(k, notif[k])
parts.append(msg)
self.note_process += '\n'.join(parts)
self.note_process += '\n'
self.note_process += '\n'
self.note_process += "\n".join(parts)
self.note_process += "\n"
self.note_process += "\n"
if st_line_ids:
self.flush()
self.env.cr.execute(
@ -185,36 +201,37 @@ class EbicsFile(models.Model):
WHERE absl.id IN %s
ORDER BY date, company_id
""",
(tuple(st_line_ids),)
(tuple(st_line_ids),),
)
sts_data = self.env.cr.dictfetchall()
else:
sts_data = []
st_cnt = len(sts_data)
if st_cnt:
self.note_process += _(
"%s bank statements have been imported: "
) % st_cnt
self.note_process += '\n'
self.note_process += _("%s bank statements have been imported: ") % st_cnt
self.note_process += "\n"
for st_data in sts_data:
self.note_process += ("\n%s, %s (%s)") % (
st_data['date'], st_data['name'], st_data['company_name'])
statement_ids = [x['statement_id'] for x in sts_data]
st_data["date"],
st_data["name"],
st_data["company_name"],
)
statement_ids = [x["statement_id"] for x in sts_data]
if statement_ids:
self.sudo().bank_statement_ids = [(6, 0, statement_ids)]
ctx = dict(self.env.context, statement_ids=statement_ids)
module = __name__.split('addons.')[1].split('.')[0]
result_view = self.env.ref('%s.ebics_file_view_form_result' % module)
module = __name__.split("addons.")[1].split(".")[0]
result_view = self.env.ref("%s.ebics_file_view_form_result" % module)
return {
'name': _('Import EBICS File'),
'res_id': self.id,
'view_type': 'form',
'view_mode': 'form',
'res_model': self._name,
'view_id': result_view.id,
'target': 'new',
'context': ctx,
'type': 'ir.actions.act_window',
"name": _("Import EBICS File"),
"res_id": self.id,
"view_type": "form",
"view_mode": "form",
"res_model": self._name,
"view_id": result_view.id,
"target": "new",
"context": ctx,
"type": "ir.actions.act_window",
}
@staticmethod
@ -223,60 +240,63 @@ class EbicsFile(models.Model):
We do not support the standard _journal_creation_wizard since a single
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)
wiz_model = 'account.statement.import'
wiz_model = "account.statement.import"
data_file = base64.b64decode(self.data)
lines = data_file.split(b'\n')
lines = data_file.split(b"\n")
wiz_vals_list = []
st_lines = b''
st_lines = b""
transactions = False
for line in lines:
rec_type = line[0:2]
acc_number = line[21:32]
st_lines += line + b'\n'
if rec_type == b'04':
st_lines += line + b"\n"
if rec_type == b"04":
transactions = True
if rec_type == b'07':
if rec_type == b"07":
if transactions:
fn = '_'.join([acc_number.decode(), self.name])
wiz_vals_list.append({
'statement_filename': fn,
'statement_file': base64.b64encode(st_lines)
})
st_lines = b''
fn = "_".join([acc_number.decode(), self.name])
wiz_vals_list.append(
{
"statement_filename": fn,
"statement_file": base64.b64encode(st_lines),
}
)
st_lines = b""
transactions = False
result = {
'type': 'ir.actions.client',
'tag': 'bank_statement_reconciliation_view',
'context': {'statement_line_ids': [],
'company_ids': self.env.user.company_ids.ids,
'notifications': []},
"type": "ir.actions.client",
"tag": "bank_statement_reconciliation_view",
"context": {
"statement_line_ids": [],
"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):
wiz = self.env[wiz_model].with_context(wiz_ctx).create(wiz_vals)
res = wiz.import_file_button()
ctx = res.get('context')
if (res.get('res_model')
== 'account.bank.statement.import.journal.creation'):
message = _(
"Error detected while importing statement number %s.\n"
) % i
ctx = res.get("context")
if res.get("res_model") == "account.bank.statement.import.journal.creation":
message = _("Error detected while importing statement number %s.\n") % i
message += _("No financial journal found.")
details = _(
'Bank account number: %s'
) % ctx.get('default_bank_acc_number')
result['context']['notifications'].extend([{
'type': 'warning',
'message': message,
'details': details,
}])
details = _("Bank account number: %s") % ctx.get(
"default_bank_acc_number"
)
result["context"]["notifications"].extend(
[
{
"type": "warning",
"message": message,
"details": details,
}
]
)
continue
result['context']['statement_line_ids'].extend(
ctx['statement_line_ids'])
result['context']['notifications'].extend(
ctx['notifications'])
result["context"]["statement_line_ids"].extend(ctx["statement_line_ids"])
result["context"]["notifications"].extend(ctx["notifications"])
return self._process_result_action(result)
@staticmethod
@ -285,11 +305,10 @@ class EbicsFile(models.Model):
Placeholder for cfonb120 specific actions before removing the
EBICS data file and its related bank statements.
"""
pass
@staticmethod
def _process_camt052(self):
import_module = 'account_statement_import_camt'
import_module = "account_statement_import_camt"
self._check_import_module(import_module)
return self._process_camt053(self)
@ -299,11 +318,10 @@ class EbicsFile(models.Model):
Placeholder for camt052 specific actions before removing the
EBICS data file and its related bank statements.
"""
pass
@staticmethod
def _process_camt054(self):
import_module = 'account_statement_import_camt'
import_module = "account_statement_import_camt"
self._check_import_module(import_module)
return self._process_camt053(self)
@ -313,84 +331,89 @@ class EbicsFile(models.Model):
Placeholder for camt054 specific actions before removing the
EBICS data file and its related bank statements.
"""
pass
@staticmethod
def _process_camt053(self):
modules = [
('oca', 'account_statement_import_camt'),
('oe', 'account_bank_statement_import_camt'),
("oca", "account_statement_import_camt"),
("oe", "account_bank_statement_import_camt"),
]
found = False
for src, mod in modules:
for _src, mod in modules:
if self._check_import_module(mod, raise_if_not_found=False):
found = True
break
if not found:
raise UserError(_(
raise UserError(
_(
"The module to process the '%s' format is not installed "
"on your system. "
"\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()
else:
self._process_camt053_oe()
def _process_camt053_oca(self):
wiz_model = 'account.statement.import'
wiz_model = "account.statement.import"
wiz_vals = {
'statement_filename': self.name,
'statement_file': self.data,
"statement_filename": self.name,
"statement_file": self.data,
}
result = {
'type': 'ir.actions.client',
'tag': 'bank_statement_reconciliation_view',
'context': {'statement_line_ids': [],
'company_ids': self.env.user.company_ids.ids,
'notifications': []},
"type": "ir.actions.client",
"tag": "bank_statement_reconciliation_view",
"context": {
"statement_line_ids": [],
"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)
res = wiz.import_file_button()
ctx = res.get('context')
if (res.get('res_model')
== 'account.bank.statement.import.journal.creation'):
message = _(
"Error detected while importing statement %s.\n"
) % self.name
ctx = res.get("context")
if res.get("res_model") == "account.bank.statement.import.journal.creation":
message = _("Error detected while importing statement %s.\n") % self.name
message += _("No financial journal found.")
details = _(
'Bank account number: %s'
) % ctx.get('default_bank_acc_number')
result['context']['notifications'].extend([{
'type': 'warning',
'message': message,
'details': details,
}])
result['context']['statement_line_ids'].extend(
ctx['statement_line_ids'])
result['context']['notifications'].extend(
ctx['notifications'])
details = _("Bank account number: %s") % ctx.get("default_bank_acc_number")
result["context"]["notifications"].extend(
[
{
"type": "warning",
"message": message,
"details": details,
}
]
)
result["context"]["statement_line_ids"].extend(ctx["statement_line_ids"])
result["context"]["notifications"].extend(ctx["notifications"])
return self._process_result_action(result)
def _process_camt053_oe(self):
wiz_model = 'account.bank.statement.import'
wiz_model = "account.bank.statement.import"
wiz_vals = {
'attachment_ids': [(0, 0, {'name': self.name,
'datas': self.data,
'store_fname': self.name})]}
ctx = dict(self.env.context, active_model='ebics.file')
"attachment_ids": [
(
0,
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)
res = wiz.import_file()
if res.get('res_model') \
== 'account.bank.statement.import.journal.creation':
if res.get('context'):
bank_account = res['context'].get('default_bank_acc_number')
raise UserError(_(
"No financial journal found for Company Bank Account %s"
) % bank_account)
if res.get("res_model") == "account.bank.statement.import.journal.creation":
if res.get("context"):
bank_account = res["context"].get("default_bank_acc_number")
raise UserError(
_("No financial journal found for Company Bank Account %s")
% bank_account
)
return self._process_result_action(res)
@staticmethod
@ -399,7 +422,6 @@ class EbicsFile(models.Model):
Placeholder for camt053 specific actions before removing the
EBICS data file and its related bank statements.
"""
pass
@staticmethod
def _process_pain002(self):
@ -408,7 +430,6 @@ class EbicsFile(models.Model):
TODO:
add import logic based upon OCA 'account_payment_return_import'
"""
pass
@staticmethod
def _unlink_pain002(self):
@ -419,8 +440,11 @@ class EbicsFile(models.Model):
raise NotImplementedError
def _process_undefined_format(self):
raise UserError(_(
raise UserError(
_(
"The current version of the 'account_ebics' module "
"has no support to automatically process EBICS files "
"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):
_name = 'ebics.file.format'
_description = 'EBICS File Formats'
_order = 'type,name,order_type'
_name = "ebics.file.format"
_description = "EBICS File Formats"
_order = "type,name,order_type"
name = fields.Char(
string='Request Type',
string="Request Type",
required=True,
help="E.g. camt.xxx.cfonb120.stm, pain.001.001.03.sct.\n"
"Specify camt.052, camt.053, camt.054 for camt "
"Order Types such as C53, Z53, C54, Z54.\n"
"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(
selection=[('down', 'Download'),
('up', 'Upload')],
required=True)
selection=[("down", "Download"), ("up", "Upload")], required=True
)
order_type = fields.Char(
string='Order Type',
string="Order Type",
required=True,
help="E.g. C53 (check your EBICS contract).\n"
"For most banks in France you should use the "
"format neutral Order Types 'FUL' for upload "
"and 'FDL' for download.")
"and 'FDL' for download.",
)
download_process_method = fields.Selection(
selection='_selection_download_process_method',
selection="_selection_download_process_method",
help="Enable processing within Odoo of the downloaded file "
"via the 'Process' button."
"E.g. specify camt.053 to import a camt.053 file and create "
"a bank statement.")
"a bank statement.",
)
# TODO:
# move signature_class parameter so that it can be set per EBICS config
signature_class = fields.Selection(
selection=[('E', 'Single signature'),
('T', 'Transport signature')],
string='Signature Class',
selection=[("E", "Single signature"), ("T", "Transport signature")],
string="Signature Class",
help="Please doublecheck the security of your Odoo "
"ERP system when using class 'E' to prevent unauthorised "
"users to make supplier payments."
"\nLeave this field empty to use the default "
"defined for your EBICS UserID.")
"defined for your EBICS UserID.",
)
description = fields.Char()
suffix = fields.Char(
required=True,
help="Specify the filename suffix for this File Format."
"\nE.g. c53.xml")
help="Specify the filename suffix for this File Format." "\nE.g. c53.xml",
)
@api.model
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]
@api.onchange('type')
@api.onchange("type")
def _onchange_type(self):
if self.type == 'up':
if self.type == "up":
self.download_process_method = False

View File

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

View File

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

View File

@ -1,32 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="res.groups" id="group_ebics_manager">
<field name="name">EBICS Manager</field>
<field name="category_id" ref="base.module_category_hidden"/>
<field name="category_id" ref="base.module_category_hidden" />
</record>
<data noupdate="1">
<record id="ebics_config_comp_rule" model="ir.rule">
<field name="name">EBICS Configuration model company rule</field>
<field name="model_id" ref="model_ebics_config"/>
<field eval="True" name="global"/>
<field name="domain_force">['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
<field name="model_id" ref="model_ebics_config" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record>
<record id="ebics_userid_comp_rule" model="ir.rule">
<field name="name">EBICS UserID model company rule</field>
<field name="model_id" ref="model_ebics_userid"/>
<field eval="True" name="global"/>
<field name="domain_force">['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
<field name="model_id" ref="model_ebics_userid" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record>
<record id="ebics_file_comp_rule" model="ir.rule">
<field name="name">EBICS File model company rule</field>
<field name="model_id" ref="model_ebics_file"/>
<field eval="True" name="global"/>
<field name="domain_force">['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
<field name="model_id" ref="model_ebics_file" />
<field eval="True" name="global" />
<field
name="domain_force"
>['|', ('company_ids', '=', False), ('company_ids', 'in', user.company_ids.ids)]</field>
</record>
</data>

View File

@ -6,10 +6,10 @@
<field name="model">ebics.config</field>
<field name="arch" type="xml">
<tree string="EBICS Configuration" decoration-muted="state == 'draft'">
<field name="name"/>
<field name="ebics_host"/>
<field name="state"/>
<field name="active"/>
<field name="name" />
<field name="ebics_host" />
<field name="state" />
<field name="active" />
</tree>
</field>
</record>
@ -20,44 +20,63 @@
<field name="arch" type="xml">
<form string="EBICS Configuration">
<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"
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"
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"
groups="account_ebics.group_ebics_manager"
help="The EBICS configuration must be confirmed before it can used for bank transactions."/>
<field name="state" widget="statusbar"/>
help="The EBICS configuration must be confirmed before it can used for bank transactions."
/>
<field name="state" widget="statusbar" />
</header>
<field name="active" invisible="1" />
<widget name="web_ribbon"
<widget
name="web_ribbon"
text="Archived"
bg_color="bg-danger"
attrs="{'invisible': [('active', '=', True)]}"/>
attrs="{'invisible': [('active', '=', True)]}"
/>
<group name="main">
<group name="main-left">
<field name="name" colspan="2"/>
<field name="ebics_host"/>
<field name="ebics_url"/>
<field name="ebics_partner"/>
<field name="ebics_files"/>
<field name="ebics_keys"/>
<field name="name" colspan="2" />
<field name="ebics_host" />
<field name="ebics_url" />
<field name="ebics_partner" />
<field name="ebics_files" />
<field name="ebics_keys" />
</group>
<group name="main-right">
<field name="journal_ids" widget="many2many_tags" options="{'no_create': True}"/>
<field name="ebics_version"/>
<field name="ebics_key_version"/>
<field name="ebics_key_bitlength"/>
<field name="order_number"
attrs="{'invisible': [('ebics_version', '=', 'H004')]}"/>
<field
name="journal_ids"
widget="many2many_tags"
options="{'no_create': True}"
/>
<field name="ebics_version" />
<field name="ebics_key_version" />
<field name="ebics_key_bitlength" />
<field
name="order_number"
attrs="{'invisible': [('ebics_version', '=', 'H004')]}"
/>
</group>
<field name="company_ids" invisible="1"/>
<field name="company_ids" invisible="1" />
</group>
<notebook>
<page string="EBICS Users" groups="account_ebics.group_ebics_manager">
<field name="ebics_userid_ids"/>
<field name="ebics_userid_ids" />
</page>
<page string="File Formats" groups="account_ebics.group_ebics_manager">
<field name="ebics_file_format_ids"/>
<field name="ebics_file_format_ids" />
</page>
</notebook>
</form>

View File

@ -6,11 +6,11 @@
<field name="model">ebics.file.format</field>
<field name="arch" type="xml">
<tree string="EBICS File Formats">
<field name="type"/>
<field name="order_type"/>
<field name="signature_class"/>
<field name="name"/>
<field name="description"/>
<field name="type" />
<field name="order_type" />
<field name="signature_class" />
<field name="name" />
<field name="description" />
</tree>
</field>
</record>
@ -22,20 +22,22 @@
<form string="EBICS File Format">
<group name="main">
<group name="main-left">
<field name="type"/>
<field name="suffix"/>
<field name="download_process_method"
<field name="type" />
<field name="suffix" />
<field
name="download_process_method"
attrs="{'invisible': [('type', '=', 'up')]}"
force_save="1"/>
<field name="signature_class"/>
force_save="1"
/>
<field name="signature_class" />
</group>
<group name="main-right">
<field name="order_type"/>
<field name="name"/>
<field name="order_type" />
<field name="name" />
</group>
</group>
<group name="description">
<field name="description"/>
<field name="description" />
</group>
</form>
</field>

View File

@ -7,18 +7,26 @@
<field name="arch" type="xml">
<search string="Search EBICS Files">
<group col="10" colspan="4">
<field name="date_from"/>
<field name="date_to"/>
<field name="name"/>
<field name="format_id"/>
<field name="user_id"/>
<field name="company_ids" widget="selection" groups="base.group_multi_company"/>
<field name="date_from" />
<field name="date_to" />
<field name="name" />
<field name="format_id" />
<field name="user_id" />
<field
name="company_ids"
widget="selection"
groups="base.group_multi_company"
/>
</group>
<newline/>
<newline />
<group expand="0" string="Group By">
<filter string="File Format" name="file_format" context="{'group_by':'format_id'}"/>
<filter string="State" name="state" context="{'group_by':'state'}"/>
<filter string="User" name="user" context="{'group_by':'user_id'}"/>
<filter
string="File Format"
name="file_format"
context="{'group_by':'format_id'}"
/>
<filter string="State" name="state" context="{'group_by':'state'}" />
<filter string="User" name="user" context="{'group_by':'user_id'}" />
</group>
</search>
</field>
@ -31,14 +39,18 @@
<field name="model">ebics.file</field>
<field name="arch" type="xml">
<tree string="EBICS Files" decoration-muted="state=='draft'" create="false">
<field name="date" string="Download Date"/>
<field name="name"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="user_id"/>
<field name="state"/>
<field name="format_id"/>
<field name="company_ids" widget="many2many_tags" groups="base.group_multi_company"/>
<field name="date" string="Download Date" />
<field name="name" />
<field name="date_from" />
<field name="date_to" />
<field name="user_id" />
<field name="state" />
<field name="format_id" />
<field
name="company_ids"
widget="many2many_tags"
groups="base.group_multi_company"
/>
</tree>
</field>
</record>
@ -50,34 +62,55 @@
<field name="arch" type="xml">
<form string="EBICS File" create="false">
<header>
<button name="set_to_draft" states="done" string="Set to Draft" type="object" groups="account.group_account_manager"/>
<button name="process"
<button
name="set_to_draft"
states="done"
string="Set to Draft"
type="object"
groups="account.group_account_manager"
/>
<button
name="process"
class="oe_highlight"
states="draft"
string="Process"
type="object"
groups="account.group_account_invoice"
help="Process the EBICS File"/>
<button name="set_to_done" states="draft" string="Set to Done" type="object" groups="account.group_account_manager"/>
<field name="state" widget="statusbar"/>
help="Process the EBICS File"
/>
<button
name="set_to_done"
states="draft"
string="Set to Done"
type="object"
groups="account.group_account_manager"
/>
<field name="state" widget="statusbar" />
</header>
<group colspan="4" col="4">
<field name="date" string="Download Date"/>
<field name="name"/>
<field name="data" filename="name"/>
<field name="format_id"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="user_id"/>
<field name="ebics_userid_id"/>
<field name="company_ids" widget="many2many_tags" groups="base.group_multi_company"/>
<field name="date" string="Download Date" />
<field name="name" />
<field name="data" filename="name" />
<field name="format_id" />
<field name="date_from" />
<field name="date_to" />
<field name="user_id" />
<field name="ebics_userid_id" />
<field
name="company_ids"
widget="many2many_tags"
groups="base.group_multi_company"
/>
</group>
<notebook>
<page string="Additional Information">
<field name="note" nolabel="1"/>
<field name="note" nolabel="1" />
</page>
<page string="Bank Statements" attrs="{'invisible':[('bank_statement_ids','=',[])]}">
<field name="bank_statement_ids" nolabel="1"/>
<page
string="Bank Statements"
attrs="{'invisible':[('bank_statement_ids','=',[])]}"
>
<field name="bank_statement_ids" nolabel="1" />
</page>
</notebook>
</form>
@ -91,12 +124,16 @@
<field name="arch" type="xml">
<form string="Process EBICS File">
<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>
<button name="action_open_bank_statements" 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="action_open_bank_statements"
string="View Bank Statement(s)"
type="object"
class="oe_highlight"
invisible="not context.get('statement_ids')"
/>
<button name="button_close" type="object" string="Close" />
</footer>
</form>
</field>
@ -107,23 +144,23 @@
<field name="type">ir.actions.act_window</field>
<field name="res_model">ebics.file</field>
<field name="view_mode">tree,form</field>
<field name="view_id" eval="False"/>
<field name="view_id" eval="False" />
<field name="domain">[('type','=','down')]</field>
<field name="search_view_id" ref="ebics_file_view_search"/>
<field name="search_view_id" ref="ebics_file_view_search" />
</record>
<record id="ebics_file_action_download_tree" model="ir.actions.act_window.view">
<field eval="1" name="sequence"/>
<field eval="1" name="sequence" />
<field name="view_mode">tree</field>
<field name="view_id" ref="ebics_file_view_tree_download"/>
<field name="act_window_id" ref="ebics_file_action_download"/>
<field name="view_id" ref="ebics_file_view_tree_download" />
<field name="act_window_id" ref="ebics_file_action_download" />
</record>
<record id="ebics_file_action_download_form" model="ir.actions.act_window.view">
<field eval="2" name="sequence"/>
<field eval="2" name="sequence" />
<field name="view_mode">form</field>
<field name="view_id" ref="ebics_file_view_form_download"/>
<field name="act_window_id" ref="ebics_file_action_download"/>
<field name="view_id" ref="ebics_file_view_form_download" />
<field name="act_window_id" ref="ebics_file_action_download" />
</record>
<!-- Upload -->
@ -133,12 +170,16 @@
<field name="model">ebics.file</field>
<field name="arch" type="xml">
<tree string="EBICS Files" decoration-muted="state=='draft'" create="false">
<field name="date" string="Upload Date"/>
<field name="name"/>
<field name="user_id"/>
<field name="state"/>
<field name="format_id"/>
<field name="company_ids" widget="many2many_tags" groups="base.group_multi_company"/>
<field name="date" string="Upload Date" />
<field name="name" />
<field name="user_id" />
<field name="state" />
<field name="format_id" />
<field
name="company_ids"
widget="many2many_tags"
groups="base.group_multi_company"
/>
</tree>
</field>
</record>
@ -150,22 +191,38 @@
<field name="arch" type="xml">
<form string="EBICS File" create="false">
<header>
<button 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"/>
<button
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" />
</header>
<group colspan="4" col="4">
<field name="date" string="Upload Date"/>
<field name="name"/>
<field name="data" filename="name"/>
<field name="format_id"/>
<field name="user_id"/>
<field name="ebics_userid_id"/>
<field name="company_ids" widget="many2many_tags" groups="base.group_multi_company"/>
<field name="date" string="Upload Date" />
<field name="name" />
<field name="data" filename="name" />
<field name="format_id" />
<field name="user_id" />
<field name="ebics_userid_id" />
<field
name="company_ids"
widget="many2many_tags"
groups="base.group_multi_company"
/>
</group>
<notebook>
<page string="Additional Information">
<field name="note" nolabel="1"/>
<field name="note" nolabel="1" />
</page>
</notebook>
</form>
@ -177,23 +234,23 @@
<field name="type">ir.actions.act_window</field>
<field name="res_model">ebics.file</field>
<field name="view_mode">tree,form</field>
<field name="view_id" eval="False"/>
<field name="view_id" eval="False" />
<field name="domain">[('type','=','up')]</field>
<field name="search_view_id" ref="ebics_file_view_search"/>
<field name="search_view_id" ref="ebics_file_view_search" />
</record>
<record id="ebics_file_action_upload_tree" model="ir.actions.act_window.view">
<field eval="1" name="sequence"/>
<field eval="1" name="sequence" />
<field name="view_mode">tree</field>
<field name="view_id" ref="ebics_file_view_tree_upload"/>
<field name="act_window_id" ref="ebics_file_action_upload"/>
<field name="view_id" ref="ebics_file_view_tree_upload" />
<field name="act_window_id" ref="ebics_file_action_upload" />
</record>
<record id="ebics_file_action_upload_form" model="ir.actions.act_window.view">
<field eval="2" name="sequence"/>
<field eval="2" name="sequence" />
<field name="view_mode">form</field>
<field name="view_id" ref="ebics_file_view_form_upload"/>
<field name="act_window_id" ref="ebics_file_action_upload"/>
<field name="view_id" ref="ebics_file_view_form_upload" />
<field name="act_window_id" ref="ebics_file_action_upload" />
</record>
</odoo>

View File

@ -6,10 +6,10 @@
<field name="model">ebics.userid</field>
<field name="arch" type="xml">
<tree string="EBICS UserID" decoration-muted="state != 'active_keys'">
<field name="name"/>
<field name="signature_class"/>
<field name="state"/>
<field name="active"/>
<field name="name" />
<field name="signature_class" />
<field name="state" />
<field name="active" />
</tree>
</field>
</record>
@ -20,67 +20,136 @@
<field name="arch" type="xml">
<form string="EBICS UserID">
<header groups="account_ebics.group_ebics_manager">
<button name="ebics_init_1" states="draft" string="EBICS Initialisation" type="object" class="oe_highlight"
help="Initialise EBICS Bank Keys"/>
<button name="ebics_init_2" states="init" string="Account activated" type="object" class="oe_highlight"
help="EBICS Initialisation - Push this button when the account has been activated by the bank."/>
<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"/>
<button
name="ebics_init_1"
states="draft"
string="EBICS Initialisation"
type="object"
class="oe_highlight"
help="Initialise EBICS Bank Keys"
/>
<button
name="ebics_init_2"
states="init"
string="Account activated"
type="object"
class="oe_highlight"
help="EBICS Initialisation - Push this button when the account has been activated by the bank."
/>
<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" />
</header>
<group name="main" attrs="{'readonly': [('state', '!=', 'draft')]}">
<field name="ebics_keys_found" invisible="1"/>
<field name="ebics_keys_fn" invisible="1"/>
<field name="ebics_keys_found" invisible="1" />
<field name="ebics_keys_fn" invisible="1" />
<group name="main-left">
<field name="name"/>
<field name="ebics_passphrase" password="True"
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" filename="swift_3skey_certificate_fn"
attrs="{'invisible': [('swift_3skey', '=', False)], 'required': [('swift_3skey', '=', True)]}"/>
<field name="active"/>
<field name="name" />
<field
name="ebics_passphrase"
password="True"
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"
filename="swift_3skey_certificate_fn"
attrs="{'invisible': [('swift_3skey', '=', False)], 'required': [('swift_3skey', '=', True)]}"
/>
<field name="active" />
</group>
<group name="main-right">
<field name="signature_class"/>
<field name="user_ids" widget="many2many_tags" options="{'no_create': True}"/>
<field name="ebics_key_x509"/>
<field name="signature_class" />
<field
name="user_ids"
widget="many2many_tags"
options="{'no_create': True}"
/>
<field name="ebics_key_x509" />
</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">
<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 name="dn_l" colspan="2">
<field name="ebics_key_x509_dn_cn"/>
<field name="ebics_key_x509_dn_o"/>
<field name="ebics_key_x509_dn_l"/>
<field name="ebics_key_x509_dn_c"/>
<field name="ebics_key_x509_dn_cn" />
<field name="ebics_key_x509_dn_o" />
<field name="ebics_key_x509_dn_l" />
<field name="ebics_key_x509_dn_c" />
</group>
<group name="dn_r" colspan="2">
<field name="ebics_key_x509_dn_e"/>
<field name="ebics_key_x509_dn_ou"/>
<field name="ebics_key_x509_dn_st"/>
<field name="ebics_key_x509_dn_e" />
<field name="ebics_key_x509_dn_ou" />
<field name="ebics_key_x509_dn_st" />
</group>
</group>
<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" filename="ebics_ini_letter_fn"/>
<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" filename="ebics_ini_letter_fn" />
</group>
<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" filename="ebics_public_bank_keys_fn"/>
<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" filename="ebics_public_bank_keys_fn" />
</group>
</form>
</field>

View File

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

View File

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

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="ebics_change_passphrase_view_form" model="ir.ui.view">
@ -8,14 +8,19 @@
<field name="arch" type="xml">
<form string="EBICS Keys Change Passphrase">
<group>
<field name="old_pass" password="True"/>
<field name="new_pass" password="True"/>
<field name="new_pass_check" password="True"/>
<field name="old_pass" password="True" />
<field name="new_pass" password="True" />
<field name="new_pass_check" password="True" />
</group>
<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
<button string="Cancel" class="oe_link" special="cancel"/>
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
@ -28,9 +33,9 @@
<field name="arch" type="xml">
<form string="EBICS Keys Change Passphrase">
<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>
<button name="button_close" type="object" string="Close"/>
<button name="button_close" type="object" string="Close" />
</footer>
</form>
</field>

View File

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

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="ebics_xfer_view_form_download" model="ir.ui.view">
@ -8,23 +8,36 @@
<field name="arch" type="xml">
<form string="EBICS File Download">
<group>
<separator string="Select your bank :" colspan="2"/>
<field name="ebics_config_id" required="1" options="{'no_create': True, 'no_open': True}"/>
<field name="ebics_userid_id"
<separator string="Select your bank :" colspan="2" />
<field
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)]"
required="1"
options="{'no_create': True, 'no_open': True}"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="format_id"
domain="[('type', '=', 'down'), ('id', 'in', allowed_format_ids)]"/>
<field name="order_type"/>
<field name="allowed_format_ids" invisible="1"/>
options="{'no_create': True, 'no_open': True}"
/>
<field name="date_from" />
<field name="date_to" />
<field
name="format_id"
domain="[('type', '=', 'down'), ('id', 'in', allowed_format_ids)]"
/>
<field name="order_type" />
<field name="allowed_format_ids" invisible="1" />
</group>
<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
<button string="Cancel" class="oe_link" special="cancel"/>
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
@ -37,26 +50,43 @@
<field name="arch" type="xml">
<form string="EBICS File Upload">
<group>
<separator string="Select your bank :" colspan="2"/>
<field name="ebics_config_id" required="1" options="{'no_create': True, 'no_open': True}"/>
<field name="ebics_userid_id"
<separator string="Select your bank :" colspan="2" />
<field
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)]"
required="1"
options="{'no_create': True, 'no_open': True}"/>
<separator string="Select your file :" colspan="2"/>
<field name="upload_data" filename="upload_fname" required="1"/>
<field name="upload_fname" invisible="1"/>
<field name="upload_fname_dummy"/>
<field name="format_id" required="1"
domain="[('type', '=', 'up'), ('id', 'in', allowed_format_ids)]"/>
<field name="order_type"/>
<field name="test_mode" attrs="{'invisible': [('order_type', '!=', 'FUL')]}"/>
<field name="allowed_format_ids" invisible="1"/>
options="{'no_create': True, 'no_open': True}"
/>
<separator string="Select your file :" colspan="2" />
<field name="upload_data" filename="upload_fname" required="1" />
<field name="upload_fname" invisible="1" />
<field name="upload_fname_dummy" />
<field
name="format_id"
required="1"
domain="[('type', '=', 'up'), ('id', 'in', allowed_format_ids)]"
/>
<field name="order_type" />
<field
name="test_mode"
attrs="{'invisible': [('order_type', '!=', 'FUL')]}"
/>
<field name="allowed_format_ids" invisible="1" />
</group>
<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
<button string="Cancel" class="oe_link" special="cancel"/>
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
@ -69,11 +99,16 @@
<field name="arch" type="xml">
<form string="EBICS File Transfer">
<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>
<button 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="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" />
</footer>
</form>
</field>
@ -86,7 +121,7 @@
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'ebics_download': 1}</field>
<field name="view_id" ref="ebics_xfer_view_form_download"/>
<field name="view_id" ref="ebics_xfer_view_form_download" />
</record>
<record id="ebics_xfer_action_upload" model="ir.actions.act_window">
@ -96,7 +131,7 @@
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'ebics_upload': 1}</field>
<field name="view_id" ref="ebics_xfer_view_form_upload"/>
<field name="view_id" ref="ebics_xfer_view_form_upload" />
</record>
</odoo>

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).
{
'name': 'account_ebics with OCA Bank Statement Imoort',
'summary': "Use OCA Bank Statement Import with account_ebics",
'version': '14.0.1.0.0',
'author': 'Noviat',
'category': 'Hidden',
'license': 'LGPL-3',
'depends': [
'account_ebics',
'account_statement_import',
"name": "account_ebics with OCA Bank Statement Imoort",
"summary": "Use OCA Bank Statement Import with account_ebics",
"version": "14.0.1.0.0",
"author": "Noviat",
"category": "Hidden",
"license": "LGPL-3",
"depends": [
"account_ebics",
"account_statement_import",
],
'installable': True,
'auto_install': True,
"installable": True,
"auto_install": True,
}

View File

@ -3,32 +3,34 @@
import logging
from odoo import models, _
from odoo import _, models
_logger = logging.getLogger(__name__)
class AccountStatementImport(models.TransientModel):
_inherit = 'account.statement.import'
_inherit = "account.statement.import"
def _check_parsed_data(self, stmts_vals):
""" Basic and structural verifications """
if self.env.context.get('active_model') == 'ebics.file':
if self.env.context.get("active_model") == "ebics.file":
message = False
if len(stmts_vals) == 0:
message = _("This file doesn't contain any statement.")
if not message:
no_st_line = True
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
break
if no_st_line:
message = _('This file doesn\'t contain any transaction.')
message = _("This file doesn't contain any transaction.")
if message:
log_msg = _(
"Error detected while processing and EBICS File"
) + ':\n' + message
log_msg = (
_("Error detected while processing and EBICS File")
+ ":\n"
+ message
)
_logger.warn(log_msg)
return
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
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
for st_vals in stmts_vals:
if st_vals.get('transactions'):
if st_vals.get("transactions"):
transactions = True
break
if not transactions:
message = _('This file doesn\'t contain any transaction.')
message = _("This file doesn't contain any transaction.")
st_line_ids = []
notifications = {
'type': 'warning',
'message': message,
'details': ''
}
notifications = {"type": "warning", "message": message, "details": ""}
return st_line_ids, [notifications]
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).
{
'name': 'account_ebics on Odoo Enterprise',
'summary': "Deploy account_ebics module on Odoo Enterprise",
'version': '14.0.1.0.0',
'author': 'Noviat',
'category': 'Hidden',
'license': 'LGPL-3',
'depends': [
'account_ebics',
'account_accountant',
"name": "account_ebics on Odoo Enterprise",
"summary": "Deploy account_ebics module on Odoo Enterprise",
"version": "14.0.1.0.0",
"author": "Noviat",
"category": "Hidden",
"license": "LGPL-3",
"depends": [
"account_ebics",
"account_accountant",
],
'data': [
'views/account_ebics_menu.xml'
],
'installable': True,
'auto_install': True,
"data": ["views/account_ebics_menu.xml"],
"installable": True,
"auto_install": True,
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="account_ebics.ebics_processing_menu" model="ir.ui.menu">
<field name="parent_id" eval="ref('account_accountant.menu_accounting')"/>
<field name="parent_id" eval="ref('account_accountant.menu_accounting')" />
</record>
</odoo>

View File

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

View File

@ -3,32 +3,34 @@
import logging
from odoo import models, _
from odoo import _, models
_logger = logging.getLogger(__name__)
class AccountBankStatementImport(models.TransientModel):
_inherit = 'account.bank.statement.import'
_inherit = "account.bank.statement.import"
def _check_parsed_data(self, stmts_vals, account_number):
""" Basic and structural verifications """
if self.env.context.get('active_model') == 'ebics.file':
if self.env.context.get("active_model") == "ebics.file":
message = False
if len(stmts_vals) == 0:
message = _("This file doesn't contain any statement.")
if not message:
no_st_line = True
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
break
if no_st_line:
message = _('This file doesn\'t contain any transaction.')
message = _("This file doesn't contain any transaction.")
if message:
log_msg = _(
"Error detected while processing and EBICS File"
) + ':\n' + message
log_msg = (
_("Error detected while processing and EBICS File")
+ ":\n"
+ message
)
_logger.warn(log_msg)
return
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
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
for st_vals in stmts_vals:
if st_vals.get('transactions'):
if st_vals.get("transactions"):
transactions = True
break
if not transactions:
message = _('This file doesn\'t contain any transaction.')
message = _("This file doesn't contain any transaction.")
st_line_ids = []
notifications = {
'type': 'warning',
'message': message,
'details': ''
}
notifications = {"type": "warning", "message": message, "details": ""}
return st_line_ids, [notifications]
return super()._create_bank_statements(stmts_vals)

View File

@ -2,16 +2,14 @@
# License LGPL-3 or later (http://www.gnu.org/licenses/lpgl).
{
'name': 'Upload Payment Order via EBICS',
'version': '14.0.1.0.0',
'license': 'LGPL-3',
'author': 'Noviat',
'category': 'Accounting & Finance',
'depends': [
'account_ebics',
'account_payment_order'],
'data': [
'views/account_payment_order.xml',
"name": "Upload Payment Order via EBICS",
"version": "14.0.1.0.0",
"license": "LGPL-3",
"author": "Noviat",
"category": "Accounting & Finance",
"depends": ["account_ebics", "account_payment_order"],
"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):
_inherit = 'account.payment.order'
_inherit = "account.payment.order"
def ebics_upload(self):
self.ensure_one()
ctx = self._context.copy()
attach = self.env['ir.attachment'].search(
[('res_model', '=', self._name),
('res_id', '=', self.id)])
attach = self.env["ir.attachment"].search(
[("res_model", "=", self._name), ("res_id", "=", self.id)]
)
if not attach:
raise UserError(_(
raise UserError(
_(
"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:
raise UserError(_(
raise UserError(
_(
"This payment order contains multiple attachments."
"\nPlease remove the obsolete attachments or upload "
"the payment order file via the "
"EBICS Processing > EBICS Upload menu"))
"EBICS Processing > EBICS Upload menu"
)
)
else:
origin = _("Payment Order") + ': ' + self.name
ebics_config = self.env['ebics.config'].search([
('journal_ids', '=', self.journal_id.id),
('state', '=', 'confirm'),
])
origin = _("Payment Order") + ": " + self.name
ebics_config = self.env["ebics.config"].search(
[
("journal_ids", "=", self.journal_id.id),
("state", "=", "confirm"),
]
)
if not ebics_config:
raise UserError(_(
raise UserError(
_(
"No active EBICS configuration available "
"for the selected bank."
))
)
)
if len(ebics_config) == 1:
ctx["default_ebics_config_id"] = ebics_config.id
ctx.update({
'default_upload_data': attach.datas,
'default_upload_fname': attach.name,
'origin': origin,
'force_comany': self.company_id.id,
})
ebics_xfer = self.env['ebics.xfer'].with_context(ctx).create({})
ctx.update(
{
"default_upload_data": attach.datas,
"default_upload_fname": attach.name,
"origin": origin,
"force_comany": self.company_id.id,
}
)
ebics_xfer = self.env["ebics.xfer"].with_context(ctx).create({})
ebics_xfer._onchange_ebics_config_id()
ebics_xfer._onchange_upload_data()
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 = {
'name': _('EBICS Upload'),
'view_type': 'form',
'view_mode': 'form',
'res_model': 'ebics.xfer',
'view_id': view.id,
'res_id': ebics_xfer.id,
'type': 'ir.actions.act_window',
'target': 'new',
'context': ctx,
"name": _("EBICS Upload"),
"view_type": "form",
"view_mode": "form",
"res_model": "ebics.xfer",
"view_id": view.id,
"res_id": ebics_xfer.id,
"type": "ir.actions.act_window",
"target": "new",
"context": ctx,
}
return act

View File

@ -1,15 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data>
<record id="account_payment_order_form" model="ir.ui.view">
<field name="name">account.payment.order.form</field>
<field name="model">account.payment.order</field>
<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">
<button name="open2generated" position="after">
<button name="ebics_upload" type="object" states="generated"
string="EBICS Upload" class="oe_highlight"/>
<button
name="ebics_upload"
type="object"
states="generated"
string="EBICS Upload"
class="oe_highlight"
/>
</button>
</field>
</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,
)