credativ team mailing list archive
-
credativ team
-
Mailing list archive
-
Message #04681
[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