From db393aa146b606aad618c08bf74e1987cde3132c Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 26 Nov 2020 15:50:54 +0200 Subject: [PATCH] Add support for variable currency precisions --- lib/data/models/invoice_model.dart | 4 +- lib/data/models/mixins/invoice_mixin.dart | 77 +++++++++++-------- lib/redux/invoice/invoice_selectors.dart | 7 ++ lib/ui/credit/edit/credit_edit.dart | 3 +- lib/ui/invoice/edit/invoice_edit.dart | 3 +- lib/ui/invoice/edit/invoice_edit_desktop.dart | 9 ++- .../invoice/view/invoice_view_overview.dart | 10 ++- lib/ui/quote/quote_edit.dart | 3 +- .../edit/recurring_invoice_edit.dart | 3 +- lib/ui/reports/tax_rate_report.dart | 3 +- 10 files changed, 75 insertions(+), 47 deletions(-) diff --git a/lib/data/models/invoice_model.dart b/lib/data/models/invoice_model.dart index 6249c9b88..8b135f99d 100644 --- a/lib/data/models/invoice_model.dart +++ b/lib/data/models/invoice_model.dart @@ -879,9 +879,9 @@ abstract class InvoiceEntity extends Object } /// Gets taxes in the form { taxName1: { amount: 0, paid: 0} , ... } - Map> getTaxes() { + Map> getTaxes(int precision) { final taxes = >{}; - final taxable = calculateTaxes(usesInclusiveTaxes); + final taxable = calculateTaxes(useInclusiveTaxes: usesInclusiveTaxes, precision: precision); final paidAmount = amount - balance; if (taxRate1 != 0) { diff --git a/lib/data/models/mixins/invoice_mixin.dart b/lib/data/models/mixins/invoice_mixin.dart index fd149e5af..371a58158 100644 --- a/lib/data/models/mixins/invoice_mixin.dart +++ b/lib/data/models/mixins/invoice_mixin.dart @@ -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 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 calculateTaxes(bool useInclusiveTaxes) { - double total = subtotal; + Map calculateTaxes( + {@required bool useInclusiveTaxes, @required int precision}) { + double total = calculateSubtotal(precision: precision); double taxAmount; final map = {}; @@ -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; diff --git a/lib/redux/invoice/invoice_selectors.dart b/lib/redux/invoice/invoice_selectors.dart index 3a1ebc6b5..35a37c6ed 100644 --- a/lib/redux/invoice/invoice_selectors.dart +++ b/lib/redux/invoice/invoice_selectors.dart @@ -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 invoiceMap) => invoice.isNew ? invoice.isChanged : invoice != invoiceMap[invoice.id]; diff --git a/lib/ui/credit/edit/credit_edit.dart b/lib/ui/credit/edit/credit_edit.dart index e94747e96..6dd4ed67b 100644 --- a/lib/ui/credit/edit/credit_edit.dart +++ b/lib/ui/credit/edit/credit_edit.dart @@ -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 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 diff --git a/lib/ui/invoice/edit/invoice_edit.dart b/lib/ui/invoice/edit/invoice_edit.dart index 2708182c9..e7f83e61a 100644 --- a/lib/ui/invoice/edit/invoice_edit.dart +++ b/lib/ui/invoice/edit/invoice_edit.dart @@ -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 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 diff --git a/lib/ui/invoice/edit/invoice_edit_desktop.dart b/lib/ui/invoice/edit/invoice_edit_desktop.dart index b492917e3..f084afcf8 100644 --- a/lib/ui/invoice/edit/invoice_edit_desktop.dart +++ b/lib/ui/invoice/edit/invoice_edit_desktop.dart @@ -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 ), 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 ), 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) diff --git a/lib/ui/invoice/view/invoice_view_overview.dart b/lib/ui/invoice/view/invoice_view_overview.dart index 26850878f..f2814bcac 100644 --- a/lib/ui/invoice/view/invoice_view_overview.dart +++ b/lib/ui/invoice/view/invoice_view_overview.dart @@ -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)); }); diff --git a/lib/ui/quote/quote_edit.dart b/lib/ui/quote/quote_edit.dart index 371d00d4b..31f2bdc77 100644 --- a/lib/ui/quote/quote_edit.dart +++ b/lib/ui/quote/quote_edit.dart @@ -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 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 diff --git a/lib/ui/recurring_invoice/edit/recurring_invoice_edit.dart b/lib/ui/recurring_invoice/edit/recurring_invoice_edit.dart index 0ac6e2c42..ee9361e93 100644 --- a/lib/ui/recurring_invoice/edit/recurring_invoice_edit.dart +++ b/lib/ui/recurring_invoice/edit/recurring_invoice_edit.dart @@ -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 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 diff --git a/lib/ui/reports/tax_rate_report.dart b/lib/ui/reports/tax_rate_report.dart index d3c0b14c4..3bd3ab757 100644 --- a/lib/ui/reports/tax_rate_report.dart +++ b/lib/ui/reports/tax_rate_report.dart @@ -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 row = [];