Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gui/src/components/editors/BatchInvoiceGenerator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ export function BatchInvoiceGenerator({ persona, onBack }: BatchInvoiceGenerator
<label htmlFor="generatePdf" className="ml-2 block text-sm text-gray-700">
Generate PDF
{!libreOfficeStatus?.available && (
<span className="text-gray-400 ml-1">(LibreOffice not available)</span>
<span className="text-gray-400 ml-1">(no PDF converter: install WordCraft or LibreOffice)</span>
)}
</label>
</div>
Expand Down
2 changes: 1 addition & 1 deletion gui/src/components/editors/InvoiceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export function InvoiceForm({ persona, clientName, onBack }: InvoiceFormProps) {
<label htmlFor="generatePdf" className="ml-2 block text-sm text-gray-700">
Generate PDF
{!libreOfficeStatus?.available && (
<span className="text-gray-400 ml-1">(LibreOffice not available)</span>
<span className="text-gray-400 ml-1">(no PDF converter: install WordCraft or LibreOffice)</span>
)}
</label>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/api/routes/invoices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
applyCurrencyConversion,
generateDocx,
generateDocuments,
isLibreOfficeAvailable,
detectPdfConverter,
addHistoryEntry,
parseMonthArg,
getDefaultBillingMonth,
Expand Down Expand Up @@ -136,7 +136,7 @@ export function generateInvoice(ctx: ServerContext) {
currency: string;
};

if (generatePdf && isLibreOfficeAvailable()) {
if (generatePdf && detectPdfConverter() !== null) {
const docs = await generateDocuments(context, clientInfo.directory, {
template: effectiveTemplate,
personaDir
Expand Down
9 changes: 4 additions & 5 deletions src/api/routes/system.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/**
* System routes - health check, LibreOffice status, and countries
* System routes - health check, PDF converter status, and countries
*/

import type { ApiRequest, ApiResponse, ServerContext } from '../types.js';
import { isLibreOfficeAvailable, getLibreOfficeVersion } from '../../lib/index.js';
import { detectPdfConverter } from '../../lib/index.js';
import { getAllCountries } from '../../data/countries.js';

export function healthCheck(ctx: ServerContext) {
Expand All @@ -19,9 +19,8 @@ export function healthCheck(ctx: ServerContext) {

export function libreOfficeStatus() {
return async (_req: ApiRequest, res: ApiResponse): Promise<void> => {
const available = isLibreOfficeAvailable();
const version = available ? getLibreOfficeVersion() : null;
res.json({ available, version });
const version = detectPdfConverter();
res.json({ available: version !== null, version });
};
}

Expand Down
12 changes: 6 additions & 6 deletions src/invoice.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
#!/usr/bin/env node
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
import { Provider, Client, Translations, InvoiceContext, ResolvedLineItem, EInvoiceFormat, CountryCode } from './types.js';
import { formatDate, formatCurrency, getServiceDescription, calculateDueDate, sanitizeFilenamePart } from './utils.js';
import { generateInvoiceFromTemplate } from './lib/template-generator.js';
import { applyCurrencyConversion } from './lib/invoice-builder.js';
import { createEmail } from './email.js';
import { convertDocxToPdf } from './lib/pdf-converter.js';
import { validateProvider, validateClient } from './schemas/index.js';
import { saveToHistory } from './history.js';
import { canGenerateEInvoice, getDefaultFormat, validateForEInvoice, generateEInvoice, saveEInvoice } from './einvoice/index.js';
Expand Down Expand Up @@ -375,17 +375,17 @@ async function runInvoice() {
}

// Generate DOCX and PDF
await generateInvoiceFromTemplate(ctx, effectiveTemplateName, cwd).then(buffer => {
await generateInvoiceFromTemplate(ctx, effectiveTemplateName, cwd).then(async buffer => {
fs.writeFileSync(docxPath, buffer);
console.log(`DOCX created: ${docxPath}`);

// Convert to PDF using LibreOffice
// Convert to PDF; abort before history/numbering so a failure leaves them untouched
try {
execSync(`soffice --headless --convert-to pdf --outdir "${outputDir}" "${docxPath}"`, { stdio: 'inherit' });
await convertDocxToPdf(docxPath);
console.log(`PDF created: ${pdfPath}`);
} catch (err) {
console.error('PDF conversion failed. Make sure LibreOffice is installed.');
console.error('Install with: brew install --cask libreoffice');
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}

// Save to history
Expand Down
58 changes: 5 additions & 53 deletions src/lib/document-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
*/
import * as fs from 'fs';
import * as path from 'path';
import { execSync } from 'child_process';
import { InvoiceContext } from '../types.js';
import {
generateInvoiceFromTemplate,
getBuiltInTemplates,
} from './template-generator.js';
import { sanitizeFilenamePart } from '../utils.js';
import { convertDocxToPdf } from './pdf-converter.js';

export interface GeneratedDocument {
docxPath: string;
Expand Down Expand Up @@ -64,34 +64,10 @@ export async function generateDocx(
}

/**
* Convert DOCX to PDF using LibreOffice
* Convert DOCX to PDF using WordCraft or LibreOffice
*/
export function convertToPdf(docxPath: string): string {
const outputDir = path.dirname(docxPath);
const baseName = path.basename(docxPath, '.docx');
const pdfPath = path.join(outputDir, `${baseName}.pdf`);

try {
execSync(
`soffice --headless --convert-to pdf --outdir "${outputDir}" "${docxPath}"`,
{ stdio: 'pipe' }
);

if (!fs.existsSync(pdfPath)) {
throw new Error('PDF file was not created');
}

return pdfPath;
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
throw new Error(
`PDF conversion failed: ${message}\n` +
'Make sure LibreOffice is installed:\n' +
' macOS: brew install --cask libreoffice\n' +
' Linux: sudo apt install libreoffice\n' +
' Windows: https://www.libreoffice.org/download/'
);
}
export async function convertToPdf(docxPath: string): Promise<string> {
return convertDocxToPdf(docxPath);
}

/**
Expand All @@ -111,7 +87,7 @@ export async function generateDocuments(
const { docxPath, docxBuffer } = await generateDocx(ctx, outputDir, opts);

// Convert to PDF
const pdfPath = convertToPdf(docxPath);
const pdfPath = await convertToPdf(docxPath);

return {
docxPath,
Expand All @@ -120,30 +96,6 @@ export async function generateDocuments(
};
}

/**
* Check if LibreOffice is available
*/
export function isLibreOfficeAvailable(): boolean {
try {
execSync('which soffice', { stdio: 'pipe' });
return true;
} catch {
return false;
}
}

/**
* Get LibreOffice version if available
*/
export function getLibreOfficeVersion(): string | null {
try {
const output = execSync('soffice --version', { encoding: 'utf8' });
return output.trim();
} catch {
return null;
}
}

/**
* Generate output paths for invoice
*/
Expand Down
9 changes: 7 additions & 2 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,18 @@ export {
generateDocx,
convertToPdf,
generateDocuments,
isLibreOfficeAvailable,
getLibreOfficeVersion,
generateOutputPaths,
type GeneratedDocument,
type GeneratedPDF
} from './document-generator.js';

// PDF Conversion
export {
detectPdfConverter,
convertDocxToPdf,
type PdfConverter
} from './pdf-converter.js';

// History Management
export {
getHistoryPath,
Expand Down
Loading
Loading