diff --git a/modules/cmso/module.py b/modules/cmso/module.py index 7bdf9299f89e30ab8456e0b4db3926cffb040350..ca1bcd4ef88ce56d425fd5ad6986c42d1f58e1bf 100644 --- a/modules/cmso/module.py +++ b/modules/cmso/module.py @@ -93,6 +93,11 @@ def iter_coming(self, account): def iter_investment(self, account): return self.browser.iter_investment(account) + def iter_market_orders(self, account): + if self.config['website'].get() != "par": + raise NotImplementedError() + return self.browser.iter_market_orders(account) + def iter_transfer_recipients(self, origin_account): if self.config['website'].get() != "par": raise NotImplementedError() diff --git a/modules/cmso/par/browser.py b/modules/cmso/par/browser.py index 9e6eba230f6419fee5306f4a0876306c49353fbc..3a2f1195a85d9f0cca7d683dd526827b500fe2b2 100644 --- a/modules/cmso/par/browser.py +++ b/modules/cmso/par/browser.py @@ -112,7 +112,7 @@ class CmsoParBrowser(TwoFactorBrowser): lifeinsurance = URL(r'https://domiweb.suravenir.fr', LifeinsurancePage) market = URL( r'/domiapi/oauth/json/ssoDomifronttitre', - r'https://www.(?P.*)/domifronttitre/front/sso/domiweb/01/(?P.*)Portefeuille\?csrf=', + r'https://www.(?P.*)/domifronttitre/front/sso/domiweb/01/(?P.*)\?csrf=', r'https://www.*/domiweb/prive/particulier', MarketPage ) @@ -334,7 +334,7 @@ def _go_market_history(self): content = self.market.go(json={'place': 'SITUATION_PORTEFEUILLE'}).text self.location(json.loads(content)['urlSSO']) - return self.market.go(website=self.website, action='historique') + return self.market.go(website=self.website, action='historiquePortefeuille') @retry((ClientError, ServerError)) @need_login @@ -442,12 +442,35 @@ def iter_investment(self, account): data = {"place": "SITUATION_PORTEFEUILLE"} response = self.market.go(json=data) self.location(json.loads(response.text)['urlSSO']) - self.market.go(website=self.website, action="situation") + self.market.go(website=self.website, action="situationPortefeuille") if self.page.go_account(account.label, account._owner): return self.page.iter_investment() return [] raise NotImplementedError() + @retry((ClientError, ServerError)) + @need_login + def iter_market_orders(self, account): + if account.type not in (Account.TYPE_MARKET, Account.TYPE_PEA): + return + + data = {"place": "SITUATION_PORTEFEUILLE"} + response = self.market.go(json=data) + self.location(json.loads(response.text)['urlSSO']) + self.market.go(website=self.website, action="carnetOrdre") + if self.page.go_account(account.label, account._owner): + orders_list_url = self.url + error_message = self.page.get_error_message() + if error_message: + if 'AUCUN ORDRE' in error_message: + return + raise AssertionError('Unexpected error while fetching market orders') + for order in self.page.iter_market_orders(): + self.page.go_order_detail(order) + self.page.fill_market_order(obj=order) + self.location(orders_list_url) + yield order + def iter_internal_recipients(self, account): self.int_recipients_list.go() all_int_recipients = list(self.page.iter_int_recipients()) diff --git a/modules/cmso/par/pages.py b/modules/cmso/par/pages.py index 33048638ff763614a558e4803fb53dc77d278fd4..150123e9bae7b9f670733f549cd24357a6f383b4 100644 --- a/modules/cmso/par/pages.py +++ b/modules/cmso/par/pages.py @@ -34,13 +34,13 @@ from weboob.browser.elements import DictElement, ItemElement, TableElement, SkipItem, method from weboob.browser.filters.standard import ( CleanText, Upper, Date, Regexp, Format, CleanDecimal, Filter, Env, Slugify, - Field, Currency, + Field, Currency, Map, Base, MapIn, ) from weboob.browser.filters.json import Dict from weboob.browser.filters.html import Attr, Link, TableCell, AbsoluteLink from weboob.browser.exceptions import ServerError from weboob.capabilities.bank import Account, Loan, AccountOwnership -from weboob.capabilities.wealth import Investment +from weboob.capabilities.wealth import Investment, MarketOrder, MarketOrderDirection, MarketOrderType from weboob.capabilities.contact import Advisor from weboob.capabilities.base import NotAvailable from weboob.capabilities.profile import Profile @@ -564,21 +564,42 @@ def obj_diff_ratio(self): return NotAvailable +MARKET_ORDER_DIRECTIONS = { + 'A': MarketOrderDirection.BUY, + 'S': MarketOrderDirection.BUY, + 'V': MarketOrderDirection.SALE, +} + +MARKET_ORDER_TYPES = { + 'MARCHE': MarketOrderType.MARKET, + 'LIMITE': MarketOrderType.LIMIT, + 'DECLENCH': MarketOrderType.TRIGGER, +} + + class MarketPage(LoggedPage, HTMLPage): def find_account(self, acclabel, accowner): + # Depending on what we're fetching (history, invests or orders), + # the parameter to choose the account has a different name. + if 'carnetOrdre' in self.url: + param_name = 'idCompte' + else: + param_name = 'indiceCompte' # first name and last name may not be ordered the same way on market site... accowner = sorted(accowner.lower().split()) - def get_ids(ref, acclabel, accowner): + def get_ids(ref, acclabel, accowner, param_name): ids = None - for a in self.doc.xpath('//a[contains(@%s, "indiceCompte")]' % ref): + for a in self.doc.xpath('//a[contains(@%s, "%s")]' % (ref, param_name)): self.logger.debug("get investment from %s" % ref) label = CleanText('.')(a) owner = CleanText('./ancestor::tr/preceding-sibling::tr[@class="LnMnTiers"][1]')(a) owner = re.sub(r' \(.+', '', owner) owner = sorted(owner.lower().split()) if label == acclabel and owner == accowner: - ids = list(re.search(r'indiceCompte[^\d]+(\d+).*idRacine[^\d]+(\d+)', Attr('.', ref)(a)).groups()) + ids = list( + re.search(r'%s[^\d]+(\d+).*idRacine[^\d]+(\d+)' % param_name, Attr('.', ref)(a)).groups() + ) ids.append(CleanText('./ancestor::td/preceding-sibling::td')(a)) self.logger.debug("assign value to ids: {}".format(ids)) return ids @@ -587,11 +608,11 @@ def get_ids(ref, acclabel, accowner): if CleanText(default=None).filter(self.doc.xpath('//body/p[contains(text(), "indisponible pour le moment")]')): return False - ref = CleanText(self.doc.xpath('//a[contains(@href, "indiceCompte")]'))(self) + ref = CleanText(self.doc.xpath('//a[contains(@href, "%s")]' % param_name))(self) if not ref: - return get_ids('onclick', acclabel, accowner) + return get_ids('onclick', acclabel, accowner, param_name) else: - return get_ids('href', acclabel, accowner) + return get_ids('href', acclabel, accowner, param_name) def get_account_id(self, acclabel, owner): account = self.find_account(acclabel, owner) @@ -599,12 +620,17 @@ def get_account_id(self, acclabel, owner): return account[2].replace(' ', '') def go_account(self, acclabel, owner): + if 'carnetOrdre' in self.url: + param_name = 'idCompte' + else: + param_name = 'indiceCompte' + ids = self.find_account(acclabel, owner) if not ids: return form = self.get_form(name="formCompte") - form['indiceCompte'] = ids[0] + form[param_name] = ids[0] form['idRacine'] = ids[1] try: return form.submit() @@ -681,6 +707,74 @@ def obj_code(self): obj_code_type = IsinType(CleanText(TableCell('code')), default=NotAvailable) + def get_error_message(self): + return CleanText('//div[has-class("titError") or has-class("TitError")]')(self.doc) + + @method + class iter_market_orders(TableElement): + item_xpath = '//table[has-class("domifrontTb")]/tr[not(has-class("LnTit") or has-class("LnTot"))]' + head_xpath = '//table[has-class("domifrontTb")]/tr[1]/td' + + col_label = 'Valeur' + col_direction = 'Sens' + col_state = 'Status' + col_quantity = 'Qté' + col_validity_date = 'Validité' + + class item(ItemElement): + klass = MarketOrder + + obj_label = Regexp(CleanText(TableCell('label')), r'([^\(]*) \(') + obj_code = IsinCode(Regexp(CleanText(TableCell('label')), r'\(\w+\)'), default=NotAvailable) + obj_direction = Map( + CleanText(TableCell('direction')), + MARKET_ORDER_DIRECTIONS, + MarketOrderDirection.UNKNOWN + ) + obj_state = CleanText(TableCell('state')) + obj_quantity = CleanDecimal.French(TableCell('quantity'), sign='+') + obj_validity_date = Date(CleanText(TableCell('validity_date')), dayfirst=True, default=NotAvailable) + + obj__index = Base(TableCell('label'), Regexp(Attr('.//a', 'onclick'), r'indiceOrdre, ([^,]*),')) + obj__type = Base(TableCell('label'), Regexp(Attr('.//a', 'onclick'), r"typeOrdre, '([^']*)'")) + + def go_order_detail(self, order): + form = self.get_form(name="parametres") + form.url = '2%s' % Regexp(pattern=r'\/\d([^\/]+)$').filter(self.url) + form['indiceOrdre'] = order._index + form['typeOrdre'] = order._type + form.submit() + + @method + class fill_market_order(ItemElement): + obj_order_type = MapIn( + CleanText('//tr/td[@class="CelTitCol" and contains(text(), "Mention")]/following-sibling::td[1]'), + MARKET_ORDER_TYPES, + MarketOrderType.UNKNOWN + ) + obj_ordervalue = CleanDecimal.French( + '//tr/td[@class="CelTitCol" and contains(text(), "Cours limite")]/following-sibling::td[1]', + default=NotAvailable + ) + obj_date = Date( + Regexp( + CleanText( + '//tr/td[@class="CelTitCol" and contains(text(), "enregistrement")]/following-sibling::td[1]' + ), + r'(.*) à' + ), + dayfirst=True + ) + obj_execution_date = Date( + Regexp( + CleanText('//tr/td[@class="CelTitCol" and contains(text(), "exécution")]/following-sibling::td[1]'), + r'(.*) à', + default=NotAvailable + ), + dayfirst=True, + default=NotAvailable + ) + class AdvisorPage(LoggedPage, JsonPage): @method