Back to Projects
Proforma Builder

Proforma Builder

Invoice generator for heavy machinery sales — PDF export, email delivery, live currency conversion

Laravel iconReact icon Inertia TypeScript iconTailwind iconMySQL iconTanStack icon
GitHub

Every sales quote was built manually in Excel. Open a template, fill in client details, copy-paste specs from another document, add images by hand, export to PDF. No central record of sent quotes, no status tracking, no audit trail. When machine specs changed, every future quote needed manual updates from scratch.

This project is a direct digitization of that workflow.

The Problem

A heavy machinery dealer needed to replace a manual Excel-based quoting process. The existing workflow was slow, error-prone, and entirely dependent on individual knowledge. There was no way to track which quotes had been sent, accepted, or rejected — and no audit trail when disputes arose.

What It Does

Proforma Builder covers the full lifecycle of a proforma invoice:

A salesperson selects a client and a machine template. The app pre-fills all technical specifications, standard features, optional extras, and pricing. Line items are auto-generated with correct VAT rates. If the sale is in RSD or HUF, the current EUR exchange rate is fetched automatically and prices are converted. A PDF is generated on demand — matching the layout of the original Excel document — and can be emailed directly from the app with the PDF as an attachment.

Status is tracked throughout: draft → sent → accepted → cancelled

Architecture

Built as a Laravel 13 + React 19 monolith connected via Inertia.js. No separate API — Inertia passes typed props directly from Laravel controllers to React page components. This keeps the codebase cohesive and avoids the overhead of a decoupled SPA while still delivering a fast, app-like experience.

Key Technical Decisions

Machine Templates are the core abstraction. Each template stores a base price in EUR, dynamic spec key-value pairs, included features, and optional extras with individual pricing and gratis toggles. Templates can be duplicated for machine variants.

Image compression pipeline — uncompressed machine images were producing 6MB+ PDF attachments. After upload, Intervention Image scales each file to max 1200px width and re-encodes as JPEG at 80% quality. Typical sizes drop from ~3MB to ~200KB, PDF attachments from ~6MB to ~1.3MB.

Currency conversion — template prices are stored in EUR. Switching to RSD or HUF triggers an automatic fetch of the current rate from exchangerate-api.com, which prefills an editable field. The rate is stored on the proforma record so the conversion is always reproducible on the PDF.

Proforma number generation — format PRF-{YEAR}-{0001} with yearly reset, generated in the Eloquent booted() hook. The sequence query uses withTrashed() to include soft-deleted records — without this, a deleted number could be re-issued and hit a unique constraint violation.

protected static function booted(): void
{
    static::creating(function (Proforma $proforma): void {
        if (filled($proforma->number)) {
            return;
        }

        $year = now()->year;
        $prefix = "PRF-{$year}-";

        $last = Proforma::withTrashed()
            ->where('number', 'like', "{$prefix}%")
            ->orderByDesc('number')
            ->value('number');

        $next = 1;
        if ($last) {
            $suffix = substr($last, strlen($prefix));
            if (is_numeric($suffix)) {
                $next = (int) $suffix + 1;
            }
        }

        $proforma->number = $prefix . str_pad($next, 4, '0', STR_PAD_LEFT);
    });
}

PDF generation uses barryvdh/laravel-dompdf with a custom Blade template written entirely in inline CSS — a hard DomPDF requirement. Company logo is loaded via filesystem path, not URL. The items table contains a nested sub-table of all machine spec fields. Both inline preview and force-download are supported.

Email delivery — the PDF is generated in memory and attached via Attachment::fromData() without writing any temporary files to disk. Multiple recipients supported, reply-to from company settings, status automatically transitions to sent after successful dispatch.

Screenshots