- 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>
222 lines
8.9 KiB
PHP
222 lines
8.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Finanzmanager\Services;
|
|
|
|
use Finanzmanager\Database;
|
|
use Finanzmanager\Mandant;
|
|
use Finanzmanager\Models\Depotbuchung;
|
|
use Finanzmanager\Models\Wertpapier;
|
|
|
|
/**
|
|
* Der Depotbestand laut Bank (FinTS-Depotaufstellung) im Vergleich mit dem
|
|
* eigenen Bestand.
|
|
*
|
|
* Der Abruf übernimmt nur, was sicher ist: der Bestand landet in
|
|
* `depot_bankbestaende`, und die Bewertungskurse der Papiere, die es im
|
|
* Finanzmanager schon gibt, werden als Kurs des Abrufdatums eingetragen. Ein
|
|
* Unterschied im Bestand ist zunächst ein Hinweis; erst `einbuchen()` macht
|
|
* daraus Depotbuchungen (auf ausdrücklichen Knopfdruck).
|
|
*/
|
|
class Depotabgleich
|
|
{
|
|
/**
|
|
* Ersetzt den Bankbestand eines Depots durch den frischen Abruf.
|
|
*
|
|
* @param list<\Fhp\Model\StatementOfHoldings\Holding> $positionen
|
|
* @return array{positionen: int, kurse: int}
|
|
*/
|
|
public static function uebernehmen(int $kontoId, array $positionen): array
|
|
{
|
|
return Database::transaktion(static function () use ($kontoId, $positionen): array {
|
|
Database::execute(
|
|
'DELETE FROM depot_bankbestaende WHERE konto_id = ? AND benutzer_id = ?',
|
|
[$kontoId, Mandant::id()]
|
|
);
|
|
|
|
$kurse = 0;
|
|
foreach ($positionen as $position) {
|
|
$isin = strtoupper(trim((string)$position->getISIN()));
|
|
$wkn = trim((string)$position->getWKN());
|
|
$name = trim((string)$position->getName());
|
|
$stand = $position->getDate()?->format('Y-m-d');
|
|
$kurs = $position->getPrice();
|
|
|
|
Database::execute(
|
|
'INSERT INTO depot_bankbestaende
|
|
(benutzer_id, konto_id, isin, wkn, name, stueck, einstiegskurs, kurs, wert, waehrung, stand)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
[
|
|
Mandant::id(),
|
|
$kontoId,
|
|
$isin,
|
|
$wkn,
|
|
mb_substr($name, 0, 255),
|
|
(float)$position->getAmount(),
|
|
$position->getAcquisitionPrice(),
|
|
$kurs,
|
|
$position->getValue() === null ? null : round($position->getValue(), 2),
|
|
strtoupper((string)$position->getCurrency()) ?: 'EUR',
|
|
$stand,
|
|
]
|
|
);
|
|
|
|
if ($kurs === null || $kurs <= 0.0) {
|
|
continue;
|
|
}
|
|
$papier = Wertpapier::finde($isin, $wkn, $name);
|
|
if ($papier !== null) {
|
|
Wertpapier::kursSetzen(
|
|
(int)$papier['id'],
|
|
$stand ?? date('Y-m-d'),
|
|
$kurs,
|
|
'bank',
|
|
strtoupper((string)$position->getCurrency()) ?: 'EUR'
|
|
);
|
|
$kurse++;
|
|
}
|
|
}
|
|
|
|
return ['positionen' => count($positionen), 'kurse' => $kurse];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Bankbestand und eigener Bestand nebeneinander, je Papier eine Zeile.
|
|
* Verglichen wird über die ISIN, ohne ISIN über den Namen.
|
|
*
|
|
* @return list<array<string,mixed>> mit `bank`, `eigen` und `abweichung`
|
|
* (Stück laut Bank minus eigene Stück)
|
|
*/
|
|
public static function vergleich(int $kontoId): array
|
|
{
|
|
$bank = Database::fetchAll(
|
|
'SELECT * FROM depot_bankbestaende WHERE konto_id = ? AND benutzer_id = ? ORDER BY name',
|
|
[$kontoId, Mandant::id()]
|
|
);
|
|
|
|
$eigen = [];
|
|
foreach (Depot::bestand($kontoId)['positionen'] as $position) {
|
|
if (abs((float)$position['stueck']) < 0.000001) {
|
|
continue;
|
|
}
|
|
$eigen[self::schluessel((string)$position['isin'], (string)$position['name'])] = $position;
|
|
}
|
|
|
|
$zeilen = [];
|
|
foreach ($bank as $eintrag) {
|
|
$schluessel = self::schluessel((string)$eintrag['isin'], (string)$eintrag['name']);
|
|
$mein = $eigen[$schluessel] ?? null;
|
|
unset($eigen[$schluessel]);
|
|
|
|
$zeilen[] = [
|
|
'name' => (string)$eintrag['name'],
|
|
'isin' => (string)$eintrag['isin'],
|
|
'bank' => $eintrag,
|
|
'eigen' => $mein,
|
|
'abweichung' => round((float)$eintrag['stueck'] - (float)($mein['stueck'] ?? 0.0), 6),
|
|
];
|
|
}
|
|
|
|
// Was nur im Finanzmanager steht, hat die Bank nicht (mehr) im Depot.
|
|
foreach ($eigen as $mein) {
|
|
$zeilen[] = [
|
|
'name' => (string)$mein['name'],
|
|
'isin' => (string)$mein['isin'],
|
|
'bank' => null,
|
|
'eigen' => $mein,
|
|
'abweichung' => round(-(float)$mein['stueck'], 6),
|
|
];
|
|
}
|
|
|
|
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. */
|
|
public static function abgerufenAm(int $kontoId): ?string
|
|
{
|
|
$wert = Database::fetchValue(
|
|
'SELECT MAX(abgerufen_am) FROM depot_bankbestaende WHERE konto_id = ? AND benutzer_id = ?',
|
|
[$kontoId, Mandant::id()]
|
|
);
|
|
|
|
return $wert === false || $wert === null ? null : (string)$wert;
|
|
}
|
|
|
|
private static function schluessel(string $isin, string $name): string
|
|
{
|
|
$isin = strtoupper(trim($isin));
|
|
|
|
return $isin !== '' ? $isin : 'name:' . mb_strtolower(trim($name));
|
|
}
|
|
}
|