Skip to content

Commit e3c30d0

Browse files
authored
Merge pull request Expensify#63054 from daledah/fix/58588-2
fix: reimbursable features
2 parents 4ee8d7b + bac0a46 commit e3c30d0

47 files changed

Lines changed: 623 additions & 31 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/CONST/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2878,6 +2878,12 @@ const CONST = {
28782878
REIMBURSEMENT_NO: 'reimburseNo', // None
28792879
REIMBURSEMENT_MANUAL: 'reimburseManual', // Indirect
28802880
},
2881+
CASH_EXPENSE_REIMBURSEMENT_CHOICES: {
2882+
REIMBURSABLE_DEFAULT: 'reimbursableDefault', // Reimbursable by default
2883+
NON_REIMBURSABLE_DEFAULT: 'nonReimbursableDefault', // Non-reimbursable by default
2884+
ALWAYS_REIMBURSABLE: 'alwaysReimbursable', // Always Reimbursable
2885+
ALWAYS_NON_REIMBURSABLE: 'alwaysNonReimbursable', // Always Non Reimbursable
2886+
},
28812887
ID_FAKE: '_FAKE_',
28822888
EMPTY: 'EMPTY',
28832889
SECONDARY_ACTIONS: {
@@ -3737,6 +3743,7 @@ const CONST = {
37373743
TAG: 'tag',
37383744
TAX_RATE: 'taxRate',
37393745
TAX_AMOUNT: 'taxAmount',
3746+
REIMBURSABLE: 'reimbursable',
37403747
REPORT: 'report',
37413748
},
37423749
FOOTER: {

src/ROUTES.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1916,6 +1916,10 @@ const ROUTES = {
19161916
route: 'workspaces/:policyID/rules/billable',
19171917
getRoute: (policyID: string) => `workspaces/${policyID}/rules/billable` as const,
19181918
},
1919+
RULES_REIMBURSABLE_DEFAULT: {
1920+
route: 'workspaces/:policyID/rules/reimbursable',
1921+
getRoute: (policyID: string) => `workspaces/${policyID}/rules/reimbursable` as const,
1922+
},
19191923
RULES_PROHIBITED_DEFAULT: {
19201924
route: 'workspaces/:policyID/rules/prohibited',
19211925
getRoute: (policyID: string) => `workspaces/${policyID}/rules/prohibited` as const,

src/SCREENS.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,7 @@ const SCREENS = {
632632
RULES_MAX_EXPENSE_AMOUNT: 'Rules_Max_Expense_Amount',
633633
RULES_MAX_EXPENSE_AGE: 'Rules_Max_Expense_Age',
634634
RULES_BILLABLE_DEFAULT: 'Rules_Billable_Default',
635+
RULES_REIMBURSABLE_DEFAULT: 'Rules_Reimbursable_Default',
635636
RULES_CUSTOM: 'Rules_Custom',
636637
RULES_PROHIBITED_DEFAULT: 'Rules_Prohibited_Default',
637638
PER_DIEM: 'Per_Diem',

src/components/MoneyRequestConfirmationList.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,12 @@ type MoneyRequestConfirmationListProps = {
183183
/** The PDF password callback */
184184
onPDFPassword?: () => void;
185185

186+
/** Function to toggle reimbursable */
187+
onToggleReimbursable?: (isOn: boolean) => void;
188+
189+
/** Flag indicating if the IOU is reimbursable */
190+
iouIsReimbursable?: boolean;
191+
186192
/** Show remove expense confirmation modal */
187193
showRemoveExpenseConfirmModal?: () => void;
188194
};
@@ -225,6 +231,8 @@ function MoneyRequestConfirmationList({
225231
isConfirming,
226232
onPDFLoadError,
227233
onPDFPassword,
234+
iouIsReimbursable = true,
235+
onToggleReimbursable,
228236
showRemoveExpenseConfirmModal,
229237
}: MoneyRequestConfirmationListProps) {
230238
const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true});
@@ -1153,6 +1161,8 @@ function MoneyRequestConfirmationList({
11531161
unit={unit}
11541162
onPDFLoadError={onPDFLoadError}
11551163
onPDFPassword={onPDFPassword}
1164+
iouIsReimbursable={iouIsReimbursable}
1165+
onToggleReimbursable={onToggleReimbursable}
11561166
isReceiptEditable={isReceiptEditable}
11571167
/>
11581168
);

src/components/MoneyRequestConfirmationListFooter.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
getTaxAmount,
3232
getTaxName,
3333
isAmountMissing,
34+
isCardTransaction,
3435
isCreatedMissing,
3536
isFetchingWaypointsFromServer,
3637
shouldShowAttendees as shouldShowAttendeesTransactionUtils,
@@ -198,6 +199,12 @@ type MoneyRequestConfirmationListFooterProps = {
198199

199200
/** The PDF password callback */
200201
onPDFPassword?: () => void;
202+
203+
/** Function to toggle reimbursable */
204+
onToggleReimbursable?: (isOn: boolean) => void;
205+
206+
/** Flag indicating if the IOU is reimbursable */
207+
iouIsReimbursable: boolean;
201208
};
202209

203210
function MoneyRequestConfirmationListFooter({
@@ -247,6 +254,8 @@ function MoneyRequestConfirmationListFooter({
247254
unit,
248255
onPDFLoadError,
249256
onPDFPassword,
257+
iouIsReimbursable,
258+
onToggleReimbursable,
250259
isReceiptEditable = false,
251260
}: MoneyRequestConfirmationListFooterProps) {
252261
const styles = useThemeStyles();
@@ -313,6 +322,7 @@ function MoneyRequestConfirmationListFooter({
313322
const canModifyTaxFields = !isReadOnly && !isDistanceRequest && !isPerDiemRequest;
314323
// A flag for showing the billable field
315324
const shouldShowBillable = policy?.disabledFields?.defaultBillable === false;
325+
const shouldShowReimbursable = isPaidGroupPolicy(policy) && policy?.disabledFields?.reimbursable === false && !isCardTransaction(transaction) && !isTypeInvoice;
316326
// Calculate the formatted tax amount based on the transaction's tax amount and the IOU currency code
317327
const taxAmount = getTaxAmount(transaction, false);
318328
const formattedTaxAmount = convertToDisplayString(taxAmount, iouCurrencyCode);
@@ -641,6 +651,25 @@ function MoneyRequestConfirmationListFooter({
641651
),
642652
shouldShow: shouldShowAttendees,
643653
},
654+
{
655+
item: (
656+
<View
657+
key={Str.UCFirst(translate('iou.reimbursable'))}
658+
style={[styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8, styles.optionRow]}
659+
>
660+
<ToggleSettingOptionRow
661+
switchAccessibilityLabel={Str.UCFirst(translate('iou.reimbursable'))}
662+
title={Str.UCFirst(translate('iou.reimbursable'))}
663+
onToggle={(isOn) => onToggleReimbursable?.(isOn)}
664+
isActive={iouIsReimbursable}
665+
disabled={isReadOnly}
666+
wrapperStyle={styles.flex1}
667+
/>
668+
</View>
669+
),
670+
shouldShow: shouldShowReimbursable,
671+
isSupplementary: true,
672+
},
644673
{
645674
item: (
646675
<View

src/components/ReportActionItem/MoneyRequestView.tsx

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {Str} from 'expensify-common';
12
import mapValues from 'lodash/mapValues';
23
import React, {useCallback, useEffect, useMemo, useState} from 'react';
34
import {View} from 'react-native';
@@ -57,6 +58,7 @@ import {
5758
getDescription,
5859
getDistanceInMeters,
5960
getOriginalTransactionWithSplitInfo,
61+
getReimbursable,
6062
getTagForDisplay,
6163
getTaxName,
6264
hasMissingSmartscanFields,
@@ -72,7 +74,7 @@ import {
7274
import ViolationsUtils from '@libs/Violations/ViolationsUtils';
7375
import Navigation from '@navigation/Navigation';
7476
import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground';
75-
import {cleanUpMoneyRequest, updateMoneyRequestBillable} from '@userActions/IOU';
77+
import {cleanUpMoneyRequest, updateMoneyRequestBillable, updateMoneyRequestReimbursable} from '@userActions/IOU';
7678
import {navigateToConciergeChatAndDeleteReport} from '@userActions/Report';
7779
import {clearAllRelatedReportActionErrors} from '@userActions/ReportActions';
7880
import {clearError, getLastModifiedExpense, revert} from '@userActions/Transaction';
@@ -164,6 +166,7 @@ function MoneyRequestView({allReports, report, policy, shouldShowAnimatedBackgro
164166
currency: transactionCurrency,
165167
comment: transactionDescription,
166168
merchant: transactionMerchant,
169+
reimbursable: transactionReimbursable,
167170
billable: transactionBillable,
168171
category: transactionCategory,
169172
tag: transactionTag,
@@ -199,6 +202,7 @@ function MoneyRequestView({allReports, report, policy, shouldShowAnimatedBackgro
199202
const isSettled = isSettledReportUtils(moneyRequestReport?.reportID);
200203
const isCancelled = moneyRequestReport && moneyRequestReport?.isCancelledIOU;
201204
const isChatReportArchived = useReportIsArchived(moneyRequestReport?.chatReportID);
205+
const shouldShowPaid = isSettled && transactionReimbursable;
202206

203207
// Flags for allowing or disallowing editing an expense
204208
// Used for non-restricted fields such as: description, category, tag, billable, etc...
@@ -242,6 +246,8 @@ function MoneyRequestView({allReports, report, policy, shouldShowAnimatedBackgro
242246
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
243247
const shouldShowTag = isPolicyExpenseChat && (transactionTag || hasEnabledTags(policyTagLists));
244248
const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true) || !!updatedTransaction?.billable);
249+
const shouldShowReimbursable = isPolicyExpenseChat && !isCardTransaction && !isInvoice;
250+
const canEditReimbursable = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.REIMBURSABLE);
245251
const shouldShowAttendees = useMemo(() => shouldShowAttendeesTransactionUtils(iouType, policy), [iouType, policy]);
246252

247253
const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest, isPerDiemRequest);
@@ -291,6 +297,17 @@ function MoneyRequestView({allReports, report, policy, shouldShowAnimatedBackgro
291297
[transaction, report, policy, policyTagList, policyCategories],
292298
);
293299

300+
const saveReimbursable = useCallback(
301+
(newReimbursable: boolean) => {
302+
// If the value hasn't changed, don't request to save changes on the server and just close the modal
303+
if (newReimbursable === getReimbursable(transaction) || !transaction?.transactionID || !report?.reportID) {
304+
return;
305+
}
306+
updateMoneyRequestReimbursable(transaction.transactionID, report?.reportID, newReimbursable, policy, policyTagList, policyCategories);
307+
},
308+
[transaction, report, policy, policyTagList, policyCategories],
309+
);
310+
294311
if (isCardTransaction) {
295312
if (transactionPostedDate) {
296313
dateDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.posted')} ${transactionPostedDate}`;
@@ -312,7 +329,7 @@ function MoneyRequestView({allReports, report, policy, shouldShowAnimatedBackgro
312329
amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.canceled')}`;
313330
} else if (isApproved) {
314331
amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.approved')}`;
315-
} else if (isSettled) {
332+
} else if (shouldShowPaid) {
316333
amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.settledExpensify')}`;
317334
}
318335
}
@@ -654,7 +671,7 @@ function MoneyRequestView({allReports, report, policy, shouldShowAnimatedBackgro
654671
<OfflineWithFeedback pendingAction={getPendingFieldAction('amount') ?? (amountTitle ? getPendingFieldAction('customUnitRateID') : undefined)}>
655672
<MenuItemWithTopDescription
656673
title={amountTitle}
657-
shouldShowTitleIcon={isSettled}
674+
shouldShowTitleIcon={shouldShowPaid}
658675
titleIcon={Expensicons.Checkmark}
659676
description={amountDescription}
660677
titleStyle={styles.textHeadlineH2}
@@ -837,8 +854,27 @@ function MoneyRequestView({allReports, report, policy, shouldShowAnimatedBackgro
837854
/>
838855
</OfflineWithFeedback>
839856
)}
857+
{shouldShowReimbursable && (
858+
<OfflineWithFeedback
859+
pendingAction={getPendingFieldAction('reimbursable')}
860+
contentContainerStyle={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}
861+
>
862+
<View>
863+
<Text>{Str.UCFirst(translate('iou.reimbursable'))}</Text>
864+
</View>
865+
<Switch
866+
accessibilityLabel={Str.UCFirst(translate('iou.reimbursable'))}
867+
isOn={updatedTransaction?.reimbursable ?? !!transactionReimbursable}
868+
onToggle={saveReimbursable}
869+
disabled={!canEditReimbursable}
870+
/>
871+
</OfflineWithFeedback>
872+
)}
840873
{shouldShowBillable && (
841-
<View style={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}>
874+
<OfflineWithFeedback
875+
pendingAction={getPendingFieldAction('billable')}
876+
contentContainerStyle={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}
877+
>
842878
<View>
843879
<Text>{translate('common.billable')}</Text>
844880
{!!getErrorForField('billable') && (
@@ -857,7 +893,7 @@ function MoneyRequestView({allReports, report, policy, shouldShowAnimatedBackgro
857893
onToggle={saveBillable}
858894
disabled={!canEdit}
859895
/>
860-
</View>
896+
</OfflineWithFeedback>
861897
)}
862898
{!!parentReportID && (
863899
<OfflineWithFeedback pendingAction={getPendingFieldAction('reportID')}>

src/languages/de.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5481,6 +5481,17 @@ const translations = {
54815481
one: '1 Tag',
54825482
other: (count: number) => `${count} Tage`,
54835483
}),
5484+
cashExpenseDefault: 'Bargeldausgabe standard',
5485+
cashExpenseDefaultDescription:
5486+
'Wählen Sie, wie Bargeldausgaben erstellt werden sollen. Eine Ausgabe gilt als Bargeldausgabe, wenn sie keine importierte Firmenkartentransaktion ist. Dazu gehören manuell erstellte Ausgaben, Belege, Pauschalen, Kilometer- und Zeitaufwand.',
5487+
reimbursableDefault: 'Erstattungsfähig',
5488+
reimbursableDefaultDescription: 'Ausgaben werden meistens an Mitarbeiter zurückgezahlt',
5489+
nonReimbursableDefault: 'Nicht erstattungsfähig',
5490+
nonReimbursableDefaultDescription: 'Ausgaben werden gelegentlich an Mitarbeiter zurückgezahlt',
5491+
alwaysReimbursable: 'Immer erstattungsfähig',
5492+
alwaysReimbursableDescription: 'Ausgaben werden immer an Mitarbeiter zurückgezahlt',
5493+
alwaysNonReimbursable: 'Nie erstattungsfähig',
5494+
alwaysNonReimbursableDescription: 'Ausgaben werden nie an Mitarbeiter zurückgezahlt',
54845495
billableDefault: 'Abrechnungsstandard',
54855496
billableDefaultDescription: 'Wählen Sie, ob Bar- und Kreditkartenausgaben standardmäßig abrechenbar sein sollen. Abrechenbare Ausgaben werden aktiviert oder deaktiviert in',
54865497
billable: 'Abrechenbar',
@@ -5782,6 +5793,7 @@ const translations = {
57825793
},
57835794
updateDefaultBillable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) =>
57845795
`aktualisiert "Kosten an Kunden weiterberechnen" auf "${newValue}" (vorher "${oldValue}")`,
5796+
updateDefaultReimbursable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `aktualisiert "Bargeldausgabe Standard" auf "${newValue}" (vorher "${oldValue}")`,
57855797
updateDefaultTitleEnforced: ({value}: UpdatedPolicyFieldWithValueParam) => `"Standardberichtstitel erzwingen" ${value ? 'on' : 'aus'}`,
57865798
renamedWorkspaceNameAction: ({oldName, newName}: RenamedWorkspaceNameActionParams) => `hat den Namen dieses Arbeitsbereichs in "${newName}" geändert (vorher "${oldName}")`,
57875799
updateWorkspaceDescription: ({newDescription, oldDescription}: UpdatedPolicyDescriptionParams) =>

src/languages/en.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5453,6 +5453,17 @@ const translations = {
54535453
one: '1 day',
54545454
other: (count: number) => `${count} days`,
54555455
}),
5456+
cashExpenseDefault: 'Cash expense default',
5457+
cashExpenseDefaultDescription:
5458+
'Choose how cash expenses should be created. An expense is considered a cash expense if it is not an imported company card transaction. This includes manually created expenses, receipts, per diem, distance, and time expenses.',
5459+
reimbursableDefault: 'Reimbursable',
5460+
reimbursableDefaultDescription: 'Expenses are most often paid back to employees',
5461+
nonReimbursableDefault: 'Non-reimbursable',
5462+
nonReimbursableDefaultDescription: 'Expenses are occasionally paid back to employees',
5463+
alwaysReimbursable: 'Always reimbursable',
5464+
alwaysReimbursableDescription: 'Expenses are always paid back to employees',
5465+
alwaysNonReimbursable: 'Always non-reimbursable',
5466+
alwaysNonReimbursableDescription: 'Expenses are never paid back to employees',
54565467
billableDefault: 'Billable default',
54575468
billableDefaultDescription: 'Choose whether cash and credit card expenses should be billable by default. Billable expenses are enabled or disabled in',
54585469
billable: 'Billable',
@@ -5754,6 +5765,7 @@ const translations = {
57545765
return `updated the monthly report submission date to "${newValue}" (previously "${oldValue}")`;
57555766
},
57565767
updateDefaultBillable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `updated "Re-bill expenses to clients" to "${newValue}" (previously "${oldValue}")`,
5768+
updateDefaultReimbursable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `updated "Cash expense default" to "${newValue}" (previously "${oldValue}")`,
57575769
updateDefaultTitleEnforced: ({value}: UpdatedPolicyFieldWithValueParam) => `turned "Enforce default report titles" ${value ? 'on' : 'off'}`,
57585770
renamedWorkspaceNameAction: ({oldName, newName}: RenamedWorkspaceNameActionParams) => `updated the name of this workspace to "${newName}" (previously "${oldName}")`,
57595771
updateWorkspaceDescription: ({newDescription, oldDescription}: UpdatedPolicyDescriptionParams) =>

src/languages/es.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5491,6 +5491,17 @@ const translations = {
54915491
one: '1 día',
54925492
other: (count: number) => `${count} días`,
54935493
}),
5494+
cashExpenseDefault: 'Valor predeterminado para gastos en efectivo',
5495+
cashExpenseDefaultDescription:
5496+
'Elige cómo deben crearse los gastos en efectivo. Un gasto se considera en efectivo si no es una transacción importada desde una tarjeta de empresa. Esto incluye gastos creados manualmente, recibos, viáticos y gastos de distancia y tiempo.',
5497+
reimbursableDefault: 'Reembolsable',
5498+
reimbursableDefaultDescription: 'Los gastos suelen ser reembolsados a los empleados',
5499+
nonReimbursableDefault: 'No reembolsable',
5500+
nonReimbursableDefaultDescription: 'Los gastos ocasionalmente son reembolsados a los empleados',
5501+
alwaysReimbursable: 'Siempre reembolsable',
5502+
alwaysReimbursableDescription: 'Los gastos siempre se reembolsados a los empleados',
5503+
alwaysNonReimbursable: 'Siempre no reembolsable',
5504+
alwaysNonReimbursableDescription: 'Los gastos nunca son reembolsados a los empleados',
54945505
billableDefault: 'Valor predeterminado facturable',
54955506
billableDefaultDescription: 'Elige si los gastos en efectivo y con tarjeta de crédito deben ser facturables por defecto. Los gastos facturables se activan o desactivan en',
54965507
billable: 'Facturable',
@@ -5770,6 +5781,8 @@ const translations = {
57705781
`actualizó "Antigüedad máxima de gastos (días)" a "${newValue}" (previamente "${oldValue === 'false' ? CONST.POLICY.DEFAULT_MAX_EXPENSE_AGE : oldValue}")`,
57715782
updateDefaultBillable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) =>
57725783
`actualizó "Volver a facturar gastos a clientes" a "${newValue}" (previamente "${oldValue}")`,
5784+
updateDefaultReimbursable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) =>
5785+
`actualizó "Valor predeterminado para gastos en efectivo" a "${newValue}" (previamente "${oldValue}")`,
57735786
updateMonthlyOffset: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => {
57745787
if (!oldValue) {
57755788
return `establecer la fecha de envío del informe mensual a "${newValue}"`;

0 commit comments

Comments
 (0)