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 « J'aime »

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 « J'aime »

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 @anon16835425 :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 « J'aime »

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

1 « J'aime »

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 ?