初次提交代码
This commit is contained in:
402
src/pages/api/approval/exportInvoiceDataFile.ts
Normal file
402
src/pages/api/approval/exportInvoiceDataFile.ts
Normal file
@@ -0,0 +1,402 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import prismaClient from '@/lib/prisma'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import archiver from 'archiver'
|
||||
import XLSX from 'xlsx'
|
||||
import XLSXStyle from 'xlsx-style';
|
||||
import dayjs from 'dayjs'
|
||||
import AuthClient from '@/api/dingding/auth'
|
||||
import { getHanShuiAmount, getBuHanShuiAmount, getAllAmount } from '@/pages/api/approval/util'
|
||||
import { AGREEMENT_TYPE_CODE, AGREEMENT_TYPES, XIANGMU_ZUBIE_MAP } from '@/CONSTANTS'
|
||||
import { downloadAttachments, processInvoiceDataItem } from '@/lib/util'
|
||||
|
||||
// 定义协议类型名称映射
|
||||
const AGREEMENT_CODE_TO_NAME = {
|
||||
'1': '大协议',
|
||||
'2': '独立协议',
|
||||
'3': '大协议',
|
||||
'4': '独立协议'
|
||||
};
|
||||
|
||||
// 定义列宽配置
|
||||
// 列宽:增加“开票作业序号”,移除“集团内外”
|
||||
const COLUMN_WIDTHS = {
|
||||
"开票作业序号": 15,
|
||||
"数据ID": 20,
|
||||
"项目编号": 15,
|
||||
"单位": 25,
|
||||
// 移除“集团内外”
|
||||
"票号": 15,
|
||||
"不含税金额": 15,
|
||||
"税额": 15,
|
||||
"含税金额": 15,
|
||||
"发票类型": 20,
|
||||
"申请人": 15,
|
||||
"专业组": 15,
|
||||
"开票日期": 15,
|
||||
"账期": 15,
|
||||
"回款日期": 15,
|
||||
"回款金额": 15,
|
||||
"统一社会信用代码": 25,
|
||||
"开户行名称": 25,
|
||||
"银行账号": 25,
|
||||
"业务内容": 30
|
||||
};
|
||||
|
||||
// 定义表头样式
|
||||
const HEADER_STYLE = {
|
||||
fill: {
|
||||
fgColor: { rgb: "bce672" } // 蓝色背景
|
||||
},
|
||||
font: {
|
||||
color: { rgb: "000000" }, // 白色字体
|
||||
bold: true
|
||||
},
|
||||
alignment: {
|
||||
horizontal: "center",
|
||||
vertical: "center"
|
||||
}
|
||||
};
|
||||
|
||||
// 定义数据单元格样式
|
||||
const CELL_STYLE = {
|
||||
alignment: {
|
||||
horizontal: "left",
|
||||
vertical: "center"
|
||||
},
|
||||
numFmt: "@"
|
||||
};
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ message: 'Method not allowed' })
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取access token,实际实现中需要从请求上下文或认证系统获取
|
||||
const accessToken = await AuthClient.getAccessToken();
|
||||
|
||||
const { ids } = req.body
|
||||
console.log('Received IDs:', ids);
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({ message: 'Invalid or missing IDs array' })
|
||||
}
|
||||
if (ids.length > 1000) {
|
||||
return res.status(400).json({ message: 'Too many IDs requested. Maximum 1000 IDs allowed.' })
|
||||
}
|
||||
console.log('Fetching data for IDs:', ids);
|
||||
const invoiceData = await prismaClient.invoice_application_match_view.findMany({
|
||||
where: {
|
||||
dataId: {
|
||||
in: ids
|
||||
}
|
||||
},
|
||||
// 预排序(视图字段为数字时有效;为字符串时按字典序,下面还有兜底排序)
|
||||
orderBy: { invoiceJobNumber: 'asc' }
|
||||
})
|
||||
|
||||
// 提取大协议的dataId
|
||||
const largeAgreementIds = invoiceData
|
||||
.filter(item => item.agreementType === '1')
|
||||
.map(item => item.dataId);
|
||||
|
||||
// 从invoice_amount_details获取大协议的业务内容
|
||||
const largeAgreementDetails = await prismaClient.invoice_amount_details.findMany({
|
||||
where: {
|
||||
dataid: {
|
||||
in: largeAgreementIds
|
||||
},
|
||||
agreementType: AGREEMENT_TYPE_CODE[AGREEMENT_TYPES.LARGE]
|
||||
}
|
||||
});
|
||||
|
||||
// 创建一个映射,存储每个dataid对应的多个business_content
|
||||
const businessContentMap = new Map<string, string[]>();
|
||||
largeAgreementDetails.forEach((detail, idx) => {
|
||||
if (detail.dataid && detail.business_content) {
|
||||
if (!businessContentMap.has(detail.dataid)) {
|
||||
businessContentMap.set(detail.dataid, []);
|
||||
}
|
||||
businessContentMap.get(detail.dataid)?.push(`${idx}. [${detail.project_name}]${detail.business_content}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('拉取开票申请数据数量:', invoiceData.length);
|
||||
const modifiedInvoiceData = invoiceData
|
||||
.map(item => {
|
||||
const processed = processInvoiceDataItem(item)
|
||||
const buHanShuiAmount = getBuHanShuiAmount(item)
|
||||
const hanShuiAmount = getHanShuiAmount(item)
|
||||
const AllAmount = getAllAmount(buHanShuiAmount, hanShuiAmount);
|
||||
|
||||
// 业务内容统一取开票事由
|
||||
const businessContent = item.kai_piao_shi_you || '';
|
||||
|
||||
// 根据新的协议类型显示逻辑
|
||||
let agreementTypeName = '';
|
||||
if (item.agreementType === '2' || item.agreementType === '4') {
|
||||
agreementTypeName = '独立协议';
|
||||
} else if (item.agreementType === '1' || item.agreementType === '3') {
|
||||
agreementTypeName = '大协议';
|
||||
} else {
|
||||
agreementTypeName = AGREEMENT_CODE_TO_NAME[item.agreementType as keyof typeof AGREEMENT_CODE_TO_NAME] || '';
|
||||
}
|
||||
|
||||
// 专业组逻辑:提取项目编号的所有字母部分映射,如果没有项目编号则为客户服务室
|
||||
const getProjectGroupByProjectId = (projectId: string | null | undefined): string => {
|
||||
if (!projectId) {
|
||||
return '客户服务室';
|
||||
}
|
||||
|
||||
// 提取项目编号的所有字母部分(匹配连续的字母字符,不限位数)
|
||||
const letterMatch = projectId.match(/^[A-Za-z]+/);
|
||||
const prefix = letterMatch ? letterMatch[0] : '';
|
||||
|
||||
// 根据字母前缀映射专业组
|
||||
const groupMapping: { [key: string]: string } = {
|
||||
'YY': '运营室',
|
||||
'ZP': '人员招聘项目组',
|
||||
'ZW': '总务服务项目组',
|
||||
'XC': '薪酬核算项目组',
|
||||
'TX': '退休衔接服务项目组',
|
||||
'TC': '统筹支持项目组',
|
||||
'RZL': '人员入转离项目组',
|
||||
'PJ': '人才评鉴项目组',
|
||||
'HW': '会务服务项目组',
|
||||
'GJ': '国际化服务项目组',
|
||||
'GZ': '管理咨询项目组',
|
||||
'KF': '客户服务室',
|
||||
'FL': '福利业务项目组',
|
||||
'FW': '法务合规中心',
|
||||
'SJ': '数字化与交付项目组',
|
||||
'KJ': '科技培训项目组',
|
||||
'JG': '经营管理项目组',
|
||||
'JN': '技能培训项目组',
|
||||
'DJ': '党建培训项目组',
|
||||
'JH': '计划核定项目组',
|
||||
'LGB': '老干部服务项目组'
|
||||
};
|
||||
|
||||
return groupMapping[prefix] || '客户服务室';
|
||||
};
|
||||
|
||||
const zhuan_ye_zu = getProjectGroupByProjectId(item.projectNumber);
|
||||
|
||||
// 账期计算逻辑
|
||||
let zhang_qi = '';
|
||||
const kai_piao_ri_qi = item.kai_piao_ri_qi;
|
||||
const hui_kuan_ri_qi = item.hui_kuan_ri_qi;
|
||||
|
||||
// 优先检查回款日期:如果有回款日期,则没有账期
|
||||
if (hui_kuan_ri_qi) {
|
||||
zhang_qi = '';
|
||||
} else if (kai_piao_ri_qi) {
|
||||
// 只有开票日期:账期 = 当前日期 - 开票日期(单位:天)
|
||||
const kp = dayjs(kai_piao_ri_qi);
|
||||
const now = dayjs();
|
||||
if (kp.isValid()) {
|
||||
const dayDiff = now.diff(kp, 'day');
|
||||
zhang_qi = dayDiff.toString();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"开票作业序号": item.invoiceJobNumber || '',
|
||||
"数据ID": item.dataId,
|
||||
"项目编号": item.projectNumber || '',
|
||||
"单位": processed.customerCompanyName || '',
|
||||
"票号": item.piao_hao || '',
|
||||
"不含税金额": buHanShuiAmount,
|
||||
"税额": AllAmount,
|
||||
"含税金额": hanShuiAmount,
|
||||
"发票类型": item.invoiceType || '',
|
||||
"申请人": item.applicant || '',
|
||||
"专业组": zhuan_ye_zu || '',
|
||||
"开票日期": item.kai_piao_ri_qi || '',
|
||||
"账期": zhang_qi, // 使用计算后的账期值
|
||||
"回款日期": item.hui_kuan_ri_qi || '',
|
||||
"回款金额": item.hui_kuan_jin_e || '',
|
||||
"统一社会信用代码": processed.tong_yi_she_hui_xin_yong_dai_ma || '',
|
||||
"开户行名称": processed.bankName || '',
|
||||
"银行账号": processed.bankAccount || '',
|
||||
"业务内容": businessContent,
|
||||
"协议类型": agreementTypeName
|
||||
};
|
||||
})
|
||||
// 兜底排序:有效数字升序,空值/非数字排最后
|
||||
.sort((a, b) => {
|
||||
const aRaw = a["开票作业序号"];
|
||||
const bRaw = b["开票作业序号"];
|
||||
|
||||
const aEmpty = aRaw === '' || aRaw == null;
|
||||
const bEmpty = bRaw === '' || bRaw == null;
|
||||
if (aEmpty && bEmpty) return 0;
|
||||
if (aEmpty) return 1;
|
||||
if (bEmpty) return -1;
|
||||
|
||||
const aNum = Number(aRaw);
|
||||
const bNum = Number(bRaw);
|
||||
const aNumValid = Number.isFinite(aNum);
|
||||
const bNumValid = Number.isFinite(bNum);
|
||||
if (aNumValid && bNumValid) return aNum - bNum;
|
||||
if (aNumValid) return -1;
|
||||
if (bNumValid) return 1;
|
||||
|
||||
return String(aRaw).localeCompare(String(bRaw), 'zh-Hans-CN', { numeric: true });
|
||||
});
|
||||
// 转为工作表时的表头移除“集团内外”,统一“开票作业序号”
|
||||
const worksheet = XLSX.utils.json_to_sheet(modifiedInvoiceData, {
|
||||
header: [
|
||||
"开票作业序号",
|
||||
"数据ID",
|
||||
"项目编号",
|
||||
"单位",
|
||||
// "集团内外" 已移除
|
||||
"票号",
|
||||
"不含税金额",
|
||||
"税额",
|
||||
"含税金额",
|
||||
"发票类型",
|
||||
"申请人",
|
||||
"专业组",
|
||||
"开票日期",
|
||||
"账期",
|
||||
"回款日期",
|
||||
"回款金额",
|
||||
"统一社会信用代码",
|
||||
"开户行名称",
|
||||
"银行账号",
|
||||
"业务内容"
|
||||
]
|
||||
})
|
||||
|
||||
// 设置列宽
|
||||
const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1');
|
||||
const cols = [];
|
||||
for (let i = 0; i <= range.e.c; i++) {
|
||||
const colHeader = worksheet[XLSX.utils.encode_cell({ r: 0, c: i })]?.v;
|
||||
cols.push({ wch: COLUMN_WIDTHS[colHeader] || 15 });
|
||||
}
|
||||
worksheet['!cols'] = cols;
|
||||
|
||||
// 设置表头样式和单元格格式
|
||||
for (let R = range.s.r; R <= range.e.r; R++) {
|
||||
for (let C = range.s.c; C <= range.e.c; C++) {
|
||||
const cellAddress = XLSX.utils.encode_cell({ r: R, c: C });
|
||||
if (R === 0) {
|
||||
// 表头行
|
||||
worksheet[cellAddress].s = HEADER_STYLE;
|
||||
} else {
|
||||
// 数据行
|
||||
worksheet[cellAddress].s = CELL_STYLE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new workbook and append the worksheet
|
||||
const workbook = XLSX.utils.book_new()
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, '开票申请数据')
|
||||
|
||||
const buffer = XLSXStyle.write(workbook, { type: 'buffer', bookType: 'xlsx', cellStyles: true })
|
||||
|
||||
// 创建日期时间戳文件夹
|
||||
const now = dayjs();
|
||||
const dateStr = now.format('YYYY-MM-DD');
|
||||
const timeStr = now.format('HHmmss');
|
||||
const folderName = `${dateStr}_${timeStr}`;
|
||||
const baseDir = path.join(process.cwd(), 'filebase');
|
||||
const folderPath = path.join(baseDir, folderName);
|
||||
|
||||
// 确保filebase目录存在
|
||||
if (!fs.existsSync(baseDir)) {
|
||||
fs.mkdirSync(baseDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(folderPath)) {
|
||||
fs.mkdirSync(folderPath, { recursive: true });
|
||||
}
|
||||
|
||||
// 保存Excel文件到文件夹
|
||||
const excelFileName = '开票申请数据.xlsx';
|
||||
const excelFilePath = path.join(folderPath, excelFileName);
|
||||
fs.writeFileSync(excelFilePath, buffer);
|
||||
|
||||
for (const item of invoiceData) {
|
||||
const invoiceFolderPath = path.join(folderPath, `${item.invoiceJobNumber}-${item.applicant}`)
|
||||
if (item.uploadedContractAttachment) {
|
||||
await downloadAttachments(
|
||||
item.uploadedContractAttachment,
|
||||
invoiceFolderPath,
|
||||
'上传合同及相关附件',
|
||||
item.dataId,
|
||||
accessToken,
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
// 处理uploadedSealedCostList
|
||||
if (item.uploadedSealedCostListDa) {
|
||||
await downloadAttachments(
|
||||
item.uploadedSealedCostListDa,
|
||||
invoiceFolderPath,
|
||||
'上传费用清单',
|
||||
item.dataId,
|
||||
accessToken,
|
||||
''
|
||||
);
|
||||
}
|
||||
// 处理uploadedSealedCostList
|
||||
if (item.uploadedSealedCostListDu) {
|
||||
await downloadAttachments(
|
||||
item.uploadedSealedCostListDu,
|
||||
invoiceFolderPath,
|
||||
'上传盖章费用清单(独)',
|
||||
item.dataId,
|
||||
accessToken,
|
||||
''
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建压缩文件
|
||||
const zipFileName = `${folderName}.zip`;
|
||||
const zipFilePath = path.join(process.env.DOWNLOAD_BASE_DIR ?? '', zipFileName);
|
||||
const output = fs.createWriteStream(zipFilePath);
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 9 } // 设置压缩级别
|
||||
});
|
||||
|
||||
output.on('error', function (err) {
|
||||
console.error('Error writing zip file:', err);
|
||||
res.status(500).json({ message: 'Error writing zip file' });
|
||||
});
|
||||
// 当归档完成时发送响应
|
||||
output.on('close', function () {
|
||||
console.log(archive.pointer() + ' total bytes');
|
||||
console.log('archiver has been finalized and the output file descriptor has closed.');
|
||||
console.log(`${process.env.DOWNLOAD_BASE_URL ?? ''}/${zipFileName}`)
|
||||
res.status(200).json({ code: 0, "error": null, message: '文件已经被妥善下载。', data: { zipFilePath: `${process.env.DOWNLOAD_BASE_URL ?? ''}/${zipFileName}` } });
|
||||
});
|
||||
|
||||
// 处理归档错误
|
||||
archive.on('error', function (err) {
|
||||
console.error('Error creating zip file:', err);
|
||||
res.status(500).json({ message: 'Error creating zip file' });
|
||||
});
|
||||
|
||||
// 开始归档过程
|
||||
archive.pipe(output);
|
||||
archive.directory(folderPath, false); // 将整个文件夹添加到归档中 TODO 这个foldername 是个啥
|
||||
archive.finalize();
|
||||
} catch (error) {
|
||||
console.error('Error exporting invoice data:', error)
|
||||
res.status(500).json({ message: 'Internal server error', error })
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user