[FIX] account_banking: replace parts of ase_iban for better control

[FIX] account_banking_clieop: use BBAN's when IBAN's are given
[IMP] account_banking: Integrated several online databases for online
                       completion
[IMP] account_banking: Overhaul of base_iban: BBAN and IBAN are now
                       equivalent.
[IMP] account_banking: Better packaging
This commit is contained in:
Pieter J. Kersten
2010-02-04 00:36:03 +01:00
parent 8cdbd4028e
commit 380193e09d
13 changed files with 1231 additions and 297 deletions

View File

@@ -24,6 +24,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
############################################################################## ##############################################################################
import sepa
import record
import account_banking import account_banking
import parsers import parsers
import wizard import wizard

View File

@@ -25,12 +25,12 @@
############################################################################## ##############################################################################
{ {
'name': 'Account Banking', 'name': 'Account Banking',
'version': '0.1', 'version': '0.1.9',
'license': 'GPL-3', 'license': 'GPL-3',
'author': 'EduSense BV', 'author': 'EduSense BV',
'website': 'http://www.edusense.nl', 'website': 'http://www.edusense.nl',
'category': 'Account Banking', 'category': 'Account Banking',
'depends': ['base', 'base_iban', 'account', 'account_payment'], 'depends': ['base', 'account', 'account_payment'],
'init_xml': [], 'init_xml': [],
'update_xml': [ 'update_xml': [
#'security/ir.model.access.csv', #'security/ir.model.access.csv',
@@ -42,8 +42,7 @@
'description': ''' 'description': '''
Module to do banking. Module to do banking.
Note: This module is depending on BeautifulSoup when using the Dutch Note: This module is depending on BeautifulSoup.
online database. Make sure it is installed.
This modules tries to combine all current banking import and export This modules tries to combine all current banking import and export
schemes. Rationale for this is that it is quite common to have foreign 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 + Each bank can have its own pace in introducing SEPA into their
communication with their customers. communication with their customers.
+ National online databases can be used to convert BBAN's to IBAN's. + 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: * Adds dropin extensible import facility for bank communication in:
- Drop-in input parser development. - Drop-in input parser development.

View File

@@ -57,8 +57,11 @@ Modifications are extensive:
default behavior is to flag the orders as 'sent', not as 'done'. default behavior is to flag the orders as 'sent', not as 'done'.
''' '''
import time import time
import sys
import sepa
from osv import osv, fields from osv import osv, fields
from tools.translate import _ from tools.translate import _
from wizard.banktools import get_or_create_bank
class account_banking_account_settings(osv.osv): class account_banking_account_settings(osv.osv):
'''Default Journal for Bank Account''' '''Default Journal for Bank Account'''
@@ -351,7 +354,7 @@ class account_bank_statement_line(osv.osv):
_description = 'Bank Transaction' _description = 'Bank Transaction'
def _get_period(self, cursor, uid, context={}): 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) periods = self.pool.get('account.period').find(cursor, uid, dt=date)
return periods and periods[0] or False return periods and periods[0] or False
@@ -381,7 +384,7 @@ class account_bank_statement_line(osv.osv):
states={'draft': [('readonly', False)]}), states={'draft': [('readonly', False)]}),
'ref': fields.char('Ref.', size=32, readonly=True, 'ref': fields.char('Ref.', size=32, readonly=True,
states={'draft': [('readonly', False)]}), 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)]}), states={'draft': [('readonly', False)]}),
'date': fields.date('Date', required=True, readonly=True, 'date': fields.date('Date', required=True, readonly=True,
states={'draft': [('readonly', False)]}), states={'draft': [('readonly', False)]}),
@@ -750,4 +753,211 @@ class payment_order(osv.osv):
payment_order() 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: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -207,5 +207,66 @@
</field> </field>
</record> </record>
<!-- Set trigger on IBAN and acc_number fields in res_partner_bank form -->
<record id="view_partner_bank_account_banking_form_1" model="ir.ui.view">
<field name="name">res.partner.bank.form.account_banking.inherit</field>
<field name="model">res.partner.bank</field>
<field name="inherit_id" ref="base_iban.view_partner_bank_iban_form"/>
<field name="type">form</field>
<field name="arch" type="xml">
<field name="iban" position="replace">
<field name="iban" on_change="onchange_iban(iban, acc_number)"/>
</field>
</field>
</record>
<record id="view_partner_bank_account_banking_form_2" model="ir.ui.view">
<field name="name">res.partner.bank.form.account_banking.inherit</field>
<field name="model">res.partner.bank</field>
<field name="inherit_id" ref="base_iban.view_partner_bank_iban_form"/>
<field name="type">form</field>
<field name="arch" type="xml">
<field name="acc_number" position="replace">
<field name="acc_number" on_change="onchange_iban(iban, acc_number)"/>
</field>
</field>
</record>
<!-- Set trigger on IBAN and acc_number field in res_partner form -->
<record id="view_partner_account_banking_form_1" model="ir.ui.view">
<field name="name">res.partner.form.account_banking.inherit-1</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base_iban.view_partner_iban_form"/>
<field name="type">form</field>
<field name="arch" type="xml">
<field name="iban" position="replace">
<field name="iban" on_change="onchange_iban(iban, acc_number)"/>
</field>
</field>
</record>
<record id="view_partner_account_banking_form_2" model="ir.ui.view">
<field name="name">res.partner.form.account_banking.inherit-2</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base_iban.view_partner_iban_form"/>
<field name="type">form</field>
<field name="arch" type="xml">
<field name="acc_number" position="replace">
<field name="acc_number" on_change="onchange_iban(iban, acc_number)"/>
</field>
</field>
</record>
<!-- Set trigger on BIC in res_bank form -->
<record id="view_res_bank_account_banking_form_1" model="ir.ui.view">
<field name="name">res.bank.form.account_banking.inherit-1</field>
<field name="model">res.bank</field>
<field name="inherit_id" ref="base.view_res_bank_form"/>
<field name="type">form</field>
<field name="arch" type="xml">
<field name="bic" position="replace">
<field name="bic" on_change="onchange_bic(bic, name)"/>
</field>
</field>
</record>
</data> </data>
</openerp> </openerp>

View File

@@ -0,0 +1,23 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import iban
import online
IBAN = iban.IBAN

View File

@@ -0,0 +1,186 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
'''
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

View File

@@ -0,0 +1,159 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
'''
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 <str_> in (postalcode, remainder) following the specs of
country <iso>.
Returns both the postal code and the remaining part of <str_>
'''
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
<iso>.
'''
if iso in cls._formats:
return cls._formats[iso].get(str_)
return ''
get = PostalCode.get
split = PostalCode.split

View File

@@ -0,0 +1,221 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
'''
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))

55
account_banking/struct.py Normal file
View File

@@ -0,0 +1,55 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
'''
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

View File

@@ -19,7 +19,13 @@
# #
############################################################################## ##############################################################################
import sys
import datetime
import re
from tools.translate import _ from tools.translate import _
from account_banking.parsers import convert
from account_banking import sepa
from account_banking.struct import struct
__all__ = [ __all__ = [
'get_period', 'get_period',
@@ -27,42 +33,8 @@ __all__ = [
'get_or_create_partner', 'get_or_create_partner',
'get_company_bank_account', 'get_company_bank_account',
'create_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): def get_period(pool, cursor, uid, date, company, log):
''' '''
Get a suitable period for the given date range and the given company. 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') fiscalyear_obj = pool.get('account.fiscalyear')
period_obj = pool.get('account.period') period_obj = pool.get('account.period')
if not date: 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, [ fiscalyear_ids = fiscalyear_obj.search(cursor, uid, [
('date_start','<=', search_date), ('date_stop','>=', search_date), ('date_start','<=', search_date), ('date_stop','>=', search_date),
('state','=','draft'), ('company_id','=',company.id) ('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 results.default_credit_account_id = settings.default_credit_account_id
return results 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 Find or create the bank with the provided BIC code.
and the bank to which it belongs. Will not work offline, is limited to When online, the SWIFT database will be consulted in order to
banks operating in the Netherlands and will only convert Dutch local provide for missing information.
account numbers.
''' '''
import urllib, urllib2 bank_obj = pool.get('res.bank')
from BeautifulSoup import BeautifulSoup if len(bic) < 8:
# search key
IBANlink = 'http://www.ibannl.org/iban_check.php' bank_ids = bank_obj.search(
data = urllib.urlencode(dict(number=bank_acc, method='POST')) cursor, uid, [
request = urllib2.Request(IBANlink, data) ('bic', 'ilike', bic + '%')
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]
else: else:
attr = td.find('strong').contents[0][:4].strip().lower() bank_ids = bank_obj.search(
if result: cursor, uid, [
result.account = bank_acc ('bic', '=', bic)
result.country_id = result.bic[4:6] ])
# Nationalized bank code if bank_ids and len(bank_ids) == 1:
result.code = result.bic[:6] return bank_ids[0], bank_ids[0].country
# All Dutch banks use generic channels
result.bic += 'XXX'
return result
return None
online_account_info = { country_obj = pool.get('res.country')
# TODO: Add more online data banks country_ids = country_obj.search(
'NL': get_iban_bic_NL, 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:
bank_id = False
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, def create_bank_account(pool, cursor, uid, partner_id,
account_number, holder_name, log account_number, holder_name, log
@@ -238,6 +225,9 @@ def create_bank_account(pool, cursor, uid, partner_id,
partner_id = partner_id, partner_id = partner_id,
owner_name = holder_name, owner_name = holder_name,
) )
bankcode = None
bic = None
# Are we dealing with IBAN? # Are we dealing with IBAN?
iban = sepa.IBAN(account_number) iban = sepa.IBAN(account_number)
if iban.valid: if iban.valid:
@@ -250,29 +240,25 @@ def create_bank_account(pool, cursor, uid, partner_id,
cursor, uid, partner_id).country_id cursor, uid, partner_id).country_id
values.state = 'bank' values.state = 'bank'
values.acc_number = account_number values.acc_number = account_number
if country.code in sepa.IBAN.countries \ if country.code in sepa.IBAN.countries:
and country.code in online_account_info \ account_info = sepa.online.account_info(country.code,
: values.acc_number
account_info = online_account_info[country.code](values.acc_number) )
if account_info and iban in account_info: if account_info:
values.iban = iban = account_info.iban values.iban = iban = account_info.iban
values.state = 'iban' values.state = 'iban'
bankcode = account_info.code bankcode = account_info.code
bic = account_info.bic 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 # Try to link bank
bank_obj = pool.get('res.bank') bank_obj = pool.get('res.bank')
bank_ids = bank_obj.search(cursor, uid, [ bank_ids = bank_obj.search(cursor, uid, [
('code', 'ilike', bankcode) ('code', 'ilike', bankcode)
]) ])
if not bank_ids and bic:
bank_ids = bank_obj.search(cursor, uid, [
('bic', 'ilike', bic)
])
if bank_ids: if bank_ids:
# Check BIC on existing banks # Check BIC on existing banks
values.bank_id = bank_ids[0] values.bank_id = bank_ids[0]
@@ -283,7 +269,9 @@ def create_bank_account(pool, cursor, uid, partner_id,
# New bank - create # New bank - create
values.bank_id = bank_obj.create(cursor, uid, dict( values.bank_id = bank_obj.create(cursor, uid, dict(
code = account_info.code, 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, name = account_info.bank,
country_id = country.id, country_id = country.id,
)) ))

View File

@@ -21,15 +21,15 @@
from account_banking import record from account_banking import record
__all__ = ['IncassoBatch', 'BetalingsBatch', 'Incasso', 'Betaling', __all__ = ['DirectDebitBatch', 'PaymentsBatch', 'DirectDebit', 'Payment',
'IncassoFile', 'BetalingsFile', 'SalarisFile', 'DirectDebitFile', 'PaymentsFile', 'SalaryPaymentsFile',
'SalarisbetalingsOpdracht', 'BetalingsOpdracht', 'IncassoOpdracht', 'SalaryPaymentOrder', 'PaymentOrder', 'DirectDebitOrder',
'OpdrachtenFile', '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 r = 0
l = len(s) l = len(s)
@@ -42,19 +42,19 @@ class HeaderRecord(record.Record): #{{{
_fields = [ _fields = [
record.Filler('recordcode', 4, '0001'), record.Filler('recordcode', 4, '0001'),
record.Filler('variantcode', 1, 'A'), record.Filler('variantcode', 1, 'A'),
record.DateField('aanmaakdatum', '%d%m%y', auto=True), record.DateField('creation_date', '%d%m%y', auto=True),
record.Filler('bestandsnaam', 8, 'CLIEOP03'), record.Filler('filename', 8, 'CLIEOP03'),
record.Field('inzender_id', 5), record.Field('sender_id', 5),
record.Field('bestands_id', 4), record.Field('file_id', 4),
record.Field('duplicaatcode', 1), record.Field('duplicatecode', 1),
record.Filler('filler', 21), record.Filler('filler', 21),
] ]
def __init__(self, id='1', volgnr=1, duplicate=False): def __init__(self, id='1', volgnr=1, duplicate=False):
super(HeaderRecord, self).__init__() super(HeaderRecord, self).__init__()
self.inzender_id = id self.sender_id = id
self.bestands_id = '%02d%02d' % (self.aanmaakdatum.day, volgnr) self.file_id = '%02d%02d' % (self.creation_date.day, volgnr)
self.duplicaatcode = duplicate and '2' or '1' self.duplicatecode = duplicate and '2' or '1'
#}}} #}}}
class FooterRecord(record.Record): class FooterRecord(record.Record):
@@ -70,10 +70,10 @@ class BatchHeaderRecord(record.Record):
_fields = [ _fields = [
record.Filler('recordcode', 4, '0010'), record.Filler('recordcode', 4, '0010'),
record.Field('variantcode', 1), record.Field('variantcode', 1),
record.Field('transactiegroep', 2), record.Field('transactiongroup', 2),
record.NumberField('rekeningnr_opdrachtgever', 10), record.NumberField('accountno_sender', 10),
record.NumberField('batchvolgnummer', 4), record.NumberField('batch_tracer', 4),
record.Filler('aanlevermuntsoort', 3, 'EUR'), record.Filler('currency_order', 3, 'EUR'),
record.Field('batch_id', 16), record.Field('batch_id', 16),
record.Filler('filler', 10), record.Filler('filler', 10),
] ]
@@ -83,112 +83,113 @@ class BatchFooterRecord(record.Record):
_fields = [ _fields = [
record.Filler('recordcode', 4, '9990'), record.Filler('recordcode', 4, '9990'),
record.Filler('variantcode', 1, 'A'), record.Filler('variantcode', 1, 'A'),
record.NumberField('totaalbedrag', 18), record.NumberField('total_amount', 18),
record.NumberField('totaal_rekeningnummers', 10), record.NumberField('total_accountnos', 10),
record.NumberField('aantal_posten', 7), record.NumberField('nr_posts', 7),
record.Filler('filler', 10), record.Filler('filler', 10),
] ]
class VasteOmschrijvingRecord(record.Record): class FixedMessageRecord(record.Record):
'''Fixed message''' '''Fixed message'''
_fields = [ _fields = [
record.Filler('recordcode', 4, '0020'), record.Filler('recordcode', 4, '0020'),
record.Filler('variantcode', 1, 'A'), record.Filler('variantcode', 1, 'A'),
record.Field('vaste_omschrijving', 32), record.Field('fixed_message', 32),
record.Filler('filler', 13), record.Filler('filler', 13),
] ]
class OpdrachtgeverRecord(record.Record): class SenderRecord(record.Record):
'''Ordering party''' '''Ordering party'''
_fields = [ _fields = [
record.Filler('recordcode', 4, '0030'), record.Filler('recordcode', 4, '0030'),
record.Filler('variantcode', 1, 'B'), record.Filler('variantcode', 1, 'B'),
# NAW = Name, Address, Residence
record.Field('NAWcode', 1), record.Field('NAWcode', 1),
record.DateField('gewenste_verwerkingsdatum', '%d%m%y', auto=True), record.DateField('preferred_execution_date', '%d%m%y', auto=True),
record.Field('naam_opdrachtgever', 35), record.Field('name_sender', 35),
record.Field('testcode', 1), record.Field('testcode', 1),
record.Filler('filler', 2), record.Filler('filler', 2),
] ]
class TransactieRecord(record.Record): class TransactionRecord(record.Record):
'''Transaction''' '''Transaction'''
_fields = [ _fields = [
record.Filler('recordcode', 4, '0100'), record.Filler('recordcode', 4, '0100'),
record.Filler('variantcode', 1, 'A'), record.Filler('variantcode', 1, 'A'),
record.NumberField('transactiesoort', 4), record.NumberField('transactiontype', 4),
record.NumberField('bedrag', 12), record.NumberField('amount', 12),
record.NumberField('rekeningnr_betaler', 10), record.NumberField('accountno_payer', 10),
record.NumberField('rekeningnr_begunstigde', 10), record.NumberField('accountno_beneficiary', 10),
record.Filler('filler', 9), record.Filler('filler', 9),
] ]
class NaamBetalerRecord(record.Record): class NamePayerRecord(record.Record):
'''Name payer''' '''Name payer'''
_fields = [ _fields = [
record.Filler('recordcode', 4, '0110'), record.Filler('recordcode', 4, '0110'),
record.Filler('variantcode', 1, 'B'), record.Filler('variantcode', 1, 'B'),
record.Field('naam', 35), record.Field('name', 35),
record.Filler('filler', 10), record.Filler('filler', 10),
] ]
class BetalingskenmerkRecord(record.Record): class PaymentReferenceRecord(record.Record):
'''Payment reference''' '''Payment reference'''
_fields = [ _fields = [
record.Filler('recordcode', 4, '0150'), record.Filler('recordcode', 4, '0150'),
record.Filler('variantcode', 1, 'A'), record.Filler('variantcode', 1, 'A'),
record.Field('betalingskenmerk', 16), record.Field('paymentreference', 16),
record.Filler('filler', 29), record.Filler('filler', 29),
] ]
class OmschrijvingRecord(record.Record): class DescriptionRecord(record.Record):
'''Description''' '''Description'''
_fields = [ _fields = [
record.Filler('recordcode', 4, '0160'), record.Filler('recordcode', 4, '0160'),
record.Filler('variantcode', 1, 'B'), record.Filler('variantcode', 1, 'B'),
record.Field('omschrijving', 32), record.Field('description', 32),
record.Filler('filler', 13), record.Filler('filler', 13),
] ]
class NaamBegunstigdeRecord(record.Record): class NameBeneficiaryRecord(record.Record):
'''Name receiving party''' '''Name receiving party'''
_fields = [ _fields = [
record.Filler('recordcode', 4, '0170'), record.Filler('recordcode', 4, '0170'),
record.Filler('variantcode', 1, 'B'), record.Filler('variantcode', 1, 'B'),
record.Field('naam', 35), record.Field('name', 35),
record.Filler('filler', 10), record.Filler('filler', 10),
] ]
class OpdrachtRecord(record.Record): class OrderRecord(record.Record):
'''Order details''' '''Order details'''
_fields = [ _fields = [
record.Filler('recordcode', 6, 'KAE092'), record.Filler('recordcode', 6, 'KAE092'),
record.Field('naam_transactiecode', 18), record.Field('name_transactioncode', 18),
record.NumberField('totaalbedrag', 13), record.NumberField('total_amount', 13),
record.Field('rekeningnr_opdrachtgever', 10), record.Field('accountno_sender', 10),
record.NumberField('totaal_rekeningnummers', 5), record.NumberField('total_accountnos', 5),
record.NumberField('aantal_posten', 6), record.NumberField('nr_posts', 6),
record.Field('identificatie', 6), record.Field('identification', 6),
record.DateField('gewenste_verwerkingsdatum', '%y%m%d'), record.DateField('preferred_execution_date', '%y%m%d'),
record.Field('batch_medium', 18), record.Field('batch_medium', 18),
record.Filler('muntsoort', 3, 'EUR'), record.Filler('currency', 3, 'EUR'),
record.Field('testcode', 1), record.Field('testcode', 1),
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(OpdrachtRecord, self).__init__(*args, **kwargs) super(OrderRecord, self).__init__(*args, **kwargs)
self.batch_medium = 'DATACOM' self.batch_medium = 'DATACOM'
self.naam_transactiecode = self._transactiecode self.name_transactioncode = self._transactioncode
class SalarisbetalingsOpdracht(OpdrachtRecord): class SalaryPaymentOrder(OrderRecord):
'''Salary payment batch record''' '''Salary payment batch record'''
_transactiecode = 'SALARIS' _transactioncode = 'SALARIS'
class BetalingsOpdracht(OpdrachtRecord): class PaymentOrder(OrderRecord):
'''Payment batch record''' '''Payment batch record'''
_transactiecode = 'CREDBET' _transactioncode = 'CREDBET'
class IncassoOpdracht(OpdrachtRecord): class DirectDebitOrder(OrderRecord):
'''Direct debit payments batch record''' '''Direct debit payments batch record'''
_transactiecode = 'INCASSO' _transactioncode = 'INCASSO'
class Optional(object): class Optional(object):
'''Auxilliary class to handle optional records''' '''Auxilliary class to handle optional records'''
@@ -218,135 +219,135 @@ class Optional(object):
'''Make sure to adapt''' '''Make sure to adapt'''
return self._guts.__iter__() return self._guts.__iter__()
class OpdrachtenFile(object): class OrdersFile(object):
'''A payment orders file''' '''A payment orders file'''
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.opdrachten = [] self.orders = []
@property @property
def rawdata(self): def rawdata(self):
''' '''
Return a writeable file content object 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''' '''Generic transaction class'''
def __init__(self, soort=0, naam=None, referentie=None, omschrijvingen=[], def __init__(self, type_=0, name=None, reference=None, messages=[],
rekeningnr_begunstigde=None, rekeningnr_betaler=None, accountno_beneficiary=None, accountno_payer=None,
bedrag=0 amount=0
): ):
self.transactie = TransactieRecord() self.transaction = TransactionRecord()
self.betalingskenmerk = Optional(BetalingskenmerkRecord) self.paymentreference = Optional(PaymentReferenceRecord)
self.omschrijving = Optional(OmschrijvingRecord, 4) self.description = Optional(DescriptionRecord, 4)
self.transactie.transactiesoort = soort self.transaction.transactiontype = type_
self.transactie.rekeningnr_begunstigde = rekeningnr_begunstigde self.transaction.accountno_beneficiary = accountno_beneficiary
self.transactie.rekeningnr_betaler = rekeningnr_betaler self.transaction.accountno_payer = accountno_payer
self.transactie.bedrag = int(bedrag * 100) self.transaction.amount = int(amount * 100)
if referentie: if reference:
self.betalingskenmerk.betalingskenmerk = referentie self.paymentreference.paymentreference = reference
for oms in omschrijvingen: for msg in messages:
self.omschrijving.omschrijving = oms self.description.description = msg
self.naam.naam = naam self.name.name = name
class Incasso(Transactie): class DirectDebit(Transaction):
'''Direct Debit Payment transaction''' '''Direct Debit Payment transaction'''
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
reknr = kwargs['rekeningnr_betaler'] reknr = kwargs['accountno_payer']
kwargs['soort'] = len(reknr.lstrip('0')) <= 7 and 1002 or 1001 kwargs['type_'] = len(reknr.lstrip('0')) <= 7 and 1002 or 1001
self.naam = NaamBetalerRecord() self.name = NamePayerRecord()
super(Incasso, self).__init__(*args, **kwargs) super(DirectDebit, self).__init__(*args, **kwargs)
@property @property
def rawdata(self): def rawdata(self):
''' '''
Return self as writeable file content object Return self as writeable file content object
''' '''
items = [str(self.transactie)] items = [str(self.transaction)]
if self.naam: if self.name:
items.append(str(self.naam)) items.append(str(self.name))
for kenmerk in self.betalingskenmerk: for kenmerk in self.paymentreference:
items.append(str(kenmerk)) items.append(str(kenmerk))
for omschrijving in self.omschrijving: for description in self.description:
items.append(str(omschrijving)) items.append(str(description))
return '\r\n'.join(items) return '\r\n'.join(items)
class Betaling(Transactie): class Payment(Transaction):
'''Payment transaction''' '''Payment transaction'''
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
reknr = kwargs['rekeningnr_begunstigde'] reknr = kwargs['accountno_beneficiary']
if len(reknr.lstrip('0')) > 7: 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 raise ValueError, '%s is not a valid bank account' % reknr
kwargs['soort'] = 5 kwargs['type_'] = 5
self.naam = NaamBegunstigdeRecord() self.name = NameBeneficiaryRecord()
super(Betaling, self).__init__(*args, **kwargs) super(Payment, self).__init__(*args, **kwargs)
@property @property
def rawdata(self): def rawdata(self):
''' '''
Return self as writeable file content object Return self as writeable file content object
''' '''
items = [str(self.transactie)] items = [str(self.transaction)]
for kenmerk in self.betalingskenmerk: for kenmerk in self.paymentreference:
items.append(str(kenmerk)) items.append(str(kenmerk))
if self.naam: if self.name:
items.append(str(self.naam)) items.append(str(self.name))
for omschrijving in self.omschrijving: for description in self.description:
items.append(str(omschrijving)) items.append(str(description))
return '\r\n'.join(items) return '\r\n'.join(items)
class SalarisBetaling(Betaling): class SalaryPayment(Payment):
'''Salary Payment transaction''' '''Salary Payment transaction'''
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
reknr = kwargs['rekeningnr_begunstigde'] reknr = kwargs['accountno_beneficiary']
kwargs['soort'] = len(reknr.lstrip('0')) <= 7 and 3 or 8 kwargs['type_'] = len(reknr.lstrip('0')) <= 7 and 3 or 8
super(SalarisBetaling, self).__init__(*args, **kwargs) super(SalaryPayment, self).__init__(*args, **kwargs)
class Batch(object): class Batch(object):
'''Generic batch class''' '''Generic batch class'''
transactieclass = None transactionclass = None
def __init__(self, opdrachtgever, rekeningnr, verwerkingsdatum=None, def __init__(self, sender, rekeningnr, execution_date=None,
test=True, omschrijvingen=[], transactiegroep=None, test=True, messages=[], transactiongroup=None,
batchvolgnummer=1, batch_id='' batch_tracer=1, batch_id=''
): ):
self.header = BatchHeaderRecord() self.header = BatchHeaderRecord()
self.vaste_omschrijving = Optional(VasteOmschrijvingRecord, 4) self.fixed_message = Optional(FixedMessageRecord, 4)
self.opdrachtgever = OpdrachtgeverRecord() self.sender = SenderRecord()
self.footer = BatchFooterRecord() self.footer = BatchFooterRecord()
self.header.variantcode = batch_id and 'C' or 'B' self.header.variantcode = batch_id and 'C' or 'B'
self.header.transactiegroep = transactiegroep self.header.transactiongroup = transactiongroup
self.header.batchvolgnummer = batchvolgnummer self.header.batch_tracer = batch_tracer
self.header.batch_id = batch_id self.header.batch_id = batch_id
self.header.rekeningnr_opdrachtgever = rekeningnr self.header.accountno_sender = rekeningnr
self.opdrachtgever.naam_opdrachtgever = opdrachtgever self.sender.name_sender = sender
self.opdrachtgever.gewenste_verwerkingsdatum = verwerkingsdatum self.sender.preferred_execution_date = execution_date
self.opdrachtgever.NAWcode = 1 self.sender.NAWcode = 1
self.opdrachtgever.testcode = test and 'T' or 'P' self.sender.testcode = test and 'T' or 'P'
self.transacties = [] self.transactions = []
for omschrijving in omschrijvingen: for message in messages:
self.vaste_omschrijving.omschrijving = omschrijving self.fixed_message.fixed_message = message
@property @property
def aantal_posten(self): def nr_posts(self):
'''nr of posts''' '''nr of posts'''
return len(self.transacties) return len(self.transactions)
@property @property
def totaalbedrag(self): def total_amount(self):
'''total amount transferred''' '''total amount transferred'''
return reduce(lambda x,y: x + int(y.transactie.bedrag), return reduce(lambda x,y: x + int(y.transaction.amount),
self.transacties, 0 self.transactions, 0
) )
@property @property
def totaal_rekeningnummers(self): def total_accountnos(self):
'''check number on account numbers''' '''check number on account numbers'''
return reduce(lambda x,y: return reduce(lambda x,y:
x + int(y.transactie.rekeningnr_betaler) + \ x + int(y.transaction.accountno_payer) + \
int(y.transactie.rekeningnr_begunstigde), int(y.transaction.accountno_beneficiary),
self.transacties, 0 self.transactions, 0
) )
@property @property
@@ -354,49 +355,49 @@ class Batch(object):
''' '''
Return self as writeable file content object Return self as writeable file content object
''' '''
self.footer.aantal_posten = self.aantal_posten self.footer.nr_posts = self.nr_posts
self.footer.totaalbedrag = self.totaalbedrag self.footer.total_amount = self.total_amount
self.footer.totaal_rekeningnummers = self.totaal_rekeningnummers self.footer.total_accountnos = self.total_accountnos
lines = [str(self.header)] lines = [str(self.header)]
for oms in self.vaste_omschrijving: for msg in self.fixed_message:
lines.append(str(oms)) lines.append(str(msg))
lines += [ lines += [
str(self.opdrachtgever), str(self.sender),
'\r\n'.join([x.rawdata for x in self.transacties]), '\r\n'.join([x.rawdata for x in self.transactions]),
str(self.footer) str(self.footer)
] ]
return '\r\n'.join(lines) return '\r\n'.join(lines)
def transactie(self, *args, **kwargs): def transaction(self, *args, **kwargs):
'''generic factory method''' '''generic factory method'''
retval = self.transactieclass(*args, **kwargs) retval = self.transactionclass(*args, **kwargs)
self.transacties.append(retval) self.transactions.append(retval)
return retval return retval
class IncassoBatch(Batch): class DirectDebitBatch(Batch):
'''Direct Debig Payment batch''' '''Direct Debig Payment batch'''
transactieclass = Incasso transactionclass = DirectDebit
class BetalingsBatch(Batch): class PaymentsBatch(Batch):
'''Payment batch''' '''Payment batch'''
transactieclass = Betaling transactionclass = Payment
class SalarisBatch(Batch): class SalarisBatch(Batch):
'''Salary payment class''' '''Salary payment class'''
transactieclass = SalarisBetaling transactionclass = SalaryPayment
class ClieOpFile(object): class ClieOpFile(object):
'''The grand unifying class''' '''The grand unifying class'''
def __init__(self, identificatie='1', uitvoeringsdatum=None, def __init__(self, identification='1', execution_date=None,
naam_opdrachtgever='', rekeningnr_opdrachtgever='', name_sender='', accountno_sender='',
test=False, **kwargs): test=False, **kwargs):
self.header = HeaderRecord(id=identificatie,) self.header = HeaderRecord(id=identification,)
self.footer = FooterRecord() self.footer = FooterRecord()
self.batches = [] self.batches = []
self._uitvoeringsdatum = uitvoeringsdatum self._execution_date = execution_date
self._identificatie = identificatie self._identification = identification
self._naam_opdrachtgever = naam_opdrachtgever self._name_sender = name_sender
self._reknr_opdrachtgever = rekeningnr_opdrachtgever self._accno_sender = accountno_sender
self._test = test self._test = test
@property @property
@@ -410,51 +411,51 @@ class ClieOpFile(object):
def batch(self, *args, **kwargs): def batch(self, *args, **kwargs):
'''Create batch''' '''Create batch'''
kwargs['transactiegroep'] = self.transactiegroep kwargs['transactiongroup'] = self.transactiongroup
kwargs['batchvolgnummer'] = len(self.batches) +1 kwargs['batch_tracer'] = len(self.batches) +1
kwargs['verwerkingsdatum'] = self._uitvoeringsdatum kwargs['execution_date'] = self._execution_date
kwargs['test'] = self._test kwargs['test'] = self._test
args = (self._naam_opdrachtgever, self._reknr_opdrachtgever) args = (self._name_sender, self._accno_sender)
retval = self.batchclass(*args, **kwargs) retval = self.batchclass(*args, **kwargs)
self.batches.append(retval) self.batches.append(retval)
return retval return retval
@property @property
def opdracht(self): def order(self):
'''Produce an order to go with the file''' '''Produce an order to go with the file'''
totaal_rekeningnummers = 0 total_accountnos = 0
totaalbedrag = 0 total_amount = 0
aantal_posten = 0 nr_posts = 0
for batch in self.batches: for batch in self.batches:
totaal_rekeningnummers += batch.totaal_rekeningnummers total_accountnos += batch.total_accountnos
totaalbedrag += batch.totaalbedrag total_amount += batch.total_amount
aantal_posten += batch.aantal_posten nr_posts += batch.nr_posts
retval = self.opdrachtclass() retval = self.orderclass()
retval.identificatie = self._identificatie retval.identification = self._identification
retval.rekeningnr_opdrachtgever = self._reknr_opdrachtgever retval.accountno_sender = self._accno_sender
retval.gewenste_verwerkingsdatum = self._uitvoeringsdatum retval.preferred_execution_date = self._execution_date
retval.testcode = self._test and 'T' or 'P' retval.testcode = self._test and 'T' or 'P'
retval.totaalbedrag = totaalbedrag retval.total_amount = total_amount
retval.aantal_posten = aantal_posten retval.nr_posts = nr_posts
retval.totaal_rekeningnummers = totaal_rekeningnummers retval.total_accountnos = total_accountnos
return retval return retval
class IncassoFile(ClieOpFile): class DirectDebitFile(ClieOpFile):
'''Direct Debit Payments file''' '''Direct Debit Payments file'''
transactiegroep = '10' transactiongroup = '10'
batchclass = IncassoBatch batchclass = DirectDebitBatch
opdrachtclass = IncassoOpdracht orderclass = DirectDebitOrder
class BetalingsFile(ClieOpFile): class PaymentsFile(ClieOpFile):
'''Payments file''' '''Payments file'''
transactiegroep = '00' transactiongroup = '00'
batchclass = BetalingsBatch batchclass = PaymentsBatch
opdrachtclass = BetalingsOpdracht orderclass = PaymentOrder
class SalarisFile(BetalingsFile): class SalaryPaymentsFile(PaymentsFile):
'''Salary Payments file''' '''Salary Payments file'''
batchclass = SalarisBatch batchclass = SalarisBatch
opdrachtclass = SalarisbetalingsOpdracht orderclass = SalaryPaymentOrder
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -22,9 +22,10 @@
import wizard import wizard
import pooler import pooler
import base64 import base64
from datetime import datetime, date, timedelta
from account_banking import sepa
#from osv import osv #from osv import osv
from tools.translate import _ from tools.translate import _
from datetime import datetime, date, timedelta
#import pdb; pdb.set_trace() #import pdb; pdb.set_trace()
import clieop import clieop
@@ -102,6 +103,7 @@ file_form = '''<?xml version="1.0"?>
<field name="no_transactions" /> <field name="no_transactions" />
<field name="prefered_date" /> <field name="prefered_date" />
<field name="testcode" /> <field name="testcode" />
<newline/>
<field name="file" /> <field name="file" />
<field name="log" colspan="4" nolabel="1" /> <field name="log" colspan="4" nolabel="1" />
</form>''' </form>'''
@@ -252,60 +254,80 @@ def _create_clieop(self, cursor, uid, data, context):
payment_orders = payment_order_obj.browse(cursor, uid, data['ids']) payment_orders = payment_order_obj.browse(cursor, uid, data['ids'])
for payment_order in payment_orders: for payment_order in payment_orders:
if not clieopfile: if not clieopfile:
# Just once: create clieop file
our_account_owner = payment_order.mode.bank_id.owner_name our_account_owner = payment_order.mode.bank_id.owner_name
our_account_nr = payment_order.mode.bank_id.acc_number our_account_nr = payment_order.mode.bank_id.acc_number
clieopfile = {'CLIEOPPAY': clieop.BetalingsFile, if not our_account_nr and payment_order.mode.bank_id.iban:
'CLIEOPINC': clieop.IncassoFile, our_account_nr = sepa.IBAN(
'CLIEOPSAL': clieop.SalarisFile, 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']]( }[form['batchtype']](
identificatie = form['reference'], identification = form['reference'],
uitvoeringsdatum = form['execution_date'], execution_date = form['execution_date'],
naam_opdrachtgever = our_account_owner, name_sender = our_account_owner,
rekeningnr_opdrachtgever = our_account_nr, accountno_sender = our_account_nr,
test = form['test'] test = form['test']
) )
# As payment_orders can have multiple transactions, create a new batch
# for each payment_order
if form['fixed_message']: if form['fixed_message']:
omschrijvingen = [form['fixed_message']] messages = [form['fixed_message']]
else: else:
omschrijvingen = [] messages = []
batch = clieopfile.batch( batch = clieopfile.batch(
#our_account_owner, messages = messages,
#our_account_nr,
#verwerkingsdatum = strpdate(form['execution_date']),
#test = form['test'],
omschrijvingen = omschrijvingen,
batch_id = payment_order.reference batch_id = payment_order.reference
) )
for line in payment_order.line_ids: for line in payment_order.line_ids:
kwargs = dict( kwargs = dict(
naam = line.bank_id.owner_name, name = line.bank_id.owner_name,
bedrag = line.amount_currency, amount = line.amount_currency,
referentie = line.communication or None, reference = line.communication or None,
) )
if line.communication2: if line.communication2:
kwargs['omschrijvingen'] = [line.communication2] kwargs['messages'] = [line.communication2]
if form['batchtype'] in ['CLIEOPPAY', 'CLIEOPSAL']: other_account_nr = line.bank_id.acc_number
kwargs['rekeningnr_begunstigde'] = line.bank_id.acc_number iban = sepa.IBAN(other_account_nr)
kwargs['rekeningnr_betaler'] = our_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: else:
kwargs['rekeningnr_begunstigde'] = our_account_nr kwargs['accountno_beneficiary'] = other_account_nr
kwargs['rekeningnr_betaler'] = line.bank_id.acc_number kwargs['accountno_payer'] = our_account_nr
transaction = batch.transactie(**kwargs) transaction = batch.transaction(**kwargs)
opdracht = clieopfile.opdracht # Generate the specifics of this clieopfile
order = clieopfile.order
values = dict( values = dict(
filetype = opdracht.naam_transactiecode, filetype = order.name_transactioncode,
identification = opdracht.identificatie, identification = order.identification,
prefered_date = strfdate(opdracht.gewenste_verwerkingsdatum), prefered_date = strfdate(order.preferred_execution_date),
total_amount = int(opdracht.totaalbedrag) / 100.0, total_amount = int(order.total_amount) / 100.0,
check_no_accounts = opdracht.totaal_rekeningnummers, check_no_accounts = order.total_accountnos,
no_transactions = opdracht.aantal_posten, no_transactions = order.nr_posts,
testcode = opdracht.testcode, testcode = order.testcode,
file = base64.encodestring(clieopfile.rawdata), file = base64.encodestring(clieopfile.rawdata),
) )
form.update(values) 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'])) values['payment_order_ids'] = ','.join(map(str, data['ids']))
data['file_id'] = pool.get('banking.export.clieop').create(cursor, uid, values) data['file_id'] = pool.get('banking.export.clieop').create(cursor, uid, values)
data['clieop'] = clieopfile data['clieop'] = clieopfile
@@ -313,11 +335,17 @@ def _create_clieop(self, cursor, uid, data, context):
return form return form
def _cancel_clieop(self, cursor, uid, data, context): def _cancel_clieop(self, cursor, uid, data, context):
'''
Cancel the ClieOp: just drop the file
'''
pool = pooler.get_pool(cursor.dbname) pool = pooler.get_pool(cursor.dbname)
pool.get('banking.export.clieop').unlink(cursor, uid, data['file_id']) pool.get('banking.export.clieop').unlink(cursor, uid, data['file_id'])
return {'state': 'end'} return {'state': 'end'}
def _save_clieop(self, cursor, uid, data, context): 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) pool = pooler.get_pool(cursor.dbname)
clieop_obj = pool.get('banking.export.clieop') clieop_obj = pool.get('banking.export.clieop')
payment_order_obj = pool.get('payment.order') payment_order_obj = pool.get('payment.order')