Skip to content

Commit

Permalink
Merge pull request #3744 from CAFECA-IO/feature/refactor-export-ledge…
Browse files Browse the repository at this point in the history
…r-api

Feature/refactor export ledger api
  • Loading branch information
Luphia authored Dec 25, 2024
2 parents ae9a341 + 7404cae commit f495a24
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 120 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "iSunFA",
"version": "0.8.5+230",
"version": "0.8.5+231",
"private": false,
"scripts": {
"dev": "next dev",
Expand Down
3 changes: 1 addition & 2 deletions src/constants/export_ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ export enum ExportLedgerFileType {
}

export const LedgerFieldsMap: Record<ILedgerHeader, string> = {
accountId: '會計科目編號',
no: '會計科目代號',
no: '科目編號',
accountingTitle: '會計科目',
voucherNumber: '傳票編號',
voucherDate: '傳票日期',
Expand Down
14 changes: 14 additions & 0 deletions src/interfaces/line_item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,17 @@ export type IGetLineItemByAccount = PrismaLineItem & {
})[];
};
};

export interface ILineItemSimpleAccountVoucher extends PrismaLineItem {
account: {
code: string;
name: string;
parentId: number;
};
voucher: {
id: number;
type: string;
no: string;
date: number;
};
}
152 changes: 151 additions & 1 deletion src/lib/utils/ledger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { LabelType } from '@/constants/ledger';
import { IAccountBookLedgerJSON } from '@/interfaces/account_book_node';
import { ILedgerItem } from '@/interfaces/ledger';
import { getAllLineItemsInPrisma } from '@/lib/utils/repo/line_item.repo';
import { ILedgerItem, ILedgerTotal } from '@/interfaces/ledger';
import { getLedgerJSON } from '@/lib/utils/repo/account_book.repo';
import { EventType, EVENT_TYPE_TO_VOUCHER_TYPE_MAP } from '@/constants/account';
import { ILineItemSimpleAccountVoucher } from '@/interfaces/line_item';

export const getLedgerFromAccountBook = async (
companyId: number,
Expand Down Expand Up @@ -83,3 +86,150 @@ export const convertLedgerJsonToCsvData = (
});
return csvData;
};

export const convertLedgerItemToCsvData = (
ledgerItems: ILedgerItem[],
voucherMap: Map<
number,
{
id: number;
date: string;
no: string;
type: string;
}
>
) => {
const csvData = ledgerItems.map((item) => {
return {
no: item.no,
accountingTitle: item.accountingTitle,
voucherNumber: voucherMap.get(item.voucherId)?.no,
voucherDate: voucherMap.get(item.voucherId)?.date,
particulars: item.particulars,
debitAmount: item.debitAmount,
creditAmount: item.creditAmount,
balance: item.balance,
};
});
return csvData;
};

/** Info: (20241224 - Shirley)
* 獲取分錄明細
*/
export const fetchLineItems = async (
companyId: number,
startDate: number,
endDate: number
): Promise<ILineItemSimpleAccountVoucher[]> => {
const rs = await getAllLineItemsInPrisma(companyId, startDate, endDate, false);
return rs;
};

/** Info: (20241224 - Shirley)
* 根據科目範圍過濾分錄
*/
export const filterByAccountRange = (
lineItems: ILineItemSimpleAccountVoucher[],
startAccountNo?: string,
endAccountNo?: string
): ILineItemSimpleAccountVoucher[] => {
if (!startAccountNo && !endAccountNo) return lineItems;

return lineItems.filter((item) => {
const no = item.account.code;
if (startAccountNo && endAccountNo) {
return no >= startAccountNo && no <= endAccountNo;
} else if (startAccountNo) {
return no >= startAccountNo;
} else if (endAccountNo) {
return no <= endAccountNo;
}
return true;
});
};

/** Info: (20241224 - Shirley)
* 根據標籤類型過濾分錄
*/
export const filterByLabelType = (
lineItems: ILineItemSimpleAccountVoucher[],
labelType: LabelType
): ILineItemSimpleAccountVoucher[] => {
if (labelType === LabelType.ALL) return lineItems;

return lineItems.filter((item) => {
const hasDash = item.account.code.includes('-');
if (labelType === LabelType.GENERAL) {
return !hasDash;
} else if (labelType === LabelType.DETAILED) {
return hasDash;
}
return true;
});
};

/** Info: (20241224 - Shirley)
* 排序並計算餘額變化
*/
export const sortAndCalculateBalances = (
lineItems: ILineItemSimpleAccountVoucher[]
): ILedgerItem[] => {
const accountBalances: { [key: string]: number } = {};

return lineItems
.sort((a, b) => {
const codeCompare = a.account.code.localeCompare(b.account.code);
if (codeCompare === 0) {
return a.voucher.date - b.voucher.date;
}
return codeCompare;
})
.map((item) => {
const accountKey = item.account.code;
if (!accountBalances[accountKey]) {
accountBalances[accountKey] = 0;
}
const debit = item.debit ? item.amount : 0;
const credit = !item.debit ? item.amount : 0;
const balanceChange = item.debit ? item.amount : -item.amount;
accountBalances[accountKey] += balanceChange;

return {
id: item.id,
accountId: item.accountId,
voucherId: item.voucherId,
voucherDate: item.voucher.date,
no: item.account.code,
accountingTitle: item.account.name,
voucherNumber: item.voucher.no,
voucherType:
EVENT_TYPE_TO_VOUCHER_TYPE_MAP[item.voucher.type as EventType] || item.voucher.type,
particulars: item.description,
debitAmount: debit,
creditAmount: credit,
balance: accountBalances[accountKey],
createdAt: item.createdAt,
updatedAt: item.updatedAt,
};
});
};

/** Info: (20241224 - Shirley)
* 計算總額
*/
export const calculateTotals = (processedLineItems: ILedgerItem[]): ILedgerTotal => {
return processedLineItems.reduce(
(acc: ILedgerTotal, item: ILedgerItem) => {
acc.totalDebitAmount += item.debitAmount;
acc.totalCreditAmount += item.creditAmount;
return acc;
},
{
totalDebitAmount: 0,
totalCreditAmount: 0,
createdAt: 0,
updatedAt: 0,
}
);
};
3 changes: 2 additions & 1 deletion src/lib/utils/repo/line_item.repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import prisma from '@/client';
import { AccountType } from '@/constants/account';
import { Prisma } from '@prisma/client';
import { setTimestampToDayEnd, setTimestampToDayStart } from '@/lib/utils/common';
import { ILineItemSimpleAccountVoucher } from '@/interfaces/line_item';

export async function getLineItemsInPrisma(
companyId: number,
Expand Down Expand Up @@ -52,7 +53,7 @@ export async function getAllLineItemsInPrisma(
startDate: number,
endDate: number,
isDeleted?: boolean
) {
): Promise<ILineItemSimpleAccountVoucher[]> {
const startDateInSecond = setTimestampToDayStart(startDate);
const endDateInSecond = setTimestampToDayEnd(endDate);
const deletedAt = isDeleted ? { not: null } : { equals: null };
Expand Down
1 change: 0 additions & 1 deletion src/lib/utils/zod_schema/export_ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const exportLedgerFiltersSchema = z.object({
});

export const exportLedgerFieldsSchema = z.enum([
'accountId',
'no',
'accountingTitle',
'voucherNumber',
Expand Down
52 changes: 31 additions & 21 deletions src/pages/api/v2/company/[companyId]/ledger/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,21 @@ import { APIName } from '@/constants/api_connection';
import { loggerError } from '@/lib/utils/logger_back';
import { formatApiResponse, formatTimestampByTZ } from '@/lib/utils/common';
import {
convertLedgerJsonToCsvData,
filterLedgerJSONByLabelType,
getLedgerFromAccountBook,
convertLedgerItemToCsvData,
fetchLineItems,
filterByAccountRange,
filterByLabelType,
sortAndCalculateBalances,
} from '@/lib/utils/ledger';
import { DEFAULT_TIMEZONE } from '@/constants/common';
import { ledgerAvailableFields, LedgerFieldsMap } from '@/constants/export_ledger';
import { findVouchersByVoucherIds } from '@/lib/utils/repo/voucher.repo';
import { LabelType } from '@/constants/ledger';

const handlePostRequest = async (req: NextApiRequest, res: NextApiResponse) => {
const { fileType, filters, options } = req.body;
const { companyId } = req.query;
const { startDate, endDate, labelType } = filters;
const { startDate, endDate, labelType, startAccountNo, endAccountNo } = filters;

if (!companyId) {
throw new Error(STATUS_MESSAGE.INVALID_COMPANY_ID);
Expand All @@ -37,29 +40,36 @@ const handlePostRequest = async (req: NextApiRequest, res: NextApiResponse) => {
throw new Error(STATUS_MESSAGE.INVALID_FILE_TYPE);
}

const rawLedgerJSON = await getLedgerFromAccountBook(+companyId, +startDate, +endDate);
const ledgerJSON = filterLedgerJSONByLabelType(rawLedgerJSON, labelType);
try {
let lineItems = await fetchLineItems(+companyId, +startDate, +endDate);
lineItems = filterByAccountRange(lineItems, startAccountNo, endAccountNo);
lineItems = filterByLabelType(lineItems, labelType as LabelType);
const processedLineItems = sortAndCalculateBalances(lineItems);

const voucherIds = ledgerJSON.map((ledger) => ledger.voucherId);
const vouchers = await findVouchersByVoucherIds(voucherIds);
const vouchersWithTz = vouchers.map((voucher) => ({
...voucher,
date: formatTimestampByTZ(voucher.date, options?.timezone || DEFAULT_TIMEZONE, 'YYYY-MM-DD'),
}));
const vouchersMap = new Map(vouchersWithTz.map((voucher) => [voucher.id, voucher]));
const voucherIds = processedLineItems.map((ledger) => ledger.voucherId);
const vouchers = await findVouchersByVoucherIds(voucherIds);
const vouchersWithTz = vouchers.map((voucher) => ({
...voucher,
date: formatTimestampByTZ(voucher.date, options?.timezone || DEFAULT_TIMEZONE, 'YYYY-MM-DD'),
}));
const vouchersMap = new Map(vouchersWithTz.map((voucher) => [voucher.id, voucher]));

const ledgerCsvData = convertLedgerJsonToCsvData(ledgerJSON, vouchersMap);
const ledgerCsvData = convertLedgerItemToCsvData(processedLineItems, vouchersMap);

const data = ledgerCsvData;
const data = ledgerCsvData;

// Info: (20241212 - Shirley) 處理欄位選擇
const fields = options?.fields || ledgerAvailableFields;
// Info: (20241212 - Shirley) 處理欄位選擇
const fields = options?.fields || ledgerAvailableFields;

const csv = convertToCSV(fields, data, LedgerFieldsMap);
const csv = convertToCSV(fields, data, LedgerFieldsMap);

res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename=ledger_${Date.now()}.csv`);
res.send(csv);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename=ledger_${Date.now()}.csv`);
res.send(csv);
} catch (error) {
const err = error as Error;
throw new Error(err.message);
}
};

const methodHandlers: {
Expand Down
Loading

0 comments on commit f495a24

Please sign in to comment.