$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> 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)); } }