receipt_engine 0.6.1 copy "receipt_engine: ^0.6.1" to clipboard
receipt_engine: ^0.6.1 copied to clipboard

Motore di calcolo per scontrini fiscali - righe, sconti, IVA multi-aliquota, storni e resi, arrotondamenti e resto. Logica pura, zero dipendenze.

receipt_engine #

Motore di calcolo per scontrini fiscali italiani: righe, sconti, IVA multi-aliquota, storni e resi, arrotondamenti e resto. Logica pura in Dart, senza dipendenze da Flutter, database o I/O.

pub package pub points CI license: MIT

Perché esiste #

Nasce da un problema reale incontrato lavorando su software per registratori di cassa: il calcolo di uno scontrino sembra banale finché non ci si scontra con gli arrotondamenti. Se l'imposta si calcola riga per riga, la somma può differire di qualche centesimo dall'imposta calcolata sul totale — e uno scontrino che non quadra per un centesimo è un problema contabile, non un dettaglio estetico.

Questo pacchetto isola quella logica in un modulo testabile in millisecondi, riutilizzabile da un'app mobile, da un backend o da un tool a riga di comando.

Scelte di progetto #

Scelta Motivo
Importi in centesimi (Money), mai double 0.1 + 0.2 != 0.3 in virgola mobile binaria. Sul denaro non è accettabile
IVA scorporata sul totale per aliquota, non per riga È il requisito normativo, ed evita che gli arrotondamenti di riga si accumulino
Imposta derivata per differenza dall'imponibile Garantisce l'invariante imponibile + imposta == lordo, sempre
Receipt immutabile, ReceiptBuilder mutabile Un documento fiscale non si modifica: si emette e semmai si storna
Il reso è un secondo documento, con importi negativi Rende sommabili scontrini e resi: il totale di giornata è la somma di tutto, senza casi particolari
Si rimborsa l'incassato di riga, non il totale di riga Con uno sconto di documento le due cose differiscono, e rimborsare la seconda restituirebbe anche lo sconto
Resi parziali con arrotondamento cumulativo Tre resi da un pezzo devono restituire esattamente quanto un reso da tre
VatRate come classe e non come enum Le aliquote cambiano per legge e per paese: un enum imporrebbe un rilascio a ogni variazione
Sconti polimorfi invece di uno switch Aggiungere un "3x2" non richiede di modificare il calcolo esistente
Ripartizione dello sconto come strategia sostituibile È una scelta contabile, non un dettaglio di calcolo
Formattazione fuori dal value object Cambia con la lingua e col contesto: non deve stare nel dominio
Il resto esce solo da mezzi che danno resto Totale 22 €, pagati 25 con la carta: i 3 € di resto non sono in cassa. È un errore, non un resto
Serializzazione fuori dai modelli, e a mano Uno scontrino non deve sapere come viaggia. json_serializable porterebbe build_runner e file generati per sei modelli che cambiano di rado
La rilettura non ricalcola Un documento fiscale emesso si rilegge, non si rifà: altrimenti una correzione futura agli arrotondamenti cambierebbe scontrini già consegnati
Zero dipendenze a runtime Il dominio non deve sapere che esistono Flutter o un database

Installazione #

dart pub add receipt_engine
import 'package:receipt_engine/receipt_engine.dart';

L'API completa è su pub.flutter-io.cn/documentation/receipt_engine.

Esempio #

import 'package:receipt_engine/receipt_engine.dart';

void main() {
  final Receipt receipt = ReceiptBuilder(id: 'T-0001')
      .addLine(
        description: 'Caffè',
        unitPrice: Money.fromEuro(1.20),
        vatRate: VatRate.reduced,
        quantity: 2,
      )
      .addLine(
        description: 'Vino',
        unitPrice: Money.fromEuro(12.20),
        vatRate: VatRate.standard,
        discount: Discount.percent(10),
      )
      .applyDocumentDiscount(Discount.amount(Money.fromEuro(1)))
      .close(paid: Money.fromEuro(20));

  const MoneyFormatter fmt = ItalianMoneyFormatter();
  print(fmt.format(receipt.total));   // 12,38 €
  print(fmt.format(receipt.change));  // 7,62 €

  for (final VatBreakdown v in receipt.vatSummary) {
    print(v); // IVA 10%: imponibile ..., imposta ...
  }
}

Il cliente riporta il vino. Lo scontrino non si tocca: si emette un reso.

final ReturnReceipt reversal =
    ReturnBuilder(id: 'R-0001', original: receipt).addLine(1).close();

print(fmt.format(reversal.refund));                // 10,16 € da restituire
print(fmt.format(receipt.lines[1].total));         // 10,98 € il totale di riga
print(fmt.format(receipt.total + reversal.total)); //  2,22 € resta il caffè

Il rimborso è 10,16 e non 10,98 perché su quella riga il cliente aveva già goduto della sua quota di sconto di documento. Il riepilogo IVA del reso è quello dello scontrino con il segno cambiato, e la somma dei due torna a zero sulle righe rese — imposta compresa.

L'esempio completo, eseguibile, è in example/receipt_engine_example.dart.

Pagamenti misti #

// Totale 22 €: 15 con la carta, 10 in contanti.
final Receipt receipt = builder.closeWithPayments(<Payment>[
  Payment.electronic(Money.fromEuro(15)),
  Payment.cash(Money.fromEuro(10)),
]);

print(fmt.format(receipt.change)); // 3,00 €, dal cassetto

La regola che conta: il resto esce solo da mezzi che danno resto. Pagare 25 € con la sola carta su un totale di 22 non produce 3 € di resto, solleva ChangeNotAvailableError: la carta ha addebitato 25 e nel cassetto non è entrato niente. E non basta che ci siano dei contanti — 23 con la carta e 5 in contanti fanno un resto di 6, e dal cassetto ne possono uscire al massimo 5.

close(paid:) resta la scorciatoia per il caso comune, un pagamento in contanti, e si comporta esattamente come prima. I mezzi di pagamento sono aperti come le aliquote: un buono pasto è PaymentMethod('meal_voucher', label: 'Buono pasto'), e non dà resto a meno di dirlo.

Fuori dal processo: JSON #

Chi lo usa da un backend, o dietro una coda, deve poter scrivere uno scontrino da qualche parte e rileggerlo identico.

const ReceiptJson codec = ReceiptJson();

final String wire = jsonEncode(codec.encodeReceipt(receipt));
final Receipt again =
    codec.decodeReceipt(jsonDecode(wire) as Map<String, Object?>);

Il codec produce una mappa, non una stringa: dart:convert non compare da nessuna parte dentro il pacchetto, così la mappa si annida in un documento più grande, va a un encoder diverso o finisce in un database che parla già di mappe.

Due cose vale la pena sapere prima di usarlo.

La rilettura non ricalcola niente. I totali di riga al netto dello sconto e il riepilogo IVA vengono riletti dal documento, non rifatti passando per ReceiptBuilder. Un documento fiscale emesso si rilegge: se lo si ricostruisse dal builder, una correzione futura agli arrotondamenti cambierebbe retroattivamente scontrini già consegnati al cliente.

Ogni documento porta uno schemaVersion. È la differenza fra un formato e uno scarico di campi: rileggere un documento scritto da una versione più recente del pacchetto solleva un errore invece di produrre uno scontrino monco.

È servito subito. La 0.6.0 aggiunge i pagamenti e scrive lo schema 2; un documento salvato con la 0.5.0 si rilegge ancora, e i suoi pagamenti diventano un solo pagamento in contanti — che è quello che paid ha sempre voluto dire.

Struttura #

lib/
  receipt_engine.dart              API pubblica
  src/
    money.dart                     valore monetario in centesimi
    vat_calculator.dart            scorporo dell'imposta
    discount_allocator.dart        strategia di ripartizione dello sconto
    vat_summary_calculator.dart    ripartizione dello sconto e riepilogo per aliquota
    receipt_builder.dart           ciclo di vita dello scontrino
    return_builder.dart            ciclo di vita del reso
    formatting/money_formatter.dart
    serialization/receipt_json.dart  lettura e scrittura JSON
    models/
      vat_rate.dart  discount.dart  receipt_line.dart
      receipt.dart   return_receipt.dart  payment.dart
example/                           programma eseguibile: emissione e storno
test/                              invarianti, contratto dei sottotipi, allocazione

Le scelte architetturali e i principi applicati sono in ARCHITECTURE.md.

Test #

dart pub get
dart test
dart test --coverage=coverage

La pipeline misura anche il punteggio di pub.flutter-io.cn a ogni push, con pana, e fallisce se scende sotto 130 su 160: un criterio scritto dove può fallire vale più dello stesso criterio scritto in un documento. pana non gira su Windows — il suo sandbox rifiuta i percorsi con i due punti dei dischi — quindi la CI non è una comodità, è l'unico posto dove quel numero esiste.

I test non verificano solo i casi felici. Due esempi di invarianti verificate:

  • imponibile + imposta == lordo per ogni importo da 1 a 2000 centesimi e per ogni aliquota;
  • la somma dei lordi per aliquota è sempre uguale al totale del documento, anche in presenza di uno sconto di documento che va distribuito fra aliquote diverse;
  • il contratto di Discount — mai negativo, mai superiore alla base — vale per ogni sottotipo, incluso uno definito dentro il file di test per dimostrare che la gerarchia è davvero aperta all'estensione;
  • lo scorporo di un importo negativo è esattamente l'opposto di quello positivo, su ottomila combinazioni di importo e aliquota: è la proprietà su cui si regge il riepilogo IVA dei resi;
  • comunque si spezzi il reso di una riga — un pezzo alla volta, in qualunque ordine — la somma dei rimborsi è esattamente quanto quella riga aveva incassato, verificato su millequattrocento combinazioni di prezzo e quantità.

In più, in test/properties, sette invarianti verificate su scontrini generati invece che scelti: è lì che si rompono gli arrotondamenti, perché nessuno scrive a mano «2,04 € per 1,8 kg con l'8% di sconto». Hanno già trovato un difetto vero — l'ultima tranche di un reso su merce a peso veniva rifiutata — e si sono ridotti da soli al caso minimo che lo mostra.

Il seme è fisso, così un fallimento è sempre riproducibile. Per cercare più a fondo:

PROPERTY_SEED=12345 dart test test/properties

Stato e prossimi passi #

Pubblicato su pub.flutter-io.cn con 160/160 al punteggio.

Le tre cose che questa sezione elencava come mancanti — storni e resi, serializzazione JSON, pagamenti misti — sono dentro. Non ne aggiungo altre per riempire la lista: la prossima la decide chi lo usa, e le issue sono il posto dove dirlo.

Resta una cosa che non arriverà, che è diverso dal mancare. Fino alla 0.3.1 questa sezione prometteva il «supporto ad aliquote di altri paesi»: la promessa è ritirata. Dalla 0.4.0 un'aliquota è un num, quindi il 5,5% francese o il 13,5% irlandese si calcolano senza problemi — ma sapere quale bene sta a quale aliquota, quando vale il reverse charge e come si numera un documento in un altro ordinamento è lavoro di dominio, non di aritmetica. receipt_engine resta un motore a IVA italiana con i conti aperti a qualunque percentuale, e chi ha bisogno delle regole di un altro paese sa già, leggendo questa riga, che qui non le trova.

Licenza #

MIT

1
likes
160
points
164
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

Motore di calcolo per scontrini fiscali - righe, sconti, IVA multi-aliquota, storni e resi, arrotondamenti e resto. Logica pura, zero dipendenze.

Repository (GitHub)
View/report issues

Topics

#receipt #pos #vat #money #retail

License

MIT (license)

More

Packages that depend on receipt_engine