← Back to team overview

credativ team mailing list archive

[Merge] lp:~jamesj/account-banking/us-canada-payments into lp:~credativ/account-banking/upgrade-6.0

 

James Jesudason has proposed merging lp:~jamesj/account-banking/us-canada-payments into lp:~credativ/account-banking/upgrade-6.0.

Requested reviews:
  credativ (credativ)

For more details, see:
https://code.launchpad.net/~jamesj/account-banking/us-canada-payments/+merge/88822

Allows Priority Payments to non-UK bank accounts and includes validation of US and Canada accounts.
Catches more exceptions to display a user-friendly error message instead of the default exception screen.
Files generated by the module have been validated by HSBCNet and are currently undergoing final approval by HSBC. 
-- 
https://code.launchpad.net/~jamesj/account-banking/us-canada-payments/+merge/88822
Your team credativ is requested to review the proposed merge of lp:~jamesj/account-banking/us-canada-payments into lp:~credativ/account-banking/upgrade-6.0.
=== modified file 'account_banking/account_banking_view.xml'
--- account_banking/account_banking_view.xml	2011-07-21 11:30:59 +0000
+++ account_banking/account_banking_view.xml	2012-01-17 08:49:27 +0000
@@ -384,7 +384,7 @@
             <field name="type">form</field>
             <field name="arch" type="xml">
                 <field name="bic" position="replace">
-                    <field name="bic" on_change="onchange_bic(bic, name)"/>
+                    <field name="bic" />
                 </field>
             </field>
         </record>

=== modified file 'account_banking_uk_hsbc/data/banking_export_hsbc.xml'
--- account_banking_uk_hsbc/data/banking_export_hsbc.xml	2011-10-25 11:51:12 +0000
+++ account_banking_uk_hsbc/data/banking_export_hsbc.xml	2012-01-17 08:49:27 +0000
@@ -17,5 +17,13 @@
             <field name="ir_model_id"
                 ref="account_banking_uk_hsbc.model_banking_export_hsbc_wizard"/>
         </record>
+        <record model="payment.mode.type" id="export_priority_payment">
+            <field name="name">Priority Payment</field>
+            <field name="code">not used</field>
+            <field name="suitable_bank_types"
+                eval="[(6,0,[ref('base_iban.bank_iban'),ref('base.bank_normal'),])]" />
+            <field name="ir_model_id"
+                ref="account_banking_uk_hsbc.model_banking_export_hsbc_wizard"/>
+        </record>
     </data>
 </openerp>

=== modified file 'account_banking_uk_hsbc/wizard/export_hsbc.py'
--- account_banking_uk_hsbc/wizard/export_hsbc.py	2011-10-25 11:51:12 +0000
+++ account_banking_uk_hsbc/wizard/export_hsbc.py	2012-01-17 08:49:27 +0000
@@ -28,6 +28,7 @@
 import paymul
 import string
 import random
+import netsvc
 
 def strpdate(arg, format='%Y-%m-%d'):
     '''shortcut'''
@@ -98,6 +99,8 @@
             ),
         }
 
+    logger = netsvc.Logger()
+
     def create(self, cursor, uid, wizard_data, context=None):
         '''
         Retrieve a sane set of default values based on the payment orders
@@ -139,8 +142,14 @@
     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,
@@ -151,6 +160,7 @@
                 '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
@@ -167,11 +177,53 @@
             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:
-            raise osv.except_osv(
-                _('Error'),
-                _('%s: only UK accounts and IBAN are supported') % (holder)
+            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
 
@@ -185,14 +237,19 @@
                     '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}.get(line.order_id.mode.type.name)
+                 '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)),
@@ -221,6 +278,7 @@
 
 
         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]
@@ -237,11 +295,12 @@
                   "account number (not IBAN)" + str(type(src_account)))
             )
 
-        transactions = []
-        for po in payment_orders:
-            transactions += [self._create_transaction(l) for l in po.line_ids]
-
         try:
+            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,

=== modified file 'account_banking_uk_hsbc/wizard/paymul.py'
--- account_banking_uk_hsbc/wizard/paymul.py	2011-10-25 11:51:12 +0000
+++ account_banking_uk_hsbc/wizard/paymul.py	2012-01-17 08:49:27 +0000
@@ -23,6 +23,10 @@
 from decimal import Decimal
 import datetime
 import re
+import unicodedata
+
+def strip_accents(string):
+    return unicodedata.normalize('NFKD', unicode(string)).encode('ASCII', 'ignore')
 
 def split_account_holder(holder):
     holder_parts = holder.split("\n")
@@ -40,13 +44,19 @@
 def edifact_isalnum(s):
     return bool(re.match(r'^[A-Za-z0-9 ]*$', s))
 
-def edifact_digits(val, digits, mindigits=None):
+def edifact_digits(val, digits=None, mindigits=None):
+    if digits is None:
+        digits = ''
     if mindigits is None:
         mindigits = digits
 
     pattern = r'^[0-9]{' + str(mindigits) + ',' + str(digits) + r'}$'
     return bool(re.match(pattern, str(val)))
 
+def edifact_isalnum_size(val, digits):
+    pattern = r'^[A-Za-z0-9 ]{' + str(digits) + ',' + str(digits) + r'}$'
+    return bool(re.match(pattern, str(val)))
+
 class HasCurrency(object):
     def _get_currency(self):
         return self._currency
@@ -73,14 +83,14 @@
         segments = self.segments()
 
         def format_segment(segment):
-            return '+'.join([':'.join([str(y) for y in x]) for x in segment]) + "'"
+            return '+'.join([':'.join([str(strip_accents(y)) for y in x]) for x in segment]) + "'"
 
         return "\n".join([format_segment(s) for s in segments])
 
 
 def _fii_segment(self, party_qualifier):
     holder = split_account_holder(self.holder)
-    account_identification = [self.number, holder[0]]
+    account_identification = [self.number.replace(' ',''), holder[0]]
     if holder[1] or self.currency:
         account_identification.append(holder[1])
     if self.currency: 
@@ -126,16 +136,16 @@
         holder_parts = split_account_holder(holder)
 
         if not len(holder_parts[0]) <= 35:
-            raise ValueError("Account holder must be <= 35 characters long")
+            raise ValueError("Account holder must be <= 35 characters long: " + str(holder_parts[0]))
 
         if not len(holder_parts[1]) <= 35:
-            raise ValueError("Second line of account holder must be <= 35 characters long")
+            raise ValueError("Second line of account holder must be <= 35 characters long: " + str(holder_parts[1]))
 
         if not edifact_isalnum(holder_parts[0]):
-            raise ValueError("Account holder must be alphanumeric")
+            raise ValueError("Account holder must be alphanumeric: " + str(holder_parts[0]))
 
         if not edifact_isalnum(holder_parts[1]):
-            raise ValueError("Second line of account holder must be alphanumeric")
+            raise ValueError("Second line of account holder must be alphanumeric: " + str(holder_parts[1]))
 
         self._holder = holder.upper()
 
@@ -155,6 +165,113 @@
     def fii_or_segment(self):
         return _fii_segment(self, 'OR')
 
+
+class NorthAmericanAccount(UKAccount):
+
+    def _set_account_ident(self):
+        if self.origin_country in ('US','CA'):
+            # Use the routing number
+            account_ident = ['', '', '', self.sortcode, 155, 114]
+        else:
+            # Using the BIC/Swift Code
+            account_ident = [self.bic, 25, 5, '', '', '']
+        return account_ident
+
+    def _set_sortcode(self, sortcode):
+        if not edifact_digits(sortcode, 9):
+            raise ValueError("Account routing number must be 9 digits long: " +
+                             str(sortcode))
+
+        
+        self._sortcode = sortcode
+
+    def _get_sortcode(self):
+        return self._sortcode
+
+    sortcode = property(_get_sortcode, _set_sortcode)
+
+    def _set_bic(self, bic):
+        if not edifact_isalnum_size(bic, 8) and not edifact_isalnum_size(bic, 11):
+            raise ValueError("Account BIC/Swift code must be 8 or 11 characters long: " +
+                             str(bic))
+        self._bic = bic
+
+    def _get_bic(self):
+        return self._bic
+
+    bic = property(_get_bic, _set_bic)
+
+    def _set_number(self, number):
+        if not edifact_digits(number, mindigits=1):
+            raise ValueError("Account number is invalid: " +
+                             str(number))
+
+        self._number = number
+
+    def _get_number(self):
+        return self._number
+
+    number = property(_get_number, _set_number)
+
+    def __init__(self, number, holder, currency, sortcode, swiftcode, country, origin_country=None):
+        self.number = number
+        self.holder = holder
+        self.currency = currency
+        self.sortcode = sortcode
+        self.country = country
+        self.bic = swiftcode
+        self.origin_country = origin_country
+        self.institution_identification = self._set_account_ident()
+
+
+class SWIFTAccount(UKAccount):
+
+    def _set_account_ident(self):
+        # Using the BIC/Swift Code
+        return [self.bic, 25, 5, '', '', '']
+
+    def _set_sortcode(self, sortcode):
+        self._sortcode = sortcode
+
+    def _get_sortcode(self):
+        return self._sortcode
+
+    sortcode = property(_get_sortcode, _set_sortcode)
+
+    def _set_bic(self, bic):
+        if not edifact_isalnum_size(bic, 8) and not edifact_isalnum_size(bic, 11):
+            raise ValueError("Account BIC/Swift code must be 8 or 11 characters long: " +
+                             str(bic))
+        self._bic = bic
+
+    def _get_bic(self):
+        return self._bic
+
+    bic = property(_get_bic, _set_bic)
+
+    def _set_number(self, number):
+        if not edifact_digits(number, mindigits=1):
+            raise ValueError("Account number is invalid: " +
+                             str(number))
+
+        self._number = number
+
+    def _get_number(self):
+        return self._number
+
+    number = property(_get_number, _set_number)
+
+    def __init__(self, number, holder, currency, sortcode, swiftcode, country, origin_country=None):
+        self.number = number
+        self.holder = holder
+        self.currency = currency
+        self.sortcode = sortcode
+        self.country = country
+        self.bic = swiftcode
+        self.origin_country = origin_country
+        self.institution_identification = self._set_account_ident()
+
+
 class IBANAccount(HasCurrency):
     def _get_iban(self):
         return self._iban
@@ -162,7 +279,7 @@
     def _set_iban(self, iban):
         iban_obj = sepa.IBAN(iban)
         if not iban_obj.valid:
-            raise ValueError("IBAN is invalid")
+            raise ValueError("IBAN is invalid: " + str(iban))
 
         self._iban = iban
         self.country = iban_obj.countrycode
@@ -186,10 +303,10 @@
 
     def _set_reference(self, reference):
         if not len(reference) <= 15:
-            raise ValueError("Reference must be <= 15 characters long")
+            raise ValueError("Reference must be <= 15 characters long: " + str(reference))
 
         if not edifact_isalnum(reference):
-            raise ValueError("Reference must be alphanumeric")
+            raise ValueError("Reference must be alphanumeric: " + str(reference))
 
         self._reference = reference.upper()
 
@@ -226,10 +343,10 @@
 
     def _set_reference(self, reference):
         if not len(reference) <= 35:
-            raise ValueError("Reference must be <= 35 characters long")
+            raise ValueError("Reference must be <= 35 characters long: " + str(reference))
 
         if not edifact_isalnum(reference):
-            raise ValueError("Reference must be alphanumeric")
+            raise ValueError("Reference must be alphanumeric: " + str(reference))
 
         self._reference = reference.upper()
 
@@ -285,10 +402,10 @@
 
     def _set_reference(self, reference):
         if not len(reference) <= 18:
-            raise ValueError("Reference must be <= 18 characters long")
+            raise ValueError("Reference must be <= 18 characters long: " + str(reference))
 
         if not edifact_isalnum(reference):
-            raise ValueError("Reference must be alphanumeric")
+            raise ValueError("Reference must be alphanumeric: " + str(reference))
 
         self._reference = reference.upper()
 
@@ -306,41 +423,78 @@
 
     def segments(self, index):
         if not edifact_digits(index, 6, 1):
-            raise ValueError("Index must be 6 digits or less")
+            raise ValueError("Index must be 6 digits or less: " + str(index))
+
+        # Store the payment means
+        means = None
+        if len(self.transactions)>0:
+            means = self.transactions[0].means
 
         segments = []
 
-        segments.append([
-            ['LIN'],
-            [index],
-        ])
-        segments.append([
-            ['DTM'],
-            [203, self.exec_date.strftime('%Y%m%d'), 102],
-        ])
-        segments.append([
-            ['RFF'],
-            ['AEK', self.reference],
-        ])
-
-        currencies = set([x.currency for x in self.transactions])
-        if len(currencies) > 1:
-            raise ValueError("All transactions in a batch must have the same currency")
-
-        segments.append([
-            ['MOA'],
-            [9, self.amount().quantize(Decimal('0.00')), currencies.pop()],
-        ])
-        segments.append(self.debit_account.fii_or_segment())
-        segments.append([
-            ['NAD'],
-            ['OY'],
-            [''],
-            self.name_address.upper().split("\n")[0:5],
-        ])
+        if means != MEANS_PRIORITY_PAYMENT:
+            segments.append([
+                ['LIN'],
+                [index],
+            ])
+            segments.append([
+                ['DTM'],
+                [203, self.exec_date.strftime('%Y%m%d'), 102],
+            ])
+            segments.append([
+                ['RFF'],
+                ['AEK', self.reference],
+            ])
+             
+            currencies = set([x.currency for x in self.transactions])
+            if len(currencies) > 1:
+                raise ValueError("All transactions in a batch must have the same currency")
+             
+            segments.append([
+                ['MOA'],
+                [9, self.amount().quantize(Decimal('0.00')), currencies.pop()],
+            ])
+            segments.append(self.debit_account.fii_or_segment())
+            segments.append([
+                ['NAD'],
+                ['OY'],
+                [''],
+                self.name_address.upper().split("\n")[0:5],
+            ])
 
         for index, transaction in enumerate(self.transactions):
-            segments += transaction.segments(index + 1)
+            if transaction.means == MEANS_PRIORITY_PAYMENT:
+                # Need a debit-credit format for Priority Payments
+                segments.append([
+                    ['LIN'],
+                    [index+1],
+                ])
+                segments.append([
+                    ['DTM'],
+                    [203, self.exec_date.strftime('%Y%m%d'), 102],
+                ])
+                segments.append([
+                    ['RFF'],
+                    ['AEK', self.reference],
+                ])
+                
+                # Use the transaction amount and currency for the debit line
+                segments.append([
+                    ['MOA'],
+                    [9, transaction.amount.quantize(Decimal('0.00')), transaction.currency],
+                ])
+                segments.append(self.debit_account.fii_or_segment())
+                segments.append([
+                    ['NAD'],
+                    ['OY'],
+                    [''],
+                    self.name_address.upper().split("\n")[0:5],
+                ])
+                use_index = 1
+            else:
+                use_index = index + 1
+                 
+            segments += transaction.segments(use_index)
 
         return segments
 
@@ -367,7 +521,7 @@
 
     def _set_amount(self, amount):
         if len(str(amount)) > 18:
-            raise ValueError("Amount must be shorter than 18 bytes")
+            raise ValueError("Amount must be shorter than 18 bytes: " + str(amount))
 
         self._amount = amount
 
@@ -378,10 +532,10 @@
 
     def _set_payment_reference(self, payment_reference):
         if not len(payment_reference) <= 18:
-            raise ValueError("Payment reference must be <= 18 characters long")
+            raise ValueError("Payment reference must be <= 18 characters long: " + str(payment_reference))
 
         if not edifact_isalnum(payment_reference):
-            raise ValueError("Payment reference must be alphanumeric")
+            raise ValueError("Payment reference must be alphanumeric: " + str(payment_reference))
 
         self._payment_reference = payment_reference.upper()
 
@@ -392,10 +546,10 @@
 
     def _set_customer_reference(self, customer_reference):
         if not len(customer_reference) <= 18:
-            raise ValueError("Customer reference must be <= 18 characters long")
+            raise ValueError("Customer reference must be <= 18 characters long: " + str(customer_reference))
 
         if not edifact_isalnum(customer_reference):
-            raise ValueError("Customer reference must be alphanumeric")
+            raise ValueError("Customer reference must be alphanumeric: " + str(customer_reference))
 
         self._customer_reference = customer_reference.upper()
 
@@ -472,3 +626,4 @@
         segments.append(nad_segment)
 
         return segments
+


Follow ups