v0.5.0
This commit is contained in:
parent
770fb33bb0
commit
aaf7d22374
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "0.4.8",
|
"version": "0.5.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "WEB: Vite (Chrome)",
|
"name": "WEB: Vite (Chrome)",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/factuges-server",
|
"name": "@erp/factuges-server",
|
||||||
"version": "0.4.8",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup src/index.ts --config tsup.config.ts",
|
"build": "tsup src/index.ts --config tsup.config.ts",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/factuges-web",
|
"name": "@erp/factuges-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.8",
|
"version": "0.5.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host --clearScreen false",
|
"dev": "vite --host --clearScreen false",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/auth",
|
"name": "@erp/auth",
|
||||||
"version": "0.4.8",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/core",
|
"name": "@erp/core",
|
||||||
"version": "0.4.8",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
export * from "./company-certificate-context.interface";
|
export * from "./company-certificate-context.interface";
|
||||||
export * from "./document.interface";
|
export * from "./document.interface";
|
||||||
export * from "./document-metadata.interface";
|
export * from "./document-metadata.interface";
|
||||||
|
export * from "./document-properties.interface";
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { ApplicationError } from "@repo/rdx-ddd";
|
|||||||
|
|
||||||
type DocumentGenerationErrorCode =
|
type DocumentGenerationErrorCode =
|
||||||
| "METADATA_ERROR"
|
| "METADATA_ERROR"
|
||||||
|
| "PROPERTIES_ERROR"
|
||||||
| "CACHE_ERROR" // no fatal (solo para métricas/logs si se usa)
|
| "CACHE_ERROR" // no fatal (solo para métricas/logs si se usa)
|
||||||
| "RENDER_ERROR"
|
| "RENDER_ERROR"
|
||||||
| "POST_PROCESS_ERROR"
|
| "POST_PROCESS_ERROR"
|
||||||
@ -30,6 +31,10 @@ export class DocumentGenerationError extends ApplicationError {
|
|||||||
return new DocumentGenerationError("Invalid document metadata", "METADATA_ERROR", cause);
|
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) {
|
static render(cause: unknown) {
|
||||||
return new DocumentGenerationError("Document render failed", "RENDER_ERROR", cause);
|
return new DocumentGenerationError("Document render failed", "RENDER_ERROR", cause);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { 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 { DocumentGenerationError } from "../errors";
|
||||||
|
|
||||||
import type { IDocumentMetadataFactory } from "./document-metadata-factory.interface";
|
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]
|
* 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 type DocumentGenerationServiceRenderParams = Record<string, string>;
|
||||||
|
|
||||||
export class DocumentGenerationService<TSnapshot> {
|
export class DocumentGenerationService<TSnapshot> {
|
||||||
constructor(
|
private readonly renderer: IDocumentRenderer<TSnapshot>;
|
||||||
private readonly metadataFactory: IDocumentMetadataFactory<TSnapshot>,
|
private readonly metadataFactory: IDocumentMetadataFactory<TSnapshot>;
|
||||||
private readonly preProcessors: readonly IDocumentPreProcessor[],
|
private readonly propertiesFactory: IDocumentPropertiesFactory<TSnapshot>;
|
||||||
private readonly renderer: IDocumentRenderer<TSnapshot>,
|
private readonly preProcessors: readonly IDocumentPreProcessor[];
|
||||||
private readonly postProcessor: IDocumentPostProcessor,
|
private readonly postProcessor: IDocumentPostProcessor;
|
||||||
private readonly sideEffects: readonly IDocumentSideEffect[]
|
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(
|
async generate(
|
||||||
snapshot: TSnapshot,
|
snapshot: TSnapshot,
|
||||||
params: DocumentGenerationServiceRenderParams
|
params: DocumentGenerationServiceRenderParams
|
||||||
): Promise<Result<IDocument, DocumentGenerationError>> {
|
): Promise<Result<IDocument, DocumentGenerationError>> {
|
||||||
let metadata: IDocumentMetadata;
|
let metadata: IDocumentMetadata;
|
||||||
|
let properties: IDocumentProperties;
|
||||||
|
|
||||||
// 1. Metadata
|
// 1. Metadata and Properties
|
||||||
try {
|
try {
|
||||||
metadata = this.metadataFactory.build(snapshot);
|
metadata = this.metadataFactory.build(snapshot);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Result.fail(DocumentGenerationError.metadata(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)
|
// 2. Pre-processors (cache / short-circuit)
|
||||||
for (const preProcessor of this.preProcessors) {
|
for (const preProcessor of this.preProcessors) {
|
||||||
try {
|
try {
|
||||||
@ -66,6 +90,7 @@ export class DocumentGenerationService<TSnapshot> {
|
|||||||
languageCode: metadata.languageCode,
|
languageCode: metadata.languageCode,
|
||||||
filename: metadata.filename,
|
filename: metadata.filename,
|
||||||
mimeType: metadata.format === "PDF" ? "application/pdf" : "text/html",
|
mimeType: metadata.format === "PDF" ? "application/pdf" : "text/html",
|
||||||
|
properties,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Result.fail(DocumentGenerationError.render(error));
|
return Result.fail(DocumentGenerationError.render(error));
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
import type { IDocumentProperties } from "../application-models";
|
||||||
|
|
||||||
|
export interface IDocumentPropertiesFactory<TSnapshot> {
|
||||||
|
build(snapshot: TSnapshot): IDocumentProperties;
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ export * from "./document-metadata-factory.interface";
|
|||||||
export * from "./document-post-processor.interface";
|
export * from "./document-post-processor.interface";
|
||||||
export * from "./document-post-processor-chain";
|
export * from "./document-post-processor-chain";
|
||||||
export * from "./document-pre-processor.interface";
|
export * from "./document-pre-processor.interface";
|
||||||
|
export * from "./document-properties-factory.interface";
|
||||||
export * from "./document-renderer.interface";
|
export * from "./document-renderer.interface";
|
||||||
export * from "./document-side-effect.interface";
|
export * from "./document-side-effect.interface";
|
||||||
export * from "./document-signing-service.interface";
|
export * from "./document-signing-service.interface";
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import fs from "node:fs/promises";
|
|||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
|
||||||
import { FastReportExecutionError } from "./fastreport-errors";
|
import { FastReportExecutionError } from "./fastreport-errors";
|
||||||
|
import type { FastReportRenderReportProperties } from "./fastreport-render-report-properties.type";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ejecuta el binario FastReport como proceso externo.
|
* Ejecuta el binario FastReport como proceso externo.
|
||||||
@ -17,8 +18,9 @@ import { FastReportExecutionError } from "./fastreport-errors";
|
|||||||
export type FastReportProcessRunnerArgs = {
|
export type FastReportProcessRunnerArgs = {
|
||||||
templatePath: string; // Path to FRX template (required)
|
templatePath: string; // Path to FRX template (required)
|
||||||
data: string; // JSON data as string
|
data: string; // JSON data as string
|
||||||
format: "PDF" | "HTML";
|
format: string;
|
||||||
output: string; // Directorio de trabajo temporal
|
output: string; // Directorio de trabajo temporal
|
||||||
|
properties: FastReportRenderReportProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FastReportProcessRunner {
|
export class FastReportProcessRunner {
|
||||||
@ -26,25 +28,14 @@ export class FastReportProcessRunner {
|
|||||||
executablePath: string,
|
executablePath: string,
|
||||||
executableArgs: FastReportProcessRunnerArgs
|
executableArgs: FastReportProcessRunnerArgs
|
||||||
): Promise<Result<Buffer | string, FastReportExecutionError>> {
|
): Promise<Result<Buffer | string, FastReportExecutionError>> {
|
||||||
const { templatePath, data, format, output } = executableArgs;
|
const { templatePath, data, format, output, properties } = 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 args = this.buildArgs({
|
const args = this.buildArgs({
|
||||||
templatePath,
|
templatePath,
|
||||||
data,
|
data,
|
||||||
output,
|
output,
|
||||||
format,
|
format,
|
||||||
|
properties,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.executeProcess(executablePath, args, output, executableArgs.format);
|
return this.executeProcess(executablePath, args, output, executableArgs.format);
|
||||||
@ -54,21 +45,41 @@ export class FastReportProcessRunner {
|
|||||||
templatePath: string;
|
templatePath: string;
|
||||||
data: string;
|
data: string;
|
||||||
output: string;
|
output: string;
|
||||||
format: "PDF" | "HTML";
|
format: string;
|
||||||
|
properties: FastReportRenderReportProperties;
|
||||||
}): string[] {
|
}): string[] {
|
||||||
return [
|
const { templatePath, data, output, format, properties } = params;
|
||||||
`--template=${params.templatePath}`,
|
|
||||||
`--data=${params.data}`,
|
const args: string[] = [
|
||||||
`--output=${params.output}`,
|
`--template=${templatePath}`,
|
||||||
`--format=${params.format}`,
|
`--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(
|
private executeProcess(
|
||||||
executablePath: string,
|
executablePath: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
outputPath: string,
|
outputPath: string,
|
||||||
format: "PDF" | "HTML"
|
format: string
|
||||||
): Promise<Result<Buffer | string, FastReportExecutionError>> {
|
): Promise<Result<Buffer | string, FastReportExecutionError>> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const child = spawn(executablePath, args, {
|
const child = spawn(executablePath, args, {
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
|
import type { FastReportRenderReportProperties } from "./fastreport-render-report-properties.type";
|
||||||
|
|
||||||
export type FastReportRenderOptions = {
|
export type FastReportRenderOptions = {
|
||||||
templatePath: string;
|
templatePath: string;
|
||||||
inputData: unknown;
|
inputData: unknown;
|
||||||
format: "PDF" | "HTML";
|
format: string;
|
||||||
storageKey?: string;
|
storageKey?: string;
|
||||||
|
properties: FastReportRenderReportProperties;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
@ -40,6 +40,7 @@ export class FastReportRenderer extends Renderer<unknown, FastReportRenderOutput
|
|||||||
data: inputPath,
|
data: inputPath,
|
||||||
output: outputPath,
|
output: outputPath,
|
||||||
format: options.format,
|
format: options.format,
|
||||||
|
properties: options.properties,
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = await readFile(outputPath);
|
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|||||||
@ -18,17 +18,21 @@ export class FastReportTemplateResolver {
|
|||||||
* Devuelve el directorio donde residen las plantillas de un módulo/empresa
|
* Devuelve el directorio donde residen las plantillas de un módulo/empresa
|
||||||
* según el entorno (dev/prod).
|
* 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";
|
const isDev = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
// <root>/<module>/templates/<companySlug>/
|
// <root>/<module>/templates/<companySlug>/<languageCode>/
|
||||||
return this.resolveJoin([module, "templates", companySlug]);
|
return this.resolveJoin([module, "templates", companySlug, languageCode]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// <root>/templates/<module>/<companySlug>/
|
// <root>/templates/<companySlug>/<module>/<languageCode>
|
||||||
//return this.resolveJoin(["templates", module, companySlug]);
|
return this.resolveJoin([companySlug, module, languageCode]);
|
||||||
return this.resolveJoin([module]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resuelve una ruta de recurso relativa al directorio de plantilla */
|
/** 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.
|
* Devuelve la ruta absoluta del fichero de plantilla.
|
||||||
*/
|
*/
|
||||||
public resolveTemplatePath(module: string, companySlug: string, templateName: string): string {
|
public resolveTemplatePath(params: {
|
||||||
const dir = this.resolveTemplateDirectory(module, companySlug);
|
module: string;
|
||||||
const filePath = this.resolveAssetPath(dir, templateName);
|
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)) {
|
if (!existsSync(filePath)) {
|
||||||
throw new FastReportTemplateNotFoundError(
|
throw new FastReportTemplateNotFoundError(
|
||||||
`Template not found: module=${module} company=${companySlug} name=${templateName}`
|
`Template not found: module=${module} company=${companySlug} name=${templateFilename}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,5 +2,6 @@ export * from "./fastreport-errors";
|
|||||||
export * from "./fastreport-executable-resolver";
|
export * from "./fastreport-executable-resolver";
|
||||||
export * from "./fastreport-process-runner";
|
export * from "./fastreport-process-runner";
|
||||||
export * from "./fastreport-render-options.type";
|
export * from "./fastreport-render-options.type";
|
||||||
|
export * from "./fastreport-render-report-properties.type";
|
||||||
export * from "./fastreport-renderer";
|
export * from "./fastreport-renderer";
|
||||||
export * from "./fastreport-template-resolver";
|
export * from "./fastreport-template-resolver";
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./handlebars-template-resolver";
|
|
||||||
@ -1,3 +1 @@
|
|||||||
export * from "./fastreport";
|
export * from "./fastreport";
|
||||||
export * from "./handlebars";
|
|
||||||
export * from "./renderer-template-resolver-SOBRA";
|
|
||||||
|
|||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/customer-invoices",
|
"name": "@erp/customer-invoices",
|
||||||
"version": "0.4.8",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export * from "./issued-invoice-document-generator.interface";
|
export * from "./issued-invoice-document-generator.interface";
|
||||||
export * from "./issued-invoice-document-metadata-factory";
|
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-document-renderer.interface";
|
||||||
export * from "./issued-invoice-finder";
|
export * from "./issued-invoice-finder";
|
||||||
|
|||||||
@ -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",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,7 +16,6 @@ export const buildIssuedInvoiceDocumentService = (params: ModuleParams) => {
|
|||||||
documentSigningService: documentSigning.signingService,
|
documentSigningService: documentSigning.signingService,
|
||||||
|
|
||||||
//
|
//
|
||||||
documentCacheStore: documentStorage.cacheStore,
|
|
||||||
documentStorage: documentStorage.storage,
|
documentStorage: documentStorage.storage,
|
||||||
|
|
||||||
templateResolver: documentRenderers.fastReportTemplateResolver,
|
templateResolver: documentRenderers.fastReportTemplateResolver,
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
type IssuedInvoiceDocumentGeneratorService,
|
type IssuedInvoiceDocumentGeneratorService,
|
||||||
IssuedInvoiceDocumentMetadataFactory,
|
IssuedInvoiceDocumentMetadataFactory,
|
||||||
|
IssuedInvoiceDocumentPropertiesFactory,
|
||||||
type IssuedInvoiceReportSnapshot,
|
type IssuedInvoiceReportSnapshot,
|
||||||
} from "../../../application";
|
} from "../../../application";
|
||||||
import { DigitalSignaturePostProcessor } from "../post-processors";
|
import { DigitalSignaturePostProcessor } from "../post-processors";
|
||||||
@ -44,13 +45,14 @@ export class IssuedInvoiceDocumentPipelineFactory {
|
|||||||
const preProcessors = [new IssuedInvoiceSignedDocumentCachePreProcessor(deps.documentStorage)];
|
const preProcessors = [new IssuedInvoiceSignedDocumentCachePreProcessor(deps.documentStorage)];
|
||||||
|
|
||||||
// 2. Renderer (FastReport)
|
// 2. Renderer (FastReport)
|
||||||
const documentRenderer = new IssuedInvoiceDocumentRenderer(
|
const renderer = new IssuedInvoiceDocumentRenderer(
|
||||||
deps.fastReportRenderer,
|
deps.fastReportRenderer,
|
||||||
deps.templateResolver
|
deps.templateResolver
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3) Metadata factory (Application)
|
// 3) Metadata and properties factory (Application)
|
||||||
const metadataFactory = new IssuedInvoiceDocumentMetadataFactory();
|
const metadataFactory = new IssuedInvoiceDocumentMetadataFactory();
|
||||||
|
const propertiesFactory = new IssuedInvoiceDocumentPropertiesFactory();
|
||||||
|
|
||||||
// 3) Firma real (Core / Infra)
|
// 3) Firma real (Core / Infra)
|
||||||
const postProcessor: IDocumentPostProcessor = new DocumentPostProcessorChain([
|
const postProcessor: IDocumentPostProcessor = new DocumentPostProcessorChain([
|
||||||
@ -63,12 +65,13 @@ export class IssuedInvoiceDocumentPipelineFactory {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// 5. Pipeline final
|
// 5. Pipeline final
|
||||||
return new DocumentGenerationService<IssuedInvoiceReportSnapshot>(
|
return new DocumentGenerationService<IssuedInvoiceReportSnapshot>({
|
||||||
metadataFactory,
|
renderer,
|
||||||
preProcessors,
|
preProcessors,
|
||||||
documentRenderer,
|
|
||||||
postProcessor,
|
postProcessor,
|
||||||
sideEffects
|
sideEffects,
|
||||||
);
|
metadataFactory,
|
||||||
|
propertiesFactory,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type {
|
|||||||
FastReportRenderer,
|
FastReportRenderer,
|
||||||
FastReportTemplateResolver,
|
FastReportTemplateResolver,
|
||||||
IDocument,
|
IDocument,
|
||||||
|
IDocumentProperties,
|
||||||
IDocumentRenderer,
|
IDocumentRenderer,
|
||||||
} from "@erp/core/api";
|
} from "@erp/core/api";
|
||||||
|
|
||||||
@ -20,6 +21,11 @@ import type { IssuedInvoiceReportSnapshot } from "../../../../application";
|
|||||||
|
|
||||||
export type IssuedInvoiceDocumentRenderParams = {
|
export type IssuedInvoiceDocumentRenderParams = {
|
||||||
companySlug: string;
|
companySlug: string;
|
||||||
|
format: string;
|
||||||
|
languageCode: string;
|
||||||
|
filename: string;
|
||||||
|
mimeType: string;
|
||||||
|
properties: IDocumentProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class IssuedInvoiceDocumentRenderer
|
export class IssuedInvoiceDocumentRenderer
|
||||||
@ -34,23 +40,27 @@ export class IssuedInvoiceDocumentRenderer
|
|||||||
snapshot: IssuedInvoiceReportSnapshot,
|
snapshot: IssuedInvoiceReportSnapshot,
|
||||||
params: IssuedInvoiceDocumentRenderParams
|
params: IssuedInvoiceDocumentRenderParams
|
||||||
): Promise<IDocument> {
|
): Promise<IDocument> {
|
||||||
const { companySlug } = params;
|
const { companySlug, format, languageCode, filename, mimeType, properties } = params;
|
||||||
const templatePath = this.templateResolver.resolveTemplatePath(
|
|
||||||
"customer-invoices",
|
// Template
|
||||||
|
const templatePath = this.templateResolver.resolveTemplatePath({
|
||||||
|
module: "customer-invoices",
|
||||||
companySlug,
|
companySlug,
|
||||||
"issued-invoice.frx"
|
languageCode,
|
||||||
);
|
templateFilename: "issued-invoice.frx",
|
||||||
|
});
|
||||||
|
|
||||||
const output = await this.fastReportRenderer.render({
|
const output = await this.fastReportRenderer.render({
|
||||||
templatePath,
|
templatePath,
|
||||||
inputData: snapshot,
|
inputData: snapshot,
|
||||||
format: "PDF",
|
format,
|
||||||
|
properties,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
payload: this.normalizePayload(output.payload),
|
payload: this.normalizePayload(output.payload),
|
||||||
mimeType: "application/pdf",
|
mimeType,
|
||||||
filename: "issued-invoice.pdf",
|
filename,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/customers",
|
"name": "@erp/customers",
|
||||||
"version": "0.4.8",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@erp/doc-numbering",
|
"name": "@erp/doc-numbering",
|
||||||
"version": "0.4.8",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@repo/rdx-criteria",
|
"name": "@repo/rdx-criteria",
|
||||||
"version": "0.4.8",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@repo/rdx-ddd",
|
"name": "@repo/rdx-ddd",
|
||||||
"version": "0.4.8",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@repo/rdx-logger",
|
"name": "@repo/rdx-logger",
|
||||||
"version": "0.4.8",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@repo/rdx-utils",
|
"name": "@repo/rdx-utils",
|
||||||
"version": "0.4.8",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyVersion>1.8.2.0</AssemblyVersion>
|
<AssemblyVersion>1.9.0.0</AssemblyVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -28,7 +28,7 @@ namespace NetCore8._0
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validación de obligatorios
|
// Validación de argumentos obligatorios
|
||||||
if (!options.TryGetValue("template", out var frxPath) ||
|
if (!options.TryGetValue("template", out var frxPath) ||
|
||||||
!options.TryGetValue("data", out var jsonPath) ||
|
!options.TryGetValue("data", out var jsonPath) ||
|
||||||
!options.TryGetValue("output", out var outputPath))
|
!options.TryGetValue("output", out var outputPath))
|
||||||
@ -38,6 +38,15 @@ namespace NetCore8._0
|
|||||||
return 1;
|
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))
|
if (!File.Exists(jsonPath))
|
||||||
throw new FileNotFoundException($"Data file not found: {jsonPath}");
|
throw new FileNotFoundException($"Data file not found: {jsonPath}");
|
||||||
|
|
||||||
@ -105,12 +114,13 @@ namespace NetCore8._0
|
|||||||
pdfExport.PdfCompliance = PDFExport.PdfStandard.PdfX_3;
|
pdfExport.PdfCompliance = PDFExport.PdfStandard.PdfX_3;
|
||||||
pdfExport.ShowProgress = false;
|
pdfExport.ShowProgress = false;
|
||||||
|
|
||||||
pdfExport.Subject = "Generated by FastReportCliGenerator";
|
// Metadatos dinámicos
|
||||||
pdfExport.Author = "FastReport .NET";
|
pdfExport.Title = title ?? "";
|
||||||
pdfExport.Title = "FastReport CLI Report";
|
pdfExport.Author = author ?? "";
|
||||||
pdfExport.Keywords = "";
|
pdfExport.Subject = subject ?? "";
|
||||||
pdfExport.Creator = GetVersion();
|
pdfExport.Keywords = keywords ?? "";
|
||||||
pdfExport.Producer = GetVersion();
|
pdfExport.Creator = creator ?? GetVersion();
|
||||||
|
pdfExport.Producer = producer ?? GetVersion();
|
||||||
|
|
||||||
report.Export(pdfExport, fs);
|
report.Export(pdfExport, fs);
|
||||||
Console.WriteLine($"Generated PDF: {outputPath}");
|
Console.WriteLine($"Generated PDF: {outputPath}");
|
||||||
@ -190,14 +200,27 @@ Options:
|
|||||||
--data Path to JSON data file (required)
|
--data Path to JSON data file (required)
|
||||||
--output Output file path (required)
|
--output Output file path (required)
|
||||||
--format html or pdf (default)
|
--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
|
--version Show version and exit
|
||||||
--help Show this help
|
--help Show this help
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
FastReportCliGenerator \
|
FastReportCliGenerator \
|
||||||
--template=invoice.frx \
|
--template=factura.frx \
|
||||||
--data=invoice.json \
|
--data=factura.json \
|
||||||
--output=invoice.html
|
--output=factura.pdf \
|
||||||
|
--format=pdf \
|
||||||
|
--title='Factura 2025-001' \
|
||||||
|
--author='FactuGES ERP' \
|
||||||
|
--subject='Factura cliente ACME' \
|
||||||
|
--keywords='factura,cliente,acme'
|
||||||
|
|
||||||
FastReportCliGenerator \
|
FastReportCliGenerator \
|
||||||
--template=invoice.frx \
|
--template=invoice.frx \
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user