...
 
Commits (50)
......@@ -46,6 +46,7 @@ ACCOUNT_TYPES = {
'PEG': Account.TYPE_PEE,
'PEI': Account.TYPE_PEE,
'PERCO': Account.TYPE_PERCO,
'PERCOI': Account.TYPE_PERCO,
'RSP': Account.TYPE_RSP,
}
......
......@@ -27,7 +27,9 @@ from weboob.browser import LoginBrowser, URL, need_login, StatesMixin
from weboob.browser.exceptions import ClientError, HTTPNotFound
from weboob.capabilities.base import NotAvailable
from weboob.capabilities.bill import Subscription
from weboob.capabilities.bank import Account, Transaction, AddRecipientStep, Recipient
from weboob.capabilities.bank import (
Account, Transaction, AddRecipientStep, Recipient, AccountOwnership,
)
from weboob.exceptions import BrowserIncorrectPassword, ActionNeeded
from weboob.tools.value import Value
from weboob.tools.capabilities.bank.transactions import sorted_transactions
......@@ -175,6 +177,7 @@ class AXABanque(AXABrowser, StatesMixin):
if 'accs' not in self.cache.keys():
accounts = []
ids = set()
owner_name = self.get_profile().name.upper().split(' ', 1)[1]
# Get accounts
self.transactions.go()
self.bank_accounts.go()
......@@ -220,6 +223,7 @@ class AXABanque(AXABrowser, StatesMixin):
break
# Need it to get accounts from tabs
a._tab, a._pargs, a._purl = tab, page_args, self.url
self.set_ownership(a, owner_name)
accounts.append(a)
# Get investment accounts if there has
self.wealth_accounts.go()
......@@ -233,6 +237,24 @@ class AXABanque(AXABrowser, StatesMixin):
self.bank_accounts.go()
return self.cache['accs']
def set_ownership(self, account, owner_name):
# Some accounts _owner attribute says 'MLLE PRENOM NOM1' or other
# only 'NOM' while profile.name is 'MME PRENOM NOM1 NOM2' or 'MME PRENOM NOM'
# It makes it pretty hard to determine precisely wether the owernship
# should be OWNER or ATTORNEY. So we prefer set it to NotAvailable:
# better no information than an inaccurate one.
if not account.ownership:
if account.parent and account.parent.ownership:
account.ownership = account.parent.ownership
elif re.search(r'(m|mr|me|mme|mlle|mle|ml)\.? (.*)\bou (m|mr|me|mme|mlle|mle|ml)\b(.*)', account._owner, re.IGNORECASE):
account.ownership = AccountOwnership.CO_OWNER
elif all(n in account._owner for n in owner_name.split()):
account.ownership = AccountOwnership.OWNER
elif 'Mandat' in account.label:
account.ownership = AccountOwnership.ATTORNEY
else:
account.ownership = NotAvailable
@need_login
def go_account_pages(self, account, action):
# Default to "comptes"
......
......@@ -29,7 +29,7 @@ from weboob.browser.pages import HTMLPage, PDFPage, LoggedPage, AbstractPage
from weboob.browser.elements import ItemElement, TableElement, method
from weboob.browser.filters.standard import CleanText, CleanDecimal, Date, Regexp, Field, Env, Currency
from weboob.browser.filters.html import Attr, Link, TableCell
from weboob.capabilities.bank import Account, Investment
from weboob.capabilities.bank import Account, Investment, AccountOwnership
from weboob.tools.capabilities.bank.iban import is_iban_valid
from weboob.capabilities.base import NotAvailable, empty
from weboob.capabilities.profile import Person
......@@ -135,6 +135,7 @@ class AccountsPage(LoggedPage, MyHTMLPage):
self.browser.bank_accounts.open()
account.balance = loan_details.get_loan_balance()
account.currency = loan_details.get_loan_currency()
account.ownership = loan_details.get_loan_ownership()
# Skip loans without any balance (already fully reimbursed)
if empty(account.balance):
continue
......@@ -237,6 +238,7 @@ class AccountsPage(LoggedPage, MyHTMLPage):
account._url = self.doc.xpath('//form[contains(@action, "panorama")]/@action')[0]
account._acctype = "bank"
account._owner = CleanText('./td[has-class("libelle")]')(box)
# get accounts currency
currency_title = table.xpath('./thead//th[@class="montant"]')[0].text.strip()
......@@ -267,7 +269,7 @@ class IbanPage(PDFPage):
# findall will find something like
# ['FRXX', '1234', ... , '9012', 'FRXX', '1234', ... , '9012']
iban += part
iban = iban[:len(iban)/2]
iban = iban[:len(iban)//2]
# we suppose that all iban are French iban
iban_last_part = re.findall(r'([A-Z0-9]{3})\1\1Titulaire', extract_text(self.data), flags=re.MULTILINE)
......@@ -319,6 +321,12 @@ class TransactionsPage(LoggedPage, MyHTMLPage):
def get_loan_currency(self):
return Currency('//*[@id="table-detail"]/tbody/tr/td[@class="capital"]', default=NotAvailable)(self.doc)
def get_loan_ownership(self):
co_owner = CleanText('//td[@class="coEmprunteur"]')(self.doc)
if co_owner:
return AccountOwnership.CO_OWNER
return AccountOwnership.OWNER
def open_market(self):
# only for netfinca PEA
self.browser.bourse.go()
......
......@@ -30,7 +30,7 @@ from weboob.browser.filters.standard import (
)
from weboob.browser.filters.json import Dict
from weboob.browser.filters.html import Attr, Link, TableCell
from weboob.capabilities.bank import Account, Investment
from weboob.capabilities.bank import Account, Investment, AccountOwnership
from weboob.capabilities.profile import Person
from weboob.capabilities.base import NotAvailable, NotLoaded, empty
from weboob.tools.capabilities.bank.transactions import FrenchTransaction
......@@ -68,6 +68,7 @@ class AccountsPage(LoggedPage, HTMLPage):
obj__acctype = "investment"
obj_type = MapIn(Lower(Field('label')), TYPES, Account.TYPE_UNKNOWN)
obj_url = Attr('.', 'data-module-open-link--link')
obj_ownership = AccountOwnership.OWNER
class InvestmentPage(LoggedPage, HTMLPage):
......
......@@ -76,7 +76,13 @@ class BinckBrowser(LoginBrowser):
if self.login.is_here():
error = self.page.get_error()
if error and 'mot de passe' in error:
# The message for the second error is :
# Vous ne pouvez plus vous servir de cet identifiant pour vous connecter,
# Nous vous prions d'utiliser celui que vous avez récemment créé.
if error and any((
'mot de passe' in error,
'Vous ne pouvez plus vous servir de cet identifiant' in error,
)):
raise BrowserIncorrectPassword(error)
elif error and any((
'Votre compte a été bloqué / clôturé' in error,
......
......@@ -504,7 +504,7 @@ class BoursoramaBrowser(RetryLoginBrowser, StatesMixin):
assert self.transfer_sent.is_here()
transfer_error = self.page.get_transfer_error()
if transfer_error:
raise TransferBankError(transfer_error)
raise TransferBankError(message=transfer_error)
# the last page contains no info, return the last transfer object from init_transfer
return transfer
......
......@@ -129,6 +129,11 @@ class BPBrowser(LoginBrowser, StatesMixin):
transfer_choose = URL(r'/voscomptes/canalXHTML/virement/mpiaiguillage/init-saisieComptes.ea', TransferChooseAccounts)
transfer_complete = URL(r'/voscomptes/canalXHTML/virement/mpiaiguillage/soumissionChoixComptes-saisieComptes.ea',
r'/voscomptes/canalXHTML/virement/virementSafran_national/init-creerVirementNational.ea',
# The two following urls are obtained after a redirection made after a form
# No parameters or data seem to change that the website go back to the evious folder, using ".."
# We can't do much since it is finaly handled by the module requests
r'/voscomptes/canalXHTML/virement/mpiaiguillage/\.\./virementSafran_national/init-creerVirementNational.ea',
r'/voscomptes/canalXHTML/virement/mpiaiguillage/\.\./virementSafran_sepa/init-creerVirementSepa.ea',
r'/voscomptes/canalXHTML/virement/virementSafran_sepa/init-creerVirementSepa.ea',
CompleteTransfer)
transfer_confirm = URL(r'/voscomptes/canalXHTML/virement/virementSafran_pea/validerVirementPea-virementPea.ea',
......@@ -464,9 +469,11 @@ class BPBrowser(LoginBrowser, StatesMixin):
@need_login
def init_transfer(self, account, recipient, amount, transfer):
self.transfer_choose.stay_or_go()
self.page.init_transfer(account.id, recipient._value)
assert self.transfer_complete.is_here()
self.page.complete_transfer(amount, transfer)
self.page.init_transfer(account.id, recipient._value, amount)
assert self.transfer_complete.is_here(), 'An error occured while validating the first part of the transfer.'
self.page.complete_transfer(transfer)
return self.page.handle_response(account, recipient, amount, transfer.label)
@need_login
......
......@@ -19,6 +19,7 @@
from decimal import Decimal
from datetime import timedelta
from weboob.capabilities.bank import CapBankWealth, CapBankTransferAddRecipient, Account, AccountNotFound, RecipientNotFound
from weboob.capabilities.contact import CapContact
from weboob.capabilities.base import find_object, strict_find_object, NotAvailable
......@@ -105,6 +106,9 @@ class BPModule(
old = old.encode('latin-1', errors="xmlcharrefreplace").decode('latin-1')
return super(BPModule, self).transfer_check_label(old, new)
def transfer_check_date(self, old_exec_date, new_exec_date):
return old_exec_date <= new_exec_date <= old_exec_date + timedelta(days=2)
def execute_transfer(self, transfer, **params):
return self.browser.execute_transfer(transfer)
......
......@@ -19,7 +19,6 @@
from __future__ import unicode_literals
from io import BytesIO
import re
from decimal import Decimal
......@@ -36,6 +35,7 @@ from weboob.browser.filters.standard import (
)
from weboob.exceptions import BrowserUnavailable
from weboob.tools.compat import urljoin, unicode
from weboob.tools.pdf import extract_text
from .base import MyHTMLPage
......@@ -55,10 +55,18 @@ class item_account_generic(ItemElement):
def condition(self):
# For some loans the following xpath is absent and we don't want to skip them
# Also a case of loan that is empty and has no information exists and will be ignored
return (len(self.el.xpath('.//span[@class="number"]')) > 0 or
(Field('type')(self) == Account.TYPE_LOAN and
(len(self.el.xpath('.//div//*[contains(text(),"pas la restitution de ces données.")]')) == 0 and
len(self.el.xpath('.//div[@class="amount"]/span[contains(text(), "Contrat résilié")]')) == 0)))
return (
len(self.el.xpath('.//span[@class="number"]')) > 0 or
(
Field('type')(self) == Account.TYPE_LOAN and
(
not bool(self.el.xpath('.//div//*[contains(text(),"pas la restitution de ces données.")]'))
and not bool(self.el.xpath('.//div[@class="amount"]/span[contains(text(), "Contrat résilié")]'))
and not bool(self.el.xpath('.//div[@class="amount"]/span[contains(text(), "Remboursé intégralement")]'))
and not bool(self.el.xpath('.//div[@class="amount"]/span[contains(text(), "Prêt non débloqué")]'))
)
)
)
obj_id = obj_number = CleanText('.//abbr/following-sibling::text()')
obj_currency = Coalesce(Currency('.//span[@class="number"]'), Currency('.//span[@class="thick"]'))
......@@ -424,52 +432,8 @@ class Advisor(LoggedPage, MyHTMLPage):
class AccountRIB(LoggedPage, RawPage):
iban_regexp = r'[A-Z]{2}\d{12}[0-9A-Z]{11}\d{2}'
def __init__(self, *args, **kwargs):
super(AccountRIB, self).__init__(*args, **kwargs)
self.parsed_text = b''
try:
try:
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdfpage import PDFPage
newapi = True
except ImportError:
from pdfminer.pdfparser import PDFDocument
newapi = False
from pdfminer.pdfparser import PDFParser, PDFSyntaxError
from pdfminer.converter import TextConverter
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
except ImportError:
self.logger.warning('Please install python-pdfminer to get IBANs')
else:
parser = PDFParser(BytesIO(self.doc))
try:
if newapi:
doc = PDFDocument(parser)
else:
doc = PDFDocument()
parser.set_document(doc)
doc.set_parser(parser)
except PDFSyntaxError:
return
rsrcmgr = PDFResourceManager()
out = BytesIO()
device = TextConverter(rsrcmgr, out)
interpreter = PDFPageInterpreter(rsrcmgr, device)
if newapi:
pages = PDFPage.create_pages(doc)
else:
doc.initialize()
pages = doc.get_pages()
for page in pages:
interpreter.process_page(page)
self.parsed_text = out.getvalue()
def get_iban(self):
m = re.search(self.iban_regexp, self.parsed_text.decode('utf-8'))
m = re.search(self.iban_regexp, extract_text(self.data))
if m:
return unicode(m.group(0))
return None
......
......@@ -25,7 +25,7 @@ from weboob.capabilities.bank import (
TransferBankError, Transfer, TransferStep, NotAvailable, Recipient,
AccountNotFound, AddRecipientBankError
)
from weboob.capabilities.base import find_object
from weboob.capabilities.base import find_object, empty
from weboob.browser.pages import LoggedPage
from weboob.browser.filters.standard import CleanText, Env, Regexp, Date, CleanDecimal
from weboob.browser.filters.html import Attr, Link
......@@ -114,21 +114,23 @@ class TransferChooseAccounts(LoggedPage, MyHTMLPage):
if self.env['id'] in self.parent.objects: # user add two recipients with same iban...
raise SkipItem()
def init_transfer(self, account_id, recipient_value):
def init_transfer(self, account_id, recipient_value, amount):
matched_values = [Attr('.', 'value')(option) for option in self.doc.xpath('//select[@id="donneesSaisie.idxCompteEmetteur"]/option') \
if account_id in CleanText('.')(option)]
assert len(matched_values) == 1
form = self.get_form(xpath='//form[@class="formvirement"]')
form = self.get_form(xpath='//form[@class="choix-compte"]')
form['donneesSaisie.idxCompteReceveur'] = recipient_value
form['donneesSaisie.idxCompteEmetteur'] = matched_values[0]
form['donneesSaisie.montant'] = amount
form.submit()
class CompleteTransfer(LoggedPage, CheckTransferError):
def complete_transfer(self, amount, transfer):
def complete_transfer(self, transfer):
form = self.get_form(xpath='//form[@method]')
form['montant'] = amount
if 'commentaire' in form and transfer.label:
# for this bank the 'commentaire' is not a real label
# but a reason of transfer
form['commentaire'] = transfer.label
form['dateVirement'] = transfer.exec_date.strftime('%d/%m/%Y')
form.submit()
......@@ -136,7 +138,10 @@ class CompleteTransfer(LoggedPage, CheckTransferError):
class TransferConfirm(LoggedPage, CheckTransferError):
def is_here(self):
return not CleanText('//p[contains(text(), "Vous pouvez le consulter dans le menu")]')(self.doc)
return (
not CleanText('//p[contains(text(), "Vous pouvez le consulter dans le menu")]')(self.doc)
or self.doc.xpath('//input[@title="Confirmer la demande de virement"]')
)
def double_auth(self, transfer):
code_needed = CleanText('//label[@for="code_securite"]')(self.doc)
......@@ -148,15 +153,21 @@ class TransferConfirm(LoggedPage, CheckTransferError):
form.submit()
def handle_response(self, account, recipient, amount, reason):
account_txt = CleanText('//form//dl/dt[span[contains(text(), "biter")]]/following::dd[1]', replace=[(' ', '')])(self.doc)
recipient_txt = CleanText('//form//dl/dt[span[contains(text(), "diter")]]/following::dd[1]', replace=[(' ', '')])(self.doc)
# handle error
error_msg = CleanText('//div[@id="blocErreur"]')(self.doc)
if error_msg:
raise TransferBankError(message=error_msg)
account_txt = CleanText('//form//h3[contains(text(), "débiter")]//following::span[1]', replace=[(' ', '')])(self.doc)
recipient_txt = CleanText('//form//h3[contains(text(), "créditer")]//following::span[1]', replace=[(' ', '')])(self.doc)
assert account.id in account_txt or ''.join(account.label.split()) == account_txt, 'Something went wrong'
assert recipient.id in recipient_txt or ''.join(recipient.label.split()) == recipient_txt, 'Something went wrong'
r_amount = CleanDecimal('//form//dl/dt[span[contains(text(), "Montant")]]/following::dd[1]', replace_dots=True)(self.doc)
exec_date = Date(CleanText('//form//dl/dt[span[contains(text(), "Date")]]/following::dd[1]'), dayfirst=True)(self.doc)
currency = FrenchTransaction.Currency('//form//dl/dt[span[contains(text(), "Montant")]]/following::dd[1]')(self.doc)
amount_element = self.doc.xpath('//h3[contains(text(), "Montant du virement")]//following::span[@class="price"]')[0]
r_amount = CleanDecimal.French('.')(amount_element)
exec_date = Date(CleanText('//h3[contains(text(), "virement")]//following::span[@class="date"]'), dayfirst=True)(self.doc)
currency = FrenchTransaction.Currency('.')(amount_element)
transfer = Transfer()
transfer.currency = currency
......@@ -175,20 +186,53 @@ class TransferConfirm(LoggedPage, CheckTransferError):
class TransferSummary(LoggedPage, CheckTransferError):
def handle_response(self, transfer):
# NotAvailable in case of future exec_date not on a working day.
transfer.id = Regexp(CleanText('//div[@class="bloc Tmargin"]'), 'virement N.+ (\d+) ', default=NotAvailable)(self.doc)
if not transfer.id:
summary_filter = CleanText(
'//div[contains(@class, "bloc-recapitulatif")]//p'
)
# handle error
if "Votre virement n'a pas pu" in summary_filter(self.doc):
raise TransferBankError(message=summary_filter(self.doc))
transfer_id = Regexp(summary_filter, r'référence n° (\d+)', default=None)(self.doc)
# not always available
if transfer_id and not transfer.id:
transfer.id = transfer_id
else:
# TODO handle transfer with sms code.
if 'veuillez saisir votre code de validation' in CleanText('//div[@class="bloc Tmargin"]')(self.doc):
raise NotImplementedError()
# WARNING: At this point, the transfer was made.
# The following code is made to retrieve the transfer execution date,
# so there is no falsy data.
# But the bp website is unstable with changing layout and messages.
# One of the goals here is for the code not to crash to avoid the user thinking
# that the transfer was not made while it was.
old_date = transfer.exec_date
# the date was modified because on a weekend
if 'date correspondant à un week-end' in summary_filter(self.doc):
transfer.exec_date = Date(Regexp(
summary_filter,
r'jour ouvré suivant \((\d{2}/\d{2}/\d{4})\)',
default=''
), dayfirst=True, default=NotAvailable)(self.doc)
self.logger.warning('The transfer execution date changed from %s to %s' % (old_date.strftime('%Y-%m-%d'), transfer.exec_date.strftime('%Y-%m-%d')))
# made today
elif 'date du jour de ce virement' in summary_filter(self.doc):
# there are several regexp for transfer date:
# Date ([\d\/]+)|le ([\d\/]+)|suivant \(([\d\/]+)\)
# be more passive to avoid impulsive reaction from user
transfer.exec_date = Date(Regexp(
CleanText('//div[@class="bloc Tmargin"]'),
r' (\d{2}/\d{2}/\d{4})'
), dayfirst=True)(self.doc)
summary_filter,
r' (\d{2}/\d{2}/\d{4})',
default=''
), dayfirst=True, default=NotAvailable)(self.doc)
# else: using the same date because the website does not give one
if empty(transfer.exec_date):
transfer.exec_date = old_date
return transfer
......
......@@ -98,7 +98,8 @@ class CenetBrowser(LoginBrowser, StatesMixin):
elif not self.nuser:
raise BrowserIncorrectPassword("Erreur: Numéro d'utilisateur requis.")
assert "authMode" in data and data['authMode'] == 'redirect', 'should not be on the cenet website'
if "authMode" in data and data['authMode'] != 'redirect':
raise BrowserIncorrectPassword()
payload = {'contexte': '', 'dataEntree': None, 'donneesEntree': "{}", 'filtreEntree': "\"false\""}
res = self.cenet_vk.open(data=json.dumps(payload), headers={'Content-Type': "application/json"})
......
......@@ -1874,13 +1874,17 @@ class SubscriptionPage(LoggedPage, HTMLPage):
class item(ItemElement):
klass = Document
obj_type = DocumentTypes.OTHER
obj_format = 'pdf'
obj_url = Regexp(Link('.//td[@class="telecharger"]//a'), r'WebForm_PostBackOptions\("(\S*)"')
obj_id = Format('%s_%s_%s', Env('sub_id'), CleanText('./td[2]', symbols='/', replace=[(' ', '_')]), Regexp(CleanText('./td[3]'), r'([\wé]*)'))
obj_label = Format('%s %s', CleanText('./td[3]'), CleanText('./td[2]'))
obj_date = Date(CleanText('./td[2]'), dayfirst=True)
def obj_type(self):
if 'Relevé' in CleanText('./td[3]')(self):
return DocumentTypes.STATEMENT
return DocumentTypes.OTHER
def download_document(self, document):
form = self.get_form(id='main')
form['__EVENTTARGET'] = document.url
......
......@@ -218,7 +218,7 @@ class CmsoParBrowser(LoginBrowser, StatesMixin):
return self.accounts_list
def _go_market_history(self):
content = self.market.go(data=json.dumps({'place': 'SITUATION_PORTEFEUILLE'}), headers=self.json_headers).content
content = self.market.go(data=json.dumps({'place': 'SITUATION_PORTEFEUILLE'}), headers=self.json_headers).text
self.location(json.loads(content)['urlSSO'])
return self.market.go(website=self.website, action='historique')
......@@ -232,7 +232,7 @@ class CmsoParBrowser(LoginBrowser, StatesMixin):
return iter([])
if account.type == Account.TYPE_LIFE_INSURANCE:
url = json.loads(self.lifeinsurance.go(accid=account._index).content)['url']
url = json.loads(self.lifeinsurance.go(accid=account._index).text)['url']
url = self.location(url).page.get_link("opérations")
return self.location(url).page.iter_history()
......@@ -302,7 +302,7 @@ class CmsoParBrowser(LoginBrowser, StatesMixin):
account = self.get_account(account.id)
if account.type in (Account.TYPE_LIFE_INSURANCE, Account.TYPE_PERP):
url = json.loads(self.lifeinsurance.go(accid=account._index).content)['url']
url = json.loads(self.lifeinsurance.go(accid=account._index).text)['url']
url = self.location(url).page.get_link("supports")
if not url:
return iter([])
......@@ -310,7 +310,7 @@ class CmsoParBrowser(LoginBrowser, StatesMixin):
elif account.type in (Account.TYPE_MARKET, Account.TYPE_PEA):
data = {"place": "SITUATION_PORTEFEUILLE"}
response = self.market.go(data=json.dumps(data), headers=self.json_headers)
self.location(json.loads(response.content)['urlSSO'])
self.location(json.loads(response.text)['urlSSO'])
self.market.go(website=self.website, action="situation")
if self.page.go_account(account.label, account._owner):
return self.page.iter_investment()
......
......@@ -239,7 +239,7 @@ class AccountsPage(LoggedPage, JsonPage):
def get_lifenumber(self):
index = Dict('index')(self)
data = json.loads(self.page.browser.lifeinsurance.open(accid=index).content)
data = json.loads(self.page.browser.lifeinsurance.open(accid=index).text)
if not data:
raise SkipItem('account seems unavailable')
url = data['url']
......
......@@ -338,7 +338,7 @@ class CragrRegion(LoginBrowser):
self.accounts.stay_or_go()
self.page.set_cragr_code()
for account in self.page.iter_accounts():
self.accounts.go()
self.accounts.stay_or_go()
if iban and account._form:
# Refresh account form in case it expired
refreshed_account = find_object(self.page.iter_accounts(), id=account.id)
......
......@@ -276,6 +276,11 @@ ACCOUNT_TYPES = {
class AccountsPage(LoggedPage, CragrPage):
def on_load(self):
# Verify that all accounts page have the text 'Synthèse comptes'
if not CleanText('//h1[contains(text(), "Synthèse comptes")]')(self.doc):
self.logger.warning('We found an AccountsPage without the "Synthèse comptes" text.')
def no_other_perimeter(self):
return not CleanText('//a[@title="Espace Autres Comptes"]')(self.doc)
......@@ -681,7 +686,7 @@ class SavingsHistoryPage(LoggedPage, CragrPage):
class OtherSavingsHistoryPage(LoggedPage, CragrPage):
def is_here(self):
return CleanText('//span[@class="tdb-cartes-prop"]/b[contains(text(), "HISTORIQUE DES OPERATIONS")]')(self.doc)
return CleanText('//span[@class="tdb-cartes-prop"]/b[contains(text(), "HISTORIQUE DES OPERATIONS") or text()="OPERATIONS"]')(self.doc)
@pagination
@method
......
......@@ -90,6 +90,7 @@ class CreditDuNordBrowser(LoginBrowser):
raise BrowserIncorrectPassword()
def _iter_accounts(self):
owner_name = self.get_profile().name.upper()
self.loans.go(account_type=self.account_type, loans_page_label=self.loans_page_label)
for a in self.page.get_list():
yield a
......@@ -104,8 +105,12 @@ class CreditDuNordBrowser(LoginBrowser):
self.page.fill_diff_currency(a)
yield a
self.accounts.go(account_type=self.account_type, accounts_page_label=self.accounts_page_label)
for a in self.page.get_list():
yield a
if self.accounts.is_here():
for a in self.page.get_list(name=owner_name):
yield a
else:
for a in self.page.get_list():
yield a
@need_login
def get_pages_labels(self):
......
This diff is collapsed.
......@@ -630,6 +630,9 @@ class CreditMutuelBrowser(LoginBrowser, StatesMixin):
def continue_new_recipient(self, recipient, **params):
if 'Clé' in params:
self.page.post_code(params['Clé'])
if self.verify_pass.is_here():
self.page.handle_error()
assert False, 'An error occured while checking the card code'
self.page.add_recipient(recipient)
if self.page.bic_needed():
self.page.ask_bic(self.get_recipient_object(recipient))
......
......@@ -1772,6 +1772,12 @@ class VerifCodePage(LoggedPage, HTMLPage):
form['valChx.y'] = '1'
form.submit()
def handle_error(self):
error_msg = CleanText('//div[@class="blocmsg info"]/p')(self.doc)
# the card was not activated yet
if 'veuillez activer votre carte' in error_msg:
raise AddRecipientBankError(message=error_msg)
class RecipientsListPage(LoggedPage, HTMLPage):
def on_load(self):
......
......@@ -40,7 +40,7 @@ from weboob.browser.filters.json import Dict
class LoginPage(HTMLPage):
def login(self, login, passwd):
tab = re.search(r'clavierAChristian = (\[[\d,\s]*\])', self.content).group(1)
tab = re.search(r'clavierAChristian = (\[[\d,\s]*\])', self.text).group(1)
number_list = ast.literal_eval(tab)
key_map = {}
for i, number in enumerate(number_list):
......
......@@ -58,7 +58,7 @@ class SearchPage(HTMLPage):
Attr('div[@class="title"]/a', 'title'),
CleanText('span[@class="company"]')),
replace=[(" ", "-"), ("/", "-")])
obj_title = Attr('/a', 'title')
obj_title = Attr('div[@class="title"]/a', 'title')
obj_society_name = CleanText('span[@class="company"]')
obj_place = CleanText('span/span[@class="location"]')
obj_publication_date = IndeedDate(CleanText('table/tr/td/span[@class="date"]'))
......
......@@ -28,7 +28,10 @@ from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable
from weboob.browser import LoginBrowser, URL, need_login, StatesMixin
from weboob.browser.exceptions import ServerError
from weboob.capabilities.base import NotAvailable
from weboob.capabilities.bank import Account, AddRecipientBankError, AddRecipientStep, Recipient, AccountOwnerType
from weboob.capabilities.bank import (
Account, AddRecipientBankError, AddRecipientStep, Recipient, AccountOwnerType,
AccountOwnership,
)
from weboob.capabilities.base import find_object
from weboob.tools.capabilities.bank.investments import create_french_liquidity
from weboob.tools.compat import basestring, urlsplit, unicode
......@@ -268,13 +271,19 @@ class LCLBrowser(LoginBrowser, StatesMixin):
if self.login.is_here():
return self.get_accounts_list()
profile_name = self.get_profile_name()
if ' ' in profile_name:
owner_name = re.search(r' (.+)', profile_name).group(1).upper()
else:
owner_name = profile_name.upper()
# retrieve life insurance accounts
self.assurancevie.stay_or_go()
if self.no_perm.is_here():
self.logger.warning('Life insurances are unavailable.')
else:
# retrieve life insurances from popups
for a in self.page.get_popup_life_insurance():
for a in self.page.get_popup_life_insurance(name=owner_name):
self.update_accounts(a)
# retrieve life insurances from calie website
......@@ -312,7 +321,7 @@ class LCLBrowser(LoginBrowser, StatesMixin):
# retrieve accounts on main page
self.accounts.go()
for a in self.page.get_list():
for a in self.page.get_accounts_list(name=owner_name):
if not self.check_accounts(a):
continue
......@@ -346,7 +355,7 @@ class LCLBrowser(LoginBrowser, StatesMixin):
self.update_accounts(a)
if self.connexion_bourse():
for a in self.page.get_list():
for a in self.page.get_list(name=owner_name):
self.update_accounts(a)
self.deconnexion_bourse()
# Disconnecting from bourse portal before returning account list
......@@ -357,7 +366,7 @@ class LCLBrowser(LoginBrowser, StatesMixin):
if self.no_perm.is_here():
self.logger.warning('Deposits are unavailable.')
else:
for a in self.page.get_list():
for a in self.page.get_list(name=owner_name):
# There is no id on the page listing the 'Compte à terme'
# So a form must be submitted to access the id of the contract
self.set_deposit_account_id(a)
......@@ -390,11 +399,29 @@ class LCLBrowser(LoginBrowser, StatesMixin):
a._card_position = card_position
self.update_accounts(a)
profile_name = self.get_profile_name()
if ' ' in profile_name:
owner_name = re.search(r' (.+)', profile_name).group(1).upper()
else:
owner_name = profile_name.upper()
for account in self.accounts_list:
account.owner_type = self.owner_type
self.set_ownership(account, owner_name)
return iter(self.accounts_list)
def set_ownership(self, account, owner_name):
if not account.ownership:
if account.parent and account.parent.ownership:
account.ownership = account.parent.ownership
elif re.search(r'(m|mr|me|mme|mlle|mle|ml)\.? (.*)\bou (m|mr|me|mme|mlle|mle|ml)\b(.*)', account.label, re.IGNORECASE):
account.ownership = AccountOwnership.CO_OWNER
elif all(n in account.label for n in owner_name.split()):
account.ownership = AccountOwnership.OWNER
else:
account.ownership = AccountOwnership.ATTORNEY
def get_bourse_accounts_ids(self):
bourse_accounts_ids = []
for account in self.get_accounts_list():
......@@ -624,10 +651,15 @@ class LCLBrowser(LoginBrowser, StatesMixin):
documents.append(document)
return documents
def get_profile_name(self):
self.accounts.stay_or_go()
return self.page.get_name()
@need_login
def get_profile(self):
self.accounts.stay_or_go()
name = self.page.get_name()
name = self.get_profile_name()
# The self.get_profile_name() already does a
# self.accounts.stay_or_go()
self.profile.go(method="POST")
profile = self.page.get_profile(name=name)
return profile
......
......@@ -30,6 +30,7 @@ from datetime import datetime, timedelta
from weboob.capabilities.base import empty, find_object, NotAvailable
from weboob.capabilities.bank import (
Account, Investment, Recipient, TransferError, TransferBankError, Transfer,
AccountOwnership,
)
from weboob.capabilities.bill import Document, Subscription, DocumentTypes
from weboob.capabilities.profile import Person, ProfileMissing
......@@ -231,6 +232,15 @@ class ContractsChoicePage(ContractsPage):
self.select_contract()
class OwnedItemElement(ItemElement):
def get_ownership(self, owner):
if re.search(r'(m|mr|me|mme|mlle|mle|ml)\.? (.*)\bou (m|mr|me|mme|mlle|mle|ml)\b(.*)', owner, re.IGNORECASE):
return AccountOwnership.CO_OWNER
elif all(n in owner for n in self.env['name'].split()):
return AccountOwnership.OWNER
return AccountOwnership.ATTORNEY
class AccountsPage(LoggedPage, HTMLPage):
def on_load(self):
warn = self.doc.xpath('//div[@id="attTxt"]')
......@@ -241,7 +251,7 @@ class AccountsPage(LoggedPage, HTMLPage):
return CleanText('//li[@id="nomClient"]/p')(self.doc)
@method
class get_list(ListElement):
class get_accounts_list(ListElement):
# XXX Ugly Hack to replace account by second occurrence.
# LCL pro website sometimes display the same account twice and only second link is valid to fetch transactions.
......@@ -255,12 +265,19 @@ class AccountsPage(LoggedPage, HTMLPage):
item_xpath = '//tr[contains(@onclick, "redirect")]'
flush_at_end = True
class account(ItemElement):
class account(OwnedItemElement):
klass = Account
def condition(self):
return '/outil/UWLM/ListeMouvement' in self.el.attrib['onclick']
def load_details(self):
link_id = Field('_link_id')(self)
if link_id:
account_url = urljoin(self.page.browser.BASEURL, link_id)
return self.page.browser.async_open(url=account_url)
return NotAvailable
NATURE2TYPE = {
'001': Account.TYPE_SAVINGS,
'004': Account.TYPE_CHECKING,
......@@ -290,6 +307,11 @@ class AccountsPage(LoggedPage, HTMLPage):
obj__market_link = None
obj_number = Field('id')
def obj_ownership(self):
async_page = Async('details').loaded_page(self)
owner = CleanText('//h5[contains(text(), "Titulaire")]')(async_page.doc)
return self.get_ownership(owner)
def get_deferred_cards(self):
trs = self.doc.xpath('//tr[contains(@onclick, "EncoursCB")]')
links = []
......@@ -340,6 +362,11 @@ class LoansPage(LoggedPage, HTMLPage):
has_type = CleanText('./ancestor::table[.//th[contains(text(), "Type")]]', default=None)(self)
return CleanText('./td[2]')(self) if has_type else CleanText('./ancestor::table/preceding-sibling::div[1]')(self).split(' - ')[0]
def obj_ownership(self):
if re.search(r'(m|mr|me|mme|mlle|mle|ml)\.? (.*)\b(ou)? (m|mr|me|mme|mlle|mle|ml)\b(.*)', CleanText(TableCell('id'))(self), re.IGNORECASE):
return AccountOwnership.CO_OWNER
return AccountOwnership.OWNER
def parse(self, el):
label = Field('label')(self)
trs = self.xpath('//td[contains(text(), $label)]/ancestor::tr[1] | ./ancestor::table[1]/tbody/tr', label=label)
......@@ -668,10 +695,11 @@ class BoursePage(LoggedPage, HTMLPage):
head_xpath = '//table[has-class("tableau_comptes_details")]/thead/tr/th'
col_label = 'Comptes'
col_owner = re.compile('Titulaire')
col_titres = re.compile('Valorisation')
col_especes = re.compile('Solde espèces')
class item(ItemElement):
class item(OwnedItemElement):
klass = Account
load_details = Field('_market_link') & AsyncLoad
......@@ -708,6 +736,10 @@ class BoursePage(LoggedPage, HTMLPage):
return self.page.TYPES.get(key)
return Account.TYPE_MARKET
def obj_ownership(self):
owner = CleanText(TableCell('owner'))(self)
return self.get_ownership(owner)
def get_logout_link(self):
return Link('//a[@class="link-underline" and contains(text(), "espace client")]')(self.doc)
......@@ -828,7 +860,7 @@ class AVPage(LoggedPage, HTMLPage):
class get_popup_life_insurance(ListElement):
item_xpath = '//table[@class]/tbody/tr'
class item(ItemElement):
class item(OwnedItemElement):
klass = Account
def condition(self):
......@@ -851,6 +883,10 @@ class AVPage(LoggedPage, HTMLPage):
obj__external_website = False
obj__is_calie_account = False
def obj_ownership(self):
owner = CleanText(Field('_owner'))(self)
return self.get_ownership(owner)
def obj_id(self):
_id = CleanText('.//td/@id')(self)
# in old code, we use _id, it seems that is not used anymore
......@@ -1162,7 +1198,7 @@ class TransferPage(LoggedPage, HTMLPage):
# This aims to track input errors.
script_error = CleanText(u"//script[contains(text(), 'if (\"true\"===\"true\")')]")(self.doc)
if script_error:
raise TransferBankError(CleanText().filter(html2text(re.search(u'\.html\("(.*?)"\)', script_error).group(1))))
raise TransferBankError(message=CleanText().filter(html2text(re.search(u'\.html\("(.*?)"\)', script_error).group(1))))
def can_transfer(self, account_transfer_id):
for div in self.doc.xpath('//div[input[@id="indexCompteEmetteur"]]//div[@class="infoCompte" and not(@title)]'):
......@@ -1418,7 +1454,7 @@ class DepositPage(LoggedPage, HTMLPage):
col_name = 'Nom du contrat'
col_balance = 'Capital investi'
class item(ItemElement):
class item(OwnedItemElement):
klass = Account
obj_type = Account.TYPE_DEPOSIT
......@@ -1431,5 +1467,9 @@ class DepositPage(LoggedPage, HTMLPage):
obj_id = None
obj__transfer_id = None
def obj_ownership(self):
owner = CleanText(TableCell('owner'))(self)
return self.get_ownership(owner)
def set_deposit_account_id(self, account):
account.id = CleanText('//td[contains(text(), "N° contrat")]/following::td[1]//b')(self.doc)
......@@ -29,8 +29,9 @@ class LdlcParBrowser(AbstractBrowser):
documents = URL(r'/fr-fr/Orders/PartialCompletedOrdersHeader', DocumentsPage)
def __init__(self, *args, **kwargs):
super(LdlcParBrowser, self).__init__(*args, **kwargs)
def __init__(self, config, *args, **kwargs):
super(LdlcParBrowser, self).__init__(config, *args, **kwargs)
self.config = config
self.lang = 'fr-fr/'
@need_login
......
......@@ -37,18 +37,25 @@ class LdlcModule(AbstractModule, CapDocument):
EMAIL = 'vparedes@budget-insight.com'
LICENSE = 'LGPLv3+'
VERSION = '1.6'
CONFIG = BackendConfig(Value('login', label='Email'),
ValueBackendPassword('password', label='Password'),
Value('website', label='Site web', default='part',
choices={'pro': 'Professionnels', 'part': 'Particuliers'}),
Value('captcha_response', label='Réponse captcha', default='', required=False))
CONFIG = BackendConfig(
ValueBackendPassword('login', label='Email'),
ValueBackendPassword('password', label='Password'),
Value('website', label='Site web', default='part',
choices={'pro': 'Professionnels', 'part': 'Particuliers'}),
Value('captcha_response', label='Réponse captcha', default='', required=False),
)
PARENT = 'materielnet'
def create_default_browser(self):
if self.config['website'].get() == 'part':
self.BROWSER = LdlcParBrowser
return self.create_browser(self.config['login'].get(), self.config['password'].get(), weboob=self.weboob)
return self.create_browser(
self.config,
self.config['login'].get(),
self.config['password'].get(),
weboob=self.weboob,
)
else:
self.BROWSER = LdlcProBrowser
return self.create_browser(self.config, self.config['login'].get(), self.config['password'].get())
......@@ -60,5 +67,4 @@ class LdlcModule(AbstractModule, CapDocument):
return
if self.config['website'].get() == 'part':
return self.browser.open(bill.url).content
else:
return self.browser.download_document(bill)
return self.browser.download_document(bill)
......@@ -41,6 +41,7 @@ class HomePage(LoggedPage, HTMLPage):
@method
class get_subscriptions(ListElement):
item_xpath = '//div[@id="divAccueilInformationClient"]//div[@id="divInformationClient"]'
class item(ItemElement):
klass = Subscription
......
......@@ -30,7 +30,7 @@ from weboob.browser.filters.standard import CleanText
from weboob.exceptions import (
BrowserIncorrectPassword, BrowserUnavailable, BrowserQuestion, NeedInteractiveFor2FA,
)
from weboob.browser.exceptions import ClientError
from weboob.browser.exceptions import ClientError, BrowserTooManyRequests
from weboob.tools.value import Value
# Do not use an APIBrowser since APIBrowser sends all its requests bodies as
......@@ -79,11 +79,11 @@ class Number26Browser(DomainBrowser, StatesMixin):
try:
result = self.request('/api/mfa/challenge', json=data)
except ClientError as e:
response = e.response.json()
json_response = e.response.json()
# if we send more than 5 otp without success, the server will warn the user to
# wait 12h before retrying, but in fact it seems that we can resend otp 5 mins later
if e.response.status_code == 429:
raise BrowserUnavailable(response['detail'])
raise BrowserUnavailable(json_response['detail'])
raise BrowserQuestion(Value('otp', label='Veuillez entrer le code reçu par sms au ' + result['obfuscatedPhoneNumber']))
def update_token(self, auth_method, bearer, refresh_token, expires_in):
......@@ -103,11 +103,13 @@ class Number26Browser(DomainBrowser, StatesMixin):
try:
result = self.request('/oauth2/token', data=data)
except ClientError as e:
json_response = e.response.json()
if e.response.status_code == 401:
self.update_token('Basic', self.INITIAL_TOKEN, None, None)
return False
else:
assert False, 'Unhandled error: %s' % e.response.status_code
if e.response.status_code == 429:
raise BrowserTooManyRequests(json_response['detail'])
raise
self.update_token(result['token_type'], result['access_token'], result['refresh_token'], result['expires_in'])
return True
......@@ -136,23 +138,22 @@ class Number26Browser(DomainBrowser, StatesMixin):
try:
result = self.request('/oauth2/token', data=data)
except ClientError as ex:
response = ex.response.json()
if response.get('title') == 'A second authentication factor is required.':
self.mfaToken = response.get('mfaToken')
json_response = ex.response.json()
if json_response.get('title') == 'A second authentication factor is required.':
self.mfaToken = json_response.get('mfaToken')
self.do_otp(self.mfaToken)
elif response.get('error') == 'invalid_grant':
raise BrowserIncorrectPassword(response['error_description'])
elif response.get('title') == 'Error':
raise BrowserUnavailable(response['message'])
elif response.get('title') == 'invalid_otp':
raise BrowserIncorrectPassword(response['userMessage']['detail'])
elif json_response.get('error') == 'invalid_grant':
raise BrowserIncorrectPassword(json_response['error_description'])
elif json_response.get('title') == 'Error':
raise BrowserUnavailable(json_response['message'])
elif json_response.get('title') == 'invalid_otp':
raise BrowserIncorrectPassword(json_response['userMessage']['detail'])
# if we try too many requests, it will return a 429 and the user will have
# to wait 30 minutes before retrying, and if he retries at 29 min, he will have
# to wait 30 minutes more
elif ex.response.status_code == 429:
raise BrowserUnavailable(response['detail'])
else:
assert False, "Unhandled error on '/oauth2/token' request"
raise BrowserTooManyRequests(json_response['detail'])
raise
self.update_token(result['token_type'], result['access_token'], result['refresh_token'], result['expires_in'])
......
......@@ -17,6 +17,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from weboob.capabilities.bill import DocumentTypes, CapDocument, Subscription, Document, SubscriptionNotFound, DocumentNotFound
from weboob.capabilities.base import find_object, NotAvailable
......@@ -31,13 +32,15 @@ __all__ = ['OnlinenetModule']
class OnlinenetModule(Module, CapDocument):
NAME = 'onlinenet'
DESCRIPTION = u'Online.net'
MAINTAINER = u'Edouard Lambert'
DESCRIPTION = 'Online.net'
MAINTAINER = 'Edouard Lambert'
EMAIL = 'elambert@budget-insight.com'
LICENSE = 'LGPLv3+'
VERSION = '1.6'
CONFIG = BackendConfig(Value('login', label='Identifiant'),
ValueBackendPassword('password', label='Mot de passe'))
CONFIG = BackendConfig(
Value('login', label='Identifiant'),
ValueBackendPassword('password', label='Mot de passe'),
)
BROWSER = OnlinenetBrowser
......
......@@ -17,6 +17,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
import re
......@@ -42,7 +43,11 @@ class ProfilPage(LoggedPage, HTMLPage):
class item(ItemElement):
klass = Subscription
obj_subscriber = Format('%s %s', CleanText('//label[@for="form_firstname"]/../following-sibling::div'), CleanText('//label[@for="form_firstname"]/../following-sibling::div'))
obj_subscriber = Format(
'%s %s',
CleanText('//label[@for="form_firstname"]/../following-sibling::div'),
CleanText('//label[@for="form_firstname"]/../following-sibling::div')
)
obj_id = Env('username')
obj_label = obj_id
......@@ -56,9 +61,9 @@ class DocumentsPage(LoggedPage, HTMLPage):
item_xpath = '//h3[contains(text(), "bills")]/following-sibling::table//tr[position() > 1]'
head_xpath = '//h3[contains(text(), "bills")]/following-sibling::table//tr/th'
col_id = u'Id'
col_date = u'Date'
col_price = u'Total TTC'
col_id = 'Id'
col_date = 'Date'
col_price = 'Total TTC'
class item(ItemElement):
klass = Bill
......@@ -66,11 +71,11 @@ class DocumentsPage(LoggedPage, HTMLPage):
obj_id = Format('%s_%s', Env('username'), CleanDecimal(TableCell('id')))
obj__url = Attr('.//a[contains(text(), "PDF")]', 'href', default=NotAvailable)
obj_date = Date(CleanText(TableCell('date')))
obj_format = u"pdf"
obj_format = 'pdf'
obj_label = Format('Facture %s', CleanDecimal(TableCell('id')))
obj_type = DocumentTypes.BILL
obj_price = CleanDecimal(TableCell('price'))
obj_currency = u'EUR'
obj_currency = 'EUR'
def condition(self):
return CleanText(TableCell('id'))(self) != "No bills"
......@@ -78,7 +83,6 @@ class DocumentsPage(LoggedPage, HTMLPage):
def parse(self, el):
self.env['username'] = self.page.browser.username
@method
class get_documents(ListElement):
item_xpath = '//a[contains(@href, ".pdf")]'
......@@ -88,7 +92,7 @@ class DocumentsPage(LoggedPage, HTMLPage):
obj_id = Format('%s_%s', Env('username'), Env('docid'))
obj__url = Attr('.', 'href')
obj_format = u"pdf"
obj_format = 'pdf'
obj_label = CleanText('.')
obj_type = DocumentTypes.OTHER
......
# -*- coding: utf-8 -*-
# Copyright(C) 2016 Edouard Lambert
#
# This file is part of a weboob module.
#
# This weboob module is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This weboob module is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
from weboob.tools.test import BackendTest
class OnlinenetTest(BackendTest):
MODULE = 'onlinenet'
def test_onlinenet(self):
raise NotImplementedError()
......@@ -25,7 +25,10 @@ from weboob.browser import LoginBrowser, URL, need_login
from weboob.exceptions import BrowserIncorrectPassword, BrowserUnavailable, ActionNeeded, BrowserPasswordExpired
from .pages import LoginPage, BillsPage
from .pages.login import ManageCGI, HomePage, PasswordPage
from .pages.bills import SubscriptionsPage, BillsApiProPage, BillsApiParPage, ContractsPage
from .pages.bills import (
SubscriptionsPage, SubscriptionsApiPage, BillsApiProPage, BillsApiParPage,
ContractsPage,
)
from .pages.profile import ProfilePage
from weboob.browser.exceptions import ClientError, ServerError
from weboob.tools.compat import basestring
......@@ -38,28 +41,37 @@ __all__ = ['OrangeBillBrowser']
class OrangeBillBrowser(LoginBrowser):
BASEURL = 'https://espaceclientv3.orange.fr'
home_page = URL('https://businesslounge.orange.fr/$', HomePage)
loginpage = URL('https://login.orange.fr/\?service=sosh&return_url=https://www.sosh.fr/',
'https://login.orange.fr/front/login', LoginPage)
home_page = URL(r'https://businesslounge.orange.fr/$', HomePage)
loginpage = URL(
r'https://login.orange.fr/\?service=sosh&return_url=https://www.sosh.fr/',
r'https://login.orange.fr/front/login',
LoginPage,
)
password_page = URL(r'https://login.orange.fr/front/password', PasswordPage)
contracts = URL('https://espaceclientpro.orange.fr/api/contracts\?page=1&nbcontractsbypage=15', ContractsPage)
contracts = URL(r'https://espaceclientpro.orange.fr/api/contracts\?page=1&nbcontractsbypage=15', ContractsPage)
subscriptions = URL(r'https://espaceclientv3.orange.fr/js/necfe.php\?zonetype=bandeau&idPage=gt-home-page', SubscriptionsPage)
manage_cgi = URL('https://eui.orange.fr/manage_eui/bin/manage.cgi', ManageCGI)
subscriptions_api = URL(r'https://sso-f.orange.fr/omoi_erb/portfoliomanager/v2.0/contractSelector/users/current', SubscriptionsApiPage)
manage_cgi = URL(r'https://eui.orange.fr/manage_eui/bin/manage.cgi', ManageCGI)
# is billspage deprecated ?
billspage = URL('https://m.espaceclientv3.orange.fr/\?page=factures-archives',
'https://.*.espaceclientv3.orange.fr/\?page=factures-archives',
'https://espaceclientv3.orange.fr/\?page=factures-archives',
'https://espaceclientv3.orange.fr/\?page=facture-telecharger',
'https://espaceclientv3.orange.fr/maf.php',
'https://espaceclientv3.orange.fr/\?idContrat=(?P<subid>.*)&page=factures-historique',
'https://espaceclientv3.orange.fr/\?page=factures-historique&idContrat=(?P<subid>.*)',
BillsPage)
bills_api_pro = URL('https://espaceclientpro.orange.fr/api/contract/(?P<subid>\d+)/bills\?count=(?P<count>)',
BillsApiProPage)
billspage = URL(
r'https://m.espaceclientv3.orange.fr/\?page=factures-archives',
r'https://.*.espaceclientv3.orange.fr/\?page=factures-archives',
r'https://espaceclientv3.orange.fr/\?page=factures-archives',
r'https://espaceclientv3.orange.fr/\?page=facture-telecharger',
r'https://espaceclientv3.orange.fr/maf.php',
r'https://espaceclientv3.orange.fr/\?idContrat=(?P<subid>.*)&page=factures-historique',
r'https://espaceclientv3.orange.fr/\?page=factures-historique&idContrat=(?P<subid>.*)',
BillsPage,
)
bills_api_pro = URL(
r'https://espaceclientpro.orange.fr/api/contract/(?P<subid>\d+)/bills\?count=(?P<count>)',
BillsApiProPage,
)
bills_api_par = URL(r'https://sso-f.orange.fr/omoi_erb/facture/v2.0/billsAndPaymentInfos/users/current/contracts/(?P<subid>\d+)', BillsApiParPage)
doc_api_par = URL(r'https://sso-f.orange.fr/omoi_erb/facture/v1.0/pdf')
......@@ -137,10 +149,22 @@ class OrangeBillBrowser(LoginBrowser):
# if nb_sub is 0, we continue, because we can get them in next url
for sub in self._iter_subscriptions_by_type(profile.name, 'sosh'):
nb_sub += 1
yield sub
for sub in self._iter_subscriptions_by_type(profile.name, 'orange'):
nb_sub += 1
yield sub
if nb_sub == 0:
# No subscriptions found, trying with the API.
headers = {
'X-Orange-Caller-Id': 'ECQ',
}
self.subscriptions_api.go(headers=headers)
for sub in self.page.iter_subscription():
sub.subscriber = profile.name
yield sub
@need_login
def iter_documents(self, subscription):
documents = []
......
......@@ -17,13 +17,14 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this weboob module. If not, see <http://www.gnu.org/licenses/>.
from __future__ import unicode_literals
from weboob.capabilities.bill import DocumentTypes, CapDocument, Subscription, Document, SubscriptionNotFound, DocumentNotFound
from weboob.capabilities.base import find_object, NotAvailable
from weboob.capabilities.account import CapAccount
from weboob.capabilities.profile import CapProfile
from weboob.tools.backend import Module, BackendConfig
from weboob.tools.value import ValueBackendPassword, Value
from weboob.tools.value import ValueBackendPassword
from .browser import OrangeBillBrowser
......@@ -38,8 +39,10 @@ class OrangeModule(Module, CapAccount, CapDocument, CapProfile):
VERSION = '1.6'
DESCRIPTION = 'Orange French mobile phone provider'
LICENSE = 'LGPLv3+'
CONFIG = BackendConfig(Value('login', label='Login'),
ValueBackendPassword('password', label='Password'))
CONFIG = BackendConfig(
ValueBackendPassword('login', label='Login'),
ValueBackendPassword('password', label='Password'),
)
BROWSER = OrangeBillBrowser
def __init__(self, *args, **kwargs):
......@@ -49,7 +52,10 @@ class OrangeModule(Module, CapAccount, CapDocument, CapProfile):
accepted_document_types = (DocumentTypes.BILL,)
def create_default_browser(self):
return self.create_browser(self.config['login'].get(), self.config['password'].get())
return self.create_browser(
self.config['login'].get(),
self.config['password'].get(),
)
def iter_subscription(self):
return self.browser.get_subscription_list()
......
......@@ -52,7 +52,7 @@ class BillsApiProPage(LoggedPage, JsonPage):
class item(ItemElement):
klass = Bill
obj_date = Date(Dict('dueDate'), parse_func=parse_french_date, default=NotAvailable)
obj_date = Date(Dict('dueDate'), parse_func=parse_french_date, default=NotAvailable)
obj_price = CleanDecimal(Dict('amountIncludingTax'))
obj_format = 'pdf'