diff --git a/pages/api/Schema/reports/fourteenMonth/datahandler.ts b/pages/api/Schema/reports/fourteenMonth/datahandler.ts
index a3cbf53ca..13408bbe9 100644
--- a/pages/api/Schema/reports/fourteenMonth/datahandler.ts
+++ b/pages/api/Schema/reports/fourteenMonth/datahandler.ts
@@ -78,7 +78,7 @@ export const mapFourteenMonthReport = (
     salaryCurrency: data.attributes.salary_currency,
     currencyGroups: Object.entries(data.attributes.currency_groups).map(
       ([currency, currencyGroup]) => ({
-        currency,
+        currency: currency.toUpperCase(),
         totals: {
           year: Number(
             isSalaryType
diff --git a/pages/api/Schema/reports/fourteenMonth/fourteenMonth.graphql b/pages/api/Schema/reports/fourteenMonth/fourteenMonth.graphql
index 8d890578a..82879d4b5 100644
--- a/pages/api/Schema/reports/fourteenMonth/fourteenMonth.graphql
+++ b/pages/api/Schema/reports/fourteenMonth/fourteenMonth.graphql
@@ -41,7 +41,7 @@ type FourteenMonthReportContact {
   total: Float!
   average: Float!
   minimum: Float!
-  months: [FourteenMonthReportContactMonths!]
+  months: [FourteenMonthReportContactMonths!]!
   accountNumbers: [String!]!
   lateBy30Days: Boolean!
   lateBy60Days: Boolean!
diff --git a/src/components/Constants/LoadConstants.graphql b/src/components/Constants/LoadConstants.graphql
index f582e7882..9a0b4a940 100644
--- a/src/components/Constants/LoadConstants.graphql
+++ b/src/components/Constants/LoadConstants.graphql
@@ -29,9 +29,11 @@ query LoadConstants {
     #   value
     # }
     pledgeCurrency {
+      id
       codeSymbolString
       name
       code
+      symbol
     }
     pledgeFrequency {
       id
diff --git a/src/components/Dashboard/DonationHistories/DonationHistories.tsx b/src/components/Dashboard/DonationHistories/DonationHistories.tsx
index 581685c8b..a8e2164dc 100644
--- a/src/components/Dashboard/DonationHistories/DonationHistories.tsx
+++ b/src/components/Dashboard/DonationHistories/DonationHistories.tsx
@@ -320,11 +320,9 @@ const DonationHistories = ({
                             offset={0}
                             angle={-90}
                           >
-                            {
-                              t('Amount ({{ currencyCode }})', {
-                                currencyCode,
-                              }) as string
-                            }
+                            {t('Amount ({{ currencyCode }})', {
+                              currencyCode,
+                            })}
                           </Text>
                         }
                       />
diff --git a/src/components/Reports/FourteenMonthReports/FourteenMonthReport.test.tsx b/src/components/Reports/FourteenMonthReports/FourteenMonthReport.test.tsx
index 6193e13d5..17b9fd9bf 100644
--- a/src/components/Reports/FourteenMonthReports/FourteenMonthReport.test.tsx
+++ b/src/components/Reports/FourteenMonthReports/FourteenMonthReport.test.tsx
@@ -1,7 +1,7 @@
 import React from 'react';
 import { MockedProvider, MockedResponse } from '@apollo/client/testing';
 import { ThemeProvider } from '@mui/material/styles';
-import { render, waitFor } from '@testing-library/react';
+import { render, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
 import { FourteenMonthReportCurrencyType } from 'src/graphql/types.generated';
@@ -21,6 +21,7 @@ const defaultProps = {
   title,
   onNavListToggle,
   onSelectContact,
+  isNavListOpen: false,
 };
 
 const mocks = {
@@ -47,8 +48,7 @@ const mocks = {
                     },
                   ],
                   month: '2020-10-01',
-                  salaryCurrencyTotal: 50,
-                  total: 35,
+                  total: 50,
                 },
                 {
                   donations: [
@@ -60,8 +60,7 @@ const mocks = {
                     },
                   ],
                   month: '2020-11-01',
-                  salaryCurrencyTotal: 50,
-                  total: 35,
+                  total: 50,
                 },
                 {
                   donations: [
@@ -73,8 +72,7 @@ const mocks = {
                     },
                   ],
                   month: '2020-12-01',
-                  salaryCurrencyTotal: 50,
-                  total: 35,
+                  total: 50,
                 },
                 {
                   donations: [
@@ -86,8 +84,7 @@ const mocks = {
                     },
                   ],
                   month: '2021-1-01',
-                  salaryCurrencyTotal: 50,
-                  total: 35,
+                  total: 50,
                 },
               ],
               name: 'test name',
@@ -115,8 +112,7 @@ const mocks = {
                     },
                   ],
                   month: '2020-10-01',
-                  salaryCurrencyTotal: 50,
-                  total: 35,
+                  total: 50,
                 },
                 {
                   donations: [
@@ -128,8 +124,7 @@ const mocks = {
                     },
                   ],
                   month: '2020-11-01',
-                  salaryCurrencyTotal: 50,
-                  total: 35,
+                  total: 50,
                 },
                 {
                   donations: [
@@ -141,8 +136,7 @@ const mocks = {
                     },
                   ],
                   month: '2020-12-01',
-                  salaryCurrencyTotal: 50,
-                  total: 35,
+                  total: 50,
                 },
                 {
                   donations: [
@@ -154,8 +148,7 @@ const mocks = {
                     },
                   ],
                   month: '2021-1-01',
-                  salaryCurrencyTotal: 50,
-                  total: 35,
+                  total: 50,
                 },
               ],
               name: 'test name',
@@ -166,7 +159,7 @@ const mocks = {
               total: 1290,
             },
           ],
-          currency: 'cad',
+          currency: 'CAD',
           totals: {
             months: [
               {
@@ -208,8 +201,7 @@ const mocks = {
                     },
                   ],
                   month: '2020-10-01',
-                  salaryCurrencyTotal: 50,
-                  total: 35,
+                  total: 50,
                 },
                 {
                   donations: [
@@ -221,8 +213,7 @@ const mocks = {
                     },
                   ],
                   month: '2020-11-01',
-                  salaryCurrencyTotal: 50,
-                  total: 35,
+                  total: 50,
                 },
                 {
                   donations: [
@@ -234,8 +225,7 @@ const mocks = {
                     },
                   ],
                   month: '2020-12-01',
-                  salaryCurrencyTotal: 50,
-                  total: 35,
+                  total: 50,
                 },
                 {
                   donations: [
@@ -247,8 +237,7 @@ const mocks = {
                     },
                   ],
                   month: '2021-1-01',
-                  salaryCurrencyTotal: 50,
-                  total: 35,
+                  total: 50,
                 },
               ],
               name: 'test name',
@@ -259,7 +248,7 @@ const mocks = {
               total: 1290,
             },
           ],
-          currency: 'usd',
+          currency: 'USD',
           totals: {
             months: [
               {
@@ -299,7 +288,7 @@ const errorMock: MockedResponse = {
 
 describe('FourteenMonthReport', () => {
   it('salary report loading', async () => {
-    const { queryByTestId, queryByText } = render(
+    const { getByTestId, getByText, queryByTestId } = render(
       <ThemeProvider theme={theme}>
         <GqlMockedProvider>
           <FourteenMonthReport
@@ -314,13 +303,13 @@ describe('FourteenMonthReport', () => {
       </ThemeProvider>,
     );
 
-    expect(queryByText(title)).toBeInTheDocument();
-    expect(queryByTestId('LoadingFourteenMonthReport')).toBeInTheDocument();
-    expect(queryByTestId('Notification')).toBeNull();
+    expect(getByText(title)).toBeInTheDocument();
+    expect(getByTestId('LoadingFourteenMonthReport')).toBeInTheDocument();
+    expect(queryByTestId('Notification')).not.toBeInTheDocument();
   });
 
   it('salary report loaded', async () => {
-    const { getAllByTestId, getByTestId, queryByTestId, getByRole } = render(
+    const { getAllByTestId, queryByTestId, getAllByRole } = render(
       <ThemeProvider theme={theme}>
         <GqlMockedProvider<{ FourteenMonthReport: FourteenMonthReportQuery }>
           mocks={mocks}
@@ -343,13 +332,13 @@ describe('FourteenMonthReport', () => {
       ).not.toBeInTheDocument();
     });
 
-    expect(getByRole('table')).toBeInTheDocument();
-    expect(getAllByTestId('FourteenMonthReportTableRow').length).toBe(3);
-    expect(getByTestId('FourteenMonthReport')).toBeInTheDocument();
+    expect(getAllByRole('table')).toHaveLength(2);
+    expect(getAllByTestId('FourteenMonthReportTableRow')).toHaveLength(3);
+    expect(getAllByTestId('FourteenMonthReport')).toHaveLength(2);
   });
 
   it('partner report loading', async () => {
-    const { queryByTestId, queryByText } = render(
+    const { getByTestId, getByText, queryByTestId } = render(
       <ThemeProvider theme={theme}>
         <GqlMockedProvider>
           <FourteenMonthReport
@@ -364,13 +353,13 @@ describe('FourteenMonthReport', () => {
       </ThemeProvider>,
     );
 
-    expect(queryByText(title)).toBeInTheDocument();
-    expect(queryByTestId('LoadingFourteenMonthReport')).toBeInTheDocument();
-    expect(queryByTestId('Notification')).toBeNull();
+    expect(getByText(title)).toBeInTheDocument();
+    expect(getByTestId('LoadingFourteenMonthReport')).toBeInTheDocument();
+    expect(queryByTestId('Notification')).not.toBeInTheDocument();
   });
 
   it('partner report loaded', async () => {
-    const { getByTestId, queryByTestId, queryByText } = render(
+    const { getAllByTestId, queryByTestId, getByText } = render(
       <ThemeProvider theme={theme}>
         <GqlMockedProvider<{ FourteenMonthReport: FourteenMonthReportQuery }>
           mocks={mocks}
@@ -393,12 +382,12 @@ describe('FourteenMonthReport', () => {
       ).not.toBeInTheDocument();
     });
 
-    expect(queryByText(title)).toBeInTheDocument();
-    expect(getByTestId('FourteenMonthReport')).toBeInTheDocument();
+    expect(getByText(title)).toBeInTheDocument();
+    expect(getAllByTestId('FourteenMonthReport')).toHaveLength(2);
   });
 
   it('salary report error', async () => {
-    const { queryByTestId, getByTestId, queryByText } = render(
+    const { queryByTestId, getByTestId, getByText } = render(
       <ThemeProvider theme={theme}>
         <MockedProvider mocks={[errorMock]}>
           <FourteenMonthReport
@@ -419,12 +408,12 @@ describe('FourteenMonthReport', () => {
       ).not.toBeInTheDocument();
     });
 
-    expect(queryByText(title)).toBeInTheDocument();
+    expect(getByText(title)).toBeInTheDocument();
     expect(getByTestId('Notification')).toBeInTheDocument();
   });
 
   it('partner report error', async () => {
-    const { queryByTestId, getByTestId, queryByText } = render(
+    const { queryByTestId, getByTestId, getByText } = render(
       <ThemeProvider theme={theme}>
         <MockedProvider mocks={[errorMock]}>
           <FourteenMonthReport
@@ -445,12 +434,12 @@ describe('FourteenMonthReport', () => {
       ).not.toBeInTheDocument();
     });
 
-    expect(queryByText(title)).toBeInTheDocument();
+    expect(getByText(title)).toBeInTheDocument();
     expect(getByTestId('Notification')).toBeInTheDocument();
   });
 
   it('nav list closed', async () => {
-    const { getByTestId, queryByTestId, queryByText } = render(
+    const { getAllByTestId, getByText, queryByTestId } = render(
       <ThemeProvider theme={theme}>
         <GqlMockedProvider<{ FourteenMonthReport: FourteenMonthReportQuery }>
           mocks={mocks}
@@ -473,9 +462,9 @@ describe('FourteenMonthReport', () => {
       ).not.toBeInTheDocument();
     });
 
-    expect(queryByText(title)).toBeInTheDocument();
-    expect(getByTestId('FourteenMonthReport')).toBeInTheDocument();
-    expect(queryByTestId('MultiPageMenu')).toBeNull();
+    expect(getByText(title)).toBeInTheDocument();
+    expect(getAllByTestId('FourteenMonthReport')).toHaveLength(2);
+    expect(queryByTestId('MultiPageMenu')).not.toBeInTheDocument();
   });
 
   it('filters report by designation account', async () => {
@@ -500,13 +489,8 @@ describe('FourteenMonthReport', () => {
     );
 
     await waitFor(() =>
-      expect(mutationSpy.mock.calls[1][0]).toMatchObject({
-        operation: {
-          operationName: 'FourteenMonthReport',
-          variables: {
-            designationAccountIds: ['account-1'],
-          },
-        },
+      expect(mutationSpy).toHaveGraphqlOperation('FourteenMonthReport', {
+        designationAccountIds: ['account-1'],
       }),
     );
   });
@@ -532,13 +516,8 @@ describe('FourteenMonthReport', () => {
     );
 
     await waitFor(() =>
-      expect(mutationSpy.mock.calls[1][0]).toMatchObject({
-        operation: {
-          operationName: 'FourteenMonthReport',
-          variables: {
-            designationAccountIds: null,
-          },
-        },
+      expect(mutationSpy).toHaveGraphqlOperation('FourteenMonthReport', {
+        designationAccountIds: null,
       }),
     );
   });
@@ -570,37 +549,53 @@ describe('FourteenMonthReport', () => {
     expect(onSelectContact).toHaveBeenCalledWith('contact-1');
   });
 
-  it('should calulate totals correctly', async () => {
-    const mutationSpy = jest.fn();
-    const { getAllByTestId, queryByTestId } = render(
-      <ThemeProvider theme={theme}>
-        <GqlMockedProvider<FourteenMonthReportQuery>
-          mocks={mocks}
-          onCall={mutationSpy}
-        >
-          <FourteenMonthReport
-            {...defaultProps}
-            isNavListOpen={true}
-            currencyType={FourteenMonthReportCurrencyType.Donor}
-          />
-        </GqlMockedProvider>
-      </ThemeProvider>,
-    );
+  describe('partner report', () => {
+    it('should render one table for each partner currency', async () => {
+      const { findAllByRole } = render(
+        <ThemeProvider theme={theme}>
+          <GqlMockedProvider<FourteenMonthReportQuery> mocks={mocks}>
+            <FourteenMonthReport
+              {...defaultProps}
+              currencyType={FourteenMonthReportCurrencyType.Donor}
+            />
+          </GqlMockedProvider>
+        </ThemeProvider>,
+      );
+
+      const tables = await findAllByRole('table', {
+        name: 'Fourteen month report table',
+      });
+      expect(tables).toHaveLength(2);
 
-    await waitFor(() => {
       expect(
-        queryByTestId('LoadingFourteenMonthReport'),
-      ).not.toBeInTheDocument();
-    });
+        within(tables[0]).getByRole('heading', { name: 'CAD' }),
+      ).toBeInTheDocument();
+      const table1MonthlyTotals = within(tables[0]).getAllByTestId(
+        'monthlyTotals',
+      );
+      // There are 2 contacts in this table who each gave 50
+      expect(table1MonthlyTotals[0]).toHaveTextContent('100');
+      expect(table1MonthlyTotals[1]).toHaveTextContent('100');
+      expect(table1MonthlyTotals[2]).toHaveTextContent('100');
+      expect(table1MonthlyTotals[3]).toHaveTextContent('100');
+      expect(within(tables[0]).getByTestId('overallTotal')).toHaveTextContent(
+        '400',
+      );
 
-    const contactTotal = getAllByTestId('monthlyTotals');
-    // 50 * 3 contacts with different currencies
-    expect(contactTotal[0].innerHTML).toEqual('150');
-    expect(contactTotal[1].innerHTML).toEqual('150');
-    expect(contactTotal[2].innerHTML).toEqual('150');
-    expect(contactTotal[3].innerHTML).toEqual('150');
-
-    // 50 * 12 (All dontions from all currecnies)
-    expect(getAllByTestId('overallTotal')[0].innerHTML).toEqual('600');
+      expect(
+        within(tables[1]).getByRole('heading', { name: 'USD' }),
+      ).toBeInTheDocument();
+      const table2MonthlyTotals = within(tables[1]).getAllByTestId(
+        'monthlyTotals',
+      );
+      // There is 1 contact in this table who gave 50
+      expect(table2MonthlyTotals[0]).toHaveTextContent('50');
+      expect(table2MonthlyTotals[1]).toHaveTextContent('50');
+      expect(table2MonthlyTotals[2]).toHaveTextContent('50');
+      expect(table2MonthlyTotals[3]).toHaveTextContent('50');
+      expect(within(tables[1]).getByTestId('overallTotal')).toHaveTextContent(
+        '200',
+      );
+    });
   });
 });
diff --git a/src/components/Reports/FourteenMonthReports/FourteenMonthReport.tsx b/src/components/Reports/FourteenMonthReports/FourteenMonthReport.tsx
index d9671d83e..5d06877af 100644
--- a/src/components/Reports/FourteenMonthReports/FourteenMonthReport.tsx
+++ b/src/components/Reports/FourteenMonthReports/FourteenMonthReport.tsx
@@ -1,18 +1,25 @@
 import React, { useMemo, useState } from 'react';
 import { Box, CircularProgress, useMediaQuery } from '@mui/material';
 import { Theme } from '@mui/material/styles';
-import { DateTime } from 'luxon';
 import { useTranslation } from 'react-i18next';
-import { useApiConstants } from 'src/components/Constants/UseApiConstants';
 import { Notification } from 'src/components/Notification/Notification';
 import { EmptyReport } from 'src/components/Reports/EmptyReport/EmptyReport';
 import { FourteenMonthReportCurrencyType } from 'src/graphql/types.generated';
-import { useLocale } from 'src/hooks/useLocale';
 import { useFourteenMonthReportQuery } from './GetFourteenMonthReport.generated';
 import { FourteenMonthReportHeader as Header } from './Layout/Header/Header';
-import { FourteenMonthReportTable as Table } from './Layout/Table/Table';
+import {
+  FourteenMonthReportTable as Table,
+  FourteenMonthReportTableProps as TableProps,
+} from './Layout/Table/Table';
+import { calculateTotals, sortContacts } from './Layout/Table/helpers';
+import { useCsvData } from './useCsvData';
 import type { Order } from '../Reports.type';
-import type { Contact, OrderBy } from './Layout/Table/TableHead/TableHead';
+import type { OrderBy } from './Layout/Table/TableHead/TableHead';
+
+export interface CurrencyTable
+  extends Pick<TableProps, 'totals' | 'orderedContacts'> {
+  currency: string;
+}
 
 interface Props {
   accountListId: string;
@@ -23,7 +30,8 @@ interface Props {
   currencyType: FourteenMonthReportCurrencyType;
   onSelectContact: (contactId: string) => void;
 }
-export interface Totals {
+
+export interface MonthTotal {
   total: number;
   month: string;
 }
@@ -39,17 +47,14 @@ export const FourteenMonthReport: React.FC<Props> = ({
 }) => {
   const [isExpanded, setExpanded] = useState<boolean>(false);
   const [order, setOrder] = useState<Order>('asc');
-  const [orderBy, setOrderBy] = useState<OrderBy | number | null>(null);
+  const [orderBy, setOrderBy] = useState<OrderBy | null>(null);
   const { t } = useTranslation();
-  const locale = useLocale();
 
   const isMobile = useMediaQuery((theme: Theme) =>
     theme.breakpoints.down('sm'),
   );
 
-  const apiConstants = useApiConstants();
-
-  const { data, loading, error } = useFourteenMonthReportQuery({
+  const { data, error } = useFourteenMonthReportQuery({
     variables: {
       accountListId,
       designationAccountIds: designationAccounts?.length
@@ -59,34 +64,16 @@ export const FourteenMonthReport: React.FC<Props> = ({
     },
   });
 
-  const contacts = useMemo(() => {
-    return data?.fourteenMonthReport?.currencyGroups?.flatMap(
-      (currencyGroup) => [...currencyGroup?.contacts],
-    );
-  }, [data?.fourteenMonthReport.currencyGroups]);
-
-  const orderedContacts = useMemo(() => {
-    if (contacts && orderBy !== null) {
-      const getSortValue = (contact: Contact) =>
-        (typeof orderBy === 'number'
-          ? contact['months']?.[orderBy]['total'].toString()
-          : contact[orderBy]?.toString()) ?? contact.name;
-
-      return contacts.sort((a, b) => {
-        const compare = getSortValue(a)?.localeCompare(
-          getSortValue(b),
-          undefined,
-          {
-            numeric: true,
-          },
-        );
-
-        return order === 'asc' ? compare : -compare;
-      });
-    } else {
-      return contacts;
-    }
-  }, [contacts, order, orderBy]);
+  // Generate a table for each currency group in the report
+  const currencyTables = useMemo<CurrencyTable[]>(
+    () =>
+      data?.fourteenMonthReport.currencyGroups.map((currencyGroup) => ({
+        currency: currencyGroup.currency,
+        orderedContacts: sortContacts(currencyGroup.contacts, orderBy, order),
+        totals: calculateTotals(currencyGroup.contacts),
+      })) ?? [],
+    [data, orderBy, order],
+  );
 
   const handleExpandToggle = (): void => {
     setExpanded((prevExpanded) => !prevExpanded);
@@ -96,161 +83,14 @@ export const FourteenMonthReport: React.FC<Props> = ({
 
   const handleRequestSort = (
     event: React.MouseEvent<unknown>,
-    property: OrderBy | number,
+    property: OrderBy,
   ) => {
     const isAsc = orderBy === property && order === 'asc';
     setOrder(isAsc ? 'desc' : 'asc');
     setOrderBy(property);
   };
 
-  const formatMonth = (month: string) =>
-    DateTime.fromISO(month).toJSDate().toLocaleDateString(locale, {
-      month: 'numeric',
-      year: '2-digit',
-    });
-
-  const csvData = useMemo(() => {
-    if (!contacts) {
-      return [];
-    }
-
-    const months =
-      data?.fourteenMonthReport.currencyGroups[0]?.totals.months ?? [];
-
-    const csvHeaders = [
-      [t('Currency'), data?.fourteenMonthReport.salaryCurrency],
-      [
-        t('Partner'),
-        t('Status'),
-        t('Commitment Amount'),
-        t('Commitment Currency'),
-        t('Commitment Frequency'),
-        t('Committed Monthly Equivalent'),
-        t('In Hand Monthly Equivalent'),
-        t('Missing In Hand Monthly Equivalent'),
-        t('In Hand Special Gifts'),
-        t('In Hand Date Range'),
-        ...months.map(({ month }) => month),
-        t('Total (last month excluded from total)'),
-      ],
-    ];
-
-    const csvBody = [
-      ...contacts.map((contact) => {
-        const numMonthsforMonthlyEquivalent = Math.max(
-          4,
-          parseInt(contact.pledgeFrequency ?? '4'),
-        );
-
-        const pledgedMonthlyEquivalent =
-          contact.status === 'Partner - Financial' &&
-          contact.pledgeAmount &&
-          contact.pledgeFrequency
-            ? Math.round(
-                contact.pledgeAmount / parseFloat(contact.pledgeFrequency),
-              )
-            : '';
-
-        const inHandMonths = contact.months?.slice(
-          15 - numMonthsforMonthlyEquivalent - 1,
-          15 - 1,
-        );
-
-        const inHandMonthlyEquivalent =
-          contact.status === 'Partner - Financial' &&
-          contact.pledgeFrequency &&
-          inHandMonths
-            ? Math.round(
-                inHandMonths.reduce((sum, month) => sum + month.total, 0) /
-                  numMonthsforMonthlyEquivalent,
-              )
-            : '';
-
-        const inHandDateRange =
-          inHandMonths && inHandMonthlyEquivalent
-            ? `${formatMonth(inHandMonths[0].month)} - ${formatMonth(
-                inHandMonths[inHandMonths.length - 1].month,
-              )}`
-            : '';
-
-        return [
-          contact.name,
-          contact.status ?? '',
-          contact.pledgeAmount ?? '',
-          contact.pledgeCurrency ?? '',
-          apiConstants?.pledgeFrequency?.find(
-            ({ key }) => key === contact.pledgeFrequency,
-          )?.value ?? '',
-          pledgedMonthlyEquivalent,
-          inHandMonthlyEquivalent !== '' && pledgedMonthlyEquivalent !== ''
-            ? Math.min(pledgedMonthlyEquivalent, inHandMonthlyEquivalent)
-            : '',
-          inHandMonthlyEquivalent !== '' && pledgedMonthlyEquivalent !== ''
-            ? -Math.max(0, pledgedMonthlyEquivalent - inHandMonthlyEquivalent)
-            : '',
-          inHandMonthlyEquivalent !== '' && pledgedMonthlyEquivalent !== ''
-            ? Math.max(0, inHandMonthlyEquivalent - pledgedMonthlyEquivalent) *
-              numMonthsforMonthlyEquivalent
-            : Math.round(contact.total),
-          inHandDateRange,
-          ...(contact?.months?.map((month) => Math.round(month.total)) || []),
-          Math.round(contact.total),
-        ];
-      }),
-    ];
-
-    const csvTotals = [
-      t('Totals'),
-      '',
-      '',
-      '',
-      '',
-      csvBody.reduce(
-        (sum, row) => sum + (typeof row[5] === 'number' ? row[5] : 0),
-        0,
-      ),
-      csvBody.reduce(
-        (sum, row) => sum + (typeof row[6] === 'number' ? row[6] : 0),
-        0,
-      ),
-      csvBody.reduce(
-        (sum, row) => sum + (typeof row[7] === 'number' ? row[7] : 0),
-        0,
-      ),
-      csvBody.reduce(
-        (sum, row) => sum + (typeof row[8] === 'number' ? row[8] : 0),
-        0,
-      ),
-      '',
-      ...months.map(({ total }) => Math.round(total)),
-      months
-        .map(({ total }) => Math.round(total))
-        .reduce((sum, monthTotal) => sum + monthTotal, 0),
-    ];
-
-    return [...csvHeaders, ...csvBody, csvTotals];
-  }, [apiConstants, contacts]);
-
-  const totals: Totals[] = useMemo(() => {
-    const totals: Totals[] = [];
-    data?.fourteenMonthReport.currencyGroups.forEach((current) => {
-      current.contacts.forEach((contact) => {
-        if (contact?.months) {
-          contact.months.forEach((month, idx) => {
-            if (!totals[idx]?.total && totals[idx]?.total !== 0) {
-              totals.push({
-                month: month.month,
-                total: month.salaryCurrencyTotal,
-              });
-            } else {
-              totals[idx].total = totals[idx].total + month.salaryCurrencyTotal;
-            }
-          });
-        }
-      });
-    });
-    return totals;
-  }, [data?.fourteenMonthReport]);
+  const csvData = useCsvData(currencyTables);
 
   return (
     <Box>
@@ -265,7 +105,7 @@ export const FourteenMonthReport: React.FC<Props> = ({
         onPrint={handlePrint}
         title={title}
       />
-      {loading ? (
+      {!data && !error ? (
         <Box
           display="flex"
           justifyContent="center"
@@ -276,17 +116,22 @@ export const FourteenMonthReport: React.FC<Props> = ({
         </Box>
       ) : error ? (
         <Notification type="error" message={error.toString()} />
-      ) : contacts && contacts.length > 0 ? (
-        <Table
-          isExpanded={isExpanded}
-          onSelectContact={onSelectContact}
-          onRequestSort={handleRequestSort}
-          order={order}
-          orderBy={orderBy}
-          orderedContacts={orderedContacts}
-          salaryCurrency={data?.fourteenMonthReport.salaryCurrency}
-          totals={totals}
-        />
+      ) : currencyTables.length > 0 ? (
+        <Box display="flex" flexDirection="column" gap={4}>
+          {currencyTables.map(({ currency, orderedContacts, totals }) => (
+            <Table
+              key={currency}
+              isExpanded={isExpanded}
+              onSelectContact={onSelectContact}
+              onRequestSort={handleRequestSort}
+              order={order}
+              orderBy={orderBy}
+              orderedContacts={orderedContacts}
+              salaryCurrency={currency}
+              totals={totals}
+            />
+          ))}
+        </Box>
       ) : (
         <EmptyReport
           title={t(
diff --git a/src/components/Reports/FourteenMonthReports/GetFourteenMonthReport.graphql b/src/components/Reports/FourteenMonthReports/GetFourteenMonthReport.graphql
index 41de4bc38..33c6aed3b 100644
--- a/src/components/Reports/FourteenMonthReports/GetFourteenMonthReport.graphql
+++ b/src/components/Reports/FourteenMonthReports/GetFourteenMonthReport.graphql
@@ -22,30 +22,33 @@ query FourteenMonthReport(
         minimum
       }
       contacts {
-        id
-        name
-        total
-        average
-        minimum
-        months {
-          month
-          total
-          salaryCurrencyTotal
-          donations {
-            amount
-            currency
-            date
-            paymentMethod
-          }
-        }
-        accountNumbers
-        lateBy30Days
-        lateBy60Days
-        pledgeAmount
-        pledgeCurrency
-        pledgeFrequency
-        status
+        ...FourteenMonthReportContact
       }
     }
   }
 }
+
+fragment FourteenMonthReportContact on FourteenMonthReportContact {
+  id
+  name
+  total
+  average
+  minimum
+  months {
+    month
+    total
+    donations {
+      amount
+      currency
+      date
+      paymentMethod
+    }
+  }
+  accountNumbers
+  lateBy30Days
+  lateBy60Days
+  pledgeAmount
+  pledgeCurrency
+  pledgeFrequency
+  status
+}
diff --git a/src/components/Reports/FourteenMonthReports/Layout/Header/Actions/Actions.tsx b/src/components/Reports/FourteenMonthReports/Layout/Header/Actions/Actions.tsx
index c5bdebac3..f8a4f4b51 100644
--- a/src/components/Reports/FourteenMonthReports/Layout/Header/Actions/Actions.tsx
+++ b/src/components/Reports/FourteenMonthReports/Layout/Header/Actions/Actions.tsx
@@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
 import { FourteenMonthReportCurrencyType } from 'src/graphql/types.generated';
 
 interface FourteenMonthReportActionsProps {
-  csvData: ((string | undefined)[] | (string | number)[])[];
+  csvData: (string | number)[][];
   currencyType: FourteenMonthReportCurrencyType;
   isExpanded: boolean;
   isMobile: boolean;
diff --git a/src/components/Reports/FourteenMonthReports/Layout/Header/Header.tsx b/src/components/Reports/FourteenMonthReports/Layout/Header/Header.tsx
index 8f8c67988..638aace5a 100644
--- a/src/components/Reports/FourteenMonthReports/Layout/Header/Header.tsx
+++ b/src/components/Reports/FourteenMonthReports/Layout/Header/Header.tsx
@@ -8,7 +8,7 @@ import theme from 'src/theme';
 import { FourteenMonthReportActions } from './Actions/Actions';
 
 interface FourteenMonthReportHeaderProps {
-  csvData: ((string | undefined)[] | (string | number)[])[];
+  csvData: (string | number)[][];
   currencyType: FourteenMonthReportCurrencyType;
   isExpanded: boolean;
   isMobile: boolean;
diff --git a/src/components/Reports/FourteenMonthReports/Layout/Table/Table.test.tsx b/src/components/Reports/FourteenMonthReports/Layout/Table/Table.test.tsx
index ee655da4e..af0d53b5e 100644
--- a/src/components/Reports/FourteenMonthReports/Layout/Table/Table.test.tsx
+++ b/src/components/Reports/FourteenMonthReports/Layout/Table/Table.test.tsx
@@ -32,8 +32,7 @@ const mocks = {
                     },
                   ],
                   month: '2020-10-01',
-                  salaryCurrencyTotal: 255,
-                  total: 35,
+                  total: 255,
                 },
                 {
                   donations: [
@@ -45,8 +44,7 @@ const mocks = {
                     },
                   ],
                   month: '2020-11-01',
-                  salaryCurrencyTotal: 255,
-                  total: 35,
+                  total: 255,
                 },
                 {
                   donations: [
@@ -58,8 +56,7 @@ const mocks = {
                     },
                   ],
                   month: '2020-12-01',
-                  salaryCurrencyTotal: 255,
-                  total: 35,
+                  total: 255,
                 },
                 {
                   donations: [
@@ -71,8 +68,7 @@ const mocks = {
                     },
                   ],
                   month: '2021-1-01',
-                  salaryCurrencyTotal: 255,
-                  total: 35,
+                  total: 255,
                 },
               ],
               minimum: 255,
@@ -98,8 +94,7 @@ const mocks = {
                     },
                   ],
                   month: '2020-10-01',
-                  salaryCurrencyTotal: 255,
-                  total: 35,
+                  total: 255,
                 },
                 {
                   donations: [
@@ -111,8 +106,7 @@ const mocks = {
                     },
                   ],
                   month: '2020-11-01',
-                  salaryCurrencyTotal: 255,
-                  total: 35,
+                  total: 255,
                 },
                 {
                   donations: [
@@ -124,8 +118,7 @@ const mocks = {
                     },
                   ],
                   month: '2020-12-01',
-                  salaryCurrencyTotal: 255,
-                  total: 35,
+                  total: 255,
                 },
                 {
                   donations: [
@@ -137,8 +130,7 @@ const mocks = {
                     },
                   ],
                   month: '2021-1-01',
-                  salaryCurrencyTotal: 255,
-                  total: 35,
+                  total: 255,
                 },
               ],
               minimum: 255,
@@ -230,10 +222,10 @@ describe('FourteenMonthReportTable', () => {
     });
 
     expect(getByRole('table')).toBeInTheDocument();
-    expect(getAllByTestId('FourteenMonthReportTableRow').length).toBe(2);
+    expect(getAllByTestId('FourteenMonthReportTableRow')).toHaveLength(2);
     expect(queryByTestId('FourteenMonthReport')).toBeInTheDocument();
     const contactTotal = getAllByTestId('totalGivenByContact');
-    expect(contactTotal[0].innerHTML).toEqual('1,020');
+    expect(contactTotal[0]).toHaveTextContent('3,366');
   });
 
   it('should order by name', async () => {
@@ -269,7 +261,7 @@ describe('FourteenMonthReportTable', () => {
     const fourteenMonthReportRow = getAllByTestId(
       'FourteenMonthReportTableRow',
     );
-    expect(fourteenMonthReportRow.length).toBe(2);
+    expect(fourteenMonthReportRow).toHaveLength(2);
     expect(fourteenMonthReportRow[0]).toHaveTextContent('test name');
     expect(fourteenMonthReportRow[1]).toHaveTextContent('name again');
     expect(queryByTestId('FourteenMonthReport')).toBeInTheDocument();
@@ -308,77 +300,10 @@ describe('FourteenMonthReportTable', () => {
     userEvent.click(getByText('name again'));
     expect(onSelectContact).toHaveBeenCalledWith('contact-2');
     await waitFor(() => {
-      expect(getAllByTestId('pledgeAmount')[1].innerHTML).toEqual('16 USD ');
+      expect(getAllByTestId('pledgeAmount')[1]).toHaveTextContent('16 USD');
     });
   });
 
-  it('should return 0 if no months', async () => {
-    const newMocks = {
-      FourteenMonthReport: {
-        fourteenMonthReport: {
-          currencyGroups: [
-            {
-              contacts: [
-                {
-                  accountNumbers: ['11609'],
-                  average: 258,
-                  id: 'contact-1',
-                  lateBy30Days: false,
-                  lateBy60Days: false,
-                  months: null,
-                  minimum: 255,
-                  name: 'name again',
-                  pledgeAmount: null,
-                  status: null,
-                  total: 3366,
-                },
-              ],
-              currency: 'cad',
-              totals: {
-                average: 1831,
-                minimum: 1583,
-                months: null,
-                year: 24613,
-              },
-            },
-          ],
-        },
-      },
-    };
-
-    const { queryByTestId, getAllByTestId } = render(
-      <ThemeProvider theme={theme}>
-        <GqlMockedProvider>
-          <FourteenMonthReportTable
-            isExpanded={true}
-            order="asc"
-            orderBy="name"
-            orderedContacts={
-              newMocks.FourteenMonthReport.fourteenMonthReport.currencyGroups[0]
-                .contacts
-            }
-            salaryCurrency={
-              newMocks.FourteenMonthReport.fourteenMonthReport.currencyGroups[0]
-                .currency
-            }
-            onRequestSort={onRequestSort}
-            onSelectContact={onSelectContact}
-            totals={totals}
-          />
-        </GqlMockedProvider>
-      </ThemeProvider>,
-    );
-
-    await waitFor(() => {
-      expect(
-        queryByTestId('LoadingFourteenMonthReport'),
-      ).not.toBeInTheDocument();
-    });
-
-    const contactTotal = getAllByTestId('totalGivenByContact');
-    expect(contactTotal[0].innerHTML).toEqual('0');
-  });
-
   it('should calculate the correct monthly totals', async () => {
     const { queryByTestId, getByTestId, getAllByTestId } = render(
       <ThemeProvider theme={theme}>
@@ -410,14 +335,13 @@ describe('FourteenMonthReportTable', () => {
     });
 
     const contactTotal = getAllByTestId('monthlyTotals');
-    expect(contactTotal[0].innerHTML).toEqual('1,836');
-    expect(contactTotal[1].innerHTML).toEqual('1,487');
-    expect(contactTotal[2].innerHTML).toEqual('1,836');
-    expect(contactTotal[3].innerHTML).toEqual('1,836');
-
-    expect(getAllByTestId('overallTotal')[0].innerHTML).toEqual('6,996');
+    expect(contactTotal[0]).toHaveTextContent('1,836');
+    expect(contactTotal[1]).toHaveTextContent('1,487');
+    expect(contactTotal[2]).toHaveTextContent('1,836');
+    expect(contactTotal[3]).toHaveTextContent('1,836');
 
     expect(getByTestId('averageTotal')).toHaveTextContent('516');
     expect(getByTestId('minimumTotal')).toHaveTextContent('510');
+    expect(getAllByTestId('overallTotal')[0]).toHaveTextContent('6,996');
   });
 });
diff --git a/src/components/Reports/FourteenMonthReports/Layout/Table/Table.tsx b/src/components/Reports/FourteenMonthReports/Layout/Table/Table.tsx
index 2903fea27..f7bcc6b0b 100644
--- a/src/components/Reports/FourteenMonthReports/Layout/Table/Table.tsx
+++ b/src/components/Reports/FourteenMonthReports/Layout/Table/Table.tsx
@@ -16,18 +16,18 @@ import { useLocale } from 'src/hooks/useLocale';
 import theme from 'src/theme';
 import { numberFormat } from '../../../../../lib/intlFormat';
 import { useApiConstants } from '../../../../Constants/UseApiConstants';
-import { Totals } from '../../FourteenMonthReport';
+import { MonthTotal } from '../../FourteenMonthReport';
 import { StyledTableCell } from './StyledComponents';
 import {
   FourteenMonthReportTableHead as TableHead,
   FourteenMonthReportTableHeadProps as TableHeadProps,
 } from './TableHead/TableHead';
-import type { Contact, Month } from './TableHead/TableHead';
+import type { Contact } from './TableHead/TableHead';
 
-interface FourteenMonthReportTableProps extends TableHeadProps {
+export interface FourteenMonthReportTableProps extends TableHeadProps {
   isExpanded: boolean;
-  orderedContacts: Contact[] | undefined;
-  totals: Totals[];
+  orderedContacts: Contact[];
+  totals: MonthTotal[];
   onSelectContact: (contactId: string) => void;
 }
 
@@ -44,29 +44,18 @@ const NameTypography = styled(Typography, {
   },
 }));
 
-const PrintableContainer = styled(TableContainer)(() => ({
-  // First style set size as landscape
-  height: 'calc(100vh - 160px)',
+const PrintableContainer = styled(TableContainer)({
+  // First style sets size as landscape
   '@media print': {
-    ['@page']: { size: 'landscape' },
-    overflow: 'auto',
-    height: '100%',
+    '@page': { size: 'landscape' },
   },
-}));
-
-const StickyTable = styled(Table)(({}) => ({
-  height: 'calc(100vh - 96px)',
-  '@media print': {
-    overflow: 'auto',
-    height: '100%',
-  },
-}));
+});
 
-const StyledInfoIcon = styled(InfoIcon)(({}) => ({
+const StyledInfoIcon = styled(InfoIcon)({
   '@media print': {
     display: 'none',
   },
-}));
+});
 
 const StyledTotalsRow = styled(TableRow)({
   '.MuiTableCell-root': {
@@ -113,7 +102,7 @@ export const FourteenMonthReportTable: React.FC<
 
   return (
     <PrintableContainer className="fourteen-month-report">
-      <StickyTable
+      <Table
         stickyHeader={true}
         aria-label={t('Fourteen month report table')}
         data-testid="FourteenMonthReport"
@@ -127,82 +116,67 @@ export const FourteenMonthReportTable: React.FC<
           onRequestSort={onRequestSort}
         />
         <TableBody>
-          {orderedContacts?.map((contact) => {
-            const totalDonated = useMemo(() => {
-              if (contact?.months) {
-                return contact.months.reduce((partialSum, month) => {
-                  return partialSum + month.salaryCurrencyTotal;
-                }, 0);
-              } else {
-                return 0;
-              }
-            }, [contact]);
-            return (
-              <TableRow
-                key={contact.id}
-                hover
-                data-testid="FourteenMonthReportTableRow"
-              >
-                <StyledTableCell>
-                  <Box display="flex" flexDirection="column">
-                    <Box display="flex" alignItems="center">
-                      {!isExpanded && <StyledInfoIcon fontSize="small" />}
-                      <NameTypography variant="body1" expanded={isExpanded}>
-                        <Link
-                          onClick={() => onSelectContact(contact.id)}
-                          onMouseEnter={preloadContactsRightPanel}
-                          underline="hover"
-                        >
-                          {contact.name}
-                        </Link>
-                      </NameTypography>
-                    </Box>
-                    {isExpanded && (
-                      <Typography variant="body2" color="textSecondary">
-                        {contact.accountNumbers.join(', ')}
-                      </Typography>
-                    )}
+          {orderedContacts?.map((contact) => (
+            <TableRow
+              key={contact.id}
+              hover
+              data-testid="FourteenMonthReportTableRow"
+            >
+              <StyledTableCell>
+                <Box display="flex" flexDirection="column">
+                  <Box display="flex" alignItems="center">
+                    {!isExpanded && <StyledInfoIcon fontSize="small" />}
+                    <NameTypography variant="body1" expanded={isExpanded}>
+                      <Link
+                        onClick={() => onSelectContact(contact.id)}
+                        onMouseEnter={preloadContactsRightPanel}
+                        underline="hover"
+                      >
+                        {contact.name}
+                      </Link>
+                    </NameTypography>
                   </Box>
-                </StyledTableCell>
-                {isExpanded && (
-                  <>
-                    <StyledTableCell>{contact.status}</StyledTableCell>
-                    <StyledTableCell data-testid="pledgeAmount">
-                      {contact.pledgeAmount &&
-                        `${numberFormat(
-                          Math.round(contact.pledgeAmount),
-                          locale,
-                        )} ${contact.pledgeCurrency} ${
-                          apiConstants?.pledgeFrequency?.find(
-                            ({ key }) => key === contact.pledgeFrequency,
-                          )?.value ?? ''
-                        }`}
-                    </StyledTableCell>
-                    <StyledTableCell>
-                      {numberFormat(Math.round(contact.average), locale)}
-                    </StyledTableCell>
-                    <StyledTableCell>
-                      {numberFormat(Math.round(contact.minimum), locale)}
-                    </StyledTableCell>
-                  </>
-                )}
-                {contact.months?.map((month: Month) => (
-                  <StyledTableCell key={month?.month} align="center">
-                    {month?.salaryCurrencyTotal &&
-                      numberFormat(
-                        Math.round(month?.salaryCurrencyTotal),
+                  {isExpanded && (
+                    <Typography variant="body2" color="textSecondary">
+                      {contact.accountNumbers.join(', ')}
+                    </Typography>
+                  )}
+                </Box>
+              </StyledTableCell>
+              {isExpanded && (
+                <>
+                  <StyledTableCell>{contact.status}</StyledTableCell>
+                  <StyledTableCell data-testid="pledgeAmount">
+                    {contact.pledgeAmount &&
+                      `${numberFormat(
+                        Math.round(contact.pledgeAmount),
                         locale,
-                      )}
+                      )} ${contact.pledgeCurrency} ${
+                        apiConstants?.pledgeFrequency?.find(
+                          ({ key }) => key === contact.pledgeFrequency,
+                        )?.value ?? ''
+                      }`}
+                  </StyledTableCell>
+                  <StyledTableCell>
+                    {numberFormat(Math.round(contact.average), locale)}
                   </StyledTableCell>
-                ))}
-                <StyledTableCell align="right">
-                  <strong data-testid="totalGivenByContact">
-                    {numberFormat(Math.round(totalDonated), locale)}
-                  </strong>
+                  <StyledTableCell>
+                    {numberFormat(Math.round(contact.minimum), locale)}
+                  </StyledTableCell>
+                </>
+              )}
+              {contact.months.map((month) => (
+                <StyledTableCell key={month.month} align="center">
+                  {numberFormat(Math.round(month.total), locale)}
                 </StyledTableCell>
-              </TableRow>
-            );
-          })}
+              ))}
+              <StyledTableCell align="right">
+                <strong data-testid="totalGivenByContact">
+                  {numberFormat(Math.round(contact.total), locale)}
+                </strong>
+              </StyledTableCell>
+            </TableRow>
+          ))}
           <StyledTotalsRow>
             <StyledTableCell>{t('Totals')}</StyledTableCell>
             {isExpanded && (
@@ -236,7 +210,7 @@ export const FourteenMonthReportTable: React.FC<
             </StyledTableCell>
           </StyledTotalsRow>
         </TableBody>
-      </StickyTable>
+      </Table>
     </PrintableContainer>
   );
 };
diff --git a/src/components/Reports/FourteenMonthReports/Layout/Table/TableHead/TableHead.tsx b/src/components/Reports/FourteenMonthReports/Layout/Table/TableHead/TableHead.tsx
index 0f60af9e0..a72c9d0c1 100644
--- a/src/components/Reports/FourteenMonthReports/Layout/Table/TableHead/TableHead.tsx
+++ b/src/components/Reports/FourteenMonthReports/Layout/Table/TableHead/TableHead.tsx
@@ -4,31 +4,27 @@ import { styled } from '@mui/material/styles';
 import { DateTime } from 'luxon';
 import { useTranslation } from 'react-i18next';
 import { useLocale } from 'src/hooks/useLocale';
-import { Totals } from '../../../FourteenMonthReport';
-import { FourteenMonthReportQuery } from '../../../GetFourteenMonthReport.generated';
+import { MonthTotal } from '../../../FourteenMonthReport';
+import {
+  FourteenMonthReportContactFragment,
+  FourteenMonthReportQuery,
+} from '../../../GetFourteenMonthReport.generated';
 import { StyledTableCell } from '../StyledComponents';
 import { TableHeadCell } from './TableHeadCell/TableHeadCell';
-import type { Order, Unarray } from '../../../../Reports.type';
+import type { Order } from '../../../../Reports.type';
 
-export type Contacts =
-  FourteenMonthReportQuery['fourteenMonthReport']['currencyGroups'][0]['contacts'];
-export type Contact = Contacts[0];
-export type Months = Contact['months'];
-export type Month = Unarray<Months>;
-export type OrderBy = keyof Contact | keyof Unarray<Months>;
+export type Contact = FourteenMonthReportContactFragment;
+export type OrderBy = keyof FourteenMonthReportContactFragment | number; // numbers mean sorting by a specific month index
 
 export interface FourteenMonthReportTableHeadProps {
   isExpanded: boolean;
-  totals: Totals[] | undefined;
+  totals: MonthTotal[] | undefined;
   salaryCurrency:
     | FourteenMonthReportQuery['fourteenMonthReport']['salaryCurrency']
     | undefined;
-  onRequestSort: (
-    event: React.MouseEvent<unknown>,
-    property: OrderBy | number,
-  ) => void;
+  onRequestSort: (event: React.MouseEvent<unknown>, property: OrderBy) => void;
   order: Order;
-  orderBy: string | number | null;
+  orderBy: OrderBy | null;
 }
 
 const YearTableCell = styled(TableCell)(({}) => ({
@@ -46,7 +42,7 @@ export const FourteenMonthReportTableHead: FC<
   const locale = useLocale();
 
   const createSortHandler =
-    (property: OrderBy | number) => (event: React.MouseEvent<unknown>) => {
+    (property: OrderBy) => (event: React.MouseEvent<unknown>) => {
       onRequestSort(event, property);
     };
 
diff --git a/src/components/Reports/FourteenMonthReports/Layout/Table/helpers.test.ts b/src/components/Reports/FourteenMonthReports/Layout/Table/helpers.test.ts
new file mode 100644
index 000000000..c513170c3
--- /dev/null
+++ b/src/components/Reports/FourteenMonthReports/Layout/Table/helpers.test.ts
@@ -0,0 +1,100 @@
+import { ErgonoMockShape } from 'graphql-ergonomock';
+import { DeepPartial } from 'ts-essentials';
+import { gqlMock } from '__tests__/util/graphqlMocking';
+import {
+  FourteenMonthReportContactFragment,
+  FourteenMonthReportContactFragmentDoc,
+} from '../../GetFourteenMonthReport.generated';
+import { calculateTotals, extractSortKey, sortContacts } from './helpers';
+
+const mockContact = (
+  mocks: ErgonoMockShape & DeepPartial<FourteenMonthReportContactFragment>,
+) =>
+  gqlMock<FourteenMonthReportContactFragment>(
+    FourteenMonthReportContactFragmentDoc,
+    {
+      mocks,
+    },
+  );
+
+describe('extractSortKey', () => {
+  it('extracts string values from the contact', () => {
+    const contact = mockContact({
+      status: 'Partner',
+    });
+
+    expect(extractSortKey(contact, 'status')).toBe('Partner');
+  });
+
+  it('converts number values to strings', () => {
+    const contact = mockContact({
+      total: 1000,
+    });
+
+    expect(extractSortKey(contact, 'total')).toBe('1000');
+  });
+
+  it('extracts month totals from the contact', () => {
+    const contact = mockContact({
+      months: [{}, { total: 100 }],
+    });
+
+    expect(extractSortKey(contact, 1)).toBe('100');
+  });
+
+  it('defaults to the contact name', () => {
+    const contact = mockContact({
+      name: 'Contact',
+      status: null,
+    });
+
+    expect(extractSortKey(contact, 'status')).toBe('Contact');
+  });
+});
+
+describe('sortContacts', () => {
+  const contacts = [100, 200, 150, 1000].map((total) => mockContact({ total }));
+
+  it('sorts contacts ascending', () => {
+    expect(
+      sortContacts(contacts, 'total', 'asc').map((contact) => contact.total),
+    ).toEqual([100, 150, 200, 1000]);
+  });
+
+  it('sorts contacts descending', () => {
+    expect(
+      sortContacts(contacts, 'total', 'desc').map((contact) => contact.total),
+    ).toEqual([1000, 200, 150, 100]);
+  });
+
+  it('ignores null order by', () => {
+    expect(
+      sortContacts(contacts, null, 'asc').map((contact) => contact.total),
+    ).toEqual([100, 200, 150, 1000]);
+  });
+});
+
+describe('calculateTotals', () => {
+  const makeContact = (month1: number, month2: number, month3: number) =>
+    mockContact({
+      months: [
+        { month: 'Jan', total: month1 },
+        { month: 'Feb', total: month2 },
+        { month: 'Mar', total: month3 },
+      ],
+    });
+
+  it('sums totals for each month across contacts', () => {
+    expect(
+      calculateTotals([
+        makeContact(100, 200, 150),
+        makeContact(70, 90, 40),
+        makeContact(400, 250, 125),
+      ]),
+    ).toEqual([
+      { month: 'Jan', total: 570 },
+      { month: 'Feb', total: 540 },
+      { month: 'Mar', total: 315 },
+    ]);
+  });
+});
diff --git a/src/components/Reports/FourteenMonthReports/Layout/Table/helpers.ts b/src/components/Reports/FourteenMonthReports/Layout/Table/helpers.ts
new file mode 100644
index 000000000..af1afe36a
--- /dev/null
+++ b/src/components/Reports/FourteenMonthReports/Layout/Table/helpers.ts
@@ -0,0 +1,65 @@
+import { Order } from '../../../Reports.type';
+import { MonthTotal } from '../../FourteenMonthReport';
+import { Contact, OrderBy } from './TableHead/TableHead';
+
+// Given a contact and a sorting field, calculate a sort key for the contact that can be
+// compared against other contacts' sort keys
+export const extractSortKey = (
+  contact: Contact,
+  sortField: OrderBy,
+): string => {
+  const sortKey =
+    typeof sortField === 'number'
+      ? contact.months[sortField].total
+      : contact[sortField];
+  return sortKey?.toString() ?? contact.name;
+};
+
+/**
+ * Sort the contacts array by the orderBy field using the direction order.
+ *
+ * @param contacts Contacts to sort
+ * @param orderBy Field to sort by
+ * @param order Sort direction
+ * @returns Sorted contacts
+ */
+export const sortContacts = (
+  contacts: Contact[],
+  orderBy: OrderBy | null,
+  order: Order,
+): Contact[] => {
+  if (!contacts || orderBy === null) {
+    return contacts;
+  }
+
+  return [...contacts].sort((a, b) => {
+    const compare = extractSortKey(a, orderBy)?.localeCompare(
+      extractSortKey(b, orderBy),
+      undefined,
+      {
+        numeric: true,
+      },
+    );
+
+    return order === 'asc' ? compare : -compare;
+  });
+};
+
+// Calculate the total amounts given by the contacts in each month
+// Returns an array of the total for each month in the report
+export const calculateTotals = (contacts: Contact[]): MonthTotal[] => {
+  const totals: MonthTotal[] = [];
+  contacts.forEach((contact) => {
+    contact.months.forEach((month, idx) => {
+      if (!totals[idx]) {
+        totals.push({
+          month: month.month,
+          total: month.total,
+        });
+      } else {
+        totals[idx].total += month.total;
+      }
+    });
+  });
+  return totals;
+};
diff --git a/src/components/Reports/FourteenMonthReports/useCsvData.test.ts b/src/components/Reports/FourteenMonthReports/useCsvData.test.ts
new file mode 100644
index 000000000..2716dd1f5
--- /dev/null
+++ b/src/components/Reports/FourteenMonthReports/useCsvData.test.ts
@@ -0,0 +1,260 @@
+import { renderHook } from '@testing-library/react-hooks';
+import { ErgonoMockShape } from 'graphql-ergonomock';
+import { DeepPartial } from 'ts-essentials';
+import { gqlMock } from '__tests__/util/graphqlMocking';
+import {
+  LoadConstantsDocument,
+  LoadConstantsQuery,
+} from 'src/components/Constants/LoadConstants.generated';
+import { useApiConstants } from 'src/components/Constants/UseApiConstants';
+import { CurrencyTable } from './FourteenMonthReport';
+import {
+  FourteenMonthReportContactFragment,
+  FourteenMonthReportContactFragmentDoc,
+} from './GetFourteenMonthReport.generated';
+import { useCsvData } from './useCsvData';
+
+jest.mock('src/components/Constants/UseApiConstants.tsx');
+
+// Mock useApiConstants to make the data available synchronously instead of having to wait for the GraphQL call
+(useApiConstants as jest.MockedFn<typeof useApiConstants>).mockReturnValue(
+  gqlMock<LoadConstantsQuery>(LoadConstantsDocument, {
+    mocks: {
+      constant: {
+        pledgeCurrency: [
+          {
+            code: 'USD',
+            symbol: '$',
+          },
+          {
+            code: 'CAD',
+            symbol: '$',
+          },
+        ],
+        pledgeFrequency: [
+          {
+            key: '3.0',
+            value: 'Quarterly',
+          },
+          {
+            key: '12.0',
+            value: 'Annual',
+          },
+        ],
+      },
+    },
+  }).constant,
+);
+
+const mockContact = (
+  mocks?: ErgonoMockShape & DeepPartial<FourteenMonthReportContactFragment>,
+) =>
+  gqlMock<FourteenMonthReportContactFragment>(
+    FourteenMonthReportContactFragmentDoc,
+    {
+      mocks,
+    },
+  );
+
+describe('useCsvData', () => {
+  it('generates CSV data', async () => {
+    const tables: CurrencyTable[] = [
+      {
+        currency: 'USD',
+        totals: [
+          { total: 100, month: '2020-12-01' },
+          { total: 200, month: '2020-11-01' },
+          { total: 150, month: '2020-10-01' },
+          { total: 100, month: '2020-09-01' },
+          { total: 200, month: '2020-08-01' },
+          { total: 150, month: '2020-07-01' },
+          { total: 100, month: '2020-06-01' },
+          { total: 200, month: '2020-05-01' },
+          { total: 150, month: '2020-04-01' },
+          { total: 100, month: '2020-03-01' },
+          { total: 200, month: '2020-02-01' },
+          { total: 150, month: '2020-01-01' },
+        ],
+        orderedContacts: [
+          // Partner pledged for $450/quarter but who gave $275/month average over the last quarter
+          mockContact({
+            name: 'Quarterly Partner',
+            status: 'Partner - Financial',
+            pledgeAmount: 450,
+            pledgeCurrency: 'USD',
+            pledgeFrequency: '3.0',
+            months: [
+              { total: 0, month: '2020-12-01' },
+              { total: 0, month: '2020-11-01' },
+              { total: 0, month: '2020-10-01' },
+              { total: 0, month: '2020-09-01' },
+              { total: 0, month: '2020-08-01' },
+              { total: 0, month: '2020-07-01' },
+              { total: 0, month: '2020-06-01' },
+              { total: 0, month: '2020-05-01' },
+              { total: 350, month: '2020-04-01' },
+              { total: 300, month: '2020-03-01' },
+              { total: 250, month: '2020-02-01' },
+              { total: 200, month: '2020-01-01' },
+            ],
+          }),
+
+          // Partner pledged for $1200/year but who gave $75/month average over the last quarter
+          mockContact({
+            name: 'Annual Partner',
+            status: 'Partner - Financial',
+            pledgeAmount: 1200,
+            pledgeCurrency: 'USD',
+            pledgeFrequency: '12.0',
+            months: [
+              { total: 0, month: '2020-12-01' },
+              { total: 100, month: '2020-11-01' },
+              { total: 0, month: '2020-10-01' },
+              { total: 200, month: '2020-09-01' },
+              { total: 0, month: '2020-08-01' },
+              { total: 100, month: '2020-07-01' },
+              { total: 0, month: '2020-06-01' },
+              { total: 200, month: '2020-05-01' },
+              { total: 0, month: '2020-04-01' },
+              { total: 100, month: '2020-03-01' },
+              { total: 0, month: '2020-02-01' },
+              { total: 200, month: '2020-01-01' },
+            ],
+          }),
+        ],
+      },
+      {
+        currency: 'CAD',
+        totals: [
+          { total: 100, month: '2020-03-01' },
+          { total: 200, month: '2020-02-01' },
+          { total: 150, month: '2020-01-01' },
+        ],
+        orderedContacts: [],
+      },
+    ];
+    const { result } = renderHook(() => useCsvData(tables), {});
+
+    expect(result.current).toEqual([
+      ['Currency', 'USD', '$'],
+      [
+        'Partner',
+        'Status',
+        'Commitment Amount',
+        'Commitment Currency',
+        'Commitment Frequency',
+        'Committed Monthly Equivalent',
+        'In Hand Monthly Equivalent',
+        'Missing In Hand Monthly Equivalent',
+        'In Hand Special Gifts',
+        'In Hand Date Range',
+        'Dec 20',
+        'Nov 20',
+        'Oct 20',
+        'Sep 20',
+        'Aug 20',
+        'Jul 20',
+        'Jun 20',
+        'May 20',
+        'Apr 20',
+        'Mar 20',
+        'Feb 20',
+        'Jan 20',
+        'Total (last month excluded from total)',
+      ],
+      [
+        'Quarterly Partner',
+        'Partner - Financial',
+        450,
+        'USD',
+        'Quarterly',
+        150,
+        150,
+        -0,
+        500,
+        'Jan 20 - Apr 20',
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        350,
+        300,
+        250,
+        200,
+        84,
+      ],
+      [
+        'Annual Partner',
+        'Partner - Financial',
+        1200,
+        'USD',
+        'Annual',
+        100,
+        75,
+        -25,
+        0,
+        'Jan 20 - Dec 20',
+        0,
+        100,
+        0,
+        200,
+        0,
+        100,
+        0,
+        200,
+        0,
+        100,
+        0,
+        200,
+        84,
+      ],
+      [
+        'Totals',
+        '',
+        '',
+        '',
+        '',
+        250,
+        225,
+        -25,
+        500,
+        '',
+        100,
+        200,
+        150,
+        100,
+        200,
+        150,
+        100,
+        200,
+        150,
+        100,
+        200,
+        150,
+        1800,
+      ],
+      ['Currency', 'CAD', '$'],
+      [
+        'Partner',
+        'Status',
+        'Commitment Amount',
+        'Commitment Currency',
+        'Commitment Frequency',
+        'Committed Monthly Equivalent',
+        'In Hand Monthly Equivalent',
+        'Missing In Hand Monthly Equivalent',
+        'In Hand Special Gifts',
+        'In Hand Date Range',
+        'Mar 20',
+        'Feb 20',
+        'Jan 20',
+        'Total (last month excluded from total)',
+      ],
+      ['Totals', '', '', '', '', 0, 0, 0, 0, '', 100, 200, 150, 450],
+    ]);
+  });
+});
diff --git a/src/components/Reports/FourteenMonthReports/useCsvData.ts b/src/components/Reports/FourteenMonthReports/useCsvData.ts
new file mode 100644
index 000000000..235648796
--- /dev/null
+++ b/src/components/Reports/FourteenMonthReports/useCsvData.ts
@@ -0,0 +1,149 @@
+import { useMemo } from 'react';
+import { DateTime } from 'luxon';
+import { useTranslation } from 'react-i18next';
+import { useApiConstants } from 'src/components/Constants/UseApiConstants';
+import { useLocale } from 'src/hooks/useLocale';
+import type { CurrencyTable } from './FourteenMonthReport';
+
+export type CsvData = (string | number)[][];
+
+const formatMonth = (month: string, locale: string): string =>
+  DateTime.fromISO(month).toJSDate().toLocaleDateString(locale, {
+    month: 'short',
+    year: '2-digit',
+  });
+
+/*
+ * Return the data for a CSV export of the provided currency tables.
+ */
+export const useCsvData = (currencyTables: CurrencyTable[]): CsvData => {
+  const { t } = useTranslation();
+  const locale = useLocale();
+  const apiConstants = useApiConstants();
+
+  const csvData = useMemo(
+    () =>
+      currencyTables.flatMap(({ currency, orderedContacts, totals }) => {
+        // Each table starts with two rows of headers
+        const csvHeaders = [
+          [
+            t('Currency'),
+            currency,
+            apiConstants?.pledgeCurrency?.find(({ code }) => code === currency)
+              ?.symbol ?? '',
+          ],
+          [
+            t('Partner'),
+            t('Status'),
+            t('Commitment Amount'),
+            t('Commitment Currency'),
+            t('Commitment Frequency'),
+            t('Committed Monthly Equivalent'),
+            t('In Hand Monthly Equivalent'),
+            t('Missing In Hand Monthly Equivalent'),
+            t('In Hand Special Gifts'),
+            t('In Hand Date Range'),
+            ...totals.map(({ month }) => formatMonth(month, locale)),
+            t('Total (last month excluded from total)'),
+          ],
+        ];
+
+        // Then one row for each contact
+        const csvBody = orderedContacts.map((contact) => {
+          const numMonthsForMonthlyEquivalent = Math.max(
+            4,
+            parseInt(contact.pledgeFrequency ?? '4'),
+          );
+
+          const pledgedMonthlyEquivalent =
+            contact.status === 'Partner - Financial' &&
+            contact.pledgeAmount &&
+            contact.pledgeFrequency
+              ? Math.round(
+                  contact.pledgeAmount / parseFloat(contact.pledgeFrequency),
+                )
+              : '';
+
+          const inHandMonths = contact.months.slice(
+            -numMonthsForMonthlyEquivalent,
+          );
+
+          const inHandMonthlyEquivalent =
+            contact.status === 'Partner - Financial' && contact.pledgeFrequency
+              ? Math.round(
+                  inHandMonths.reduce((sum, month) => sum + month.total, 0) /
+                    numMonthsForMonthlyEquivalent,
+                )
+              : '';
+
+          const inHandDateRange = inHandMonthlyEquivalent
+            ? `${formatMonth(
+                inHandMonths[inHandMonths.length - 1].month,
+                locale,
+              )} - ${formatMonth(inHandMonths[0].month, locale)}`
+            : '';
+
+          return [
+            contact.name,
+            contact.status ?? '',
+            contact.pledgeAmount ? Math.round(contact.pledgeAmount) : 0,
+            contact.pledgeCurrency ?? '',
+            apiConstants?.pledgeFrequency?.find(
+              ({ key }) => key === contact.pledgeFrequency,
+            )?.value ?? '',
+            pledgedMonthlyEquivalent,
+            inHandMonthlyEquivalent !== '' && pledgedMonthlyEquivalent !== ''
+              ? Math.min(pledgedMonthlyEquivalent, inHandMonthlyEquivalent)
+              : '',
+            inHandMonthlyEquivalent !== '' && pledgedMonthlyEquivalent !== ''
+              ? -Math.max(0, pledgedMonthlyEquivalent - inHandMonthlyEquivalent)
+              : '',
+            inHandMonthlyEquivalent !== '' && pledgedMonthlyEquivalent !== ''
+              ? Math.max(
+                  0,
+                  inHandMonthlyEquivalent - pledgedMonthlyEquivalent,
+                ) * numMonthsForMonthlyEquivalent
+              : Math.round(contact.total),
+            inHandDateRange,
+            ...contact.months.map((month) => Math.round(month.total)),
+            Math.round(contact.total),
+          ];
+        });
+
+        const roundedTotals = totals.map(({ total }) => Math.round(total));
+
+        // Then one row of totals
+        const csvTotals = [
+          t('Totals'),
+          '',
+          '',
+          '',
+          '',
+          csvBody.reduce(
+            (sum, row) => sum + (typeof row[5] === 'number' ? row[5] : 0),
+            0,
+          ),
+          csvBody.reduce(
+            (sum, row) => sum + (typeof row[6] === 'number' ? row[6] : 0),
+            0,
+          ),
+          csvBody.reduce(
+            (sum, row) => sum + (typeof row[7] === 'number' ? row[7] : 0),
+            0,
+          ),
+          csvBody.reduce(
+            (sum, row) => sum + (typeof row[8] === 'number' ? row[8] : 0),
+            0,
+          ),
+          '',
+          ...roundedTotals,
+          roundedTotals.reduce((sum, monthTotal) => sum + monthTotal, 0),
+        ];
+
+        return [...csvHeaders, ...csvBody, csvTotals];
+      }),
+    [currencyTables, apiConstants],
+  );
+
+  return csvData;
+};
diff --git a/src/lib/apollo/cache.ts b/src/lib/apollo/cache.ts
index 2e962a634..5011dd80c 100644
--- a/src/lib/apollo/cache.ts
+++ b/src/lib/apollo/cache.ts
@@ -64,6 +64,9 @@ export const createCache = () =>
           },
         },
       },
+      // Disable cache normalization for 14 month report contacts because a contact in one currency group should not be
+      // merged a contact with the same id in a different currency group
+      FourteenMonthReportContact: { keyFields: false },
       // Disable cache normalization for tags because a tag like { id: 'abc', count: 3 } in one period should not be
       // merged with a tag like { id: 'def', count 2 } in another period
       Tag: { keyFields: false },