工会报销导出修改
This commit is contained in:
+3
-3
@@ -3,7 +3,7 @@ package com.budwk.app.zhgh.dayofficework.unionReimburse.service;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
||||
import java.util.Map;
|
||||
|
||||
/** 工会报销两类审批单共用的导出服务。 */
|
||||
/** 工会报销活动及慰问审批单共用的导出服务。 */
|
||||
public interface UnionReimburseExportService {
|
||||
/**
|
||||
* @param id 当前登录人已提交的报销单ID,不接受其他人的单据或待提交单据
|
||||
@@ -14,13 +14,13 @@ public interface UnionReimburseExportService {
|
||||
/**
|
||||
* 汇总页所有状态的报销单均可打印,必须具有汇总权限并符合所属工会数据范围。
|
||||
* @param id 汇总页选中的报销单 ID,可为待提交或其他申请状态
|
||||
* @return 与我的报销导出使用相同模板生成的完整 PDF 字节数组,无权限或生成失败时抛出业务异常
|
||||
* @return 按慰问类型选择模板生成的完整 PDF 字节数组,活动沿用原模板;无权限或生成失败时抛出业务异常
|
||||
*/
|
||||
byte[] exportCollectPdf(String id);
|
||||
|
||||
/**
|
||||
* @param form 已加载发票明细的报销单,支付明细为空时兼容旧主表字段
|
||||
* @return 两份模板通用的平铺字段及 payments、invoices 循环行
|
||||
* @return 活动及慰问模板通用的平铺字段及 payments、invoices 循环行
|
||||
*/
|
||||
Map<String, Object> buildExportData(UnionReimburse form);
|
||||
}
|
||||
|
||||
+35
-5
@@ -16,6 +16,7 @@ import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburseInvoiceDetail;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseExportService;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService;
|
||||
import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType;
|
||||
import com.deepoove.poi.XWPFTemplate;
|
||||
import com.deepoove.poi.config.Configure;
|
||||
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
||||
@@ -49,6 +50,13 @@ import java.util.*;
|
||||
public class UnionReimburseExportServiceImpl implements UnionReimburseExportService {
|
||||
public static final String ACTIVITY_TEMPLATE = "union_reimburse_activity_pdf";
|
||||
public static final String CONDOLENCE_TEMPLATE = "union_reimburse_condolence_pdf";
|
||||
// 使用慰问类型业务编码匹配模板,避免类型名称调整后选择错误;编码与现有申请页一致。
|
||||
private static final Map<String, String> COLLECT_CONDOLENCE_TEMPLATES = Map.of(
|
||||
"A", "union_reimburse_condolence_marriage_pdf",
|
||||
"D", "union_reimburse_condolence_birth_pdf",
|
||||
"E", "union_reimburse_condolence_hospital_pdf",
|
||||
"G", "union_reimburse_condolence_member_death_pdf",
|
||||
"H", "union_reimburse_condolence_relative_death_pdf");
|
||||
@Inject
|
||||
private UnionReimburseService unionReimburseService;
|
||||
@Inject
|
||||
@@ -62,7 +70,7 @@ public class UnionReimburseExportServiceImpl implements UnionReimburseExportServ
|
||||
return renderDocument(loadExportForm(id, false, false), true, false);
|
||||
}
|
||||
|
||||
/** 汇总打印支持所有状态,并严格按汇总权限和所属工会范围读取,复用我的报销 PDF 模板。 */
|
||||
/** 汇总打印支持所有状态,按汇总权限和所属工会范围读取,并按慰问类型选择模板。 */
|
||||
@Override
|
||||
public byte[] exportCollectPdf(String id) {
|
||||
return renderDocument(loadExportForm(id, true, true), true, true);
|
||||
@@ -103,10 +111,32 @@ public class UnionReimburseExportServiceImpl implements UnionReimburseExportServ
|
||||
return form;
|
||||
}
|
||||
|
||||
/** 两份模板共用字段和循环行配置;collectPrint 仅为汇总打印补充编号二维码。 */
|
||||
/**
|
||||
* 汇总页五类慰问使用独立模板,其余类型和我的报销沿用原模板。
|
||||
* @param form 已通过数据权限校验的报销单,使用慰问类型 ID 读取业务编码
|
||||
* @param collectPrint 是否为汇总页打印,只有该入口启用分类模板
|
||||
* @return 系统导出模板管理中的模板编码;类型关联缺失时提示核对,不猜测类型名称
|
||||
*/
|
||||
private String resolveTemplateCode(UnionReimburse form, boolean collectPrint) {
|
||||
if (!"UNION_REIMBURSE_PROJECT_1".equals(form.getReimburseProject())) {
|
||||
return ACTIVITY_TEMPLATE;
|
||||
}
|
||||
if (!collectPrint) {
|
||||
return CONDOLENCE_TEMPLATE;
|
||||
}
|
||||
// 兼容旧申请将类型 ID 保存在 condolenceType 的情况,不回写历史数据。
|
||||
String typeId = StrUtil.blankToDefault(form.getCondolenceTypeId(), form.getCondolenceType());
|
||||
CondolenceType type = StrUtil.isBlank(typeId) ? null : dao.fetch(CondolenceType.class, typeId);
|
||||
if (type == null || StrUtil.isBlank(type.getCode())) {
|
||||
throw new BaseException("慰问类型不存在或编码为空,请核对后打印");
|
||||
}
|
||||
// 五类之外的既有慰问继续使用通用模板;已匹配的专用模板缺失时由模板加载器明确报错。
|
||||
return COLLECT_CONDOLENCE_TEMPLATES.getOrDefault(type.getCode(), CONDOLENCE_TEMPLATE);
|
||||
}
|
||||
|
||||
/** 模板共用字段和循环行配置;汇总打印按慰问类型选模板并补充编号二维码。 */
|
||||
private byte[] renderDocument(UnionReimburse form, boolean asPdf, boolean collectPrint) {
|
||||
String templateCode = "UNION_REIMBURSE_PROJECT_1".equals(form.getReimburseProject())
|
||||
? CONDOLENCE_TEMPLATE : ACTIVITY_TEMPLATE;
|
||||
String templateCode = resolveTemplateCode(form, collectPrint);
|
||||
Path docx = null;
|
||||
Path pdf = null;
|
||||
try {
|
||||
@@ -114,7 +144,7 @@ public class UnionReimburseExportServiceImpl implements UnionReimburseExportServ
|
||||
if (asPdf) pdf = Files.createTempFile("union_reimburse_", ".pdf");
|
||||
// 活动和慰问模板的支付标记均位于表头,支付明细统一循环下一行。
|
||||
// 活动模板的发票标记也位于表头,循环下一行;慰问保留原有的当前行配置。
|
||||
boolean invoiceOnSameRow = CONDOLENCE_TEMPLATE.equals(templateCode);
|
||||
boolean invoiceOnSameRow = "UNION_REIMBURSE_PROJECT_1".equals(form.getReimburseProject());
|
||||
Configure config = Configure.builder()
|
||||
.bind("payments", new LoopRowTableRenderPolicy(false))
|
||||
.bind("invoices", new LoopRowTableRenderPolicy(invoiceOnSameRow)).build();
|
||||
|
||||
+18
-6
@@ -125,7 +125,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
/**
|
||||
* 保存、提交及重新提交共用支付信息校验,并在同一事务内保存申请和发票明细。
|
||||
* @param unionReimburse 申请表单,paymentDetails 为支付方式、金额及对私收款资料组成的明细列表
|
||||
* @param stateId 1 表示保存草稿,2 表示提交;均要求支付信息完整、发票必填且通过服务端验真
|
||||
* @param stateId 1 表示保存草稿,2 表示提交;均要求支付信息完整,发票选填,上传后须通过服务端验真
|
||||
* @return Result 成功或失败结果,msg 包含失败明细的行号及原因
|
||||
*/
|
||||
@Override
|
||||
@@ -134,7 +134,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
if (unionReimburse == null) {
|
||||
return Result.error("报销数据不能为空");
|
||||
}
|
||||
// 保存、提交及重提统一要求发票完整,不能通过慰问类型或草稿状态跳过。
|
||||
// 发票选填;保存、提交及重提均对已上传的发票检查完整性。
|
||||
Result invoiceResult = validateInvoiceDetails(unionReimburse);
|
||||
if (invoiceResult != null) {
|
||||
return invoiceResult;
|
||||
@@ -669,7 +669,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有报销类型由必填发票明细自动汇总金额和张数。
|
||||
* 有发票时汇总票面金额和张数;无发票时按已校验的支付明细汇总金额,张数为零。
|
||||
*/
|
||||
private void fillInvoiceSummary(UnionReimburse unionReimburse) {
|
||||
double totalMoney = 0D;
|
||||
@@ -684,6 +684,14 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
invoiceFileCount++;
|
||||
}
|
||||
}
|
||||
// 无发票申请从服务端已校验的支付明细计算金额,不能信任前端传入的报销总额。
|
||||
if (Lang.isEmpty(unionReimburse.getInvoiceDetails())) {
|
||||
BigDecimal paymentTotal = BigDecimal.ZERO;
|
||||
for (cn.hutool.json.JSONObject detail : unionReimburse.getPaymentDetails()) {
|
||||
paymentTotal = paymentTotal.add(new BigDecimal(detail.get("money").toString()));
|
||||
}
|
||||
totalMoney = paymentTotal.setScale(2, RoundingMode.HALF_UP).doubleValue();
|
||||
}
|
||||
unionReimburse.setMoney(BigDecimal.valueOf(totalMoney).setScale(2, RoundingMode.HALF_UP).doubleValue());
|
||||
unionReimburse.setInvoiceNumber(String.valueOf(invoiceFileCount));
|
||||
}
|
||||
@@ -754,6 +762,10 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
*/
|
||||
private Result validatePaymentAmountLimit(UnionReimburse form) {
|
||||
List<UnionReimburseInvoiceDetail> invoices = form.getInvoiceDetails();
|
||||
// 无发票时不设票面金额上限,支付明细仍须通过必填及正金额校验。
|
||||
if (Lang.isEmpty(invoices)) {
|
||||
return null;
|
||||
}
|
||||
BigDecimal invoiceTotal = BigDecimal.ZERO.setScale(2);
|
||||
if (invoices != null) {
|
||||
for (int i = 0; i < invoices.size(); i++) {
|
||||
@@ -821,11 +833,11 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存及提交前检查发票完整性,每条明细必须且只能包含一张可定位的发票文件。
|
||||
* 发票列表允许为空;已有明细必须且只能包含一张可定位的发票文件。
|
||||
*/
|
||||
private Result validateInvoiceDetails(UnionReimburse unionReimburse) {
|
||||
if (Lang.isEmpty(unionReimburse.getInvoiceDetails())) {
|
||||
return Result.error("请至少维护一条发票明细");
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < unionReimburse.getInvoiceDetails().size(); i++) {
|
||||
UnionReimburseInvoiceDetail detail = unionReimburse.getInvoiceDetails().get(i);
|
||||
@@ -1596,7 +1608,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
}
|
||||
|
||||
/**
|
||||
* 区分活动类与慰问类的基本信息校验;所有类型的发票均为必填。
|
||||
* 区分活动类与慰问类的基本信息校验;所有类型的发票均为选填。
|
||||
*/
|
||||
private boolean needInvoiceDetails(UnionReimburse unionReimburse) {
|
||||
return unionReimburse != null && !"UNION_REIMBURSE_PROJECT_1".equals(unionReimburse.getReimburseProject());
|
||||
|
||||
+35
-11
@@ -438,7 +438,7 @@ layout("/layouts/platform.html"){
|
||||
<el-col :span="12">
|
||||
<el-form-item label="报销金额" prop="money">
|
||||
<div class="invoice-field-title">报销金额</div>
|
||||
<el-input v-model="formData.money" placeholder="自动汇总发票金额" readonly></el-input>
|
||||
<el-input v-model="formData.money" placeholder="有发票按发票汇总,无发票按支付信息汇总" readonly></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
@@ -587,11 +587,11 @@ layout("/layouts/platform.html"){
|
||||
<div>
|
||||
<div class="invoice-toolbar-title">发票明细列表</div>
|
||||
<div class="invoice-summary-tip">
|
||||
所有报销类型均须上传发票并通过验真;每条明细对应一张发票图片或单页 PDF,金额和张数自动汇总。
|
||||
发票选填;无发票时按支付信息汇总报销金额。上传后须通过验真,每条明细对应一张发票图片或单页 PDF。
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-tag type="info">发票必须验真</el-tag>
|
||||
<el-tag type="info">发票选填,上传须验真</el-tag>
|
||||
<el-button @click="openInvoiceDialog('add')" plain type="primary"
|
||||
style="margin-left: 12px">新增发票
|
||||
</el-button>
|
||||
@@ -943,6 +943,15 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 无发票时支付明细决定报销金额,支付行增删或金额变化后同步更新汇总。
|
||||
"formData.paymentDetails": {
|
||||
handler() {
|
||||
if (!(this.formData.invoiceDetails || []).length) {
|
||||
this.rebuildInvoiceSummary()
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
"detailForm.invoiceFiles": {
|
||||
handler(newVal) {
|
||||
const file = newVal && newVal.length ? newVal[0] : null
|
||||
@@ -1221,7 +1230,14 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
}
|
||||
this.$set(row, "historyData", accounts)
|
||||
this.$set(row, "bankAccountId", selectedId)
|
||||
// 新建户名的选项必须先完成渲染,再更新选中值,避免 el-select 将临时账户ID直接显示为户名。
|
||||
this.$nextTick(() => {
|
||||
// 重复输入、账户请求回填或切换付款人会替换选项列表;旧回调不得覆盖最新选择。
|
||||
if (row.historyData !== accounts) {
|
||||
return
|
||||
}
|
||||
this.$set(row, "bankAccountId", selectedId)
|
||||
})
|
||||
},
|
||||
// 选中框只显示户名,下拉内容展示开户行和尾号,同名多卡仍按账户ID选择。
|
||||
bankAccountLabel(account) {
|
||||
@@ -1545,17 +1561,24 @@ layout("/layouts/platform.html"){
|
||||
fileCount = fileCount + 1
|
||||
}
|
||||
}
|
||||
// 所有类型都按必填发票汇总,不再使用无票慰问金额兜底。
|
||||
// 无发票时按支付明细汇总,使用整数分避免小数累加误差。
|
||||
if (!invoiceDetails.length) {
|
||||
let paymentCents = 0
|
||||
const paymentDetails = this.formData.paymentDetails || []
|
||||
paymentDetails.forEach(item => {
|
||||
const cents = this.moneyToCents(item && item.money)
|
||||
if (cents !== null) {
|
||||
paymentCents += cents
|
||||
}
|
||||
})
|
||||
totalMoney = paymentCents / 100
|
||||
}
|
||||
this.$set(this.formData, "money", totalMoney ? totalMoney.toFixed(2) : "0.00")
|
||||
this.$set(this.formData, "invoiceNumber", String(fileCount))
|
||||
},
|
||||
validateInvoiceDetailsBeforeSubmit() {
|
||||
const invoiceDetails = this.formData.invoiceDetails || []
|
||||
// 保存、提交及重提均要求至少一张发票,包括慰问类申请。
|
||||
if (!invoiceDetails.length) {
|
||||
this.$message.warning("请至少维护一条发票明细")
|
||||
return false
|
||||
}
|
||||
// 发票可不上传;已经添加的发票仍须填写完整并沿用验真流程。
|
||||
for (let i = 0; i < invoiceDetails.length; i++) {
|
||||
const currentDetail = invoiceDetails[i]
|
||||
if (!currentDetail.invoiceFiles || currentDetail.invoiceFiles.length === 0) {
|
||||
@@ -1675,7 +1698,8 @@ layout("/layouts/platform.html"){
|
||||
if (!Number.isSafeInteger(invoiceCents) || !Number.isSafeInteger(paymentCents)) {
|
||||
return "金额合计超出可处理范围,请检查填写金额"
|
||||
}
|
||||
if (paymentCents > invoiceCents) {
|
||||
// 无发票时只校验支付金额本身;有发票才限制支付合计不得超过票面总额。
|
||||
if (invoices.length && paymentCents > invoiceCents) {
|
||||
return "支付金额合计不能超过发票总额(支付合计:" + (paymentCents / 100).toFixed(2)
|
||||
+ "元,发票总额:" + (invoiceCents / 100).toFixed(2) + "元)"
|
||||
}
|
||||
|
||||
+27
-9
@@ -761,10 +761,11 @@ layout("/layouts/platform_h5.html"){
|
||||
</van-action-sheet>
|
||||
</van-cell-group>
|
||||
<van-cell-group class="form-section" v-if="formData.reimburseProject">
|
||||
<div class="form-section__title">电子发票</div>
|
||||
<div class="form-section__title">电子发票(选填)</div>
|
||||
<div class="invoice-tip">无发票时按支付信息汇总报销金额;上传的发票仍须通过验真。</div>
|
||||
<van-field label="报销金额"
|
||||
v-model="formData.money"
|
||||
placeholder="自动汇总发票金额"
|
||||
placeholder="有发票按发票汇总,无发票按支付信息汇总"
|
||||
readonly></van-field>
|
||||
<van-field label="发票张数"
|
||||
v-model="formData.invoiceNumber"
|
||||
@@ -1167,6 +1168,15 @@ layout("/layouts/platform_h5.html"){
|
||||
// 监听报销类别发生变化,报销项目联动改变
|
||||
// 修复 watch 中的监听器
|
||||
watch: {
|
||||
// 无发票时支付明细决定报销金额,支付行增删或金额变化后同步更新汇总。
|
||||
"formData.paymentDetails": {
|
||||
handler() {
|
||||
if (!(this.formData.invoiceDetails || []).length) {
|
||||
this.rebuildInvoiceSummary()
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
"detailForm.invoiceFiles": {
|
||||
handler(newVal) {
|
||||
const file = newVal && newVal.length ? newVal[0] : null
|
||||
@@ -1854,17 +1864,24 @@ layout("/layouts/platform_h5.html"){
|
||||
fileCount = fileCount + 1
|
||||
}
|
||||
}
|
||||
// 所有类型都按必填发票汇总,不再使用无票慰问金额兜底。
|
||||
// 无发票时按支付明细汇总,使用整数分避免小数累加误差。
|
||||
if (!invoiceDetails.length) {
|
||||
let paymentCents = 0
|
||||
const paymentDetails = this.formData.paymentDetails || []
|
||||
paymentDetails.forEach(item => {
|
||||
const cents = this.moneyToCents(item && item.money)
|
||||
if (cents !== null) {
|
||||
paymentCents += cents
|
||||
}
|
||||
})
|
||||
totalMoney = paymentCents / 100
|
||||
}
|
||||
this.$set(this.formData, "money", totalMoney ? totalMoney.toFixed(2) : "0.00")
|
||||
this.$set(this.formData, "invoiceNumber", String(fileCount))
|
||||
},
|
||||
validateInvoiceDetailsBeforeSubmit() {
|
||||
const invoiceDetails = this.formData.invoiceDetails || []
|
||||
// 与 PC 一致:草稿、提交和重新提交均要求所有类型提供发票。
|
||||
if (!invoiceDetails.length) {
|
||||
this.$toast.fail("请至少维护一条发票明细")
|
||||
return false
|
||||
}
|
||||
// 发票可不上传;已经添加的发票仍须填写完整并沿用验真流程。
|
||||
for (let i = 0; i < invoiceDetails.length; i++) {
|
||||
const currentDetail = invoiceDetails[i]
|
||||
if (!currentDetail.invoiceFiles || currentDetail.invoiceFiles.length === 0) {
|
||||
@@ -2167,7 +2184,8 @@ layout("/layouts/platform_h5.html"){
|
||||
if (!Number.isSafeInteger(invoiceCents) || !Number.isSafeInteger(paymentCents)) {
|
||||
return "金额合计超出可处理范围,请检查填写金额"
|
||||
}
|
||||
if (paymentCents > invoiceCents) {
|
||||
// 无发票时只校验支付金额本身;有发票才限制支付合计不得超过票面总额。
|
||||
if (invoices.length && paymentCents > invoiceCents) {
|
||||
return "支付金额合计不能超过发票总额(支付合计:" + (paymentCents / 100).toFixed(2)
|
||||
+ "元,发票总额:" + (invoiceCents / 100).toFixed(2) + "元)"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user