diff --git a/modules/paypal/browser.py b/modules/paypal/browser.py
index c9dfa75b..ac975aa0 100644
--- a/modules/paypal/browser.py
+++ b/modules/paypal/browser.py
@@ -18,10 +18,12 @@
# along with weboob. If not, see .
-from weboob.deprecated.browser import Browser, BrowserIncorrectPassword
-from .pages import LoginPage, AccountPage, DownloadHistoryPage, LastDownloadHistoryPage, SubmitPage, HistoryParser, UselessPage, HistoryPage, CSVAlreadyAsked, HistoryDetailsPage
-from .newpages import NewHomePage, NewAccountPage, NewProHistoryPage, NewPartHistoryPage
import datetime
+from dateutil.relativedelta import relativedelta
+
+from weboob.deprecated.browser import Browser, BrowserIncorrectPassword
+
+from .pages import LoginPage, AccountPage, UselessPage, HomePage, ProHistoryPage, PartHistoryPage, HistoryDetailsPage
__all__ = ['Paypal']
@@ -36,33 +38,28 @@ class Paypal(Browser):
'/cgi-bin/webscr\?cmd=_login-run$': LoginPage,
'/cgi-bin/webscr\?cmd=_login-submit.+$': LoginPage, # wrong login
'/cgi-bin/webscr\?cmd=_login-processing.+$': UselessPage,
- '/cgi-bin/webscr\?cmd=_account&nav=0.0$': AccountPage,
- '/cgi-bin/webscr\?cmd=_login-done.+$': AccountPage,
- '/cgi-bin/webscr\?cmd=_home&country_lang.x=true$': NewHomePage,
- '/cgi-bin/webscr\?cmd=_history-download&nav=0.3.1$': DownloadHistoryPage,
- '/cgi-bin/webscr\?cmd=_history&nav=0.3.0$': HistoryPage,
- '/cgi-bin/webscr\?cmd=_history&dispatch=[a-z0-9]+$': HistoryPage,
- '/cgi-bin/webscr\?cmd=_history-download-recent$': LastDownloadHistoryPage,
- '/cgi-bin/webscr\?dispatch=[a-z0-9]+$': (SubmitPage, HistoryParser()),
- '/cgi-bin/webscr\?cmd=_history-download-recent-submit&dispatch=[a-z0-9]+$': (SubmitPage, HistoryParser()),
- 'https://history.paypal.com/cgi-bin/webscr\?cmd=_history-details-from-hub&id=[A-Z0-9]+$': HistoryDetailsPage,
- 'https://www.paypal.com/webapps/business/\?nav=0.0': NewHomePage,
- 'https://www.paypal.com/webapps/business/\?country_lang.x=true': NewHomePage,
- 'https://www.paypal.com/myaccount/\?nav=0.0': NewHomePage,
- 'https://www.paypal.com/businessexp/money': NewAccountPage,
- 'https://www.paypal.com/webapps/business/activity\?.*': NewProHistoryPage,
- 'https://www.paypal.com/myaccount/activity/.*': (NewPartHistoryPage, 'json'),
- 'https://www.paypal.com/myaccount/': NewProHistoryPage,
+ '/cgi-bin/webscr\?cmd=_account.*$': UselessPage,
+ '/cgi-bin/webscr\?cmd=_login-done.+$': UselessPage,
+ '/cgi-bin/webscr\?cmd=_home&country_lang.x=true$': HomePage,
+ 'https://\w+.paypal.com/cgi-bin/webscr\?cmd=_history-details-from-hub&id=[A-Z0-9]+$': HistoryDetailsPage,
+ 'https://\w+.paypal.com/webapps/business/\?nav=0.0': HomePage,
+ 'https://\w+.paypal.com/webapps/business/\?country_lang.x=true': HomePage,
+ 'https://\w+.paypal.com/myaccount/\?nav=0.0': HomePage,
+ 'https://\w+.paypal.com/businessexp/money': AccountPage,
+ 'https://\w+.paypal.com/webapps/business/activity\?.*': ProHistoryPage,
+ 'https://\w+.paypal.com/myaccount/activity/.*': (PartHistoryPage, 'json'),
+ 'https://\w+.paypal.com/myaccount/': ProHistoryPage,
}
- DEFAULT_TIMEOUT = 30 # CSV export is slow
+ DEFAULT_TIMEOUT = 60
BEGINNING = datetime.date(1998, 6, 1) # The day PayPal was founded
- website = None
+ account_type = None
- def find_website_version(self):
- self.website = "new"
- if self.is_on_page(NewHomePage):
+ def find_account_type(self):
+ if self.is_on_page(HomePage):
+ # XXX Unable to get more than 2 years of history on pro accounts.
+ self.BEGINNING = datetime.date.today() - relativedelta(months=24)
self.account_type = "pro"
return
self.location(self._response.info().getheader('refresh').split("bin/")[1])
@@ -71,7 +68,7 @@ class Paypal(Browser):
self.account_type = "perso"
else:
self.location('/webapps/business/?nav=0.0')
- if self.is_on_page(NewHomePage):
+ if self.is_on_page(HomePage):
self.account_type = "pro"
else:
self.account_type = "perso"
@@ -95,73 +92,37 @@ class Paypal(Browser):
if self.is_on_page(LoginPage):
raise BrowserIncorrectPassword()
- self.find_website_version()
+ self.find_account_type()
def get_accounts(self):
- if self.website == "old":
- if not self.is_on_page(AccountPage):
- self.location('/en/cgi-bin/webscr?cmd=_account&nav=0.0')
- elif not self.is_on_page(NewAccountPage):
+ if not self.is_on_page(AccountPage):
self.location('/businessexp/money')
return self.page.get_accounts()
def get_account(self, _id):
- if self.website == "old":
- if not self.is_on_page(AccountPage):
- self.location('/en/cgi-bin/webscr?cmd=_account&nav=0.0')
- elif not self.is_on_page(NewAccountPage):
+ if not self.is_on_page(AccountPage):
self.location('/businessexp/money')
return self.page.get_account(_id)
- def get_history(self, account, step_min=90, step_max=365*10):
- def fetch_fn(start, end):
- def transactions():
- parse = True
- while parse:
- for trans in self.page.iter_transactions(account):
- yield trans
- parse = self.page.next()
- self.history(start=start, end=end)
- if next(self.page.parse(), False):
- return transactions()
- return self.smart_fetch(beginning=self.BEGINNING,
- end=datetime.date.today(),
- step_min=step_min,
- step_max=step_max,
- fetch_fn=fetch_fn)
-
- def history(self, start, end):
- self.location('/en/cgi-bin/webscr?cmd=_history&nav=0.3.0')
- self.page.filter(start, end)
- assert self.is_on_page(HistoryPage)
-
def get_download_history(self, account, step_min=None, step_max=None):
if step_min is None and step_max is None:
- if self.website == "old":
- step_min = 90
- step_max = 365*2
- else:
- step_min = 90
- step_max = 180
+ step_min = 30
+ step_max = 180
def fetch_fn(start, end):
- if self.website == "old" and self.download_history(start, end).rows:
- return self.page.iter_transactions(account)
- elif self.download_history(start, end):
+ if self.download_history(start, end):
return self.page.iter_transactions(account)
+ return iter([])
+
assert step_max <= 365*2 # PayPal limitations as of 2014-06-16
- try:
- for i in self.smart_fetch(beginning=self.BEGINNING,
- end=datetime.date.today(),
- step_min=step_min,
- step_max=step_max,
- fetch_fn=fetch_fn):
- yield i
- except CSVAlreadyAsked:
- for i in self.download_last_history(account):
- yield i
+ for i in self.smart_fetch(beginning=self.BEGINNING,
+ end=datetime.date.today(),
+ step_min=step_min,
+ step_max=step_max,
+ fetch_fn=fetch_fn):
+ yield i
def smart_fetch(self, beginning, end, step_min, step_max, fetch_fn):
"""
@@ -172,57 +133,45 @@ class Paypal(Browser):
step = step_min
while end > beginning:
start = end - datetime.timedelta(step)
- chunk = fetch_fn(start, end)
+ chunk = list(fetch_fn(start, end))
end = start - datetime.timedelta(1)
- if chunk:
- # If there're transactions in current period,
- # decrease the period.
+ if len(chunk) > 50:
+ # If there're too much transactions in current period, decrease
+ # the period.
step = max(step_min, step/FACTOR)
- for trans in chunk:
- yield trans
else:
- # If there's no transactions in current period,
+ # If there's no transactions, or only a bit, in current period,
# increase the period.
step = min(step_max, step*FACTOR)
+ for trans in chunk:
+ yield trans
def download_history(self, start, end):
"""
- Download CSV history.
+ Download history.
However, it is not normalized, and sometimes the download is refused
and sent later by mail.
"""
- if self.website == "old":
- self.location('/en/cgi-bin/webscr?cmd=_history-download&nav=0.3.1')
- assert self.is_on_page(DownloadHistoryPage)
- self.page.download(start, end)
- assert self.is_on_page(SubmitPage)
- return self.page.document
+ s = start.strftime('%d/%m/%Y')
+ e = end.strftime('%d/%m/%Y')
+ # Settings a big magic number so we hope to get all transactions for the period
+ LIMIT = '9999'
+ if self.account_type == "pro":
+ self.location('https://www.paypal.com/webapps/business/activity?fromdate=' + s + '&todate=' + e + '&transactiontype=ALL_TRANSACTIONS¤cy=ALL_TRANSACTIONS_CURRENCY&limit=' + LIMIT)
else:
- s = start.strftime('%d/%m/%Y')
- e = end.strftime('%d/%m/%Y')
- # Settings a big magic number so we get all transaction for the period
- LIMIT = '9999'
- if self.account_type == "pro":
- self.location('/webapps/business/activity?fromdate=' + s + '&todate=' + e + '&transactiontype=ALL_TRANSACTIONS¤cy=ALL_TRANSACTIONS_CURRENCY&limit=' + LIMIT)
- else:
- self.location('/myaccount/activity/filter?typeFilter=all&isNewSearch=true&startDate=' + s + '&endDate=' + e + '&limit=' + LIMIT)
- return self.page.transaction_left()
-
- def download_last_history(self, account):
- self.location('/en/cgi-bin/webscr?cmd=_history-download-recent')
- self.page.download()
- if self.page.document.rows:
- return self.page.iter_transactions(account)
+ self.location('https://www.paypal.com/myaccount/activity/filter?typeFilter=all&isNewSearch=true&startDate=' + s + '&endDate=' + e + '&limit=' + LIMIT)
+ return self.page.transaction_left()
def transfer(self, from_id, to_id, amount, reason=None):
raise NotImplementedError()
def convert_amount(self, account, trans):
- if(trans['actions']['details']['action'] == 'ACTIVITY_DETAILS'):
+ if trans['actions']['details']['action'] == 'ACTIVITY_DETAILS':
self.location(trans['actions']['details']['url'])
if self.is_on_page(HistoryDetailsPage):
cc = self.page.get_converted_amount(account)
if cc:
trans['originalAmount'] = trans['netAmount']
trans['netAmount'] = cc
+
return trans
diff --git a/modules/paypal/module.py b/modules/paypal/module.py
index f1f4cf40..8357b2e8 100644
--- a/modules/paypal/module.py
+++ b/modules/paypal/module.py
@@ -47,14 +47,12 @@ class PaypalModule(Module, CapBank):
return self.browser.get_accounts().itervalues()
def get_account(self, _id):
- with self.browser:
- account = self.browser.get_account(_id)
+ account = self.browser.get_account(_id)
if account:
return account
else:
raise AccountNotFound()
def iter_history(self, account):
- with self.browser:
- for history in self.browser.get_download_history(account):
- yield history
+ for history in self.browser.get_download_history(account):
+ yield history
diff --git a/modules/paypal/newpages.py b/modules/paypal/newpages.py
deleted file mode 100644
index 6528e46e..00000000
--- a/modules/paypal/newpages.py
+++ /dev/null
@@ -1,133 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright(C) 2014-2015 Budget Insight
-#
-# 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 .
-
-import re
-from decimal import Decimal
-
-from weboob.deprecated.browser import Page
-from weboob.capabilities.bank import Account
-from weboob.tools.capabilities.bank.transactions import FrenchTransaction
-from weboob.tools.date import parse_french_date
-
-
-class NewHomePage(Page):
- pass
-
-
-class NewAccountPage(Page):
- def get_account(self, _id):
- return self.get_accounts().get(_id)
-
- def get_accounts(self):
- accounts = {}
- content = self.document.xpath('//div[@id="moneyPage"]')[0]
-
- # Primary currency account
- primary_account = Account()
- primary_account.type = Account.TYPE_CHECKING
- balance = self.parser.tocleanstring(content.xpath('//div[contains(@class, "col-md-6")][contains(@class, "available")]')[0])
- primary_account.currency = Account.get_currency(balance)
- primary_account.id = unicode(primary_account.currency)
-
- primary_account.balance = Decimal(FrenchTransaction.clean_amount(balance))
-
- primary_account.label = u'%s %s*' % (self.browser.username, primary_account.currency)
-
- accounts[primary_account.id] = primary_account
-
- return accounts
-
-
-class NewProHistoryPage(Page):
-
- def iter_transactions(self, account):
- for trans in self.parse():
- if trans._currency == account.currency:
- yield trans
-
- def parse(self):
- for i, tr in enumerate(self.document.xpath('//tr')):
- t = FrenchTransaction(tr.xpath('./td[@class="transactionId"]/span')[0].text.strip())
- date = parse_french_date(tr.xpath('./td[@class="date"]')[0].text.strip())
- status = tr.xpath('./td[@class="desc"]/ul/li[@class="first"]')[0].text.strip()
- #We pass this because it's not transaction
- if status == u'Créé' or status == u'Annulé' or status == u'Suspendu':
- continue
- raw = tr.xpath('./td[@class="desc"]/strong')[0].text.strip()
- t.parse(date=date, raw=raw)
- amount = tr.xpath('./td[@class="price"]/span')[0].text.strip()
- t.set_amount(amount)
- t._currency = Account.get_currency(amount)
- yield t
-
- def transaction_left(self):
- return (len(self.document.xpath('//div[@class="no-records"]')) == 0)
-
-
-class NewPartHistoryPage(Page):
- def transaction_left(self):
- return (len(self.document['data']['activity']['COMPLETED']) > 0 or len(self.document['data']['activity']['PENDING']) > 0)
-
- def iter_transactions(self, account):
- for trans in self.parse(account):
- yield trans
-
- def parse(self, account):
- transactions = list()
-
- for status in ['PENDING', 'COMPLETED']:
- transac = self.document['data']['activity'][status]
- for t in transac:
- tran = self.parse_transaction(t, account)
- if tran:
- transactions.append(tran)
-
- transactions.sort(key=lambda tr: tr.rdate, reverse=True)
- for t in transactions:
- yield t
-
- def parse_transaction(self, transaction, account):
- t = FrenchTransaction(transaction['activityId'])
- date = parse_french_date(transaction['date'])
- raw = transaction.get('counterparty', transaction['displayType'])
- t.parse(date=date, raw=raw)
-
- if transaction['currencyCode'] != account.currency:
- transaction = self.browser.convert_amount(account, transaction)
- try:
- t.original_amount = self.format_amount(transaction['originalAmount'], transaction["isCredit"])
- t.original_currency = transaction["currencyCode"]
- except KeyError:
- return
- try:
- t.amount = self.format_amount(transaction['netAmount'], transaction["isCredit"])
- except KeyError:
- return
-
- t._currency = transaction['currencyCode']
-
- return t
-
- def format_amount(self, to_format, is_credit):
- m = re.search(r"\D", to_format[::-1])
- amount = Decimal(re.sub(r'[^\d]', '', to_format))/Decimal((10 ** m.start()))
- if is_credit:
- return abs(amount)
- else:
- return -abs(amount)
diff --git a/modules/paypal/pages.py b/modules/paypal/pages.py
index 3af24697..3430b516 100644
--- a/modules/paypal/pages.py
+++ b/modules/paypal/pages.py
@@ -17,24 +17,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
-from decimal import InvalidOperation
+from decimal import Decimal
import re
-import datetime
-import dateutil.parser
-
-from weboob.deprecated.browser import Page, BrokenPageError
-from weboob.deprecated.browser.parsers.csvparser import CsvParser
-from weboob.tools.misc import to_unicode
-from weboob.tools.date import parse_french_date
-from weboob.capabilities.bank import Account, Transaction
-from weboob.tools.capabilities.bank.transactions import \
- AmericanTransaction as AmTr
+from weboob.capabilities.bank import Account
from weboob.capabilities.base import NotAvailable
-
-
-class CSVAlreadyAsked(Exception):
- pass
+from weboob.deprecated.browser import Page
+from weboob.tools.capabilities.bank.transactions import FrenchTransaction
+from weboob.tools.date import parse_french_date
class LoginPage(Page):
@@ -45,331 +35,121 @@ class LoginPage(Page):
self.browser.submit(nologin=True)
+class UselessPage(Page):
+ pass
+
+
+class HomePage(Page):
+ pass
+
+
class AccountPage(Page):
def get_account(self, _id):
return self.get_accounts().get(_id)
def get_accounts(self):
accounts = {}
- content = self.document.xpath('//div[@id="main"]//div[@class="col first"]')[0]
+ content = self.document.xpath('//div[@id="moneyPage"]')[0]
# Primary currency account
primary_account = Account()
primary_account.type = Account.TYPE_CHECKING
-
- # Total currency balance.
- # If there are multiple currencies, this balance is all currencies
- # converted to the main currency.
try:
- balance = content.xpath('.//h3/span[@class="balance"]')
- if not balance:
- balance = content.xpath('.//li[@class="balance"]//span/strong')
- balance = balance[0].text_content().strip()
- primary_account.balance = AmTr.decimal_amount(balance)
- primary_account.currency = Account.get_currency(balance)
- primary_account.id = unicode(primary_account.currency)
- primary_account.label = u'%s %s*' % (self.browser.username, balance.split()[-1])
+ balance = self.parser.tocleanstring(content.xpath('//div[contains(@class, "col-md-6")][contains(@class, "available")]')[0])
except IndexError:
+ primary_account.id = 'EUR'
+ primary_account.currency = 'EUR'
primary_account.balance = NotAvailable
primary_account.label = u'%s' % (self.browser.username)
+ else:
+ primary_account.currency = Account.get_currency(balance)
+ primary_account.id = unicode(primary_account.currency)
+ primary_account.balance = Decimal(FrenchTransaction.clean_amount(balance))
+ primary_account.label = u'%s %s*' % (self.browser.username, primary_account.currency)
+
accounts[primary_account.id] = primary_account
- # The following code will only work if the user enabled multiple currencies.
- balance = content.xpath('.//div[@class="body"]//ul/li[@class="balance"]/span')
- table = content.xpath('.//table[@id="balanceDetails"]//tbody//tr')
-
- # sanity check
- if bool(balance) is not bool(table):
- raise BrokenPageError('Unable to find all required multiple currency entries')
-
- # Primary currency balance.
- # If the user enabled multiple currencies, we get this one instead.
- # An Account object has only one currency; secondary currencies should be other accounts.
- if balance:
- balance = balance[0].text_content().strip()
- primary_account.balance = AmTr.decimal_amount(balance)
- # The primary currency of the "head balance" is the same; ensure we got the right one
- assert primary_account.currency == primary_account.get_currency(balance)
-
- for row in table:
- balance = row.xpath('.//td')[-1].text_content().strip()
- account = Account()
- account.type = Account.TYPE_CHECKING
- # XXX it ignores 5+ devises, so it's bad, but it prevents a crash, cf #1216
- try:
- account.balance = AmTr.decimal_amount(balance)
- except InvalidOperation:
- continue
- account.currency = Account.get_currency(balance)
- account.id = unicode(account.currency)
- account.label = u'%s %s' % (self.browser.username, balance.split()[-1])
- if account.id == primary_account.id:
- assert account.balance == primary_account.balance
- assert account.currency == primary_account.currency
- elif account.currency:
- accounts[account.id] = account
-
return accounts
-class DownloadHistoryPage(Page):
- def download(self, start, end):
- tr_last_file_request = self.document.xpath('//table//table//table[@width="100%"]//tr[2]//td')
- if len(tr_last_file_request) > 1 and tr_last_file_request[1].text is not None:
- last_file_request = tr_last_file_request[1].text[:-1]
- try:
- last_file_request = dateutil.parser.parse(last_file_request.encode('utf-8')).date()
- except ValueError:
- last_file_request = parse_french_date(last_file_request).date()
-
- if last_file_request == datetime.date.today():
- raise CSVAlreadyAsked('')
- self.browser.select_form(name='form1')
- self.browser['to_c'] = str(end.year)
- self.browser['to_a'] = str(end.month)
- self.browser['to_b'] = str(end.day)
- self.browser['from_c'] = str(start.year)
- self.browser['from_a'] = str(start.month)
- self.browser['from_b'] = str(start.day)
-
- self.browser['custom_file_type'] = ['comma_allactivity']
- self.browser['latest_completed_file_type'] = ['']
-
- self.browser.submit()
-
-
-class LastDownloadHistoryPage(Page):
- def download(self):
- self.browser.select_form(nr=1)
- log_select = self.document.xpath('//table//form//input[@type="radio"]')[0].attrib['value']
- self.browser['log_select'] = [log_select]
- self.browser.submit()
-
-
-class SubmitPage(Page):
- """
- Any result of form submission
- """
-
- def iter_transactions(self, account):
- csv = self.document
-
- if len(csv.header) == 42 or len(csv.header) == 43:
- # Merchant multi-currency account
- # 42 is for when the user can't access the balance on the website
- # 43 is for full acces to the account
- DATE = 0
- TIME = 1
- NAME = 3
- TYPE = 4
- if csv.header[7] == "Devise":
- CURRENCY = 7
- GROSS = 8
- FEE = 9
- NET = 10
- FROM = 11
- TO = 12
- TRANS_ID = 13
- ITEM = 16
- SITE = -1
-
- else:
- CURRENCY = 6
- GROSS = 7
- FEE = 8
- NET = 9
- FROM = 10
- TO = 11
- TRANS_ID = 12
- ITEM = 15
- SITE = 24
- elif len(csv.header) == 11:
- # Regular multi-currency account
- DATE = 0
- TIME = 1
- NAME = 3
- TYPE = 4
- CURRENCY = 6
- GROSS = -1
- FEE = -1
- NET = 7
- FROM = -1
- TO = -1
- TRANS_ID = -1
- ITEM = -1
- SITE = -1
- else:
- raise ValueError('CSV fields count of %i is not supported' % len(csv.header))
-
- for row in csv.rows:
- # we filter transaction currceny to match account currency, except if we don't now the account currency
- # we ignore canceled transactions
- if (account.balance != NotAvailable and account.get_currency(row[CURRENCY]) != account.currency) or row[NET] == '...':
- continue
-
- # analog to dict.get()
- get = lambda i, v=None: row[i] if 0 <= i < len(row) else v
-
- trans = Transaction(get(TRANS_ID, u''))
-
- # silly American locale
- if re.search(r'\d\.\d\d$', row[NET]):
- date = datetime.datetime.strptime(row[DATE] + ' ' + row[TIME], "%m/%d/%Y %H:%M:%S")
- else:
- date = datetime.datetime.strptime(row[DATE] + ' ' + row[TIME], "%d/%m/%Y %H:%M:%S")
- trans.date = date
- trans.rdate = date
-
- line = row[NAME]
- if get(ITEM):
- line += u' ' + row[ITEM]
- if get(SITE):
- line += u"(" + row[SITE] + u")"
- trans.raw = line
- trans.label = row[NAME]
-
- if row[TYPE].startswith(u'Update to eCheck') or \
- row[TYPE].startswith(u'Order'):
- continue
-
- if row[TYPE].endswith(u'Credit Card') or row[TYPE].endswith(u'carte bancaire'):
- trans.type = Transaction.TYPE_CARD
- elif row[TYPE].endswith(u'Payment Sent') or row[TYPE].startswith(u'Paiement'):
- trans.type = Transaction.TYPE_ORDER
- elif row[TYPE] in (u'Currency Conversion', u'Conversion de devise'):
- trans.type = Transaction.TYPE_BANK
- else:
- trans.type = Transaction.TYPE_UNKNOWN
-
- # Net is what happens after the fee (0 for most users), so what is the most "real"
- trans.amount = AmTr.decimal_amount(row[NET])
- trans._gross = AmTr.decimal_amount(get(GROSS, row[NET]))
- trans._fees = AmTr.decimal_amount(get(FEE, u'0.00'))
-
- trans._to = get(TO)
- trans._from = get(FROM)
-
- yield trans
-
-
-class HistoryParser(CsvParser):
- HEADER = True
- FMTPARAMS = {'skipinitialspace': True}
-
- def decode_row(self, row, encoding):
- """
- PayPal returns different encodings (latin-1 and utf-8 are know ones)
- """
- return [to_unicode(cell) for cell in row]
-
-
-class UselessPage(Page):
- pass
-
-
-class HistoryPage(Page):
- def guess_format(self):
- rp = re.compile('PAYPAL\.widget\.CalendarLocales\.MDY_([A-Z]+)_POSITION\s*=\s*(\d)')
- rd = re.compile('PAYPAL\.widget\.CalendarLocales\.DATE_DELIMITER\s*=\s*"(.)"')
- rm = re.compile('PAYPAL\.widget\.CalendarLocales\.MONTH_NAMES\s*=\s*\[(.+)\]')
- translate = {'DAY': '%d', 'MONTH': '%m', 'YEAR': '%Y'}
- pos = {}
- delim = '/'
- months = {}
- for script in self.document.xpath('//script'):
- for line in script.text_content().splitlines():
- m = rp.match(line)
- if m and m.groups():
- pos[int(m.groups()[1])] = translate[m.groups()[0]]
- else:
- m = rd.match(line)
- if m:
- delim = m.groups()[0]
- else:
- m = rm.match(line)
- if m:
- months = [month.strip("'").strip().lower()[0:3]
- for month
- in m.groups()[0].split(',')]
- date_format = delim.join((pos[0], pos[1], pos[2]))
- if date_format == "%m/%d/%Y":
- time_format = "%I:%M:%S %p"
- else:
- time_format = "%H:%M:%S"
- return date_format, time_format, months
-
- def filter(self, start, end):
- date_format = self.guess_format()[0]
- self.browser.select_form(name='history')
- self.browser['dateoption'] = ['dateselect']
- self.browser['from_date'] = start.strftime(date_format)
- self.browser['to_date'] = end.strftime(date_format)
- self.browser.submit(name='show')
- self.browser.select_form(name='history')
- self.browser.submit(name='filter_2')
-
- def next(self):
- if self.document.xpath('//input[@name="next"]'):
- self.browser.select_form(name='history')
- self.browser.submit(name='next')
- return True
-
- def parse(self):
- emonths = ['January', 'February', 'March', 'April',
- 'May', 'June', 'July', 'August',
- 'September', 'October', 'November', 'December']
- date_format, time_format, months = self.guess_format()
- for row in self.document.xpath('//table[@id="transactionTable"]/tbody/tr'):
- if len(row.xpath('.//td')) < 5:
- continue
-
- amount = row.xpath('.//td[@headers="gross"]')[-1].text_content().strip()
- if re.search('\d', amount):
- currency = Account.get_currency(amount)
- amount = AmTr.decimal_amount(amount)
- else:
- continue
-
- idtext = row.xpath('.//td[@class="detailsNoPrint"]//span[@class="accessAid"]')[0] \
- .text_content().replace(u'\xa0', u' ').strip().rpartition(' ')[-1]
- trans = Transaction(idtext)
- trans.amount = amount
- trans._currency = currency
-
- datetext = row.xpath('.//td[@class="dateInfo"]')[0].text_content().strip()
- for i in range(0, 12):
- datetext = datetext.replace(months[i], emonths[i])
- date = dateutil.parser.parse(datetext)
- trans.date = date
- trans.rdate = date
-
- trans.label = to_unicode(row.xpath('.//td[@class="emailInfo"]')[0].text_content().strip())
- info = to_unicode(row.xpath('.//td[@class="paymentTypeInfo"]')[0].text_content().strip())
- trans.raw = info + u' ' + trans.label
-
- if u'Authorization' in info or u'Autorisation' in info or \
- u'Order' in info:
- continue
-
- if u'Credit Card' in trans.label or u'Carte bancaire' in trans.label:
- trans.type = Transaction.TYPE_CARD
- elif info.startswith(u'Payment') or info.startswith(u'Paiement'):
- trans.type = Transaction.TYPE_ORDER
- elif u'Currency Conversion' in info or u'Conversion de devise' in info:
- trans.type = Transaction.TYPE_BANK
- else:
- trans.type = Transaction.TYPE_UNKNOWN
-
- yield trans
-
+class ProHistoryPage(Page):
def iter_transactions(self, account):
for trans in self.parse():
if trans._currency == account.currency:
yield trans
+ def parse(self):
+ for tr in self.document.xpath('//tr'):
+ t = FrenchTransaction(tr.xpath('./td[@class="transactionId"]/span')[0].text.strip())
+ date = parse_french_date(tr.xpath('./td[@class="date"]')[0].text.strip())
+ status = tr.xpath('./td[@class="desc"]/ul/li[@class="first"]')[0].text.strip()
+ #We pass this because it's not transaction
+ if status in [u'Créé', u'Annulé', u'Suspendu', u'Mis à jour']:
+ continue
+ raw = tr.xpath('./td[@class="desc"]/strong')[0].text.strip()
+ t.parse(date=date, raw=raw)
+ amount = tr.xpath('./td[@class="price"]/span')[0].text.strip()
+ t.set_amount(amount)
+ t._currency = Account.get_currency(amount)
+ yield t
+
+ def transaction_left(self):
+ return len(self.document.xpath('//div[@class="no-records"]')) == 0
+
+
+class PartHistoryPage(Page):
+ def transaction_left(self):
+ return len(self.document['data']['activity']['COMPLETED']) > 0 or len(self.document['data']['activity']['PENDING']) > 0
+
+ def iter_transactions(self, account):
+ for trans in self.parse(account):
+ yield trans
+
+ def parse(self, account):
+ transactions = list()
+
+ for status in ['PENDING', 'COMPLETED']:
+ transac = self.document['data']['activity'][status]
+ for t in transac:
+ tran = self.parse_transaction(t, account)
+ if tran:
+ transactions.append(tran)
+
+ transactions.sort(key=lambda tr: tr.rdate, reverse=True)
+ for t in transactions:
+ yield t
+
+ def parse_transaction(self, transaction, account):
+ t = FrenchTransaction(transaction['activityId'])
+ date = parse_french_date(transaction['date'])
+ raw = transaction.get('counterparty', transaction['displayType'])
+ t.parse(date=date, raw=raw)
+
+ if transaction['currencyCode'] != account.currency:
+ transaction = self.browser.convert_amount(account, transaction)
+ try:
+ t.original_amount = self.format_amount(transaction['originalAmount'], transaction["isCredit"])
+ t.original_currency = transaction["currencyCode"]
+ except KeyError:
+ return
+ try:
+ t.amount = self.format_amount(transaction['netAmount'], transaction["isCredit"])
+ except KeyError:
+ return
+
+ t._currency = transaction['currencyCode']
+
+ return t
+
+ def format_amount(self, to_format, is_credit):
+ m = re.search(r"\D", to_format[::-1])
+ amount = Decimal(re.sub(r'[^\d]', '', to_format))/Decimal((10 ** m.start()))
+ if is_credit:
+ return abs(amount)
+ else:
+ return -abs(amount)
class HistoryDetailsPage(Page):
-
def get_converted_amount(self, account):
find_td = self.document.xpath('//td[contains(text(),"' + account.currency + ')")]')
if len(find_td) > 0 :