create invoice

This commit is contained in:
Ruidy 2025-02-04 08:06:15 +01:00
parent bfde4eb601
commit 7796f71590
No known key found for this signature in database
GPG key ID: E00F51288CB857CC
9 changed files with 302 additions and 227 deletions

View file

@ -1,5 +1,6 @@
<!doctype html>
<html lang="fr">
<head>
<style>
body {
@ -147,142 +148,143 @@
</head>
<body>
<div class="header space-between">
<img class="logo" src="assets/img/logo.png"/>
<div class="payee">
<b>{{ .Host.Name }}</b><br/>
{{ .Host.Address }}<br/>
{{ .Host.ZipCode }} {{ .Host.City }}<br/>
<b>Tel :</b> {{ .Host.Phone }}<br/>
<b>Mail :</b> {{ .Host.Email }}<br/>
<div class="header space-between">
<img class="logo" src="assets/img/logo.png" />
<div class="payee">
<b>{{ .Host.Name }}</b><br />
{{ .Host.Address }}<br />
{{ .Host.ZipCode }} {{ .Host.City }}<br />
<b>Tel :</b> {{ .Host.Phone }}<br />
<b>Mail :</b> {{ .Host.Email }}<br />
</div>
</div>
<hr />
<div class="billing-details space-between">
<table class="info-table">
<tbody>
<tr>
<td><strong>{{ .Name }}</strong></td>
</tr>
<tr>
<td><strong>Tel :</strong></td>
<td>{{ .PhoneNumber }}</td>
</tr>
<tr>
<td><strong>Client :</strong></td>
<td>{{ .CustomersNumber }}</td>
</tr>
<tr>
<td><strong>Plateforme :</strong></td>
<td>{{ .Platform }}</td>
</tr>
</tbody>
</table>
<table class="info-table">
<tbody>
<tr>
<td><strong>Nº de facture :</strong></td>
<td>{{ .ID }}</td>
</tr>
<tr>
<td><strong>Du :</strong></td>
<td>{{ .From }}</td>
</tr>
<tr>
<td><strong>Au :</strong></td>
<td>{{ .To }}</td>
</tr>
<tr>
<td><strong>Montant Total :</strong></td>
<td>{{ .Total }} €</td>
</tr>
</tbody>
</table>
</div>
<hr />
<div class="order-details">
<table class="items-table">
<thead>
<tr>
<th>Objet</th>
<th>Quantité</th>
<th>Prix (€)</th>
<th class="align-right">Total (€)</th>
</tr>
</thead>
<tbody class="bg-gray rounded">
{{ range .Lines }}
<tr class="item-row">
<td class="text-break product_name">{{ .Name }}</td>
<td>{{ .Quantity }}</td>
<td>{{ .Price }}</td>
<td class="align-right">{{ .Total }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
<hr/>
<div class="billing-details space-between">
<table class="info-table">
<tbody>
<tr>
<td><strong>{{ .Name }}</strong></td>
</tr>
<tr>
<td><strong>Tel :</strong></td>
<td>{{ .PhoneNumber }}</td>
</tr>
<tr>
<td><strong>Client :</strong></td>
<td>{{ .CustomersNumber }}</td>
</tr>
<tr>
<td><strong>Plateforme :</strong></td>
<td>{{ .Platform }}</td>
</tr>
</tbody>
</table>
<table class="info-table">
<tbody>
<tr>
<td><strong>Nº de facture :</strong></td>
<td>{{ .ID }}</td>
</tr>
<tr>
<td><strong>Du :</strong></td>
<td>{{ .From }}</td>
</tr>
<tr>
<td><strong>Au :</strong></td>
<td>{{ .To }}</td>
</tr>
<tr>
<td><strong>Montant Total :</strong></td>
<td>{{ .Total }}</td>
</tr>
</tbody>
</table>
</div>
<hr/>
<div class="order-details">
<table class="items-table">
<thead>
<tr>
<th>Objet</th>
<th>Quantité</th>
<th>Prix (€)</th>
<th class="align-right">Total (€)</th>
</tr>
</thead>
<tbody class="bg-gray rounded">
{{ range .Lines }}
<tr class="item-row">
<td class="text-break product_name">{{ .Name }}</td>
<td>{{ .Quantity }}</td>
<td>{{ .Price }}</td>
<td class="align-right">{{ .Total }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<div class="payment-history">
<h3>Historique des Paiements</h3>
<table class="history-table">
<thead>
<tr>
<th>Date</th>
<th>Mode de Paiement</th>
<th>Montant (€)</th>
</tr>
</thead>
<tbody>
{{ range .Payments }}
<tr>
<td>{{ .Date }}</td>
<td>{{ .Method }}</td>
<td>{{ .Amount }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<div class="payment-history">
<h3>Historique des Paiements</h3>
<table class="history-table">
<thead>
<tr>
<th>Date</th>
<th>Mode de Paiement</th>
<th>Montant (€)</th>
</tr>
</thead>
<tbody>
{{ range .Payments }}
<tr>
<td>{{ .Date }}</td>
<td>{{ .Method }}</td>
<td>{{ .Amount }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<div class="payment-summary space-between">
<table class="summary-table">
<tbody>
<tr>
<td><strong>Montant Total :</strong></td>
<td>{{ .Total }}</td>
</tr>
<tr>
<td><strong>Montant Payé :</strong></td>
<td>{{ .AmountPaid }}</td>
</tr>
<tr>
<td><strong>Solde Restant :</strong></td>
<td>{{ .BalanceDue }}</td>
</tr>
</tbody>
</table>
</div>
<hr/>
<div class="order-summary space-between">
<div class="card">
<b>Notes</b> <br/>
TVA non applicable, art. 293 B du CGI <br/>
Dispensé dimmatriculation au registre du commerce et des sociétés (RCS)
et au répertoire des métiers. <br/>
Conditions de paiement : paiement à réception de facture. Aucun escompte
consenti pour règlement anticipé ou désistement. Tout incident de
paiement est passible d'intérêts de retard. Le montant des pénalités
résulte de l'application aux sommes restant dues d'un taux d'intérêt
légal en vigueur au moment de l'incident. <br/>
<div class="payment-summary space-between">
<table class="summary-table">
<tbody>
<tr>
<td><strong>Montant Total :</strong></td>
<td>{{ .Total }}</td>
</tr>
<tr>
<td><strong>Montant Payé :</strong></td>
<td>{{ .AmountPaid }}</td>
</tr>
<tr>
<td><strong>Solde Restant :</strong></td>
<td>{{ .BalanceDue }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="order-total space-between">
<div></div>
<div class="total">
<div class="amount-due">Total</div>
<div class="amount-due-total">{{ .Total }}</div>
<hr />
<div class="order-summary space-between">
<div class="card">
<b>Notes</b> <br />
TVA non applicable, art. 293 B du CGI <br />
Dispensé dimmatriculation au registre du commerce et des sociétés (RCS)
et au répertoire des métiers. <br />
Conditions de paiement : paiement à réception de facture. Aucun escompte
consenti pour règlement anticipé ou désistement. Tout incident de
paiement est passible d'intérêts de retard. Le montant des pénalités
résulte de l'application aux sommes restant dues d'un taux d'intérêt
légal en vigueur au moment de l'incident. <br />
</div>
</div>
<div class="order-total space-between">
<div></div>
<div class="total">
<div class="amount-due">Total</div>
<div class="amount-due-total">{{ .Total }} €</div>
</div>
</div>
</div>
</body>
</html>
</html>

View file

@ -5,12 +5,13 @@ import (
"html/template"
"log"
"os"
"time"
"github.com/rjNemo/rentease/internal/config"
"github.com/rjNemo/rentease/internal/service/booking"
)
const invoiceTemplate = "assets/html/invoice.html"
func main() {
// Define the invoice data structure
type Invoice struct {
Host struct {
Name string
@ -42,79 +43,37 @@ func main() {
Amount string
}
}
// Assume these values come from your application's context.
host := config.NewHost()
// Read the template file
tmpl, err := template.ParseFiles(invoiceTemplate)
booking := booking.Booking{
Name: "Michel Le Corre",
PhoneNumber: "+590 690 44 15 30",
CustomerNumber: 2,
Platform: "Privée",
From: time.Date(2025, time.January, 15, 0, 0, 0, 0, time.UTC),
To: time.Date(2025, time.March, 15, 0, 0, 0, 0, time.UTC),
Items: []booking.Item{
{Item: "T2", Quantity: 59, Price: 35.00},
},
}
// Get dynamic invoice data from the booking via Serialize.
invoiceData := booking.Serialize(host)
// Parse the HTML template.
tmpl, err := template.ParseFiles("assets/html/invoice.html")
if err != nil {
log.Fatalf("Error parsing template: %v", err)
}
// Create sample invoice data
invoice := Invoice{
Host: struct {
Name string
Address string
ZipCode string
City string
Phone string
Email string
}{
Name: "VillaFleurie",
Address: "4 rue Gerty Archimede",
ZipCode: "97190",
City: "Le Gosier",
Phone: "+590 690 44 15 30",
Email: "location.villafleurie@gmail.com",
},
Name: "Michel Le Corre",
//PhoneNumber: "+590 690 44 15 30",
CustomersNumber: 2,
Platform: "Privée",
ID: "VFNI0332",
From: "15 Janvier 2025",
To: "15 Mars 2025",
Total: "2065.00 €",
AmountPaid: "1565.00 €",
BalanceDue: "500.00 €",
Lines: []struct {
Name string
Quantity int
Price string
Total string
}{
{
Name: "T2",
Quantity: 59,
Price: "35.00",
Total: "2065.00",
},
},
Payments: []struct {
Date string
Method string
Amount string
}{
{
"",
"Espèces",
"500.00",
},
{
"",
"Chèque",
"1065.00",
},
},
}
// Create a buffer to store the rendered HTML
// Create a buffer to hold the rendered HTML.
var buf bytes.Buffer
// Execute the template with the invoice data
if err := tmpl.Execute(&buf, invoice); err != nil {
if err := tmpl.Execute(&buf, invoiceData); err != nil {
log.Fatalf("Error executing template: %v", err)
}
// Write the rendered HTML to a file
// Write the rendered HTML to an output file.
outputPath := "output.html"
if err := os.WriteFile(outputPath, buf.Bytes(), 0644); err != nil {
log.Fatalf("Error writing HTML file: %v", err)

View file

@ -40,3 +40,32 @@ func handleSync(bs *booking.Service) echo.HandlerFunc {
return c.JSON(http.StatusCreated, "👍")
}
}
func handleCreateBooking(bs *booking.Service) echo.HandlerFunc {
type BookingInfo struct {
Content string `json:"content"`
}
return func(c echo.Context) error {
log.Info("received booking sync request from booking")
x := c.Request().Body
body, err := io.ReadAll(x)
if err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}
log.Info(string(body))
bookingInfo := new(BookingInfo)
err = json.Unmarshal(body, bookingInfo)
if err != nil {
return c.String(http.StatusInternalServerError, fmt.Sprintf("error unmarshalling JSON: %s", err))
}
b, err := bs.ParseFromApi(bookingInfo.Content)
if err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}
log.Infof("created booking %q from %q", b.Name, b.Platform)
return c.JSON(http.StatusCreated, "👍")
}
}

View file

@ -26,7 +26,8 @@ func handlePdfCreateInvoice(bs *booking.Service, hc *config.Host) echo.HandlerFu
if err != nil {
return err
}
return c.Attachment("tmp.pdf", fmt.Sprintf("VFNI-%s.pdf", b.InvoiceNumber(hc)))
return c.File("output.html")
// return c.File("output.html", fmt.Sprintf("VFNI-%s.pdf", b.InvoiceNumber(hc)))
}
}

View file

@ -19,6 +19,7 @@ func (s Server) MountHandlers() {
}))
api.POST("/sync", handleSync(s.bs))
api.GET("/bookings", handleBookingList(s.bs))
api.POST("/bookings", handleCreateBooking(s.bs))
private := s.Router.Group("")
private.Use(MakeAuthMiddleware(s.as))

View file

@ -22,6 +22,7 @@ func SentryTracingMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
sentry.WithOpName("http.server"),
sentry.ContinueFromRequest(c.Request()),
sentry.WithTransactionSource(sentry.SourceURL),
}
transaction := sentry.StartTransaction(ctx,

View file

@ -0,0 +1,37 @@
package booking
type InvoiceLine struct {
Name string
Quantity int
Price string
Total string
}
type PaymentLine struct {
Date string
Method string
Amount string
}
type Invoice struct {
Host struct {
Name string
Address string
ZipCode string
City string
Phone string
Email string
}
Name string
PhoneNumber string
CustomersNumber int
Platform string
ID string
From string
To string
Total string
AmountPaid string
BalanceDue string
Lines []InvoiceLine
Payments []PaymentLine
}

View file

@ -49,34 +49,55 @@ func (b Booking) InvoiceNumber(hc *config.Host) string {
return fmt.Sprintf("%s%04s", hc.InvoicePrefix, strconv.Itoa(b.Id+hc.CustomerSeed))
}
func (b Booking) Serialize(hc *config.Host) map[string]any {
return map[string]any{
"host": map[string]any{
"name": hc.Name,
"address": hc.Address,
"zip": hc.ZipCode,
"city": hc.City,
"phone": hc.PhoneNumber,
"email": hc.Email,
func (b Booking) ToInvoice(hc *config.Host) Invoice {
total := u.Reduce(b.Items, func(i Item, sum float64) float64 {
return sum + i.Price*float64(i.Quantity)
}, 0.0)
amountPaid := u.Reduce(b.Payments, func(i Payment, sum float64) float64 {
return sum + i.Amount
}, 0.0)
return Invoice{
Host: struct {
Name string
Address string
ZipCode string
City string
Phone string
Email string
}{
Name: hc.Name,
Address: hc.Address,
ZipCode: hc.ZipCode,
City: hc.City,
Phone: hc.PhoneNumber,
Email: hc.Email,
},
"id": b.InvoiceNumber(hc),
"name": b.Name,
"phone_number": b.PhoneNumber,
"customers_number": b.CustomerNumber,
"platform": b.Platform,
"from": b.From.Format("02/01/2006"),
"to": b.To.Format("02/01/2006"),
"lines": u.Map(b.Items, func(i Item) map[string]any {
return map[string]any{
"name": i.ToFrench(),
"quantity": i.Quantity,
"price": i.Price,
"total": i.Price * float64(i.Quantity),
Name: b.Name,
PhoneNumber: b.PhoneNumber,
CustomersNumber: b.CustomerNumber,
Platform: b.Platform,
ID: b.InvoiceNumber(hc),
From: b.From.Format("02/01/2006"),
To: b.To.Format("02/01/2006"),
Total: strconv.FormatFloat(total, 'f', 2, 64),
AmountPaid: strconv.FormatFloat(amountPaid, 'f', 2, 64),
BalanceDue: strconv.FormatFloat(total-amountPaid, 'f', 2, 64),
Lines: u.Map(b.Items, func(i Item) InvoiceLine {
return InvoiceLine{
Name: i.ToFrench(),
Quantity: i.Quantity,
Price: strconv.FormatFloat(i.Price, 'f', 2, 64),
Total: strconv.FormatFloat(i.Price*float64(i.Quantity), 'f', 2, 64),
}
}),
Payments: u.Map(b.Payments, func(p Payment) PaymentLine {
return PaymentLine{
Date: p.CreatedAt.Format("02/01/2006"),
Method: p.PaymentMethod,
Amount: strconv.FormatFloat(p.Amount, 'f', 2, 64),
}
}),
"total": strconv.FormatFloat(u.Reduce(b.Items, func(i Item, sum float64) float64 {
return sum + i.Price*float64(i.Quantity)
}, 0.0), 'f', 2, 64),
}
}

View file

@ -1,7 +1,10 @@
package booking
import (
"bytes"
"log"
"os"
"text/template"
"time"
"github.com/rjNemo/rentease/internal/config"
@ -101,5 +104,26 @@ func (bs Service) Cancel(id int) {
}
func (bs Service) BuildInvoice(b *Booking, hc *config.Host) error {
return bs.pdf.BuildInvoice(b.Serialize(hc))
invoiceData := b.ToInvoice(hc)
log.Printf("%+v", invoiceData)
tmpl, err := template.ParseFiles("assets/html/invoice.html")
if err != nil {
log.Fatalf("Error parsing template: %v", err)
}
// Create a buffer to hold the rendered HTML.
var buf bytes.Buffer
if err := tmpl.Execute(&buf, invoiceData); err != nil {
log.Fatalf("Error executing template: %v", err)
}
// Write the rendered HTML to an output file.
outputPath := "output.html"
if err := os.WriteFile(outputPath, buf.Bytes(), 0644); err != nil {
log.Fatalf("Error writing HTML file: %v", err)
}
log.Printf("HTML file created successfully: %s", outputPath)
return nil
}