|
@@ -38,6 +38,8 @@ import java.io.ByteArrayInputStream;
|
|
|
import java.io.ByteArrayOutputStream;
|
|
|
import java.io.File;
|
|
|
import java.io.InputStream;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
import java.net.URL;
|
|
|
import java.util.*;
|
|
|
import java.util.stream.Collectors;
|
|
@@ -162,9 +164,15 @@ public class IVController {
|
|
|
image = pdfUrlConvertBase64(image);
|
|
|
}
|
|
|
List<Map> invoices = (List<Map>) txyInvoice.doRecognizeGeneralInvoice(image).get("MixedInvoiceItems");
|
|
|
+ List<String> nos=new ArrayList<>();
|
|
|
List<McInvoiceDto> result = invoices.stream().map(item -> {
|
|
|
Map prop = UtilMap.getMap(UtilMap.getMap(item, "SingleInvoiceInfos"), UtilMap.getString(item, "SubType"));
|
|
|
-
|
|
|
+ // 2025.5.12 去除小计金额,并去除发票编号重复数据
|
|
|
+ String no=UtilMap.getString(prop, "Number");
|
|
|
+ if(nos.contains(no)){
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ nos.add(no);
|
|
|
String kind = UtilMap.getString(item, "TypeDescription");
|
|
|
String invoiceName = UtilMap.getString(item, "SubTypeDescription");
|
|
|
if (kind.equals("全电发票")) {
|
|
@@ -185,9 +193,9 @@ public class IVController {
|
|
|
.serial(UtilMap.getString(prop, "Number"))
|
|
|
.date(UtilString.replaceDateZH_cn(UtilMap.getString(prop, "Date")))
|
|
|
.checkCode(UtilMap.getString(prop, "CheckCode"))
|
|
|
- // ppExt: 多明细行时, 优先取值合计 [全电票返回了subTotal字段, 但值为空]
|
|
|
- .amount(UtilNumber.setBigDecimal(UtilMap.getString_first(prop, "SubTotal", "Total", "Fare")))
|
|
|
- .tax(UtilNumber.setBigDecimal(UtilMap.getString_first(prop, "SubTax", "Tax")))
|
|
|
+ // ppExt: 多明细行时, 优先取值合计 [全电票返回了subTotal字段, 但值为空] // 2025.5.12 去除小计金额,并去除发票编号重复数据
|
|
|
+ .amount(UtilNumber.setBigDecimal(UtilMap.getString_first(prop, "Total", "Fare"))) // "SubTotal",
|
|
|
+ .tax(UtilNumber.setBigDecimal(UtilMap.getString_first(prop, "Tax"))) // "SubTax",
|
|
|
.excludingTax(UtilNumber.setBigDecimal(UtilMap.getString(prop, "PretaxAmount")))
|
|
|
.buyerName(StringUtils.isBlank(guyuanNameRepalce(UtilMap.getString(prop, "Buyer")))?"":guyuanNameRepalce(UtilMap.getString(prop, "Buyer")))
|
|
|
// ppExt: 中央非税未返回税号官方说明: 非税发票理论是没有税号的,图片中属于信用代码
|
|
@@ -206,6 +214,10 @@ public class IVController {
|
|
|
.constructionCosts(UtilNumber.setBigDecimal((UtilMap.getString(prop, "AirDevelopmentFund")))) // 行程单: 民航发展基金
|
|
|
.build();
|
|
|
// ppExt: 机票行程单, 行程与座位信息在明细内
|
|
|
+ if("火车票".equals(kind)){
|
|
|
+ invoiceDto.setTax(calculateTax(invoiceDto.getAmount(), new BigDecimal("0.09"), 2));
|
|
|
+ invoiceDto.setExcludingTax(invoiceDto.getAmount().subtract(invoiceDto.getTax()));
|
|
|
+ }
|
|
|
if ("机票行程单".equals(item.get("TypeDescription"))) {
|
|
|
Map flight = (Map) UtilMap.getList(prop, "FlightItems").get(0);
|
|
|
invoiceDto.setDepartureTime(UtilString.replaceDateZH_cn(UtilMap.getString(item, "DateGetOn")) + " " + UtilMap.getString(prop, "TimeGetOn"));
|
|
@@ -218,89 +230,25 @@ public class IVController {
|
|
|
invoiceDto.setDepartureTime(UtilMap.getString(prop, "TimeGetOn") + " ~ " + UtilMap.getString(prop, "TimeGetOff"));
|
|
|
}
|
|
|
return invoiceDto;
|
|
|
- }).collect(Collectors.toList());
|
|
|
+ }).filter(item -> item!=null).collect(Collectors.toList());
|
|
|
return McR.success(McInvoiceDto.formatResponse(result));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 混票识别 [旧版本, 已废弃]
|
|
|
+ * 计算税额方法
|
|
|
+ * @param faceValue 票面金额
|
|
|
+ * @param taxRate 税率(如9%传0.09)
|
|
|
+ * @param scale 小数位数
|
|
|
+ * @return 税额
|
|
|
*/
|
|
|
- @PostMapping("invoice-iv")
|
|
|
- McR invoice_iv(@RequestBody Map<String, String> data) throws TencentCloudSDKException {
|
|
|
-
|
|
|
- McException.assertParamException_Null(data, "url");
|
|
|
- String image = ydClient.convertTemporaryUrl(data.get("url"),0);
|
|
|
- log.info("混票识别, 免登地址, {}", image);
|
|
|
- // 非PDF, 且内存大于3M, 压缩后上传
|
|
|
- if (UtilMap.getFloat(data, "size") > 3.0f && !UtilMap.getBoolean(data, "isPdf")) {
|
|
|
- image = imageUrlConvertBase64(image);
|
|
|
+ public static BigDecimal calculateTax(BigDecimal faceValue, BigDecimal taxRate, int scale) {
|
|
|
+ if (faceValue == null || taxRate == null) {
|
|
|
+ throw new IllegalArgumentException("参数不能为null");
|
|
|
}
|
|
|
- if (UtilMap.getFloat(data, "size") > 6.0f && UtilMap.getBoolean(data, "isPdf")) {
|
|
|
- image = pdfUrlConvertBase64(image);
|
|
|
- }
|
|
|
- // ppExt: 通用字段定义
|
|
|
- List<Map> invoices = (List<Map>) txyInvoice.doMixedInvoiceOCR(image).get("MixedInvoiceItems");
|
|
|
- List<McInvoiceDto> result = invoices.stream().map(item -> {
|
|
|
- String kind = TXYConf.TYPE_INVOICE.get(item.get("Type").toString());
|
|
|
- List<Map<String, String>> infos = (List<Map<String, String>>) item.get("SingleInvoiceInfos");
|
|
|
-
|
|
|
- McInvoiceDto.assertSuccess(item, kind); // 响应断言
|
|
|
- String invoiceName = findValue(infos, "发票名称");
|
|
|
- if (kind.equals("全电发票")) {
|
|
|
- kind = invoiceName.contains("增值税专用发票") ? "全电专用发票" : "全电普通发票";
|
|
|
- }
|
|
|
- if (kind.equals("增值税发票")) {
|
|
|
- kind = invoiceName.contains("增值税专用发票") ? "增值税专用发票" : "增值税普通发票";
|
|
|
- if (invoiceName.contains("增值税电子")) {
|
|
|
- kind = invoiceName.contains("专用发票") ? "增值税电子专用发票" : "增值税电子普通发票";
|
|
|
- }
|
|
|
- }
|
|
|
- McInvoiceDto invoiceDto = McInvoiceDto.builder()
|
|
|
- .name(invoiceName)
|
|
|
- .kindName(kind)
|
|
|
- .kind(McInvoiceKind.getKindCode(kind))
|
|
|
- .code(findValue(infos, "发票代码", "票据代码")) // 发票, 非税发票
|
|
|
- // 储存唯一ID [发票, 火车票, 行程单]
|
|
|
- .serial(findValue(infos, "发票号码", "编号", "电子客票号码", "票据号码").replace("No", "")) // 发票, 非税发票
|
|
|
- .date(findValue(infos, "开票日期").replace("年", "-").replace("月", "-").replace("日", ""))
|
|
|
- .checkCode(findValue(infos, "校验码"))
|
|
|
- .amount(UtilNumber.replaceCurrencyCHYToDecimal(findValue(infos, "小写金额", "价税合计(小写)", "合计金额", "票价", "金额"))) // 发票, 全电票, 行程单, 火车票, 过路过桥费
|
|
|
- .excludingTax(UtilNumber.replaceCurrencyCHYToDecimal(findValue(infos, "合计金额", "金额", "票价", "小写金额"))) // [ppExt: 多明细行时, 优先取值合计] 行程单, 火车票, 定额发票
|
|
|
- .tax(UtilNumber.replaceCurrencyCHYToDecimal(findValue(infos, "合计税额"))) // 增值税发票
|
|
|
- .buyerName(guyuanNameRepalce(findValue(infos, "购买方名称", "交款人"))) // 发票, 非税发票
|
|
|
- .buyerTaxId(findValue(infos, "购买方识别号", "购买方统一社会信用代码/纳税人识别号", "交款人统一社会信用代码")) // 发票, 全电票, 非税发票
|
|
|
- .sellerName(guyuanNameRepalce(findValue(infos, "销售方名称", "填开单位"))) // 行程单
|
|
|
- .sellerTaxId(findValue(infos, "销售方识别号", "销售方统一社会信用代码/纳税人识别号", "销售单位代号")) // 发票, 全电票, 行程单
|
|
|
- .passengerName(findValue(infos, "旅客姓名", "姓名")) // 行程单, 火车票
|
|
|
- .seatType(findValue(infos, "座位等级", "席别")) // 行程单, 火车票
|
|
|
- .departurePort(findValue(infos, "始发地", "出发站", "入口")) // 行程单, 火车票, 过路过桥费
|
|
|
- .arrivePort(findValue(infos, "目的地", "到达站", "出口")) // 行程单, 火车票, 过路过桥费
|
|
|
- .trainNo(findValue(infos, "航班号", "车次", "车牌号")) // 行程单, 火车票, 出租车
|
|
|
- .insuranceCosts(UtilNumber.setBigDecimal((findValue(infos, "保险费")))) // 行程单
|
|
|
- .fuelCosts(UtilNumber.setBigDecimal((findValue(infos, "燃油附加费")))) // 行程单
|
|
|
- .constructionCosts(UtilNumber.setBigDecimal((findValue(infos, "民航发展基金")))) // 行程单
|
|
|
- .build();
|
|
|
- // 价格不一致情况下, 通过合计返回
|
|
|
- if (!UtilNumber.equalBigDecimal(invoiceDto.getAmount(), invoiceDto.getExcludingTax().add(invoiceDto.getTax()))) {
|
|
|
- invoiceDto.setAmount(invoiceDto.getExcludingTax().add(invoiceDto.getTax()));
|
|
|
- }
|
|
|
- // 机票行程单
|
|
|
- if (kind.equals(McInvoiceKind.JP.getDesc())) {
|
|
|
- String date = findValue(infos, "日期").replace("年", "-").replace("月", "-").replace("日", " ");
|
|
|
- invoiceDto.setDepartureTime(date + " " + findValue(infos, "时间"));
|
|
|
- }
|
|
|
- // 火车票
|
|
|
- if (kind.equals(McInvoiceKind.HC.getDesc())) {
|
|
|
- invoiceDto.setDepartureTime(findValue(infos, "出发时间").replace("年", "-").replace("月", "-").replace("日", " "));
|
|
|
- }
|
|
|
- // 出租车
|
|
|
- if (kind.equals(McInvoiceKind.CZC.getDesc())) {
|
|
|
- String date = findValue(infos, "日期").replace("年", "-").replace("月", "-").replace("日", " ");
|
|
|
- invoiceDto.setDepartureTime(date + " " + findValue(infos, "上车"));
|
|
|
- }
|
|
|
- return invoiceDto;
|
|
|
- }).collect(Collectors.toList());
|
|
|
- return McR.success(McInvoiceDto.formatResponse(result));
|
|
|
+ BigDecimal denominator = BigDecimal.ONE.add(taxRate);
|
|
|
+ return faceValue.divide(denominator, scale, RoundingMode.HALF_UP)
|
|
|
+ .multiply(taxRate)
|
|
|
+ .setScale(scale, RoundingMode.HALF_UP);
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -328,30 +276,37 @@ public class IVController {
|
|
|
if (idList.size() > 0) {
|
|
|
McException.exceptionAccess(serial + "已存在, 请勿重复提交!");
|
|
|
}
|
|
|
+ List<String> yzType=Arrays.asList("增值税普通发票",
|
|
|
+ "增值税专用发票",
|
|
|
+ "增值税电子专用发票",
|
|
|
+ "增值税电子普通发票",
|
|
|
+ "全电普通发票",
|
|
|
+ "全电专用发票");
|
|
|
// prd 仅仅识别 报销 用途的发票
|
|
|
- if (dto.getType().contains("报销") && !dto.getKindName().contains("车票") && !dto.getKindName().contains("车发票") && !dto.getKindName().contains("定额发票")) {
|
|
|
- String serialTips = serial + "有疑问";
|
|
|
- try {
|
|
|
- // ppExt: 识别与验真后抬头对比 [全电票, 新版本识别接口, 返回名称为: 电子发票(普通发票) 不包含全电标识, 发类型为: 全电发票. 注意取值]
|
|
|
- Map rsp = txyInvoice.doVatInvoiceVerifyNew(dto.getKindName(), dto.getCode(), invoiceNo, dto.getDate(), String.valueOf(dto.getAmount()), dto.getCheckCode(), String.valueOf(dto.getExcludingTax()), serialTips);
|
|
|
- Map invoice = (Map) rsp.get("Invoice");
|
|
|
- McException.assertAccessException(!dto.getBuyerName().equals(guyuanNameRepalce(invoice.get("BuyerName").toString())), serialTips + ", 购买方名称不匹配!");
|
|
|
- McException.assertAccessException(!dto.getBuyerTaxId().equals(invoice.get("BuyerTaxCode")), serialTips + ", 购买方税号不匹配!");
|
|
|
- McException.assertAccessException(!dto.getSellerName().equals(guyuanNameRepalce(invoice.get("SellerName").toString())), serialTips + ", 销售方名称不匹配!");
|
|
|
- McException.assertAccessException(!dto.getSellerTaxId().equals(invoice.get("SellerTaxCode")), serialTips + ", 销售方税号不匹配!");
|
|
|
- } catch (TencentCloudSDKException e) {
|
|
|
- log.error(e.getMessage(), e);
|
|
|
- // prd: 上传发票为假发票时,提示:该发票有疑问,请联系财务人员
|
|
|
- String message = e.getMessage();
|
|
|
- // ppExt: 已经是新版本接口, 过滤提示 [官方答复: 提示不会检测您是否使用的是新版,所有的用户都会提示, 忽略即可]
|
|
|
- if (message.contains("温馨提示")) {
|
|
|
- message = message.split("温馨提示")[0];
|
|
|
- }
|
|
|
- if (message.contains("发票不存在")) {
|
|
|
- message = "有疑问,请联系财务人员";
|
|
|
- }
|
|
|
- McException.exceptionAccess(serial + message);
|
|
|
+ if(!dto.getType().contains("报销")||!yzType.contains(dto.getKindName())){
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ String serialTips = serial + "有疑问";
|
|
|
+ try {
|
|
|
+ // ppExt: 识别与验真后抬头对比 [全电票, 新版本识别接口, 返回名称为: 电子发票(普通发票) 不包含全电标识, 发类型为: 全电发票. 注意取值]
|
|
|
+ Map rsp = txyInvoice.doVatInvoiceVerifyNew(dto.getKindName(), dto.getCode(), invoiceNo, dto.getDate(), String.valueOf(dto.getAmount()), dto.getCheckCode(), String.valueOf(dto.getExcludingTax()), serialTips,dto.getSellerTaxId());
|
|
|
+ Map invoice = (Map) rsp.get("Invoice");
|
|
|
+ McException.assertAccessException(!dto.getBuyerName().equals(guyuanNameRepalce(invoice.get("BuyerName").toString())), serialTips + ", 购买方名称不匹配!");
|
|
|
+ McException.assertAccessException(!dto.getBuyerTaxId().equals(invoice.get("BuyerTaxCode")), serialTips + ", 购买方税号不匹配!");
|
|
|
+ McException.assertAccessException(!dto.getSellerName().equals(guyuanNameRepalce(invoice.get("SellerName").toString())), serialTips + ", 销售方名称不匹配!");
|
|
|
+ McException.assertAccessException(!dto.getSellerTaxId().equals(invoice.get("SellerTaxCode")), serialTips + ", 销售方税号不匹配!");
|
|
|
+ } catch (TencentCloudSDKException e) {
|
|
|
+ log.error(e.getMessage(), e);
|
|
|
+ // prd: 上传发票为假发票时,提示:该发票有疑问,请联系财务人员
|
|
|
+ String message = e.getMessage();
|
|
|
+ // ppExt: 已经是新版本接口, 过滤提示 [官方答复: 提示不会检测您是否使用的是新版,所有的用户都会提示, 忽略即可]
|
|
|
+ if (message.contains("温馨提示")) {
|
|
|
+ message = message.split("温馨提示")[0];
|
|
|
+ }
|
|
|
+ if (message.contains("发票不存在")) {
|
|
|
+ message = "有疑问,请联系财务人员";
|
|
|
}
|
|
|
+ McException.exceptionAccess(serial + message);
|
|
|
}
|
|
|
}));
|
|
|
return McR.success();
|