diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 0000000..c599526 --- /dev/null +++ b/.copier-answers.yml @@ -0,0 +1,26 @@ +# Do NOT update manually; changes here will be overwritten by Copier +_commit: v1.20 +_src_path: https://github.com/OCA/oca-addons-repo-template +additional_ruff_rules: [] +ci: GitHub +convert_readme_fragments_to_markdown: false +generate_requirements_txt: true +github_check_license: true +github_ci_extra_env: {} +github_enable_codecov: true +github_enable_makepot: true +github_enable_stale_action: true +github_enforce_dev_status_compatibility: true +include_wkhtmltopdf: false +odoo_test_flavor: OCB +odoo_version: 16.0 +org_name: Akretion +org_slug: akretion +rebel_module_groups: [] +repo_description: '' +repo_name: Account Move Import +repo_slug: account-move-import +repo_website: https://github.com/akretion/account-move-import +use_pyproject_toml: false +use_ruff: true + diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bfd7ac5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# Configuration for known file extensions +[*.{css,js,json,less,md,py,rst,sass,scss,xml,yaml,yml}] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{json,yml,yaml,rst,md}] +indent_size = 2 + +# Do not configure editor for libs and autogenerated content +[{*/static/{lib,src/lib}/**,*/static/description/index.html,*/readme/../README.rst}] +charset = unset +end_of_line = unset +indent_size = unset +indent_style = unset +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..fed88d7 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,188 @@ +env: + browser: true + es6: true + +# See https://github.com/OCA/odoo-community.org/issues/37#issuecomment-470686449 +parserOptions: + ecmaVersion: 2019 + +overrides: + - files: + - "**/*.esm.js" + parserOptions: + sourceType: module + +# Globals available in Odoo that shouldn't produce errorings +globals: + _: readonly + $: readonly + fuzzy: readonly + jQuery: readonly + moment: readonly + odoo: readonly + openerp: readonly + owl: readonly + luxon: readonly + +# Styling is handled by Prettier, so we only need to enable AST rules; +# see https://github.com/OCA/maintainer-quality-tools/pull/618#issuecomment-558576890 +rules: + accessor-pairs: warn + array-callback-return: warn + callback-return: warn + capitalized-comments: + - warn + - always + - ignoreConsecutiveComments: true + ignoreInlineComments: true + complexity: + - warn + - 15 + constructor-super: warn + dot-notation: warn + eqeqeq: warn + global-require: warn + handle-callback-err: warn + id-blacklist: warn + id-match: warn + init-declarations: error + max-depth: warn + max-nested-callbacks: warn + max-statements-per-line: warn + no-alert: warn + no-array-constructor: warn + no-caller: warn + no-case-declarations: warn + no-class-assign: warn + no-cond-assign: error + no-const-assign: error + no-constant-condition: warn + no-control-regex: warn + no-debugger: error + no-delete-var: warn + no-div-regex: warn + no-dupe-args: error + no-dupe-class-members: error + no-dupe-keys: error + no-duplicate-case: error + no-duplicate-imports: error + no-else-return: warn + no-empty-character-class: warn + no-empty-function: error + no-empty-pattern: error + no-empty: warn + no-eq-null: error + no-eval: error + no-ex-assign: error + no-extend-native: warn + no-extra-bind: warn + no-extra-boolean-cast: warn + no-extra-label: warn + no-fallthrough: warn + no-func-assign: error + no-global-assign: error + no-implicit-coercion: + - warn + - allow: ["~"] + no-implicit-globals: warn + no-implied-eval: warn + no-inline-comments: warn + no-inner-declarations: warn + no-invalid-regexp: warn + no-irregular-whitespace: warn + no-iterator: warn + no-label-var: warn + no-labels: warn + no-lone-blocks: warn + no-lonely-if: error + no-mixed-requires: error + no-multi-str: warn + no-native-reassign: error + no-negated-condition: warn + no-negated-in-lhs: error + no-new-func: warn + no-new-object: warn + no-new-require: warn + no-new-symbol: warn + no-new-wrappers: warn + no-new: warn + no-obj-calls: warn + no-octal-escape: warn + no-octal: warn + no-param-reassign: warn + no-path-concat: warn + no-process-env: warn + no-process-exit: warn + no-proto: warn + no-prototype-builtins: warn + no-redeclare: warn + no-regex-spaces: warn + no-restricted-globals: warn + no-restricted-imports: warn + no-restricted-modules: warn + no-restricted-syntax: warn + no-return-assign: error + no-script-url: warn + no-self-assign: warn + no-self-compare: warn + no-sequences: warn + no-shadow-restricted-names: warn + no-shadow: warn + no-sparse-arrays: warn + no-sync: warn + no-this-before-super: warn + no-throw-literal: warn + no-undef-init: warn + no-undef: error + no-unmodified-loop-condition: warn + no-unneeded-ternary: error + no-unreachable: error + no-unsafe-finally: error + no-unused-expressions: error + no-unused-labels: error + no-unused-vars: error + no-use-before-define: error + no-useless-call: warn + no-useless-computed-key: warn + no-useless-concat: warn + no-useless-constructor: warn + no-useless-escape: warn + no-useless-rename: warn + no-void: warn + no-with: warn + operator-assignment: [error, always] + prefer-const: warn + radix: warn + require-yield: warn + sort-imports: warn + spaced-comment: [error, always] + strict: [error, function] + use-isnan: error + valid-jsdoc: + - warn + - prefer: + arg: param + argument: param + augments: extends + constructor: class + exception: throws + func: function + method: function + prop: property + return: returns + virtual: abstract + yield: yields + preferType: + array: Array + bool: Boolean + boolean: Boolean + number: Number + object: Object + str: String + string: String + requireParamDescription: false + requireReturn: false + requireReturnDescription: false + requireReturnType: false + valid-typeof: warn + yoda: warn diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..38b0ba1 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,46 @@ +name: pre-commit + +on: + pull_request: + branches: + - "16.0*" + push: + branches: + - "16.0" + - "16.0-ocabot-*" + +jobs: + pre-commit: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v2 + with: + python-version: "3.11" + - name: Get python version + run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV + - uses: actions/cache@v1 + with: + path: ~/.cache/pre-commit + key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} + - name: Install pre-commit + run: pip install pre-commit + - name: Run pre-commit + run: pre-commit run --all-files --show-diff-on-failure --color=always + env: + # Consider valid a PR that changes README fragments but doesn't + # change the README.rst file itself. It's not really a problem + # because the bot will update it anyway after merge. This way, we + # lower the barrier for functional contributors that want to fix the + # readme fragments, while still letting developers get README + # auto-generated (which also helps functionals when using runboat). + # DOCS https://pre-commit.com/#temporarily-disabling-hooks + SKIP: oca-gen-addon-readme + - name: Check that all files generated by pre-commit are in git + run: | + newfiles="$(git ls-files --others --exclude-from=.gitignore)" + if [ "$newfiles" != "" ] ; then + echo "Please check-in the following files:" + echo "$newfiles" + exit 1 + fi diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..1693a12 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,69 @@ +name: Mark stale issues and pull requests + +on: + schedule: + - cron: "0 12 * * 0" + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - name: Stale PRs and issues policy + uses: actions/stale@v4 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + # General settings. + ascending: true + remove-stale-when-updated: true + # Pull Requests settings. + # 120+30 day stale policy for PRs + # * Except PRs marked as "no stale" + days-before-pr-stale: 120 + days-before-pr-close: 30 + exempt-pr-labels: "no stale" + stale-pr-label: "stale" + stale-pr-message: > + There hasn't been any activity on this pull request in the past 4 months, so + it has been marked as stale and it will be closed automatically if no + further activity occurs in the next 30 days. + + If you want this PR to never become stale, please ask a PSC member to apply + the "no stale" label. + # Issues settings. + # 180+30 day stale policy for open issues + # * Except Issues marked as "no stale" + days-before-issue-stale: 180 + days-before-issue-close: 30 + exempt-issue-labels: "no stale,needs more information" + stale-issue-label: "stale" + stale-issue-message: > + There hasn't been any activity on this issue in the past 6 months, so it has + been marked as stale and it will be closed automatically if no further + activity occurs in the next 30 days. + + If you want this issue to never become stale, please ask a PSC member to + apply the "no stale" label. + + # 15+30 day stale policy for issues pending more information + # * Issues that are pending more information + # * Except Issues marked as "no stale" + - name: Needs more information stale issues policy + uses: actions/stale@v4 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + ascending: true + only-labels: "needs more information" + exempt-issue-labels: "no stale" + days-before-stale: 15 + days-before-close: 30 + days-before-pr-stale: -1 + days-before-pr-close: -1 + remove-stale-when-updated: true + stale-issue-label: "stale" + stale-issue-message: > + This issue needs more information and there hasn't been any activity + recently, so it has been marked as stale and it will be closed automatically + if no further activity occurs in the next 30 days. + + If you think this is a mistake, please ask a PSC member to remove the "needs + more information" label. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ff8460e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,67 @@ +name: tests + +on: + pull_request: + branches: + - "16.0*" + push: + branches: + - "16.0" + - "16.0-ocabot-*" + +jobs: + unreleased-deps: + runs-on: ubuntu-latest + name: Detect unreleased dependencies + steps: + - uses: actions/checkout@v3 + - run: | + for reqfile in requirements.txt test-requirements.txt ; do + if [ -f ${reqfile} ] ; then + result=0 + # reject non-comment lines that contain a / (i.e. URLs, relative paths) + grep "^[^#].*/" ${reqfile} || result=$? + if [ $result -eq 0 ] ; then + echo "Unreleased dependencies found in ${reqfile}." + exit 1 + fi + fi + done + test: + runs-on: ubuntu-22.04 + container: ${{ matrix.container }} + name: ${{ matrix.name }} + strategy: + fail-fast: false + matrix: + include: + - container: ghcr.io/oca/oca-ci/py3.10-ocb16.0:latest + name: test with OCB + makepot: "true" + services: + postgres: + image: postgres:12.0 + env: + POSTGRES_USER: odoo + POSTGRES_PASSWORD: odoo + POSTGRES_DB: odoo + ports: + - 5432:5432 + steps: + - uses: actions/checkout@v3 + with: + persist-credentials: false + - name: Install addons and dependencies + run: oca_install_addons + - name: Check licenses + run: manifestoo -d . check-licenses + - name: Check development status + run: manifestoo -d . check-dev-status --default-dev-status=Beta + - name: Initialize test db + run: oca_init_test_database + - name: Run tests + run: oca_run_tests + - uses: codecov/codecov-action@v1 + - name: Update .pot files + run: oca_export_and_push_pot https://x-access-token:${{ secrets.GIT_PUSH_TOKEN }}@github.com/${{ github.repository }} + if: ${{ matrix.makepot == 'true' && github.event_name == 'push' && github.repository_owner == 'akretion' }} diff --git a/.gitignore b/.gitignore index ba74660..0090721 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] +/.venv +/.pytest_cache +/.ruff_cache # C extensions *.so @@ -8,13 +11,11 @@ __pycache__/ # Distribution / packaging .Python env/ +bin/ build/ develop-eggs/ dist/ -downloads/ eggs/ -.eggs/ -lib/ lib64/ parts/ sdist/ @@ -22,12 +23,7 @@ var/ *.egg-info/ .installed.cfg *.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec +*.eggs # Installer logs pip-log.txt @@ -37,21 +33,43 @@ pip-delete-this-directory.txt htmlcov/ .tox/ .coverage -.coverage.* .cache nosetests.xml coverage.xml -*,cover # Translations *.mo -*.pot + +# Pycharm +.idea + +# Eclipse +.settings + +# Visual Studio cache/options directory +.vs/ +.vscode + +# OSX Files +.DS_Store # Django stuff: *.log +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + # Sphinx documentation docs/_build/ -# PyBuilder -target/ +# Backup files +*~ +*.swp + +# OCA rules +!static/lib/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..35a04c9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,127 @@ +exclude: | + (?x) + # NOT INSTALLABLE ADDONS + # END NOT INSTALLABLE ADDONS + # Files and folders generated by bots, to avoid loops + ^setup/|/static/description/index\.html$| + # We don't want to mess with tool-generated files + .svg$|/tests/([^/]+/)?cassettes/|^.copier-answers.yml$|^.github/| + # Maybe reactivate this when all README files include prettier ignore tags? + ^README\.md$| + # Library files can have extraneous formatting (even minimized) + /static/(src/)?lib/| + # Repos using Sphinx to generate docs don't need prettying + ^docs/_templates/.*\.html$| + # Don't bother non-technical authors with formatting issues in docs + readme/.*\.(rst|md)$| + # Ignore build and dist directories in addons + /build/|/dist/| + # You don't usually want a bot to modify your legal texts + (LICENSE.*|COPYING.*) +default_language_version: + python: python3 + node: "16.17.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$" + - id: en-po-files + name: en.po files cannot exist + entry: found a en.po file + language: fail + files: '[a-zA-Z0-9_]*/i18n/en\.po$' + - repo: https://github.com/oca/maintainer-tools + rev: 9a170331575a265c092ee6b24b845ec508e8ef75 + hooks: + # update the NOT INSTALLABLE ADDONS section above + - id: oca-update-pre-commit-excluded-addons + - id: oca-fix-manifest-website + args: ["https://github.com/akretion/account-move-import"] + - id: oca-gen-addon-readme + args: + - --addons-dir=. + - --branch=16.0 + - --org-name=akretion + - --repo-name=account-move-import + - --if-source-changed + - --keep-source-digest + - repo: https://github.com/OCA/odoo-pre-commit-hooks + rev: v0.0.25 + hooks: + - id: oca-checks-odoo-module + - id: oca-checks-po + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.7.1 + hooks: + - id: prettier + name: prettier (with plugin-xml) + additional_dependencies: + - "prettier@2.7.1" + - "@prettier/plugin-xml@2.2.0" + args: + - --plugin=@prettier/plugin-xml + files: \.(css|htm|html|js|json|jsx|less|md|scss|toml|ts|xml|yaml|yml)$ + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v8.24.0 + hooks: + - id: eslint + verbose: true + args: + - --color + - --fix + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.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/acsone/setuptools-odoo + rev: 3.1.8 + hooks: + - id: setuptools-odoo-make-default + - id: setuptools-odoo-get-requirements + args: + - --output + - requirements.txt + - --header + - "# generated from manifests external_dependencies" + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.3 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + - repo: https://github.com/OCA/pylint-odoo + rev: v8.0.19 + hooks: + - id: pylint_odoo + name: pylint with optional checks + args: + - --rcfile=.pylintrc + - --exit-zero + verbose: true + - id: pylint_odoo + args: + - --rcfile=.pylintrc-mandatory diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..5b6d4b3 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,8 @@ +# Defaults for all prettier-supported languages. +# Prettier will complete this with settings from .editorconfig file. +bracketSpacing: false +printWidth: 88 +proseWrap: always +semi: true +trailingComma: "es5" +xmlWhitespaceSensitivity: "strict" diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..b8bdae0 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,123 @@ + + +[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=Akretion +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=16.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, + attribute-string-redundant, + character-not-valid-in-resource-link, + consider-merging-classes-inherited, + context-overridden, + create-user-wo-reset-password, + dangerous-filter-wo-user, + dangerous-qweb-replace-wo-priority, + deprecated-data-xml-node, + deprecated-openerp-xml-node, + duplicate-po-message-definition, + except-pass, + file-not-used, + invalid-commit, + manifest-maintainers-list, + missing-newline-extrafiles, + missing-readme, + missing-return, + odoo-addons-relative-import, + old-api7-method-defined, + po-msgstr-variables, + po-syntax-error, + renamed-field-parameter, + resource-not-exist, + str-format-used, + test-folder-imported, + translation-contains-variable, + translation-positional-used, + unnecessary-utf8-coding-comment, + website-manifest-key-not-valid-uri, + xml-attribute-translatable, + xml-deprecated-qweb-directive, + xml-deprecated-tree-attribute, + external-request-timeout, + # messages that do not cause the lint step to fail + consider-merging-classes-inherited, + create-user-wo-reset-password, + dangerous-filter-wo-user, + deprecated-module, + file-not-used, + invalid-commit, + missing-manifest-dependency, + missing-newline-extrafiles, + missing-readme, + no-utf8-coding-comment, + odoo-addons-relative-import, + old-api7-method-defined, + redefined-builtin, + too-complex, + unnecessary-utf8-coding-comment + + +[REPORTS] +msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} +output-format=colorized +reports=no diff --git a/.pylintrc-mandatory b/.pylintrc-mandatory new file mode 100644 index 0000000..4a7d901 --- /dev/null +++ b/.pylintrc-mandatory @@ -0,0 +1,98 @@ + +[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=Akretion +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=16.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, + attribute-string-redundant, + character-not-valid-in-resource-link, + consider-merging-classes-inherited, + context-overridden, + create-user-wo-reset-password, + dangerous-filter-wo-user, + dangerous-qweb-replace-wo-priority, + deprecated-data-xml-node, + deprecated-openerp-xml-node, + duplicate-po-message-definition, + except-pass, + file-not-used, + invalid-commit, + manifest-maintainers-list, + missing-newline-extrafiles, + missing-readme, + missing-return, + odoo-addons-relative-import, + old-api7-method-defined, + po-msgstr-variables, + po-syntax-error, + renamed-field-parameter, + resource-not-exist, + str-format-used, + test-folder-imported, + translation-contains-variable, + translation-positional-used, + unnecessary-utf8-coding-comment, + website-manifest-key-not-valid-uri, + xml-attribute-translatable, + xml-deprecated-qweb-directive, + xml-deprecated-tree-attribute, + external-request-timeout + +[REPORTS] +msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} +output-format=colorized +reports=no diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..0240c75 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,30 @@ + +target-version = "py310" +fix = true + +[lint] +extend-select = [ + "B", + "C90", + "E501", # line too long (default 88) + "I", # isort + "UP", # pyupgrade +] +exclude = ["setup/*"] + +[format] +exclude = ["setup/*"] + +[per-file-ignores] +"__init__.py" = ["F401", "I001"] # ignore unused and unsorted imports in __init__.py +"__manifest__.py" = ["B018"] # useless expression + +[isort] +section-order = ["future", "standard-library", "third-party", "odoo", "odoo-addons", "first-party", "local-folder"] + +[isort.sections] +"odoo" = ["odoo"] +"odoo-addons" = ["odoo.addons"] + +[mccabe] +max-complexity = 16 diff --git a/LICENSE b/LICENSE index 9591157..be3f7b2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 - Copyright (C) 2007 Free Software Foundation, Inc. + Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. @@ -633,8 +633,8 @@ the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, @@ -643,7 +643,7 @@ the "copyright" line and a pointer to where the full notice is found. GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. @@ -658,5 +658,4 @@ specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see -. - +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e6dbd41 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ + + +[![Pre-commit Status](https://github.com/akretion/account-move-import/actions/workflows/pre-commit.yml/badge.svg?branch=16.0)](https://github.com/akretion/account-move-import/actions/workflows/pre-commit.yml?query=branch%3A16.0) +[![Build Status](https://github.com/akretion/account-move-import/actions/workflows/test.yml/badge.svg?branch=16.0)](https://github.com/akretion/account-move-import/actions/workflows/test.yml?query=branch%3A16.0) +[![codecov](https://codecov.io/gh/akretion/account-move-import/branch/16.0/graph/badge.svg)](https://codecov.io/gh/akretion/account-move-import) + + + + +# Account Move Import + + + + + + + +[//]: # (addons) + +This part will be replaced when running the oca-gen-addons-table script from OCA/maintainer-tools. + +[//]: # (end addons) + + + +## Licenses + +This repository is licensed under [AGPL-3.0](LICENSE). + +However, each module can have a totally different license, as long as they adhere to Akretion +policy. Consult each module's `__manifest__.py` file, which contains a `license` key +that explains its license. + +---- + diff --git a/account_move_csv_import/__manifest__.py b/account_move_csv_import/__manifest__.py index 5611da7..1bd015b 100644 --- a/account_move_csv_import/__manifest__.py +++ b/account_move_csv_import/__manifest__.py @@ -4,12 +4,12 @@ { - 'name': 'Account Move Import', - 'version': '16.0.1.0.0', - 'category': 'Accounting', - 'summary': 'Import account moves generated by external software', - 'license': 'AGPL-3', - 'description': """ + "name": "Account Move Import", + "version": "16.0.1.0.0", + "category": "Accounting", + "summary": "Import account moves generated by external software", + "license": "AGPL-3", + "description": """ Account Move Import =================== @@ -44,17 +44,17 @@ This module has been written by Alexis de Lattre from Akretion (alexis.delattre@akretion.com). """, - 'author': 'Akretion', - 'website': 'http://www.akretion.com', - 'depends': ['account'], - 'demo': ['demo/demo.xml'], + "author": "Akretion", + "website": "https://github.com/akretion/account-move-import", + "depends": ["account"], + "demo": ["demo/demo.xml"], # for the moment, I don't add the 'rows' lib in external_dependencies # I wait a new release on pypi https://github.com/turicas/rows/issues/368 - 'external_dependencies': {'python': ['openpyxl', 'xlrd']}, - 'data': [ - 'data/sequence.xml', - 'security/ir.model.access.csv', - 'wizard/account_move_import_view.xml', + "external_dependencies": {"python": ["openpyxl", "xlrd"]}, + "data": [ + "data/sequence.xml", + "security/ir.model.access.csv", + "wizard/account_move_import_view.xml", ], - 'installable': True, + "installable": True, } diff --git a/account_move_csv_import/data/sequence.xml b/account_move_csv_import/data/sequence.xml index 2acae20..adfc294 100644 --- a/account_move_csv_import/data/sequence.xml +++ b/account_move_csv_import/data/sequence.xml @@ -1,5 +1,4 @@ - - + @@ -7,7 +6,7 @@ account.move.import IMPORT 3 - + diff --git a/account_move_csv_import/demo/demo.xml b/account_move_csv_import/demo/demo.xml index 8f084e5..49e86a7 100644 --- a/account_move_csv_import/demo/demo.xml +++ b/account_move_csv_import/demo/demo.xml @@ -1,35 +1,34 @@ - + - Purchase PUR - + Support and operations SUPP - + - + Camembert en folie - + X1242 - + In tartiflette we trust - + X1243 diff --git a/account_move_csv_import/models/account_move_line.py b/account_move_csv_import/models/account_move_line.py index e5e0fa2..f8ff299 100644 --- a/account_move_csv_import/models/account_move_line.py +++ b/account_move_csv_import/models/account_move_line.py @@ -8,9 +8,10 @@ class AccountMoveLine(models.Model): _inherit = "account.move.line" - import_reconcile = fields.Char(string='Import Reconcile Ref') + import_reconcile = fields.Char(string="Import Reconcile Ref") import_external_id = fields.Char( string="Import External ID", - help='Can be used to tag imported journal items. ' - 'Can be useful to delete imported journal items in case of ' - 'error on the imported file.') + help="Can be used to tag imported journal items. " + "Can be useful to delete imported journal items in case of " + "error on the imported file.", + ) diff --git a/account_move_csv_import/wizard/account_move_import.py b/account_move_csv_import/wizard/account_move_import.py index 6152af7..671bd56 100644 --- a/account_move_csv_import/wizard/account_move_import.py +++ b/account_move_csv_import/wizard/account_move_import.py @@ -2,39 +2,41 @@ # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models, _ -from odoo.exceptions import UserError -from odoo.tools.mimetypes import guess_mimetype -from datetime import datetime, date as datelib -import csv -from tempfile import NamedTemporaryFile -from collections import OrderedDict import base64 +import csv import logging +from collections import OrderedDict +from datetime import date as datelib +from datetime import datetime +from tempfile import NamedTemporaryFile + +from odoo import _, api, fields, models +from odoo.exceptions import UserError +from odoo.tools.mimetypes import guess_mimetype logger = logging.getLogger(__name__) try: import openpyxl # for XLSX except ImportError: - logger.debug('Cannot import openpyxl') + logger.debug("Cannot import openpyxl") try: import xlrd # for XLS except ImportError: - logger.debug('Cannot import xlrd') + logger.debug("Cannot import xlrd") try: import rows # for ODS... but could be later used for XLS, XLSX, CSV # But they must make a new release first https://github.com/turicas/rows/issues/368 except ImportError: rows = None - logger.debug('Cannot import rows') + logger.debug("Cannot import rows") -GENERIC_CSV_DEFAULT_DATE = '%d/%m/%Y' +GENERIC_CSV_DEFAULT_DATE = "%d/%m/%Y" DELIMITER = { - 'coma': ',', - 'semicolon': ';', - 'tab': '\t', - } + "coma": ",", + "semicolon": ";", + "tab": "\t", +} class AccountMoveImport(models.TransientModel): @@ -43,77 +45,100 @@ class AccountMoveImport(models.TransientModel): _check_company_auto = True company_id = fields.Many2one( - 'res.company', string='Company', - required=True, default=lambda self: self.env.company) - file_to_import = fields.Binary(string='File to Import') + "res.company", + string="Company", + required=True, + default=lambda self: self.env.company, + ) + file_to_import = fields.Binary(string="File to Import") filename = fields.Char() - file_format = fields.Selection([ - ('genericxlsx', 'Generic XLSX/XLS/ODS'), - ('genericcsv', 'Generic CSV'), - ('fec_txt', 'FEC (text)'), - ('nibelis', 'Nibelis (Prisme)'), - ('quadra', 'Quadra (without analytic)'), - ('extenso', 'In Extenso'), - ('cielpaye', 'Ciel Paye'), - ('payfit', 'Payfit'), - ], string='File Format', required=True, default='genericxlsx') + file_format = fields.Selection( + [ + ("genericxlsx", "Generic XLSX/XLS/ODS"), + ("genericcsv", "Generic CSV"), + ("fec_txt", "FEC (text)"), + ("nibelis", "Nibelis (Prisme)"), + ("quadra", "Quadra (without analytic)"), + ("extenso", "In Extenso"), + ("cielpaye", "Ciel Paye"), + ("payfit", "Payfit"), + ], + string="File Format", + required=True, + default="genericxlsx", + ) post_move = fields.Boolean( - string='Post Journal Entry', - help="If True, the journal entry will be posted after the import.") + string="Post Journal Entry", + help="If True, the journal entry will be posted after the import.", + ) force_journal_id = fields.Many2one( - 'account.journal', string="Force Journal", - domain="[('company_id', '=', company_id)]", check_company=True, + "account.journal", + string="Force Journal", + domain="[('company_id', '=', company_id)]", + check_company=True, help="Journal in which the journal entry will be created, " - "even if the file indicate another journal.") - force_move_ref = fields.Char('Force Reference') - force_move_line_name = fields.Char('Force Label') - force_move_date = fields.Date('Force Date') - file_encoding = fields.Selection([ - ('ascii', 'ASCII'), - ('latin1', 'ISO 8859-15 (alias Latin1)'), - ('utf-8', 'UTF-8'), - ], string='File Encoding', default='utf-8') - delimiter = fields.Selection([ - ('coma', 'Coma'), - ('semicolon', 'Semicolon'), - ('tab', 'Tab'), - ], default='coma', string="Field Delimiter") + "even if the file indicate another journal.", + ) + force_move_ref = fields.Char("Force Reference") + force_move_line_name = fields.Char("Force Label") + force_move_date = fields.Date("Force Date") + file_encoding = fields.Selection( + [ + ("ascii", "ASCII"), + ("latin1", "ISO 8859-15 (alias Latin1)"), + ("utf-8", "UTF-8"), + ], + string="File Encoding", + default="utf-8", + ) + delimiter = fields.Selection( + [ + ("coma", "Coma"), + ("semicolon", "Semicolon"), + ("tab", "Tab"), + ], + default="coma", + string="Field Delimiter", + ) # technical fields - force_move_date_required = fields.Boolean(compute='_compute_force_required') - force_journal_required = fields.Boolean(compute='_compute_force_required') + force_move_date_required = fields.Boolean(compute="_compute_force_required") + force_journal_required = fields.Boolean(compute="_compute_force_required") advanced_options = fields.Boolean() # START GENERIC advanced options date_by_move_line = fields.Boolean( - string='Is date by move line ?', - help="If enabled, we don't use date to detect the split " - "of journal entries.") - skip_null_lines = fields.Boolean( - string="Skip lines with debit = credit = 0") + string="Is date by move line ?", + help="If enabled, we don't use date to detect the split " "of journal entries.", + ) + skip_null_lines = fields.Boolean(string="Skip lines with debit = credit = 0") keep_odoo_move_name = fields.Boolean( string="Don't Force Journal Entry Name", help="If 'move_name' is present in the pivot format and " "this option is enabled, it will ignore the value of 'move_name' " - "and use the sequence generated by Odoo when posting the journal entry.") - split_move_method = fields.Selection([ - ('balanced', 'Balanced'), - ('move_name', 'Journal Entry Number'), - ], default='balanced', required=True, - help="If you select the method 'Balanced', Odoo will cut the move when a group of lines is balanced with the same journal and date. If you select the method 'Journal Entry Number', Odoo will cut the move using the field 'move_name' of the pivot format (this field is optional, but it will have to be present if you select this method).") + "and use the sequence generated by Odoo when posting the journal entry.", + ) + split_move_method = fields.Selection( + [ + ("balanced", "Balanced"), + ("move_name", "Journal Entry Number"), + ], + default="balanced", + required=True, + help="If you select the method 'Balanced', Odoo will cut the move when a group of lines is balanced with the same journal and date. If you select the method 'Journal Entry Number', Odoo will cut the move using the field 'move_name' of the pivot format (this field is optional, but it will have to be present if you select this method).", + ) # START advanced options used in 'genericcsv' import # (but could be used by other imports if needed) - date_format = fields.Char( - default=GENERIC_CSV_DEFAULT_DATE, - required=True) + date_format = fields.Char(default=GENERIC_CSV_DEFAULT_DATE, required=True) file_with_header = fields.Boolean( - string='Has Header Line', - help="Indicate if the first line is a header line and should be ignored.") + string="Has Header Line", + help="Indicate if the first line is a header line and should be ignored.", + ) - @api.depends('file_format') + @api.depends("file_format") def _compute_force_required(self): for wiz in self: force_move_date_required = False force_journal_required = False - if wiz.file_format == 'payfit': + if wiz.file_format == "payfit": force_move_date_required = True force_journal_required = True wiz.force_move_date_required = force_move_date_required @@ -127,10 +152,11 @@ def button_hide_advanced_options(self): def _set_advanced_options(self, advanced_options): self.ensure_one() - self.write({'advanced_options': advanced_options}) + self.write({"advanced_options": advanced_options}) action = self.env["ir.actions.actions"]._for_xml_id( - "account_move_csv_import.account_move_import_action") - action['res_id'] = self.id + "account_move_csv_import.account_move_import_action" + ) + action["res_id"] = self.id return action # PIVOT FORMAT @@ -159,21 +185,21 @@ def _set_advanced_options(self, advanced_options): def file2pivot(self, fileobj, file_bytes): file_format = self.file_format - if file_format == 'nibelis': + if file_format == "nibelis": return self.nibelis2pivot(fileobj) - elif file_format == 'genericcsv': + elif file_format == "genericcsv": return self.genericcsv2pivot(fileobj) - elif file_format == 'genericxlsx': + elif file_format == "genericxlsx": return self.genericxlsx_autodetect(fileobj, file_bytes) - elif file_format == 'quadra': + elif file_format == "quadra": return self.quadra2pivot(file_bytes) - elif file_format == 'extenso': + elif file_format == "extenso": return self.extenso2pivot(fileobj) - elif file_format == 'payfit': + elif file_format == "payfit": return self.payfit2pivot(fileobj) - elif file_format == 'cielpaye': + elif file_format == "cielpaye": return self.cielpaye2pivot(fileobj) - elif file_format == 'fec_txt': + elif file_format == "fec_txt": return self.fectxt2pivot(fileobj) else: raise UserError(_("You must select a file format.")) @@ -182,33 +208,38 @@ def run_import(self): self.ensure_one() if not self.file_to_import: raise UserError(_("You must upload a file to import.")) - fileobj = NamedTemporaryFile('wb+', prefix='odoo-move_import-', suffix='.xlsx') + fileobj = NamedTemporaryFile("wb+", prefix="odoo-move_import-", suffix=".xlsx") file_bytes = base64.b64decode(self.file_to_import) fileobj.write(file_bytes) fileobj.seek(0) # We must start reading from the beginning ! pivot = self.file2pivot(fileobj, file_bytes) fileobj.close() - logger.debug('pivot before update: %s', pivot) + logger.debug("pivot before update: %s", pivot) self.clean_strip_pivot(pivot) self.update_pivot(pivot) moves = self.create_moves_from_pivot(pivot, post=self.post_move) self.reconcile_move_lines(moves) action = self.env["ir.actions.actions"]._for_xml_id( - "account.action_move_journal_line") + "account.action_move_journal_line" + ) # We need to remove from context 'search_default_posted': 1 - action['context'] = {'default_move_type': 'entry', 'view_no_maturity': True} + action["context"] = {"default_move_type": "entry", "view_no_maturity": True} if len(moves) == 1: - action.update({ - 'view_mode': 'form,tree', - 'res_id': moves[0].id, - 'view_id': False, - 'views': False, - }) + action.update( + { + "view_mode": "form,tree", + "res_id": moves[0].id, + "view_id": False, + "views": False, + } + ) else: - action.update({ - 'view_mode': 'tree,form', - 'domain': [('id', 'in', moves.ids)], - }) + action.update( + { + "view_mode": "tree,form", + "domain": [("id", "in", moves.ids)], + } + ) return action def clean_strip_pivot(self, pivot): @@ -224,128 +255,142 @@ def update_pivot(self, pivot): force_move_date = self.force_move_date force_move_ref = self.force_move_ref force_move_line_name = self.force_move_line_name - force_journal_code =\ + force_journal_code = ( self.force_journal_id and self.force_journal_id.code or False + ) for l in pivot: if force_move_date: - l['date'] = force_move_date + l["date"] = force_move_date if force_move_line_name: - l['name'] = force_move_line_name + l["name"] = force_move_line_name if force_move_ref: - l['ref'] = force_move_ref + l["ref"] = force_move_ref if force_journal_code: - l['journal'] = force_journal_code - if not l['credit']: - l['credit'] = 0.0 - if not l['debit']: - l['debit'] = 0.0 + l["journal"] = force_journal_code + if not l["credit"]: + l["credit"] = 0.0 + if not l["debit"]: + l["debit"] = 0.0 def extenso2pivot(self, fileobj): fieldnames = [ - 'journal', 'date', False, 'account', False, False, False, False, - 'debit', 'credit'] + "journal", + "date", + False, + "account", + False, + False, + False, + False, + "debit", + "credit", + ] res = [] - with open(fileobj.name, newline='', encoding='utf-8') as f: + with open(fileobj.name, newline="", encoding="utf-8") as f: reader = csv.DictReader( - f, - fieldnames=fieldnames, - delimiter='\t', - quoting=csv.QUOTE_MINIMAL) + f, fieldnames=fieldnames, delimiter="\t", quoting=csv.QUOTE_MINIMAL + ) i = 0 for l in reader: i += 1 - l['credit'] = l['credit'] or '0' - l['debit'] = l['debit'] or '0' + l["credit"] = l["credit"] or "0" + l["debit"] = l["debit"] or "0" vals = { - 'journal': l['journal'], - 'account': l['account'], - 'credit': float(l['credit'].replace(',', '.')), - 'debit': float(l['debit'].replace(',', '.')), - 'date': datetime.strptime(l['date'], '%d%m%Y'), - 'line': i, + "journal": l["journal"], + "account": l["account"], + "credit": float(l["credit"].replace(",", ".")), + "debit": float(l["debit"].replace(",", ".")), + "date": datetime.strptime(l["date"], "%d%m%Y"), + "line": i, } res.append(vals) return res def cielpaye2pivot(self, fileobj): fieldnames = [ - False, 'journal', 'date', 'account', False, 'amount', 'sign', - False, 'name', False] + False, + "journal", + "date", + "account", + False, + "amount", + "sign", + False, + "name", + False, + ] res = [] - with open(fileobj.name, newline='', encoding='utf-8') as f: + with open(fileobj.name, newline="", encoding="utf-8") as f: reader = csv.DictReader( - f, - fieldnames=fieldnames, - delimiter='\t', - quoting=csv.QUOTE_MINIMAL) + f, fieldnames=fieldnames, delimiter="\t", quoting=csv.QUOTE_MINIMAL + ) i = 0 for l in reader: i += 1 # skip non-move lines - if l.get('date') and l.get('name') and l.get('amount'): - amount = float(l['amount'].replace(',', '.')) + if l.get("date") and l.get("name") and l.get("amount"): + amount = float(l["amount"].replace(",", ".")) vals = { - 'journal': l['journal'], - 'account': l['account'], - 'credit': l['sign'] == 'C' and amount or 0, - 'debit': l['sign'] == 'D' and amount or 0, - 'date': datetime.strptime(l['date'], '%d/%m/%Y'), - 'name': l['name'], - 'line': i, + "journal": l["journal"], + "account": l["account"], + "credit": l["sign"] == "C" and amount or 0, + "debit": l["sign"] == "D" and amount or 0, + "date": datetime.strptime(l["date"], "%d/%m/%Y"), + "name": l["name"], + "line": i, } res.append(vals) return res def fectxt2pivot(self, fileobj): fieldnames = [ - 'journal', # JournalCode - False, # JournalLib - 'move_name', # EcritureNum - 'date', # EcritureDate - 'account', # CompteNum - False, # CompteLib - 'partner_ref', # CompAuxNum - False, # CompAuxLib - 'ref', # PieceRef - False, # PieceDate - 'name', # EcritureLib - 'debit', # Debit - 'credit', # Credit - 'reconcile_ref', # EcritureLet - False, # DateLet - False, # ValidDate - False, # Montantdevise - False, # Idevise - ] + "journal", # JournalCode + False, # JournalLib + "move_name", # EcritureNum + "date", # EcritureDate + "account", # CompteNum + False, # CompteLib + "partner_ref", # CompAuxNum + False, # CompAuxLib + "ref", # PieceRef + False, # PieceDate + "name", # EcritureLib + "debit", # Debit + "credit", # Credit + "reconcile_ref", # EcritureLet + False, # DateLet + False, # ValidDate + False, # Montantdevise + False, # Idevise + ] res = [] first_line = fileobj.readline().decode() dialect = csv.Sniffer().sniff(first_line, delimiters="|\t") fileobj.seek(0) - with open(fileobj.name, newline='', encoding=self.file_encoding) as f: + with open(fileobj.name, newline="", encoding=self.file_encoding) as f: reader = csv.DictReader( - f, - fieldnames=fieldnames, - delimiter=dialect.delimiter) + f, fieldnames=fieldnames, delimiter=dialect.delimiter + ) i = 0 for l in reader: i += 1 # Skip header line if i == 1: continue - l['credit'] = l['credit'] or '0' - l['debit'] = l['debit'] or '0' + l["credit"] = l["credit"] or "0" + l["debit"] = l["debit"] or "0" vals = { - 'journal': l['journal'], - 'move_name': l['move_name'], - 'account': l['account'], - 'partner': l['partner_ref'], - 'credit': float(l['credit'].replace(',', '.')), - 'debit': float(l['debit'].replace(',', '.')), - 'date': datetime.strptime(l['date'], '%Y%m%d'), - 'name': l['name'], - 'ref': l['ref'], - 'reconcile_ref': l['reconcile_ref'], - 'line': i, + "journal": l["journal"], + "move_name": l["move_name"], + "account": l["account"], + "partner": l["partner_ref"], + "credit": float(l["credit"].replace(",", ".")), + "debit": float(l["debit"].replace(",", ".")), + "date": datetime.strptime(l["date"], "%Y%m%d"), + "name": l["name"], + "ref": l["ref"], + "reconcile_ref": l["reconcile_ref"], + "line": i, } res.append(vals) return res @@ -353,59 +398,74 @@ def fectxt2pivot(self, fileobj): def genericcsv2pivot(self, fileobj): # Prisme fieldnames = [ - 'date', 'journal', 'account', 'partner', - 'analytic', 'name', 'debit', 'credit', - 'ref', 'reconcile_ref' - ] + "date", + "journal", + "account", + "partner", + "analytic", + "name", + "debit", + "credit", + "ref", + "reconcile_ref", + ] # I use utf-8-sig instead of utf-8 to transparently handle BOM # https://en.wikipedia.org/wiki/Byte_order_mark - encoding = self.file_encoding == 'utf-8' and 'utf-8-sig' or self.file_encoding + encoding = self.file_encoding == "utf-8" and "utf-8-sig" or self.file_encoding res = [] - with open(fileobj.name, newline='', encoding=encoding) as f: + with open(fileobj.name, newline="", encoding=encoding) as f: reader = csv.DictReader( f, fieldnames=fieldnames, delimiter=DELIMITER[self.delimiter], quotechar='"', - quoting=csv.QUOTE_MINIMAL) + quoting=csv.QUOTE_MINIMAL, + ) i = 0 for l in reader: i += 1 if i == 1 and self.file_with_header: continue - date_str = l['date'] + date_str = l["date"] try: date = datetime.strptime(date_str, self.date_format) except Exception: - raise UserError(_( - "Date parsing error: '%s' in line %s does not match " - "date format '%s'.") % (date_str, i, self.date_format)) + raise UserError( + _( + "Date parsing error: '%s' in line %s does not match " + "date format '%s'." + ) + % (date_str, i, self.date_format) + ) vals = { - 'journal': l['journal'], - 'account': l['account'], - 'credit': float(l['credit'].replace(',', '.') or 0), - 'debit': float(l['debit'].replace(',', '.') or 0), - 'date': date, - 'name': l['name'], - 'ref': l.get('ref', ''), - 'reconcile_ref': l.get('reconcile_ref', ''), - 'line': i, - } - if l['analytic']: - vals['analytic'] = l['analytic'] - if l['partner']: - vals['partner'] = l['partner'] + "journal": l["journal"], + "account": l["account"], + "credit": float(l["credit"].replace(",", ".") or 0), + "debit": float(l["debit"].replace(",", ".") or 0), + "date": date, + "name": l["name"], + "ref": l.get("ref", ""), + "reconcile_ref": l.get("reconcile_ref", ""), + "line": i, + } + if l["analytic"]: + vals["analytic"] = l["analytic"] + if l["partner"]: + vals["partner"] = l["partner"] res.append(vals) return res def genericxlsx_autodetect(self, fileobj, file_bytes): mime_res = guess_mimetype(file_bytes) - if mime_res == 'application/vnd.oasis.opendocument.spreadsheet': # ODS + if mime_res == "application/vnd.oasis.opendocument.spreadsheet": # ODS return self.genericods2pivot(fileobj) - elif mime_res == 'application/vnd.ms-excel': # XLS + elif mime_res == "application/vnd.ms-excel": # XLS return self.genericxls2pivot(fileobj) - elif mime_res == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': # XLSX + elif ( + mime_res + == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ): # XLSX return self.genericxlsx2pivot(fileobj) else: raise UserError(_("Are you sure this file is an XLSX, XLS or ODS file?")) @@ -425,18 +485,18 @@ def genericxlsx2pivot(self, fileobj): # skip empty line continue vals = { - 'date': row[0].value, - 'journal': row[1].value, - 'account': str(row[2].value), - 'partner': row[3].value or False, - 'analytic': row[4].value or False, - 'name': row[5].value, - 'debit': row[6].value, - 'credit': row[7].value, - 'ref': len(row) > 8 and row[8].value or '', - 'reconcile_ref': len(row) > 9 and row[9].value or '', - 'line': i, - } + "date": row[0].value, + "journal": row[1].value, + "account": str(row[2].value), + "partner": row[3].value or False, + "analytic": row[4].value or False, + "name": row[5].value, + "debit": row[6].value, + "credit": row[7].value, + "ref": len(row) > 8 and row[8].value or "", + "reconcile_ref": len(row) > 9 and row[9].value or "", + "line": i, + } res.append(vals) return res @@ -461,42 +521,50 @@ def genericxls2pivot(self, fileobj): elif isinstance(account, int): account = str(account) vals = { - 'date': datetime(*xlrd.xldate_as_tuple(row[0].value, wb.datemode)), - 'journal': row[1].value, - 'account': account, - 'partner': row[3].value or False, - 'analytic': row[4].value or False, - 'name': row[5].value, - 'debit': row[6].value, - 'credit': row[7].value, - 'ref': len(row) > 8 and row[8].value or '', - 'reconcile_ref': len(row) > 9 and row[9].value or '', - 'line': i, - } + "date": datetime(*xlrd.xldate_as_tuple(row[0].value, wb.datemode)), + "journal": row[1].value, + "account": account, + "partner": row[3].value or False, + "analytic": row[4].value or False, + "name": row[5].value, + "debit": row[6].value, + "credit": row[7].value, + "ref": len(row) > 8 and row[8].value or "", + "reconcile_ref": len(row) > 9 and row[9].value or "", + "line": i, + } res.append(vals) return res def genericods2pivot(self, fileobj): if rows is None: - raise UserError(_( - "To import ods files, you must install the rows lib from " - "https://github.com/turicas/rows")) - if rows.__version__ <= '0.4.1': - raise UserError(_( - "Python lib 'rows' 0.4.1 is buggy. " - "You should checkout the code from https://github.com/turicas/rows")) - fields_ods = OrderedDict([ - ('date', rows.fields.DateField), - ('journal', rows.fields.TextField), - ('account', rows.fields.TextField), - ('partner', rows.fields.TextField), - ('analytic', rows.fields.TextField), - ('name', rows.fields.TextField), - ('debit', rows.fields.FloatField), - ('credit', rows.fields.FloatField), - ('ref', rows.fields.TextField), - ('reconcile_ref', rows.fields.TextField), - ]) + raise UserError( + _( + "To import ods files, you must install the rows lib from " + "https://github.com/turicas/rows" + ) + ) + if rows.__version__ <= "0.4.1": + raise UserError( + _( + "Python lib 'rows' 0.4.1 is buggy. " + "You should checkout the code from https://github.com/turicas/rows" + ) + ) + fields_ods = OrderedDict( + [ + ("date", rows.fields.DateField), + ("journal", rows.fields.TextField), + ("account", rows.fields.TextField), + ("partner", rows.fields.TextField), + ("analytic", rows.fields.TextField), + ("name", rows.fields.TextField), + ("debit", rows.fields.FloatField), + ("credit", rows.fields.FloatField), + ("ref", rows.fields.TextField), + ("reconcile_ref", rows.fields.TextField), + ] + ) sh = rows.import_from_ods(fileobj.name, fields=fields_ods, skip_header=False) @@ -508,56 +576,80 @@ def genericods2pivot(self, fileobj): if i == 1 and self.file_with_header: continue vals = { - 'date': row.date, - 'journal': row.journal, - 'account': row.account, - 'partner': row.partner, - 'analytic': row.analytic, - 'name': row.name, - 'debit': row.debit, - 'credit': row.credit, - 'ref': row.ref, - 'reconcile_ref': row.reconcile_ref, - 'line': i, - } + "date": row.date, + "journal": row.journal, + "account": row.account, + "partner": row.partner, + "analytic": row.analytic, + "name": row.name, + "debit": row.debit, + "credit": row.credit, + "ref": row.ref, + "reconcile_ref": row.reconcile_ref, + "line": i, + } res.append(vals) return res def nibelis2pivot(self, fileobj): fieldnames = [ - 'trasha', 'trashb', 'journal', 'trashd', 'trashe', - 'trashf', 'trashg', 'date', 'trashi', 'trashj', 'trashk', - 'trashl', 'trashm', 'trashn', 'account', 'trashp', - 'trashq', 'amount', 'trashs', 'sign', 'trashu', - 'trashv', 'name', - 'trashx', 'trashy', 'trashz', 'trashaa', 'trashab', - 'trashac', 'trashad', 'trashae', 'analytic'] + "trasha", + "trashb", + "journal", + "trashd", + "trashe", + "trashf", + "trashg", + "date", + "trashi", + "trashj", + "trashk", + "trashl", + "trashm", + "trashn", + "account", + "trashp", + "trashq", + "amount", + "trashs", + "sign", + "trashu", + "trashv", + "name", + "trashx", + "trashy", + "trashz", + "trashaa", + "trashab", + "trashac", + "trashad", + "trashae", + "analytic", + ] res = [] - with open(fileobj.name, newline='', encoding='latin1') as f: + with open(fileobj.name, newline="", encoding="latin1") as f: reader = csv.DictReader( - f, - fieldnames=fieldnames, - delimiter=';', - quoting=csv.QUOTE_MINIMAL) + f, fieldnames=fieldnames, delimiter=";", quoting=csv.QUOTE_MINIMAL + ) i = 0 for l in reader: i += 1 if i == 1: continue - amount = float(l['amount'].replace(',', '.')) - credit = l['sign'] == 'C' and amount or False - debit = l['sign'] == 'D' and amount or False + amount = float(l["amount"].replace(",", ".")) + credit = l["sign"] == "C" and amount or False + debit = l["sign"] == "D" and amount or False vals = { - 'journal': l['journal'], - 'account': l['account'], - 'credit': credit, - 'debit': debit, - 'date': datetime.strptime(l['date'], '%y%m%d'), - 'name': l['name'], - 'line': i, + "journal": l["journal"], + "account": l["account"], + "credit": credit, + "debit": debit, + "date": datetime.strptime(l["date"], "%y%m%d"), + "name": l["name"], + "line": i, } - if l.get('analytic'): - vals['analytic'] = l['analytic'] + if l.get("analytic"): + vals["analytic"] = l["analytic"] res.append(vals) return res @@ -565,21 +657,21 @@ def quadra2pivot(self, file_bytes): i = 0 res = [] file_str = file_bytes.decode(self.file_encoding) - for l in file_str.split('\n'): + for l in file_str.split("\n"): i += 1 if len(l) < 54: continue - if l[0] == 'M' and l[41] in ('C', 'D'): + if l[0] == "M" and l[41] in ("C", "D"): amount_cents = int(l[42:55]) amount = amount_cents / 100.0 vals = { - 'journal': l[9:11], - 'account': l[1:9], - 'credit': l[41] == 'C' and amount or False, - 'debit': l[41] == 'D' and amount or False, - 'date': datetime.strptime(l[14:20], '%d%m%y'), - 'name': l[21:41], - 'line': i, + "journal": l[9:11], + "account": l[1:9], + "credit": l[41] == "C" and amount or False, + "debit": l[41] == "D" and amount or False, + "date": datetime.strptime(l[14:20], "%d%m%y"), + "name": l[21:41], + "line": i, } res.append(vals) return res @@ -616,17 +708,18 @@ def payfit2pivot(self, fileobj): def _prepare_partner_speeddict(self, company_id): speeddict = {} - partner_sr = self.env['res.partner'].search_read( + partner_sr = self.env["res.partner"].search_read( [ - '|', - ('company_id', '=', company_id), - ('company_id', '=', False), - ('ref', '!=', False), - ('parent_id', '=', False), + "|", + ("company_id", "=", company_id), + ("company_id", "=", False), + ("ref", "!=", False), + ("parent_id", "=", False), ], - ['ref']) + ["ref"], + ) for l in partner_sr: - speeddict[l['ref'].upper()] = l['id'] + speeddict[l["ref"].upper()] = l["id"] return speeddict def _prepare_speeddict(self, company_id): @@ -635,127 +728,146 @@ def _prepare_speeddict(self, company_id): "journal": {}, "account": {}, "analytic": {}, - } - acc_sr = self.env['account.account'].search_read([ - ('company_id', '=', company_id), - ('deprecated', '=', False)], ['code']) + } + acc_sr = self.env["account.account"].search_read( + [("company_id", "=", company_id), ("deprecated", "=", False)], ["code"] + ) for l in acc_sr: - speeddict['account'][l['code'].upper()] = l['id'] - aacc_sr = self.env['account.analytic.account'].search_read( - [('company_id', '=', company_id), ('code', '!=', False)], - ['code']) + speeddict["account"][l["code"].upper()] = l["id"] + aacc_sr = self.env["account.analytic.account"].search_read( + [("company_id", "=", company_id), ("code", "!=", False)], ["code"] + ) for l in aacc_sr: - speeddict['analytic'][l['code'].upper()] = l['id'] - journal_sr = self.env['account.journal'].search_read([ - ('company_id', '=', company_id)], ['code']) + speeddict["analytic"][l["code"].upper()] = l["id"] + journal_sr = self.env["account.journal"].search_read( + [("company_id", "=", company_id)], ["code"] + ) for l in journal_sr: - speeddict['journal'][l['code'].upper()] = l['id'] + speeddict["journal"][l["code"].upper()] = l["id"] return speeddict def create_moves_from_pivot(self, pivot, post=False): - logger.debug('Final pivot: %s', pivot) - amo = self.env['account.move'] + logger.debug("Final pivot: %s", pivot) + amo = self.env["account.move"] company_id = self.company_id.id speeddict = self._prepare_speeddict(company_id) key2label = { - 'journal': _('journal codes'), - 'account': _('account codes'), - 'partner': _('partner reference'), - 'analytic': _('analytic codes'), - } - errors = {'other': []} + "journal": _("journal codes"), + "account": _("account codes"), + "partner": _("partner reference"), + "analytic": _("analytic codes"), + } + errors = {"other": []} for key in key2label.keys(): errors[key] = {} # MATCHES + CHECKS for l in pivot: - assert l.get('line') and isinstance(l.get('line'), int), \ - 'missing line number' - if l['account'] in speeddict['account']: - l['account_id'] = speeddict['account'][l['account']] - if not l.get('account_id'): + assert l.get("line") and isinstance( + l.get("line"), int + ), "missing line number" + if l["account"] in speeddict["account"]: + l["account_id"] = speeddict["account"][l["account"]] + if not l.get("account_id"): # Match when import = 61100000 and Odoo has 611000 - acc_code_tmp = l['account'] - while acc_code_tmp and acc_code_tmp[-1] == '0': + acc_code_tmp = l["account"] + while acc_code_tmp and acc_code_tmp[-1] == "0": acc_code_tmp = acc_code_tmp[:-1] - if acc_code_tmp and acc_code_tmp in speeddict['account']: - l['account_id'] = speeddict['account'][acc_code_tmp] + if acc_code_tmp and acc_code_tmp in speeddict["account"]: + l["account_id"] = speeddict["account"][acc_code_tmp] break - if not l.get('account_id'): + if not l.get("account_id"): # Match when import = 611000 and Odoo has 611000XX - for code, account_id in speeddict['account'].items(): - if code.startswith(l['account']): + for code, account_id in speeddict["account"].items(): + if code.startswith(l["account"]): logger.warning( "Approximate match: import account %s has been matched " - "with Odoo account %s" % (l['account'], code)) - l['account_id'] = account_id + "with Odoo account %s" % (l["account"], code) + ) + l["account_id"] = account_id break - if not l.get('account_id'): - errors['account'].setdefault(l['account'], []).append(l['line']) - if l.get('partner'): - if l['partner'] in speeddict['partner']: - l['partner_id'] = speeddict['partner'][l['partner']] + if not l.get("account_id"): + errors["account"].setdefault(l["account"], []).append(l["line"]) + if l.get("partner"): + if l["partner"] in speeddict["partner"]: + l["partner_id"] = speeddict["partner"][l["partner"]] else: - errors['partner'].setdefault(l['partner'], []).append(l['line']) - if l.get('analytic'): - l['analytic_distribution'] = {} - for ana_entry in l['analytic'].split('|'): + errors["partner"].setdefault(l["partner"], []).append(l["line"]) + if l.get("analytic"): + l["analytic_distribution"] = {} + for ana_entry in l["analytic"].split("|"): ana_entry = ana_entry.strip() if ana_entry: - ana_entry_split = ana_entry.split(':') + ana_entry_split = ana_entry.split(":") if len(ana_entry_split) == 1: ana_account_code = ana_entry_split[0].strip() ana_pct = 100 elif len(ana_entry_split) > 1: - ana_account_code = ':'.join(ana_entry_split[:-1]).strip() + ana_account_code = ":".join(ana_entry_split[:-1]).strip() ana_pct_str = ana_entry_split[-1] - ana_pct_str_ready = ana_pct_str.replace(',', '.') + ana_pct_str_ready = ana_pct_str.replace(",", ".") try: ana_pct = float(ana_pct_str_ready) except Exception: - errors['other'].append("Line %d: wrong analytic percentage: '%s' is not a number." % (l['line'], ana_pct_str)) + errors["other"].append( + "Line %d: wrong analytic percentage: '%s' is not a number." + % (l["line"], ana_pct_str) + ) ana_pct = 1 if ana_pct < 0 or ana_pct > 100: - errors['other'].append("Line %d: wrong analytic percentage: '%s' is not between 0 and 100." % (l['line'], ana_pct_str)) - if ana_account_code in speeddict['analytic']: - l['analytic_distribution'][speeddict['analytic'][ana_account_code]] = ana_pct + errors["other"].append( + "Line %d: wrong analytic percentage: '%s' is not between 0 and 100." + % (l["line"], ana_pct_str) + ) + if ana_account_code in speeddict["analytic"]: + l["analytic_distribution"][ + speeddict["analytic"][ana_account_code] + ] = ana_pct else: - errors['analytic'].setdefault(ana_account_code, []).append(l['line']) + errors["analytic"].setdefault(ana_account_code, []).append( + l["line"] + ) - if l['journal'] in speeddict['journal']: - l['journal_id'] = speeddict['journal'][l['journal']] + if l["journal"] in speeddict["journal"]: + l["journal_id"] = speeddict["journal"][l["journal"]] else: - errors['journal'].setdefault(l['journal'], []).append(l['line']) - if not l.get('date'): - errors['other'].append(_( - 'Line %d: missing date.') % l['line']) + errors["journal"].setdefault(l["journal"], []).append(l["line"]) + if not l.get("date"): + errors["other"].append(_("Line %d: missing date.") % l["line"]) else: - if not isinstance(l.get('date'), datelib): + if not isinstance(l.get("date"), datelib): try: - l['date'] = datetime.strptime(l['date'], '%Y-%m-%d') + l["date"] = datetime.strptime(l["date"], "%Y-%m-%d") except Exception: - errors['other'].append(_( - 'Line %d: bad date format %s') % (l['line'], l['date'])) - if not isinstance(l.get('credit'), (float, int)): - errors['other'].append(_( - 'Line %d: bad value for credit (%s).') - % (l['line'], l['credit'])) - if not isinstance(l.get('debit'), (float, int)): - errors['other'].append(_( - 'Line %d: bad value for debit (%s).') - % (l['line'], l['debit'])) + errors["other"].append( + _("Line %d: bad date format %s") % (l["line"], l["date"]) + ) + if not isinstance(l.get("credit"), (float, int)): + errors["other"].append( + _("Line %d: bad value for credit (%s).") % (l["line"], l["credit"]) + ) + if not isinstance(l.get("debit"), (float, int)): + errors["other"].append( + _("Line %d: bad value for debit (%s).") % (l["line"], l["debit"]) + ) # test that they don't have both a value # LIST OF ERRORS - msg = '' + msg = "" for key, label in key2label.items(): if errors[key]: msg += _("List of %s that don't exist in Odoo:\n%s\n\n") % ( label, - '\n'.join([ - '- %s : line(s) %s' % (code, ', '.join([str(i) for i in lines])) - for (code, lines) in errors[key].items()])) - if errors['other']: - msg += _('List of misc errors:\n%s') % ( - '\n'.join(['- %s' % e for e in errors['other']])) + "\n".join( + [ + "- %s : line(s) %s" + % (code, ", ".join([str(i) for i in lines])) + for (code, lines) in errors[key].items() + ] + ), + ) + if errors["other"]: + msg += _("List of misc errors:\n%s") % ( + "\n".join(["- %s" % e for e in errors["other"]]) + ) if msg: raise UserError(msg) # EXTRACT MOVES @@ -767,90 +879,102 @@ def create_moves_from_pivot(self, pivot, post=False): cur_date = False cur_balance = 0.0 comp_cur = self.company_id.currency_id - seq = self.env['ir.sequence'].next_by_code('account.move.import') + seq = self.env["ir.sequence"].next_by_code("account.move.import") cur_move = {} for l in pivot: if ( - skip_null_lines and - comp_cur.is_zero(l['credit']) and - comp_cur.is_zero(l['debit'])): - logger.info('Skip line %d which has debit=credit=0', l['line']) + skip_null_lines + and comp_cur.is_zero(l["credit"]) + and comp_cur.is_zero(l["debit"]) + ): + logger.info("Skip line %d which has debit=credit=0", l["line"]) continue - move_name = l.get('move_name') - if split_move_method == 'move_name': + move_name = l.get("move_name") + if split_move_method == "move_name": if not move_name: - errors['other'].append(_( - 'Line %d: missing journal entry number.') % l['line']) + errors["other"].append( + _("Line %d: missing journal entry number.") % l["line"] + ) same_move = [cur_move_name == move_name] - elif split_move_method == 'balanced': + elif split_move_method == "balanced": same_move = [ - cur_journal_id == l['journal_id'], - not comp_cur.is_zero(cur_balance)] + cur_journal_id == l["journal_id"], + not comp_cur.is_zero(cur_balance), + ] if not self.date_by_move_line: - same_move.append(cur_date == l['date']) + same_move.append(cur_date == l["date"]) else: raise UserError(_("Wrong Move Split Method.")) if all(same_move): # append to current move - cur_move['line_ids'].append((0, 0, self._prepare_move_line(l, seq))) + cur_move["line_ids"].append((0, 0, self._prepare_move_line(l, seq))) else: # new move if cur_move: - if len(cur_move['line_ids']) <= 1: - raise UserError(_( - "Journal entry on line %d only has 1 line.\n\n" - "Debug data: %s") % (l['line'], cur_move['line_ids'])) + if len(cur_move["line_ids"]) <= 1: + raise UserError( + _( + "Journal entry on line %d only has 1 line.\n\n" + "Debug data: %s" + ) + % (l["line"], cur_move["line_ids"]) + ) moves.append(cur_move) cur_move = self._prepare_move(l) - cur_move['line_ids'] = [(0, 0, self._prepare_move_line(l, seq))] - cur_date = l['date'] + cur_move["line_ids"] = [(0, 0, self._prepare_move_line(l, seq))] + cur_date = l["date"] cur_move_name = move_name - cur_journal_id = l['journal_id'] + cur_journal_id = l["journal_id"] cur_balance = 0.0 - cur_balance += l['credit'] - l['debit'] + cur_balance += l["credit"] - l["debit"] if cur_move: moves.append(cur_move) if not comp_cur.is_zero(cur_balance): - raise UserError(_( - "The journal entry that ends on the last line is not " - "balanced (balance is %s).") % cur_balance) - rmoves = self.env['account.move'] + raise UserError( + _( + "The journal entry that ends on the last line is not " + "balanced (balance is %s)." + ) + % cur_balance + ) + rmoves = self.env["account.move"] for move in moves: rmoves += amo.create(move) - logger.info( - 'Account moves IDs %s created via file import' % rmoves.ids) + logger.info("Account moves IDs %s created via file import" % rmoves.ids) if post: rmoves.action_post() return rmoves def _prepare_move(self, pivot_line): vals = { - 'journal_id': pivot_line['journal_id'], - 'ref': pivot_line.get('ref'), - 'date': pivot_line['date'], - } - if pivot_line.get('move_name') and not self.keep_odoo_move_name: - vals['name'] = pivot_line['move_name'] + "journal_id": pivot_line["journal_id"], + "ref": pivot_line.get("ref"), + "date": pivot_line["date"], + } + if pivot_line.get("move_name") and not self.keep_odoo_move_name: + vals["name"] = pivot_line["move_name"] return vals def _prepare_move_line(self, pivot_line, sequence): vals = { - 'credit': pivot_line['credit'], - 'debit': pivot_line['debit'], - 'name': pivot_line['name'], - 'partner_id': pivot_line.get('partner_id'), - 'account_id': pivot_line['account_id'], - 'analytic_distribution': pivot_line.get('analytic_distribution'), - 'import_reconcile': pivot_line.get('reconcile_ref'), - 'import_external_id': '%s-%s' % (sequence, pivot_line.get('line')), - } + "credit": pivot_line["credit"], + "debit": pivot_line["debit"], + "name": pivot_line["name"], + "partner_id": pivot_line.get("partner_id"), + "account_id": pivot_line["account_id"], + "analytic_distribution": pivot_line.get("analytic_distribution"), + "import_reconcile": pivot_line.get("reconcile_ref"), + "import_external_id": "%s-%s" % (sequence, pivot_line.get("line")), + } return vals def reconcile_move_lines(self, moves): comp_cur = self.company_id.currency_id - logger.info('Start to reconcile imported moves') - lines = self.env['account.move.line'].search([ - ('move_id', 'in', moves.ids), - ('import_reconcile', '!=', False), - ]) + logger.info("Start to reconcile imported moves") + lines = self.env["account.move.line"].search( + [ + ("move_id", "in", moves.ids), + ("import_reconcile", "!=", False), + ] + ) torec = {} # key = reconcile mark, value = movelines_recordset for line in lines: if line.import_reconcile in torec: @@ -861,7 +985,9 @@ def reconcile_move_lines(self, moves): if len(lines_to_rec) < 2: logger.warning( "Skip reconcile of ref '%s' because " - "this ref is only on 1 move line", rec_ref) + "this ref is only on 1 move line", + rec_ref, + ) continue total = 0.0 accounts = {} @@ -874,25 +1000,34 @@ def reconcile_move_lines(self, moves): if not comp_cur.is_zero(total): logger.warning( "Skip reconcile of ref '%s' because the lines with " - "this ref are not balanced (%s)", rec_ref, total) + "this ref are not balanced (%s)", + rec_ref, + total, + ) continue if len(accounts) > 1: logger.warning( "Skip reconcile of ref '%s' because the lines with " "this ref have different accounts (%s)", - rec_ref, ', '.join([acc.code for acc in accounts.keys()])) + rec_ref, + ", ".join([acc.code for acc in accounts.keys()]), + ) continue if not list(accounts)[0].reconcile: logger.warning( "Skip reconcile of ref '%s' because the account '%s' " "is not configured with 'Allow Reconciliation'", - rec_ref, list(accounts)[0].display_name) + rec_ref, + list(accounts)[0].display_name, + ) continue if len(partners) > 1: logger.warning( "Skip reconcile of ref '%s' because the lines with " "this ref have different partners (IDs %s)", - rec_ref, ', '.join(partners.keys())) + rec_ref, + ", ".join(partners.keys()), + ) continue lines_to_rec.reconcile() - logger.info('Reconcile imported moves finished') + logger.info("Reconcile imported moves finished") diff --git a/account_move_csv_import/wizard/account_move_import_view.xml b/account_move_csv_import/wizard/account_move_import_view.xml index 99ccf49..a7493e4 100644 --- a/account_move_csv_import/wizard/account_move_import_view.xml +++ b/account_move_csv_import/wizard/account_move_import_view.xml @@ -1,11 +1,9 @@ - - + - @@ -14,36 +12,75 @@
- - - - + + + + - - + + - - - + + + - - - + + + -