[Dev] Comment je paie mes salaires avec Budgea Pay

Lorsque nous avons développé la fonctionnalité Budgea Pay chez Budget Insight fin 2016, la chose la plus tentante fut de la tester sur nous même, en automatisant le virement des salaires à la fin du mois. Cela s’est matérialisé par l’écriture d’un script qui utilise nos API pour OCRizer les fiches de paie, extraire les informations utiles au paiement, trouver les bénéficiaire bancaires correspondants aux salariés, créer ceux qui sont manquants, et réaliser les virements.

Nous allons voir le détail de chacune de ces étapes, sur la manière d’utiliser l’API.

Il est à noter que le script est disponible sous license AGPLv3 :

Pré-requis

Je pars du principe que vous connaissez les concepts de base de l’API, que les comptes bancaires ont déjà été synchronisés, et que vous disposez d’un accès-token valide. Les exemples de code sont en Python et sont davantage illustratifs que fonctionnels, dans le sens où je simplifie pour que ce soit compréhensible.

Vous pouvez lire le code du projet budgea-salary pour voir une véritable implémentation.

OCR

La première étape est d’extraire de chaque fiche de paie les informations suivantes :

  • Nom
  • IBAN
  • Salaire net à payer

Le service d’OCR permet d’envoyer un PDF non textuel pour en obtenir le texte de manière non structuré :

>>> requests.post('/ocr', files={'file': (filename, open(filename, 'rb'), 'application/pdf')}).json()
{
    'data': "BUDGET INSIGHT\n84 rue Beaubourg\n75003 PARIS\nSiret : 74986720600031 Code Naf : 5829C\nUrssaf/Msa : 117000001XXXXXXXXX\nBUDGET INS##BULLETIN##09-2018##00006##SMITH##John##23478929389283\nBUDGET INS\nBULLETIN DE SALAIRE\nPériode : Septembre 2018\nMatricule :\nN° SS :\nIban / Rib :\nEmploi :\nStatut professionnel :\nPosition :\nCoefficient :\n00006\n989476728392098\nFR76 8293 7446 9203 1293 7384 282\nINGENIEUR DEVELOPPEMENT\nCadre\n2.1\n115\nEntrée :\nAncienneté :\n15/02/2016\n2 ans et 7 mois\nConvention collective :\nBureaux d'études techniques\nMonsieur John SMITH\n28 rue de Paris\n75001 PARIS\nEléments de paie\nBase\nTaux\nA déduire\nA payer\nCharges patronales\nSalaire de base\nHeures mensuelles majorées 25%\nSous total Salaire de base\nHeures supplémentaires 50%\nPrime d'astreinte semaine\nPrime d'astreinte Week-End\nMajoration dimanche\nMajoration nuit\nSalaire brut\nSanté\nComplémentaire - Incap. Inval. Décès\nComplémentaire - Incap. Inval. Décès\nComplémentaire - Santé\nAccidents du travail & mal. professionnelles\nRetraite\nSécurité Sociale plafonnée\nSécurité Sociale déplafonnée\nComplémentaire Tranche A\nComplémentaire Tranche B\nFamille\nAssurance chômage\nChômage\nAPEC\nCot. statutaires ou prévues par la conv. coll.\nContribution ADESATT\nAutres contributions dues par l'employeur\nCSG déduct. de l'impôt sur le revenu\nCSG déduct. de l'impôt sur le revenu\nCSG/CRDS non déduct. de l'impôt sur le revenu\nCSG/CRDS non déduct. de l'impôt sur le revenu\nExonérations de cotisations employeur\n151.67\n17.33\n169.00\n22.1154\n27.6443\n3.96\n5.00\n1.00\n3.83\n0.13\n34.0466\n52.0000\n102.0000\n22.6824\n11.0577\n3 354.25\n479.07\n3 833.32\n134.82\n260.00\n102.00\n86.87\n1.44\n4 418.45\n1 107.45\n3 311.00\n0.5650\n1.2100\n3 311.00\n4 418.45\n3 311.00\n1 107.45\n6.9000\n0.4000\n4.0300\n8.8300\n4 418.45\n4 418.45\n0.9500\n0.0240\n6.26\n40.06\n228.46\n17.67\n133.43\n97.79\n41.98\n1.06\n4 438.43\n6.8000\n301.81\n4 438.43\n2.9000\n128.71\n3 311.00\n1 107.45\n3 311.00\n4 418.45\n1.5400\n0.5650\n1.2100\n1.0000\n50.98\n6.26\n40.06\n44.18\n3 311.00\n1 107.45\n6.0700\n14.2700\n200.97\n158.04\n4 418.45\n4 418.45\n4.2000\n0.0360\n185.58\n1.59\n4 418.45\n0.0200\n0.88\n72.74\n21.29\n- 1.5000\n- 31.94\nTotal des cotisations et contributions\n997.23\n729.34\nRéintégration fiscale\nNet payé\n40.06\n3 421.22\nHeures\nHeures suppl.\nBrut\nBase S.S.\nPlafond S.S. Net imposable Ch. patronales\nTotal versé\nAllègements\nMensuel\nAnnuel\n172.96\n1 525.53\nCongés N-1\n21.29\n160.50\n4 418.45\n38 347.84\n3 311.00\n29 799.00\n3 311.00\n29 799.00\n3 589.99\n31 142.28\n729.34\n6 269.55\n5 147.79\n44 692.59\n111.47\n928.03\nCongés N\nAcquis\nPris\nSolde\nDans votre intérêt, et pour vous aider à faire valoir vos droits, conservez ce bulletin de paie sans limitation de durée. Informations complémentaires : www.service-public.fr\nNet payé : 3 421.22 
euros\nPaiement le 30/09/2018 par Virement\n10.00\n10.00\n30.00\n12.00\n18.00\nB LLETT D PAIE\n\nPaie du 01/XX/2015 au XX/XX/ZÛ15\n\nSIMPLIFICATION BDP\n\nCONVENTION COLLECTIVE DEYŸ‘WMW\n\nFeuillet 111\n\nMatricule\n\nEmploi\n\nNiveau\n\nLieu de versement des cotisations Sécurité Sociale\n\nGL0123458789\n\nSimplificateul\n\n3A\n\nTOULOUSE RUE PIERRE ET MARIE CURlE\n\numêlo Sécurité SocialeHoraire mensuel rémunéré Horaire hebdamadaire elleolil Siret\n\nNuméro URSSAF\n\nCode APE\n\n17501771771?7 77\n\neules\n\n1XXZXX xxxxx\n\nV100503P\n\nZ\n\nImpact des absences sur la paie de .…\n\n\' iodes de congés annuels avec maintien de Iêmunêlatinn\n\n01I0102l0117l01\n\n30003XXXXX\n\nGL01 23456189\n\nCongés payés\n\nRepos complémentaire\n\nSolde pêlinde en l:uurs\n\n\' zs iour Pris dans l\'année\n\n1\n\niour\n\nr\n\nr\n\nDont fractionnement\n\n0\n\niour Fils dans le mais\n\nl\n\niour\n\nr\n\n15\n\nM. LEBON Juste\n\nSolde potentiel\n\niour\n\nMessages\n\n5 avenue de la République\n\nCe bulletin est un (est alllchanl toutes les lubllques patronales pour un salarl
ê au SMIC\n\n94000 CRETEIL\n\nELEMENTS DE PAIE E\n\nEté-ent\n\nRappel\n\na\n\nou base Va e\n\n:“.ÈP“°S\n\nMontant\n\nRete-ue\n\nVaut pat-onale\n\nSALAIRE DE BASE\n\n1 457.52\n\nCSG NON IMPOSABLE\n\n1 432.01\n\n5,100\n\n13.03\n\nCSG IMRO5ABLE\n\n1 432.01\n\n2,400\n\n34.37\n\nCROS\n\n1 432.01\n\n0,500\n\n7,18\n\nACCIDENT DU TRAVAIL\n\n145752\n\n15,03\n\n1 457,52\n\n155,55\n\nASSURANCE MALADIE\n\n0.750\n\n10,93\n\nASS. VIEILLESSE DEPLAFONNEE\n\n1 457.52\n\n0,300\n\n437\n\n25.24\n\nASSURANCE VIEILLESSE\n\n145752\n\n6 850\n\n83,84\n\n123.88\n\nALLOCATIONS FAMILIALES\n\n1 45752\n\n31450\n\n50,28\n\n50.28\n\nCONTRIBUTION JOUR SOLIDARITE\n\n1 45752\n\n4.37\n\nFNAL non plalonné\n\n1 457,52\n\n7,23\n\nRETRAITE ARRCO TAUX 1\n\n1 457,52\n\n3,100\n\n45.18\n\n61.7?\n\nAGFF TAUX1\n\n145752\n\n0800\n\n11,66\n\n17,45\n\nRETRAITE SUPPL. UALMY\n\n1 457.52\n\n0,500\n\n7,23\n\n2252\n\nPOLE EMPLOI\n\n1 457,52\n\n2.400\n\n34,38\n\n55,30\n\nPOLE EMPLOIAGS\n\n145752\n\n4.37\n\nCOMPTE PENIBILITE\n\n145752\n\n1,45\n\nGARANTIE INCAPACITE INVALIDITE\n\n145752\n\n0,208\n\n3,03\n\n4.55\n\nGARANTIE DECES\n\n1 457,52\n\n0,134\n\n1,95\n\n2.34\n\nMUTUELLE\n\n1 45752\n\n1,155\n\n18,83\n\n13,7?\n\nSubvention CE Activités Sociales et Culturelles\n\n1 457.52\n\n14.33\n\n1 45752\n\nSubvention CE Fonctionnement\n\n1,55\n\nSubvention CE autres activités\n\n1 45752\n\n1,75\n\nTaxe d\'apprentissage\n\n1 457,52\n\n3,81\n\nTaxe Formation Professionnelle\n\n145752\n\n11,88\n\n1 457,52\n\nParticipation à l‘Effort de construction\n\n6,58\n\nFongecil\n\n1 45752\n\n2.32\n\nVersement Transport\n\n145752\n\n38,35\n\nFallait Social SZ\n\n13,11\n\n1,10\n\nFallait social 202\n\n30,41\n\n6,08\n\nTaxe sur les Salaires salaire total\n\n1 45752\n\n51,34\n\nTaxe sur les salaires 1ère tranche maiorée\n\n1336 83\n\n27,07\n\nTaxe sur les salaires 2ème "anche maiorée\n\n151.05\n\n17.00\n\nREDUCTION GENERALE DE DE COTISATIONS\n\n145752\n\n41321\n\nREMBOURSEMENT TRANSPORT PARIS\n\n53,40\n\nTITRES RESTAURANT\n\n20,00\n\n3,25\n\n65.00\n\n57.00\n\n7\n\n1 510.32 \'\n\n485,92 \'\n\n455.72\n\nDate de paiement\n\nType de versement\n\nRéférences bancaires\n\nNet payé
 en euros\n\ne\n\nPalemenl par compte\n\n& 854.75\n\n1 045,00\n\nMDNTANTS CUMULES DEPUIS LE DEBUT DE L\'ANNEE\n\nSécurité & plaiunnt‘\n\nSécurité 5. de lalonnêe\n\nAss dll:\n\nReturn taux |\n\nBases\n\n.\n\n45,2\n\n145752\n\n145752\n\nPlalonds\n\n3 170,00\n\n12 580,00\n\n3 170,00\n\namants : declarer Année 2 15\n\nBru! fiscal\n\nNet fiscal\n\nrr. en nal, lugerr Arr. en nat. Nnurrit. e lndtês et rembts\n\nEuros\n\n1 45752\n\n2.18\n\nFrancs\n\n3 580.70\n\n7 823,47\n\n350,28\n\n",
    'id_ocr': 5809
}

Quelques regexp permettent de récupérer les informations utiles :

>>> re.findall('Net payé : ([\d\s\.,]+) euros', data)
['3 421.22']
>>> re.findall('(FR\w\w \w\w\w\w \w\w\w\w \w\w\w\w \w\w\w\w \w\w\w\w \w\w\w)', data)
['FR76 8293 7446 9203 1293 7384 282']
>>> re.findall('(Mademoiselle|Madame|Monsieur) ([^\)\r\n]+)', data)
[('Monsieur', 'John SMITH')]

Liste de bénéficiaires

Pour initier le paiement de 3 421.22 € à John SMITH, nous allons lister les bénéficiaires de notre compte bancaire pour voir s’il est présent :

>>> requests.get('/users/me/accounts/%s/recipients' % account_id).json()
{
    'total': 1,
    'recipients': [
        {
            'category': 'Salariés',
            'bank_name': 'LCL (LE CREDIT LYONN',
            'time_scraped': '2016-12-04 18:24:35',
            'deleted': None,
            'id_account': 1199,
            'id_target_account': None,
            'bic': 'CRLYFRPPXXX', 
            'label': 'ARTHUR RIMBAUD', 
            'currency': {'symbol': '€', 'prefix': False, 'id': 'EUR'}, 
            'expire': None,
            'iban': 'FR7689475678902938470002238',
            'webid': 'FR7689475678902938470002238', 
            'last_update': '2018-09-11 17:52:16', 
            'add_verified': None, 
            'id': 433, 
            'enabled_at': '2016-12-04 18:24:32'
        }
    ]
}

Ajouter un bénéficiaire

John Smith n’étant pas dedans, nous pouvons le rajouter :

def add_recipient(account_id, name, iban):
    print('Adding recipient...', end=' ', flush=True)
    r = requests.post('/users/me/accounts/%s/recipients' % account_id,
                      data={'label': name,
                            'iban': iban,
                            'category': 'Salariés',
                           }).json()
    while True:
        if 'code' in r:
            print('%s %s' % (r['code'], r.get('message', r.get('description', ''))))
            return None
        recipient_id = r['id']
        if 'fields' in r:
            values = {}
            for field in r['fields']:
                print('%s: ' % field['label'], end=' ', flush=True)
                r = sys.stdin.readline().strip()
                values[field['name']] = r
            print('Adding recipient...', end=' ', flush=True)
            r = requests.post('/users/me/recipients/%s?all' % recipient_id,
                              data=values).json()
            continue
        break

    print('ok')
    return r

Cette fonction tirée de salary.py est un peu plus complexe que les exemples précédents.

Lors de l’ajout d’un bénéficiaire dans la liste d’exemption, il y a quasi systématiquement plusieurs étapes requises pour qu’elle soit effective, à savoir une ou plusieurs interactions avec l’utilisateur. Dans le cas où le retour contient un champ « fields », cela signifie qu’il est nécessaire d’afficher un ou plusieurs champs à l’utilisateur. L’interaction se présentera de la manière suivante :

>>> add_recipient(1199, 'John SMITH', 'FR7682937446920312937384282')
Adding recipient... Veuillez saisir le code de la case G8: 2345
Adding recipient... Veuillez saisir le code reçu par SMS: 12345678
Adding recipient... ok
{
    'category': 'Salariés',
    'bank_name': 'LCL (LE CREDIT LYONN',
    'time_scraped': '2018-10-01 13:11:52',
    'deleted': None,
    'id_account': 1199,
    'id_target_account': None,
    'bic': 'CRLYFRPPXXX', 
    'label': 'JOHN SMITH', 
    'currency': {'symbol': '€', 'prefix': False, 'id': 'EUR'}, 
    'expire': None,
    'iban': 'FR7682937446920312937384282',
    'webid': 'FR7682937446920312937384282',
    'last_update': '2018-09-01 13:08:27',
    'add_verified': None,
    'id': 2837,
    'enabled_at': '2018-10-01 13:11:52'
}

Exécuter un virement

Maintenant que le bénéficiaire est présent auprès de la banque, et compte tenu du fait qu’il est actif immédiatement (ce qui n’est pas le cas au sein de toutes les banques, certaines nécessitant l’attente d’un délai de plusieurs jours), il est maintenant possible d’exécuter le paiement :

def transfer(account_id, recipient_id, label, amount):
    print('Transfering %s € (%s)...' % (amount, label), end=' ', flush=True)
    r = requests.post('/users/me/accounts/%s/recipients/%s/transfers' 
                      % (account_id, recipient_id),
                      data={'amount':   amount,
                            'label':    label,
                          }).json()
    transfer_id = r['id']
    values = {}

    while True:
        r = requests.post('/users/me/transfers/%s' % transfer_id,
                          data={'validated': True} + values).json()
        if 'code' in r:
            print('%s %s' % (r['code'], r.get('message', r.get('description', ''))))
            return None
        if 'fields' in r:
            values = {}
            for field in r['fields']:
                print('%s: ' % field['label'], end=' ', flush=True)
                r = sys.stdin.readline().strip()
                values[field['name']] = r
            print('Transfering %s € (%s)...' (amount, label), end=' ', flush=True)
            continue
        break

    print('done! (%s)' % r['state'])
    return r

En premier lieu, nous créons le virement, associé à un compte d’origine, un destinataire, un libellé et un montant, puis nous faisons un POST dessus avec le flag validated à true. S’ensuit une gestion de la même cinématique qu’à l’ajout de bénéficiaire, en cas d’authentification forte. Puisque John est inscrit à la liste des bénéficiaires de confiances, et que nous utilisons un token sur l’API Budgea ayant le scope transfer, il n’y aura pas d’étapes supplémentaire :

>>> transfer(1199, 2837, 'Salaire de John', 3421.22)
Transfering 3421.22 € (Salaire de John)... done! (pending)
{
    'account_balance': 1074049.17,
    'account_iban': 'FR7610278060430001353892421',
    'amount': 3421.22,
    'currency': {'id': 'EUR', 'prefix': False, 'symbol': '€'},
    'error': None,
    'exec_date': '2018-10-01',
    'fees': None,
    'formatted_amount': '3\xa0421,22\xa0€',
    'id': 866,
    'id_account': 1199,
    'id_recipient': 2837,
    'id_transaction': None,
    'label': 'Salaire de John',
    'recipient_iban': 'FR7682937446920312937384282',
    'register_date': '2018-10-01 16:09:12',
    'state': 'pending',
    'webid': 'vR/IuRV8iUipnVGdowhOQw==MTAuNDYuNC4xMzM6ODAwMw=='
}

Conclusion

Dans cet article, nous avons vu :

  • Comment interagir avec l’OCR ;
  • Quelques regexps pour extraire des informations utiles ;
  • Lister les bénéficiaires existants d’un compte bancaire ;
  • Ajouter un bénéficiaire ;
  • Réaliser un virement vers un bénéficiaire ;
  • De jolis exemples de code Python pour illustrer le tout.

Il s’agit d’un cas d’utilisation de notre API parmi de nombreux autres (règlement de factures, initiation de paiement marchand, rechargement de wallet comme on le fait avec Lydia, virements peer-to-peer, etc.), et ils s’étendront encore davantage lorsque l’Instant Payment et les API de la PSD2 seront disponibles !