mirror of
https://github.com/OCA/bank-payment.git
synced 2025-02-02 10:37:31 +02:00
[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:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
23
account_banking/sepa/__init__.py
Normal file
23
account_banking/sepa/__init__.py
Normal 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
|
||||||
186
account_banking/sepa/online.py
Normal file
186
account_banking/sepa/online.py
Normal 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
|
||||||
|
|
||||||
159
account_banking/sepa/postalcode.py
Normal file
159
account_banking/sepa/postalcode.py
Normal 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
|
||||||
221
account_banking/sepa/urlagent.py
Normal file
221
account_banking/sepa/urlagent.py
Normal 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
55
account_banking/struct.py
Normal 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
|
||||||
@@ -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,
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user