Script pour générer la création et validation d'une commande client et d'une facture de vente

Bonjour,

J’aimerais créer un script qui fait la même chose que la case « Générer la facture au début de la période » dans un abonnement mais qui ne prend pas le même déclencheur.

J’aimerais que dès que la propriété « Facturable » de l’abonnement passe à « vrai », alors une commande client est créée depuis l’abonnement puis une facture de vente est créée depuis cette commande client. Comme ça je peux déclancher manuellement le process complet.

Est-ce qu’il y a de la doc quelque part pour m’aider. Je n’ai vu que ceci qui reste un peu léger pour mon usage :

Merci d’avance :blue_heart:

Bonjour @TimJIndieHosters,

La logique de génération de commandes/factures n’est pas documentée, mais tu peux la trouver ici si tu le souhaites:

  1. Le système valide les périodes
  2. Il valide les statuts des plans d’abonnement
  3. Il valide le statut de l’abonnement (erpnext/accounts/doctype/subscription/subscription_state_manager.py · develop · Dokos / Dokos · GitLab)
  4. Il va déclencher la facturation

Si tu veux déclencher la facturation manuellement, tu peux:

  1. Désactiver la tâche de fond programmée qui fait cette opération toutes les heures: ex. Dokos

  2. Enregistrer l’abonnement concerné pour mettre à jour toutes ses données (statuts/plans/etc…), ça ne déclenchera pas la facturation.

doc = frappe.get_doc("Subscription", "SUB00001")
doc.save()

Tu peux aussi forcer le statut avec frappe.db.set_value("Subscription", "SUB00001", "status", "Billable") pour forcer la facturation juste derrière, mais attention tu risque de créer des incohérences de statut.

  1. Déclencher la facturation quand tu l’entends en lançant la méthode process ou juste process_order_and_payment (attention à set_customer_status aussi qui met à jour automatiquement le statut des clients, mais dont tu n’as pas forcément besoin).
doc = frappe.get_doc("Subscription", "SUB00001")
doc.run_method("process_order_and_payment")

Bonne fin de journée,

Charles-Henri

Merci pour ta réponse, De ce que je comprends il faut forcer le statut frappe.db.set_value("Subscription", "SUB00001", "status", "Billable") avant d’appeler process_order_and_payment.

J’aimerais garder les vérifications dans process avant d’éditer la facture

SubscriptionPeriod(self).validate()
SubscriptionPlansManager(self).set_plans_status()
SubscriptionPlansManager(self).set_plans_rates()
SubscriptionStateManager(self).set_status()

Je me dis que pour ne pas créer de conflit de statut le pus simple est d’avoir un champ custom pour gérer cette facturation manuelle. Je vois donc plusieurs options:

Option 1 :
elle me parait plus cohérente mais je ne sais pas si c’est possible :

  • Désactiver la tâche de fond comme évoqué
  • puis créer un script serveur avec un déclenchement programmé. Au lieu d’appeler process_all qui appelle process, la tâche appellerait un version légèrement différente de process avec une condition supplémentaire
subscriptions = frappe.get_all("Subscription", filters={"status": ("!=", "Cancelled")})
for sub in subscriptions:
   subscription = frappe.get_doc("Subscription", sub.name)
   subscription.process_custom()
   frappe.db.commit()

def process_custom(self):
   SubscriptionPeriod(self).validate()
   SubscriptionPlansManager(self).set_plans_status()
   SubscriptionPlansManager(self).set_plans_rates()
   SubscriptionStateManager(self).set_status()
   if subscription.get("custom_status") == "Billable":
       SubscriptionStateManager(self).override_status()

   self.process_order_and_payment()
   self.set_customer_status()
   self.run_method("on_process")

def override_status(self);
   if not subscription.is_cancelled() and not subscription.is_trial():
       status = "Billable"

       if status != subscription.subscription.status:
           subscription.subscription.set_state("previous_status", subscription.subscription.status)
           subscription.subscription.db_set("status", status)

           if subscription.order_can_be_generated_before_period_start() or subscription.order_can_be_generated_before_period_end():
               subscription.subscription.clear_period_state()
           subscription.subscription.reload()

Cependant, dans le script, je ne peux pas initier SubscriptionPeriod, SubscriptionPlansManager et SubscriptionStateManagercar le script semble ne pas avoir accès aux classes et je n’arrive pas à les importer.

Option 2 :
Plus accessible mais qui nécessite d’avoir 2 automatisations qui sont en conflit :

  • laisser la tâche de fond
  • ajouter un script serveur sur l’enregistrement d’un abonnement qui reprend la même logique que dans override_status si le champ custom est « Billable » puis appelle process_order_and_payment()

Le problème que je vois c’est que si ce script s’exécute et en même temps la tâche de fond alors possiblement les deux scripts modifient le statut et si le statut à été « reset » par la tâche de fond alors le processus de facturation n’est pas enclenché car le statut n’a pas changé.

Option 3 :

  • Désactiver la tâche de fond
  • Ajouter un script serveur programmé qui appelle process comme actuellement puis reprend la même logique que dans override_status si le champ custom est « Billable » puis appelle process_order_and_payment()

L’avantage c’est qu’il n’y aura pas de conflit, l’inconvénient c’est que toute la partie process_order_and_payment() tourne 2 fois (dont une inutilement) sur tout les abonnements gérés avec le champ custom.

Est-ce l’option 1 serait possible / comment appeler les fonctions de calidation déjà existantes ?

Bonjour @TimJIndieHosters?

L’option 1 est tout à fait réalisable, mais il faut passer par une application personnalisée, car tu n’as effectivement pas accès aux classes nécessaires dans les scripts serveurs.
Voici le tuto sur le sujet: Create an App

=> Aucun souci pour héberger une application personnalisée avec votre offre cloud si besoin

Sinon le plus simple est de privilégier l’option 3.
Si le statut standard n’est pas « Facturable », il va juste déclencher SubscriptionStateManager(self).set_status() deux fois, ça me paraît plus qu’acceptable :wink:

ça marche, je vais faire l’option 3 alors dans un premier temps et quand j’ai le temps / besoin de faire un truc plus optimal je teste l’application personnalisée