403 lines
14 KiB
TypeScript
403 lines
14 KiB
TypeScript
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 })
|
||
}
|
||
|
||
|
||
|
||
}
|