diff --git a/.github/scripts/createHelpRedirects.sh b/.github/scripts/createHelpRedirects.sh index 4ce3d0753f58..1425939ff3ec 100755 --- a/.github/scripts/createHelpRedirects.sh +++ b/.github/scripts/createHelpRedirects.sh @@ -27,32 +27,6 @@ function checkCloudflareResult { declare -a ITEMS_TO_ADD while read -r line; do - # Split each line of the file into a source and destination so we can sanity check - # and compare against the current list. - read -r -a LINE_PARTS < <(echo "$line" | tr ',' ' ') - SOURCE_URL=${LINE_PARTS[0]} - DEST_URL=${LINE_PARTS[1]} - - # Make sure the format of the line is as execpted. - if [[ "${#LINE_PARTS[@]}" -gt 2 ]]; then - error "Found a line with more than one comma: $line" - exit 1 - fi - - # Basic sanity checking to make sure that the source and destination are in expected - # subdomains. - if ! [[ $SOURCE_URL =~ ^https://(community|help)\.expensify\.com ]]; then - error "Found source URL that is not a communityDot or helpDot URL: $SOURCE_URL" - exit 1 - fi - - if ! [[ $DEST_URL =~ ^https://(help|use|integrations)\.expensify\.com|^https://www\.expensify\.org ]]; then - error "Found destination URL that is not a supported URL: $DEST_URL" - exit 1 - fi - - info "Source: $SOURCE_URL and destination: $DEST_URL appear to be formatted correctly." - ITEMS_TO_ADD+=("$line") # This line skips the first line in the csv because the first line is a header row. @@ -83,6 +57,9 @@ done | jq -n '. |= [inputs]') info "Adding redirects for $PUT_JSON" +# Dump $PUT_JSON into a file otherwise the curl request below will fail with too many arguments +echo "$PUT_JSON" > redirects.json + # We use PUT here instead of POST so that we replace the entire list in place. This has many benefits: # 1. We don't have to check if items are already in the list, allowing this script to run faster # 2. We can support deleting redirects this way by simply removing them from the list @@ -93,7 +70,7 @@ info "Adding redirects for $PUT_JSON" PUT_RESULT=$(curl -s --request PUT --url "https://api.cloudflare.com/client/v4/accounts/$ZONE_ID/rules/lists/$LIST_ID/items" \ --header 'Content-Type: application/json' \ --header "Authorization: Bearer $CLOUDFLARE_LIST_TOKEN" \ - --data "$PUT_JSON") + --data-binary @redirects.json) checkCloudflareResult "$PUT_RESULT" OPERATION_ID=$(echo "$PUT_RESULT" | jq -r .result.operation_id) diff --git a/.github/scripts/verifyRedirect.sh b/.github/scripts/verifyRedirect.sh index b8942cd5b23d..3d96ba17a799 100755 --- a/.github/scripts/verifyRedirect.sh +++ b/.github/scripts/verifyRedirect.sh @@ -3,7 +3,10 @@ # HelpDot - Verifies that redirects.csv does not have any duplicates # Duplicate sourceURLs break redirection on cloudflare pages +source scripts/shellUtils.sh + declare -r REDIRECTS_FILE="docs/redirects.csv" +declare -a ITEMS_TO_ADD declare -r RED='\033[0;31m' declare -r GREEN='\033[0;32m' @@ -22,5 +25,44 @@ if [[ DETECT_CYCLE_EXIT_CODE -eq 1 ]]; then exit 1 fi +while read -r line; do + # Split each line of the file into a source and destination so we can sanity check + # and compare against the current list. + read -r -a LINE_PARTS < <(echo "$line" | tr ',' ' ') + SOURCE_URL=${LINE_PARTS[0]} + DEST_URL=${LINE_PARTS[1]} + + # Make sure the format of the line is as expected. + if [[ "${#LINE_PARTS[@]}" -gt 2 ]]; then + error "Found a line with more than one comma: $line" + exit 1 + fi + + # Basic sanity checking to make sure that the source and destination are in expected + # subdomains. + if ! [[ $SOURCE_URL =~ ^https://(community|help)\.expensify\.com ]] || [[ $SOURCE_URL =~ \# ]]; then + error "Found source URL that is not a communityDot or helpDot URL, or contains a '#': $SOURCE_URL" + exit 1 +fi + + if ! [[ $DEST_URL =~ ^https://(help|use|integrations)\.expensify\.com|^https://www\.expensify\.org ]]; then + error "Found destination URL that is not a supported URL: $DEST_URL" + exit 1 + fi + + info "Source: $SOURCE_URL and destination: $DEST_URL appear to be formatted correctly." + + ITEMS_TO_ADD+=("$line") + +# This line skips the first line in the csv because the first line is a header row. +done <<< "$(tail +2 $REDIRECTS_FILE)" + +# Sanity check that we should actually be running this and we aren't about to delete +# every single redirect. +if [[ "${#ITEMS_TO_ADD[@]}" -lt 1 ]]; then + error "No items found to add, why are we running?" + exit 1 +fi + echo -e "${GREEN}The redirects.csv is valid!${NC}" exit 0 diff --git a/android/app/build.gradle b/android/app/build.gradle index 6b3e2f51b55d..e407b3324a88 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -108,8 +108,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009002300 - versionName "9.0.23-0" + versionCode 1009002400 + versionName "9.0.24-0" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/feed.svg b/assets/images/feed.svg new file mode 100644 index 000000000000..2fd03eeadd00 --- /dev/null +++ b/assets/images/feed.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/simple-illustrations/simple-illustration__rules.svg b/assets/images/simple-illustrations/simple-illustration__rules.svg new file mode 100644 index 000000000000..6432f26d9ac6 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__rules.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md index 18b8a9ec31f9..d3dcda91ffcc 100644 --- a/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md +++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md @@ -2,87 +2,101 @@ title: Configure Quickbooks Desktop description: Configure Quickbooks Desktop --- -# How to configure export settings for QuickBooks Desktop -To Configure Settings, go to **Settings** > **Policies** > **Group** > _[Policy Name]_ > **Connections** and click **Configure**. Click on the Export tab. +Our new QuickBooks Desktop integration allows you to automate the import and export process with Expensify. -## Preferred Exporter -This person is used in QuickBooks Desktop as the export user. They will also receive notifications for errors. +# Step 1: Configure export settings +The following steps will determine how data will be exported from Expensify to QuickBooks Desktop. -## Date -Choose either the report's submitted date, the report's exported date, or the date of the last expense on the report when exporting reports to QuickBooks Desktop. +1. In Expensify, hover over **Settings** and click **Workspaces**. +2. Select the Workspace you want to connect to QuickBooks Desktop. +3. Click the **Connections** tab. +4. Click **Export** under the QuickBooks Desktop connection. +5. Review each of the following export settings: +- **Preferred Exporter**: This person is used in QuickBooks Desktop as the export user. They will receive notifications for errors, as well as prompts to export reports via the Home page of their Expensify account. +- **Date**: You can choose either the report’s submitted date, the report’s exported date, or the date of the last expense on the report when exporting reports to QuickBooks Desktop. +- **Unique reference numbers**: Enable this to allow the use of a unique reference number for each transaction. Disable this to use the same Report ID for all expenses from a certain report. +- **Reimbursable expenses**: Reimbursable options include: + - **Vendor Bill (recommended)**: A single itemized vendor bill for each Expensify report. An A/P account is required to export to a vendor bill. + - **Check**: A single itemized check for each Expensify report. + - **Journal Entry**: A single itemized journal entry for each Expensify report. +- **Non-reimbursable expenses**: Non-reimbursable options include: + - **Vendor Bill**: Each Expensify report results in a single itemized vendor bill. The bill is associated with the “vendor,” which is the individual responsible for creating or submitting the report in Expensify. + - **Credit Card expenses**: Each expense appears as a separate credit card transaction with a post date that matches your credit card statement. If you centrally manage company cards through your domain, you can export expenses from each card to a specific QuickBooks account by clicking Edit Exports next to each user’s card. To display the merchant name in the payee field in QuickBooks Desktop, ensure that a matching Vendor exists in QuickBooks. Expensify searches for an exact match during export. If no match is found, the payee is mapped to a Credit Card Misc. Vendor created by Expensify. + - **Debit Card expenses**: Expenses are exported as individual itemized checks for each Expensify report. The check is written to the “vendor,” which is the person who created or submitted the report in Expensify. -## Use unique reference numbers -Enable this to allow use of a unique reference number for each transaction. Disable this to use the same Report ID for all expenses from a certain report. +# Step 2: Configure coding/import settings -## Reimbursable expenses -* **Vendor Bill (recommended):** A single itemized vendor bill for each Expensify report. An A/P account is required to export to a vendor bill. -* **Check:** A single itemized check for each Expensify report. -* **Journal Entry:** A single itemized journal entry for each Expensify report. - * When exporting as journal entries to an Accounts Payable, this requires a vendor record, not an employee. The vendor record must have the email address of the report creator/submitter. - * If the report creator/submitter also has an employee record, you need to remove the email, because Expensify will try to export to the employee record first for journal entries. +The following steps help you determine how data will be imported from QuickBooks Online to Expensify: -**Note on negative expenses:** In general, you can export negative expenses successfully to QuickBooks Desktop regardless of which export option you choose. The one thing to keep in mind is that if you have Check selected as your export option, the total of the report can not be negative. +1. Click Import under the QuickBooks Online connection. +2. Review each of the following import settings: +- **Chart of Accounts**: The Chart of Accounts is automatically imported from QuickBooks Desktop as categories. This cannot be amended. +- **Classes**: Choose whether to import classes, which will be shown in Expensify as tags for expense-level coding. +- **Customers/Projects**: Choose whether to import customers/projects, which will be shown in Expensify as tags for expense-level coding. +- **Locations**: Choose whether to import locations, which will be shown in Expensify as tags for expense-level coding. -**Note on exporting to Employee Records:** If you want to export reports to your users' Employee Records instead of their Vendor Records, you will need to select Check or Journal Entry for your reimbursable export option. There isn't a way to export as a Vendor Bill to an Employee Record. If you are setting up Expensify users as employees, you will need to activate QuickBooks Desktop Payroll to view the Employee Profile tab where submitter's email addresses need to be entered. +# Step 3: Configure advanced settings +The following steps help you determine the advanced settings for your connection, like auto-sync and employee invitation settings. -## Non-reimbursable expenses -**Credit Card Expenses:** -* Each expense will appear as a separate credit card transaction. -* The posting date will match your credit card statement. -* To display the merchant name in the payee field in QuickBooks Desktop, ensure that a matching Vendor exists in QuickBooks. Expensify searches for an exact match during export. If no match is found, the payee is mapped to a **Credit Card Misc.** Vendor created by Expensify. -* If you're centrally managing company cards through Domain Control, you can export expenses from each card to a specific QuickBooks account (detailed instructions available). - -**Debit Card Expenses:** -* Expenses export as individual itemized checks for each Expensify report. -* The check is written to the "vendor," which is the person who created or submitted the report in Expensify. +1. Click **Advanced** under the QuickBooks Desktop connection. +2. **Enable or disable Auto-Sync**: If enabled, QuickBooks Desktop automatically communicates changes with Expensify to ensure that the data shared between the two systems is up to date. New report approvals/reimbursements will be synced during the next auto-sync period. -**Vendor Bill:** -* Each Expensify report results in a single itemized vendor bill. -* The bill is associated with the "vendor," which is the individual responsible for creating or submitting the report in Expensify. +# FAQ -# How to configure coding for QuickBooks Desktop -To Configure Settings, go to **Settings** > **Policies** > **Group** > _[Policy Name]_ > **Connections** and click **Configure**. Click on the Coding tab. +## **How do I manually sync my QuickBooks Desktop if I have Auto-Sync disabled?** -## Categories -Expensify's integration with QuickBooks brings in your Chart of Accounts as Categories in Expensify automatically. Here's how to manage them: -1. After connecting, go to **Settings** > **Policies** > **Group** > _[Policy Name]_ > **Categories** to view the accounts imported from QuickBooks Desktop. -2. You can use the enable/disable button to choose which Categories your employees can access. Additionally, you can set specific rules for each Category via the blue settings cog. -3. Expensify offers Auto-Categorization to automatically assign expenses to the appropriate expense categories. -4. If needed, you can edit the names of the imported Categories to simplify expense coding for your employees. Keep in mind that if you make changes to these accounts in QuickBooks Desktop, the category names in Expensify will update to match them during the next sync. -5. _**Important:**_ Each expense must have a category selected to export to QuickBooks Desktop. The selected category must be one imported from QuickBooks Desktop; you cannot manually create categories within Expensify policy settings. +To manually sync your connection: -## Classes -Classes can be imported from QuickBooks as either tags (line-item level) or report fields (header level). +1. In Expensify, hover over **Settings** and select **Workspaces**. +2. Click the Workspace name that is connected to QuickBooks Desktop. +3. Click the **Connections** tab on the left. +4. Click **Sync Now** under QuickBooks Desktop. -## Customers/Projects -You can bring in Customers/Projects from QuickBooks into Expensify in two ways: as tags (at the line-item level) or as report fields (at the header level). If you're utilizing Billable Expenses in Expensify, here's what you need to know: -* Customers/Projects must be enabled if you're using Billable Expenses. -* Expenses marked as "Billable" need to be tagged with a Customer/Project to successfully export them to QuickBooks. +{% include info.html %} +For manual syncing, we recommend completing this process at least once a week and/or after making changes in QuickBooks Desktop that could impact how reports export from Expensify. Changes may include adjustments to your chart of accounts, vendors, employees, customers/jobs, or items. Remember: Both the Web Connector and QuickBooks Desktop need to be running for syncing or exporting to work. +{% include end-info.html %} -## Items -Items can be imported from QuickBooks as categories alongside your expense accounts. +## **Can I sync Expensify and QuickBooks Desktop (QBD) and use the platforms at the same time?** -{% include faq-begin.md %} -## How do I sync my connection? -1. Ensure that both the Expensify Sync Manager and QuickBooks Desktop are running. -2. On the Expensify website, navigate to **Settings** > **Policies** > **Group** > _[Policy Name]_ > **Connections** > **QuickBooks Desktop**, and click **Sync now**. -3. Wait for the syncing process to finish. Typically, this takes about 2-5 minutes, but it might take longer, depending on when you last synced and the size of your QuickBooks company file. The page will refresh automatically once syncing is complete. +When syncing Expensify to QuickBooks Desktop, we recommend waiting until the sync finishes to access either Expensify and/or QuickBooks Desktop, as performance may vary during this process. You cannot open an instance of QuickBooks Desktop while a program is syncing - this may cause QuickBooks Desktop to behave unexpectedly. -We recommend syncing at least once a week or whenever you make changes in QuickBooks Desktop that could impact how your reports export from Expensify. Changes could include adjustments to your Chart of Accounts, Vendors, Employees, Customers/Jobs, or Items. Remember, both the Sync Manager and QuickBooks Desktop need to be running for syncing or exporting to work. +## **What are the different types of accounts that can be imported from Quickbooks Desktop?** -## How do I export reports? -The Sync Manager and QuickBooks Desktop both need to be running in order to sync or export. -* **Exporting an Individual Report:** You can export reports to QuickBooks Desktop one at a time from within an individual report on the Expensify website by clicking the "Export to" button. -* **Exporting Reports in Bulk:** To export multiple reports at a time, select the reports that you'd like to export from the Reports page on the website and click the "Export to" button near the top of the page. +Here is the list of accounts from QuickBooks Desktop and how they are pulled into Expensify: -Once reports have been exported to QuickBooks Desktop successfully, you will see a green QuickBooks icon next to each report on the Reports page. You can check to see when a report was exported in the Comments section of the individual report. +| QBD account type | How it imports to Expensify | +| ------------- | ------------- | +| Accounts payable | Vendor bill or journal entry export options | +| Accounts receivable | Do not import | +| Accumulated adjustment | Do not import | +| Bank | Debit card or check export options | +| Credit card | Credit card export options | +| Equity | Do not import | +| Fixed assets | Categories | +| Income | Do not import | +| Long-term liabilities | Do not import | +| Other assets | Do not import | +| Other current assets | Categories or journal entry export options | +| Other current liabilities | Journal Entry export options if the report creator is set up as an Employee within QuickBooks | +| Other expense | All detail types except Exchange Gain or Loss import as categories; Exchange Gain or Loss does not import | +| Other income | Do not import | -## Can I export negative expenses? -Generally, you can export negative expenses to QuickBooks Desktop successfully, regardless of your option. However, please keep in mind that if you have *Check* selected as your export option, the report's total cannot be negative. +## **Why are exports showing as “Credit Card Misc.”?** + +When exporting as credit or debit card expenses, Expensify checks for an exact vendor match. If none are found, the payee will be mapped to a vendor that Expensify will automatically create and label as Credit Card Misc. or Debit Card Misc. + +If you centrally manage your company cards through domains, you can export expenses from each card to a specific account in QuickBooks: + +1. In Expensify, hover over Settings and click Domains. +2. Select the desired domain. +3. Click the **Company Cards** tab. +4. Click **Export**. + +## **How does multi-currency work with QuickBooks Desktop?** -## How does multi-currency work with QuickBooks Desktop? When using QuickBooks Desktop Multi-Currency, there are some limitations to consider based on your export options: -1. **Vendor Bills and Checks:** The currency of the vendor and the currency of the account must match, but they do not have to be in the home currency. -2. **Credit Card:** If an expense doesn't match an existing vendor in QuickBooks, it exports to the **Credit Card Misc.** vendor created by Expensify. When exporting a report in a currency other than your home currency, the transaction will be created under the vendor's currency with a 1:1 conversion. For example, a transaction in Expensify for $50 CAD will appear in QuickBooks as $50 USD. -3. **Journal Entries:** Multi-currency exports will fail because the account currency must match both the vendor currency and the home currency. + +- **Vendor Bills and Checks**: The currency of the vendor and the currency of the account must match, but they do not have to be in the home currency. +- **Credit Card**: If an expense doesn’t match an existing vendor in QuickBooks, it exports to the Credit Card Misc. vendor created by Expensify. When exporting a report in a currency other than your home currency, the transaction will be created under the vendor’s currency with a 1:1 conversion. For example, a transaction in Expensify for $50 CAD will appear in QuickBooks as $50 USD. +- **Journal Entries**: Multi-currency exports will fail because the account currency must match both the vendor currency and the home currency. diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md index 92e1e4dd841f..50e3e0971869 100644 --- a/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md +++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Connect-To-QuickBooks-Desktop.md @@ -4,59 +4,95 @@ description: Connect Expensify to QuickBooks Desktop order: 1 --- # Overview -To connect Expensify to QuickBooks Desktop, use Right Networks as the hosting platform if possible. Right Networks is a cloud-based service that was built specifically for this integration. If you need a Right Networks account, complete [this form](https://info.rightnetworks.com/partner-expensify) to start the process. +QuickBooks Desktop is an accounting package developed by Intuit. It is designed for small and medium-sized businesses to help them manage their financial and accounting tasks. You can connect Expensify to QuickBooks Desktop to make expense management seamless. -**A couple of notes before connecting QuickBooks Desktop to Expensify:** -- Make sure you're logged into QuickBooks Desktop as an admin -- Check that the company file you want to connect Expensify to is the only one open +# Connect to QuickBooks Desktop +{% include info.html %} +To connect QuickBooks Desktop to Expensify, you must log into QuickBooks Desktop as an Admin, and the company file that you want to connect to Expensify must be the only one that is open. +{% include end-info.html %} -# Connect to QuickBooks Desktop +1. In Expensify, hover over **Settings** and click on **Workspaces**. +2. Select the workspace you want to connect to QuickBooks Desktop. +3. Click the **Connections** tab. +4. Click **Connect to QuickBooks Desktop**. +5. Click Copy to copy the link, then paste the link into the computer where QuickBooks Desktop is running. + +![QuickBooks Desktop Setup pop-up link, containing the URL to paste](https://help.expensify.com/assets/images/QBO_desktop_01.png){:width="100%"} + +6. Select the version of QuickBooks Desktop that you currently have. + +![The Web Connnector Pop-up to allow you to select the type of QuickBooks Desktop you have](https://help.expensify.com/assets/images/QBO_desktop_02.png){:width="100%"} + +7. Download the Web Connector and go through the guided installation process. +8. Open the Web Connector. +9. Click on **Add an Application**. + +![The Web Connnector Pop-up where you will need to click on Add an Application](https://help.expensify.com/assets/images/QBO_desktop_03.png){:width="100%"} + +{% include info.html %} +For this step, it is key to ensure that the correct company file is open in QuickBooks Desktop and that it is the only one open. +{% include end-info.html %} + +10. In QuickBooks Desktop, select **"Yes, always allow access, even when QuickBooks is not running"** and click **Continue**. + +![The QuickBooks Desktop pop-up, where you will need to select "Yes, always allow access, even when QuickBooks is not running"](https://help.expensify.com/assets/images/QBO_desktop_04.png){:width="100%"} + +11. Click **OK**, then click **Yes**. + +![The QuickBooks Desktop pop-up, where you will need to click "Ok" then select "Yes"](https://help.expensify.com/assets/images/QBO_desktop_05.png){:width="100%"} + +12. Click **Copy** to copy the password. + +![The Web Connector pop-up, where you will need to click "Copy"](https://help.expensify.com/assets/images/QBO_desktop_06.png){:width="100%"} + +13. Paste the password into the Password field of the Web Connector and press **Enter**. + +![The Web Connector pop-up, where you will need to paste the password into the password field](https://help.expensify.com/assets/images/QBO_desktop_08.png){:width="100%"} + +14. Click **Yes** to save the password. The new connection now appears in the Web Connector. + +![The Web Connector pop-up, where you will need to click "Yes"](https://help.expensify.com/assets/images/QBO_desktop_07.png){:width="100%"} + +# FAQ + +## What are the hardware and software requirements for the QuickBooks Desktop connector? + +- Hardware requirements: You will need to ensure that the host machine meets [Intuit's recommended specifications](https://quickbooks.intuit.com/learn-support/en-us/help-article/install-products/system-requirements-quickbooks-desktop-2022/L9664spDA_US_en_US) for running QuickBooks Desktop. +- Software requirements: Windows 10 or Windows 11 with the latest service pack(s) installed. Users have successfully run the connector on older versions of Windows; however, we do not officially support this due to Microsoft's withdrawal of support for these operating systems. The web connector will not run on Mac OS. + +## What versions of QuickBooks Desktop are supported? + +Expensify’s QuickBooks Desktop integration follows [Intuit’s service discontinuation policy](https://quickbooks.intuit.com/learn-support/en-us/help-article/feature-preferences/quickbooks-desktop-service-discontinuation-policy/L17cXxlie_US_en_US) for QuickBooks Desktop and fully supports the following versions, version tiers, and special editions: + +- The latest three versions of: + + - QuickBooks Desktop (US) + - QuickBooks Desktop (Canada) + +- Version tiers: + + - Accountant + - Pro + - Pro Plus + - Premier + - Premier Plus + - Enterprise + +- Special editions: + + - Contractor edition + - Manufacturing and Wholesale edition + - Accountant edition + - Professional Services edition + - Nonprofit edition + +## Can multiple QuickBooks Desktop Connectors be installed on the same machine? + +Yes. You must have one connector per company file, but you can install multiple QuickBooks Desktop Connectors to sync multiple company files to Expensify from the same computer. + +If syncing multiple companies, make sure you’re logged in to the correct QuickBooks company file when syncing between QuickBooks and Expensify. + +## Can I export negative expenses? -## Step 1: Set up submitters in QuickBooks Desktop -- Make sure all report submitters are set up as Vendors in QuickBooks Desktop and their Expensify email is in the "Main Email" field of their Vendor record. You can do this in the vendor section of QuickBooks. -- If you want to export reports to your users' employee records instead of vendor records, select Check or Journal Entry as your reimbursable export option. -- To set up Expensify users as employees, activate QuickBooks Desktop Payroll. This module is necessary to access the Employee Profile tab, where you can enter the submitter's email addresses. - -## Step 2: Enable/install the Expensify Sync Manager -1. Navigate to **Settings > Workspaces > Group > [Workspace Name] > Connections** -2. Click **Connect to QuickBooks Desktop** to initiate the connection - -**Option 1: Enable the Expensify Sync Manager in Right Networks (recommended)** -- For this option, **single-user mode** in QuickBooks Desktop is required. -- If you don't have an account with Right Networks, you can contact Right Networks [here](https://info.rightnetworks.com/partner-expensify) -- Once set up, you can enable the Expensify Sync Manager from the **My Account** section in Right Networks' portal - -**Option 2: Install the Expensify Sync Manager on Your Third-Party Remote Desktop.** -To download the Sync Manager to your desktop, you must contact your third-party remote desktop provider and request permission. They might have security restrictions, so it's best to communicate with them directly to avoid potential problems with the Sync Manager. Remember that the Sync Manager program file should be stored in the same location (i.e., the same drive) as your QuickBooks Desktop program. - -## Step 3: Complete the connection -1. Open QuickBooks and access the desired Company File using the QuickBooks Admin credentials (admin credentials are necessary for creating the connection) -2. Navigate to **Settings > Workspaces > Group > [Workspace Name] > Connections** -3. Copy the Token by selecting the copy icon -4. While QuickBooks is still running, launch the Expensify Sync Manager by pasting the Token into the Sync Manager -5. Click **Save** -6. Once the Sync Manager status displays **Connected**, return to Expensify and click **Continue** - -## Step 4: Allow access -1. Return to QuickBooks where you'll see an **Application Certificate** screen - - On the first page of the Certificate screen, click **Yes, always; allow access even if QuickBooks is not running** -3. Click **Continue** -4. On the second page of the Certificate screen, choose the Admin user from the dropdown menu -5. Click **Done** -7. Return to Expensify and wait for the sync to complete - -{% include faq-begin.md %} - -## After connecting, how do I sync QuickBooks and Expensify? -1. Confirm that both the Expensify Sync Manager and QuickBooks Desktop are running -2. On the Expensify website, navigate to **Settings > Workspaces > Group > [Workspace Name] > Connections**, and click **Sync now** -3. Wait for the sync to complete - -Typically, this takes about 2-5 minutes, but it might take longer, depending on when you last synced and the size of your QuickBooks company file. The page will refresh automatically once syncing is complete. - -We recommend syncing at least once a week or whenever you make changes in QuickBooks Desktop that could impact how your reports export from Expensify. Changes could include adjustments to your Chart of Accounts, Vendors, Employees, Customers/Jobs, or Items. - -Remember, both the Sync Manager and QuickBooks Desktop need to be running for syncing or exporting to work. - -{% include faq-end.md %} +Generally, yes. However, if you have Check selected as your export option, the report’s total cannot be negative. This also applies to non-reimbursable expenses exported as debit card transactions. Because QuickBooks Desktop does not have debit card functionality, the transactions export as a non-reimbursable check, which must have a positive total amount. diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md index 061b01b7a924..09afd2e4e7f2 100644 --- a/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md +++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md @@ -3,43 +3,89 @@ title: Quickbooks Desktop Troubleshooting description: Quickbooks Desktop Troubleshooting --- -# Sync and export errors -## Error: No Vendor Found For Email in QuickBooks -To address this issue, ensure that each submitter's email is saved as the **Main Email** in their Vendor record within QuickBooks Desktop. Here's how to resolve it: -1. Go to your Vendor section in QuickBooks. -2. Verify that the email mentioned in the error matches the **Main Email** field in the respective vendor's record. It's important to note that this comparison is case-sensitive, so ensure that capitalization matches as well. -3. If you prefer to export reports to your users' employee records instead of their vendor records, select either **Check** or **Journal Entry** as your reimbursable export option. If you are setting up Expensify users as employees, activate QuickBooks Desktop Payroll to access the Employee Profile tab where submitter email addresses need to be entered. -4. Once you've added the correct email to the vendor record, save this change, and then sync your policy before attempting to export the report again. - -## Error: Do Not Have Permission to Access Company Data File -To resolve this error, follow these steps: -1. Log into QuickBooks Desktop as an Admin in single-user mode. -2. Go to **Edit** > **Preferences** > **Integrated Applications** > **Company Preferences**. -3. Select the Expensify Sync Manager and click on **Properties**. -4. Ensure that **Allow this application to login automatically** is checked, and then click **OK**. Close all windows within QuickBooks. -5. If you still encounter the error after following the above steps, go to **Edit** > **Preferences** > **Integrated Applications** > **Company Preferences**, and remove the Expensify Sync Manager from the list. -6. Next, attempt to sync your policy again in Expensify. You'll be prompted to re-authorize the connection in QuickBooks. -7. Click **Yes, always; allow access even if QuickBooks is not running.** -8. From the dropdown, select the Admin user, and then click **Continue**. Note that selecting **Admin** here doesn't mean you always have to be logged in as an admin to use the connection; it's just required for setting up the connection. -9. Click **Done** on the pop-up window and return to Expensify, where your policy should complete the syncing process. - -## Error: The Wrong QuickBooks Company is Open. -This error suggests that the wrong company file is open in QuickBooks Desktop. To resolve this issue, follow these steps: -1. First, go through the general troubleshooting steps as outlined. -2. If you can confirm that the incorrect company file is open in QuickBooks, go to QuickBooks and select **File** > **Open or Restore Company** > _[Company Name]_ to open the correct company file. After doing this, try syncing your policy again. -3. If the correct company file is open, but you're still encountering the error, completely close QuickBooks Desktop, reopen the desired company file and then attempt to sync again. -4. If the error persists, log into QuickBooks as an admin in single-user mode. Then, go to **Edit** > **Preferences** > **Integrated Applications** > **Company Preferences** and remove the Expensify Sync Manager from the list. -5. Next, try syncing your policy again in Expensify. You'll be prompted to re-authorize the connection in QuickBooks, allowing you to sync successfully. -6. If the error continues even after trying the steps above, double-check that the token you see in the Sync Manager matches the token in your connection settings. - -## Error: The Expensify Sync Manager Could Not Be Reached. -To resolve this error, follow these steps: -*Note: You must be in single-user mode to sync.* - -1. Ensure that both the Sync Manager and QuickBooks Desktop are running. -2. Confirm that the Sync Manager is installed in the correct location. It should be in the same location as your QuickBooks application. If QuickBooks is on your local desktop, the Sync Manager should be there, too. If QuickBooks is on a remote server, install the Sync Manager there. -Verify that the Sync Manager's status is **Connected**. -3. If the Sync Manager status is already **Connected**, click **Edit** and then *Save* to refresh the connection. Afterwards, try syncing your policy again. -4. If the error persists, double-check that the token you see in the Sync Manager matches the token in your connection settings. - -{% include faq-end.md %} +# The Web Connector cannot be reached + +Generally, these errors indicate that there is a connection issue, where there’s a breakdown between Expensify and QuickBooks. + +## How to resolve + +1. Make sure that the Web Connector and QuickBooks Desktop are both running. +2. Make sure that the Web Connector is installed in the same location as your QuickBooks application. For example, if QuickBooks is installed on your local desktop, the Web Connector should be too. Or if QuickBooks is installed on a remote server, the Web Connector should be installed there as well. + +If the error persists: + +1. Close the Web Connector completely (you may want to use Task Manager to do this). +2. Right-click the Web Connector icon on your desktop and select **Run as administrator**. +3. Sync your Workspace again. + +If this doesn’t work, the final troubleshooting steps should be: + +1. Quit QuickBooks Desktop, then reopen it. +2. In Expensify, hover over **Settings** and select **Workspaces**. +3. Click the workspace name that is connected to QuickBooks Desktop. +4. Click the **Connections** tab on the left. +5. Click **QuickBooks Desktop**. +6. Click **Sync Now**. +7. If this still doesn’t resolve the issue, use the link to reinstall the Web Connector. + +# Connection and/or authentication issue + +Generally, these errors indicate that there is a credentials issue. + +## How to resolve + +1. Make sure QuickBooks Desktop is open with the correct company file. This must be the same company file that you have connected to Expensify. +2. Make sure the QuickBooks Web Connector is open and the connector is online. +3. Make sure that there are no dialogue boxes open in QuickBooks that are interfering with attempts to sync or export. To resolve this, close any open windows in QuickBooks Desktop so that you only see a gray screen, then try exporting or syncing again. +4. Check that you have the correct permissions. +5. Log in to QuickBooks Desktop as an Admin (in single-user mode). +6. Go to **Edit** > **Preferences** > **Integrated Applications** > **Company Preferences**. +7. Select the Web Connector and click **Properties**. +8. Make sure that the "Allow this application to login automatically" checkbox is selected and click **OK**. +9. Close all windows in QuickBooks. + +If these general troubleshooting steps don’t work, reach out to Concierge and have the following information ready to provide: + +1. What version of QuickBooks Desktop do you have (Enterprise 2016, Pro 2014, etc.)? +2. Is your QuickBooks program installed on your computer or a remote network/drive? +3. Is your QuickBooks company file installed on your computer or a remote network/drive? +4. Is your Web Connector installed on your computer or a remote network/drive? +5. If any of the above are on a remote option, is there a company that runs that remote environment? If so, who (ie: RightNetworks, SwissNet, Cloud9, etc.)? + +# Import issue or missing categories and/or tags + +Generally, if you are having issues importing data from QuickBooks to Expensify, this indicates that the integration needs to be updated or your version of QuickBooks may not support a specific configuration. + +## How to resolve + +1. Re-sync the connection between Expensify and QuickBooks Desktop. A fresh sync can often resolve any issues, especially if you have recently updated your chart of accounts or projects, customers, or jobs in QuickBooks Desktop. +2. Check your configuration in QuickBooks Desktop. Expensify will import the chart of accounts to be utilized either as categories or export account options, while projects, customers, and tags will be imported as tags. + +If these general troubleshooting steps don’t work, reach out to Concierge with context on what is specifically missing in Expensify, as well as screenshots from your QuickBooks Desktop setup. + +# Export or "can't find category/class/location/account" issue + +Generally, when an export error occurs, we’ll share the reason in the Report Comments section at the bottom of the report. This will give you an indication of how to resolve the error. + +## How to resolve + +1. Re-sync the connection between Expensify and QuickBooks Desktop. A fresh sync can often resolve any issues, especially if you have recently updated your chart of accounts or projects, customers, or jobs in QuickBooks Desktop. +2. Re-apply coding to expenses and re-export the report. If you’ve recently synced Expensify and QuickBooks or recently made changes to your Workspace category or tags settings, you may need to re-apply coding to expenses. +3. Make sure that your current version of QuickBooks Desktop supports the selected export option. Different versions of QuickBooks Desktop support different export options and the [version that you own](https://quickbooks.intuit.com/desktop/) may not be compatible with the export type. + +If these general troubleshooting steps don’t work, reach out to Concierge with the Report ID, some context on what you’re trying to do, and a screenshot of the Expensify error message. + +# “Oops!” error when syncing or exporting + +Generally, an “Oops!” error can often be temporary or a false error. Although you will see a message pop up, there may actually not be an actual issue. + +## How to resolve + +1. Check to see if the sync or export was successful. +2. If it wasn't, please attempt to sync or export the connection again. + +If the problem persists, download the QuickBooks Desktop log file via the Web Connector (click View Logs to download them) and reach out to Concierge for further assistance. + +{% include info.html %} +If you’re using a remote server (e.g. RightNetworks), you may need to contact that support team to request your logs. +{% include end-info.html %} diff --git a/docs/articles/expensify-classic/expensify-card/Expensify-Card-Perks.md b/docs/articles/expensify-classic/expensify-card/Expensify-Card-Perks.md new file mode 100644 index 000000000000..922bf9d07056 --- /dev/null +++ b/docs/articles/expensify-classic/expensify-card/Expensify-Card-Perks.md @@ -0,0 +1,172 @@ +--- +title: Expensify Card Perks +description: An overview of all the various perks the Expensify Card offers +--- +## Expensify-specific Perks + +### Cashback +Get 1% cash back with every swipe — no minimums necessary — and 2% back if you spend $250k+/month across cards. + +This applies to USD purchases only. + +### Discounts on Monthly Expensify Bill +Get the Expensify Visa® Commercial Card and use it for at least half of your organization's monthly expenses to save 50% on your monthly Expensify bill. + +## Partner-specific Perks + +### Amazon AWS +Whether you're a two-person startup or a venture-backed company, AWS Activate provides access to the resources you need to get started on AWS quickly, including free credits, technical support, and training. All Expensify Cardholders qualify when they add their Expensify Card for AWS billing! + +**How to redeem:** Apply [here](https://aws.amazon.com/activate/portfolio-signup) using OrgID: 0qyIA (Case Sensitive). + +For more details, refer to the [AWS Activate terms and conditions](https://aws.amazon.com/activate/terms/) and the [Activate FAQs](https://aws.amazon.com/activate/faq/). + +### Stripe +Stripe’s integrated payments platform helps you build and scale your business globally, whether you're creating a subscription service, an on-demand marketplace, or an e-commerce store. + +Stripe will waive the processing fees for the first $5,000 in payments for Expensify Cardholders. + +**How to redeem:** Sign up for Stripe using your Expensify Card. + +### Lamar Advertising +Lamar offers out-of-home advertising on billboards, digital displays, airport signage, transit, and highway logo signs. Expensify Cardholders receive a minimum 10% discount on their first campaign. + +**How to redeem:** Contact Expensify’s dedicated account manager, Lisa Kane (email: lkane@lamar.com), and mention that you’re an Expensify Cardholder. + +### Carta +Simplify equity management with Carta. Expensify Cardholders receive a 20% discount on the first year and waived implementation fees. + +**How to redeem:** Sign up using your Expensify Card. + +### Pilot +Pilot specializes in bookkeeping and tax preparation for startups and e-commerce providers. When you use Pilot, you're paired with a dedicated finance expert who handles the work for you and answers your questions. + +**Offer:** 20% off the first six months of Pilot Core. + +**How to redeem:** Sign up using your Expensify Card. + +### Spotlight Reporting +Spotlight Reporting is a cloud-based reporting and forecasting tool designed by accountants, for accountants. Expensify Cardholders receive a 20% discount on their subscription for the first six months, plus one free seat for Spotlight Certification. + +**How to redeem:** Sign up using your Expensify Card. + +### Guideline +Guideline's full-service 401(k) plans make it easier and more affordable to offer your employees the retirement benefits they deserve. + +**Offer:** Three months free. + +**How to redeem:** Sign up using your Expensify Card. + +### Gusto +Gusto's platform helps businesses onboard, pay, insure, and support their teams. Expensify Cardholders receive three months of free service. + +**How to redeem:** Sign up using your Expensify Card. + +### QuickBooks Online +QuickBooks accounting software keeps your books accurate and up to date with features like invoicing, cash flow management, expense tracking, and more. + +**Offer:** 30% off QuickBooks Online for the first 12 months. + +**How to redeem:** Sign up using your Expensify Card. + +### Highfive +Highfive improves the ease and quality of in-room video conferencing. Expensify Cardholders receive 50% off the Highfive Select Starter Package and 10% off the Highfive Premium Package. + +**How to redeem:** Sign up using your Expensify Card. + +### Zendesk +Expensify Cardholders receive $436 in credits for Zendesk Suite products per month for the first year. + +**How to redeem:** +- Reach out to startups@zendesk.com with the message: "Expensify asked me to send an email regarding the Zendesk promotion.” You'll receive a promo code to use. +- Start a Zendesk trial (Suite or another plan) in USD (if your trial is not in USD, contact Zendesk). +- Click the "Buy Now" button inside your trial. +- Select your plan with monthly billing. + - The $436 monthly credit applies to up to four licenses of the Suite but can also be applied to any other monthly plan. +- Enter the promo code you receive. +- Complete the checkout process. + - After 12 monthly billing periods, your free credit will expire, and you'll be charged for the next month. + +### Xero +Xero offers accounting software with everything you need to run your business seamlessly. + +**Offer:** U.S. residents receive 50% off Xero for six months. + +**How to redeem:** Visit [this page](https://apps.xero.com/us/app/expensify?xtid=x30expensify&utm_source=expensify&utm_medium=web&utm_campaign=cardoffer) and sign up using your Expensify Card. + +### Freshworks +Boost your startup journey with customer and employee engagement solutions from Freshworks, including CRM, live chat, support, marketing automation, ITSM, and HRMS. + +**Offer:** $4,000 in credits on Freshworks products. + +**How to redeem:** Click [here](https://www.freshworks.com/partners/startup-program/expensify-card/) and fill out the form; Freshworks will automatically recognize your company as an Expensify Card customer. + +### Slack +Get 25% off your first year with Slack, enjoying premium features like unlimited messaging, apps, Slack Connect channels, group video calls, priority support, and more. + +**How to redeem:** Click [here](https://slack.com/promo/partner?remote_promo=ead919f5) to redeem the offer using your Expensify Card. + +### Deel.com +Deel simplifies onboarding international team members in 150 different countries. Expensify Cardholders receive three months free, followed by 30% off for the rest of the year. + +**How to redeem:** Click [here](https://www.deel.com/partners/expensify) and sign up using your Expensify Card. + +### Snap +Expensify Cardholders receive $1,000 in Snap credits when they spend $1,000 in Snapchat's Ads Manager. + +**How to redeem:** +- Click "Create Ad" or "Request a Call" by clicking [here](https://forbusiness.snapchat.com/). +- Enter your details to set up your account if you don't already have one. +- Add the Expensify Card as your payment option for your Snap Business account. +- Credits will automatically be placed in your account once you've reached $1,000 in spend. + +### Aircall +Aircall is a cloud-based phone system for sales and support teams. Expensify Cardholders receive two months free, with discounts ranging from $270 to $9,000+ depending on the number of users. + +**How to redeem:** +- Click [here](http://pages.aircall.io/Expensify-RewardsPartnerReferral.html) to sign up for a demo. +- Let the Aircall team know you're an Expensify customer. + +### NetSuite +NetSuite helps companies manage core business processes with a cloud-based ERP and accounting software. Expensify has a direct integration with NetSuite to synchronize data and customize expense coding. + +**Offer:** 10% off for the first year. + +**How to redeem:** +- Fill out this [Google form](https://docs.google.com/forms/d/e/1FAIpQLSeiOzLrdO-MgqeEMwEqgdQoh4SJBi42MZME9ycHp4SQjlk3bQ/viewform?usp=sf_link). +- An Expensify rep will introduce you to a NetSuite sales rep to start the process. +- Once set up, pay for your first year with NetSuite, and Expensify will send you a payment equal to 10% of your first-year contract within three months of your first NetSuite invoice. + +**Note:** This offer is only for prospective NetSuite customers. + +### PagerDuty +PagerDuty's platform integrates machine data and human intelligence to improve visibility and agility across organizations. + +**Offer:** 25% off. + +**How to redeem:** Sign up using your Expensify Card and enter the discount code EXPENSIFYPDTEAM for the Team plan or EXPENSIFYPDBUSINESS for the Business plan at checkout. + +### Typeform +Typeform makes collecting and sharing information easy and conversational, allowing you to create anything from surveys to apps without writing a single line of code. + +**Offer:** 30% off annual Premium and Professional plans. + +**How to redeem:** +- Click [here](https://try.typeform.com/expensify/?utm_source=expensify&utm_medium=referral&utm_campaign=expensify_integration&utm_content=directory) to get Typeform. +- Enter your details and set up your free account. +- Verify your email by clicking on the link Typeform sends you. +- Complete the onboarding flow within Typeform. +- Click the "Upgrade" button in your workspace. +- Select your plan. +- Enter the coupon code EXPENSIFY30 on the checkout page. +- Fill out all payment details using your Expensify Card and click "Upgrade now." + +### Intercom +Intercom offers a suite of messaging-first products to help businesses accelerate growth across the customer lifecycle. + +**Offer:** Three months of free service. + +**How to redeem:** Sign up using your Expensify Card. + +### Talkspace +Talkspace provides prescription management and personalized treatment through a network of licensed prescribers trained in mental healthcare. Expensify Cardholders receive $125 off Talk diff --git a/docs/articles/expensify-classic/workspaces/Enable-per-diem-expenses.md b/docs/articles/expensify-classic/workspaces/Enable-per-diem-expenses.md index 3ab757e75e9a..2d2f1b5afddc 100644 --- a/docs/articles/expensify-classic/workspaces/Enable-per-diem-expenses.md +++ b/docs/articles/expensify-classic/workspaces/Enable-per-diem-expenses.md @@ -14,9 +14,9 @@ To enable and set per diem rates, 4. Click the **Per Diem** tab on the left. 5. Click the Per Diem toggle to enable it. 6. Create a .csv, .txt, .xls, or .xlsx spreadsheet containing four columns: Destination, Sub-rate, Amount, and Currency. You’ll want a different row for each location that an employee may travel to, which may include states and/or countries to help account for cost differences across various locations. Here are some example templates you can use: - - [Germany multi-subrates](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fs3-us-west-1.amazonaws.com%2Fconcierge-responses-expensify-com%2Fuploads%252F1596692482998-Germany%2B-%2BPer%2BDiem.csv) - - [Sweden multi-subrates](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fs3-us-west-1.amazonaws.com%2Fconcierge-responses-expensify-com%2Fuploads%252F1604410653223-Swedish%2BPer%2BDiem%2BRates.csv) - - [South Africa single rates](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fs3-us-west-1.amazonaws.com%2Fconcierge-responses-expensify-com%2Fuploads%252F1596692413995-SA%2BPer%2BDiem%2BRates.csv) + - [Germany rates]({{site.url}}/assets/Files/Germany-per-diem.csv) + - [Sweden rates]({{site.url}}/assets/Files/Sweden-per-diem.csv) + - [South Africa single rates]({{site.url}}/assets/Files/South-Africa-per-diem.csv) 7. Click **Import from spreadsheet**. 8. Click **Upload** to select your spreadsheet. diff --git a/docs/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses.md index c037e8fe9cd3..5754d5f2ff90 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses.md +++ b/docs/articles/new-expensify/expenses-&-payments/Approve-and-pay-expenses.md @@ -4,18 +4,23 @@ description: Approve, hold, or pay expenses submitted to you ---
-When expenses are sent to you for approval, you have the option to: -- Approve and pay the expenses. -- Hold the expenses if payment needs to be delayed or if the expenses require additional information before they can be approved. +As a workspace admin, you can set an approval workflow for the expenses submitted to you. Expenses can be, + +- Instantly submitted without needing approval. +- Submitted at a desired frequency (daily, weekly, monthly) and follow an approval workflow. + +**Setting approval workflow and submission frequencies** + +# Manually approve expense + +When someone sends an expense or a group of expenses to you for approval, you’ll receive the expense in Expensify Chat for the related workspace. Chats with new updates appear with a green dot to the right of the chat message. Concierge also sends you an email notification for the new expense. {% include info.html %} -If your workspace does not require expense approvals, or if the expense is sent to you by a friend, you will not need to approve the expense and instead can immediately pay the expense when you are ready. +If an expense is sent to you by a friend, you will not need to approve the expense. Instead, you can immediately pay the expense when you are ready. {% include end-info.html %} # Approve expenses -When someone sends an expense or a group of expenses to you for approval, you’ll receive the expense in Expensify Chat for the related workspace. Chats with new updates appear with a green dot to the right of the chat message. Concierge also sends you an email notification for the new expense. - To approve an expense, 1. Open the Expensify Chat thread for the expense. @@ -27,7 +32,7 @@ To approve an expense, - **Request changes**: You can add a comment to the expense’s chat thread in your Expensify Chat inbox to request changes to the expense details. {% include info.html %} -Admins can modify an expense, if needed. +If the transaction is pending (a common occurrence with recent company card expenses or SmartScan expenses), you’ll need to wait until the transaction posts before approving it. {% include end-info.html %} ![The approve button in an expense]({{site.url}}/assets/images/ExpensifyHelp_ApproveExpense_1.png){:width="100%"} @@ -57,12 +62,28 @@ Once the expense has been approved, you can now pay the expense. Held expenses will not be available for payment until they have been approved. {% include end-info.html %} +# Unapprove an expense + +Some details of approved expenses and reports cannot be edited. If you need to edit an expense that has been approved, admins and the last approver have the option to unapprove reports. + +1. Click the workspace logo in the top left corner. +2. Select the workspace associated with the expense report. +3. Find the approved report by searching for the submitter. +4. Click the dropdown arrow at the top of the report to view the report actions. +5. Click **Unapprove**. + +The unapproved report will return to an editable state, and the submitter will receive an email and chat notification that the expense has been unapproved. + +{% include info.html %} +Reports that have been paid cannot be unapproved. If the approved expense has already been exported to an accounting package, you’ll see a warning that unapproving an expense can cause data discrepancies and Expensify Card reconciliation issues. Ideally, you’ll want to delete the data that has already been exported to the accounting package before approving the expense again. +{% include end-info.html %} + # Pay expenses Once you’ve approved an expense—or if the expense does not require approval—you’ll be able to pay it. {% include info.html %} -To pay expenses within Expensify, you’ll need to set up your Expensify Wallet. +To pay expenses within Expensify, you’ll need to [set up your Expensify Wallet](https://help.expensify.com/articles/new-expensify/expenses-&-payments/Set-up-your-wallet). {% include end-info.html %} To pay an expense, @@ -73,4 +94,14 @@ To pay an expense, - Click **Pay** to pay the full expense within Expensify. If the expenses contain one that has been held, the pay amount will only include the expenses that have not been held. Then you’ll select your payment method. - Click **Pay Elsewhere** to indicate that a payment has been sent using a method outside of Expensify, such as cash or a check. This will label the expense as Paid. +# FAQ + +**Why was an expense automatically approved?** + +We refer to this as **Instant Submit**. If a workspace doesn’t have Delayed Submission enabled, an expense report will automatically be submitted. + +**Why is an employee expense showing as ‘pending?’** + +An Expensify Card expense will show as pending if the merchant hasn’t posted it. This is usually the case with hotel holds, or card rental holds. A hold will normally last no more than 7-10 business days unless it’s a hotel hold, which can last 31 days. +
diff --git a/docs/articles/new-expensify/expenses-&-payments/Send-an-invoice.md b/docs/articles/new-expensify/expenses-&-payments/Send-an-invoice.md index 6546f57073ee..1154a6a3163e 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Send-an-invoice.md +++ b/docs/articles/new-expensify/expenses-&-payments/Send-an-invoice.md @@ -1,12 +1,38 @@ --- -title: Send an invoice -description: Notify a customer that a payment is due +title: Send an invoice and receive payment +description: steps to send an invoice to someone and receipt payment ---
-You can send invoices directly from Expensify to notify customers that a payment is due. +Workspace admins can enable invoicing on a workspace to send invoices and receive invoice payments through Expensify. -To create and send an invoice, +# Enable invoicing + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Scroll down and click **Workspaces** in the left menu. +3. Select the workspace for which you want to enable invoicing. +4. Click **More features** in the left menu. +5. Under the Earn section, enable the Invoice toggle + +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap **Workspaces**. +3. Select the workspace for which you want to enable invoicing. +4. Tap **More features**. +5. Under the Earn section, enable the Invoice toggle. + +{% include end-option.html %} + +# Send an invoice + +{% include info.html %} +Only workspace admins can send invoices. Invoices can be sent directly from Expensify to any customer, even if they do not have an Expensify account. +{% include end-info.html %} {% include selector.html values="desktop, mobile" %} @@ -28,28 +54,38 @@ To create and send an invoice, {% include end-selector.html %} -# How the customer pays an invoice - -Once an invoice is sent, the customer receives an automated email or text message to notify them of the invoice. They can use this notification to pay the invoice whenever they are ready. They will: +# Receive invoice payment -1. Click the link in the email or text notification they receive from Expensify. -2. Click **Pay**. -3. Choose **Paying as an individual** or **Paying as a business**. -4. Click **Pay Elsewhere**, which will mark the invoice as Paid. - -Currently, invoices must be paid outside of Expensify. However, the ability to make payments through Expensify is coming soon. - -![A photo of the pay button]({{site.url}}/assets/images/ExpensifyHelp-Invoice-1.png){:width="100%"} +If you have not [connected a business bank account](https://help.expensify.com/articles/new-expensify/expenses-&-payments/Connect-a-Business-Bank-Account) to receive invoice payments, you will see an **Invoice balance** in your [Wallet](https://help.expensify.com/articles/new-expensify/expenses-&-payments/Set-up-your-wallet). Expensify will automatically transfer these invoice payments once a business bank account is connected. # FAQs +**Why do I need to create a workspace to send an invoice?** + +A workspace is a configuration of settings related to your business. Since invoicing is considered a business feature, you must have a workspace to configure and use invoicing. + **How do I communicate with the sender/recipient about the invoice?** -You can communicate with the recipient in New Expensify. After sending an invoice, Expensify automatically creates an invoice room between the invoice sender and the payer to discuss anything related to the invoice. You can invite users to join the conversation, remove them from the room, and leave the room at any time. +Expensify will automatically notify the invoice recipient about the new invoice via email, SMS, and a mobile app notification, along with instructions on how to pay it. Daily reminders will be sent until the invoice is paid. + +Additionally, an invoice chat room will be automatically created in Expensify between the invoice sender, their workspace admins, and the payer. You can use this chat to discuss anything related to the invoice. + +**Can you export invoices between an accounting integration?** + +Yes, you can export invoices between Expensify and your connected [accounting integration](https://help.expensify.com/new-expensify/hubs/connections/). + +**Who can send and pay an invoice?** + +All workspace admins will be able to send and pay invoices. Invoices can also be paid by anyone, including recipients without an Expensify account. + +**What happens if I disable invoicing in the future?** + +When invoicing is disabled, all previously created invoice rooms and historical invoices will remain unaffected and continue to exist. However, all workspace admins will no longer have the option to send an invoice. + +**Why am I getting an error after I enter my website when connecting a business bank account?** -**Can you import and export invoices between an accounting integration?** +We can only accept a private domain website to ensure the security of your business. If you receive an error when entering your website, it is likely because the domain is not recognized as private. Make sure you are using a business email with a private domain. If you continue to experience issues, contact our support team at concierge@expensify.com for further assistance. -Yes, you can export and import invoices between Expensify and your QuickBooks Online or Xero integration.
diff --git a/docs/articles/new-expensify/expenses-&-payments/pay-an-invoice.md b/docs/articles/new-expensify/expenses-&-payments/pay-an-invoice.md new file mode 100644 index 000000000000..6221117ed962 --- /dev/null +++ b/docs/articles/new-expensify/expenses-&-payments/pay-an-invoice.md @@ -0,0 +1,56 @@ +--- +title: Pay an Invoice +description: process to pay an invoice +--- +
+ +{% include info.html %} +Anyone who receives an Expensify invoice can pay it using Expensify—even if they don’t have an Expensify account. +{% include end-info.html %} + +You'll receive an automated email or text notification when an invoice is sent to you for payment. + +To pay an invoice, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the link in the email or text notification you receive from Expensify. +2. Click **Pay**. +3. Choose **Pay as an individual** or **Pay as a business**. +4. Click **Add Bank Account** or **Add debit or credit card** to issue payment. +{% include end-option.html %} + +You can also view all unpaid invoices by searching for the sender’s email or phone number on the left-hand side of the app. The invoices waiting for your payment will have a green dot. + +{% include option.html value="mobile" %} +1. Tap the link in the email or text notification they receive from Expensify. +2. Tap **Pay**. +3. Choose **Pay as an individual** or **Pay as a business**. +4. Tap **Add Bank Account** or **Add debit or credit card** to issue payment. +{% include end-option.html %} + +# FAQ + +**Can someone else pay an invoice besides the person who received it?** + +No, only the person who received the invoice will see the option to pay it. + +**Who can send an invoice?** + +Anyone can pay an invoice they’ve received, but only Expensify customers can send an invoice. This feature ensures that businesses using Expensify can seamlessly manage their invoice billing processes while providing flexibility for their customers to make payments. + +Expensify invoicing is designed to cater to both business-to-customer (B2C) and business-to-business (B2B) needs, making it a versatile tool for businesses of all sizes. + +**Can I pay an invoice outside of Expensify** + +You will need to work with the vendor to discuss alternative payment options. You can chat with your vendor directly at expensify.com in the designated invoice room. + +**Can I add additional payment methods?** + +You can add additional payment methods to your [Wallet](https://help.expensify.com/articles/new-expensify/expenses-&-payments/Set-up-your-wallet). Click **Account Settings** > **Wallet** > click **Add Bank Account**. + +You will be prompted to choose a payment method when paying future invoices. + + +
diff --git a/docs/assets/Files/Germany-per-diem.csv b/docs/assets/Files/Germany-per-diem.csv new file mode 100644 index 000000000000..217cc0a2fed1 --- /dev/null +++ b/docs/assets/Files/Germany-per-diem.csv @@ -0,0 +1,1417 @@ +Destination,Currency,Subrate,Amount +Afghanistan,EUR,Full Day,30 +Afghanistan,EUR,Part Day,20 +Afghanistan,EUR,Overnight,95 +Afghanistan,EUR,Breakfast,-6 +Afghanistan,EUR,Lunch,-12 +Afghanistan,EUR,Dinner,-12 +Albania,EUR,Full Day,29 +Albania,EUR,Part Day,20 +Albania,EUR,Overnight,113 +Albania,EUR,Breakfast,-5.8 +Albania,EUR,Lunch,-11.6 +Albania,EUR,Dinner,-11.6 +Algeria,EUR,Full Day,51 +Algeria,EUR,Part Day,34 +Algeria,EUR,Overnight,173 +Algeria,EUR,Breakfast,-10.2 +Algeria,EUR,Lunch,-20.4 +Algeria,EUR,Dinner,-20.4 +Andorra,EUR,Full Day,34 +Andorra,EUR,Part Day,23 +Andorra,EUR,Overnight,45 +Andorra,EUR,Breakfast,-6.8 +Andorra,EUR,Lunch,-13.6 +Andorra,EUR,Dinner,-13.6 +Angola,EUR,Full Day,77 +Angola,EUR,Part Day,52 +Angola,EUR,Overnight,265 +Angola,EUR,Breakfast,-15.4 +Angola,EUR,Lunch,-30.8 +Angola,EUR,Dinner,-30.8 +Antigua and Barbuda,EUR,Full Day,45 +Antigua and Barbuda,EUR,Part Day,30 +Antigua and Barbuda,EUR,Overnight,177 +Antigua and Barbuda,EUR,Breakfast,-9 +Antigua and Barbuda,EUR,Lunch,-18 +Antigua and Barbuda,EUR,Dinner,-18 +Argentina,EUR,Full Day,34 +Argentina,EUR,Part Day,23 +Argentina,EUR,Overnight,144 +Argentina,EUR,Breakfast,-6.8 +Argentina,EUR,Lunch,-13.6 +Argentina,EUR,Dinner,-13.6 +Armenia,EUR,Full Day,23 +Armenia,EUR,Part Day,16 +Armenia,EUR,Overnight,63 +Armenia,EUR,Breakfast,-4.6 +Armenia,EUR,Lunch,-9.2 +Armenia,EUR,Dinner,-9.2 +Australia - Canberra,EUR,Full Day,51 +Australia - Canberra,EUR,Part Day,34 +Australia - Canberra,EUR,Overnight,158 +Australia - Canberra,EUR,Breakfast,-10.2 +Australia - Canberra,EUR,Lunch,-20.4 +Australia - Canberra,EUR,Dinner,-20.4 +Australia - Sydney,EUR,Full Day,68 +Australia - Sydney,EUR,Part Day,45 +Australia - Sydney,EUR,Overnight,184 +Australia - Sydney,EUR,Breakfast,-13.6 +Australia - Sydney,EUR,Lunch,-27.2 +Australia - Sydney,EUR,Dinner,-27.2 +Australia - for the rest,EUR,Full Day,51 +Australia - for the rest,EUR,Part Day,34 +Australia - for the rest,EUR,Overnight,158 +Australia - for the rest,EUR,Breakfast,-10.2 +Australia - for the rest,EUR,Lunch,-20.4 +Australia - for the rest,EUR,Dinner,-20.4 +Austria,EUR,Full Day,40 +Austria,EUR,Part Day,27 +Austria,EUR,Overnight,108 +Austria,EUR,Breakfast,-8 +Austria,EUR,Lunch,-16 +Austria,EUR,Dinner,-16 +Azerbaijan,EUR,Full Day,30 +Azerbaijan,EUR,Part Day,20 +Azerbaijan,EUR,Overnight,72 +Azerbaijan,EUR,Breakfast,-6 +Azerbaijan,EUR,Lunch,-12 +Azerbaijan,EUR,Dinner,-12 +Bahrain,EUR,Full Day,45 +Bahrain,EUR,Part Day,30 +Bahrain,EUR,Overnight,180 +Bahrain,EUR,Breakfast,-9 +Bahrain,EUR,Lunch,-18 +Bahrain,EUR,Dinner,-18 +Bangladesh,EUR,Full Day,30 +Bangladesh,EUR,Part Day,20 +Bangladesh,EUR,Overnight,111 +Bangladesh,EUR,Breakfast,-6 +Bangladesh,EUR,Lunch,-12 +Bangladesh,EUR,Dinner,-12 +Barbados,EUR,Full Day,52 +Barbados,EUR,Part Day,35 +Barbados,EUR,Overnight,165 +Barbados,EUR,Breakfast,-10.4 +Barbados,EUR,Lunch,-20.8 +Barbados,EUR,Dinner,-20.8 +Belarus,EUR,Full Day,20 +Belarus,EUR,Part Day,13 +Belarus,EUR,Overnight,98 +Belarus,EUR,Breakfast,-4 +Belarus,EUR,Lunch,-8 +Belarus,EUR,Dinner,-8 +Belgium,EUR,Full Day,42 +Belgium,EUR,Part Day,28 +Belgium,EUR,Overnight,135 +Belgium,EUR,Breakfast,-8.4 +Belgium,EUR,Lunch,-16.8 +Belgium,EUR,Dinner,-16.8 +Benin,EUR,Full Day,40 +Benin,EUR,Part Day,27 +Benin,EUR,Overnight,101 +Benin,EUR,Breakfast,-8 +Benin,EUR,Lunch,-16 +Benin,EUR,Dinner,-16 +Bolivia,EUR,Full Day,30 +Bolivia,EUR,Part Day,20 +Bolivia,EUR,Overnight,93 +Bolivia,EUR,Breakfast,-6 +Bolivia,EUR,Lunch,-12 +Bolivia,EUR,Dinner,-12 +Bosnia and Herzegovina,EUR,Full Day,18 +Bosnia and Herzegovina,EUR,Part Day,12 +Bosnia and Herzegovina,EUR,Overnight,73 +Bosnia and Herzegovina,EUR,Breakfast,-3.6 +Bosnia and Herzegovina,EUR,Lunch,-7.2 +Bosnia and Herzegovina,EUR,Dinner,-7.2 +Botswana,EUR,Full Day,40 +Botswana,EUR,Part Day,27 +Botswana,EUR,Overnight,102 +Botswana,EUR,Breakfast,-8 +Botswana,EUR,Lunch,-16 +Botswana,EUR,Dinner,-16 +Brazil - Brasilia,EUR,Full Day,57 +Brazil - Brasilia,EUR,Part Day,38 +Brazil - Brasilia,EUR,Overnight,127 +Brazil - Brasilia,EUR,Breakfast,-11.4 +Brazil - Brasilia,EUR,Lunch,-22.8 +Brazil - Brasilia,EUR,Dinner,-22.8 +Brazil - Rio de Janeiro,EUR,Full Day,57 +Brazil - Rio de Janeiro,EUR,Part Day,38 +Brazil - Rio de Janeiro,EUR,Overnight,145 +Brazil - Rio de Janeiro,EUR,Breakfast,-11.4 +Brazil - Rio de Janeiro,EUR,Lunch,-22.8 +Brazil - Rio de Janeiro,EUR,Dinner,-22.8 +Brazil - São Paulo,EUR,Full Day,53 +Brazil - São Paulo,EUR,Part Day,36 +Brazil - São Paulo,EUR,Overnight,132 +Brazil - São Paulo,EUR,Breakfast,-10.6 +Brazil - São Paulo,EUR,Lunch,-21.2 +Brazil - São Paulo,EUR,Dinner,-21.2 +Brazil - for the rest,EUR,Full Day,51 +Brazil - for the rest,EUR,Part Day,34 +Brazil - for the rest,EUR,Overnight,84 +Brazil - for the rest,EUR,Breakfast,-10.2 +Brazil - for the rest,EUR,Lunch,-20.4 +Brazil - for the rest,EUR,Dinner,-20.4 +Brunei,EUR,Full Day,48 +Brunei,EUR,Part Day,32 +Brunei,EUR,Overnight,106 +Brunei,EUR,Breakfast,-9.6 +Brunei,EUR,Lunch,-19.2 +Brunei,EUR,Dinner,-19.2 +Bulgaria,EUR,Full Day,22 +Bulgaria,EUR,Part Day,15 +Bulgaria,EUR,Overnight,90 +Bulgaria,EUR,Breakfast,-4.4 +Bulgaria,EUR,Lunch,-8.8 +Bulgaria,EUR,Dinner,-8.8 +Burkina Faso,EUR,Full Day,44 +Burkina Faso,EUR,Part Day,29 +Burkina Faso,EUR,Overnight,84 +Burkina Faso,EUR,Breakfast,-8.8 +Burkina Faso,EUR,Lunch,-17.6 +Burkina Faso,EUR,Dinner,-17.6 +Burundi,EUR,Full Day,47 +Burundi,EUR,Part Day,32 +Burundi,EUR,Overnight,98 +Burundi,EUR,Breakfast,-9.4 +Burundi,EUR,Lunch,-18.8 +Burundi,EUR,Dinner,-18.8 +Cambodia,EUR,Full Day,38 +Cambodia,EUR,Part Day,25 +Cambodia,EUR,Overnight,94 +Cambodia,EUR,Breakfast,-7.6 +Cambodia,EUR,Lunch,-15.2 +Cambodia,EUR,Dinner,-15.2 +Cameroon,EUR,Full Day,50 +Cameroon,EUR,Part Day,33 +Cameroon,EUR,Overnight,180 +Cameroon,EUR,Breakfast,-10 +Cameroon,EUR,Lunch,-20 +Cameroon,EUR,Dinner,-20 +Canada - Ottawa,EUR,Full Day,47 +Canada - Ottawa,EUR,Part Day,32 +Canada - Ottawa,EUR,Overnight,142 +Canada - Ottawa,EUR,Breakfast,-9.4 +Canada - Ottawa,EUR,Lunch,-18.8 +Canada - Ottawa,EUR,Dinner,-18.8 +Canada - Toronto,EUR,Full Day,51 +Canada - Toronto,EUR,Part Day,34 +Canada - Toronto,EUR,Overnight,161 +Canada - Toronto,EUR,Breakfast,-10.2 +Canada - Toronto,EUR,Lunch,-20.4 +Canada - Toronto,EUR,Dinner,-20.4 +Canada - Vancouver,EUR,Full Day,50 +Canada - Vancouver,EUR,Part Day,33 +Canada - Vancouver,EUR,Overnight,140 +Canada - Vancouver,EUR,Breakfast,-10 +Canada - Vancouver,EUR,Lunch,-20 +Canada - Vancouver,EUR,Dinner,-20 +Canada - for the rest,EUR,Full Day,47 +Canada - for the rest,EUR,Part Day,32 +Canada - for the rest,EUR,Overnight,134 +Canada - for the rest,EUR,Breakfast,-9.4 +Canada - for the rest,EUR,Lunch,-18.8 +Canada - for the rest,EUR,Dinner,-18.8 +Cape Verde,EUR,Full Day,30 +Cape Verde,EUR,Part Day,20 +Cape Verde,EUR,Overnight,105 +Cape Verde,EUR,Breakfast,-6 +Cape Verde,EUR,Lunch,-12 +Cape Verde,EUR,Dinner,-12 +Central African Republic,EUR,Full Day,46 +Central African Republic,EUR,Part Day,31 +Central African Republic,EUR,Overnight,74 +Central African Republic,EUR,Breakfast,-9.2 +Central African Republic,EUR,Lunch,-18.4 +Central African Republic,EUR,Dinner,-18.4 +Chad,EUR,Full Day,64 +Chad,EUR,Part Day,43 +Chad,EUR,Overnight,163 +Chad,EUR,Breakfast,-12.8 +Chad,EUR,Lunch,-25.6 +Chad,EUR,Dinner,-25.6 +Chile,EUR,Full Day,44 +Chile,EUR,Part Day,29 +Chile,EUR,Overnight,187 +Chile,EUR,Breakfast,-8.8 +Chile,EUR,Lunch,-17.6 +Chile,EUR,Dinner,-17.6 +China - Beijing,EUR,Full Day,46 +China - Beijing,EUR,Part Day,31 +China - Beijing,EUR,Overnight,142 +China - Beijing,EUR,Breakfast,-9.2 +China - Beijing,EUR,Lunch,-18.4 +China - Beijing,EUR,Dinner,-18.4 +China - Canton,EUR,Full Day,40 +China - Canton,EUR,Part Day,27 +China - Canton,EUR,Overnight,113 +China - Canton,EUR,Breakfast,-8 +China - Canton,EUR,Lunch,-16 +China - Canton,EUR,Dinner,-16 +China - Chengdu,EUR,Full Day,35 +China - Chengdu,EUR,Part Day,24 +China - Chengdu,EUR,Overnight,105 +China - Chengdu,EUR,Breakfast,-7 +China - Chengdu,EUR,Lunch,-14 +China - Chengdu,EUR,Dinner,-14 +China - Hong Kong,EUR,Full Day,74 +China - Hong Kong,EUR,Part Day,49 +China - Hong Kong,EUR,Overnight,145 +China - Hong Kong,EUR,Breakfast,-14.8 +China - Hong Kong,EUR,Lunch,-29.6 +China - Hong Kong,EUR,Dinner,-29.6 +China - Shanghai,EUR,Full Day,50 +China - Shanghai,EUR,Part Day,33 +China - Shanghai,EUR,Overnight,128 +China - Shanghai,EUR,Breakfast,-10 +China - Shanghai,EUR,Lunch,-20 +China - Shanghai,EUR,Dinner,-20 +China - for the rest,EUR,Full Day,50 +China - for the rest,EUR,Part Day,33 +China - for the rest,EUR,Overnight,78 +China - for the rest,EUR,Breakfast,-10 +China - for the rest,EUR,Lunch,-20 +China - for the rest,EUR,Dinner,-20 +Colombia,EUR,Full Day,41 +Colombia,EUR,Part Day,28 +Colombia,EUR,Overnight,126 +Colombia,EUR,Breakfast,-8.2 +Colombia,EUR,Lunch,-16.4 +Colombia,EUR,Dinner,-16.4 +"Congo, Democratic Republic",EUR,Full Day,68 +"Congo, Democratic Republic",EUR,Part Day,45 +"Congo, Democratic Republic",EUR,Overnight,171 +"Congo, Democratic Republic",EUR,Breakfast,-13.6 +"Congo, Democratic Republic",EUR,Lunch,-27.2 +"Congo, Democratic Republic",EUR,Dinner,-27.2 +"Congo, Republic",EUR,Full Day,50 +"Congo, Republic",EUR,Part Day,33 +"Congo, Republic",EUR,Overnight,200 +"Congo, Republic",EUR,Breakfast,-10 +"Congo, Republic",EUR,Lunch,-20 +"Congo, Republic",EUR,Dinner,-20 +Costa Rica,EUR,Full Day,47 +Costa Rica,EUR,Part Day,32 +Costa Rica,EUR,Overnight,93 +Costa Rica,EUR,Breakfast,-9.4 +Costa Rica,EUR,Lunch,-18.8 +Costa Rica,EUR,Dinner,-18.8 +Côte d’Ivoire,EUR,Full Day,51 +Côte d’Ivoire,EUR,Part Day,34 +Côte d’Ivoire,EUR,Overnight,146 +Côte d’Ivoire,EUR,Breakfast,-10.2 +Côte d’Ivoire,EUR,Lunch,-20.4 +Côte d’Ivoire,EUR,Dinner,-20.4 +Croatia,EUR,Full Day,28 +Croatia,EUR,Part Day,19 +Croatia,EUR,Overnight,75 +Croatia,EUR,Breakfast,-5.6 +Croatia,EUR,Lunch,-11.2 +Croatia,EUR,Dinner,-11.2 +Cuba,EUR,Full Day,46 +Cuba,EUR,Part Day,31 +Cuba,EUR,Overnight,228 +Cuba,EUR,Breakfast,-9.2 +Cuba,EUR,Lunch,-18.4 +Cuba,EUR,Dinner,-18.4 +Cyprus,EUR,Full Day,45 +Cyprus,EUR,Part Day,30 +Cyprus,EUR,Overnight,116 +Cyprus,EUR,Breakfast,-9 +Cyprus,EUR,Lunch,-18 +Cyprus,EUR,Dinner,-18 +Czech Republic,EUR,Full Day,35 +Czech Republic,EUR,Part Day,24 +Czech Republic,EUR,Overnight,94 +Czech Republic,EUR,Breakfast,-7 +Czech Republic,EUR,Lunch,-14 +Czech Republic,EUR,Dinner,-14 +Denmark,EUR,Full Day,58 +Denmark,EUR,Part Day,39 +Denmark,EUR,Overnight,143 +Denmark,EUR,Breakfast,-11.6 +Denmark,EUR,Lunch,-23.2 +Denmark,EUR,Dinner,-23.2 +Djibouti,EUR,Full Day,65 +Djibouti,EUR,Part Day,44 +Djibouti,EUR,Overnight,305 +Djibouti,EUR,Breakfast,-13 +Djibouti,EUR,Lunch,-26 +Djibouti,EUR,Dinner,-26 +Dominica,EUR,Full Day,45 +Dominica,EUR,Part Day,30 +Dominica,EUR,Overnight,177 +Dominica,EUR,Breakfast,-9 +Dominica,EUR,Lunch,-18 +Dominica,EUR,Dinner,-18 +Dominican Republic,EUR,Full Day,45 +Dominican Republic,EUR,Part Day,30 +Dominican Republic,EUR,Overnight,147 +Dominican Republic,EUR,Breakfast,-9 +Dominican Republic,EUR,Lunch,-18 +Dominican Republic,EUR,Dinner,-18 +Ecuador,EUR,Full Day,44 +Ecuador,EUR,Part Day,29 +Ecuador,EUR,Overnight,97 +Ecuador,EUR,Breakfast,-8.8 +Ecuador,EUR,Lunch,-17.6 +Ecuador,EUR,Dinner,-17.6 +Egypt,EUR,Full Day,41 +Egypt,EUR,Part Day,28 +Egypt,EUR,Overnight,125 +Egypt,EUR,Breakfast,-8.2 +Egypt,EUR,Lunch,-16.4 +Egypt,EUR,Dinner,-16.4 +El Salvador,EUR,Full Day,44 +El Salvador,EUR,Part Day,29 +El Salvador,EUR,Overnight,119 +El Salvador,EUR,Breakfast,-8.8 +El Salvador,EUR,Lunch,-17.6 +El Salvador,EUR,Dinner,-17.6 +Equatorial Guinea,EUR,Full Day,36 +Equatorial Guinea,EUR,Part Day,24 +Equatorial Guinea,EUR,Overnight,166 +Equatorial Guinea,EUR,Breakfast,-7.2 +Equatorial Guinea,EUR,Lunch,-14.4 +Equatorial Guinea,EUR,Dinner,-14.4 +Eritrea,EUR,Full Day,50 +Eritrea,EUR,Part Day,33 +Eritrea,EUR,Overnight,91 +Eritrea,EUR,Breakfast,-10 +Eritrea,EUR,Lunch,-20 +Eritrea,EUR,Dinner,-20 +Estonia,EUR,Full Day,27 +Estonia,EUR,Part Day,18 +Estonia,EUR,Overnight,71 +Estonia,EUR,Breakfast,-5.4 +Estonia,EUR,Lunch,-10.8 +Estonia,EUR,Dinner,-10.8 +Ethiopia,EUR,Full Day,27 +Ethiopia,EUR,Part Day,18 +Ethiopia,EUR,Overnight,86 +Ethiopia,EUR,Breakfast,-5.4 +Ethiopia,EUR,Lunch,-10.8 +Ethiopia,EUR,Dinner,-10.8 +Fiji,EUR,Full Day,34 +Fiji,EUR,Part Day,23 +Fiji,EUR,Overnight,69 +Fiji,EUR,Breakfast,-6.8 +Fiji,EUR,Lunch,-13.6 +Fiji,EUR,Dinner,-13.6 +Finland,EUR,Full Day,50 +Finland,EUR,Part Day,33 +Finland,EUR,Overnight,136 +Finland,EUR,Breakfast,-10 +Finland,EUR,Lunch,-20 +Finland,EUR,Dinner,-20 +France - Lyon,EUR,Full Day,53 +France - Lyon,EUR,Part Day,36 +France - Lyon,EUR,Overnight,115 +France - Lyon,EUR,Breakfast,-10.6 +France - Lyon,EUR,Lunch,-21.2 +France - Lyon,EUR,Dinner,-21.2 +France - Marseille,EUR,Full Day,46 +France - Marseille,EUR,Part Day,31 +France - Marseille,EUR,Overnight,101 +France - Marseille,EUR,Breakfast,-9.2 +France - Marseille,EUR,Lunch,-18.4 +France - Marseille,EUR,Dinner,-18.4 +"France - Paris and Departments 92, 93 and 94",EUR,Full Day,58 +"France - Paris and Departments 92, 93 and 94",EUR,Part Day,39 +"France - Paris and Departments 92, 93 and 94",EUR,Overnight,152 +"France - Paris and Departments 92, 93 and 94",EUR,Breakfast,-11.6 +"France - Paris and Departments 92, 93 and 94",EUR,Lunch,-23.2 +"France - Paris and Departments 92, 93 and 94",EUR,Dinner,-23.2 +France - Strasbourg,EUR,Full Day,51 +France - Strasbourg,EUR,Part Day,34 +France - Strasbourg,EUR,Overnight,96 +France - Strasbourg,EUR,Breakfast,-10.2 +France - Strasbourg,EUR,Lunch,-20.4 +France - Strasbourg,EUR,Dinner,-20.4 +France - for the rest,EUR,Full Day,44 +France - for the rest,EUR,Part Day,29 +France - for the rest,EUR,Overnight,115 +France - for the rest,EUR,Breakfast,-8.8 +France - for the rest,EUR,Lunch,-17.6 +France - for the rest,EUR,Dinner,-17.6 +Gabon,EUR,Full Day,62 +Gabon,EUR,Part Day,41 +Gabon,EUR,Overnight,278 +Gabon,EUR,Breakfast,-12.4 +Gabon,EUR,Lunch,-24.8 +Gabon,EUR,Dinner,-24.8 +Gambia,EUR,Full Day,30 +Gambia,EUR,Part Day,20 +Gambia,EUR,Overnight,125 +Gambia,EUR,Breakfast,-6 +Gambia,EUR,Lunch,-12 +Gambia,EUR,Dinner,-12 +Georgia,EUR,Full Day,35 +Georgia,EUR,Part Day,24 +Georgia,EUR,Overnight,88 +Georgia,EUR,Breakfast,-7 +Georgia,EUR,Lunch,-14 +Georgia,EUR,Dinner,-14 +Ghana,EUR,Full Day,46 +Ghana,EUR,Part Day,31 +Ghana,EUR,Overnight,148 +Ghana,EUR,Breakfast,-9.2 +Ghana,EUR,Lunch,-18.4 +Ghana,EUR,Dinner,-18.4 +Greece - Athens,EUR,Full Day,46 +Greece - Athens,EUR,Part Day,31 +Greece - Athens,EUR,Overnight,132 +Greece - Athens,EUR,Breakfast,-9.2 +Greece - Athens,EUR,Lunch,-18.4 +Greece - Athens,EUR,Dinner,-18.4 +Greece - for the rest,EUR,Full Day,36 +Greece - for the rest,EUR,Part Day,24 +Greece - for the rest,EUR,Overnight,135 +Greece - for the rest,EUR,Breakfast,-7.2 +Greece - for the rest,EUR,Lunch,-14.4 +Greece - for the rest,EUR,Dinner,-14.4 +Grenada,EUR,Full Day,45 +Grenada,EUR,Part Day,30 +Grenada,EUR,Overnight,177 +Grenada,EUR,Breakfast,-9 +Grenada,EUR,Lunch,-18 +Grenada,EUR,Dinner,-18 +Guatemala,EUR,Full Day,34 +Guatemala,EUR,Part Day,23 +Guatemala,EUR,Overnight,90 +Guatemala,EUR,Breakfast,-6.8 +Guatemala,EUR,Lunch,-13.6 +Guatemala,EUR,Dinner,-13.6 +Guinea,EUR,Full Day,46 +Guinea,EUR,Part Day,31 +Guinea,EUR,Overnight,118 +Guinea,EUR,Breakfast,-9.2 +Guinea,EUR,Lunch,-18.4 +Guinea,EUR,Dinner,-18.4 +Guinea-Bissau,EUR,Full Day,24 +Guinea-Bissau,EUR,Part Day,16 +Guinea-Bissau,EUR,Overnight,86 +Guinea-Bissau,EUR,Breakfast,-4.8 +Guinea-Bissau,EUR,Lunch,-9.6 +Guinea-Bissau,EUR,Dinner,-9.6 +Guyana,EUR,Full Day,45 +Guyana,EUR,Part Day,30 +Guyana,EUR,Overnight,177 +Guyana,EUR,Breakfast,-9 +Guyana,EUR,Lunch,-18 +Guyana,EUR,Dinner,-18 +Haiti,EUR,Full Day,58 +Haiti,EUR,Part Day,39 +Haiti,EUR,Overnight,130 +Haiti,EUR,Breakfast,-11.6 +Haiti,EUR,Lunch,-23.2 +Haiti,EUR,Dinner,-23.2 +Honduras,EUR,Full Day,48 +Honduras,EUR,Part Day,32 +Honduras,EUR,Overnight,101 +Honduras,EUR,Breakfast,-9.6 +Honduras,EUR,Lunch,-19.2 +Honduras,EUR,Dinner,-19.2 +Hungary,EUR,Full Day,22 +Hungary,EUR,Part Day,15 +Hungary,EUR,Overnight,63 +Hungary,EUR,Breakfast,-4.4 +Hungary,EUR,Lunch,-8.8 +Hungary,EUR,Dinner,-8.8 +Iceland,EUR,Full Day,47 +Iceland,EUR,Part Day,32 +Iceland,EUR,Overnight,108 +Iceland,EUR,Breakfast,-9.4 +Iceland,EUR,Lunch,-18.8 +Iceland,EUR,Dinner,-18.8 +India - Chennai,EUR,Full Day,32 +India - Chennai,EUR,Part Day,21 +India - Chennai,EUR,Overnight,85 +India - Chennai,EUR,Breakfast,-6.4 +India - Chennai,EUR,Lunch,-12.8 +India - Chennai,EUR,Dinner,-12.8 +India - Kolkata,EUR,Full Day,35 +India - Kolkata,EUR,Part Day,24 +India - Kolkata,EUR,Overnight,145 +India - Kolkata,EUR,Breakfast,-7 +India - Kolkata,EUR,Lunch,-14 +India - Kolkata,EUR,Dinner,-14 +India - Mumbai,EUR,Full Day,50 +India - Mumbai,EUR,Part Day,33 +India - Mumbai,EUR,Overnight,146 +India - Mumbai,EUR,Breakfast,-10 +India - Mumbai,EUR,Lunch,-20 +India - Mumbai,EUR,Dinner,-20 +India - New Delhi,EUR,Full Day,38 +India - New Delhi,EUR,Part Day,25 +India - New Delhi,EUR,Overnight,185 +India - New Delhi,EUR,Breakfast,-7.6 +India - New Delhi,EUR,Lunch,-15.2 +India - New Delhi,EUR,Dinner,-15.2 +India - for the rest,EUR,Full Day,32 +India - for the rest,EUR,Part Day,21 +India - for the rest,EUR,Overnight,85 +India - for the rest,EUR,Breakfast,-6.4 +India - for the rest,EUR,Lunch,-12.8 +India - for the rest,EUR,Dinner,-12.8 +Indonesia,EUR,Full Day,38 +Indonesia,EUR,Part Day,25 +Indonesia,EUR,Overnight,130 +Indonesia,EUR,Breakfast,-7.6 +Indonesia,EUR,Lunch,-15.2 +Indonesia,EUR,Dinner,-15.2 +Iran,EUR,Full Day,33 +Iran,EUR,Part Day,22 +Iran,EUR,Overnight,196 +Iran,EUR,Breakfast,-6.6 +Iran,EUR,Lunch,-13.2 +Iran,EUR,Dinner,-13.2 +Ireland,EUR,Full Day,44 +Ireland,EUR,Part Day,29 +Ireland,EUR,Overnight,92 +Ireland,EUR,Breakfast,-8.8 +Ireland,EUR,Lunch,-17.6 +Ireland,EUR,Dinner,-17.6 +Israel,EUR,Full Day,56 +Israel,EUR,Part Day,37 +Israel,EUR,Overnight,191 +Israel,EUR,Breakfast,-11.2 +Israel,EUR,Lunch,-22.4 +Israel,EUR,Dinner,-22.4 +Italy - Milan,EUR,Full Day,45 +Italy - Milan,EUR,Part Day,30 +Italy - Milan,EUR,Overnight,158 +Italy - Milan,EUR,Breakfast,-9 +Italy - Milan,EUR,Lunch,-18 +Italy - Milan,EUR,Dinner,-18 +Italy - Rome,EUR,Full Day,40 +Italy - Rome,EUR,Part Day,27 +Italy - Rome,EUR,Overnight,135 +Italy - Rome,EUR,Breakfast,-8 +Italy - Rome,EUR,Lunch,-16 +Italy - Rome,EUR,Dinner,-16 +Italy - for the rest,EUR,Full Day,40 +Italy - for the rest,EUR,Part Day,27 +Italy - for the rest,EUR,Overnight,135 +Italy - for the rest,EUR,Breakfast,-8 +Italy - for the rest,EUR,Lunch,-16 +Italy - for the rest,EUR,Dinner,-16 +Jamaica,EUR,Full Day,57 +Jamaica,EUR,Part Day,38 +Jamaica,EUR,Overnight,138 +Jamaica,EUR,Breakfast,-11.4 +Jamaica,EUR,Lunch,-22.8 +Jamaica,EUR,Dinner,-22.8 +Japan - Tokyo,EUR,Full Day,66 +Japan - Tokyo,EUR,Part Day,44 +Japan - Tokyo,EUR,Overnight,233 +Japan - Tokyo,EUR,Breakfast,-13.2 +Japan - Tokyo,EUR,Lunch,-26.4 +Japan - Tokyo,EUR,Dinner,-26.4 +Japan - for the rest,EUR,Full Day,51 +Japan - for the rest,EUR,Part Day,34 +Japan - for the rest,EUR,Overnight,156 +Japan - for the rest,EUR,Breakfast,-10.2 +Japan - for the rest,EUR,Lunch,-20.4 +Japan - for the rest,EUR,Dinner,-20.4 +Jordan,EUR,Full Day,46 +Jordan,EUR,Part Day,31 +Jordan,EUR,Overnight,126 +Jordan,EUR,Breakfast,-9.2 +Jordan,EUR,Lunch,-18.4 +Jordan,EUR,Dinner,-18.4 +Kazakhstan,EUR,Full Day,45 +Kazakhstan,EUR,Part Day,30 +Kazakhstan,EUR,Overnight,111 +Kazakhstan,EUR,Breakfast,-9 +Kazakhstan,EUR,Lunch,-18 +Kazakhstan,EUR,Dinner,-18 +Kenya,EUR,Full Day,42 +Kenya,EUR,Part Day,28 +Kenya,EUR,Overnight,223 +Kenya,EUR,Breakfast,-8.4 +Kenya,EUR,Lunch,-16.8 +Kenya,EUR,Dinner,-16.8 +"Korea, Democratic People's Republic",EUR,Full Day,39 +"Korea, Democratic People's Republic",EUR,Part Day,26 +"Korea, Democratic People's Republic",EUR,Overnight,132 +"Korea, Democratic People's Republic",EUR,Breakfast,-7.8 +"Korea, Democratic People's Republic",EUR,Lunch,-15.6 +"Korea, Democratic People's Republic",EUR,Dinner,-15.6 +"Korea, People's Republic",EUR,Full Day,58 +"Korea, People's Republic",EUR,Part Day,39 +"Korea, People's Republic",EUR,Overnight,112 +"Korea, People's Republic",EUR,Breakfast,-11.6 +"Korea, People's Republic",EUR,Lunch,-23.2 +"Korea, People's Republic",EUR,Dinner,-23.2 +Kosovo,EUR,Full Day,23 +Kosovo,EUR,Part Day,16 +Kosovo,EUR,Overnight,57 +Kosovo,EUR,Breakfast,-4.6 +Kosovo,EUR,Lunch,-9.2 +Kosovo,EUR,Dinner,-9.2 +Kuwait,EUR,Full Day,42 +Kuwait,EUR,Part Day,28 +Kuwait,EUR,Overnight,185 +Kuwait,EUR,Breakfast,-8.4 +Kuwait,EUR,Lunch,-16.8 +Kuwait,EUR,Dinner,-16.8 +Kyrgyzstan,EUR,Full Day,29 +Kyrgyzstan,EUR,Part Day,20 +Kyrgyzstan,EUR,Overnight,91 +Kyrgyzstan,EUR,Breakfast,-5.8 +Kyrgyzstan,EUR,Lunch,-11.6 +Kyrgyzstan,EUR,Dinner,-11.6 +Laos,EUR,Full Day,33 +Laos,EUR,Part Day,22 +Laos,EUR,Overnight,96 +Laos,EUR,Breakfast,-6.6 +Laos,EUR,Lunch,-13.2 +Laos,EUR,Dinner,-13.2 +Latvia,EUR,Full Day,30 +Latvia,EUR,Part Day,20 +Latvia,EUR,Overnight,80 +Latvia,EUR,Breakfast,-6 +Latvia,EUR,Lunch,-12 +Latvia,EUR,Dinner,-12 +Lebanon,EUR,Full Day,59 +Lebanon,EUR,Part Day,40 +Lebanon,EUR,Overnight,123 +Lebanon,EUR,Breakfast,-11.8 +Lebanon,EUR,Lunch,-23.6 +Lebanon,EUR,Dinner,-23.6 +Lesotho,EUR,Full Day,24 +Lesotho,EUR,Part Day,16 +Lesotho,EUR,Overnight,103 +Lesotho,EUR,Breakfast,-4.8 +Lesotho,EUR,Lunch,-9.6 +Lesotho,EUR,Dinner,-9.6 +Libya,EUR,Full Day,63 +Libya,EUR,Part Day,42 +Libya,EUR,Overnight,135 +Libya,EUR,Breakfast,-12.6 +Libya,EUR,Lunch,-25.2 +Libya,EUR,Dinner,-25.2 +Liechtenstein,EUR,Full Day,53 +Liechtenstein,EUR,Part Day,36 +Liechtenstein,EUR,Overnight,180 +Liechtenstein,EUR,Breakfast,-10.6 +Liechtenstein,EUR,Lunch,-21.2 +Liechtenstein,EUR,Dinner,-21.2 +Lithuania,EUR,Full Day,24 +Lithuania,EUR,Part Day,16 +Lithuania,EUR,Overnight,68 +Lithuania,EUR,Breakfast,-4.8 +Lithuania,EUR,Lunch,-9.6 +Lithuania,EUR,Dinner,-9.6 +Luxembourg,EUR,Full Day,47 +Luxembourg,EUR,Part Day,32 +Luxembourg,EUR,Overnight,130 +Luxembourg,EUR,Breakfast,-9.4 +Luxembourg,EUR,Lunch,-18.8 +Luxembourg,EUR,Dinner,-18.8 +Macedonia,EUR,Full Day,29 +Macedonia,EUR,Part Day,20 +Macedonia,EUR,Overnight,95 +Macedonia,EUR,Breakfast,-5.8 +Macedonia,EUR,Lunch,-11.6 +Macedonia,EUR,Dinner,-11.6 +Madagascar,EUR,Full Day,34 +Madagascar,EUR,Part Day,23 +Madagascar,EUR,Overnight,87 +Madagascar,EUR,Breakfast,-6.8 +Madagascar,EUR,Lunch,-13.6 +Madagascar,EUR,Dinner,-13.6 +Malawi,EUR,Full Day,47 +Malawi,EUR,Part Day,32 +Malawi,EUR,Overnight,123 +Malawi,EUR,Breakfast,-9.4 +Malawi,EUR,Lunch,-18.8 +Malawi,EUR,Dinner,-18.8 +Malaysia,EUR,Full Day,34 +Malaysia,EUR,Part Day,23 +Malaysia,EUR,Overnight,88 +Malaysia,EUR,Breakfast,-6.8 +Malaysia,EUR,Lunch,-13.6 +Malaysia,EUR,Dinner,-13.6 +Maldives,EUR,Full Day,52 +Maldives,EUR,Part Day,35 +Maldives,EUR,Overnight,170 +Maldives,EUR,Breakfast,-10.4 +Maldives,EUR,Lunch,-20.8 +Maldives,EUR,Dinner,-20.8 +Mali,EUR,Full Day,41 +Mali,EUR,Part Day,28 +Mali,EUR,Overnight,122 +Mali,EUR,Breakfast,-8.2 +Mali,EUR,Lunch,-16.4 +Mali,EUR,Dinner,-16.4 +Malta,EUR,Full Day,45 +Malta,EUR,Part Day,30 +Malta,EUR,Overnight,112 +Malta,EUR,Breakfast,-9 +Malta,EUR,Lunch,-18 +Malta,EUR,Dinner,-18 +Marshall Islands,EUR,Full Day,63 +Marshall Islands,EUR,Part Day,42 +Marshall Islands,EUR,Overnight,102 +Marshall Islands,EUR,Breakfast,-12.6 +Marshall Islands,EUR,Lunch,-25.2 +Marshall Islands,EUR,Dinner,-25.2 +Mauritania,EUR,Full Day,39 +Mauritania,EUR,Part Day,26 +Mauritania,EUR,Overnight,105 +Mauritania,EUR,Breakfast,-7.8 +Mauritania,EUR,Lunch,-15.6 +Mauritania,EUR,Dinner,-15.6 +Mauritius,EUR,Full Day,54 +Mauritius,EUR,Part Day,36 +Mauritius,EUR,Overnight,220 +Mauritius,EUR,Breakfast,-10.8 +Mauritius,EUR,Lunch,-21.6 +Mauritius,EUR,Dinner,-21.6 +Mexico,EUR,Full Day,41 +Mexico,EUR,Part Day,28 +Mexico,EUR,Overnight,141 +Mexico,EUR,Breakfast,-8.2 +Mexico,EUR,Lunch,-16.4 +Mexico,EUR,Dinner,-16.4 +Micronesia,EUR,Full Day,33 +Micronesia,EUR,Part Day,22 +Micronesia,EUR,Overnight,116 +Micronesia,EUR,Breakfast,-6.6 +Micronesia,EUR,Lunch,-13.2 +Micronesia,EUR,Dinner,-13.2 +"Moldova, Republic",EUR,Full Day,24 +"Moldova, Republic",EUR,Part Day,16 +"Moldova, Republic",EUR,Overnight,88 +"Moldova, Republic",EUR,Breakfast,-4.8 +"Moldova, Republic",EUR,Lunch,-9.6 +"Moldova, Republic",EUR,Dinner,-9.6 +Monaco,EUR,Full Day,42 +Monaco,EUR,Part Day,28 +Monaco,EUR,Overnight,180 +Monaco,EUR,Breakfast,-8.4 +Monaco,EUR,Lunch,-16.8 +Monaco,EUR,Dinner,-16.8 +Mongolia,EUR,Full Day,27 +Mongolia,EUR,Part Day,18 +Mongolia,EUR,Overnight,92 +Mongolia,EUR,Breakfast,-5.4 +Mongolia,EUR,Lunch,-10.8 +Mongolia,EUR,Dinner,-10.8 +Montenegro,EUR,Full Day,29 +Montenegro,EUR,Part Day,20 +Montenegro,EUR,Overnight,94 +Montenegro,EUR,Breakfast,-5.8 +Montenegro,EUR,Lunch,-11.6 +Montenegro,EUR,Dinner,-11.6 +Morocco,EUR,Full Day,42 +Morocco,EUR,Part Day,28 +Morocco,EUR,Overnight,129 +Morocco,EUR,Breakfast,-8.4 +Morocco,EUR,Lunch,-16.8 +Morocco,EUR,Dinner,-16.8 +Mozambique,EUR,Full Day,38 +Mozambique,EUR,Part Day,25 +Mozambique,EUR,Overnight,146 +Mozambique,EUR,Breakfast,-7.6 +Mozambique,EUR,Lunch,-15.2 +Mozambique,EUR,Dinner,-15.2 +Myanmar,EUR,Full Day,35 +Myanmar,EUR,Part Day,24 +Myanmar,EUR,Overnight,155 +Myanmar,EUR,Breakfast,-7 +Myanmar,EUR,Lunch,-14 +Myanmar,EUR,Dinner,-14 +Namibia,EUR,Full Day,23 +Namibia,EUR,Part Day,16 +Namibia,EUR,Overnight,77 +Namibia,EUR,Breakfast,-4.6 +Namibia,EUR,Lunch,-9.2 +Namibia,EUR,Dinner,-9.2 +Nepal,EUR,Full Day,28 +Nepal,EUR,Part Day,19 +Nepal,EUR,Overnight,86 +Nepal,EUR,Breakfast,-5.6 +Nepal,EUR,Lunch,-11.2 +Nepal,EUR,Dinner,-11.2 +Netherlands,EUR,Full Day,46 +Netherlands,EUR,Part Day,31 +Netherlands,EUR,Overnight,119 +Netherlands,EUR,Breakfast,-9.2 +Netherlands,EUR,Lunch,-18.4 +Netherlands,EUR,Dinner,-18.4 +New Zealand,EUR,Full Day,56 +New Zealand,EUR,Part Day,37 +New Zealand,EUR,Overnight,153 +New Zealand,EUR,Breakfast,-11.2 +New Zealand,EUR,Lunch,-22.4 +New Zealand,EUR,Dinner,-22.4 +Nicaragua,EUR,Full Day,36 +Nicaragua,EUR,Part Day,24 +Nicaragua,EUR,Overnight,81 +Nicaragua,EUR,Breakfast,-7.2 +Nicaragua,EUR,Lunch,-14.4 +Nicaragua,EUR,Dinner,-14.4 +Niger,EUR,Full Day,41 +Niger,EUR,Part Day,28 +Niger,EUR,Overnight,89 +Niger,EUR,Breakfast,-8.2 +Niger,EUR,Lunch,-16.4 +Niger,EUR,Dinner,-16.4 +Nigeria,EUR,Full Day,63 +Nigeria,EUR,Part Day,42 +Nigeria,EUR,Overnight,255 +Nigeria,EUR,Breakfast,-12.6 +Nigeria,EUR,Lunch,-25.2 +Nigeria,EUR,Dinner,-25.2 +Norway,EUR,Full Day,80 +Norway,EUR,Part Day,53 +Norway,EUR,Overnight,182 +Norway,EUR,Breakfast,-16 +Norway,EUR,Lunch,-32 +Norway,EUR,Dinner,-32 +Oman,EUR,Full Day,60 +Oman,EUR,Part Day,40 +Oman,EUR,Overnight,200 +Oman,EUR,Breakfast,-12 +Oman,EUR,Lunch,-24 +Oman,EUR,Dinner,-24 +Pakistan - Islamabad,EUR,Full Day,30 +Pakistan - Islamabad,EUR,Part Day,20 +Pakistan - Islamabad,EUR,Overnight,165 +Pakistan - Islamabad,EUR,Breakfast,-6 +Pakistan - Islamabad,EUR,Lunch,-12 +Pakistan - Islamabad,EUR,Dinner,-12 +Pakistan - for the rest,EUR,Full Day,27 +Pakistan - for the rest,EUR,Part Day,18 +Pakistan - for the rest,EUR,Overnight,68 +Pakistan - for the rest,EUR,Breakfast,-5.4 +Pakistan - for the rest,EUR,Lunch,-10.8 +Pakistan - for the rest,EUR,Dinner,-10.8 +Palau,EUR,Full Day,51 +Palau,EUR,Part Day,34 +Palau,EUR,Overnight,179 +Palau,EUR,Breakfast,-10.2 +Palau,EUR,Lunch,-20.4 +Palau,EUR,Dinner,-20.4 +Panama,EUR,Full Day,39 +Panama,EUR,Part Day,26 +Panama,EUR,Overnight,111 +Panama,EUR,Breakfast,-7.8 +Panama,EUR,Lunch,-15.6 +Panama,EUR,Dinner,-15.6 +Papua New Guinea,EUR,Full Day,60 +Papua New Guinea,EUR,Part Day,40 +Papua New Guinea,EUR,Overnight,234 +Papua New Guinea,EUR,Breakfast,-12 +Papua New Guinea,EUR,Lunch,-24 +Papua New Guinea,EUR,Dinner,-24 +Paraguay,EUR,Full Day,38 +Paraguay,EUR,Part Day,25 +Paraguay,EUR,Overnight,108 +Paraguay,EUR,Breakfast,-7.6 +Paraguay,EUR,Lunch,-15.2 +Paraguay,EUR,Dinner,-15.2 +Peru,EUR,Full Day,30 +Peru,EUR,Part Day,20 +Peru,EUR,Overnight,93 +Peru,EUR,Breakfast,-6 +Peru,EUR,Lunch,-12 +Peru,EUR,Dinner,-12 +Philippines,EUR,Full Day,33 +Philippines,EUR,Part Day,22 +Philippines,EUR,Overnight,116 +Philippines,EUR,Breakfast,-6.6 +Philippines,EUR,Lunch,-13.2 +Philippines,EUR,Dinner,-13.2 +Poland - Danzig,EUR,Full Day,30 +Poland - Danzig,EUR,Part Day,20 +Poland - Danzig,EUR,Overnight,84 +Poland - Danzig,EUR,Breakfast,-6 +Poland - Danzig,EUR,Lunch,-12 +Poland - Danzig,EUR,Dinner,-12 +Poland - Kraków,EUR,Full Day,27 +Poland - Kraków,EUR,Part Day,18 +Poland - Kraków,EUR,Overnight,86 +Poland - Kraków,EUR,Breakfast,-5.4 +Poland - Kraków,EUR,Lunch,-10.8 +Poland - Kraków,EUR,Dinner,-10.8 +Poland - Warsaw,EUR,Full Day,29 +Poland - Warsaw,EUR,Part Day,20 +Poland - Warsaw,EUR,Overnight,109 +Poland - Warsaw,EUR,Breakfast,-5.8 +Poland - Warsaw,EUR,Lunch,-11.6 +Poland - Warsaw,EUR,Dinner,-11.6 +Poland - Wrocław,EUR,Full Day,33 +Poland - Wrocław,EUR,Part Day,22 +Poland - Wrocław,EUR,Overnight,117 +Poland - Wrocław,EUR,Breakfast,-6.6 +Poland - Wrocław,EUR,Lunch,-13.2 +Poland - Wrocław,EUR,Dinner,-13.2 +Poland - for the rest,EUR,Full Day,29 +Poland - for the rest,EUR,Part Day,20 +Poland - for the rest,EUR,Overnight,60 +Poland - for the rest,EUR,Breakfast,-5.8 +Poland - for the rest,EUR,Lunch,-11.6 +Poland - for the rest,EUR,Dinner,-11.6 +Portugal,EUR,Full Day,36 +Portugal,EUR,Part Day,24 +Portugal,EUR,Overnight,102 +Portugal,EUR,Breakfast,-7.2 +Portugal,EUR,Lunch,-14.4 +Portugal,EUR,Dinner,-14.4 +Qatar,EUR,Full Day,56 +Qatar,EUR,Part Day,37 +Qatar,EUR,Overnight,170 +Qatar,EUR,Breakfast,-11.2 +Qatar,EUR,Lunch,-22.4 +Qatar,EUR,Dinner,-22.4 +Romania - Bucharest,EUR,Full Day,32 +Romania - Bucharest,EUR,Part Day,21 +Romania - Bucharest,EUR,Overnight,100 +Romania - Bucharest,EUR,Breakfast,-6.4 +Romania - Bucharest,EUR,Lunch,-12.8 +Romania - Bucharest,EUR,Dinner,-12.8 +Romania - for the rest,EUR,Full Day,26 +Romania - for the rest,EUR,Part Day,17 +Romania - for the rest,EUR,Overnight,62 +Romania - for the rest,EUR,Breakfast,-5.2 +Romania - for the rest,EUR,Lunch,-10.4 +Romania - for the rest,EUR,Dinner,-10.4 +Russia - Moscow,EUR,Full Day,30 +Russia - Moscow,EUR,Part Day,20 +Russia - Moscow,EUR,Overnight,110 +Russia - Moscow,EUR,Breakfast,-6 +Russia - Moscow,EUR,Lunch,-12 +Russia - Moscow,EUR,Dinner,-12 +Russia - St. Petersburg,EUR,Full Day,26 +Russia - St. Petersburg,EUR,Part Day,17 +Russia - St. Petersburg,EUR,Overnight,114 +Russia - St. Petersburg,EUR,Breakfast,-5.2 +Russia - St. Petersburg,EUR,Lunch,-10.4 +Russia - St. Petersburg,EUR,Dinner,-10.4 +Russia - Yekaterinburg,EUR,Full Day,28 +Russia - Yekaterinburg,EUR,Part Day,19 +Russia - Yekaterinburg,EUR,Overnight,84 +Russia - Yekaterinburg,EUR,Breakfast,-5.6 +Russia - Yekaterinburg,EUR,Lunch,-11.2 +Russia - Yekaterinburg,EUR,Dinner,-11.2 +Russia - for the rest,EUR,Full Day,24 +Russia - for the rest,EUR,Part Day,16 +Russia - for the rest,EUR,Overnight,58 +Russia - for the rest,EUR,Breakfast,-4.8 +Russia - for the rest,EUR,Lunch,-9.6 +Russia - for the rest,EUR,Dinner,-9.6 +Rwanda,EUR,Full Day,46 +Rwanda,EUR,Part Day,31 +Rwanda,EUR,Overnight,141 +Rwanda,EUR,Breakfast,-9.2 +Rwanda,EUR,Lunch,-18.4 +Rwanda,EUR,Dinner,-18.4 +Samoa,EUR,Full Day,29 +Samoa,EUR,Part Day,20 +Samoa,EUR,Overnight,85 +Samoa,EUR,Breakfast,-5.8 +Samoa,EUR,Lunch,-11.6 +Samoa,EUR,Dinner,-11.6 +San Marino,EUR,Full Day,34 +San Marino,EUR,Part Day,23 +San Marino,EUR,Overnight,75 +San Marino,EUR,Breakfast,-6.8 +San Marino,EUR,Lunch,-13.6 +San Marino,EUR,Dinner,-13.6 +São Tomé and Príncipe,EUR,Full Day,47 +São Tomé and Príncipe,EUR,Part Day,32 +São Tomé and Príncipe,EUR,Overnight,80 +São Tomé and Príncipe,EUR,Breakfast,-9.4 +São Tomé and Príncipe,EUR,Lunch,-18.8 +São Tomé and Príncipe,EUR,Dinner,-18.8 +Saudi Arabia - Jeddah,EUR,Full Day,38 +Saudi Arabia - Jeddah,EUR,Part Day,25 +Saudi Arabia - Jeddah,EUR,Overnight,234 +Saudi Arabia - Jeddah,EUR,Breakfast,-7.6 +Saudi Arabia - Jeddah,EUR,Lunch,-15.2 +Saudi Arabia - Jeddah,EUR,Dinner,-15.2 +Saudi Arabia - Riyadh,EUR,Full Day,48 +Saudi Arabia - Riyadh,EUR,Part Day,32 +Saudi Arabia - Riyadh,EUR,Overnight,179 +Saudi Arabia - Riyadh,EUR,Breakfast,-9.6 +Saudi Arabia - Riyadh,EUR,Lunch,-19.2 +Saudi Arabia - Riyadh,EUR,Dinner,-19.2 +Saudi Arabia - for the rest,EUR,Full Day,48 +Saudi Arabia - for the rest,EUR,Part Day,32 +Saudi Arabia - for the rest,EUR,Overnight,80 +Saudi Arabia - for the rest,EUR,Breakfast,-9.6 +Saudi Arabia - for the rest,EUR,Lunch,-19.2 +Saudi Arabia - for the rest,EUR,Dinner,-19.2 +Senegal,EUR,Full Day,45 +Senegal,EUR,Part Day,30 +Senegal,EUR,Overnight,128 +Senegal,EUR,Breakfast,-9 +Senegal,EUR,Lunch,-18 +Senegal,EUR,Dinner,-18 +Serbia,EUR,Full Day,20 +Serbia,EUR,Part Day,13 +Serbia,EUR,Overnight,74 +Serbia,EUR,Breakfast,-4 +Serbia,EUR,Lunch,-8 +Serbia,EUR,Dinner,-8 +Sierra Leone,EUR,Full Day,48 +Sierra Leone,EUR,Part Day,32 +Sierra Leone,EUR,Overnight,161 +Sierra Leone,EUR,Breakfast,-9.6 +Sierra Leone,EUR,Lunch,-19.2 +Sierra Leone,EUR,Dinner,-19.2 +Singapore,EUR,Full Day,54 +Singapore,EUR,Part Day,36 +Singapore,EUR,Overnight,197 +Singapore,EUR,Breakfast,-10.8 +Singapore,EUR,Lunch,-21.6 +Singapore,EUR,Dinner,-21.6 +Slovakia,EUR,Full Day,24 +Slovakia,EUR,Part Day,16 +Slovakia,EUR,Overnight,85 +Slovakia,EUR,Breakfast,-4.8 +Slovakia,EUR,Lunch,-9.6 +Slovakia,EUR,Dinner,-9.6 +Slovenia,EUR,Full Day,33 +Slovenia,EUR,Part Day,22 +Slovenia,EUR,Overnight,95 +Slovenia,EUR,Breakfast,-6.6 +Slovenia,EUR,Lunch,-13.2 +Slovenia,EUR,Dinner,-13.2 +South Africa - Cape Town,EUR,Full Day,27 +South Africa - Cape Town,EUR,Part Day,18 +South Africa - Cape Town,EUR,Overnight,112 +South Africa - Cape Town,EUR,Breakfast,-5.4 +South Africa - Cape Town,EUR,Lunch,-10.8 +South Africa - Cape Town,EUR,Dinner,-10.8 +South Africa - Johannesburg,EUR,Full Day,29 +South Africa - Johannesburg,EUR,Part Day,20 +South Africa - Johannesburg,EUR,Overnight,124 +South Africa - Johannesburg,EUR,Breakfast,-5.8 +South Africa - Johannesburg,EUR,Lunch,-11.6 +South Africa - Johannesburg,EUR,Dinner,-11.6 +Souh Africa - for the rest,EUR,Full Day,22 +Souh Africa - for the rest,EUR,Part Day,15 +Souh Africa - for the rest,EUR,Overnight,94 +Souh Africa - for the rest,EUR,Breakfast,-4.4 +Souh Africa - for the rest,EUR,Lunch,-8.8 +Souh Africa - for the rest,EUR,Dinner,-8.8 +South Sudan,EUR,Full Day,34 +South Sudan,EUR,Part Day,23 +South Sudan,EUR,Overnight,150 +South Sudan,EUR,Breakfast,-6.8 +South Sudan,EUR,Lunch,-13.6 +South Sudan,EUR,Dinner,-13.6 +Spain - Barcelona,EUR,Full Day,34 +Spain - Barcelona,EUR,Part Day,23 +Spain - Barcelona,EUR,Overnight,118 +Spain - Barcelona,EUR,Breakfast,-6.8 +Spain - Barcelona,EUR,Lunch,-13.6 +Spain - Barcelona,EUR,Dinner,-13.6 +Spain - Canary Islands,EUR,Full Day,40 +Spain - Canary Islands,EUR,Part Day,27 +Spain - Canary Islands,EUR,Overnight,115 +Spain - Canary Islands,EUR,Breakfast,-8 +Spain - Canary Islands,EUR,Lunch,-16 +Spain - Canary Islands,EUR,Dinner,-16 +Spain - Madrid,EUR,Full Day,40 +Spain - Madrid,EUR,Part Day,27 +Spain - Madrid,EUR,Overnight,118 +Spain - Madrid,EUR,Breakfast,-8 +Spain - Madrid,EUR,Lunch,-16 +Spain - Madrid,EUR,Dinner,-16 +Spain - Palma de Mallorca,EUR,Full Day,35 +Spain - Palma de Mallorca,EUR,Part Day,24 +Spain - Palma de Mallorca,EUR,Overnight,121 +Spain - Palma de Mallorca,EUR,Breakfast,-7 +Spain - Palma de Mallorca,EUR,Lunch,-14 +Spain - Palma de Mallorca,EUR,Dinner,-14 +Spain - for the rest,EUR,Full Day,34 +Spain - for the rest,EUR,Part Day,23 +Spain - for the rest,EUR,Overnight,115 +Spain - for the rest,EUR,Breakfast,-6.8 +Spain - for the rest,EUR,Lunch,-13.6 +Spain - for the rest,EUR,Dinner,-13.6 +Sri Lanka,EUR,Full Day,42 +Sri Lanka,EUR,Part Day,28 +Sri Lanka,EUR,Overnight,100 +Sri Lanka,EUR,Breakfast,-8.4 +Sri Lanka,EUR,Lunch,-16.8 +Sri Lanka,EUR,Dinner,-16.8 +St. Kitts and Nevis,EUR,Full Day,45 +St. Kitts and Nevis,EUR,Part Day,30 +St. Kitts and Nevis,EUR,Overnight,177 +St. Kitts and Nevis,EUR,Breakfast,-9 +St. Kitts and Nevis,EUR,Lunch,-18 +St. Kitts and Nevis,EUR,Dinner,-18 +St. Lucia,EUR,Full Day,45 +St. Lucia,EUR,Part Day,30 +St. Lucia,EUR,Overnight,177 +St. Lucia,EUR,Breakfast,-9 +St. Lucia,EUR,Lunch,-18 +St. Lucia,EUR,Dinner,-18 +St. Vincent and the Grenadines,EUR,Full Day,45 +St. Vincent and the Grenadines,EUR,Part Day,30 +St. Vincent and the Grenadines,EUR,Overnight,177 +St. Vincent and the Grenadines,EUR,Breakfast,-9 +St. Vincent and the Grenadines,EUR,Lunch,-18 +St. Vincent and the Grenadines,EUR,Dinner,-18 +Sudan,EUR,Full Day,35 +Sudan,EUR,Part Day,24 +Sudan,EUR,Overnight,115 +Sudan,EUR,Breakfast,-7 +Sudan,EUR,Lunch,-14 +Sudan,EUR,Dinner,-14 +Suriname,EUR,Full Day,45 +Suriname,EUR,Part Day,30 +Suriname,EUR,Overnight,177 +Suriname,EUR,Breakfast,-9 +Suriname,EUR,Lunch,-18 +Suriname,EUR,Dinner,-18 +Sweden,EUR,Full Day,50 +Sweden,EUR,Part Day,33 +Sweden,EUR,Overnight,168 +Sweden,EUR,Breakfast,-10 +Sweden,EUR,Lunch,-20 +Sweden,EUR,Dinner,-20 +Switzerland - Geneva,EUR,Full Day,64 +Switzerland - Geneva,EUR,Part Day,43 +Switzerland - Geneva,EUR,Overnight,195 +Switzerland - Geneva,EUR,Breakfast,-12.8 +Switzerland - Geneva,EUR,Lunch,-25.6 +Switzerland - Geneva,EUR,Dinner,-25.6 +Switzerland - for the rest,EUR,Full Day,62 +Switzerland - for the rest,EUR,Part Day,41 +Switzerland - for the rest,EUR,Overnight,169 +Switzerland - for the rest,EUR,Breakfast,-12.4 +Switzerland - for the rest,EUR,Lunch,-24.8 +Switzerland - for the rest,EUR,Dinner,-24.8 +Syria,EUR,Full Day,38 +Syria,EUR,Part Day,25 +Syria,EUR,Overnight,140 +Syria,EUR,Breakfast,-7.6 +Syria,EUR,Lunch,-15.2 +Syria,EUR,Dinner,-15.2 +Taiwan,EUR,Full Day,51 +Taiwan,EUR,Part Day,34 +Taiwan,EUR,Overnight,126 +Taiwan,EUR,Breakfast,-10.2 +Taiwan,EUR,Lunch,-20.4 +Taiwan,EUR,Dinner,-20.4 +Tajikistan,EUR,Full Day,27 +Tajikistan,EUR,Part Day,18 +Tajikistan,EUR,Overnight,118 +Tajikistan,EUR,Breakfast,-5.4 +Tajikistan,EUR,Lunch,-10.8 +Tajikistan,EUR,Dinner,-10.8 +Tanzania,EUR,Full Day,47 +Tanzania,EUR,Part Day,32 +Tanzania,EUR,Overnight,201 +Tanzania,EUR,Breakfast,-9.4 +Tanzania,EUR,Lunch,-18.8 +Tanzania,EUR,Dinner,-18.8 +Thailand,EUR,Full Day,38 +Thailand,EUR,Part Day,25 +Thailand,EUR,Overnight,110 +Thailand,EUR,Breakfast,-7.6 +Thailand,EUR,Lunch,-15.2 +Thailand,EUR,Dinner,-15.2 +Togo,EUR,Full Day,35 +Togo,EUR,Part Day,24 +Togo,EUR,Overnight,108 +Togo,EUR,Breakfast,-7 +Togo,EUR,Lunch,-14 +Togo,EUR,Dinner,-14 +Tonga,EUR,Full Day,39 +Tonga,EUR,Part Day,26 +Tonga,EUR,Overnight,94 +Tonga,EUR,Breakfast,-7.8 +Tonga,EUR,Lunch,-15.6 +Tonga,EUR,Dinner,-15.6 +Trinidad and Tobago,EUR,Full Day,45 +Trinidad and Tobago,EUR,Part Day,30 +Trinidad and Tobago,EUR,Overnight,177 +Trinidad and Tobago,EUR,Breakfast,-9 +Trinidad and Tobago,EUR,Lunch,-18 +Trinidad and Tobago,EUR,Dinner,-18 +Tunisia,EUR,Full Day,40 +Tunisia,EUR,Part Day,27 +Tunisia,EUR,Overnight,115 +Tunisia,EUR,Breakfast,-8 +Tunisia,EUR,Lunch,-16 +Tunisia,EUR,Dinner,-16 +Turkey - Istanbul,EUR,Full Day,35 +Turkey - Istanbul,EUR,Part Day,24 +Turkey - Istanbul,EUR,Overnight,104 +Turkey - Istanbul,EUR,Breakfast,-7 +Turkey - Istanbul,EUR,Lunch,-14 +Turkey - Istanbul,EUR,Dinner,-14 +Turkey - Izmir,EUR,Full Day,42 +Turkey - Izmir,EUR,Part Day,28 +Turkey - Izmir,EUR,Overnight,80 +Turkey - Izmir,EUR,Breakfast,-8.4 +Turkey - Izmir,EUR,Lunch,-16.8 +Turkey - Izmir,EUR,Dinner,-16.8 +Turkey - for the rest,EUR,Full Day,40 +Turkey - for the rest,EUR,Part Day,27 +Turkey - for the rest,EUR,Overnight,78 +Turkey - for the rest,EUR,Breakfast,-8 +Turkey - for the rest,EUR,Lunch,-16 +Turkey - for the rest,EUR,Dinner,-16 +Turkmenistan,EUR,Full Day,33 +Turkmenistan,EUR,Part Day,22 +Turkmenistan,EUR,Overnight,108 +Turkmenistan,EUR,Breakfast,-6.6 +Turkmenistan,EUR,Lunch,-13.2 +Turkmenistan,EUR,Dinner,-13.2 +Uganda,EUR,Full Day,35 +Uganda,EUR,Part Day,24 +Uganda,EUR,Overnight,129 +Uganda,EUR,Breakfast,-7 +Uganda,EUR,Lunch,-14 +Uganda,EUR,Dinner,-14 +Ukraine,EUR,Full Day,32 +Ukraine,EUR,Part Day,21 +Ukraine,EUR,Overnight,98 +Ukraine,EUR,Breakfast,-6.4 +Ukraine,EUR,Lunch,-12.8 +Ukraine,EUR,Dinner,-12.8 +United Arab Emirates,EUR,Full Day,45 +United Arab Emirates,EUR,Part Day,30 +United Arab Emirates,EUR,Overnight,155 +United Arab Emirates,EUR,Breakfast,-9 +United Arab Emirates,EUR,Lunch,-18 +United Arab Emirates,EUR,Dinner,-18 +United Kingdom of Great Britain and Ireland,EUR,Full Day, +United Kingdom of Great Britain and Ireland,EUR,Part Day, +United Kingdom of Great Britain and Ireland,EUR,Overnight, +United Kingdom of Great Britain and Ireland,EUR,Breakfast,0 +United Kingdom of Great Britain and Ireland,EUR,Lunch,0 +United Kingdom of Great Britain and Ireland,EUR,Dinner,0 +United Kingdom - London,EUR,Full Day,62 +United Kingdom - London,EUR,Part Day,41 +United Kingdom - London,EUR,Overnight,224 +United Kingdom - London,EUR,Breakfast,-12.4 +United Kingdom - London,EUR,Lunch,-24.8 +United Kingdom - London,EUR,Dinner,-24.8 +United Kingdom - for the rest,EUR,Full Day,45 +United Kingdom - for the rest,EUR,Part Day,30 +United Kingdom - for the rest,EUR,Overnight,115 +United Kingdom - for the rest,EUR,Breakfast,-9 +United Kingdom - for the rest,EUR,Lunch,-18 +United Kingdom - for the rest,EUR,Dinner,-18 +United States of America - Atlanta,EUR,Full Day,62 +United States of America - Atlanta,EUR,Part Day,41 +United States of America - Atlanta,EUR,Overnight,175 +United States of America - Atlanta,EUR,Breakfast,-12.4 +United States of America - Atlanta,EUR,Lunch,-24.8 +United States of America - Atlanta,EUR,Dinner,-24.8 +United States of America - Boston,EUR,Full Day,58 +United States of America - Boston,EUR,Part Day,39 +United States of America - Boston,EUR,Overnight,265 +United States of America - Boston,EUR,Breakfast,-11.6 +United States of America - Boston,EUR,Lunch,-23.2 +United States of America - Boston,EUR,Dinner,-23.2 +United States of America - Chicago,EUR,Full Day,54 +United States of America - Chicago,EUR,Part Day,36 +United States of America - Chicago,EUR,Overnight,209 +United States of America - Chicago,EUR,Breakfast,-10.8 +United States of America - Chicago,EUR,Lunch,-21.6 +United States of America - Chicago,EUR,Dinner,-21.6 +United States of America - Houston,EUR,Full Day,63 +United States of America - Houston,EUR,Part Day,42 +United States of America - Houston,EUR,Overnight,138 +United States of America - Houston,EUR,Breakfast,-12.6 +United States of America - Houston,EUR,Lunch,-25.2 +United States of America - Houston,EUR,Dinner,-25.2 +United States of America - Los Angeles,EUR,Full Day,56 +United States of America - Los Angeles,EUR,Part Day,37 +United States of America - Los Angeles,EUR,Overnight,274 +United States of America - Los Angeles,EUR,Breakfast,-11.2 +United States of America - Los Angeles,EUR,Lunch,-22.4 +United States of America - Los Angeles,EUR,Dinner,-22.4 +United States of America - Miami,EUR,Full Day,64 +United States of America - Miami,EUR,Part Day,43 +United States of America - Miami,EUR,Overnight,151 +United States of America - Miami,EUR,Breakfast,-12.8 +United States of America - Miami,EUR,Lunch,-25.6 +United States of America - Miami,EUR,Dinner,-25.6 +United States of America - New York,EUR,Full Day,58 +United States of America - New York,EUR,Part Day,39 +United States of America - New York,EUR,Overnight,282 +United States of America - New York,EUR,Breakfast,-11.6 +United States of America - New York,EUR,Lunch,-23.2 +United States of America - New York,EUR,Dinner,-23.2 +United States of America - San Francisco,EUR,Full Day,51 +United States of America - San Francisco,EUR,Part Day,34 +United States of America - San Francisco,EUR,Overnight,314 +United States of America - San Francisco,EUR,Breakfast,-10.2 +United States of America - San Francisco,EUR,Lunch,-20.4 +United States of America - San Francisco,EUR,Dinner,-20.4 +United States of America - Washington D.C.,EUR,Full Day,62 +United States of America - Washington D.C.,EUR,Part Day,41 +United States of America - Washington D.C.,EUR,Overnight,276 +United States of America - Washington D.C.,EUR,Breakfast,-12.4 +United States of America - Washington D.C.,EUR,Lunch,-24.8 +United States of America - Washington D.C.,EUR,Dinner,-24.8 +United States of America - for the rest,EUR,Full Day,51 +United States of America - for the rest,EUR,Part Day,34 +United States of America - for the rest,EUR,Overnight,138 +United States of America - for the rest,EUR,Breakfast,-10.2 +United States of America - for the rest,EUR,Lunch,-20.4 +United States of America - for the rest,EUR,Dinner,-20.4 +Uruguay,EUR,Full Day,44 +Uruguay,EUR,Part Day,29 +Uruguay,EUR,Overnight,109 +Uruguay,EUR,Breakfast,-8.8 +Uruguay,EUR,Lunch,-17.6 +Uruguay,EUR,Dinner,-17.6 +Uzbekistan,EUR,Full Day,34 +Uzbekistan,EUR,Part Day,23 +Uzbekistan,EUR,Overnight,123 +Uzbekistan,EUR,Breakfast,-6.8 +Uzbekistan,EUR,Lunch,-13.6 +Uzbekistan,EUR,Dinner,-13.6 +Vatican City,EUR,Full Day,52 +Vatican City,EUR,Part Day,35 +Vatican City,EUR,Overnight,160 +Vatican City,EUR,Breakfast,-10.4 +Vatican City,EUR,Lunch,-20.8 +Vatican City,EUR,Dinner,-20.8 +Venezuela,EUR,Full Day,69 +Venezuela,EUR,Part Day,46 +Venezuela,EUR,Overnight,127 +Venezuela,EUR,Breakfast,-13.8 +Venezuela,EUR,Lunch,-27.6 +Venezuela,EUR,Dinner,-27.6 +Vietnam,EUR,Full Day,41 +Vietnam,EUR,Part Day,28 +Vietnam,EUR,Overnight,86 +Vietnam,EUR,Breakfast,-8.2 +Vietnam,EUR,Lunch,-16.4 +Vietnam,EUR,Dinner,-16.4 +Yemen,EUR,Full Day,24 +Yemen,EUR,Part Day,16 +Yemen,EUR,Overnight,95 +Yemen,EUR,Breakfast,-4.8 +Yemen,EUR,Lunch,-9.6 +Yemen,EUR,Dinner,-9.6 +Zambia,EUR,Full Day,36 +Zambia,EUR,Part Day,24 +Zambia,EUR,Overnight,130 +Zambia,EUR,Breakfast,-7.2 +Zambia,EUR,Lunch,-14.4 +Zambia,EUR,Dinner,-14.4 +Zimbabwe,EUR,Full Day,45 +Zimbabwe,EUR,Part Day,30 +Zimbabwe,EUR,Overnight,140 +Zimbabwe,EUR,Breakfast,-9 +Zimbabwe,EUR,Lunch,-18 +Zimbabwe,EUR,Dinner,-18 \ No newline at end of file diff --git a/docs/assets/Files/South-Africa-per-diem.csv b/docs/assets/Files/South-Africa-per-diem.csv new file mode 100644 index 000000000000..d8ed328edec6 --- /dev/null +++ b/docs/assets/Files/South-Africa-per-diem.csv @@ -0,0 +1,186 @@ +rateID,Destination,Subrate,Amount,Currency +5e6618c88c782,Albania,International,100,EUR +5e6618c88c788,Algeria,International,110,EUR +5e6618c88c78a,Angola,International,303,USD +5e6618c88c78b,Antigua and Barbuda,International,220,USD +5e6618c88c78d,Argentina,International,133,USD +5e6618c88c78e,Armenia,International,220,USD +5e6618c88c78f,Australia,International,230,AUD +5e6618c88c790,Austria,International,131,EUR +5e6618c88c792,Bahamas,International,191,USD +5e6618c88c793,Bahrain,International,36,BHD +5e6618c88c794,Bangladesh,International,79,USD +5e6618c88c795,Barbados,International,202,USD +5e6618c88c796,Belarus,International,62,EUR +5e6618c88c798,Belgium,International,146,EUR +5e6618c88c799,Belize,International,152,USD +5e6618c88c79a,Benin,International,111,EUR +5e6618c88c79b,Bolivia,International,78,USD +5e6618c88c79c,Bosnia-Herzegovina,International,75,EUR +5e6618c88c79d,Botswana,International,826,BWP +5e6618c88c79e,Brazil,International,409,BRL +5e6618c88c79f,Brunei,International,88,USD +5e6618c88c7a0,Bulgaria,International,91,EUR +5e6618c88c7a1,Burkina Faso,International,"58,790.00",XAF +5e6618c88c7a3,Burundi,International,73,EUR +5e6618c88c7a4,Cambodia,International,99,USD +5e6618c88c7a5,Cameroon,International,120,EUR +5e6618c88c7a6,Cape Verde Islands,International,65,EUR +5e6618c88c7a7,Central African Republic,International,94,EUR +5e6618c88c7a9,Chad,International,121,EUR +5e6618c88c7aa,Chile,International,106,USD +5e6618c88c7af,China (People's Republic),International,127,USD +5e6618c88c7b0,Colombia,International,94,USD +5e6618c88c7b1,Comoro Island,International,122,EUR +5e6618c88c7b2,Cook Islands,International,211,NZD +5e6618c88c7b3,Costa Rica,International,116,USD +5e6618c88c7b4,Cote D'Ivoire,International,119,EUR +5e6618c88c7b6,Croatia,International,99,EUR +5e6618c88c7b7,Cuba,International,114,USD +5e6618c88c7b8,Cyprus,International,117,EUR +5e6618c88c7b9,Czech Republic,International,90,EUR +5e6618c88c7ba,Democratic Republic of Congo,International,164,USD +5e6618c88c7bb,Denmark,International,"2,328.00",DKK +5e6618c88c7bc,Djibouti,International,99,USD +5e6618c88c7bd,Dominican Republic,International,99,USD +5e6618c88c7be,Ecuador,International,163,USD +5e6618c88c7bf,Egypt,International,873,USD +5e6618c88c7c0,El Salvador,International,98,USD +5e6618c88c7c1,Equatorial Guinea,International,166,EUR +5e6618c88c7c2,Eritrea,International,109,USD +5e6618c88c7c3,Estonia,International,92,EUR +5e6618c88c7c4,Ethiopia,International,95,USD +5e6618c88c7c5,Fiji,International,102,USD +5e6618c88c7c6,Finland,International,171,EUR +5e6618c88c7c7,France,International,129,EUR +5e6618c88c7c8,Gabon,International,160,EUR +5e6618c88c7c9,Gambia,International,74,EUR +5e6618c88c7ca,Georgia,International,95,USD +5e6618c88c7cb,Germany,International,125,EUR +5e6618c88c7cc,Ghana,International,130,USD +5e6618c88c7cd,Greece,International,138,EUR +5e6618c88c7ce,Grenada,International,151,USD +5e6618c88c7cf,Guatemala,International,114,USD +5e6618c88c7d0,Guinea,International,78,EUR +5e6618c88c7d1,Guinea Bissau,International,59,EUR +5e6618c88c7d2,Guyana,International,118,USD +5e6618c88c7d3,Haiti,International,109,USD +5e6618c88c7d4,Honduras,International,186,USD +5e6618c88c7d5,Hong Kong,International,"1,000.00",HKD +5e6618c88c7d6,Hungary,International,92,EUR +5e6618c88c7d7,Iceland,International,"25,466.00",ISK +5e6618c88c7d8,India,International,"5,852.00",INR +5e6618c88c7d9,Indonesia,International,86,USD +5e6618c88c7da,Iran,International,120,USD +5e6618c88c7db,Iraq,International,125,USD +5e6618c88c7dc,Ireland,International,139,EUR +5e6618c88c7dd,Israel,International,209,USD +5e6618c88c7e1,Italy,International,125,EUR +5e6618c88c7e3,Jamaica,International,151,USD +5e6618c88c7e4,Japan,International,"16,275.00",JPY +5e6618c88c7e5,Jordan,International,201,USD +5e6618c88c7e6,Kazakhstan,International,100,USD +5e6618c88c7e7,Kenya,International,138,USD +5e6618c88c7e8,Kiribati,International,233,AUD +5e6618c88c7e9,Kuwait (State of),International,51,KWD +5e6618c88c7ea,Kyrgyzstan,International,172,USD +5e6618c88c7eb,Laos,International,92,USD +5e6618c88c7ec,Latvia,International,150,USD +5e6618c88c7ed,Lebanon,International,158,USD +5e6618c88c7ee,Lesotho,International,750,ZAR +5e6618c88c7ef,Liberia,International,112,USD +5e6618c88c7f0,Libya,International,120,USD +5e6618c88c7f1,Lithuania,International,154,EUR +5e6618c88c7f2,Macedonia (Former Yugoslav),International,100,EUR +5e6618c88c7f3,Madagascar,International,58,EUR +5e6618c88c7f4,Malawi,International,"31,254.00",MWK +5e6618c88c7f5,Malaysia,International,382,MYR +5e6618c88c7f6,Maldives,International,202,USD +5e6618c88c7f7,Mali,International,178,EUR +5e6618c88c7f8,Malta,International,132,EUR +5e6618c88c7f9,Marshall Islands,International,255,USD +5e6618c88c7fa,Mauritania,International,97,EUR +5e6618c88c7fb,Mauritius,International,114,USD +5e6618c88c7fc,Mexico,International,"1,313.00",MXN +5e6618c88c7fd,Moldova,International,117,USD +5e6618c88c7fe,Mongolia,International,69,USD +5e6618c88c7ff,Montenegro,International,94,EUR +5e6618c88c800,Morocco,International,"1,081.00",AED +5e6618c88c801,Mozambique,International,101,USD +5e6618c88c802,Myanmar,International,123,USD +5e6618c88c803,Namibia,International,950,ZAR +5e6618c88c804,Nauru,International,278,AUD +5e6618c88c805,Nepal,International,64,USD +5e6618c88c806,Netherlands,International,122,EUR +5e6618c88c807,New Zealand,International,206,NZD +5e6618c88c808,Nicaragua,International,90,USD +5e6618c88c809,Niger,International,75,EUR +5e6618c88c80a,Nigeria,International,242,USD +5e6618c88c80b,Niue,International,252,NZD +5e6618c88c80c,Norway,International,"1,760.00",NOK +5e6618c88c80d,Oman,International,77,OMR +5e6618c88c80e,Pakistan,International,"6,235.00",PKR +5e6618c88c80f,Palau,International,252,USD +5e6618c88c810,Palestine,International,147,USD +5e6618c88c811,Panama,International,105,USD +5e6618c88c812,Papa New Guinea,International,285,PGK +5e6618c88c813,Paraguay,International,76,USD +5e6618c88c815,Peru,International,139,USD +5e6618c88c816,Philippines,International,122,USD +5e6618c88c817,Poland,International,88,EUR +5e6618c88c818,Portugal,International,87,EUR +5e6618c88c819,Puerto Rico,International,215,USD +5e6618c88c81a,Qatar,International,715,QAR +5e6618c88c81b,Republic of Congo,International,149,EUR +5e6618c88c81c,Reunion,International,164,EUR +5e6618c88c81d,Romania,International,83,EUR +5e6618c88c81e,Russia,International,330,EUR +5e6618c88c81f,Rwanda,International,102,USD +5e6618c88c823,Samoa,International,193,WST +5e6618c88c825,Sao Tome & Principe,International,160,EUR +5e6618c88c826,Saudi Arabia,International,512,SAR +5e6618c88c827,Senegal,International,113,EUR +5e6618c88c829,Serbia,International,83,EUR +5e6618c88c82a,Seychelles,International,132,EUR +5e6618c88c82c,Sierra Leone,International,90,USD +5e6618c88c83b,Slovakia,International,102,EUR +5e6618c88c83c,Slovenia,International,106,EUR +5e6618c88c83d,Solomon Islands,International,"1,107.00",SBD +5e6618c88c83e,South Korea Republic,International,"187,735.00",KRW +5e6618c88c840,South Sudan,International,146,USD +5e6618c88c841,Spain,International,112,EUR +5e6618c88c842,Sri Lanka,International,100,USD +5e6618c88c843,St. Kitts & Nevis,International,227,USD +5e6618c88c844,St. Lucia,International,215,USD +5e6618c88c845,St. Vincent & The Grenadines,International,187,USD +5e6618c88c846,Sudan,International,200,USD +5e6618c88c847,Suriname,International,107,USD +5e6618c88c848,Swaziland,International,"1,367.00",ZAR +5e6618c88c849,Sweden,International,"1,317.00",SEK +5e6618c88c84a,Switzerland,International,201,CHF +5e6618c88c84b,Syria,International,185,USD +5e6618c88c84c,Taiwan,International,"3,505.00",TWD +5e6618c88c84d,Tajikistan,International,97,USD +5e6618c88c84e,Tanzania,International,129,USD +5e6618c88c84f,Togo,International,"64,214.00",XAF +5e6618c88c850,Tonga,International,251,TOP +5e6618c88c851,Trinidad & Tobago,International,213,USD +5e6618c88c852,Tunisia,International,198,TND +5e6618c88c853,Turkey,International,101,EUR +5e6618c88c854,Turkmenistan,International,125,USD +5e6618c88c855,Tuvalu,International,339,AUD +5e6618c88c856,USA,International,215,USD +5e6618c88c857,Uganda,International,111,USD +5e6618c88c858,Ukraine,International,131,EUR +5e6618c88c859,Unit Kingdom - Fiber Lean,International,40,GBP +5e6618c88c85a,United Arab Emirates,International,699,AED +5e6618c88c85e,United Kingdom,International,102,GBP +5e6618c88c860,Uruguay,International,133,USD +5e6618c88c861,Uzbekistan,International,80,EUR +5e6618c88c863,Vanuatu,International,166,USD +5e6618c88c864,Venezuela,International,294,USD +5e6618c88c865,Vietnam,International,91,USD +5e6618c88c866,Yemen,International,94,USD +5e6618c88c867,Zambia,International,119,USD +5e6618c88c868,Zimbabwe,International,123,USD +5e6618c88c8a4,South Africa,Local,452,ZAR \ No newline at end of file diff --git a/docs/assets/Files/Sweden-per-diem.csv b/docs/assets/Files/Sweden-per-diem.csv new file mode 100644 index 000000000000..d20c0ac8e991 --- /dev/null +++ b/docs/assets/Files/Sweden-per-diem.csv @@ -0,0 +1,2037 @@ +Destination,Currency,Subrate,Amount +Albanien (2018),SEK,Normalbelopp,230 +Albanien (2018),SEK,Breakfast,-34.5 +Albanien (2018),SEK,Lunch,-80.5 +Albanien (2018),SEK,Dinner,-80.5 +Albanien (2019),SEK,Normalbelopp,242 +Albanien (2019),SEK,Breakfast,-36.3 +Albanien (2019),SEK,Lunch,-84.7 +Albanien (2019),SEK,Dinner,-84.7 +Albanien (2020),SEK,Normalbelopp,254 +Albanien (2020),SEK,Breakfast,-38.1 +Albanien (2020),SEK,Lunch,-88.9 +Albanien (2020),SEK,Dinner,-88.9 +Algeriet (2018),SEK,Normalbelopp,335 +Algeriet (2018),SEK,Breakfast,-50.25 +Algeriet (2018),SEK,Lunch,-117.25 +Algeriet (2018),SEK,Dinner,-117.25 +Algeriet (2019),SEK,Normalbelopp,362 +Algeriet (2019),SEK,Breakfast,-54.3 +Algeriet (2019),SEK,Lunch,-126.7 +Algeriet (2019),SEK,Dinner,-126.7 +Algeriet (2020),SEK,Normalbelopp,378 +Algeriet (2020),SEK,Breakfast,-56.7 +Algeriet (2020),SEK,Lunch,-132.3 +Algeriet (2020),SEK,Dinner,-132.3 +Angola (2018),SEK,Normalbelopp,937 +Angola (2018),SEK,Breakfast,-140.55 +Angola (2018),SEK,Lunch,-327.95 +Angola (2018),SEK,Dinner,-327.95 +Angola (2019),SEK,Normalbelopp,567 +Angola (2019),SEK,Breakfast,-85.05 +Angola (2019),SEK,Lunch,-198.45 +Angola (2019),SEK,Dinner,-198.45 +Angola (2020),SEK,Normalbelopp,346 +Angola (2020),SEK,Breakfast,-51.9 +Angola (2020),SEK,Lunch,-121.1 +Angola (2020),SEK,Dinner,-121.1 +Antigua och Barbuda (2018),SEK,Normalbelopp,613 +Antigua och Barbuda (2018),SEK,Breakfast,-91.95 +Antigua och Barbuda (2018),SEK,Lunch,-214.55 +Antigua och Barbuda (2018),SEK,Dinner,-214.55 +Antigua och Barbuda (2019),SEK,Normalbelopp,670 +Antigua och Barbuda (2019),SEK,Breakfast,-100.5 +Antigua och Barbuda (2019),SEK,Lunch,-234.5 +Antigua och Barbuda (2019),SEK,Dinner,-234.5 +Antigua och Barbuda (2020),SEK,Normalbelopp,697 +Antigua och Barbuda (2020),SEK,Breakfast,-104.55 +Antigua och Barbuda (2020),SEK,Lunch,-243.95 +Antigua och Barbuda (2020),SEK,Dinner,-243.95 +Argentina (2018),SEK,Normalbelopp,454 +Argentina (2018),SEK,Breakfast,-68.1 +Argentina (2018),SEK,Lunch,-158.9 +Argentina (2018),SEK,Dinner,-158.9 +Argentina (2019),SEK,Normalbelopp,276 +Argentina (2019),SEK,Breakfast,-41.4 +Argentina (2019),SEK,Lunch,-96.6 +Argentina (2019),SEK,Dinner,-96.6 +Argentina (2020),SEK,Normalbelopp,240 +Argentina (2020),SEK,Breakfast,-36 +Argentina (2020),SEK,Lunch,-84 +Argentina (2020),SEK,Dinner,-84 +Armenien (2018),SEK,Normalbelopp,313 +Armenien (2018),SEK,Breakfast,-46.95 +Armenien (2018),SEK,Lunch,-109.55 +Armenien (2018),SEK,Dinner,-109.55 +Armenien (2019),SEK,Normalbelopp,329 +Armenien (2019),SEK,Breakfast,-49.35 +Armenien (2019),SEK,Lunch,-115.15 +Armenien (2019),SEK,Dinner,-115.15 +Armenien (2020),SEK,Normalbelopp,358 +Armenien (2020),SEK,Breakfast,-53.7 +Armenien (2020),SEK,Lunch,-125.3 +Armenien (2020),SEK,Dinner,-125.3 +Australien (2018),SEK,Normalbelopp,683 +Australien (2018),SEK,Breakfast,-102.45 +Australien (2018),SEK,Lunch,-239.05 +Australien (2018),SEK,Dinner,-239.05 +Australien (2019),SEK,Normalbelopp,685 +Australien (2019),SEK,Breakfast,-102.75 +Australien (2019),SEK,Lunch,-239.75 +Australien (2019),SEK,Dinner,-239.75 +Australien (2020),SEK,Normalbelopp,697 +Australien (2020),SEK,Breakfast,-104.55 +Australien (2020),SEK,Lunch,-243.95 +Australien (2020),SEK,Dinner,-243.95 +Azerbajdzjan (2018),SEK,Normalbelopp,322 +Azerbajdzjan (2018),SEK,Breakfast,-48.3 +Azerbajdzjan (2018),SEK,Lunch,-112.7 +Azerbajdzjan (2018),SEK,Dinner,-112.7 +Azerbajdzjan (2019),SEK,Normalbelopp,345 +Azerbajdzjan (2019),SEK,Breakfast,-51.75 +Azerbajdzjan (2019),SEK,Lunch,-120.75 +Azerbajdzjan (2019),SEK,Dinner,-120.75 +Azerbajdzjan (2020),SEK,Normalbelopp,359 +Azerbajdzjan (2020),SEK,Breakfast,-53.85 +Azerbajdzjan (2020),SEK,Lunch,-125.65 +Azerbajdzjan (2020),SEK,Dinner,-125.65 +Bahamas (2018),SEK,Normalbelopp,797 +Bahamas (2018),SEK,Breakfast,-119.55 +Bahamas (2018),SEK,Lunch,-278.95 +Bahamas (2018),SEK,Dinner,-278.95 +Bahamas (2019),SEK,Normalbelopp,878 +Bahamas (2019),SEK,Breakfast,-131.7 +Bahamas (2019),SEK,Lunch,-307.3 +Bahamas (2019),SEK,Dinner,-307.3 +Bahamas (2020),SEK,Normalbelopp,953 +Bahamas (2020),SEK,Breakfast,-142.95 +Bahamas (2020),SEK,Lunch,-333.55 +Bahamas (2020),SEK,Dinner,-333.55 +Bahrain (2018),SEK,Normalbelopp,581 +Bahrain (2018),SEK,Breakfast,-87.15 +Bahrain (2018),SEK,Lunch,-203.35 +Bahrain (2018),SEK,Dinner,-203.35 +Bahrain (2019),SEK,Normalbelopp,661 +Bahrain (2019),SEK,Breakfast,-99.15 +Bahrain (2019),SEK,Lunch,-231.35 +Bahrain (2019),SEK,Dinner,-231.35 +Bahrain (2020),SEK,Normalbelopp,696 +Bahrain (2020),SEK,Breakfast,-104.4 +Bahrain (2020),SEK,Lunch,-243.6 +Bahrain (2020),SEK,Dinner,-243.6 +Bangladesh (2018),SEK,Normalbelopp,389 +Bangladesh (2018),SEK,Breakfast,-58.35 +Bangladesh (2018),SEK,Lunch,-136.15 +Bangladesh (2018),SEK,Dinner,-136.15 +Bangladesh (2019),SEK,Normalbelopp,429 +Bangladesh (2019),SEK,Breakfast,-64.35 +Bangladesh (2019),SEK,Lunch,-150.15 +Bangladesh (2019),SEK,Dinner,-150.15 +Bangladesh (2020),SEK,Normalbelopp,457 +Bangladesh (2020),SEK,Breakfast,-68.55 +Bangladesh (2020),SEK,Lunch,-159.95 +Bangladesh (2020),SEK,Dinner,-159.95 +Barbados (2018),SEK,Normalbelopp,738 +Barbados (2018),SEK,Breakfast,-110.7 +Barbados (2018),SEK,Lunch,-258.3 +Barbados (2018),SEK,Dinner,-258.3 +Barbados (2019),SEK,Normalbelopp,818 +Barbados (2019),SEK,Breakfast,-122.7 +Barbados (2019),SEK,Lunch,-286.3 +Barbados (2019),SEK,Dinner,-286.3 +Barbados (2020),SEK,Normalbelopp,893 +Barbados (2020),SEK,Breakfast,-133.95 +Barbados (2020),SEK,Lunch,-312.55 +Barbados (2020),SEK,Dinner,-312.55 +Belgien (2018),SEK,Normalbelopp,678 +Belgien (2018),SEK,Breakfast,-101.7 +Belgien (2018),SEK,Lunch,-237.3 +Belgien (2018),SEK,Dinner,-237.3 +Belgien (2019),SEK,Normalbelopp,723 +Belgien (2019),SEK,Breakfast,-108.45 +Belgien (2019),SEK,Lunch,-253.05 +Belgien (2019),SEK,Dinner,-253.05 +Belgien (2020),SEK,Normalbelopp,756 +Belgien (2020),SEK,Breakfast,-113.4 +Belgien (2020),SEK,Lunch,-264.6 +Belgien (2020),SEK,Dinner,-264.6 +Belize (2018),SEK,Normalbelopp,453 +Belize (2018),SEK,Breakfast,-67.95 +Belize (2018),SEK,Lunch,-158.55 +Belize (2018),SEK,Dinner,-158.55 +Belize (2019),SEK,Normalbelopp,485 +Belize (2019),SEK,Breakfast,-72.75 +Belize (2019),SEK,Lunch,-169.75 +Belize (2019),SEK,Dinner,-169.75 +Belize (2020),SEK,Normalbelopp,524 +Belize (2020),SEK,Breakfast,-78.6 +Belize (2020),SEK,Lunch,-183.4 +Belize (2020),SEK,Dinner,-183.4 +Benin (2018),SEK,Normalbelopp,471 +Benin (2018),SEK,Breakfast,-70.65 +Benin (2018),SEK,Lunch,-164.85 +Benin (2018),SEK,Dinner,-164.85 +Benin (2019),SEK,Normalbelopp,507 +Benin (2019),SEK,Breakfast,-76.05 +Benin (2019),SEK,Lunch,-177.45 +Benin (2019),SEK,Dinner,-177.45 +Benin (2020),SEK,Normalbelopp,523 +Benin (2020),SEK,Breakfast,-78.45 +Benin (2020),SEK,Lunch,-183.05 +Benin (2020),SEK,Dinner,-183.05 +Bolivia (2018),SEK,Normalbelopp,280 +Bolivia (2018),SEK,Breakfast,-42 +Bolivia (2018),SEK,Lunch,-98 +Bolivia (2018),SEK,Dinner,-98 +Bolivia (2019),SEK,Normalbelopp,322 +Bolivia (2019),SEK,Breakfast,-48.3 +Bolivia (2019),SEK,Lunch,-112.7 +Bolivia (2019),SEK,Dinner,-112.7 +Bolivia (2020),SEK,Normalbelopp,363 +Bolivia (2020),SEK,Breakfast,-54.45 +Bolivia (2020),SEK,Lunch,-127.05 +Bolivia (2020),SEK,Dinner,-127.05 +Bosnien-Hercegovina (2018),SEK,Normalbelopp,323 +Bosnien-Hercegovina (2018),SEK,Breakfast,-48.45 +Bosnien-Hercegovina (2018),SEK,Lunch,-113.05 +Bosnien-Hercegovina (2018),SEK,Dinner,-113.05 +Bosnien-Hercegovina (2019),SEK,Normalbelopp,352 +Bosnien-Hercegovina (2019),SEK,Breakfast,-52.8 +Bosnien-Hercegovina (2019),SEK,Lunch,-123.2 +Bosnien-Hercegovina (2019),SEK,Dinner,-123.2 +Bosnien-Hercegovina (2020),SEK,Normalbelopp,357 +Bosnien-Hercegovina (2020),SEK,Breakfast,-53.55 +Bosnien-Hercegovina (2020),SEK,Lunch,-124.95 +Bosnien-Hercegovina (2020),SEK,Dinner,-124.95 +Botswana (2018),SEK,Normalbelopp,259 +Botswana (2018),SEK,Breakfast,-38.85 +Botswana (2018),SEK,Lunch,-90.65 +Botswana (2018),SEK,Dinner,-90.65 +Botswana (2019),SEK,Normalbelopp,301 +Botswana (2019),SEK,Breakfast,-45.15 +Botswana (2019),SEK,Lunch,-105.35 +Botswana (2019),SEK,Dinner,-105.35 +Botswana (2020),SEK,Normalbelopp,357 +Botswana (2020),SEK,Breakfast,-53.55 +Botswana (2020),SEK,Lunch,-124.95 +Botswana (2020),SEK,Dinner,-124.95 +Brasilien (2018),SEK,Normalbelopp,419 +Brasilien (2018),SEK,Breakfast,-62.85 +Brasilien (2018),SEK,Lunch,-146.65 +Brasilien (2018),SEK,Dinner,-146.65 +Brasilien (2019),SEK,Normalbelopp,411 +Brasilien (2019),SEK,Breakfast,-61.65 +Brasilien (2019),SEK,Lunch,-143.85 +Brasilien (2019),SEK,Dinner,-143.85 +Brasilien (2020),SEK,Normalbelopp,424 +Brasilien (2020),SEK,Breakfast,-63.6 +Brasilien (2020),SEK,Lunch,-148.4 +Brasilien (2020),SEK,Dinner,-148.4 +Brunei Darussalam (2018),SEK,Normalbelopp,366 +Brunei Darussalam (2018),SEK,Breakfast,-54.9 +Brunei Darussalam (2018),SEK,Lunch,-128.1 +Brunei Darussalam (2018),SEK,Dinner,-128.1 +Brunei Darussalam (2019),SEK,Normalbelopp,426 +Brunei Darussalam (2019),SEK,Breakfast,-63.9 +Brunei Darussalam (2019),SEK,Lunch,-149.1 +Brunei Darussalam (2019),SEK,Dinner,-149.1 +Brunei Darussalam (2020),SEK,Normalbelopp,470 +Brunei Darussalam (2020),SEK,Breakfast,-70.5 +Brunei Darussalam (2020),SEK,Lunch,-164.5 +Brunei Darussalam (2020),SEK,Dinner,-164.5 +Bulgarien (2018),SEK,Normalbelopp,347 +Bulgarien (2018),SEK,Breakfast,-52.05 +Bulgarien (2018),SEK,Lunch,-121.45 +Bulgarien (2018),SEK,Dinner,-121.45 +Bulgarien (2019),SEK,Normalbelopp,365 +Bulgarien (2019),SEK,Breakfast,-54.75 +Bulgarien (2019),SEK,Lunch,-127.75 +Bulgarien (2019),SEK,Dinner,-127.75 +Bulgarien (2020),SEK,Normalbelopp,375 +Bulgarien (2020),SEK,Breakfast,-56.25 +Bulgarien (2020),SEK,Lunch,-131.25 +Bulgarien (2020),SEK,Dinner,-131.25 +Burkina Faso (2018),SEK,Normalbelopp,400 +Burkina Faso (2018),SEK,Breakfast,-60 +Burkina Faso (2018),SEK,Lunch,-140 +Burkina Faso (2018),SEK,Dinner,-140 +Burkina Faso (2019),SEK,Normalbelopp,411 +Burkina Faso (2019),SEK,Breakfast,-61.65 +Burkina Faso (2019),SEK,Lunch,-143.85 +Burkina Faso (2019),SEK,Dinner,-143.85 +Burkina Faso (2020),SEK,Normalbelopp,419 +Burkina Faso (2020),SEK,Breakfast,-62.85 +Burkina Faso (2020),SEK,Lunch,-146.65 +Burkina Faso (2020),SEK,Dinner,-146.65 +Burma (2018),SEK,Normalbelopp,315 +Burma (2018),SEK,Breakfast,-47.25 +Burma (2018),SEK,Lunch,-110.25 +Burma (2018),SEK,Dinner,-110.25 +Burma (2019),SEK,Normalbelopp,286 +Burma (2019),SEK,Breakfast,-42.9 +Burma (2019),SEK,Lunch,-100.1 +Burma (2019),SEK,Dinner,-100.1 +Burma (2020),SEK,Normalbelopp,318 +Burma (2020),SEK,Breakfast,-47.7 +Burma (2020),SEK,Lunch,-111.3 +Burma (2020),SEK,Dinner,-111.3 +Centralafrikanska republiken (2018),SEK,Normalbelopp,393 +Centralafrikanska republiken (2018),SEK,Breakfast,-58.95 +Centralafrikanska republiken (2018),SEK,Lunch,-137.55 +Centralafrikanska republiken (2018),SEK,Dinner,-137.55 +Centralafrikanska republiken (2019),SEK,Normalbelopp,436 +Centralafrikanska republiken (2019),SEK,Breakfast,-65.4 +Centralafrikanska republiken (2019),SEK,Lunch,-152.6 +Centralafrikanska republiken (2019),SEK,Dinner,-152.6 +Centralafrikanska republiken (2020),SEK,Normalbelopp,463 +Centralafrikanska republiken (2020),SEK,Breakfast,-69.45 +Centralafrikanska republiken (2020),SEK,Lunch,-162.05 +Centralafrikanska republiken (2020),SEK,Dinner,-162.05 +Chile (2018),SEK,Normalbelopp,424 +Chile (2018),SEK,Breakfast,-63.6 +Chile (2018),SEK,Lunch,-148.4 +Chile (2018),SEK,Dinner,-148.4 +Chile (2019),SEK,Normalbelopp,422 +Chile (2019),SEK,Breakfast,-63.3 +Chile (2019),SEK,Lunch,-147.7 +Chile (2019),SEK,Dinner,-147.7 +Chile (2020),SEK,Normalbelopp,436 +Chile (2020),SEK,Breakfast,-65.4 +Chile (2020),SEK,Lunch,-152.6 +Chile (2020),SEK,Dinner,-152.6 +Colombia (2018),SEK,Normalbelopp,329 +Colombia (2018),SEK,Breakfast,-49.35 +Colombia (2018),SEK,Lunch,-115.15 +Colombia (2018),SEK,Dinner,-115.15 +Colombia (2019),SEK,Normalbelopp,345 +Colombia (2019),SEK,Breakfast,-51.75 +Colombia (2019),SEK,Lunch,-120.75 +Colombia (2019),SEK,Dinner,-120.75 +Colombia (2020),SEK,Normalbelopp,345 +Colombia (2020),SEK,Breakfast,-51.75 +Colombia (2020),SEK,Lunch,-120.75 +Colombia (2020),SEK,Dinner,-120.75 +Costa Rica (2018),SEK,Normalbelopp,445 +Costa Rica (2018),SEK,Breakfast,-66.75 +Costa Rica (2018),SEK,Lunch,-155.75 +Costa Rica (2018),SEK,Dinner,-155.75 +Costa Rica (2019),SEK,Normalbelopp,464 +Costa Rica (2019),SEK,Breakfast,-69.6 +Costa Rica (2019),SEK,Lunch,-162.4 +Costa Rica (2019),SEK,Dinner,-162.4 +Costa Rica (2020),SEK,Normalbelopp,534 +Costa Rica (2020),SEK,Breakfast,-80.1 +Costa Rica (2020),SEK,Lunch,-186.9 +Costa Rica (2020),SEK,Dinner,-186.9 +Cypern (2018),SEK,Normalbelopp,543 +Cypern (2018),SEK,Breakfast,-81.45 +Cypern (2018),SEK,Lunch,-190.05 +Cypern (2018),SEK,Dinner,-190.05 +Cypern (2019),SEK,Normalbelopp,505 +Cypern (2019),SEK,Breakfast,-75.75 +Cypern (2019),SEK,Lunch,-176.75 +Cypern (2019),SEK,Dinner,-176.75 +Cypern (2020),SEK,Normalbelopp,605 +Cypern (2020),SEK,Breakfast,-90.75 +Cypern (2020),SEK,Lunch,-211.75 +Cypern (2020),SEK,Dinner,-211.75 +Danmark (2018),SEK,Normalbelopp,915 +Danmark (2018),SEK,Breakfast,-137.25 +Danmark (2018),SEK,Lunch,-320.25 +Danmark (2018),SEK,Dinner,-320.25 +Danmark (2019),SEK,Normalbelopp,993 +Danmark (2019),SEK,Breakfast,-148.95 +Danmark (2019),SEK,Lunch,-347.55 +Danmark (2019),SEK,Dinner,-347.55 +Danmark (2020),SEK,Normalbelopp,1016 +Danmark (2020),SEK,Breakfast,-152.4 +Danmark (2020),SEK,Lunch,-355.6 +Danmark (2020),SEK,Dinner,-355.6 +Djibouti (2018),SEK,Normalbelopp,480 +Djibouti (2018),SEK,Breakfast,-72 +Djibouti (2018),SEK,Lunch,-168 +Djibouti (2018),SEK,Dinner,-168 +Djibouti (2019),SEK,Normalbelopp,529 +Djibouti (2019),SEK,Breakfast,-79.35 +Djibouti (2019),SEK,Lunch,-185.15 +Djibouti (2019),SEK,Dinner,-185.15 +Djibouti (2020),SEK,Normalbelopp,561 +Djibouti (2020),SEK,Breakfast,-84.15 +Djibouti (2020),SEK,Lunch,-196.35 +Djibouti (2020),SEK,Dinner,-196.35 +Ecuador (2018),SEK,Normalbelopp,512 +Ecuador (2018),SEK,Breakfast,-76.8 +Ecuador (2018),SEK,Lunch,-179.2 +Ecuador (2018),SEK,Dinner,-179.2 +Ecuador (2019),SEK,Normalbelopp,562 +Ecuador (2019),SEK,Breakfast,-84.3 +Ecuador (2019),SEK,Lunch,-196.7 +Ecuador (2019),SEK,Dinner,-196.7 +Ecuador (2020),SEK,Normalbelopp,592 +Ecuador (2020),SEK,Breakfast,-88.8 +Ecuador (2020),SEK,Lunch,-207.2 +Ecuador (2020),SEK,Dinner,-207.2 +Egypten (2018),SEK,Normalbelopp,230 +Egypten (2018),SEK,Breakfast,-34.5 +Egypten (2018),SEK,Lunch,-80.5 +Egypten (2018),SEK,Dinner,-80.5 +Egypten (2019),SEK,Normalbelopp,233 +Egypten (2019),SEK,Breakfast,-34.95 +Egypten (2019),SEK,Lunch,-81.55 +Egypten (2019),SEK,Dinner,-81.55 +Egypten (2020),SEK,Normalbelopp,302 +Egypten (2020),SEK,Breakfast,-45.3 +Egypten (2020),SEK,Lunch,-105.7 +Egypten (2020),SEK,Dinner,-105.7 +El Salvador (2018),SEK,Normalbelopp,405 +El Salvador (2018),SEK,Breakfast,-60.75 +El Salvador (2018),SEK,Lunch,-141.75 +El Salvador (2018),SEK,Dinner,-141.75 +El Salvador (2019),SEK,Normalbelopp,449 +El Salvador (2019),SEK,Breakfast,-67.35 +El Salvador (2019),SEK,Lunch,-157.15 +El Salvador (2019),SEK,Dinner,-157.15 +El Salvador (2020),SEK,Normalbelopp,484 +El Salvador (2020),SEK,Breakfast,-72.6 +El Salvador (2020),SEK,Lunch,-169.4 +El Salvador (2020),SEK,Dinner,-169.4 +Elfenbenskusten (2018),SEK,Normalbelopp,512 +Elfenbenskusten (2018),SEK,Breakfast,-76.8 +Elfenbenskusten (2018),SEK,Lunch,-179.2 +Elfenbenskusten (2018),SEK,Dinner,-179.2 +Elfenbenskusten (2019),SEK,Normalbelopp,562 +Elfenbenskusten (2019),SEK,Breakfast,-84.3 +Elfenbenskusten (2019),SEK,Lunch,-196.7 +Elfenbenskusten (2019),SEK,Dinner,-196.7 +Elfenbenskusten (2020),SEK,Normalbelopp,608 +Elfenbenskusten (2020),SEK,Breakfast,-91.2 +Elfenbenskusten (2020),SEK,Lunch,-212.8 +Elfenbenskusten (2020),SEK,Dinner,-212.8 +Eritrea (2018),SEK,Normalbelopp,471 +Eritrea (2018),SEK,Breakfast,-70.65 +Eritrea (2018),SEK,Lunch,-164.85 +Eritrea (2018),SEK,Dinner,-164.85 +Eritrea (2019),SEK,Normalbelopp,515 +Eritrea (2019),SEK,Breakfast,-77.25 +Eritrea (2019),SEK,Lunch,-180.25 +Eritrea (2019),SEK,Dinner,-180.25 +Eritrea (2020),SEK,Normalbelopp,468 +Eritrea (2020),SEK,Breakfast,-70.2 +Eritrea (2020),SEK,Lunch,-163.8 +Eritrea (2020),SEK,Dinner,-163.8 +Estland (2018),SEK,Normalbelopp,459 +Estland (2018),SEK,Breakfast,-68.85 +Estland (2018),SEK,Lunch,-160.65 +Estland (2018),SEK,Dinner,-160.65 +Estland (2019),SEK,Normalbelopp,491 +Estland (2019),SEK,Breakfast,-73.65 +Estland (2019),SEK,Lunch,-171.85 +Estland (2019),SEK,Dinner,-171.85 +Estland (2020),SEK,Normalbelopp,506 +Estland (2020),SEK,Breakfast,-75.9 +Estland (2020),SEK,Lunch,-177.1 +Estland (2020),SEK,Dinner,-177.1 +Etiopien (2018),SEK,Normalbelopp,230 +Etiopien (2018),SEK,Breakfast,-34.5 +Etiopien (2018),SEK,Lunch,-80.5 +Etiopien (2018),SEK,Dinner,-80.5 +Etiopien (2019),SEK,Normalbelopp,265 +Etiopien (2019),SEK,Breakfast,-39.75 +Etiopien (2019),SEK,Lunch,-92.75 +Etiopien (2019),SEK,Dinner,-92.75 +Etiopien (2020),SEK,Normalbelopp,266 +Etiopien (2020),SEK,Breakfast,-39.9 +Etiopien (2020),SEK,Lunch,-93.1 +Etiopien (2020),SEK,Dinner,-93.1 +Filippinerna (2018),SEK,Normalbelopp,357 +Filippinerna (2018),SEK,Breakfast,-53.55 +Filippinerna (2018),SEK,Lunch,-124.95 +Filippinerna (2018),SEK,Dinner,-124.95 +Filippinerna (2019),SEK,Normalbelopp,384 +Filippinerna (2019),SEK,Breakfast,-57.6 +Filippinerna (2019),SEK,Lunch,-134.4 +Filippinerna (2019),SEK,Dinner,-134.4 +Filippinerna (2020),SEK,Normalbelopp,441 +Filippinerna (2020),SEK,Breakfast,-66.15 +Filippinerna (2020),SEK,Lunch,-154.35 +Filippinerna (2020),SEK,Dinner,-154.35 +Finland (2018),SEK,Normalbelopp,680 +Finland (2018),SEK,Breakfast,-102 +Finland (2018),SEK,Lunch,-238 +Finland (2018),SEK,Dinner,-238 +Finland (2019),SEK,Normalbelopp,729 +Finland (2019),SEK,Breakfast,-109.35 +Finland (2019),SEK,Lunch,-255.15 +Finland (2019),SEK,Dinner,-255.15 +Finland (2020),SEK,Normalbelopp,759 +Finland (2020),SEK,Breakfast,-113.85 +Finland (2020),SEK,Lunch,-265.65 +Finland (2020),SEK,Dinner,-265.65 +Förenade Arabemiraten (2018),SEK,Normalbelopp,703 +Förenade Arabemiraten (2018),SEK,Breakfast,-105.45 +Förenade Arabemiraten (2018),SEK,Lunch,-246.05 +Förenade Arabemiraten (2018),SEK,Dinner,-246.05 +Förenade Arabemiraten (2019),SEK,Normalbelopp,849 +Förenade Arabemiraten (2019),SEK,Breakfast,-127.35 +Förenade Arabemiraten (2019),SEK,Lunch,-297.15 +Förenade Arabemiraten (2019),SEK,Dinner,-297.15 +Förenade Arabemiraten (2020),SEK,Normalbelopp,899 +Förenade Arabemiraten (2020),SEK,Breakfast,-134.85 +Förenade Arabemiraten (2020),SEK,Lunch,-314.65 +Förenade Arabemiraten (2020),SEK,Dinner,-314.65 +Frankrike (2018),SEK,Normalbelopp,714 +Frankrike (2018),SEK,Breakfast,-107.1 +Frankrike (2018),SEK,Lunch,-249.9 +Frankrike (2018),SEK,Dinner,-249.9 +Frankrike (2019),SEK,Normalbelopp,760 +Frankrike (2019),SEK,Breakfast,-114 +Frankrike (2019),SEK,Lunch,-266 +Frankrike (2019),SEK,Dinner,-266 +Frankrike (2020),SEK,Normalbelopp,794 +Frankrike (2020),SEK,Breakfast,-119.1 +Frankrike (2020),SEK,Lunch,-277.9 +Frankrike (2020),SEK,Dinner,-277.9 +Gabon (2018),SEK,Normalbelopp,708 +Gabon (2018),SEK,Breakfast,-106.2 +Gabon (2018),SEK,Lunch,-247.8 +Gabon (2018),SEK,Dinner,-247.8 +Gabon (2019),SEK,Normalbelopp,742 +Gabon (2019),SEK,Breakfast,-111.3 +Gabon (2019),SEK,Lunch,-259.7 +Gabon (2019),SEK,Dinner,-259.7 +Gabon (2020),SEK,Normalbelopp,751 +Gabon (2020),SEK,Breakfast,-112.65 +Gabon (2020),SEK,Lunch,-262.85 +Gabon (2020),SEK,Dinner,-262.85 +Gambia (2018),SEK,Normalbelopp,237 +Gambia (2018),SEK,Breakfast,-35.55 +Gambia (2018),SEK,Lunch,-82.95 +Gambia (2018),SEK,Dinner,-82.95 +Gambia (2019),SEK,Normalbelopp,244 +Gambia (2019),SEK,Breakfast,-36.6 +Gambia (2019),SEK,Lunch,-85.4 +Gambia (2019),SEK,Dinner,-85.4 +Gambia (2020),SEK,Normalbelopp,260 +Gambia (2020),SEK,Breakfast,-39 +Gambia (2020),SEK,Lunch,-91 +Gambia (2020),SEK,Dinner,-91 +Georgien (2018),SEK,Normalbelopp,230 +Georgien (2018),SEK,Breakfast,-34.5 +Georgien (2018),SEK,Lunch,-80.5 +Georgien (2018),SEK,Dinner,-80.5 +Georgien (2019),SEK,Normalbelopp,235 +Georgien (2019),SEK,Breakfast,-35.25 +Georgien (2019),SEK,Lunch,-82.25 +Georgien (2019),SEK,Dinner,-82.25 +Georgien (2020),SEK,Normalbelopp,240 +Georgien (2020),SEK,Breakfast,-36 +Georgien (2020),SEK,Lunch,-84 +Georgien (2020),SEK,Dinner,-84 +Ghana (2018),SEK,Normalbelopp,404 +Ghana (2018),SEK,Breakfast,-60.6 +Ghana (2018),SEK,Lunch,-141.4 +Ghana (2018),SEK,Dinner,-141.4 +Ghana (2019),SEK,Normalbelopp,407 +Ghana (2019),SEK,Breakfast,-61.05 +Ghana (2019),SEK,Lunch,-142.45 +Ghana (2019),SEK,Dinner,-142.45 +Ghana (2020),SEK,Normalbelopp,372 +Ghana (2020),SEK,Breakfast,-55.8 +Ghana (2020),SEK,Lunch,-130.2 +Ghana (2020),SEK,Dinner,-130.2 +Grekland (2018),SEK,Normalbelopp,589 +Grekland (2018),SEK,Breakfast,-88.35 +Grekland (2018),SEK,Lunch,-206.15 +Grekland (2018),SEK,Dinner,-206.15 +Grekland (2019),SEK,Normalbelopp,625 +Grekland (2019),SEK,Breakfast,-93.75 +Grekland (2019),SEK,Lunch,-218.75 +Grekland (2019),SEK,Dinner,-218.75 +Grekland (2020),SEK,Normalbelopp,650 +Grekland (2020),SEK,Breakfast,-97.5 +Grekland (2020),SEK,Lunch,-227.5 +Grekland (2020),SEK,Dinner,-227.5 +Grenada (2018),SEK,Normalbelopp,477 +Grenada (2018),SEK,Breakfast,-71.55 +Grenada (2018),SEK,Lunch,-166.95 +Grenada (2018),SEK,Dinner,-166.95 +Grenada (2019),SEK,Normalbelopp,550 +Grenada (2019),SEK,Breakfast,-82.5 +Grenada (2019),SEK,Lunch,-192.5 +Grenada (2019),SEK,Dinner,-192.5 +Grenada (2020),SEK,Normalbelopp,581 +Grenada (2020),SEK,Breakfast,-87.15 +Grenada (2020),SEK,Lunch,-203.35 +Grenada (2020),SEK,Dinner,-203.35 +Grönland (2018),SEK,Normalbelopp,915 +Grönland (2018),SEK,Breakfast,-137.25 +Grönland (2018),SEK,Lunch,-320.25 +Grönland (2018),SEK,Dinner,-320.25 +Grönland (2019),SEK,Normalbelopp,993 +Grönland (2019),SEK,Breakfast,-148.95 +Grönland (2019),SEK,Lunch,-347.55 +Grönland (2019),SEK,Dinner,-347.55 +Grönland (2020),SEK,Normalbelopp,1016 +Grönland (2020),SEK,Breakfast,-152.4 +Grönland (2020),SEK,Lunch,-355.6 +Grönland (2020),SEK,Dinner,-355.6 +Guinea (2018),SEK,Normalbelopp,400 +Guinea (2018),SEK,Breakfast,-60 +Guinea (2018),SEK,Lunch,-140 +Guinea (2018),SEK,Dinner,-140 +Guinea (2019),SEK,Normalbelopp,444 +Guinea (2019),SEK,Breakfast,-66.6 +Guinea (2019),SEK,Lunch,-155.4 +Guinea (2019),SEK,Dinner,-155.4 +Guinea (2020),SEK,Normalbelopp,475 +Guinea (2020),SEK,Breakfast,-71.25 +Guinea (2020),SEK,Lunch,-166.25 +Guinea (2020),SEK,Dinner,-166.25 +Guyana (2018),SEK,Normalbelopp,494 +Guyana (2018),SEK,Breakfast,-74.1 +Guyana (2018),SEK,Lunch,-172.9 +Guyana (2018),SEK,Dinner,-172.9 +Guyana (2019),SEK,Normalbelopp,518 +Guyana (2019),SEK,Breakfast,-77.7 +Guyana (2019),SEK,Lunch,-181.3 +Guyana (2019),SEK,Dinner,-181.3 +Guyana (2020),SEK,Normalbelopp,548 +Guyana (2020),SEK,Breakfast,-82.2 +Guyana (2020),SEK,Lunch,-191.8 +Guyana (2020),SEK,Dinner,-191.8 +Haiti (2018),SEK,Normalbelopp,525 +Haiti (2018),SEK,Breakfast,-78.75 +Haiti (2018),SEK,Lunch,-183.75 +Haiti (2018),SEK,Dinner,-183.75 +Haiti (2019),SEK,Normalbelopp,548 +Haiti (2019),SEK,Breakfast,-82.2 +Haiti (2019),SEK,Lunch,-191.8 +Haiti (2019),SEK,Dinner,-191.8 +Haiti (2020),SEK,Normalbelopp,451 +Haiti (2020),SEK,Breakfast,-67.65 +Haiti (2020),SEK,Lunch,-157.85 +Haiti (2020),SEK,Dinner,-157.85 +Honduras (2018),SEK,Normalbelopp,312 +Honduras (2018),SEK,Breakfast,-46.8 +Honduras (2018),SEK,Lunch,-109.2 +Honduras (2018),SEK,Dinner,-109.2 +Honduras (2019),SEK,Normalbelopp,328 +Honduras (2019),SEK,Breakfast,-49.2 +Honduras (2019),SEK,Lunch,-114.8 +Honduras (2019),SEK,Dinner,-114.8 +Honduras (2020),SEK,Normalbelopp,351 +Honduras (2020),SEK,Breakfast,-52.65 +Honduras (2020),SEK,Lunch,-122.85 +Honduras (2020),SEK,Dinner,-122.85 +Hong Kong (2018),SEK,Normalbelopp,538 +Hong Kong (2018),SEK,Breakfast,-80.7 +Hong Kong (2018),SEK,Lunch,-188.3 +Hong Kong (2018),SEK,Dinner,-188.3 +Hong Kong (2019),SEK,Normalbelopp,578 +Hong Kong (2019),SEK,Breakfast,-86.7 +Hong Kong (2019),SEK,Lunch,-202.3 +Hong Kong (2019),SEK,Dinner,-202.3 +Hong Kong (2020),SEK,Normalbelopp,590 +Hong Kong (2020),SEK,Breakfast,-88.5 +Hong Kong (2020),SEK,Lunch,-206.5 +Hong Kong (2020),SEK,Dinner,-206.5 +Indien (2018),SEK,Normalbelopp,319 +Indien (2018),SEK,Breakfast,-47.85 +Indien (2018),SEK,Lunch,-111.65 +Indien (2018),SEK,Dinner,-111.65 +Indien (2019),SEK,Normalbelopp,320 +Indien (2019),SEK,Breakfast,-48 +Indien (2019),SEK,Lunch,-112 +Indien (2019),SEK,Dinner,-112 +Indien (2020),SEK,Normalbelopp,356 +Indien (2020),SEK,Breakfast,-53.4 +Indien (2020),SEK,Lunch,-124.6 +Indien (2020),SEK,Dinner,-124.6 +Indonesien (2018),SEK,Normalbelopp,358 +Indonesien (2018),SEK,Breakfast,-53.7 +Indonesien (2018),SEK,Lunch,-125.3 +Indonesien (2018),SEK,Dinner,-125.3 +Indonesien (2019),SEK,Normalbelopp,348 +Indonesien (2019),SEK,Breakfast,-52.2 +Indonesien (2019),SEK,Lunch,-121.8 +Indonesien (2019),SEK,Dinner,-121.8 +Indonesien (2020),SEK,Normalbelopp,404 +Indonesien (2020),SEK,Breakfast,-60.6 +Indonesien (2020),SEK,Lunch,-141.4 +Indonesien (2020),SEK,Dinner,-141.4 +Irak (2018),SEK,Normalbelopp,626 +Irak (2018),SEK,Breakfast,-93.9 +Irak (2018),SEK,Lunch,-219.1 +Irak (2018),SEK,Dinner,-219.1 +Irak (2019),SEK,Normalbelopp,671 +Irak (2019),SEK,Breakfast,-100.65 +Irak (2019),SEK,Lunch,-234.85 +Irak (2019),SEK,Dinner,-234.85 +Irak (2020),SEK,Normalbelopp,703 +Irak (2020),SEK,Breakfast,-105.45 +Irak (2020),SEK,Lunch,-246.05 +Irak (2020),SEK,Dinner,-246.05 +Iran (2018),SEK,Normalbelopp,410 +Iran (2018),SEK,Breakfast,-61.5 +Iran (2018),SEK,Lunch,-143.5 +Iran (2018),SEK,Dinner,-143.5 +Iran (2019),SEK,Normalbelopp,362 +Iran (2019),SEK,Breakfast,-54.3 +Iran (2019),SEK,Lunch,-126.7 +Iran (2019),SEK,Dinner,-126.7 +Iran (2020),SEK,Normalbelopp,452 +Iran (2020),SEK,Breakfast,-67.8 +Iran (2020),SEK,Lunch,-158.2 +Iran (2020),SEK,Dinner,-158.2 +Irland (2018),SEK,Normalbelopp,654 +Irland (2018),SEK,Breakfast,-98.1 +Irland (2018),SEK,Lunch,-228.9 +Irland (2018),SEK,Dinner,-228.9 +Irland (2019),SEK,Normalbelopp,703 +Irland (2019),SEK,Breakfast,-105.45 +Irland (2019),SEK,Lunch,-246.05 +Irland (2019),SEK,Dinner,-246.05 +Irland (2020),SEK,Normalbelopp,727 +Irland (2020),SEK,Breakfast,-109.05 +Irland (2020),SEK,Lunch,-254.45 +Irland (2020),SEK,Dinner,-254.45 +Island (2018),SEK,Normalbelopp,837 +Island (2018),SEK,Breakfast,-125.55 +Island (2018),SEK,Lunch,-292.95 +Island (2018),SEK,Dinner,-292.95 +Island (2019),SEK,Normalbelopp,798 +Island (2019),SEK,Breakfast,-119.7 +Island (2019),SEK,Lunch,-279.3 +Island (2019),SEK,Dinner,-279.3 +Island (2020),SEK,Normalbelopp,838 +Island (2020),SEK,Breakfast,-125.7 +Island (2020),SEK,Lunch,-293.3 +Island (2020),SEK,Dinner,-293.3 +Israel (2018),SEK,Normalbelopp,828 +Israel (2018),SEK,Breakfast,-124.2 +Israel (2018),SEK,Lunch,-289.8 +Israel (2018),SEK,Dinner,-289.8 +Israel (2019),SEK,Normalbelopp,822 +Israel (2019),SEK,Breakfast,-123.3 +Israel (2019),SEK,Lunch,-287.7 +Israel (2019),SEK,Dinner,-287.7 +Israel (2020),SEK,Normalbelopp,921 +Israel (2020),SEK,Breakfast,-138.15 +Israel (2020),SEK,Lunch,-322.35 +Israel (2020),SEK,Dinner,-322.35 +Italien (2018),SEK,Normalbelopp,540 +Italien (2018),SEK,Breakfast,-81 +Italien (2018),SEK,Lunch,-189 +Italien (2018),SEK,Dinner,-189 +Italien (2019),SEK,Normalbelopp,590 +Italien (2019),SEK,Breakfast,-88.5 +Italien (2019),SEK,Lunch,-206.5 +Italien (2019),SEK,Dinner,-206.5 +Italien (2020),SEK,Normalbelopp,627 +Italien (2020),SEK,Breakfast,-94.05 +Italien (2020),SEK,Lunch,-219.45 +Italien (2020),SEK,Dinner,-219.45 +Jamaica (2018),SEK,Normalbelopp,475 +Jamaica (2018),SEK,Breakfast,-71.25 +Jamaica (2018),SEK,Lunch,-166.25 +Jamaica (2018),SEK,Dinner,-166.25 +Jamaica (2019),SEK,Normalbelopp,539 +Jamaica (2019),SEK,Breakfast,-80.85 +Jamaica (2019),SEK,Lunch,-188.65 +Jamaica (2019),SEK,Dinner,-188.65 +Jamaica (2020),SEK,Normalbelopp,519 +Jamaica (2020),SEK,Breakfast,-77.85 +Jamaica (2020),SEK,Lunch,-181.65 +Jamaica (2020),SEK,Dinner,-181.65 +Japan (2018),SEK,Normalbelopp,503 +Japan (2018),SEK,Breakfast,-75.45 +Japan (2018),SEK,Lunch,-176.05 +Japan (2018),SEK,Dinner,-176.05 +Japan (2019),SEK,Normalbelopp,537 +Japan (2019),SEK,Breakfast,-80.55 +Japan (2019),SEK,Lunch,-187.95 +Japan (2019),SEK,Dinner,-187.95 +Japan (2020),SEK,Normalbelopp,587 +Japan (2020),SEK,Breakfast,-88.05 +Japan (2020),SEK,Lunch,-205.45 +Japan (2020),SEK,Dinner,-205.45 +Jordanien (2018),SEK,Normalbelopp,651 +Jordanien (2018),SEK,Breakfast,-97.65 +Jordanien (2018),SEK,Lunch,-227.85 +Jordanien (2018),SEK,Dinner,-227.85 +Jordanien (2019),SEK,Normalbelopp,709 +Jordanien (2019),SEK,Breakfast,-106.35 +Jordanien (2019),SEK,Lunch,-248.15 +Jordanien (2019),SEK,Dinner,-248.15 +Jordanien (2020),SEK,Normalbelopp,748 +Jordanien (2020),SEK,Breakfast,-112.2 +Jordanien (2020),SEK,Lunch,-261.8 +Jordanien (2020),SEK,Dinner,-261.8 +Kambodja (2018),SEK,Normalbelopp,380 +Kambodja (2018),SEK,Breakfast,-57 +Kambodja (2018),SEK,Lunch,-133 +Kambodja (2018),SEK,Dinner,-133 +Kambodja (2019),SEK,Normalbelopp,434 +Kambodja (2019),SEK,Breakfast,-65.1 +Kambodja (2019),SEK,Lunch,-151.9 +Kambodja (2019),SEK,Dinner,-151.9 +Kambodja (2020),SEK,Normalbelopp,485 +Kambodja (2020),SEK,Breakfast,-72.75 +Kambodja (2020),SEK,Lunch,-169.75 +Kambodja (2020),SEK,Dinner,-169.75 +Kamerun (2018),SEK,Normalbelopp,491 +Kamerun (2018),SEK,Breakfast,-73.65 +Kamerun (2018),SEK,Lunch,-171.85 +Kamerun (2018),SEK,Dinner,-171.85 +Kamerun (2019),SEK,Normalbelopp,504 +Kamerun (2019),SEK,Breakfast,-75.6 +Kamerun (2019),SEK,Lunch,-176.4 +Kamerun (2019),SEK,Dinner,-176.4 +Kamerun (2020),SEK,Normalbelopp,520 +Kamerun (2020),SEK,Breakfast,-78 +Kamerun (2020),SEK,Lunch,-182 +Kamerun (2020),SEK,Dinner,-182 +Kanada (2018),SEK,Normalbelopp,556 +Kanada (2018),SEK,Breakfast,-83.4 +Kanada (2018),SEK,Lunch,-194.6 +Kanada (2018),SEK,Dinner,-194.6 +Kanada (2019),SEK,Normalbelopp,616 +Kanada (2019),SEK,Breakfast,-92.4 +Kanada (2019),SEK,Lunch,-215.6 +Kanada (2019),SEK,Dinner,-215.6 +Kanada (2020),SEK,Normalbelopp,662 +Kanada (2020),SEK,Breakfast,-99.3 +Kanada (2020),SEK,Lunch,-231.7 +Kanada (2020),SEK,Dinner,-231.7 +Kazakstan (2018),SEK,Normalbelopp,262 +Kazakstan (2018),SEK,Breakfast,-39.3 +Kazakstan (2018),SEK,Lunch,-91.7 +Kazakstan (2018),SEK,Dinner,-91.7 +Kazakstan (2019),SEK,Normalbelopp,261 +Kazakstan (2019),SEK,Breakfast,-39.15 +Kazakstan (2019),SEK,Lunch,-91.35 +Kazakstan (2019),SEK,Dinner,-91.35 +Kazakstan (2020),SEK,Normalbelopp,257 +Kazakstan (2020),SEK,Breakfast,-38.55 +Kazakstan (2020),SEK,Lunch,-89.95 +Kazakstan (2020),SEK,Dinner,-89.95 +Kenya (2018),SEK,Normalbelopp,438 +Kenya (2018),SEK,Breakfast,-65.7 +Kenya (2018),SEK,Lunch,-153.3 +Kenya (2018),SEK,Dinner,-153.3 +Kenya (2019),SEK,Normalbelopp,488 +Kenya (2019),SEK,Breakfast,-73.2 +Kenya (2019),SEK,Lunch,-170.8 +Kenya (2019),SEK,Dinner,-170.8 +Kenya (2020),SEK,Normalbelopp,505 +Kenya (2020),SEK,Breakfast,-75.75 +Kenya (2020),SEK,Lunch,-176.75 +Kenya (2020),SEK,Dinner,-176.75 +Kina (2018),SEK,Normalbelopp,538 +Kina (2018),SEK,Breakfast,-80.7 +Kina (2018),SEK,Lunch,-188.3 +Kina (2018),SEK,Dinner,-188.3 +Kina (2019),SEK,Normalbelopp,578 +Kina (2019),SEK,Breakfast,-86.7 +Kina (2019),SEK,Lunch,-202.3 +Kina (2019),SEK,Dinner,-202.3 +Kina (2020),SEK,Normalbelopp,590 +Kina (2020),SEK,Breakfast,-88.5 +Kina (2020),SEK,Lunch,-206.5 +Kina (2020),SEK,Dinner,-206.5 +Kirgizistan (2018),SEK,Normalbelopp,230 +Kirgizistan (2018),SEK,Breakfast,-34.5 +Kirgizistan (2018),SEK,Lunch,-80.5 +Kirgizistan (2018),SEK,Dinner,-80.5 +Kirgizistan (2019),SEK,Normalbelopp,259 +Kirgizistan (2019),SEK,Breakfast,-38.85 +Kirgizistan (2019),SEK,Lunch,-90.65 +Kirgizistan (2019),SEK,Dinner,-90.65 +Kirgizistan (2020),SEK,Normalbelopp,271 +Kirgizistan (2020),SEK,Breakfast,-40.65 +Kirgizistan (2020),SEK,Lunch,-94.85 +Kirgizistan (2020),SEK,Dinner,-94.85 +Kongo (Brazzaville) (2018),SEK,Normalbelopp,500 +Kongo (Brazzaville) (2018),SEK,Breakfast,-75 +Kongo (Brazzaville) (2018),SEK,Lunch,-175 +Kongo (Brazzaville) (2018),SEK,Dinner,-175 +Kongo (Brazzaville) (2019),SEK,Normalbelopp,542 +Kongo (Brazzaville) (2019),SEK,Breakfast,-81.3 +Kongo (Brazzaville) (2019),SEK,Lunch,-189.7 +Kongo (Brazzaville) (2019),SEK,Dinner,-189.7 +Kongo (Brazzaville) (2020),SEK,Normalbelopp,560 +Kongo (Brazzaville) (2020),SEK,Breakfast,-84 +Kongo (Brazzaville) (2020),SEK,Lunch,-196 +Kongo (Brazzaville) (2020),SEK,Dinner,-196 +Kongo (Demokratiska Republiken) (2018),SEK,Normalbelopp,464 +Kongo (Demokratiska Republiken) (2018),SEK,Breakfast,-69.6 +Kongo (Demokratiska Republiken) (2018),SEK,Lunch,-162.4 +Kongo (Demokratiska Republiken) (2018),SEK,Dinner,-162.4 +Kongo (Demokratiska Republiken) (2019),SEK,Normalbelopp,638 +Kongo (Demokratiska Republiken) (2019),SEK,Breakfast,-95.7 +Kongo (Demokratiska Republiken) (2019),SEK,Lunch,-223.3 +Kongo (Demokratiska Republiken) (2019),SEK,Dinner,-223.3 +Kongo (Demokratiska Republiken) (2020),SEK,Normalbelopp,634 +Kongo (Demokratiska Republiken) (2020),SEK,Breakfast,-95.1 +Kongo (Demokratiska Republiken) (2020),SEK,Lunch,-221.9 +Kongo (Demokratiska Republiken) (2020),SEK,Dinner,-221.9 +Kosovo (2018),SEK,Normalbelopp,240 +Kosovo (2018),SEK,Breakfast,-36 +Kosovo (2018),SEK,Lunch,-84 +Kosovo (2018),SEK,Dinner,-84 +Kosovo (2019),SEK,Normalbelopp,246 +Kosovo (2019),SEK,Breakfast,-36.9 +Kosovo (2019),SEK,Lunch,-86.1 +Kosovo (2019),SEK,Dinner,-86.1 +Kosovo (2020),SEK,Normalbelopp,240 +Kosovo (2020),SEK,Breakfast,-36 +Kosovo (2020),SEK,Lunch,-84 +Kosovo (2020),SEK,Dinner,-84 +Kroatien (2018),SEK,Normalbelopp,410 +Kroatien (2018),SEK,Breakfast,-61.5 +Kroatien (2018),SEK,Lunch,-143.5 +Kroatien (2018),SEK,Dinner,-143.5 +Kroatien (2019),SEK,Normalbelopp,439 +Kroatien (2019),SEK,Breakfast,-65.85 +Kroatien (2019),SEK,Lunch,-153.65 +Kroatien (2019),SEK,Dinner,-153.65 +Kroatien (2020),SEK,Normalbelopp,448 +Kroatien (2020),SEK,Breakfast,-67.2 +Kroatien (2020),SEK,Lunch,-156.8 +Kroatien (2020),SEK,Dinner,-156.8 +Kuba (2018),SEK,Normalbelopp,379 +Kuba (2018),SEK,Breakfast,-56.85 +Kuba (2018),SEK,Lunch,-132.65 +Kuba (2018),SEK,Dinner,-132.65 +Kuba (2019),SEK,Normalbelopp,413 +Kuba (2019),SEK,Breakfast,-61.95 +Kuba (2019),SEK,Lunch,-144.55 +Kuba (2019),SEK,Dinner,-144.55 +Kuba (2020),SEK,Normalbelopp,449 +Kuba (2020),SEK,Breakfast,-67.35 +Kuba (2020),SEK,Lunch,-157.15 +Kuba (2020),SEK,Dinner,-157.15 +Kuwait (2018),SEK,Normalbelopp,677 +Kuwait (2018),SEK,Breakfast,-101.55 +Kuwait (2018),SEK,Lunch,-236.95 +Kuwait (2018),SEK,Dinner,-236.95 +Kuwait (2019),SEK,Normalbelopp,741 +Kuwait (2019),SEK,Breakfast,-111.15 +Kuwait (2019),SEK,Lunch,-259.35 +Kuwait (2019),SEK,Dinner,-259.35 +Kuwait (2020),SEK,Normalbelopp,791 +Kuwait (2020),SEK,Breakfast,-118.65 +Kuwait (2020),SEK,Lunch,-276.85 +Kuwait (2020),SEK,Dinner,-276.85 +Laos (2018),SEK,Normalbelopp,278 +Laos (2018),SEK,Breakfast,-41.7 +Laos (2018),SEK,Lunch,-97.3 +Laos (2018),SEK,Dinner,-97.3 +Laos (2019),SEK,Normalbelopp,302 +Laos (2019),SEK,Breakfast,-45.3 +Laos (2019),SEK,Lunch,-105.7 +Laos (2019),SEK,Dinner,-105.7 +Laos (2020),SEK,Normalbelopp,308 +Laos (2020),SEK,Breakfast,-46.2 +Laos (2020),SEK,Lunch,-107.8 +Laos (2020),SEK,Dinner,-107.8 +Lettland (2018),SEK,Normalbelopp,460 +Lettland (2018),SEK,Breakfast,-69 +Lettland (2018),SEK,Lunch,-161 +Lettland (2018),SEK,Dinner,-161 +Lettland (2019),SEK,Normalbelopp,505 +Lettland (2019),SEK,Breakfast,-75.75 +Lettland (2019),SEK,Lunch,-176.75 +Lettland (2019),SEK,Dinner,-176.75 +Lettland (2020),SEK,Normalbelopp,529 +Lettland (2020),SEK,Breakfast,-79.35 +Lettland (2020),SEK,Lunch,-185.15 +Lettland (2020),SEK,Dinner,-185.15 +Libanon (2018),SEK,Normalbelopp,674 +Libanon (2018),SEK,Breakfast,-101.1 +Libanon (2018),SEK,Lunch,-235.9 +Libanon (2018),SEK,Dinner,-235.9 +Libanon (2019),SEK,Normalbelopp,738 +Libanon (2019),SEK,Breakfast,-110.7 +Libanon (2019),SEK,Lunch,-258.3 +Libanon (2019),SEK,Dinner,-258.3 +Libanon (2020),SEK,Normalbelopp,766 +Libanon (2020),SEK,Breakfast,-114.9 +Libanon (2020),SEK,Lunch,-268.1 +Libanon (2020),SEK,Dinner,-268.1 +Liberia (2018),SEK,Normalbelopp,549 +Liberia (2018),SEK,Breakfast,-82.35 +Liberia (2018),SEK,Lunch,-192.15 +Liberia (2018),SEK,Dinner,-192.15 +Liberia (2019),SEK,Normalbelopp,569 +Liberia (2019),SEK,Breakfast,-85.35 +Liberia (2019),SEK,Lunch,-199.15 +Liberia (2019),SEK,Dinner,-199.15 +Liberia (2020),SEK,Normalbelopp,604 +Liberia (2020),SEK,Breakfast,-90.6 +Liberia (2020),SEK,Lunch,-211.4 +Liberia (2020),SEK,Dinner,-211.4 +Libyen (2018),SEK,Normalbelopp,584 +Libyen (2018),SEK,Breakfast,-87.6 +Libyen (2018),SEK,Lunch,-204.4 +Libyen (2018),SEK,Dinner,-204.4 +Libyen (2019),SEK,Normalbelopp,707 +Libyen (2019),SEK,Breakfast,-106.05 +Libyen (2019),SEK,Lunch,-247.45 +Libyen (2019),SEK,Dinner,-247.45 +Libyen (2020),SEK,Normalbelopp,775 +Libyen (2020),SEK,Breakfast,-116.25 +Libyen (2020),SEK,Lunch,-271.25 +Libyen (2020),SEK,Dinner,-271.25 +Liechtenstein (2018),SEK,Normalbelopp,705 +Liechtenstein (2018),SEK,Breakfast,-105.75 +Liechtenstein (2018),SEK,Lunch,-246.75 +Liechtenstein (2018),SEK,Dinner,-246.75 +Liechtenstein (2019),SEK,Normalbelopp,794 +Liechtenstein (2019),SEK,Breakfast,-119.1 +Liechtenstein (2019),SEK,Lunch,-277.9 +Liechtenstein (2019),SEK,Dinner,-277.9 +Liechtenstein (2020),SEK,Normalbelopp,914 +Liechtenstein (2020),SEK,Breakfast,-137.1 +Liechtenstein (2020),SEK,Lunch,-319.9 +Liechtenstein (2020),SEK,Dinner,-319.9 +Litauen (2018),SEK,Normalbelopp,345 +Litauen (2018),SEK,Breakfast,-51.75 +Litauen (2018),SEK,Lunch,-120.75 +Litauen (2018),SEK,Dinner,-120.75 +Litauen (2019),SEK,Normalbelopp,377 +Litauen (2019),SEK,Breakfast,-56.55 +Litauen (2019),SEK,Lunch,-131.95 +Litauen (2019),SEK,Dinner,-131.95 +Litauen (2020),SEK,Normalbelopp,398 +Litauen (2020),SEK,Breakfast,-59.7 +Litauen (2020),SEK,Lunch,-139.3 +Litauen (2020),SEK,Dinner,-139.3 +Luxemburg (2018),SEK,Normalbelopp,669 +Luxemburg (2018),SEK,Breakfast,-100.35 +Luxemburg (2018),SEK,Lunch,-234.15 +Luxemburg (2018),SEK,Dinner,-234.15 +Luxemburg (2019),SEK,Normalbelopp,735 +Luxemburg (2019),SEK,Breakfast,-110.25 +Luxemburg (2019),SEK,Lunch,-257.25 +Luxemburg (2019),SEK,Dinner,-257.25 +Luxemburg (2020),SEK,Normalbelopp,776 +Luxemburg (2020),SEK,Breakfast,-116.4 +Luxemburg (2020),SEK,Lunch,-271.6 +Luxemburg (2020),SEK,Dinner,-271.6 +Macao (2018),SEK,Normalbelopp,538 +Macao (2018),SEK,Breakfast,-80.7 +Macao (2018),SEK,Lunch,-188.3 +Macao (2018),SEK,Dinner,-188.3 +Macao (2019),SEK,Normalbelopp,578 +Macao (2019),SEK,Breakfast,-86.7 +Macao (2019),SEK,Lunch,-202.3 +Macao (2019),SEK,Dinner,-202.3 +Macao (2020),SEK,Normalbelopp,590 +Macao (2020),SEK,Breakfast,-88.5 +Macao (2020),SEK,Lunch,-206.5 +Macao (2020),SEK,Dinner,-206.5 +Madagaskar (2018),SEK,Normalbelopp,230 +Madagaskar (2018),SEK,Breakfast,-34.5 +Madagaskar (2018),SEK,Lunch,-80.5 +Madagaskar (2018),SEK,Dinner,-80.5 +Madagaskar (2019),SEK,Normalbelopp,230 +Madagaskar (2019),SEK,Breakfast,-34.5 +Madagaskar (2019),SEK,Lunch,-80.5 +Madagaskar (2019),SEK,Dinner,-80.5 +Madagaskar (2020),SEK,Normalbelopp,240 +Madagaskar (2020),SEK,Breakfast,-36 +Madagaskar (2020),SEK,Lunch,-84 +Madagaskar (2020),SEK,Dinner,-84 +Makedonien (2018),SEK,Normalbelopp,297 +Makedonien (2018),SEK,Breakfast,-44.55 +Makedonien (2018),SEK,Lunch,-103.95 +Makedonien (2018),SEK,Dinner,-103.95 +Makedonien (2019),SEK,Normalbelopp,321 +Makedonien (2019),SEK,Breakfast,-48.15 +Makedonien (2019),SEK,Lunch,-112.35 +Makedonien (2019),SEK,Dinner,-112.35 +Makedonien (2020),SEK,Normalbelopp,327 +Makedonien (2020),SEK,Breakfast,-49.05 +Makedonien (2020),SEK,Lunch,-114.45 +Makedonien (2020),SEK,Dinner,-114.45 +Malawi (2018),SEK,Normalbelopp,230 +Malawi (2018),SEK,Breakfast,-34.5 +Malawi (2018),SEK,Lunch,-80.5 +Malawi (2018),SEK,Dinner,-80.5 +Malawi (2019),SEK,Normalbelopp,281 +Malawi (2019),SEK,Breakfast,-42.15 +Malawi (2019),SEK,Lunch,-98.35 +Malawi (2019),SEK,Dinner,-98.35 +Malawi (2020),SEK,Normalbelopp,385 +Malawi (2020),SEK,Breakfast,-57.75 +Malawi (2020),SEK,Lunch,-134.75 +Malawi (2020),SEK,Dinner,-134.75 +Malaysia (2018),SEK,Normalbelopp,274 +Malaysia (2018),SEK,Breakfast,-41.1 +Malaysia (2018),SEK,Lunch,-95.9 +Malaysia (2018),SEK,Dinner,-95.9 +Malaysia (2019),SEK,Normalbelopp,307 +Malaysia (2019),SEK,Breakfast,-46.05 +Malaysia (2019),SEK,Lunch,-107.45 +Malaysia (2019),SEK,Dinner,-107.45 +Malaysia (2020),SEK,Normalbelopp,330 +Malaysia (2020),SEK,Breakfast,-49.5 +Malaysia (2020),SEK,Lunch,-115.5 +Malaysia (2020),SEK,Dinner,-115.5 +Maldiverna (2018),SEK,Normalbelopp,339 +Maldiverna (2018),SEK,Breakfast,-50.85 +Maldiverna (2018),SEK,Lunch,-118.65 +Maldiverna (2018),SEK,Dinner,-118.65 +Maldiverna (2019),SEK,Normalbelopp,393 +Maldiverna (2019),SEK,Breakfast,-58.95 +Maldiverna (2019),SEK,Lunch,-137.55 +Maldiverna (2019),SEK,Dinner,-137.55 +Maldiverna (2020),SEK,Normalbelopp,433 +Maldiverna (2020),SEK,Breakfast,-64.95 +Maldiverna (2020),SEK,Lunch,-151.55 +Maldiverna (2020),SEK,Dinner,-151.55 +Mali (2018),SEK,Normalbelopp,440 +Mali (2018),SEK,Breakfast,-66 +Mali (2018),SEK,Lunch,-154 +Mali (2018),SEK,Dinner,-154 +Mali (2019),SEK,Normalbelopp,450 +Mali (2019),SEK,Breakfast,-67.5 +Mali (2019),SEK,Lunch,-157.5 +Mali (2019),SEK,Dinner,-157.5 +Mali (2020),SEK,Normalbelopp,454 +Mali (2020),SEK,Breakfast,-68.1 +Mali (2020),SEK,Lunch,-158.9 +Mali (2020),SEK,Dinner,-158.9 +Malta (2018),SEK,Normalbelopp,480 +Malta (2018),SEK,Breakfast,-72 +Malta (2018),SEK,Lunch,-168 +Malta (2018),SEK,Dinner,-168 +Malta (2019),SEK,Normalbelopp,510 +Malta (2019),SEK,Breakfast,-76.5 +Malta (2019),SEK,Lunch,-178.5 +Malta (2019),SEK,Dinner,-178.5 +Malta (2020),SEK,Normalbelopp,538 +Malta (2020),SEK,Breakfast,-80.7 +Malta (2020),SEK,Lunch,-188.3 +Malta (2020),SEK,Dinner,-188.3 +Marocko (2018),SEK,Normalbelopp,376 +Marocko (2018),SEK,Breakfast,-56.4 +Marocko (2018),SEK,Lunch,-131.6 +Marocko (2018),SEK,Dinner,-131.6 +Marocko (2019),SEK,Normalbelopp,414 +Marocko (2019),SEK,Breakfast,-62.1 +Marocko (2019),SEK,Lunch,-144.9 +Marocko (2019),SEK,Dinner,-144.9 +Marocko (2020),SEK,Normalbelopp,446 +Marocko (2020),SEK,Breakfast,-66.9 +Marocko (2020),SEK,Lunch,-156.1 +Marocko (2020),SEK,Dinner,-156.1 +Mauretanien (2018),SEK,Normalbelopp,319 +Mauretanien (2018),SEK,Breakfast,-47.85 +Mauretanien (2018),SEK,Lunch,-111.65 +Mauretanien (2018),SEK,Dinner,-111.65 +Mauretanien (2019),SEK,Normalbelopp,340 +Mauretanien (2019),SEK,Breakfast,-51 +Mauretanien (2019),SEK,Lunch,-119 +Mauretanien (2019),SEK,Dinner,-119 +Mauretanien (2020),SEK,Normalbelopp,353 +Mauretanien (2020),SEK,Breakfast,-52.95 +Mauretanien (2020),SEK,Lunch,-123.55 +Mauretanien (2020),SEK,Dinner,-123.55 +Mauritius (2018),SEK,Normalbelopp,399 +Mauritius (2018),SEK,Breakfast,-59.85 +Mauritius (2018),SEK,Lunch,-139.65 +Mauritius (2018),SEK,Dinner,-139.65 +Mauritius (2019),SEK,Normalbelopp,456 +Mauritius (2019),SEK,Breakfast,-68.4 +Mauritius (2019),SEK,Lunch,-159.6 +Mauritius (2019),SEK,Dinner,-159.6 +Mauritius (2020),SEK,Normalbelopp,465 +Mauritius (2020),SEK,Breakfast,-69.75 +Mauritius (2020),SEK,Lunch,-162.75 +Mauritius (2020),SEK,Dinner,-162.75 +Mexiko (2018),SEK,Normalbelopp,293 +Mexiko (2018),SEK,Breakfast,-43.95 +Mexiko (2018),SEK,Lunch,-102.55 +Mexiko (2018),SEK,Dinner,-102.55 +Mexiko (2019),SEK,Normalbelopp,301 +Mexiko (2019),SEK,Breakfast,-45.15 +Mexiko (2019),SEK,Lunch,-105.35 +Mexiko (2019),SEK,Dinner,-105.35 +Mexiko (2020),SEK,Normalbelopp,367 +Mexiko (2020),SEK,Breakfast,-55.05 +Mexiko (2020),SEK,Lunch,-128.45 +Mexiko (2020),SEK,Dinner,-128.45 +Mikronesien (2018),SEK,Normalbelopp,396 +Mikronesien (2018),SEK,Breakfast,-59.4 +Mikronesien (2018),SEK,Lunch,-138.6 +Mikronesien (2018),SEK,Dinner,-138.6 +Mikronesien (2019),SEK,Normalbelopp,443 +Mikronesien (2019),SEK,Breakfast,-66.45 +Mikronesien (2019),SEK,Lunch,-155.05 +Mikronesien (2019),SEK,Dinner,-155.05 +Mikronesien (2020),SEK,Normalbelopp,464 +Mikronesien (2020),SEK,Breakfast,-69.6 +Mikronesien (2020),SEK,Lunch,-162.4 +Mikronesien (2020),SEK,Dinner,-162.4 +Mocambique (2018),SEK,Normalbelopp,255 +Mocambique (2018),SEK,Breakfast,-38.25 +Mocambique (2018),SEK,Lunch,-89.25 +Mocambique (2018),SEK,Dinner,-89.25 +Mocambique (2019),SEK,Normalbelopp,288 +Mocambique (2019),SEK,Breakfast,-43.2 +Mocambique (2019),SEK,Lunch,-100.8 +Mocambique (2019),SEK,Dinner,-100.8 +Mocambique (2020),SEK,Normalbelopp,297 +Mocambique (2020),SEK,Breakfast,-44.55 +Mocambique (2020),SEK,Lunch,-103.95 +Mocambique (2020),SEK,Dinner,-103.95 +Moldavien (2018),SEK,Normalbelopp,230 +Moldavien (2018),SEK,Breakfast,-34.5 +Moldavien (2018),SEK,Lunch,-80.5 +Moldavien (2018),SEK,Dinner,-80.5 +Moldavien (2019),SEK,Normalbelopp,248 +Moldavien (2019),SEK,Breakfast,-37.2 +Moldavien (2019),SEK,Lunch,-86.8 +Moldavien (2019),SEK,Dinner,-86.8 +Moldavien (2020),SEK,Normalbelopp,285 +Moldavien (2020),SEK,Breakfast,-42.75 +Moldavien (2020),SEK,Lunch,-99.75 +Moldavien (2020),SEK,Dinner,-99.75 +Monaco (2018),SEK,Normalbelopp,818 +Monaco (2018),SEK,Breakfast,-122.7 +Monaco (2018),SEK,Lunch,-286.3 +Monaco (2018),SEK,Dinner,-286.3 +Monaco (2019),SEK,Normalbelopp,858 +Monaco (2019),SEK,Breakfast,-128.7 +Monaco (2019),SEK,Lunch,-300.3 +Monaco (2019),SEK,Dinner,-300.3 +Monaco (2020),SEK,Normalbelopp,906 +Monaco (2020),SEK,Breakfast,-135.9 +Monaco (2020),SEK,Lunch,-317.1 +Monaco (2020),SEK,Dinner,-317.1 +Mongoliet (2018),SEK,Normalbelopp,230 +Mongoliet (2018),SEK,Breakfast,-34.5 +Mongoliet (2018),SEK,Lunch,-80.5 +Mongoliet (2018),SEK,Dinner,-80.5 +Mongoliet (2019),SEK,Normalbelopp,280 +Mongoliet (2019),SEK,Breakfast,-42 +Mongoliet (2019),SEK,Lunch,-98 +Mongoliet (2019),SEK,Dinner,-98 +Mongoliet (2020),SEK,Normalbelopp,283 +Mongoliet (2020),SEK,Breakfast,-42.45 +Mongoliet (2020),SEK,Lunch,-99.05 +Mongoliet (2020),SEK,Dinner,-99.05 +Montenegro (2018),SEK,Normalbelopp,393 +Montenegro (2018),SEK,Breakfast,-58.95 +Montenegro (2018),SEK,Lunch,-137.55 +Montenegro (2018),SEK,Dinner,-137.55 +Montenegro (2019),SEK,Normalbelopp,423 +Montenegro (2019),SEK,Breakfast,-63.45 +Montenegro (2019),SEK,Lunch,-148.05 +Montenegro (2019),SEK,Dinner,-148.05 +Montenegro (2020),SEK,Normalbelopp,391 +Montenegro (2020),SEK,Breakfast,-58.65 +Montenegro (2020),SEK,Lunch,-136.85 +Montenegro (2020),SEK,Dinner,-136.85 +Myanmar (2018),SEK,Normalbelopp,315 +Myanmar (2018),SEK,Breakfast,-47.25 +Myanmar (2018),SEK,Lunch,-110.25 +Myanmar (2018),SEK,Dinner,-110.25 +Myanmar (2019),SEK,Normalbelopp,286 +Myanmar (2019),SEK,Breakfast,-42.9 +Myanmar (2019),SEK,Lunch,-100.1 +Myanmar (2019),SEK,Dinner,-100.1 +Myanmar (2020),SEK,Normalbelopp,318 +Myanmar (2020),SEK,Breakfast,-47.7 +Myanmar (2020),SEK,Lunch,-111.3 +Myanmar (2020),SEK,Dinner,-111.3 +Nederländerna (2018),SEK,Normalbelopp,554 +Nederländerna (2018),SEK,Breakfast,-83.1 +Nederländerna (2018),SEK,Lunch,-193.9 +Nederländerna (2018),SEK,Dinner,-193.9 +Nederländerna (2019),SEK,Normalbelopp,610 +Nederländerna (2019),SEK,Breakfast,-91.5 +Nederländerna (2019),SEK,Lunch,-213.5 +Nederländerna (2019),SEK,Dinner,-213.5 +Nederländerna (2020),SEK,Normalbelopp,648 +Nederländerna (2020),SEK,Breakfast,-97.2 +Nederländerna (2020),SEK,Lunch,-226.8 +Nederländerna (2020),SEK,Dinner,-226.8 +Nederländska Antillerna (2018),SEK,Normalbelopp,490 +Nederländska Antillerna (2018),SEK,Breakfast,-73.5 +Nederländska Antillerna (2018),SEK,Lunch,-171.5 +Nederländska Antillerna (2018),SEK,Dinner,-171.5 +Nederländska Antillerna (2019),SEK,Normalbelopp,544 +Nederländska Antillerna (2019),SEK,Breakfast,-81.6 +Nederländska Antillerna (2019),SEK,Lunch,-190.4 +Nederländska Antillerna (2019),SEK,Dinner,-190.4 +Nederländska Antillerna (2020),SEK,Normalbelopp,601 +Nederländska Antillerna (2020),SEK,Breakfast,-90.15 +Nederländska Antillerna (2020),SEK,Lunch,-210.35 +Nederländska Antillerna (2020),SEK,Dinner,-210.35 +Nepal (2018),SEK,Normalbelopp,268 +Nepal (2018),SEK,Breakfast,-40.2 +Nepal (2018),SEK,Lunch,-93.8 +Nepal (2018),SEK,Dinner,-93.8 +Nepal (2019),SEK,Normalbelopp,258 +Nepal (2019),SEK,Breakfast,-38.7 +Nepal (2019),SEK,Lunch,-90.3 +Nepal (2019),SEK,Dinner,-90.3 +Nepal (2020),SEK,Normalbelopp,287 +Nepal (2020),SEK,Breakfast,-43.05 +Nepal (2020),SEK,Lunch,-100.45 +Nepal (2020),SEK,Dinner,-100.45 +Nicaragua (2018),SEK,Normalbelopp,328 +Nicaragua (2018),SEK,Breakfast,-49.2 +Nicaragua (2018),SEK,Lunch,-114.8 +Nicaragua (2018),SEK,Dinner,-114.8 +Nicaragua (2019),SEK,Normalbelopp,334 +Nicaragua (2019),SEK,Breakfast,-50.1 +Nicaragua (2019),SEK,Lunch,-116.9 +Nicaragua (2019),SEK,Dinner,-116.9 +Nicaragua (2020),SEK,Normalbelopp,367 +Nicaragua (2020),SEK,Breakfast,-55.05 +Nicaragua (2020),SEK,Lunch,-128.45 +Nicaragua (2020),SEK,Dinner,-128.45 +Niger (2018),SEK,Normalbelopp,306 +Niger (2018),SEK,Breakfast,-45.9 +Niger (2018),SEK,Lunch,-107.1 +Niger (2018),SEK,Dinner,-107.1 +Niger (2019),SEK,Normalbelopp,339 +Niger (2019),SEK,Breakfast,-50.85 +Niger (2019),SEK,Lunch,-118.65 +Niger (2019),SEK,Dinner,-118.65 +Niger (2020),SEK,Normalbelopp,354 +Niger (2020),SEK,Breakfast,-53.1 +Niger (2020),SEK,Lunch,-123.9 +Niger (2020),SEK,Dinner,-123.9 +Nigeria (2018),SEK,Normalbelopp,418 +Nigeria (2018),SEK,Breakfast,-62.7 +Nigeria (2018),SEK,Lunch,-146.3 +Nigeria (2018),SEK,Dinner,-146.3 +Nigeria (2019),SEK,Normalbelopp,501 +Nigeria (2019),SEK,Breakfast,-75.15 +Nigeria (2019),SEK,Lunch,-175.35 +Nigeria (2019),SEK,Dinner,-175.35 +Nigeria (2020),SEK,Normalbelopp,549 +Nigeria (2020),SEK,Breakfast,-82.35 +Nigeria (2020),SEK,Lunch,-192.15 +Nigeria (2020),SEK,Dinner,-192.15 +Nordmakedonien - f.d. Makedonien (2020),SEK,Normalbelopp,327 +Nordmakedonien - f.d. Makedonien (2020),SEK,Breakfast,-49.05 +Nordmakedonien - f.d. Makedonien (2020),SEK,Lunch,-114.45 +Nordmakedonien - f.d. Makedonien (2020),SEK,Dinner,-114.45 +Norge (2018),SEK,Normalbelopp,823 +Norge (2018),SEK,Breakfast,-123.45 +Norge (2018),SEK,Lunch,-288.05 +Norge (2018),SEK,Dinner,-288.05 +Norge (2019),SEK,Normalbelopp,876 +Norge (2019),SEK,Breakfast,-131.4 +Norge (2019),SEK,Lunch,-306.6 +Norge (2019),SEK,Dinner,-306.6 +Norge (2020),SEK,Normalbelopp,893 +Norge (2020),SEK,Breakfast,-133.95 +Norge (2020),SEK,Lunch,-312.55 +Norge (2020),SEK,Dinner,-312.55 +Nya Zeeland (2018),SEK,Normalbelopp,538 +Nya Zeeland (2018),SEK,Breakfast,-80.7 +Nya Zeeland (2018),SEK,Lunch,-188.3 +Nya Zeeland (2018),SEK,Dinner,-188.3 +Nya Zeeland (2019),SEK,Normalbelopp,565 +Nya Zeeland (2019),SEK,Breakfast,-84.75 +Nya Zeeland (2019),SEK,Lunch,-197.75 +Nya Zeeland (2019),SEK,Dinner,-197.75 +Nya Zeeland (2020),SEK,Normalbelopp,585 +Nya Zeeland (2020),SEK,Breakfast,-87.75 +Nya Zeeland (2020),SEK,Lunch,-204.75 +Nya Zeeland (2020),SEK,Dinner,-204.75 +Oman (2018),SEK,Normalbelopp,685 +Oman (2018),SEK,Breakfast,-102.75 +Oman (2018),SEK,Lunch,-239.75 +Oman (2018),SEK,Dinner,-239.75 +Oman (2019),SEK,Normalbelopp,748 +Oman (2019),SEK,Breakfast,-112.2 +Oman (2019),SEK,Lunch,-261.8 +Oman (2019),SEK,Dinner,-261.8 +Oman (2020),SEK,Normalbelopp,777 +Oman (2020),SEK,Breakfast,-116.55 +Oman (2020),SEK,Lunch,-271.95 +Oman (2020),SEK,Dinner,-271.95 +Österrike (2018),SEK,Normalbelopp,583 +Österrike (2018),SEK,Breakfast,-87.45 +Österrike (2018),SEK,Lunch,-204.05 +Österrike (2018),SEK,Dinner,-204.05 +Österrike (2019),SEK,Normalbelopp,616 +Österrike (2019),SEK,Breakfast,-92.4 +Österrike (2019),SEK,Lunch,-215.6 +Österrike (2019),SEK,Dinner,-215.6 +Österrike (2020),SEK,Normalbelopp,641 +Österrike (2020),SEK,Breakfast,-96.15 +Österrike (2020),SEK,Lunch,-224.35 +Österrike (2020),SEK,Dinner,-224.35 +Övriga länder och områden (2018),SEK,Normalbelopp,357 +Övriga länder och områden (2018),SEK,Breakfast,-53.55 +Övriga länder och områden (2018),SEK,Lunch,-124.95 +Övriga länder och områden (2018),SEK,Dinner,-124.95 +Övriga länder och områden (2019),SEK,Normalbelopp,369 +Övriga länder och områden (2019),SEK,Breakfast,-55.35 +Övriga länder och områden (2019),SEK,Lunch,-129.15 +Övriga länder och områden (2019),SEK,Dinner,-129.15 +Övriga länder och områden (2020),SEK,Normalbelopp,392 +Övriga länder och områden (2020),SEK,Breakfast,-58.8 +Övriga länder och områden (2020),SEK,Lunch,-137.2 +Övriga länder och områden (2020),SEK,Dinner,-137.2 +Pakistan (2018),SEK,Normalbelopp,296 +Pakistan (2018),SEK,Breakfast,-44.4 +Pakistan (2018),SEK,Lunch,-103.6 +Pakistan (2018),SEK,Dinner,-103.6 +Pakistan (2019),SEK,Normalbelopp,230 +Pakistan (2019),SEK,Breakfast,-34.5 +Pakistan (2019),SEK,Lunch,-80.5 +Pakistan (2019),SEK,Dinner,-80.5 +Pakistan (2020),SEK,Normalbelopp,240 +Pakistan (2020),SEK,Breakfast,-36 +Pakistan (2020),SEK,Lunch,-84 +Pakistan (2020),SEK,Dinner,-84 +Panama (2018),SEK,Normalbelopp,444 +Panama (2018),SEK,Breakfast,-66.6 +Panama (2018),SEK,Lunch,-155.4 +Panama (2018),SEK,Dinner,-155.4 +Panama (2019),SEK,Normalbelopp,505 +Panama (2019),SEK,Breakfast,-75.75 +Panama (2019),SEK,Lunch,-176.75 +Panama (2019),SEK,Dinner,-176.75 +Panama (2020),SEK,Normalbelopp,564 +Panama (2020),SEK,Breakfast,-84.6 +Panama (2020),SEK,Lunch,-197.4 +Panama (2020),SEK,Dinner,-197.4 +Papua Nya Guinea (2018),SEK,Normalbelopp,452 +Papua Nya Guinea (2018),SEK,Breakfast,-67.8 +Papua Nya Guinea (2018),SEK,Lunch,-158.2 +Papua Nya Guinea (2018),SEK,Dinner,-158.2 +Papua Nya Guinea (2019),SEK,Normalbelopp,487 +Papua Nya Guinea (2019),SEK,Breakfast,-73.05 +Papua Nya Guinea (2019),SEK,Lunch,-170.45 +Papua Nya Guinea (2019),SEK,Dinner,-170.45 +Papua Nya Guinea (2020),SEK,Normalbelopp,515 +Papua Nya Guinea (2020),SEK,Breakfast,-77.25 +Papua Nya Guinea (2020),SEK,Lunch,-180.25 +Papua Nya Guinea (2020),SEK,Dinner,-180.25 +Paraguay (2018),SEK,Normalbelopp,298 +Paraguay (2018),SEK,Breakfast,-44.7 +Paraguay (2018),SEK,Lunch,-104.3 +Paraguay (2018),SEK,Dinner,-104.3 +Paraguay (2019),SEK,Normalbelopp,316 +Paraguay (2019),SEK,Breakfast,-47.4 +Paraguay (2019),SEK,Lunch,-110.6 +Paraguay (2019),SEK,Dinner,-110.6 +Paraguay (2020),SEK,Normalbelopp,314 +Paraguay (2020),SEK,Breakfast,-47.1 +Paraguay (2020),SEK,Lunch,-109.9 +Paraguay (2020),SEK,Dinner,-109.9 +Peru (2018),SEK,Normalbelopp,457 +Peru (2018),SEK,Breakfast,-68.55 +Peru (2018),SEK,Lunch,-159.95 +Peru (2018),SEK,Dinner,-159.95 +Peru (2019),SEK,Normalbelopp,481 +Peru (2019),SEK,Breakfast,-72.15 +Peru (2019),SEK,Lunch,-168.35 +Peru (2019),SEK,Dinner,-168.35 +Peru (2020),SEK,Normalbelopp,511 +Peru (2020),SEK,Breakfast,-76.65 +Peru (2020),SEK,Lunch,-178.85 +Peru (2020),SEK,Dinner,-178.85 +Polen (2018),SEK,Normalbelopp,411 +Polen (2018),SEK,Breakfast,-61.65 +Polen (2018),SEK,Lunch,-143.85 +Polen (2018),SEK,Dinner,-143.85 +Polen (2019),SEK,Normalbelopp,436 +Polen (2019),SEK,Breakfast,-65.4 +Polen (2019),SEK,Lunch,-152.6 +Polen (2019),SEK,Dinner,-152.6 +Polen (2020),SEK,Normalbelopp,461 +Polen (2020),SEK,Breakfast,-69.15 +Polen (2020),SEK,Lunch,-161.35 +Polen (2020),SEK,Dinner,-161.35 +Portugal (2018),SEK,Normalbelopp,432 +Portugal (2018),SEK,Breakfast,-64.8 +Portugal (2018),SEK,Lunch,-151.2 +Portugal (2018),SEK,Dinner,-151.2 +Portugal (2019),SEK,Normalbelopp,461 +Portugal (2019),SEK,Breakfast,-69.15 +Portugal (2019),SEK,Lunch,-161.35 +Portugal (2019),SEK,Dinner,-161.35 +Portugal (2020),SEK,Normalbelopp,486 +Portugal (2020),SEK,Breakfast,-72.9 +Portugal (2020),SEK,Lunch,-170.1 +Portugal (2020),SEK,Dinner,-170.1 +Puerto Rico (2018),SEK,Normalbelopp,689 +Puerto Rico (2018),SEK,Breakfast,-103.35 +Puerto Rico (2018),SEK,Lunch,-241.15 +Puerto Rico (2018),SEK,Dinner,-241.15 +Puerto Rico (2019),SEK,Normalbelopp,762 +Puerto Rico (2019),SEK,Breakfast,-114.3 +Puerto Rico (2019),SEK,Lunch,-266.7 +Puerto Rico (2019),SEK,Dinner,-266.7 +Puerto Rico (2020),SEK,Normalbelopp,851 +Puerto Rico (2020),SEK,Breakfast,-127.65 +Puerto Rico (2020),SEK,Lunch,-297.85 +Puerto Rico (2020),SEK,Dinner,-297.85 +Qatar (2018),SEK,Normalbelopp,645 +Qatar (2018),SEK,Breakfast,-96.75 +Qatar (2018),SEK,Lunch,-225.75 +Qatar (2018),SEK,Dinner,-225.75 +Qatar (2019),SEK,Normalbelopp,716 +Qatar (2019),SEK,Breakfast,-107.4 +Qatar (2019),SEK,Lunch,-250.6 +Qatar (2019),SEK,Dinner,-250.6 +Qatar (2020),SEK,Normalbelopp,766 +Qatar (2020),SEK,Breakfast,-114.9 +Qatar (2020),SEK,Lunch,-268.1 +Qatar (2020),SEK,Dinner,-268.1 +Rumänien (2018),SEK,Normalbelopp,312 +Rumänien (2018),SEK,Breakfast,-46.8 +Rumänien (2018),SEK,Lunch,-109.2 +Rumänien (2018),SEK,Dinner,-109.2 +Rumänien (2019),SEK,Normalbelopp,332 +Rumänien (2019),SEK,Breakfast,-49.8 +Rumänien (2019),SEK,Lunch,-116.2 +Rumänien (2019),SEK,Dinner,-116.2 +Rumänien (2020),SEK,Normalbelopp,337 +Rumänien (2020),SEK,Breakfast,-50.55 +Rumänien (2020),SEK,Lunch,-117.95 +Rumänien (2020),SEK,Dinner,-117.95 +Rwanda (2018),SEK,Normalbelopp,296 +Rwanda (2018),SEK,Breakfast,-44.4 +Rwanda (2018),SEK,Lunch,-103.6 +Rwanda (2018),SEK,Dinner,-103.6 +Rwanda (2019),SEK,Normalbelopp,333 +Rwanda (2019),SEK,Breakfast,-49.95 +Rwanda (2019),SEK,Lunch,-116.55 +Rwanda (2019),SEK,Dinner,-116.55 +Rwanda (2020),SEK,Normalbelopp,332 +Rwanda (2020),SEK,Breakfast,-49.8 +Rwanda (2020),SEK,Lunch,-116.2 +Rwanda (2020),SEK,Dinner,-116.2 +Ryssland (2018),SEK,Normalbelopp,470 +Ryssland (2018),SEK,Breakfast,-70.5 +Ryssland (2018),SEK,Lunch,-164.5 +Ryssland (2018),SEK,Dinner,-164.5 +Ryssland (2019),SEK,Normalbelopp,455 +Ryssland (2019),SEK,Breakfast,-68.25 +Ryssland (2019),SEK,Lunch,-159.25 +Ryssland (2019),SEK,Dinner,-159.25 +Ryssland (2020),SEK,Normalbelopp,520 +Ryssland (2020),SEK,Breakfast,-78 +Ryssland (2020),SEK,Lunch,-182 +Ryssland (2020),SEK,Dinner,-182 +Saint Lucia (2018),SEK,Normalbelopp,524 +Saint Lucia (2018),SEK,Breakfast,-78.6 +Saint Lucia (2018),SEK,Lunch,-183.4 +Saint Lucia (2018),SEK,Dinner,-183.4 +Saint Lucia (2019),SEK,Normalbelopp,555 +Saint Lucia (2019),SEK,Breakfast,-83.25 +Saint Lucia (2019),SEK,Lunch,-194.25 +Saint Lucia (2019),SEK,Dinner,-194.25 +Saint Lucia (2020),SEK,Normalbelopp,573 +Saint Lucia (2020),SEK,Breakfast,-85.95 +Saint Lucia (2020),SEK,Lunch,-200.55 +Saint Lucia (2020),SEK,Dinner,-200.55 +Saint Vincent och Grenadinerna (2018),SEK,Normalbelopp,460 +Saint Vincent och Grenadinerna (2018),SEK,Breakfast,-69 +Saint Vincent och Grenadinerna (2018),SEK,Lunch,-161 +Saint Vincent och Grenadinerna (2018),SEK,Dinner,-161 +Saint Vincent och Grenadinerna (2019),SEK,Normalbelopp,502 +Saint Vincent och Grenadinerna (2019),SEK,Breakfast,-75.3 +Saint Vincent och Grenadinerna (2019),SEK,Lunch,-175.7 +Saint Vincent och Grenadinerna (2019),SEK,Dinner,-175.7 +Saint Vincent och Grenadinerna (2020),SEK,Normalbelopp,516 +Saint Vincent och Grenadinerna (2020),SEK,Breakfast,-77.4 +Saint Vincent och Grenadinerna (2020),SEK,Lunch,-180.6 +Saint Vincent och Grenadinerna (2020),SEK,Dinner,-180.6 +Samoa - Självständiga staten (2018),SEK,Normalbelopp,479 +Samoa - Självständiga staten (2018),SEK,Breakfast,-71.85 +Samoa - Självständiga staten (2018),SEK,Lunch,-167.65 +Samoa - Självständiga staten (2018),SEK,Dinner,-167.65 +Samoa - Självständiga staten (2019),SEK,Normalbelopp,513 +Samoa - Självständiga staten (2019),SEK,Breakfast,-76.95 +Samoa - Självständiga staten (2019),SEK,Lunch,-179.55 +Samoa - Självständiga staten (2019),SEK,Dinner,-179.55 +Samoa - Självständiga staten (2020),SEK,Normalbelopp,572 +Samoa - Självständiga staten (2020),SEK,Breakfast,-85.8 +Samoa - Självständiga staten (2020),SEK,Lunch,-200.2 +Samoa - Självständiga staten (2020),SEK,Dinner,-200.2 +San Marino (2018),SEK,Normalbelopp,540 +San Marino (2018),SEK,Breakfast,-81 +San Marino (2018),SEK,Lunch,-189 +San Marino (2018),SEK,Dinner,-189 +San Marino (2019),SEK,Normalbelopp,590 +San Marino (2019),SEK,Breakfast,-88.5 +San Marino (2019),SEK,Lunch,-206.5 +San Marino (2019),SEK,Dinner,-206.5 +San Marino (2020),SEK,Normalbelopp,627 +San Marino (2020),SEK,Breakfast,-94.05 +San Marino (2020),SEK,Lunch,-219.45 +San Marino (2020),SEK,Dinner,-219.45 +Saudiarabien (2018),SEK,Normalbelopp,568 +Saudiarabien (2018),SEK,Breakfast,-85.2 +Saudiarabien (2018),SEK,Lunch,-198.8 +Saudiarabien (2018),SEK,Dinner,-198.8 +Saudiarabien (2019),SEK,Normalbelopp,652 +Saudiarabien (2019),SEK,Breakfast,-97.8 +Saudiarabien (2019),SEK,Lunch,-228.2 +Saudiarabien (2019),SEK,Dinner,-228.2 +Saudiarabien (2020),SEK,Normalbelopp,742 +Saudiarabien (2020),SEK,Breakfast,-111.3 +Saudiarabien (2020),SEK,Lunch,-259.7 +Saudiarabien (2020),SEK,Dinner,-259.7 +Schweiz (2018),SEK,Normalbelopp,819 +Schweiz (2018),SEK,Breakfast,-122.85 +Schweiz (2018),SEK,Lunch,-286.65 +Schweiz (2018),SEK,Dinner,-286.65 +Schweiz (2019),SEK,Normalbelopp,888 +Schweiz (2019),SEK,Breakfast,-133.2 +Schweiz (2019),SEK,Lunch,-310.8 +Schweiz (2019),SEK,Dinner,-310.8 +Schweiz (2020),SEK,Normalbelopp,979 +Schweiz (2020),SEK,Breakfast,-146.85 +Schweiz (2020),SEK,Lunch,-342.65 +Schweiz (2020),SEK,Dinner,-342.65 +Senegal (2018),SEK,Normalbelopp,511 +Senegal (2018),SEK,Breakfast,-76.65 +Senegal (2018),SEK,Lunch,-178.85 +Senegal (2018),SEK,Dinner,-178.85 +Senegal (2019),SEK,Normalbelopp,539 +Senegal (2019),SEK,Breakfast,-80.85 +Senegal (2019),SEK,Lunch,-188.65 +Senegal (2019),SEK,Dinner,-188.65 +Senegal (2020),SEK,Normalbelopp,563 +Senegal (2020),SEK,Breakfast,-84.45 +Senegal (2020),SEK,Lunch,-197.05 +Senegal (2020),SEK,Dinner,-197.05 +Serbien (2018),SEK,Normalbelopp,362 +Serbien (2018),SEK,Breakfast,-54.3 +Serbien (2018),SEK,Lunch,-126.7 +Serbien (2018),SEK,Dinner,-126.7 +Serbien (2019),SEK,Normalbelopp,388 +Serbien (2019),SEK,Breakfast,-58.2 +Serbien (2019),SEK,Lunch,-135.8 +Serbien (2019),SEK,Dinner,-135.8 +Serbien (2020),SEK,Normalbelopp,395 +Serbien (2020),SEK,Breakfast,-59.25 +Serbien (2020),SEK,Lunch,-138.25 +Serbien (2020),SEK,Dinner,-138.25 +Seychellerna (2018),SEK,Normalbelopp,542 +Seychellerna (2018),SEK,Breakfast,-81.3 +Seychellerna (2018),SEK,Lunch,-189.7 +Seychellerna (2018),SEK,Dinner,-189.7 +Seychellerna (2019),SEK,Normalbelopp,620 +Seychellerna (2019),SEK,Breakfast,-93 +Seychellerna (2019),SEK,Lunch,-217 +Seychellerna (2019),SEK,Dinner,-217 +Seychellerna (2020),SEK,Normalbelopp,696 +Seychellerna (2020),SEK,Breakfast,-104.4 +Seychellerna (2020),SEK,Lunch,-243.6 +Seychellerna (2020),SEK,Dinner,-243.6 +Sierra Leone (2018),SEK,Normalbelopp,287 +Sierra Leone (2018),SEK,Breakfast,-43.05 +Sierra Leone (2018),SEK,Lunch,-100.45 +Sierra Leone (2018),SEK,Dinner,-100.45 +Sierra Leone (2019),SEK,Normalbelopp,328 +Sierra Leone (2019),SEK,Breakfast,-49.2 +Sierra Leone (2019),SEK,Lunch,-114.8 +Sierra Leone (2019),SEK,Dinner,-114.8 +Sierra Leone (2020),SEK,Normalbelopp,351 +Sierra Leone (2020),SEK,Breakfast,-52.65 +Sierra Leone (2020),SEK,Lunch,-122.85 +Sierra Leone (2020),SEK,Dinner,-122.85 +Singapore (2018),SEK,Normalbelopp,576 +Singapore (2018),SEK,Breakfast,-86.4 +Singapore (2018),SEK,Lunch,-201.6 +Singapore (2018),SEK,Dinner,-201.6 +Singapore (2019),SEK,Normalbelopp,632 +Singapore (2019),SEK,Breakfast,-94.8 +Singapore (2019),SEK,Lunch,-221.2 +Singapore (2019),SEK,Dinner,-221.2 +Singapore (2020),SEK,Normalbelopp,690 +Singapore (2020),SEK,Breakfast,-103.5 +Singapore (2020),SEK,Lunch,-241.5 +Singapore (2020),SEK,Dinner,-241.5 +Slovakien (2018),SEK,Normalbelopp,451 +Slovakien (2018),SEK,Breakfast,-67.65 +Slovakien (2018),SEK,Lunch,-157.85 +Slovakien (2018),SEK,Dinner,-157.85 +Slovakien (2019),SEK,Normalbelopp,479 +Slovakien (2019),SEK,Breakfast,-71.85 +Slovakien (2019),SEK,Lunch,-167.65 +Slovakien (2019),SEK,Dinner,-167.65 +Slovakien (2020),SEK,Normalbelopp,519 +Slovakien (2020),SEK,Breakfast,-77.85 +Slovakien (2020),SEK,Lunch,-181.65 +Slovakien (2020),SEK,Dinner,-181.65 +Slovenien (2018),SEK,Normalbelopp,463 +Slovenien (2018),SEK,Breakfast,-69.45 +Slovenien (2018),SEK,Lunch,-162.05 +Slovenien (2018),SEK,Dinner,-162.05 +Slovenien (2019),SEK,Normalbelopp,503 +Slovenien (2019),SEK,Breakfast,-75.45 +Slovenien (2019),SEK,Lunch,-176.05 +Slovenien (2019),SEK,Dinner,-176.05 +Slovenien (2020),SEK,Normalbelopp,529 +Slovenien (2020),SEK,Breakfast,-79.35 +Slovenien (2020),SEK,Lunch,-185.15 +Slovenien (2020),SEK,Dinner,-185.15 +Spanien (2018),SEK,Normalbelopp,513 +Spanien (2018),SEK,Breakfast,-76.95 +Spanien (2018),SEK,Lunch,-179.55 +Spanien (2018),SEK,Dinner,-179.55 +Spanien (2019),SEK,Normalbelopp,558 +Spanien (2019),SEK,Breakfast,-83.7 +Spanien (2019),SEK,Lunch,-195.3 +Spanien (2019),SEK,Dinner,-195.3 +Spanien (2020),SEK,Normalbelopp,615 +Spanien (2020),SEK,Breakfast,-92.25 +Spanien (2020),SEK,Lunch,-215.25 +Spanien (2020),SEK,Dinner,-215.25 +Sri Lanka (2018),SEK,Normalbelopp,283 +Sri Lanka (2018),SEK,Breakfast,-42.45 +Sri Lanka (2018),SEK,Lunch,-99.05 +Sri Lanka (2018),SEK,Dinner,-99.05 +Sri Lanka (2019),SEK,Normalbelopp,263 +Sri Lanka (2019),SEK,Breakfast,-39.45 +Sri Lanka (2019),SEK,Lunch,-92.05 +Sri Lanka (2019),SEK,Dinner,-92.05 +Sri Lanka (2020),SEK,Normalbelopp,272 +Sri Lanka (2020),SEK,Breakfast,-40.8 +Sri Lanka (2020),SEK,Lunch,-95.2 +Sri Lanka (2020),SEK,Dinner,-95.2 +Storbritannien och Nordirland (2018),SEK,Normalbelopp,655 +Storbritannien och Nordirland (2018),SEK,Breakfast,-98.25 +Storbritannien och Nordirland (2018),SEK,Lunch,-229.25 +Storbritannien och Nordirland (2018),SEK,Dinner,-229.25 +Storbritannien och Nordirland (2019),SEK,Normalbelopp,689 +Storbritannien och Nordirland (2019),SEK,Breakfast,-103.35 +Storbritannien och Nordirland (2019),SEK,Lunch,-241.15 +Storbritannien och Nordirland (2019),SEK,Dinner,-241.15 +Storbritannien och Nordirland (2020),SEK,Normalbelopp,746 +Storbritannien och Nordirland (2020),SEK,Breakfast,-111.9 +Storbritannien och Nordirland (2020),SEK,Lunch,-261.1 +Storbritannien och Nordirland (2020),SEK,Dinner,-261.1 +Sudan (2018),SEK,Normalbelopp,507 +Sudan (2018),SEK,Breakfast,-76.05 +Sudan (2018),SEK,Lunch,-177.45 +Sudan (2018),SEK,Dinner,-177.45 +Sudan (2019),SEK,Normalbelopp,230 +Sudan (2019),SEK,Breakfast,-34.5 +Sudan (2019),SEK,Lunch,-80.5 +Sudan (2019),SEK,Dinner,-80.5 +Sudan (2020),SEK,Normalbelopp,240 +Sudan (2020),SEK,Breakfast,-36 +Sudan (2020),SEK,Lunch,-84 +Sudan (2020),SEK,Dinner,-84 +Swaziland (2018),SEK,Normalbelopp,230 +Swaziland (2018),SEK,Breakfast,-34.5 +Swaziland (2018),SEK,Lunch,-80.5 +Swaziland (2018),SEK,Dinner,-80.5 +Swaziland (2019),SEK,Normalbelopp,230 +Swaziland (2019),SEK,Breakfast,-34.5 +Swaziland (2019),SEK,Lunch,-80.5 +Swaziland (2019),SEK,Dinner,-80.5 +Swaziland (2020),SEK,Normalbelopp,240 +Swaziland (2020),SEK,Breakfast,-36 +Swaziland (2020),SEK,Lunch,-84 +Swaziland (2020),SEK,Dinner,-84 +Sydafrika (2018),SEK,Normalbelopp,249 +Sydafrika (2018),SEK,Breakfast,-37.35 +Sydafrika (2018),SEK,Lunch,-87.15 +Sydafrika (2018),SEK,Dinner,-87.15 +Sydafrika (2019),SEK,Normalbelopp,262 +Sydafrika (2019),SEK,Breakfast,-39.3 +Sydafrika (2019),SEK,Lunch,-91.7 +Sydafrika (2019),SEK,Dinner,-91.7 +Sydafrika (2020),SEK,Normalbelopp,286 +Sydafrika (2020),SEK,Breakfast,-42.9 +Sydafrika (2020),SEK,Lunch,-100.1 +Sydafrika (2020),SEK,Dinner,-100.1 +Sydkorea (2018),SEK,Normalbelopp,624 +Sydkorea (2018),SEK,Breakfast,-93.6 +Sydkorea (2018),SEK,Lunch,-218.4 +Sydkorea (2018),SEK,Dinner,-218.4 +Sydkorea (2019),SEK,Normalbelopp,675 +Sydkorea (2019),SEK,Breakfast,-101.25 +Sydkorea (2019),SEK,Lunch,-236.25 +Sydkorea (2019),SEK,Dinner,-236.25 +Sydkorea (2020),SEK,Normalbelopp,696 +Sydkorea (2020),SEK,Breakfast,-104.4 +Sydkorea (2020),SEK,Lunch,-243.6 +Sydkorea (2020),SEK,Dinner,-243.6 +Taiwan (2018),SEK,Normalbelopp,465 +Taiwan (2018),SEK,Breakfast,-69.75 +Taiwan (2018),SEK,Lunch,-162.75 +Taiwan (2018),SEK,Dinner,-162.75 +Taiwan (2019),SEK,Normalbelopp,505 +Taiwan (2019),SEK,Breakfast,-75.75 +Taiwan (2019),SEK,Lunch,-176.75 +Taiwan (2019),SEK,Dinner,-176.75 +Taiwan (2020),SEK,Normalbelopp,546 +Taiwan (2020),SEK,Breakfast,-81.9 +Taiwan (2020),SEK,Lunch,-191.1 +Taiwan (2020),SEK,Dinner,-191.1 +Tanzania (2018),SEK,Normalbelopp,328 +Tanzania (2018),SEK,Breakfast,-49.2 +Tanzania (2018),SEK,Lunch,-114.8 +Tanzania (2018),SEK,Dinner,-114.8 +Tanzania (2019),SEK,Normalbelopp,349 +Tanzania (2019),SEK,Breakfast,-52.35 +Tanzania (2019),SEK,Lunch,-122.15 +Tanzania (2019),SEK,Dinner,-122.15 +Tanzania (2020),SEK,Normalbelopp,360 +Tanzania (2020),SEK,Breakfast,-54 +Tanzania (2020),SEK,Lunch,-126 +Tanzania (2020),SEK,Dinner,-126 +Thailand (2018),SEK,Normalbelopp,475 +Thailand (2018),SEK,Breakfast,-71.25 +Thailand (2018),SEK,Lunch,-166.25 +Thailand (2018),SEK,Dinner,-166.25 +Thailand (2019),SEK,Normalbelopp,525 +Thailand (2019),SEK,Breakfast,-78.75 +Thailand (2019),SEK,Lunch,-183.75 +Thailand (2019),SEK,Dinner,-183.75 +Thailand (2020),SEK,Normalbelopp,617 +Thailand (2020),SEK,Breakfast,-92.55 +Thailand (2020),SEK,Lunch,-215.95 +Thailand (2020),SEK,Dinner,-215.95 +Tjeckien (2018),SEK,Normalbelopp,388 +Tjeckien (2018),SEK,Breakfast,-58.2 +Tjeckien (2018),SEK,Lunch,-135.8 +Tjeckien (2018),SEK,Dinner,-135.8 +Tjeckien (2019),SEK,Normalbelopp,417 +Tjeckien (2019),SEK,Breakfast,-62.55 +Tjeckien (2019),SEK,Lunch,-145.95 +Tjeckien (2019),SEK,Dinner,-145.95 +Tjeckien (2020),SEK,Normalbelopp,436 +Tjeckien (2020),SEK,Breakfast,-65.4 +Tjeckien (2020),SEK,Lunch,-152.6 +Tjeckien (2020),SEK,Dinner,-152.6 +Togo (2018),SEK,Normalbelopp,438 +Togo (2018),SEK,Breakfast,-65.7 +Togo (2018),SEK,Lunch,-153.3 +Togo (2018),SEK,Dinner,-153.3 +Togo (2019),SEK,Normalbelopp,457 +Togo (2019),SEK,Breakfast,-68.55 +Togo (2019),SEK,Lunch,-159.95 +Togo (2019),SEK,Dinner,-159.95 +Togo (2020),SEK,Normalbelopp,482 +Togo (2020),SEK,Breakfast,-72.3 +Togo (2020),SEK,Lunch,-168.7 +Togo (2020),SEK,Dinner,-168.7 +Tonga (2018),SEK,Normalbelopp,380 +Tonga (2018),SEK,Breakfast,-57 +Tonga (2018),SEK,Lunch,-133 +Tonga (2018),SEK,Dinner,-133 +Tonga (2019),SEK,Normalbelopp,435 +Tonga (2019),SEK,Breakfast,-65.25 +Tonga (2019),SEK,Lunch,-152.25 +Tonga (2019),SEK,Dinner,-152.25 +Tonga (2020),SEK,Normalbelopp,455 +Tonga (2020),SEK,Breakfast,-68.25 +Tonga (2020),SEK,Lunch,-159.25 +Tonga (2020),SEK,Dinner,-159.25 +Trinidad och Tobago (2018),SEK,Normalbelopp,674 +Trinidad och Tobago (2018),SEK,Breakfast,-101.1 +Trinidad och Tobago (2018),SEK,Lunch,-235.9 +Trinidad och Tobago (2018),SEK,Dinner,-235.9 +Trinidad och Tobago (2019),SEK,Normalbelopp,728 +Trinidad och Tobago (2019),SEK,Breakfast,-109.2 +Trinidad och Tobago (2019),SEK,Lunch,-254.8 +Trinidad och Tobago (2019),SEK,Dinner,-254.8 +Trinidad och Tobago (2020),SEK,Normalbelopp,757 +Trinidad och Tobago (2020),SEK,Breakfast,-113.55 +Trinidad och Tobago (2020),SEK,Lunch,-264.95 +Trinidad och Tobago (2020),SEK,Dinner,-264.95 +Tunisien (2018),SEK,Normalbelopp,282 +Tunisien (2018),SEK,Breakfast,-42.3 +Tunisien (2018),SEK,Lunch,-98.7 +Tunisien (2018),SEK,Dinner,-98.7 +Tunisien (2019),SEK,Normalbelopp,240 +Tunisien (2019),SEK,Breakfast,-36 +Tunisien (2019),SEK,Lunch,-84 +Tunisien (2019),SEK,Dinner,-84 +Tunisien (2020),SEK,Normalbelopp,251 +Tunisien (2020),SEK,Breakfast,-37.65 +Tunisien (2020),SEK,Lunch,-87.85 +Tunisien (2020),SEK,Dinner,-87.85 +Turkiet (2018),SEK,Normalbelopp,274 +Turkiet (2018),SEK,Breakfast,-41.1 +Turkiet (2018),SEK,Lunch,-95.9 +Turkiet (2018),SEK,Dinner,-95.9 +Turkiet (2019),SEK,Normalbelopp,230 +Turkiet (2019),SEK,Breakfast,-34.5 +Turkiet (2019),SEK,Lunch,-80.5 +Turkiet (2019),SEK,Dinner,-80.5 +Turkiet (2020),SEK,Normalbelopp,251 +Turkiet (2020),SEK,Breakfast,-37.65 +Turkiet (2020),SEK,Lunch,-87.85 +Turkiet (2020),SEK,Dinner,-87.85 +Turkmenistan (2018),SEK,Normalbelopp,395 +Turkmenistan (2018),SEK,Breakfast,-59.25 +Turkmenistan (2018),SEK,Lunch,-138.25 +Turkmenistan (2018),SEK,Dinner,-138.25 +Turkmenistan (2019),SEK,Normalbelopp,473 +Turkmenistan (2019),SEK,Breakfast,-70.95 +Turkmenistan (2019),SEK,Lunch,-165.55 +Turkmenistan (2019),SEK,Dinner,-165.55 +Turkmenistan (2020),SEK,Normalbelopp,560 +Turkmenistan (2020),SEK,Breakfast,-84 +Turkmenistan (2020),SEK,Lunch,-196 +Turkmenistan (2020),SEK,Dinner,-196 +Tyskland (2018),SEK,Normalbelopp,620 +Tyskland (2018),SEK,Breakfast,-93 +Tyskland (2018),SEK,Lunch,-217 +Tyskland (2018),SEK,Dinner,-217 +Tyskland (2019),SEK,Normalbelopp,646 +Tyskland (2019),SEK,Breakfast,-96.9 +Tyskland (2019),SEK,Lunch,-226.1 +Tyskland (2019),SEK,Dinner,-226.1 +Tyskland (2020),SEK,Normalbelopp,668 +Tyskland (2020),SEK,Breakfast,-100.2 +Tyskland (2020),SEK,Lunch,-233.8 +Tyskland (2020),SEK,Dinner,-233.8 +Uganda (2018),SEK,Normalbelopp,243 +Uganda (2018),SEK,Breakfast,-36.45 +Uganda (2018),SEK,Lunch,-85.05 +Uganda (2018),SEK,Dinner,-85.05 +Uganda (2019),SEK,Normalbelopp,267 +Uganda (2019),SEK,Breakfast,-40.05 +Uganda (2019),SEK,Lunch,-93.45 +Uganda (2019),SEK,Dinner,-93.45 +Uganda (2020),SEK,Normalbelopp,321 +Uganda (2020),SEK,Breakfast,-48.15 +Uganda (2020),SEK,Lunch,-112.35 +Uganda (2020),SEK,Dinner,-112.35 +Ukraina (2018),SEK,Normalbelopp,257 +Ukraina (2018),SEK,Breakfast,-38.55 +Ukraina (2018),SEK,Lunch,-89.95 +Ukraina (2018),SEK,Dinner,-89.95 +Ukraina (2019),SEK,Normalbelopp,278 +Ukraina (2019),SEK,Breakfast,-41.7 +Ukraina (2019),SEK,Lunch,-97.3 +Ukraina (2019),SEK,Dinner,-97.3 +Ukraina (2020),SEK,Normalbelopp,356 +Ukraina (2020),SEK,Breakfast,-53.4 +Ukraina (2020),SEK,Lunch,-124.6 +Ukraina (2020),SEK,Dinner,-124.6 +Ungern (2018),SEK,Normalbelopp,393 +Ungern (2018),SEK,Breakfast,-58.95 +Ungern (2018),SEK,Lunch,-137.55 +Ungern (2018),SEK,Dinner,-137.55 +Ungern (2019),SEK,Normalbelopp,417 +Ungern (2019),SEK,Breakfast,-62.55 +Ungern (2019),SEK,Lunch,-145.95 +Ungern (2019),SEK,Dinner,-145.95 +Ungern (2020),SEK,Normalbelopp,436 +Ungern (2020),SEK,Breakfast,-65.4 +Ungern (2020),SEK,Lunch,-152.6 +Ungern (2020),SEK,Dinner,-152.6 +Uruguay (2018),SEK,Normalbelopp,503 +Uruguay (2018),SEK,Breakfast,-75.45 +Uruguay (2018),SEK,Lunch,-176.05 +Uruguay (2018),SEK,Dinner,-176.05 +Uruguay (2019),SEK,Normalbelopp,495 +Uruguay (2019),SEK,Breakfast,-74.25 +Uruguay (2019),SEK,Lunch,-173.25 +Uruguay (2019),SEK,Dinner,-173.25 +Uruguay (2020),SEK,Normalbelopp,479 +Uruguay (2020),SEK,Breakfast,-71.85 +Uruguay (2020),SEK,Lunch,-167.65 +Uruguay (2020),SEK,Dinner,-167.65 +USA (2018),SEK,Normalbelopp,689 +USA (2018),SEK,Breakfast,-103.35 +USA (2018),SEK,Lunch,-241.15 +USA (2018),SEK,Dinner,-241.15 +USA (2019),SEK,Normalbelopp,762 +USA (2019),SEK,Breakfast,-114.3 +USA (2019),SEK,Lunch,-266.7 +USA (2019),SEK,Dinner,-266.7 +USA (2020),SEK,Normalbelopp,851 +USA (2020),SEK,Breakfast,-127.65 +USA (2020),SEK,Lunch,-297.85 +USA (2020),SEK,Dinner,-297.85 +Uzbekistan (2018),SEK,Normalbelopp,230 +Uzbekistan (2018),SEK,Breakfast,-34.5 +Uzbekistan (2018),SEK,Lunch,-80.5 +Uzbekistan (2018),SEK,Dinner,-80.5 +Uzbekistan (2019),SEK,Normalbelopp,230 +Uzbekistan (2019),SEK,Breakfast,-34.5 +Uzbekistan (2019),SEK,Lunch,-80.5 +Uzbekistan (2019),SEK,Dinner,-80.5 +Uzbekistan (2020),SEK,Normalbelopp,240 +Uzbekistan (2020),SEK,Breakfast,-36 +Uzbekistan (2020),SEK,Lunch,-84 +Uzbekistan (2020),SEK,Dinner,-84 +Vanuatu (2018),SEK,Normalbelopp,587 +Vanuatu (2018),SEK,Breakfast,-88.05 +Vanuatu (2018),SEK,Lunch,-205.45 +Vanuatu (2018),SEK,Dinner,-205.45 +Vanuatu (2019),SEK,Normalbelopp,626 +Vanuatu (2019),SEK,Breakfast,-93.9 +Vanuatu (2019),SEK,Lunch,-219.1 +Vanuatu (2019),SEK,Dinner,-219.1 +Vanuatu (2020),SEK,Normalbelopp,650 +Vanuatu (2020),SEK,Breakfast,-97.5 +Vanuatu (2020),SEK,Lunch,-227.5 +Vanuatu (2020),SEK,Dinner,-227.5 +Venezuela (2020),SEK,Normalbelopp,240 +Venezuela (2020),SEK,Breakfast,-36 +Venezuela (2020),SEK,Lunch,-84 +Venezuela (2020),SEK,Dinner,-84 +Vietnam (2018),SEK,Normalbelopp,308 +Vietnam (2018),SEK,Breakfast,-46.2 +Vietnam (2018),SEK,Lunch,-107.8 +Vietnam (2018),SEK,Dinner,-107.8 +Vietnam (2019),SEK,Normalbelopp,333 +Vietnam (2019),SEK,Breakfast,-49.95 +Vietnam (2019),SEK,Lunch,-116.55 +Vietnam (2019),SEK,Dinner,-116.55 +Vietnam (2020),SEK,Normalbelopp,374 +Vietnam (2020),SEK,Breakfast,-56.1 +Vietnam (2020),SEK,Lunch,-130.9 +Vietnam (2020),SEK,Dinner,-130.9 +Vitryssland (2018),SEK,Normalbelopp,230 +Vitryssland (2018),SEK,Breakfast,-34.5 +Vitryssland (2018),SEK,Lunch,-80.5 +Vitryssland (2018),SEK,Dinner,-80.5 +Vitryssland (2019),SEK,Normalbelopp,233 +Vitryssland (2019),SEK,Breakfast,-34.95 +Vitryssland (2019),SEK,Lunch,-81.55 +Vitryssland (2019),SEK,Dinner,-81.55 +Vitryssland (2020),SEK,Normalbelopp,271 +Vitryssland (2020),SEK,Breakfast,-40.65 +Vitryssland (2020),SEK,Lunch,-94.85 +Vitryssland (2020),SEK,Dinner,-94.85 +Zambia (2018),SEK,Normalbelopp,355 +Zambia (2018),SEK,Breakfast,-53.25 +Zambia (2018),SEK,Lunch,-124.25 +Zambia (2018),SEK,Dinner,-124.25 +Zambia (2019),SEK,Normalbelopp,295 +Zambia (2019),SEK,Breakfast,-44.25 +Zambia (2019),SEK,Lunch,-103.25 +Zambia (2019),SEK,Dinner,-103.25 +Zambia (2020),SEK,Normalbelopp,260 +Zambia (2020),SEK,Breakfast,-39 +Zambia (2020),SEK,Lunch,-91 +Zambia (2020),SEK,Dinner,-91 +Zimbabwe (2018),SEK,Normalbelopp,511 +Zimbabwe (2018),SEK,Breakfast,-76.65 +Zimbabwe (2018),SEK,Lunch,-178.85 +Zimbabwe (2018),SEK,Dinner,-178.85 +Zimbabwe (2019),SEK,Normalbelopp,561 +Zimbabwe (2019),SEK,Breakfast,-84.15 +Zimbabwe (2019),SEK,Lunch,-196.35 +Zimbabwe (2019),SEK,Dinner,-196.35 +Zimbabwe (2020),SEK,Normalbelopp,487 +Zimbabwe (2020),SEK,Breakfast,-73.05 +Zimbabwe (2020),SEK,Lunch,-170.45 +Zimbabwe (2020),SEK,Dinner,-170.45 \ No newline at end of file diff --git a/docs/assets/images/QBO_desktop_01.png b/docs/assets/images/QBO_desktop_01.png new file mode 100644 index 000000000000..7d04236a9765 Binary files /dev/null and b/docs/assets/images/QBO_desktop_01.png differ diff --git a/docs/assets/images/QBO_desktop_02.png b/docs/assets/images/QBO_desktop_02.png new file mode 100644 index 000000000000..07bf2f006043 Binary files /dev/null and b/docs/assets/images/QBO_desktop_02.png differ diff --git a/docs/assets/images/QBO_desktop_03.png b/docs/assets/images/QBO_desktop_03.png new file mode 100644 index 000000000000..81a4176fdf52 Binary files /dev/null and b/docs/assets/images/QBO_desktop_03.png differ diff --git a/docs/assets/images/QBO_desktop_04.png b/docs/assets/images/QBO_desktop_04.png new file mode 100644 index 000000000000..d516a9e2c56a Binary files /dev/null and b/docs/assets/images/QBO_desktop_04.png differ diff --git a/docs/assets/images/QBO_desktop_05.png b/docs/assets/images/QBO_desktop_05.png new file mode 100644 index 000000000000..123adf897820 Binary files /dev/null and b/docs/assets/images/QBO_desktop_05.png differ diff --git a/docs/assets/images/QBO_desktop_06.png b/docs/assets/images/QBO_desktop_06.png new file mode 100644 index 000000000000..4e0dfbf4b574 Binary files /dev/null and b/docs/assets/images/QBO_desktop_06.png differ diff --git a/docs/assets/images/QBO_desktop_07.png b/docs/assets/images/QBO_desktop_07.png new file mode 100644 index 000000000000..519e0d3738f4 Binary files /dev/null and b/docs/assets/images/QBO_desktop_07.png differ diff --git a/docs/assets/images/QBO_desktop_08.png b/docs/assets/images/QBO_desktop_08.png new file mode 100644 index 000000000000..1573277c15c2 Binary files /dev/null and b/docs/assets/images/QBO_desktop_08.png differ diff --git a/docs/redirects.csv b/docs/redirects.csv index 532bd4ca1752..a1a8346b4789 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -477,7 +477,6 @@ https://community.expensify.com/discussion/5321/how-to-set-up-saml-authenticatio https://community.expensify.com/discussion/5499/deep-dive-configure-coding-for-sage-intacct,https://help.expensify.com/articles/expensify-classic/connections/sage-intacct/Configure-Sage-Intacct https://community.expensify.com/discussion/5580/deep-dive-configure-advanced-settings-for-netsuite,https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-to-NetSuite#step-3-configure-advanced-settings https://community.expensify.com/discussion/5632/deep-dive-configure-coding-for-netsuite,https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-to-NetSuite#configure-netsuite-integration -https://community.expensify.com/discussion/5632/deep-dive-configure-coding-for-netsuite#tax,https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-to-NetSuite#configure-netsuite-integration https://community.expensify.com/discussion/5649/deep-dive-configure-advanced-settings-for-quickbooks-online,https://help.expensify.com/articles/expensify-classic/connections/quickbooks-online/Configure-Quickbooks-Online https://community.expensify.com/discussion/5654/deep-dive-using-expense-rules-to-vendor-match-when-exporting-to-an-accounting-package,https://help.expensify.com/articles/expensify-classic/expenses/Expense-Rules#how-can-i-use-expense-rules-to-vendor-match-when-exporting-to-an-accounting-package https://community.expensify.com/discussion/5656/deep-dive-configure-coding-for-xero/,https://help.expensify.com/articles/new-expensify/connections/xero/Connect-to-Xero#step-2-configure-import-settings @@ -490,7 +489,6 @@ https://community.expensify.com/discussion/5864/how-to-add-a-personal-bank-accou https://community.expensify.com/discussion/5941/how-to-reimburse-overseas-employees-for-us-employers/,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/bank-accounts/Enable-Global-Reimbursements https://community.expensify.com/discussion/6203/deep-dive-expensify-card-and-netsuite-auto-reconciliation-how-it-works,https://help.expensify.com/articles/expensify-classic/expensify-card/Expensify-Card-Reconciliation https://community.expensify.com/discussion/6698/faq-troubleshooting-bank-and-card-errors,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting -https://community.expensify.com/discussion/6698/faq-troubleshooting-bank-and-card-errors#account-type-not-supported,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting https://community.expensify.com/discussion/6827/what-s-happening-to-my-expensify-bill,https://help.expensify.com/articles/expensify-classic/expensify-billing/Billing-Overview https://community.expensify.com/discussion/6898/deep-dive-guide-to-billing,https://help.expensify.com/articles/expensify-classic/expensify-billing/Billing-Overview https://community.expensify.com/discussion/7231/how-to-export-invoices-to-netsuite,https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-to-NetSuite#export-invoices-to diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index ac9c1b318aad..bd301e478ada 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -1015,11 +1015,7 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; @@ -1822,11 +1818,7 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; @@ -1894,11 +1886,7 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = ""; PRODUCT_NAME = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; @@ -1976,11 +1964,7 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; @@ -2125,11 +2109,7 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; @@ -2266,11 +2246,7 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = ""; PRODUCT_NAME = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; @@ -2405,11 +2381,7 @@ "$(inherited)", "-DRN_FABRIC_ENABLED", ); - OTHER_LDFLAGS = ( - "$(inherited)", - "-Wl", - "-ld_classic", - ); + OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = ""; PRODUCT_NAME = ""; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index d67a06a0cad7..d601d4a5eb05 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.23 + 9.0.24 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.23.0 + 9.0.24.0 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index d808278550be..2faff3e32557 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.23 + 9.0.24 CFBundleSignature ???? CFBundleVersion - 9.0.23.0 + 9.0.24.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 7fb5a7c657c6..fce2a5696573 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.23 + 9.0.24 CFBundleVersion - 9.0.23.0 + 9.0.24.0 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 7112eb5e83a7..1a1a21aba7a3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1891,7 +1891,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.113): + - RNLiveMarkdown (0.1.117): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1909,9 +1909,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.113) + - RNLiveMarkdown/common (= 0.1.117) - Yoga - - RNLiveMarkdown/common (0.1.113): + - RNLiveMarkdown/common (0.1.117): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2634,7 +2634,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 235376cd828014e8bad6949ea5bb202688fa5bb0 + RNLiveMarkdown: 54e6a7dfd3e92fdb1d2dab1b64ee8a56d56acd91 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c RNPermissions: d2392b754e67bc14491f5b12588bef2864e783f3 diff --git a/package-lock.json b/package-lock.json index 503aeefb1963..8394997955f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "new.expensify", - "version": "9.0.23-0", + "version": "9.0.24-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.23-0", + "version": "9.0.24-0", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "^0.1.113", + "@expensify/react-native-live-markdown": "0.1.117", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -3952,9 +3952,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.113", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.113.tgz", - "integrity": "sha512-MeZTqW1Dd2oUAVmedaU6p/TE+mmhWibSkcz8VC10PyCn6HkwO7ZykaSXMjsJHoUyvx9vYaG/4iF9enWXe4+h5w==", + "version": "0.1.117", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.117.tgz", + "integrity": "sha512-MMs8U7HRNilTc5PaCODpWL89/+fo61Np1tUBjVaiA4QQw2h5Qta8V5/YexUA4wG29M0N7gcGkxapVhfUoEB0vQ==", "workspaces": [ "parser", "example", diff --git a/package.json b/package.json index 1f0f1b9e089e..c561320cc6da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.23-0", + "version": "9.0.24-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -69,7 +69,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "^0.1.113", + "@expensify/react-native-live-markdown": "0.1.117", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", diff --git a/patches/react-native-modal+13.0.1.patch b/patches/react-native-modal+13.0.1.patch index cc9c8531e3a3..885faec875bc 100644 --- a/patches/react-native-modal+13.0.1.patch +++ b/patches/react-native-modal+13.0.1.patch @@ -11,10 +11,22 @@ index b63bcfc..bd6419e 100644 buildPanResponder: () => void; getAccDistancePerDirection: (gestureState: PanResponderGestureState) => number; diff --git a/node_modules/react-native-modal/dist/modal.js b/node_modules/react-native-modal/dist/modal.js -index 80f4e75..5a58eae 100644 +index 80f4e75..602cdff 100644 --- a/node_modules/react-native-modal/dist/modal.js +++ b/node_modules/react-native-modal/dist/modal.js -@@ -75,6 +75,13 @@ export class ReactNativeModal extends React.Component { +@@ -59,6 +59,11 @@ export class ReactNativeModal extends React.Component { + deviceHeight: Dimensions.get('window').height, + isSwipeable: !!this.props.swipeDirection, + pan: null, ++ backdrop: { ++ prevOpacity: 0, ++ opacity: 0, ++ }, ++ contentAnimation: {}, + }; + this.isTransitioning = false; + this.inSwipeClosingState = false; +@@ -75,6 +80,13 @@ export class ReactNativeModal extends React.Component { } return false; }; @@ -28,7 +40,202 @@ index 80f4e75..5a58eae 100644 this.shouldPropagateSwipe = (evt, gestureState) => { return typeof this.props.propagateSwipe === 'function' ? this.props.propagateSwipe(evt, gestureState) -@@ -453,10 +460,18 @@ export class ReactNativeModal extends React.Component { +@@ -134,10 +146,12 @@ export class ReactNativeModal extends React.Component { + if (this.isSwipeDirectionAllowed(gestureState)) { + // Dim the background while swiping the modal + const newOpacityFactor = 1 - this.calcDistancePercentage(gestureState); +- this.backdropRef && +- this.backdropRef.transitionTo({ +- opacity: this.props.backdropOpacity * newOpacityFactor, +- }); ++ this.setState((prevState) => ({ ++ backdrop: { ++ prevOpacity: prevState.backdrop.opacity, ++ opacity: newOpacityFactor * this.props.backdropOpacity, ++ } ++ })) + animEvt(evt, gestureState); + if (this.props.onSwipeMove) { + this.props.onSwipeMove(newOpacityFactor, gestureState); +@@ -185,11 +199,13 @@ export class ReactNativeModal extends React.Component { + if (this.props.onSwipeCancel) { + this.props.onSwipeCancel(gestureState); + } +- if (this.backdropRef) { +- this.backdropRef.transitionTo({ ++ this.setState((prevState) => ({ ++ backdrop: { ++ prevOpacity: prevState.backdrop.opacity, + opacity: this.props.backdropOpacity, +- }); +- } ++ duration: undefined, ++ } ++ })) + Animated.spring(this.state.pan, { + toValue: { x: 0, y: 0 }, + bounciness: 0, +@@ -300,40 +316,53 @@ export class ReactNativeModal extends React.Component { + } + } + }; ++ this.onContentAnimationEnd = () => { ++ this.isTransitioning = false; ++ ++ if (this.interactionHandle) { ++ InteractionManager.clearInteractionHandle(this.interactionHandle); ++ this.interactionHandle = null; ++ } ++ if (!this.props.isVisible) { ++ this.setState({ ++ showContent: false, ++ }, () => { ++ this.setState({ ++ isVisible: false, ++ }, () => { ++ this.props.onModalHide(); ++ }); ++ }); ++ } else { ++ this.props.onModalShow(); ++ } ++ } + this.open = () => { + if (this.isTransitioning) { + return; + } + this.isTransitioning = true; +- if (this.backdropRef) { +- this.backdropRef.transitionTo({ opacity: this.props.backdropOpacity }, this.props.backdropTransitionInTiming); +- } ++ ++ this.setState((prevState) => ({ ++ backdrop: { ++ prevOpacity: prevState.backdrop.opacity, ++ opacity: this.props.backdropOpacity, ++ duration: this.props.backdropTransitionInTiming, ++ }, ++ contentAnimation: { ++ animation: this.animationIn, ++ duration: this.props.animationInTiming, ++ } ++ })) + // This is for resetting the pan position,otherwise the modal gets stuck + // at the last released position when you try to open it. + // TODO: Could certainly be improved - no idea for the moment. + if (this.state.isSwipeable) { + this.state.pan.setValue({ x: 0, y: 0 }); + } +- if (this.contentRef) { +- this.props.onModalWillShow && this.props.onModalWillShow(); +- if (this.interactionHandle == null) { +- this.interactionHandle = InteractionManager.createInteractionHandle(); +- } +- this.contentRef +- .animate(this.animationIn, this.props.animationInTiming) +- .then(() => { +- this.isTransitioning = false; +- if (this.interactionHandle) { +- InteractionManager.clearInteractionHandle(this.interactionHandle); +- this.interactionHandle = null; +- } +- if (!this.props.isVisible) { +- this.close(); +- } +- else { +- this.props.onModalShow(); +- } +- }); ++ this.props.onModalWillShow && this.props.onModalWillShow(); ++ if (this.interactionHandle === null) { ++ this.interactionHandle = InteractionManager.createInteractionHandle(); + } + }; + this.close = () => { +@@ -341,9 +370,6 @@ export class ReactNativeModal extends React.Component { + return; + } + this.isTransitioning = true; +- if (this.backdropRef) { +- this.backdropRef.transitionTo({ opacity: 0 }, this.props.backdropTransitionOutTiming); +- } + let animationOut = this.animationOut; + if (this.inSwipeClosingState) { + this.inSwipeClosingState = false; +@@ -360,35 +386,22 @@ export class ReactNativeModal extends React.Component { + animationOut = 'slideOutLeft'; + } + } +- if (this.contentRef) { +- this.props.onModalWillHide && this.props.onModalWillHide(); +- if (this.interactionHandle == null) { +- this.interactionHandle = InteractionManager.createInteractionHandle(); +- } +- this.contentRef +- .animate(animationOut, this.props.animationOutTiming) +- .then(() => { +- this.isTransitioning = false; +- if (this.interactionHandle) { +- InteractionManager.clearInteractionHandle(this.interactionHandle); +- this.interactionHandle = null; +- } +- if (this.props.isVisible) { +- this.open(); +- } +- else { +- this.setState({ +- showContent: false, +- }, () => { +- this.setState({ +- isVisible: false, +- }, () => { +- this.props.onModalHide(); +- }); +- }); +- } +- }); ++ this.props.onModalWillHide && this.props.onModalWillHide(); ++ if (this.interactionHandle == null) { ++ this.interactionHandle = InteractionManager.createInteractionHandle(); + } ++ ++ this.setState((prevState) => ({ ++ backdrop: { ++ prevOpacity: prevState.backdrop.opacity, ++ opacity: 0, ++ duration: this.props.backdropTransitionOutTiming, ++ }, ++ contentAnimation: { ++ animation: animationOut, ++ duration: this.props.animationOutTiming, ++ } ++ })) + }; + this.makeBackdrop = () => { + if (!this.props.hasBackdrop) { +@@ -409,9 +422,20 @@ export class ReactNativeModal extends React.Component { + : 'transparent', + }, + ]; ++ const animation = this.state.backdrop.opacity !== this.state.backdrop.prevOpacity ? { ++ from: { ++ opacity: this.state.backdrop.prevOpacity, ++ }, ++ to: { ++ opacity: this.state.backdrop.opacity, ++ } ++ } : undefined; + const backdropWrapper = (React.createElement(animatable.View, { ref: ref => (this.backdropRef = ref), useNativeDriver: useNativeDriverForBackdrop !== undefined + ? useNativeDriverForBackdrop +- : useNativeDriver, style: [styles.backdrop, backdropComputedStyle] }, hasCustomBackdrop && customBackdrop)); ++ : useNativeDriver, ++ duration: this.state.backdrop.duration, ++ animation, ++ style: [styles.backdrop, backdropComputedStyle] }, hasCustomBackdrop && customBackdrop)); + if (hasCustomBackdrop) { + // The user will handle backdrop presses himself + return backdropWrapper; +@@ -453,10 +477,18 @@ export class ReactNativeModal extends React.Component { if (this.state.isVisible) { this.open(); } @@ -48,7 +255,7 @@ index 80f4e75..5a58eae 100644 if (this.didUpdateDimensionsEmitter) { this.didUpdateDimensionsEmitter.remove(); } -@@ -464,6 +479,9 @@ export class ReactNativeModal extends React.Component { +@@ -464,6 +496,9 @@ export class ReactNativeModal extends React.Component { InteractionManager.clearInteractionHandle(this.interactionHandle); this.interactionHandle = null; } @@ -58,7 +265,40 @@ index 80f4e75..5a58eae 100644 } componentDidUpdate(prevProps) { // If the animations have been changed then rebuild them to make sure we're -@@ -525,7 +543,7 @@ export class ReactNativeModal extends React.Component { +@@ -475,9 +510,14 @@ export class ReactNativeModal extends React.Component { + this.animationOut = animationOut; + } + // If backdrop opacity has been changed then make sure to update it +- if (this.props.backdropOpacity !== prevProps.backdropOpacity && +- this.backdropRef) { +- this.backdropRef.transitionTo({ opacity: this.props.backdropOpacity }, this.props.backdropTransitionInTiming); ++ if (this.props.backdropOpacity !== prevProps.backdropOpacity) { ++ this.setState((prevState) => ({ ++ backdrop: { ++ prevOpacity: prevState.backdrop.opacity, ++ opacity: this.props.backdropOpacity, ++ duration: this.props.backdropTransitionInTiming, ++ } ++ })) + } + // On modal open request, we slide the view up and fade in the backdrop + if (this.props.isVisible && !prevProps.isVisible) { +@@ -515,7 +555,13 @@ export class ReactNativeModal extends React.Component { + const _children = this.props.hideModalContentWhileAnimating && + this.props.useNativeDriver && + !this.state.showContent ? (React.createElement(animatable.View, null)) : (children); +- const containerView = (React.createElement(animatable.View, Object.assign({}, panHandlers, { ref: ref => (this.contentRef = ref), style: [panPosition, computedStyle], pointerEvents: "box-none", useNativeDriver: useNativeDriver }, containerProps), _children)); ++ const containerView = (React.createElement(animatable.View, Object.assign({}, panHandlers, { ++ ref: ref => (this.contentRef = ref), style: [panPosition, computedStyle], ++ pointerEvents: "box-none", useNativeDriver: useNativeDriver, ++ animation: this.state.contentAnimation.animation, ++ duration: this.state.contentAnimation.duration, ++ onAnimationEnd: this.onContentAnimationEnd, ++ }, containerProps), _children)); + // If coverScreen is set to false by the user + // we render the modal inside the parent view directly + if (!coverScreen && this.state.isVisible) { +@@ -525,7 +571,7 @@ export class ReactNativeModal extends React.Component { } return (React.createElement(Modal, Object.assign({ transparent: true, animationType: 'none', visible: this.state.isVisible, onRequestClose: onBackButtonPress }, otherProps), this.makeBackdrop(), diff --git a/src/CONST.ts b/src/CONST.ts index 754f9f5264ec..93d2921e704f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -86,6 +86,7 @@ const CONST = { DEFAULT_TABLE_NAME: 'keyvaluepairs', DEFAULT_ONYX_DUMP_FILE_NAME: 'onyx-state.txt', DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], + DISABLED_MAX_EXPENSE_VALUE: 10000000000, // Note: Group and Self-DM excluded as these are not tied to a Workspace WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT], @@ -382,6 +383,7 @@ const CONST = { REPORT_FIELDS_FEATURE: 'reportFieldsFeature', WORKSPACE_FEEDS: 'workspaceFeeds', NETSUITE_USA_TAX: 'netsuiteUsaTax', + WORKSPACE_RULES: 'workspaceRules', }, BUTTON_STATES: { DEFAULT: 'default', @@ -709,6 +711,7 @@ const CONST = { FORWARDED: 'FORWARDED', // OldDot Action HOLD: 'HOLD', HOLD_COMMENT: 'HOLDCOMMENT', + INTEGRATION_SYNC_FAILED: 'INTEGRATIONSYNCFAILED', IOU: 'IOU', INTEGRATIONS_MESSAGE: 'INTEGRATIONSMESSAGE', // OldDot Action MANAGER_ATTACH_RECEIPT: 'MANAGERATTACHRECEIPT', // OldDot Action @@ -973,10 +976,11 @@ const CONST = { HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render', REPORT_INITIAL_RENDER: 'report_initial_render', SWITCH_REPORT: 'switch_report', - SWITCH_REPORT_FROM_PREVIEW: 'switch_report_from_preview', - SWITCH_REPORT_THREAD: 'switch_report_thread', + OPEN_REPORT_FROM_PREVIEW: 'open_report_from_preview', + OPEN_REPORT_THREAD: 'open_report_thread', SIDEBAR_LOADED: 'sidebar_loaded', LOAD_SEARCH_OPTIONS: 'load_search_options', + MESSAGE_SENT: 'message_sent', COLD: 'cold', WARM: 'warm', REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500, @@ -2084,6 +2088,7 @@ const CONST = { ARE_EXPENSIFY_CARDS_ENABLED: 'areExpensifyCardsEnabled', ARE_INVOICES_ENABLED: 'areInvoicesEnabled', ARE_TAXES_ENABLED: 'tax', + ARE_RULES_ENABLED: 'areRulesEnabled', }, DEFAULT_CATEGORIES: [ 'Advertising', @@ -2426,6 +2431,7 @@ const CONST = { WORKSPACE_BANK_ACCOUNT: 'WorkspaceBankAccount', WORKSPACE_SETTINGS: 'WorkspaceSettings', WORKSPACE_FEATURES: 'WorkspaceFeatures', + WORKSPACE_RULES: 'WorkspaceRules', }, get EXPENSIFY_EMAILS() { return [ @@ -5473,6 +5479,14 @@ const CONST = { description: 'workspace.upgrade.taxCodes.description' as const, icon: 'Coins', }, + rules: { + id: 'rules' as const, + alias: 'rules', + name: 'Rules', + title: 'workspace.upgrade.rules.title' as const, + description: 'workspace.upgrade.rules.description' as const, + icon: 'Rules', + }, }; }, REPORT_FIELD_TYPES: { diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 620243440384..8a2ef4a2b2f4 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -256,6 +256,7 @@ function Expensify({ {shouldInit && ( <> diff --git a/src/ROUTES.ts b/src/ROUTES.ts index dd87e5a9996f..73271d85ea49 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -350,7 +350,7 @@ const ROUTES = { }, ROOM_INVITE: { route: 'r/:reportID/invite/:role?', - getRoute: (reportID: string, role?: string) => `r/${reportID}/invite/${role}` as const, + getRoute: (reportID: string, role?: string) => `r/${reportID}/invite/${role ?? ''}` as const, }, MONEY_REQUEST_HOLD_REASON: { route: ':type/edit/reason/:transactionID?', @@ -928,6 +928,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/expensify-card/settings/frequency', getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/settings/frequency` as const, }, + WORKSPACE_RULES: { + route: 'settings/workspaces/:policyID/rules', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules` as const, + }, WORKSPACE_DISTANCE_RATES: { route: 'settings/workspaces/:policyID/distance-rates', getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index cc4360d7695d..142b2f80a66e 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -441,6 +441,7 @@ const SCREENS = { DISTANCE_RATE_TAX_RECLAIMABLE_ON_EDIT: 'Distance_Rate_Tax_Reclaimable_On_Edit', DISTANCE_RATE_TAX_RATE_EDIT: 'Distance_Rate_Tax_Rate_Edit', UPGRADE: 'Workspace_Upgrade', + RULES: 'Policy_Rules', }, EDIT_REQUEST: { diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index b801743732bc..a2fcae901681 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -1,11 +1,12 @@ import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; import type {ForwardedRef} from 'react'; -import React, {useCallback, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {NativeSyntheticEvent, TextInput, TextInputChangeEventData, TextInputPasteEventData} from 'react-native'; import {StyleSheet} from 'react-native'; import type {FileObject} from '@components/AttachmentModal'; import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useMarkdownStyle from '@hooks/useMarkdownStyle'; import useResetComposerFocus from '@hooks/useResetComposerFocus'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -46,6 +47,15 @@ function Composer( const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {inputCallbackRef, inputRef: autoFocusInputRef} = useAutoFocusInput(); + + useEffect(() => { + if (autoFocus === !!autoFocusInputRef.current) { + return; + } + inputCallbackRef(autoFocus ? textInput.current : null); + }, [autoFocus, inputCallbackRef, autoFocusInputRef]); + /** * Set the TextInput Ref * @param {Element} el @@ -57,6 +67,10 @@ function Composer( return; } + if (autoFocus) { + inputCallbackRef(el); + } + // This callback prop is used by the parent component using the constructor to // get a ref to the inner textInput element e.g. if we do // this.textInput = el} /> this will not diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 3db92a2122b3..7a4512ad5aea 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -292,7 +292,7 @@ function Composer( return; } - const currentText = textInput.current.innerText; + const currentText = textInput.current.value; textInput.current.clear(); // We need to reset the selection to 0,0 manually after clearing the text input on web diff --git a/src/components/DeeplinkWrapper/index.website.tsx b/src/components/DeeplinkWrapper/index.website.tsx index 649e66ccefa8..b395eb12c5fe 100644 --- a/src/components/DeeplinkWrapper/index.website.tsx +++ b/src/components/DeeplinkWrapper/index.website.tsx @@ -5,6 +5,7 @@ import Navigation from '@libs/Navigation/Navigation'; import navigationRef from '@libs/Navigation/navigationRef'; import shouldPreventDeeplinkPrompt from '@libs/Navigation/shouldPreventDeeplinkPrompt'; import * as App from '@userActions/App'; +import * as Link from '@userActions/Link'; import * as Session from '@userActions/Session'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; @@ -15,7 +16,7 @@ function isMacOSWeb(): boolean { return !Browser.isMobile() && typeof navigator === 'object' && typeof navigator.userAgent === 'string' && /Mac/i.test(navigator.userAgent) && !/Electron/i.test(navigator.userAgent); } -function promptToOpenInDesktopApp() { +function promptToOpenInDesktopApp(initialUrl = '') { // If the current url path is /transition..., meaning it was opened from oldDot, during this transition period: // 1. The user session may not exist, because sign-in has not been completed yet. // 2. There may be non-idempotent operations (e.g. create a new workspace), which obviously should not be executed again in the desktop app. @@ -26,11 +27,11 @@ function promptToOpenInDesktopApp() { // Match any magic link (/v//<6 digit code>) const isMagicLink = CONST.REGEX.ROUTES.VALIDATE_LOGIN.test(window.location.pathname); - App.beginDeepLinkRedirect(!isMagicLink); + App.beginDeepLinkRedirect(!isMagicLink, Link.getInternalNewExpensifyPath(initialUrl)); } } -function DeeplinkWrapper({children, isAuthenticated, autoAuthState}: DeeplinkWrapperProps) { +function DeeplinkWrapper({children, isAuthenticated, autoAuthState, initialUrl}: DeeplinkWrapperProps) { const [currentScreen, setCurrentScreen] = useState(); const [hasShownPrompt, setHasShownPrompt] = useState(false); const removeListener = useRef<() => void>(); @@ -77,7 +78,7 @@ function DeeplinkWrapper({children, isAuthenticated, autoAuthState}: DeeplinkWra // Otherwise, we want to wait until the navigation state is set up // and we know the user is on a screen that supports deeplinks. if (isAuthenticated) { - promptToOpenInDesktopApp(); + promptToOpenInDesktopApp(initialUrl); setHasShownPrompt(true); } else { // Navigation state is not set up yet, we're unsure if we should show the deep link prompt or not @@ -93,7 +94,7 @@ function DeeplinkWrapper({children, isAuthenticated, autoAuthState}: DeeplinkWra promptToOpenInDesktopApp(); setHasShownPrompt(true); } - }, [currentScreen, hasShownPrompt, isAuthenticated, autoAuthState]); + }, [currentScreen, hasShownPrompt, isAuthenticated, autoAuthState, initialUrl]); return children; } diff --git a/src/components/DeeplinkWrapper/types.ts b/src/components/DeeplinkWrapper/types.ts index db61e5b01c24..23e096d6a093 100644 --- a/src/components/DeeplinkWrapper/types.ts +++ b/src/components/DeeplinkWrapper/types.ts @@ -6,6 +6,8 @@ type DeeplinkWrapperProps = ChildrenProps & { /** The auto authentication status */ autoAuthState?: string; + + initialUrl?: string; }; export default DeeplinkWrapperProps; diff --git a/src/components/FeedbackSurvey.tsx b/src/components/FeedbackSurvey.tsx index a17cca5efae4..3c677a7b0f6d 100644 --- a/src/components/FeedbackSurvey.tsx +++ b/src/components/FeedbackSurvey.tsx @@ -43,9 +43,12 @@ type FeedbackSurveyProps = { /** Indicates whether a loading indicator should be shown */ isLoading?: boolean; + + /** Should the submit button be enabled when offline */ + enabledWhenOffline?: boolean; }; -function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerText, isNoteRequired, isLoading, formID}: FeedbackSurveyProps) { +function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerText, isNoteRequired, isLoading, formID, enabledWhenOffline = true}: FeedbackSurveyProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [draft, draftResults] = useOnyx(`${formID}Draft`); @@ -103,7 +106,7 @@ function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerTe onSubmit={handleSubmit} submitButtonText={translate('common.submit')} isSubmitButtonVisible={false} - enabledWhenOffline + enabledWhenOffline={enabledWhenOffline} > {title} @@ -138,7 +141,7 @@ function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerTe onSubmit={handleSubmit} message={translate('common.error.pleaseCompleteForm')} buttonText={translate('common.submit')} - enabledWhenOffline + enabledWhenOffline={enabledWhenOffline} containerStyles={styles.mt3} isLoading={isLoading} /> diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index ea33af302670..b1adf360bae6 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -82,6 +82,7 @@ import ExpensifyLogoNew from '@assets/images/expensify-logo-new.svg'; import ExpensifyWordmark from '@assets/images/expensify-wordmark.svg'; import EyeDisabled from '@assets/images/eye-disabled.svg'; import Eye from '@assets/images/eye.svg'; +import Feed from '@assets/images/feed.svg'; import Filter from '@assets/images/filter.svg'; import Filters from '@assets/images/filters.svg'; import Flag from '@assets/images/flag.svg'; @@ -386,4 +387,5 @@ export { Filters, CalendarSolid, Filter, + Feed, }; diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 9537e7a0a7a7..3b7b2068acd1 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -87,6 +87,7 @@ import ReceiptEnvelope from '@assets/images/simple-illustrations/simple-illustra import ReceiptLocationMarker from '@assets/images/simple-illustrations/simple-illustration__receipt-location-marker.svg'; import ReceiptWrangler from '@assets/images/simple-illustrations/simple-illustration__receipt-wrangler.svg'; import ReceiptUpload from '@assets/images/simple-illustrations/simple-illustration__receiptupload.svg'; +import Rules from '@assets/images/simple-illustrations/simple-illustration__rules.svg'; import SanFrancisco from '@assets/images/simple-illustrations/simple-illustration__sanfrancisco.svg'; import SendMoney from '@assets/images/simple-illustrations/simple-illustration__sendmoney.svg'; import ShieldYellow from '@assets/images/simple-illustrations/simple-illustration__shield.svg'; @@ -216,4 +217,5 @@ export { Tire, BigVault, Filters, + Rules, }; diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index b35b14016235..a734890a1f38 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -188,8 +188,8 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio ); const extraData = useMemo( - () => [reportActions, reports, policy, personalDetails, data.length, draftComments, optionMode, preferredLocale], - [reportActions, reports, policy, personalDetails, data.length, draftComments, optionMode, preferredLocale], + () => [reportActions, reports, transactionViolations, policy, personalDetails, data.length, draftComments, optionMode, preferredLocale], + [reportActions, reports, transactionViolations, policy, personalDetails, data.length, draftComments, optionMode, preferredLocale], ); const previousOptionMode = usePrevious(optionMode); diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 32c1a3852c86..ee3929292cd3 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -106,6 +106,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const connectedIntegration = PolicyUtils.getConnectedIntegration(policy); const navigateBackToAfterDelete = useRef(); const hasScanningReceipt = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).some((t) => TransactionUtils.isReceiptBeingScanned(t)); + const hasOnlyPendingTransactions = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).every( + (t) => TransactionUtils.isExpensifyCardTransaction(t) && TransactionUtils.isPending(t), + ); const transactionIDs = allTransactions.map((t) => t.transactionID); const allHavePendingRTERViolation = TransactionUtils.allHavePendingRTERViolation(transactionIDs); const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID); @@ -131,7 +134,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; - const shouldShowStatusBar = allHavePendingRTERViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense; + const shouldShowStatusBar = allHavePendingRTERViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions; const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar; const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || allHavePendingRTERViolation || shouldShowExportIntegrationButton; @@ -217,6 +220,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (allHavePendingRTERViolation) { return {icon: getStatusIcon(Expensicons.Hourglass), description: translate('iou.pendingMatchWithCreditCardDescription')}; } + if (hasOnlyPendingTransactions) { + return {icon: getStatusIcon(Expensicons.CreditCardHourglass), description: translate('iou.transactionPendingDescription')}; + } if (hasScanningReceipt) { return {icon: getStatusIcon(Expensicons.ReceiptScan), description: translate('iou.receiptScanInProgressDescription')}; } diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index ce6495e1385e..1350fd30ca54 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -2,7 +2,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import lodashIsEqual from 'lodash/isEqual'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDebouncedState from '@hooks/useDebouncedState'; @@ -37,6 +37,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type {SplitShares} from '@src/types/onyx/Transaction'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import type {DropdownOption} from './ButtonWithDropdownMenu/types'; import FormHelpMessage from './FormHelpMessage'; @@ -177,7 +178,7 @@ function MoneyRequestConfirmationList({ iouAmount, policyCategories: policyCategoriesReal, policyCategoriesDraft, - mileageRates, + mileageRates: mileageRatesReal, isDistanceRequest = false, policy: policyReal, policyDraft, @@ -211,6 +212,10 @@ function MoneyRequestConfirmationList({ }: MoneyRequestConfirmationListProps) { const policy = policyReal ?? policyDraft; const policyCategories = policyCategoriesReal ?? policyCategoriesDraft; + const [mileageRatesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, { + selector: (selectedPolicy: OnyxEntry) => DistanceRequestUtils.getMileageRates(selectedPolicy), + }); + const mileageRates = isEmptyObject(mileageRatesReal) ? mileageRatesDraft : mileageRatesReal; const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); @@ -228,14 +233,15 @@ function MoneyRequestConfirmationList({ const customUnitRateID = TransactionUtils.getRateID(transaction) ?? '-1'; useEffect(() => { - if (customUnitRateID || !canUseP2PDistanceRequests) { + if ((customUnitRateID && customUnitRateID !== '-1') || !isDistanceRequest) { return; } - if (!customUnitRateID) { - const rateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? defaultMileageRate?.customUnitRateID ?? ''; - IOU.setCustomUnitRateID(transactionID, rateID); - } - }, [defaultMileageRate, customUnitRateID, lastSelectedDistanceRates, policy?.id, canUseP2PDistanceRequests, transactionID]); + + const defaultRate = defaultMileageRate?.customUnitRateID ?? ''; + const lastSelectedRate = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? defaultRate; + const rateID = canUseP2PDistanceRequests ? lastSelectedRate : defaultRate; + IOU.setCustomUnitRateID(transactionID, rateID); + }, [defaultMileageRate, customUnitRateID, lastSelectedDistanceRates, policy?.id, canUseP2PDistanceRequests, transactionID, isDistanceRequest]); const policyCurrency = policy?.outputCurrency ?? PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index a38845ec3362..4a76f33de346 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -19,7 +19,6 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useViolations from '@hooks/useViolations'; import type {ViolationField} from '@hooks/useViolations'; -import * as CardUtils from '@libs/CardUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import type {MileageRate} from '@libs/DistanceRequestUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; @@ -163,14 +162,14 @@ function MoneyRequestView({ tag: transactionTag, originalAmount: transactionOriginalAmount, originalCurrency: transactionOriginalCurrency, - cardID: transactionCardID, } = useMemo>(() => ReportUtils.getTransactionDetails(transaction) ?? {}, [transaction]); const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency); const isCardTransaction = TransactionUtils.isCardTransaction(transaction); - const cardProgramName = isCardTransaction && transactionCardID !== undefined ? CardUtils.getCardDescription(transactionCardID) : ''; + const cardProgramName = TransactionUtils.getCardName(transaction); + const shouldShowCard = isCardTransaction && cardProgramName; const isApproved = ReportUtils.isReportApproved(moneyRequestReport); const isInvoice = ReportUtils.isInvoiceReport(moneyRequestReport); const isPaidReport = ReportActionsUtils.isPayAction(parentReportAction); @@ -187,7 +186,7 @@ function MoneyRequestView({ // Flags for allowing or disallowing editing an expense // Used for non-restricted fields such as: description, category, tag, billable, etc... - const canUserPerformWriteAction = !!ReportUtils.canUserPerformWriteAction(report); + const canUserPerformWriteAction = !!ReportUtils.canUserPerformWriteAction(report) && !readonly; const canEdit = ReportActionsUtils.isMoneyRequestAction(parentReportAction) && ReportUtils.canEditMoneyRequest(parentReportAction, transaction) && canUserPerformWriteAction; const canEditTaxFields = canEdit && !isDistanceRequest; @@ -345,8 +344,8 @@ function MoneyRequestView({ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1')) @@ -371,8 +370,8 @@ function MoneyRequestView({ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))} /> @@ -429,8 +428,8 @@ function MoneyRequestView({ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.EDIT, iouType, orderWeight, transaction?.transactionID ?? '', report?.reportID ?? '-1')) @@ -502,7 +501,7 @@ function MoneyRequestView({ {shouldShowReceiptEmptyState && ( Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( @@ -525,8 +524,8 @@ function MoneyRequestView({ titleIcon={Expensicons.Checkmark} description={amountDescription} titleStyle={styles.textHeadlineH2} - interactive={canEditAmount && !readonly} - shouldShowRightIcon={canEditAmount && !readonly} + interactive={canEditAmount} + shouldShowRightIcon={canEditAmount} onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1')) } @@ -539,8 +538,8 @@ function MoneyRequestView({ description={translate('common.description')} shouldParseTitle title={updatedTransactionDescription ?? transactionDescription} - interactive={canEdit && !readonly} - shouldShowRightIcon={canEdit && !readonly} + interactive={canEdit} + shouldShowRightIcon={canEdit} titleStyle={styles.flex1} onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1')) @@ -558,8 +557,8 @@ function MoneyRequestView({ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1')) @@ -575,8 +574,8 @@ function MoneyRequestView({ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1' ?? '-1')) @@ -590,8 +589,8 @@ function MoneyRequestView({ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1')) @@ -602,7 +601,7 @@ function MoneyRequestView({ )} {shouldShowTag && tagList} - {isCardTransaction && ( + {shouldShowCard && ( Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1')) @@ -633,8 +632,8 @@ function MoneyRequestView({ Navigation.navigate( @@ -672,7 +671,7 @@ function MoneyRequestView({ accessibilityLabel={translate('common.billable')} isOn={updatedTransaction?.billable ?? !!transactionBillable} onToggle={saveBillable} - disabled={!canEdit || readonly} + disabled={!canEdit} /> )} diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index dae33b438a43..45a06968cefd 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -365,7 +365,7 @@ function ReportPreview({ } return { supportText: translate('iou.expenseCount', { - count: numberOfRequests - numberOfScanningReceipts - numberOfPendingRequests, + count: numberOfRequests, scanningReceipts: numberOfScanningReceipts, pendingReceipts: numberOfPendingRequests, }), @@ -389,7 +389,7 @@ function ReportPreview({ { - Timing.start(CONST.TIMING.SWITCH_REPORT_FROM_PREVIEW); + Timing.start(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID)); }} onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 7e4b69c39596..bae08155c262 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -157,9 +157,12 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa } const reportIDList = (selectedReports?.filter((report) => !!report) as string[]) ?? []; - SearchActions.exportSearchItemsToCSV({query: status, reportIDList, transactionIDList: selectedTransactionsKeys, policyIDs: [activeWorkspaceID ?? '']}, () => { - setDownloadErrorModalOpen?.(); - }); + SearchActions.exportSearchItemsToCSV( + {query: status, jsonQuery: JSON.stringify(queryJSON), reportIDList, transactionIDList: selectedTransactionsKeys, policyIDs: [activeWorkspaceID ?? '']}, + () => { + setDownloadErrorModalOpen?.(); + }, + ); }, }); @@ -247,6 +250,7 @@ function SearchPageHeader({queryJSON, hash, onSelectDeleteOption, setOfflineModa return options; }, [ + queryJSON, status, selectedTransactionsKeys, selectedTransactions, diff --git a/src/hooks/useHtmlPaste/index.ts b/src/hooks/useHtmlPaste/index.ts index 022d6178877d..6199a36abdca 100644 --- a/src/hooks/useHtmlPaste/index.ts +++ b/src/hooks/useHtmlPaste/index.ts @@ -1,6 +1,5 @@ import {useNavigation} from '@react-navigation/native'; import {useCallback, useEffect} from 'react'; -import type {ClipboardEvent as PasteEvent} from 'react'; import Parser from '@libs/Parser'; import type UseHtmlPaste from './types'; @@ -21,10 +20,6 @@ const insertAtCaret = (target: HTMLElement, text: string) => { range.setEnd(node, node.length); selection.setBaseAndExtent(range.startContainer, range.startOffset, range.endContainer, range.endOffset); - // Dispatch paste event to make Markdown Input properly set cursor position - const pasteEvent = new ClipboardEvent('paste', {bubbles: true, cancelable: true}); - (pasteEvent as unknown as PasteEvent).isDefaultPrevented = () => false; - target.dispatchEvent(pasteEvent); // Dispatch input event to trigger Markdown Input to parse the new text target.dispatchEvent(new Event('input', {bubbles: true})); } else { @@ -48,11 +43,6 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeLi insertByCommand(text); } - if (!textInputRef.current?.isFocused()) { - textInputRef.current?.focus(); - return; - } - // Pointer will go out of sight when a large paragraph is pasted on the web. Refocusing the input keeps the cursor in view. // To avoid the keyboard toggle issue in mWeb if using blur() and focus() functions, we just need to dispatch the event to trigger the onFocus handler // We need to trigger the bubbled "focusin" event to make sure the onFocus handler is triggered diff --git a/src/languages/en.ts b/src/languages/en.ts index 1b9b9a51ebd7..f5d441923344 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -750,10 +750,17 @@ export default { yourCompanyWebsiteNote: "If you don't have a website, you can provide your company's LinkedIn or social media profile instead.", invalidDomainError: 'You have entered an invalid domain. To continue, please enter a valid domain.', publicDomainError: 'You have entered a public domain. To continue, please enter a private domain.', - expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => - `${count} ${Str.pluralize('expense', 'expenses', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} scanning` : ''}${ - pendingReceipts > 0 ? `, ${pendingReceipts} pending` : '' - }`, + expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => { + const expenseText = `${count} ${Str.pluralize('expense', 'expenses', count)}`; + const statusText = []; + if (scanningReceipts > 0) { + statusText.push(`${scanningReceipts} scanning`); + } + if (pendingReceipts > 0) { + statusText.push(`${pendingReceipts} pending`); + } + return statusText.length > 0 ? `${expenseText} (${statusText.join(', ')})` : expenseText; + }, deleteExpense: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `Delete ${Str.pluralize('expense', 'expenses', count)}`, deleteConfirmation: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `Are you sure that you want to delete ${Str.pluralize('this expense', 'these expenses', count)}?`, settledExpensify: 'Paid', @@ -1427,7 +1434,7 @@ export default { addPaymentMethod: 'Add payment method', addNewDebitCard: 'Add new debit card', addNewBankAccount: 'Add new bank account', - accountLastFour: 'Account ending in', + accountLastFour: 'Ending in', cardLastFour: 'Card ending in', addFirstPaymentMethod: 'Add a payment method to send and receive payments directly in the app.', defaultPaymentMethod: 'Default', @@ -2122,6 +2129,7 @@ export default { travel: 'Travel', members: 'Members', accounting: 'Accounting', + rules: 'Rules', displayedAs: 'Displayed as', plan: 'Plan', profile: 'Profile', @@ -2585,7 +2593,7 @@ export default { }, }, customersOrJobs: { - title: 'Customers / projects', + title: 'Customers/projects', subtitle: 'Choose how to handle NetSuite *customers* and *projects* in Expensify.', importCustomers: 'Import customers', importJobs: 'Import projects', @@ -2600,7 +2608,7 @@ export default { customSegments: { title: 'Custom segments/records', addText: 'Add custom segment/record', - recordTitle: 'Custom segment', + recordTitle: 'Custom segment/record', helpLink: CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS, helpLinkText: 'View detailed instructions', helpText: ' on configuring custom segments/records.', @@ -2831,6 +2839,10 @@ export default { title: 'Spend', subtitle: 'Enable functionality that helps you scale your team.', }, + manageSection: { + title: 'Manage', + subtitle: 'Add controls that help keep spend within budget.', + }, earnSection: { title: 'Earn', subtitle: 'Enable optional functionality to streamline your revenue and get paid faster.', @@ -2898,6 +2910,10 @@ export default { disconnectText: "To disable accounting, you'll need to disconnect your accounting connection from your workspace.", manageSettings: 'Manage settings', }, + rules: { + title: 'Rules', + subtitle: 'Configure when receipts are required, flag high spend, and more.', + }, }, reportFields: { addField: 'Add field', @@ -3543,6 +3559,11 @@ export default { description: `Add tax codes to your taxes for easy export of expenses to your accounting and payroll systems.`, onlyAvailableOnPlan: 'Tax codes are only available on the Control plan, starting at ', }, + rules: { + title: 'Rules', + description: `Rules run in the background and keep your spend under control so you don't have to sweat the small stuff.\n\nRequire expense details like receipts and descriptions, set limits and defaults, and automate approvals and payments – all in one place.`, + onlyAvailableOnPlan: 'Rules are only available on the Control plan, starting at ', + }, pricing: { amount: '$9 ', perActiveMember: 'per active member per month.', @@ -3574,6 +3595,16 @@ export default { chatInAdmins: 'Chat in #admins', addPaymentCard: 'Add payment card', }, + rules: { + individualExpenseRules: { + title: 'Expenses', + subtitle: 'Set spend controls and defaults for individual expenses. You can also create rules for', + }, + expenseReportRules: { + title: 'Expense reports', + subtitle: 'Automate expense report compliance, approvals, and payment.', + }, + }, }, getAssistancePage: { title: 'Get assistance', @@ -3857,6 +3888,7 @@ export default { stripePaid: ({amount, currency}: StripePaidParams) => `paid ${currency}${amount}`, takeControl: `took control`, unapproved: ({amount, currency}: UnapprovedParams) => `unapproved ${currency}${amount}`, + integrationSyncFailed: (label: string, errorMessage: string) => `failed to sync with ${label} ("${errorMessage}")`, addEmployee: (email: string, role: string) => `added ${email} as ${role === 'user' ? 'member' : 'admin'}`, updateRole: (email: string, currentRole: string, newRole: string) => `updated the role of ${email} from ${currentRole} to ${newRole}`, removeMember: (email: string, role: string) => `removed ${role} ${email}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 64a3973e3953..ca84c83d16e1 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -743,11 +743,17 @@ export default { yourCompanyWebsiteNote: 'Si no tiene un sitio web, puede proporcionar el perfil de LinkedIn o de las redes sociales de su empresa.', invalidDomainError: 'Ha introducido un dominio no válido. Para continuar, introduzca un dominio válido.', publicDomainError: 'Ha introducido un dominio público. Para continuar, introduzca un dominio privado.', - expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => - `${count} ${Str.pluralize('gasto', 'gastos', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}${ - pendingReceipts > 0 ? `, ${pendingReceipts} pendiente` : '' - }`, - + expenseCount: ({count, scanningReceipts = 0, pendingReceipts = 0}: RequestCountParams) => { + const expenseText = `${count} ${Str.pluralize('gasto', 'gastos', count)}`; + const statusText = []; + if (scanningReceipts > 0) { + statusText.push(`${scanningReceipts} escaneando`); + } + if (pendingReceipts > 0) { + statusText.push(`${pendingReceipts} pendiente`); + } + return statusText.length > 0 ? `${expenseText} (${statusText.join(', ')})` : expenseText; + }, deleteExpense: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `Eliminar ${Str.pluralize('gasto', 'gastos', count)}`, deleteConfirmation: ({count}: DeleteExpenseTranslationParams = {count: 1}) => `¿Estás seguro de que quieres eliminar ${Str.pluralize('esta solicitud', 'estas solicitudes', count)}?`, settledExpensify: 'Pagado', @@ -1436,7 +1442,7 @@ export default { addPaymentMethod: 'Añadir método de pago', addNewDebitCard: 'Añadir nueva tarjeta de débito', addNewBankAccount: 'Añadir nueva cuenta de banco', - accountLastFour: 'Cuenta terminada en', + accountLastFour: 'Terminada en', cardLastFour: 'Tarjeta terminada en', addFirstPaymentMethod: 'Añade un método de pago para enviar y recibir pagos directamente desde la aplicación.', defaultPaymentMethod: 'Predeterminado', @@ -2153,6 +2159,7 @@ export default { travel: 'Viajes', members: 'Miembros', accounting: 'Contabilidad', + rules: 'Reglas', plan: 'Plan', profile: 'Perfil', bankAccount: 'Cuenta bancaria', @@ -2631,7 +2638,7 @@ export default { }, }, customersOrJobs: { - title: 'Clientes / proyectos', + title: 'Clientes/proyectos', subtitle: 'Elija cómo manejar los *clientes* y *proyectos* de NetSuite en Expensify.', importCustomers: 'Importar clientes', importJobs: 'Importar proyectos', @@ -2646,7 +2653,7 @@ export default { customSegments: { title: 'Segmentos/registros personalizados', addText: 'Añadir segmento/registro personalizado', - recordTitle: 'Segmento personalizado', + recordTitle: 'Segmento/registro personalizado', helpLink: CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS, helpLinkText: 'Ver instrucciones detalladas', helpText: ' sobre la configuración de segmentos/registros personalizado.', @@ -2880,6 +2887,10 @@ export default { title: 'Gasto', subtitle: 'Habilita otras funcionalidades que ayudan a aumentar tu equipo.', }, + manageSection: { + title: 'Gestionar', + subtitle: 'Añade controles que ayudan a mantener los gastos dentro del presupuesto.', + }, earnSection: { title: 'Gane', subtitle: 'Habilita funciones opcionales para agilizar tus ingresos y recibir pagos más rápido.', @@ -2947,6 +2958,10 @@ export default { disconnectText: 'Para desactivar la contabilidad, desconecta tu conexión contable del espacio de trabajo.', manageSettings: 'Gestionar la configuración', }, + rules: { + title: 'Reglas', + subtitle: 'Configura cuándo se exigen los recibos, marca los gastos elevados y mucho más.', + }, }, reportFields: { addField: 'Añadir campo', @@ -3593,6 +3608,11 @@ export default { description: `Añada código de impuesto mayor a sus categorías para exportar fácilmente los gastos a sus sistemas de contabilidad y nómina.`, onlyAvailableOnPlan: 'Los código de impuesto mayor solo están disponibles en el plan Control, a partir de ', }, + rules: { + title: 'Reglas', + description: `Las reglas se ejecutan en segundo plano y mantienen tus gastos bajo control para que no tengas que preocuparte por los detalles pequeños.\n\nExige detalles de los gastos, como recibos y descripciones, establece límites y valores predeterminados, y automatiza las aprobaciones y los pagos, todo en un mismo lugar.`, + onlyAvailableOnPlan: 'Las reglas están disponibles solo en el plan Control, que comienza en ', + }, note: { upgradeWorkspace: 'Mejore su espacio de trabajo para acceder a esta función, o', learnMore: 'más información', @@ -3624,6 +3644,16 @@ export default { chatInAdmins: 'Chatea en #admins', addPaymentCard: 'Agregar tarjeta de pago', }, + rules: { + individualExpenseRules: { + title: 'Gastos', + subtitle: 'Establece controles y valores predeterminados para gastos individuales. También puedes crear reglas para', + }, + expenseReportRules: { + title: 'Informes de gastos', + subtitle: 'Automatiza el cumplimiento, la aprobación y el pago de los informes de gastos.', + }, + }, }, getAssistancePage: { title: 'Obtener ayuda', @@ -3909,6 +3939,7 @@ export default { stripePaid: ({amount, currency}: StripePaidParams) => `pagado ${currency}${amount}`, takeControl: `tomó el control`, unapproved: ({amount, currency}: UnapprovedParams) => `no aprobado ${currency}${amount}`, + integrationSyncFailed: (label: string, errorMessage: string) => `no se pudo sincronizar con ${label} ("${errorMessage}")`, addEmployee: (email: string, role: string) => `agregó a ${email} como ${role === 'user' ? 'miembro' : 'administrador'}`, updateRole: (email: string, currentRole: string, newRole: string) => `actualicé el rol ${email} de ${currentRole === 'user' ? 'miembro' : 'administrador'} a ${newRole === 'user' ? 'miembro' : 'administrador'}`, diff --git a/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts b/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts index 2659fac6810a..057b6188e3ea 100644 --- a/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts +++ b/src/libs/API/parameters/ExportSearchItemsToCSVParams.ts @@ -1,7 +1,8 @@ -import type {SearchStatus} from '@components/Search/types'; +import type {SearchQueryString, SearchStatus} from '@components/Search/types'; type ExportSearchItemsToCSVParams = { query: SearchStatus; + jsonQuery: SearchQueryString; reportIDList: string[]; transactionIDList: string[]; policyIDs: string[]; diff --git a/src/libs/API/parameters/SendInvoiceParams.ts b/src/libs/API/parameters/SendInvoiceParams.ts index c95ffce14b2c..e2cac84e0d12 100644 --- a/src/libs/API/parameters/SendInvoiceParams.ts +++ b/src/libs/API/parameters/SendInvoiceParams.ts @@ -20,6 +20,9 @@ type SendInvoiceParams = RequireAtLeastOne< transactionThreadReportID: string; companyName?: string; companyWebsite?: string; + createdIOUReportActionID: string; + createdReportActionIDForThread: string; + reportActionID: string; }, 'receiverEmail' | 'receiverInvoiceRoomID' >; diff --git a/src/libs/API/parameters/SetPolicyRulesEnabledParams.ts b/src/libs/API/parameters/SetPolicyRulesEnabledParams.ts new file mode 100644 index 000000000000..c748a98e4119 --- /dev/null +++ b/src/libs/API/parameters/SetPolicyRulesEnabledParams.ts @@ -0,0 +1,6 @@ +type SetPolicyRulesEnabledParams = { + policyID: string; + enabled: boolean; +}; + +export default SetPolicyRulesEnabledParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index c95355dada31..a72220c3d943 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -272,6 +272,7 @@ export type {default as ExportSearchItemsToCSVParams} from './ExportSearchItemsT export type {default as UpdateExpensifyCardLimitParams} from './UpdateExpensifyCardLimitParams'; export type {CreateWorkspaceApprovalParams, UpdateWorkspaceApprovalParams, RemoveWorkspaceApprovalParams} from './WorkspaceApprovalParams'; export type {default as StartIssueNewCardFlowParams} from './StartIssueNewCardFlowParams'; +export type {default as SetPolicyRulesEnabledParams} from './SetPolicyRulesEnabledParams'; export type {default as ConfigureExpensifyCardsForPolicyParams} from './ConfigureExpensifyCardsForPolicyParams'; export type {default as CreateExpensifyCardParams} from './CreateExpensifyCardParams'; export type {default as UpdateExpensifyCardTitleParams} from './UpdateExpensifyCardTitleParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index cc3504ad89ab..de63ed032afe 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -200,6 +200,7 @@ const WRITE_COMMANDS = { ENABLE_POLICY_REPORT_FIELDS: 'EnablePolicyReportFields', ENABLE_POLICY_EXPENSIFY_CARDS: 'EnablePolicyExpensifyCards', ENABLE_POLICY_INVOICING: 'EnablePolicyInvoicing', + SET_POLICY_RULES_ENABLED: 'SetPolicyRulesEnabled', SET_POLICY_TAXES_CURRENCY_DEFAULT: 'SetPolicyCurrencyDefaultTax', SET_POLICY_TAXES_FOREIGN_CURRENCY_DEFAULT: 'SetPolicyForeignCurrencyDefaultTax', SET_POLICY_CUSTOM_TAX_NAME: 'SetPolicyCustomTaxName', @@ -530,6 +531,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS]: Parameters.EnablePolicyReportFieldsParams; [WRITE_COMMANDS.ENABLE_POLICY_EXPENSIFY_CARDS]: Parameters.EnablePolicyExpensifyCardsParams; [WRITE_COMMANDS.ENABLE_POLICY_INVOICING]: Parameters.EnablePolicyInvoicingParams; + [WRITE_COMMANDS.SET_POLICY_RULES_ENABLED]: Parameters.SetPolicyRulesEnabledParams; [WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams; [WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams; [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams; diff --git a/src/libs/Browser/index.website.ts b/src/libs/Browser/index.website.ts index b89190dc7f78..2007f2c6cbc0 100644 --- a/src/libs/Browser/index.website.ts +++ b/src/libs/Browser/index.website.ts @@ -79,12 +79,12 @@ const isSafari: IsSafari = () => getBrowser() === 'safari' || isMobileSafari(); /** * The session information needs to be passed to the Desktop app, and the only way to do that is by using query params. There is no other way to transfer the data. */ -const openRouteInDesktopApp: OpenRouteInDesktopApp = (shortLivedAuthToken = '', email = '') => { +const openRouteInDesktopApp: OpenRouteInDesktopApp = (shortLivedAuthToken = '', email = '', initialRoute = '') => { const params = new URLSearchParams(); // If the user is opening the desktop app through a third party signin flow, we need to manually add the exitTo param // so that the desktop app redirects to the correct home route after signin is complete. const openingFromDesktopRedirect = window.location.pathname === `/${ROUTES.DESKTOP_SIGN_IN_REDIRECT}`; - params.set('exitTo', `${openingFromDesktopRedirect ? '/r' : window.location.pathname}${window.location.search}${window.location.hash}`); + params.set('exitTo', `${openingFromDesktopRedirect ? '/r' : initialRoute || window.location.pathname}${window.location.search}${window.location.hash}`); if (email && shortLivedAuthToken) { params.set('email', email); params.set('shortLivedAuthToken', shortLivedAuthToken); diff --git a/src/libs/Browser/types.ts b/src/libs/Browser/types.ts index cb242d3729aa..ff0de91e7b78 100644 --- a/src/libs/Browser/types.ts +++ b/src/libs/Browser/types.ts @@ -12,6 +12,6 @@ type IsChromeIOS = () => boolean; type IsSafari = () => boolean; -type OpenRouteInDesktopApp = (shortLivedAuthToken?: string, email?: string) => void; +type OpenRouteInDesktopApp = (shortLivedAuthToken?: string, email?: string, initialRoute?: string) => void; export type {GetBrowser, IsMobile, IsMobileSafari, IsMobileChrome, IsMobileWebKit, IsSafari, IsChromeIOS, OpenRouteInDesktopApp}; diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index fa98cf32ee39..2b2aad59d58d 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -248,7 +248,7 @@ function convertToDistanceInMeters(distance: number, unit: Unit): number { /** * Returns custom unit rate ID for the distance transaction */ -function getCustomUnitRateID(reportID: string) { +function getCustomUnitRateID(reportID: string, shouldUseDefault?: boolean) { const allReports = ReportConnection.getAllReports(); const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; @@ -259,7 +259,7 @@ function getCustomUnitRateID(reportID: string) { const distanceUnit = Object.values(policy?.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); const lastSelectedDistanceRateID = lastSelectedDistanceRates?.[policy?.id ?? '-1'] ?? '-1'; const lastSelectedDistanceRate = distanceUnit?.rates[lastSelectedDistanceRateID] ?? {}; - if (lastSelectedDistanceRate.enabled && lastSelectedDistanceRateID) { + if (lastSelectedDistanceRate.enabled && lastSelectedDistanceRateID && !shouldUseDefault) { customUnitRateID = lastSelectedDistanceRateID; } else { customUnitRateID = getDefaultMileageRate(policy)?.customUnitRateID ?? '-1'; diff --git a/src/libs/E2E/tests/reportTypingTest.e2e.ts b/src/libs/E2E/tests/reportTypingTest.e2e.ts index 9624d7ab992b..efe1c380dfd0 100644 --- a/src/libs/E2E/tests/reportTypingTest.e2e.ts +++ b/src/libs/E2E/tests/reportTypingTest.e2e.ts @@ -1,13 +1,16 @@ import type {NativeConfig} from 'react-native-config'; import Config from 'react-native-config'; +import {runOnUI} from 'react-native-reanimated'; import E2ELogin from '@libs/E2E/actions/e2eLogin'; import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import waitForKeyboard from '@libs/E2E/actions/waitForKeyboard'; import E2EClient from '@libs/E2E/client'; import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; +import getPromiseWithResolve from '@libs/E2E/utils/getPromiseWithResolve'; import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import {getRerenderCount, resetRerenderCount} from '@pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e'; +import {onSubmitAction} from '@pages/home/report/ReportActionCompose/ReportActionCompose'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import * as NativeCommands from '../../../../tests/e2e/nativeCommands/NativeCommandsAction'; @@ -17,6 +20,7 @@ const test = (config: NativeConfig) => { console.debug('[E2E] Logging in for typing'); const reportID = getConfigValueOrThrow('reportID', config); + const message = getConfigValueOrThrow('message', config); E2ELogin().then((neededLogin) => { if (neededLogin) { @@ -28,7 +32,26 @@ const test = (config: NativeConfig) => { console.debug('[E2E] Logged in, getting typing metrics and submitting them…'); + const [renderTimesPromise, renderTimesResolve] = getPromiseWithResolve(); + const [messageSentPromise, messageSentResolve] = getPromiseWithResolve(); + + Promise.all([renderTimesPromise, messageSentPromise]).then(() => { + console.debug(`[E2E] Submitting!`); + + E2EClient.submitTestDone(); + }); + Performance.subscribeToMeasurements((entry) => { + if (entry.name === CONST.TIMING.MESSAGE_SENT) { + E2EClient.submitTestResults({ + branch: Config.E2E_BRANCH, + name: 'Message sent', + metric: entry.duration, + unit: 'ms', + }).then(messageSentResolve); + return; + } + if (entry.name !== CONST.TIMING.SIDEBAR_LOADED) { return; } @@ -46,18 +69,26 @@ const test = (config: NativeConfig) => { return Promise.resolve(); }) .then(() => E2EClient.sendNativeCommand(NativeCommands.makeTypeTextCommand('A'))) - .then(() => { - setTimeout(() => { - const rerenderCount = getRerenderCount(); + .then( + () => + new Promise((resolve) => { + setTimeout(() => { + const rerenderCount = getRerenderCount(); - E2EClient.submitTestResults({ - branch: Config.E2E_BRANCH, - name: 'Composer typing rerender count', - metric: rerenderCount, - unit: 'renders', - }).then(E2EClient.submitTestDone); - }, 3000); - }) + E2EClient.submitTestResults({ + branch: Config.E2E_BRANCH, + name: 'Composer typing rerender count', + metric: rerenderCount, + unit: 'renders', + }) + .then(renderTimesResolve) + .then(resolve); + }, 3000); + }), + ) + .then(() => E2EClient.sendNativeCommand(NativeCommands.makeBackspaceCommand())) + .then(() => E2EClient.sendNativeCommand(NativeCommands.makeTypeTextCommand(message))) + .then(() => runOnUI(onSubmitAction)()) .catch((error) => { console.error('[E2E] Error while test', error); E2EClient.submitTestDone(); diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 986fd165d2d7..d1bf8fcd8c8c 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -141,10 +141,11 @@ function insertTagIntoTransactionTagsString(transactionTags: string, tag: string const tagArray = TransactionUtils.getTagArrayFromName(transactionTags); tagArray[tagIndex] = tag; - return tagArray - .map((tagItem) => tagItem.trim()) - .filter((tagItem) => !!tagItem) - .join(CONST.COLON); + while (tagArray.length > 0 && !tagArray[tagArray.length - 1]) { + tagArray.pop(); + } + + return tagArray.map((tagItem) => tagItem.trim()).join(CONST.COLON); } function isMovingTransactionFromTrackExpense(action?: IOUAction) { diff --git a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx index 748d92b49a1c..077f42d32ec5 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx @@ -33,6 +33,7 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.REPORT_FIELDS]: () => require('../../../../pages/workspace/reportFields/WorkspaceReportFieldsPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardPage').default, [SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default, + [SCREENS.WORKSPACE.RULES]: () => require('../../../../pages/workspace/rules/PolicyRulesPage').default, } satisfies Screens; function FullScreenNavigator() { diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 6b4d7eca95c1..bb9d92c7a5a3 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1099,6 +1099,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.DISTANCE_RATES]: { path: ROUTES.WORKSPACE_DISTANCE_RATES.route, }, + [SCREENS.WORKSPACE.RULES]: { + path: ROUTES.WORKSPACE_RULES.route, + }, }, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b689f36d8a35..c85f0972d84a 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1194,6 +1194,9 @@ type FullScreenNavigatorParamList = { [SCREENS.WORKSPACE.EXPENSIFY_CARD]: { policyID: string; }; + [SCREENS.WORKSPACE.RULES]: { + policyID: string; + }; }; type OnboardingModalNavigatorParamList = { diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index bf296ba0e549..3f519f4de7c4 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -53,7 +53,6 @@ import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PhoneNumber from './PhoneNumber'; import * as PolicyUtils from './PolicyUtils'; import * as ReportActionUtils from './ReportActionsUtils'; -import * as ReportConnection from './ReportConnection'; import * as ReportUtils from './ReportUtils'; import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; @@ -220,7 +219,6 @@ type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: bo type FilterOptionsConfig = Pick & { preferChatroomsOverThreads?: boolean; - includeChatRoomsByParticipants?: boolean; preferPolicyExpenseChat?: boolean; }; @@ -351,35 +349,12 @@ Onyx.connect({ }, {}); }, }); - -let allReportsDraft: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_DRAFT, - waitForCollectionCallback: true, - callback: (value) => (allReportsDraft = value), -}); - let activePolicyID: OnyxEntry; Onyx.connect({ key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, callback: (value) => (activePolicyID = value), }); -/** - * Get the report or draft report given a reportID - */ -function getReportOrDraftReport(reportID: string | undefined): OnyxEntry { - const allReports = ReportConnection.getAllReports(); - if (!allReports && !allReportsDraft) { - return undefined; - } - - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - const draftReport = allReportsDraft?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`]; - - return report ?? draftReport; -} - /** * @param defaultValues {login: accountID} In workspace invite page, when new user is added we pass available data to opt in * @returns Returns avatar data for a list of user accountIDs @@ -473,31 +448,6 @@ function getParticipantsOption(participant: ReportUtils.OptionData | Participant }; } -/** - * Constructs a Set with all possible names (displayName, firstName, lastName, email) for all participants in a report, - * to be used in isSearchStringMatch. - */ -function getParticipantNames(personalDetailList?: Array> | null): Set { - // We use a Set because `Set.has(value)` on a Set of with n entries is up to n (or log(n)) times faster than - // `_.contains(Array, value)` for an Array with n members. - const participantNames = new Set(); - personalDetailList?.forEach((participant) => { - if (participant.login) { - participantNames.add(participant.login.toLowerCase()); - } - if (participant.firstName) { - participantNames.add(participant.firstName.toLowerCase()); - } - if (participant.lastName) { - participantNames.add(participant.lastName.toLowerCase()); - } - if (participant.displayName) { - participantNames.add(PersonalDetailsUtils.getDisplayNameOrDefault(participant).toLowerCase()); - } - }); - return participantNames; -} - /** * A very optimized method to remove duplicates from an array. * Taken from https://stackoverflow.com/a/9229821/9114791 @@ -578,7 +528,7 @@ function getLastActorDisplayName(lastActorDetails: Partial | nu * Update alternate text for the option when applicable */ function getAlternateText(option: ReportUtils.OptionData, {showChatPreviewLine = false, forcePolicyNamePreview = false}: PreviewConfig) { - const report = getReportOrDraftReport(option.reportID); + const report = ReportUtils.getReportOrDraftReport(option.reportID); const isAdminRoom = ReportUtils.isAdminRoom(report); const isAnnounceRoom = ReportUtils.isAnnounceRoom(report); @@ -634,7 +584,7 @@ function getIOUReportIDOfLastAction(report: OnyxEntry): string | undefin if (!ReportActionUtils.isReportPreviewAction(lastAction)) { return; } - return getReportOrDraftReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastAction))?.reportID; + return ReportUtils.getReportOrDraftReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastAction))?.reportID; } /** @@ -675,7 +625,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails const properSchemaForMoneyRequestMessage = ReportUtils.getReportPreviewMessage(report, lastReportAction, true, false, null, true); lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForMoneyRequestMessage); } else if (ReportActionUtils.isReportPreviewAction(lastReportAction)) { - const iouReport = getReportOrDraftReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastReportAction)); + const iouReport = ReportUtils.getReportOrDraftReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastReportAction)); const lastIOUMoneyReportAction = allSortedReportActions[iouReport?.reportID ?? '-1']?.find( (reportAction, key): reportAction is ReportAction => ReportActionUtils.shouldReportActionBeVisible(reportAction, key) && @@ -862,7 +812,7 @@ function createOption( * Get the option for a given report. */ function getReportOption(participant: Participant): ReportUtils.OptionData { - const report = getReportOrDraftReport(participant.reportID); + const report = ReportUtils.getReportOrDraftReport(participant.reportID); const visibleParticipantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report, true); const option = createOption( @@ -896,7 +846,7 @@ function getReportOption(participant: Participant): ReportUtils.OptionData { * Get the option for a policy expense report. */ function getPolicyExpenseReportOption(participant: Participant | ReportUtils.OptionData): ReportUtils.OptionData { - const expenseReport = ReportUtils.isPolicyExpenseChat(participant) ? getReportOrDraftReport(participant.reportID) : null; + const expenseReport = ReportUtils.isPolicyExpenseChat(participant) ? ReportUtils.getReportOrDraftReport(participant.reportID) : null; const visibleParticipantAccountIDs = Object.entries(expenseReport?.participants ?? {}) .filter(([, reportParticipant]) => reportParticipant && !reportParticipant.hidden) @@ -2438,7 +2388,6 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt maxRecentReportsToShow = 0, excludeLogins = [], preferChatroomsOverThreads = false, - includeChatRoomsByParticipants = false, preferPolicyExpenseChat = false, } = config ?? {}; if (searchInputValue.trim() === '' && maxRecentReportsToShow > 0) { @@ -2455,29 +2404,9 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt optionsToExclude.push({login}); }); - const getParticipantsLoginsArray = (item: ReportUtils.OptionData) => { - const keys: string[] = []; - const visibleChatMemberAccountIDs = item.participantsList ?? []; - if (allPersonalDetails) { - visibleChatMemberAccountIDs.forEach((participant) => { - const login = participant?.login; - - if (participant?.displayName) { - keys.push(participant.displayName); - } - - if (login) { - keys.push(login); - keys.push(login.replace(CONST.EMAIL_SEARCH_REGEX, '')); - } - }); - } - - return keys; - }; const matchResults = searchTerms.reduceRight((items, term) => { const recentReports = filterArrayByMatch(items.recentReports, term, (item) => { - let values: string[] = []; + const values: string[] = []; if (item.text) { values.push(item.text); } @@ -2491,21 +2420,10 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt if (item.alternateText) { values.push(item.alternateText); } - values = values.concat(getParticipantsLoginsArray(item)); } else if (!!item.isChatRoom || !!item.isPolicyExpenseChat) { if (item.subtitle) { values.push(item.subtitle); } - - if (includeChatRoomsByParticipants) { - values = values.concat(getParticipantsLoginsArray(item)); - } - } - - if (!item.isChatRoom) { - const participantNames = getParticipantNames(item.participantsList ?? []); - values = values.concat(Array.from(participantNames)); - values = values.concat(getParticipantsLoginsArray(item)); } return uniqFast(values); diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 15c15e113c8c..0a6756034f7d 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -36,6 +36,10 @@ function canUseNetSuiteUSATax(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.NETSUITE_USA_TAX) || canUseAllBetas(betas); } +function canUseWorkspaceRules(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.WORKSPACE_RULES) || canUseAllBetas(betas); +} + /** * Link previews are temporarily disabled. */ @@ -52,4 +56,5 @@ export default { canUseSpotnanaTravel, canUseWorkspaceFeeds, canUseNetSuiteUSATax, + canUseWorkspaceRules, }; diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 6f87e09a61d7..fe3ea7de3bac 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -33,8 +33,19 @@ Onyx.connect({ }, }); -const hiddenTranslation = Localize.translateLocal('common.hidden'); -const youTranslation = Localize.translateLocal('common.you').toLowerCase(); +let hiddenTranslation = ''; +let youTranslation = ''; + +Onyx.connect({ + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + callback: (value) => { + if (!value) { + return; + } + hiddenTranslation = Localize.translateLocal('common.hidden'); + youTranslation = Localize.translateLocal('common.you').toLowerCase(); + }, +}); const regexMergedAccount = new RegExp(CONST.REGEX.MERGED_ACCOUNT_PREFIX); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e6d4d84adc58..38b73ffc2057 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -658,14 +658,7 @@ function getChatType(report: OnyxInputOrEntry | Participant): ValueOf { const allReports = ReportConnection.getAllReports(); - if (!allReports && !allReportsDraft) { - return undefined; - } - - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - const draftReport = allReportsDraft?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`]; - - return report ?? draftReport; + return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? allReportsDraft?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`]; } /** @@ -5820,7 +5813,11 @@ function doesTransactionThreadHaveViolations( if (report?.stateNum !== CONST.REPORT.STATE_NUM.OPEN && report?.stateNum !== CONST.REPORT.STATE_NUM.SUBMITTED) { return false; } - return TransactionUtils.hasViolation(IOUTransactionID, transactionViolations) || TransactionUtils.hasWarningTypeViolation(IOUTransactionID, transactionViolations); + return ( + TransactionUtils.hasViolation(IOUTransactionID, transactionViolations) || + TransactionUtils.hasWarningTypeViolation(IOUTransactionID, transactionViolations) || + TransactionUtils.hasModifiedAmountOrDateViolation(IOUTransactionID, transactionViolations) + ); } /** @@ -7843,6 +7840,7 @@ export { getReportParticipantsTitle, getReportPreviewMessage, getReportRecipientAccountIDs, + getReportOrDraftReport, getRoom, getRootParentReport, getRouteFromLink, diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 3aaf4476eb0b..e5bd5d9b0753 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -4,7 +4,6 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {TransactionMergeParams} from '@libs/API/parameters'; -import {isCorporateCard, isExpensifyCard} from '@libs/CardUtils'; import {getCurrencyDecimals} from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; @@ -507,18 +506,18 @@ function getFormattedCreated(transaction: OnyxInputOrEntry, dateFor * Determine whether a transaction is made with an Expensify card. */ function isExpensifyCardTransaction(transaction: OnyxEntry): boolean { - if (!transaction?.cardID) { - return false; - } - return isExpensifyCard(transaction.cardID); + return transaction?.bank === CONST.EXPENSIFY_CARD.BANK; } /** * Determine whether a transaction is made with a card (Expensify or Company Card). */ function isCardTransaction(transaction: OnyxEntry): boolean { - const cardID = transaction?.cardID ?? -1; - return isCorporateCard(cardID); + return !!transaction?.managedCard; +} + +function getCardName(transaction: OnyxEntry): string { + return transaction?.cardName ?? ''; } /** @@ -734,6 +733,15 @@ function hasWarningTypeViolation(transactionID: string, transactionViolations: O return !!transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.WARNING); } +/** + * Checks if any violations for the provided transaction are of modifiedAmount or modifiedDate + */ +function hasModifiedAmountOrDateViolation(transactionID: string, transactionViolations: OnyxCollection): boolean { + return !!transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some( + (violation: TransactionViolation) => violation.name === CONST.VIOLATIONS.MODIFIED_AMOUNT || violation.name === CONST.VIOLATIONS.MODIFIED_DATE, + ); +} + /** * Calculates tax amount from the given expense amount and tax percentage */ @@ -1107,6 +1115,7 @@ export { hasViolation, hasNoticeTypeViolation, hasWarningTypeViolation, + hasModifiedAmountOrDateViolation, isCustomUnitRateIDForP2P, getRateID, getTransaction, @@ -1116,6 +1125,7 @@ export { buildTransactionsMergeParams, getReimbursable, isPayAtEndExpense, + getCardName, }; export type {TransactionChanges}; diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index cb7731628b25..24e554af111a 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -449,7 +449,7 @@ function redirectThirdPartyDesktopSignIn() { /** * @param shouldAuthenticateWithCurrentAccount Optional, indicates whether default authentication method (shortLivedAuthToken) should be used */ -function beginDeepLinkRedirect(shouldAuthenticateWithCurrentAccount = true) { +function beginDeepLinkRedirect(shouldAuthenticateWithCurrentAccount = true, initialRoute?: string) { // There's no support for anonymous users on desktop if (Session.isAnonymousUser()) { return; @@ -475,7 +475,7 @@ function beginDeepLinkRedirect(shouldAuthenticateWithCurrentAccount = true) { return; } - Browser.openRouteInDesktopApp(response.shortLivedAuthToken, currentUserEmail); + Browser.openRouteInDesktopApp(response.shortLivedAuthToken, currentUserEmail, initialRoute); }); } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index e84a14836efd..5dab6176847d 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -111,6 +111,9 @@ type SendInvoiceInformation = { reportPreviewReportActionID: string; transactionID: string; transactionThreadReportID: string; + createdIOUReportActionID: string; + createdReportActionIDForThread: string; + reportActionID: string; onyxData: OnyxData; }; @@ -161,13 +164,6 @@ Onyx.connect({ }, }); -let allReportsDraft: OnyxCollection; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_DRAFT, - waitForCollectionCallback: true, - callback: (value) => (allReportsDraft = value), -}); - let allTransactions: NonNullable> = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION, @@ -275,21 +271,6 @@ Onyx.connect({ callback: (value) => (activePolicyID = value), }); -/** - * Get the report or draft report given a reportID - */ -function getReportOrDraftReport(reportID: string | undefined): OnyxEntry { - const allReports = ReportConnection.getAllReports(); - if (!allReports && !allReportsDraft) { - return undefined; - } - - const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - const draftReport = allReportsDraft?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`]; - - return report ?? draftReport; -} - /** * Find the report preview action from given chat report and iou report */ @@ -1966,6 +1947,9 @@ function getSendInvoiceInformation( ); return { + createdIOUReportActionID: optimisticCreatedActionForIOUReport.reportActionID, + createdReportActionIDForThread: optimisticCreatedActionForTransactionThread?.reportActionID ?? '-1', + reportActionID: iouAction.reportActionID, senderWorkspaceID, receiver, invoiceRoom: chatReport, @@ -3478,7 +3462,7 @@ function requestMoney( ) { // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); - const currentChatReport = isMoneyRequestReport ? getReportOrDraftReport(report?.chatReportID) : report; + const currentChatReport = isMoneyRequestReport ? ReportUtils.getReportOrDraftReport(report?.chatReportID) : report; const moneyRequestReportID = isMoneyRequestReport ? report?.reportID : ''; const isMovingTransactionFromTrackExpense = IOUUtils.isMovingTransactionFromTrackExpense(action); @@ -3604,10 +3588,25 @@ function sendInvoice( companyName?: string, companyWebsite?: string, ) { - const {senderWorkspaceID, receiver, invoiceRoom, createdChatReportActionID, invoiceReportID, reportPreviewReportActionID, transactionID, transactionThreadReportID, onyxData} = - getSendInvoiceInformation(transaction, currentUserAccountID, invoiceChatReport, receiptFile, policy, policyTagList, policyCategories, companyName, companyWebsite); + const { + senderWorkspaceID, + receiver, + invoiceRoom, + createdChatReportActionID, + invoiceReportID, + reportPreviewReportActionID, + transactionID, + transactionThreadReportID, + createdIOUReportActionID, + createdReportActionIDForThread, + reportActionID, + onyxData, + } = getSendInvoiceInformation(transaction, currentUserAccountID, invoiceChatReport, receiptFile, policy, policyTagList, policyCategories, companyName, companyWebsite); const parameters: SendInvoiceParams = { + createdIOUReportActionID, + createdReportActionIDForThread, + reportActionID, senderWorkspaceID, accountID: currentUserAccountID, amount: transaction?.amount ?? 0, @@ -3663,7 +3662,7 @@ function trackExpense( linkedTrackedExpenseReportID?: string, ) { const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); - const currentChatReport = isMoneyRequestReport ? getReportOrDraftReport(report.chatReportID) : report; + const currentChatReport = isMoneyRequestReport ? ReportUtils.getReportOrDraftReport(report.chatReportID) : report; const moneyRequestReportID = isMoneyRequestReport ? report.reportID : ''; const isMovingTransactionFromTrackExpense = IOUUtils.isMovingTransactionFromTrackExpense(action); @@ -5036,7 +5035,7 @@ function createDistanceRequest( ) { // If the report is an iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); - const currentChatReport = isMoneyRequestReport ? getReportOrDraftReport(report?.chatReportID) : report; + const currentChatReport = isMoneyRequestReport ? ReportUtils.getReportOrDraftReport(report?.chatReportID) : report; const moneyRequestReportID = isMoneyRequestReport ? report?.reportID : ''; const optimisticReceipt: Receipt = { @@ -6421,7 +6420,7 @@ function getReportFromHoldRequestsOnyxData( pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }; - const heldReport = getReportOrDraftReport(holdReportAction.childReportID); + const heldReport = ReportUtils.getReportOrDraftReport(holdReportAction.childReportID); if (heldReport) { optimisticHoldReportExpenseActionIDs.push({optimisticReportActionID: reportActionID, oldReportActionID: holdReportAction.reportActionID}); @@ -6864,7 +6863,7 @@ function hasIOUToApproveOrPay(chatReport: OnyxEntry, excludedI const chatReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`] ?? {}; return Object.values(chatReportActions).some((action) => { - const iouReport = getReportOrDraftReport(action.childReportID ?? '-1'); + const iouReport = ReportUtils.getReportOrDraftReport(action.childReportID ?? '-1'); const policy = PolicyUtils.getPolicy(iouReport?.policyID); const shouldShowSettlementButton = canIOUBePaid(iouReport, chatReport, policy) || canApproveIOU(iouReport, chatReport, policy); return action.childReportID?.toString() !== excludedIOUReportID && action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && shouldShowSettlementButton; @@ -6898,7 +6897,7 @@ function approveMoneyRequest(expenseReport: OnyxEntry, full?: const predictedNextState = isLastApprover(approvalChain) ? CONST.REPORT.STATE_NUM.APPROVED : CONST.REPORT.STATE_NUM.SUBMITTED; const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, predictedNextStatus); - const chatReport = getReportOrDraftReport(expenseReport?.chatReportID); + const chatReport = ReportUtils.getReportOrDraftReport(expenseReport?.chatReportID); const optimisticReportActionsData: OnyxUpdate = { onyxMethod: Onyx.METHOD.MERGE, @@ -7135,7 +7134,7 @@ function submitReport(expenseReport: OnyxTypes.Report) { } const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${expenseReport.reportID}`] ?? null; - const parentReport = getReportOrDraftReport(expenseReport.parentReportID); + const parentReport = ReportUtils.getReportOrDraftReport(expenseReport.parentReportID); const policy = PolicyUtils.getPolicy(expenseReport.policyID); const isCurrentUserManager = currentUserPersonalDetails?.accountID === expenseReport.managerID; const isSubmitAndClosePolicy = PolicyUtils.isSubmitAndClose(policy); @@ -7483,7 +7482,7 @@ function replaceReceipt(transactionID: string, file: File, source: string) { */ function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxEntry): Participant[] { // If the report is iou or expense report, we should get the chat report to set participant for request money - const chatReport = ReportUtils.isMoneyRequestReport(report) ? getReportOrDraftReport(report?.chatReportID) : report; + const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReportOrDraftReport(report?.chatReportID) : report; const currentUserAccountID = currentUserPersonalDetails?.accountID; const shouldAddAsReport = !isEmptyObject(chatReport) && ReportUtils.isSelfDM(chatReport); let participants: Participant[] = []; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index d0913a2b0ac1..19585a5e69c5 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -42,6 +42,7 @@ import type { UpdateWorkspaceGeneralSettingsParams, UpgradeToCorporateParams, } from '@libs/API/parameters'; +import type SetPolicyRulesEnabledParams from '@libs/API/parameters/SetPolicyRulesEnabledParams'; import type UpdatePolicyAddressParams from '@libs/API/parameters/UpdatePolicyAddressParams'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import DateUtils from '@libs/DateUtils'; @@ -1038,6 +1039,9 @@ function updateGeneralSettings(policyID: string, name: string, currencyValue?: s const customUnitID = distanceUnit?.customUnitID; const currency = currencyValue ?? policy?.outputCurrency ?? CONST.CURRENCY.USD; + const currencyPendingAction = currency !== policy?.outputCurrency ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : undefined; + const namePendingAction = name !== policy?.name ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : undefined; + const currentRates = distanceUnit?.rates ?? {}; const optimisticRates: Record = {}; const finallyRates: Record = {}; @@ -1073,12 +1077,14 @@ function updateGeneralSettings(policyID: string, name: string, currencyValue?: s pendingFields: { ...policy.pendingFields, - generalSettings: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + ...(namePendingAction !== undefined && {name: namePendingAction}), + ...(currencyPendingAction !== undefined && {outputCurrency: currencyPendingAction}), }, // Clear errorFields in case the user didn't dismiss the general settings error errorFields: { - generalSettings: null, + name: null, + outputCurrency: null, }, name, outputCurrency: currency, @@ -1099,7 +1105,8 @@ function updateGeneralSettings(policyID: string, name: string, currencyValue?: s key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { pendingFields: { - generalSettings: null, + name: null, + outputCurrency: null, }, ...(customUnitID && { customUnits: { @@ -1113,14 +1120,20 @@ function updateGeneralSettings(policyID: string, name: string, currencyValue?: s }, ]; + const errorFields: Policy['errorFields'] = { + name: namePendingAction && ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.editor.genericFailureMessage'), + }; + + if (!errorFields.name && currencyPendingAction) { + errorFields.outputCurrency = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.editor.genericFailureMessage'); + } + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - errorFields: { - generalSettings: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.editor.genericFailureMessage'), - }, + errorFields, ...(customUnitID && { customUnits: { [customUnitID]: { @@ -1280,12 +1293,29 @@ function updateAddress(policyID: string, newAddress: CompanyAddress) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { address: newAddress, + pendingFields: { + address: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + ]; + + const finallyData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + address: newAddress, + pendingFields: { + address: null, + }, }, }, ]; API.write(WRITE_COMMANDS.UPDATE_POLICY_ADDRESS, parameters, { optimisticData, + finallyData, }); } @@ -1597,7 +1627,9 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName autoReporting: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, approvalMode: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, reimbursementChoice: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - generalSettings: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + name: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + outputCurrency: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + address: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, description: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }, }, @@ -1674,6 +1706,9 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName autoReporting: null, approvalMode: null, reimbursementChoice: null, + name: null, + outputCurrency: null, + address: null, }, }, }, @@ -2940,6 +2975,68 @@ function enablePolicyWorkflows(policyID: string, enabled: boolean) { } } +const DISABLED_MAX_EXPENSE_VALUES: Pick = { + maxExpenseAmountNoReceipt: CONST.DISABLED_MAX_EXPENSE_VALUE, + maxExpenseAmount: CONST.DISABLED_MAX_EXPENSE_VALUE, + maxExpenseAge: CONST.DISABLED_MAX_EXPENSE_VALUE, +}; + +function enablePolicyRules(policyID: string, enabled: boolean, disableRedirect = false) { + const policy = getPolicy(policyID); + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + areRulesEnabled: enabled, + ...(!enabled ? DISABLED_MAX_EXPENSE_VALUES : {}), + pendingFields: { + areRulesEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + pendingFields: { + areRulesEnabled: null, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + areRulesEnabled: !enabled, + ...(!enabled + ? { + maxExpenseAmountNoReceipt: policy?.maxExpenseAmountNoReceipt, + maxExpenseAmount: policy?.maxExpenseAmount, + maxExpenseAge: policy?.maxExpenseAge, + } + : {}), + pendingFields: { + areRulesEnabled: null, + }, + }, + }, + ], + }; + + const parameters: SetPolicyRulesEnabledParams = {policyID, enabled}; + API.write(WRITE_COMMANDS.SET_POLICY_RULES_ENABLED, parameters, onyxData); + + if (enabled && getIsNarrowLayout() && !disableRedirect) { + navigateWhenEnableFeature(policyID); + } +} + function enableDistanceRequestTax(policyID: string, customUnitName: string, customUnitID: string, attributes: Attributes) { const policy = getPolicy(policyID); const onyxData: OnyxData = { @@ -3367,6 +3464,7 @@ export { getAdminPoliciesConnectedToNetSuite, getAdminPoliciesConnectedToSageIntacct, hasInvoicingDetails, + enablePolicyRules, }; export type {NewCustomUnit}; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 2611dae20d7e..7eb7e01d6edd 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1238,6 +1238,11 @@ function readNewestAction(reportID: string, shouldResetUnreadMarker = false) { * Sets the last read time on a report */ function markCommentAsUnread(reportID: string, reportActionCreated: string) { + if (reportID === '-1') { + Log.warn('7339cd6c-3263-4f89-98e5-730f0be15784 Invalid report passed to MarkCommentAsUnread. Not calling the API because it wil fail.'); + return; + } + const reportActions = allReportActions?.[reportID]; // Find the latest report actions from other users diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 040fd6e47491..b51955c9cc59 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -97,9 +97,10 @@ function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { type Params = Record; -function exportSearchItemsToCSV({query, reportIDList, transactionIDList, policyIDs}: ExportSearchItemsToCSVParams, onDownloadFailed: () => void) { +function exportSearchItemsToCSV({query, jsonQuery, reportIDList, transactionIDList, policyIDs}: ExportSearchItemsToCSVParams, onDownloadFailed: () => void) { const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_SEARCH_ITEMS_TO_CSV, { query, + jsonQuery, reportIDList, transactionIDList, policyIDs, diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index c06e6087d336..b687abd61bb9 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -1001,6 +1001,7 @@ function handleExitToNavigation(exitTo: Route | HybridAppRoute) { waitForUserSignIn().then(() => { Navigation.waitForProtectedRoutes().then(() => { const url = NativeModules.HybridAppModule ? Navigation.parseHybridAppUrl(exitTo) : (exitTo as Route); + Navigation.goBack(); Navigation.navigate(url); }); }); diff --git a/src/pages/ReimbursementAccount/EnableBankAccount/EnableBankAccount.tsx b/src/pages/ReimbursementAccount/EnableBankAccount/EnableBankAccount.tsx index 8687ff89ba62..599a7a1cf6f1 100644 --- a/src/pages/ReimbursementAccount/EnableBankAccount/EnableBankAccount.tsx +++ b/src/pages/ReimbursementAccount/EnableBankAccount/EnableBankAccount.tsx @@ -42,7 +42,7 @@ function EnableBankAccount({reimbursementAccount, user, onBackButtonPress}: Enab const achData = reimbursementAccount?.achData ?? {}; const {icon, iconSize} = getBankIcon({bankName: achData.bankName, styles}); const isUsingExpensifyCard = user?.isUsingExpensifyCard; - const formattedBankAccountNumber = achData.accountNumber ? `${translate('paymentMethodList.accountLastFour')} ${achData.accountNumber.slice(-4)}` : ''; + const formattedBankAccountNumber = achData.accountNumber ? `${translate('bankAccount.accountEnding')} ${achData.accountNumber.slice(-4)}` : ''; const bankAccountOwnerName = achData.addressName; const errors = reimbursementAccount?.errors ?? {}; const pendingAction = reimbursementAccount?.pendingAction; diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index a3b5a98c8d2f..db7e482a0457 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -470,6 +470,9 @@ const ContextMenuActions: ContextMenuAction[] = [ setClipboardMessage(ReportActionsUtils.getPolicyChangeLogChangeRoleMessage(reportAction)); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_EMPLOYEE) { setClipboardMessage(ReportActionsUtils.getPolicyChangeLogDeleteMemberMessage(reportAction)); + } else if (ReportActionsUtils.isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.INTEGRATION_SYNC_FAILED)) { + const {label, errorMessage} = ReportActionsUtils.getOriginalMessage(reportAction) ?? {label: '', errorMessage: ''}; + setClipboardMessage(Localize.translateLocal('report.actions.type.integrationSyncFailed', label, errorMessage)); } else if (content) { setClipboardMessage( content.replace(/()(.*?)(<\/mention-user>)/gi, (match, openTag: string, innerContent: string, closeTag: string): string => { @@ -637,7 +640,6 @@ const ContextMenuActions: ContextMenuAction[] = [ ]; const restrictedReadOnlyActions: TranslationPaths[] = [ - 'common.download', 'reportActionContextMenu.replyInThread', 'reportActionContextMenu.editAction', 'reportActionContextMenu.joinThread', diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 005824fa949f..da7b4f72bfb6 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -1,4 +1,5 @@ import {useNavigation} from '@react-navigation/native'; +import noop from 'lodash/noop'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputFocusEventData, TextInputSelectionChangeEventData} from 'react-native'; import {View} from 'react-native'; @@ -31,6 +32,7 @@ import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import {getDraftComment} from '@libs/DraftCommentUtils'; import getModalState from '@libs/getModalState'; +import Performance from '@libs/Performance'; import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; @@ -116,6 +118,9 @@ const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); +// eslint-disable-next-line import/no-mutable-exports +let onSubmitAction = noop; + function ReportActionCompose({ blockedFromConcierge, currentUserPersonalDetails, @@ -309,6 +314,7 @@ function ReportActionCompose({ Report.addAttachment(reportID, attachmentFileRef.current, newCommentTrimmed); attachmentFileRef.current = null; } else { + Performance.markStart(CONST.TIMING.MESSAGE_SENT, {message: newCommentTrimmed}); onSubmit(newCommentTrimmed); } }, @@ -390,6 +396,9 @@ function ReportActionCompose({ clearComposer(); }, [isSendDisabled, isReportReadyForDisplay, composerRefShared]); + // eslint-disable-next-line react-compiler/react-compiler + onSubmitAction = handleSendMessage; + const emojiShiftVertical = useMemo(() => { const chatItemComposeSecondaryRowHeight = styles.chatItemComposeSecondaryRow.height + styles.chatItemComposeSecondaryRow.marginTop + styles.chatItemComposeSecondaryRow.marginBottom; const reportActionComposeHeight = styles.chatItemComposeBox.minHeight + chatItemComposeSecondaryRowHeight; @@ -594,5 +603,5 @@ export default withCurrentUserPersonalDetails( }, })(memo(ReportActionCompose)), ); - +export {onSubmitAction}; export type {SuggestionsRef, ComposerRef}; diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index c30d71038096..e4dab8518eb2 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -673,6 +673,9 @@ function ReportActionItem({ } else if (ReportActionsUtils.isRenamedAction(action)) { const message = ReportActionsUtils.getRenamedAction(action); children = ; + } else if (ReportActionsUtils.isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.INTEGRATION_SYNC_FAILED)) { + const {label, errorMessage} = ReportActionsUtils.getOriginalMessage(action) ?? {label: '', errorMessage: ''}; + children = ; } else { const hasBeenFlagged = ![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === moderationDecision) && diff --git a/src/pages/home/report/ReportActionItemThread.tsx b/src/pages/home/report/ReportActionItemThread.tsx index d18adf6ba0d2..94a8592d9607 100644 --- a/src/pages/home/report/ReportActionItemThread.tsx +++ b/src/pages/home/report/ReportActionItemThread.tsx @@ -46,7 +46,7 @@ function ReportActionItemThread({numberOfReplies, icons, mostRecentReply, childR { Report.navigateToAndOpenChildReport(childReportID); - Timing.start(CONST.TIMING.SWITCH_REPORT_THREAD); + Timing.start(CONST.TIMING.OPEN_REPORT_THREAD); }} role={CONST.ROLE.BUTTON} accessibilityLabel={`${numberOfReplies} ${replyText}`} diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 463bbe119514..0386b37f1aa9 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -436,8 +436,8 @@ function ReportActionsView({ Performance.markEnd(CONST.TIMING.SWITCH_REPORT); } Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActionOnFirstRender ? CONST.TIMING.WARM : CONST.TIMING.COLD); - Timing.end(CONST.TIMING.SWITCH_REPORT_THREAD); - Timing.end(CONST.TIMING.SWITCH_REPORT_FROM_PREVIEW); + Timing.end(CONST.TIMING.OPEN_REPORT_THREAD); + Timing.end(CONST.TIMING.OPEN_REPORT_FROM_PREVIEW); }, [hasCachedActionOnFirstRender]); // Check if the first report action in the list is the one we're currently linked to diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index 68827de96172..9d1a4d9ad4b7 100644 --- a/src/pages/home/report/comment/TextCommentFragment.tsx +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -1,6 +1,6 @@ import {Str} from 'expensify-common'; import {isEmpty} from 'lodash'; -import React, {memo} from 'react'; +import React, {memo, useEffect} from 'react'; import type {StyleProp, TextStyle} from 'react-native'; import Text from '@components/Text'; import ZeroWidthView from '@components/ZeroWidthView'; @@ -11,6 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import convertToLTR from '@libs/convertToLTR'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as EmojiUtils from '@libs/EmojiUtils'; +import Performance from '@libs/Performance'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; @@ -48,6 +49,10 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + useEffect(() => { + Performance.markEnd(CONST.TIMING.MESSAGE_SENT, {message: text}); + }, [text]); + // If the only difference between fragment.text and fragment.html is
tags and emoji tag // on native, we render it as text, not as html // on other device, only render it as text if the only difference is
tag diff --git a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx index 81d9577d4d4d..3b42b1d9a1be 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceRate.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import Text from '@components/Text'; @@ -8,7 +8,6 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as IOU from '@libs/actions/IOU'; import * as CurrencyUtils from '@libs/CurrencyUtils'; -import type {MileageRate} from '@libs/DistanceRequestUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getCustomUnitRate, isTaxTrackingEnabled} from '@libs/PolicyUtils'; @@ -33,9 +32,6 @@ type IOURequestStepDistanceRateOnyxProps = { /** Collection of tags attached to the policy */ policyTags: OnyxEntry; - - /** Mileage rates */ - rates: Record; }; type IOURequestStepDistanceRateProps = IOURequestStepDistanceRateOnyxProps & @@ -45,16 +41,21 @@ type IOURequestStepDistanceRateProps = IOURequestStepDistanceRateOnyxProps & }; function IOURequestStepDistanceRate({ - policy, + policy: policyReal, report, + reportDraft, route: { params: {action, reportID, backTo, transactionID}, }, transaction, - rates, policyTags, policyCategories, }: IOURequestStepDistanceRateProps) { + const [policyDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${IOU.getIOURequestPolicyID(transaction, reportDraft) ?? '-1'}`); + + const policy = policyReal ?? policyDraft; + const rates = DistanceRequestUtils.getMileageRates(policy); + const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); @@ -138,10 +139,6 @@ const IOURequestStepDistanceRateWithOnyx = withOnyx `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, }, - rates: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? '-1'}`, - selector: (policy: OnyxEntry) => DistanceRequestUtils.getMileageRates(policy), - }, })(IOURequestStepDistanceRate); // eslint-disable-next-line rulesdir/no-negated-variables diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index 7fbc8d260f8a..552ad4d54e39 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -4,6 +4,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormHelpMessage from '@components/FormHelpMessage'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import {READ_COMMANDS} from '@libs/API/types'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; @@ -45,6 +46,7 @@ function IOURequestStepParticipants({ const {translate} = useLocalize(); const styles = useThemeStyles(); const isFocused = useIsFocused(); + const {canUseP2PDistanceRequests} = usePermissions(iouType); // We need to set selectedReportID if user has navigated back from confirmation page and navigates to confirmation page with already selected participant const selectedReportID = useRef(participants?.length === 1 ? participants[0]?.reportID ?? reportID : reportID); @@ -93,7 +95,7 @@ function IOURequestStepParticipants({ HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS); const firstParticipantReportID = val[0]?.reportID ?? ''; - const rateID = DistanceRequestUtils.getCustomUnitRateID(firstParticipantReportID); + const rateID = DistanceRequestUtils.getCustomUnitRateID(firstParticipantReportID, !canUseP2PDistanceRequests); const isInvoice = iouType === CONST.IOU.TYPE.INVOICE && ReportUtils.isInvoiceRoomWithID(firstParticipantReportID); numberOfParticipants.current = val.length; @@ -110,7 +112,7 @@ function IOURequestStepParticipants({ // When a participant is selected, the reportID needs to be saved because that's the reportID that will be used in the confirmation step. selectedReportID.current = firstParticipantReportID || reportID; }, - [iouType, reportID, transactionID], + [iouType, reportID, transactionID, canUseP2PDistanceRequests], ); const goToNextStep = useCallback(() => { diff --git a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx index 7f753e02ee6d..3f5db6cf5613 100644 --- a/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx +++ b/src/pages/settings/AboutPage/ShareLogList/BaseShareLogList.tsx @@ -60,7 +60,6 @@ function BaseShareLogList({onAttachLogToReport}: BaseShareLogListProps) { } const filteredOptions = OptionsListUtils.filterOptions(defaultOptions, debouncedSearchValue, { - includeChatRoomsByParticipants: true, preferChatroomsOverThreads: true, sortByReportTypeInSearch: true, }); diff --git a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx index ab148f8d8edc..d74e126ff94e 100644 --- a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx +++ b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx @@ -107,6 +107,7 @@ function RequestEarlyCancellationPage() { footerText={{acknowledgementText}} isNoteRequired isLoading={isLoading} + enabledWhenOffline={false} /> ), [acknowledgementText, isLoading, styles.flex1, styles.mb2, styles.mt4, translate], diff --git a/src/pages/signin/UnlinkLoginForm.tsx b/src/pages/signin/UnlinkLoginForm.tsx index 2fa43fbe61b2..9ccbbaaa56cd 100644 --- a/src/pages/signin/UnlinkLoginForm.tsx +++ b/src/pages/signin/UnlinkLoginForm.tsx @@ -32,6 +32,7 @@ function UnlinkLoginForm({account, credentials}: UnlinkLoginFormProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); + const unlinkMessage = account?.message === 'unlinkLoginForm.linkSent' || account?.message === 'unlinkLoginForm.succesfullyUnlinkedLogin' ? translate(account?.message) : account?.message; const primaryLogin = useMemo(() => { if (!account?.primaryLogin) { return ''; @@ -53,13 +54,13 @@ function UnlinkLoginForm({account, credentials}: UnlinkLoginFormProps) { {translate('unlinkLoginForm.noLongerHaveAccess', {primaryLogin})} - {!!account?.message && ( + {!!unlinkMessage && ( // DotIndicatorMessage mostly expects onyxData errors, so we need to mock an object so that the messages looks similar to prop.account.errors )} {!!account?.errors && !isEmptyObject(account.errors) && ( diff --git a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx index df894ac3e3a7..1754c512bd6b 100644 --- a/src/pages/tasks/TaskShareDestinationSelectorModal.tsx +++ b/src/pages/tasks/TaskShareDestinationSelectorModal.tsx @@ -91,7 +91,6 @@ function TaskShareDestinationSelectorModal() { const filteredReports = OptionsListUtils.filterOptions(defaultOptions, debouncedSearchValue.trim(), { excludeLogins: CONST.EXPENSIFY_EMAILS, canInviteUser: false, - includeChatRoomsByParticipants: true, }); const header = OptionsListUtils.getHeaderMessage(filteredReports.recentReports && filteredReports.recentReports.length !== 0, false, debouncedSearchValue); return {...filteredReports, header}; diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 115a24691838..76ef67bdb0f0 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -16,6 +16,7 @@ import ScrollView from '@components/ScrollView'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useSingleExecution from '@hooks/useSingleExecution'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -64,7 +65,8 @@ type WorkspaceMenuItem = { | typeof SCREENS.WORKSPACE.PROFILE | typeof SCREENS.WORKSPACE.MEMBERS | typeof SCREENS.WORKSPACE.EXPENSIFY_CARD - | typeof SCREENS.WORKSPACE.REPORT_FIELDS; + | typeof SCREENS.WORKSPACE.REPORT_FIELDS + | typeof SCREENS.WORKSPACE.RULES; }; type WorkspaceInitialPageOnyxProps = { @@ -99,6 +101,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc const activeRoute = useNavigationState(getTopmostRouteName); const {translate} = useLocalize(); const {isOffline} = useNetwork(); + const {canUseWorkspaceRules} = usePermissions(); const wasRendered = useRef(false); const prevPendingFields = usePrevious(policy?.pendingFields); @@ -112,6 +115,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc [CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED]: !!policy?.areConnectionsEnabled || !isEmptyObject(policy?.connections), [CONST.POLICY.MORE_FEATURES.ARE_EXPENSIFY_CARDS_ENABLED]: policy?.areExpensifyCardsEnabled, [CONST.POLICY.MORE_FEATURES.ARE_REPORT_FIELDS_ENABLED]: policy?.areReportFieldsEnabled, + [CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED]: policy?.areRulesEnabled, }), [policy], ) as PolicyFeatureStates; @@ -162,7 +166,11 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc const hasMembersError = PolicyUtils.hasEmployeeListError(policy); const hasPolicyCategoryError = PolicyUtils.hasPolicyCategoriesError(policyCategories); - const hasGeneralSettingsError = !isEmptyObject(policy?.errorFields?.generalSettings ?? {}) || !isEmptyObject(policy?.errorFields?.avatarURL ?? {}); + const hasGeneralSettingsError = + !isEmptyObject(policy?.errorFields?.name ?? {}) || + !isEmptyObject(policy?.errorFields?.avatarURL ?? {}) || + !isEmptyObject(policy?.errorFields?.ouputCurrency ?? {}) || + !isEmptyObject(policy?.errorFields?.address ?? {}); const {login} = useCurrentUserPersonalDetails(); const shouldShowProtectedItems = PolicyUtils.isPolicyAdmin(policy, login); const isPaidGroupPolicy = PolicyUtils.isPaidGroupPolicy(policy); @@ -306,6 +314,15 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc }); } + if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED] && canUseWorkspaceRules) { + protectedCollectPolicyMenuItems.push({ + translationKey: 'workspace.common.rules', + icon: Expensicons.Feed, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_RULES.getRoute(policyID)))), + routeName: SCREENS.WORKSPACE.RULES, + }); + } + protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.moreFeatures', icon: Expensicons.Gear, diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 47af86b53315..d33a83c4363c 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -61,7 +61,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {translate} = useLocalize(); - const {canUseWorkspaceFeeds} = usePermissions(); + const {canUseWorkspaceFeeds, canUseWorkspaceRules} = usePermissions(); const hasAccountingConnection = !isEmptyObject(policy?.connections); const isAccountingEnabled = !!policy?.areConnectionsEnabled || !isEmptyObject(policy?.connections); const isSyncTaxEnabled = @@ -90,19 +90,6 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro DistanceRate.enablePolicyDistanceRates(policyID, isEnabled); }, }, - { - icon: Illustrations.Workflows, - titleTranslationKey: 'workspace.moreFeatures.workflows.title', - subtitleTranslationKey: 'workspace.moreFeatures.workflows.subtitle', - isActive: policy?.areWorkflowsEnabled ?? false, - pendingAction: policy?.pendingFields?.areWorkflowsEnabled, - action: (isEnabled: boolean) => { - if (!policyID) { - return; - } - Policy.enablePolicyWorkflows(policyID, isEnabled); - }, - }, ]; // TODO remove this when feature will be fully done, and move spend item inside spendItems array @@ -126,6 +113,44 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro }); } + const manageItems: Item[] = [ + { + icon: Illustrations.Workflows, + titleTranslationKey: 'workspace.moreFeatures.workflows.title', + subtitleTranslationKey: 'workspace.moreFeatures.workflows.subtitle', + isActive: policy?.areWorkflowsEnabled ?? false, + pendingAction: policy?.pendingFields?.areWorkflowsEnabled, + action: (isEnabled: boolean) => { + if (!policyID) { + return; + } + Policy.enablePolicyWorkflows(policyID, isEnabled); + }, + }, + ]; + + // TODO remove this when feature will be fully done, and move manage item inside manageItems array + if (canUseWorkspaceRules) { + manageItems.splice(1, 0, { + icon: Illustrations.Rules, + titleTranslationKey: 'workspace.moreFeatures.rules.title', + subtitleTranslationKey: 'workspace.moreFeatures.rules.subtitle', + isActive: policy?.areRulesEnabled ?? false, + pendingAction: policy?.pendingFields?.areRulesEnabled, + action: (isEnabled: boolean) => { + if (!policyID) { + return; + } + + if (isEnabled && !isControlPolicy(policy)) { + Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.rules.alias, ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID))); + return; + } + Policy.enablePolicyRules(policyID, isEnabled); + }, + }); + } + const earnItems: Item[] = [ { icon: Illustrations.InvoiceBlue, @@ -262,6 +287,11 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro subtitleTranslationKey: 'workspace.moreFeatures.spendSection.subtitle', items: spendItems, }, + { + titleTranslationKey: 'workspace.moreFeatures.manageSection.title', + subtitleTranslationKey: 'workspace.moreFeatures.manageSection.subtitle', + items: manageItems, + }, { titleTranslationKey: 'workspace.moreFeatures.earnSection.title', subtitleTranslationKey: 'workspace.moreFeatures.earnSection.subtitle', diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 025cc9587bf6..8cd5c7c1127b 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -36,8 +36,8 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import withPolicy from './withPolicy'; import type {WithPolicyProps} from './withPolicy'; +import withPolicy from './withPolicy'; import WorkspacePageWithSections from './WorkspacePageWithSections'; type WorkspaceProfilePageOnyxProps = { @@ -196,7 +196,7 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, currencyList = { disabledStyle={styles.cursorDefault} errorRowStyles={styles.mt3} /> - + )} Policy.clearPolicyErrorField(policy?.id ?? '-1', CONST.POLICY.COLLECTION_KEYS.GENERAL_SETTINGS)} errorRowStyles={[styles.mt2]} @@ -249,7 +249,7 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, currencyList = {
{canUseSpotnanaTravel && shouldShowAddress && ( - + item && toggleValue(item)} sections={listValuesSections} onCheckboxPress={toggleValue} diff --git a/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx b/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx index d4cc5a65e272..872946905974 100644 --- a/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx +++ b/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx @@ -57,7 +57,7 @@ function WorkspaceReportFieldsPage({ params: {policyID}, }, }: WorkspaceReportFieldsPageProps) { - const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); @@ -78,7 +78,7 @@ function WorkspaceReportFieldsPage({ const isConnectedToAccounting = Object.keys(policy?.connections ?? {}).length > 0; const currentConnectionName = PolicyUtils.getCurrentConnectionName(policy); - const canSelectMultiple = !hasAccountingConnections && shouldUseNarrowLayout ? selectionMode?.isEnabled : true; + const canSelectMultiple = !hasAccountingConnections && (isSmallScreenWidth ? selectionMode?.isEnabled : true); const fetchReportFields = useCallback(() => { ReportField.openPolicyReportFieldsPage(policyID); @@ -297,7 +297,7 @@ function WorkspaceReportFieldsPage({ {!shouldShowEmptyState && !isLoading && ( item && updateSelectedReportFields(item)} sections={reportFieldsSections} onCheckboxPress={updateSelectedReportFields} diff --git a/src/pages/workspace/rules/PolicyRulesPage.tsx b/src/pages/workspace/rules/PolicyRulesPage.tsx new file mode 100644 index 000000000000..ec7cdffb8df5 --- /dev/null +++ b/src/pages/workspace/rules/PolicyRulesPage.tsx @@ -0,0 +1,106 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import {View} from 'react-native'; +import Section from '@components/Section'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; +import usePolicy from '@hooks/usePolicy'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; +import * as Illustrations from '@src/components/Icon/Illustrations'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type PolicyRulesPageProps = StackScreenProps; + +function PolicyRulesPage({route}: PolicyRulesPageProps) { + const {translate} = useLocalize(); + const {policyID} = route.params; + const policy = usePolicy(policyID); + const styles = useThemeStyles(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {canUseWorkspaceRules} = usePermissions(); + + const handleOnPressCategoriesLink = () => { + if (policy?.areCategoriesEnabled) { + Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)); + return; + } + + Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)); + }; + + const handleOnPressTagsLink = () => { + if (policy?.areTagsEnabled) { + Navigation.navigate(ROUTES.WORKSPACE_TAGS.getRoute(policyID)); + return; + } + + Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)); + }; + + return ( + + + +
( + + {translate('workspace.rules.individualExpenseRules.subtitle')}{' '} + + {translate('workspace.common.categories').toLowerCase()} + {' '} + {translate('common.and')}{' '} + + {translate('workspace.common.tags').toLowerCase()} + + . + + )} + subtitle={translate('workspace.rules.individualExpenseRules.subtitle')} + titleStyles={styles.accountSettingsSectionTitle} + /> +
+ + + + ); +} + +PolicyRulesPage.displayName = 'PolicyRulesPage'; + +export default PolicyRulesPage; diff --git a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx index 0837ac164600..bb4bdff27097 100644 --- a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx +++ b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx @@ -47,6 +47,9 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) { case CONST.UPGRADE_FEATURE_INTRO_MAPPING.reportFields.id: Policy.enablePolicyReportFields(policyID, true, true); return Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)); + case CONST.UPGRADE_FEATURE_INTRO_MAPPING.rules.id: + Policy.enablePolicyRules(policyID, true, true); + return Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)); default: return route.params.backTo ? Navigation.navigate(route.params.backTo) : Navigation.goBack(); } diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index dbd7a7cb3a58..91f069ac2224 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -45,6 +45,7 @@ type PolicyRoute = RouteProp< | typeof SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE | typeof SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS | typeof SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION + | typeof SCREENS.WORKSPACE.RULES >; function getPolicyIDFromRoute(route: PolicyRoute): string { diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index 18b1314c9b1e..23f341d308e2 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -127,7 +127,7 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr } const hasReimburserError = !!policy?.errorFields?.reimburser; const hasApprovalError = !!policy?.errorFields?.approvalMode; - const hasDelayedSubmissionError = !!policy?.errorFields?.autoReporting; + const hasDelayedSubmissionError = !!policy?.errorFields?.autoReporting ?? !!policy?.errorFields?.autoReportingFrequency; return [ { @@ -158,7 +158,7 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr /> ), isActive: (policy?.autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT && !hasDelayedSubmissionError) ?? false, - pendingAction: policy?.pendingFields?.autoReporting, + pendingAction: policy?.pendingFields?.autoReporting ?? policy?.pendingFields?.autoReportingFrequency, errors: ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.AUTOREPORTING), onCloseError: () => Policy.clearPolicyErrorField(route.params.policyID, CONST.POLICY.COLLECTION_KEYS.AUTOREPORTING), }, diff --git a/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx b/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx index a0b9beb5bec4..9864241a8e84 100644 --- a/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx +++ b/src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx @@ -98,7 +98,7 @@ function ApprovalWorkflowEditor({approvalWorkflow, removeApprovalWorkflow, polic // User should be allowed to add additional approver only if they upgraded to Control Plan, otherwise redirected to the Upgrade Page const addAdditionalApprover = useCallback(() => { if (!PolicyUtils.isControlPolicy(policy)) { - Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.alias, ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID))); + Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.approvals.alias, Navigation.getActiveRoute())); return; } Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_APPROVER.getRoute(policyID, approvalWorkflow.approvers.length, ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.getRoute(policyID))); diff --git a/src/styles/index.ts b/src/styles/index.ts index fe65e48bc4a1..9f93c799abb5 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3777,7 +3777,7 @@ const styles = (theme: ThemeColors) => paymentMethod: { paddingHorizontal: 20, - height: variables.optionRowHeight, + minHeight: variables.optionRowHeight, }, chatFooterBanner: { diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 8bcd5932bfc6..c18992923c21 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -496,6 +496,20 @@ type OriginalMessageUnapproved = { */ type OriginalMessageAddPaymentCard = Record; +/** + * Original message for INTEGRATIONSYNCFAILED actions + */ +type OriginalMessageIntegrationSyncFailed = { + /** The user friendly connection name */ + label: string; + + /** The source of the connection sync */ + source: string; + + /** The error message from Integration Server */ + errorMessage: string; +}; + /** The map type of original message */ type OriginalMessageMap = { /** */ @@ -624,6 +638,8 @@ type OriginalMessageMap = { [CONST.REPORT.ACTIONS.TYPE.CARD_MISSING_ADDRESS]: never; /** */ [CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED_VIRTUAL]: never; + /** */ + [CONST.REPORT.ACTIONS.TYPE.INTEGRATION_SYNC_FAILED]: OriginalMessageIntegrationSyncFailed; } & OldDotOriginalMessageMap & { [T in ValueOf]: OriginalMessageChangeLog; } & { diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 8f48205d8749..9bac5f2e4de4 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1510,6 +1510,9 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< /** Whether the workflows feature is enabled */ areWorkflowsEnabled?: boolean; + /** Whether the reules feature is enabled */ + areRulesEnabled?: boolean; + /** Whether the Report Fields feature is enabled */ areReportFieldsEnabled?: boolean; @@ -1555,7 +1558,7 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< /** Workspace account ID configured for Expensify Card */ workspaceAccountID?: number; } & Partial, - 'generalSettings' | 'addWorkspaceRoom' | 'employeeList' | keyof ACHAccount | keyof Attributes + 'addWorkspaceRoom' | 'employeeList' | keyof ACHAccount | keyof Attributes >; /** Stages of policy connection sync */ diff --git a/tests/actions/EnforceActionExportRestrictions.ts b/tests/actions/EnforceActionExportRestrictions.ts index bd2a13304078..9590f878116e 100644 --- a/tests/actions/EnforceActionExportRestrictions.ts +++ b/tests/actions/EnforceActionExportRestrictions.ts @@ -28,11 +28,6 @@ describe('ReportUtils', () => { // @ts-expect-error the test is asserting that it's undefined, so the TS error is normal expect(ReportUtils.getAllReportActions).toBeUndefined(); }); - - it('does not export getReport', () => { - // @ts-expect-error the test is asserting that it's undefined, so the TS error is normal - expect(ReportUtils.getReportOrDraftReport).toBeUndefined(); - }); }); describe('Policy', () => { diff --git a/tests/e2e/config.ts b/tests/e2e/config.ts index 8963e07c31c8..bdb24bee0bc5 100644 --- a/tests/e2e/config.ts +++ b/tests/e2e/config.ts @@ -83,6 +83,7 @@ export default { }, // Crowded Policy (Do Not Delete) Report, has a input bar available: reportID: '8268282951170052', + message: `Measure_performance#${Math.floor(Math.random() * 1000000)}`, }, [TEST_NAMES.ChatOpening]: { name: TEST_NAMES.ChatOpening, diff --git a/tests/e2e/nativeCommands/adbBackspace.ts b/tests/e2e/nativeCommands/adbBackspace.ts index 2891d1daf0e9..112a80bdb37e 100644 --- a/tests/e2e/nativeCommands/adbBackspace.ts +++ b/tests/e2e/nativeCommands/adbBackspace.ts @@ -1,10 +1,9 @@ import execAsync from '../utils/execAsync'; import * as Logger from '../utils/logger'; -const adbBackspace = () => { +const adbBackspace = (): Promise => { Logger.log(`🔙 Pressing backspace`); - execAsync(`adb shell input keyevent KEYCODE_DEL`); - return true; + return execAsync(`adb shell input keyevent KEYCODE_DEL`).then(() => true); }; export default adbBackspace; diff --git a/tests/e2e/nativeCommands/adbTypeText.ts b/tests/e2e/nativeCommands/adbTypeText.ts index 72fefbd25d26..deea16b198c8 100644 --- a/tests/e2e/nativeCommands/adbTypeText.ts +++ b/tests/e2e/nativeCommands/adbTypeText.ts @@ -3,8 +3,7 @@ import * as Logger from '../utils/logger'; const adbTypeText = (text: string) => { Logger.log(`📝 Typing text: ${text}`); - execAsync(`adb shell input text "${text}"`); - return true; + return execAsync(`adb shell input text "${text}"`).then(() => true); }; export default adbTypeText; diff --git a/tests/e2e/nativeCommands/index.ts b/tests/e2e/nativeCommands/index.ts index 31af618c8ec1..310aa2ab3c22 100644 --- a/tests/e2e/nativeCommands/index.ts +++ b/tests/e2e/nativeCommands/index.ts @@ -4,7 +4,7 @@ import adbTypeText from './adbTypeText'; // eslint-disable-next-line rulesdir/prefer-import-module-contents import {NativeCommandsAction} from './NativeCommandsAction'; -const executeFromPayload = (actionName?: string, payload?: NativeCommandPayload): boolean => { +const executeFromPayload = (actionName?: string, payload?: NativeCommandPayload): Promise => { switch (actionName) { case NativeCommandsAction.scroll: throw new Error('Not implemented yet'); diff --git a/tests/e2e/server/index.ts b/tests/e2e/server/index.ts index 494b8b4644e1..512492908049 100644 --- a/tests/e2e/server/index.ts +++ b/tests/e2e/server/index.ts @@ -169,8 +169,8 @@ const createServerInstance = (): ServerInstance => { case Routes.testNativeCommand: { getPostJSONRequestData(req, res) - ?.then((data) => { - const status = nativeCommands.executeFromPayload(data?.actionName, data?.payload); + ?.then((data) => nativeCommands.executeFromPayload(data?.actionName, data?.payload)) + .then((status) => { if (status) { res.end('ok'); return; diff --git a/tests/perf-test/ReportActionCompose.perf-test.tsx b/tests/perf-test/ReportActionCompose.perf-test.tsx index e3aaccd1f050..f8f3ce499218 100644 --- a/tests/perf-test/ReportActionCompose.perf-test.tsx +++ b/tests/perf-test/ReportActionCompose.perf-test.tsx @@ -1,5 +1,5 @@ import {fireEvent, screen} from '@testing-library/react-native'; -import type {ComponentType} from 'react'; +import type {ComponentType, EffectCallback} from 'react'; import React from 'react'; import Onyx from 'react-native-onyx'; import type Animated from 'react-native-reanimated'; @@ -36,6 +36,7 @@ jest.mock('@react-navigation/native', () => { }), useIsFocused: () => true, useNavigationState: () => {}, + useFocusEffect: (cb: EffectCallback) => cb(), }; }); diff --git a/tests/unit/IOUUtilsTest.ts b/tests/unit/IOUUtilsTest.ts index b8640e4ecdf1..04a5b3babd5e 100644 --- a/tests/unit/IOUUtilsTest.ts +++ b/tests/unit/IOUUtilsTest.ts @@ -114,6 +114,24 @@ describe('IOUUtils', () => { expect(IOUUtils.calculateAmount(participantsAccountIDs.length, 100, 'BHD')).toBe(33); }); }); + + describe('insertTagIntoTransactionTagsString', () => { + test('Inserting a tag into tag string should update the tag', () => { + expect(IOUUtils.insertTagIntoTransactionTagsString(':NY:Texas', 'California', 2)).toBe(':NY:California'); + }); + + test('Inserting a tag into an index with no tags should update the tag', () => { + expect(IOUUtils.insertTagIntoTransactionTagsString('::California', 'NY', 1)).toBe(':NY:California'); + }); + + test('Inserting a tag with colon in name into tag string should keep the colon in tag', () => { + expect(IOUUtils.insertTagIntoTransactionTagsString('East:NY:California', 'City \\: \\:', 1)).toBe('East:City \\: \\::California'); + }); + + test('Remove a tag from tagString', () => { + expect(IOUUtils.insertTagIntoTransactionTagsString('East:City \\: \\::California', '', 1)).toBe('East::California'); + }); + }); }); describe('isValidMoneyRequestType', () => { diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 34a0a9af7383..c2beebfa563d 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -297,24 +297,6 @@ describe('OptionsListUtils', () => { }, }; - const REPORTS_WITH_WORKSPACE: OnyxCollection = { - ...REPORTS, - '15': { - lastReadTime: '2021-01-14 11:25:39.295', - lastVisibleActionCreated: '2022-11-22 03:26:02.015', - isPinned: false, - isChatRoom: false, - reportID: '15', - participants: { - 1: {}, - 2: {}, - }, - reportName: 'Test Workspace', - type: CONST.REPORT.TYPE.CHAT, - chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, - }, - }; - const REPORTS_WITH_CHAT_ROOM: OnyxCollection = { ...REPORTS, 15: { @@ -411,7 +393,6 @@ describe('OptionsListUtils', () => { let OPTIONS_WITH_CHRONOS: OptionsListUtils.OptionList; let OPTIONS_WITH_RECEIPTS: OptionsListUtils.OptionList; let OPTIONS_WITH_WORKSPACE_ROOM: OptionsListUtils.OptionList; - let OPTIONS_WITH_WORKSPACE: OptionsListUtils.OptionList; beforeEach(() => { OPTIONS = OptionsListUtils.createOptionList(PERSONAL_DETAILS, REPORTS); @@ -419,7 +400,6 @@ describe('OptionsListUtils', () => { OPTIONS_WITH_CHRONOS = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_CHRONOS, REPORTS_WITH_CHRONOS); OPTIONS_WITH_RECEIPTS = OptionsListUtils.createOptionList(PERSONAL_DETAILS_WITH_RECEIPTS, REPORTS_WITH_RECEIPTS); OPTIONS_WITH_WORKSPACE_ROOM = OptionsListUtils.createOptionList(PERSONAL_DETAILS, REPORTS_WITH_WORKSPACE_ROOMS); - OPTIONS_WITH_WORKSPACE = OptionsListUtils.createOptionList(PERSONAL_DETAILS, REPORTS_WITH_WORKSPACE); }); it('getSearchOptions()', () => { @@ -2638,12 +2618,11 @@ describe('OptionsListUtils', () => { const options = OptionsListUtils.getSearchOptions(OPTIONS, '', [CONST.BETAS.ALL]); const filteredOptions = OptionsListUtils.filterOptions(options, searchText, {sortByReportTypeInSearch: true}); - expect(filteredOptions.recentReports.length).toBe(5); + expect(filteredOptions.recentReports.length).toBe(4); expect(filteredOptions.recentReports[0].text).toBe('Invisible Woman'); expect(filteredOptions.recentReports[1].text).toBe('Spider-Man'); expect(filteredOptions.recentReports[2].text).toBe('Black Widow'); expect(filteredOptions.recentReports[3].text).toBe('Mister Fantastic, Invisible Woman'); - expect(filteredOptions.recentReports[4].text).toBe("SHIELD's workspace (archived)"); }); it('should filter users by email', () => { @@ -2692,7 +2671,7 @@ describe('OptionsListUtils', () => { const filteredOptions = OptionsListUtils.filterOptions(options, searchText); - expect(filteredOptions.recentReports.length).toBe(2); + expect(filteredOptions.recentReports.length).toBe(1); expect(filteredOptions.recentReports[0].login).toBe(searchText); }); @@ -2744,17 +2723,6 @@ describe('OptionsListUtils', () => { expect(filteredOptions.userToInvite?.login).toBe(searchText); }); - it('should return the workspaces that match the participant login', () => { - const searchText = 'reedrichards@expensify.com'; - - const options = OptionsListUtils.getSearchOptions(OPTIONS_WITH_WORKSPACE, ''); - const filteredOptions = OptionsListUtils.filterOptions(options, searchText); - - const recentReportsNames = filteredOptions.recentReports.map((option) => option.text); - - expect(recentReportsNames).toContain('Test Workspace'); - }); - it('should return limited amount of recent reports if the limit is set', () => { const searchText = '';