Fix n.n.: Depot-Zuordnung, Bestand einbuchen mit Stichtag
- Depot der Bank wird einem Depot im Finanzmanager zugeordnet (Migration 022) - Bestand einbuchen: Ein-/Auslieferungen aus dem Bankbestand, Stichtag per Dialog - Zuordnungsliste: Depots nur noch als Wertpapiergeschäft Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b4a1b0acbc
commit
9a3472f4b7
13 changed files with 378 additions and 42 deletions
23
Changes.md
23
Changes.md
|
|
@ -1,5 +1,18 @@
|
||||||
# Änderungen
|
# Änderungen
|
||||||
|
|
||||||
|
## Stichtag beim Einbuchen des Depotbestands (30.09.2026)
|
||||||
|
|
||||||
|
- „Bestand einbuchen“ (Wertpapiere → Bestand laut Bank) fragt in einem modalen Dialog nach dem
|
||||||
|
Stichtag. Er ist das Datum aller Ein- und Auslieferungen des Laufs; vorbelegt ist der Stand des
|
||||||
|
Bankabrufs. Die Notiz nennt weiterhin den Bankstand.
|
||||||
|
|
||||||
|
## Zuordnungsliste: Depots nur noch als Wertpapiergeschäft (30.09.2026)
|
||||||
|
|
||||||
|
- Wo die Gruppe „Wertpapiergeschäft im Depot“ steht (Kontoblatt, Buchungsmaske), fehlen die Depots
|
||||||
|
in der Gruppe „Umbuchung auf ein Konto“. Die fast gleich benannten Einträge führten dazu, dass
|
||||||
|
ein Kauf als schlichte Umbuchung ohne Stückzahl, Kurs und Nebenkosten gespeichert wurde. In den
|
||||||
|
Splitzeilen bleibt die Umbuchung ins Depot erhalten.
|
||||||
|
|
||||||
## Depotabruf per FinTS und Nachweis zur Überweisung (30.09.2026)
|
## Depotabruf per FinTS und Nachweis zur Überweisung (30.09.2026)
|
||||||
|
|
||||||
- Das Depot (Kontoart 30, ohne IBAN) fehlte in der Kontenabfrage, weil `HKSPA` nur Konten mit
|
- Das Depot (Kontoart 30, ohne IBAN) fehlte in der Kontenabfrage, weil `HKSPA` nur Konten mit
|
||||||
|
|
@ -17,6 +30,16 @@
|
||||||
Auftrag, allen Durchläufen im vollen Wortlaut und den Prüfsummen; ein nachträglich geändertes
|
Auftrag, allen Durchläufen im vollen Wortlaut und den Prüfsummen; ein nachträglich geändertes
|
||||||
Protokoll fällt dort als ABWEICHUNG auf.
|
Protokoll fällt dort als ABWEICHUNG auf.
|
||||||
- phpFinTS auf v4.1.5: Die DKB meldet die Depotaufstellung nur in Version 6 (`HKWPD`/`HIWPD`/`HIWPDS`); Upstream-PR nemiah/phpFinTS#598.
|
- phpFinTS auf v4.1.5: Die DKB meldet die Depotaufstellung nur in Version 6 (`HKWPD`/`HIWPD`/`HIWPDS`); Upstream-PR nemiah/phpFinTS#598.
|
||||||
|
- Zuordnung des Depots der Bank zu einem Depot im Finanzmanager (Migration 022,
|
||||||
|
`bank_konten.art`): Das Depot steht als eigene Zeile bei den Bankkonten des Zugangs und wird beim
|
||||||
|
Kontenabruf (ohne zusätzliche TAN) oder beim Depotabruf erkannt. Passt die Kontonummer zu einem
|
||||||
|
eigenen Depot, wird gleich zugeordnet; sonst unter Online-Banking → Konten zuordnen (dort auch „neues Konto anlegen“). Der Umsatzabruf sieht nur
|
||||||
|
Konten (`Bankzugang::konten()`), Depots über `Bankzugang::depots()`.
|
||||||
|
- „Bestand einbuchen“ über der Tabelle unter Wertpapiere → Bestand laut Bank
|
||||||
|
(`Depotabgleich::einbuchen()`): Mehrbestand wird als Einlieferung, Minderbestand als Auslieferung gebucht
|
||||||
|
(ohne Geldseite), unbekannte Wertpapiere werden angelegt. Kurs der Einlieferung: Einstandskurs der Bank,
|
||||||
|
sonst Bewertungskurs.
|
||||||
|
- Spalte „Einstandskurs“ (soweit die Bank ihn meldet) in der Tabelle Bestand laut Bank.
|
||||||
- Test 51.
|
- Test 51.
|
||||||
|
|
||||||
## Buchungsmaske: Splitzeilen bleiben bei falscher Summe erhalten (29.09.2026)
|
## Buchungsmaske: Splitzeilen bleiben bei falscher Summe erhalten (29.09.2026)
|
||||||
|
|
|
||||||
8
migrations/022_bankdepots.sql
Normal file
8
migrations/022_bankdepots.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- Depots der Bank neben den Konten mit IBAN.
|
||||||
|
--
|
||||||
|
-- Ein Depot hat keine IBAN und fehlt in der Kontenabfrage (HKSPA). Es steht in den
|
||||||
|
-- Kontostammdaten der Bank und wird als eigene Zeile geführt, damit es wie ein
|
||||||
|
-- Bankkonto einem eigenen Konto (der Kontoart Depot) zugeordnet werden kann. Der
|
||||||
|
-- Umsatzabruf sieht nur `art = 'konto'`.
|
||||||
|
ALTER TABLE bank_konten
|
||||||
|
ADD COLUMN IF NOT EXISTS art ENUM('konto','depot') NOT NULL DEFAULT 'konto' AFTER konto_id;
|
||||||
|
|
@ -286,6 +286,7 @@ $router->post('/wertpapiere/stammdaten/{id}/kurs',
|
||||||
$router->post('/wertpapiere/kurse', fn() => (new WertpapiereController())->kurseAbrufen());
|
$router->post('/wertpapiere/kurse', fn() => (new WertpapiereController())->kurseAbrufen());
|
||||||
|
|
||||||
$router->get('/wertpapiere/bank', fn() => (new WertpapiereController())->bankbestand());
|
$router->get('/wertpapiere/bank', fn() => (new WertpapiereController())->bankbestand());
|
||||||
|
$router->post('/wertpapiere/bank/einbuchen', fn() => (new WertpapiereController())->bankbestandEinbuchen());
|
||||||
$router->get('/wertpapiere/import', fn() => (new WertpapiereController())->importFormular());
|
$router->get('/wertpapiere/import', fn() => (new WertpapiereController())->importFormular());
|
||||||
$router->post('/wertpapiere/import', fn() => (new WertpapiereController())->importAusfuehren());
|
$router->post('/wertpapiere/import', fn() => (new WertpapiereController())->importAusfuehren());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -304,6 +304,16 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Depotbestand einbuchen: Der Knopf öffnet den Dialog mit der Stichtagsabfrage.
|
||||||
|
document.querySelectorAll('[data-einbuchdialog]').forEach(function (knopf) {
|
||||||
|
knopf.addEventListener('click', function () {
|
||||||
|
const dialog = document.getElementById(knopf.dataset.einbuchdialog);
|
||||||
|
if (dialog && typeof dialog.showModal === 'function') {
|
||||||
|
dialog.showModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Übermittlungsprotokoll im Zahlungsverkehr: Ein Klick auf den Status holt das
|
// Übermittlungsprotokoll im Zahlungsverkehr: Ein Klick auf den Status holt das
|
||||||
// Protokoll der Überweisung in einen modalen Dialog. Ohne <dialog> oder
|
// Protokoll der Überweisung in einen modalen Dialog. Ohne <dialog> oder
|
||||||
// fetch bleibt der Status ein Link auf dieselbe Ansicht als eigene Seite.
|
// fetch bleibt der Status ein Link auf dieselbe Ansicht als eigene Seite.
|
||||||
|
|
|
||||||
|
|
@ -396,7 +396,7 @@ class BankingController extends BaseController
|
||||||
$this->render('banking/konten.php', [
|
$this->render('banking/konten.php', [
|
||||||
'titel' => 'Konten zuordnen – ' . $zugang['name'],
|
'titel' => 'Konten zuordnen – ' . $zugang['name'],
|
||||||
'zugang' => $zugang,
|
'zugang' => $zugang,
|
||||||
'konten' => Bankzugang::konten((int)$zugang['id']),
|
'konten' => Bankzugang::konten((int)$zugang['id'], true),
|
||||||
'eigene' => Konto::alle(),
|
'eigene' => Konto::alle(),
|
||||||
'einrichtung' => $this->query('einrichtung') === '1',
|
'einrichtung' => $this->query('einrichtung') === '1',
|
||||||
]);
|
]);
|
||||||
|
|
@ -1197,7 +1197,7 @@ class BankingController extends BaseController
|
||||||
}
|
}
|
||||||
|
|
||||||
$bekannt = [];
|
$bekannt = [];
|
||||||
foreach (Bankzugang::konten((int)$zugang['id']) as $eintrag) {
|
foreach (Bankzugang::konten((int)$zugang['id'], true) as $eintrag) {
|
||||||
$bekannt[(int)$eintrag['id']] = $eintrag;
|
$bekannt[(int)$eintrag['id']] = $eintrag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1219,7 +1219,9 @@ class BankingController extends BaseController
|
||||||
$kontoId = $wahl === '' ? null : (int)$wahl;
|
$kontoId = $wahl === '' ? null : (int)$wahl;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bankzugang::ordneKontoZu($bankKontoId, $kontoId, !empty($werte['abrufen']));
|
// Ein Depot hat keinen Umsatzabruf, den man abwählen könnte.
|
||||||
|
$istDepot = (string)$bekannt[$bankKontoId]['art'] === 'depot';
|
||||||
|
Bankzugang::ordneKontoZu($bankKontoId, $kontoId, $istDepot || !empty($werte['abrufen']));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $neue;
|
return $neue;
|
||||||
|
|
@ -1247,7 +1249,7 @@ class BankingController extends BaseController
|
||||||
|
|
||||||
return Konto::anlegen([
|
return Konto::anlegen([
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'kontoart' => (string)($werte['neue_kontoart'] ?? 'bank'),
|
'kontoart' => (string)$bankkonto['art'] === 'depot' ? 'depot' : (string)($werte['neue_kontoart'] ?? 'bank'),
|
||||||
'bankname' => (string)($zugang['bankname'] !== '' ? $zugang['bankname'] : $zugang['name']),
|
'bankname' => (string)($zugang['bankname'] !== '' ? $zugang['bankname'] : $zugang['name']),
|
||||||
'iban' => (string)$bankkonto['iban'],
|
'iban' => (string)$bankkonto['iban'],
|
||||||
'kontonummer' => (string)$bankkonto['kontonummer'],
|
'kontonummer' => (string)$bankkonto['kontonummer'],
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,41 @@ class WertpapiereController extends BaseController
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Bucht die Unterschiede zwischen Bank- und eigenem Bestand ein. */
|
||||||
|
public function bankbestandEinbuchen(): void
|
||||||
|
{
|
||||||
|
$this->verlangeAnmeldung();
|
||||||
|
|
||||||
|
$depotId = (int)$this->post('konto_id');
|
||||||
|
if (Depot::depots(true) === [] || Konto::find($depotId) === null) {
|
||||||
|
View::redirect('/wertpapiere/bank', 'Dieses Depot gibt es nicht.', 'fehler');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stichtag = trim((string)$this->post('stichtag'));
|
||||||
|
if ($stichtag !== '') {
|
||||||
|
$datum = \DateTimeImmutable::createFromFormat('!Y-m-d', $stichtag);
|
||||||
|
if ($datum === false || $datum->format('Y-m-d') !== $stichtag) {
|
||||||
|
View::redirect('/wertpapiere/bank', 'Der Stichtag ist kein gültiges Datum.', 'fehler');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$e = Depotabgleich::einbuchen($depotId, $stichtag !== '' ? $stichtag : null);
|
||||||
|
} catch (\Throwable $ex) {
|
||||||
|
View::redirect('/wertpapiere/bank', 'Einbuchen fehlgeschlagen: ' . $ex->getMessage(), 'fehler');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
View::redirect('/wertpapiere/bank', sprintf(
|
||||||
|
'%d Einlieferung(en), %d Auslieferung(en) gebucht, %d Wertpapier(e) neu angelegt.',
|
||||||
|
$e['eingeliefert'],
|
||||||
|
$e['ausgeliefert'],
|
||||||
|
$e['angelegt']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// ── Bestandsimport aus einer CSV-Datei ───────────────────────────────────
|
// ── Bestandsimport aus einer CSV-Datei ───────────────────────────────────
|
||||||
|
|
||||||
public function importFormular(): void
|
public function importFormular(): void
|
||||||
|
|
|
||||||
|
|
@ -435,18 +435,72 @@ class Bankzugang
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return list<array<string,mixed>> */
|
/** @return list<array<string,mixed>> */
|
||||||
public static function konten(int $zugangId): array
|
public static function konten(int $zugangId, bool $mitDepots = false): array
|
||||||
{
|
{
|
||||||
return Database::fetchAll(
|
return Database::fetchAll(
|
||||||
'SELECT bk.*, k.name AS kontoname
|
'SELECT bk.*, k.name AS kontoname
|
||||||
FROM bank_konten bk
|
FROM bank_konten bk
|
||||||
LEFT JOIN konten k ON k.id = bk.konto_id
|
LEFT JOIN konten k ON k.id = bk.konto_id
|
||||||
WHERE bk.zugang_id = ?
|
WHERE bk.zugang_id = ?' . ($mitDepots ? '' : " AND bk.art = 'konto'") . '
|
||||||
ORDER BY bk.iban, bk.kontonummer',
|
ORDER BY bk.art, bk.iban, bk.kontonummer',
|
||||||
[$zugangId]
|
[$zugangId]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die Depots, die die Bank für diesen Zugang gemeldet hat.
|
||||||
|
*
|
||||||
|
* @return list<array<string,mixed>>
|
||||||
|
*/
|
||||||
|
public static function depots(int $zugangId): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter(
|
||||||
|
self::konten($zugangId, true),
|
||||||
|
static fn(array $zeile): bool => (string)$zeile['art'] === 'depot'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Das Depot der Bank mit dieser Depotnummer – angelegt, falls es noch fehlt.
|
||||||
|
*
|
||||||
|
* Gibt es im Finanzmanager schon ein Depot (Kontoart Depot) mit derselben
|
||||||
|
* Kontonummer, ist die Zuordnung offensichtlich und wird gleich getroffen.
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public static function findeOderLegeDepotAn(int $zugangId, string $depotnummer, string $bezeichnung = ''): array
|
||||||
|
{
|
||||||
|
$depotnummer = trim($depotnummer);
|
||||||
|
|
||||||
|
$zeile = Database::fetchOne(
|
||||||
|
"SELECT * FROM bank_konten WHERE zugang_id = ? AND art = 'depot' AND kontonummer = ? LIMIT 1",
|
||||||
|
[$zugangId, $depotnummer]
|
||||||
|
);
|
||||||
|
if ($zeile !== false) {
|
||||||
|
return $zeile;
|
||||||
|
}
|
||||||
|
|
||||||
|
$gesucht = ltrim($depotnummer, '0');
|
||||||
|
$konto = null;
|
||||||
|
foreach (Database::fetchAll(
|
||||||
|
"SELECT id, kontonummer FROM konten WHERE benutzer_id = ? AND kontoart = 'depot'",
|
||||||
|
[Mandant::id()]
|
||||||
|
) as $eigenes) {
|
||||||
|
if ($gesucht !== '' && ltrim(trim((string)$eigenes['kontonummer']), '0') === $gesucht) {
|
||||||
|
$konto = (int)$eigenes['id'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Database::execute(
|
||||||
|
"INSERT INTO bank_konten (zugang_id, konto_id, art, iban, kontonummer, bezeichnung)
|
||||||
|
VALUES (?, ?, 'depot', '', ?, ?)",
|
||||||
|
[$zugangId, $konto, mb_substr($depotnummer, 0, 34), mb_substr($bezeichnung, 0, 128)]
|
||||||
|
);
|
||||||
|
|
||||||
|
return Database::fetchOne('SELECT * FROM bank_konten WHERE id = ?', [Database::lastInsertId()]) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Das eine Konto eines PayPal-Zugangs – angelegt, falls es noch keines gibt.
|
* Das eine Konto eines PayPal-Zugangs – angelegt, falls es noch keines gibt.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,18 @@ namespace Finanzmanager\Services;
|
||||||
|
|
||||||
use Finanzmanager\Database;
|
use Finanzmanager\Database;
|
||||||
use Finanzmanager\Mandant;
|
use Finanzmanager\Mandant;
|
||||||
|
use Finanzmanager\Models\Depotbuchung;
|
||||||
use Finanzmanager\Models\Wertpapier;
|
use Finanzmanager\Models\Wertpapier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Der Depotbestand laut Bank (FinTS-Depotaufstellung) im Vergleich mit dem
|
* Der Depotbestand laut Bank (FinTS-Depotaufstellung) im Vergleich mit dem
|
||||||
* eigenen Bestand.
|
* eigenen Bestand.
|
||||||
*
|
*
|
||||||
* Übernommen wird nur, was sicher ist: der Bestand landet in
|
* Der Abruf übernimmt nur, was sicher ist: der Bestand landet in
|
||||||
* `depot_bankbestaende`, und die Bewertungskurse der Papiere, die es im
|
* `depot_bankbestaende`, und die Bewertungskurse der Papiere, die es im
|
||||||
* Finanzmanager schon gibt, werden als Kurs des Abrufdatums eingetragen. Es
|
* Finanzmanager schon gibt, werden als Kurs des Abrufdatums eingetragen. Ein
|
||||||
* entstehen weder Depotbuchungen noch neue Wertpapiere – ein Unterschied im
|
* Unterschied im Bestand ist zunächst ein Hinweis; erst `einbuchen()` macht
|
||||||
* Bestand ist ein Hinweis, keine Buchung.
|
* daraus Depotbuchungen (auf ausdrücklichen Knopfdruck).
|
||||||
*/
|
*/
|
||||||
class Depotabgleich
|
class Depotabgleich
|
||||||
{
|
{
|
||||||
|
|
@ -132,6 +133,75 @@ class Depotabgleich
|
||||||
return $zeilen;
|
return $zeilen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bucht die Unterschiede zwischen Bankbestand und eigenem Bestand ein:
|
||||||
|
* Mehrbestand als Einlieferung, Minderbestand als Auslieferung, jeweils
|
||||||
|
* ohne Geldseite (kein Verrechnungskonto). Unbekannte Wertpapiere werden
|
||||||
|
* angelegt. Der Einlieferungskurs ist der Einstandskurs der Bank, ohne
|
||||||
|
* den der Bewertungskurs. Danach stimmen beide Bestände überein.
|
||||||
|
*
|
||||||
|
* Der Stichtag (`Y-m-d`) ist das Datum aller Buchungen; ohne Angabe gilt
|
||||||
|
* der Stand des Bankabrufs, sonst heute.
|
||||||
|
*
|
||||||
|
* @return array{eingeliefert: int, ausgeliefert: int, angelegt: int}
|
||||||
|
*/
|
||||||
|
public static function einbuchen(int $kontoId, ?string $stichtag = null): array
|
||||||
|
{
|
||||||
|
return Database::transaktion(static function () use ($kontoId, $stichtag): array {
|
||||||
|
$ergebnis = ['eingeliefert' => 0, 'ausgeliefert' => 0, 'angelegt' => 0];
|
||||||
|
|
||||||
|
foreach (self::vergleich($kontoId) as $zeile) {
|
||||||
|
$diff = (float)$zeile['abweichung'];
|
||||||
|
if (abs($diff) < 0.000001) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bank = $zeile['bank'];
|
||||||
|
$stand = (string)($bank['stand'] ?? '') !== '' ? (string)$bank['stand'] : date('Y-m-d');
|
||||||
|
$datum = $stichtag ?? $stand;
|
||||||
|
|
||||||
|
if ($diff > 0) {
|
||||||
|
$papier = Wertpapier::finde((string)$zeile['isin'], (string)($bank['wkn'] ?? ''), (string)$zeile['name']);
|
||||||
|
if ($papier === null) {
|
||||||
|
$ergebnis['angelegt']++;
|
||||||
|
}
|
||||||
|
$wertpapierId = $papier !== null ? (int)$papier['id'] : Wertpapier::findenOderAnlegen([
|
||||||
|
'name' => $zeile['name'],
|
||||||
|
'isin' => $zeile['isin'],
|
||||||
|
'wkn' => $bank['wkn'] ?? '',
|
||||||
|
'art' => 'aktie',
|
||||||
|
'waehrung' => $bank['waehrung'] ?? 'EUR',
|
||||||
|
]);
|
||||||
|
$kurs = (float)($bank['einstiegskurs'] ?? 0) > 0
|
||||||
|
? (float)$bank['einstiegskurs'] : (float)($bank['kurs'] ?? 0);
|
||||||
|
$vorgang = 'einlage';
|
||||||
|
} else {
|
||||||
|
$wertpapierId = (int)Wertpapier::finde((string)$zeile['isin'], '', (string)$zeile['name'])['id'];
|
||||||
|
$kurs = 0.0;
|
||||||
|
$vorgang = 'entnahme';
|
||||||
|
}
|
||||||
|
|
||||||
|
Depotbuchung::anlegen([
|
||||||
|
'konto_id' => $kontoId,
|
||||||
|
'wertpapier_id' => $wertpapierId,
|
||||||
|
'datum' => $datum,
|
||||||
|
'vorgang' => $vorgang,
|
||||||
|
'stueck' => abs($diff),
|
||||||
|
'kurs' => $kurs > 0 ? $kurs : null,
|
||||||
|
'betrag' => round(abs($diff) * $kurs, 2),
|
||||||
|
'waehrung' => $bank['waehrung'] ?? 'EUR',
|
||||||
|
'notiz' => 'Bestand laut Bank vom ' . date('d.m.Y', strtotime($stand))
|
||||||
|
. ($datum !== $stand ? ', gebucht zum ' . date('d.m.Y', strtotime($datum)) : ''),
|
||||||
|
'wertpapiername' => $zeile['name'],
|
||||||
|
'quelle' => 'fints',
|
||||||
|
]);
|
||||||
|
$ergebnis[$vorgang === 'einlage' ? 'eingeliefert' : 'ausgeliefert']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ergebnis;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** Wann der Bankbestand des Depots zuletzt abgerufen wurde. */
|
/** Wann der Bankbestand des Depots zuletzt abgerufen wurde. */
|
||||||
public static function abgerufenAm(int $kontoId): ?string
|
public static function abgerufenAm(int $kontoId): ?string
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ use Finanzmanager\Models\Bankprotokoll;
|
||||||
use Finanzmanager\Models\Bankumsatz;
|
use Finanzmanager\Models\Bankumsatz;
|
||||||
use Finanzmanager\Models\Bankzugang;
|
use Finanzmanager\Models\Bankzugang;
|
||||||
use Finanzmanager\Models\Einstellung;
|
use Finanzmanager\Models\Einstellung;
|
||||||
|
use Finanzmanager\Models\Konto;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Der Umsatzabruf über FinTS.
|
* Der Umsatzabruf über FinTS.
|
||||||
|
|
@ -528,9 +529,8 @@ class FinTsAbruf
|
||||||
$this->zustand['depot_nummer'] = $nummer;
|
$this->zustand['depot_nummer'] = $nummer;
|
||||||
$depot = $depots[$nummer];
|
$depot = $depots[$nummer];
|
||||||
|
|
||||||
if (self::eigenesDepot((string)$depot['kontonummer']) === null) {
|
if ($this->eigenesDepot($depot) === null) {
|
||||||
$this->notiere('Übersprungen: Depot ' . $depot['kontonummer']
|
$this->notiere('Übersprungen: Depot ' . $depot['kontonummer'] . ' ist keinem Depot zugeordnet.');
|
||||||
. ' – im Finanzmanager gibt es kein Depot mit dieser Kontonummer.');
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -553,8 +553,8 @@ class FinTsAbruf
|
||||||
if ((int)$this->zustand['depot_geholt'] === 0) {
|
if ((int)$this->zustand['depot_geholt'] === 0) {
|
||||||
return $this->beenden(
|
return $this->beenden(
|
||||||
'fehler',
|
'fehler',
|
||||||
'Kein Depot der Bank passt zu einem Depot im Finanzmanager. Das Konto der Kontoart '
|
'Das Depot ' . implode(' / ', array_column($depots, 'kontonummer'))
|
||||||
. 'Depot braucht die Kontonummer ' . implode(' / ', array_column($depots, 'kontonummer')) . '.'
|
. ' ist noch keinem Depot im Finanzmanager zugeordnet. Bitte unter Online-Banking → Konten zuordnen zuordnen und dann erneut abrufen.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -611,27 +611,24 @@ class FinTsAbruf
|
||||||
return $depots;
|
return $depots;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Das Depot im Finanzmanager, dessen Kontonummer zu der der Bank passt. */
|
/**
|
||||||
private static function eigenesDepot(string $kontonummer): ?array
|
* Das Depot im Finanzmanager, dem das Depot der Bank zugeordnet ist.
|
||||||
|
*
|
||||||
|
* @param array<string,string> $depot
|
||||||
|
* @return array<string,mixed>|null
|
||||||
|
*/
|
||||||
|
private function eigenesDepot(array $depot): ?array
|
||||||
{
|
{
|
||||||
$gesucht = ltrim(trim($kontonummer), '0');
|
$bankkonto = Bankzugang::findeOderLegeDepotAn((int)$this->zugang['id'], $depot['kontonummer'], $depot['name']);
|
||||||
if ($gesucht === '') {
|
$kontoId = (int)($bankkonto['konto_id'] ?? 0);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (Depot::depots() as $depot) {
|
return $kontoId > 0 ? Konto::find($kontoId) : null;
|
||||||
if (ltrim(trim((string)$depot['kontonummer']), '0') === $gesucht) {
|
|
||||||
return $depot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string,string> $depot */
|
/** @param array<string,string> $depot */
|
||||||
private function schreibeDepot(array $depot, GetDepotAufstellung $aktion): void
|
private function schreibeDepot(array $depot, GetDepotAufstellung $aktion): void
|
||||||
{
|
{
|
||||||
$konto = self::eigenesDepot((string)$depot['kontonummer']);
|
$konto = $this->eigenesDepot($depot);
|
||||||
if ($konto === null) {
|
if ($konto === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -825,6 +822,26 @@ class FinTsAbruf
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->notiere(count($konten) . ' Konto/Konten bei der Bank gefunden.');
|
$this->notiere(count($konten) . ' Konto/Konten bei der Bank gefunden.');
|
||||||
|
|
||||||
|
// Depots haben keine IBAN und stehen nicht in dieser Liste, sondern in
|
||||||
|
// den Kontostammdaten. Sie werden hier gleich mit vermerkt, damit sie
|
||||||
|
// sich zuordnen lassen – ohne zusätzliche TAN.
|
||||||
|
try {
|
||||||
|
$this->merkeDepots();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->notiere('Depots nicht auslesbar: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function merkeDepots(): void
|
||||||
|
{
|
||||||
|
foreach ($this->depotkonten() as $depot) {
|
||||||
|
Bankzugang::findeOderLegeDepotAn(
|
||||||
|
(int)$this->zugang['id'],
|
||||||
|
$depot['kontonummer'],
|
||||||
|
$depot['name'] !== '' ? $depot['name'] : 'Depot'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string,mixed> $bankkonto */
|
/** @param array<string,mixed> $bankkonto */
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<?php foreach ($konten as $bankkonto): ?>
|
<?php foreach ($konten as $bankkonto): ?>
|
||||||
<?php
|
<?php
|
||||||
$id = (int)$bankkonto['id'];
|
$id = (int)$bankkonto['id'];
|
||||||
|
$istDepot = (string)$bankkonto['art'] === 'depot';
|
||||||
$kennung = (string)($bankkonto['iban'] ?: $bankkonto['kontonummer']);
|
$kennung = (string)($bankkonto['iban'] ?: $bankkonto['kontonummer']);
|
||||||
// Namensvorschlag für ein neues Konto: Bank und die letzten vier
|
// Namensvorschlag für ein neues Konto: Bank und die letzten vier
|
||||||
// Stellen – „Sparkasse 2051“ ist in der Kontenliste eindeutiger
|
// Stellen – „Sparkasse 2051“ ist in der Kontenliste eindeutiger
|
||||||
|
|
@ -83,6 +84,7 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<?= View::h($kennung) ?>
|
<?= View::h($kennung) ?>
|
||||||
|
<?php if ($istDepot): ?><span class="nebentext">Depot</span><?php endif; ?>
|
||||||
<?php if ($bankkonto['bezeichnung'] !== '' && $bankkonto['bezeichnung'] !== $kennung): ?>
|
<?php if ($bankkonto['bezeichnung'] !== '' && $bankkonto['bezeichnung'] !== $kennung): ?>
|
||||||
<span class="nebentext"><?= View::h($bankkonto['bezeichnung']) ?></span>
|
<span class="nebentext"><?= View::h($bankkonto['bezeichnung']) ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
@ -92,6 +94,7 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
data-neues-konto="neues-konto-<?= $id ?>">
|
data-neues-konto="neues-konto-<?= $id ?>">
|
||||||
<option value="">– nicht zugeordnet –</option>
|
<option value="">– nicht zugeordnet –</option>
|
||||||
<?php foreach ($eigene as $konto): ?>
|
<?php foreach ($eigene as $konto): ?>
|
||||||
|
<?php if ($istDepot !== ((string)$konto['kontoart'] === 'depot')) { continue; } ?>
|
||||||
<option value="<?= (int)$konto['id'] ?>"
|
<option value="<?= (int)$konto['id'] ?>"
|
||||||
<?= (int)$bankkonto['konto_id'] === (int)$konto['id'] ? 'selected' : '' ?>>
|
<?= (int)$bankkonto['konto_id'] === (int)$konto['id'] ? 'selected' : '' ?>>
|
||||||
<?= View::h($konto['name']) ?>
|
<?= View::h($konto['name']) ?>
|
||||||
|
|
@ -108,11 +111,15 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
: View::betrag($bankkonto['saldo']) ?>
|
: View::betrag($bankkonto['saldo']) ?>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<?php if ($istDepot): ?>
|
||||||
|
<span class="nebentext">unter Wertpapiere</span>
|
||||||
|
<?php else: ?>
|
||||||
<label class="ankreuzfeld">
|
<label class="ankreuzfeld">
|
||||||
<input type="checkbox" name="bankkonto[<?= $id ?>][abrufen]" value="1"
|
<input type="checkbox" name="bankkonto[<?= $id ?>][abrufen]" value="1"
|
||||||
<?= (int)$bankkonto['abrufen'] === 1 ? 'checked' : '' ?>>
|
<?= (int)$bankkonto['abrufen'] === 1 ? 'checked' : '' ?>>
|
||||||
ja
|
ja
|
||||||
</label>
|
</label>
|
||||||
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php /* Die Felder des neuen Kontos stehen in einer eigenen Zeile:
|
<?php /* Die Felder des neuen Kontos stehen in einer eigenen Zeile:
|
||||||
|
|
@ -128,10 +135,11 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
value="<?= View::h($vorschlag) ?>">
|
value="<?= View::h($vorschlag) ?>">
|
||||||
|
|
||||||
<label for="neue-kontoart-<?= $id ?>">Kontoart</label>
|
<label for="neue-kontoart-<?= $id ?>">Kontoart</label>
|
||||||
<select id="neue-kontoart-<?= $id ?>" name="bankkonto[<?= $id ?>][neue_kontoart]">
|
<select id="neue-kontoart-<?= $id ?>" name="bankkonto[<?= $id ?>][neue_kontoart]"
|
||||||
|
<?= $istDepot ? 'disabled' : '' ?>>
|
||||||
<?php foreach (Konto::ARTEN as $schluessel => $beschriftung): ?>
|
<?php foreach (Konto::ARTEN as $schluessel => $beschriftung): ?>
|
||||||
<option value="<?= View::h($schluessel) ?>"
|
<option value="<?= View::h($schluessel) ?>"
|
||||||
<?= $schluessel === 'bank' ? 'selected' : '' ?>>
|
<?= $schluessel === ($istDepot ? 'depot' : 'bank') ? 'selected' : '' ?>>
|
||||||
<?= View::h($beschriftung) ?>
|
<?= View::h($beschriftung) ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,9 @@ use Finanzmanager\View;
|
||||||
* diesmal als Wertpapiergeschäft. Ein Kauf ist am Ende zwar eine Umbuchung ins
|
* diesmal als Wertpapiergeschäft. Ein Kauf ist am Ende zwar eine Umbuchung ins
|
||||||
* Depot, aber erst, wenn Stückzahl, Kurs und Nebenkosten feststehen – die
|
* Depot, aber erst, wenn Stückzahl, Kurs und Nebenkosten feststehen – die
|
||||||
* Auswahl führt deshalb in die Wertpapiermaske, statt die Zuordnung sofort zu
|
* Auswahl führt deshalb in die Wertpapiermaske, statt die Zuordnung sofort zu
|
||||||
* speichern. Die schlichte Umbuchung bleibt daneben stehen: Auf ein Depot lässt
|
* speichern. Wo diese Gruppe steht, fehlen die Depots in der Gruppe „Umbuchung
|
||||||
* sich auch Geld überweisen, ohne dass etwas gekauft wird.
|
* auf ein Konto“: Zwei fast gleich benannte Einträge führten dazu, dass ein
|
||||||
|
* Kauf als schlichte Umbuchung ohne Stückzahl und Nebenkosten endete.
|
||||||
*
|
*
|
||||||
* In den Splitzeilen fehlt die Gruppe. Dort steht der Betrag der Zeile schon
|
* In den Splitzeilen fehlt die Gruppe. Dort steht der Betrag der Zeile schon
|
||||||
* fest, und die Umbuchung ins Depot ist genau das, was gebraucht wird.
|
* fest, und die Umbuchung ins Depot ist genau das, was gebraucht wird.
|
||||||
|
|
@ -48,6 +49,7 @@ $depots = !empty($mitDepot) ? array_filter($konten, $istDepot) : [];
|
||||||
<optgroup label="Umbuchung auf ein Konto">
|
<optgroup label="Umbuchung auf ein Konto">
|
||||||
<?php foreach ($konten as $eintrag): ?>
|
<?php foreach ($konten as $eintrag): ?>
|
||||||
<?php if ((int)$eintrag['id'] === (int)$eigenesKonto) { continue; } ?>
|
<?php if ((int)$eintrag['id'] === (int)$eigenesKonto) { continue; } ?>
|
||||||
|
<?php if ($depots !== [] && $istDepot($eintrag)) { continue; } ?>
|
||||||
<?php $wert = 'konto:' . (int)$eintrag['id']; ?>
|
<?php $wert = 'konto:' . (int)$eintrag['id']; ?>
|
||||||
<option value="<?= View::h($wert) ?>"<?= ($gewaehlt ?? '') === $wert ? ' selected' : '' ?>>
|
<option value="<?= View::h($wert) ?>"<?= ($gewaehlt ?? '') === $wert ? ' selected' : '' ?>>
|
||||||
[<?= View::h($eintrag['name']) ?>]
|
[<?= View::h($eintrag['name']) ?>]
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,9 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
</div>
|
</div>
|
||||||
<p class="nebentext">
|
<p class="nebentext">
|
||||||
Der Abruf fragt die Bank nach der Depotaufstellung (FinTS HKWPD) und verlangt in der Regel
|
Der Abruf fragt die Bank nach der Depotaufstellung (FinTS HKWPD) und verlangt in der Regel
|
||||||
eine TAN. Er ändert keine Buchungen: Die Kurse bekannter Wertpapiere werden übernommen,
|
eine TAN. Der Abruf ändert keine Buchungen: Die Kurse bekannter Wertpapiere werden übernommen,
|
||||||
Unterschiede im Bestand zeigt die Tabelle nur an. Das Depot im Finanzmanager braucht dafür
|
Unterschiede im Bestand zeigt die Tabelle an. „Bestand einbuchen“ macht daraus Einlieferungen (Mehrbestand) und Auslieferungen (Minderbestand). Das Depot der Bank muss dafür einem
|
||||||
die Kontonummer der Bank (Kontenverwaltung).
|
Depot im Finanzmanager zugeordnet sein (Online-Banking → Konten zuordnen).
|
||||||
</p>
|
</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
|
@ -64,6 +64,28 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<p class="nebentext">Abgerufen am <?= View::h(View::datum(substr((string)$depot['abgerufen_am'], 0, 10))) ?>
|
<p class="nebentext">Abgerufen am <?= View::h(View::datum(substr((string)$depot['abgerufen_am'], 0, 10))) ?>
|
||||||
um <?= View::h(substr((string)$depot['abgerufen_am'], 11, 5)) ?> Uhr.</p>
|
um <?= View::h(substr((string)$depot['abgerufen_am'], 11, 5)) ?> Uhr.</p>
|
||||||
|
|
||||||
|
<?php $dialogId = 'einbuchdialog-' . (int)$depot['id']; ?>
|
||||||
|
<button type="button" class="knopf" data-einbuchdialog="<?= View::h($dialogId) ?>">Bestand einbuchen</button>
|
||||||
|
|
||||||
|
<dialog class="formatdialog" id="<?= View::h($dialogId) ?>">
|
||||||
|
<h2>Bestand einbuchen – <?= View::h($depot['name']) ?></h2>
|
||||||
|
<form method="post" action="/wertpapiere/bank/einbuchen">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
||||||
|
<input type="hidden" name="konto_id" value="<?= (int)$depot['id'] ?>">
|
||||||
|
<label for="<?= View::h($dialogId) ?>-stichtag">Stichtag</label>
|
||||||
|
<input type="date" id="<?= View::h($dialogId) ?>-stichtag" name="stichtag" required
|
||||||
|
value="<?= View::h(substr((string)$depot['abgerufen_am'], 0, 10)) ?>">
|
||||||
|
<p class="nebentext">
|
||||||
|
Die Unterschiede werden als Einlieferungen (Mehrbestand) und Auslieferungen
|
||||||
|
(Minderbestand) zu diesem Datum gebucht.
|
||||||
|
</p>
|
||||||
|
<div class="knopfleiste">
|
||||||
|
<button type="submit" class="knopf primaer">Einbuchen</button>
|
||||||
|
<button type="submit" class="knopf" formmethod="dialog" formnovalidate>Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<table class="liste zahlen">
|
<table class="liste zahlen">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -71,6 +93,7 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<th class="rechts">Stück laut Bank</th>
|
<th class="rechts">Stück laut Bank</th>
|
||||||
<th class="rechts">Stück im Finanzmanager</th>
|
<th class="rechts">Stück im Finanzmanager</th>
|
||||||
<th class="rechts">Abweichung</th>
|
<th class="rechts">Abweichung</th>
|
||||||
|
<th class="rechts">Einstandskurs</th>
|
||||||
<th class="rechts">Kurs</th>
|
<th class="rechts">Kurs</th>
|
||||||
<th class="rechts">Wert laut Bank</th>
|
<th class="rechts">Wert laut Bank</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -94,6 +117,10 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<?= abs((float)$zeile['abweichung']) < 0.000001
|
<?= abs((float)$zeile['abweichung']) < 0.000001
|
||||||
? '✓' : View::h(View::stueck($zeile['abweichung'])) ?>
|
? '✓' : View::h(View::stueck($zeile['abweichung'])) ?>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="rechts">
|
||||||
|
<?= $zeile['bank'] === null || $zeile['bank']['einstiegskurs'] === null
|
||||||
|
? '–' : View::h(number_format((float)$zeile['bank']['einstiegskurs'], 2, ',', '.')) ?>
|
||||||
|
</td>
|
||||||
<td class="rechts">
|
<td class="rechts">
|
||||||
<?= $zeile['bank'] === null || $zeile['bank']['kurs'] === null
|
<?= $zeile['bank'] === null || $zeile['bank']['kurs'] === null
|
||||||
? '–' : View::h(number_format((float)$zeile['bank']['kurs'], 2, ',', '.')) ?>
|
? '–' : View::h(number_format((float)$zeile['bank']['kurs'], 2, ',', '.')) ?>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
use Fhp\Model\StatementOfHoldings\Holding;
|
use Fhp\Model\StatementOfHoldings\Holding;
|
||||||
use Finanzmanager\Models\Bankprotokoll;
|
use Finanzmanager\Models\Bankprotokoll;
|
||||||
|
use Finanzmanager\Models\Bankzugang;
|
||||||
use Finanzmanager\Models\Depotbuchung;
|
use Finanzmanager\Models\Depotbuchung;
|
||||||
use Finanzmanager\Models\Konto;
|
use Finanzmanager\Models\Konto;
|
||||||
use Finanzmanager\Models\Wertpapier;
|
use Finanzmanager\Models\Wertpapier;
|
||||||
|
|
@ -34,7 +35,10 @@ class DepotBankbestandTest extends TestCase
|
||||||
|
|
||||||
$this->bestandUndKurs();
|
$this->bestandUndKurs();
|
||||||
$this->vergleichZeigtAbweichungen();
|
$this->vergleichZeigtAbweichungen();
|
||||||
|
$this->einbuchen();
|
||||||
|
$this->einbuchenMitStichtag();
|
||||||
$this->zweiterAbrufErsetztDenErsten();
|
$this->zweiterAbrufErsetztDenErsten();
|
||||||
|
$this->depotZuordnung();
|
||||||
$this->nachweisMitPruefsumme();
|
$this->nachweisMitPruefsumme();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,6 +79,42 @@ class DepotBankbestandTest extends TestCase
|
||||||
$this->assertNotEmpty(Depotabgleich::abgerufenAm($this->depot), 'Der Abrufzeitpunkt ist vermerkt');
|
$this->assertNotEmpty(Depotabgleich::abgerufenAm($this->depot), 'Der Abrufzeitpunkt ist vermerkt');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function einbuchen(): void
|
||||||
|
{
|
||||||
|
$e = Depotabgleich::einbuchen($this->depot);
|
||||||
|
|
||||||
|
$this->assertEquals(2, $e['eingeliefert'], 'Zwei Positionen mit Mehrbestand eingeliefert');
|
||||||
|
$this->assertEquals(1, $e['ausgeliefert'], 'Das nur im Finanzmanager geführte Papier ausgeliefert');
|
||||||
|
$this->assertEquals(1, $e['angelegt'], 'Das unbekannte Papier wurde angelegt');
|
||||||
|
$this->assertNotEmpty(Wertpapier::finde('DE0002222222'), 'Neues Wertpapier existiert');
|
||||||
|
|
||||||
|
foreach (Depotabgleich::vergleich($this->depot) as $zeile) {
|
||||||
|
$this->assert(abs((float)$zeile['abweichung']) < 0.000001, 'Nach dem Einbuchen stimmt ' . $zeile['name']);
|
||||||
|
}
|
||||||
|
$this->assertEquals(
|
||||||
|
['eingeliefert' => 0, 'ausgeliefert' => 0, 'angelegt' => 0],
|
||||||
|
Depotabgleich::einbuchen($this->depot),
|
||||||
|
'Ein zweites Einbuchen ändert nichts'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function einbuchenMitStichtag(): void
|
||||||
|
{
|
||||||
|
$depot = Konto::anlegen([
|
||||||
|
'name' => 'Stichtagsdepot', 'kontoart' => 'depot', 'kontonummer' => '502022999',
|
||||||
|
'anfangssaldo' => 0, 'aktiv' => 1,
|
||||||
|
]);
|
||||||
|
Depotabgleich::uebernehmen($depot, [$this->position('DE0004444444', 'Stichtag AG', 5.0, 20.0)]);
|
||||||
|
|
||||||
|
$e = Depotabgleich::einbuchen($depot, '2025-12-31');
|
||||||
|
$this->assertEquals(1, $e['eingeliefert'], 'Der Mehrbestand wird eingeliefert');
|
||||||
|
|
||||||
|
$buchungen = Depotbuchung::liste(['konto_id' => $depot]);
|
||||||
|
$this->assertEquals(1, count($buchungen), 'Eine Depotbuchung entstanden');
|
||||||
|
$this->assertEquals('2025-12-31', substr((string)$buchungen[0]['datum'], 0, 10), 'Die Einlieferung trägt den Stichtag');
|
||||||
|
$this->assert(str_contains((string)$buchungen[0]['notiz'], '30.09.2026'), 'Die Notiz nennt den Bankstand');
|
||||||
|
}
|
||||||
|
|
||||||
private function vergleichZeigtAbweichungen(): void
|
private function vergleichZeigtAbweichungen(): void
|
||||||
{
|
{
|
||||||
$bekannt = Wertpapier::finde('DE0001111111');
|
$bekannt = Wertpapier::finde('DE0001111111');
|
||||||
|
|
@ -115,6 +155,45 @@ class DepotBankbestandTest extends TestCase
|
||||||
$this->assertEquals(1, count($bank), 'Der neue Abruf ersetzt den alten Bankbestand');
|
$this->assertEquals(1, count($bank), 'Der neue Abruf ersetzt den alten Bankbestand');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function depotZuordnung(): void
|
||||||
|
{
|
||||||
|
$zugang = Bankzugang::anlegen([
|
||||||
|
'name' => 'Depotbank', 'blz' => '12030000', 'fints_url' => 'https://example.org/fints',
|
||||||
|
'benutzerkennung' => 'kennung', 'pin' => '1234',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$giro = Bankzugang::findeOderLegeKontoAn($zugang, 'DE02120300000000202051', '202051', 'Giro');
|
||||||
|
$bankdepot = Bankzugang::findeOderLegeDepotAn($zugang, '0502022353', 'Depot');
|
||||||
|
|
||||||
|
$this->assertEquals('depot', $bankdepot['art'], 'Das Depot der Bank ist als Depot vermerkt');
|
||||||
|
$this->assertEquals(
|
||||||
|
$this->depot,
|
||||||
|
(int)$bankdepot['konto_id'],
|
||||||
|
'Passt die Kontonummer (auch mit führender Null), wird gleich zugeordnet'
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
(int)$bankdepot['id'],
|
||||||
|
(int)Bankzugang::findeOderLegeDepotAn($zugang, '0502022353')['id'],
|
||||||
|
'Ein zweites Erkennen legt nichts doppelt an'
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
[(int)$giro['id']],
|
||||||
|
array_map(static fn(array $z): int => (int)$z['id'], Bankzugang::konten($zugang)),
|
||||||
|
'Der Umsatzabruf sieht das Depot nicht'
|
||||||
|
);
|
||||||
|
$this->assertEquals(1, count(Bankzugang::depots($zugang)), 'Das Depot steht in der Depotliste');
|
||||||
|
|
||||||
|
$unbekannt = Bankzugang::findeOderLegeDepotAn($zugang, '999');
|
||||||
|
$this->assertNull($unbekannt['konto_id'], 'Ohne passende Kontonummer bleibt das Depot unzugeordnet');
|
||||||
|
|
||||||
|
Bankzugang::ordneKontoZu((int)$unbekannt['id'], $this->depot, true);
|
||||||
|
$this->assertEquals(
|
||||||
|
$this->depot,
|
||||||
|
(int)Bankzugang::depots($zugang)[1]['konto_id'],
|
||||||
|
'Die Zuordnung lässt sich von Hand treffen'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function nachweisMitPruefsumme(): void
|
private function nachweisMitPruefsumme(): void
|
||||||
{
|
{
|
||||||
$verrechnung = Konto::anlegen([
|
$verrechnung = Konto::anlegen([
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue