diff --git a/lib/constants.dart b/lib/constants.dart index e11d832fc..a39aad119 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -542,6 +542,7 @@ const String kReportRecurringExpense = 'recurring_expense'; const String kReportRecurringInvoice = 'recurring_invoice'; const String kReportPurchaseOrder = 'purchase_order'; const String kReportVendor = 'vendor'; +const String kReportTransaction = 'transaction'; const String kPdfFieldsClientDetails = 'client_details'; const String kPdfFieldsCompanyDetails = 'company_details'; diff --git a/lib/ui/app/presenters/entity_presenter.dart b/lib/ui/app/presenters/entity_presenter.dart index 30cacf03a..79b24fd61 100644 --- a/lib/ui/app/presenters/entity_presenter.dart +++ b/lib/ui/app/presenters/entity_presenter.dart @@ -152,6 +152,8 @@ class EntityPresenter { 'stock_quantity', 'notification_threshold', 'partial', + 'withdrawal', + 'deposit', ].contains(field); return value; diff --git a/lib/ui/reports/reports_screen.dart b/lib/ui/reports/reports_screen.dart index f7a657620..748e0f324 100644 --- a/lib/ui/reports/reports_screen.dart +++ b/lib/ui/reports/reports_screen.dart @@ -117,6 +117,8 @@ class ReportsScreen extends StatelessWidget { if (state.company.isModuleEnabled(EntityType.purchaseOrder)) kReportPurchaseOrder, ], + if (state.company.isModuleEnabled(EntityType.transaction)) + kReportTransaction, ]..sort((a, b) => a.compareTo(b)); final reportChildren = [ diff --git a/lib/ui/reports/reports_screen_vm.dart b/lib/ui/reports/reports_screen_vm.dart index 8e8cedbb0..f90734e63 100644 --- a/lib/ui/reports/reports_screen_vm.dart +++ b/lib/ui/reports/reports_screen_vm.dart @@ -14,6 +14,7 @@ import 'package:flutter_styled_toast/flutter_styled_toast.dart'; import 'package:invoiceninja_flutter/ui/reports/purchase_order_report.dart'; import 'package:invoiceninja_flutter/ui/reports/recurring_expense_report.dart'; import 'package:invoiceninja_flutter/ui/reports/recurring_invoice_report.dart'; +import 'package:invoiceninja_flutter/ui/reports/transaction_report.dart'; import 'package:invoiceninja_flutter/ui/reports/vendor_report.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; import 'package:memoize/memoize.dart'; @@ -336,6 +337,18 @@ class ReportsScreenVM { state.staticState, ); break; + case kReportTransaction: + reportResult = memoizedTransactionReport( + state.userCompany, + state.uiState.reportsUIState, + state.transactionState.map, + state.vendorState.map, + state.expenseState.map, + state.expenseCategoryState.map, + state.invoiceState.map, + state.bankAccountState.map, + state.staticState, + ); } final groupTotals = memoizeedGroupTotals( diff --git a/lib/ui/reports/transaction_report.dart b/lib/ui/reports/transaction_report.dart new file mode 100644 index 000000000..3b216bce7 --- /dev/null +++ b/lib/ui/reports/transaction_report.dart @@ -0,0 +1,204 @@ +// Package imports: +import 'package:built_collection/built_collection.dart'; +import 'package:invoiceninja_flutter/utils/strings.dart'; +import 'package:memoize/memoize.dart'; + +// Project imports: +import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/reports/reports_state.dart'; +import 'package:invoiceninja_flutter/redux/static/static_state.dart'; +import 'package:invoiceninja_flutter/ui/reports/reports_screen.dart'; +import 'package:invoiceninja_flutter/utils/enums.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; + +enum TransactionReportFields { + id, + amount, + date, + description, + deposit, + withdrawal, + vendor, + category, + bankAccount, + invoices, + expenseNumber, + status, + accountType, + defaultCategory, + created_at, + updated_at, +} + +var memoizedTransactionReport = memo9(( + UserCompanyEntity userCompany, + ReportsUIState reportsUIState, + BuiltMap transactionMap, + BuiltMap vendorMap, + BuiltMap expenseMap, + BuiltMap categoryMap, + BuiltMap invoiceMap, + BuiltMap bankAccountMap, + StaticState staticState, +) => + transactionReport( + userCompany, + reportsUIState, + transactionMap, + vendorMap, + expenseMap, + categoryMap, + invoiceMap, + bankAccountMap, + staticState, + )); + +ReportResult transactionReport( + UserCompanyEntity userCompany, + ReportsUIState reportsUIState, + BuiltMap transactionMap, + BuiltMap vendorMap, + BuiltMap expenseMap, + BuiltMap categoryMap, + BuiltMap invoiceMap, + BuiltMap bankAccountMap, + StaticState staticState, +) { + final List> data = []; + final List entities = []; + BuiltList columns; + + final reportSettings = userCompany.settings?.reportSettings; + final transactionReportSettings = + reportSettings != null && reportSettings.containsKey(kReportTransaction) + ? reportSettings[kReportTransaction] + : ReportSettingsEntity(); + + final defaultColumns = [ + TransactionReportFields.status, + TransactionReportFields.deposit, + TransactionReportFields.withdrawal, + TransactionReportFields.date, + TransactionReportFields.description, + TransactionReportFields.invoices, + TransactionReportFields.expenseNumber, + ]; + + if (transactionReportSettings.columns.isNotEmpty) { + columns = BuiltList(transactionReportSettings.columns + .map((e) => EnumUtils.fromString(TransactionReportFields.values, e)) + .where((element) => element != null) + .toList()); + } else { + columns = BuiltList(defaultColumns); + } + + for (var transactionId in transactionMap.keys) { + final transaction = transactionMap[transactionId]; + + if (transaction.isDeleted) { + continue; + } + + bool skip = false; + final List row = []; + + for (var column in columns) { + dynamic value = ''; + + switch (column) { + case TransactionReportFields.id: + value = transaction.id; + break; + case TransactionReportFields.accountType: + value = toTitleCase( + bankAccountMap[transaction.bankAccountId]?.type ?? ''); + break; + case TransactionReportFields.amount: + value = transaction.amount; + break; + case TransactionReportFields.withdrawal: + value = transaction.withdrawal; + break; + case TransactionReportFields.deposit: + value = transaction.deposit; + break; + case TransactionReportFields.category: + value = categoryMap[transaction.categoryId]?.name ?? ''; + break; + case TransactionReportFields.date: + value = transaction.date; + break; + case TransactionReportFields.description: + value = transaction.description; + break; + case TransactionReportFields.vendor: + value = vendorMap[transaction.vendorId]?.name ?? ''; + break; + case TransactionReportFields.status: + value = kTransactionStatuses[transaction.statusId]; + break; + case TransactionReportFields.bankAccount: + value = bankAccountMap[transaction.bankAccountId]?.name ?? ''; + break; + case TransactionReportFields.defaultCategory: + value = transaction.category; + break; + case TransactionReportFields.expenseNumber: + value = expenseMap[transaction.expenseId]?.number ?? ''; + break; + case TransactionReportFields.invoices: + value = transaction.invoiceIds + .split(',') + .map((invoiceId) => invoiceMap[invoiceId]?.number ?? '') + .where((number) => number.isNotEmpty) + .join(', '); + break; + case TransactionReportFields.updated_at: + value = convertTimestampToDateString(transaction.updatedAt); + break; + case TransactionReportFields.created_at: + value = convertTimestampToDateString(transaction.createdAt); + break; + } + + if (!ReportResult.matchField( + value: value, + userCompany: userCompany, + reportsUIState: reportsUIState, + column: EnumUtils.parse(column), + )) { + skip = true; + } + + if (value.runtimeType == bool) { + row.add(transaction.getReportBool(value: value)); + } else if (value.runtimeType == double || value.runtimeType == int) { + row.add(transaction.getReportDouble( + value: value, currencyId: transaction.currencyId)); + } else { + row.add(transaction.getReportString(value: value)); + } + } + + if (!skip) { + data.add(row); + entities.add(transaction); + } + } + + final selectedColumns = columns.map((item) => EnumUtils.parse(item)).toList(); + data.sort((rowA, rowB) => sortReportTableRows( + rowA, rowB, transactionReportSettings, selectedColumns)); + + return ReportResult( + allColumns: + TransactionReportFields.values.map((e) => EnumUtils.parse(e)).toList(), + columns: selectedColumns, + defaultColumns: + defaultColumns.map((item) => EnumUtils.parse(item)).toList(), + data: data, + entities: entities, + ); +}