diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayBillController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayBillController.java index 0de22d4d..5bac68c6 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayBillController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayBillController.java @@ -1,8 +1,14 @@ package com.bms.web.controller.price; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.List; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -15,6 +21,7 @@ import com.bms.common.core.domain.AjaxResult; import com.bms.common.core.page.TableDataInfo; import com.bms.price.domain.PayBill; import com.bms.price.domain.PayBillCommand; +import com.bms.price.domain.PayBillFile; import com.bms.price.service.IPayBillService; /** @@ -119,4 +126,176 @@ public class PayBillController extends BaseController payBillService.voidBill(command); return success(); } + + /** + * 草稿提交审核。 + * + * @param billId 应付账单ID + */ + @PreAuthorize("@ss.hasAnyPermi('price:payBill:submit,price:payBill:confirm')") + @PutMapping("/submit/{billId}") + public AjaxResult submit(@PathVariable Long billId) + { + payBillService.submit(billId); + return success(); + } + + /** + * 审核通过。蓝字账单需要先有凭证。 + * + * @param billId 应付账单ID + */ + @PreAuthorize("@ss.hasAnyPermi('price:payBill:approve,price:payBill:confirm')") + @PutMapping("/approve/{billId}") + public AjaxResult approve(@PathVariable Long billId) + { + payBillService.approve(billId); + return success(); + } + + /** + * 审核驳回。 + * + * @param command 账单ID和驳回意见 + */ + @PreAuthorize("@ss.hasAnyPermi('price:payBill:approve,price:payBill:confirm')") + @PutMapping("/reject") + public AjaxResult reject(@RequestBody PayBillCommand command) + { + payBillService.reject(command); + return success(); + } + + /** + * 草稿登记对方金额。 + * + * @param command 账单和费用行 + */ + @PreAuthorize("@ss.hasAnyPermi('price:payBill:add,price:payBill:submit')") + @PutMapping("/diff") + public AjaxResult diff(@RequestBody PayBillCommand command) + { + payBillService.saveSupplierAmounts(command); + return success(); + } + + /** + * 接受我方或接受对方金额。 + * + * @param command 费用行和处理方式 + */ + @PreAuthorize("@ss.hasAnyPermi('price:payBill:add,price:payBill:submit')") + @PutMapping("/diff/accept") + public AjaxResult accept(@RequestBody PayBillCommand command) + { + payBillService.acceptDiff(command); + return success(); + } + + /** + * 从草稿剔除一条费用。 + * + * @param command 账单ID和行ID + */ + @PreAuthorize("@ss.hasAnyPermi('price:payBill:add,price:payBill:submit')") + @PutMapping("/line/remove") + public AjaxResult removeLine(@RequestBody PayBillCommand command) + { + payBillService.removeLine(command); + return success(); + } + + /** + * 上传应付凭证。 + * + * @param file 凭证 + */ + @PreAuthorize("@ss.hasAnyPermi('price:payVoucher:add,price:payBill:add')") + @PostMapping("/file") + public AjaxResult saveFile(@RequestBody PayBillFile file) + { + payBillService.saveFile(file); + return success(); + } + + /** + * 删除应付凭证。 + * + * @param fileId 凭证ID + */ + @PreAuthorize("@ss.hasAnyPermi('price:payVoucher:remove,price:payBill:void')") + @DeleteMapping("/file/{fileId}") + public AjaxResult deleteFile(@PathVariable Long fileId) + { + payBillService.deleteFile(fileId); + return success(); + } + + /** + * 查询应付凭证。 + * + * @param query 账单号、供应商、发票号、类型 + */ + @PreAuthorize("@ss.hasAnyPermi('price:payVoucher:list,price:payBill:query')") + @GetMapping("/file/list") + public TableDataInfo fileList(PayBillFile query) + { + startPage(); + return getDataTable(payBillService.listFiles(query)); + } + + /** + * 对未付款的已确认账单开红字。 + * + * @param command 原账单和原因 + */ + @PreAuthorize("@ss.hasAnyPermi('price:payBill:approve,price:payBill:confirm')") + @PostMapping("/red") + public AjaxResult red(@RequestBody PayBillCommand command) + { + return success(payBillService.redBill(command)); + } + + /** + * 草稿保存税率和汇率。 + * + * @param command 账单、税率、汇率和汇率日期 + */ + @PreAuthorize("@ss.hasAnyPermi('price:payBill:add,price:payBill:submit')") + @PutMapping("/finance") + public AjaxResult finance(@RequestBody PayBillCommand command) + { + payBillService.saveFinance(command); + return success(); + } + + /** + * 导出对账单。 + * + * @param billId 应付账单ID + * @param response 文件流 + */ + @PreAuthorize("@ss.hasPermi('price:payBill:query')") + @GetMapping("/statement/{billId}") + public void statement(@PathVariable Long billId, HttpServletResponse response) throws Exception + { + String name = URLEncoder.encode("应付对账单.xlsx", StandardCharsets.UTF_8).replace("+", "%20"); + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + name); + payBillService.exportStatement(billId, response.getOutputStream()); + } + + /** + * 导入对方金额。 + * + * @param billId 应付账单ID + * @param file 导出的对账单 + */ + @PreAuthorize("@ss.hasAnyPermi('price:payBill:add,price:payBill:submit')") + @PostMapping("/diff/import") + public AjaxResult importDiff(@RequestParam Long billId, @RequestParam("file") MultipartFile file) + { + payBillService.importSupplierAmounts(billId, file); + return success(); + } } diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayPaymentController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayPaymentController.java new file mode 100644 index 00000000..5c8f9379 --- /dev/null +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayPaymentController.java @@ -0,0 +1,137 @@ +package com.bms.web.controller.price; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.bms.common.core.controller.BaseController; +import com.bms.common.core.domain.AjaxResult; +import com.bms.common.core.page.TableDataInfo; +import com.bms.price.domain.PayPayment; +import com.bms.price.domain.PayPaymentAlloc; +import com.bms.price.service.IPayPaymentService; + +/** + * 账单管理下的应付付款。 + */ +@RestController +@RequestMapping("/price/payPayment") +public class PayPaymentController extends BaseController +{ + @Autowired + private IPayPaymentService payPaymentService; + + /** + * 分页查询应付付款。 + * + * @param query 付款单号、供应商、状态、币种 + * @return 付款单列表 + */ + @PreAuthorize("@ss.hasPermi('price:payPayment:list')") + @GetMapping("/list") + public TableDataInfo list(PayPayment query) + { + startPage(); + List list = payPaymentService.selectPayPaymentList(query); + return getDataTable(list); + } + + /** + * 付款单详情,含核销明细。 + * + * @param paymentId 付款单ID + * @return 付款单 + */ + @PreAuthorize("@ss.hasPermi('price:payPayment:query')") + @GetMapping("/{paymentId}") + public AjaxResult detail(@PathVariable Long paymentId) + { + return success(payPaymentService.selectPayPaymentDetail(paymentId)); + } + + /** + * 查询这个供应商、这个币种下还能核销的账单。 + * + * @param supplierId 供应商ID + * @param currency 币种 + * @return 未付账单 + */ + @PreAuthorize("@ss.hasPermi('price:payPayment:query')") + @GetMapping("/open") + public AjaxResult open(Long supplierId, String currency) + { + List list = payPaymentService.selectOpenBills(supplierId, currency); + return success(list); + } + + /** + * 登记草稿付款。确认后才核销账单。 + * + * @param payment 付款单和核销明细 + * @return 付款单 + */ + @PreAuthorize("@ss.hasPermi('price:payPayment:add')") + @PostMapping + public AjaxResult create(@RequestBody PayPayment payment) + { + return success(payPaymentService.create(payment)); + } + + /** + * 确认付款并核销。 + * + * @param paymentId 付款单ID + */ + @PreAuthorize("@ss.hasPermi('price:payPayment:confirm')") + @PutMapping("/confirm/{paymentId}") + public AjaxResult confirm(@PathVariable Long paymentId) + { + payPaymentService.confirm(paymentId); + return success(); + } + + /** + * 作废草稿付款。已确认的付款不在这里作废。 + * + * @param paymentId 付款单ID + */ + @PreAuthorize("@ss.hasPermi('price:payPayment:void')") + @PutMapping("/void/{paymentId}") + public AjaxResult voidPayment(@PathVariable Long paymentId) + { + payPaymentService.voidPayment(paymentId); + return success(); + } + + /** + * 反核销。账单未付金额恢复。 + * + * @param paymentId 付款单ID + */ + @PreAuthorize("@ss.hasPermi('price:payPayment:confirm')") + @PutMapping("/reverse/{paymentId}") + public AjaxResult reverse(@PathVariable Long paymentId) + { + payPaymentService.reverse(paymentId); + return success(); + } + + /** + * 退款。冲回核销,付款单改为已退款。 + * + * @param paymentId 付款单ID + */ + @PreAuthorize("@ss.hasPermi('price:payPayment:confirm')") + @PutMapping("/refund/{paymentId}") + public AjaxResult refund(@PathVariable Long paymentId) + { + payPaymentService.refund(paymentId); + return success(); + } +} diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayProductController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayProductController.java index 93b78133..1fdc8e99 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayProductController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/PayProductController.java @@ -30,8 +30,10 @@ import com.bms.price.domain.PayProduct; import com.bms.price.domain.PayProductTerm; import com.bms.price.domain.PaySaveCommand; import com.bms.price.pay.PayDetailExporter; +import com.bms.price.domain.PaySurchargeRuleSave; import com.bms.price.service.IPayOrderCostService; import com.bms.price.service.IPayProductService; +import com.bms.price.service.IPaySurchargeRuleService; import jakarta.servlet.http.HttpServletResponse; /** @@ -47,6 +49,9 @@ public class PayProductController extends BaseController @Autowired private IPayOrderCostService payOrderCostService; + @Autowired + private IPaySurchargeRuleService paySurchargeRuleService; + @PreAuthorize("@ss.hasPermi('price:pay:list')") @GetMapping("/list") public TableDataInfo list(PayProduct query) @@ -102,6 +107,31 @@ public class PayProductController extends BaseController payProductService.writeSource(productId, response.getOutputStream()); } + /** 当前版本的附加费计费条件。同一版本只有一份。 */ + @PreAuthorize("@ss.hasAnyPermi('price:pay:query,price:pay:edit')") + @GetMapping("/surchargeRule") + public AjaxResult surchargeRule(@RequestParam Long productId, @RequestParam Long termId) + { + return success(paySurchargeRuleService.list(productId, termId)); + } + + /** 上一版条件,复制到当前页后再保存。 */ + @PreAuthorize("@ss.hasAnyPermi('price:pay:query,price:pay:edit')") + @GetMapping("/surchargeRule/previous") + public AjaxResult previousSurchargeRule(@RequestParam Long productId) + { + return success(paySurchargeRuleService.previous(productId)); + } + + @PreAuthorize("@ss.hasPermi('price:pay:edit')") + @Log(title = "应付附加费计费条件", businessType = BusinessType.UPDATE) + @PostMapping("/surchargeRule") + public AjaxResult saveSurchargeRule(@RequestBody PaySurchargeRuleSave command) + { + paySurchargeRuleService.save(command); + return success(); + } + @PreAuthorize("@ss.hasPermi('price:pay:query')") @GetMapping("/{productId}") public AjaxResult getInfo(@PathVariable Long productId) diff --git a/bms-back/bms-admin/src/test/java/com/bms/price/pay/PaySurchargeConfigCalcTest.java b/bms-back/bms-admin/src/test/java/com/bms/price/pay/PaySurchargeConfigCalcTest.java new file mode 100644 index 00000000..4be0c8f2 --- /dev/null +++ b/bms-back/bms-admin/src/test/java/com/bms/price/pay/PaySurchargeConfigCalcTest.java @@ -0,0 +1,172 @@ +package com.bms.price.pay; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.bms.price.domain.PayOrderCostResult; +import com.bms.price.domain.PayProduct; +import com.bms.price.domain.PaySurcharge; +import com.bms.price.domain.PaySurchargeRule; +import com.bms.price.domain.PayWeightBand; + +/** 用 DE-iMile 价卡和附加费配置,按测试订单的重量、尺寸、日期测算。 */ +class PaySurchargeConfigCalcTest +{ + @Test + void lightOctoberOrderChargesFreightAndCarbonOnly() + { + PayOrderCostResult result = PayCostCalculator.calculate(imile(), new BigDecimal("0.451"), + new BigDecimal("35.70"), new BigDecimal("32.80"), new BigDecimal("4.00"), + null, null, day(2026, 10, 17), 8); + assertTrue(hasFee(result, "基础运费", "2.99")); + assertTrue(hasFee(result, "CO₂附加费", "0.03")); + assertTrue(result.getFees().stream().noneMatch(fee -> fee.getFeeName().contains("超尺寸"))); + assertTrue(result.getFees().stream().noneMatch(fee -> fee.getFeeName().contains("旺季"))); + assertEquals(new BigDecimal("3.02"), result.getPayTotal()); + } + + @Test + void heavierOctoberOrderUsesTheNextWeightBand() + { + PayOrderCostResult result = PayCostCalculator.calculate(imile(), new BigDecimal("1.246"), + new BigDecimal("41.10"), new BigDecimal("34.20"), new BigDecimal("6.10"), + null, null, day(2026, 10, 17), 5); + assertTrue(hasFee(result, "基础运费", "3.12")); + assertTrue(hasFee(result, "CO₂附加费", "0.03")); + assertEquals(new BigDecimal("3.15"), result.getPayTotal()); + } + + @Test + void oversizeAndPeakSeasonUseTheConfiguredRules() + { + PayOrderCostResult oversize = PayCostCalculator.calculate(imile(), new BigDecimal("1.246"), + new BigDecimal("130"), new BigDecimal("70"), new BigDecimal("40"), + null, null, day(2026, 11, 12), 5); + assertTrue(hasFee(oversize, "基础运费", "3.12")); + assertTrue(hasFee(oversize, "CO₂附加费", "0.03")); + assertTrue(hasFee(oversize, "异形件/超尺寸处理费", "3.60")); + assertTrue(hasFee(oversize, "旺季附加费", "0.19")); + assertEquals(new BigDecimal("6.94"), oversize.getPayTotal()); + + PayOrderCostResult peak = PayCostCalculator.calculate(dhl(), new BigDecimal("0.664"), + new BigDecimal("25.50"), new BigDecimal("18.90"), new BigDecimal("7.80"), + null, null, day(2026, 11, 4), 11); + assertTrue(hasFee(peak, "基础运费", "3.13")); + assertTrue(hasFee(peak, "燃油附加费", "0.15")); + assertTrue(hasFee(peak, "旺季附加费", "0.19")); + assertTrue(peak.getFees().stream().noneMatch(fee -> "高峰旺季附加费".equals(fee.getFeeName()))); + assertTrue(peak.getFees().stream().noneMatch(fee -> fee.getFeeName().contains("超尺寸"))); + } + + private static boolean hasFee(PayOrderCostResult result, String name, String amount) + { + return result.getFees().stream().anyMatch(fee -> name.equals(fee.getFeeName()) + && fee.getAmount().compareTo(new BigDecimal(amount)) == 0); + } + + private static Date day(int year, int month, int day) + { + return Date.from(LocalDate.of(year, month, day).atStartOfDay(ZoneId.systemDefault()).toInstant()); + } + + private static PayProduct imile() + { + PayProduct product = new PayProduct(); + product.setProductName("DE-iMile"); + product.setCurrency("EUR"); + product.setWeightBands(List.of( + band("0", "1", "2.99"), + band("1", "2", "3.12"))); + product.setSurcharges(List.of( + fee("CO₂附加费", "0.03", "ALWAYS"), + fee("异形件/超尺寸处理费", "3.60", "SIZE"), + fee("旺季附加费", "0.19", "ALWAYS"))); + product.setBillingRules(List.of( + always(null, null, null), + always("旺季附加费", "11-01", "12-31"), + sizeBox())); + return product; + } + + private static PayProduct dhl() + { + PayProduct product = new PayProduct(); + product.setProductName("德国DHL"); + product.setCurrency("EUR"); + product.setWeightBands(List.of(band("0", "1", "3.13"))); + product.setSurcharges(List.of( + fee("燃油附加费", "0.15", "ALWAYS"), + fee("超尺寸超重费", "14.00", "SIZE"), + fee("旺季附加费", "0.19", "ALWAYS"), + fee("高峰旺季附加费", "0.50", "ALWAYS"))); + product.setBillingRules(List.of( + always(null, null, null), + always("旺季附加费", "11-01", "12-31"), + always("高峰旺季附加费", "11-24", "12-07"), + sizeBox())); + return product; + } + + private static PaySurchargeRule always(String feeName, String start, String end) + { + PaySurchargeRule rule = new PaySurchargeRule(); + rule.setCalcRule("ALWAYS"); + rule.setEnabled("1"); + Map detail = new LinkedHashMap<>(); + if (feeName != null) + { + detail.put("feeName", feeName); + detail.put("periodStart", start); + detail.put("periodEnd", end); + } + rule.setDetail(detail); + return rule; + } + + private static PaySurchargeRule sizeBox() + { + PaySurchargeRule rule = new PaySurchargeRule(); + rule.setCalcRule("SIZE"); + rule.setEnabled("1"); + Map detail = new LinkedHashMap<>(); + detail.put("matchMode", "ANY"); + detail.put("boxMode", "OVER"); + detail.put("boxLength", new BigDecimal("120")); + detail.put("boxWidth", new BigDecimal("60")); + detail.put("boxHeight", new BigDecimal("60")); + rule.setDetail(detail); + return rule; + } + + private static PayWeightBand band(String min, String max, String price) + { + PayWeightBand band = new PayWeightBand(); + band.setWeightMin(new BigDecimal(min)); + band.setWeightMax(new BigDecimal(max)); + band.setWeightLabel(min + "-" + max); + band.setPrice(new BigDecimal(price)); + band.setPriceUnit("EUR/ITEM"); + return band; + } + + private static PaySurcharge fee(String name, String amount, String rule) + { + PaySurcharge fee = new PaySurcharge(); + fee.setFeeName(name); + fee.setAmount(new BigDecimal(amount)); + fee.setCurrency("EUR"); + fee.setCalcRule(rule); + fee.setChargeText(amount + " EUR/ITEM"); + return fee; + } +} diff --git a/bms-back/bms-common/src/main/java/com/bms/common/enums/PaymentMesEnum.java b/bms-back/bms-common/src/main/java/com/bms/common/enums/PaymentMesEnum.java index 115be0eb..c8bbf81d 100644 --- a/bms-back/bms-common/src/main/java/com/bms/common/enums/PaymentMesEnum.java +++ b/bms-back/bms-common/src/main/java/com/bms/common/enums/PaymentMesEnum.java @@ -117,7 +117,49 @@ public enum PaymentMesEnum BMS_PAY_CUR_DICT("BMS_PAY_CUR_DICT", "提单费用校验币种", "收费币种不在币种字典中"), BMS_PAY_REMARK("BMS_PAY_REMARK", "提单费用校验备注", "备注不能超过500字"), BMS_PAY_LAD_BLANK("BMS_PAY_LAD_BLANK", "提单费用校验提单", "提单号不能为空"), - BMS_PAY_LAD_MISS("BMS_PAY_LAD_MISS", "提单费用校验提单", "提单不存在"); + BMS_PAY_LAD_MISS("BMS_PAY_LAD_MISS", "提单费用校验提单", "提单不存在"), + + BMS_PAY_USE_AUDIT("BMS_PAY_USE_AUDIT", "应付账单确认", "请先提交审核,审核通过后账单才确认"), + BMS_PAY_USE_PAY("BMS_PAY_USE_PAY", "应付账单付款", "请到应付付款登记付款并核销"), + BMS_PAY_SUBMIT("BMS_PAY_SUBMIT", "应付账单提交", "只有草稿可以提交审核"), + BMS_PAY_DIFF_OPEN("BMS_PAY_DIFF_OPEN", "应付账单提交", "还有未处理的对账差异"), + BMS_PAY_NO_LINE("BMS_PAY_NO_LINE", "应付账单提交", "账单没有费用行"), + BMS_PAY_PENDING("BMS_PAY_PENDING", "应付账单审核", "只有待审核的账单可以审核"), + BMS_PAY_NO_FILE("BMS_PAY_NO_FILE", "应付账单审核", "请先上传发票或对账凭证"), + BMS_PAY_REJECT_WHY("BMS_PAY_REJECT_WHY", "应付账单审核", "请填写驳回意见"), + BMS_PAY_LINE_MISS("BMS_PAY_LINE_MISS", "应付账单差异", "费用行不存在"), + BMS_PAY_LINE_LOCK("BMS_PAY_LINE_LOCK", "应付账单差异", "只有草稿可以调整费用行"), + BMS_PAY_NO_DIFF("BMS_PAY_NO_DIFF", "应付账单差异", "这行没有待处理的差异"), + BMS_PAY_NO_VOID("BMS_PAY_NO_VOID", "应付账单作废", "已确认的账单不能直接作废,请做红字"), + BMS_PAY_FEE_TAKEN("BMS_PAY_FEE_TAKEN", "应付账单生成", "费用已经进入其他账单"), + BMS_PAY_RED_STAT("BMS_PAY_RED_STAT", "应付账单红字", "只有已确认或部分付款的账单可以红冲"), + BMS_PAY_RED_PAID("BMS_PAY_RED_PAID", "应付账单红字", "账单已有付款,请先处理付款后再红冲"), + BMS_PAY_RED_KIND("BMS_PAY_RED_KIND", "应付账单红字", "红字账单不能再红冲"), + BMS_PAY_FILE_NEED("BMS_PAY_FILE_NEED", "应付凭证", "请先上传文件"), + BMS_PAY_FILE_MISS("BMS_PAY_FILE_MISS", "应付凭证", "凭证不存在"), + BMS_PAY_FILE_LOCK("BMS_PAY_FILE_LOCK", "应付凭证", "已确认的账单不能删除凭证"), + BMS_PAY_PAY_MISS("BMS_PAY_PAY_MISS", "应付付款", "付款单不存在"), + BMS_PAY_PAY_DRAFT("BMS_PAY_PAY_DRAFT", "应付付款", "只有草稿付款单可以确认或作废"), + BMS_PAY_PAY_DATE("BMS_PAY_PAY_DATE", "应付付款", "请填写付款日期"), + BMS_PAY_PAY_AMT("BMS_PAY_PAY_AMT", "应付付款", "请填写付款金额"), + BMS_PAY_ALLOC_NEED("BMS_PAY_ALLOC_NEED", "应付付款", "请填写本次核销金额"), + BMS_PAY_ALLOC_AMT("BMS_PAY_ALLOC_AMT", "应付付款", "核销金额超过账单未付金额"), + BMS_PAY_ALLOC_DUP("BMS_PAY_ALLOC_DUP", "应付付款", "同一张账单不能重复核销"), + BMS_PAY_ALLOC_OVER("BMS_PAY_ALLOC_OVER", "应付付款", "核销合计不能超过付款金额"), + BMS_PAY_PAY_BILL("BMS_PAY_PAY_BILL", "应付付款", "只能核销已确认或部分付款的账单"), + BMS_PAY_PAY_SAME("BMS_PAY_PAY_SAME", "应付付款", "付款单和账单的供应商、币种需要一致"), + BMS_PAY_SAME_USER("BMS_PAY_SAME_USER", "应付审核", "提交人和审核人不能是同一人"), + BMS_PAY_SAME_PAY("BMS_PAY_SAME_PAY", "应付付款", "登记人和确认人不能是同一人"), + BMS_PAY_INV_NEED("BMS_PAY_INV_NEED", "应付发票", "审核通过前请上传发票,并填写发票号和价税合计"), + BMS_PAY_INV_AMT("BMS_PAY_INV_AMT", "应付发票", "发票价税合计与账单金额不一致"), + BMS_PAY_INV_TAX("BMS_PAY_INV_TAX", "应付发票", "发票税额与账单税额不一致"), + BMS_PAY_INV_DUP("BMS_PAY_INV_DUP", "应付发票", "这张发票号已经在该供应商下勾稽过"), + BMS_PAY_INV_NO("BMS_PAY_INV_NO", "应付发票", "请填写发票号"), + BMS_PAY_TAX_RATE("BMS_PAY_TAX_RATE", "应付价税", "税率要在 0 到 1 之间,例如 0.06"), + BMS_PAY_RATE("BMS_PAY_RATE", "应付汇率", "请填写大于 0 的汇率"), + BMS_PAY_NO_REVERSE("BMS_PAY_NO_REVERSE", "应付付款", "只有已付款的单据可以反核销或退款"), + BMS_PAY_NO_BACK("BMS_PAY_NO_BACK", "应付付款", "没有可冲回的核销"), + BMS_PAY_XLS_LINE("BMS_PAY_XLS_LINE", "应付对账单", "对账单里没有可导入的行号"); private static final Map BY_CODE = new HashMap<>(); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBill.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBill.java index b674b759..f3787a16 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBill.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBill.java @@ -33,9 +33,23 @@ public class PayBill private BigDecimal totalAmount; - /** DRAFT CONFIRMED PAID VOID */ + private BigDecimal supplierAmount; + + private BigDecimal diffAmount; + + private BigDecimal paidAmount; + + @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") + private Date dueDate; + + /** DRAFT PENDING CONFIRMED PARTIAL PAID RED VOID */ private String status; + /** BLUE 蓝字,RED 红字 */ + private String billKind; + + private Long originalBillId; + private String voidReason; private String voidBy; @@ -43,6 +57,29 @@ public class PayBill @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private Date voidTime; + private String confirmBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date confirmTime; + + /** 0.06 表示 6% */ + private BigDecimal taxRate; + + private BigDecimal amountNet; + + private BigDecimal taxAmount; + + private BigDecimal exchangeRate; + + private BigDecimal baseAmount; + + @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") + private Date rateDate; + + private Integer termDays; + + private String submitBy; + private String createBy; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @@ -58,4 +95,10 @@ public class PayBill private List lines; private List logs; + + private List diffs; + + private List files; + + private List approvals; } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillApproval.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillApproval.java new file mode 100644 index 00000000..7aa9257c --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillApproval.java @@ -0,0 +1,24 @@ +package com.bms.price.domain; + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +/** 应付账单审核记录。 */ +@Data +public class PayBillApproval +{ + private Long approvalId; + + private Long billId; + + /** SUBMIT APPROVE REJECT */ + private String action; + + private String opinion; + + private String operatorName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillCommand.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillCommand.java index 57034158..b176e066 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillCommand.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillCommand.java @@ -32,4 +32,34 @@ public class PayBillCommand /** 要作废的应付账单ID */ private Long billId; + + /** 到期日,yyyy-MM-dd */ + private String dueDate; + + /** 账单费用行。登记对方金额时使用 */ + private java.util.List lines; + + /** 要处理的账单行 */ + private Long lineId; + + /** 对方金额 */ + private java.math.BigDecimal supplierAmount; + + /** OURS 接受我方,THEIRS 接受对方 */ + private String handleType; + + /** 差异原因或红字原因 */ + private String reason; + + /** 驳回意见 */ + private String opinion; + + /** 税率,0.06 表示 6% */ + private java.math.BigDecimal taxRate; + + /** 折人民币汇率 */ + private java.math.BigDecimal exchangeRate; + + /** 汇率日期,yyyy-MM-dd */ + private String rateDate; } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillDiff.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillDiff.java new file mode 100644 index 00000000..ecf42012 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillDiff.java @@ -0,0 +1,37 @@ +package com.bms.price.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +/** 应付账单上的一行对账差异。 */ +@Data +public class PayBillDiff +{ + private Long diffId; + + private Long billId; + + private Long lineId; + + private String feeId; + + private BigDecimal ourAmount; + + private BigDecimal supplierAmount; + + private BigDecimal diffAmount; + + private String reason; + + /** OPEN ACCEPT_OURS ACCEPT_THEIRS */ + private String handleType; + + private String adjustFeeId; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillFile.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillFile.java new file mode 100644 index 00000000..611ec10d --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillFile.java @@ -0,0 +1,50 @@ +package com.bms.price.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +/** 应付凭证。发票、对账单或费用通知挂在应付账单上。 */ +@Data +public class PayBillFile +{ + private Long fileId; + + private Long billId; + + private String billNo; + + private String supplierName; + + /** INVOICE STATEMENT DEBIT OTHER */ + private String fileType; + + private String invoiceNo; + + @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") + private Date invoiceDate; + + private BigDecimal amount; + + private BigDecimal taxAmount; + + private BigDecimal amountNet; + + private String sellerName; + + private BigDecimal matchedAmount; + + private String currency; + + private String fileName; + + private String fileUrl; + + private String deleted; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillLine.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillLine.java index b3e94268..70275661 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillLine.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillLine.java @@ -13,6 +13,8 @@ public class PayBillLine private Long billId; + private String feeId; + private String orderBillId; private String orderNo; @@ -23,8 +25,27 @@ public class PayBillLine private String feeName; + private String feeSource; + + private String feeLevel; + private BigDecimal amount; + private BigDecimal supplierAmount; + + private BigDecimal diffAmount; + + /** NONE OPEN ACCEPT_OURS ACCEPT_THEIRS */ + private String diffStatus; + + private BigDecimal taxRate; + + private BigDecimal amountNet; + + private BigDecimal taxAmount; + + private BigDecimal baseAmount; + private String currency; @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillLog.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillLog.java index 544bc9cf..886908e3 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillLog.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillLog.java @@ -1,5 +1,6 @@ package com.bms.price.domain; +import java.math.BigDecimal; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; @@ -12,6 +13,14 @@ public class PayBillLog private Long billId; + private String bizType; + + private Long bizId; + + private Long lineId; + + private String feeId; + private String operationType; private String fromStatus; @@ -20,6 +29,10 @@ public class PayBillLog private String operationDesc; + private BigDecimal amountBefore; + + private BigDecimal amountAfter; + private String operatorName; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillOrder.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillOrder.java index 7ca399c7..a03d392e 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillOrder.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayBillOrder.java @@ -45,4 +45,10 @@ public class PayBillOrder /** 已挂在未作废账单上时的账单号 */ private String occupiedBillNo; + + /** 未删除费用行数 */ + private Integer feeCount; + + /** 还没进入未作废账单的费用行数 */ + private Integer unbilledCount; } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayPayment.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayPayment.java new file mode 100644 index 00000000..16d63839 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayPayment.java @@ -0,0 +1,57 @@ +package com.bms.price.domain; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +/** 应付付款单。一张付款可以核销多张已确认账单。 */ +@Data +public class PayPayment +{ + private Long paymentId; + + private String paymentNo; + + private Long supplierId; + + private String supplierName; + + private String currency; + + @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") + private Date payDate; + + /** BANK CASH OTHER */ + private String payMethod; + + private String bankAccount; + + private BigDecimal amount; + + private BigDecimal bankFee; + + private BigDecimal exchangeRate; + + private BigDecimal baseAmount; + + private BigDecimal allocAmount; + + /** DRAFT PAID VOID REFUNDED */ + private String status; + + private String remark; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + + private String confirmBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date confirmTime; + + private List allocs; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayPaymentAlloc.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayPaymentAlloc.java new file mode 100644 index 00000000..be0f96af --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayPaymentAlloc.java @@ -0,0 +1,31 @@ +package com.bms.price.domain; + +import java.math.BigDecimal; +import lombok.Data; + +/** 应付付款核销到一张账单的金额。 */ +@Data +public class PayPaymentAlloc +{ + private Long allocId; + + private Long paymentId; + + private Long billId; + + private String billNo; + + private BigDecimal allocAmount; + + /** ACTIVE REVERSED */ + private String allocStatus; + + /** 账单未核销金额,只在查询未付账单时带出 */ + private BigDecimal openAmount; + + private String currency; + + private Long supplierId; + + private String supplierName; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayProduct.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayProduct.java index cb5aa51d..5ba18fdc 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PayProduct.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PayProduct.java @@ -151,5 +151,8 @@ public class PayProduct extends BaseEntity private List surcharges; + /** 这一版附加费配置。只在计费时带上,不入库 */ + private List billingRules; + private List extraLines; } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurchargeRule.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurchargeRule.java new file mode 100644 index 00000000..5e9edad7 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurchargeRule.java @@ -0,0 +1,54 @@ +package com.bms.price.domain; + +import java.math.BigDecimal; +import java.util.Map; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +/** + * 应付附加费计费条件。同一产品版本的全部条件都在这张表,用 calcRule 区分列表。 + */ +@Data +public class PaySurchargeRule +{ + private Long ruleId; + + /** 这一版应付产品 */ + private Long productId; + + /** 价卡版本周期 */ + private Long termId; + + private Integer versionNo; + + /** ALWAYS、POSTCODE、ZONE、EXTRA_KG、WEIGHT、SIZE、RETURN */ + private String calcRule; + + /** 1开 0关 */ + private String enabled; + + /** 条件摘要,由明细生成 */ + private String calcParam; + + /** 条件明细 JSON。接口不直接收发这一列,用 detail */ + @JsonIgnore + private String ruleDetail; + + /** 这一条规则的结构化参数。超尺寸含长宽高、围长、体积、加收比例和命中方式 */ + private Map detail; + + /** 续重行的邮编 */ + private String postcodeParam; + + private String zoneName; + + private BigDecimal weightMin; + + private BigDecimal weightMax; + + private Integer sortNo; + + private String createBy; + + private String updateBy; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurchargeRuleSave.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurchargeRuleSave.java new file mode 100644 index 00000000..1affd297 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PaySurchargeRuleSave.java @@ -0,0 +1,28 @@ +package com.bms.price.domain; + +import java.util.ArrayList; +import java.util.List; +import lombok.Data; + +/** + * 把同一套计费条件写到若干产品的当前版本。每个版本只保留这一份。 + */ +@Data +public class PaySurchargeRuleSave +{ + private List products = new ArrayList<>(); + + private List rules = new ArrayList<>(); + + @Data + public static class PaySurchargeRuleBind + { + private Long productId; + + private Long termId; + + private Integer versionNo; + + private String productName; + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPayBillFee.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPayBillFee.java index c71cc16a..35011024 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPayBillFee.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcPayBillFee.java @@ -35,6 +35,14 @@ public class PrcPayBillFee implements Serializable /** 是否删除:0未删除 1已删除 */ private String isDeleted; + + /** 已进入的应付账单行。空表示还没出账 */ + private Long statementLineId; + + /** 结算供应商。空表示还没标明付款对象 */ + private Long settleSupplierId; + + private String settleSupplierName; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime; } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayBillMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayBillMapper.java index d73f563e..ec93452b 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayBillMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayBillMapper.java @@ -3,9 +3,13 @@ package com.bms.price.mapper; import java.util.List; import org.apache.ibatis.annotations.Param; import com.bms.price.domain.PayBill; +import com.bms.price.domain.PayBillApproval; +import com.bms.price.domain.PayBillDiff; +import com.bms.price.domain.PayBillFile; import com.bms.price.domain.PayBillLine; import com.bms.price.domain.PayBillLog; import com.bms.price.domain.PayBillOrder; +import com.bms.price.domain.PayPaymentAlloc; import com.bms.price.domain.PrcPayBillFee; /** 供应商应付账单 pay_bill 及其明细、日志。 */ @@ -63,6 +67,22 @@ public interface PayBillMapper */ List selectOrderFees(@Param("billIds") List billIds); + /** + * 读取还没被未作废账单占用的费用。旧账单没有费用行号时,整票视为已占用。 + * + * @param billIds 计费订单ID + * @return 可抄进本次账单的费用 + */ + List selectFreeFees(@Param("billIds") List billIds); + + /** + * 这票订单还有没有未删除的计算费用。 + * + * @param billId 计费订单ID + * @return 计算费用条数 + */ + int countCalcFees(@Param("billId") String billId); + /** * 插入一张草稿应付账单。 * @@ -87,6 +107,103 @@ public interface PayBillMapper */ int insertLines(@Param("list") List list); + /** + * 写入一条账单费用快照,并回填行号。 + * + * @param line 费用行 + * @return 影响行数 + */ + int insertLine(PayBillLine line); + + /** + * 把费用明细占用到刚写入的账单行。已被占用时影响行数为 0。 + * + * @param feeId 费用明细ID + * @param lineId 账单行ID + * @return 影响行数 + */ + int bindFeeLine(@Param("feeId") String feeId, @Param("lineId") Long lineId); + + /** + * 释放一张账单占用的费用明细。 + * + * @param billId 应付账单ID + * @return 影响行数 + */ + int clearFeeLines(@Param("billId") Long billId); + + /** + * 释放某一条账单行占用的费用明细。 + * + * @param lineId 账单行ID + * @return 影响行数 + */ + int clearFeeLine(@Param("lineId") Long lineId); + + /** + * 按费用行重算账单金额、订单数,并写回状态。 + * + * @param bill 账单ID、金额、状态和更新人 + * @return 影响行数 + */ + int updatePayBillMoney(PayBill bill); + + /** + * 删除草稿中的一条费用快照。 + * + * @param billId 应付账单ID + * @param lineId 账单行ID + * @return 影响行数 + */ + int deleteLine(@Param("billId") Long billId, @Param("lineId") Long lineId); + + int updateLineAmount(PayBillLine line); + + int deleteOpenDiff(@Param("lineId") Long lineId); + + /** + * 读取一张账单的差异、凭证和审核。 + * + * @param billId 应付账单ID + * @return 对应记录 + */ + List selectDiffs(@Param("billId") Long billId); + + List selectFiles(@Param("billId") Long billId); + + List selectApprovals(@Param("billId") Long billId); + + PayBillLine selectLine(@Param("lineId") Long lineId); + + PayBillDiff selectOpenDiff(@Param("lineId") Long lineId); + + int insertDiff(PayBillDiff diff); + + int updateDiff(PayBillDiff diff); + + int deleteDiffsByLine(@Param("lineId") Long lineId); + + int insertFile(PayBillFile file); + + PayBillFile selectFile(@Param("fileId") Long fileId); + + int markFileDeleted(@Param("fileId") Long fileId); + + int insertApproval(PayBillApproval approval); + + List selectFileList(PayBillFile query); + + int countActiveFiles(@Param("billId") Long billId); + + /** + * 查询可以核销的账单。已确认或部分付款,且还有未付金额。 + * + * @param supplierId 供应商 + * @param currency 币种 + * @return 未付账单 + */ + List selectOpenBills(@Param("supplierId") Long supplierId, @Param("currency") String currency); + /** * 写入一条账单操作日志。 * diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayFinanceMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayFinanceMapper.java new file mode 100644 index 00000000..535c8add --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayFinanceMapper.java @@ -0,0 +1,28 @@ +package com.bms.price.mapper; + +import java.math.BigDecimal; +import java.util.Date; +import org.apache.ibatis.annotations.Param; + +/** 应付价税、发票勾稽和费用付款对象。 */ +public interface PayFinanceMapper +{ + int updateFinanceHead(@Param("billId") Long billId, @Param("taxRate") BigDecimal taxRate, + @Param("exchangeRate") BigDecimal exchangeRate, @Param("rateDate") Date rateDate); + + int updateSubmitBy(@Param("billId") Long billId, @Param("submitBy") String submitBy); + + int updateLineTax(@Param("billId") Long billId, @Param("taxRate") BigDecimal taxRate, + @Param("exchangeRate") BigDecimal exchangeRate); + + int countDuplicateInvoice(@Param("supplierId") Long supplierId, @Param("invoiceNo") String invoiceNo, + @Param("fileId") Long fileId); + + int markInvoiceMatched(@Param("billId") Long billId); + + Long selectLadingSupplier(@Param("ladingNo") String ladingNo); + + int stampFee(@Param("feeId") String feeId, @Param("supplierId") Long supplierId, @Param("supplierName") String supplierName); + + int stampCalc(@Param("billId") String billId, @Param("supplierId") Long supplierId, @Param("supplierName") String supplierName); +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayPaymentMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayPaymentMapper.java new file mode 100644 index 00000000..b4e7f569 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayPaymentMapper.java @@ -0,0 +1,24 @@ +package com.bms.price.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.bms.price.domain.PayPayment; +import com.bms.price.domain.PayPaymentAlloc; + +/** 应付付款单和核销。 */ +public interface PayPaymentMapper +{ + List selectPayPaymentList(PayPayment query); + + PayPayment selectPayPaymentById(@Param("paymentId") Long paymentId); + + List selectAllocs(@Param("paymentId") Long paymentId); + + int insertPayPayment(PayPayment payment); + + int insertAlloc(PayPaymentAlloc alloc); + + int updatePayPaymentStatus(PayPayment payment); + + int reverseAlloc(@Param("allocId") Long allocId, @Param("reverseBy") String reverseBy); +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PaySurchargeRuleMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PaySurchargeRuleMapper.java new file mode 100644 index 00000000..97af1682 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PaySurchargeRuleMapper.java @@ -0,0 +1,18 @@ +package com.bms.price.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.bms.price.domain.PaySurchargeRule; + +/** 应付附加费计费条件。一个产品版本一份。 */ +public interface PaySurchargeRuleMapper +{ + List selectByProduct(@Param("productId") Long productId, @Param("termId") Long termId); + + int deleteByTerm(@Param("productId") Long productId, @Param("termId") Long termId); + + int insertBatch(@Param("list") List rules); + + /** 同一产品字典下,比当前版本号小的最近一版 */ + PaySurchargeRule selectPreviousVersion(@Param("productId") Long productId); +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/pay/PayCostCalculator.java b/bms-back/bms-system/src/main/java/com/bms/price/pay/PayCostCalculator.java index 2118e836..5277f439 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/pay/PayCostCalculator.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/pay/PayCostCalculator.java @@ -9,12 +9,14 @@ import java.util.Date; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import com.bms.common.enums.PaymentMesEnum; import com.bms.price.domain.PayCostLine; import com.bms.price.domain.PayOrderCostResult; import com.bms.price.domain.PayProduct; import com.bms.price.domain.PaySurcharge; +import com.bms.price.domain.PaySurchargeRule; import com.bms.price.domain.PayWeightBand; /** @@ -69,7 +71,7 @@ public final class PayCostCalculator public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width, BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus) { - return calculate(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, null); + return calculate(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, null, true, true); } /** @@ -89,6 +91,16 @@ public final class PayCostCalculator */ public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width, BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, List remoteHits) + { + return calculate(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, remoteHits, true, true); + } + + /** + * postcodeOn、zoneOn 来自附加费配置的两个开关。关掉时不再按价卡上的邮编或分区条件加附加费。 + */ + public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width, + BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, List remoteHits, + boolean postcodeOn, boolean zoneOn) { PayOrderCostResult result = new PayOrderCostResult(); result.setFees(new ArrayList<>()); @@ -146,7 +158,7 @@ public final class PayCostCalculator result.getFees().add(freightLine); result.setCurrency(currency); addSurcharges(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, freight, currency, - band.getSectionName(), remoteHits, result); + band.getSectionName(), remoteHits, postcodeOn, zoneOn, result); BigDecimal total = BigDecimal.ZERO; for (PayCostLine fee : result.getFees()) { @@ -266,7 +278,7 @@ public final class PayCostCalculator */ private static void addSurcharges(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width, BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, BigDecimal freight, String currency, - String freightSection, List remoteHits, PayOrderCostResult result) + String freightSection, List remoteHits, boolean postcodeOn, boolean zoneOn, PayOrderCostResult result) { if (product.getSurcharges() == null) { @@ -283,6 +295,7 @@ public final class PayCostCalculator if (remoteHits != null && remoteFee(fee)) { String name = blank(fee.getFeeName()); + boolean added = false; if (postcode == null || postcode.isBlank()) { result.getSkipped().add(name + ":订单没有邮编"); @@ -302,7 +315,21 @@ public final class PayCostCalculator { addFlat(result.getFees(), fee, currency, remark); } + added = true; } + if (added || !PaySurchargeRules.ZONE.equals(rule)) + { + continue; + } + } + if (!postcodeOn && PaySurchargeRules.POSTCODE.equals(rule)) + { + result.getSkipped().add(blank(fee.getFeeName()) + ":未打开命中邮编"); + continue; + } + if (!zoneOn && PaySurchargeRules.ZONE.equals(rule)) + { + result.getSkipped().add(blank(fee.getFeeName()) + ":未打开命中分区"); continue; } boolean postcodeDriven = PaySurchargeRules.POSTCODE.equals(rule) || !blank(fee.getPostcodeParam()).isEmpty(); @@ -346,9 +373,9 @@ public final class PayCostCalculator { result.getSkipped().add(name + ":订单没有长宽高"); } - else if (PaySurchargeRules.matchesSize(fee.getCalcParam(), length, width, height)) + else if (sizeHit(product, fee, length, width, height, weight)) { - PayCostLine matched = matchedFee(fee, freight, currency, "尺寸命中 " + fee.getCalcParam()); + PayCostLine matched = matchedFee(fee, freight, currency, "尺寸命中"); if (matched != null) { sized.add(matched); @@ -384,6 +411,10 @@ public final class PayCostCalculator } else if (PaySurchargeRules.ALWAYS.equals(rule)) { + if (!PaySurchargeRules.allowsAlways(ruleDetails(product, PaySurchargeRules.ALWAYS), name, bizDate)) + { + continue; + } PayCostLine matched = matchedFee(fee, freight, currency, blank(fee.getChargeText())); if (matched != null) { @@ -517,6 +548,42 @@ public final class PayCostCalculator return top; } + private static boolean sizeHit(PayProduct product, PaySurcharge fee, BigDecimal length, BigDecimal width, BigDecimal height, + BigDecimal weight) + { + List> details = ruleDetails(product, PaySurchargeRules.SIZE); + if (details.isEmpty()) + { + return PaySurchargeRules.matchesSize(fee.getCalcParam(), length, width, height); + } + for (Map detail : details) + { + if (PaySurchargeRules.matchesConfiguredSize(detail, length, width, height, weight)) + { + return true; + } + } + return false; + } + + private static List> ruleDetails(PayProduct product, String calcRule) + { + List> details = new ArrayList<>(); + if (product == null || product.getBillingRules() == null) + { + return details; + } + for (PaySurchargeRule rule : product.getBillingRules()) + { + if (rule == null || !calcRule.equals(rule.getCalcRule()) || "0".equals(rule.getEnabled()) || rule.getDetail() == null) + { + continue; + } + details.add(rule.getDetail()); + } + return details; + } + private static boolean remoteFee(PaySurcharge fee) { String name = blank(fee.getFeeName()).toLowerCase(Locale.ROOT); @@ -540,7 +607,7 @@ public final class PayCostCalculator continue; } boolean areaKnown = !blank(hit.getZoneName()).isEmpty() || !blank(hit.getRegionName()).isEmpty(); - if (zoned && areaKnown && !sameArea(hit, fee)) + if (zoned && (!areaKnown || !sameArea(hit, fee))) { continue; } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/pay/PayRemoteBook.java b/bms-back/bms-system/src/main/java/com/bms/price/pay/PayRemoteBook.java index 9b134711..a75e7c07 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/pay/PayRemoteBook.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/pay/PayRemoteBook.java @@ -121,7 +121,8 @@ public final class PayRemoteBook List hits = new ArrayList<>(); for (PayPostcodeLine line : lines) { - if (line == null || !remote.containsKey(line.getGroupId()) || !PayZoneMatcher.matchesLine(line, postcode)) + if (line == null || !remote.containsKey(line.getGroupId()) || !sameLineCountry(line, countryCode, countryName) + || !PayZoneMatcher.matchesLine(line, postcode)) { continue; } @@ -146,6 +147,21 @@ public final class PayRemoteBook return new BigDecimal(matcher.group(1)); } + /** 明细上写了国家时必须和订单目的国一致。没写则沿用分组上的国家。 */ + private static boolean sameLineCountry(PayPostcodeLine line, String countryCode, String countryName) + { + boolean lineBlank = blank(line.getCountryCode()) && blank(line.getCountryName()); + if (lineBlank) + { + return true; + } + if (!blank(countryCode) && countryCode.equalsIgnoreCase(line.getCountryCode() == null ? "" : line.getCountryCode().trim())) + { + return true; + } + return !blank(countryName) && countryName.trim().equals(line.getCountryName() == null ? "" : line.getCountryName().trim()); + } + private static boolean sameCountry(PayPostcodeGroup group, String countryCode, String countryName) { boolean orderBlank = blank(countryCode) && blank(countryName); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/pay/PaySettleSupport.java b/bms-back/bms-system/src/main/java/com/bms/price/pay/PaySettleSupport.java new file mode 100644 index 00000000..cb68728e --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/pay/PaySettleSupport.java @@ -0,0 +1,110 @@ +package com.bms.price.pay; + +import java.util.List; +import java.util.Locale; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import com.bms.customer.supplierinfo.domain.BmsSupplier; +import com.bms.customer.supplierinfo.mapper.BmsSupplierMapper; +import com.bms.price.mapper.PayFinanceMapper; + +/** 把价卡上的归属名称对到供应商主数据。 */ +@Component +public class PaySettleSupport +{ + @Autowired + private BmsSupplierMapper supplierMapper; + + @Autowired + private PayFinanceMapper payFinanceMapper; + + /** + * 计算费用落库后,按价卡归属写上结算供应商。对不上供应商时保持为空。 + * + * @param billId 计费订单ID + * @param cardName 价卡归属名称 + */ + public void stampCalc(String billId, String cardName) + { + BmsSupplier supplier = match(cardName); + if (supplier == null || billId == null || billId.isBlank()) + { + return; + } + payFinanceMapper.stampCalc(billId, supplier.getSupplierId(), supplier.getSupplierName()); + } + + /** + * 用价卡归属名称找到唯一供应商。名称、编码或对外代码一致优先,其次互相包含。 + * + * @param cardName 价卡上的供应商名称 + * @return 对上的供应商;对不上或对上多家且没有精确项时返回 null + */ + public BmsSupplier match(String cardName) + { + String card = norm(cardName); + if (card.isEmpty()) + { + return null; + } + List all = supplierMapper.selectBmsSupplierList(new BmsSupplier()); + BmsSupplier exact = null; + BmsSupplier fuzzy = null; + int fuzzyCount = 0; + if (all == null) + { + return null; + } + for (BmsSupplier supplier : all) + { + if (supplier == null || supplier.getSupplierId() == null) + { + continue; + } + String name = norm(supplier.getSupplierName()); + String code = norm(supplier.getSupplierCode()); + String dw = norm(supplier.getSupplierCodeDw()); + if (card.equals(name) || card.equals(code) || (!dw.isEmpty() && card.equals(dw))) + { + exact = supplier; + break; + } + if (card.length() >= 2 && name.length() >= 2 && (name.contains(card) || card.contains(name))) + { + fuzzy = supplier; + fuzzyCount++; + } + } + if (exact != null) + { + return exact; + } + return fuzzyCount == 1 ? fuzzy : null; + } + + /** + * 所选供应商和价卡归属是不是同一家。 + * + * @param supplier 账单上选择的供应商 + * @param cardName 价卡归属名称 + * @return 对得上时为 true + */ + public boolean same(BmsSupplier supplier, String cardName) + { + if (supplier == null || supplier.getSupplierId() == null) + { + return false; + } + BmsSupplier matched = match(cardName); + return matched != null && supplier.getSupplierId().equals(matched.getSupplierId()); + } + + private static String norm(String value) + { + if (value == null) + { + return ""; + } + return value.replaceAll("\\s+", "").toLowerCase(Locale.ROOT); + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/pay/PaySurchargeRules.java b/bms-back/bms-system/src/main/java/com/bms/price/pay/PaySurchargeRules.java index 83e99a93..9e83f378 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/pay/PaySurchargeRules.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/pay/PaySurchargeRules.java @@ -125,13 +125,23 @@ public final class PaySurchargeRules { continue; } + String compact = value.replaceAll("[^A-Z0-9]", ""); + String whole = postcode.toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]", ""); + if (!compact.isEmpty() && compact.equals(whole)) + { + return true; + } if (parsed != null && tokenMatches(value, parsed)) { return true; } - if (parsed == null && value.matches("\\d{1,5}") && !digits.isEmpty() && digits.startsWith(value)) + if (parsed == null && value.matches("\\d{1,5}") && !digits.isEmpty()) { - return true; + boolean full = value.length() >= 4 ? digits.equals(value) : digits.startsWith(value); + if (full) + { + return true; + } } } return false; @@ -159,6 +169,218 @@ public final class PaySurchargeRules return String.join(",", tokens); } + /** + * 按附加费配置里的超尺寸明细判断。上限表示超过这个值才命中。箱规按从大到小比,任一边超过对应边即命中。 + */ + public static boolean matchesConfiguredSize(java.util.Map detail, BigDecimal length, BigDecimal width, + BigDecimal height, BigDecimal weight) + { + if (detail == null || detail.isEmpty() || length == null || width == null || height == null) + { + return false; + } + BigDecimal weightMin = number(detail.get("weightMin")); + BigDecimal weightMax = number(detail.get("weightMax")); + if (weightMin != null || weightMax != null) + { + if (weight == null) + { + return false; + } + if (weightMin != null && weight.compareTo(weightMin) <= 0) + { + return false; + } + if (weightMax != null && weight.compareTo(weightMax) > 0) + { + return false; + } + } + BigDecimal[] sides = desc(length, width, height); + List checks = new ArrayList<>(); + addBound(checks, sides[0], detail.get("longestMin"), detail.get("longestMax")); + addBound(checks, sides[1], detail.get("secondMin"), detail.get("secondMax")); + addBound(checks, length.add(width).add(height), detail.get("sumMin"), detail.get("sumMax")); + addBound(checks, sides[0].add(sides[1].add(sides[2]).multiply(new BigDecimal("2"))), detail.get("girthMin"), detail.get("girthMax")); + BigDecimal volumeMin = number(detail.get("volumeMin")); + BigDecimal volumeMax = number(detail.get("volumeMax")); + if (volumeMin != null || volumeMax != null) + { + BigDecimal cubic = length.multiply(width).multiply(height).divide(new BigDecimal("1000000"), 6, RoundingMode.HALF_UP); + addBound(checks, cubic, volumeMin, volumeMax); + } + BigDecimal cubicMax = number(detail.get("cubicMax")); + if (cubicMax != null) + { + checks.add(length.multiply(width).multiply(height).compareTo(cubicMax) > 0); + } + BigDecimal boxLength = number(detail.get("boxLength")); + BigDecimal boxWidth = number(detail.get("boxWidth")); + BigDecimal boxHeight = number(detail.get("boxHeight")); + if (boxLength != null && boxWidth != null && boxHeight != null) + { + boolean under = "UNDER".equals(String.valueOf(detail.get("boxMode"))); + checks.add(under ? withinBox(sides, boxLength, boxWidth, boxHeight) : exceedsBox(sides, boxLength, boxWidth, boxHeight)); + } + if (checks.isEmpty()) + { + return false; + } + if ("ALL".equals(String.valueOf(detail.get("matchMode")))) + { + return !checks.contains(Boolean.FALSE); + } + return checks.contains(Boolean.TRUE); + } + + /** 每票都收。写了费用名的规则只约束这一项;带生效月日的规则只在这段日期内收取。 */ + public static boolean allowsAlways(List> details, String feeName, java.util.Date bizDate) + { + if (details == null || details.isEmpty()) + { + return true; + } + String name = feeName == null ? "" : feeName.trim(); + List> named = new ArrayList<>(); + List> shared = new ArrayList<>(); + for (java.util.Map detail : details) + { + if (detail == null) + { + continue; + } + String ruleName = detail.get("feeName") == null ? "" : String.valueOf(detail.get("feeName")).trim(); + if (ruleName.isEmpty()) + { + shared.add(detail); + } + else if (ruleName.equals(name)) + { + named.add(detail); + } + } + List> mine = named.isEmpty() ? shared : named; + if (mine.isEmpty()) + { + return shared.isEmpty(); + } + java.time.LocalDate day = bizDate == null ? null : bizDate.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate(); + for (java.util.Map detail : mine) + { + if (!hasPeriod(detail)) + { + return true; + } + if (day != null && inPeriod(day, detail)) + { + return true; + } + } + return false; + } + + private static void addBound(List checks, BigDecimal actual, Object min, Object max) + { + BigDecimal lower = min instanceof BigDecimal ? (BigDecimal) min : number(min); + BigDecimal upper = max instanceof BigDecimal ? (BigDecimal) max : number(max); + if (lower == null && upper == null) + { + return; + } + if (lower != null && upper != null) + { + checks.add(actual.compareTo(lower) > 0 && actual.compareTo(upper) <= 0); + } + else if (upper != null) + { + checks.add(actual.compareTo(upper) > 0); + } + else + { + checks.add(actual.compareTo(lower) > 0); + } + } + + private static boolean withinBox(BigDecimal[] parcel, BigDecimal a, BigDecimal b, BigDecimal c) + { + BigDecimal[] limit = desc(a, b, c); + return parcel[0].compareTo(limit[0]) < 0 && parcel[1].compareTo(limit[1]) < 0 && parcel[2].compareTo(limit[2]) < 0; + } + + private static boolean hasPeriod(java.util.Map detail) + { + return text(detail.get("periodStart")) != null || text(detail.get("periodEnd")) != null; + } + + private static boolean inPeriod(java.time.LocalDate day, java.util.Map detail) + { + int[] start = monthDay(text(detail.get("periodStart"))); + int[] end = monthDay(text(detail.get("periodEnd"))); + int current = day.getMonthValue() * 100 + day.getDayOfMonth(); + int from = start == null ? 101 : start[0] * 100 + start[1]; + int to = end == null ? 1231 : end[0] * 100 + end[1]; + if (from <= to) + { + return current >= from && current <= to; + } + return current >= from || current <= to; + } + + private static int[] monthDay(String text) + { + if (text == null) + { + return null; + } + String[] parts = text.split("-"); + try + { + if (parts.length == 2) + { + return new int[] { Integer.parseInt(parts[0]), Integer.parseInt(parts[1]) }; + } + if (parts.length == 3) + { + return new int[] { Integer.parseInt(parts[1]), Integer.parseInt(parts[2]) }; + } + } + catch (NumberFormatException ex) + { + return null; + } + return null; + } + + private static String text(Object value) + { + if (value == null) + { + return null; + } + String text = String.valueOf(value).trim(); + return text.isEmpty() ? null : text; + } + + private static BigDecimal number(Object value) + { + if (value == null || (value instanceof String text && text.isBlank())) + { + return null; + } + if (value instanceof BigDecimal decimal) + { + return decimal; + } + try + { + return new BigDecimal(String.valueOf(value).trim()); + } + catch (NumberFormatException ex) + { + return null; + } + } + public static boolean matchesWeight(String param, BigDecimal weight) { if (param == null || param.isBlank() || weight == null) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/pay/PayZoneMatcher.java b/bms-back/bms-system/src/main/java/com/bms/price/pay/PayZoneMatcher.java index 4cccd8aa..d892b4f8 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/pay/PayZoneMatcher.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/pay/PayZoneMatcher.java @@ -247,11 +247,56 @@ public final class PayZoneMatcher private static int matchedLength(PayPostcodeLine line, String postcode) { int best = -1; - best = longer(best, line.getPostcode(), postcode); - best = longer(best, line.getPostcodeEnd(), postcode); + String start = line.getPostcode(); + String end = line.getPostcodeEnd(); + if (start != null && end != null && !end.isBlank() && !compact(start).equals(compact(end))) + { + best = Math.max(best, rangeLength(start, end, postcode)); + } + best = longer(best, start, postcode); return best; } + /** 完整邮编按整段相等,短码按前缀。区间用起止邮编,含两端。 */ + private static int rangeLength(String start, String end, String postcode) + { + String from = compact(start); + String to = compact(end); + String value = compact(postcode); + if (from.isEmpty() || to.isEmpty() || value.isEmpty()) + { + return -1; + } + if (from.compareTo(to) > 0) + { + String swap = from; + from = to; + to = swap; + } + if (digits(from) && digits(to) && digits(value) && value.length() == from.length()) + { + long low = Long.parseLong(from); + long high = Long.parseLong(to); + long current = Long.parseLong(value); + return current >= low && current <= high ? from.length() : -1; + } + if (from.length() == to.length() && value.length() == from.length() && value.compareTo(from) >= 0 && value.compareTo(to) <= 0) + { + return from.length(); + } + return -1; + } + + private static String compact(String value) + { + return value == null ? "" : value.toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]", ""); + } + + private static boolean digits(String value) + { + return !value.isEmpty() && value.chars().allMatch(Character::isDigit); + } + private static int longer(int best, String raw, String postcode) { if (raw == null || raw.isBlank()) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IPayBillService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IPayBillService.java index 2c40a95c..818758bb 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/IPayBillService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IPayBillService.java @@ -1,8 +1,11 @@ package com.bms.price.service; +import java.io.OutputStream; import java.util.List; +import org.springframework.web.multipart.MultipartFile; import com.bms.price.domain.PayBill; import com.bms.price.domain.PayBillCommand; +import com.bms.price.domain.PayBillFile; import com.bms.price.domain.PayBillPreview; /** @@ -66,4 +69,43 @@ public interface IPayBillService * @return 成功时为 1 */ int voidBill(PayBillCommand command); + + /** 草稿提交审核。 */ + int submit(Long billId); + + /** 审核通过。蓝字需要已有凭证。红字会冲回原账单并释放费用。 */ + int approve(Long billId); + + /** 审核驳回,回到草稿。 */ + int reject(PayBillCommand command); + + /** 草稿登记对方金额。 */ + int saveSupplierAmounts(PayBillCommand command); + + /** 接受我方或接受对方。接受对方会另写调整费用。 */ + int acceptDiff(PayBillCommand command); + + /** 从草稿剔除一条费用。 */ + int removeLine(PayBillCommand command); + + /** 上传应付凭证。 */ + int saveFile(PayBillFile file); + + /** 删除草稿或待审核账单上的凭证。 */ + int deleteFile(Long fileId); + + /** 查询应付凭证。 */ + List listFiles(PayBillFile query); + + /** 对未付款的已确认账单开红字草稿。 */ + PayBill redBill(PayBillCommand command); + + /** 草稿保存税率和汇率,并重算不含税、税额、本位币。 */ + int saveFinance(PayBillCommand command); + + /** 导出对账单,供供应商回填对方金额。 */ + void exportStatement(Long billId, OutputStream out); + + /** 按行号导入对方金额。 */ + int importSupplierAmounts(Long billId, MultipartFile file); } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IPayOrderCostService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IPayOrderCostService.java index b07849d4..4f5ed862 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/IPayOrderCostService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IPayOrderCostService.java @@ -14,4 +14,20 @@ public interface IPayOrderCostService * @return 本次算出的产品、版本、币种和费用明细 */ PayOrderCostResult generate(String billId); + + /** + * 单独提交一票尾程。失败时只回滚这一票,不影响外面的对账单事务。 + * + * @param billId 计费订单ID + * @return 本次算出的产品、版本、币种和费用明细 + */ + PayOrderCostResult generateIsolated(String billId); + + /** + * 这票订单当前尾程价卡上的归属供应商名称。没有价卡时返回空字符串。 + * + * @param billId 计费订单ID + * @return 价卡归属名称 + */ + String cardSupplierName(String billId); } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IPayPaymentService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IPayPaymentService.java new file mode 100644 index 00000000..13417547 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IPayPaymentService.java @@ -0,0 +1,27 @@ +package com.bms.price.service; + +import java.util.List; +import com.bms.price.domain.PayPayment; +import com.bms.price.domain.PayPaymentAlloc; + +/** 应付付款。草稿保存核销计划,确认后才回写账单已付金额。 */ +public interface IPayPaymentService +{ + List selectPayPaymentList(PayPayment query); + + PayPayment selectPayPaymentDetail(Long paymentId); + + List selectOpenBills(Long supplierId, String currency); + + PayPayment create(PayPayment payment); + + int confirm(Long paymentId); + + int voidPayment(Long paymentId); + + /** 反核销已确认付款。账单未付金额回来。 */ + int reverse(Long paymentId); + + /** 供应商退回这笔付款。核销冲回。 */ + int refund(Long paymentId); +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IPaySurchargeRuleService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IPaySurchargeRuleService.java new file mode 100644 index 00000000..33de2802 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IPaySurchargeRuleService.java @@ -0,0 +1,17 @@ +package com.bms.price.service; + +import java.util.List; +import com.bms.price.domain.PaySurchargeRule; +import com.bms.price.domain.PaySurchargeRuleSave; + +/** 应付附加费计费条件。一个产品版本只保留一份。 */ +public interface IPaySurchargeRuleService +{ + List list(Long productId, Long termId); + + /** 上一版的条件,只读出来供当前页编辑,不直接落库 */ + List previous(Long productId); + + /** 覆盖所选产品当前版本的配置。同一版本不会留下两份。 */ + void save(PaySurchargeRuleSave command); +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayBillServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayBillServiceImpl.java index 0b979ef8..e20e8115 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayBillServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayBillServiceImpl.java @@ -1,5 +1,7 @@ package com.bms.price.service.impl; +import java.io.InputStream; +import java.io.OutputStream; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.SimpleDateFormat; @@ -7,13 +9,22 @@ import java.time.LocalDate; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; +import java.util.UUID; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import com.bms.common.enums.PaymentMesEnum; import com.bms.common.exception.ServiceException; import com.bms.common.utils.SecurityUtils; @@ -21,7 +32,10 @@ import com.bms.common.utils.uuid.IdUtils; import com.bms.customer.supplierinfo.domain.BmsSupplier; import com.bms.customer.supplierinfo.mapper.BmsSupplierMapper; import com.bms.price.domain.PayBill; +import com.bms.price.domain.PayBillApproval; import com.bms.price.domain.PayBillCommand; +import com.bms.price.domain.PayBillDiff; +import com.bms.price.domain.PayBillFile; import com.bms.price.domain.PayBillLine; import com.bms.price.domain.PayBillLog; import com.bms.price.domain.PayBillOrder; @@ -29,8 +43,12 @@ import com.bms.price.domain.PayBillPreview; import com.bms.price.domain.PayOrderCostResult; import com.bms.price.domain.PrcPayBillFee; import com.bms.price.mapper.PayBillMapper; +import com.bms.price.mapper.PayFinanceMapper; +import com.bms.price.mapper.PrcPayBillFeeMapper; +import com.bms.price.pay.PaySettleSupport; import com.bms.price.service.IPayBillService; import com.bms.price.service.IPayOrderCostService; +import com.bms.price.service.IPrcPayBillFeeService; /** * 应付账单。先按订单号、计费日或提单查出订单,再把当时的费用抄进账单。作废只改状态。 @@ -49,6 +67,18 @@ public class PayBillServiceImpl implements IPayBillService @Autowired private BmsSupplierMapper supplierMapper; + @Autowired + private PrcPayBillFeeMapper payBillFeeMapper; + + @Autowired + private IPrcPayBillFeeService payBillFeeService; + + @Autowired + private PayFinanceMapper payFinanceMapper; + + @Autowired + private PaySettleSupport paySettleSupport; + /** * 按账单号、供应商、状态、币种查询应付账单。 * @@ -77,6 +107,9 @@ public class PayBillServiceImpl implements IPayBillService } bill.setLines(payBillMapper.selectLines(billId)); bill.setLogs(payBillMapper.selectLogs(billId)); + bill.setDiffs(payBillMapper.selectDiffs(billId)); + bill.setFiles(payBillMapper.selectFiles(billId)); + bill.setApprovals(payBillMapper.selectApprovals(billId)); return bill; } @@ -120,20 +153,31 @@ public class PayBillServiceImpl implements IPayBillService } String currency = currencyOf(orders); Map versions = new HashMap<>(); - for (PayBillOrder order : orders) - { - PayOrderCostResult cost = payOrderCostService.generate(order.getBillId()); - if (cost != null) - { - versions.put(order.getBillId(), cost.getVersionNo()); - } - } + List skipped = new ArrayList<>(); List billIds = new ArrayList<>(); for (PayBillOrder order : orders) { billIds.add(order.getBillId()); + if (payBillMapper.countCalcFees(order.getBillId()) > 0) + { + continue; + } + try + { + PayOrderCostResult cost = payOrderCostService.generateIsolated(order.getBillId()); + if (cost != null) + { + versions.put(order.getBillId(), cost.getVersionNo()); + } + } + catch (ServiceException ex) + { + skipped.add(order.getOrderNo() + ":" + ex.getMessage()); + } } - List fees = billIds.isEmpty() ? new ArrayList<>() : payBillMapper.selectOrderFees(billIds); + List fees = billIds.isEmpty() ? new ArrayList<>() : payBillMapper.selectFreeFees(billIds); + List skippedParty = new ArrayList<>(); + fees = keepSupplierFees(fees, orders, supplier, skippedParty); if (!fees.isEmpty() && fees.get(0).getCurrency() != null && !fees.get(0).getCurrency().isBlank()) { currency = fees.get(0).getCurrency(); @@ -146,17 +190,24 @@ public class PayBillServiceImpl implements IPayBillService line.setOrderBillId(fee.getBillId()); line.setOrderNo(order == null ? "" : order.getOrderNo()); line.setLadingNo(order == null ? "" : order.getLadingNo()); + line.setFeeId(fee.getFeeId()); line.setFeeName(fee.getFeeName() == null || fee.getFeeName().isBlank() ? fee.getFeeType() : fee.getFeeName()); + line.setFeeSource(fee.getFeeSource()); + line.setFeeLevel(fee.getFeeLevel()); line.setAmount(fee.getAmount()); + line.setDiffStatus("NONE"); line.setCurrency(fee.getCurrency()); line.setPriceDate(order == null || order.getBizDate() == null ? null : order.getBizDate()); line.setVersionNo(versions.get(fee.getBillId())); - line.setLineSource("ORDER"); + line.setLineSource("LADING".equals(fee.getFeeSource()) ? "LADING" : "ORDER"); lines.add(line); } if (lines.isEmpty()) { - throw PaymentMesEnum.BMS_PAY_BILL_FEE.exception(); + List notes = new ArrayList<>(); + notes.addAll(skipped); + notes.addAll(skippedParty); + throw new ServiceException(notes.isEmpty() ? PaymentMesEnum.BMS_PAY_BILL_FEE.getCnMes() : String.join(";", notes)); } BigDecimal total = BigDecimal.ZERO; for (PayBillLine line : lines) @@ -177,17 +228,53 @@ public class PayBillServiceImpl implements IPayBillService bill.setSourceType(mode(command)); bill.setPeriodStart(parseDay(command.getBeginDate())); bill.setPeriodEnd(parseDay(command.getEndDate())); - bill.setOrderCount(orders.size()); - bill.setTotalAmount(total.setScale(2, RoundingMode.HALF_UP)); + Set orderIds = new HashSet<>(); + for (PayBillLine line : lines) + { + if (line.getOrderBillId() != null) + { + orderIds.add(line.getOrderBillId()); + } + } + BigDecimal scaled = total.setScale(2, RoundingMode.HALF_UP); + bill.setOrderCount(orderIds.size()); + bill.setTotalAmount(scaled); + bill.setSupplierAmount(scaled); + bill.setDiffAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)); + bill.setPaidAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)); + bill.setBillKind("BLUE"); + int termDays = supplier.getZq() == null || supplier.getZq() < 0 ? 30 : supplier.getZq().intValue(); + bill.setTermDays(termDays); + Date due = parseDay(command.getDueDate()); + bill.setDueDate(due == null ? java.sql.Date.valueOf(LocalDate.now().plusDays(termDays)) : due); + bill.setTaxRate(BigDecimal.ZERO.setScale(4, RoundingMode.HALF_UP)); + bill.setExchangeRate(BigDecimal.ONE); + bill.setAmountNet(scaled); + bill.setTaxAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)); + bill.setBaseAmount(scaled); + bill.setRateDate(java.sql.Date.valueOf(LocalDate.now())); bill.setCreateBy(operator); bill.setRemark(command.getRemark()); payBillMapper.insertPayBill(bill); for (PayBillLine line : lines) { line.setBillId(bill.getBillId()); + payBillMapper.insertLine(line); + if (line.getFeeId() != null && !line.getFeeId().isBlank() && payBillMapper.bindFeeLine(line.getFeeId(), line.getLineId()) != 1) + { + throw PaymentMesEnum.BMS_PAY_FEE_TAKEN.exception(); + } } - payBillMapper.insertLines(lines); - writeLog(bill.getBillId(), "CREATE", null, "DRAFT", "生成应付账单,订单 " + orders.size() + " 票", operator); + String desc = "生成应付账单,订单 " + orderIds.size() + " 票,费用 " + lines.size() + " 行"; + if (!skipped.isEmpty()) + { + desc = desc + ";未补尾程:" + String.join(";", skipped); + } + if (!skippedParty.isEmpty()) + { + desc = desc + ";未并入:" + String.join(";", skippedParty); + } + writeLog(bill.getBillId(), "CREATE", null, "DRAFT", desc.length() > 500 ? desc.substring(0, 500) : desc, operator); return selectPayBillDetail(bill.getBillId()); } @@ -201,17 +288,7 @@ public class PayBillServiceImpl implements IPayBillService @Transactional(rollbackFor = Exception.class) public int confirm(Long billId) { - PayBill bill = required(billId); - if (!"DRAFT".equals(bill.getStatus())) - { - throw PaymentMesEnum.BMS_PAY_DRAFT.exception(); - } - String operator = SecurityUtils.getUsername(); - bill.setStatus("CONFIRMED"); - bill.setUpdateBy(operator); - payBillMapper.updatePayBillStatus(bill); - writeLog(billId, "CONFIRM", "DRAFT", "CONFIRMED", "确认应付账单", operator); - return 1; + throw PaymentMesEnum.BMS_PAY_USE_AUDIT.exception(); } /** @@ -224,17 +301,7 @@ public class PayBillServiceImpl implements IPayBillService @Transactional(rollbackFor = Exception.class) public int markPaid(Long billId) { - PayBill bill = required(billId); - if (!"CONFIRMED".equals(bill.getStatus())) - { - throw PaymentMesEnum.BMS_PAY_CONFIRM.exception(); - } - String operator = SecurityUtils.getUsername(); - bill.setStatus("PAID"); - bill.setUpdateBy(operator); - payBillMapper.updatePayBillStatus(bill); - writeLog(billId, "PAY", "CONFIRMED", "PAID", "登记付款", operator); - return 1; + throw PaymentMesEnum.BMS_PAY_USE_PAY.exception(); } /** @@ -260,12 +327,17 @@ public class PayBillServiceImpl implements IPayBillService { throw PaymentMesEnum.BMS_PAY_VOIDED.exception(); } - if ("PAID".equals(bill.getStatus())) + if ("PAID".equals(bill.getStatus()) || "PARTIAL".equals(bill.getStatus())) { throw PaymentMesEnum.BMS_PAY_PAID.exception(); } + if (!"DRAFT".equals(bill.getStatus()) && !"PENDING".equals(bill.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_NO_VOID.exception(); + } String operator = SecurityUtils.getUsername(); String from = bill.getStatus(); + payBillMapper.clearFeeLines(bill.getBillId()); bill.setStatus("VOID"); bill.setVoidReason(command.getVoidReason().trim()); bill.setVoidBy(operator); @@ -276,6 +348,483 @@ public class PayBillServiceImpl implements IPayBillService return 1; } + /** + * 草稿提交审核。有未处理差异或没有费用行时不能提交。 + * + * @param billId 应付账单ID + * @return 成功时为 1 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int submit(Long billId) + { + PayBill bill = required(billId); + if (!"DRAFT".equals(bill.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_SUBMIT.exception(); + } + List lines = payBillMapper.selectLines(billId); + if (lines.isEmpty()) + { + throw PaymentMesEnum.BMS_PAY_NO_LINE.exception(); + } + for (PayBillLine line : lines) + { + if ("OPEN".equals(line.getDiffStatus())) + { + throw PaymentMesEnum.BMS_PAY_DIFF_OPEN.exception(); + } + } + String operator = SecurityUtils.getUsername(); + payFinanceMapper.updateSubmitBy(billId, operator); + changeStatus(bill, "PENDING", operator); + approval(billId, "SUBMIT", "提交审核", operator); + writeLog(billId, "SUBMIT", "DRAFT", "PENDING", "提交审核", operator); + return 1; + } + + /** + * 审核通过。蓝字账单必须已经有发票或对账凭证。红字审核通过后,原账单改为已红冲并释放费用。 + * + * @param billId 应付账单ID + * @return 成功时为 1 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int approve(Long billId) + { + PayBill bill = required(billId); + if (!"PENDING".equals(bill.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_PENDING.exception(); + } + String operator = SecurityUtils.getUsername(); + if (operator.equals(bill.getCreateBy()) || (bill.getSubmitBy() != null && operator.equals(bill.getSubmitBy()))) + { + throw PaymentMesEnum.BMS_PAY_SAME_USER.exception(); + } + if (!"RED".equals(bill.getBillKind())) + { + matchInvoice(bill); + } + bill.setConfirmBy(operator); + bill.setConfirmTime(new Date()); + changeStatus(bill, "CONFIRMED", operator); + if ("RED".equals(bill.getBillKind()) && bill.getOriginalBillId() != null) + { + PayBill original = required(bill.getOriginalBillId()); + String originalStatus = original.getStatus(); + changeStatus(original, "RED", operator); + payBillMapper.clearFeeLines(original.getBillId()); + writeLog(original.getBillId(), "RED", originalStatus, "RED", "红字账单 " + bill.getBillNo() + " 已审核", operator); + } + approval(billId, "APPROVE", "审核通过", operator); + writeLog(billId, "APPROVE", "PENDING", "CONFIRMED", "审核通过", operator); + return 1; + } + + /** + * 审核驳回,账单回到草稿。 + * + * @param command 账单ID和驳回意见 + * @return 成功时为 1 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int reject(PayBillCommand command) + { + if (command == null || command.getBillId() == null) + { + throw PaymentMesEnum.BMS_PAY_PICK_BILL.exception(); + } + if (command.getOpinion() == null || command.getOpinion().isBlank()) + { + throw PaymentMesEnum.BMS_PAY_REJECT_WHY.exception(); + } + PayBill bill = required(command.getBillId()); + if (!"PENDING".equals(bill.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_PENDING.exception(); + } + String operator = SecurityUtils.getUsername(); + String opinion = command.getOpinion().trim(); + changeStatus(bill, "DRAFT", operator); + approval(bill.getBillId(), "REJECT", opinion, operator); + writeLog(bill.getBillId(), "REJECT", "PENDING", "DRAFT", opinion, operator); + return 1; + } + + /** + * 草稿登记对方金额。金额不一致时记为待处理差异。 + * + * @param command 账单ID和费用行上的对方金额 + * @return 成功时为 1 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int saveSupplierAmounts(PayBillCommand command) + { + if (command == null || command.getBillId() == null || command.getLines() == null) + { + throw PaymentMesEnum.BMS_PAY_LINE_MISS.exception(); + } + PayBill bill = required(command.getBillId()); + if (!"DRAFT".equals(bill.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_LINE_LOCK.exception(); + } + String operator = SecurityUtils.getUsername(); + for (PayBillLine input : command.getLines()) + { + if (input.getLineId() == null) + { + continue; + } + PayBillLine line = ownedLine(bill.getBillId(), input.getLineId()); + BigDecimal supplier = input.getSupplierAmount(); + line.setSupplierAmount(supplier); + if (supplier == null || sameMoney(line.getAmount(), supplier)) + { + line.setDiffAmount(null); + line.setDiffStatus("NONE"); + payBillMapper.deleteOpenDiff(line.getLineId()); + } + else + { + BigDecimal diff = supplier.subtract(line.getAmount() == null ? BigDecimal.ZERO : line.getAmount()); + line.setDiffAmount(diff); + line.setDiffStatus("OPEN"); + PayBillDiff open = payBillMapper.selectOpenDiff(line.getLineId()); + if (open == null) + { + open = new PayBillDiff(); + open.setBillId(bill.getBillId()); + open.setLineId(line.getLineId()); + open.setFeeId(line.getFeeId()); + open.setCreateBy(operator); + } + open.setOurAmount(line.getAmount()); + open.setSupplierAmount(supplier); + open.setDiffAmount(diff); + open.setReason(command.getReason()); + open.setHandleType("OPEN"); + if (open.getDiffId() == null) + { + payBillMapper.insertDiff(open); + } + else + { + payBillMapper.updateDiff(open); + } + } + payBillMapper.updateLineAmount(line); + } + refreshBillAmounts(bill.getBillId(), bill.getStatus(), operator); + writeLog(bill.getBillId(), "DIFF", "DRAFT", "DRAFT", "登记对方金额", operator); + return 1; + } + + /** + * 处理一行待处理差异。接受对方时按差额另写一条调整费用,不改原来的计算金额。 + * + * @param command 账单行和处理方式,OURS 或 THEIRS + * @return 成功时为 1 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int acceptDiff(PayBillCommand command) + { + if (command == null || command.getLineId() == null) + { + throw PaymentMesEnum.BMS_PAY_LINE_MISS.exception(); + } + PayBillLine line = payBillMapper.selectLine(command.getLineId()); + if (line == null) + { + throw PaymentMesEnum.BMS_PAY_LINE_MISS.exception(); + } + PayBill bill = required(line.getBillId()); + if (!"DRAFT".equals(bill.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_LINE_LOCK.exception(); + } + PayBillDiff open = payBillMapper.selectOpenDiff(line.getLineId()); + if (open == null || !"OPEN".equals(line.getDiffStatus())) + { + throw PaymentMesEnum.BMS_PAY_NO_DIFF.exception(); + } + String operator = SecurityUtils.getUsername(); + String handle = "THEIRS".equalsIgnoreCase(command.getHandleType()) ? "ACCEPT_THEIRS" : "ACCEPT_OURS"; + open.setHandleType(handle); + open.setReason(command.getReason() == null ? open.getReason() : command.getReason().trim()); + if ("ACCEPT_THEIRS".equals(handle)) + { + BigDecimal delta = open.getDiffAmount(); + PrcPayBillFee source = null; + if (line.getFeeId() != null) + { + List existed = payBillFeeMapper.selectByBillId(line.getOrderBillId()); + for (PrcPayBillFee fee : existed) + { + if (line.getFeeId().equals(fee.getFeeId())) + { + source = fee; + break; + } + } + } + PrcPayBillFee adjust = new PrcPayBillFee(); + adjust.setFeeId(UUID.randomUUID().toString()); + adjust.setBillId(line.getOrderBillId()); + adjust.setPayBillId(source == null ? null : source.getPayBillId()); + adjust.setFeeType("对账调整"); + adjust.setFeeName("对账调整"); + adjust.setScId(open.getDiffId()); + adjust.setFeeLevel(line.getFeeLevel()); + adjust.setAmount(delta); + adjust.setCurrency(line.getCurrency()); + adjust.setFeeSource("MANUAL"); + adjust.setSourceId(String.valueOf(open.getDiffId())); + adjust.setRemark(open.getReason()); + adjust.setSettleSupplierId(bill.getSupplierId()); + adjust.setSettleSupplierName(bill.getSupplierName()); + payBillFeeMapper.insertPrcPayBillFee(adjust); + payFinanceMapper.stampFee(adjust.getFeeId(), bill.getSupplierId(), bill.getSupplierName()); + if (line.getOrderBillId() != null) + { + payBillFeeService.refreshTotals(List.of(line.getOrderBillId())); + } + PayBillLine extra = new PayBillLine(); + extra.setBillId(bill.getBillId()); + extra.setFeeId(adjust.getFeeId()); + extra.setOrderBillId(line.getOrderBillId()); + extra.setOrderNo(line.getOrderNo()); + extra.setLadingNo(line.getLadingNo()); + extra.setFeeName(adjust.getFeeName()); + extra.setFeeSource("MANUAL"); + extra.setFeeLevel(line.getFeeLevel()); + extra.setAmount(delta); + extra.setDiffStatus("NONE"); + extra.setCurrency(line.getCurrency()); + extra.setLineSource("ORDER"); + payBillMapper.insertLine(extra); + payBillMapper.bindFeeLine(adjust.getFeeId(), extra.getLineId()); + open.setAdjustFeeId(adjust.getFeeId()); + } + payBillMapper.updateDiff(open); + line.setDiffStatus(handle); + payBillMapper.updateLineAmount(line); + refreshBillAmounts(bill.getBillId(), bill.getStatus(), operator); + writeLog(bill.getBillId(), "ACCEPT", "DRAFT", "DRAFT", "ACCEPT_THEIRS".equals(handle) ? "接受对方金额" : "接受我方金额", operator); + return 1; + } + + /** + * 从草稿剔除一条费用,并释放这条费用,使它可以进入别的账单。 + * + * @param command 账单ID和行ID + * @return 成功时为 1 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int removeLine(PayBillCommand command) + { + if (command == null || command.getBillId() == null || command.getLineId() == null) + { + throw PaymentMesEnum.BMS_PAY_LINE_MISS.exception(); + } + PayBill bill = required(command.getBillId()); + if (!"DRAFT".equals(bill.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_LINE_LOCK.exception(); + } + PayBillLine line = ownedLine(bill.getBillId(), command.getLineId()); + String operator = SecurityUtils.getUsername(); + payBillMapper.clearFeeLine(line.getLineId()); + payBillMapper.deleteDiffsByLine(line.getLineId()); + payBillMapper.deleteLine(bill.getBillId(), line.getLineId()); + refreshBillAmounts(bill.getBillId(), bill.getStatus(), operator); + PayBillLog log = new PayBillLog(); + log.setBillId(bill.getBillId()); + log.setLineId(line.getLineId()); + log.setFeeId(line.getFeeId()); + log.setOperationType("REMOVE"); + log.setFromStatus("DRAFT"); + log.setToStatus("DRAFT"); + log.setOperationDesc("剔除 " + line.getFeeName() + " " + line.getAmount()); + log.setAmountBefore(line.getAmount()); + log.setOperatorName(operator); + log.setBizType("BILL"); + log.setBizId(bill.getBillId()); + payBillMapper.insertLog(log); + return 1; + } + + /** + * 给账单挂上一张发票或对账文件。 + * + * @param file 账单、类型、发票号和文件地址 + * @return 成功时为 1 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int saveFile(PayBillFile file) + { + if (file == null || file.getBillId() == null || file.getFileUrl() == null || file.getFileUrl().isBlank()) + { + throw PaymentMesEnum.BMS_PAY_FILE_NEED.exception(); + } + PayBill bill = required(file.getBillId()); + if ("VOID".equals(bill.getStatus()) || "RED".equals(bill.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_FILE_LOCK.exception(); + } + if (file.getFileType() == null || file.getFileType().isBlank()) + { + file.setFileType("OTHER"); + } + if ("INVOICE".equals(file.getFileType())) + { + if (file.getInvoiceNo() == null || file.getInvoiceNo().isBlank()) + { + throw PaymentMesEnum.BMS_PAY_INV_NO.exception(); + } + if (file.getAmount() == null) + { + throw PaymentMesEnum.BMS_PAY_INV_NEED.exception(); + } + BigDecimal tax = file.getTaxAmount() == null ? BigDecimal.ZERO : file.getTaxAmount(); + file.setInvoiceNo(file.getInvoiceNo().trim()); + file.setTaxAmount(tax.setScale(2, RoundingMode.HALF_UP)); + file.setAmount(file.getAmount().setScale(2, RoundingMode.HALF_UP)); + file.setAmountNet(file.getAmount().subtract(file.getTaxAmount())); + file.setCurrency(bill.getCurrency()); + if (payFinanceMapper.countDuplicateInvoice(bill.getSupplierId(), file.getInvoiceNo(), null) > 0) + { + throw PaymentMesEnum.BMS_PAY_INV_DUP.exception(); + } + } + file.setCreateBy(SecurityUtils.getUsername()); + payBillMapper.insertFile(file); + writeLog(file.getBillId(), "FILE", bill.getStatus(), bill.getStatus(), "上传凭证 " + (file.getInvoiceNo() == null ? file.getFileName() : file.getInvoiceNo()), file.getCreateBy()); + return 1; + } + + /** + * 删除草稿或待审核账单上的凭证。 + * + * @param fileId 凭证ID + * @return 成功时为 1 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteFile(Long fileId) + { + PayBillFile file = payBillMapper.selectFile(fileId); + if (file == null || "1".equals(file.getDeleted())) + { + throw PaymentMesEnum.BMS_PAY_FILE_MISS.exception(); + } + PayBill bill = required(file.getBillId()); + if (!"DRAFT".equals(bill.getStatus()) && !"PENDING".equals(bill.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_FILE_LOCK.exception(); + } + payBillMapper.markFileDeleted(fileId); + writeLog(file.getBillId(), "FILE_DEL", bill.getStatus(), bill.getStatus(), "删除凭证", SecurityUtils.getUsername()); + return 1; + } + + /** + * 按账单号、供应商、发票号查询未删除的应付凭证。 + * + * @param query 查询条件 + * @return 凭证列表 + */ + @Override + public List listFiles(PayBillFile query) + { + return payBillMapper.selectFileList(query == null ? new PayBillFile() : query); + } + + /** + * 对已确认且还没有付款的账单开红字草稿。审核通过后原费用才能再次出账。 + * + * @param command 原账单ID和原因 + * @return 红字草稿 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public PayBill redBill(PayBillCommand command) + { + if (command == null || command.getBillId() == null) + { + throw PaymentMesEnum.BMS_PAY_PICK_BILL.exception(); + } + PayBill original = required(command.getBillId()); + if ("RED".equals(original.getBillKind())) + { + throw PaymentMesEnum.BMS_PAY_RED_KIND.exception(); + } + if (!"CONFIRMED".equals(original.getStatus()) && !"PARTIAL".equals(original.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_RED_STAT.exception(); + } + if (original.getPaidAmount() != null && original.getPaidAmount().compareTo(BigDecimal.ZERO) > 0) + { + throw PaymentMesEnum.BMS_PAY_RED_PAID.exception(); + } + String operator = SecurityUtils.getUsername(); + List sourceLines = payBillMapper.selectLines(original.getBillId()); + PayBill bill = new PayBill(); + bill.setBillNo(nextNo()); + bill.setSupplierId(original.getSupplierId()); + bill.setSupplierName(original.getSupplierName()); + bill.setCurrency(original.getCurrency()); + bill.setSourceType(original.getSourceType()); + bill.setOrderCount(original.getOrderCount()); + bill.setTotalAmount(original.getTotalAmount() == null ? BigDecimal.ZERO : original.getTotalAmount().negate()); + bill.setSupplierAmount(bill.getTotalAmount()); + bill.setDiffAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)); + bill.setPaidAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)); + bill.setBillKind("RED"); + bill.setOriginalBillId(original.getBillId()); + BigDecimal rate = original.getTaxRate() == null ? BigDecimal.ZERO : original.getTaxRate(); + BigDecimal fx = original.getExchangeRate() == null ? BigDecimal.ONE : original.getExchangeRate(); + bill.setTaxRate(rate); + bill.setExchangeRate(fx); + bill.setAmountNet(original.getAmountNet() == null ? bill.getTotalAmount() : original.getAmountNet().negate()); + bill.setTaxAmount(original.getTaxAmount() == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) : original.getTaxAmount().negate()); + bill.setBaseAmount(bill.getTotalAmount().multiply(fx).setScale(2, RoundingMode.HALF_UP)); + bill.setRateDate(original.getRateDate()); + bill.setTermDays(original.getTermDays()); + bill.setDueDate(original.getDueDate()); + bill.setCreateBy(operator); + bill.setRemark(command.getReason()); + payBillMapper.insertPayBill(bill); + for (PayBillLine source : sourceLines) + { + PayBillLine line = new PayBillLine(); + line.setBillId(bill.getBillId()); + line.setFeeId(source.getFeeId()); + line.setOrderBillId(source.getOrderBillId()); + line.setOrderNo(source.getOrderNo()); + line.setLadingNo(source.getLadingNo()); + line.setFeeName(source.getFeeName()); + line.setFeeSource(source.getFeeSource()); + line.setFeeLevel(source.getFeeLevel()); + line.setAmount(source.getAmount() == null ? BigDecimal.ZERO : source.getAmount().negate()); + line.setDiffStatus("NONE"); + line.setCurrency(source.getCurrency()); + line.setLineSource(source.getLineSource()); + payBillMapper.insertLine(line); + } + payFinanceMapper.updateLineTax(bill.getBillId(), bill.getTaxRate(), bill.getExchangeRate()); + writeLog(bill.getBillId(), "RED", null, "DRAFT", "红字,原账单 " + original.getBillNo(), operator); + writeLog(original.getBillId(), "RED", original.getStatus(), original.getStatus(), "开出红字 " + bill.getBillNo(), operator); + return selectPayBillDetail(bill.getBillId()); + } + /** * 按订单号、计费日或提单号查出订单,并分成可生成和已入账两组。 * @@ -332,7 +881,11 @@ public class PayBillServiceImpl implements IPayBillService } for (PayBillOrder order : found) { - if (order.getOccupiedBillNo() != null && !order.getOccupiedBillNo().isBlank()) + int free = order.getUnbilledCount() == null ? 0 : order.getUnbilledCount(); + int feeCount = order.getFeeCount() == null ? 0 : order.getFeeCount(); + boolean locked = feeCount > 0 && free == 0; + boolean legacyEmpty = feeCount == 0 && order.getOccupiedBillNo() != null && !order.getOccupiedBillNo().isBlank(); + if (locked || legacyEmpty) { preview.getOccupied().add(order); } @@ -402,12 +955,359 @@ public class PayBillServiceImpl implements IPayBillService return null; } - /** - * 读取账单头。账单不存在时抛出业务异常。 - * - * @param billId 应付账单ID - * @return 账单头 - */ + @Override + @Transactional(rollbackFor = Exception.class) + public int saveFinance(PayBillCommand command) + { + if (command == null || command.getBillId() == null) + { + throw PaymentMesEnum.BMS_PAY_PICK_BILL.exception(); + } + PayBill bill = required(command.getBillId()); + if (!"DRAFT".equals(bill.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_LINE_LOCK.exception(); + } + BigDecimal rate = command.getTaxRate() == null ? BigDecimal.ZERO : command.getTaxRate(); + if (rate.compareTo(BigDecimal.ZERO) < 0 || rate.compareTo(BigDecimal.ONE) >= 0) + { + throw PaymentMesEnum.BMS_PAY_TAX_RATE.exception(); + } + BigDecimal fx = command.getExchangeRate() == null ? BigDecimal.ONE : command.getExchangeRate(); + if (fx.compareTo(BigDecimal.ZERO) <= 0) + { + throw PaymentMesEnum.BMS_PAY_RATE.exception(); + } + Date rateDate = parseDay(command.getRateDate()); + if (rateDate == null) + { + rateDate = java.sql.Date.valueOf(LocalDate.now()); + } + payFinanceMapper.updateFinanceHead(bill.getBillId(), rate, fx, rateDate); + refreshBillAmounts(bill.getBillId(), bill.getStatus(), SecurityUtils.getUsername()); + writeLog(bill.getBillId(), "TAX", "DRAFT", "DRAFT", "登记税率 " + rate + ",汇率 " + fx, SecurityUtils.getUsername()); + return 1; + } + + @Override + public void exportStatement(Long billId, OutputStream out) + { + PayBill bill = required(billId); + List lines = payBillMapper.selectLines(billId); + try (XSSFWorkbook book = new XSSFWorkbook()) + { + Sheet sheet = book.createSheet("对账单"); + Row head = sheet.createRow(0); + String[] titles = { "行号", "订单号", "提单号", "费用", "我方金额", "对方金额" }; + for (int i = 0; i < titles.length; i++) + { + head.createCell(i).setCellValue(titles[i]); + } + int rowNo = 1; + for (PayBillLine line : lines) + { + Row row = sheet.createRow(rowNo++); + row.createCell(0).setCellValue(line.getLineId() == null ? 0 : line.getLineId()); + row.createCell(1).setCellValue(line.getOrderNo() == null ? "" : line.getOrderNo()); + row.createCell(2).setCellValue(line.getLadingNo() == null ? "" : line.getLadingNo()); + row.createCell(3).setCellValue(line.getFeeName() == null ? "" : line.getFeeName()); + row.createCell(4).setCellValue(line.getAmount() == null ? 0 : line.getAmount().doubleValue()); + if (line.getSupplierAmount() != null) + { + row.createCell(5).setCellValue(line.getSupplierAmount().doubleValue()); + } + } + book.write(out); + } + catch (Exception ex) + { + throw new ServiceException("对账单导出失败"); + } + writeLog(bill.getBillId(), "EXPORT", bill.getStatus(), bill.getStatus(), "导出对账单", SecurityUtils.getUsername()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int importSupplierAmounts(Long billId, MultipartFile file) + { + PayBill bill = required(billId); + if (!"DRAFT".equals(bill.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_LINE_LOCK.exception(); + } + if (file == null || file.isEmpty()) + { + throw PaymentMesEnum.BMS_PAY_PICK_XLS.exception(); + } + List inputs = new ArrayList<>(); + DataFormatter formatter = new DataFormatter(); + try (InputStream in = file.getInputStream(); Workbook book = new XSSFWorkbook(in)) + { + Sheet sheet = book.getNumberOfSheets() == 0 ? null : book.getSheetAt(0); + if (sheet == null || sheet.getPhysicalNumberOfRows() < 2) + { + throw PaymentMesEnum.BMS_PAY_XLS_LINE.exception(); + } + int lineCol = 0; + int amountCol = 5; + Row head = sheet.getRow(0); + if (head != null) + { + for (int i = 0; i < head.getLastCellNum(); i++) + { + String title = formatter.formatCellValue(head.getCell(i)).trim(); + if ("行号".equals(title)) + { + lineCol = i; + } + if ("对方金额".equals(title)) + { + amountCol = i; + } + } + } + for (int i = 1; i <= sheet.getLastRowNum(); i++) + { + Row row = sheet.getRow(i); + if (row == null) + { + continue; + } + String lineText = formatter.formatCellValue(row.getCell(lineCol)).trim(); + String amountText = formatter.formatCellValue(row.getCell(amountCol)).trim(); + if (lineText.isEmpty() || amountText.isEmpty()) + { + continue; + } + PayBillLine line = new PayBillLine(); + line.setLineId(new BigDecimal(lineText).longValue()); + line.setSupplierAmount(new BigDecimal(amountText)); + inputs.add(line); + } + } + catch (ServiceException ex) + { + throw ex; + } + catch (Exception ex) + { + throw new ServiceException("对账单无法读取,请使用导出的 xlsx"); + } + if (inputs.isEmpty()) + { + throw PaymentMesEnum.BMS_PAY_XLS_LINE.exception(); + } + PayBillCommand command = new PayBillCommand(); + command.setBillId(billId); + command.setLines(inputs); + return saveSupplierAmounts(command); + } + + private List keepSupplierFees(List fees, List orders, BmsSupplier supplier, List skipped) + { + List kept = new ArrayList<>(); + Set notes = new LinkedHashSet<>(); + Map cards = new HashMap<>(); + for (PrcPayBillFee fee : fees) + { + PayBillOrder order = findOrder(orders, fee.getBillId()); + String orderNo = order == null || order.getOrderNo() == null ? fee.getBillId() : order.getOrderNo(); + String why = partyReason(fee, order, supplier, cards); + if (why == null) + { + payFinanceMapper.stampFee(fee.getFeeId(), supplier.getSupplierId(), supplier.getSupplierName()); + kept.add(fee); + } + else + { + notes.add(orderNo + ":" + why); + } + } + skipped.addAll(notes); + return kept; + } + + private String partyReason(PrcPayBillFee fee, PayBillOrder order, BmsSupplier supplier, Map cards) + { + if (fee.getSettleSupplierId() != null) + { + return fee.getSettleSupplierId().equals(supplier.getSupplierId()) ? null : "付款对象不是所选供应商"; + } + String source = fee.getFeeSource() == null || fee.getFeeSource().isBlank() ? "CALC" : fee.getFeeSource(); + if ("LADING".equals(source)) + { + String ladingNo = order == null ? null : order.getLadingNo(); + Long settleId = ladingNo == null || ladingNo.isBlank() ? null : payFinanceMapper.selectLadingSupplier(ladingNo); + if (settleId == null) + { + return "提单费用没有付款对象,未并入"; + } + return settleId.equals(supplier.getSupplierId()) ? null : "提单费用付给其他供应商"; + } + if ("MANUAL".equals(source)) + { + return "人工费用没有付款对象,未并入"; + } + String card = cards.computeIfAbsent(fee.getBillId(), payOrderCostService::cardSupplierName); + if (card == null || card.isBlank()) + { + return "价卡没有归属供应商,未并入"; + } + if (!paySettleSupport.same(supplier, card)) + { + return "价卡归属「" + card + "」,与所选供应商不一致"; + } + return null; + } + + private void matchInvoice(PayBill bill) + { + List files = payBillMapper.selectFiles(bill.getBillId()); + BigDecimal invoice = BigDecimal.ZERO; + BigDecimal tax = BigDecimal.ZERO; + int count = 0; + for (PayBillFile file : files) + { + if (!"INVOICE".equals(file.getFileType())) + { + continue; + } + if (file.getInvoiceNo() == null || file.getInvoiceNo().isBlank() || file.getAmount() == null) + { + throw PaymentMesEnum.BMS_PAY_INV_NEED.exception(); + } + count++; + invoice = invoice.add(file.getAmount()); + tax = tax.add(file.getTaxAmount() == null ? BigDecimal.ZERO : file.getTaxAmount()); + } + if (count == 0) + { + throw PaymentMesEnum.BMS_PAY_INV_NEED.exception(); + } + if (invoice.setScale(2, RoundingMode.HALF_UP).compareTo(money(bill.getTotalAmount())) != 0) + { + throw PaymentMesEnum.BMS_PAY_INV_AMT.exception(); + } + if (tax.setScale(2, RoundingMode.HALF_UP).compareTo(money(bill.getTaxAmount())) != 0) + { + throw PaymentMesEnum.BMS_PAY_INV_TAX.exception(); + } + payFinanceMapper.markInvoiceMatched(bill.getBillId()); + } + + private static BigDecimal money(BigDecimal value) + { + return (value == null ? BigDecimal.ZERO : value).setScale(2, RoundingMode.HALF_UP); + } + + private PayBillLine ownedLine(Long billId, Long lineId) + { + PayBillLine line = payBillMapper.selectLine(lineId); + if (line == null || line.getBillId() == null || !line.getBillId().equals(billId)) + { + throw PaymentMesEnum.BMS_PAY_LINE_MISS.exception(); + } + return line; + } + + private void changeStatus(PayBill bill, String status, String operator) + { + if (bill.getTotalAmount() == null) + { + bill.setTotalAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)); + } + if (bill.getSupplierAmount() == null) + { + bill.setSupplierAmount(bill.getTotalAmount()); + } + if (bill.getDiffAmount() == null) + { + bill.setDiffAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)); + } + if (bill.getPaidAmount() == null) + { + bill.setPaidAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)); + } + if (bill.getTaxRate() == null) + { + bill.setTaxRate(BigDecimal.ZERO.setScale(4, RoundingMode.HALF_UP)); + } + if (bill.getExchangeRate() == null || bill.getExchangeRate().compareTo(BigDecimal.ZERO) <= 0) + { + bill.setExchangeRate(BigDecimal.ONE); + } + if (bill.getAmountNet() == null) + { + bill.setAmountNet(bill.getTotalAmount()); + } + if (bill.getTaxAmount() == null) + { + bill.setTaxAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)); + } + if (bill.getBaseAmount() == null) + { + bill.setBaseAmount(bill.getTotalAmount().multiply(bill.getExchangeRate()).setScale(2, RoundingMode.HALF_UP)); + } + if (bill.getOrderCount() == null) + { + bill.setOrderCount(0); + } + bill.setStatus(status); + bill.setUpdateBy(operator); + payBillMapper.updatePayBillMoney(bill); + } + + private void refreshBillAmounts(Long billId, String status, String operator) + { + PayBill bill = required(billId); + List lines = payBillMapper.selectLines(billId); + BigDecimal total = BigDecimal.ZERO; + BigDecimal supplier = BigDecimal.ZERO; + Set orders = new HashSet<>(); + for (PayBillLine line : lines) + { + BigDecimal amount = line.getAmount() == null ? BigDecimal.ZERO : line.getAmount(); + total = total.add(amount); + supplier = supplier.add(line.getSupplierAmount() == null ? amount : line.getSupplierAmount()); + if (line.getOrderBillId() != null) + { + orders.add(line.getOrderBillId()); + } + } + BigDecimal scaledTotal = total.setScale(2, RoundingMode.HALF_UP); + BigDecimal rate = bill.getTaxRate() == null ? BigDecimal.ZERO : bill.getTaxRate(); + BigDecimal fx = bill.getExchangeRate() == null || bill.getExchangeRate().compareTo(BigDecimal.ZERO) <= 0 ? BigDecimal.ONE : bill.getExchangeRate(); + BigDecimal net = scaledTotal.divide(BigDecimal.ONE.add(rate), 2, RoundingMode.HALF_UP); + bill.setTotalAmount(scaledTotal); + bill.setSupplierAmount(supplier.setScale(2, RoundingMode.HALF_UP)); + bill.setDiffAmount(supplier.subtract(total).setScale(2, RoundingMode.HALF_UP)); + bill.setTaxRate(rate); + bill.setExchangeRate(fx); + bill.setAmountNet(net); + bill.setTaxAmount(scaledTotal.subtract(net)); + bill.setBaseAmount(scaledTotal.multiply(fx).setScale(2, RoundingMode.HALF_UP)); + bill.setOrderCount(orders.size()); + payFinanceMapper.updateLineTax(billId, rate, fx); + changeStatus(bill, status, operator); + } + + private void approval(Long billId, String action, String opinion, String operator) + { + PayBillApproval row = new PayBillApproval(); + row.setBillId(billId); + row.setAction(action); + row.setOpinion(opinion); + row.setOperatorName(operator); + payBillMapper.insertApproval(row); + } + + private boolean sameMoney(BigDecimal left, BigDecimal right) + { + BigDecimal one = left == null ? BigDecimal.ZERO : left; + BigDecimal two = right == null ? BigDecimal.ZERO : right; + return one.setScale(4, RoundingMode.HALF_UP).compareTo(two.setScale(4, RoundingMode.HALF_UP)) == 0; + } + private PayBill required(Long billId) { PayBill bill = payBillMapper.selectPayBillById(billId); @@ -432,6 +1332,8 @@ public class PayBillServiceImpl implements IPayBillService { PayBillLog log = new PayBillLog(); log.setBillId(billId); + log.setBizType("BILL"); + log.setBizId(billId); log.setOperationType(type); log.setFromStatus(from); log.setToStatus(to); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayOrderCostServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayOrderCostServiceImpl.java index b7736d7c..a22499c2 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayOrderCostServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayOrderCostServiceImpl.java @@ -5,6 +5,7 @@ import java.util.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import com.bms.common.enums.PaymentMesEnum; import com.bms.common.exception.ServiceException; @@ -17,6 +18,7 @@ import com.bms.price.domain.PayPostcodeGroup; import com.bms.price.domain.PayPostcodeLine; import com.bms.price.domain.PayProduct; import com.bms.price.domain.PaySurcharge; +import com.bms.price.domain.PaySurchargeRule; import com.bms.price.domain.PayWeightBand; import com.bms.price.domain.PrcOrderBill; import com.bms.price.domain.PrcOrderBillExt; @@ -28,11 +30,13 @@ import com.bms.price.mapper.PrcOrderBillMapper; import com.bms.price.mapper.PrcPayBillFeeMapper; import com.bms.price.mapper.PrcPayBillMapper; import com.bms.price.pay.PayCostCalculator; +import com.bms.price.pay.PaySettleSupport; import com.bms.price.pay.PayRemoteBook; import com.bms.price.pay.PaySurchargeRules; import com.bms.price.pay.PayZoneMatcher; import com.bms.price.service.IPayOrderCostService; import com.bms.price.service.IPayProductService; +import com.bms.price.service.IPaySurchargeRuleService; import com.bms.price.service.IPrcPayBillFeeService; /** @@ -62,9 +66,15 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService @Autowired private IPrcPayBillFeeService payBillFeeService; + @Autowired + private PaySettleSupport paySettleSupport; + @Autowired private PayPostcodeMapper payPostcodeMapper; + @Autowired + private IPaySurchargeRuleService paySurchargeRuleService; + /** * 重算一票订单的尾程应付并落库。 * 先找渠道和计费日对应的价卡版本,再用重量、尺寸、邮编算出费用,写回订单应付。 @@ -72,6 +82,13 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService * @param billId 计费订单ID * @return 本次使用的渠道、价卡版本和费用明细 */ + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) + public PayOrderCostResult generateIsolated(String billId) + { + return generate(billId); + } + @Override @Transactional(rollbackFor = Exception.class) public PayOrderCostResult generate(String billId) @@ -93,10 +110,21 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService Integer orderStatus = bill.getOrderStatus() != null ? bill.getOrderStatus() : (ext == null ? null : ext.getOrderStatus()); List linked = linkedGroups(product); String zoneName = resolveZone(channel, product, bill.getDestPostcode(), bill.getCountryCode(), bill.getCountryName(), linked); - // 根据邮编判断是否是偏远地区 - List remoteHits = remoteHits(linked, bill.getDestPostcode(), bill.getCountryCode(), bill.getCountryName()); + List billingRules = billingRules(product); + product.setBillingRules(billingRules); + boolean postcodeOn = ruleOn(billingRules, PaySurchargeRules.POSTCODE); + boolean zoneOn = ruleOn(billingRules, PaySurchargeRules.ZONE); + List remoteHits = null; + if (postcodeOn) + { + remoteHits = remoteHits(linked, bill.getDestPostcode(), bill.getCountryCode(), bill.getCountryName()); + if (remoteHits == null) + { + remoteHits = new ArrayList<>(); + } + } PayOrderCostResult result = PayCostCalculator.calculate(product, weight, bill.getPkgLength(), bill.getPkgWidth(), - bill.getPkgHeight(), bill.getDestPostcode(), zoneName, bizDate, orderStatus, remoteHits); + bill.getPkgHeight(), bill.getDestPostcode(), zoneName, bizDate, orderStatus, remoteHits, postcodeOn, zoneOn); result.setBillId(bill.getBillId()); result.setChannelId(channel.getId()); result.setChannelName(channel.getChannelName()); @@ -112,15 +140,64 @@ public class PayOrderCostServiceImpl implements IPayOrderCostService String end = product.getTermEnd() == null ? "不限" : day(product.getTermEnd()); result.setMessage("渠道「" + channelName + "」产品编号 " + product.getOriginId() + " 第 " + product.getVersionNo() + " 版(" + day(product.getTermStart()) + " 至 " + end + ")," + result.getMessage()); + paySettleSupport.stampCalc(bill.getBillId(), product.getSupplierName()); return result; } + @Override + public String cardSupplierName(String billId) + { + if (billId == null || billId.isBlank()) + { + return ""; + } + try + { + PrcOrderBill bill = orderBillMapper.selectPrcOrderBillById(billId.trim()); + if (bill == null) + { + return ""; + } + PrcOrderBillExt ext = orderBillExtMapper.selectByBillId(bill.getBillId()); + ServiceChannel channel = resolveChannel(ext); + Date bizDate = bill.getBizDate() != null ? bill.getBizDate() : bill.getOrderTime(); + PayProduct product = payProductService.selectPriceForDate(channel.getId(), bizDate); + return product.getSupplierName() == null ? "" : product.getSupplierName().trim(); + } + catch (ServiceException ex) + { + return ""; + } + } + /** * 用产品名称、报价工作表、字典值和关联分区表,对上这份产品的邮编配置。 * * @param product 当前价卡版本 * @return 名称能对上的邮编组,没有时为空列表 */ + private List billingRules(PayProduct product) + { + if (product == null || product.getProductId() == null || product.getTermId() == null) + { + return new ArrayList<>(); + } + List rules = paySurchargeRuleService.list(product.getProductId(), product.getTermId()); + return rules == null ? new ArrayList<>() : rules; + } + + private static boolean ruleOn(List rules, String calcRule) + { + for (PaySurchargeRule rule : rules) + { + if (rule != null && calcRule.equals(rule.getCalcRule()) && !"0".equals(rule.getEnabled())) + { + return true; + } + } + return false; + } + private List linkedGroups(PayProduct product) { List names = new ArrayList<>(); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayPaymentServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayPaymentServiceImpl.java new file mode 100644 index 00000000..ca85a16f --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayPaymentServiceImpl.java @@ -0,0 +1,413 @@ +package com.bms.price.service.impl; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.bms.common.enums.PaymentMesEnum; +import com.bms.common.utils.SecurityUtils; +import com.bms.common.utils.uuid.IdUtils; +import com.bms.customer.supplierinfo.domain.BmsSupplier; +import com.bms.customer.supplierinfo.mapper.BmsSupplierMapper; +import com.bms.price.domain.PayBill; +import com.bms.price.domain.PayBillLog; +import com.bms.price.domain.PayPayment; +import com.bms.price.domain.PayPaymentAlloc; +import com.bms.price.mapper.PayBillMapper; +import com.bms.price.mapper.PayPaymentMapper; +import com.bms.price.service.IPayPaymentService; + +/** 应付付款。确认时按核销金额回写账单,一张付款可以核多张账单。 */ +@Service +public class PayPaymentServiceImpl implements IPayPaymentService +{ + @Autowired + private PayPaymentMapper payPaymentMapper; + + @Autowired + private PayBillMapper payBillMapper; + + @Autowired + private BmsSupplierMapper supplierMapper; + + @Override + public List selectPayPaymentList(PayPayment query) + { + return payPaymentMapper.selectPayPaymentList(query); + } + + @Override + public PayPayment selectPayPaymentDetail(Long paymentId) + { + PayPayment payment = required(paymentId); + payment.setAllocs(payPaymentMapper.selectAllocs(paymentId)); + return payment; + } + + @Override + public List selectOpenBills(Long supplierId, String currency) + { + if (supplierId == null) + { + throw PaymentMesEnum.BMS_PAY_PICK_SUP.exception(); + } + if (currency == null || currency.isBlank()) + { + throw PaymentMesEnum.BMS_PAY_CURRENCY.exception(); + } + return payBillMapper.selectOpenBills(supplierId, currency.trim()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public PayPayment create(PayPayment payment) + { + if (payment == null || payment.getSupplierId() == null) + { + throw PaymentMesEnum.BMS_PAY_PICK_SUP.exception(); + } + if (payment.getAmount() == null || payment.getAmount().compareTo(BigDecimal.ZERO) <= 0) + { + throw PaymentMesEnum.BMS_PAY_PAY_AMT.exception(); + } + if (payment.getPayDate() == null) + { + throw PaymentMesEnum.BMS_PAY_PAY_DATE.exception(); + } + if (payment.getCurrency() == null || payment.getCurrency().isBlank()) + { + throw PaymentMesEnum.BMS_PAY_CURRENCY.exception(); + } + BmsSupplier supplier = supplierMapper.selectBmsSupplierBySupplierId(payment.getSupplierId()); + if (supplier == null) + { + throw PaymentMesEnum.BMS_PAY_SUP_MISS.exception(); + } + String operator = SecurityUtils.getUsername(); + payment.setPaymentNo(nextNo()); + payment.setSupplierName(supplier.getSupplierName()); + payment.setCurrency(payment.getCurrency().trim()); + payment.setPayMethod(payment.getPayMethod() == null || payment.getPayMethod().isBlank() ? "BANK" : payment.getPayMethod()); + payment.setBankFee(payment.getBankFee() == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) : payment.getBankFee()); + payment.setAmount(payment.getAmount().setScale(2, RoundingMode.HALF_UP)); + if (payment.getExchangeRate() == null || payment.getExchangeRate().compareTo(BigDecimal.ZERO) <= 0) + { + payment.setExchangeRate(BigDecimal.ONE); + } + payment.setCreateBy(operator); + checkAllocs(payment); + payPaymentMapper.insertPayPayment(payment); + if (payment.getAllocs() != null) + { + for (PayPaymentAlloc alloc : payment.getAllocs()) + { + if (alloc.getBillId() == null || alloc.getAllocAmount() == null || alloc.getAllocAmount().compareTo(BigDecimal.ZERO) <= 0) + { + continue; + } + alloc.setPaymentId(payment.getPaymentId()); + alloc.setAllocAmount(alloc.getAllocAmount().setScale(2, RoundingMode.HALF_UP)); + payPaymentMapper.insertAlloc(alloc); + } + } + return selectPayPaymentDetail(payment.getPaymentId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int confirm(Long paymentId) + { + PayPayment payment = required(paymentId); + if (!"DRAFT".equals(payment.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_PAY_DRAFT.exception(); + } + payment.setAllocs(payPaymentMapper.selectAllocs(paymentId)); + checkAllocs(payment); + String operator = SecurityUtils.getUsername(); + if (operator.equals(payment.getCreateBy())) + { + throw PaymentMesEnum.BMS_PAY_SAME_PAY.exception(); + } + BigDecimal rate = payment.getExchangeRate() == null ? BigDecimal.ONE : payment.getExchangeRate(); + BigDecimal allocSum = BigDecimal.ZERO; + if (payment.getAllocs() != null) + { + for (PayPaymentAlloc alloc : payment.getAllocs()) + { + if (alloc.getAllocAmount() == null || alloc.getAllocAmount().compareTo(BigDecimal.ZERO) <= 0) + { + continue; + } + PayBill bill = payBillMapper.selectPayBillById(alloc.getBillId()); + applyAlloc(payment, bill, alloc.getAllocAmount(), operator); + allocSum = allocSum.add(alloc.getAllocAmount()); + } + } + payment.setExchangeRate(rate); + payment.setAllocAmount(allocSum.setScale(2, RoundingMode.HALF_UP)); + payment.setBaseAmount(payment.getAmount().multiply(rate).setScale(2, RoundingMode.HALF_UP)); + payment.setStatus("PAID"); + payment.setConfirmBy(operator); + payment.setConfirmTime(new Date()); + payPaymentMapper.updatePayPaymentStatus(payment); + return 1; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int reverse(Long paymentId) + { + PayPayment payment = required(paymentId); + if (!"PAID".equals(payment.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_NO_REVERSE.exception(); + } + String operator = SecurityUtils.getUsername(); + if (operator.equals(payment.getConfirmBy())) + { + throw PaymentMesEnum.BMS_PAY_SAME_PAY.exception(); + } + BigDecimal back = releaseAllocs(payment, operator); + if (back.compareTo(BigDecimal.ZERO) == 0) + { + throw PaymentMesEnum.BMS_PAY_NO_BACK.exception(); + } + return 1; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int refund(Long paymentId) + { + PayPayment payment = required(paymentId); + if (!"PAID".equals(payment.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_NO_REVERSE.exception(); + } + String operator = SecurityUtils.getUsername(); + if (operator.equals(payment.getConfirmBy())) + { + throw PaymentMesEnum.BMS_PAY_SAME_PAY.exception(); + } + BigDecimal back = releaseAllocs(payment, operator); + if (back.compareTo(BigDecimal.ZERO) == 0) + { + throw PaymentMesEnum.BMS_PAY_NO_BACK.exception(); + } + payment.setAllocAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)); + payment.setStatus("REFUNDED"); + payPaymentMapper.updatePayPaymentStatus(payment); + return 1; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public int voidPayment(Long paymentId) + { + PayPayment payment = required(paymentId); + if (!"DRAFT".equals(payment.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_PAY_DRAFT.exception(); + } + payment.setStatus("VOID"); + payPaymentMapper.updatePayPaymentStatus(payment); + return 1; + } + + private void checkAllocs(PayPayment payment) + { + BigDecimal sum = BigDecimal.ZERO; + Set seen = new HashSet<>(); + if (payment.getAllocs() == null) + { + return; + } + for (PayPaymentAlloc alloc : payment.getAllocs()) + { + if (alloc.getBillId() == null || alloc.getAllocAmount() == null || alloc.getAllocAmount().compareTo(BigDecimal.ZERO) <= 0) + { + continue; + } + if (!seen.add(alloc.getBillId())) + { + throw PaymentMesEnum.BMS_PAY_ALLOC_DUP.exception(); + } + PayBill bill = payBillMapper.selectPayBillById(alloc.getBillId()); + if (bill == null) + { + throw PaymentMesEnum.BMS_PAY_BILL_MISS.exception(); + } + if (!payment.getSupplierId().equals(bill.getSupplierId()) || !payment.getCurrency().equals(bill.getCurrency())) + { + throw PaymentMesEnum.BMS_PAY_PAY_SAME.exception(); + } + if (!"CONFIRMED".equals(bill.getStatus()) && !"PARTIAL".equals(bill.getStatus())) + { + throw PaymentMesEnum.BMS_PAY_PAY_BILL.exception(); + } + BigDecimal open = openAmount(bill); + if (alloc.getAllocAmount().compareTo(open) > 0) + { + throw PaymentMesEnum.BMS_PAY_ALLOC_AMT.exception(); + } + sum = sum.add(alloc.getAllocAmount()); + } + if (sum.compareTo(payment.getAmount()) > 0) + { + throw PaymentMesEnum.BMS_PAY_ALLOC_OVER.exception(); + } + } + + /** + * 冲回仍有效的核销,并减少付款单上的已核销金额。 + * + * @param payment 已付款单 + * @param operator 操作人 + * @return 冲回的金额 + */ + private BigDecimal releaseAllocs(PayPayment payment, String operator) + { + List allocs = payPaymentMapper.selectAllocs(payment.getPaymentId()); + BigDecimal back = BigDecimal.ZERO; + for (PayPaymentAlloc alloc : allocs) + { + if (alloc.getAllocAmount() == null || !"ACTIVE".equals(alloc.getAllocStatus())) + { + continue; + } + PayBill bill = payBillMapper.selectPayBillById(alloc.getBillId()); + if (bill != null) + { + undoAlloc(payment, bill, alloc.getAllocAmount(), operator); + } + payPaymentMapper.reverseAlloc(alloc.getAllocId(), operator); + back = back.add(alloc.getAllocAmount()); + } + back = back.setScale(2, RoundingMode.HALF_UP); + if (back.compareTo(BigDecimal.ZERO) > 0) + { + BigDecimal allocAmount = payment.getAllocAmount() == null ? BigDecimal.ZERO : payment.getAllocAmount(); + BigDecimal nextAlloc = allocAmount.subtract(back); + payment.setAllocAmount(nextAlloc.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP) : nextAlloc.setScale(2, RoundingMode.HALF_UP)); + payPaymentMapper.updatePayPaymentStatus(payment); + } + return back; + } + + private void undoAlloc(PayPayment payment, PayBill bill, BigDecimal allocAmount, String operator) + { + BigDecimal paid = bill.getPaidAmount() == null ? BigDecimal.ZERO : bill.getPaidAmount(); + BigDecimal next = paid.subtract(allocAmount); + if (next.compareTo(BigDecimal.ZERO) < 0) + { + next = BigDecimal.ZERO; + } + next = next.setScale(2, RoundingMode.HALF_UP); + BigDecimal total = bill.getTotalAmount() == null ? BigDecimal.ZERO : bill.getTotalAmount(); + String from = bill.getStatus(); + bill.setPaidAmount(next); + if (next.compareTo(BigDecimal.ZERO) == 0) + { + bill.setStatus("CONFIRMED"); + } + else if (next.compareTo(total) >= 0) + { + bill.setStatus("PAID"); + } + else + { + bill.setStatus("PARTIAL"); + } + bill.setUpdateBy(operator); + if (bill.getSupplierAmount() == null) + { + bill.setSupplierAmount(total); + } + if (bill.getDiffAmount() == null) + { + bill.setDiffAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)); + } + if (bill.getOrderCount() == null) + { + bill.setOrderCount(0); + } + payBillMapper.updatePayBillMoney(bill); + PayBillLog log = new PayBillLog(); + log.setBillId(bill.getBillId()); + log.setBizType("PAYMENT"); + log.setBizId(payment.getPaymentId()); + log.setOperationType("REVERSE"); + log.setFromStatus(from); + log.setToStatus(bill.getStatus()); + log.setOperationDesc("付款 " + payment.getPaymentNo() + " 反核销 " + allocAmount); + log.setAmountBefore(paid); + log.setAmountAfter(next); + log.setOperatorName(operator); + payBillMapper.insertLog(log); + } + + private void applyAlloc(PayPayment payment, PayBill bill, BigDecimal allocAmount, String operator) + { + BigDecimal paid = bill.getPaidAmount() == null ? BigDecimal.ZERO : bill.getPaidAmount(); + BigDecimal next = paid.add(allocAmount).setScale(2, RoundingMode.HALF_UP); + BigDecimal total = bill.getTotalAmount() == null ? BigDecimal.ZERO : bill.getTotalAmount(); + String from = bill.getStatus(); + bill.setPaidAmount(next); + bill.setStatus(next.compareTo(total) >= 0 ? "PAID" : "PARTIAL"); + bill.setUpdateBy(operator); + if (bill.getSupplierAmount() == null) + { + bill.setSupplierAmount(total); + } + if (bill.getDiffAmount() == null) + { + bill.setDiffAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)); + } + if (bill.getOrderCount() == null) + { + bill.setOrderCount(0); + } + payBillMapper.updatePayBillMoney(bill); + PayBillLog log = new PayBillLog(); + log.setBillId(bill.getBillId()); + log.setBizType("PAYMENT"); + log.setBizId(payment.getPaymentId()); + log.setOperationType("PAY"); + log.setFromStatus(from); + log.setToStatus(bill.getStatus()); + log.setOperationDesc("付款 " + payment.getPaymentNo() + " 核销 " + allocAmount); + log.setAmountBefore(paid); + log.setAmountAfter(next); + log.setOperatorName(operator); + payBillMapper.insertLog(log); + } + + private BigDecimal openAmount(PayBill bill) + { + BigDecimal total = bill.getTotalAmount() == null ? BigDecimal.ZERO : bill.getTotalAmount(); + BigDecimal paid = bill.getPaidAmount() == null ? BigDecimal.ZERO : bill.getPaidAmount(); + return total.subtract(paid); + } + + private PayPayment required(Long paymentId) + { + PayPayment payment = paymentId == null ? null : payPaymentMapper.selectPayPaymentById(paymentId); + if (payment == null) + { + throw PaymentMesEnum.BMS_PAY_PAY_MISS.exception(); + } + return payment; + } + + private String nextNo() + { + return "FK" + new SimpleDateFormat("yyyyMM").format(new Date()) + IdUtils.fastSimpleUUID().substring(0, 8).toUpperCase(); + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PaySurchargeRuleServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PaySurchargeRuleServiceImpl.java new file mode 100644 index 00000000..1c5dde23 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PaySurchargeRuleServiceImpl.java @@ -0,0 +1,563 @@ +package com.bms.price.service.impl; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; +import com.bms.common.exception.ServiceException; +import com.bms.common.utils.SecurityUtils; +import com.bms.price.domain.PayProduct; +import com.bms.price.domain.PayProductTerm; +import com.bms.price.domain.PaySurchargeRule; +import com.bms.price.domain.PaySurchargeRuleSave; +import com.bms.price.mapper.PayProductMapper; +import com.bms.price.mapper.PayProductTermMapper; +import com.bms.price.mapper.PaySurchargeRuleMapper; +import com.bms.price.service.IPaySurchargeRuleService; + +@Service +public class PaySurchargeRuleServiceImpl implements IPaySurchargeRuleService +{ + private static final Set RULES = Set.of("ALWAYS", "POSTCODE", "ZONE", "EXTRA_KG", "WEIGHT", "SIZE", "RETURN"); + + private static final Set NUMBERS = Set.of( + "longestMin", "longestMax", "secondMin", "secondMax", "sumMin", "sumMax", "girthMin", "girthMax", + "boxLength", "boxWidth", "boxHeight", "volumeMin", "volumeMax", "cubicMax", "weightMin", "weightMax", + "exceedStepCm", "exceedRatio", "thresholdKg", "stepKg", "minCharge", "ratio"); + + @Autowired + private PaySurchargeRuleMapper ruleMapper; + + @Autowired + private PayProductMapper productMapper; + + @Autowired + private PayProductTermMapper termMapper; + + @Override + public List list(Long productId, Long termId) + { + if (productId == null || termId == null) + { + return List.of(); + } + List rows = ruleMapper.selectByProduct(productId, termId); + rows.forEach(PaySurchargeRuleServiceImpl::hydrate); + return rows; + } + + @Override + public List previous(Long productId) + { + if (productId == null) + { + throw new ServiceException("请选择产品版本"); + } + PaySurchargeRule previous = ruleMapper.selectPreviousVersion(productId); + if (previous == null || previous.getProductId() == null || previous.getTermId() == null) + { + return List.of(); + } + List rows = ruleMapper.selectByProduct(previous.getProductId(), previous.getTermId()); + rows.forEach(PaySurchargeRuleServiceImpl::hydrate); + return rows; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void save(PaySurchargeRuleSave command) + { + if (command == null || command.getProducts() == null || command.getProducts().isEmpty()) + { + throw new ServiceException("请选择要绑定的产品"); + } + List rules = normalize(command.getRules()); + String operator = SecurityUtils.getUsername(); + Map versions = new LinkedHashMap<>(); + for (PaySurchargeRuleSave.PaySurchargeRuleBind bind : command.getProducts()) + { + if (bind == null || bind.getProductId() == null || bind.getTermId() == null) + { + throw new ServiceException("产品还没有可绑定的生效版本"); + } + versions.put(bind.getProductId() + ":" + bind.getTermId(), bind); + } + for (PaySurchargeRuleSave.PaySurchargeRuleBind bind : versions.values()) + { + PayProduct product = productMapper.selectPayProductById(bind.getProductId()); + if (product == null) + { + throw new ServiceException("应付产品不存在"); + } + PayProductTerm term = termMapper.selectById(bind.getTermId()); + if (term == null || term.getProductId() == null || !term.getProductId().equals(bind.getProductId())) + { + throw new ServiceException("计费条件必须跟这一版有效期走"); + } + ruleMapper.deleteByTerm(bind.getProductId(), bind.getTermId()); + if (rules.isEmpty()) + { + continue; + } + List rows = new ArrayList<>(); + int sort = 0; + String lastRule = ""; + for (PaySurchargeRule rule : rules) + { + if (!rule.getCalcRule().equals(lastRule)) + { + sort = 0; + lastRule = rule.getCalcRule(); + } + PaySurchargeRule row = copy(rule); + row.setProductId(bind.getProductId()); + row.setTermId(bind.getTermId()); + row.setVersionNo(term.getVersionNo() == null ? bind.getVersionNo() : term.getVersionNo()); + row.setSortNo(sort++); + row.setCreateBy(operator); + row.setUpdateBy(operator); + rows.add(row); + } + ruleMapper.insertBatch(rows); + } + } + + private static List normalize(List raw) + { + List rules = new ArrayList<>(); + if (raw == null) + { + return rules; + } + for (PaySurchargeRule rule : raw) + { + if (rule == null) + { + continue; + } + String calcRule = rule.getCalcRule() == null ? "" : rule.getCalcRule().trim().toUpperCase(Locale.ROOT); + if (!RULES.contains(calcRule)) + { + throw new ServiceException("计费条件不在可选范围内"); + } + PaySurchargeRule row = new PaySurchargeRule(); + row.setCalcRule(calcRule); + row.setEnabled("0".equals(rule.getEnabled()) ? "0" : "1"); + Map detail = cleanDetail(calcRule, rule.getDetail()); + row.setDetail(detail); + row.setRuleDetail(detail.isEmpty() ? null : JSON.toJSONString(detail)); + row.setCalcParam(trim(summary(calcRule, detail), 2000)); + row.setPostcodeParam(trim(text(detail.get("postcodes")), 2000)); + row.setZoneName(trim(text(detail.get("zoneName")), 200)); + row.setWeightMin(weightMin(calcRule, detail)); + row.setWeightMax(decimal(detail.get("weightMax"), "重量上限")); + rules.add(row); + } + return rules; + } + + private static void hydrate(PaySurchargeRule row) + { + Map detail = parse(row.getRuleDetail()); + if (detail.isEmpty()) + { + detail = legacy(row); + } + row.setDetail(detail.isEmpty() ? null : detail); + } + + private static Map parse(String json) + { + if (json == null || json.isBlank()) + { + return new LinkedHashMap<>(); + } + Map parsed = JSON.parseObject(json, new TypeReference>() { }); + return parsed == null ? new LinkedHashMap<>() : parsed; + } + + /** 还没写明细的旧行,用原来的摘要列带回弹框。 */ + private static Map legacy(PaySurchargeRule row) + { + Map detail = new LinkedHashMap<>(); + String calc = row.getCalcParam(); + String postcode = row.getPostcodeParam(); + if ("POSTCODE".equals(row.getCalcRule())) + { + put(detail, "postcodeMode", "PREFIX"); + put(detail, "postcodes", postcode == null || postcode.isBlank() ? calc : postcode); + } + else if ("ZONE".equals(row.getCalcRule())) + { + put(detail, "zoneName", row.getZoneName()); + } + else if ("EXTRA_KG".equals(row.getCalcRule())) + { + put(detail, "thresholdKg", row.getWeightMin() == null ? calc : row.getWeightMin()); + put(detail, "postcodes", postcode); + put(detail, "weightBasis", "ACTUAL"); + } + else if ("WEIGHT".equals(row.getCalcRule())) + { + put(detail, "weightMin", row.getWeightMin() == null ? calc : row.getWeightMin()); + put(detail, "weightBasis", "ACTUAL"); + } + else if ("SIZE".equals(row.getCalcRule())) + { + put(detail, "matchMode", "ANY"); + put(detail, "remark", calc); + } + else + { + put(detail, "remark", calc); + } + if (row.getWeightMin() != null) + { + detail.putIfAbsent("weightMin", row.getWeightMin()); + } + if (row.getWeightMax() != null) + { + detail.put("weightMax", row.getWeightMax()); + } + return detail; + } + + private static Map cleanDetail(String calcRule, Map raw) + { + Map detail = new LinkedHashMap<>(); + if (raw != null) + { + raw.forEach((key, value) -> put(detail, key, value)); + } + checkChoice(detail, "matchMode", Set.of("ANY", "ALL"), "命中方式"); + checkChoice(detail, "postcodeMode", Set.of("PREFIX", "EXACT"), "邮编匹配"); + checkChoice(detail, "weightBasis", Set.of("ACTUAL", "CHARGE"), "重量口径"); + checkChoice(detail, "rateMode", Set.of("FIXED", "RATIO"), "计费方式"); + checkChoice(detail, "chargeBase", Set.of("HANDLE", "DELIVERY", "FREIGHT_FEE"), "计费基数"); + checkChoice(detail, "chargeMode", Set.of("SAME", "AIR", "ROAD", "FIXED"), "退回方式"); + checkChoice(detail, "boxMode", Set.of("OVER", "UNDER"), "箱规比较"); + for (String key : NUMBERS) + { + if (!detail.containsKey(key)) + { + continue; + } + BigDecimal number = decimal(detail.get(key), numberLabel(key)); + if (number == null) + { + detail.remove(key); + } + else + { + detail.put(key, number); + } + } + if ("SIZE".equals(calcRule)) + { + require(detail.get("matchMode") != null, "超尺寸要选择任一命中或全部命中"); + require(hasSize(detail), "超尺寸至少要填最长边、次长边、三边之和、围长、箱规、体积或超出步长"); + require(boxComplete(detail), "箱规的长、宽、高要一起填"); + } + else if ("WEIGHT".equals(calcRule)) + { + require(detail.get("weightMin") != null || detail.get("weightMax") != null, "超重要填写重量下限或上限"); + } + else if ("EXTRA_KG".equals(calcRule)) + { + require(detail.get("thresholdKg") != null, "超重续收要填写超过多少公斤后开始续收"); + } + else if ("POSTCODE".equals(calcRule) || "ZONE".equals(calcRule)) + { + detail.clear(); + } + else if ("RETURN".equals(calcRule)) + { + require(detail.get("chargeMode") != null, "退件要选择退回方式"); + require(boxComplete(detail), "箱规的长、宽、高要一起填"); + } + return detail; + } + + private static boolean hasSize(Map detail) + { + return present(detail, "longestMin", "longestMax", "secondMin", "secondMax", "sumMin", "sumMax", + "girthMin", "girthMax", "boxLength", "volumeMin", "volumeMax", "cubicMax", "exceedStepCm"); + } + + private static boolean boxComplete(Map detail) + { + boolean any = present(detail, "boxLength", "boxWidth", "boxHeight"); + boolean all = detail.get("boxLength") != null && detail.get("boxWidth") != null && detail.get("boxHeight") != null; + return !any || all; + } + + private static boolean present(Map detail, String... keys) + { + for (String key : keys) + { + if (detail.get(key) != null) + { + return true; + } + } + return false; + } + + private static BigDecimal weightMin(String calcRule, Map detail) + { + if ("EXTRA_KG".equals(calcRule) && detail.get("thresholdKg") != null) + { + return (BigDecimal) detail.get("thresholdKg"); + } + return (BigDecimal) detail.get("weightMin"); + } + + private static String summary(String calcRule, Map detail) + { + if ("POSTCODE".equals(calcRule)) + { + return "按应付邮编配置判断,邮编前缀和完整邮编都认"; + } + if ("ZONE".equals(calcRule)) + { + return "按应付邮编配置的分区判断"; + } + List parts = new ArrayList<>(); + add(parts, choice(detail.get("matchMode"), Map.of("ANY", "任一命中", "ALL", "全部命中"))); + add(parts, choice(detail.get("postcodeMode"), Map.of("PREFIX", "邮编开头", "EXACT", "完整邮编"))); + add(parts, span("最长边", detail.get("longestMin"), detail.get("longestMax"), "cm")); + add(parts, span("次长边", detail.get("secondMin"), detail.get("secondMax"), "cm")); + add(parts, span("三边之和", detail.get("sumMin"), detail.get("sumMax"), "cm")); + add(parts, span("围长", detail.get("girthMin"), detail.get("girthMax"), "cm")); + if (detail.get("boxLength") != null) + { + String mode = "UNDER".equals(detail.get("boxMode")) ? "小于" : "超过"; + parts.add(mode + "箱规" + plain(detail.get("boxLength")) + "×" + plain(detail.get("boxWidth")) + "×" + plain(detail.get("boxHeight")) + "cm"); + } + add(parts, span("体积", detail.get("volumeMin"), detail.get("volumeMax"), "m³")); + if (detail.get("cubicMax") != null) + { + parts.add("长×宽×高≤" + plain(detail.get("cubicMax")) + "cm³"); + } + if (detail.get("exceedStepCm") != null) + { + parts.add("每超出" + plain(detail.get("exceedStepCm")) + "cm"); + } + if (detail.get("exceedRatio") != null) + { + parts.add("运费加收" + plain(detail.get("exceedRatio")) + "%"); + } + add(parts, span("重量", detail.get("weightMin"), detail.get("weightMax"), "kg")); + add(parts, choice(detail.get("weightBasis"), Map.of("ACTUAL", "实重", "CHARGE", "计费重"))); + if (detail.get("thresholdKg") != null) + { + parts.add("超过" + plain(detail.get("thresholdKg")) + "kg后续收"); + } + if (detail.get("stepKg") != null) + { + parts.add("每" + plain(detail.get("stepKg")) + "kg"); + } + if (detail.get("minCharge") != null) + { + parts.add("最低" + plain(detail.get("minCharge"))); + } + add(parts, text(detail.get("zoneName"))); + add(parts, text(detail.get("postcodes"))); + add(parts, text(detail.get("countries"))); + add(parts, choice(detail.get("chargeMode"), Map.of("SAME", "同正向运费", "AIR", "空运退回", "ROAD", "陆运退回", "FIXED", "固定金额"))); + add(parts, choice(detail.get("rateMode"), Map.of("FIXED", "固定金额", "RATIO", "按比例"))); + if (detail.get("ratio") != null) + { + parts.add(plain(detail.get("ratio")) + "%"); + } + String period = joinPeriod(text(detail.get("periodStart")), text(detail.get("periodEnd"))); + add(parts, period); + if ("ALWAYS".equals(calcRule) || "RETURN".equals(calcRule) || "SIZE".equals(calcRule)) + { + add(parts, text(detail.get("remark"))); + } + return parts.isEmpty() ? null : String.join(";", parts); + } + + private static String span(String name, Object min, Object max, String unit) + { + if (min == null && max == null) + { + return null; + } + if (min != null && max != null) + { + return name + " " + plain(min) + "<x≤" + plain(max) + unit; + } + if (min != null) + { + return name + ">" + plain(min) + unit; + } + return name + "≤" + plain(max) + unit; + } + + private static String joinPeriod(String start, String end) + { + if (start == null && end == null) + { + return null; + } + return (start == null ? "不限" : start) + "至" + (end == null ? "不限" : end); + } + + private static String choice(Object value, Map labels) + { + String text = text(value); + return text == null ? null : labels.get(text); + } + + private static void checkChoice(Map detail, String key, Set allowed, String label) + { + String value = text(detail.get(key)); + if (value == null) + { + detail.remove(key); + return; + } + if (!allowed.contains(value)) + { + throw new ServiceException(label + "不在可选范围内"); + } + detail.put(key, value); + } + + private static void put(Map detail, String key, Object value) + { + if (value == null) + { + return; + } + if (value instanceof String text) + { + if (text.isBlank()) + { + return; + } + if (text.length() > 2000) + { + throw new ServiceException("条件内容不能超过2000字"); + } + detail.put(key, text.trim()); + return; + } + detail.put(key, value); + } + + private static BigDecimal decimal(Object value, String label) + { + if (value == null || (value instanceof String text && text.isBlank())) + { + return null; + } + try + { + return new BigDecimal(String.valueOf(value).trim()); + } + catch (NumberFormatException ex) + { + throw new ServiceException(label + "要填写数字"); + } + } + + private static String numberLabel(String key) + { + return switch (key) + { + case "longestMin", "longestMax" -> "最长边"; + case "secondMin", "secondMax" -> "次长边"; + case "sumMin", "sumMax" -> "三边之和"; + case "girthMin", "girthMax" -> "围长"; + case "boxLength" -> "箱规长"; + case "boxWidth" -> "箱规宽"; + case "boxHeight" -> "箱规高"; + case "volumeMin", "volumeMax" -> "体积"; + case "cubicMax" -> "长宽高乘积"; + case "exceedStepCm" -> "超出步长"; + case "exceedRatio", "ratio" -> "比例"; + case "thresholdKg", "weightMin", "weightMax", "stepKg" -> "重量"; + case "minCharge" -> "最低消费"; + default -> "数值"; + }; + } + + private static String text(Object value) + { + if (value == null) + { + return null; + } + String text = String.valueOf(value).trim(); + return text.isEmpty() ? null : text; + } + + private static String plain(Object value) + { + if (value instanceof BigDecimal number) + { + return number.stripTrailingZeros().toPlainString(); + } + return text(value); + } + + private static void add(List parts, String value) + { + if (value != null && !value.isBlank()) + { + parts.add(value); + } + } + + private static void require(boolean ok, String message) + { + if (!ok) + { + throw new ServiceException(message); + } + } + + private static PaySurchargeRule copy(PaySurchargeRule rule) + { + PaySurchargeRule row = new PaySurchargeRule(); + row.setCalcRule(rule.getCalcRule()); + row.setEnabled(rule.getEnabled()); + row.setCalcParam(rule.getCalcParam()); + row.setRuleDetail(rule.getRuleDetail()); + row.setDetail(rule.getDetail()); + row.setPostcodeParam(rule.getPostcodeParam()); + row.setZoneName(rule.getZoneName()); + row.setWeightMin(rule.getWeightMin()); + row.setWeightMax(rule.getWeightMax()); + return row; + } + + private static String trim(String value, int max) + { + if (value == null) + { + return null; + } + String text = value.trim(); + if (text.isEmpty()) + { + return null; + } + if (text.length() > max) + { + throw new ServiceException("条件内容不能超过" + max + "字"); + } + return text; + } +} diff --git a/bms-back/bms-system/src/main/resources/mapper/price/PayBillMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PayBillMapper.xml index 727efe32..3c32e45e 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/PayBillMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/PayBillMapper.xml @@ -5,8 +5,13 @@ select bill_id as billId, bill_no as billNo, supplier_id as supplierId, supplier_name as supplierName, currency, source_type as sourceType, period_start as periodStart, period_end as periodEnd, - order_count as orderCount, total_amount as totalAmount, status, void_reason as voidReason, - void_by as voidBy, void_time as voidTime, create_by as createBy, create_time as createTime, remark + order_count as orderCount, total_amount as totalAmount, supplier_amount as supplierAmount, + diff_amount as diffAmount, paid_amount as paidAmount, due_date as dueDate, status, + bill_kind as billKind, original_bill_id as originalBillId, void_reason as voidReason, + void_by as voidBy, void_time as voidTime, confirm_by as confirmBy, confirm_time as confirmTime, + tax_rate as taxRate, amount_net as amountNet, tax_amount as taxAmount, exchange_rate as exchangeRate, + base_amount as baseAmount, rate_date as rateDate, term_days as termDays, submit_by as submitBy, + create_by as createBy, create_time as createTime, remark from pay_bill where bill_id = #{billId} @@ -48,9 +61,21 @@ (select group_concat(concat(ifnull(nullif(trim(f.fee_name), ''), ifnull(f.fee_type, '费用')), ' ', format(f.amount, 2)) order by field(f.fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE', 'REFUND'), f.fee_id separator ';') from prc_pay_bill_fee f where f.bill_id = o.bill_id and f.is_deleted = '0') as feeText, + (select count(*) from prc_pay_bill_fee f + where f.bill_id = o.bill_id and ifnull(f.is_deleted, '0') = '0') as feeCount, + (select count(*) from prc_pay_bill_fee f + where f.bill_id = o.bill_id and ifnull(f.is_deleted, '0') = '0' + and f.statement_line_id is null + and not exists ( + select 1 from pay_bill_line l + inner join pay_bill b on b.bill_id = l.bill_id + where l.order_bill_id = o.bill_id + and (l.fee_id is null or l.fee_id = '') + and b.status not in ('VOID', 'RED') + )) as unbilledCount, (select b.bill_no from pay_bill_line l inner join pay_bill b on b.bill_id = l.bill_id - where l.order_bill_id = o.bill_id and b.status <> 'VOID' + where l.order_bill_id = o.bill_id and b.status not in ('VOID', 'RED') limit 1) as occupiedBillNo from prc_order_bill o left join prc_order_bill_ext e on e.bill_id = o.bill_id @@ -81,8 +106,9 @@ + select f.fee_id as feeId, f.bill_id as billId, f.pay_bill_id as payBillId, f.fee_type as feeType, + f.fee_name as feeName, f.sc_id as scId, f.fee_level as feeLevel, f.amount, f.currency, + f.fee_source as feeSource, f.source_id as sourceId, f.settle_supplier_id as settleSupplierId, + f.settle_supplier_name as settleSupplierName + from prc_pay_bill_fee f + where ifnull(f.is_deleted, '0') = '0' + and f.statement_line_id is null + and f.bill_id in + #{id} + and not exists ( + select 1 from pay_bill_line l + inner join pay_bill b on b.bill_id = l.bill_id + where l.order_bill_id = f.bill_id + and (l.fee_id is null or l.fee_id = '') + and b.status not in ('VOID', 'RED') + ) + order by f.bill_id, f.fee_id + + + + + + + + + + + + + + + + insert into pay_bill_diff ( + bill_id, line_id, fee_id, our_amount, supplier_amount, diff_amount, reason, handle_type, adjust_fee_id, + create_by, create_time + ) values ( + #{billId}, #{lineId}, #{feeId}, #{ourAmount}, #{supplierAmount}, #{diffAmount}, #{reason}, #{handleType}, + #{adjustFeeId}, #{createBy}, sysdate() + ) + + + + update pay_bill_diff + set our_amount = #{ourAmount}, + supplier_amount = #{supplierAmount}, + diff_amount = #{diffAmount}, + reason = #{reason}, + handle_type = #{handleType}, + adjust_fee_id = #{adjustFeeId} + where diff_id = #{diffId} + + + + delete from pay_bill_diff where line_id = #{lineId} + + + + insert into pay_bill_file ( + bill_id, file_type, invoice_no, invoice_date, amount, tax_amount, amount_net, seller_name, matched_amount, + currency, file_name, file_url, deleted, create_by, create_time + ) values ( + #{billId}, #{fileType}, #{invoiceNo}, #{invoiceDate}, #{amount}, #{taxAmount}, #{amountNet}, #{sellerName}, + ifnull(#{matchedAmount}, 0), #{currency}, #{fileName}, #{fileUrl}, '0', #{createBy}, sysdate() + ) + + + + + + update pay_bill_file set deleted = '1' where file_id = #{fileId} + + + + insert into pay_bill_approval (bill_id, action, opinion, operator_name, create_time) + values (#{billId}, #{action}, #{opinion}, #{operatorName}, sysdate()) + + + + + + + + - insert into pay_bill_log (bill_id, operation_type, from_status, to_status, operation_desc, operator_name, create_time) - values (#{billId}, #{operationType}, #{fromStatus}, #{toStatus}, #{operationDesc}, #{operatorName}, sysdate()) + insert into pay_bill_log ( + bill_id, biz_type, biz_id, line_id, fee_id, operation_type, from_status, to_status, operation_desc, + amount_before, amount_after, operator_name, create_time + ) values ( + #{billId}, #{bizType}, #{bizId}, #{lineId}, #{feeId}, #{operationType}, #{fromStatus}, #{toStatus}, + #{operationDesc}, #{amountBefore}, #{amountAfter}, #{operatorName}, sysdate() + ) diff --git a/bms-back/bms-system/src/main/resources/mapper/price/PayFinanceMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PayFinanceMapper.xml new file mode 100644 index 00000000..cfb23841 --- /dev/null +++ b/bms-back/bms-system/src/main/resources/mapper/price/PayFinanceMapper.xml @@ -0,0 +1,71 @@ + + + + + + update pay_bill + set tax_rate = #{taxRate}, + exchange_rate = #{exchangeRate}, + rate_date = #{rateDate}, + update_time = sysdate() + where bill_id = #{billId} + + + + update pay_bill set submit_by = #{submitBy} where bill_id = #{billId} + + + + update pay_bill_line + set tax_rate = #{taxRate}, + amount_net = round(amount / (1 + #{taxRate}), 4), + tax_amount = round(amount - amount / (1 + #{taxRate}), 4), + base_amount = round(amount * #{exchangeRate}, 4) + where bill_id = #{billId} + + + + + + update pay_bill_file + set matched_amount = ifnull(amount, 0) + where bill_id = #{billId} + and deleted = '0' + and file_type = 'INVOICE' + + + + + + update prc_pay_bill_fee + set settle_supplier_id = #{supplierId}, + settle_supplier_name = #{supplierName} + where fee_id = #{feeId} + + + + update prc_pay_bill_fee + set settle_supplier_id = #{supplierId}, + settle_supplier_name = #{supplierName} + where bill_id = #{billId} + and ifnull(is_deleted, '0') = '0' + and statement_line_id is null + and (fee_source is null or fee_source = '' or fee_source = 'CALC') + + diff --git a/bms-back/bms-system/src/main/resources/mapper/price/PayPaymentMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PayPaymentMapper.xml new file mode 100644 index 00000000..4f61e693 --- /dev/null +++ b/bms-back/bms-system/src/main/resources/mapper/price/PayPaymentMapper.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + insert into pay_payment ( + payment_no, supplier_id, supplier_name, currency, pay_date, pay_method, bank_account, amount, bank_fee, + exchange_rate, status, remark, create_by, create_time + ) values ( + #{paymentNo}, #{supplierId}, #{supplierName}, #{currency}, #{payDate}, #{payMethod}, #{bankAccount}, + #{amount}, #{bankFee}, ifnull(#{exchangeRate}, 1), 'DRAFT', #{remark}, #{createBy}, sysdate() + ) + + + + insert into pay_payment_alloc (payment_id, bill_id, alloc_amount, alloc_status) + values (#{paymentId}, #{billId}, #{allocAmount}, 'ACTIVE') + + + + update pay_payment + set status = #{status}, + confirm_by = #{confirmBy}, + confirm_time = #{confirmTime}, + exchange_rate = ifnull(#{exchangeRate}, exchange_rate), + base_amount = ifnull(#{baseAmount}, base_amount), + alloc_amount = ifnull(#{allocAmount}, alloc_amount) + where payment_id = #{paymentId} + + + + update pay_payment_alloc + set alloc_status = 'REVERSED', + reverse_by = #{reverseBy}, + reverse_time = sysdate() + where alloc_id = #{allocId} + and alloc_status = 'ACTIVE' + + diff --git a/bms-back/bms-system/src/main/resources/mapper/price/PayPostcodeMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PayPostcodeMapper.xml index be2da3d6..14d8a716 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/PayPostcodeMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/PayPostcodeMapper.xml @@ -155,8 +155,8 @@ + select rule_id as ruleId, product_id as productId, term_id as termId, version_no as versionNo, + calc_rule as calcRule, enabled, calc_param as calcParam, rule_detail as ruleDetail, postcode_param as postcodeParam, + zone_name as zoneName, weight_min as weightMin, weight_max as weightMax, sort_no as sortNo + from pay_surcharge_rule + where product_id = #{productId} + and term_id = #{termId} + order by calc_rule, sort_no, rule_id + + + + delete from pay_surcharge_rule + where product_id = #{productId} + and term_id = #{termId} + + + + insert into pay_surcharge_rule ( + product_id, term_id, version_no, calc_rule, enabled, calc_param, rule_detail, postcode_param, + zone_name, weight_min, weight_max, sort_no, create_by, create_time, update_by, update_time + ) values + + (#{row.productId}, #{row.termId}, ifnull(#{row.versionNo}, 1), #{row.calcRule}, ifnull(#{row.enabled}, '1'), + #{row.calcParam}, #{row.ruleDetail}, #{row.postcodeParam}, #{row.zoneName}, #{row.weightMin}, #{row.weightMax}, ifnull(#{row.sortNo}, 0), + #{row.createBy}, sysdate(), #{row.updateBy}, sysdate()) + + + + + diff --git a/bms-back/sql/price/pay_bill_ap.sql b/bms-back/sql/price/pay_bill_ap.sql new file mode 100644 index 00000000..b3e96eb2 --- /dev/null +++ b/bms-back/sql/price/pay_bill_ap.sql @@ -0,0 +1,213 @@ +-- `pay_bill` 增加对方金额 +ALTER TABLE `bms`.`pay_bill` ADD COLUMN `supplier_amount` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '对方金额' AFTER `total_amount`; + +-- `pay_bill` 增加对账差额 +ALTER TABLE `bms`.`pay_bill` ADD COLUMN `diff_amount` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '差额,对方减我方' AFTER `supplier_amount`; + +-- `pay_bill` 增加已核销金额 +ALTER TABLE `bms`.`pay_bill` ADD COLUMN `paid_amount` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '已核销金额' AFTER `diff_amount`; + +-- `pay_bill` 增加到期日 +ALTER TABLE `bms`.`pay_bill` ADD COLUMN `due_date` date DEFAULT NULL COMMENT '到期日' AFTER `paid_amount`; + +-- `pay_bill` 增加蓝字红字 +ALTER TABLE `bms`.`pay_bill` ADD COLUMN `bill_kind` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'BLUE' COMMENT 'BLUE蓝字 RED红字' AFTER `status`; + +-- `pay_bill` 增加原账单 +ALTER TABLE `bms`.`pay_bill` ADD COLUMN `original_bill_id` bigint(20) DEFAULT NULL COMMENT '红字对应的原账单ID' AFTER `bill_kind`; + +-- `pay_bill` 增加审核人 +ALTER TABLE `bms`.`pay_bill` ADD COLUMN `confirm_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '审核人' AFTER `void_time`; + +-- `pay_bill` 增加审核时间 +ALTER TABLE `bms`.`pay_bill` ADD COLUMN `confirm_time` datetime DEFAULT NULL COMMENT '审核时间' AFTER `confirm_by`; + +-- `pay_bill_line` 增加费用明细ID +ALTER TABLE `bms`.`pay_bill_line` ADD COLUMN `fee_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '应付费用明细ID' AFTER `bill_id`; + +-- `pay_bill_line` 增加费用来源 +ALTER TABLE `bms`.`pay_bill_line` ADD COLUMN `fee_source` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'CALC计算 LADING提单分摊 MANUAL人工' AFTER `fee_name`; + +-- `pay_bill_line` 增加费用阶段 +ALTER TABLE `bms`.`pay_bill_line` ADD COLUMN `fee_level` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '费用阶段 1A 2B 3C 4D' AFTER `fee_source`; + +-- `pay_bill_line` 增加对方金额 +ALTER TABLE `bms`.`pay_bill_line` ADD COLUMN `supplier_amount` decimal(18,4) DEFAULT NULL COMMENT '对方金额' AFTER `amount`; + +-- `pay_bill_line` 增加差额 +ALTER TABLE `bms`.`pay_bill_line` ADD COLUMN `diff_amount` decimal(18,4) DEFAULT NULL COMMENT '差额,对方减我方' AFTER `supplier_amount`; + +-- `pay_bill_line` 增加差异状态 +ALTER TABLE `bms`.`pay_bill_line` ADD COLUMN `diff_status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'NONE' COMMENT 'NONE无差异 OPEN待处理 ACCEPT_OURS接受我方 ACCEPT_THEIRS接受对方' AFTER `diff_amount`; + +-- `prc_pay_bill_fee` 增加对账单行占用 +ALTER TABLE `bms`.`prc_pay_bill_fee` ADD COLUMN `statement_line_id` bigint(20) DEFAULT NULL COMMENT '已进入的应付账单行ID,空表示未出账' AFTER `is_deleted`; + +-- `pay_bill_log` 增加业务类型 +ALTER TABLE `bms`.`pay_bill_log` ADD COLUMN `biz_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'BILL账单 PAYMENT付款 FILE凭证' AFTER `bill_id`; + +-- `pay_bill_log` 增加业务单号 +ALTER TABLE `bms`.`pay_bill_log` ADD COLUMN `biz_id` bigint(20) DEFAULT NULL COMMENT '业务单号' AFTER `biz_type`; + +-- `pay_bill_log` 增加账单行 +ALTER TABLE `bms`.`pay_bill_log` ADD COLUMN `line_id` bigint(20) DEFAULT NULL COMMENT '账单行ID' AFTER `biz_id`; + +-- `pay_bill_log` 增加费用明细 +ALTER TABLE `bms`.`pay_bill_log` ADD COLUMN `fee_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '费用明细ID' AFTER `line_id`; + +-- `pay_bill_log` 增加改前金额 +ALTER TABLE `bms`.`pay_bill_log` ADD COLUMN `amount_before` decimal(18,4) DEFAULT NULL COMMENT '改前金额' AFTER `operation_desc`; + +-- `pay_bill_log` 增加改后金额 +ALTER TABLE `bms`.`pay_bill_log` ADD COLUMN `amount_after` decimal(18,4) DEFAULT NULL COMMENT '改后金额' AFTER `amount_before`; + +-- 新建应付账单差异表 +CREATE TABLE `bms`.`pay_bill_diff` ( + `diff_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '差异ID', + `bill_id` bigint(20) NOT NULL COMMENT '应付账单ID', + `line_id` bigint(20) NOT NULL COMMENT '账单行ID', + `fee_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '费用明细ID', + `our_amount` decimal(18,4) NOT NULL COMMENT '我方金额', + `supplier_amount` decimal(18,4) NOT NULL COMMENT '对方金额', + `diff_amount` decimal(18,4) NOT NULL COMMENT '差额,对方减我方', + `reason` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '差异原因', + `handle_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'OPEN未处理 ACCEPT_OURS接受我方 ACCEPT_THEIRS接受对方', + `adjust_fee_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '接受对方时新增的调整费用', + `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL COMMENT '创建时间', + PRIMARY KEY (`diff_id`), + KEY `idx_pay_bill_diff_line` (`line_id`, `handle_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='应付账单差异'; + +-- 新建应付凭证表 +CREATE TABLE `bms`.`pay_bill_file` ( + `file_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '凭证ID', + `bill_id` bigint(20) NOT NULL COMMENT '应付账单ID', + `file_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'INVOICE发票 STATEMENT对账单 DEBIT费用通知 OTHER其他', + `invoice_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发票号', + `invoice_date` date DEFAULT NULL COMMENT '开票日', + `amount` decimal(18,2) DEFAULT NULL COMMENT '价税合计', + `tax_amount` decimal(18,2) DEFAULT NULL COMMENT '税额', + `file_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '文件名', + `file_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文件地址', + `deleted` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '0正常 1删除', + `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '上传人', + `create_time` datetime NOT NULL COMMENT '上传时间', + PRIMARY KEY (`file_id`), + KEY `idx_pay_bill_file_bill` (`bill_id`, `deleted`), + KEY `idx_pay_bill_file_invoice` (`invoice_no`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='应付凭证'; + +-- 新建应付账单审核表 +CREATE TABLE `bms`.`pay_bill_approval` ( + `approval_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '审核ID', + `bill_id` bigint(20) NOT NULL COMMENT '应付账单ID', + `action` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'SUBMIT提交 APPROVE通过 REJECT驳回', + `opinion` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '意见', + `operator_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '操作人', + `create_time` datetime NOT NULL COMMENT '操作时间', + PRIMARY KEY (`approval_id`), + KEY `idx_pay_bill_approval_bill` (`bill_id`, `create_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='应付账单审核'; + +-- 新建应付付款表 +CREATE TABLE `bms`.`pay_payment` ( + `payment_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '付款单ID', + `payment_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '付款单号', + `supplier_id` bigint(20) NOT NULL COMMENT '供应商ID', + `supplier_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '供应商名称', + `currency` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '币种', + `pay_date` date NOT NULL COMMENT '付款日', + `pay_method` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'BANK银行转账 CASH现金 OTHER其他', + `bank_account` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '付款账户', + `amount` decimal(18,2) NOT NULL COMMENT '付款金额', + `bank_fee` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '手续费', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'DRAFT' COMMENT 'DRAFT草稿 PAID已付款 VOID已作废', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL COMMENT '创建时间', + `confirm_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '确认人', + `confirm_time` datetime DEFAULT NULL COMMENT '确认时间', + PRIMARY KEY (`payment_id`), + UNIQUE KEY `uk_pay_payment_no` (`payment_no`), + KEY `idx_pay_payment_supplier` (`supplier_id`, `status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='应付付款'; + +-- 新建应付付款核销表 +CREATE TABLE `bms`.`pay_payment_alloc` ( + `alloc_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '核销ID', + `payment_id` bigint(20) NOT NULL COMMENT '付款单ID', + `bill_id` bigint(20) NOT NULL COMMENT '应付账单ID', + `alloc_amount` decimal(18,2) NOT NULL COMMENT '本次核销金额', + PRIMARY KEY (`alloc_id`), + KEY `idx_pay_payment_alloc_pay` (`payment_id`), + KEY `idx_pay_payment_alloc_bill` (`bill_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='应付付款核销'; + +-- 账单管理下新增应付付款菜单 +INSERT INTO `bms`.`sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`) +SELECT '应付付款', parent.menu_id, 9, 'payPayment', 'price/billing/payPayment/index', 1, 0, 'C', '0', '0', 'price:payPayment:list', 'money', 'admin', sysdate(), '应付付款单,核销已确认的应付账单' +FROM `bms`.`sys_menu` parent +WHERE parent.menu_name = '账单管理' AND parent.menu_type = 'M' +ORDER BY parent.menu_id +LIMIT 1; + +-- 应付付款查询按钮 +INSERT INTO `bms`.`sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`) +SELECT '应付付款查询', m.menu_id, 1, '', '', 1, 0, 'F', '0', '0', 'price:payPayment:query', '#', 'admin', sysdate() +FROM `bms`.`sys_menu` m WHERE m.perms = 'price:payPayment:list' LIMIT 1; + +-- 应付付款登记按钮 +INSERT INTO `bms`.`sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`) +SELECT '应付付款登记', m.menu_id, 2, '', '', 1, 0, 'F', '0', '0', 'price:payPayment:add', '#', 'admin', sysdate() +FROM `bms`.`sys_menu` m WHERE m.perms = 'price:payPayment:list' LIMIT 1; + +-- 应付付款确认按钮 +INSERT INTO `bms`.`sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`) +SELECT '应付付款确认', m.menu_id, 3, '', '', 1, 0, 'F', '0', '0', 'price:payPayment:confirm', '#', 'admin', sysdate() +FROM `bms`.`sys_menu` m WHERE m.perms = 'price:payPayment:list' LIMIT 1; + +-- 应付付款作废按钮 +INSERT INTO `bms`.`sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`) +SELECT '应付付款作废', m.menu_id, 4, '', '', 1, 0, 'F', '0', '0', 'price:payPayment:void', '#', 'admin', sysdate() +FROM `bms`.`sys_menu` m WHERE m.perms = 'price:payPayment:list' LIMIT 1; + +-- 账单管理下新增应付凭证菜单 +INSERT INTO `bms`.`sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`) +SELECT '应付凭证', parent.menu_id, 10, 'payVoucher', 'price/billing/payVoucher/index', 1, 0, 'C', '0', '0', 'price:payVoucher:list', 'documentation', 'admin', sysdate(), '应付发票和对账凭证' +FROM `bms`.`sys_menu` parent +WHERE parent.menu_name = '账单管理' AND parent.menu_type = 'M' +ORDER BY parent.menu_id +LIMIT 1; + +-- 应付凭证查询按钮 +INSERT INTO `bms`.`sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`) +SELECT '应付凭证查询', m.menu_id, 1, '', '', 1, 0, 'F', '0', '0', 'price:payVoucher:query', '#', 'admin', sysdate() +FROM `bms`.`sys_menu` m WHERE m.perms = 'price:payVoucher:list' LIMIT 1; + +-- 应付凭证上传按钮 +INSERT INTO `bms`.`sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`) +SELECT '应付凭证上传', m.menu_id, 2, '', '', 1, 0, 'F', '0', '0', 'price:payVoucher:add', '#', 'admin', sysdate() +FROM `bms`.`sys_menu` m WHERE m.perms = 'price:payVoucher:list' LIMIT 1; + +-- 应付凭证删除按钮 +INSERT INTO `bms`.`sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`) +SELECT '应付凭证删除', m.menu_id, 3, '', '', 1, 0, 'F', '0', '0', 'price:payVoucher:remove', '#', 'admin', sysdate() +FROM `bms`.`sys_menu` m WHERE m.perms = 'price:payVoucher:list' LIMIT 1; + +-- 应付账单提交审核按钮 +INSERT INTO `bms`.`sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`) +SELECT '应付账单提交', m.menu_id, 5, '', '', 1, 0, 'F', '0', '0', 'price:payBill:submit', '#', 'admin', sysdate() +FROM `bms`.`sys_menu` m WHERE m.perms = 'price:payBill:list' LIMIT 1; + +-- 应付账单审核按钮 +INSERT INTO `bms`.`sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`) +SELECT '应付账单审核', m.menu_id, 6, '', '', 1, 0, 'F', '0', '0', 'price:payBill:approve', '#', 'admin', sysdate() +FROM `bms`.`sys_menu` m WHERE m.perms = 'price:payBill:list' LIMIT 1; + +-- 应付付款和应付凭证授给已有应付账单菜单的角色 +INSERT INTO `bms`.`sys_role_menu` (`role_id`, `menu_id`) +SELECT rm.role_id, dst.menu_id +FROM `bms`.`sys_role_menu` rm +INNER JOIN `bms`.`sys_menu` src ON src.menu_id = rm.menu_id AND src.perms = 'price:payBill:list' +INNER JOIN `bms`.`sys_menu` dst ON dst.perms IN ('price:payPayment:list', 'price:payPayment:query', 'price:payPayment:add', 'price:payPayment:confirm', 'price:payPayment:void', 'price:payVoucher:list', 'price:payVoucher:query', 'price:payVoucher:add', 'price:payVoucher:remove', 'price:payBill:submit', 'price:payBill:approve'); diff --git a/bms-back/sql/price/pay_bill_finance.sql b/bms-back/sql/price/pay_bill_finance.sql new file mode 100644 index 00000000..3d85c1bc --- /dev/null +++ b/bms-back/sql/price/pay_bill_finance.sql @@ -0,0 +1,74 @@ +-- `pay_bill` 增加税率 +ALTER TABLE `bms`.`pay_bill` ADD COLUMN `tax_rate` decimal(8,4) NOT NULL DEFAULT 0.0000 COMMENT '税率,0.06 表示 6%' AFTER `confirm_time`; + +-- `pay_bill` 增加不含税金额 +ALTER TABLE `bms`.`pay_bill` ADD COLUMN `amount_net` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '不含税金额' AFTER `tax_rate`; + +-- `pay_bill` 增加税额 +ALTER TABLE `bms`.`pay_bill` ADD COLUMN `tax_amount` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '税额' AFTER `amount_net`; + +-- `pay_bill` 增加折本位币汇率 +ALTER TABLE `bms`.`pay_bill` ADD COLUMN `exchange_rate` decimal(18,6) NOT NULL DEFAULT 1.000000 COMMENT '折人民币汇率' AFTER `tax_amount`; + +-- `pay_bill` 增加本位币金额 +ALTER TABLE `bms`.`pay_bill` ADD COLUMN `base_amount` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '本位币金额,人民币' AFTER `exchange_rate`; + +-- `pay_bill` 增加汇率日期 +ALTER TABLE `bms`.`pay_bill` ADD COLUMN `rate_date` date DEFAULT NULL COMMENT '汇率日期' AFTER `base_amount`; + +-- `pay_bill` 增加账期天数 +ALTER TABLE `bms`.`pay_bill` ADD COLUMN `term_days` int(11) DEFAULT NULL COMMENT '账期天数,来自供应商应付账期' AFTER `rate_date`; + +-- `pay_bill` 增加提交人 +ALTER TABLE `bms`.`pay_bill` ADD COLUMN `submit_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '提交审核人' AFTER `term_days`; + +-- `pay_bill_line` 增加税率 +ALTER TABLE `bms`.`pay_bill_line` ADD COLUMN `tax_rate` decimal(8,4) NOT NULL DEFAULT 0.0000 COMMENT '税率,0.06 表示 6%' AFTER `diff_status`; + +-- `pay_bill_line` 增加不含税金额 +ALTER TABLE `bms`.`pay_bill_line` ADD COLUMN `amount_net` decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT '不含税金额' AFTER `tax_rate`; + +-- `pay_bill_line` 增加税额 +ALTER TABLE `bms`.`pay_bill_line` ADD COLUMN `tax_amount` decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT '税额' AFTER `amount_net`; + +-- `pay_bill_line` 增加本位币金额 +ALTER TABLE `bms`.`pay_bill_line` ADD COLUMN `base_amount` decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT '本位币金额,人民币' AFTER `tax_amount`; + +-- `prc_pay_bill_fee` 增加结算供应商 +ALTER TABLE `bms`.`prc_pay_bill_fee` ADD COLUMN `settle_supplier_id` bigint(20) DEFAULT NULL COMMENT '结算供应商ID,空表示还没标明付款对象' AFTER `statement_line_id`; + +-- `prc_pay_bill_fee` 增加结算供应商名称 +ALTER TABLE `bms`.`prc_pay_bill_fee` ADD COLUMN `settle_supplier_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '结算供应商名称' AFTER `settle_supplier_id`; + +-- `prc_pay_bill_fee` 增加结算供应商索引 +ALTER TABLE `bms`.`prc_pay_bill_fee` ADD INDEX `idx_pay_fee_settle` (`settle_supplier_id`, `statement_line_id`); + +-- `pay_bill_file` 增加不含税金额 +ALTER TABLE `bms`.`pay_bill_file` ADD COLUMN `amount_net` decimal(18,2) DEFAULT NULL COMMENT '不含税金额' AFTER `tax_amount`; + +-- `pay_bill_file` 增加销方 +ALTER TABLE `bms`.`pay_bill_file` ADD COLUMN `seller_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '销方名称' AFTER `amount_net`; + +-- `pay_bill_file` 增加已勾稽金额 +ALTER TABLE `bms`.`pay_bill_file` ADD COLUMN `matched_amount` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '已勾稽到账单的金额' AFTER `seller_name`; + +-- `pay_bill_file` 增加币种 +ALTER TABLE `bms`.`pay_bill_file` ADD COLUMN `currency` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '币种' AFTER `matched_amount`; + +-- `pay_payment` 增加汇率 +ALTER TABLE `bms`.`pay_payment` ADD COLUMN `exchange_rate` decimal(18,6) NOT NULL DEFAULT 1.000000 COMMENT '折人民币汇率' AFTER `bank_fee`; + +-- `pay_payment` 增加本位币金额 +ALTER TABLE `bms`.`pay_payment` ADD COLUMN `base_amount` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '本位币金额,人民币' AFTER `exchange_rate`; + +-- `pay_payment` 增加已核销金额 +ALTER TABLE `bms`.`pay_payment` ADD COLUMN `alloc_amount` decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT '已核销到账单的金额' AFTER `base_amount`; + +-- `pay_payment_alloc` 增加核销状态 +ALTER TABLE `bms`.`pay_payment_alloc` ADD COLUMN `alloc_status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE有效 REVERSED已反核销' AFTER `alloc_amount`; + +-- `pay_payment_alloc` 增加反核销人 +ALTER TABLE `bms`.`pay_payment_alloc` ADD COLUMN `reverse_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '反核销人' AFTER `alloc_status`; + +-- `pay_payment_alloc` 增加反核销时间 +ALTER TABLE `bms`.`pay_payment_alloc` ADD COLUMN `reverse_time` datetime DEFAULT NULL COMMENT '反核销时间' AFTER `reverse_by`; diff --git a/bms-back/sql/price/pay_surcharge_rule.sql b/bms-back/sql/price/pay_surcharge_rule.sql new file mode 100644 index 00000000..a000ad3b --- /dev/null +++ b/bms-back/sql/price/pay_surcharge_rule.sql @@ -0,0 +1,26 @@ +-- 应付附加费计费条件。同一产品版本只有一份,各条件的明细都在这张表。可重复执行。 +SET NAMES utf8mb4; + +CREATE TABLE IF NOT EXISTS `pay_surcharge_rule` ( + `rule_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '规则ID', + `product_id` bigint(20) NOT NULL COMMENT '应付产品版本ID,对应这一版 pay_product.product_id', + `term_id` bigint(20) NOT NULL COMMENT '价卡版本周期,对应 pay_product_term.term_id', + `version_no` int(11) NOT NULL DEFAULT 1 COMMENT '版本号,跟随这一版价格', + `calc_rule` varchar(20) NOT NULL COMMENT '计费条件:ALWAYS每票都收 POSTCODE命中邮编 ZONE命中分区 EXTRA_KG超重续收 WEIGHT超重 SIZE超尺寸 RETURN退件', + `enabled` char(1) NOT NULL DEFAULT '1' COMMENT '开关:1开 0关', + `calc_param` varchar(2000) DEFAULT NULL COMMENT '条件参数摘要,便于查看', + `postcode_param` varchar(2000) DEFAULT NULL COMMENT '邮编参数。续重行的邮编放这里', + `zone_name` varchar(200) DEFAULT NULL COMMENT '分区', + `weight_min` decimal(10,3) DEFAULT NULL COMMENT '重量下限kg', + `weight_max` decimal(10,3) DEFAULT NULL COMMENT '重量上限kg', + `sort_no` int(11) NOT NULL DEFAULT 0 COMMENT '同一条件内的顺序', + `create_by` varchar(64) DEFAULT '' COMMENT '创建者', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_by` varchar(64) DEFAULT '' COMMENT '更新者', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`rule_id`), + KEY `idx_pay_surcharge_rule_ver` (`product_id`, `term_id`, `calc_rule`, `sort_no`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应付附加费计费条件,一个版本一份'; + +-- `pay_surcharge_rule` 表中添加`rule_detail` 属性 +ALTER TABLE `bms`.`pay_surcharge_rule` ADD COLUMN `rule_detail` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '条件明细。超尺寸含长宽高、围长、体积、超出步长、加收比例、任一或全部命中' AFTER `calc_param`; diff --git a/bms-front/src/api/price/pay.js b/bms-front/src/api/price/pay.js index fbdb60e5..60f3948e 100644 --- a/bms-front/src/api/price/pay.js +++ b/bms-front/src/api/price/pay.js @@ -171,3 +171,30 @@ export function delPay(productIds) { method: 'delete' }) } + +/** 当前版本的附加费计费条件 */ +export function listPaySurchargeRule(productId, termId) { + return request({ + url: '/price/pay/surchargeRule', + method: 'get', + params: { productId, termId } + }) +} + +/** 上一版计费条件,复制到当前页编辑 */ +export function previousPaySurchargeRule(productId) { + return request({ + url: '/price/pay/surchargeRule/previous', + method: 'get', + params: { productId } + }) +} + +/** 覆盖绑定产品当前版本的计费条件。同一版本只保留这一份。 */ +export function savePaySurchargeRule(data) { + return request({ + url: '/price/pay/surchargeRule', + method: 'post', + data + }) +} diff --git a/bms-front/src/api/price/payBill.js b/bms-front/src/api/price/payBill.js index 3c5f3d05..73ff4f7c 100644 --- a/bms-front/src/api/price/payBill.js +++ b/bms-front/src/api/price/payBill.js @@ -27,3 +27,58 @@ export function payPayBill(billId) { export function voidPayBill(data) { return request({ url: '/price/payBill/void', method: 'put', data }) } + +export function submitPayBill(billId) { + return request({ url: '/price/payBill/submit/' + billId, method: 'put' }) +} + +export function approvePayBill(billId) { + return request({ url: '/price/payBill/approve/' + billId, method: 'put' }) +} + +export function rejectPayBill(data) { + return request({ url: '/price/payBill/reject', method: 'put', data }) +} + +export function savePayBillDiff(data) { + return request({ url: '/price/payBill/diff', method: 'put', data }) +} + +export function acceptPayBillDiff(data) { + return request({ url: '/price/payBill/diff/accept', method: 'put', data }) +} + +export function removePayBillLine(data) { + return request({ url: '/price/payBill/line/remove', method: 'put', data }) +} + +export function savePayBillFile(data) { + return request({ url: '/price/payBill/file', method: 'post', data }) +} + +export function deletePayBillFile(fileId) { + return request({ url: '/price/payBill/file/' + fileId, method: 'delete' }) +} + +export function listPayBillFile(query) { + return request({ url: '/price/payBill/file/list', method: 'get', params: query }) +} + +export function redPayBill(data) { + return request({ url: '/price/payBill/red', method: 'post', data }) +} + +export function savePayFinance(data) { + return request({ url: '/price/payBill/finance', method: 'put', data }) +} + +export function exportPayStatement(billId) { + return request({ url: '/price/payBill/statement/' + billId, method: 'get', responseType: 'blob' }) +} + +export function importPaySupplier(billId, file) { + const data = new FormData() + data.append('billId', billId) + data.append('file', file) + return request({ url: '/price/payBill/diff/import', method: 'post', data }) +} diff --git a/bms-front/src/api/price/payPayment.js b/bms-front/src/api/price/payPayment.js new file mode 100644 index 00000000..fa6be3c5 --- /dev/null +++ b/bms-front/src/api/price/payPayment.js @@ -0,0 +1,33 @@ +import request from '@/utils/request' + +export function listPayPayment(query) { + return request({ url: '/price/payPayment/list', method: 'get', params: query }) +} + +export function getPayPayment(paymentId) { + return request({ url: '/price/payPayment/' + paymentId, method: 'get' }) +} + +export function openPayBills(supplierId, currency) { + return request({ url: '/price/payPayment/open', method: 'get', params: { supplierId, currency } }) +} + +export function createPayPayment(data) { + return request({ url: '/price/payPayment', method: 'post', data }) +} + +export function confirmPayPayment(paymentId) { + return request({ url: '/price/payPayment/confirm/' + paymentId, method: 'put' }) +} + +export function voidPayPayment(paymentId) { + return request({ url: '/price/payPayment/void/' + paymentId, method: 'put' }) +} + +export function reversePayPayment(paymentId) { + return request({ url: '/price/payPayment/reverse/' + paymentId, method: 'put' }) +} + +export function refundPayPayment(paymentId) { + return request({ url: '/price/payPayment/refund/' + paymentId, method: 'put' }) +} diff --git a/bms-front/src/components/ExcelImportDialog/index.vue b/bms-front/src/components/ExcelImportDialog/index.vue index 503fe00b..311fea5a 100644 --- a/bms-front/src/components/ExcelImportDialog/index.vue +++ b/bms-front/src/components/ExcelImportDialog/index.vue @@ -8,9 +8,22 @@ @closed="handleClosed" class="excel-import-dialog" > +
-
+
  • {{ line }}
  • @@ -180,6 +193,11 @@ const props = defineProps({ type: Array, default: () => [] }, + // 为 true 时,说明改到标题文字后的图标上,悬停展开 + descOnTitle: { + type: Boolean, + default: false + }, extraQuery: { type: Object, default: () => ({}) @@ -577,6 +595,32 @@ defineExpose({ open, close, setProgress }) vertical-align: baseline; } +.import-title-bar { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 16px; + font-weight: 600; + line-height: 24px; + color: var(--el-text-color-primary); +} + +.title-tip-icon { + font-size: 16px; + color: var(--el-color-warning); + cursor: help; +} + +.title-tip-list { + margin: 0; + padding-left: 18px; + max-width: 420px; + font-size: 13px; + font-weight: 400; + line-height: 1.7; + color: var(--el-text-color-regular); +} + .import-desc { margin-bottom: 14px; padding: 12px 14px; diff --git a/bms-front/src/views/price/basic/pay/components/PayConfigDetail.vue b/bms-front/src/views/price/basic/pay/components/PayConfigDetail.vue index ac71f769..ef7ea6a8 100644 --- a/bms-front/src/views/price/basic/pay/components/PayConfigDetail.vue +++ b/bms-front/src/views/price/basic/pay/components/PayConfigDetail.vue @@ -191,23 +191,11 @@ - - - @@ -471,50 +459,16 @@ const CellText = { } } -const calcRules = [ - { value: 'ALWAYS', label: '每票都收' }, - { value: 'POSTCODE', label: '命中邮编' }, - { value: 'ZONE', label: '命中分区' }, - { value: 'EXTRA_KG', label: '超重续收' }, - { value: 'WEIGHT', label: '超重' }, - { value: 'SIZE', label: '超尺寸' }, - { value: 'RETURN', label: '退件' }, - { value: 'MANUAL', label: '暂不计费' } -] - -function calcRuleLabel(value) { - return calcRules.find(item => item.value === value)?.label || '暂不计费' +function feeBillFlag(row) { + return row?.calcRule && row.calcRule !== 'MANUAL' ? 'BILL' : 'MANUAL' } -function conditionHint(row) { - const parts = [] - const hasMin = row.weightMin != null && row.weightMin !== '' - const hasMax = row.weightMax != null && row.weightMax !== '' - if (hasMin || hasMax) { - parts.push(`${hasMin ? row.weightMin : ''}-${hasMax ? row.weightMax : ''}kg`) +function setFeeBill(row, value) { + if (value === 'MANUAL') { + row.calcRule = 'MANUAL' + return } - if (row.postcodeParam) { - parts.push(`邮编 ${row.postcodeParam}`) - } - return parts.join(';') -} - -function hasCalcParam(row) { - const text = String(row?.calcParam ?? '').trim() - if (!text) return false - return text !== String(row?.postcodeParam ?? '').trim() -} - -function showCalcParamEditor(row) { - return hasCalcParam(row) || !conditionHint(row) -} - -function feeCondition(row) { - const parts = [] - const hint = conditionHint(row) - if (hint) parts.push(hint) - if (hasCalcParam(row)) parts.push(String(row.calcParam).trim()) - return parts.join(';') || '-' + if (!row.calcRule || row.calcRule === 'MANUAL') row.calcRule = 'ALWAYS' } const pageSize = 20 diff --git a/bms-front/src/views/price/basic/pay/components/PaySurchargeRuleDialog.vue b/bms-front/src/views/price/basic/pay/components/PaySurchargeRuleDialog.vue new file mode 100644 index 00000000..4dc22e95 --- /dev/null +++ b/bms-front/src/views/price/basic/pay/components/PaySurchargeRuleDialog.vue @@ -0,0 +1,530 @@ + + + + + diff --git a/bms-front/src/views/price/basic/pay/index.vue b/bms-front/src/views/price/basic/pay/index.vue index 6dc2d4b0..55848e9b 100644 --- a/bms-front/src/views/price/basic/pay/index.vue +++ b/bms-front/src/views/price/basic/pay/index.vue @@ -22,6 +22,9 @@ 导出明细 + + 附加费配置 +