Skip to content

Commit f5698b9

Browse files
authored
Merge pull request Expensify#68073 from daledah/fix/58588-3
feat: reimbursable features
2 parents d80a995 + d58a528 commit f5698b9

48 files changed

Lines changed: 635 additions & 30 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
@@ -2883,6 +2883,12 @@ const CONST = {
28832883
REIMBURSEMENT_NO: 'reimburseNo', // None
28842884
REIMBURSEMENT_MANUAL: 'reimburseManual', // Indirect
28852885
},
2886+
CASH_EXPENSE_REIMBURSEMENT_CHOICES: {
2887+
REIMBURSABLE_DEFAULT: 'reimbursableDefault', // Reimbursable by default
2888+
NON_REIMBURSABLE_DEFAULT: 'nonReimbursableDefault', // Non-reimbursable by default
2889+
ALWAYS_REIMBURSABLE: 'alwaysReimbursable', // Always Reimbursable
2890+
ALWAYS_NON_REIMBURSABLE: 'alwaysNonReimbursable', // Always Non Reimbursable
2891+
},
28862892
ID_FAKE: '_FAKE_',
28872893
EMPTY: 'EMPTY',
28882894
SECONDARY_ACTIONS: {
@@ -3749,6 +3755,7 @@ const CONST = {
37493755
TAG: 'tag',
37503756
TAX_RATE: 'taxRate',
37513757
TAX_AMOUNT: 'taxAmount',
3758+
REIMBURSABLE: 'reimbursable',
37523759
REPORT: 'report',
37533760
},
37543761
FOOTER: {

src/ROUTES.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1928,6 +1928,10 @@ const ROUTES = {
19281928
route: 'workspaces/:policyID/rules/billable',
19291929
getRoute: (policyID: string) => `workspaces/${policyID}/rules/billable` as const,
19301930
},
1931+
RULES_REIMBURSABLE_DEFAULT: {
1932+
route: 'workspaces/:policyID/rules/reimbursable',
1933+
getRoute: (policyID: string) => `workspaces/${policyID}/rules/reimbursable` as const,
1934+
},
19311935
RULES_PROHIBITED_DEFAULT: {
19321936
route: 'workspaces/:policyID/rules/prohibited',
19331937
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
@@ -640,6 +640,7 @@ const SCREENS = {
640640
RULES_MAX_EXPENSE_AMOUNT: 'Rules_Max_Expense_Amount',
641641
RULES_MAX_EXPENSE_AGE: 'Rules_Max_Expense_Age',
642642
RULES_BILLABLE_DEFAULT: 'Rules_Billable_Default',
643+
RULES_REIMBURSABLE_DEFAULT: 'Rules_Reimbursable_Default',
643644
RULES_CUSTOM: 'Rules_Custom',
644645
RULES_PROHIBITED_DEFAULT: 'Rules_Prohibited_Default',
645646
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});
@@ -1154,6 +1162,8 @@ function MoneyRequestConfirmationList({
11541162
unit={unit}
11551163
onPDFLoadError={onPDFLoadError}
11561164
onPDFPassword={onPDFPassword}
1165+
iouIsReimbursable={iouIsReimbursable}
1166+
onToggleReimbursable={onToggleReimbursable}
11571167
isReceiptEditable={isReceiptEditable}
11581168
/>
11591169
);

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';
@@ -62,6 +63,7 @@ import {
6263
getDescription,
6364
getDistanceInMeters,
6465
getOriginalTransactionWithSplitInfo,
66+
getReimbursable,
6567
getTagForDisplay,
6668
getTaxName,
6769
hasMissingSmartscanFields,
@@ -77,7 +79,7 @@ import {
7779
import ViolationsUtils from '@libs/Violations/ViolationsUtils';
7880
import Navigation from '@navigation/Navigation';
7981
import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground';
80-
import {cleanUpMoneyRequest, updateMoneyRequestBillable} from '@userActions/IOU';
82+
import {cleanUpMoneyRequest, updateMoneyRequestBillable, updateMoneyRequestReimbursable} from '@userActions/IOU';
8183
import {navigateToConciergeChatAndDeleteReport} from '@userActions/Report';
8284
import {clearAllRelatedReportActionErrors} from '@userActions/ReportActions';
8385
import {clearError, getLastModifiedExpense, revert} from '@userActions/Transaction';
@@ -183,6 +185,7 @@ function MoneyRequestView({
183185
currency: transactionCurrency,
184186
comment: transactionDescription,
185187
merchant: transactionMerchant,
188+
reimbursable: transactionReimbursable,
186189
billable: transactionBillable,
187190
category: transactionCategory,
188191
tag: transactionTag,
@@ -224,6 +227,7 @@ function MoneyRequestView({
224227
const isSettled = isSettledReportUtils(moneyRequestReport?.reportID);
225228
const isCancelled = moneyRequestReport && moneyRequestReport?.isCancelledIOU;
226229
const isChatReportArchived = useReportIsArchived(moneyRequestReport?.chatReportID);
230+
const shouldShowPaid = isSettled && transactionReimbursable;
227231

228232
// Flags for allowing or disallowing editing an expense
229233
// Used for non-restricted fields such as: description, category, tag, billable, etc...
@@ -267,6 +271,8 @@ function MoneyRequestView({
267271
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
268272
const shouldShowTag = isPolicyExpenseChat && (transactionTag || hasEnabledTags(policyTagLists));
269273
const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true) || !!updatedTransaction?.billable);
274+
const shouldShowReimbursable = isPolicyExpenseChat && !policy?.disabledFields?.reimbursable && !isCardTransaction && !isInvoice;
275+
const canEditReimbursable = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.REIMBURSABLE);
270276
const shouldShowAttendees = useMemo(() => shouldShowAttendeesTransactionUtils(iouType, policy), [iouType, policy]);
271277

272278
const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest, isPerDiemRequest);
@@ -316,6 +322,17 @@ function MoneyRequestView({
316322
[transaction, report, policy, policyTagList, policyCategories],
317323
);
318324

325+
const saveReimbursable = useCallback(
326+
(newReimbursable: boolean) => {
327+
// If the value hasn't changed, don't request to save changes on the server and just close the modal
328+
if (newReimbursable === getReimbursable(transaction) || !transaction?.transactionID || !report?.reportID) {
329+
return;
330+
}
331+
updateMoneyRequestReimbursable(transaction.transactionID, report?.reportID, newReimbursable, policy, policyTagList, policyCategories);
332+
},
333+
[transaction, report, policy, policyTagList, policyCategories],
334+
);
335+
319336
if (isCardTransaction) {
320337
if (transactionPostedDate) {
321338
dateDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.posted')} ${transactionPostedDate}`;
@@ -337,7 +354,7 @@ function MoneyRequestView({
337354
amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.canceled')}`;
338355
} else if (isApproved) {
339356
amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.approved')}`;
340-
} else if (isSettled) {
357+
} else if (shouldShowPaid) {
341358
amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.settledExpensify')}`;
342359
}
343360
}
@@ -696,7 +713,7 @@ function MoneyRequestView({
696713
<OfflineWithFeedback pendingAction={getPendingFieldAction('amount') ?? (amountTitle ? getPendingFieldAction('customUnitRateID') : undefined)}>
697714
<MenuItemWithTopDescription
698715
title={amountTitle}
699-
shouldShowTitleIcon={isSettled}
716+
shouldShowTitleIcon={shouldShowPaid}
700717
titleIcon={Expensicons.Checkmark}
701718
description={amountDescription}
702719
titleStyle={styles.textHeadlineH2}
@@ -879,8 +896,27 @@ function MoneyRequestView({
879896
/>
880897
</OfflineWithFeedback>
881898
)}
899+
{shouldShowReimbursable && (
900+
<OfflineWithFeedback
901+
pendingAction={getPendingFieldAction('reimbursable')}
902+
contentContainerStyle={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}
903+
>
904+
<View>
905+
<Text>{Str.UCFirst(translate('iou.reimbursable'))}</Text>
906+
</View>
907+
<Switch
908+
accessibilityLabel={Str.UCFirst(translate('iou.reimbursable'))}
909+
isOn={updatedTransaction?.reimbursable ?? !!transactionReimbursable}
910+
onToggle={saveReimbursable}
911+
disabled={!canEditReimbursable}
912+
/>
913+
</OfflineWithFeedback>
914+
)}
882915
{shouldShowBillable && (
883-
<View style={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}>
916+
<OfflineWithFeedback
917+
pendingAction={getPendingFieldAction('billable')}
918+
contentContainerStyle={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}
919+
>
884920
<View>
885921
<Text>{translate('common.billable')}</Text>
886922
{!!getErrorForField('billable') && (
@@ -899,7 +935,7 @@ function MoneyRequestView({
899935
onToggle={saveBillable}
900936
disabled={!canEdit}
901937
/>
902-
</View>
938+
</OfflineWithFeedback>
903939
)}
904940
{!!parentReportID && (
905941
<OfflineWithFeedback pendingAction={getPendingFieldAction('reportID')}>

src/languages/de.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5545,6 +5545,17 @@ const translations = {
55455545
one: '1 Tag',
55465546
other: (count: number) => `${count} Tage`,
55475547
}),
5548+
cashExpenseDefault: 'Bargeldausgabe standard',
5549+
cashExpenseDefaultDescription:
5550+
'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.',
5551+
reimbursableDefault: 'Erstattungsfähig',
5552+
reimbursableDefaultDescription: 'Ausgaben werden meistens an Mitarbeiter zurückgezahlt',
5553+
nonReimbursableDefault: 'Nicht erstattungsfähig',
5554+
nonReimbursableDefaultDescription: 'Ausgaben werden gelegentlich an Mitarbeiter zurückgezahlt',
5555+
alwaysReimbursable: 'Immer erstattungsfähig',
5556+
alwaysReimbursableDescription: 'Ausgaben werden immer an Mitarbeiter zurückgezahlt',
5557+
alwaysNonReimbursable: 'Nie erstattungsfähig',
5558+
alwaysNonReimbursableDescription: 'Ausgaben werden nie an Mitarbeiter zurückgezahlt',
55485559
billableDefault: 'Abrechnungsstandard',
55495560
billableDefaultDescription: 'Wählen Sie, ob Bar- und Kreditkartenausgaben standardmäßig abrechenbar sein sollen. Abrechenbare Ausgaben werden aktiviert oder deaktiviert in',
55505561
billable: 'Abrechenbar',
@@ -5834,6 +5845,7 @@ const translations = {
58345845
},
58355846
updateDefaultBillable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) =>
58365847
`aktualisiert "Kosten an Kunden weiterberechnen" auf "${newValue}" (vorher "${oldValue}")`,
5848+
updateDefaultReimbursable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `aktualisiert "Bargeldausgabe Standard" auf "${newValue}" (vorher "${oldValue}")`,
58375849
updateDefaultTitleEnforced: ({value}: UpdatedPolicyFieldWithValueParam) => `"Standardberichtstitel erzwingen" ${value ? 'on' : 'aus'}`,
58385850
renamedWorkspaceNameAction: ({oldName, newName}: RenamedWorkspaceNameActionParams) => `hat den Namen dieses Arbeitsbereichs in "${newName}" geändert (vorher "${oldName}")`,
58395851
updateWorkspaceDescription: ({newDescription, oldDescription}: UpdatedPolicyDescriptionParams) =>

src/languages/en.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5521,6 +5521,17 @@ const translations = {
55215521
one: '1 day',
55225522
other: (count: number) => `${count} days`,
55235523
}),
5524+
cashExpenseDefault: 'Cash expense default',
5525+
cashExpenseDefaultDescription:
5526+
'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.',
5527+
reimbursableDefault: 'Reimbursable',
5528+
reimbursableDefaultDescription: 'Expenses are most often paid back to employees',
5529+
nonReimbursableDefault: 'Non-reimbursable',
5530+
nonReimbursableDefaultDescription: 'Expenses are occasionally paid back to employees',
5531+
alwaysReimbursable: 'Always reimbursable',
5532+
alwaysReimbursableDescription: 'Expenses are always paid back to employees',
5533+
alwaysNonReimbursable: 'Always non-reimbursable',
5534+
alwaysNonReimbursableDescription: 'Expenses are never paid back to employees',
55245535
billableDefault: 'Billable default',
55255536
billableDefaultDescription: 'Choose whether cash and credit card expenses should be billable by default. Billable expenses are enabled or disabled in',
55265537
billable: 'Billable',
@@ -5810,6 +5821,7 @@ const translations = {
58105821
return `updated the monthly report submission date to "${newValue}" (previously "${oldValue}")`;
58115822
},
58125823
updateDefaultBillable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `updated "Re-bill expenses to clients" to "${newValue}" (previously "${oldValue}")`,
5824+
updateDefaultReimbursable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `updated "Cash expense default" to "${newValue}" (previously "${oldValue}")`,
58135825
updateDefaultTitleEnforced: ({value}: UpdatedPolicyFieldWithValueParam) => `turned "Enforce default report titles" ${value ? 'on' : 'off'}`,
58145826
renamedWorkspaceNameAction: ({oldName, newName}: RenamedWorkspaceNameActionParams) => `updated the name of this workspace to "${newName}" (previously "${oldName}")`,
58155827
updateWorkspaceDescription: ({newDescription, oldDescription}: UpdatedPolicyDescriptionParams) =>

src/languages/es.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5555,6 +5555,17 @@ const translations = {
55555555
one: '1 día',
55565556
other: (count: number) => `${count} días`,
55575557
}),
5558+
cashExpenseDefault: 'Valor predeterminado para gastos en efectivo',
5559+
cashExpenseDefaultDescription:
5560+
'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.',
5561+
reimbursableDefault: 'Reembolsable',
5562+
reimbursableDefaultDescription: 'Los gastos suelen ser reembolsados a los empleados',
5563+
nonReimbursableDefault: 'No reembolsable',
5564+
nonReimbursableDefaultDescription: 'Los gastos ocasionalmente son reembolsados a los empleados',
5565+
alwaysReimbursable: 'Siempre reembolsable',
5566+
alwaysReimbursableDescription: 'Los gastos siempre se reembolsados a los empleados',
5567+
alwaysNonReimbursable: 'Siempre no reembolsable',
5568+
alwaysNonReimbursableDescription: 'Los gastos nunca son reembolsados a los empleados',
55585569
billableDefault: 'Valor predeterminado facturable',
55595570
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',
55605571
billable: 'Facturable',
@@ -5822,6 +5833,8 @@ const translations = {
58225833
`actualizó "Antigüedad máxima de gastos (días)" a "${newValue}" (previamente "${oldValue === 'false' ? CONST.POLICY.DEFAULT_MAX_EXPENSE_AGE : oldValue}")`,
58235834
updateDefaultBillable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) =>
58245835
`actualizó "Volver a facturar gastos a clientes" a "${newValue}" (previamente "${oldValue}")`,
5836+
updateDefaultReimbursable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) =>
5837+
`actualizó "Valor predeterminado para gastos en efectivo" a "${newValue}" (previamente "${oldValue}")`,
58255838
updateMonthlyOffset: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => {
58265839
if (!oldValue) {
58275840
return `establecer la fecha de envío del informe mensual a "${newValue}"`;

0 commit comments

Comments
 (0)