Files
enterprise-addons/l10n_be_codaclean/models/account_journal.py
T

185 lines
9.2 KiB
Python

import logging
from dateutil.relativedelta import relativedelta
from odoo import _, api, fields, models, modules
from odoo.exceptions import RedirectWarning, UserError
from odoo.tools import exception_to_unicode
from odoo.addons.base.models.res_bank import sanitize_account_number
from odoo.addons.l10n_be_codaclean.tools.iap_api import get_error_message
_logger = logging.getLogger(__name__)
class AccountJournal(models.Model):
_inherit = "account.journal"
def __get_bank_statements_available_sources(self):
# Extends 'account'
rslt = super().__get_bank_statements_available_sources()
rslt.append(("l10n_be_codaclean", _("Codaclean Synchronization")))
return rslt
@api.model
def _l10n_be_codaclean_fetch_coda_transactions(self, company):
if not company.l10n_be_codaclean_is_connected:
raise RedirectWarning(
message=_("Not connected to Codaclean. Please check your configuration."),
action=self.env.ref('account.action_account_config').id,
button_text=_("Go to settings"),
)
codaclean_journals = self.search([
("bank_statements_source", "=", "l10n_be_codaclean"),
("bank_acc_number", "!=", False),
("company_id", "=", company.id),
])
if not codaclean_journals:
return []
# We fetch only coda files with a statment date after
# - The most recent bank statement
# - The most recent bank statement line (if there are no bank statements)
# - The date 1 year ago (if we did not find bank statement lines either)
last_statement_date = dict(self.env["account.bank.statement"]._read_group(
domain=[('journal_id', 'in', codaclean_journals.ids)],
groupby=['journal_id'],
aggregates=['date:max'],
))
# Note that there can be entries with value `False` in last_statement_date
remaining_journals = self.browse([journal.id for journal in codaclean_journals if not last_statement_date.get(journal)])
last_statement_line_date = dict(self.env["account.bank.statement.line"]._read_group(
domain=[('journal_id', 'in', remaining_journals.ids)],
groupby=['journal_id'],
aggregates=['date:max'],
)) if remaining_journals else {}
date_one_year_ago = fields.Date.today() - relativedelta(years=1)
ibans = {} # {iban: last_date} where last_date is the date of the last bank statement or transaction
for journal in codaclean_journals:
iban = journal.bank_acc_number.replace(" ", "").upper()
# Note that there can be entries with value `False` in last_statement_date / last_statement_line_date
last_date = last_statement_date.get(journal) or last_statement_line_date.get(journal) or date_one_year_ago
ibans[iban] = min(ibans.get(iban) or last_date, last_date)
date_from = min(ibans.values()) or date_one_year_ago
# Format the dates
ibans = {iban: fields.Date.to_string(date) for iban, date in ibans.items()}
date_from = fields.Date.to_string(date_from)
result = company._l10n_be_codaclean_fetch_coda_files(date_from, ibans)
if not company.l10n_be_codaclean_iap_token:
# Modify the status in a new cursor to prevent the changes from being rolled back
with company.pool.cursor() as new_cr:
company = company.with_env(company.env(cr=new_cr))
company.l10n_be_codaclean_iap_token = False
if error := result.get("error", {}):
raise UserError(get_error_message(error))
return result.get('files', [])
@api.model
def _l10n_be_codaclean_import_coda_files(self, company, codas):
if not codas:
return []
statement_ids_all = []
skipped_bank_accounts = set()
acc_journal_map = {
# A same account number could be formatted differently in journal.acc_number and
# coda statement. Therefor we must match sanitized versions of both.
sanitize_account_number(acc_num):
journals for acc_num, journals in self._read_group(
[
("company_id", "=", company.id),
("bank_statements_source", "in", {"l10n_be_codaclean", "undefined"}),
],
['bank_acc_number'],
['id:recordset'],
)
}
for coda_b64, pdf_b64 in codas:
try:
coda_attachment = self.env["ir.attachment"].create({
"name": 'codaclean_coda.coda',
'type': 'binary',
'datas': coda_b64,
})
statement_ids = []
for currency, account_number, *data in self._parse_bank_statement_file(coda_attachment.raw):
if self._fields.get('extension_number'): # Only on stable because hard to extend code in new module.
extension_number, stmt_vals = data
else:
extension_number = None
stmt_vals = data[-1]
journals = [
journal for
journal in acc_journal_map.get(sanitize_account_number(account_number), [])
if (
(journal.currency_id and journal.currency_id.name == currency)
or not journal.currency_id
)
]
if len(journals) > 1 and extension_number:
journal = next((j for j in journals if j.extension_number == extension_number), False)
else:
journal = journals[0] if journals else None
if journal:
journal.bank_statements_source = "l10n_be_codaclean"
else:
skipped_bank_accounts.add(f"{account_number} ({currency})")
continue
stmt_vals = journal._complete_bank_statement_vals(stmt_vals, journal, account_number, coda_attachment)
statement_ids.extend(journal.with_context(skip_pdf_attachment_generation=True)._create_bank_statements(stmt_vals, raise_no_imported_file=False)[0])
if statement_ids:
statement_ids_all.extend(statement_ids)
# We can not add an attachment to multiple bank statements at once.
# (See function `write` of model 'account.bank.statement' in module 'account'.)
pdf_attachment = self.env['ir.attachment'].create({
'name': 'codaclean_pdf.pdf',
'type': 'binary',
'mimetype': 'application/pdf',
'datas': pdf_b64,
})
for statement in self.env['account.bank.statement'].browse(statement_ids):
statement.attachment_ids |= pdf_attachment
# We may have a lot of statements to import, so we commit after each so that a later error doesn't discard previous work
if not modules.module.current_test:
self.env.cr.commit()
except (UserError, ValueError) as e:
_logger.error("Error while importing Codaclean file: %s", e)
# We need to rollback here otherwise the next iteration will still have the error when trying to commit
self.env.cr.rollback()
if skipped_bank_accounts:
_logger.info("No journals were found for the following bank accounts parsed from the Coda files: %s", ','.join(skipped_bank_accounts))
return statement_ids_all
def l10n_be_codaclean_manually_fetch_coda_transactions(self):
self.ensure_one()
codas = self._l10n_be_codaclean_fetch_coda_transactions(self.company_id)
statement_ids = self._l10n_be_codaclean_import_coda_files(self.company_id, codas)
return self.env["account.bank.statement.line"]._action_open_bank_reconciliation_widget(
extra_domain=[("statement_id", "in", statement_ids)],
)
@api.model
def _l10n_be_codaclean_cron_fetch_coda_transactions(self):
coda_companies = self.env['res.company'].search([
('l10n_be_codaclean_is_connected', '=', True),
])
if not coda_companies:
_logger.info("No company is connected to Codaclean.")
return
for company in coda_companies:
# We want to avoid raising in the cron
try:
codas = self._l10n_be_codaclean_fetch_coda_transactions(company)
except (UserError, RedirectWarning) as e:
_logger.warning("Fetching coda files for company '%s' (id = %s) failed: %s", company.name, company.id, exception_to_unicode(e))
continue
_logger.info("%s coda files were fetched for company '%s' (id = %s).", len(codas), company.name, company.id)
statement_count = len(self._l10n_be_codaclean_import_coda_files(company, codas))
_logger.info("%s bank statements were imported for company '%s' (id = %s).", statement_count, company.name, company.id)