Support group/client default rates

This commit is contained in:
Hillel Coren 2021-10-25 18:27:18 +03:00
parent 9a417c41be
commit 549a546b9e
12 changed files with 145 additions and 110 deletions

View File

@ -9,6 +9,7 @@ import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/data/models/quote_model.dart'; import 'package:invoiceninja_flutter/data/models/quote_model.dart';
import 'package:invoiceninja_flutter/data/models/recurring_invoice_model.dart'; import 'package:invoiceninja_flutter/data/models/recurring_invoice_model.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/redux/client/client_selectors.dart';
import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart';
import 'package:invoiceninja_flutter/utils/money.dart'; import 'package:invoiceninja_flutter/utils/money.dart';
import 'package:invoiceninja_flutter/utils/strings.dart'; import 'package:invoiceninja_flutter/utils/strings.dart';
@ -119,6 +120,8 @@ abstract class InvoiceEntity extends Object
EntityType entityType, EntityType entityType,
}) { }) {
final company = state?.company; final company = state?.company;
final settings = getClientSettings(state, client);
double exchangeRate = 1; double exchangeRate = 1;
if ((client?.currencyId ?? '').isNotEmpty) { if ((client?.currencyId ?? '').isNotEmpty) {
exchangeRate = getExchangeRate( exchangeRate = getExchangeRate(
@ -148,22 +151,22 @@ abstract class InvoiceEntity extends Object
footer: '', footer: '',
designId: '', designId: '',
taxName1: (company?.numberOfInvoiceTaxRates ?? 0) >= 1 taxName1: (company?.numberOfInvoiceTaxRates ?? 0) >= 1
? company?.settings?.defaultTaxName1 ?? '' ? settings.defaultTaxName1 ?? ''
: '', : '',
taxRate1: (company?.numberOfInvoiceTaxRates ?? 0) >= 1 taxRate1: (company?.numberOfInvoiceTaxRates ?? 0) >= 1
? company?.settings?.defaultTaxRate1 ?? 0.0 ? settings.defaultTaxRate1 ?? 0.0
: 0, : 0,
taxName2: (company?.numberOfInvoiceTaxRates ?? 0) >= 2 taxName2: (company?.numberOfInvoiceTaxRates ?? 0) >= 2
? company?.settings?.defaultTaxName2 ?? '' ? settings.defaultTaxName2 ?? ''
: '', : '',
taxRate2: (company?.numberOfInvoiceTaxRates ?? 0) >= 2 taxRate2: (company?.numberOfInvoiceTaxRates ?? 0) >= 2
? company?.settings?.defaultTaxRate2 ?? 0.0 ? settings.defaultTaxRate2 ?? 0.0
: 0, : 0,
taxName3: (company?.numberOfInvoiceTaxRates ?? 0) >= 3 taxName3: (company?.numberOfInvoiceTaxRates ?? 0) >= 3
? company?.settings?.defaultTaxName3 ?? '' ? settings.defaultTaxName3 ?? ''
: '', : '',
taxRate3: (company?.numberOfInvoiceTaxRates ?? 0) >= 3 taxRate3: (company?.numberOfInvoiceTaxRates ?? 0) >= 3
? company?.settings?.defaultTaxRate3 ?? 0.0 ? settings.defaultTaxRate3 ?? 0.0
: 0, : 0,
isAmountDiscount: false, isAmountDiscount: false,
partial: 0.0, partial: 0.0,

View File

@ -59,6 +59,24 @@ abstract class SettingsEntity
defaultValidUntil: clientSettings?.defaultValidUntil ?? defaultValidUntil: clientSettings?.defaultValidUntil ??
groupSettings?.defaultValidUntil ?? groupSettings?.defaultValidUntil ??
companySettings?.defaultValidUntil, companySettings?.defaultValidUntil,
defaultTaxRate1: clientSettings?.defaultTaxRate1 ??
groupSettings?.defaultTaxRate1 ??
companySettings?.defaultTaxRate1,
defaultTaxName1: clientSettings?.defaultTaxName1 ??
groupSettings?.defaultTaxName1 ??
companySettings?.defaultTaxName1,
defaultTaxRate2: clientSettings?.defaultTaxRate2 ??
groupSettings?.defaultTaxRate2 ??
companySettings?.defaultTaxRate2,
defaultTaxName2: clientSettings?.defaultTaxName2 ??
groupSettings?.defaultTaxName2 ??
companySettings?.defaultTaxName2,
defaultTaxRate3: clientSettings?.defaultTaxRate3 ??
groupSettings?.defaultTaxRate3 ??
companySettings?.defaultTaxRate3,
defaultTaxName3: clientSettings?.defaultTaxName3 ??
groupSettings?.defaultTaxName3 ??
companySettings?.defaultTaxName3,
); );
} }

View File

@ -13,6 +13,7 @@ import 'package:invoiceninja_flutter/data/models/static/static_data_model.dart';
import 'package:invoiceninja_flutter/main_app.dart'; import 'package:invoiceninja_flutter/main_app.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/redux/client/client_actions.dart'; import 'package:invoiceninja_flutter/redux/client/client_actions.dart';
import 'package:invoiceninja_flutter/redux/client/client_selectors.dart';
import 'package:invoiceninja_flutter/redux/company_gateway/company_gateway_actions.dart'; import 'package:invoiceninja_flutter/redux/company_gateway/company_gateway_actions.dart';
import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart'; import 'package:invoiceninja_flutter/redux/credit/credit_actions.dart';
import 'package:invoiceninja_flutter/redux/dashboard/dashboard_actions.dart'; import 'package:invoiceninja_flutter/redux/dashboard/dashboard_actions.dart';
@ -1110,11 +1111,7 @@ void editEntity({
case EntityType.invoice: case EntityType.invoice:
final invoice = entity as InvoiceEntity; final invoice = entity as InvoiceEntity;
final client = state.clientState.get(invoice.clientId); final client = state.clientState.get(invoice.clientId);
final settings = SettingsEntity( final settings = getClientSettings(state, client);
clientSettings: client.settings,
groupSettings: state.groupState.get(client.groupId).settings,
companySettings: state.company.settings,
);
if (settings.lockInvoices == SettingsEntity.LOCK_INVOICES_PAID && if (settings.lockInvoices == SettingsEntity.LOCK_INVOICES_PAID &&
invoice.isPaid) { invoice.isPaid) {

View File

@ -127,6 +127,22 @@ List<String> filteredClientsSelector(
return list; return list;
} }
SettingsEntity getClientSettings(AppState state, ClientEntity client) {
if (state == null) {
return SettingsEntity();
}
client ??= ClientEntity();
final company = state.company;
final group = state.groupState.get(client.groupId);
return SettingsEntity(
clientSettings: client.settings,
groupSettings: group.settings,
companySettings: company.settings,
);
}
bool hasClientChanges( bool hasClientChanges(
ClientEntity client, BuiltMap<String, ClientEntity> clientMap) => ClientEntity client, BuiltMap<String, ClientEntity> clientMap) =>
client.isNew ? client.isChanged : client != clientMap[client.id]; client.isNew ? client.isChanged : client != clientMap[client.id];

View File

@ -1,5 +1,6 @@
import 'package:invoiceninja_flutter/data/models/design_model.dart'; import 'package:invoiceninja_flutter/data/models/design_model.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/redux/client/client_selectors.dart';
import 'package:memoize/memoize.dart'; import 'package:memoize/memoize.dart';
import 'package:built_collection/built_collection.dart'; import 'package:built_collection/built_collection.dart';
import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/data/models/models.dart';
@ -68,12 +69,7 @@ bool hasDesignChanges(
String getDesignIdForClientByEntity( String getDesignIdForClientByEntity(
{AppState state, String clientId, EntityType entityType}) { {AppState state, String clientId, EntityType entityType}) {
final client = state.clientState.get(clientId); final client = state.clientState.get(clientId);
final group = state.groupState.get(client.groupId); final settings = getClientSettings(state, client);
final settings = SettingsEntity(
clientSettings: client.settings,
groupSettings: group.settings,
companySettings: state.company.settings,
);
switch (entityType) { switch (entityType) {
case EntityType.invoice: case EntityType.invoice:
return settings.defaultInvoiceDesignId; return settings.defaultInvoiceDesignId;

View File

@ -2,6 +2,7 @@ import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/data/models/entities.dart'; import 'package:invoiceninja_flutter/data/models/entities.dart';
import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart';
import 'package:invoiceninja_flutter/redux/client/client_selectors.dart';
import 'package:invoiceninja_flutter/ui/app/form_card.dart'; import 'package:invoiceninja_flutter/ui/app/form_card.dart';
import 'package:invoiceninja_flutter/ui/app/forms/app_tab_bar.dart'; import 'package:invoiceninja_flutter/ui/app/forms/app_tab_bar.dart';
import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart';
@ -159,11 +160,7 @@ class _InvoiceEmailViewState extends State<InvoiceEmailView>
final invoice = widget.viewModel.invoice; final invoice = widget.viewModel.invoice;
final client = viewModel.client; final client = viewModel.client;
final state = viewModel.state; final state = viewModel.state;
final settings = SettingsEntity( final settings = getClientSettings(state, client);
clientSettings: client.settings,
groupSettings: state.groupState.get(client.groupId).settings,
companySettings: state.company.settings,
);
final contacts = invoice.invitations final contacts = invoice.invitations
.map((invitation) => client.contacts.firstWhere( .map((invitation) => client.contacts.firstWhere(
(contact) => contact.id == invitation.contactId, (contact) => contact.id == invitation.contactId,

View File

@ -6,6 +6,7 @@ import 'package:flutter_redux/flutter_redux.dart';
import 'package:invoiceninja_flutter/data/models/serializers.dart'; import 'package:invoiceninja_flutter/data/models/serializers.dart';
import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/app/app_actions.dart';
import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart';
import 'package:invoiceninja_flutter/redux/client/client_selectors.dart';
import 'package:invoiceninja_flutter/ui/app/app_scrollbar.dart'; import 'package:invoiceninja_flutter/ui/app/app_scrollbar.dart';
import 'package:invoiceninja_flutter/ui/app/buttons/elevated_button.dart'; import 'package:invoiceninja_flutter/ui/app/buttons/elevated_button.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
@ -212,12 +213,7 @@ class InvoiceEditDesktopState extends State<InvoiceEditDesktop>
!item.isEmpty && item.typeId == InvoiceItemEntity.TYPE_TASK) !item.isEmpty && item.typeId == InvoiceItemEntity.TYPE_TASK)
.length; .length;
final settings = SettingsEntity( final settings = getClientSettings(state, client);
clientSettings: client.settings,
groupSettings: state.groupState.get(client.groupId).settings,
companySettings: state.company.settings,
);
final terms = entityType == EntityType.quote final terms = entityType == EntityType.quote
? settings.defaultValidUntil ? settings.defaultValidUntil
: settings.defaultPaymentTerms; : settings.defaultPaymentTerms;

View File

@ -1,5 +1,6 @@
import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/redux/client/client_selectors.dart';
import 'package:invoiceninja_flutter/redux/invoice/invoice_selectors.dart'; import 'package:invoiceninja_flutter/redux/invoice/invoice_selectors.dart';
import 'package:invoiceninja_flutter/ui/app/forms/app_dropdown_button.dart'; import 'package:invoiceninja_flutter/ui/app/forms/app_dropdown_button.dart';
import 'package:invoiceninja_flutter/ui/app/forms/client_picker.dart'; import 'package:invoiceninja_flutter/ui/app/forms/client_picker.dart';
@ -137,11 +138,7 @@ class InvoiceEditDetailsState extends State<InvoiceEditDetails> {
state.getEntity(invoice.entityType, invoice.id) as InvoiceEntity; state.getEntity(invoice.entityType, invoice.id) as InvoiceEntity;
final client = state.clientState.get(invoice.clientId); final client = state.clientState.get(invoice.clientId);
final settings = SettingsEntity( final settings = getClientSettings(state, client);
clientSettings: client.settings,
groupSettings: state.groupState.get(client.groupId).settings,
companySettings: state.company.settings,
);
final terms = widget.entityType == EntityType.quote final terms = widget.entityType == EntityType.quote
? settings.defaultValidUntil ? settings.defaultValidUntil
: settings.defaultPaymentTerms; : settings.defaultPaymentTerms;

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/data/models/entities.dart'; import 'package:invoiceninja_flutter/data/models/entities.dart';
import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/redux/client/client_selectors.dart';
import 'package:invoiceninja_flutter/ui/app/form_card.dart'; import 'package:invoiceninja_flutter/ui/app/form_card.dart';
import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart';
import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart'; import 'package:invoiceninja_flutter/ui/app/scrollable_listview.dart';
@ -83,12 +84,7 @@ class InvoiceEditNotesState extends State<InvoiceEditNotes> {
final state = viewModel.state; final state = viewModel.state;
final invoice = viewModel.invoice; final invoice = viewModel.invoice;
final client = state.clientState.get(invoice.clientId); final client = state.clientState.get(invoice.clientId);
final settings = getClientSettings(state, client);
final settings = SettingsEntity(
clientSettings: client.settings,
groupSettings: state.groupState.get(client.groupId).settings,
companySettings: state.company.settings,
);
return ScrollableListView( return ScrollableListView(
children: <Widget>[ children: <Widget>[

View File

@ -120,11 +120,10 @@ class _SettingsListState extends State<SettingsList> {
section: kSettingsOnlinePayments, section: kSettingsOnlinePayments,
viewModel: widget.viewModel, viewModel: widget.viewModel,
), ),
if (showAll) SettingsListTile(
SettingsListTile( section: kSettingsTaxSettings,
section: kSettingsTaxSettings, viewModel: widget.viewModel,
viewModel: widget.viewModel, ),
),
if (showAll) if (showAll)
SettingsListTile( SettingsListTile(
section: kSettingsProducts, section: kSettingsProducts,

View File

@ -56,76 +56,74 @@ class _TaxSettingsState extends State<TaxSettings> {
formKey: _formKey, formKey: _formKey,
focusNode: _focusNode, focusNode: _focusNode,
children: <Widget>[ children: <Widget>[
FormCard( if (!state.settingsUIState.isFiltered)
children: <Widget>[
NumberOfRatesSelector(
label: localization.invoiceTaxRates,
numberOfRates: company.numberOfInvoiceTaxRates,
onChanged: (value) => viewModel.onCompanyChanged(
company.rebuild((b) => b..numberOfInvoiceTaxRates = value)),
),
NumberOfRatesSelector(
label: localization.itemTaxRates,
numberOfRates: company.numberOfItemTaxRates,
onChanged: (value) => viewModel.onCompanyChanged(
company.rebuild((b) => b..numberOfItemTaxRates = value)),
),
SizedBox(height: 16),
BoolDropdownButton(
iconData: MdiIcons.percent,
label: localization.inclusiveTaxes,
value: settings.enableInclusiveTaxes,
onChanged: (value) => viewModel.onSettingsChanged(
settings.rebuild((b) => b..enableInclusiveTaxes = value)),
helpLabel:
'\n${localization.exclusive}: 100 + 10% = 100 + 10\n${localization.inclusive}: 100 + 10% = 90.91 + 9.09',
),
],
),
if (company.enableFirstInvoiceTaxRate &&
state.taxRateState.list.isNotEmpty)
FormCard( FormCard(
children: <Widget>[ children: <Widget>[
NumberOfRatesSelector(
label: localization.invoiceTaxRates,
numberOfRates: company.numberOfInvoiceTaxRates,
onChanged: (value) => viewModel.onCompanyChanged(company
.rebuild((b) => b..numberOfInvoiceTaxRates = value)),
),
NumberOfRatesSelector(
label: localization.itemTaxRates,
numberOfRates: company.numberOfItemTaxRates,
onChanged: (value) => viewModel.onCompanyChanged(
company.rebuild((b) => b..numberOfItemTaxRates = value)),
),
SizedBox(height: 16),
BoolDropdownButton(
iconData: MdiIcons.percent,
label: localization.inclusiveTaxes,
value: settings.enableInclusiveTaxes,
onChanged: (value) => viewModel.onSettingsChanged(
settings.rebuild((b) => b..enableInclusiveTaxes = value)),
helpLabel:
'\n${localization.exclusive}: 100 + 10% = 100 + 10\n${localization.inclusive}: 100 + 10% = 90.91 + 9.09',
),
],
),
FormCard(
children: <Widget>[
TaxRateDropdown(
onSelected: (taxRate) =>
viewModel.onSettingsChanged(settings.rebuild((b) => b
..defaultTaxName1 = taxRate.name
..defaultTaxRate1 = taxRate.rate)),
labelText: localization.defaultTaxRate,
initialTaxName: settings.defaultTaxName1,
initialTaxRate: settings.defaultTaxRate1,
),
if (company.enableSecondInvoiceTaxRate)
TaxRateDropdown( TaxRateDropdown(
onSelected: (taxRate) => onSelected: (taxRate) =>
viewModel.onSettingsChanged(settings.rebuild((b) => b viewModel.onSettingsChanged(settings.rebuild((b) => b
..defaultTaxName1 = taxRate.name ..defaultTaxName2 = taxRate.name
..defaultTaxRate1 = taxRate.rate)), ..defaultTaxRate2 = taxRate.rate)),
labelText: localization.defaultTaxRate, labelText: localization.defaultTaxRate,
initialTaxName: settings.defaultTaxName1, initialTaxName: settings.defaultTaxName2,
initialTaxRate: settings.defaultTaxRate1, initialTaxRate: settings.defaultTaxRate2,
), ),
if (company.enableSecondInvoiceTaxRate) if (company.enableThirdInvoiceTaxRate)
TaxRateDropdown( TaxRateDropdown(
onSelected: (taxRate) => onSelected: (taxRate) =>
viewModel.onSettingsChanged(settings.rebuild((b) => b viewModel.onSettingsChanged(settings.rebuild((b) => b
..defaultTaxName2 = taxRate.name ..defaultTaxName3 = taxRate.name
..defaultTaxRate2 = taxRate.rate)), ..defaultTaxRate3 = taxRate.rate)),
labelText: localization.defaultTaxRate, labelText: localization.defaultTaxRate,
initialTaxName: settings.defaultTaxName2, initialTaxName: settings.defaultTaxName3,
initialTaxRate: settings.defaultTaxRate2, initialTaxRate: settings.defaultTaxRate3,
), ),
if (company.enableThirdInvoiceTaxRate) ],
TaxRateDropdown( ),
onSelected: (taxRate) => Padding(
viewModel.onSettingsChanged(settings.rebuild((b) => b padding: const EdgeInsets.symmetric(horizontal: 16.0),
..defaultTaxName3 = taxRate.name child: AppButton(
..defaultTaxRate3 = taxRate.rate)), iconData: Icons.settings,
labelText: localization.defaultTaxRate, label: localization.configureRates.toUpperCase(),
initialTaxName: settings.defaultTaxName3, onPressed: () => viewModel.onConfigureRatesPressed(context),
initialTaxRate: settings.defaultTaxRate3,
),
],
),
if (!state.uiState.settingsUIState.isFiltered)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: AppButton(
iconData: Icons.settings,
label: localization.configureRates.toUpperCase(),
onPressed: () => viewModel.onConfigureRatesPressed(context),
),
), ),
),
], ],
), ),
); );

View File

@ -2,9 +2,14 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/data/models/client_model.dart';
import 'package:invoiceninja_flutter/data/models/company_model.dart'; import 'package:invoiceninja_flutter/data/models/company_model.dart';
import 'package:invoiceninja_flutter/data/models/entities.dart';
import 'package:invoiceninja_flutter/data/models/group_model.dart';
import 'package:invoiceninja_flutter/data/models/settings_model.dart'; import 'package:invoiceninja_flutter/data/models/settings_model.dart';
import 'package:invoiceninja_flutter/redux/client/client_actions.dart';
import 'package:invoiceninja_flutter/redux/company/company_actions.dart'; import 'package:invoiceninja_flutter/redux/company/company_actions.dart';
import 'package:invoiceninja_flutter/redux/group/group_actions.dart';
import 'package:invoiceninja_flutter/redux/settings/settings_actions.dart'; import 'package:invoiceninja_flutter/redux/settings/settings_actions.dart';
import 'package:invoiceninja_flutter/ui/settings/tax_settings.dart'; import 'package:invoiceninja_flutter/ui/settings/tax_settings.dart';
import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/completers.dart';
@ -56,10 +61,27 @@ class TaxSettingsVM {
onSavePressed: (context) { onSavePressed: (context) {
Debouncer.runOnComplete(() { Debouncer.runOnComplete(() {
final settingsUIState = store.state.uiState.settingsUIState; final settingsUIState = store.state.uiState.settingsUIState;
final completer = snackBarCompleter<Null>(
context, AppLocalization.of(context).savedSettings); switch (settingsUIState.entityType) {
store.dispatch(SaveCompanyRequest( case EntityType.company:
completer: completer, company: settingsUIState.company)); final completer = snackBarCompleter<Null>(
context, AppLocalization.of(context).savedSettings);
store.dispatch(SaveCompanyRequest(
completer: completer, company: settingsUIState.company));
break;
case EntityType.group:
final completer = snackBarCompleter<GroupEntity>(
context, AppLocalization.of(context).savedSettings);
store.dispatch(SaveGroupRequest(
completer: completer, group: settingsUIState.group));
break;
case EntityType.client:
final completer = snackBarCompleter<ClientEntity>(
context, AppLocalization.of(context).savedSettings);
store.dispatch(SaveClientRequest(
completer: completer, client: settingsUIState.client));
break;
}
}); });
}, },
onConfigureRatesPressed: (context) { onConfigureRatesPressed: (context) {