From 9e131dadb14370c75b98e5776675cdf7f15dfed8 Mon Sep 17 00:00:00 2001 From: Benjamin Tampigny Date: Mon, 6 Apr 2020 17:17:26 +0200 Subject: [PATCH] [axabanque] implement iter_transfers --- modules/axabanque/browser.py | 65 +++++++++++++- modules/axabanque/module.py | 4 + modules/axabanque/pages/transfer.py | 128 +++++++++++++++++++++++++++- 3 files changed, 193 insertions(+), 4 deletions(-) diff --git a/modules/axabanque/browser.py b/modules/axabanque/browser.py index 3f412519e1..9efb95c798 100644 --- a/modules/axabanque/browser.py +++ b/modules/axabanque/browser.py @@ -50,7 +50,8 @@ ) from .pages.transfer import ( RecipientsPage, AddRecipientPage, ValidateTransferPage, RegisterTransferPage, - ConfirmTransferPage, RecipientConfirmationPage, + ConfirmTransferPage, RecipientConfirmationPage, ScheduledTransfersPage, + ScheduledTransferDetailsPage, ) from .pages.document import DocumentsPage, DownloadPage @@ -176,6 +177,7 @@ class AXABanque(AXABrowser, StatesMixin): add_recipient = URL(r'/webapp/axabanque/jsp/beneficiaireSepa/saisieBeneficiaireSepaOTP.faces', AddRecipientPage) recipient_confirmation_page = URL(r'/webapp/axabanque/jsp/beneficiaireSepa/saisieBeneficiaireSepaOTP.faces', RecipientConfirmationPage) validate_transfer = URL(r'/webapp/axabanque/jsp/virementSepa/saisieVirementSepa.faces', ValidateTransferPage) + # also displays recent single transfers register_transfer = URL( r'/transactionnel/client/virement.html', r'/webapp/axabanque/jsp/virementSepa/saisieVirementSepa.faces', @@ -183,6 +185,8 @@ class AXABanque(AXABrowser, StatesMixin): ) confirm_transfer = URL('/webapp/axabanque/jsp/virementSepa/confirmationVirementSepa.faces', ConfirmTransferPage) profile_page = URL('/transactionnel/client/coordonnees.html', BankProfilePage) + scheduled_transfers = URL(r'/transactionnel/client/virements-en-cours.html', ScheduledTransfersPage) + scheduled_transfer_details = URL(r'/webapp/axabanque/jsp/virementSepa/virementEnCoursSepa.faces', ScheduledTransferDetailsPage) reload_state = None @@ -618,6 +622,65 @@ def iter_emitters(self): self.register_transfer.go() return self.page.iter_emitters() + def _get_recipients_by_emitter(self): + recipients_by_emitter = [] + for emitter in self.iter_emitters(): + self.page.set_account(emitter.id) + recipients = [] + for recipient in self.page.get_recipients(): + recipients.append(recipient) + recipients_by_emitter.append((emitter, recipients,)) + return recipients_by_emitter + + @staticmethod + def _find_emitter_and_recipient_from_recipient_name(recipients_by_emitters, recipient_name): + # find more information on non deferred single transfers + # in this case, the only information we have about recipient is a part of its label + matching_recipients = [] + for emitter, recipients in recipients_by_emitters: + for recipient in recipients: + # a recipient label is constructed using the following syntax: + # - + # the transfer list only provides the recipient name + # we will base our matching on it + if recipient.label.endswith(' - {}'.format(recipient_name)): + matching_recipients.append((emitter, recipient,)) + # information is valuable only if one recipient from one account matched the recipient name + # otherwise, we won't know which emitter and recipient to use + if len(matching_recipients) == 1: + return matching_recipients[0] + + @need_login + def iter_transfers(self, account): + self.scheduled_transfers.go() + for transfer in self.page.iter_transfers(): + if not account or account.iban == transfer.account_iban: + transfer_page = self.page.open_transfer_page(transfer._unparsed_js_args) + # will get additional information on the transfer's detailed page + transfer_page.fill_scheduled_transfer(obj=transfer) + yield transfer + + # now the awful part, single non deferred transfers displayed with very few information + self.register_transfer.go() + recipients_by_emitter = self._get_recipients_by_emitter() + # Very few information on immediate transfers. In order to retrieve something, a matching is done + # between recipient name given with transfer and the recipient, and since the recipient is linked to an + # emitter account, information about the emitter are found. + for transfer in self.page.iter_transfers(): + matched_information = self._find_emitter_and_recipient_from_recipient_name(recipients_by_emitter, transfer._recipient_name) + # keep in mind that if we were not able to find an emitter, + # there is no way to filter with the account parameter + if matched_information: + emitter, recipient = matched_information + # not 100% efficient, because children accounts can have the same beginning of id. + # but we don't have to use the heavy iter_accounts + if account and not account.id.startswith(emitter.id): + continue + transfer.recipient_iban = recipient.iban + transfer.recipient_label = recipient.label + transfer.account_label = emitter.label + yield transfer + class AXAAssurance(AXABrowser): BASEURL = 'https://espaceclient.axa.fr' diff --git a/modules/axabanque/module.py b/modules/axabanque/module.py index 4992a79c63..67eaf823fe 100644 --- a/modules/axabanque/module.py +++ b/modules/axabanque/module.py @@ -30,6 +30,7 @@ from weboob.capabilities.profile import CapProfile from weboob.capabilities.bill import CapDocument, Subscription, Document, DocumentNotFound, SubscriptionNotFound from weboob.tools.backend import Module, BackendConfig +from weboob.tools.capabilities.bank.bank_transfer import sorted_transfers from weboob.tools.value import ValueBackendPassword from .browser import AXABanque, AXAAssurance @@ -173,3 +174,6 @@ def iter_emitters(self): if self.BROWSER != AXABanque: raise NotImplementedError() return self.browser.iter_emitters() + + def iter_transfers(self, account=None): + return sorted_transfers(self.browser.iter_transfers(account)) diff --git a/modules/axabanque/pages/transfer.py b/modules/axabanque/pages/transfer.py index 0102ac107f..7e11e2c224 100644 --- a/modules/axabanque/pages/transfer.py +++ b/modules/axabanque/pages/transfer.py @@ -22,17 +22,19 @@ import re import string from io import BytesIO +from itertools import chain from PIL import Image, ImageFilter from datetime import date from weboob.browser.pages import HTMLPage, LoggedPage from weboob.browser.elements import method, TableElement, ItemElement, ListElement -from weboob.browser.filters.html import TableCell +from weboob.browser.filters.html import TableCell, Attr from weboob.browser.filters.standard import ( - CleanText, Date, Regexp, CleanDecimal, Currency, Format, Field, + CleanText, Date, Regexp, CleanDecimal, Currency, Format, Field, Map, ) from weboob.capabilities.bank import ( - Recipient, Transfer, TransferBankError, AddRecipientBankError, RecipientNotFound, Emitter, + Recipient, TransferBankError, AddRecipientBankError, RecipientNotFound, Emitter, + Transfer, TransferDateType, TransferFrequency, ) from weboob.tools.captcha.virtkeyboard import SimpleVirtualKeyboard from weboob.capabilities.base import find_object, NotAvailable @@ -336,6 +338,35 @@ def obj_label(self): label = raw_label.split('-') return '%s - %s' % (label[0].strip(), label[2].strip()) + @method + class iter_transfers(TableElement): + head_xpath = '//table[@id="idFormSaisieVirement:table-vrtDerniers"]//thead//th' + item_xpath = '//table[@id="idFormSaisieVirement:table-vrtDerniers"]//tbody//tr' + + col_amount = 'Montant' + col_exec_date = "Date d'effet" + col__rcpt_name = 'Bénéficiaire' + col__created_date = 'Date de saisie' + + class item(ItemElement): + klass = Transfer + + def condition(self): + # website don't let you plan deferred for the same day + # so if "date de saisie" is "date d'effet", it is an immediate transfer + # deferred transfers will be handle on another page with more information + return ( + Date(CleanText(TableCell('exec_date')), dayfirst=True)(self) + == Date(CleanText(TableCell('_created_date')), dayfirst=True)(self) + ) + + obj_currency = 'EUR' + obj_exec_date = Date(CleanText(TableCell('exec_date')), dayfirst=True) + obj_amount = CleanDecimal.US(TableCell('amount')) + # will be used after to try to get some information on the recipient and emitter account + obj__recipient_name = CleanText(TableCell('_rcpt_name')) + obj_date_type = TransferDateType.FIRST_OPEN_DAY + class ValidateTransferPage(LoggedPage, HTMLPage): is_here = '//p[contains(text(), "votre code confidentiel")]' @@ -413,3 +444,94 @@ def on_load(self): confirm_transfer_xpath = '//h2[contains(text(), "Virement enregistr")]' assert self.doc.xpath(confirm_transfer_xpath) + + +class BaseScheduledTransferElement(ItemElement): + klass = Transfer + + obj_recipient_label = CleanText('./td[@class="destinataire"]') + obj_amount = CleanDecimal.US('./td[@class="montant"]') + obj_account_id = CleanText('./td[@class="numCompteEmetteur"]') + obj_exec_date = Date(CleanText('./td[@class="dateEffet"]'), dayfirst=True) + + obj_label = CleanText('./preceding-sibling::tr[2]//p/text()') + obj_id = Regexp(Attr('./following-sibling::tr[1]//a[1]', 'onclick'), r"'virNumOperation','(\d+)'") + obj_currency = Currency(Regexp(CleanText('./preceding-sibling::tr[1]//td[contains(@class, "montant")]'), r'Montant \((.+)\)')) + obj__unparsed_js_args = Attr('./following-sibling::tr[1]//a[1]', 'onclick') + + +class ScheduledTransfersPage(LoggedPage, HTMLPage): + def js2args(self, s): + args = {} + for sub in re.findall("\['([^']+)','([^']+)'\]", s): + args[sub[0]] = sub[1] + + sub = re.search('oamSubmitForm.+?,\'([^:]+).([^\']+)', s) + args['%s:_idcl' % sub.group(1)] = "%s:%s" % (sub.group(1), sub.group(2)) + args['%s_SUBMIT' % sub.group(1)] = 1 + args['_form_name'] = sub.group(1) + + return args + + def iter_transfers(self): + return chain(self.iter_periodic_transfers(), self.iter_deferred_transfers()) + + @method + class iter_periodic_transfers(ListElement): + item_xpath = '//table[@id="table-virements-en-cours"]//tr[@class="ligne1"]' + + class item(BaseScheduledTransferElement): + obj_date_type = TransferDateType.PERIODIC + + @method + class iter_deferred_transfers(ListElement): + item_xpath = '//table[@id="table-virements-en-attente"]//tr[@class="ligne1"]' + + class item(BaseScheduledTransferElement): + obj_date_type = TransferDateType.DEFERRED + + def open_transfer_page(self, unparsed_js_args): + js_args = self.js2args(unparsed_js_args) + form = self.get_form(name=js_args['_form_name']) + form.update(js_args) + # instead of using Form.submit, we just use the form's request to avoid the useless browser.location + # otherwise, after each transfer filling we would have to locate back to the transfers list page + return self.browser.open(form.request).page + + +class ScheduledTransferDetailsPage(HTMLPage, LoggedPage): + def fill_scheduled_transfer(self, obj): + self._fill_common_scheduled(obj=obj) + if obj.date_type == TransferDateType.PERIODIC: + self._fill_periodic(obj=obj) + # no details specific to deferred transfers + + @method + class _fill_common_scheduled(ItemElement): + obj_account_label = CleanText('//table[@id="tableVirt1"]//td[@class="intituleCompte"]') + obj__rcpt_name = CleanText( + '//table[@id="tableVirt2"]//td[text()="Nom du bénéficiaire :"]/following-sibling::td' + ) + obj__acc_name = CleanText( + '//table[@id="tableVirt2"]//td[text()="Nom du compte :"]/following-sibling::td' + ) + + # constructed as in iter_recipients + obj_recipient_label = Format('%s - %s', Field('_acc_name'), Field('_rcpt_name')) + obj_recipient_iban = CleanText('//table[@id="tableVirt2"]//td[text()="IBAN :"]/following-sibling::td') + + @method + class _fill_periodic(ItemElement): + FREQ_LABELS = { + 'Mensuelle': TransferFrequency.MONTHLY, + 'Trimestrielle': TransferFrequency.QUARTERLY, + 'Semestrielle': TransferFrequency.BIANNUAL, + 'Annuelle': TransferFrequency.YEARLY, + } + + obj_last_due_date = Date(CleanText('//table[@id="tableVirt3"]//td[@class="fin"]'), dayfirst=True) + obj_frequency = Map( + CleanText('//table[@id="tableVirt3"]//td[@class="libPeriodicite"]'), + FREQ_LABELS, + TransferFrequency.UNKNOWN + ) -- GitLab