From 2096975080758bbccb10f7d3dbc3f01ecd1f318f Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 7 Sep 2023 22:05:32 +0300 Subject: [PATCH 01/14] Adjust build --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0515cbb8c66..de67b99d350 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,7 +60,7 @@ jobs: rm ./public/index.html git add . git commit -m 'Admin Portal - Hosted' - #git push + git push cd .. #sentry-cli --auth-token ${{secrets.sentry_auth_token}} --url ${{secrets.sentry_url}} releases --project ${{secrets.sentry_project}} --org ${{secrets.sentry_org}} files $SENTRY_RELEASE upload-sourcemaps . --ext dart --rewrite From 685b0908af478857fbdf61693347dbf918ce481d Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Thu, 7 Sep 2023 22:13:59 +0300 Subject: [PATCH 02/14] Fix redirect for document company --- lib/redux/app/app_actions.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/redux/app/app_actions.dart b/lib/redux/app/app_actions.dart index 7f52c719eca..2390fe03bf3 100644 --- a/lib/redux/app/app_actions.dart +++ b/lib/redux/app/app_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:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/ui/bank_account/bank_account_screen.dart'; import 'package:invoiceninja_flutter/ui/schedule/schedule_screen.dart'; import 'package:invoiceninja_flutter/ui/transaction_rule/transaction_rule_screen.dart'; @@ -373,6 +374,7 @@ void viewEntitiesByType({ company: store.state.company, user: store.state.user, clearFilter: true, + section: kSettingsCompanyDetails, ); break; case EntityType.client: From 724d4ea2f8f39fc011220dfd1609711f26504ed8 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 11 Sep 2023 13:28:40 +0300 Subject: [PATCH 03/14] Use nulls in settings instead of empty strings --- lib/ui/settings/client_portal.dart | 66 +++++++++++++++---- lib/ui/settings/company_details.dart | 94 ++++++++++++++++++++-------- lib/ui/settings/email_settings.dart | 45 +++++++++---- 3 files changed, 156 insertions(+), 49 deletions(-) diff --git a/lib/ui/settings/client_portal.dart b/lib/ui/settings/client_portal.dart index f38fff9df39..4b57edbc663 100644 --- a/lib/ui/settings/client_portal.dart +++ b/lib/ui/settings/client_portal.dart @@ -189,26 +189,68 @@ class _ClientPortalState extends State void _onChanged() { _debouncer.run(() { + final portalDomain = _portalDomainController.text.trim(); + final subdomain = _subdomainController.text.trim(); + final customMessageDashboard = _customMessageDashboard.text.trim(); + final customMessageUnpaidInvoice = + _customMessageUnpaidInvoice.text.trim(); + final customMessagePaidInvoice = _customMessagePaidInvoice.text.trim(); + final customMessageUnapprovedQuote = + _customMessageUnapprovedQuote.text.trim(); + final clientPortalTerms = _termsController.text.trim(); + final clientPortalPrivacy = _privacyController.text.trim(); + final clientPortalCustomJs = _customJavaScriptController.text.trim(); + final clientPortalCustomCss = _customCssController.text.trim(); + final clientPortalCustomHeader = _customHeaderController.text.trim(); + final clientPortalCustomFooter = _customFooterController.text.trim(); + + final viewModel = widget.viewModel; + final isFiltered = viewModel.state.settingsUIState.isFiltered; + final company = widget.viewModel.company.rebuild((b) => b - ..portalDomain = _portalDomainController.text.trim() - ..subdomain = _subdomainController.text.trim()); + ..portalDomain = + isFiltered && portalDomain.isEmpty ? null : portalDomain + ..subdomain = isFiltered && subdomain.isEmpty ? null : subdomain); if (company != widget.viewModel.company) { widget.viewModel.onCompanyChanged(company); } final settings = widget.viewModel.settings.rebuild((b) => b - ..customMessageDashboard = _customMessageDashboard.text.trim() - ..customMessageUnpaidInvoice = _customMessageUnpaidInvoice.text.trim() - ..customMessagePaidInvoice = _customMessagePaidInvoice.text.trim() + ..customMessageDashboard = isFiltered && customMessageDashboard.isEmpty + ? null + : customMessageDashboard + ..customMessageUnpaidInvoice = + isFiltered && customMessageUnpaidInvoice.isEmpty + ? null + : customMessageUnpaidInvoice + ..customMessagePaidInvoice = + isFiltered && customMessagePaidInvoice.isEmpty + ? null + : customMessagePaidInvoice ..customMessageUnapprovedQuote = - _customMessageUnapprovedQuote.text.trim() - ..clientPortalTerms = _termsController.text.trim() - ..clientPortalPrivacy = _privacyController.text.trim() - ..clientPortalCustomJs = _customJavaScriptController.text.trim() - ..clientPortalCustomCss = _customCssController.text.trim() - ..clientPortalCustomHeader = _customHeaderController.text.trim() - ..clientPortalCustomFooter = _customFooterController.text.trim()); + isFiltered && customMessageUnapprovedQuote.isEmpty + ? null + : customMessageUnapprovedQuote + ..clientPortalTerms = + isFiltered && clientPortalTerms.isEmpty ? null : clientPortalTerms + ..clientPortalPrivacy = isFiltered && clientPortalPrivacy.isEmpty + ? null + : clientPortalPrivacy + ..clientPortalCustomJs = isFiltered && clientPortalCustomJs.isEmpty + ? null + : clientPortalCustomJs + ..clientPortalCustomCss = isFiltered && clientPortalCustomCss.isEmpty + ? null + : clientPortalCustomCss + ..clientPortalCustomHeader = + isFiltered && clientPortalCustomHeader.isEmpty + ? null + : clientPortalCustomHeader + ..clientPortalCustomFooter = + isFiltered && clientPortalCustomFooter.isEmpty + ? null + : clientPortalCustomFooter); if (settings != widget.viewModel.settings) { widget.viewModel.onSettingsChanged(settings); } diff --git a/lib/ui/settings/company_details.dart b/lib/ui/settings/company_details.dart index 315289dbc54..f4877557235 100644 --- a/lib/ui/settings/company_details.dart +++ b/lib/ui/settings/company_details.dart @@ -183,32 +183,74 @@ class _CompanyDetailsState extends State } void _onSettingsChanged() { - final settings = widget.viewModel.settings.rebuild((b) => b - ..name = _nameController.text.trim() - ..idNumber = _idNumberController.text.trim() - ..vatNumber = _vatNumberController.text.trim() - ..phone = _phoneController.text.trim() - ..email = _emailController.text.trim() - ..website = _websiteController.text.trim() - ..address1 = _address1Controller.text.trim() - ..address2 = _address2Controller.text.trim() - ..city = _cityController.text.trim() - ..state = _stateController.text.trim() - ..postalCode = _postalCodeController.text.trim() - ..customValue1 = _custom1Controller.text.trim() - ..customValue2 = _custom2Controller.text.trim() - ..customValue3 = _custom3Controller.text.trim() - ..customValue4 = _custom4Controller.text.trim() - ..defaultInvoiceFooter = _invoiceFooterController.text.trim() - ..defaultInvoiceTerms = _invoiceTermsController.text.trim() - ..defaultQuoteFooter = _quoteFooterController.text.trim() - ..defaultQuoteTerms = _quoteTermsController.text.trim() - ..defaultCreditFooter = _creditFooterController.text.trim() - ..defaultCreditTerms = _creditTermsController.text.trim() - ..defaultPurchaseOrderFooter = _purchaseOrderFooterController.text.trim() - ..defaultPurchaseOrderTerms = _purchaseOrderTermsController.text.trim() - ..qrIban = _qrIbanController.text.trim() - ..besrId = _besrIdController.text.trim()); + final name = _nameController.text.trim(); + final idNumber = _idNumberController.text.trim(); + final vatNumber = _vatNumberController.text.trim(); + final phone = _phoneController.text.trim(); + final email = _emailController.text.trim(); + final website = _websiteController.text.trim(); + final address1 = _address1Controller.text.trim(); + final address2 = _address2Controller.text.trim(); + final city = _cityController.text.trim(); + final state = _stateController.text.trim(); + final postalCode = _postalCodeController.text.trim(); + final customValue1 = _custom1Controller.text.trim(); + final customValue2 = _custom2Controller.text.trim(); + final customValue3 = _custom3Controller.text.trim(); + final customValue4 = _custom4Controller.text.trim(); + final defaultInvoiceFooter = _invoiceFooterController.text.trim(); + final defaultInvoiceTerms = _invoiceTermsController.text.trim(); + final defaultQuoteFooter = _quoteFooterController.text.trim(); + final defaultQuoteTerms = _quoteTermsController.text.trim(); + final defaultCreditFooter = _creditFooterController.text.trim(); + final defaultCreditTerms = _creditTermsController.text.trim(); + final defaultPurchaseOrderFooter = + _purchaseOrderFooterController.text.trim(); + final defaultPurchaseOrderTerms = _purchaseOrderTermsController.text.trim(); + final qrIban = _qrIbanController.text.trim(); + final besrId = _besrIdController.text.trim(); + + final viewModel = widget.viewModel; + final isFiltered = viewModel.state.settingsUIState.isFiltered; + final settings = viewModel.settings.rebuild((b) => b + ..name = isFiltered && name.isEmpty ? null : name + ..idNumber = isFiltered && idNumber.isEmpty ? null : idNumber + ..vatNumber = isFiltered && vatNumber.isEmpty ? null : vatNumber + ..phone = isFiltered && phone.isEmpty ? null : phone + ..email = isFiltered && email.isEmpty ? null : email + ..website = isFiltered && website.isEmpty ? null : website + ..address1 = isFiltered && address1.isEmpty ? null : address1 + ..address2 = isFiltered && address2.isEmpty ? null : address2 + ..city = isFiltered && city.isEmpty ? null : city + ..state = isFiltered && state.isEmpty ? null : state + ..postalCode = isFiltered && postalCode.isEmpty ? null : postalCode + ..customValue1 = isFiltered && customValue1.isEmpty ? null : customValue1 + ..customValue2 = isFiltered && customValue2.isEmpty ? null : customValue2 + ..customValue3 = isFiltered && customValue3.isEmpty ? null : customValue3 + ..customValue4 = isFiltered && customValue4.isEmpty ? null : customValue4 + ..defaultInvoiceFooter = isFiltered && defaultInvoiceFooter.isEmpty + ? null + : defaultInvoiceFooter + ..defaultInvoiceTerms = + isFiltered && defaultInvoiceTerms.isEmpty ? null : defaultInvoiceTerms + ..defaultQuoteFooter = + isFiltered && defaultQuoteFooter.isEmpty ? null : defaultQuoteFooter + ..defaultQuoteTerms = + isFiltered && defaultQuoteTerms.isEmpty ? null : defaultQuoteTerms + ..defaultCreditFooter = + isFiltered && defaultCreditFooter.isEmpty ? null : defaultCreditFooter + ..defaultCreditTerms = + isFiltered && defaultCreditTerms.isEmpty ? null : defaultCreditTerms + ..defaultPurchaseOrderFooter = + isFiltered && defaultPurchaseOrderFooter.isEmpty + ? null + : defaultPurchaseOrderFooter + ..defaultPurchaseOrderTerms = + isFiltered && defaultPurchaseOrderTerms.isEmpty + ? null + : defaultPurchaseOrderTerms + ..qrIban = isFiltered && qrIban.isEmpty ? null : qrIban + ..besrId = isFiltered && besrId.isEmpty ? null : besrId); if (settings != widget.viewModel.settings) { _debouncer.run(() { widget.viewModel.onSettingsChanged(settings); diff --git a/lib/ui/settings/email_settings.dart b/lib/ui/settings/email_settings.dart index 91d06244cc8..c06e780ff6a 100644 --- a/lib/ui/settings/email_settings.dart +++ b/lib/ui/settings/email_settings.dart @@ -116,25 +116,48 @@ class _EmailSettingsState extends State { } void _onChanged() { + final emailFromName = _fromNameController.text.trim(); + final replyToEmail = _replyToEmailController.text.trim(); + final replyToName = _replyToNameController.text.trim(); + final bccEmail = _bccEmailController.text.trim(); + final emailStyleCustom = _emailStyleCustomController.text.trim(); + final emailSignature = _emailSignatureController.text.trim(); + final postmarkSecret = _postmarkSecretController.text.trim(); + final mailgunSecret = _mailgunSecretController.text.trim(); + final mailgunDomain = _mailgunDomainController.text.trim(); + final customSendingEmail = _customSendingEmailController.text.trim(); + final eInvoiceCertificatePassphrase = + _eInvoiceCertificatePassphraseController.text.trim(); + final viewModel = widget.viewModel; + final isFiltered = viewModel.state.settingsUIState.isFiltered; final settings = viewModel.settings.rebuild((b) => b - ..emailFromName = _fromNameController.text.trim() - ..replyToEmail = _replyToEmailController.text.trim() - ..replyToName = _replyToNameController.text.trim() - ..bccEmail = _bccEmailController.text.trim() - ..emailStyleCustom = _emailStyleCustomController.text.trim() - ..emailSignature = _emailSignatureController.text.trim() - ..postmarkSecret = _postmarkSecretController.text.trim() - ..mailgunSecret = _mailgunSecretController.text.trim() - ..mailgunDomain = _mailgunDomainController.text.trim() - ..customSendingEmail = _customSendingEmailController.text.trim()); + ..emailFromName = + isFiltered && emailFromName.isEmpty ? null : emailFromName + ..replyToEmail = isFiltered && replyToEmail.isEmpty ? null : replyToEmail + ..replyToName = isFiltered && replyToName.isEmpty ? null : replyToName + ..bccEmail = isFiltered && bccEmail.isEmpty ? null : bccEmail + ..emailStyleCustom = + isFiltered && emailStyleCustom.isEmpty ? null : emailStyleCustom + ..emailSignature = + isFiltered && emailSignature.isEmpty ? null : emailSignature + ..postmarkSecret = + isFiltered && postmarkSecret.isEmpty ? null : postmarkSecret + ..mailgunSecret = + isFiltered && mailgunSecret.isEmpty ? null : mailgunSecret + ..mailgunDomain = + isFiltered && mailgunDomain.isEmpty ? null : mailgunDomain + ..customSendingEmail = + isFiltered && customSendingEmail.isEmpty ? null : customSendingEmail); if (settings != viewModel.settings) { viewModel.onSettingsChanged(settings); } final company = viewModel.company.rebuild((b) => b ..eInvoiceCertificatePassphrase = - _eInvoiceCertificatePassphraseController.text.trim()); + isFiltered && eInvoiceCertificatePassphrase.isEmpty + ? null + : eInvoiceCertificatePassphrase); if (company != viewModel.company) { viewModel.onCompanyChanged(company); } From 8ac4bed3e1d2470a3687d5eed1cf28226286ae48 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 11 Sep 2023 17:42:27 +0300 Subject: [PATCH 04/14] Add history to purchase orders --- lib/data/repositories/purchase_order_repository.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/data/repositories/purchase_order_repository.dart b/lib/data/repositories/purchase_order_repository.dart index d4c337d899c..9b35dac65f3 100644 --- a/lib/data/repositories/purchase_order_repository.dart +++ b/lib/data/repositories/purchase_order_repository.dart @@ -18,7 +18,8 @@ class PurchaseOrderRepository { Future loadItem( Credentials credentials, String entityId) async { final dynamic response = await webClient.get( - '${credentials.url}/purchase_orders/$entityId', credentials.token); + '${credentials.url}/purchase_orders/$entityId?include=activities.history', + credentials.token); final InvoiceItemResponse purchaseOrderResponse = serializers.deserializeWith(InvoiceItemResponse.serializer, response); @@ -86,10 +87,10 @@ class PurchaseOrderRepository { dynamic response; if (purchaseOrder.isNew) { - url = credentials.url + '/purchase_orders?include=activities'; + url = credentials.url + '/purchase_orders?include=activities.history'; } else { url = - '${credentials.url}/purchase_orders/${purchaseOrder.id}?include=activities'; + '${credentials.url}/purchase_orders/${purchaseOrder.id}?include=activities.history'; } if (action == EntityAction.markSent) { From 828a1e84c18b00aeac1a03e8488544f412ea8530 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 11 Sep 2023 22:00:02 +0300 Subject: [PATCH 05/14] Support adding tasks to quotes --- lib/ui/invoice/edit/invoice_edit_desktop.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/ui/invoice/edit/invoice_edit_desktop.dart b/lib/ui/invoice/edit/invoice_edit_desktop.dart index 4f466e5ba30..02de16a179e 100644 --- a/lib/ui/invoice/edit/invoice_edit_desktop.dart +++ b/lib/ui/invoice/edit/invoice_edit_desktop.dart @@ -250,7 +250,8 @@ class InvoiceEditDesktopState extends State .length; final showTasksTable = - invoice.hasTasks || (company.showTasksTable ?? false); + (invoice.hasTasks || (company.showTasksTable ?? false)) && + (invoice.isInvoice || invoice.isQuote); final settings = getClientSettings(state, client); final terms = entityType == EntityType.quote @@ -601,7 +602,7 @@ class InvoiceEditDesktopState extends State ), ], ), - if (invoice.isInvoice && showTasksTable) + if (showTasksTable) Padding( padding: const EdgeInsets.symmetric(horizontal: 18), child: AppTabBar( @@ -640,7 +641,7 @@ class InvoiceEditDesktopState extends State if (entityType == EntityType.credit) CreditEditItemsScreen( viewModel: widget.entityViewModel, - isTasks: _selectTasksTable && showTasksTable, + isTasks: _selectTasksTable, ) else if (entityType == EntityType.quote) QuoteEditItemsScreen( @@ -649,7 +650,7 @@ class InvoiceEditDesktopState extends State else if (entityType == EntityType.invoice) InvoiceEditItemsScreen( viewModel: widget.entityViewModel, - isTasks: _selectTasksTable && showTasksTable, + isTasks: _selectTasksTable, ) else if (entityType == EntityType.recurringInvoice) RecurringInvoiceEditItemsScreen( From 3613efa8545b21a2fc21f6354b8596e164c1cc63 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 11 Sep 2023 22:03:09 +0300 Subject: [PATCH 06/14] Support adding tasks to quotes --- lib/ui/invoice/edit/invoice_edit_desktop.dart | 1 + lib/ui/quote/edit/quote_edit_items_vm.dart | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/ui/invoice/edit/invoice_edit_desktop.dart b/lib/ui/invoice/edit/invoice_edit_desktop.dart index 02de16a179e..aaf4647bcba 100644 --- a/lib/ui/invoice/edit/invoice_edit_desktop.dart +++ b/lib/ui/invoice/edit/invoice_edit_desktop.dart @@ -646,6 +646,7 @@ class InvoiceEditDesktopState extends State else if (entityType == EntityType.quote) QuoteEditItemsScreen( viewModel: widget.entityViewModel, + isTasks: _selectTasksTable, ) else if (entityType == EntityType.invoice) InvoiceEditItemsScreen( diff --git a/lib/ui/quote/edit/quote_edit_items_vm.dart b/lib/ui/quote/edit/quote_edit_items_vm.dart index c0ed219cbd0..ace0f16c862 100644 --- a/lib/ui/quote/edit/quote_edit_items_vm.dart +++ b/lib/ui/quote/edit/quote_edit_items_vm.dart @@ -18,22 +18,24 @@ class QuoteEditItemsScreen extends StatelessWidget { const QuoteEditItemsScreen({ Key key, @required this.viewModel, + this.isTasks = false, }) : super(key: key); final AbstractInvoiceEditVM viewModel; + final bool isTasks; @override Widget build(BuildContext context) { return StoreConnector( converter: (Store store) { - return QuoteEditItemsVM.fromStore(store); + return QuoteEditItemsVM.fromStore(store, isTasks); }, builder: (context, viewModel) { if (viewModel.state.prefState.isEditorFullScreen(EntityType.invoice)) { return InvoiceEditItemsDesktop( viewModel: viewModel, entityViewModel: this.viewModel, - isTasks: false, + isTasks: isTasks, ); } else { return InvoiceEditItems( @@ -71,7 +73,10 @@ class QuoteEditItemsVM extends EntityEditItemsVM { onMovedInvoiceItem: onMovedInvoiceItem, ); - factory QuoteEditItemsVM.fromStore(Store store) { + factory QuoteEditItemsVM.fromStore( + Store store, + bool isTasks, + ) { return QuoteEditItemsVM( state: store.state, company: store.state.company, @@ -86,7 +91,11 @@ class QuoteEditItemsVM extends EntityEditItemsVM { onChangedInvoiceItem: (quoteItem, index) { final quote = store.state.quoteUIState.editing; if (index == quote.lineItems.length) { - store.dispatch(AddQuoteItem(quoteItem: quoteItem)); + store.dispatch(AddQuoteItem( + quoteItem: quoteItem.rebuild((b) => b + ..typeId = isTasks + ? InvoiceItemEntity.TYPE_TASK + : InvoiceItemEntity.TYPE_STANDARD))); } else { store.dispatch(UpdateQuoteItem(quoteItem: quoteItem, index: index)); } From 33b2f64b53e21d8129022041edfce12f245e997b Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 11 Sep 2023 22:51:12 +0300 Subject: [PATCH 07/14] Add tax classification --- lib/constants.dart | 8 +++++++ lib/data/models/client_model.dart | 7 +++++- lib/data/models/client_model.g.dart | 23 +++++++++++++++++++ lib/data/models/settings_model.dart | 3 +++ lib/data/models/settings_model.g.dart | 32 +++++++++++++++++++++++---- lib/data/models/vendor_model.dart | 7 +++++- lib/data/models/vendor_model.g.dart | 22 ++++++++++++++++++ 7 files changed, 96 insertions(+), 6 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index aa53bb88fa7..a41a00ff934 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -214,6 +214,14 @@ const kTaxCategories = { kTaxCategoryReverseTax: 'reverse_tax', }; +const String kTaxClassificationIndividual = 'individual'; +const String kTaxClassificationCompany = 'company'; +const String kTaxClassificationPartnership = 'partnership'; +const String kTaxClassificationTrust = 'trust'; +const String kTaxClassificationCharity = 'charity'; +const String kTaxClassificationGovernment = 'government'; +const String kTaxClassificationOther = 'other'; + const String kEInvoiceTypeEN16931 = 'EN16931'; const String kEInvoiceTypeXInvoice_2_2 = 'XInvoice_2_2'; const String kEInvoiceTypeXInvoice_2_1 = 'XInvoice_2_1'; diff --git a/lib/data/models/client_model.dart b/lib/data/models/client_model.dart index 4c2e72dabb9..6da12455423 100644 --- a/lib/data/models/client_model.dart +++ b/lib/data/models/client_model.dart @@ -107,6 +107,7 @@ class ClientFields { static const String group = 'group'; static const String routingId = 'routing_id'; static const String isTaxExempt = 'tax_exempt'; + static const String classification = 'classification'; } abstract class ClientEntity extends Object @@ -157,6 +158,7 @@ abstract class ClientEntity extends Object customValue4: '', routingId: '', isTaxExempt: false, + classification: '', taxData: TaxDataEntity(), contacts: BuiltList( [ @@ -318,6 +320,8 @@ abstract class ClientEntity extends Object @BuiltValueField(wireName: 'tax_info') TaxDataEntity get taxData; + String get classification; + BuiltList get contacts; @override @@ -781,7 +785,8 @@ abstract class ClientEntity extends Object ..routingId = '' ..isTaxExempt = false ..taxData.replace(TaxDataEntity()) - ..paymentBalance = 0; + ..paymentBalance = 0 + ..classification = kTaxClassificationCompany; static Serializer get serializer => _$clientEntitySerializer; } diff --git a/lib/data/models/client_model.g.dart b/lib/data/models/client_model.g.dart index d0653a97317..089136d973a 100644 --- a/lib/data/models/client_model.g.dart +++ b/lib/data/models/client_model.g.dart @@ -224,6 +224,9 @@ class _$ClientEntitySerializer implements StructuredSerializer { 'tax_info', serializers.serialize(object.taxData, specifiedType: const FullType(TaxDataEntity)), + 'classification', + serializers.serialize(object.classification, + specifiedType: const FullType(String)), 'contacts', serializers.serialize(object.contacts, specifiedType: const FullType( @@ -465,6 +468,10 @@ class _$ClientEntitySerializer implements StructuredSerializer { result.taxData.replace(serializers.deserialize(value, specifiedType: const FullType(TaxDataEntity)) as TaxDataEntity); break; + case 'classification': + result.classification = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; case 'contacts': result.contacts.replace(serializers.deserialize(value, specifiedType: const FullType( @@ -1024,6 +1031,8 @@ class _$ClientEntity extends ClientEntity { @override final TaxDataEntity taxData; @override + final String classification; + @override final BuiltList contacts; @override final BuiltList activities; @@ -1095,6 +1104,7 @@ class _$ClientEntity extends ClientEntity { this.routingId, this.isTaxExempt, this.taxData, + this.classification, this.contacts, this.activities, this.ledger, @@ -1176,6 +1186,8 @@ class _$ClientEntity extends ClientEntity { BuiltValueNullFieldError.checkNotNull( isTaxExempt, r'ClientEntity', 'isTaxExempt'); BuiltValueNullFieldError.checkNotNull(taxData, r'ClientEntity', 'taxData'); + BuiltValueNullFieldError.checkNotNull( + classification, r'ClientEntity', 'classification'); BuiltValueNullFieldError.checkNotNull( contacts, r'ClientEntity', 'contacts'); BuiltValueNullFieldError.checkNotNull( @@ -1245,6 +1257,7 @@ class _$ClientEntity extends ClientEntity { routingId == other.routingId && isTaxExempt == other.isTaxExempt && taxData == other.taxData && + classification == other.classification && contacts == other.contacts && activities == other.activities && ledger == other.ledger && @@ -1304,6 +1317,7 @@ class _$ClientEntity extends ClientEntity { _$hash = $jc(_$hash, routingId.hashCode); _$hash = $jc(_$hash, isTaxExempt.hashCode); _$hash = $jc(_$hash, taxData.hashCode); + _$hash = $jc(_$hash, classification.hashCode); _$hash = $jc(_$hash, contacts.hashCode); _$hash = $jc(_$hash, activities.hashCode); _$hash = $jc(_$hash, ledger.hashCode); @@ -1364,6 +1378,7 @@ class _$ClientEntity extends ClientEntity { ..add('routingId', routingId) ..add('isTaxExempt', isTaxExempt) ..add('taxData', taxData) + ..add('classification', classification) ..add('contacts', contacts) ..add('activities', activities) ..add('ledger', ledger) @@ -1551,6 +1566,11 @@ class ClientEntityBuilder _$this._taxData ??= new TaxDataEntityBuilder(); set taxData(TaxDataEntityBuilder taxData) => _$this._taxData = taxData; + String _classification; + String get classification => _$this._classification; + set classification(String classification) => + _$this._classification = classification; + ListBuilder _contacts; ListBuilder get contacts => _$this._contacts ??= new ListBuilder(); @@ -1666,6 +1686,7 @@ class ClientEntityBuilder _routingId = $v.routingId; _isTaxExempt = $v.isTaxExempt; _taxData = $v.taxData.toBuilder(); + _classification = $v.classification; _contacts = $v.contacts.toBuilder(); _activities = $v.activities.toBuilder(); _ledger = $v.ledger.toBuilder(); @@ -1751,6 +1772,7 @@ class ClientEntityBuilder routingId: BuiltValueNullFieldError.checkNotNull(routingId, r'ClientEntity', 'routingId'), isTaxExempt: BuiltValueNullFieldError.checkNotNull(isTaxExempt, r'ClientEntity', 'isTaxExempt'), taxData: taxData.build(), + classification: BuiltValueNullFieldError.checkNotNull(classification, r'ClientEntity', 'classification'), contacts: contacts.build(), activities: activities.build(), ledger: ledger.build(), @@ -1773,6 +1795,7 @@ class ClientEntityBuilder _$failedField = 'taxData'; taxData.build(); + _$failedField = 'contacts'; contacts.build(); _$failedField = 'activities'; diff --git a/lib/data/models/settings_model.dart b/lib/data/models/settings_model.dart index edc0620b2b3..4599ed6578c 100644 --- a/lib/data/models/settings_model.dart +++ b/lib/data/models/settings_model.dart @@ -1000,6 +1000,9 @@ abstract class SettingsEntity @BuiltValueField(wireName: 'default_expense_payment_type_id') String get defaultExpensePaymentTypeId; + @nullable + String get classification; + bool get hasAddress => address1 != null && address1.isNotEmpty; bool get hasLogo => companyLogo != null && companyLogo.isNotEmpty; diff --git a/lib/data/models/settings_model.g.dart b/lib/data/models/settings_model.g.dart index 9e0ee9a18f0..e1ee1a7ed58 100644 --- a/lib/data/models/settings_model.g.dart +++ b/lib/data/models/settings_model.g.dart @@ -1534,6 +1534,13 @@ class _$SettingsEntitySerializer ..add(serializers.serialize(value, specifiedType: const FullType(String))); } + value = object.classification; + if (value != null) { + result + ..add('classification') + ..add(serializers.serialize(value, + specifiedType: const FullType(String))); + } return result; } @@ -2426,6 +2433,10 @@ class _$SettingsEntitySerializer result.defaultExpensePaymentTypeId = serializers.deserialize(value, specifiedType: const FullType(String)) as String; break; + case 'classification': + result.classification = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; } } @@ -2940,6 +2951,8 @@ class _$SettingsEntity extends SettingsEntity { final String eInvoiceType; @override final String defaultExpensePaymentTypeId; + @override + final String classification; factory _$SettingsEntity([void Function(SettingsEntityBuilder) updates]) => (new SettingsEntityBuilder()..update(updates))._build(); @@ -3162,7 +3175,8 @@ class _$SettingsEntity extends SettingsEntity { this.showTaskItemDescription, this.enableEInvoice, this.eInvoiceType, - this.defaultExpensePaymentTypeId}) + this.defaultExpensePaymentTypeId, + this.classification}) : super._(); @override @@ -3399,7 +3413,8 @@ class _$SettingsEntity extends SettingsEntity { showTaskItemDescription == other.showTaskItemDescription && enableEInvoice == other.enableEInvoice && eInvoiceType == other.eInvoiceType && - defaultExpensePaymentTypeId == other.defaultExpensePaymentTypeId; + defaultExpensePaymentTypeId == other.defaultExpensePaymentTypeId && + classification == other.classification; } int __hashCode; @@ -3625,6 +3640,7 @@ class _$SettingsEntity extends SettingsEntity { _$hash = $jc(_$hash, enableEInvoice.hashCode); _$hash = $jc(_$hash, eInvoiceType.hashCode); _$hash = $jc(_$hash, defaultExpensePaymentTypeId.hashCode); + _$hash = $jc(_$hash, classification.hashCode); _$hash = $jf(_$hash); return __hashCode ??= _$hash; } @@ -3854,7 +3870,8 @@ class _$SettingsEntity extends SettingsEntity { ..add('showTaskItemDescription', showTaskItemDescription) ..add('enableEInvoice', enableEInvoice) ..add('eInvoiceType', eInvoiceType) - ..add('defaultExpensePaymentTypeId', defaultExpensePaymentTypeId)) + ..add('defaultExpensePaymentTypeId', defaultExpensePaymentTypeId) + ..add('classification', classification)) .toString(); } } @@ -4933,6 +4950,11 @@ class SettingsEntityBuilder set defaultExpensePaymentTypeId(String defaultExpensePaymentTypeId) => _$this._defaultExpensePaymentTypeId = defaultExpensePaymentTypeId; + String _classification; + String get classification => _$this._classification; + set classification(String classification) => + _$this._classification = classification; + SettingsEntityBuilder(); SettingsEntityBuilder get _$this { @@ -5156,6 +5178,7 @@ class SettingsEntityBuilder _enableEInvoice = $v.enableEInvoice; _eInvoiceType = $v.eInvoiceType; _defaultExpensePaymentTypeId = $v.defaultExpensePaymentTypeId; + _classification = $v.classification; _$v = null; } return this; @@ -5398,7 +5421,8 @@ class SettingsEntityBuilder showTaskItemDescription: showTaskItemDescription, enableEInvoice: enableEInvoice, eInvoiceType: eInvoiceType, - defaultExpensePaymentTypeId: defaultExpensePaymentTypeId); + defaultExpensePaymentTypeId: defaultExpensePaymentTypeId, + classification: classification); } catch (_) { String _$failedField; try { diff --git a/lib/data/models/vendor_model.dart b/lib/data/models/vendor_model.dart index 520baf1b6ea..a904e21f665 100644 --- a/lib/data/models/vendor_model.dart +++ b/lib/data/models/vendor_model.dart @@ -82,6 +82,7 @@ class VendorFields { static const String postalCity = 'postal_city'; static const String lastLoginAt = 'last_login_at'; static const String contactEmail = 'contact_email'; + static const String classification = 'classification'; } abstract class VendorEntity extends Object @@ -124,6 +125,7 @@ abstract class VendorEntity extends Object createdUserId: '', createdAt: 0, lastLogin: 0, + classification: '', documents: BuiltList(), ); } @@ -217,6 +219,8 @@ abstract class VendorEntity extends Object @BuiltValueField(wireName: 'last_login') int get lastLogin; + String get classification; + BuiltList get contacts; @override @@ -512,7 +516,8 @@ abstract class VendorEntity extends Object static void _initializeBuilder(VendorEntityBuilder builder) => builder ..activities.replace(BuiltList()) ..lastLogin = 0 - ..languageId = ''; + ..languageId = '' + ..classification = kTaxClassificationCompany; static Serializer get serializer => _$vendorEntitySerializer; } diff --git a/lib/data/models/vendor_model.g.dart b/lib/data/models/vendor_model.g.dart index c5609d8cc59..d775c7c2e9d 100644 --- a/lib/data/models/vendor_model.g.dart +++ b/lib/data/models/vendor_model.g.dart @@ -173,6 +173,9 @@ class _$VendorEntitySerializer implements StructuredSerializer { 'last_login', serializers.serialize(object.lastLogin, specifiedType: const FullType(int)), + 'classification', + serializers.serialize(object.classification, + specifiedType: const FullType(String)), 'contacts', serializers.serialize(object.contacts, specifiedType: const FullType( @@ -334,6 +337,10 @@ class _$VendorEntitySerializer implements StructuredSerializer { result.lastLogin = serializers.deserialize(value, specifiedType: const FullType(int)) as int; break; + case 'classification': + result.classification = serializers.deserialize(value, + specifiedType: const FullType(String)) as String; + break; case 'contacts': result.contacts.replace(serializers.deserialize(value, specifiedType: const FullType( @@ -820,6 +827,8 @@ class _$VendorEntity extends VendorEntity { @override final int lastLogin; @override + final String classification; + @override final BuiltList contacts; @override final BuiltList activities; @@ -868,6 +877,7 @@ class _$VendorEntity extends VendorEntity { this.customValue3, this.customValue4, this.lastLogin, + this.classification, this.contacts, this.activities, this.documents, @@ -916,6 +926,8 @@ class _$VendorEntity extends VendorEntity { customValue4, r'VendorEntity', 'customValue4'); BuiltValueNullFieldError.checkNotNull( lastLogin, r'VendorEntity', 'lastLogin'); + BuiltValueNullFieldError.checkNotNull( + classification, r'VendorEntity', 'classification'); BuiltValueNullFieldError.checkNotNull( contacts, r'VendorEntity', 'contacts'); BuiltValueNullFieldError.checkNotNull( @@ -963,6 +975,7 @@ class _$VendorEntity extends VendorEntity { customValue3 == other.customValue3 && customValue4 == other.customValue4 && lastLogin == other.lastLogin && + classification == other.classification && contacts == other.contacts && activities == other.activities && documents == other.documents && @@ -1002,6 +1015,7 @@ class _$VendorEntity extends VendorEntity { _$hash = $jc(_$hash, customValue3.hashCode); _$hash = $jc(_$hash, customValue4.hashCode); _$hash = $jc(_$hash, lastLogin.hashCode); + _$hash = $jc(_$hash, classification.hashCode); _$hash = $jc(_$hash, contacts.hashCode); _$hash = $jc(_$hash, activities.hashCode); _$hash = $jc(_$hash, documents.hashCode); @@ -1042,6 +1056,7 @@ class _$VendorEntity extends VendorEntity { ..add('customValue3', customValue3) ..add('customValue4', customValue4) ..add('lastLogin', lastLogin) + ..add('classification', classification) ..add('contacts', contacts) ..add('activities', activities) ..add('documents', documents) @@ -1149,6 +1164,11 @@ class VendorEntityBuilder int get lastLogin => _$this._lastLogin; set lastLogin(int lastLogin) => _$this._lastLogin = lastLogin; + String _classification; + String get classification => _$this._classification; + set classification(String classification) => + _$this._classification = classification; + ListBuilder _contacts; ListBuilder get contacts => _$this._contacts ??= new ListBuilder(); @@ -1230,6 +1250,7 @@ class VendorEntityBuilder _customValue3 = $v.customValue3; _customValue4 = $v.customValue4; _lastLogin = $v.lastLogin; + _classification = $v.classification; _contacts = $v.contacts.toBuilder(); _activities = $v.activities.toBuilder(); _documents = $v.documents.toBuilder(); @@ -1297,6 +1318,7 @@ class VendorEntityBuilder customValue3: BuiltValueNullFieldError.checkNotNull(customValue3, r'VendorEntity', 'customValue3'), customValue4: BuiltValueNullFieldError.checkNotNull(customValue4, r'VendorEntity', 'customValue4'), lastLogin: BuiltValueNullFieldError.checkNotNull(lastLogin, r'VendorEntity', 'lastLogin'), + classification: BuiltValueNullFieldError.checkNotNull(classification, r'VendorEntity', 'classification'), contacts: contacts.build(), activities: activities.build(), documents: documents.build(), From 2e7cdaf53d4583dc27390a0a599f50d2fe017c5d Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 12 Sep 2023 10:44:52 +0300 Subject: [PATCH 08/14] Add tax classification --- lib/constants.dart | 10 +++++++ lib/data/models/client_model.dart | 2 +- lib/data/models/vendor_model.dart | 2 +- lib/ui/client/edit/client_edit_details.dart | 16 ++++++++++++ lib/ui/settings/company_details.dart | 17 ++++++++++++ lib/ui/vendor/edit/vendor_edit_details.dart | 17 ++++++++++++ lib/utils/i18n.dart | 29 +++++++++++++++++++++ 7 files changed, 91 insertions(+), 2 deletions(-) diff --git a/lib/constants.dart b/lib/constants.dart index a41a00ff934..afc29d8c9b9 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -222,6 +222,16 @@ const String kTaxClassificationCharity = 'charity'; const String kTaxClassificationGovernment = 'government'; const String kTaxClassificationOther = 'other'; +const kTaxClassifications = [ + kTaxClassificationIndividual, + kTaxClassificationCompany, + kTaxClassificationPartnership, + kTaxClassificationTrust, + kTaxClassificationCharity, + kTaxClassificationGovernment, + kTaxClassificationOther, +]; + const String kEInvoiceTypeEN16931 = 'EN16931'; const String kEInvoiceTypeXInvoice_2_2 = 'XInvoice_2_2'; const String kEInvoiceTypeXInvoice_2_1 = 'XInvoice_2_1'; diff --git a/lib/data/models/client_model.dart b/lib/data/models/client_model.dart index 6da12455423..c6fa41ec163 100644 --- a/lib/data/models/client_model.dart +++ b/lib/data/models/client_model.dart @@ -786,7 +786,7 @@ abstract class ClientEntity extends Object ..isTaxExempt = false ..taxData.replace(TaxDataEntity()) ..paymentBalance = 0 - ..classification = kTaxClassificationCompany; + ..classification = ''; static Serializer get serializer => _$clientEntitySerializer; } diff --git a/lib/data/models/vendor_model.dart b/lib/data/models/vendor_model.dart index a904e21f665..9aca42627f8 100644 --- a/lib/data/models/vendor_model.dart +++ b/lib/data/models/vendor_model.dart @@ -517,7 +517,7 @@ abstract class VendorEntity extends Object ..activities.replace(BuiltList()) ..lastLogin = 0 ..languageId = '' - ..classification = kTaxClassificationCompany; + ..classification = ''; static Serializer get serializer => _$vendorEntitySerializer; } diff --git a/lib/ui/client/edit/client_edit_details.dart b/lib/ui/client/edit/client_edit_details.dart index ca5b7954857..60e048167dc 100644 --- a/lib/ui/client/edit/client_edit_details.dart +++ b/lib/ui/client/edit/client_edit_details.dart @@ -13,6 +13,7 @@ import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/static/static_selectors.dart'; import 'package:invoiceninja_flutter/ui/app/form_card.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/app_dropdown_button.dart'; import 'package:invoiceninja_flutter/ui/app/forms/custom_field.dart'; import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; import 'package:invoiceninja_flutter/ui/app/forms/dynamic_selector.dart'; @@ -295,6 +296,21 @@ class ClientEditDetailsState extends State { onSavePressed: _onSavePressed, ), if (state.company.calculateTaxes) ...[ + AppDropdownButton( + labelText: localization.classification, + showBlank: true, + value: client.classification, + onChanged: (dynamic value) { + viewModel.onChanged( + client.rebuild((b) => b..classification = value)); + }, + items: kTaxClassifications + .map((classification) => DropdownMenuItem( + child: Text(localization.lookup(classification)), + value: classification, + )) + .toList(), + ), SizedBox(height: 20), SwitchListTile( title: Text(localization.isTaxExempt), diff --git a/lib/ui/settings/company_details.dart b/lib/ui/settings/company_details.dart index f4877557235..44e41ad6acb 100644 --- a/lib/ui/settings/company_details.dart +++ b/lib/ui/settings/company_details.dart @@ -1,6 +1,7 @@ // Flutter imports: import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; @@ -378,6 +379,22 @@ class _CompanyDetailsState extends State ), ], ), + if (state.company.calculateTaxes) + AppDropdownButton( + labelText: localization.classification, + showBlank: true, + value: settings.classification, + onChanged: (dynamic value) { + viewModel.onSettingsChanged( + settings.rebuild((b) => b..classification = value)); + }, + items: kTaxClassifications + .map((classification) => DropdownMenuItem( + child: Text(localization.lookup(classification)), + value: classification, + )) + .toList(), + ), if (company.supportsQrIban) FormCard( children: [ diff --git a/lib/ui/vendor/edit/vendor_edit_details.dart b/lib/ui/vendor/edit/vendor_edit_details.dart index f278378e58b..02378ceba50 100644 --- a/lib/ui/vendor/edit/vendor_edit_details.dart +++ b/lib/ui/vendor/edit/vendor_edit_details.dart @@ -12,6 +12,7 @@ import 'package:contacts_service/contacts_service.dart'; import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/ui/app/form_card.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/app_dropdown_button.dart'; import 'package:invoiceninja_flutter/ui/app/forms/custom_field.dart'; import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; import 'package:invoiceninja_flutter/ui/app/forms/user_picker.dart'; @@ -273,6 +274,22 @@ class VendorEditDetailsState extends State { value: vendor.customValue4, onSavePressed: _onSavePressed, ), + if (state.company.calculateTaxes) + AppDropdownButton( + labelText: localization.classification, + showBlank: true, + value: vendor.classification, + onChanged: (dynamic value) { + viewModel.onChanged( + vendor.rebuild((b) => b..classification = value)); + }, + items: kTaxClassifications + .map((classification) => DropdownMenuItem( + child: Text(localization.lookup(classification)), + value: classification, + )) + .toList(), + ), ], ), ); diff --git a/lib/utils/i18n.dart b/lib/utils/i18n.dart index 7a20083df2b..8ec05c8d072 100644 --- a/lib/utils/i18n.dart +++ b/lib/utils/i18n.dart @@ -18,6 +18,12 @@ mixin LocalizationsProvider on LocaleCodeAware { static final Map> _localizedValues = { 'en': { // STARTER: lang key - do not remove comment + 'individual': 'Individual', + 'partnership': 'Partnership', + 'trust': 'Trust', + 'charity': 'Charity', + 'government': 'Government', + 'classification': 'Classification', 'click_or_drop_files_here': 'Click or drop files here', 'public': 'Public', 'private': 'Private', @@ -109398,6 +109404,29 @@ mixin LocalizationsProvider on LocaleCodeAware { _localizedValues[localeCode]['click_or_drop_files_here'] ?? _localizedValues['en']['click_or_drop_files_here']; + String get classification => + _localizedValues[localeCode]['classification'] ?? + _localizedValues['en']['classification']; + + String get individual => + _localizedValues[localeCode]['individual'] ?? + _localizedValues['en']['individual']; + + String get partnership => + _localizedValues[localeCode]['partnership'] ?? + _localizedValues['en']['partnership']; + + String get trust => + _localizedValues[localeCode]['trust'] ?? _localizedValues['en']['trust']; + + String get charity => + _localizedValues[localeCode]['charity'] ?? + _localizedValues['en']['charity']; + + String get government => + _localizedValues[localeCode]['government'] ?? + _localizedValues['en']['government']; + // STARTER: lang field - do not remove comment String lookup(String key) { From 89e133ed03e42fa1d990d8a0cc6aa2d1e4d0fcd3 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 12 Sep 2023 10:49:47 +0300 Subject: [PATCH 09/14] Add tax classification --- lib/data/models/client_model.dart | 3 +++ lib/data/models/vendor_model.dart | 3 +++ lib/ui/client/client_presenter.dart | 3 +++ lib/ui/vendor/vendor_presenter.dart | 5 +++++ 4 files changed, 14 insertions(+) diff --git a/lib/data/models/client_model.dart b/lib/data/models/client_model.dart index c6fa41ec163..019b4090ab4 100644 --- a/lib/data/models/client_model.dart +++ b/lib/data/models/client_model.dart @@ -556,6 +556,9 @@ abstract class ClientEntity extends Object case ClientFields.group: response = clientA.groupId.compareTo(clientB.groupId); break; + case ClientFields.classification: + response = clientA.classification.compareTo(clientB.classification); + break; default: print('## ERROR: sort by client.$sortField not implemented'); break; diff --git a/lib/data/models/vendor_model.dart b/lib/data/models/vendor_model.dart index 9aca42627f8..355306adaa1 100644 --- a/lib/data/models/vendor_model.dart +++ b/lib/data/models/vendor_model.dart @@ -375,6 +375,9 @@ abstract class VendorEntity extends Object case VendorFields.customValue4: response = vendorA.customValue4.compareTo(vendorB.customValue4); break; + case VendorFields.classification: + response = vendorA.classification.compareTo(vendorB.classification); + break; default: print('## ERROR: sort by vendor.$sortField is not implemented'); break; diff --git a/lib/ui/client/client_presenter.dart b/lib/ui/client/client_presenter.dart index 4987fec2d7a..c69d5a337dd 100644 --- a/lib/ui/client/client_presenter.dart +++ b/lib/ui/client/client_presenter.dart @@ -56,6 +56,7 @@ class ClientPresenter extends EntityPresenter { ClientFields.contacts, ClientFields.routingId, ClientFields.isTaxExempt, + if (userCompany.company.calculateTaxes) ClientFields.classification, ]; } @@ -157,6 +158,8 @@ class ClientPresenter extends EntityPresenter { final contacts = client.contacts.map((contact) => contact.fullName).join('\n'); return TableTooltip(message: contacts); + case ClientFields.classification: + return Text(localization.lookup(client.classification)); } return super.getField(field: field, context: context); diff --git a/lib/ui/vendor/vendor_presenter.dart b/lib/ui/vendor/vendor_presenter.dart index c179aaac316..3c3238317ae 100644 --- a/lib/ui/vendor/vendor_presenter.dart +++ b/lib/ui/vendor/vendor_presenter.dart @@ -10,6 +10,7 @@ import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/ui/app/copy_to_clipboard.dart'; import 'package:invoiceninja_flutter/ui/app/presenters/entity_presenter.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:url_launcher/url_launcher.dart'; class VendorPresenter extends EntityPresenter { @@ -49,12 +50,14 @@ class VendorPresenter extends EntityPresenter { VendorFields.archivedAt, VendorFields.documents, VendorFields.contacts, + if (userCompany.company.calculateTaxes) VendorFields.classification, ]; } @override Widget getField({String field, BuildContext context}) { final vendor = entity as VendorEntity; + final localization = AppLocalization.of(context); final store = StoreProvider.of(context); final state = store.state; @@ -121,6 +124,8 @@ class VendorPresenter extends EntityPresenter { ? '' : formatDate( convertTimestampToDateString(vendor.lastLogin), context)); + case VendorFields.classification: + return Text(localization.lookup(vendor.classification)); } return super.getField(field: field, context: context); From c36411668ee0ce8f6f15e0f8041e4d6161c8bb42 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 12 Sep 2023 10:57:38 +0300 Subject: [PATCH 10/14] Add tax classification --- lib/ui/client/client_presenter.dart | 10 +++++++--- lib/ui/reports/client_report.dart | 7 +++++++ lib/ui/reports/vendor_report.dart | 8 ++++++++ lib/ui/vendor/vendor_presenter.dart | 4 +++- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/ui/client/client_presenter.dart b/lib/ui/client/client_presenter.dart index c69d5a337dd..af29d2baa5b 100644 --- a/lib/ui/client/client_presenter.dart +++ b/lib/ui/client/client_presenter.dart @@ -54,9 +54,13 @@ class ClientPresenter extends EntityPresenter { ClientFields.group, ClientFields.contactPhone, ClientFields.contacts, - ClientFields.routingId, - ClientFields.isTaxExempt, - if (userCompany.company.calculateTaxes) ClientFields.classification, + if (userCompany.company.settings.enableEInvoice) ...[ + ClientFields.routingId, + ], + if (userCompany.company.calculateTaxes) ...[ + ClientFields.isTaxExempt, + ClientFields.classification, + ] ]; } diff --git a/lib/ui/reports/client_report.dart b/lib/ui/reports/client_report.dart index 3f6d31a89bd..def5f12ef85 100644 --- a/lib/ui/reports/client_report.dart +++ b/lib/ui/reports/client_report.dart @@ -1,7 +1,9 @@ // Package imports: import 'package:built_collection/built_collection.dart'; import 'package:invoiceninja_flutter/data/models/group_model.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; import 'package:invoiceninja_flutter/redux/reports/reports_selectors.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:memoize/memoize.dart'; // Project imports: @@ -74,6 +76,7 @@ enum ClientReportFields { documents, routing_id, tax_exempt, + classification, } var memoizedClientReport = memo6(( @@ -359,6 +362,10 @@ ReportResult clientReport( case ClientReportFields.tax_exempt: value = client.isTaxExempt; break; + case ClientReportFields.classification: + value = AppLocalization.of(navigatorKey.currentContext) + .lookup(client.classification); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/reports/vendor_report.dart b/lib/ui/reports/vendor_report.dart index bd1f7acee55..4b90d79d561 100644 --- a/lib/ui/reports/vendor_report.dart +++ b/lib/ui/reports/vendor_report.dart @@ -1,7 +1,9 @@ // Package imports: import 'package:built_collection/built_collection.dart'; import 'package:invoiceninja_flutter/data/models/group_model.dart'; +import 'package:invoiceninja_flutter/main_app.dart'; import 'package:invoiceninja_flutter/redux/reports/reports_selectors.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:memoize/memoize.dart'; // Project imports: @@ -52,6 +54,7 @@ enum VendorReportFields { updated_at, documents, last_login, + classification, /* contact_last_login, shipping_address1, @@ -315,6 +318,11 @@ ReportResult vendorReport( break; case VendorReportFields.documents: value = vendor.documents.length; + break; + case VendorReportFields.classification: + value = AppLocalization.of(navigatorKey.currentContext) + .lookup(vendor.classification); + break; } if (!ReportResult.matchField( diff --git a/lib/ui/vendor/vendor_presenter.dart b/lib/ui/vendor/vendor_presenter.dart index 3c3238317ae..2343a5e45c1 100644 --- a/lib/ui/vendor/vendor_presenter.dart +++ b/lib/ui/vendor/vendor_presenter.dart @@ -50,7 +50,9 @@ class VendorPresenter extends EntityPresenter { VendorFields.archivedAt, VendorFields.documents, VendorFields.contacts, - if (userCompany.company.calculateTaxes) VendorFields.classification, + if (userCompany.company.calculateTaxes) ...[ + VendorFields.classification, + ], ]; } From 516a7c1476c35010301e553ff05511459f0ee827 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 12 Sep 2023 12:55:54 +0300 Subject: [PATCH 11/14] Fix for logo CORS issue --- lib/ui/app/menu_drawer.dart | 2 ++ lib/ui/app/resources/cached_image.dart | 13 +++++++++++-- lib/ui/settings/company_details.dart | 2 ++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/ui/app/menu_drawer.dart b/lib/ui/app/menu_drawer.dart index c9a15464e44..22a92caa26b 100644 --- a/lib/ui/app/menu_drawer.dart +++ b/lib/ui/app/menu_drawer.dart @@ -87,7 +87,9 @@ class _MenuDrawerState extends State { company.settings.companyLogo.isNotEmpty ? CachedImage( width: MenuDrawer.LOGO_WIDTH, + //url: state.credentials.url + '/companies/' + company.id + '/logo', url: company.settings.companyLogo, + sendApiToken: true, ) : Image.asset('assets/images/icon.png', width: MenuDrawer.LOGO_WIDTH); diff --git a/lib/ui/app/resources/cached_image.dart b/lib/ui/app/resources/cached_image.dart index 5ba1a6f3928..2869de0657a 100644 --- a/lib/ui/app/resources/cached_image.dart +++ b/lib/ui/app/resources/cached_image.dart @@ -10,13 +10,19 @@ import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; class CachedImage extends StatelessWidget { - const CachedImage( - {this.url, this.width, this.height, this.showNinjaOnError = true}); + const CachedImage({ + this.url, + this.width, + this.height, + this.showNinjaOnError = true, + this.sendApiToken = false, + }); final String url; final bool showNinjaOnError; final double width; final double height; + final bool sendApiToken; @override Widget build(BuildContext context) { @@ -39,6 +45,7 @@ class CachedImage extends StatelessWidget { height: height, key: ValueKey(url), fit: BoxFit.contain, + headers: sendApiToken ? {'X-API-TOKEN': state.credentials.token} : null, ); } @@ -50,6 +57,8 @@ class CachedImage extends StatelessWidget { placeholder: (context, url) => Center(child: CircularProgressIndicator()), errorWidget: (context, url, Object error) => Image.asset('assets/images/icon.png', width: 32, height: 30), + httpHeaders: + sendApiToken ? {'X-API-TOKEN': state.credentials.token} : null, ); } } diff --git a/lib/ui/settings/company_details.dart b/lib/ui/settings/company_details.dart index 44e41ad6acb..e5ec3b3d467 100644 --- a/lib/ui/settings/company_details.dart +++ b/lib/ui/settings/company_details.dart @@ -574,7 +574,9 @@ class _CompanyDetailsState extends State padding: const EdgeInsets.symmetric(vertical: 20), child: CachedImage( width: double.infinity, + //url: state.credentials.url + '/companies/' + company.id + '/logo', url: settings.companyLogo, + sendApiToken: true, )), ], ), From 1fd5f380f96ee29c18b9f777a9f4686aefbdac8a Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 12 Sep 2023 13:17:39 +0300 Subject: [PATCH 12/14] Fix for logo CORS issue --- lib/ui/app/menu_drawer.dart | 9 ++++++--- lib/ui/app/resources/cached_image.dart | 13 ++++++------- lib/ui/settings/company_details.dart | 8 +++++--- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/ui/app/menu_drawer.dart b/lib/ui/app/menu_drawer.dart index 22a92caa26b..c9831905212 100644 --- a/lib/ui/app/menu_drawer.dart +++ b/lib/ui/app/menu_drawer.dart @@ -87,9 +87,12 @@ class _MenuDrawerState extends State { company.settings.companyLogo.isNotEmpty ? CachedImage( width: MenuDrawer.LOGO_WIDTH, - //url: state.credentials.url + '/companies/' + company.id + '/logo', - url: company.settings.companyLogo, - sendApiToken: true, + url: state.credentials.url + '/companies/' + company.id + '/logo', + apiToken: state.userCompanyStates + .firstWhere((userCompanyState) => + userCompanyState.company.id == company.id) + .token + .token, ) : Image.asset('assets/images/icon.png', width: MenuDrawer.LOGO_WIDTH); diff --git a/lib/ui/app/resources/cached_image.dart b/lib/ui/app/resources/cached_image.dart index 2869de0657a..e1405040c64 100644 --- a/lib/ui/app/resources/cached_image.dart +++ b/lib/ui/app/resources/cached_image.dart @@ -15,14 +15,14 @@ class CachedImage extends StatelessWidget { this.width, this.height, this.showNinjaOnError = true, - this.sendApiToken = false, + this.apiToken, }); final String url; final bool showNinjaOnError; final double width; final double height; - final bool sendApiToken; + final String apiToken; @override Widget build(BuildContext context) { @@ -43,22 +43,21 @@ class CachedImage extends StatelessWidget { url, width: width, height: height, - key: ValueKey(url), + key: ValueKey(url + (apiToken != null ? apiToken.substring(0, 8) : '')), fit: BoxFit.contain, - headers: sendApiToken ? {'X-API-TOKEN': state.credentials.token} : null, + headers: apiToken != null ? {'X-API-TOKEN': apiToken} : null, ); } return CachedNetworkImage( width: width, height: height, - key: ValueKey(url), + key: ValueKey(url + (apiToken != null ? apiToken.substring(0, 8) : '')), imageUrl: url, placeholder: (context, url) => Center(child: CircularProgressIndicator()), errorWidget: (context, url, Object error) => Image.asset('assets/images/icon.png', width: 32, height: 30), - httpHeaders: - sendApiToken ? {'X-API-TOKEN': state.credentials.token} : null, + httpHeaders: apiToken != null ? {'X-API-TOKEN': apiToken} : null, ); } } diff --git a/lib/ui/settings/company_details.dart b/lib/ui/settings/company_details.dart index e5ec3b3d467..92bcafad0cf 100644 --- a/lib/ui/settings/company_details.dart +++ b/lib/ui/settings/company_details.dart @@ -574,9 +574,11 @@ class _CompanyDetailsState extends State padding: const EdgeInsets.symmetric(vertical: 20), child: CachedImage( width: double.infinity, - //url: state.credentials.url + '/companies/' + company.id + '/logo', - url: settings.companyLogo, - sendApiToken: true, + url: state.credentials.url + + '/companies/' + + company.id + + '/logo', + apiToken: state.userCompany.token.token, )), ], ), From 2cf69ca74584232bcea796aa757e474ee5093325 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 12 Sep 2023 13:19:57 +0300 Subject: [PATCH 13/14] Fix for health check --- lib/ui/app/dialogs/health_check_dialog.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ui/app/dialogs/health_check_dialog.dart b/lib/ui/app/dialogs/health_check_dialog.dart index c65b9dcb364..c396684caa9 100644 --- a/lib/ui/app/dialogs/health_check_dialog.dart +++ b/lib/ui/app/dialogs/health_check_dialog.dart @@ -148,8 +148,8 @@ class _HealthCheckDialogState extends State { title: 'PHP Info', // TODO move this logic to the backend isValid: _response.phpVersion.isOkay && - webPhpVersion.startsWith('8') && - cliPhpVersion.startsWith('8'), + webPhpVersion.startsWith('v8') && + cliPhpVersion.startsWith('v8'), subtitle: 'Web: $webPhpVersion\nCLI: $cliPhpVersion' + (phpMemoryLimit.isNotEmpty ? '\nMemory Limit: $phpMemoryLimit' From ad29f2cec34e5e3129bcdd0e924f7548f660d893 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Tue, 12 Sep 2023 13:20:59 +0300 Subject: [PATCH 14/14] Disable hosted build --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index de67b99d350..0515cbb8c66 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,7 +60,7 @@ jobs: rm ./public/index.html git add . git commit -m 'Admin Portal - Hosted' - git push + #git push cd .. #sentry-cli --auth-token ${{secrets.sentry_auth_token}} --url ${{secrets.sentry_url}} releases --project ${{secrets.sentry_project}} --org ${{secrets.sentry_org}} files $SENTRY_RELEASE upload-sourcemaps . --ext dart --rewrite