UK HSBC module

This commit is contained in:
Dmitrijs Ledkovs (credativ)
2012-02-10 13:33:01 +00:00
12 changed files with 1712 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
# All Rights Reserved
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import account_banking_uk_hsbc
import wizard
import hsbc_mt940
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -0,0 +1,50 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
# All Rights Reserved
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'HSBC Account Banking',
'version': '0.4',
'license': 'AGPL-3',
'author': 'credativ Ltd',
'website': 'http://www.credativ.co.uk',
'category': 'Account Banking',
'depends': ['account_banking'],
'init_xml': [],
'update_xml': [
'account_banking_uk_hsbc.xml',
'data/banking_export_hsbc.xml',
'wizard/export_hsbc_view.xml',
],
'demo_xml': [],
'description': '''
Module to import HSBC format transation files (S.W.I.F.T MT940) and to export payments for HSBC.net (PAYMUL).
Currently it is targetting UK market, due to country variances of the MT940 and PAYMUL.
It is possible to extend this module to work with HSBC.net in other countries and potentially other banks.
This module adds above import/export filter to the account_banking module.
All business logic is in account_banking module.
Initial release of this module was co-sponsored by canonical.
''',
'active': False,
'installable': True,
}

View File

@@ -0,0 +1,65 @@
##############################################################################
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
# 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/>.
#
##############################################################################
from osv import osv, fields
from datetime import date
from tools.translate import _
class hsbc_export(osv.osv):
'''HSBC Export'''
_name = 'banking.export.hsbc'
_description = __doc__
_rec_name = 'execution_date'
_columns = {
'payment_order_ids': fields.many2many(
'payment.order',
'account_payment_order_hsbc_rel',
'banking_export_hsbc_id', 'account_order_id',
'Payment Orders',
readonly=True),
'identification':
fields.char('Identification', size=15, readonly=True, select=True),
'execution_date':
fields.date('Execution Date',readonly=True),
'no_transactions':
fields.integer('Number of Transactions', readonly=True),
'total_amount':
fields.float('Total Amount', readonly=True),
'date_generated':
fields.datetime('Generation Date', readonly=True, select=True),
'file':
fields.binary('HSBC File', readonly=True),
'state':
fields.selection([
('draft', 'Draft'),
('sent', 'Sent'),
('done', 'Reconciled'),
], 'State', readonly=True),
}
_defaults = {
'date_generated': lambda *a: date.today().strftime('%Y-%m-%d'),
'state': lambda *a: 'draft',
}
hsbc_export()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) EduSense BV <http://www.edusense.nl>
All rights reserved.
Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>)
The licence is in the file __openerp__.py
-->
<openerp>
<data>
<!-- Make new view on HSBC exports -->
<record id="view_banking_export_hsbc_form" model="ir.ui.view">
<field name="name">account.banking.export.hsbc.form</field>
<field name="model">banking.export.hsbc</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="HSBC Export">
<notebook>
<page string="General Information">
<separator string="HSBC Information" colspan="4" />
<field name="total_amount" />
<field name="no_transactions" />
<separator string="Processing Information" colspan="4" />
<field name="execution_date" />
<field name="date_generated" />
<newline />
<field name="file" colspan="4" />
</page>
<page string="Payment Orders">
<field name="payment_order_ids" colspan="4" nolabel="1">
<tree colors="blue:state in ('draft');gray:state in ('cancel','done');black:state in ('open')" string="Payment order">
<field name="reference"/>
<field name="date_created"/>
<field name="date_done"/>
<field name="total"/>
<field name="state"/>
</tree>
</field>
</page>
</notebook>
</form>
</field>
</record>
<record id="view_banking_export_hsbc_tree" model="ir.ui.view">
<field name="name">account.banking.export.hsbc.tree</field>
<field name="model">banking.export.hsbc</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="HSBC Export">
<field name="execution_date" search="2"/>
<field name="date_generated" />
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="action_account_banking_hsbcs">
<field name="name">Generated HSBC files</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">banking.export.hsbc</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Add a menu item for it -->
<menuitem name="Generated HSBC files"
id="menu_action_account_banking_exported_hsbc_files"
parent="account_banking.menu_finance_banking_actions"
action="action_account_banking_hsbcs"
sequence="12"
/>
<!-- Create right menu entry to see generated files -->
<act_window name="Generated HSBC files"
domain="[('payment_order_ids', '=', active_id)]"
res_model="banking.export.hsbc"
src_model="payment.order"
view_type="form"
view_mode="tree,form"
id="act_banking_export_hsbc_payment_order"/>
</data>
</openerp>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record model="payment.mode.type" id="export_acm_or_ezone">
<field name="name">ACH or EZONE</field>
<field name="code">not used</field>
<field name="suitable_bank_types"
eval="[(6,0,[ref('base_iban.bank_iban'),ref('base.bank_normal'),])]" />
<field name="ir_model_id"
ref="account_banking_uk_hsbc.model_banking_export_hsbc_wizard"/>
</record>
<record model="payment.mode.type" id="export_faster_payment">
<field name="name">Faster Payment</field>
<field name="code">not used</field>
<field name="suitable_bank_types"
eval="[(6,0,[ref('base.bank_normal'),])]" />
<field name="ir_model_id"
ref="account_banking_uk_hsbc.model_banking_export_hsbc_wizard"/>
</record>
</data>
</openerp>

View File

@@ -0,0 +1,161 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
# All Rights Reserved
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
# Import of HSBC data in Swift MT940 format
#
from account_banking.parsers import models
from account_banking.parsers.convert import str2date
from tools.translate import _
from mt940_parser import HSBCParser
import re
bt = models.mem_bank_transaction
def record2float(record, value):
if record['creditmarker'][-1] == 'C':
return float(record[value])
return -float(record[value])
class transaction(models.mem_bank_transaction):
mapping = {
'execution_date' : 'valuedate',
'effective_date' : 'bookingdate',
'local_currency' : 'currency',
'transfer_type' : 'bookingcode',
'reference' : 'custrefno',
'message' : 'furtherinfo'
}
type_map = {
'TRF': bt.ORDER,
}
def __init__(self, record, *args, **kwargs):
'''
Transaction creation
'''
super(transaction, self).__init__(*args, **kwargs)
for key, value in self.mapping.iteritems():
if record.has_key(value):
setattr(self, key, record[value])
self.transferred_amount = record2float(record, 'amount')
#print record.get('bookingcode')
if not self.is_valid():
print "Invalid: %s" % record
def is_valid(self):
'''
We don't have remote_account so override base
'''
return (self.execution_date
and self.transferred_amount and True) or False
class statement(models.mem_bank_statement):
'''
Bank statement imported data
'''
def import_record(self, record):
def _transmission_number():
self.id = record['transref']
def _account_number():
# The wizard doesn't check for sort code
self.local_account = record['sortcode'] + ' ' + record['accnum'].zfill(8)
def _statement_number():
self.id = '-'.join([self.id, self.local_account, record['statementnr']])
def _opening_balance():
self.start_balance = record2float(record,'startingbalance')
self.local_currency = record['currencycode']
def _closing_balance():
self.end_balance = record2float(record, 'endingbalance')
self.date = record['bookingdate']
def _transaction_new():
self.transactions.append(transaction(record))
def _transaction_info():
self.transaction_info(record)
def _not_used():
print "Didn't use record: %s" % (record,)
rectypes = {
'20' : _transmission_number,
'25' : _account_number,
'28' : _statement_number,
'28C': _statement_number,
'60F': _opening_balance,
'62F': _closing_balance,
#'64' : _forward_available,
#'62M': _interim_balance,
'61' : _transaction_new,
'86' : _transaction_info,
}
rectypes.get(record['recordid'], _not_used)()
def transaction_info(self, record):
'''
Add extra information to transaction
'''
# Additional information for previous transaction
if len(self.transactions) < 1:
raise_error('Received additional information for non existent transaction', record)
transaction = self.transactions[-1]
transaction.id = ','.join([record[k] for k in ['infoline{0}'.format(i) for i in range(1,5)] if record.has_key(k)])
def raise_error(message, line):
raise osv.except_osv(_('Import error'),
'Error in import:%s\n\n%s' % (message, line))
class parser_hsbc_mt940(models.parser):
code = 'HSBC-MT940'
name = _('HSBC Swift MT940 statement export')
country_code = 'GB'
doc = _('''\
This format is available through
the HSBC web interface.
''')
def parse(self, data):
result = []
parser = HSBCParser()
# Split into statements
statements = [st for st in re.split('[\r\n]*(?=:20:)', data)]
# Split by records
statement_list = [re.split('[\r\n ]*(?=:\d\d[\w]?:)', st) for st in statements]
for statement_lines in statement_list:
stmnt = statement()
records = [parser.parse_record(record) for record in statement_lines]
[stmnt.import_record(r) for r in records if r is not None]
if stmnt.is_valid():
result.append(stmnt)
else:
print "Invalid Statement:"
print records[0]
return result
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -0,0 +1,156 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
# All Rights Reserved
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
"""
Parser for HSBC UK MT940 format files
Based on fi_patu's parser
"""
import re
from datetime import datetime
class HSBCParser(object):
def __init__( self ):
recparse = dict()
patterns = {'ebcdic': "\w/\?:\(\).,'+{} -"}
# MT940 header
recparse["20"] = ":(?P<recordid>20):(?P<transref>.{1,16})"
recparse["25"] = ":(?P<recordid>25):(?P<sortcode>\d{6})(?P<accnum>\d{1,29})"
recparse["28"] = ":(?P<recordid>28C?):(?P<statementnr>.{1,8})"
# Opening balance 60F
recparse["60F"] = ":(?P<recordid>60F):(?P<creditmarker>[CD])" \
+ "(?P<prevstmtdate>\d{6})(?P<currencycode>.{3})" \
+ "(?P<startingbalance>[\d,]{1,15})"
# Transaction
recparse["61"] = """\
:(?P<recordid>61):\
(?P<valuedate>\d{6})(?P<bookingdate>\d{4})?\
(?P<creditmarker>R?[CD])\
(?P<currency>[A-Z])?\
(?P<amount>[\d,]{1,15})\
(?P<bookingcode>[A-Z][A-Z0-9]{3})\
(?P<custrefno>[%(ebcdic)s]{1,16})\
(?://)\
(?P<bankref>[%(ebcdic)s]{1,16})?\
(?:\n(?P<furtherinfo>[%(ebcdic)s]))?\
""" % (patterns)
# Further info
recparse["86"] = ":(?P<recordid>86):" \
+ "(?P<infoline1>.{1,80})?" \
+ "(?:\n(?P<infoline2>.{1,80}))?" \
+ "(?:\n(?P<infoline3>.{1,80}))?" \
+ "(?:\n(?P<infoline4>.{1,80}))?" \
+ "(?:\n(?P<infoline5>.{1,80}))?"
# Forward available balance (64) / Closing balance (62F) / Interim balance (62M)
recparse["64"] = ":(?P<recordid>64|62[FM]):" \
+ "(?P<creditmarker>[CD])" \
+ "(?P<bookingdate>\d{6})(?P<currencycode>.{3})" \
+ "(?P<endingbalance>[\d,]{1,15})"
for record in recparse:
recparse[record] = re.compile(recparse[record])
self.recparse = recparse
def parse_record(self, line):
"""
Parse record using regexps and apply post processing
"""
for matcher in self.recparse:
matchobj = self.recparse[matcher].match(line)
if matchobj:
break
if not matchobj:
print " **** failed to match line '%s'" % (line)
return
# Strip strings
matchdict = matchobj.groupdict()
# Remove members set to None
matchdict=dict([(k,v) for k,v in matchdict.iteritems() if v])
matchkeys = set(matchdict.keys())
needstrip = set(["transref", "accnum", "statementnr", "custrefno",
"bankref", "furtherinfo", "infoline1", "infoline2", "infoline3",
"infoline4", "infoline5", "startingbalance", "endingbalance"])
for field in matchkeys & needstrip:
matchdict[field] = matchdict[field].strip()
# Convert to float. Comma is decimal separator
needsfloat = set(["startingbalance", "endingbalance", "amount"])
for field in matchkeys & needsfloat:
matchdict[field] = float(matchdict[field].replace(',','.'))
# Convert date fields
needdate = set(["prevstmtdate", "valuedate", "bookingdate"])
for field in matchkeys & needdate:
datestring = matchdict[field]
post_check = False
if len(datestring) == 4 and field=="bookingdate" and matchdict.has_key("valuedate"):
# Get year from valuedate
datestring = matchdict['valuedate'].strftime('%y') + datestring
post_check = True
try:
matchdict[field] = datetime.strptime(datestring,'%y%m%d')
if post_check and matchdict[field] > matchdict["valuedate"]:
matchdict[field]=matchdict[field].replace(year=matchdict[field].year-1)
except ValueError:
matchdict[field] = None
return matchdict
def parse(self, data):
records = []
# Some records are multiline
for line in data:
if len(line) <= 1:
continue
if line[0] == ':' and len(line) > 1:
records.append(line)
else:
records[-1] = '\n'.join([records[-1], line])
output = []
for rec in records:
output.append(self.parse_record(rec))
return output
def parse_file(filename):
hsbcfile = open(filename, "r")
p = HSBCParser().parse(hsbcfile.readlines())
def main():
"""The main function, currently just calls a dummy filename
:returns: description
"""
parse_file("testfile")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,23 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
# 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 export_hsbc

View File

@@ -0,0 +1,341 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
# 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 base64
from datetime import datetime, date, timedelta
from osv import osv, fields
from tools.translate import _
from decimal import Decimal
import paymul
import string
import random
def strpdate(arg, format='%Y-%m-%d'):
'''shortcut'''
return datetime.strptime(arg, format).date()
def strfdate(arg, format='%Y-%m-%d'):
'''shortcut'''
return arg.strftime(format)
class banking_export_hsbc_wizard(osv.osv_memory):
_name = 'banking.export.hsbc.wizard'
_description = 'HSBC Export'
_columns = {
'state': fields.selection(
[
('create', 'Create'),
('finish', 'Finish')
],
'State',
readonly=True,
),
'test': fields.boolean(),
'reference': fields.char(
'Reference', size=35,
help=('The bank will use this reference in feedback communication '
'to refer to this run. 35 characters are available.'
),
),
'execution_date_create': fields.date(
'Execution Date',
help=('This is the date the file should be processed by the bank. '
'Don\'t choose a date beyond the nearest date in your '
'payments. The latest allowed date is 30 days from now.\n'
'Please keep in mind that banks only execute on working days '
'and typically use a delay of two days between execution date '
'and effective transfer date.'
),
),
'file_id': fields.many2one(
'banking.export.hsbc',
'hsbc File',
readonly=True
),
'file': fields.related(
'file_id', 'file', type='binary',
readonly=True,
string='File',
),
'execution_date_finish': fields.related(
'file_id', 'execution_date', type='date',
readonly=True,
string='Execution Date',
),
'total_amount': fields.related(
'file_id', 'total_amount',
type='float',
string='Total Amount',
readonly=True,
),
'no_transactions': fields.integer(
'Number of Transactions',
readonly=True,
),
'payment_order_ids': fields.many2many(
'payment.order', 'rel_wiz_payorders', 'wizard_id',
'payment_order_id', 'Payment Orders',
readonly=True,
),
}
def create(self, cursor, uid, wizard_data, context=None):
'''
Retrieve a sane set of default values based on the payment orders
from the context.
'''
if not 'execution_date_create' in wizard_data:
po_ids = context.get('active_ids', [])
po_model = self.pool.get('payment.order')
pos = po_model.browse(cursor, uid, po_ids)
execution_date = date.today()
for po in pos:
if po.date_prefered == 'fixed' and po.date_planned:
execution_date = strpdate(po.date_planned)
elif po.date_prefered == 'due':
for line in po.line_ids:
if line.move_line_id.date_maturity:
date_maturity = strpdate(line.move_line_id.date_maturity)
if date_maturity < execution_date:
execution_date = date_maturity
execution_date = max(execution_date, date.today())
# The default reference contains a /, which is invalid for PAYMUL
reference = pos[0].reference.replace('/', ' ')
wizard_data.update({
'execution_date_create': strfdate(execution_date),
'reference': reference,
'payment_order_ids': [[6, 0, po_ids]],
'state': 'create',
})
return super(banking_export_hsbc_wizard, self).create(
cursor, uid, wizard_data, context)
def _create_account(self, oe_account):
currency = None # let the receiving bank select the currency from the batch
holder = oe_account.owner_name or oe_account.partner_id.name
if oe_account.iban:
paymul_account = paymul.IBANAccount(
iban=oe_account.iban,
bic=oe_account.bank.bic,
holder=holder,
currency=currency,
)
transaction_kwargs = {
'charges': paymul.CHARGES_EACH_OWN,
}
elif oe_account.country_id.code == 'GB':
split = oe_account.acc_number.split(" ", 2)
if len(split) == 2:
sortcode, accountno = split
else:
raise osv.except_osv(
_('Error'),
"Invalid GB acccount number '%s'" % oe_account.acc_number)
paymul_account = paymul.UKAccount(
number=accountno,
sortcode=sortcode,
holder=holder,
currency=currency,
)
transaction_kwargs = {
'charges': paymul.CHARGES_PAYEE,
}
else:
raise osv.except_osv(
_('Error'),
_('%s: only UK accounts and IBAN are supported') % (holder)
)
return paymul_account, transaction_kwargs
def _create_transaction(self, line):
# Check on missing partner of bank account (this can happen!)
if not line.bank_id or not line.bank_id.partner_id:
raise osv.except_osv(
_('Error'),
_('There is insufficient information.\r\n'
'Both destination address and account '
'number must be provided'
)
)
dest_account, transaction_kwargs = self._create_account(line.bank_id)
means = {'ACH or EZONE': paymul.MEANS_ACH_OR_EZONE,
'Faster Payment': paymul.MEANS_FASTER_PAYMENT}.get(line.order_id.mode.type.name)
if means is None:
raise osv.except_osv('Error', "Invalid payment type mode for HSBC '%s'" % line.order_id.mode.type.name)
try:
return paymul.Transaction(
amount=Decimal(str(line.amount_currency)),
currency=line.currency.name,
account=dest_account,
means=means,
name_address=line.info_partner,
customer_reference=line.name,
payment_reference=line.name,
**transaction_kwargs
)
except ValueError as exc:
raise osv.except_osv(
_('Error'),
_('Transaction invalid: ') + str(exc)
)
def wizard_export(self, cursor, uid, wizard_data_ids, context):
'''
Wizard to actually create the HSBC file
'''
wizard_data = self.browse(cursor, uid, wizard_data_ids, context)[0]
result_model = self.pool.get('banking.export.hsbc')
payment_orders = wizard_data.payment_order_ids
try:
src_account = self._create_account(
payment_orders[0].mode.bank_id,
)[0]
except ValueError as exc:
raise osv.except_osv(
_('Error'),
_('Source account invalid: ') + str(exc)
)
if not isinstance(src_account, paymul.UKAccount):
raise osv.except_osv(
_('Error'),
_("Your company's bank account has to have a valid UK "
"account number (not IBAN)" + str(type(src_account)))
)
transactions = []
for po in payment_orders:
transactions += [self._create_transaction(l) for l in po.line_ids]
try:
batch = paymul.Batch(
exec_date=strpdate(wizard_data.execution_date_create),
reference=wizard_data.reference,
debit_account=src_account,
name_address=payment_orders[0].line_ids[0].info_owner,
)
batch.transactions = transactions
except ValueError as exc:
raise osv.except_osv(
_('Error'),
_('Batch invalid: ') + str(exc)
)
# Generate random identifier until an unused one is found
while True:
ref = ''.join(random.choice(string.ascii_uppercase + string.digits)
for x in range(15))
ids = result_model.search(cursor, uid, [
('identification', '=', ref)
])
if not ids:
break
message = paymul.Message(reference=ref)
message.batches.append(batch)
interchange = paymul.Interchange(client_id='CLIENTID',
reference=ref,
message=message)
export_result = {
'identification': interchange.reference,
'execution_date': batch.exec_date,
'total_amount': batch.amount(),
'no_transactions': len(batch.transactions),
'file': base64.encodestring(str(interchange)),
'payment_order_ids': [
[6, 0, [po.id for po in payment_orders]]
],
}
file_id = result_model.create(cursor, uid, export_result, context)
self.write(cursor, uid, [wizard_data_ids[0]], {
'file_id': file_id,
'no_transactions' : len(batch.transactions),
'state': 'finish',
}, context)
return {
'name': _('HSBC Export'),
'view_type': 'form',
'view_mode': 'form',
'res_model': self._name,
'domain': [],
'context': dict(context, active_ids=wizard_data_ids),
'type': 'ir.actions.act_window',
'target': 'new',
'res_id': wizard_data_ids[0] or False,
}
def wizard_cancel(self, cursor, uid, ids, context):
'''
Cancel the export: just drop the file
'''
wizard_data = self.browse(cursor, uid, ids, context)[0]
result_model = self.pool.get('banking.export.hsbc')
try:
result_model.unlink(cursor, uid, wizard_data.file_id.id)
except AttributeError:
# file_id missing, wizard storage gone, server was restarted
pass
return {'type': 'ir.actions.act_window_close'}
def wizard_save(self, cursor, uid, ids, context):
'''
Save the export: mark all payments in the file as 'sent'
'''
wizard_data = self.browse(cursor, uid, ids, context)[0]
result_model = self.pool.get('banking.export.hsbc')
po_model = self.pool.get('payment.order')
result_model.write(cursor, uid, [wizard_data.file_id.id],
{'state':'sent'})
po_ids = [po.id for po in wizard_data.payment_order_ids]
po_model.action_sent(cursor, uid, po_ids)
return {'type': 'ir.actions.act_window_close'}
banking_export_hsbc_wizard()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="wizard_banking_export_wizard_view" model="ir.ui.view">
<field name="name">banking.export.hsbc.wizard.view</field>
<field name="model">banking.export.hsbc.wizard</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="HSBC Export">
<field name="state" invisible="True"/>
<group states="create">
<separator colspan="4" string="Processing Details"/>
<field name="execution_date_create"/>
<field name="test"/>
<separator colspan="4" string="Reference for further communication"/>
<field name="reference" colspan="2"/>
<separator colspan="4" string="Additional message for all transactions"/>
<newline/>
<button icon="gtk-close" special="cancel" string="Cancel"/>
<button icon="gtk-ok" string="Export" name="wizard_export" type="object"/>
</group>
<group states="finish">
<field name="total_amount"/>
<field name="no_transactions"/>
<field name="execution_date_finish"/>
<newline/>
<!--<field name="file_id"/>-->
<field name="file"/>
<newline/>
<button icon="gtk-close" string="Cancel" name="wizard_cancel" type="object"/>
<button icon="gtk-ok" string="Finish" name="wizard_save" type="object"/>
</group>
</form>
</field>
</record>
</data>
</openerp>

View File

@@ -0,0 +1,474 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
# 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/>.
#
##############################################################################
from account_banking import sepa
from decimal import Decimal
import datetime
import re
def split_account_holder(holder):
holder_parts = holder.split("\n")
try:
line2 = holder_parts[1]
except IndexError:
line2 = ''
return holder_parts[0], line2
"""
The standard says alphanumeric characters, but spaces are also allowed
"""
def edifact_isalnum(s):
return bool(re.match(r'^[A-Za-z0-9 ]*$', s))
def edifact_digits(val, digits, mindigits=None):
if mindigits is None:
mindigits = digits
pattern = r'^[0-9]{' + str(mindigits) + ',' + str(digits) + r'}$'
return bool(re.match(pattern, str(val)))
class HasCurrency(object):
def _get_currency(self):
return self._currency
def _set_currency(self, currency):
if currency is None:
self._currency = None
else:
if not len(currency) <= 3:
raise ValueError("Currency must be <= 3 characters long: " +
str(currency))
if not edifact_isalnum(currency):
raise ValueError("Currency must be alphanumeric: " + str(currency))
self._currency = currency.upper()
currency = property(_get_currency, _set_currency)
class LogicalSection(object):
def __str__(self):
segments = self.segments()
def format_segment(segment):
return '+'.join([':'.join([str(y) for y in x]) for x in segment]) + "'"
return "\n".join([format_segment(s) for s in segments])
def _fii_segment(self, party_qualifier):
holder = split_account_holder(self.holder)
account_identification = [self.number, holder[0]]
if holder[1] or self.currency:
account_identification.append(holder[1])
if self.currency:
account_identification.append(self.currency)
return [
['FII'],
[party_qualifier],
account_identification,
self.institution_identification,
[self.country],
]
class UKAccount(HasCurrency):
def _get_number(self):
return self._number
def _set_number(self, number):
if not edifact_digits(number, 8):
raise ValueError("Account number must be 8 digits long: " +
str(number))
self._number = number
number = property(_get_number, _set_number)
def _get_sortcode(self):
return self._sortcode
def _set_sortcode(self, sortcode):
if not edifact_digits(sortcode, 6):
raise ValueError("Account sort code must be 6 digits long: " +
str(sortcode))
self._sortcode = sortcode
sortcode = property(_get_sortcode, _set_sortcode)
def _get_holder(self):
return self._holder
def _set_holder(self, holder):
holder_parts = split_account_holder(holder)
if not len(holder_parts[0]) <= 35:
raise ValueError("Account holder must be <= 35 characters long")
if not len(holder_parts[1]) <= 35:
raise ValueError("Second line of account holder must be <= 35 characters long")
if not edifact_isalnum(holder_parts[0]):
raise ValueError("Account holder must be alphanumeric")
if not edifact_isalnum(holder_parts[1]):
raise ValueError("Second line of account holder must be alphanumeric")
self._holder = holder.upper()
holder = property(_get_holder, _set_holder)
def __init__(self, number, holder, currency, sortcode):
self.number = number
self.holder = holder
self.currency = currency
self.sortcode = sortcode
self.country = 'GB'
self.institution_identification = ['', '', '', self.sortcode, 154, 133]
def fii_bf_segment(self):
return _fii_segment(self, 'BF')
def fii_or_segment(self):
return _fii_segment(self, 'OR')
class IBANAccount(HasCurrency):
def _get_iban(self):
return self._iban
def _set_iban(self, iban):
iban_obj = sepa.IBAN(iban)
if not iban_obj.valid:
raise ValueError("IBAN is invalid")
self._iban = iban
self.country = iban_obj.countrycode
iban = property(_get_iban, _set_iban)
def __init__(self, iban, bic, currency, holder):
self.iban = iban
self.number = iban
self.bic = bic
self.currency = currency
self.holder = holder
self.institution_identification = [self.bic, 25, 5, '', '', '' ]
def fii_bf_segment(self):
return _fii_segment(self, 'BF')
class Interchange(LogicalSection):
def _get_reference(self):
return self._reference
def _set_reference(self, reference):
if not len(reference) <= 15:
raise ValueError("Reference must be <= 15 characters long")
if not edifact_isalnum(reference):
raise ValueError("Reference must be alphanumeric")
self._reference = reference.upper()
reference = property(_get_reference, _set_reference)
def __init__(self, client_id, reference, create_dt=None, message=None):
self.client_id = client_id
self.create_dt = create_dt or datetime.datetime.now()
self.reference = reference
self.message = message
def segments(self):
segments = []
segments.append([
['UNB'],
['UNOA', 3],
['', '', self.client_id],
['', '', 'HEXAGON ABC'],
[self.create_dt.strftime('%y%m%d'), self.create_dt.strftime('%H%M')],
[self.reference],
])
segments += self.message.segments()
segments.append([
['UNZ'],
[1],
[self.reference],
])
return segments
class Message(LogicalSection):
def _get_reference(self):
return self._reference
def _set_reference(self, reference):
if not len(reference) <= 35:
raise ValueError("Reference must be <= 35 characters long")
if not edifact_isalnum(reference):
raise ValueError("Reference must be alphanumeric")
self._reference = reference.upper()
reference = property(_get_reference, _set_reference)
def __init__(self, reference, dt=None):
if dt:
self.dt = dt
else:
self.dt = datetime.datetime.now()
self.reference = reference
self.batches = []
def segments(self):
# HSBC only accepts one message per interchange
message_reference_number = 1
segments = []
segments.append([
['UNH'],
[message_reference_number],
['PAYMUL', 'D', '96A', 'UN', 'FUN01G'],
])
segments.append([
['BGM'],
[452],
[self.reference],
[9],
])
segments.append([
['DTM'],
(137, self.dt.strftime('%Y%m%d'), 102),
])
for index, batch in enumerate(self.batches):
segments += batch.segments(index + 1)
segments.append([
['CNT'],
['39', sum([len(x.transactions) for x in self.batches])],
])
segments.append([
['UNT'],
[len(segments) + 1],
[message_reference_number]
])
return segments
class Batch(LogicalSection):
def _get_reference(self):
return self._reference
def _set_reference(self, reference):
if not len(reference) <= 18:
raise ValueError("Reference must be <= 18 characters long")
if not edifact_isalnum(reference):
raise ValueError("Reference must be alphanumeric")
self._reference = reference.upper()
reference = property(_get_reference, _set_reference)
def __init__(self, exec_date, reference, debit_account, name_address):
self.exec_date = exec_date
self.reference = reference
self.debit_account = debit_account
self.name_address = name_address
self.transactions = []
def amount(self):
return sum([x.amount for x in self.transactions])
def segments(self, index):
if not edifact_digits(index, 6, 1):
raise ValueError("Index must be 6 digits or less")
segments = []
segments.append([
['LIN'],
[index],
])
segments.append([
['DTM'],
[203, self.exec_date.strftime('%Y%m%d'), 102],
])
segments.append([
['RFF'],
['AEK', self.reference],
])
currencies = set([x.currency for x in self.transactions])
if len(currencies) > 1:
raise ValueError("All transactions in a batch must have the same currency")
segments.append([
['MOA'],
[9, self.amount().quantize(Decimal('0.00')), currencies.pop()],
])
segments.append(self.debit_account.fii_or_segment())
segments.append([
['NAD'],
['OY'],
[''],
self.name_address.upper().split("\n")[0:5],
])
for index, transaction in enumerate(self.transactions):
segments += transaction.segments(index + 1)
return segments
# From the spec for FCA segments:
# 13 = All charges borne by payee (or beneficiary)
# 14 = Each pay own cost
# 15 = All charges borne by payor (or ordering customer)
# For Faster Payments this should always be 14
# Where this field is not present, “14” will be used as a default.
CHARGES_PAYEE = 13
CHARGES_EACH_OWN = 14
CHARGES_PAYER = 15
# values per section 2.8.5 "PAI, Payment Instructions" of "HSBC - CRG Paymul Message Implementation Guide"
MEANS_ACH_OR_EZONE = 2
MEANS_PRIORITY_PAYMENT = 52
MEANS_FASTER_PAYMENT = 'FPS'
CHANNEL_INTRA_COMPANY = 'Z24'
class Transaction(LogicalSection, HasCurrency):
def _get_amount(self):
return self._amount
def _set_amount(self, amount):
if len(str(amount)) > 18:
raise ValueError("Amount must be shorter than 18 bytes")
self._amount = amount
amount = property(_get_amount, _set_amount)
def _get_payment_reference(self):
return self._payment_reference
def _set_payment_reference(self, payment_reference):
if not len(payment_reference) <= 18:
raise ValueError("Payment reference must be <= 18 characters long")
if not edifact_isalnum(payment_reference):
raise ValueError("Payment reference must be alphanumeric")
self._payment_reference = payment_reference.upper()
payment_reference = property(_get_payment_reference, _set_payment_reference)
def _get_customer_reference(self):
return self._customer_reference
def _set_customer_reference(self, customer_reference):
if not len(customer_reference) <= 18:
raise ValueError("Customer reference must be <= 18 characters long")
if not edifact_isalnum(customer_reference):
raise ValueError("Customer reference must be alphanumeric")
self._customer_reference = customer_reference.upper()
customer_reference = property(_get_customer_reference, _set_customer_reference)
def __init__(self, amount, currency, account, means,
name_address=None, party_name=None, channel='',
charges=CHARGES_EACH_OWN, customer_reference=None,
payment_reference=None):
self.amount = amount
self.currency = currency
self.account = account
self.name_address = name_address
self.party_name = party_name
self.means = means
self.channel = channel
self.charges = charges
self.payment_reference = payment_reference
self.customer_reference = customer_reference
def segments(self, index):
segments = []
segments.append([
['SEQ'],
[''],
[index],
])
segments.append([
['MOA'],
[9, self.amount.quantize(Decimal('0.00')), self.currency],
])
if self.customer_reference:
segments.append([
['RFF'],
['CR', self.customer_reference],
])
if self.payment_reference:
segments.append([
['RFF'],
['PQ', self.payment_reference],
])
if self.channel:
segments.append([
['PAI'],
['', '', self.means, '', '', self.channel],
])
else:
segments.append([
['PAI'],
['', '', self.means],
])
segments.append([
['FCA'],
[self.charges],
])
segments.append(self.account.fii_bf_segment())
nad_segment = [
['NAD'],
['BE'],
[''],
]
if self.name_address:
nad_segment.append(self.name_address.upper().split("\n")[0:5])
else:
nad_segment.append('')
if self.party_name:
nad_segment.append(self.party_name.upper().split("\n")[0:5])
segments.append(nad_segment)
return segments

View File

@@ -0,0 +1,274 @@
# -*- encoding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
# 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 datetime
import unittest2 as unittest
import paymul
from decimal import Decimal
class PaymulTestCase(unittest.TestCase):
def setUp(self):
self.maxDiff = None
def test_uk_high_value_priority_payment(self):
# Changes from spec example: Removed DTM for transaction, HSBC ignores it (section 2.8.3)
expected = \
"""UNB+UNOA:3+::ABC00000001+::HEXAGON ABC+041111:1500+UKHIGHVALUE'
UNH+1+PAYMUL:D:96A:UN:FUN01G'
BGM+452+UKHIGHVALUE+9'
DTM+137:20041111:102'
LIN+1'
DTM+203:20041112:102'
RFF+AEK:UKHIGHVALUE'
MOA+9:1.00:GBP'
FII+OR+12345678:HSBC NET TEST::GBP+:::400515:154:133+GB'
NAD+OY++HSBC BANK PLC:HSBC NET TEST:TEST:TEST:UNITED KINGDOM'
SEQ++1'
MOA+9:1.00:GBP'
RFF+CR:CRUKHV5'
RFF+PQ:PQUKHV5'
PAI+::52:::Z24'
FCA+13'
FII+BF+87654321:XYX LTD FROM FII BF 1:BEN NAME 2:GBP+:::403124:154:133+GB'
NAD+BE++SOME BANK PLC:HSBC NET TEST:TEST:TEST:UNITED KINGDOM'
CNT+39:1'
UNT+19+1'
UNZ+1+UKHIGHVALUE'"""
src_account = paymul.UKAccount(number=12345678,
holder='HSBC NET TEST',
currency='GBP',
sortcode=400515)
dest_account = paymul.UKAccount(number=87654321,
holder="XYX LTD FROM FII BF 1\nBEN NAME 2",
currency='GBP',
sortcode=403124)
transaction = paymul.Transaction(amount=Decimal('1.00'),
currency='GBP',
account=dest_account,
charges=paymul.CHARGES_PAYEE,
means=paymul.MEANS_PRIORITY_PAYMENT,
channel=paymul.CHANNEL_INTRA_COMPANY,
name_address="SOME BANK PLC\nHSBC NET TEST\nTEST\nTEST\nUNITED KINGDOM",
customer_reference='CRUKHV5',
payment_reference='PQUKHV5')
batch = paymul.Batch(exec_date=datetime.date(2004, 11, 12),
reference='UKHIGHVALUE',
debit_account=src_account,
name_address="HSBC BANK PLC\nHSBC NET TEST\nTEST\nTEST\nUNITED KINGDOM")
batch.transactions.append(transaction)
message = paymul.Message(reference='UKHIGHVALUE',
dt=datetime.datetime(2004, 11, 11))
message.batches.append(batch)
interchange = paymul.Interchange(client_id='ABC00000001',
reference='UKHIGHVALUE',
create_dt=datetime.datetime(2004, 11, 11, 15, 00),
message=message)
self.assertMultiLineEqual(expected, str(interchange))
def test_ezone(self):
# Changes from example in spec: Changed CNT from 27 to 39, because we only generate that
# and it makes no difference which one we use
# Removed DTM for transaction, HSBC ignores it (section 2.8.3)
expected = """UNB+UNOA:3+::ABC12016001+::HEXAGON ABC+080110:0856+EZONE'
UNH+1+PAYMUL:D:96A:UN:FUN01G'
BGM+452+EZONE+9'
DTM+137:20080110:102'
LIN+1'
DTM+203:20080114:102'
RFF+AEK:EZONE'
MOA+9:1.00:EUR'
FII+OR+12345678:ACCOUNT HOLDER NAME::EUR+:::403124:154:133+GB'
NAD+OY++ORD PARTY NAME NADOY 01:CRG TC5 001 NADOY ADDRESS LINE 0001:CRG TC5 001 NADOY ADDRESS LINE 0002'
SEQ++1'
MOA+9:1.00:EUR'
RFF+CR:EZONE 1A'
RFF+PQ:EZONE 1A'
PAI+::2'
FCA+14'
FII+BF+DE23300308800099990031:CRG TC5 001 BENE NAME FIIBF 000001::EUR+AACSDE33:25:5:::+DE'
NAD+BE+++BENE NAME NADBE T1 001:CRG TC5 001T1 NADBE ADD LINE 1 0001:CRG TC5 001T1 NADBE ADD LINE 2 0001'
CNT+39:1'
UNT+19+1'
UNZ+1+EZONE'"""
src_account = paymul.UKAccount(number=12345678,
holder='ACCOUNT HOLDER NAME',
currency='EUR',
sortcode=403124)
dest_account = paymul.IBANAccount(iban="DE23300308800099990031",
holder="CRG TC5 001 BENE NAME FIIBF 000001",
currency='EUR',
bic="AACSDE33")
party_name = "BENE NAME NADBE T1 001\n" \
+ "CRG TC5 001T1 NADBE ADD LINE 1 0001\n" \
+ "CRG TC5 001T1 NADBE ADD LINE 2 0001"
transaction = paymul.Transaction(amount=Decimal('1.00'),
currency='EUR',
account=dest_account,
party_name=party_name,
charges=paymul.CHARGES_EACH_OWN,
means=paymul.MEANS_EZONE,
customer_reference='EZONE 1A',
payment_reference='EZONE 1A')
name_address = "ORD PARTY NAME NADOY 01\n" \
+ "CRG TC5 001 NADOY ADDRESS LINE 0001\n" \
+ "CRG TC5 001 NADOY ADDRESS LINE 0002"
batch = paymul.Batch(exec_date=datetime.date(2008, 1, 14),
reference='EZONE',
debit_account=src_account,
name_address=name_address)
batch.transactions.append(transaction)
message = paymul.Message(reference='EZONE',
dt=datetime.datetime(2008, 1, 10))
message.batches.append(batch)
interchange = paymul.Interchange(client_id='ABC12016001',
reference='EZONE',
create_dt=datetime.datetime(2008, 1, 10, 8, 56),
message=message)
self.assertMultiLineEqual(expected, str(interchange))
def test_uk_low_value_ach_instruction_level(self):
dest_account1 = paymul.UKAccount(number=87654321,
holder="HSBC NET RPS TEST\nHSBC BANK",
currency='GBP',
sortcode=403124)
name_address = "HSBC BANK PLC\n" \
+ "PCM\n" \
+ "8CS37\n" \
+ "E14 5HQ\n" \
+ "UNITED KINGDOM"
transaction1 = paymul.Transaction(amount=Decimal('1.00'),
currency='GBP',
account=dest_account1,
name_address=name_address,
charges=paymul.CHARGES_PAYEE,
means=paymul.MEANS_ACH,
customer_reference='CREDIT',
payment_reference='CREDIT')
dest_account2 = paymul.UKAccount(number=12341234,
holder="HSBC NET RPS TEST\nHSBC BANK",
currency='GBP',
sortcode=403124)
name_address = "HSBC BANK PLC\n" \
+ "PCM\n" \
+ "8CS37\n" \
+ "E14 5HQ\n" \
+ "UNITED KINGDOM"
transaction2 = paymul.Transaction(amount=Decimal('1.00'),
currency='GBP',
account=dest_account2,
name_address=name_address,
charges=paymul.CHARGES_PAYEE,
means=paymul.MEANS_ACH,
customer_reference='CREDIT1',
payment_reference='CREDIT1')
name_address = "HSBC BANK PLC\n" \
+ "PCM\n" \
+ "8CS37\n" \
+ "E14 5HQ\n" \
+ "UNITED KINGDOM"
src_account = paymul.UKAccount(number=12345678,
holder='BHEX RPS TEST',
currency='GBP',
sortcode=401234)
batch = paymul.Batch(exec_date=datetime.date(2004, 11, 15),
reference='UKLVPLIL',
debit_account=src_account,
name_address=name_address)
batch.transactions = [transaction1, transaction2]
message = paymul.Message(reference='UKLVPLIL',
dt=datetime.datetime(2004, 11, 11))
message.batches.append(batch)
interchange = paymul.Interchange(client_id='ABC00000001',
reference='UKLVPLIL',
create_dt=datetime.datetime(2004, 11, 11, 15, 0),
message=message)
# Changes from example:
# * Change second transaction from EUR to GBP, because we don't support
# multi-currency batches
# * Removed DTM for transaction, HSBC ignores it (section 2.8.3)
expected = """UNB+UNOA:3+::ABC00000001+::HEXAGON ABC+041111:1500+UKLVPLIL'
UNH+1+PAYMUL:D:96A:UN:FUN01G'
BGM+452+UKLVPLIL+9'
DTM+137:20041111:102'
LIN+1'
DTM+203:20041115:102'
RFF+AEK:UKLVPLIL'
MOA+9:2.00:GBP'
FII+OR+12345678:BHEX RPS TEST::GBP+:::401234:154:133+GB'
NAD+OY++HSBC BANK PLC:PCM:8CS37:E14 5HQ:UNITED KINGDOM'
SEQ++1'
MOA+9:1.00:GBP'
RFF+CR:CREDIT'
RFF+PQ:CREDIT'
PAI+::2'
FCA+13'
FII+BF+87654321:HSBC NET RPS TEST:HSBC BANK:GBP+:::403124:154:133+GB'
NAD+BE++HSBC BANK PLC:PCM:8CS37:E14 5HQ:UNITED KINGDOM'
SEQ++2'
MOA+9:1.00:GBP'
RFF+CR:CREDIT1'
RFF+PQ:CREDIT1'
PAI+::2'
FCA+13'
FII+BF+12341234:HSBC NET RPS TEST:HSBC BANK:GBP+:::403124:154:133+GB'
NAD+BE++HSBC BANK PLC:PCM:8CS37:E14 5HQ:UNITED KINGDOM'
CNT+39:2'
UNT+27+1'
UNZ+1+UKLVPLIL'"""
self.assertMultiLineEqual(expected, str(interchange))
if __name__ == "__main__":
# I ran this with
# env PYTHONPATH=$HOME/src/canonical/hsbc-banking:$HOME/src/openerp/6.0/server/bin:$HOME/src/openerp/6.0/addons python wizard/paymul_test.py
# is there a better way?
unittest.main()