diff --git a/account_banking/__init__.py b/account_banking/__init__.py
index 3cd966b9c..fd44236e4 100644
--- a/account_banking/__init__.py
+++ b/account_banking/__init__.py
@@ -24,6 +24,8 @@
# along with this program. If not, see .
#
##############################################################################
+import sepa
+import record
import account_banking
import parsers
import wizard
diff --git a/account_banking/__terp__.py b/account_banking/__terp__.py
index 4f3f6e253..00b4eddb1 100644
--- a/account_banking/__terp__.py
+++ b/account_banking/__terp__.py
@@ -25,12 +25,12 @@
##############################################################################
{
'name': 'Account Banking',
- 'version': '0.1',
+ 'version': '0.1.9',
'license': 'GPL-3',
'author': 'EduSense BV',
'website': 'http://www.edusense.nl',
'category': 'Account Banking',
- 'depends': ['base', 'base_iban', 'account', 'account_payment'],
+ 'depends': ['base', 'account', 'account_payment'],
'init_xml': [],
'update_xml': [
#'security/ir.model.access.csv',
@@ -42,8 +42,7 @@
'description': '''
Module to do banking.
- Note: This module is depending on BeautifulSoup when using the Dutch
- online database. Make sure it is installed.
+ Note: This module is depending on BeautifulSoup.
This modules tries to combine all current banking import and export
schemes. Rationale for this is that it is quite common to have foreign
@@ -70,6 +69,7 @@
+ Each bank can have its own pace in introducing SEPA into their
communication with their customers.
+ National online databases can be used to convert BBAN's to IBAN's.
+ + The SWIFT database is consulted for bank information.
* Adds dropin extensible import facility for bank communication in:
- Drop-in input parser development.
diff --git a/account_banking/account_banking.py b/account_banking/account_banking.py
index 83897b5a2..62282e3fd 100644
--- a/account_banking/account_banking.py
+++ b/account_banking/account_banking.py
@@ -57,8 +57,11 @@ Modifications are extensive:
default behavior is to flag the orders as 'sent', not as 'done'.
'''
import time
+import sys
+import sepa
from osv import osv, fields
from tools.translate import _
+from wizard.banktools import get_or_create_bank
class account_banking_account_settings(osv.osv):
'''Default Journal for Bank Account'''
@@ -351,7 +354,7 @@ class account_bank_statement_line(osv.osv):
_description = 'Bank Transaction'
def _get_period(self, cursor, uid, context={}):
- date = context.get('date') and context['date'] or None
+ date = context.get('date', None)
periods = self.pool.get('account.period').find(cursor, uid, dt=date)
return periods and periods[0] or False
@@ -381,7 +384,7 @@ class account_bank_statement_line(osv.osv):
states={'draft': [('readonly', False)]}),
'ref': fields.char('Ref.', size=32, readonly=True,
states={'draft': [('readonly', False)]}),
- 'name': fields.char('Name', size=64, required=True, readonly=True,
+ 'name': fields.char('Name', size=64, required=False, readonly=True,
states={'draft': [('readonly', False)]}),
'date': fields.date('Date', required=True, readonly=True,
states={'draft': [('readonly', False)]}),
@@ -750,4 +753,211 @@ class payment_order(osv.osv):
payment_order()
+class res_partner_bank(osv.osv):
+ '''
+ This is a hack to circumvent the ugly account/base_iban dependency. The
+ usage of __mro__ requires inside information of inheritence. This code is
+ tested and works - it bypasses base_iban altogether. Be sure to use
+ 'super' for inherited classes from here though.
+
+ Extended functionality:
+ 1. BBAN and IBAN are considered equal
+ 2. Online databases are checked when available
+ 3. Banks are created on the fly when using IBAN
+ 4. Storage is uppercase, not lowercase
+ 5. Presentation is formal IBAN
+ 6. BBAN's are generated from IBAN when possible
+ '''
+ _inherit = 'res.partner.bank'
+ _columns = {
+ 'iban': fields.char('IBAN', size=34, readonly=True,
+ help="International Bank Account Number"
+ ),
+ }
+
+ def create(self, cursor, uid, vals, context={}):
+ '''
+ Create dual function IBAN account for SEPA countries
+ Note: No check on validity IBAN/Country
+ '''
+ if 'iban' in vals and vals['iban']:
+ iban = sepa.IBAN(vals['iban'])
+ vals['iban'] = str(iban)
+ vals['acc_number'] = iban.localized_BBAN
+ return self.__class__.__mro__[4].create(self, cursor, uid, vals,
+ context
+ )
+
+ def write(self, cursor, uid, ids, vals, context={}):
+ '''
+ Create dual function IBAN account for SEPA countries
+ Note: No check on validity IBAN/Country
+ '''
+ if 'iban' in vals and vals['iban']:
+ iban = sepa.IBAN(vals['iban'])
+ vals['iban'] = str(iban)
+ vals['acc_number'] = iban.localized_BBAN
+ return self.__class__.__mro__[4].write(self, cursor, uid, ids,
+ vals, context
+ )
+
+ def read(self, *args, **kwargs):
+ records = self.__class__.__mro__[4].read(self, *args, **kwargs)
+ for record in records:
+ if 'iban' in record and record['iban']:
+ record['iban'] = unicode(sepa.IBAN(record['iban']))
+ return records
+
+ def search(self, cr, uid, args, offset=0, limit=None, order=None,
+ context=None, count=False
+ ):
+ '''
+ Extend the search method to search not only on
+ bank type == basic account number,
+ but also on
+ type == iban
+ '''
+ res = self.__class__.__mro__[4].search(self,
+ cr, uid, args, offset, limit, order, context=context, count=count
+ )
+ if filter(lambda x:x[0]=='acc_number' ,args):
+ # get the value of the search
+ iban_value = filter(lambda x: x[0] == 'acc_number', args)[0][2]
+ # get the other arguments of the search
+ args1 = filter(lambda x:x[0]!='acc_number' ,args)
+ # add the new criterion
+ args1 += [('iban', 'ilike',
+ iban_value.replace(' ','').replace('-','').replace('/','')
+ )]
+ # append the results to the older search
+ res += super(res_partner_bank, self).search(
+ cr, uid, args1, offset, limit, order, context=context,
+ count=count
+ )
+ return res
+
+ def check_iban(self, cursor, uid, ids):
+ '''
+ Check IBAN number
+ '''
+ for bank_acc in self.browse(cursor, uid, ids):
+ if not bank_acc.iban:
+ continue
+ iban = sepa.IBAN(bank_acc.iban)
+ if not iban.valid:
+ return False
+ return True
+
+ def get_bban_from_iban(self, cursor, uid, context={}):
+ '''
+ Get the local BBAN from the IBAN
+ '''
+ for record in self.browse(cursor, uid, ids, context):
+ if record.iban:
+ res[record.id] = record.iban.localized_BBAN
+ else:
+ res[record.id] = False
+ return res
+
+ def onchange_iban(self, cursor, uid, ids, iban, acc_number, context={}):
+ '''
+ Trigger to auto complete other fields.
+ '''
+ acc_number = acc_number.strip()
+ country_obj = self.pool.get('res.country')
+ partner_obj = self.pool.get('res.partner')
+ bic = None
+ country_ids = []
+
+ if not iban:
+ # Pre fill country based on company address
+ user_obj = self.pool.get('res.users')
+ user = user_obj.browse(cursor, uid, uid, context)
+ country = partner_obj.browse(cursor, uid,
+ user.company_id.partner_id.id
+ ).country
+ country_ids = [country.id]
+
+ # Complete data with online database when available
+ if country.code in sepa.IBAN.countries:
+ info = sepa.online.account_info(country.code, acc_number)
+ if info:
+ bic = info.bic
+ iban = info.iban
+ else:
+ return {}
+
+ iban_acc = sepa.IBAN(iban)
+ if iban_acc.valid:
+ bank_id, country_id = get_or_create_bank(
+ self.pool, cursor, uid, bic or iban_acc.BIC_searchkey
+ )
+ return {
+ 'value': {
+ 'acc_number': iban_acc.localized_BBAN,
+ 'iban': unicode(iban_acc),
+ 'country':
+ country_id or
+ country_ids and country_ids[0] or
+ False,
+ 'bank':
+ bank_id or bank_ids and bank_id[0] or False,
+ }
+ }
+ raise osv.except_osv(_('Invalid IBAN account number!'),
+ _("The IBAN number doesn't seem to be correct")
+ )
+
+ _constraints = [
+ (check_iban, "The IBAN number doesn't seem to be correct", ["iban"])
+ ]
+ _defaults = {
+ 'acc_number': get_bban_from_iban,
+ }
+
+res_partner_bank()
+
+class res_bank(osv.osv):
+ '''
+ Add a on_change trigger to automagically fill bank details from the
+ online SWIFT database. Allow hand filled names to overrule SWIFT names.
+ '''
+ _inherit = 'res.bank'
+ def onchange_bic(self, cursor, uid, ids, bic, name, context={}):
+ '''
+ Trigger to auto complete other fields.
+ '''
+ if not bic:
+ return {}
+
+ info, address = sepa.online.bank_info(bic)
+ if not info:
+ return {}
+
+ if address and address.country_id:
+ country_id = self.pool.get('res.country').search(
+ cursor, uid, [('code','=',address.country_id)]
+ )
+ country_id = country_id and country_id[0] or False
+ else:
+ country_id = False
+
+ return {
+ 'value': {
+ # Only the first eight positions of BIC are used for bank
+ # transfers, so ditch the rest.
+ 'bic': info.bic[:8],
+ 'code': info.code,
+ 'street': address.street,
+ 'street2':
+ address.has_key('street2') and address.street2 or False,
+ 'zip': address.zip,
+ 'city': address.city,
+ 'country': country_id,
+ 'name': name and name or info.name,
+ }
+ }
+
+res_bank()
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/account_banking/account_banking_view.xml b/account_banking/account_banking_view.xml
index 1606c0cae..8b48d0a9c 100644
--- a/account_banking/account_banking_view.xml
+++ b/account_banking/account_banking_view.xml
@@ -207,5 +207,66 @@
+
+
+ res.partner.bank.form.account_banking.inherit
+ res.partner.bank
+
+ form
+
+
+
+
+
+
+
+ res.partner.bank.form.account_banking.inherit
+ res.partner.bank
+
+ form
+
+
+
+
+
+
+
+
+
+ res.partner.form.account_banking.inherit-1
+ res.partner
+
+ form
+
+
+
+
+
+
+
+ res.partner.form.account_banking.inherit-2
+ res.partner
+
+ form
+
+
+
+
+
+
+
+
+
+ res.bank.form.account_banking.inherit-1
+ res.bank
+
+ form
+
+
+
+
+
+
+
diff --git a/account_banking/sepa/__init__.py b/account_banking/sepa/__init__.py
new file mode 100644
index 000000000..93f2dc0fe
--- /dev/null
+++ b/account_banking/sepa/__init__.py
@@ -0,0 +1,23 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# Copyright (C) 2009 EduSense BV ().
+# All Rights Reserved
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+import iban
+import online
+IBAN = iban.IBAN
diff --git a/account_banking/sepa.py b/account_banking/sepa/iban.py
similarity index 100%
rename from account_banking/sepa.py
rename to account_banking/sepa/iban.py
diff --git a/account_banking/sepa/online.py b/account_banking/sepa/online.py
new file mode 100644
index 000000000..9d175ad33
--- /dev/null
+++ b/account_banking/sepa/online.py
@@ -0,0 +1,186 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# Copyright (C) 2009 EduSense BV ().
+# All Rights Reserved
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+'''
+This module provides online bank databases for conversion between BBAN and
+IBAN numbers and for consulting.
+'''
+import re
+import urllib, urllib2
+from BeautifulSoup import BeautifulSoup
+from account_banking.sepa import postalcode
+from account_banking.sepa.urlagent import URLAgent, SoupForm
+from account_banking.struct import struct
+
+__all__ = [
+ 'account_info',
+ 'bank_info',
+]
+
+IBANlink_NL = 'http://www.ibannl.org/iban_check.php'
+
+def get_iban_bic_NL(bank_acc):
+ '''
+ Consult the Dutch online banking database to check both the account number
+ and the bank to which it belongs. Will not work offline, is limited to
+ banks operating in the Netherlands and will only convert Dutch local
+ account numbers.
+ '''
+ data = urllib.urlencode(dict(number=bank_acc, method='POST'))
+ request = urllib2.Request(IBANlink_NL, data)
+ response = urllib2.urlopen(request)
+ soup = BeautifulSoup(response)
+ result = struct()
+ for _pass, td in enumerate(soup.findAll('td')):
+ if _pass % 2 == 1:
+ result[attr] = unicode(td.find('font').contents[0])
+ else:
+ attr = td.find('strong').contents[0][:4].strip().lower()
+ if result:
+ result.account = bank_acc
+ result.country_id = result.bic[4:6]
+ # Nationalized bank code
+ result.code = result.bic[:6]
+ # All Dutch banks use generic channels
+ # result.bic += 'XXX'
+ return result
+ return None
+
+_account_info = {
+ # TODO: Add more online data banks
+ 'NL': get_iban_bic_NL,
+}
+
+def account_info(iso, bank_acc):
+ '''
+ Consult the online database for this country or return None
+ '''
+ if iso in _account_info:
+ return _account_info[iso](bank_acc)
+ return None
+
+bic_re = re.compile("[^']+'([^']*)'.*")
+SWIFTlink = 'http://www.swift.com/bsl/freequery.do'
+
+def bank_info(bic):
+ '''
+ Consult the free online SWIFT service to obtain the name and address of a
+ bank. This call may take several seconds to complete, due to the number of
+ requests to make. In total three HTTP requests are made per function call.
+ In theory one request could be stripped, but the SWIFT terms of use prevent
+ automated usage, so user like behavior is required.
+ '''
+ def harvest(soup):
+ retval = struct()
+ for trsoup in soup('tr'):
+ for stage, tdsoup in enumerate(trsoup('td')):
+ if stage == 0:
+ attr = tdsoup.contents[0].strip().replace(' ','_')
+ elif stage == 2:
+ if tdsoup.contents:
+ retval[attr] = tdsoup.contents[0].strip()
+ else:
+ retval[attr] = ''
+ return retval
+
+ # Get form
+ agent = URLAgent()
+ request = agent.open(SWIFTlink)
+ soup = BeautifulSoup(request)
+
+ # Parse request form. As this form is intertwined with a table, use the parent
+ # as root to search for form elements.
+ form = SoupForm(soup.find('form', {'id': 'frmFreeSearch1'}), parent=True)
+
+ # Fill form fields
+ form['selected_bic'] = bic
+
+ # Get intermediate response
+ response = agent.submit(form)
+
+ # Parse response
+ soup = BeautifulSoup(response)
+
+ # Isolate the full 11 BIC - there may be more, but we only use the first
+ bic_button = soup.find('a', {'class': 'bigbuttonblack'})
+ if not bic_button:
+ return None, None
+
+ # Overwrite the location with 'any' ('XXX') to narrow the results to one or less.
+ # Assume this regexp will never fail...
+ full_bic = bic_re.match(bic_button.get('href')).groups()[0][:8] + 'XXX'
+
+ # Get the detail form
+ form = SoupForm(soup.find('form', {'id': 'frmDetail'}))
+
+ # Fill detail fields
+ form['selected_bic11'] = full_bic
+
+ # Get final response
+ response = agent.submit(form)
+ soup = BeautifulSoup(response)
+
+ # Now parse the results
+ tables = soup.find('div', {'id':'Middle'}).findAll('table')
+ if not tables:
+ return None, None
+ tablesoup = tables[2]('table')
+ if not tablesoup:
+ return None, None
+
+ codes = harvest(tablesoup[0])
+ if not codes:
+ return None, None
+
+ bankinfo = struct(
+ # Most banks use the first four chars of the BIC as an identifier for
+ # their 'virtual bank' accross the world, containing all national
+ # banks world wide using the same name.
+ # The concatenation with the two character country code is for most
+ # national branches sufficient as a unique identifier.
+ code = full_bic[:6],
+ bic = full_bic,
+ name = codes.Institution_name,
+ )
+
+ address = harvest(tablesoup[1])
+ # The address in the SWIFT database includes a postal code.
+ # We need to split it into two fields...
+ if not address.Zip_Code:
+ if address.Location:
+ address.Zip_Code, address.Location = \
+ postalcode.split(full_bic[4:6], address.Location)
+
+ bankaddress = struct(
+ street = address.Address.title(),
+ city = address.Location.strip().title(),
+ zip = address.Zip_Code,
+ country = address.Country.title(),
+ country_id = full_bic[4:6],
+ )
+ if ' ' in bankaddress.street:
+ bankaddress.street, bankaddress.street2 = [
+ x.strip() for x in bankaddress.street.split(' ', 1)
+ ]
+ else:
+ bankaddress.street2 = ''
+
+ return bankinfo, bankaddress
+
diff --git a/account_banking/sepa/postalcode.py b/account_banking/sepa/postalcode.py
new file mode 100644
index 000000000..6fad63bc6
--- /dev/null
+++ b/account_banking/sepa/postalcode.py
@@ -0,0 +1,159 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# Copyright (C) 2009 EduSense BV ().
+# All Rights Reserved
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+'''
+This module provides a utility class to extract postal codes from address
+strings.
+'''
+import re
+
+__all__ = ['split', 'get', 'PostalCode']
+
+class PostalCode(object):
+ '''
+ The PostalCode class is a wrapper around PostCodeFormat and an internal
+ database of postalcode formats. It provides the class methods split() and
+ get(), both of which must be called with the two character iso country
+ code as first parameter.
+ '''
+
+ class PostalCodeFormat(object):
+ '''
+ Utility class of PostalCode.
+ Allows finding and splitting of postalcode in strings
+ '''
+ def __init__(self, format):
+ '''
+ Create regexp patterns for matching
+ '''
+ # Sort formats on length, longest first
+ formats = [(len(x), x) for x in format.split('|')]
+ formats.sort()
+ formats.reverse()
+ formats = [x[1] for x in formats]
+ self.res = [re.compile(x.replace('#', '\\d').replace('@','[A-Z]'))
+ for x in formats
+ ]
+
+ def get(self, str_):
+ '''
+ Return the postal code from the string str_
+ '''
+ for re_ in self.res:
+ retval = re_.findall(str_)
+ if retval:
+ break
+ return retval and retval[0] or ''
+
+ def split(self, str_):
+ '''
+ Split str_ into (postalcode, remainder)
+ '''
+ for re_ in self.res:
+ pos = re_.search(str_)
+ if pos:
+ break
+ if pos:
+ return (pos.group(), str_[pos.end():])
+ return ('', str_)
+
+ _formats = {
+ 'AF': '', 'AX': '', 'AL': '', 'DZ': '#####', 'AS': '', 'AD': 'AD###',
+ 'AO': '', 'AI': '', 'AQ': '', 'AG': '', 'AR': '@####@@@',
+ 'AM': '######', 'AW': '', 'AU': '####', 'AT': '####', 'AZ': 'AZ ####',
+ 'BS': '', 'BH': '####|###', 'BD': '####', 'BB': 'BB#####',
+ 'BY': '######', 'BE': '####', 'BZ': '', 'BJ': '', 'BM': '@@ ##',
+ 'BT': '', 'BO': '', 'BA': '#####', 'BW': '', 'BV': '',
+ 'BR': '#####-###', 'IO': '', 'BN': '@@####', 'BG': '####', 'BF': '',
+ 'BI': '', 'KH': '#####', 'CM': '', 'CA': '@#@ #@#', 'CV': '####',
+ 'KY': '', 'CF': '', 'TD': '', 'CL': '#######', 'CN': '######',
+ 'CX': '####', 'CC': '', 'CO': '', 'KM': '', 'CG': '', 'CD': '',
+ 'CK': '', 'CR': '####', 'CI': '', 'HR': 'HR-#####', 'CU': 'CP #####',
+ 'CY': '####', 'CZ': '### ##', 'DK': '####', 'DJ': '', 'DM': '',
+ 'DO': '#####', 'EC': '@####@', 'EG': '#####', 'SV': 'CP ####',
+ 'GQ': '', 'ER': '', 'EE': '#####', 'ET': '####', 'FK': '',
+ 'FO': 'FO-###', 'FJ': '', 'FI': 'FI-#####', 'FR': '#####',
+ 'GF': '#####', 'PF': '#####', 'TF': '', 'GA': '', 'GM': '',
+ 'GE': '####', 'DE': '#####', 'GH': '', 'GI': '', 'GR': '### ##',
+ 'GL': '####', 'GD': '', 'GP': '#####', 'GU': '969##', 'GT': '#####',
+ 'GG': '@# #@@|@## #@@|@@# #@@|@@## #@@|@#@ #@@|@@#@ #@@|GIR0AA',
+ 'GN': '', 'GW': '####', 'GY': '', 'HT': 'HT####', 'HM': '', 'VA': '',
+ 'HN': '@@####', 'HK': '', 'HU': '####', 'IS': '###', 'IN': '######',
+ 'ID': '#####', 'IR': '##########', 'IQ': '#####', 'IE': '',
+ 'IM': '@# #@@|@## #@@|@@# #@@|@@## #@@|@#@ #@@|@@#@ #@@|GIR0AA',
+ 'IL': '#####', 'IT': '####', 'JM': '', 'JP': '###-####',
+ 'JE': '@# #@@|@## #@@|@@# #@@|@@## #@@|@#@ #@@|@@#@ #@@|GIR0AA',
+ 'JO': '#####', 'KZ': '######', 'KE': '#####', 'KI': '', 'KP': '###-###',
+ 'KR': 'SEOUL ###-###', 'KW': '#####', 'KG': '######', 'LA': '#####',
+ 'LV': 'LV-####', 'LB': '#### ####|####', 'LS': '###', 'LR': '####',
+ 'LY': '', 'LI': '####', 'LT': 'LT-#####', 'LU': '####', 'MO': '',
+ 'MK': '####', 'MG': '###', 'MW': '', 'MY': '#####', 'MV': '#####',
+ 'ML': '', 'MT': '@@@ ###|@@@ ##', 'MH': '', 'MQ': '#####', 'MR': '',
+ 'MU': '', 'YT': '#####', 'MX': '#####', 'FM': '#####', 'MD': 'MD-####',
+ 'MC': '#####', 'MN': '######', 'ME': '#####', 'MS': '', 'MA': '#####',
+ 'MZ': '####', 'MM': '#####', 'NA': '', 'NR': '', 'NP': '#####',
+ 'NL': '#### @@', 'AN': '', 'NC': '#####', 'NZ': '####',
+ 'NI': '###-###-#', 'NE': '####', 'NG': '######', 'NU': '', 'NF': '',
+ 'MP': '', 'NO': '####', 'OM': '###', 'PK': '#####', 'PW': '96940',
+ 'PS': '', 'PA': '', 'PG': '###', 'PY': '####', 'PE': '', 'PH': '####',
+ 'PN': '', 'PL': '##-###', 'PT': '####-###', 'PR': '#####-####',
+ 'QA': '', 'RE': '#####', 'RO': '######', 'RU': '######', 'RW': '',
+ 'BL': '### ###', 'SH': 'STHL 1ZZ', 'KN': '', 'LC': '', 'MF': '### ###',
+ 'PM': '', 'VC': '', 'WS': '', 'SM': '4789#', 'ST': '', 'SA': '#####',
+ 'SN': '#####', 'RS': '######', 'SC': '', 'SL': '', 'SG': '######',
+ 'SK': '### ##', 'SI': 'SI- ####', 'SB': '', 'SO': '@@ #####',
+ 'ZA': '####', 'GS': '', 'ES': '#####', 'LK': '#####', 'SD': '#####',
+ 'SR': '', 'SJ': '', 'SZ': '@###', 'SE': 'SE-### ##', 'CH': '####',
+ 'SY': '', 'TW': '#####', 'TJ': '######', 'TZ': '', 'TH': '#####',
+ 'TL': '', 'TG': '', 'TK': '', 'TO': '', 'TT': '', 'TN': '####',
+ 'TR': '#####', 'TM': '######', 'TC': 'TKCA 1ZZ', 'TV': '', 'UG': '',
+ 'UA': '#####', 'AE': '',
+ 'GB': '@# #@@|@## #@@|@@# #@@|@@## #@@|@#@ #@@|@@#@ #@@|GIR0AA',
+ 'US': '#####-####', 'UM': '', 'UY': '#####', 'UZ': '######', 'VU': '',
+ 'VE': '####', 'VN': '######', 'VG': '', 'VI': '', 'WF': '', 'EH': '',
+ 'YE': '', 'ZM': '#####', 'ZW': ''
+ }
+ for iso, formatstr in _formats.iteritems():
+ _formats[iso] = PostalCodeFormat(formatstr)
+
+ @classmethod
+ def split(cls, iso, str_):
+ '''
+ Split string in (postalcode, remainder) following the specs of
+ country .
+ Returns both the postal code and the remaining part of
+ '''
+ if iso in cls._formats:
+ return cls._formats[iso].split(str_)
+ return ('', str_)
+
+ @classmethod
+ def get(cls, iso, str_):
+ '''
+ Extracts the postal code from str_ following the specs of country
+ .
+ '''
+ if iso in cls._formats:
+ return cls._formats[iso].get(str_)
+ return ''
+
+get = PostalCode.get
+split = PostalCode.split
diff --git a/account_banking/sepa/urlagent.py b/account_banking/sepa/urlagent.py
new file mode 100644
index 000000000..05f867cdb
--- /dev/null
+++ b/account_banking/sepa/urlagent.py
@@ -0,0 +1,221 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# Copyright (C) 2009 EduSense BV ().
+# All Rights Reserved
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+'''
+This module presents a browser like class to browse the web, fill and submit
+forms and to parse the results back in. It is heavily based on BeautifulSoup.
+'''
+
+import urllib
+from BeautifulSoup import BeautifulSoup
+
+__all__ = ['urlsplit', 'urljoin', 'pathbase', 'urlbase', 'SoupForm',
+ 'URLAgent'
+ ]
+
+def urlsplit(url):
+ '''
+ Split an URL into scheme, host and path parts. Helper function.
+ '''
+ if ':' in url:
+ parts = url.split(':')
+ scheme = parts[0]
+ url = ':'.join(parts[1:])
+ else:
+ scheme = ''
+ host, path = urllib.splithost(url)
+ return (scheme, host, path)
+
+def urljoin(scheme, host, path, args = None):
+ '''
+ Join scheme, host and path to a full URL.
+ Optional: add urlencoded args.
+ Helper function.
+ '''
+ url = '%s://%s/%s' % (scheme or 'http', host, path)
+ if args:
+ url += '?%s' % urllib.urlencode(args)
+ return url
+
+def pathbase(path):
+ '''
+ Return the base for the path in order to satisfy relative paths.
+ Helper function.
+ '''
+ if path and '/' in path:
+ return path[:path.rfind('/') +1]
+ return path
+
+def urlbase(url):
+ '''
+ Return the base URL for url in order to satisfy relative paths.
+ Helper function.
+ '''
+ scheme, host, path = urlsplit(url)
+ return urljoin(scheme, host, pathbase(path))
+
+class SoupForm(object):
+ '''
+ A SoupForm is a representation of a HTML Form in BeautifulSoup terms.
+ It has a helper method __setitem__ to set or replace form fields.
+ It gets initiated from a soup object.
+ '''
+ def __init__(self, soup, parent=False):
+ '''
+ Parse the form attributes and fields from the soup. Make sure
+ to get the action right. When parent is set, then the parent
+ element is used as anchor for the search for form elements.
+ '''
+ self._extra_args = {}
+ self.soup = soup
+
+ # Make sure to use base strings, not unicode
+ for attr, value in soup.attrMap.iteritems():
+ setattr(self, str(attr), str(value))
+
+ # Set right anchor point for harvest
+ if parent:
+ self.soup = soup.parent
+
+ # Harvest input elements
+ self._args = {}
+ for item in self.soup.findAll('input'):
+ self._args[str(item.get('name'))] = item.get('value')
+
+ # Harvest url
+ self.scheme, self.host, self.action = urlsplit(self.action)
+ self.action, args = urllib.splitquery(self.action)
+ if args:
+ args = args.split('&')
+ for arg in args:
+ attr, value = urllib.splitvalue(arg)
+ self._extra_args[str(attr)] = value
+
+ def __setitem__(self, name, value, force=False):
+ '''
+ Set values for the form attributes when present
+ '''
+ if name in self._args or force:
+ self._extra_args[name] = value
+ else:
+ raise AttributeError('No such attribute: %s' % name)
+
+ def __getitem__(self, name):
+ '''
+ Get a value. Set values overrule got values.
+ '''
+ if name in self._extra_args:
+ return self._extra_args[name]
+ if name in self._args:
+ return self._args[name]
+ raise AttributeError('No attribute with name "%s" found.' % name)
+
+ def set(self, **kwargs):
+ '''
+ Forcibly sets an attribute to the supplied value, even if it is not
+ part of the parsed form.
+ Can be useful in situations where forms are deliberatly chunked in
+ order to make it difficult to automate form requests, e.g. the
+ SWIFT BIC service, which uses JavaScript to add form attributes to an
+ emtpy base form.
+ '''
+ for name, value in kwargs.iteritems():
+ self.__setitem__(name, value, force=True)
+
+ def args(self):
+ '''
+ Return the field values as attributes, updated with the modified
+ values.
+ '''
+ args = dict(self._args)
+ args.update(self._extra_args)
+ return args
+
+class URLAgent(object):
+ '''
+ Assistent object to ease HTTP(S) requests.
+ Mimics a normal web browser.
+ '''
+
+ def __init__(self, *args, **kwargs):
+ super(URLAgent, self).__init__(*args, **kwargs)
+ self._extra_headers = {}
+ self.headers = {
+ 'User-Agent': 'Mozilla/5.0 (X11; U; Linux x86_64; us; rv:1.9.0.10) Gecko/2009042708 Fedora/3.0.10-1.fc9 Firefox/3.0.10',
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'Accept-Language': 'en-us;q=1.0',
+ 'Accept-Charset': 'UTF-8,*',
+ 'Cache-Control': 'max-age=0'
+ }
+
+ def add_headers(self, **kwargs):
+ self._extra_headers.update(**kwargs)
+
+ def open(self, URL):
+ '''
+ Open a URL and set some vars based on the used URL.
+ Meant to be used on a single server.
+ '''
+ self.scheme, self.host, self.path = urlsplit(URL)
+
+ # Create agent
+ self.agent = urllib.URLopener()
+ headers = self._extra_headers.copy()
+ headers.update(self.headers)
+ for key, value in headers.iteritems():
+ self.agent.addheader(key, value)
+
+ # Open webpage
+ request = self.agent.open(URL)
+
+ # Get and set cookies for next actions
+ attributes = request.info()
+ if attributes.has_key('set-cookie'):
+ self.agent.addheader('Cookie', attributes['set-cookie'])
+
+ # Add referer
+ self.agent.addheader('Referer', URL)
+
+ # Return request
+ return request
+
+ def submit(self, form, action=None, method=None, **kwargs):
+ '''
+ Submit a SoupForm. Override missing attributes in action from our own
+ initial URL.
+ '''
+ if action:
+ scheme, host, path = urlsplit(action)
+ else:
+ scheme = form.scheme or self.scheme
+ host = form.host or self.host
+ action = form.action
+ method = (method or form.method).lower()
+ args = urllib.urlencode(kwargs or form.args())
+
+ if not action.startswith('/'):
+ # Relative path
+ action = pathbase(self.path) + action
+
+ function = getattr(self.agent, 'open_%s' % scheme)
+ if method == 'post':
+ return function('//%s%s' % (host, action), args)
+ return function('//%s%s?%s' % (host, action, args))
diff --git a/account_banking/struct.py b/account_banking/struct.py
new file mode 100644
index 000000000..cd10f2248
--- /dev/null
+++ b/account_banking/struct.py
@@ -0,0 +1,55 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# Copyright (C) 2009 EduSense BV ().
+# All Rights Reserved
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+'''
+Define a struct class which behaves like a dict, but allows using
+object.attr alongside object['attr'].
+'''
+
+__all__ = ['struct']
+
+class struct(dict):
+ '''
+ Ease working with dicts. Allow dict.key alongside dict['key']
+ '''
+ def __setattr__(self, item, value):
+ self.__setitem__(item, value)
+
+ def __getattr__(self, item):
+ return self.__getitem__(item)
+
+ def show(self, indent=0, align=False, ralign=False):
+ '''
+ PrettyPrint method. Aligns keys right (ralign) or left (align).
+ '''
+ if align or ralign:
+ width = 0
+ for key in self.iterkeys():
+ width = max(width, len(key))
+ alignment = ''
+ if not ralign:
+ alignment = '-'
+ fmt = '%*.*s%%%s%d.%ds: %%s' % (
+ indent, indent, '', alignment, width, width
+ )
+ else:
+ fmt = '%*.*s%%s: %%s' % (indent, indent, '')
+ for item in self.iteritems():
+ print fmt % item
diff --git a/account_banking/wizard/banktools.py b/account_banking/wizard/banktools.py
index 0417867f4..c93e13cb3 100644
--- a/account_banking/wizard/banktools.py
+++ b/account_banking/wizard/banktools.py
@@ -19,7 +19,13 @@
#
##############################################################################
+import sys
+import datetime
+import re
from tools.translate import _
+from account_banking.parsers import convert
+from account_banking import sepa
+from account_banking.struct import struct
__all__ = [
'get_period',
@@ -27,42 +33,8 @@ __all__ = [
'get_or_create_partner',
'get_company_bank_account',
'create_bank_account',
- 'struct',
]
-class struct(dict):
- '''
- Ease working with dicts. Allow dict.key alongside dict['key']
- '''
- def __setattr__(self, item, value):
- self.__setitem__(item, value)
-
- def __getattr__(self, item):
- return self.__getitem__(item)
-
- def show(self, indent=0, align=False, ralign=False):
- '''
- PrettyPrint method. Aligns keys right (ralign) or left (align).
- '''
- if align or ralign:
- width = 0
- for key in self.iterkeys():
- width = max(width, len(key))
- alignment = ''
- if not ralign:
- alignment = '-'
- fmt = '%*.*s%%%s%d.%ds: %%s' % (
- indent, indent, '', alignment, width, width
- )
- else:
- fmt = '%*.*s%%s: %%s' % (indent, indent, '')
- for item in self.iteritems():
- print fmt % item
-
-import datetime
-from account_banking import sepa
-from account_banking.parsers.convert import *
-
def get_period(pool, cursor, uid, date, company, log):
'''
Get a suitable period for the given date range and the given company.
@@ -70,9 +42,9 @@ def get_period(pool, cursor, uid, date, company, log):
fiscalyear_obj = pool.get('account.fiscalyear')
period_obj = pool.get('account.period')
if not date:
- date = date2str(datetime.datetime.today())
+ date = convert.date2str(datetime.datetime.today())
- search_date = date2str(date)
+ search_date = convert.date2str(date)
fiscalyear_ids = fiscalyear_obj.search(cursor, uid, [
('date_start','<=', search_date), ('date_stop','>=', search_date),
('state','=','draft'), ('company_id','=',company.id)
@@ -192,41 +164,56 @@ def get_company_bank_account(pool, cursor, uid, account_number,
results.default_credit_account_id = settings.default_credit_account_id
return results
-def get_iban_bic_NL(bank_acc):
+def get_or_create_bank(pool, cursor, uid, bic, online=True):
'''
- Consult the Dutch online banking database to check both the account number
- and the bank to which it belongs. Will not work offline, is limited to
- banks operating in the Netherlands and will only convert Dutch local
- account numbers.
+ Find or create the bank with the provided BIC code.
+ When online, the SWIFT database will be consulted in order to
+ provide for missing information.
'''
- import urllib, urllib2
- from BeautifulSoup import BeautifulSoup
+ bank_obj = pool.get('res.bank')
+ if len(bic) < 8:
+ # search key
+ bank_ids = bank_obj.search(
+ cursor, uid, [
+ ('bic', 'ilike', bic + '%')
+ ])
+ else:
+ bank_ids = bank_obj.search(
+ cursor, uid, [
+ ('bic', '=', bic)
+ ])
+ if bank_ids and len(bank_ids) == 1:
+ return bank_ids[0], bank_ids[0].country
- IBANlink = 'http://www.ibannl.org/iban_check.php'
- data = urllib.urlencode(dict(number=bank_acc, method='POST'))
- request = urllib2.Request(IBANlink, data)
- response = urllib2.urlopen(request)
- soup = BeautifulSoup(response)
- result = struct()
- for _pass, td in enumerate(soup.findAll('td')):
- if _pass % 2 == 1:
- result[attr] = td.find('font').contents[0]
+ country_obj = pool.get('res.country')
+ country_ids = country_obj.search(
+ cursor, uid, [('code', '=', bic[4:6])]
+ )
+ if online:
+ info, address = sepa.online.bank_info(bic)
+ if info:
+ bank_id = bank_obj.create(cursor, uid, dict(
+ code = info.code,
+ name = info.name,
+ street = address.street,
+ street2 = address.street2,
+ zip = address.zip,
+ city = address.city,
+ country = country_ids and country_ids[0] or False,
+ bic = info.bic,
+ ))
else:
- attr = td.find('strong').contents[0][:4].strip().lower()
- if result:
- result.account = bank_acc
- result.country_id = result.bic[4:6]
- # Nationalized bank code
- result.code = result.bic[:6]
- # All Dutch banks use generic channels
- result.bic += 'XXX'
- return result
- return None
+ bank_id = False
-online_account_info = {
- # TODO: Add more online data banks
- 'NL': get_iban_bic_NL,
-}
+ country_id = country_ids and country_ids[0] or False
+ if not online or not bank_id:
+ bank_id = bank_obj.create(cursor, uid, dict(
+ code = info.code,
+ name = _('Unknown Bank'),
+ country = country_id,
+ bic = bic,
+ ))
+ return bank_id, country_id
def create_bank_account(pool, cursor, uid, partner_id,
account_number, holder_name, log
@@ -238,6 +225,9 @@ def create_bank_account(pool, cursor, uid, partner_id,
partner_id = partner_id,
owner_name = holder_name,
)
+ bankcode = None
+ bic = None
+
# Are we dealing with IBAN?
iban = sepa.IBAN(account_number)
if iban.valid:
@@ -250,29 +240,25 @@ def create_bank_account(pool, cursor, uid, partner_id,
cursor, uid, partner_id).country_id
values.state = 'bank'
values.acc_number = account_number
- if country.code in sepa.IBAN.countries \
- and country.code in online_account_info \
- :
- account_info = online_account_info[country.code](values.acc_number)
- if account_info and iban in account_info:
+ if country.code in sepa.IBAN.countries:
+ account_info = sepa.online.account_info(country.code,
+ values.acc_number
+ )
+ if account_info:
values.iban = iban = account_info.iban
values.state = 'iban'
bankcode = account_info.code
bic = account_info.bic
- else:
- bankcode = None
- bic = None
- if bankcode:
+ if bic:
+ values.bank_id = get_or_create_bank(pool, cursor, uid, bic)
+
+ elif bankcode:
# Try to link bank
bank_obj = pool.get('res.bank')
bank_ids = bank_obj.search(cursor, uid, [
('code', 'ilike', bankcode)
])
- if not bank_ids and bic:
- bank_ids = bank_obj.search(cursor, uid, [
- ('bic', 'ilike', bic)
- ])
if bank_ids:
# Check BIC on existing banks
values.bank_id = bank_ids[0]
@@ -283,7 +269,9 @@ def create_bank_account(pool, cursor, uid, partner_id,
# New bank - create
values.bank_id = bank_obj.create(cursor, uid, dict(
code = account_info.code,
- bic = account_info.bic,
+ # Only the first eight positions of BIC are used for bank
+ # transfers, so ditch the rest.
+ bic = account_info.bic[:8],
name = account_info.bank,
country_id = country.id,
))
diff --git a/account_banking_nl_clieop/wizard/clieop.py b/account_banking_nl_clieop/wizard/clieop.py
index 24425e873..38b7a2cf3 100644
--- a/account_banking_nl_clieop/wizard/clieop.py
+++ b/account_banking_nl_clieop/wizard/clieop.py
@@ -21,15 +21,15 @@
from account_banking import record
-__all__ = ['IncassoBatch', 'BetalingsBatch', 'Incasso', 'Betaling',
- 'IncassoFile', 'BetalingsFile', 'SalarisFile',
- 'SalarisbetalingsOpdracht', 'BetalingsOpdracht', 'IncassoOpdracht',
- 'OpdrachtenFile',
+__all__ = ['DirectDebitBatch', 'PaymentsBatch', 'DirectDebit', 'Payment',
+ 'DirectDebitFile', 'PaymentsFile', 'SalaryPaymentsFile',
+ 'SalaryPaymentOrder', 'PaymentOrder', 'DirectDebitOrder',
+ 'OrdersFile',
]
-def elfproef(s):
+def eleven_test(s):
'''
- Dutch elfproef for validating 9-long local bank account numbers.
+ Dutch eleven-test for validating 9-long local bank account numbers.
'''
r = 0
l = len(s)
@@ -42,19 +42,19 @@ class HeaderRecord(record.Record): #{{{
_fields = [
record.Filler('recordcode', 4, '0001'),
record.Filler('variantcode', 1, 'A'),
- record.DateField('aanmaakdatum', '%d%m%y', auto=True),
- record.Filler('bestandsnaam', 8, 'CLIEOP03'),
- record.Field('inzender_id', 5),
- record.Field('bestands_id', 4),
- record.Field('duplicaatcode', 1),
+ record.DateField('creation_date', '%d%m%y', auto=True),
+ record.Filler('filename', 8, 'CLIEOP03'),
+ record.Field('sender_id', 5),
+ record.Field('file_id', 4),
+ record.Field('duplicatecode', 1),
record.Filler('filler', 21),
]
def __init__(self, id='1', volgnr=1, duplicate=False):
super(HeaderRecord, self).__init__()
- self.inzender_id = id
- self.bestands_id = '%02d%02d' % (self.aanmaakdatum.day, volgnr)
- self.duplicaatcode = duplicate and '2' or '1'
+ self.sender_id = id
+ self.file_id = '%02d%02d' % (self.creation_date.day, volgnr)
+ self.duplicatecode = duplicate and '2' or '1'
#}}}
class FooterRecord(record.Record):
@@ -70,10 +70,10 @@ class BatchHeaderRecord(record.Record):
_fields = [
record.Filler('recordcode', 4, '0010'),
record.Field('variantcode', 1),
- record.Field('transactiegroep', 2),
- record.NumberField('rekeningnr_opdrachtgever', 10),
- record.NumberField('batchvolgnummer', 4),
- record.Filler('aanlevermuntsoort', 3, 'EUR'),
+ record.Field('transactiongroup', 2),
+ record.NumberField('accountno_sender', 10),
+ record.NumberField('batch_tracer', 4),
+ record.Filler('currency_order', 3, 'EUR'),
record.Field('batch_id', 16),
record.Filler('filler', 10),
]
@@ -83,112 +83,113 @@ class BatchFooterRecord(record.Record):
_fields = [
record.Filler('recordcode', 4, '9990'),
record.Filler('variantcode', 1, 'A'),
- record.NumberField('totaalbedrag', 18),
- record.NumberField('totaal_rekeningnummers', 10),
- record.NumberField('aantal_posten', 7),
+ record.NumberField('total_amount', 18),
+ record.NumberField('total_accountnos', 10),
+ record.NumberField('nr_posts', 7),
record.Filler('filler', 10),
]
-class VasteOmschrijvingRecord(record.Record):
+class FixedMessageRecord(record.Record):
'''Fixed message'''
_fields = [
record.Filler('recordcode', 4, '0020'),
record.Filler('variantcode', 1, 'A'),
- record.Field('vaste_omschrijving', 32),
+ record.Field('fixed_message', 32),
record.Filler('filler', 13),
]
-class OpdrachtgeverRecord(record.Record):
+class SenderRecord(record.Record):
'''Ordering party'''
_fields = [
record.Filler('recordcode', 4, '0030'),
record.Filler('variantcode', 1, 'B'),
+ # NAW = Name, Address, Residence
record.Field('NAWcode', 1),
- record.DateField('gewenste_verwerkingsdatum', '%d%m%y', auto=True),
- record.Field('naam_opdrachtgever', 35),
+ record.DateField('preferred_execution_date', '%d%m%y', auto=True),
+ record.Field('name_sender', 35),
record.Field('testcode', 1),
record.Filler('filler', 2),
]
-class TransactieRecord(record.Record):
+class TransactionRecord(record.Record):
'''Transaction'''
_fields = [
record.Filler('recordcode', 4, '0100'),
record.Filler('variantcode', 1, 'A'),
- record.NumberField('transactiesoort', 4),
- record.NumberField('bedrag', 12),
- record.NumberField('rekeningnr_betaler', 10),
- record.NumberField('rekeningnr_begunstigde', 10),
+ record.NumberField('transactiontype', 4),
+ record.NumberField('amount', 12),
+ record.NumberField('accountno_payer', 10),
+ record.NumberField('accountno_beneficiary', 10),
record.Filler('filler', 9),
]
-class NaamBetalerRecord(record.Record):
+class NamePayerRecord(record.Record):
'''Name payer'''
_fields = [
record.Filler('recordcode', 4, '0110'),
record.Filler('variantcode', 1, 'B'),
- record.Field('naam', 35),
+ record.Field('name', 35),
record.Filler('filler', 10),
]
-class BetalingskenmerkRecord(record.Record):
+class PaymentReferenceRecord(record.Record):
'''Payment reference'''
_fields = [
record.Filler('recordcode', 4, '0150'),
record.Filler('variantcode', 1, 'A'),
- record.Field('betalingskenmerk', 16),
+ record.Field('paymentreference', 16),
record.Filler('filler', 29),
]
-class OmschrijvingRecord(record.Record):
+class DescriptionRecord(record.Record):
'''Description'''
_fields = [
record.Filler('recordcode', 4, '0160'),
record.Filler('variantcode', 1, 'B'),
- record.Field('omschrijving', 32),
+ record.Field('description', 32),
record.Filler('filler', 13),
]
-class NaamBegunstigdeRecord(record.Record):
+class NameBeneficiaryRecord(record.Record):
'''Name receiving party'''
_fields = [
record.Filler('recordcode', 4, '0170'),
record.Filler('variantcode', 1, 'B'),
- record.Field('naam', 35),
+ record.Field('name', 35),
record.Filler('filler', 10),
]
-class OpdrachtRecord(record.Record):
+class OrderRecord(record.Record):
'''Order details'''
_fields = [
record.Filler('recordcode', 6, 'KAE092'),
- record.Field('naam_transactiecode', 18),
- record.NumberField('totaalbedrag', 13),
- record.Field('rekeningnr_opdrachtgever', 10),
- record.NumberField('totaal_rekeningnummers', 5),
- record.NumberField('aantal_posten', 6),
- record.Field('identificatie', 6),
- record.DateField('gewenste_verwerkingsdatum', '%y%m%d'),
+ record.Field('name_transactioncode', 18),
+ record.NumberField('total_amount', 13),
+ record.Field('accountno_sender', 10),
+ record.NumberField('total_accountnos', 5),
+ record.NumberField('nr_posts', 6),
+ record.Field('identification', 6),
+ record.DateField('preferred_execution_date', '%y%m%d'),
record.Field('batch_medium', 18),
- record.Filler('muntsoort', 3, 'EUR'),
+ record.Filler('currency', 3, 'EUR'),
record.Field('testcode', 1),
]
def __init__(self, *args, **kwargs):
- super(OpdrachtRecord, self).__init__(*args, **kwargs)
+ super(OrderRecord, self).__init__(*args, **kwargs)
self.batch_medium = 'DATACOM'
- self.naam_transactiecode = self._transactiecode
+ self.name_transactioncode = self._transactioncode
-class SalarisbetalingsOpdracht(OpdrachtRecord):
+class SalaryPaymentOrder(OrderRecord):
'''Salary payment batch record'''
- _transactiecode = 'SALARIS'
+ _transactioncode = 'SALARIS'
-class BetalingsOpdracht(OpdrachtRecord):
+class PaymentOrder(OrderRecord):
'''Payment batch record'''
- _transactiecode = 'CREDBET'
+ _transactioncode = 'CREDBET'
-class IncassoOpdracht(OpdrachtRecord):
+class DirectDebitOrder(OrderRecord):
'''Direct debit payments batch record'''
- _transactiecode = 'INCASSO'
+ _transactioncode = 'INCASSO'
class Optional(object):
'''Auxilliary class to handle optional records'''
@@ -218,135 +219,135 @@ class Optional(object):
'''Make sure to adapt'''
return self._guts.__iter__()
-class OpdrachtenFile(object):
+class OrdersFile(object):
'''A payment orders file'''
def __init__(self, *args, **kwargs):
- self.opdrachten = []
+ self.orders = []
@property
def rawdata(self):
'''
Return a writeable file content object
'''
- return '\r\n'.join(self.opdrachten)
+ return '\r\n'.join(self.orders)
-class Transactie(object):
+class Transaction(object):
'''Generic transaction class'''
- def __init__(self, soort=0, naam=None, referentie=None, omschrijvingen=[],
- rekeningnr_begunstigde=None, rekeningnr_betaler=None,
- bedrag=0
+ def __init__(self, type_=0, name=None, reference=None, messages=[],
+ accountno_beneficiary=None, accountno_payer=None,
+ amount=0
):
- self.transactie = TransactieRecord()
- self.betalingskenmerk = Optional(BetalingskenmerkRecord)
- self.omschrijving = Optional(OmschrijvingRecord, 4)
- self.transactie.transactiesoort = soort
- self.transactie.rekeningnr_begunstigde = rekeningnr_begunstigde
- self.transactie.rekeningnr_betaler = rekeningnr_betaler
- self.transactie.bedrag = int(bedrag * 100)
- if referentie:
- self.betalingskenmerk.betalingskenmerk = referentie
- for oms in omschrijvingen:
- self.omschrijving.omschrijving = oms
- self.naam.naam = naam
+ self.transaction = TransactionRecord()
+ self.paymentreference = Optional(PaymentReferenceRecord)
+ self.description = Optional(DescriptionRecord, 4)
+ self.transaction.transactiontype = type_
+ self.transaction.accountno_beneficiary = accountno_beneficiary
+ self.transaction.accountno_payer = accountno_payer
+ self.transaction.amount = int(amount * 100)
+ if reference:
+ self.paymentreference.paymentreference = reference
+ for msg in messages:
+ self.description.description = msg
+ self.name.name = name
-class Incasso(Transactie):
+class DirectDebit(Transaction):
'''Direct Debit Payment transaction'''
def __init__(self, *args, **kwargs):
- reknr = kwargs['rekeningnr_betaler']
- kwargs['soort'] = len(reknr.lstrip('0')) <= 7 and 1002 or 1001
- self.naam = NaamBetalerRecord()
- super(Incasso, self).__init__(*args, **kwargs)
+ reknr = kwargs['accountno_payer']
+ kwargs['type_'] = len(reknr.lstrip('0')) <= 7 and 1002 or 1001
+ self.name = NamePayerRecord()
+ super(DirectDebit, self).__init__(*args, **kwargs)
@property
def rawdata(self):
'''
Return self as writeable file content object
'''
- items = [str(self.transactie)]
- if self.naam:
- items.append(str(self.naam))
- for kenmerk in self.betalingskenmerk:
+ items = [str(self.transaction)]
+ if self.name:
+ items.append(str(self.name))
+ for kenmerk in self.paymentreference:
items.append(str(kenmerk))
- for omschrijving in self.omschrijving:
- items.append(str(omschrijving))
+ for description in self.description:
+ items.append(str(description))
return '\r\n'.join(items)
-class Betaling(Transactie):
+class Payment(Transaction):
'''Payment transaction'''
def __init__(self, *args, **kwargs):
- reknr = kwargs['rekeningnr_begunstigde']
+ reknr = kwargs['accountno_beneficiary']
if len(reknr.lstrip('0')) > 7:
- if not elfproef(reknr):
+ if not eleven_test(reknr):
raise ValueError, '%s is not a valid bank account' % reknr
- kwargs['soort'] = 5
- self.naam = NaamBegunstigdeRecord()
- super(Betaling, self).__init__(*args, **kwargs)
+ kwargs['type_'] = 5
+ self.name = NameBeneficiaryRecord()
+ super(Payment, self).__init__(*args, **kwargs)
@property
def rawdata(self):
'''
Return self as writeable file content object
'''
- items = [str(self.transactie)]
- for kenmerk in self.betalingskenmerk:
+ items = [str(self.transaction)]
+ for kenmerk in self.paymentreference:
items.append(str(kenmerk))
- if self.naam:
- items.append(str(self.naam))
- for omschrijving in self.omschrijving:
- items.append(str(omschrijving))
+ if self.name:
+ items.append(str(self.name))
+ for description in self.description:
+ items.append(str(description))
return '\r\n'.join(items)
-class SalarisBetaling(Betaling):
+class SalaryPayment(Payment):
'''Salary Payment transaction'''
def __init__(self, *args, **kwargs):
- reknr = kwargs['rekeningnr_begunstigde']
- kwargs['soort'] = len(reknr.lstrip('0')) <= 7 and 3 or 8
- super(SalarisBetaling, self).__init__(*args, **kwargs)
+ reknr = kwargs['accountno_beneficiary']
+ kwargs['type_'] = len(reknr.lstrip('0')) <= 7 and 3 or 8
+ super(SalaryPayment, self).__init__(*args, **kwargs)
class Batch(object):
'''Generic batch class'''
- transactieclass = None
+ transactionclass = None
- def __init__(self, opdrachtgever, rekeningnr, verwerkingsdatum=None,
- test=True, omschrijvingen=[], transactiegroep=None,
- batchvolgnummer=1, batch_id=''
+ def __init__(self, sender, rekeningnr, execution_date=None,
+ test=True, messages=[], transactiongroup=None,
+ batch_tracer=1, batch_id=''
):
self.header = BatchHeaderRecord()
- self.vaste_omschrijving = Optional(VasteOmschrijvingRecord, 4)
- self.opdrachtgever = OpdrachtgeverRecord()
+ self.fixed_message = Optional(FixedMessageRecord, 4)
+ self.sender = SenderRecord()
self.footer = BatchFooterRecord()
self.header.variantcode = batch_id and 'C' or 'B'
- self.header.transactiegroep = transactiegroep
- self.header.batchvolgnummer = batchvolgnummer
+ self.header.transactiongroup = transactiongroup
+ self.header.batch_tracer = batch_tracer
self.header.batch_id = batch_id
- self.header.rekeningnr_opdrachtgever = rekeningnr
- self.opdrachtgever.naam_opdrachtgever = opdrachtgever
- self.opdrachtgever.gewenste_verwerkingsdatum = verwerkingsdatum
- self.opdrachtgever.NAWcode = 1
- self.opdrachtgever.testcode = test and 'T' or 'P'
- self.transacties = []
- for omschrijving in omschrijvingen:
- self.vaste_omschrijving.omschrijving = omschrijving
+ self.header.accountno_sender = rekeningnr
+ self.sender.name_sender = sender
+ self.sender.preferred_execution_date = execution_date
+ self.sender.NAWcode = 1
+ self.sender.testcode = test and 'T' or 'P'
+ self.transactions = []
+ for message in messages:
+ self.fixed_message.fixed_message = message
@property
- def aantal_posten(self):
+ def nr_posts(self):
'''nr of posts'''
- return len(self.transacties)
+ return len(self.transactions)
@property
- def totaalbedrag(self):
+ def total_amount(self):
'''total amount transferred'''
- return reduce(lambda x,y: x + int(y.transactie.bedrag),
- self.transacties, 0
+ return reduce(lambda x,y: x + int(y.transaction.amount),
+ self.transactions, 0
)
@property
- def totaal_rekeningnummers(self):
+ def total_accountnos(self):
'''check number on account numbers'''
return reduce(lambda x,y:
- x + int(y.transactie.rekeningnr_betaler) + \
- int(y.transactie.rekeningnr_begunstigde),
- self.transacties, 0
+ x + int(y.transaction.accountno_payer) + \
+ int(y.transaction.accountno_beneficiary),
+ self.transactions, 0
)
@property
@@ -354,49 +355,49 @@ class Batch(object):
'''
Return self as writeable file content object
'''
- self.footer.aantal_posten = self.aantal_posten
- self.footer.totaalbedrag = self.totaalbedrag
- self.footer.totaal_rekeningnummers = self.totaal_rekeningnummers
+ self.footer.nr_posts = self.nr_posts
+ self.footer.total_amount = self.total_amount
+ self.footer.total_accountnos = self.total_accountnos
lines = [str(self.header)]
- for oms in self.vaste_omschrijving:
- lines.append(str(oms))
+ for msg in self.fixed_message:
+ lines.append(str(msg))
lines += [
- str(self.opdrachtgever),
- '\r\n'.join([x.rawdata for x in self.transacties]),
+ str(self.sender),
+ '\r\n'.join([x.rawdata for x in self.transactions]),
str(self.footer)
]
return '\r\n'.join(lines)
- def transactie(self, *args, **kwargs):
+ def transaction(self, *args, **kwargs):
'''generic factory method'''
- retval = self.transactieclass(*args, **kwargs)
- self.transacties.append(retval)
+ retval = self.transactionclass(*args, **kwargs)
+ self.transactions.append(retval)
return retval
-class IncassoBatch(Batch):
+class DirectDebitBatch(Batch):
'''Direct Debig Payment batch'''
- transactieclass = Incasso
+ transactionclass = DirectDebit
-class BetalingsBatch(Batch):
+class PaymentsBatch(Batch):
'''Payment batch'''
- transactieclass = Betaling
+ transactionclass = Payment
class SalarisBatch(Batch):
'''Salary payment class'''
- transactieclass = SalarisBetaling
+ transactionclass = SalaryPayment
class ClieOpFile(object):
'''The grand unifying class'''
- def __init__(self, identificatie='1', uitvoeringsdatum=None,
- naam_opdrachtgever='', rekeningnr_opdrachtgever='',
+ def __init__(self, identification='1', execution_date=None,
+ name_sender='', accountno_sender='',
test=False, **kwargs):
- self.header = HeaderRecord(id=identificatie,)
+ self.header = HeaderRecord(id=identification,)
self.footer = FooterRecord()
self.batches = []
- self._uitvoeringsdatum = uitvoeringsdatum
- self._identificatie = identificatie
- self._naam_opdrachtgever = naam_opdrachtgever
- self._reknr_opdrachtgever = rekeningnr_opdrachtgever
+ self._execution_date = execution_date
+ self._identification = identification
+ self._name_sender = name_sender
+ self._accno_sender = accountno_sender
self._test = test
@property
@@ -410,51 +411,51 @@ class ClieOpFile(object):
def batch(self, *args, **kwargs):
'''Create batch'''
- kwargs['transactiegroep'] = self.transactiegroep
- kwargs['batchvolgnummer'] = len(self.batches) +1
- kwargs['verwerkingsdatum'] = self._uitvoeringsdatum
+ kwargs['transactiongroup'] = self.transactiongroup
+ kwargs['batch_tracer'] = len(self.batches) +1
+ kwargs['execution_date'] = self._execution_date
kwargs['test'] = self._test
- args = (self._naam_opdrachtgever, self._reknr_opdrachtgever)
+ args = (self._name_sender, self._accno_sender)
retval = self.batchclass(*args, **kwargs)
self.batches.append(retval)
return retval
@property
- def opdracht(self):
+ def order(self):
'''Produce an order to go with the file'''
- totaal_rekeningnummers = 0
- totaalbedrag = 0
- aantal_posten = 0
+ total_accountnos = 0
+ total_amount = 0
+ nr_posts = 0
for batch in self.batches:
- totaal_rekeningnummers += batch.totaal_rekeningnummers
- totaalbedrag += batch.totaalbedrag
- aantal_posten += batch.aantal_posten
- retval = self.opdrachtclass()
- retval.identificatie = self._identificatie
- retval.rekeningnr_opdrachtgever = self._reknr_opdrachtgever
- retval.gewenste_verwerkingsdatum = self._uitvoeringsdatum
+ total_accountnos += batch.total_accountnos
+ total_amount += batch.total_amount
+ nr_posts += batch.nr_posts
+ retval = self.orderclass()
+ retval.identification = self._identification
+ retval.accountno_sender = self._accno_sender
+ retval.preferred_execution_date = self._execution_date
retval.testcode = self._test and 'T' or 'P'
- retval.totaalbedrag = totaalbedrag
- retval.aantal_posten = aantal_posten
- retval.totaal_rekeningnummers = totaal_rekeningnummers
+ retval.total_amount = total_amount
+ retval.nr_posts = nr_posts
+ retval.total_accountnos = total_accountnos
return retval
-class IncassoFile(ClieOpFile):
+class DirectDebitFile(ClieOpFile):
'''Direct Debit Payments file'''
- transactiegroep = '10'
- batchclass = IncassoBatch
- opdrachtclass = IncassoOpdracht
+ transactiongroup = '10'
+ batchclass = DirectDebitBatch
+ orderclass = DirectDebitOrder
-class BetalingsFile(ClieOpFile):
+class PaymentsFile(ClieOpFile):
'''Payments file'''
- transactiegroep = '00'
- batchclass = BetalingsBatch
- opdrachtclass = BetalingsOpdracht
+ transactiongroup = '00'
+ batchclass = PaymentsBatch
+ orderclass = PaymentOrder
-class SalarisFile(BetalingsFile):
+class SalaryPaymentsFile(PaymentsFile):
'''Salary Payments file'''
batchclass = SalarisBatch
- opdrachtclass = SalarisbetalingsOpdracht
+ orderclass = SalaryPaymentOrder
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/account_banking_nl_clieop/wizard/export_clieop.py b/account_banking_nl_clieop/wizard/export_clieop.py
index 21a115a92..d32bb4885 100644
--- a/account_banking_nl_clieop/wizard/export_clieop.py
+++ b/account_banking_nl_clieop/wizard/export_clieop.py
@@ -22,9 +22,10 @@
import wizard
import pooler
import base64
+from datetime import datetime, date, timedelta
+from account_banking import sepa
#from osv import osv
from tools.translate import _
-from datetime import datetime, date, timedelta
#import pdb; pdb.set_trace()
import clieop
@@ -102,6 +103,7 @@ file_form = '''
+
'''
@@ -252,60 +254,80 @@ def _create_clieop(self, cursor, uid, data, context):
payment_orders = payment_order_obj.browse(cursor, uid, data['ids'])
for payment_order in payment_orders:
if not clieopfile:
+ # Just once: create clieop file
our_account_owner = payment_order.mode.bank_id.owner_name
our_account_nr = payment_order.mode.bank_id.acc_number
- clieopfile = {'CLIEOPPAY': clieop.BetalingsFile,
- 'CLIEOPINC': clieop.IncassoFile,
- 'CLIEOPSAL': clieop.SalarisFile,
+ if not our_account_nr and payment_order.mode.bank_id.iban:
+ our_account_nr = sepa.IBAN(
+ payment_order.mode.bank_id.iban
+ ).localized_BBAN
+ if not our_account_nr:
+ raise wizard.except_wizard(
+ _('Error'),
+ _('Your bank account has to have a valid account number')
+ )
+ clieopfile = {'CLIEOPPAY': clieop.PaymentsFile,
+ 'CLIEOPINC': clieop.DirectDebitFile,
+ 'CLIEOPSAL': clieop.SalaryPaymentsFile,
}[form['batchtype']](
- identificatie = form['reference'],
- uitvoeringsdatum = form['execution_date'],
- naam_opdrachtgever = our_account_owner,
- rekeningnr_opdrachtgever = our_account_nr,
+ identification = form['reference'],
+ execution_date = form['execution_date'],
+ name_sender = our_account_owner,
+ accountno_sender = our_account_nr,
test = form['test']
)
+ # As payment_orders can have multiple transactions, create a new batch
+ # for each payment_order
if form['fixed_message']:
- omschrijvingen = [form['fixed_message']]
+ messages = [form['fixed_message']]
else:
- omschrijvingen = []
+ messages = []
batch = clieopfile.batch(
- #our_account_owner,
- #our_account_nr,
- #verwerkingsdatum = strpdate(form['execution_date']),
- #test = form['test'],
- omschrijvingen = omschrijvingen,
+ messages = messages,
batch_id = payment_order.reference
)
+
for line in payment_order.line_ids:
kwargs = dict(
- naam = line.bank_id.owner_name,
- bedrag = line.amount_currency,
- referentie = line.communication or None,
+ name = line.bank_id.owner_name,
+ amount = line.amount_currency,
+ reference = line.communication or None,
)
if line.communication2:
- kwargs['omschrijvingen'] = [line.communication2]
- if form['batchtype'] in ['CLIEOPPAY', 'CLIEOPSAL']:
- kwargs['rekeningnr_begunstigde'] = line.bank_id.acc_number
- kwargs['rekeningnr_betaler'] = our_account_nr
+ kwargs['messages'] = [line.communication2]
+ other_account_nr = line.bank_id.acc_number
+ iban = sepa.IBAN(other_account_nr)
+ if iban.valid:
+ if iban.countrycode != 'NL':
+ raise wizard.except_wizard(
+ _('Error'),
+ _('You cannot send international bank transfers '
+ 'through ClieOp3!')
+ )
+ other_account_nr = iban.localized_BBAN
+ if form['batchtype'] == 'CLIEOPINC':
+ kwargs['accountno_beneficiary'] = our_account_nr
+ kwargs['accountno_payer'] = other_account_nr
else:
- kwargs['rekeningnr_begunstigde'] = our_account_nr
- kwargs['rekeningnr_betaler'] = line.bank_id.acc_number
- transaction = batch.transactie(**kwargs)
+ kwargs['accountno_beneficiary'] = other_account_nr
+ kwargs['accountno_payer'] = our_account_nr
+ transaction = batch.transaction(**kwargs)
- opdracht = clieopfile.opdracht
+ # Generate the specifics of this clieopfile
+ order = clieopfile.order
values = dict(
- filetype = opdracht.naam_transactiecode,
- identification = opdracht.identificatie,
- prefered_date = strfdate(opdracht.gewenste_verwerkingsdatum),
- total_amount = int(opdracht.totaalbedrag) / 100.0,
- check_no_accounts = opdracht.totaal_rekeningnummers,
- no_transactions = opdracht.aantal_posten,
- testcode = opdracht.testcode,
+ filetype = order.name_transactioncode,
+ identification = order.identification,
+ prefered_date = strfdate(order.preferred_execution_date),
+ total_amount = int(order.total_amount) / 100.0,
+ check_no_accounts = order.total_accountnos,
+ no_transactions = order.nr_posts,
+ testcode = order.testcode,
file = base64.encodestring(clieopfile.rawdata),
)
form.update(values)
- values['daynumber'] = int(clieopfile.header.bestands_id[2:])
+ values['daynumber'] = int(clieopfile.header.file_id[2:])
values['payment_order_ids'] = ','.join(map(str, data['ids']))
data['file_id'] = pool.get('banking.export.clieop').create(cursor, uid, values)
data['clieop'] = clieopfile
@@ -313,11 +335,17 @@ def _create_clieop(self, cursor, uid, data, context):
return form
def _cancel_clieop(self, cursor, uid, data, context):
+ '''
+ Cancel the ClieOp: just drop the file
+ '''
pool = pooler.get_pool(cursor.dbname)
pool.get('banking.export.clieop').unlink(cursor, uid, data['file_id'])
return {'state': 'end'}
def _save_clieop(self, cursor, uid, data, context):
+ '''
+ Save the ClieOp: mark all payments in the file as 'sent'.
+ '''
pool = pooler.get_pool(cursor.dbname)
clieop_obj = pool.get('banking.export.clieop')
payment_order_obj = pool.get('payment.order')