Add support for variable currency precisions

This commit is contained in:
Hillel Coren 2020-11-26 15:50:54 +02:00
parent 32e11c5880
commit db393aa146
10 changed files with 75 additions and 47 deletions

View File

@ -879,9 +879,9 @@ abstract class InvoiceEntity extends Object
}
/// Gets taxes in the form { taxName1: { amount: 0, paid: 0} , ... }
Map<String, Map<String, dynamic>> getTaxes() {
Map<String, Map<String, dynamic>> getTaxes(int precision) {
final taxes = <String, Map<String, dynamic>>{};
final taxable = calculateTaxes(usesInclusiveTaxes);
final taxable = calculateTaxes(useInclusiveTaxes: usesInclusiveTaxes, precision: precision);
final paidAmount = amount - balance;
if (taxRate1 != 0) {

View File

@ -1,4 +1,5 @@
import 'package:built_collection/built_collection.dart';
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/data/models/invoice_model.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart';
@ -40,18 +41,19 @@ abstract class CalculateInvoiceTotal {
BuiltList<InvoiceItemEntity> get lineItems;
double _calculateTaxAmount(
double amount, double rate, bool useInclusiveTaxes) {
double amount, double rate, bool useInclusiveTaxes, int precision) {
double taxAmount;
if (useInclusiveTaxes) {
taxAmount = amount - (amount / (1 + (rate / 100)));
} else {
taxAmount = amount * rate / 100;
}
return round(taxAmount, 2);
return round(taxAmount, precision);
}
Map<String, double> calculateTaxes(bool useInclusiveTaxes) {
double total = subtotal;
Map<String, double> calculateTaxes(
{@required bool useInclusiveTaxes, @required int precision}) {
double total = calculateSubtotal(precision: precision);
double taxAmount;
final map = <String, double>{};
@ -59,20 +61,23 @@ abstract class CalculateInvoiceTotal {
final double taxRate1 = round(item.taxRate1, 3);
final double taxRate2 = round(item.taxRate2, 3);
final lineTotal = getItemTaxable(item, total);
final lineTotal = getItemTaxable(item, total, precision);
if (taxRate1 != 0) {
taxAmount = _calculateTaxAmount(lineTotal, taxRate1, useInclusiveTaxes);
taxAmount = _calculateTaxAmount(
lineTotal, taxRate1, useInclusiveTaxes, precision);
map.update(item.taxName1, (value) => value + taxAmount,
ifAbsent: () => taxAmount);
}
if (taxRate2 != 0) {
taxAmount = _calculateTaxAmount(lineTotal, taxRate2, useInclusiveTaxes);
taxAmount = _calculateTaxAmount(
lineTotal, taxRate2, useInclusiveTaxes, precision);
map.update(item.taxName2, (value) => value + taxAmount,
ifAbsent: () => taxAmount);
}
if (taxRate3 != 0) {
taxAmount = _calculateTaxAmount(lineTotal, taxRate3, useInclusiveTaxes);
taxAmount = _calculateTaxAmount(
lineTotal, taxRate3, useInclusiveTaxes, precision);
map.update(item.taxName3, (value) => value + taxAmount,
ifAbsent: () => taxAmount);
}
@ -80,34 +85,37 @@ abstract class CalculateInvoiceTotal {
if (discount != 0.0) {
if (isAmountDiscount) {
total -= round(discount, 2);
total -= round(discount, precision);
} else {
total -= round(total * discount / 100, 2);
total -= round(total * discount / 100, precision);
}
}
if (customSurcharge1 != 0.0 && customTaxes1) {
total += round(customSurcharge1, 2);
total += round(customSurcharge1, precision);
}
if (customSurcharge2 != 0.0 && customTaxes2) {
total += round(customSurcharge2, 2);
total += round(customSurcharge2, precision);
}
if (taxRate1 != 0) {
taxAmount = _calculateTaxAmount(total, taxRate1, useInclusiveTaxes);
taxAmount =
_calculateTaxAmount(total, taxRate1, useInclusiveTaxes, precision);
map.update(taxName1, (value) => value + taxAmount,
ifAbsent: () => taxAmount);
}
if (taxRate2 != 0) {
taxAmount = _calculateTaxAmount(total, taxRate2, useInclusiveTaxes);
taxAmount =
_calculateTaxAmount(total, taxRate2, useInclusiveTaxes, precision);
map.update(taxName2, (value) => value + taxAmount,
ifAbsent: () => taxAmount);
}
if (taxRate3 != 0) {
taxAmount = _calculateTaxAmount(total, taxRate3, useInclusiveTaxes);
taxAmount =
_calculateTaxAmount(total, taxRate3, useInclusiveTaxes, precision);
map.update(taxName3, (value) => value + taxAmount,
ifAbsent: () => taxAmount);
}
@ -115,10 +123,11 @@ abstract class CalculateInvoiceTotal {
return map;
}
double getItemTaxable(InvoiceItemEntity item, double invoiceTotal) {
double getItemTaxable(
InvoiceItemEntity item, double invoiceTotal, int precision) {
final double qty = round(item.quantity, 4);
final double cost = round(item.cost, 4);
final double itemDiscount = round(item.discount, 2);
final double itemDiscount = round(item.discount, precision);
double lineTotal = qty * cost;
if (discount != 0) {
@ -137,17 +146,17 @@ abstract class CalculateInvoiceTotal {
}
}
return round(lineTotal, 2);
return round(lineTotal, precision);
}
double get calculateTotal {
double total = subtotal;
double calculateTotal({@required int precision}) {
double total = calculateSubtotal(precision: precision);
double itemTax = 0.0;
lineItems.forEach((item) {
final double qty = round(item.quantity, 4);
final double cost = round(item.cost, 4);
final double itemDiscount = round(item.discount, 2);
final double itemDiscount = round(item.discount, precision);
final double taxRate1 = round(item.taxRate1, 3);
final double taxRate2 = round(item.taxRate2, 3);
double lineTotal = qty * cost;
@ -168,54 +177,54 @@ abstract class CalculateInvoiceTotal {
}
}
if (taxRate1 != 0) {
itemTax += round(lineTotal * taxRate1 / 100, 2);
itemTax += round(lineTotal * taxRate1 / 100, precision);
}
if (taxRate2 != 0) {
itemTax += round(lineTotal * taxRate2 / 100, 2);
itemTax += round(lineTotal * taxRate2 / 100, precision);
}
});
if (discount != 0.0) {
if (isAmountDiscount) {
total -= round(discount, 2);
total -= round(discount, precision);
} else {
total -= round(total * discount / 100, 2);
total -= round(total * discount / 100, precision);
}
}
if (customSurcharge1 != 0.0 && customTaxes1) {
total += round(customSurcharge1, 2);
total += round(customSurcharge1, precision);
}
if (customSurcharge2 != 0.0 && customTaxes2) {
total += round(customSurcharge2, 2);
total += round(customSurcharge2, precision);
}
if (!usesInclusiveTaxes) {
final double taxAmount1 = round(total * taxRate1 / 100, 2);
final double taxAmount2 = round(total * taxRate2 / 100, 2);
final double taxAmount1 = round(total * taxRate1 / 100, precision);
final double taxAmount2 = round(total * taxRate2 / 100, precision);
total += itemTax + taxAmount1 + taxAmount2;
}
if (customSurcharge1 != 0.0 && !customTaxes1) {
total += round(customSurcharge1, 2);
total += round(customSurcharge1, precision);
}
if (customSurcharge2 != 0.0 && !customTaxes2) {
total += round(customSurcharge2, 2);
total += round(customSurcharge2, precision);
}
return total;
}
double get subtotal {
double calculateSubtotal({@required int precision}) {
var total = 0.0;
lineItems.forEach((item) {
final double qty = round(item.quantity, 4);
final double cost = round(item.cost, 4);
final double discount = round(item.discount, 2);
final double discount = round(item.discount, precision);
double lineTotal = qty * cost;
@ -227,7 +236,7 @@ abstract class CalculateInvoiceTotal {
}
}
total += round(lineTotal, 2);
total += round(lineTotal, precision);
});
return total;

View File

@ -1,3 +1,4 @@
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/redux/static/static_state.dart';
import 'package:memoize/memoize.dart';
import 'package:built_collection/built_collection.dart';
@ -216,6 +217,12 @@ EntityStats invoiceStatsForUser(
return EntityStats(countActive: countActive, countArchived: countArchived);
}
int precisionForInvoice(AppState state, InvoiceEntity invoice) {
final client = state.clientState.get(invoice.clientId);
final currency = state.staticState.currencyMap[client.currencyId];
return currency.precision;
}
bool hasInvoiceChanges(
InvoiceEntity invoice, BuiltMap<String, InvoiceEntity> invoiceMap) =>
invoice.isNew ? invoice.isChanged : invoice != invoiceMap[invoice.id];

View File

@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/redux/invoice/invoice_selectors.dart';
import 'package:invoiceninja_flutter/ui/app/app_border.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_item_selector.dart';
@ -136,7 +137,7 @@ class _CreditEditState extends State<CreditEdit>
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'${localization.total}: ${formatNumber(invoice.calculateTotal, context, clientId: viewModel.invoice.clientId)}',
'${localization.total}: ${formatNumber(invoice.calculateTotal(precision: precisionForInvoice(state, invoice)), context, clientId: viewModel.invoice.clientId)}',
style: TextStyle(
//color: Theme.of(context).selectedRowColor,
color: state.prefState.enableDarkMode

View File

@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/redux/invoice/invoice_selectors.dart';
import 'package:invoiceninja_flutter/ui/app/app_border.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_contacts_vm.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_details_vm.dart';
@ -147,7 +148,7 @@ class _InvoiceEditState extends State<InvoiceEdit>
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'${localization.lookup('${invoice.entityType}_total')}: ${formatNumber(invoice.calculateTotal, context, clientId: viewModel.invoice.clientId)}',
'${localization.lookup('${invoice.entityType}_total')}: ${formatNumber(invoice.calculateTotal(precision: precisionForInvoice(state, invoice)), context, clientId: viewModel.invoice.clientId)}',
style: TextStyle(
//color: Theme.of(context).selectedRowColor,
color: state.prefState.enableDarkMode

View File

@ -5,6 +5,7 @@ import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/data/models/company_model.dart';
import 'package:invoiceninja_flutter/data/models/entities.dart';
import 'package:invoiceninja_flutter/data/models/invoice_model.dart';
import 'package:invoiceninja_flutter/redux/invoice/invoice_selectors.dart';
import 'package:invoiceninja_flutter/ui/app/form_card.dart';
import 'package:invoiceninja_flutter/ui/app/forms/app_dropdown_button.dart';
import 'package:invoiceninja_flutter/ui/app/forms/app_tab_bar.dart';
@ -646,8 +647,8 @@ class InvoiceEditDesktopState extends State<InvoiceEditDesktop>
),
textAlign: TextAlign.end,
key: ValueKey(
'__invoice_subtotal_${invoice.subtotal}_${invoice.clientId}__'),
initialValue: formatNumber(invoice.subtotal, context,
'__invoice_subtotal_${invoice.calculateSubtotal(precision: precisionForInvoice(state, invoice))}_${invoice.clientId}__'),
initialValue: formatNumber(invoice.calculateSubtotal(precision: precisionForInvoice(state, invoice)), context,
clientId: invoice.clientId),
),
if (invoice.isOld)
@ -710,9 +711,9 @@ class InvoiceEditDesktopState extends State<InvoiceEditDesktop>
),
textAlign: TextAlign.end,
key: ValueKey(
'__invoice_total_${invoice.calculateTotal}_${invoice.clientId}__'),
'__invoice_total_${invoice.calculateTotal(precision: precisionForInvoice(state, invoice))}_${invoice.clientId}__'),
initialValue: formatNumber(
invoice.calculateTotal - invoice.paidToDate, context,
invoice.calculateTotal(precision: precisionForInvoice(state, invoice)) - invoice.paidToDate, context,
clientId: invoice.clientId),
),
if (invoice.partial != 0)

View File

@ -6,6 +6,7 @@ import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/data/models/quote_model.dart';
import 'package:invoiceninja_flutter/data/models/recurring_invoice_model.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/redux/invoice/invoice_selectors.dart';
import 'package:invoiceninja_flutter/redux/payment/payment_selectors.dart';
import 'package:invoiceninja_flutter/redux/recurring_invoice/recurring_invoice_selectors.dart';
import 'package:invoiceninja_flutter/ui/app/FieldGrid.dart';
@ -340,7 +341,10 @@ class InvoiceOverview extends StatelessWidget {
widgets.addAll([
SizedBox(height: 8),
surchargeRow(localization.subtotal, invoice.calculateTotal),
surchargeRow(
localization.subtotal,
invoice.calculateTotal(
precision: precisionForInvoice(state, invoice))),
surchargeRow(localization.paidToDate, invoice.paidToDate),
]);
@ -357,7 +361,9 @@ class InvoiceOverview extends StatelessWidget {
}
invoice
.calculateTaxes(invoice.usesInclusiveTaxes)
.calculateTaxes(
useInclusiveTaxes: invoice.usesInclusiveTaxes,
precision: precisionForInvoice(state, invoice))
.forEach((taxName, taxAmount) {
widgets.add(surchargeRow(taxName, taxAmount));
});

View File

@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/redux/invoice/invoice_selectors.dart';
import 'package:invoiceninja_flutter/ui/app/app_border.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_vm.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_item_selector.dart';
@ -136,7 +137,7 @@ class _QuoteEditState extends State<QuoteEdit>
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'${localization.total}: ${formatNumber(invoice.calculateTotal, context, clientId: viewModel.invoice.clientId)}',
'${localization.total}: ${formatNumber(invoice.calculateTotal(precision: precisionForInvoice(state, invoice)), context, clientId: viewModel.invoice.clientId)}',
style: TextStyle(
//color: Theme.of(context).selectedRowColor,
color: state.prefState.enableDarkMode

View File

@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/redux/invoice/invoice_selectors.dart';
import 'package:invoiceninja_flutter/ui/app/app_border.dart';
import 'package:invoiceninja_flutter/ui/app/edit_scaffold.dart';
import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_vm.dart';
@ -137,7 +138,7 @@ class _RecurringInvoiceEditState extends State<RecurringInvoiceEdit>
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'${localization.total}: ${formatNumber(widget.viewModel.invoice.calculateTotal, context, clientId: viewModel.invoice.clientId)}',
'${localization.total}: ${formatNumber(widget.viewModel.invoice.calculateTotal(precision: precisionForInvoice(state, invoice)), context, clientId: viewModel.invoice.clientId)}',
style: TextStyle(
//color: Theme.of(context).selectedRowColor,
color: state.prefState.enableDarkMode

View File

@ -76,7 +76,8 @@ ReportResult taxRateReport(
//final invoiceTaxAmount = invoice.calculateTaxes(invoice.usesInclusiveTaxes);
final invoicePaidAmount = invoice.amount - invoice.balance;
final taxes = invoice.getTaxes();
final precision = staticState.currencyMap[client.currencyId].precision;
final taxes = invoice.getTaxes(precision);
for (final key in taxes.keys) {
bool skip = false;
final List<ReportElement> row = [];