diff --git a/components/PDFOptionsModal.vue b/components/PDFOptionsModal.vue index 13408dc..9326d9f 100644 --- a/components/PDFOptionsModal.vue +++ b/components/PDFOptionsModal.vue @@ -132,8 +132,21 @@ + + + + + - + ({ includeSummary: true, includeDetails: true, includeProcessingFee: true, - pageFormat: 'A4' + pageFormat: 'A4', + targetCurrency: 'EUR' }); // Form options @@ -260,6 +275,11 @@ const pageFormatOptions = [ { text: 'Legal (8.5 × 14 in)', value: 'Legal' } ]; +const currencyOptions = [ + { text: 'Euro (EUR)', value: 'EUR' }, + { text: 'US Dollar (USD)', value: 'USD' } +]; + // Validation rules const rules = { required: (value: string) => !!value || 'This field is required' diff --git a/pages/dashboard/expenses.vue b/pages/dashboard/expenses.vue index 62b10aa..1299082 100644 --- a/pages/dashboard/expenses.vue +++ b/pages/dashboard/expenses.vue @@ -291,6 +291,36 @@ v-model="showCreateModal" @created="handleExpenseCreated" /> + + + + + + + + Generating PDF... + + Your expense report is being generated with receipt images + + + This may take a moment for large reports + + + + @@ -324,6 +354,7 @@ const showDetailsModal = ref(false); const showCreateModal = ref(false); const selectedExpense = ref(null); const activeTab = ref(''); +const generatingPDF = ref(false); // Filters const filters = ref({ @@ -494,6 +525,9 @@ const exportCSV = async () => { }; const generatePDF = async (options: any) => { + generatingPDF.value = true; + showPDFModal.value = false; // Close the modal immediately + try { console.log('[expenses] Generating PDF with options:', options); @@ -536,11 +570,11 @@ const generatePDF = async (options: any) => { console.log('[expenses] PDF downloaded successfully:', response.data.filename); } - showPDFModal.value = false; - } catch (err: any) { console.error('[expenses] Error generating PDF:', err); error.value = err.message || 'Failed to generate PDF'; + } finally { + generatingPDF.value = false; } }; diff --git a/server/api/expenses/generate-pdf.ts b/server/api/expenses/generate-pdf.ts index d73f774..3c9e93e 100644 --- a/server/api/expenses/generate-pdf.ts +++ b/server/api/expenses/generate-pdf.ts @@ -16,6 +16,7 @@ interface PDFOptions { includeDetails: boolean; pageFormat: 'A4' | 'Letter' | 'Legal'; includeProcessingFee?: boolean; + targetCurrency?: 'USD' | 'EUR'; } interface Expense { @@ -23,7 +24,11 @@ interface Expense { 'Establishment Name': string; Price: string; PriceNumber: number; + Currency?: string; + CurrencySymbol?: string; DisplayPrice: string; + DisplayPriceWithEUR?: string; + PriceEUR?: number; PriceUSD?: number; ConversionRate?: number; Payer: string; @@ -57,12 +62,13 @@ export default defineEventHandler(async (event) => { console.log('[expenses/generate-pdf] PDF generation requested for expenses:', expenseIds); try { - // Fetch expense data + // Fetch expense data with target currency processing + const targetCurrency = options.targetCurrency || 'EUR'; const expenses: Expense[] = []; for (const expenseId of expenseIds) { const expense = await getExpenseById(expenseId); if (expense) { - const processedExpense = await processExpenseWithCurrency(expense); + const processedExpense = await processExpenseWithCurrency(expense, targetCurrency); expenses.push(processedExpense); } } @@ -75,7 +81,7 @@ export default defineEventHandler(async (event) => { } // Calculate totals - const totals = calculateTotals(expenses, options.includeProcessingFee); + const totals = calculateTotals(expenses, options.includeProcessingFee, targetCurrency); console.log('[expenses/generate-pdf] Successfully calculated totals:', totals); console.log('[expenses/generate-pdf] Options received:', options); @@ -105,18 +111,33 @@ export default defineEventHandler(async (event) => { } }); -function calculateTotals(expenses: Expense[], includeProcessingFee: boolean = false) { - const originalTotal = expenses.reduce((sum, exp) => sum + (exp.PriceNumber || 0), 0); +function calculateTotals(expenses: Expense[], includeProcessingFee: boolean = false, targetCurrency: string = 'EUR') { + // Calculate target currency total + const targetTotal = expenses.reduce((sum, exp) => { + if (targetCurrency.toUpperCase() === 'USD') { + return sum + (exp.PriceUSD || exp.PriceNumber || 0); + } else { + return sum + (exp.PriceEUR || exp.PriceNumber || 0); + } + }, 0); + + // Calculate EUR total for compatibility + const eurTotal = expenses.reduce((sum, exp) => sum + (exp.PriceEUR || exp.PriceNumber || 0), 0); + + // Calculate USD total for compatibility const usdTotal = expenses.reduce((sum, exp) => sum + (exp.PriceUSD || exp.PriceNumber || 0), 0); - const processingFee = includeProcessingFee ? originalTotal * 0.05 : 0; - const finalTotal = originalTotal + processingFee; + // Processing fee is calculated on target currency total + const processingFee = includeProcessingFee ? targetTotal * 0.05 : 0; + const finalTotal = targetTotal + processingFee; return { - originalTotal, + targetTotal, + eurTotal, usdTotal, processingFee, finalTotal, + targetCurrency: targetCurrency.toUpperCase(), count: expenses.length }; } @@ -242,34 +263,43 @@ function addSummary(doc: PDFKit.PDFDocument, totals: any, options: PDFOptions) { .font('Helvetica'); const leftX = 80; - const rightX = doc.page.width - 200; + const targetCurrency = totals.targetCurrency || 'EUR'; + const targetSymbol = targetCurrency === 'USD' ? '$' : '€'; doc.text(`Total Expenses:`, leftX, doc.y, { continued: true }) .font('Helvetica-Bold') .text(` ${totals.count}`, { align: 'left' }); doc.font('Helvetica') - .text(`Subtotal:`, leftX, doc.y + 5, { continued: true }) + .text(`Subtotal (${targetCurrency}):`, leftX, doc.y + 5, { continued: true }) .font('Helvetica-Bold') - .text(` €${totals.originalTotal.toFixed(2)}`, { align: 'left' }); + .text(` ${targetSymbol}${totals.targetTotal.toFixed(2)}`, { align: 'left' }); - doc.font('Helvetica') - .text(`USD Equivalent:`, leftX, doc.y + 5, { continued: true }) - .font('Helvetica-Bold') - .text(` $${totals.usdTotal.toFixed(2)}`, { align: 'left' }); + // Show the other currency as reference + if (targetCurrency === 'USD') { + doc.font('Helvetica') + .text(`EUR Equivalent:`, leftX, doc.y + 5, { continued: true }) + .font('Helvetica-Bold') + .text(` €${totals.eurTotal.toFixed(2)}`, { align: 'left' }); + } else { + doc.font('Helvetica') + .text(`USD Equivalent:`, leftX, doc.y + 5, { continued: true }) + .font('Helvetica-Bold') + .text(` $${totals.usdTotal.toFixed(2)}`, { align: 'left' }); + } if (options.includeProcessingFee) { doc.font('Helvetica') .text(`Processing Fee (5%):`, leftX, doc.y + 5, { continued: true }) .font('Helvetica-Bold') - .text(` €${totals.processingFee.toFixed(2)}`, { align: 'left' }); + .text(` ${targetSymbol}${totals.processingFee.toFixed(2)}`, { align: 'left' }); } doc.font('Helvetica') .text(`Final Total:`, leftX, doc.y + 5, { continued: true }) .font('Helvetica-Bold') .fontSize(14) - .text(` €${totals.finalTotal.toFixed(2)}`, { align: 'left' }); + .text(` ${targetSymbol}${totals.finalTotal.toFixed(2)}`, { align: 'left' }); doc.fontSize(12) .font('Helvetica') @@ -338,8 +368,8 @@ async function addExpenseTable(doc: PDFKit.PDFDocument, expenses: Expense[], opt currentY = 60; } - // Group header - const groupTotal = groupExpenses.reduce((sum, exp) => sum + (exp.PriceNumber || 0), 0); + // Group header - show EUR total + const groupEurTotal = groupExpenses.reduce((sum, exp) => sum + (exp.PriceEUR || exp.PriceNumber || 0), 0); doc.fontSize(fontSize + 1) .font('Helvetica-Bold') .fillColor('#000000'); @@ -351,7 +381,7 @@ async function addExpenseTable(doc: PDFKit.PDFDocument, expenses: Expense[], opt .stroke(); doc.fillColor('#000000') - .text(`${groupKey} (${groupExpenses.length} expenses - €${groupTotal.toFixed(2)})`, + .text(`${groupKey} (${groupExpenses.length} expenses - €${groupEurTotal.toFixed(2)})`, 65, currentY + 8, { width: doc.page.width - 130 }); currentY += rowHeight; @@ -392,12 +422,21 @@ async function drawExpenseRows( doc.fillColor('#000000'); - // Draw row data + // Draw row data - show original amount with EUR conversion const date = expense.Time ? formatDate(expense.Time) : 'N/A'; const establishment = expense['Establishment Name'] || 'N/A'; const category = expense.Category || 'N/A'; const payer = expense.Payer || 'N/A'; - const amount = `€${expense.PriceNumber ? expense.PriceNumber.toFixed(2) : '0.00'}`; + + // Display amount with EUR conversion if needed + let amount; + if (expense.Currency && expense.Currency.toUpperCase() !== 'EUR' && expense.PriceEUR) { + const symbol = expense.CurrencySymbol || expense.Currency; + amount = `${symbol}${expense.PriceNumber?.toFixed(2)} (€${expense.PriceEUR.toFixed(2)})`; + } else { + amount = `€${expense.PriceNumber ? expense.PriceNumber.toFixed(2) : '0.00'}`; + } + const payment = expense['Payment Method'] || 'N/A'; const rowData = [date, establishment, category, payer, amount, payment]; @@ -526,10 +565,18 @@ async function addReceiptImages(doc: PDFKit.PDFDocument, expenses: Expense[]) { .text(`Receipt Image ${currentReceiptNumber} of ${totalReceiptImages}`, 70, 80, { align: 'left' }); + // Show amount with EUR conversion + let amountText; + if (expense.Currency && expense.Currency.toUpperCase() !== 'EUR' && expense.PriceEUR) { + const symbol = expense.CurrencySymbol || expense.Currency; + amountText = `${expense['Establishment Name']} - ${symbol}${expense.PriceNumber?.toFixed(2)} (€${expense.PriceEUR.toFixed(2)})`; + } else { + amountText = `${expense['Establishment Name']} - €${expense.PriceNumber?.toFixed(2)}`; + } + doc.fontSize(14) .font('Helvetica-Bold') - .text(`${expense['Establishment Name']} - €${expense.PriceNumber?.toFixed(2)}`, - 70, 105, { align: 'left' }); + .text(amountText, 70, 105, { align: 'left' }); doc.fontSize(12) .font('Helvetica') @@ -597,10 +644,18 @@ async function addReceiptImages(doc: PDFKit.PDFDocument, expenses: Expense[]) { .text(`Receipt Image ${currentReceiptNumber} of ${totalReceiptImages}`, { align: 'center' }); + // Show amount with EUR conversion + let centerAmountText; + if (expense.Currency && expense.Currency.toUpperCase() !== 'EUR' && expense.PriceEUR) { + const symbol = expense.CurrencySymbol || expense.Currency; + centerAmountText = `${expense['Establishment Name']} - ${symbol}${expense.PriceNumber?.toFixed(2)} (€${expense.PriceEUR.toFixed(2)})`; + } else { + centerAmountText = `${expense['Establishment Name']} - €${expense.PriceNumber?.toFixed(2)}`; + } + doc.fontSize(14) .font('Helvetica') - .text(`${expense['Establishment Name']} - €${expense.PriceNumber?.toFixed(2)}`, - { align: 'center' }); + .text(centerAmountText, { align: 'center' }); doc.y += 50; @@ -622,10 +677,18 @@ async function addReceiptImages(doc: PDFKit.PDFDocument, expenses: Expense[]) { .text(`Receipt Image ${currentReceiptNumber} of ${totalReceiptImages}`, { align: 'center' }); + // Show amount with EUR conversion + let errorAmountText; + if (expense.Currency && expense.Currency.toUpperCase() !== 'EUR' && expense.PriceEUR) { + const symbol = expense.CurrencySymbol || expense.Currency; + errorAmountText = `${expense['Establishment Name']} - ${symbol}${expense.PriceNumber?.toFixed(2)} (€${expense.PriceEUR.toFixed(2)})`; + } else { + errorAmountText = `${expense['Establishment Name']} - €${expense.PriceNumber?.toFixed(2)}`; + } + doc.fontSize(14) .font('Helvetica') - .text(`${expense['Establishment Name']} - €${expense.PriceNumber?.toFixed(2)}`, - { align: 'center' }); + .text(errorAmountText, { align: 'center' }); doc.y += 50; @@ -685,18 +748,56 @@ async function fetchReceiptImage(receipt: any): Promise { console.log('[expenses/generate-pdf] Detected S3 URL, fetching directly...'); try { - // Fetch image directly from S3 URL - const response = await fetch(rawPath, { + // Ensure URL is properly encoded + let encodedUrl = rawPath; + try { + // Parse and reconstruct URL to ensure proper encoding + const url = new URL(rawPath); + // Re-encode the pathname to handle special characters + url.pathname = url.pathname.split('/').map(segment => encodeURIComponent(decodeURIComponent(segment))).join('/'); + encodedUrl = url.toString(); + console.log('[expenses/generate-pdf] URL encoded:', encodedUrl); + } catch (urlError) { + console.log('[expenses/generate-pdf] Using original URL (encoding failed):', rawPath); + encodedUrl = rawPath; + } + + // Fetch image directly from S3 URL with proper headers + const response = await fetch(encodedUrl, { method: 'GET', headers: { - 'Accept': 'image/*' + 'Accept': 'image/*', + 'User-Agent': 'PortNimara-Client-Portal/1.0', + 'Cache-Control': 'no-cache' }, // Add timeout to prevent hanging - signal: AbortSignal.timeout(30000) // 30 second timeout + signal: AbortSignal.timeout(45000) // 45 second timeout }); if (!response.ok) { console.error(`[expenses/generate-pdf] Failed to fetch image from S3: ${response.status} ${response.statusText}`); + console.error('[expenses/generate-pdf] Response headers:', Object.fromEntries(response.headers.entries())); + + // Try with the original URL if encoding failed + if (encodedUrl !== rawPath) { + console.log('[expenses/generate-pdf] Retrying with original URL...'); + const originalResponse = await fetch(rawPath, { + method: 'GET', + headers: { + 'Accept': 'image/*', + 'User-Agent': 'PortNimara-Client-Portal/1.0' + }, + signal: AbortSignal.timeout(30000) + }); + + if (originalResponse.ok) { + const arrayBuffer = await originalResponse.arrayBuffer(); + const imageBuffer = Buffer.from(arrayBuffer); + console.log('[expenses/generate-pdf] Successfully fetched with original URL, Size:', imageBuffer.length); + return imageBuffer; + } + } + return null; } @@ -710,26 +811,23 @@ async function fetchReceiptImage(receipt: any): Promise { } catch (fetchError: any) { console.error('[expenses/generate-pdf] Error fetching from S3 URL:', fetchError.message); - // If it's a timeout, try once more with a longer timeout - if (fetchError.name === 'TimeoutError' || fetchError.name === 'AbortError') { - console.log('[expenses/generate-pdf] Retrying with longer timeout...'); + // If it's a timeout or network error, try one more time with simpler approach + if (fetchError.name === 'TimeoutError' || fetchError.name === 'AbortError' || fetchError.code === 'ECONNRESET') { + console.log('[expenses/generate-pdf] Network error, trying simplified approach...'); try { - const retryResponse = await fetch(rawPath, { + const simpleResponse = await fetch(rawPath, { method: 'GET', - headers: { - 'Accept': 'image/*' - }, - signal: AbortSignal.timeout(60000) // 60 second timeout for retry + signal: AbortSignal.timeout(90000) // Extended timeout for final attempt }); - if (retryResponse.ok) { - const arrayBuffer = await retryResponse.arrayBuffer(); + if (simpleResponse.ok) { + const arrayBuffer = await simpleResponse.arrayBuffer(); const imageBuffer = Buffer.from(arrayBuffer); - console.log('[expenses/generate-pdf] Successfully fetched image on retry, Size:', imageBuffer.length); + console.log('[expenses/generate-pdf] Successfully fetched image with simplified approach, Size:', imageBuffer.length); return imageBuffer; } - } catch (retryError) { - console.error('[expenses/generate-pdf] Retry also failed:', retryError); + } catch (finalError) { + console.error('[expenses/generate-pdf] Final attempt also failed:', finalError); } } diff --git a/server/utils/currency.ts b/server/utils/currency.ts index ad5afa0..15fbb0b 100644 --- a/server/utils/currency.ts +++ b/server/utils/currency.ts @@ -303,6 +303,80 @@ export const convertToUSD = async (amount: number, fromCurrency: string): Promis } }; +/** + * Convert amount from one currency to EUR + */ +export const convertToEUR = async (amount: number, fromCurrency: string): Promise<{ + eurAmount: number; + rate: number; + conversionDate: string; +} | null> => { + // If already EUR, no conversion needed + if (fromCurrency.toUpperCase() === 'EUR') { + return { + eurAmount: amount, + rate: 1.0, + conversionDate: new Date().toISOString() + }; + } + + try { + const rateCache = await getExchangeRates(); + + if (!rateCache) { + console.error('[currency] No exchange rates available for conversion'); + return null; + } + + const fromCurrencyUpper = fromCurrency.toUpperCase(); + + // Get USD -> EUR rate + const usdToEurRate = rateCache.rates['EUR']; + + if (!usdToEurRate) { + console.error('[currency] EUR rate not available'); + return null; + } + + // If converting from USD to EUR + if (fromCurrencyUpper === 'USD') { + const eurAmount = amount * usdToEurRate; + console.log(`[currency] Converted ${amount} USD to ${eurAmount.toFixed(2)} EUR (rate: ${usdToEurRate.toFixed(4)})`); + + return { + eurAmount: parseFloat(eurAmount.toFixed(2)), + rate: parseFloat(usdToEurRate.toFixed(4)), + conversionDate: rateCache.lastUpdated + }; + } + + // For other currencies, convert through USD first + const usdToSourceRate = rateCache.rates[fromCurrencyUpper]; + + if (!usdToSourceRate) { + console.error(`[currency] Currency ${fromCurrencyUpper} not supported`); + return null; + } + + // Calculate: Source -> USD -> EUR + // Source -> USD: amount / usdToSourceRate + // USD -> EUR: (amount / usdToSourceRate) * usdToEurRate + const sourceToEurRate = usdToEurRate / usdToSourceRate; + const eurAmount = amount * sourceToEurRate; + + console.log(`[currency] Converted ${amount} ${fromCurrencyUpper} to ${eurAmount.toFixed(2)} EUR (rate: ${sourceToEurRate.toFixed(4)})`); + + return { + eurAmount: parseFloat(eurAmount.toFixed(2)), + rate: parseFloat(sourceToEurRate.toFixed(4)), + conversionDate: rateCache.lastUpdated + }; + } catch (error) { + console.error('[currency] Error during EUR conversion:', error); + return null; + } +}; + /** * Format price with currency symbol */ @@ -403,46 +477,160 @@ export const getCacheStatus = async (): Promise<{ } }; +/** + * Convert amount from any currency to target currency + */ +export const convertToTargetCurrency = async ( + amount: number, + fromCurrency: string, + targetCurrency: string +): Promise<{ + targetAmount: number; + rate: number; + conversionDate: string; +} | null> => { + // If same currency, no conversion needed + if (fromCurrency.toUpperCase() === targetCurrency.toUpperCase()) { + return { + targetAmount: amount, + rate: 1.0, + conversionDate: new Date().toISOString() + }; + } + + // Use existing functions for specific conversions + if (targetCurrency.toUpperCase() === 'USD') { + const result = await convertToUSD(amount, fromCurrency); + if (result) { + return { + targetAmount: result.usdAmount, + rate: result.rate, + conversionDate: result.conversionDate + }; + } + return null; + } + + if (targetCurrency.toUpperCase() === 'EUR') { + const result = await convertToEUR(amount, fromCurrency); + if (result) { + return { + targetAmount: result.eurAmount, + rate: result.rate, + conversionDate: result.conversionDate + }; + } + return null; + } + + // For other currencies, convert through USD + try { + const rateCache = await getExchangeRates(); + + if (!rateCache) { + console.error('[currency] No exchange rates available for conversion'); + return null; + } + + const fromCurrencyUpper = fromCurrency.toUpperCase(); + const targetCurrencyUpper = targetCurrency.toUpperCase(); + + // Get rates + const usdToFromRate = rateCache.rates[fromCurrencyUpper]; + const usdToTargetRate = rateCache.rates[targetCurrencyUpper]; + + if (!usdToFromRate || !usdToTargetRate) { + console.error(`[currency] Currency not supported: ${!usdToFromRate ? fromCurrencyUpper : targetCurrencyUpper}`); + return null; + } + + // Calculate: Source -> USD -> Target + const fromToTargetRate = usdToTargetRate / usdToFromRate; + const targetAmount = amount * fromToTargetRate; + + console.log(`[currency] Converted ${amount} ${fromCurrencyUpper} to ${targetAmount.toFixed(2)} ${targetCurrencyUpper} (rate: ${fromToTargetRate.toFixed(4)})`); + + return { + targetAmount: parseFloat(targetAmount.toFixed(2)), + rate: parseFloat(fromToTargetRate.toFixed(4)), + conversionDate: rateCache.lastUpdated + }; + } catch (error) { + console.error('[currency] Error during currency conversion:', error); + return null; + } +}; + /** * Enhanced expense processing with currency conversion */ -export const processExpenseWithCurrency = async (expense: any): Promise => { +export const processExpenseWithCurrency = async (expense: any, targetCurrency: string = 'EUR'): Promise => { const processedExpense = { ...expense }; // Parse price number const priceNumber = parseFloat(expense.Price?.toString().replace(/[^\d.-]/g, '')) || 0; processedExpense.PriceNumber = priceNumber; - // Get currency symbol + // Get currency code and symbol const currencyCode = expense.currency || 'USD'; + processedExpense.Currency = currencyCode; processedExpense.CurrencySymbol = getCurrencySymbol(currencyCode); - // Convert to USD if not already USD - if (currencyCode.toUpperCase() !== 'USD') { - const conversion = await convertToUSD(priceNumber, currencyCode); + // Convert to target currency if not already in target + const targetCurrencyUpper = targetCurrency.toUpperCase(); + const targetField = `Price${targetCurrencyUpper}`; + + if (currencyCode.toUpperCase() !== targetCurrencyUpper) { + const conversion = await convertToTargetCurrency(priceNumber, currencyCode, targetCurrency); if (conversion) { - processedExpense.PriceUSD = conversion.usdAmount; + processedExpense[targetField] = conversion.targetAmount; processedExpense.ConversionRate = conversion.rate; processedExpense.ConversionDate = conversion.conversionDate; + processedExpense.TargetCurrency = targetCurrencyUpper; } } else { - // If already USD, set USD amount to original amount - processedExpense.PriceUSD = priceNumber; + // If already in target currency, set target amount to original amount + processedExpense[targetField] = priceNumber; processedExpense.ConversionRate = 1.0; processedExpense.ConversionDate = new Date().toISOString(); + processedExpense.TargetCurrency = targetCurrencyUpper; + } + + // Also convert to USD and EUR for compatibility + if (currencyCode.toUpperCase() !== 'USD') { + const usdConversion = await convertToUSD(priceNumber, currencyCode); + if (usdConversion) { + processedExpense.PriceUSD = usdConversion.usdAmount; + } + } else { + processedExpense.PriceUSD = priceNumber; + } + + if (currencyCode.toUpperCase() !== 'EUR') { + const eurConversion = await convertToEUR(priceNumber, currencyCode); + if (eurConversion) { + processedExpense.PriceEUR = eurConversion.eurAmount; + } + } else { + processedExpense.PriceEUR = priceNumber; } // Create display prices - processedExpense.DisplayPrice = createDisplayPrice( - priceNumber, - currencyCode, - processedExpense.PriceUSD - ); + processedExpense.DisplayPrice = formatPriceWithCurrency(priceNumber, currencyCode); - processedExpense.DisplayPriceUSD = formatPriceWithCurrency( - processedExpense.PriceUSD || priceNumber, - 'USD' + // Create display price with target currency conversion + const targetAmount = processedExpense[targetField]; + if (currencyCode.toUpperCase() !== targetCurrencyUpper && targetAmount) { + const targetSymbol = getCurrencySymbol(targetCurrency); + processedExpense.DisplayPriceWithTarget = `${formatPriceWithCurrency(priceNumber, currencyCode)} (${targetSymbol}${targetAmount.toFixed(2)})`; + } else { + processedExpense.DisplayPriceWithTarget = formatPriceWithCurrency(priceNumber, currencyCode); + } + + processedExpense.DisplayPriceTarget = formatPriceWithCurrency( + targetAmount || priceNumber, + targetCurrency ); return processedExpense;
+ Your expense report is being generated with receipt images +
+ This may take a moment for large reports +