Skip to main content

Overview

The Invoicing module automatically generates invoices from completed sales with support for multiple print formats. It integrates with the NCF system for tax compliance and provides flexible printing options for different business scenarios.

Auto-Generate

Invoices created from sales

Multiple Formats

Ticket, letter, and route formats

Tax Compliant

NCF integration for DGII

Invoice Structure

Core Fields

app/Models/Sales/Invoice.php
protected $fillable = [
    'sale_id',
    'invoice_number',
    'type',              // Not actively used
    'format_type',       // ticket, letter, route
    'status',            // active, cancelled
    'due_date',          // For credit sales
    'generated_by',      // User who generated
];

protected $casts = [
    'due_date' => 'date',
    'created_at' => 'datetime',
];

Invoice Status

const STATUS_ACTIVE = 'active';
const STATUS_CANCELLED = 'cancelled';

public static function getStatuses(): array
{
    return [
        self::STATUS_ACTIVE    => 'Vigente',
        self::STATUS_CANCELLED => 'Anulada',
    ];
}
Status Flow: The system supports three print formats for different use cases:
For: Point of sale, retail storesFeatures:
  • Thermal printer compatible
  • 80mm paper width
  • Compact format
  • Fast printing
  • Essential information only
const FORMAT_TICKET = 'ticket';
Layout:
┌──────────────────────┐
│   COMPANY LOGO       │
│   Company Name       │
│   RNC: ###-###-###   │
├──────────────────────┤
│ FAC-2024-001        │
│ NCF: B0100000001    │
│ Date: 2024-03-05    │
├──────────────────────┤
│ Product        Qty  │
│ Price      Subtotal │
├──────────────────────┤
│ Item 1         2    │
│ $10.00      $20.00  │
├──────────────────────┤
│ TOTAL:       $20.00 │
│ Payment: Cash      │
│ Received:    $25.00 │
│ Change:       $5.00 │
└──────────────────────┘

Invoice Generation

Automatic Generation

Invoices are automatically created when a sale is completed:
app/Services/Sales/SalesServices/SaleService.php
public function create(array $data): Sale
{
    return DB::transaction(function () use ($data) {
        // Create sale
        $sale = Sale::create([...]);

        // Add items
        foreach ($data['items'] as $item) {
            $sale->items()->create([...]);
        }

        // Generate invoice
        $this->invoiceService->createFromSale($sale);

        return $sale;
    });
}

Invoice Service

app/Services/Sales/InvoicesServices/InvoiceService.php
public function createFromSale(Sale $sale): Invoice
{
    // Determine format based on payment type or client type
    $format = $this->determineFormat($sale);

    // Calculate due date for credit sales
    $dueDate = null;
    if ($sale->payment_type === Sale::PAYMENT_CREDIT) {
        $dueDate = $sale->sale_date->copy()
            ->addDays($sale->client->payment_terms ?? 30);
    }

    // Create invoice
    return Invoice::create([
        'sale_id' => $sale->id,
        'invoice_number' => $sale->number,
        'format_type' => $format,
        'status' => Invoice::STATUS_ACTIVE,
        'due_date' => $dueDate,
        'generated_by' => auth()->id(),
    ]);
}

protected function determineFormat(Sale $sale): string
{
    // Letter format for credit sales
    if ($sale->payment_type === Sale::PAYMENT_CREDIT) {
        return Invoice::FORMAT_LETTER;
    }

    // Route format for route clients (if configured)
    if ($sale->client->route_sale) {
        return Invoice::FORMAT_ROUTE;
    }

    // Default: ticket format
    return Invoice::FORMAT_TICKET;
}

Printing Invoices

Controller Method

app/Http/Controllers/Sales/InvoiceController.php
public function print(Invoice $invoice)
{
    // Load all relationships
    $invoice->load([
        'sale.client',
        'sale.items.product',
        'sale.warehouse',
        'sale.ncfLog',
    ]);

    // Select view based on format
    $view = match($invoice->format_type) {
        Invoice::FORMAT_LETTER => 'invoices.print.letter',
        Invoice::FORMAT_ROUTE  => 'invoices.print.route',
        default                => 'invoices.print.ticket',
    };

    // Generate PDF
    $pdf = PDF::loadView($view, ['invoice' => $invoice]);

    // Set paper size
    $paperSize = match($invoice->format_type) {
        Invoice::FORMAT_TICKET => [80, 297],  // 80mm width
        Invoice::FORMAT_ROUTE  => [58, 297],  // 58mm width
        default                => 'letter',
    };

    $pdf->setPaper($paperSize, 'portrait');

    return $pdf->stream("invoice-{$invoice->invoice_number}.pdf");
}

From Sale

app/Http/Controllers/Sales/SaleController.php
public function printInvoice(Sale $sale)
{
    $invoice = $sale->invoice;

    if (!$invoice) {
        return back()->with('error', 'Esta venta aún no tiene una factura generada.');
    }

    return app(InvoiceController::class)->print($invoice);
}

Invoice Views

Ticket Template

resources/views/invoices/print/ticket.blade.php
<!DOCTYPE html>
<html>
<head>
    <style>
        body {
            font-family: 'Courier New', monospace;
            font-size: 10px;
            width: 80mm;
            margin: 0;
            padding: 5mm;
        }
        .center { text-align: center; }
        .bold { font-weight: bold; }
        .line { border-bottom: 1px dashed #000; margin: 5px 0; }
    </style>
</head>
<body>
    <div class="center bold">
        {{ config('app.name') }}
    </div>
    <div class="center">
        RNC: {{ config('company.rnc') }}
    </div>
    
    <div class="line"></div>

    <div>Factura: {{ $invoice->invoice_number }}</div>
    @if($invoice->sale->ncf)
    <div>NCF: {{ $invoice->sale->ncf }}</div>
    @endif
    <div>Fecha: {{ $invoice->created_at->format('d/m/Y H:i') }}</div>
    <div>Cliente: {{ $invoice->sale->client->display_name }}</div>

    <div class="line"></div>

    @foreach($invoice->sale->items as $item)
    <div>
        {{ $item->product->name }}
        <br>
        {{ $item->quantity }} x ${{ number_format($item->unit_price, 2) }}
        = ${{ number_format($item->subtotal, 2) }}
    </div>
    @endforeach

    <div class="line"></div>

    <div class="bold">TOTAL: ${{ number_format($invoice->sale->total_amount, 2) }}</div>

    @if($invoice->sale->payment_type === 'cash')
    <div>Pago: Contado</div>
    <div>Recibido: ${{ number_format($invoice->sale->cash_received, 2) }}</div>
    <div>Cambio: ${{ number_format($invoice->sale->cash_change, 2) }}</div>
    @else
    <div>Pago: Crédito</div>
    <div>Vence: {{ $invoice->due_date->format('d/m/Y') }}</div>
    @endif

    <div class="line"></div>
    <div class="center">Gracias por su compra!</div>
</body>
</html>

NCF Integration

Invoices display the fiscal number when available:
// Check if sale has NCF
@if($invoice->sale->ncf)
    <div>NCF: {{ $invoice->sale->ncf }}</div>
@endif
NCF Details:
$ncfLog = $invoice->sale->ncfLog;

if ($ncfLog) {
    echo "Type: {$ncfLog->ncfType->name}\n";
    echo "Number: {$ncfLog->full_ncf}\n";
    echo "Status: {$ncfLog->status}\n";
}

Invoice Cancellation

When a sale is canceled, its invoice is also canceled:
app/Services/Sales/InvoicesServices/InvoiceService.php
public function cancelInvoice(Sale $sale): void
{
    $invoice = $sale->invoice;

    if ($invoice) {
        $invoice->update([
            'status' => Invoice::STATUS_CANCELLED,
        ]);
    }
}
Canceled invoices cannot be reprinted or reactivated. The sale must be recreated if the cancellation was an error.

Querying Invoices

Recent Invoices

$recentInvoices = Invoice::withIndexRelations()
    ->latest()
    ->take(50)
    ->get();

Invoices by Status

$active = Invoice::where('status', Invoice::STATUS_ACTIVE)->count();
$cancelled = Invoice::where('status', Invoice::STATUS_CANCELLED)->count();

Invoices by Format

$tickets = Invoice::where('format_type', Invoice::FORMAT_TICKET)->get();
$letters = Invoice::where('format_type', Invoice::FORMAT_LETTER)->get();
$routes = Invoice::where('format_type', Invoice::FORMAT_ROUTE)->get();

Overdue Invoices (Credit)

$overdue = Invoice::where('status', Invoice::STATUS_ACTIVE)
    ->whereNotNull('due_date')
    ->where('due_date', '<', now())
    ->whereHas('sale', function($q) {
        $q->where('payment_type', Sale::PAYMENT_CREDIT);
    })
    ->with('sale.client')
    ->get();

Optimized Loading

app/Models/Sales/Invoice.php
public function scopeWithIndexRelations($query)
{
    return $query->with([
        'sale:id,number,payment_type,client_id,total_amount',
        'sale.client:id,name',
    ]);
}
Usage:
$invoices = Invoice::withIndexRelations()
    ->paginate(50);

// All data loaded efficiently
foreach ($invoices as $invoice) {
    echo "{$invoice->invoice_number} - {$invoice->sale->client->name}\n";
}

Format Icons

public static function getFormatIcons(): array
{
    return [
        self::FORMAT_TICKET => 'heroicon-s-printer',
        self::FORMAT_LETTER => 'heroicon-s-document-text',
        self::FORMAT_ROUTE  => 'heroicon-s-truck',
    ];
}
Display in Blade:
<x-icon :name="Invoice::getFormatIcons()[$invoice->format_type]" />
{{ Invoice::getFormats()[$invoice->format_type] }}

Best Practices

Select format based on business context:
  • Ticket - Fast retail transactions
  • Letter - Credit sales, corporate clients
  • Route - Mobile sales, deliveries
$format = $sale->payment_type === Sale::PAYMENT_CREDIT
    ? Invoice::FORMAT_LETTER
    : Invoice::FORMAT_TICKET;
Tax regulations require:
  • Company name and RNC
  • Invoice number
  • NCF (if applicable)
  • Date and time
  • Client information
  • Itemized products
  • Total amount
Configure printer settings for each format:
// Thermal printer settings
$pdf->setPaper([80, 297], 'portrait');
$pdf->setOption('dpi', 203);
$pdf->setOption('margin-top', 0);
$pdf->setOption('margin-bottom', 0);
Never delete invoices - mark as cancelled:
// Wrong
$invoice->delete();

// Correct
$invoice->update(['status' => Invoice::STATUS_CANCELLED]);
This maintains audit trails for tax compliance.

Customization

Company Branding

Add logos and styling to templates:
<div class="header">
    <img src="{{ public_path('images/logo.png') }}" width="150">
    <h2>{{ config('company.name') }}</h2>
    <p>{{ config('company.address') }}</p>
    <p>Tel: {{ config('company.phone') }}</p>
</div>

Custom Layouts

Create custom views for specific clients or industries:
if ($invoice->sale->client->custom_template) {
    $view = "invoices.custom.{$invoice->sale->client->template_name}";
}

Multi-Language Support

@if(app()->getLocale() === 'en')
    <div>Invoice Number: {{ $invoice->invoice_number }}</div>
@else
    <div>Número de Factura: {{ $invoice->invoice_number }}</div>
@endif

Sales Module

Sale creation and management

NCF Generation

Fiscal number compliance

Invoice Model API

Model reference

Point of Sale

POS integration

Build docs developers (and LLMs) love