feat(invoice): add invoice document model

This commit is contained in:
dualfroz committed 2026-08-27 11:17:00 +02:00
1 parent 96e45a2877
commit d47d138f9a
1 file changed
+175
+175
View File
@@ -0,0 +1,175 @@
import 'money.dart';
class VatRate {
final String label;
final double rate;
const VatRate(this.label, this.rate);
static const standard = VatRate('23%', 0.23);
static const reduced8 = VatRate('8%', 0.08);
static const reduced5 = VatRate('5%', 0.05);
static const zero = VatRate('0%', 0.0);
static const exempt = VatRate('zw', 0.0);
static VatRate fromPercent(num percent) => VatRate('$percent%', percent / 100);
@override
bool operator ==(Object other) => other is VatRate && other.label == label && other.rate == rate;
@override
int get hashCode => Object.hash(label, rate);
}
class Party {
final String name;
final String? address;
final String? taxId;
final String? email;
const Party({required this.name, this.address, this.taxId, this.email});
}
class InvoiceLine {
final String description;
final num quantity;
final Money unitPrice;
final double discountRate;
final VatRate vat;
final String unit;
const InvoiceLine({
required this.description,
required this.quantity,
required this.unitPrice,
required this.vat,
this.discountRate = 0.0,
this.unit = 'pcs',
});
Money get grossBeforeDiscount => unitPrice.scaled(quantity);
Money get discountAmount => grossBeforeDiscount.scaled(discountRate);
Money get net => grossBeforeDiscount - discountAmount;
Money get vatAmount => net.scaled(vat.rate);
Money get total => net + vatAmount;
}
class VatSummaryRow {
final VatRate rate;
final Money net;
final Money vat;
const VatSummaryRow(this.rate, this.net, this.vat);
Money get gross => net + vat;
}
class Invoice {
final String number;
final DateTime issueDate;
final DateTime? dueDate;
final Party seller;
final Party buyer;
final Currency currency;
final List<InvoiceLine> lines;
final String? notes;
final String? paymentTerms;
final Currency? reportingCurrency;
final double? reportingRate;
const Invoice({
required this.number,
required this.issueDate,
required this.seller,
required this.buyer,
required this.currency,
required this.lines,
this.dueDate,
this.notes,
this.paymentTerms,
this.reportingCurrency,
this.reportingRate,
});
List<VatSummaryRow> vatSummary() {
final order = <String>[];
final rates = <String, VatRate>{};
final nets = <String, Money>{};
for (final line in lines) {
final key = line.vat.label;
if (!nets.containsKey(key)) {
order.add(key);
rates[key] = line.vat;
nets[key] = Money.zero(currency);
}
nets[key] = nets[key]! + line.net;
}
return [
for (final key in order)
VatSummaryRow(rates[key]!, nets[key]!, nets[key]!.scaled(rates[key]!.rate)),
];
}
Money get totalNet {
var sum = Money.zero(currency);
for (final row in vatSummary()) {
sum += row.net;
}
return sum;
}
Money get totalVat {
var sum = Money.zero(currency);
for (final row in vatSummary()) {
sum += row.vat;
}
return sum;
}
Money get totalGross => totalNet + totalVat;
Money? get reportingTotalGross {
final target = reportingCurrency;
final rate = reportingRate;
if (target == null || rate == null) return null;
return totalGross.convertedTo(target, rate);
}
Money? get reportingTotalVat {
final target = reportingCurrency;
final rate = reportingRate;
if (target == null || rate == null) return null;
return totalVat.convertedTo(target, rate);
}
}
class InvoiceSequence {
final String pattern;
int counter;
InvoiceSequence(this.pattern, {this.counter = 1});
String format(DateTime date, int seq) {
return pattern.replaceAllMapped(RegExp(r'\{(\w+)(?::(\d+))?\}'), (match) {
final field = match.group(1)!;
final width = match.group(2) == null ? 0 : int.parse(match.group(2)!);
final value = switch (field) {
'seq' => seq,
'year' => date.year,
'month' => date.month,
'day' => date.day,
_ => throw ArgumentError('Unknown numbering field: $field'),
};
return value.toString().padLeft(width, '0');
});
}
String next(DateTime date) {
final formatted = format(date, counter);
counter++;
return formatted;
}
}