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

Proposed by Dimitri John Ledkov (ex-credativ)
Status: Merged
Merged at revision: 72
Proposed branch: lp:~credativ/banking-addons/trunk-wip
Merge into: lp:banking-addons/6.0
Diff against target: 2086 lines (+1979/-1)
15 files modified
account_banking/account_banking.py (+32/-0)
account_banking/sepa/online.py (+9/-0)
account_banking/wizard/bank_import.py (+4/-1)
account_banking_uk_hsbc/__init__.py (+25/-0)
account_banking_uk_hsbc/__openerp__.py (+50/-0)
account_banking_uk_hsbc/account_banking_uk_hsbc.py (+65/-0)
account_banking_uk_hsbc/account_banking_uk_hsbc.xml (+85/-0)
account_banking_uk_hsbc/data/banking_export_hsbc.xml (+29/-0)
account_banking_uk_hsbc/hsbc_mt940.py (+161/-0)
account_banking_uk_hsbc/mt940_parser.py (+156/-0)
account_banking_uk_hsbc/wizard/__init__.py (+23/-0)
account_banking_uk_hsbc/wizard/export_hsbc.py (+400/-0)
account_banking_uk_hsbc/wizard/export_hsbc_view.xml (+37/-0)
account_banking_uk_hsbc/wizard/paymul.py (+629/-0)
account_banking_uk_hsbc/wizard/paymul_test.py (+274/-0)
To merge this branch: bzr merge lp:~credativ/banking-addons/trunk-wip
Reviewer Review Type Date Requested Status
Stefan Rijnhart (Opener) test, technical Approve
Review via email: mp+83131@code.launchpad.net

Description of the change

Please review:
- 2 bug fixes
- [new] UK HSBC module

To post a comment you must log in.
75. By Dimitri John Ledkov (ex-credativ)

Merge US&CA changes from canonical

76. By Dimitri John Ledkov (ex-credativ)

Re-enable onchange trigger

77. By Dimitri John Ledkov (ex-credativ)

[MERGE] [FIX] disable call to SWIFT lookup page that no longer exists

Revision history for this message
Stefan Rijnhart (Opener) (stefan-opener) wrote :

Hi Dmitrijs,

Excellent work! Merged verbatim, except for adding the context to the call to button_dummy in account_banking/wizard/bank_import.py:853 as per convention.

Thanks again,
Stefan.

review: Approve (test, technical)
Revision history for this message
Stefan Rijnhart (Opener) (stefan-opener) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'account_banking/account_banking.py'
2--- account_banking/account_banking.py 2011-11-01 15:28:18 +0000
3+++ account_banking/account_banking.py 2012-02-10 14:09:45 +0000
4@@ -243,6 +243,7 @@
5 Extensions from account_bank_statement:
6 1. Removed period_id (transformed to optional boolean) - as it is no
7 longer needed.
8+ NB! because of #1. changes required to account_voucher!
9 2. Extended 'button_confirm' trigger to cope with the period per
10 statement_line situation.
11 3. Added optional relation with imported statements file
12@@ -453,8 +454,39 @@
13
14 return move_id
15
16+ def button_confirm_bank(self, cr, uid, ids, context=None):
17+ if context is None: context = {}
18+ obj_seq = self.pool.get('ir.sequence')
19+ if not isinstance(ids, list): ids = [ids]
20+ noname_ids = self.search(cr, uid, [('id','in',ids),('name','=','/')])
21+ for st in self.browse(cr, uid, noname_ids, context=context):
22+ if st.journal_id.sequence_id:
23+ year = self.pool.get('account.period').browse(cr, uid, self._get_period(cr, uid, st.date)).fiscalyear_id.id
24+ c = {'fiscalyear_id': year}
25+ st_number = obj_seq.get_id(cr, uid, st.journal_id.sequence_id.id, context=c)
26+ self.write(cr, uid, ids, {'name': st_number})
27+
28+ return super(account_bank_statement, self).button_confirm_bank(cr, uid, ids, context)
29+
30 account_bank_statement()
31
32+class account_voucher(osv.osv):
33+ _inherit = 'account.voucher'
34+
35+ def _get_period(self, cr, uid, context=None):
36+ if context is None: context = {}
37+ if not context.get('period_id') and context.get('move_line_ids'):
38+ res = self.pool.get('account.move.line').browse(cr, uid , context.get('move_line_ids'))[0].period_id.id
39+ context['period_id'] = res
40+ return super(account_voucher, self)._get_period(cr, uid, context)
41+
42+ def create(self, cr, uid, values, context=None):
43+ if values.get('period_id') == False and context.get('move_line_ids'):
44+ values['period_id'] = self._get_period(cr, uid, context)
45+ return super(account_voucher, self).create(cr, uid, values, context)
46+
47+account_voucher()
48+
49 class account_bank_statement_line(osv.osv):
50 '''
51 Extension on basic class:
52
53=== modified file 'account_banking/sepa/online.py'
54--- account_banking/sepa/online.py 2011-05-20 16:51:17 +0000
55+++ account_banking/sepa/online.py 2012-02-10 14:09:45 +0000
56@@ -175,7 +175,16 @@
57 requests to make. In total three HTTP requests are made per function call.
58 In theory one request could be stripped, but the SWIFT terms of use prevent
59 automated usage, so user like behavior is required.
60+
61+ Update January 2012: Always return None, as the SWIFT page to retrieve the
62+ information does no longer exist.
63+ If demand exists, maybe bite the bullet and integrate with a paid web
64+ service such as http://www.iban-rechner.de.
65+ lp914922 additionally suggests to make online lookup optional.
66 '''
67+
68+ return None, None
69+
70 def harvest(soup):
71 retval = struct()
72 for trsoup in soup('tr'):
73
74=== modified file 'account_banking/wizard/bank_import.py'
75--- account_banking/wizard/bank_import.py 2011-12-13 21:03:09 +0000
76+++ account_banking/wizard/bank_import.py 2012-02-10 14:09:45 +0000
77@@ -848,7 +848,10 @@
78 i += 1
79
80 results.stat_loaded_cnt += 1
81-
82+
83+ #recompute statement end_balance for validation
84+ statement_obj.button_dummy(cursor, uid, imported_statement_ids)
85+
86 if payment_lines:
87 # As payments lines are treated as individual transactions, the
88 # batch as a whole is only marked as 'done' when all payment lines
89
90=== added directory 'account_banking_uk_hsbc'
91=== added file 'account_banking_uk_hsbc/__init__.py'
92--- account_banking_uk_hsbc/__init__.py 1970-01-01 00:00:00 +0000
93+++ account_banking_uk_hsbc/__init__.py 2012-02-10 14:09:45 +0000
94@@ -0,0 +1,25 @@
95+# -*- encoding: utf-8 -*-
96+##############################################################################
97+#
98+# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
99+# All Rights Reserved
100+#
101+# This program is free software: you can redistribute it and/or modify
102+# it under the terms of the GNU Affero General Public License as
103+# published by the Free Software Foundation, either version 3 of the
104+# License, or (at your option) any later version.
105+#
106+# This program is distributed in the hope that it will be useful,
107+# but WITHOUT ANY WARRANTY; without even the implied warranty of
108+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
109+# GNU Affero General Public License for more details.
110+#
111+# You should have received a copy of the GNU Affero General Public License
112+# along with this program. If not, see <http://www.gnu.org/licenses/>.
113+#
114+##############################################################################
115+
116+import account_banking_uk_hsbc
117+import wizard
118+import hsbc_mt940
119+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
120
121=== added file 'account_banking_uk_hsbc/__openerp__.py'
122--- account_banking_uk_hsbc/__openerp__.py 1970-01-01 00:00:00 +0000
123+++ account_banking_uk_hsbc/__openerp__.py 2012-02-10 14:09:45 +0000
124@@ -0,0 +1,50 @@
125+# -*- encoding: utf-8 -*-
126+##############################################################################
127+#
128+# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
129+# All Rights Reserved
130+#
131+# This program is free software: you can redistribute it and/or modify
132+# it under the terms of the GNU Affero General Public License as
133+# published by the Free Software Foundation, either version 3 of the
134+# License, or (at your option) any later version.
135+#
136+# This program is distributed in the hope that it will be useful,
137+# but WITHOUT ANY WARRANTY; without even the implied warranty of
138+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
139+# GNU Affero General Public License for more details.
140+#
141+# You should have received a copy of the GNU Affero General Public License
142+# along with this program. If not, see <http://www.gnu.org/licenses/>.
143+#
144+##############################################################################
145+{
146+ 'name': 'HSBC Account Banking',
147+ 'version': '0.4',
148+ 'license': 'AGPL-3',
149+ 'author': 'credativ Ltd',
150+ 'website': 'http://www.credativ.co.uk',
151+ 'category': 'Account Banking',
152+ 'depends': ['account_banking'],
153+ 'init_xml': [],
154+ 'update_xml': [
155+ 'account_banking_uk_hsbc.xml',
156+ 'data/banking_export_hsbc.xml',
157+ 'wizard/export_hsbc_view.xml',
158+ ],
159+ 'demo_xml': [],
160+ 'description': '''
161+ Module to import HSBC format transation files (S.W.I.F.T MT940) and to export payments for HSBC.net (PAYMUL).
162+
163+ Currently it is targetting UK market, due to country variances of the MT940 and PAYMUL.
164+
165+ It is possible to extend this module to work with HSBC.net in other countries and potentially other banks.
166+
167+ This module adds above import/export filter to the account_banking module.
168+ All business logic is in account_banking module.
169+
170+ Initial release of this module was co-sponsored by canonical.
171+ ''',
172+ 'active': False,
173+ 'installable': True,
174+}
175
176=== added file 'account_banking_uk_hsbc/account_banking_uk_hsbc.py'
177--- account_banking_uk_hsbc/account_banking_uk_hsbc.py 1970-01-01 00:00:00 +0000
178+++ account_banking_uk_hsbc/account_banking_uk_hsbc.py 2012-02-10 14:09:45 +0000
179@@ -0,0 +1,65 @@
180+##############################################################################
181+#
182+# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
183+# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
184+# All Rights Reserved
185+#
186+# This program is free software: you can redistribute it and/or modify
187+# it under the terms of the GNU General Public License as published by
188+# the Free Software Foundation, either version 3 of the License, or
189+# (at your option) any later version.
190+#
191+# This program is distributed in the hope that it will be useful,
192+# but WITHOUT ANY WARRANTY; without even the implied warranty of
193+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
194+# GNU General Public License for more details.
195+#
196+# You should have received a copy of the GNU General Public License
197+# along with this program. If not, see <http://www.gnu.org/licenses/>.
198+#
199+##############################################################################
200+
201+from osv import osv, fields
202+from datetime import date
203+from tools.translate import _
204+
205+class hsbc_export(osv.osv):
206+ '''HSBC Export'''
207+ _name = 'banking.export.hsbc'
208+ _description = __doc__
209+ _rec_name = 'execution_date'
210+
211+ _columns = {
212+ 'payment_order_ids': fields.many2many(
213+ 'payment.order',
214+ 'account_payment_order_hsbc_rel',
215+ 'banking_export_hsbc_id', 'account_order_id',
216+ 'Payment Orders',
217+ readonly=True),
218+ 'identification':
219+ fields.char('Identification', size=15, readonly=True, select=True),
220+ 'execution_date':
221+ fields.date('Execution Date',readonly=True),
222+ 'no_transactions':
223+ fields.integer('Number of Transactions', readonly=True),
224+ 'total_amount':
225+ fields.float('Total Amount', readonly=True),
226+ 'date_generated':
227+ fields.datetime('Generation Date', readonly=True, select=True),
228+ 'file':
229+ fields.binary('HSBC File', readonly=True),
230+ 'state':
231+ fields.selection([
232+ ('draft', 'Draft'),
233+ ('sent', 'Sent'),
234+ ('done', 'Reconciled'),
235+ ], 'State', readonly=True),
236+ }
237+
238+ _defaults = {
239+ 'date_generated': lambda *a: date.today().strftime('%Y-%m-%d'),
240+ 'state': lambda *a: 'draft',
241+ }
242+hsbc_export()
243+
244+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
245
246=== added file 'account_banking_uk_hsbc/account_banking_uk_hsbc.xml'
247--- account_banking_uk_hsbc/account_banking_uk_hsbc.xml 1970-01-01 00:00:00 +0000
248+++ account_banking_uk_hsbc/account_banking_uk_hsbc.xml 2012-02-10 14:09:45 +0000
249@@ -0,0 +1,85 @@
250+<?xml version="1.0" encoding="utf-8"?>
251+<!--
252+ Copyright (C) EduSense BV <http://www.edusense.nl>
253+ All rights reserved.
254+
255+ Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>)
256+
257+ The licence is in the file __openerp__.py
258+
259+
260+-->
261+<openerp>
262+ <data>
263+
264+ <!-- Make new view on HSBC exports -->
265+ <record id="view_banking_export_hsbc_form" model="ir.ui.view">
266+ <field name="name">account.banking.export.hsbc.form</field>
267+ <field name="model">banking.export.hsbc</field>
268+ <field name="type">form</field>
269+ <field name="arch" type="xml">
270+ <form string="HSBC Export">
271+ <notebook>
272+ <page string="General Information">
273+ <separator string="HSBC Information" colspan="4" />
274+ <field name="total_amount" />
275+ <field name="no_transactions" />
276+ <separator string="Processing Information" colspan="4" />
277+ <field name="execution_date" />
278+ <field name="date_generated" />
279+ <newline />
280+ <field name="file" colspan="4" />
281+ </page>
282+ <page string="Payment Orders">
283+ <field name="payment_order_ids" colspan="4" nolabel="1">
284+ <tree colors="blue:state in ('draft');gray:state in ('cancel','done');black:state in ('open')" string="Payment order">
285+ <field name="reference"/>
286+ <field name="date_created"/>
287+ <field name="date_done"/>
288+ <field name="total"/>
289+ <field name="state"/>
290+ </tree>
291+ </field>
292+ </page>
293+ </notebook>
294+ </form>
295+ </field>
296+ </record>
297+ <record id="view_banking_export_hsbc_tree" model="ir.ui.view">
298+ <field name="name">account.banking.export.hsbc.tree</field>
299+ <field name="model">banking.export.hsbc</field>
300+ <field name="type">tree</field>
301+ <field name="arch" type="xml">
302+ <tree string="HSBC Export">
303+ <field name="execution_date" search="2"/>
304+ <field name="date_generated" />
305+ </tree>
306+ </field>
307+ </record>
308+ <record model="ir.actions.act_window" id="action_account_banking_hsbcs">
309+ <field name="name">Generated HSBC files</field>
310+ <field name="type">ir.actions.act_window</field>
311+ <field name="res_model">banking.export.hsbc</field>
312+ <field name="view_type">form</field>
313+ <field name="view_mode">tree,form</field>
314+ </record>
315+
316+ <!-- Add a menu item for it -->
317+ <menuitem name="Generated HSBC files"
318+ id="menu_action_account_banking_exported_hsbc_files"
319+ parent="account_banking.menu_finance_banking_actions"
320+ action="action_account_banking_hsbcs"
321+ sequence="12"
322+ />
323+
324+ <!-- Create right menu entry to see generated files -->
325+ <act_window name="Generated HSBC files"
326+ domain="[('payment_order_ids', '=', active_id)]"
327+ res_model="banking.export.hsbc"
328+ src_model="payment.order"
329+ view_type="form"
330+ view_mode="tree,form"
331+ id="act_banking_export_hsbc_payment_order"/>
332+
333+ </data>
334+</openerp>
335
336=== added directory 'account_banking_uk_hsbc/data'
337=== added file 'account_banking_uk_hsbc/data/banking_export_hsbc.xml'
338--- account_banking_uk_hsbc/data/banking_export_hsbc.xml 1970-01-01 00:00:00 +0000
339+++ account_banking_uk_hsbc/data/banking_export_hsbc.xml 2012-02-10 14:09:45 +0000
340@@ -0,0 +1,29 @@
341+<?xml version="1.0" encoding="utf-8"?>
342+<openerp>
343+ <data>
344+ <record model="payment.mode.type" id="export_acm_or_ezone">
345+ <field name="name">ACH or EZONE</field>
346+ <field name="code">not used</field>
347+ <field name="suitable_bank_types"
348+ eval="[(6,0,[ref('base_iban.bank_iban'),ref('base.bank_normal'),])]" />
349+ <field name="ir_model_id"
350+ ref="account_banking_uk_hsbc.model_banking_export_hsbc_wizard"/>
351+ </record>
352+ <record model="payment.mode.type" id="export_faster_payment">
353+ <field name="name">Faster Payment</field>
354+ <field name="code">not used</field>
355+ <field name="suitable_bank_types"
356+ eval="[(6,0,[ref('base.bank_normal'),])]" />
357+ <field name="ir_model_id"
358+ ref="account_banking_uk_hsbc.model_banking_export_hsbc_wizard"/>
359+ </record>
360+ <record model="payment.mode.type" id="export_priority_payment">
361+ <field name="name">Priority Payment</field>
362+ <field name="code">not used</field>
363+ <field name="suitable_bank_types"
364+ eval="[(6,0,[ref('base_iban.bank_iban'),ref('base.bank_normal'),])]" />
365+ <field name="ir_model_id"
366+ ref="account_banking_uk_hsbc.model_banking_export_hsbc_wizard"/>
367+ </record>
368+ </data>
369+</openerp>
370
371=== added file 'account_banking_uk_hsbc/hsbc_mt940.py'
372--- account_banking_uk_hsbc/hsbc_mt940.py 1970-01-01 00:00:00 +0000
373+++ account_banking_uk_hsbc/hsbc_mt940.py 2012-02-10 14:09:45 +0000
374@@ -0,0 +1,161 @@
375+# -*- encoding: utf-8 -*-
376+##############################################################################
377+#
378+# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
379+# All Rights Reserved
380+#
381+# This program is free software: you can redistribute it and/or modify
382+# it under the terms of the GNU Affero General Public License as
383+# published by the Free Software Foundation, either version 3 of the
384+# License, or (at your option) any later version.
385+#
386+# This program is distributed in the hope that it will be useful,
387+# but WITHOUT ANY WARRANTY; without even the implied warranty of
388+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
389+# GNU Affero General Public License for more details.
390+#
391+# You should have received a copy of the GNU Affero General Public License
392+# along with this program. If not, see <http://www.gnu.org/licenses/>.
393+#
394+##############################################################################
395+# Import of HSBC data in Swift MT940 format
396+#
397+
398+from account_banking.parsers import models
399+from account_banking.parsers.convert import str2date
400+from tools.translate import _
401+from mt940_parser import HSBCParser
402+import re
403+
404+bt = models.mem_bank_transaction
405+
406+def record2float(record, value):
407+ if record['creditmarker'][-1] == 'C':
408+ return float(record[value])
409+ return -float(record[value])
410+
411+class transaction(models.mem_bank_transaction):
412+
413+ mapping = {
414+ 'execution_date' : 'valuedate',
415+ 'effective_date' : 'valuedate',
416+ 'local_currency' : 'currency',
417+ 'transfer_type' : 'bookingcode',
418+ 'reference' : 'custrefno',
419+ 'message' : 'furtherinfo'
420+ }
421+
422+ type_map = {
423+ 'TRF': bt.ORDER,
424+ }
425+
426+ def __init__(self, record, *args, **kwargs):
427+ '''
428+ Transaction creation
429+ '''
430+ super(transaction, self).__init__(*args, **kwargs)
431+ for key, value in self.mapping.iteritems():
432+ if record.has_key(value):
433+ setattr(self, key, record[value])
434+
435+ self.transferred_amount = record2float(record, 'amount')
436+
437+ #print record.get('bookingcode')
438+ if not self.is_valid():
439+ print "Invalid: %s" % record
440+ def is_valid(self):
441+ '''
442+ We don't have remote_account so override base
443+ '''
444+ return (self.execution_date
445+ and self.transferred_amount and True) or False
446+
447+class statement(models.mem_bank_statement):
448+ '''
449+ Bank statement imported data
450+ '''
451+
452+ def import_record(self, record):
453+ def _transmission_number():
454+ self.id = record['transref']
455+ def _account_number():
456+ # The wizard doesn't check for sort code
457+ self.local_account = record['sortcode'] + ' ' + record['accnum'].zfill(8)
458+ def _statement_number():
459+ self.id = '-'.join([self.id, self.local_account, record['statementnr']])
460+ def _opening_balance():
461+ self.start_balance = record2float(record,'startingbalance')
462+ self.local_currency = record['currencycode']
463+ def _closing_balance():
464+ self.end_balance = record2float(record, 'endingbalance')
465+ self.date = record['bookingdate']
466+ def _transaction_new():
467+ self.transactions.append(transaction(record))
468+ def _transaction_info():
469+ self.transaction_info(record)
470+ def _not_used():
471+ print "Didn't use record: %s" % (record,)
472+
473+ rectypes = {
474+ '20' : _transmission_number,
475+ '25' : _account_number,
476+ '28' : _statement_number,
477+ '28C': _statement_number,
478+ '60F': _opening_balance,
479+ '62F': _closing_balance,
480+ #'64' : _forward_available,
481+ #'62M': _interim_balance,
482+ '61' : _transaction_new,
483+ '86' : _transaction_info,
484+ }
485+
486+ rectypes.get(record['recordid'], _not_used)()
487+
488+ def transaction_info(self, record):
489+ '''
490+ Add extra information to transaction
491+ '''
492+ # Additional information for previous transaction
493+ if len(self.transactions) < 1:
494+ raise_error('Received additional information for non existent transaction', record)
495+
496+ transaction = self.transactions[-1]
497+
498+ transaction.id = ','.join([record[k] for k in ['infoline{0}'.format(i) for i in range(1,5)] if record.has_key(k)])
499+
500+def raise_error(message, line):
501+ raise osv.except_osv(_('Import error'),
502+ 'Error in import:%s\n\n%s' % (message, line))
503+
504+class parser_hsbc_mt940(models.parser):
505+ code = 'HSBC-MT940'
506+ name = _('HSBC Swift MT940 statement export')
507+ country_code = 'GB'
508+ doc = _('''\
509+ This format is available through
510+ the HSBC web interface.
511+ ''')
512+
513+ def parse(self, data):
514+ result = []
515+ parser = HSBCParser()
516+ # Split into statements
517+ statements = [st for st in re.split('[\r\n]*(?=:20:)', data)]
518+ # Split by records
519+ statement_list = [re.split('[\r\n ]*(?=:\d\d[\w]?:)', st) for st in statements]
520+
521+ for statement_lines in statement_list:
522+ stmnt = statement()
523+ records = [parser.parse_record(record) for record in statement_lines]
524+ [stmnt.import_record(r) for r in records if r is not None]
525+
526+
527+ if stmnt.is_valid():
528+ result.append(stmnt)
529+ else:
530+ print "Invalid Statement:"
531+ print records[0]
532+
533+ return result
534+
535+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
536
537=== added file 'account_banking_uk_hsbc/mt940_parser.py'
538--- account_banking_uk_hsbc/mt940_parser.py 1970-01-01 00:00:00 +0000
539+++ account_banking_uk_hsbc/mt940_parser.py 2012-02-10 14:09:45 +0000
540@@ -0,0 +1,156 @@
541+#!/usr/bin/env python
542+# -*- encoding: utf-8 -*-
543+##############################################################################
544+#
545+# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
546+# All Rights Reserved
547+#
548+# This program is free software: you can redistribute it and/or modify
549+# it under the terms of the GNU Affero General Public License as
550+# published by the Free Software Foundation, either version 3 of the
551+# License, or (at your option) any later version.
552+#
553+# This program is distributed in the hope that it will be useful,
554+# but WITHOUT ANY WARRANTY; without even the implied warranty of
555+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
556+# GNU Affero General Public License for more details.
557+#
558+# You should have received a copy of the GNU Affero General Public License
559+# along with this program. If not, see <http://www.gnu.org/licenses/>.
560+#
561+##############################################################################
562+
563+"""
564+Parser for HSBC UK MT940 format files
565+Based on fi_patu's parser
566+"""
567+import re
568+from datetime import datetime
569+
570+class HSBCParser(object):
571+
572+ def __init__( self ):
573+ recparse = dict()
574+ patterns = {'ebcdic': "\w/\?:\(\).,'+{} -"}
575+
576+ # MT940 header
577+ recparse["20"] = ":(?P<recordid>20):(?P<transref>.{1,16})"
578+ recparse["25"] = ":(?P<recordid>25):(?P<sortcode>\d{6})(?P<accnum>\d{1,29})"
579+ recparse["28"] = ":(?P<recordid>28C?):(?P<statementnr>.{1,8})"
580+
581+ # Opening balance 60F
582+ recparse["60F"] = ":(?P<recordid>60F):(?P<creditmarker>[CD])" \
583+ + "(?P<prevstmtdate>\d{6})(?P<currencycode>.{3})" \
584+ + "(?P<startingbalance>[\d,]{1,15})"
585+
586+ # Transaction
587+ recparse["61"] = """\
588+:(?P<recordid>61):\
589+(?P<valuedate>\d{6})(?P<bookingdate>\d{4})?\
590+(?P<creditmarker>R?[CD])\
591+(?P<currency>[A-Z])?\
592+(?P<amount>[\d,]{1,15})\
593+(?P<bookingcode>[A-Z][A-Z0-9]{3})\
594+(?P<custrefno>[%(ebcdic)s]{1,16})\
595+(?://)\
596+(?P<bankref>[%(ebcdic)s]{1,16})?\
597+(?:\n(?P<furtherinfo>[%(ebcdic)s]))?\
598+""" % (patterns)
599+
600+ # Further info
601+ recparse["86"] = ":(?P<recordid>86):" \
602+ + "(?P<infoline1>.{1,80})?" \
603+ + "(?:\n(?P<infoline2>.{1,80}))?" \
604+ + "(?:\n(?P<infoline3>.{1,80}))?" \
605+ + "(?:\n(?P<infoline4>.{1,80}))?" \
606+ + "(?:\n(?P<infoline5>.{1,80}))?"
607+
608+ # Forward available balance (64) / Closing balance (62F) / Interim balance (62M)
609+ recparse["64"] = ":(?P<recordid>64|62[FM]):" \
610+ + "(?P<creditmarker>[CD])" \
611+ + "(?P<bookingdate>\d{6})(?P<currencycode>.{3})" \
612+ + "(?P<endingbalance>[\d,]{1,15})"
613+
614+ for record in recparse:
615+ recparse[record] = re.compile(recparse[record])
616+ self.recparse = recparse
617+
618+
619+ def parse_record(self, line):
620+ """
621+ Parse record using regexps and apply post processing
622+ """
623+ for matcher in self.recparse:
624+ matchobj = self.recparse[matcher].match(line)
625+ if matchobj:
626+ break
627+ if not matchobj:
628+ print " **** failed to match line '%s'" % (line)
629+ return
630+ # Strip strings
631+ matchdict = matchobj.groupdict()
632+
633+ # Remove members set to None
634+ matchdict=dict([(k,v) for k,v in matchdict.iteritems() if v])
635+
636+ matchkeys = set(matchdict.keys())
637+ needstrip = set(["transref", "accnum", "statementnr", "custrefno",
638+ "bankref", "furtherinfo", "infoline1", "infoline2", "infoline3",
639+ "infoline4", "infoline5", "startingbalance", "endingbalance"])
640+ for field in matchkeys & needstrip:
641+ matchdict[field] = matchdict[field].strip()
642+
643+ # Convert to float. Comma is decimal separator
644+ needsfloat = set(["startingbalance", "endingbalance", "amount"])
645+ for field in matchkeys & needsfloat:
646+ matchdict[field] = float(matchdict[field].replace(',','.'))
647+
648+ # Convert date fields
649+ needdate = set(["prevstmtdate", "valuedate", "bookingdate"])
650+ for field in matchkeys & needdate:
651+ datestring = matchdict[field]
652+
653+ post_check = False
654+ if len(datestring) == 4 and field=="bookingdate" and matchdict.has_key("valuedate"):
655+ # Get year from valuedate
656+ datestring = matchdict['valuedate'].strftime('%y') + datestring
657+ post_check = True
658+ try:
659+ matchdict[field] = datetime.strptime(datestring,'%y%m%d')
660+ if post_check and matchdict[field] > matchdict["valuedate"]:
661+ matchdict[field]=matchdict[field].replace(year=matchdict[field].year-1)
662+ except ValueError:
663+ matchdict[field] = None
664+
665+ return matchdict
666+
667+ def parse(self, data):
668+ records = []
669+ # Some records are multiline
670+ for line in data:
671+ if len(line) <= 1:
672+ continue
673+ if line[0] == ':' and len(line) > 1:
674+ records.append(line)
675+ else:
676+ records[-1] = '\n'.join([records[-1], line])
677+
678+ output = []
679+ for rec in records:
680+ output.append(self.parse_record(rec))
681+
682+ return output
683+
684+def parse_file(filename):
685+ hsbcfile = open(filename, "r")
686+ p = HSBCParser().parse(hsbcfile.readlines())
687+
688+def main():
689+ """The main function, currently just calls a dummy filename
690+
691+ :returns: description
692+ """
693+ parse_file("testfile")
694+
695+if __name__ == '__main__':
696+ main()
697
698=== added directory 'account_banking_uk_hsbc/wizard'
699=== added file 'account_banking_uk_hsbc/wizard/__init__.py'
700--- account_banking_uk_hsbc/wizard/__init__.py 1970-01-01 00:00:00 +0000
701+++ account_banking_uk_hsbc/wizard/__init__.py 2012-02-10 14:09:45 +0000
702@@ -0,0 +1,23 @@
703+# -*- encoding: utf-8 -*-
704+##############################################################################
705+#
706+# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
707+# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
708+# All Rights Reserved
709+#
710+# This program is free software: you can redistribute it and/or modify
711+# it under the terms of the GNU General Public License as published by
712+# the Free Software Foundation, either version 3 of the License, or
713+# (at your option) any later version.
714+#
715+# This program is distributed in the hope that it will be useful,
716+# but WITHOUT ANY WARRANTY; without even the implied warranty of
717+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
718+# GNU General Public License for more details.
719+#
720+# You should have received a copy of the GNU General Public License
721+# along with this program. If not, see <http://www.gnu.org/licenses/>.
722+#
723+##############################################################################
724+
725+import export_hsbc
726\ No newline at end of file
727
728=== added file 'account_banking_uk_hsbc/wizard/export_hsbc.py'
729--- account_banking_uk_hsbc/wizard/export_hsbc.py 1970-01-01 00:00:00 +0000
730+++ account_banking_uk_hsbc/wizard/export_hsbc.py 2012-02-10 14:09:45 +0000
731@@ -0,0 +1,400 @@
732+# -*- encoding: utf-8 -*-
733+##############################################################################
734+#
735+# Copyright (C) 2009 EduSense BV (<http://www.edusense.nl>).
736+# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
737+# All Rights Reserved
738+#
739+# This program is free software: you can redistribute it and/or modify
740+# it under the terms of the GNU General Public License as published by
741+# the Free Software Foundation, either version 3 of the License, or
742+# (at your option) any later version.
743+#
744+# This program is distributed in the hope that it will be useful,
745+# but WITHOUT ANY WARRANTY; without even the implied warranty of
746+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
747+# GNU General Public License for more details.
748+#
749+# You should have received a copy of the GNU General Public License
750+# along with this program. If not, see <http://www.gnu.org/licenses/>.
751+#
752+##############################################################################
753+
754+import base64
755+from datetime import datetime, date, timedelta
756+from osv import osv, fields
757+from tools.translate import _
758+from decimal import Decimal
759+import paymul
760+import string
761+import random
762+import netsvc
763+
764+def strpdate(arg, format='%Y-%m-%d'):
765+ '''shortcut'''
766+ return datetime.strptime(arg, format).date()
767+
768+def strfdate(arg, format='%Y-%m-%d'):
769+ '''shortcut'''
770+ return arg.strftime(format)
771+
772+class banking_export_hsbc_wizard(osv.osv_memory):
773+ _name = 'banking.export.hsbc.wizard'
774+ _description = 'HSBC Export'
775+ _columns = {
776+ 'state': fields.selection(
777+ [
778+ ('create', 'Create'),
779+ ('finish', 'Finish')
780+ ],
781+ 'State',
782+ readonly=True,
783+ ),
784+ 'test': fields.boolean(),
785+ 'reference': fields.char(
786+ 'Reference', size=35,
787+ help=('The bank will use this reference in feedback communication '
788+ 'to refer to this run. 35 characters are available.'
789+ ),
790+ ),
791+ 'execution_date_create': fields.date(
792+ 'Execution Date',
793+ help=('This is the date the file should be processed by the bank. '
794+ 'Don\'t choose a date beyond the nearest date in your '
795+ 'payments. The latest allowed date is 30 days from now.\n'
796+ 'Please keep in mind that banks only execute on working days '
797+ 'and typically use a delay of two days between execution date '
798+ 'and effective transfer date.'
799+ ),
800+ ),
801+ 'file_id': fields.many2one(
802+ 'banking.export.hsbc',
803+ 'hsbc File',
804+ readonly=True
805+ ),
806+ 'file': fields.related(
807+ 'file_id', 'file', type='binary',
808+ readonly=True,
809+ string='File',
810+ ),
811+ 'execution_date_finish': fields.related(
812+ 'file_id', 'execution_date', type='date',
813+ readonly=True,
814+ string='Execution Date',
815+ ),
816+ 'total_amount': fields.related(
817+ 'file_id', 'total_amount',
818+ type='float',
819+ string='Total Amount',
820+ readonly=True,
821+ ),
822+ 'no_transactions': fields.integer(
823+ 'Number of Transactions',
824+ readonly=True,
825+ ),
826+ 'payment_order_ids': fields.many2many(
827+ 'payment.order', 'rel_wiz_payorders', 'wizard_id',
828+ 'payment_order_id', 'Payment Orders',
829+ readonly=True,
830+ ),
831+ }
832+
833+ logger = netsvc.Logger()
834+
835+ def create(self, cursor, uid, wizard_data, context=None):
836+ '''
837+ Retrieve a sane set of default values based on the payment orders
838+ from the context.
839+ '''
840+
841+ if not 'execution_date_create' in wizard_data:
842+ po_ids = context.get('active_ids', [])
843+ po_model = self.pool.get('payment.order')
844+ pos = po_model.browse(cursor, uid, po_ids)
845+
846+ execution_date = date.today()
847+
848+ for po in pos:
849+ if po.date_prefered == 'fixed' and po.date_planned:
850+ execution_date = strpdate(po.date_planned)
851+ elif po.date_prefered == 'due':
852+ for line in po.line_ids:
853+ if line.move_line_id.date_maturity:
854+ date_maturity = strpdate(line.move_line_id.date_maturity)
855+ if date_maturity < execution_date:
856+ execution_date = date_maturity
857+
858+ execution_date = max(execution_date, date.today())
859+
860+ # The default reference contains a /, which is invalid for PAYMUL
861+ reference = pos[0].reference.replace('/', ' ')
862+
863+ wizard_data.update({
864+ 'execution_date_create': strfdate(execution_date),
865+ 'reference': reference,
866+ 'payment_order_ids': [[6, 0, po_ids]],
867+ 'state': 'create',
868+ })
869+
870+ return super(banking_export_hsbc_wizard, self).create(
871+ cursor, uid, wizard_data, context)
872+
873+ def _create_account(self, oe_account):
874+ currency = None # let the receiving bank select the currency from the batch
875+ holder = oe_account.owner_name or oe_account.partner_id.name
876+ self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'Create account %s' % (holder))
877+ self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'-- %s' % (oe_account.country_id.code))
878+ self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'-- %s' % (oe_account.acc_number))
879+ self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'-- %s' % (oe_account.iban))
880+
881+
882+ if oe_account.iban:
883+ self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'IBAN: %s' % (oe_account.iban))
884+ paymul_account = paymul.IBANAccount(
885+ iban=oe_account.iban,
886+ bic=oe_account.bank.bic,
887+ holder=holder,
888+ currency=currency,
889+ )
890+ transaction_kwargs = {
891+ 'charges': paymul.CHARGES_EACH_OWN,
892+ }
893+ elif oe_account.country_id.code == 'GB':
894+ self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'GB: %s %s' % (oe_account.country_id.code,oe_account.acc_number))
895+ split = oe_account.acc_number.split(" ", 2)
896+ if len(split) == 2:
897+ sortcode, accountno = split
898+ else:
899+ raise osv.except_osv(
900+ _('Error'),
901+ "Invalid GB acccount number '%s'" % oe_account.acc_number)
902+ paymul_account = paymul.UKAccount(
903+ number=accountno,
904+ sortcode=sortcode,
905+ holder=holder,
906+ currency=currency,
907+ )
908+ transaction_kwargs = {
909+ 'charges': paymul.CHARGES_PAYEE,
910+ }
911+ elif oe_account.country_id.code in ('US','CA'):
912+ self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'US/CA: %s %s' % (oe_account.country_id.code,oe_account.acc_number))
913+ split = oe_account.acc_number.split(' ', 2)
914+ if len(split) == 2:
915+ sortcode, accountno = split
916+ else:
917+ raise osv.except_osv(
918+ _('Error'),
919+ "Invalid %s account number '%s'" % (oe_account.country_id.code,oe_account.acc_number))
920+ paymul_account = paymul.NorthAmericanAccount(
921+ number=accountno,
922+ sortcode=sortcode,
923+ holder=holder,
924+ currency=currency,
925+ swiftcode=oe_account.bank.bic,
926+ country=oe_account.country_id.code,
927+ #origin_country=origin_country
928+ )
929+ transaction_kwargs = {
930+ 'charges': paymul.CHARGES_PAYEE,
931+ }
932+ transaction_kwargs = {
933+ 'charges': paymul.CHARGES_PAYEE,
934+ }
935+ else:
936+ self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'SWIFT Account: %s' % (oe_account.country_id.code))
937+ split = oe_account.acc_number.split(' ', 2)
938+ if len(split) == 2:
939+ sortcode, accountno = split
940+ else:
941+ raise osv.except_osv(
942+ _('Error'),
943+ "Invalid %s account number '%s'" % (oe_account.country_id.code,oe_account.acc_number))
944+ paymul_account = paymul.SWIFTAccount(
945+ number=accountno,
946+ sortcode=sortcode,
947+ holder=holder,
948+ currency=currency,
949+ swiftcode=oe_account.bank.bic,
950+ country=oe_account.country_id.code,
951+ )
952+ transaction_kwargs = {
953+ 'charges': paymul.CHARGES_PAYEE,
954+ }
955+ transaction_kwargs = {
956+ 'charges': paymul.CHARGES_PAYEE,
957+ }
958+
959+ return paymul_account, transaction_kwargs
960+
961+ def _create_transaction(self, line):
962+ # Check on missing partner of bank account (this can happen!)
963+ if not line.bank_id or not line.bank_id.partner_id:
964+ raise osv.except_osv(
965+ _('Error'),
966+ _('There is insufficient information.\r\n'
967+ 'Both destination address and account '
968+ 'number must be provided'
969+ )
970+ )
971+
972+ self.logger.notifyChannel('paymul', netsvc.LOG_INFO, '====')
973+ dest_account, transaction_kwargs = self._create_account(line.bank_id)
974+
975+ means = {'ACH or EZONE': paymul.MEANS_ACH_OR_EZONE,
976+ 'Faster Payment': paymul.MEANS_FASTER_PAYMENT,
977+ 'Priority Payment': paymul.MEANS_PRIORITY_PAYMENT}.get(line.order_id.mode.type.name)
978+ if means is None:
979+ raise osv.except_osv('Error', "Invalid payment type mode for HSBC '%s'" % line.order_id.mode.type.name)
980+
981+ if not line.info_partner:
982+ raise osv.except_osv('Error', "No default address for transaction '%s'" % line.name)
983+
984+ try:
985+ return paymul.Transaction(
986+ amount=Decimal(str(line.amount_currency)),
987+ currency=line.currency.name,
988+ account=dest_account,
989+ means=means,
990+ name_address=line.info_partner,
991+ customer_reference=line.name,
992+ payment_reference=line.name,
993+ **transaction_kwargs
994+ )
995+ except ValueError as exc:
996+ raise osv.except_osv(
997+ _('Error'),
998+ _('Transaction invalid: ') + str(exc)
999+ )
1000+
1001+ def wizard_export(self, cursor, uid, wizard_data_ids, context):
1002+ '''
1003+ Wizard to actually create the HSBC file
1004+ '''
1005+
1006+ wizard_data = self.browse(cursor, uid, wizard_data_ids, context)[0]
1007+ result_model = self.pool.get('banking.export.hsbc')
1008+ payment_orders = wizard_data.payment_order_ids
1009+
1010+
1011+ try:
1012+ self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'Source - %s (%s) %s' % (payment_orders[0].mode.bank_id.partner_id.name, payment_orders[0].mode.bank_id.acc_number, payment_orders[0].mode.bank_id.country_id.code))
1013+ src_account = self._create_account(
1014+ payment_orders[0].mode.bank_id,
1015+ )[0]
1016+ except ValueError as exc:
1017+ raise osv.except_osv(
1018+ _('Error'),
1019+ _('Source account invalid: ') + str(exc)
1020+ )
1021+
1022+ if not isinstance(src_account, paymul.UKAccount):
1023+ raise osv.except_osv(
1024+ _('Error'),
1025+ _("Your company's bank account has to have a valid UK "
1026+ "account number (not IBAN)" + str(type(src_account)))
1027+ )
1028+
1029+ try:
1030+ self.logger.notifyChannel('paymul', netsvc.LOG_INFO, 'Create transactions...')
1031+ transactions = []
1032+ for po in payment_orders:
1033+ transactions += [self._create_transaction(l) for l in po.line_ids]
1034+
1035+ batch = paymul.Batch(
1036+ exec_date=strpdate(wizard_data.execution_date_create),
1037+ reference=wizard_data.reference,
1038+ debit_account=src_account,
1039+ name_address=payment_orders[0].line_ids[0].info_owner,
1040+ )
1041+ batch.transactions = transactions
1042+ except ValueError as exc:
1043+ raise osv.except_osv(
1044+ _('Error'),
1045+ _('Batch invalid: ') + str(exc)
1046+ )
1047+
1048+ # Generate random identifier until an unused one is found
1049+ while True:
1050+ ref = ''.join(random.choice(string.ascii_uppercase + string.digits)
1051+ for x in range(15))
1052+
1053+ ids = result_model.search(cursor, uid, [
1054+ ('identification', '=', ref)
1055+ ])
1056+
1057+ if not ids:
1058+ break
1059+
1060+ message = paymul.Message(reference=ref)
1061+ message.batches.append(batch)
1062+ interchange = paymul.Interchange(client_id='CLIENTID',
1063+ reference=ref,
1064+ message=message)
1065+
1066+ export_result = {
1067+ 'identification': interchange.reference,
1068+ 'execution_date': batch.exec_date,
1069+ 'total_amount': batch.amount(),
1070+ 'no_transactions': len(batch.transactions),
1071+ 'file': base64.encodestring(str(interchange)),
1072+ 'payment_order_ids': [
1073+ [6, 0, [po.id for po in payment_orders]]
1074+ ],
1075+ }
1076+ file_id = result_model.create(cursor, uid, export_result, context)
1077+
1078+ self.write(cursor, uid, [wizard_data_ids[0]], {
1079+ 'file_id': file_id,
1080+ 'no_transactions' : len(batch.transactions),
1081+ 'state': 'finish',
1082+ }, context)
1083+
1084+ return {
1085+ 'name': _('HSBC Export'),
1086+ 'view_type': 'form',
1087+ 'view_mode': 'form',
1088+ 'res_model': self._name,
1089+ 'domain': [],
1090+ 'context': dict(context, active_ids=wizard_data_ids),
1091+ 'type': 'ir.actions.act_window',
1092+ 'target': 'new',
1093+ 'res_id': wizard_data_ids[0] or False,
1094+ }
1095+
1096+ def wizard_cancel(self, cursor, uid, ids, context):
1097+ '''
1098+ Cancel the export: just drop the file
1099+ '''
1100+
1101+ wizard_data = self.browse(cursor, uid, ids, context)[0]
1102+ result_model = self.pool.get('banking.export.hsbc')
1103+
1104+ try:
1105+ result_model.unlink(cursor, uid, wizard_data.file_id.id)
1106+ except AttributeError:
1107+ # file_id missing, wizard storage gone, server was restarted
1108+ pass
1109+
1110+ return {'type': 'ir.actions.act_window_close'}
1111+
1112+ def wizard_save(self, cursor, uid, ids, context):
1113+ '''
1114+ Save the export: mark all payments in the file as 'sent'
1115+ '''
1116+
1117+ wizard_data = self.browse(cursor, uid, ids, context)[0]
1118+ result_model = self.pool.get('banking.export.hsbc')
1119+ po_model = self.pool.get('payment.order')
1120+
1121+ result_model.write(cursor, uid, [wizard_data.file_id.id],
1122+ {'state':'sent'})
1123+
1124+ po_ids = [po.id for po in wizard_data.payment_order_ids]
1125+ po_model.action_sent(cursor, uid, po_ids)
1126+
1127+ return {'type': 'ir.actions.act_window_close'}
1128+
1129+banking_export_hsbc_wizard()
1130+
1131+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1132
1133=== added file 'account_banking_uk_hsbc/wizard/export_hsbc_view.xml'
1134--- account_banking_uk_hsbc/wizard/export_hsbc_view.xml 1970-01-01 00:00:00 +0000
1135+++ account_banking_uk_hsbc/wizard/export_hsbc_view.xml 2012-02-10 14:09:45 +0000
1136@@ -0,0 +1,37 @@
1137+<?xml version="1.0" encoding="utf-8"?>
1138+<openerp>
1139+ <data>
1140+ <record id="wizard_banking_export_wizard_view" model="ir.ui.view">
1141+ <field name="name">banking.export.hsbc.wizard.view</field>
1142+ <field name="model">banking.export.hsbc.wizard</field>
1143+ <field name="type">form</field>
1144+ <field name="arch" type="xml">
1145+ <form string="HSBC Export">
1146+ <field name="state" invisible="True"/>
1147+ <group states="create">
1148+ <separator colspan="4" string="Processing Details"/>
1149+ <field name="execution_date_create"/>
1150+ <field name="test"/>
1151+ <separator colspan="4" string="Reference for further communication"/>
1152+ <field name="reference" colspan="2"/>
1153+ <separator colspan="4" string="Additional message for all transactions"/>
1154+ <newline/>
1155+ <button icon="gtk-close" special="cancel" string="Cancel"/>
1156+ <button icon="gtk-ok" string="Export" name="wizard_export" type="object"/>
1157+ </group>
1158+ <group states="finish">
1159+ <field name="total_amount"/>
1160+ <field name="no_transactions"/>
1161+ <field name="execution_date_finish"/>
1162+ <newline/>
1163+ <!--<field name="file_id"/>-->
1164+ <field name="file"/>
1165+ <newline/>
1166+ <button icon="gtk-close" string="Cancel" name="wizard_cancel" type="object"/>
1167+ <button icon="gtk-ok" string="Finish" name="wizard_save" type="object"/>
1168+ </group>
1169+ </form>
1170+ </field>
1171+ </record>
1172+ </data>
1173+</openerp>
1174
1175=== added file 'account_banking_uk_hsbc/wizard/paymul.py'
1176--- account_banking_uk_hsbc/wizard/paymul.py 1970-01-01 00:00:00 +0000
1177+++ account_banking_uk_hsbc/wizard/paymul.py 2012-02-10 14:09:45 +0000
1178@@ -0,0 +1,629 @@
1179+# -*- encoding: utf-8 -*-
1180+##############################################################################
1181+#
1182+# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
1183+# All Rights Reserved
1184+#
1185+# This program is free software: you can redistribute it and/or modify
1186+# it under the terms of the GNU General Public License as published by
1187+# the Free Software Foundation, either version 3 of the License, or
1188+# (at your option) any later version.
1189+#
1190+# This program is distributed in the hope that it will be useful,
1191+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1192+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1193+# GNU General Public License for more details.
1194+#
1195+# You should have received a copy of the GNU General Public License
1196+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1197+#
1198+##############################################################################
1199+
1200+from account_banking import sepa
1201+from decimal import Decimal
1202+import datetime
1203+import re
1204+import unicodedata
1205+
1206+def strip_accents(string):
1207+ return unicodedata.normalize('NFKD', unicode(string)).encode('ASCII', 'ignore')
1208+
1209+def split_account_holder(holder):
1210+ holder_parts = holder.split("\n")
1211+
1212+ try:
1213+ line2 = holder_parts[1]
1214+ except IndexError:
1215+ line2 = ''
1216+
1217+ return holder_parts[0], line2
1218+
1219+"""
1220+The standard says alphanumeric characters, but spaces are also allowed
1221+"""
1222+def edifact_isalnum(s):
1223+ return bool(re.match(r'^[A-Za-z0-9 ]*$', s))
1224+
1225+def edifact_digits(val, digits=None, mindigits=None):
1226+ if digits is None:
1227+ digits = ''
1228+ if mindigits is None:
1229+ mindigits = digits
1230+
1231+ pattern = r'^[0-9]{' + str(mindigits) + ',' + str(digits) + r'}$'
1232+ return bool(re.match(pattern, str(val)))
1233+
1234+def edifact_isalnum_size(val, digits):
1235+ pattern = r'^[A-Za-z0-9 ]{' + str(digits) + ',' + str(digits) + r'}$'
1236+ return bool(re.match(pattern, str(val)))
1237+
1238+class HasCurrency(object):
1239+ def _get_currency(self):
1240+ return self._currency
1241+
1242+ def _set_currency(self, currency):
1243+ if currency is None:
1244+ self._currency = None
1245+ else:
1246+ if not len(currency) <= 3:
1247+ raise ValueError("Currency must be <= 3 characters long: " +
1248+ str(currency))
1249+
1250+ if not edifact_isalnum(currency):
1251+ raise ValueError("Currency must be alphanumeric: " + str(currency))
1252+
1253+ self._currency = currency.upper()
1254+
1255+ currency = property(_get_currency, _set_currency)
1256+
1257+
1258+class LogicalSection(object):
1259+
1260+ def __str__(self):
1261+ segments = self.segments()
1262+
1263+ def format_segment(segment):
1264+ return '+'.join([':'.join([str(strip_accents(y)) for y in x]) for x in segment]) + "'"
1265+
1266+ return "\n".join([format_segment(s) for s in segments])
1267+
1268+
1269+def _fii_segment(self, party_qualifier):
1270+ holder = split_account_holder(self.holder)
1271+ account_identification = [self.number.replace(' ',''), holder[0]]
1272+ if holder[1] or self.currency:
1273+ account_identification.append(holder[1])
1274+ if self.currency:
1275+ account_identification.append(self.currency)
1276+ return [
1277+ ['FII'],
1278+ [party_qualifier],
1279+ account_identification,
1280+ self.institution_identification,
1281+ [self.country],
1282+ ]
1283+
1284+
1285+class UKAccount(HasCurrency):
1286+ def _get_number(self):
1287+ return self._number
1288+
1289+ def _set_number(self, number):
1290+ if not edifact_digits(number, 8):
1291+ raise ValueError("Account number must be 8 digits long: " +
1292+ str(number))
1293+
1294+ self._number = number
1295+
1296+ number = property(_get_number, _set_number)
1297+
1298+ def _get_sortcode(self):
1299+ return self._sortcode
1300+
1301+ def _set_sortcode(self, sortcode):
1302+ if not edifact_digits(sortcode, 6):
1303+ raise ValueError("Account sort code must be 6 digits long: " +
1304+ str(sortcode))
1305+
1306+ self._sortcode = sortcode
1307+
1308+ sortcode = property(_get_sortcode, _set_sortcode)
1309+
1310+ def _get_holder(self):
1311+ return self._holder
1312+
1313+ def _set_holder(self, holder):
1314+ holder_parts = split_account_holder(holder)
1315+
1316+ if not len(holder_parts[0]) <= 35:
1317+ raise ValueError("Account holder must be <= 35 characters long: " + str(holder_parts[0]))
1318+
1319+ if not len(holder_parts[1]) <= 35:
1320+ raise ValueError("Second line of account holder must be <= 35 characters long: " + str(holder_parts[1]))
1321+
1322+ if not edifact_isalnum(holder_parts[0]):
1323+ raise ValueError("Account holder must be alphanumeric: " + str(holder_parts[0]))
1324+
1325+ if not edifact_isalnum(holder_parts[1]):
1326+ raise ValueError("Second line of account holder must be alphanumeric: " + str(holder_parts[1]))
1327+
1328+ self._holder = holder.upper()
1329+
1330+ holder = property(_get_holder, _set_holder)
1331+
1332+ def __init__(self, number, holder, currency, sortcode):
1333+ self.number = number
1334+ self.holder = holder
1335+ self.currency = currency
1336+ self.sortcode = sortcode
1337+ self.country = 'GB'
1338+ self.institution_identification = ['', '', '', self.sortcode, 154, 133]
1339+
1340+ def fii_bf_segment(self):
1341+ return _fii_segment(self, 'BF')
1342+
1343+ def fii_or_segment(self):
1344+ return _fii_segment(self, 'OR')
1345+
1346+
1347+class NorthAmericanAccount(UKAccount):
1348+
1349+ def _set_account_ident(self):
1350+ if self.origin_country in ('US','CA'):
1351+ # Use the routing number
1352+ account_ident = ['', '', '', self.sortcode, 155, 114]
1353+ else:
1354+ # Using the BIC/Swift Code
1355+ account_ident = [self.bic, 25, 5, '', '', '']
1356+ return account_ident
1357+
1358+ def _set_sortcode(self, sortcode):
1359+ if not edifact_digits(sortcode, 9):
1360+ raise ValueError("Account routing number must be 9 digits long: " +
1361+ str(sortcode))
1362+
1363+
1364+ self._sortcode = sortcode
1365+
1366+ def _get_sortcode(self):
1367+ return self._sortcode
1368+
1369+ sortcode = property(_get_sortcode, _set_sortcode)
1370+
1371+ def _set_bic(self, bic):
1372+ if not edifact_isalnum_size(bic, 8) and not edifact_isalnum_size(bic, 11):
1373+ raise ValueError("Account BIC/Swift code must be 8 or 11 characters long: " +
1374+ str(bic))
1375+ self._bic = bic
1376+
1377+ def _get_bic(self):
1378+ return self._bic
1379+
1380+ bic = property(_get_bic, _set_bic)
1381+
1382+ def _set_number(self, number):
1383+ if not edifact_digits(number, mindigits=1):
1384+ raise ValueError("Account number is invalid: " +
1385+ str(number))
1386+
1387+ self._number = number
1388+
1389+ def _get_number(self):
1390+ return self._number
1391+
1392+ number = property(_get_number, _set_number)
1393+
1394+ def __init__(self, number, holder, currency, sortcode, swiftcode, country, origin_country=None):
1395+ self.number = number
1396+ self.holder = holder
1397+ self.currency = currency
1398+ self.sortcode = sortcode
1399+ self.country = country
1400+ self.bic = swiftcode
1401+ self.origin_country = origin_country
1402+ self.institution_identification = self._set_account_ident()
1403+
1404+
1405+class SWIFTAccount(UKAccount):
1406+
1407+ def _set_account_ident(self):
1408+ # Using the BIC/Swift Code
1409+ return [self.bic, 25, 5, '', '', '']
1410+
1411+ def _set_sortcode(self, sortcode):
1412+ self._sortcode = sortcode
1413+
1414+ def _get_sortcode(self):
1415+ return self._sortcode
1416+
1417+ sortcode = property(_get_sortcode, _set_sortcode)
1418+
1419+ def _set_bic(self, bic):
1420+ if not edifact_isalnum_size(bic, 8) and not edifact_isalnum_size(bic, 11):
1421+ raise ValueError("Account BIC/Swift code must be 8 or 11 characters long: " +
1422+ str(bic))
1423+ self._bic = bic
1424+
1425+ def _get_bic(self):
1426+ return self._bic
1427+
1428+ bic = property(_get_bic, _set_bic)
1429+
1430+ def _set_number(self, number):
1431+ if not edifact_digits(number, mindigits=1):
1432+ raise ValueError("Account number is invalid: " +
1433+ str(number))
1434+
1435+ self._number = number
1436+
1437+ def _get_number(self):
1438+ return self._number
1439+
1440+ number = property(_get_number, _set_number)
1441+
1442+ def __init__(self, number, holder, currency, sortcode, swiftcode, country, origin_country=None):
1443+ self.number = number
1444+ self.holder = holder
1445+ self.currency = currency
1446+ self.sortcode = sortcode
1447+ self.country = country
1448+ self.bic = swiftcode
1449+ self.origin_country = origin_country
1450+ self.institution_identification = self._set_account_ident()
1451+
1452+
1453+class IBANAccount(HasCurrency):
1454+ def _get_iban(self):
1455+ return self._iban
1456+
1457+ def _set_iban(self, iban):
1458+ iban_obj = sepa.IBAN(iban)
1459+ if not iban_obj.valid:
1460+ raise ValueError("IBAN is invalid: " + str(iban))
1461+
1462+ self._iban = iban
1463+ self.country = iban_obj.countrycode
1464+
1465+ iban = property(_get_iban, _set_iban)
1466+
1467+ def __init__(self, iban, bic, currency, holder):
1468+ self.iban = iban
1469+ self.number = iban
1470+ self.bic = bic
1471+ self.currency = currency
1472+ self.holder = holder
1473+ self.institution_identification = [self.bic, 25, 5, '', '', '' ]
1474+
1475+ def fii_bf_segment(self):
1476+ return _fii_segment(self, 'BF')
1477+
1478+class Interchange(LogicalSection):
1479+ def _get_reference(self):
1480+ return self._reference
1481+
1482+ def _set_reference(self, reference):
1483+ if not len(reference) <= 15:
1484+ raise ValueError("Reference must be <= 15 characters long: " + str(reference))
1485+
1486+ if not edifact_isalnum(reference):
1487+ raise ValueError("Reference must be alphanumeric: " + str(reference))
1488+
1489+ self._reference = reference.upper()
1490+
1491+ reference = property(_get_reference, _set_reference)
1492+
1493+ def __init__(self, client_id, reference, create_dt=None, message=None):
1494+ self.client_id = client_id
1495+ self.create_dt = create_dt or datetime.datetime.now()
1496+ self.reference = reference
1497+ self.message = message
1498+
1499+ def segments(self):
1500+ segments = []
1501+ segments.append([
1502+ ['UNB'],
1503+ ['UNOA', 3],
1504+ ['', '', self.client_id],
1505+ ['', '', 'HEXAGON ABC'],
1506+ [self.create_dt.strftime('%y%m%d'), self.create_dt.strftime('%H%M')],
1507+ [self.reference],
1508+ ])
1509+ segments += self.message.segments()
1510+ segments.append([
1511+ ['UNZ'],
1512+ [1],
1513+ [self.reference],
1514+ ])
1515+ return segments
1516+
1517+
1518+class Message(LogicalSection):
1519+ def _get_reference(self):
1520+ return self._reference
1521+
1522+ def _set_reference(self, reference):
1523+ if not len(reference) <= 35:
1524+ raise ValueError("Reference must be <= 35 characters long: " + str(reference))
1525+
1526+ if not edifact_isalnum(reference):
1527+ raise ValueError("Reference must be alphanumeric: " + str(reference))
1528+
1529+ self._reference = reference.upper()
1530+
1531+ reference = property(_get_reference, _set_reference)
1532+
1533+ def __init__(self, reference, dt=None):
1534+ if dt:
1535+ self.dt = dt
1536+ else:
1537+ self.dt = datetime.datetime.now()
1538+
1539+ self.reference = reference
1540+ self.batches = []
1541+
1542+ def segments(self):
1543+ # HSBC only accepts one message per interchange
1544+ message_reference_number = 1
1545+
1546+ segments = []
1547+
1548+ segments.append([
1549+ ['UNH'],
1550+ [message_reference_number],
1551+ ['PAYMUL', 'D', '96A', 'UN', 'FUN01G'],
1552+ ])
1553+ segments.append([
1554+ ['BGM'],
1555+ [452],
1556+ [self.reference],
1557+ [9],
1558+ ])
1559+ segments.append([
1560+ ['DTM'],
1561+ (137, self.dt.strftime('%Y%m%d'), 102),
1562+ ])
1563+ for index, batch in enumerate(self.batches):
1564+ segments += batch.segments(index + 1)
1565+ segments.append([
1566+ ['CNT'],
1567+ ['39', sum([len(x.transactions) for x in self.batches])],
1568+ ])
1569+ segments.append([
1570+ ['UNT'],
1571+ [len(segments) + 1],
1572+ [message_reference_number]
1573+ ])
1574+
1575+ return segments
1576+
1577+class Batch(LogicalSection):
1578+ def _get_reference(self):
1579+ return self._reference
1580+
1581+ def _set_reference(self, reference):
1582+ if not len(reference) <= 18:
1583+ raise ValueError("Reference must be <= 18 characters long: " + str(reference))
1584+
1585+ if not edifact_isalnum(reference):
1586+ raise ValueError("Reference must be alphanumeric: " + str(reference))
1587+
1588+ self._reference = reference.upper()
1589+
1590+ reference = property(_get_reference, _set_reference)
1591+
1592+ def __init__(self, exec_date, reference, debit_account, name_address):
1593+ self.exec_date = exec_date
1594+ self.reference = reference
1595+ self.debit_account = debit_account
1596+ self.name_address = name_address
1597+ self.transactions = []
1598+
1599+ def amount(self):
1600+ return sum([x.amount for x in self.transactions])
1601+
1602+ def segments(self, index):
1603+ if not edifact_digits(index, 6, 1):
1604+ raise ValueError("Index must be 6 digits or less: " + str(index))
1605+
1606+ # Store the payment means
1607+ means = None
1608+ if len(self.transactions)>0:
1609+ means = self.transactions[0].means
1610+
1611+ segments = []
1612+
1613+ if means != MEANS_PRIORITY_PAYMENT:
1614+ segments.append([
1615+ ['LIN'],
1616+ [index],
1617+ ])
1618+ segments.append([
1619+ ['DTM'],
1620+ [203, self.exec_date.strftime('%Y%m%d'), 102],
1621+ ])
1622+ segments.append([
1623+ ['RFF'],
1624+ ['AEK', self.reference],
1625+ ])
1626+
1627+ currencies = set([x.currency for x in self.transactions])
1628+ if len(currencies) > 1:
1629+ raise ValueError("All transactions in a batch must have the same currency")
1630+
1631+ segments.append([
1632+ ['MOA'],
1633+ [9, self.amount().quantize(Decimal('0.00')), currencies.pop()],
1634+ ])
1635+ segments.append(self.debit_account.fii_or_segment())
1636+ segments.append([
1637+ ['NAD'],
1638+ ['OY'],
1639+ [''],
1640+ self.name_address.upper().split("\n")[0:5],
1641+ ])
1642+
1643+ for index, transaction in enumerate(self.transactions):
1644+ if transaction.means == MEANS_PRIORITY_PAYMENT:
1645+ # Need a debit-credit format for Priority Payments
1646+ segments.append([
1647+ ['LIN'],
1648+ [index+1],
1649+ ])
1650+ segments.append([
1651+ ['DTM'],
1652+ [203, self.exec_date.strftime('%Y%m%d'), 102],
1653+ ])
1654+ segments.append([
1655+ ['RFF'],
1656+ ['AEK', self.reference],
1657+ ])
1658+
1659+ # Use the transaction amount and currency for the debit line
1660+ segments.append([
1661+ ['MOA'],
1662+ [9, transaction.amount.quantize(Decimal('0.00')), transaction.currency],
1663+ ])
1664+ segments.append(self.debit_account.fii_or_segment())
1665+ segments.append([
1666+ ['NAD'],
1667+ ['OY'],
1668+ [''],
1669+ self.name_address.upper().split("\n")[0:5],
1670+ ])
1671+ use_index = 1
1672+ else:
1673+ use_index = index + 1
1674+
1675+ segments += transaction.segments(use_index)
1676+
1677+ return segments
1678+
1679+# From the spec for FCA segments:
1680+# 13 = All charges borne by payee (or beneficiary)
1681+# 14 = Each pay own cost
1682+# 15 = All charges borne by payor (or ordering customer)
1683+# For Faster Payments this should always be ‘14’
1684+# Where this field is not present, “14” will be used as a default.
1685+CHARGES_PAYEE = 13
1686+CHARGES_EACH_OWN = 14
1687+CHARGES_PAYER = 15
1688+
1689+# values per section 2.8.5 "PAI, Payment Instructions" of "HSBC - CRG Paymul Message Implementation Guide"
1690+MEANS_ACH_OR_EZONE = 2
1691+MEANS_PRIORITY_PAYMENT = 52
1692+MEANS_FASTER_PAYMENT = 'FPS'
1693+
1694+CHANNEL_INTRA_COMPANY = 'Z24'
1695+
1696+class Transaction(LogicalSection, HasCurrency):
1697+ def _get_amount(self):
1698+ return self._amount
1699+
1700+ def _set_amount(self, amount):
1701+ if len(str(amount)) > 18:
1702+ raise ValueError("Amount must be shorter than 18 bytes: " + str(amount))
1703+
1704+ self._amount = amount
1705+
1706+ amount = property(_get_amount, _set_amount)
1707+
1708+ def _get_payment_reference(self):
1709+ return self._payment_reference
1710+
1711+ def _set_payment_reference(self, payment_reference):
1712+ if not len(payment_reference) <= 18:
1713+ raise ValueError("Payment reference must be <= 18 characters long: " + str(payment_reference))
1714+
1715+ if not edifact_isalnum(payment_reference):
1716+ raise ValueError("Payment reference must be alphanumeric: " + str(payment_reference))
1717+
1718+ self._payment_reference = payment_reference.upper()
1719+
1720+ payment_reference = property(_get_payment_reference, _set_payment_reference)
1721+
1722+ def _get_customer_reference(self):
1723+ return self._customer_reference
1724+
1725+ def _set_customer_reference(self, customer_reference):
1726+ if not len(customer_reference) <= 18:
1727+ raise ValueError("Customer reference must be <= 18 characters long: " + str(customer_reference))
1728+
1729+ if not edifact_isalnum(customer_reference):
1730+ raise ValueError("Customer reference must be alphanumeric: " + str(customer_reference))
1731+
1732+ self._customer_reference = customer_reference.upper()
1733+
1734+ customer_reference = property(_get_customer_reference, _set_customer_reference)
1735+
1736+ def __init__(self, amount, currency, account, means,
1737+ name_address=None, party_name=None, channel='',
1738+ charges=CHARGES_EACH_OWN, customer_reference=None,
1739+ payment_reference=None):
1740+ self.amount = amount
1741+ self.currency = currency
1742+ self.account = account
1743+ self.name_address = name_address
1744+ self.party_name = party_name
1745+ self.means = means
1746+ self.channel = channel
1747+ self.charges = charges
1748+ self.payment_reference = payment_reference
1749+ self.customer_reference = customer_reference
1750+
1751+ def segments(self, index):
1752+ segments = []
1753+ segments.append([
1754+ ['SEQ'],
1755+ [''],
1756+ [index],
1757+ ])
1758+ segments.append([
1759+ ['MOA'],
1760+ [9, self.amount.quantize(Decimal('0.00')), self.currency],
1761+ ])
1762+
1763+ if self.customer_reference:
1764+ segments.append([
1765+ ['RFF'],
1766+ ['CR', self.customer_reference],
1767+ ])
1768+
1769+ if self.payment_reference:
1770+ segments.append([
1771+ ['RFF'],
1772+ ['PQ', self.payment_reference],
1773+ ])
1774+
1775+ if self.channel:
1776+ segments.append([
1777+ ['PAI'],
1778+ ['', '', self.means, '', '', self.channel],
1779+ ])
1780+ else:
1781+ segments.append([
1782+ ['PAI'],
1783+ ['', '', self.means],
1784+ ])
1785+
1786+ segments.append([
1787+ ['FCA'],
1788+ [self.charges],
1789+ ])
1790+
1791+ segments.append(self.account.fii_bf_segment())
1792+
1793+ nad_segment = [
1794+ ['NAD'],
1795+ ['BE'],
1796+ [''],
1797+ ]
1798+ if self.name_address:
1799+ nad_segment.append(self.name_address.upper().split("\n")[0:5])
1800+ else:
1801+ nad_segment.append('')
1802+ if self.party_name:
1803+ nad_segment.append(self.party_name.upper().split("\n")[0:5])
1804+ segments.append(nad_segment)
1805+
1806+ return segments
1807+
1808
1809=== added file 'account_banking_uk_hsbc/wizard/paymul_test.py'
1810--- account_banking_uk_hsbc/wizard/paymul_test.py 1970-01-01 00:00:00 +0000
1811+++ account_banking_uk_hsbc/wizard/paymul_test.py 2012-02-10 14:09:45 +0000
1812@@ -0,0 +1,274 @@
1813+# -*- encoding: utf-8 -*-
1814+##############################################################################
1815+#
1816+# Copyright (C) 2011 credativ Ltd (<http://www.credativ.co.uk>).
1817+# All Rights Reserved
1818+#
1819+# This program is free software: you can redistribute it and/or modify
1820+# it under the terms of the GNU General Public License as published by
1821+# the Free Software Foundation, either version 3 of the License, or
1822+# (at your option) any later version.
1823+#
1824+# This program is distributed in the hope that it will be useful,
1825+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1826+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1827+# GNU General Public License for more details.
1828+#
1829+# You should have received a copy of the GNU General Public License
1830+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1831+#
1832+##############################################################################
1833+
1834+import datetime
1835+import unittest2 as unittest
1836+import paymul
1837+
1838+from decimal import Decimal
1839+
1840+class PaymulTestCase(unittest.TestCase):
1841+
1842+ def setUp(self):
1843+ self.maxDiff = None
1844+
1845+ def test_uk_high_value_priority_payment(self):
1846+ # Changes from spec example: Removed DTM for transaction, HSBC ignores it (section 2.8.3)
1847+ expected = \
1848+ """UNB+UNOA:3+::ABC00000001+::HEXAGON ABC+041111:1500+UKHIGHVALUE'
1849+UNH+1+PAYMUL:D:96A:UN:FUN01G'
1850+BGM+452+UKHIGHVALUE+9'
1851+DTM+137:20041111:102'
1852+LIN+1'
1853+DTM+203:20041112:102'
1854+RFF+AEK:UKHIGHVALUE'
1855+MOA+9:1.00:GBP'
1856+FII+OR+12345678:HSBC NET TEST::GBP+:::400515:154:133+GB'
1857+NAD+OY++HSBC BANK PLC:HSBC NET TEST:TEST:TEST:UNITED KINGDOM'
1858+SEQ++1'
1859+MOA+9:1.00:GBP'
1860+RFF+CR:CRUKHV5'
1861+RFF+PQ:PQUKHV5'
1862+PAI+::52:::Z24'
1863+FCA+13'
1864+FII+BF+87654321:XYX LTD FROM FII BF 1:BEN NAME 2:GBP+:::403124:154:133+GB'
1865+NAD+BE++SOME BANK PLC:HSBC NET TEST:TEST:TEST:UNITED KINGDOM'
1866+CNT+39:1'
1867+UNT+19+1'
1868+UNZ+1+UKHIGHVALUE'"""
1869+
1870+ src_account = paymul.UKAccount(number=12345678,
1871+ holder='HSBC NET TEST',
1872+ currency='GBP',
1873+ sortcode=400515)
1874+
1875+ dest_account = paymul.UKAccount(number=87654321,
1876+ holder="XYX LTD FROM FII BF 1\nBEN NAME 2",
1877+ currency='GBP',
1878+ sortcode=403124)
1879+
1880+ transaction = paymul.Transaction(amount=Decimal('1.00'),
1881+ currency='GBP',
1882+ account=dest_account,
1883+ charges=paymul.CHARGES_PAYEE,
1884+ means=paymul.MEANS_PRIORITY_PAYMENT,
1885+ channel=paymul.CHANNEL_INTRA_COMPANY,
1886+ name_address="SOME BANK PLC\nHSBC NET TEST\nTEST\nTEST\nUNITED KINGDOM",
1887+ customer_reference='CRUKHV5',
1888+ payment_reference='PQUKHV5')
1889+
1890+ batch = paymul.Batch(exec_date=datetime.date(2004, 11, 12),
1891+ reference='UKHIGHVALUE',
1892+ debit_account=src_account,
1893+ name_address="HSBC BANK PLC\nHSBC NET TEST\nTEST\nTEST\nUNITED KINGDOM")
1894+ batch.transactions.append(transaction)
1895+
1896+ message = paymul.Message(reference='UKHIGHVALUE',
1897+ dt=datetime.datetime(2004, 11, 11))
1898+ message.batches.append(batch)
1899+
1900+ interchange = paymul.Interchange(client_id='ABC00000001',
1901+ reference='UKHIGHVALUE',
1902+ create_dt=datetime.datetime(2004, 11, 11, 15, 00),
1903+ message=message)
1904+
1905+ self.assertMultiLineEqual(expected, str(interchange))
1906+
1907+ def test_ezone(self):
1908+ # Changes from example in spec: Changed CNT from 27 to 39, because we only generate that
1909+ # and it makes no difference which one we use
1910+ # Removed DTM for transaction, HSBC ignores it (section 2.8.3)
1911+
1912+ expected = """UNB+UNOA:3+::ABC12016001+::HEXAGON ABC+080110:0856+EZONE'
1913+UNH+1+PAYMUL:D:96A:UN:FUN01G'
1914+BGM+452+EZONE+9'
1915+DTM+137:20080110:102'
1916+LIN+1'
1917+DTM+203:20080114:102'
1918+RFF+AEK:EZONE'
1919+MOA+9:1.00:EUR'
1920+FII+OR+12345678:ACCOUNT HOLDER NAME::EUR+:::403124:154:133+GB'
1921+NAD+OY++ORD PARTY NAME NADOY 01:CRG TC5 001 NADOY ADDRESS LINE 0001:CRG TC5 001 NADOY ADDRESS LINE 0002'
1922+SEQ++1'
1923+MOA+9:1.00:EUR'
1924+RFF+CR:EZONE 1A'
1925+RFF+PQ:EZONE 1A'
1926+PAI+::2'
1927+FCA+14'
1928+FII+BF+DE23300308800099990031:CRG TC5 001 BENE NAME FIIBF 000001::EUR+AACSDE33:25:5:::+DE'
1929+NAD+BE+++BENE NAME NADBE T1 001:CRG TC5 001T1 NADBE ADD LINE 1 0001:CRG TC5 001T1 NADBE ADD LINE 2 0001'
1930+CNT+39:1'
1931+UNT+19+1'
1932+UNZ+1+EZONE'"""
1933+
1934+
1935+ src_account = paymul.UKAccount(number=12345678,
1936+ holder='ACCOUNT HOLDER NAME',
1937+ currency='EUR',
1938+ sortcode=403124)
1939+
1940+ dest_account = paymul.IBANAccount(iban="DE23300308800099990031",
1941+ holder="CRG TC5 001 BENE NAME FIIBF 000001",
1942+ currency='EUR',
1943+ bic="AACSDE33")
1944+
1945+ party_name = "BENE NAME NADBE T1 001\n" \
1946+ + "CRG TC5 001T1 NADBE ADD LINE 1 0001\n" \
1947+ + "CRG TC5 001T1 NADBE ADD LINE 2 0001"
1948+ transaction = paymul.Transaction(amount=Decimal('1.00'),
1949+ currency='EUR',
1950+ account=dest_account,
1951+ party_name=party_name,
1952+ charges=paymul.CHARGES_EACH_OWN,
1953+ means=paymul.MEANS_EZONE,
1954+ customer_reference='EZONE 1A',
1955+ payment_reference='EZONE 1A')
1956+
1957+ name_address = "ORD PARTY NAME NADOY 01\n" \
1958+ + "CRG TC5 001 NADOY ADDRESS LINE 0001\n" \
1959+ + "CRG TC5 001 NADOY ADDRESS LINE 0002"
1960+ batch = paymul.Batch(exec_date=datetime.date(2008, 1, 14),
1961+ reference='EZONE',
1962+ debit_account=src_account,
1963+ name_address=name_address)
1964+ batch.transactions.append(transaction)
1965+
1966+ message = paymul.Message(reference='EZONE',
1967+ dt=datetime.datetime(2008, 1, 10))
1968+ message.batches.append(batch)
1969+
1970+ interchange = paymul.Interchange(client_id='ABC12016001',
1971+ reference='EZONE',
1972+ create_dt=datetime.datetime(2008, 1, 10, 8, 56),
1973+ message=message)
1974+
1975+ self.assertMultiLineEqual(expected, str(interchange))
1976+
1977+ def test_uk_low_value_ach_instruction_level(self):
1978+ dest_account1 = paymul.UKAccount(number=87654321,
1979+ holder="HSBC NET RPS TEST\nHSBC BANK",
1980+ currency='GBP',
1981+ sortcode=403124)
1982+ name_address = "HSBC BANK PLC\n" \
1983+ + "PCM\n" \
1984+ + "8CS37\n" \
1985+ + "E14 5HQ\n" \
1986+ + "UNITED KINGDOM"
1987+ transaction1 = paymul.Transaction(amount=Decimal('1.00'),
1988+ currency='GBP',
1989+ account=dest_account1,
1990+ name_address=name_address,
1991+ charges=paymul.CHARGES_PAYEE,
1992+ means=paymul.MEANS_ACH,
1993+ customer_reference='CREDIT',
1994+ payment_reference='CREDIT')
1995+
1996+ dest_account2 = paymul.UKAccount(number=12341234,
1997+ holder="HSBC NET RPS TEST\nHSBC BANK",
1998+ currency='GBP',
1999+ sortcode=403124)
2000+ name_address = "HSBC BANK PLC\n" \
2001+ + "PCM\n" \
2002+ + "8CS37\n" \
2003+ + "E14 5HQ\n" \
2004+ + "UNITED KINGDOM"
2005+ transaction2 = paymul.Transaction(amount=Decimal('1.00'),
2006+ currency='GBP',
2007+ account=dest_account2,
2008+ name_address=name_address,
2009+ charges=paymul.CHARGES_PAYEE,
2010+ means=paymul.MEANS_ACH,
2011+ customer_reference='CREDIT1',
2012+ payment_reference='CREDIT1')
2013+
2014+
2015+ name_address = "HSBC BANK PLC\n" \
2016+ + "PCM\n" \
2017+ + "8CS37\n" \
2018+ + "E14 5HQ\n" \
2019+ + "UNITED KINGDOM"
2020+
2021+ src_account = paymul.UKAccount(number=12345678,
2022+ holder='BHEX RPS TEST',
2023+ currency='GBP',
2024+ sortcode=401234)
2025+ batch = paymul.Batch(exec_date=datetime.date(2004, 11, 15),
2026+ reference='UKLVPLIL',
2027+ debit_account=src_account,
2028+ name_address=name_address)
2029+ batch.transactions = [transaction1, transaction2]
2030+
2031+
2032+ message = paymul.Message(reference='UKLVPLIL',
2033+ dt=datetime.datetime(2004, 11, 11))
2034+ message.batches.append(batch)
2035+
2036+
2037+ interchange = paymul.Interchange(client_id='ABC00000001',
2038+ reference='UKLVPLIL',
2039+ create_dt=datetime.datetime(2004, 11, 11, 15, 0),
2040+ message=message)
2041+
2042+
2043+ # Changes from example:
2044+ # * Change second transaction from EUR to GBP, because we don't support
2045+ # multi-currency batches
2046+ # * Removed DTM for transaction, HSBC ignores it (section 2.8.3)
2047+ expected = """UNB+UNOA:3+::ABC00000001+::HEXAGON ABC+041111:1500+UKLVPLIL'
2048+UNH+1+PAYMUL:D:96A:UN:FUN01G'
2049+BGM+452+UKLVPLIL+9'
2050+DTM+137:20041111:102'
2051+LIN+1'
2052+DTM+203:20041115:102'
2053+RFF+AEK:UKLVPLIL'
2054+MOA+9:2.00:GBP'
2055+FII+OR+12345678:BHEX RPS TEST::GBP+:::401234:154:133+GB'
2056+NAD+OY++HSBC BANK PLC:PCM:8CS37:E14 5HQ:UNITED KINGDOM'
2057+SEQ++1'
2058+MOA+9:1.00:GBP'
2059+RFF+CR:CREDIT'
2060+RFF+PQ:CREDIT'
2061+PAI+::2'
2062+FCA+13'
2063+FII+BF+87654321:HSBC NET RPS TEST:HSBC BANK:GBP+:::403124:154:133+GB'
2064+NAD+BE++HSBC BANK PLC:PCM:8CS37:E14 5HQ:UNITED KINGDOM'
2065+SEQ++2'
2066+MOA+9:1.00:GBP'
2067+RFF+CR:CREDIT1'
2068+RFF+PQ:CREDIT1'
2069+PAI+::2'
2070+FCA+13'
2071+FII+BF+12341234:HSBC NET RPS TEST:HSBC BANK:GBP+:::403124:154:133+GB'
2072+NAD+BE++HSBC BANK PLC:PCM:8CS37:E14 5HQ:UNITED KINGDOM'
2073+CNT+39:2'
2074+UNT+27+1'
2075+UNZ+1+UKLVPLIL'"""
2076+
2077+ self.assertMultiLineEqual(expected, str(interchange))
2078+
2079+
2080+
2081+
2082+if __name__ == "__main__":
2083+ # I ran this with
2084+ # 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
2085+ # is there a better way?
2086+ unittest.main()

Subscribers

People subscribed via source and target branches