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 IBillingService billing;
private readonly ILogger logger; private readonly ILogger logger;
private readonly SiteSettings siteSettings; private readonly SiteSettings siteSettings;
private readonly IViewEngine viewEngine; private readonly IRazorViewEngine viewEngine;
// The view-rendering dependencies (siteSettings, viewEngine) are // The view-rendering dependencies (siteSettings, viewEngine) are
// optional: only EstimateTex/EstimatePdf use them, and those run in // optional: only EstimateTex/EstimatePdf use them, and those run in
@ -51,7 +51,7 @@ namespace Yavsc.ApiControllers
this.billing = billing ?? new BillingService(context); this.billing = billing ?? new BillingService(context);
logger = loggerFactory.CreateLogger<FrontOfficeApiController>(); logger = loggerFactory.CreateLogger<FrontOfficeApiController>();
this.siteSettings = siteSettings?.Value; this.siteSettings = siteSettings?.Value;
this.viewEngine = viewEngine as IViewEngine; this.viewEngine = viewEngine;
} }
[HttpGet("profiles/{actCode}")] [HttpGet("profiles/{actCode}")]
@ -97,9 +97,14 @@ namespace Yavsc.ApiControllers
public async Task<IActionResult> EstimatePdf(long queryId, CancellationToken token) public async Task<IActionResult> EstimatePdf(long queryId, CancellationToken token)
{ {
if (viewEngine is null || siteSettings is null) 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); statusCode: StatusCodes.Status503ServiceUnavailable);
}
var estimate = await LoadEstimateForRenderAsync(queryId, token); var estimate = await LoadEstimateForRenderAsync(queryId, token);
if (estimate is null) return NotFound(new { Error = "no estimate linked to this query" }); if (estimate is null) return NotFound(new { Error = "no estimate linked to this query" });
@ -135,21 +140,20 @@ namespace Yavsc.ApiControllers
// we stream the generated PDF bytes — but driving the // we stream the generated PDF bytes — but driving the
// generation through the view keeps the template as the // generation through the view keeps the template as the
// output producer, per the original design. // 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) catch (Exception ex)
{ {
logger.LogError(ex, "estimate {QueryId}: TeX/PDF render failed", queryId); logger.LogError(ex, "estimate {QueryId}: TeX/PDF render failed", queryId);
return Problem("TeX render failed: " + ex.Message); 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 // Loads the estimate linked to a query with the navigation the

View file

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

@ -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

@ -113,10 +113,10 @@
% Logo de la société % Logo de la société
@if (from.Avatar != null) { @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> </text>
} else { } else {
<text>%\includegraphics{logo.png} <text>%\includegraphics[height=60pt, viewport=0 0 128 128]{logo.png}
</text> </text>
} }
% Nom et adresse de la société % Nom et adresse de la société