diff --git a/datev_export/models/res_company.py b/datev_export/models/res_company.py
index d8a5f9b00..f8f363217 100644
--- a/datev_export/models/res_company.py
+++ b/datev_export/models/res_company.py
@@ -18,3 +18,32 @@ class ResCompany(models.Model):
size=5,
help="Number from 0 to 99999",
)
+
+ datev_account_code_length = fields.Integer(
+ string="DATEV account code length",
+ default=5,
+ )
+
+ datev_partner_numbering = fields.Selection(
+ selection="_selection_datev_partner_numbering",
+ string="DATEV Partner numbering",
+ default="none",
+ )
+
+ datev_customer_sequence_id = fields.Many2one(
+ "ir.sequence", "DATEV sequence for customers"
+ )
+
+ datev_supplier_sequence_id = fields.Many2one(
+ "ir.sequence", "DATEV sequence for suppliers"
+ )
+
+ def _selection_datev_partner_numbering(self):
+ reports_installed = (
+ "l10n_de_datev_reports" in self.env["ir.module.module"]._installed()
+ )
+ return (
+ [("none", "None")]
+ + ([("ee", "Enterprises Edition")] if reports_installed else [])
+ + [("sequence", "Sequence")]
+ )
diff --git a/datev_export/models/res_config_settings.py b/datev_export/models/res_config_settings.py
index d1e2a6cf1..87301f9a1 100644
--- a/datev_export/models/res_config_settings.py
+++ b/datev_export/models/res_config_settings.py
@@ -16,3 +16,19 @@ class ResConfigSettings(models.TransientModel):
related="company_id.datev_client_number",
readonly=False,
)
+
+ datev_account_code_length = fields.Integer(
+ related="company_id.datev_account_code_length",
+ readonly=False,
+ )
+ datev_partner_numbering = fields.Selection(
+ related="company_id.datev_partner_numbering", readonly=False
+ )
+
+ datev_customer_sequence_id = fields.Many2one(
+ related="company_id.datev_customer_sequence_id", readonly=False
+ )
+
+ datev_supplier_sequence_id = fields.Many2one(
+ related="company_id.datev_supplier_sequence_id", readonly=False
+ )
diff --git a/datev_export/views/res_config_settings_views.xml b/datev_export/views/res_config_settings_views.xml
index 30cf2db63..4ede3c571 100644
--- a/datev_export/views/res_config_settings_views.xml
+++ b/datev_export/views/res_config_settings_views.xml
@@ -55,6 +55,110 @@
+
+
+
+
Account code length
+
+
+ Account code length
+
+
+
+
+
+
+
Partner numbering
+
+
+ Select the way partners in DATEV export are numbered
+
+
+
+
+
+
+
Customer sequence
+
+
+ The sequence used to number customers
+
+
+
+
+
+
+
Supplier sequence
+
+
+ The sequence used to number suppliers
+
+
+
diff --git a/datev_export_dtvf/README.rst b/datev_export_dtvf/README.rst
new file mode 100644
index 000000000..471bf4c60
--- /dev/null
+++ b/datev_export_dtvf/README.rst
@@ -0,0 +1,97 @@
+=====
+DATEV
+=====
+
+.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
+ :target: https://odoo-community.org/page/development-status
+ :alt: Beta
+.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+ :alt: License: AGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fl10n--germany-lightgray.png?logo=github
+ :target: https://github.com/OCA/l10n-germany/tree/14.0/datev_export_dtvf
+ :alt: OCA/l10n-germany
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/l10n-germany-14-0/l10n-germany-14-0-datev_export_dtvf
+ :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+ :target: https://runboat.odoo-community.org/webui/builds.html?repo=OCA/l10n-germany&target_branch=14.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This module implements DATEV exports in the dtvf format.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Configuration
+=============
+
+To configure this module, you need to:
+
+#. Go to your company
+#. Fill in the fields in the `DATEV` tab
+#. For accounts where you want to suppress automatic calculations (for ie taxes), set the according flag
+
+Usage
+=====
+
+To use this module, you need to:
+
+#. Go to `Invoicing` / `Reporting` / `DATEV export`
+#. Create an export, choose a date range to use as fiscal year, and ranges to export
+#. Click ``Generate``
+
+Known issues / Roadmap
+======================
+
+* support missing formats
+* add empty fields
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues `_.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us smashing it by providing a detailed and welcomed
+`feedback `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+~~~~~~~
+
+* Hunki Enterprises BV
+
+Contributors
+~~~~~~~~~~~~
+
+* Holger Brunn (https://hunki-enterprises.com)
+
+Maintainers
+~~~~~~~~~~~
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+This module is part of the `OCA/l10n-germany `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/datev_export_dtvf/__init__.py b/datev_export_dtvf/__init__.py
new file mode 100644
index 000000000..ec8a53e9d
--- /dev/null
+++ b/datev_export_dtvf/__init__.py
@@ -0,0 +1,4 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from . import models
+from . import datev
diff --git a/datev_export_dtvf/__manifest__.py b/datev_export_dtvf/__manifest__.py
new file mode 100644
index 000000000..f3a5c31a3
--- /dev/null
+++ b/datev_export_dtvf/__manifest__.py
@@ -0,0 +1,23 @@
+# Copyright 2022 Hunki Enterprises BV
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+{
+ "name": "DATEV",
+ "summary": "Export Data for DATEV (dtvf)",
+ "version": "14.0.1.0.0",
+ "development_status": "Beta",
+ "category": "Accounting",
+ "website": "https://github.com/OCA/l10n-germany",
+ "author": "Hunki Enterprises BV, Odoo Community Association (OCA)",
+ "license": "AGPL-3",
+ "depends": [
+ "account",
+ "date_range",
+ "datev_export",
+ ],
+ "data": [
+ "security/ir.model.access.csv",
+ "views/account_account.xml",
+ "views/datev_export_dtvf.xml",
+ "views/res_partner.xml",
+ ],
+}
diff --git a/datev_export_dtvf/datev.py b/datev_export_dtvf/datev.py
new file mode 100644
index 000000000..e833e6af3
--- /dev/null
+++ b/datev_export_dtvf/datev.py
@@ -0,0 +1,613 @@
+# Copyright 2022 Hunki Enterprises BV
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+from codecs import BOM_UTF8
+from collections import OrderedDict
+from datetime import datetime
+from io import StringIO
+
+
+class DatevField:
+ __slots__ = ("name", "length", "quote", "regex")
+
+ def __init__(self, name, length=None, quote=False, regex=".*"):
+ self.name = name
+ self.length = length
+ self.quote = quote
+ self.regex = regex
+
+
+class DatevWriter(object):
+ def __init__(
+ self,
+ data_type,
+ data_name,
+ data_version,
+ consultant_id,
+ client_id,
+ fiscal_year_start,
+ account_code_length,
+ period_start,
+ period_end,
+ dataset_name,
+ user_initials,
+ currency,
+ fields,
+ ):
+ self.buffer = StringIO()
+ self.header = [
+ "EXTF", # constant for external programs
+ 700, # header version
+ data_type, # type of data - 21=transactions
+ data_name, # name of type
+ data_version, # version of data
+ datetime.now().strftime("%Y%m%d%H%M%S%f")[:-3],
+ None,
+ "",
+ "",
+ "",
+ consultant_id,
+ client_id,
+ fiscal_year_start,
+ account_code_length,
+ period_start,
+ period_end,
+ dataset_name,
+ user_initials,
+ 1, # 1 default, 2 end of year
+ 0,
+ 0, # 0 not locked, 1 locked
+ currency,
+ None,
+ "",
+ None,
+ None,
+ "",
+ "",
+ None,
+ "",
+ "",
+ ]
+ self.fields = OrderedDict([(field[0], DatevField(*field)) for field in fields])
+ self.header_fields = [
+ DatevField(*header_field)
+ for header_field in [
+ ("Flag", None, True),
+ ("Version number",),
+ ("Format Category",),
+ ("Format Name", None, True),
+ ("Format Version",),
+ ("Created on",),
+ ("Reserved1",),
+ ("Reserved2", None, True),
+ ("Reserved3", None, True),
+ ("Reserved4", None, True),
+ ("Consultant number",),
+ ("Client number",),
+ ("Start of business year",),
+ ("G/L account length",),
+ ("Date from",),
+ ("Date till",),
+ ("Designation", None, True),
+ ("Initials", None, True),
+ ("Record Type",),
+ ("Accounting reason",),
+ ("Locking",),
+ ("Currency Code", None, True),
+ ("Reserved5",),
+ ("Derivatives Flag", None, True),
+ ("Reserved6",),
+ ("Reserved7",),
+ ("G/L chart of accounts",),
+ ("Industry Solution ID",),
+ ("Reserved8",),
+ ("Reserved9", None, True),
+ ("Application information", 16, True),
+ ]
+ ]
+
+ def writeheader(self):
+ self.buffer.write(BOM_UTF8.decode("utf8"))
+ for i, (field, value) in enumerate(zip(self.header_fields, self.header)):
+ if i > 0:
+ self.buffer.write(";")
+ self.buffer.write(self._coerce_value(field, value))
+ self.buffer.write("\n")
+ for i, name in enumerate(self.fields):
+ if i > 0:
+ self.buffer.write(";")
+ self.buffer.write(name)
+ self.buffer.write("\n")
+
+ def _coerce_value(self, field, value):
+ if not value:
+ if field.quote:
+ return '""'
+ return ""
+ if field.length:
+ value = str(value)[: field.length]
+ if field.quote:
+ value = '"%s"' % str(value).replace('"', '""')
+ return str(value)
+
+ def writerow(self, row):
+ for i, field in enumerate(self.fields.values()):
+ if i > 0:
+ self.buffer.write(";")
+ self.buffer.write(self._coerce_value(field, row.get(field.name)))
+ self.buffer.write("\n")
+
+ def writerows(self, rows):
+ for row in rows:
+ self.writerow(row)
+
+
+class DatevTransactionWriter(DatevWriter):
+ def __init__(
+ self,
+ consultant_id,
+ client_id,
+ fiscal_year_start,
+ account_code_length,
+ period_start,
+ period_end,
+ user_initials,
+ currency,
+ ):
+ super().__init__(
+ 21,
+ "Buchungsstapel",
+ 12,
+ consultant_id,
+ client_id,
+ fiscal_year_start,
+ account_code_length,
+ period_start,
+ period_end,
+ "Buchungsstapel %s" % period_start,
+ user_initials,
+ currency,
+ [
+ ("Umsatz (ohne Soll/Haben-Kz)", None),
+ ("Soll/Haben-Kennzeichen", None, True),
+ ("WKZ Umsatz", None, True),
+ ("Kurs", None),
+ ("Basis-Umsatz", None),
+ ("WKZ Basis-Umsatz", None, True),
+ ("Konto", None),
+ ("Gegenkonto (ohne BU-Schlüssel)", 9),
+ ("BU-Schlüssel", 4, True),
+ ("Belegdatum", 4),
+ ("Belegfeld 1", 36, True),
+ ("Belegfeld 2", 12, True),
+ ("Skonto", None),
+ ("Buchungstext", 60, True),
+ ("Postensperre", None),
+ ("Diverse Adressnummer", None, True),
+ ("Geschäftspartnerbank", None),
+ ("Sachverhalt", None),
+ ("Zinssperre", None),
+ ("Beleglink", None, True),
+ ("Beleginfo - Art 1", None, True),
+ ("Beleginfo - Inhalt 1", None, True),
+ ("Beleginfo - Art 2", None, True),
+ ("Beleginfo - Inhalt 2", None, True),
+ ("Beleginfo - Art 3", None, True),
+ ("Beleginfo - Inhalt 3", None, True),
+ ("Beleginfo - Art 4", None, True),
+ ("Beleginfo - Inhalt 4", None, True),
+ ("Beleginfo - Art 5", None, True),
+ ("Beleginfo - Inhalt 5", None, True),
+ ("Beleginfo - Art 6", None, True),
+ ("Beleginfo - Inhalt 6", None, True),
+ ("Beleginfo - Art 7", None, True),
+ ("Beleginfo - Inhalt 7", None, True),
+ ("Beleginfo - Art 8", None, True),
+ ("Beleginfo - Inhalt 8", None, True),
+ ("KOST1 - Kostenstelle", 36, True),
+ ("KOST2 - Kostenstelle", 36, True),
+ ("Kost-Menge", None),
+ ("EU-Land u. UStID (Bestimmung)", None, True),
+ ("EU-Steuersatz (Bestimmung)", None),
+ ("Abw. Versteuerungsart", None, True),
+ ("Sachverhalt L+L", None),
+ ("Funktionsergänzung L+L", None),
+ ("BU 49 Hauptfunktionstyp", None),
+ ("BU 49 Hauptfunktionsnummer", None),
+ ("BU 49 Funktionsergänzung", None),
+ ("Zusatzinformation - Art 1", None, True),
+ ("Zusatzinformation- Inhalt 1", None, True),
+ ("Zusatzinformation - Art 2", None, True),
+ ("Zusatzinformation- Inhalt 2", None, True),
+ ("Zusatzinformation - Art 3", None, True),
+ ("Zusatzinformation- Inhalt 3", None, True),
+ ("Zusatzinformation - Art 4", None, True),
+ ("Zusatzinformation- Inhalt 4", None, True),
+ ("Zusatzinformation - Art 5", None, True),
+ ("Zusatzinformation- Inhalt 5", None, True),
+ ("Zusatzinformation - Art 6", None, True),
+ ("Zusatzinformation- Inhalt 6", None, True),
+ ("Zusatzinformation - Art 7", None, True),
+ ("Zusatzinformation- Inhalt 7", None, True),
+ ("Zusatzinformation - Art 8", None, True),
+ ("Zusatzinformation- Inhalt 8", None, True),
+ ("Zusatzinformation - Art 9", None, True),
+ ("Zusatzinformation- Inhalt 9", None, True),
+ ("Zusatzinformation - Art 10", None, True),
+ ("Zusatzinformation- Inhalt 10", None, True),
+ ("Zusatzinformation - Art 11", None, True),
+ ("Zusatzinformation- Inhalt 11", None, True),
+ ("Zusatzinformation - Art 12", None, True),
+ ("Zusatzinformation- Inhalt 12", None, True),
+ ("Zusatzinformation - Art 13", None, True),
+ ("Zusatzinformation- Inhalt 13", None, True),
+ ("Zusatzinformation - Art 14", None, True),
+ ("Zusatzinformation- Inhalt 14", None, True),
+ ("Zusatzinformation - Art 15", None, True),
+ ("Zusatzinformation- Inhalt 15", None, True),
+ ("Zusatzinformation - Art 16", None, True),
+ ("Zusatzinformation- Inhalt 16", None, True),
+ ("Zusatzinformation - Art 17", None, True),
+ ("Zusatzinformation- Inhalt 17", None, True),
+ ("Zusatzinformation - Art 18", None, True),
+ ("Zusatzinformation- Inhalt 18", None, True),
+ ("Zusatzinformation - Art 19", None, True),
+ ("Zusatzinformation- Inhalt 19", None, True),
+ ("Zusatzinformation - Art 20", None, True),
+ ("Zusatzinformation- Inhalt 20", None, True),
+ ("Stück", None),
+ ("Gewicht", None),
+ ("Zahlweise", None),
+ ("Forderungsart", None, True),
+ ("Veranlagungsjahr", None),
+ ("Zugeordnete Fälligkeit", None),
+ ("Skontotyp", None),
+ ("Auftragsnummer", None, True),
+ ("Buchungstyp", None, True),
+ ("USt-Schlüssel (Anzahlungen)", None),
+ ("EU-Land (Anzahlungen)", None, True),
+ ("Sachverhalt L+L (Anzahlungen)", None),
+ ("EU-Steuersatz (Anzahlungen)", None),
+ ("Erlöskonto (Anzahlungen)", None),
+ ("Herkunft-Kz", None, True),
+ ("Buchungs GUID", None, True),
+ ("KOST-Datum", None),
+ ("SEPA-Mandatsreferenz", None, True),
+ ("Skontosperre", None),
+ ("Gesellschaftername", None, True),
+ ("Beteiligtennummer", None),
+ ("Identifikationsnummer", None, True),
+ ("Zeichnernummer", None, True),
+ ("Postensperre bis", None),
+ ("Bezeichnung SoBil-Sachverhalt", None, True),
+ ("Kennzeichen SoBil-Buchung", None),
+ ("Festschreibung", None),
+ ("Leistungsdatum", None),
+ ("Datum Zuord. Steuerperiode", None),
+ ("Fälligkeit", None),
+ ("Generalumkehr (GU)", None, True),
+ ("Steuersatz", None),
+ ("Land", None, True),
+ ("Abrechnungsreferenz", None, True),
+ ("BVV-Position", None),
+ ("EU-Land u. UStID (Ursprung)", None, True),
+ ("EU-Steuersatz (Ursprung)", None),
+ ],
+ )
+
+
+class DatevPartnerWriter(DatevWriter):
+ def __init__(
+ self,
+ consultant_id,
+ client_id,
+ fiscal_year_start,
+ account_code_length,
+ period_start,
+ period_end,
+ user_initials,
+ currency,
+ ):
+ super().__init__(
+ 16,
+ "Debitoren/Kreditoren",
+ 5,
+ consultant_id,
+ client_id,
+ fiscal_year_start,
+ account_code_length,
+ period_start,
+ period_end,
+ "Debitoren/Kreditoren",
+ user_initials,
+ currency,
+ [
+ ("Konto", 9),
+ ("Name (Adressattyp Unternehmen)", 50, True),
+ ("Unternehmensgegenstand", 50, True),
+ ("Name (Adressattyp natürl. Person)", 30, True),
+ ("Vorname (Adressattyp natürl. Person)", 30, True),
+ ("Name (Adressattyp keine Angabe)", 50, True),
+ ("Adressattyp", 1, True),
+ ("Kurzbezeichnung", 15, True),
+ ("EU-Land", None, True),
+ ("EU-UStID", 13, True),
+ ("Anrede", None, True),
+ ("Titel/Akad. Grad", None, True),
+ ("Adelstitel", None, True),
+ ("Namensvorsatz", None, True),
+ ("Adressart", None, True),
+ ("Straße", None, True),
+ ("Postfach", None, True),
+ ("Postleitzahl", None, True),
+ ("Ort", None, True),
+ ("Land", None, True),
+ ("Versandzusatz", None, True),
+ ("Adresszusatz", None, True),
+ ("Abweichende Anrede", None, True),
+ ("Abw. Zustellbezeichnung 1", None, True),
+ ("Abw. Zustellbezeichnung 2", None, True),
+ ("Kennz. Korrespondenzadresse", None),
+ ("Adresse Gültig von", None),
+ ("Adresse Gültig bis", None),
+ ("Telefon", None, True),
+ ("Bemerkung (Telefon)", None, True),
+ ("Telefon GL", None, True),
+ ("Bemerkung (Telefon GL)", None, True),
+ ("E-Mail", None, True),
+ ("Bemerkung (E-Mail)", None, True),
+ ("Internet", None, True),
+ ("Bemerkung (Internet)", None, True),
+ ("Fax", None, True),
+ ("Bemerkung (Fax)", None, True),
+ ("Sonstige", None, True),
+ ("Bemerkung (Sonstige)", None, True),
+ ("Bankleitzahl 1", None, True),
+ ("Bankbezeichnung 1", None, True),
+ ("Bank-Kontonummer 1", None, True),
+ ("Länderkennzeichen 1", None, True),
+ ("IBAN-Nr. 1", None, True),
+ ("Leerfeld1", None, True),
+ ("SWIFT-Code 1", None, True),
+ ("Abw. Kontoinhaber 1", None, True),
+ ("Kennz. Hauptbankverb. 1", None, True),
+ ("Bankverb 1 Gültig von", None),
+ ("Bankverb 1 Gültig bis", None),
+ ("Bankleitzahl 2", None, True),
+ ("Bankbezeichnung 2", None, True),
+ ("Bank-Kontonummer 2", None, True),
+ ("Länderkennzeichen 2", None, True),
+ ("IBAN-Nr. 2", None, True),
+ ("Leerfeld2", None, True),
+ ("SWIFT-Code 2", None, True),
+ ("Abw. Kontoinhaber 2", None, True),
+ ("Kennz. Hauptbankverb. 2", None, True),
+ ("Bankverb 2 Gültig von", None),
+ ("Bankverb 2 Gültig bis", None),
+ ("Bankleitzahl 3", None, True),
+ ("Bankbezeichnung 3", None, True),
+ ("Bank-Kontonummer 3", None, True),
+ ("Länderkennzeichen 3", None, True),
+ ("IBAN-Nr. 3", None, True),
+ ("Leerfeld3", None, True),
+ ("SWIFT-Code 3", None, True),
+ ("Abw. Kontoinhaber 3", None, True),
+ ("Kennz. Hauptbankverb. 3", None, True),
+ ("Bankverb 3 Gültig von", None),
+ ("Bankverb 3 Gültig bis", None),
+ ("Bankleitzahl 4", None, True),
+ ("Bankbezeichnung 4", None, True),
+ ("Bank-Kontonummer 4", None, True),
+ ("Länderkennzeichen 4", None, True),
+ ("IBAN-Nr. 4", None, True),
+ ("Leerfeld4", None, True),
+ ("SWIFT-Code 4", None, True),
+ ("Abw. Kontoinhaber 4", None, True),
+ ("Kennz. Hauptbankverb. 4", None, True),
+ ("Bankverb 4 Gültig von", None),
+ ("Bankverb 4 Gültig bis", None),
+ ("Bankleitzahl 5", None, True),
+ ("Bankbezeichnung 5", None, True),
+ ("Bank-Kontonummer 5", None, True),
+ ("Länderkennzeichen 5", None, True),
+ ("IBAN-Nr. 5", None, True),
+ ("Leerfeld5", None, True),
+ ("SWIFT-Code 5", None, True),
+ ("Abw. Kontoinhaber 5", None, True),
+ ("Kennz. Hauptbankverb. 5", None, True),
+ ("Bankverb 5 Gültig von", None),
+ ("Bankverb 5 Gültig bis", None),
+ ("Leerfeld6", None, True),
+ ("Briefanrede", None, True),
+ ("Grußformel", None, True),
+ ("Kunden-/Lief.-Nr.", None, True),
+ ("Steuernummer", None, True),
+ ("Sprache", None),
+ ("Ansprechpartner", None, True),
+ ("Vertreter", None, True),
+ ("Sachbearbeiter", None, True),
+ ("Diverse-Konto", None),
+ ("Ausgabeziel", None),
+ ("Währungssteuerung", None, True),
+ ("Kreditlimit (Debitor)", None),
+ ("Zahlungsbedingung", None),
+ ("Fälligkeit in Tagen (Debitor)", None),
+ ("Skonto in Prozent (Debitor)", None),
+ ("Kreditoren-Ziel 1 Tg.", None),
+ ("Kreditoren-Skonto 1 %", None),
+ ("Kreditoren-Ziel 2 Tg.", None),
+ ("Kreditoren-Skonto 2 %", None),
+ ("Kreditoren-Ziel 3 Brutto Tg.", None),
+ ("Kreditoren-Ziel 4 Tg.", None),
+ ("Kreditoren-Skonto 4 %", None),
+ ("Kreditoren-Ziel 5 Tg.", None),
+ ("Kreditoren-Skonto 5 %", None),
+ ("Mahnung", None),
+ ("Kontoauszug", None),
+ ("Mahntext 1", None),
+ ("Mahntext 2", None),
+ ("Mahntext 3", None),
+ ("Kontoauszugstext", None),
+ ("Mahnlimit Betrag", None),
+ ("Mahnlimit %", None),
+ ("Zinsberechnung", None),
+ ("Mahnzinssatz 1", None),
+ ("Mahnzinssatz 2", None),
+ ("Mahnzinssatz 3", None),
+ ("Lastschrift", None, True),
+ ("Leerfeld7", None),
+ ("Mandantenbank", None),
+ ("Zahlungsträger", None, True),
+ ("Indiv. Feld 1", None, True),
+ ("Indiv. Feld 2", None, True),
+ ("Indiv. Feld 3", None, True),
+ ("Indiv. Feld 4", None, True),
+ ("Indiv. Feld 5", None, True),
+ ("Indiv. Feld 6", None, True),
+ ("Indiv. Feld 7", None, True),
+ ("Indiv. Feld 8", None, True),
+ ("Indiv. Feld 9", None, True),
+ ("Indiv. Feld 10", None, True),
+ ("Indiv. Feld 11", None, True),
+ ("Indiv. Feld 12", None, True),
+ ("Indiv. Feld 13", None, True),
+ ("Indiv. Feld 14", None, True),
+ ("Indiv. Feld 15", None, True),
+ ("Abweichende Anrede (Rechnungsadresse)", None, True),
+ ("Adressart (Rechnungsadresse)", None, True),
+ ("Straße (Rechnungsadresse)", None, True),
+ ("Postfach (Rechnungsadresse)", None, True),
+ ("Postleitzahl (Rechnungsadresse)", None, True),
+ ("Ort (Rechnungsadresse)", None, True),
+ ("Land (Rechnungsadresse)", None, True),
+ ("Versandzusatz (Rechnungsadresse)", None, True),
+ ("Adresszusatz (Rechnungsadresse)", None, True),
+ ("Abw. Zustellbezeichnung 1 (Rechnungsadresse)", None, True),
+ ("Abw. Zustellbezeichnung 2 (Rechnungsadresse)", None, True),
+ ("Adresse Gültig von (Rechnungsadresse)", None),
+ ("Adresse Gültig bis (Rechnungsadresse)", None),
+ ("Bankleitzahl 6", None, True),
+ ("Bankbezeichnung 6", None, True),
+ ("Bank-Kontonummer 6", None, True),
+ ("Länderkennzeichen 6", None, True),
+ ("IBAN-Nr. 6", None, True),
+ ("Leerfeld8", None, True),
+ ("SWIFT-Code 6", None, True),
+ ("Abw. Kontoinhaber 6", None, True),
+ ("Kennz. Hauptbankverb. 6", None, True),
+ ("Bankverb 6 Gültig von", None),
+ ("Bankverb 6 Gültig bis", None),
+ ("Bankleitzahl 7", None, True),
+ ("Bankbezeichnung 7", None, True),
+ ("Bank-Kontonummer 7", None, True),
+ ("Länderkennzeichen 7", None, True),
+ ("IBAN-Nr. 7", None, True),
+ ("Leerfeld9", None, True),
+ ("SWIFT-Code 7", None, True),
+ ("Abw. Kontoinhaber 7", None, True),
+ ("Kennz. Hauptbankverb. 7", None, True),
+ ("Bankverb 7 Gültig von", None),
+ ("Bankverb 7 Gültig bis", None),
+ ("Bankleitzahl 8", None, True),
+ ("Bankbezeichnung 8", None, True),
+ ("Bank-Kontonummer 8", None, True),
+ ("Länderkennzeichen 8", None, True),
+ ("IBAN-Nr. 8", None, True),
+ ("Leerfeld10", None, True),
+ ("SWIFT-Code 8", None, True),
+ ("Abw. Kontoinhaber 8", None, True),
+ ("Kennz. Hauptbankverb. 8", None, True),
+ ("Bankverb 8 Gültig von", None),
+ ("Bankverb 8 Gültig bis", None),
+ ("Bankleitzahl 9", None, True),
+ ("Bankbezeichnung 9", None, True),
+ ("Bank-Kontonummer 9", None, True),
+ ("Länderkennzeichen 9", None, True),
+ ("IBAN-Nr. 9", None, True),
+ ("Leerfeld11", None, True),
+ ("SWIFT-Code 9", None, True),
+ ("Abw. Kontoinhaber 9", None, True),
+ ("Kennz. Hauptbankverb. 9", None, True),
+ ("Bankverb 9 Gültig von", None),
+ ("Bankverb 9 Gültig bis", None),
+ ("Bankleitzahl 10", None, True),
+ ("Bankbezeichnung 10", None, True),
+ ("Bank-Kontonummer 10", None, True),
+ ("Länderkennzeichen 10", None, True),
+ ("IBAN-Nr. 10", None, True),
+ ("Leerfeld12", None, True),
+ ("SWIFT-Code 10", None, True),
+ ("Abw. Kontoinhaber 10", None, True),
+ ("Kennz. Hauptbankverb. 10", None, True),
+ ("Bankverb 10 Gültig von", None),
+ ("Bankverb 10 Gültig bis", None),
+ ("Nummer Fremdsystem", None, True),
+ ("Insolvent", None),
+ ("SEPA-Mandatsreferenz 1", None, True),
+ ("SEPA-Mandatsreferenz 2", None, True),
+ ("SEPA-Mandatsreferenz 3", None, True),
+ ("SEPA-Mandatsreferenz 4", None, True),
+ ("SEPA-Mandatsreferenz 5", None, True),
+ ("SEPA-Mandatsreferenz 6", None, True),
+ ("SEPA-Mandatsreferenz 7", None, True),
+ ("SEPA-Mandatsreferenz 8", None, True),
+ ("SEPA-Mandatsreferenz 9", None, True),
+ ("SEPA-Mandatsreferenz 10", None, True),
+ ("Verknüpftes OPOS-Konto", None),
+ ("Mahnsperre bis", None),
+ ("Lastschriftsperre bis", None),
+ ("Zahlungssperre bis", None),
+ ("Gebührenberechnung", None),
+ ("Mahngebühr 1", None),
+ ("Mahngebühr 2", None),
+ ("Mahngebühr 3", None),
+ ("Pauschalenberechnung", None),
+ ("Verzugspauschale 1", None),
+ ("Verzugspauschale 2", None),
+ ("Verzugspauschale 3", None),
+ ("Alternativer Suchname", None, True),
+ ("Status", None),
+ ("Anschrift manuell geändert (Korrespondenzadresse)", None),
+ ("Anschrift individuell (Korrespondenzadresse)", None, True),
+ ("Anschrift manuell geändert (Rechnungsadresse)", None),
+ ("Anschrift individuell (Rechnungsadresse)", None, True),
+ ("Fristberechnung bei Debitor", None),
+ ("Mahnfrist 1", None),
+ ("Mahnfrist 2", None),
+ ("Mahnfrist 3", None),
+ ("Letzte Frist", None),
+ ],
+ )
+
+
+class DatevAccountWriter(DatevWriter):
+ def __init__(
+ self,
+ consultant_id,
+ client_id,
+ fiscal_year_start,
+ account_code_length,
+ period_start,
+ period_end,
+ user_initials,
+ currency,
+ ):
+ super().__init__(
+ 20,
+ "Kontenbeschriftungen",
+ 3,
+ consultant_id,
+ client_id,
+ fiscal_year_start,
+ account_code_length,
+ period_start,
+ period_end,
+ "Kontenbeschriftungen",
+ user_initials,
+ currency,
+ [
+ ("Konto", 9),
+ ("Kontobeschriftung", 40, True),
+ ("SprachId", 5, True),
+ ("Kontenbeschriftung lang", 300, True),
+ ],
+ )
diff --git a/datev_export_dtvf/i18n/.empty b/datev_export_dtvf/i18n/.empty
new file mode 100644
index 000000000..e69de29bb
diff --git a/datev_export_dtvf/models/__init__.py b/datev_export_dtvf/models/__init__.py
new file mode 100644
index 000000000..2af41d0e1
--- /dev/null
+++ b/datev_export_dtvf/models/__init__.py
@@ -0,0 +1,5 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from . import account_account
+from . import datev_export_dtvf
+from . import res_partner
diff --git a/datev_export_dtvf/models/account_account.py b/datev_export_dtvf/models/account_account.py
new file mode 100644
index 000000000..7e0f8b586
--- /dev/null
+++ b/datev_export_dtvf/models/account_account.py
@@ -0,0 +1,14 @@
+# Copyright 2022 Hunki Enterprises BV
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+from odoo import fields, models
+
+
+class AccountAccount(models.Model):
+ _inherit = "account.account"
+
+ datev_export_nonautomatic = fields.Boolean(
+ "Suppress automatic calculations in DATEV",
+ help="When this flag is set, journal items from this account will be exported "
+ "with field 'BU-Schlussel' set to '40', which inhibits automatic calculations "
+ "in DATEV.",
+ )
diff --git a/datev_export_dtvf/models/datev_export_dtvf.py b/datev_export_dtvf/models/datev_export_dtvf.py
new file mode 100644
index 000000000..ea0172198
--- /dev/null
+++ b/datev_export_dtvf/models/datev_export_dtvf.py
@@ -0,0 +1,346 @@
+# Copyright 2022 Hunki Enterprises BV
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+import base64
+import io
+import string
+import zipfile
+
+from odoo import _, api, exceptions, fields, models
+from odoo.osv.expression import TRUE_LEAF
+
+from ..datev import DatevAccountWriter, DatevPartnerWriter, DatevTransactionWriter
+
+
+class DatevExportDtvfExport(models.Model):
+ _name = "datev_export_dtvf.export"
+ _description = "DATEV export"
+ _order = "fiscalyear_id desc, create_date desc"
+
+ state = fields.Selection(
+ [("draft", "Draft"), ("done", "Done")], default="draft", copy=False
+ )
+ fiscalyear_id = fields.Many2one(
+ "date.range",
+ string="Fiscal year",
+ states={"draft": [("required", True), ("readonly", False)]},
+ readonly=True,
+ )
+ name = fields.Char(
+ states={"draft": [("required", True), ("readonly", False)]},
+ readonly=True,
+ )
+ fiscalyear_start = fields.Date(related=["fiscalyear_id", "date_start"])
+ fiscalyear_end = fields.Date(related=["fiscalyear_id", "date_end"])
+ period_ids = fields.Many2many(
+ "date.range",
+ string="Periods",
+ states={"draft": [("required", True), ("readonly", False)]},
+ readonly=True,
+ )
+ journal_ids = fields.Many2many(
+ "account.journal",
+ string="Journals",
+ states={"draft": [("readonly", False)]},
+ readonly=True,
+ )
+ date_generated = fields.Datetime("Generated at", readonly=True, copy=False)
+ file_data = fields.Binary("Data", readonly=True, copy=False)
+ file_name = fields.Char("Filename", readonly=True, compute="_compute_file_name")
+ company_id = fields.Many2one(
+ "res.company",
+ required=True,
+ default=lambda self: self.env.company,
+ )
+
+ @api.onchange("fiscalyear_id")
+ def _onchange_fiscalyear_id(self):
+ self.name = self.name or self.fiscalyear_id.display_name
+
+ @api.depends("name")
+ def _compute_file_name(self):
+ for this in self:
+ this.file_name = (
+ "".join(
+ c if c in string.ascii_letters + string.digits + "-_" else "_"
+ for c in (this.name or "datev_export")
+ )
+ + ".zip"
+ )
+
+ def action_draft(self):
+ return self.filtered(lambda x: x.state != "draft").write({"state": "draft"})
+
+ def action_generate(self):
+ for this in self:
+ if not all(
+ (
+ this.company_id.datev_consultant_number,
+ this.company_id.datev_client_number,
+ this.company_id.datev_account_code_length,
+ )
+ ):
+ raise exceptions.ValidationError(
+ _("Please fill in the DATEV tab of your company")
+ )
+
+ zip_buffer = io.BytesIO()
+ zip_file = zipfile.ZipFile(zip_buffer, mode="w")
+
+ account_code_length = this.company_id.datev_account_code_length
+ user_initials = "".join(
+ token[:1].upper() for token in self.env.user.name.split()
+ )[:2]
+
+ partners = self.env["res.partner"].browse([])
+
+ for date_range in this.period_ids:
+ moves = self.env["account.move"].search(
+ [
+ ("state", "=", "posted"),
+ ("date", ">=", date_range.date_start),
+ ("date", "<=", date_range.date_end),
+ ("company_id", "=", this.company_id.id),
+ ("journal_id", "in", this.journal_ids.ids)
+ if this.journal_ids
+ else TRUE_LEAF,
+ ],
+ order="date desc",
+ )
+ partners += moves.mapped("line_ids.partner_id") + moves.mapped(
+ "partner_id"
+ )
+ writer = DatevTransactionWriter(
+ this.company_id.datev_consultant_number,
+ this.company_id.datev_client_number,
+ this.fiscalyear_id.date_start.strftime("%Y%m%d"),
+ account_code_length,
+ date_range.date_start.strftime("%Y%m%d"),
+ date_range.date_end.strftime("%Y%m%d"),
+ user_initials,
+ self.company_id.currency_id.name,
+ )
+ writer.writeheader()
+ for move in moves:
+ writer.writerows(this._get_data_transaction(move))
+
+ filename = (
+ "EXTF_Buchungsstapel_%s.csv"
+ % date_range.date_start.strftime("%Y%m%d")
+ )
+ zip_file.writestr(filename, writer.buffer.getvalue())
+
+ writer = DatevPartnerWriter(
+ self.company_id.datev_consultant_number,
+ self.company_id.datev_client_number,
+ this.fiscalyear_id.date_start.strftime("%Y%m%d"),
+ account_code_length,
+ None,
+ None,
+ user_initials,
+ self.company_id.currency_id.name,
+ )
+ writer.writeheader()
+ for partner in partners:
+ writer.writerows(this._get_data_partner(partner))
+ zip_file.writestr("EXTF_DebKred_Stamm.csv", writer.buffer.getvalue())
+
+ accounts = self.env["account.account"].search(
+ [
+ ("company_id", "=", this.company_id.id),
+ ]
+ )
+ writer = DatevAccountWriter(
+ self.company_id.datev_consultant_number,
+ self.company_id.datev_client_number,
+ this.fiscalyear_id.date_start.strftime("%Y%m%d"),
+ account_code_length + 1,
+ None,
+ None,
+ user_initials,
+ self.company_id.currency_id.name,
+ )
+ writer.writeheader()
+ for account in accounts:
+ writer.writerows(this._get_data_account(account))
+ zip_file.writestr("EXTF_Kontenbeschriftungen.csv", writer.buffer.getvalue())
+
+ zip_file.close()
+ this.write(
+ {
+ "file_data": base64.b64encode(zip_buffer.getvalue()),
+ "state": "done",
+ "date_generated": fields.Datetime.now(),
+ }
+ )
+
+ def _get_data_transaction(self, move):
+ # split move into single transactions from one account to another
+ move_line2amount = {
+ move_line: move_line.credit or move_line.debit
+ for move_line in move.line_ids
+ if not move_line.display_type and (move_line.debit or move_line.credit)
+ }
+ currency = move.currency_id or move.company_id.currency_id
+ code_length = move.company_id.datev_account_code_length
+ while move_line2amount:
+ move_line = min(move_line2amount, key=move_line2amount.get)
+ amount = move_line2amount.pop(move_line)
+ move_line2 = move_line
+ for move_line2 in move_line2amount:
+ if (
+ move_line.debit
+ and not move_line2.debit
+ or move_line.credit
+ and not move_line2.credit
+ ):
+ move_line2amount[move_line2] = currency.round(
+ move_line2amount[move_line2] - amount
+ )
+ if currency.is_zero(move_line2amount[move_line2]):
+ move_line2amount.pop(move_line2)
+ break
+ if move_line.account_id.internal_type not in (
+ "receivable",
+ "payable",
+ ) and move_line2.account_id.internal_type in ("receivable", "payable"):
+ move_line, move_line2 = move_line2, move_line
+ if move_line.account_id.datev_export_nonautomatic:
+ move_line, move_line2 = move_line2, move_line
+ account_number = move_line.account_id.code[-code_length:]
+ offset_account_number = move_line2.account_id.code[-code_length:]
+ if self.company_id.datev_partner_numbering in ("ee", "sequence"):
+ for ml in (move_line, move_line2):
+ number = (
+ account_number if ml == move_line else offset_account_number
+ )
+ number_type = (
+ "customer"
+ if ml.account_id.internal_type == "receivable"
+ else (
+ "supplier"
+ if ml.account_id.internal_type == "payable"
+ else None
+ )
+ )
+ if number_type and ml.partner_id:
+ number = self._get_partner_number(
+ ml.partner_id, number_type, True
+ )
+ if ml == move_line:
+ account_number = number
+ else:
+ offset_account_number = number
+ data = {
+ "Umsatz (ohne Soll/Haben-Kz)": ("%.2f" % abs(amount)).replace(".", ","),
+ "Soll/Haben-Kennzeichen": move_line.debit and "S" or "H",
+ "Konto": account_number,
+ "Gegenkonto (ohne BU-Schlüssel)": offset_account_number,
+ "BU-Schlüssel": "40"
+ if move_line.account_id.datev_export_nonautomatic
+ or move_line2.account_id.datev_export_nonautomatic
+ else "",
+ "Buchungstext": move_line.name,
+ "Belegdatum": move.date.strftime("%d%m"),
+ "Belegfeld 1": move.name,
+ "Belegfeld 2": move_line2.name,
+ "KOST1 - Kostenstelle": move_line.analytic_account_id.code
+ or move_line.analytic_account_id.name
+ or move_line2.analytic_account_id.code
+ or move_line2.analytic_account_id.name,
+ "KOST-Datum": move.date.strftime("%d%m%Y"),
+ }
+ if move_line.amount_currency:
+ factor = abs(amount / (move_line.debit or move_line.credit))
+ data.update(
+ {
+ "Umsatz (ohne Soll/Haben-Kz)": (
+ "%.2f" % abs(move_line.amount_currency * factor)
+ ).replace(".", ","),
+ "WKZ Umsatz": move_line.currency_id.name,
+ "Kurs": (
+ "%.6f"
+ % (
+ 1
+ / currency._get_conversion_rate(
+ move_line.currency_id,
+ currency,
+ move.company_id,
+ move.date,
+ )
+ )
+ ).replace(".", ","),
+ "Basis-Umsatz": ("%.2f" % abs(amount)).replace(".", ","),
+ "WKZ Basis-Umsatz": currency.name,
+ }
+ )
+ yield data
+
+ def _get_data_partner(self, partner):
+ data = {
+ "Konto": self._get_partner_number(partner, "customer")
+ or self._get_partner_number(partner, "supplier")
+ or partner.id,
+ "Name (Adressattyp Unternehmen)": partner.name,
+ "Name (Adressattyp natürl. Person)": partner.name,
+ "Adressattyp": partner.is_company and "2" or "1",
+ "EU-Land": partner.country_id.code,
+ "EU-UStID": partner.vat,
+ "Kurzbezeichnung": partner.ref,
+ "Adressart": "STR",
+ "Straße": partner.street,
+ "Postleitzahl": partner.zip,
+ "Ort": partner.city,
+ "Land": partner.country_id.code,
+ "Telefon": partner.phone,
+ "E-Mail": partner.email,
+ "Internet": partner.website,
+ "IBAN-Nr. 1": partner.bank_ids[:1].acc_number,
+ "IBAN-Nr. 2": partner.bank_ids[1:2].acc_number,
+ "IBAN-Nr. 3": partner.bank_ids[2:3].acc_number,
+ "IBAN-Nr. 4": partner.bank_ids[3:4].acc_number,
+ "IBAN-Nr. 5": partner.bank_ids[4:5].acc_number,
+ "Kunden-/Lief.-Nr.": partner.ref,
+ "Steuernummer": partner.vat,
+ "Sprache": "1"
+ if (not partner.lang or partner.lang[:2] == "de")
+ else "4"
+ if partner.lang[:2] == "fr"
+ else "10"
+ if partner.lang[:2] == "es"
+ else "19"
+ if partner.lang[:2] == "it"
+ else "5",
+ }
+ yield data
+ if self._get_partner_number(partner, "customer") and self._get_partner_number(
+ partner, "supplier"
+ ):
+ data["Konto"] = self._get_partner_number(partner, "supplier")
+ yield data
+
+ def _get_data_account(self, account):
+ yield {
+ "Konto": account.code[-account.company_id.datev_account_code_length :],
+ "Kontobeschriftung": account.name,
+ "SprachId": self.env.user.lang.replace("_", "-"),
+ "Kontenbeschriftung lang": account.name,
+ }
+
+ def _get_partner_number(self, partner, number_type, generate=False):
+ if self.company_id.datev_partner_numbering == "sequence":
+ field_name = "l10n_de_datev_export_identifier_%s" % number_type
+ if not partner[field_name] and generate:
+ getattr(
+ partner,
+ "action_l10n_de_datev_export_identifier_%s" % number_type,
+ )()
+ return partner[field_name]
+ elif self.company_id.datev_partner_numbering == "ee":
+ account_length = self.env["account.general.ledger"]._get_account_length()
+ return partner[
+ "l10n_de_datev_identifier%s"
+ % ("_customer" if number_type == "customer" else "")
+ ] or str(
+ (1 if number_type == "customer" else 7) * 10**account_length
+ + partner.id
+ )
diff --git a/datev_export_dtvf/models/res_partner.py b/datev_export_dtvf/models/res_partner.py
new file mode 100644
index 000000000..bc05588ae
--- /dev/null
+++ b/datev_export_dtvf/models/res_partner.py
@@ -0,0 +1,34 @@
+# Copyright 2022 Hunki Enterprises BV
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+from odoo import fields, models
+
+
+class ResPartner(models.Model):
+ _inherit = "res.partner"
+
+ l10n_de_datev_export_identifier_customer = fields.Char("DATEV number (customer)")
+ l10n_de_datev_export_identifier_supplier = fields.Char("DATEV number (supplier)")
+ l10n_de_datev_export_show = fields.Boolean(
+ compute="_compute_l10n_de_datev_export_show"
+ )
+
+ def _compute_l10n_de_datev_export_show(self):
+ """Determine if we show the identifiers in the form"""
+ for this in self:
+ this.l10n_de_datev_export_show = (
+ self.env.company.datev_partner_numbering == "sequence"
+ )
+
+ def action_l10n_de_datev_export_identifier_customer(self):
+ """Generate number if not set"""
+ self.l10n_de_datev_export_identifier_customer = (
+ self.l10n_de_datev_export_identifier_customer
+ or self.env.company.datev_customer_sequence_id._next()
+ )
+
+ def action_l10n_de_datev_export_identifier_supplier(self):
+ """Generate number if not set"""
+ self.l10n_de_datev_export_identifier_supplier = (
+ self.l10n_de_datev_export_identifier_supplier
+ or self.env.company.datev_supplier_sequence_id._next()
+ )
diff --git a/datev_export_dtvf/readme/CONFIGURE.rst b/datev_export_dtvf/readme/CONFIGURE.rst
new file mode 100644
index 000000000..af5ebfb51
--- /dev/null
+++ b/datev_export_dtvf/readme/CONFIGURE.rst
@@ -0,0 +1,5 @@
+To configure this module, you need to:
+
+#. Go to your company
+#. Fill in the fields in the `DATEV` tab
+#. For accounts where you want to suppress automatic calculations (for ie taxes), set the according flag
diff --git a/datev_export_dtvf/readme/CONTRIBUTORS.rst b/datev_export_dtvf/readme/CONTRIBUTORS.rst
new file mode 100644
index 000000000..33b6eb2c3
--- /dev/null
+++ b/datev_export_dtvf/readme/CONTRIBUTORS.rst
@@ -0,0 +1 @@
+* Holger Brunn (https://hunki-enterprises.com)
diff --git a/datev_export_dtvf/readme/DESCRIPTION.rst b/datev_export_dtvf/readme/DESCRIPTION.rst
new file mode 100644
index 000000000..d1903e98c
--- /dev/null
+++ b/datev_export_dtvf/readme/DESCRIPTION.rst
@@ -0,0 +1 @@
+This module implements DATEV exports in the dtvf format.
diff --git a/datev_export_dtvf/readme/ROADMAP.rst b/datev_export_dtvf/readme/ROADMAP.rst
new file mode 100644
index 000000000..30ea0a1cc
--- /dev/null
+++ b/datev_export_dtvf/readme/ROADMAP.rst
@@ -0,0 +1,2 @@
+* support missing formats
+* add empty fields
diff --git a/datev_export_dtvf/readme/USAGE.rst b/datev_export_dtvf/readme/USAGE.rst
new file mode 100644
index 000000000..88aece36b
--- /dev/null
+++ b/datev_export_dtvf/readme/USAGE.rst
@@ -0,0 +1,5 @@
+To use this module, you need to:
+
+#. Go to `Invoicing` / `Reporting` / `DATEV export`
+#. Create an export, choose a date range to use as fiscal year, and ranges to export
+#. Click ``Generate``
diff --git a/datev_export_dtvf/readme/newsfragments/.gitkeep b/datev_export_dtvf/readme/newsfragments/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/datev_export_dtvf/security/ir.model.access.csv b/datev_export_dtvf/security/ir.model.access.csv
new file mode 100644
index 000000000..2a8ba62e4
--- /dev/null
+++ b/datev_export_dtvf/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+datev_export_dtvf_user,datev_export_dtvf,model_datev_export_dtvf_export,account.group_account_user,1,1,1,1
+datev_export_dtvf_manager,datev_export_dtvf,model_datev_export_dtvf_export,account.group_account_manager,1,1,1,1
diff --git a/datev_export_dtvf/static/description/icon.png b/datev_export_dtvf/static/description/icon.png
new file mode 100644
index 000000000..3a0328b51
Binary files /dev/null and b/datev_export_dtvf/static/description/icon.png differ
diff --git a/datev_export_dtvf/static/description/index.html b/datev_export_dtvf/static/description/index.html
new file mode 100644
index 000000000..dab88635b
--- /dev/null
+++ b/datev_export_dtvf/static/description/index.html
@@ -0,0 +1,447 @@
+
+
+
+
+
+
+DATEV
+
+
+
+
+
DATEV
+
+
+
+
This module implements DATEV exports in the dtvf format.
+
Table of contents
+
+
+
+
To configure this module, you need to:
+
+Go to your company
+Fill in the fields in the DATEV tab
+For accounts where you want to suppress automatic calculations (for ie taxes), set the according flag
+
+
+
+
+
To use this module, you need to:
+
+Go to Invoicing / Reporting / DATEV export
+Create an export, choose a date range to use as fiscal year, and ranges to export
+Click Generate
+
+
+
+
+
+support missing formats
+add empty fields
+
+
+
+
+
Bugs are tracked on GitHub Issues .
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us smashing it by providing a detailed and welcomed
+feedback .
+
Do not contact contributors directly about support or help with technical issues.
+
+
+
+
+
+
+
+
This module is maintained by the OCA.
+
+
OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
This module is part of the OCA/l10n-germany project on GitHub.
+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute .
+
+
+
+
+
diff --git a/datev_export_dtvf/tests/__init__.py b/datev_export_dtvf/tests/__init__.py
new file mode 100644
index 000000000..f63234ed0
--- /dev/null
+++ b/datev_export_dtvf/tests/__init__.py
@@ -0,0 +1,3 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from . import test_datev_export_dtvf
diff --git a/datev_export_dtvf/tests/test_datev_export_dtvf.py b/datev_export_dtvf/tests/test_datev_export_dtvf.py
new file mode 100644
index 000000000..b8d2897f4
--- /dev/null
+++ b/datev_export_dtvf/tests/test_datev_export_dtvf.py
@@ -0,0 +1,189 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+import base64
+import datetime
+import io
+import unittest
+import zipfile
+
+from odoo.exceptions import ValidationError
+from odoo.tests.common import Form, TransactionCase, can_import
+
+
+class TestDatevExportDtvf(TransactionCase):
+ def setUp(self):
+ super().setUp()
+ self.range = self.env["date.range"].create(
+ {
+ "name": "testrange",
+ "type_id": self.env["date.range.type"]
+ .create(
+ {
+ "name": "testtype",
+ }
+ )
+ .id,
+ "date_start": datetime.date.today(),
+ "date_end": datetime.date.today(),
+ }
+ )
+ with Form(self.env["datev_export_dtvf.export"]) as WizardForm:
+ WizardForm.fiscalyear_id = self.range
+ WizardForm.period_ids.add(self.range)
+ self.wizard = WizardForm.save()
+ self.env.user.company_id.write(
+ {
+ "datev_consultant_number": "4242424",
+ "datev_client_number": "42424",
+ "datev_account_code_length": 4,
+ }
+ )
+ self.journal = self.env["account.journal"].create(
+ {
+ "name": "Testjournal",
+ "type": "sale",
+ "code": "DTV",
+ }
+ )
+ self.account1 = self.env["account.account"].create(
+ {
+ "name": "Revenue",
+ "code": "424242",
+ "user_type_id": self.env.ref("account.data_account_type_revenue").id,
+ }
+ )
+ self.account2 = self.env["account.account"].create(
+ {
+ "name": "Receivable",
+ "code": "424243",
+ "user_type_id": self.env.ref("account.data_account_type_receivable").id,
+ "reconcile": True,
+ }
+ )
+ self.customer = self.env["res.partner"].search(
+ [("is_company", "=", True)],
+ limit=1,
+ )
+ self.move = self.env["account.move"].create(
+ {
+ "journal_id": self.journal.id,
+ "line_ids": [
+ (
+ 0,
+ 0,
+ {
+ "account_id": self.account1.id,
+ "credit": 42,
+ },
+ ),
+ (
+ 0,
+ 0,
+ {
+ "account_id": self.account2.id,
+ "debit": 42,
+ "partner_id": self.customer.id,
+ },
+ ),
+ ],
+ }
+ )
+
+ def test_validation(self):
+ """Test that we validate our input data"""
+ self.env.user.company_id.write(
+ {
+ "datev_consultant_number": None,
+ }
+ )
+ with self.assertRaises(ValidationError):
+ self.wizard.action_generate()
+
+ def test_happy_flow(self):
+ """Test generation works as expected"""
+ self.wizard.name = "Hello World"
+ self.move.action_post()
+ self.wizard.action_generate()
+ self.assertEqual(self.wizard.file_name, "Hello_World.zip")
+ zip_buffer = io.BytesIO(base64.b64decode(self.wizard.file_data))
+ self.assertTrue(zipfile.is_zipfile(zip_buffer))
+ with zipfile.ZipFile(zip_buffer) as zip_file:
+ files = zip_file.namelist()
+ partners = "EXTF_DebKred_Stamm.csv"
+ self.assertIn(partners, files)
+ self.assertIn(
+ self.customer.name,
+ zip_file.open(partners).read().decode("utf8"),
+ )
+ self.wizard.action_draft()
+ self.assertEqual(self.wizard.state, "draft")
+
+ def test_nonautomatic_flag(self):
+ """Test setting BU-Schlussel 40 works as it should"""
+ self.account2.datev_export_nonautomatic = True
+ self.move.action_post()
+ self.wizard.journal_ids = self.journal
+ self.wizard.action_generate()
+ zip_buffer = io.BytesIO(base64.b64decode(self.wizard.file_data))
+ with zipfile.ZipFile(zip_buffer) as zip_file:
+ move_line_file_name = [
+ f for f in zip_file.namelist() if f.startswith("EXTF_Buchungsstapel")
+ ][0]
+ with zip_file.open(move_line_file_name) as move_line_file:
+ move_line = move_line_file.readlines()[2].decode("utf8")
+ self.assertIn('"40"', move_line)
+
+ def test_move_line_without_account(self):
+ """Test that non-accounting (display_type!=False) lines don't crash the export"""
+ self.move.write(
+ {
+ "line_ids": [
+ (
+ 0,
+ 0,
+ {
+ "display_type": "line_note",
+ "name": "This should not crash the export",
+ },
+ )
+ ],
+ }
+ )
+ self.move.action_post()
+ self.wizard.action_generate()
+
+ def test_sequence(self):
+ """Test datev_partner_numbering = sequence"""
+ self.wizard.company_id.datev_partner_numbering = "sequence"
+ self.wizard.company_id.datev_customer_sequence_id = self.env[
+ "ir.sequence"
+ ].create(
+ {
+ "name": "DATEV customer sequence",
+ }
+ )
+ self.wizard.company_id.datev_supplier_sequence_id = self.env[
+ "ir.sequence"
+ ].create(
+ {
+ "name": "DATEV supplier sequence",
+ }
+ )
+ self.move.line_ids.write({"partner_id": self.customer.id})
+ self.assertFalse(self.customer.l10n_de_datev_export_identifier_customer)
+ self.assertFalse(self.customer.l10n_de_datev_export_identifier_supplier)
+ self.move.action_post()
+ self.wizard.action_generate()
+ self.assertTrue(self.customer.l10n_de_datev_export_identifier_customer)
+
+ @unittest.skipUnless(
+ can_import("odoo.addons.l10n_de_datev_reports"),
+ "l10n_de_datev_reports is not installed, not testing it",
+ )
+ def test_ee(self):
+ """Test datev_partner_numbering = ee"""
+ self.wizard.company_id.datev_partner_numbering = "ee"
+ self.move.line_ids.write({"partner_id": self.customer.id})
+ self.customer.l10n_de_datev_identifier_customer = "424242"
+ self.move.action_post()
+ self.wizard.action_generate()
diff --git a/datev_export_dtvf/views/account_account.xml b/datev_export_dtvf/views/account_account.xml
new file mode 100644
index 000000000..637a1360b
--- /dev/null
+++ b/datev_export_dtvf/views/account_account.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+ account.account
+
+
+
+
+
+
+
+
+
+
+
diff --git a/datev_export_dtvf/views/datev_export_dtvf.xml b/datev_export_dtvf/views/datev_export_dtvf.xml
new file mode 100644
index 000000000..cf00bb4f5
--- /dev/null
+++ b/datev_export_dtvf/views/datev_export_dtvf.xml
@@ -0,0 +1,74 @@
+
+
+
+
+
+ datev_export_dtvf.export
+
+
+
+
+
+
+ datev_export_dtvf.export
+
+
+
+
+
+
+
+
+
+
+
+ datev_export_dtvf.export
+ DATEV DTVF Export
+
+
+
+
+
diff --git a/datev_export_dtvf/views/res_partner.xml b/datev_export_dtvf/views/res_partner.xml
new file mode 100644
index 000000000..14a568574
--- /dev/null
+++ b/datev_export_dtvf/views/res_partner.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+ res.partner
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/setup/datev_export_dtvf/odoo/addons/datev_export_dtvf b/setup/datev_export_dtvf/odoo/addons/datev_export_dtvf
new file mode 120000
index 000000000..a58d18b8b
--- /dev/null
+++ b/setup/datev_export_dtvf/odoo/addons/datev_export_dtvf
@@ -0,0 +1 @@
+../../../../datev_export_dtvf
\ No newline at end of file
diff --git a/setup/datev_export_dtvf/setup.py b/setup/datev_export_dtvf/setup.py
new file mode 100644
index 000000000..28c57bb64
--- /dev/null
+++ b/setup/datev_export_dtvf/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)