├── erpnext_datev ├── config │ ├── __init__.py │ ├── desktop.py │ └── docs.py ├── utils │ ├── __init__.py │ ├── datev_csv.py │ └── datev_constants.py ├── modules.txt ├── templates │ ├── __init__.py │ └── pages │ │ └── __init__.py ├── erpnext_datev │ ├── __init__.py │ ├── doctype │ │ ├── __init__.py │ │ ├── datev_settings │ │ │ ├── __init__.py │ │ │ ├── test_datev_settings.py │ │ │ ├── datev_settings.js │ │ │ ├── datev_settings.py │ │ │ └── datev_settings.json │ │ ├── datev_voucher_config │ │ │ ├── __init__.py │ │ │ ├── datev_voucher_config.py │ │ │ └── datev_voucher_config.json │ │ └── datev_unternehmen_online_settings │ │ │ ├── __init__.py │ │ │ ├── test_datev_unternehmen_online_settings.py │ │ │ ├── datev_unternehmen_online_settings.js │ │ │ ├── datev_unternehmen_online_settings.json │ │ │ └── datev_unternehmen_online_settings.py │ └── report │ │ ├── __init__.py │ │ └── datev │ │ ├── __init__.py │ │ ├── datev.json │ │ ├── datev.js │ │ ├── test_datev.py │ │ └── datev.py ├── __init__.py ├── patches.txt ├── install.py ├── hooks.py └── locale │ ├── main.pot │ └── de.po ├── .gitignore ├── .editorconfig ├── .releaserc ├── .github └── workflows │ └── release.yaml ├── .pre-commit-config.yaml ├── pyproject.toml ├── .eslintrc └── README.md /erpnext_datev/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_datev/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_datev/modules.txt: -------------------------------------------------------------------------------- 1 | Erpnext Datev -------------------------------------------------------------------------------- /erpnext_datev/templates/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_datev/templates/pages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/doctype/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/report/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_datev/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "15.3.3" 2 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/report/datev/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/doctype/datev_settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/doctype/datev_voucher_config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.egg-info 4 | *.swp 5 | tags 6 | erpnext_datev/docs/current 7 | build/ 8 | -------------------------------------------------------------------------------- /erpnext_datev/patches.txt: -------------------------------------------------------------------------------- 1 | [pre_model_sync] 2 | 3 | [post_model_sync] 4 | execute:frappe.db.delete("Custom Role", {"report": "DATEV"}) 5 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/doctype/datev_settings/test_datev_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | import unittest 6 | 7 | 8 | class TestDATEVSettings(unittest.TestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /erpnext_datev/config/desktop.py: -------------------------------------------------------------------------------- 1 | from frappe import _ 2 | 3 | 4 | def get_data(): 5 | return [ 6 | { 7 | "module_name": "ERPNext DATEV", 8 | "color": "grey", 9 | "icon": "octicon octicon-file-directory", 10 | "type": "module", 11 | "label": _("ERPNext DATEV"), 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/test_datev_unternehmen_online_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Alyf and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | import unittest 6 | 7 | 8 | class TestDATEVUnternehmenOnlineSettings(unittest.TestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Alyf and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class DATEVVoucherConfig(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /erpnext_datev/config/docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration for docs 3 | """ 4 | 5 | # source_link = "https://github.com/[org_name]/erpnext_datev" 6 | # headline = "App that does everything" 7 | # sub_heading = "Yes, you got that right the first time, everything" 8 | 9 | 10 | def get_context(context): 11 | context.brand_html = "ERPNext DATEV" 12 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("DATEV Settings", { 5 | refresh: function (frm) { 6 | frm.add_custom_button( 7 | "Show Report", 8 | () => frappe.set_route("query-report", "DATEV"), 9 | "fa fa-table" 10 | ); 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Root editor config file 2 | root = true 3 | 4 | # Common settings 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | 11 | # python, js indentation settings 12 | [{*.py,*.js,*.vue,*.css,*.scss,*.html}] 13 | indent_style = tab 14 | indent_size = 4 15 | max_line_length = 99 16 | 17 | # JSON files - mostly doctype schema files 18 | [{*.json}] 19 | insert_final_newline = false 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /erpnext_datev/install.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe.custom.doctype.custom_field.custom_field import create_custom_fields 3 | 4 | 5 | def after_install(): 6 | make_custom_fields() 7 | 8 | 9 | def make_custom_fields(): 10 | custom_fields = { 11 | "Party Account": [ 12 | dict( 13 | fieldname="debtor_creditor_number", 14 | label="Debtor/Creditor Number", 15 | fieldtype="Data", 16 | insert_after="account", 17 | translatable=0, 18 | ) 19 | ] 20 | } 21 | 22 | create_custom_fields(custom_fields) 23 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/report/datev/datev.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_total_row": 0, 3 | "columns": [], 4 | "creation": "2022-02-17 23:29:46.760108", 5 | "disabled": 0, 6 | "docstatus": 0, 7 | "doctype": "Report", 8 | "filters": [], 9 | "idx": 0, 10 | "is_standard": "Yes", 11 | "letterhead": null, 12 | "modified": "2024-07-15 23:10:51.572551", 13 | "modified_by": "Administrator", 14 | "module": "Erpnext Datev", 15 | "name": "DATEV", 16 | "owner": "Administrator", 17 | "prepared_report": 0, 18 | "ref_doctype": "GL Entry", 19 | "report_name": "DATEV", 20 | "report_type": "Script Report", 21 | "roles": [ 22 | { 23 | "role": "Accounts User" 24 | }, 25 | { 26 | "role": "Accounts Manager" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Alyf and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on("DATEV Unternehmen Online Settings", { 5 | refresh: function (frm) { 6 | frm.set_query("voucher_type", "datev_voucher_config", function (doc, cdt, cdn) { 7 | return { 8 | filters: { 9 | name: [ 10 | "in", 11 | ["Sales Invoice", "Purchase Invoice", "Expense Claim", "E Invoice Import"], 12 | ], 13 | }, 14 | }; 15 | }); 16 | 17 | frm.set_query("print_format", "datev_voucher_config", function (doc, cdt, cdn) { 18 | let row = locals[cdt][cdn]; 19 | return { 20 | filters: { 21 | doc_type: row.voucher_type, 22 | }, 23 | }; 24 | }); 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, ALYF GmbH and contributors 2 | # For license information, please see license.txt 3 | 4 | from frappe import _, throw 5 | from frappe.model.document import Document 6 | 7 | 8 | class DATEVSettings(Document): 9 | def validate(self): 10 | if ( 11 | self.temporary_against_account_number 12 | and len(self.temporary_against_account_number) != self.account_number_length 13 | ): 14 | throw( 15 | _("Temporary Against Account Number must be {0} digits long").format( 16 | self.account_number_length 17 | ) 18 | ) 19 | 20 | if ( 21 | self.opening_against_account_number 22 | and len(self.opening_against_account_number) != self.account_number_length 23 | ): 24 | throw( 25 | _("Opening Against Account Number must be {0} digits long").format(self.account_number_length) 26 | ) 27 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["version-14", "version-15"], 3 | "plugins": [ 4 | [ 5 | "@semantic-release/commit-analyzer", { 6 | "releaseRules": [ 7 | {"breaking": true, "release": "minor"}, 8 | {"revert": true, "release": "patch"}, 9 | {"type": "feat", "release": "minor"}, 10 | {"type": "patch", "release": "minor"}, 11 | {"type": "fix", "release": "patch"}, 12 | {"type": "perf", "release": "patch"}, 13 | {"type": "refactor", "release": "patch"}, 14 | {"type": "docs", "release": "patch"}, 15 | {"type": "chore", "release": "patch"}, 16 | {"type": "ci", "release": "patch"} 17 | ] 18 | } 19 | ], 20 | "@semantic-release/release-notes-generator", 21 | [ 22 | "@semantic-release/exec", { 23 | "prepareCmd": 'sed -ir -E "s/\"[0-9]+\.[0-9]+\.[0-9]+\"/\"${nextRelease.version}\"/" erpnext_datev/__init__.py' 24 | } 25 | ], 26 | [ 27 | "@semantic-release/git", { 28 | "assets": ["erpnext_datev/__init__.py"], 29 | "message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}" 30 | } 31 | ], 32 | "@semantic-release/github" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Generate Semantic Release 2 | on: 3 | push: 4 | branches: 5 | - version-15 6 | 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Entire Repository 13 | uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | persist-credentials: false # https://github.com/semantic-release/semantic-release/blob/master/docs/recipes/ci-configurations/github-actions.md#pushing-packagejson-changes-to-a-master-branch 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: "lts/*" 21 | - name: Setup dependencies 22 | run: | 23 | npm install @semantic-release/git @semantic-release/exec --no-save 24 | - name: Create Release 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 27 | GIT_AUTHOR_NAME: "alyf-linus" 28 | GIT_AUTHOR_EMAIL: "136631072+alyf-linus@users.noreply.github.com" 29 | GIT_COMMITTER_NAME: "alyf-linus" 30 | GIT_COMMITTER_EMAIL: "136631072+alyf-linus@users.noreply.github.com" 31 | run: npx semantic-release 32 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'node_modules|.git' 2 | default_stages: [pre-commit] 3 | fail_fast: false 4 | 5 | 6 | repos: 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v4.3.0 9 | hooks: 10 | - id: trailing-whitespace 11 | files: "frappe.*" 12 | exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" 13 | - id: check-yaml 14 | - id: no-commit-to-branch 15 | args: ['--branch', 'develop'] 16 | - id: check-merge-conflict 17 | - id: check-ast 18 | - id: check-json 19 | - id: check-toml 20 | - id: check-yaml 21 | - id: debug-statements 22 | 23 | - repo: https://github.com/astral-sh/ruff-pre-commit 24 | rev: v0.2.0 25 | hooks: 26 | - id: ruff 27 | name: "Run ruff import sorter" 28 | args: ["--select=I", "--fix"] 29 | 30 | - id: ruff 31 | name: "Run ruff linter" 32 | 33 | - id: ruff-format 34 | name: "Run ruff formatter" 35 | 36 | - repo: https://github.com/pre-commit/mirrors-prettier 37 | rev: v2.7.1 38 | hooks: 39 | - id: prettier 40 | types_or: [javascript, vue, scss] 41 | 42 | - repo: https://github.com/pre-commit/mirrors-eslint 43 | rev: v8.44.0 44 | hooks: 45 | - id: eslint 46 | types_or: [javascript] 47 | args: ['--quiet'] 48 | 49 | ci: 50 | autoupdate_schedule: weekly 51 | skip: [] 52 | submodules: false 53 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "erpnext_datev" 3 | authors = [ 4 | { name = "ALYF GmbH", email = "hallo@alyf.de"} 5 | ] 6 | description = "ERPNext-DATEV Inegration" 7 | requires-python = ">=3.10" 8 | readme = "README.md" 9 | dynamic = ["version"] 10 | dependencies = [ 11 | "pandas~=2.2.2", 12 | ] 13 | 14 | [build-system] 15 | requires = ["flit_core >=3.4,<4"] 16 | build-backend = "flit_core.buildapi" 17 | 18 | [tool.bench.frappe-dependencies] 19 | frappe = ">=15.0.0,<16.0.0" 20 | erpnext = ">=15.0.0,<16.0.0" 21 | 22 | [tool.ruff] 23 | line-length = 110 24 | target-version = "py310" 25 | 26 | [tool.ruff.lint] 27 | select = [ 28 | "F", 29 | "E", 30 | "W", 31 | "I", 32 | "UP", 33 | "B", 34 | "RUF", 35 | ] 36 | ignore = [ 37 | "B017", # assertRaises(Exception) - should be more specific 38 | "B018", # useless expression, not assigned to anything 39 | "B023", # function doesn't bind loop variable - will have last iteration's value 40 | "B904", # raise inside except without from 41 | "E101", # indentation contains mixed spaces and tabs 42 | "E402", # module level import not at top of file 43 | "E501", # line too long 44 | "E741", # ambiguous variable name 45 | "F401", # "unused" imports 46 | "F403", # can't detect undefined names from * import 47 | "F405", # can't detect undefined names from * import 48 | "F722", # syntax error in forward type annotation 49 | "W191", # indentation contains tabs 50 | "RUF001", # string contains ambiguous unicode character 51 | "UP032", # use f-string instead of format 52 | ] 53 | typing-modules = ["frappe.types.DF"] 54 | 55 | [tool.ruff.format] 56 | quote-style = "double" 57 | indent-style = "tab" 58 | docstring-code-format = true 59 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "creation": "2021-12-03 18:53:41.716854", 4 | "doctype": "DocType", 5 | "editable_grid": 1, 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "voucher_type", 9 | "recipient", 10 | "attach_print", 11 | "print_format", 12 | "attach_files" 13 | ], 14 | "fields": [ 15 | { 16 | "columns": 2, 17 | "fieldname": "voucher_type", 18 | "fieldtype": "Link", 19 | "in_list_view": 1, 20 | "label": "Voucher Type", 21 | "options": "DocType", 22 | "reqd": 1 23 | }, 24 | { 25 | "depends_on": "attach_print", 26 | "fieldname": "print_format", 27 | "fieldtype": "Link", 28 | "label": "Print Format", 29 | "mandatory_depends_on": "eval: doc.attach_print === 1", 30 | "options": "Print Format" 31 | }, 32 | { 33 | "columns": 4, 34 | "fieldname": "recipient", 35 | "fieldtype": "Data", 36 | "in_list_view": 1, 37 | "label": "Recipient", 38 | "options": "Email", 39 | "reqd": 1 40 | }, 41 | { 42 | "columns": 2, 43 | "default": "0", 44 | "description": "Send the voucher printed as PDF", 45 | "fieldname": "attach_print", 46 | "fieldtype": "Check", 47 | "in_list_view": 1, 48 | "label": "Attach Print" 49 | }, 50 | { 51 | "columns": 2, 52 | "default": "0", 53 | "description": "Send files attached to the voucher", 54 | "fieldname": "attach_files", 55 | "fieldtype": "Check", 56 | "in_list_view": 1, 57 | "label": "Attach Files" 58 | } 59 | ], 60 | "istable": 1, 61 | "links": [], 62 | "modified": "2021-12-14 14:44:10.678868", 63 | "modified_by": "Administrator", 64 | "module": "Erpnext Datev", 65 | "name": "DATEV Voucher Config", 66 | "owner": "Administrator", 67 | "permissions": [], 68 | "sort_field": "modified", 69 | "sort_order": "DESC" 70 | } -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/report/datev/datev.js: -------------------------------------------------------------------------------- 1 | frappe.query_reports["DATEV"] = { 2 | filters: [ 3 | { 4 | fieldname: "company", 5 | label: __("Company"), 6 | fieldtype: "Link", 7 | options: "Company", 8 | default: 9 | frappe.defaults.get_user_default("Company") || 10 | frappe.defaults.get_global_default("Company"), 11 | reqd: 1, 12 | }, 13 | { 14 | fieldname: "from_date", 15 | label: __("From Date"), 16 | default: moment().subtract(1, "month").startOf("month").format(), 17 | fieldtype: "Date", 18 | reqd: 1, 19 | }, 20 | { 21 | fieldname: "to_date", 22 | label: __("To Date"), 23 | default: moment().subtract(1, "month").endOf("month").format(), 24 | fieldtype: "Date", 25 | reqd: 1, 26 | }, 27 | { 28 | fieldname: "voucher_type", 29 | label: __("Voucher Type"), 30 | fieldtype: "Select", 31 | options: 32 | "\nSales Invoice\nPurchase Invoice\nPayment Entry\nExpense Claim\nPayroll Entry\nBank Reconciliation\nAsset\nStock Entry\nJournal Entry", 33 | }, 34 | ], 35 | onload: function (query_report) { 36 | let company = frappe.query_report.get_filter_value("company"); 37 | frappe.db.exists("DATEV Settings", company).then((settings_exist) => { 38 | if (!settings_exist) { 39 | frappe.confirm( 40 | __( 41 | "DATEV Settings for your Company are missing. Would you like to create them now?" 42 | ), 43 | () => frappe.new_doc("DATEV Settings", { company: company }) 44 | ); 45 | } 46 | }); 47 | 48 | query_report.page.set_primary_action(__("Download DATEV File"), () => { 49 | const filters = encodeURIComponent(JSON.stringify(query_report.get_values())); 50 | window.open( 51 | `/api/method/erpnext_datev.erpnext_datev.report.datev.datev.download_datev_csv?filters=${filters}` 52 | ); 53 | }); 54 | 55 | query_report.page.add_menu_item(__("Change DATEV Settings"), () => { 56 | let company = frappe.query_report.get_filter_value("company"); // read company from filters again – it might have changed by now. 57 | frappe.set_route("Form", "DATEV Settings", company); 58 | }); 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2021-12-03 18:51:04.279493", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "enabled", 10 | "column_break_xkqo", 11 | "default_print_language", 12 | "section_break_2", 13 | "sender", 14 | "datev_voucher_config" 15 | ], 16 | "fields": [ 17 | { 18 | "default": "0", 19 | "fieldname": "enabled", 20 | "fieldtype": "Check", 21 | "label": "Enabled" 22 | }, 23 | { 24 | "fieldname": "section_break_2", 25 | "fieldtype": "Section Break" 26 | }, 27 | { 28 | "fieldname": "datev_voucher_config", 29 | "fieldtype": "Table", 30 | "label": "DATEV Voucher Config", 31 | "mandatory_depends_on": "eval: doc.enabled === 1", 32 | "options": "DATEV Voucher Config" 33 | }, 34 | { 35 | "fieldname": "sender", 36 | "fieldtype": "Link", 37 | "label": "Sender", 38 | "mandatory_depends_on": "eval: doc.enabled === 1", 39 | "options": "Email Account" 40 | }, 41 | { 42 | "fieldname": "column_break_xkqo", 43 | "fieldtype": "Column Break" 44 | }, 45 | { 46 | "description": "A document is printed according to its language field. If it doesn't have a language field or it is empty, the Default Print Language is used. If Default Print Language is empty, the language according to System Settings is used.", 47 | "fieldname": "default_print_language", 48 | "fieldtype": "Link", 49 | "label": "Default Print Language", 50 | "options": "Language" 51 | } 52 | ], 53 | "index_web_pages_for_search": 1, 54 | "issingle": 1, 55 | "links": [], 56 | "modified": "2025-03-10 12:10:58.029963", 57 | "modified_by": "Administrator", 58 | "module": "Erpnext Datev", 59 | "name": "DATEV Unternehmen Online Settings", 60 | "owner": "Administrator", 61 | "permissions": [ 62 | { 63 | "create": 1, 64 | "delete": 1, 65 | "email": 1, 66 | "print": 1, 67 | "read": 1, 68 | "role": "System Manager", 69 | "share": 1, 70 | "write": 1 71 | } 72 | ], 73 | "sort_field": "modified", 74 | "sort_order": "DESC", 75 | "states": [] 76 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es2022": true 6 | }, 7 | "parserOptions": { 8 | "sourceType": "module" 9 | }, 10 | "extends": "eslint:recommended", 11 | "rules": { 12 | "indent": "off", 13 | "brace-style": "off", 14 | "no-mixed-spaces-and-tabs": "off", 15 | "no-useless-escape": "off", 16 | "space-unary-ops": ["error", { "words": true }], 17 | "linebreak-style": "off", 18 | "quotes": ["off"], 19 | "semi": "off", 20 | "camelcase": "off", 21 | "no-unused-vars": "off", 22 | "no-console": ["warn"], 23 | "no-extra-boolean-cast": ["off"], 24 | "no-control-regex": ["off"] 25 | }, 26 | "root": true, 27 | "globals": { 28 | "frappe": true, 29 | "Vue": true, 30 | "SetVueGlobals": true, 31 | "__": true, 32 | "repl": true, 33 | "Class": true, 34 | "locals": true, 35 | "cint": true, 36 | "cstr": true, 37 | "cur_frm": true, 38 | "cur_dialog": true, 39 | "cur_page": true, 40 | "cur_list": true, 41 | "cur_tree": true, 42 | "msg_dialog": true, 43 | "is_null": true, 44 | "in_list": true, 45 | "has_common": true, 46 | "posthog": true, 47 | "has_words": true, 48 | "validate_email": true, 49 | "open_web_template_values_editor": true, 50 | "validate_name": true, 51 | "validate_phone": true, 52 | "validate_url": true, 53 | "get_number_format": true, 54 | "format_number": true, 55 | "format_currency": true, 56 | "comment_when": true, 57 | "open_url_post": true, 58 | "toTitle": true, 59 | "lstrip": true, 60 | "rstrip": true, 61 | "strip": true, 62 | "strip_html": true, 63 | "replace_all": true, 64 | "flt": true, 65 | "precision": true, 66 | "CREATE": true, 67 | "AMEND": true, 68 | "CANCEL": true, 69 | "copy_dict": true, 70 | "get_number_format_info": true, 71 | "strip_number_groups": true, 72 | "print_table": true, 73 | "Layout": true, 74 | "web_form_settings": true, 75 | "$c": true, 76 | "$a": true, 77 | "$i": true, 78 | "$bg": true, 79 | "$y": true, 80 | "$c_obj": true, 81 | "refresh_many": true, 82 | "refresh_field": true, 83 | "toggle_field": true, 84 | "get_field_obj": true, 85 | "get_query_params": true, 86 | "unhide_field": true, 87 | "hide_field": true, 88 | "set_field_options": true, 89 | "getCookie": true, 90 | "getCookies": true, 91 | "get_url_arg": true, 92 | "md5": true, 93 | "$": true, 94 | "jQuery": true, 95 | "moment": true, 96 | "hljs": true, 97 | "Awesomplete": true, 98 | "Sortable": true, 99 | "Showdown": true, 100 | "Taggle": true, 101 | "Gantt": true, 102 | "Slick": true, 103 | "Webcam": true, 104 | "PhotoSwipe": true, 105 | "PhotoSwipeUI_Default": true, 106 | "io": true, 107 | "JsBarcode": true, 108 | "L": true, 109 | "Chart": true, 110 | "DataTable": true, 111 | "Cypress": true, 112 | "cy": true, 113 | "it": true, 114 | "describe": true, 115 | "expect": true, 116 | "context": true, 117 | "before": true, 118 | "beforeEach": true, 119 | "after": true, 120 | "qz": true, 121 | "localforage": true, 122 | "extend_cscript": true 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "autoname": "field:client", 4 | "creation": "2022-02-17 23:29:46.086115", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "client", 10 | "client_number", 11 | "column_break_2", 12 | "consultant_number", 13 | "consultant", 14 | "section_break_4", 15 | "account_number_length", 16 | "column_break_6", 17 | "temporary_against_account_number", 18 | "opening_against_account_number" 19 | ], 20 | "fields": [ 21 | { 22 | "fieldname": "client", 23 | "fieldtype": "Link", 24 | "in_list_view": 1, 25 | "label": "Client", 26 | "options": "Company", 27 | "reqd": 1, 28 | "unique": 1 29 | }, 30 | { 31 | "fieldname": "client_number", 32 | "fieldtype": "Data", 33 | "in_list_view": 1, 34 | "label": "Client ID", 35 | "length": 5, 36 | "reqd": 1 37 | }, 38 | { 39 | "fieldname": "consultant", 40 | "fieldtype": "Link", 41 | "in_list_view": 1, 42 | "label": "Consultant", 43 | "options": "Supplier" 44 | }, 45 | { 46 | "fieldname": "consultant_number", 47 | "fieldtype": "Data", 48 | "in_list_view": 1, 49 | "label": "Consultant ID", 50 | "length": 7, 51 | "reqd": 1 52 | }, 53 | { 54 | "fieldname": "column_break_2", 55 | "fieldtype": "Column Break" 56 | }, 57 | { 58 | "fieldname": "section_break_4", 59 | "fieldtype": "Section Break" 60 | }, 61 | { 62 | "fieldname": "column_break_6", 63 | "fieldtype": "Column Break" 64 | }, 65 | { 66 | "default": "4", 67 | "fieldname": "account_number_length", 68 | "fieldtype": "Int", 69 | "label": "Account Number Length", 70 | "reqd": 1 71 | }, 72 | { 73 | "allow_in_quick_entry": 1, 74 | "default": "9090", 75 | "description": "Will be used as against account for all normal ledger entries", 76 | "fieldname": "temporary_against_account_number", 77 | "fieldtype": "Data", 78 | "label": "Temporary Against Account Number", 79 | "reqd": 1 80 | }, 81 | { 82 | "default": "9000", 83 | "description": "Will be used as against account for opening ledger entries", 84 | "fieldname": "opening_against_account_number", 85 | "fieldtype": "Data", 86 | "label": "Opening Against Account Number" 87 | } 88 | ], 89 | "grid_page_length": 50, 90 | "links": [], 91 | "modified": "2025-06-12 16:30:22.776994", 92 | "modified_by": "Administrator", 93 | "module": "Erpnext Datev", 94 | "name": "DATEV Settings", 95 | "naming_rule": "By fieldname", 96 | "owner": "Administrator", 97 | "permissions": [ 98 | { 99 | "create": 1, 100 | "delete": 1, 101 | "email": 1, 102 | "export": 1, 103 | "print": 1, 104 | "read": 1, 105 | "report": 1, 106 | "role": "System Manager", 107 | "share": 1, 108 | "write": 1 109 | }, 110 | { 111 | "create": 1, 112 | "delete": 1, 113 | "email": 1, 114 | "export": 1, 115 | "print": 1, 116 | "read": 1, 117 | "report": 1, 118 | "role": "Accounts Manager", 119 | "share": 1, 120 | "write": 1 121 | }, 122 | { 123 | "create": 1, 124 | "email": 1, 125 | "export": 1, 126 | "print": 1, 127 | "read": 1, 128 | "report": 1, 129 | "role": "Accounts User", 130 | "share": 1 131 | } 132 | ], 133 | "quick_entry": 1, 134 | "row_format": "Dynamic", 135 | "sort_field": "modified", 136 | "sort_order": "DESC", 137 | "states": [], 138 | "track_changes": 1 139 | } -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, ALYF GmbH and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe import _ 6 | from frappe.core.doctype.communication.email import make as make_communication 7 | from frappe.model.document import Document 8 | from frappe.translate import print_language 9 | 10 | 11 | class DATEVUnternehmenOnlineSettings(Document): 12 | def validate(self): 13 | for voucher_config in self.datev_voucher_config: 14 | if not voucher_config.attach_print and not voucher_config.attach_files: 15 | frappe.throw( 16 | _("Please configure attachments for voucher type {}.").format( 17 | _(voucher_config.voucher_type) 18 | ) 19 | ) 20 | 21 | 22 | def send(doc, method): 23 | settings = frappe.get_single("DATEV Unternehmen Online Settings") 24 | if not settings.enabled: 25 | return 26 | 27 | voucher_config = get_voucher_config(settings, doc.doctype) 28 | if not voucher_config: 29 | return 30 | 31 | attachments = [] 32 | 33 | if voucher_config.attach_print: 34 | document_language = doc.language if hasattr(doc, "language") else None 35 | print_language = document_language or settings.default_print_language or frappe.db.get_default("lang") 36 | filename = attach_print( 37 | doc.doctype, 38 | doc.name, 39 | print_language, 40 | voucher_config.print_format, 41 | ) 42 | attachments.append(filename) 43 | 44 | if voucher_config.attach_files: 45 | attachments.extend(get_attached_files(doc.doctype, doc.name)) 46 | 47 | if not attachments: 48 | frappe.msgprint( 49 | # fmt: off 50 | _("{} was not sent to DATEV because no attachments have been found.").format(_(doc.doctype)) 51 | # fmt: on 52 | ) 53 | return 54 | 55 | make_communication( 56 | doctype=doc.doctype, 57 | name=doc.name, 58 | content=_("New {0} {1} sent by the ERPNext-DATEV integration.").format(_(doc.doctype), doc.name), 59 | subject=f"{_(doc.doctype)}: {doc.name}", 60 | sender=frappe.get_value("Email Account", settings.sender, "email_id"), 61 | recipients=[voucher_config.recipient], 62 | communication_medium="Email", 63 | send_email=True, 64 | attachments=attachments, 65 | communication_type="Automated Message", 66 | ) 67 | 68 | 69 | def attach_print(doctype, name, language, print_format): 70 | with print_language(language): 71 | data = frappe.get_print(doctype, name, print_format, as_pdf=True) 72 | 73 | if doctype == "Sales Invoice" and "eu_einvoice" in frappe.get_installed_apps(): 74 | try: 75 | from eu_einvoice.european_e_invoice.custom.sales_invoice import attach_xml_to_pdf 76 | 77 | data = attach_xml_to_pdf(name, data) 78 | except Exception: 79 | msg = _("Failed to attach XML to Sales Invoice PDF for DATEV") 80 | frappe.log_error(title=msg, reference_doctype=doctype, reference_name=name) 81 | frappe.msgprint(msg, indicator="red", alert=True) 82 | 83 | file = frappe.new_doc("File") 84 | file.file_name = f"{name}.pdf" 85 | file.content = data 86 | file.attached_to_doctype = doctype 87 | file.attached_to_name = name 88 | file.is_private = 1 89 | file.save() 90 | 91 | return file.name 92 | 93 | 94 | def get_voucher_config(settings: DATEVUnternehmenOnlineSettings, doctype: str): 95 | voucher_config = settings.get("datev_voucher_config", filters={"voucher_type": doctype}) 96 | if not voucher_config: 97 | return 98 | 99 | return voucher_config[0] 100 | 101 | 102 | def get_attached_files(doctype: str, docname: str): 103 | return frappe.get_all( 104 | "File", 105 | filters={ 106 | "attached_to_doctype": doctype, 107 | "attached_to_name": docname, 108 | }, 109 | pluck="name", 110 | ) 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ERPNext - DATEV Integration 2 | 3 | Integration between [ERPNext](https://github.com/frappe/erpnext) and DATEV. 4 | 5 | - [DATEV Unternehmen Online](https://www.datev.de/web/de/mydatev/online-anwendungen/datev-unternehmen-online/) 6 | 7 | When a voucher is submitted, it will be sent to DATEV Unternehmen Online by email. Either by converting the document to PDF first (outgoing vouchers) or by sending files attached to the document (incoming vouchers). 8 | 9 | - DATEV CSV Export 10 | 11 | Export raw **GL Entries** from ERPNext in the DATEV CSV format. 12 | 13 | ## Install on Frappe Cloud 14 | 15 | 1. Go to https://frappecloud.com/dashboard/#/sites and click the "New Site" button. 16 | 2. In Step 2 ("Select apps to install"), select "ERPNext" and "DATEV Unternehmen Online Integration". 17 | 3. Complete the new site wizard. 18 | 19 | 20 | ## Setup DATEV CSV Export 21 | 22 | 1. Datev Settings 23 | 24 | Configure you client number, you tax consultant's number and a temporary against account. We recommend keeping the default against account "9090" as described in the [DATEV Help Center](https://apps.datev.de/help-center/documents/1002764). 25 | 26 | 2. DATEV Report 27 | 28 | Now you can use the report "DATEV". This is a preview of the transactions data. It can be exported, along with the master data, as zip file via the report's menu. Your tax xonsultant can then import your GL Entries into his DATEV system. 29 | 30 | > [!IMPORTANT] 31 | > ERPNext does not have automatic VAT deduction ("Automatikkonten") on the GL Entry level. By using the default against account "9090", the automation is disabled. 32 | > 33 | > If you use a different against account, please ensure to only book to non-automatic accounts in ERPNext. Otherwise, the VAT deduction will be incorrect. 34 | 35 | ## Setup DATEV Unternehmen Online [en] 36 | 37 | 1. Open **DATEV Unternehmen Online Settings** 38 | 2. Enable the integration 39 | 3. Select the _Email Account_ that should be used to send receipts to DATEV Unternehmen Online 40 | 4. Add a row to the table 41 | 5. Select the _Voucher Type_ (**Sales Invoice**, **Purchase Invoice** or **Expense Claim**) 42 | 6. Paste the target email address provided by DATEV Unternehmen Online ([DATEV Help Center](https://apps.datev.de/help-center/documents/1007550)) 43 | 8. Enable "Add Attachments" or "Add Print" 44 | 9. Save 45 | 46 | ![datev-unternehmen-online-settings](https://user-images.githubusercontent.com/14891507/155744820-f7eb3aa7-ba36-4a66-aa12-80e75fc467de.png) 47 | 48 | ## Einrichtung DATEV Unternehmen Online [de] 49 | 50 | 1. Öffnen Sie **DATEV Unternehmen Online-Einstellungen** (engl. **DATEV Unternehmen Online Settings**) 51 | 2. Aktivieren Sie die Integration 52 | 3. Wählen Sie das _E-Mail-Konto_ (engl. _Email Account_) aus, das für den Versand von Belegen an DATEV Unternehmen Online verwendet werden soll 53 | 4. Fügen Sie der Tabelle eine Zeile hinzu 54 | 5. Wählen Sie die _Belegart_ (engl. _Voucher Type_) 55 | 6. Fügen Sie die für diese Belegart von DATEV Unternehmen Online bereitgestellte Ziel-E-Mail-Adresse ein (mehr dazu im [DATEV Help Center](https://apps.datev.de/help-center/documents/1007550)) 56 | > **Achtung:** Die E-Mail-Adresse des Senders muss mit dem in Schritt 3 ausgewählten E-Mail-Konto übereinstimmen 57 | 8. Aktivieren Sie "Anhänge hinzufügen" oder "Druck hinzufügen". 58 | 9. Speichern Sie die **DATEV Unternehmen Online-Einstellungen** 59 | 60 | ## Kompatibilität mit _PDF on Submit_ 61 | 62 | Falls Sie [PDF on Submit](https://github.com/alyf-de/erpnext_pdf-on-submit) für dieselbe Belegart verwenden, wählen Sie "Anhänge hinzufügen" statt "Druck hinzufügen". _PDF on Submit_ fügt dann die PDF-Datei als Anhang zum Beleg hinzu und die DATEV-Integration versendet diesen. 63 | 64 | ## Disclaimer 65 | 66 | "DATEV" and "DATEV Unternehmen Online" are trademarks of [DATEV eG](https://www.datev.de/). This integration is not approved or endorsed by DATEV eG. 67 | 68 | ## License 69 | 70 | GPLv3 71 | -------------------------------------------------------------------------------- /erpnext_datev/hooks.py: -------------------------------------------------------------------------------- 1 | from . import __version__ as app_version 2 | 3 | app_name = "erpnext_datev" 4 | app_title = "ERPNext DATEV Integration" 5 | app_publisher = "ALYF GmbH" 6 | app_description = "DATEV integration for ERPNext" 7 | required_apps = ["frappe/erpnext"] 8 | app_icon = "octicon octicon-file-directory" 9 | app_color = "grey" 10 | app_email = "hallo@alyf.de" 11 | app_license = "GPLv3" 12 | 13 | # fixtures = [] 14 | 15 | # Includes in 16 | # ------------------ 17 | 18 | # include js, css files in header of desk.html 19 | # app_include_css = "/assets/erpnext_datev/css/erpnext_datev.css" 20 | # app_include_js = "/assets/erpnext_datev/js/erpnext_datev.js" 21 | 22 | # include js, css files in header of web template 23 | # web_include_css = "/assets/erpnext_datev/css/erpnext_datev.css" 24 | # web_include_js = "/assets/erpnext_datev/js/erpnext_datev.js" 25 | 26 | # include custom scss in every website theme (without file extension ".scss") 27 | # website_theme_scss = "erpnext_datev/public/scss/website" 28 | 29 | # include js, css files in header of web form 30 | # webform_include_js = {"doctype": "public/js/doctype.js"} 31 | # webform_include_css = {"doctype": "public/css/doctype.css"} 32 | 33 | # include js in page 34 | # page_js = {"page" : "public/js/file.js"} 35 | 36 | # include js in doctype views 37 | # doctype_js = {"doctype" : "public/js/doctype.js"} 38 | # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} 39 | # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} 40 | # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} 41 | 42 | # Home Pages 43 | # ---------- 44 | 45 | # application home page (will override Website Settings) 46 | # home_page = "login" 47 | 48 | # website user home page (by Role) 49 | # role_home_page = { 50 | # "Role": "home_page" 51 | # } 52 | 53 | # Generators 54 | # ---------- 55 | 56 | # automatically create page for each record of this doctype 57 | # website_generators = ["Web Page"] 58 | 59 | # Jinja 60 | # ---------- 61 | 62 | # add methods and filters to jinja environment 63 | # jinja = { 64 | # "methods": "erpnext_datev.utils.jinja_methods", 65 | # "filters": "erpnext_datev.utils.jinja_filters" 66 | # } 67 | 68 | # Installation 69 | # ------------ 70 | 71 | # before_install = "erpnext_datev.install.before_install" 72 | after_install = "erpnext_datev.install.after_install" 73 | 74 | # Desk Notifications 75 | # ------------------ 76 | # See frappe.core.notifications.get_notification_config 77 | 78 | # notification_config = "erpnext_datev.notifications.get_notification_config" 79 | 80 | # Permissions 81 | # ----------- 82 | # Permissions evaluated in scripted ways 83 | 84 | # permission_query_conditions = { 85 | # "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions", 86 | # } 87 | # 88 | # has_permission = { 89 | # "Event": "frappe.desk.doctype.event.event.has_permission", 90 | # } 91 | 92 | # DocType Class 93 | # --------------- 94 | # Override standard doctype classes 95 | 96 | # override_doctype_class = { 97 | # "ToDo": "custom_app.overrides.CustomToDo" 98 | # } 99 | 100 | # Document Events 101 | # --------------- 102 | # Hook on document methods and events 103 | 104 | doc_events = { 105 | "*": { 106 | "on_submit": "erpnext_datev.erpnext_datev.doctype.datev_unternehmen_online_settings.datev_unternehmen_online_settings.send", 107 | }, 108 | } 109 | 110 | # Scheduled Tasks 111 | # --------------- 112 | 113 | # scheduler_events = { 114 | # "all": [ 115 | # "erpnext_datev.tasks.all" 116 | # ], 117 | # "daily": [ 118 | # "erpnext_datev.tasks.daily" 119 | # ], 120 | # "hourly": [ 121 | # "erpnext_datev.tasks.hourly" 122 | # ], 123 | # "weekly": [ 124 | # "erpnext_datev.tasks.weekly" 125 | # ], 126 | # "monthly": [ 127 | # "erpnext_datev.tasks.monthly" 128 | # ], 129 | # } 130 | 131 | # Testing 132 | # ------- 133 | 134 | # before_tests = "erpnext_datev.install.before_tests" 135 | 136 | # Overriding Methods 137 | # ------------------------------ 138 | # 139 | # override_whitelisted_methods = { 140 | # "frappe.desk.doctype.event.event.get_events": "erpnext_datev.event.get_events" 141 | # } 142 | # 143 | # each overriding function accepts a `data` argument; 144 | # generated from the base implementation of the doctype dashboard, 145 | # along with any modifications made in other Frappe apps 146 | # override_doctype_dashboards = { 147 | # "Task": "erpnext_datev.task.get_dashboard_data" 148 | # } 149 | 150 | # exempt linked doctypes from being automatically cancelled 151 | # 152 | # auto_cancel_exempted_doctypes = ["Auto Repeat"] 153 | 154 | 155 | # User Data Protection 156 | # -------------------- 157 | 158 | # user_data_fields = [ 159 | # { 160 | # "doctype": "{doctype_1}", 161 | # "filter_by": "{filter_by}", 162 | # "redact_fields": ["{field_1}", "{field_2}"], 163 | # "partial": 1, 164 | # }, 165 | # { 166 | # "doctype": "{doctype_2}", 167 | # "filter_by": "{filter_by}", 168 | # "partial": 1, 169 | # }, 170 | # { 171 | # "doctype": "{doctype_3}", 172 | # "strict": False, 173 | # }, 174 | # { 175 | # "doctype": "{doctype_4}" 176 | # } 177 | # ] 178 | 179 | # Authentication and authorization 180 | # -------------------------------- 181 | 182 | # auth_hooks = [ 183 | # "erpnext_datev.auth.validate"# ] 184 | -------------------------------------------------------------------------------- /erpnext_datev/utils/datev_csv.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import zipfile 3 | from csv import QUOTE_NONNUMERIC 4 | from io import BytesIO 5 | 6 | import frappe 7 | import pandas as pd 8 | from frappe import _ 9 | 10 | from .datev_constants import DataCategory 11 | 12 | 13 | def get_datev_csv(data, filters, csv_class): 14 | """ 15 | Fill in missing columns and return a CSV in DATEV Format. 16 | 17 | For automatic processing, DATEV requires the first line of the CSV file to 18 | hold meta data such as the length of account numbers oder the category of 19 | the data. 20 | 21 | Arguments: 22 | data -- array of dictionaries 23 | filters -- dict 24 | csv_class -- defines DATA_CATEGORY, FORMAT_NAME and COLUMNS 25 | """ 26 | empty_df = pd.DataFrame(columns=csv_class.COLUMNS) 27 | data_df = pd.DataFrame.from_records(data) 28 | result = pd.concat([empty_df, data_df], ignore_index=True) 29 | 30 | if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS: 31 | result["Belegdatum"] = pd.to_datetime(result["Belegdatum"]) 32 | 33 | result["Beleginfo - Inhalt 6"] = pd.to_datetime(result["Beleginfo - Inhalt 6"]) 34 | result["Beleginfo - Inhalt 6"] = result["Beleginfo - Inhalt 6"].dt.strftime("%d%m%Y") 35 | 36 | result["Fälligkeit"] = pd.to_datetime(result["Fälligkeit"]) 37 | result["Fälligkeit"] = result["Fälligkeit"].dt.strftime("%d%m%Y") 38 | 39 | result = result.sort_values(by="Belegdatum", kind="stable", ignore_index=True) 40 | 41 | if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES: 42 | result["Sprach-ID"] = "de-DE" 43 | 44 | data = result.to_csv( 45 | # Reason for str(';'): https://github.com/pandas-dev/pandas/issues/6035 46 | sep=";", 47 | # European decimal seperator 48 | decimal=",", 49 | # Windows "ANSI" encoding 50 | encoding="latin_1", 51 | # format date as DDMM 52 | date_format="%d%m", 53 | # Windows line terminator 54 | lineterminator="\r\n", 55 | # Do not number rows 56 | index=False, 57 | # Use all columns defined above 58 | columns=csv_class.COLUMNS, 59 | # Quote most fields, even currency values with "," separator 60 | quoting=QUOTE_NONNUMERIC, 61 | ) 62 | 63 | data = data.encode("latin_1", errors="replace") 64 | 65 | header = get_header(filters, csv_class) 66 | header = ";".join(header).encode("latin_1", errors="replace") 67 | 68 | # 1st Row: Header with meta data 69 | # 2nd Row: Data heading (Überschrift der Nutzdaten), included in `data` here. 70 | # 3rd - nth Row: Data (Nutzdaten) 71 | return header + b"\r\n" + data 72 | 73 | 74 | def get_header(filters, csv_class): 75 | description = filters.get("voucher_type", csv_class.FORMAT_NAME) 76 | company = filters.get("company") 77 | datev_settings = frappe.get_doc("DATEV Settings", {"client": company}) 78 | default_currency = frappe.get_value("Company", company, "default_currency") 79 | coa = frappe.get_value("Company", company, "chart_of_accounts") 80 | coa_short_code = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "") 81 | 82 | header = [ 83 | # DATEV format 84 | # "DTVF" = created by DATEV software, 85 | # "EXTF" = created by other software 86 | '"EXTF"', 87 | # version of the DATEV format 88 | # 141 = 1.41, 89 | # 510 = 5.10, 90 | # 720 = 7.20 91 | "700", 92 | csv_class.DATA_CATEGORY, 93 | '"%s"' % csv_class.FORMAT_NAME, 94 | # Format version (regarding format name) 95 | csv_class.FORMAT_VERSION, 96 | # Generated on 97 | datetime.datetime.now().strftime("%Y%m%d%H%M%S") + "000", 98 | # Imported on -- stays empty 99 | "", 100 | # Origin. Any two symbols, will be replaced by "SV" on import. 101 | '"EN"', 102 | # I = Exported by 103 | '"%s"' % frappe.session.user, 104 | # J = Imported by -- stays empty 105 | "", 106 | # K = Tax consultant number (Beraternummer) 107 | datev_settings.get("consultant_number", "0000000"), 108 | # L = Tax client number (Mandantennummer) 109 | datev_settings.get("client_number", "00000"), 110 | # M = Start of the fiscal year (Wirtschaftsjahresbeginn) 111 | frappe.utils.formatdate(filters.get("fiscal_year_start"), "yyyyMMdd"), 112 | # N = Length of account numbers (Sachkontenlänge) 113 | str(filters.get("account_number_length", 4)), 114 | # O = Transaction batch start date (YYYYMMDD) 115 | frappe.utils.formatdate(filters.get("from_date"), "yyyyMMdd") 116 | if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS 117 | else "", 118 | # P = Transaction batch end date (YYYYMMDD) 119 | frappe.utils.formatdate(filters.get("to_date"), "yyyyMMdd") 120 | if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS 121 | else "", 122 | # Q = Description (for example, "Sales Invoice") Max. 30 chars 123 | '"{}"'.format(_(description)) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", 124 | # R = Diktatkürzel 125 | "", 126 | # S = Buchungstyp 127 | # 1 = Transaction batch (Finanzbuchführung), 128 | # 2 = Annual financial statement (Jahresabschluss) 129 | "1" if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", 130 | # T = Rechnungslegungszweck 131 | # 0 oder leer = vom Rechnungslegungszweck unabhängig 132 | # 50 = Handelsrecht 133 | # 30 = Steuerrecht 134 | # 64 = IFRS 135 | # 40 = Kalkulatorik 136 | # 11 = Reserviert 137 | # 12 = Reserviert 138 | "0" if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", 139 | # U = Festschreibung 140 | # TODO: Filter by Accounting Period. In export for closed Accounting Period, this will be "1" 141 | "0", 142 | # V = Default currency, for example, "EUR" 143 | '"%s"' % default_currency if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", 144 | # reserviert 145 | "", 146 | # Derivatskennzeichen 147 | "", 148 | # reserviert 149 | "", 150 | # reserviert 151 | "", 152 | # SKR 153 | '"%s"' % coa_short_code, 154 | # Branchen-Lösungs-ID 155 | "", 156 | # reserviert 157 | "", 158 | # reserviert 159 | "", 160 | # Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung) 161 | "", 162 | ] 163 | return header 164 | 165 | 166 | def zip_and_download(zip_filename, csv_files): 167 | """ 168 | Put CSV files in a zip archive and send that to the client. 169 | 170 | Params: 171 | zip_filename Name of the zip file 172 | csv_files list of dicts [{'file_name': 'my_file.csv', 'csv_data': 'comma,separated,values'}] 173 | """ 174 | zip_buffer = BytesIO() 175 | 176 | zip_file = zipfile.ZipFile(zip_buffer, mode="w", compression=zipfile.ZIP_DEFLATED) 177 | for csv_file in csv_files: 178 | zip_file.writestr(csv_file.get("file_name"), csv_file.get("csv_data")) 179 | 180 | zip_file.close() 181 | 182 | frappe.response["filecontent"] = zip_buffer.getvalue() 183 | frappe.response["filename"] = zip_filename 184 | frappe.response["type"] = "binary" 185 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/report/datev/test_datev.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | from io import BytesIO 3 | from unittest import TestCase 4 | 5 | import frappe 6 | from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( 7 | create_sales_invoice, 8 | ) 9 | from frappe.utils import cstr, now_datetime, today 10 | 11 | from erpnext_datev.erpnext_datev.report.datev.datev import ( 12 | download_datev_csv, 13 | get_account_names, 14 | get_customers, 15 | get_suppliers, 16 | get_transactions, 17 | ) 18 | from erpnext_datev.utils.datev_constants import ( 19 | AccountNames, 20 | DebtorsCreditors, 21 | Transactions, 22 | ) 23 | from erpnext_datev.utils.datev_csv import get_datev_csv, get_header 24 | 25 | 26 | def make_company(company_name, abbr): 27 | if not frappe.db.exists("Company", company_name): 28 | company = frappe.get_doc( 29 | { 30 | "doctype": "Company", 31 | "company_name": company_name, 32 | "abbr": abbr, 33 | "default_currency": "EUR", 34 | "country": "Germany", 35 | "create_chart_of_accounts_based_on": "Standard Template", 36 | "chart_of_accounts": "SKR04 mit Kontonummern", 37 | } 38 | ) 39 | company.insert() 40 | else: 41 | company = frappe.get_doc("Company", company_name) 42 | 43 | # indempotent 44 | company.create_default_warehouses() 45 | 46 | if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": company.name}): 47 | company.create_default_cost_center() 48 | 49 | company.save() 50 | return company 51 | 52 | 53 | def setup_fiscal_year(): 54 | fiscal_year = None 55 | year = cstr(now_datetime().year) 56 | if not frappe.db.get_value("Fiscal Year", {"year": year}, "name"): 57 | try: 58 | fiscal_year = frappe.get_doc( 59 | { 60 | "doctype": "Fiscal Year", 61 | "year": year, 62 | "year_start_date": "{0}-01-01".format(year), 63 | "year_end_date": "{0}-12-31".format(year), 64 | } 65 | ) 66 | fiscal_year.insert() 67 | except frappe.NameError: 68 | pass 69 | 70 | if fiscal_year: 71 | fiscal_year.set_as_default() 72 | 73 | 74 | def make_customer_with_account(customer_name, company): 75 | acc_name = frappe.db.get_value( 76 | "Account", {"account_name": customer_name, "company": company.name}, "name" 77 | ) 78 | 79 | if not acc_name: 80 | acc = frappe.get_doc( 81 | { 82 | "doctype": "Account", 83 | "parent_account": "1 - Forderungen aus Lieferungen und Leistungen - _TG", 84 | "account_name": customer_name, 85 | "company": company.name, 86 | "account_type": "Receivable", 87 | "account_number": "10001", 88 | } 89 | ) 90 | acc.insert() 91 | acc_name = acc.name 92 | 93 | if not frappe.db.exists("Customer", customer_name): 94 | customer = frappe.get_doc( 95 | { 96 | "doctype": "Customer", 97 | "customer_name": customer_name, 98 | "customer_type": "Company", 99 | "accounts": [{"company": company.name, "account": acc_name}], 100 | } 101 | ) 102 | customer.insert() 103 | else: 104 | customer = frappe.get_doc("Customer", customer_name) 105 | 106 | return customer 107 | 108 | 109 | def make_item(item_code, company): 110 | warehouse_name = frappe.db.get_value( 111 | "Warehouse", {"warehouse_name": "Stores", "company": company.name}, "name" 112 | ) 113 | 114 | if not frappe.db.exists("Item", item_code): 115 | item = frappe.get_doc( 116 | { 117 | "doctype": "Item", 118 | "item_code": item_code, 119 | "item_name": item_code, 120 | "description": item_code, 121 | "item_group": "All Item Groups", 122 | "is_stock_item": 0, 123 | "is_purchase_item": 0, 124 | "is_customer_provided_item": 0, 125 | "item_defaults": [{"default_warehouse": warehouse_name, "company": company.name}], 126 | } 127 | ) 128 | item.insert() 129 | else: 130 | item = frappe.get_doc("Item", item_code) 131 | return item 132 | 133 | 134 | def make_datev_settings(company): 135 | if not frappe.db.exists("DATEV Settings", company.name): 136 | frappe.get_doc( 137 | { 138 | "doctype": "DATEV Settings", 139 | "client": company.name, 140 | "client_number": "12345", 141 | "consultant_number": "67890", 142 | "temporary_against_account_number": "9999", 143 | } 144 | ).insert() 145 | 146 | 147 | class TestDatev(TestCase): 148 | def setUp(self): 149 | self.company = make_company("_Test GmbH", "_TG") 150 | self.customer = make_customer_with_account("_Test Kunde GmbH", self.company) 151 | self.filters = { 152 | "company": self.company.name, 153 | "from_date": today(), 154 | "to_date": today(), 155 | "temporary_against_account_number": "9999", 156 | } 157 | 158 | make_datev_settings(self.company) 159 | item = make_item("_Test Item", self.company) 160 | setup_fiscal_year() 161 | 162 | warehouse = frappe.db.get_value( 163 | "Item Default", 164 | {"parent": item.name, "company": self.company.name}, 165 | "default_warehouse", 166 | ) 167 | 168 | income_account = frappe.db.get_value( 169 | "Account", {"account_number": "4200", "company": self.company.name}, "name" 170 | ) 171 | 172 | tax_account = frappe.db.get_value( 173 | "Account", {"account_number": "3806", "company": self.company.name}, "name" 174 | ) 175 | 176 | si = create_sales_invoice( 177 | company=self.company.name, 178 | customer=self.customer.name, 179 | currency=self.company.default_currency, 180 | debit_to=self.customer.accounts[0].account, 181 | income_account=income_account, 182 | expense_account="6990 - Herstellungskosten - _TG", 183 | cost_center=self.company.cost_center, 184 | warehouse=warehouse, 185 | item=item.name, 186 | do_not_save=1, 187 | ) 188 | 189 | si.append( 190 | "taxes", 191 | { 192 | "charge_type": "On Net Total", 193 | "account_head": tax_account, 194 | "description": "Umsatzsteuer 19 %", 195 | "rate": 19, 196 | "cost_center": self.company.cost_center, 197 | }, 198 | ) 199 | 200 | si.cost_center = self.company.cost_center 201 | 202 | si.save() 203 | si.submit() 204 | 205 | def test_columns(self): 206 | def is_subset(get_data, allowed_keys): 207 | """ 208 | Validate that the dict contains only allowed keys. 209 | 210 | Params: 211 | get_data -- Function that returns a list of dicts. 212 | allowed_keys -- List of allowed keys 213 | """ 214 | data = get_data(self.filters) 215 | if data == []: 216 | # No data and, therefore, no columns is okay 217 | return True 218 | actual_set = set(data[0].keys()) 219 | # allowed set must be interpreted as unicode to match the actual set 220 | allowed_set = set({frappe.as_unicode(key) for key in allowed_keys}) 221 | return actual_set.issubset(allowed_set) 222 | 223 | self.assertTrue(is_subset(get_transactions, Transactions.COLUMNS)) 224 | self.assertTrue(is_subset(get_customers, DebtorsCreditors.COLUMNS)) 225 | self.assertTrue(is_subset(get_suppliers, DebtorsCreditors.COLUMNS)) 226 | self.assertTrue(is_subset(get_account_names, AccountNames.COLUMNS)) 227 | 228 | def test_header(self): 229 | self.assertTrue(Transactions.DATA_CATEGORY in get_header(self.filters, Transactions)) 230 | self.assertTrue(AccountNames.DATA_CATEGORY in get_header(self.filters, AccountNames)) 231 | self.assertTrue(DebtorsCreditors.DATA_CATEGORY in get_header(self.filters, DebtorsCreditors)) 232 | 233 | def test_csv(self): 234 | test_data = [ 235 | { 236 | "Umsatz (ohne Soll/Haben-Kz)": 100, 237 | "Soll/Haben-Kennzeichen": "H", 238 | "Kontonummer": "4200", 239 | "Gegenkonto (ohne BU-Schlüssel)": "10000", 240 | "Belegdatum": today(), 241 | "Buchungstext": "No remark", 242 | "Beleginfo - Art 1": "Sales Invoice", 243 | "Beleginfo - Inhalt 1": "SINV-0001", 244 | } 245 | ] 246 | get_datev_csv(data=test_data, filters=self.filters, csv_class=Transactions) 247 | 248 | def test_download(self): 249 | """Assert that the returned file is a ZIP file.""" 250 | download_datev_csv(self.filters) 251 | 252 | # zipfile.is_zipfile() expects a file-like object 253 | zip_buffer = BytesIO() 254 | zip_buffer.write(frappe.response["filecontent"]) 255 | 256 | self.assertTrue(zipfile.is_zipfile(zip_buffer)) 257 | -------------------------------------------------------------------------------- /erpnext_datev/locale/main.pot: -------------------------------------------------------------------------------- 1 | # Translations template for ERPNext DATEV Integration. 2 | # Copyright (C) 2025 ALYF GmbH 3 | # This file is distributed under the same license as the ERPNext DATEV Integration project. 4 | # FIRST AUTHOR , 2025. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: ERPNext DATEV Integration VERSION\n" 9 | "Report-Msgid-Bugs-To: hallo@alyf.de\n" 10 | "POT-Creation-Date: 2025-03-10 12:21+0053\n" 11 | "PO-Revision-Date: 2025-03-10 12:21+0053\n" 12 | "Last-Translator: hallo@alyf.de\n" 13 | "Language-Team: hallo@alyf.de\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=utf-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Generated-By: Babel 2.13.1\n" 18 | 19 | #: erpnext_datev/erpnext_datev/report/datev/datev.py:173 20 | msgid "Company is a mandatory filter." 21 | msgstr "" 22 | 23 | #: erpnext_datev/erpnext_datev/report/datev/datev.py:177 24 | msgid "From Date is a mandatory filter." 25 | msgstr "" 26 | 27 | #: erpnext_datev/erpnext_datev/report/datev/datev.py:181 28 | msgid "To Date is a mandatory filter." 29 | msgstr "" 30 | 31 | #. Description of the 'Default Print Language' (Link) field in DocType 'DATEV 32 | #. Unternehmen Online Settings' 33 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.json 34 | msgid "A document is printed according to its language field. If it doesn't have a language field or it is empty, the Default Print Language is used. If Default Print Language is empty, the language according to System Settings is used." 35 | msgstr "" 36 | 37 | #. Label of a Int field in DocType 'DATEV Settings' 38 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 39 | msgid "Account Number Length" 40 | msgstr "" 41 | 42 | #. Name of a role 43 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 44 | msgid "Accounts Manager" 45 | msgstr "" 46 | 47 | #. Name of a role 48 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 49 | msgid "Accounts User" 50 | msgstr "" 51 | 52 | #. Label of a Check field in DocType 'DATEV Voucher Config' 53 | #: erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json 54 | msgid "Attach Files" 55 | msgstr "" 56 | 57 | #. Label of a Check field in DocType 'DATEV Voucher Config' 58 | #: erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json 59 | msgid "Attach Print" 60 | msgstr "" 61 | 62 | #: erpnext_datev/erpnext_datev/report/datev/datev.js:55 63 | msgid "Change DATEV Settings" 64 | msgstr "" 65 | 66 | #. Label of a Link field in DocType 'DATEV Settings' 67 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 68 | msgid "Client" 69 | msgstr "" 70 | 71 | #. Label of a Data field in DocType 'DATEV Settings' 72 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 73 | msgid "Client ID" 74 | msgstr "" 75 | 76 | #: erpnext_datev/erpnext_datev/report/datev/datev.js:5 77 | msgid "Company" 78 | msgstr "" 79 | 80 | #. Label of a Link field in DocType 'DATEV Settings' 81 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 82 | msgid "Consultant" 83 | msgstr "" 84 | 85 | #. Label of a Data field in DocType 'DATEV Settings' 86 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 87 | msgid "Consultant ID" 88 | msgstr "" 89 | 90 | #. Name of a report 91 | #: erpnext_datev/erpnext_datev/report/datev/datev.json 92 | msgid "DATEV" 93 | msgstr "" 94 | 95 | #. Name of a DocType 96 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 97 | msgid "DATEV Settings" 98 | msgstr "" 99 | 100 | #: erpnext_datev/erpnext_datev/report/datev/datev.js:40 101 | msgid "DATEV Settings for your Company are missing. Would you like to create them now?" 102 | msgstr "" 103 | 104 | #: erpnext_datev/erpnext_datev/report/datev/datev.py:187 105 | msgid "DATEV Settings missing" 106 | msgstr "" 107 | 108 | #. Name of a DocType 109 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.json 110 | msgid "DATEV Unternehmen Online Settings" 111 | msgstr "" 112 | 113 | #. Label of a Table field in DocType 'DATEV Unternehmen Online Settings' 114 | #. Name of a DocType 115 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.json 116 | #: erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json 117 | msgid "DATEV Voucher Config" 118 | msgstr "" 119 | 120 | #: erpnext_datev/erpnext_datev/report/datev/datev.py:197 121 | msgid "Dates {} and {} are not in the same fiscal year." 122 | msgstr "" 123 | 124 | #. Label of a Link field in DocType 'DATEV Unternehmen Online Settings' 125 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.json 126 | msgid "Default Print Language" 127 | msgstr "" 128 | 129 | #: erpnext_datev/erpnext_datev/report/datev/datev.js:48 130 | msgid "Download DATEV File" 131 | msgstr "" 132 | 133 | #: erpnext_datev/config/desktop.py:11 134 | msgid "ERPNext DATEV" 135 | msgstr "" 136 | 137 | #. Label of a Check field in DocType 'DATEV Unternehmen Online Settings' 138 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.json 139 | msgid "Enabled" 140 | msgstr "" 141 | 142 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.py:79 143 | msgid "Failed to attach XML to Sales Invoice PDF for DATEV" 144 | msgstr "" 145 | 146 | #: erpnext_datev/erpnext_datev/report/datev/datev.js:15 147 | msgid "From Date" 148 | msgstr "" 149 | 150 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.py:58 151 | msgid "New {0} {1} sent by the ERPNext-DATEV integration." 152 | msgstr "" 153 | 154 | #. Label of a Data field in DocType 'DATEV Settings' 155 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 156 | msgid "Opening Against Account Number" 157 | msgstr "" 158 | 159 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.py:25 160 | msgid "Opening Against Account Number must be {0} digits long" 161 | msgstr "" 162 | 163 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.py:16 164 | msgid "Please configure attachments for voucher type {}." 165 | msgstr "" 166 | 167 | #: erpnext_datev/erpnext_datev/report/datev/datev.py:186 168 | msgid "Please create DATEV Settings for Company {}" 169 | msgstr "" 170 | 171 | #. Label of a Link field in DocType 'DATEV Voucher Config' 172 | #: erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json 173 | msgid "Print Format" 174 | msgstr "" 175 | 176 | #. Label of a Data field in DocType 'DATEV Voucher Config' 177 | #: erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json 178 | msgid "Recipient" 179 | msgstr "" 180 | 181 | #. Description of the 'Attach Files' (Check) field in DocType 'DATEV Voucher 182 | #. Config' 183 | #: erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json 184 | msgid "Send files attached to the voucher" 185 | msgstr "" 186 | 187 | #. Description of the 'Attach Print' (Check) field in DocType 'DATEV Voucher 188 | #. Config' 189 | #: erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json 190 | msgid "Send the voucher printed as PDF" 191 | msgstr "" 192 | 193 | #. Label of a Link field in DocType 'DATEV Unternehmen Online Settings' 194 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.json 195 | msgid "Sender" 196 | msgstr "" 197 | 198 | #. Name of a role 199 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 200 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.json 201 | msgid "System Manager" 202 | msgstr "" 203 | 204 | #. Label of a Data field in DocType 'DATEV Settings' 205 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 206 | msgid "Temporary Against Account Number" 207 | msgstr "" 208 | 209 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.py:15 210 | msgid "Temporary Against Account Number must be {0} digits long" 211 | msgstr "" 212 | 213 | #: erpnext_datev/erpnext_datev/report/datev/datev.js:22 214 | msgid "To Date" 215 | msgstr "" 216 | 217 | #. Label of a Link field in DocType 'DATEV Voucher Config' 218 | #: erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json 219 | #: erpnext_datev/erpnext_datev/report/datev/datev.js:29 220 | msgid "Voucher Type" 221 | msgstr "" 222 | 223 | #. Description of the 'Temporary Against Account Number' (Data) field in 224 | #. DocType 'DATEV Settings' 225 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 226 | msgid "Will be used as against account for all normal ledger entries" 227 | msgstr "" 228 | 229 | #. Description of the 'Opening Against Account Number' (Data) field in DocType 230 | #. 'DATEV Settings' 231 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 232 | msgid "Will be used as against account for opening ledger entries" 233 | msgstr "" 234 | 235 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.py:50 236 | msgid "{} was not sent to DATEV because no attachments have been found." 237 | msgstr "" 238 | 239 | -------------------------------------------------------------------------------- /erpnext_datev/locale/de.po: -------------------------------------------------------------------------------- 1 | # Translations template for ERPNext DATEV Integration. 2 | # Copyright (C) 2024 ALYF GmbH 3 | # This file is distributed under the same license as the ERPNext DATEV Integration project. 4 | # FIRST AUTHOR , 2024. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: ERPNext DATEV Integration VERSION\n" 9 | "Report-Msgid-Bugs-To: hallo@alyf.de\n" 10 | "POT-Creation-Date: 2025-03-10 12:21+0053\n" 11 | "PO-Revision-Date: 2024-04-30 17:37+0053\n" 12 | "Last-Translator: hallo@alyf.de\n" 13 | "Language-Team: hallo@alyf.de\n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=utf-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Generated-By: Babel 2.13.1\n" 18 | 19 | #: erpnext_datev/erpnext_datev/report/datev/datev.py:173 20 | msgid "Company is a mandatory filter." 21 | msgstr "Unternehmen ist ein Pflichtfilter." 22 | 23 | #: erpnext_datev/erpnext_datev/report/datev/datev.py:177 24 | msgid "From Date is a mandatory filter." 25 | msgstr "Von Datum ist ein Pflichtfilter." 26 | 27 | #: erpnext_datev/erpnext_datev/report/datev/datev.py:181 28 | msgid "To Date is a mandatory filter." 29 | msgstr "Bis Datum ist ein Pflichtfilter." 30 | 31 | #. Description of the 'Default Print Language' (Link) field in DocType 'DATEV 32 | #. Unternehmen Online Settings' 33 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.json 34 | msgid "A document is printed according to its language field. If it doesn't have a language field or it is empty, the Default Print Language is used. If Default Print Language is empty, the language according to System Settings is used." 35 | msgstr "Ein Dokument wird entsprechend seinem Feld Sprache (language) gedruckt. Wenn das Feld Sprache nicht vorhanden oder leer ist, wird die Standarddrucksprache verwendet. Wenn das Feld Standarddrucksprache leer ist, wird die Sprache gemäß den Systemeinstellungen verwendet." 36 | 37 | #. Label of a Int field in DocType 'DATEV Settings' 38 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 39 | msgid "Account Number Length" 40 | msgstr "Kontonummerlänge" 41 | 42 | #. Name of a role 43 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 44 | msgid "Accounts Manager" 45 | msgstr "" 46 | 47 | #. Name of a role 48 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 49 | msgid "Accounts User" 50 | msgstr "" 51 | 52 | #. Label of a Check field in DocType 'DATEV Voucher Config' 53 | #: erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json 54 | msgid "Attach Files" 55 | msgstr "Anhänge anfügen" 56 | 57 | #. Label of a Check field in DocType 'DATEV Voucher Config' 58 | #: erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json 59 | msgid "Attach Print" 60 | msgstr "Druck anfügen" 61 | 62 | #: erpnext_datev/erpnext_datev/report/datev/datev.js:55 63 | msgid "Change DATEV Settings" 64 | msgstr "DATEV-Einstellungen ändern" 65 | 66 | #. Label of a Link field in DocType 'DATEV Settings' 67 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 68 | msgid "Client" 69 | msgstr "Mandant" 70 | 71 | #. Label of a Data field in DocType 'DATEV Settings' 72 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 73 | msgid "Client ID" 74 | msgstr "Mandantennummer" 75 | 76 | #: erpnext_datev/erpnext_datev/report/datev/datev.js:5 77 | msgid "Company" 78 | msgstr "Unternehmen" 79 | 80 | #. Label of a Link field in DocType 'DATEV Settings' 81 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 82 | msgid "Consultant" 83 | msgstr "Berater" 84 | 85 | #. Label of a Data field in DocType 'DATEV Settings' 86 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 87 | msgid "Consultant ID" 88 | msgstr "Beraternummer" 89 | 90 | #. Name of a report 91 | #: erpnext_datev/erpnext_datev/report/datev/datev.json 92 | msgid "DATEV" 93 | msgstr "DATEV" 94 | 95 | #. Name of a DocType 96 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 97 | msgid "DATEV Settings" 98 | msgstr "DATEV-Einstellungen" 99 | 100 | #: erpnext_datev/erpnext_datev/report/datev/datev.js:40 101 | msgid "DATEV Settings for your Company are missing. Would you like to create them now?" 102 | msgstr "DATEV-Einstellungen für Ihr Unternehmen fehlen. Möchten Sie diese jetzt erstellen?" 103 | 104 | #: erpnext_datev/erpnext_datev/report/datev/datev.py:187 105 | msgid "DATEV Settings missing" 106 | msgstr "DATEV-Einstellungen fehlen" 107 | 108 | #. Name of a DocType 109 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.json 110 | msgid "DATEV Unternehmen Online Settings" 111 | msgstr "DATEV Unternehmen Online-Einstellungen" 112 | 113 | #. Label of a Table field in DocType 'DATEV Unternehmen Online Settings' 114 | #. Name of a DocType 115 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.json 116 | #: erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json 117 | msgid "DATEV Voucher Config" 118 | msgstr "DATEV Belegkonfiguration" 119 | 120 | #: erpnext_datev/erpnext_datev/report/datev/datev.py:197 121 | msgid "Dates {} and {} are not in the same fiscal year." 122 | msgstr "Die Daten {} und {} liegen nicht im gleichen Geschäftsjahr." 123 | 124 | #. Label of a Link field in DocType 'DATEV Unternehmen Online Settings' 125 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.json 126 | msgid "Default Print Language" 127 | msgstr "Standarddrucksprache" 128 | 129 | #: erpnext_datev/erpnext_datev/report/datev/datev.js:48 130 | msgid "Download DATEV File" 131 | msgstr "DATEV-Datei herunterladen" 132 | 133 | #: erpnext_datev/config/desktop.py:11 134 | msgid "ERPNext DATEV" 135 | msgstr "ERPNext-DATEV" 136 | 137 | #. Label of a Check field in DocType 'DATEV Unternehmen Online Settings' 138 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.json 139 | msgid "Enabled" 140 | msgstr "Aktiviert" 141 | 142 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.py:79 143 | msgid "Failed to attach XML to Sales Invoice PDF for DATEV" 144 | msgstr "Fehler beim Anfügen der XML-Datei zu einem PDF-Druck für DATEV" 145 | 146 | #: erpnext_datev/erpnext_datev/report/datev/datev.js:15 147 | msgid "From Date" 148 | msgstr "Von Datum" 149 | 150 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.py:58 151 | msgid "New {0} {1} sent by the ERPNext-DATEV integration." 152 | msgstr "Neue {0} {1}, gesendet von der ERPNext-DATEV-Integration." 153 | 154 | #. Label of a Data field in DocType 'DATEV Settings' 155 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 156 | msgid "Opening Against Account Number" 157 | msgstr "Gegenkontonummer für Eröffnungsbuchungen" 158 | 159 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.py:25 160 | msgid "Opening Against Account Number must be {0} digits long" 161 | msgstr "Gegenkontonummer für Eröffnungsbuchungen muss {0} Ziffern lang sein" 162 | 163 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.py:16 164 | msgid "Please configure attachments for voucher type {}." 165 | msgstr "Bitte konfigurieren Sie Anhänge für Belegart {}." 166 | 167 | #: erpnext_datev/erpnext_datev/report/datev/datev.py:186 168 | msgid "Please create DATEV Settings for Company {}" 169 | msgstr "Bitte erstellen Sie DATEV-Einstellungen für Unternehmen {}" 170 | 171 | #. Label of a Link field in DocType 'DATEV Voucher Config' 172 | #: erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json 173 | msgid "Print Format" 174 | msgstr "" 175 | 176 | #. Label of a Data field in DocType 'DATEV Voucher Config' 177 | #: erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json 178 | msgid "Recipient" 179 | msgstr "Empfänger" 180 | 181 | #. Description of the 'Attach Files' (Check) field in DocType 'DATEV Voucher 182 | #. Config' 183 | #: erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json 184 | msgid "Send files attached to the voucher" 185 | msgstr "Dateien anhängen, die zu dem Beleg gehören" 186 | 187 | #. Description of the 'Attach Print' (Check) field in DocType 'DATEV Voucher 188 | #. Config' 189 | #: erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json 190 | msgid "Send the voucher printed as PDF" 191 | msgstr "Beleg als PDF senden" 192 | 193 | #. Label of a Link field in DocType 'DATEV Unternehmen Online Settings' 194 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.json 195 | msgid "Sender" 196 | msgstr "Absender" 197 | 198 | #. Name of a role 199 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 200 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.json 201 | msgid "System Manager" 202 | msgstr "" 203 | 204 | #. Label of a Data field in DocType 'DATEV Settings' 205 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 206 | msgid "Temporary Against Account Number" 207 | msgstr "Temporäre Gegenkontonummer" 208 | 209 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.py:15 210 | msgid "Temporary Against Account Number must be {0} digits long" 211 | msgstr "Temporäre Gegenkontonummer muss {0} Ziffern lang sein" 212 | 213 | #: erpnext_datev/erpnext_datev/report/datev/datev.js:22 214 | msgid "To Date" 215 | msgstr "Bis Datum" 216 | 217 | #. Label of a Link field in DocType 'DATEV Voucher Config' 218 | #: erpnext_datev/erpnext_datev/doctype/datev_voucher_config/datev_voucher_config.json 219 | #: erpnext_datev/erpnext_datev/report/datev/datev.js:29 220 | msgid "Voucher Type" 221 | msgstr "Belegart" 222 | 223 | #. Description of the 'Temporary Against Account Number' (Data) field in 224 | #. DocType 'DATEV Settings' 225 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 226 | msgid "Will be used as against account for all normal ledger entries" 227 | msgstr "Wird als Gegenkontonummer für alle normalen Buchungen verwendet" 228 | 229 | #. Description of the 'Opening Against Account Number' (Data) field in DocType 230 | #. 'DATEV Settings' 231 | #: erpnext_datev/erpnext_datev/doctype/datev_settings/datev_settings.json 232 | msgid "Will be used as against account for opening ledger entries" 233 | msgstr "Wird als Gegenkontonummer für Eröffnungsbuchungen verwendet" 234 | 235 | #: erpnext_datev/erpnext_datev/doctype/datev_unternehmen_online_settings/datev_unternehmen_online_settings.py:50 236 | msgid "{} was not sent to DATEV because no attachments have been found." 237 | msgstr "{} wurde nicht an DATEV gesendet weil keine Anhänge gefunden wurden." 238 | 239 | -------------------------------------------------------------------------------- /erpnext_datev/utils/datev_constants.py: -------------------------------------------------------------------------------- 1 | """Constants used in datev.py.""" 2 | 3 | TRANSACTION_COLUMNS = [ 4 | # All possible columns must tbe listed here, because DATEV requires them to 5 | # be present in the CSV. 6 | # --- 7 | # Umsatz 8 | "Umsatz (ohne Soll/Haben-Kz)", 9 | "Soll/Haben-Kennzeichen", 10 | "WKZ Umsatz", 11 | "Kurs", 12 | "Basis-Umsatz", 13 | "WKZ Basis-Umsatz", 14 | # Konto/Gegenkonto 15 | "Konto", 16 | "Gegenkonto (ohne BU-Schlüssel)", 17 | "BU-Schlüssel", 18 | # Datum 19 | "Belegdatum", 20 | # Rechnungs- / Belegnummer 21 | "Belegfeld 1", 22 | # z.B. Fälligkeitsdatum Format: TTMMJJ 23 | "Belegfeld 2", 24 | # Skonto-Betrag / -Abzug (Der Wert 0 ist unzulässig) 25 | "Skonto", 26 | # Beschreibung des Buchungssatzes 27 | "Buchungstext", 28 | # Mahn- / Zahl-Sperre (1 = Postensperre) 29 | "Postensperre", 30 | "Diverse Adressnummer", 31 | "Geschäftspartnerbank", 32 | "Sachverhalt", 33 | # Keine Mahnzinsen 34 | "Zinssperre", 35 | # Link auf den Buchungsbeleg (Programmkürzel + GUID) 36 | "Beleglink", 37 | # Beleginfo 38 | "Beleginfo - Art 1", 39 | "Beleginfo - Inhalt 1", 40 | "Beleginfo - Art 2", 41 | "Beleginfo - Inhalt 2", 42 | "Beleginfo - Art 3", 43 | "Beleginfo - Inhalt 3", 44 | "Beleginfo - Art 4", 45 | "Beleginfo - Inhalt 4", 46 | "Beleginfo - Art 5", 47 | "Beleginfo - Inhalt 5", 48 | "Beleginfo - Art 6", 49 | "Beleginfo - Inhalt 6", 50 | "Beleginfo - Art 7", 51 | "Beleginfo - Inhalt 7", 52 | "Beleginfo - Art 8", 53 | "Beleginfo - Inhalt 8", 54 | # Zuordnung des Geschäftsvorfalls für die Kostenrechnung 55 | "KOST1 - Kostenstelle", 56 | "KOST2 - Kostenstelle", 57 | "KOST-Menge", 58 | # USt-ID-Nummer (Beispiel: DE133546770) 59 | "EU-Mitgliedstaat u. USt-IdNr.", 60 | # Der im EU-Bestimmungsland gültige Steuersatz 61 | "EU-Steuersatz", 62 | # I = Ist-Versteuerung, 63 | # K = keine Umsatzsteuerrechnung 64 | # P = Pauschalierung (z. B. für Land- und Forstwirtschaft), 65 | # S = Soll-Versteuerung 66 | "Abw. Versteuerungsart", 67 | # Sachverhalte gem. § 13b Abs. 1 Satz 1 Nrn. 1.-5. UStG 68 | "Sachverhalt L+L", 69 | # Steuersatz / Funktion zum L+L-Sachverhalt (Beispiel: Wert 190 für 19%) 70 | "Funktionsergänzung L+L", 71 | # Bei Verwendung des BU-Schlüssels 49 für „andere Steuersätze“ muss der 72 | # steuerliche Sachverhalt mitgegeben werden 73 | "BU 49 Hauptfunktionstyp", 74 | "BU 49 Hauptfunktionsnummer", 75 | "BU 49 Funktionsergänzung", 76 | # Zusatzinformationen, besitzen den Charakter eines Notizzettels und können 77 | # frei erfasst werden. 78 | "Zusatzinformation - Art 1", 79 | "Zusatzinformation - Inhalt 1", 80 | "Zusatzinformation - Art 2", 81 | "Zusatzinformation - Inhalt 2", 82 | "Zusatzinformation - Art 3", 83 | "Zusatzinformation - Inhalt 3", 84 | "Zusatzinformation - Art 4", 85 | "Zusatzinformation - Inhalt 4", 86 | "Zusatzinformation - Art 5", 87 | "Zusatzinformation - Inhalt 5", 88 | "Zusatzinformation - Art 6", 89 | "Zusatzinformation - Inhalt 6", 90 | "Zusatzinformation - Art 7", 91 | "Zusatzinformation - Inhalt 7", 92 | "Zusatzinformation - Art 8", 93 | "Zusatzinformation - Inhalt 8", 94 | "Zusatzinformation - Art 9", 95 | "Zusatzinformation - Inhalt 9", 96 | "Zusatzinformation - Art 10", 97 | "Zusatzinformation - Inhalt 10", 98 | "Zusatzinformation - Art 11", 99 | "Zusatzinformation - Inhalt 11", 100 | "Zusatzinformation - Art 12", 101 | "Zusatzinformation - Inhalt 12", 102 | "Zusatzinformation - Art 13", 103 | "Zusatzinformation - Inhalt 13", 104 | "Zusatzinformation - Art 14", 105 | "Zusatzinformation - Inhalt 14", 106 | "Zusatzinformation - Art 15", 107 | "Zusatzinformation - Inhalt 15", 108 | "Zusatzinformation - Art 16", 109 | "Zusatzinformation - Inhalt 16", 110 | "Zusatzinformation - Art 17", 111 | "Zusatzinformation - Inhalt 17", 112 | "Zusatzinformation - Art 18", 113 | "Zusatzinformation - Inhalt 18", 114 | "Zusatzinformation - Art 19", 115 | "Zusatzinformation - Inhalt 19", 116 | "Zusatzinformation - Art 20", 117 | "Zusatzinformation - Inhalt 20", 118 | # Wirkt sich nur bei Sachverhalt mit SKR 14 Land- und Forstwirtschaft aus, 119 | # für andere SKR werden die Felder beim Import / Export überlesen bzw. 120 | # leer exportiert. 121 | "Stück", 122 | "Gewicht", 123 | # 1 = Lastschrift 124 | # 2 = Mahnung 125 | # 3 = Zahlung 126 | "Zahlweise", 127 | "Forderungsart", 128 | # JJJJ 129 | "Veranlagungsjahr", 130 | # TTMMJJJJ 131 | "Zugeordnete Fälligkeit", 132 | # 1 = Einkauf von Waren 133 | # 2 = Erwerb von Roh-Hilfs- und Betriebsstoffen 134 | "Skontotyp", 135 | # Allgemeine Bezeichnung, des Auftrags / Projekts. 136 | "Auftragsnummer", 137 | # AA = Angeforderte Anzahlung / Abschlagsrechnung 138 | # AG = Erhaltene Anzahlung (Geldeingang) 139 | # AV = Erhaltene Anzahlung (Verbindlichkeit) 140 | # SR = Schlussrechnung 141 | # SU = Schlussrechnung (Umbuchung) 142 | # SG = Schlussrechnung (Geldeingang) 143 | # SO = Sonstige 144 | "Buchungstyp", 145 | "USt-Schlüssel (Anzahlungen)", 146 | "EU-Mitgliedstaat (Anzahlungen)", 147 | "Sachverhalt L+L (Anzahlungen)", 148 | "EU-Steuersatz (Anzahlungen)", 149 | "Erlöskonto (Anzahlungen)", 150 | # Wird beim Import durch SV (Stapelverarbeitung) ersetzt. 151 | "Herkunft-Kz", 152 | # Wird von DATEV verwendet. 153 | "Leerfeld", 154 | # Format TTMMJJJJ 155 | "KOST-Datum", 156 | # Vom Zahlungsempfänger individuell vergebenes Kennzeichen eines Mandats 157 | # (z.B. Rechnungs- oder Kundennummer). 158 | "SEPA-Mandatsreferenz", 159 | # 1 = Skontosperre 160 | # 0 = Keine Skontosperre 161 | "Skontosperre", 162 | # Gesellschafter und Sonderbilanzsachverhalt 163 | "Gesellschaftername", 164 | # Amtliche Nummer aus der Feststellungserklärung 165 | "Beteiligtennummer", 166 | "Identifikationsnummer", 167 | "Zeichnernummer", 168 | # Format TTMMJJJJ 169 | "Postensperre bis", 170 | # Gesellschafter und Sonderbilanzsachverhalt 171 | "Bezeichnung SoBil-Sachverhalt", 172 | "Kennzeichen SoBil-Buchung", 173 | # 0 = keine Festschreibung 174 | # 1 = Festschreibung 175 | "Festschreibung", 176 | # Format TTMMJJJJ 177 | "Leistungsdatum", 178 | # Format TTMMJJJJ 179 | "Datum Zuord. Steuerperiode", 180 | # OPOS-Informationen, Format TTMMJJJJ 181 | "Fälligkeit", 182 | # G oder 1 = Generalumkehr 183 | # 0 = keine Generalumkehr 184 | "Generalumkehr (GU)", 185 | # Steuersatz für Steuerschlüssel 186 | "Steuersatz", 187 | # Beispiel: DE für Deutschland 188 | "Land", 189 | ] 190 | 191 | DEBTOR_CREDITOR_COLUMNS = [ 192 | # All possible columns must tbe listed here, because DATEV requires them to 193 | # be present in the CSV. 194 | # Columns "Leerfeld" have been replaced with "Leerfeld #" to not confuse pandas 195 | # --- 196 | "Konto", 197 | "Name (Adressatentyp Unternehmen)", 198 | "Unternehmensgegenstand", 199 | "Name (Adressatentyp natürl. Person)", 200 | "Vorname (Adressatentyp natürl. Person)", 201 | "Name (Adressatentyp keine Angabe)", 202 | "Adressatentyp", 203 | "Kurzbezeichnung", 204 | "EU-Land", 205 | "EU-USt-IdNr.", 206 | "Anrede", 207 | "Titel/Akad. Grad", 208 | "Adelstitel", 209 | "Namensvorsatz", 210 | "Adressart", 211 | "Straße", 212 | "Postfach", 213 | "Postleitzahl", 214 | "Ort", 215 | "Land", 216 | "Versandzusatz", 217 | "Adresszusatz", 218 | "Abweichende Anrede", 219 | "Abw. Zustellbezeichnung 1", 220 | "Abw. Zustellbezeichnung 2", 221 | "Kennz. Korrespondenzadresse", 222 | "Adresse gültig von", 223 | "Adresse gültig bis", 224 | "Telefon", 225 | "Bemerkung (Telefon)", 226 | "Telefon Geschäftsleitung", 227 | "Bemerkung (Telefon GL)", 228 | "E-Mail", 229 | "Bemerkung (E-Mail)", 230 | "Internet", 231 | "Bemerkung (Internet)", 232 | "Fax", 233 | "Bemerkung (Fax)", 234 | "Sonstige", 235 | "Bemerkung (Sonstige)", 236 | "Bankleitzahl 1", 237 | "Bankbezeichnung 1", 238 | "Bankkonto-Nummer 1", 239 | "Länderkennzeichen 1", 240 | "IBAN 1", 241 | "Leerfeld 1", 242 | "SWIFT-Code 1", 243 | "Abw. Kontoinhaber 1", 244 | "Kennz. Haupt-Bankverb. 1", 245 | "Bankverb. 1 Gültig von", 246 | "Bankverb. 1 Gültig bis", 247 | "Bankleitzahl 2", 248 | "Bankbezeichnung 2", 249 | "Bankkonto-Nummer 2", 250 | "Länderkennzeichen 2", 251 | "IBAN 2", 252 | "Leerfeld 2", 253 | "SWIFT-Code 2", 254 | "Abw. Kontoinhaber 2", 255 | "Kennz. Haupt-Bankverb. 2", 256 | "Bankverb. 2 gültig von", 257 | "Bankverb. 2 gültig bis", 258 | "Bankleitzahl 3", 259 | "Bankbezeichnung 3", 260 | "Bankkonto-Nummer 3", 261 | "Länderkennzeichen 3", 262 | "IBAN 3", 263 | "Leerfeld 3", 264 | "SWIFT-Code 3", 265 | "Abw. Kontoinhaber 3", 266 | "Kennz. Haupt-Bankverb. 3", 267 | "Bankverb. 3 gültig von", 268 | "Bankverb. 3 gültig bis", 269 | "Bankleitzahl 4", 270 | "Bankbezeichnung 4", 271 | "Bankkonto-Nummer 4", 272 | "Länderkennzeichen 4", 273 | "IBAN 4", 274 | "Leerfeld 4", 275 | "SWIFT-Code 4", 276 | "Abw. Kontoinhaber 4", 277 | "Kennz. Haupt-Bankverb. 4", 278 | "Bankverb. 4 Gültig von", 279 | "Bankverb. 4 Gültig bis", 280 | "Bankleitzahl 5", 281 | "Bankbezeichnung 5", 282 | "Bankkonto-Nummer 5", 283 | "Länderkennzeichen 5", 284 | "IBAN 5", 285 | "Leerfeld 5", 286 | "SWIFT-Code 5", 287 | "Abw. Kontoinhaber 5", 288 | "Kennz. Haupt-Bankverb. 5", 289 | "Bankverb. 5 gültig von", 290 | "Bankverb. 5 gültig bis", 291 | "Leerfeld 6", 292 | "Briefanrede", 293 | "Grußformel", 294 | "Kundennummer", 295 | "Steuernummer", 296 | "Sprache", 297 | "Ansprechpartner", 298 | "Vertreter", 299 | "Sachbearbeiter", 300 | "Diverse-Konto", 301 | "Ausgabeziel", 302 | "Währungssteuerung", 303 | "Kreditlimit (Debitor)", 304 | "Zahlungsbedingung", 305 | "Fälligkeit in Tagen (Debitor)", 306 | "Skonto in Prozent (Debitor)", 307 | "Kreditoren-Ziel 1 (Tage)", 308 | "Kreditoren-Skonto 1 (%)", 309 | "Kreditoren-Ziel 2 (Tage)", 310 | "Kreditoren-Skonto 2 (%)", 311 | "Kreditoren-Ziel 3 Brutto (Tage)", 312 | "Kreditoren-Ziel 4 (Tage)", 313 | "Kreditoren-Skonto 4 (%)", 314 | "Kreditoren-Ziel 5 (Tage)", 315 | "Kreditoren-Skonto 5 (%)", 316 | "Mahnung", 317 | "Kontoauszug", 318 | "Mahntext 1", 319 | "Mahntext 2", 320 | "Mahntext 3", 321 | "Kontoauszugstext", 322 | "Mahnlimit Betrag", 323 | "Mahnlimit %", 324 | "Zinsberechnung", 325 | "Mahnzinssatz 1", 326 | "Mahnzinssatz 2", 327 | "Mahnzinssatz 3", 328 | "Lastschrift", 329 | "Verfahren", 330 | "Mandantenbank", 331 | "Zahlungsträger", 332 | "Indiv. Feld 1", 333 | "Indiv. Feld 2", 334 | "Indiv. Feld 3", 335 | "Indiv. Feld 4", 336 | "Indiv. Feld 5", 337 | "Indiv. Feld 6", 338 | "Indiv. Feld 7", 339 | "Indiv. Feld 8", 340 | "Indiv. Feld 9", 341 | "Indiv. Feld 10", 342 | "Indiv. Feld 11", 343 | "Indiv. Feld 12", 344 | "Indiv. Feld 13", 345 | "Indiv. Feld 14", 346 | "Indiv. Feld 15", 347 | "Abweichende Anrede (Rechnungsadresse)", 348 | "Adressart (Rechnungsadresse)", 349 | "Straße (Rechnungsadresse)", 350 | "Postfach (Rechnungsadresse)", 351 | "Postleitzahl (Rechnungsadresse)", 352 | "Ort (Rechnungsadresse)", 353 | "Land (Rechnungsadresse)", 354 | "Versandzusatz (Rechnungsadresse)", 355 | "Adresszusatz (Rechnungsadresse)", 356 | "Abw. Zustellbezeichnung 1 (Rechnungsadresse)", 357 | "Abw. Zustellbezeichnung 2 (Rechnungsadresse)", 358 | "Adresse Gültig von (Rechnungsadresse)", 359 | "Adresse Gültig bis (Rechnungsadresse)", 360 | "Bankleitzahl 6", 361 | "Bankbezeichnung 6", 362 | "Bankkonto-Nummer 6", 363 | "Länderkennzeichen 6", 364 | "IBAN 6", 365 | "Leerfeld 7", 366 | "SWIFT-Code 6", 367 | "Abw. Kontoinhaber 6", 368 | "Kennz. Haupt-Bankverb. 6", 369 | "Bankverb 6 gültig von", 370 | "Bankverb 6 gültig bis", 371 | "Bankleitzahl 7", 372 | "Bankbezeichnung 7", 373 | "Bankkonto-Nummer 7", 374 | "Länderkennzeichen 7", 375 | "IBAN 7", 376 | "Leerfeld 8", 377 | "SWIFT-Code 7", 378 | "Abw. Kontoinhaber 7", 379 | "Kennz. Haupt-Bankverb. 7", 380 | "Bankverb 7 gültig von", 381 | "Bankverb 7 gültig bis", 382 | "Bankleitzahl 8", 383 | "Bankbezeichnung 8", 384 | "Bankkonto-Nummer 8", 385 | "Länderkennzeichen 8", 386 | "IBAN 8", 387 | "Leerfeld 9", 388 | "SWIFT-Code 8", 389 | "Abw. Kontoinhaber 8", 390 | "Kennz. Haupt-Bankverb. 8", 391 | "Bankverb 8 gültig von", 392 | "Bankverb 8 gültig bis", 393 | "Bankleitzahl 9", 394 | "Bankbezeichnung 9", 395 | "Bankkonto-Nummer 9", 396 | "Länderkennzeichen 9", 397 | "IBAN 9", 398 | "Leerfeld 10", 399 | "SWIFT-Code 9", 400 | "Abw. Kontoinhaber 9", 401 | "Kennz. Haupt-Bankverb. 9", 402 | "Bankverb 9 gültig von", 403 | "Bankverb 9 gültig bis", 404 | "Bankleitzahl 10", 405 | "Bankbezeichnung 10", 406 | "Bankkonto-Nummer 10", 407 | "Länderkennzeichen 10", 408 | "IBAN 10", 409 | "Leerfeld 11", 410 | "SWIFT-Code 10", 411 | "Abw. Kontoinhaber 10", 412 | "Kennz. Haupt-Bankverb. 10", 413 | "Bankverb 10 gültig von", 414 | "Bankverb 10 gültig bis", 415 | "Nummer Fremdsystem", 416 | "Insolvent", 417 | "SEPA-Mandatsreferenz 1", 418 | "SEPA-Mandatsreferenz 2", 419 | "SEPA-Mandatsreferenz 3", 420 | "SEPA-Mandatsreferenz 4", 421 | "SEPA-Mandatsreferenz 5", 422 | "SEPA-Mandatsreferenz 6", 423 | "SEPA-Mandatsreferenz 7", 424 | "SEPA-Mandatsreferenz 8", 425 | "SEPA-Mandatsreferenz 9", 426 | "SEPA-Mandatsreferenz 10", 427 | "Verknüpftes OPOS-Konto", 428 | "Mahnsperre bis", 429 | "Lastschriftsperre bis", 430 | "Zahlungssperre bis", 431 | "Gebührenberechnung", 432 | "Mahngebühr 1", 433 | "Mahngebühr 2", 434 | "Mahngebühr 3", 435 | "Pauschalberechnung", 436 | "Verzugspauschale 1", 437 | "Verzugspauschale 2", 438 | "Verzugspauschale 3", 439 | "Alternativer Suchname", 440 | "Status", 441 | "Anschrift manuell geändert (Korrespondenzadresse)", 442 | "Anschrift individuell (Korrespondenzadresse)", 443 | "Anschrift manuell geändert (Rechnungsadresse)", 444 | "Anschrift individuell (Rechnungsadresse)", 445 | "Fristberechnung bei Debitor", 446 | "Mahnfrist 1", 447 | "Mahnfrist 2", 448 | "Mahnfrist 3", 449 | "Letzte Frist", 450 | ] 451 | 452 | ACCOUNT_NAME_COLUMNS = [ 453 | # Account number 454 | "Konto", 455 | # Account name 456 | "Kontenbeschriftung", 457 | # Language of the account name 458 | # "de-DE" or "en-GB" 459 | "Sprach-ID", 460 | ] 461 | 462 | 463 | class DataCategory: 464 | """Field of the CSV Header.""" 465 | 466 | DEBTORS_CREDITORS = "16" 467 | ACCOUNT_NAMES = "20" 468 | TRANSACTIONS = "21" 469 | POSTING_TEXT_CONSTANTS = "67" 470 | 471 | 472 | class FormatName: 473 | """Field of the CSV Header, corresponds to DataCategory.""" 474 | 475 | DEBTORS_CREDITORS = "Debitoren/Kreditoren" 476 | ACCOUNT_NAMES = "Kontenbeschriftungen" 477 | TRANSACTIONS = "Buchungsstapel" 478 | POSTING_TEXT_CONSTANTS = "Buchungstextkonstanten" 479 | 480 | 481 | class Transactions: 482 | DATA_CATEGORY = DataCategory.TRANSACTIONS 483 | FORMAT_NAME = FormatName.TRANSACTIONS 484 | FORMAT_VERSION = "9" 485 | COLUMNS = TRANSACTION_COLUMNS 486 | 487 | 488 | class DebtorsCreditors: 489 | DATA_CATEGORY = DataCategory.DEBTORS_CREDITORS 490 | FORMAT_NAME = FormatName.DEBTORS_CREDITORS 491 | FORMAT_VERSION = "5" 492 | COLUMNS = DEBTOR_CREDITOR_COLUMNS 493 | 494 | 495 | class AccountNames: 496 | DATA_CATEGORY = DataCategory.ACCOUNT_NAMES 497 | FORMAT_NAME = FormatName.ACCOUNT_NAMES 498 | FORMAT_VERSION = "2" 499 | COLUMNS = ACCOUNT_NAME_COLUMNS 500 | -------------------------------------------------------------------------------- /erpnext_datev/erpnext_datev/report/datev/datev.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provide a report and downloadable CSV according to the German DATEV format. 3 | 4 | - Query report showing only the columns that contain data, formatted nicely for 5 | dispay to the user. 6 | - CSV download functionality `download_datev_csv` that provides a CSV file with 7 | all required columns. Used to import the data into the DATEV Software. 8 | """ 9 | 10 | import json 11 | 12 | import frappe 13 | from erpnext.accounts.utils import get_fiscal_year 14 | from frappe import _ 15 | 16 | from erpnext_datev.utils.datev_constants import ( 17 | AccountNames, 18 | DebtorsCreditors, 19 | Transactions, 20 | ) 21 | from erpnext_datev.utils.datev_csv import get_datev_csv, zip_and_download 22 | 23 | COLUMNS = [ 24 | { 25 | "label": "Umsatz (ohne Soll/Haben-Kz)", 26 | "fieldname": "Umsatz (ohne Soll/Haben-Kz)", 27 | "fieldtype": "Currency", 28 | "width": 100, 29 | }, 30 | { 31 | "label": "Soll/Haben-Kennzeichen", 32 | "fieldname": "Soll/Haben-Kennzeichen", 33 | "fieldtype": "Data", 34 | "width": 100, 35 | }, 36 | {"label": "Konto", "fieldname": "Konto", "fieldtype": "Data", "width": 100}, 37 | { 38 | "label": "Gegenkonto (ohne BU-Schlüssel)", 39 | "fieldname": "Gegenkonto (ohne BU-Schlüssel)", 40 | "fieldtype": "Data", 41 | "width": 100, 42 | }, 43 | { 44 | "label": "BU-Schlüssel", 45 | "fieldname": "BU-Schlüssel", 46 | "fieldtype": "Data", 47 | "width": 100, 48 | }, 49 | { 50 | "label": "Belegdatum", 51 | "fieldname": "Belegdatum", 52 | "fieldtype": "Date", 53 | "width": 100, 54 | }, 55 | { 56 | "label": "Belegfeld 1", 57 | "fieldname": "Belegfeld 1", 58 | "fieldtype": "Data", 59 | "width": 150, 60 | }, 61 | { 62 | "label": "Buchungstext", 63 | "fieldname": "Buchungstext", 64 | "fieldtype": "Text", 65 | "width": 300, 66 | }, 67 | { 68 | "label": "Beleginfo - Art 1", 69 | "fieldname": "Beleginfo - Art 1", 70 | "fieldtype": "Link", 71 | "options": "DocType", 72 | "width": 100, 73 | }, 74 | { 75 | "label": "Beleginfo - Inhalt 1", 76 | "fieldname": "Beleginfo - Inhalt 1", 77 | "fieldtype": "Dynamic Link", 78 | "options": "Beleginfo - Art 1", 79 | "width": 150, 80 | }, 81 | { 82 | "label": "Beleginfo - Art 2", 83 | "fieldname": "Beleginfo - Art 2", 84 | "fieldtype": "Link", 85 | "options": "DocType", 86 | "width": 100, 87 | }, 88 | { 89 | "label": "Beleginfo - Inhalt 2", 90 | "fieldname": "Beleginfo - Inhalt 2", 91 | "fieldtype": "Dynamic Link", 92 | "options": "Beleginfo - Art 2", 93 | "width": 150, 94 | }, 95 | { 96 | "label": "Beleginfo - Art 3", 97 | "fieldname": "Beleginfo - Art 3", 98 | "fieldtype": "Link", 99 | "options": "DocType", 100 | "width": 100, 101 | }, 102 | { 103 | "label": "Beleginfo - Inhalt 3", 104 | "fieldname": "Beleginfo - Inhalt 3", 105 | "fieldtype": "Dynamic Link", 106 | "options": "Beleginfo - Art 3", 107 | "width": 150, 108 | }, 109 | { 110 | "label": "Beleginfo - Art 4", 111 | "fieldname": "Beleginfo - Art 4", 112 | "fieldtype": "Data", 113 | "width": 100, 114 | }, 115 | { 116 | "label": "Beleginfo - Inhalt 4", 117 | "fieldname": "Beleginfo - Inhalt 4", 118 | "fieldtype": "Data", 119 | "width": 150, 120 | }, 121 | { 122 | "label": "Beleginfo - Art 5", 123 | "fieldname": "Beleginfo - Art 5", 124 | "fieldtype": "Data", 125 | "width": 150, 126 | }, 127 | { 128 | "label": "Beleginfo - Inhalt 5", 129 | "fieldname": "Beleginfo - Inhalt 5", 130 | "fieldtype": "Data", 131 | "width": 100, 132 | }, 133 | { 134 | "label": "Beleginfo - Art 6", 135 | "fieldname": "Beleginfo - Art 6", 136 | "fieldtype": "Data", 137 | "width": 150, 138 | }, 139 | { 140 | "label": "Beleginfo - Inhalt 6", 141 | "fieldname": "Beleginfo - Inhalt 6", 142 | "fieldtype": "Date", 143 | "width": 100, 144 | }, 145 | { 146 | "label": "Fälligkeit", 147 | "fieldname": "Fälligkeit", 148 | "fieldtype": "Date", 149 | "width": 100, 150 | }, 151 | ] 152 | 153 | 154 | def execute(filters=None): 155 | """Entry point for frappe.""" 156 | data = [] 157 | if filters and validate(filters): 158 | temp, opening = frappe.get_value( 159 | "DATEV Settings", 160 | filters.get("company"), 161 | ["temporary_against_account_number", "opening_against_account_number"], 162 | ) 163 | filters.update({"against_account": temp, "opening_account": opening or temp}) 164 | data = get_transactions(filters, as_dict=0) 165 | 166 | return COLUMNS, data 167 | 168 | 169 | def validate(filters): 170 | """Make sure all mandatory filters and settings are present.""" 171 | company = filters.get("company") 172 | if not company: 173 | frappe.throw(_("Company is a mandatory filter.")) 174 | 175 | from_date = filters.get("from_date") 176 | if not from_date: 177 | frappe.throw(_("From Date is a mandatory filter.")) 178 | 179 | to_date = filters.get("to_date") 180 | if not to_date: 181 | frappe.throw(_("To Date is a mandatory filter.")) 182 | 183 | validate_fiscal_year(from_date, to_date, company) 184 | 185 | if not frappe.db.exists("DATEV Settings", filters.get("company")): 186 | msg = _("Please create DATEV Settings for Company {}").format(filters.get("company")) 187 | frappe.log_error(message=msg, title=_("DATEV Settings missing")) 188 | return False 189 | 190 | return True 191 | 192 | 193 | def validate_fiscal_year(from_date, to_date, company): 194 | from_fiscal_year = get_fiscal_year(date=from_date, company=company) 195 | to_fiscal_year = get_fiscal_year(date=to_date, company=company) 196 | if from_fiscal_year != to_fiscal_year: 197 | frappe.throw(_("Dates {} and {} are not in the same fiscal year.").format(from_date, to_date)) 198 | 199 | 200 | def get_transactions(filters, as_dict=1): 201 | def run(params_method, filters): 202 | extra_fields, extra_joins, extra_filters = params_method(filters) 203 | return run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=as_dict) 204 | 205 | def sort_by(row): 206 | # "Belegdatum" is in the fifth column when list format is used 207 | return row["Belegdatum" if as_dict else 5] 208 | 209 | type_map = { 210 | # specific query methods for some voucher types 211 | "Payment Entry": get_payment_entry_params, 212 | "Sales Invoice": get_sales_invoice_params, 213 | "Purchase Invoice": get_purchase_invoice_params, 214 | } 215 | 216 | only_voucher_type = filters.get("voucher_type") 217 | transactions = [] 218 | 219 | for voucher_type, get_voucher_params in type_map.items(): 220 | if only_voucher_type and only_voucher_type != voucher_type: 221 | continue 222 | 223 | transactions.extend(run(params_method=get_voucher_params, filters=filters)) 224 | 225 | if not only_voucher_type or only_voucher_type not in type_map: 226 | # generic query method for all other voucher types 227 | filters["exclude_voucher_types"] = type_map.keys() 228 | transactions.extend(run(params_method=get_generic_params, filters=filters)) 229 | 230 | return sorted(transactions, key=sort_by) 231 | 232 | 233 | def get_payment_entry_params(filters): 234 | extra_fields = """ 235 | , 'Zahlungsreferenz' as 'Beleginfo - Art 5' 236 | , pe.reference_no as 'Beleginfo - Inhalt 5' 237 | , 'Buchungstag' as 'Beleginfo - Art 6' 238 | , pe.reference_date as 'Beleginfo - Inhalt 6' 239 | , '' as 'Fälligkeit' 240 | """ 241 | 242 | extra_joins = """ 243 | LEFT JOIN `tabPayment Entry` pe 244 | ON gl.voucher_no = pe.name 245 | """ 246 | 247 | extra_filters = """ 248 | AND gl.voucher_type = 'Payment Entry' 249 | """ 250 | 251 | return extra_fields, extra_joins, extra_filters 252 | 253 | 254 | def get_sales_invoice_params(filters): 255 | extra_fields = """ 256 | , '' as 'Beleginfo - Art 5' 257 | , '' as 'Beleginfo - Inhalt 5' 258 | , '' as 'Beleginfo - Art 6' 259 | , '' as 'Beleginfo - Inhalt 6' 260 | , si.due_date as 'Fälligkeit' 261 | """ 262 | 263 | extra_joins = """ 264 | LEFT JOIN `tabSales Invoice` si 265 | ON gl.voucher_no = si.name 266 | """ 267 | 268 | extra_filters = """ 269 | AND gl.voucher_type = 'Sales Invoice' 270 | """ 271 | 272 | return extra_fields, extra_joins, extra_filters 273 | 274 | 275 | def get_purchase_invoice_params(filters): 276 | extra_fields = """ 277 | , 'Lieferanten-Rechnungsnummer' as 'Beleginfo - Art 5' 278 | , pi.bill_no as 'Beleginfo - Inhalt 5' 279 | , 'Lieferanten-Rechnungsdatum' as 'Beleginfo - Art 6' 280 | , pi.bill_date as 'Beleginfo - Inhalt 6' 281 | , pi.due_date as 'Fälligkeit' 282 | """ 283 | 284 | extra_joins = """ 285 | LEFT JOIN `tabPurchase Invoice` pi 286 | ON gl.voucher_no = pi.name 287 | """ 288 | 289 | extra_filters = """ 290 | AND gl.voucher_type = 'Purchase Invoice' 291 | """ 292 | 293 | return extra_fields, extra_joins, extra_filters 294 | 295 | 296 | def get_generic_params(filters): 297 | # produce empty fields so all rows will have the same length 298 | extra_fields = """ 299 | , '' as 'Beleginfo - Art 5' 300 | , '' as 'Beleginfo - Inhalt 5' 301 | , '' as 'Beleginfo - Art 6' 302 | , '' as 'Beleginfo - Inhalt 6' 303 | , '' as 'Fälligkeit' 304 | """ 305 | extra_joins = "" 306 | 307 | if filters.get("exclude_voucher_types"): 308 | # exclude voucher types that are queried by a dedicated method 309 | exclude = "({})".format(", ".join("'{}'".format(key) for key in filters.get("exclude_voucher_types"))) 310 | extra_filters = "AND gl.voucher_type NOT IN {}".format(exclude) 311 | 312 | # if voucher type filter is set, allow only this type 313 | if filters.get("voucher_type"): 314 | extra_filters += " AND gl.voucher_type = %(voucher_type)s" 315 | 316 | return extra_fields, extra_joins, extra_filters 317 | 318 | 319 | def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1): 320 | """ 321 | Get a list of accounting entries. 322 | 323 | Select GL Entries joined with Account and Party Account in order to get the 324 | account numbers. Returns a list of accounting entries. 325 | 326 | Arguments: 327 | filters -- dict of filters to be passed to the sql query 328 | as_dict -- return as list of dicts [0,1] 329 | """ 330 | query = """ 331 | SELECT 332 | 333 | /* either debit or credit amount; always positive */ 334 | case ROUND(gl.debit, 2) when 0 then ROUND(gl.credit, 2) else ROUND(gl.debit, 2) end as 'Umsatz (ohne Soll/Haben-Kz)', 335 | 336 | /* 'H' when credit, 'S' when debit */ 337 | case ROUND(gl.debit, 2) when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen', 338 | 339 | /* account number or, if empty, party account number */ 340 | acc.account_number as 'Konto', 341 | 342 | /* against number or, if empty, party against number */ 343 | CASE gl.is_opening when 'Yes' then %(opening_account)s else %(against_account)s end as 'Gegenkonto (ohne BU-Schlüssel)', 344 | 345 | /* disable automatic VAT deduction */ 346 | '' as 'BU-Schlüssel', 347 | 348 | gl.posting_date as 'Belegdatum', 349 | gl.voucher_no as 'Belegfeld 1', 350 | REPLACE(LEFT(gl.remarks, 60), '\n', ' ') as 'Buchungstext', 351 | gl.voucher_type as 'Beleginfo - Art 1', 352 | gl.voucher_no as 'Beleginfo - Inhalt 1', 353 | gl.against_voucher_type as 'Beleginfo - Art 2', 354 | gl.against_voucher as 'Beleginfo - Inhalt 2', 355 | gl.party_type as 'Beleginfo - Art 3', 356 | gl.party as 'Beleginfo - Inhalt 3', 357 | case gl.party_type when 'Customer' then 'Debitorennummer' when 'Supplier' then 'Kreditorennummer' else NULL end as 'Beleginfo - Art 4', 358 | par.debtor_creditor_number as 'Beleginfo - Inhalt 4' 359 | 360 | {extra_fields} 361 | 362 | FROM `tabGL Entry` gl 363 | 364 | /* Kontonummer */ 365 | LEFT JOIN `tabAccount` acc 366 | ON gl.account = acc.name 367 | 368 | LEFT JOIN `tabParty Account` par 369 | ON par.parent = gl.party 370 | AND par.parenttype = gl.party_type 371 | AND par.company = %(company)s 372 | 373 | {extra_joins} 374 | 375 | WHERE gl.company = %(company)s 376 | AND DATE(gl.posting_date) >= %(from_date)s 377 | AND DATE(gl.posting_date) <= %(to_date)s 378 | 379 | {extra_filters} 380 | 381 | ORDER BY 'Belegdatum', gl.voucher_no""".format( 382 | extra_fields=extra_fields, extra_joins=extra_joins, extra_filters=extra_filters 383 | ) 384 | 385 | gl_entries = frappe.db.sql(query, filters, as_dict=as_dict) 386 | 387 | return gl_entries 388 | 389 | 390 | def get_customers(filters): 391 | """ 392 | Get a list of Customers. 393 | 394 | Arguments: 395 | filters -- dict of filters to be passed to the sql query 396 | """ 397 | return frappe.db.sql( 398 | """ 399 | SELECT 400 | 401 | par.debtor_creditor_number as 'Konto', 402 | CASE cus.customer_type 403 | WHEN 'Company' THEN cus.customer_name 404 | ELSE null 405 | END as 'Name (Adressatentyp Unternehmen)', 406 | CASE cus.customer_type 407 | WHEN 'Individual' THEN TRIM(SUBSTR(cus.customer_name, LOCATE(' ', cus.customer_name))) 408 | ELSE null 409 | END as 'Name (Adressatentyp natürl. Person)', 410 | CASE cus.customer_type 411 | WHEN 'Individual' THEN SUBSTRING_INDEX(SUBSTRING_INDEX(cus.customer_name, ' ', 1), ' ', -1) 412 | ELSE null 413 | END as 'Vorname (Adressatentyp natürl. Person)', 414 | CASE cus.customer_type 415 | WHEN 'Individual' THEN '1' 416 | WHEN 'Company' THEN '2' 417 | ELSE '0' 418 | END as 'Adressatentyp', 419 | adr.address_line1 as 'Straße', 420 | adr.pincode as 'Postleitzahl', 421 | adr.city as 'Ort', 422 | UPPER(country.code) as 'Land', 423 | adr.address_line2 as 'Adresszusatz', 424 | adr.email_id as 'E-Mail', 425 | adr.phone as 'Telefon', 426 | adr.fax as 'Fax', 427 | cus.website as 'Internet', 428 | cus.tax_id as 'Steuernummer' 429 | 430 | FROM `tabCustomer` cus 431 | 432 | left join `tabParty Account` par 433 | on par.parent = cus.name 434 | and par.parenttype = 'Customer' 435 | and par.company = %(company)s 436 | 437 | left join `tabDynamic Link` dyn_adr 438 | on dyn_adr.link_name = cus.name 439 | and dyn_adr.link_doctype = 'Customer' 440 | and dyn_adr.parenttype = 'Address' 441 | 442 | left join `tabAddress` adr 443 | on adr.name = dyn_adr.parent 444 | and adr.is_primary_address = '1' 445 | 446 | left join `tabCountry` country 447 | on country.name = adr.country 448 | 449 | WHERE adr.is_primary_address = '1' 450 | """, 451 | filters, 452 | as_dict=1, 453 | ) 454 | 455 | 456 | def get_suppliers(filters): 457 | """ 458 | Get a list of Suppliers. 459 | 460 | Arguments: 461 | filters -- dict of filters to be passed to the sql query 462 | """ 463 | return frappe.db.sql( 464 | """ 465 | SELECT 466 | 467 | par.debtor_creditor_number as 'Konto', 468 | CASE sup.supplier_type 469 | WHEN 'Company' THEN sup.supplier_name 470 | ELSE null 471 | END as 'Name (Adressatentyp Unternehmen)', 472 | CASE sup.supplier_type 473 | WHEN 'Individual' THEN TRIM(SUBSTR(sup.supplier_name, LOCATE(' ', sup.supplier_name))) 474 | ELSE null 475 | END as 'Name (Adressatentyp natürl. Person)', 476 | CASE sup.supplier_type 477 | WHEN 'Individual' THEN SUBSTRING_INDEX(SUBSTRING_INDEX(sup.supplier_name, ' ', 1), ' ', -1) 478 | ELSE null 479 | END as 'Vorname (Adressatentyp natürl. Person)', 480 | CASE sup.supplier_type 481 | WHEN 'Individual' THEN '1' 482 | WHEN 'Company' THEN '2' 483 | ELSE '0' 484 | END as 'Adressatentyp', 485 | adr.address_line1 as 'Straße', 486 | adr.pincode as 'Postleitzahl', 487 | adr.city as 'Ort', 488 | UPPER(country.code) as 'Land', 489 | adr.address_line2 as 'Adresszusatz', 490 | adr.email_id as 'E-Mail', 491 | adr.phone as 'Telefon', 492 | adr.fax as 'Fax', 493 | sup.website as 'Internet', 494 | sup.tax_id as 'Steuernummer', 495 | case sup.on_hold when 1 then sup.release_date else null end as 'Zahlungssperre bis' 496 | 497 | FROM `tabSupplier` sup 498 | 499 | left join `tabParty Account` par 500 | on par.parent = sup.name 501 | and par.parenttype = 'Supplier' 502 | and par.company = %(company)s 503 | 504 | left join `tabDynamic Link` dyn_adr 505 | on dyn_adr.link_name = sup.name 506 | and dyn_adr.link_doctype = 'Supplier' 507 | and dyn_adr.parenttype = 'Address' 508 | 509 | left join `tabAddress` adr 510 | on adr.name = dyn_adr.parent 511 | and adr.is_primary_address = '1' 512 | 513 | left join `tabCountry` country 514 | on country.name = adr.country 515 | 516 | WHERE adr.is_primary_address = '1' 517 | """, 518 | filters, 519 | as_dict=1, 520 | ) 521 | 522 | 523 | def get_account_names(filters): 524 | return frappe.db.sql( 525 | """ 526 | SELECT 527 | 528 | account_number as 'Konto', 529 | LEFT(account_name, 40) as 'Kontenbeschriftung', 530 | 'de-DE' as 'Sprach-ID' 531 | 532 | FROM `tabAccount` 533 | WHERE company = %(company)s 534 | AND is_group = 0 535 | AND account_number != '' 536 | """, 537 | filters, 538 | as_dict=1, 539 | ) 540 | 541 | 542 | @frappe.whitelist() 543 | def download_datev_csv(filters): 544 | """ 545 | Provide accounting entries for download in DATEV format. 546 | 547 | Validate the filters, get the data, produce the CSV file and provide it for 548 | download. Can be called like this: 549 | 550 | GET /api/method/erpnext_datev.erpnext_datev.report.datev.datev.download_datev_csv 551 | 552 | Arguments / Params: 553 | filters -- dict of filters to be passed to the sql query 554 | """ 555 | frappe.only_for(["Accounts User", "Accounts Manager"]) 556 | 557 | if isinstance(filters, str): 558 | filters = json.loads(filters) 559 | 560 | validate(filters) 561 | 562 | company = filters.get("company") 563 | fiscal_year = get_fiscal_year(date=filters.get("from_date"), company=company) 564 | coa = frappe.get_value("Company", company, "chart_of_accounts") 565 | datev_settings = frappe.get_doc("DATEV Settings", company) 566 | 567 | filters.update( 568 | { 569 | "fiscal_year_start": fiscal_year[1], 570 | "skr": "04" if "SKR04" in coa else ("03" if "SKR03" in coa else ""), 571 | "account_number_length": datev_settings.account_number_length, 572 | "against_account": datev_settings.temporary_against_account_number, 573 | "opening_account": datev_settings.opening_against_account_number 574 | or datev_settings.temporary_against_account_number, 575 | } 576 | ) 577 | 578 | transactions = get_transactions(filters) 579 | account_names = get_account_names(filters) 580 | customers = get_customers(filters) 581 | suppliers = get_suppliers(filters) 582 | 583 | zip_name = "{} DATEV.zip".format(frappe.utils.datetime.date.today()) 584 | zip_and_download( 585 | zip_name, 586 | [ 587 | { 588 | "file_name": "EXTF_Buchungsstapel.csv", 589 | "csv_data": get_datev_csv(transactions, filters, csv_class=Transactions), 590 | }, 591 | { 592 | "file_name": "EXTF_Kontenbeschriftungen.csv", 593 | "csv_data": get_datev_csv(account_names, filters, csv_class=AccountNames), 594 | }, 595 | { 596 | "file_name": "EXTF_Kunden.csv", 597 | "csv_data": get_datev_csv(customers, filters, csv_class=DebtorsCreditors), 598 | }, 599 | { 600 | "file_name": "EXTF_Lieferanten.csv", 601 | "csv_data": get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors), 602 | }, 603 | ], 604 | ) 605 | --------------------------------------------------------------------------------