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

176 lines
9.1 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import io
import logging
from lxml import etree
from odoo import models, _
from odoo.exceptions import UserError
from odoo.addons.base.models.res_bank import sanitize_account_number
from odoo.addons.account_bank_statement_import_camt.lib.camt import CAMT
_logger = logging.getLogger(__name__)
class AccountJournal(models.Model):
_inherit = 'account.journal'
def _get_bank_statements_available_import_formats(self):
rslt = super()._get_bank_statements_available_import_formats()
rslt.append('CAMT')
return rslt
def _check_camt(self, raw_file):
try:
root = etree.parse(io.BytesIO(raw_file)).getroot()
except Exception:
return None
if root.tag.find('camt.053') != -1:
return root
return None
def _parse_bank_statement_file(self, raw_file):
root = self._check_camt(raw_file)
if root is not None:
return self._parse_bank_statement_file_camt(root)
return super()._parse_bank_statement_file(raw_file)
def _parse_bank_statement_file_camt(self, root):
ns = {'ns': root.xpath('namespace-uri(.)')}
curr_cache = {c['name']: c['id'] for c in self.env['res.currency'].search_read([], ['id', 'name'])}
statements_per_iban = {}
currency_per_iban = {}
unique_import_set = set([])
currency = account_no = False
has_multi_currency = self.env.user.has_group('base.group_multi_currency')
journal_currency = self.currency_id or self.company_id.currency_id
for statement in root[0].findall('ns:Stmt', ns):
statement_vals = {}
statement_vals['name'] = (statement.xpath('ns:LglSeqNb/text()', namespaces=ns) or statement.xpath('ns:Id/text()', namespaces=ns))[0]
statement_date = CAMT._get_statement_date(statement, namespaces=ns)
# Transaction Entries 0..n
transactions = []
sequence = 0
# Account Number 1..1
# if not IBAN value then... <Othr><Id> would have.
account_no = sanitize_account_number(statement.xpath('ns:Acct/ns:Id/ns:IBAN/text() | ns:Acct/ns:Id/ns:Othr/ns:Id/text()',
namespaces=ns)[0])
# Currency 0..1
currency = statement.xpath('ns:Acct/ns:Ccy/text() | ns:Bal/ns:Amt/@Ccy', namespaces=ns)[0]
if currency and journal_currency and currency != journal_currency.name:
continue
for entry in statement.findall('ns:Ntry', ns):
# Date 0..1
date = CAMT._get_transaction_date(entry, namespaces=ns) or statement_date
transaction_details = entry.xpath('.//ns:TxDtls', namespaces=ns)
entry_details_sum = 0
largest_entry_vals = {'amount': 0}
for entry_details in transaction_details or [entry]:
sequence += 1
counter_party = CAMT._get_counter_party(entry_details, entry, namespaces=ns)
partner_name = CAMT._get_partner_name(entry_details, placeholder=counter_party, namespaces=ns)
entry_vals = {
'sequence': sequence,
'date': date,
'amount': CAMT._get_signed_amount(entry_details, entry, namespaces=ns, journal_currency=journal_currency),
'payment_ref': CAMT._get_transaction_name(entry_details, namespaces=ns, entry=entry),
'partner_name': partner_name,
'account_number': CAMT._get_account_number(entry_details, placeholder=counter_party, namespaces=ns),
'ref': CAMT._get_ref(entry_details, counter_party=counter_party, prefix='', namespaces=ns),
}
entry_vals['unique_import_id'] = CAMT._get_unique_import_id(
entry=entry_details,
sequence=sequence,
name=statement_vals['name'],
date=entry_vals['date'],
unique_import_set=unique_import_set,
namespaces=ns)
CAMT._set_amount_in_currency(
node=entry_details,
getters=CAMT._currency_amount_getters,
entry_vals=entry_vals,
currency=currency,
curr_cache=curr_cache,
has_multi_currency=has_multi_currency,
namespaces=ns)
BkTxCd = entry.xpath('ns:BkTxCd', namespaces=ns)[0]
entry_vals.update(CAMT._get_transaction_type(BkTxCd, namespaces=ns))
notes = []
entry_info = CAMT._get_additional_entry_info(entry, namespaces=ns)
if entry_info:
notes.append(_('Entry Info: %s', entry_info))
text_info = CAMT._get_additional_text_info(entry_details, namespaces=ns)
if text_info:
notes.append(_('Additional Info: %s', text_info))
if partner_name:
notes.append(_('Counter Party: %(partner)s', partner=partner_name))
partner_address = CAMT._get_partner_address(entry_details, ns, counter_party)
if partner_address:
notes.append(_('Address:\n%s', partner_address))
transaction_id = CAMT._get_transaction_id(entry_details, namespaces=ns)
if transaction_id:
notes.append(_('Transaction ID: %s', transaction_id))
instruction_id = CAMT._get_instruction_id(entry_details, namespaces=ns)
if instruction_id:
notes.append(_('Instruction ID: %s', instruction_id))
end_to_end_id = CAMT._get_end_to_end_id(entry_details, namespaces=ns)
if end_to_end_id:
notes.append(_('End to end ID: %s', end_to_end_id))
mandate_id = CAMT._get_mandate_id(entry_details, namespaces=ns)
if mandate_id:
notes.append(_('Mandate ID: %s', mandate_id))
check_number = CAMT._get_check_number(entry_details, namespaces=ns)
if check_number:
notes.append(_('Check Number: %s', check_number))
entry_vals['narration'] = "\n".join(notes)
unique_import_set.add(entry_vals['unique_import_id'])
transactions.append(entry_vals)
entry_details_sum += entry_vals['amount']
if abs(entry_vals['amount']) >= abs(largest_entry_vals['amount']):
largest_entry_vals = entry_vals
# In a multi-currency entry (Ntry) with multiple entry details, we might have some rounding differences when applying the currency rate.
# We add this difference back on the largest amount.
transaction_amount = float(entry.find('ns:Amt', namespaces=ns).text)
transaction_amount = -transaction_amount if entry.find('ns:CdtDbtInd', namespaces=ns).text == 'DBIT' else transaction_amount
largest_entry_vals['amount'] += transaction_amount - entry_details_sum
statement_vals['transactions'] = transactions
statement_vals['balance_start'] = CAMT._get_signed_balance(node=statement, namespaces=ns, getters=CAMT._start_balance_getters)
statement_vals['balance_end_real'] = CAMT._get_signed_balance(node=statement, namespaces=ns, getters=CAMT._end_balance_getters)
# Save statements and currency
statements_per_iban.setdefault(account_no, []).append(statement_vals)
currency_per_iban[account_no] = currency
# If statements target multiple journals, returns thoses targeting the current journal
if len(statements_per_iban) > 1:
account_no = sanitize_account_number(self.bank_acc_number)
_logger.warning("The following statements will not be imported because they are targeting another journal (current journal id: %s):\n- %s",
account_no, "\n- ".join("{}: {} statement(s)".format(iban, len(statements)) for iban, statements in statements_per_iban.items() if iban != account_no))
if not account_no:
raise UserError(_("Please set the IBAN account on your bank journal.\n\nThis CAMT file is targeting several IBAN accounts but none match the current journal."))
# Otherwise, returns those from only account_no
statement_list = statements_per_iban.get(account_no, [])
currency = currency_per_iban.get(account_no)
if not currency and not statement_list:
raise UserError(_("Please check the currency on your bank journal.\n"
"No statements in currency %s were found in this CAMT file.", journal_currency.name))
return [[currency, account_no, statement_list]]