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