diff --git a/src/CONST.ts b/src/CONST.ts
index 57903e64abcd..3c76c4f2c464 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 1fb611691fbb..9a3d91b35208 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) {
let 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);
// On a distance request the merchant of the transaction will be used for the description since that's where it's stored in the database
@@ -246,6 +248,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 44b0126d891d..4526251d7609 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',
@@ -457,6 +460,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 7cd532a06275..9d2ea535afe0 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',
@@ -456,6 +459,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 e4c55f852922..4aa1ce14cf5c 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 7f247e6a649f..d47c26555a62 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -1359,6 +1359,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.
*
@@ -3581,4 +3592,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 d4fc1073bc46..61fa3d0d0259 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,