From e80702b6c2ab7f215c5f6a08f8a6c7e06aa97f5f Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Sun, 19 Apr 2020 16:46:18 +0200 Subject: [PATCH] fix(regional): backport DATEV fix (#21281) * fix: quote nonnumeric values * fix(DATEV Settings): restrict max length of IDs * fix: display Columns as Dynamic Link instead of as Data * fix: add column "Belegfeld 1" * fix: truncate column Buchungstext to 60 chars * fix: make header compatible to current DATEV Format 7.00 * fix: column names and descriptions --- .../datev_settings/datev_settings.json | 4 +- erpnext/regional/report/datev/datev.py | 175 ++++++++++++------ 2 files changed, 122 insertions(+), 57 deletions(-) diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.json b/erpnext/regional/doctype/datev_settings/datev_settings.json index 6860ed3fdae..caed7367dc1 100644 --- a/erpnext/regional/doctype/datev_settings/datev_settings.json +++ b/erpnext/regional/doctype/datev_settings/datev_settings.json @@ -28,6 +28,7 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Client ID", + "length": 5, "reqd": 1 }, { @@ -42,6 +43,7 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Consultant ID", + "length": 7, "reqd": 1 }, { @@ -57,7 +59,7 @@ "fieldtype": "Column Break" } ], - "modified": "2019-08-14 00:03:26.616460", + "modified": "2020-04-15 12:59:57.786506", "modified_by": "Administrator", "module": "Regional", "name": "DATEV Settings", diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index a58e13a0947..a2d74b889ad 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -12,6 +12,7 @@ import datetime import json import six from six import string_types +from csv import QUOTE_NONNUMERIC import frappe from frappe import _ @@ -57,8 +58,8 @@ def get_columns(): "fieldtype": "Data", }, { - "label": "Kontonummer", - "fieldname": "Kontonummer", + "label": "Konto", + "fieldname": "Konto", "fieldtype": "Data", }, { @@ -71,6 +72,11 @@ def get_columns(): "fieldname": "Belegdatum", "fieldtype": "Date", }, + { + "label": "Belegfeld 1", + "fieldname": "Belegfeld 1", + "fieldtype": "Data", + }, { "label": "Buchungstext", "fieldname": "Buchungstext", @@ -79,22 +85,26 @@ def get_columns(): { "label": "Beleginfo - Art 1", "fieldname": "Beleginfo - Art 1", - "fieldtype": "Data", + "fieldtype": "Link", + "options": "DocType" }, { "label": "Beleginfo - Inhalt 1", "fieldname": "Beleginfo - Inhalt 1", - "fieldtype": "Data", + "fieldtype": "Dynamic Link", + "options": "Beleginfo - Art 1" }, { "label": "Beleginfo - Art 2", "fieldname": "Beleginfo - Art 2", - "fieldtype": "Data", + "fieldtype": "Link", + "options": "DocType" }, { "label": "Beleginfo - Inhalt 2", "fieldname": "Beleginfo - Inhalt 2", - "fieldtype": "Data", + "fieldtype": "Dynamic Link", + "options": "Beleginfo - Art 2" } ] @@ -122,13 +132,14 @@ def get_gl_entries(filters, as_dict): 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', + coalesce(acc.account_number, acc_pa.account_number) as 'Konto', /* 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_no as 'Belegfeld 1', + LEFT(gl.remarks, 60) as 'Buchungstext', gl.voucher_type as 'Beleginfo - Art 1', gl.voucher_no as 'Beleginfo - Inhalt 1', gl.against_voucher_type as 'Beleginfo - Art 2', @@ -177,16 +188,19 @@ def get_datev_csv(data, filters): data -- array of dictionaries filters -- dict """ + coa = frappe.get_value("Company", filters.get("company"), "chart_of_accounts") + coa_used = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "") + header = [ - # A = DATEV format + # A = DATEV-Format-KZ # DTVF = created by DATEV software, # EXTF = created by other software - "EXTF", + '"EXTF"', # B = version of the DATEV format # 141 = 1.41, # 510 = 5.10, # 720 = 7.20 - "510", + "700", # C = Data category # 21 = Transaction batch (Buchungsstapel), # 67 = Buchungstextkonstanten, @@ -200,23 +214,22 @@ def get_datev_csv(data, filters): # Kontenbeschriftungen "Buchungsstapel", # E = Format version (regarding format name) - "", + "9", # F = Generated on - datetime.datetime.now().strftime("%Y%m%d"), + datetime.datetime.now().strftime("%Y%m%d%H%M%S") + '000', # G = Imported on -- stays empty "", - # H = Origin (SV = other (?), RE = KARE) - "SV", + # H = Herkunfts-Kennzeichen (Origin) + # Any two letters + '"EN"', # I = Exported by - frappe.session.user, + '"%s"' % frappe.session.user, # J = Imported by -- stays empty "", # K = Tax consultant number (Beraternummer) frappe.get_value("DATEV Settings", filters.get("company"), "consultant_number") or "", - "", # L = Tax client number (Mandantennummer) frappe.get_value("DATEV Settings", filters.get("company"), "client_number") or "", - "", # 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) @@ -226,10 +239,7 @@ def get_datev_csv(data, filters): # P = Transaction batch end date (YYYYMMDD) frappe.utils.formatdate(filters.get('to_date'), "yyyyMMdd"), # Q = Description (for example, "January - February 2019 Transactions") - "{} - {} Buchungsstapel".format( - frappe.utils.formatdate(filters.get('from_date'), "MMMM yyyy"), - frappe.utils.formatdate(filters.get('to_date'), "MMMM yyyy") - ), + "Buchungsstapel", # R = Diktatkürzel "", # S = Buchungstyp @@ -237,11 +247,29 @@ def get_datev_csv(data, filters): # 2 = Annual financial statement (Jahresabschluss) "1", # T = Rechnungslegungszweck - "", + "0", # vom Rechnungslegungszweck unabhängig # U = Festschreibung - "", + "0", # keine Festschreibung # V = Kontoführungs-Währungskennzeichen des Geldkontos - frappe.get_value("Company", filters.get("company"), "default_currency") + frappe.get_value("Company", filters.get("company"), "default_currency"), + # reserviert + '', + # Derivatskennzeichen + '', + # reserviert + '', + # reserviert + '', + # SKR + '"%s"' % coa_used, + # Branchen-Lösungs-ID + '', + # reserviert + '', + # reserviert + '', + # Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung) + '' ] columns = [ # All possible columns must tbe listed here, because DATEV requires them to @@ -255,24 +283,27 @@ def get_datev_csv(data, filters): "Basis-Umsatz", "WKZ Basis-Umsatz", # Konto/Gegenkonto - "Kontonummer", + "Konto", "Gegenkonto (ohne BU-Schlüssel)", "BU-Schlüssel", # Datum "Belegdatum", - # Belegfelder + # Rechnungs- / Belegnummer "Belegfeld 1", + # z.B. Fälligkeitsdatum Format: TTMMJJ "Belegfeld 2", - # Weitere Felder + # Skonto-Betrag / -Abzug (Der Wert 0 ist unzulässig) "Skonto", + # Beschreibung des Buchungssatzes "Buchungstext", - # OPOS-Informationen + # Mahn- / Zahl-Sperre (1 = Postensperre) "Postensperre", "Diverse Adressnummer", "Geschäftspartnerbank", "Sachverhalt", + # Keine Mahnzinsen "Zinssperre", - # Digitaler Beleg + # Link auf den Buchungsbeleg (Programmkürzel + GUID) "Beleglink", # Beleginfo "Beleginfo - Art 1", @@ -291,22 +322,30 @@ def get_datev_csv(data, filters): "Beleginfo - Inhalt 7", "Beleginfo - Art 8", "Beleginfo - Inhalt 8", - # Kostenrechnung - "Kost 1 - Kostenstelle", - "Kost 2 - Kostenstelle", - "Kost-Menge", - # Steuerrechnung - "EU-Land u. UStID", + # Zuordnung des Geschäftsvorfalls für die Kostenrechnung + "KOST1 - Kostenstelle", + "KOST2 - Kostenstelle", + "KOST-Menge", + # USt-ID-Nummer (Beispiel: DE133546770) + "EU-Mitgliedstaat u. USt-IdNr.", + # Der im EU-Bestimmungsland gültige Steuersatz "EU-Steuersatz", + # I = Ist-Versteuerung, + # K = keine Umsatzsteuerrechnung + # P = Pauschalierung (z. B. für Land- und Forstwirtschaft), + # S = Soll-Versteuerung "Abw. Versteuerungsart", - # L+L Sachverhalt + # Sachverhalte gem. § 13b Abs. 1 Satz 1 Nrn. 1.-5. UStG "Sachverhalt L+L", + # Steuersatz / Funktion zum L+L-Sachverhalt (Beispiel: Wert 190 für 19%) "Funktionsergänzung L+L", - # Funktion Steuerschlüssel 49 + # Bei Verwendung des BU-Schlüssels 49 für „andere Steuersätze“ muss der + # steuerliche Sachverhalt mitgegeben werden "BU 49 Hauptfunktionstyp", "BU 49 Hauptfunktionsnummer", "BU 49 Funktionsergänzung", - # Zusatzinformationen + # Zusatzinformationen, besitzen den Charakter eines Notizzettels und können + # frei erfasst werden. "Zusatzinformation - Art 1", "Zusatzinformation - Inhalt 1", "Zusatzinformation - Art 2", @@ -347,54 +386,76 @@ def get_datev_csv(data, filters): "Zusatzinformation - Inhalt 19", "Zusatzinformation - Art 20", "Zusatzinformation - Inhalt 20", - # Mengenfelder LuF + # Wirkt sich nur bei Sachverhalt mit SKR 14 Land- und Forstwirtschaft aus, + # für andere SKR werden die Felder beim Import / Export überlesen bzw. + # leer exportiert. "Stück", "Gewicht", - # Forderungsart + # 1 = Lastschrift + # 2 = Mahnung + # 3 = Zahlung "Zahlweise", "Forderungsart", + # JJJJ "Veranlagungsjahr", + # TTMMJJJJ "Zugeordnete Fälligkeit", - # Weitere Felder + # 1 = Einkauf von Waren + # 2 = Erwerb von Roh-Hilfs- und Betriebsstoffen "Skontotyp", - # Anzahlungen + # Allgemeine Bezeichnung, des Auftrags / Projekts. "Auftragsnummer", + # AA = Angeforderte Anzahlung / Abschlagsrechnung + # AG = Erhaltene Anzahlung (Geldeingang) + # AV = Erhaltene Anzahlung (Verbindlichkeit) + # SR = Schlussrechnung + # SU = Schlussrechnung (Umbuchung) + # SG = Schlussrechnung (Geldeingang) + # SO = Sonstige "Buchungstyp", "USt-Schlüssel (Anzahlungen)", - "EU-Land (Anzahlungen)", + "EU-Mitgliedstaat (Anzahlungen)", "Sachverhalt L+L (Anzahlungen)", "EU-Steuersatz (Anzahlungen)", "Erlöskonto (Anzahlungen)", - # Stapelinformationen + # Wird beim Import durch SV (Stapelverarbeitung) ersetzt. "Herkunft-Kz", - # Technische Identifikation - "Buchungs GUID", - # Kostenrechnung - "Kost-Datum", - # OPOS-Informationen + # Wird von DATEV verwendet. + "Leerfeld", + # Format TTMMJJJJ + "KOST-Datum", + # Vom Zahlungsempfänger individuell vergebenes Kennzeichen eines Mandats + # (z.B. Rechnungs- oder Kundennummer). "SEPA-Mandatsreferenz", + # 1 = Skontosperre + # 0 = Keine Skontosperre "Skontosperre", # Gesellschafter und Sonderbilanzsachverhalt "Gesellschaftername", + # Amtliche Nummer aus der Feststellungserklärung "Beteiligtennummer", "Identifikationsnummer", "Zeichnernummer", - # OPOS-Informationen + # Format TTMMJJJJ "Postensperre bis", # Gesellschafter und Sonderbilanzsachverhalt "Bezeichnung SoBil-Sachverhalt", "Kennzeichen SoBil-Buchung", - # Stapelinformationen + # 0 = keine Festschreibung + # 1 = Festschreibung "Festschreibung", - # Datum + # Format TTMMJJJJ "Leistungsdatum", + # Format TTMMJJJJ "Datum Zuord. Steuerperiode", - # OPOS-Informationen + # OPOS-Informationen, Format TTMMJJJJ "Fälligkeit", - # Konto/Gegenkonto + # G oder 1 = Generalumkehr + # 0 = keine Generalumkehr "Generalumkehr (GU)", # Steuersatz für Steuerschlüssel "Steuersatz", + # Beispiel: DE für Deutschland "Land" ] @@ -419,7 +480,9 @@ def get_datev_csv(data, filters): # Do not number rows index=False, # Use all columns defined above - columns=columns + columns=columns, + # Quote most fields, even currency values with "," separator + quoting=QUOTE_NONNUMERIC ) if not six.PY2: