ruoyi-common/src/main/java/com/ruoyi/common/tax/TaxtUtils.java
@@ -66,6 +66,10 @@ /** * 通过税后工资,算出税前工资 * <p> * 注意:除数必须用字符串/常量构造的精确值。原先写成 {@code new BigDecimal(0.84)} 等, * 会把 double 的二进制误差带进除法(如 8313.06 / 0.84 变成 9896.500000…001), * 再叠加向上取整就多出 1 分钱,导致“税后反算税前”无法还原(8300.00 类金额反解出 10000.03)。 * * @param money * @return @@ -78,21 +82,21 @@ } //第一个参数是除数,第二个参数代表保留几位小数,第三个代表的是使用的模式 if (money.doubleValue() > 800 && money.doubleValue() <= 3360) { taxMoney = money.subtract(new BigDecimal(160)).divide(new BigDecimal(0.8), 2, BigDecimal.ROUND_CEILING); taxMoney = money.subtract(new BigDecimal(160)).divide(new BigDecimal("0.8"), 2, BigDecimal.ROUND_CEILING); } if (money.doubleValue() > 3360 && money.doubleValue() <= 21000) { taxMoney = money.divide(new BigDecimal(0.84), 2, BigDecimal.ROUND_CEILING); taxMoney = money.divide(new BigDecimal("0.84"), 2, BigDecimal.ROUND_CEILING); } if (money.doubleValue() > 21000 && money.doubleValue() <= 49500) { money = money.subtract(new BigDecimal(2000)); taxMoney = money.divide(new BigDecimal(0.76), 2, BigDecimal.ROUND_CEILING); taxMoney = money.divide(new BigDecimal("0.76"), 2, BigDecimal.ROUND_CEILING); } if (money.doubleValue() > 49500) { money = money.subtract(new BigDecimal(7000)); taxMoney = money.divide(new BigDecimal(0.68), 2, BigDecimal.ROUND_CEILING); taxMoney = money.divide(new BigDecimal("0.68"), 2, BigDecimal.ROUND_CEILING); } ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFund.java
@@ -299,10 +299,37 @@ private Double pretaxcost; /** * 税后金额 * 增值税合计 */ @ApiModelProperty("税后金额") @Excel(name = "税后金额") @ApiModelProperty("增值税合计") @Excel(name = "增值税") private Double vatCost; /** * 城建税合计 */ @ApiModelProperty("城建税合计") @Excel(name = "城建税") private Double cityTaxCost; /** * 劳务税前合计 */ @ApiModelProperty("劳务税前合计") @Excel(name = "劳务税前") private Double laborPretaxCost; /** * 税口径版本:v1=历史口径(无增值税/城建税),v2=新增口径 */ @ApiModelProperty("税口径版本") private String taxVersion; /** * 劳务税后(原“税后金额”) */ @ApiModelProperty("劳务税后") @Excel(name = "劳务税后") private Double taxedcost; /** @@ -387,10 +414,10 @@ private Long fundTaxId = null; /** * 税金 * 劳务个税合计(原“税金”) */ @ApiModelProperty("税金") @Excel(name = "税金") @ApiModelProperty("劳务个税合计") @Excel(name = "劳务个税") private Double taxcost; @ApiModelProperty("协友要求再次发起时,带上这个值") ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFunddetail.java
@@ -217,11 +217,38 @@ private Double prepaidamount; /** * 扣税金额 * 劳务个税(原“扣税金额”) */ @ApiModelProperty("扣税金额") @Excel(name = "扣税金额") @ApiModelProperty("劳务个税") @Excel(name = "劳务个税") private Double taxamount; /** * 增值税 */ @ApiModelProperty("增值税") @Excel(name = "增值税") private Double vatAmount; /** * 城建税 */ @ApiModelProperty("城建税") @Excel(name = "城建税") private Double cityTaxAmount; /** * 劳务税前金额 */ @ApiModelProperty("劳务税前金额") @Excel(name = "劳务税前") private Double laborPretaxAmount; /** * 税口径版本:v1=历史口径(无增值税/城建税),v2=新增口径 */ @ApiModelProperty("税口径版本") private String taxVersion; /** * 发票张数 @@ -237,10 +264,10 @@ @Excel(name = "上传时间") private BigDecimal reimbursementamount; /** * 税后金额 * 劳务税后(原“税后金额”) */ @ApiModelProperty("税后金额") @Excel(name = "税后金额") @ApiModelProperty("劳务税后") @Excel(name = "劳务税后") private Double taxedamount; /** ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFunddetailExcel.java
@@ -72,13 +72,39 @@ @Excel(name = "税前金额") private Double amount; /** * 增值税 */ @ApiModelProperty("增值税") @Excel(name = "增值税") private Double vatAmount; /** * 税后金额 * 城建税 */ @ApiModelProperty("税后金额") @Excel(name = "税后金额") @ApiModelProperty("城建税") @Excel(name = "城建税") private Double cityTaxAmount; /** * 劳务税前 */ @ApiModelProperty("劳务税前") @Excel(name = "劳务税前") private Double laborPretaxAmount; /** * 劳务个税 */ @ApiModelProperty("劳务个税") @Excel(name = "劳务个税") private Double taxamount; /** * 劳务税后 */ @ApiModelProperty("劳务税后") @Excel(name = "劳务税后") private Double taxedamount; /** ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFunddetailShared.java
@@ -242,20 +242,41 @@ private Double amount; /** * 扣税金额 * 劳务个税(原“扣税金额”) */ @ApiModelProperty("扣税金额") @Excel(name = "扣税金额") @ApiModelProperty("劳务个税") @Excel(name = "劳务个税") private Double taxamount; /** * 税后金额 * 劳务税后(原“税后金额”) */ @ApiModelProperty("税后金额") @Excel(name = "税后金额") @ApiModelProperty("劳务税后") @Excel(name = "劳务税后") private Double taxedamount; /** * 增值税 */ @ApiModelProperty("增值税") @Excel(name = "增值税") private Double vatAmount; /** * 城建税 */ @ApiModelProperty("城建税") @Excel(name = "城建税") private Double cityTaxAmount; /** * 劳务税前金额 */ @ApiModelProperty("劳务税前金额") @Excel(name = "劳务税前") private Double laborPretaxAmount; /** * 预支金额 */ @ApiModelProperty("预支金额") ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFunddetailVO.java
@@ -202,11 +202,38 @@ private Double prepaidamount; /** * 扣税金额 * 劳务个税(原“扣税金额”) */ @ApiModelProperty("扣税金额") @Excel(name = "扣税金额") @ApiModelProperty("劳务个税") @Excel(name = "劳务个税") private Double taxamount; /** * 增值税 */ @ApiModelProperty("增值税") @Excel(name = "增值税") private Double vatAmount; /** * 城建税 */ @ApiModelProperty("城建税") @Excel(name = "城建税") private Double cityTaxAmount; /** * 劳务税前金额 */ @ApiModelProperty("劳务税前金额") @Excel(name = "劳务税前") private Double laborPretaxAmount; /** * 税口径版本:v1=历史口径(无增值税/城建税),v2=新增口径 */ @ApiModelProperty("税口径版本") private String taxVersion; /** * 发票张数 @@ -216,10 +243,10 @@ private Long invoicecount; /** * 税后金额 * 劳务税后(原“税后金额”) */ @ApiModelProperty("税后金额") @Excel(name = "税后金额") @ApiModelProperty("劳务税后") @Excel(name = "劳务税后") private Double taxedamount; /** ruoyi-project/src/main/java/com/ruoyi/project/domain/ServiceFundtax.java
@@ -98,24 +98,51 @@ private BigDecimal pretaxamount; /** * 税后金额合计 * 增值税合计 */ @ApiModelProperty("税后金额合计") @Excel(name = "税后金额合计") @ApiModelProperty("增值税合计") @Excel(name = "增值税合计") private BigDecimal vatAmount; /** * 城建税合计 */ @ApiModelProperty("城建税合计") @Excel(name = "城建税合计") private BigDecimal cityTaxAmount; /** * 劳务税前合计 */ @ApiModelProperty("劳务税前合计") @Excel(name = "劳务税前合计") private BigDecimal laborPretaxAmount; /** * 税口径版本:v1=历史口径(无增值税/城建税),v2=新增口径 */ @ApiModelProperty("税口径版本") private String taxVersion; /** * 劳务税后(原“税后金额合计”) */ @ApiModelProperty("劳务税后合计") @Excel(name = "劳务税后合计") private BigDecimal taxedamount; /** * 费用总金额 * 劳务个税合计 */ @ApiModelProperty("费用总金额") @Excel(name = "费用总金额") @ApiModelProperty("劳务个税合计") @Excel(name = "劳务个税合计") private BigDecimal taxamount; /** * 扣税金额 * 劳务个税合计(历史字段 tax_money,页面“扣税金额”取此列) */ @ApiModelProperty("扣税金额") @Excel(name = "扣税金额") @ApiModelProperty("劳务个税(扣税金额)") @Excel(name = "劳务个税") private BigDecimal taxMoney; /** ruoyi-project/src/main/java/com/ruoyi/project/domain/vo/TaxMoneySumEO.java
@@ -7,19 +7,35 @@ public class TaxMoneySumEO extends BaseEntity { /** * 税前总额 * 税前总额(Amount 合计) */ private double amounts; /** * 税总额 * 增值税总额 */ private double vatAmounts; /** * 城建税总额 */ private double cityTaxAmounts; /** * 劳务税前总额(劳务个税累计基数) */ private double laborPretaxAmounts; /** * 劳务个税总额 */ private double taxAmounts; /** * 税后总额 * 劳务税后总额 */ private double taxedAmounts; /** * 序号 */ @@ -29,5 +45,9 @@ */ private String IDCardNo; /** * 当月已存在的历史口径(v1)明细条数,用于阻止同一月份 v1/v2 混算 */ private Long v1Count; } ruoyi-project/src/main/java/com/ruoyi/project/mapper/ServiceFundtaxMapper.java
@@ -29,4 +29,13 @@ * @return */ public Integer getMaxFundTaxId(); /** * 对当前批次汇总单加行锁(SELECT ... FOR UPDATE),防止并发重复算税。 * 必须在事务中调用。 * * @param id 汇总单 ID * @return 汇总单(不存在返回 null) */ public ServiceFundtax selectByIdForUpdate(Long id); } ruoyi-project/src/main/java/com/ruoyi/project/service/IServiceFunddetailService.java
@@ -1,5 +1,6 @@ package com.ruoyi.project.service; import java.util.Date; import java.util.List; import java.util.Map; @@ -62,12 +63,21 @@ List<ServiceFunddetailVO> getFundDetail(Long id); /** * 根据条件算税 * 根据条件算税(劳务个税 + 增值税 + 城建税) * * @param serviceFunddetails */ public Boolean calculateTax(List<ServiceFunddetail> serviceFunddetails, Map<String, TaxMoneySumEO> tempTaxSumMap); /** * 根据条件算税,并指定本批算税时间(同批明细 tax_time 一致) * * @param serviceFunddetails 明细列表 * @param tempTaxSumMap 本月累计缓存 * @param taxTime 本次算税时间 */ public Boolean calculateTax(List<ServiceFunddetail> serviceFunddetails, Map<String, TaxMoneySumEO> tempTaxSumMap, Date taxTime); // public Boolean calculateTax2(List<ServiceFunddetail> serviceFunddetails); /** ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceFundServiceImpl.java
@@ -1705,10 +1705,23 @@ //通过受益人名称,将税前、税后的值相加 Map<String, Double> sumAmount = serviceFunddetails.stream().collect(Collectors.groupingBy(ServiceFunddetail::getBeneficiaryname, Collectors.summingDouble(ServiceFunddetail::getAmount))); Map<String, Double> sumTaxdAmount = serviceFunddetails.stream().collect(Collectors.groupingBy(ServiceFunddetail::getBeneficiaryname, Collectors.summingDouble(ServiceFunddetail::getTaxedamount))); // 新增口径:增值税、城建税、劳务税前、劳务个税按受益人合计 Map<String, Double> sumVat = serviceFunddetails.stream().collect(Collectors.groupingBy(ServiceFunddetail::getBeneficiaryname, Collectors.summingDouble(d -> d.getVatAmount() == null ? 0.0 : d.getVatAmount()))); Map<String, Double> sumCityTax = serviceFunddetails.stream().collect(Collectors.groupingBy(ServiceFunddetail::getBeneficiaryname, Collectors.summingDouble(d -> d.getCityTaxAmount() == null ? 0.0 : d.getCityTaxAmount()))); Map<String, Double> sumLaborPretax = serviceFunddetails.stream().collect(Collectors.groupingBy(ServiceFunddetail::getBeneficiaryname, Collectors.summingDouble(d -> d.getLaborPretaxAmount() == null ? 0.0 : d.getLaborPretaxAmount()))); Map<String, Double> sumTax = serviceFunddetails.stream().collect(Collectors.groupingBy(ServiceFunddetail::getBeneficiaryname, Collectors.summingDouble(d -> d.getTaxamount() == null ? 0.0 : d.getTaxamount()))); for (String key : sumAmount.keySet()) { ServiceFunddetailExcel serviceFunddetailExcel = new ServiceFunddetailExcel(); serviceFunddetailExcel.setAmount(sumAmount.getOrDefault(key, 0.0)); serviceFunddetailExcel.setVatAmount(sumVat.getOrDefault(key, 0.0)); serviceFunddetailExcel.setCityTaxAmount(sumCityTax.getOrDefault(key, 0.0)); serviceFunddetailExcel.setLaborPretaxAmount(sumLaborPretax.getOrDefault(key, 0.0)); serviceFunddetailExcel.setTaxamount(sumTax.getOrDefault(key, 0.0)); serviceFunddetailExcel.setTaxedamount(sumTaxdAmount.getOrDefault(key, 0.0)); for (ServiceFunddetail serviceFunddetail2 : serviceFunddetails) { if (serviceFunddetail2.getBeneficiaryname().equals(key)) { ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceFunddetailServiceImpl.java
@@ -13,6 +13,7 @@ import com.ruoyi.common.exception.base.BaseException; import com.ruoyi.common.tax.PerformanceTaxtUtils; import com.ruoyi.common.tax.TaxtUtils; import com.ruoyi.common.tax.VatTaxUtils; import com.ruoyi.common.utils.bean.DtoConversionUtils; import com.ruoyi.project.domain.*; import com.ruoyi.project.domain.vo.*; @@ -253,15 +254,22 @@ Long fundid = serviceFunddetail.getFundid(); ServiceFund serviceFund = serviceFundMapper.selectById(fundid); if (ObjectUtils.isNotEmpty(serviceFund)) { Double pretax = serviceFund.getPretaxcost(); Double aftertax = serviceFund.getTaxedcost(); pretax = pretax - serviceFunddetail.getAmount(); aftertax = aftertax - serviceFunddetail.getTaxedamount(); Double amount = pretax + aftertax; // 金额一律用 BigDecimal 累加/扣减,避免浮点尾差 BigDecimal pretax = toMoney(serviceFund.getPretaxcost()).subtract(toMoney(serviceFunddetail.getAmount())); BigDecimal aftertax = toMoney(serviceFund.getTaxedcost()).subtract(toMoney(serviceFunddetail.getTaxedamount())); BigDecimal vat = toMoney(serviceFund.getVatCost()).subtract(toMoney(serviceFunddetail.getVatAmount())); BigDecimal cityTax = toMoney(serviceFund.getCityTaxCost()).subtract(toMoney(serviceFunddetail.getCityTaxAmount())); BigDecimal laborPretax = toMoney(serviceFund.getLaborPretaxCost()).subtract(toMoney(serviceFunddetail.getLaborPretaxAmount())); BigDecimal tax = toMoney(serviceFund.getTaxcost()).subtract(toMoney(serviceFunddetail.getTaxamount())); BigDecimal amount = pretax.add(aftertax); serviceFund.setPretaxcost(pretax); serviceFund.setTaxedcost(aftertax); serviceFund.setAmountrequested(amount); serviceFund.setPretaxcost(pretax.doubleValue()); serviceFund.setTaxedcost(aftertax.doubleValue()); serviceFund.setVatCost(vat.doubleValue()); serviceFund.setCityTaxCost(cityTax.doubleValue()); serviceFund.setLaborPretaxCost(laborPretax.doubleValue()); serviceFund.setTaxcost(tax.doubleValue()); serviceFund.setAmountrequested(amount.doubleValue()); int updateAmount = serviceFundMapper.updateById(serviceFund); } return serviceFunddetailMapper.deleteById(id); @@ -285,145 +293,228 @@ } /** * 批量算税主方法 * 1. 校验参数 * 2. 排序明细,保证税时间为空的排在后面 * 3. 遍历每条明细,分身份证号为空和不为空两种情况分别处理 * - 身份证号为空:直接按规则赋值税后金额、税金 * - 身份证号不为空: * a. 查询本月第一天,获取最大序号 * b. 查询本月累计税前、税金、税后金额 * c. 根据"税前/税后"类型分别计算税金、税后金额 * d. 设置算税时间、序号等 * 4. 累计本批次总税前、税后金额 * 5. 批量更新所有明细 * 6. 更新主表ServiceFund的总金额、算税时间、状态 * 批量算税主方法(劳务个税 + 增值税 + 城建税,v2 口径) * <p> * 金额模型: * <pre> * 税前录入:V = ceil(P × 1%)、C = ceil(V × 3.5%)、L = P - V - C、 * T = 劳务个税(当月累计 L) - 原累计 T、N = L - T * 税后录入:N累计 = 原累计 N + N、L累计 = getTaxationBefore(N累计)、 * L = L累计 - 原累计 L、T = L - N、P/V/C = solveGross(L) * 无身份证:V = C = T = 0,L = N = P * </pre> * * @param serviceFunddetails 费用明细列表 * @param tempTaxSumMap 本月累计金额缓存(按身份证号) * @return 是否算税成功 */ @Override public Boolean calculateTax(List<ServiceFunddetail> serviceFunddetails, Map<String, TaxMoneySumEO> tempTaxSumMap) { return calculateTax(serviceFunddetails, tempTaxSumMap, new Date()); } /** * 批量算税(指定本批算税时间,保证同批明细 tax_time 一致) * * @param serviceFunddetails 费用明细列表 * @param tempTaxSumMap 本月累计金额缓存(按身份证号) * @param taxTime 本次算税时间 * @return 是否算税成功 */ @Override public Boolean calculateTax(List<ServiceFunddetail> serviceFunddetails, Map<String, TaxMoneySumEO> tempTaxSumMap, Date taxTime) { // 1. 校验参数 if (CollectionUtils.isEmpty(serviceFunddetails)) { throw new BaseException("算税条件为空,请检查后,再进行计算"); } // 2. 排序,税时间为空的排后面 serviceFunddetails.sort(Comparator.comparing(obj -> ObjectUtils.isEmpty(obj.getTaxTime()) ? 1 : 0)); // 2. 排序:税时间为空的排后面,再按 id 保证同批顺序稳定 serviceFunddetails.sort(Comparator .comparing((ServiceFunddetail obj) -> ObjectUtils.isEmpty(obj.getTaxTime()) ? 1 : 0) .thenComparing(obj -> obj.getId() == null ? Long.MAX_VALUE : obj.getId())); // 3. 获取本批次fundid及主表信息 // 3. 获取本批次 fundid 及主表信息 Long fundid = serviceFunddetails.get(0).getFundid(); log.info("算税的fundid的值为:{}", fundid); ServiceFund serviceFund1 = serviceFundMapper.selectById(fundid); log.info("fund表的fundtextime值为:{}", serviceFund1.getFundtaxtime()); // 4. 初始化累计金额、待更新明细列表、序号缓存、累计金额缓存 BigDecimal pretaxcost = BigDecimal.ZERO; BigDecimal taxedcost = BigDecimal.ZERO; if (serviceFund1 == null) { throw new BaseException("未找到费用申请单,ID=" + fundid); } // 明细 tax_time 统一使用本次算税时间 if (taxTime == null) { taxTime = new Date(); } //创建一个用于存放“需要更新到数据库的ServiceFunddetail对象”的列表 // 4. 本批次合计(全部用 BigDecimal,禁止 double 累加) BigDecimal pretaxTotal = BigDecimal.ZERO; BigDecimal vatTotal = BigDecimal.ZERO; BigDecimal cityTaxTotal = BigDecimal.ZERO; BigDecimal laborPretaxTotal = BigDecimal.ZERO; BigDecimal taxTotal = BigDecimal.ZERO; BigDecimal taxedTotal = BigDecimal.ZERO; List<ServiceFunddetail> updateList = new ArrayList<>(); // 5. 遍历每条明细,分情况处理 // 5. 遍历每条明细 for (ServiceFunddetail detail : serviceFunddetails) { // 5.1 身份证号为空,直接赋值(一些人或公司,不需要算税) if (detail.getAmount() == null) { detail.setAmount(0.0); } if (detail.getTaxedamount() == null) { detail.setTaxedamount(0.0); } // 服务说明为空按“非税后”处理 String scopeName = detail.getServicesscopename() == null ? "" : detail.getServicesscopename().trim(); boolean afterTax = scopeName.contains("税后"); // 5.1 身份证号为空:不扣增值税/城建税/劳务个税 if (StringUtils.isEmpty(detail.getIdcardno())) { if (StringUtils.isEmpty(detail.getServicesscopename()) || !detail.getServicesscopename().contains("税后")) { // 普通明细,税后金额=申请金额 detail.setTaxedamount(detail.getAmount()); } else { // 税后明细,申请金额=税后金额 detail.setAmount(detail.getTaxedamount()); } // 累计本批次总金额 pretaxcost = pretaxcost.add(BigDecimal.valueOf(detail.getAmount())); taxedcost = taxedcost.add(BigDecimal.valueOf(detail.getTaxedamount())); BigDecimal p = afterTax ? toMoney(detail.getTaxedamount()) : toMoney(detail.getAmount()); detail.setAmount(p.doubleValue()); detail.setVatAmount(0.0); detail.setCityTaxAmount(0.0); detail.setLaborPretaxAmount(p.doubleValue()); detail.setTaxamount(0.0); detail.setTaxedamount(p.doubleValue()); detail.setTaxVersion(VatTaxUtils.TAX_VERSION_V2); detail.setTaxTime(taxTime); updateList.add(detail); pretaxTotal = pretaxTotal.add(p); laborPretaxTotal = laborPretaxTotal.add(p); taxedTotal = taxedTotal.add(p); continue; } // 5.2 身份证号不为空,需查历史累计金额、序号 // 5.2 身份证号不为空:本月累计 + 分档算税 String idcard = detail.getIdcardno().trim(); // 5.2.1 查询本月累计金额(缓存避免重复查库) TaxMoneySumEO taxSum = tempTaxSumMap.getOrDefault(idcard, null); // 5.2.1 取本月累计(缓存避免重复查库) TaxMoneySumEO taxSum = tempTaxSumMap.get(idcard); if (taxSum == null) { taxSum = new TaxMoneySumEO(); taxSum.setXh(1L); detail.setXh(1L); taxSum.setXh(0L); tempTaxSumMap.put(idcard, taxSum); detail.setAllAmount(taxSum.getAmounts()); } long xh = (taxSum.getXh() == null ? 0L : taxSum.getXh()) + 1; taxSum.setXh(xh); detail.setXh(xh); // 累计税前金额(不含本笔),用于页面展示 detail.setAllAmount(taxSum.getAmounts()); BigDecimal prevAmount = toMoney(taxSum.getAmounts()); BigDecimal prevLaborPretax = toMoney(taxSum.getLaborPretaxAmounts()); BigDecimal prevTax = toMoney(taxSum.getTaxAmounts()); BigDecimal prevTaxed = toMoney(taxSum.getTaxedAmounts()); BigDecimal prevVat = toMoney(taxSum.getVatAmounts()); BigDecimal prevCityTax = toMoney(taxSum.getCityTaxAmounts()); BigDecimal pretax; BigDecimal vat; BigDecimal cityTax; BigDecimal laborPretax; BigDecimal taxAmount; BigDecimal taxedAmount; if (afterTax) { // 5.2.2 税后录入:先反算劳务税前,再循环反解税前/增值税/城建税 taxedAmount = toMoney(detail.getTaxedamount()); BigDecimal sumTaxed = prevTaxed.add(taxedAmount); BigDecimal sumLaborPretax = new BigDecimal(TaxtUtils.getTaxationBefore(sumTaxed)); laborPretax = VatTaxUtils.roundMoney(sumLaborPretax.subtract(prevLaborPretax)); if (laborPretax.signum() < 0) { // 逐笔向上取整可能带来几分钱的反算误差,容差内按 0 处理;超出容差说明数据异常 if (laborPretax.abs().compareTo(new BigDecimal("0.05")) <= 0) { log.warn("税后金额反算劳务税前出现负尾差,已按 0 处理。人员:{},反算劳务税前={},原累计劳务税前={}", safeName(detail), sumLaborPretax, prevLaborPretax); laborPretax = BigDecimal.ZERO; } else { throw new BaseException("税后金额反算劳务税前失败,人员:" + safeName(detail)); } } taxAmount = VatTaxUtils.roundMoney(laborPretax.subtract(taxedAmount)); if (taxAmount.signum() < 0) { taxAmount = BigDecimal.ZERO; } VatTaxUtils.GrossAmount gross = VatTaxUtils.solveGross(laborPretax); pretax = gross.getPretax(); vat = gross.getVat(); cityTax = gross.getCityTax(); } else { long xh = taxSum.getXh() + 1; taxSum.setXh(xh); tempTaxSumMap.put(idcard, taxSum); detail.setXh(xh); detail.setAllAmount(taxSum.getAmounts()); // 5.2.3 税前录入:先算增值税/城建税/劳务税前,再按累计劳务税前算劳务个税 pretax = toMoney(detail.getAmount()); vat = VatTaxUtils.calcVat(pretax); cityTax = VatTaxUtils.calcCityTax(vat); laborPretax = VatTaxUtils.roundMoney(pretax.subtract(vat).subtract(cityTax)); BigDecimal sumLaborPretax = prevLaborPretax.add(laborPretax); BigDecimal sumTax = new BigDecimal(TaxtUtils.getTaxation(sumLaborPretax)); taxAmount = VatTaxUtils.roundMoney(sumTax.subtract(prevTax)); if (taxAmount.signum() < 0) { taxAmount = BigDecimal.ZERO; } taxedAmount = VatTaxUtils.roundMoney(laborPretax.subtract(taxAmount)); } // 5.2.3 算税逻辑 if (detail.getServicesscopename().contains("税后")) { // 税后明细:累计税后金额,反推税前金额和税金 // if (idcard.equals("362329199103224511")) { // log.info("朱玮安"); // } BigDecimal newTaxedAmounts = BigDecimal.valueOf(taxSum.getTaxedAmounts()).add(BigDecimal.valueOf(detail.getTaxedamount())); String taxationBefore = TaxtUtils.getTaxationBefore(newTaxedAmounts); BigDecimal amountNow = new BigDecimal(taxationBefore).subtract(BigDecimal.valueOf(taxSum.getAmounts())); BigDecimal tax = amountNow.subtract(BigDecimal.valueOf(detail.getTaxedamount())); detail.setAmount(amountNow.doubleValue()); detail.setTaxamount(tax.doubleValue()); if (ObjectUtils.isEmpty(serviceFund1.getFundtaxtime())) { detail.setTaxTime(new Date()); } else { detail.setTaxTime(serviceFund1.getFundtaxtime()); } taxSum.setAmounts((BigDecimal.valueOf(taxSum.getAmounts()).add(amountNow)).doubleValue()); taxSum.setTaxAmounts((BigDecimal.valueOf(taxSum.getTaxAmounts()).add(tax)).doubleValue()); taxSum.setTaxedAmounts(newTaxedAmounts.doubleValue()); } else { // 税前明细:累计税前金额,算总税金,再算本次税金和税后金额 BigDecimal newAmounts = BigDecimal.valueOf(taxSum.getAmounts()).add(BigDecimal.valueOf(detail.getAmount())); // if (idcard.equals("362329199103224511")) { // log.info("朱玮安"); // } String taxation = TaxtUtils.getTaxation(newAmounts); BigDecimal taxAmountNow = new BigDecimal(taxation).subtract(new BigDecimal(String.valueOf(taxSum.getTaxAmounts()))); BigDecimal texdAfterNow = BigDecimal.valueOf(detail.getAmount()).subtract(taxAmountNow); detail.setTaxamount(taxAmountNow.doubleValue()); detail.setTaxedamount(texdAfterNow.doubleValue()); if (ObjectUtils.isEmpty(serviceFund1.getFundtaxtime())) { detail.setTaxTime(new Date()); } else { detail.setTaxTime(serviceFund1.getFundtaxtime()); } taxSum.setAmounts(newAmounts.doubleValue()); taxSum.setTaxAmounts((BigDecimal.valueOf(taxSum.getTaxAmounts()).add(taxAmountNow).doubleValue())); taxSum.setTaxedAmounts((BigDecimal.valueOf(taxSum.getTaxedAmounts()).add(texdAfterNow)).doubleValue()); } detail.setAmount(pretax.doubleValue()); detail.setVatAmount(vat.doubleValue()); detail.setCityTaxAmount(cityTax.doubleValue()); detail.setLaborPretaxAmount(laborPretax.doubleValue()); detail.setTaxamount(taxAmount.doubleValue()); detail.setTaxedamount(taxedAmount.doubleValue()); detail.setTaxVersion(VatTaxUtils.TAX_VERSION_V2); detail.setTaxTime(taxTime); updateList.add(detail); pretaxcost = pretaxcost.add(BigDecimal.valueOf(detail.getAmount())); taxedcost = taxedcost.add(BigDecimal.valueOf(detail.getTaxedamount())); // 5.2.4 更新本月累计 taxSum.setAmounts(prevAmount.add(pretax).doubleValue()); taxSum.setVatAmounts(prevVat.add(vat).doubleValue()); taxSum.setCityTaxAmounts(prevCityTax.add(cityTax).doubleValue()); taxSum.setLaborPretaxAmounts(prevLaborPretax.add(laborPretax).doubleValue()); taxSum.setTaxAmounts(prevTax.add(taxAmount).doubleValue()); taxSum.setTaxedAmounts(prevTaxed.add(taxedAmount).doubleValue()); pretaxTotal = pretaxTotal.add(pretax); vatTotal = vatTotal.add(vat); cityTaxTotal = cityTaxTotal.add(cityTax); laborPretaxTotal = laborPretaxTotal.add(laborPretax); taxTotal = taxTotal.add(taxAmount); taxedTotal = taxedTotal.add(taxedAmount); } // 6. 批量更新所有明细 for (ServiceFunddetail d : updateList) { this.updateById(d); } // 7. 更新主表ServiceFund ServiceFund serviceFund = new ServiceFund(); serviceFund.setPretaxcost(pretaxcost.doubleValue()); serviceFund.setTaxedcost(taxedcost.doubleValue()); serviceFund.setId(serviceFunddetails.get(0).getFundid()); serviceFund.setFundtaxtime(new Date()); // 7. 更新主表 service_fund 的 6 个金额、算税时间、状态、版本 ServiceFund serviceFund = new ServiceFund(); serviceFund.setId(fundid); serviceFund.setPretaxcost(pretaxTotal.doubleValue()); serviceFund.setVatCost(vatTotal.doubleValue()); serviceFund.setCityTaxCost(cityTaxTotal.doubleValue()); serviceFund.setLaborPretaxCost(laborPretaxTotal.doubleValue()); serviceFund.setTaxcost(taxTotal.doubleValue()); serviceFund.setTaxedcost(taxedTotal.doubleValue()); serviceFund.setTaxVersion(VatTaxUtils.TAX_VERSION_V2); serviceFund.setFundtaxtime(taxTime); serviceFund.setIstax(1); serviceFundMapper.updateById(serviceFund); return true; } /** * null 安全的金额转换 */ private static BigDecimal toMoney(Double value) { return value == null ? BigDecimal.ZERO : BigDecimal.valueOf(value); } /** * 报错时能定位到具体人员 */ private static String safeName(ServiceFunddetail detail) { String name = detail.getBeneficiaryname() == null ? "" : detail.getBeneficiaryname(); String idcard = detail.getIdcardno() == null ? "" : detail.getIdcardno(); return name + "(" + idcard + ")"; } // @Override @@ -493,6 +584,10 @@ //将计算出来的数据放到serviceFunddetail中 serviceFunddetail.setTaxamount(nowSingleTax.doubleValue()); serviceFunddetail.setTaxedamount(taxAfter.doubleValue()); // 绩效不属于专家费:不计算增值税/城建税,劳务税前 = 税前金额 serviceFunddetail.setVatAmount(0.0); serviceFunddetail.setCityTaxAmount(0.0); serviceFunddetail.setLaborPretaxAmount(serviceFunddetail.getAmount()); serviceFunddetail.setJxrq(nowMonth); save(serviceFunddetail); ruoyi-project/src/main/java/com/ruoyi/project/service/impl/ServiceFundtaxServiceImpl.java
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.common.exception.base.BaseException; import com.ruoyi.common.tax.VatTaxUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.project.domain.ServiceFund; import com.ruoyi.project.domain.ServiceFunddetail; @@ -23,6 +24,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDate; import java.time.ZoneId; import java.time.temporal.TemporalAdjusters; @@ -171,66 +173,128 @@ /** * 批量算税主入口 * 1. 校验参数 * 2. 校验批次合法性(如有必要) * 3. 查询本批次所有ServiceFund(按id升序) * 4. 预查询本月所有身份证号的累计金额,缓存到Map,提升后续算税效率 * 5. 遍历每个ServiceFund,批量算税 * 2. 只允许最新批次算税 * 3. 对当前批次 service_fundtax 加行锁,防并发重复算税 * 4. 查询本批次所有 ServiceFund(按 id 升序),并重置 istax * 5. 预查询本月所有身份证号的累计金额,缓存到 Map * 6. 校验当月是否存在历史口径(v1)算税记录,防止新旧口径混算 * 7. 遍历每个 ServiceFund 逐单算税,汇总后回写 service_fundtax * <p> * 整批在一个事务内完成:任一条失败则整批回滚,不会出现半批数据。 * * @param fundTaxVO 批次算税参数 * @return 是否算税成功 */ @Override @Transactional(rollbackFor = Exception.class) public Boolean batchFundTax(FundTaxVO fundTaxVO) { log.info("批量算税的fundTaxVO入参为:{}", fundTaxVO); // 1. 校验参数 if (fundTaxVO.getFundTaxId() == null) { throw new BaseException("分批算税出问题了,请检查后再进行计算"); } // 2. 校验批次合法性(仅限修改操作) // 2. 只允许最新批次算税 if (fundTaxVO.getAddOrupdate() == 1) { Integer maxFundTaxId = serviceFundtaxMapper.getMaxFundTaxId(); log.info("批量算税的maxFundTaxId为:{}", maxFundTaxId); if (!fundTaxVO.getFundTaxId().equals(Long.valueOf(maxFundTaxId))) { if (maxFundTaxId == null || !fundTaxVO.getFundTaxId().equals(Long.valueOf(maxFundTaxId))) { throw new BaseException("该批数据不能算税"); } } // 3. 查询本批次所有ServiceFund(按id升序) // 3. 对当前批次汇总单加行锁(事务内有效),防止并发重复算税 ServiceFundtax fundtax = serviceFundtaxMapper.selectByIdForUpdate(fundTaxVO.getFundTaxId()); if (fundtax == null) { throw new BaseException("未找到算税汇总单,ID=" + fundTaxVO.getFundTaxId()); } // 4. 查询本批次所有 ServiceFund(按 id 升序) ServiceFund serviceFund = new ServiceFund(); serviceFund.setFundTaxId(fundTaxVO.getFundTaxId()); serviceFund.setDel_flag(0); List<ServiceFund> serviceFunds = serviceFundService.selectServiceFundList(serviceFund); for (ServiceFund serviceFund1 : serviceFunds) { serviceFundMapper.updateFundIsTexById(serviceFund1.getId(), 0L); } // 按id升序排序,保证处理顺序一致 serviceFunds.sort(Comparator.comparing(ServiceFund::getId)); if (CollectionUtils.isEmpty(serviceFunds)) { log.warn("未查询到相关ServiceFund,fundTaxId={}", fundTaxVO.getFundTaxId()); return false; } // 重置本批 istax 与算税时间,使本月累计查询不会重复计入本批 for (ServiceFund serviceFund1 : serviceFunds) { serviceFundMapper.updateFundIsTexById(serviceFund1.getId(), 0L); } // 按 id 升序排序,保证处理顺序一致 serviceFunds.sort(Comparator.comparing(ServiceFund::getId)); // 4. 预查询本月所有身份证号的累计金额,缓存到Map,提升后续算税效率 TaxMoneyByItemEO taxMoneyVO = new TaxMoneyByItemEO(); // 5. 预查询本月所有身份证号的累计金额,缓存到 Map,提升后续算税效率 Date taxTime = new Date(); Date firstDay = Date.from(LocalDate.now().with(TemporalAdjusters.firstDayOfMonth()).atStartOfDay(ZoneId.systemDefault()).toInstant()); TaxMoneyByItemEO taxMoneyVO = new TaxMoneyByItemEO(); taxMoneyVO.setFirstDay(firstDay); taxMoneyVO.setTaxTime(new Date()); taxMoneyVO.setTaxTime(taxTime); Map<String, TaxMoneySumEO> tempTaxSumMap = new HashMap<>(); List<TaxMoneySumEO> taxSum = serviceFunddetailMapper.getTaxSum(taxMoneyVO); for (TaxMoneySumEO taxMoneySumEO : taxSum) { tempTaxSumMap.put(taxMoneySumEO.getIDCardNo(), taxMoneySumEO); if (CollectionUtils.isNotEmpty(taxSum)) { for (TaxMoneySumEO taxMoneySumEO : taxSum) { tempTaxSumMap.put(taxMoneySumEO.getIDCardNo(), taxMoneySumEO); // 6. 当月已存在的历史口径(v1)记录会让累计基数新旧混算,直接阻止 if (taxMoneySumEO.getV1Count() != null && taxMoneySumEO.getV1Count() > 0) { throw new BaseException("当月已存在旧口径(v1)算税记录,为避免新旧口径混算," + "本月请继续使用旧口径或从下个月起再按新口径算税。人员身份证号:" + taxMoneySumEO.getIDCardNo()); } } } // 5. 遍历每个ServiceFund,批量算税 // 7. 遍历每个 ServiceFund,逐单算税并汇总 BigDecimal pretaxTotal = BigDecimal.ZERO; BigDecimal vatTotal = BigDecimal.ZERO; BigDecimal cityTaxTotal = BigDecimal.ZERO; BigDecimal laborPretaxTotal = BigDecimal.ZERO; BigDecimal taxTotal = BigDecimal.ZERO; BigDecimal taxedTotal = BigDecimal.ZERO; for (ServiceFund serviceFund1 : serviceFunds) { ServiceFunddetail serviceFunddetail = new ServiceFunddetail(); serviceFunddetail.setFundid(serviceFund1.getId()); List<ServiceFunddetail> serviceFunddetails = serviceFunddetailMapper.selectServiceFunddetailList(serviceFunddetail); // 传入累计金额缓存,提升算税效率 serviceFunddetailService.calculateTax(serviceFunddetails, tempTaxSumMap); if (CollectionUtils.isEmpty(serviceFunddetails)) { throw new BaseException("单据没有可算税的明细,单据ID=" + serviceFund1.getId() + ",捐献案例=" + serviceFund1.getDonorname()); } // 传入累计金额缓存与统一算税时间 serviceFunddetailService.calculateTax(serviceFunddetails, tempTaxSumMap, taxTime); for (ServiceFunddetail detail : serviceFunddetails) { pretaxTotal = pretaxTotal.add(toMoney(detail.getAmount())); vatTotal = vatTotal.add(toMoney(detail.getVatAmount())); cityTaxTotal = cityTaxTotal.add(toMoney(detail.getCityTaxAmount())); laborPretaxTotal = laborPretaxTotal.add(toMoney(detail.getLaborPretaxAmount())); taxTotal = taxTotal.add(toMoney(detail.getTaxamount())); taxedTotal = taxedTotal.add(toMoney(detail.getTaxedamount())); } } // 8. 回写汇总单 service_fundtax(6 个金额 + 算税状态 + 版本) ServiceFundtax updateFundtax = new ServiceFundtax(); updateFundtax.setId(fundTaxVO.getFundTaxId()); updateFundtax.setPretaxamount(VatTaxUtils.roundMoney(pretaxTotal)); updateFundtax.setVatAmount(VatTaxUtils.roundMoney(vatTotal)); updateFundtax.setCityTaxAmount(VatTaxUtils.roundMoney(cityTaxTotal)); updateFundtax.setLaborPretaxAmount(VatTaxUtils.roundMoney(laborPretaxTotal)); updateFundtax.setTaxamount(VatTaxUtils.roundMoney(taxTotal)); updateFundtax.setTaxMoney(VatTaxUtils.roundMoney(taxTotal)); updateFundtax.setTaxedamount(VatTaxUtils.roundMoney(taxedTotal)); updateFundtax.setTaxVersion(VatTaxUtils.TAX_VERSION_V2); updateFundtax.setIstax(1); updateFundtax.setTaxedtime(taxTime); serviceFundtaxMapper.updateById(updateFundtax); log.info("批量算税完成,fundTaxId={},税前={},增值税={},城建税={},劳务税前={},劳务个税={},劳务税后={}", fundTaxVO.getFundTaxId(), pretaxTotal, vatTotal, cityTaxTotal, laborPretaxTotal, taxTotal, taxedTotal); return true; } /** * null 安全的金额转换 */ private static BigDecimal toMoney(Double value) { return value == null ? BigDecimal.ZERO : BigDecimal.valueOf(value); } } ruoyi-project/src/main/resources/mapper/project/ServiceFundMapper.xml
@@ -51,6 +51,10 @@ <result property="uploadtime" column="uploadtime"/> <result property="flowlevel" column="FlowLevel"/> <result property="pretaxcost" column="PretaxCost"/> <result property="vatCost" column="vat_cost"/> <result property="cityTaxCost" column="city_tax_cost"/> <result property="laborPretaxCost" column="labor_pretax_cost"/> <result property="taxVersion" column="tax_version"/> <result property="taxedcost" column="TaxedCost"/> <result property="backflowlevel" column="backFlowLevel"/> <result property="zffs" column="zffs"/> @@ -87,6 +91,10 @@ UnitNo, UnitUserNo, PretaxCost, vat_cost, city_tax_cost, labor_pretax_cost, tax_version, TaxedCost, Phone, AnnexBankCard, ruoyi-project/src/main/resources/mapper/project/ServiceFunddetailMapper.xml
@@ -37,6 +37,10 @@ <result property="amount" column="Amount"/> <result property="taxamount" column="TaxAmount"/> <result property="taxedamount" column="TaxedAmount"/> <result property="vatAmount" column="vat_amount"/> <result property="cityTaxAmount" column="city_tax_amount"/> <result property="laborPretaxAmount" column="labor_pretax_amount"/> <result property="taxVersion" column="tax_version"/> <result property="prepaidamount" column="PrepaidAmount"/> <result property="invoicecount" column="InvoiceCount"/> <result property="attachcount" column="AttachCount"/> @@ -108,6 +112,10 @@ TaxAmount, InvoiceCount, TaxedAmount, vat_amount, city_tax_amount, labor_pretax_amount, tax_version, AttachCount, Quantity, Price, @@ -232,6 +240,10 @@ a.TaxAmount, a.InvoiceCount, a.TaxedAmount, a.vat_amount, a.city_tax_amount, a.labor_pretax_amount, a.tax_version, a.AttachCount, a.Quantity, a.Price, @@ -281,7 +293,15 @@ <select id="getTaxSum" parameterType="com.ruoyi.project.domain.vo.TaxMoneyByItemEO" resultType="com.ruoyi.project.domain.vo.TaxMoneySumEO"> SELECT sum(a.Amount) amounts, sum(a.TaxAmount) taxAmounts, sum(a.TaxedAmount) taxedAmounts, max(a.xh) as xh,a.IDCardNo SELECT sum(a.Amount) amounts, sum(COALESCE(a.labor_pretax_amount, a.Amount)) laborPretaxAmounts, sum(COALESCE(a.TaxAmount, 0)) taxAmounts, sum(COALESCE(a.TaxedAmount, 0)) taxedAmounts, sum(COALESCE(a.vat_amount, 0)) vatAmounts, sum(COALESCE(a.city_tax_amount, 0)) cityTaxAmounts, sum(CASE WHEN a.tax_version = 'v1' THEN 1 ELSE 0 END) v1Count, max(a.xh) xh, a.IDCardNo FROM service_funddetail a, service_fund b where a.FundID = b.id ruoyi-project/src/main/resources/mapper/project/ServiceFunddetailSharedMapper.xml
@@ -37,6 +37,9 @@ <result property="amount" column="Amount"/> <result property="taxamount" column="TaxAmount"/> <result property="taxedamount" column="TaxedAmount"/> <result property="vatAmount" column="vat_amount"/> <result property="cityTaxAmount" column="city_tax_amount"/> <result property="laborPretaxAmount" column="labor_pretax_amount"/> <result property="prepaidamount" column="PrepaidAmount"/> <result property="paidamount" column="paidAmount"/> <result property="paidtime" column="paidTime"/> @@ -96,6 +99,9 @@ Amount, TaxAmount, TaxedAmount, vat_amount, city_tax_amount, labor_pretax_amount, PrepaidAmount, paidAmount, paidTime, ruoyi-project/src/main/resources/mapper/project/ServiceFundtaxMapper.xml
@@ -15,6 +15,10 @@ <result property="managerno" column="ManagerNo"/> <result property="managername" column="ManagerName"/> <result property="pretaxamount" column="Pretaxamount"/> <result property="vatAmount" column="vat_amount"/> <result property="cityTaxAmount" column="city_tax_amount"/> <result property="laborPretaxAmount" column="labor_pretax_amount"/> <result property="taxVersion" column="tax_version"/> <result property="taxedamount" column="TaxedAmount"/> <result property="taxamount" column="TaxAmount"/> <result property="medicalcost" column="MedicalCost"/> @@ -60,6 +64,10 @@ Pretaxamount, TaxedAmount, TaxAmount, vat_amount, city_tax_amount, labor_pretax_amount, tax_version, MedicalCost, BodyMaintainCost, OrganGainCost, @@ -143,4 +151,10 @@ where del_flag = 0; </select> <select id="selectByIdForUpdate" resultMap="ServiceFundtaxResult"> <include refid="selectServiceFundtaxVo"/> where ID = #{id} for update </select> </mapper>