From db8b515761cb3bc7474d9a71ba9f14515aec9b26 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 4 Feb 2024 18:54:48 +0200 Subject: [PATCH 01/30] iOS build fix --- ios/Podfile.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1dc8b7fd3b1..2ec5280cb99 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -54,7 +54,7 @@ PODS: - GTMAppAuth (2.0.0): - AppAuth/Core (~> 1.6) - GTMSessionFetcher/Core (< 4.0, >= 1.5) - - GTMSessionFetcher/Core (3.2.0) + - GTMSessionFetcher/Core (3.3.1) - image_cropper (0.0.4): - Flutter - TOCropViewController (~> 2.6.1) @@ -78,9 +78,9 @@ PODS: - Flutter - printing (1.0.0): - Flutter - - SDWebImage (5.18.7): - - SDWebImage/Core (= 5.18.7) - - SDWebImage/Core (5.18.7) + - SDWebImage (5.18.10): + - SDWebImage/Core (= 5.18.10) + - SDWebImage/Core (5.18.10) - Sentry/HybridSDK (8.15.2): - SentryPrivate (= 8.15.2) - sentry_flutter (0.0.1): @@ -204,7 +204,7 @@ SPEC CHECKSUMS: google_sign_in_ios: 8115e3fbe097e6509beb819ed602d47369d9011f GoogleSignIn: b232380cf495a429b8095d3178a8d5855b42e842 GTMAppAuth: 99fb010047ba3973b7026e45393f51f27ab965ae - GTMSessionFetcher: 41b9ef0b4c08a6db4b7eb51a21ae5183ec99a2c8 + GTMSessionFetcher: 8a1b34ad97ebe6f909fb8b9b77fba99943007556 image_cropper: a3291c624a953049bc6a02e1f8c8ceb162a24b25 image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 in_app_purchase_storekit: 4fb7ee9e824b1f09107fbfbbce8c4b276366dc43 @@ -215,7 +215,7 @@ SPEC CHECKSUMS: path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 printing: 233e1b73bd1f4a05615548e9b5a324c98588640b - SDWebImage: f9258c58221ed854cfa0e2b80ee4033710b1c6d3 + SDWebImage: fc8f2d48bbfd72ef39d70e981bd24a3f3be53fec Sentry: 6f5742b4c47c17c9adcf265f6f328cf4a0ed1923 sentry_flutter: 2c309a1d4b45e59d02cfa15795705687f1e2081b SentryPrivate: b2f7996f37781080f04a946eb4e377ff63c64195 From 32afc403d94396fccbc594af748d8cc51f141d50 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 4 Feb 2024 18:54:55 +0200 Subject: [PATCH 02/30] macOS build fix --- macos/Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index f389c9da026..7630377e229 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -136,4 +136,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 8d40c19d3cbdb380d870685c3a564c989f1efa52 -COCOAPODS: 1.11.3 +COCOAPODS: 1.14.3 From d7695f359103b42794e2e25bb7c72931b18b1196 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 4 Feb 2024 20:29:38 +0200 Subject: [PATCH 03/30] macOS build fix --- ios/Podfile.lock | 14 +++++++------- macos/Podfile.lock | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2ec5280cb99..322e3e9d27e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -81,13 +81,13 @@ PODS: - SDWebImage (5.18.10): - SDWebImage/Core (= 5.18.10) - SDWebImage/Core (5.18.10) - - Sentry/HybridSDK (8.15.2): - - SentryPrivate (= 8.15.2) + - Sentry/HybridSDK (8.18.0): + - SentryPrivate (= 8.18.0) - sentry_flutter (0.0.1): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.15.2) - - SentryPrivate (8.15.2) + - Sentry/HybridSDK (= 8.18.0) + - SentryPrivate (8.18.0) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -216,9 +216,9 @@ SPEC CHECKSUMS: permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 printing: 233e1b73bd1f4a05615548e9b5a324c98588640b SDWebImage: fc8f2d48bbfd72ef39d70e981bd24a3f3be53fec - Sentry: 6f5742b4c47c17c9adcf265f6f328cf4a0ed1923 - sentry_flutter: 2c309a1d4b45e59d02cfa15795705687f1e2081b - SentryPrivate: b2f7996f37781080f04a946eb4e377ff63c64195 + Sentry: 8984a4ffb2b9bd2894d74fb36e6f5833865bc18e + sentry_flutter: c87a0556eeb6cbf7f9f924d30e878bdedf22d364 + SentryPrivate: 2f0c9ba4c3fc993f70eab6ca95673509561e0085 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440 diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 7630377e229..96fd011302d 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -22,13 +22,13 @@ PODS: - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS - - Sentry/HybridSDK (8.15.2): - - SentryPrivate (= 8.15.2) + - Sentry/HybridSDK (8.18.0): + - SentryPrivate (= 8.18.0) - sentry_flutter (0.0.1): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.15.2) - - SentryPrivate (8.15.2) + - Sentry/HybridSDK (= 8.18.0) + - SentryPrivate (8.18.0) - share_plus (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): @@ -123,9 +123,9 @@ SPEC CHECKSUMS: path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 printing: 1dd6a1fce2209ec240698e2439a4adbb9b427637 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - Sentry: 6f5742b4c47c17c9adcf265f6f328cf4a0ed1923 - sentry_flutter: 2c309a1d4b45e59d02cfa15795705687f1e2081b - SentryPrivate: b2f7996f37781080f04a946eb4e377ff63c64195 + Sentry: 8984a4ffb2b9bd2894d74fb36e6f5833865bc18e + sentry_flutter: c87a0556eeb6cbf7f9f924d30e878bdedf22d364 + SentryPrivate: 2f0c9ba4c3fc993f70eab6ca95673509561e0085 share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sign_in_with_apple: a9e97e744e8edc36aefc2723111f652102a7a727 From 8584bf964924942bf9f49ff2932eb2e9c3d19382 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 5 Feb 2024 15:28:31 +0200 Subject: [PATCH 04/30] Transactions still showing newlines #625 --- lib/data/models/transaction_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/data/models/transaction_model.dart b/lib/data/models/transaction_model.dart index 2d78aac2afa..189828a4d34 100644 --- a/lib/data/models/transaction_model.dart +++ b/lib/data/models/transaction_model.dart @@ -193,7 +193,7 @@ abstract class TransactionEntity extends Object bool get isConverted => statusId == kTransactionStatusConverted; - String get formattedDescription => description.replaceAll('\n', ' '); + String get formattedDescription => description.replaceAll('\\n', ' '); @override List getActions( From 24fdc51b9b65c1de9f8847f63cf2f67f278928f0 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 5 Feb 2024 16:16:00 +0200 Subject: [PATCH 05/30] Code cleanup --- lib/ui/invoice/edit/invoice_edit_items_desktop.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/ui/invoice/edit/invoice_edit_items_desktop.dart b/lib/ui/invoice/edit/invoice_edit_items_desktop.dart index 597d13d076d..945a88cb054 100644 --- a/lib/ui/invoice/edit/invoice_edit_items_desktop.dart +++ b/lib/ui/invoice/edit/invoice_edit_items_desktop.dart @@ -766,7 +766,6 @@ class _InvoiceEditItemsDesktopState extends State { VoidCallback onFieldSubmitted) { return DecoratedFormField( showClear: false, - autofocus: false, controller: textEditingController, keyboardType: TextInputType.text, focusNode: focusNode, From 5a385442378968421bbdafcc631de1c131d6d651 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 5 Feb 2024 16:16:25 +0200 Subject: [PATCH 06/30] Return default client language --- lib/data/models/client_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/data/models/client_model.dart b/lib/data/models/client_model.dart index 91d2025346b..e8657a86bc1 100644 --- a/lib/data/models/client_model.dart +++ b/lib/data/models/client_model.dart @@ -764,7 +764,7 @@ abstract class ClientEntity extends Object bool get hasCurrency => settings.currencyId != null && settings.currencyId!.isNotEmpty; - String? get languageId => settings.languageId; + String get languageId => settings.languageId ?? kLanguageEnglish; ClientContactEntity getContact(String? contactId) => contacts.firstWhere((contact) => contact.id == contactId, From 1935a81b663531f7e7baf1215beb1d24222cef50 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 5 Feb 2024 16:18:04 +0200 Subject: [PATCH 07/30] Return default client language --- lib/ui/client/view/client_view_fullwidth.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/ui/client/view/client_view_fullwidth.dart b/lib/ui/client/view/client_view_fullwidth.dart index c9f9e5f1e9b..774e485a5b0 100644 --- a/lib/ui/client/view/client_view_fullwidth.dart +++ b/lib/ui/client/view/client_view_fullwidth.dart @@ -185,8 +185,7 @@ class _ClientViewFullwidthState extends State overflow: TextOverflow.ellipsis, ), ), - if ((client.languageId ?? '').isNotEmpty && - client.languageId != state.company.languageId) + if (client.languageId != state.company.languageId) Padding( padding: const EdgeInsets.only(bottom: 1), child: Text( From 0d9da6c4607016ff6c5acc5ab500cd45df2742de Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 5 Feb 2024 16:18:47 +0200 Subject: [PATCH 08/30] =?UTF-8?q?Use=20client=E2=80=99s=20language=20to=20?= =?UTF-8?q?name=20downloaded=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/redux/credit/credit_actions.dart | 10 +++++++--- lib/utils/files.dart | 18 +++++++++++++++++- lib/utils/i18n.dart | 9 +++++---- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/lib/redux/credit/credit_actions.dart b/lib/redux/credit/credit_actions.dart index 21bd57a864f..fe3f7580bae 100644 --- a/lib/redux/credit/credit_actions.dart +++ b/lib/redux/credit/credit_actions.dart @@ -634,8 +634,12 @@ Future handleCreditAction(BuildContext context, List credits, .get(credit.invitationDownloadLink, state.token, rawResponse: true) .then((response) { store.dispatch(StopLoading()); - saveDownloadedFile(response.bodyBytes, - localization!.credit + '_' + credit.number + '.pdf'); + saveDownloadedFile( + response.bodyBytes, + credit.number + '.pdf', + prefix: EntityType.credit.name, + languageId: client.languageId, + ); }).catchError((_) { store.dispatch(StopLoading()); }); @@ -694,7 +698,7 @@ Future handleCreditAction(BuildContext context, List credits, break; case EntityAction.bulkPrint: store.dispatch(StartSaving()); - final url = state.credentials.url+ '/credits/bulk'; + final url = state.credentials.url + '/credits/bulk'; final data = json.encode( {'ids': creditIds, 'action': EntityAction.bulkPrint.toApiParam()}); final http.Response? response = await WebClient() diff --git a/lib/utils/files.dart b/lib/utils/files.dart index b3e1f64b00f..9800444ea6b 100644 --- a/lib/utils/files.dart +++ b/lib/utils/files.dart @@ -11,6 +11,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; import 'package:http/http.dart'; +import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/main_app.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/utils/dialogs.dart'; @@ -101,7 +102,22 @@ Future?> _pickFiles({ return null; } -void saveDownloadedFile(Uint8List data, String fileName) async { +void saveDownloadedFile( + Uint8List data, + String fileName, { + String? prefix, + String languageId = kLanguageEnglish, +}) async { + if (prefix != null) { + final localization = AppLocalization.of(navigatorKey.currentContext!)!; + final store = StoreProvider.of(navigatorKey.currentContext!); + final localeCode = store.state.staticState.languageMap[languageId]!.locale; + + fileName = localization.lookup(prefix, overrideLocaleCode: localeCode) + + '_' + + fileName; + } + if (kIsWeb) { WebUtils.downloadBinaryFile(fileName, data); } else { diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 2e1554ff5f9..1b822257732 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -114859,7 +114859,7 @@ mixin LocalizationsProvider on LocaleCodeAware { // STARTER: lang field - do not remove comment - String lookup(String? key) { + String lookup(String? key, {String? overrideLocaleCode}) { final lookupKey = toSnakeCase(key); if ((key ?? '').isEmpty) { @@ -114870,9 +114870,10 @@ mixin LocalizationsProvider on LocaleCodeAware { return key ?? ''; } - final value = _localizedValues[localeCode]![lookupKey] ?? - _localizedValues[localeCode]![lookupKey.replaceFirst('_id', '')] ?? - ''; + final value = + _localizedValues[overrideLocaleCode ?? localeCode]![lookupKey] ?? + _localizedValues[localeCode]![lookupKey.replaceFirst('_id', '')] ?? + ''; if (value.isEmpty) { print('## ERROR: localization key not found - $key'); From 6e1f62ffa24a5fa3fb4f4a0630fd7b81b527e9a3 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 5 Feb 2024 16:31:09 +0200 Subject: [PATCH 09/30] =?UTF-8?q?Use=20client=E2=80=99s=20language=20to=20?= =?UTF-8?q?name=20downloaded=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/redux/invoice/invoice_actions.dart | 16 ++++++++++++---- .../purchase_order/purchase_order_actions.dart | 11 ++++++++--- lib/redux/quote/quote_actions.dart | 11 ++++++++--- lib/ui/client/client_pdf.dart | 12 +++++++----- lib/ui/invoice/invoice_pdf.dart | 18 ++++++++++-------- 5 files changed, 45 insertions(+), 23 deletions(-) diff --git a/lib/redux/invoice/invoice_actions.dart b/lib/redux/invoice/invoice_actions.dart index e07c762c6a9..afc3ac68d3e 100644 --- a/lib/redux/invoice/invoice_actions.dart +++ b/lib/redux/invoice/invoice_actions.dart @@ -782,8 +782,12 @@ void handleInvoiceAction(BuildContext? context, List invoices, .get(invoice.invitationDownloadLink, state.token, rawResponse: true) .then((response) { store.dispatch(StopLoading()); - saveDownloadedFile(response.bodyBytes, - localization!.invoice + '_' + invoice.number + '.pdf'); + saveDownloadedFile( + response.bodyBytes, + invoice.number + '.pdf', + prefix: EntityType.invoice.apiValue, + languageId: client.languageId, + ); }).catchError((_) { store.dispatch(StopLoading()); }); @@ -795,8 +799,12 @@ void handleInvoiceAction(BuildContext? context, List invoices, rawResponse: true) .then((response) { store.dispatch(StopLoading()); - saveDownloadedFile(response.bodyBytes, - localization!.invoice + '_' + invoice.number + '.xml'); + saveDownloadedFile( + response.bodyBytes, + invoice.number + '.xml', + prefix: EntityType.invoice.apiValue, + languageId: client.languageId, + ); }).catchError((_) { store.dispatch(StopLoading()); }); diff --git a/lib/redux/purchase_order/purchase_order_actions.dart b/lib/redux/purchase_order/purchase_order_actions.dart index 39799ea2b5c..5ed51c69b08 100644 --- a/lib/redux/purchase_order/purchase_order_actions.dart +++ b/lib/redux/purchase_order/purchase_order_actions.dart @@ -597,6 +597,7 @@ void handlePurchaseOrderAction(BuildContext? context, final purchaseOrder = purchaseOrders.first as InvoiceEntity; final purchaseOrderIds = purchaseOrders.map((purchaseOrder) => purchaseOrder.id).toList(); + final vendor = state.vendorState.get(purchaseOrder.vendorId); switch (action) { case EntityAction.edit: @@ -632,7 +633,7 @@ void handlePurchaseOrderAction(BuildContext? context, break; case EntityAction.bulkPrint: store.dispatch(StartSaving()); - final url = state.credentials.url+ '/purchase_orders/bulk'; + final url = state.credentials.url + '/purchase_orders/bulk'; final data = json.encode({ 'ids': purchaseOrderIds, 'action': EntityAction.bulkPrint.toApiParam() @@ -833,8 +834,12 @@ void handlePurchaseOrderAction(BuildContext? context, rawResponse: true) .then((response) { store.dispatch(StopLoading()); - saveDownloadedFile(response.bodyBytes, - localization!.purchaseOrder + '_' + purchaseOrder.number + '.pdf'); + saveDownloadedFile( + response.bodyBytes, + purchaseOrder.number + '.pdf', + prefix: EntityType.purchaseOrder.apiValue, + languageId: vendor.languageId, + ); }).catchError((_) { store.dispatch(StopLoading()); }); diff --git a/lib/redux/quote/quote_actions.dart b/lib/redux/quote/quote_actions.dart index 60ecf01eac8..960aa33b28b 100644 --- a/lib/redux/quote/quote_actions.dart +++ b/lib/redux/quote/quote_actions.dart @@ -524,6 +524,7 @@ Future handleQuoteAction( final localization = AppLocalization.of(context); final quote = quotes.first as InvoiceEntity; final quoteIds = quotes.map((quote) => quote.id).toList(); + final client = state.clientState.get(quote.clientId); switch (action) { case EntityAction.edit: @@ -687,8 +688,12 @@ Future handleQuoteAction( .get(quote.invitationDownloadLink, state.token, rawResponse: true) .then((response) { store.dispatch(StopLoading()); - saveDownloadedFile(response.bodyBytes, - localization!.quote + '_' + quote.number + '.pdf'); + saveDownloadedFile( + response.bodyBytes, + quote.number + '.pdf', + prefix: EntityType.quote.apiValue, + languageId: client.languageId, + ); }).catchError((_) { store.dispatch(StopLoading()); }); @@ -747,7 +752,7 @@ Future handleQuoteAction( break; case EntityAction.bulkPrint: store.dispatch(StartSaving()); - final url = state.credentials.url+ '/quotes/bulk'; + final url = state.credentials.url + '/quotes/bulk'; final data = json.encode( {'ids': quoteIds, 'action': EntityAction.bulkPrint.toApiParam()}); final http.Response? response = await WebClient() diff --git a/lib/ui/client/client_pdf.dart b/lib/ui/client/client_pdf.dart index c9282fb2252..6410dbfc31b 100644 --- a/lib/ui/client/client_pdf.dart +++ b/lib/ui/client/client_pdf.dart @@ -320,11 +320,13 @@ class _ClientPdfViewState extends State { onPressed: _response == null ? null : () async { - final fileName = localization.statement + - '_' + - (client.number) + - '.pdf'; - saveDownloadedFile(_response!.bodyBytes, fileName); + final fileName = client.number + '.pdf'; + saveDownloadedFile( + _response!.bodyBytes, + fileName, + prefix: 'statement', + languageId: client.languageId, + ); }, ), AppTextButton( diff --git a/lib/ui/invoice/invoice_pdf.dart b/lib/ui/invoice/invoice_pdf.dart index ad811469f29..f34b84fc417 100644 --- a/lib/ui/invoice/invoice_pdf.dart +++ b/lib/ui/invoice/invoice_pdf.dart @@ -239,14 +239,16 @@ class _InvoicePdfViewState extends State { onPressed: _response == null ? null : () async { - final fileName = - localization.lookup('${invoice.entityType}') + - '_' + - (invoice.number.isEmpty - ? localization.pending - : invoice.number) + - '.pdf'; - saveDownloadedFile(_response!.bodyBytes, fileName); + final fileName = (invoice.number.isEmpty + ? localization.pending + : invoice.number) + + '.pdf'; + saveDownloadedFile( + _response!.bodyBytes, + fileName, + prefix: invoice.entityType!.apiValue, + languageId: client.languageId, + ); }, ), if (isDesktop(context)) From f66fab6d083b1b47040bf0e3e014aafbc2b77012 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 7 Feb 2024 17:25:24 +0200 Subject: [PATCH 10/30] Code cleanup --- lib/redux/credit/credit_actions.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/redux/credit/credit_actions.dart b/lib/redux/credit/credit_actions.dart index fe3f7580bae..d1f5fec57a4 100644 --- a/lib/redux/credit/credit_actions.dart +++ b/lib/redux/credit/credit_actions.dart @@ -637,7 +637,7 @@ Future handleCreditAction(BuildContext context, List credits, saveDownloadedFile( response.bodyBytes, credit.number + '.pdf', - prefix: EntityType.credit.name, + prefix: EntityType.credit.apiValue, languageId: client.languageId, ); }).catchError((_) { From 73123a7c94daf5d36450df514d6fc4c7b8c0a123 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 7 Feb 2024 17:25:33 +0200 Subject: [PATCH 11/30] Support importing quotes --- lib/data/models/import_model.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/data/models/import_model.dart b/lib/data/models/import_model.dart index ee42b40ee97..a52f66a83fd 100644 --- a/lib/data/models/import_model.dart +++ b/lib/data/models/import_model.dart @@ -149,6 +149,7 @@ class ImportType extends EnumClass { EntityType.invoice.apiValue: 'invoices', EntityType.recurringInvoice.apiValue: 'recurring_invoices', EntityType.payment.apiValue: 'payments', + EntityType.quote.apiValue: 'quotes', EntityType.task.apiValue: 'tasks', EntityType.vendor.apiValue: 'vendors', EntityType.expense.apiValue: 'expenses', From 336c02948d8cfde1ed0c665542309fba7c0ad5ab Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 7 Feb 2024 17:28:11 +0200 Subject: [PATCH 12/30] Incorrect Frequency in Recurring Expenses Report (Issue #628) --- lib/ui/reports/recurring_expense_report.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ui/reports/recurring_expense_report.dart b/lib/ui/reports/recurring_expense_report.dart index acb37b2eb7b..4cac95bdac2 100644 --- a/lib/ui/reports/recurring_expense_report.dart +++ b/lib/ui/reports/recurring_expense_report.dart @@ -218,7 +218,7 @@ ReportResult recurringExpenseReport( value = expense.privateNotes; break; case RecurringExpenseReportFields.frequency: - value = localization!.lookup(kFrequencies[invoice.frequencyId]); + value = localization!.lookup(kFrequencies[expense.frequencyId]); break; case RecurringExpenseReportFields.start_date: value = invoice.nextSendDate; From 0045a4f01a86977610b877a51992d537965742e8 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 7 Feb 2024 17:29:30 +0200 Subject: [PATCH 13/30] Update version --- .github/workflows/flatpak.yml | 2 +- flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml | 1 + lib/constants.dart | 2 +- pubspec.foss.yaml | 2 +- pubspec.yaml | 2 +- snap/snapcraft.yaml | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 463b105ea65..89662ece1f7 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -86,7 +86,7 @@ jobs: draft: false prerelease: false title: "Latest Release" - automatic_release_tag: "v5.0.154" + automatic_release_tag: "v5.0.155" files: | ${{ github.workspace }}/artifacts/Invoice-Ninja-Archive ${{ github.workspace }}/artifacts/Invoice-Ninja-Hash diff --git a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml index 895ac4ffc22..3038d086c0c 100644 --- a/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml +++ b/flatpak/com.invoiceninja.InvoiceNinja.metainfo.xml @@ -50,6 +50,7 @@ + diff --git a/lib/constants.dart b/lib/constants.dart index 998c7f6d1a2..71f76f4be1c 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -6,7 +6,7 @@ class Constants { } // TODO remove version once #46609 is fixed -const String kClientVersion = '5.0.154'; +const String kClientVersion = '5.0.155'; const String kMinServerVersion = '5.0.4'; const String kAppName = 'Invoice Ninja'; diff --git a/pubspec.foss.yaml b/pubspec.foss.yaml index 204cd5b27e1..92307bd80f4 100644 --- a/pubspec.foss.yaml +++ b/pubspec.foss.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.154+154 +version: 5.0.155+155 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/pubspec.yaml b/pubspec.yaml index cc5a0a21797..b32515f19a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: invoiceninja_flutter description: Client for Invoice Ninja -version: 5.0.154+154 +version: 5.0.155+155 homepage: https://invoiceninja.com documentation: https://invoiceninja.github.io publish_to: none diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 3050492c7c7..1df84c537ce 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: invoiceninja -version: '5.0.154' +version: '5.0.155' summary: Create invoices, accept payments, track expenses & time tasks description: "### Note: if the app fails to run using `snap run invoiceninja` it may help to run `/snap/invoiceninja/current/bin/invoiceninja` instead From a25538722010cfe554d4ba28cb249c9148736379 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 7 Feb 2024 18:54:26 +0200 Subject: [PATCH 14/30] Recurring expense report fixes --- lib/ui/reports/recurring_expense_report.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/ui/reports/recurring_expense_report.dart b/lib/ui/reports/recurring_expense_report.dart index 4cac95bdac2..3ff4f25b1de 100644 --- a/lib/ui/reports/recurring_expense_report.dart +++ b/lib/ui/reports/recurring_expense_report.dart @@ -117,7 +117,6 @@ ReportResult recurringExpenseReport( for (var expenseId in expenseMap.keys) { final expense = expenseMap[expenseId]!; final client = clientMap[expense.clientId] ?? ClientEntity(); - final invoice = invoiceMap[expense.invoiceId] ?? InvoiceEntity(); final vendor = vendorMap[expense.vendorId] ?? VendorEntity(); if (expense.isDeleted! && !userCompany.company.reportIncludeDeleted) { @@ -221,12 +220,12 @@ ReportResult recurringExpenseReport( value = localization!.lookup(kFrequencies[expense.frequencyId]); break; case RecurringExpenseReportFields.start_date: - value = invoice.nextSendDate; + value = expense.nextSendDate; break; case RecurringExpenseReportFields.remaining_cycles: - value = invoice.remainingCycles == -1 + value = expense.remainingCycles == -1 ? localization!.endless - : '${invoice.remainingCycles}'; + : '${expense.remainingCycles}'; break; case RecurringExpenseReportFields.record_state: value = AppLocalization.of(navigatorKey.currentContext!)! From 58c087947472f62c9c6012692d3d718dc639998b Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 13 Feb 2024 12:45:10 +0200 Subject: [PATCH 15/30] Fix gateway_type in payment columns selector --- lib/utils/i18n.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 1b822257732..360f8ca40d3 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -18,6 +18,7 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'gateway_type': 'Gateway Type', 'please_select_an_invoice_or_credit': 'Please select an invoice or credit', 'mobile_version': 'Mobile Version', From 2d55e196861f6e6d52a3edcf9dc8e9ccfa7f3a16 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 13 Feb 2024 12:58:23 +0200 Subject: [PATCH 16/30] Attachments Feature on Payments #623 --- lib/ui/payment/view/payment_view.dart | 296 ++++++++++++++++---------- 1 file changed, 181 insertions(+), 115 deletions(-) diff --git a/lib/ui/payment/view/payment_view.dart b/lib/ui/payment/view/payment_view.dart index 021639e5a4a..309d0da7ba5 100644 --- a/lib/ui/payment/view/payment_view.dart +++ b/lib/ui/payment/view/payment_view.dart @@ -4,6 +4,9 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja_flutter/redux/invoice/invoice_selectors.dart'; +import 'package:invoiceninja_flutter/redux/payment/payment_actions.dart'; +import 'package:invoiceninja_flutter/ui/payment/view/payment_view_documents.dart'; +import 'package:invoiceninja_flutter/ui/payment/view/payment_view_overview.dart'; import 'package:url_launcher/url_launcher.dart'; // Project imports: @@ -27,16 +30,58 @@ class PaymentView extends StatefulWidget { Key? key, required this.viewModel, required this.isFilter, + required this.tabIndex, }) : super(key: key); final PaymentViewVM viewModel; final bool isFilter; + final int tabIndex; @override _PaymentViewState createState() => new _PaymentViewState(); } -class _PaymentViewState extends State { +class _PaymentViewState extends State + with SingleTickerProviderStateMixin { + TabController? _controller; + + @override + void initState() { + super.initState(); + + final state = widget.viewModel.state; + _controller = TabController( + vsync: this, + length: 2, + initialIndex: widget.isFilter ? 0 : state.paymentUIState.tabIndex); + _controller!.addListener(_onTabChanged); + } + + void _onTabChanged() { + if (widget.isFilter) { + return; + } + + final store = StoreProvider.of(context); + store.dispatch(UpdatePaymentTab(tabIndex: _controller!.index)); + } + + @override + void didUpdateWidget(oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.tabIndex != widget.tabIndex) { + _controller!.index = widget.tabIndex; + } + } + + @override + void dispose() { + _controller!.removeListener(_onTabChanged); + _controller!.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final viewModel = widget.viewModel; @@ -46,6 +91,7 @@ class _PaymentViewState extends State { ClientEntity(id: payment.clientId); final transaction = state.transactionState.get(payment.transactionId); final localization = AppLocalization.of(context); + final company = state.company; final companyGateway = state.companyGatewayState.get(payment.companyGatewayId); @@ -87,124 +133,144 @@ class _PaymentViewState extends State { entity: payment, body: Builder( builder: (BuildContext context) { - return RefreshIndicator( - onRefresh: () => viewModel.onRefreshed(context), - child: Column( - children: [ - Expanded( - child: ScrollableListView( - children: [ - EntityHeader( - entity: payment, - statusColor: - PaymentStatusColors(state.prefState.colorThemeModel) - .colors[payment.statusId], - statusLabel: localization! - .lookup('payment_status_${payment.statusId}'), - label: localization.amount, - value: formatNumber( - payment.amount - payment.refunded, context, - clientId: client.id), - secondLabel: localization.applied, - secondValue: formatNumber(payment.applied, context, - clientId: client.id), - ), - ListDivider(), - EntityListTile( - isFilter: widget.isFilter, - entity: client, - subtitle: client - .getContact( - invoice.invitations.first.clientContactId) - .emailOrFullName, - ), - for (final paymentable in payment.invoicePaymentables) - EntityListTile( - isFilter: widget.isFilter, - entity: - state.invoiceState.map[paymentable.invoiceId]!, - subtitle: formatNumber(paymentable.amount, context, - clientId: payment.clientId)! + - ' • ' + - formatDate( - convertTimestampToDateString( - paymentable.createdAt), - context), - ), - for (final paymentable in payment.creditPaymentables) - EntityListTile( - isFilter: widget.isFilter, - entity: state.creditState.map[paymentable.creditId]!, - subtitle: formatNumber(paymentable.amount, context, - clientId: payment.clientId)! + - ' • ' + - formatDate( - convertTimestampToDateString( - paymentable.createdAt), - context), - ), - if (payment.companyGatewayId.isNotEmpty) ...[ - ListTile( - title: Text( - '${localization.gateway} › ${companyGateway.label}'), - onTap: companyGatewayLink != null - ? () => launchUrl(Uri.parse(companyGatewayLink)) - : null, - leading: IgnorePointer( - child: IconButton( - icon: Icon(Icons.payment), - onPressed: () => null, - ), + final paymentView = ScrollableListView( + children: [ + EntityHeader( + entity: payment, + statusColor: + PaymentStatusColors(state.prefState.colorThemeModel) + .colors[payment.statusId], + statusLabel: + localization!.lookup('payment_status_${payment.statusId}'), + label: localization.amount, + value: formatNumber(payment.amount - payment.refunded, context, + clientId: client.id), + secondLabel: localization.applied, + secondValue: + formatNumber(payment.applied, context, clientId: client.id), + ), + ListDivider(), + EntityListTile( + isFilter: widget.isFilter, + entity: client, + subtitle: client + .getContact(invoice.invitations.first.clientContactId) + .emailOrFullName, + ), + for (final paymentable in payment.invoicePaymentables) + EntityListTile( + isFilter: widget.isFilter, + entity: state.invoiceState.map[paymentable.invoiceId]!, + subtitle: formatNumber(paymentable.amount, context, + clientId: payment.clientId)! + + ' • ' + + formatDate( + convertTimestampToDateString(paymentable.createdAt), + context), + ), + for (final paymentable in payment.creditPaymentables) + EntityListTile( + isFilter: widget.isFilter, + entity: state.creditState.map[paymentable.creditId]!, + subtitle: formatNumber(paymentable.amount, context, + clientId: payment.clientId)! + + ' • ' + + formatDate( + convertTimestampToDateString(paymentable.createdAt), + context), + ), + if (payment.companyGatewayId.isNotEmpty) ...[ + ListTile( + title: Text( + '${localization.gateway} › ${companyGateway.label}'), + onTap: companyGatewayLink != null + ? () => launchUrl(Uri.parse(companyGatewayLink)) + : null, + leading: IgnorePointer( + child: IconButton( + icon: Icon(Icons.payment), + onPressed: () => null, + ), + ), + trailing: companyGatewayLink != null + ? IgnorePointer( + child: IconButton( + icon: Icon(Icons.open_in_new), + onPressed: () => null, ), - trailing: companyGatewayLink != null - ? IgnorePointer( - child: IconButton( - icon: Icon(Icons.open_in_new), - onPressed: () => null, - ), - ) - : null, + ) + : null, + ), + ListDivider(), + ], + if (payment.transactionId.isNotEmpty) + EntityListTile( + isFilter: widget.isFilter, + entity: transaction, + ), + payment.privateNotes.isNotEmpty + ? Column( + children: [ + IconMessage(payment.privateNotes, + copyToClipboard: true), + Container( + color: Theme.of(context).cardColor, + height: 12.0, ), - ListDivider(), ], - if (payment.transactionId.isNotEmpty) - EntityListTile( - isFilter: widget.isFilter, - entity: transaction, + ) + : Container(), + FieldGrid(fields), + ], + ); + + return Column( + children: [ + Expanded( + child: company.isModuleEnabled(EntityType.document) + ? TabBarView( + controller: _controller, + children: [ + RefreshIndicator( + onRefresh: () => viewModel.onRefreshed(context), + child: PaymentOverview( + viewModel: viewModel, + key: ValueKey(viewModel.payment.id), + ), + ), + RefreshIndicator( + onRefresh: () => viewModel.onRefreshed(context), + child: PaymentViewDocuments( + viewModel: viewModel, + key: ValueKey(viewModel.payment.id), + ), + ), + ], + ) + : RefreshIndicator( + onRefresh: () => viewModel.onRefreshed(context), + child: PaymentOverview( + viewModel: viewModel, + key: ValueKey(viewModel.payment.id), ), - payment.privateNotes.isNotEmpty - ? Column( - children: [ - IconMessage(payment.privateNotes, - copyToClipboard: true), - Container( - color: Theme.of(context).cardColor, - height: 12.0, - ), - ], - ) - : Container(), - FieldGrid(fields), - ], - ), - ), - BottomButtons( - entity: payment, - action1: state.company.enableApplyingPayments - ? EntityAction.applyPayment - : EntityAction.sendEmail, - action1Enabled: state.company.enableApplyingPayments - ? payment.applied < payment.amount && - memoizedHasActiveUnpaidInvoices( - payment.clientId, - state.invoiceState.map, - ) - : true, - action2: EntityAction.refundPayment, - action2Enabled: payment.refunded < payment.amount, - ), - ], - ), + ), + ), + BottomButtons( + entity: payment, + action1: state.company.enableApplyingPayments + ? EntityAction.applyPayment + : EntityAction.sendEmail, + action1Enabled: state.company.enableApplyingPayments + ? payment.applied < payment.amount && + memoizedHasActiveUnpaidInvoices( + payment.clientId, + state.invoiceState.map, + ) + : true, + action2: EntityAction.refundPayment, + action2Enabled: payment.refunded < payment.amount, + ), + ], ); }, ), From 562184f41cf495c5e1694a081a57fcd8b457b65c Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 13 Feb 2024 13:03:02 +0200 Subject: [PATCH 17/30] Attachments Feature on Payments #623 --- lib/ui/payment/view/payment_view.dart | 113 -------------- .../payment/view/payment_view_documents.dart | 30 ++++ .../payment/view/payment_view_overview.dart | 144 ++++++++++++++++++ 3 files changed, 174 insertions(+), 113 deletions(-) create mode 100644 lib/ui/payment/view/payment_view_documents.dart create mode 100644 lib/ui/payment/view/payment_view_overview.dart diff --git a/lib/ui/payment/view/payment_view.dart b/lib/ui/payment/view/payment_view.dart index 309d0da7ba5..6a616597a4d 100644 --- a/lib/ui/payment/view/payment_view.dart +++ b/lib/ui/payment/view/payment_view.dart @@ -106,124 +106,11 @@ class _PaymentViewState extends State invoice = state.invoiceState.get(invoiceId!); } - final fields = {}; - /* - fields[PaymentFields.paymentStatusId] = - localization.lookup('payment_status_${payment.statusId}'); - */ - if (payment.date.isNotEmpty) { - fields[PaymentFields.date] = formatDate(payment.date, context); - } - if (payment.typeId.isNotEmpty) { - final paymentType = state.staticState.paymentTypeMap[payment.typeId]; - if (paymentType != null) { - fields[PaymentFields.typeId] = paymentType.name; - } - } - if (payment.transactionReference.isNotEmpty) { - fields[PaymentFields.transactionReference] = payment.transactionReference; - } - if (payment.refunded != 0) { - fields[PaymentFields.refunded] = - formatNumber(payment.refunded, context, clientId: client.id); - } - return ViewScaffold( isFilter: widget.isFilter, entity: payment, body: Builder( builder: (BuildContext context) { - final paymentView = ScrollableListView( - children: [ - EntityHeader( - entity: payment, - statusColor: - PaymentStatusColors(state.prefState.colorThemeModel) - .colors[payment.statusId], - statusLabel: - localization!.lookup('payment_status_${payment.statusId}'), - label: localization.amount, - value: formatNumber(payment.amount - payment.refunded, context, - clientId: client.id), - secondLabel: localization.applied, - secondValue: - formatNumber(payment.applied, context, clientId: client.id), - ), - ListDivider(), - EntityListTile( - isFilter: widget.isFilter, - entity: client, - subtitle: client - .getContact(invoice.invitations.first.clientContactId) - .emailOrFullName, - ), - for (final paymentable in payment.invoicePaymentables) - EntityListTile( - isFilter: widget.isFilter, - entity: state.invoiceState.map[paymentable.invoiceId]!, - subtitle: formatNumber(paymentable.amount, context, - clientId: payment.clientId)! + - ' • ' + - formatDate( - convertTimestampToDateString(paymentable.createdAt), - context), - ), - for (final paymentable in payment.creditPaymentables) - EntityListTile( - isFilter: widget.isFilter, - entity: state.creditState.map[paymentable.creditId]!, - subtitle: formatNumber(paymentable.amount, context, - clientId: payment.clientId)! + - ' • ' + - formatDate( - convertTimestampToDateString(paymentable.createdAt), - context), - ), - if (payment.companyGatewayId.isNotEmpty) ...[ - ListTile( - title: Text( - '${localization.gateway} › ${companyGateway.label}'), - onTap: companyGatewayLink != null - ? () => launchUrl(Uri.parse(companyGatewayLink)) - : null, - leading: IgnorePointer( - child: IconButton( - icon: Icon(Icons.payment), - onPressed: () => null, - ), - ), - trailing: companyGatewayLink != null - ? IgnorePointer( - child: IconButton( - icon: Icon(Icons.open_in_new), - onPressed: () => null, - ), - ) - : null, - ), - ListDivider(), - ], - if (payment.transactionId.isNotEmpty) - EntityListTile( - isFilter: widget.isFilter, - entity: transaction, - ), - payment.privateNotes.isNotEmpty - ? Column( - children: [ - IconMessage(payment.privateNotes, - copyToClipboard: true), - Container( - color: Theme.of(context).cardColor, - height: 12.0, - ), - ], - ) - : Container(), - FieldGrid(fields), - ], - ); - return Column( children: [ Expanded( diff --git a/lib/ui/payment/view/payment_view_documents.dart b/lib/ui/payment/view/payment_view_documents.dart new file mode 100644 index 00000000000..977413326ed --- /dev/null +++ b/lib/ui/payment/view/payment_view_documents.dart @@ -0,0 +1,30 @@ +// Flutter imports: +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/payment/payment_actions.dart'; + +// Project imports: +import 'package:invoiceninja_flutter/ui/app/document_grid.dart'; +import 'package:invoiceninja_flutter/ui/app/screen_imports.dart'; + +class PaymentViewDocuments extends StatelessWidget { + const PaymentViewDocuments({Key? key, required this.viewModel}) + : super(key: key); + + final PaymentViewVM viewModel; + + @override + Widget build(BuildContext context) { + final store = StoreProvider.of(context); + final payment = viewModel.payment; + + return DocumentGrid( + documents: payment.documents.toList(), + onUploadDocument: (path, isPrivate) => + viewModel.onUploadDocuments(context, path, isPrivate), + onRenamedDocument: () => + store.dispatch(LoadPayment(paymentId: payment.id)), + ); + } +} diff --git a/lib/ui/payment/view/payment_view_overview.dart b/lib/ui/payment/view/payment_view_overview.dart new file mode 100644 index 00000000000..7d2439c63c7 --- /dev/null +++ b/lib/ui/payment/view/payment_view_overview.dart @@ -0,0 +1,144 @@ +// Flutter imports: +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/constants.dart'; + +// Project imports: +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/app/FieldGrid.dart'; +import 'package:invoiceninja_flutter/ui/app/entity_header.dart'; +import 'package:invoiceninja_flutter/ui/app/lists/list_divider.dart'; +import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart'; +import 'package:invoiceninja_flutter/ui/payment/view/payment_view_vm.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; + +class PaymentOverview extends StatefulWidget { + const PaymentOverview({ + Key? key, + required this.viewModel, + }) : super(key: key); + + final PaymentViewVM viewModel; + + @override + _PaymentOverviewState createState() => _PaymentOverviewState(); +} + +class _PaymentOverviewState extends State { + @override + Widget build(BuildContext context) { + final localization = AppLocalization.of(context)!; + final viewModel = widget.viewModel; + final state = viewModel.state; + final payment = viewModel.payment; + final company = viewModel.company!; + + final fields = {}; + /* + fields[PaymentFields.paymentStatusId] = + localization.lookup('payment_status_${payment.statusId}'); + */ + if (payment.date.isNotEmpty) { + fields[PaymentFields.date] = formatDate(payment.date, context); + } + if (payment.typeId.isNotEmpty) { + final paymentType = state.staticState.paymentTypeMap[payment.typeId]; + if (paymentType != null) { + fields[PaymentFields.typeId] = paymentType.name; + } + } + if (payment.transactionReference.isNotEmpty) { + fields[PaymentFields.transactionReference] = payment.transactionReference; + } + if (payment.refunded != 0) { + fields[PaymentFields.refunded] = + formatNumber(payment.refunded, context, clientId: client.id); + } + + return ScrollableListView( + children: [ + EntityHeader( + entity: payment, + statusColor: PaymentStatusColors(state.prefState.colorThemeModel) + .colors[payment.statusId], + statusLabel: + localization!.lookup('payment_status_${payment.statusId}'), + label: localization.amount, + value: formatNumber(payment.amount - payment.refunded, context, + clientId: client.id), + secondLabel: localization.applied, + secondValue: + formatNumber(payment.applied, context, clientId: client.id), + ), + ListDivider(), + EntityListTile( + isFilter: widget.isFilter, + entity: client, + subtitle: client + .getContact(invoice.invitations.first.clientContactId) + .emailOrFullName, + ), + for (final paymentable in payment.invoicePaymentables) + EntityListTile( + isFilter: widget.isFilter, + entity: state.invoiceState.map[paymentable.invoiceId]!, + subtitle: formatNumber(paymentable.amount, context, + clientId: payment.clientId)! + + ' • ' + + formatDate(convertTimestampToDateString(paymentable.createdAt), + context), + ), + for (final paymentable in payment.creditPaymentables) + EntityListTile( + isFilter: widget.isFilter, + entity: state.creditState.map[paymentable.creditId]!, + subtitle: formatNumber(paymentable.amount, context, + clientId: payment.clientId)! + + ' • ' + + formatDate(convertTimestampToDateString(paymentable.createdAt), + context), + ), + if (payment.companyGatewayId.isNotEmpty) ...[ + ListTile( + title: Text('${localization.gateway} › ${companyGateway.label}'), + onTap: companyGatewayLink != null + ? () => launchUrl(Uri.parse(companyGatewayLink)) + : null, + leading: IgnorePointer( + child: IconButton( + icon: Icon(Icons.payment), + onPressed: () => null, + ), + ), + trailing: companyGatewayLink != null + ? IgnorePointer( + child: IconButton( + icon: Icon(Icons.open_in_new), + onPressed: () => null, + ), + ) + : null, + ), + ListDivider(), + ], + if (payment.transactionId.isNotEmpty) + EntityListTile( + isFilter: widget.isFilter, + entity: transaction, + ), + payment.privateNotes.isNotEmpty + ? Column( + children: [ + IconMessage(payment.privateNotes, copyToClipboard: true), + Container( + color: Theme.of(context).cardColor, + height: 12.0, + ), + ], + ) + : Container(), + FieldGrid(fields), + ], + ); + } +} From a000117601ba50facb3f98220a2a4aba312cb6e1 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 13 Feb 2024 13:13:46 +0200 Subject: [PATCH 18/30] Attachments Feature on Payments #623 --- lib/data/models/payment_model.dart | 12 ++++++++ lib/data/models/payment_model.g.dart | 28 +++++++++++++++++++ lib/data/models/serializers.g.dart | 3 ++ lib/ui/payment/view/payment_view.dart | 27 ------------------ .../payment/view/payment_view_overview.dart | 23 +++++++++++++-- 5 files changed, 64 insertions(+), 29 deletions(-) diff --git a/lib/data/models/payment_model.dart b/lib/data/models/payment_model.dart index fe786bb5db0..a052370d8c1 100644 --- a/lib/data/models/payment_model.dart +++ b/lib/data/models/payment_model.dart @@ -76,6 +76,7 @@ class PaymentFields { static const String customValue2 = 'custom2'; static const String customValue3 = 'custom3'; static const String customValue4 = 'custom4'; + static const String documents = 'documents'; } abstract class PaymentEntity extends Object @@ -124,6 +125,7 @@ abstract class PaymentEntity extends Object invitationId: '', isApplying: false, gatewayTypeId: '', + documents: BuiltList(), ); } @@ -229,6 +231,8 @@ abstract class PaymentEntity extends Object BuiltList get credits; + BuiltList get documents; + bool get canBeAppliedOrRefunded => [ kPaymentStatusCompleted, kPaymentStatusPartiallyRefunded, @@ -353,6 +357,10 @@ abstract class PaymentEntity extends Object response = stateA.name.toLowerCase().compareTo(stateB.name.toLowerCase()); break; + case PaymentFields.documents: + response = + paymentA!.documents.length.compareTo(paymentB!.documents.length); + break; default: print('## ERROR: sort by payment.$sortField is not implemented'); break; @@ -451,6 +459,10 @@ abstract class PaymentEntity extends Object } } + if (!isDeleted! && multiselect) { + actions.add(EntityAction.documents); + } + if (actions.isNotEmpty && actions.last != null) { actions.add(null); } diff --git a/lib/data/models/payment_model.g.dart b/lib/data/models/payment_model.g.dart index 21946336f07..c442e10e841 100644 --- a/lib/data/models/payment_model.g.dart +++ b/lib/data/models/payment_model.g.dart @@ -206,6 +206,10 @@ class _$PaymentEntitySerializer implements StructuredSerializer { serializers.serialize(object.credits, specifiedType: const FullType( BuiltList, const [const FullType(PaymentableEntity)])), + 'documents', + serializers.serialize(object.documents, + specifiedType: const FullType( + BuiltList, const [const FullType(DocumentEntity)])), 'created_at', serializers.serialize(object.createdAt, specifiedType: const FullType(int)), @@ -424,6 +428,12 @@ class _$PaymentEntitySerializer implements StructuredSerializer { BuiltList, const [const FullType(PaymentableEntity)]))! as BuiltList); break; + case 'documents': + result.documents.replace(serializers.deserialize(value, + specifiedType: const FullType( + BuiltList, const [const FullType(DocumentEntity)]))! + as BuiltList); + break; case 'isChanged': result.isChanged = serializers.deserialize(value, specifiedType: const FullType(bool)) as bool?; @@ -816,6 +826,8 @@ class _$PaymentEntity extends PaymentEntity { @override final BuiltList credits; @override + final BuiltList documents; + @override final bool? isChanged; @override final int createdAt; @@ -868,6 +880,7 @@ class _$PaymentEntity extends PaymentEntity { required this.paymentables, required this.invoices, required this.credits, + required this.documents, this.isChanged, required this.createdAt, required this.updatedAt, @@ -927,6 +940,8 @@ class _$PaymentEntity extends PaymentEntity { BuiltValueNullFieldError.checkNotNull( invoices, r'PaymentEntity', 'invoices'); BuiltValueNullFieldError.checkNotNull(credits, r'PaymentEntity', 'credits'); + BuiltValueNullFieldError.checkNotNull( + documents, r'PaymentEntity', 'documents'); BuiltValueNullFieldError.checkNotNull( createdAt, r'PaymentEntity', 'createdAt'); BuiltValueNullFieldError.checkNotNull( @@ -979,6 +994,7 @@ class _$PaymentEntity extends PaymentEntity { paymentables == other.paymentables && invoices == other.invoices && credits == other.credits && + documents == other.documents && isChanged == other.isChanged && createdAt == other.createdAt && updatedAt == other.updatedAt && @@ -1026,6 +1042,7 @@ class _$PaymentEntity extends PaymentEntity { _$hash = $jc(_$hash, paymentables.hashCode); _$hash = $jc(_$hash, invoices.hashCode); _$hash = $jc(_$hash, credits.hashCode); + _$hash = $jc(_$hash, documents.hashCode); _$hash = $jc(_$hash, isChanged.hashCode); _$hash = $jc(_$hash, createdAt.hashCode); _$hash = $jc(_$hash, updatedAt.hashCode); @@ -1073,6 +1090,7 @@ class _$PaymentEntity extends PaymentEntity { ..add('paymentables', paymentables) ..add('invoices', invoices) ..add('credits', credits) + ..add('documents', documents) ..add('isChanged', isChanged) ..add('createdAt', createdAt) ..add('updatedAt', updatedAt) @@ -1231,6 +1249,12 @@ class PaymentEntityBuilder set credits(ListBuilder? credits) => _$this._credits = credits; + ListBuilder? _documents; + ListBuilder get documents => + _$this._documents ??= new ListBuilder(); + set documents(ListBuilder? documents) => + _$this._documents = documents; + bool? _isChanged; bool? get isChanged => _$this._isChanged; set isChanged(bool? isChanged) => _$this._isChanged = isChanged; @@ -1304,6 +1328,7 @@ class PaymentEntityBuilder _paymentables = $v.paymentables.toBuilder(); _invoices = $v.invoices.toBuilder(); _credits = $v.credits.toBuilder(); + _documents = $v.documents.toBuilder(); _isChanged = $v.isChanged; _createdAt = $v.createdAt; _updatedAt = $v.updatedAt; @@ -1377,6 +1402,7 @@ class PaymentEntityBuilder paymentables: paymentables.build(), invoices: invoices.build(), credits: credits.build(), + documents: documents.build(), isChanged: isChanged, createdAt: BuiltValueNullFieldError.checkNotNull(createdAt, r'PaymentEntity', 'createdAt'), updatedAt: BuiltValueNullFieldError.checkNotNull(updatedAt, r'PaymentEntity', 'updatedAt'), @@ -1394,6 +1420,8 @@ class PaymentEntityBuilder invoices.build(); _$failedField = 'credits'; credits.build(); + _$failedField = 'documents'; + documents.build(); } catch (e) { throw new BuiltValueNestedFieldError( r'PaymentEntity', _$failedField, e.toString()); diff --git a/lib/data/models/serializers.g.dart b/lib/data/models/serializers.g.dart index 41f8be57e94..5eb4e02e4e9 100644 --- a/lib/data/models/serializers.g.dart +++ b/lib/data/models/serializers.g.dart @@ -543,6 +543,9 @@ Serializers _$serializers = (new Serializers().toBuilder() ..addBuilderFactory( const FullType(BuiltList, const [const FullType(PaymentableEntity)]), () => new ListBuilder()) + ..addBuilderFactory( + const FullType(BuiltList, const [const FullType(DocumentEntity)]), + () => new ListBuilder()) ..addBuilderFactory( const FullType(BuiltList, const [const FullType(ProductEntity)]), () => new ListBuilder()) diff --git a/lib/ui/payment/view/payment_view.dart b/lib/ui/payment/view/payment_view.dart index 6a616597a4d..dd8c0e9a938 100644 --- a/lib/ui/payment/view/payment_view.dart +++ b/lib/ui/payment/view/payment_view.dart @@ -7,23 +7,13 @@ import 'package:invoiceninja_flutter/redux/invoice/invoice_selectors.dart'; import 'package:invoiceninja_flutter/redux/payment/payment_actions.dart'; import 'package:invoiceninja_flutter/ui/payment/view/payment_view_documents.dart'; import 'package:invoiceninja_flutter/ui/payment/view/payment_view_overview.dart'; -import 'package:url_launcher/url_launcher.dart'; // Project imports: -import 'package:invoiceninja_flutter/colors.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; -import 'package:invoiceninja_flutter/ui/app/FieldGrid.dart'; import 'package:invoiceninja_flutter/ui/app/buttons/bottom_buttons.dart'; -import 'package:invoiceninja_flutter/ui/app/entities/entity_list_tile.dart'; -import 'package:invoiceninja_flutter/ui/app/entity_header.dart'; -import 'package:invoiceninja_flutter/ui/app/icon_message.dart'; -import 'package:invoiceninja_flutter/ui/app/lists/list_divider.dart'; -import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart'; import 'package:invoiceninja_flutter/ui/app/view_scaffold.dart'; import 'package:invoiceninja_flutter/ui/payment/view/payment_view_vm.dart'; -import 'package:invoiceninja_flutter/utils/formatting.dart'; -import 'package:invoiceninja_flutter/utils/localization.dart'; class PaymentView extends StatefulWidget { const PaymentView({ @@ -87,25 +77,8 @@ class _PaymentViewState extends State final viewModel = widget.viewModel; final payment = viewModel.payment; final state = StoreProvider.of(context).state; - final client = state.clientState.map[payment.clientId] ?? - ClientEntity(id: payment.clientId); - final transaction = state.transactionState.get(payment.transactionId); - final localization = AppLocalization.of(context); final company = state.company; - final companyGateway = - state.companyGatewayState.get(payment.companyGatewayId); - final companyGatewayLink = GatewayEntity.getPaymentUrl( - gatewayId: companyGateway.gatewayId, - transactionReference: payment.transactionReference, - ); - - var invoice = InvoiceEntity(client: client); - if (payment.invoicePaymentables.isNotEmpty) { - final invoiceId = payment.invoicePaymentables.first.invoiceId; - invoice = state.invoiceState.get(invoiceId!); - } - return ViewScaffold( isFilter: widget.isFilter, entity: payment, diff --git a/lib/ui/payment/view/payment_view_overview.dart b/lib/ui/payment/view/payment_view_overview.dart index 7d2439c63c7..4fba46712fc 100644 --- a/lib/ui/payment/view/payment_view_overview.dart +++ b/lib/ui/payment/view/payment_view_overview.dart @@ -1,6 +1,5 @@ // Flutter imports: import 'package:flutter/material.dart'; -import 'package:invoiceninja_flutter/constants.dart'; // Project imports: import 'package:invoiceninja_flutter/data/models/models.dart'; @@ -11,6 +10,10 @@ import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart'; import 'package:invoiceninja_flutter/ui/payment/view/payment_view_vm.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:invoiceninja_flutter/ui/app/entities/entity_list_tile.dart'; +import 'package:invoiceninja_flutter/colors.dart'; +import 'package:invoiceninja_flutter/ui/app/icon_message.dart'; +import 'package:url_launcher/url_launcher.dart'; class PaymentOverview extends StatefulWidget { const PaymentOverview({ @@ -31,7 +34,23 @@ class _PaymentOverviewState extends State { final viewModel = widget.viewModel; final state = viewModel.state; final payment = viewModel.payment; - final company = viewModel.company!; + + final client = state.clientState.map[payment.clientId] ?? + ClientEntity(id: payment.clientId); + final transaction = state.transactionState.get(payment.transactionId); + + final companyGateway = + state.companyGatewayState.get(payment.companyGatewayId); + final companyGatewayLink = GatewayEntity.getPaymentUrl( + gatewayId: companyGateway.gatewayId, + transactionReference: payment.transactionReference, + ); + + var invoice = InvoiceEntity(client: client); + if (payment.invoicePaymentables.isNotEmpty) { + final invoiceId = payment.invoicePaymentables.first.invoiceId; + invoice = state.invoiceState.get(invoiceId!); + } final fields = {}; /* From 4c2f74c36aa1b342d06c33afe2fd892bdffc7f74 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 13 Feb 2024 13:16:30 +0200 Subject: [PATCH 19/30] Attachments Feature on Payments #623 --- lib/redux/payment/payment_actions.dart | 27 ++++++++++++++++++++++++ lib/ui/payment/view/payment_view_vm.dart | 26 +++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/lib/redux/payment/payment_actions.dart b/lib/redux/payment/payment_actions.dart index 28b9ef05be8..ef0c622154e 100644 --- a/lib/redux/payment/payment_actions.dart +++ b/lib/redux/payment/payment_actions.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:built_collection/built_collection.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:http/http.dart'; // Project imports: import 'package:invoiceninja_flutter/data/models/models.dart'; @@ -315,6 +316,32 @@ class RemoveFromPaymentMultiselect { class ClearPaymentMultiselect {} +class SavePaymentDocumentRequest implements StartSaving { + SavePaymentDocumentRequest({ + required this.completer, + required this.multipartFiles, + required this.payment, + required this.isPrivate, + }); + + final Completer completer; + final List multipartFiles; + final PaymentEntity payment; + final bool isPrivate; +} + +class SavePaymentDocumentSuccess implements StopSaving, PersistData, PersistUI { + SavePaymentDocumentSuccess(this.document); + + final DocumentEntity document; +} + +class SavePaymentDocumentFailure implements StopSaving { + SavePaymentDocumentFailure(this.error); + + final Object error; +} + class UpdatePaymentTab implements PersistUI { UpdatePaymentTab({this.tabIndex}); diff --git a/lib/ui/payment/view/payment_view_vm.dart b/lib/ui/payment/view/payment_view_vm.dart index 0642a4a8d02..75609617e84 100644 --- a/lib/ui/payment/view/payment_view_vm.dart +++ b/lib/ui/payment/view/payment_view_vm.dart @@ -1,8 +1,13 @@ // Flutter imports: +import 'dart:async'; + import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_redux/flutter_redux.dart'; +import 'package:flutter_styled_toast/flutter_styled_toast.dart'; +import 'package:http/http.dart'; +import 'package:invoiceninja_flutter/ui/app/dialogs/error_dialog.dart'; import 'package:redux/redux.dart'; // Project imports: @@ -48,6 +53,7 @@ class PaymentViewVM { required this.isSaving, required this.isLoading, required this.isDirty, + required this.onUploadDocuments, }); factory PaymentViewVM.fromStore(Store store) { @@ -75,6 +81,25 @@ class PaymentViewVM { onRefreshed: (context) => _handleRefresh(context), onEntityAction: (BuildContext context, EntityAction action) => handleEntitiesActions([payment], action, autoPop: true), + onUploadDocuments: (BuildContext context, + List multipartFile, bool isPrivate) { + final completer = Completer>(); + store.dispatch(SavePaymentDocumentRequest( + isPrivate: isPrivate, + multipartFiles: multipartFile, + payment: payment, + completer: completer, + )); + completer.future.then((client) { + showToast(AppLocalization.of(context)!.uploadedDocument); + }).catchError((Object error) { + showDialog( + context: context, + builder: (BuildContext context) { + return ErrorDialog(error); + }); + }); + }, ); } @@ -83,6 +108,7 @@ class PaymentViewVM { final CompanyEntity? company; final Function(BuildContext, EntityAction) onEntityAction; final Function(BuildContext) onRefreshed; + final Function(BuildContext, List, bool) onUploadDocuments; final bool isSaving; final bool isLoading; final bool isDirty; From 82ecd189c7083fa1ecac3694507702638f9fefbd Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 13 Feb 2024 13:18:20 +0200 Subject: [PATCH 20/30] Attachments Feature on Payments #623 --- lib/ui/payment/view/payment_view.dart | 2 ++ lib/ui/payment/view/payment_view_overview.dart | 4 +++- lib/ui/payment/view/payment_view_vm.dart | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/ui/payment/view/payment_view.dart b/lib/ui/payment/view/payment_view.dart index dd8c0e9a938..2dcbf66ec41 100644 --- a/lib/ui/payment/view/payment_view.dart +++ b/lib/ui/payment/view/payment_view.dart @@ -96,6 +96,7 @@ class _PaymentViewState extends State child: PaymentOverview( viewModel: viewModel, key: ValueKey(viewModel.payment.id), + isFilter: widget.isFilter, ), ), RefreshIndicator( @@ -112,6 +113,7 @@ class _PaymentViewState extends State child: PaymentOverview( viewModel: viewModel, key: ValueKey(viewModel.payment.id), + isFilter: widget.isFilter, ), ), ), diff --git a/lib/ui/payment/view/payment_view_overview.dart b/lib/ui/payment/view/payment_view_overview.dart index 4fba46712fc..20ebee53910 100644 --- a/lib/ui/payment/view/payment_view_overview.dart +++ b/lib/ui/payment/view/payment_view_overview.dart @@ -19,9 +19,11 @@ class PaymentOverview extends StatefulWidget { const PaymentOverview({ Key? key, required this.viewModel, + required this.isFilter, }) : super(key: key); final PaymentViewVM viewModel; + final bool isFilter; @override _PaymentOverviewState createState() => _PaymentOverviewState(); @@ -81,7 +83,7 @@ class _PaymentOverviewState extends State { statusColor: PaymentStatusColors(state.prefState.colorThemeModel) .colors[payment.statusId], statusLabel: - localization!.lookup('payment_status_${payment.statusId}'), + localization.lookup('payment_status_${payment.statusId}'), label: localization.amount, value: formatNumber(payment.amount - payment.refunded, context, clientId: client.id), diff --git a/lib/ui/payment/view/payment_view_vm.dart b/lib/ui/payment/view/payment_view_vm.dart index 75609617e84..ca6471e5abe 100644 --- a/lib/ui/payment/view/payment_view_vm.dart +++ b/lib/ui/payment/view/payment_view_vm.dart @@ -37,6 +37,7 @@ class PaymentViewScreen extends StatelessWidget { return PaymentView( viewModel: vm, isFilter: isFilter, + tabIndex: vm.state.paymentUIState.tabIndex, ); }, ); From 0aa2c43de18960c77d1dd8cb91f5502c14e55244 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 13 Feb 2024 13:24:46 +0200 Subject: [PATCH 21/30] Attachments Feature on Payments --- lib/data/repositories/payment_repository.dart | 27 +++++++++++-- lib/redux/payment/payment_middleware.dart | 40 +++++++++++++++++++ lib/ui/payment/view/payment_view.dart | 19 +++++++++ 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/lib/data/repositories/payment_repository.dart b/lib/data/repositories/payment_repository.dart index 9461245af98..1f5ddbb15d7 100644 --- a/lib/data/repositories/payment_repository.dart +++ b/lib/data/repositories/payment_repository.dart @@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart'; // Package imports: import 'package:built_collection/built_collection.dart'; +import 'package:http/http.dart'; // Project imports: import 'package:invoiceninja_flutter/constants.dart'; @@ -38,7 +39,7 @@ class PaymentRepository { Future> loadList(Credentials credentials, int page, int createdAt, bool filterDeleted) async { - String url = credentials.url+ + String url = credentials.url + '/payments?per_page=$kMaxRecordsPerPage&page=$page&created_at=$createdAt'; if (filterDeleted) { @@ -61,7 +62,7 @@ class PaymentRepository { } final url = - credentials.url+ '/payments/bulk?per_page=$kMaxEntitiesPerBulkAction'; + credentials.url + '/payments/bulk?per_page=$kMaxEntitiesPerBulkAction'; final dynamic response = await webClient.post(url, credentials.token, data: json.encode({'ids': ids, 'action': action.toApiParam()})); @@ -100,7 +101,7 @@ class PaymentRepository { final data = serializers.serializeWith(PaymentEntity.serializer, payment); dynamic response; - var url = credentials.url+ '/payments/refund?'; + var url = credentials.url + '/payments/refund?'; if (payment.sendEmail == true) { url += '&email_receipt=true'; } @@ -115,4 +116,24 @@ class PaymentRepository { return paymentResponse.data; } + + Future uploadDocument( + Credentials credentials, + BaseEntity entity, + List multipartFiles, + bool isPrivate) async { + final fields = { + '_method': 'put', + 'is_public': isPrivate ? '0' : '1', + }; + + final dynamic response = await webClient.post( + '${credentials.url}/payments/${entity.id}/upload', credentials.token, + data: fields, multipartFiles: multipartFiles); + + final PaymentItemResponse paymentItemResponse = + serializers.deserializeWith(PaymentItemResponse.serializer, response)!; + + return paymentItemResponse.data; + } } diff --git a/lib/redux/payment/payment_middleware.dart b/lib/redux/payment/payment_middleware.dart index bde3d08efda..c082e88701d 100644 --- a/lib/redux/payment/payment_middleware.dart +++ b/lib/redux/payment/payment_middleware.dart @@ -1,6 +1,7 @@ // Flutter imports: import 'package:flutter/material.dart'; import 'package:invoiceninja_flutter/constants.dart'; +import 'package:invoiceninja_flutter/redux/document/document_actions.dart'; import 'package:invoiceninja_flutter/redux/quote/quote_actions.dart'; // Package imports: @@ -34,6 +35,7 @@ List> createStorePaymentsMiddleware([ final deletePayment = _deletePayment(repository); final restorePayment = _restorePayment(repository); final emailPayment = _emailPayment(repository); + final saveDocument = _saveDocument(repository); return [ TypedMiddleware(viewPaymentList), @@ -48,6 +50,7 @@ List> createStorePaymentsMiddleware([ TypedMiddleware(deletePayment), TypedMiddleware(restorePayment), TypedMiddleware(emailPayment), + TypedMiddleware(saveDocument), ]; } @@ -320,3 +323,40 @@ Middleware _loadPayments(PaymentRepository repository) { next(action); }; } + +Middleware _saveDocument(PaymentRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as SavePaymentDocumentRequest?; + if (store.state.isEnterprisePlan) { + repository + .uploadDocument( + store.state.credentials, + action!.payment, + action.multipartFiles, + action.isPrivate, + ) + .then((product) { + store.dispatch(SavePaymentSuccess(product)); + + final documents = []; + product.documents.forEach((document) { + documents.add(document.rebuild((b) => b + ..parentId = product.id + ..parentType = EntityType.product)); + }); + store.dispatch(LoadDocumentsSuccess(documents)); + action.completer.complete(documents); + }).catchError((Object error) { + print(error); + store.dispatch(SavePaymentDocumentFailure(error)); + action.completer.completeError(error); + }); + } else { + const error = 'Uploading documents requires an enterprise plan'; + store.dispatch(SavePaymentDocumentFailure(error)); + action!.completer.completeError(error); + } + + next(action); + }; +} diff --git a/lib/ui/payment/view/payment_view.dart b/lib/ui/payment/view/payment_view.dart index 2dcbf66ec41..de42aff0790 100644 --- a/lib/ui/payment/view/payment_view.dart +++ b/lib/ui/payment/view/payment_view.dart @@ -14,6 +14,7 @@ import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/ui/app/buttons/bottom_buttons.dart'; import 'package:invoiceninja_flutter/ui/app/view_scaffold.dart'; import 'package:invoiceninja_flutter/ui/payment/view/payment_view_vm.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; class PaymentView extends StatefulWidget { const PaymentView({ @@ -74,14 +75,32 @@ class _PaymentViewState extends State @override Widget build(BuildContext context) { + final localization = AppLocalization.of(context); final viewModel = widget.viewModel; final payment = viewModel.payment; final state = StoreProvider.of(context).state; final company = state.company; + final documents = payment.documents; return ViewScaffold( isFilter: widget.isFilter, entity: payment, + appBarBottom: company.isModuleEnabled(EntityType.document) + ? TabBar( + controller: _controller, + isScrollable: false, + tabs: [ + Tab( + text: localization!.overview, + ), + Tab( + text: documents.isEmpty + ? localization.documents + : '${localization.documents} (${documents.length})', + ), + ], + ) + : null, body: Builder( builder: (BuildContext context) { return Column( From 9fc014c05950ac18f7967aa665692ecfe151a7c0 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 13 Feb 2024 13:28:02 +0200 Subject: [PATCH 22/30] Attachments Feature on Payments --- lib/redux/document/document_reducer.dart | 8 ++++++++ lib/redux/payment/payment_middleware.dart | 10 +++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/redux/document/document_reducer.dart b/lib/redux/document/document_reducer.dart index 3e54a7e077e..3e0218920c5 100644 --- a/lib/redux/document/document_reducer.dart +++ b/lib/redux/document/document_reducer.dart @@ -364,6 +364,14 @@ DocumentState _setLoadedCompany( }); }); + company.payments.forEach((payment) { + payment.documents.forEach((document) { + documents.add(document.rebuild((b) => b + ..parentId = payment.id + ..parentType = EntityType.payment)); + }); + }); + final state = documentState.rebuild((b) => b ..map.addAll(Map.fromIterable( documents, diff --git a/lib/redux/payment/payment_middleware.dart b/lib/redux/payment/payment_middleware.dart index c082e88701d..27709310736 100644 --- a/lib/redux/payment/payment_middleware.dart +++ b/lib/redux/payment/payment_middleware.dart @@ -335,14 +335,14 @@ Middleware _saveDocument(PaymentRepository repository) { action.multipartFiles, action.isPrivate, ) - .then((product) { - store.dispatch(SavePaymentSuccess(product)); + .then((payment) { + store.dispatch(SavePaymentSuccess(payment)); final documents = []; - product.documents.forEach((document) { + payment.documents.forEach((document) { documents.add(document.rebuild((b) => b - ..parentId = product.id - ..parentType = EntityType.product)); + ..parentId = payment.id + ..parentType = EntityType.payment)); }); store.dispatch(LoadDocumentsSuccess(documents)); action.completer.complete(documents); From 7bee9abb8e23ea59f0a30ee39e2de8f52869f1dc Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 13 Feb 2024 13:32:00 +0200 Subject: [PATCH 23/30] Attachments Feature on Payments --- lib/data/repositories/payment_repository.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/data/repositories/payment_repository.dart b/lib/data/repositories/payment_repository.dart index 1f5ddbb15d7..c865ea05faa 100644 --- a/lib/data/repositories/payment_repository.dart +++ b/lib/data/repositories/payment_repository.dart @@ -74,6 +74,7 @@ class PaymentRepository { Future saveData(Credentials credentials, PaymentEntity payment, {bool? sendEmail = false}) async { + payment = payment.rebuild((b) => b..documents.clear()); final data = serializers.serializeWith(PaymentEntity.serializer, payment); dynamic response; From 662d49624080bd0dd2e2e8c0898b73b66863d029 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 13 Feb 2024 13:42:01 +0200 Subject: [PATCH 24/30] Attachments Feature on Payments --- lib/redux/payment/payment_middleware.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/redux/payment/payment_middleware.dart b/lib/redux/payment/payment_middleware.dart index 27709310736..6e0d7db62ad 100644 --- a/lib/redux/payment/payment_middleware.dart +++ b/lib/redux/payment/payment_middleware.dart @@ -301,6 +301,17 @@ Middleware _loadPayments(PaymentRepository repository) { ) .then((data) { store.dispatch(LoadPaymentsSuccess(data)); + + final documents = []; + data.forEach((product) { + product.documents.forEach((document) { + documents.add(document.rebuild((b) => b + ..parentId = product.id + ..parentType = EntityType.payment)); + }); + }); + store.dispatch(LoadDocumentsSuccess(documents)); + if (data.length == kMaxRecordsPerPage) { store.dispatch(LoadPayments( completer: action.completer, From cbacd264c9acc5f55ecc12539697e9ab5bb8aba6 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 13 Feb 2024 13:47:45 +0200 Subject: [PATCH 25/30] Attachments Feature on Payments --- lib/redux/document/document_actions.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/redux/document/document_actions.dart b/lib/redux/document/document_actions.dart index 9b81551f805..5cd42dc14fe 100644 --- a/lib/redux/document/document_actions.dart +++ b/lib/redux/document/document_actions.dart @@ -19,6 +19,7 @@ import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; import 'package:invoiceninja_flutter/redux/expense/expense_actions.dart'; import 'package:invoiceninja_flutter/redux/group/group_actions.dart'; import 'package:invoiceninja_flutter/redux/invoice/invoice_actions.dart'; +import 'package:invoiceninja_flutter/redux/payment/payment_actions.dart'; import 'package:invoiceninja_flutter/redux/product/product_actions.dart'; import 'package:invoiceninja_flutter/redux/project/project_actions.dart'; import 'package:invoiceninja_flutter/redux/purchase_order/purchase_order_actions.dart'; @@ -502,6 +503,10 @@ void handleDocumentAction( completer.future.then((_) => store .dispatch(LoadVendor(vendorId: document.parentId))); break; + case EntityType.payment: + completer.future.then((_) => store + .dispatch(LoadPayment(paymentId: document.parentId))); + break; default: completer.future .then((_) => store.dispatch(RefreshData())); From 54a261a68a2629af956965825ff0091312e1f942 Mon Sep 17 00:00:00 2001 From: lsaudon Date: Tue, 13 Feb 2024 15:41:15 +0100 Subject: [PATCH 26/30] kMacOSUrl as same AppId of kAppleStoreUrl Signed-off-by: lsaudon --- lib/constants.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/constants.dart b/lib/constants.dart index 71f76f4be1c..8ecf076b124 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -43,7 +43,7 @@ const String kGoogleStoreUrl = 'https://play.google.com/store/apps/details?id=$kPlayStoreAppId'; const String kGoogleFDroidUrl = 'https://f-droid.org/packages/com.invoiceninja.app'; -const String kMacOSUrl = 'https://apps.apple.com/app/id1503970375'; +const String kMacOSUrl = 'https://apps.apple.com/app/$kAppStoreAppId'; const String kLinuxUrl = 'https://snapcraft.io/invoiceninja'; const String kWindowsUrl = 'https://apps.microsoft.com/store/detail/invoice-ninja/$kMicrosoftAppStoreId'; From 040730278d0997d65ec3dbe99ed72e595cdb6463 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 14 Feb 2024 15:12:31 +0200 Subject: [PATCH 27/30] Support bulk downloading payment documents --- lib/redux/payment/payment_actions.dart | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/redux/payment/payment_actions.dart b/lib/redux/payment/payment_actions.dart index ef0c622154e..b3ebd392a32 100644 --- a/lib/redux/payment/payment_actions.dart +++ b/lib/redux/payment/payment_actions.dart @@ -13,6 +13,7 @@ import 'package:http/http.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/redux/document/document_actions.dart'; import 'package:invoiceninja_flutter/ui/app/entities/entity_actions_dialog.dart'; import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/dialogs.dart'; @@ -438,6 +439,26 @@ void handlePaymentAction( entities: [payment], ); break; + case EntityAction.documents: + final documentIds = []; + for (var payment in payments) { + for (var document in (payment as PaymentEntity).documents) { + documentIds.add(document.id); + } + } + if (documentIds.isEmpty) { + showMessageDialog(message: localization!.noDocumentsToDownload); + } else { + store.dispatch( + DownloadDocumentsRequest( + documentIds: documentIds, + completer: snackBarCompleter( + localization!.exportedData, + ), + ), + ); + } + break; case EntityAction.runTemplate: showDialog( context: context, From 3942cb8ccf1b155930efcf9077d9e187563d1728 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 14 Feb 2024 15:16:42 +0200 Subject: [PATCH 28/30] Error when updating a payment --- lib/ui/payment/edit/payment_edit_vm.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/ui/payment/edit/payment_edit_vm.dart b/lib/ui/payment/edit/payment_edit_vm.dart index f25f7034e50..bb2d3e7a6a0 100644 --- a/lib/ui/payment/edit/payment_edit_vm.dart +++ b/lib/ui/payment/edit/payment_edit_vm.dart @@ -94,7 +94,8 @@ class PaymentEditVM { return null; } else if (!state.company.enableApplyingPayments && payment.invoices.isEmpty && - payment.credits.isEmpty) { + payment.credits.isEmpty && + payment.isNew) { showDialog( context: navigatorKey.currentContext!, builder: (BuildContext context) { From a9252c6fe0ff82677a0c67f591fa0d34b06afab5 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 14 Feb 2024 17:21:09 +0200 Subject: [PATCH 29/30] Fix invoice item report cost/profit when currencies are different --- lib/ui/reports/invoice_item_report.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/ui/reports/invoice_item_report.dart b/lib/ui/reports/invoice_item_report.dart index 3fee07c0b08..44881c42575 100644 --- a/lib/ui/reports/invoice_item_report.dart +++ b/lib/ui/reports/invoice_item_report.dart @@ -140,7 +140,10 @@ ReportResult lineItemReport( } else { cost = productId == null ? 0.0 : productMap[productId]!.cost; } - value = lineItem.netTotal(invoice, precision) - cost; + value = (lineItem.netTotal(invoice, precision) * + 1 / + invoice.exchangeRate) - + cost; if (column == InvoiceItemReportFields.markup && cost != 0) { value = '${round(value / cost * 100, 2)}%'; } @@ -229,7 +232,10 @@ ReportResult lineItemReport( value: value, currencyId: column == InvoiceItemReportFields.quantity ? null - : client.currencyId)); + : column == InvoiceItemReportFields.profit || + column == InvoiceItemReportFields.cost + ? userCompany.company.currencyId + : client.currencyId)); } else { row.add(invoice.getReportString(value: value)); } From f53aa99ac5fbfe252172eb50e521401dc693202f Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 14 Feb 2024 17:22:59 +0200 Subject: [PATCH 30/30] Fix invoice item report cost/profit when currencies are different --- lib/ui/reports/credit_item_report.dart | 10 ++++++++-- lib/ui/reports/quote_item_report.dart | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/ui/reports/credit_item_report.dart b/lib/ui/reports/credit_item_report.dart index 49f60bc194d..21064302b54 100644 --- a/lib/ui/reports/credit_item_report.dart +++ b/lib/ui/reports/credit_item_report.dart @@ -140,7 +140,10 @@ ReportResult lineItemReport( } else { cost = productId == null ? 0.0 : productMap[productId]!.cost; } - value = lineItem.netTotal(credit, precision) - cost; + value = (lineItem.netTotal(credit, precision) * + 1 / + credit.exchangeRate) - + cost; if (column == CreditItemReportFields.markup && cost != 0) { value = '${round(value / cost * 100, 2)}%'; } @@ -229,7 +232,10 @@ ReportResult lineItemReport( value: value, currencyId: column == CreditItemReportFields.quantity ? null - : client.currencyId)); + : column == CreditItemReportFields.profit || + column == CreditItemReportFields.cost + ? userCompany.company.currencyId + : client.currencyId)); } else { row.add(credit.getReportString(value: value)); } diff --git a/lib/ui/reports/quote_item_report.dart b/lib/ui/reports/quote_item_report.dart index 5ae8f75462a..74331a69991 100644 --- a/lib/ui/reports/quote_item_report.dart +++ b/lib/ui/reports/quote_item_report.dart @@ -136,7 +136,10 @@ ReportResult lineItemReport( } else { cost = productId == null ? 0.0 : productMap[productId]!.cost; } - value = lineItem.netTotal(invoice, precision) - cost; + value = (lineItem.netTotal(invoice, precision) * + 1 / + invoice.exchangeRate) - + cost; if (column == QuoteItemReportFields.markup && cost != 0) { value = '${round(value / cost * 100, 2)}%'; } @@ -225,7 +228,10 @@ ReportResult lineItemReport( value: value, currencyId: column == QuoteItemReportFields.quantity ? null - : client.currencyId)); + : column == QuoteItemReportFields.profit || + column == QuoteItemReportFields.cost + ? userCompany.company.currencyId + : client.currencyId)); } else { row.add(invoice.getReportString(value: value)); }