[FIX] l10n_fr_fec: allow exclusion of journals in FEC export

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) <flg@odoo.com>
This commit is contained in:
jbw-odoo
2023-02-09 18:07:12 +01:00
parent 3148ec8c8e
commit d050b8e8a0
3 changed files with 99 additions and 85 deletions
+42 -7
View File
@@ -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)
+56 -78
View File
@@ -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 = <siren>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
@@ -19,6 +19,7 @@
<field name="date_to"/>
<field name="test_file"/>
<field name="export_type" attrs="{'invisible': [('test_file', '=', False)]}"/>
<field name="excluded_journal_ids" widget="many2many_tags" options="{'no_create': True}"/>
</group>
</page>
<page string="Technical Info" name="technical_info">