Fix #57: Lineare Abschreibungstabelle für Anlagegüter
This commit is contained in:
parent
bbfb41232e
commit
41d64dc6f6
10 changed files with 804 additions and 62 deletions
16
Changes.md
16
Changes.md
|
|
@ -2,6 +2,22 @@
|
|||
|
||||
---
|
||||
|
||||
## 2026-04-26 – Feature #57: Lineare Abschreibungstabelle für Anlagegüter
|
||||
|
||||
### Hinzugefügt
|
||||
- `migrations/035_assets_abschreibung_flags.sql`: Neue Spalten `automatisch_berechnet` und `cent_im_ersten_jahr` in `assets`
|
||||
- `migrations/036_asset_depreciation_schedule.sql`: Neue Tabelle `asset_depreciation_schedule` (Jahr, Abschreibungsbetrag, Restwert)
|
||||
- `src/Models/AssetDepreciation.php`: Berechnung (linear, Sofortabschreibung, Cent-Option), Persistenz
|
||||
- `tests/26_AbschreibungstabelleTest.php`: 22 Tests für alle AfA-Szenarien
|
||||
|
||||
### Geändert
|
||||
- `src/Models/Asset.php`: `create()` und `update()` speichern `automatisch_berechnet` und `cent_im_ersten_jahr`
|
||||
- `src/Controllers/AssetController.php`: Abschreibungstabelle wird nach Speichern automatisch generiert; neue Endpunkte `/assets/depreciation-preview` (AJAX-Vorschau) und `/assets/{id}/depreciation` (manuelle Speicherung)
|
||||
- `src/Views/assets/form.php`: Nutzungsdauer-Dropdown (sofort, 1–10, 12, 13, 15, 20, 25, 50 Jahre + individuell), Cent-Option, „AfA-Tabelle"-Button, Modal mit Auto/Manuell-Toggle und editierbarer Tabelle
|
||||
- `public/index.php`: Neue Routen für Depreciation-Preview und -Speicherung
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-26 – Fix UStVA: Kennziffern auf offizielle ELSTER-Definitionen korrigiert
|
||||
|
||||
### Geändert
|
||||
|
|
|
|||
|
|
@ -65,5 +65,36 @@ Folgende Felder können nachträglich angepasst werden:
|
|||
- Anschaffungsdatum und -kosten
|
||||
- Zugänge und Abgänge
|
||||
- Abschreibung laufendes Jahr
|
||||
- Nutzungsdauer (in Jahren)
|
||||
- Nutzungsdauer (Dropdown: sofort, 1–10, 12, 13, 15, 20, 25, 50 Jahre oder individuell)
|
||||
- Centbeträge im ersten Jahr abschreiben (Option)
|
||||
- Notizen
|
||||
|
||||
---
|
||||
|
||||
## Abschreibungstabelle (AfA)
|
||||
|
||||
Für jedes gespeicherte Wirtschaftsgut wird automatisch eine lineare AfA-Tabelle erzeugt und persistent gespeichert (`asset_depreciation_schedule`). Die Tabelle enthält pro Jahr: **Jahr**, **Abschreibungsbetrag**, **Restwert**.
|
||||
|
||||
### Berechnungsregeln
|
||||
|
||||
| Modus | Verhalten |
|
||||
|---|---|
|
||||
| **Nutzungsdauer = sofort** | Vollständige Abschreibung im Anschaffungsjahr, Restwert = 0 |
|
||||
| **Nutzungsdauer = N Jahre** | Gleichmäßige lineare Verteilung; letztes Jahr gleicht Rundungsdifferenzen aus |
|
||||
| **Cent-Option aktiv** | Folgejahre auf volle Euro abgerundet; erstes Jahr übernimmt alle Centbeträge; Restwert nach Jahr 1 ist volle Euro |
|
||||
|
||||
Basis der Berechnung: `Anschaffungskosten + Zugänge − Abgänge`.
|
||||
|
||||
### Auto- und Manuell-Modus
|
||||
|
||||
Die Tabelle besitzt ein Flag `automatisch_berechnet`:
|
||||
- **Automatisch (Standard):** Bei jeder Parameteränderung (Nutzungsdauer, Cent-Option, Kosten) wird die Tabelle neu berechnet.
|
||||
- **Manuell:** Durch Klick auf „Manuell bearbeiten" im Modal wird das Flag deaktiviert. Parameteränderungen zeigen eine Bestätigungsabfrage, bevor neu berechnet wird.
|
||||
|
||||
### Bedienung
|
||||
|
||||
1. Wirtschaftsgut speichern → Tabelle wird automatisch berechnet.
|
||||
2. Button **„AfA-Tabelle"** öffnet den modalen Dialog.
|
||||
3. Im Dialog: Toggle „Manuell bearbeiten" aktiviert editierbare Zellen.
|
||||
4. **„Neu berechnen"** überschreibt manuelle Änderungen (nach Bestätigung).
|
||||
5. **„Speichern"** sichert die aktuelle Tabelle (inklusive manueller Änderungen) in der Datenbank.
|
||||
|
|
|
|||
8
migrations/035_assets_abschreibung_flags.sql
Normal file
8
migrations/035_assets_abschreibung_flags.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- Migration 035: Abschreibungsflags für Wirtschaftsgüter
|
||||
ALTER TABLE assets
|
||||
ADD COLUMN IF NOT EXISTS automatisch_berechnet TINYINT(1) NOT NULL DEFAULT 1
|
||||
COMMENT 'true = Abschreibungstabelle automatisch berechnet, false = manuell bearbeitet'
|
||||
AFTER nutzungsdauer,
|
||||
ADD COLUMN IF NOT EXISTS cent_im_ersten_jahr TINYINT(1) NOT NULL DEFAULT 0
|
||||
COMMENT 'Centbeträge vollständig im ersten Jahr abschreiben; Folgejahre auf volle Euro'
|
||||
AFTER automatisch_berechnet;
|
||||
13
migrations/036_asset_depreciation_schedule.sql
Normal file
13
migrations/036_asset_depreciation_schedule.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
-- Migration 036: Abschreibungstabelle je Wirtschaftsgut
|
||||
CREATE TABLE IF NOT EXISTS asset_depreciation_schedule (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
asset_id INT UNSIGNED NOT NULL,
|
||||
jahr SMALLINT UNSIGNED NOT NULL COMMENT 'Kalenderjahr der Abschreibung',
|
||||
abschreibungsbetrag DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
restwert DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
position SMALLINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Reihenfolge (0-basiert)',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_ads_asset_id (asset_id),
|
||||
UNIQUE KEY uq_ads_asset_jahr (asset_id, jahr)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
COMMENT='Lineare Abschreibungstabelle je Wirtschaftsgut';
|
||||
|
|
@ -103,9 +103,11 @@ $router->post('/invoices/{id}/lines/{line_id}/delete', fn($p) => (new InvoiceCon
|
|||
$router->get('/assets', fn($p) => (new AssetController())->index($p));
|
||||
$router->get('/assets/new', fn($p) => (new AssetController())->create($p));
|
||||
$router->post('/assets', fn($p) => (new AssetController())->store($p));
|
||||
$router->get('/assets/depreciation-preview', fn($p) => (new AssetController())->depreciationPreview($p));
|
||||
$router->get('/assets/{id}/edit', fn($p) => (new AssetController())->edit($p));
|
||||
$router->post('/assets/{id}', fn($p) => (new AssetController())->update($p));
|
||||
$router->post('/assets/{id}/delete', fn($p) => (new AssetController())->destroy($p));
|
||||
$router->post('/assets/{id}/depreciation', fn($p) => (new AssetController())->saveDepreciation($p));
|
||||
|
||||
// ── UStVA ────────────────────────────────────────────────────
|
||||
$router->get('/tax/ustva', fn($p) => (new UstvaController())->index($p));
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace Steuerkram\Controllers;
|
||||
|
||||
use Steuerkram\Models\Asset;
|
||||
use Steuerkram\Models\AssetDepreciation;
|
||||
use Steuerkram\View;
|
||||
|
||||
class AssetController
|
||||
|
|
@ -33,6 +34,7 @@ class AssetController
|
|||
'errors' => [],
|
||||
'action' => '/assets',
|
||||
'anlagearten' => Asset::anlagearten(),
|
||||
'scheduleRows' => [],
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
|
@ -51,13 +53,19 @@ class AssetController
|
|||
'errors' => $errors,
|
||||
'action' => '/assets',
|
||||
'anlagearten' => Asset::anlagearten(),
|
||||
'scheduleRows' => [],
|
||||
]),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
Asset::create($data);
|
||||
View::redirect('/assets', 'Wirtschaftsgut erfolgreich angelegt.');
|
||||
$id = Asset::create($data);
|
||||
$asset = Asset::find($id);
|
||||
if ($asset && (int)($asset['automatisch_berechnet'] ?? 1) === 1) {
|
||||
AssetDepreciation::generateAndSave($asset);
|
||||
}
|
||||
|
||||
View::redirect('/assets/' . $id . '/edit', 'Wirtschaftsgut erfolgreich angelegt.');
|
||||
}
|
||||
|
||||
public function edit(array $params): void
|
||||
|
|
@ -65,6 +73,8 @@ class AssetController
|
|||
$asset = Asset::find((int)$params['id']);
|
||||
if (!$asset) { http_response_code(404); return; }
|
||||
|
||||
$scheduleRows = AssetDepreciation::findByAsset((int)$asset['id']);
|
||||
|
||||
$view = new View();
|
||||
$view->render('layout/base.php', [
|
||||
'pageTitle' => 'Wirtschaftsgut bearbeiten',
|
||||
|
|
@ -73,6 +83,7 @@ class AssetController
|
|||
'errors' => [],
|
||||
'action' => '/assets/' . $asset['id'],
|
||||
'anlagearten' => Asset::anlagearten(),
|
||||
'scheduleRows' => $scheduleRows,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
|
@ -87,6 +98,7 @@ class AssetController
|
|||
$errors = $this->validate($data);
|
||||
|
||||
if ($errors) {
|
||||
$scheduleRows = AssetDepreciation::findByAsset($id);
|
||||
$view = new View();
|
||||
$view->render('layout/base.php', [
|
||||
'pageTitle' => 'Wirtschaftsgut bearbeiten',
|
||||
|
|
@ -95,24 +107,103 @@ class AssetController
|
|||
'errors' => $errors,
|
||||
'action' => '/assets/' . $id,
|
||||
'anlagearten' => Asset::anlagearten(),
|
||||
'scheduleRows' => $scheduleRows,
|
||||
]),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Wenn Parameter geändert und Tabelle war manuell: Flag beibehalten
|
||||
$wasAutomatisch = (bool)(int)($asset['automatisch_berechnet'] ?? 1);
|
||||
Asset::update($id, $data);
|
||||
View::redirect('/assets', 'Wirtschaftsgut aktualisiert.');
|
||||
|
||||
$updatedAsset = Asset::find($id);
|
||||
$isAutomatisch = (bool)(int)($updatedAsset['automatisch_berechnet'] ?? 1);
|
||||
|
||||
if ($isAutomatisch) {
|
||||
AssetDepreciation::generateAndSave($updatedAsset);
|
||||
}
|
||||
|
||||
View::redirect('/assets/' . $id . '/edit', 'Wirtschaftsgut aktualisiert.');
|
||||
}
|
||||
|
||||
public function destroy(array $params): void
|
||||
{
|
||||
if (!Asset::delete((int)$params['id'])) {
|
||||
$id = (int)$params['id'];
|
||||
AssetDepreciation::deleteByAsset($id);
|
||||
if (!Asset::delete($id)) {
|
||||
View::redirect('/assets', 'Wirtschaftsgut konnte nicht gelöscht werden.', 'error');
|
||||
return;
|
||||
}
|
||||
View::redirect('/assets', 'Wirtschaftsgut gelöscht.');
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Abschreibungstabelle vorschau berechnen (ohne DB-Speicherung)
|
||||
* GET /assets/depreciation-preview?anschaffungskosten=...&nutzungsdauer=...&...
|
||||
*/
|
||||
public function depreciationPreview(array $params): void
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$asset = [
|
||||
'anschaffungskosten' => (float)($_GET['anschaffungskosten'] ?? 0),
|
||||
'zugaenge' => (float)($_GET['zugaenge'] ?? 0),
|
||||
'abgaenge' => (float)($_GET['abgaenge'] ?? 0),
|
||||
'nutzungsdauer' => (int)($_GET['nutzungsdauer'] ?? 0),
|
||||
'cent_im_ersten_jahr'=> !empty($_GET['cent_im_ersten_jahr']) ? 1 : 0,
|
||||
'anschaffungsdatum' => $_GET['anschaffungsdatum'] ?? date('Y-01-01'),
|
||||
];
|
||||
|
||||
$rows = AssetDepreciation::calculate($asset);
|
||||
echo json_encode(['rows' => $rows]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Manuell bearbeitete Abschreibungstabelle speichern
|
||||
* POST /assets/{id}/depreciation
|
||||
*/
|
||||
public function saveDepreciation(array $params): void
|
||||
{
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$id = (int)$params['id'];
|
||||
$asset = Asset::find($id);
|
||||
if (!$asset) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Asset nicht gefunden']);
|
||||
return;
|
||||
}
|
||||
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
if (!is_array($body) || !isset($body['rows'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Ungültige Daten']);
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($body['rows'] as $i => $row) {
|
||||
$rows[] = [
|
||||
'jahr' => (int)($row['jahr'] ?? 0),
|
||||
'abschreibungsbetrag' => (float)str_replace(',', '.', $row['abschreibungsbetrag'] ?? 0),
|
||||
'restwert' => (float)str_replace(',', '.', $row['restwert'] ?? 0),
|
||||
'position' => $i,
|
||||
];
|
||||
}
|
||||
|
||||
AssetDepreciation::saveForAsset($id, $rows);
|
||||
|
||||
// Flag setzen: wenn Rows manuell geändert
|
||||
$automatisch = !empty($body['automatisch']) ? 1 : 0;
|
||||
\Steuerkram\Database::execute(
|
||||
'UPDATE assets SET automatisch_berechnet = ? WHERE id = ?',
|
||||
[$automatisch, $id]
|
||||
);
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
}
|
||||
|
||||
// ── Private helpers ──────────────────────────────────────────
|
||||
|
||||
private function extractPost(): array
|
||||
|
|
@ -126,6 +217,8 @@ class AssetController
|
|||
'abgaenge' => $_POST['abgaenge'] ?? '0',
|
||||
'abschreibung_jahr' => $_POST['abschreibung_jahr'] ?? '0',
|
||||
'nutzungsdauer' => trim($_POST['nutzungsdauer'] ?? ''),
|
||||
'automatisch_berechnet'=> isset($_POST['automatisch_berechnet']) ? (int)(bool)$_POST['automatisch_berechnet'] : 1,
|
||||
'cent_im_ersten_jahr' => isset($_POST['cent_im_ersten_jahr']) ? 1 : 0,
|
||||
'notizen' => trim($_POST['notizen'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,8 +85,9 @@ class Asset
|
|||
Database::execute(
|
||||
'INSERT INTO assets
|
||||
(lfd_nr, anlage_nr, bezeichnung, account_id, anschaffungsdatum, anschaffungskosten,
|
||||
zugaenge, abgaenge, abschreibung_jahr, nutzungsdauer, notizen, fiscal_year_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
zugaenge, abgaenge, abschreibung_jahr, nutzungsdauer, automatisch_berechnet,
|
||||
cent_im_ersten_jahr, notizen, fiscal_year_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
$data['lfd_nr'] ?? self::nextLfdNr(),
|
||||
$anlageNr,
|
||||
|
|
@ -98,6 +99,8 @@ class Asset
|
|||
$data['abgaenge'] ?? 0,
|
||||
$data['abschreibung_jahr'] ?? 0,
|
||||
isset($data['nutzungsdauer']) && $data['nutzungsdauer'] !== '' ? (int)$data['nutzungsdauer'] : null,
|
||||
isset($data['automatisch_berechnet']) ? (int)(bool)$data['automatisch_berechnet'] : 1,
|
||||
isset($data['cent_im_ersten_jahr']) ? (int)(bool)$data['cent_im_ersten_jahr'] : 0,
|
||||
$data['notizen'] ?? null,
|
||||
$fyId,
|
||||
]
|
||||
|
|
@ -124,6 +127,8 @@ class Asset
|
|||
abgaenge = ?,
|
||||
abschreibung_jahr = ?,
|
||||
nutzungsdauer = ?,
|
||||
automatisch_berechnet = ?,
|
||||
cent_im_ersten_jahr = ?,
|
||||
notizen = ?,
|
||||
fiscal_year_id = COALESCE(?, fiscal_year_id),
|
||||
updated_at = NOW()
|
||||
|
|
@ -137,6 +142,8 @@ class Asset
|
|||
$data['abgaenge'] ?? 0,
|
||||
$data['abschreibung_jahr'] ?? 0,
|
||||
isset($data['nutzungsdauer']) && $data['nutzungsdauer'] !== '' ? (int)$data['nutzungsdauer'] : null,
|
||||
isset($data['automatisch_berechnet']) ? (int)(bool)$data['automatisch_berechnet'] : 1,
|
||||
isset($data['cent_im_ersten_jahr']) ? (int)(bool)$data['cent_im_ersten_jahr'] : 0,
|
||||
$data['notizen'] ?? null,
|
||||
$fyId,
|
||||
$id,
|
||||
|
|
|
|||
116
src/Models/AssetDepreciation.php
Normal file
116
src/Models/AssetDepreciation.php
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Steuerkram\Models;
|
||||
|
||||
use Steuerkram\Database;
|
||||
|
||||
class AssetDepreciation
|
||||
{
|
||||
public static function findByAsset(int $assetId): array
|
||||
{
|
||||
return Database::fetchAll(
|
||||
'SELECT id, asset_id, jahr, abschreibungsbetrag, restwert, position
|
||||
FROM asset_depreciation_schedule
|
||||
WHERE asset_id = ?
|
||||
ORDER BY position ASC',
|
||||
[$assetId]
|
||||
);
|
||||
}
|
||||
|
||||
public static function calculate(array $asset): array
|
||||
{
|
||||
$anschaffungskosten = (float)($asset['anschaffungskosten'] ?? 0);
|
||||
$zugaenge = (float)($asset['zugaenge'] ?? 0);
|
||||
$abgaenge = (float)($asset['abgaenge'] ?? 0);
|
||||
$nutzungsdauer = (int)($asset['nutzungsdauer'] ?? 0);
|
||||
$centImErstenJahr = (bool)($asset['cent_im_ersten_jahr'] ?? false);
|
||||
$startjahr = !empty($asset['anschaffungsdatum'])
|
||||
? (int)date('Y', strtotime($asset['anschaffungsdatum']))
|
||||
: (int)date('Y');
|
||||
|
||||
$basis = $anschaffungskosten + $zugaenge - $abgaenge;
|
||||
$rows = [];
|
||||
|
||||
if ($basis <= 0) {
|
||||
return $rows;
|
||||
}
|
||||
|
||||
// Sofortabschreibung
|
||||
if ($nutzungsdauer === 0) {
|
||||
$rows[] = [
|
||||
'jahr' => $startjahr,
|
||||
'abschreibungsbetrag' => round($basis, 2),
|
||||
'restwert' => 0.00,
|
||||
'position' => 0,
|
||||
];
|
||||
return $rows;
|
||||
}
|
||||
|
||||
if ($centImErstenJahr) {
|
||||
// Folgejahre: auf volle Euro gerundet (abgerundet)
|
||||
$regelJahr = floor($basis / $nutzungsdauer);
|
||||
$erstesJahr = round($basis - $regelJahr * ($nutzungsdauer - 1), 2);
|
||||
} else {
|
||||
$regelJahr = round($basis / $nutzungsdauer, 2);
|
||||
$erstesJahr = round($basis - $regelJahr * ($nutzungsdauer - 1), 2);
|
||||
}
|
||||
|
||||
$restwert = $basis;
|
||||
for ($i = 0; $i < $nutzungsdauer; $i++) {
|
||||
$afa = ($i === 0) ? $erstesJahr : (float)$regelJahr;
|
||||
$restwert = round($restwert - $afa, 2);
|
||||
// Letztes Jahr: Restwert auf 0 normalisieren (Rundungsfehler)
|
||||
if ($i === $nutzungsdauer - 1) {
|
||||
$afa = round($afa + $restwert, 2);
|
||||
$restwert = 0.00;
|
||||
}
|
||||
$rows[] = [
|
||||
'jahr' => $startjahr + $i,
|
||||
'abschreibungsbetrag' => $afa,
|
||||
'restwert' => $restwert,
|
||||
'position' => $i,
|
||||
];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public static function saveForAsset(int $assetId, array $rows): void
|
||||
{
|
||||
self::deleteByAsset($assetId);
|
||||
foreach ($rows as $row) {
|
||||
Database::execute(
|
||||
'INSERT INTO asset_depreciation_schedule
|
||||
(asset_id, jahr, abschreibungsbetrag, restwert, position)
|
||||
VALUES (?, ?, ?, ?, ?)',
|
||||
[
|
||||
$assetId,
|
||||
(int)$row['jahr'],
|
||||
round((float)$row['abschreibungsbetrag'], 2),
|
||||
round((float)$row['restwert'], 2),
|
||||
(int)$row['position'],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static function deleteByAsset(int $assetId): void
|
||||
{
|
||||
Database::execute(
|
||||
'DELETE FROM asset_depreciation_schedule WHERE asset_id = ?',
|
||||
[$assetId]
|
||||
);
|
||||
}
|
||||
|
||||
public static function generateAndSave(array $asset): void
|
||||
{
|
||||
$id = (int)($asset['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
return;
|
||||
}
|
||||
$rows = self::calculate($asset);
|
||||
self::saveForAsset($id, $rows);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,17 @@
|
|||
<?php use Steuerkram\View; $h = fn($v) => View::h($v); ?>
|
||||
<?php
|
||||
use Steuerkram\View;
|
||||
$h = fn($v) => View::h($v);
|
||||
|
||||
<div class="card" style="max-width:600px">
|
||||
$assetId = (int)($asset['id'] ?? 0);
|
||||
$isEdit = $assetId > 0;
|
||||
$nutzung = isset($asset['nutzungsdauer']) && $asset['nutzungsdauer'] !== '' ? (int)$asset['nutzungsdauer'] : null;
|
||||
$predefined = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 15, 20, 25, 50];
|
||||
$isCustom = $nutzung !== null && !in_array($nutzung, $predefined, true);
|
||||
$centFlag = !empty($asset['cent_im_ersten_jahr']);
|
||||
$autoFlag = isset($asset['automatisch_berechnet']) ? (bool)(int)$asset['automatisch_berechnet'] : true;
|
||||
?>
|
||||
|
||||
<div class="card" style="max-width:620px">
|
||||
<div class="card-body">
|
||||
<form method="post" action="<?= $h($action) ?>">
|
||||
|
||||
|
|
@ -65,18 +76,55 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
|
||||
<div class="form-group">
|
||||
<label for="abschreibung_jahr">Abschreibung lfd. Jahr (€)</label>
|
||||
<input type="number" id="abschreibung_jahr" name="abschreibung_jahr"
|
||||
value="<?= $h($asset['abschreibung_jahr'] ?? '0') ?>"
|
||||
min="0" step="0.01">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="nutzungsdauer">Nutzungsdauer (Jahre)</label>
|
||||
<input type="number" id="nutzungsdauer" name="nutzungsdauer"
|
||||
value="<?= $h($asset['nutzungsdauer'] ?? '') ?>"
|
||||
min="1" step="1" placeholder="optional">
|
||||
|
||||
<!-- Nutzungsdauer + Abschreibungstabelle -->
|
||||
<div style="display:grid;grid-template-columns:1fr auto;gap:.75rem;align-items:flex-end">
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label for="nutzungsdauer_select">Nutzungsdauer (AfA)</label>
|
||||
<div style="display:flex;gap:.5rem">
|
||||
<select id="nutzungsdauer_select" style="flex:1">
|
||||
<option value="" <?= $nutzung === null ? 'selected' : '' ?>>– keine –</option>
|
||||
<option value="0" <?= $nutzung === 0 ? 'selected' : '' ?>>sofort</option>
|
||||
<?php foreach ([1,2,3,4,5,6,7,8,9,10,12,13,15,20,25,50] as $j): ?>
|
||||
<option value="<?= $j ?>" <?= $nutzung === $j ? 'selected' : '' ?>>
|
||||
<?= $j ?> <?= $j === 1 ? 'Jahr' : 'Jahre' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<option value="custom" <?= $isCustom ? 'selected' : '' ?>>Individuell …</option>
|
||||
</select>
|
||||
<input type="number" id="nutzungsdauer_custom" name="nutzungsdauer"
|
||||
value="<?= $h($nutzung !== null ? $nutzung : '') ?>"
|
||||
min="1" step="1" placeholder="Jahre"
|
||||
style="width:90px;<?= !$isCustom ? 'display:none' : '' ?>">
|
||||
<input type="hidden" id="nutzungsdauer_hidden" name="nutzungsdauer"
|
||||
value="<?= $h($nutzung !== null ? $nutzung : '') ?>"
|
||||
<?= $isCustom ? 'disabled' : '' ?>>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<?php if ($isEdit): ?>
|
||||
<button type="button" class="btn btn-secondary" data-modal-open="modal-afa"
|
||||
style="white-space:nowrap" id="btn-show-afa">
|
||||
AfA-Tabelle
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top:.75rem">
|
||||
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;font-weight:normal">
|
||||
<input type="checkbox" name="cent_im_ersten_jahr" id="cent_im_ersten_jahr" value="1"
|
||||
<?= $centFlag ? 'checked' : '' ?>>
|
||||
Centbeträge im ersten Jahr abschreiben
|
||||
</label>
|
||||
<div class="text-muted text-sm" style="margin-top:.25rem;margin-left:1.5rem">
|
||||
Wenn aktiv: Restwert nach Jahr 1 auf volle Euro gerundet; Folgejahre gleichmäßig ohne Centabweichungen.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -85,6 +133,10 @@
|
|||
<textarea id="notizen" name="notizen" rows="3"><?= $h($asset['notizen'] ?? '') ?></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Automatisch-Flag (versteckt, wird per JS aktualisiert) -->
|
||||
<input type="hidden" name="automatisch_berechnet" id="automatisch_berechnet_input"
|
||||
value="<?= $autoFlag ? '1' : '0' ?>">
|
||||
|
||||
<div style="display:flex;gap:.75rem;margin-top:1rem">
|
||||
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||
<a href="/assets" class="btn btn-secondary">Abbrechen</a>
|
||||
|
|
@ -92,3 +144,286 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($isEdit): ?>
|
||||
<!-- ── AfA-Tabellen-Modal ─────────────────────────────────── -->
|
||||
<div class="modal-overlay" id="modal-afa">
|
||||
<div class="modal" style="width:min(680px,95vw);max-height:85vh;overflow-y:auto">
|
||||
<div class="modal-title">Abschreibungstabelle</div>
|
||||
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem;flex-wrap:wrap;gap:.5rem">
|
||||
<div style="display:flex;align-items:center;gap:.75rem">
|
||||
<span class="text-sm text-muted" id="afa-mode-label">Modus:</span>
|
||||
<label style="display:flex;align-items:center;gap:.4rem;cursor:pointer;font-size:.875rem;font-weight:normal">
|
||||
<input type="checkbox" id="afa-manual-toggle">
|
||||
Manuell bearbeiten
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="afa-recalc-btn"
|
||||
title="Tabelle neu berechnen (überschreibt manuelle Änderungen)">
|
||||
Neu berechnen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="afa-warning" class="flash flash-warning text-sm" style="display:none;margin-bottom:.75rem">
|
||||
Achtung: Die Abschreibungstabelle wurde manuell bearbeitet. Neu berechnen überschreibt alle manuellen Änderungen.
|
||||
</div>
|
||||
|
||||
<div id="afa-table-wrap">
|
||||
<table style="width:100%;border-collapse:collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left;padding:.5rem .75rem;border-bottom:2px solid var(--color-border)">Jahr</th>
|
||||
<th style="text-align:right;padding:.5rem .75rem;border-bottom:2px solid var(--color-border)">Abschreibung (€)</th>
|
||||
<th style="text-align:right;padding:.5rem .75rem;border-bottom:2px solid var(--color-border)">Restwert (€)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="afa-table-body">
|
||||
<?php if (empty($scheduleRows)): ?>
|
||||
<tr id="afa-empty-row">
|
||||
<td colspan="3" class="text-center text-muted" style="padding:1.5rem">
|
||||
Noch keine Abschreibungstabelle vorhanden. Klicke „Neu berechnen".
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($scheduleRows as $row): ?>
|
||||
<tr>
|
||||
<td style="padding:.4rem .75rem;border-bottom:1px solid var(--color-border)">
|
||||
<?= $h($row['jahr']) ?>
|
||||
</td>
|
||||
<td class="td-num afa-betrag" style="padding:.4rem .75rem;border-bottom:1px solid var(--color-border)">
|
||||
<?= number_format((float)$row['abschreibungsbetrag'], 2, ',', '.') ?>
|
||||
</td>
|
||||
<td class="td-num afa-restwert" style="padding:.4rem .75rem;border-bottom:1px solid var(--color-border)">
|
||||
<?= number_format((float)$row['restwert'], 2, ',', '.') ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-secondary" data-modal-close>Schließen</button>
|
||||
<button type="button" class="btn btn-primary" id="afa-save-btn">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// ── Nutzungsdauer Dropdown ──────────────────────────────
|
||||
const sel = document.getElementById('nutzungsdauer_select');
|
||||
const custom = document.getElementById('nutzungsdauer_custom');
|
||||
const hidden = document.getElementById('nutzungsdauer_hidden');
|
||||
|
||||
if (sel) {
|
||||
sel.addEventListener('change', () => {
|
||||
const val = sel.value;
|
||||
if (val === 'custom') {
|
||||
custom.style.display = '';
|
||||
custom.removeAttribute('disabled');
|
||||
hidden.setAttribute('disabled', '');
|
||||
custom.focus();
|
||||
} else {
|
||||
custom.style.display = 'none';
|
||||
custom.setAttribute('disabled', '');
|
||||
hidden.removeAttribute('disabled');
|
||||
hidden.value = val;
|
||||
}
|
||||
// Automatisch-Flag zurücksetzen bei Parameteränderung
|
||||
scheduleMarkAuto();
|
||||
});
|
||||
|
||||
custom.addEventListener('input', () => {
|
||||
scheduleMarkAuto();
|
||||
});
|
||||
}
|
||||
|
||||
function getNutzungsdauer() {
|
||||
if (!sel) return null;
|
||||
const val = sel.value;
|
||||
if (val === 'custom') return parseInt(custom.value, 10) || null;
|
||||
if (val === '') return null;
|
||||
return parseInt(val, 10);
|
||||
}
|
||||
|
||||
function scheduleMarkAuto() {
|
||||
// Wenn Tabelle war manuell und Parameter ändert sich: Rückfrage
|
||||
const autoInput = document.getElementById('automatisch_berechnet_input');
|
||||
if (autoInput && autoInput.value === '0') {
|
||||
const confirm = window.confirm(
|
||||
'Manuell geänderte Werte vorhanden. Sollen diese überschrieben werden?\n\n' +
|
||||
'OK = Neu berechnen, Abbrechen = Manuelle Werte behalten'
|
||||
);
|
||||
if (confirm) {
|
||||
autoInput.value = '1';
|
||||
updateAfaWarning(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── AfA-Tabellen-Modal ──────────────────────────────────
|
||||
const manualToggle = document.getElementById('afa-manual-toggle');
|
||||
const recalcBtn = document.getElementById('afa-recalc-btn');
|
||||
const saveBtn = document.getElementById('afa-save-btn');
|
||||
const tbody = document.getElementById('afa-table-body');
|
||||
const autoInput = document.getElementById('automatisch_berechnet_input');
|
||||
const assetId = <?= $assetId ?>;
|
||||
|
||||
// Spiegele aktuellen Auto-Status in Modal-Toggle
|
||||
function syncToggle() {
|
||||
if (!manualToggle || !autoInput) return;
|
||||
manualToggle.checked = autoInput.value === '0';
|
||||
updateAfaWarning(autoInput.value === '0');
|
||||
}
|
||||
|
||||
function updateAfaWarning(isManual) {
|
||||
const warn = document.getElementById('afa-warning');
|
||||
if (warn) warn.style.display = isManual ? '' : 'none';
|
||||
if (recalcBtn) recalcBtn.style.display = isManual ? '' : 'none';
|
||||
}
|
||||
|
||||
// Modal öffnen → Toggle syncen
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('[data-modal-open="modal-afa"]')) {
|
||||
syncToggle();
|
||||
setTableEditable(manualToggle?.checked ?? false);
|
||||
}
|
||||
});
|
||||
|
||||
if (manualToggle) {
|
||||
manualToggle.addEventListener('change', () => {
|
||||
const isManual = manualToggle.checked;
|
||||
if (autoInput) autoInput.value = isManual ? '0' : '1';
|
||||
updateAfaWarning(isManual);
|
||||
setTableEditable(isManual);
|
||||
});
|
||||
}
|
||||
|
||||
function setTableEditable(editable) {
|
||||
if (!tbody) return;
|
||||
tbody.querySelectorAll('tr').forEach(tr => {
|
||||
tr.querySelectorAll('.afa-betrag, .afa-restwert').forEach(cell => {
|
||||
cell.contentEditable = editable ? 'true' : 'false';
|
||||
cell.style.background = editable ? 'var(--color-input-bg, #fff)' : '';
|
||||
cell.style.outline = editable ? '1px solid var(--color-border)' : '';
|
||||
cell.style.borderRadius = editable ? '3px' : '';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function fmtNum(v) {
|
||||
return parseFloat(String(v).replace(',', '.')).toLocaleString('de-DE', {
|
||||
minimumFractionDigits: 2, maximumFractionDigits: 2
|
||||
});
|
||||
}
|
||||
|
||||
function parseNum(s) {
|
||||
return parseFloat(String(s).replace(/\./g, '').replace(',', '.')) || 0;
|
||||
}
|
||||
|
||||
// Neu berechnen
|
||||
if (recalcBtn) {
|
||||
recalcBtn.addEventListener('click', async () => {
|
||||
if (!window.confirm('Abschreibungstabelle neu berechnen? Manuelle Änderungen gehen verloren.')) return;
|
||||
await loadPreview();
|
||||
if (autoInput) autoInput.value = '1';
|
||||
if (manualToggle) manualToggle.checked = false;
|
||||
updateAfaWarning(false);
|
||||
setTableEditable(false);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadPreview() {
|
||||
const aks = document.getElementById('anschaffungskosten')?.value ?? '0';
|
||||
const zug = document.getElementById('zugaenge')?.value ?? '0';
|
||||
const abg = document.getElementById('abgaenge')?.value ?? '0';
|
||||
const dat = document.getElementById('anschaffungsdatum')?.value ?? '';
|
||||
const nd = getNutzungsdauer();
|
||||
const cent = document.getElementById('cent_im_ersten_jahr')?.checked ? '1' : '0';
|
||||
|
||||
if (nd === null) {
|
||||
if (tbody) tbody.innerHTML = '<tr><td colspan="3" class="text-center text-muted" style="padding:1.5rem">Keine Nutzungsdauer angegeben.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
const qs = new URLSearchParams({
|
||||
anschaffungskosten: aks, zugaenge: zug, abgaenge: abg,
|
||||
nutzungsdauer: nd, cent_im_ersten_jahr: cent,
|
||||
anschaffungsdatum: dat
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await fetch('/assets/depreciation-preview?' + qs);
|
||||
const data = await res.json();
|
||||
renderTable(data.rows ?? []);
|
||||
} catch (_) {
|
||||
if (tbody) tbody.innerHTML = '<tr><td colspan="3" class="text-center text-muted" style="padding:1rem">Fehler beim Laden.</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable(rows) {
|
||||
if (!tbody) return;
|
||||
if (!rows.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="text-center text-muted" style="padding:1.5rem">Keine Daten.</td></tr>';
|
||||
return;
|
||||
}
|
||||
const isManual = manualToggle?.checked ?? false;
|
||||
tbody.innerHTML = rows.map(r => `
|
||||
<tr>
|
||||
<td style="padding:.4rem .75rem;border-bottom:1px solid var(--color-border)">${r.jahr}</td>
|
||||
<td class="td-num afa-betrag" contenteditable="${isManual}"
|
||||
style="padding:.4rem .75rem;border-bottom:1px solid var(--color-border)${isManual ? ';background:var(--color-input-bg,#fff);outline:1px solid var(--color-border);border-radius:3px' : ''}">
|
||||
${fmtNum(r.abschreibungsbetrag)}
|
||||
</td>
|
||||
<td class="td-num afa-restwert" contenteditable="${isManual}"
|
||||
style="padding:.4rem .75rem;border-bottom:1px solid var(--color-border)${isManual ? ';background:var(--color-input-bg,#fff);outline:1px solid var(--color-border);border-radius:3px' : ''}">
|
||||
${fmtNum(r.restwert)}
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
// Speichern
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
const rows = [];
|
||||
if (tbody) {
|
||||
tbody.querySelectorAll('tr').forEach((tr, i) => {
|
||||
const cells = tr.querySelectorAll('td');
|
||||
if (cells.length < 3) return;
|
||||
rows.push({
|
||||
jahr: parseInt(cells[0].textContent.trim(), 10),
|
||||
abschreibungsbetrag: cells[1].textContent.trim(),
|
||||
restwert: cells[2].textContent.trim(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const automatisch = !(manualToggle?.checked ?? false) ? 1 : 0;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/assets/${assetId}/depreciation`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ rows, automatisch }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
if (autoInput) autoInput.value = automatisch ? '1' : '0';
|
||||
document.getElementById('modal-afa')?.classList.remove('open');
|
||||
} else {
|
||||
alert('Fehler beim Speichern: ' + (data.error ?? 'Unbekannt'));
|
||||
}
|
||||
} catch (_) {
|
||||
alert('Netzwerkfehler beim Speichern.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Init: Neu-Berechnen-Button nur bei manueller Tabelle zeigen
|
||||
syncToggle();
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
121
tests/26_AbschreibungstabelleTest.php
Normal file
121
tests/26_AbschreibungstabelleTest.php
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Steuerkram\Models\AssetDepreciation;
|
||||
|
||||
class AbschreibungstabelleTest extends TestCase
|
||||
{
|
||||
protected string $testName = 'Test 26: Abschreibungstabelle (lineare AfA)';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
// ── Sofortabschreibung ───────────────────────────────────
|
||||
$rows = AssetDepreciation::calculate([
|
||||
'anschaffungskosten' => 1000.00,
|
||||
'zugaenge' => 0,
|
||||
'abgaenge' => 0,
|
||||
'nutzungsdauer' => 0,
|
||||
'cent_im_ersten_jahr'=> false,
|
||||
'anschaffungsdatum' => '2025-03-15',
|
||||
]);
|
||||
|
||||
$this->assertEquals(1, count($rows), 'Sofortabschreibung: genau 1 Zeile');
|
||||
$this->assertEquals(2025, $rows[0]['jahr'], 'Sofortabschreibung: Startjahr korrekt');
|
||||
$this->assertEquals(1000.00, $rows[0]['abschreibungsbetrag'], 'Sofortabschreibung: voller Betrag');
|
||||
$this->assertEquals(0.00, $rows[0]['restwert'], 'Sofortabschreibung: Restwert 0');
|
||||
|
||||
// ── Gleichmäßige lineare AfA ohne Cent-Option ────────────
|
||||
$rows = AssetDepreciation::calculate([
|
||||
'anschaffungskosten' => 3000.00,
|
||||
'zugaenge' => 0,
|
||||
'abgaenge' => 0,
|
||||
'nutzungsdauer' => 3,
|
||||
'cent_im_ersten_jahr'=> false,
|
||||
'anschaffungsdatum' => '2023-01-01',
|
||||
]);
|
||||
|
||||
$this->assertEquals(3, count($rows), 'Lineare AfA 3 Jahre: 3 Zeilen');
|
||||
$this->assertEquals(1000.00, $rows[0]['abschreibungsbetrag'], 'Jahr 1: 1000,00');
|
||||
$this->assertEquals(2000.00, $rows[0]['restwert'], 'Jahr 1 Restwert: 2000,00');
|
||||
$this->assertEquals(1000.00, $rows[2]['abschreibungsbetrag'], 'Jahr 3: 1000,00');
|
||||
$this->assertEquals(0.00, $rows[2]['restwert'], 'Jahr 3 Restwert: 0,00');
|
||||
|
||||
// ── Cent-Option: Centbeträge im ersten Jahr ──────────────
|
||||
$rows = AssetDepreciation::calculate([
|
||||
'anschaffungskosten' => 1000.75,
|
||||
'zugaenge' => 0,
|
||||
'abgaenge' => 0,
|
||||
'nutzungsdauer' => 3,
|
||||
'cent_im_ersten_jahr'=> true,
|
||||
'anschaffungsdatum' => '2024-01-01',
|
||||
]);
|
||||
|
||||
$this->assertEquals(3, count($rows), 'Cent-Option 3 Jahre: 3 Zeilen');
|
||||
// Folgejahre: floor(1000.75/3) = 333 €
|
||||
$this->assertEquals(333.00, $rows[1]['abschreibungsbetrag'], 'Jahr 2 mit Cent-Option: 333,00');
|
||||
$this->assertEquals(333.00, $rows[2]['abschreibungsbetrag'], 'Jahr 3 mit Cent-Option: 333,00');
|
||||
// Erstes Jahr: 1000.75 - 333*2 = 334.75
|
||||
$this->assertEquals(334.75, $rows[0]['abschreibungsbetrag'], 'Jahr 1 mit Cent-Option: 334,75');
|
||||
// Restwert nach Jahr 1 auf volle Euro
|
||||
$restwertJ1 = $rows[0]['restwert'];
|
||||
$this->assert(
|
||||
$restwertJ1 == floor($restwertJ1),
|
||||
'Restwert nach Jahr 1 ist volle Euro: ' . $restwertJ1
|
||||
);
|
||||
// Gesamtsumme muss Basis entsprechen
|
||||
$sumAfa = array_sum(array_column($rows, 'abschreibungsbetrag'));
|
||||
$this->assertEquals(1000.75, round($sumAfa, 2), 'Summe aller Abschreibungen = Basis');
|
||||
|
||||
// ── Basis mit Zugängen und Abgängen ─────────────────────
|
||||
$rows = AssetDepreciation::calculate([
|
||||
'anschaffungskosten' => 5000.00,
|
||||
'zugaenge' => 500.00,
|
||||
'abgaenge' => 200.00,
|
||||
'nutzungsdauer' => 5,
|
||||
'cent_im_ersten_jahr'=> false,
|
||||
'anschaffungsdatum' => '2022-06-01',
|
||||
]);
|
||||
|
||||
$this->assertEquals(5, count($rows), 'AfA mit Zu-/Abgängen: 5 Zeilen');
|
||||
$basis = 5000.00 + 500.00 - 200.00;
|
||||
$sumAfa = array_sum(array_column($rows, 'abschreibungsbetrag'));
|
||||
$this->assertEquals($basis, round($sumAfa, 2), 'Summe AfA = Basis mit Zu-/Abgängen');
|
||||
|
||||
// ── Leere Tabelle bei Basis <= 0 ─────────────────────────
|
||||
$rows = AssetDepreciation::calculate([
|
||||
'anschaffungskosten' => 0,
|
||||
'zugaenge' => 0,
|
||||
'abgaenge' => 0,
|
||||
'nutzungsdauer' => 5,
|
||||
'cent_im_ersten_jahr'=> false,
|
||||
'anschaffungsdatum' => '2024-01-01',
|
||||
]);
|
||||
$this->assertEquals(0, count($rows), 'Keine Zeilen bei Basis = 0');
|
||||
|
||||
// ── Persistenz: Speichern und Laden ──────────────────────
|
||||
// Erst ein Test-Asset anlegen (Rollback durch TestCase)
|
||||
\Steuerkram\Database::execute(
|
||||
"INSERT INTO assets (lfd_nr, anlage_nr, bezeichnung, anschaffungsdatum, anschaffungskosten)
|
||||
VALUES (9999, 'TEST-DEPR', 'Test Abschreibung', '2024-01-01', 2400.00)"
|
||||
);
|
||||
$assetId = (int)\Steuerkram\Database::lastInsertId();
|
||||
|
||||
$testRows = [
|
||||
['jahr' => 2024, 'abschreibungsbetrag' => 800.00, 'restwert' => 1600.00, 'position' => 0],
|
||||
['jahr' => 2025, 'abschreibungsbetrag' => 800.00, 'restwert' => 800.00, 'position' => 1],
|
||||
['jahr' => 2026, 'abschreibungsbetrag' => 800.00, 'restwert' => 0.00, 'position' => 2],
|
||||
];
|
||||
|
||||
AssetDepreciation::saveForAsset($assetId, $testRows);
|
||||
$loaded = AssetDepreciation::findByAsset($assetId);
|
||||
|
||||
$this->assertEquals(3, count($loaded), 'Persistenz: 3 Zeilen gespeichert und geladen');
|
||||
$this->assertEquals(2024, (int)$loaded[0]['jahr'], 'Persistenz: Jahr 2024 korrekt');
|
||||
$this->assertEquals(800.00, (float)$loaded[0]['abschreibungsbetrag'], 'Persistenz: Betrag korrekt');
|
||||
|
||||
AssetDepreciation::deleteByAsset($assetId);
|
||||
$loaded = AssetDepreciation::findByAsset($assetId);
|
||||
$this->assertEquals(0, count($loaded), 'Persistenz: Nach deleteByAsset keine Zeilen mehr');
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue