Shopping : la facture
Dans cet article nous allons aborder la partie création de facture. C’est un chapitre important pour une boutique en ligne. Il faut se poser une question de base : est-ce qu’on génère et mémorise la facture directement dans la boutique ou est-ce qu’on utilise un service tiers ?
La réglementation anti fraude à la TVA est devenue très contraignante. On peut concevoir que ces fraudes sont sans doutes fréquentes et répandues mais comme toujours ce sont les agissements d’un petit nombre qui compliquent la vie de la majorité, c’est vrai dans tous les domaines. Toujours est-il qu’il faut respecter la réglementation en vigueur et on doit pouvoir fournir une attestation à l’administration qui prouve qu’on respecte bien les 4 conditions : inaliénabilité, sécurisation, conservation et archivage. Mais évidemment on ne peut pas s’auto-certifier !
Vous pouvez télécharger un ZIP du projet ici.
VosFactures
Finalement le plus simple est de gérer la facturation avec une application tierce qui fournit cette certification. Après quelques recherches j’ai opté pour Vosfactures. C’est une application simple et complète avec un bon rapport qualité-prix. Il existe même une option gratuite mais limitée à 3 documents par mois, ce qui n’est pas très réaliste pour une boutique en ligne parce qu’à ce niveau la rentabilité semble impossible. Par contre l’option basique à 4€ HT permet un nombre illimité de documents. Je précise que je n’ai aucun lien commercial avec ce site mis à part le fait que j’y suis un client. On peut s’inscrire gratuitement pendant un mois en utilisant toutes les fonctionnalités, ce qui permet de se faire une bonne idée des possibilités. Pour continuer ce tutoriel de façon efficace vous pouvez donc prendre un compte gratuit.
Pour que les factures soient renseignées il faut au moins dans l’application Vosfactures créer ce qu’ils appellent un département :
Vous pouvez accéder à un formulaire très complet en cliquant le petit bouton avec le pignon à droite. Là vous pouvez préciser le nom de l’entreprise, son adresse, les mentions légales à préciser en bas des documents…
Si vous ne précisez pas tout ça il y aura des trous dans vos factures même pour vos essais !
Vosfacture propose une API très complète et documentée ici. On va voir dans cet article comment la mettre en œuvre. Une fois que vous avez un compte vous pouvez récupérer url et token à cette adresse : https://{votrecompte}.vosfactures.fr/api_tokens.
On va mémoriser ces information dans le fichier .env :
FACTURES_TOKEN=votre token ici FACTURES_URL=https://votre compte.vosfactures.fr/ FACTURES_TEST=true
N’oubliez pas d’ajouter la variable pour préciser que vous êtes en test !
Et évidemment on crée un fichier de configuration
<?php return [ 'token' => env('FACTURES_TOKEN'), 'url' => env('FACTURES_URL'), 'test' => env('FACTURES_TEST'), ];
Je rappelle que c’est nécessaire si vous créez un cache pour la configuration.
Un service pour la facture
Comme on va utiliser cette API de plusieurs points de la boutique on va créer un service :
Je rappelle qu’il n’existe pas de commande Artisan pour créer un service. C’est une façon d’organiser le code pour rendre les contrôleurs plus légers, d’autant qu’on peut injecter ce service facilement dans une méthode.
Le code du service va être un peu chargé parce qu’on doit envoyer pas mal d’informations :
<?php namespace App\Services; use App\Models\Order; use Illuminate\Support\Facades\Http; class Facture { public function create(Order $order, $paid = false) { $order->load('adresses', 'products', 'adresses.country', 'user'); // Adresse de facturation $addressOrder = $order->adresses->first(); $text = $addressOrder->address; if($addressOrder->addressbis) { $text .= "\n" . $addressOrder->addressbis; } $invoice = [ 'kind' => 'vat', 'test' => config('invoice.test') ? 'true' : 'false', 'title' => 'Commande référence ' . $order->reference, 'buyer_street' => $text, 'buyer_country' => $addressOrder->country->name, 'buyer_post_code' => $addressOrder->postal, 'buyer_city' => $addressOrder->city, 'buyer_company' => $addressOrder->professionnal ? '1' : '0', 'buyer_email' => $order->user->email, 'buyer_phone' => $addressOrder->phone, 'payment_type' => $order->payment_text, 'payment_to' => now()->endOfMonth()->addMonth()->format('Y-m-d'), 'status' => $paid ? 'Payé' : 'Créé', ]; // Bon de commande éventuel if($order->payment === 'mandat') { $invoice['oid'] = $order->purchase_order; } // Si la facture a été payée if($paid) { //$invoice['paid_date'] = now()->format('Y-m-d'); $invoice['paid'] = $order->totalOrder; } // Si c'est un professionnel if($addressOrder->professionnal) { $invoice['buyer_name'] = "$addressOrder->company"; if(isset($addressOrder->name)) { $invoice['buyer_first_name'] = $addressOrder->firstname; $invoice['buyer_last_name'] = $addressOrder->name; } else { $invoice['buyer_first_name'] = $order->user->firstname; $invoice['buyer_last_name'] = $order->user->name; } } else { //$invoice['buyer_name'] = "$addressOrder->name $addressOrder->firstname"; $invoice['buyer_first_name'] = $addressOrder->firstname; $invoice['buyer_last_name'] = $addressOrder->name; } // Adresse et boîte postale $text = $addressOrder->address; if($addressOrder->addressbis) { $text .= " " . $addressOrder->addressbis; } if($addressOrder->bp) { $text .= " " . $addressOrder->bp; } $invoice['buyer_street'] = $text; // S'il y a une adresse de livraison if($order->adresses->count() === 2) { $invoice['use_delivery_address'] = true; $addressdelivery = $order->adresses->get(1); $text = ''; if(isset($addressdelivery->name)) { $text .= "$addressdelivery->civility $addressdelivery->name $addressdelivery->firstname \n"; } if($addressdelivery->company) { $text .= $addressdelivery->company . "\n"; } $text .= $addressdelivery->address . "\n"; if($addressdelivery->addressbis) { $text .= $addressdelivery->addressbis . "\n"; } if($addressdelivery->bp) { $text .= 'BP ' . $addressdelivery->bp . "\n"; } $text .= "$addressdelivery->postal $addressdelivery->city" . "\n"; $text .= $addressdelivery->country->name . "\n"; $text .= $addressdelivery->phone; $invoice['delivery_address'] = $text; } // Taxe if($order->pick) { $tax = .2; } else { if(isset($addressdelivery)) { $tax = $addressdelivery->country->tax; } else { $tax = $addressOrder->country->tax; } } // Produits $positions = []; foreach($order->products as $product) { array_push($positions, [ 'name' => $product->name, 'quantity' => $product->quantity, 'tax' => $tax * 100, 'total_price_gross' => $product->total_price_gross, ]); } // Frais d'expédition if($order->shipping > 0) { array_push($positions, [ 'name' => 'Frais d\'expédition', 'quantity' => 1, 'tax' => 0, 'total_price_gross' => $order->shipping, ]); } $invoice['positions'] = $positions; // Envoi return Http::post(config('invoice.url') . 'invoices.json', [ 'api_token' => config('invoice.token'), 'invoice' => $invoice, ]); } }
On commence par récupérer toutes les informations dans la base :
$order->load('adresses', 'products', 'adresses.country', 'user');
L’adresse de facturation est la première :
$addressOrder = $order->adresses->first();
On récupère le texte de l’adresse avec son complément éventuel :
$text = $addressOrder->address; if($addressOrder->addressbis) { $text .= "\n" . $addressOrder->addressbis; }
Ensuite on prépare un certain nombre de données :
$invoice = [ 'kind' => 'vat', 'test' => config('invoice.test') ? 'true' : 'false', 'title' => 'Commande référence ' . $order->reference, 'buyer_street' => $text, 'buyer_country' => $addressOrder->country->name, 'buyer_post_code' => $addressOrder->postal, 'buyer_city' => $addressOrder->city, 'buyer_company' => $addressOrder->professionnal ? '1' : '0', 'buyer_email' => $order->user->email, 'buyer_phone' => $addressOrder->phone, 'payment_type' => $order->payment_text, 'payment_to' => now()->endOfMonth()->addMonth()->format('Y-m-d'), 'status' => $paid ? 'Payé' : 'Créé', ];
Pour le paiement on prévoit ici 30 jours fin de mois mais vous pouvez prévoir évidemment autre chose.
S’il y a eu un bon de commande il faut ajouter le numéro dans la facture :
// Bon de commande éventuel if($order->payment === 'mandat') { $invoice['oid'] = $order->purchase_order; }
S’il y a eu un paiement il faut aussi le préciser, avec sa valeur :
// Si la facture a été payée if($paid) { $invoice['paid'] = $order->totalOrder; }
Ensuite on a du code que je ne détaille pas concernant les adresses.
On précise ensuite la TVA :
// Taxe if($order->pick) { $tax = .2; } else { if(isset($addressdelivery)) { $tax = $addressdelivery->country->tax; } else { $tax = $addressOrder->country->tax; } }
On renseigne après tous les produits commandés :
// Produits $positions = []; foreach($order->products as $product) { array_push($positions, [ 'name' => $product->name, 'quantity' => $product->quantity, 'tax' => $tax * 100, 'total_price_gross' => $product->total_price_gross, ]); }
Les frais d’expédition éventuels :
// Frais d'expédition if($order->shipping > 0) { array_push($positions, [ 'name' => 'Frais d\'expédition', 'quantity' => 1, 'tax' => 0, 'total_price_gross' => $order->shipping, ]); }
Et enfin on envoi tout en profitant des nouvelles possibilités de Laravel 7 avec le client HTTP :
// Envoi return Http::post(config('invoice.url') . 'invoices.json', [ 'api_token' => config('invoice.token'), 'invoice' => $invoice, ]);
Création de la facture
Dans le contrôleur PaymentController on avait juste laissé un commentaire pour la création de la facture, on va maintenant pouvoir utiliser notre service, je remets le code complet :
use App\Services\Facture; ... public function __invoke(Request $request, Facture $facture, Order $order) { $this->authorize('manage', $order); $state = null; if($request->payment_intent_id === 'error') { $state = 'erreur'; } else { \Stripe\Stripe::setApiKey(config('stripe.secret_key')); $intent = \Stripe\PaymentIntent::retrieve($request->payment_intent_id); if ($intent->status === 'succeeded') { $request->session()->forget($order->reference); $order->payment_infos()->create(['payment_id' => $intent->id]); $state = 'paiement_ok'; // Création de la facture $response = $facture->create($order, true); if($response->successful()) { $data = json_decode($response->body()); $order->invoice_id = $data->id; $order->invoice_number = $data->number; } } else { $state = 'erreur'; } } $order->state_id = State::whereSlug($state)->first()->id; $order->save(); }
On crée la facture avec le service, ensuite on récupère l’identifaint de la facture qui nous sera utile pour aller la chercher avec l’API, ainsi que le numéro de la facture. On mémorise tout ça dans la commande.
Mise en oeuvre
Maintenant on va voir si ça fonctionne…
On commande deux produits identiques :
On commande en utilisant un numéro de carte de test :
Le paiement réussit :
Sur le tableau de bord de VosFactures on trouve la facture qui apparaît bien comme payée :
Et on a bien la facture générée et disponible, on retrouve bien les deux produits, le bon prix, la référence de la commande, le paiement par carte bancaire :
Il existe plusieurs modèles de facture selon les goûts (une vingtaine) et on peut évidemment ajouter un logo.
Dans la table orders on a bien les renseignements désirés :
Conclusion
On a encore avancé avec la création de la facture. Il faudra encore prévoir que le client puisse aller la récupérer, on fera ça à partir de son compte personnel. Il faudra aussi pouvoir en générer une à partir de l’administration lorsque le paiement se fera par chèque ou virement.
11 commentaires
softcode
Bonjour bestmomo je viens de renconter une erreur qui arrive souvent lors de la creation de la facture l’erreur est la suivante:
cURL error 28: Resolving timed out after 10000 milliseconds (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for https://dinconcept.vosfactures.fr/invoices.json
Comment resoudre ce problème au niveau de laravel ou du serveur 02swuich parce que mon site est hebergé sur o2swich?? merci
bestmomo
Salut, je suis aussi hébergé par O2switch et je ne rencontre pas ce problème. Est-ce que tu constates des ralentissements sur ton site ?
softcode
Non il n’ya pas de renlentissement le site tourne très rapide comme toujours c’est juste quand je crée la facture apartir de cet api vosfacture que que je constate un ralentisement et après quelque seconde ça me donne la même erreur : cURL error 28: Resolving timed out after 10000 milliseconds,
je constate aussi qu’à chaque fois que je fais appel à un service distant via un api quelconque cette erreur revient toujours : cURL error 28: Resolving timed out after 10000 milliseconds
je me demande si c’est un problème lié à laravel ou à o2swich??
bestmomo
Salut,
Peut-être une solution ici.
Sinon, tu peux appeler O2Switch, ils sont rapides et compétents.
softcode
Bonjour bestmomo, j’avais une préocupation à vous demander, de comment je peux faire pour que après validation du paiment, lors de l’enregistrement de la facture via l’api vosfacture, que l’utilisateur recoive un email comptenant la facture de sa commande sous format pdf?? Merci pour votre aide
bestmomo
Salut,
La création de la facture peur prendre un certain temps, ta question repose sur un petit souci de synchronisation. On ne peut pas récupérer la facture avant sa création et l’on ne sait pas quand elle va être créée. Quand on crée une facture avec l’API, on n’a pas de réponse. La documentation complète est ici. Il existe bien la possibilité d’envoyer la facture par email. Mais, je ne sais pas trop comment résoudre élégamment ce souci de synchro…
Rosisse25
Salut,
La facturation des commandes ne peut pas être générée dans mon tableau de bord de VosFactures.
Revenus/Facture est vide.
malgré que tout a été bien fait.
bestmomo
Salut,
Il faudrait voir la réponse à l’appel de l’API pour voir s’il y a une erreur.
jeromeborg
Salut,
j’ai du mal a comprendre un concept, peux tu m’éclairer ?
Dans le stripejs, a la validation, il appelle Route::name(‘commandes.payment’)->post(‘paiement/{order}’, ‘PaymentController’) avec pour parametre un id; //OK
dans paymentcontroller on utilise la méthode magique __invoke // OK
Mais a quel moment ou endroit appelles ton paymentcontroller comme une fonction ?
Je bloque, j’ai mis du débuggage via un Storage::disk(‘public’)->append(, le fichier n’est jamais écrit, donc aucun événement ne déclenche le __invoke, dans pma le order n’est pas validé
Merci
bestmomo
Salut,
Je ne suis pas sûr de bien comprendre la question. Le contrôleur est appelé par le routeur de Laravel et je ne suis jamais allé voir comment il fait… Est-ce que tu as une erreur ?
jeromeborg
Je n’ai pas d’erreur mais la magique du controleur n »était jamais appelée.
Je viens de trouver erreur 501 a l’appel via le js
javascript?v=1590077519:1173 POST http://127.0.0.1/eshopmomo/commandes/paiement/61 500 (Internal Server Error)
je dois avoir une erreur de syntaxe
je vais regarder, merci d’avoir pris le temps de répondre