diff --git a/modules/ccf/browser.py b/modules/ccf/browser.py
index 4d4b39b3d5bd7b0b726e83af2f45a2cd57355c58..0965d47492a2e8ce8ec5c69de364ae46014fa0d4 100644
--- a/modules/ccf/browser.py
+++ b/modules/ccf/browser.py
@@ -15,10 +15,15 @@
# You should have received a copy of the GNU Lesser General Public License
# along with this woob module. If not, see .
+from datetime import date
+from dateutil.relativedelta import relativedelta
+
import random
from base64 import b64encode
from hashlib import sha256
+from schwifty import IBAN
+
from woob.browser import URL, need_login
from woob.browser.browsers import ClientError, ServerError
from woob_modules.cmso.par.browser import CmsoParBrowser
@@ -27,7 +32,7 @@
from woob.tools.decorators import retry
-from .pages import SubscriptionsPage, DocumentsPage, RibPage, TransactionsPage
+from .pages import SubscriptionsPage, DocumentsPage, RibPage, TransactionsPage, AccountsPage
__all__ = ["CCFParBrowser", "CCFProBrowser"]
@@ -37,6 +42,23 @@ class CCFBrowser(CmsoParBrowser):
arkea_si = None
AUTH_CLIENT_ID = "S4dgkKwTA7FQzWxGRHPXe6xNvihEATOY"
+ # Use CmsoParBrowser as base, but rely on /distri-account-api/api
+ # for accounts list & balance. Like modules/allianzbanque/browser.py
+ # We should probably extract a common browser.
+
+
+ # accounts_: note the trailing underscore
+ # don't override super.accounts, used indirectly by get_ibans_from_ribs
+ accounts_ = URL(
+ r"/distri-account-api/api/v1/persons/me/accounts", AccountsPage
+ )
+ balance = URL(
+ r"/distri-account-api/api/v1/customers/me/accounts/(?P.*)/balances", AccountsPage
+ )
+ balances_comings = URL(
+ r'/distri-account-api/api/v1/persons/me/accounts/(?P[A-Z0-9]{10})/total-upcoming-transactions',
+ AccountsPage
+ )
subscriptions = URL(
r"/distri-account-api/api/v1/customers/me/accounts", SubscriptionsPage
)
@@ -104,17 +126,12 @@ def build_request(self, *args, **kwargs):
@need_login
def get_subscription_list(self):
- accounts_list = self.iter_accounts()
- subscriptions = []
- for account in accounts_list:
- s = Subscription()
- s.label = account._lib
- if account.number:
- s.label = f"{s.label} {account.number}"
- s.subscriber = account._owner_name
- s.id = account.id
- subscriptions.append(s)
- return subscriptions
+ params = {
+ 'types': 'CHECKING',
+ 'roles': 'TIT,COT',
+ }
+ self.subscriptions.go(params=params)
+ return self.page.iter_subscriptions()
@need_login
def iter_documents(self, subscription):
@@ -126,18 +143,54 @@ def download_document(self, document):
params = {"flattenDoc": False}
return self.open(document.url, params=params).content
- def update_iban(self, account):
- self.rib_details.go(json={"numeroContratSouscritCrypte": account._index})
- iban_number = self.page.get_iban()
- if not account.iban:
- account.iban = iban_number
+ def _update_iban(self, account, ibans):
+ iban_obj = None
+ if account.id in ibans:
+ account.iban = ibans.get(account.id)
+ iban_obj = IBAN(account.iban) if account.iban else None
+ else:
+ _iban = account._iban_offended
+ _contract = account._contract_id
+
+ if _iban:
+ iban_obj = IBAN.generate(
+ country_code=_iban[:2],
+ bank_code=_iban[4:9],
+ branch_code=_iban[9:14],
+ account_code=_contract[0]+_contract[4:14])
+
+ if iban_obj:
+ account.iban = iban_obj.formatted
+ account.number = iban_obj.account_code
+ account.bank_name = iban_obj.bank_name
+ if not account.bank_name:
+ account.bank_name = 'CCF'
+
+ def _get_ibans_from_ribs(self):
+ accounts_list = super().iter_accounts()
+ for account in accounts_list:
+ self.rib_details.go(json={"numeroContratSouscritCrypte": account._index})
+ iban_number = self.page.get_iban()
+ if not account.iban:
+ account.iban = iban_number
+ return { account.id: account.iban for account in accounts_list }
+ @need_login
def iter_accounts(self):
- accounts_list = super().iter_accounts()
+ ibans = self._get_ibans_from_ribs()
+
+ go_accounts = retry(ClientError, tries=5)(self.accounts_.go)
+ go_accounts(params={'types': 'CHECKING,SAVING'})
+
+ accounts_list = list(self.page.iter_accounts())
for account in accounts_list:
- account._original_id = account.id
- self.update_iban(account)
- return accounts_list
+ self.balance.go(account_id=account.id)
+ self.page.fill_balance(account)
+ date_to = (date.today() + relativedelta(months=1)).strftime('%Y-%m-%dT00:00:00.000Z')
+ self.balances_comings.go(account_id=account.id, params={'dateTo': date_to})
+ self.page.fill_coming(account)
+ self._update_iban(account, ibans)
+ yield account
@need_login
def iter_history(self, account):
diff --git a/modules/ccf/pages.py b/modules/ccf/pages.py
index 2c90ffbbf6abc68c6e27b301d72b82ec60d5e772..1e618c223795016f2a1cbef66d0a75e67b32e5de 100644
--- a/modules/ccf/pages.py
+++ b/modules/ccf/pages.py
@@ -28,18 +28,48 @@
Eval,
Field,
Format,
+ Lower,
Map,
+ MapIn,
Regexp,
+ Upper,
)
from woob.browser.pages import JsonPage, LoggedPage
+from woob.capabilities.bank import Account, AccountType, AccountOwnerType, AccountOwnership
from woob.capabilities.bill import Document, DocumentTypes, Subscription
from woob.tools.capabilities.bank.transactions import FrenchTransaction
+from woob_modules.axabanque.pages.bank import AccountsPage as _AccountsPage, ACCOUNT_TYPES as ACCOUNT_TYPES_GENERIC
class RibPage(LoggedPage, JsonPage):
def get_iban(self):
- return Dict("iban")(self.doc)
+ return Dict("iban", default=None)(self.doc)
+
+
+ACCOUNT_TYPES_OVERRIDE = {
+ "checking": AccountType.CHECKING,
+ "stock": AccountType.PEA, # 'compte titres' and 'plan d'epargne en actions' have both the same type with the type field
+ "hav": AccountType.LIFE_INSURANCE,
+}
+
+ACCOUNT_TYPES = {**ACCOUNT_TYPES_GENERIC, **ACCOUNT_TYPES_OVERRIDE}
+
+class AccountsPage(_AccountsPage):
+ @method
+ class iter_accounts(_AccountsPage.iter_accounts.klass):
+ class item(_AccountsPage.iter_accounts.klass.item):
+ obj_type = Coalesce(
+ MapIn(Lower(Dict('type')), ACCOUNT_TYPES, Account.TYPE_UNKNOWN),
+ MapIn(Lower(Dict('label')), ACCOUNT_TYPES, Account.TYPE_UNKNOWN),
+ default=Account.TYPE_UNKNOWN,
+ )
+ obj__iban_encrypted = Dict("iban")
+ obj__iban_offended = Dict("offendedIBAN")
+ obj__contract_id = Dict("sourceContractId")
+
+ def validate(self, obj):
+ return True
class SubscriptionsPage(LoggedPage, JsonPage):
@@ -48,16 +78,29 @@ class iter_subscriptions(DictElement):
class item(ItemElement):
klass = Subscription
- obj_id = Dict("sourceContractId")
+ obj_id = Dict("accountId")
+ obj_label = Dict("label")
# there can be several "participants" but no matter what _contract_id is,
# list of related documents will be the same, so we can simply take the first one
obj__contract_id = Dict("participants/0/id") # CAUTION non persistant
- obj_subscriber = Format(
- "%s %s",
- CleanText(Dict("participants/0/firstName")),
- CleanText(Dict("participants/0/lastName")),
- )
+ def obj_subscriber(self):
+ def key_participants(participant):
+ role = participant.get("role", None)
+ if role == "TIT":
+ role_idx = 0
+ else:
+ role_idx = 1
+ return '%s-%s-%s' % (role_idx, participant.get("lastName"), participant.get("firstName"))
+
+ result = ""
+ for participant in sorted(Dict("participants")(self), key=key_participants):
+ result += Format(
+ "%s %s / ",
+ CleanText(Dict("lastName")),
+ CleanText(Dict("firstName")),
+ )(participant)
+ return result.strip("/ ")
DOCUMENT_TYPES = {
diff --git a/modules/ccf/requirements.txt b/modules/ccf/requirements.txt
index 1ef9ec69aeef940f517291b33ae648a50354b369..2ab9712ca5ead082a41c1f2f25b4cf60ccec0a7b 100644
--- a/modules/ccf/requirements.txt
+++ b/modules/ccf/requirements.txt
@@ -1 +1,2 @@
woob ~= 3.6
+schwifty