Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -37,6 +37,7 @@ import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.GET;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
import org.nutz.mvc.annotation.POST;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.Valid;
|
||||
@@ -499,6 +500,24 @@ public class SysRoleController {
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为全部 member=1 的用户设置会员角色,无需传入用户 ID、角色 ID 或列表筛选参数。
|
||||
*
|
||||
* @return JSON Result,data.addedCount 为新增关联数,data.enabledCount 为恢复启用关联数
|
||||
*/
|
||||
@At
|
||||
@POST
|
||||
@Ok("json")
|
||||
@SaCheckPermission("sys.manager.role.user")
|
||||
@SLog(tag = "一键设置会员角色", msg = "为全部member=1的人员补齐会员角色")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result doSettingMemberRole() {
|
||||
NutMap counts = sysRoleService.settingMemberRole();
|
||||
sysRoleService.clearCache();
|
||||
sysUserService.clearCache();
|
||||
return Result.success("会员角色设置完成", counts);
|
||||
}
|
||||
|
||||
@At
|
||||
@Ok("json")
|
||||
@SaCheckPermission("sys.manager.role")
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.budwk.app.base.service.BaseService;
|
||||
import com.budwk.app.sys.models.Sys_menu;
|
||||
import com.budwk.app.sys.models.Sys_role;
|
||||
import com.budwk.app.sys.models.Sys_unit;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -128,6 +129,13 @@ public interface SysRoleService extends BaseService<Sys_role> {
|
||||
*/
|
||||
List<String> getRoleIdsByCode(List<RoleConstant> roleConstantList);
|
||||
|
||||
/**
|
||||
* 为用户表中 member=1 的全部人员补齐启用的 MEMBER 角色,不受列表筛选影响。
|
||||
*
|
||||
* @return addedCount 为新增关联数,enabledCount 为恢复启用的关联数;已有启用关联不重复添加
|
||||
*/
|
||||
NutMap settingMemberRole();
|
||||
|
||||
/**
|
||||
* 清空缓存
|
||||
*/
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.Strings;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.plugins.wkcache.annotation.CacheDefaults;
|
||||
import org.nutz.plugins.wkcache.annotation.CacheRemoveAll;
|
||||
import org.nutz.plugins.wkcache.annotation.CacheResult;
|
||||
@@ -284,4 +285,44 @@ public class SysRoleServiceImpl extends BaseServiceImpl<Sys_role> implements Sys
|
||||
public void clearCache() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量补齐会员角色。仅处理 member=1 的用户,保留其他角色及账号禁用状态。
|
||||
*
|
||||
* @return addedCount 为新增关联数,enabledCount 为恢复启用的关联数
|
||||
*/
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public NutMap settingMemberRole() {
|
||||
// 锁定同一会员角色,使本接口的并发请求依次执行,避免重复插入用户角色关系。
|
||||
Sql roleSql = Sqls.create("SELECT * FROM sys_role WHERE code=@code FOR UPDATE");
|
||||
roleSql.setParam("code", RoleConstant.MEMBER.name());
|
||||
roleSql.setEntity(dao().getEntity(Sys_role.class));
|
||||
roleSql.setCallback(Sqls.callback.entities());
|
||||
dao().execute(roleSql);
|
||||
List<Sys_role> roles = roleSql.getList(Sys_role.class);
|
||||
if (roles.isEmpty()) {
|
||||
throw new BaseException("会员角色(MEMBER)不存在,请先配置会员角色");
|
||||
}
|
||||
Sys_role role = roles.get(0);
|
||||
if (role.isDisabled()) {
|
||||
throw new BaseException("会员角色(MEMBER)已禁用,请先启用会员角色");
|
||||
}
|
||||
|
||||
// 历史关联可能已停用或未填写 enable;恢复关联即可,无需重新插入。
|
||||
Sql enableSql = Sqls.create("UPDATE sys_user_role SET enable=1 WHERE roleId=@roleId "
|
||||
+ "AND (enable IS NULL OR enable<>1) "
|
||||
+ "AND EXISTS (SELECT 1 FROM sys_user u WHERE u.id=sys_user_role.userId AND u.member=1)");
|
||||
enableSql.setParam("roleId", role.getId());
|
||||
dao().execute(enableSql);
|
||||
|
||||
// 按用户、角色判重,只为缺少会员关联的人员添加记录;不删除任何已有角色。
|
||||
Sql insertSql = Sqls.create("INSERT INTO sys_user_role (userId,roleId,enable) "
|
||||
+ "SELECT u.id,@roleId,1 FROM sys_user u WHERE u.member=1 "
|
||||
+ "AND NOT EXISTS (SELECT 1 FROM sys_user_role ur WHERE ur.userId=u.id AND ur.roleId=@roleId)");
|
||||
insertSql.setParam("roleId", role.getId());
|
||||
dao().execute(insertSql);
|
||||
return NutMap.NEW().addv("addedCount", insertSql.getUpdateCount())
|
||||
.addv("enabledCount", enableSql.getUpdateCount());
|
||||
}
|
||||
}
|
||||
|
||||
+36
-1
@@ -27,6 +27,7 @@ import com.budwk.app.zhgh.staffbenefit.condolence.service.CondolenceTypeService;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
||||
import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseBankAccountService;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.vo.UnionReimburseBankHistoryVO;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
@@ -45,6 +46,7 @@ import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
import org.nutz.mvc.annotation.POST;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
@@ -65,6 +67,8 @@ public class UnionReimburseApplyController {
|
||||
@Inject
|
||||
private UnionReimburseService unionReimburseService;
|
||||
@Inject
|
||||
private UnionReimburseBankAccountService unionReimburseBankAccountService;
|
||||
@Inject
|
||||
private FlowCommonService flowCommonService;
|
||||
@Inject
|
||||
private CondolenceTypeService typeService;
|
||||
@@ -83,6 +87,7 @@ public class UnionReimburseApplyController {
|
||||
|
||||
@At
|
||||
@ApiOperation("保存申请")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
|
||||
@SLog(type = "unionReimburse", tag = "工会报销-报销申请", msg = "保存报销申请")
|
||||
public Result save(@Param("data") UnionReimburse unionReimburse) {
|
||||
@@ -204,6 +209,31 @@ public class UnionReimburseApplyController {
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param payer 付款人用户ID,不传姓名或工号
|
||||
* @param reimburseFundSource 当前经费来源编码
|
||||
* @param clubId 协会经费对应的协会ID,其他来源可为空
|
||||
* @return Result.data 为账户列表,每项包含 id、bankUserName、bankCardNumber、bankOfDeposit
|
||||
*/
|
||||
@At
|
||||
@POST
|
||||
@ApiOperation("查询付款人常用收款账户")
|
||||
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
|
||||
public Result listBankAccounts(String payer, String reimburseFundSource, String clubId) {
|
||||
return Result.success(unionReimburseBankAccountService.listAccounts(payer, reimburseFundSource, clubId));
|
||||
}
|
||||
|
||||
/** 无请求参数;仅系统管理员执行。Result.data 为扫描的历史单据数量,重复调用不会新增重复账户。 */
|
||||
@At
|
||||
@POST
|
||||
@ApiOperation("初始化历史收款账户")
|
||||
@SaCheckLogin
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SLog(type = "unionReimburse", tag = "历史收款账户初始化", msg = "导入历史报销收款账户")
|
||||
public Result importBankAccountHistory() {
|
||||
return Result.success(unionReimburseBankAccountService.importHistory());
|
||||
}
|
||||
|
||||
@At
|
||||
@ApiOperation("校验发票号码是否重复")
|
||||
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
|
||||
@@ -212,8 +242,13 @@ public class UnionReimburseApplyController {
|
||||
return result == null ? Result.success() : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fileId 上传接口返回的文件ID,服务端读取对应发票文件
|
||||
* @param verifySwitch 旧页面兼容参数,任何取值均强制验真
|
||||
* @return Result,code=0 时 data 为票号、金额、销售方、项目及 verified=true;非零时 msg 为失败原因
|
||||
*/
|
||||
@At
|
||||
@ApiOperation("自动识别发票信息")
|
||||
@ApiOperation("识别发票并强制验真")
|
||||
@SaCheckPermission(value = {"unionReimburse.apply", "h5.unionReimburse.apply"}, mode = SaMode.OR)
|
||||
public Result recognizeInvoice(String fileId, @Param("verifySwitch") Boolean verifySwitch) {
|
||||
return unionReimburseService.recognizeInvoice(fileId, verifySwitch);
|
||||
|
||||
+18
@@ -22,6 +22,7 @@ import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseExportService;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.vo.UnionReimburseCollectExcelVO;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
@@ -64,6 +65,9 @@ public class UnionReimburseCollectController {
|
||||
@Inject
|
||||
private UnionReimburseService unionReimburseService;
|
||||
|
||||
@Inject
|
||||
private UnionReimburseExportService unionReimburseExportService;
|
||||
|
||||
@At("/index")
|
||||
@Ok("beetl:/platform/zhgh/dayofficework/unionReimburse/collect/index.html")
|
||||
@SaCheckPermission("unionReimburse.collect")
|
||||
@@ -134,6 +138,20 @@ public class UnionReimburseCollectController {
|
||||
return unionReimburseService.updateReimburseProject(id, reimburseProject);
|
||||
}
|
||||
|
||||
/**
|
||||
* 汇总页打印复用审批单 PDF 导出,支持所有申请状态,数据范围由 service 校验。
|
||||
* @param id 汇总页选中的报销单 ID,不受申请状态限制
|
||||
* @param response 文件下载响应,返回完整 PDF 附件;失败由统一异常处理返回业务提示
|
||||
*/
|
||||
@At
|
||||
@Ok("void")
|
||||
@ApiOperation("打印报销审批单PDF")
|
||||
@SaCheckPermission("unionReimburse.collect")
|
||||
public void exportPdf(@Param("id") String id, HttpServletResponse response) {
|
||||
byte[] pdf = unionReimburseExportService.exportCollectPdf(id);
|
||||
CommonDownloadUtil.download("工会经费报销审批单_" + id + ".pdf", pdf, response);
|
||||
}
|
||||
|
||||
@At
|
||||
@Ok("void")
|
||||
@SaCheckPermission(value = {"unionReimburse.collect", "h5.unionReimburse.collect"}, mode = SaMode.OR)
|
||||
|
||||
+12
-276
@@ -1,62 +1,39 @@
|
||||
package com.budwk.app.zhgh.dayofficework.unionReimburse.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.annotation.SLog;
|
||||
import com.budwk.app.base.model.Audit;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.base.page.Pagination;
|
||||
import com.budwk.app.base.param.PageForm;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.base.utils.CommonDownloadUtil;
|
||||
import com.budwk.app.base.utils.MoneyUtil;
|
||||
import com.budwk.app.base.utils.OfficePlusUtil;
|
||||
import com.budwk.app.base.utils.PageUtil;
|
||||
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
|
||||
import com.budwk.app.flow.engine.FlowEngine;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService;
|
||||
import com.deepoove.poi.XWPFTemplate;
|
||||
import com.deepoove.poi.config.Configure;
|
||||
import com.deepoove.poi.data.PictureRenderData;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.client.j2se.MatrixToImageWriter;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseExportService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.entity.Record;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.dao.util.cri.SqlExpressionGroup;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.Strings;
|
||||
import org.nutz.lang.random.R;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* @ClassName UnionReimburseMineController
|
||||
@@ -73,11 +50,11 @@ import java.util.HashMap;
|
||||
public class UnionReimburseMineController {
|
||||
|
||||
|
||||
@Inject
|
||||
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
|
||||
@Inject
|
||||
private UnionReimburseService unionReimburseService;
|
||||
@Inject
|
||||
private UnionReimburseExportService unionReimburseExportService;
|
||||
@Inject
|
||||
private FlowEngine flowEngine;
|
||||
@Inject
|
||||
private Dao dao;
|
||||
@@ -206,258 +183,17 @@ public class UnionReimburseMineController {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param id 当前登录人的已提交报销单ID
|
||||
* @param response 返回PDF附件;失败由统一异常处理返回业务提示
|
||||
*/
|
||||
@At
|
||||
@Ok("void")
|
||||
public void ActExport(String id, HttpServletResponse response, boolean Print) throws Exception {
|
||||
UnionReimburse unionReimburse = unionReimburseService.fetch(id);
|
||||
if (unionReimburse == null) {
|
||||
throw new RuntimeException("报销记录不存在");
|
||||
}
|
||||
HashMap<String, Object> docData = new HashMap<>();
|
||||
|
||||
QRCodeWriter qrCodeWriter = new QRCodeWriter();
|
||||
BitMatrix bitMatrix = qrCodeWriter.encode(unionReimburse.getDocumentNo(), BarcodeFormat.QR_CODE, 80, 80);
|
||||
BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
|
||||
File tempFile = File.createTempFile("qrcode_", ".png");
|
||||
javax.imageio.ImageIO.write(bufferedImage, "PNG", tempFile);
|
||||
PictureRenderData pictureRenderData = com.deepoove.poi.data.Pictures.of(tempFile.getAbsolutePath())
|
||||
.size(80, 80)
|
||||
.create();
|
||||
docData.put("qrcode", pictureRenderData);
|
||||
|
||||
tempFile.deleteOnExit();
|
||||
|
||||
docData.put("createTime", DateUtil.format(unionReimburse.getCreateTime(), "yyyy-MM-dd HH:mm:ss"));
|
||||
docData.put("unitName", unionReimburse.getUnitName());
|
||||
docData.put("userName", unionReimburse.getUserName());
|
||||
docData.put("loginName", unionReimburse.getLoginName());
|
||||
docData.put("mobile", unionReimburse.getMobile());
|
||||
docData.put("paymentNotes", unionReimburse.getPaymentNotes());
|
||||
docData.put("invoiceNumber", unionReimburse.getInvoiceNumber());
|
||||
docData.put("notes", unionReimburse.getNotes());
|
||||
docData.put("realMoney", unionReimburse.getRealMoney());
|
||||
docData.put("reimburseProject", unionReimburse.getReimburseProject());
|
||||
docData.put("paymentWay", unionReimburse.getPaymentWay());
|
||||
//对私支付,付款人信息
|
||||
docData.put("payerLoginname", unionReimburse.getPayerLoginname());
|
||||
docData.put("payerName", unionReimburse.getPayerName());
|
||||
docData.put("bankOfDeposit", unionReimburse.getBankOfDeposit());
|
||||
docData.put("bankCardNumber", unionReimburse.getBankCardNumber());
|
||||
|
||||
// 金额转大写
|
||||
if (unionReimburse.getRealMoney() != null) {
|
||||
docData.put("money_big", MoneyUtil.toRMBUpper(String.valueOf(unionReimburse.getRealMoney())));
|
||||
} else {
|
||||
docData.put("money_big", "");
|
||||
}
|
||||
|
||||
// 设置报销项目类型标记
|
||||
String wtAct = "□文体活动 □慰问 □日常活动 □专项活动";
|
||||
if ("UNION_REIMBURSE_PROJECT_2".equals(unionReimburse.getReimburseProject())) {
|
||||
wtAct = wtAct.replace("□文体活动", "√文体活动");
|
||||
} else if ("UNION_REIMBURSE_PROJECT_3".equals(unionReimburse.getReimburseProject())) {
|
||||
wtAct = wtAct.replace("□日常活动", "√日常活动");
|
||||
} else if ("UNION_REIMBURSE_PROJECT_4".equals(unionReimburse.getReimburseProject())) {
|
||||
wtAct = wtAct.replace("□专项活动", "√专项活动");
|
||||
} else if ("UNION_REIMBURSE_PROJECT_1".equals(unionReimburse.getReimburseProject())) {
|
||||
wtAct = wtAct.replace("□慰问", "√慰问");
|
||||
}
|
||||
docData.put("bxxm", wtAct);
|
||||
String templateName;
|
||||
Double realMoney = unionReimburse.getRealMoney() != null ? unionReimburse.getRealMoney() : 0.0;
|
||||
String paymentWay = unionReimburse.getPaymentWay();
|
||||
|
||||
if (("UNION_REIMBURSE_PAYMENT_WAY_2").equals(paymentWay)) { // 对私支付
|
||||
docData.put("paymentWay", "对私支付");
|
||||
if (realMoney < 5000) {
|
||||
templateName = "unionReimburse_act_private_small";
|
||||
} else {
|
||||
templateName = "unionReimburse_act_private_big";
|
||||
}
|
||||
} else { // 对公支付
|
||||
docData.put("paymentWay", "对公支付");
|
||||
if (realMoney < 5000) {
|
||||
templateName = "unionReimburse_act_public_small";
|
||||
} else {
|
||||
templateName = "unionReimburse_act_public_big";
|
||||
}
|
||||
}
|
||||
|
||||
if (Print) {
|
||||
exportAsPDF(templateName, docData, unionReimburse, response);
|
||||
} else {
|
||||
exportAsWord(templateName, docData, unionReimburse, response);
|
||||
}
|
||||
@ApiOperation("导出报销审批单PDF")
|
||||
@SaCheckPermission("unionReimburse.mine")
|
||||
public void exportPdf(@Param("id") String id, HttpServletResponse response) {
|
||||
byte[] pdf = unionReimburseExportService.exportPdf(id);
|
||||
CommonDownloadUtil.download("工会经费报销审批单_" + id + ".pdf", pdf, response);
|
||||
}
|
||||
|
||||
@At
|
||||
@Ok("void")
|
||||
public void ConExport(String id, HttpServletResponse response, boolean Print) throws Exception {
|
||||
UnionReimburse unionReimburse = unionReimburseService.fetch(id);
|
||||
if (unionReimburse == null) {
|
||||
throw new RuntimeException("报销记录不存在");
|
||||
}
|
||||
HashMap<String, Object> docData = new HashMap<>();
|
||||
|
||||
QRCodeWriter qrCodeWriter = new QRCodeWriter();
|
||||
BitMatrix bitMatrix = qrCodeWriter.encode(unionReimburse.getDocumentNo(), BarcodeFormat.QR_CODE, 80, 80);
|
||||
BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
|
||||
File tempFile = File.createTempFile("qrcode_", ".png");
|
||||
javax.imageio.ImageIO.write(bufferedImage, "PNG", tempFile);
|
||||
PictureRenderData pictureRenderData = com.deepoove.poi.data.Pictures.of(tempFile.getAbsolutePath())
|
||||
.size(80, 80)
|
||||
.create();
|
||||
docData.put("qrcode", pictureRenderData);
|
||||
|
||||
docData.put("createTime", DateUtil.format(unionReimburse.getCreateTime(), "yyyy-MM-dd HH:mm:ss"));
|
||||
docData.put("unitName", unionReimburse.getUnitName());
|
||||
docData.put("userName", unionReimburse.getUserName());
|
||||
docData.put("loginName", unionReimburse.getLoginName());
|
||||
docData.put("mobile", unionReimburse.getMobile());
|
||||
docData.put("paymentNotes", unionReimburse.getPaymentNotes());
|
||||
docData.put("invoiceNumber", unionReimburse.getInvoiceNumber());
|
||||
docData.put("notes", unionReimburse.getNotes());
|
||||
docData.put("realMoney", unionReimburse.getRealMoney());
|
||||
docData.put("reimburseProject", unionReimburse.getReimburseProject());
|
||||
docData.put("paymentWay", unionReimburse.getPaymentWay());
|
||||
//对私支付,付款人信息
|
||||
docData.put("payerLoginname", unionReimburse.getPayerLoginname());
|
||||
docData.put("payerName", unionReimburse.getPayerName());
|
||||
docData.put("bankOfDeposit", unionReimburse.getBankOfDeposit());
|
||||
docData.put("bankCardNumber", unionReimburse.getBankCardNumber());
|
||||
|
||||
//慰问
|
||||
docData.put("condolenceUserName", unionReimburse.getCondolenceUserName());
|
||||
docData.put("condolenceLoginName", unionReimburse.getCondolenceLoginName());
|
||||
docData.put("typeName", unionReimburse.getTypeName());
|
||||
docData.put("way", unionReimburse.getWay());
|
||||
docData.put("condolenceTime", DateUtil.format(unionReimburse.getCondolenceTime(), "yyyy-MM-dd"));
|
||||
docData.put("participants", unionReimburse.getParticipants());
|
||||
//结婚
|
||||
docData.put("marryTime", DateUtil.format(unionReimburse.getMarryTime(), "yyyy-MM-dd"));
|
||||
//生育
|
||||
docData.put("fertilityTime", DateUtil.format(unionReimburse.getFertilityTime(), "yyyy-MM-dd"));
|
||||
//生病住院
|
||||
docData.put("hospitalCausation", unionReimburse.getHospitalCausation());
|
||||
docData.put("hospital", unionReimburse.getHospital());
|
||||
docData.put("hospitalCount", unionReimburse.getHospitalCount());
|
||||
String hospitalTime = DateUtil.format(unionReimburse.getHospitalizationStartTime(), "yyyy-MM-dd") +
|
||||
" 至 " +
|
||||
DateUtil.format(unionReimburse.getHospitalizationEndTime(), "yyyy-MM-dd");
|
||||
docData.put("hospitalTime", hospitalTime);
|
||||
//去世
|
||||
docData.put("deathTime", DateUtil.format(unionReimburse.getDeathTime(), "yyyy-MM-dd"));
|
||||
docData.put("condolenceRelationship", unionReimburse.getCondolenceRelationship());
|
||||
|
||||
|
||||
// 金额转大写
|
||||
if (unionReimburse.getRealMoney() != null) {
|
||||
docData.put("money_big", MoneyUtil.toRMBUpper(String.valueOf(unionReimburse.getRealMoney())));
|
||||
} else {
|
||||
docData.put("money_big", "");
|
||||
}
|
||||
|
||||
// 设置报销项目类型标记
|
||||
String wtAct = "□文体活动 □慰问 □日常活动 □专项活动";
|
||||
if ("UNION_REIMBURSE_PROJECT_2".equals(unionReimburse.getReimburseProject())) {
|
||||
wtAct = wtAct.replace("□文体活动", "√文体活动");
|
||||
} else if ("UNION_REIMBURSE_PROJECT_3".equals(unionReimburse.getReimburseProject())) {
|
||||
wtAct = wtAct.replace("□日常活动", "√日常活动");
|
||||
} else if ("UNION_REIMBURSE_PROJECT_4".equals(unionReimburse.getReimburseProject())) {
|
||||
wtAct = wtAct.replace("□专项活动", "√专项活动");
|
||||
} else if ("UNION_REIMBURSE_PROJECT_1".equals(unionReimburse.getReimburseProject())) {
|
||||
wtAct = wtAct.replace("□慰问", "√慰问");
|
||||
}
|
||||
docData.put("bxxm", wtAct);
|
||||
String templateName;
|
||||
Double realMoney = unionReimburse.getRealMoney() != null ? unionReimburse.getRealMoney() : 0.0;
|
||||
String paymentWay = unionReimburse.getPaymentWay();
|
||||
|
||||
if (("UNION_REIMBURSE_PAYMENT_WAY_2").equals(paymentWay)) { // 对私支付
|
||||
docData.put("paymentWay", "对私支付");
|
||||
if (("生病住院").equals(unionReimburse.getTypeName())) {
|
||||
templateName = "unionReimburse_con_private_hospital";
|
||||
} else if (("结婚").equals(unionReimburse.getTypeName())) {
|
||||
templateName = "unionReimburse_con_private_mary";
|
||||
} else if (("生育").equals(unionReimburse.getTypeName())) {
|
||||
templateName = "unionReimburse_con_private_birth";
|
||||
} else if (("会员去世").equals(unionReimburse.getTypeName()) || ("直系亲属去世").equals(unionReimburse.getTypeName())) {
|
||||
if (realMoney < 5000) {
|
||||
templateName = "unionReimburse_con_private_dead_small";
|
||||
} else {
|
||||
templateName = "unionReimburse_con_private_dead_big";
|
||||
}
|
||||
} else {//其他
|
||||
templateName = "unionReimburse_con_private_other";
|
||||
}
|
||||
} else { // 对公支付
|
||||
docData.put("paymentWay", "对公支付");
|
||||
if (("生病住院").equals(unionReimburse.getTypeName())) {
|
||||
templateName = "unionReimburse_con_public_hospital";
|
||||
} else if (("结婚").equals(unionReimburse.getTypeName())) {
|
||||
templateName = "unionReimburse_con_public_mary";
|
||||
} else if (("生育").equals(unionReimburse.getTypeName())) {
|
||||
templateName = "unionReimburse_con_public_birth";
|
||||
} else if (("会员去世").equals(unionReimburse.getTypeName()) || ("直系亲属去世").equals(unionReimburse.getTypeName())) {
|
||||
if (realMoney < 5000) {
|
||||
templateName = "unionReimburse_con_public_dead_small";
|
||||
} else {
|
||||
templateName = "unionReimburse_con_public_dead_big";
|
||||
}
|
||||
} else {//其他
|
||||
templateName = "unionReimburse_con_public_other";
|
||||
}
|
||||
}
|
||||
|
||||
if (Print) {
|
||||
exportAsPDF(templateName, docData, unionReimburse, response);
|
||||
} else {
|
||||
exportAsWord(templateName, docData, unionReimburse, response);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出PDF
|
||||
@At
|
||||
private void exportAsPDF(String templateName, HashMap<String, Object> docData,
|
||||
UnionReimburse unionReimburse, HttpServletResponse response) throws Exception {
|
||||
String fileName = "中国地质大学(武汉)工会经费日常报销_" + unionReimburse.getUserName() + "_" +
|
||||
DateUtil.format(unionReimburse.getCreateTime(), "yyyyMMdd") + ".pdf";
|
||||
|
||||
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
|
||||
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(templateName))
|
||||
.render(docData)
|
||||
.writeAndClose(byteArrayOutputStream);
|
||||
byte[] bytes = byteArrayOutputStream.toByteArray();
|
||||
File sourceFile = File.createTempFile("file_convert_origin", ".docx");
|
||||
Files.write(sourceFile.toPath(), bytes, StandardOpenOption.WRITE);
|
||||
|
||||
File pdfFile = File.createTempFile("file_convert", ".pdf");
|
||||
OfficePlusUtil.convert(sourceFile.getPath(), pdfFile.getPath());
|
||||
CommonDownloadUtil.download(fileName, IoUtil.readBytes(FileUtil.getInputStream(pdfFile)), response);
|
||||
// 删除临时文件
|
||||
FileUtil.del(sourceFile);
|
||||
FileUtil.del(pdfFile.toPath());
|
||||
} catch (IOException e) {
|
||||
log.error("工会报销单据导出PDF失败,id:{},错误信息:{}", unionReimburse.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 导出Word
|
||||
@At
|
||||
private void exportAsWord(String templateName, HashMap<String, Object> docData,
|
||||
UnionReimburse unionReimburse, HttpServletResponse response) throws Exception {
|
||||
String fileName = "中国地质大学(武汉)工会经费日常报销_" + unionReimburse.getUserName() + "_" +
|
||||
DateUtil.format(unionReimburse.getCreateTime(), "yyyyMMdd") + ".docx";
|
||||
|
||||
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
|
||||
XWPFTemplate.compile(sysOfficeTemplateUtil.getTemplate(templateName))
|
||||
.render(docData)
|
||||
.writeAndClose(byteArrayOutputStream);
|
||||
CommonDownloadUtil.download(fileName, byteArrayOutputStream.toByteArray(), response);
|
||||
} catch (IOException e) {
|
||||
log.error("工会报销单据导出Word失败,id:{},错误信息:{}", unionReimburse.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -279,6 +279,9 @@ public class UnionReimburse extends BaseModel {
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
private String condolenceTypeId;
|
||||
|
||||
/** 慰问类型业务编码,仅供申请和详情回显控制字段显示,不映射数据库列。 */
|
||||
private String condolenceTypeCode;
|
||||
|
||||
@Column
|
||||
@Comment("慰问类型方式")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.budwk.app.zhgh.dayofficework.unionReimburse.model;
|
||||
|
||||
import com.budwk.app.base.model.BaseModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.nutz.dao.entity.annotation.*;
|
||||
import org.nutz.dao.interceptor.annotation.PrevInsert;
|
||||
|
||||
/** 付款人常用收款账户;报销单仍独立保留本次支付资料快照。 */
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Table("union_reimburse_bank_account")
|
||||
@TableMeta("{'mysql-charset':'utf8mb4'}")
|
||||
@TableIndexes(@Index(name = "uk_reimburse_bank_account", fields = {
|
||||
"payer", "bankUserName", "bankCardNumber", "bankOfDeposit"
|
||||
}, unique = true))
|
||||
public class UnionReimburseBankAccount extends BaseModel {
|
||||
@Name
|
||||
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||
@PrevInsert(uu32 = true)
|
||||
private String id;
|
||||
|
||||
@Column
|
||||
@Comment("付款人用户ID")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String payer;
|
||||
|
||||
@Column
|
||||
@Comment("户名")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String bankUserName;
|
||||
|
||||
@Column
|
||||
@Comment("银行卡号,保留字符串格式")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String bankCardNumber;
|
||||
|
||||
@Column
|
||||
@Comment("开户行")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String bankOfDeposit;
|
||||
}
|
||||
+15
@@ -44,11 +44,26 @@ public class UnionReimburseInvoiceDetail extends BaseModel {
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
private String invoiceNo;
|
||||
|
||||
@Column
|
||||
@Comment("开票日期,格式 yyyy-MM-dd")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 10)
|
||||
private String invoiceDate;
|
||||
|
||||
@Column
|
||||
@Comment("发票金额")
|
||||
@ColDefine(type = ColType.FLOAT, width = 10, precision = 2)
|
||||
private Double invoiceAmount;
|
||||
|
||||
@Column
|
||||
@Comment("税前金额")
|
||||
@ColDefine(type = ColType.FLOAT, width = 10, precision = 2)
|
||||
private Double amountWithoutTax;
|
||||
|
||||
@Column
|
||||
@Comment("合计税额")
|
||||
@ColDefine(type = ColType.FLOAT, width = 10, precision = 2)
|
||||
private Double taxAmount;
|
||||
|
||||
@Column
|
||||
@Comment("销售方信息名称")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 200)
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.budwk.app.zhgh.dayofficework.unionReimburse.service;
|
||||
|
||||
import com.budwk.app.base.service.BaseService;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburseBankAccount;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import java.util.List;
|
||||
|
||||
public interface UnionReimburseBankAccountService extends BaseService<UnionReimburseBankAccount> {
|
||||
/**
|
||||
* @param payer 付款人用户ID,非姓名或工号
|
||||
* @param reimburseFundSource 当前经费来源字典编码
|
||||
* @param clubId 协会经费对应的协会ID,其他来源可为空
|
||||
* @return 可选账户列表,每项包含 id、bankUserName、bankCardNumber、bankOfDeposit
|
||||
*/
|
||||
List<NutMap> listAccounts(String payer, String reimburseFundSource, String clubId);
|
||||
|
||||
/** @param form 已通过校验的报销单;提取全部对私支付明细,去重保存完整账户。 */
|
||||
void saveAccounts(UnionReimburse form);
|
||||
|
||||
/** @return 扫描的历史报销单数量;仅系统管理员可执行,可重复执行且不覆盖已有账户。 */
|
||||
int importHistory();
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
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,不接受其他人的单据或待提交单据
|
||||
* @return 完整PDF字节数组,模板或转换失败时抛出业务异常
|
||||
*/
|
||||
byte[] exportPdf(String id);
|
||||
|
||||
/**
|
||||
* 汇总页所有状态的报销单均可打印,必须具有汇总权限并符合所属工会数据范围。
|
||||
* @param id 汇总页选中的报销单 ID,可为待提交或其他申请状态
|
||||
* @return 与我的报销导出使用相同模板生成的完整 PDF 字节数组,无权限或生成失败时抛出业务异常
|
||||
*/
|
||||
byte[] exportCollectPdf(String id);
|
||||
|
||||
/**
|
||||
* @param form 已加载发票明细的报销单,支付明细为空时兼容旧主表字段
|
||||
* @return 两份模板通用的平铺字段及 payments、invoices 循环行
|
||||
*/
|
||||
Map<String, Object> buildExportData(UnionReimburse form);
|
||||
}
|
||||
+4
-1
@@ -50,7 +50,10 @@ public interface UnionReimburseService extends BaseService<UnionReimburse> {
|
||||
Result checkInvoiceDuplicate(String invoiceNo, String reimburseId);
|
||||
|
||||
/**
|
||||
* 识别单张发票文件,并在需要时调用验真接口补齐票面信息。
|
||||
* 识别单张发票文件并强制验真。
|
||||
* @param fileId 已上传发票的文件ID
|
||||
* @param verifySwitch 兼容旧页面参数,不再允许关闭验真
|
||||
* @return Result.data 包含票号、金额、销售方、项目名称及 verified=true;失败返回非零 code 和原因
|
||||
*/
|
||||
Result recognizeInvoice(String fileId, Boolean verifySwitch);
|
||||
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package com.budwk.app.zhgh.dayofficework.unionReimburse.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.sys.views.View_user;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
import com.budwk.app.zhgh.club.model.ClubUser;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburse;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.model.UnionReimburseBankAccount;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseBankAccountService;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
@IocBean(args = {"refer:dao"})
|
||||
public class UnionReimburseBankAccountServiceImpl extends BaseServiceImpl<UnionReimburseBankAccount>
|
||||
implements UnionReimburseBankAccountService {
|
||||
public UnionReimburseBankAccountServiceImpl(Dao dao) {
|
||||
super(dao);
|
||||
}
|
||||
|
||||
/** 按当前付款人候选范围校验用户ID,防止直接传入其他人员ID读取账户。 */
|
||||
@Override
|
||||
public List<NutMap> listAccounts(String payer, String reimburseFundSource, String clubId) {
|
||||
if (StrUtil.isBlank(payer)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
View_user user = dao().fetch(View_user.class, Cnd.where("id", "=", payer));
|
||||
if (user == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if ("UNION_REIMBURSE_FUND_SOURCE_3".equals(reimburseFundSource)) {
|
||||
if (StrUtil.isBlank(clubId) || dao().count(ClubUser.class,
|
||||
Cnd.where("clubId", "=", clubId).and("userId", "=", payer)) == 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
} else if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())
|
||||
&& (StrUtil.isBlank(SecurityUtil.getUnionId())
|
||||
|| !Objects.equals(user.getUnionId(), SecurityUtil.getUnionId()))) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Sql sql = Sqls.create("SELECT id, bankUserName, bankCardNumber, bankOfDeposit "
|
||||
+ "FROM union_reimburse_bank_account WHERE payer = @payer AND delFlag = 0 "
|
||||
+ "ORDER BY createdAt DESC, id DESC");
|
||||
sql.params().set("payer", payer);
|
||||
return listMap(sql);
|
||||
}
|
||||
|
||||
/** 保存全部对私明细,账户写入与调用方的报销保存共用事务。 */
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void saveAccounts(UnionReimburse form) {
|
||||
if (form == null) {
|
||||
return;
|
||||
}
|
||||
if (form.getPaymentDetails() != null && !form.getPaymentDetails().isEmpty()) {
|
||||
for (JSONObject detail : form.getPaymentDetails()) {
|
||||
if (detail != null && "UNION_REIMBURSE_PAYMENT_WAY_2".equals(detail.getStr("paymentWay"))) {
|
||||
saveAccount(detail.getStr("payer"), detail.getStr("bankUserName"),
|
||||
detail.getStr("bankCardNumber"), detail.getStr("bankOfDeposit"));
|
||||
}
|
||||
}
|
||||
} else if ("UNION_REIMBURSE_PAYMENT_WAY_2".equals(form.getPaymentWay())) {
|
||||
// 仅无支付明细的旧单据使用主表字段,避免导入已过时的第一条账户副本。
|
||||
saveAccount(form.getPayer(), form.getBankUserName(), form.getBankCardNumber(), form.getBankOfDeposit());
|
||||
}
|
||||
}
|
||||
|
||||
/** 过滤不完整历史资料;唯一索引配合原子插入,防止并发报销生成重复账户。 */
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
protected void saveAccount(String payer, String name, String card, String bank) {
|
||||
payer = StrUtil.trim(payer);
|
||||
name = StrUtil.trim(name);
|
||||
card = StrUtil.trim(card);
|
||||
bank = StrUtil.trim(bank);
|
||||
if (StrUtil.hasBlank(payer, name, card, bank) || payer.length() > 100 || name.length() > 100
|
||||
|| card.length() > 100 || bank.length() > 100 || !card.matches("[1-9]([0-9]{15}|[0-9]{18})")) {
|
||||
return;
|
||||
}
|
||||
Sql sql = Sqls.create("""
|
||||
INSERT INTO union_reimburse_bank_account
|
||||
(id, payer, bankUserName, bankCardNumber, bankOfDeposit,
|
||||
createdBy, createdAt, updatedBy, updatedAt, delFlag)
|
||||
VALUES (@id, @payer, @name, @card, @bank, @userId, @now, @userId, @now, 0)
|
||||
ON DUPLICATE KEY UPDATE id = id
|
||||
""");
|
||||
sql.params().set("id", UUID.randomUUID().toString().replace("-", ""));
|
||||
sql.params().set("payer", payer).set("name", name).set("card", card).set("bank", bank);
|
||||
sql.params().set("userId", SecurityUtil.getUserId()).set("now", System.currentTimeMillis());
|
||||
dao().execute(sql);
|
||||
}
|
||||
|
||||
/** 管理员一次性初始化;按ID分批读取,避免一次加载全部历史单据。 */
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public int importHistory() {
|
||||
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name())) {
|
||||
throw new BaseException("仅系统管理员可初始化历史收款账户");
|
||||
}
|
||||
int processed = 0;
|
||||
String lastId = "";
|
||||
while (true) {
|
||||
List<UnionReimburse> records = dao().query(UnionReimburse.class,
|
||||
Cnd.where("id", ">", lastId).and("delFlag", "=", false).asc("id"), dao().createPager(1, 200));
|
||||
if (records.isEmpty()) {
|
||||
return processed;
|
||||
}
|
||||
for (UnionReimburse record : records) {
|
||||
saveAccounts(record);
|
||||
}
|
||||
processed += records.size();
|
||||
lastId = records.get(records.size() - 1).getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
package com.budwk.app.zhgh.dayofficework.unionReimburse.service.impl;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import com.budwk.app.base.constant.RoleConstant;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.base.utils.MoneyUtil;
|
||||
import com.budwk.app.base.utils.OfficePlusUtil;
|
||||
import com.budwk.app.base.utils.SysOfficeTemplateUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
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.deepoove.poi.XWPFTemplate;
|
||||
import com.deepoove.poi.config.Configure;
|
||||
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
|
||||
import com.deepoove.poi.data.PictureType;
|
||||
import com.deepoove.poi.data.Pictures;
|
||||
import com.deepoove.poi.template.ElementTemplate;
|
||||
import com.google.zxing.*;
|
||||
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
|
||||
import com.google.zxing.client.j2se.MatrixToImageWriter;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
import com.google.zxing.qrcode.QRCodeReader;
|
||||
import org.apache.poi.xwpf.usermodel.*;
|
||||
import org.apache.poi.util.Units;
|
||||
import javax.imageio.ImageIO;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@IocBean
|
||||
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";
|
||||
@Inject
|
||||
private UnionReimburseService unionReimburseService;
|
||||
@Inject
|
||||
private SysOfficeTemplateUtil sysOfficeTemplateUtil;
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
/** 当前“我的报销”导出仅允许单据经办人,状态白名单与页面按钮保持一致。 */
|
||||
@Override
|
||||
public byte[] exportPdf(String id) {
|
||||
return renderDocument(loadExportForm(id, false, false), true, false);
|
||||
}
|
||||
|
||||
/** 汇总打印支持所有状态,并严格按汇总权限和所属工会范围读取,复用我的报销 PDF 模板。 */
|
||||
@Override
|
||||
public byte[] exportCollectPdf(String id) {
|
||||
return renderDocument(loadExportForm(id, true, true), true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 只读取申请与发票明细,不修改业务数据。
|
||||
* @param id 报销单 ID,空值、记录不存在或已删除时拒绝导出
|
||||
* @param allowCollect 是否允许具有汇总数据权限的用户读取他人单据
|
||||
* @param collectPrint 是否为汇总页全状态打印;此时必须通过汇总数据权限校验,经办人身份不能绕过工会范围
|
||||
* @return 已校验权限并补齐发票明细的报销单,原有导出入口仍只允许已提交状态
|
||||
*/
|
||||
private UnionReimburse loadExportForm(String id, boolean allowCollect, boolean collectPrint) {
|
||||
if (StrUtil.isBlank(id)) {
|
||||
throw new BaseException("请选择需要导出的报销单");
|
||||
}
|
||||
UnionReimburse form = unionReimburseService.fetch(id);
|
||||
if (form == null || Boolean.TRUE.equals(form.getDelFlag())) {
|
||||
throw new BaseException("报销记录不存在");
|
||||
}
|
||||
String userId = SecurityUtil.getUserId();
|
||||
boolean owner = StrUtil.isNotBlank(userId) && Objects.equals(form.getUserId(), userId);
|
||||
boolean collectAllowed = allowCollect && StrUtil.isNotBlank(userId)
|
||||
&& AuthUtil.hasPermission("unionReimburse.collect")
|
||||
&& (AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())
|
||||
|| (StrUtil.isNotBlank(SecurityUtil.getUnionId())
|
||||
&& Objects.equals(form.getUnionId(), SecurityUtil.getUnionId())));
|
||||
if (collectPrint ? !collectAllowed : (!owner && !collectAllowed)) {
|
||||
throw new BaseException("无权导出此报销单");
|
||||
}
|
||||
// 汇总页打印不限制申请状态,我的报销导出仍只允许已提交状态。
|
||||
if (!collectPrint && !Set.of(2, 3, 4, 5).contains(form.getStateId())) {
|
||||
throw new BaseException("报销单提交后才能导出");
|
||||
}
|
||||
// 已核对导出数据权限,单独补齐发票明细,避免申请页权限误拦截汇总打印。
|
||||
form.setInvoiceDetails(dao.query(UnionReimburseInvoiceDetail.class,
|
||||
Cnd.where("reimburseId", "=", id).asc("createdAt")));
|
||||
return form;
|
||||
}
|
||||
|
||||
/** 两份模板共用字段和循环行配置;collectPrint 仅为汇总打印补充编号二维码。 */
|
||||
private byte[] renderDocument(UnionReimburse form, boolean asPdf, boolean collectPrint) {
|
||||
String templateCode = "UNION_REIMBURSE_PROJECT_1".equals(form.getReimburseProject())
|
||||
? CONDOLENCE_TEMPLATE : ACTIVITY_TEMPLATE;
|
||||
Path docx = null;
|
||||
Path pdf = null;
|
||||
try {
|
||||
docx = Files.createTempFile("union_reimburse_", ".docx");
|
||||
if (asPdf) pdf = Files.createTempFile("union_reimburse_", ".pdf");
|
||||
// 活动和慰问模板的支付标记均位于表头,支付明细统一循环下一行。
|
||||
// 活动模板的发票标记也位于表头,循环下一行;慰问保留原有的当前行配置。
|
||||
boolean invoiceOnSameRow = CONDOLENCE_TEMPLATE.equals(templateCode);
|
||||
Configure config = Configure.builder()
|
||||
.bind("payments", new LoopRowTableRenderPolicy(false))
|
||||
.bind("invoices", new LoopRowTableRenderPolicy(invoiceOnSameRow)).build();
|
||||
try (InputStream input = sysOfficeTemplateUtil.getTemplate(templateCode);
|
||||
XWPFTemplate template = XWPFTemplate.compile(input, config);
|
||||
OutputStream output = Files.newOutputStream(docx)) {
|
||||
Map<String, Object> data = buildExportData(form);
|
||||
if (collectPrint) {
|
||||
prepareCollectQrCode(template, data, form.getDocumentNo());
|
||||
}
|
||||
template.render(data).write(output);
|
||||
}
|
||||
if (!asPdf) {
|
||||
return Files.readAllBytes(docx);
|
||||
}
|
||||
OfficePlusUtil.convert(docx.toString(), pdf.toString());
|
||||
byte[] bytes = Files.readAllBytes(pdf);
|
||||
// 公共转换器可能记录异常后返回,下载前验证结果,避免把空文件当成功响应。
|
||||
if (bytes.length < 5 || bytes[0] != '%' || bytes[1] != 'P' || bytes[2] != 'D'
|
||||
|| bytes[3] != 'F' || bytes[4] != '-') {
|
||||
throw new BaseException("PDF生成失败,请检查模板及转换服务");
|
||||
}
|
||||
return bytes;
|
||||
} catch (BaseException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("报销导出失败,单据ID:{},模板:{}", form.getId(), templateCode, e);
|
||||
throw new BaseException("报销导出失败,请检查模板配置");
|
||||
} finally {
|
||||
// 成功或异常均清理本次转换文件,不影响其他并发导出。
|
||||
for (Path path : new Path[]{docx, pdf}) {
|
||||
if (path != null) {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (Exception e) {
|
||||
log.warn("报销导出临时文件清理失败:{}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 汇总打印沿用老系统的单据编号二维码,仅修改本次生成的文档,不覆盖模板。
|
||||
* @param template 当前已编译模板,优先使用 qrcode 图片标记,其次替换标题区的固定二维码
|
||||
* @param data 本次渲染数据,qrcode 字段为 80×80 的 PNG 图片对象
|
||||
* @param documentNo 原始单据编号,缺失时拒绝打印,避免用业务 ID 冒充编号
|
||||
*/
|
||||
private void prepareCollectQrCode(XWPFTemplate template, Map<String, Object> data,
|
||||
String documentNo) throws Exception {
|
||||
if (StrUtil.isBlank(documentNo)) {
|
||||
throw new BaseException("报销单编号为空,无法生成打印二维码,请先核对单据编号");
|
||||
}
|
||||
var matrix = new QRCodeWriter().encode(documentNo, BarcodeFormat.QR_CODE, 80, 80);
|
||||
var image = MatrixToImageWriter.toBufferedImage(matrix);
|
||||
data.put("qrcode", Pictures.ofBufferedImage(image, PictureType.PNG).size(80, 80).create());
|
||||
boolean hasTag = template.getElementTemplates().stream()
|
||||
.anyMatch(item -> item instanceof ElementTemplate
|
||||
&& "qrcode".equals(((ElementTemplate) item).getTagName()));
|
||||
if (hasTag) {
|
||||
return;
|
||||
}
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "png", bytes);
|
||||
XWPFDocument document = template.getXWPFDocument();
|
||||
// 部分现用模板内嵌了固定二维码。仅识别第一张业务表格之前的二维码,保留徽标等普通图片。
|
||||
boolean replaced = false;
|
||||
for (IBodyElement element : document.getBodyElements()) {
|
||||
if (element instanceof XWPFTable) break;
|
||||
if (!(element instanceof XWPFParagraph)) continue;
|
||||
for (XWPFRun run : ((XWPFParagraph) element).getRuns()) {
|
||||
for (XWPFPicture picture : run.getEmbeddedPictures()) {
|
||||
if (picture.getPictureData() == null) continue;
|
||||
var existing = ImageIO.read(new ByteArrayInputStream(picture.getPictureData().getData()));
|
||||
if (existing == null) continue;
|
||||
try {
|
||||
new QRCodeReader().decode(new BinaryBitmap(new HybridBinarizer(
|
||||
new BufferedImageLuminanceSource(existing))));
|
||||
} catch (NotFoundException | ChecksumException | FormatException e) {
|
||||
continue;
|
||||
}
|
||||
String relationship = document.addPictureData(bytes.toByteArray(), Document.PICTURE_TYPE_PNG);
|
||||
picture.getCTPicture().getBlipFill().getBlip().setEmbed(relationship);
|
||||
replaced = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (replaced) return;
|
||||
// 无二维码标记或固定二维码的模板,在正文开头添加右对齐图片,不依赖模板重新上传。
|
||||
XWPFParagraph paragraph;
|
||||
if (document.getBodyElements().isEmpty()) {
|
||||
paragraph = document.createParagraph();
|
||||
} else {
|
||||
IBodyElement first = document.getBodyElements().get(0);
|
||||
var cursor = first instanceof XWPFParagraph
|
||||
? ((XWPFParagraph) first).getCTP().newCursor()
|
||||
: ((XWPFTable) first).getCTTbl().newCursor();
|
||||
try {
|
||||
paragraph = document.insertNewParagraph(cursor);
|
||||
} finally {
|
||||
cursor.dispose();
|
||||
}
|
||||
}
|
||||
paragraph.setAlignment(ParagraphAlignment.RIGHT);
|
||||
paragraph.setSpacingBefore(0);
|
||||
paragraph.setSpacingAfter(0);
|
||||
paragraph.createRun().addPicture(new ByteArrayInputStream(bytes.toByteArray()),
|
||||
Document.PICTURE_TYPE_PNG, "qrcode.png", Units.pixelToEMU(80), Units.pixelToEMU(80));
|
||||
}
|
||||
|
||||
/** 平铺字段供两类模板复用,支付总额与发票总额分别计算,不使用第一条支付代替全部明细。 */
|
||||
@Override
|
||||
public Map<String, Object> buildExportData(UnionReimburse form) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("date", date(form.getCreateTime()));
|
||||
data.put("no", text(form.getDocumentNo()));
|
||||
String unit = text(form.getUnitName());
|
||||
data.put("union", text(form.getUnionName()) + (unit.isEmpty() ? "" : "(" + unit + ")"));
|
||||
data.put("handler", text(form.getUserName()));
|
||||
data.put("login", text(form.getLoginName()));
|
||||
data.put("phone", text(form.getMobile()));
|
||||
String project = form.getReimburseProject();
|
||||
data.put("project", ("UNION_REIMBURSE_PROJECT_2".equals(project) ? "√" : "□") + "文体活动 "
|
||||
+ ("UNION_REIMBURSE_PROJECT_1".equals(project) ? "√" : "□") + "慰问 "
|
||||
+ ("UNION_REIMBURSE_PROJECT_3".equals(project) ? "√" : "□") + "日常活动 "
|
||||
+ ("UNION_REIMBURSE_PROJECT_4".equals(project) ? "√" : "□") + "专项活动");
|
||||
data.put("activity", text(form.getActivityName()));
|
||||
data.put("reason", text(form.getPaymentNotes()));
|
||||
data.put("notes", text(form.getNotes()));
|
||||
// 附件只有上传文件数量,没有纸质页数,模板明确标注为“附件份数”。
|
||||
data.put("attachments", form.getFiles() == null ? 0 : form.getFiles().size());
|
||||
List<JSONObject> details = form.getPaymentDetails();
|
||||
boolean legacy = details == null || details.isEmpty();
|
||||
if (legacy) {
|
||||
JSONObject item = new JSONObject();
|
||||
item.set("paymentWay", form.getPaymentWay());
|
||||
item.set("payerLoginname", form.getPayerLoginname());
|
||||
item.set("payerName", form.getPayerName());
|
||||
item.set("bankUserName", form.getBankUserName());
|
||||
item.set("bankCardNumber", form.getBankCardNumber());
|
||||
item.set("bankOfDeposit", form.getBankOfDeposit());
|
||||
// 历史记录未保存支付明细时,沿用实际金额、申请金额的优先级;慰问单再回退到慰问金额。
|
||||
// 仅空值触发回退,已明确填写的零金额不能被其他字段覆盖。
|
||||
Double legacyMoney = form.getRealMoney() != null ? form.getRealMoney() : form.getMoney();
|
||||
if (legacyMoney == null && "UNION_REIMBURSE_PROJECT_1".equals(project)) {
|
||||
legacyMoney = form.getCondolenceMoney();
|
||||
}
|
||||
item.set("money", legacyMoney);
|
||||
details = List.of(item);
|
||||
}
|
||||
List<Map<String, Object>> payments = new ArrayList<>();
|
||||
BigDecimal paymentTotal = BigDecimal.ZERO;
|
||||
for (JSONObject item : details) {
|
||||
if (item == null) continue;
|
||||
boolean privatePayment = "UNION_REIMBURSE_PAYMENT_WAY_2".equals(item.getStr("paymentWay"));
|
||||
BigDecimal amount = money(item.get("money"));
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("way", privatePayment ? "对私支付" : "对公支付");
|
||||
row.put("login", privatePayment ? text(item.getStr("payerLoginname")) : "");
|
||||
row.put("name", privatePayment ? text(item.getStr("payerName")) : "");
|
||||
// 开户行仅输出银行名称,不拼接户名或文字前缀;对公支付保持空白。
|
||||
row.put("bank", privatePayment ? text(item.getStr("bankOfDeposit")) : "");
|
||||
row.put("card", privatePayment ? text(item.getStr("bankCardNumber")) : "");
|
||||
row.put("amount", amount.toPlainString());
|
||||
payments.add(row);
|
||||
paymentTotal = paymentTotal.add(amount);
|
||||
}
|
||||
data.put("payments", payments);
|
||||
// 两类模板共用支付合计 total 与大写 upper,单条支付使用 payments.amount。
|
||||
data.put("total", paymentTotal.setScale(2).toPlainString());
|
||||
data.put("upper", upper(paymentTotal));
|
||||
List<Map<String, Object>> invoices = new ArrayList<>();
|
||||
BigDecimal invoiceTotal = BigDecimal.ZERO;
|
||||
if (form.getInvoiceDetails() != null) {
|
||||
for (UnionReimburseInvoiceDetail item : form.getInvoiceDetails()) {
|
||||
if (item == null) continue;
|
||||
BigDecimal amount = money(item.getInvoiceAmount());
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("seq", invoices.size() + 1);
|
||||
row.put("no", text(item.getInvoiceNo()));
|
||||
row.put("seller", text(item.getSellerName()));
|
||||
// 发票行分别绑定开票日期、税前金额、税额及价税合计;历史空值保持空白。
|
||||
row.put("date", text(item.getInvoiceDate()));
|
||||
row.put("amount", item.getAmountWithoutTax() == null ? "" : money(item.getAmountWithoutTax()).toPlainString());
|
||||
row.put("tax", item.getTaxAmount() == null ? "" : money(item.getTaxAmount()).toPlainString());
|
||||
row.put("total", amount.toPlainString());
|
||||
invoices.add(row);
|
||||
invoiceTotal = invoiceTotal.add(amount);
|
||||
}
|
||||
}
|
||||
data.put("invoices", invoices);
|
||||
data.put("invoiceTotal", invoiceTotal.setScale(2).toPlainString());
|
||||
data.put("invoiceCount", invoices.isEmpty() ? text(form.getInvoiceNumber()) : invoices.size());
|
||||
// 验真状态没有单独落库,导出模板不可将其固定写成“已验真”。
|
||||
data.put("person", text(form.getCondolenceUserName()));
|
||||
data.put("personNo", text(form.getCondolenceLoginName()));
|
||||
data.put("type", text(form.getTypeName()));
|
||||
data.put("way", text(form.getWay()));
|
||||
data.put("visitDate", date(form.getCondolenceTime()));
|
||||
data.put("participants", text(form.getParticipants()));
|
||||
data.put("illness", text(form.getHospitalCausation()));
|
||||
data.put("hospital", text(form.getHospital()));
|
||||
data.put("times", text(form.getHospitalCount()));
|
||||
String start = date(form.getHospitalizationStartTime());
|
||||
String end = date(form.getHospitalizationEndTime());
|
||||
data.put("hospitalDates", start + (start.isEmpty() || end.isEmpty() ? "" : " 至 ") + end);
|
||||
data.put("birthDate", date(form.getFertilityTime()));
|
||||
data.put("marryDate", date(form.getMarryTime()));
|
||||
data.put("deathDate", date(form.getDeathTime()));
|
||||
data.put("relation", text(form.getCondolenceRelationship()));
|
||||
return data;
|
||||
}
|
||||
|
||||
/** 金额统一为两位小数,空值显示0.00,非法历史金额拒绝生成错误单据。 */
|
||||
private BigDecimal money(Object value) {
|
||||
if (value == null || StrUtil.isBlank(value.toString())) return BigDecimal.ZERO.setScale(2);
|
||||
try {
|
||||
BigDecimal result = new BigDecimal(value.toString()).setScale(2, RoundingMode.HALF_UP);
|
||||
if (result.signum() < 0) throw new NumberFormatException();
|
||||
return result;
|
||||
} catch (NumberFormatException e) {
|
||||
throw new BaseException("报销记录金额异常,请核对后导出");
|
||||
}
|
||||
}
|
||||
|
||||
/** 复用项目人民币大写转换,将转换异常统一为导出业务提示。 */
|
||||
private String upper(BigDecimal value) {
|
||||
try {
|
||||
return MoneyUtil.toRMBUpper(value.toPlainString());
|
||||
} catch (Exception e) {
|
||||
throw new BaseException("报销金额大写转换失败,请核对金额");
|
||||
}
|
||||
}
|
||||
|
||||
/** 空字段在审批单中显示为空白。 */
|
||||
private String text(String value) { return StrUtil.nullToEmpty(value); }
|
||||
|
||||
/** 业务日期只显示年月日,未填写的日期保持空白。 */
|
||||
private String date(Date value) { return value == null ? "" : DateUtil.format(value, "yyyy-MM-dd"); }
|
||||
}
|
||||
+303
-73
@@ -16,10 +16,12 @@ import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayMan
|
||||
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.model.OutlayUseDetail;
|
||||
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.school.model.OutlayManageSchool;
|
||||
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.union.model.OutlayManageUnion;
|
||||
import com.budwk.app.zhgh.staffbenefit.condolence.model.CondolenceType;
|
||||
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.param.UnionReimbursePageForm;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseService;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.service.UnionReimburseBankAccountService;
|
||||
import com.budwk.app.zhgh.dayofficework.unionReimburse.vo.UnionReimburseBankHistoryVO;
|
||||
import com.budwk.app.web.commons.auth.utils.AuthUtil;
|
||||
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
|
||||
@@ -47,8 +49,8 @@ import com.tencentcloudapi.ocr.v20181119.models.VatInvoice;
|
||||
import com.tencentcloudapi.ocr.v20181119.models.VatInvoiceItem;
|
||||
import com.tencentcloudapi.ocr.v20181119.models.VatInvoiceOCRRequest;
|
||||
import com.tencentcloudapi.ocr.v20181119.models.VatInvoiceOCRResponse;
|
||||
import com.tencentcloudapi.ocr.v20181119.models.VatInvoiceVerifyRequest;
|
||||
import com.tencentcloudapi.ocr.v20181119.models.VatInvoiceVerifyResponse;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
@@ -80,6 +82,8 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
@Inject
|
||||
private SysFileService sysFileService;
|
||||
@Inject
|
||||
private UnionReimburseBankAccountService unionReimburseBankAccountService;
|
||||
@Inject
|
||||
private PropertiesProxy conf;
|
||||
public UnionReimburseServiceImpl(Dao dao) {
|
||||
super(dao);
|
||||
@@ -121,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
|
||||
@@ -130,10 +134,20 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
if (unionReimburse == null) {
|
||||
return Result.error("报销数据不能为空");
|
||||
}
|
||||
// 保存、提交及重提统一要求发票完整,不能通过慰问类型或草稿状态跳过。
|
||||
Result invoiceResult = validateInvoiceDetails(unionReimburse);
|
||||
if (invoiceResult != null) {
|
||||
return invoiceResult;
|
||||
}
|
||||
Result paymentResult = validatePaymentDetails(unionReimburse);
|
||||
if (paymentResult != null) {
|
||||
return paymentResult;
|
||||
}
|
||||
// 保存、提交及重提均在写入前检查支付总额,不能只依赖页面的金额限制。
|
||||
Result amountResult = validatePaymentAmountLimit(unionReimburse);
|
||||
if (amountResult != null) {
|
||||
return amountResult;
|
||||
}
|
||||
UnionReimburse oldRecord = null;
|
||||
if (StrUtil.isNotBlank(unionReimburse.getId())) {
|
||||
oldRecord = this.fetch(unionReimburse.getId());
|
||||
@@ -174,11 +188,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
}
|
||||
|
||||
normalizeInvoiceDetails(unionReimburse);
|
||||
if (needInvoiceDetails(unionReimburse)) {
|
||||
fillInvoiceSummary(unionReimburse);
|
||||
} else {
|
||||
fillCondolenceInvoiceSummary(unionReimburse);
|
||||
}
|
||||
fillInvoiceSummary(unionReimburse);
|
||||
fillDefaultRealMoney(unionReimburse);
|
||||
if (stateId == 2) {
|
||||
Result validateResult = validateBeforeSubmit(unionReimburse);
|
||||
@@ -195,6 +205,11 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
}
|
||||
}
|
||||
|
||||
// 校验实际文件与票面内容后才允许写入,不接收前端的验真状态作为凭据。
|
||||
Result verifyResult = validateInvoiceAuthenticity(unionReimburse);
|
||||
if (verifyResult != null) {
|
||||
return verifyResult;
|
||||
}
|
||||
if (oldRecord == null) {
|
||||
unionReimburse.setCreateTime(toSecondPrecision(DateUtil.date()));
|
||||
unionReimburse.setDocumentNo(generateDocumentNo());
|
||||
@@ -206,6 +221,8 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
fillDefaultRealMoney(unionReimburse);
|
||||
this.dao().insertOrUpdate(unionReimburse);
|
||||
saveInvoiceDetails(unionReimburse.getId(), unionReimburse.getInvoiceDetails());
|
||||
// 报销保存成功后积累每条对私账户;失败时随本次报销事务一起回滚。
|
||||
unionReimburseBankAccountService.saveAccounts(unionReimburse);
|
||||
return Result.success(stateId == 2 ? "提交成功" : "保存成功");
|
||||
}
|
||||
|
||||
@@ -219,6 +236,10 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
return new Date((date.getTime() / 1000) * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id 报销申请 ID,用于读取申请及关联的慰问类型
|
||||
* @return 申请对象,condolenceTypeCode 为类型业务编码;未找到申请返回 null,类型缺失时编码为空
|
||||
*/
|
||||
@Override
|
||||
public UnionReimburse getApplyForm(String id) {
|
||||
UnionReimburse unionReimburse = this.fetch(id);
|
||||
@@ -228,6 +249,10 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
if (!canReadApply(unionReimburse)) {
|
||||
throw new BaseException("无权查看此报销申请");
|
||||
}
|
||||
// 兼容旧申请将类型 ID 存在 condolenceType 中的情况;只读取编码,不改动历史关联。
|
||||
String typeId = StrUtil.blankToDefault(unionReimburse.getCondolenceTypeId(), unionReimburse.getCondolenceType());
|
||||
CondolenceType type = StrUtil.isBlank(typeId) ? null : dao().fetch(CondolenceType.class, typeId);
|
||||
unionReimburse.setCondolenceTypeCode(type == null ? null : type.getCode());
|
||||
List<UnionReimburseInvoiceDetail> invoiceDetails = this.dao().query(
|
||||
UnionReimburseInvoiceDetail.class,
|
||||
Cnd.where("reimburseId", "=", id).asc("createdAt")
|
||||
@@ -416,6 +441,11 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
return queryHistoryInvoiceDuplicate(currentInvoiceNo, reimburseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fileId 已上传发票的文件ID,只读取服务端存储的文件
|
||||
* @param verifySwitch 兼容旧页面参数,传 false 或不传也必须验真
|
||||
* @return Result.data 为票号、金额、销售方、项目等字段及 verified=true;识别或验真失败返回非零 code
|
||||
*/
|
||||
@Override
|
||||
public Result recognizeInvoice(String fileId, Boolean verifySwitch) {
|
||||
if (StrUtil.isBlank(fileId)) {
|
||||
@@ -431,14 +461,22 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
}
|
||||
try {
|
||||
byte[] fileBytes = sysFileService.download(fileId);
|
||||
// OCR当前仅读取PDF第一页,拒绝多页文件,避免后续页未核验却随整份附件入库。
|
||||
if (StrUtil.blankToDefault(sysFile.getName(), "").toLowerCase(java.util.Locale.ROOT).endsWith(".pdf")) {
|
||||
try (org.apache.pdfbox.pdmodel.PDDocument document = org.apache.pdfbox.Loader.loadPDF(fileBytes)) {
|
||||
if (document.getNumberOfPages() != 1) {
|
||||
return Result.error("请将发票拆分为单页PDF后逐张上传验真");
|
||||
}
|
||||
}
|
||||
}
|
||||
VatInvoiceOCRResponse response = executeOcr(credential, sysFile, fileBytes);
|
||||
NutMap result = buildOcrResult(response);
|
||||
String msg = "发票识别成功";
|
||||
if (Boolean.TRUE.equals(verifySwitch)) {
|
||||
String verifyMsg = enrichByVerify(credential, result);
|
||||
msg = StrUtil.isBlank(verifyMsg) ? "发票识别并校验成功" : verifyMsg;
|
||||
String verifyMsg = enrichByVerify(credential, result);
|
||||
if (StrUtil.isNotBlank(verifyMsg)) {
|
||||
return Result.error(verifyMsg);
|
||||
}
|
||||
return Result.success(msg, result);
|
||||
result.setv("verified", true);
|
||||
return Result.success("发票识别并验真成功", result);
|
||||
} catch (TencentCloudSDKException e) {
|
||||
log.error("腾讯云发票识别失败,fileId:{}", fileId, e);
|
||||
return Result.error("腾讯云发票识别失败:" + e.getMessage());
|
||||
@@ -631,7 +669,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
}
|
||||
|
||||
/**
|
||||
* 活动类报销由发票明细自动汇总金额和发票张数,减少手工输入出错。
|
||||
* 所有报销类型由必填发票明细自动汇总金额和张数。
|
||||
*/
|
||||
private void fillInvoiceSummary(UnionReimburse unionReimburse) {
|
||||
double totalMoney = 0D;
|
||||
@@ -650,29 +688,6 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
unionReimburse.setInvoiceNumber(String.valueOf(invoiceFileCount));
|
||||
}
|
||||
|
||||
/**
|
||||
* 慰问类报销允许不填发票;有发票时按发票汇总金额申请,没有发票时按慰问金额申请。
|
||||
*/
|
||||
private void fillCondolenceInvoiceSummary(UnionReimburse unionReimburse) {
|
||||
double totalMoney = 0D;
|
||||
int invoiceFileCount = 0;
|
||||
for (UnionReimburseInvoiceDetail detail : unionReimburse.getInvoiceDetails()) {
|
||||
if (detail.getInvoiceAmount() != null) {
|
||||
totalMoney = BigDecimal.valueOf(totalMoney)
|
||||
.add(BigDecimal.valueOf(detail.getInvoiceAmount()))
|
||||
.doubleValue();
|
||||
}
|
||||
if (Lang.isNotEmpty(detail.getInvoiceFiles())) {
|
||||
invoiceFileCount++;
|
||||
}
|
||||
}
|
||||
Double applyMoney = invoiceFileCount > 0
|
||||
? BigDecimal.valueOf(totalMoney).setScale(2, RoundingMode.HALF_UP).doubleValue()
|
||||
: unionReimburse.getCondolenceMoney();
|
||||
unionReimburse.setMoney(applyMoney);
|
||||
unionReimburse.setInvoiceNumber(String.valueOf(invoiceFileCount));
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认实际金额跟随申请金额,后续管理员可在汇总页按最终报销金额调整。
|
||||
*/
|
||||
@@ -718,6 +733,11 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
if (StrUtil.isBlank(detail.getStr("bankOfDeposit"))) {
|
||||
return Result.error(prefix + "请填写开户行");
|
||||
}
|
||||
// 账户表和报销快照的文本字段长度一致,超长输入应明确拒绝而非丢失账户。
|
||||
if (detail.getStr("payer").length() > 100 || detail.getStr("bankUserName").length() > 100
|
||||
|| detail.getStr("bankOfDeposit").length() > 100) {
|
||||
return Result.error(prefix + "付款人、户名及开户行不能超过100个字符");
|
||||
}
|
||||
String card = detail.getStr("bankCardNumber");
|
||||
if (card == null || !card.matches("[1-9]([0-9]{15}|[0-9]{18})")) {
|
||||
return Result.error(prefix + "请填写正确的银行卡号");
|
||||
@@ -727,6 +747,42 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付明细须先通过单行金额校验,再按分比较全部对公、对私金额与发票总额。
|
||||
* @param form 当前申请,使用 paymentDetails 和 invoiceDetails 重新汇总,不信任前端 money 字段
|
||||
* @return 超额或发票金额无效时返回 Result 错误,通过返回 null
|
||||
*/
|
||||
private Result validatePaymentAmountLimit(UnionReimburse form) {
|
||||
List<UnionReimburseInvoiceDetail> invoices = form.getInvoiceDetails();
|
||||
BigDecimal invoiceTotal = BigDecimal.ZERO.setScale(2);
|
||||
if (invoices != null) {
|
||||
for (int i = 0; i < invoices.size(); i++) {
|
||||
UnionReimburseInvoiceDetail detail = invoices.get(i);
|
||||
if (detail == null) {
|
||||
continue;
|
||||
}
|
||||
Double amount = detail.getInvoiceAmount();
|
||||
if (amount == null || !Double.isFinite(amount) || amount <= 0) {
|
||||
return Result.error("第" + (i + 1) + "条发票明细请填写正确的发票金额");
|
||||
}
|
||||
BigDecimal invoiceMoney = BigDecimal.valueOf(amount).setScale(2, RoundingMode.HALF_UP);
|
||||
if (invoiceMoney.signum() <= 0) {
|
||||
return Result.error("第" + (i + 1) + "条发票明细请填写正确的发票金额");
|
||||
}
|
||||
invoiceTotal = invoiceTotal.add(invoiceMoney);
|
||||
}
|
||||
}
|
||||
BigDecimal paymentTotal = BigDecimal.ZERO.setScale(2);
|
||||
for (cn.hutool.json.JSONObject detail : form.getPaymentDetails()) {
|
||||
paymentTotal = paymentTotal.add(new BigDecimal(detail.get("money").toString()));
|
||||
}
|
||||
if (paymentTotal.compareTo(invoiceTotal) > 0) {
|
||||
return Result.error("支付金额合计不能超过发票总额(支付合计:" + paymentTotal.toPlainString()
|
||||
+ "元,发票总额:" + invoiceTotal.toPlainString() + "元)");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交前补一层后端校验,防止前端绕过校验直接提交。
|
||||
*/
|
||||
@@ -747,7 +803,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
// return Result.error("请填写开户行");
|
||||
// }
|
||||
if (!needInvoiceDetails(unionReimburse)) {
|
||||
return validateInvoiceDetails(unionReimburse, false);
|
||||
return validateInvoiceDetails(unionReimburse);
|
||||
}
|
||||
if (StrUtil.isBlank(unionReimburse.getActivityName())) {
|
||||
return Result.error("请填写活动名称");
|
||||
@@ -761,29 +817,29 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
if (Lang.isEmpty(unionReimburse.getFiles())) {
|
||||
return Result.error("请上传附件");
|
||||
}
|
||||
return validateInvoiceDetails(unionReimburse, true);
|
||||
return validateInvoiceDetails(unionReimburse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发票明细提交校验:活动类必须至少一条,慰问类允许为空但填写后必须完整。
|
||||
* 保存及提交前检查发票完整性,每条明细必须且只能包含一张可定位的发票文件。
|
||||
*/
|
||||
private Result validateInvoiceDetails(UnionReimburse unionReimburse, boolean required) {
|
||||
private Result validateInvoiceDetails(UnionReimburse unionReimburse) {
|
||||
if (Lang.isEmpty(unionReimburse.getInvoiceDetails())) {
|
||||
if (!required) {
|
||||
return null;
|
||||
}
|
||||
return Result.error("请至少维护一条发票明细");
|
||||
}
|
||||
for (int i = 0; i < unionReimburse.getInvoiceDetails().size(); i++) {
|
||||
UnionReimburseInvoiceDetail detail = unionReimburse.getInvoiceDetails().get(i);
|
||||
int rowIndex = i + 1;
|
||||
if (Lang.isEmpty(detail.getInvoiceFiles())) {
|
||||
if (detail == null || Lang.isEmpty(detail.getInvoiceFiles())) {
|
||||
return Result.error("第" + rowIndex + "条发票明细请上传发票文件");
|
||||
}
|
||||
if (detail.getInvoiceFiles().size() != 1 || StrUtil.isBlank(getInvoiceFileId(detail.getInvoiceFiles().get(0)))) {
|
||||
return Result.error("第" + rowIndex + "条发票明细须上传一个有效发票文件");
|
||||
}
|
||||
if (StrUtil.isBlank(detail.getInvoiceNo())) {
|
||||
return Result.error("第" + rowIndex + "条发票明细请填写发票号码");
|
||||
}
|
||||
if (detail.getInvoiceAmount() == null || detail.getInvoiceAmount() <= 0) {
|
||||
if (detail.getInvoiceAmount() == null || !Double.isFinite(detail.getInvoiceAmount()) || detail.getInvoiceAmount() <= 0) {
|
||||
return Result.error("第" + rowIndex + "条发票明细请填写正确的发票金额");
|
||||
}
|
||||
if (StrUtil.isBlank(detail.getSellerName())) {
|
||||
@@ -796,6 +852,67 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param form 已通过完整性校验的申请,逐条使用附件ID读取真实文件并验真
|
||||
* @return 全部通过返回 null;失败返回包含明细行号的 Result,不写入任何报销数据
|
||||
*/
|
||||
private Result validateInvoiceAuthenticity(UnionReimburse form) {
|
||||
Set<String> invoiceNumbers = new LinkedHashSet<>();
|
||||
for (int i = 0; i < form.getInvoiceDetails().size(); i++) {
|
||||
UnionReimburseInvoiceDetail detail = form.getInvoiceDetails().get(i);
|
||||
String prefix = "第" + (i + 1) + "条发票明细:";
|
||||
if (!invoiceNumbers.add(detail.getInvoiceNo())) {
|
||||
return Result.error(prefix + "发票号码重复,不能保存或提交");
|
||||
}
|
||||
Result verified = recognizeInvoice(getInvoiceFileId(detail.getInvoiceFiles().get(0)), true);
|
||||
if (verified.getCode() != 0) {
|
||||
return Result.error(prefix + verified.getMsg());
|
||||
}
|
||||
if (!(verified.getData() instanceof NutMap data) || !Boolean.TRUE.equals(data.get("verified"))) {
|
||||
return Result.error(prefix + "未取得有效验真结果,请重试");
|
||||
}
|
||||
Double verifiedAmount = parseMoney(data.get("invoiceAmount"));
|
||||
if (!Objects.equals(detail.getInvoiceNo(), data.getString("invoiceNo"))
|
||||
|| verifiedAmount == null
|
||||
|| BigDecimal.valueOf(detail.getInvoiceAmount()).setScale(2, RoundingMode.HALF_UP)
|
||||
.compareTo(BigDecimal.valueOf(verifiedAmount).setScale(2, RoundingMode.HALF_UP)) != 0
|
||||
|| !Objects.equals(detail.getSellerName(), data.getString("sellerName"))
|
||||
|| !Objects.equals(detail.getItemName(), data.getString("itemName"))) {
|
||||
return Result.error(prefix + "票号、金额、销售方或项目名称与验真结果不一致,请重新识别后保存");
|
||||
}
|
||||
// 日期和分项金额均采用服务端重读发票后的结果,不信任前端传入值。
|
||||
detail.setInvoiceDate(data.getString("invoiceDate"));
|
||||
detail.setAmountWithoutTax(parseMoney(data.get("amountWithoutTax")));
|
||||
detail.setTaxAmount(parseMoney(data.get("taxAmount")));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 兼容上传组件完整对象和历史下载地址;只提取ID,绝不请求前端传入的URL。 */
|
||||
private String getInvoiceFileId(JSONObject file) {
|
||||
if (file == null) {
|
||||
return "";
|
||||
}
|
||||
if (StrUtil.isNotBlank(file.getStr("id"))) {
|
||||
return file.getStr("id").trim();
|
||||
}
|
||||
JSONObject response = file.getJSONObject("response");
|
||||
Object data = response == null ? null : response.get("data");
|
||||
if (data instanceof JSONObject object && StrUtil.isNotBlank(object.getStr("id"))) {
|
||||
return object.getStr("id").trim();
|
||||
}
|
||||
String value = data instanceof String ? (String) data
|
||||
: StrUtil.blankToDefault(file.getStr("downloadPath"), file.getStr("url"));
|
||||
if (StrUtil.isBlank(value)) {
|
||||
return "";
|
||||
}
|
||||
java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("[?&]id=([^&#]+)").matcher(value);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
return data instanceof String && value.matches("[a-zA-Z0-9_-]+") ? value : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交和审核前统一校验预算是否已分配、余额是否充足。
|
||||
*/
|
||||
@@ -1227,52 +1344,153 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
result.setv("invoiceCode", pickValue(infos, "发票代码", "代码", "Code"));
|
||||
result.setv("invoiceDate", normalizeDate(pickValue(infos, "开票日期", "日期", "Date")));
|
||||
result.setv("invoiceCheckCode", pickValue(infos, "校验码", "校验码后6位", "CheckCode"));
|
||||
result.setv("invoiceAmount", parseMoney(pickValue(infos, "小写金额", "价税合计(小写)", "价税合计", "合计金额", "AmountWithTax")));
|
||||
// 含税总额按字段优先级提取;“合计金额”通常是不含税金额,不能用于全电票核验。
|
||||
result.setv("invoiceAmount", parseMoney(pickPreferredValue(infos,
|
||||
"价税合计(小写)", "价税合计(小写)", "价税合计", "小写金额", "AmountWithTax")));
|
||||
result.setv("amountWithoutTax", parseMoney(pickValue(infos, "合计金额", "金额合计", "不含税金额", "AmountWithoutTax")));
|
||||
result.setv("taxAmount", parseMoney(pickPreferredValue(infos, "合计税额", "税额合计", "TaxAmount")));
|
||||
result.setv("sellerName", pickValue(infos, "销售方名称", "销售方信息名称", "销售方", "SellerName"));
|
||||
result.setv("itemName", joinItemNames(items));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动校验需要票面关键信息,缺字段时只提示人工核对,不阻塞用户继续录入。
|
||||
* 从OCR票面构造新版核验参数:全电票使用含税金额,普通票使用校验码,专票使用不含税金额。
|
||||
* @param result 服务端识别的票面数据,不接受前端手工填写的验真参数
|
||||
* @return 腾讯云请求参数;信息不足时抛出异常,由验真入口返回失败
|
||||
*/
|
||||
private String enrichByVerify(Credential credential, NutMap result) {
|
||||
private NutMap buildVerifyRequest(NutMap result) {
|
||||
String invoiceCode = result.getString("invoiceCode");
|
||||
String invoiceNo = result.getString("invoiceNo");
|
||||
String invoiceDate = result.getString("invoiceDate");
|
||||
String checkCode = lastDigits(result.getString("invoiceCheckCode"), 6);
|
||||
if (StrUtil.isBlank(checkCode)) {
|
||||
checkCode = moneyToVerifyValue(result.get("invoiceAmount"));
|
||||
boolean electronic = invoiceNo != null && invoiceNo.matches("[0-9]{20}");
|
||||
if (StrUtil.hasBlank(invoiceNo, invoiceDate) || (!electronic && StrUtil.isBlank(invoiceCode))) {
|
||||
throw new IllegalArgumentException("发票号码、代码或开票日期识别不完整,请上传清晰发票后重试");
|
||||
}
|
||||
if (StrUtil.hasBlank(invoiceCode, invoiceNo, invoiceDate, checkCode)) {
|
||||
return "发票识别成功,但自动校验信息不完整,请人工核对后提交";
|
||||
NutMap request = NutMap.NEW().setv("InvoiceNo", invoiceNo).setv("InvoiceDate", invoiceDate);
|
||||
if (!electronic) {
|
||||
request.setv("InvoiceCode", invoiceCode);
|
||||
}
|
||||
String amount = moneyToVerifyValue(result.get(electronic ? "invoiceAmount" : "amountWithoutTax"));
|
||||
if (electronic && StrUtil.isBlank(amount)
|
||||
|| !electronic && checkCode.length() != 6 && StrUtil.isBlank(amount)) {
|
||||
throw new IllegalArgumentException("发票校验码或核验金额识别不完整,请上传清晰发票后重试");
|
||||
}
|
||||
if (!electronic && checkCode.length() == 6) {
|
||||
request.setv("CheckCode", checkCode);
|
||||
}
|
||||
if (StrUtil.isNotBlank(amount)) {
|
||||
request.setv("Amount", amount);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
/** 强制验真:缺字段、无结果、作废红冲、票面不符及接口异常均不允许按成功返回。 */
|
||||
private String enrichByVerify(Credential credential, NutMap result) {
|
||||
try {
|
||||
NutMap request = buildVerifyRequest(result);
|
||||
OcrClient client = createOcrClient(credential);
|
||||
VatInvoiceVerifyRequest request = new VatInvoiceVerifyRequest();
|
||||
request.setInvoiceCode(invoiceCode);
|
||||
request.setInvoiceNo(invoiceNo);
|
||||
request.setInvoiceDate(invoiceDate.replace("-", ""));
|
||||
request.setAdditional(checkCode);
|
||||
VatInvoiceVerifyResponse response = client.VatInvoiceVerify(request);
|
||||
VatInvoice invoice = response.getInvoice();
|
||||
if (invoice == null) {
|
||||
return "发票识别成功,但自动校验未返回有效结果,请人工核对后提交";
|
||||
// 当前SDK没有新版模型,通过已有签名调用能力请求新版接口,避免升级整个SDK。
|
||||
JSONObject response = JSONUtil.parseObj(client.call("VatInvoiceVerifyNew", JSONUtil.toJsonStr(request)))
|
||||
.getJSONObject("Response");
|
||||
if (response == null || response.getJSONObject("Error") != null) {
|
||||
String code = response == null ? "EmptyResponse" : response.getJSONObject("Error").getStr("Code");
|
||||
return "发票验真未通过(" + code + "),请核对票据或稍后重试";
|
||||
}
|
||||
result.setv("invoiceCode", invoice.getCode());
|
||||
result.setv("invoiceNo", invoice.getNumber());
|
||||
result.setv("invoiceDate", normalizeDate(invoice.getDate()));
|
||||
result.setv("invoiceCheckCode", invoice.getCheckCode());
|
||||
result.setv("sellerName", invoice.getSellerName());
|
||||
result.setv("invoiceAmount", parseMoney(invoice.getAmountWithTax()));
|
||||
result.setv("itemName", joinItemNames(invoice.getItems()));
|
||||
return null;
|
||||
return applyVerifiedInvoice(result, response.getJSONObject("Invoice"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return e.getMessage();
|
||||
} catch (TencentCloudSDKException e) {
|
||||
String code = StrUtil.blankToDefault(e.getErrorCode(), "UnknownError");
|
||||
log.warn("腾讯云发票验真失败,invoiceNo:{},invoiceDate:{},amount:{},errorCode:{},requestId:{}",
|
||||
result.getString("invoiceNo"), result.getString("invoiceDate"),
|
||||
result.get("invoiceAmount"), code, e.getRequestId(), e);
|
||||
if ("FailedOperation.InvoiceMismatch".equals(code)
|
||||
|| "InvalidParameterValue.PriceOrVerificationParameterValueLimit".equals(code)) {
|
||||
return "发票验真数据不一致,请核对票号、开票日期及价税合计金额后重新上传";
|
||||
}
|
||||
if ("FailedOperation.CountLimitError".equals(code)) {
|
||||
return "该发票今日核验次数已达上限,请次日再试";
|
||||
}
|
||||
if ("ResourceNotFound.NotSupportCurrentInvoiceQuery".equals(code)) {
|
||||
return "该发票今天开具,暂时无法核验,请稍后再试";
|
||||
}
|
||||
return "发票验真未完成(" + code + "),请核对票据或稍后重试";
|
||||
} catch (Exception e) {
|
||||
log.warn("腾讯云发票验真失败,invoiceNo:{}", invoiceNo, e);
|
||||
return "发票识别成功,但自动校验失败,请人工核对后提交";
|
||||
log.warn("腾讯云发票验真失败,invoiceNo:{}", result.getString("invoiceNo"), e);
|
||||
return "发票验真未完成,请稍后重试;验真通过后才能保存或提交";
|
||||
}
|
||||
}
|
||||
|
||||
/** 核验回包必须与上传票面的票号、金额、销售方一致,避免仅凭真实票号放行被改动的票面。 */
|
||||
private String applyVerifiedInvoice(NutMap result, JSONObject verifiedInvoice) {
|
||||
if (verifiedInvoice == null) {
|
||||
return "发票验真未返回有效票据,不能保存或提交";
|
||||
}
|
||||
VatInvoice invoice = new com.google.gson.Gson().fromJson(verifiedInvoice.toString(), VatInvoice.class);
|
||||
if (!"N".equalsIgnoreCase(invoice.getIsAbandoned()) || Boolean.TRUE.equals(verifiedInvoice.getBool("RedLetterInvoiceMark"))) {
|
||||
return "发票已作废、红冲或状态无法确认,不能保存或提交";
|
||||
}
|
||||
Double amount = parseMoney(invoice.getAmountWithTax());
|
||||
Double ocrAmount = parseMoney(result.get("invoiceAmount"));
|
||||
// 分项比对只记录必要字段,区分票面识别差异;金额必须保持一致,不能用验真值覆盖后放行。
|
||||
if (!Objects.equals(result.getString("invoiceNo"), StrUtil.trim(invoice.getNumber()))) {
|
||||
log.warn("发票票号比对失败,ocrInvoiceNo:{},verifiedInvoiceNo:{}",
|
||||
result.getString("invoiceNo"), StrUtil.trim(invoice.getNumber()));
|
||||
return "发票号码与验真结果不一致,请核对上传的发票";
|
||||
}
|
||||
if (amount == null || amount <= 0 || ocrAmount == null
|
||||
|| BigDecimal.valueOf(amount).compareTo(BigDecimal.valueOf(ocrAmount)) != 0) {
|
||||
log.warn("发票价税合计比对失败,invoiceNo:{},ocrAmount:{},verifiedAmount:{}",
|
||||
result.getString("invoiceNo"), ocrAmount, amount);
|
||||
return "发票价税合计与验真结果不一致,请核对含税总金额后重新识别";
|
||||
}
|
||||
if (!Objects.equals(normalizeInvoiceSellerName(result.getString("sellerName")),
|
||||
normalizeInvoiceSellerName(invoice.getSellerName()))) {
|
||||
log.warn("发票销售方比对失败,invoiceNo:{},ocrSellerName:{},verifiedSellerName:{}",
|
||||
result.getString("invoiceNo"), result.getString("sellerName"), invoice.getSellerName());
|
||||
return "发票销售方名称与验真结果不一致,请核对上传的发票";
|
||||
}
|
||||
String itemName = joinItemNames(invoice.getItems());
|
||||
if (StrUtil.hasBlank(invoice.getSellerName(), itemName)) {
|
||||
return "发票验真结果缺少销售方或项目名称,不能保存或提交";
|
||||
}
|
||||
result.setv("invoiceCode", invoice.getCode());
|
||||
result.setv("invoiceNo", StrUtil.trim(invoice.getNumber()));
|
||||
// 个别票种核验回包不提供日期,保留已用于本次验真请求的 OCR 开票日期。
|
||||
if (StrUtil.isNotBlank(invoice.getDate())) {
|
||||
result.setv("invoiceDate", normalizeDate(invoice.getDate()));
|
||||
}
|
||||
result.setv("invoiceCheckCode", invoice.getCheckCode());
|
||||
result.setv("sellerName", StrUtil.trim(invoice.getSellerName()));
|
||||
result.setv("invoiceAmount", amount);
|
||||
// 核验结果优先;部分票种未返回分项金额时保留 OCR 提取值,不用差额推算税额。
|
||||
Double amountWithoutTax = parseMoney(invoice.getAmountWithoutTax());
|
||||
Double taxAmount = parseMoney(invoice.getTaxAmount());
|
||||
Double ocrAmountWithoutTax = parseMoney(result.get("amountWithoutTax"));
|
||||
Double ocrTaxAmount = parseMoney(result.get("taxAmount"));
|
||||
// 腾讯云对部分票种用 0.00 占位;票面 OCR 有正数时保留票面值。
|
||||
if (amountWithoutTax == null || amountWithoutTax == 0D && ocrAmountWithoutTax != null && ocrAmountWithoutTax > 0) {
|
||||
amountWithoutTax = ocrAmountWithoutTax;
|
||||
}
|
||||
if (taxAmount == null || taxAmount == 0D && ocrTaxAmount != null && ocrTaxAmount > 0) {
|
||||
taxAmount = ocrTaxAmount;
|
||||
}
|
||||
result.setv("amountWithoutTax", amountWithoutTax);
|
||||
result.setv("taxAmount", taxAmount);
|
||||
result.setv("itemName", itemName);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销售方比对仅统一中英文括号及首尾空白,保留名称文字与内部空白,避免将不同主体误判为一致。
|
||||
* 空值保持为空值;回填仍使用验真返回的原名称,不使用此比较用字符串。
|
||||
*/
|
||||
private String normalizeInvoiceSellerName(String name) {
|
||||
return name == null ? null : StrUtil.trim(name).replace('(', '(').replace(')', ')');
|
||||
}
|
||||
|
||||
private String pickValue(TextVatInvoice[] infos, String... aliases) {
|
||||
if (infos == null || aliases == null) {
|
||||
return "";
|
||||
@@ -1292,6 +1510,17 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
return "";
|
||||
}
|
||||
|
||||
/** 按候选字段优先级取首个非空票面值,避免 OCR 返回顺序决定金额含义。 */
|
||||
private String pickPreferredValue(TextVatInvoice[] infos, String... aliases) {
|
||||
for (String alias : aliases) {
|
||||
String value = pickValue(infos, alias);
|
||||
if (StrUtil.isNotBlank(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String normalizeFieldName(String name) {
|
||||
return StrUtil.blankToDefault(name, "")
|
||||
.replace(":", "")
|
||||
@@ -1353,6 +1582,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
return BigDecimal.valueOf(money).setScale(2, RoundingMode.HALF_UP).toPlainString();
|
||||
}
|
||||
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
private void saveInvoiceDetails(String reimburseId, List<UnionReimburseInvoiceDetail> invoiceDetails) {
|
||||
this.dao().clear(UnionReimburseInvoiceDetail.class, Cnd.where("reimburseId", "=", reimburseId));
|
||||
if (Lang.isEmpty(invoiceDetails)) {
|
||||
@@ -1366,7 +1596,7 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
|
||||
}
|
||||
|
||||
/**
|
||||
* 非慰问类报销必须维护发票明细,慰问类发票明细为可选。
|
||||
* 区分活动类与慰问类的基本信息校验;所有类型的发票均为必填。
|
||||
*/
|
||||
private boolean needInvoiceDetails(UnionReimburse unionReimburse) {
|
||||
return unionReimburse != null && !"UNION_REIMBURSE_PROJECT_1".equals(unionReimburse.getReimburseProject());
|
||||
|
||||
@@ -155,14 +155,13 @@ layout("/layouts/platform.html"){
|
||||
</el-button>
|
||||
<el-button @click="$refs.sortRef.onOpen()" size="medium" type="primary" icon="el-icon-sort">排序
|
||||
</el-button>
|
||||
<el-button v-if="$auth.hasPermission('sys.manager.role.user')" size="medium" type="primary"
|
||||
icon="el-icon-user" :loading="memberRoleLoading" @click="doSettingMemberRole">
|
||||
一键设置会员角色
|
||||
</el-button>
|
||||
</div>
|
||||
</el-row>
|
||||
|
||||
<!-- <div class="pull-right offscreen-right mt5 ml5">-->
|
||||
<!-- <el-button size="medium" @click="doSettingMemberRole" type="primary"><i class="el-icon-s-tools"></i>-->
|
||||
<!-- 一键设会员角色-->
|
||||
<!-- </el-button>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="pull-right offscreen-right mt5 ml5">-->
|
||||
<!-- <el-button size="medium" @click="doPublic" type="primary">一键设置公共角色</el-button>-->
|
||||
<!-- </div>-->
|
||||
@@ -516,6 +515,7 @@ layout("/layouts/platform.html"){
|
||||
roleDialogVisible: false,
|
||||
menuDialogVisible: false,
|
||||
userDialogVisible: false,
|
||||
memberRoleLoading: false,
|
||||
treeData: {
|
||||
label: "name",
|
||||
children: "zones",
|
||||
@@ -689,18 +689,30 @@ layout("/layouts/platform.html"){
|
||||
})
|
||||
},
|
||||
doSettingMemberRole() {
|
||||
this.$confirm("是否确定一键设置会员角色?", "提示", {
|
||||
if (this.memberRoleLoading) {
|
||||
return
|
||||
}
|
||||
// 操作面向全部会员,忽略当前列表筛选;确认期间也锁定按钮,避免重复提交。
|
||||
this.$set(this, "memberRoleLoading", true)
|
||||
this.$confirm("确定为全部已标记为会员的人员设置会员角色吗?已有角色将保留。", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
})
|
||||
.then(async () => {
|
||||
const {code, msg} = await this.$axios.post("/platform/sys/role/doSettingMemberRole")
|
||||
if (code === 0) {
|
||||
this.$message.success(msg)
|
||||
}
|
||||
.then(() => {
|
||||
return this.$axios.post("/platform/sys/role/doSettingMemberRole").then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success("设置完成:新增 " + res.data.addedCount
|
||||
+ " 条,恢复启用 " + res.data.enabledCount + " 条会员角色关联")
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消确认无需提示;请求异常由全局 axios 拦截器统一提示。
|
||||
})
|
||||
.finally(() => {
|
||||
this.$set(this, "memberRoleLoading", false)
|
||||
})
|
||||
},
|
||||
formatAt(val) {
|
||||
|
||||
+463
-259
File diff suppressed because it is too large
Load Diff
+29
-29
@@ -13,24 +13,24 @@ layout("/layouts/platform.html"){
|
||||
<el-input placeholder="请输入姓名或工号" clearable v-model="pageForm.searchKeyword"></el-input>
|
||||
</search-item>
|
||||
<search-item label="所属工会" v-if="isAdmin">
|
||||
<el-select v-model="pageForm.unionId" @change="doSearch" style="width: 100%"
|
||||
placeholder="请选择所属工会" clearable>
|
||||
<el-option v-for="item in unionOptions"
|
||||
:value="item.id"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<el-select v-model="pageForm.unionId" @change="doSearch" style="width: 100%"
|
||||
placeholder="请选择所属工会" clearable>
|
||||
<el-option v-for="item in unionOptions"
|
||||
:value="item.id"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="所属单位" v-if="isAdmin">
|
||||
<el-select v-model="pageForm.unitId" @change="doSearch" style="width: 100%"
|
||||
placeholder="请选择所属单位" clearable>
|
||||
<el-option v-for="item in unitOptions"
|
||||
:value="item.id"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<el-select v-model="pageForm.unitId" @change="doSearch" style="width: 100%"
|
||||
placeholder="请选择所属单位" clearable>
|
||||
<el-option v-for="item in unitOptions"
|
||||
:value="item.id"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</search-item>
|
||||
<search-item label="报销项目">
|
||||
<el-select v-model="pageForm.reimburseProject" @change="doSearch" style="width: 100%"
|
||||
@@ -95,9 +95,13 @@ layout("/layouts/platform.html"){
|
||||
<el-table-column label="操作" fixed="right" width="350px">
|
||||
<template slot-scope="{row}">
|
||||
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
|
||||
<el-button v-if="isAdmin && row.stateId == 3" @click="openEdit(row)" size="mini" type="primary">实际金额</el-button>
|
||||
<el-button v-if="row.stateId == 3" @click="doPrint(row)" size="mini" type="primary">打印</el-button>
|
||||
<el-button v-if="$auth.hasRole('SYSADMIN')" @click="onDelete(row.id)" size="mini" type="danger">删除</el-button>
|
||||
<el-button v-if="isAdmin && row.stateId == 3" @click="openEdit(row)" size="mini" type="primary">
|
||||
实际金额
|
||||
</el-button>
|
||||
<el-button @click="doPrint(row)" size="mini" type="primary">打印</el-button>
|
||||
<el-button v-if="$auth.hasRole('SYSADMIN')" @click="onDelete(row.id)" size="mini" type="danger">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -124,13 +128,12 @@ layout("/layouts/platform.html"){
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script nonce="${cspNonce!}">
|
||||
<!--#include('../info.js'){}#-->
|
||||
new Vue({
|
||||
el: "#app",
|
||||
store,
|
||||
dicts: ["UNION_REIMBURSE_TYPE","UNION_REIMBURSE_PROJECT"],
|
||||
dicts: ["UNION_REIMBURSE_TYPE", "UNION_REIMBURSE_PROJECT"],
|
||||
//分页数据
|
||||
mixins: [initTableMixins],
|
||||
components: {
|
||||
@@ -149,8 +152,8 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
editFormRules: {
|
||||
realMoney: [
|
||||
{ required: true, message: '请输入实际报销金额', trigger: 'blur' },
|
||||
{ pattern: /^([0-9]+)(\.[0-9]{1,2})?$/, message: '请输入正确的金额格式', trigger: 'blur' }
|
||||
{required: true, message: '请输入实际报销金额', trigger: 'blur'},
|
||||
{pattern: /^([0-9]+)(\.[0-9]{1,2})?$/, message: '请输入正确的金额格式', trigger: 'blur'}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -204,7 +207,7 @@ layout("/layouts/platform.html"){
|
||||
}
|
||||
},
|
||||
onView(row) {
|
||||
this.$refs.guava.edit(()=>{
|
||||
this.$refs.guava.edit(() => {
|
||||
this.showApprovalForm = false
|
||||
this.$refs.unionReimburseInfoRef.onOpen(row)
|
||||
})
|
||||
@@ -266,11 +269,8 @@ layout("/layouts/platform.html"){
|
||||
window.open('/platform/unionReimburse/collect/exportSkr?year=' + year + "&unionId=" + unionId + '&unitId=' + unitId + "&reimburseProject=" + reimburseProject)
|
||||
},
|
||||
doPrint(row) {
|
||||
if (row.reimburseProject != "UNION_REIMBURSE_PROJECT_1") {
|
||||
window.location.href = "/platform/unionReimburse/mine/ActExport?id=" + (row.id || '') + "&Print=true"
|
||||
} else {
|
||||
window.location.href = "/platform/unionReimburse/mine/ConExport?id=" + (row.id || '') + "&Print=true"
|
||||
}
|
||||
// 所有状态均可下载审批单 PDF,复用公共下载的 POST 请求、错误提示和 loading 收尾。
|
||||
this.$downLoad("/platform/unionReimburse/collect/exportPdf", {id: row.id})
|
||||
},
|
||||
onExport() {
|
||||
this.$downLoad('/platform/unionReimburse/collect/onExport', this.pageForm)
|
||||
|
||||
@@ -130,38 +130,38 @@ const unionReimburseInfo = {
|
||||
<span>{{ viewData.condolenceTime | dateFormat }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="结婚时间" v-if="viewData.condolenceTypeId === 'cf4ce7af322d464f8f86d818fcc00203'">
|
||||
<el-descriptions-item label="结婚时间" v-if="condolenceTypeCode === 'A'">
|
||||
<span>{{ viewData.marryTime | dateFormat }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="生育时间" v-if="viewData.condolenceTypeId === 'c90cb10dce6542e99ae271bee6fe8cc0'">
|
||||
<el-descriptions-item label="生育时间" v-if="condolenceTypeCode === 'D'">
|
||||
<span>{{ viewData.fertilityTime | dateFormat }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="住院病由" v-if="viewData.condolenceTypeId === '4e316737e71047e0b01aea78524c1416'">
|
||||
<el-descriptions-item label="住院病由" v-if="condolenceTypeCode === 'E'">
|
||||
<span>{{ viewData.hospitalCausation }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="住院时间" v-if="viewData.condolenceTypeId === '4e316737e71047e0b01aea78524c1416'">
|
||||
<el-descriptions-item label="住院时间" v-if="condolenceTypeCode === 'E'">
|
||||
<span v-if="viewData.hospitalizationStartTime && viewData.hospitalizationEndTime">
|
||||
{{ $moment(viewData.hospitalizationStartTime).format('YYYY-MM-DD') }} 至 {{ $moment(viewData.hospitalizationEndTime).format('YYYY-MM-DD') }}
|
||||
</span>
|
||||
<span v-else>暂无住院时间</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="入住医院" v-if="viewData.condolenceTypeId === '4e316737e71047e0b01aea78524c1416'">
|
||||
<el-descriptions-item label="入住医院" v-if="condolenceTypeCode === 'E'">
|
||||
<span>{{ viewData.hospital }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="当年次数" v-if="viewData.condolenceTypeId === '4e316737e71047e0b01aea78524c1416'">
|
||||
<el-descriptions-item label="当年次数" v-if="condolenceTypeCode === 'E'">
|
||||
<span>{{ viewData.hospitalCount }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="去逝时间" v-if="viewData.condolenceTypeId === '60f03a87b62b4823855814afdbf5672a'">
|
||||
<el-descriptions-item label="去逝时间" v-if="condolenceTypeCode === 'G' || condolenceTypeCode === 'H'">
|
||||
<span>{{ viewData.deathTime | dateFormat }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="与被慰问人关系" v-if="viewData.condolenceTypeId === '60f03a87b62b4823855814afdbf5672a'">
|
||||
<el-descriptions-item label="与被慰问人关系" v-if="condolenceTypeCode === 'H'">
|
||||
<span>{{ viewData.condolenceRelationship }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
@@ -174,10 +174,6 @@ const unionReimburseInfo = {
|
||||
<span>{{ viewData.paymentNotes }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="备注" :span="3" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
|
||||
<span>{{ viewData.notes }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="附件" :span="3">
|
||||
<file-preview v-if="viewData.files && viewData.files.length > 0"
|
||||
:files="viewData.files"
|
||||
@@ -204,15 +200,6 @@ const unionReimburseInfo = {
|
||||
<el-col :span="24">
|
||||
<el-form-item label="支付内容" label-width="100px">
|
||||
<el-input :autosize="{ minRows: 4, maxRows: 8}"
|
||||
:value="viewData.paymentNotes"
|
||||
readonly
|
||||
type="textarea"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" label-width="100px">
|
||||
<el-input :autosize="{ minRows: 3, maxRows: 6}"
|
||||
:value="viewData.notes"
|
||||
readonly
|
||||
type="textarea"></el-input>
|
||||
@@ -240,7 +227,17 @@ const unionReimburseInfo = {
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发票号码" min-width="140" prop="invoiceNo"></el-table-column>
|
||||
<el-table-column align="right" label="发票金额" min-width="110">
|
||||
<el-table-column align="right" label="税前金额" min-width="110">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.amountWithoutTax == null ? '-' : formatMoney(scope.row.amountWithoutTax) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="right" label="税额" min-width="100">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.taxAmount == null ? '-' : formatMoney(scope.row.taxAmount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="right" label="价税合计" min-width="110">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatMoney(scope.row.invoiceAmount) }}</span>
|
||||
</template>
|
||||
@@ -299,6 +296,11 @@ const unionReimburseInfo = {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 详情接口按关联类型返回业务编码,避免不同环境的类型 ID 影响字段显示。
|
||||
condolenceTypeCode() {
|
||||
return this.viewData.reimburseProject === "UNION_REIMBURSE_PROJECT_1"
|
||||
? (this.viewData.condolenceTypeCode || "") : ""
|
||||
},
|
||||
showInvoiceTab() {
|
||||
return !!this.viewData.reimburseProject
|
||||
},
|
||||
|
||||
+3
-15
@@ -86,10 +86,9 @@ layout("/layouts/platform.html"){
|
||||
<el-table-column label="操作" fixed="right" width="300px">
|
||||
<template slot-scope="{row}">
|
||||
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
|
||||
<el-button v-if="[2, 3, 4, 5].includes(Number(row.stateId))" @click="doExport(row)" size="mini" type="primary">导出</el-button>
|
||||
<el-button v-if="row.stateId == 1 || row.stateId == 5" @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
|
||||
<el-button v-if="row.stateId == 2" @click="onRevoke(row.id)" size="mini" type="danger">撤回</el-button>
|
||||
<el-button v-if="row.stateId == 3" @click="doExport(row)" size="mini" type="primary">导出报销表</el-button>
|
||||
<el-button v-if="row.stateId == 3" @click="doPrint(row)" size="mini" type="primary">打印</el-button>
|
||||
<el-button v-if="row.stateId == 1" @click="onDelete(row.id)" size="mini" type="danger">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -171,19 +170,8 @@ layout("/layouts/platform.html"){
|
||||
},
|
||||
|
||||
doExport(row) {
|
||||
if (row.reimburseProject != "UNION_REIMBURSE_PROJECT_1") {
|
||||
window.location.href = "/platform/unionReimburse/mine/ActExport?id=" + (row.id || '')
|
||||
} else {
|
||||
window.location.href = "/platform/unionReimburse/mine/ConExport?id=" + (row.id || '')
|
||||
}
|
||||
},
|
||||
|
||||
doPrint(row) {
|
||||
if (row.reimburseProject != "UNION_REIMBURSE_PROJECT_1") {
|
||||
window.location.href = "/platform/unionReimburse/mine/ActExport?id=" + (row.id || '') + "&Print=true"
|
||||
} else {
|
||||
window.location.href = "/platform/unionReimburse/mine/ConExport?id=" + (row.id || '') + "&Print=true"
|
||||
}
|
||||
// 复用公共下载的POST请求、错误提示和loading收尾,由后端选择对应审批单模板。
|
||||
this.$downLoad("/platform/unionReimburse/mine/exportPdf", {id: row.id})
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
+2
-2
@@ -136,9 +136,9 @@ layout("/layouts/platform.html"){
|
||||
this.$set(this.formData, 'isRead', true)
|
||||
this.$commonUtil.previewFile({
|
||||
suffix: "pdf",
|
||||
downloadPath: "/platform/sys/file/download?id=orpgde84uggjmpflbob83tadan",
|
||||
downloadPath: "/platform/sys/file/download?id=15eblmambgh3hoj7hg20n4obsg",
|
||||
name: "中国地质大学(武汉)教职工重大疾病互助基金管理办法.pdf",
|
||||
id: "orpgde84uggjmpflbob83tadan"
|
||||
id: "15eblmambgh3hoj7hg20n4obsg"
|
||||
})
|
||||
},
|
||||
isReadChange() {
|
||||
|
||||
+536
-380
File diff suppressed because it is too large
Load Diff
@@ -116,15 +116,15 @@ const UNION_REIMBURSE_INFO = {
|
||||
<div class="union-info-label">慰问时间</div>
|
||||
<div class="union-info-value">{{ formatDateText(viewData.condolenceTime) }}</div>
|
||||
</div>
|
||||
<div class="union-info-row" v-if="isCondolenceType('cf4ce7af322d464f8f86d818fcc00203')">
|
||||
<div class="union-info-row" v-if="condolenceTypeCode === 'A'">
|
||||
<div class="union-info-label">结婚时间</div>
|
||||
<div class="union-info-value">{{ formatDateText(viewData.marryTime) }}</div>
|
||||
</div>
|
||||
<div class="union-info-row" v-if="isCondolenceType('c90cb10dce6542e99ae271bee6fe8cc0')">
|
||||
<div class="union-info-row" v-if="condolenceTypeCode === 'D'">
|
||||
<div class="union-info-label">生育时间</div>
|
||||
<div class="union-info-value">{{ formatDateText(viewData.fertilityTime) }}</div>
|
||||
</div>
|
||||
<template v-if="isCondolenceType('4e316737e71047e0b01aea78524c1416')">
|
||||
<template v-if="condolenceTypeCode === 'E'">
|
||||
<div class="union-info-row">
|
||||
<div class="union-info-label">住院病由</div>
|
||||
<div class="union-info-value">{{ viewData.hospitalCausation }}</div>
|
||||
@@ -146,12 +146,12 @@ const UNION_REIMBURSE_INFO = {
|
||||
<div class="union-info-value">{{ viewData.hospitalCount }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="isCondolenceType('60f03a87b62b4823855814afdbf5672a')">
|
||||
<template v-if="condolenceTypeCode === 'G' || condolenceTypeCode === 'H'">
|
||||
<div class="union-info-row">
|
||||
<div class="union-info-label">去逝时间</div>
|
||||
<div class="union-info-value">{{ formatDateText(viewData.deathTime) }}</div>
|
||||
</div>
|
||||
<div class="union-info-row">
|
||||
<div class="union-info-row" v-if="condolenceTypeCode === 'H'">
|
||||
<div class="union-info-label">与被慰问人关系</div>
|
||||
<div class="union-info-value">{{ viewData.condolenceRelationship }}</div>
|
||||
</div>
|
||||
@@ -164,10 +164,6 @@ const UNION_REIMBURSE_INFO = {
|
||||
<div class="union-info-label">报销事由</div>
|
||||
<div class="union-info-value">{{ viewData.paymentNotes }}</div>
|
||||
</div>
|
||||
<div class="union-info-row">
|
||||
<div class="union-info-label">备注</div>
|
||||
<div class="union-info-value">{{ viewData.notes }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="union-info-section" v-if="viewData.reimburseProject && viewData.reimburseProject !== 'UNION_REIMBURSE_PROJECT_1'">
|
||||
@@ -188,10 +184,6 @@ const UNION_REIMBURSE_INFO = {
|
||||
<div class="union-info-label">活动时间</div>
|
||||
<div class="union-info-value">{{ formatDateText(viewData.activityTime) }}</div>
|
||||
</div>
|
||||
<div class="union-info-row">
|
||||
<div class="union-info-label">支付内容</div>
|
||||
<div class="union-info-value">{{ viewData.paymentNotes }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="union-info-section">
|
||||
@@ -212,6 +204,10 @@ const UNION_REIMBURSE_INFO = {
|
||||
<div class="union-info-label">发票张数</div>
|
||||
<div class="union-info-value">{{ viewData.invoiceNumber }}</div>
|
||||
</div>
|
||||
<div class="union-info-row">
|
||||
<div class="union-info-label">支付内容</div>
|
||||
<div class="union-info-value">{{ viewData.notes }}</div>
|
||||
</div>
|
||||
<div class="union-invoice-subtitle">发票明细表</div>
|
||||
<div class="union-invoice-card-list" v-if="viewData.invoiceDetails && viewData.invoiceDetails.length > 0">
|
||||
<div class="union-invoice-card" v-for="(item, index) in viewData.invoiceDetails" :key="index">
|
||||
@@ -221,7 +217,15 @@ const UNION_REIMBURSE_INFO = {
|
||||
<div class="union-info-value">{{ item.invoiceNo || '未填写' }}</div>
|
||||
</div>
|
||||
<div class="union-info-row">
|
||||
<div class="union-info-label">发票金额</div>
|
||||
<div class="union-info-label">税前金额</div>
|
||||
<div class="union-info-value">{{ item.amountWithoutTax == null ? '未识别' : formatMoney(item.amountWithoutTax) }}</div>
|
||||
</div>
|
||||
<div class="union-info-row">
|
||||
<div class="union-info-label">税额</div>
|
||||
<div class="union-info-value">{{ item.taxAmount == null ? '未识别' : formatMoney(item.taxAmount) }}</div>
|
||||
</div>
|
||||
<div class="union-info-row">
|
||||
<div class="union-info-label">价税合计</div>
|
||||
<div class="union-info-value">{{ formatMoney(item.invoiceAmount) }}</div>
|
||||
</div>
|
||||
<div class="union-info-row">
|
||||
@@ -333,6 +337,11 @@ const UNION_REIMBURSE_INFO = {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 详情接口按关联类型返回业务编码,避免不同环境的类型 ID 影响字段显示。
|
||||
condolenceTypeCode() {
|
||||
return this.viewData.reimburseProject === "UNION_REIMBURSE_PROJECT_1"
|
||||
? (this.viewData.condolenceTypeCode || "") : ""
|
||||
},
|
||||
paymentInfoRows() {
|
||||
return this.viewData.paymentDetails || []
|
||||
}
|
||||
@@ -406,9 +415,6 @@ const UNION_REIMBURSE_INFO = {
|
||||
const numberValue = Number(value)
|
||||
return isNaN(numberValue) ? value : numberValue.toFixed(2)
|
||||
},
|
||||
isCondolenceType(typeId) {
|
||||
return this.viewData.condolenceTypeId === typeId || this.viewData.condolenceType === typeId
|
||||
},
|
||||
getPaymentWayText(paymentWay) {
|
||||
const matchedPaymentWay = this.dict.type.UNION_REIMBURSE_PAYMENT_WAY.find(item => item.code === paymentWay)
|
||||
return matchedPaymentWay ? matchedPaymentWay.name : ""
|
||||
|
||||
Reference in New Issue
Block a user