Merge US&CA changes from canonical

This commit is contained in:
Dmitrijs Ledkovs (credativ)
2012-02-10 13:57:25 +00:00
5 changed files with 283 additions and 61 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -38,7 +38,7 @@ class transaction(models.mem_bank_transaction):
mapping = {
'execution_date' : 'valuedate',
'effective_date' : 'bookingdate',
'effective_date' : 'valuedate',
'local_currency' : 'currency',
'transfer_type' : 'bookingcode',
'reference' : 'custrefno',

View File

@@ -28,6 +28,7 @@ from decimal import Decimal
import paymul
import string
import random
import netsvc
def strpdate(arg, format='%Y-%m-%d'):
'''shortcut'''
@@ -98,6 +99,8 @@ class banking_export_hsbc_wizard(osv.osv_memory):
),
}
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 @@ class banking_export_hsbc_wizard(osv.osv_memory):
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 @@ class banking_export_hsbc_wizard(osv.osv_memory):
'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 @@ class banking_export_hsbc_wizard(osv.osv_memory):
transaction_kwargs = {
'charges': paymul.CHARGES_PAYEE,
}
else:
raise osv.except_osv(
_('Error'),
_('%s: only UK accounts and IBAN are supported') % (holder)
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
@@ -185,14 +237,19 @@ class banking_export_hsbc_wizard(osv.osv_memory):
'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 @@ class banking_export_hsbc_wizard(osv.osv_memory):
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 @@ class banking_export_hsbc_wizard(osv.osv_memory):
"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,

View File

@@ -23,6 +23,10 @@ from account_banking import sepa
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 @@ The standard says alphanumeric characters, but spaces are also allowed
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 @@ class LogicalSection(object):
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 @@ class UKAccount(HasCurrency):
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 @@ class UKAccount(HasCurrency):
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 @@ class IBANAccount(HasCurrency):
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 @@ class Interchange(LogicalSection):
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 @@ class Message(LogicalSection):
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 @@ class Batch(LogicalSection):
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 @@ class Batch(LogicalSection):
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 @@ class Transaction(LogicalSection, HasCurrency):
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 @@ class Transaction(LogicalSection, HasCurrency):
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 @@ class Transaction(LogicalSection, HasCurrency):
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 @@ class Transaction(LogicalSection, HasCurrency):
segments.append(nad_segment)
return segments