diff --git a/modules/boursorama/browser.py b/modules/boursorama/browser.py index 0c8e2153a31d671bcc9f073f1b8fb0f93933073e..be5db48a037460dc4ace63e2acdf9963e151fe6b 100644 --- a/modules/boursorama/browser.py +++ b/modules/boursorama/browser.py @@ -109,7 +109,7 @@ class BoursoramaBrowser(RetryLoginBrowser, TwoFactorBrowser): '/compte/pea/.*/mouvements', '/compte/0%25pea/.*/mouvements', '/compte/pea-pme/.*/mouvements', SavingMarketPage) - market = URL('/compte/(?!assurance|cav|epargne).*/(positions|mouvements)', + market = URL('/compte/(?!assurance|cav|epargne).*/(positions|mouvements|ordres)', '/compte/ord/.*/positions', MarketPage) loans = URL(r'/credit/paiement-3x/.*/informations', r'/credit/immobilier/.*/informations', @@ -400,18 +400,30 @@ def get_invest_transactions(self, account, coming): for t in sorted(transactions, key=lambda tr: tr.date, reverse=True): yield t + @retry_on_logout() @need_login - def get_investment(self, account): - if '/compte/derive' in account.url: - return iter([]) - if not account.type in (Account.TYPE_LIFE_INSURANCE, Account.TYPE_MARKET, Account.TYPE_PEA): - raise NotImplementedError() + def iter_investment(self, account): + if '/compte/derive' in account.url or account.type not in (Account.TYPE_LIFE_INSURANCE, Account.TYPE_MARKET, Account.TYPE_PEA): + return [] self.location(account.url) - # We might deconnect at this point. - if self.login.is_here(): - return self.get_investment(account) return self.page.iter_investment() + @retry_on_logout() + @need_login + def iter_market_orders(self, account): + # Only Market & PEA accounts have the Market Orders tab + if '/compte/derive' in account.url or account.type not in (Account.TYPE_MARKET, Account.TYPE_PEA): + return [] + self.location(account.url) + + # Go to Market Orders tab ('Mes ordres') + market_order_link = self.page.get_market_order_link() + if not market_order_link: + self.logger.warning('Could not find market orders link for account "%s".', account.label) + return [] + self.location(market_order_link) + return self.page.iter_market_orders() + @need_login def get_profile(self): return self.profile.stay_or_go().get_profile() diff --git a/modules/boursorama/module.py b/modules/boursorama/module.py index ba590cd8705c1bff70a114a5d7f1cfaef8b1272d..89930c96ebbe51c5c26da8925bb5d441187269a5 100644 --- a/modules/boursorama/module.py +++ b/modules/boursorama/module.py @@ -74,7 +74,10 @@ def iter_coming(self, account): yield tr def iter_investment(self, account): - return self.browser.get_investment(account) + return self.browser.iter_investment(account) + + def iter_market_orders(self, account): + return self.browser.iter_market_orders(account) def get_profile(self): return self.browser.get_profile() diff --git a/modules/boursorama/pages.py b/modules/boursorama/pages.py index b1f4ced14aa9a6c6a2fb84ef60acd6f7e46c5d52..88b783513d30cce2beaca9709c0d9120158d1cf0 100644 --- a/modules/boursorama/pages.py +++ b/modules/boursorama/pages.py @@ -33,19 +33,21 @@ CleanText, CleanDecimal, Field, Format, Regexp, Date, AsyncLoad, Async, Eval, Env, Currency as CleanCurrency, Map, Coalesce, - MapIn, Lower, + MapIn, Lower, Base, ) from weboob.browser.filters.json import Dict from weboob.browser.filters.html import Attr, Link, TableCell from weboob.capabilities.bank import ( - Account, Investment, Recipient, Transfer, AccountNotFound, + Account, Investment, MarketOrder, Recipient, Transfer, AccountNotFound, AddRecipientBankError, TransferInvalidAmount, Loan, AccountOwnership, + MarketOrderType, MarketOrderDirection, ) from weboob.tools.capabilities.bank.investments import create_french_liquidity from weboob.capabilities.base import NotAvailable, Currency, find_object, empty from weboob.capabilities.profile import Person from weboob.tools.capabilities.bank.transactions import FrenchTransaction from weboob.tools.capabilities.bank.iban import is_iban_valid +from weboob.tools.capabilities.bank.investments import IsinCode from weboob.tools.value import Value from weboob.tools.date import parse_french_date from weboob.tools.compat import urljoin, urlencode, urlparse, range @@ -691,6 +693,16 @@ def inner(page, *args, **kwargs): return inner +MARKET_ORDER_TYPES = { + 'LIM': MarketOrderType.LIMIT, +} + +MARKET_DIRECTIONS = { + 'Achat': MarketOrderDirection.BUY, + 'Vente': MarketOrderDirection.SALE, +} + + class MarketPage(LoggedPage, HTMLPage): def get_balance(self, account_type): txt = u"Solde au" if account_type is Account.TYPE_LIFE_INSURANCE else u"Total Portefeuille" @@ -699,6 +711,9 @@ def get_balance(self, account_type): span_balance = CleanDecimal('//li/span[contains(text(), "%s")]/following-sibling::span' % txt, replace_dots=True, default=None)(self.doc) return h_balance or span_balance or None + def get_market_order_link(self): + return Link('//a[contains(@data-url, "orders")]', default=None)(self.doc) + @my_pagination @method class iter_history(TableElement): @@ -778,6 +793,45 @@ def get_transactions_from_detail(self, account): yield t + @method + class iter_market_orders(TableElement): + item_xpath = '//table/tbody/tr[td]' + head_xpath = '//table/thead/tr/th' + + col_date = 'Date' + col_label = 'Libellé' + col_direction = 'Sens' + col_state = 'Etat' + col_quantity = 'Qté' + col_order_type = 'Type' + col_unitvalue = 'Cours' + col_validity_date = 'Validité' + col_stock_market = 'Marché' + + class item(ItemElement): + klass = MarketOrder + + obj_id = Base(TableCell('date'), CleanText('.//a')) + obj_label = CleanText(TableCell('label'), children=False) + obj_direction = Map(CleanText(TableCell('direction')), MARKET_DIRECTIONS, MarketOrderDirection.UNKNOWN) + obj_code = IsinCode(Base(TableCell('label'), CleanText('.//a'))) + obj_stock_market = CleanText(TableCell('stock_market')) + obj_currency = CleanCurrency(TableCell('unitvalue')) + + # Unitprice may be absent if the order is still ongoing + obj_unitprice = CleanDecimal.US(TableCell('state'), default=NotAvailable) + obj_unitvalue = CleanDecimal.French(TableCell('unitvalue')) + obj_ordervalue = CleanDecimal.French(TableCell('order_type')) + obj_quantity = CleanDecimal.French(TableCell('quantity')) + + obj_date = Date(Base(TableCell('date'), CleanText('.//span')), dayfirst=True) + obj_validity_date = Date(CleanText(TableCell('validity_date')), dayfirst=True) + + # Text format looks like 'LIM 49,000', we only use the 'LIM' for typing + obj_order_type = Map(Regexp(CleanText(TableCell('order_type')), r'^([^ ]+) '), MARKET_ORDER_TYPES, MarketOrderType.UNKNOWN) + # Text format looks like 'Exécuté 12.345 $' or 'En cours', we only fetch the first words + obj_state = CleanText(Regexp(CleanText(TableCell('state')), r'^(\D+)')) + class SavingMarketPage(MarketPage): @pagination