Skip to content

Commit

Permalink
Expense: add support for item currency (#975)
Browse files Browse the repository at this point in the history
  • Loading branch information
Betree authored Dec 21, 2023
1 parent 346753a commit ac03ad0
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 11 deletions.
3 changes: 2 additions & 1 deletion components/ExpenseInvoice.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import StyledLink from '@opencollective/frontend-components/components/StyledLin
import { H2, P, Span } from '@opencollective/frontend-components/components/Text';
import Container from '@opencollective/frontend-components/components/Container';
import StyledHr from '@opencollective/frontend-components/components/StyledHr';
import { sumItemsInExpenseCurrency } from '../lib/expenses';

const getPageHeight = (pageFormat) => {
const dimensions = PageFormat[pageFormat];
Expand Down Expand Up @@ -60,7 +61,7 @@ const ExpenseInvoice = ({ expense, pageFormat }) => {
const { account, payee, payeeLocation } = expense;
const billToAccount = getBillTo(expense);
const chunkedItems = chunkItems(expense, billToAccount);
const grossAmount = sumBy(expense.items, 'amount');
const grossAmount = sumItemsInExpenseCurrency(expense.items);
return (
<div>
{chunkedItems.map((itemsChunk, pageNumber) => (
Expand Down
34 changes: 29 additions & 5 deletions components/ExpenseItemsTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Tr, Td } from './StyledTable';
import { FormattedMessage, FormattedDate } from 'react-intl';
import { formatCurrency } from '../lib/utils';
import { formatAmount, formatCurrency } from '../lib/utils';
import { round, sumBy, uniq } from 'lodash';
import { Span } from '@opencollective/frontend-components/components/Text';
import { getItemAmounts } from '../lib/expenses';

const ExpenseItemsTable = ({ items, expense }) => {
const allTaxTypes = uniq(expense.taxes.map((tax) => tax.type));
Expand Down Expand Up @@ -33,6 +34,7 @@ const ExpenseItemsTable = ({ items, expense }) => {
</thead>
<tbody>
{items.map((item) => {
const amounts = getItemAmounts(item);
return (
<tr key={item.id}>
<Td fontSize="11px" width={50}>
Expand All @@ -45,9 +47,31 @@ const ExpenseItemsTable = ({ items, expense }) => {
</Span>
)}
</Td>
<Td textAlign="right">{formatCurrency(item.amount, expense.currency)}</Td>
<Td textAlign="right">{formatCurrency(item.amount * taxRate, expense.currency)}</Td>
<Td textAlign="right">{formatCurrency(item.amount * (1 + taxRate), expense.currency)}</Td>
<Td textAlign="right">
{!amounts.inItemCurrency.exchangeRate ? (
formatAmount(amounts.inItemCurrency, { showCurrencySymbol: true })
) : (
<div>
{formatAmount(amounts.inExpenseCurrency, { showCurrencySymbol: true })}
<small>
{' ('}
{formatAmount(amounts.inItemCurrency, { showCurrencySymbol: true })}
{' * '}
{amounts.inItemCurrency.exchangeRate.value}
{')'}
</small>
</div>
)}
</Td>
<Td textAlign="right">
{formatCurrency(amounts.inExpenseCurrency.valueInCents * taxRate, amounts.inExpenseCurrency.currency)}
</Td>
<Td textAlign="right">
{formatCurrency(
amounts.inExpenseCurrency.valueInCents * (1 + taxRate),
amounts.inExpenseCurrency.currency,
)}
</Td>
</tr>
);
})}
Expand All @@ -63,7 +87,7 @@ ExpenseItemsTable.propTypes = {
}),
items: PropTypes.arrayOf(
PropTypes.shape({
amount: PropTypes.number,
amountV2: PropTypes.object,
description: PropTypes.string,
incurredAt: PropTypes.string,
}),
Expand Down
18 changes: 18 additions & 0 deletions lib/constants/currency.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const ZERO_DECIMAL_CURRENCIES = [
'BIF',
'CLP',
'DJF',
'GNF',
'JPY',
'KMF',
'KRW',
'MGA',
'PYG',
'RWF',
'UGX',
'VND',
'VUV',
'XAF',
'XOF',
'XPF',
];
23 changes: 23 additions & 0 deletions lib/expenses.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { round, sumBy } from 'lodash';
import { getCurrencyPrecision } from './utils';

export const getItemAmounts = (item) => {
if (!item.amountV2.exchangeRate) {
return { inItemCurrency: item.amountV2, inExpenseCurrency: item.amountV2 };
} else {
return {
inItemCurrency: item.amountV2,
inExpenseCurrency: {
currency: item.amountV2.exchangeRate.toCurrency,
valueInCents: round(
item.amountV2.valueInCents * item.amountV2.exchangeRate.value,
getCurrencyPrecision(item.amountV2.exchangeRate.toCurrency),
),
},
};
}
};

export const sumItemsInExpenseCurrency = (items) => {
return sumBy(items, (item) => getItemAmounts(item).inExpenseCurrency.valueInCents);
};
10 changes: 9 additions & 1 deletion lib/graphql/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,18 @@ export async function fetchExpenseInvoiceData(expenseId, accessToken, apiKey) {
}
items {
id
amount
description
incurredAt
url
amountV2 {
valueInCents
currency
exchangeRate {
fromCurrency
toCurrency
value
}
}
}
}
}
Expand Down
19 changes: 15 additions & 4 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { get, pickBy, isEmpty } from 'lodash';
import { get, pickBy, isEmpty, isNil } from 'lodash';
import { ZERO_DECIMAL_CURRENCIES } from './constants/currency';

export function getCurrencyPrecision(currency) {
return ZERO_DECIMAL_CURRENCIES.includes(currency) ? 0 : 2;
}

export function formatCurrency(amount, currency = 'USD', options = {}) {
amount = amount / 100;

let minimumFractionDigits = 2;
let maximumFractionDigits = 2;
const defaultPrecision = getCurrencyPrecision(currency);
let minimumFractionDigits = defaultPrecision;
let maximumFractionDigits = defaultPrecision;

if (Object.prototype.hasOwnProperty.call(options, 'minimumFractionDigits')) {
minimumFractionDigits = options.minimumFractionDigits;
Expand All @@ -21,13 +27,18 @@ export function formatCurrency(amount, currency = 'USD', options = {}) {
currencyDisplay: 'symbol',
});

if (options?.showCurrencySymbol) {
if (options?.showCurrencySymbol && !/^[A-Z]{2,3}\$/.test(result)) {
return `${currency} ${result}`;
} else {
return result;
}
}

export function formatAmount(amount, options = {}) {
const valueInCents = amount?.valueInCents || amount?.value * 100;
return isNil(valueInCents) || isNaN(valueInCents) ? '--,--' : formatCurrency(valueInCents, amount.currency, options);
}

function getLocaleFromCurrency(currency) {
let locale;
switch (currency) {
Expand Down

0 comments on commit ac03ad0

Please sign in to comment.