Relance automatique pour factures impayées / en retard

Hello, je ne vois pas de ressources à ce propos ici.

Nous avons pas mal d’abonnements automatisés avec demandes de paiement et passerelle configurée. Nous souhaiterions pouvoir programmer également 2 rangs de relances automatiques si jamais le contrib. est en retard.

Quelle serait une bonne manière de faire ? Passer par le système de notification ? Je vois qu’il semble y avoir un document type “relance” peut-être est-ce par là ?

Merci de votre aide :slight_smile:

Salut @maximeIndieHosters,

Le meilleur moyen, à mon avis, est effectivement de passer par les notifications.
Le type de document “Relance” peut aussi être utilisé, mais peut être un peu trop poussé s’il n’y a pas de frais ou d’intérêts associé à la relance (Dunning)

Bonne journée

Entendu, merci @chdecultot :slight_smile:

Encore quelques petites questions pour valider tout ça :slight_smile:

Je pensais partir sur la stratégie suivante :

Factures par virement manuel

  • notif basé sur le document “facture de vente”, dont le statut est “impayé” ou “en retard” avec déclenchement X jours après date d’émission (ou date d’échéance) et mail incluant en pièce jointe la facture.
    → Pourrais-tu me confirmer qu’en condition pour le statut il faudrait mettre doc.status == "Unpaid" or doc.status == "Unpaid and Discounted" or doc.status == "Overdue and Discounted" or doc.status == "Overdue"

→ Mais comment conditionner pour que ça ne cible que les factures qui n’ont pas de demandes de paiement lié ? (J’ai cherché avec les variables “Paiement inclus” ou “Mode de paiement” mais à chaque fois ces variables ne semblent pas définies dans tous les cas)

Facture avec demandes de paiement

  • notif sur type de document “demande de paiement” (pour récupérer {{ payment_link }} et l’insérer dans l’email) X jours après la date de création de la demande et filtrer sur celles qui ne sont pas encore payées doc.status == "Initiated" or doc.status == "Pending"

→ Mais peut-être qu’il est inutile d’inclure les “initiated” ? À quel moment une demande passe en “pending” ?

Tester avant ?

De manière générale, j’aimerais éviter d’envoyer une bêtise et tester bien que tout est en place avant de confirmer la notif, notamment voir concrètement à quoi ressemble l’email lorsqu’il arrive en boite de réception. Comment t’y prends-tu pour cela ?

Merci !! :hugs:

Salut @maximeIndieHosters,

Pour tes factures par virement manuel, tu peux utiliser la condition suivante:

doc.outstanding_amount > 0.0 and not frappe.db.exists("Payment Request", {"reference_doctype": "Sales Invoice", "reference_name": doc.name, "docstatus": 1})

La première partie permet d’éviter de gérer les statuts, puisque je suppose que tu veux envoyer une relance quand tout ou partie de la facture est impayée.
La seconde partie vérifie s’il existe une demande de paiement (non annulée) associée à cette facture.

Pour les factures avec demandes de paiement, tu peux effectivement n’inclure que les demandes en statut “Initiated”.
Les demandes au statut “Pending” dépendent de la passerelle de paiement:

  • Pour Stripe, quand quelqu’un clique sur le bouton “Payer” depuis le lien de paiement, ça déclenche une intention de payer (Payment Intent: Die Payment Intents API | Stripe-Dokumentation).
    La demande de paiement reste “En Attente” tant que la personnne n’a pas complété son paiement, car l’intention de payer reste ouverte.

  • Pour GoCardless, ça passe “En Attente” quand un client a déjà un mandat valide et qu’un paiement est créé automatiquement.
    Normalement si le paiement échoue, le statut passe à Echoué, sinon à Terminé.

Pour tester ta notification, tu peux soit cliquer sur bouton “Tester” et t’envoyer un mail de test. Ca ne teste pas encore la condition, mais ça permet de vérifier l’email.
Sinon tu mets un rôle que tu as dans le champ “Destinataire par rôle” au lieu de mettre un destinataire par champ de document.
Comme ça c’est toi qui recevra les premières notifications. Quand ça te convient, tu change le/les destinataires.

Bonne journée !

2 Likes

Merci @chdecultot c’est très clair ! Je teste tout ça de ce pas :slight_smile:

Re,

Pour les factures avec paiement manuel, c’est bon :slight_smile: Merci !

Pour les factures avec paiement auto c’est plus compliqué. J’en suis là:

<p>
    Bonjour, 
</p>
<br>
<p>Il semblerait que nous n'ayons pas reçu la contribution de XXX pour un montant de {{ doc.grand_total }}€. </p>
<br>
<p>Merci de vous en occuper dés que possible. ☺️ (détails dans facture en pj)
<br><br>
{% if not payment_can_be_processed_immediately %}
👉 <a href="{{ doc.payment_url }}">Procéder au paiement sécurisé en ligne maintenant</a>
{% endif %}
</p>
<p>Si vous avez la moindre question ou autre, n'hésitez pas à nous contacter en répondant à cet email.</p>
<br>
<p>Tout le collectif IndieHosters vous remercie pour votre soutien.</p>

Mais l’url ne fonctionne pas, je me retrouve avec un lien vide.

Par ailleurs, comment récupérer le nom du customer (qui est dans la facture liée) ? Et comment attacher la facture en pièce jointe ?

À moins qu’il ne faille régler la notif sur le document “facture de vente” mais comment alors cibler celles qui sont liées à une demande de paiement et récupérer le lien de paiement ?

Salut Maxime,

Voici deux approches possibles pour ce que tu veux faire:

  1. En passant par un script python, tu peux renvoyer automatiquement le message initial de la demande de paiement. L’inconvénient dans ce cas est que ce n’est pas un message dédié à la relance, mais ça peut être suffisant pour certains.

Code:

for payment_request in frappe.get_all("Payment Request", filters={"docstatus": 1, "status": "Initiated"}):
    doc = frappe.get_doc("Payment Request", payment_request.name)
    communication = doc.make_communication_entry()
    doc.send_email(communication)

Script python:

  1. Via les notifications, si tu veux renvoyer le PDF de la facture, il faut effectivement que ton document de référence soit la facture.

La condition sera légèrement différente de celle pour les factures manuelles:
doc.outstanding_amount > 0.0 and frappe.db.exists("Payment Request", {"reference_doctype": "Sales Invoice", "reference_name": doc.name, "docstatus": 1, "status": ("in", ("Initiated", "Pending"))})

Et dans ton corps d’email, il faut récupérer la clé de paiement disponible dans la demande de paiement pour obtenir l’URL:

<p>
    Bonjour, 
</p>
<br>
<p>Il semblerait que nous n'ayons pas reçu la contribution de {{ doc.customer }} pour un montant de {{ frappe.utils.fmt_money(doc.grand_total, currency=doc.currency) }}. </p>
<br>
<p>Merci de vous en occuper dés que possible. ☺️ (détails dans facture en pj)
<br><br>
{% set payment_key = frappe.db.get_value("Payment Request", {"reference_doctype": "Sales Invoice", "reference_name": doc.name, "docstatus": 1, "status": ("in", ("Initiated", "Pending"))}, "payment_key") %}
👉 <a href="{{ frappe.utils.get_url("/payments?link={0}".format(payment_key)) }}">Procéder au paiement sécurisé en ligne maintenant</a>
</p>
<p>Si vous avez la moindre question ou autre, n'hésitez pas à nous contacter en répondant à cet email.</p>
<br>
<p>Tout le collectif IndieHosters vous remercie pour votre soutien.</p>

Bonne soirée!

1 Like

Top ! Je teste la version avec notif sous peu ! Merci je n’aurais pas trouvé tout seul :slight_smile:

En place ! Merci c’est top :slight_smile:

Pour référence pour celles et ceux qui voudraient aussi mettre en place des relances automatiques, voici une capture d’écran des deux cas :slight_smile:

Relance auto pour paiements par virement manuel

Relance auto pour paiements avec demandes de paiement passant par une passerelle strapi (CB)

Attention j’ai rajouté à la condition une vérification qu’il ne s’agit pas d’une facture en brouillon :

doc.status != "Draft" and doc.outstanding_amount > 0.0 and not frappe.db.exists("Payment Request", {"reference_doctype": "Sales Invoice", "reference_name": doc.name, "docstatus": 1})

car j’ai remarqué un envoi de relance à une personne qui avait bien en effet une facture en attente mais qui était en réalité encore en brouillon pour nous (car renouvellement pas validé entre nous).

Hello, je ne suis pas certain que les relances partent bien en fait. :thinking:

Comment fais-tu pour checker ?

Bonjour @maximeIndieHosters,

Tu peux vérifier les envois des emails depuis le document Journal des emails, tu peux voir tous les emails qui sont en cours d’envoi ou alors ceux qui ont été envoyés.

Merci @Nicolas_Tissot :slight_smile: Je vois beaucoup d’emails par là en effet mais difficile de s’y retrouver. En tt cas je n’ai pas l’impression d’y voir de notifs de relances auto envoyées par email curieusement. :thinking:

Il n’y aurait pas un filtre qui permettrait d’isoler les emails qui sont partis dans le cadre de telle ou telle notif par hasard ? Ce serait ainsi bien plus facile d’identifier si les emails partent effectivement ou pas pour une notif en particulier.

Hello @maximeIndieHosters,

Quand une notification est envoyée, l’email (la communication associée pour être plus précis) est automatiquement ajouté dans la chronologie du document de référence (ici ta facture relancée j’imagine).
C’est plus facile pour les retrouver.

Regarde si tu n’as pas d’erreur associée dans le journal d’erreurs, sinon il y a peut-être un problème avec les conditions.

Hello @chdecultot j’ai checké en effet dans l’historique des factures et je ne vois pas de relances envoyées.

Je suis retourné sur les notifs et lorsque je clique sur “obtenir les notifs pour aujourd’hui” j’obtiens en effet une erreur :

Traceback (most recent call last):
  File "/home/dokos/hetz1/apps/frappe/frappe/app.py", line 68, in application
    response = frappe.api.handle()
  File "/home/dokos/hetz1/apps/frappe/frappe/api.py", line 54, in handle
    return frappe.handler.handle()
  File "/home/dokos/hetz1/apps/frappe/frappe/handler.py", line 28, in handle
    data = execute_cmd(cmd)
  File "/home/dokos/hetz1/apps/frappe/frappe/handler.py", line 64, in execute_cmd
    return frappe.call(method, **frappe.form_dict)
  File "/home/dokos/hetz1/apps/frappe/frappe/__init__.py", line 1176, in call
    return fn(*args, **newargs)
  File "/home/dokos/hetz1/apps/frappe/frappe/email/doctype/notification/notification.py", line 403, in get_documents_for_today
    return [d.name for d in notification.get_documents_for_today()]
  File "/home/dokos/hetz1/apps/frappe/frappe/email/doctype/notification/notification.py", line 112, in get_documents_for_today
    if self.condition and not frappe.safe_eval(self.condition, None, get_context(doc)):
  File "/home/dokos/hetz1/apps/frappe/frappe/__init__.py", line 1723, in safe_eval
    return eval(code, eval_globals, eval_locals)
  File "", line 1, in 
AttributeError: 'NoneType' object has no attribute 'exists'

J’ai la même chose sur la plupart des notifs. Ça provient des conditions ?

Les conditions pour les paiements manuels sont de ce type:
doc.status != "Draft" and doc.outstanding_amount > 0.0 and not frappe.db.exists("Payment Request", {"reference_doctype": "Sales Invoice", "reference_name": doc.name, "docstatus": 1})

Les conditions pour les paiements auto sont de ce type:
doc.outstanding_amount > 0.0 and frappe.db.exists("Payment Request", {"reference_doctype": "Sales Invoice", "reference_name": doc.name, "docstatus": 1, "status": ("in", ("Initiated", "Pending"))})

À moins que ce ne soit lié au contenu des messages ?

Merci bien pour ton aide,

Bonjour @maximeIndieHosters,

Le problème vient effectivement de la condition.
La méthode frappe.db.exists n’est pas accessible dans les notifications (désolé, l’habitude de toujours passer par des scripts…)
Je vais autoriser la méthode frappe.db.get_value, qui permet d’avoir un résultat équivalent.
Ca passera ce soir en production.

Il faudra simplement que tu changes frappe.db.exists par frappe.db.get_value

Bonne fin de journée!

1 Like

Merci @chdecultot ! Je viens de faire le changement, donc si tout roule j’espère que tout fonctionnera dés demain :slight_smile:

1 Like

Hello, je viens de retester à l’instant et j’obtiens maintenant cette erreur :

Traceback (most recent call last):
  File "/home/dokos/hetz1/apps/frappe/frappe/app.py", line 68, in application
    response = frappe.api.handle()
  File "/home/dokos/hetz1/apps/frappe/frappe/api.py", line 54, in handle
    return frappe.handler.handle()
  File "/home/dokos/hetz1/apps/frappe/frappe/handler.py", line 28, in handle
    data = execute_cmd(cmd)
  File "/home/dokos/hetz1/apps/frappe/frappe/handler.py", line 64, in execute_cmd
    return frappe.call(method, **frappe.form_dict)
  File "/home/dokos/hetz1/apps/frappe/frappe/__init__.py", line 1176, in call
    return fn(*args, **newargs)
  File "/home/dokos/hetz1/apps/frappe/frappe/email/doctype/notification/notification.py", line 403, in get_documents_for_today
    return [d.name for d in notification.get_documents_for_today()]
  File "/home/dokos/hetz1/apps/frappe/frappe/email/doctype/notification/notification.py", line 112, in get_documents_for_today
    if self.condition and not frappe.safe_eval(self.condition, None, get_context(doc)):
  File "/home/dokos/hetz1/apps/frappe/frappe/__init__.py", line 1723, in safe_eval
    return eval(code, eval_globals, eval_locals)
  File "", line 1, in 
AttributeError: 'dict' object has no attribute 'get_value'

Ça n’a pas l’air de lui plaire non plus le get_value :confused:

Salut @maximeIndieHosters,

Désolé petite erreur de ma part, c’est corrigé depuis normalement.

Bonne journée !

Hello merci !

En effet je ne vois plus d’erreur s’afficher lorsque je clique sur “obtenir les notifs d’aujourd’hui”. Mais il ne se passe rien non plus, c’est peut-être normal. Mais en fait, qu’est-ce que ce bouton est censé produire ?