diff --git a/modules/ideel/__init__.py b/modules/ideel/__init__.py new file mode 100644 index 00000000..22e4fdd2 --- /dev/null +++ b/modules/ideel/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 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 . + + +from .module import IdeelModule + +__all__ = ['IdeelModule'] diff --git a/modules/ideel/browser.py b/modules/ideel/browser.py new file mode 100644 index 00000000..e00bd862 --- /dev/null +++ b/modules/ideel/browser.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 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 . + + +from weboob.tools.capabilities.bank.transactions import \ + AmericanTransaction as AmTr +from weboob.browser import LoginBrowser, URL, need_login +from weboob.browser.pages import HTMLPage +from weboob.capabilities.base import Currency +from weboob.capabilities.shop import Order, Item, Payment, OrderNotFound +from weboob.exceptions import BrowserIncorrectPassword + +import re +from decimal import Decimal +from datetime import datetime +from itertools import takewhile, count + + +__all__ = ['Ideel'] + + +class IdeelPage(HTMLPage): + @property + def logged(self): + return bool(self.doc.xpath('//a[@href="/logout"]')) + + +class LoginPage(IdeelPage): + def login(self, username, password): + form = self.get_form(xpath='//form[@id="iform"]') + form['login'] = username + form['password'] = password + form.submit() + + +class HistoryPage(IdeelPage): + def exists(self): + return bool(self.doc.xpath('//table[@id="order_history"]')) + + def iter_orders(self): + return (tr.xpath('td[1]/a/text()')[0][1:] + for tr in self.doc.xpath('//table[@id="order_history"]/tbody/tr')) + + +class OrderPage(IdeelPage): + def exists(self): + return bool(self.order_number()) + + def order(self): + order = Order(id=self.order_number()) + order.date = self.order_date() + order.tax = self.tax() + order.shipping = self.shipping() + order.discount = self.discount() + order.total = self.total() + return order + + def items(self): + for tr in self.doc.xpath('//table[contains(@class,"items_table")]' + '/tr[td[@class="items_desc"]]'): + label = tr.xpath('*//div[@class="item_desc"]//span/text()')[0] + url = tr.xpath('*//div[@class="item_img"]//@src')[0] + onclk = tr.xpath('*//div[@class="item_img"]//@onclick') + if onclk: + url=re.match(r'window.open\(\'([^\']*)\'.*', onclk[0]).group(1) + if url.startswith('/'): + url = self.browser.BASEURL + url + price = tr.xpath('td[@class="items_price"]/span/text()')[0] + qty = tr.xpath('td[@class="items_qty"]//span/text()')[0] + price = AmTr.decimal_amount(price) * Decimal(qty) + item = Item() + item.label = unicode(label) + item.url = unicode(url) + item.price = price + yield item + + def payments(self): + # There's no payment information on Ideel, so we'll make one up. + p = Payment() + p.date = self.order_date() + p.method = u'DEFAULT PAYMENT' + p.amount = self.total() + yield p + + def order_number(self): + return next(iter(self.doc.xpath( + u'//b[text()="Order Number:"]/../strong/text()')), None) + + def order_date(self): + txt = self.doc.xpath('//div[@id="purchase-notice"]/text()')[0] + date = re.match(r'.* (\w+ \d+, \d+)$', txt).group(1) + return datetime.strptime(date, '%b %d, %Y') + + def tax(self): + return AmTr.decimal_amount(self.doc.xpath( + '//span[@id="taxes"]/text()')[0]) + + def shipping(self): + return AmTr.decimal_amount(self.doc.xpath( + '//span[@id="shipping_fee"]/text()')[0]) + + def discount(self): + TAGS = ['coupon_discount_amount', 'promo_discount_amount', + 'total_rewards', 'applied_credit'] + return -sum(AmTr.decimal_amount(x[1:][:-1]) for tag in TAGS + for x in self.doc.xpath('//span[@id="%s"]/text()' % tag)) + + def total(self): + return AmTr.decimal_amount(self.doc.xpath( + '//span[@id="total"]/text()')[0]) + + +class Ideel(LoginBrowser): + BASEURL = 'http://www.ideel.com' + login = URL(r'https://www.ideel.com/login$', LoginPage) + history = URL(r'/my_account/orders\?page=(?P\d+)$', HistoryPage) + order = URL(r'/my_account/orders/(?P\d+)$', OrderPage) + unknown = URL(r'/.*$', IdeelPage) + + def get_currency(self): + # Ideel uses only U.S. dollars. + return Currency.get_currency(u'$') + + @need_login + def get_order(self, id_): + if self.order.go(order=id_).exists(): + return self.page.order() + raise OrderNotFound() + + @need_login + def iter_orders(self): + exists = HistoryPage.exists + hists = takewhile(exists, (self.history.go(page=i) for i in count(1))) + return (self.get_order(x) for h in hists for x in h.iter_orders()) + + @need_login + def iter_payments(self, order): + return self.order.stay_or_go(order=order.id).payments() + + @need_login + def iter_items(self, order): + return self.order.stay_or_go(order=order.id).items() + + def do_login(self): + self.login.stay_or_go().login(self.username, self.password) + if not self.page.logged: + raise BrowserIncorrectPassword() diff --git a/modules/ideel/favicon.png b/modules/ideel/favicon.png new file mode 100644 index 00000000..0b434100 Binary files /dev/null and b/modules/ideel/favicon.png differ diff --git a/modules/ideel/module.py b/modules/ideel/module.py new file mode 100644 index 00000000..e446c163 --- /dev/null +++ b/modules/ideel/module.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 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 . + + +from weboob.capabilities.shop import CapShop +from weboob.tools.backend import Module, BackendConfig +from weboob.tools.value import ValueBackendPassword + +from .browser import Ideel + +__all__ = ['IdeelModule'] + +class IdeelModule(Module, CapShop): + NAME = 'ideel' + MAINTAINER = u'Oleg Plakhotniuk' + EMAIL = 'olegus8@gmail.com' + VERSION = '1.1' + LICENSE = 'AGPLv3+' + DESCRIPTION = u'Ideel' + CONFIG = BackendConfig( + ValueBackendPassword('username', label='User name', masked=False), + ValueBackendPassword('password', label='Password')) + BROWSER = Ideel + + def create_default_browser(self): + return self.create_browser(self.config['username'].get(), + self.config['password'].get()) + + def get_currency(self): + return self.browser.get_currency() + + def get_order(self, id_): + return self.browser.get_order(id_) + + def iter_orders(self): + return self.browser.iter_orders() + + def iter_payments(self, order): + return self.browser.iter_payments(order) + + def iter_items(self, order): + return self.browser.iter_items(order) diff --git a/modules/ideel/test.py b/modules/ideel/test.py new file mode 100644 index 00000000..19884d55 --- /dev/null +++ b/modules/ideel/test.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2015 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 . + +from weboob.tools.test import BackendTest + + +class IdeelTest(BackendTest): + MODULE = 'ideel' + + def test_history(self): + """ + Test that at least one item was ordered in the whole history. + """ + b = self.backend + items = (i for o in b.iter_orders() for i in b.iter_items(o)) + item = next(items, None) + self.assertNotEqual(item, None)