feat(invoice): add invoice document model
This commit is contained in:
1 parent
96e45a2877
commit
d47d138f9a
1 file changed
+175
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user