diff --git a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md index 73b6c9106e4e..fb84e3484598 100644 --- a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md +++ b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md @@ -65,7 +65,7 @@ Once Auto-Reconciliation is enabled, there are a few things that happen. Let’s ### How This Works 1. On the day of your first card settlement, we'll create the Expensify Card Liability account in your QuickBooks Online general ledger. If you've opted for Daily Settlement, we'll also create an Expensify Clearing Account. 2. During your QuickBooks Online auto-sync on that same day, if there are unsettled transactions, we'll generate a journal entry totaling all posted transactions since the last settlement. This entry will credit the selected bank account and debit the new Expensify Clearing Account (for Daily Settlement) or the Expensify Liability Account (for Monthly Settlement). -3. Once the transactions are posted and the expense report is approved in Expensify, the report will be exported to QuickBooks Online with each line as individual credit card expenses. For Daily Settlement, an additional journal entry will credit the Expensify Clearing Account and debit the Expensify Card Liability Account. For Monthly Settlement, the journal entry will credit the Liability account directly and debit the appropriate expense categories. +3. Once the transactions are posted and the expense report is approved in Expensify, the report will be exported to QuickBooks Online with each line as individual card expenses. For Daily Settlement, an additional journal entry will credit the Expensify Clearing Account and debit the Expensify Card Liability Account. For Monthly Settlement, the journal entry will credit the Liability account directly and debit the appropriate expense categories. ### Example - We have card transactions for the day totaling $100, so we create the following journal entry upon sync: diff --git a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md index 2dec9ae752b8..c5e8da3fae6a 100644 --- a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md +++ b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md @@ -80,7 +80,7 @@ For an efficiency-focused company, we recommend setting up [Scheduled Submit](ht 4. You’ll notice *Scheduled Submit* is located directly under *Report Basics* 5. Choose *Daily* -Between Expensify's SmartScan technology, direct corporate card feed import, automatic categorization, and [DoubleCheck](https://community.expensify.com/discussion/5738/deep-dive-how-does-concierge-receipt-audit-work) features, your employees shouldn't need to do anything more than swipe their Expensify Card or scan their receipt. +Between Expensify's SmartScan technology, direct corporate card feed import, automatic categorization, and [DoubleCheck](https://community.expensify.com/discussion/5738/deep-dive-how-does-concierge-receipt-audit-work) features, your employees shouldn't need to do anything more than swipe their Expensify Visa® Commercial Card or scan their receipt. Scheduled Submit will ensure all expenses are submitted automatically. Any expenses that do not fall within the rules you’ve set up for your policy will be escalated to you for manual review. @@ -155,7 +155,7 @@ The Expensify Card has many benefits for your company. Two in particular are wor ### If you don't have a corporate card, use the Expensify Card Expensify provides a corporate card with the following features: -- Up to 2% cash back (within the US) +- Up to 2% cash back (Applies to USD purchases only) - [SmartLimits](https://community.expensify.com/discussion/4851/deep-dive-what-are-unapproved-expense-limits#latest) - Unlimited Virtual Cards - single-purpose cards with a fixed or monthly limit for specific company purchases - A stable, unbreakable connection (third-party bank feeds can run into connectivity issues) diff --git a/src/CONST.ts b/src/CONST.ts index e3cce4b613af..0b10e5767328 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3069,7 +3069,8 @@ const CONST = { }, /** - * Constants for maxToRenderPerBatch parameter that is used for FlatList or SectionList. This controls the amount of items rendered per batch, which is the next chunk of items rendered on every scroll. + * Constants for maxToRenderPerBatch parameter that is used for FlatList or SectionList. This controls the amount of items rendered per batch, which is the next chunk of items + * rendered on every scroll. */ MAX_TO_RENDER_PER_BATCH: { DEFAULT: 5, @@ -3081,6 +3082,11 @@ const CONST = { RBR: 'RBR', }, + /** + * Constants for types of violations. + * Defined here because they need to be referenced by the type system to generate the + * ViolationNames type. + */ VIOLATIONS: { ALL_TAG_LEVELS_REQUIRED: 'allTagLevelsRequired', AUTO_REPORTED_REJECTED_EXPENSE: 'autoReportedRejectedExpense', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 98e3856f4544..40a43d8195de 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -461,6 +461,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; + [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations; [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index 6bb4973d0c64..4c8b0e1102b9 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -8,10 +8,12 @@ import _ from 'underscore'; import participantPropTypes from '@components/participantPropTypes'; import transactionPropTypes from '@components/transactionPropTypes'; import withCurrentReportID, {withCurrentReportIDDefaultProps, withCurrentReportIDPropTypes} from '@components/withCurrentReportID'; +import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import {transactionViolationsPropType} from '@libs/Violations/propTypes'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import reportPropTypes from '@pages/reportPropTypes'; import stylePropTypes from '@styles/stylePropTypes'; @@ -63,8 +65,13 @@ const propTypes = { /** The transaction from the parent report action */ transactions: PropTypes.objectOf(transactionPropTypes), + /** List of draft comments */ draftComments: PropTypes.objectOf(PropTypes.string), + + /** The list of transaction violations */ + transactionViolations: transactionViolationsPropType, + ...withCurrentReportIDPropTypes, }; @@ -78,6 +85,7 @@ const defaultProps = { personalDetails: {}, transactions: {}, draftComments: {}, + transactionViolations: {}, ...withCurrentReportIDDefaultProps, }; @@ -98,8 +106,10 @@ function LHNOptionsList({ transactions, draftComments, currentReportID, + transactionViolations, }) { const styles = useThemeStyles(); + const {canUseViolations} = usePermissions(); /** * Function which renders a row in the list * @@ -137,10 +147,26 @@ function LHNOptionsList({ onSelectRow={onSelectRow} preferredLocale={preferredLocale} comment={itemComment} + transactionViolations={transactionViolations} + canUseViolations={canUseViolations} /> ); }, - [currentReportID, draftComments, onSelectRow, optionMode, personalDetails, policy, preferredLocale, reportActions, reports, shouldDisableFocusOptions, transactions], + [ + currentReportID, + draftComments, + onSelectRow, + optionMode, + personalDetails, + policy, + preferredLocale, + reportActions, + reports, + shouldDisableFocusOptions, + transactions, + transactionViolations, + canUseViolations, + ], ); return ( @@ -189,5 +215,8 @@ export default compose( draftComments: { key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, }, + transactionViolations: { + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + }, }), )(LHNOptionsList); diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js index e11bfc3cab98..8bdf065a94fd 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.js @@ -5,8 +5,10 @@ import _ from 'underscore'; import participantPropTypes from '@components/participantPropTypes'; import transactionPropTypes from '@components/transactionPropTypes'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import {transactionViolationsPropType} from '@libs/Violations/propTypes'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; @@ -42,6 +44,9 @@ const propTypes = { /** The transaction from the parent report action */ transaction: transactionPropTypes, + /** Any violations associated with the transaction */ + transactionViolations: transactionViolationsPropType, + ...basePropTypes, }; @@ -73,6 +78,8 @@ function OptionRowLHNData({ receiptTransactions, parentReportAction, transaction, + transactionViolations, + canUseViolations, ...propsToForward }) { const reportID = propsToForward.reportID; @@ -85,9 +92,19 @@ function OptionRowLHNData({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [fullReport.reportID, receiptTransactions, reportActions]); + const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(fullReport, transactionViolations, parentReportAction); + const optionItem = useMemo(() => { // Note: ideally we'd have this as a dependent selector in onyx! - const item = SidebarUtils.getOptionData(fullReport, reportActions, personalDetails, preferredLocale, policy, parentReportAction); + const item = SidebarUtils.getOptionData({ + report: fullReport, + reportActions, + personalDetails, + preferredLocale, + policy, + parentReportAction, + hasViolations, + }); if (deepEqual(item, optionItemRef.current)) { return optionItemRef.current; } @@ -96,7 +113,7 @@ function OptionRowLHNData({ // Listen parentReportAction to update title of thread report when parentReportAction changed // Listen to transaction to update title of transaction report when transaction changed // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction, transaction]); + }, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction, transaction, transactionViolations, canUseViolations]); useEffect(() => { if (!optionItem || optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index c7d038888c39..7a0a8286f901 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -9,7 +9,7 @@ import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; import CONST from '@src/CONST'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -import {isNotEmptyObject} from '@src/types/utils/EmptyObject'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import MessagesRow from './MessagesRow'; /** @@ -82,10 +82,10 @@ function OfflineWithFeedback({ const StyleUtils = useStyleUtils(); const {isOffline} = useNetwork(); - const hasErrors = isNotEmptyObject(errors ?? {}); + const hasErrors = !isEmptyObject(errors ?? {}); // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. const errorMessages = omitBy(errors, (e) => e === null); - const hasErrorMessages = isNotEmptyObject(errorMessages); + const hasErrorMessages = !isEmptyObject(errorMessages); const isOfflinePendingAction = !!isOffline && !!pendingAction; const isUpdateOrDeleteError = hasErrors && (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); const isAddError = hasErrors && pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 8483b7a481f2..204c9b5e31d4 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -16,6 +16,7 @@ import Text from '@components/Text'; import transactionPropTypes from '@components/transactionPropTypes'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -27,6 +28,7 @@ import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportActionUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import {transactionViolationsPropType} from '@libs/Violations/propTypes'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import reportPropTypes from '@pages/reportPropTypes'; import * as IOU from '@userActions/IOU'; @@ -108,6 +110,9 @@ const propTypes = { /** All the transactions, used to update ReportPreview label and status */ transactions: PropTypes.objectOf(transactionPropTypes), + /** All of the transaction violations */ + transactionViolations: transactionViolationsPropType, + ...withLocalizePropTypes, }; @@ -121,6 +126,9 @@ const defaultProps = { accountID: null, }, isWhisper: false, + transactionViolations: { + violations: [], + }, policy: { isHarvestingEnabled: false, }, @@ -131,6 +139,7 @@ function ReportPreview(props) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); + const {canUseViolations} = usePermissions(); const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyDistanceRequests, hasNonReimbursableTransactions} = useMemo( () => ({ @@ -162,7 +171,7 @@ function ReportPreview(props) { const numberOfScanningReceipts = _.filter(transactionsWithReceipts, (transaction) => TransactionUtils.isReceiptBeingScanned(transaction)).length; const hasReceipts = transactionsWithReceipts.length > 0; const isScanning = hasReceipts && areAllRequestsBeingSmartScanned; - const hasErrors = hasReceipts && hasMissingSmartscanFields; + const hasErrors = (hasReceipts && hasMissingSmartscanFields) || (canUseViolations && ReportUtils.hasViolations(props.iouReportID, props.transactionViolations)); const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; @@ -365,5 +374,8 @@ export default compose( transactions: { key: ONYXKEYS.COLLECTION.TRANSACTION, }, + transactionViolations: { + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + }, }), )(ReportPreview); diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index 8ef837ed986d..cbd166d79d3a 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -34,7 +34,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; type PolicyRole = { /** The role of current user */ - role: string; + role: Task.PolicyValue | undefined; }; type TaskPreviewOnyxProps = { @@ -94,7 +94,7 @@ function TaskPreview({ ? taskReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && taskReport.statusNum === CONST.REPORT.STATUS_NUM.APPROVED : action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED; const taskTitle = Str.htmlEncode(TaskUtils.getTaskTitle(taskReportID, action?.childReportName ?? '')); - const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport ?? {}) ?? action?.childManagerAccountID ?? ''; + const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? ''; const assigneeLogin = personalDetails[taskAssigneeAccountID]?.login ?? ''; const assigneeDisplayName = personalDetails[taskAssigneeAccountID]?.displayName ?? ''; const taskAssignee = assigneeDisplayName || LocalePhoneNumber.formatPhoneNumber(assigneeLogin); @@ -124,12 +124,12 @@ function TaskPreview({ style={[styles.mr2]} containerStyle={[styles.taskCheckbox]} isChecked={isTaskCompleted} - disabled={!Task.canModifyTask(taskReport ?? {}, currentUserPersonalDetails.accountID, rootParentReportpolicy?.role ?? '')} + disabled={!Task.canModifyTask(taskReport, currentUserPersonalDetails.accountID, rootParentReportpolicy?.role)} onPress={Session.checkIfActionIsAllowed(() => { if (isTaskCompleted) { - Task.reopenTask(taskReport ?? {}); + Task.reopenTask(taskReport); } else { - Task.completeTask(taskReport ?? {}); + Task.completeTask(taskReport); } })} accessibilityLabel={translate('task.task')} @@ -154,7 +154,7 @@ export default withCurrentUserPersonalDetails( }, rootParentReportpolicy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? '0'}`, - selector: (policy: Policy | null) => ({role: policy?.role ?? ''}), + selector: (policy: Policy | null) => ({role: policy?.role}), }, })(TaskPreview), ); diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx index 24acdf6c5f0b..d39f03c5aad4 100644 --- a/src/components/TaskHeaderActionButton.tsx +++ b/src/components/TaskHeaderActionButton.tsx @@ -32,7 +32,7 @@ function TaskHeaderActionButton({report, session, policy}: TaskHeaderActionButto