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/>.
|
||||
#
|
||||
##############################################################################
|
||||
import sepa
|
||||
import record
|
||||
import account_banking
|
||||
import parsers
|
||||
import wizard
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
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 account_banking.parsers import convert
|
||||
from account_banking import sepa
|
||||
from account_banking.struct import struct
|
||||
|
||||
__all__ = [
|
||||
'get_period',
|
||||
@@ -27,42 +33,8 @@ __all__ = [
|
||||
'get_or_create_partner',
|
||||
'get_company_bank_account',
|
||||
'create_bank_account',
|
||||
'struct',
|
||||
]
|
||||
|
||||
class struct(dict):
|
||||
'''
|
||||
Ease working with dicts. Allow dict.key alongside dict['key']
|
||||
'''
|
||||
def __setattr__(self, item, value):
|
||||
self.__setitem__(item, value)
|
||||
|
||||
def __getattr__(self, item):
|
||||
return self.__getitem__(item)
|
||||
|
||||
def show(self, indent=0, align=False, ralign=False):
|
||||
'''
|
||||
PrettyPrint method. Aligns keys right (ralign) or left (align).
|
||||
'''
|
||||
if align or ralign:
|
||||
width = 0
|
||||
for key in self.iterkeys():
|
||||
width = max(width, len(key))
|
||||
alignment = ''
|
||||
if not ralign:
|
||||
alignment = '-'
|
||||
fmt = '%*.*s%%%s%d.%ds: %%s' % (
|
||||
indent, indent, '', alignment, width, width
|
||||
)
|
||||
else:
|
||||
fmt = '%*.*s%%s: %%s' % (indent, indent, '')
|
||||
for item in self.iteritems():
|
||||
print fmt % item
|
||||
|
||||
import datetime
|
||||
from account_banking import sepa
|
||||
from account_banking.parsers.convert import *
|
||||
|
||||
def get_period(pool, cursor, uid, date, company, log):
|
||||
'''
|
||||
Get a suitable period for the given date range and the given company.
|
||||
@@ -70,9 +42,9 @@ def get_period(pool, cursor, uid, date, company, log):
|
||||
fiscalyear_obj = pool.get('account.fiscalyear')
|
||||
period_obj = pool.get('account.period')
|
||||
if not date:
|
||||
date = date2str(datetime.datetime.today())
|
||||
date = convert.date2str(datetime.datetime.today())
|
||||
|
||||
search_date = date2str(date)
|
||||
search_date = convert.date2str(date)
|
||||
fiscalyear_ids = fiscalyear_obj.search(cursor, uid, [
|
||||
('date_start','<=', search_date), ('date_stop','>=', search_date),
|
||||
('state','=','draft'), ('company_id','=',company.id)
|
||||
@@ -192,41 +164,56 @@ def get_company_bank_account(pool, cursor, uid, account_number,
|
||||
results.default_credit_account_id = settings.default_credit_account_id
|
||||
return results
|
||||
|
||||
def get_iban_bic_NL(bank_acc):
|
||||
def get_or_create_bank(pool, cursor, uid, bic, online=True):
|
||||
'''
|
||||
Consult the Dutch online banking database to check both the account number
|
||||
and the bank to which it belongs. Will not work offline, is limited to
|
||||
banks operating in the Netherlands and will only convert Dutch local
|
||||
account numbers.
|
||||
Find or create the bank with the provided BIC code.
|
||||
When online, the SWIFT database will be consulted in order to
|
||||
provide for missing information.
|
||||
'''
|
||||
import urllib, urllib2
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
bank_obj = pool.get('res.bank')
|
||||
if len(bic) < 8:
|
||||
# search key
|
||||
bank_ids = bank_obj.search(
|
||||
cursor, uid, [
|
||||
('bic', 'ilike', bic + '%')
|
||||
])
|
||||
else:
|
||||
bank_ids = bank_obj.search(
|
||||
cursor, uid, [
|
||||
('bic', '=', bic)
|
||||
])
|
||||
if bank_ids and len(bank_ids) == 1:
|
||||
return bank_ids[0], bank_ids[0].country
|
||||
|
||||
IBANlink = 'http://www.ibannl.org/iban_check.php'
|
||||
data = urllib.urlencode(dict(number=bank_acc, method='POST'))
|
||||
request = urllib2.Request(IBANlink, data)
|
||||
response = urllib2.urlopen(request)
|
||||
soup = BeautifulSoup(response)
|
||||
result = struct()
|
||||
for _pass, td in enumerate(soup.findAll('td')):
|
||||
if _pass % 2 == 1:
|
||||
result[attr] = td.find('font').contents[0]
|
||||
country_obj = pool.get('res.country')
|
||||
country_ids = country_obj.search(
|
||||
cursor, uid, [('code', '=', bic[4:6])]
|
||||
)
|
||||
if online:
|
||||
info, address = sepa.online.bank_info(bic)
|
||||
if info:
|
||||
bank_id = bank_obj.create(cursor, uid, dict(
|
||||
code = info.code,
|
||||
name = info.name,
|
||||
street = address.street,
|
||||
street2 = address.street2,
|
||||
zip = address.zip,
|
||||
city = address.city,
|
||||
country = country_ids and country_ids[0] or False,
|
||||
bic = info.bic,
|
||||
))
|
||||
else:
|
||||
attr = td.find('strong').contents[0][:4].strip().lower()
|
||||
if result:
|
||||
result.account = bank_acc
|
||||
result.country_id = result.bic[4:6]
|
||||
# Nationalized bank code
|
||||
result.code = result.bic[:6]
|
||||
# All Dutch banks use generic channels
|
||||
result.bic += 'XXX'
|
||||
return result
|
||||
return None
|
||||
bank_id = False
|
||||
|
||||
online_account_info = {
|
||||
# TODO: Add more online data banks
|
||||
'NL': get_iban_bic_NL,
|
||||
}
|
||||
country_id = country_ids and country_ids[0] or False
|
||||
if not online or not bank_id:
|
||||
bank_id = bank_obj.create(cursor, uid, dict(
|
||||
code = info.code,
|
||||
name = _('Unknown Bank'),
|
||||
country = country_id,
|
||||
bic = bic,
|
||||
))
|
||||
return bank_id, country_id
|
||||
|
||||
def create_bank_account(pool, cursor, uid, partner_id,
|
||||
account_number, holder_name, log
|
||||
@@ -238,6 +225,9 @@ def create_bank_account(pool, cursor, uid, partner_id,
|
||||
partner_id = partner_id,
|
||||
owner_name = holder_name,
|
||||
)
|
||||
bankcode = None
|
||||
bic = None
|
||||
|
||||
# Are we dealing with IBAN?
|
||||
iban = sepa.IBAN(account_number)
|
||||
if iban.valid:
|
||||
@@ -250,29 +240,25 @@ def create_bank_account(pool, cursor, uid, partner_id,
|
||||
cursor, uid, partner_id).country_id
|
||||
values.state = 'bank'
|
||||
values.acc_number = account_number
|
||||
if country.code in sepa.IBAN.countries \
|
||||
and country.code in online_account_info \
|
||||
:
|
||||
account_info = online_account_info[country.code](values.acc_number)
|
||||
if account_info and iban in account_info:
|
||||
if country.code in sepa.IBAN.countries:
|
||||
account_info = sepa.online.account_info(country.code,
|
||||
values.acc_number
|
||||
)
|
||||
if account_info:
|
||||
values.iban = iban = account_info.iban
|
||||
values.state = 'iban'
|
||||
bankcode = account_info.code
|
||||
bic = account_info.bic
|
||||
else:
|
||||
bankcode = None
|
||||
bic = None
|
||||
|
||||
if bankcode:
|
||||
if bic:
|
||||
values.bank_id = get_or_create_bank(pool, cursor, uid, bic)
|
||||
|
||||
elif bankcode:
|
||||
# Try to link bank
|
||||
bank_obj = pool.get('res.bank')
|
||||
bank_ids = bank_obj.search(cursor, uid, [
|
||||
('code', 'ilike', bankcode)
|
||||
])
|
||||
if not bank_ids and bic:
|
||||
bank_ids = bank_obj.search(cursor, uid, [
|
||||
('bic', 'ilike', bic)
|
||||
])
|
||||
if bank_ids:
|
||||
# Check BIC on existing banks
|
||||
values.bank_id = bank_ids[0]
|
||||
@@ -283,7 +269,9 @@ def create_bank_account(pool, cursor, uid, partner_id,
|
||||
# New bank - create
|
||||
values.bank_id = bank_obj.create(cursor, uid, dict(
|
||||
code = account_info.code,
|
||||
bic = account_info.bic,
|
||||
# Only the first eight positions of BIC are used for bank
|
||||
# transfers, so ditch the rest.
|
||||
bic = account_info.bic[:8],
|
||||
name = account_info.bank,
|
||||
country_id = country.id,
|
||||
))
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user