corrige la génération du Pdf

This commit is contained in:
Paul Schneider 2026-09-22 16:34:51 +01:00
commit bbc37d3095
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
9 changed files with 34 additions and 211 deletions

View file

@ -23,7 +23,7 @@ namespace Yavsc.ApiControllers
private readonly IBillingService billing;
private readonly ILogger logger;
private readonly SiteSettings siteSettings;
private readonly IViewEngine viewEngine;
private readonly IRazorViewEngine viewEngine;
// The view-rendering dependencies (siteSettings, viewEngine) are
// optional: only EstimateTex/EstimatePdf use them, and those run in
@ -51,7 +51,7 @@ namespace Yavsc.ApiControllers
this.billing = billing ?? new BillingService(context);
logger = loggerFactory.CreateLogger<FrontOfficeApiController>();
this.siteSettings = siteSettings?.Value;
this.viewEngine = viewEngine as IViewEngine;
this.viewEngine = viewEngine;
}
[HttpGet("profiles/{actCode}")]
@ -97,9 +97,14 @@ namespace Yavsc.ApiControllers
public async Task<IActionResult> EstimatePdf(long queryId, CancellationToken token)
{
if (viewEngine is null || siteSettings is null)
return Problem("PDF rendering is not configured on this host.",
{
string msg = "PDF rendering is not configured on this host.";
logger.LogError(msg);
return Problem(msg,
statusCode: StatusCodes.Status503ServiceUnavailable);
}
var estimate = await LoadEstimateForRenderAsync(queryId, token);
if (estimate is null) return NotFound(new { Error = "no estimate linked to this query" });
@ -135,22 +140,21 @@ namespace Yavsc.ApiControllers
// we stream the generated PDF bytes — but driving the
// generation through the view keeps the template as the
// output producer, per the original design.
this.RenderViewToString(viewEngine, "Estimate_pdf", model);
if (model.GenerateEstimatePdf() is FileInfo fo)
{
return File(fo.OpenRead(), "application/pdf");
}
string msg = "TeX render failed: " + model.GenerationErrorMessage;
logger.LogError(msg);
return Problem(msg);
}
catch (Exception ex)
{
logger.LogError(ex, "estimate {QueryId}: TeX/PDF render failed", queryId);
return Problem("TeX render failed: " + ex.Message);
}
if (!model.Generated)
return Problem("PDF generation failed: "
+ (model.GenerationErrorMessage?.Value ?? "unknown error"));
var pdfPath = System.IO.Path.Combine(billsDir, baseFileName + ".pdf");
var bytes = await System.IO.File.ReadAllBytesAsync(pdfPath, token);
return File(bytes, "application/pdf", baseFileName + ".pdf");
}
}
// Loads the estimate linked to a query with the navigation the
// Estimate_tex template reads (client + performer profile + bill).
@ -402,4 +406,4 @@ namespace Yavsc.ViewModels.FrontOffice
/// </summary>
public SignatureType? SignatureType { get; set; }
}
}
}

View file

@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace Yavsc.Helpers
{
using Microsoft.AspNetCore.Mvc.Razor;
using ViewModels.Gen;
public class TeXString : HtmlString
{
@ -115,7 +116,7 @@ namespace Yavsc.Helpers
return new TeXString(string.Join(lineSeparator,texlines));
}
public static bool GenerateEstimatePdf(this SiteSettings settings, PdfGenerationViewModel Model)
public static FileInfo GenerateEstimatePdf(this PdfGenerationViewModel Model)
{
string errorMsg = null;
// Resolve to an absolute path: SiteSettings.Bills may be a
@ -195,17 +196,18 @@ namespace Yavsc.Helpers
".fdb_latexmk", ".synctex.gz", ".toc" })
{
if (ext == ".log" && !fo.Exists) continue;
var f = new FileInfo(System.IO.Path.Combine(billdir, name + ext));
var fileName = name + ext;
var f = new FileInfo(System.IO.Path.Combine(billdir, fileName));
if (f.Exists) { try { f.Delete(); } catch { } }
}
Model.Generated = fo.Exists;
Model.GenerationErrorMessage = new HtmlString(errorMsg);
return fo.Exists;
return fo;
}
public static string RenderViewToString(
this Controller controller, IViewEngine engine,
this Controller controller, IRazorViewEngine engine,
string viewName, object model, bool isMainPage = true)
{
if (engine == null)

View file

@ -1,108 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Yavsc.Billing;
using Yavsc.Helpers;
using Yavsc.Models;
using Yavsc.ViewModels;
using Yavsc.ViewModels.Gen;
using Yavsc.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace Yavsc.ViewComponents
{
public class BillViewComponent : ViewComponent
{
readonly ApplicationDbContext dbContext;
readonly IBillingService billing;
readonly IStringLocalizer<BillViewComponent> localizer;
private readonly SiteSettings siteSettings;
public BillViewComponent(ApplicationDbContext dbContext,
IStringLocalizer<BillViewComponent> localizer,
IBillingService billing,
IOptions<SiteSettings> siteSettings)
{
this.billing = billing;
this.dbContext = dbContext;
this.localizer = localizer;
this.siteSettings = siteSettings.Value;
}
public async Task<IViewComponentResult> InvokeAsync(
string code,
IBillable billable,
OutputFormat format,
bool asBill,
SiteSettings settings
)
{
var di = new DirectoryInfo(settings.Bills);
var dia = new DirectoryInfo(settings.Avatars);
ViewBag.BillsDir = di.FullName;
ViewBag.AvatarsDir = dia.FullName;
ViewBag.AsBill = asBill; // vrai pour une facture, sinon, c'est un devis
ViewBag.Acquitted = billable.GetIsAcquitted();
ViewBag.BillingCode = code;
var client = await dbContext.Users
.Include(u=>u.PostalAddress)
.SingleAsync(u=>u.Id == billable.ClientId);
ViewBag.Client = client;
var performer = await dbContext.Users
.Include(u=>u.BankInfo)
.Include(u=>u.PostalAddress)
.SingleAsync(u=>u.Id == billable.PerformerId);
ViewBag.Performer = performer;
string clientAddress = client.PostalAddress?.Address ?? null;
ViewBag.ClientAddress = clientAddress.SplitAddressToTeX();
var profile = await dbContext.Performers
.Include(p=>p.OrganizationAddress)
.SingleAsync(p=>p.PerformerId == billable.PerformerId);
ViewBag.PerformerProfile = profile;
ViewBag.ActivityLabel = (await dbContext.Activities.SingleAsync(a => a.Code == billable.ActivityCode)).Name;
var proAddr = profile.OrganizationAddress.Address;
ViewBag.PerformerOrganizationAddress = proAddr.SplitAddressToTeX() ;
ViewBag.FooterPerformerOrganizationAddress = proAddr.SplitAddressToTeX(", ");
ViewBag.PerformerAddress = performer.PostalAddress?.Address.SplitAddressToTeX() ;
switch (format) {
case OutputFormat.LaTeX :
return this.View("Bill_tex", billable);
case OutputFormat.Pdf :
string tex = null;
var oldWriter = ViewComponentContext.ViewContext.Writer;
using (var writer = new StringWriter())
{
this.ViewComponentContext.ViewContext.Writer = writer;
var resultTex = View("Bill_tex", billable);
await resultTex.ExecuteAsync(this.ViewComponentContext);
tex = writer.ToString();
}
ViewComponentContext.ViewContext.Writer = oldWriter;
var pdfGenerationViewModel = new PdfGenerationViewModel
{
Temp = siteSettings.TempDir,
TeXSource = tex,
DestDir = siteSettings.Bills,
BaseFileName = billable.GetFileBaseName(billing)
};
if (settings.GenerateEstimatePdf(pdfGenerationViewModel)) {
return this.View(new { Generated = pdfGenerationViewModel.BaseFileName+".pdf" });
} else {
return View(new { Error = pdfGenerationViewModel.GenerationErrorMessage } );
}
}
ViewBag.BillFileInfo = billable.GetBillInfo(billing, siteSettings);
ViewBag.Settings = settings;
return View("Default",billable);
}
}
}

View file

@ -5,7 +5,7 @@ namespace Yavsc.ViewModels.Gen
{
public class PdfGenerationViewModel
{
[Required]
public string TeXSource { get; set; }
[Required]

View file

@ -1,21 +0,0 @@
@using System.Reflection
@using System.IO
@using Microsoft.Extensions.WebEncoders
@using System.Diagnostics
@using System.Text
@using Yavsc.Helpers
@model Yavsc.ViewModels.Gen.PdfGenerationViewModel
@{
ViewBag.Settings.GenerateEstimatePdf(Model);
}
@if (Model.Generated) {
<div>
@(Model.BaseFileName).pdf was generated
</div>
} else {
<div class="error">
@Model.GenerationErrorMessage
<p>Something went wrong ...</p>
</div>
}

View file

@ -1,33 +0,0 @@
@model IBillable
<div class="bill">
@if (ViewBag.BillFileInfo.Exists) {
<a class="btn btn-link" href="~/api/bill/facture-@(ViewBag.BillingCode)-@(Model.Id).pdf" >La facture au format Pdf</a>
} else {
<script>
var onGenerate = function () {
var _btn = "this";
$.post('/api/bill/genpdf/@ViewBag.BillingCode/@Model.Id')
.done(function(data){
var res = JSON.parse(data);
window.location.href = '/api/bill/' + res.Generated;
_btn.disabled = "true";
})
.fail(function( jqXHR, textStatus, errorThrown ) {
$('#billgenerr').html('<em>La génération a échoué</em><p>'+textStatus+'</p>')
})
};
$(document).ready(function(){$('#btnGen').on('click',onGenerate)})
</script>
<div id="billgen">
<form action= method="POST">
<input id="btnGen" class="btn btn-primary" type="button" onclick="onGenerate" value="Générer la facture au format Pdf" />
<div id="billgenerr" class="error">
</div>
</form>
</div>
}
</div>

View file

@ -1,21 +0,0 @@
@using System.Reflection
@using System.IO
@using Microsoft.Extensions.WebEncoders
@using System.Diagnostics
@using System.Text
@using Yavsc.Helpers
@model Yavsc.ViewModels.Gen.PdfGenerationViewModel
@{
SiteSettings.Value.GenerateEstimatePdf(Model);
}
@if (Model.Generated) {
<div>
@(Model.BaseFileName).pdf was generated
</div>
} else {
<div class="error">
@Model.GenerationErrorMessage
<p>Something went wrong ...</p>
</div>
}

View file

@ -49,7 +49,7 @@
\global\let\TotalHT\TotalHT
\global\let\TotalTVA\TotalTVA
\global\let\TotalTTC\TotalTTC
\cr
\cr
\hline
\textbf{Total} & & & \TotalHT
}
@ -77,12 +77,12 @@
% Infos Client
\def\ClientNom{@TeXHelpers.ToTeX(to.UserName)} % Nom du client
\def\ClientAdresse{
\def\ClientAdresse{
% Adresse du client
@PostalAddress
@if (PostalAddress!=null) {<text>@PostalAddress</text>}
@if (!string.IsNullOrWhiteSpace(to.PhoneNumber)) {<text>@TeXHelpers.ToTeX(to.PhoneNumber)<text>\\</text>
</text>}
</text>}
E-mail: @TeXHelpers.ToTeX(to.Email)
}
@ -105,7 +105,7 @@
\renewcommand{\headrulewidth}{0pt}
\cfoot{ @TeXHelpers.ToTeX(from.UserName) @if (proaddrm!=null) { <text> - @proaddrm </text> } \newline
\small{ E-mail: @TeXHelpers.ToTeX(from.Email) @if (!string.IsNullOrWhiteSpace(from.PhoneNumber)) { <text> - Téléphone fixe: @TeXHelpers.ToTeX(from.PhoneNumber) </text> }
\small{ E-mail: @TeXHelpers.ToTeX(from.Email) @if (!string.IsNullOrWhiteSpace(from.PhoneNumber)) { <text> - Téléphone fixe: @TeXHelpers.ToTeX(from.PhoneNumber) </text> }
}
}
@ -113,10 +113,10 @@
% Logo de la société
@if (from.Avatar != null) {
<text>\includegraphics[height=60pt]{@(ViewBag.AvatarsDir)/@(from.UserName).png}
<text>\includegraphics[height=60pt, viewport=0 0 128 128]{@(ViewBag.AvatarsDir)/@(from.UserName).png}
</text>
} else {
<text>%\includegraphics{logo.png}
} else {
<text>%\includegraphics[height=60pt, viewport=0 0 128 128]{logo.png}
</text>
}
% Nom et adresse de la société
@ -170,12 +170,12 @@
\begin{center}
\begin{tabular}{|c c c c|}
@if (!string.IsNullOrWhiteSpace(bi.BankCode) && !string.IsNullOrWhiteSpace(bi.WicketCode)
&& !string.IsNullOrWhiteSpace(bi.AccountNumber) ) {
&& !string.IsNullOrWhiteSpace(bi.AccountNumber) ) {
<text>\hline \textbf{Code banque} & \textbf{Code guichet} & \textbf{N° de Compte} & \textbf{Clé RIB} \\
@bi.BankCode & @bi.WicketCode & @bi.AccountNumber & @bi.BankedKey \\
</text>
}
@if (!string.IsNullOrWhiteSpace(@bi.IBAN) && !string.IsNullOrWhiteSpace(@bi.BIC)) {
@if (!string.IsNullOrWhiteSpace(@bi.IBAN) && !string.IsNullOrWhiteSpace(@bi.BIC)) {
<text>
\hline \textbf{IBAN N°} & \multicolumn{3}{|l|}{ @bi.IBAN } \\
\hline \textbf{Code BIC} & \multicolumn{3}{|l|}{ @bi.BIC }