From d050b8e8a080ccb4a95d346b71326dc69f8e99db Mon Sep 17 00:00:00 2001 From: jbw-odoo Date: Mon, 16 Jan 2023 15:10:04 +0000 Subject: [PATCH] [FIX] l10n_fr_fec: allow exclusion of journals in FEC export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In French accounting, they need to provide a FEC file to the auditors/tax authority to control their accounting. So far it's fine in Odoo. While a company is managing IFRS accounting, the usual way to do it in Odoo is to use a specific journal "IFRS adjustments" for example. This way of working allows you to have a balance in local GAAP (excluding IFRS entries) and in IFRS GAAP (including the IFRS entries). So far it's fine in Odoo. The issue that we met is once a customer need to send his FEC file to the auditors/tax authority, he needs to send only local entries (then excluding IFRS entries). Therefore, it would be great to have an option to exclude some journals from the FEC export. closes odoo/odoo#110655 Task: 3133678 Signed-off-by: Florian Gilbert (flg) --- addons/l10n_fr_fec/tests/test_wizard.py | 49 ++++++- addons/l10n_fr_fec/wizard/account_fr_fec.py | 134 ++++++++---------- .../wizard/account_fr_fec_view.xml | 1 + 3 files changed, 99 insertions(+), 85 deletions(-) diff --git a/addons/l10n_fr_fec/tests/test_wizard.py b/addons/l10n_fr_fec/tests/test_wizard.py index f87f37ed89b..db1e00de86e 100644 --- a/addons/l10n_fr_fec/tests/test_wizard.py +++ b/addons/l10n_fr_fec/tests/test_wizard.py @@ -23,7 +23,7 @@ class TestAccountFrFec(AccountTestInvoicingCommon): lines_data = [(1437.12, 'Hello\tDarkness'), (1676.64, 'my\rold\nfriend'), (3353.28, '\t\t\r')] with freeze_time('2021-05-02'): - today = fields.Date.today().strftime('%Y-%m-%d') + cls.today = fields.Date.today().strftime('%Y-%m-%d') cls.wizard = cls.env['account.fr.fec'].create({ 'date_from': fields.Date.today() - timedelta(days=1), @@ -53,8 +53,8 @@ class TestAccountFrFec(AccountTestInvoicingCommon): cls.invoice_a = cls.env['account.move'].create({ 'move_type': 'out_invoice', 'partner_id': cls.partner_a.id, - 'date': today, - 'invoice_date': today, + 'date': cls.today, + 'invoice_date': cls.today, 'currency_id': company.currency_id.id, 'invoice_line_ids': [(0, None, { 'name': name, @@ -66,15 +66,50 @@ class TestAccountFrFec(AccountTestInvoicingCommon): }) cls.invoice_a.action_post() - def test_generate_fec_sanitize_pieceref(self): - self.wizard.generate_fec() - expected_content = ( + cls.expected_report = ( "JournalCode|JournalLib|EcritureNum|EcritureDate|CompteNum|CompteLib|CompAuxNum|CompAuxLib|PieceRef|PieceDate|EcritureLib|Debit|Credit|EcritureLet|DateLet|ValidDate|Montantdevise|Idevise\r\n" "INV|Customer Invoices|INV/2021/00001|20210502|701100|Finished products (or group) A|||-|20210502|Hello Darkness|0,00| 000000000001437,12|||20210502|-000000000001437,12|EUR\r\n" "INV|Customer Invoices|INV/2021/00001|20210502|701100|Finished products (or group) A|||-|20210502|my old friend|0,00| 000000000001676,64|||20210502|-000000000001676,64|EUR\r\n" "INV|Customer Invoices|INV/2021/00001|20210502|701100|Finished products (or group) A|||-|20210502|/|0,00| 000000000003353,28|||20210502|-000000000003353,28|EUR\r\n" "INV|Customer Invoices|INV/2021/00001|20210502|445710|VAT collected|||-|20210502|TVA 20,0%|0,00| 000000000001293,41|||20210502|-000000000001293,41|EUR\r\n" - f"INV|Customer Invoices|INV/2021/00001|20210502|411100|Customers - Sales of goods or services|{self.partner_a.id}|partner_a|-|20210502|INV/2021/00001| 000000000007760,45|0,00|||20210502| 000000000007760,45|EUR" + f"INV|Customer Invoices|INV/2021/00001|20210502|411100|Customers - Sales of goods or services|{cls.partner_a.id}|partner_a|-|20210502|INV/2021/00001| 000000000007760,45|0,00|||20210502| 000000000007760,45|EUR" + ) + + def test_generate_fec_sanitize_pieceref(self): + self.wizard.generate_fec() + content = base64.b64decode(self.wizard.fec_data).decode() + self.assertEqual(self.expected_report, content) + + def test_generate_fec_exclude_journals(self): + journal = self.company_data['default_journal_misc'] + self.env['account.move'].create({ + 'journal_id': journal.id, + 'date': self.today, + 'line_ids': [ + Command.create({ + 'account_id': self.company_data['default_account_payable'].id, + 'debit': 500, + 'company_id': self.company_data['company'].id + }), + Command.create({ + 'account_id': self.company_data['default_account_receivable'].id, + 'credit': 500, + 'company_id': self.company_data['company'].id + }) + ] + }).action_post() + self.env.flush_all() + + self.wizard.generate_fec() + expected_content = self.expected_report + ( + "\r\n" + "MISC|Miscellaneous Operations|MISC/2021/05/0001|20210502|400000|Suppliers and related accounts|||-|20210502|/| 000000000000500,00|0,00|||20210502| 000000000000500,00|EUR\r\n" + "MISC|Miscellaneous Operations|MISC/2021/05/0001|20210502|411100|Customers - Sales of goods or services|||-|20210502|/|0,00| 000000000000500,00|||20210502|-000000000000500,00|EUR" ) content = base64.b64decode(self.wizard.fec_data).decode() self.assertEqual(expected_content, content) + + self.wizard.excluded_journal_ids = journal + self.wizard.generate_fec() + content = base64.b64decode(self.wizard.fec_data).decode() + self.assertEqual(self.expected_report, content) diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py index f7d0481f73b..af2becc3d68 100644 --- a/addons/l10n_fr_fec/wizard/account_fr_fec.py +++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py @@ -25,36 +25,48 @@ class AccountFrFec(models.TransientModel): export_type = fields.Selection([ ('official', 'Official FEC report (posted entries only)'), ('nonofficial', 'Non-official FEC report (posted and unposted entries)'), - ], string='Export Type', required=True, default='official') + ], string='Export Type', required=True, default='official') + excluded_journal_ids = fields.Many2many('account.journal', string="Excluded Journals", domain="[('company_id', '=', current_company_id)]") @api.onchange('test_file') def _onchange_export_file(self): if not self.test_file: self.export_type = 'official' + def _get_where_query(self): + where_params = {'company_id': self.env.company.id} + where_query = "am.company_id = %(company_id)s\n" + # For official report: only use posted entries + if self.export_type == "official": + where_query += "AND am.state = 'posted'\n" + if self.excluded_journal_ids: + where_params['excluded_journal_ids'] = tuple(self.excluded_journal_ids.ids) + where_query += "AND am.journal_id NOT IN %(excluded_journal_ids)s\n" + return where_query, where_params + def _do_query_unaffected_earnings(self): ''' Compute the sum of ending balances for all accounts that are of a type that does not bring forward the balance in new fiscal years. This is needed because we have to display only one line for the initial balance of all expense/revenue accounts in the FEC. ''' - - sql_query = ''' + where_query, where_params = self._get_where_query() + sql_query = f''' SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, - 'OUVERTURE/' || %s AS EcritureNum, - %s AS EcritureDate, + 'OUVERTURE/' || %(formatted_date_year)s AS EcritureNum, + %(formatted_date_from)s AS EcritureDate, '120/129' AS CompteNum, 'Benefice (perte) reporte(e)' AS CompteLib, '' AS CompAuxNum, '' AS CompAuxLib, '-' AS PieceRef, - %s AS PieceDate, + %(formatted_date_from)s AS PieceDate, '/' AS EcritureLib, replace(CASE WHEN COALESCE(sum(aml.balance), 0) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN COALESCE(sum(aml.balance), 0) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit, '' AS EcritureLet, '' AS DateLet, - %s AS ValidDate, + %(formatted_date_from)s AS ValidDate, '' AS Montantdevise, '' AS Idevise FROM @@ -62,25 +74,17 @@ class AccountFrFec(models.TransientModel): LEFT JOIN account_move am ON am.id=aml.move_id JOIN account_account aa ON aa.id = aml.account_id WHERE - am.date < %s - AND am.company_id = %s + {where_query} + AND am.date < %(date_from)s AND aa.include_initial_balance IS NOT TRUE ''' - # For official report: only use posted entries - if self.export_type == "official": - sql_query += ''' - AND am.state = 'posted' - ''' - company = self.env.company - formatted_date_from = fields.Date.to_string(self.date_from).replace('-', '') - date_from = self.date_from - formatted_date_year = date_from.year - self._cr.execute( - sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) - listrow = [] - row = self._cr.fetchone() - listrow = list(row) - return listrow + self._cr.execute(sql_query, { + **where_params, + 'formatted_date_year': self.date_from.year, + 'formatted_date_from': fields.Date.to_string(self.date_from).replace('-', ''), + 'date_from': self.date_from, + }) + return list(self._cr.fetchone()) def _get_company_legal_data(self, company): """ @@ -162,24 +166,26 @@ class AccountFrFec(models.TransientModel): aa_name = f"COALESCE(aa.name->>'{lang}', aa.name->>'en_US')" else: aa_name = "aa.name" + + where_query, where_params = self._get_where_query() sql_query = f''' SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, - 'OUVERTURE/' || %s AS EcritureNum, - %s AS EcritureDate, + 'OUVERTURE/' || %(formatted_date_year)s AS EcritureNum, + %(formatted_date_from)s AS EcritureDate, MIN(aa.code) AS CompteNum, replace(replace(MIN({aa_name}), '|', '/'), '\t', '') AS CompteLib, '' AS CompAuxNum, '' AS CompAuxLib, '-' AS PieceRef, - %s AS PieceDate, + %(formatted_date_from)s AS PieceDate, '/' AS EcritureLib, replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit, '' AS EcritureLet, '' AS DateLet, - %s AS ValidDate, + %(formatted_date_from)s AS ValidDate, '' AS Montantdevise, '' AS Idevise, MIN(aa.id) AS CompteID @@ -188,29 +194,21 @@ class AccountFrFec(models.TransientModel): LEFT JOIN account_move am ON am.id=aml.move_id JOIN account_account aa ON aa.id = aml.account_id WHERE - am.date < %s - AND am.company_id = %s + {where_query} + AND am.date < %(date_from)s AND aa.include_initial_balance = 't' - ''' - - # For official report: only use posted entries - if self.export_type == "official": - sql_query += ''' - AND am.state = 'posted' - ''' - - sql_query += ''' GROUP BY aml.account_id, aa.account_type HAVING aa.account_type not in ('asset_receivable', 'liability_payable') ''' - formatted_date_from = fields.Date.to_string(self.date_from).replace('-', '') - date_from = self.date_from - formatted_date_year = date_from.year + params = { + **where_params, + 'formatted_date_year': self.date_from.year, + 'formatted_date_from': fields.Date.to_string(self.date_from).replace('-', ''), + 'date_from': self.date_from, + } + self._cr.execute(sql_query, params) + currency_digits = 2 - - self._cr.execute( - sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) - for row in self._cr.fetchall(): listrow = list(row) account_id = listrow.pop() @@ -250,8 +248,8 @@ class AccountFrFec(models.TransientModel): SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, - 'OUVERTURE/' || %s AS EcritureNum, - %s AS EcritureDate, + 'OUVERTURE/' || %(formatted_date_year)s AS EcritureNum, + %(formatted_date_from)s AS EcritureDate, MIN(aa.code) AS CompteNum, replace(MIN({aa_name}), '|', '/') AS CompteLib, CASE WHEN MIN(aa.account_type) IN ('asset_receivable', 'liability_payable') @@ -268,13 +266,13 @@ class AccountFrFec(models.TransientModel): ELSE '' END AS CompAuxLib, '-' AS PieceRef, - %s AS PieceDate, + %(formatted_date_from)s AS PieceDate, '/' AS EcritureLib, replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit, '' AS EcritureLet, '' AS DateLet, - %s AS ValidDate, + %(formatted_date_from)s AS ValidDate, '' AS Montantdevise, '' AS Idevise, MIN(aa.id) AS CompteID @@ -284,23 +282,13 @@ class AccountFrFec(models.TransientModel): LEFT JOIN res_partner rp ON rp.id=aml.partner_id JOIN account_account aa ON aa.id = aml.account_id WHERE - am.date < %s - AND am.company_id = %s + {where_query} + AND am.date < %(date_from)s AND aa.include_initial_balance = 't' - ''' - - # For official report: only use posted entries - if self.export_type == "official": - sql_query += ''' - AND am.state = 'posted' - ''' - - sql_query += ''' GROUP BY aml.account_id, aa.account_type, rp.ref, rp.id HAVING aa.account_type in ('asset_receivable', 'liability_payable') ''' - self._cr.execute( - sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) + self._cr.execute(sql_query, params) for row in self._cr.fetchall(): listrow = list(row) @@ -362,25 +350,15 @@ class AccountFrFec(models.TransientModel): LEFT JOIN res_currency rc ON rc.id = aml.currency_id LEFT JOIN account_full_reconcile rec ON rec.id = aml.full_reconcile_id WHERE - am.date >= %s - AND am.date <= %s - AND am.company_id = %s - ''' - - # For official report: only use posted entries - if self.export_type == "official": - sql_query += ''' - AND am.state = 'posted' - ''' - - sql_query += ''' + {where_query} + AND am.date >= %(date_from)s + AND am.date <= %(date_to)s ORDER BY am.date, am.name, aml.id ''' - self._cr.execute( - sql_query, (self.date_from, self.date_to, company.id)) + self._cr.execute(sql_query, {**params, 'date_to': self.date_to}) for row in self._cr.fetchall(): rows_to_write.append(list(row)) @@ -395,7 +373,7 @@ class AccountFrFec(models.TransientModel): 'fec_data': base64.encodebytes(fecvalue), # Filename = FECYYYYMMDD where YYYMMDD is the closing date 'filename': '%sFEC%s%s.csv' % (company_legal_data, end_date, suffix), - }) + }) # Set fiscal year lock date to the end date (not in test) fiscalyear_lock_date = self.env.company.fiscalyear_lock_date diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec_view.xml b/addons/l10n_fr_fec/wizard/account_fr_fec_view.xml index 0d6c85c52ca..525a106aac3 100644 --- a/addons/l10n_fr_fec/wizard/account_fr_fec_view.xml +++ b/addons/l10n_fr_fec/wizard/account_fr_fec_view.xml @@ -19,6 +19,7 @@ +