$betraege Kostenart => Betrag * @return array{ * kurswert: float, * gebuehren: float, * steuern: float, * kosten: list * } */ public static function aufteilung(float $geld, array $betraege, ?int $sonstigeKategorie = null): array { $geld = round($geld, 2); $zeilen = Wertpapierkosten::zeilen($betraege, $sonstigeKategorie); $summen = Wertpapierkosten::summen($zeilen); return [ 'kurswert' => round($geld + $summen['summe'], 2), 'gebuehren' => $summen['gebuehren'], 'steuern' => $summen['steuern'], 'kosten' => $zeilen, ]; } /** * Der Vorgang zum Geldfluss. Ein Abfluss kann nur ein Kauf sein; ein Zufluss * ist ein Verkauf oder – wenn die Maske es so sagt – eine Dividende. */ public static function vorgang(float $geld, string $gewaehlt = ''): string { if ($geld < 0) { return 'kauf'; } return $gewaehlt === 'dividende' ? 'dividende' : 'verkauf'; } /** * Depotbuchung und Aufteilung in einem Zug. * * @param array $buchung die Buchung des Verrechnungskontos * @param array $eingaben depot_id, wertpapier_id, wertpapier_name, isin, * wkn, art, vorgang, datum, valuta, stueck, kurs, * notiz, kosten, kosten_sonstige_kategorie * @return array{depotbuchung_id:int,vorgang:string,betrag:float,gesplittet:bool} */ public static function buchen(array $buchung, array $eingaben): array { $depot = Konto::find((int)($eingaben['depot_id'] ?? 0)); if ($depot === null || (string)$depot['kontoart'] !== 'depot') { throw new \InvalidArgumentException('Für ein Wertpapiergeschäft fehlt das Depot.'); } if (Depotbuchung::findeZuBuchung((int)$buchung['id']) !== null) { throw new \RuntimeException( 'Zu dieser Buchung gibt es schon ein Wertpapiergeschäft. Es ist unter den ' . 'Depotbuchungen zu löschen, bevor ein neues entstehen kann.' ); } $geld = round((float)$buchung['betrag'], 2); if (abs($geld) < 0.005) { throw new \InvalidArgumentException( 'Eine Buchung über null lässt sich nicht in ein Geschäft aufteilen.' ); } $aufteilung = self::aufteilung( $geld, is_array($eingaben['kosten'] ?? null) ? $eingaben['kosten'] : [], ($eingaben['kosten_sonstige_kategorie'] ?? null) ?: null ); $kurswert = $aufteilung['kurswert']; if (abs($kurswert) < 0.005 || ($kurswert < 0) !== ($geld < 0)) { throw new \InvalidArgumentException( 'Die Nebenkosten sind so hoch wie die Buchung – für das Wertpapier bliebe nichts übrig.' ); } $vorgang = self::vorgang($geld, (string)($eingaben['vorgang'] ?? '')); $stueck = abs((float)($eingaben['stueck'] ?? 0)); if ($vorgang !== 'dividende' && $stueck <= 0.0) { throw new \InvalidArgumentException('Ohne Stückzahl lässt sich kein Kauf oder Verkauf buchen.'); } $kurs = abs((float)($eingaben['kurs'] ?? 0)); if ($kurs <= 0.0 && $stueck > 0.0) { $kurs = round(abs($kurswert) / $stueck, 6); } return Database::transaktion(static function () use ( $buchung, $depot, $eingaben, $aufteilung, $kurswert, $vorgang, $stueck, $kurs ): array { $wertpapierId = (int)($eingaben['wertpapier_id'] ?? 0); $name = trim((string)($eingaben['wertpapier_name'] ?? '')); if ($wertpapierId === 0) { if ($name === '') { throw new \InvalidArgumentException('Ein neues Wertpapier braucht einen Namen.'); } $wertpapierId = Wertpapier::findenOderAnlegen([ 'name' => $name, 'isin' => (string)($eingaben['isin'] ?? ''), 'wkn' => (string)($eingaben['wkn'] ?? ''), 'art' => (string)($eingaben['art'] ?? 'aktie'), ]); } $papier = Wertpapier::find($wertpapierId); $bezeichnung = $papier === null ? 'Wertpapier' : (string)$papier['name']; $depotbuchungId = Depotbuchung::anlegen([ 'konto_id' => (int)$depot['id'], 'wertpapier_id' => $wertpapierId, 'datum' => ($eingaben['datum'] ?? '') ?: substr((string)$buchung['datum'], 0, 10), 'valuta' => $eingaben['valuta'] ?? '', 'vorgang' => $vorgang, 'stueck' => $stueck, 'kurs' => $kurs > 0.0 ? $kurs : null, 'betrag' => abs($kurswert), 'gebuehren' => $aufteilung['gebuehren'], 'steuern' => $aufteilung['steuern'], 'verrechnungskonto_id' => (int)$buchung['konto_id'], // Der Kern des Wegs übers Kontoblatt: Die Geldseite gibt es // schon, sie kommt vom Bankabruf. 'buchung_id' => (int)$buchung['id'], 'notiz' => ($eingaben['notiz'] ?? '') ?: (string)$buchung['buchungstext'], ]); $gesplittet = $aufteilung['kosten'] !== []; if (!$gesplittet) { Buchung::zuordnen((int)$buchung['id'], null, (int)$depot['id']); } else { $splits = [[ 'gegenkonto_id' => (int)$depot['id'], 'betrag' => $kurswert, 'notiz' => $bezeichnung, ]]; foreach ($aufteilung['kosten'] as $zeile) { $splits[] = [ 'kategorie_id' => $zeile['kategorie_id'], // Nebenkosten sind Aufwand – beim Kauf wie beim Verkauf. 'betrag' => -$zeile['betrag'], 'notiz' => $zeile['name'], ]; } Buchung::splitsErsetzen((int)$buchung['id'], $splits); } return [ 'depotbuchung_id' => $depotbuchungId, 'vorgang' => $vorgang, 'betrag' => abs($kurswert), 'gesplittet' => $gesplittet, ]; }); } }