← Back to team overview

credativ team mailing list archive

[Merge] lp:~credativ/banking-addons/trunk-wip into lp:banking-addons

 

Dmitrijs Ledkovs (credativ) has proposed merging lp:~credativ/banking-addons/trunk-wip into lp:banking-addons.

Requested reviews:
  Banking Addons Team (banking-addons-team)

For more details, see:
https://code.launchpad.net/~credativ/banking-addons/trunk-wip/+merge/83131

Please review:
- 2 bug fixes
- [new] UK HSBC module
-- 
https://code.launchpad.net/~credativ/banking-addons/trunk-wip/+merge/83131
Your team credativ is subscribed to branch lp:~credativ/banking-addons/trunk-wip.
=== modified file 'account_banking/account_banking.py'
--- account_banking/account_banking.py	2011-11-01 15:28:18 +0000
+++ account_banking/account_banking.py	2012-02-10 13:37:18 +0000
@@ -243,6 +243,7 @@
     Extensions from account_bank_statement:
         1. Removed period_id (transformed to optional boolean) - as it is no
            longer needed.
+           NB! because of #1. changes required to account_voucher!
         2. Extended 'button_confirm' trigger to cope with the period per
            statement_line situation.
         3. Added optional relation with imported statements file
@@ -453,8 +454,39 @@
 
         return move_id
 
+    def button_confirm_bank(self, cr, uid, ids, context=None):
+        if context is None: context = {}
+        obj_seq = self.pool.get('ir.sequence')
+        if not isinstance(ids, list): ids = [ids]
+        noname_ids = self.search(cr, uid, [('id','in',ids),('name','=','/')])
+        for st in self.browse(cr, uid, noname_ids, context=context):
+                if st.journal_id.sequence_id:
+                    year = self.pool.get('account.period').browse(cr, uid, self._get_period(cr, uid, st.date)).fiscalyear_id.id
+                    c = {'fiscalyear_id': year}
+                    st_number = obj_seq.get_id(cr, uid, st.journal_id.sequence_id.id, context=c)
+                    self.write(cr, uid, ids, {'name': st_number})
+        
+        return super(account_bank_statement, self).button_confirm_bank(cr, uid, ids, context)
+
 account_bank_statement()
 
+class account_voucher(osv.osv):
+    _inherit = 'account.voucher'
+
+    def _get_period(self, cr, uid, context=None):
+        if context is None: context = {}
+        if not context.get('period_id') and context.get('move_line_ids'):
+            res = self.pool.get('account.move.line').browse(cr, uid , context.get('move_line_ids'))[0].period_id.id
+            context['period_id'] = res
+        return super(account_voucher, self)._get_period(cr, uid, context)
+
+    def create(self, cr, uid, values, context=None):
+        if values.get('period_id') == False and context.get('move_line_ids'):
+            values['period_id'] = self._get_period(cr, uid, context)
+        return super(account_voucher, self).create(cr, uid, values, context)
+
+account_voucher()
+
 class account_bank_statement_line(osv.osv):
     '''
     Extension on basic class:

=== modified file 'account_banking/wizard/bank_import.py'
--- account_banking/wizard/bank_import.py	2011-12-13 21:03:09 +0000
+++ account_banking/wizard/bank_import.py	2012-02-10 13:37:18 +0000
@@ -848,7 +848,10 @@
                     i += 1
 
             results.stat_loaded_cnt += 1
-            
+
+        #recompute statement end_balance for validation
+        statement_obj.button_dummy(cursor, uid, imported_statement_ids)
+    
         if payment_lines:
             # As payments lines are treated as individual transactions, the
             # batch as a whole is only marked as 'done' when all payment lines

=== added directory 'account_banking_uk_hsbc'
=== added file 'account_banking_uk_hsbc/__init__.py'
--- account_banking_uk_hsbc/__init__.py	1970-01-01 00:00:00 +0000
+++ account_banking_uk_hsbc/__init__.py	2012-02-10 13:37:18 +0000
@@ -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:

=== added file 'account_banking_uk_hsbc/__openerp__.py'
--- account_banking_uk_hsbc/__openerp__.py	1970-01-01 00:00:00 +0000
+++ account_banking_uk_hsbc/__openerp__.py	2012-02-10 13:37:18 +0000
@@ -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,
+}

=== added file 'account_banking_uk_hsbc/account_banking_uk_hsbc.py'
--- account_banking_uk_hsbc/account_banking_uk_hsbc.py	1970-01-01 00:00:00 +0000
+++ account_banking_uk_hsbc/account_banking_uk_hsbc.py	2012-02-10 13:37:18 +0000
@@ -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:

=== added file 'account_banking_uk_hsbc/account_banking_uk_hsbc.xml'
--- account_banking_uk_hsbc/account_banking_uk_hsbc.xml	1970-01-01 00:00:00 +0000
+++ account_banking_uk_hsbc/account_banking_uk_hsbc.xml	2012-02-10 13:37:18 +0000
@@ -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>

=== added directory 'account_banking_uk_hsbc/data'
=== added file 'account_banking_uk_hsbc/data/banking_export_hsbc.xml'
--- account_banking_uk_hsbc/data/banking_export_hsbc.xml	1970-01-01 00:00:00 +0000
+++ account_banking_uk_hsbc/data/banking_export_hsbc.xml	2012-02-10 13:37:18 +0000
@@ -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>

=== added file 'account_banking_uk_hsbc/hsbc_mt940.py'
--- account_banking_uk_hsbc/hsbc_mt940.py	1970-01-01 00:00:00 +0000
+++ account_banking_uk_hsbc/hsbc_mt940.py	2012-02-10 13:37:18 +0000
@@ -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:

=== added file 'account_banking_uk_hsbc/mt940_parser.py'
--- account_banking_uk_hsbc/mt940_parser.py	1970-01-01 00:00:00 +0000
+++ account_banking_uk_hsbc/mt940_parser.py	2012-02-10 13:37:18 +0000
@@ -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()

=== added directory 'account_banking_uk_hsbc/wizard'
=== added file 'account_banking_uk_hsbc/wizard/__init__.py'
--- account_banking_uk_hsbc/wizard/__init__.py	1970-01-01 00:00:00 +0000
+++ account_banking_uk_hsbc/wizard/__init__.py	2012-02-10 13:37:18 +0000
@@ -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
\ No newline at end of file

=== added file 'account_banking_uk_hsbc/wizard/export_hsbc.py'
--- account_banking_uk_hsbc/wizard/export_hsbc.py	1970-01-01 00:00:00 +0000
+++ account_banking_uk_hsbc/wizard/export_hsbc.py	2012-02-10 13:37:18 +0000
@@ -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:

=== added file 'account_banking_uk_hsbc/wizard/export_hsbc_view.xml'
--- account_banking_uk_hsbc/wizard/export_hsbc_view.xml	1970-01-01 00:00:00 +0000
+++ account_banking_uk_hsbc/wizard/export_hsbc_view.xml	2012-02-10 13:37:18 +0000
@@ -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>

=== added file 'account_banking_uk_hsbc/wizard/paymul.py'
--- account_banking_uk_hsbc/wizard/paymul.py	1970-01-01 00:00:00 +0000
+++ account_banking_uk_hsbc/wizard/paymul.py	2012-02-10 13:37:18 +0000
@@ -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

=== added file 'account_banking_uk_hsbc/wizard/paymul_test.py'
--- account_banking_uk_hsbc/wizard/paymul_test.py	1970-01-01 00:00:00 +0000
+++ account_banking_uk_hsbc/wizard/paymul_test.py	2012-02-10 13:37:18 +0000
@@ -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()


Follow ups