Browse Source

New backport from odoo/master

Fix bug #5
pull/6/head
Alexis de Lattre 10 years ago
parent
commit
c017b6864b
  1. 5
      account_bank_statement_import/__openerp__.py
  2. 303
      account_bank_statement_import/account_bank_statement_import.py
  3. 27
      account_bank_statement_import/account_bank_statement_import_view.xml
  4. 178
      account_bank_statement_import/static/description/icon_src.svg
  5. 3
      account_bank_statement_import_ofx/__openerp__.py
  6. 71
      account_bank_statement_import_ofx/account_bank_statement_import_ofx.py
  7. BIN
      account_bank_statement_import_ofx/static/description/icon.png
  8. 4
      account_bank_statement_import_ofx/tests/__init__.py
  9. 3
      account_bank_statement_import_ofx/tests/test_import_bank_statement.py
  10. 20
      account_bank_statement_import_qif/__openerp__.py
  11. 81
      account_bank_statement_import_qif/account_bank_statement_import_qif.py
  12. 24
      account_bank_statement_import_qif/account_bank_statement_import_qif_view.xml
  13. BIN
      account_bank_statement_import_qif/static/description/icon.png
  14. 4
      account_bank_statement_import_qif/tests/__init__.py
  15. 6
      account_bank_statement_import_qif/tests/test_import_bank_statement.py

5
account_bank_statement_import/__openerp__.py

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

303
account_bank_statement_import/account_bank_statement_import.py

@ -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):
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): 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):
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, 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 Warning(_('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 Warning(_('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 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
#chosen at the bank statement final confirmation time.
if bank_account.journal_id.id:
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')
bank_account_vals = {
'acc_number': acc_number,
account_number = account_number.replace(' ', '').replace('-', '')
vals_acc = {
'acc_number': account_number,
'state': bank_code, '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
# 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
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"""
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 = [] 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
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.'))
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
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
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 Warning(_('You have already imported that file.'))
# 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)
}
}]
return statement_ids, notifications

27
account_bank_statement_import/account_bank_statement_import_view.xml

@ -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">
<group>
<group>
<form string="Import Bank Statements">
<field name="data_file"/> <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>
<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> <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
parent="account.menu_finance_bank_and_cash"
id="menu_account_bank_statement_import"
action="action_account_bank_statement_import"
sequence="11"
/>
<menuitem id="menu_account_bank_statement_import" parent="account.menu_finance_bank_and_cash" action="action_account_bank_statement_import" sequence="8"/>
</data> </data>
</openerp> </openerp>

178
account_bank_statement_import/static/description/icon_src.svg

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

3
account_bank_statement_import_ofx/__openerp__.py

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

71
account_bank_statement_import_ofx/account_bank_statement_import_ofx.py

@ -3,74 +3,67 @@
# flake8: noqa # flake8: noqa
import logging import logging
import base64
import os
import StringIO
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):
""" Import a file in the .OFX format"""
def _check_ofx(self, cr, uid, file, context=None):
if ofxparser is 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: try:
tempfile = open("temp.ofx", "w+")
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))
ofx = ofxparser.parse(file)
except: except:
raise osv.except_osv(_('Import Error!'), _('Please check OFX file format is proper or not.'))
line_ids = []
return False
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))
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)
raise Warning(_("The following problem occurred during import. The file might not be valid.\n\n %s" % e.message))
vals_bank_statement = { vals_bank_statement = {
'name': ofx.account.routing_number, 'name': ofx.account.routing_number,
'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
'transactions': transactions,
'balance_start': float(ofx.account.statement.balance) - total_amt,
'balance_end_real': float(ofx.account.statement.balance),
} }
vals_bank_statement.update({'line_ids': line_ids})
os.remove(path)
return [vals_bank_statement]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
return ofx.account.statement.currency, ofx.account.number, [vals_bank_statement]

BIN
account_bank_statement_import_ofx/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 6.1 KiB

4
account_bank_statement_import_ofx/tests/__init__.py

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

3
account_bank_statement_import_ofx/tests/test_import_bank_statement.py

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

20
account_bank_statement_import_qif/__openerp__.py

@ -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
creation of the Financial Accounting records).
Backported from Odoo 9.0
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.
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'], '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:

81
account_bank_statement_import_qif/account_bank_statement_import_qif.py

@ -3,29 +3,49 @@
# flake8: noqa # flake8: noqa
import dateutil.parser import dateutil.parser
import base64
from tempfile import TemporaryFile
import StringIO
from openerp.tools.translate import _ 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
from openerp.exceptions import Warning
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):
""" 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, 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.'))
line_ids = []
raise Warning(_('Could not decipher the QIF file.'))
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,))
vals_bank_statement.update({'balance_end_real': total,
'line_ids': line_ids,
'journal_id': journal_id})
return [vals_bank_statement]
raise Warning(_('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]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

24
account_bank_statement_import_qif/account_bank_statement_import_qif_view.xml

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

BIN
account_bank_statement_import_qif/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 6.0 KiB

4
account_bank_statement_import_qif/tests/__init__.py

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

6
account_bank_statement_import_qif/tests/test_import_bank_statement.py

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

Loading…
Cancel
Save