weboob-devel/modules/wellsfargo/pages.py

385 lines
13 KiB
Python

# -*- coding: utf-8 -*-
# Copyright(C) 2014 Oleg Plakhotniuk
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from weboob.capabilities.bank import Account, Transaction
from weboob.tools.capabilities.bank.transactions import \
AmericanTransaction as AmTr
from weboob.browser.pages import HTMLPage, LoggedPage, RawPage
from urllib import unquote
from decimal import Decimal
from requests.cookies import morsel_to_cookie
from .parsers import StatementParser, clean_label
import itertools
import re
import datetime
import Cookie
class LoginProceedPage(LoggedPage, HTMLPage):
is_here = '//script[contains(text(),"setAndCheckCookie")]'
def proceed(self):
script = self.doc.xpath('//script/text()')[0]
cookieStr = re.match('.*document\.cookie = "([^"]+)".*',
script, re.DOTALL).group(1)
morsel = Cookie.Cookie(cookieStr).values()[0]
self.browser.session.cookies.set_cookie(morsel_to_cookie(morsel))
form = self.get_form()
return form.submit()
class LoginRedirectPage(LoggedPage, HTMLPage):
is_here = 'contains(//meta[@http-equiv="Refresh"]/@content,' \
'"SIGNON_PORTAL_PAUSE")'
def redirect(self):
refresh = self.doc.xpath(
'//meta[@http-equiv="Refresh"]/@content')[0]
url = re.match(r'^.*URL=(.*)$', refresh).group(1)
return self.browser.location(url)
class LoggedInPage(HTMLPage):
@property
def logged(self):
return bool(self.doc.xpath(u'//a[text()="Sign Off"]')) \
or bool(self.doc.xpath(u'//title[text()="Splash Page"]'))
class SummaryPage(LoggedInPage):
is_here = u'//title[contains(text(),"Account Summary")]'
def to_activity(self):
href = self.doc.xpath(u'//a[text()="Account Activity"]/@href')[0]
self.browser.location(href)
def to_statements(self):
href = self.doc.xpath('//a[text()="Statements & Documents"]'
'/@href')[0]
self.browser.location(href)
class AccountPage(LoggedInPage):
def account_id(self, name=None):
if name:
return name[-4:] # Last 4 digits of "BLAH XXXXXXX1234"
else:
return self.account_id(self.account_name())
class ActivityPage(AccountPage):
def is_here(self):
return bool(self.doc.xpath(
u'contains(//title/text(),"Account Activity")'))
def accounts_names(self):
return self.doc.xpath(
u'//select[@name="selectedAccountUID"]/option/text()')
def accounts_ids(self):
return [self.account_id(name) for name in self.accounts_names()]
def account_uid(self, id_=None):
if id_:
return self.doc.xpath(
u'//select[@name="selectedAccountUID"]'
u'/option[contains(text(),"%s")]/@value' % id_)[0]
else:
return self.doc.xpath(
u'//select[@name="selectedAccountUID"]'
u'/option[@selected="selected"]/@value')[0]
def account_name(self):
for name in self.doc.xpath(u'//select[@name="selectedAccountUID"]'
u'/option[@selected="selected"]/text()'):
return name
return u''
def account_type(self, name=None):
raise NotImplementedError()
def account_balance(self):
raise NotImplementedError()
def account_paydatemin(self):
return None, None
def account_cardlimit(self):
return None
def to_account(self, id_):
form = self.get_form(xpath='//form[@name="AccountActivityForm"]')
form['selectedAccountUID'] = [self.account_uid(id_)]
form.submit()
def get_account(self):
name = self.account_name()
balance = self.account_balance()
currency = Account.get_currency(balance)
id_ = self.account_id()
type_ = self.account_type()
paydate, paymin = self.account_paydatemin()
cardlimit = self.account_cardlimit()
account = Account()
account.id = id_
account.label = name
account.currency = currency
account.balance = AmTr.decimal_amount(balance)
account.type = type_
if paydate is not None:
account.paydate = paydate
if paymin is not None:
account.paymin = paymin
if cardlimit is not None:
account.cardlimit = AmTr.decimal_amount(cardlimit)
return account
def since_last_statement(self):
raise NotImplementedError()
def iter_transactions(self):
raise NotImplementedError()
def next_(self):
raise NotImplementedError()
class ActivityCashPage(ActivityPage):
def is_here(self):
return super(ActivityCashPage, self).is_here() and \
(u'CHECKING' in self.account_name() or
u'SAVINGS' in self.account_name())
def account_type(self, name=None):
name = name or self.account_name()
if u'CHECKING' in name:
return Account.TYPE_CHECKING
elif u'SAVINGS' in name:
return Account.TYPE_SAVINGS
else:
return Account.TYPE_UNKNOWN
def account_balance(self):
return self.doc.xpath(
u'//td[@headers="currentPostedBalance"]/span/text()')[0]
def since_last_statement(self):
form = self.get_form(xpath='//form[@id="ddaShowForm"]')
form['showTabDDACommand.transactionTypeFilterValue'] = [
u'All Transactions']
form['showTabDDACommand.timeFilterValue'] = ['8']
form.submit()
return True
def iter_transactions(self):
for row in self.doc.xpath('//tr/th[@headers='
'"postedHeader dateHeader"]/..'):
date = row.xpath('th[@headers="postedHeader '
'dateHeader"]/text()')[0]
desc = row.xpath('td[@headers="postedHeader '
'descriptionHeader"]/span/text()')[0]
deposit = row.xpath('td[@headers="postedHeader '
'depositsConsumerHeader"]/span/text()')[0]
withdraw = row.xpath('td[@headers="postedHeader '
'withdrawalsConsumerHeader"]/span/text()')[0]
date = datetime.datetime.strptime(date, '%m/%d/%y')
desc = clean_label(desc)
deposit = deposit.strip()
deposit = AmTr.decimal_amount(deposit or '0')
withdraw = withdraw.strip()
withdraw = AmTr.decimal_amount(withdraw or '0')
amount = deposit - withdraw
trans = Transaction(u'')
trans.date = date
trans.rdate = date
trans.type = Transaction.TYPE_UNKNOWN
trans.raw = desc
trans.label = desc
trans.amount = amount
yield trans
def next_(self):
links = self.doc.xpath('//a[@title="Go To Next Page"]/@href')
if links:
self.browser.location(links[0])
return True
else:
return False
class ActivityCardPage(ActivityPage):
def is_here(self):
return super(ActivityCardPage, self).is_here() and \
u'CARD' in self.account_name()
def account_type(self, name=None):
return Account.TYPE_CARD
def account_balance(self):
return self.doc.xpath(
u'//td[@headers="outstandingBalance"]/text()')[0]
def account_cardlimit(self):
return self.doc.xpath(
u'//td[@headers="totalCreditLimit"]/text()')[0]
def account_paydatemin(self):
if self.doc.xpath(u'//p[contains(text(),'
'"payment due date is not yet scheduled")]'):
# If payment date is not scheduled yet, set it somewhere in a
# distant future, so that we always have a valid date.
return datetime.datetime.now() + datetime.timedelta(days=999), \
Decimal(0)
else:
date = self.doc.xpath(u'//span[contains(text(),"Minimum Payment")]'
'/span/text()')[0]
date = re.match(u'.*(../../..).*', date).group(1)
date = datetime.datetime.strptime(date, '%m/%d/%y')
amount = self.doc.xpath(u'//td[@headers="minimumPaymentDue"]'
'//text()')[0].strip()
return date, AmTr.decimal_amount(amount)
def get_account(self):
account = ActivityPage.get_account(self)
# Credit card is essentially a liability.
# Negative amount means there's a payment due.
account.balance = -account.balance
return account
def since_last_statement(self):
if self.doc.xpath('//select[@name="showTabCommand.'
'transactionTypeFilterValue"]'
'/option[@value="sincelastStmt"]'):
form = self.get_form(xpath='//form[@id="creditCardShowForm"]')
form['showTabCommand.transactionTypeFilterValue'] = [
'sincelastStmt']
form.submit()
return True
def iter_transactions(self):
for row in self.doc.xpath('//tr/th[@headers='
'"postedHeader transactionDateHeader"]/..'):
tdate = row.xpath('th[@headers="postedHeader '
'transactionDateHeader"]/text()')[0]
pdate = row.xpath('td[@headers="postedHeader '
'postingDateHeader"]/text()')[0]
desc = row.xpath('td[@headers="postedHeader '
'descriptionHeader"]/span/text()')[0]
ref = row.xpath('td[@headers="postedHeader '
'descriptionHeader"]/text()')[0]
amount = row.xpath('td[@headers="postedHeader '
'amountHeader"]/text()')[0]
tdate = datetime.datetime.strptime(tdate, '%m/%d/%y')
pdate = datetime.datetime.strptime(pdate, '%m/%d/%y')
desc = clean_label(desc)
ref = re.match('.*<REFERENCE ([^>]+)>.*', ref).group(1)
if amount.startswith('+'):
amount = AmTr.decimal_amount(amount[1:])
else:
amount = -AmTr.decimal_amount(amount)
trans = Transaction(ref)
trans.date = tdate
trans.rdate = pdate
trans.type = Transaction.TYPE_UNKNOWN
trans.raw = desc
trans.label = desc
trans.amount = amount
yield trans
def next_(self):
# As of 2014-07-05, there's only one page for cards history.
return False
class StatementsPage(AccountPage):
is_here = u'contains(//title/text(),"Statements")'
def account_name(self):
return self.doc.xpath(
u'//select[@name="selectedAccountKey"]'
u'/option[@selected="selected"]/text()')[0]
def account_uid(self, id_):
return self.doc.xpath(
u'//select[@name="selectedAccountKey"]'
u'/option[contains(text(),"%s")]/@value' % id_)[0]
def to_account(self, id_):
form = self.get_form(xpath='//form[@id="statementsAndDocumentsModel"]')
form['selectedAccountKey'] = [self.account_uid(id_)]
form.submit()
def year(self):
for text in self.doc.xpath('//h2/strong/text()'):
try:
return int(text)
except ValueError:
pass
def years(self):
for text in self.doc.xpath('//h2//strong/text()'):
try:
yield int(text)
except ValueError:
pass
def to_year(self, year):
href = self.doc.xpath('//h2/a/strong[text()="%s"]/../@href' % year)[0]
self.browser.location(href)
def statements(self):
for outer_uri in self.doc.xpath(
'//table[@id="listOfStatements"]'
'//a[contains(text(), "Statement")]/@href'):
inner_uri = re.match('.*destinationClickUrl=([^&]+)&.*',
outer_uri).group(1)
yield unquote(inner_uri)
class StatementPage(LoggedPage, RawPage):
def __init__(self, *args, **kwArgs):
RawPage.__init__(self, *args, **kwArgs)
self._parser = StatementParser(self.doc)
def is_here(self):
return self.doc[:4] == '%PDF'
def iter_transactions(self):
# Maintain a nice consistent newer-to-older order of transactions.
return sorted(
itertools.chain(
self._parser.read_cash_transactions(),
self._parser.read_card_transactions()),
cmp=lambda t1, t2: cmp(t2.date, t1.date) or
cmp(t1.label, t2.label) or
cmp(t1.amount, t2.amount))