From e440dcd2b5629a1704815ae2b80ee6c81daf06a9 Mon Sep 17 00:00:00 2001 From: sanchezvem Date: Wed, 3 Dec 2025 17:41:19 +0100 Subject: [PATCH] added pdf generation for all orders --- .../Request/GetDeliveryNotePdfDto.cs | 6 + .../Request/GetPurchaseOrderPdfDto.cs | 6 + .../GetDeliveryNotePdfEndpoint.cs | 41 +++++ .../GetPurchaseOrderPdfEndpoint.cs | 39 +++++ .../Quotations/GetQuotationPdfEndpoint.cs | 1 - PyroFetes/Program.cs | 2 + .../Services/Pdf/DeliveryNotePdfService.cs | 133 ++++++++++++++++ .../Services/Pdf/PurchaseOrderPdfService.cs | 146 ++++++++++++++++++ .../GetDeliveryNoteByIdWithProductsSpec.cs | 15 ++ .../GetPurchaseOrderByIdWithProductsSpec.cs | 15 ++ 10 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 PyroFetes/DTO/DeliveryNote/Request/GetDeliveryNotePdfDto.cs create mode 100644 PyroFetes/DTO/PurchaseOrder/Request/GetPurchaseOrderPdfDto.cs create mode 100644 PyroFetes/Endpoints/DeliveryNotes/GetDeliveryNotePdfEndpoint.cs create mode 100644 PyroFetes/Endpoints/PurchaseOrders/GetPurchaseOrderPdfEndpoint.cs create mode 100644 PyroFetes/Services/Pdf/DeliveryNotePdfService.cs create mode 100644 PyroFetes/Services/Pdf/PurchaseOrderPdfService.cs create mode 100644 PyroFetes/Specifications/DeliveryNotes/GetDeliveryNoteByIdWithProductsSpec.cs create mode 100644 PyroFetes/Specifications/PurchaseOrders/GetPurchaseOrderByIdWithProductsSpec.cs diff --git a/PyroFetes/DTO/DeliveryNote/Request/GetDeliveryNotePdfDto.cs b/PyroFetes/DTO/DeliveryNote/Request/GetDeliveryNotePdfDto.cs new file mode 100644 index 0000000..080ef0d --- /dev/null +++ b/PyroFetes/DTO/DeliveryNote/Request/GetDeliveryNotePdfDto.cs @@ -0,0 +1,6 @@ +namespace PyroFetes.DTO.DeliveryNote.Request; + +public class GetDeliveryNotePdfDto +{ + public int Id { get; set; } +} \ No newline at end of file diff --git a/PyroFetes/DTO/PurchaseOrder/Request/GetPurchaseOrderPdfDto.cs b/PyroFetes/DTO/PurchaseOrder/Request/GetPurchaseOrderPdfDto.cs new file mode 100644 index 0000000..5ed195d --- /dev/null +++ b/PyroFetes/DTO/PurchaseOrder/Request/GetPurchaseOrderPdfDto.cs @@ -0,0 +1,6 @@ +namespace PyroFetes.DTO.PurchaseOrder.Request; + +public class GetPurchaseOrderPdfDto +{ + public int Id { get; set; } +} \ No newline at end of file diff --git a/PyroFetes/Endpoints/DeliveryNotes/GetDeliveryNotePdfEndpoint.cs b/PyroFetes/Endpoints/DeliveryNotes/GetDeliveryNotePdfEndpoint.cs new file mode 100644 index 0000000..e9dd25a --- /dev/null +++ b/PyroFetes/Endpoints/DeliveryNotes/GetDeliveryNotePdfEndpoint.cs @@ -0,0 +1,41 @@ +using FastEndpoints; +using PyroFetes.DTO.DeliveryNote.Request; +using PyroFetes.Models; +using PyroFetes.Repositories; +using PyroFetes.Services.Pdf; +using PyroFetes.Specifications.DeliveryNotes; +using PyroFetes.Specifications.Quotations; + +namespace PyroFetes.Endpoints.DeliveryNotes; + +public class GetDeliveryNotePdfEndpoint( + DeliveryNotesRepository deliveryNotesRepository, + IDeliveryNotePdfService deliveryNotePdfService) + : Endpoint +{ + public override void Configure() + { + Get("/deliveryNotes/{@Id}/pdf", x => new {x.Id}); + AllowAnonymous(); + } + + public override async Task HandleAsync(GetDeliveryNotePdfDto req, CancellationToken ct) + { + DeliveryNote? deliveryNote = await deliveryNotesRepository + .FirstOrDefaultAsync(new GetDeliveryNoteByIdWithProductsSpec(req.Id), ct); + + if (deliveryNote == null) + { + await Send.NotFoundAsync(ct); + return; + } + + var bytes = deliveryNotePdfService.Generate(deliveryNote, deliveryNote.ProductDeliveries!); + + await Send.BytesAsync( + bytes: bytes, + contentType: "application/pdf", + fileName: $"bon-de-livraison-{deliveryNote.Id}.pdf", + cancellation: ct); + } +} \ No newline at end of file diff --git a/PyroFetes/Endpoints/PurchaseOrders/GetPurchaseOrderPdfEndpoint.cs b/PyroFetes/Endpoints/PurchaseOrders/GetPurchaseOrderPdfEndpoint.cs new file mode 100644 index 0000000..a0f48df --- /dev/null +++ b/PyroFetes/Endpoints/PurchaseOrders/GetPurchaseOrderPdfEndpoint.cs @@ -0,0 +1,39 @@ +using FastEndpoints; +using PyroFetes.DTO.PurchaseOrder.Request; +using PyroFetes.Models; +using PyroFetes.Repositories; +using PyroFetes.Services.Pdf; +using PyroFetes.Specifications.PurchaseOrders; +namespace PyroFetes.Endpoints.PurchaseOrders; + +public class GetPurchaseOrderPdfEndpoint( + PurchaseOrdersRepository purchaseOrdersRepository, + IPurchaseOrderPdfService purchaseOrderPdfService) + : Endpoint +{ + public override void Configure() + { + Get("/purchaseOrders/{@Id}/pdf", x => new {x.Id}); + AllowAnonymous(); + } + + public override async Task HandleAsync(GetPurchaseOrderPdfDto req, CancellationToken ct) + { + PurchaseOrder? purchaseOrder = await purchaseOrdersRepository + .FirstOrDefaultAsync(new GetPurchaseOrderByIdWithProductsSpec(req.Id), ct); + + if (purchaseOrder == null) + { + await Send.NotFoundAsync(ct); + return; + } + + var bytes = purchaseOrderPdfService.Generate(purchaseOrder, purchaseOrder.PurchaseProducts!); + + await Send.BytesAsync( + bytes: bytes, + contentType: "application/pdf", + fileName: $"bon-de-commande-{purchaseOrder.Id}.pdf", + cancellation: ct); + } +} \ No newline at end of file diff --git a/PyroFetes/Endpoints/Quotations/GetQuotationPdfEndpoint.cs b/PyroFetes/Endpoints/Quotations/GetQuotationPdfEndpoint.cs index d0d015a..1b425f9 100644 --- a/PyroFetes/Endpoints/Quotations/GetQuotationPdfEndpoint.cs +++ b/PyroFetes/Endpoints/Quotations/GetQuotationPdfEndpoint.cs @@ -9,7 +9,6 @@ namespace PyroFetes.Endpoints.Quotations; public class GetQuotationPdfEndpoint( QuotationsRepository quotationRepository, - QuotationProductsRepository quotationProductRepository, IQuotationPdfService quotationPdfService) : Endpoint { diff --git a/PyroFetes/Program.cs b/PyroFetes/Program.cs index 9e62ec9..e7c8595 100644 --- a/PyroFetes/Program.cs +++ b/PyroFetes/Program.cs @@ -51,6 +51,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); MapperConfiguration mappingConfig = new(mc => diff --git a/PyroFetes/Services/Pdf/DeliveryNotePdfService.cs b/PyroFetes/Services/Pdf/DeliveryNotePdfService.cs new file mode 100644 index 0000000..e084ec2 --- /dev/null +++ b/PyroFetes/Services/Pdf/DeliveryNotePdfService.cs @@ -0,0 +1,133 @@ +using PyroFetes.Models; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace PyroFetes.Services.Pdf; + +public interface IDeliveryNotePdfService +{ + byte[] Generate(DeliveryNote deliveryNote, List lignes); +} + +public class DeliveryNotePdfService : IDeliveryNotePdfService +{ + public byte[] Generate(DeliveryNote deliveryNote, List lignes) + { + var logoPath = Path.Combine(AppContext.BaseDirectory, "wwwroot", "Images", "logo.jpg"); + var signaturePath = Path.Combine(AppContext.BaseDirectory, "wwwroot", "Images", "signature.png"); + int total = 0; + int totalQuantity = 0; + var document = Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(30); + page.DefaultTextStyle(x => x.FontSize(11)); + + page.Header().Row(row => + { + // Client à gauche + row.RelativeItem().Column(col => + { + col.Item().Text(""); + col.Item().Text(""); + col.Item().Text(""); + col.Item().Text(""); + col.Item().Text("Transporteur").SemiBold().FontSize(12); + col.Item().Text($"{deliveryNote.Deliverer?.Transporter}"); + col.Item().Height(5); + col.Item().AlignLeft().Text($"Expédiée le {deliveryNote.ExpeditionDate}"); + col.Item().Height(5); + col.Item().AlignLeft().Text($"Estimée au {deliveryNote.EstimateDeliveryDate}"); + col.Item().Height(5); + col.Item().AlignLeft().Text($"Reçu le {deliveryNote.RealDeliveryDate}"); + }); + + // Logo + société à droite + row.ConstantItem(200).Column(col => + { + col.Item().AlignRight().Height(70).Image(logoPath, ImageScaling.FitArea); + col.Item().Height(20); + col.Item().AlignRight().Text("Pyro-Fêtes").SemiBold(); + col.Item().Height(5); + col.Item().AlignRight().Text("24, rue La Fosse Mardeau\n41700 Le Controis-en-Sologne"); + col.Item().Height(5); + col.Item().AlignRight().Text("Téléphone: 02 54 78 77 66"); + col.Item().Height(5); + col.Item().AlignRight().Text("SIRET: 82031463100012"); + col.Item().Height(40); + }); + }); + + page.Content().Column(col => + { + // Titre + date + col.Item().Row(row => + { + row.RelativeItem().Text($"Bon de livraison n° {deliveryNote.TrackingNumber}") + .FontSize(16).SemiBold(); + }); + col.Item().Height(20); + + col.Item().LineHorizontal(1); + + // Tableau des lignes + col.Item().Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(4); // Produit + columns.RelativeColumn(2); // Qté + columns.RelativeColumn(2); // PU + columns.RelativeColumn(2); // Total + }); + + // En-têtes + table.Header(header => + { + header.Cell().Element(CellHeader).Text("Produit"); + header.Cell().Element(CellHeader).AlignRight().Text("Qté"); + header.Cell().Element(CellHeader).AlignRight().Text("PU"); + header.Cell().Element(CellHeader).AlignRight().Text("Total"); + }); + + foreach (var l in lignes) + { + table.Cell().Element(CellBody).Text(l.Product?.Name); + table.Cell().Element(CellBody).AlignRight().Text(l.Quantity.ToString()); + table.Cell().Element(CellBody).AlignRight().Text($"{l.Quantity:n2} €"); + table.Cell().Element(CellBody).AlignRight().Text($"{l.Quantity * l.Quantity:n2} €"); + + totalQuantity += l.Quantity; + total += l.Quantity * l.Quantity; + } + + IContainer CellHeader(IContainer c) => + c.BorderBottom(1).PaddingVertical(5).DefaultTextStyle(x => x.SemiBold()); + + IContainer CellBody(IContainer c) => + c.PaddingVertical(2); + }); + + col.Item().LineHorizontal(1); + col.Item().Height(30); + + col.Item().AlignRight().Text($"Total: {totalQuantity:n2} produits"); + col.Item().AlignRight().Text($"Total HT: {total:n2} €"); + col.Item().AlignRight().Text("Taxe : 20 %"); + col.Item().AlignRight().Text($"Total TTC: {(total * 1.2):n2} €"); + }); + + // Signature en bas à droite + page.Footer().AlignRight().Column(col => + { + col.Item().AlignRight().Height(100).Image(signaturePath, ImageScaling.FitArea); + }); + }); + }); + + return document.GeneratePdf(); + } +} diff --git a/PyroFetes/Services/Pdf/PurchaseOrderPdfService.cs b/PyroFetes/Services/Pdf/PurchaseOrderPdfService.cs new file mode 100644 index 0000000..f5cfd90 --- /dev/null +++ b/PyroFetes/Services/Pdf/PurchaseOrderPdfService.cs @@ -0,0 +1,146 @@ +using PyroFetes.Models; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace PyroFetes.Services.Pdf; + +public interface IPurchaseOrderPdfService +{ + byte[] Generate(PurchaseOrder purchaseOrder, List lignes); +} + +public class PurchaseOrderPdfService : IPurchaseOrderPdfService +{ + public byte[] Generate(PurchaseOrder purchaseOrder, List lignes) + { + var logoPath = Path.Combine(AppContext.BaseDirectory, "wwwroot", "Images", "logo.jpg"); + var signaturePath = Path.Combine(AppContext.BaseDirectory, "wwwroot", "Images", "signature.png"); + int totalQuantity = 0; + int total = 0; + var document = Document.Create(container => + { + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(30); + page.DefaultTextStyle(x => x.FontSize(11)); + + page.Header().Row(row => + { + // Client à gauche + row.RelativeItem().Column(col => + { + col.Item().Text(""); + col.Item().Text(""); + col.Item().Text(""); + col.Item().Text(""); + col.Item().Text("Fournisseur").SemiBold().FontSize(12); + col.Item().Text("Mettre fournisseur ici"); + }); + + // Logo + société à droite + row.ConstantItem(200).Column(col => + { + col.Item().AlignRight().Height(70).Image(logoPath, ImageScaling.FitArea); + col.Item().Height(20); + col.Item().AlignRight().Text("Pyro-Fêtes").SemiBold(); + col.Item().Height(5); + col.Item().AlignRight().Text("24, rue La Fosse Mardeau\n41700 Le Controis-en-Sologne"); + col.Item().Height(5); + col.Item().AlignRight().Text("Téléphone: 02 54 78 77 66"); + col.Item().Height(5); + col.Item().AlignRight().Text("SIRET: 82031463100012"); + col.Item().Height(40); + }); + }); + + page.Content().Column(col => + { + // Titre + date + col.Item().Row(row => + { + row.RelativeItem().Text($"Bon de commande n° {purchaseOrder.Id}") + .FontSize(16).SemiBold(); + + row.ConstantItem(200).AlignRight().Text( + $"Le {DateTime.Now:dd/MM/yyyy}"); + }); + col.Item().Height(20); + + col.Item().LineHorizontal(1); + + // Tableau des lignes + col.Item().Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(4); // Produit + columns.RelativeColumn(1); // Qté + columns.RelativeColumn(2); // PU + columns.RelativeColumn(2); // Total + }); + + // En-têtes + table.Header(header => + { + header.Cell().Element(CellHeader).Text("Produit"); + header.Cell().Element(CellHeader).AlignRight().Text("Qté"); + header.Cell().Element(CellHeader).AlignRight().Text("PU"); + header.Cell().Element(CellHeader).AlignRight().Text("Total"); + }); + + foreach (var l in lignes) + { + table.Cell().Element(CellBody).Text(l.Product?.Name); + table.Cell().Element(CellBody).AlignRight().Text(l.Quantity.ToString()); + table.Cell().Element(CellBody).AlignRight().Text($"{l.Quantity:n2} €"); + table.Cell().Element(CellBody).AlignRight().Text($"{l.Quantity * l.Quantity:n2} €"); + + totalQuantity += l.Quantity; + total += l.Quantity * l.Quantity; + } + + IContainer CellHeader(IContainer c) => + c.BorderBottom(1).PaddingVertical(5).DefaultTextStyle(x => x.SemiBold()); + + IContainer CellBody(IContainer c) => + c.PaddingVertical(2); + }); + + col.Item().LineHorizontal(1); + col.Item().Height(30); + + col.Item().Row(row => + { + // Colonne gauche : conditions de vente + row.RelativeItem().Column(left => + { + left.Item().Text("Conditions de vente") + .SemiBold().FontSize(12); + left.Item().Text(purchaseOrder.PurchaseConditions) + .FontSize(9); + }); + + // Colonne droite : totaux + row.ConstantItem(180).Column(right => + { + right.Item().AlignRight().Text($"Total: {totalQuantity:n2} produits"); + right.Item().AlignRight().Text($"Total HT: {total:n2} €"); + right.Item().AlignRight().Text("Taxe: 20 %"); + right.Item().AlignRight().Text($"Total TTC: {(total * 1.2):n2} €"); + }); + }); + }); + + // Signature en bas à droite + page.Footer().AlignRight().Column(col => + { + col.Item().AlignRight().Height(100).Image(signaturePath, ImageScaling.FitArea); + }); + }); + }); + + return document.GeneratePdf(); + } +} diff --git a/PyroFetes/Specifications/DeliveryNotes/GetDeliveryNoteByIdWithProductsSpec.cs b/PyroFetes/Specifications/DeliveryNotes/GetDeliveryNoteByIdWithProductsSpec.cs new file mode 100644 index 0000000..5849019 --- /dev/null +++ b/PyroFetes/Specifications/DeliveryNotes/GetDeliveryNoteByIdWithProductsSpec.cs @@ -0,0 +1,15 @@ +using Ardalis.Specification; +using PyroFetes.Models; + +namespace PyroFetes.Specifications.DeliveryNotes; + +public class GetDeliveryNoteByIdWithProductsSpec : Specification +{ + public GetDeliveryNoteByIdWithProductsSpec(int deliveryNoteId) + { + Query + .Where(d => d.Id == deliveryNoteId) + .Include(d => d.ProductDeliveries!) + .ThenInclude(dp => dp.Product); + } +} \ No newline at end of file diff --git a/PyroFetes/Specifications/PurchaseOrders/GetPurchaseOrderByIdWithProductsSpec.cs b/PyroFetes/Specifications/PurchaseOrders/GetPurchaseOrderByIdWithProductsSpec.cs new file mode 100644 index 0000000..734738e --- /dev/null +++ b/PyroFetes/Specifications/PurchaseOrders/GetPurchaseOrderByIdWithProductsSpec.cs @@ -0,0 +1,15 @@ +using Ardalis.Specification; +using PyroFetes.Models; + +namespace PyroFetes.Specifications.PurchaseOrders; + +public class GetPurchaseOrderByIdWithProductsSpec : Specification +{ + public GetPurchaseOrderByIdWithProductsSpec(int purchaseOrderId) + { + Query + .Where(p => p.Id == purchaseOrderId) + .Include(p => p.PurchaseProducts!) + .ThenInclude(pp => pp.Product); + } +} \ No newline at end of file