diff --git a/src/CONST.ts b/src/CONST.ts
index 9aef93f1a1b8..3cd522027cd3 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -666,6 +666,7 @@ const CONST = {
},
TRANSACTION: {
DEFAULT_MERCHANT: 'Request',
+ UNKNOWN_MERCHANT: 'Unknown Merchant',
TYPE: {
CUSTOM_UNIT: 'customUnit',
},
diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js
index e284244f694c..adf3fa0cdd80 100644
--- a/src/components/MenuItem.js
+++ b/src/components/MenuItem.js
@@ -54,6 +54,7 @@ const defaultProps = {
disabled: false,
isSelected: false,
subtitle: undefined,
+ subtitleTextStyle: {},
iconType: CONST.ICON_TYPE_ICON,
onPress: () => {},
onSecondaryInteraction: undefined,
@@ -278,7 +279,7 @@ const MenuItem = React.forwardRef((props, ref) => {
{/* Since subtitle can be of type number, we should allow 0 to be shown */}
{(props.subtitle || props.subtitle === 0) && (
- {props.subtitle}
+ {props.subtitle}
)}
{!_.isEmpty(props.floatRightAvatars) && (
diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js
index 61f82fe120a4..5c834a53a00e 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview.js
+++ b/src/components/ReportActionItem/MoneyRequestPreview.js
@@ -34,6 +34,7 @@ import PressableWithFeedback from '../Pressable/PressableWithoutFeedback';
import * as ReceiptUtils from '../../libs/ReceiptUtils';
import ReportActionItemImages from './ReportActionItemImages';
import transactionPropTypes from '../transactionPropTypes';
+import colors from '../../styles/colors';
const propTypes = {
/** The active IOUReport, used for Onyx subscription */
@@ -152,6 +153,7 @@ function MoneyRequestPreview(props) {
const description = requestComment;
const hasReceipt = TransactionUtils.hasReceipt(props.transaction);
const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(props.transaction);
+ const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(props.transaction);
const isDistanceRequest = TransactionUtils.isDistanceRequest(props.transaction);
const getSettledMessage = () => {
@@ -241,6 +243,12 @@ function MoneyRequestPreview(props) {
>
)}
+ {hasFieldErrors && (
+
+ )}
Navigation.navigate(ROUTES.getEditRequestRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.AMOUNT))}
+ brickRoadIndicator={hasErrors && transactionAmount === 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
+ subtitle={hasErrors && transactionAmount === 0 ? translate('common.error.enterAmount') : ''}
+ subtitleTextStyle={styles.textLabelError}
/>
@@ -140,6 +145,9 @@ function MoneyRequestView({report, parentReport, shouldShowHorizontalRule, trans
shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DATE))}
+ brickRoadIndicator={hasErrors && transactionDate === '' ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
+ subtitle={hasErrors && transactionDate === '' ? translate('common.error.enterDate') : ''}
+ subtitleTextStyle={styles.textLabelError}
/>
@@ -150,6 +158,9 @@ function MoneyRequestView({report, parentReport, shouldShowHorizontalRule, trans
shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.MERCHANT))}
+ brickRoadIndicator={hasErrors && transactionMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
+ subtitle={hasErrors && transactionMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT ? translate('common.error.enterMerchant') : ''}
+ subtitleTextStyle={styles.textLabelError}
/>
{shouldShowHorizontalRule && }
diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js
index 3c4153fcc5e0..e962d4ea757b 100644
--- a/src/components/ReportActionItem/ReportPreview.js
+++ b/src/components/ReportActionItem/ReportPreview.js
@@ -32,6 +32,7 @@ import * as ReceiptUtils from '../../libs/ReceiptUtils';
import * as ReportActionUtils from '../../libs/ReportActionsUtils';
import * as TransactionUtils from '../../libs/TransactionUtils';
import ReportActionItemImages from './ReportActionItemImages';
+import colors from '../../styles/colors';
const propTypes = {
/** All the data of the action */
@@ -115,11 +116,12 @@ function ReportPreview(props) {
const numberOfScanningReceipts = _.filter(transactionsWithReceipts, (transaction) => TransactionUtils.isReceiptBeingScanned(transaction)).length;
const hasReceipts = transactionsWithReceipts.length > 0;
const isScanning = hasReceipts && ReportUtils.areAllRequestsBeingSmartScanned(props.iouReportID, props.action);
+ const hasErrors = hasReceipts && ReportUtils.hasMissingSmartscanFields(props.iouReportID);
const lastThreeTransactionsWithReceipts = ReportUtils.getReportPreviewDisplayTransactions(props.action);
const hasOnlyOneReceiptRequest = numberOfRequests === 1 && hasReceipts;
const previewSubtitle = hasOnlyOneReceiptRequest
- ? transactionsWithReceipts[0].merchant
+ ? TransactionUtils.getMerchant(transactionsWithReceipts[0])
: props.translate('iou.requestCount', {
count: numberOfRequests,
scanningReceipts: numberOfScanningReceipts,
@@ -188,6 +190,12 @@ function ReportPreview(props) {
{getPreviewMessage()}
+ {hasErrors && (
+
+ )}
`Exceeds the maximum length of ${limit} characters`,
dateInvalid: 'Please select a valid date',
invalidCharacter: 'Invalid character',
+ enterMerchant: 'Enter a merchant name',
+ enterAmount: 'Enter an amount',
+ enterDate: 'Enter a date',
},
comma: 'comma',
semicolon: 'semicolon',
@@ -454,6 +457,7 @@ export default {
genericCreateFailureMessage: 'Unexpected error requesting money, please try again later',
genericDeleteFailureMessage: 'Unexpected error deleting the money request, please try again later',
genericEditFailureMessage: 'Unexpected error editing the money request, please try again later',
+ genericSmartscanFailureMessage: 'Transaction is missing fields',
},
},
notificationPreferencesPage: {
diff --git a/src/languages/es.js b/src/languages/es.js
index 45e69f82210a..2e7ae7dd09eb 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -106,6 +106,9 @@ export default {
characterLimit: ({limit}) => `Supera el límite de ${limit} caracteres`,
dateInvalid: 'Por favor, selecciona una fecha válida',
invalidCharacter: 'Carácter invalido',
+ enterMerchant: 'Introduce un comerciante',
+ enterAmount: 'Introduce un importe',
+ enterDate: 'Introduce una fecha',
},
comma: 'la coma',
semicolon: 'el punto y coma',
@@ -453,6 +456,7 @@ export default {
genericCreateFailureMessage: 'Error inesperado solicitando dinero, Por favor, inténtalo más tarde',
genericDeleteFailureMessage: 'Error inesperado eliminando la solicitud de dinero. Por favor, inténtalo más tarde',
genericEditFailureMessage: 'Error inesperado al guardar la solicitud de dinero. Por favor, inténtalo más tarde',
+ genericSmartscanFailureMessage: 'La transacción tiene campos vacíos',
},
},
notificationPreferencesPage: {
diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js
index 2b39c420511e..a9c319865bbb 100644
--- a/src/libs/OptionsListUtils.js
+++ b/src/libs/OptionsListUtils.js
@@ -17,6 +17,7 @@ import * as LocalePhoneNumber from './LocalePhoneNumber';
import * as UserUtils from './UserUtils';
import * as ReportActionUtils from './ReportActionsUtils';
import * as PersonalDetailsUtils from './PersonalDetailsUtils';
+import * as ErrorUtils from './ErrorUtils';
/**
* OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can
@@ -350,11 +351,20 @@ function getSearchText(report, reportName, personalDetailList, isChatRoomOrPolic
function getAllReportErrors(report, reportActions) {
const reportErrors = report.errors || {};
const reportErrorFields = report.errorFields || {};
- const reportActionErrors = _.reduce(
- reportActions,
- (prevReportActionErrors, action) => (!action || _.isEmpty(action.errors) ? prevReportActionErrors : _.extend(prevReportActionErrors, action.errors)),
- {},
- );
+ const reportActionErrors = {};
+ _.each(reportActions, (action) => {
+ if (action && !_.isEmpty(action.errors)) {
+ _.extend(reportActionErrors, action.errors);
+ } else if (ReportActionUtils.isReportPreviewAction(action)) {
+ const iouReportID = ReportActionUtils.getIOUReportIDFromReportActionPreview(action);
+
+ // Instead of adding all Smartscan errors, let's just add a generic error if there are any. This
+ // will be more performant and provide the same result in the UI
+ if (ReportUtils.hasMissingSmartscanFields(iouReportID)) {
+ _.extend(reportActionErrors, {smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')});
+ }
+ }
+ });
// All error objects related to the report. Each object in the sources contains error messages keyed by microtime
const errorSources = {
diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js
index 25569c63f2c8..ff9a5ee14519 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -1370,6 +1370,17 @@ function areAllRequestsBeingSmartScanned(iouReportID, reportPreviewAction) {
return _.all(transactionsWithReceipts, (transaction) => TransactionUtils.isReceiptBeingScanned(transaction));
}
+/**
+ * Check if any of the transactions in the report has required missing fields
+ *
+ * @param {Object|null} iouReportID
+ * @returns {Boolean}
+ */
+function hasMissingSmartscanFields(iouReportID) {
+ const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID);
+ return _.some(transactionsWithReceipts, (transaction) => TransactionUtils.hasMissingSmartscanFields(transaction));
+}
+
/**
* Given a parent IOU report action get report name for the LHN.
*
@@ -3593,4 +3604,5 @@ export {
areAllRequestsBeingSmartScanned,
getReportPreviewDisplayTransactions,
getTransactionsWithReceipts,
+ hasMissingSmartscanFields,
};
diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js
index 7fb07f216f5f..39ec2082d84a 100644
--- a/src/libs/TransactionUtils.js
+++ b/src/libs/TransactionUtils.js
@@ -85,6 +85,14 @@ function hasReceipt(transaction) {
return lodashGet(transaction, 'receipt.state', '') !== '';
}
+/**
+ * @param {Object} transaction
+ * @returns {Boolean}
+ */
+function areModifiedFieldsPopulated(transaction) {
+ return transaction.modifiedMerchant !== CONST.TRANSACTION.UNKNOWN_MERCHANT && transaction.modifiedAmount !== 0 && transaction.modifiedCreated !== '';
+}
+
/**
* Given the edit made to the money request, return an updated transaction object.
*
@@ -126,6 +134,7 @@ function getUpdatedTransaction(transaction, transactionChanges, isFromExpenseRep
if (shouldStopSmartscan && _.has(transaction, 'receipt') && !_.isEmpty(transaction.receipt) && lodashGet(transaction, 'receipt.state') !== CONST.IOU.RECEIPT_STATE.OPEN) {
updatedTransaction.receipt.state = CONST.IOU.RECEIPT_STATE.OPEN;
}
+
updatedTransaction.pendingFields = {
...(_.has(transactionChanges, 'comment') && {comment: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(_.has(transactionChanges, 'created') && {created: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
@@ -228,6 +237,20 @@ function getCreated(transaction) {
return '';
}
+function isReceiptBeingScanned(transaction) {
+ return _.contains([CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING], transaction.receipt.state);
+}
+
+/**
+ * Check if the transaction has a non-smartscanning receipt and is missing required fields
+ *
+ * @param {Object} transaction
+ * @returns {Boolean}
+ */
+function hasMissingSmartscanFields(transaction) {
+ return hasReceipt(transaction) && !isReceiptBeingScanned(transaction) && !areModifiedFieldsPopulated(transaction);
+}
+
/**
* Get the transactions related to a report preview with receipts
* Get the details linked to the IOU reportAction
@@ -244,10 +267,6 @@ function getAllReportTransactions(reportID) {
return _.filter(allTransactions, (transaction) => transaction.reportID === reportID);
}
-function isReceiptBeingScanned(transaction) {
- return transaction.receipt.state === CONST.IOU.RECEIPT_STATE.SCANREADY || transaction.receipt.state === CONST.IOU.RECEIPT_STATE.SCANNING;
-}
-
/**
* Verifies that the provided waypoints are valid
* @param {Object} waypoints
@@ -308,4 +327,5 @@ export {
isReceiptBeingScanned,
validateWaypoints,
isDistanceRequest,
+ hasMissingSmartscanFields,
};
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 57a8fab32153..c38da2ecc814 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -1114,6 +1114,12 @@ const styles = {
color: themeColors.textSupporting,
},
+ textLabelError: {
+ fontFamily: fontFamily.EXP_NEUE,
+ fontSize: variables.fontSizeLabel,
+ color: themeColors.textError,
+ },
+
textReceiptUpload: {
...headlineFont,
fontSize: variables.fontSizeXLarge,