refactor: move datev-specific stuff to utils
diff --git a/erpnext/regional/germany/utils/__init__.py b/erpnext/regional/germany/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/regional/germany/utils/__init__.py
diff --git a/erpnext/regional/germany/utils/datev/__init__.py b/erpnext/regional/germany/utils/datev/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/regional/germany/utils/datev/__init__.py
diff --git a/erpnext/regional/report/datev/datev_constants.py b/erpnext/regional/germany/utils/datev/datev_constants.py
similarity index 89%
rename from erpnext/regional/report/datev/datev_constants.py
rename to erpnext/regional/germany/utils/datev/datev_constants.py
index e063703..88dc02d 100644
--- a/erpnext/regional/report/datev/datev_constants.py
+++ b/erpnext/regional/germany/utils/datev/datev_constants.py
@@ -460,79 +460,6 @@
"Sprach-ID"
]
-QUERY_REPORT_COLUMNS = [
- {
- "label": "Umsatz (ohne Soll/Haben-Kz)",
- "fieldname": "Umsatz (ohne Soll/Haben-Kz)",
- "fieldtype": "Currency",
- "width": 100
- },
- {
- "label": "Soll/Haben-Kennzeichen",
- "fieldname": "Soll/Haben-Kennzeichen",
- "fieldtype": "Data",
- "width": 100
- },
- {
- "label": "Konto",
- "fieldname": "Konto",
- "fieldtype": "Data",
- "width": 100
- },
- {
- "label": "Gegenkonto (ohne BU-Schlüssel)",
- "fieldname": "Gegenkonto (ohne BU-Schlüssel)",
- "fieldtype": "Data",
- "width": 100
- },
- {
- "label": "Belegdatum",
- "fieldname": "Belegdatum",
- "fieldtype": "Date",
- "width": 100
- },
- {
- "label": "Belegfeld 1",
- "fieldname": "Belegfeld 1",
- "fieldtype": "Data",
- "width": 150
- },
- {
- "label": "Buchungstext",
- "fieldname": "Buchungstext",
- "fieldtype": "Text",
- "width": 300
- },
- {
- "label": "Beleginfo - Art 1",
- "fieldname": "Beleginfo - Art 1",
- "fieldtype": "Link",
- "options": "DocType",
- "width": 100
- },
- {
- "label": "Beleginfo - Inhalt 1",
- "fieldname": "Beleginfo - Inhalt 1",
- "fieldtype": "Dynamic Link",
- "options": "Beleginfo - Art 1",
- "width": 150
- },
- {
- "label": "Beleginfo - Art 2",
- "fieldname": "Beleginfo - Art 2",
- "fieldtype": "Link",
- "options": "DocType",
- "width": 100
- },
- {
- "label": "Beleginfo - Inhalt 2",
- "fieldname": "Beleginfo - Inhalt 2",
- "fieldtype": "Dynamic Link",
- "options": "Beleginfo - Art 2",
- "width": 150
- }
-]
-
class DataCategory():
"""Field of the CSV Header."""
diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py
new file mode 100644
index 0000000..be41a4a
--- /dev/null
+++ b/erpnext/regional/germany/utils/datev/datev_csv.py
@@ -0,0 +1,176 @@
+# coding: utf-8
+from __future__ import unicode_literals
+
+import datetime
+import zipfile
+import six
+import frappe
+import pandas as pd
+from frappe import _
+from csv import QUOTE_NONNUMERIC
+from six import BytesIO
+from six import string_types
+from .datev_constants import DataCategory
+from .datev_constants import Transactions
+from .datev_constants import AccountNames
+
+
+def get_datev_csv(data, filters, csv_class):
+ """
+ Fill in missing columns and return a CSV in DATEV Format.
+
+ For automatic processing, DATEV requires the first line of the CSV file to
+ hold meta data such as the length of account numbers oder the category of
+ the data.
+
+ Arguments:
+ data -- array of dictionaries
+ filters -- dict
+ csv_class -- defines DATA_CATEGORY, FORMAT_NAME and COLUMNS
+ """
+ empty_df = pd.DataFrame(columns=csv_class.COLUMNS)
+ data_df = pd.DataFrame.from_records(data)
+ result = empty_df.append(data_df, sort=True)
+
+ if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS:
+ result['Belegdatum'] = pd.to_datetime(result['Belegdatum'])
+
+ if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES:
+ result['Sprach-ID'] = 'de-DE'
+
+ data = result.to_csv(
+ # Reason for str(';'): https://github.com/pandas-dev/pandas/issues/6035
+ sep=str(';'),
+ # European decimal seperator
+ decimal=',',
+ # Windows "ANSI" encoding
+ encoding='latin_1',
+ # format date as DDMM
+ date_format='%d%m',
+ # Windows line terminator
+ line_terminator='\r\n',
+ # Do not number rows
+ index=False,
+ # Use all columns defined above
+ columns=csv_class.COLUMNS,
+ # Quote most fields, even currency values with "," separator
+ quoting=QUOTE_NONNUMERIC
+ )
+
+ if not six.PY2:
+ data = data.encode('latin_1')
+
+ header = get_header(filters, csv_class)
+ header = ';'.join(header).encode('latin_1')
+
+ # 1st Row: Header with meta data
+ # 2nd Row: Data heading (Überschrift der Nutzdaten), included in `data` here.
+ # 3rd - nth Row: Data (Nutzdaten)
+ return header + b'\r\n' + data
+
+
+def get_header(filters, csv_class):
+ description = filters.get('voucher_type', csv_class.FORMAT_NAME)
+ company = filters.get('company')
+ datev_settings = frappe.get_doc('DATEV Settings', {'client': company})
+ default_currency = frappe.get_value('Company', company, 'default_currency')
+ coa = frappe.get_value('Company', company, 'chart_of_accounts')
+ coa_short_code = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '')
+
+ header = [
+ # DATEV format
+ # "DTVF" = created by DATEV software,
+ # "EXTF" = created by other software
+ '"EXTF"',
+ # version of the DATEV format
+ # 141 = 1.41,
+ # 510 = 5.10,
+ # 720 = 7.20
+ '700',
+ csv_class.DATA_CATEGORY,
+ '"%s"' % csv_class.FORMAT_NAME,
+ # Format version (regarding format name)
+ csv_class.FORMAT_VERSION,
+ # Generated on
+ datetime.datetime.now().strftime("%Y%m%d%H%M%S") + '000',
+ # Imported on -- stays empty
+ '',
+ # Origin. Any two symbols, will be replaced by "SV" on import.
+ '"EN"',
+ # I = Exported by
+ '"%s"' % frappe.session.user,
+ # J = Imported by -- stays empty
+ '',
+ # K = Tax consultant number (Beraternummer)
+ datev_settings.get('consultant_number', '0000000'),
+ # L = Tax client number (Mandantennummer)
+ datev_settings.get('client_number', '00000'),
+ # M = Start of the fiscal year (Wirtschaftsjahresbeginn)
+ frappe.utils.formatdate(frappe.defaults.get_user_default('year_start_date'), 'yyyyMMdd'),
+ # N = Length of account numbers (Sachkontenlänge)
+ datev_settings.get('account_number_length', '4'),
+ # O = Transaction batch start date (YYYYMMDD)
+ frappe.utils.formatdate(filters.get('from_date'), 'yyyyMMdd') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
+ # P = Transaction batch end date (YYYYMMDD)
+ frappe.utils.formatdate(filters.get('to_date'), 'yyyyMMdd') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
+ # Q = Description (for example, "Sales Invoice") Max. 30 chars
+ '"{}"'.format(_(description)) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
+ # R = Diktatkürzel
+ '',
+ # S = Buchungstyp
+ # 1 = Transaction batch (Finanzbuchführung),
+ # 2 = Annual financial statement (Jahresabschluss)
+ '1' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
+ # T = Rechnungslegungszweck
+ # 0 oder leer = vom Rechnungslegungszweck unabhängig
+ # 50 = Handelsrecht
+ # 30 = Steuerrecht
+ # 64 = IFRS
+ # 40 = Kalkulatorik
+ # 11 = Reserviert
+ # 12 = Reserviert
+ '0' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
+ # U = Festschreibung
+ # TODO: Filter by Accounting Period. In export for closed Accounting Period, this will be "1"
+ '0',
+ # V = Default currency, for example, "EUR"
+ '"%s"' % default_currency if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
+ # reserviert
+ '',
+ # Derivatskennzeichen
+ '',
+ # reserviert
+ '',
+ # reserviert
+ '',
+ # SKR
+ '"%s"' % coa_short_code,
+ # Branchen-Lösungs-ID
+ '',
+ # reserviert
+ '',
+ # reserviert
+ '',
+ # Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung)
+ ''
+ ]
+ return header
+
+
+def download_csv_files_as_zip(csv_data_list):
+ """
+ Put CSV files in a zip archive and send that to the client.
+
+ Params:
+ csv_data_list -- list of dicts [{'file_name': 'EXTF_Buchunsstapel.zip', 'csv_data': get_datev_csv()}]
+ """
+ zip_buffer = BytesIO()
+
+ datev_zip = zipfile.ZipFile(zip_buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
+ for csv_file in csv_data_list:
+ datev_zip.writestr(csv_file.get('file_name'), csv_file.get('csv_data'))
+ datev_zip.close()
+
+ frappe.response['filecontent'] = zip_buffer.getvalue()
+ frappe.response['filename'] = 'DATEV.zip'
+ frappe.response['type'] = 'binary'
diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py
index 7fec94e..97d93d3 100644
--- a/erpnext/regional/report/datev/datev.py
+++ b/erpnext/regional/report/datev/datev.py
@@ -9,31 +9,92 @@
"""
from __future__ import unicode_literals
-import datetime
import json
-import zipfile
import six
import frappe
-import pandas as pd
-
from frappe import _
-from csv import QUOTE_NONNUMERIC
-from six import BytesIO
from six import string_types
-from .datev_constants import DataCategory
-from .datev_constants import Transactions
-from .datev_constants import DebtorsCreditors
-from .datev_constants import AccountNames
-from .datev_constants import QUERY_REPORT_COLUMNS
+from erpnext.regional.germany.utils.datev.datev_csv import download_csv_files_as_zip, get_datev_csv
+from erpnext.regional.germany.utils.datev.datev_constants import Transactions, DebtorsCreditors, AccountNames
+
+COLUMNS = [
+ {
+ "label": "Umsatz (ohne Soll/Haben-Kz)",
+ "fieldname": "Umsatz (ohne Soll/Haben-Kz)",
+ "fieldtype": "Currency",
+ "width": 100
+ },
+ {
+ "label": "Soll/Haben-Kennzeichen",
+ "fieldname": "Soll/Haben-Kennzeichen",
+ "fieldtype": "Data",
+ "width": 100
+ },
+ {
+ "label": "Konto",
+ "fieldname": "Konto",
+ "fieldtype": "Data",
+ "width": 100
+ },
+ {
+ "label": "Gegenkonto (ohne BU-Schlüssel)",
+ "fieldname": "Gegenkonto (ohne BU-Schlüssel)",
+ "fieldtype": "Data",
+ "width": 100
+ },
+ {
+ "label": "Belegdatum",
+ "fieldname": "Belegdatum",
+ "fieldtype": "Date",
+ "width": 100
+ },
+ {
+ "label": "Belegfeld 1",
+ "fieldname": "Belegfeld 1",
+ "fieldtype": "Data",
+ "width": 150
+ },
+ {
+ "label": "Buchungstext",
+ "fieldname": "Buchungstext",
+ "fieldtype": "Text",
+ "width": 300
+ },
+ {
+ "label": "Beleginfo - Art 1",
+ "fieldname": "Beleginfo - Art 1",
+ "fieldtype": "Link",
+ "options": "DocType",
+ "width": 100
+ },
+ {
+ "label": "Beleginfo - Inhalt 1",
+ "fieldname": "Beleginfo - Inhalt 1",
+ "fieldtype": "Dynamic Link",
+ "options": "Beleginfo - Art 1",
+ "width": 150
+ },
+ {
+ "label": "Beleginfo - Art 2",
+ "fieldname": "Beleginfo - Art 2",
+ "fieldtype": "Link",
+ "options": "DocType",
+ "width": 100
+ },
+ {
+ "label": "Beleginfo - Inhalt 2",
+ "fieldname": "Beleginfo - Inhalt 2",
+ "fieldtype": "Dynamic Link",
+ "options": "Beleginfo - Art 2",
+ "width": 150
+ }
+]
def execute(filters=None):
"""Entry point for frappe."""
validate(filters)
- result = get_transactions(filters, as_dict=0)
- columns = QUERY_REPORT_COLUMNS
-
- return columns, result
+ return COLUMNS, get_transactions(filters, as_dict=0)
def validate(filters):
@@ -240,146 +301,8 @@
""", filters, as_dict=1)
-def get_datev_csv(data, filters, csv_class):
- """
- Fill in missing columns and return a CSV in DATEV Format.
-
- For automatic processing, DATEV requires the first line of the CSV file to
- hold meta data such as the length of account numbers oder the category of
- the data.
-
- Arguments:
- data -- array of dictionaries
- filters -- dict
- csv_class -- defines DATA_CATEGORY, FORMAT_NAME and COLUMNS
- """
- empty_df = pd.DataFrame(columns=csv_class.COLUMNS)
- data_df = pd.DataFrame.from_records(data)
-
- result = empty_df.append(data_df, sort=True)
-
- if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS:
- result['Belegdatum'] = pd.to_datetime(result['Belegdatum'])
-
- if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES:
- result['Sprach-ID'] = 'de-DE'
-
- data = result.to_csv(
- # Reason for str(';'): https://github.com/pandas-dev/pandas/issues/6035
- sep=str(';'),
- # European decimal seperator
- decimal=',',
- # Windows "ANSI" encoding
- encoding='latin_1',
- # format date as DDMM
- date_format='%d%m',
- # Windows line terminator
- line_terminator='\r\n',
- # Do not number rows
- index=False,
- # Use all columns defined above
- columns=csv_class.COLUMNS,
- # Quote most fields, even currency values with "," separator
- quoting=QUOTE_NONNUMERIC
- )
-
- if not six.PY2:
- data = data.encode('latin_1')
-
- header = get_header(filters, csv_class)
- header = ';'.join(header).encode('latin_1')
-
- # 1st Row: Header with meta data
- # 2nd Row: Data heading (Überschrift der Nutzdaten), included in `data` here.
- # 3rd - nth Row: Data (Nutzdaten)
- return header + b'\r\n' + data
-
-
-def get_header(filters, csv_class):
- description = filters.get('voucher_type', csv_class.FORMAT_NAME)
-
- header = [
- # DATEV format
- # "DTVF" = created by DATEV software,
- # "EXTF" = created by other software
- '"EXTF"',
- # version of the DATEV format
- # 141 = 1.41,
- # 510 = 5.10,
- # 720 = 7.20
- '700',
- csv_class.DATA_CATEGORY,
- '"%s"' % csv_class.FORMAT_NAME,
- # Format version (regarding format name)
- csv_class.FORMAT_VERSION,
- # Generated on
- datetime.datetime.now().strftime("%Y%m%d%H%M%S") + '000',
- # Imported on -- stays empty
- '',
- # Origin. Any two symbols, will be replaced by "SV" on import.
- '"EN"',
- # I = Exported by
- '"%s"' % frappe.session.user,
- # J = Imported by -- stays empty
- '',
- # K = Tax consultant number (Beraternummer)
- filters.get('consultant_number', '0000000'),
- # L = Tax client number (Mandantennummer)
- filters.get('client_number', '00000'),
- # M = Start of the fiscal year (Wirtschaftsjahresbeginn)
- frappe.utils.formatdate(frappe.defaults.get_user_default("year_start_date"), "yyyyMMdd"),
- # N = Length of account numbers (Sachkontenlänge)
- '%d' % filters.get('acc_len', 4),
- # O = Transaction batch start date (YYYYMMDD)
- frappe.utils.formatdate(filters.get('from_date'), "yyyyMMdd") if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
- # P = Transaction batch end date (YYYYMMDD)
- frappe.utils.formatdate(filters.get('to_date'), "yyyyMMdd") if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
- # Q = Description (for example, "Sales Invoice") Max. 30 chars
- '"{}"'.format(_(description)) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
- # R = Diktatkürzel
- '',
- # S = Buchungstyp
- # 1 = Transaction batch (Finanzbuchführung),
- # 2 = Annual financial statement (Jahresabschluss)
- '1' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
- # T = Rechnungslegungszweck
- # 0 oder leer = vom Rechnungslegungszweck unabhängig
- # 50 = Handelsrecht
- # 30 = Steuerrecht
- # 64 = IFRS
- # 40 = Kalkulatorik
- # 11 = Reserviert
- # 12 = Reserviert
- '0' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
- # U = Festschreibung
- # TODO: Filter by Accounting Period. In export for closed Accounting Period, this will be "1"
- '0',
- # V = Default currency, for example, "EUR"
- '"%s"' % filters.get('default_currency', 'EUR') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
- # reserviert
- '',
- # Derivatskennzeichen
- '',
- # reserviert
- '',
- # reserviert
- '',
- # SKR
- '"%s"' % filters.get('skr', '04'),
- # Branchen-Lösungs-ID
- '',
- # reserviert
- '',
- # reserviert
- '',
- # Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung)
- ''
- ]
- return header
-
-
@frappe.whitelist()
-def download_datev_csv(filters=None):
+def download_datev_csv(filters):
"""
Provide accounting entries for download in DATEV format.
@@ -400,38 +323,26 @@
coa = frappe.get_value('Company', filters.get('company'), 'chart_of_accounts')
filters['skr'] = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '')
- # set account number length
- account_numbers = frappe.get_list('Account', fields=['account_number'], filters={'is_group': 0, 'account_number': ('!=', '')})
- filters['acc_len'] = max([len(a.account_number) for a in account_numbers])
-
- filters['consultant_number'] = frappe.get_value('DATEV Settings', filters.get('company'), 'consultant_number')
- filters['client_number'] = frappe.get_value('DATEV Settings', filters.get('company'), 'client_number')
- filters['default_currency'] = frappe.get_value('Company', filters.get('company'), 'default_currency')
-
- # This is where my zip will be written
- zip_buffer = BytesIO()
- # This is my zip file
- datev_zip = zipfile.ZipFile(zip_buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
-
transactions = get_transactions(filters)
- transactions_csv = get_datev_csv(transactions, filters, csv_class=Transactions)
- datev_zip.writestr('EXTF_Buchungsstapel.csv', transactions_csv)
-
account_names = get_account_names(filters)
- account_names_csv = get_datev_csv(account_names, filters, csv_class=AccountNames)
- datev_zip.writestr('EXTF_Kontenbeschriftungen.csv', account_names_csv)
-
customers = get_customers(filters)
- customers_csv = get_datev_csv(customers, filters, csv_class=DebtorsCreditors)
- datev_zip.writestr('EXTF_Kunden.csv', customers_csv)
-
suppliers = get_suppliers(filters)
- suppliers_csv = get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors)
- datev_zip.writestr('EXTF_Lieferanten.csv', suppliers_csv)
-
- # You must call close() before exiting your program or essential records will not be written.
- datev_zip.close()
- frappe.response['filecontent'] = zip_buffer.getvalue()
- frappe.response['filename'] = 'DATEV.zip'
- frappe.response['type'] = 'binary'
+ download_csv_files_as_zip([
+ {
+ 'file_name': 'EXTF_Buchungsstapel.csv',
+ 'csv_data': get_datev_csv(transactions, filters, csv_class=Transactions)
+ },
+ {
+ 'file_name': 'EXTF_Kontenbeschriftungen.csv',
+ 'csv_data': get_datev_csv(account_names, filters, csv_class=AccountNames)
+ },
+ {
+ 'file_name': 'EXTF_Kunden.csv',
+ 'csv_data': get_datev_csv(customers, filters, csv_class=DebtorsCreditors)
+ },
+ {
+ 'file_name': 'EXTF_Lieferanten.csv',
+ 'csv_data': get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors)
+ },
+ ])
diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py
index eed62a8..9529923 100644
--- a/erpnext/regional/report/datev/test_datev.py
+++ b/erpnext/regional/report/datev/test_datev.py
@@ -1,32 +1,22 @@
# coding=utf-8
from __future__ import unicode_literals
-import os
-import json
import zipfile
+import frappe
from six import BytesIO
from unittest import TestCase
-
-import frappe
-from frappe.utils import getdate, today, now_datetime, cstr
-from frappe.test_runner import make_test_objects
+from frappe.utils import today, now_datetime, cstr
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
-from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
from erpnext.regional.report.datev.datev import validate
from erpnext.regional.report.datev.datev import get_transactions
from erpnext.regional.report.datev.datev import get_customers
from erpnext.regional.report.datev.datev import get_suppliers
from erpnext.regional.report.datev.datev import get_account_names
-from erpnext.regional.report.datev.datev import get_datev_csv
-from erpnext.regional.report.datev.datev import get_header
from erpnext.regional.report.datev.datev import download_datev_csv
-from erpnext.regional.report.datev.datev_constants import DataCategory
-from erpnext.regional.report.datev.datev_constants import Transactions
-from erpnext.regional.report.datev.datev_constants import DebtorsCreditors
-from erpnext.regional.report.datev.datev_constants import AccountNames
-from erpnext.regional.report.datev.datev_constants import QUERY_REPORT_COLUMNS
+from erpnext.regional.germany.utils.datev.datev_csv import get_datev_csv, get_header
+from erpnext.regional.germany.utils.datev.datev_constants import Transactions, DebtorsCreditors, AccountNames
def make_company(company_name, abbr):
if not frappe.db.exists("Company", company_name):