Merge lp:~credativ/banking-addons/trunk-wip into lp:banking-addons/6.0
- trunk-wip
- Merge into 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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stefan Rijnhart (Opener) | test, technical | Approve | |
Review via email: mp+83131@code.launchpad.net |
Commit message
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 : | # |
review:
Approve
(test, technical)
Revision history for this message
Stefan Rijnhart (Opener) (stefan-opener) wrote : | # |
FYI,
also merged your work with
https:/
Cheers,
Stefan.
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() |
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.