New backport from odoo/master

Fix bug #5
This commit is contained in:
Alexis de Lattre
2015-01-23 22:50:24 +01:00
parent e0e6663ba1
commit c017b6864b
15 changed files with 558 additions and 193 deletions

View File

@@ -3,14 +3,13 @@
# flake8: noqa # flake8: noqa
{ {
'name': 'Account Bank Statement Import', 'name': 'Account Bank Statement Import',
'category' : 'Accounting & Finance',
'version': '1.0', 'version': '1.0',
'author': 'OpenERP SA', 'author': 'OpenERP SA',
'depends': ['account'], 'depends': ['account'],
'demo': [], 'demo': [],
'description' : """Generic Wizard to Import Bank Statements. 'description' : """Generic Wizard to Import Bank Statements.
Includes the import of files in .OFX format
Backport from Odoo 9.0 Backport from Odoo 9.0
""", """,
'data' : [ 'data' : [
@@ -23,5 +22,3 @@
'auto_install': False, 'auto_install': False,
'installable': True, 'installable': True,
} }
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -2,97 +2,274 @@
# noqa: This is a backport from Odoo. OCA has no control over style here. # noqa: This is a backport from Odoo. OCA has no control over style here.
# flake8: noqa # flake8: noqa
import base64
from openerp import SUPERUSER_ID
from openerp.osv import fields, osv from openerp.osv import fields, osv
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.exceptions import Warning
import logging import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
_IMPORT_FILE_TYPE = [('none', _('No Import Format Available'))]
def add_file_type(selection_value): class account_bank_statement_line(osv.osv):
global _IMPORT_FILE_TYPE _inherit = "account.bank.statement.line"
if _IMPORT_FILE_TYPE[0][0] == 'none':
_IMPORT_FILE_TYPE = [selection_value] _columns = {
else: # Ensure transactions can be imported only once (if the import format provides unique transaction ids)
_IMPORT_FILE_TYPE.append(selection_value) '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): class account_bank_statement_import(osv.TransientModel):
_name = 'account.bank.statement.import' _name = 'account.bank.statement.import'
_description = 'Import Bank Statement' _description = 'Import Bank Statement'
def _get_import_file_type(self, cr, uid, context=None):
return _IMPORT_FILE_TYPE
_columns = { _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.'), '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): def import_file(self, cr, uid, ids, context=None):
return self._get_import_file_type(cr, uid, context=context)[0][0] """ 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): data_file = self.browse(cr, uid, ids[0], context=context).data_file
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
_defaults = { # The appropriate implementation module returns the required data
'file_type': _get_first_file_type, currency_code, account_number, stmts_vals = self._parse_file(cr, uid, base64.b64decode(data_file), context=context)
'journal_id': _get_default_journal, # 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)
# 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',
} }
def _detect_partner(self, cr, uid, identifying_string, identifying_field='acc_number', context=None): def _parse_file(self, cr, uid, data_file, context=None):
"""Try to find a bank account and its related partner for the given 'identifying_string', looking on the field 'identifying_field'. """ 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.
:param identifying_string: varchar rtype: triplet (if a value can't be retrieved, use None)
:param identifying_field: varchar corresponding to the name of a field of res.partner.bank - currency code: string (e.g: 'EUR')
:returns: tuple(ID of the bank account found or False, ID of the partner for the bank account found or False) 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 raise Warning(_('Could not make sense of the given file.\nDid you install the module to support this type of file ?'))
bank_account_id = False
if identifying_string: def _check_parsed_data(self, cr, uid, stmts_vals, context=None):
ids = self.pool.get('res.partner.bank').search(cr, uid, [(identifying_field, '=', identifying_string)], context=context) """ Basic and structural verifications """
if ids: if len(stmts_vals) == 0:
bank_account_id = ids[0] raise Warning(_('This file doesn\'t contain any statement.'))
partner_id = self.pool.get('res.partner.bank').browse(cr, uid, bank_account_id, context=context).partner_id.id
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 Warning(_('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,' ',''),'-','') like %s and journal_id is not null", ('%' + account_number + '%',))
bank_account_ids = [id[0] for id in cr.fetchall()]
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 Warning(_('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: else:
#create the bank account, not linked to any partner. The reconciliation will link the partner manually if bank_account.journal_id.id:
#chosen at the bank statement final confirmation time. journal_id = bank_account.journal_id.id
# 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 Warning(_('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)
# I think it's too dangerous, so I disable that code -- Alexis de Lattre
#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 Warning(_('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: try:
type_model, type_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'bank_normal') 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) type_id = self.pool.get('res.partner.bank.type').browse(cr, uid, type_id, context=context)
bank_code = type_id.code bank_code = type_id.code
except ValueError: except ValueError:
bank_code = 'bank' bank_code = 'bank'
acc_number = identifying_field == 'acc_number' and identifying_string or _('Undefined') account_number = account_number.replace(' ', '').replace('-', '')
bank_account_vals = { vals_acc = {
'acc_number': acc_number, 'acc_number': account_number,
'state': bank_code, 'state': bank_code,
} }
bank_account_vals[identifying_field] = identifying_string # Odoo users bank accounts (which we import statement from) have company_id and journal_id set
bank_account_id = self.pool.get('res.partner.bank').create(cr, uid, bank_account_vals, context=context) # while 'counterpart' bank accounts (from which statement transactions originate) don't.
return bank_account_id, partner_id # 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
def import_bank_statement(self, cr, uid, bank_statement_vals=False, context=None): return self.pool.get('res.partner.bank').create(cr, uid, vals_acc, context=context)
""" 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"""
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 = [] statement_ids = []
for vals in bank_statement_vals: ignored_statement_lines_import_ids = []
statement_ids.append(self.pool.get('account.bank.statement').create(cr, uid, vals, context=context)) for st_vals in stmts_vals:
return statement_ids 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 Warning(_('You have already imported that file.'))
def process_none(self, cr, uid, data_file, journal_id=False, context=None): # Prepare import feedback
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.')) 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): return statement_ids, notifications
""" 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
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -7,22 +7,14 @@
<field name="model">account.bank.statement.import</field> <field name="model">account.bank.statement.import</field>
<field name="priority">1</field> <field name="priority">1</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Import Bank Statements" version="7.0"> <form string="Import Bank Statements">
<group>
<group>
<field name="data_file"/> <field name="data_file"/>
<field name="file_type"/> <br/><br/><b> How to import your bank statement :</b>
<field name="journal_id" domain="[('type', '=', 'bank')]" context="{'default_type':'bank'}"/> <br/><label string= "1. Download your bank statements from your bank website."/>
</group> <br/><label string= "2. Make sure you have installed the right module to support the file format."/>
<group> <br/><label string= "3. Select the file and click 'Import'."/>
<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>
<footer> <footer>
<button name="parse_file" string="_Import" type="object" class="oe_highlight"/> <button name="import_file" string="_Import" type="object" class="oe_highlight"/>
or or
<button string="Cancel" class="oe_link" special="cancel"/> <button string="Cancel" class="oe_link" special="cancel"/>
</footer> </footer>
@@ -40,12 +32,7 @@
<field name="view_id" ref="account_bank_statement_import_view"/> <field name="view_id" ref="account_bank_statement_import_view"/>
</record> </record>
<menuitem <menuitem id="menu_account_bank_statement_import" parent="account.menu_finance_bank_and_cash" action="action_account_bank_statement_import" sequence="8"/>
parent="account.menu_finance_bank_and_cash"
id="menu_account_bank_statement_import"
action="action_account_bank_statement_import"
sequence="11"
/>
</data> </data>
</openerp> </openerp>

View File

@@ -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

View File

@@ -3,6 +3,7 @@
# flake8: noqa # flake8: noqa
{ {
'name': 'Import OFX Bank Statement', 'name': 'Import OFX Bank Statement',
'category' : 'Accounting & Finance',
'version': '1.0', 'version': '1.0',
'author': 'OpenERP SA', 'author': 'OpenERP SA',
'depends': ['account_bank_statement_import'], 'depends': ['account_bank_statement_import'],
@@ -28,5 +29,3 @@ create periods for the year 2013.
'auto_install': False, 'auto_install': False,
'installable': True, 'installable': True,
} }
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -3,74 +3,67 @@
# flake8: noqa # flake8: noqa
import logging import logging
import base64 import StringIO
import os
from openerp.osv import osv from openerp.osv import osv
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.exceptions import Warning
_logger = logging.getLogger(__name__) _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: try:
from ofxparse import OfxParser as ofxparser from ofxparse import OfxParser as ofxparser
except ImportError: 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`.") "It can be downloaded and installed from `https://pypi.python.org/pypi/ofxparse`.")
ofxparser = None ofxparser = None
class account_bank_statement_import(osv.TransientModel): class account_bank_statement_import(osv.TransientModel):
_inherit = 'account.bank.statement.import' _inherit = 'account.bank.statement.import'
def process_ofx(self, cr, uid, data_file, journal_id=False, context=None): def _check_ofx(self, cr, uid, file, context=None):
""" Import a file in the .OFX format"""
if ofxparser is None: if ofxparser is None:
raise osv.except_osv(_("Error"), _("OFX parser unavailable because the `ofxparse` Python library cannot be found." return False
"It can be downloaded and installed from `https://pypi.python.org/pypi/ofxparse`."))
try: try:
tempfile = open("temp.ofx", "w+") ofx = ofxparser.parse(file)
tempfile.write(base64.decodestring(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: except:
raise osv.except_osv(_('Import Error!'), _('Please check OFX file format is proper or not.')) return False
line_ids = [] return ofx
def _parse_file(self, cr, uid, data_file, context=None):
ofx = self._check_ofx(cr, uid, StringIO.StringIO(data_file), 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 total_amt = 0.00
try: try:
for transaction in ofx.account.statement.transactions: 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 = { vals_line = {
'date': transaction.date, 'date': transaction.date,
'name': transaction.payee + ': ' + transaction.memo, 'name': transaction.payee + (transaction.memo and ': ' + transaction.memo or ''),
'ref': transaction.id, 'ref': transaction.id,
'amount': transaction.amount, 'amount': transaction.amount,
'partner_id': partner_id, 'unique_import_id': transaction.id,
'bank_account_id': bank_account_id, 'bank_account_id': bank_account_id,
'partner_id': partner_id,
} }
total_amt += float(transaction.amount) total_amt += float(transaction.amount)
line_ids.append((0, 0, vals_line)) transactions.append(vals_line)
except Exception, e: 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)) raise Warning(_("The following problem occurred during import. The file might not be valid.\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)
vals_bank_statement = { vals_bank_statement = {
'name': ofx.account.routing_number, 'name': ofx.account.routing_number,
'balance_start': ofx.account.statement.balance, 'transactions': transactions,
'balance_end_real': float(ofx.account.statement.balance) + total_amt, 'balance_start': float(ofx.account.statement.balance) - total_amt,
'period_id': period_ids and period_ids[0] or False, 'balance_end_real': float(ofx.account.statement.balance),
'journal_id': journal_id
} }
vals_bank_statement.update({'line_ids': line_ids}) return ofx.account.statement.currency, ofx.account.number, [vals_bank_statement]
os.remove(path)
return [vals_bank_statement]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -2,7 +2,3 @@
# noqa: This is a backport from Odoo. OCA has no control over style here. # noqa: This is a backport from Odoo. OCA has no control over style here.
# flake8: noqa # flake8: noqa
from . import test_import_bank_statement from . import test_import_bank_statement
checks = [
test_import_bank_statement
]

View File

@@ -23,10 +23,9 @@ class TestOfxFile(TransactionCase):
ofx_file_path = get_module_resource('account_bank_statement_import_ofx', 'test_ofx_file', 'test_ofx.ofx') 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') ofx_file = open(ofx_file_path, 'rb').read().encode('base64')
bank_statement_id = self.statement_import_model.create(cr, uid, dict( bank_statement_id = self.statement_import_model.create(cr, uid, dict(
file_type='ofx',
data_file=ofx_file, data_file=ofx_file,
)) ))
self.statement_import_model.parse_file(cr, uid, [bank_statement_id]) self.statement_import_model.import_file(cr, uid, [bank_statement_id])
statement_id = self.bank_statement_model.search(cr, uid, [('name', '=', '000000123')])[0] statement_id = self.bank_statement_model.search(cr, uid, [('name', '=', '000000123')])[0]
bank_st_record = self.bank_statement_model.browse(cr, uid, statement_id) bank_st_record = self.bank_statement_model.browse(cr, uid, statement_id)
self.assertEquals(bank_st_record.balance_start, 2156.56) self.assertEquals(bank_st_record.balance_start, 2156.56)

View File

@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# noqa: This is a backport from Odoo. OCA has no control over style here. # noqa: This is a backport from Odoo. OCA has no control over style here.
# flake8: noqa # flake8: noqa
{ {
'name': 'Import QIF Bank Statement', 'name': 'Import QIF Bank Statement',
'category' : 'Accounting & Finance',
'version': '1.0', 'version': '1.0',
'author': 'OpenERP SA', 'author': 'OpenERP SA',
'description': ''' 'description': '''
@@ -12,21 +14,15 @@ 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. 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 Important Note
creation of the Financial Accounting records). ---------------------------------------------
Because of the QIF format limitation, we cannot ensure the same transactions aren't imported several times or handle multicurrency.
Backported from Odoo 9.0 Whenever possible, you should use a more appropriate file format like OFX.
When testing with the provided test file, make sure the demo data from the
base account_bank_statement_import module has been imported, or manually
create periods for the year 2013.
''', ''',
'images': [], 'images': [],
'depends': ['account_bank_statement_import'], 'depends': ['account_bank_statement_import'],
'demo': [], 'demo': [],
'data': [], 'data': ['account_bank_statement_import_qif_view.xml'],
'auto_install': False, 'auto_install': False,
'installable': True, 'installable': True,
} }
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -3,29 +3,49 @@
# flake8: noqa # flake8: noqa
import dateutil.parser import dateutil.parser
import base64 import StringIO
from tempfile import TemporaryFile
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.osv import osv from openerp.osv import osv, fields
from openerp.exceptions import Warning
from openerp.addons.account_bank_statement_import import account_bank_statement_import as ibs
ibs.add_file_type(('qif', 'QIF'))
class account_bank_statement_import(osv.TransientModel): class account_bank_statement_import(osv.TransientModel):
_inherit = "account.bank.statement.import" _inherit = "account.bank.statement.import"
def process_qif(self, cr, uid, data_file, journal_id=False, context=None): _columns = {
""" Import a file in the .QIF format""" '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, 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: try:
fileobj = TemporaryFile('wb+')
fileobj.write(base64.b64decode(data_file))
fileobj.seek(0)
file_data = "" file_data = ""
for line in fileobj.readlines(): for line in StringIO.StringIO(data_file).readlines():
file_data += line file_data += line
fileobj.close()
if '\r' in file_data: if '\r' in file_data:
data_list = file_data.split('\r') data_list = file_data.split('\r')
else: else:
@@ -33,8 +53,8 @@ class account_bank_statement_import(osv.TransientModel):
header = data_list[0].strip() header = data_list[0].strip()
header = header.split(":")[1] header = header.split(":")[1]
except: except:
raise osv.except_osv(_('Import Error!'), _('Please check QIF file format is proper or not.')) raise Warning(_('Could not decipher the QIF file.'))
line_ids = [] transactions = []
vals_line = {} vals_line = {}
total = 0 total = 0
if header == "Bank": if header == "Bank":
@@ -45,33 +65,34 @@ class account_bank_statement_import(osv.TransientModel):
continue continue
if line[0] == 'D': # date of transaction if line[0] == 'D': # date of transaction
vals_line['date'] = dateutil.parser.parse(line[1:], fuzzy=True).date() 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 elif line[0] == 'T': # Total amount
total += float(line[1:].replace(',', '')) total += float(line[1:].replace(',', ''))
vals_line['amount'] = float(line[1:].replace(',', '')) vals_line['amount'] = float(line[1:].replace(',', ''))
elif line[0] == 'N': # Check number elif line[0] == 'N': # Check number
vals_line['ref'] = line[1:] vals_line['ref'] = line[1:]
elif line[0] == 'P': # Payee 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:] 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 elif line[0] == 'M': # Memo
vals_line['name'] = 'name' in vals_line and vals_line['name'] + ': ' + line[1:] or line[1:] vals_line['name'] = 'name' in vals_line and vals_line['name'] + ': ' + line[1:] or line[1:]
elif line[0] == '^': # end of item elif line[0] == '^': # end of item
line_ids.append((0, 0, vals_line)) transactions.append(vals_line)
vals_line = {} vals_line = {}
elif line[0] == '\n': elif line[0] == '\n':
line_ids = [] transactions = []
else: else:
pass pass
else: else:
raise osv.except_osv(_('Error!'), _('Cannot support this Format !Type:%s.') % (header,)) raise Warning(_('This file is either not a bank statement or is not correctly formed.'))
vals_bank_statement.update({'balance_end_real': total,
'line_ids': line_ids, vals_bank_statement.update({
'journal_id': journal_id}) 'balance_end_real': total,
return [vals_bank_statement] 'transactions': transactions
})
return None, None, [vals_bank_statement]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -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

View File

@@ -2,7 +2,3 @@
# noqa: This is a backport from Odoo. OCA has no control over style here. # noqa: This is a backport from Odoo. OCA has no control over style here.
# flake8: noqa # flake8: noqa
from . import test_import_bank_statement from . import test_import_bank_statement
checks = [
test_import_bank_statement
]

View File

@@ -20,10 +20,12 @@ class TestQifFile(TransactionCase):
qif_file_path = get_module_resource('account_bank_statement_import_qif', 'test_qif_file', 'test_qif.qif') 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') qif_file = open(qif_file_path, 'rb').read().encode('base64')
bank_statement_id = self.statement_import_model.create(cr, uid, dict( bank_statement_id = self.statement_import_model.create(cr, uid, dict(
file_type='qif',
data_file=qif_file, data_file=qif_file,
)) ))
self.statement_import_model.parse_file(cr, uid, [bank_statement_id]) 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] 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 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) bank_st_record = self.bank_statement_model.browse(cr, uid, statement_id)