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
30 changes: 30 additions & 0 deletions Modules/Core/Filament/Company/Pages/CompanySettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use BackedEnum;
use Filament\Actions\Action;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
Expand Down Expand Up @@ -153,6 +154,31 @@ protected function getFormSchema(): array
->placeholder('INV-')
->helperText(trans('ip.invoice_number_prefix_help')),
]),

Section::make(trans('ip.company_branding'))->columns(2)->schema([
ColorPicker::make(Setting::KEY_PRIMARY_COLOR)
->label(trans('ip.primary_color')),

ColorPicker::make(Setting::KEY_ACCENT_COLOR)
->label(trans('ip.accent_color')),

Select::make(Setting::KEY_FONT_FAMILY)
->label(trans('ip.font_family'))
->options([
'Arial' => 'Arial',
'Helvetica' => 'Helvetica',
'Georgia' => 'Georgia',
'Times New Roman' => 'Times New Roman',
])
->placeholder(trans('ip.none')),
Comment thread
coderabbitai[bot] marked this conversation as resolved.

TextInput::make(Setting::KEY_FONT_SIZE)
->label(trans('ip.font_size'))
->numeric()
->minValue(8)
->maxValue(72)
->placeholder('14'),
]),
]),

Tab::make('Amounts')
Expand Down Expand Up @@ -415,6 +441,10 @@ private function allKeys(): array
return [
Setting::KEY_COMPANY_NAME,
Setting::KEY_INVOICE_NUMBER_PREFIX,
Setting::KEY_PRIMARY_COLOR,
Setting::KEY_ACCENT_COLOR,
Setting::KEY_FONT_FAMILY,
Setting::KEY_FONT_SIZE,
Setting::KEY_CURRENCY_CODE,
Setting::KEY_DASHBOARD_SHOW_REVENUE_CHART,
Setting::KEY_CRON_FREQUENCY,
Expand Down
8 changes: 8 additions & 0 deletions Modules/Core/Models/Setting.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ class Setting extends Model

public const KEY_INVOICE_NUMBER_PREFIX = 'invoice_number_prefix';

public const KEY_PRIMARY_COLOR = 'primary_color';

public const KEY_ACCENT_COLOR = 'accent_color';

public const KEY_FONT_FAMILY = 'font_family';

public const KEY_FONT_SIZE = 'font_size';

public const KEY_CURRENCY_CODE = 'currency_code';

public const KEY_DASHBOARD_SHOW_REVENUE_CHART = 'dashboard_show_revenue_chart';
Expand Down
21 changes: 21 additions & 0 deletions Modules/Core/Tests/Feature/CompanySettingsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,27 @@ public function it_persists_a_long_text_setting(): void
$this->assertSame($text, Setting::getForCompany($this->company->id, Setting::KEY_INVOICE_DEFAULT_TERMS));
}

#[Test]
#[Group('per-company')]
public function it_persists_company_branding_settings(): void
{
/* Act */
Livewire::actingAs($this->user)
->test(CompanySettings::class)
->set('data.' . Setting::KEY_PRIMARY_COLOR, '#ff0000')
->set('data.' . Setting::KEY_ACCENT_COLOR, '#00ff00')
->set('data.' . Setting::KEY_FONT_FAMILY, 'Georgia')
->set('data.' . Setting::KEY_FONT_SIZE, 16)
->call('save')
->assertHasNoErrors();

/* Assert */
$this->assertSame('#ff0000', Setting::getForCompany($this->company->id, Setting::KEY_PRIMARY_COLOR));
$this->assertSame('#00ff00', Setting::getForCompany($this->company->id, Setting::KEY_ACCENT_COLOR));
$this->assertSame('Georgia', Setting::getForCompany($this->company->id, Setting::KEY_FONT_FAMILY));
$this->assertSame('16', Setting::getForCompany($this->company->id, Setting::KEY_FONT_SIZE));
}

#[Test]
#[Group('per-company')]
public function it_prefills_form_state_from_existing_settings(): void
Expand Down
30 changes: 29 additions & 1 deletion Modules/Invoices/Services/InvoiceService.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Modules\Clients\Enums\CommunicationType;
use Modules\Core\Enums\MailType;
use Modules\Core\Models\EmailTemplate;
use Modules\Core\Models\Setting;
use Modules\Core\Services\BaseService;
use Modules\Core\Support\DateHelpers;
use Modules\Core\Support\EmailTemplatePreview;
Expand Down Expand Up @@ -328,7 +330,10 @@ public function renderHtml(Invoice $invoice): string
{
$invoice->loadMissing(['company', 'customer', 'invoiceItems']);

return view('invoices::pdf.invoice', ['invoice' => $invoice])->render();
return view('invoices::pdf.invoice', [
'invoice' => $invoice,
'branding' => $this->resolveBranding($invoice),
])->render();
}

/**
Expand Down Expand Up @@ -411,6 +416,29 @@ public function createCreditNote(Invoice $invoice): Invoice
});
}

/**
* Company branding for the invoice PDF/preview: colors, font, and logo.
* Falls back to the current hardcoded look when a company hasn't set
* any branding, so existing invoices render unchanged.
*
* @return array{primary_color: string, accent_color: string, font_family: string, font_size: string, logo_path: ?string}
*/
private function resolveBranding(Invoice $invoice): array
{
$companyId = $invoice->company_id;

$logoPath = Setting::getForCompany($companyId, Setting::KEY_INVOICE_LOGO);
$logoDisk = Storage::disk(config('filament.default_filesystem_disk'));

return [
'primary_color' => Setting::getForCompany($companyId, Setting::KEY_PRIMARY_COLOR) ?: '#1f2937',
'accent_color' => Setting::getForCompany($companyId, Setting::KEY_ACCENT_COLOR) ?: '#6b7280',
'font_family' => Setting::getForCompany($companyId, Setting::KEY_FONT_FAMILY) ?: 'DejaVu Sans, Helvetica, Arial, sans-serif',
'font_size' => Setting::getForCompany($companyId, Setting::KEY_FONT_SIZE) ?: '12',
'logo_path' => $logoPath && $logoDisk->exists($logoPath) ? $logoDisk->path($logoPath) : null,
];
}

/**
* Shared resolution logic for the "Email Invoice" and "Send Reminder"
* modals: loads the named company EmailTemplate once, renders its
Expand Down
59 changes: 59 additions & 0 deletions Modules/Invoices/Tests/Feature/InvoicePdfAndCreditNoteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

namespace Modules\Invoices\Tests\Feature;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use InvalidArgumentException;
use Livewire\Livewire;
use Modules\Clients\Models\Relation;
use Modules\Core\Database\Seeders\PermissionsSeeder;
use Modules\Core\Database\Seeders\RolesSeeder;
use Modules\Core\Enums\UserRole;
use Modules\Core\Models\Numbering;
use Modules\Core\Models\Setting;
use Modules\Core\Support\PDF\PDFFactory;
use Modules\Core\Tests\AbstractCompanyPanelTestCase;
use Modules\Invoices\Enums\InvoiceStatus;
Expand Down Expand Up @@ -63,6 +66,62 @@ public function it_renders_invoice_html_with_number_and_customer(): void
$this->assertStringNotContainsString('<iframe', $html);
}

#[Test]
#[Group('crud')]
public function it_falls_back_to_default_branding_when_none_is_set(): void
{
/* Arrange */
$invoice = $this->createInvoice(InvoiceStatus::SENT, ['footer' => 'Thank you for your business.']);

/* Act */
$html = $this->service->renderHtml($invoice);

/* Assert */
$this->assertStringContainsString('#1f2937', $html);
$this->assertStringContainsString('#6b7280', $html);
$this->assertStringNotContainsString('<img', $html);
}

#[Test]
#[Group('crud')]
public function it_renders_company_branding_colors_and_font_in_the_invoice_html(): void
{
/* Arrange */
Setting::saveForCompany($this->company->id, Setting::KEY_PRIMARY_COLOR, '#112233');
Setting::saveForCompany($this->company->id, Setting::KEY_ACCENT_COLOR, '#445566');
Setting::saveForCompany($this->company->id, Setting::KEY_FONT_FAMILY, 'Georgia');
Setting::saveForCompany($this->company->id, Setting::KEY_FONT_SIZE, '16');

$invoice = $this->createInvoice(InvoiceStatus::SENT, ['footer' => 'Thank you for your business.']);

/* Act */
$html = $this->service->renderHtml($invoice);

/* Assert */
$this->assertStringContainsString('#112233', $html);
$this->assertStringContainsString('#445566', $html);
$this->assertStringContainsString('Georgia', $html);
$this->assertStringContainsString('16px', $html);
}

#[Test]
#[Group('crud')]
public function it_renders_the_company_logo_when_set(): void
{
/* Arrange */
Storage::fake('local');
$path = UploadedFile::fake()->image('logo.png')->store('invoice-logos', 'local');
Setting::saveForCompany($this->company->id, Setting::KEY_INVOICE_LOGO, $path);

$invoice = $this->createInvoice(InvoiceStatus::SENT);

/* Act */
$html = $this->service->renderHtml($invoice);

/* Assert */
$this->assertStringContainsString('<img src="' . Storage::disk('local')->path($path) . '"', $html);
}

// dompdf/dompdf is in composer.lock but not actually installed in the
// ip2-test-php:8.4 image's vendor tree.
#[Test]
Expand Down
27 changes: 17 additions & 10 deletions Modules/Invoices/resources/views/pdf/invoice.blade.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
{{-- Invoice document markup — used by the PDF driver and the on-screen preview. --}}
<div class="ip-invoice" style="font-family: DejaVu Sans, Helvetica, Arial, sans-serif; color: #1f2937; font-size: 12px; line-height: 1.5;">
@php
$primaryColor = $branding['primary_color'];
$accentColor = $branding['accent_color'];
@endphp
<div class="ip-invoice" style="font-family: {{ $branding['font_family'] }}; color: {{ $primaryColor }}; font-size: {{ $branding['font_size'] }}px; line-height: 1.5;">
<table style="width: 100%; border-collapse: collapse; margin-bottom: 24px;">
<tr>
<td style="vertical-align: top;">
@if ($branding['logo_path'])
<img src="{{ $branding['logo_path'] }}" alt="{{ $invoice->company?->name }}" style="max-height: 60px; max-width: 240px; margin-bottom: 8px;">
@endif
<div style="font-size: 20px; font-weight: bold;">{{ $invoice->company?->name }}</div>
</td>
<td style="vertical-align: top; text-align: right;">
Expand Down Expand Up @@ -31,11 +38,11 @@
<table style="width: 100%; border-collapse: collapse; margin-bottom: 24px;">
<thead>
<tr>
<th style="text-align: left; border-bottom: 2px solid #1f2937; padding: 6px 4px;">{{ trans('ip.item') }}</th>
<th style="text-align: right; border-bottom: 2px solid #1f2937; padding: 6px 4px;">{{ trans('ip.quantity') }}</th>
<th style="text-align: right; border-bottom: 2px solid #1f2937; padding: 6px 4px;">{{ trans('ip.price') }}</th>
<th style="text-align: right; border-bottom: 2px solid #1f2937; padding: 6px 4px;">{{ trans('ip.discount') }}</th>
<th style="text-align: right; border-bottom: 2px solid #1f2937; padding: 6px 4px;">{{ trans('ip.subtotal') }}</th>
<th style="text-align: left; border-bottom: 2px solid {{ $primaryColor }}; padding: 6px 4px;">{{ trans('ip.item') }}</th>
<th style="text-align: right; border-bottom: 2px solid {{ $primaryColor }}; padding: 6px 4px;">{{ trans('ip.quantity') }}</th>
<th style="text-align: right; border-bottom: 2px solid {{ $primaryColor }}; padding: 6px 4px;">{{ trans('ip.price') }}</th>
<th style="text-align: right; border-bottom: 2px solid {{ $primaryColor }}; padding: 6px 4px;">{{ trans('ip.discount') }}</th>
<th style="text-align: right; border-bottom: 2px solid {{ $primaryColor }}; padding: 6px 4px;">{{ trans('ip.subtotal') }}</th>
</tr>
</thead>
<tbody>
Expand All @@ -44,7 +51,7 @@
<td style="border-bottom: 1px solid #e5e7eb; padding: 6px 4px;">
{{ $item->item_name }}
@if ($item->description)
<div style="color: #6b7280;">{{ $item->description }}</div>
<div style="color: {{ $accentColor }};">{{ $item->description }}</div>
@endif
</td>
<td style="text-align: right; border-bottom: 1px solid #e5e7eb; padding: 6px 4px;">{{ $item->quantity + 0 }}</td>
Expand Down Expand Up @@ -72,8 +79,8 @@
</tr>
@endif
<tr>
<td style="padding: 4px; border-top: 2px solid #1f2937; font-weight: bold;">{{ trans('ip.total') }}</td>
<td style="text-align: right; padding: 4px; border-top: 2px solid #1f2937; font-weight: bold;">{{ number_format((float) $invoice->invoice_total, 2) }}</td>
<td style="padding: 4px; border-top: 2px solid {{ $primaryColor }}; font-weight: bold;">{{ trans('ip.total') }}</td>
<td style="text-align: right; padding: 4px; border-top: 2px solid {{ $primaryColor }}; font-weight: bold;">{{ number_format((float) $invoice->invoice_total, 2) }}</td>
</tr>
</table>

Expand All @@ -92,6 +99,6 @@
@endif

@if ($invoice->footer)
<div style="color: #6b7280; margin-top: 24px;">{{ $invoice->footer }}</div>
<div style="color: {{ $accentColor }}; margin-top: 24px;">{{ $invoice->footer }}</div>
@endif
</div>
71 changes: 38 additions & 33 deletions resources/lang/en/ip.php
Original file line number Diff line number Diff line change
Expand Up @@ -903,29 +903,29 @@
#endregion

#region NUMBERING
'numbering' => 'Numbering',
'numberings' => 'Numberings',
'numbering_company' => 'Company',
'numbering_company_assignment' => 'Company Assignment',
'numbering_select_company_help' => 'Select which company this numbering scheme belongs to',
'numbering_type' => 'Type',
'numbering_name' => 'Name',
'numbering_next_id' => 'Next ID',
'numbering_next_id_help' => 'Can be adjusted to troubleshoot numbering issues',
'numbering_left_pad' => 'Left Pad',
'numbering_prefix' => 'Prefix',
'numbering_format' => 'Format',
'numbering_format_placeholder' => '{{prefix}}-{{number}}',
'numbering_format_help' => 'Use {{prefix}}, {{number}}, {{year}}, {{yy}}, {{month}}, {{day}} as placeholders, or click a token below to insert it. Only dash (-) or underscore (_) separators allowed.',
'numbering_format_helper' => 'You can customize the format using placeholders: {{prefix}} for prefix, {{number}} for sequential number, {{year}} for 4-digit year, {{yy}} for 2-digit year, {{month}} for month, {{day}} for day. The number will be left-padded according to the Left Pad setting.',
'numbering_format_help_label' => 'Format Help',
'numbering_group_identifier_format' => 'Group Identifier Format',
'numbering' => 'Numbering',
'numberings' => 'Numberings',
'numbering_company' => 'Company',
'numbering_company_assignment' => 'Company Assignment',
'numbering_select_company_help' => 'Select which company this numbering scheme belongs to',
'numbering_type' => 'Type',
'numbering_name' => 'Name',
'numbering_next_id' => 'Next ID',
'numbering_next_id_help' => 'Can be adjusted to troubleshoot numbering issues',
'numbering_left_pad' => 'Left Pad',
'numbering_prefix' => 'Prefix',
'numbering_format' => 'Format',
'numbering_format_placeholder' => '{{prefix}}-{{number}}',
'numbering_format_help' => 'Use {{prefix}}, {{number}}, {{year}}, {{yy}}, {{month}}, {{day}} as placeholders, or click a token below to insert it. Only dash (-) or underscore (_) separators allowed.',
'numbering_format_helper' => 'You can customize the format using placeholders: {{prefix}} for prefix, {{number}} for sequential number, {{year}} for 4-digit year, {{yy}} for 2-digit year, {{month}} for month, {{day}} for day. The number will be left-padded according to the Left Pad setting.',
'numbering_format_help_label' => 'Format Help',
'numbering_group_identifier_format' => 'Group Identifier Format',
'numbering_group_identifier_format_placeholder' => '{{prefix}}-{{year}}-{{number}}',
'numbering_group_identifier_format_help' => 'A separate, optional format used only to group related numbers together (e.g. by year or batch) -- distinct from the main Format above. Uses the same placeholders: {{prefix}}, {{number}}, {{year}}, {{yy}}, {{month}}, {{day}}.',
'duplicate_invoice_number' => 'Duplicate invoice number :number for company :company',
'duplicate_quote_number' => 'Duplicate quote number :number for company :company',
'quote_created_with_number' => 'Quote :number created',
'invoice_created_with_number' => 'Invoice :number created',
'duplicate_invoice_number' => 'Duplicate invoice number :number for company :company',
'duplicate_quote_number' => 'Duplicate quote number :number for company :company',
'quote_created_with_number' => 'Quote :number created',
'invoice_created_with_number' => 'Invoice :number created',
#endregion

#region REPORT BUILDER
Expand Down Expand Up @@ -1288,17 +1288,22 @@
#endregion

#region COMPANY SETTINGS (2026-07-19, epic #508)
'dashboard_show_revenue_chart' => 'Show Revenue Chart on Dashboard',
'cron_frequency' => 'Recurring Invoice Frequency',
'cron_frequency_daily' => 'Daily',
'cron_frequency_weekly' => 'Weekly',
'cron_frequency_monthly' => 'Monthly',
'time_format' => 'Time Format',
'invoice_numbering' => 'Numbering',
'invoice_number_prefix' => 'Invoice Number Prefix',
'invoice_number_prefix_help' => 'Prepended to every auto-generated invoice number for this company (e.g. INV-).',
'invoice_email_subject' => 'Invoice Email Subject',
'default_invoice_footer' => 'Default Invoice Footer',
'default_quote_tax_rate' => 'Default Quote Tax Rate',
'dashboard_show_revenue_chart' => 'Show Revenue Chart on Dashboard',
'cron_frequency' => 'Recurring Invoice Frequency',
'cron_frequency_daily' => 'Daily',
'cron_frequency_weekly' => 'Weekly',
'cron_frequency_monthly' => 'Monthly',
'time_format' => 'Time Format',
'invoice_numbering' => 'Numbering',
'invoice_number_prefix' => 'Invoice Number Prefix',
'invoice_number_prefix_help' => 'Prepended to every auto-generated invoice number for this company (e.g. INV-).',
'company_branding' => 'Company Branding',
'primary_color' => 'Primary Color',
'accent_color' => 'Accent Color',
'font_family' => 'Font Family',
'font_size' => 'Font Size',
'invoice_email_subject' => 'Invoice Email Subject',
'default_invoice_footer' => 'Default Invoice Footer',
'default_quote_tax_rate' => 'Default Quote Tax Rate',
#endregion
];
Loading