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
15 changes: 13 additions & 2 deletions app/Filament/Resources/ExpenseResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Closure;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Number;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\BulkActionGroup;
Expand Down Expand Up @@ -73,8 +74,18 @@ public static function table(Table $table): Table
->label(__('vat'))
->money('eur')
->fontFamily(FontFamily::Mono)
->state(fn(Expense $record): float => $record->vat)
->color(fn(string $state): string => $state == 0 ? 'gray' : 'normal')
->state(fn(Expense $record): float => $record->deductibleVat)
->color(fn(Expense $record): string => match (true) {
$record->taxable_ratio < 1 => 'warning',
$record->deductibleVat == 0 => 'gray',
default => 'normal',
})
->tooltip(fn(Expense $record): ?string => $record->taxable_ratio < 1
? __('taxableRatioTooltip', [
'ratio' => Number::percentage($record->taxable_ratio * 100),
'vat' => Number::currency($record->vat, 'eur'),
])
: null)
->sortable(),
TextColumn::make('quantity')
->label(__('quantity'))
Expand Down
15 changes: 13 additions & 2 deletions app/Filament/Widgets/MinorAssetsList.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Filament\Widgets\TableWidget;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Number;

class MinorAssetsList extends TableWidget
{
Expand Down Expand Up @@ -60,8 +61,18 @@ public function table(Table $table): Table
->label(__('vat'))
->money('eur')
->fontFamily(FontFamily::Mono)
->state(fn(Expense $record): float => $record->vat)
->color(fn(string $state): string => $state == 0 ? 'gray' : 'normal')
->state(fn(Expense $record): float => $record->deductibleVat)
->color(fn(Expense $record): string => match (true) {
$record->taxable_ratio < 1 => 'warning',
$record->deductibleVat == 0 => 'gray',
default => 'normal',
})
->tooltip(fn(Expense $record): ?string => $record->taxable_ratio < 1
? __('taxableRatioTooltip', [
'ratio' => Number::percentage($record->taxable_ratio * 100),
'vat' => Number::currency($record->vat, 'eur'),
])
: null)
->sortable(),
TextColumn::make('net')
->label(__('net'))
Expand Down
10 changes: 9 additions & 1 deletion app/Models/Expense.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public static function ofTime(Carbon $d, TimeUnit $u, ?ExpenseCategory $category
->whereIn('category', $categories)
->get();
$net = array_sum($records->map(fn(self $r) => $r->deductibleNet)->toArray());
$vat = array_sum($records->map(fn(self $r) => $r->vat)->toArray());
$vat = array_sum($records->map(fn(self $r) => $r->deductibleVat)->toArray());
return [$net, $vat];
}

Expand Down Expand Up @@ -136,4 +136,12 @@ protected function deductibleNet(): Attribute
{
return Attribute::make(fn(): float => round($this->net * $this->taxable_ratio, 2));
}

/**
* Vat amount of this expense that is actually deductible, pro-rated by taxable_ratio
*/
protected function deductibleVat(): Attribute
{
return Attribute::make(fn(): float => round($this->vat * $this->taxable_ratio, 2));
}
}
1 change: 1 addition & 0 deletions lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@
"sumWorkingHoursPerYear": "Summe bezahlter Arbeitsstunden pro Jahr",
"taxable": "Versteuert",
"taxableRatio": "Abzugsfähiger Anteil",
"taxableRatioTooltip": "Anteilig :ratio von :vat USt.",
"taxes": "Steuern",
"taxOffice": "Finanzamt",
"taxReport": "Steuererklärung",
Expand Down
1 change: 1 addition & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@
"sumWorkingHoursPerYear": "Sum of paid working hours per year",
"taxable": "Taxable",
"taxableRatio": "Deductible share",
"taxableRatioTooltip": "Pro-rated to :ratio of :vat VAT",
"taxes": "Taxes",
"taxOffice": "Tax office",
"taxReport": "Tax return",
Expand Down
17 changes: 17 additions & 0 deletions tests/Feature/ExpenseResourceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@ public function it_lists_expenses_in_the_table(): void
->assertCanSeeTableRecords($expenses);
}

#[Test]
public function it_shows_the_pro_rated_vat_in_the_table(): void
{
$this->actingAs(User::factory()->create());
$expense = Expense::factory()->create([
'price' => 100,
'quantity' => 1,
'taxable' => true,
'vat_rate' => 0.19,
'taxable_ratio' => 0.5,
]);

Livewire::test(ListExpenses::class, ['activeTab' => 'all'])
->loadTable()
->assertTableColumnStateSet('vat', 7.99, $expense);
}

#[Test]
public function it_creates_an_expense(): void
{
Expand Down
20 changes: 20 additions & 0 deletions tests/Feature/MinorAssetsListTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,26 @@ public function it_only_lists_minor_assets_expenses_from_the_selected_year(): vo
$this->assertTrue($records->contains($inYear));
}

#[Test]
public function it_shows_the_pro_rated_vat_in_the_table(): void
{
$this->actingAs(User::factory()->create());
$year = now()->year - 1;
$expense = Expense::factory()->create([
'expended_at' => "$year-06-01",
'category' => ExpenseCategory::MinorAssets,
'price' => 500,
'quantity' => 1,
'taxable' => true,
'vat_rate' => 0.19,
'taxable_ratio' => 0.5,
]);

Livewire::test(MinorAssetsList::class)
->loadTable()
->assertTableColumnStateSet('vat', round($expense->vat * 0.5, 2), $expense);
}

#[Test]
public function it_hides_the_bill_download_until_a_document_exists(): void
{
Expand Down
20 changes: 20 additions & 0 deletions tests/Feature/TaxOverviewTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ public function it_summarizes_taxable_net_and_vat_for_the_current_month(): void
$this->assertSame(174.03, $record['totalVat']);
}

#[Test]
public function it_pro_rates_the_expense_vat_by_taxable_ratio(): void
{
Expense::factory()->create([
'expended_at' => now(),
'category' => ExpenseCategory::Good,
'price' => 100,
'quantity' => 1,
'taxable' => true,
'vat_rate' => 0.19,
'taxable_ratio' => 0.5,
]);

$widget = new TaxOverview();
$record = $widget->getTableRecords()->first();

$this->assertSame(7.99, $record['vatExpenses']);
$this->assertSame(-7.99, $record['totalVat']);
}

#[Test]
public function it_disables_the_last_advance_vat_action_when_it_already_exists(): void
{
Expand Down
23 changes: 23 additions & 0 deletions tests/Feature/TaxReturnFormInputTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,29 @@ public function it_computes_the_tax_return_lines_for_the_selected_year(): void
$this->assertSame(161.0, $records[1]['value']); // itr1 - taxable profit, rounded to whole euros for Elster (raw value is 160.76)
}

#[Test]
public function it_pro_rates_expense_vat_by_taxable_ratio(): void
{
$year = now()->year - 1;

Expense::factory()->create([
'expended_at' => "$year-06-01",
'category' => ExpenseCategory::Good,
'price' => 100,
'quantity' => 1,
'taxable' => true,
'vat_rate' => 0.19,
'taxable_ratio' => 0.5,
]);

$widget = new TaxReturnFormInput();
$widget->filter = $year;
$records = $widget->getTableRecords()->keyBy('__key');

$this->assertSame(7.99, $records[11]['value']); // rsc57 - vat expended, pro-rated by taxable_ratio
$this->assertSame(-7.99, $records[14]['value']); // vr118 - vat payable, must reflect the pro-rated vat
}

#[Test]
public function it_rounds_the_final_profit_line_to_cents_despite_floating_point_noise(): void
{
Expand Down
31 changes: 28 additions & 3 deletions tests/Unit/Models/ExpenseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,30 @@
expect($expense->deductibleNet)->toBe($expense->net);
});

it('pro-rates the deductible vat amount by taxable_ratio', function () {
$expense = Expense::factory()->create([
'price' => 100,
'quantity' => 1,
'taxable' => true,
'vat_rate' => 0.19,
'taxable_ratio' => 0.5,
]);

expect($expense->vat)->toBe(15.97)
->and($expense->deductibleVat)->toBe(7.99);
});

it('leaves the deductible vat amount unchanged when taxable_ratio is 1', function () {
$expense = Expense::factory()->create([
'price' => 100,
'quantity' => 1,
'taxable' => true,
'vat_rate' => 0.19,
])->refresh();

expect($expense->deductibleVat)->toBe($expense->vat);
});

it('calculates gross, net and vat amounts based on price, quantity and vat rate', function () {
$expense = Expense::factory()->create([
'price' => 100,
Expand Down Expand Up @@ -201,7 +225,7 @@
->and($vat)->toBe(0.0);
});

it('sums the deductible net amount honoring taxable_ratio within a time range', function () {
it('sums the deductible net and vat amounts honoring taxable_ratio within a time range', function () {
$month = Carbon::parse('2026-03-15');

Expense::factory()->create([
Expand All @@ -214,7 +238,8 @@
'category' => ExpenseCategory::Good,
]);

[$net] = Expense::ofTime($month, TimeUnit::MONTH, ExpenseCategory::Good);
[$net, $vat] = Expense::ofTime($month, TimeUnit::MONTH, ExpenseCategory::Good);

expect($net)->toBe(42.02);
expect($net)->toBe(42.02)
->and($vat)->toBe(7.99);
});
Loading