[MERGE] merged the feature branch that improves the import of bank statements. PR 3718

This commit is contained in:
qdp-odoo
2015-01-14 18:32:28 +01:00
parent c79fdfceb4
commit 327cb95867
30 changed files with 915 additions and 467 deletions
+40 -39
View File
@@ -719,7 +719,6 @@ class account_account(osv.osv):
self._check_moves(cr, uid, ids, "unlink", context=context)
return super(account_account, self).unlink(cr, uid, ids, context=context)
class account_journal(osv.osv):
_name = "account.journal"
_description = "Journal"
@@ -3399,7 +3398,7 @@ class wizard_multi_charts_accounts(osv.osv_memory):
obj_wizard = self.browse(cr, uid, ids[0])
company_id = obj_wizard.company_id.id
self.pool.get('res.company').write(cr, uid, [company_id], {'currency_id': obj_wizard.currency_id.id})
self.pool.get('res.company').write(cr, uid, [company_id], {'currency_id': obj_wizard.currency_id.id, 'accounts_code_digits': obj_wizard.code_digits}, context=context)
# When we install the CoA of first company, set the currency to price types and pricelists
if company_id==1:
@@ -3429,80 +3428,90 @@ class wizard_multi_charts_accounts(osv.osv_memory):
self._create_bank_journals_from_o2m(cr, uid, obj_wizard, company_id, acc_template_ref, context=context)
return {}
def _prepare_bank_journal(self, cr, uid, line, current_num, default_account_id, company_id, context=None):
def _prepare_bank_journal(self, cr, uid, company, line, default_account_id, context=None):
'''
This function prepares the value to use for the creation of a bank journal created through the wizard of
generating COA from templates.
:param line: dictionary containing the values encoded by the user related to his bank account
:param current_num: integer corresponding to a counter of the already created bank journals through this wizard.
:param default_account_id: id of the default debit.credit account created before for this journal.
:param company_id: id of the company for which the wizard is running
:return: mapping of field names and values
:rtype: dict
'''
obj_data = self.pool.get('ir.model.data')
obj_journal = self.pool.get('account.journal')
# we need to loop again to find next number for journal code
# because we can't rely on the value current_num as,
# its possible that we already have bank journals created (e.g. by the creation of res.partner.bank)
# and the next number for account code might have been already used before for journal
for num in xrange(current_num, 100):
# we need to loop to find next number for journal code
for num in xrange(1, 100):
# journal_code has a maximal size of 5, hence we can enforce the boundary num < 100
journal_code = _('BNK')[:3] + str(num)
ids = obj_journal.search(cr, uid, [('code', '=', journal_code), ('company_id', '=', company_id)], context=context)
ids = obj_journal.search(cr, uid, [('code', '=', journal_code), ('company_id', '=', company.id)], context=context)
if not ids:
break
else:
raise osv.except_osv(_('Error!'), _('Cannot generate an unused journal code.'))
vals = {
return {
'name': line['acc_name'],
'code': journal_code,
'type': line['account_type'] == 'cash' and 'cash' or 'bank',
'company_id': company_id,
'company_id': company.id,
'analytic_journal_id': False,
'currency': False,
'currency': line['currency_id'] or False,
'default_credit_account_id': default_account_id,
'default_debit_account_id': default_account_id,
}
if line['currency_id']:
vals['currency'] = line['currency_id']
return vals
}
def _prepare_bank_account(self, cr, uid, line, new_code, acc_template_ref, ref_acc_bank, company_id, context=None):
def _prepare_bank_account(self, cr, uid, company, line, acc_template_ref=False, ref_acc_bank=False, context=None):
'''
This function prepares the value to use for the creation of the default debit and credit accounts of a
bank journal created through the wizard of generating COA from templates.
:param company: company for which the wizard is running
:param line: dictionary containing the values encoded by the user related to his bank account
:param new_code: integer corresponding to the next available number to use as account code
:param acc_template_ref: the dictionary containing the mapping between the ids of account templates and the ids
of the accounts that have been generated from them.
:param ref_acc_bank: browse record of the account template set as root of all bank accounts for the chosen
template
:param company_id: id of the company for which the wizard is running
:return: mapping of field names and values
:rtype: dict
'''
obj_data = self.pool.get('ir.model.data')
obj_acc = self.pool.get('account.account')
# Seek the next available number for the account code
code_digits = company.accounts_code_digits or 0
bank_account_code_char = company.bank_account_code_char or ''
for num in xrange(1, 100):
new_code = str(bank_account_code_char.ljust(code_digits - 1, '0')) + str(num)
ids = obj_acc.search(cr, uid, [('code', '=', new_code), ('company_id', '=', company.id)])
if not ids:
break
else:
raise osv.except_osv(_('Error!'), _('Cannot generate an unused account code.'))
# Get the id of the user types fr-or cash and bank
tmp = obj_data.get_object_reference(cr, uid, 'account', 'data_account_type_cash')
cash_type = tmp and tmp[1] or False
tmp = obj_data.get_object_reference(cr, uid, 'account', 'data_account_type_bank')
bank_type = tmp and tmp[1] or False
parent_id = False
if acc_template_ref:
parent_id = acc_template_ref[ref_acc_bank.id]
else:
tmp = self.pool.get('account.account').search(cr, uid, [('code', '=', company.bank_account_code_char)], context=context)
if tmp:
parent_id = tmp[0]
return {
'name': line['acc_name'],
'currency_id': line['currency_id'],
'currency_id': line['currency_id'] or False,
'code': new_code,
'type': 'liquidity',
'user_type': line['account_type'] == 'cash' and cash_type or bank_type,
'parent_id': acc_template_ref[ref_acc_bank.id] or False,
'company_id': company_id,
# TODO: refactor when parent_id removed from account.account
'parent_id': parent_id,
'company_id': company.id,
}
def _create_bank_journals_from_o2m(self, cr, uid, obj_wizard, company_id, acc_template_ref, context=None):
@@ -3518,7 +3527,7 @@ class wizard_multi_charts_accounts(osv.osv_memory):
'''
obj_acc = self.pool.get('account.account')
obj_journal = self.pool.get('account.journal')
code_digits = obj_wizard.code_digits
company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
# Build a list with all the data to process
journal_data = []
@@ -3530,28 +3539,20 @@ class wizard_multi_charts_accounts(osv.osv_memory):
'currency_id': acc.currency_id.id,
}
journal_data.append(vals)
ref_acc_bank = obj_wizard.chart_template_id.bank_account_view_id
if journal_data and not ref_acc_bank.code:
raise osv.except_osv(_('Configuration Error!'), _('You have to set a code for the bank account defined on the selected chart of accounts.'))
self.pool.get('res.company').write(cr, uid, [company.id], {'bank_account_code_char': ref_acc_bank.code}, context=context)
current_num = 1
for line in journal_data:
# Seek the next available number for the account code
while True:
new_code = str(ref_acc_bank.code.ljust(code_digits-len(str(current_num)), '0')) + str(current_num)
ids = obj_acc.search(cr, uid, [('code', '=', new_code), ('company_id', '=', company_id)])
if not ids:
break
else:
current_num += 1
# Create the default debit/credit accounts for this bank journal
vals = self._prepare_bank_account(cr, uid, line, new_code, acc_template_ref, ref_acc_bank, company_id, context=context)
vals = self._prepare_bank_account(cr, uid, company, line, acc_template_ref, ref_acc_bank, context=context)
default_account_id = obj_acc.create(cr, uid, vals, context=context)
#create the bank journal
vals_journal = self._prepare_bank_journal(cr, uid, line, current_num, default_account_id, company_id, context=context)
vals_journal = self._prepare_bank_journal(cr, uid, company, line, default_account_id, context=context)
obj_journal.create(cr, uid, vals_journal)
current_num += 1
return True
+3
View File
@@ -25,6 +25,8 @@ class res_company(osv.osv):
_inherit = "res.company"
_columns = {
'expects_chart_of_accounts': fields.boolean('Expects a Chart of Accounts'),
'bank_account_code_char': fields.char('Code of the main bank account'),
'accounts_code_digits': fields.integer('Number of digits in an account code'),
'tax_calculation_rounding_method': fields.selection([
('round_per_line', 'Round per Line'),
('round_globally', 'Round Globally'),
@@ -37,6 +39,7 @@ class res_company(osv.osv):
_defaults = {
'expects_chart_of_accounts': True,
'tax_calculation_rounding_method': 'round_per_line',
'accounts_code_digits': 6,
'overdue_msg': '''Dear Sir/Madam,
Our records indicate that some payments on your account are still due. Please find details below.
+4
View File
@@ -12,6 +12,10 @@
<field name="overdue_msg" nolabel="1" colspan ="4"/>
</page>
</notebook>
<xpath expr="//group[@name='account_grp']" position="inside">
<field name="bank_account_code_char"/>
<field name="accounts_code_digits"/>
</xpath>
</field>
</record>
+4
View File
@@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<record id="base.main_company" model="res.company">
<field name="bank_account_code_char">X1100</field>
<field name="accounts_code_digits">7</field>
</record>
<!--
Account Type
-->
@@ -55,6 +55,14 @@
top: 2px;
z-index: 10;
text-shadow: -1px -1px 0 #f5f5f5, 1px -1px 0 #f5f5f5, -1px 1px 0 #f5f5f5, 1px 1px 0 #f5f5f5; }
.openerp .oe_bank_statement_reconciliation .notification_area {
margin: 0 15px; }
.openerp .oe_bank_statement_reconciliation .interface_options {
margin-bottom: 25px;
padding-left: 15px; }
.openerp .oe_bank_statement_reconciliation .interface_options .oe_radio_input {
border: none;
display: inline; }
.openerp .oe_bank_statement_reconciliation .protip {
margin: 0;
position: absolute;
@@ -91,6 +91,20 @@ $aestetic_animation_speed: 300ms;
}
}
.notification_area {
margin: 0 $actionColWidth;
}
.interface_options {
margin-bottom: 25px;
padding-left: $actionColWidth;
.oe_radio_input {
border: none;
display: inline;
}
}
.protip {
margin: 0;
position: absolute;
@@ -11,6 +11,7 @@ openerp.account = function (instance) {
className: 'oe_bank_statement_reconciliation',
events: {
"click *[rel='do_action']": "doActionClickHandler",
"click .statement_name span": "statementNameClickHandler",
"keyup .change_statement_name_field": "changeStatementNameFieldHandler",
"click .change_statement_name_button": "changeStatementButtonClickHandler",
@@ -24,6 +25,7 @@ openerp.account = function (instance) {
this.single_statement = this.statement_ids !== undefined && this.statement_ids.length === 1;
this.multiple_statements = this.statement_ids !== undefined && this.statement_ids.length > 1;
this.title = context.context.title || _t("Reconciliation");
this.notifications = context.context.notifications || [];
this.st_lines = [];
this.last_displayed_reconciliation_index = undefined; // Flow control
this.reconciled_lines = 0; // idem
@@ -249,11 +251,41 @@ openerp.account = function (instance) {
$.when.apply($, child_promises).then(function(){
self.$(".reconciliation_lines_container").animate({opacity: 1}, self.aestetic_animation_speed);
self.getChildren()[0].set("mode", "match");
if (self.notifications) {
self.displayNotifications(self.notifications);
}
});
});
});
},
doActionClickHandler: function(e) {
var name = e.currentTarget.dataset.action_nam;
var model = e.currentTarget.dataset.model;
var ids = e.currentTarget.dataset.ids.split(",").map(Number);
this.action_manager.do_action({
name: name,
res_model: model,
domain: [['id', 'in', ids]],
views: [[false, 'list'], [false, 'form']],
type: 'ir.actions.act_window',
view_type: "list",
view_mode: "list"
});
},
displayNotifications: function(notifications, speed) {
speed = speed === undefined ? this.aestetic_animation_speed : speed;
for (var i=0; i<notifications.length; i++) {
var $notification = $(QWeb.render("reconciliation_notification", {
type: notifications[i].type,
message: notifications[i].message,
details: notifications[i].details,
})).hide();
$notification.appendTo(this.$(".notification_area")).slideDown(speed);
}
},
statementNameClickHandler: function() {
if (! this.single_statement) return;
this.$(".statement_name span").hide();
@@ -4,6 +4,7 @@
<t t-name="bank_statement_nothing_to_reconcile">
<div class="oe_form_sheetbg"><div class="oe_form_sheet oe_form_sheet_width">
<div class="notification_area"></div>
<div class="oe_view_nocontent"><p>
<b>Good Job!</b> Bank statements are fully reconciled.</p>
<p>This page displays all the bank transactions that are to be reconciled and provides with a neat interface to do so.</p>
@@ -31,6 +32,7 @@
</div>
</div>
<div class="oe_clear"></div>
<div class="notification_area"></div>
<div class="reconciliation_lines_container"></div>
<p class='protip'>Tip : Hit ctrl-enter to validate the whole sheet.</p>
</div></div>
@@ -57,6 +59,19 @@
</div>
</t>
<t t-name="reconciliation_notification">
<div t-att-class="'notification alert-dismissible alert alert-' + type" role="alert">
<button type="button" class="close" data-dismiss="alert"><span aria-hidden="true" class="glyphicon glyphicon-remove"></span><span class="sr-only">Close</span></button>
<t t-esc="message" />
<t t-if="details !== undefined">
<a class="oe_e" style="line-height: 14px; position: relative; top: 3px" rel="do_action" href="javascript:void(0)"
t-att-data-action_nam="details.name"
t-att-data-model="details.model"
t-att-data-ids="details.ids">/</a>
</t>
</div>
</t>
<t t-name="bank_statement_reconciliation_line">
<table t-att-data-mode="mode">
<tr>
@@ -1,6 +1,7 @@
# -*- encoding: utf-8 -*-
{
'name': 'Account Bank Statement Import',
'category' : 'Accounting & Finance',
'version': '1.0',
'author': 'OpenERP SA',
'depends': ['account'],
@@ -1,94 +1,271 @@
# -*- coding: utf-8 -*-
import base64
from openerp import SUPERUSER_ID
from openerp.osv import fields, osv
from openerp.tools.translate import _
import logging
_logger = logging.getLogger(__name__)
_IMPORT_FILE_TYPE = [('none', _('No Import Format Available'))]
def add_file_type(selection_value):
global _IMPORT_FILE_TYPE
if _IMPORT_FILE_TYPE[0][0] == 'none':
_IMPORT_FILE_TYPE = [selection_value]
else:
_IMPORT_FILE_TYPE.append(selection_value)
class account_bank_statement_line(osv.osv):
_inherit = "account.bank.statement.line"
_columns = {
# Ensure transactions can be imported only once (if the import format provides unique transaction ids)
'unique_import_id': fields.char('Import ID', readonly=True, copy=False),
}
_sql_constraints = [
('unique_import_id', 'unique (unique_import_id)', 'A bank account transactions can be imported only once !')
]
class account_bank_statement_import(osv.TransientModel):
_name = 'account.bank.statement.import'
_description = 'Import Bank Statement'
def _get_import_file_type(self, cr, uid, context=None):
return _IMPORT_FILE_TYPE
_columns = {
'data_file': fields.binary('Bank Statement File', required=True, help='Get you bank statements in electronic format from your bank and select them here.'),
'file_type': fields.selection(_get_import_file_type, 'File Type', required=True),
'journal_id': fields.many2one('account.journal', 'Journal', required=True, help="The journal for which the bank statements will be created"),
}
def _get_first_file_type(self, cr, uid, context=None):
return self._get_import_file_type(cr, uid, context=context)[0][0]
def import_file(self, cr, uid, ids, context=None):
""" Process the file chosen in the wizard, create bank statement(s) and go to reconciliation. """
if context is None:
context = {}
#set the active_id in the context, so that any extension module could
#reuse the fields chosen in the wizard if needed (see .QIF for example)
context.update({'active_id': ids[0]})
def _get_default_journal(self, cr, uid, context=None):
company_id = self.pool.get('res.company')._company_default_get(cr, uid, 'account.bank.statement', context=context)
journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'bank'), ('company_id', '=', company_id)], context=context)
return journal_ids and journal_ids[0] or False
data_file = self.browse(cr, uid, ids[0], context=context).data_file
_defaults = {
'file_type': _get_first_file_type,
'journal_id': _get_default_journal,
}
# The appropriate implementation module returns the required data
currency_code, account_number, stmts_vals = self._parse_file(cr, uid, base64.b64decode(data_file), context=context)
# Check raw data
self._check_parsed_data(cr, uid, stmts_vals, context=context)
# Try to find the bank account and currency in odoo
currency_id, bank_account_id = self._find_additional_data(cr, uid, currency_code, account_number, context=context)
# Find or create the bank journal
journal_id = self._get_journal(cr, uid, currency_id, bank_account_id, account_number, context=context)
# Create the bank account if not already existing
if not bank_account_id and account_number:
self._create_bank_account(cr, uid, account_number, journal_id=journal_id, partner_id=uid, context=context)
# Prepare statement data to be used for bank statements creation
stmts_vals = self._complete_stmts_vals(cr, uid, stmts_vals, journal_id, account_number, context=context)
# Create the bank statements
statement_ids, notifications = self._create_bank_statements(cr, uid, stmts_vals, context=context)
def _detect_partner(self, cr, uid, identifying_string, identifying_field='acc_number', context=None):
"""Try to find a bank account and its related partner for the given 'identifying_string', looking on the field 'identifying_field'.
# Finally dispatch to reconciliation interface
model, action_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'action_bank_reconcile_bank_statements')
action = self.pool[model].browse(cr, uid, action_id, context=context)
return {
'name': action.name,
'tag': action.tag,
'context': {
'statement_ids': statement_ids,
'notifications': notifications
},
'type': 'ir.actions.client',
}
:param identifying_string: varchar
:param identifying_field: varchar corresponding to the name of a field of res.partner.bank
:returns: tuple(ID of the bank account found or False, ID of the partner for the bank account found or False)
def _parse_file(self, cr, uid, data_file=None, context=None):
""" Each module adding a file support must extends this method. It processes the file if it can, returns super otherwise, resulting in a chain of responsability.
This method parses the given file and returns the data required by the bank statement import process, as specified below.
rtype: triplet (if a value can't be retrieved, use None)
- currency code: string (e.g: 'EUR')
The ISO 4217 currency code, case insensitive
- account number: string (e.g: 'BE1234567890')
The number of the bank account which the statement belongs to
- bank statements data: list of dict containing (optional items marked by o) :
- 'name': string (e.g: '000000123')
- 'date': date (e.g: 2013-06-26)
-o 'balance_start': float (e.g: 8368.56)
-o 'balance_end_real': float (e.g: 8888.88)
- 'transactions': list of dict containing :
- 'name': string (e.g: 'KBC-INVESTERINGSKREDIET 787-5562831-01')
- 'date': date
- 'amount': float
- 'unique_import_id': string
-o 'account_number': string
Will be used to find/create the res.partner.bank in odoo
-o 'note': string
-o 'partner_name': string
-o 'ref': string
"""
partner_id = False
bank_account_id = False
if identifying_string:
ids = self.pool.get('res.partner.bank').search(cr, uid, [(identifying_field, '=', identifying_string)], context=context)
if ids:
bank_account_id = ids[0]
partner_id = self.pool.get('res.partner.bank').browse(cr, uid, bank_account_id, context=context).partner_id.id
raise osv.except_osv(_('Error'), _('Could not make sense of the given file.\nDid you install the module to support this type of file ?'))
def _check_parsed_data(self, cr, uid, stmts_vals, context=None):
""" Basic and structural verifications """
if len(stmts_vals) == 0:
raise osv.except_osv(_('Error'), _('This file doesn\'t contain any statement.'))
no_st_line = True
for vals in stmts_vals:
if vals['transactions'] and len(vals['transactions']) > 0:
no_st_line = False
break
if no_st_line:
raise osv.except_osv(_('Error'), _('This file doesn\'t contain any transaction.'))
def _find_additional_data(self, cr, uid, currency_code, account_number, context=None):
""" Get the res.currency ID and the res.partner.bank ID """
currency_id = False # So if no currency_code is provided, we'll use the company currency
if currency_code:
currency_ids = self.pool.get('res.currency').search(cr, uid, [('name', '=ilike', currency_code)], context=context)
company_currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
if currency_ids:
if currency_ids[0] != company_currency_id:
currency_id = currency_ids[0]
bank_account_id = None
if account_number and len(account_number) > 4:
account_number = account_number.replace(' ', '').replace('-', '')
cr.execute("select id from res_partner_bank where replace(replace(acc_number,' ',''),'-','') = %s", (account_number,))
bank_account_ids = [id[0] for id in cr.fetchall()]
bank_account_ids = self.pool.get('res.partner.bank').search(cr, uid, [('id', 'in', bank_account_ids)], context=context)
if bank_account_ids:
bank_account_id = bank_account_ids[0]
return currency_id, bank_account_id
def _get_journal(self, cr, uid, currency_id, bank_account_id, account_number, context=None):
""" Find or create the journal """
bank_pool = self.pool.get('res.partner.bank')
# Find the journal from context or bank account
journal_id = context.get('journal_id')
if bank_account_id:
bank_account = bank_pool.browse(cr, uid, bank_account_id, context=context)
if journal_id:
if bank_account.journal_id.id and bank_account.journal_id.id != journal_id:
raise osv.except_osv(_('Error'), _('The account of this statement is linked to another journal.'))
if not bank_account.journal_id.id:
bank_pool.write(cr, uid, [bank_account_id], {'journal_id': journal_id}, context=context)
else:
#create the bank account, not linked to any partner. The reconciliation will link the partner manually
#chosen at the bank statement final confirmation time.
try:
type_model, type_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'bank_normal')
type_id = self.pool.get('res.partner.bank.type').browse(cr, uid, type_id, context=context)
bank_code = type_id.code
except ValueError:
bank_code = 'bank'
acc_number = identifying_field == 'acc_number' and identifying_string or _('Undefined')
bank_account_vals = {
'acc_number': acc_number,
'state': bank_code,
}
bank_account_vals[identifying_field] = identifying_string
bank_account_id = self.pool.get('res.partner.bank').create(cr, uid, bank_account_vals, context=context)
return bank_account_id, partner_id
if bank_account.journal_id.id:
journal_id = bank_account.journal_id.id
def import_bank_statement(self, cr, uid, bank_statement_vals=False, context=None):
""" Get a list of values to pass to the create() of account.bank.statement object, and returns a list of ID created using those values"""
# If importing into an existing journal, its currency must be the same as the bank statement
if journal_id:
journal_currency_id = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context).currency.id
if currency_id and currency_id != journal_currency_id:
raise osv.except_osv(_('Error'), _('The currency of the bank statement is not the same as the currency of the journal !'))
# If there is no journal, create one (and its account)
if not journal_id and account_number:
journal_id = self._create_journal(cr, uid, currency_id, account_number, context=context)
if bank_account_id:
bank_pool.write(cr, uid, [bank_account_id], {'journal_id': journal_id}, context=context)
# If we couldn't find/create a journal, everything is lost
if not journal_id:
raise osv.except_osv(_('Error'), _('Cannot find in which journal import this statement. Please manually select a journal.'))
return journal_id
def _create_journal(self, cr, uid, currency_id, account_number, context=None):
""" Create a journal and its account """
wmca_pool = self.pool.get('wizard.multi.charts.accounts')
company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
vals_account = {'currency_id': currency_id, 'acc_name': account_number, 'account_type': 'bank', 'currency_id': currency_id}
vals_account = wmca_pool._prepare_bank_account(cr, uid, company, vals_account, context=context)
account_id = self.pool.get('account.account').create(cr, uid, vals_account, context=context)
vals_journal = {'currency_id': currency_id, 'acc_name': _('Bank') + ' ' + account_number, 'account_type': 'bank'}
vals_journal = wmca_pool._prepare_bank_journal(cr, uid, company, vals_journal, account_id, context=context)
return self.pool.get('account.journal').create(cr, uid, vals_journal, context=context)
def _create_bank_account(self, cr, uid, account_number, journal_id=False, partner_id=False, context=None):
try:
type_model, type_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'bank_normal')
type_id = self.pool.get('res.partner.bank.type').browse(cr, uid, type_id, context=context)
bank_code = type_id.code
except ValueError:
bank_code = 'bank'
account_number = account_number.replace(' ', '').replace('-', '')
vals_acc = {
'acc_number': account_number,
'state': bank_code,
}
# Odoo users bank accounts (which we import statement from) have company_id and journal_id set
# while 'counterpart' bank accounts (from which statement transactions originate) don't.
# Warning : if company_id is set, the method post_write of class bank will create a journal
if journal_id:
vals_acc['partner_id'] = uid
vals_acc['journal_id'] = journal_id
vals_acc['company_id'] = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
return self.pool.get('res.partner.bank').create(cr, uid, vals_acc, context=context)
def _complete_stmts_vals(self, cr, uid, stmts_vals, journal_id, account_number, context=None):
for st_vals in stmts_vals:
st_vals['journal_id'] = journal_id
for line_vals in st_vals['transactions']:
unique_import_id = line_vals.get('unique_import_id', False)
if unique_import_id:
line_vals['unique_import_id'] = (account_number and account_number + '-' or '') + unique_import_id
if not 'bank_account_id' in line_vals or not line_vals['bank_account_id']:
# Find the partner and his bank account or create the bank account. The partner selected during the
# reconciliation process will be linked to the bank when the statement is closed.
partner_id = False
bank_account_id = False
identifying_string = line_vals.get('account_number', False)
if identifying_string:
ids = self.pool.get('res.partner.bank').search(cr, uid, [('acc_number', '=', identifying_string)], context=context)
if ids:
bank_account_id = ids[0]
partner_id = self.pool.get('res.partner.bank').browse(cr, uid, bank_account_id, context=context).partner_id.id
else:
bank_account_id = self._create_bank_account(cr, uid, identifying_string, context=context)
line_vals['partner_id'] = partner_id
line_vals['bank_account_id'] = bank_account_id
return stmts_vals
def _create_bank_statements(self, cr, uid, stmts_vals, context=None):
""" Create new bank statements from imported values, filtering out already imported transactions, and returns data used by the reconciliation widget """
bs_obj = self.pool.get('account.bank.statement')
bsl_obj = self.pool.get('account.bank.statement.line')
# Filter out already imported transactions and create statements
statement_ids = []
for vals in bank_statement_vals:
statement_ids.append(self.pool.get('account.bank.statement').create(cr, uid, vals, context=context))
return statement_ids
ignored_statement_lines_import_ids = []
for st_vals in stmts_vals:
filtered_st_lines = []
for line_vals in st_vals['transactions']:
if not 'unique_import_id' in line_vals \
or not line_vals['unique_import_id'] \
or not bool(bsl_obj.search(cr, SUPERUSER_ID, [('unique_import_id', '=', line_vals['unique_import_id'])], limit=1, context=context)):
filtered_st_lines.append(line_vals)
else:
ignored_statement_lines_import_ids.append(line_vals['unique_import_id'])
if len(filtered_st_lines) > 0:
# Remove values that won't be used to create records
st_vals.pop('transactions', None)
for line_vals in filtered_st_lines:
line_vals.pop('account_number', None)
# Create the satement
st_vals['line_ids'] = [[0, False, line] for line in filtered_st_lines]
statement_ids.append(bs_obj.create(cr, uid, st_vals, context=context))
if len(statement_ids) == 0:
raise osv.except_osv(_('Error'), _('You have already imported that file.'))
def process_none(self, cr, uid, data_file, journal_id=False, context=None):
raise osv.except_osv(_('Error'), _('No available format for importing bank statement. You can install one of the file format available through the module installation.'))
# Prepare import feedback
notifications = []
num_ignored = len(ignored_statement_lines_import_ids)
if num_ignored > 0:
notifications += [{
'type': 'warning',
'message': _("%d transactions had already been imported and were ignored.") % num_ignored if num_ignored > 1 else _("1 transaction had already been imported and was ignored."),
'details': {
'name': _('Already imported items'),
'model': 'account.bank.statement.line',
'ids': bsl_obj.search(cr, uid, [('unique_import_id', 'in', ignored_statement_lines_import_ids)], context=context)
}
}]
def parse_file(self, cr, uid, ids, context=None):
""" Process the file chosen in the wizard and returns a list view of the imported bank statements"""
data = self.browse(cr, uid, ids[0], context=context)
vals = getattr(self, "process_%s" % data.file_type)(cr, uid, data.data_file, data.journal_id.id, context=context)
statement_ids = self.import_bank_statement(cr, uid, vals, context=context)
model, action_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'action_bank_statement_tree')
action = self.pool[model].read(cr, uid, action_id, context=context)
action['domain'] = "[('id', 'in', [" + ', '.join(map(str, statement_ids)) + "])]"
return action
return statement_ids, notifications
@@ -7,22 +7,14 @@
<field name="model">account.bank.statement.import</field>
<field name="priority">1</field>
<field name="arch" type="xml">
<form string="Import Bank Statements" version="7.0">
<group>
<group>
<field name="data_file"/>
<field name="file_type"/>
<field name="journal_id" domain="[('type', '=', 'bank')]" context="{'default_type':'bank'}"/>
</group>
<group>
<b colspan="2"> How to import your bank statement in OpenERP.</b>
<label string= "1. Go to your bank account website." colspan="2"/>
<label string= "2. Download your bank statements in the right format. (.OFX, .QIF or CODA are accepted)" colspan="2"/>
<label string= "3. Upload right here the bank statements file into OpenERP. Click Import." colspan="2"/>
</group>
</group>
<form string="Import Bank Statements">
<field name="data_file"/>
<br/><br/><b> How to import your bank statement :</b>
<br/><label string= "1. Download your bank statements from your bank website."/>
<br/><label string= "2. Make sure you have installed the right module to support the file format."/>
<br/><label string= "3. Select the file and click 'Import'."/>
<footer>
<button name="parse_file" string="_Import" type="object" class="oe_highlight"/>
<button name="import_file" string="_Import" type="object" class="oe_highlight"/>
or
<button string="Cancel" class="oe_link" special="cancel"/>
</footer>
@@ -0,0 +1,178 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
enable-background="new 0 0 100 100"
height="100px"
id="Layer_1"
version="1.1"
viewBox="0 0 100 100"
width="100px"
xml:space="preserve"
inkscape:version="0.48.2 r9819"
sodipodi:docname="1409271720_Noun_Project_100Icon_10px_grid-17.svg"
inkscape:export-filename="/Users/arthurmaniet/Desktop/icon.png"
inkscape:export-xdpi="115.2"
inkscape:export-ydpi="115.2"><metadata
id="metadata9"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs7" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1733"
inkscape:window-height="1001"
id="namedview5"
showgrid="false"
inkscape:zoom="11.62"
inkscape:cx="21.99675"
inkscape:cy="56.127828"
inkscape:window-x="76"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="Layer_1" /><path
d="M79.043,31.615l-5.742,5.742V13h-58v74h58V48.67l11.398-11.399L79.043,31.615z M71.301,39.357L50.758,59.898l-1.414,4.242 l-1.414,4.244l8.486-2.828L71.301,50.67V85h-54V15h54V39.357z M54.564,65.119l-3.182,1.06l-1.248-1.248l1.061-3.182l3.1,3.099 L54.564,65.119z"
id="path3" /><text
xml:space="preserve"
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
x="18.006462"
y="17.887218"
id="text2986"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
x="18.006462"
y="17.887218"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3520">08/12/13 1000.00 Delta PC</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="21.637218"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3731">08/15/13 75.46 Walts Drugs</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="25.387218"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3733">03/03/13 379.00 Epic Technologies</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="29.137218"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3735">03/04/13 20.28 YOUR LOCAL SU</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="32.887218"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3737">03/03/13 421.35 SPRINGFIELD WA</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="36.637218"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3739">03/03/13 379.00 Epic Technologies</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="40.387218"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3743">03/04/13 20.28 YOUR LOCAL SUP</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="44.137218"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3846">08/15/13 75.46 Walts Drugs</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="47.887218"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3745">08/12/13 1000.00 Delta PC</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="51.637218"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3747">03/03/13 421.35 SPRINGFIELD WA</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="55.387218"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3749">03/04/13 20.28 YOUR LOCAL SU</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="59.137218"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3751">03/03/13 379.00 Epic Technologies</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="62.887218"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3753">08/12/13 1000.00 De a PC</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="66.637222"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3755">03/03/13 379.00 E Technologies</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="70.387222"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3757">08/15/13 75.46 Walts Drugs</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="74.137222"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3759">03/04/13 20.28 YOUR LOCAL SU</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="77.887222"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3761">03/03/13 379.00 Epic Technologies</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="81.637222"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3763">08/12/13 1000.00 Delta PC</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="85.387222"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3765">08/15/13 75.46 Walts Drugs</tspan><tspan
sodipodi:role="line"
x="18.006462"
y="89.137222"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3783" /><tspan
sodipodi:role="line"
x="18.006462"
y="92.887222"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3799" /><tspan
sodipodi:role="line"
x="18.006462"
y="96.637222"
style="font-size:3px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold"
id="tspan3801" /></text>
<text
xml:space="preserve"
style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
x="43.851177"
y="32.13871"
id="text3838"
sodipodi:linespacing="125%"
inkscape:export-filename="/Users/arthurmaniet/Desktop/icon.png"
inkscape:export-xdpi="115.2"
inkscape:export-ydpi="115.2"><tspan
sodipodi:role="line"
id="tspan3840"
x="43.851177"
y="32.13871"
style="font-size:16px;font-weight:bold;text-align:center;text-anchor:middle;-inkscape-font-specification:Sans Bold" /></text>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

@@ -1,6 +1,7 @@
# -*- encoding: utf-8 -*-
{
'name': 'Import OFX Bank Statement',
'category' : 'Accounting & Finance',
'version': '1.0',
'author': 'OpenERP SA',
'depends': ['account_bank_statement_import'],
@@ -17,7 +18,8 @@ creation of the Financial Accounting records).
""",
'data' : [],
'depends': ['account_bank_statement_import'],
'demo': [],
'auto_install': False,
'auto_install': True,
'installable': True,
}
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
import logging
import base64
import os
from openerp.osv import osv
@@ -9,64 +8,70 @@ from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
from openerp.addons.account_bank_statement_import import account_bank_statement_import as ibs
ibs.add_file_type(('ofx', 'OFX'))
try:
from ofxparse import OfxParser as ofxparser
except ImportError:
_logger.warning("OFX parser unavailable because the `ofxparse` Python library cannot be found."
_logger.error("OFX parser unavailable because the `ofxparse` Python library cannot be found."
"It can be downloaded and installed from `https://pypi.python.org/pypi/ofxparse`.")
ofxparser = None
class account_bank_statement_import(osv.TransientModel):
_inherit = 'account.bank.statement.import'
def process_ofx(self, cr, uid, data_file, journal_id=False, context=None):
""" Import a file in the .OFX format"""
def _check_ofx(self, cr, uid, file, context=None):
if ofxparser is None:
raise osv.except_osv(_("Error"), _("OFX parser unavailable because the `ofxparse` Python library cannot be found."
"It can be downloaded and installed from `https://pypi.python.org/pypi/ofxparse`."))
return False
try:
ofx = ofxparser.parse(file)
except:
return False
return ofx
def _parse_file(self, cr, uid, data_file=None, context=None):
try:
tempfile = open("temp.ofx", "w+")
tempfile.write(base64.decodestring(data_file))
tempfile.write(data_file)
tempfile.read()
pathname = os.path.dirname('temp.ofx')
path = os.path.join(os.path.abspath(pathname), 'temp.ofx')
ofx = ofxparser.parse(file(path))
except:
raise osv.except_osv(_('Import Error!'), _('Please check OFX file format is proper or not.'))
line_ids = []
raise osv.except_osv(_('Import Error!'), _('File handling error.'))
ofx = self._check_ofx(cr, uid, file(path), context=context)
if not ofx:
return super(account_bank_statement_import, self)._parse_file(cr, uid, data_file, context=context)
transactions = []
total_amt = 0.00
try:
for transaction in ofx.account.statement.transactions:
bank_account_id, partner_id = self._detect_partner(cr, uid, transaction.payee, identifying_field='owner_name', context=context)
# Since ofxparse doesn't provide account numbers, we'll have to find res.partner and res.partner.bank here
# (normal behavious is to provide 'account_number', which the generic module uses to find partner/bank)
bank_account_id = partner_id = False
ids = self.pool.get('res.partner.bank').search(cr, uid, [('owner_name', '=', transaction.payee)], context=context)
if ids:
bank_account_id = bank_account_id = ids[0]
partner_id = self.pool.get('res.partner.bank').browse(cr, uid, bank_account_id, context=context).partner_id.id
vals_line = {
'date': transaction.date,
'name': transaction.payee + ': ' + transaction.memo,
'name': transaction.payee + (transaction.memo and ': ' + transaction.memo or ''),
'ref': transaction.id,
'amount': transaction.amount,
'partner_id': partner_id,
'unique_import_id': transaction.id,
'bank_account_id': bank_account_id,
'partner_id': partner_id,
}
total_amt += float(transaction.amount)
line_ids.append((0, 0, vals_line))
transactions.append(vals_line)
except Exception, e:
raise osv.except_osv(_('Error!'), _("Following problem has been occurred while importing your file, Please verify the file is proper or not.\n\n %s" % e.message))
st_start_date = ofx.account.statement.start_date or False
st_end_date = ofx.account.statement.end_date or False
period_obj = self.pool.get('account.period')
if st_end_date:
period_ids = period_obj.find(cr, uid, st_end_date, context=context)
else:
period_ids = period_obj.find(cr, uid, st_start_date, context=context)
os.remove(path)
raise osv.except_osv(_('Error!'), _("The following problem occurred during import. The file might not be valid.\n\n %s" % e.message))
vals_bank_statement = {
'name': ofx.account.routing_number,
'transactions': transactions,
'balance_start': ofx.account.statement.balance,
'balance_end_real': float(ofx.account.statement.balance) + total_amt,
'period_id': period_ids and period_ids[0] or False,
'journal_id': journal_id
}
vals_bank_statement.update({'line_ids': line_ids})
os.remove(path)
return [vals_bank_statement]
return ofx.account.statement.currency, ofx.account.number, [vals_bank_statement]
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

@@ -20,10 +20,9 @@ class TestOfxFile(TransactionCase):
ofx_file_path = get_module_resource('account_bank_statement_import_ofx', 'test_ofx_file', 'test_ofx.ofx')
ofx_file = open(ofx_file_path, 'rb').read().encode('base64')
bank_statement_id = self.statement_import_model.create(cr, uid, dict(
file_type='ofx',
data_file=ofx_file,
))
self.statement_import_model.parse_file(cr, uid, [bank_statement_id])
data_file=ofx_file,
))
self.statement_import_model.import_file(cr, uid, [bank_statement_id])
statement_id = self.bank_statement_model.search(cr, uid, [('name', '=', '000000123')])[0]
bank_st_record = self.bank_statement_model.browse(cr, uid, statement_id)
self.assertEquals(bank_st_record.balance_start, 2156.56)
@@ -2,22 +2,25 @@
{
'name': 'Import QIF Bank Statement',
'category' : 'Accounting & Finance',
'version': '1.0',
'author': 'OpenERP SA',
'description': '''
Module to import QIF bank statements.
======================================
This module allows you to import the machine readable QIF Files in Odoo: they are parsed and stored in human readable format in
This module allows you to import the machine readable QIF Files in Odoo: they are parsed and stored in human readable format in
Accounting \ Bank and Cash \ Bank Statements.
Bank Statements may be generated containing a subset of the QIF information (only those transaction lines that are required for the
creation of the Financial Accounting records).
Important Note
---------------------------------------------
Because of the QIF format limitation, we cannot ensure the same transactions aren't imported several times or handle multicurrency.
Whenever possible, you should use a more appropriate file format like OFX.
''',
'images' : [],
'images': [],
'depends': ['account_bank_statement_import'],
'demo': [],
'data': [],
'data': ['account_bank_statement_import_qif_view.xml'],
'auto_install': False,
'installable': True,
}
@@ -1,24 +1,47 @@
# -*- coding: utf-8 -*-
import dateutil.parser
import base64
from tempfile import TemporaryFile
from openerp.tools.translate import _
from openerp.osv import osv
from openerp.addons.account_bank_statement_import import account_bank_statement_import as ibs
ibs.add_file_type(('qif', 'QIF'))
from openerp.osv import osv, fields
class account_bank_statement_import(osv.TransientModel):
_inherit = "account.bank.statement.import"
def process_qif(self, cr, uid, data_file, journal_id=False, context=None):
""" Import a file in the .QIF format"""
_columns = {
'journal_id': fields.many2one('account.journal', string='Journal', help='Accounting journal related to the bank statement you\'re importing. It has be be manually chosen for statement formats which doesn\'t allow automatic journal detection (QIF for example).'),
'hide_journal_field': fields.boolean('Hide the journal field in the view'),
}
def _get_hide_journal_field(self, cr, uid, context=None):
return context and 'journal_id' in context or False
_defaults = {
'hide_journal_field': _get_hide_journal_field,
}
def _get_journal(self, cr, uid, currency_id, bank_account_id, account_number, context=None):
""" As .QIF format does not allow us to detect the journal, we need to let the user choose it.
We set it in context before to call super so it's the same as calling the widget from a journal """
if context is None:
context = {}
if context.get('active_id'):
record = self.browse(cr, uid, context.get('active_id'), context=context)
if record.journal_id:
context['journal_id'] = record.journal_id.id
return super(account_bank_statement_import, self)._get_journal(cr, uid, currency_id, bank_account_id, account_number, context=context)
def _check_qif(self, cr, uid, data_file, context=None):
return data_file.strip().startswith('!Type:')
def _parse_file(self, cr, uid, data_file=None, context=None):
if not self._check_qif(cr, uid, data_file, context=context):
return super(account_bank_statement_import, self)._parse_file(cr, uid, data_file, context=context)
try:
fileobj = TemporaryFile('wb+')
fileobj.write(base64.b64decode(data_file))
fileobj.write(data_file)
fileobj.seek(0)
file_data = ""
for line in fileobj.readlines():
@@ -31,8 +54,8 @@ class account_bank_statement_import(osv.TransientModel):
header = data_list[0].strip()
header = header.split(":")[1]
except:
raise osv.except_osv(_('Import Error!'), _('Please check QIF file format is proper or not.'))
line_ids = []
raise osv.except_osv(_('Import Error!'), _('Could not decipher the QIF file.'))
transactions = []
vals_line = {}
total = 0
if header == "Bank":
@@ -43,31 +66,33 @@ class account_bank_statement_import(osv.TransientModel):
continue
if line[0] == 'D': # date of transaction
vals_line['date'] = dateutil.parser.parse(line[1:], fuzzy=True).date()
if vals_line.get('date') and not vals_bank_statement.get('period_id'):
period_ids = self.pool.get('account.period').find(cr, uid, vals_line['date'], context=context)
vals_bank_statement.update({'period_id': period_ids and period_ids[0] or False})
elif line[0] == 'T': # Total amount
total += float(line[1:].replace(',', ''))
vals_line['amount'] = float(line[1:].replace(',', ''))
elif line[0] == 'N': # Check number
vals_line['ref'] = line[1:]
elif line[0] == 'P': # Payee
bank_account_id, partner_id = self._detect_partner(cr, uid, line[1:], identifying_field='owner_name', context=context)
vals_line['partner_id'] = partner_id
vals_line['bank_account_id'] = bank_account_id
vals_line['name'] = 'name' in vals_line and line[1:] + ': ' + vals_line['name'] or line[1:]
# Since QIF doesn't provide account numbers, we'll have to find res.partner and res.partner.bank here
# (normal behavious is to provide 'account_number', which the generic module uses to find partner/bank)
ids = self.pool.get('res.partner.bank').search(cr, uid, [('owner_name', '=', line[1:])], context=context)
if ids:
vals_line['bank_account_id'] = bank_account_id = ids[0]
vals_line['partner_id'] = self.pool.get('res.partner.bank').browse(cr, uid, bank_account_id, context=context).partner_id.id
elif line[0] == 'M': # Memo
vals_line['name'] = 'name' in vals_line and vals_line['name'] + ': ' + line[1:] or line[1:]
elif line[0] == '^': # end of item
line_ids.append((0, 0, vals_line))
transactions.append(vals_line)
vals_line = {}
elif line[0] == '\n':
line_ids = []
transactions = []
else:
pass
else:
raise osv.except_osv(_('Error!'), _('Cannot support this Format !Type:%s.') % (header,))
vals_bank_statement.update({'balance_end_real': total,
'line_ids': line_ids,
'journal_id': journal_id})
return [vals_bank_statement]
raise osv.except_osv(_('Error!'), _('This file is either not a bank statement or is not correctly formed.'))
vals_bank_statement.update({
'balance_end_real': total,
'transactions': transactions
})
return None, None, [vals_bank_statement]
@@ -0,0 +1,24 @@
<?xml version="1.0" ?>
<openerp>
<data>
<record id="account_bank_statement_import_view_inherited" model="ir.ui.view">
<field name="name">Import Bank Statements Inherited</field>
<field name="model">account.bank.statement.import</field>
<field name="priority" eval="20"/>
<field name="inherit_id" ref="account_bank_statement_import.account_bank_statement_import_view" />
<field name="arch" type="xml">
<xpath expr="//field[@name='data_file']" position="after">
<field name="hide_journal_field" invisible="1"/>
<label for="journal_id"/>
<field name="journal_id"
domain="[('type', '=', 'bank')]"
attrs="{'invisible': [('hide_journal_field', '=', True)]}"
context="{'default_type':'bank'}"/>
</xpath>
</field>
</record>
</data>
</openerp>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

@@ -17,10 +17,12 @@ class TestQifFile(TransactionCase):
qif_file_path = get_module_resource('account_bank_statement_import_qif', 'test_qif_file', 'test_qif.qif')
qif_file = open(qif_file_path, 'rb').read().encode('base64')
bank_statement_id = self.statement_import_model.create(cr, uid, dict(
file_type='qif',
data_file=qif_file,
))
self.statement_import_model.parse_file(cr, uid, [bank_statement_id])
data_file=qif_file,
))
context = {
'journal_id': self.registry('ir.model.data').get_object_reference(cr, uid, 'account', 'bank_journal')[1]
}
self.statement_import_model.import_file(cr, uid, [bank_statement_id], context=context)
line_id = self.bank_statement_line_model.search(cr, uid, [('name', '=', 'YOUR LOCAL SUPERMARKET')])[0]
statement_id = self.bank_statement_line_model.browse(cr, uid, line_id).statement_id.id
bank_st_record = self.bank_statement_model.browse(cr, uid, statement_id)
@@ -13,12 +13,10 @@
-
I create currency USD in OpenERP for January of 1.333333 Rate
-
!python {model: res.currency.rate}: |
from datetime import datetime
curr_id = self.pool.get('res.currency').search(cr, uid, [('name', '=', 'USD')])[0]
date = '%s-01-01' %(datetime.now().year)
ids = self.search(cr, uid, [('currency_id','=',curr_id), ('name', '=', date)])
self.write(cr, uid, ids, {'rate': 1.333333})
!record {model: res.currency.rate, id: jan_usd}:
currency_id: base.USD
name: !eval "'%s-01-01' %(datetime.now().year)"
rate: 1.333333
-
I create currency USD in OpenERP for February of 1.250000 Rate
-
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

@@ -9,9 +9,9 @@
2300020000BE61310126985517 PARTNER 2 0 1
3100020001OL4414AC8BOVSOVSOVERS001500001001PARTNER 2 1 0
3200020001MOLENSTRAAT 60 9340 LEDE 0 0
2100030000AFECA0CVA IKLINNINNIG1000000000479040110111313410000 KBC-INVESTERINGSKREDIET 737-6543210-21 11011113510 0
2100030001AFECA0CVA IKLINNINNIG1000000000419920110111813410660 11011113500 0
2100030002AFECA0CVA IKLINNINNIG1000000000059120110111813410020 11011113510 0
2100030000AFECA0BIS IKLINNINBIS1000000000479040110111313410000 KBC-INVESTERINGSKREDIET 737-6543210-21 11011113510 0
2100030001AFECA0BIS IKLINNINBIS1000000000419920110111813410660 11011113500 0
2100030002AFECA0BIS IKLINNINBIS1000000000059120110111813410020 11011113510 0
2100040000AFECA0CVA IKLINNINNIG1000000000479040110111313410000 KBC-INVESTERINGSKREDIET 737-6543210-21 11011113510 0
2100040001AFECA0CVA IKLINNINNIG1000000000419920110111813410660 11011113500 0
2100040002AFECA0CVA IKLINNINNIG1000000000059120110111813410020 11011113510 0
@@ -20,5 +20,5 @@
2300050000BE43730004200601 KBC VERZEKERINGEN NV 0 1
3100050001AOGM00160BSCTOBOGOVER001500001001KBC VERZEKERINGEN NV 1 0
3200050001VAN OVERSTRAETENPLEIN 2 3000 LEUVEN 0 0
8135BE44734024486445 EUR0000000013527810110111 0
8135BE44734024486445 EUR0000000013646050110111 0
9 000022000000001393080000000003108190 2
@@ -1,5 +1,6 @@
from openerp.tests.common import TransactionCase
from openerp.modules.module import get_module_resource
from openerp.tools import float_compare
class TestCodaFile(TransactionCase):
"""Tests for import bank statement coda file format (account.bank.statement.import)
@@ -7,32 +8,38 @@ class TestCodaFile(TransactionCase):
def setUp(self):
super(TestCodaFile, self).setUp()
cr, uid = self.cr, self.uid
self.statement_import_model = self.registry('account.bank.statement.import')
self.bank_statement_model = self.registry('account.bank.statement')
coda_file_path = get_module_resource('l10n_be_coda', 'test_coda_file', 'Ontvangen_CODA.2013-01-11-18.59.15.txt')
self.coda_file = open(coda_file_path, 'rb').read().encode('base64')
self.context = {
'journal_id': self.registry('ir.model.data').get_object_reference(cr, uid, 'account', 'bank_journal')[1]
}
self.bank_statement_id = self.statement_import_model.create(cr, uid, dict(
data_file = self.coda_file,
))
def test_coda_file_import(self):
cr, uid = self.cr, self.uid
bank_temp_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'account', 'conf_bnk')
partner_id_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'main_partner')
company_id_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'main_company')
self.bank_temp_id = bank_temp_ref and bank_temp_ref[1] or False
self.partner_id = partner_id_ref and partner_id_ref[1] or False
self.company_id = company_id_ref and company_id_ref[1] or False
coda_file_path = get_module_resource('l10n_be_coda', 'test_coda_file', 'Ontvangen_CODA.2013-01-11-18.59.15.txt')
coda_file = open(coda_file_path, 'rb').read().encode('base64')
bank_account_id = self.registry('res.partner.bank').create(cr, uid, dict(
state = 'bank',
acc_number = 'BE33737018595246',
bank_name = 'Reserve',
partner_id = self.partner_id,
company_id = self.company_id
))
bank_statement_id = self.statement_import_model.create(cr, uid, dict(
file_type = 'coda',
data_file = coda_file,
))
self.statement_import_model.parse_file(cr, uid, [bank_statement_id])
self.statement_import_model.import_file(cr, uid, [self.bank_statement_id], context=self.context)
statement_id = self.bank_statement_model.search(cr, uid, [('name', '=', '135')])[0]
bank_st_record = self.bank_statement_model.browse(cr, uid, statement_id)
self.assertEquals(bank_st_record.balance_start, 11812.70)
self.assertEquals(bank_st_record.balance_end_real, 13527.81)
self.assertEqual(float_compare(bank_st_record.balance_start, 11812.70, precision_digits=2), 0)
self.assertEqual(float_compare(bank_st_record.balance_end_real, 13646.05, precision_digits=2), 0)
def test_coda_file_import_twice(self):
cr, uid = self.cr, self.uid
self.statement_import_model.import_file(cr, uid, [self.bank_statement_id], context=self.context)
with self.assertRaises(Exception):
self.statement_import_model.import_file(cr, uid, [self.bank_statement_id], context=self.context)
def test_coda_file_wrong_journal(self):
""" The demo account used by the CODA file is linked to the demo bank_journal """
cr, uid = self.cr, self.uid
bank_statement_id = self.statement_import_model.create(cr, uid, dict(
data_file = self.coda_file,
))
self.context['journal_id'] = self.registry('ir.model.data').get_object_reference(cr, uid, 'account', 'check_journal')[1]
with self.assertRaises(Exception):
self.statement_import_model.import_file(cr, uid, [bank_statement_id], context=self.context)
@@ -19,8 +19,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import base64
import time
import re
from openerp.osv import osv
from openerp.tools.translate import _
@@ -30,17 +30,20 @@ import logging
_logger = logging.getLogger(__name__)
from openerp.addons.account_bank_statement_import import account_bank_statement_import as coda_ibs
coda_ibs.add_file_type(('coda', 'CODA'))
class account_bank_statement_import(osv.TransientModel):
_inherit = "account.bank.statement.import"
def process_coda(self, cr, uid, codafile=None, journal_id=False, context=None):
def _check_coda(self, cr, uid, data_file, context=None):
# Matches the first 24 characters of a CODA file, as defined by the febelfin specifications
return re.match('0{5}\d{9}05[ D] {7}', data_file) != None
def _parse_file(self, cr, uid, data_file=None, context=None):
if not self._check_coda(cr, uid, data_file, context=context):
return super(account_bank_statement_import, self)._parse_file(cr, uid, data_file, context=context)
if context is None:
context = {}
recordlist = unicode(base64.decodestring(codafile), 'windows-1252', 'strict').split('\n')
recordlist = unicode(data_file, 'windows-1252', 'strict').split('\n')
statements = []
globalisation_comm = {}
for line in recordlist:
@@ -74,32 +77,6 @@ class account_bank_statement_import(osv.TransientModel):
raise osv.except_osv(_('Error') + ' R1002', _('Foreign bank accounts with IBAN structure are not supported '))
else: # Something else, not supported
raise osv.except_osv(_('Error') + ' R1003', _('Unsupported bank account structure '))
statement['journal_id'] = False
statement['bank_account'] = False
# Belgian Account Numbers are composed of 12 digits.
# In OpenERP, the user can fill the bank number in any format: With or without IBan code, with or without spaces, with or without '-'
# The two following sql requests handle those cases.
if len(statement['acc_number']) >= 12:
# If the Account Number is >= 12 digits, it is mostlikely a Belgian Account Number (With or without IBAN).
# The following request try to find the Account Number using a 'like' operator.
# So, if the Account Number is stored with IBAN code, it can be found thanks to this.
cr.execute("select id from res_partner_bank where replace(replace(acc_number,' ',''),'-','') like %s", ('%' + statement['acc_number'] + '%',))
else:
# This case is necessary to avoid cases like the Account Number in the CODA file is set to a single or few digits,
# and so a 'like' operator would return the first account number in the database which matches.
cr.execute("select id from res_partner_bank where replace(replace(acc_number,' ',''),'-','') = %s", (statement['acc_number'],))
bank_ids = [id[0] for id in cr.fetchall()]
# Filter bank accounts which are not allowed
bank_ids = self.pool.get('res.partner.bank').search(cr, uid, [('id', 'in', bank_ids)])
if bank_ids and len(bank_ids) > 0:
bank_accs = self.pool.get('res.partner.bank').browse(cr, uid, bank_ids)
for bank_acc in bank_accs:
if bank_acc.journal_id.id and ((bank_acc.journal_id.currency.id and bank_acc.journal_id.currency.name == statement['currency']) or (not bank_acc.journal_id.currency.id and bank_acc.journal_id.company_id.currency_id.name == statement['currency'])):
statement['journal_id'] = bank_acc.journal_id
statement['bank_account'] = bank_acc
break
if not statement['bank_account']:
raise osv.except_osv(_('Error') + ' R1004', _("No matching Bank Account (with Account Journal) found.\n\nPlease set-up a Bank Account with as Account Number '%s' and as Currency '%s' and an Account Journal.") % (statement['acc_number'], statement['currency']))
statement['description'] = rmspaces(line[90:125])
statement['balance_start'] = float(rmspaces(line[43:58])) / 1000
if line[42] == '1': # 1 = Debit, the starting balance is negative
@@ -205,42 +182,18 @@ class account_bank_statement_import(osv.TransientModel):
statement['balance_end_realDate'] = time.strftime(tools.DEFAULT_SERVER_DATE_FORMAT, time.strptime(rmspaces(line[57:63]), '%d%m%y'))
if statement['debit'] == '1': # 1=Debit
statement['balance_end_real'] = - statement['balance_end_real']
if statement['balance_end_realDate']:
period_id = self.pool.get('account.period').search(cr, uid, [('company_id', '=', statement['journal_id'].company_id.id), ('date_start', '<=', statement['balance_end_realDate']), ('date_stop', '>=', statement['balance_end_realDate'])])
else:
period_id = self.pool.get('account.period').search(cr, uid, [('company_id', '=', statement['journal_id'].company_id.id), ('date_start', '<=', statement['date']), ('date_stop', '>=', statement['date'])])
if not period_id and len(period_id) == 0:
raise osv.except_osv(_('Error') + 'R0002', _("The CODA Statement New Balance date doesn't fall within a defined Accounting Period! Please create the Accounting Period for date %s for the company %s.") % (statement['balance_end_realDate'], statement['journal_id'].company_id.name))
statement['period_id'] = period_id[0]
elif line[0] == '9':
statement['balanceMin'] = float(rmspaces(line[22:37])) / 1000
statement['balancePlus'] = float(rmspaces(line[37:52])) / 1000
if not statement.get('balance_end_real'):
statement['balance_end_real'] = statement['balance_start'] + statement['balancePlus'] - statement['balanceMin']
ret_statements = []
for i, statement in enumerate(statements):
statement['coda_note'] = ''
statement_line = []
balance_start_check_date = (len(statement['lines']) > 0 and statement['lines'][0]['entryDate']) or statement['date']
cr.execute('SELECT balance_end_real \
FROM account_bank_statement \
WHERE journal_id = %s and date <= %s \
ORDER BY date DESC,id DESC LIMIT 1', (statement['journal_id'].id, balance_start_check_date))
res = cr.fetchone()
balance_start_check = res and res[0]
if balance_start_check is None:
if statement['journal_id'].default_debit_account_id and (statement['journal_id'].default_credit_account_id == statement['journal_id'].default_debit_account_id):
balance_start_check = statement['journal_id'].default_debit_account_id.balance
else:
raise osv.except_osv(_('Error'), _("Configuration Error in journal %s!\nPlease verify the Default Debit and Credit Account settings.") % statement['journal_id'].name)
if balance_start_check != statement['balance_start']:
statement['coda_note'] = _("The CODA Statement %s Starting Balance (%.2f) does not correspond with the previous Closing Balance (%.2f) in journal %s!") % (statement['description'] + ' #' + statement['paperSeqNumber'], statement['balance_start'], balance_start_check, statement['journal_id'].name)
if not(statement.get('period_id')):
raise osv.except_osv(_('Error') + ' R3006', _(' No transactions or no period in coda file !'))
statement_data = {
'name': statement['paperSeqNumber'],
'date': statement['date'],
'journal_id': statement['journal_id'].id,
'period_id': statement['period_id'],
'balance_start': statement['balance_start'],
'balance_end_real': statement['balance_end_real'],
}
@@ -251,11 +204,11 @@ class account_bank_statement_import(osv.TransientModel):
statement['coda_note'] = "\n".join([statement['coda_note'], line['type'].title() + ' with Ref. ' + str(line['ref']), 'Ref: ', 'Communication: ' + line['communication'], ''])
elif line['type'] == 'normal':
note = []
if 'counterpartyName' in line and line['counterpartyName'] != '':
if line.get('counterpartyName'):
note.append(_('Counter Party') + ': ' + line['counterpartyName'])
else:
line['counterpartyName'] = False
if 'counterpartyNumber' in line and line['counterpartyNumber'] != '':
if line.get('counterpartyNumber'):
try:
if int(line['counterpartyNumber']) == 0:
line['counterpartyNumber'] = False
@@ -266,15 +219,11 @@ class account_bank_statement_import(osv.TransientModel):
else:
line['counterpartyNumber'] = False
if 'counterpartyAddress' in line and line['counterpartyAddress'] != '':
if line.get('counterpartyAddress'):
note.append(_('Counter Party Address') + ': ' + line['counterpartyAddress'])
structured_com = False
if line['communication_struct'] and 'communication_type' in line and line['communication_type'] == '101':
structured_com = line['communication']
bank_account_id = False
partner_id = False
if 'counterpartyNumber' in line and line['counterpartyNumber']:
bank_account_id, partner_id = self._detect_partner(cr, uid, str(line['counterpartyNumber']), identifying_field='acc_number', context=context)
if line.get('communication', ''):
note.append(_('Communication') + ': ' + line['communication'])
line_data = {
@@ -282,17 +231,19 @@ class account_bank_statement_import(osv.TransientModel):
'note': "\n".join(note),
'date': line['entryDate'],
'amount': line['amount'],
'partner_id': partner_id,
'account_number': line.get('counterpartyNumber', None),
'partner_name': line['counterpartyName'],
'ref': line['ref'],
'sequence': line['sequence'],
'bank_account_id': bank_account_id,
'unique_import_id': line['transactionRef'],
}
statement_line.append((0, 0, line_data))
statement_line.append(line_data)
if statement['coda_note'] != '':
statement_data.update({'coda_note': statement['coda_note']})
statement_data.update({'journal_id': journal_id, 'line_ids': statement_line})
return [statement_data]
statement_data.update({'transactions': statement_line})
ret_statements.append(statement_data)
currency_code = statement['currency']
acc_number = statements[0] and statements[0]['acc_number'] or False
return currency_code, acc_number, ret_statements
def rmspaces(s):
return " ".join(s.split())
+1 -1
View File
@@ -44,7 +44,7 @@
<record id="rateEUR" model="res.currency.rate">
<field name="rate">1.0</field>
<field name="currency_id" ref="EUR"/>
<field eval="time.strftime('%Y-01-01')" name="name"/>
<field name="name">2010-01-01</field>
</record>
<!-- Basic Company -->
+1 -1
View File
@@ -56,7 +56,7 @@ class res_currency(osv.osv):
res[id] = 0
else:
currency = self.browse(cr, uid, id, context=context)
raise osv.except_osv(_('Error!'),_("No currency rate associated for currency '%s' for the given period" % (currency.name)))
raise osv.except_osv(_('Error!'),_("No currency rate associated for currency '%s' for the given period: %s" % (currency.name, date)))
return res
_name = "res.currency"
File diff suppressed because it is too large Load Diff
+3
View File
@@ -541,6 +541,9 @@ class res_users(osv.osv):
(uid, module, ext_id))
return bool(cr.fetchone())
def get_company_currency_id(self, cr, uid, context=None):
return self.browse(cr, uid, uid, context=context).company_id.currency_id.id
#----------------------------------------------------------
# Implied groups
#