diff --git a/account_banking/account_banking.py b/account_banking/account_banking.py index d0aecd54d..e7ab4a719 100644 --- a/account_banking/account_banking.py +++ b/account_banking/account_banking.py @@ -243,6 +243,7 @@ class account_bank_statement(osv.osv): Extensions from account_bank_statement: 1. Removed period_id (transformed to optional boolean) - as it is no longer needed. + NB! because of #1. changes required to account_voucher! 2. Extended 'button_confirm' trigger to cope with the period per statement_line situation. 3. Added optional relation with imported statements file @@ -453,8 +454,39 @@ class account_bank_statement(osv.osv): return move_id + def button_confirm_bank(self, cr, uid, ids, context=None): + if context is None: context = {} + obj_seq = self.pool.get('ir.sequence') + if not isinstance(ids, list): ids = [ids] + noname_ids = self.search(cr, uid, [('id','in',ids),('name','=','/')]) + for st in self.browse(cr, uid, noname_ids, context=context): + if st.journal_id.sequence_id: + year = self.pool.get('account.period').browse(cr, uid, self._get_period(cr, uid, st.date)).fiscalyear_id.id + c = {'fiscalyear_id': year} + st_number = obj_seq.get_id(cr, uid, st.journal_id.sequence_id.id, context=c) + self.write(cr, uid, ids, {'name': st_number}) + + return super(account_bank_statement, self).button_confirm_bank(cr, uid, ids, context) + account_bank_statement() +class account_voucher(osv.osv): + _inherit = 'account.voucher' + + def _get_period(self, cr, uid, context=None): + if context is None: context = {} + if not context.get('period_id') and context.get('move_line_ids'): + res = self.pool.get('account.move.line').browse(cr, uid , context.get('move_line_ids'))[0].period_id.id + context['period_id'] = res + return super(account_voucher, self)._get_period(cr, uid, context) + + def create(self, cr, uid, values, context=None): + if values.get('period_id') == False and context.get('move_line_ids'): + values['period_id'] = self._get_period(cr, uid, context) + return super(account_voucher, self).create(cr, uid, values, context) + +account_voucher() + class account_bank_statement_line(osv.osv): ''' Extension on basic class: diff --git a/account_banking/sepa/online.py b/account_banking/sepa/online.py index 04eccd148..52bca9da9 100644 --- a/account_banking/sepa/online.py +++ b/account_banking/sepa/online.py @@ -175,7 +175,16 @@ def bank_info(bic): requests to make. In total three HTTP requests are made per function call. In theory one request could be stripped, but the SWIFT terms of use prevent automated usage, so user like behavior is required. + + Update January 2012: Always return None, as the SWIFT page to retrieve the + information does no longer exist. + If demand exists, maybe bite the bullet and integrate with a paid web + service such as http://www.iban-rechner.de. + lp914922 additionally suggests to make online lookup optional. ''' + + return None, None + def harvest(soup): retval = struct() for trsoup in soup('tr'): diff --git a/account_banking/wizard/bank_import.py b/account_banking/wizard/bank_import.py index ab4791c17..230d976b3 100644 --- a/account_banking/wizard/bank_import.py +++ b/account_banking/wizard/bank_import.py @@ -848,7 +848,11 @@ class banking_import(osv.osv_memory): i += 1 results.stat_loaded_cnt += 1 - + + #recompute statement end_balance for validation + statement_obj.button_dummy( + cursor, uid, imported_statement_ids, context=context) + if payment_lines: # As payments lines are treated as individual transactions, the # batch as a whole is only marked as 'done' when all payment lines diff --git a/account_banking_uk_hsbc/__init__.py b/account_banking_uk_hsbc/__init__.py new file mode 100644 index 000000000..b9c3e7a5e --- /dev/null +++ b/account_banking_uk_hsbc/__init__.py @@ -0,0 +1,25 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2011 credativ Ltd (). +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import account_banking_uk_hsbc +import wizard +import hsbc_mt940 +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/account_banking_uk_hsbc/__openerp__.py b/account_banking_uk_hsbc/__openerp__.py new file mode 100644 index 000000000..f1a2fc471 --- /dev/null +++ b/account_banking_uk_hsbc/__openerp__.py @@ -0,0 +1,50 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2011 credativ Ltd (). +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + 'name': 'HSBC Account Banking', + 'version': '0.4', + 'license': 'AGPL-3', + 'author': 'credativ Ltd', + 'website': 'http://www.credativ.co.uk', + 'category': 'Account Banking', + 'depends': ['account_banking'], + 'init_xml': [], + 'update_xml': [ + 'account_banking_uk_hsbc.xml', + 'data/banking_export_hsbc.xml', + 'wizard/export_hsbc_view.xml', + ], + 'demo_xml': [], + 'description': ''' + Module to import HSBC format transation files (S.W.I.F.T MT940) and to export payments for HSBC.net (PAYMUL). + + Currently it is targetting UK market, due to country variances of the MT940 and PAYMUL. + + It is possible to extend this module to work with HSBC.net in other countries and potentially other banks. + + This module adds above import/export filter to the account_banking module. + All business logic is in account_banking module. + + Initial release of this module was co-sponsored by canonical. + ''', + 'active': False, + 'installable': True, +} diff --git a/account_banking_uk_hsbc/account_banking_uk_hsbc.py b/account_banking_uk_hsbc/account_banking_uk_hsbc.py new file mode 100644 index 000000000..77bc7b09b --- /dev/null +++ b/account_banking_uk_hsbc/account_banking_uk_hsbc.py @@ -0,0 +1,65 @@ +############################################################################## +# +# Copyright (C) 2009 EduSense BV (). +# Copyright (C) 2011 credativ Ltd (). +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################## + +from osv import osv, fields +from datetime import date +from tools.translate import _ + +class hsbc_export(osv.osv): + '''HSBC Export''' + _name = 'banking.export.hsbc' + _description = __doc__ + _rec_name = 'execution_date' + + _columns = { + 'payment_order_ids': fields.many2many( + 'payment.order', + 'account_payment_order_hsbc_rel', + 'banking_export_hsbc_id', 'account_order_id', + 'Payment Orders', + readonly=True), + 'identification': + fields.char('Identification', size=15, readonly=True, select=True), + 'execution_date': + fields.date('Execution Date',readonly=True), + 'no_transactions': + fields.integer('Number of Transactions', readonly=True), + 'total_amount': + fields.float('Total Amount', readonly=True), + 'date_generated': + fields.datetime('Generation Date', readonly=True, select=True), + 'file': + fields.binary('HSBC File', readonly=True), + 'state': + fields.selection([ + ('draft', 'Draft'), + ('sent', 'Sent'), + ('done', 'Reconciled'), + ], 'State', readonly=True), + } + + _defaults = { + 'date_generated': lambda *a: date.today().strftime('%Y-%m-%d'), + 'state': lambda *a: 'draft', + } +hsbc_export() + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/account_banking_uk_hsbc/account_banking_uk_hsbc.xml b/account_banking_uk_hsbc/account_banking_uk_hsbc.xml new file mode 100644 index 000000000..02b382256 --- /dev/null +++ b/account_banking_uk_hsbc/account_banking_uk_hsbc.xml @@ -0,0 +1,85 @@ + + + + + + + + account.banking.export.hsbc.form + banking.export.hsbc + form + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + account.banking.export.hsbc.tree + banking.export.hsbc + tree + + + + + + + + + Generated HSBC files + ir.actions.act_window + banking.export.hsbc + form + tree,form + + + + + + + + +
+
diff --git a/account_banking_uk_hsbc/data/banking_export_hsbc.xml b/account_banking_uk_hsbc/data/banking_export_hsbc.xml new file mode 100644 index 000000000..d6cc30e93 --- /dev/null +++ b/account_banking_uk_hsbc/data/banking_export_hsbc.xml @@ -0,0 +1,29 @@ + + + + + ACH or EZONE + not used + + + + + Faster Payment + not used + + + + + Priority Payment + not used + + + + + diff --git a/account_banking_uk_hsbc/hsbc_mt940.py b/account_banking_uk_hsbc/hsbc_mt940.py new file mode 100644 index 000000000..510147de4 --- /dev/null +++ b/account_banking_uk_hsbc/hsbc_mt940.py @@ -0,0 +1,161 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2011 credativ Ltd (). +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +# Import of HSBC data in Swift MT940 format +# + +from account_banking.parsers import models +from account_banking.parsers.convert import str2date +from tools.translate import _ +from mt940_parser import HSBCParser +import re + +bt = models.mem_bank_transaction + +def record2float(record, value): + if record['creditmarker'][-1] == 'C': + return float(record[value]) + return -float(record[value]) + +class transaction(models.mem_bank_transaction): + + mapping = { + 'execution_date' : 'valuedate', + 'effective_date' : 'valuedate', + 'local_currency' : 'currency', + 'transfer_type' : 'bookingcode', + 'reference' : 'custrefno', + 'message' : 'furtherinfo' + } + + type_map = { + 'TRF': bt.ORDER, + } + + def __init__(self, record, *args, **kwargs): + ''' + Transaction creation + ''' + super(transaction, self).__init__(*args, **kwargs) + for key, value in self.mapping.iteritems(): + if record.has_key(value): + setattr(self, key, record[value]) + + self.transferred_amount = record2float(record, 'amount') + + #print record.get('bookingcode') + if not self.is_valid(): + print "Invalid: %s" % record + def is_valid(self): + ''' + We don't have remote_account so override base + ''' + return (self.execution_date + and self.transferred_amount and True) or False + +class statement(models.mem_bank_statement): + ''' + Bank statement imported data + ''' + + def import_record(self, record): + def _transmission_number(): + self.id = record['transref'] + def _account_number(): + # The wizard doesn't check for sort code + self.local_account = record['sortcode'] + ' ' + record['accnum'].zfill(8) + def _statement_number(): + self.id = '-'.join([self.id, self.local_account, record['statementnr']]) + def _opening_balance(): + self.start_balance = record2float(record,'startingbalance') + self.local_currency = record['currencycode'] + def _closing_balance(): + self.end_balance = record2float(record, 'endingbalance') + self.date = record['bookingdate'] + def _transaction_new(): + self.transactions.append(transaction(record)) + def _transaction_info(): + self.transaction_info(record) + def _not_used(): + print "Didn't use record: %s" % (record,) + + rectypes = { + '20' : _transmission_number, + '25' : _account_number, + '28' : _statement_number, + '28C': _statement_number, + '60F': _opening_balance, + '62F': _closing_balance, + #'64' : _forward_available, + #'62M': _interim_balance, + '61' : _transaction_new, + '86' : _transaction_info, + } + + rectypes.get(record['recordid'], _not_used)() + + def transaction_info(self, record): + ''' + Add extra information to transaction + ''' + # Additional information for previous transaction + if len(self.transactions) < 1: + raise_error('Received additional information for non existent transaction', record) + + transaction = self.transactions[-1] + + transaction.id = ','.join([record[k] for k in ['infoline{0}'.format(i) for i in range(1,5)] if record.has_key(k)]) + +def raise_error(message, line): + raise osv.except_osv(_('Import error'), + 'Error in import:%s\n\n%s' % (message, line)) + +class parser_hsbc_mt940(models.parser): + code = 'HSBC-MT940' + name = _('HSBC Swift MT940 statement export') + country_code = 'GB' + doc = _('''\ + This format is available through + the HSBC web interface. + ''') + + def parse(self, data): + result = [] + parser = HSBCParser() + # Split into statements + statements = [st for st in re.split('[\r\n]*(?=:20:)', data)] + # Split by records + statement_list = [re.split('[\r\n ]*(?=:\d\d[\w]?:)', st) for st in statements] + + for statement_lines in statement_list: + stmnt = statement() + records = [parser.parse_record(record) for record in statement_lines] + [stmnt.import_record(r) for r in records if r is not None] + + + if stmnt.is_valid(): + result.append(stmnt) + else: + print "Invalid Statement:" + print records[0] + + return result + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/account_banking_uk_hsbc/mt940_parser.py b/account_banking_uk_hsbc/mt940_parser.py new file mode 100644 index 000000000..d4a6a2ba0 --- /dev/null +++ b/account_banking_uk_hsbc/mt940_parser.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2011 credativ Ltd (). +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +""" +Parser for HSBC UK MT940 format files +Based on fi_patu's parser +""" +import re +from datetime import datetime + +class HSBCParser(object): + + def __init__( self ): + recparse = dict() + patterns = {'ebcdic': "\w/\?:\(\).,'+{} -"} + + # MT940 header + recparse["20"] = ":(?P20):(?P.{1,16})" + recparse["25"] = ":(?P25):(?P\d{6})(?P\d{1,29})" + recparse["28"] = ":(?P28C?):(?P.{1,8})" + + # Opening balance 60F + recparse["60F"] = ":(?P60F):(?P[CD])" \ + + "(?P\d{6})(?P.{3})" \ + + "(?P[\d,]{1,15})" + + # Transaction + recparse["61"] = """\ +:(?P61):\ +(?P\d{6})(?P\d{4})?\ +(?PR?[CD])\ +(?P[A-Z])?\ +(?P[\d,]{1,15})\ +(?P[A-Z][A-Z0-9]{3})\ +(?P[%(ebcdic)s]{1,16})\ +(?://)\ +(?P[%(ebcdic)s]{1,16})?\ +(?:\n(?P[%(ebcdic)s]))?\ +""" % (patterns) + + # Further info + recparse["86"] = ":(?P86):" \ + + "(?P.{1,80})?" \ + + "(?:\n(?P.{1,80}))?" \ + + "(?:\n(?P.{1,80}))?" \ + + "(?:\n(?P.{1,80}))?" \ + + "(?:\n(?P.{1,80}))?" + + # Forward available balance (64) / Closing balance (62F) / Interim balance (62M) + recparse["64"] = ":(?P64|62[FM]):" \ + + "(?P[CD])" \ + + "(?P\d{6})(?P.{3})" \ + + "(?P[\d,]{1,15})" + + for record in recparse: + recparse[record] = re.compile(recparse[record]) + self.recparse = recparse + + + def parse_record(self, line): + """ + Parse record using regexps and apply post processing + """ + for matcher in self.recparse: + matchobj = self.recparse[matcher].match(line) + if matchobj: + break + if not matchobj: + print " **** failed to match line '%s'" % (line) + return + # Strip strings + matchdict = matchobj.groupdict() + + # Remove members set to None + matchdict=dict([(k,v) for k,v in matchdict.iteritems() if v]) + + matchkeys = set(matchdict.keys()) + needstrip = set(["transref", "accnum", "statementnr", "custrefno", + "bankref", "furtherinfo", "infoline1", "infoline2", "infoline3", + "infoline4", "infoline5", "startingbalance", "endingbalance"]) + for field in matchkeys & needstrip: + matchdict[field] = matchdict[field].strip() + + # Convert to float. Comma is decimal separator + needsfloat = set(["startingbalance", "endingbalance", "amount"]) + for field in matchkeys & needsfloat: + matchdict[field] = float(matchdict[field].replace(',','.')) + + # Convert date fields + needdate = set(["prevstmtdate", "valuedate", "bookingdate"]) + for field in matchkeys & needdate: + datestring = matchdict[field] + + post_check = False + if len(datestring) == 4 and field=="bookingdate" and matchdict.has_key("valuedate"): + # Get year from valuedate + datestring = matchdict['valuedate'].strftime('%y') + datestring + post_check = True + try: + matchdict[field] = datetime.strptime(datestring,'%y%m%d') + if post_check and matchdict[field] > matchdict["valuedate"]: + matchdict[field]=matchdict[field].replace(year=matchdict[field].year-1) + except ValueError: + matchdict[field] = None + + return matchdict + + def parse(self, data): + records = [] + # Some records are multiline + for line in data: + if len(line) <= 1: + continue + if line[0] == ':' and len(line) > 1: + records.append(line) + else: + records[-1] = '\n'.join([records[-1], line]) + + output = [] + for rec in records: + output.append(self.parse_record(rec)) + + return output + +def parse_file(filename): + hsbcfile = open(filename, "r") + p = HSBCParser().parse(hsbcfile.readlines()) + +def main(): + """The main function, currently just calls a dummy filename + + :returns: description + """ + parse_file("testfile") + +if __name__ == '__main__': + main() diff --git a/account_banking_uk_hsbc/wizard/__init__.py b/account_banking_uk_hsbc/wizard/__init__.py new file mode 100644 index 000000000..83ee32b16 --- /dev/null +++ b/account_banking_uk_hsbc/wizard/__init__.py @@ -0,0 +1,23 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2009 EduSense BV (). +# Copyright (C) 2011 credativ Ltd (). +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################## + +import export_hsbc \ No newline at end of file diff --git a/account_banking_uk_hsbc/wizard/export_hsbc.py b/account_banking_uk_hsbc/wizard/export_hsbc.py new file mode 100644 index 000000000..72ffe43e9 --- /dev/null +++ b/account_banking_uk_hsbc/wizard/export_hsbc.py @@ -0,0 +1,400 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2009 EduSense BV (). +# Copyright (C) 2011 credativ Ltd (). +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################## + +import base64 +from datetime import datetime, date, timedelta +from osv import osv, fields +from tools.translate import _ +from decimal import Decimal +import paymul +import string +import random +import netsvc + +def strpdate(arg, format='%Y-%m-%d'): + '''shortcut''' + return datetime.strptime(arg, format).date() + +def strfdate(arg, format='%Y-%m-%d'): + '''shortcut''' + return arg.strftime(format) + +class banking_export_hsbc_wizard(osv.osv_memory): + _name = 'banking.export.hsbc.wizard' + _description = 'HSBC Export' + _columns = { + 'state': fields.selection( + [ + ('create', 'Create'), + ('finish', 'Finish') + ], + 'State', + readonly=True, + ), + 'test': fields.boolean(), + 'reference': fields.char( + 'Reference', size=35, + help=('The bank will use this reference in feedback communication ' + 'to refer to this run. 35 characters are available.' + ), + ), + 'execution_date_create': fields.date( + 'Execution Date', + help=('This is the date the file should be processed by the bank. ' + 'Don\'t choose a date beyond the nearest date in your ' + 'payments. The latest allowed date is 30 days from now.\n' + 'Please keep in mind that banks only execute on working days ' + 'and typically use a delay of two days between execution date ' + 'and effective transfer date.' + ), + ), + 'file_id': fields.many2one( + 'banking.export.hsbc', + 'hsbc File', + readonly=True + ), + 'file': fields.related( + 'file_id', 'file', type='binary', + readonly=True, + string='File', + ), + 'execution_date_finish': fields.related( + 'file_id', 'execution_date', type='date', + readonly=True, + string='Execution Date', + ), + 'total_amount': fields.related( + 'file_id', 'total_amount', + type='float', + string='Total Amount', + readonly=True, + ), + 'no_transactions': fields.integer( + 'Number of Transactions', + readonly=True, + ), + 'payment_order_ids': fields.many2many( + 'payment.order', 'rel_wiz_payorders', 'wizard_id', + 'payment_order_id', 'Payment Orders', + readonly=True, + ), + } + + logger = netsvc.Logger() + + def create(self, cursor, uid, wizard_data, context=None): + ''' + Retrieve a sane set of default values based on the payment orders + from the context. + ''' + + if not 'execution_date_create' in wizard_data: + po_ids = context.get('active_ids', []) + po_model = self.pool.get('payment.order') + pos = po_model.browse(cursor, uid, po_ids) + + execution_date = date.today() + + for po in pos: + if po.date_prefered == 'fixed' and po.date_planned: + execution_date = strpdate(po.date_planned) + elif po.date_prefered == 'due': + for line in po.line_ids: + if line.move_line_id.date_maturity: + date_maturity = strpdate(line.move_line_id.date_maturity) + if date_maturity < execution_date: + execution_date = date_maturity + + execution_date = max(execution_date, date.today()) + + # The default reference contains a /, which is invalid for PAYMUL + reference = pos[0].reference.replace('/', ' ') + + wizard_data.update({ + 'execution_date_create': strfdate(execution_date), + 'reference': reference, + 'payment_order_ids': [[6, 0, po_ids]], + 'state': 'create', + }) + + return super(banking_export_hsbc_wizard, self).create( + cursor, uid, wizard_data, context) + + def _create_account(self, oe_account): + currency = None # let the receiving bank select the currency from the batch + holder = oe_account.owner_name or oe_account.partner_id.name + self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'Create account %s' % (holder)) + self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'-- %s' % (oe_account.country_id.code)) + self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'-- %s' % (oe_account.acc_number)) + self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'-- %s' % (oe_account.iban)) + + + if oe_account.iban: + self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'IBAN: %s' % (oe_account.iban)) + paymul_account = paymul.IBANAccount( + iban=oe_account.iban, + bic=oe_account.bank.bic, + holder=holder, + currency=currency, + ) + transaction_kwargs = { + 'charges': paymul.CHARGES_EACH_OWN, + } + elif oe_account.country_id.code == 'GB': + self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'GB: %s %s' % (oe_account.country_id.code,oe_account.acc_number)) + split = oe_account.acc_number.split(" ", 2) + if len(split) == 2: + sortcode, accountno = split + else: + raise osv.except_osv( + _('Error'), + "Invalid GB acccount number '%s'" % oe_account.acc_number) + paymul_account = paymul.UKAccount( + number=accountno, + sortcode=sortcode, + holder=holder, + currency=currency, + ) + transaction_kwargs = { + 'charges': paymul.CHARGES_PAYEE, + } + elif oe_account.country_id.code in ('US','CA'): + self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'US/CA: %s %s' % (oe_account.country_id.code,oe_account.acc_number)) + split = oe_account.acc_number.split(' ', 2) + if len(split) == 2: + sortcode, accountno = split + else: + raise osv.except_osv( + _('Error'), + "Invalid %s account number '%s'" % (oe_account.country_id.code,oe_account.acc_number)) + paymul_account = paymul.NorthAmericanAccount( + number=accountno, + sortcode=sortcode, + holder=holder, + currency=currency, + swiftcode=oe_account.bank.bic, + country=oe_account.country_id.code, + #origin_country=origin_country + ) + transaction_kwargs = { + 'charges': paymul.CHARGES_PAYEE, + } + transaction_kwargs = { + 'charges': paymul.CHARGES_PAYEE, + } + else: + self.logger.notifyChannel('paymul', netsvc.LOG_INFO,'SWIFT Account: %s' % (oe_account.country_id.code)) + split = oe_account.acc_number.split(' ', 2) + if len(split) == 2: + sortcode, accountno = split + else: + raise osv.except_osv( + _('Error'), + "Invalid %s account number '%s'" % (oe_account.country_id.code,oe_account.acc_number)) + paymul_account = paymul.SWIFTAccount( + number=accountno, + sortcode=sortcode, + holder=holder, + currency=currency, + swiftcode=oe_account.bank.bic, + country=oe_account.country_id.code, + ) + transaction_kwargs = { + 'charges': paymul.CHARGES_PAYEE, + } + transaction_kwargs = { + 'charges': paymul.CHARGES_PAYEE, + } + + return paymul_account, transaction_kwargs + + def _create_transaction(self, line): + # Check on missing partner of bank account (this can happen!) + if not line.bank_id or not line.bank_id.partner_id: + raise osv.except_osv( + _('Error'), + _('There is insufficient information.\r\n' + 'Both destination address and account ' + 'number must be provided' + ) + ) + + self.logger.notifyChannel('paymul', netsvc.LOG_INFO, '====') + dest_account, transaction_kwargs = self._create_account(line.bank_id) + + means = {'ACH or EZONE': paymul.MEANS_ACH_OR_EZONE, + 'Faster Payment': paymul.MEANS_FASTER_PAYMENT, + 'Priority Payment': paymul.MEANS_PRIORITY_PAYMENT}.get(line.order_id.mode.type.name) + if means is None: + raise osv.except_osv('Error', "Invalid payment type mode for HSBC '%s'" % line.order_id.mode.type.name) + + if not line.info_partner: + raise osv.except_osv('Error', "No default address for transaction '%s'" % line.name) + + try: + return paymul.Transaction( + amount=Decimal(str(line.amount_currency)), + currency=line.currency.name, + account=dest_account, + means=means, + name_address=line.info_partner, + customer_reference=line.name, + payment_reference=line.name, + **transaction_kwargs + ) + except ValueError as exc: + raise osv.except_osv( + _('Error'), + _('Transaction invalid: ') + str(exc) + ) + + def wizard_export(self, cursor, uid, wizard_data_ids, context): + ''' + Wizard to actually create the HSBC file + ''' + + wizard_data = self.browse(cursor, uid, wizard_data_ids, context)[0] + result_model = self.pool.get('banking.export.hsbc') + payment_orders = wizard_data.payment_order_ids + + + try: + 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)) + src_account = self._create_account( + payment_orders[0].mode.bank_id, + )[0] + except ValueError as exc: + raise osv.except_osv( + _('Error'), + _('Source account invalid: ') + str(exc) + ) + + if not isinstance(src_account, paymul.UKAccount): + raise osv.except_osv( + _('Error'), + _("Your company's bank account has to have a valid UK " + "account number (not IBAN)" + str(type(src_account))) + ) + + try: + self.logger.notifyChannel('paymul', netsvc.LOG_INFO, 'Create transactions...') + transactions = [] + for po in payment_orders: + transactions += [self._create_transaction(l) for l in po.line_ids] + + batch = paymul.Batch( + exec_date=strpdate(wizard_data.execution_date_create), + reference=wizard_data.reference, + debit_account=src_account, + name_address=payment_orders[0].line_ids[0].info_owner, + ) + batch.transactions = transactions + except ValueError as exc: + raise osv.except_osv( + _('Error'), + _('Batch invalid: ') + str(exc) + ) + + # Generate random identifier until an unused one is found + while True: + ref = ''.join(random.choice(string.ascii_uppercase + string.digits) + for x in range(15)) + + ids = result_model.search(cursor, uid, [ + ('identification', '=', ref) + ]) + + if not ids: + break + + message = paymul.Message(reference=ref) + message.batches.append(batch) + interchange = paymul.Interchange(client_id='CLIENTID', + reference=ref, + message=message) + + export_result = { + 'identification': interchange.reference, + 'execution_date': batch.exec_date, + 'total_amount': batch.amount(), + 'no_transactions': len(batch.transactions), + 'file': base64.encodestring(str(interchange)), + 'payment_order_ids': [ + [6, 0, [po.id for po in payment_orders]] + ], + } + file_id = result_model.create(cursor, uid, export_result, context) + + self.write(cursor, uid, [wizard_data_ids[0]], { + 'file_id': file_id, + 'no_transactions' : len(batch.transactions), + 'state': 'finish', + }, context) + + return { + 'name': _('HSBC Export'), + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': self._name, + 'domain': [], + 'context': dict(context, active_ids=wizard_data_ids), + 'type': 'ir.actions.act_window', + 'target': 'new', + 'res_id': wizard_data_ids[0] or False, + } + + def wizard_cancel(self, cursor, uid, ids, context): + ''' + Cancel the export: just drop the file + ''' + + wizard_data = self.browse(cursor, uid, ids, context)[0] + result_model = self.pool.get('banking.export.hsbc') + + try: + result_model.unlink(cursor, uid, wizard_data.file_id.id) + except AttributeError: + # file_id missing, wizard storage gone, server was restarted + pass + + return {'type': 'ir.actions.act_window_close'} + + def wizard_save(self, cursor, uid, ids, context): + ''' + Save the export: mark all payments in the file as 'sent' + ''' + + wizard_data = self.browse(cursor, uid, ids, context)[0] + result_model = self.pool.get('banking.export.hsbc') + po_model = self.pool.get('payment.order') + + result_model.write(cursor, uid, [wizard_data.file_id.id], + {'state':'sent'}) + + po_ids = [po.id for po in wizard_data.payment_order_ids] + po_model.action_sent(cursor, uid, po_ids) + + return {'type': 'ir.actions.act_window_close'} + +banking_export_hsbc_wizard() + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/account_banking_uk_hsbc/wizard/export_hsbc_view.xml b/account_banking_uk_hsbc/wizard/export_hsbc_view.xml new file mode 100644 index 000000000..617080c61 --- /dev/null +++ b/account_banking_uk_hsbc/wizard/export_hsbc_view.xml @@ -0,0 +1,37 @@ + + + + + banking.export.hsbc.wizard.view + banking.export.hsbc.wizard + form + +
+ + + + + + + + + +