Skip to content
7 changes: 7 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2881,6 +2881,12 @@ const CONST = {
REIMBURSEMENT_NO: 'reimburseNo', // None
REIMBURSEMENT_MANUAL: 'reimburseManual', // Indirect
},
CASH_EXPENSE_REIMBURSEMENT_CHOICES: {
REIMBURSABLE_DEFAULT: 'reimbursableDefault', // Reimbursable by default
NON_REIMBURSABLE_DEFAULT: 'nonReimbursableDefault', // Non-reimbursable by default
ALWAYS_REIMBURSABLE: 'alwaysReimbursable', // Always Reimbursable
ALWAYS_NON_REIMBURSABLE: 'alwaysNonReimbursable', // Always Non Reimbursable
},
ID_FAKE: '_FAKE_',
EMPTY: 'EMPTY',
SECONDARY_ACTIONS: {
Expand Down Expand Up @@ -3747,6 +3753,7 @@ const CONST = {
TAG: 'tag',
TAX_RATE: 'taxRate',
TAX_AMOUNT: 'taxAmount',
REIMBURSABLE: 'reimbursable',
REPORT: 'report',
},
FOOTER: {
Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1933,6 +1933,10 @@ const ROUTES = {
route: 'workspaces/:policyID/rules/billable',
getRoute: (policyID: string) => `workspaces/${policyID}/rules/billable` as const,
},
RULES_REIMBURSABLE_DEFAULT: {
route: 'workspaces/:policyID/rules/reimbursable',
getRoute: (policyID: string) => `workspaces/${policyID}/rules/reimbursable` as const,
},
RULES_PROHIBITED_DEFAULT: {
route: 'workspaces/:policyID/rules/prohibited',
getRoute: (policyID: string) => `workspaces/${policyID}/rules/prohibited` as const,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ const SCREENS = {
RULES_MAX_EXPENSE_AMOUNT: 'Rules_Max_Expense_Amount',
RULES_MAX_EXPENSE_AGE: 'Rules_Max_Expense_Age',
RULES_BILLABLE_DEFAULT: 'Rules_Billable_Default',
RULES_REIMBURSABLE_DEFAULT: 'Rules_Reimbursable_Default',
RULES_CUSTOM: 'Rules_Custom',
RULES_PROHIBITED_DEFAULT: 'Rules_Prohibited_Default',
PER_DIEM: 'Per_Diem',
Expand Down
10 changes: 10 additions & 0 deletions src/components/MoneyRequestConfirmationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ type MoneyRequestConfirmationListProps = {
/** The PDF password callback */
onPDFPassword?: () => void;

/** Function to toggle reimbursable */
onToggleReimbursable?: (isOn: boolean) => void;

/** Flag indicating if the IOU is reimbursable */
iouIsReimbursable?: boolean;

/** Show remove expense confirmation modal */
showRemoveExpenseConfirmModal?: () => void;
};
Expand Down Expand Up @@ -225,6 +231,8 @@ function MoneyRequestConfirmationList({
isConfirming,
onPDFLoadError,
onPDFPassword,
iouIsReimbursable = true,
onToggleReimbursable,
showRemoveExpenseConfirmModal,
}: MoneyRequestConfirmationListProps) {
const [policyCategoriesReal] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true});
Expand Down Expand Up @@ -1154,6 +1162,8 @@ function MoneyRequestConfirmationList({
unit={unit}
onPDFLoadError={onPDFLoadError}
onPDFPassword={onPDFPassword}
iouIsReimbursable={iouIsReimbursable}
onToggleReimbursable={onToggleReimbursable}
isReceiptEditable={isReceiptEditable}
/>
);
Expand Down
29 changes: 29 additions & 0 deletions src/components/MoneyRequestConfirmationListFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
getTaxAmount,
getTaxName,
isAmountMissing,
isCardTransaction,
isCreatedMissing,
isFetchingWaypointsFromServer,
shouldShowAttendees as shouldShowAttendeesTransactionUtils,
Expand Down Expand Up @@ -198,6 +199,12 @@ type MoneyRequestConfirmationListFooterProps = {

/** The PDF password callback */
onPDFPassword?: () => void;

/** Function to toggle reimbursable */
onToggleReimbursable?: (isOn: boolean) => void;

/** Flag indicating if the IOU is reimbursable */
iouIsReimbursable: boolean;
};

function MoneyRequestConfirmationListFooter({
Expand Down Expand Up @@ -247,6 +254,8 @@ function MoneyRequestConfirmationListFooter({
unit,
onPDFLoadError,
onPDFPassword,
iouIsReimbursable,
onToggleReimbursable,
isReceiptEditable = false,
}: MoneyRequestConfirmationListFooterProps) {
const styles = useThemeStyles();
Expand Down Expand Up @@ -313,6 +322,7 @@ function MoneyRequestConfirmationListFooter({
const canModifyTaxFields = !isReadOnly && !isDistanceRequest && !isPerDiemRequest;
// A flag for showing the billable field
const shouldShowBillable = policy?.disabledFields?.defaultBillable === false;
const shouldShowReimbursable = isPolicyExpenseChat && policy?.disabledFields?.reimbursable === false && !isTypeInvoice;
// Calculate the formatted tax amount based on the transaction's tax amount and the IOU currency code
const taxAmount = getTaxAmount(transaction, false);
const formattedTaxAmount = convertToDisplayString(taxAmount, iouCurrencyCode);
Expand Down Expand Up @@ -643,6 +653,25 @@ function MoneyRequestConfirmationListFooter({
),
shouldShow: shouldShowAttendees,
},
{
item: (
<View
key={Str.UCFirst(translate('iou.reimbursable'))}
style={[styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8, styles.optionRow]}
>
<ToggleSettingOptionRow
switchAccessibilityLabel={Str.UCFirst(translate('iou.reimbursable'))}
title={Str.UCFirst(translate('iou.reimbursable'))}
onToggle={(isOn) => onToggleReimbursable?.(isOn)}
isActive={iouIsReimbursable}
disabled={isReadOnly}
wrapperStyle={styles.flex1}
/>
</View>
),
shouldShow: shouldShowReimbursable,
isSupplementary: true,
},
{
item: (
<View
Expand Down
49 changes: 44 additions & 5 deletions src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {Str} from 'expensify-common';
import mapValues from 'lodash/mapValues';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {View} from 'react-native';
Expand Down Expand Up @@ -61,6 +62,7 @@ import {
getCurrency,
getDescription,
getDistanceInMeters,
getReimbursable,
getTagForDisplay,
getTaxName,
hasMissingSmartscanFields,
Expand All @@ -77,7 +79,7 @@ import {
import ViolationsUtils from '@libs/Violations/ViolationsUtils';
import Navigation from '@navigation/Navigation';
import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground';
import {cleanUpMoneyRequest, updateMoneyRequestBillable} from '@userActions/IOU';
import {cleanUpMoneyRequest, updateMoneyRequestBillable, updateMoneyRequestReimbursable} from '@userActions/IOU';
import {navigateToConciergeChatAndDeleteReport} from '@userActions/Report';
import {clearAllRelatedReportActionErrors} from '@userActions/ReportActions';
import {clearError, getLastModifiedExpense, revert} from '@userActions/Transaction';
Expand Down Expand Up @@ -187,6 +189,7 @@ function MoneyRequestView({
currency: transactionCurrency,
comment: transactionDescription,
merchant: transactionMerchant,
reimbursable: transactionReimbursable,
billable: transactionBillable,
category: transactionCategory,
tag: transactionTag,
Expand Down Expand Up @@ -228,6 +231,7 @@ function MoneyRequestView({
const isSettled = isSettledReportUtils(moneyRequestReport?.reportID);
const isCancelled = moneyRequestReport && moneyRequestReport?.isCancelledIOU;
const isChatReportArchived = useReportIsArchived(moneyRequestReport?.chatReportID);
const shouldShowPaid = isSettled && transactionReimbursable;

// Flags for allowing or disallowing editing an expense
// Used for non-restricted fields such as: description, category, tag, billable, etc...
Expand Down Expand Up @@ -274,6 +278,11 @@ function MoneyRequestView({
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const shouldShowTag = isPolicyExpenseChat && (transactionTag || hasEnabledTags(policyTagLists));
const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true) || !!updatedTransaction?.billable);
const isCurrentTransactionReimbursableDifferentFromPolicyDefault =
policy?.defaultReimbursable !== undefined && !!(updatedTransaction?.reimbursable ?? transactionReimbursable) !== policy.defaultReimbursable;
const shouldShowReimbursable =
isPolicyExpenseChat && (!policy?.disabledFields?.reimbursable || isCurrentTransactionReimbursableDifferentFromPolicyDefault) && !isCardTransaction && !isInvoice;
const canEditReimbursable = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.REIMBURSABLE);
const shouldShowAttendees = useMemo(() => shouldShowAttendeesTransactionUtils(iouType, policy), [iouType, policy]);

const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest, isPerDiemRequest);
Expand Down Expand Up @@ -323,6 +332,17 @@ function MoneyRequestView({
[transaction, report, policy, policyTagList, policyCategories],
);

const saveReimbursable = useCallback(
(newReimbursable: boolean) => {
// If the value hasn't changed, don't request to save changes on the server and just close the modal
if (newReimbursable === getReimbursable(transaction) || !transaction?.transactionID || !report?.reportID) {
return;
}
updateMoneyRequestReimbursable(transaction.transactionID, report?.reportID, newReimbursable, policy, policyTagList, policyCategories);
},
[transaction, report, policy, policyTagList, policyCategories],
);

if (isCardTransaction) {
if (transactionPostedDate) {
dateDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.posted')} ${transactionPostedDate}`;
Expand All @@ -347,7 +367,7 @@ function MoneyRequestView({
amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.canceled')}`;
} else if (isApproved) {
amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.approved')}`;
} else if (isSettled) {
} else if (shouldShowPaid) {
amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('iou.settledExpensify')}`;
}
}
Expand Down Expand Up @@ -706,7 +726,7 @@ function MoneyRequestView({
<OfflineWithFeedback pendingAction={getPendingFieldAction('amount') ?? (amountTitle ? getPendingFieldAction('customUnitRateID') : undefined)}>
<MenuItemWithTopDescription
title={amountTitle}
shouldShowTitleIcon={isSettled}
shouldShowTitleIcon={shouldShowPaid}
titleIcon={Expensicons.Checkmark}
description={amountDescription}
titleStyle={styles.textHeadlineH2}
Expand Down Expand Up @@ -889,8 +909,27 @@ function MoneyRequestView({
/>
</OfflineWithFeedback>
)}
{shouldShowReimbursable && (
<OfflineWithFeedback
pendingAction={getPendingFieldAction('reimbursable')}
contentContainerStyle={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}
>
<View>
<Text>{Str.UCFirst(translate('iou.reimbursable'))}</Text>
</View>
<Switch
accessibilityLabel={Str.UCFirst(translate('iou.reimbursable'))}
isOn={updatedTransaction?.reimbursable ?? !!transactionReimbursable}
onToggle={saveReimbursable}
disabled={!canEditReimbursable}
/>
</OfflineWithFeedback>
)}
{shouldShowBillable && (
<View style={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}>
<OfflineWithFeedback
pendingAction={getPendingFieldAction('billable')}
contentContainerStyle={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}
>
<View>
<Text>{translate('common.billable')}</Text>
{!!getErrorForField('billable') && (
Expand All @@ -909,7 +948,7 @@ function MoneyRequestView({
onToggle={saveBillable}
disabled={!canEdit}
/>
</View>
</OfflineWithFeedback>
)}
{!!parentReportID && (
<OfflineWithFeedback pendingAction={getPendingFieldAction('reportID')}>
Expand Down
12 changes: 12 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5546,6 +5546,17 @@ const translations = {
one: '1 Tag',
other: (count: number) => `${count} Tage`,
}),
cashExpenseDefault: 'Bargeldausgabe standard',
cashExpenseDefaultDescription:
'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.',
reimbursableDefault: 'Erstattungsfähig',
reimbursableDefaultDescription: 'Ausgaben werden meistens an Mitarbeiter zurückgezahlt',
nonReimbursableDefault: 'Nicht erstattungsfähig',
nonReimbursableDefaultDescription: 'Ausgaben werden gelegentlich an Mitarbeiter zurückgezahlt',
alwaysReimbursable: 'Immer erstattungsfähig',
alwaysReimbursableDescription: 'Ausgaben werden immer an Mitarbeiter zurückgezahlt',
alwaysNonReimbursable: 'Nie erstattungsfähig',
alwaysNonReimbursableDescription: 'Ausgaben werden nie an Mitarbeiter zurückgezahlt',
billableDefault: 'Abrechnungsstandard',
billableDefaultDescription: ({tagsPageLink}: BillableDefaultDescriptionParams) =>
`<muted-text>Wählen Sie aus, ob Bar- und Kreditkartenausgaben standardmäßig abrechnungsfähig sein sollen. Abrechnungsfähige Ausgaben werden in <a href="${tagsPageLink}">Tags</a> aktiviert oder deaktiviert.</muted-text>`,
Expand Down Expand Up @@ -5837,6 +5848,7 @@ const translations = {
},
updateDefaultBillable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) =>
`aktualisiert "Kosten an Kunden weiterberechnen" auf "${newValue}" (vorher "${oldValue}")`,
updateDefaultReimbursable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `aktualisiert "Bargeldausgabe Standard" auf "${newValue}" (vorher "${oldValue}")`,
updateDefaultTitleEnforced: ({value}: UpdatedPolicyFieldWithValueParam) => `"Standardberichtstitel erzwingen" ${value ? 'on' : 'aus'}`,
renamedWorkspaceNameAction: ({oldName, newName}: RenamedWorkspaceNameActionParams) => `hat den Namen dieses Arbeitsbereichs in "${newName}" geändert (vorher "${oldName}")`,
updateWorkspaceDescription: ({newDescription, oldDescription}: UpdatedPolicyDescriptionParams) =>
Expand Down
12 changes: 12 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5522,6 +5522,17 @@ const translations = {
one: '1 day',
other: (count: number) => `${count} days`,
}),
cashExpenseDefault: 'Cash expense default',
cashExpenseDefaultDescription:
'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.',
reimbursableDefault: 'Reimbursable',
reimbursableDefaultDescription: 'Expenses are most often paid back to employees',
nonReimbursableDefault: 'Non-reimbursable',
nonReimbursableDefaultDescription: 'Expenses are occasionally paid back to employees',
alwaysReimbursable: 'Always reimbursable',
alwaysReimbursableDescription: 'Expenses are always paid back to employees',
alwaysNonReimbursable: 'Always non-reimbursable',
alwaysNonReimbursableDescription: 'Expenses are never paid back to employees',
billableDefault: 'Billable default',
billableDefaultDescription: ({tagsPageLink}: BillableDefaultDescriptionParams) =>
`<muted-text>Choose whether cash and credit card expenses should be billable by default. Billable expenses are enabled or disabled in <a href="${tagsPageLink}">tags</a>.</muted-text>`,
Expand Down Expand Up @@ -5812,6 +5823,7 @@ const translations = {
return `updated the monthly report submission date to "${newValue}" (previously "${oldValue}")`;
},
updateDefaultBillable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `updated "Re-bill expenses to clients" to "${newValue}" (previously "${oldValue}")`,
updateDefaultReimbursable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `updated "Cash expense default" to "${newValue}" (previously "${oldValue}")`,
updateDefaultTitleEnforced: ({value}: UpdatedPolicyFieldWithValueParam) => `turned "Enforce default report titles" ${value ? 'on' : 'off'}`,
renamedWorkspaceNameAction: ({oldName, newName}: RenamedWorkspaceNameActionParams) => `updated the name of this workspace to "${newName}" (previously "${oldName}")`,
updateWorkspaceDescription: ({newDescription, oldDescription}: UpdatedPolicyDescriptionParams) =>
Expand Down
13 changes: 13 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5556,6 +5556,17 @@ const translations = {
one: '1 día',
other: (count: number) => `${count} días`,
}),
cashExpenseDefault: 'Valor predeterminado para gastos en efectivo',
cashExpenseDefaultDescription:
'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.',
reimbursableDefault: 'Reembolsable',
reimbursableDefaultDescription: 'Los gastos suelen ser reembolsados a los empleados',
nonReimbursableDefault: 'No reembolsable',
nonReimbursableDefaultDescription: 'Los gastos ocasionalmente son reembolsados a los empleados',
alwaysReimbursable: 'Siempre reembolsable',
alwaysReimbursableDescription: 'Los gastos siempre se reembolsados a los empleados',
alwaysNonReimbursable: 'Siempre no reembolsable',
alwaysNonReimbursableDescription: 'Los gastos nunca son reembolsados a los empleados',
billableDefault: 'Valor predeterminado facturable',
billableDefaultDescription: ({tagsPageLink}: BillableDefaultDescriptionParams) =>
`<muted-text>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 <a href="${tagsPageLink}">etiquetas</a>.</muted-text>`,
Expand Down Expand Up @@ -5825,6 +5836,8 @@ const translations = {
`actualizó "Antigüedad máxima de gastos (días)" a "${newValue}" (previamente "${oldValue === 'false' ? CONST.POLICY.DEFAULT_MAX_EXPENSE_AGE : oldValue}")`,
updateDefaultBillable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) =>
`actualizó "Volver a facturar gastos a clientes" a "${newValue}" (previamente "${oldValue}")`,
updateDefaultReimbursable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) =>
`actualizó "Valor predeterminado para gastos en efectivo" a "${newValue}" (previamente "${oldValue}")`,
updateMonthlyOffset: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => {
if (!oldValue) {
return `establecer la fecha de envío del informe mensual a "${newValue}"`;
Expand Down
Loading