verteileRabatt function

List<KasseneckItem> verteileRabatt(
  1. List<KasseneckItem> positionen,
  2. int rabattCents, {
  3. String name = 'Rabatt',
})

Rabatt als negative Position(en) — eine je Steuersatz, anteilig zum Bruttoumsatz dieses Satzes.

Gerundet nach dem größten Rest (Hare-Niemeyer): die Cent, die beim Abrunden übrig bleiben, gehen der Reihe nach an die Gruppen mit dem größten Bruchteil; keine Zeile ist je größer als der Umsatz ihres Satzes, und die Summe der Zeilen ist immer genau der Rabatt.

Rabatt- und Stornozeilen (negativ) zählen nicht als Umsatz: ein zweiter Rabatt rechnet nur auf die Ware.

Implementation

List<KasseneckItem> verteileRabatt(List<KasseneckItem> positionen, int rabattCents, {String name = 'Rabatt'}) {
  if (rabattCents < 0) {
    throw ArgumentError.value(rabattCents, 'rabattCents', 'Rabatt muss eine ganze Zahl in Cent >= 0 sein');
  }
  if (rabattCents == 0) return const [];

  final gruppen = <VatRate, int>{};
  for (final p in positionen) {
    final betrag = p.totalCents;
    if (betrag <= 0) continue;
    gruppen[p.vat] = (gruppen[p.vat] ?? 0) + betrag;
  }
  final saetze = gruppen.keys.toList();
  final gesamt = gruppen.values.fold(0, (s, u) => s + u);
  if (rabattCents > gesamt) {
    throw ArgumentError.value(rabattCents, 'rabattCents', 'Rabatt uebersteigt den Umsatz');
  }

  final exakt = [for (final s in saetze) rabattCents * gruppen[s]! / gesamt];
  final anteile = [for (final x in exakt) x.floor()];
  var rest = rabattCents - anteile.fold(0, (s, a) => s + a);

  // Restcent: größter Bruchteil zuerst, bei Gleichstand größerer Umsatz.
  final reihenfolge = List<int>.generate(saetze.length, (i) => i)
    ..sort((a, b) {
      final bruch = (exakt[b] - anteile[b]).compareTo(exakt[a] - anteile[a]);
      return bruch != 0 ? bruch : gruppen[saetze[b]]!.compareTo(gruppen[saetze[a]]!);
    });
  for (var runde = 0; rest > 0 && runde <= saetze.length; runde++) {
    for (final i in reihenfolge) {
      if (rest == 0) break;
      if (anteile[i] < gruppen[saetze[i]]!) {
        anteile[i] += 1;
        rest -= 1;
      }
    }
  }

  final zeilen = <KasseneckItem>[];
  for (var i = 0; i < saetze.length; i++) {
    if (anteile[i] <= 0) continue;
    // kind 'discount': der Bon fasst die Zeilen zu einer Summenzeile mit
    // Zwischensumme zusammen, der Bericht fuehrt sie als "Rabatte"
    // (Zwilling von verteileRabatt im JS-Paket seit 0.6.42).
    zeilen.add(KasseneckItem(name: name, quantity: 1, priceCents: -anteile[i], vat: saetze[i], kind: 'discount'));
  }
  return zeilen;
}