From 3594cfaa325fa51ab872f71a57b6a522749e1db5 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 25 Sep 2025 16:19:28 +0200 Subject: [PATCH 01/15] fix: revert revert --- src/CONST/index.ts | 8 ++++ .../ReportActionItem/MoneyRequestView.tsx | 16 ++++++- src/languages/de.ts | 6 +++ src/languages/en.ts | 5 ++ src/languages/es.ts | 5 ++ src/languages/fr.ts | 6 +++ src/languages/it.ts | 5 ++ src/languages/ja.ts | 5 ++ src/languages/nl.ts | 5 ++ src/languages/pl.ts | 5 ++ src/languages/pt-BR.ts | 5 ++ src/languages/zh-hans.ts | 5 ++ src/libs/ModifiedExpenseMessage.ts | 2 +- src/libs/Permissions.ts | 8 ---- src/libs/ReportUtils.ts | 24 +++++----- src/libs/TransactionUtils/index.ts | 6 --- src/libs/actions/IOU.ts | 2 + src/libs/actions/Transaction.ts | 1 + src/pages/Search/SearchPage.tsx | 4 +- .../Search/SearchTransactionsChangeReport.tsx | 23 +++++++++ src/pages/home/report/ReportActionItem.tsx | 4 +- .../step/IOURequestEditReportCommon.tsx | 48 +++++++++++++++---- .../iou/request/step/IOURequestStepReport.tsx | 10 ++++ .../step/IOURequestStepTaxAmountPage.tsx | 26 ++++++++-- .../request/step/IOURequestStepUpgrade.tsx | 9 +++- src/pages/workspace/upgrade/UpgradeIntro.tsx | 6 ++- 26 files changed, 203 insertions(+), 46 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 3db16d7fe6cf9..8ef201c98e20b 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6959,6 +6959,14 @@ const CONST = { description: 'workspace.upgrade.travel.description' as const, icon: 'Luggage', }, + reports: { + id: 'reports' as const, + alias: 'reports', + name: 'Reports', + title: 'workspace.upgrade.reports.title' as const, + description: 'workspace.upgrade.reports.description' as const, + icon: 'ReportReceipt', + }, distanceRates: { id: 'distanceRates' as const, alias: 'distance-rates', diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 24639427b7aad..c55c4d89644b6 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -841,7 +841,7 @@ function MoneyRequestView({ , betaConfiguration?: OnyxEntry): boolean { const hasAllBetasEnabled = canUseAllBetas(betas); const isFeatureEnabled = !!betas?.includes(beta); @@ -38,5 +31,4 @@ function isBetaEnabled(beta: Beta, betas: OnyxEntry, betaConfiguration?: export default { canUseLinkPreviews, isBetaEnabled, - canUseUnreportedExpense, }; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index cc5e5495c411c..0ad526cec2171 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4332,7 +4332,11 @@ function canEditFieldOfMoneyRequest( // Unreported transaction from OldDot can have the reportID as an empty string const isUnreportedExpense = !transaction?.reportID || transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; - if (!isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID) && !isUnreportedExpense) { + if (isUnreportedExpense) { + return true; + } + + if (!isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID)) { return false; } @@ -4342,7 +4346,7 @@ function canEditFieldOfMoneyRequest( } const isOwner = moneyRequestReport?.ownerAccountID === currentUserAccountID; - if (isInvoiceReport(moneyRequestReport) && !isUnreportedExpense) { + if (isInvoiceReport(moneyRequestReport)) { return ( getOutstandingReportsForUser( moneyRequestReport?.policyID, @@ -4355,18 +4359,16 @@ function canEditFieldOfMoneyRequest( // If the report is Open, then only submitters, admins can move expenses const isOpen = isOpenExpenseReport(moneyRequestReport); - if (!isUnreportedExpense && isOpen && !isSubmitter && !isAdmin) { + if (isOpen && !isSubmitter && !isAdmin) { return false; } - return isUnreportedExpense - ? Object.values(allPolicies ?? {}).flatMap((currentPolicy) => - getOutstandingReportsForUser(currentPolicy?.id, currentUserAccountID, outstandingReportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}), - ).length > 0 - : Object.values(allPolicies ?? {}).flatMap((currentPolicy) => - getOutstandingReportsForUser(currentPolicy?.id, moneyRequestReport?.ownerAccountID, outstandingReportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}), - ).length > 1 || - (isOwner && isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID)); + return ( + Object.values(allPolicies ?? {}).flatMap((currentPolicy) => + getOutstandingReportsForUser(currentPolicy?.id, moneyRequestReport?.ownerAccountID, outstandingReportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}), + ).length > 1 || + (isOwner && isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID)) + ); } const isUnreportedExpense = !transaction?.reportID || transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 2b0d7c5a9b985..64d7e15733df8 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -18,7 +18,6 @@ import {toLocaleDigit} from '@libs/LocaleDigitUtils'; import {translateLocal} from '@libs/Localize'; import Log from '@libs/Log'; import {rand64, roundToTwoDecimalPlaces} from '@libs/NumberUtils'; -import Permissions from '@libs/Permissions'; import {getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils'; import { getCommaSeparatedTagNameWithSanitizedColons, @@ -1952,12 +1951,7 @@ function createUnreportedExpenseSections(transactions: Array, paymentPolicyID?: string, full = true) { if (chatReport.policyID && shouldRestrictUserBillableActions(chatReport.policyID)) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(chatReport.policyID)); diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index daa1fedd0f3a7..9ebab3748dc29 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -343,6 +343,7 @@ function getRoute(transactionID: string, waypoints: WaypointCollection, routeTyp API.read(command, parameters, getOnyxDataForRouteRequest(transactionID, routeType)); } + /** * Updates all waypoints stored in the transaction specified by the provided transactionID. * diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 1c3618dbaabdd..db8408f8db5b6 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -390,7 +390,8 @@ function SearchPage({route}: SearchPageProps) { }); } - const canAllTransactionsBeMoved = selectedTransactionsKeys.every((id) => selectedTransactions[id].canChangeReport); + const canAllTransactionsBeMoved = + selectedTransactionsKeys.every((id) => selectedTransactions[id].canChangeReport) && !!activePolicy && activePolicy?.type !== CONST.POLICY.TYPE.PERSONAL; if (canAllTransactionsBeMoved) { options.push({ @@ -444,6 +445,7 @@ function SearchPage({route}: SearchPageProps) { return options; }, [ + activePolicy, selectedTransactionsKeys, status, hash, diff --git a/src/pages/Search/SearchTransactionsChangeReport.tsx b/src/pages/Search/SearchTransactionsChangeReport.tsx index 9591597097561..965a08464a844 100644 --- a/src/pages/Search/SearchTransactionsChangeReport.tsx +++ b/src/pages/Search/SearchTransactionsChangeReport.tsx @@ -3,7 +3,9 @@ import {InteractionManager} from 'react-native'; import {useSession} from '@components/OnyxListItemProvider'; import {useSearchContext} from '@components/Search/SearchContext'; import type {ListItem} from '@components/SelectionList/types'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useOnyx from '@hooks/useOnyx'; +import {createNewReport} from '@libs/actions/Report'; import {changeTransactionsReport} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; @@ -25,6 +27,7 @@ function SearchTransactionsChangeReport() { const isASAPSubmitBetaEnabled = Permissions.isBetaEnabled(CONST.BETAS.ASAP_SUBMIT, allBetas); const session = useSession(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const firstTransactionKey = selectedTransactionsKeys.at(0); const firstTransactionReportID = firstTransactionKey ? selectedTransactions[firstTransactionKey]?.reportID : undefined; @@ -33,6 +36,25 @@ function SearchTransactionsChangeReport() { ? firstTransactionReportID : undefined; + // Get the policy ID from the first transaction + const activePolicyID = firstTransactionKey ? selectedTransactions[firstTransactionKey]?.policyID : undefined; + + const createReport = () => { + const createdReportID = createNewReport(currentUserPersonalDetails, activePolicyID); + const reportNextStep = allReportNextSteps?.[`${ONYXKEYS.COLLECTION.NEXT_STEP}${createdReportID}`]; + changeTransactionsReport( + selectedTransactionsKeys, + createdReportID, + isASAPSubmitBetaEnabled, + session?.accountID ?? CONST.DEFAULT_NUMBER_ID, + session?.email ?? '', + activePolicyID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`] : undefined, + reportNextStep, + ); + clearSelectedTransactions(); + Navigation.goBack(); + }; + const selectReport = (item: TransactionGroupListItem) => { if (selectedTransactionsKeys.length === 0) { return; @@ -71,6 +93,7 @@ function SearchTransactionsChangeReport() { selectedReportID={selectedReportID} selectReport={selectReport} removeFromReport={removeFromReport} + createReport={createReport} isEditing /> ); diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 74f235774d7b1..2312bfd7535f1 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -17,6 +17,7 @@ import { isChatThread, isClosedExpenseReportWithNoExpenses, isCurrentUserTheOnlyParticipant, + isSelfDM, } from '@libs/ReportUtils'; import { deleteReportActionDraft, @@ -97,6 +98,7 @@ function ReportActionItem({ const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getIOUReportIDFromReportActionPreview(action)}`]; const movedFromReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(action, CONST.REPORT.MOVE_TYPE.FROM)}`]; const movedToReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(action, CONST.REPORT.MOVE_TYPE.TO)}`]; + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); // The app would crash due to subscribing to the entire report collection if parentReportID is an empty string. So we should have a fallback ID here. @@ -148,7 +150,7 @@ function ReportActionItem({ )} modifiedExpenseMessage={getForReportAction({ reportAction: action, - policyID: report?.policyID, + policyID: isSelfDM(parentReport) ? activePolicyID : report?.policyID, movedFromReport, movedToReport, })} diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx index 01b207c0a9a55..79e8feb7aef11 100644 --- a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx +++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx @@ -38,6 +38,7 @@ type Props = { isEditing?: boolean; isUnreported?: boolean; shouldShowNotFoundPage?: boolean; + createReport?: () => void; }; const policyIdSelector = (policy: OnyxEntry) => policy?.id; @@ -54,6 +55,7 @@ function IOURequestEditReportCommon({ isEditing = false, isUnreported, shouldShowNotFoundPage: shouldShowNotFoundPageFromProps, + createReport, }: Props) { const {translate, localeCompare} = useLocalize(); const {options} = useOptionsList(); @@ -62,6 +64,11 @@ function IOURequestEditReportCommon({ const [selectedReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selectedReportID}`, {canBeMissing: true}); const reportOwnerAccountID = useMemo(() => selectedReport?.ownerAccountID ?? currentUserPersonalDetails.accountID, [selectedReport, currentUserPersonalDetails.accountID]); const reportPolicy = usePolicy(selectedReport?.policyID); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, { + canBeMissing: true, + selector: (policy) => (policy?.type !== CONST.POLICY.TYPE.PERSONAL ? policy : undefined), + }); const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {canBeMissing: true}); const [allPoliciesID] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: policiesSelector, canBeMissing: false}); @@ -145,8 +152,27 @@ function IOURequestEditReportCommon({ const headerMessage = useMemo(() => (searchValue && !reportOptions.length ? translate('common.noResultsFound') : ''), [searchValue, reportOptions, translate]); + const createReportOption = useMemo(() => { + if (!createReport || !isUnreported) { + return undefined; + } + + return ( + + ); + }, [createReport, isUnreported, translate, activePolicy?.name]); + // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useMemo(() => { + if (createReport) { + return false; + } + if (expenseReports.length === 0 || shouldShowNotFoundPageFromProps) { return true; } @@ -160,7 +186,7 @@ function IOURequestEditReportCommon({ const isSubmitter = isReportOwner(selectedReport); // If the report is Open, then only submitters, admins can move expenses return isOpen && !isAdmin && !isSubmitter; - }, [selectedReport, reportPolicy, expenseReports.length, shouldShowNotFoundPageFromProps]); + }, [createReport, selectedReport, reportPolicy, expenseReports.length, shouldShowNotFoundPageFromProps]); return ( - ) : undefined + <> + {shouldShowRemoveFromReport && ( + + )} + {createReportOption} + } + listEmptyContent={createReportOption} /> ); diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index b5e331da9af59..afa9219645416 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -3,8 +3,10 @@ import {InteractionManager} from 'react-native'; import {useSession} from '@components/OnyxListItemProvider'; import {useSearchContext} from '@components/Search/SearchContext'; import type {ListItem} from '@components/SelectionList/types'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useOnyx from '@hooks/useOnyx'; import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; +import {createNewReport} from '@libs/actions/Report'; import {changeTransactionsReport, setTransactionReport} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; @@ -38,6 +40,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { const selectedReportID = shouldUseTransactionReport ? transactionReport?.reportID : outstandingReportID; const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const {removeTransaction} = useSearchContext(); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const reportOrDraftReport = getReportOrDraftReport(reportIDFromRoute); const isEditing = action === CONST.IOU.ACTION.EDIT; const isCreateReport = action === CONST.IOU.ACTION.CREATE; @@ -45,6 +48,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); const isASAPSubmitBetaEnabled = Permissions.isBetaEnabled(CONST.BETAS.ASAP_SUBMIT, allBetas); const session = useSession(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const handleGoBack = () => { if (isEditing) { @@ -158,6 +162,11 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useShowNotFoundPageInIOUStep(action, iouType, reportActionID, reportOrDraftReport, transaction); + const createReport = () => { + const createdReportID = createNewReport(currentUserPersonalDetails, activePolicyID); + handleRegularReportSelection({value: createdReportID}); + }; + return ( ); } diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx index b0b3e09819106..8f8d507f0683e 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx @@ -8,7 +8,14 @@ import {setDraftSplitTransaction, setMoneyRequestCurrency, setMoneyRequestPartic import {convertToBackendAmount, isValidCurrencyCode} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getTransactionDetails} from '@libs/ReportUtils'; -import {calculateTaxAmount, getAmount, getDefaultTaxCode, getTaxValue, getTaxAmount as getTransactionTaxAmount} from '@libs/TransactionUtils'; +import { + calculateTaxAmount, + getAmount, + getDefaultTaxCode, + getTaxValue, + getTaxAmount as getTransactionTaxAmount, + isExpenseUnreported as isExpenseUnreportedTransactionUtils, +} from '@libs/TransactionUtils'; import type {CurrentMoney} from '@pages/iou/MoneyRequestAmountForm'; import MoneyRequestAmountForm from '@pages/iou/MoneyRequestAmountForm'; import CONST from '@src/CONST'; @@ -48,9 +55,18 @@ function IOURequestStepTaxAmountPage({ transaction, report, }: IOURequestStepTaxAmountPageProps) { + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, { + canBeMissing: true, + selector: (policy) => (policy?.type !== CONST.POLICY.TYPE.PERSONAL ? policy : undefined), + }); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, {canBeMissing: true}); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report?.policyID}`, {canBeMissing: true}); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: true}); + const isExpenseUnreported = isExpenseUnreportedTransactionUtils(transaction); + const taxPolicy = isExpenseUnreported ? activePolicy : policy; + const taxPolicyID = isExpenseUnreported ? activePolicyID : report?.policyID; + + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${taxPolicyID}`, {canBeMissing: true}); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${taxPolicyID}`, {canBeMissing: true}); const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, {canBeMissing: true}); const {translate} = useLocalize(); const textInput = useRef(null); @@ -109,7 +125,7 @@ function IOURequestStepTaxAmountPage({ navigateBack(); return; } - updateMoneyRequestTaxAmount(transactionID, report?.reportID, taxAmountInSmallestCurrencyUnits, policy, policyTags, policyCategories); + updateMoneyRequestTaxAmount(transactionID, report?.reportID, taxAmountInSmallestCurrencyUnits, taxPolicy, policyTags, policyCategories); navigateBack(); return; } @@ -151,7 +167,7 @@ function IOURequestStepTaxAmountPage({ isEditing={!!(backTo || isEditing)} currency={currency} amount={Math.abs(transactionDetails?.taxAmount ?? 0)} - taxAmount={getTaxAmount(currentTransaction, policy, currency, !!(backTo || isEditing))} + taxAmount={getTaxAmount(currentTransaction, taxPolicy, currency, !!(backTo || isEditing))} ref={(e) => { textInput.current = e; }} diff --git a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx index f763b7ceadab2..21f7c702db2f9 100644 --- a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx +++ b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx @@ -74,6 +74,9 @@ function IOURequestStepUpgrade({ } Navigation.goBack(); + // If we're submitting the expense to the workspace, we don't need the backTo param + const backTo = action === CONST.IOU.ACTION.CATEGORIZE ? '' : ROUTES.REPORT_WITH_ID.getRoute(reportID); + switch (upgradePath) { case CONST.UPGRADE_PATHS.DISTANCE_RATES: { if (!policyID || !reportID) { @@ -87,7 +90,10 @@ function IOURequestStepUpgrade({ break; } case CONST.UPGRADE_PATHS.CATEGORIES: - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, reportID, ROUTES.REPORT_WITH_ID.getRoute(reportID))); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, reportID, backTo)); + break; + case CONST.UPGRADE_PATHS.REPORTS: + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_REPORT.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, reportID)); break; default: } @@ -168,6 +174,7 @@ function IOURequestStepUpgrade({ buttonDisabled={isOffline} loading={false} isCategorizing={isCategorizing} + isReporting={isReporting} isDistanceRateUpgrade={isDistanceRateUpgrade} /> )} diff --git a/src/pages/workspace/upgrade/UpgradeIntro.tsx b/src/pages/workspace/upgrade/UpgradeIntro.tsx index 9a90ab3c26332..6cdb53763cf8a 100644 --- a/src/pages/workspace/upgrade/UpgradeIntro.tsx +++ b/src/pages/workspace/upgrade/UpgradeIntro.tsx @@ -30,12 +30,14 @@ type Props = { onUpgrade: () => void; /** Whether is categorizing the expense */ isCategorizing?: boolean; + /** Whether is adding an unreported expense to a report */ + isReporting?: boolean; isDistanceRateUpgrade?: boolean; policyID?: string; backTo?: Route; }; -function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizing, isDistanceRateUpgrade, policyID, backTo}: Props) { +function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizing, isDistanceRateUpgrade, isReporting, policyID, backTo}: Props) { const styles = useThemeStyles(); const {isExtraSmallScreenWidth} = useResponsiveLayout(); const {translate} = useLocalize(); @@ -68,7 +70,7 @@ function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizi * The "isCategorizing" flag is set to true when the user accesses the "Categorize" option in the Self-DM whisper. * In such scenarios, a separate Categories upgrade UI is displayed. */ - if (!feature || (!isCategorizing && !isDistanceRateUpgrade && !policyID)) { + if (!feature || (!isCategorizing && !isDistanceRateUpgrade && !isReporting && !policyID)) { return ( Date: Thu, 25 Sep 2025 10:51:40 +0200 Subject: [PATCH 02/15] fix: do not show category when no categories on the workspace --- .../ReportActionItem/MoneyRequestView.tsx | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index c55c4d89644b6..fe16b35b0feed 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -83,6 +83,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {TransactionPendingFieldsKey} from '@src/types/onyx/Transaction'; import MoneyRequestReceiptView from './MoneyRequestReceiptView'; + type MoneyRequestViewProps = { /** All the data of the report collection */ allReports: OnyxCollection; @@ -110,15 +111,15 @@ type MoneyRequestViewProps = { }; function MoneyRequestView({ - allReports, - report, - expensePolicy, - shouldShowAnimatedBackground, - readonly = false, - updatedTransaction, - isFromReviewDuplicates = false, - mergeTransactionID, -}: MoneyRequestViewProps) { + allReports, + report, + expensePolicy, + shouldShowAnimatedBackground, + readonly = false, + updatedTransaction, + isFromReviewDuplicates = false, + mergeTransactionID, + }: MoneyRequestViewProps) { const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); @@ -145,6 +146,8 @@ function MoneyRequestView({ const isExpenseUnreported = isExpenseUnreportedTransactionUtils(updatedTransaction ?? transaction); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); + + console.log({activePolicyID}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, { canBeMissing: true, selector: activePolicySelector, @@ -266,7 +269,8 @@ function MoneyRequestView({ // Flags for showing categories and tags // transactionCategory can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const shouldShowCategory = (isPolicyExpenseChat && (categoryForDisplay || hasEnabledOptions(policyCategories ?? {}))) || isExpenseUnreported; + const areCategoriesEnabledAndHasCategory = categoryForDisplay || hasEnabledOptions(policyCategories ?? {}); + const shouldShowCategory = (isPolicyExpenseChat && areCategoriesEnabledAndHasCategory) || (isExpenseUnreported && (!activePolicy || areCategoriesEnabledAndHasCategory)); // transactionTag can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const shouldShowTag = shouldShowPolicySpecificFields && (transactionTag || hasEnabledTags(policyTagLists)); From 20d69168d0d1ce2f838afd9c6095aad115b39f10 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 25 Sep 2025 11:22:41 +0200 Subject: [PATCH 03/15] fix: show the category violation when category removed --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 -- src/libs/Violations/ViolationsUtils.ts | 5 +++-- src/libs/actions/IOU.ts | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index fe16b35b0feed..7c1e8bf89e104 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -146,8 +146,6 @@ function MoneyRequestView({ const isExpenseUnreported = isExpenseUnreportedTransactionUtils(updatedTransaction ?? transaction); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); - - console.log({activePolicyID}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, { canBeMissing: true, selector: activePolicySelector, diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index efd8931f9aee5..d2be0093f3a47 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -225,6 +225,7 @@ const ViolationsUtils = { policyCategories: PolicyCategories, hasDependentTags: boolean, isInvoiceTransaction: boolean, + isSelfDM?: boolean, ): OnyxUpdate { const isScanning = TransactionUtils.isScanning(updatedTransaction); const isScanRequest = TransactionUtils.isScanRequest(updatedTransaction); @@ -271,12 +272,12 @@ const ViolationsUtils = { } // Remove 'missingCategory' violation if category is valid according to policy - if (hasMissingCategoryViolation && isCategoryInPolicy) { + if (hasMissingCategoryViolation && (isCategoryInPolicy || isSelfDM)) { newTransactionViolations = reject(newTransactionViolations, {name: 'missingCategory'}); } // Add 'missingCategory' violation if category is required and not set - if (!hasMissingCategoryViolation && policyRequiresCategories && !categoryKey) { + if (!hasMissingCategoryViolation && policyRequiresCategories && !categoryKey && !isSelfDM) { newTransactionViolations.push({name: 'missingCategory', type: CONST.VIOLATION_TYPES.VIOLATION, showInReview: true}); } } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 92d5b0e56682a..ee08f75bf82b3 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4429,7 +4429,6 @@ function getUpdateMoneyRequestParams( if ( policy && isPaidGroupPolicy(policy) && - !isSelfDM(iouReport) && !isInvoice && updatedTransaction && (hasModifiedTag || @@ -4457,6 +4456,7 @@ function getUpdateMoneyRequestParams( policyCategories ?? {}, hasDependentTags(policy, policyTagList ?? {}), isInvoice, + isSelfDM(iouReport) ); optimisticData.push(violationsOnyxData); failureData.push({ From bdf65a82b2186e6e5cf7e1eaa0ecb5101bee0ee2 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 25 Sep 2025 16:07:20 +0200 Subject: [PATCH 04/15] fix: do not show violation on unreported expense --- src/libs/TransactionPreviewUtils.ts | 5 ----- src/libs/Violations/ViolationsUtils.ts | 2 +- src/pages/iou/request/step/IOURequestStepParticipants.tsx | 8 +++++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/libs/TransactionPreviewUtils.ts b/src/libs/TransactionPreviewUtils.ts index 6339848ed9b87..c7940d3c87e09 100644 --- a/src/libs/TransactionPreviewUtils.ts +++ b/src/libs/TransactionPreviewUtils.ts @@ -236,10 +236,6 @@ function getTransactionPreviewTextAndTranslationPaths({ if (isDistanceRequest(transaction)) { previewHeaderText = [{translationPath: 'common.distance'}]; - - if (RBRMessage === undefined && isUnreportedAndHasInvalidDistanceRateTransaction(transaction)) { - RBRMessage = {translationPath: 'violations.customUnitOutOfPolicy'}; - } } else if (isPerDiemRequest(transaction)) { previewHeaderText = [{translationPath: 'common.perDiem'}]; } else if (isTransactionScanning) { @@ -345,7 +341,6 @@ function createTransactionPreviewConditionals({ const shouldShowCategory = !!categoryForDisplay && isReportAPolicyExpenseChat; const hasAnyViolations = - isUnreportedAndHasInvalidDistanceRateTransaction(transaction) || // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing hasViolationsOfTypeNotice || hasWarningTypeViolation(transaction, violations, true) || diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index d2be0093f3a47..4561d01c68a29 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -444,7 +444,7 @@ const ViolationsUtils = { newTransactionViolations.push({name: CONST.VIOLATIONS.TAX_OUT_OF_POLICY, type: CONST.VIOLATION_TYPES.VIOLATION}); } - if (isPolicyTrackTaxEnabled && hasTaxOutOfPolicyViolation && isTaxInPolicy) { + if ((isPolicyTrackTaxEnabled && hasTaxOutOfPolicyViolation && isTaxInPolicy) || isSelfDM) { newTransactionViolations = reject(newTransactionViolations, {name: CONST.VIOLATIONS.TAX_OUT_OF_POLICY}); } return { diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index 60962df3058f6..a50bacd1565b2 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -255,8 +255,10 @@ function IOURequestStepParticipants({ const newReportID = selectedReportID.current; transactions.forEach((transaction) => { - setMoneyRequestTag(transaction.transactionID, ''); - setMoneyRequestCategory(transaction.transactionID, ''); + if (!isMovingTransactionFromTrackExpense) { + setMoneyRequestTag(transaction.transactionID, ''); + setMoneyRequestCategory(transaction.transactionID, ''); + } if (participants?.at(0)?.reportID !== newReportID) { setTransactionReport(transaction.transactionID, {reportID: newReportID}, true); } @@ -307,7 +309,7 @@ function IOURequestStepParticipants({ Navigation.navigate(route); } }); - }, [action, participants, iouType, initialTransaction, transactions, initialTransactionID, reportID, waitForKeyboardDismiss, backTo]); + }, [action, participants, iouType, initialTransaction, transactions, initialTransactionID, reportID, waitForKeyboardDismiss, isMovingTransactionFromTrackExpense, backTo]); const navigateBack = useCallback(() => { if (backTo) { From 8e850fd432f7ddfec55b8b3389795ffc0c2428bf Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 25 Sep 2025 16:23:12 +0200 Subject: [PATCH 05/15] fix: prettier --- .../ReportActionItem/MoneyRequestView.tsx | 19 +++++++++---------- src/libs/TransactionPreviewUtils.ts | 4 +--- src/libs/actions/IOU.ts | 2 +- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 7c1e8bf89e104..d11b8bbb50e25 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -83,7 +83,6 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {TransactionPendingFieldsKey} from '@src/types/onyx/Transaction'; import MoneyRequestReceiptView from './MoneyRequestReceiptView'; - type MoneyRequestViewProps = { /** All the data of the report collection */ allReports: OnyxCollection; @@ -111,15 +110,15 @@ type MoneyRequestViewProps = { }; function MoneyRequestView({ - allReports, - report, - expensePolicy, - shouldShowAnimatedBackground, - readonly = false, - updatedTransaction, - isFromReviewDuplicates = false, - mergeTransactionID, - }: MoneyRequestViewProps) { + allReports, + report, + expensePolicy, + shouldShowAnimatedBackground, + readonly = false, + updatedTransaction, + isFromReviewDuplicates = false, + mergeTransactionID, +}: MoneyRequestViewProps) { const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); diff --git a/src/libs/TransactionPreviewUtils.ts b/src/libs/TransactionPreviewUtils.ts index c7940d3c87e09..d8cbf1a68bbce 100644 --- a/src/libs/TransactionPreviewUtils.ts +++ b/src/libs/TransactionPreviewUtils.ts @@ -342,9 +342,7 @@ function createTransactionPreviewConditionals({ const hasAnyViolations = // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - hasViolationsOfTypeNotice || - hasWarningTypeViolation(transaction, violations, true) || - hasViolation(transaction, violations, true); + hasViolationsOfTypeNotice || hasWarningTypeViolation(transaction, violations, true) || hasViolation(transaction, violations, true); const hasErrorOrOnHold = hasFieldErrors || (!isFullySettled && !isFullyApproved && isTransactionOnHold); const hasReportViolationsOrActionErrors = (isReportOwner(iouReport) && hasReportViolations(iouReport?.reportID)) || hasActionWithErrorsForTransaction(iouReport?.reportID, transaction); const shouldShowRBR = hasAnyViolations || hasErrorOrOnHold || hasReportViolationsOrActionErrors || hasReceiptError(transaction); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index ee08f75bf82b3..2f9225e1ca98f 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4456,7 +4456,7 @@ function getUpdateMoneyRequestParams( policyCategories ?? {}, hasDependentTags(policy, policyTagList ?? {}), isInvoice, - isSelfDM(iouReport) + isSelfDM(iouReport), ); optimisticData.push(violationsOnyxData); failureData.push({ From 209eb2adf143bd88d4224e568ed8642ac8c53649 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 25 Sep 2025 16:35:01 +0200 Subject: [PATCH 06/15] fix: eslint --- src/libs/TransactionPreviewUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/TransactionPreviewUtils.ts b/src/libs/TransactionPreviewUtils.ts index d8cbf1a68bbce..2654457d949b6 100644 --- a/src/libs/TransactionPreviewUtils.ts +++ b/src/libs/TransactionPreviewUtils.ts @@ -43,7 +43,6 @@ import { isPending, isPerDiemRequest, isScanning, - isUnreportedAndHasInvalidDistanceRateTransaction, } from './TransactionUtils'; const emptyPersonalDetails: OnyxTypes.PersonalDetails = { From f6367d07b888837e8d0c4cd52f80e0628cf5baca Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 25 Sep 2025 18:00:00 +0200 Subject: [PATCH 07/15] fix: show reports name --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index d11b8bbb50e25..c5e70cfc24c59 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -842,7 +842,7 @@ function MoneyRequestView({ Date: Fri, 26 Sep 2025 15:32:41 +0200 Subject: [PATCH 08/15] fix: show create report button on moving expenses --- .../step/IOURequestEditReportCommon.tsx | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx index 79e8feb7aef11..e784f203209e9 100644 --- a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx +++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx @@ -1,12 +1,12 @@ -import {createPoliciesSelector} from '@selectors/Policy'; -import React, {useMemo} from 'react'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import { createPoliciesSelector } from '@selectors/Policy'; +import React, { useMemo } from 'react'; +import type { OnyxCollection, OnyxEntry } from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; -import {useOptionsList} from '@components/OptionListContextProvider'; +import { useOptionsList } from '@components/OptionListContextProvider'; import SelectionList from '@components/SelectionList'; import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem'; -import type {ListItem} from '@components/SelectionList/types'; +import type { ListItem } from '@components/SelectionList/types'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; @@ -14,15 +14,16 @@ import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useReportTransactions from '@hooks/useReportTransactions'; import Navigation from '@libs/Navigation/Navigation'; -import {getPersonalPolicy, isPolicyAdmin} from '@libs/PolicyUtils'; -import {getOutstandingReportsForUser, getPolicyName, isIOUReport, isOpenReport, isReportOwner, isSelfDM, sortOutstandingReportsBySelected} from '@libs/ReportUtils'; +import { getPersonalPolicy, isPolicyAdmin } from '@libs/PolicyUtils'; +import { getOutstandingReportsForUser, getPolicyName, isIOUReport, isOpenReport, isReportOwner, isSelfDM, sortOutstandingReportsBySelected } from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Route} from '@src/ROUTES'; -import type {Policy} from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type { Route } from '@src/ROUTES'; +import type { Policy } from '@src/types/onyx'; +import { isEmptyObject } from '@src/types/utils/EmptyObject'; import StepScreenWrapper from './StepScreenWrapper'; + type TransactionGroupListItem = ListItem & { /** reportID of the report */ value: string; @@ -153,7 +154,7 @@ function IOURequestEditReportCommon({ const headerMessage = useMemo(() => (searchValue && !reportOptions.length ? translate('common.noResultsFound') : ''), [searchValue, reportOptions, translate]); const createReportOption = useMemo(() => { - if (!createReport || !isUnreported) { + if (!createReport || isUnreported === false) { return undefined; } From 0c2698c034d0b6453d3910a2ae95ae19564e5bfe Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 26 Sep 2025 15:49:52 +0200 Subject: [PATCH 09/15] fix: prettier + tax amount name --- .../step/IOURequestEditReportCommon.tsx | 21 +++++++++---------- .../iou/request/step/IOURequestStepAmount.tsx | 15 ++++++++++--- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx index e784f203209e9..1fb5971d27dc6 100644 --- a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx +++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx @@ -1,12 +1,12 @@ -import { createPoliciesSelector } from '@selectors/Policy'; -import React, { useMemo } from 'react'; -import type { OnyxCollection, OnyxEntry } from 'react-native-onyx'; +import {createPoliciesSelector} from '@selectors/Policy'; +import React, {useMemo} from 'react'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; -import { useOptionsList } from '@components/OptionListContextProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import SelectionList from '@components/SelectionList'; import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem'; -import type { ListItem } from '@components/SelectionList/types'; +import type {ListItem} from '@components/SelectionList/types'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; @@ -14,16 +14,15 @@ import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useReportTransactions from '@hooks/useReportTransactions'; import Navigation from '@libs/Navigation/Navigation'; -import { getPersonalPolicy, isPolicyAdmin } from '@libs/PolicyUtils'; -import { getOutstandingReportsForUser, getPolicyName, isIOUReport, isOpenReport, isReportOwner, isSelfDM, sortOutstandingReportsBySelected } from '@libs/ReportUtils'; +import {getPersonalPolicy, isPolicyAdmin} from '@libs/PolicyUtils'; +import {getOutstandingReportsForUser, getPolicyName, isIOUReport, isOpenReport, isReportOwner, isSelfDM, sortOutstandingReportsBySelected} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type { Route } from '@src/ROUTES'; -import type { Policy } from '@src/types/onyx'; -import { isEmptyObject } from '@src/types/utils/EmptyObject'; +import type {Route} from '@src/ROUTES'; +import type {Policy} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import StepScreenWrapper from './StepScreenWrapper'; - type TransactionGroupListItem = ListItem & { /** reportID of the report */ value: string; diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index f2b8ad744581f..20aa18e47ffa3 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -1,5 +1,6 @@ import {useFocusEffect} from '@react-navigation/native'; import reportsSelector from '@selectors/Attributes'; +import {activePolicySelector} from '@selectors/Policy'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; @@ -20,7 +21,7 @@ import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils'; import {isPaidGroupPolicy} from '@libs/PolicyUtils'; import {getPolicyExpenseChat, getTransactionDetails, isPolicyExpenseChat} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; -import {calculateTaxAmount, getAmount, getCurrency, getDefaultTaxCode, getRequestType, getTaxValue} from '@libs/TransactionUtils'; +import {calculateTaxAmount, getAmount, getCurrency, getDefaultTaxCode, getRequestType, getTaxValue, isExpenseUnreported as isExpenseUnreportedTransactionUtils} from '@libs/TransactionUtils'; import MoneyRequestAmountForm from '@pages/iou/MoneyRequestAmountForm'; import { getMoneyRequestParticipantsFromReport, @@ -78,10 +79,18 @@ function IOURequestStepAmount({ const focusTimeoutRef = useRef(null); const isSaveButtonPressed = useRef(false); const iouRequestType = getRequestType(transaction, isBetaEnabled(CONST.BETAS.MANUAL_DISTANCE)); - const policyID = report?.policyID; + const isExpenseUnreported = isExpenseUnreportedTransactionUtils(transaction); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, { + canBeMissing: true, + selector: activePolicySelector, + }); + const policyID = isExpenseUnreported ? activePolicyID : report?.policyID; const isReportArchived = useReportIsArchived(report?.reportID); - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: true}); + const [reportPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, {canBeMissing: true}); + const policy = isExpenseUnreported ? activePolicy : reportPolicy; + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false}); const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {canBeMissing: true}); From ebc13722bc80ba2c3376cc59fe26529b9548af5a Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 26 Sep 2025 15:57:43 +0200 Subject: [PATCH 10/15] fix: show NO violations for unreported expense --- src/libs/Violations/ViolationsUtils.ts | 5 ++--- src/libs/actions/IOU.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 4561d01c68a29..dd3c8f5ea9797 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -225,7 +225,6 @@ const ViolationsUtils = { policyCategories: PolicyCategories, hasDependentTags: boolean, isInvoiceTransaction: boolean, - isSelfDM?: boolean, ): OnyxUpdate { const isScanning = TransactionUtils.isScanning(updatedTransaction); const isScanRequest = TransactionUtils.isScanRequest(updatedTransaction); @@ -272,12 +271,12 @@ const ViolationsUtils = { } // Remove 'missingCategory' violation if category is valid according to policy - if (hasMissingCategoryViolation && (isCategoryInPolicy || isSelfDM)) { + if (hasMissingCategoryViolation && isCategoryInPolicy) { newTransactionViolations = reject(newTransactionViolations, {name: 'missingCategory'}); } // Add 'missingCategory' violation if category is required and not set - if (!hasMissingCategoryViolation && policyRequiresCategories && !categoryKey && !isSelfDM) { + if (!hasMissingCategoryViolation && policyRequiresCategories && !categoryKey) { newTransactionViolations.push({name: 'missingCategory', type: CONST.VIOLATION_TYPES.VIOLATION, showInReview: true}); } } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 2f9225e1ca98f..92d5b0e56682a 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4429,6 +4429,7 @@ function getUpdateMoneyRequestParams( if ( policy && isPaidGroupPolicy(policy) && + !isSelfDM(iouReport) && !isInvoice && updatedTransaction && (hasModifiedTag || @@ -4456,7 +4457,6 @@ function getUpdateMoneyRequestParams( policyCategories ?? {}, hasDependentTags(policy, policyTagList ?? {}), isInvoice, - isSelfDM(iouReport), ); optimisticData.push(violationsOnyxData); failureData.push({ From ed4ae7249b14e295f61bfcb6f6ae5d9e74049d45 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 26 Sep 2025 16:17:14 +0200 Subject: [PATCH 11/15] fix: reset categories and tax when choosing another workspace --- .../request/step/IOURequestStepParticipants.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index a50bacd1565b2..23594d31a0b12 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -213,17 +213,25 @@ function IOURequestStepParticipants({ setMoneyRequestParticipants(transaction.transactionID, val); }); + const isPolicyExpenseChat = !!firstParticipant?.isPolicyExpenseChat; + const policy = isPolicyExpenseChat && firstParticipant?.policyID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${firstParticipant.policyID}`] : undefined; + if (!isMovingTransactionFromTrackExpense) { // If not moving the transaction from track expense, select the default rate automatically. // Otherwise, keep the original p2p rate and let the user manually change it to the one they want from the workspace. - const isPolicyExpenseChat = !!firstParticipant?.isPolicyExpenseChat; - const policy = isPolicyExpenseChat && firstParticipant?.policyID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${firstParticipant.policyID}`] : undefined; const rateID = DistanceRequestUtils.getCustomUnitRateID({reportID: firstParticipantReportID, isPolicyExpenseChat, policy, lastSelectedDistanceRates}); transactions.forEach((transaction) => { setCustomUnitRateID(transaction.transactionID, rateID); }); } + if (isMovingTransactionFromTrackExpense && isPolicyExpenseChat && policy?.id !== activePolicy?.id) { + transactions.forEach((transaction) => { + setMoneyRequestTag(transaction.transactionID, ''); + setMoneyRequestCategory(transaction.transactionID, ''); + }); + } + // When multiple valid participants are selected, the reportID is generated at the end of the confirmation step. // So we are resetting selectedReportID ref to the reportID coming from params. // For invoices, a valid participant must have a login. @@ -240,7 +248,7 @@ function IOURequestStepParticipants({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing selectedReportID.current = firstParticipantReportID || generateReportID(); }, - [iouType, transactions, isMovingTransactionFromTrackExpense, reportID, trackExpense, allPolicies, lastSelectedDistanceRates], + [iouType, transactions, activePolicy, allPolicies, isMovingTransactionFromTrackExpense, reportID, trackExpense, lastSelectedDistanceRates], ); const goToNextStep = useCallback(() => { From 427d395c31df876b5676aa09bd3be88e2a031e57 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 26 Sep 2025 16:26:14 +0200 Subject: [PATCH 12/15] fix: minor fix --- src/libs/Violations/ViolationsUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index dd3c8f5ea9797..efd8931f9aee5 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -443,7 +443,7 @@ const ViolationsUtils = { newTransactionViolations.push({name: CONST.VIOLATIONS.TAX_OUT_OF_POLICY, type: CONST.VIOLATION_TYPES.VIOLATION}); } - if ((isPolicyTrackTaxEnabled && hasTaxOutOfPolicyViolation && isTaxInPolicy) || isSelfDM) { + if (isPolicyTrackTaxEnabled && hasTaxOutOfPolicyViolation && isTaxInPolicy) { newTransactionViolations = reject(newTransactionViolations, {name: CONST.VIOLATIONS.TAX_OUT_OF_POLICY}); } return { From f973f55b48b04934bd04a9eeab491edb6a7c99c6 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 26 Sep 2025 16:26:57 +0200 Subject: [PATCH 13/15] fix: enable distance rate field --- src/libs/ReportUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0ad526cec2171..ffb09e8d697a7 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4324,8 +4324,10 @@ function canEditFieldOfMoneyRequest( } if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.DISTANCE_RATE) { + // Unreported transaction from OldDot can have the reportID as an empty string + const isUnreportedExpense = !transaction?.reportID || transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; // The distance rate can be modified only on the distance expense reports - return isExpenseReport(moneyRequestReport) && isDistanceRequest(transaction); + return (isUnreportedExpense || isExpenseReport(moneyRequestReport)) && isDistanceRequest(transaction); } if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.REPORT) { From 16754f89a6c34328043a46a6b7c1ca39d7e78d57 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 26 Sep 2025 16:28:45 +0200 Subject: [PATCH 14/15] fix: enable remibursable --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index c5e70cfc24c59..45a5f729d5d42 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -275,7 +275,7 @@ function MoneyRequestView({ const isCurrentTransactionReimbursableDifferentFromPolicyDefault = policy?.defaultReimbursable !== undefined && !!(updatedTransaction?.reimbursable ?? transactionReimbursable) !== policy.defaultReimbursable; const shouldShowReimbursable = - isPolicyExpenseChat && (!policy?.disabledFields?.reimbursable || isCurrentTransactionReimbursableDifferentFromPolicyDefault) && !isCardTransaction && !isInvoice; + shouldShowPolicySpecificFields && (!policy?.disabledFields?.reimbursable || isCurrentTransactionReimbursableDifferentFromPolicyDefault) && !isCardTransaction && !isInvoice; const canEditReimbursable = isEditable && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.REIMBURSABLE, undefined, isChatReportArchived); const shouldShowAttendees = useMemo(() => shouldShowAttendeesTransactionUtils(iouType, policy), [iouType, policy]); From 029f05512901def2df917f02ee4a25762bb781a3 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 29 Sep 2025 12:22:24 +0200 Subject: [PATCH 15/15] feat: add create report option for reported expenses --- src/pages/iou/request/step/IOURequestEditReportCommon.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx index 1b7eef67801e4..aa9bc39766583 100644 --- a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx +++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx @@ -153,7 +153,7 @@ function IOURequestEditReportCommon({ const headerMessage = useMemo(() => (searchValue && !reportOptions.length ? translate('common.noResultsFound') : ''), [searchValue, reportOptions, translate]); const createReportOption = useMemo(() => { - if (!createReport || isUnreported === false) { + if (!createReport) { return undefined; } @@ -165,7 +165,7 @@ function IOURequestEditReportCommon({ icon={Expensicons.DocumentPlus} /> ); - }, [createReport, isUnreported, translate, activePolicy?.name]); + }, [createReport, translate, activePolicy?.name]); // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useMemo(() => {