This commit is contained in:
David Arranz 2026-02-13 16:26:50 +01:00
parent 770fb33bb0
commit aaf7d22374
38 changed files with 219 additions and 337 deletions

2
.vscode/launch.json vendored
View File

@ -1,5 +1,5 @@
{
"version": "0.4.8",
"version": "0.5.0",
"configurations": [
{
"name": "WEB: Vite (Chrome)",

View File

@ -1,6 +1,6 @@
{
"name": "@erp/factuges-server",
"version": "0.4.8",
"version": "0.5.0",
"private": true,
"scripts": {
"build": "tsup src/index.ts --config tsup.config.ts",

View File

@ -1,7 +1,7 @@
{
"name": "@erp/factuges-web",
"private": true,
"version": "0.4.8",
"version": "0.5.0",
"type": "module",
"scripts": {
"dev": "vite --host --clearScreen false",

View File

@ -1,6 +1,6 @@
{
"name": "@erp/auth",
"version": "0.4.8",
"version": "0.5.0",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@erp/core",
"version": "0.4.8",
"version": "0.5.0",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -0,0 +1,8 @@
export interface IDocumentProperties {
readonly title?: string;
readonly author?: string;
readonly subject?: string;
readonly keywords?: string;
readonly creator?: string;
readonly producer?: string;
}

View File

@ -1,3 +1,4 @@
export * from "./company-certificate-context.interface";
export * from "./document.interface";
export * from "./document-metadata.interface";
export * from "./document-properties.interface";

View File

@ -2,6 +2,7 @@ import { ApplicationError } from "@repo/rdx-ddd";
type DocumentGenerationErrorCode =
| "METADATA_ERROR"
| "PROPERTIES_ERROR"
| "CACHE_ERROR" // no fatal (solo para métricas/logs si se usa)
| "RENDER_ERROR"
| "POST_PROCESS_ERROR"
@ -30,6 +31,10 @@ export class DocumentGenerationError extends ApplicationError {
return new DocumentGenerationError("Invalid document metadata", "METADATA_ERROR", cause);
}
static properties(cause: unknown) {
return new DocumentGenerationError("Invalid document properties", "PROPERTIES_ERROR", cause);
}
static render(cause: unknown) {
return new DocumentGenerationError("Document render failed", "RENDER_ERROR", cause);
}

View File

@ -1,7 +1,7 @@
import { logger } from "@erp/core/api";
import { type IDocumentPropertiesFactory, logger } from "@erp/core/api";
import { Result } from "@repo/rdx-utils";
import type { IDocument, IDocumentMetadata } from "../application-models";
import type { IDocument, IDocumentMetadata, IDocumentProperties } from "../application-models";
import { DocumentGenerationError } from "../errors";
import type { IDocumentMetadataFactory } from "./document-metadata-factory.interface";
@ -21,30 +21,54 @@ import type { IDocumentSideEffect } from "./document-side-effect.interface";
* 5. Side-effects (persistencia, métricas) [best-effort]
*/
export type DocumentGenerationServiceDeps<TSnapshot> = {
renderer: IDocumentRenderer<TSnapshot>;
metadataFactory: IDocumentMetadataFactory<TSnapshot>;
propertiesFactory: IDocumentPropertiesFactory<TSnapshot>;
preProcessors: readonly IDocumentPreProcessor[];
postProcessor: IDocumentPostProcessor;
sideEffects: readonly IDocumentSideEffect[];
};
export type DocumentGenerationServiceRenderParams = Record<string, string>;
export class DocumentGenerationService<TSnapshot> {
constructor(
private readonly metadataFactory: IDocumentMetadataFactory<TSnapshot>,
private readonly preProcessors: readonly IDocumentPreProcessor[],
private readonly renderer: IDocumentRenderer<TSnapshot>,
private readonly postProcessor: IDocumentPostProcessor,
private readonly sideEffects: readonly IDocumentSideEffect[]
) {}
private readonly renderer: IDocumentRenderer<TSnapshot>;
private readonly metadataFactory: IDocumentMetadataFactory<TSnapshot>;
private readonly propertiesFactory: IDocumentPropertiesFactory<TSnapshot>;
private readonly preProcessors: readonly IDocumentPreProcessor[];
private readonly postProcessor: IDocumentPostProcessor;
private readonly sideEffects: readonly IDocumentSideEffect[];
constructor(deps: DocumentGenerationServiceDeps<TSnapshot>) {
this.renderer = deps.renderer;
this.metadataFactory = deps.metadataFactory;
this.propertiesFactory = deps.propertiesFactory;
this.preProcessors = deps.preProcessors;
this.postProcessor = deps.postProcessor;
this.sideEffects = deps.sideEffects;
}
async generate(
snapshot: TSnapshot,
params: DocumentGenerationServiceRenderParams
): Promise<Result<IDocument, DocumentGenerationError>> {
let metadata: IDocumentMetadata;
let properties: IDocumentProperties;
// 1. Metadata
// 1. Metadata and Properties
try {
metadata = this.metadataFactory.build(snapshot);
} catch (error) {
return Result.fail(DocumentGenerationError.metadata(error));
}
try {
properties = this.propertiesFactory.build(snapshot);
} catch (error) {
return Result.fail(DocumentGenerationError.properties(error));
}
// 2. Pre-processors (cache / short-circuit)
for (const preProcessor of this.preProcessors) {
try {
@ -66,6 +90,7 @@ export class DocumentGenerationService<TSnapshot> {
languageCode: metadata.languageCode,
filename: metadata.filename,
mimeType: metadata.format === "PDF" ? "application/pdf" : "text/html",
properties,
});
} catch (error) {
return Result.fail(DocumentGenerationError.render(error));

View File

@ -0,0 +1,5 @@
import type { IDocumentProperties } from "../application-models";
export interface IDocumentPropertiesFactory<TSnapshot> {
build(snapshot: TSnapshot): IDocumentProperties;
}

View File

@ -4,6 +4,7 @@ export * from "./document-metadata-factory.interface";
export * from "./document-post-processor.interface";
export * from "./document-post-processor-chain";
export * from "./document-pre-processor.interface";
export * from "./document-properties-factory.interface";
export * from "./document-renderer.interface";
export * from "./document-side-effect.interface";
export * from "./document-signing-service.interface";

View File

@ -4,6 +4,7 @@ import fs from "node:fs/promises";
import { Result } from "@repo/rdx-utils";
import { FastReportExecutionError } from "./fastreport-errors";
import type { FastReportRenderReportProperties } from "./fastreport-render-report-properties.type";
/**
* Ejecuta el binario FastReport como proceso externo.
@ -17,8 +18,9 @@ import { FastReportExecutionError } from "./fastreport-errors";
export type FastReportProcessRunnerArgs = {
templatePath: string; // Path to FRX template (required)
data: string; // JSON data as string
format: "PDF" | "HTML";
format: string;
output: string; // Directorio de trabajo temporal
properties: FastReportRenderReportProperties;
};
export class FastReportProcessRunner {
@ -26,25 +28,14 @@ export class FastReportProcessRunner {
executablePath: string,
executableArgs: FastReportProcessRunnerArgs
): Promise<Result<Buffer | string, FastReportExecutionError>> {
const { templatePath, data, format, output } = executableArgs;
// Guardar datos de entrada en JSON
//const dataPath = buildSafePath({ basePath: workdir, segments: [], filename: "data.json" });
// Path de output según formato y con
/*const outputPath = buildSafePath({
basePath: workdir,
segments: [],
filename: format === "PDF" ? "output.pdf" : "output.html",
});
await fs.writeFile(dataPath, data, "utf-8");*/
const { templatePath, data, format, output, properties } = executableArgs;
const args = this.buildArgs({
templatePath,
data,
output,
format,
properties,
});
return this.executeProcess(executablePath, args, output, executableArgs.format);
@ -54,21 +45,41 @@ export class FastReportProcessRunner {
templatePath: string;
data: string;
output: string;
format: "PDF" | "HTML";
format: string;
properties: FastReportRenderReportProperties;
}): string[] {
return [
`--template=${params.templatePath}`,
`--data=${params.data}`,
`--output=${params.output}`,
`--format=${params.format}`,
const { templatePath, data, output, format, properties } = params;
const args: string[] = [
`--template=${templatePath}`,
`--data=${data}`,
`--output=${output}`,
`--format=${format}`,
];
const optionalArgs: Record<string, string | undefined> = {
title: properties.title,
author: properties.author,
subject: properties.subject,
keywords: properties.keywords,
creator: properties.creator,
producer: properties.producer,
};
for (const [key, value] of Object.entries(optionalArgs)) {
if (value !== undefined) {
args.push(`--${key}=${value}`);
}
}
return args;
}
private executeProcess(
executablePath: string,
args: string[],
outputPath: string,
format: "PDF" | "HTML"
format: string
): Promise<Result<Buffer | string, FastReportExecutionError>> {
return new Promise((resolve) => {
const child = spawn(executablePath, args, {

View File

@ -1,6 +1,9 @@
import type { FastReportRenderReportProperties } from "./fastreport-render-report-properties.type";
export type FastReportRenderOptions = {
templatePath: string;
inputData: unknown;
format: "PDF" | "HTML";
format: string;
storageKey?: string;
properties: FastReportRenderReportProperties;
};

View File

@ -0,0 +1,8 @@
export type FastReportRenderReportProperties = {
readonly title?: string;
readonly author?: string;
readonly subject?: string;
readonly keywords?: string;
readonly creator?: string;
readonly producer?: string;
};

View File

@ -40,6 +40,7 @@ export class FastReportRenderer extends Renderer<unknown, FastReportRenderOutput
data: inputPath,
output: outputPath,
format: options.format,
properties: options.properties,
});
const payload = await readFile(outputPath);
@ -82,98 +83,3 @@ export class FastReportRenderer extends Renderer<unknown, FastReportRenderOutput
}
}
}
/*
protected async renderInternal(
options: FastReportRenderOptions
): Promise<FastReportRenderOutput> {
if (process.env.NODE_ENV !== "development") {
// Cache (read-through)
const cached = await this.tryReadFromCache(options.storageKey);
if (cached.isSome()) {
return {
payload: cached.unwrap(),
templateChecksum: "CACHED",
};
}
}
// Resolver plantilla
const templatePath = this.resolveTemplatePath();
console.log("Using FastReport template:", templatePath);
if (!fs.existsSync(templatePath)) {
throw new FastReportTemplateNotFoundError(templatePath);
}
const templateChecksum = this.calculateChecksum(templatePath);
// Llamar a FastReport
const callResult = await this.callFastReportGenerator(options, templatePath);
if (callResult.isFailure) {
throw callResult.error;
}
// Guardar documento generado (best-effort)
await this.storageReport(options.storageKey, callResult.data);
return {
payload: callResult.data,
templateChecksum,
};
}
protected async tryReadFromCache(docKey: ReportStorageKey): Promise<Maybe<Buffer | string>> {
if (await this.reportStorage.exists(docKey)) {
const cached = await this.reportStorage.read(docKey);
return Maybe.some(cached);
}
return Maybe.none();
}
protected async callFastReportGenerator(
options: FastReportRenderOptions,
templatePath: string
): Promise<Result<Buffer | string, FastReportError>> {
const executablePath = this.executableResolver.resolve();
const workdir = this.resolveWorkdir();
const runResult = await this.processRunner.run(executablePath, {
templatePath,
data: JSON.stringify(options.inputData),
format: options.format,
workdir,
});
if (runResult.isFailure) {
return Result.fail(new FastReportExecutionError(runResult.error.message, runResult.error));
}
return Result.ok(runResult.data);
}
protected async storageReport(key: ReportStorageKey, payload: Buffer | string): Promise<void> {
try {
await this.reportStorage.write(key, payload);
} catch (error) {
// ⚠️ Importante: no romper generación por fallo de cache
}
}
protected resolveWorkdir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "fastreport-"));
}
protected abstract resolveTemplatePath(): string;
protected calculateChecksum(filePath: string): string
{
const buffer = fs.readFileSync(filePath);
return crypto.createHash("sha256").update(buffer).digest("hex");
}
}
*/

View File

@ -18,17 +18,21 @@ export class FastReportTemplateResolver {
* Devuelve el directorio donde residen las plantillas de un módulo/empresa
* según el entorno (dev/prod).
*/
protected resolveTemplateDirectory(module: string, companySlug: string): string {
protected resolveTemplateDirectory(params: {
module: string;
companySlug: string;
languageCode: string;
}): string {
const { module, companySlug, languageCode } = params;
const isDev = process.env.NODE_ENV === "development";
if (isDev) {
// <root>/<module>/templates/<companySlug>/
return this.resolveJoin([module, "templates", companySlug]);
// <root>/<module>/templates/<companySlug>/<languageCode>/
return this.resolveJoin([module, "templates", companySlug, languageCode]);
}
// <root>/templates/<module>/<companySlug>/
//return this.resolveJoin(["templates", module, companySlug]);
return this.resolveJoin([module]);
// <root>/templates/<companySlug>/<module>/<languageCode>
return this.resolveJoin([companySlug, module, languageCode]);
}
/** Resuelve una ruta de recurso relativa al directorio de plantilla */
@ -39,13 +43,19 @@ export class FastReportTemplateResolver {
/**
* Devuelve la ruta absoluta del fichero de plantilla.
*/
public resolveTemplatePath(module: string, companySlug: string, templateName: string): string {
const dir = this.resolveTemplateDirectory(module, companySlug);
const filePath = this.resolveAssetPath(dir, templateName);
public resolveTemplatePath(params: {
module: string;
companySlug: string;
languageCode: string;
templateFilename: string;
}): string {
const { module, companySlug, languageCode, templateFilename } = params;
const dir = this.resolveTemplateDirectory({ module, companySlug, languageCode });
const filePath = this.resolveAssetPath(dir, templateFilename);
if (!existsSync(filePath)) {
throw new FastReportTemplateNotFoundError(
`Template not found: module=${module} company=${companySlug} name=${templateName}`
`Template not found: module=${module} company=${companySlug} name=${templateFilename}`
);
}

View File

@ -2,5 +2,6 @@ export * from "./fastreport-errors";
export * from "./fastreport-executable-resolver";
export * from "./fastreport-process-runner";
export * from "./fastreport-render-options.type";
export * from "./fastreport-render-report-properties.type";
export * from "./fastreport-renderer";
export * from "./fastreport-template-resolver";

View File

@ -1,97 +0,0 @@
import { existsSync, readFileSync } from "node:fs";
import Handlebars from "handlebars";
import { lookup } from "mime-types";
import { RendererTemplateResolver } from "../renderer-template-resolver-SOBRA";
export class HandlebarsTemplateResolver extends RendererTemplateResolver {
protected readonly hbs = Handlebars.create();
protected registered = false;
protected readonly assetCache = new Map<string, string>();
/**
* Registra el helper "asset".
*
* - Si el fichero termina en .b64 se asume que el contenido ya es base64
* - Si no se lee binario y se convierte a base64
*/
protected registerAssetHelper(templateDir: string) {
// Si ya está registrado, no hacer nada
if (this.registered) return;
this.hbs.registerHelper("asset", (resource: string) => {
const assetPath = this.resolveAssetPath(templateDir, resource);
const cacheKey = `${assetPath}`;
// 1) Caché en memoria
const cached = this.assetCache.get(cacheKey);
if (cached) {
return cached;
}
if (!existsSync(assetPath)) {
throw new Error(`Asset not found: ${assetPath}`);
}
// 3) Modo "base64"
const isPreencoded = assetPath.endsWith(".b64");
let base64: string;
let mimeType: string;
let value: string;
if (isPreencoded) {
// Fichero ya contiene el base64 en texto plano
base64 = readFileSync(assetPath, "utf8").trim();
// Para el MIME usamos el nombre "original" sin .b64
const mimeLookupPath = assetPath.replace(/\.b64$/, "");
mimeType = (lookup(mimeLookupPath) || "application/octet-stream") as string;
value = `data:${mimeType};base64,${base64}`;
} else {
// Fichero normal
// Si es un CSS no se convierte y se incrusta
const isCSS = assetPath.endsWith(".css");
if (isCSS) {
const buffer = readFileSync(assetPath);
value = buffer.toString();
} else {
// En otro caso, se transforma a Base64
const buffer = readFileSync(assetPath);
mimeType = (lookup(assetPath) || "application/octet-stream") as string;
base64 = buffer.toString("base64");
value = `data:${mimeType};base64,${base64}`;
}
}
this.assetCache.set(cacheKey, value);
return value;
});
this.registered = true;
}
/** Compilación directa desde string (sin resolución de rutas) */
public compile(templateSource: string) {
return this.hbs.compile(templateSource);
}
/** Localiza → lee → registra helpers → compila */
public compileTemplate(
module: string,
companySlug: string,
templateName: string
): Handlebars.TemplateDelegate {
// 1) Directorio de plantillas
const templateDir = this.resolveTemplateDirectory(module, companySlug);
const templatePath = this.resolveTemplatePath(module, companySlug, templateName); // 2) Path completo del template
const source = this.readTemplateFile(templatePath); // Contenido
this.registerAssetHelper(templateDir);
// 5) Compilar
return this.compile(source);
}
}

View File

@ -1 +0,0 @@
export * from "./handlebars-template-resolver";

View File

@ -1,3 +1 @@
export * from "./fastreport";
export * from "./handlebars";
export * from "./renderer-template-resolver-SOBRA";

View File

@ -1,61 +0,0 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import type { IRendererTemplateResolver } from "../../../application";
import { FastReportTemplateNotFoundError } from "./fastreport";
/**
* Resuelve rutas de plantillas para desarrollo y producción.
*/
export abstract class RendererTemplateResolver implements IRendererTemplateResolver {
constructor(protected readonly rootPath: string) {}
/** Une partes de ruta relativas al rootPath */
protected resolveJoin(parts: string[]): string {
return join(this.rootPath, ...parts);
}
/**
* Devuelve el directorio donde residen las plantillas de un módulo/empresa
* según el entorno (dev/prod).
*/
protected resolveTemplateDirectory(module: string, companySlug: string): string {
const isDev = process.env.NODE_ENV === "development";
if (isDev) {
// <root>/<module>/templates/<companySlug>/
return this.resolveJoin([module, "templates", companySlug]);
}
// <root>/templates/<module>/<companySlug>/
//return this.resolveJoin(["templates", module, companySlug]);
return this.resolveJoin([module]);
}
/** Resuelve una ruta de recurso relativa al directorio de plantilla */
protected resolveAssetPath(templateDir: string, relative: string): string {
return join(templateDir, relative);
}
/**
* Devuelve la ruta absoluta del fichero de plantilla.
*/
public resolveTemplatePath(module: string, companySlug: string, templateName: string): string {
const dir = this.resolveTemplateDirectory(module, companySlug);
const filePath = this.resolveAssetPath(dir, templateName);
if (!existsSync(filePath)) {
throw new FastReportTemplateNotFoundError(
`Template not found: module=${module} company=${companySlug} name=${templateName}`
);
}
return filePath;
}
/** Lee el contenido de un fichero plantilla */
protected readTemplateFile(templatePath: string): string {
return readFileSync(templatePath, "utf8");
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@erp/customer-invoices",
"version": "0.4.8",
"version": "0.5.0",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,4 +1,5 @@
export * from "./issued-invoice-document-generator.interface";
export * from "./issued-invoice-document-metadata-factory";
export * from "./issued-invoice-document-properties-factory";
export * from "./issued-invoice-document-renderer.interface";
export * from "./issued-invoice-finder";

View File

@ -0,0 +1,23 @@
import type { IDocumentProperties, IDocumentPropertiesFactory } from "@erp/core/api";
import type { IssuedInvoiceReportSnapshot } from "../application-models";
/**
* Construye los metadatos del documento PDF de una factura emitida.
*
* - Application-level
* - Determinista
* - Sin IO
*/
export class IssuedInvoiceDocumentPropertiesFactory
implements IDocumentPropertiesFactory<IssuedInvoiceReportSnapshot>
{
build(snapshot: IssuedInvoiceReportSnapshot): IDocumentProperties {
return {
title: snapshot.reference,
subject: "issued-invoice",
author: snapshot.company_slug,
creator: "FactuGES ERP",
};
}
}

View File

@ -16,7 +16,6 @@ export const buildIssuedInvoiceDocumentService = (params: ModuleParams) => {
documentSigningService: documentSigning.signingService,
//
documentCacheStore: documentStorage.cacheStore,
documentStorage: documentStorage.storage,
templateResolver: documentRenderers.fastReportTemplateResolver,

View File

@ -13,6 +13,7 @@ import {
import {
type IssuedInvoiceDocumentGeneratorService,
IssuedInvoiceDocumentMetadataFactory,
IssuedInvoiceDocumentPropertiesFactory,
type IssuedInvoiceReportSnapshot,
} from "../../../application";
import { DigitalSignaturePostProcessor } from "../post-processors";
@ -44,13 +45,14 @@ export class IssuedInvoiceDocumentPipelineFactory {
const preProcessors = [new IssuedInvoiceSignedDocumentCachePreProcessor(deps.documentStorage)];
// 2. Renderer (FastReport)
const documentRenderer = new IssuedInvoiceDocumentRenderer(
const renderer = new IssuedInvoiceDocumentRenderer(
deps.fastReportRenderer,
deps.templateResolver
);
// 3) Metadata factory (Application)
// 3) Metadata and properties factory (Application)
const metadataFactory = new IssuedInvoiceDocumentMetadataFactory();
const propertiesFactory = new IssuedInvoiceDocumentPropertiesFactory();
// 3) Firma real (Core / Infra)
const postProcessor: IDocumentPostProcessor = new DocumentPostProcessorChain([
@ -63,12 +65,13 @@ export class IssuedInvoiceDocumentPipelineFactory {
];
// 5. Pipeline final
return new DocumentGenerationService<IssuedInvoiceReportSnapshot>(
metadataFactory,
return new DocumentGenerationService<IssuedInvoiceReportSnapshot>({
renderer,
preProcessors,
documentRenderer,
postProcessor,
sideEffects
);
sideEffects,
metadataFactory,
propertiesFactory,
});
}
}

View File

@ -2,6 +2,7 @@ import type {
FastReportRenderer,
FastReportTemplateResolver,
IDocument,
IDocumentProperties,
IDocumentRenderer,
} from "@erp/core/api";
@ -20,6 +21,11 @@ import type { IssuedInvoiceReportSnapshot } from "../../../../application";
export type IssuedInvoiceDocumentRenderParams = {
companySlug: string;
format: string;
languageCode: string;
filename: string;
mimeType: string;
properties: IDocumentProperties;
};
export class IssuedInvoiceDocumentRenderer
@ -34,23 +40,27 @@ export class IssuedInvoiceDocumentRenderer
snapshot: IssuedInvoiceReportSnapshot,
params: IssuedInvoiceDocumentRenderParams
): Promise<IDocument> {
const { companySlug } = params;
const templatePath = this.templateResolver.resolveTemplatePath(
"customer-invoices",
const { companySlug, format, languageCode, filename, mimeType, properties } = params;
// Template
const templatePath = this.templateResolver.resolveTemplatePath({
module: "customer-invoices",
companySlug,
"issued-invoice.frx"
);
languageCode,
templateFilename: "issued-invoice.frx",
});
const output = await this.fastReportRenderer.render({
templatePath,
inputData: snapshot,
format: "PDF",
format,
properties,
});
return {
payload: this.normalizePayload(output.payload),
mimeType: "application/pdf",
filename: "issued-invoice.pdf",
mimeType,
filename,
};
}

View File

@ -1,6 +1,6 @@
{
"name": "@erp/customers",
"version": "0.4.8",
"version": "0.5.0",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@erp/doc-numbering",
"version": "0.4.8",
"version": "0.5.0",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-criteria",
"version": "0.4.8",
"version": "0.5.0",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-ddd",
"version": "0.4.8",
"version": "0.5.0",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-logger",
"version": "0.4.8",
"version": "0.5.0",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-utils",
"version": "0.4.8",
"version": "0.5.0",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -11,7 +11,7 @@
</PropertyGroup>
<PropertyGroup>
<AssemblyVersion>1.8.2.0</AssemblyVersion>
<AssemblyVersion>1.9.0.0</AssemblyVersion>
</PropertyGroup>
<ItemGroup>

View File

@ -28,7 +28,7 @@ namespace NetCore8._0
return 0;
}
// Validación de obligatorios
// Validación de argumentos obligatorios
if (!options.TryGetValue("template", out var frxPath) ||
!options.TryGetValue("data", out var jsonPath) ||
!options.TryGetValue("output", out var outputPath))
@ -38,6 +38,15 @@ namespace NetCore8._0
return 1;
}
// Argumentos opcionales
options.TryGetValue("title", out var title);
options.TryGetValue("author", out var author);
options.TryGetValue("subject", out var subject);
options.TryGetValue("keywords", out var keywords);
options.TryGetValue("creator", out var creator);
options.TryGetValue("producer", out var producer);
if (!File.Exists(jsonPath))
throw new FileNotFoundException($"Data file not found: {jsonPath}");
@ -105,12 +114,13 @@ namespace NetCore8._0
pdfExport.PdfCompliance = PDFExport.PdfStandard.PdfX_3;
pdfExport.ShowProgress = false;
pdfExport.Subject = "Generated by FastReportCliGenerator";
pdfExport.Author = "FastReport .NET";
pdfExport.Title = "FastReport CLI Report";
pdfExport.Keywords = "";
pdfExport.Creator = GetVersion();
pdfExport.Producer = GetVersion();
// Metadatos dinámicos
pdfExport.Title = title ?? "";
pdfExport.Author = author ?? "";
pdfExport.Subject = subject ?? "";
pdfExport.Keywords = keywords ?? "";
pdfExport.Creator = creator ?? GetVersion();
pdfExport.Producer = producer ?? GetVersion();
report.Export(pdfExport, fs);
Console.WriteLine($"Generated PDF: {outputPath}");
@ -190,14 +200,27 @@ Options:
--data Path to JSON data file (required)
--output Output file path (required)
--format html or pdf (default)
--title PDF document title (document id or number)
--author PDF document author (company)
--subject PDF document subject (document type)
--keywords PDF document keywords
--creator PDF document creator (application name)
--producer PDF document producer (version)
--version Show version and exit
--help Show this help
Examples:
FastReportCliGenerator \
--template=invoice.frx \
--data=invoice.json \
--output=invoice.html
--template=factura.frx \
--data=factura.json \
--output=factura.pdf \
--format=pdf \
--title='Factura 2025-001' \
--author='FactuGES ERP' \
--subject='Factura cliente ACME' \
--keywords='factura,cliente,acme'
FastReportCliGenerator \
--template=invoice.frx \