# -*- coding: utf-8 -*- import csv import itertools import zipfile from io import BytesIO, TextIOWrapper, StringIO from freezegun import freeze_time from odoo import Command, fields from odoo.tests import tagged, Form from odoo.addons.account.tests.common import AccountTestInvoicingCommon @tagged('post_install_l10n', 'post_install', '-at_install') class TestDatevCSV(AccountTestInvoicingCommon): @classmethod @AccountTestInvoicingCommon.setup_chart_template('de_skr03') def setUpClass(cls): super().setUpClass() cls.account_3400 = cls.env['account.account'].search([ ('code', '=', 3400), ('company_ids', '=', cls.company_data['company'].id), ], limit=1) cls.account_4980 = cls.env['account.account'].search([ ('code', '=', 4980), ('company_ids', '=', cls.company_data['company'].id), ], limit=1) cls.account_1500 = cls.env['account.account'].search([ ('code', '=', 1500), ('company_ids', '=', cls.company_data['company'].id), ], limit=1) cls.tax_19 = cls.env['account.tax'].search([ ('name', '=', '19% I'), ('company_id', '=', cls.company_data['company'].id), ], limit=1) cls.tax_7 = cls.env['account.tax'].search([ ('name', '=', '7% I'), ('company_id', '=', cls.company_data['company'].id), ], limit=1) def _setup_three_currency_test(self, date='2020-12-01'): usd = self.setup_other_currency('USD', rates=[(date, 50)]) aed = self.setup_other_currency('AED', rates=[(date, 30)]) bank_journal = self.env['account.journal'].create({ 'name': 'Three Currency Bank', 'code': '3CUR', 'type': 'bank', 'currency_id': aed.id, 'company_id': self.company_data['company'].id, }) return { 'usd': usd, 'aed': aed, 'bank_journal': bank_journal, 'bank_account': bank_journal.default_account_id, 'ar_account': self.company_data['default_account_receivable'], 'ap_account': self.company_data['default_account_payable'], } def _create_three_currency_transaction(self, journal_id, amount, invoice_values, date='2020-12-01'): statement = self.env['account.bank.statement'].create({ 'line_ids': [ Command.create({ 'journal_id': journal_id, 'date': date, 'amount': amount, 'payment_ref': 'Three currency test', 'partner_id': self.partner_a.id, }), ], }) arap_lines = self.env['account.move.line'] for values in invoice_values: invoice = self._create_invoice_one_line( move_type=values['move_type'], currency_id=values['currency_id'], invoice_date=date, price_unit=values['price_unit'], tax_ids=[], post=True, ) arap_lines |= invoice.line_ids.filtered( lambda line: line.account_id.account_type in ( 'asset_receivable', 'liability_payable', ) ) statement.line_ids.set_line_bank_statement_line(arap_lines.ids) return statement.line_ids.move_id def test_datev_in_invoice(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) move = self.env['account.move'].create([{ 'move_type': 'in_invoice', 'partner_id': self.env['res.partner'].create({'name': 'Res Partner 12'}).id, 'invoice_date': fields.Date.to_date('2020-12-01'), 'date': fields.Date.to_date('2020-12-01'), 'ref': 'Brocken123', 'invoice_line_ids': [ (0, None, { 'name': 'Line Number 1', 'price_unit': 100, 'account_id': self.account_3400.id, 'tax_ids': [(6, 0, self.tax_19.ids)], }), (0, None, { 'name': 'Line Number 2', 'price_unit': 100, 'account_id': self.account_3400.id, 'tax_ids': [], }), (0, None, { 'name': 'Line Number 3', 'price_unit': 100, 'account_id': self.account_4980.id, 'tax_ids': [(6, 0, self.tax_19.ids)], }), ] }]) move.action_post() move.line_ids.flush_recordset() zf = zipfile.ZipFile(BytesIO(self.env[report.custom_handler_model_name].l10n_de_datev_export_to_zip(options)['file_content']), 'r') f = TextIOWrapper(zf.open('EXTF_accounting_entries.csv'), "utf-8") reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[6], x[7], x[8], x[9], x[10], x[13]] for x in reader][2:] self.addCleanup(zf.close) self.assertEqual(3, len(data), "csv should have 3 lines") self.assertIn(['119,00', 'S', 'EUR', '34000000', str(move.partner_id.id + 700000000), self.tax_19.l10n_de_datev_code, '112', move.name, move.invoice_line_ids[0].name], data) self.assertIn(['100,00', 'S', 'EUR', '34000000', str(move.partner_id.id + 700000000), '', '112', move.name, move.invoice_line_ids[1].name], data) self.assertIn(['119,00', 'S', 'EUR', '49800000', str(move.partner_id.id + 700000000), self.tax_19.l10n_de_datev_code, '112', move.name, move.invoice_line_ids[2].name], data) def test_datev_in_receipt(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) move = self.env['account.move'].create([{ 'move_type': 'in_receipt', 'invoice_date': fields.Date.to_date('2020-12-01'), 'date': fields.Date.to_date('2020-12-01'), 'ref': 'Brocken123', 'invoice_line_ids': [Command.create({ 'name': 'Line Number 1', 'price_unit': 100, 'account_id': self.account_3400.id, 'tax_ids': [Command.set(self.tax_19.ids)], }), Command.create({ 'name': 'Line Number 2', 'price_unit': 100, 'account_id': self.account_3400.id, 'tax_ids': [Command.set(self.tax_19.ids)], }), Command.create({ 'name': 'Line Number 3', 'price_unit': 100, 'account_id': self.account_4980.id, 'tax_ids': [Command.set(self.tax_19.ids)], }), ] }]) move.action_post() move.line_ids.flush_recordset() account = move.l10n_de_datev_main_account_id with zipfile.ZipFile(BytesIO(self.env[report.custom_handler_model_name].l10n_de_datev_export_to_zip(options)['file_content']), 'r') as zf: reader = csv.reader(TextIOWrapper(zf.open('EXTF_accounting_entries.csv'), "utf-8"), delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[6], x[7], x[8], x[9], x[10], x[13]] for x in reader][2:][::-1] self.assertCountEqual([ ['119,00', 'S', 'EUR', '34000000', str(account.code).ljust(8, '0'), self.tax_19.l10n_de_datev_code, '112', move.name, move.invoice_line_ids[0].name], ['119,00', 'S', 'EUR', '34000000', str(account.code).ljust(8, '0'), self.tax_19.l10n_de_datev_code, '112', move.name, move.invoice_line_ids[1].name], ['119,00', 'S', 'EUR', '49800000', str(account.code).ljust(8, '0'), self.tax_19.l10n_de_datev_code, '112', move.name, move.invoice_line_ids[2].name], ], data) def test_datev_out_invoice(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) move = self.env['account.move'].create([{ 'move_type': 'out_invoice', 'partner_id': self.env['res.partner'].create({'name': 'Res Partner 12'}).id, 'invoice_date': fields.Date.to_date('2020-12-01'), 'invoice_line_ids': [ (0, None, { 'price_unit': 100, 'account_id': self.account_4980.id, 'tax_ids': [(6, 0, self.tax_19.ids)], }), ] }]) move.action_post() move.line_ids.flush_recordset() zf = zipfile.ZipFile(BytesIO(self.env[report.custom_handler_model_name].l10n_de_datev_export_to_zip(options)['file_content']), 'r') f = TextIOWrapper(zf.open('EXTF_accounting_entries.csv'), "utf-8") reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[6], x[7], x[8], x[9], x[10], x[13]] for x in reader][2:] self.addCleanup(zf.close) self.assertEqual(1, len(data), "csv should have 1 line") self.assertIn(['119,00', 'H', 'EUR', '49800000', str(move.partner_id.id + 100000000), self.tax_19.l10n_de_datev_code, '112', move.name, move.name], data) def test_datev_miscellaneous(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) move = self.env['account.move'].create({ 'move_type': 'entry', 'date': '2020-12-01', 'journal_id': self.company_data['default_journal_misc'].id, 'line_ids': [ (0, 0, { 'debit': 100, 'credit': 0, 'partner_id': self.partner_a.id, 'account_id': self.account_4980.id, }), (0, 0, { 'debit': 0, 'credit': 100, 'partner_id': self.partner_a.id, 'account_id': self.account_3400.id, 'tax_ids': [], }), ] }) move.action_post() move.line_ids.flush_recordset() zf = zipfile.ZipFile(BytesIO(self.env[report.custom_handler_model_name].l10n_de_datev_export_to_zip(options)['file_content']), 'r') f = TextIOWrapper(zf.open('EXTF_accounting_entries.csv'), "utf-8") reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[6], x[7], x[9], x[10], x[13]] for x in reader][2:] self.addCleanup(zf.close) self.assertEqual(1, len(data), "csv should have 1 lines") self.assertIn(['100,00', 'H', 'EUR', '34000000', '49800000', '112', move.name, move.name], data) def test_datev_out_invoice_payment(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) move = self.env['account.move'].create([{ 'move_type': 'out_invoice', 'partner_id': self.env['res.partner'].create({'name': 'Res Partner 12'}).id, 'invoice_date': fields.Date.to_date('2020-12-01'), 'invoice_line_ids': [ (0, None, { 'price_unit': 100, 'account_id': self.account_4980.id, 'tax_ids': [(6, 0, self.tax_19.ids)], }), ] }]) move.action_post() pay = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=move.ids).create({ 'payment_date': fields.Date.to_date('2020-12-03'), })._create_payments() self.env.flush_all() debit_account_code = str(pay.payment_method_line_id.payment_account_id.code).ljust(8, '0') zf = zipfile.ZipFile(BytesIO(self.env[report.custom_handler_model_name].l10n_de_datev_export_to_zip(options)['file_content']), 'r') f = TextIOWrapper(zf.open('EXTF_accounting_entries.csv'), "utf-8") reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[6], x[7], x[8], x[9], x[10], x[13]] for x in reader][2:] self.addCleanup(zf.close) self.assertEqual(2, len(data), "csv should have 2 lines") self.assertIn(['119,00', 'H', 'EUR', '49800000', str(move.partner_id.id + 100000000), self.tax_19.l10n_de_datev_code, '112', move.name, move.name], data) self.assertIn(['119,00', 'H', 'EUR', str(move.partner_id.id + 100000000), debit_account_code, '', '312', pay.name, pay.move_id.line_ids[0].name], data) def test_datev_out_invoice_payment_same_account_counteraccount(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) move = self.env['account.move'].create([{ 'move_type': 'out_invoice', 'partner_id': self.env['res.partner'].create({'name': 'Res Partner 12'}).id, 'invoice_date': fields.Date.to_date('2020-12-01'), 'invoice_line_ids': [ (0, None, { 'price_unit': 100, 'account_id': self.account_4980.id, 'tax_ids': [(6, 0, self.tax_19.ids)], }), ] }]) move.action_post() # set counter account = account bank_journal = self.company_data['default_journal_bank'] bank_journal.inbound_payment_method_line_ids.payment_account_id = bank_journal.default_account_id pay = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=move.ids).create({ 'payment_date': fields.Date.to_date('2020-12-03'), })._create_payments() debit_account_code = str(bank_journal.default_account_id.code).ljust(8, '0') zf = zipfile.ZipFile(BytesIO(self.env[report.custom_handler_model_name].l10n_de_datev_export_to_zip(options)['file_content']), 'r') self.addCleanup(zf.close) f = TextIOWrapper(zf.open('EXTF_accounting_entries.csv'), "utf-8") reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[6], x[7], x[8], x[9], x[10], x[13]] for x in reader][2:] self.assertEqual(2, len(data), "csv should have 2 lines") self.assertIn(['119,00', 'H', 'EUR', '49800000', str(move.partner_id.id + 100000000), self.tax_19.l10n_de_datev_code, '112', move.name, move.name], data) self.assertIn(['119,00', 'H', 'EUR', str(move.partner_id.id + 100000000), debit_account_code, '', '312', pay.name, pay.move_id.line_ids[0].name], data) def test_datev_in_invoice_payment(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) move = self.env['account.move'].create([{ 'move_type': 'in_invoice', 'partner_id': self.env['res.partner'].create({'name': 'Res Partner 12'}).id, 'invoice_date': fields.Date.to_date('2020-12-01'), 'date': fields.Date.to_date('2020-12-01'), 'invoice_line_ids': [ (0, None, { 'price_unit': 100, 'account_id': self.account_4980.id, 'tax_ids': [(6, 0, self.tax_19.ids)], }), ] }]) move.action_post() pay = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=move.ids).create({ 'payment_date': fields.Date.to_date('2020-12-03'), })._create_payments() self.env.flush_all() credit_account_code = str(pay.payment_method_line_id.payment_account_id.code).ljust(8, '0') zf = zipfile.ZipFile(BytesIO(self.env[report.custom_handler_model_name].l10n_de_datev_export_to_zip(options)['file_content']), 'r') f = TextIOWrapper(zf.open('EXTF_accounting_entries.csv'), "utf-8") reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[6], x[7], x[8], x[9], x[10], x[13]] for x in reader][2:] self.addCleanup(zf.close) self.assertEqual(2, len(data), "csv should have 2 lines") self.assertIn(['119,00', 'S', 'EUR', '49800000', str(move.partner_id.id + 700000000), self.tax_19.l10n_de_datev_code, '112', move.name, move.name], data) self.assertIn(['119,00', 'S', 'EUR', str(move.partner_id.id + 700000000), credit_account_code, '', '312', pay.name, pay.move_id.line_ids[0].name], data) def test_datev_bank_statement(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) statement = self.env['account.bank.statement'].create({ 'balance_end_real': 100.0, 'line_ids': [ (0, 0, { 'journal_id': self.company_data['default_journal_bank'].id, 'payment_ref': 'line1', 'amount': 100.0, 'date': '2020-01-01', }), ], }) statement.line_ids.move_id.flush_recordset() suspense_account_code = str(self.env.company.account_journal_suspense_account_id.code).ljust(8, '0') bank_account_code = str(self.env.company.bank_journal_ids.default_account_id.code).ljust(8, '0') zf = zipfile.ZipFile(BytesIO(self.env[report.custom_handler_model_name].l10n_de_datev_export_to_zip(options)['file_content']), 'r') self.addCleanup(zf.close) f = TextIOWrapper(zf.open('EXTF_accounting_entries.csv'), "utf-8") reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[6], x[7], x[9], x[10], x[13]] for x in reader][2:] self.assertIn(['100,00', 'H', 'EUR', suspense_account_code, bank_account_code, '101', statement.line_ids[0].name, statement.line_ids[0].payment_ref], data) def test_datev_out_invoice_paid(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) move = self.env['account.move'].create({ 'move_type': 'out_invoice', 'partner_id': self.partner_a.id, 'invoice_date': fields.Date.to_date('2020-12-01'), 'invoice_line_ids': [(0, 0, { 'price_unit': 100.0, 'quantity': 1, 'account_id': self.company_data['default_account_revenue'].id, 'tax_ids': [], })], }) move.action_post() statement = self.env['account.bank.statement'].create({ 'balance_end_real': 100.0, 'line_ids': [ (0, 0, { 'journal_id': self.company_data['default_journal_bank'].id, 'payment_ref': 'line_1', 'amount': 100.0, 'partner_id': self.partner_a.id, 'date': '2020-01-01', }), ], }) receivable_line = move.line_ids.filtered(lambda line: line.account_id.account_type in ('asset_receivable', 'liability_payable')) statement.line_ids.set_line_bank_statement_line(receivable_line.id) bank_account_code = str(self.env.company.bank_journal_ids.default_account_id.code).ljust(8, '0') self.env.flush_all() file_dict = self.env[report.custom_handler_model_name].l10n_de_datev_export_to_zip(options) zf = zipfile.ZipFile(BytesIO(file_dict['file_content']), 'r') f = TextIOWrapper(zf.open('EXTF_accounting_entries.csv'), "utf-8") reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[6], x[7], x[9], x[10], x[13]] for x in reader][2:] self.addCleanup(zf.close) self.assertEqual(2, len(data), "csv should have 2 lines") self.assertIn(['100,00', 'H', 'EUR', str(self.company_data['default_account_revenue'].code).ljust(8, '0'), str(100000000 + move.partner_id.id), '112', move.name, move.name], data) self.assertIn(['100,00', 'H', 'EUR', str(100000000 + move.partner_id.id), bank_account_code, '101', statement.line_ids[0].name, statement.line_ids[0].line_ids[1].name], data) # 2nd line of the statement because it's the line without the bank account def test_datev_out_invoice_with_negative_amounts(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) move = self.env['account.move'].create([{ 'move_type': 'out_invoice', 'partner_id': self.env['res.partner'].create({'name': 'Res Partner 12'}).id, 'invoice_date': fields.Date.to_date('2020-12-01'), 'invoice_line_ids': [ (0, None, { 'name': 'Line Number 1', 'price_unit': 1000, 'account_id': self.account_4980.id, 'tax_ids': [(6, 0, self.tax_19.ids)], }), (0, None, { 'name': 'Line Number 2', 'price_unit': -1000, 'account_id': self.account_4980.id, 'tax_ids': [(6, 0, self.tax_19.ids)], }), (0, None, { 'name': 'Line Number 3', 'price_unit': 1000, 'quantity': -1, 'account_id': self.account_4980.id, 'tax_ids': [(6, 0, self.tax_19.ids)], }), (0, None, { 'price_unit': 2000, 'account_id': self.account_1500.id, 'tax_ids': [(6, 0, self.tax_7.ids)], }), (0, None, { 'price_unit': 3000, 'account_id': self.account_3400.id, 'tax_ids': [(6, 0, self.tax_19.ids)], }), ] }]) move.action_post() move.line_ids.flush_recordset() zf = zipfile.ZipFile(BytesIO(self.env[report.custom_handler_model_name].l10n_de_datev_export_to_zip(options)['file_content']), 'r') f = TextIOWrapper(zf.open('EXTF_accounting_entries.csv'), "utf-8") reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[6], x[7], x[8], x[9], x[10], x[13]] for x in reader][2:] self.addCleanup(zf.close) self.assertEqual(5, len(data), "csv should have 5 line") self.assertIn(['1190,00', 'H', 'EUR', '49800000', str(move.partner_id.id + 100000000), self.tax_19.l10n_de_datev_code, '112', move.name, move.invoice_line_ids[0].name], data) self.assertIn(['1190,00', 'S', 'EUR', '49800000', str(move.partner_id.id + 100000000), self.tax_19.l10n_de_datev_code, '112', move.name, move.invoice_line_ids[1].name], data) self.assertIn(['1190,00', 'S', 'EUR', '49800000', str(move.partner_id.id + 100000000), self.tax_19.l10n_de_datev_code, '112', move.name, move.invoice_line_ids[2].name], data) self.assertIn(['2140,00', 'H', 'EUR', '15000000', str(move.partner_id.id + 100000000), self.tax_7.l10n_de_datev_code, '112', move.name, move.name], data) self.assertIn(['3570,00', 'H', 'EUR', '34000000', str(move.partner_id.id + 100000000), self.tax_19.l10n_de_datev_code, '112', move.name, move.name], data) def test_datev_miscellaneous_several_line_same_account(self): """ Tests that if we have only a single account to the debit, we have a contra-account that is this account, and we can put all the credit lines against this account """ report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) account_1600 = self.env['account.account'].search( [('code', '=', 1600), ('company_ids', '=', self.company_data['company'].id)], limit=1) move = self.env['account.move'].create({ 'move_type': 'entry', 'date': '2020-12-01', 'journal_id': self.company_data['default_journal_misc'].id, 'line_ids': [ (0, 0, { 'debit': 100, 'credit': 0, 'partner_id': self.partner_a.id, 'account_id': self.account_4980.id, }), (0, 0, { 'debit': 100, 'credit': 0, 'partner_id': self.partner_a.id, 'account_id': self.account_4980.id, }), (0, 0, { 'debit': 0, 'credit': 100, 'partner_id': self.partner_a.id, 'account_id': account_1600.id, }), (0, 0, { 'debit': 0, 'credit': 100, 'partner_id': self.partner_a.id, 'account_id': self.account_1500.id, }), ] }) move.action_post() zf = zipfile.ZipFile(BytesIO(self.env[report.custom_handler_model_name].l10n_de_datev_export_to_zip(options)['file_content']), 'r') f = TextIOWrapper(zf.open('EXTF_accounting_entries.csv'), "utf-8") reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[6], x[7], x[9], x[10], x[13]] for x in reader][2:] self.addCleanup(zf.close) self.assertEqual(2, len(data), "csv should have 2 lines") self.assertIn(['100,00', 'H', 'EUR', str(700000000 + self.partner_a.id), '49800000', '112', move.name, move.name], data) self.assertIn(['100,00', 'H', 'EUR', '15000000', '49800000', '112', move.name, move.name], data) def test_datev_all_aml_present(self): report = self.env.ref('account_reports.general_ledger_report') report.load_more_limit = 3 options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) for _dummy in range(5): move = self.env['account.move'].create([{ 'move_type': 'out_invoice', 'partner_id': self.env['res.partner'].create({'name': 'Res Partner 12'}).id, 'invoice_date': fields.Date.to_date('2020-12-01'), 'invoice_line_ids': [ (0, None, { 'price_unit': 100, 'account_id': self.account_4980.id, 'tax_ids': [(6, 0, self.tax_19.ids)], }), ] }]) move.action_post() self.env.flush_all() zf = zipfile.ZipFile(BytesIO(self.env[report.custom_handler_model_name].l10n_de_datev_export_to_zip(options)['file_content']), 'r') self.addCleanup(zf.close) f = TextIOWrapper(zf.open('EXTF_accounting_entries.csv'), "utf-8") reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = list(reader) self.assertEqual(7, len(data), "csv should have 5 (+2 header) lines") def test_datev_vat_export(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) partners_list = [ {'name': 'partner1', 'vat': 'BE0897223670'}, {'name': 'partner2'}, {'name': 'partner3', 'vat': 'US12345671'}, {'name': 'partner4', 'vat': ''}, {'name': 'partner5', 'vat': 'NA'}, {'name': 'partner6', 'vat': '1234567890'}, {'name': 'partner7', 'vat': 'NL000099998B57'}, {'name': 'partner8', 'vat': '/'}, {'name': 'partner9', 'vat': '12AAAAA1234AAZA'}, {'name': 'partner10', 'vat': 'EL123456783'}, {'name': 'partner11', 'vat': 'EL123456783', 'country_id': self.ref('base.gr')}, {'name': 'partner12', 'vat': 'BE0897223670', 'country_id': self.ref('base.ch')}, ] partners = self.env['res.partner'].create(partners_list) move = self.env['account.move'].create([{ 'move_type': 'out_invoice', 'invoice_date': fields.Date.to_date('2020-12-01'), 'date': fields.Date.to_date('2020-12-01'), 'partner_id': partner.id, 'invoice_line_ids': [ Command.create({ 'name': 'Invoice Line', 'price_unit': 100, 'account_id': self.account_3400.id, 'tax_ids': [Command.set(self.tax_19.ids)], }), ] } for partner in partners]) move.action_post() with zipfile.ZipFile(BytesIO(self.env[report.custom_handler_model_name].l10n_de_datev_export_to_zip(options)['file_content']), 'r') as zf, \ TextIOWrapper(zf.open('EXTF_customer_accounts.csv'), 'utf-8') as f: reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) # first 2 rows are just headers and needn't be validated # first 2 columns are 'account' and 'name' and they are irrelevant to this test # column 19 'land' required for non DE data = [row[2:10] + [row[19]] for row in itertools.islice(reader, 2, None)] self.assertEqual( data, [ ["", "partner1", "", "", "1", "", "BE", "0897223670", "BE"], ["", "partner2", "", "", "1", "", "", "", ""], ["", "partner3", "", "", "1", "", "", "", "US"], ["", "partner4", "", "", "1", "", "", "", ""], ["", "partner5", "", "", "1", "", "", "", ""], ["", "partner6", "", "", "1", "", "", "", ""], ["", "partner7", "", "", "1", "", "NL", "000099998B57", "NL"], ["", "partner8", "", "", "1", "", "", "", ""], ["", "partner9", "", "", "1", "", "", "", ""], ["", "partner10", "", "", "1", "", "EL", "123456783", "GR"], ["", "partner11", "", "", "1", "", "EL", "123456783", "GR"], ["", "partner12", "", "", "1", "", "BE", "0897223670", "CH"], ], ) def test_datev_out_invoice_payment_epd_rounding(self): ''' Test epd rounding error correction is applied also when exporting in datev, in order to avoid a mismatch between stored and exported data ''' report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) self.early_pay_2_percents_10_days = self.env['account.payment.term'].create({ 'name': '2% discount if paid within 10 days', 'company_id': self.company_data['company'].id, 'early_discount': True, 'discount_percentage': 2, 'discount_days': 10, 'line_ids': [Command.create({ 'value': 'percent', 'value_amount': 100, 'nb_days': 30, })] }) move = self.env['account.move'].create([{ 'move_type': 'out_invoice', 'partner_id': self.env['res.partner'].create({'name': 'Res Partner 12'}).id, 'invoice_date': fields.Date.to_date('2020-12-01'), 'invoice_payment_term_id': self.early_pay_2_percents_10_days.id, 'invoice_line_ids': [ Command.create({ 'quantity': 1.0, 'price_unit': 761.76, 'tax_ids': [Command.set(self.tax_19.ids)], }), Command.create({ 'quantity': 1.0, 'price_unit': 100, 'tax_ids': [Command.set(self.tax_7.ids)], }), Command.create({ 'quantity': 1.0, 'price_unit': 200, 'tax_ids': [], }), ]}]) move.action_post() pay = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=move.ids).create({ 'payment_date': fields.Date.to_date('2020-12-03'), })._create_payments() debit_account_code = str(pay.payment_method_line_id.payment_account_id.code).ljust(8, '0') moves = move + pay.move_id f = StringIO(self.env[report.custom_handler_model_name]._l10n_de_datev_get_csv(options, moves)) reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[6], x[7], x[8], x[9], x[10], x[13]] for x in reader][2:] epd_loss_account_code = str(self.env.company.account_journal_early_pay_discount_loss_account_id.code).ljust(8, '0') self.assertIn(['18,14', 'S', 'EUR', epd_loss_account_code, debit_account_code, self.tax_19.l10n_de_datev_code, '312', pay.name, pay.move_id.line_ids[2].name], data) self.assertIn(['2,13', 'S', 'EUR', epd_loss_account_code, debit_account_code, self.tax_7.l10n_de_datev_code, '312', pay.name, pay.move_id.line_ids[2].name], data) def test_datev_out_bank_payment_epd_rounding(self): report = self.env.ref('account_reports.general_ledger_report') self.company_data['default_account_tax_purchase'].reconcile = True options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) self.early_pay_2_percents_10_days = self.env['account.payment.term'].create({ 'name': '2% discount if paid within 10 days', 'company_id': self.company_data['company'].id, 'early_discount': True, 'discount_percentage': 2, 'discount_days': 10, 'line_ids': [Command.create({ 'value': 'percent', 'value_amount': 100, 'nb_days': 30, })] }) move_1 = self.env['account.move'].create([{ 'move_type': 'in_invoice', 'partner_id': self.partner_a.id, 'invoice_date': fields.Date.to_date('2020-12-01'), 'invoice_payment_term_id': self.early_pay_2_percents_10_days.id, 'invoice_line_ids': [ Command.create({ 'quantity': 1.0, 'price_unit': 161.10, 'tax_ids': [Command.set(self.tax_19.ids)], }), ]}]) move_2 = self.env['account.move'].create([{ 'move_type': 'in_invoice', 'partner_id': self.partner_a.id, 'invoice_date': fields.Date.to_date('2020-12-01'), 'invoice_payment_term_id': self.early_pay_2_percents_10_days.id, 'invoice_line_ids': [ Command.create({ 'quantity': 1.0, 'price_unit': 77.50, 'tax_ids': [Command.set(self.tax_19.ids)], }), ]}]) moves = (move_1 | move_2) moves.action_post() # Create the payment statement statement = self.env['account.bank.statement'].create({ 'name': 'test_statement', 'line_ids': [ (0, 0, { 'journal_id': self.company_data['default_journal_bank'].id, 'date': '2020-12-02', 'payment_ref': 'test', 'amount': -278.27, 'partner_id': self.partner_a.id, })] }) statement.line_ids.set_line_bank_statement_line( ( move_1.line_ids.filtered(lambda x: x.account_id.account_type == 'liability_payable') + move_2.line_ids.filtered(lambda x: x.account_id.account_type == 'liability_payable') ).ids ) payment_move = statement.line_ids.move_id f = StringIO(self.env[report.custom_handler_model_name]._l10n_de_datev_get_csv(options, payment_move)) reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[6], x[7], x[8], x[9], x[10], x[13]] for x in reader][2:] epd_gain_account_code = str(self.env.company.account_journal_early_pay_discount_gain_account_id.code).ljust(8, '0') self.assertIn(['3,83', 'H', 'EUR', epd_gain_account_code, '12010000', self.tax_19.l10n_de_datev_code, '212', payment_move.name, "Early Payment Discount"], data) self.assertIn(['1,84', 'H', 'EUR', epd_gain_account_code, '12010000', self.tax_19.l10n_de_datev_code, '212', payment_move.name, "Early Payment Discount"], data) @freeze_time('2021-01-02 18:00') def test_datev_out_invoice_with_attch(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) move = self.env['account.move'].create([{ 'move_type': 'out_invoice', 'partner_id': self.env['res.partner'].create({'name': 'Res Partner 12'}).id, 'invoice_date': fields.Date.to_date('2020-12-01'), 'invoice_line_ids': [ Command.create({ 'price_unit': 100, 'account_id': self.account_4980.id, 'tax_ids': [Command.set(self.tax_19.ids)], }), ] }]) move.action_post() self.env['account.move.send.wizard'] \ .with_context(active_model='account.move', active_ids=move.ids) \ .create({}).action_send_and_print() move.line_ids.flush_recordset() move_guid = move._l10n_de_datev_get_guid() with zipfile.ZipFile(BytesIO(self.env[report.custom_handler_model_name].l10_de_datev_export_to_zip_and_attach(options)['file_content']), 'r') as zf: xml = zf.open('document.xml').read() f = StringIO(self.env[report.custom_handler_model_name]._l10n_de_datev_get_csv(options, move)) reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) csv_data = list(reader)[2] self.assertEqual(f'BEDI "{move_guid}"', csv_data[19]) # xml document is generated during tests because of an override in _hook_invoice_document_after_pdf_report_render expected_tree = f"""
2021-01-02T18:00:00
""" self.assertXmlTreeEqual(self.get_xml_tree_from_string(xml), self.get_xml_tree_from_string(expected_tree)) @freeze_time('2021-01-02 18:00') def test_datev_out_invoice_with_orphaned_attachment(self): """ `message_main_attachment_id` may point to an attachment not linked to the move (`res_id=0`, `res_model=False`, e.g. an image uploaded by another user). Exporting as a plain accounting user (not manager, not system) must not raise an `AccessError`. """ accountant = self.env['res.users'].create({ 'name': 'Accountant', 'login': 'accountant', 'group_ids': [Command.set([self.env.ref('account.group_account_user').id])], }) report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) move = self.env['account.move'].create({ 'move_type': 'out_invoice', 'partner_id': self.partner_a.id, 'invoice_date': fields.Date.to_date('2020-12-01'), 'invoice_line_ids': [ Command.create({ 'price_unit': 100, 'account_id': self.account_4980.id, 'tax_ids': [Command.set(self.tax_19.ids)], }), ] }) move.action_post() move.message_main_attachment_id = self.env['ir.attachment'].create({ 'name': 'orphan.png', 'mimetype': 'image/png', 'raw': b'Fake Image', }) # Access is only re-checked when fields are actually fetched from the DB; the ORM # cache is shared for the whole transaction, so without this the accountant would # transparently reuse the values already cached by the admin's create() above move.message_main_attachment_id.invalidate_recordset() self.env[report.custom_handler_model_name].with_user(accountant).l10_de_datev_export_to_zip_and_attach(options) def test_datev_out_invoice_with_included_tax(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) tax_19_incl = self.tax_19.copy({'name': 'Tax 19% incl.', 'price_include_override': 'tax_included'}) move = self.env['account.move'].create([{ 'move_type': 'out_invoice', 'partner_id': self.env['res.partner'].create({'name': 'Partner XYZ'}).id, 'invoice_date': fields.Date.to_date('2020-12-01'), 'invoice_line_ids': [ Command.create({ 'name': 'Line', 'price_unit': 19.95, 'account_id': self.account_3400.id, 'tax_ids': [Command.set(tax_19_incl.ids)], }), ] }]) move.action_post() f = StringIO(self.env[report.custom_handler_model_name]._l10n_de_datev_get_csv(options, move)) reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[6], x[7], x[8], x[9], x[10], x[13]] for x in reader][2:] self.assertIn(['19,95', 'H', 'EUR', '34000000', str(move.partner_id.id + 100000000), self.tax_19.l10n_de_datev_code, '112', move.name, move.invoice_line_ids[0].name], data) def test_datev_out_invoice_in_foreign_currency(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) tax_19_incl = self.tax_19.copy({'name': 'Tax 19% incl.', 'price_include_override': 'tax_included'}) foreign_currency = self.env['res.currency'].create({ 'name': "XYZ", 'symbol': 'X', 'rate_ids': [ Command.create({'name': '2020-01-01', 'rate': 0.5}), ], }) move = self.env['account.move'].create([{ 'move_type': 'out_invoice', 'partner_id': self.env['res.partner'].create({'name': 'Partner XYZ'}).id, 'currency_id': foreign_currency.id, 'invoice_date': fields.Date.to_date('2020-12-01'), 'invoice_line_ids': [ Command.create({ 'name': 'Line', 'price_unit': 100.00, 'account_id': self.account_3400.id, 'tax_ids': [Command.set(self.tax_19.ids)], }), Command.create({ 'name': 'Line with included tax', 'price_unit': 19.95, 'account_id': self.account_3400.id, 'tax_ids': [Command.set(tax_19_incl.ids)], }), ] }]) move.action_post() f = StringIO(self.env[report.custom_handler_model_name]._l10n_de_datev_get_csv(options, move)) reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[13]] for x in reader][2:] self.assertIn(['119,00', 'H', 'XYZ', '0,5', '238,00', 'EUR', '34000000', str(move.partner_id.id + 100000000), self.tax_19.l10n_de_datev_code, '112', move.name, move.invoice_line_ids[0].name], data) self.assertIn(['19,95', 'H', 'XYZ', '0,5', '39,90', 'EUR', '34000000', str(move.partner_id.id + 100000000), self.tax_19.l10n_de_datev_code, '112', move.name, move.invoice_line_ids[1].name], data) def test_datev_entry_in_foreign_currency(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) foreign_currency = self.env['res.currency'].create({ 'name': "XYZ", 'symbol': 'X', 'rate_ids': [ Command.create({'name': '2020-01-01', 'rate': 0.5}), ], }) move = self.env['account.move'].create([{ 'move_type': 'entry', 'partner_id': self.env['res.partner'].create({'name': 'Partner XYZ'}).id, 'date': fields.Date.to_date('2020-12-01'), 'journal_id': self.company_data['default_journal_misc'].id, 'line_ids': [ Command.create({ 'name': 'Line', 'balance': 100.0, 'amount_currency': 50, 'account_id': self.account_4980.id, 'currency_id': foreign_currency.id, }), Command.create({ 'name': 'Counterpart Line', 'balance': -100.0, 'amount_currency': -50.0, 'account_id': self.account_3400.id, 'tax_ids': [], 'currency_id': foreign_currency.id, }), ] }]) move.action_post() f = StringIO(self.env[report.custom_handler_model_name]._l10n_de_datev_get_csv(options, move)) reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[13]] for x in reader][2:] self.assertIn(['50,00', 'H', 'XYZ', '0,5', '100,00', 'EUR', '34000000', '49800000', '', '112', move.name, move.line_ids[1].name], data) def test_datev_miscellaneous_with_updated_credit_and_debit_in_aml(self): """ Test that the datev exported values are correct when we update the credit and debit in aml of an miscellaneous move. """ report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) options['date'].update({ 'date_from': '2020-01-01', 'date_to': '2020-12-31', }) move = self.env['account.move'].create({ 'move_type': 'entry', 'date': '2020-01-01', 'journal_id': self.company_data['default_journal_misc'].id, 'line_ids': [ Command.create({ 'debit': 1000, 'credit': 0, 'account_id': self.account_4980.id, }), Command.create({ 'debit': 0, 'credit': 1000, 'account_id': self.account_1500.id, }), ], }) move.write({ 'line_ids': [ Command.update(move.line_ids[0].id, {'debit': 3004}), Command.update(move.line_ids[1].id, {'credit': 3004}), ] }) move.action_post() f = StringIO(self.env[report.custom_handler_model_name]._l10n_de_datev_get_csv(options, move)) reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[13]] for x in reader][2:] self.assertIn(['3004,00', move.name], data) def test_datev_select_receivable_payable_l10n_de_datev_main_account_id(self): """ Test that when an entry contains both a revenue and a refund (like the ones generated by pos) The field l10n_de_datev_main_account_id is still populated """ line_vals = [ [self.company_data['default_account_revenue'].id, -100.0, self.tax_19.ids, 'Sale 19%'], [self.company_data['default_account_expense'].id, 50.0, self.tax_19.ids, 'Refund 19%'], [self.company_data['default_account_revenue'].id, -100.0, self.tax_7.ids, 'Sale 7%'], [self.company_data['default_account_expense'].id, 50.0, self.tax_7.ids, 'Refund 7%'], [self.company_data['default_account_receivable'].id, 113.0, False, ''], ] move = self.env['account.move'].create({ 'line_ids': [ Command.create({ 'account_id': account_id, 'debit': amount if amount > 0 else 0.0, 'credit': -amount if amount < 0 else 0.0, 'tax_ids': tax_ids, 'name': name, }) for account_id, amount, tax_ids, name in line_vals ] }) self.assertEqual(move.l10n_de_datev_main_account_id, self.company_data['default_account_receivable']) @freeze_time('2021-01-02 18:00') def test_datev_company_name(self): """ Test that DATEV report is exported succesfully even with complex company name """ self.company_data['company'].name = 'Test Company GmbH & Co. AB' report = self.env.ref('account_reports.general_ledger_report') options = report.get_options({}) vals = self.env[report.custom_handler_model_name].l10n_de_datev_export_to_zip(options) self.assertEqual(vals['file_name'], 'general_ledger_jan_2021_test_company_gmbh_&_co._ab_data.ZIP') def test_datev_in_invoice_total_tax_edited(self): """ Test that the correct amount is computed when the total of a tax group is edited manually """ report = self.env.ref('account_reports.general_ledger_report') options = report.get_options(previous_options={'date': { 'date_from': '2020-12-01', 'date_to': '2020-12-31' }}) move = self.env['account.move'].create([{ 'move_type': 'in_invoice', 'partner_id': self.env['res.partner'].create({'name': 'Partner XYZ'}).id, 'invoice_date': '2020-12-01', 'date': '2020-12-01', 'ref': 'Brocken123', 'invoice_line_ids': [ Command.create({ 'name': 'Line 19% #1', 'price_unit': 100, 'account_id': self.account_3400.id, 'tax_ids': [Command.set(self.tax_19.ids)], }), Command.create({ 'name': 'Line 19% #2', 'price_unit': 200, 'account_id': self.account_3400.id, 'tax_ids': [Command.set(self.tax_19.ids)], }), Command.create({ 'name': 'Line 7%', 'price_unit': 100, 'account_id': self.account_3400.id, 'tax_ids': [Command.set(self.tax_7.ids)], }), ] }]) with Form(move) as move_form: tax_totals = move.tax_totals tax_groups = tax_totals['subtotals'][0]['tax_groups'] index = next( (i for i, data in enumerate(tax_groups) if data['id'] == self.tax_19.tax_group_id.id) ) self.assertEqual(tax_totals['subtotals'][0]['tax_groups'][index]['tax_amount_currency'], 57.0) # remove 0.05 from Tax 19% group tax_totals['subtotals'][0]['tax_groups'][index]['tax_amount_currency'] = 56.95 move_form.tax_totals = tax_totals move.action_post() move.line_ids.flush_recordset() f = StringIO(self.env[report.custom_handler_model_name]._l10n_de_datev_get_csv(options, move)) reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[8]] for x in reader][2:] self.assertEqual([ # 0.03 should be dispatched to the first line using Tax 19% ['118,97', self.tax_19.l10n_de_datev_code], # 0.02 should be dispatched to the second line using Tax 19% ['237,98', self.tax_19.l10n_de_datev_code], ['107,00', self.tax_7.l10n_de_datev_code], ], data) def test_datev_out_invoice_in_foreign_currency_rate_set_manually(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options(previous_options={'date': { 'date_from': '2020-01-01', 'date_to': '2020-12-31', }}) foreign_currency = self.env['res.currency'].create({ 'name': "XYZ", 'symbol': 'X', }) move = self.env['account.move'].create([{ 'move_type': 'out_invoice', 'partner_id': self.partner_a.id, 'currency_id': foreign_currency.id, 'invoice_date': '2020-12-01', 'invoice_currency_rate': 0.5, 'invoice_line_ids': [ Command.create({ 'name': 'Line', 'price_unit': 100.00, 'account_id': self.account_3400.id, 'tax_ids': [Command.set(self.tax_19.ids)], }), ] }]) move.action_post() f = StringIO(self.env[report.custom_handler_model_name]._l10n_de_datev_get_csv(options, move)) reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[13]] for x in reader][2:] self.assertIn(['119,00', 'H', 'XYZ', '0,5', '238,00', 'EUR', '34000000', str(move.partner_id.id + 100000000), self.tax_19.l10n_de_datev_code, '112', move.name, move.invoice_line_ids[0].name], data) def test_datev_expense_payment_with_tax(self): """ Test that the tax code is exported from a payment move """ report = self.env.ref('account_reports.general_ledger_report') options = report.get_options(previous_options={'date': { 'date_from': '2020-12-01', 'date_to': '2020-12-31' }}) bank_journal = self.company_data['default_journal_bank'] account_1203 = self.env['account.account'].search([ ('code', '=', 1203), ('company_ids', '=', self.company_data['company'].id) ], limit=1) tax_19_incl = self.tax_19.copy({'name': 'Tax 19% incl.', 'price_include': True}) tax_repartition_line = tax_19_incl.refund_repartition_line_ids.filtered(lambda line: line.repartition_type == 'tax') skip_context = { 'skip_invoice_sync': True, 'skip_invoice_line_sync': True, 'skip_account_move_synchronization': True, } payment = self.env['account.payment'].with_context(**skip_context).create({ 'amount': 150.00, 'payment_type': 'outbound', 'partner_type': 'supplier', 'date': '2020-12-01', 'journal_id': bank_journal.id, 'line_ids': [ Command.create({ 'name': 'Line 19% #1', 'debit': 126.05, 'credit': 0.0, 'account_id': self.account_3400.id, 'tax_ids': [Command.set(tax_19_incl.ids)], }), Command.create({ 'name': 'Line 19% tax', 'debit': 23.95, 'credit': 0.0, 'account_id': self.account_1500.id, 'tax_repartition_line_id': tax_repartition_line.id, }), Command.create({ 'name': 'expense line', 'credit': 150.0, 'debit': 0.0, 'account_id': account_1203.id, }), ], }) payment.action_post() f = StringIO(self.env[report.custom_handler_model_name]._l10n_de_datev_get_csv(options, payment.move_id)) reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[6], x[7], x[8]] for x in reader][2:] self.assertEqual([ ['150,00', 'H', 'EUR', '12030000', '34000000', tax_19_incl.l10n_de_datev_code], ], data) def test_datev_out_invoice_in_foreign_currency_with_exchange_loss(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options(previous_options={'date': { 'date_from': '2026-01-01', 'date_to': '2026-12-31' }}) # Create exchange rate entries for the foreign currency foreign_currency = self.env['res.currency'].create({ 'name': "XYZ", 'symbol': 'X', 'rate_ids': [ Command.create({'name': '2026-01-01', 'rate': 1.0}), Command.create({'name': '2026-01-15', 'rate': 1.1}), ], }) # Create the invoice on any date before the exchange rate changes (i.e before 15th in this case) move = self.env['account.move'].create([{ 'move_type': 'out_invoice', 'partner_id': self.partner_a.id, 'currency_id': foreign_currency.id, 'invoice_date': '2026-01-01', 'invoice_line_ids': [ Command.create({ 'name': 'Line', 'price_unit': 100.00, 'tax_ids': [], }), ] }]) move.action_post() # Make the payment on any date with the new exchange rate (15th or after) # The payment should create two entries in the report, one for the invoice payment # and another for the currency exchange difference. pay = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=move.ids).create({ 'payment_date': fields.Date.to_date('2026-01-16'), })._create_payments() # Get the exchange move exchange_move = move.line_ids.matched_credit_ids.exchange_move_id # Get the payment move payment_move = pay.move_id moves = payment_move | exchange_move f = StringIO(self.env[report.custom_handler_model_name]._l10n_de_datev_get_csv(options, moves)) reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[13]] for x in reader][2:] self.assertIn(['100,00', 'H', 'XYZ', '1,099989', '90,91', 'EUR', str(move.partner_id.id + 100000000), '12030000', '', '1601', payment_move.name, payment_move.line_ids[0].name], data) self.assertIn(['0,00', 'H', 'XYZ', '1,0', '9,09', 'EUR', str(move.partner_id.id + 100000000), '21500000', '', '3101', exchange_move.name, exchange_move.line_ids[0].name], data) def test_datev_out_invoice_in_foreign_currency_with_exchange_gain(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options(previous_options={'date': { 'date_from': '2026-01-01', 'date_to': '2026-12-31' }}) # Create exchange rate entries for the foreign currency foreign_currency = self.env['res.currency'].create({ 'name': "XYZ", 'symbol': 'X', 'rate_ids': [ Command.create({'name': '2026-01-01', 'rate': 1.0}), Command.create({'name': '2026-01-15', 'rate': 0.5}), ], }) # Create the invoice on any date before the exchange rate changes (i.e before 15th in this case) move = self.env['account.move'].create([{ 'move_type': 'out_invoice', 'partner_id': self.partner_a.id, 'currency_id': foreign_currency.id, 'invoice_date': '2026-01-01', 'invoice_line_ids': [ Command.create({ 'name': 'Line', 'price_unit': 100.00, 'tax_ids': [], }), ] }]) move.action_post() # Make the payment on any date with the new exchange rate (15th or after) # The payment should create two entries in the report, one for the invoice payment # and another for the currency exchange difference. pay = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=move.ids).create({ 'payment_date': fields.Date.to_date('2026-01-16'), })._create_payments() # Get the exchange move exchange_move = move.line_ids.matched_credit_ids.exchange_move_id # Get the payment move payment_move = pay.move_id moves = payment_move | exchange_move f = StringIO(self.env[report.custom_handler_model_name]._l10n_de_datev_get_csv(options, moves)) reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10], x[13]] for x in reader][2:] self.assertIn(['100,00', 'H', 'XYZ', '0,5', '200,00', 'EUR', str(move.partner_id.id + 100000000), '12030000', '', '1601', payment_move.name, payment_move.line_ids[0].name], data) self.assertIn(['0,00', 'S', 'XYZ', '1,0', '100,00', 'EUR', str(move.partner_id.id + 100000000), '26600000', '', '3101', exchange_move.name, exchange_move.line_ids[0].name], data) def test_datev_in_invoice_tax_with_multiple_repartition_lines(self): report = self.env.ref('account_reports.general_ledger_report') options = report.get_options(previous_options={'date': { 'date_from': '2026-01-01', 'date_to': '2026-01-31', }}) foreign_currency = self.env['res.currency'].create({ 'name': "foreign currency", 'symbol': 'abc', 'rounding': 0.01, 'rate_ids': [ Command.create({'name': '2025-01-01', 'rate': 3}), ], }) tax_account = self.company_data['default_account_tax_purchase'] tax = self.env['account.tax'].create({ 'name': 'multiple repartition line tax', 'amount_type': 'percent', 'amount': 10.0, 'type_tax_use': 'purchase', 'repartition_line_ids': [ Command.create({'document_type': 'invoice', 'repartition_type': 'base'}), Command.create({'document_type': 'invoice', 'repartition_type': 'tax', 'account_id': tax_account.id, 'factor_percent': 50.0}), Command.create({'document_type': 'invoice', 'repartition_type': 'tax', 'account_id': tax_account.id, 'factor_percent': 50.0}), Command.create({'document_type': 'refund', 'repartition_type': 'base'}), Command.create({'document_type': 'refund', 'repartition_type': 'tax', 'account_id': tax_account.id, 'factor_percent': 50.0}), Command.create({'document_type': 'refund', 'repartition_type': 'tax', 'account_id': tax_account.id, 'factor_percent': 50.0}), ] }) move = self.env['account.move'].create([{ 'move_type': 'in_invoice', 'partner_id': self.partner_a.id, 'date': '2026-01-01', 'invoice_date': '2026-01-01', 'currency_id': foreign_currency.id, 'invoice_line_ids': [Command.create({ 'price_unit': 100.00, 'tax_ids': [tax.id], })] }]) move.action_post() f = StringIO(self.env[report.custom_handler_model_name]._l10n_de_datev_get_csv(options, move)) reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[6], x[7], x[8]] for x in reader][2:] self.assertEqual([ ['110,00', 'S', 'for', '34000000', str(move.partner_id.id + 700000000), ''], ], data) def test_datev_exchange_rate_zero_denominator(self): """Test that an exchange rate defaults to 1,0 when the base currency amount is zero""" report = self.env.ref('account_reports.general_ledger_report') options = report.get_options(previous_options={ 'date': {'date_from': '2026-01-01', 'date_to': '2026-12-31'} }) foreign_currency = self.env['res.currency'].create({ 'name': "XYZ", 'symbol': 'X', }) invoice = self.env['account.move'].create({ 'move_type': 'out_invoice', 'partner_id': self.partner_a.id, 'invoice_date': '2026-01-15', 'currency_id': foreign_currency.id, 'invoice_line_ids': [ Command.create({ 'name': 'Zero Base Test Line', 'quantity': 1, 'price_unit': 100.00, 'account_id': self.company_data['default_account_revenue'].id, 'tax_ids': [], }) ] }) invoice.action_post() f = StringIO(self.env[report.custom_handler_model_name]._l10n_de_datev_get_csv(options, invoice)) reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [[x[0], x[1], x[2], x[3], x[4], x[5]] for x in reader][2:] self.assertIn(['100,00', 'H', 'XYZ', '1,0', '100,00', 'EUR'], data) def test_datev_three_currency_basic(self): """ Tests 3-currency export with one foreign AR line. Bank [AED] → AR [USD] → Company Currency [EUR] """ report = self.env.ref('account_reports.general_ledger_report') options = report.get_options(previous_options={'date': { 'date_from': '2020-01-01', 'date_to': '2020-12-31', }}) setup = self._setup_three_currency_test() move = self._create_three_currency_transaction( setup['bank_journal'].id, amount=3000.0, invoice_values=[{ 'move_type': 'out_invoice', 'currency_id': setup['usd'].id, 'price_unit': 5000.0, }], ) f = StringIO(self.env[report.custom_handler_model_name]._l10n_de_datev_get_csv(options, move)) reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [ [x[0], x[1], x[2], x[4], x[5], x[7]] for x in list(reader)[2:] ] self.assertEqual(len(data), 2) self.assertIn(['3000,00', 'S', 'AED', '100,00', 'EUR', '13600000'], data) self.assertIn(['5000,00', 'H', 'USD', '100,00', 'EUR', '13600000'], data) def test_datev_three_currency_multiple_lines(self): """ Tests multiple lines with mixed 2/3-currency handling. One bank transaction: -4,500 AED = -150 EUR Reconciled against: 2,500 USD = 50 EUR 2,500 USD = 50 EUR 1,500 AED = 50 EUR """ report = self.env.ref('account_reports.general_ledger_report') options = report.get_options(previous_options={'date': { 'date_from': '2020-01-01', 'date_to': '2020-12-31', }}) setup = self._setup_three_currency_test() move = self._create_three_currency_transaction( setup['bank_journal'].id, amount=-4500.0, invoice_values=[ { 'move_type': 'in_invoice', 'currency_id': setup['usd'].id, 'price_unit': 2500.0, }, { 'move_type': 'in_invoice', 'currency_id': setup['usd'].id, 'price_unit': 2500.0, }, { 'move_type': 'in_invoice', 'currency_id': setup['aed'].id, 'price_unit': 1500.0, }, ], ) f = StringIO(self.env[report.custom_handler_model_name]._l10n_de_datev_get_csv(options, move)) reader = csv.reader(f, delimiter=';', quotechar='"', quoting=2) data = [ [x[0], x[1], x[2], x[4], x[5], x[7]] for x in list(reader)[2:] ] self.assertEqual(len(data), 4) self.assertEqual(data.count(['4500,00', 'H', 'AED', '100,00', 'EUR', '13600000']), 1) self.assertEqual(data.count(['2500,00', 'S', 'USD', '50,00', 'EUR', '13600000']), 2) self.assertEqual(data.count(['1500,00', 'S', 'AED', '50,00', 'EUR', '12060000']), 1)