[14.0]add account_ebics_batch

This commit is contained in:
Luc De Meyer 2022-05-07 18:42:03 +02:00
parent 4e9e558e16
commit 79250ab61a
37 changed files with 1316 additions and 244 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

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

View File

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

View File

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

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