diff --git a/weboob/backends/cragr/backend.py b/weboob/backends/cragr/backend.py
index 664116d4..8e93de22 100644
--- a/weboob/backends/cragr/backend.py
+++ b/weboob/backends/cragr/backend.py
@@ -94,3 +94,7 @@ class CragrBackend(BaseBackend, ICapBank):
def iter_history(self, account):
for history in self.browser.get_history(account):
yield history
+
+
+ def transfer(self, account, to, amount, reason=None):
+ return self.browser.do_transfer(account, to, amount, reason)
diff --git a/weboob/backends/cragr/browser.py b/weboob/backends/cragr/browser.py
index f1a4df24..16d295f8 100644
--- a/weboob/backends/cragr/browser.py
+++ b/weboob/backends/cragr/browser.py
@@ -17,7 +17,10 @@
from weboob.tools.browser import BaseBrowser, BrowserIncorrectPassword
+from weboob.capabilities.bank import AccountNotFound, Transfer, TransferError
from weboob.backends.cragr import pages
+import mechanize
+from datetime import datetime
# Browser
class Cragr(BaseBrowser):
@@ -88,6 +91,103 @@ class Cragr(BaseBrowser):
yield page_operation
page_url = self.page.next_page_url()
+ def dict_find_value(self, dictionary, value):
+ """
+ Returns the first key pointing on the given value, or None if none
+ is found.
+ """
+ for k, v in dictionary.iteritems():
+ if v == value:
+ return k
+ return None
+
+ def do_transfer(self, account, to, amount, reason=None):
+ """
+ Transfer the given amount of money from an account to another,
+ tagging the transfer with the given reason.
+ """
+ # access the transfer page
+ transfer_page_unreachable_message = u'Could not reach the transfer page.'
+ self.home()
+ if not self.page.is_accounts_list():
+ raise TransferError(transfer_page_unreachable_message)
+
+ operations_url = self.page.operations_page_url()
+
+ self.location('https://%s%s' % (self.DOMAIN, operations_url))
+ transfer_url = self.page.transfer_page_url()
+
+ abs_transfer_url = 'https://%s%s' % (self.DOMAIN, transfer_url)
+ self.location(abs_transfer_url)
+ if not self.page.is_transfer_page():
+ raise TransferError(transfer_page_unreachable_message)
+
+ source_accounts = self.page.get_transfer_source_accounts()
+ target_accounts = self.page.get_transfer_target_accounts()
+
+ # check that the given source account can be used
+ if not account in source_accounts.values():
+ raise TransferError('You cannot use account %s as a source account.' % account)
+
+ # check that the given source account can be used
+ if not to in target_accounts.values():
+ raise TransferError('You cannot use account %s as a target account.' % to)
+
+ # separate euros from cents
+ amount_euros = int(amount)
+ amount_cents = int((amount - amount_euros) * 100)
+
+ # let's circumvent https://github.com/jjlee/mechanize/issues/closed#issue/17
+ # using http://wwwsearch.sourceforge.net/mechanize/faq.html#usage
+ adjusted_response = self.response().get_data().replace('
', '
')
+ response = mechanize.make_response(adjusted_response, [('Content-Type', 'text/html')], abs_transfer_url, 200, 'OK')
+ self.set_response(response)
+
+ # fill the form
+ self.select_form(nr=0)
+ self['numCompteEmetteur'] = ['%s' % self.dict_find_value(source_accounts, account)]
+ self['numCompteBeneficiaire'] = ['%s' % self.dict_find_value(target_accounts, to)]
+ self['montantPartieEntiere'] = '%s' % amount_euros
+ self['montantPartieDecimale'] = '%s' % amount_cents
+ self['libelle'] = reason
+ self.submit()
+
+ # look for known errors
+ content = unicode(self.response().get_data(), 'utf-8')
+ insufficient_amount_message = u'Montant insuffisant.'
+ maximum_allowed_balance_message = u'Solde maximum autorisé dépassé.'
+
+ if content.find(insufficient_amount_message) != -1:
+ raise TransferError('The amount you tried to transfer is too low.')
+
+ if content.find(maximum_allowed_balance_message) != -1:
+ raise TransferError('The maximum allowed balance for the target account has been / would be reached.')
+
+ # look for the known "all right" message
+ ready_for_transfer_message = u'Vous allez effectuer un virement'
+ if not content.find(ready_for_transfer_message):
+ raise TransferError('The expected message "%s" was not found.' % ready_for_transfer_message)
+
+ # submit the last form
+ self.select_form(nr=0)
+ submit_date = datetime.now()
+ self.submit()
+
+ # look for the known "everything went well" message
+ content = unicode(self.response().get_data(), 'utf-8')
+ transfer_ok_message = u'Vous venez d\'effectuer un virement du compte'
+ if not content.find(transfer_ok_message):
+ raise TransferError('The expected message "%s" was not found.' % transfer_ok_message)
+
+ # We now have to return a Transfer object
+ # the final page does not provide any transfer id, so we'll use the submit date
+ transfer = Transfer(submit_date.strftime('%Y%m%d%H%M%S'))
+ transfer.amount = amount
+ transfer.origin = account
+ transfer.recipient = to
+ transfer.date = submit_date
+ return transfer
+
#def get_coming_operations(self, account):
# if not self.is_on_page(pages.AccountComing) or self.page.account.id != account.id:
# self.location('/NS_AVEEC?ch4=%s' % account.link_id)
diff --git a/weboob/backends/cragr/pages/accounts_list.py b/weboob/backends/cragr/pages/accounts_list.py
index eb9a61c2..ae61e762 100644
--- a/weboob/backends/cragr/pages/accounts_list.py
+++ b/weboob/backends/cragr/pages/accounts_list.py
@@ -53,6 +53,15 @@ class AccountsList(CragrBasePage):
l.append(account)
return l
+ def is_accounts_list(self):
+ """
+ Returns True if the current page appears to be the page dedicated to
+ list the accounts.
+ """
+ # we check for the presence of a "mes comptes titres" link_id
+ link = self.document.xpath('/html/body//a[contains(text(), "comptes titres")]')
+ return bool(link)
+
def is_account_page(self):
"""
Returns True if the current page appears to be a page dedicated to list
@@ -66,6 +75,39 @@ class AccountsList(CragrBasePage):
return True
return False
+ def is_transfer_page(self):
+ """
+ Returns True if the current page appears to be the page dedicated to
+ order transfers between accounts.
+ """
+ source_account_select_field = self.document.xpath('/html/body//form//select[@name="numCompteEmetteur"]')
+ target_account_select_field = self.document.xpath('/html/body//form//select[@name="numCompteBeneficiaire"]')
+ return bool(source_account_select_field) and bool(target_account_select_field)
+
+ def get_transfer_accounts(self, select_name):
+ """
+ Returns the accounts proposed for a transfer in a select field.
+ This method assumes the current page is the one dedicated to transfers.
+ select_name is the name of the select field to analyze
+ """
+ if not self.is_transfer_page():
+ return False
+ source_accounts = {}
+ source_account_options = self.document.xpath('/html/body//form//select[@name="%s"]/option' % select_name)
+ for option in source_account_options:
+ source_account_value = option.get('value', -1)
+ if (source_account_value != -1):
+ matches = re.findall('^[A-Z0-9]+.*([0-9]{11}).*$', self.extract_text(option))
+ if matches:
+ source_accounts[source_account_value] = matches[0]
+ return source_accounts
+
+ def get_transfer_source_accounts(self):
+ return self.get_transfer_accounts('numCompteEmetteur')
+
+ def get_transfer_target_accounts(self):
+ return self.get_transfer_accounts('numCompteBeneficiaire')
+
def next_page_url(self):
"""
When on a page dedicated to list the history of a specific account (see
@@ -79,6 +121,22 @@ class AccountsList(CragrBasePage):
else:
return a[0].get('href', '')
+ def operations_page_url(self):
+ """
+ Returns the link to the "Opérations" page. This function assumes the
+ current page is the accounts list (see is_accounts_list)
+ """
+ link = self.document.xpath(u'/html/body//a[contains(text(), "Opérations")]')
+ return link[0].get('href')
+
+ def transfer_page_url(self):
+ """
+ Returns the link to the "Virements" page. This function assumes the
+ current page is the operations list (see operations_page_url)
+ """
+ link = self.document.xpath('/html/body//a[@accesskey=1]/@href')
+ return link[0]
+
def is_right_aligned_div(self, div_elmt):
"""
Returns True if the given div element is right-aligned
diff --git a/weboob/backends/cragr/pages/base.py b/weboob/backends/cragr/pages/base.py
index 55a441a0..eb64c7d7 100644
--- a/weboob/backends/cragr/pages/base.py
+++ b/weboob/backends/cragr/pages/base.py
@@ -35,7 +35,7 @@ class CragrBasePage(BasePage):
raise BrowserUnavailable()
def is_logged(self):
- for form in self.document.getiterator('form'):
+ for form in self.document.xpath('/html/body//form//input[@name = "code"]'):
return False
return True