[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/>.
#
##############################################################################
import sepa
import record
import account_banking
import parsers
import wizard

View File

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

View File

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

View File

@@ -207,5 +207,66 @@
</field>
</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>
</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 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
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]
bank_obj = pool.get('res.bank')
if len(bic) < 8:
# search key
bank_ids = bank_obj.search(
cursor, uid, [
('bic', 'ilike', 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_ids = bank_obj.search(
cursor, uid, [
('bic', '=', bic)
])
if bank_ids and len(bank_ids) == 1:
return bank_ids[0], bank_ids[0].country
online_account_info = {
# TODO: Add more online data banks
'NL': get_iban_bic_NL,
}
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:
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,
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,
))

View File

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

View File

@@ -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 = '''<?xml version="1.0"?>
<field name="no_transactions" />
<field name="prefered_date" />
<field name="testcode" />
<newline/>
<field name="file" />
<field name="log" colspan="4" nolabel="1" />
</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')