diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 3d2d7b47486b1..d691560a992f6 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -89,6 +89,7 @@ import { isOpenExpenseReport, isProcessingReport, isReportOwner, + isSelfDM, navigateOnDeleteExpense, navigateToDetailsPage, rejectMoneyRequestReason, @@ -101,6 +102,7 @@ import { getOriginalTransactionWithSplitInfo, hasCustomUnitOutOfPolicyViolation as hasCustomUnitOutOfPolicyViolationTransactionUtils, hasDuplicateTransactions, + isDistanceRequest, isDuplicate, isExpensifyCardTransaction, isPayAtEndExpense as isPayAtEndExpenseTransactionUtils, @@ -438,7 +440,12 @@ function MoneyReportHeader({ ); const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport); + const isDistanceExpenseUnsupportedForDuplicating = !!( + isDistanceRequest(transaction) && + (isArchivedReport || isChatReportArchived || (activePolicyExpenseChat && (isDM(chatReport) || isSelfDM(chatReport)))) + ); + const [duplicateDistanceErrorModalVisible, setDuplicateDistanceErrorModalVisible] = useState(false); const [rateErrorModalVisible, setRateErrorModalVisible] = useState(false); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); const [isHoldEducationalModalVisible, setIsHoldEducationalModalVisible] = useState(false); @@ -1500,6 +1507,11 @@ function MoneyReportHeader({ return; } + if (isDistanceExpenseUnsupportedForDuplicating) { + setDuplicateDistanceErrorModalVisible(true); + return; + } + if (isPerDiemRequestOnNonDefaultWorkspace) { setDuplicatePerDiemErrorModalVisible(true); return; @@ -1513,7 +1525,11 @@ function MoneyReportHeader({ duplicateExpenseTransaction([transaction]); }, - shouldCloseModalOnSelect: isPerDiemRequestOnNonDefaultWorkspace || hasCustomUnitOutOfPolicyViolation || activePolicyExpenseChat?.iouReportID === moneyRequestReport?.reportID, + shouldCloseModalOnSelect: + isDistanceExpenseUnsupportedForDuplicating || + isPerDiemRequestOnNonDefaultWorkspace || + hasCustomUnitOutOfPolicyViolation || + activePolicyExpenseChat?.iouReportID === moneyRequestReport?.reportID, }, [CONST.REPORT.SECONDARY_ACTIONS.DUPLICATE_REPORT]: { text: translate('common.duplicateReport'), @@ -2022,6 +2038,15 @@ function MoneyReportHeader({ prompt={translate('iou.correctRateError')} shouldShowCancelButton={false} /> + setDuplicateDistanceErrorModalVisible(false)} + onCancel={() => setDuplicateDistanceErrorModalVisible(false)} + confirmText={translate('common.buttonConfirm')} + prompt={translate('iou.cannotDuplicateDistanceExpense')} + shouldShowCancelButton={false} + /> = { failedToSubmitViaDEW: (reason: string) => `Der Bericht konnte nicht übermittelt werden. ${reason}`, failedToAutoApproveViaDEW: (reason: string) => `Genehmigung über Workspace-Regeln fehlgeschlagen. ${reason}`, failedToApproveViaDEW: (reason: string) => `Genehmigung fehlgeschlagen. ${reason}`, + cannotDuplicateDistanceExpense: + 'Sie können Entfernungsausgaben nicht über mehrere Arbeitsbereiche hinweg duplizieren, da sich die Sätze zwischen den Arbeitsbereichen unterscheiden können.', }, transactionMerge: { listPage: { @@ -8596,7 +8598,6 @@ Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`, addMember: 'Dieses Mitglied kann nicht hinzugefügt werden. Bitte versuche es erneut.', vacationDelegate: 'Dieser Benutzer kann nicht als Urlaubsvertretung festgelegt werden. Bitte versuche es erneut.', }, - reportSuspiciousActivityPrompt: (email: string) => `Bist du sicher? Dadurch wird das Konto von ${email} gesperrt.

Unser Team wird das Konto anschließend überprüfen und unbefugten Zugriff entfernen. Um den Zugriff wiederherzustellen, muss die Person mit Concierge zusammenarbeiten.`, reportSuspiciousActivityConfirmationPrompt: 'Wir überprüfen das Konto, um sicherzustellen, dass es sicher entsperrt werden kann, und melden uns bei Fragen über Concierge.', diff --git a/src/languages/en.ts b/src/languages/en.ts index d1c59616cbf91..56f7b42edf8b4 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1620,6 +1620,7 @@ const translations = { formatPolicyRules: (fragments: string, route: string) => `${fragments} via workspace rules`, }, duplicateNonDefaultWorkspacePerDiemError: "You can't duplicate per diem expenses across workspaces because the rates may differ between workspaces.", + cannotDuplicateDistanceExpense: "You can't duplicate distance expenses across workspaces because the rates may differ between workspaces.", }, transactionMerge: { listPage: { diff --git a/src/languages/es.ts b/src/languages/es.ts index eda4ec0b7899e..6edcdf440cf7e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1454,6 +1454,7 @@ const translations: TranslationDeepObject = { formatPolicyRules: (fragments: string, route: string) => `${fragments} vía reglas del espacio de trabajo`, }, duplicateNonDefaultWorkspacePerDiemError: 'No puedes duplicar gastos de viáticos entre espacios de trabajo porque las tarifas pueden variar entre ellos.', + cannotDuplicateDistanceExpense: 'No puedes duplicar gastos de distancia entre espacios de trabajo porque las tasas pueden diferir entre espacios de trabajo.', }, transactionMerge: { listPage: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 4e3309f1fbf39..fa75cf0325823 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1606,6 +1606,7 @@ const translations: TranslationDeepObject = { failedToAutoApproveViaDEW: (reason: string) => `impossible d’approuver via les règles de l’espace de travail. ${reason}`, failedToApproveViaDEW: (reason: string) => `échec de l’approbation. ${reason}`, + cannotDuplicateDistanceExpense: 'Vous ne pouvez pas dupliquer des dépenses de distance entre espaces de travail, car les taux peuvent différer d’un espace de travail à l’autre.', }, transactionMerge: { listPage: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 21557b0403610..9e78b24f5005b 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1597,6 +1597,7 @@ const translations: TranslationDeepObject = { failedToAutoApproveViaDEW: (reason: string) => `approvazione non riuscita tramite le regole dello spazio di lavoro. ${reason}`, failedToApproveViaDEW: (reason: string) => `approvazione non riuscita. ${reason}`, + cannotDuplicateDistanceExpense: 'Non puoi duplicare le spese chilometriche tra diversi spazi di lavoro perché le tariffe potrebbero essere diverse.', }, transactionMerge: { listPage: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 90d56d212fe61..f7f87c719fe4f 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1586,6 +1586,7 @@ const translations: TranslationDeepObject = { failedToSubmitViaDEW: (reason: string) => `レポートの送信に失敗しました。${reason}`, failedToAutoApproveViaDEW: (reason: string) => `ワークスペースルールで承認に失敗しました。${reason}`, failedToApproveViaDEW: (reason: string) => `承認に失敗しました。${reason}`, + cannotDuplicateDistanceExpense: '距離精算はワークスペースごとにレートが異なる可能性があるため、ワークスペース間で複製することはできません。', }, transactionMerge: { listPage: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 5b0bf0eee75a6..7de585afa8938 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1594,6 +1594,7 @@ const translations: TranslationDeepObject = { failedToSubmitViaDEW: (reason: string) => `het is niet gelukt om het rapport in te dienen. ${reason}`, failedToAutoApproveViaDEW: (reason: string) => `goedkeuren via werkruimte­regels is mislukt. ${reason}`, failedToApproveViaDEW: (reason: string) => `goedkeuren mislukt. ${reason}`, + cannotDuplicateDistanceExpense: 'Je kunt afstandsvergoedingen niet dupliceren tussen werkruimtes, omdat de tarieven per werkruimte kunnen verschillen.', }, transactionMerge: { listPage: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 20148b5e53e78..af0b92efdd988 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1595,6 +1595,7 @@ const translations: TranslationDeepObject = { failedToAutoApproveViaDEW: (reason: string) => `nie udało się zatwierdzić przez zasady w przestrzeni roboczej. ${reason}`, failedToApproveViaDEW: (reason: string) => `nie udało się zaakceptować. ${reason}`, + cannotDuplicateDistanceExpense: 'Nie możesz duplikować wydatków za przejazdy między przestrzeniami roboczymi, ponieważ stawki mogą się różnić między poszczególnymi przestrzeniami.', }, transactionMerge: { listPage: { @@ -8546,7 +8547,6 @@ Oto *paragon testowy*, żeby pokazać Ci, jak to działa:`, vacationDelegate: 'Nie można ustawić tego użytkownika jako zastępującego na czas nieobecności. Spróbuj ponownie.', }, cannotSetVacationDelegateForMember: (email: string) => `Nie możesz ustawić zastępstwa urlopowego dla ${email}, ponieważ jest on/ona obecnie zastępcą dla następujących członków:`, - reportSuspiciousActivityPrompt: (email: string) => `Czy na pewno? To zablokuje konto użytkownika ${email}.

Nasz zespół następnie przejrzy konto i usunie wszelki nieautoryzowany dostęp. Aby odzyskać dostęp, będą musieli współpracować z Concierge.`, reportSuspiciousActivityConfirmationPrompt: 'Przejrzymy konto, aby potwierdzić, że bezpiecznie je odblokować, i skontaktujemy się przez Concierge w razie pytań.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index ba6fc6953beec..f6b5054dd567d 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1591,6 +1591,7 @@ const translations: TranslationDeepObject = { failedToSubmitViaDEW: (reason: string) => `falha ao enviar o relatório. ${reason}`, failedToAutoApproveViaDEW: (reason: string) => `falha ao aprovar pelas regras do workspace. ${reason}`, failedToApproveViaDEW: (reason: string) => `falha ao aprovar. ${reason}`, + cannotDuplicateDistanceExpense: 'Você não pode duplicar despesas de distância entre espaços de trabalho porque as tarifas podem ser diferentes entre eles.', }, transactionMerge: { listPage: { @@ -8551,7 +8552,6 @@ Aqui está um *comprovante de teste* para mostrar como funciona:`, vacationDelegate: 'Não foi possível definir este usuário como delegado de férias. Tente novamente.', }, cannotSetVacationDelegateForMember: (email: string) => `Você não pode definir um procurador de férias para ${email} porque esta pessoa já é procuradora dos seguintes membros:`, - reportSuspiciousActivityPrompt: (email: string) => `Tem certeza? Isso irá bloquear a conta de ${email}.

Nossa equipe irá então analisar a conta e remover qualquer acesso não autorizado. Para recuperar o acesso, será necessário que trabalhem com a Concierge.`, reportSuspiciousActivityConfirmationPrompt: 'Vamos revisar a conta para verificar se é seguro desbloqueá-la e entraremos em contato via Concierge caso haja dúvidas.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 2b4d9d8da7e78..479231ccb2d7e 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1563,6 +1563,7 @@ const translations: TranslationDeepObject = { failedToSubmitViaDEW: (reason: string) => `报表提交失败。${reason}`, failedToAutoApproveViaDEW: (reason: string) => `未能通过工作区规则批准。${reason}`, failedToApproveViaDEW: (reason: string) => `批准失败。${reason}`, + cannotDuplicateDistanceExpense: '你无法在不同工作区之间复制里程报销,因为各个工作区的费率可能不同。', }, transactionMerge: { listPage: { diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index e2338defac332..fb115669cb11e 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -69,7 +69,6 @@ import { getOriginalTransactionWithSplitInfo, hasReceipt as hasReceiptTransactionUtils, hasSubmissionBlockingViolations, - isDistanceRequest as isDistanceRequestTransactionUtils, isDuplicate, isManagedCardTransaction as isManagedCardTransactionTransactionUtils, isOdometerDistanceRequest, @@ -802,10 +801,6 @@ function isDuplicateAction(report: Report, reportTransactions: Transaction[]): b const reportTransaction = reportTransactions.at(0); - if (isDistanceRequestTransactionUtils(reportTransaction)) { - return false; - } - // We can't duplicate per diem expenses that don't have start & end dates. const dates = reportTransaction?.comment?.customUnit?.attributes?.dates; if (isPerDiemRequestTransactionUtils(reportTransaction) && (!dates?.start || !dates?.end)) { diff --git a/src/libs/actions/IOU/Duplicate.ts b/src/libs/actions/IOU/Duplicate.ts index 822b7c41ad138..f4f14a0a496ec 100644 --- a/src/libs/actions/IOU/Duplicate.ts +++ b/src/libs/actions/IOU/Duplicate.ts @@ -18,7 +18,7 @@ import { buildTransactionThread, getTransactionDetails, } from '@libs/ReportUtils'; -import {getRequestType, getTransactionType} from '@libs/TransactionUtils'; +import {getRequestType, getTransactionType, isDistanceRequest, isExpenseSplit} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -545,6 +545,9 @@ function duplicateExpenseTransaction({ // to try to create a transaction thread report with an ID that already exists. const {linkedTrackedExpenseReportAction, ...transactionWithoutLinkedAction} = transaction; + // We remove waypoints for split distance expenses in order to preserve the split's amount and distance. + const waypoints = !isExpenseSplit(transaction) ? (transactionDetails?.waypoints as WaypointCollection) : undefined; + const params: RequestMoneyInformation = { report: targetReport, optimisticChatReportID, @@ -571,7 +574,7 @@ function duplicateExpenseTransaction({ originalTransactionID: undefined, receipt: undefined, source: undefined, - waypoints: transactionDetails?.waypoints as WaypointCollection | undefined, + waypoints, type: transaction?.comment?.type, count: transaction?.comment?.units?.count, rate: transaction?.comment?.units?.rate, @@ -592,6 +595,11 @@ function duplicateExpenseTransaction({ personalDetails, }; + // Since we remove waypoints for split distance expenses, we need to re-add the distance param here + if (isExpenseSplit(transaction) && isDistanceRequest(transaction)) { + params.transactionParams.distance = transaction.comment?.customUnit?.quantity ?? undefined; + } + // If no workspace is provided the expense should be unreported if (!targetPolicy) { const trackExpenseParams: CreateTrackExpenseParams = { @@ -600,9 +608,21 @@ function duplicateExpenseTransaction({ ...(params.participantParams ?? {}), participant: {accountID: userAccountID, selected: true}, }, + existingTransaction: { + ...(params.transactionParams ?? {}), + comment: { + ...transaction.comment, + originalTransactionID: undefined, + source: undefined, + }, + iouRequestType: getRequestType(transaction), + modifiedCreated: '', + reportID: '1', + transactionID: '1', + }, transactionParams: { ...(params.transactionParams ?? {}), - validWaypoints: transactionDetails?.waypoints as WaypointCollection | undefined, + validWaypoints: waypoints, }, report: undefined, isDraftPolicy: false, @@ -632,7 +652,11 @@ function duplicateExpenseTransaction({ participants, existingTransaction: { ...(params.transactionParams ?? {}), - comment: transaction.comment, + comment: { + ...transaction.comment, + originalTransactionID: undefined, + source: undefined, + }, iouRequestType: getRequestType(transaction), modifiedCreated: '', reportID: '1', @@ -641,7 +665,7 @@ function duplicateExpenseTransaction({ transactionParams: { ...(params.transactionParams ?? {}), comment: Parser.htmlToMarkdown(transactionDetails?.comment ?? ''), - validWaypoints: transactionDetails?.waypoints as WaypointCollection | undefined, + validWaypoints: waypoints, }, policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [], quickAction, diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 7d13ce53d9255..66d2bc7e16c88 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -532,6 +532,7 @@ type PerDiemExpenseInformation = { policyRecentlyUsedCurrencies: string[]; betas: OnyxEntry; customUnitPolicyID?: string; + shouldHandleNavigation?: boolean; }; type PerDiemExpenseInformationParams = { @@ -749,6 +750,7 @@ type CreateTrackExpenseParams = { participantParams: RequestMoneyParticipantParams; policyParams?: BasePolicyParams; transactionParams: TrackExpenseTransactionParams; + existingTransaction?: OnyxEntry; accountantParams?: TrackExpenseAccountantParams; isRetry?: boolean; shouldPlaySound?: boolean; @@ -793,6 +795,7 @@ type GetTrackExpenseInformationParticipantParams = { type GetTrackExpenseInformationParams = { parentChatReport: OnyxEntry; moneyRequestReportID?: string; + existingTransaction?: OnyxEntry; existingTransactionID?: string; participantParams: GetTrackExpenseInformationParticipantParams; policyParams: BasePolicyParams; @@ -4077,6 +4080,7 @@ function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): T const { parentChatReport, moneyRequestReportID = '', + existingTransaction, existingTransactionID, participantParams, policyParams, @@ -4291,14 +4295,14 @@ function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): T // But we'll use the `shouldUseMoneyReport && iouReport` check further instead of `shouldUseMoneyReport` to avoid TS errors. // STEP 3: Build optimistic receipt and transaction - const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${existingTransactionID ?? CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; - const isDistanceRequest = existingTransaction && isDistanceRequestTransactionUtils(existingTransaction); - const isManualDistanceRequest = existingTransaction && isManualDistanceRequestTransactionUtils(existingTransaction); - const isOdometerDistanceRequest = existingTransaction && isOdometerDistanceRequestTransactionUtils(existingTransaction); - const isGPSDistanceRequest = existingTransaction && isGPSDistanceRequestTransactionUtils(existingTransaction); + const existingTransactionData = existingTransaction ?? allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${existingTransactionID ?? CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; + const isDistanceRequest = existingTransactionData && isDistanceRequestTransactionUtils(existingTransactionData); + const isManualDistanceRequest = existingTransactionData && isManualDistanceRequestTransactionUtils(existingTransactionData); + const isOdometerDistanceRequest = existingTransactionData && isOdometerDistanceRequestTransactionUtils(existingTransactionData); + const isGPSDistanceRequest = existingTransactionData && isGPSDistanceRequestTransactionUtils(existingTransactionData); let optimisticTransaction = buildOptimisticTransaction({ existingTransactionID: optimisticTransactionID, - existingTransaction, + existingTransaction: existingTransactionData, policy, transactionParams: { amount: -amount, @@ -4316,7 +4320,7 @@ function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): T billable, pendingFields: isDistanceRequest && !isManualDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, reimbursable, - filename: existingTransaction?.receipt?.filename, + filename: existingTransactionData?.receipt?.filename, attendees, odometerStart: isOdometerDistanceRequest ? odometerStart : undefined, odometerEnd: isOdometerDistanceRequest ? odometerEnd : undefined, @@ -4333,7 +4337,7 @@ function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): T // I want to clean this up at some point, but it's possible this will live in the code for a while so I've created https://github.com/Expensify/App/issues/25417 // to remind me to do this. if (isDistanceRequest) { - optimisticTransaction = fastMerge(existingTransaction, optimisticTransaction, false); + optimisticTransaction = fastMerge(existingTransactionData, optimisticTransaction, false); } // STEP 4: Build optimistic reportActions. We need: @@ -6878,6 +6882,7 @@ function submitPerDiemExpense(submitPerDiemExpenseInformation: PerDiemExpenseInf policyRecentlyUsedCurrencies, betas, customUnitPolicyID, + shouldHandleNavigation = true, } = submitPerDiemExpenseInformation; const {payeeAccountID} = participantParams; const {currency, comment = '', category, tag, created, customUnit, attendees, isFromGlobalCreate} = transactionParams; @@ -6969,7 +6974,7 @@ function submitPerDiemExpense(submitPerDiemExpenseInformation: PerDiemExpenseInf // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => removeDraftTransaction(CONST.IOU.OPTIMISTIC_TRANSACTION_ID)); - handleNavigateAfterExpenseCreate({activeReportID, transactionID: transaction.transactionID, isFromGlobalCreate}); + handleNavigateAfterExpenseCreate({activeReportID, transactionID: transaction.transactionID, isFromGlobalCreate, shouldHandleNavigation}); if (activeReportID) { notifyNewAction(activeReportID, undefined, payeeAccountID === currentUserAccountIDParam); @@ -7384,6 +7389,7 @@ function trackExpense(params: CreateTrackExpenseParams) { isDraftPolicy, participantParams, policyParams: policyData = {}, + existingTransaction, transactionParams: transactionData, accountantParams, shouldHandleNavigation = true, @@ -7488,6 +7494,7 @@ function trackExpense(params: CreateTrackExpenseParams) { } = getTrackExpenseInformation({ parentChatReport: currentChatReport, moneyRequestReportID, + existingTransaction, existingTransactionID: isMovingTransactionFromTrackExpense && linkedTrackedExpenseReportAction && isMoneyRequestAction(linkedTrackedExpenseReportAction) ? getOriginalMessage(linkedTrackedExpenseReportAction)?.IOUTransactionID