diff --git a/modules/fortuneo/browser.py b/modules/fortuneo/browser.py index f4a73cb300860808525e2639d7154513d8206448..870690d0ed063be90af4e0b13aa7d2d24057c788 100644 --- a/modules/fortuneo/browser.py +++ b/modules/fortuneo/browser.py @@ -74,6 +74,7 @@ class FortuneoBrowser(TwoFactorBrowser): r'.*/prive/mes-comptes/compte-titres-pea/.*', r'.*/prive/mes-comptes/ppe/.*', PeaHistoryPage) invest_history = URL(r'.*/prive/mes-comptes/assurance-vie/.*', InvestmentHistoryPage) + ajax_sync_call = URL(r'/AsynchAjax\?key0=(?P[^&]*)&div0=(?P[^&]*)&time=450') loan_contract = URL(r'/fr/prive/mes-comptes/credit-immo/contrat-credit-immo/contrat-pret-immobilier.jsp.*', LoanPage) unavailable = URL(r'/customError/indispo.html', UnavailablePage) security_page = URL(r'/fr/prive/identification-carte-securite-forte.jsp.*', SecurityPage) @@ -242,6 +243,39 @@ def iter_investments(self, account): if liquidity: yield create_french_liquidity(liquidity) + @need_login + def iter_market_orders(self, account): + if not getattr(account, '_market_orders_link'): + return + + self.location(account._market_orders_link) + + # Market orders are loaded with an AJAX call + # It loads the market orders table and the form to choose a range of dates + for _ in range(3): + self.location(account._market_orders_link) + if self.page.are_market_orders_loaded(): + break + self.logger.debug('Sleeping for a few seconds so market orders can load...') + time.sleep(3) + + form = self.page.get_date_range_form() + # Once we submit the form with the date range, + # we need to reload the page until they are loaded + for _ in range(3): + form.submit() + if self.page.are_market_orders_loaded(): + break + self.logger.debug('Sleeping for a few seconds so market orders can load...') + time.sleep(3) + + for market_order in self.page.iter_market_orders(): + if market_order._details_link: + self.location(market_order._details_link) + self.page.fill_market_order(obj=market_order) + yield market_order + + @need_login def iter_history(self, account): if account.type == Account.TYPE_LOAN: diff --git a/modules/fortuneo/module.py b/modules/fortuneo/module.py index 89cb8582902e9c10497fdfdfa95817b4643348b8..4651a2325dfed0d0002dc7cb6bd7f44c8dc24b72 100644 --- a/modules/fortuneo/module.py +++ b/modules/fortuneo/module.py @@ -71,6 +71,9 @@ def iter_coming(self, account): def iter_investment(self, account): return self.browser.iter_investments(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/fortuneo/pages/accounts_list.py b/modules/fortuneo/pages/accounts_list.py index d189b70d1a485c28359dd425c2135f4cc1e510d9..e0b936e1b0643c020a957b1f8558a4182581e616 100644 --- a/modules/fortuneo/pages/accounts_list.py +++ b/modules/fortuneo/pages/accounts_list.py @@ -31,7 +31,7 @@ CleanText, CleanDecimal, Regexp, Date, Currency, TableCell, Base, Field, MapIn, ) from weboob.capabilities import NotAvailable -from weboob.capabilities.bank import Account, AccountOwnership +from weboob.capabilities.bank import Account, AccountOwnership, MarketOrder, MarketOrderDirection, MarketOrderType from weboob.capabilities.wealth import Investment from weboob.capabilities.profile import Person from weboob.browser.pages import HTMLPage, LoggedPage, FormNotFound, CsvPage @@ -90,6 +90,21 @@ def get_local_error_message(self): return CleanText('//div[@id="error"]/p[@class="erreur_texte1"]')(self.doc) +MARKET_ORDER_DIRECTIONS = { + 'Achat': MarketOrderDirection.BUY, + 'Vente': MarketOrderDirection.SALE, +} + +MARKET_ORDER_TYPES = { + 'MAR': MarketOrderType.MARKET, + 'DC': MarketOrderType.MARKET, + 'LIM': MarketOrderType.LIMIT, + 'AML': MarketOrderType.LIMIT, + 'ASD': MarketOrderType.TRIGGER, + 'APD': MarketOrderType.TRIGGER, +} + + class PeaHistoryPage(ActionNeededPage): def on_load(self): err_msgs = [ @@ -201,6 +216,68 @@ def obj_balance(self): def obj_currency(self): return Currency('//div[@id="valorisation_compte"]//td[contains(text(), "Solde")]')(self) + def obj__market_orders_link(self): + return AbsoluteLink('//a[contains(@href, "carnet-d-ordres.jsp")]')(self) + + def get_date_range_form(self): + today = date.today() + form = self.get_form() + form['dateDeb'] = (today - relativedelta(years=1)).strftime('%d/%m/%Y') + form['dateFin'] = today.strftime('%d/%m/%Y') + return form + + def are_market_orders_loaded(self): + return bool(self.doc.xpath('//form[@id="afficherCarnetOrdre"]')) + + def get_parameters_hash(self): + return Attr('//input[@value="as_afficherCarnetOrdre.do_"]', 'name')(self.doc) + + @method + class iter_market_orders(TableElement): + item_xpath = '//table[@id="t_intraday"]/tbody/tr[td and not(./td[contains(text(), "Aucun ordre")])]' + head_xpath = '//table[@id="t_intraday"]/thead//th' + + col_label = re.compile(r'.*Libellé') + col_direction = 'Sens' + col_quantity = 'Qté' + col_order_type_ordervalue = re.compile(r'Type') + col_validity_date = 'Validité' + col_state_unitprice = 'Etat' + col_date = 'Transmission' + col_currency = 'Devise' + + class item(ItemElement): + klass = MarketOrder + + obj__details_link = AbsoluteLink('.//a[@class="bt_l_loupe"]', default=NotAvailable) + obj_label = CleanText(TableCell('label')) + obj_direction = MapIn(CleanText(TableCell('direction')), MARKET_ORDER_DIRECTIONS, MarketOrderDirection.UNKNOWN) + obj_quantity = CleanDecimal.French(TableCell('quantity')) + obj_order_type = MapIn(CleanText(TableCell('order_type_ordervalue')), MARKET_ORDER_TYPES, MarketOrderType.UNKNOWN) + obj_ordervalue = CleanDecimal.French( + Regexp(CleanText(TableCell('order_type_ordervalue')), r'\(.*\)', default=NotAvailable), + default=NotAvailable, + ) + obj_validity_date = Date(CleanText(TableCell('validity_date')), dayfirst=True) + + # If the order has been executed, the state is followed by the unit price. + obj_state = Regexp(CleanText(TableCell('state_unitprice')), r'(.+?)\d|$', default=NotAvailable) + obj_unitprice = CleanDecimal.French(TableCell('state_unitprice'), default=NotAvailable) + + obj_date = Date(CleanText(TableCell('date')), dayfirst=True) + obj_currency = Currency(TableCell('currency')) + + @method + class fill_market_order(ItemElement): + obj_execution_date = Date( + Regexp(CleanText('//tr[contains(./th, "Date et heure d\'exécution")]/td'), r'(.*) -', default=NotAvailable), + dayfirst=True, + default=NotAvailable, + ) + obj_unitvalue = CleanDecimal.French('//tr/th[contains(text(), "Dernier")]/following-sibling::td[1]') + obj_code = IsinCode(CleanText('//tr[contains(./th, "Code ISIN")]/td'), default=NotAvailable) + obj_stock_market = CleanText('//tr[contains(./th, "Place de cotation")]/td') + class InvestmentHistoryPage(ActionNeededPage): @method @@ -318,7 +395,6 @@ def build_doc(self, content): @method class fill_account(ItemElement): - def obj_coming(self): for tr in self.xpath('//table[@id="tableauConsultationHisto"]/tbody/tr'): if 'Encours' in CleanText('./td')(tr):