Estimate validation polish + PDF/TeX generation in Yavsc.Api
- List removal: GetOngoingEstimatesAsProvider filters on ProviderValidationDate so a provider's signed estimate leaves their list; PostIt refreshes the list on navigation pop-back. - Title renames: "Devis en attente de signature fournisseur/client" on both the provider and client sides (HomePage, VM, tests). - Move the estimate/bill TeX+PDF generation flow from Yavsc.Org into Yavsc.Api (TeXHelpers, PdfGenerationViewModel, BillViewComponent, Bill/Estimate templates, _ViewImports). Program.cs wires AddControllersWithViews + SiteSettings; FrontOfficeApiController injects IRazorViewEngine (cast to IViewEngine) with optional deps and exposes estimate.tex/estimate.pdf endpoints keyed by query id. - PDF generation drives through the Estimate_pdf view (side-effect render) and compiles via the system lualatex, fed TeX on stdin (no intermediate .tex), PDF to the bills dir, aux artifacts cleaned so only the .pdf persists. Resolve the bills dir to an absolute path so lualatex's -output-directory and working directory agree regardless of the host cwd (fixes "I can't write on file estimate-1.log"). - PostIt: DownloadAsync on the client + GetEstimateTex/PdfAsync; Télécharger PDF/TeX buttons on the validation and command-detail pages via a StorageProvider save helper. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
70a123b840
commit
97d0584292
41 changed files with 1036 additions and 290 deletions
|
|
@ -8,7 +8,6 @@ namespace Yavsc.Controllers
|
|||
using Helpers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Models;
|
||||
using ViewModels.FrontOffice;
|
||||
using Yavsc.Server.Helpers;
|
||||
|
|
@ -21,22 +20,20 @@ namespace Yavsc.Controllers
|
|||
readonly ILogger _logger;
|
||||
readonly IStringLocalizer _SR;
|
||||
private readonly IBillingService _billing;
|
||||
private readonly SiteSettings _siteSettings;
|
||||
|
||||
public FrontOfficeController(ApplicationDbContext context,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IBillingService billing,
|
||||
ILoggerFactory loggerFactory,
|
||||
IStringLocalizer<FrontOfficeController> SR,
|
||||
IOptions<SiteSettings> siteSettings)
|
||||
IStringLocalizer<FrontOfficeController> SR)
|
||||
{
|
||||
_context = context;
|
||||
_userManager = userManager;
|
||||
_logger = loggerFactory.CreateLogger<FrontOfficeController>();
|
||||
_SR = SR;
|
||||
_billing = billing;
|
||||
_siteSettings = siteSettings.Value;
|
||||
}
|
||||
|
||||
public ActionResult Index()
|
||||
{
|
||||
var uid = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
|
|
@ -80,61 +77,5 @@ namespace Yavsc.Controllers
|
|||
return View(result);
|
||||
}
|
||||
|
||||
|
||||
[Produces("text/x-tex"), Authorize, Route("estimate-{id}.tex")]
|
||||
[HttpGet]
|
||||
public ViewResult EstimateTex(long id)
|
||||
{
|
||||
var estimate = _context.Estimates.Include(x => x.Query)
|
||||
.Include(x => x.Query.Client)
|
||||
.Include(x => x.Query.PerformerProfile)
|
||||
.Include(x => x.Query.PerformerProfile.OrganizationAddress)
|
||||
.Include(x => x.Query.PerformerProfile.Performer)
|
||||
.Include(e => e.Bill).FirstOrDefault(x => x.Id == id);
|
||||
Response.ContentType = "text/x-tex";
|
||||
return View("Estimate.tex", estimate);
|
||||
}
|
||||
|
||||
[Authorize, Route("Estimate-{id}.pdf")]
|
||||
[HttpGet]
|
||||
public IActionResult EstimatePdf(long id)
|
||||
{
|
||||
ViewBag.TempDir = _siteSettings.TempDir;
|
||||
ViewBag.BillsDir = _siteSettings.Bills;
|
||||
var estimate = _context.Estimates.Include(x => x.Query)
|
||||
.Include(x => x.Query.Client)
|
||||
.Include(x => x.Query.PerformerProfile)
|
||||
.Include(x => x.Query.PerformerProfile.OrganizationAddress)
|
||||
.Include(x => x.Query.PerformerProfile.Performer)
|
||||
.Include(e => e.Bill).FirstOrDefault(x => x.Id == id);
|
||||
if (estimate == null)
|
||||
throw new Exception("No data");
|
||||
return View("Estimate.pdf", estimate);
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
public IActionResult EstimateProValidation()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
public IActionResult EstimateClientValidation()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
||||
}
|
||||
[Authorize]
|
||||
public IActionResult BillValidation()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
||||
}
|
||||
[Authorize]
|
||||
public IActionResult BillAcquitment()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,198 +0,0 @@
|
|||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||
|
||||
namespace Yavsc.Helpers
|
||||
{
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using ViewModels.Gen;
|
||||
public class TeXString : HtmlString
|
||||
{
|
||||
|
||||
public TeXString(TeXString teXString): base(teXString.ToString())
|
||||
{
|
||||
}
|
||||
|
||||
public TeXString(string str) : base(str)
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static TeXString operator+ (TeXString a, TeXString b) {
|
||||
return new TeXString(a.ToString()+b.ToString());
|
||||
}
|
||||
|
||||
}
|
||||
public class Replacement
|
||||
{
|
||||
string target;
|
||||
string replacement;
|
||||
public Replacement(string target, string replacement)
|
||||
{
|
||||
this.target = target;
|
||||
this.replacement = replacement;
|
||||
}
|
||||
public string Execute(string source)
|
||||
{
|
||||
return source?.Replace(target, replacement) ?? null;
|
||||
}
|
||||
}
|
||||
public static class TeXHelpers
|
||||
{
|
||||
public static readonly Replacement[] SpecialCharsDefaultRendering =
|
||||
{
|
||||
new Replacement("<","\\textless"),
|
||||
new Replacement(">","\\textgreater"),
|
||||
new Replacement("©","\\copyright"),
|
||||
new Replacement("®","\\textregistered"),
|
||||
new Replacement("\\","\\textbackslash"),
|
||||
new Replacement("™","\\texttrademark"),
|
||||
new Replacement("¶","\\P"),
|
||||
new Replacement("|","\\textbar"),
|
||||
new Replacement("%","\\%"),
|
||||
new Replacement("{","\\{"),
|
||||
new Replacement("}","\\}"),
|
||||
new Replacement("_","\\_"),
|
||||
new Replacement("#","\\#"),
|
||||
new Replacement("$","\\$"),
|
||||
new Replacement("_","\\_"),
|
||||
new Replacement("¿","\\textquestiondown"),
|
||||
new Replacement("§","\\S"),
|
||||
new Replacement("£","\\pounds"),
|
||||
new Replacement("&","\\&"),
|
||||
new Replacement("¡","\\textexclamdown"),
|
||||
new Replacement("†","\\dag"),
|
||||
new Replacement("–","\\textendash"),
|
||||
new Replacement("°","\\textdegree")
|
||||
};
|
||||
|
||||
public static TeXString ToTeX(this string source, string defaultValue="\\textit{néant}")
|
||||
{
|
||||
if (source==null) return new TeXString(defaultValue);
|
||||
string result=source;
|
||||
foreach (var r in SpecialCharsDefaultRendering)
|
||||
{
|
||||
result = r.Execute(result);
|
||||
}
|
||||
return new TeXString(result);
|
||||
}
|
||||
|
||||
public static TeXString ToTeXCell(this string source, string defaultValue="\\textit{néant}")
|
||||
{
|
||||
if (source==null) return new TeXString(defaultValue);
|
||||
string result=source;
|
||||
foreach (var r in SpecialCharsDefaultRendering)
|
||||
{
|
||||
result = r.Execute(result);
|
||||
}
|
||||
result = result.Replace("\n","\\tabularnewline ");
|
||||
return new TeXString(result);
|
||||
}
|
||||
|
||||
|
||||
public static string NewLinesWith(this string target, string separator)
|
||||
{
|
||||
var items = target.Split(new char[] { '\n' }).Where(
|
||||
s => !string.IsNullOrWhiteSpace(s));
|
||||
|
||||
return string.Join(separator, items);
|
||||
}
|
||||
|
||||
public static TeXString ToTeXLines(this string source, string defaultValue, string lineSeparator = "\n\\\\")
|
||||
{
|
||||
if (source == null) return new TeXString(defaultValue);
|
||||
return new TeXString( source.ToTeX().ToString().NewLinesWith(lineSeparator) );
|
||||
}
|
||||
|
||||
public static TeXString SplitAddressToTeX (this string source, string lineSeparator = "\n\\\\", string defaultValue = "\\textit{pas d'adresse postale}")
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(source)) return new TeXString(defaultValue);
|
||||
var alines = source.Split(',');
|
||||
var texlines = alines.Select(l=>l.ToTeX().ToString());
|
||||
return new TeXString(string.Join(lineSeparator,texlines));
|
||||
}
|
||||
|
||||
public static bool GenerateEstimatePdf(this SiteSettings settings, PdfGenerationViewModel Model)
|
||||
{
|
||||
string errorMsg = null;
|
||||
var billdir = Model.DestDir;
|
||||
var tempdir = settings.TempDir;
|
||||
string name = Model.BaseFileName;
|
||||
string fullname = new FileInfo(
|
||||
System.IO.Path.Combine(tempdir, name)).FullName;
|
||||
string ofullname = new FileInfo(
|
||||
System.IO.Path.Combine(billdir, name)).FullName;
|
||||
|
||||
FileInfo fi = new FileInfo(fullname + ".tex");
|
||||
FileInfo fo = new FileInfo(ofullname + ".pdf");
|
||||
using (StreamWriter sw = new StreamWriter(fi.FullName))
|
||||
{
|
||||
sw.Write(Model.TeXSource);
|
||||
}
|
||||
if (!fi.Exists)
|
||||
{
|
||||
errorMsg = "Source write failed";
|
||||
}
|
||||
else
|
||||
{
|
||||
using (Process p = new Process())
|
||||
{
|
||||
p.StartInfo.WorkingDirectory = tempdir;
|
||||
p.StartInfo = new ProcessStartInfo
|
||||
{
|
||||
UseShellExecute = false,
|
||||
WorkingDirectory = tempdir,
|
||||
FileName = "/usr/bin/texi2pdf",
|
||||
Arguments = $"--batch --build-dir=. -o {fo.FullName} {fi.FullName}"
|
||||
};
|
||||
p.Start();
|
||||
p.WaitForExit();
|
||||
if (p.ExitCode != 0)
|
||||
{
|
||||
errorMsg = $"Pdf generation failed with exit code: {p.ExitCode}";
|
||||
}
|
||||
else
|
||||
{
|
||||
fi.Delete();
|
||||
var di = new DirectoryInfo(Path.Combine(tempdir,$"{Model.BaseFileName}.t2d"));
|
||||
di.Delete(true);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Model.Generated = fo.Exists;
|
||||
Model.GenerationErrorMessage = new HtmlString(errorMsg);
|
||||
return fo.Exists;
|
||||
}
|
||||
|
||||
public static string RenderViewToString(
|
||||
this Controller controller, IViewEngine engine,
|
||||
IActionContextAccessor contextAccessor,
|
||||
string viewName, object model, bool isMainPage = true)
|
||||
{
|
||||
using (var sw = new StringWriter())
|
||||
{
|
||||
if (engine == null)
|
||||
throw new InvalidOperationException("no engine");
|
||||
|
||||
// try to find the specified view
|
||||
controller.TryValidateModel(model);
|
||||
ViewEngineResult viewResult = engine.FindView(contextAccessor.ActionContext, viewName, isMainPage);
|
||||
|
||||
// create the associated context
|
||||
ViewContext viewContext = new ViewContext();
|
||||
viewContext.ActionDescriptor = contextAccessor.ActionContext.ActionDescriptor;
|
||||
viewContext.HttpContext = contextAccessor.ActionContext.HttpContext;
|
||||
viewContext.TempData = controller.TempData;
|
||||
viewContext.View = viewResult.View;
|
||||
viewContext.Writer = sw;
|
||||
// write the render view with the given context to the stringwriter
|
||||
viewResult.View.RenderAsync(viewContext).Wait();
|
||||
return sw.GetStringBuilder().ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
|
||||
namespace Yavsc.ViewModels.Gen
|
||||
{
|
||||
public class PdfGenerationViewModel
|
||||
{
|
||||
|
||||
[Required]
|
||||
public string TeXSource { get; set; }
|
||||
[Required]
|
||||
public string BaseFileName { get; set; }
|
||||
[Required]
|
||||
public string DestDir { get; set; }
|
||||
public bool Generated { get; set; }
|
||||
public HtmlString GenerationErrorMessage { get; set; }
|
||||
public string Temp { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
@using Yavsc.Helpers
|
||||
@using System.IO
|
||||
@using System.Globalization
|
||||
@model IBillable
|
||||
@{
|
||||
Layout = "null";
|
||||
var pro = ViewBag.PerformerProfile;
|
||||
var from = ViewBag.Performer;
|
||||
var to = ViewBag.Client;
|
||||
var PostalAddress = ViewBag.ClientAddress;
|
||||
var proaddr = ViewBag.PerformerOrganizationAddress;
|
||||
var isestimate = !ViewBag.AsBill;
|
||||
var prosign = new FileInfo($"{ViewBag.BillsDir}/sign-{ViewBag.BillingCode}-{Model.Id}.png");
|
||||
var clisign = new FileInfo($"{ViewBag.BillsDir}/sign-{ViewBag.BillingCode}-{Model.Id}.png");
|
||||
var activity = ViewBag.ActivityLabel;
|
||||
var bill = Model.GetBillItems();
|
||||
string validationDate = null ;
|
||||
if (Model.ValidationDate.HasValue) {
|
||||
validationDate = Model.ValidationDate.Value.ToString("dddd dd MMMM yyyy", CultureInfo.CreateSpecificCulture("fr-FR"));
|
||||
}
|
||||
}
|
||||
\documentclass[french,11pt]{article}
|
||||
\usepackage{eurosym}
|
||||
\usepackage{textcomp}
|
||||
\usepackage{babel}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[a4paper]{geometry}
|
||||
\usepackage{units}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{fancyhdr}
|
||||
\usepackage{fp}
|
||||
\usepackage[space]{grffile}
|
||||
\def\TVA{20} % Taux de la TVA
|
||||
|
||||
\def\TotalHT{0}
|
||||
\def\TotalTVA{0}
|
||||
|
||||
\newcommand{\AjouterService}[3]{% Arguments : Désignation, quantité, prix
|
||||
\FPround{\prix}{#3}{2}
|
||||
\FPeval{\montant}{#2 * #3}
|
||||
\FPround{\montant}{\montant}{2}
|
||||
\FPadd{\TotalHT}{\TotalHT}{\montant}
|
||||
\eaddto\ListeProduits{#1 & \prix & #2 & \montant \cr}
|
||||
}
|
||||
|
||||
\newcommand{\AfficheResultat}{%
|
||||
\ListeProduits
|
||||
\FPeval{\TotalTVA}{\TotalHT * \TVA / 100}
|
||||
\FPadd{\TotalTTC}{\TotalHT}{\TotalTVA}
|
||||
\FPround{\TotalHT}{\TotalHT}{2}
|
||||
\FPround{\TotalTVA}{\TotalTVA}{2}
|
||||
\FPround{\TotalTTC}{\TotalTTC}{2}
|
||||
\global\let\TotalHT\TotalHT
|
||||
\global\let\TotalTVA\TotalTVA
|
||||
\global\let\TotalTTC\TotalTTC
|
||||
\cr
|
||||
\hline
|
||||
\textbf{Total} & & & \TotalHT
|
||||
}
|
||||
|
||||
\newcommand*\eaddto[2]{% version développée de \addto
|
||||
\edef\tmp{#2}%
|
||||
\expandafter\addto
|
||||
\expandafter#1%
|
||||
\expandafter{\tmp}%
|
||||
}
|
||||
|
||||
\newcommand{\ListeProduits}{}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%% A MODIFIER DANS LA FACTURE %%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
\def\FactureNum {@Model.Id.ToString()} % Numéro de facture
|
||||
\def\FactureAcquittee {@(ViewBag.Acquitted?"oui":"non")} % Facture acquittée : oui/non
|
||||
\def\FactureLieu {@proaddr} % Lieu de l'édition de la facture
|
||||
\def\FactureObjet {@(new HtmlString(isestimate?"Devis":"Facture")) en @TeXHelpers.ToTeX(activity)} % Objet du document
|
||||
% Description de la facture
|
||||
\def\FactureDescr {
|
||||
@TeXHelpers.ToTeX(Model.Description)
|
||||
}
|
||||
|
||||
% Infos Client
|
||||
\def\ClientNom{@TeXHelpers.ToTeX(to.UserName)} % Nom du client
|
||||
|
||||
\def\ClientAdresse{
|
||||
% Adresse du client
|
||||
@PostalAddress @if (!string.IsNullOrWhiteSpace(to.PhoneNumber)) {<text>\\
|
||||
@TeXHelpers.ToTeX(to.PhoneNumber)
|
||||
</text>}\\
|
||||
E-mail: @TeXHelpers.ToTeX(to.Email)
|
||||
}
|
||||
|
||||
% Liste des produits facturés : Désignation, prix
|
||||
@foreach (var line in bill) {
|
||||
<text>\AjouterService{@TeXHelpers.ToTeXCell(line.Description)}{@line.Count}{@line.UnitaryCost.ToString("F2",CultureInfo.InvariantCulture)}
|
||||
</text>}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
|
||||
|
||||
\geometry{verbose,tmargin=4em,bmargin=8em,lmargin=6em,rmargin=6em}
|
||||
\setlength{\parindent}{0pt}
|
||||
\setlength{\parskip}{1ex plus 0.5ex minus 0.2ex}
|
||||
|
||||
\thispagestyle{fancy}
|
||||
\pagestyle{fancy}
|
||||
\setlength{\parindent}{0pt}
|
||||
|
||||
\renewcommand{\headrulewidth}{0pt}
|
||||
\cfoot{ @TeXHelpers.ToTeX(from.UserName) - @ViewBag.FooterPerformerOrganizationAddress \\
|
||||
\small{ E-mail: @TeXHelpers.ToTeX(from.Email) @if (!string.IsNullOrWhiteSpace(from.PhoneNumber)) { <text> - Téléphone : @TeXHelpers.ToTeX(from.PhoneNumber) </text> }
|
||||
}
|
||||
}
|
||||
|
||||
\begin{document}
|
||||
|
||||
% Logo de la société
|
||||
@if (from.Avatar != null) {
|
||||
<text>\includegraphics[height=60pt]{@(ViewBag.AvatarsDir)/@(from.UserName).png}
|
||||
</text>
|
||||
} else {
|
||||
<text>%\includegraphics{logo.png}
|
||||
</text>
|
||||
}
|
||||
% Nom et adresse de la société
|
||||
@TeXHelpers.ToTeX(from.UserName) \\
|
||||
@proaddr
|
||||
|
||||
@(isestimate?"Devis":"Facture") n\textdegree\FactureNum
|
||||
|
||||
|
||||
{\addtolength{\leftskip}{10.5cm} %in ERT
|
||||
\textbf{\ClientNom} \\
|
||||
\ClientAdresse \\
|
||||
|
||||
} %in ERT
|
||||
|
||||
|
||||
\hspace*{10.5cm}
|
||||
\FactureLieu, le \today
|
||||
|
||||
~\\~\\
|
||||
|
||||
\textbf{Objet : \FactureObjet \\}
|
||||
|
||||
\textnormal{\FactureDescr}
|
||||
|
||||
~\\
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{lrrr}
|
||||
\textbf{Désignation ~~~~~~} & \textbf{Prix unitaire} & \textbf{Quantité} & \textbf{Montant (\euro)} \\
|
||||
\hline
|
||||
\AfficheResultat{}
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
\begin{flushright}
|
||||
\textit{Auto entreprise en franchise de TVA}\\
|
||||
|
||||
\end{flushright}
|
||||
~\\
|
||||
@if (ViewBag.AsBill) {
|
||||
if (ViewBag.Acquitted) {
|
||||
<text>
|
||||
\ifthenelse{\equal{\FactureAcquittee}{oui}}{
|
||||
Facture acquittée.
|
||||
}
|
||||
</text>
|
||||
} else {
|
||||
var bi = from.BankInfo;
|
||||
if (bi==null){
|
||||
<text>À régler sur site, en utilisant le paiment PayPal:\\
|
||||
Le relevé d'identité bancaire de ce prestaire n'est pas renseigné.
|
||||
</text>
|
||||
} else {
|
||||
<text>À régler par chèque ou par virement bancaire :
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{|c c c c|}
|
||||
@if (!string.IsNullOrWhiteSpace(bi.BankCode) && !string.IsNullOrWhiteSpace(bi.WicketCode)
|
||||
&& !string.IsNullOrWhiteSpace(bi.AccountNumber) ) {
|
||||
<text>\hline \textbf{Code banque} & \textbf{Code guichet} & \textbf{N\textdegree de Compte} & \textbf{Clé RIB} \\
|
||||
@bi.BankCode & @bi.WicketCode & @bi.AccountNumber & @bi.BankedKey \\
|
||||
</text>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(@bi.IBAN) && !string.IsNullOrWhiteSpace(@bi.BIC)) {
|
||||
<text>
|
||||
\hline \textbf{IBAN N\textdegree} & \multicolumn{3}{|l|}{ @bi.IBAN } \\
|
||||
\hline \textbf{Code BIC} & \multicolumn{3}{|l|}{ @bi.BIC }
|
||||
</text>
|
||||
} \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}</text>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@if (validationDate!=null) {
|
||||
<text>
|
||||
\begin{flushright}
|
||||
@(new TeXString(isestimate?"Devis demandé le ":"Facture suite à la demande du ")+
|
||||
TeXHelpers.ToTeX(validationDate))
|
||||
\end{flushright}
|
||||
@if (prosign.Exists) {
|
||||
<text>
|
||||
\begin{center}
|
||||
\hspace{263pt}
|
||||
\includegraphics[height=60pt]{@(ViewBag.BillsDir)/{ViewBag.BillingCode}-prosign-@(Model.Id).png}
|
||||
\end{center}
|
||||
</text>
|
||||
}
|
||||
|
||||
@if (clisign.Exists) {
|
||||
<text>
|
||||
\begin{center}
|
||||
\hspace{263pt}
|
||||
\includegraphics[height=60pt]{@(ViewBag.BillsDir)/{ViewBag.BillingCode}-clisign-@(Model.Id).png}
|
||||
\end{center}
|
||||
</text>
|
||||
}
|
||||
|
||||
</text>
|
||||
}
|
||||
\end{document}
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
|
||||
@model Estimate
|
||||
<div class="estimate">
|
||||
|
||||
<a href="~/api/pdfbill/estimate-@(Model.Id).tex" >Export au format LaTeX</a>
|
||||
|
||||
|
||||
<form action="~/api/pdfbill/gen/@Model.Id" method="POST">
|
||||
<input type="submit" value="Générer le Pdf"/>
|
||||
</form>
|
||||
|
||||
<a href="~/api/pdfbill/get/@(Model.Id)" >Télécharger le document généré</a>
|
||||
</div>
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
@using Yavsc.Helpers
|
||||
@using System.IO
|
||||
@using System.Globalization
|
||||
@model Estimate
|
||||
@{
|
||||
Layout = "null";
|
||||
var pro = Model.Query.PerformerProfile;
|
||||
var from = Model.Query.PerformerProfile.Performer;
|
||||
var to = Model.Query.Client;
|
||||
var PostalAddress = to.PostalAddress?.Address.SplitAddressToTeX();
|
||||
var proaddr = Model.Query.PerformerProfile.OrganizationAddress.Address.SplitAddressToTeX();
|
||||
var proaddrm = new HtmlString(Model.Query.PerformerProfile.OrganizationAddress.Address);
|
||||
var isestimate = !ViewBag.AsBill;
|
||||
var prosign = new FileInfo($"{ViewBag.BillsDir}/estimate-prosign-{Model.Id}.png");
|
||||
|
||||
}
|
||||
\documentclass[french,11pt]{article}
|
||||
\usepackage{eurosym}
|
||||
\usepackage{babel}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[a4paper]{geometry}
|
||||
\usepackage{units}
|
||||
\usepackage{bera}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{fancyhdr}
|
||||
\usepackage{fp}
|
||||
|
||||
\def\TVA{20} % Taux de la TVA
|
||||
|
||||
\def\TotalHT{0}
|
||||
\def\TotalTVA{0}
|
||||
|
||||
\newcommand{\AjouterService}[3]{% Arguments : Désignation, quantité, prix
|
||||
\FPround{\prix}{#3}{2}
|
||||
\FPeval{\montant}{#2 * #3}
|
||||
\FPround{\montant}{\montant}{2}
|
||||
\FPadd{\TotalHT}{\TotalHT}{\montant}
|
||||
\eaddto\ListeProduits{#1 & \prix & #2 & \montant \cr}
|
||||
}
|
||||
|
||||
\newcommand{\AfficheResultat}{%
|
||||
\ListeProduits
|
||||
\FPeval{\TotalTVA}{\TotalHT * \TVA / 100}
|
||||
\FPadd{\TotalTTC}{\TotalHT}{\TotalTVA}
|
||||
\FPround{\TotalHT}{\TotalHT}{2}
|
||||
\FPround{\TotalTVA}{\TotalTVA}{2}
|
||||
\FPround{\TotalTTC}{\TotalTTC}{2}
|
||||
\global\let\TotalHT\TotalHT
|
||||
\global\let\TotalTVA\TotalTVA
|
||||
\global\let\TotalTTC\TotalTTC
|
||||
\cr
|
||||
\hline
|
||||
\textbf{Total} & & & \TotalHT
|
||||
}
|
||||
|
||||
\newcommand*\eaddto[2]{% version développée de \addto
|
||||
\edef\tmp{#2}%
|
||||
\expandafter\addto
|
||||
\expandafter#1%
|
||||
\expandafter{\tmp}%
|
||||
}
|
||||
|
||||
\newcommand{\ListeProduits}{}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%% A MODIFIER DANS LA FACTURE %%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
\def\FactureNum {@Model.Id.ToString()} % Numéro de facture
|
||||
\def\FactureAcquittee {@ViewBag.Acquitted?"oui":"non"} % Facture acquittée : oui/non
|
||||
\def\FactureLieu {@proaddrm} % Lieu de l'édition de la facture
|
||||
\def\FactureObjet {@(new HtmlString(isestimate?"Devis":"Facture")) : @TeXHelpers.ToTeX(Model.Title)} % Objet du document
|
||||
% Description de la facture
|
||||
\def\FactureDescr {
|
||||
@TeXHelpers.ToTeX(Model.Description)
|
||||
}
|
||||
|
||||
% Infos Client
|
||||
\def\ClientNom{@TeXHelpers.ToTeX(to.UserName)} % Nom du client
|
||||
|
||||
\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>}
|
||||
E-mail: @TeXHelpers.ToTeX(to.Email)
|
||||
}
|
||||
|
||||
% Liste des produits facturés : Désignation, prix
|
||||
@if (Model.Bill!=null) { foreach (CommandLine line in Model.Bill) {
|
||||
<text>\AjouterService{@TeXHelpers.ToTeX(line.Description)}{@line.Count}{@line.UnitaryCost.ToString("F2",CultureInfo.InvariantCulture)}
|
||||
</text>} }
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
|
||||
|
||||
|
||||
\geometry{verbose,tmargin=4em,bmargin=8em,lmargin=6em,rmargin=6em}
|
||||
\setlength{\parindent}{0pt}
|
||||
\setlength{\parskip}{1ex plus 0.5ex minus 0.2ex}
|
||||
|
||||
\thispagestyle{fancy}
|
||||
\pagestyle{fancy}
|
||||
\setlength{\parindent}{0pt}
|
||||
|
||||
\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> }
|
||||
}
|
||||
}
|
||||
|
||||
\begin{document}
|
||||
|
||||
% Logo de la société
|
||||
@if (from.Avatar != null) {
|
||||
<text>\includegraphics[height=60pt]{@(ViewBag.AvatarsDir)/@(from.UserName).png}
|
||||
</text>
|
||||
} else {
|
||||
<text>%\includegraphics{logo.png}
|
||||
</text>
|
||||
}
|
||||
% Nom et adresse de la société
|
||||
@TeXHelpers.ToTeX(from.UserName) \\
|
||||
@proaddr
|
||||
|
||||
@(isestimate?"Devis":"Facture") n°\FactureNum
|
||||
|
||||
|
||||
{\addtolength{\leftskip}{10.5cm} %in ERT
|
||||
\textbf{\ClientNom} \\
|
||||
\ClientAdresse \\
|
||||
|
||||
} %in ERT
|
||||
|
||||
|
||||
\hspace*{10.5cm}
|
||||
\FactureLieu, le \today
|
||||
|
||||
~\\~\\
|
||||
|
||||
\textbf{Objet : \FactureObjet \\}
|
||||
|
||||
\textnormal{\FactureDescr}
|
||||
|
||||
~\\
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{lrrr}
|
||||
\textbf{Désignation ~~~~~~} & \textbf{Prix unitaire} & \textbf{Quantité} & \textbf{Montant (\euro)} \\
|
||||
\hline
|
||||
\AfficheResultat{}
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
\begin{flushright}
|
||||
\textit{Auto entreprise en franchise de TVA}\\
|
||||
|
||||
\end{flushright}
|
||||
~\\
|
||||
@if (!isestimate) {
|
||||
<text>
|
||||
\ifthenelse{\equal{\FactureAcquittee}{oui}}{
|
||||
Facture acquittée.
|
||||
}{ </text>
|
||||
|
||||
if (from.BankInfo!=null) foreach (var bi in from.BankInfo){
|
||||
<text>À régler par chèque ou par virement bancaire :
|
||||
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{|c c c c|}
|
||||
@if (!string.IsNullOrWhiteSpace(bi.BankCode) && !string.IsNullOrWhiteSpace(bi.WicketCode)
|
||||
&& !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)) {
|
||||
<text>
|
||||
\hline \textbf{IBAN N°} & \multicolumn{3}{|l|}{ @bi.IBAN } \\
|
||||
\hline \textbf{Code BIC} & \multicolumn{3}{|l|}{ @bi.BIC }
|
||||
</text>
|
||||
} \\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}</text>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@if (!(Model.ProviderValidationDate==null)) {
|
||||
<text>
|
||||
\begin{flushright}
|
||||
@(new HtmlString(isestimate?"Devis validé":"Facture validée")) le @TeXHelpers.ToTeX(Model.ProviderValidationDate.ToString("dddd dd MMMM yyyy"))
|
||||
\end{flushright}
|
||||
@if (prosign.Exists) {
|
||||
<text>
|
||||
\begin{center}
|
||||
\hspace{263pt}
|
||||
\includegraphics[height=60pt]{@(ViewBag.BillsDir)/estimate-prosign-@(Model.Id).png}
|
||||
\end{center}
|
||||
</text>
|
||||
}
|
||||
</text>
|
||||
}
|
||||
\end{document}
|
||||
Loading…
Reference in a new issue