# -*- 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 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 base64 from datetime import datetime, date from osv import osv, fields from tools.translate import _ from decimal import Decimal import paymul import string import random import logging 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 = logging.getLogger('export_hsbc') 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, origin_country=None, is_origin_account=False): 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.info('Create account %s' % (holder)) self.logger.info('-- %s' % (oe_account.country_id.code)) self.logger.info('-- %s' % (oe_account.acc_number)) if oe_account.state == 'iban': self.logger.info('IBAN: %s' % (oe_account.acc_number)) paymul_account = paymul.IBANAccount( iban=oe_account.acc_number, 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.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.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, is_origin_account=is_origin_account ) transaction_kwargs = { 'charges': paymul.CHARGES_PAYEE, } transaction_kwargs = { 'charges': paymul.CHARGES_PAYEE, } else: self.logger.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.info('====') dest_account, transaction_kwargs = self._create_account(line.bank_id, line.order_id.mode.bank_id.country_id.code) 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.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, payment_orders[0].mode.bank_id.country_id.code, is_origin_account=True )[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.info('Create transactions...') transactions = [] hsbc_clientid = '' for po in payment_orders: transactions += [self._create_transaction(l) for l in po.line_ids] hsbc_clientid = po.hsbc_clientid_id.clientid 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=hsbc_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: