[IMP] import module to integrate with HSBC

This commit is contained in:
Tristan Hill
2011-10-24 16:26:13 +01:00
parent 1f11e2916a
commit 3a3151c62d
10 changed files with 1378 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
# -*- 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
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -0,0 +1,45 @@
# -*- 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.1',
'license': 'GPL-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 and to export payments for HSBC.net.
Currently targetting UK market.
This modules contains no logic, just an import/export filter for account_banking.
''',
'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,13 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record model="payment.mode.type" id="export_hsbc">
<field name="name">HSBC</field>
<field name="code">HSBC</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>
</data>
</openerp>

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,339 @@
# -*- 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,
'means': paymul.MEANS_EZONE,
}
elif oe_account.country_id.code == 'GB':
sortcode, accountno = oe_account.acc_number.split(" ", 2)
paymul_account = paymul.UKAccount(
number=accountno,
sortcode=sortcode,
holder=holder,
currency=currency,
)
transaction_kwargs = {
'charges': paymul.CHARGES_PAYEE,
'means': paymul.MEANS_ACH,
}
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'
)
)
try:
dest_account, transaction_kwargs = self._create_account(
line.bank_id)
except ValueError as exc:
raise osv.except_osv(
_('Error'),
_('Destination account invalid: ') + str(exc)
)
try:
return paymul.Transaction(
amount=Decimal(str(line.amount_currency)),
currency=line.currency.name,
account=dest_account,
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,473 @@
# -*- 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
MEANS_ACH = 2
MEANS_EZONE = 2
MEANS_PRIORITY_PAYMENT = 52
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()