From 07a1fad238b702721cf60f1a54719d496f59e106 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 30 Sep 2024 11:46:28 +0300 Subject: [PATCH 01/15] Fixes for macOS build --- macos/Podfile.lock | 7 ------- 1 file changed, 7 deletions(-) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 8d79a53ba3..6ace34d559 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -5,9 +5,6 @@ PODS: - AppAuth/Core (1.7.5) - AppAuth/ExternalUserAgent (1.7.5): - AppAuth/Core - - connectivity_plus (0.0.1): - - Flutter - - FlutterMacOS - desktop_drop (0.0.1): - FlutterMacOS - device_info_plus (0.0.1): @@ -71,7 +68,6 @@ PODS: - FlutterMacOS DEPENDENCIES: - - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`) - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) @@ -102,8 +98,6 @@ SPEC REPOS: - Sentry EXTERNAL SOURCES: - connectivity_plus: - :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin desktop_drop: :path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos device_info_plus: @@ -147,7 +141,6 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa - connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f file_selector_macos: 468fb6b81fac7c0e88d71317f3eec34c3b008ff9 From 043f7ad28a40dad8ada2c7b85f0b340f3bf4ffe6 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 27 Nov 2024 16:51:07 +0200 Subject: [PATCH 02/15] Show public and private notes of a client on Invoice-, Quotes- and other document-editors #700 --- lib/ui/app/forms/client_picker.dart | 60 ++++++++++++++----- lib/ui/app/icon_text.dart | 12 +++- lib/ui/invoice/edit/invoice_edit_desktop.dart | 1 + lib/ui/invoice/edit/invoice_edit_details.dart | 2 + 4 files changed, 60 insertions(+), 15 deletions(-) diff --git a/lib/ui/app/forms/client_picker.dart b/lib/ui/app/forms/client_picker.dart index 78de6ff07a..03c5198166 100644 --- a/lib/ui/app/forms/client_picker.dart +++ b/lib/ui/app/forms/client_picker.dart @@ -13,6 +13,7 @@ import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/client/client_selectors.dart'; import 'package:invoiceninja_flutter/redux/client/client_state.dart'; import 'package:invoiceninja_flutter/ui/app/entity_dropdown.dart'; +import 'package:invoiceninja_flutter/ui/app/icon_text.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; class ClientPicker extends StatelessWidget { @@ -25,6 +26,7 @@ class ClientPicker extends StatelessWidget { this.autofocus, this.excludeIds = const [], this.isRequired = true, + this.showNotes = false, }) : super(key: key); final String? clientId; @@ -34,27 +36,57 @@ class ClientPicker extends StatelessWidget { final bool? autofocus; final List excludeIds; final bool isRequired; + final bool showNotes; @override Widget build(BuildContext context) { final localization = AppLocalization.of(context)!; final store = StoreProvider.of(context); final state = store.state; + final client = state.clientState.get(clientId ?? ''); - return EntityDropdown( - entityType: EntityType.client, - labelText: localization.client, - entityId: clientId, - autofocus: autofocus, - entityList: memoizedDropdownClientList(clientState.map, clientState.list, - state.userState.map, state.staticState), - entityMap: clientState.map, - validator: (String? val) => isRequired && (val ?? '').trim().isEmpty - ? AppLocalization.of(context)!.pleaseSelectAClient - : null, - onSelected: onSelected, - onAddPressed: onAddPressed, - excludeIds: excludeIds, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + EntityDropdown( + entityType: EntityType.client, + labelText: localization.client, + entityId: clientId, + autofocus: autofocus, + entityList: memoizedDropdownClientList(clientState.map, + clientState.list, state.userState.map, state.staticState), + entityMap: clientState.map, + validator: (String? val) => isRequired && (val ?? '').trim().isEmpty + ? AppLocalization.of(context)!.pleaseSelectAClient + : null, + onSelected: onSelected, + onAddPressed: onAddPressed, + excludeIds: excludeIds, + ), + if (showNotes) ...[ + SizedBox(height: 4), + if (client.publicNotes.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: IconText( + text: client.privateNotes, + icon: Icons.lock, + iconSize: 16, + maxLines: 3, + )), + if (client.privateNotes.isNotEmpty) + Padding( + padding: EdgeInsets.only( + top: client.publicNotes.isEmpty ? 8 : 0, bottom: 8), + child: IconText( + text: client.publicNotes, + icon: Icons.note, + iconSize: 16, + maxLines: 3, + ), + ), + ] + ], ); } } diff --git a/lib/ui/app/icon_text.dart b/lib/ui/app/icon_text.dart index ee67d9c916..e354955031 100644 --- a/lib/ui/app/icon_text.dart +++ b/lib/ui/app/icon_text.dart @@ -9,6 +9,8 @@ class IconText extends StatelessWidget { this.style, this.alignment, this.copyToClipboard = false, + this.iconSize, + this.maxLines, }); final String? text; @@ -16,6 +18,8 @@ class IconText extends StatelessWidget { final TextStyle? style; final MainAxisAlignment? alignment; final bool copyToClipboard; + final double? iconSize; + final int? maxLines; @override Widget build(BuildContext context) { @@ -23,7 +27,11 @@ class IconText extends StatelessWidget { mainAxisAlignment: alignment ?? MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, color: style?.color), + Icon( + icon, + color: style?.color, + size: iconSize, + ), SizedBox(width: 10), Flexible( child: copyToClipboard @@ -33,11 +41,13 @@ class IconText extends StatelessWidget { text ?? '', style: style, overflow: TextOverflow.ellipsis, + maxLines: maxLines, )) : Text( text ?? '', style: style, overflow: TextOverflow.ellipsis, + maxLines: maxLines, ), ), ], diff --git a/lib/ui/invoice/edit/invoice_edit_desktop.dart b/lib/ui/invoice/edit/invoice_edit_desktop.dart index 229b676edd..5c29d22b95 100644 --- a/lib/ui/invoice/edit/invoice_edit_desktop.dart +++ b/lib/ui/invoice/edit/invoice_edit_desktop.dart @@ -301,6 +301,7 @@ class InvoiceEditDesktopState extends State else ClientPicker( autofocus: true, + showNotes: true, clientId: invoice.clientId, clientState: state.clientState, onSelected: (client) { diff --git a/lib/ui/invoice/edit/invoice_edit_details.dart b/lib/ui/invoice/edit/invoice_edit_details.dart index aad555097b..9bf227558e 100644 --- a/lib/ui/invoice/edit/invoice_edit_details.dart +++ b/lib/ui/invoice/edit/invoice_edit_details.dart @@ -179,6 +179,8 @@ class InvoiceEditDetailsState extends State { viewModel.onAddVendorPressed!(context, completer), ) : ClientPicker( + autofocus: true, + showNotes: true, clientId: invoice.clientId, clientState: state.clientState, onSelected: (client) => viewModel.onClientChanged!( From 3fb5515a18b3c9bd8b29446e07e2c522adb62df3 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 27 Nov 2024 17:08:26 +0200 Subject: [PATCH 03/15] Show public and private notes of a client on Invoice-, Quotes- and other document-editors --- lib/ui/app/forms/client_picker.dart | 27 ------ lib/ui/invoice/edit/invoice_edit_desktop.dart | 60 +++++++++++- lib/ui/invoice/edit/invoice_edit_details.dart | 93 +++++++++++++++---- 3 files changed, 132 insertions(+), 48 deletions(-) diff --git a/lib/ui/app/forms/client_picker.dart b/lib/ui/app/forms/client_picker.dart index 03c5198166..a842260f5f 100644 --- a/lib/ui/app/forms/client_picker.dart +++ b/lib/ui/app/forms/client_picker.dart @@ -13,7 +13,6 @@ import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/client/client_selectors.dart'; import 'package:invoiceninja_flutter/redux/client/client_state.dart'; import 'package:invoiceninja_flutter/ui/app/entity_dropdown.dart'; -import 'package:invoiceninja_flutter/ui/app/icon_text.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; class ClientPicker extends StatelessWidget { @@ -26,7 +25,6 @@ class ClientPicker extends StatelessWidget { this.autofocus, this.excludeIds = const [], this.isRequired = true, - this.showNotes = false, }) : super(key: key); final String? clientId; @@ -36,14 +34,12 @@ class ClientPicker extends StatelessWidget { final bool? autofocus; final List excludeIds; final bool isRequired; - final bool showNotes; @override Widget build(BuildContext context) { final localization = AppLocalization.of(context)!; final store = StoreProvider.of(context); final state = store.state; - final client = state.clientState.get(clientId ?? ''); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -63,29 +59,6 @@ class ClientPicker extends StatelessWidget { onAddPressed: onAddPressed, excludeIds: excludeIds, ), - if (showNotes) ...[ - SizedBox(height: 4), - if (client.publicNotes.isNotEmpty) - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: IconText( - text: client.privateNotes, - icon: Icons.lock, - iconSize: 16, - maxLines: 3, - )), - if (client.privateNotes.isNotEmpty) - Padding( - padding: EdgeInsets.only( - top: client.publicNotes.isEmpty ? 8 : 0, bottom: 8), - child: IconText( - text: client.publicNotes, - icon: Icons.note, - iconSize: 16, - maxLines: 3, - ), - ), - ] ], ); } diff --git a/lib/ui/invoice/edit/invoice_edit_desktop.dart b/lib/ui/invoice/edit/invoice_edit_desktop.dart index 5c29d22b95..812052ea70 100644 --- a/lib/ui/invoice/edit/invoice_edit_desktop.dart +++ b/lib/ui/invoice/edit/invoice_edit_desktop.dart @@ -43,6 +43,7 @@ import 'package:invoiceninja_flutter/ui/app/forms/project_picker.dart'; import 'package:invoiceninja_flutter/ui/app/forms/user_picker.dart'; import 'package:invoiceninja_flutter/ui/app/forms/vendor_picker.dart'; import 'package:invoiceninja_flutter/ui/app/help_text.dart'; +import 'package:invoiceninja_flutter/ui/app/icon_text.dart'; import 'package:invoiceninja_flutter/ui/app/invoice/tax_rate_dropdown.dart'; import 'package:invoiceninja_flutter/ui/app/presenters/entity_presenter.dart'; import 'package:invoiceninja_flutter/ui/credit/edit/credit_edit_items_vm.dart'; @@ -301,7 +302,6 @@ class InvoiceEditDesktopState extends State else ClientPicker( autofocus: true, - showNotes: true, clientId: invoice.clientId, clientState: state.clientState, onSelected: (client) { @@ -342,7 +342,63 @@ class InvoiceEditDesktopState extends State ), ), ), - SizedBox(height: 12), + SizedBox(height: 4), + if (invoice.isPurchaseOrder) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (vendor.privateNotes.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8), + child: IconText( + text: vendor.privateNotes, + icon: Icons.lock, + iconSize: 16, + maxLines: 3, + )), + if (vendor.publicNotes.isNotEmpty) + Padding( + padding: EdgeInsets.only( + top: vendor.publicNotes.isEmpty ? 8 : 0, + bottom: 8), + child: IconText( + text: vendor.publicNotes, + icon: Icons.note, + iconSize: 16, + maxLines: 3, + ), + ), + ], + ) + else + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (client.privateNotes.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8), + child: IconText( + text: client.privateNotes, + icon: Icons.lock, + iconSize: 16, + maxLines: 3, + )), + if (client.publicNotes.isNotEmpty) + Padding( + padding: EdgeInsets.only( + top: client.publicNotes.isEmpty ? 8 : 0, + bottom: 8), + child: IconText( + text: client.publicNotes, + icon: Icons.note, + iconSize: 16, + maxLines: 3, + ), + ), + ], + ), ConstrainedBox( constraints: BoxConstraints(maxHeight: 186), child: InvoiceEditContactsScreen( diff --git a/lib/ui/invoice/edit/invoice_edit_details.dart b/lib/ui/invoice/edit/invoice_edit_details.dart index 9bf227558e..a0a53f9cb0 100644 --- a/lib/ui/invoice/edit/invoice_edit_details.dart +++ b/lib/ui/invoice/edit/invoice_edit_details.dart @@ -24,6 +24,7 @@ import 'package:invoiceninja_flutter/ui/app/forms/discount_field.dart'; import 'package:invoiceninja_flutter/ui/app/forms/project_picker.dart'; import 'package:invoiceninja_flutter/ui/app/forms/user_picker.dart'; import 'package:invoiceninja_flutter/ui/app/forms/vendor_picker.dart'; +import 'package:invoiceninja_flutter/ui/app/icon_text.dart'; import 'package:invoiceninja_flutter/ui/app/invoice/tax_rate_dropdown.dart'; import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart'; import 'package:invoiceninja_flutter/ui/invoice/edit/invoice_edit_details_vm.dart'; @@ -151,6 +152,7 @@ class InvoiceEditDetailsState extends State { state.getEntity(invoice.entityType, invoice.id) as InvoiceEntity?; final client = state.clientState.get(invoice.clientId); + final vendor = state.vendorState.get(invoice.vendorId); final settings = getClientSettings(state, client); final terms = widget.entityType == EntityType.quote ? settings.defaultValidUntil @@ -167,26 +169,79 @@ class InvoiceEditDetailsState extends State { children: [ invoice.isNew ? invoice.isPurchaseOrder - ? VendorPicker( - autofocus: true, - vendorId: invoice.vendorId, - vendorState: state.vendorState, - onSelected: (vendor) { - viewModel.onVendorChanged!( - context, invoice, vendor as VendorEntity?); - }, - onAddPressed: (completer) => - viewModel.onAddVendorPressed!(context, completer), + ? Column( + children: [ + VendorPicker( + autofocus: true, + vendorId: invoice.vendorId, + vendorState: state.vendorState, + onSelected: (vendor) { + viewModel.onVendorChanged!( + context, invoice, vendor as VendorEntity?); + }, + onAddPressed: (completer) => viewModel + .onAddVendorPressed!(context, completer), + ), + SizedBox(height: 4), + if (vendor.privateNotes.isNotEmpty) + Padding( + padding: + const EdgeInsets.symmetric(vertical: 8), + child: IconText( + text: vendor.privateNotes, + icon: Icons.lock, + iconSize: 16, + maxLines: 3, + )), + if (vendor.publicNotes.isNotEmpty) + Padding( + padding: EdgeInsets.only( + top: vendor.publicNotes.isEmpty ? 8 : 0, + bottom: 8), + child: IconText( + text: vendor.publicNotes, + icon: Icons.note, + iconSize: 16, + maxLines: 3, + ), + ), + ], ) - : ClientPicker( - autofocus: true, - showNotes: true, - clientId: invoice.clientId, - clientState: state.clientState, - onSelected: (client) => viewModel.onClientChanged!( - context, invoice, client as ClientEntity?), - onAddPressed: (completer) => - viewModel.onAddClientPressed!(context, completer), + : Column( + children: [ + ClientPicker( + autofocus: true, + clientId: invoice.clientId, + clientState: state.clientState, + onSelected: (client) => viewModel.onClientChanged!( + context, invoice, client as ClientEntity?), + onAddPressed: (completer) => viewModel + .onAddClientPressed!(context, completer), + ), + SizedBox(height: 4), + if (client.privateNotes.isNotEmpty) + Padding( + padding: + const EdgeInsets.symmetric(vertical: 8), + child: IconText( + text: client.privateNotes, + icon: Icons.lock, + iconSize: 16, + maxLines: 3, + )), + if (client.publicNotes.isNotEmpty) + Padding( + padding: EdgeInsets.only( + top: client.publicNotes.isEmpty ? 8 : 0, + bottom: 8), + child: IconText( + text: client.publicNotes, + icon: Icons.note, + iconSize: 16, + maxLines: 3, + ), + ), + ], ) : DecoratedFormField( controller: _invoiceNumberController, From 800794776c5be3f5821ae30dfdc3fa4a77423400 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 27 Nov 2024 17:10:23 +0200 Subject: [PATCH 04/15] Fix for applying white label license --- lib/ui/settings/account_management.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/ui/settings/account_management.dart b/lib/ui/settings/account_management.dart index 71e719aa7c..1b3261ab62 100644 --- a/lib/ui/settings/account_management.dart +++ b/lib/ui/settings/account_management.dart @@ -534,7 +534,6 @@ class _AccountOverview extends StatelessWidget { context: context, title: localization.applyLicense, field: localization.license, - maxLength: 24, callback: (value) { final state = viewModel.state; final credentials = state.credentials; From f886b68c3e8b0c9557319ed242bc1c343d317a34 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 27 Nov 2024 17:11:57 +0200 Subject: [PATCH 05/15] Show e-invoice settings with group settings --- lib/ui/settings/settings_list.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/ui/settings/settings_list.dart b/lib/ui/settings/settings_list.dart index e791e3b5de..d5fcc95810 100644 --- a/lib/ui/settings/settings_list.dart +++ b/lib/ui/settings/settings_list.dart @@ -212,11 +212,10 @@ class _SettingsListState extends State { section: kSettingsBankAccounts, viewModel: widget.viewModel, ), - if (showAll) - SettingsListTile( - section: kSettingsEInvoiceSettings, - viewModel: widget.viewModel, - ), + SettingsListTile( + section: kSettingsEInvoiceSettings, + viewModel: widget.viewModel, + ), if (showAll) SettingsListTile( section: kSettingsGroupSettings, From 21c050e9796b3b97d627c58bedd34d12d8363ae3 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 27 Nov 2024 17:23:32 +0200 Subject: [PATCH 06/15] Add debug for cert issue --- lib/main.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/main.dart b/lib/main.dart index 3b435c767a..3e54bc6543 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -107,6 +107,7 @@ class MyHttpOverrides extends HttpOverrides { HttpClient createHttpClient(SecurityContext? context) { return super.createHttpClient(context) ..badCertificateCallback = (X509Certificate cert, String host, int port) { + print('## Comparing ${this.host} to $host'); return this.host == host; }; } From ff5497ee02c0d63b321966e914f87fd958d85429 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 1 Dec 2024 16:00:36 +0200 Subject: [PATCH 07/15] Show max_qty 0 as blank --- lib/ui/product/edit/product_edit.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/ui/product/edit/product_edit.dart b/lib/ui/product/edit/product_edit.dart index 41dfa08873..d004d93386 100644 --- a/lib/ui/product/edit/product_edit.dart +++ b/lib/ui/product/edit/product_edit.dart @@ -91,11 +91,13 @@ class _ProductEditState extends State { context, formatNumberType: FormatNumberType.int, )!; - _maxQuantityController.text = formatNumber( - product.maxQuantity.toDouble(), - context, - formatNumberType: FormatNumberType.int, - )!; + _maxQuantityController.text = product.maxQuantity == 0 + ? '' + : formatNumber( + product.maxQuantity.toDouble(), + context, + formatNumberType: FormatNumberType.int, + )!; _imageUrlController.text = product.imageUrl; _notificationThresholdController.text = product.stockNotificationThreshold == 0 From e7054fde0adb64561f409cc680866fc99992341f Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 1 Dec 2024 16:04:27 +0200 Subject: [PATCH 08/15] Fix for selfhosted GoCardless gateway --- lib/ui/company_gateway/edit/company_gateway_edit.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/ui/company_gateway/edit/company_gateway_edit.dart b/lib/ui/company_gateway/edit/company_gateway_edit.dart index ba8c70a672..482e32565a 100644 --- a/lib/ui/company_gateway/edit/company_gateway_edit.dart +++ b/lib/ui/company_gateway/edit/company_gateway_edit.dart @@ -108,7 +108,8 @@ class _CompanyGatewayEditState extends State kGatewayGoCardlessOAuth, ]; - final disableSave = (connectGateways.contains(companyGateway.gatewayId) && + final disableSave = (state.isHosted && + connectGateways.contains(companyGateway.gatewayId) && companyGateway.isNew) || state.isDemo; final enabledGatewayIds = (gateway?.options.keys ?? []).where( @@ -170,7 +171,8 @@ class _CompanyGatewayEditState extends State ); }, ), - if (connectGateways.contains(companyGateway.gatewayId)) + if (state.isHosted & + connectGateways.contains(companyGateway.gatewayId)) if (companyGateway.isNew || (companyGateway.gatewayId == kGatewayStripeConnect && accountId.isEmpty)) ...[ From 4b80902f6e8290d79ba119d7b3b6050f2cbafc7e Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 1 Dec 2024 19:08:52 +0200 Subject: [PATCH 09/15] Add vendor.is_tax_exempt --- lib/data/models/vendor_model.dart | 6 ++++++ lib/data/models/vendor_model.g.dart | 21 +++++++++++++++++++ lib/ui/client/view/client_view_fullwidth.dart | 1 - lib/ui/invoice/edit/invoice_tax_details.dart | 4 +++- lib/ui/reports/vendor_report.dart | 4 ++++ lib/ui/vendor/edit/vendor_edit_details.dart | 9 ++++++++ lib/ui/vendor/vendor_presenter.dart | 3 +++ lib/ui/vendor/view/vendor_view_fullwidth.dart | 1 + 8 files changed, 47 insertions(+), 2 deletions(-) diff --git a/lib/data/models/vendor_model.dart b/lib/data/models/vendor_model.dart index 0e0d971e51..0ee0e529f7 100644 --- a/lib/data/models/vendor_model.dart +++ b/lib/data/models/vendor_model.dart @@ -84,6 +84,7 @@ class VendorFields { static const String contactEmail = 'contact_email'; static const String classification = 'classification'; static const String routingId = 'routing_id'; + static const String isTaxExempt = 'is_tax_exempt'; } abstract class VendorEntity extends Object @@ -124,6 +125,7 @@ abstract class VendorEntity extends Object updatedAt: 0, archivedAt: 0, isDeleted: false, + isTaxExempt: false, assignedUserId: user?.id ?? '', createdUserId: '', createdAt: 0, @@ -224,6 +226,9 @@ abstract class VendorEntity extends Object @BuiltValueField(wireName: 'routing_id') String get routingId; + @BuiltValueField(wireName: 'is_tax_exempt') + bool get isTaxExempt; + @BuiltValueField(wireName: 'last_login') int get lastLogin; @@ -534,6 +539,7 @@ abstract class VendorEntity extends Object ..lastLogin = 0 ..routingId = '' ..languageId = '' + ..isTaxExempt = false ..displayName = '' ..classification = ''; diff --git a/lib/data/models/vendor_model.g.dart b/lib/data/models/vendor_model.g.dart index 3b9bca03a4..5e3e078d92 100644 --- a/lib/data/models/vendor_model.g.dart +++ b/lib/data/models/vendor_model.g.dart @@ -178,6 +178,9 @@ class _$VendorEntitySerializer implements StructuredSerializer { 'routing_id', serializers.serialize(object.routingId, specifiedType: const FullType(String)), + 'is_tax_exempt', + serializers.serialize(object.isTaxExempt, + specifiedType: const FullType(bool)), 'last_login', serializers.serialize(object.lastLogin, specifiedType: const FullType(int)), @@ -350,6 +353,10 @@ class _$VendorEntitySerializer implements StructuredSerializer { result.routingId = serializers.deserialize(value, specifiedType: const FullType(String))! as String; break; + case 'is_tax_exempt': + result.isTaxExempt = serializers.deserialize(value, + specifiedType: const FullType(bool))! as bool; + break; case 'last_login': result.lastLogin = serializers.deserialize(value, specifiedType: const FullType(int))! as int; @@ -853,6 +860,8 @@ class _$VendorEntity extends VendorEntity { @override final String routingId; @override + final bool isTaxExempt; + @override final int lastLogin; @override final String classification; @@ -906,6 +915,7 @@ class _$VendorEntity extends VendorEntity { required this.customValue3, required this.customValue4, required this.routingId, + required this.isTaxExempt, required this.lastLogin, required this.classification, required this.contacts, @@ -958,6 +968,8 @@ class _$VendorEntity extends VendorEntity { customValue4, r'VendorEntity', 'customValue4'); BuiltValueNullFieldError.checkNotNull( routingId, r'VendorEntity', 'routingId'); + BuiltValueNullFieldError.checkNotNull( + isTaxExempt, r'VendorEntity', 'isTaxExempt'); BuiltValueNullFieldError.checkNotNull( lastLogin, r'VendorEntity', 'lastLogin'); BuiltValueNullFieldError.checkNotNull( @@ -1010,6 +1022,7 @@ class _$VendorEntity extends VendorEntity { customValue3 == other.customValue3 && customValue4 == other.customValue4 && routingId == other.routingId && + isTaxExempt == other.isTaxExempt && lastLogin == other.lastLogin && classification == other.classification && contacts == other.contacts && @@ -1052,6 +1065,7 @@ class _$VendorEntity extends VendorEntity { _$hash = $jc(_$hash, customValue3.hashCode); _$hash = $jc(_$hash, customValue4.hashCode); _$hash = $jc(_$hash, routingId.hashCode); + _$hash = $jc(_$hash, isTaxExempt.hashCode); _$hash = $jc(_$hash, lastLogin.hashCode); _$hash = $jc(_$hash, classification.hashCode); _$hash = $jc(_$hash, contacts.hashCode); @@ -1095,6 +1109,7 @@ class _$VendorEntity extends VendorEntity { ..add('customValue3', customValue3) ..add('customValue4', customValue4) ..add('routingId', routingId) + ..add('isTaxExempt', isTaxExempt) ..add('lastLogin', lastLogin) ..add('classification', classification) ..add('contacts', contacts) @@ -1208,6 +1223,10 @@ class VendorEntityBuilder String? get routingId => _$this._routingId; set routingId(String? routingId) => _$this._routingId = routingId; + bool? _isTaxExempt; + bool? get isTaxExempt => _$this._isTaxExempt; + set isTaxExempt(bool? isTaxExempt) => _$this._isTaxExempt = isTaxExempt; + int? _lastLogin; int? get lastLogin => _$this._lastLogin; set lastLogin(int? lastLogin) => _$this._lastLogin = lastLogin; @@ -1299,6 +1318,7 @@ class VendorEntityBuilder _customValue3 = $v.customValue3; _customValue4 = $v.customValue4; _routingId = $v.routingId; + _isTaxExempt = $v.isTaxExempt; _lastLogin = $v.lastLogin; _classification = $v.classification; _contacts = $v.contacts.toBuilder(); @@ -1368,6 +1388,7 @@ class VendorEntityBuilder customValue3: BuiltValueNullFieldError.checkNotNull(customValue3, r'VendorEntity', 'customValue3'), customValue4: BuiltValueNullFieldError.checkNotNull(customValue4, r'VendorEntity', 'customValue4'), routingId: BuiltValueNullFieldError.checkNotNull(routingId, r'VendorEntity', 'routingId'), + isTaxExempt: BuiltValueNullFieldError.checkNotNull(isTaxExempt, r'VendorEntity', 'isTaxExempt'), lastLogin: BuiltValueNullFieldError.checkNotNull(lastLogin, r'VendorEntity', 'lastLogin'), classification: BuiltValueNullFieldError.checkNotNull(classification, r'VendorEntity', 'classification'), contacts: contacts.build(), diff --git a/lib/ui/client/view/client_view_fullwidth.dart b/lib/ui/client/view/client_view_fullwidth.dart index 81b875143d..036661f263 100644 --- a/lib/ui/client/view/client_view_fullwidth.dart +++ b/lib/ui/client/view/client_view_fullwidth.dart @@ -128,7 +128,6 @@ class _ClientViewFullwidthState extends State style: Theme.of(context).textTheme.titleLarge, ), SizedBox(height: 8), - if (client.isTaxExempt) Text(localization.isTaxExempt), if (client.paymentBalance != 0) Text(localization.payments + ': ' + diff --git a/lib/ui/invoice/edit/invoice_tax_details.dart b/lib/ui/invoice/edit/invoice_tax_details.dart index 8c6f96485e..7e3229a1fe 100644 --- a/lib/ui/invoice/edit/invoice_tax_details.dart +++ b/lib/ui/invoice/edit/invoice_tax_details.dart @@ -20,13 +20,15 @@ class InvoiceTaxDetails extends StatelessWidget { final localization = AppLocalization.of(context)!; final state = StoreProvider.of(context).state; final client = state.clientState.get(invoice.clientId); + final vendor = state.vendorState.get(invoice.vendorId); final taxData = invoice.isNew ? client.taxData : invoice.taxData; return AlertDialog( title: Text(localization.taxDetails), content: SizedBox( width: isDesktop(context) ? 500 : null, - child: client.isTaxExempt + child: (invoice.isPurchaseOrder && vendor.isTaxExempt) || + (!invoice.isPurchaseOrder && client.isTaxExempt) ? SizedBox( child: HelpText(localization.isTaxExempt), height: 100, diff --git a/lib/ui/reports/vendor_report.dart b/lib/ui/reports/vendor_report.dart index 0db2840b77..20f6e1382a 100644 --- a/lib/ui/reports/vendor_report.dart +++ b/lib/ui/reports/vendor_report.dart @@ -58,6 +58,7 @@ enum VendorReportFields { routing_id, classification, record_state, + tax_exempt, /* contact_last_login, shipping_address1, @@ -332,6 +333,9 @@ ReportResult vendorReport( value = AppLocalization.of(navigatorKey.currentContext!)! .lookup(vendor.entityState); break; + case VendorReportFields.tax_exempt: + value = vendor.isTaxExempt; + break; } if (!ReportResult.matchField( diff --git a/lib/ui/vendor/edit/vendor_edit_details.dart b/lib/ui/vendor/edit/vendor_edit_details.dart index 8629f6192b..acfe5e6908 100644 --- a/lib/ui/vendor/edit/vendor_edit_details.dart +++ b/lib/ui/vendor/edit/vendor_edit_details.dart @@ -290,6 +290,15 @@ class VendorEditDetailsState extends State { )) .toList(), ), + SizedBox(height: 20), + SwitchListTile( + title: Text(localization.isTaxExempt), + value: vendor.isTaxExempt, + onChanged: (value) { + viewModel + .onChanged(vendor.rebuild((b) => b..isTaxExempt = value)); + }, + ), ], ), ); diff --git a/lib/ui/vendor/vendor_presenter.dart b/lib/ui/vendor/vendor_presenter.dart index 9e09b05b2f..2344780857 100644 --- a/lib/ui/vendor/vendor_presenter.dart +++ b/lib/ui/vendor/vendor_presenter.dart @@ -54,6 +54,7 @@ class VendorPresenter extends EntityPresenter { VendorFields.routingId, ], if (userCompany.company.calculateTaxes) ...[ + VendorFields.isTaxExempt, VendorFields.classification, ], ]; @@ -90,6 +91,8 @@ class VendorPresenter extends EntityPresenter { case VendorFields.countryId: return Text( state.staticState.countryMap[vendor!.countryId]?.name ?? ''); + case VendorFields.isTaxExempt: + return Text(vendor!.isTaxExempt ? localization!.yes : localization!.no); case VendorFields.privateNotes: return TableTooltip(message: vendor!.privateNotes); case VendorFields.publicNotes: diff --git a/lib/ui/vendor/view/vendor_view_fullwidth.dart b/lib/ui/vendor/view/vendor_view_fullwidth.dart index 126764b74c..b8d65f5363 100644 --- a/lib/ui/vendor/view/vendor_view_fullwidth.dart +++ b/lib/ui/vendor/view/vendor_view_fullwidth.dart @@ -92,6 +92,7 @@ class _VendorViewFullwidthState extends State style: Theme.of(context).textTheme.titleLarge, ), SizedBox(height: 8), + if (vendor.isTaxExempt) Text(localization.isTaxExempt), if (vendor.idNumber.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 1), From 7ab8ea3bafb643799d45f94367c8dc2389c0dbd4 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Sun, 1 Dec 2024 19:19:15 +0200 Subject: [PATCH 10/15] Vendor bulk updates --- lib/data/models/client_model.dart | 2 +- lib/data/models/vendor_model.dart | 6 ++++++ lib/redux/vendor/vendor_actions.dart | 11 +++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/data/models/client_model.dart b/lib/data/models/client_model.dart index 8690d55a88..62a79503a2 100644 --- a/lib/data/models/client_model.dart +++ b/lib/data/models/client_model.dart @@ -691,7 +691,7 @@ abstract class ClientEntity extends Object } } - if (!isDeleted! && multiselect) { + if (!isDeleted!) { actions.add(EntityAction.bulkUpdate); } diff --git a/lib/data/models/vendor_model.dart b/lib/data/models/vendor_model.dart index 0ee0e529f7..7f03311f41 100644 --- a/lib/data/models/vendor_model.dart +++ b/lib/data/models/vendor_model.dart @@ -278,6 +278,12 @@ abstract class VendorEntity extends Object actions.add(EntityAction.addComment); } + /* + if (!isDeleted!) { + actions.add(EntityAction.bulkUpdate); + } + */ + if (actions.isNotEmpty && actions.last != null) { actions.add(null); } diff --git a/lib/redux/vendor/vendor_actions.dart b/lib/redux/vendor/vendor_actions.dart index 0c0b3f4bb8..e668ec1d69 100644 --- a/lib/redux/vendor/vendor_actions.dart +++ b/lib/redux/vendor/vendor_actions.dart @@ -354,6 +354,17 @@ void handleVendorAction(BuildContext? context, List vendors, store.dispatch( DeleteVendorRequest(snackBarCompleter(message), vendorIds)); break; + case EntityAction.bulkUpdate: + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => BulkUpdateDialog( + entityType: EntityType.vendor, + entities: vendors, + ), + ); + break; + case EntityAction.toggleMultiselect: if (!store.state.vendorListState.isInMultiselect()) { store.dispatch(StartVendorMultiselect()); From e954ad8a8b3756750139232b2ac41e254dfbd2a1 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 2 Dec 2024 14:54:15 +0200 Subject: [PATCH 11/15] Fixes for activities --- lib/constants.dart | 3 +++ lib/data/models/entities.dart | 5 +++++ lib/utils/i18n.dart | 13 ++++++++----- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index 302456b7e5..6b8c92e2aa 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -1145,3 +1145,6 @@ const String kActivityQuoteEmailReminder1 = '142'; const String kActivityAutoBillSuccess = '143'; const String kActivityAutoBillFailure = '144'; const String kActivityEInvoiceSuccess = '145'; +const String kActivityEInvoiceDeliverySuccess = '146'; +const String kActivityEInvoiceDeliveryFailure = '147'; +const String kActivityEExpenseCreated = '148'; diff --git a/lib/data/models/entities.dart b/lib/data/models/entities.dart index ec986f6bc0..a8aaf211ab 100644 --- a/lib/data/models/entities.dart +++ b/lib/data/models/entities.dart @@ -830,6 +830,8 @@ abstract class ActivityEntity kActivityAutoBillSuccess, kActivityAutoBillFailure, kActivityEInvoiceSuccess, + kActivityEInvoiceDeliverySuccess, + kActivityEInvoiceDeliveryFailure, ].contains(activityTypeId)) { return EntityType.invoice; } else if ([ @@ -881,6 +883,7 @@ abstract class ActivityEntity kActivityRestoreExpense, kActivityUpdateExpense, kActivityExpenseNotificationSent, + kActivityEExpenseCreated, ].contains(activityTypeId)) { return EntityType.expense; } else if ([ @@ -1017,6 +1020,8 @@ abstract class ActivityEntity ':recurring_expense', vendor?.name ?? ''); // TODO implement activity = activity.replaceAll(' ', ' '); + activity = activity.replaceFirst(' - :notes', ''); + return activity; } diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index e6b4732052..c7a9d13629 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -18,12 +18,15 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'activity_146': 'E-Invoice :invoice for :client was delivered', + 'activity_147': 'E-Invoice :invoice for :client failed to send', + 'activity_148': 'E-Exepnse :expense created', 'payment_failed': 'Payment Failed', 'activity_141': 'User :user entered note :notes', 'activity_142': 'Quote :number reminder 1 sent', 'activity_143': 'Auto Bill succeeded for invoice :invoice', - 'activity_144': 'Auto Bill failed for invoice :invoice. :notes', - 'activity_145': 'EInvoice :invoice for :client was e-delivered. :notes', + 'activity_144': 'Auto Bill failed for invoice :invoice', + 'activity_145': 'E-Invoice :invoice for :client was sent', 'ssl_host_override': 'SSL Host Override', 'upload_logo_short': 'Upload Logo', 'show_pdfhtml_on_mobile_help': @@ -80,7 +83,7 @@ mixin LocalizationsProvider on LocaleCodeAware { 'round_down': 'Round Down', 'task_round_to_nearest': 'Round To Nearest', 'activity_139': 'Expense :expense notification sent to :contact', - 'activity_140': 'Statement sent to :client - :notes', + 'activity_140': 'Statement sent to :client', 'bulk_updated': 'Successfully updated data', 'bulk_update': 'Bulk Update', 'advanced_cards': 'Advanced Cards', @@ -2719,8 +2722,8 @@ mixin LocalizationsProvider on LocaleCodeAware { 'activity_141': 'User :user entered note: :notes', 'activity_142': 'Quote :number reminder 1 sent', 'activity_143': 'Auto Bill succeeded for invoice :invoice', - 'activity_144': 'Auto Bill failed for invoice :invoice. :notes', - 'activity_145': 'EInvoice :invoice for :client was e-delivered. :notes', + 'activity_144': 'Auto Bill failed for invoice :invoice', + 'activity_145': 'EInvoice :invoice for :client was e-delivered', 'ssl_host_override': 'SSL Host Override', 'upload_logo_short': 'Upload Logo', 'show_pdfhtml_on_mobile_help': From b8831d0cf0b93731405eb618c5123f990afe72f9 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 2 Dec 2024 16:32:11 +0200 Subject: [PATCH 12/15] Add merge action to vendors --- lib/data/models/vendor_model.dart | 4 + lib/data/repositories/vendor_repository.dart | 18 ++++ lib/redux/vendor/vendor_actions.dart | 102 ++++++++++++++++++- lib/redux/vendor/vendor_middleware.dart | 30 ++++++ lib/redux/vendor/vendor_reducer.dart | 8 ++ lib/ui/app/forms/vendor_picker.dart | 9 +- lib/utils/i18n.dart | 5 + 7 files changed, 172 insertions(+), 4 deletions(-) diff --git a/lib/data/models/vendor_model.dart b/lib/data/models/vendor_model.dart index 7f03311f41..7ac6e0f547 100644 --- a/lib/data/models/vendor_model.dart +++ b/lib/data/models/vendor_model.dart @@ -284,6 +284,10 @@ abstract class VendorEntity extends Object } */ + if (userCompany!.isAdmin && !multiselect) { + actions.add(EntityAction.merge); + } + if (actions.isNotEmpty && actions.last != null) { actions.add(null); } diff --git a/lib/data/repositories/vendor_repository.dart b/lib/data/repositories/vendor_repository.dart index 20ea0f8d34..3d864aa6d6 100644 --- a/lib/data/repositories/vendor_repository.dart +++ b/lib/data/repositories/vendor_repository.dart @@ -89,6 +89,24 @@ class VendorRepository { return vendorResponse.data; } + Future merge({ + required Credentials credentials, + required String? vendorId, + required String? mergeIntoVendorId, + required String? password, + required String? idToken, + }) async { + final url = credentials.url + '/vendors/$mergeIntoVendorId/$vendorId/merge'; + + final dynamic response = await webClient.post(url, credentials.token, + password: password, idToken: idToken); + + final VendorItemResponse clientResponse = + serializers.deserializeWith(VendorItemResponse.serializer, response)!; + + return clientResponse.data; + } + Future uploadDocument( Credentials credentials, BaseEntity entity, diff --git a/lib/redux/vendor/vendor_actions.dart b/lib/redux/vendor/vendor_actions.dart index e668ec1d69..c5583e9dce 100644 --- a/lib/redux/vendor/vendor_actions.dart +++ b/lib/redux/vendor/vendor_actions.dart @@ -16,6 +16,7 @@ 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/ui/app/forms/vendor_picker.dart'; import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/dialogs.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; @@ -175,6 +176,34 @@ class ArchiveVendorFailure implements StopSaving { final List vendors; } +class MergeVendorsRequest implements StartSaving { + MergeVendorsRequest({ + this.completer, + this.vendorId, + this.mergeIntoVendorId, + this.password, + this.idToken, + }); + + final Completer? completer; + final String? vendorId; + final String? mergeIntoVendorId; + final String? password; + final String? idToken; +} + +class MergeVendorsSuccess implements StopSaving, PersistData { + MergeVendorsSuccess(this.vendorId); + + final String? vendorId; +} + +class MergeVendorsFailure implements StopSaving { + MergeVendorsFailure(this.vendors); + + final List vendors; +} + class DeleteVendorRequest implements StartSaving { DeleteVendorRequest(this.completer, this.vendorIds); @@ -364,7 +393,14 @@ void handleVendorAction(BuildContext? context, List vendors, ), ); break; - + case EntityAction.merge: + showDialog( + context: context, + builder: (context) => _MergVendorPicker( + vendor: vendor, + ), + ); + break; case EntityAction.toggleMultiselect: if (!store.state.vendorListState.isInMultiselect()) { store.dispatch(StartVendorMultiselect()); @@ -473,3 +509,67 @@ class UpdateVendorTab implements PersistUI { final int? tabIndex; } + +class _MergVendorPicker extends StatefulWidget { + const _MergVendorPicker({ + Key? key, + required this.vendor, + }) : super(key: key); + + final VendorEntity? vendor; + + @override + State<_MergVendorPicker> createState() => __MergVendorPickerState(); +} + +class __MergVendorPickerState extends State<_MergVendorPicker> { + String? _mergeIntoVendorId; + + @override + Widget build(BuildContext context) { + final localization = AppLocalization.of(context)!; + final store = StoreProvider.of(context); + final state = store.state; + + return AlertDialog( + title: Text(localization.mergeInto), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + VendorPicker( + vendorId: _mergeIntoVendorId, + vendorState: state.vendorState, + excludeIds: [widget.vendor!.id], + onSelected: (vendor) => + setState(() => _mergeIntoVendorId = vendor?.id), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(localization.close), + ), + TextButton( + onPressed: () { + passwordCallback( + context: context, + callback: (password, idToken) { + store.dispatch(MergeVendorsRequest( + vendorId: widget.vendor!.id, + idToken: idToken, + password: password, + mergeIntoVendorId: _mergeIntoVendorId, + completer: snackBarCompleter( + localization.mergedVendors, + ), + )); + Navigator.of(context).pop(); + }); + }, + child: Text(localization.merge), + ), + ], + ); + } +} diff --git a/lib/redux/vendor/vendor_middleware.dart b/lib/redux/vendor/vendor_middleware.dart index f6696bf49d..e9cdefee41 100644 --- a/lib/redux/vendor/vendor_middleware.dart +++ b/lib/redux/vendor/vendor_middleware.dart @@ -29,6 +29,7 @@ List> createStoreVendorsMiddleware([ final loadVendors = _loadVendors(repository); final loadVendor = _loadVendor(repository); final saveVendor = _saveVendor(repository); + final mergeVendors = _mergeVendors(repository); final archiveVendor = _archiveVendor(repository); final deleteVendor = _deleteVendor(repository); final restoreVendor = _restoreVendor(repository); @@ -41,6 +42,7 @@ List> createStoreVendorsMiddleware([ TypedMiddleware(loadVendors), TypedMiddleware(loadVendor), TypedMiddleware(saveVendor), + TypedMiddleware(mergeVendors), TypedMiddleware(archiveVendor), TypedMiddleware(deleteVendor), TypedMiddleware(restoreVendor), @@ -118,6 +120,34 @@ Middleware _archiveVendor(VendorRepository repository) { }; } +Middleware _mergeVendors(VendorRepository repository) { + return (Store store, dynamic dynamicAction, NextDispatcher next) { + final action = dynamicAction as MergeVendorsRequest; + repository + .merge( + credentials: store.state.credentials, + vendorId: action.vendorId, + mergeIntoVendorId: action.mergeIntoVendorId, + idToken: action.idToken, + password: action.password, + ) + .then((client) { + store.dispatch(MergeVendorsSuccess(action.vendorId)); + store.dispatch(RefreshData()); + if (action.completer != null) { + action.completer!.complete(null); + } + }).catchError((Object error) { + store.dispatch(MergeVendorsFailure(error as List)); + if (action.completer != null) { + action.completer!.completeError(error); + } + }); + + next(action); + }; +} + Middleware _deleteVendor(VendorRepository repository) { return (Store store, dynamic dynamicAction, NextDispatcher next) { final action = dynamicAction as DeleteVendorRequest; diff --git a/lib/redux/vendor/vendor_reducer.dart b/lib/redux/vendor/vendor_reducer.dart index a9bfbbc4b8..dc588018ce 100644 --- a/lib/redux/vendor/vendor_reducer.dart +++ b/lib/redux/vendor/vendor_reducer.dart @@ -266,6 +266,7 @@ final vendorsReducer = combineReducers([ TypedReducer(_archiveVendorSuccess), TypedReducer(_deleteVendorSuccess), TypedReducer(_restoreVendorSuccess), + TypedReducer(_mergeVendorSuccess), ]); VendorState _archiveVendorSuccess( @@ -324,3 +325,10 @@ VendorState _setLoadedCompany( final company = action.userCompany.company; return vendorState.loadVendors(company.vendors); } + +VendorState _mergeVendorSuccess( + VendorState vendorState, MergeVendorsSuccess action) { + return vendorState.rebuild((b) => b + ..map.remove(action.vendorId) + ..list.remove(action.vendorId)); +} diff --git a/lib/ui/app/forms/vendor_picker.dart b/lib/ui/app/forms/vendor_picker.dart index 31696e2d12..b9cd18ea19 100644 --- a/lib/ui/app/forms/vendor_picker.dart +++ b/lib/ui/app/forms/vendor_picker.dart @@ -22,15 +22,17 @@ class VendorPicker extends StatelessWidget { required this.vendorId, required this.vendorState, required this.onSelected, - required this.onAddPressed, + this.onAddPressed, this.autofocus, + this.excludeIds = const [], }); - final String vendorId; + final String? vendorId; final VendorState vendorState; final Function(SelectableEntity?) onSelected; - final Function(Completer completer) onAddPressed; + final Function(Completer completer)? onAddPressed; final bool? autofocus; + final List excludeIds; @override Widget build(BuildContext context) { @@ -56,6 +58,7 @@ class VendorPicker extends StatelessWidget { vendor: VendorEntity().rebuild((b) => b..name = name), completer: completer)); }, + excludeIds: excludeIds, ); } } diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index c7a9d13629..a9d47b1c8f 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 + 'merged_vendors': 'Successfully merged vendors', 'activity_146': 'E-Invoice :invoice for :client was delivered', 'activity_147': 'E-Invoice :invoice for :client failed to send', 'activity_148': 'E-Exepnse :expense created', @@ -121185,6 +121186,10 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]!['ssl_host_override'] ?? _localizedValues['en']!['ssl_host_override']!; + String get mergedVendors => + _localizedValues[localeCode]!['merged_vendors'] ?? + _localizedValues['en']!['merged_vendors']!; + // STARTER: lang field - do not remove comment String lookup(String? key, {String? overrideLocaleCode}) { From f10d165d8ab025c0443392c4a55beb8391a22348 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 2 Dec 2024 16:33:26 +0200 Subject: [PATCH 13/15] Fix layout issue --- lib/ui/app/forms/client_picker.dart | 33 ++++++++++++----------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/lib/ui/app/forms/client_picker.dart b/lib/ui/app/forms/client_picker.dart index a842260f5f..78de6ff07a 100644 --- a/lib/ui/app/forms/client_picker.dart +++ b/lib/ui/app/forms/client_picker.dart @@ -41,25 +41,20 @@ class ClientPicker extends StatelessWidget { final store = StoreProvider.of(context); final state = store.state; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - EntityDropdown( - entityType: EntityType.client, - labelText: localization.client, - entityId: clientId, - autofocus: autofocus, - entityList: memoizedDropdownClientList(clientState.map, - clientState.list, state.userState.map, state.staticState), - entityMap: clientState.map, - validator: (String? val) => isRequired && (val ?? '').trim().isEmpty - ? AppLocalization.of(context)!.pleaseSelectAClient - : null, - onSelected: onSelected, - onAddPressed: onAddPressed, - excludeIds: excludeIds, - ), - ], + return EntityDropdown( + entityType: EntityType.client, + labelText: localization.client, + entityId: clientId, + autofocus: autofocus, + entityList: memoizedDropdownClientList(clientState.map, clientState.list, + state.userState.map, state.staticState), + entityMap: clientState.map, + validator: (String? val) => isRequired && (val ?? '').trim().isEmpty + ? AppLocalization.of(context)!.pleaseSelectAClient + : null, + onSelected: onSelected, + onAddPressed: onAddPressed, + excludeIds: excludeIds, ); } } From 97556900ceb7ad89e11201c83ef6c0b97c36cf31 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 2 Dec 2024 16:53:44 +0200 Subject: [PATCH 14/15] Auto-tax changes for the UK --- lib/constants.dart | 2 ++ lib/ui/settings/tax_settings.dart | 3 +++ 2 files changed, 5 insertions(+) diff --git a/lib/constants.dart b/lib/constants.dart index 6b8c92e2aa..f6d3ecdd71 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -303,6 +303,7 @@ const kEQuoteTypes = [ ]; const String kCountryUnitedStates = '840'; +const String kCountryUnitedKingdom = '826'; const String kCountryAustralia = '36'; const String kCountryCanada = '124'; const String kCountrySwitzerland = '756'; @@ -829,6 +830,7 @@ const String kDefaultLightBorderColor = '#dfdfdf'; const String kTaxRegionUnitedStates = 'US'; const String kTaxRegionEurope = 'EU'; const String kTaxRegionAustralia = 'AU'; +const String kTaxRegionUnitedKingdom = 'UK'; const String kReportGroupDay = 'day'; const String kReportGroupWeek = 'week'; diff --git a/lib/ui/settings/tax_settings.dart b/lib/ui/settings/tax_settings.dart index c1d9e3ee5a..947675fd98 100644 --- a/lib/ui/settings/tax_settings.dart +++ b/lib/ui/settings/tax_settings.dart @@ -41,6 +41,7 @@ class _TaxSettingsState extends State { kTaxRegionUnitedStates: false, kTaxRegionEurope: false, kTaxRegionAustralia: false, + kTaxRegionUnitedKingdom: false, }; @override @@ -71,6 +72,8 @@ class _TaxSettingsState extends State { region = kTaxRegionUnitedStates; } else if (company.settings.countryId == kCountryAustralia) { region = kTaxRegionAustralia; + } else if (company.settings.countryId == kCountryUnitedKingdom) { + region = kTaxRegionUnitedKingdom; } if (taxConfig.regions.containsKey(region)) { From 08511372fb4968574e6bff8f826a6d5375cb4209 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 3 Dec 2024 08:58:14 +0200 Subject: [PATCH 15/15] Add VAT number to auto-tax config --- lib/data/models/tax_model.dart | 7 ++++++- lib/data/models/tax_model.g.dart | 30 ++++++++++++++++++++++++++---- lib/ui/settings/tax_settings.dart | 12 +++++++++++- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/lib/data/models/tax_model.dart b/lib/data/models/tax_model.dart index 0a23b6707d..7205db9141 100644 --- a/lib/data/models/tax_model.dart +++ b/lib/data/models/tax_model.dart @@ -207,6 +207,7 @@ abstract class TaxConfigSubregionEntity taxRate: 0, reducedTaxRate: 0, taxName: '', + vatNumber: '', ); } @@ -228,13 +229,17 @@ abstract class TaxConfigSubregionEntity @BuiltValueField(wireName: 'reduced_tax_rate') double get reducedTaxRate; + @BuiltValueField(wireName: 'vat_number') + String get vatNumber; + // ignore: unused_element static void _initializeBuilder(TaxConfigSubregionEntityBuilder builder) => builder ..applyTax = false ..taxName = '' ..reducedTaxRate = 0 - ..taxRate = 0; + ..taxRate = 0 + ..vatNumber = ''; static Serializer get serializer => _$taxConfigSubregionEntitySerializer; diff --git a/lib/data/models/tax_model.g.dart b/lib/data/models/tax_model.g.dart index 939066bf64..e060cef775 100644 --- a/lib/data/models/tax_model.g.dart +++ b/lib/data/models/tax_model.g.dart @@ -287,6 +287,9 @@ class _$TaxConfigSubregionEntitySerializer 'reduced_tax_rate', serializers.serialize(object.reducedTaxRate, specifiedType: const FullType(double)), + 'vat_number', + serializers.serialize(object.vatNumber, + specifiedType: const FullType(String)), ]; return result; @@ -320,6 +323,10 @@ class _$TaxConfigSubregionEntitySerializer result.reducedTaxRate = serializers.deserialize(value, specifiedType: const FullType(double))! as double; break; + case 'vat_number': + result.vatNumber = serializers.deserialize(value, + specifiedType: const FullType(String))! as String; + break; } } @@ -870,6 +877,8 @@ class _$TaxConfigSubregionEntity extends TaxConfigSubregionEntity { final String taxName; @override final double reducedTaxRate; + @override + final String vatNumber; factory _$TaxConfigSubregionEntity( [void Function(TaxConfigSubregionEntityBuilder)? updates]) => @@ -879,7 +888,8 @@ class _$TaxConfigSubregionEntity extends TaxConfigSubregionEntity { {required this.applyTax, required this.taxRate, required this.taxName, - required this.reducedTaxRate}) + required this.reducedTaxRate, + required this.vatNumber}) : super._() { BuiltValueNullFieldError.checkNotNull( applyTax, r'TaxConfigSubregionEntity', 'applyTax'); @@ -889,6 +899,8 @@ class _$TaxConfigSubregionEntity extends TaxConfigSubregionEntity { taxName, r'TaxConfigSubregionEntity', 'taxName'); BuiltValueNullFieldError.checkNotNull( reducedTaxRate, r'TaxConfigSubregionEntity', 'reducedTaxRate'); + BuiltValueNullFieldError.checkNotNull( + vatNumber, r'TaxConfigSubregionEntity', 'vatNumber'); } @override @@ -907,7 +919,8 @@ class _$TaxConfigSubregionEntity extends TaxConfigSubregionEntity { applyTax == other.applyTax && taxRate == other.taxRate && taxName == other.taxName && - reducedTaxRate == other.reducedTaxRate; + reducedTaxRate == other.reducedTaxRate && + vatNumber == other.vatNumber; } int? __hashCode; @@ -919,6 +932,7 @@ class _$TaxConfigSubregionEntity extends TaxConfigSubregionEntity { _$hash = $jc(_$hash, taxRate.hashCode); _$hash = $jc(_$hash, taxName.hashCode); _$hash = $jc(_$hash, reducedTaxRate.hashCode); + _$hash = $jc(_$hash, vatNumber.hashCode); _$hash = $jf(_$hash); return __hashCode ??= _$hash; } @@ -929,7 +943,8 @@ class _$TaxConfigSubregionEntity extends TaxConfigSubregionEntity { ..add('applyTax', applyTax) ..add('taxRate', taxRate) ..add('taxName', taxName) - ..add('reducedTaxRate', reducedTaxRate)) + ..add('reducedTaxRate', reducedTaxRate) + ..add('vatNumber', vatNumber)) .toString(); } } @@ -956,6 +971,10 @@ class TaxConfigSubregionEntityBuilder set reducedTaxRate(double? reducedTaxRate) => _$this._reducedTaxRate = reducedTaxRate; + String? _vatNumber; + String? get vatNumber => _$this._vatNumber; + set vatNumber(String? vatNumber) => _$this._vatNumber = vatNumber; + TaxConfigSubregionEntityBuilder() { TaxConfigSubregionEntity._initializeBuilder(this); } @@ -967,6 +986,7 @@ class TaxConfigSubregionEntityBuilder _taxRate = $v.taxRate; _taxName = $v.taxName; _reducedTaxRate = $v.reducedTaxRate; + _vatNumber = $v.vatNumber; _$v = null; } return this; @@ -996,7 +1016,9 @@ class TaxConfigSubregionEntityBuilder taxName: BuiltValueNullFieldError.checkNotNull( taxName, r'TaxConfigSubregionEntity', 'taxName'), reducedTaxRate: BuiltValueNullFieldError.checkNotNull( - reducedTaxRate, r'TaxConfigSubregionEntity', 'reducedTaxRate')); + reducedTaxRate, r'TaxConfigSubregionEntity', 'reducedTaxRate'), + vatNumber: BuiltValueNullFieldError.checkNotNull( + vatNumber, r'TaxConfigSubregionEntity', 'vatNumber')); replace(_$result); return _$result; } diff --git a/lib/ui/settings/tax_settings.dart b/lib/ui/settings/tax_settings.dart index 947675fd98..c664d90ce2 100644 --- a/lib/ui/settings/tax_settings.dart +++ b/lib/ui/settings/tax_settings.dart @@ -395,6 +395,7 @@ class _EditSubregionDialog extends StatefulWidget { class __EditSubregionDialogState extends State<_EditSubregionDialog> { String _taxName = ''; + String _vatNumber = ''; double? _taxRate = 0; double? _reducedTaxRate = 0; @@ -406,6 +407,7 @@ class __EditSubregionDialogState extends State<_EditSubregionDialog> { _taxName = subregionConfig.taxName; _taxRate = subregionConfig.taxRate; _reducedTaxRate = subregionConfig.reducedTaxRate; + _vatNumber = subregionConfig.vatNumber; } void _onDone() { @@ -423,7 +425,8 @@ class __EditSubregionDialogState extends State<_EditSubregionDialog> { (b) => b ..taxName = _taxName ..taxRate = _taxRate - ..reducedTaxRate = _reducedTaxRate, + ..reducedTaxRate = _reducedTaxRate + ..vatNumber = _vatNumber, )))))); Navigator.of(context).pop(); @@ -472,6 +475,13 @@ class __EditSubregionDialogState extends State<_EditSubregionDialog> { onChanged: (value) => _reducedTaxRate = parseDouble(value), onSavePressed: (context) => _onDone(), ), + DecoratedFormField( + label: localization.vatNumber, + keyboardType: TextInputType.text, + initialValue: subregionData.vatNumber, + onChanged: (value) => _vatNumber = value.trim(), + onSavePressed: (context) => _onDone(), + ), ], )), );