Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/components/KYCWall/BaseKYCWall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ function KYCWall({
currentUserAccountID,
employeeLogin,
doesSubmitterPersonalDetailExist ?? false,
getCurrencyDecimals,
reportTransactions,
);
if (inviteResult?.policyExpenseChatReportID) {
Expand All @@ -191,7 +192,7 @@ function KYCWall({
Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID: adminPolicy.id}));
});
} else {
const moveResult = moveIOUReportToPolicy(iouReport, adminPolicy, reportPreviewAction, true, reportTransactions);
const moveResult = moveIOUReportToPolicy(iouReport, adminPolicy, reportPreviewAction, getCurrencyDecimals, true, reportTransactions);
savePreferredPaymentMethod(iouReport.policyID, adminPolicy.id, CONST.LAST_PAYMENT_METHOD.IOU, lastPaymentMethod?.[adminPolicy.id]);

if (moveResult?.policyExpenseChatReportID && !moveResult.useTemporaryOptimisticExpenseChatReportID) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';

import {useCurrencyListActions} from '@hooks/useCurrencyList';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useThemeStyles from '@hooks/useThemeStyles';

Expand Down Expand Up @@ -87,6 +88,7 @@ function ReportFieldView(reportField: EnrichedPolicyReportField, report: OnyxEnt
function MoneyRequestViewReportFields({report, policy, pendingAction}: MoneyRequestViewReportFieldsProps) {
const styles = useThemeStyles();
const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
const {getCurrencyDecimals} = useCurrencyListActions();

const sortedPolicyReportFields = useMemo<EnrichedPolicyReportField[]>((): EnrichedPolicyReportField[] => {
const {fieldValues, fieldsByName} = getReportFieldMaps(report, policy?.fieldList ?? {});
Expand All @@ -97,7 +99,7 @@ function MoneyRequestViewReportFields({report, policy, pendingAction}: MoneyRequ
.filter((reportField) => !shouldHideSingleReportField(reportField))
.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight)
.map((field): EnrichedPolicyReportField => {
const fieldValue = resolveReportFieldValue(field, report, policy, fieldValues, fieldsByName);
const fieldValue = resolveReportFieldValue(field, report, policy, fieldValues, fieldsByName, getCurrencyDecimals);
const isFieldDisabled = isReportFieldDisabledForUser(report, field, policy, currentUserAccountID);
const isDeletedFormulaField = field.type === CONST.REPORT_FIELD_TYPES.FORMULA && field.deletable;
const fieldKey = getReportFieldKey(field.fieldID);
Expand All @@ -114,7 +116,7 @@ function MoneyRequestViewReportFields({report, policy, pendingAction}: MoneyRequ
violationTranslation,
};
});
}, [policy, report, currentUserAccountID]);
}, [policy, report, currentUserAccountID, getCurrencyDecimals]);

const isGroupPolicyExpenseReport = isGroupPolicyExpenseReportUtils(report, policy?.type);
const isInvoiceReport = isInvoiceReportUtils(report);
Expand Down
4 changes: 2 additions & 2 deletions src/components/ReportActionItem/MoneyReportView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function MoneyReportView({
const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const {convertToDisplayString} = useCurrencyListActions();
const {convertToDisplayString, getCurrencyDecimals} = useCurrencyListActions();
const {isOffline} = useNetwork();
const isSettled = isSettledReportUtils(report?.reportID);
const isTotalUpdated = hasUpdatedTotal(report, policy) && !isTotalPending;
Expand Down Expand Up @@ -184,7 +184,7 @@ function MoneyReportView({
return null;
}

const fieldValue = resolveReportFieldValue(reportField, report, policy, fieldValues, fieldsByName);
const fieldValue = resolveReportFieldValue(reportField, report, policy, fieldValues, fieldsByName, getCurrencyDecimals);
const isFieldDisabled = isReportFieldDisabledForUser(report, reportField, policy, currentUserAccountID);
const fieldKey = getReportFieldKey(reportField.fieldID);

Expand Down
3 changes: 2 additions & 1 deletion src/hooks/useSearchBulkActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1260,10 +1260,11 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
accountID,
getLoginByAccountID(itemReport?.ownerAccountID, personalDetails),
doesPersonalDetailExistSelector(itemReport?.ownerAccountID)(personalDetails),
getCurrencyDecimals,
reportTransactions,
);
if (!invite?.policyExpenseChatReportID) {
moveIOUReportToPolicy(itemReport, adminPolicy, reportPreviewAction, false, reportTransactions);
moveIOUReportToPolicy(itemReport, adminPolicy, reportPreviewAction, getCurrencyDecimals, false, reportTransactions);
}
}
}
Expand Down
30 changes: 30 additions & 0 deletions src/libs/CurrencyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,35 @@ function convertToDisplayStringEnLocale(amountInCents: number, currency: string
});
}

/**
* Same as convertToDisplayStringWithoutCurrency but always formats with the `en` locale, with decimals
* injected. Used alongside convertToDisplayStringEnLocale for stored values (e.g. formula-computed
* report titles) that must not depend on the viewer's locale or this module's Onyx fallback.
*/
function convertToDisplayStringWithoutCurrencyEnLocale(
amountInCents: number,
currency: string | undefined,
getCurrencyDecimalsImpl: CurrencyListActionsContextType['getCurrencyDecimals'],
): string {
const sanitizedCurrency = sanitizeCurrencyCode(currency);
const decimals = getCurrencyDecimalsImpl(sanitizedCurrency);
const convertedAmount = convertToFrontendAmountAsInteger(amountInCents, decimals);
return formatToParts(CONST.LOCALES.EN, convertedAmount, {
style: 'currency',
currency: sanitizedCurrency,

// We are forcing the number of decimals because we override the default number of decimals in the backend for some currencies
// See: https://github.com/Expensify/PHP-Libs/pull/834
minimumFractionDigits: decimals,
// For currencies that have decimal places > 2, floor to 2 instead as we don't support more than 2 decimal places.
maximumFractionDigits: 2,
})
.filter((x) => x.type !== 'currency')
.filter((x) => x.type !== 'literal' || x.value.trim().length !== 0)
.map((x) => x.value)
.join('');
}

/** Same intended use as convertToDisplayString, but purposely omit currency symbol if not provided */
function convertToDisplayStringWithExplicitCurrency(amountInCents: number, currency: string | undefined, currencies?: CurrencyList): string {
if (!currency) {
Expand Down Expand Up @@ -285,6 +314,7 @@ export {
convertToDisplayStringEnLocale,
convertAmountToDisplayString,
convertToDisplayStringWithoutCurrency,
convertToDisplayStringWithoutCurrencyEnLocale,
convertToDisplayStringWithExplicitCurrency,
convertToShortDisplayString,
};
30 changes: 19 additions & 11 deletions src/libs/Formula.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type {CurrencyListActionsContextType} from '@hooks/useCurrencyList';

import CONST from '@src/CONST';
import type {CurrencyList, PersonalDetails, Policy, PolicyReportField, Report, Transaction} from '@src/types/onyx';
import type {PersonalDetails, Policy, PolicyReportField, Report, Transaction} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';

import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';

import {endOfDay, endOfMonth, endOfWeek, getDay, lastDayOfMonth, set, startOfMonth, startOfWeek, subDays} from 'date-fns';

import {convertToDisplayString, convertToDisplayStringWithoutCurrency, isValidCurrencyCode} from './CurrencyUtils';
import {convertToDisplayStringEnLocale, convertToDisplayStringWithoutCurrencyEnLocale, isValidCurrencyCode} from './CurrencyUtils';
import {getCurrentUserEmail} from './CurrentUserStore';
import formatDate from './FormulaDatetime';
import getBase62ReportID from './getBase62ReportID';
Expand Down Expand Up @@ -36,7 +38,7 @@ type MinimalTransaction = Pick<Transaction, 'transactionID' | 'reportID' | 'crea
type FormulaContext = {
report: Report;
policy: OnyxEntry<Policy>;
currencyList?: CurrencyList;
getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'];
transaction?: Transaction;
submitterPersonalDetails?: PersonalDetails;
managerPersonalDetails?: PersonalDetails;
Expand Down Expand Up @@ -387,12 +389,12 @@ function computeReportPart(part: FormulaPart, context: FormulaContext): string {
case 'enddate':
return formatDate(getNewestTransactionDate(report.reportID, context) ?? new Date().toISOString(), format);
case 'total': {
const formattedAmount = formatAmount(report.total, report.currency, format, context.currencyList);
const formattedAmount = formatAmount(report.total, report.currency, format, context.getCurrencyDecimals);
// Return empty string when conversion needed (formatAmount returns null for unavailable conversions)
return formattedAmount ?? '';
}
case 'reimbursable': {
const formattedAmount = formatAmount(getMoneyRequestSpendBreakdown(report).reimbursableSpend, report.currency, format, context.currencyList);
const formattedAmount = formatAmount(getMoneyRequestSpendBreakdown(report).reimbursableSpend, report.currency, format, context.getCurrencyDecimals);
return formattedAmount ?? '';
}
case 'currency':
Expand Down Expand Up @@ -580,7 +582,12 @@ function getSubstring(value: string, args: string[]): string {
* Format an amount value
* @returns The formatted amount string, or null if currency conversion is needed (unavailable on frontend)
*/
function formatAmount(amount: number | undefined, currency: string | undefined, displayCurrency?: string, currencyList?: CurrencyList): string | null {
function formatAmount(
amount: number | undefined,
currency: string | undefined,
displayCurrency: string | undefined,
getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'],
): string | null {
if (amount === undefined) {
return '';
}
Expand All @@ -592,7 +599,7 @@ function formatAmount(amount: number | undefined, currency: string | undefined,
const trimmedDisplayCurrency = displayCurrency?.trim().toUpperCase();
if (trimmedDisplayCurrency) {
if (trimmedDisplayCurrency === 'NOSYMBOL') {
return convertToDisplayStringWithoutCurrency(absoluteAmount, trimmedCurrency, currencyList);
return convertToDisplayStringWithoutCurrencyEnLocale(absoluteAmount, trimmedCurrency, getCurrencyDecimals);
}

// If a currency conversion is needed (displayCurrency differs from the source),
Expand All @@ -607,18 +614,18 @@ function formatAmount(amount: number | undefined, currency: string | undefined,
return '';
}

return convertToDisplayString(absoluteAmount, trimmedDisplayCurrency, false, currencyList);
return convertToDisplayStringEnLocale(absoluteAmount, trimmedDisplayCurrency, getCurrencyDecimals);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why we are focing EN locale here in this function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@parasharrajat Cause formula always generates name in English format only.

}

if (trimmedCurrency) {
// Return empty string for an unrecognized source currency so the placeholder is preserved upstream.
if (!isValidCurrencyCode(trimmedCurrency)) {
return '';
}
return convertToDisplayString(absoluteAmount, trimmedCurrency, true, currencyList);
return convertToDisplayStringEnLocale(absoluteAmount, trimmedCurrency, getCurrencyDecimals);
}

return convertToDisplayStringWithoutCurrency(absoluteAmount, currency, currencyList);
return convertToDisplayStringWithoutCurrencyEnLocale(absoluteAmount, currency, getCurrencyDecimals);
} catch (error) {
Log.hmmm('[Formula] formatAmount failed', {error, amount, currency, displayCurrency});
return '';
Expand Down Expand Up @@ -1010,14 +1017,15 @@ function resolveReportFieldValue(
policy: OnyxEntry<Policy>,
fieldValues: Record<string, string>,
fieldsByName: Record<string, PolicyReportField>,
getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'],
): string {
const fieldValue = field.value ?? field.defaultValue ?? '';

if (!report || !hasFieldReferences(field.defaultValue)) {
return fieldValue;
}

return compute(field.defaultValue, {report, policy, fieldValues, fieldsByName});
return compute(field.defaultValue, {report, policy, fieldValues, fieldsByName, getCurrencyDecimals});
}

export {FORMULA_PART_TYPES, compute, computeWithMetadata, parse, hasCircularReferences, resolveReportFieldValue};
Expand Down
17 changes: 7 additions & 10 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import type {
BankAccountList,
Beta,
BillingGraceEndPeriod,
CurrencyList,
IntroSelected,
OnyxInputOrEntry,
OutstandingReportsByPolicyIDDerivedValue,
Expand Down Expand Up @@ -996,7 +995,6 @@ type BuildOptimisticExpenseReportParams = {
optimisticIOUReportID?: string;
reportTransactions?: Record<string, Transaction>;
createdTimestamp?: string;
currencyList?: CurrencyList;
getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'];
};

Expand Down Expand Up @@ -7005,9 +7003,9 @@ function computeOptimisticReportName(
policy: OnyxEntry<Policy>,
policyID: string | undefined,
reportTransactions: Record<string, Transaction>,
currencyList?: CurrencyList,
getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'],
): string | null {
const result = computeOptimisticReportNameWithMetadata(report, policy, policyID, reportTransactions, currencyList);
const result = computeOptimisticReportNameWithMetadata(report, policy, policyID, reportTransactions, getCurrencyDecimals);
if (!result || result.hasUnresolvedTokens) {
return null;
}
Expand All @@ -7023,7 +7021,7 @@ function computeOptimisticReportNameWithMetadata(
policy: OnyxEntry<Policy>,
policyID: string | undefined,
reportTransactions: Record<string, Transaction>,
currencyList?: CurrencyList,
getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'],
): {value: string; hasUnresolvedTokens: boolean} | null {
if (!isGroupPolicyPolicyUtils(policy)) {
return null;
Expand All @@ -7033,7 +7031,7 @@ function computeOptimisticReportNameWithMetadata(
const formulaContext: FormulaContext = {
report,
policy,
currencyList,
getCurrencyDecimals,
allTransactions: reportTransactions,
};

Expand Down Expand Up @@ -7105,7 +7103,6 @@ function buildOptimisticExpenseReport({
optimisticIOUReportID,
reportTransactions,
createdTimestamp,
currencyList,
getCurrencyDecimals,
}: BuildOptimisticExpenseReportParams): OptimisticExpenseReport {
// The amount for Expense reports are stored as negative value in the database
Expand Down Expand Up @@ -7158,7 +7155,7 @@ function buildOptimisticExpenseReport({
}

// Compute optimistic report name if applicable
const computedName = computeOptimisticReportName(expenseReport, policy, policyID, reportTransactions ?? {}, currencyList);
const computedName = computeOptimisticReportName(expenseReport, policy, policyID, reportTransactions ?? {}, getCurrencyDecimals);
if (computedName !== null) {
expenseReport.reportName = computedName;
}
Expand All @@ -7178,7 +7175,7 @@ function buildOptimisticEmptyReport(
policy: OnyxEntry<Policy>,
timeOfCreation: string,
betas: OnyxEntry<Beta[]>,
currencyList?: CurrencyList,
getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals'],
) {
const {stateNum, statusNum} = getExpenseReportStateAndStatus(policy, betas, true);
const optimisticEmptyReport: OptimisticNewReport = {
Expand All @@ -7205,7 +7202,7 @@ function buildOptimisticEmptyReport(
};

// Compute optimistic report name if applicable
const optimisticReportName = computeOptimisticReportName(optimisticEmptyReport as Report, policy, policy?.id, {}, currencyList);
const optimisticReportName = computeOptimisticReportName(optimisticEmptyReport as Report, policy, policy?.id, {}, getCurrencyDecimals);
if (optimisticReportName !== null) {
optimisticEmptyReport.reportName = optimisticReportName;
}
Expand Down
1 change: 1 addition & 0 deletions src/libs/actions/IOU/BulkEdit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ function updateMultipleMoneyRequests({
transaction,
isTransactionOnHold,
transactionPolicy,
getCurrencyDecimals,
optimisticReportAction?.actorAccountID,
transactionChanges,
additionalTransactionsForFormula,
Expand Down
2 changes: 1 addition & 1 deletion src/libs/actions/IOU/DeleteMoneyRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ function prepareToCleanUpMoneyRequest({
overlay[priorTxn.transactionID] = {...priorTxn, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE};
}
}
updatedIOUReport = maybeUpdateReportNameForFormulaTitle(updatedIOUReport, policy, overlay);
updatedIOUReport = maybeUpdateReportNameForFormulaTitle(updatedIOUReport, policy, getCurrencyDecimals, overlay);
}
}

Expand Down
Loading
Loading