mirror of
https://github.com/OCA/bank-payment.git
synced 2025-02-02 10:37:31 +02:00
UK HSBC module
This commit is contained in:
25
account_banking_uk_hsbc/__init__.py
Normal file
25
account_banking_uk_hsbc/__init__.py
Normal 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:
|
||||
50
account_banking_uk_hsbc/__openerp__.py
Normal file
50
account_banking_uk_hsbc/__openerp__.py
Normal 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,
|
||||
}
|
||||
65
account_banking_uk_hsbc/account_banking_uk_hsbc.py
Normal file
65
account_banking_uk_hsbc/account_banking_uk_hsbc.py
Normal 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:
|
||||
85
account_banking_uk_hsbc/account_banking_uk_hsbc.xml
Normal file
85
account_banking_uk_hsbc/account_banking_uk_hsbc.xml
Normal 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>
|
||||
21
account_banking_uk_hsbc/data/banking_export_hsbc.xml
Normal file
21
account_banking_uk_hsbc/data/banking_export_hsbc.xml
Normal 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>
|
||||
161
account_banking_uk_hsbc/hsbc_mt940.py
Normal file
161
account_banking_uk_hsbc/hsbc_mt940.py
Normal 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:
|
||||
156
account_banking_uk_hsbc/mt940_parser.py
Normal file
156
account_banking_uk_hsbc/mt940_parser.py
Normal 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()
|
||||
23
account_banking_uk_hsbc/wizard/__init__.py
Normal file
23
account_banking_uk_hsbc/wizard/__init__.py
Normal 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
|
||||
341
account_banking_uk_hsbc/wizard/export_hsbc.py
Normal file
341
account_banking_uk_hsbc/wizard/export_hsbc.py
Normal 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:
|
||||
37
account_banking_uk_hsbc/wizard/export_hsbc_view.xml
Normal file
37
account_banking_uk_hsbc/wizard/export_hsbc_view.xml
Normal 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>
|
||||
474
account_banking_uk_hsbc/wizard/paymul.py
Normal file
474
account_banking_uk_hsbc/wizard/paymul.py
Normal 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
|
||||
274
account_banking_uk_hsbc/wizard/paymul_test.py
Normal file
274
account_banking_uk_hsbc/wizard/paymul_test.py
Normal 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()
|
||||
Reference in New Issue
Block a user