diff --git a/modules/creditmutuel/browser.py b/modules/creditmutuel/browser.py index 9de6a896a36e9670f99684a5adabe06e1319a181..cd20bd92a348178c79395b3bdc1da60e31f2d35e 100644 --- a/modules/creditmutuel/browser.py +++ b/modules/creditmutuel/browser.py @@ -58,6 +58,7 @@ ConditionsPage, MobileConfirmationPage, UselessPage, DecoupledStatePage, CancelDecoupled, OtpValidationPage, OtpBlockedErrorPage, TwoFAUnabledPage, LoansOperationsPage, OutagePage, PorInvestmentsPage, PorHistoryPage, PorHistoryDetailsPage, + PorMarketOrdersPage, PorMarketOrderDetailsPage, ) @@ -143,6 +144,12 @@ class CreditMutuelBrowser(TwoFactorBrowser): PorHistoryPage ) por_history_details = URL(r'/(?P.*)fr/banque/PORT_OperationsDet.aspx', PorHistoryDetailsPage) + por_market_orders = URL( + r'/(?P.*)fr/banque/PORT_OrdresLst.aspx', + r'/(?P.*)fr/banque/PORT_OrdresLst.aspx\?&ddp=(?P.*)', + PorMarketOrdersPage + ) + por_market_order_details = URL(r'/(?P.*)fr/banque/PORT_OrdresDet.aspx', PorMarketOrderDetailsPage) por_action_needed = URL(r'/(?P.*)fr/banque/ORDR_InfosGenerales.aspx', EmptyPage) li = URL(r'/(?P.*)fr/assurances/profilass.aspx\?domaine=epargne', @@ -649,6 +656,30 @@ def get_monthly_transactions(self, trs): trs.append(tr) return trs + @need_login + def iter_market_orders(self, account): + if account._is_inv and account.type in (Account.TYPE_MARKET, Account.TYPE_PEA): + self.go_por_accounts() + self.por_market_orders.go(subbank=self.currentSubBank, ddp=account._link_id) + self.page.submit_date_range_form() + if self.page.has_no_order(): + return + orders = [] + page_index = 0 + # We stop at a maximum of 100 pages to avoid an infinite loop. + while page_index < 100: + page_index += 1 + for order in self.page.iter_market_orders(): + orders.append(order) + if not self.page.has_next_page(): + break + self.page.submit_next_page_form() + for order in orders: + if order._market_order_link: + self.location(order._market_order_link) + self.page.fill_market_order(obj=order) + yield order + @need_login def get_history(self, account): transactions = [] diff --git a/modules/creditmutuel/module.py b/modules/creditmutuel/module.py index d0e286054d31f04e33da043b189f3a1439ef4d67..ae24a445c47571faf4b1fd8ce881258392dc7313 100644 --- a/modules/creditmutuel/module.py +++ b/modules/creditmutuel/module.py @@ -96,6 +96,9 @@ def iter_history(self, account): def iter_investment(self, account): return self.browser.get_investment(account) + def iter_market_orders(self, account): + return self.browser.iter_market_orders(account) + def iter_transfer_recipients(self, origin_account): if not self.browser.is_new_website: self.logger.info('On old creditmutuel website') diff --git a/modules/creditmutuel/pages.py b/modules/creditmutuel/pages.py index c7b47a6c30265558d66a65ea2a472770f7b2e043..99f98a8aeb8a4f17caf6fce2ebe23593d8ce3da0 100644 --- a/modules/creditmutuel/pages.py +++ b/modules/creditmutuel/pages.py @@ -33,6 +33,7 @@ from weboob.browser.filters.standard import ( Filter, Env, CleanText, CleanDecimal, Field, Regexp, Async, AsyncLoad, Date, Format, Type, Currency, Base, Coalesce, + Map, MapIn, ) from weboob.browser.filters.html import Link, Attr, TableCell, ColumnNotFound from weboob.exceptions import ( @@ -45,7 +46,9 @@ Account, Recipient, TransferBankError, Transfer, AddRecipientBankError, AddRecipientStep, Loan, Emitter, ) -from weboob.capabilities.wealth import Investment +from weboob.capabilities.wealth import ( + Investment, MarketOrder, MarketOrderDirection, MarketOrderType, +) from weboob.capabilities.contact import Advisor from weboob.capabilities.profile import Profile from weboob.tools.capabilities.bank.iban import is_iban_valid @@ -53,7 +56,7 @@ from weboob.tools.capabilities.bank.transactions import FrenchTransaction from weboob.capabilities.bill import DocumentTypes, Subscription, Document from weboob.tools.compat import urlparse, parse_qs, urljoin, range -from weboob.tools.date import parse_french_date +from weboob.tools.date import parse_french_date, LinearDateGuesser from weboob.tools.value import Value @@ -1825,6 +1828,96 @@ def obj_investments(self): return [investment] +MARKET_ORDER_DIRECTIONS = { + 'Achat': MarketOrderDirection.BUY, + 'Vente': MarketOrderDirection.SALE, +} + + +MARKET_ORDER_TYPES = { + 'limit': MarketOrderType.LIMIT, + 'marché': MarketOrderType.MARKET, + 'déclenchement': MarketOrderType.TRIGGER, +} + + +class PorMarketOrdersPage(PorHistoryPage): + def has_no_order(self): + return bool(self.doc.xpath('//td[contains(@id, "PORT_ListeOrdres1_bwebTdPasOrdreEnCours")]')) + + @method + class iter_market_orders(TableElement): + item_xpath = '//table[@class="liste bourse"]/tbody/tr[td]' + head_xpath = '//table[@class="liste bourse"]/thead//th' + + col_date = 'Saisie' + col_direction = 'Sens' + col_order_type = 'Modalité' + col_quantity = re.compile(r'Qté') + col_label = 'Valeur' + col_ordervalue = 'Limite-Seuil' + col_validity_date = re.compile(r'Validité') + col_state = 'Etat' + + def parse(self, el): + self.env['date_guesser'] = LinearDateGuesser() + + class item(ItemElement): + klass = MarketOrder + + obj_direction = Map( + CleanText(TableCell('direction')), + MARKET_ORDER_DIRECTIONS, + MarketOrderDirection.UNKNOWN + ) + obj_order_type = MapIn(CleanText(TableCell('order_type')), MARKET_ORDER_TYPES, MarketOrderType.UNKNOWN) + obj_quantity = CleanDecimal.French(TableCell('quantity')) + obj_label = CleanText(TableCell('label')) + + def obj_ordervalue(self): + if Field('order_type') in (MarketOrderType.LIMIT, MarketOrderType.TRIGGER): + return CleanDecimal.French(Regexp(CleanText(TableCell('ordervalue')), r'[^/]+$')) + + obj_validity_date = Date(CleanText(TableCell('validity_date')), dayfirst=True, default=NotAvailable) + + # The creation date doesn't display the year. + def obj_date(self): + validity_date = Field('validity_date')(self) + match = re.match(r'(?P\d{2})/(?P\d{2}) .*', CleanText(TableCell('date'))(self)) + if match: + day = int(match.group('day')) + month = int(match.group('month')) + # If we have a validity date we can guess the creation year. + if validity_date: + if validity_date.month > month or validity_date.day >= day: + year = validity_date.year + else: + year = validity_date.year - 1 + return date(year, month, day) + # If we don't have a validity date we use other orders to guess the year. + date_guesser = Env('date_guesser')(self) + return date_guesser.guess_date(day, month) + + obj_state = CleanText(TableCell('state')) + obj_code = Base( + TableCell('label'), + IsinCode(Regexp(Link('.//a'), r'isin=([^&]+)&'), default=NotAvailable) + ) + + obj__market_order_link = Base(TableCell('direction'), Link('.//a', default=NotAvailable)) + + +class PorMarketOrderDetailsPage(LoggedPage, HTMLPage): + @method + class fill_market_order(ItemElement): + obj_stock_market = Regexp( + CleanText('//td[contains(@id, "esdtdAchat")]/text()[contains(., "Sur")]'), + r'Sur (.*)', + default=NotAvailable + ) + obj_amount = CleanDecimal.French('//td[contains(@id, "esdtdMntEstimatif")]', default=NotAvailable) + + class MyRecipient(ItemElement): klass = Recipient