Browse Source

[ENH] Provide alternative way to check for unique import lines.

pull/77/head
Ronald Portier 8 years ago
parent
commit
ada0b526cb
  1. 1
      account_bank_statement_import/__openerp__.py
  2. 78
      account_bank_statement_import/models/account_bank_statement_import.py
  3. 12
      account_bank_statement_import/models/res_partner_bank.py
  4. 50
      account_bank_statement_import/parserlib.py
  5. 22
      account_bank_statement_import/views/res_partner_bank.xml
  6. 29
      account_bank_statement_import_camt/models/parser.py
  7. 22
      account_bank_statement_import_mt940_nl_ing/mt940.py

1
account_bank_statement_import/__openerp__.py

@ -12,6 +12,7 @@
'views/account_config_settings.xml', 'views/account_config_settings.xml',
'views/account_bank_statement_import_view.xml', 'views/account_bank_statement_import_view.xml',
'views/account_journal.xml', 'views/account_journal.xml',
'views/res_partner_bank.xml',
], ],
'demo': [ 'demo': [
'demo/fiscalyear_period.xml', 'demo/fiscalyear_period.xml',

78
account_bank_statement_import/models/account_bank_statement_import.py

@ -4,11 +4,13 @@ import logging
import base64 import base64
from StringIO import StringIO from StringIO import StringIO
from zipfile import ZipFile, BadZipfile # BadZipFile in Python >= 3.2 from zipfile import ZipFile, BadZipfile # BadZipFile in Python >= 3.2
import hashlib
from openerp import api, models, fields from openerp import api, models, fields
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.exceptions import Warning as UserError, RedirectWarning from openerp.exceptions import Warning as UserError, RedirectWarning
_logger = logging.getLogger(__name__) # pylint: disable=invalid-name _logger = logging.getLogger(__name__) # pylint: disable=invalid-name
@ -233,6 +235,16 @@ class AccountBankStatementImport(models.TransientModel):
# if no currency_code is provided, we'll use the company currency # if no currency_code is provided, we'll use the company currency
return self.env.user.company_id.currency_id.id return self.env.user.company_id.currency_id.id
@api.model
def _get_bank(self, account_number):
"""Get res.partner.bank."""
bank_model = self.env['res.partner.bank']
if account_number and len(account_number) > 4:
return bank_model.search(
[('acc_number', '=', account_number)], limit=1
)
return bank_model.browse([]) # Empty recordset
@api.model @api.model
def _find_bank_account_id(self, account_number): def _find_bank_account_id(self, account_number):
""" Get res.partner.bank ID """ """ Get res.partner.bank ID """
@ -337,12 +349,19 @@ class AccountBankStatementImport(models.TransientModel):
def _complete_statement(self, stmt_vals, journal_id, account_number): def _complete_statement(self, stmt_vals, journal_id, account_number):
"""Complete statement from information passed.""" """Complete statement from information passed."""
stmt_vals['journal_id'] = journal_id stmt_vals['journal_id'] = journal_id
statement_bank = self._get_bank(account_number)
for line_vals in stmt_vals['transactions']: for line_vals in stmt_vals['transactions']:
unique_import_id = line_vals.get('unique_import_id', False)
unique_import_id = (
statement_bank.enforce_unique_import_lines and
'data' in line_vals and line_vals['data'] and
hashlib.md5(line_vals['data']).hexdigest() or
'unique_import_id' in line_vals and
line_vals['unique_import_id'] or
False
)
if unique_import_id: if unique_import_id:
line_vals['unique_import_id'] = ( line_vals['unique_import_id'] = (
(account_number and account_number + '-' or '') +
unique_import_id
statement_bank.acc_number + '-' + unique_import_id
) )
if not line_vals.get('bank_account_id'): if not line_vals.get('bank_account_id'):
# Find the partner and his bank account or create the bank # Find the partner and his bank account or create the bank
@ -353,16 +372,15 @@ class AccountBankStatementImport(models.TransientModel):
bank_account_id = False bank_account_id = False
partner_account_number = line_vals.get('account_number') partner_account_number = line_vals.get('account_number')
if partner_account_number: if partner_account_number:
bank_model = self.env['res.partner.bank']
banks = bank_model.search(
[('acc_number', '=', partner_account_number)], limit=1)
if banks:
bank_account_id = banks[0].id
partner_id = banks[0].partner_id.id
partner_bank = self._get_bank(partner_account_number)
if partner_bank:
partner_id = partner_bank.partner_id.id
else: else:
bank_obj = self._create_bank_account(
partner_account_number)
bank_account_id = bank_obj and bank_obj.id or False
partner_bank = self._create_bank_account(
partner_account_number
)
if partner_bank:
bank_account_id = partner_bank.id
line_vals['partner_id'] = partner_id line_vals['partner_id'] = partner_id
line_vals['bank_account_id'] = bank_account_id line_vals['bank_account_id'] = bank_account_id
if 'date' in stmt_vals and 'period_id' not in stmt_vals: if 'date' in stmt_vals and 'period_id' not in stmt_vals:
@ -389,22 +407,44 @@ class AccountBankStatementImport(models.TransientModel):
# Filter out already imported transactions and create statement # Filter out already imported transactions and create statement
ignored_line_ids = [] ignored_line_ids = []
filtered_st_lines = [] filtered_st_lines = []
unique_ids = {}
duplicates = 0
for line_vals in stmt_vals['transactions']: for line_vals in stmt_vals['transactions']:
unique_id = (
'unique_import_id' in line_vals and
line_vals['unique_import_id']
)
if not unique_id or not bool(bsl_model.sudo().search(
[('unique_import_id', '=', unique_id)], limit=1)):
unique_id = line_vals.get('unique_import_id', False)
if not unique_id:
filtered_st_lines.append(line_vals) filtered_st_lines.append(line_vals)
else:
continue
if unique_id in unique_ids:
# Not unique within statement!
# In these cases the duplicates should both be imported.
# Most like case is if the same person made two equal payments
# on the same day. But duplicate will be marked.
duplicates += 1
_logger.warn(
_("line with unique_id %s is not in fact unique: %s"),
unique_id, line_vals)
unique_id += " %d" % duplicates
line_vals['name'] = (_(
"Duplicate: %s") % line_vals['name'] or '')
line_vals['unique_import_id'] = unique_id
filtered_st_lines.append(line_vals)
continue
existing_line = bsl_model.sudo().search([
('unique_import_id', '=', unique_id),
('company_id', '=', self.env.user.company_id.id)], limit=1)
if existing_line:
ignored_line_ids.append(unique_id) ignored_line_ids.append(unique_id)
continue
unique_ids[unique_id] = line_vals
filtered_st_lines.append(line_vals)
statement_id = False statement_id = False
if len(filtered_st_lines) > 0: if len(filtered_st_lines) > 0:
# Remove values that won't be used to create records # Remove values that won't be used to create records
stmt_vals.pop('transactions', None) stmt_vals.pop('transactions', None)
for line_vals in filtered_st_lines: for line_vals in filtered_st_lines:
line_vals.pop('account_number', None) line_vals.pop('account_number', None)
line_vals.pop('transaction_id', None)
line_vals.pop('data', None)
# Create the statement # Create the statement
stmt_vals['line_ids'] = [ stmt_vals['line_ids'] = [
[0, False, line] for line in filtered_st_lines] [0, False, line] for line in filtered_st_lines]

12
account_bank_statement_import/models/res_partner_bank.py

@ -33,6 +33,18 @@ class ResPartnerBank(models.Model):
sanitized_acc_number = fields.Char( sanitized_acc_number = fields.Char(
'Sanitized Account Number', size=64, readonly=True, 'Sanitized Account Number', size=64, readonly=True,
compute='_get_sanitized_account_number', store=True, index=True) compute='_get_sanitized_account_number', store=True, index=True)
enforce_unique_import_lines = fields.Boolean(
string='Force unique lines on import',
help="Some banks do not provide an unique id for transactions in"
" bank statements. In some cases it is possible that multiple"
" downloads contain overlapping transactions. In that case"
" activate this option to generate a unique id based on all the"
" information in the transaction. This prevents duplicate"
" imports, at the cost of - in exceptional cases - missing"
" transactions when all the information in two or more"
" transactions is the same.\n"
"This setting is only relevant for banks linked to a company."
)
def _sanitize_account_number(self, acc_number): def _sanitize_account_number(self, acc_number):
if acc_number: if acc_number:

50
account_bank_statement_import/parserlib.py

@ -1,28 +1,22 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Classes and definitions used in parsing bank statements.""" """Classes and definitions used in parsing bank statements."""
##############################################################################
#
# Copyright (C) 2015 Therp BV <http://therp.nl>.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
# © 2015-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
class BankTransaction(dict): class BankTransaction(dict):
"""Single transaction that is part of a bank statement.""" """Single transaction that is part of a bank statement."""
@property
def transaction_id(self):
"""property getter"""
return self['transaction_id']
@transaction_id.setter
def transaction_id(self, transaction_id):
"""property setter"""
self['transaction_id'] = transaction_id
@property @property
def value_date(self): def value_date(self):
"""property getter""" """property getter"""
@ -106,12 +100,21 @@ class BankTransaction(dict):
def note(self, note): def note(self, note):
self['note'] = note self['note'] = note
@property
def data(self):
return self['data']
@data.setter
def data(self, data):
self['data'] = data
def __init__(self): def __init__(self):
"""Define and initialize attributes. """Define and initialize attributes.
Not all attributes are already used in the actual import. Not all attributes are already used in the actual import.
""" """
super(BankTransaction, self).__init__() super(BankTransaction, self).__init__()
self.transaction_id = '' # Fill this only if unique for import
self.transfer_type = False # Action type that initiated this message self.transfer_type = False # Action type that initiated this message
self.execution_date = False # The posted date of the action self.execution_date = False # The posted date of the action
self.value_date = False # The value date of the action self.value_date = False # The value date of the action
@ -152,8 +155,15 @@ class BankStatement(dict):
subno = 0 subno = 0
for transaction in self['transactions']: for transaction in self['transactions']:
subno += 1 subno += 1
transaction['unique_import_id'] = (
self.statement_id + str(subno).zfill(4))
if transaction.transaction_id:
transaction['unique_import_id'] = transaction.transaction_id
else:
# Cut local_account from prefix if applicable:
if self.statement_id.startswith(self.local_account):
prefix = self.statement_id[len(self.local_account):]
else:
prefix = self.statement_id
transaction['unique_import_id'] = prefix + str(subno).zfill(4)
@statement_id.setter @statement_id.setter
def statement_id(self, statement_id): def statement_id(self, statement_id):

22
account_bank_statement_import/views/res_partner_bank.xml

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="view_partner_bank_form" model="ir.ui.view">
<field name="model">res.partner.bank</field>
<field name="inherit_id" ref="base.view_partner_bank_form" />
<field name="arch" type="xml">
<group name="bank" position="after">
<group
name="import_settings"
string="Bank Statement Import Settings"
invisible="context.get('company_hide', False)"
>
<field
name="enforce_unique_import_lines"
/>
</group>
</group>
</field>
</record>
</data>
</openerp>

29
account_bank_statement_import_camt/models/parser.py

@ -1,37 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Class to parse camt files."""
##############################################################################
#
# Copyright (C) 2013-2015 Therp BV <http://therp.nl>
# Copyright 2017 Open Net Sàrl
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
# Copyright 2013-2018 Therp BV <https://therp.nl>.
# Copyright 2017 Open Net Sàrl.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import re import re
from copy import copy from copy import copy
from datetime import datetime from datetime import datetime
from lxml import etree from lxml import etree
from openerp import _
from openerp import _, models
from openerp.addons.account_bank_statement_import.parserlib import ( from openerp.addons.account_bank_statement_import.parserlib import (
BankStatement) BankStatement)
from openerp import models
class CamtParser(models.AbstractModel): class CamtParser(models.AbstractModel):
_name = 'account.bank.statement.import.camt.parser' _name = 'account.bank.statement.import.camt.parser'
@ -177,6 +157,7 @@ class CamtParser(models.AbstractModel):
details_nodes = node.xpath( details_nodes = node.xpath(
'./ns:NtryDtls/ns:TxDtls', namespaces={'ns': ns}) './ns:NtryDtls/ns:TxDtls', namespaces={'ns': ns})
if len(details_nodes) == 0: if len(details_nodes) == 0:
transaction['data'] = etree.tostring(node)
yield transaction yield transaction
return return
transaction_base = transaction transaction_base = transaction

22
account_bank_statement_import_mt940_nl_ing/mt940.py

@ -1,23 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Implement BankStatementParser for MT940 IBAN ING files.""" """Implement BankStatementParser for MT940 IBAN ING files."""
##############################################################################
#
# Copyright (C) 2014-2015 Therp BV <http://therp.nl>.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
# © 2014-2016 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import re import re
from openerp.addons.account_bank_statement_import_mt940_base.mt940 import ( from openerp.addons.account_bank_statement_import_mt940_base.mt940 import (
MT940, str2amount, get_subfields, handle_common_subfields) MT940, str2amount, get_subfields, handle_common_subfields)
@ -48,7 +32,7 @@ class MT940Parser(MT940):
self.current_transaction.transferred_amount = ( self.current_transaction.transferred_amount = (
str2amount(parsed_data['sign'], parsed_data['amount'])) str2amount(parsed_data['sign'], parsed_data['amount']))
self.current_transaction.eref = parsed_data['reference'] self.current_transaction.eref = parsed_data['reference']
self.current_transaction.id = parsed_data['ingid']
self.current_transaction.transaction_id = parsed_data['ingid']
def handle_tag_86(self, data): def handle_tag_86(self, data):
"""Parse 86 tag containing reference data.""" """Parse 86 tag containing reference data."""

Loading…
Cancel
Save