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

308 lines
15 KiB
Python

import logging
import requests
from collections import defaultdict
from dateutil.relativedelta import relativedelta
from odoo import modules, models, fields, _
from odoo.exceptions import UserError
from odoo.addons.base.models.res_bank import sanitize_account_number
from odoo.addons.l10n_be_codabox.const import get_error_msg, get_iap_endpoint
_logger = logging.getLogger(__name__)
class AccountJournal(models.Model):
_inherit = "account.journal"
############################
# COMMON CODABOX METHODS
############################
def _fill_bank_cash_dashboard_data(self, dashboard_data):
super()._fill_bank_cash_dashboard_data(dashboard_data)
for journal_id in dashboard_data:
journal = self.browse(journal_id)
dashboard_data[journal_id]["l10n_be_codabox_is_connected"] = journal.company_id.l10n_be_codabox_is_connected
dashboard_data[journal_id]["l10n_be_codabox_journal_is_soda"] = journal == journal.company_id.l10n_be_codabox_soda_journal
if journal == journal.company_id.l10n_be_codabox_soda_journal:
dashboard_data[journal_id]["l10n_be_codabox_number_draft"] = self.env['account.move'].search_count([
('journal_id', '=', journal.id),
('state', '=', 'draft'),
])
def l10n_be_codabox_action_open_settings_open_draft_soda_entries(self):
return {
"type": "ir.actions.act_window",
"res_model": "account.move",
"view_mode": "list,form",
"domain": [("journal_id", "=", self.id), ("state", "=", "draft")],
"target": "current",
}
def _l10n_be_codabox_fetch_transactions_from_iap(self, session, company, file_type, date_from=None, ibans=None):
assert file_type in ("codas", "sodas")
company._l10n_be_codabox_verify_prerequisites()
if not date_from:
date_from = fields.Date.to_string(fields.Date.today() - relativedelta(years=1))
params = {
"db_uuid": self.env["ir.config_parameter"].sudo().get_param("database.uuid"),
"fidu_vat": company.l10n_be_codabox_fiduciary_vat,
"company_vat": company.l10n_be_codabox_company_vat,
"iap_token": company.sudo().l10n_be_codabox_iap_token,
"from_date": date_from,
}
if file_type == "sodas":
method = "get_soda_files"
else:
method = "get_coda_files"
params["ibans"] = ibans
try:
response = session.post(f"{get_iap_endpoint(self.env)}/{method}", json={"params": params}, timeout=(10, 900))
result = response.json().get("result", {})
error = result.get("error")
if error:
if error.get("type") in ("error_connection_not_found", "error_consent_not_valid"):
# Modify the status in a new cursor to avoid the current transaction to be rolled back
with self.pool.cursor() as new_cr:
company = company.with_env(self.env(cr=new_cr))
company.l10n_be_codabox_is_connected = False
if not self.env.context.get("l10n_be_codabox_from_cron"): # avoid disabling cron if too many fails
raise UserError(get_error_msg(error))
else:
_logger.error("L10nBeCodabox: CRON %s error: %s", file_type, error)
return result.get("files", [])
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
if not self.env.context.get("l10n_be_codabox_from_cron"):
raise UserError(get_error_msg({"type": "error_connecting_iap"}))
else:
_logger.error("L10nBeCodabox: CRON %s error: error_connecting_iap", file_type)
return []
############################
# CODA METHODS
############################
def __get_bank_statements_available_sources(self):
rslt = super().__get_bank_statements_available_sources()
rslt.append(("l10n_be_codabox", _("CodaBox Synchronization")))
return rslt
def _l10n_be_codabox_fetch_coda_transactions(self, company):
if not company.l10n_be_codabox_is_connected:
raise UserError(get_error_msg({"type": "error_codabox_not_configured"}))
date_one_year_ago = fields.Date.to_string(fields.Date.today() - relativedelta(years=1))
ibans = {} # {iban: last_date} where last_date is the date of the last bank statement or transaction
codabox_journals = self.search([
("bank_statements_source", "=", "l10n_be_codabox"),
("bank_acc_number", "!=", False),
("company_id", "=", company.id),
])
for journal in codabox_journals:
last_date = self.env["account.bank.statement"].search([
("journal_id", "=", journal.id),
], order="date DESC", limit=1).date
if not last_date:
last_date = self.env["account.bank.statement.line"].search([
("journal_id", "=", journal.id),
], order="date DESC", limit=1).date
iban = journal.bank_acc_number.replace(" ", "").upper()
last_date = fields.Date.to_string(last_date) or date_one_year_ago
if iban not in ibans:
ibans[iban] = last_date
else:
ibans[iban] = min(ibans[iban], last_date)
lock_date = max(company.user_fiscalyear_lock_date, company.user_hard_lock_date)
date_from = max(min(ibans.values()) if ibans else date_one_year_ago, fields.Date.to_string(lock_date))
statement_ids_all = []
skipped_bank_accounts = set()
session = requests.Session()
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(
[("bank_statements_source", "in", ("l10n_be_codabox", "undefined"))],
['bank_acc_number'],
['id:recordset'],
)
}
codas = self._l10n_be_codabox_fetch_transactions_from_iap(session, company, "codas", date_from, ibans)
journal_stats = defaultdict(lambda: {
"count": 0,
"dates": [],
"currency": None,
})
imported_files = 0
for coda in codas:
try:
coda_raw_b64, coda_pdf_b64 = coda
coda_attachment = self.env["ir.attachment"].create({
"name": _("Original CodaBox.coda"),
'type': 'binary',
'datas': coda_raw_b64,
})
# We parse first the CODA to get the IBAN and Currency so that we can select a matching journal
bank_statement_file_data = self.with_context(ignore_statements=True)._parse_bank_statement_file(coda_attachment.raw)
statement_ids = []
for currency, account_number, extension_number, __ in bank_statement_file_data:
# Prefer a journal with an explicit currency matching
candidates = acc_journal_map.get(
sanitize_account_number(account_number),
self.env['account.journal']
)
journals = (
candidates.filtered(lambda j: j.currency_id.name == currency)
or candidates.filtered(lambda j: not j.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_codabox"
else:
skipped_bank_accounts.add(f"{account_number} ({currency})")
continue
# We reparse now to have all info including the statements
currency, account_number, extension_number, stmt_vals = journal._parse_bank_statement_file(coda_attachment.raw)[0]
stmt_vals = journal._complete_bank_statement_vals(stmt_vals, journal, account_number, coda_attachment)
statement_id = journal.with_context(skip_pdf_attachment_generation=True)._create_bank_statements(stmt_vals, raise_no_imported_file=False)[0]
if statement_id:
statement_ids.extend(statement_id)
# Logging Part
imported_files += 1
stats = journal_stats[journal]
stats['count'] += 1
stats['currency'] = currency
stats['dates'].append(stmt_vals[0].get('date'))
if statement_ids:
statement_ids_all.extend(statement_ids)
pdf = self.env['ir.attachment'].create({
'name': _("Original CodaBox Bank Statement.pdf"),
'type': 'binary',
'mimetype': 'application/pdf',
'datas': coda_pdf_b64,
'res_model': 'account.bank.statement',
'res_id': statement_ids[0],
})
self.env['account.bank.statement'].browse(statement_ids).attachment_ids |= pdf
# 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("L10nBeCodabox: Error while importing CodaBox 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()
_logger.info("L10nBeCodabox: Coda import summary - Fetched: %s - Imported: %s - Journals: %s", len(codas), imported_files, len(journal_stats))
for journal, stats in journal_stats.items():
_logger.info("%s (%s): %s transactions (From: %s - To: %s) with currency %s", journal.code, journal.id, stats['count'], min(stats['dates']), max(stats['dates']), stats["currency"])
if skipped_bank_accounts:
_logger.info("L10nBeCodabox: No journals were found for the following bank accounts found in CodaBox: %s", ','.join(skipped_bank_accounts))
return statement_ids_all
def l10n_be_codabox_manually_fetch_coda_transactions(self):
self.ensure_one()
statement_ids = self._l10n_be_codabox_fetch_coda_transactions(self.company_id.with_context(read_only=True))
return self.env["account.bank.statement.line"]._action_open_bank_reconciliation_widget(
extra_domain=[("statement_id", "in", statement_ids)],
)
def _l10n_be_codabox_cron_fetch_coda_transactions(self):
coda_companies = self.env['res.company'].search([
('l10n_be_codabox_is_connected', '=', True),
])
if not coda_companies:
_logger.info("L10BeCodabox: CODA No company is connected to CodaBox.")
return
for company in coda_companies:
self.with_context(l10n_be_codabox_from_cron=True)._l10n_be_codabox_fetch_coda_transactions(company)
############################
# SODA METHODS
############################
def _l10n_be_codabox_fetch_soda_transactions(self, company):
soda_journal = company.l10n_be_codabox_soda_journal
session = requests.Session()
last_soda_date = self.env["account.move"].search([
("journal_id", "=", soda_journal.id),
], order="date DESC", limit=1).date
if not last_soda_date:
last_soda_date = fields.Date.today() - relativedelta(years=2) # API goes back 2 years max
lock_date = max(company.user_fiscalyear_lock_date, company.user_hard_lock_date)
date_from = max(last_soda_date, lock_date)
sodas = self._l10n_be_codabox_fetch_transactions_from_iap(session, soda_journal.company_id, "sodas", fields.Date.to_string(date_from))
_logger.info("L10nBeCodabox: %s SODAs fetched", len(sodas))
moves = self.env["account.move"]
for soda in sodas:
try:
soda_raw_b64, soda_pdf_b64 = soda
attachment_soda = self.env["ir.attachment"].create({
"name": _("Original CodaBox SODA.xml"),
'type': 'binary',
'datas': soda_raw_b64,
'company_id': self.company_id.id,
})
move = soda_journal.with_context(raise_no_imported_file=False)._l10n_be_parse_soda_file(attachment_soda, skip_wizard=True)
if move:
attachment_pdf = self.env["ir.attachment"].create({
'name': _("Original CodaBox Payroll Statement.pdf"),
'type': 'binary',
'mimetype': 'application/pdf',
'datas': soda_pdf_b64,
'res_model': move._name,
'res_id': move.id,
})
move.attachment_ids += attachment_pdf + attachment_soda
moves += move
# We may have a lot of files to import, so we commit after each file so that a later error doesn't discard previous work
self.env.cr.commit()
except (UserError, ValueError) as e:
# We need to rollback here otherwise the next iteration will still have the error when trying to commit
_logger.error("L10nBeCodabox: Error while importing CodaBox file: %s", e)
self.env.cr.rollback()
return moves
def l10n_be_codabox_manually_fetch_soda_transactions(self):
self.ensure_one()
moves = self._l10n_be_codabox_fetch_soda_transactions(self.company_id)
if not moves:
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": _("No SODA imported"),
"message": _("No new SODA is available for import."),
"sticky": False,
},
}
return {
"type": "ir.actions.act_window",
"res_model": "account.move",
"view_mode": "list,form",
"views": [(False, "list"), (False, "form")],
"domain": [("id", "in", moves.ids)],
}
def _l10n_be_codabox_cron_fetch_soda_transactions(self):
codabox_companies = self.env['res.company'].search([
('l10n_be_codabox_is_connected', '=', True),
('l10n_be_codabox_soda_journal', '!=', False),
])
if not codabox_companies:
_logger.info("L10BeCodabox: SODA No company is connected to CodaBox.")
return
for company in codabox_companies:
imported_moves = self.with_context(l10n_be_codabox_from_cron=True)._l10n_be_codabox_fetch_soda_transactions(company)
_logger.info("L10BeCodabox: %s payroll statements were imported.", len(imported_moves))