604 lines
36 KiB
Python
604 lines
36 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
import csv
|
|
|
|
from odoo import fields, models, _
|
|
from odoo.tools import float_repr, float_compare
|
|
|
|
from datetime import datetime
|
|
from collections import namedtuple, defaultdict
|
|
import tempfile
|
|
import zipfile
|
|
import io
|
|
import re
|
|
import os
|
|
|
|
BalanceKey = namedtuple('BalanceKey', ['from_code', 'to_code', 'partner_id', 'tax_id'])
|
|
|
|
|
|
class AccountGeneralLedgerReportHandler(models.AbstractModel):
|
|
_inherit = 'account.general.ledger.report.handler'
|
|
|
|
def _custom_options_initializer(self, report, options, previous_options):
|
|
"""
|
|
Add the invoice lines search domain that common for all countries.
|
|
:param dict options: Report options
|
|
:param dict previous_options: Previous report options
|
|
"""
|
|
super()._custom_options_initializer(report, options, previous_options)
|
|
if self.env.company.country_code in ('DE', 'CH', 'AT'):
|
|
options.setdefault('buttons', []).extend((
|
|
{
|
|
'name': _('Datev DATA (zip)'),
|
|
'sequence': 30,
|
|
'action': 'export_file',
|
|
'action_param': 'l10n_de_datev_export_to_zip',
|
|
'file_export_type': _('Datev zip'),
|
|
},
|
|
{
|
|
'name': _('Datev ATCH (zip)'),
|
|
'sequence': 40,
|
|
'action': 'export_file',
|
|
'action_param': 'l10_de_datev_export_to_zip_and_attach',
|
|
'file_export_type': _('Datev + batch zip'),
|
|
},
|
|
))
|
|
|
|
def _l10n_de_datev_build_line(
|
|
self,
|
|
*,
|
|
line_amount_currency,
|
|
line_amount,
|
|
currency,
|
|
company_currency,
|
|
account_code,
|
|
counterpart_code,
|
|
code_correction,
|
|
move,
|
|
receipt1,
|
|
receipt2,
|
|
reference_text,
|
|
force_debit_credit=None,
|
|
):
|
|
"""
|
|
Build and return a 125-field DATEV export row.
|
|
"""
|
|
# Idiotic program needs to have a line with 125 elements ordered in a given fashion as it
|
|
# does not take into account the header and non mandatory fields
|
|
array = ['' for _ in range(125)]
|
|
# For DateV, we can't have negative amount on a line, so we need to inverse the amount and inverse the
|
|
# credit/debit symbol.
|
|
if force_debit_credit:
|
|
array[1] = force_debit_credit
|
|
else:
|
|
array[1] = 'H' if currency.compare_amounts(line_amount, 0) < 0 else 'S'
|
|
line_amount = abs(line_amount)
|
|
# Column A: the amount in the currency that was used. It can be a foreign one, it can be the company's.
|
|
array[0] = float_repr(line_amount_currency, currency.decimal_places).replace('.', ',')
|
|
# Column C: the corresponding foreign currency used on the original record (invoice, bill, entry, ....)
|
|
array[2] = currency.name
|
|
if currency != company_currency:
|
|
# Column D: ratio is E/A if D !=0, else no rate.
|
|
if not company_currency.is_zero(line_amount) and not currency.is_zero(line_amount_currency):
|
|
rate = round(line_amount_currency / line_amount, 6) or 1.0
|
|
else:
|
|
rate = 1.0
|
|
array[3] = str(rate).replace('.', ',')
|
|
# Column E: the amount converted in the company currency if the original record was in a foreign currency
|
|
array[4] = float_repr(line_amount, company_currency.decimal_places).replace('.', ',')
|
|
# Column F: the company currency if the original record was in a foreign currency
|
|
array[5] = company_currency.name
|
|
array[6] = account_code
|
|
array[7] = counterpart_code
|
|
array[8] = code_correction
|
|
array[9] = datetime.strftime(move.date, '%-d%m')
|
|
array[10] = receipt1[-36:]
|
|
array[11] = receipt2
|
|
array[13] = reference_text or ''
|
|
if move.message_main_attachment_id:
|
|
array[19] = f'BEDI "{move._l10n_de_datev_get_guid()}"'
|
|
|
|
return array
|
|
|
|
def l10_de_datev_export_to_zip_and_attach(self, options):
|
|
options['add_attachments'] = True
|
|
return self.l10n_de_datev_export_to_zip(options)
|
|
|
|
def l10n_de_datev_export_to_zip(self, options):
|
|
"""
|
|
Check ir_attachment for method _get_path
|
|
create a sha and replace 2 first letters by something not hexadecimal
|
|
Return full_path as 2nd args, use it as name for Zipfile
|
|
Don't need to unlink as it will be done automatically by garbage collector
|
|
of attachment cron
|
|
"""
|
|
report = self.env['account.report'].browse(options['report_id'])
|
|
with tempfile.NamedTemporaryFile(mode='w+b', delete=True) as buf:
|
|
with zipfile.ZipFile(buf, mode="w", compression=zipfile.ZIP_DEFLATED, allowZip64=False) as zf:
|
|
domain = report._get_options_domain(options, 'strict_range')
|
|
move_line_ids = self.env['account.move.line'].search(domain).ids
|
|
|
|
domain = [
|
|
('line_ids', 'in', move_line_ids),
|
|
('company_id', 'in', report.get_report_company_ids(options)),
|
|
]
|
|
if options.get('all_entries'):
|
|
domain += [('state', '!=', 'cancel')]
|
|
else:
|
|
domain += [('state', '=', 'posted')]
|
|
if options.get('date'):
|
|
domain += [('date', '<=', options['date']['date_to'])]
|
|
# cannot set date_from on move as domain depends on the move line account if "strict_range" is False
|
|
domain += report._get_options_journals_domain(options)
|
|
moves = self.env['account.move'].search(domain)
|
|
if options.get('add_attachments'):
|
|
# add all moves attachments in zip file
|
|
slash_re = re.compile('[\\/]')
|
|
documents = []
|
|
for move in moves.filtered(lambda m: m.message_main_attachment_id):
|
|
# '\' is not allowed in file name, replace by '-'
|
|
base_name = slash_re.sub('-', move.name)
|
|
# The message_main_attachment_id may not be linked to the associated move causing AccessError
|
|
# For example, with an image, the res_id is 0 and the res_model is False
|
|
attachment = move.sudo().message_main_attachment_id
|
|
extension = f".{attachment.name.split('.')[-1]}"
|
|
name = '%(base)s%(extension)s' % {'base': base_name, 'extension': extension}
|
|
zf.writestr(name, attachment.raw)
|
|
documents.append({
|
|
'guid': move._l10n_de_datev_get_guid(),
|
|
'filename': name,
|
|
'type': 2 if move.is_sale_document() else 1 if move.is_purchase_document() else None,
|
|
})
|
|
if documents:
|
|
metadata_document = self.env['ir.qweb']._render(
|
|
'l10n_de_reports.datev_export_metadata',
|
|
values={
|
|
'documents': documents,
|
|
'date': fields.Datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
|
|
},
|
|
)
|
|
zf.writestr('document.xml', "<?xml version='1.0' encoding='UTF-8'?>" + str(metadata_document))
|
|
else:
|
|
# ZIP for Data => csv
|
|
set_move_line_ids = set(move_line_ids)
|
|
zf.writestr('EXTF_accounting_entries.csv', self._l10n_de_datev_get_csv(options, moves))
|
|
zf.writestr('EXTF_customer_accounts.csv', self._l10n_de_datev_get_partner_list(options, set_move_line_ids, customer=True))
|
|
zf.writestr('EXTF_vendor_accounts.csv', self._l10n_de_datev_get_partner_list(options, set_move_line_ids, customer=False))
|
|
buf.seek(0)
|
|
content = buf.read()
|
|
|
|
filename, extension = report.get_default_report_filename(options, 'ZIP').rsplit('.', 1)
|
|
return {
|
|
'file_name': f'{filename}_atch.{extension}' if options.get('add_attachments') else f'{filename}_data.{extension}',
|
|
'file_content': content,
|
|
'file_type': 'zip'
|
|
}
|
|
|
|
def _l10n_de_datev_get_client_number(self):
|
|
consultant_number = self.env.company.l10n_de_datev_consultant_number
|
|
client_number = self.env.company.l10n_de_datev_client_number
|
|
if not consultant_number:
|
|
consultant_number = 99999
|
|
if not client_number:
|
|
client_number = 999
|
|
return [consultant_number, client_number]
|
|
|
|
def _l10n_de_datev_get_partner_list(self, options, move_line_ids, customer=True):
|
|
date_to = fields.Date.from_string(options.get('date').get('date_to'))
|
|
fy = self.env.company.compute_fiscalyear_dates(date_to)
|
|
|
|
fy = datetime.strftime(fy.get('date_from'), '%Y%m%d')
|
|
datev_info = self._l10n_de_datev_get_client_number()
|
|
account_length = self._l10n_de_datev_get_account_length()
|
|
|
|
output = io.StringIO()
|
|
writer = csv.writer(output, delimiter=';', quotechar='"', quoting=2)
|
|
preheader = ['EXTF', 700, 16, 'Debitoren/Kreditoren', 5, None, None, '', '', '', datev_info[0], datev_info[1], fy, account_length,
|
|
'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
|
|
header = [
|
|
'Konto', 'Name (Adressattyp Unternehmen)', 'Unternehmensgegenstand', 'Name (Adressattyp natürl. Person)',
|
|
'Vorname (Adressattyp natürl. Person)', 'Name (Adressattyp keine Angabe)', 'Adressattyp', 'Kurzbezeichnung',
|
|
'EU-Land', 'EU-UStID', 'Anrede', 'Titel/Akad. Grad', 'Adelstitel', 'Namensvorsatz', 'Adressart', 'Straße',
|
|
'Postfach', 'Postleitzahl', 'Ort', 'Land', 'Versandzusatz', 'Adresszusatz', 'Abweichende Anrede', 'Abw. Zustellbezeichnung 1',
|
|
'Abw. Zustellbezeichnung 2', 'Kennz. Korrespondenzadresse', 'Adresse Gültig von', 'Adresse Gültig bis', 'Telefon',
|
|
'Bemerkung (Telefon)', 'Telefon GL', 'Bemerkung (Telefon GL)', 'E-Mail', 'Bemerkung (E-Mail)', 'Internet',
|
|
'Bemerkung (Internet)', 'Fax', 'Bemerkung (Fax)', 'Sonstige', 'Bemerkung (Sonstige)', 'Bankleitzahl 1',
|
|
'Bankbezeichnung 1', 'Bank-Kontonummer 1', 'Länderkennzeichen 1', 'IBAN-Nr. 1', 'Leerfeld', 'SWIFT-Code 1',
|
|
'Abw. Kontoinhaber 1', 'Kennz. Hauptbankverb. 1', 'Bankverb 1 Gültig von', 'Bankverb 1 Gültig bis', 'Bankleitzahl 2',
|
|
'Bankbezeichnung 2', 'Bank-Kontonummer 2', 'Länderkennzeichen 2', 'IBAN-Nr. 2', 'Leerfeld', 'SWIFT-Code 2',
|
|
'Abw. Kontoinhaber 2', 'Kennz. Hauptbankverb. 2', 'Bankverb 2 Gültig von', 'Bankverb 2 Gültig bis', 'Bankleitzahl 3',
|
|
'Bankbezeichnung 3', 'Bank-Kontonummer 3', 'Länderkennzeichen 3', 'IBAN-Nr. 3', 'Leerfeld', 'SWIFT-Code 3',
|
|
'Abw. Kontoinhaber 3', 'Kennz. Hauptbankverb. 3', 'Bankverb 3 Gültig von', 'Bankverb 3 Gültig bis', 'Bankleitzahl 4',
|
|
'Bankbezeichnung 4', 'Bank-Kontonummer 4', 'Länderkennzeichen 4', 'IBAN-Nr. 4', 'Leerfeld', 'SWIFT-Code 4',
|
|
'Abw. Kontoinhaber 4', 'Kennz. Hauptbankverb. 4', 'Bankverb 4 Gültig von', 'Bankverb 4 Gültig bis', 'Bankleitzahl 5',
|
|
'Bankbezeichnung 5', 'Bank-Kontonummer 5', 'Länderkennzeichen 5', 'IBAN-Nr. 5', 'Leerfeld', 'SWIFT-Code 5',
|
|
'Abw. Kontoinhaber 5', 'Kennz. Hauptbankverb. 5', 'Bankverb 5 Gültig von', 'Bankverb 5 Gültig bis', 'Leerfeld',
|
|
'Briefanrede', 'Grußformel', 'Kunden-/Lief.-Nr.', 'Steuernummer', 'Sprache', 'Ansprechpartner', 'Vertreter',
|
|
'Sachbearbeiter', 'Diverse-Konto', 'Ausgabeziel', 'Währungssteuerung', 'Kreditlimit (Debitor)', 'Zahlungsbedingung',
|
|
'Fälligkeit in Tagen (Debitor)', 'Skonto in Prozent (Debitor)', 'Kreditoren-Ziel 1 Tg.', 'Kreditoren-Skonto 1 %',
|
|
'Kreditoren-Ziel 2 Tg.', 'Kreditoren-Skonto 2 %', 'Kreditoren-Ziel 3 Brutto Tg.', 'Kreditoren-Ziel 4 Tg.',
|
|
'Kreditoren-Skonto 4 %', 'Kreditoren-Ziel 5 Tg.', 'Kreditoren-Skonto 5 %', 'Mahnung', 'Kontoauszug', 'Mahntext 1',
|
|
'Mahntext 2', 'Mahntext 3', 'Kontoauszugstext', 'Mahnlimit Betrag', 'Mahnlimit %', 'Zinsberechnung', 'Mahnzinssatz 1',
|
|
'Mahnzinssatz 2', 'Mahnzinssatz 3', 'Lastschrift', 'Leerfeld', 'Mandantenbank', 'Zahlungsträger', 'Indiv. Feld 1',
|
|
'Indiv. Feld 2', 'Indiv. Feld 3', 'Indiv. Feld 4', 'Indiv. Feld 5', 'Indiv. Feld 6', 'Indiv. Feld 7', 'Indiv. Feld 8',
|
|
'Indiv. Feld 9', 'Indiv. Feld 10', 'Indiv. Feld 11', 'Indiv. Feld 12', 'Indiv. Feld 13', 'Indiv. Feld 14',
|
|
'Indiv. Feld 15', 'Abweichende Anrede (Rechnungsadresse)', 'Adressart (Rechnungsadresse)', 'Straße (Rechnungsadresse)',
|
|
'Postfach (Rechnungsadresse)', 'Postleitzahl (Rechnungsadresse)', 'Ort (Rechnungsadresse)', 'Land (Rechnungsadresse)',
|
|
'Versandzusatz (Rechnungsadresse)', 'Adresszusatz (Rechnungsadresse)', 'Abw. Zustellbezeichnung 1 (Rechnungsadresse)',
|
|
'Abw. Zustellbezeichnung 2 (Rechnungsadresse)', 'Adresse Gültig von (Rechnungsadresse)', 'Adresse Gültig bis (Rechnungsadresse)',
|
|
'Bankleitzahl 6', 'Bankbezeichnung 6', 'Bank-Kontonummer 6', 'Länderkennzeichen 6', 'IBAN-Nr. 6', 'Leerfeld',
|
|
'SWIFT-Code 6', 'Abw. Kontoinhaber 6', 'Kennz. Hauptbankverb. 6', 'Bankverb 6 Gültig von', 'Bankverb 6 Gültig bis',
|
|
'Bankleitzahl 7', 'Bankbezeichnung 7', 'Bank-Kontonummer 7', 'Länderkennzeichen 7', 'IBAN-Nr. 7', 'Leerfeld',
|
|
'SWIFT-Code 7', 'Abw. Kontoinhaber 7', 'Kennz. Hauptbankverb. 7', 'Bankverb 7 Gültig von', 'Bankverb 7 Gültig bis',
|
|
'Bankleitzahl 8', 'Bankbezeichnung 8', 'Bank-Kontonummer 8', 'Länderkennzeichen 8', 'IBAN-Nr. 8', 'Leerfeld',
|
|
'SWIFT-Code 8', 'Abw. Kontoinhaber 8', 'Kennz. Hauptbankverb. 8', 'Bankverb 8 Gültig von', 'Bankverb 8 Gültig bis',
|
|
'Bankleitzahl 9', 'Bankbezeichnung 9', 'Bank-Kontonummer 9', 'Länderkennzeichen 9', 'IBAN-Nr. 9', 'Leerfeld',
|
|
'SWIFT-Code 9', 'Abw. Kontoinhaber 9', 'Kennz. Hauptbankverb. 9', 'Bankverb 9 Gültig von', 'Bankverb 9 Gültig bis',
|
|
'Bankleitzahl 10', 'Bankbezeichnung 10', 'Bank-Kontonummer 10', 'Länderkennzeichen 10', 'IBAN-Nr. 10', 'Leerfeld',
|
|
'SWIFT-Code 10', 'Abw. Kontoinhaber 10', 'Kennz. Hauptbankverb. 10', 'Bankverb 10 Gültig von', 'Bankverb 10 Gültig bis',
|
|
'Nummer Fremdsystem', 'Insolvent', 'SEPA-Mandatsreferenz 1', 'SEPA-Mandatsreferenz 2', 'SEPA-Mandatsreferenz 3',
|
|
'SEPA-Mandatsreferenz 4', 'SEPA-Mandatsreferenz 5', 'SEPA-Mandatsreferenz 6', 'SEPA-Mandatsreferenz 7',
|
|
'SEPA-Mandatsreferenz 8', 'SEPA-Mandatsreferenz 9', 'SEPA-Mandatsreferenz 10', 'Verknüpftes OPOS-Konto',
|
|
'Mahnsperre bis', 'Lastschriftsperre bis', 'Zahlungssperre bis', 'Gebührenberechnung', 'Mahngebühr 1', 'Mahngebühr 2',
|
|
'Mahngebühr 3', 'Pauschalenberechnung', 'Verzugspauschale 1', 'Verzugspauschale 2', 'Verzugspauschale 3',
|
|
'Alternativer Suchname', 'Status', 'Anschrift manuell geändert (Korrespondenzadresse)', 'Anschrift individuell (Korrespondenzadresse)',
|
|
'Anschrift manuell geändert (Rechnungsadresse)', 'Anschrift individuell (Rechnungsadresse)', 'Fristberechnung bei Debitor',
|
|
'Mahnfrist 1', 'Mahnfrist 2', 'Mahnfrist 3', 'Letzte Frist',
|
|
]
|
|
|
|
lines = [preheader, header]
|
|
|
|
if len(move_line_ids):
|
|
if customer:
|
|
move_types = ('out_refund', 'out_invoice', 'out_receipt')
|
|
else:
|
|
move_types = ('in_refund', 'in_invoice', 'in_receipt')
|
|
select = """SELECT distinct(aml.partner_id)
|
|
FROM account_move_line aml
|
|
JOIN account_move m
|
|
ON aml.move_id = m.id
|
|
WHERE aml.id IN %s
|
|
AND aml.tax_line_id IS NULL
|
|
AND aml.debit != aml.credit
|
|
AND m.move_type IN %s
|
|
AND aml.account_id != m.l10n_de_datev_main_account_id
|
|
AND aml.partner_id IS NOT NULL"""
|
|
self.env.cr.execute(select, (tuple(move_line_ids), move_types))
|
|
partners = self.env['res.partner'].browse([p.get('partner_id') for p in self.env.cr.dictfetchall()])
|
|
eu_country_codes = self.env.ref('base.europe').country_ids.mapped('code')
|
|
for partner in partners:
|
|
if customer:
|
|
code = self._l10n_de_datev_find_partner_account(partner.property_account_receivable_id, partner)
|
|
else:
|
|
code = self._l10n_de_datev_find_partner_account(partner.property_account_payable_id, partner)
|
|
vat_is_valid = False
|
|
|
|
country_code = ''
|
|
if partner.vat and len(partner.vat) > 2:
|
|
vat_country, vat_id_no = partner._split_vat(partner.vat)
|
|
vat_is_valid = vat_country and partner._check_vat_number(vat_country, vat_id_no)
|
|
if vat_country.isalpha():
|
|
country_code = 'GR' if vat_country.upper() == 'EL' else vat_country.upper()
|
|
is_eu_vat_number = country_code in eu_country_codes
|
|
|
|
line_value = {
|
|
'code': code,
|
|
'company_name': partner.name if partner.is_company else '',
|
|
'person_name': '' if partner.is_company else partner.name,
|
|
'natural': partner.is_company and '2' or '1',
|
|
'eu_vat_country': vat_country.upper() if vat_is_valid and is_eu_vat_number and vat_country.isalpha() else '',
|
|
'eu_vat_id_no': vat_id_no if vat_is_valid and is_eu_vat_number else '',
|
|
'country_code': partner.country_code or country_code or '',
|
|
}
|
|
# Idiotic program needs to have a line with 243 elements ordered in a given fashion as it
|
|
# does not take into account the header and non mandatory fields
|
|
array = ['' for x in range(243)]
|
|
array[0] = line_value.get('code')
|
|
array[1] = line_value.get('company_name')
|
|
array[3] = line_value.get('person_name')
|
|
array[6] = line_value.get('natural')
|
|
array[8] = line_value.get('eu_vat_country')
|
|
array[9] = line_value.get('eu_vat_id_no')
|
|
array[19] = line_value.get('country_code')
|
|
lines.append(array)
|
|
writer.writerows(lines)
|
|
return output.getvalue()
|
|
|
|
def _l10n_de_datev_get_account_length(self):
|
|
return self.env.company.l10n_de_datev_account_length
|
|
|
|
def _l10n_de_datev_find_partner_account(self, account, partner):
|
|
len_param = self._l10n_de_datev_get_account_length() + 1
|
|
codes = {
|
|
'asset_receivable': (partner.l10n_de_datev_identifier_customer or int('1'.ljust(len_param, '0')) + partner.id),
|
|
'liability_payable': (partner.l10n_de_datev_identifier or int('7'.ljust(len_param, '0')) + partner.id)
|
|
}
|
|
if account.account_type in codes and partner:
|
|
return codes[account.account_type]
|
|
else:
|
|
return str(account.code).ljust(len_param - 1, '0') if account else ''
|
|
|
|
# Source: http://www.datev.de/dnlexom/client/app/index.html#/document/1036228/D103622800029
|
|
def _l10n_de_datev_get_csv(self, options, moves):
|
|
# last 2 element of preheader should be filled by "consultant number" and "client number"
|
|
date_from = fields.Date.from_string(options.get('date').get('date_from'))
|
|
date_to = fields.Date.from_string(options.get('date').get('date_to'))
|
|
fy = self.env.company.compute_fiscalyear_dates(date_to)
|
|
|
|
date_from = datetime.strftime(date_from, '%Y%m%d')
|
|
date_to = datetime.strftime(date_to, '%Y%m%d')
|
|
fy = datetime.strftime(fy.get('date_from'), '%Y%m%d')
|
|
datev_info = self._l10n_de_datev_get_client_number()
|
|
account_length = self._l10n_de_datev_get_account_length()
|
|
|
|
output = io.StringIO()
|
|
writer = csv.writer(output, delimiter=';', quotechar='"', quoting=2)
|
|
preheader = ['EXTF', 700, 21, 'Buchungsstapel', 13, '', '', '', '', '', datev_info[0], datev_info[1], fy, account_length,
|
|
date_from, date_to, '', '', '', '', 0, 'EUR', '', '', '', '', '', '', '', '', '']
|
|
header = [
|
|
'Umsatz (ohne Soll/Haben-Kz)', 'Soll/Haben-Kennzeichen', 'WKZ Umsatz', 'Kurs', 'Basis-Umsatz', 'WKZ Basis-Umsatz',
|
|
'Konto', 'Gegenkonto (ohne BU-Schlüssel)', 'BU-Schlüssel', 'Belegdatum', 'Belegfeld 1', 'Belegfeld 2', 'Skonto',
|
|
'Buchungstext', 'Postensperre', 'Diverse Adressnummer', 'Geschäftspartnerbank', 'Sachverhalt', 'Zinssperre',
|
|
'Beleglink', 'Beleginfo - Art 1', 'Beleginfo - Inhalt 1', 'Beleginfo - Art 2', 'Beleginfo - Inhalt 2',
|
|
'Beleginfo - Art 3', 'Beleginfo - Inhalt 3', 'Beleginfo - Art 4', 'Beleginfo - Inhalt 4',
|
|
'Beleginfo - Art 5', 'Beleginfo - Inhalt 5', 'Beleginfo - Art 6', 'Beleginfo - Inhalt 6',
|
|
'Beleginfo - Art 7', 'Beleginfo - Inhalt 7', 'Beleginfo - Art 8', 'Beleginfo - Inhalt 8', 'KOST1 - Kostenstelle',
|
|
'KOST2 - Kostenstelle', 'Kost-Menge', 'EU-Land u. UStID (Bestimmung)', 'EU-Steuersatz (Bestimmung)',
|
|
'Abw. Versteuerungsart', 'Sachverhalt L+L', 'Funktionsergänzung L+L', 'BU 49 Hauptfunktionstyp', 'BU 49 Hauptfunktionsnummer',
|
|
'BU 49 Funktionsergänzung', 'Zusatzinformation - Art 1', 'Zusatzinformation- Inhalt 1', 'Zusatzinformation - Art 2',
|
|
'Zusatzinformation- Inhalt 2', 'Zusatzinformation - Art 3', 'Zusatzinformation- Inhalt 3', 'Zusatzinformation - Art 4',
|
|
'Zusatzinformation- Inhalt 4', 'Zusatzinformation - Art 5', 'Zusatzinformation- Inhalt 5', 'Zusatzinformation - Art 6',
|
|
'Zusatzinformation- Inhalt 6', 'Zusatzinformation - Art 7', 'Zusatzinformation- Inhalt 7', 'Zusatzinformation - Art 8',
|
|
'Zusatzinformation- Inhalt 8', 'Zusatzinformation - Art 9', 'Zusatzinformation- Inhalt 9', 'Zusatzinformation - Art 10',
|
|
'Zusatzinformation- Inhalt 10', 'Zusatzinformation - Art 11', 'Zusatzinformation- Inhalt 11', 'Zusatzinformation - Art 12',
|
|
'Zusatzinformation- Inhalt 12', 'Zusatzinformation - Art 13', 'Zusatzinformation- Inhalt 13', 'Zusatzinformation - Art 14',
|
|
'Zusatzinformation- Inhalt 14', 'Zusatzinformation - Art 15', 'Zusatzinformation- Inhalt 15', 'Zusatzinformation - Art 16',
|
|
'Zusatzinformation- Inhalt 16', 'Zusatzinformation - Art 17', 'Zusatzinformation- Inhalt 17', 'Zusatzinformation - Art 18',
|
|
'Zusatzinformation- Inhalt 18', 'Zusatzinformation - Art 19', 'Zusatzinformation- Inhalt 19', 'Zusatzinformation - Art 20',
|
|
'Zusatzinformation- Inhalt 20', 'Stück', 'Gewicht', 'Zahlweise', 'Forderungsart', 'Veranlagungsjahr', 'Zugeordnete Fälligkeit',
|
|
'Skontotyp', 'Auftragsnummer', 'Buchungstyp', 'USt-Schlüssel (Anzahlungen)', 'EU-Land (Anzahlungen)', 'Sachverhalt L+L (Anzahlungen)',
|
|
'EU-Steuersatz (Anzahlungen)', 'Erlöskonto (Anzahlungen)', 'Herkunft-Kz', 'Buchungs GUID', 'KOST-Datum', 'SEPA-Mandatsreferenz',
|
|
'Skontosperre', 'Gesellschaftername', 'Beteiligtennummer', 'Identifikationsnummer', 'Zeichnernummer', 'Postensperre bis',
|
|
'Bezeichnung SoBil-Sachverhalt', 'Kennzeichen SoBil-Buchung', 'Festschreibung', 'Leistungsdatum', 'Datum Zuord. Steuerperiode',
|
|
'Fälligkeit', 'Generalumkehr (GU)', 'Steuersatz', 'Land', 'Abrechnungsreferenz', 'BVV-Position', 'EU-Land u. UStID (Ursprung)',
|
|
'EU-Steuersatz (Ursprung)', 'Abw. Skontokonto',
|
|
]
|
|
|
|
lines = [preheader, header]
|
|
|
|
for m in moves:
|
|
delta_by_aml = defaultdict(float) # the final delta to apply to each aml
|
|
# For bills, it's possible to edit the total amount of a tax group manually.
|
|
# Unfortunatly this change only affects the amounts in tax_totals field, but not
|
|
# price_total field of the invoice lines, which is used in the export.
|
|
# Therefore, the exported amount can be wrong because it doesn't include the tax change.
|
|
# To detect such a case, we check if the sum of price_total of each line is equal to
|
|
# the total amount in tax_totals.
|
|
# It can happen that price_total is not set. In this case, the exported amount is
|
|
# recomputed and also differs from the edited amount.
|
|
# However, this case is not handled here for the moment.
|
|
if (m.move_type == 'in_invoice' and
|
|
all(line.price_total for line in m.invoice_line_ids) and
|
|
float_compare(
|
|
m.tax_totals['total_amount'],
|
|
sum(m.invoice_line_ids.mapped('price_total')),
|
|
precision_rounding=m.currency_id.rounding,
|
|
)
|
|
):
|
|
# The amount of a tax group has probably been edited manually.
|
|
# So we cannot use price_total of the aml as it is and we should dispatch the
|
|
# delta between all the lines where a tax of the same group has been used.
|
|
amls_by_group = defaultdict(list)
|
|
# Get the modified tax group amounts
|
|
actual_values_by_group = {
|
|
self.env['account.tax.group'].browse(tax_group['id']): tax_group['tax_amount_currency']
|
|
for subtotal in m.tax_totals['subtotals']
|
|
for tax_group in subtotal['tax_groups']
|
|
}
|
|
# Get the originally computed values
|
|
original_values_by_group = defaultdict(float)
|
|
for line in m.invoice_line_ids:
|
|
line_taxes = line.tax_ids.compute_all(line.amount_currency, line.currency_id, partner=line.partner_id, handle_price_include=False)
|
|
tax_amounts = defaultdict(float)
|
|
for tax_data in line_taxes['taxes']:
|
|
tax_amounts[tax_data['id']] += tax_data['amount']
|
|
for tax_id in tax_amounts:
|
|
tax = self.env['account.tax'].browse(tax_id)
|
|
original_values_by_group[tax.tax_group_id] += tax_amounts[tax.id]
|
|
amls_by_group[tax.tax_group_id].append(line)
|
|
# Compute deltas by tax group and assign the difference
|
|
for tax_group, actual_group_amount in actual_values_by_group.items():
|
|
delta_group_amount = actual_group_amount - original_values_by_group[tax_group]
|
|
current_lines = amls_by_group[tax_group]
|
|
nb_lines = len(current_lines)
|
|
# Round and add to every line
|
|
line_delta_amount = line.currency_id.round(delta_group_amount / nb_lines)
|
|
for line in current_lines:
|
|
delta_by_aml[line] += line_delta_amount
|
|
# Add eventual rounding remainder to the first line of the tax group
|
|
remainder = line.currency_id.round(delta_group_amount - line_delta_amount * nb_lines)
|
|
delta_by_aml[current_lines[0]] += remainder
|
|
|
|
payment_account = 0 # Used for non-reconciled payments
|
|
|
|
move_balance = 0
|
|
counterpart_amount = 0
|
|
last_tax_line_index = 0
|
|
|
|
# Check for three currency case
|
|
# A three-currency case occurs when a bank transaction involves:
|
|
# - the payer's transaction currency,
|
|
# - the bank journal's currency,
|
|
# - the company's currency.
|
|
# DATEV cannot represent both foreign currencies on the same journal item,
|
|
# so foreign AR/AP lines are routed through a clearing account.
|
|
clearing_account = ''
|
|
foreign_arap_lines = self.env['account.move.line']
|
|
emit_bank_line = True
|
|
if m.statement_line_id:
|
|
liquidity_lines, _, other_lines = m.statement_line_id._seek_for_lines()
|
|
if len(liquidity_lines) == 1:
|
|
foreign_arap_lines = other_lines.filtered(
|
|
lambda aml: (
|
|
aml.account_id.account_type in (
|
|
'asset_receivable',
|
|
'liability_payable',
|
|
)
|
|
and not aml.is_same_currency
|
|
and aml.currency_id != liquidity_lines[0].currency_id
|
|
)
|
|
)
|
|
if foreign_arap_lines:
|
|
account_length = self._l10n_de_datev_get_account_length()
|
|
clearing_base = (
|
|
"1460"
|
|
if m.company_id.chart_template == "de_skr04"
|
|
else "1360"
|
|
)
|
|
clearing_account = clearing_base.ljust(account_length, '0')
|
|
|
|
def _get_code_correction(taxes):
|
|
codes = set(taxes.mapped('l10n_de_datev_code'))
|
|
# there should be exactly one, else skip code
|
|
return len(codes) == 1 and codes.pop() or ''
|
|
|
|
for aml in m.line_ids:
|
|
code_correction = ''
|
|
if aml.debit == aml.credit:
|
|
# Ignore debit = credit = 0
|
|
continue
|
|
|
|
# account and counterpart account
|
|
to_account_code = str(self._l10n_de_datev_find_partner_account(aml.move_id.l10n_de_datev_main_account_id, aml.partner_id))
|
|
account_code = u'{code}'.format(code=self._l10n_de_datev_find_partner_account(aml.account_id, aml.partner_id))
|
|
|
|
# We don't want to have lines with our outstanding payment/receipt as they don't represent real moves
|
|
# So if payment skip one move line to write, while keeping the account
|
|
# and replace bank account for outstanding payment/receipt for the other line
|
|
|
|
if aml.payment_id:
|
|
# An expense may be encoded with a tax, we should report the corresponding tax code
|
|
if not code_correction:
|
|
code_correction = _get_code_correction(m.line_ids.tax_ids)
|
|
if payment_account == 0:
|
|
payment_account = account_code
|
|
counterpart_amount += aml.balance
|
|
continue
|
|
else:
|
|
to_account_code = payment_account
|
|
|
|
# If both account and counteraccount are the same, ignore the line
|
|
if aml.account_id == aml.move_id.l10n_de_datev_main_account_id:
|
|
if aml.statement_line_id and not aml.payment_id:
|
|
counterpart_amount += aml.balance
|
|
continue
|
|
|
|
# If line is a tax ignore it as datev requires single line with gross amount and deduct tax itself based
|
|
# on account or on the control key code
|
|
if aml.tax_line_id:
|
|
continue
|
|
|
|
if aml.price_total and m.move_type != 'entry':
|
|
sign = -1 if aml.currency_id.compare_amounts(aml.balance, 0) < 0 else 1
|
|
line_amount_currency = abs(aml.price_total) * sign
|
|
|
|
else:
|
|
aml_taxes = aml.tax_ids.compute_all(aml.amount_currency, aml.currency_id, partner=aml.partner_id, handle_price_include=False)
|
|
line_amount_currency = aml_taxes['total_included']
|
|
# convert line_amount in company currency
|
|
if aml.currency_id != aml.company_id.currency_id:
|
|
if not aml.currency_id.is_zero(line_amount_currency):
|
|
rate = m._get_product_base_line_currency_rate(aml)
|
|
line_amount = line_amount_currency / rate
|
|
else:
|
|
line_amount = aml.balance
|
|
else:
|
|
line_amount = line_amount_currency
|
|
|
|
move_balance += line_amount
|
|
|
|
if aml.tax_ids:
|
|
last_tax_line_index = len(lines)
|
|
last_tax_line_amount = line_amount
|
|
code_correction = _get_code_correction(aml.tax_ids)
|
|
|
|
# reference
|
|
receipt1 = ref = aml.move_id.name
|
|
if aml.move_id.journal_id.type == 'purchase' and aml.move_id.ref:
|
|
ref = aml.move_id.ref
|
|
|
|
# on receivable/payable aml of sales/purchases
|
|
receipt2 = ''
|
|
if to_account_code == account_code and aml.date_maturity:
|
|
receipt2 = aml.date
|
|
|
|
if aml in foreign_arap_lines:
|
|
if emit_bank_line:
|
|
lines.append(
|
|
self._l10n_de_datev_build_clearing_liquidity_line(
|
|
m,
|
|
liquidity_lines[0],
|
|
clearing_account,
|
|
foreign_arap_lines,
|
|
)
|
|
)
|
|
emit_bank_line = False
|
|
|
|
to_account_code = clearing_account
|
|
|
|
lines.append(
|
|
self._l10n_de_datev_build_line(
|
|
line_amount_currency=abs(line_amount_currency) + delta_by_aml[aml],
|
|
line_amount=line_amount,
|
|
currency=aml.currency_id,
|
|
company_currency=aml.company_id.currency_id,
|
|
account_code=account_code,
|
|
counterpart_code=to_account_code,
|
|
code_correction=code_correction,
|
|
move=m,
|
|
receipt1=receipt1,
|
|
receipt2=receipt2,
|
|
reference_text=aml.name or ref,
|
|
)
|
|
)
|
|
# In case of epd we actively fix rounding issues by checking the base line and tax line
|
|
# amounts against the move amount missing cent and adjust the vals accordingly.
|
|
# Since here we have to recompute the tax values for each line with tax, we need
|
|
# to replicate the rounding fix logic adding the difference on the last tax line
|
|
# to avoid creating a difference with the source payment move
|
|
if (m.origin_payment_id or m.statement_line_id) and move_balance and counterpart_amount and last_tax_line_index:
|
|
delta_balance = move_balance + counterpart_amount
|
|
if delta_balance:
|
|
lines[last_tax_line_index][0] = float_repr(abs(last_tax_line_amount - delta_balance), m.company_id.currency_id.decimal_places).replace('.', ',')
|
|
|
|
writer.writerows(lines)
|
|
return output.getvalue()
|
|
|
|
def _l10n_de_datev_build_clearing_liquidity_line(self, move, liquidity_line, clearing_account, foreign_arap_lines):
|
|
"""
|
|
Build the liquidity leg through the clearing account, using the combined balance of the foreign AR/AP lines.
|
|
"""
|
|
company_currency = move.company_id.currency_id
|
|
liquidity_account_code = str(
|
|
self._l10n_de_datev_find_partner_account(
|
|
liquidity_line.account_id,
|
|
liquidity_line.partner_id,
|
|
)
|
|
)
|
|
liq_sh = "S" if company_currency.compare_amounts(liquidity_line.balance, 0.0) > 0 else "H"
|
|
clearing_balance = sum(foreign_arap_lines.mapped('balance'))
|
|
return self._l10n_de_datev_build_line(
|
|
line_amount_currency=abs(liquidity_line.amount_currency),
|
|
line_amount=clearing_balance,
|
|
currency=liquidity_line.currency_id,
|
|
company_currency=company_currency,
|
|
account_code=liquidity_account_code,
|
|
counterpart_code=clearing_account,
|
|
code_correction='',
|
|
move=move,
|
|
receipt1=move.name,
|
|
receipt2='',
|
|
reference_text=liquidity_line.name or liquidity_line.ref,
|
|
force_debit_credit=liq_sh,
|
|
)
|