diff --git a/datev_export/models/res_company.py b/datev_export/models/res_company.py index bac83be3..1074d20a 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( + "DATEV account code length", + default=5, + ) + + datev_partner_numbering = fields.Selection( + string="DATEV Partner numbering", + selection="_selection_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 d1e2a6cf..1dba0b1e 100644 --- a/datev_export/models/res_config_settings.py +++ b/datev_export/models/res_config_settings.py @@ -16,3 +16,20 @@ 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 30cf2db6..4ede3c57 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 00000000..3ff87749 --- /dev/null +++ b/datev_export_dtvf/README.rst @@ -0,0 +1,102 @@ +===== +DATEV +===== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:48207fd148cec7018b86144986e3861cedd92bc471a27d6e45e6259a2b32cff6 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/15.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-15-0/l10n-germany-15-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/builds?repo=OCA/l10n-germany&target_branch=15.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 + +The module also contains an inactive cronjob and a mail template allowing you to send period DATEV export somewhere via email. It relies on finding exactly one date range for the previous month and one for the year of the previous month, otherwise you'll see an error message in your log when running the cronjob. Note that the data transmitted via email is not encrypted. + +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 to smash 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 00000000..ec8a53e9 --- /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 00000000..8d6bee87 --- /dev/null +++ b/datev_export_dtvf/__manifest__.py @@ -0,0 +1,25 @@ +# 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": "15.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": [ + "data/ir_cron.xml", + "data/mail_template.xml", + "security/ir.model.access.csv", + "views/account_account.xml", + "views/datev_export_dtvf.xml", + "views/res_partner.xml", + ], +} diff --git a/datev_export_dtvf/data/ir_cron.xml b/datev_export_dtvf/data/ir_cron.xml new file mode 100644 index 00000000..e785b0d3 --- /dev/null +++ b/datev_export_dtvf/data/ir_cron.xml @@ -0,0 +1,34 @@ + + + Monthly DATEV export via mail (dtvf) + 1 + months + -1 + + + + + + + diff --git a/datev_export_dtvf/data/mail_template.xml b/datev_export_dtvf/data/mail_template.xml new file mode 100644 index 00000000..968d6023 --- /dev/null +++ b/datev_export_dtvf/data/mail_template.xml @@ -0,0 +1,16 @@ + + + DATEV export + {{object.name}} + test@test.com + + +

Dear recipient,

+

+ please find attached the latest DATEV export from . +

+
+
+
diff --git a/datev_export_dtvf/datev.py b/datev_export_dtvf/datev.py new file mode 100644 index 00000000..e833e6af --- /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 00000000..e69de29b diff --git a/datev_export_dtvf/models/__init__.py b/datev_export_dtvf/models/__init__.py new file mode 100644 index 00000000..2af41d0e --- /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 00000000..951c7e34 --- /dev/null +++ b/datev_export_dtvf/models/account_account.py @@ -0,0 +1,17 @@ +# 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.", + ) + datev_code = fields.Char( + help="In case your COA codes don't work for DATEV, fill in an alternative code here" + ) 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 00000000..173e27ce --- /dev/null +++ b/datev_export_dtvf/models/datev_export_dtvf.py @@ -0,0 +1,363 @@ +# 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, + 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 + if "sale_line_ids" not in move_line._fields + or "purchase_line_id" not in move_line._fields + else ( + move.mapped( + "line_ids.full_reconcile_id.reconciled_line_ids.sale_line_ids.order_id" + ) + or move.mapped("line_ids.sale_line_ids.order_id") + )[:1].name + or ( + move.mapped( + "line_ids.full_reconcile_id.reconciled_line_ids.purchase_line_id" + ".order_id" + ) + or move.mapped("line_ids.purchase_line_id.order_id") + )[:1].name + or 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.datev_code + or 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 00000000..bc05588a --- /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 00000000..00c04f49 --- /dev/null +++ b/datev_export_dtvf/readme/CONFIGURE.rst @@ -0,0 +1,7 @@ +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 + +The module also contains an inactive cronjob and a mail template allowing you to send period DATEV export somewhere via email. It relies on finding exactly one date range for the previous month and one for the year of the previous month, otherwise you'll see an error message in your log when running the cronjob. Note that the data transmitted via email is not encrypted. diff --git a/datev_export_dtvf/readme/CONTRIBUTORS.rst b/datev_export_dtvf/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..33b6eb2c --- /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 00000000..d1903e98 --- /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 00000000..30ea0a1c --- /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 00000000..88aece36 --- /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 00000000..e69de29b 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 00000000..2a8ba62e --- /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 00000000..3a0328b5 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 00000000..41ef36a0 --- /dev/null +++ b/datev_export_dtvf/static/description/index.html @@ -0,0 +1,449 @@ + + + + + +DATEV + + + +
+

DATEV

+ + +

Beta License: AGPL-3 OCA/l10n-germany Translate me on Weblate Try me on Runboat

+

This module implements DATEV exports in the dtvf format.

+

Table of contents

+ +
+

Configuration

+

To configure this module, you need to:

+
    +
  1. Go to your company
  2. +
  3. Fill in the fields in the DATEV tab
  4. +
  5. For accounts where you want to suppress automatic calculations (for ie taxes), set the according flag
  6. +
+

The module also contains an inactive cronjob and a mail template allowing you to send period DATEV export somewhere via email. It relies on finding exactly one date range for the previous month and one for the year of the previous month, otherwise you’ll see an error message in your log when running the cronjob. Note that the data transmitted via email is not encrypted.

+
+
+

Usage

+

To use this module, you need to:

+
    +
  1. Go to Invoicing / Reporting / DATEV export
  2. +
  3. Create an export, choose a date range to use as fiscal year, and ranges to export
  4. +
  5. Click Generate
  6. +
+
+
+

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 to smash 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
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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 00000000..f63234ed --- /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 00000000..b8d2897f --- /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 00000000..31e17e82 --- /dev/null +++ b/datev_export_dtvf/views/account_account.xml @@ -0,0 +1,19 @@ + + + + + + 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 00000000..cf00bb4f --- /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 00000000..14a56857 --- /dev/null +++ b/datev_export_dtvf/views/res_partner.xml @@ -0,0 +1,37 @@ + + + + + + res.partner + + + + + + +