From 9c923ae418ec3d7d1a7ac91ba3f48d18d63d0965 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Thu, 25 Apr 2019 01:09:56 +0200 Subject: [PATCH 01/12] feat(regional): Report for german tax consultants (DATEV) --- erpnext/regional/report/datev/__init__.py | 0 erpnext/regional/report/datev/datev.js | 26 ++++ erpnext/regional/report/datev/datev.json | 29 +++++ erpnext/regional/report/datev/datev.py | 137 ++++++++++++++++++++++ 4 files changed, 192 insertions(+) create mode 100644 erpnext/regional/report/datev/__init__.py create mode 100644 erpnext/regional/report/datev/datev.js create mode 100644 erpnext/regional/report/datev/datev.json create mode 100644 erpnext/regional/report/datev/datev.py diff --git a/erpnext/regional/report/datev/__init__.py b/erpnext/regional/report/datev/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js new file mode 100644 index 00000000000..737a84aef24 --- /dev/null +++ b/erpnext/regional/report/datev/datev.js @@ -0,0 +1,26 @@ +frappe.query_reports["DATEV"] = { + "filters": [ + { + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname": "fiscal_year", + "label": __("Fiscal Year"), + "fieldtype": "Link", + "options": "Fiscal Year", + "default": frappe.defaults.get_user_default("fiscal_year"), + "reqd": 1 + } + ], + onload: function(query_report) { + query_report.export_report = function() { + const filters = JSON.stringify(query_report.get_values()); + window.open(`/api/method/erpnext.regional.report.datev.datev.download_datev_csv?filters=${filters}`); + }; + } +}; diff --git a/erpnext/regional/report/datev/datev.json b/erpnext/regional/report/datev/datev.json new file mode 100644 index 00000000000..d4f44b6fbb6 --- /dev/null +++ b/erpnext/regional/report/datev/datev.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "apply_user_permissions": 0, + "creation": "2019-04-24 08:45:16.650129", + "disabled": 0, + "icon": "octicon octicon-repo-pull", + "color": "#96cf41", + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "module": "Regional", + "name": "DATEV", + "owner": "Administrator", + "ref_doctype": "GL Entry", + "report_name": "DATEV", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Auditor" + } + ] +} diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py new file mode 100644 index 00000000000..268de2874d7 --- /dev/null +++ b/erpnext/regional/report/datev/datev.py @@ -0,0 +1,137 @@ +# coding: utf-8 +from __future__ import unicode_literals +import json +from six import string_types +import frappe +from frappe.utils import format_datetime +from frappe import _ + + +def execute(filters=None): + validate_filters(filters) + result = get_gl_entries(filters, as_dict=0) + columns = get_columns() + + return columns, result + + +def validate_filters(filters): + if not filters.get('company'): + frappe.throw(_('{0} is mandatory').format(_('Company'))) + + if not filters.get('fiscal_year'): + frappe.throw(_('{0} is mandatory').format(_('Fiscal Year'))) + + +def get_columns(): + columns = [ + { + "label": "Umsatz (ohne Soll/Haben-Kz)", + "fieldname": "umsatz", + "fieldtype": "Currency", + }, + { + "label": "Soll/Haben-Kennzeichen", + "fieldname": "soll_haben_kennzeichen", + "fieldtype": "Data", + }, + { + "label": "Kontonummer", + "fieldname": "kontonummer", + "fieldtype": "Data", + }, + { + "label": "Gegenkonto (ohne BU-Schlüssel)", + "fieldname": "gegenkonto_nummer", + "fieldtype": "Data", + }, + { + "label": "Belegdatum", + "fieldname": "belegdatum", + "fieldtype": "Date", + } + ] + + return columns + + +def get_gl_entries(filters, as_dict): + gl_entries = frappe.db.sql(""" + select + + case gl.debit when 0 then gl.credit else gl.debit end as Umsatz, + case gl.debit when 0 then 'H' else 'S' end as Kennzeichen, + coalesce(acc.account_number, acc_pa.account_number) as Kontonummer, + coalesce(acc_against.account_number, acc_against_pa.account_number) as Gegenkonto, + gl.posting_date as Belegdatum + + from `tabGL Entry` gl + + /* Statistisches Konto (Debitoren/Kreditoren) */ + left join `tabParty Account` pa + on gl.against = pa.parent + + /* Kontonummer */ + left join `tabAccount` acc + on gl.account = acc.name + + /* Gegenkonto-Nummer */ + left join `tabAccount` acc_against + on gl.against = acc_against.name + + /* Statistische Kontonummer */ + left join `tabAccount` acc_pa + on pa.account = acc_pa.name + + /* Statistische Gegenkonto-Nummer */ + left join `tabAccount` acc_against_pa + on pa.account = acc_against_pa.name + + where gl.company=%(company)s and gl.fiscal_year=%(fiscal_year)s + order by 'Belegdatum:Date', voucher_no""", filters, as_dict=as_dict) + + return gl_entries + + +def get_datev_csv(data): + title_row = [ + "Umsatz (ohne Soll/Haben-Kz)", + "Soll/Haben-Kennzeichen", + "Kontonummer", + "Gegenkonto (ohne BU-Schlüssel)", + "Belegdatum" + ] + + result = ['"' + '";"'.join(title_row) + '"'] + result += [ + ';'.join( + [ + '{:.2f}'.format(d.get('Umsatz')).replace('.', ','), + '"{}"'.format(d.get('Kennzeichen')), + d.get('Kontonummer'), + # Can be empty, if there are no debtor / creditor accounts + d.get('Gegenkonto') or '', + format_datetime(d.get('Belegdatum'), 'ddMMyyyy') + ] + ) for d in data + ] + + return b'\r\n'.join(result).encode(encoding='latin_1') + + +@frappe.whitelist() +def download_datev_csv(filters=None): + if isinstance(filters, string_types): + filters = json.loads(filters) + + validate_filters(filters) + data = get_gl_entries(filters, as_dict=1) + + filename = 'DATEV_Buchungsstapel_{}-{}'.format( + filters.get('company'), + filters.get('fiscal_year') + ) + + frappe.response['result'] = get_datev_csv(data) + frappe.response['doctype'] = filename + frappe.response['type'] = 'csv' From e5f7af9e9f1703b53159f046526470a2439f49f0 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Thu, 9 May 2019 03:54:49 +0200 Subject: [PATCH 02/12] Use pandas, more columns, filter by date --- erpnext/regional/report/datev/datev.js | 14 ++- erpnext/regional/report/datev/datev.py | 149 ++++++++++++++++++++----- 2 files changed, 128 insertions(+), 35 deletions(-) diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js index 737a84aef24..a556199a046 100644 --- a/erpnext/regional/report/datev/datev.js +++ b/erpnext/regional/report/datev/datev.js @@ -9,11 +9,15 @@ frappe.query_reports["DATEV"] = { "reqd": 1 }, { - "fieldname": "fiscal_year", - "label": __("Fiscal Year"), - "fieldtype": "Link", - "options": "Fiscal Year", - "default": frappe.defaults.get_user_default("fiscal_year"), + "fieldname": "from_date", + "label": __("From Date"), + "fieldtype": "Date", + "reqd": 1 + }, + { + "fieldname": "to_date", + "label": __("To Date"), + "fieldtype": "Date", "reqd": 1 } ], diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 268de2874d7..5209f34528e 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -5,6 +5,7 @@ from six import string_types import frappe from frappe.utils import format_datetime from frappe import _ +import pandas as pd def execute(filters=None): @@ -19,35 +20,38 @@ def validate_filters(filters): if not filters.get('company'): frappe.throw(_('{0} is mandatory').format(_('Company'))) - if not filters.get('fiscal_year'): - frappe.throw(_('{0} is mandatory').format(_('Fiscal Year'))) + if not filters.get('from_date'): + frappe.throw(_('{0} is mandatory').format(_('From Date'))) + + if not filters.get('to_date'): + frappe.throw(_('{0} is mandatory').format(_('To Date'))) def get_columns(): columns = [ { "label": "Umsatz (ohne Soll/Haben-Kz)", - "fieldname": "umsatz", + "fieldname": "Umsatz (ohne Soll/Haben-Kz)", "fieldtype": "Currency", }, { "label": "Soll/Haben-Kennzeichen", - "fieldname": "soll_haben_kennzeichen", + "fieldname": "Soll/Haben-Kennzeichen", "fieldtype": "Data", }, { "label": "Kontonummer", - "fieldname": "kontonummer", + "fieldname": "Kontonummer", "fieldtype": "Data", }, { "label": "Gegenkonto (ohne BU-Schlüssel)", - "fieldname": "gegenkonto_nummer", + "fieldname": "Gegenkonto (ohne BU-Schlüssel)", "fieldtype": "Data", }, { "label": "Belegdatum", - "fieldname": "belegdatum", + "fieldname": "Belegdatum", "fieldtype": "Date", } ] @@ -59,11 +63,11 @@ def get_gl_entries(filters, as_dict): gl_entries = frappe.db.sql(""" select - case gl.debit when 0 then gl.credit else gl.debit end as Umsatz, - case gl.debit when 0 then 'H' else 'S' end as Kennzeichen, - coalesce(acc.account_number, acc_pa.account_number) as Kontonummer, - coalesce(acc_against.account_number, acc_against_pa.account_number) as Gegenkonto, - gl.posting_date as Belegdatum + case gl.debit when 0 then gl.credit else gl.debit end as 'Umsatz (ohne Soll/Haben-Kz)', + case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen', + coalesce(acc.account_number, acc_pa.account_number) as 'Kontonummer', + coalesce(acc_against.account_number, acc_against_pa.account_number) as 'Gegenkonto (ohne BU-Schlüssel)', + gl.posting_date as 'Belegdatum' from `tabGL Entry` gl @@ -87,36 +91,121 @@ def get_gl_entries(filters, as_dict): left join `tabAccount` acc_against_pa on pa.account = acc_against_pa.name - where gl.company=%(company)s and gl.fiscal_year=%(fiscal_year)s - order by 'Belegdatum:Date', voucher_no""", filters, as_dict=as_dict) + where gl.company = %(company)s + and DATE(gl.posting_date) >= %(from_date)s + and DATE(gl.posting_date) <= %(to_date)s + order by 'Belegdatum', gl.voucher_no""", filters, as_dict=as_dict) return gl_entries def get_datev_csv(data): - title_row = [ + columns = [ + # Umsatz "Umsatz (ohne Soll/Haben-Kz)", "Soll/Haben-Kennzeichen", + "WKZ Umsatz", + "Kurs", + "Basis-Umsatz", + "WKZ Basis-Umsatz", + # Konto/Gegenkonto "Kontonummer", "Gegenkonto (ohne BU-Schlüssel)", - "Belegdatum" + "BU-Schlüssel", + # Datum + "Belegdatum", + # Belegfelder + "Belegfeld 1", + "Belegfeld 2", + # Weitere Felder + "Skonto", + "Buchungstext", + # OPOS-Informationen + "Postensperre", + "Diverse Adressnummer", + "Geschäftspartnerbank", + "Sachverhalt", + "Zinssperre", + # Digitaler Beleg + "Beleglink", + # Kostenrechnung + "Kost 1 - Kostenstelle", + "Kost 2 - Kostenstelle", + "Kost-Menge", + # Steuerrechnung + "EU-Land u. UStID", + "EU-Steuersatz", + "Abw. Versteuerungsart", + # L+L Sachverhalt + "Sachverhalt L+L", + "FunktionsergänzungL+L", + # Mengenfelder LuF + "Stück", + "Gewicht", + # Forderungsart + "Zahlweise", + "Forderungsart", + "Veranlagungsjahr", + "Zugeordnete Fälligkeit", + # Weitere Felder + "Skontotyp", + # Anzahlungen + "Auftragsnummer", + "Buchungstyp", + "USt-Schlüssel (Anzahlungen)", + "EU-Land (Anzahlungen)", + "Sachverhalt L+L (Anzahlungen)", + "EU-Steuersatz (Anzahlungen)", + "Erlöskonto (Anzahlungen)", + # Stapelinformationen + "Herkunft-Kz", + # Technische Identifikation + "Buchungs GUID", + # Kostenrechnung + "Kost-Datum", + # OPOS-Informationen + "SEPA-Mandatsreferenz", + "Skontosperre", + # Gesellschafter und Sonderbilanzsachverhalt + "Gesellschaftername", + "Beteiligtennummer", + "Identifikationsnummer", + "Zeichnernummer", + # OPOS-Informationen + "Postensperre bis", + # Gesellschafter und Sonderbilanzsachverhalt + "Bezeichnung SoBil-Sachverhalt", + "Kennzeichen SoBil-Buchung", + # Stapelinformationen + "Festschreibung", + # Datum + "Leistungsdatum", + "Datum Zuord. Steuerperiode", + # OPOS-Informationen + "Fälligkeit", + # Konto/Gegenkonto + "Generalumkehr (GU)", + # Steuersatz für Steuerschlüssel + "Steuersatz", + "Land" ] - result = ['"' + '";"'.join(title_row) + '"'] - result += [ - ';'.join( - [ - '{:.2f}'.format(d.get('Umsatz')).replace('.', ','), - '"{}"'.format(d.get('Kennzeichen')), - d.get('Kontonummer'), - # Can be empty, if there are no debtor / creditor accounts - d.get('Gegenkonto') or '', - format_datetime(d.get('Belegdatum'), 'ddMMyyyy') - ] - ) for d in data - ] + empty_df = pd.DataFrame(columns=columns) - return b'\r\n'.join(result).encode(encoding='latin_1') + data_df = pd.DataFrame.from_records(data) + data_df["Belegdatum"] = pd.to_datetime(data_df["Belegdatum"]) + + result = empty_df.append(data_df) + + return result.to_csv( + sep=b';', + decimal=',', + encoding='latin_1', + date_format='%d%m', + line_terminator=b'\r\n', + index=False, + columns=columns + ) @frappe.whitelist() From 4b2901704cf4fa613c7b050fb0c18f32b001b0c8 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Thu, 9 May 2019 04:03:23 +0200 Subject: [PATCH 03/12] Add download button --- erpnext/regional/report/datev/datev.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js index a556199a046..781862c2a6a 100644 --- a/erpnext/regional/report/datev/datev.js +++ b/erpnext/regional/report/datev/datev.js @@ -22,9 +22,9 @@ frappe.query_reports["DATEV"] = { } ], onload: function(query_report) { - query_report.export_report = function() { + query_report.page.add_inner_button("Download DATEV Export", () => { const filters = JSON.stringify(query_report.get_values()); window.open(`/api/method/erpnext.regional.report.datev.datev.download_datev_csv?filters=${filters}`); - }; + }); } }; From 9eef60754da9c27527a21bbd627b0e1bbe56fc2d Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Sun, 12 May 2019 02:24:39 +0200 Subject: [PATCH 04/12] remove unused import --- erpnext/regional/report/datev/datev.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 5209f34528e..e6d1cbfbf38 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import json from six import string_types import frappe -from frappe.utils import format_datetime from frappe import _ import pandas as pd From 2701f944911d98b2a6e829392814f98928d10980 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Tue, 21 May 2019 21:58:35 +0200 Subject: [PATCH 05/12] add remarks and voucher info --- erpnext/regional/report/datev/datev.py | 37 +++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index e6d1cbfbf38..4a6e0ef27e1 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -52,6 +52,31 @@ def get_columns(): "label": "Belegdatum", "fieldname": "Belegdatum", "fieldtype": "Date", + }, + { + "label": "Buchungstext", + "fieldname": "Buchungstext", + "fieldtype": "Text", + }, + { + "label": "Beleginfo - Art 1", + "fieldname": "Beleginfo - Art 1", + "fieldtype": "Data", + }, + { + "label": "Beleginfo - Inhalt 1", + "fieldname": "Beleginfo - Inhalt 1", + "fieldtype": "Data", + }, + { + "label": "Beleginfo - Art 2", + "fieldname": "Beleginfo - Art 2", + "fieldtype": "Data", + }, + { + "label": "Beleginfo - Inhalt 2", + "fieldname": "Beleginfo - Inhalt 2", + "fieldtype": "Data", } ] @@ -66,7 +91,12 @@ def get_gl_entries(filters, as_dict): case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen', coalesce(acc.account_number, acc_pa.account_number) as 'Kontonummer', coalesce(acc_against.account_number, acc_against_pa.account_number) as 'Gegenkonto (ohne BU-Schlüssel)', - gl.posting_date as 'Belegdatum' + gl.posting_date as 'Belegdatum', + gl.remarks as 'Buchungstext', + gl.voucher_type as 'Beleginfo - Art 1', + gl.voucher_no as 'Beleginfo - Inhalt 1', + gl.against_voucher_type as 'Beleginfo - Art 2', + gl.against_voucher as 'Beleginfo - Inhalt 2' from `tabGL Entry` gl @@ -127,6 +157,11 @@ def get_datev_csv(data): "Zinssperre", # Digitaler Beleg "Beleglink", + # Beleginfo + "Beleginfo - Art 1", + "Beleginfo - Inhalt 1", + "Beleginfo - Art 2", + "Beleginfo - Inhalt 2", # Kostenrechnung "Kost 1 - Kostenstelle", "Kost 2 - Kostenstelle", From 50de88417c1ce6dd08fee257a5144c043aa160b4 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Tue, 21 May 2019 21:59:04 +0200 Subject: [PATCH 06/12] fix filename --- erpnext/regional/report/datev/datev.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 4a6e0ef27e1..2012fa21bff 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -250,9 +250,10 @@ def download_datev_csv(filters=None): validate_filters(filters) data = get_gl_entries(filters, as_dict=1) - filename = 'DATEV_Buchungsstapel_{}-{}'.format( + filename = 'DATEV_Buchungsstapel_{}-{}_bis_{}'.format( filters.get('company'), - filters.get('fiscal_year') + filters.get('from_date'), + filters.get('to_date') ) frappe.response['result'] = get_datev_csv(data) From ce9239af83b519b7aad810b407acac587f022b02 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Tue, 21 May 2019 22:49:59 +0200 Subject: [PATCH 07/12] fix error when downloading empty report --- erpnext/regional/report/datev/datev.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 2012fa21bff..8b05ad0878c 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -225,11 +225,10 @@ def get_datev_csv(data): ] empty_df = pd.DataFrame(columns=columns) - data_df = pd.DataFrame.from_records(data) - data_df["Belegdatum"] = pd.to_datetime(data_df["Belegdatum"]) result = empty_df.append(data_df) + result["Belegdatum"] = pd.to_datetime(result["Belegdatum"]) return result.to_csv( sep=b';', From 31f505528705f7b1a40749cacb273a7341340ec6 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Wed, 22 May 2019 00:03:16 +0200 Subject: [PATCH 08/12] fix sql for multi-company case --- erpnext/regional/report/datev/datev.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 8b05ad0878c..84bd52926b8 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -103,6 +103,7 @@ def get_gl_entries(filters, as_dict): /* Statistisches Konto (Debitoren/Kreditoren) */ left join `tabParty Account` pa on gl.against = pa.parent + and gl.company = pa.company /* Kontonummer */ left join `tabAccount` acc From 9638f0ef2b1234bf84207b2c892cb49ffde2a7a0 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Mon, 27 May 2019 14:42:52 +0200 Subject: [PATCH 09/12] Add columns to CSV, add comments and docstrings --- erpnext/regional/report/datev/datev.py | 122 ++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 84bd52926b8..6ca1fe54c85 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -1,4 +1,12 @@ # coding: utf-8 +""" +Provide a report and downloadable CSV according to the German DATEV format. + +- Query report showing only the columns that contain data, formatted nicely for + dispay to the user. +- CSV download functionality `download_datev_csv` that provides a CSV file with + all required columns. Used to import the data into the DATEV Software. +""" from __future__ import unicode_literals import json from six import string_types @@ -8,6 +16,7 @@ import pandas as pd def execute(filters=None): + """Entry point for frappe.""" validate_filters(filters) result = get_gl_entries(filters, as_dict=0) columns = get_columns() @@ -16,6 +25,7 @@ def execute(filters=None): def validate_filters(filters): + """Make sure all mandatory filters are present.""" if not filters.get('company'): frappe.throw(_('{0} is mandatory').format(_('Company'))) @@ -27,6 +37,7 @@ def validate_filters(filters): def get_columns(): + """Return the list of columns that will be shown in query report.""" columns = [ { "label": "Umsatz (ohne Soll/Haben-Kz)", @@ -84,13 +95,31 @@ def get_columns(): def get_gl_entries(filters, as_dict): + """ + Get a list of accounting entries. + + Select GL Entries joined with Account and Party Account in order to get the + account numbers. Returns a list of accounting entries. + + Arguments: + filters -- dict of filters to be passed to the sql query + as_dict -- return as list of dicts [0,1] + """ gl_entries = frappe.db.sql(""" select + /* either debit or credit amount; always positive */ case gl.debit when 0 then gl.credit else gl.debit end as 'Umsatz (ohne Soll/Haben-Kz)', + + /* 'H' when credit, 'S' when debit */ case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen', + + /* account number or, if empty, party account number */ coalesce(acc.account_number, acc_pa.account_number) as 'Kontonummer', + + /* against number or, if empty, party against number */ coalesce(acc_against.account_number, acc_against_pa.account_number) as 'Gegenkonto (ohne BU-Schlüssel)', + gl.posting_date as 'Belegdatum', gl.remarks as 'Buchungstext', gl.voucher_type as 'Beleginfo - Art 1', @@ -130,7 +159,16 @@ def get_gl_entries(filters, as_dict): def get_datev_csv(data): + """ + Fill in missing columns and return a CSV in DATEV Format. + + Arguments: + data -- array of dictionaries + """ columns = [ + # All possible columns must tbe listed here, because DATEV requires them to + # be present in the CSV. + # --- # Umsatz "Umsatz (ohne Soll/Haben-Kz)", "Soll/Haben-Kennzeichen", @@ -159,10 +197,22 @@ def get_datev_csv(data): # Digitaler Beleg "Beleglink", # Beleginfo - "Beleginfo - Art 1", - "Beleginfo - Inhalt 1", - "Beleginfo - Art 2", - "Beleginfo - Inhalt 2", + "Beleginfo – Art 1", + "Beleginfo – Inhalt 1", + "Beleginfo – Art 2", + "Beleginfo – Inhalt 2", + "Beleginfo – Art 3", + "Beleginfo – Inhalt 3", + "Beleginfo – Art 4", + "Beleginfo – Inhalt 4", + "Beleginfo – Art 5", + "Beleginfo – Inhalt 5", + "Beleginfo – Art 6", + "Beleginfo – Inhalt 6", + "Beleginfo – Art 7", + "Beleginfo – Inhalt 7", + "Beleginfo – Art 8", + "Beleginfo – Inhalt 8", # Kostenrechnung "Kost 1 - Kostenstelle", "Kost 2 - Kostenstelle", @@ -173,7 +223,52 @@ def get_datev_csv(data): "Abw. Versteuerungsart", # L+L Sachverhalt "Sachverhalt L+L", - "FunktionsergänzungL+L", + "Funktionsergänzung L+L", + # Funktion Steuerschlüssel 49 + "BU 49 Hauptfunktionstyp", + "BU 49 Hauptfunktionsnummer", + "BU 49 Funktionsergänzung", + # Zusatzinformationen + "Zusatzinformation – Art 1", + "Zusatzinformation – Inhalt 1", + "Zusatzinformation – Art 2", + "Zusatzinformation – Inhalt 2", + "Zusatzinformation – Art 3", + "Zusatzinformation – Inhalt 3", + "Zusatzinformation – Art 4", + "Zusatzinformation – Inhalt 4", + "Zusatzinformation – Art 5", + "Zusatzinformation – Inhalt 5", + "Zusatzinformation – Art 6", + "Zusatzinformation – Inhalt 6", + "Zusatzinformation – Art 7", + "Zusatzinformation – Inhalt 7", + "Zusatzinformation – Art 8", + "Zusatzinformation – Inhalt 8", + "Zusatzinformation – Art 9", + "Zusatzinformation – Inhalt 9", + "Zusatzinformation – Art 10", + "Zusatzinformation – Inhalt 10", + "Zusatzinformation – Art 11", + "Zusatzinformation – Inhalt 11", + "Zusatzinformation – Art 12", + "Zusatzinformation – Inhalt 12", + "Zusatzinformation – Art 13", + "Zusatzinformation – Inhalt 13", + "Zusatzinformation – Art 14", + "Zusatzinformation – Inhalt 14", + "Zusatzinformation – Art 15", + "Zusatzinformation – Inhalt 15", + "Zusatzinformation – Art 16", + "Zusatzinformation – Inhalt 16", + "Zusatzinformation – Art 17", + "Zusatzinformation – Inhalt 17", + "Zusatzinformation – Art 18", + "Zusatzinformation – Inhalt 18", + "Zusatzinformation – Art 19", + "Zusatzinformation – Inhalt 19", + "Zusatzinformation – Art 20", + "Zusatzinformation – Inhalt 20", # Mengenfelder LuF "Stück", "Gewicht", @@ -233,17 +328,34 @@ def get_datev_csv(data): return result.to_csv( sep=b';', + # European decimal seperator decimal=',', + # Windows "ANSI" encoding encoding='latin_1', + # format date as DDMM date_format='%d%m', + # Windows line terminator line_terminator=b'\r\n', + # Do not number rows index=False, + # Use all columns defined above columns=columns ) @frappe.whitelist() def download_datev_csv(filters=None): + """ + Provide accounting entries for download in DATEV format. + + Validate the filters, get the data, produce the CSV file and provide it for + download. Can be called like this: + + GET /api/method/erpnext.regional.report.datev.datev.download_datev_csv + + Arguments / Params: + filters -- dict of filters to be passed to the sql query + """ if isinstance(filters, string_types): filters = json.loads(filters) From b7b5bcbd855a8b23eda9929f8c3ab66215fe15ab Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Mon, 27 May 2019 15:05:08 +0200 Subject: [PATCH 10/12] fix: em dash cannot be encoded in latin-1 --- erpnext/regional/report/datev/datev.py | 112 ++++++++++++------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 6ca1fe54c85..50aed084aba 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -197,22 +197,22 @@ def get_datev_csv(data): # Digitaler Beleg "Beleglink", # Beleginfo - "Beleginfo – Art 1", - "Beleginfo – Inhalt 1", - "Beleginfo – Art 2", - "Beleginfo – Inhalt 2", - "Beleginfo – Art 3", - "Beleginfo – Inhalt 3", - "Beleginfo – Art 4", - "Beleginfo – Inhalt 4", - "Beleginfo – Art 5", - "Beleginfo – Inhalt 5", - "Beleginfo – Art 6", - "Beleginfo – Inhalt 6", - "Beleginfo – Art 7", - "Beleginfo – Inhalt 7", - "Beleginfo – Art 8", - "Beleginfo – Inhalt 8", + "Beleginfo - Art 1", + "Beleginfo - Inhalt 1", + "Beleginfo - Art 2", + "Beleginfo - Inhalt 2", + "Beleginfo - Art 3", + "Beleginfo - Inhalt 3", + "Beleginfo - Art 4", + "Beleginfo - Inhalt 4", + "Beleginfo - Art 5", + "Beleginfo - Inhalt 5", + "Beleginfo - Art 6", + "Beleginfo - Inhalt 6", + "Beleginfo - Art 7", + "Beleginfo - Inhalt 7", + "Beleginfo - Art 8", + "Beleginfo - Inhalt 8", # Kostenrechnung "Kost 1 - Kostenstelle", "Kost 2 - Kostenstelle", @@ -229,46 +229,46 @@ def get_datev_csv(data): "BU 49 Hauptfunktionsnummer", "BU 49 Funktionsergänzung", # Zusatzinformationen - "Zusatzinformation – Art 1", - "Zusatzinformation – Inhalt 1", - "Zusatzinformation – Art 2", - "Zusatzinformation – Inhalt 2", - "Zusatzinformation – Art 3", - "Zusatzinformation – Inhalt 3", - "Zusatzinformation – Art 4", - "Zusatzinformation – Inhalt 4", - "Zusatzinformation – Art 5", - "Zusatzinformation – Inhalt 5", - "Zusatzinformation – Art 6", - "Zusatzinformation – Inhalt 6", - "Zusatzinformation – Art 7", - "Zusatzinformation – Inhalt 7", - "Zusatzinformation – Art 8", - "Zusatzinformation – Inhalt 8", - "Zusatzinformation – Art 9", - "Zusatzinformation – Inhalt 9", - "Zusatzinformation – Art 10", - "Zusatzinformation – Inhalt 10", - "Zusatzinformation – Art 11", - "Zusatzinformation – Inhalt 11", - "Zusatzinformation – Art 12", - "Zusatzinformation – Inhalt 12", - "Zusatzinformation – Art 13", - "Zusatzinformation – Inhalt 13", - "Zusatzinformation – Art 14", - "Zusatzinformation – Inhalt 14", - "Zusatzinformation – Art 15", - "Zusatzinformation – Inhalt 15", - "Zusatzinformation – Art 16", - "Zusatzinformation – Inhalt 16", - "Zusatzinformation – Art 17", - "Zusatzinformation – Inhalt 17", - "Zusatzinformation – Art 18", - "Zusatzinformation – Inhalt 18", - "Zusatzinformation – Art 19", - "Zusatzinformation – Inhalt 19", - "Zusatzinformation – Art 20", - "Zusatzinformation – Inhalt 20", + "Zusatzinformation - Art 1", + "Zusatzinformation - Inhalt 1", + "Zusatzinformation - Art 2", + "Zusatzinformation - Inhalt 2", + "Zusatzinformation - Art 3", + "Zusatzinformation - Inhalt 3", + "Zusatzinformation - Art 4", + "Zusatzinformation - Inhalt 4", + "Zusatzinformation - Art 5", + "Zusatzinformation - Inhalt 5", + "Zusatzinformation - Art 6", + "Zusatzinformation - Inhalt 6", + "Zusatzinformation - Art 7", + "Zusatzinformation - Inhalt 7", + "Zusatzinformation - Art 8", + "Zusatzinformation - Inhalt 8", + "Zusatzinformation - Art 9", + "Zusatzinformation - Inhalt 9", + "Zusatzinformation - Art 10", + "Zusatzinformation - Inhalt 10", + "Zusatzinformation - Art 11", + "Zusatzinformation - Inhalt 11", + "Zusatzinformation - Art 12", + "Zusatzinformation - Inhalt 12", + "Zusatzinformation - Art 13", + "Zusatzinformation - Inhalt 13", + "Zusatzinformation - Art 14", + "Zusatzinformation - Inhalt 14", + "Zusatzinformation - Art 15", + "Zusatzinformation - Inhalt 15", + "Zusatzinformation - Art 16", + "Zusatzinformation - Inhalt 16", + "Zusatzinformation - Art 17", + "Zusatzinformation - Inhalt 17", + "Zusatzinformation - Art 18", + "Zusatzinformation - Inhalt 18", + "Zusatzinformation - Art 19", + "Zusatzinformation - Inhalt 19", + "Zusatzinformation - Art 20", + "Zusatzinformation - Inhalt 20", # Mengenfelder LuF "Stück", "Gewicht", From 1c1a5958bc4ed65ceeb746e43177de285cb2bed4 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Mon, 27 May 2019 15:05:32 +0200 Subject: [PATCH 11/12] more pleasant icon color --- erpnext/regional/report/datev/datev.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/datev/datev.json b/erpnext/regional/report/datev/datev.json index d4f44b6fbb6..80a866cbf5c 100644 --- a/erpnext/regional/report/datev/datev.json +++ b/erpnext/regional/report/datev/datev.json @@ -4,7 +4,7 @@ "creation": "2019-04-24 08:45:16.650129", "disabled": 0, "icon": "octicon octicon-repo-pull", - "color": "#96cf41", + "color": "#4CB944", "docstatus": 0, "doctype": "Report", "idx": 0, From 0261472ecd305ac405dd7604913cf2b359e7bbbd Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Mon, 27 May 2019 16:01:05 +0200 Subject: [PATCH 12/12] add default filters --- erpnext/regional/report/datev/datev.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js index 781862c2a6a..1e000b673e6 100644 --- a/erpnext/regional/report/datev/datev.js +++ b/erpnext/regional/report/datev/datev.js @@ -5,18 +5,20 @@ frappe.query_reports["DATEV"] = { "label": __("Company"), "fieldtype": "Link", "options": "Company", - "default": frappe.defaults.get_user_default("Company"), + "default": frappe.defaults.get_user_default("Company") || frappe.defaults.get_global_default("Company"), "reqd": 1 }, { "fieldname": "from_date", "label": __("From Date"), + "default": frappe.datetime.month_start(), "fieldtype": "Date", "reqd": 1 }, { "fieldname": "to_date", "label": __("To Date"), + "default": frappe.datetime.now_date(), "fieldtype": "Date", "reqd": 1 }