新增收付款模块

This commit is contained in:
2026-08-22 17:25:26 +08:00
parent 7683ad804a
commit 3e265fc71e
96 changed files with 8527 additions and 0 deletions
@@ -0,0 +1,82 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.transport.pojo.dto.BillLedgerSaveRequest;
import org.springblade.transport.pojo.vo.BillLedgerVO;
import org.springblade.transport.service.IBillLedgerService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/** 汇票台账控制器。 @author Chill */
@RestController
@AllArgsConstructor
@PreAuth(menu = "bill_ledger")
@RequestMapping("/bill-ledger")
@Tag(name = "汇票台账", description = "汇票票据信息及可用余额管理")
public class BillLedgerController extends BladeController {
private final IBillLedgerService billLedgerService;
@GetMapping("/list")
@ApiOperationSupport(order = 1)
@Operation(summary = "汇票台账分页")
public R<IPage<BillLedgerVO>> list(BillLedgerVO query, Query pageQuery) {
return R.data(billLedgerService.selectPage(Condition.getPage(pageQuery), query));
}
@GetMapping("/detail")
@ApiOperationSupport(order = 2)
@Operation(summary = "汇票台账详情")
public R<BillLedgerVO> detail(@RequestParam Long id) {
return R.data(billLedgerService.detail(id));
}
@GetMapping("/expiry-counts")
@ApiOperationSupport(order = 3)
@Operation(summary = "汇票到期快捷统计")
public R<Map<String, Long>> expiryCounts() {
return R.data(billLedgerService.expiryCounts());
}
@GetMapping("/available-options")
@ApiOperationSupport(order = 4)
@Operation(summary = "付款申请可用汇票")
public R<List<BillLedgerVO>> availableOptions(@RequestParam(required = false) String keyword,
@RequestParam(required = false) Long deptId, @RequestParam(required = false) Long selectedId) {
return R.data(billLedgerService.availableOptions(keyword, deptId, selectedId));
}
@PostMapping("/submit")
@ApiOperationSupport(order = 5)
@Operation(summary = "新增或编辑汇票台账")
public R<Long> submit(@RequestBody BillLedgerSaveRequest request) {
return R.data(billLedgerService.submit(request));
}
@PostMapping("/remove")
@ApiOperationSupport(order = 6)
@Operation(summary = "删除汇票台账")
public R remove(@RequestParam Long id) {
billLedgerService.removeLedger(id);
return R.success("删除成功");
}
}
@@ -0,0 +1,118 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.transport.pojo.dto.BillPaymentSaveRequest;
import org.springblade.transport.pojo.dto.BillPaymentStatusRequest;
import org.springblade.transport.pojo.vo.BillPaymentVO;
import org.springblade.transport.service.IBillPaymentService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/** 汇票付款控制器。 @author Chill */
@RestController
@AllArgsConstructor
@PreAuth(menu = "bill_payment")
@RequestMapping("/bill-payment")
@Tag(name = "汇票付款", description = "汇票付款单据管理")
public class BillPaymentController extends BladeController {
private final IBillPaymentService billPaymentService;
@GetMapping("/list")
@ApiOperationSupport(order = 1)
@Operation(summary = "汇票付款分页")
public R<IPage<BillPaymentVO>> list(BillPaymentVO query, Query pageQuery) {
return R.data(billPaymentService.selectPage(Condition.getPage(pageQuery), query));
}
@GetMapping("/detail")
@ApiOperationSupport(order = 2)
@Operation(summary = "汇票付款详情")
public R<BillPaymentVO> detail(@RequestParam Long id) {
return R.data(billPaymentService.detail(id));
}
@PostMapping("/save")
@ApiOperationSupport(order = 3)
@Operation(summary = "保存汇票付款")
public R<Long> save(@RequestBody BillPaymentSaveRequest request) {
return R.data(billPaymentService.saveDraft(request));
}
@PostMapping("/remove")
@ApiOperationSupport(order = 4)
@Operation(summary = "删除汇票付款草稿")
public R remove(@RequestParam Long id) {
billPaymentService.removeDraft(id);
return R.success("删除成功");
}
@PostMapping("/submit")
@ApiOperationSupport(order = 5)
@Operation(summary = "提交汇票付款")
public R submit(@RequestBody BillPaymentStatusRequest request) {
billPaymentService.submit(request);
return R.success("提交成功");
}
@PostMapping("/approve")
@ApiOperationSupport(order = 6)
@Operation(summary = "审批通过汇票付款")
public R approve(@RequestBody BillPaymentStatusRequest request) {
billPaymentService.approve(request);
return R.success("审批通过");
}
@PostMapping("/return")
@ApiOperationSupport(order = 7)
@Operation(summary = "驳回汇票付款")
public R returnBill(@RequestBody BillPaymentStatusRequest request) {
billPaymentService.returnBill(request);
return R.success("已驳回");
}
@PostMapping("/void")
@ApiOperationSupport(order = 8)
@Operation(summary = "作废汇票付款")
public R voidBill(@RequestBody BillPaymentStatusRequest request) {
billPaymentService.voidBill(request);
return R.success("作废成功");
}
}
@@ -0,0 +1,154 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.transport.pojo.dto.InvoiceApplicationSaveRequest;
import org.springblade.transport.pojo.dto.InvoiceApplicationStatusRequest;
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
import org.springblade.transport.pojo.vo.InvoiceApplicationVO;
import org.springblade.transport.service.IInvoiceApplicationService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* 开票申请控制器
*
* @author Chill
*/
@RestController
@AllArgsConstructor
@PreAuth(menu = "invoice_application")
@RequestMapping("/invoice-application")
@Tag(name = "开票管理", description = "开票申请管理")
public class InvoiceApplicationController extends BladeController {
private final IInvoiceApplicationService invoiceApplicationService;
@GetMapping("/list")
@ApiOperationSupport(order = 1)
@Operation(summary = "开票申请分页")
public R<IPage<InvoiceApplicationVO>> list(InvoiceApplicationVO query, Query pageQuery) {
return R.data(invoiceApplicationService.selectPage(Condition.getPage(pageQuery), query));
}
@GetMapping("/detail")
@ApiOperationSupport(order = 2)
@Operation(summary = "开票申请详情")
public R<InvoiceApplicationVO> detail(@RequestParam Long id) {
return R.data(invoiceApplicationService.detail(id));
}
@GetMapping("/settlement-candidates")
@ApiOperationSupport(order = 3)
@Operation(summary = "可开票正式结算单")
public R<List<Map<String, Object>>> settlementCandidates(@RequestParam(required = false) String keyword) {
return R.data(invoiceApplicationService.settlementCandidates(keyword));
}
@GetMapping("/settlement-details")
@ApiOperationSupport(order = 4)
@Operation(summary = "结算单可选明细")
public R<List<FormalSettlementDetail>> settlementDetails(@RequestParam String settlementIds) {
return R.data(invoiceApplicationService.settlementDetails(settlementIds));
}
@GetMapping("/receiver-information")
@ApiOperationSupport(order = 5)
@Operation(summary = "受票方开票信息")
public R<Map<String, Object>> receiverInformation(@RequestParam String settlementIds) {
return R.data(invoiceApplicationService.receiverInformation(settlementIds));
}
@PostMapping("/save")
@ApiOperationSupport(order = 6)
@Operation(summary = "保存开票申请")
public R<Long> save(@RequestBody InvoiceApplicationSaveRequest request) {
return R.data(invoiceApplicationService.saveDraft(request));
}
@PostMapping("/remove")
@ApiOperationSupport(order = 7)
@Operation(summary = "删除开票申请草稿")
public R remove(@RequestParam Long id) {
invoiceApplicationService.removeDraft(id);
return R.success("删除成功");
}
@PostMapping("/submit")
@ApiOperationSupport(order = 8)
@Operation(summary = "提交开票申请")
public R submit(@RequestBody InvoiceApplicationStatusRequest request) {
invoiceApplicationService.submit(request);
return R.success("提交成功");
}
@PostMapping("/approve")
@ApiOperationSupport(order = 9)
@Operation(summary = "审批通过开票申请")
public R approve(@RequestBody InvoiceApplicationStatusRequest request) {
invoiceApplicationService.approve(request);
return R.success("审批通过");
}
@PostMapping("/return")
@ApiOperationSupport(order = 10)
@Operation(summary = "驳回开票申请")
public R returnBill(@RequestBody InvoiceApplicationStatusRequest request) {
invoiceApplicationService.returnBill(request);
return R.success("已驳回");
}
@PostMapping("/void")
@ApiOperationSupport(order = 11)
@Operation(summary = "作废开票申请")
public R voidBill(@RequestBody InvoiceApplicationStatusRequest request) {
invoiceApplicationService.voidBill(request);
return R.success("作废成功");
}
@PostMapping("/sync-kingdee")
@ApiOperationSupport(order = 12)
@Operation(summary = "同步金蝶开票申请")
public R<String> syncKingdee(@RequestParam Long id) {
return R.data(invoiceApplicationService.syncKingdee(id));
}
}
@@ -0,0 +1,157 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest;
import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
import org.springblade.transport.pojo.vo.InvoiceReceiptVO;
import org.springblade.transport.service.IInvoiceReceiptService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* 收票登记控制器
*
* @author Chill
*/
@RestController
@AllArgsConstructor
@PreAuth(menu = "invoice_receipt")
@RequestMapping("/invoice-receipt")
@Tag(name = "收票管理", description = "进项发票登记认领管理")
public class InvoiceReceiptController extends BladeController {
private final IInvoiceReceiptService invoiceReceiptService;
@GetMapping("/list")
@ApiOperationSupport(order = 1)
@Operation(summary = "收票登记分页")
public R<IPage<InvoiceReceiptVO>> list(InvoiceReceiptVO query, Query pageQuery) {
return R.data(invoiceReceiptService.selectPage(Condition.getPage(pageQuery), query));
}
@GetMapping("/detail")
@ApiOperationSupport(order = 2)
@Operation(summary = "收票登记详情")
public R<InvoiceReceiptVO> detail(@RequestParam Long id) {
return R.data(invoiceReceiptService.detail(id));
}
@GetMapping("/invoice-pool")
@ApiOperationSupport(order = 3)
@Operation(summary = "查询金蝶进项发票票据池")
public R<List<KingdeeInvoicePool>> invoicePool(@RequestParam(required = false) String keyword) {
return R.data(invoiceReceiptService.invoicePool(keyword));
}
@GetMapping("/settlement-candidates")
@ApiOperationSupport(order = 4)
@Operation(summary = "可关联的应付正式结算单")
public R<List<Map<String, Object>>> settlementCandidates(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) Long receiptId) {
return R.data(invoiceReceiptService.settlementCandidates(keyword, receiptId));
}
@GetMapping("/reference-information")
@ApiOperationSupport(order = 5)
@Operation(summary = "收票关联参考信息")
public R<Map<String, Object>> referenceInformation(@RequestParam String settlementIds) {
return R.data(invoiceReceiptService.referenceInformation(settlementIds));
}
@PostMapping("/save")
@ApiOperationSupport(order = 6)
@Operation(summary = "保存收票登记")
public R<Long> save(@RequestBody InvoiceReceiptSaveRequest request) {
return R.data(invoiceReceiptService.saveDraft(request));
}
@PostMapping("/remove")
@ApiOperationSupport(order = 7)
@Operation(summary = "删除收票登记草稿")
public R remove(@RequestParam Long id) {
invoiceReceiptService.removeDraft(id);
return R.success("删除成功");
}
@PostMapping("/submit")
@ApiOperationSupport(order = 8)
@Operation(summary = "提交收票登记")
public R submit(@RequestBody InvoiceReceiptStatusRequest request) {
invoiceReceiptService.submit(request);
return R.success("提交成功");
}
@PostMapping("/approve")
@ApiOperationSupport(order = 9)
@Operation(summary = "审批通过收票登记")
public R approve(@RequestBody InvoiceReceiptStatusRequest request) {
invoiceReceiptService.approve(request);
return R.success("审批通过");
}
@PostMapping("/return")
@ApiOperationSupport(order = 10)
@Operation(summary = "驳回收票登记")
public R returnBill(@RequestBody InvoiceReceiptStatusRequest request) {
invoiceReceiptService.returnBill(request);
return R.success("已驳回");
}
@PostMapping("/void")
@ApiOperationSupport(order = 11)
@Operation(summary = "作废收票登记")
public R voidBill(@RequestBody InvoiceReceiptStatusRequest request) {
invoiceReceiptService.voidBill(request);
return R.success("作废成功");
}
@PostMapping("/sync-kingdee")
@ApiOperationSupport(order = 12)
@Operation(summary = "同步金蝶发票状态")
public R<String> syncKingdee(@RequestParam Long id) {
return R.data(invoiceReceiptService.syncKingdee(id));
}
}
@@ -0,0 +1,87 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
* Copyright of this software remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest;
import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest;
import org.springblade.transport.pojo.vo.PaymentApplicationVO;
import org.springblade.transport.service.IPaymentApplicationService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/** 付款申请控制器。 @author Chill */
@RestController
@AllArgsConstructor
@PreAuth(menu = "payment_application")
@RequestMapping("/payment-application")
@Tag(name = "付款管理", description = "付款申请管理")
public class PaymentApplicationController extends BladeController {
private final IPaymentApplicationService paymentApplicationService;
@GetMapping("/list")
@ApiOperationSupport(order = 1)
@Operation(summary = "付款申请分页")
public R<IPage<PaymentApplicationVO>> list(PaymentApplicationVO query, Query pageQuery) { return R.data(paymentApplicationService.selectPage(Condition.getPage(pageQuery), query)); }
@GetMapping("/detail")
@ApiOperationSupport(order = 2)
@Operation(summary = "付款申请详情")
public R<PaymentApplicationVO> detail(@RequestParam Long id) { return R.data(paymentApplicationService.detail(id)); }
@PostMapping("/save")
@ApiOperationSupport(order = 3)
@Operation(summary = "保存付款申请")
public R<Long> save(@RequestBody PaymentApplicationSaveRequest request) { return R.data(paymentApplicationService.saveDraft(request)); }
@PostMapping("/remove")
@ApiOperationSupport(order = 4)
@Operation(summary = "删除付款申请草稿")
public R remove(@RequestParam Long id) { paymentApplicationService.removeDraft(id); return R.success("删除成功"); }
@PostMapping("/submit")
@ApiOperationSupport(order = 5)
@Operation(summary = "提交付款申请")
public R submit(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.submit(request); return R.success("提交成功"); }
@PostMapping("/approve")
@ApiOperationSupport(order = 6)
@Operation(summary = "审批通过付款申请")
public R approve(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.approve(request); return R.success("审批通过"); }
@PostMapping("/return")
@ApiOperationSupport(order = 7)
@Operation(summary = "驳回付款申请")
public R returnBill(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.returnBill(request); return R.success("已驳回"); }
@PostMapping("/void")
@ApiOperationSupport(order = 8)
@Operation(summary = "作废付款申请")
public R voidBill(@RequestBody PaymentApplicationStatusRequest request) { paymentApplicationService.voidBill(request); return R.success("作废成功"); }
@PostMapping("/sync-kingdee")
@ApiOperationSupport(order = 9)
@Operation(summary = "生成金蝶付款单")
public R<String> syncKingdee(@RequestParam Long id) { return R.data(paymentApplicationService.syncKingdee(id)); }
}
@@ -0,0 +1,90 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.transport.pojo.dto.ReceiptClaimAttachmentsRequest;
import org.springblade.transport.pojo.vo.ReceiptClaimRecordVO;
import org.springblade.transport.service.IReceiptClaimRecordService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* 认领记录控制器
*
* @author Chill
*/
@RestController
@AllArgsConstructor
@PreAuth(menu = "receipt_claim_record")
@RequestMapping("/receipt-claim-record")
@Tag(name = "认领记录", description = "当前用户收款认领记录查询与作废")
public class ReceiptClaimRecordController extends BladeController {
private final IReceiptClaimRecordService receiptClaimRecordService;
@GetMapping("/list")
@ApiOperationSupport(order = 1)
@Operation(summary = "当前用户认领记录分页")
public R<IPage<ReceiptClaimRecordVO>> list(ReceiptClaimRecordVO query, Query pageQuery) {
return R.data(receiptClaimRecordService.selectPage(Condition.getPage(pageQuery), query));
}
@GetMapping("/detail")
@ApiOperationSupport(order = 2)
@Operation(summary = "认领记录详情")
public R<ReceiptClaimRecordVO> detail(@RequestParam Long id) {
return R.data(receiptClaimRecordService.detail(id));
}
@PostMapping("/attachments")
@ApiOperationSupport(order = 3)
@Operation(summary = "维护本人认领记录附件")
public R<Void> updateAttachments(@RequestBody ReceiptClaimAttachmentsRequest request) {
receiptClaimRecordService.updateAttachments(request);
return R.success();
}
@PostMapping("/void")
@ApiOperationSupport(order = 4)
@Operation(summary = "作废认领记录并生成金蝶认领冲单")
public R<String> voidClaim(@RequestParam Long id) {
return R.data(receiptClaimRecordService.voidClaim(id));
}
}
@@ -0,0 +1,102 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.transport.pojo.dto.ReceiptClaimRequest;
import org.springblade.transport.pojo.dto.ReceiptFlowSyncRequest;
import org.springblade.transport.pojo.vo.ReceiptFlowVO;
import org.springblade.transport.service.IReceiptFlowService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* 收款流水控制器
*
* @author Chill
*/
@RestController
@AllArgsConstructor
@PreAuth(menu = "receipt_flow")
@RequestMapping("/receipt-flow")
@Tag(name = "收款流水", description = "金蝶收款流水同步与认领管理")
public class ReceiptFlowController extends BladeController {
private final IReceiptFlowService receiptFlowService;
@GetMapping("/list")
@ApiOperationSupport(order = 1)
@Operation(summary = "收款流水分页")
public R<IPage<ReceiptFlowVO>> list(ReceiptFlowVO query, Query pageQuery) {
return R.data(receiptFlowService.selectPage(Condition.getPage(pageQuery), query));
}
@GetMapping("/detail")
@ApiOperationSupport(order = 2)
@Operation(summary = "收款流水详情")
public R<ReceiptFlowVO> detail(@RequestParam Long id) {
return R.data(receiptFlowService.detail(id));
}
@GetMapping("/settlement-candidates")
@ApiOperationSupport(order = 3)
@Operation(summary = "可关联的应收正式结算单")
public R<List<Map<String, Object>>> settlementCandidates(
@RequestParam(required = false) String keyword,
@RequestParam Long flowId) {
return R.data(receiptFlowService.settlementCandidates(keyword, flowId));
}
@PostMapping("/claim")
@ApiOperationSupport(order = 4)
@Operation(summary = "认领收款流水")
public R<Long> claim(@RequestBody ReceiptClaimRequest request) {
return R.data(receiptFlowService.claim(request));
}
@PostMapping("/sync")
@ApiOperationSupport(order = 5)
@Operation(summary = "手动同步金蝶收款流水")
public R<Integer> sync(@RequestBody(required = false) ReceiptFlowSyncRequest request) {
return R.data(receiptFlowService.sync(request));
}
}
@@ -0,0 +1,14 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.BillLedger;
/** 汇票台账 Mapper。 @author Chill */
@Mapper
public interface BillLedgerMapper extends BaseMapper<BillLedger> {
}
@@ -0,0 +1,14 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.BillLedgerUsage;
/** 汇票使用记录 Mapper。 @author Chill */
@Mapper
public interface BillLedgerUsageMapper extends BaseMapper<BillLedgerUsage> {
}
@@ -0,0 +1,35 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.BillPayment;
/** 汇票付款 Mapper。 @author Chill */
@Mapper
public interface BillPaymentMapper extends BaseMapper<BillPayment> {
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.InvoiceApplicationDetail;
/**
* 开票申请结算明细 Mapper 接口
*
* @author Chill
*/
@Mapper
public interface InvoiceApplicationDetailMapper extends BaseMapper<InvoiceApplicationDetail> {
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.InvoiceApplicationLine;
/**
* 开票申请商品行 Mapper 接口
*
* @author Chill
*/
@Mapper
public interface InvoiceApplicationLineMapper extends BaseMapper<InvoiceApplicationLine> {
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.InvoiceApplication;
/**
* 开票申请 Mapper 接口
*
* @author Chill
*/
@Mapper
public interface InvoiceApplicationMapper extends BaseMapper<InvoiceApplication> {
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.InvoiceApplicationRecord;
/**
* 开票申请操作记录 Mapper 接口
*
* @author Chill
*/
@Mapper
public interface InvoiceApplicationRecordMapper extends BaseMapper<InvoiceApplicationRecord> {
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement;
/**
* 开票申请结算单 Mapper 接口
*
* @author Chill
*/
@Mapper
public interface InvoiceApplicationSettlementMapper extends BaseMapper<InvoiceApplicationSettlement> {
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.InvoiceApplicationSheet;
/**
* 开票申请发票张次 Mapper 接口
*
* @author Chill
*/
@Mapper
public interface InvoiceApplicationSheetMapper extends BaseMapper<InvoiceApplicationSheet> {
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.InvoiceReceipt;
/**
* 收票登记 Mapper 接口
*
* @author Chill
*/
@Mapper
public interface InvoiceReceiptMapper extends BaseMapper<InvoiceReceipt> {
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.InvoiceReceiptRecord;
/**
* 收票登记操作记录 Mapper 接口
*
* @author Chill
*/
@Mapper
public interface InvoiceReceiptRecordMapper extends BaseMapper<InvoiceReceiptRecord> {
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.InvoiceReceiptSettlement;
/**
* 收票登记结算单分摊 Mapper 接口
*
* @author Chill
*/
@Mapper
public interface InvoiceReceiptSettlementMapper extends BaseMapper<InvoiceReceiptSettlement> {
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
/**
* 金蝶进项发票票据池 Mapper 接口
*
* @author Chill
*/
@Mapper
public interface KingdeeInvoicePoolMapper extends BaseMapper<KingdeeInvoicePool> {
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.KingdeeReceiptFlow;
/**
* 金蝶收款流水镜像 Mapper 接口
*
* @author Chill
*/
@Mapper
public interface KingdeeReceiptFlowMapper extends BaseMapper<KingdeeReceiptFlow> {
}
@@ -0,0 +1,29 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
* Copyright of this software remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.PaymentApplicationInvoice;
/** 付款申请发票 Mapper。 @author Chill */
@Mapper
public interface PaymentApplicationInvoiceMapper extends BaseMapper<PaymentApplicationInvoice> {
}
@@ -0,0 +1,29 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
* Copyright of this software remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.PaymentApplication;
/** 付款申请 Mapper。 @author Chill */
@Mapper
public interface PaymentApplicationMapper extends BaseMapper<PaymentApplication> {
}
@@ -0,0 +1,29 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
* Copyright of this software remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.PaymentApplicationRecord;
/** 付款申请付款记录 Mapper。 @author Chill */
@Mapper
public interface PaymentApplicationRecordMapper extends BaseMapper<PaymentApplicationRecord> {
}
@@ -0,0 +1,50 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springblade.transport.pojo.entity.ReceiptClaim;
import org.springblade.transport.pojo.vo.ReceiptClaimRecordVO;
import java.util.List;
/**
* 收款流水认领 Mapper 接口
*
* @author Chill
*/
@Mapper
public interface ReceiptClaimMapper extends BaseMapper<ReceiptClaim> {
List<ReceiptClaimRecordVO> selectClaimRecordPage(IPage<ReceiptClaimRecordVO> page,
@Param("query") ReceiptClaimRecordVO query, @Param("claimerId") Long claimerId);
ReceiptClaimRecordVO selectClaimRecordDetail(@Param("id") Long id,
@Param("claimerId") Long claimerId);
}
@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.springblade.transport.mapper.ReceiptClaimMapper">
<sql id="claimRecordColumns">
c.id,
c.tenant_id,
c.create_user,
c.create_dept,
c.create_time,
c.update_user,
c.update_time,
c.status,
c.is_deleted,
c.receipt_flow_id,
c.claim_amount,
c.claimer_id,
c.claimer_name,
c.claimer_dept_id,
c.claimer_dept_name,
c.claim_date,
c.attachments_json,
c.remark,
c.claim_status,
c.kingdee_bill_no,
c.kingdee_bill_status,
c.voided_by,
c.voided_by_name,
c.voided_time,
f.receipt_notice_no,
f.payer_name,
f.receipt_amount,
f.counterparty_name,
f.counterparty_account,
f.counterparty_bank,
f.summary,
f.transaction_time,
f.detail_serial_no,
(SELECT GROUP_CONCAT(rcs.formal_settlement_no ORDER BY rcs.id SEPARATOR ',')
FROM blade_receipt_claim_settlement rcs
WHERE rcs.receipt_claim_id = c.id AND rcs.is_deleted = 0) AS associated_settlement_nos
</sql>
<sql id="claimRecordConditions">
c.is_deleted = 0
AND f.is_deleted = 0
AND c.claimer_id = #{claimerId}
<if test="query.receiptNoticeNo != null and query.receiptNoticeNo != ''">
<bind name="receiptNoticeNoLike" value="'%' + query.receiptNoticeNo + '%'"/>
AND f.receipt_notice_no LIKE #{receiptNoticeNoLike}
</if>
<if test="query.counterpartyName != null and query.counterpartyName != ''">
<bind name="counterpartyNameLike" value="'%' + query.counterpartyName + '%'"/>
AND f.counterparty_name LIKE #{counterpartyNameLike}
</if>
<if test="query.counterpartyBank != null and query.counterpartyBank != ''">
<bind name="counterpartyBankLike" value="'%' + query.counterpartyBank + '%'"/>
AND f.counterparty_bank LIKE #{counterpartyBankLike}
</if>
<if test="query.counterpartyAccount != null and query.counterpartyAccount != ''">
<bind name="counterpartyAccountLike" value="'%' + query.counterpartyAccount + '%'"/>
AND f.counterparty_account LIKE #{counterpartyAccountLike}
</if>
<if test="query.summary != null and query.summary != ''">
<bind name="summaryLike" value="'%' + query.summary + '%'"/>
AND f.summary LIKE #{summaryLike}
</if>
<if test="query.claimStatus != null and query.claimStatus != ''">
AND c.claim_status = #{query.claimStatus}
</if>
<if test="query.transactionStartDate != null">
AND f.transaction_time &gt;= #{query.transactionStartDate}
</if>
<if test="query.transactionEndDate != null">
AND f.transaction_time &lt; DATE_ADD(#{query.transactionEndDate}, INTERVAL 1 DAY)
</if>
<if test="query.claimerName != null and query.claimerName != ''">
<bind name="claimerNameLike" value="'%' + query.claimerName + '%'"/>
AND c.claimer_name LIKE #{claimerNameLike}
</if>
<if test="query.claimStartDate != null">
AND c.claim_date &gt;= #{query.claimStartDate}
</if>
<if test="query.claimEndDate != null">
AND c.claim_date &lt;= #{query.claimEndDate}
</if>
<if test="query.claimerDeptName != null and query.claimerDeptName != ''">
<bind name="claimerDeptNameLike" value="'%' + query.claimerDeptName + '%'"/>
AND c.claimer_dept_name LIKE #{claimerDeptNameLike}
</if>
<if test="query.kingdeeBillStatus != null and query.kingdeeBillStatus != ''">
AND c.kingdee_bill_status = #{query.kingdeeBillStatus}
</if>
</sql>
<select id="selectClaimRecordPage" resultType="org.springblade.transport.pojo.vo.ReceiptClaimRecordVO">
SELECT <include refid="claimRecordColumns"/>
FROM blade_receipt_claim c
INNER JOIN blade_kingdee_receipt_flow f ON f.id = c.receipt_flow_id
WHERE <include refid="claimRecordConditions"/>
ORDER BY c.create_time DESC
</select>
<select id="selectClaimRecordDetail" resultType="org.springblade.transport.pojo.vo.ReceiptClaimRecordVO">
SELECT <include refid="claimRecordColumns"/>
FROM blade_receipt_claim c
INNER JOIN blade_kingdee_receipt_flow f ON f.id = c.receipt_flow_id
WHERE c.id = #{id}
AND c.claimer_id = #{claimerId}
AND c.is_deleted = 0
AND f.is_deleted = 0
</select>
</mapper>
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.ReceiptClaimSettlement;
/**
* 收款认领结算单分摊 Mapper 接口
*
* @author Chill
*/
@Mapper
public interface ReceiptClaimSettlementMapper extends BaseMapper<ReceiptClaimSettlement> {
}
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.springblade.transport.pojo.entity.ReceiptFlowRecord;
/**
* 收款流水操作留痕 Mapper 接口
*
* @author Chill
*/
@Mapper
public interface ReceiptFlowRecordMapper extends BaseMapper<ReceiptFlowRecord> {
}
@@ -0,0 +1,24 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.pojo.dto.BillLedgerSaveRequest;
import org.springblade.transport.pojo.entity.BillLedger;
import org.springblade.transport.pojo.vo.BillLedgerVO;
import java.util.List;
import java.util.Map;
/** 汇票台账服务。 @author Chill */
public interface IBillLedgerService extends BaseService<BillLedger> {
IPage<BillLedgerVO> selectPage(IPage<BillLedger> page, BillLedgerVO query);
BillLedgerVO detail(Long id);
Map<String, Long> expiryCounts();
List<BillLedgerVO> availableOptions(String keyword, Long deptId, Long selectedId);
Long submit(BillLedgerSaveRequest request);
void removeLedger(Long id);
}
@@ -0,0 +1,45 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.pojo.dto.BillPaymentSaveRequest;
import org.springblade.transport.pojo.dto.BillPaymentStatusRequest;
import org.springblade.transport.pojo.entity.BillPayment;
import org.springblade.transport.pojo.vo.BillPaymentVO;
/** 汇票付款服务。 @author Chill */
public interface IBillPaymentService extends BaseService<BillPayment> {
IPage<BillPaymentVO> selectPage(IPage<BillPayment> page, BillPaymentVO query);
BillPaymentVO detail(Long id);
Long saveDraft(BillPaymentSaveRequest request);
void removeDraft(Long id);
void submit(BillPaymentStatusRequest request);
void approve(BillPaymentStatusRequest request);
void returnBill(BillPaymentStatusRequest request);
void voidBill(BillPaymentStatusRequest request);
}
@@ -0,0 +1,57 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.pojo.dto.InvoiceApplicationSaveRequest;
import org.springblade.transport.pojo.dto.InvoiceApplicationStatusRequest;
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
import org.springblade.transport.pojo.entity.InvoiceApplication;
import org.springblade.transport.pojo.vo.InvoiceApplicationVO;
import java.util.List;
import java.util.Map;
/**
* 开票申请服务
*
* @author Chill
*/
public interface IInvoiceApplicationService extends BaseService<InvoiceApplication> {
IPage<InvoiceApplicationVO> selectPage(IPage<InvoiceApplication> page, InvoiceApplicationVO query);
InvoiceApplicationVO detail(Long id);
List<Map<String, Object>> settlementCandidates(String keyword);
List<FormalSettlementDetail> settlementDetails(String settlementIds);
Map<String, Object> receiverInformation(String settlementIds);
Long saveDraft(InvoiceApplicationSaveRequest request);
void removeDraft(Long id);
void submit(InvoiceApplicationStatusRequest request);
void approve(InvoiceApplicationStatusRequest request);
void returnBill(InvoiceApplicationStatusRequest request);
void voidBill(InvoiceApplicationStatusRequest request);
String syncKingdee(Long id);
}
@@ -0,0 +1,69 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest;
import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
import org.springblade.transport.pojo.entity.InvoiceReceipt;
import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
import org.springblade.transport.pojo.vo.InvoiceReceiptVO;
import java.util.List;
import java.util.Map;
/**
* 收票登记服务
*
* @author Chill
*/
public interface IInvoiceReceiptService extends BaseService<InvoiceReceipt> {
IPage<InvoiceReceiptVO> selectPage(IPage<InvoiceReceipt> page, InvoiceReceiptVO query);
InvoiceReceiptVO detail(Long id);
List<KingdeeInvoicePool> invoicePool(String keyword);
List<Map<String, Object>> settlementCandidates(String keyword, Long receiptId);
Map<String, Object> referenceInformation(String settlementIds);
Long saveDraft(InvoiceReceiptSaveRequest request);
void removeDraft(Long id);
void submit(InvoiceReceiptStatusRequest request);
void approve(InvoiceReceiptStatusRequest request);
void returnBill(InvoiceReceiptStatusRequest request);
void voidBill(InvoiceReceiptStatusRequest request);
String syncKingdee(Long id);
}
@@ -0,0 +1,40 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
* Copyright of this software remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest;
import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.pojo.vo.PaymentApplicationVO;
/** 付款申请服务。 @author Chill */
public interface IPaymentApplicationService extends BaseService<PaymentApplication> {
IPage<PaymentApplicationVO> selectPage(IPage<PaymentApplication> page, PaymentApplicationVO query);
PaymentApplicationVO detail(Long id);
Long saveDraft(PaymentApplicationSaveRequest request);
void removeDraft(Long id);
void submit(PaymentApplicationStatusRequest request);
void approve(PaymentApplicationStatusRequest request);
void returnBill(PaymentApplicationStatusRequest request);
void voidBill(PaymentApplicationStatusRequest request);
String syncKingdee(Long id);
}
@@ -0,0 +1,49 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.pojo.dto.ReceiptClaimAttachmentsRequest;
import org.springblade.transport.pojo.entity.ReceiptClaim;
import org.springblade.transport.pojo.vo.ReceiptClaimRecordVO;
/**
* 认领记录服务
*
* @author Chill
*/
public interface IReceiptClaimRecordService extends BaseService<ReceiptClaim> {
IPage<ReceiptClaimRecordVO> selectPage(IPage<ReceiptClaimRecordVO> page,
ReceiptClaimRecordVO query);
ReceiptClaimRecordVO detail(Long id);
void updateAttachments(ReceiptClaimAttachmentsRequest request);
String voidClaim(Long id);
}
@@ -0,0 +1,54 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.pojo.dto.ReceiptClaimRequest;
import org.springblade.transport.pojo.dto.ReceiptFlowSyncRequest;
import org.springblade.transport.pojo.entity.KingdeeReceiptFlow;
import org.springblade.transport.pojo.vo.ReceiptFlowVO;
import java.util.List;
import java.util.Map;
/**
* 收款流水服务
*
* @author Chill
*/
public interface IReceiptFlowService extends BaseService<KingdeeReceiptFlow> {
IPage<ReceiptFlowVO> selectPage(IPage<KingdeeReceiptFlow> page, ReceiptFlowVO query);
ReceiptFlowVO detail(Long id);
List<Map<String, Object>> settlementCandidates(String keyword, Long flowId);
Long claim(ReceiptClaimRequest request);
int sync(ReceiptFlowSyncRequest request);
}
@@ -0,0 +1,302 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.mapper.BillLedgerMapper;
import org.springblade.transport.mapper.BillLedgerUsageMapper;
import org.springblade.transport.mapper.CustomerArchiveMapper;
import org.springblade.transport.pojo.dto.BillLedgerSaveRequest;
import org.springblade.transport.pojo.entity.BillLedger;
import org.springblade.transport.pojo.entity.BillLedgerUsage;
import org.springblade.transport.pojo.entity.CustomerArchive;
import org.springblade.transport.pojo.vo.BillLedgerVO;
import org.springblade.transport.service.IBillLedgerService;
import org.springblade.transport.wrapper.BillLedgerWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/** 汇票台账服务实现。 @author Chill */
@Service
@RequiredArgsConstructor
public class BillLedgerServiceImpl extends BaseServiceImpl<BillLedgerMapper, BillLedger>
implements IBillLedgerService {
private static final String APPROVED = "approved";
private final BillLedgerUsageMapper usageMapper;
private final CustomerArchiveMapper customerArchiveMapper;
@Override
public IPage<BillLedgerVO> selectPage(IPage<BillLedger> page, BillLedgerVO query) {
LocalDate today = LocalDate.now();
LambdaQueryWrapper<BillLedger> wrapper = Wrappers.<BillLedger>lambdaQuery()
.like(Func.isNotEmpty(query.getBillNo()), BillLedger::getBillNo, query.getBillNo())
.ge(query.getIssueStartDate() != null, BillLedger::getIssueDate, query.getIssueStartDate())
.le(query.getIssueEndDate() != null, BillLedger::getIssueDate, query.getIssueEndDate())
.like(Func.isNotEmpty(query.getIssuerName()), BillLedger::getIssuerName, query.getIssuerName())
.like(Func.isNotEmpty(query.getReceiverName()), BillLedger::getReceiverName, query.getReceiverName())
.eq(Func.isNotEmpty(query.getBillType()), BillLedger::getBillType, query.getBillType());
applyMaturityStatus(wrapper, query.getMaturityStatus(), today);
applyExpiryShortcut(wrapper, query.getExpiryShortcut(), today);
wrapper.orderByDesc(BillLedger::getCreateTime);
return page(page, wrapper).convert(item -> BillLedgerWrapper.build().entityVO(item));
}
@Override
public BillLedgerVO detail(Long id) {
BillLedgerVO vo = BillLedgerWrapper.build().entityVO(existing(id));
vo.setUsageRecords(usageMapper.selectList(Wrappers.<BillLedgerUsage>lambdaQuery()
.eq(BillLedgerUsage::getBillLedgerId, id)
.eq(BillLedgerUsage::getUsageStatus, APPROVED)
.orderByDesc(BillLedgerUsage::getCreateTime)));
return vo;
}
@Override
public Map<String, Long> expiryCounts() {
LocalDate today = LocalDate.now();
Map<String, Long> counts = new LinkedHashMap<>();
counts.put("all", count());
counts.put("within30", count(Wrappers.<BillLedger>lambdaQuery()
.ge(BillLedger::getMaturityDate, today)
.le(BillLedger::getMaturityDate, today.plusDays(30))));
counts.put("within90", count(Wrappers.<BillLedger>lambdaQuery()
.gt(BillLedger::getMaturityDate, today.plusDays(30))
.le(BillLedger::getMaturityDate, today.plusDays(90))));
counts.put("over90", count(Wrappers.<BillLedger>lambdaQuery()
.gt(BillLedger::getMaturityDate, today.plusDays(90))));
return counts;
}
@Override
public List<BillLedgerVO> availableOptions(String keyword, Long deptId, Long selectedId) {
LocalDate today = LocalDate.now();
return list(Wrappers.<BillLedger>lambdaQuery()
.and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(BillLedger::getBillNo, keyword)
.or().like(BillLedger::getIssuerName, keyword)
.or().like(BillLedger::getReceiverName, keyword))
.and(wrapper -> wrapper
.gt(BillLedger::getAvailableBalance, BigDecimal.ZERO)
.ge(BillLedger::getMaturityDate, today)
.or(selectedId != null, child -> child.eq(BillLedger::getId, selectedId)))
.orderByAsc(BillLedger::getMaturityDate)
.orderByDesc(BillLedger::getCreateTime)
.last("limit 200")).stream()
.filter(item -> selectedId != null && Objects.equals(item.getId(), selectedId)
|| departmentAvailable(item, deptId))
.map(item -> BillLedgerWrapper.build().entityVO(item))
.toList();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long submit(BillLedgerSaveRequest request) {
validateRequest(request);
BillLedger entity = request.getId() == null ? new BillLedger() : locked(request.getId());
String billNo = required(request.getBillNo(), "票据号码", 32);
if (entity.getId() != null && !billNo.equals(entity.getBillNo())) {
throw new ServiceException("票据号码编辑时不可修改");
}
Long duplicate = count(Wrappers.<BillLedger>lambdaQuery()
.eq(BillLedger::getBillNo, billNo)
.ne(entity.getId() != null, BillLedger::getId, entity.getId()));
if (duplicate > 0) {
throw new ServiceException("票据号码已存在");
}
CustomerArchive issuer = customer(request.getIssuerId(), "出票单位");
CustomerArchive feeBearer = customer(request.getFeeBearerId(), "费用承担方");
BigDecimal faceAmount = positive(request.getFaceAmount(), "票面金额").setScale(2, RoundingMode.HALF_UP);
BigDecimal usedAmount = entity.getId() == null ? BigDecimal.ZERO : activeUsedAmount(entity.getId());
if (faceAmount.compareTo(usedAmount) < 0) {
throw new ServiceException("票面金额不能小于已使用金额");
}
entity.setBillNo(billNo);
entity.setIssuerId(issuer.getId());
entity.setIssuerName(customerName(issuer));
entity.setReceiverName(required(request.getReceiverName(), "收票单位", 100));
entity.setBillType(request.getBillType());
entity.setFaceAmount(faceAmount);
entity.setAvailableBalance(faceAmount.subtract(usedAmount).setScale(2, RoundingMode.HALF_UP));
entity.setIssueDate(request.getIssueDate());
entity.setMaturityDate(request.getMaturityDate());
entity.setAvailableDeptIdsJson(request.getAvailableDeptIdsJson());
entity.setAvailableDeptNames(required(request.getAvailableDeptNames(), "可用部门", 500));
entity.setFeeBearerId(feeBearer.getId());
entity.setFeeBearerName(customerName(feeBearer));
entity.setConfirmedDiscountRate(rate(request.getConfirmedDiscountRate(), "双方确认贴现率"));
entity.setIssuingBank(required(request.getIssuingBank(), "出票行", 100));
entity.setBankDiscountReferenceRate(rate(request.getBankDiscountReferenceRate(), "银行贴现参考率"));
entity.setEstimatedDiscountFee(calculateDiscountFee(faceAmount, entity.getConfirmedDiscountRate()));
entity.setAttachmentsJson(request.getAttachmentsJson());
entity.setRemark(limit(request.getRemark(), 200, "备注"));
if (entity.getId() == null) {
entity.setStatus(1);
}
saveOrUpdate(entity);
return entity.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void removeLedger(Long id) {
BillLedger entity = locked(id);
if (usageMapper.selectCount(Wrappers.<BillLedgerUsage>lambdaQuery()
.eq(BillLedgerUsage::getBillLedgerId, id)
.eq(BillLedgerUsage::getUsageStatus, APPROVED)) > 0) {
throw new ServiceException("存在已审核使用记录的汇票不允许删除");
}
removeById(entity);
}
private void validateRequest(BillLedgerSaveRequest request) {
if (request == null) {
throw new ServiceException("请求参数不能为空");
}
if (!List.of("issued", "received").contains(request.getBillType())) {
throw new ServiceException("汇票类型不合法");
}
if (request.getIssueDate() == null) {
throw new ServiceException("出票日期不能为空");
}
if (request.getMaturityDate() == null || !request.getMaturityDate().isAfter(request.getIssueDate())) {
throw new ServiceException("到期日期必须晚于出票日期");
}
List<?> deptIds = parseArray(request.getAvailableDeptIdsJson(), "可用部门");
if (deptIds.isEmpty()) {
throw new ServiceException("可用部门不能为空");
}
}
private void applyMaturityStatus(LambdaQueryWrapper<BillLedger> wrapper, String status,
LocalDate today) {
if (Func.isEmpty(status)) return;
switch (status) {
case "expired" -> wrapper.lt(BillLedger::getMaturityDate, today);
case "due_today" -> wrapper.eq(BillLedger::getMaturityDate, today);
case "unexpired" -> wrapper.gt(BillLedger::getMaturityDate, today);
default -> throw new ServiceException("到期状态不合法");
}
}
private void applyExpiryShortcut(LambdaQueryWrapper<BillLedger> wrapper, String shortcut,
LocalDate today) {
if (Func.isEmpty(shortcut) || "all".equals(shortcut)) return;
switch (shortcut) {
case "within30" -> wrapper.ge(BillLedger::getMaturityDate, today)
.le(BillLedger::getMaturityDate, today.plusDays(30));
case "within90" -> wrapper.gt(BillLedger::getMaturityDate, today.plusDays(30))
.le(BillLedger::getMaturityDate, today.plusDays(90));
case "over90" -> wrapper.gt(BillLedger::getMaturityDate, today.plusDays(90));
default -> throw new ServiceException("到期快捷筛选不合法");
}
}
private BillLedger existing(Long id) {
BillLedger entity = getById(id);
if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) {
throw new ServiceException("汇票台账不存在");
}
return entity;
}
private BillLedger locked(Long id) {
BillLedger entity = baseMapper.selectOne(Wrappers.<BillLedger>lambdaQuery()
.eq(BillLedger::getId, id).last("FOR UPDATE"));
if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) {
throw new ServiceException("汇票台账不存在");
}
return entity;
}
private CustomerArchive customer(Long id, String name) {
if (id == null) throw new ServiceException(name + "不能为空");
CustomerArchive customer = customerArchiveMapper.selectById(id);
if (customer == null || Objects.equals(customer.getIsDeleted(), 1)) {
throw new ServiceException(name + "对应的客商档案不存在");
}
return customer;
}
private BigDecimal activeUsedAmount(Long ledgerId) {
return usageMapper.selectList(Wrappers.<BillLedgerUsage>lambdaQuery()
.eq(BillLedgerUsage::getBillLedgerId, ledgerId)
.eq(BillLedgerUsage::getUsageStatus, APPROVED)).stream()
.map(BillLedgerUsage::getUsedAmount)
.map(this::money)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private boolean departmentAvailable(BillLedger ledger, Long deptId) {
List<?> values = parseArray(ledger.getAvailableDeptIdsJson(), "可用部门");
if (values.stream().anyMatch(value -> "all".equals(String.valueOf(value)))) return true;
return deptId == null || values.stream().anyMatch(value -> String.valueOf(deptId).equals(String.valueOf(value)));
}
private List<?> parseArray(String value, String name) {
if (Func.isEmpty(value)) return List.of();
try {
Object parsed = JsonUtil.parse(value, List.class);
return parsed instanceof List<?> list ? list : List.of();
} catch (Exception exception) {
throw new ServiceException(name + "格式不正确");
}
}
private String customerName(CustomerArchive customer) {
return Func.isNotEmpty(customer.getFullName()) ? customer.getFullName() : customer.getShortName();
}
private BigDecimal calculateDiscountFee(BigDecimal faceAmount, BigDecimal rate) {
if (rate == null) return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
return faceAmount.multiply(rate).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
}
private BigDecimal positive(BigDecimal value, String name) {
if (value == null || value.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException(name + "必须大于0");
}
if (value.scale() > 2) throw new ServiceException(name + "最多保留2位小数");
return value;
}
private BigDecimal rate(BigDecimal value, String name) {
if (value == null) return null;
if (value.compareTo(BigDecimal.ZERO) < 0 || value.compareTo(BigDecimal.valueOf(100)) > 0) {
throw new ServiceException(name + "必须在0-100之间");
}
return value.setScale(Math.min(value.scale(), 4), RoundingMode.HALF_UP);
}
private BigDecimal money(BigDecimal value) {
return value == null ? BigDecimal.ZERO : value;
}
private String required(String value, String name, int length) {
if (Func.isEmpty(value) || value.trim().isEmpty()) throw new ServiceException(name + "不能为空");
return limit(value.trim(), length, name);
}
private String limit(String value, int length, String name) {
if (value != null && value.length() > length) {
throw new ServiceException(name + "不能超过" + length + "个字符");
}
return value;
}
}
@@ -0,0 +1,343 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.SysCache;
import org.springblade.transport.mapper.BillLedgerMapper;
import org.springblade.transport.mapper.BillLedgerUsageMapper;
import org.springblade.transport.mapper.BillPaymentMapper;
import org.springblade.transport.pojo.dto.BillPaymentSaveRequest;
import org.springblade.transport.pojo.dto.BillPaymentStatusRequest;
import org.springblade.transport.pojo.entity.BillLedger;
import org.springblade.transport.pojo.entity.BillLedgerUsage;
import org.springblade.transport.pojo.entity.BillPayment;
import org.springblade.transport.pojo.vo.BillPaymentVO;
import org.springblade.transport.service.IBillPaymentService;
import org.springblade.transport.wrapper.BillPaymentWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Objects;
/** 汇票付款服务实现。 @author Chill */
@Service
@RequiredArgsConstructor
public class BillPaymentServiceImpl extends BaseServiceImpl<BillPaymentMapper, BillPayment>
implements IBillPaymentService {
private static final String DRAFT = "draft";
private static final String REVIEWING = "reviewing";
private static final String APPROVED = "approved";
private static final String RETURNED = "returned";
private static final String VOIDED = "voided";
private static final String RELEASED = "released";
private final BillLedgerMapper billLedgerMapper;
private final BillLedgerUsageMapper usageMapper;
@Override
public IPage<BillPaymentVO> selectPage(IPage<BillPayment> page, BillPaymentVO query) {
LambdaQueryWrapper<BillPayment> wrapper = Wrappers.<BillPayment>lambdaQuery()
.like(Func.isNotEmpty(query.getPaymentNo()), BillPayment::getPaymentNo, query.getPaymentNo())
.like(Func.isNotEmpty(query.getDeptName()), BillPayment::getDeptName, query.getDeptName())
.ge(query.getPaymentStartDate() != null, BillPayment::getPaymentDate, query.getPaymentStartDate())
.le(query.getPaymentEndDate() != null, BillPayment::getPaymentDate, query.getPaymentEndDate())
.eq(Func.isNotEmpty(query.getApprovalStatus()), BillPayment::getApprovalStatus,
query.getApprovalStatus())
.orderByDesc(BillPayment::getCreateTime);
return page(page, wrapper).convert(item -> BillPaymentWrapper.build().entityVO(item));
}
@Override
public BillPaymentVO detail(Long id) {
BillPayment entity = existing(id);
BillPaymentVO vo = BillPaymentWrapper.build().entityVO(entity);
BillLedger ledger = billLedgerMapper.selectById(entity.getBillLedgerId());
if (ledger != null) {
vo.setBillNo(ledger.getBillNo());
vo.setFaceAmount(ledger.getFaceAmount());
vo.setAvailableBalance(ledger.getAvailableBalance());
}
return vo;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long saveDraft(BillPaymentSaveRequest request) {
validateRequest(request);
BillPayment entity = request.getId() == null ? new BillPayment() : locked(request.getId());
if (entity.getId() != null && !List.of(DRAFT, RETURNED).contains(entity.getApprovalStatus())) {
throw new ServiceException("当前状态不可编辑");
}
Long deptId = Func.firstLong(AuthUtil.getDeptId());
if (deptId == null) {
throw new ServiceException("使用部门不能为空");
}
if (entity.getId() != null && !Objects.equals(entity.getDeptId(), deptId)) {
throw new ServiceException("仅允许编辑当前部门的汇票付款");
}
String deptName = required(SysCache.getDeptName(deptId), "使用部门", 100);
BillLedger ledger = lockedBill(request.getBillLedgerId());
BigDecimal usedAmount = positive(request.getUsedAmount(), "本次使用");
validateLedgerAmount(ledger, usedAmount, deptId);
if (entity.getId() == null) {
entity.setPaymentNo(nextNo());
entity.setApprovalStatus(DRAFT);
entity.setCurrentNode("草稿");
}
entity.setBillLedgerId(ledger.getId());
entity.setBillNo(ledger.getBillNo());
entity.setFaceAmount(money(ledger.getFaceAmount()).setScale(2, RoundingMode.HALF_UP));
entity.setAvailableBalance(money(ledger.getAvailableBalance()).setScale(2, RoundingMode.HALF_UP));
entity.setUsedAmount(usedAmount);
entity.setDeptId(deptId);
entity.setDeptName(deptName);
entity.setPaymentDate(request.getPaymentDate() == null ? LocalDate.now() : request.getPaymentDate());
entity.setAttachmentsJson(request.getAttachmentsJson());
entity.setRemark(limit(request.getRemark(), 200, "备注"));
entity.setStatus(1);
saveOrUpdate(entity);
return entity.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void removeDraft(Long id) {
BillPayment entity = locked(id);
if (!DRAFT.equals(entity.getApprovalStatus())) {
throw new ServiceException("仅草稿状态的汇票付款允许删除");
}
removeById(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void submit(BillPaymentStatusRequest request) {
BillPayment entity = locked(requiredId(request));
if (!List.of(DRAFT, RETURNED).contains(entity.getApprovalStatus())) {
throw new ServiceException("当前状态不允许提交");
}
validateStoredAmount(entity);
entity.setApprovalStatus(REVIEWING);
entity.setCurrentNode("财务审核");
entity.setCurrentProcessor(AuthUtil.getUserName());
updateById(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void approve(BillPaymentStatusRequest request) {
BillPayment entity = locked(requiredId(request));
if (!REVIEWING.equals(entity.getApprovalStatus())) {
throw new ServiceException("仅审批中的汇票付款允许审核");
}
BillLedger ledger = lockedBill(entity.getBillLedgerId());
validateLedgerAmount(ledger, entity.getUsedAmount(), entity.getDeptId());
BillLedgerUsage exists = usageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
.eq(BillLedgerUsage::getBillPaymentId, entity.getId())
.eq(BillLedgerUsage::getUsageStatus, APPROVED)
.last("FOR UPDATE"));
if (exists != null) {
throw new ServiceException("该汇票付款已生成使用记录");
}
BigDecimal usedAmount = money(entity.getUsedAmount()).setScale(2, RoundingMode.HALF_UP);
ledger.setAvailableBalance(money(ledger.getAvailableBalance()).subtract(usedAmount)
.setScale(2, RoundingMode.HALF_UP));
billLedgerMapper.updateById(ledger);
BillLedgerUsage usage = new BillLedgerUsage();
usage.setBillLedgerId(ledger.getId());
usage.setBillPaymentId(entity.getId());
usage.setApplicationNo(entity.getPaymentNo());
usage.setUsedAmount(usedAmount);
usage.setUseDeptId(entity.getDeptId());
usage.setUseDeptName(entity.getDeptName());
usage.setUsageStatus(APPROVED);
usage.setStatus(1);
usageMapper.insert(usage);
entity.setAvailableBalance(ledger.getAvailableBalance());
entity.setApprovalStatus(APPROVED);
entity.setCurrentNode("审批通过");
entity.setCurrentProcessor(AuthUtil.getUserName());
updateById(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void returnBill(BillPaymentStatusRequest request) {
BillPayment entity = locked(requiredId(request));
if (!REVIEWING.equals(entity.getApprovalStatus())) {
throw new ServiceException("仅审批中的汇票付款允许驳回");
}
entity.setApprovalStatus(RETURNED);
entity.setCurrentNode("已驳回");
entity.setCurrentProcessor(AuthUtil.getUserName());
entity.setRemark(request.getReason() == null ? entity.getRemark() : limit(request.getReason(), 200, "驳回原因"));
updateById(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void voidBill(BillPaymentStatusRequest request) {
BillPayment entity = locked(requiredId(request));
if (!APPROVED.equals(entity.getApprovalStatus())) {
throw new ServiceException("仅审批通过的汇票付款允许作废");
}
BillLedgerUsage usage = usageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
.eq(BillLedgerUsage::getBillPaymentId, entity.getId())
.eq(BillLedgerUsage::getUsageStatus, APPROVED)
.last("FOR UPDATE"));
if (usage == null) {
throw new ServiceException("未找到汇票使用记录,无法作废");
}
BillLedger ledger = lockedBill(usage.getBillLedgerId());
ledger.setAvailableBalance(money(ledger.getAvailableBalance()).add(money(usage.getUsedAmount()))
.min(money(ledger.getFaceAmount())).setScale(2, RoundingMode.HALF_UP));
billLedgerMapper.updateById(ledger);
usage.setUsageStatus(RELEASED);
usageMapper.updateById(usage);
entity.setAvailableBalance(ledger.getAvailableBalance());
entity.setApprovalStatus(VOIDED);
entity.setCurrentNode("已作废");
entity.setCurrentProcessor(AuthUtil.getUserName());
entity.setRemark(request.getReason() == null ? entity.getRemark() : limit(request.getReason(), 200, "作废原因"));
updateById(entity);
}
private void validateRequest(BillPaymentSaveRequest request) {
if (request == null) throw new ServiceException("请求参数不能为空");
if (request.getBillLedgerId() == null) throw new ServiceException("票据号码不能为空");
if (request.getPaymentDate() == null) throw new ServiceException("付款日期不能为空");
positive(request.getUsedAmount(), "本次使用");
}
private void validateStoredAmount(BillPayment entity) {
BillLedger ledger = lockedBill(entity.getBillLedgerId());
validateLedgerAmount(ledger, entity.getUsedAmount(), entity.getDeptId());
entity.setBillNo(ledger.getBillNo());
entity.setFaceAmount(ledger.getFaceAmount());
entity.setAvailableBalance(ledger.getAvailableBalance());
}
private void validateLedgerAmount(BillLedger ledger, BigDecimal usedAmount, Long deptId) {
if (ledger.getMaturityDate() != null && ledger.getMaturityDate().isBefore(LocalDate.now())) {
throw new ServiceException("所选汇票已到期");
}
if (usedAmount == null || usedAmount.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("本次使用必须大于0");
}
if (usedAmount.scale() > 2) {
throw new ServiceException("本次使用最多保留2位小数");
}
if (usedAmount.compareTo(money(ledger.getAvailableBalance())) > 0) {
throw new ServiceException("本次使用不能超过汇票可用余额");
}
if (!departmentAvailable(ledger, deptId)) {
throw new ServiceException("当前使用部门不在汇票可用部门范围内");
}
}
private boolean departmentAvailable(BillLedger ledger, Long deptId) {
if (Func.isEmpty(ledger.getAvailableDeptIdsJson())) return false;
try {
Object parsed = JsonUtil.parse(ledger.getAvailableDeptIdsJson(), List.class);
if (!(parsed instanceof List<?> values)) return false;
if (values.stream().anyMatch(item -> "all".equals(String.valueOf(item)))) return true;
return deptId != null && values.stream().anyMatch(item -> String.valueOf(deptId).equals(String.valueOf(item)));
} catch (Exception exception) {
throw new ServiceException("汇票可用部门配置不正确");
}
}
private BillLedger lockedBill(Long id) {
BillLedger ledger = billLedgerMapper.selectOne(Wrappers.<BillLedger>lambdaQuery()
.eq(BillLedger::getId, id).last("FOR UPDATE"));
if (ledger == null || Objects.equals(ledger.getIsDeleted(), 1)) {
throw new ServiceException("所选汇票台账不存在");
}
return ledger;
}
private BillPayment locked(Long id) {
BillPayment entity = baseMapper.selectOne(Wrappers.<BillPayment>lambdaQuery()
.eq(BillPayment::getId, id).last("FOR UPDATE"));
if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) {
throw new ServiceException("汇票付款不存在");
}
return entity;
}
private BillPayment existing(Long id) {
BillPayment entity = getById(id);
if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) {
throw new ServiceException("汇票付款不存在");
}
return entity;
}
private Long requiredId(BillPaymentStatusRequest request) {
if (request == null || request.getId() == null) throw new ServiceException("单据不能为空");
return request.getId();
}
private synchronized String nextNo() {
String prefix = "HP" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
return prefix + String.format("%05d", count(Wrappers.<BillPayment>lambdaQuery()
.likeRight(BillPayment::getPaymentNo, prefix)) + 1);
}
private BigDecimal positive(BigDecimal value, String name) {
if (value == null || value.compareTo(BigDecimal.ZERO) <= 0) throw new ServiceException(name + "必须大于0");
if (value.scale() > 2) throw new ServiceException(name + "最多保留2位小数");
return value.setScale(2, RoundingMode.HALF_UP);
}
private BigDecimal money(BigDecimal value) {
return value == null ? BigDecimal.ZERO : value;
}
private String required(String value, String name, int length) {
if (Func.isEmpty(value) || value.trim().isEmpty()) throw new ServiceException(name + "不能为空");
return limit(value.trim(), length, name);
}
private String limit(String value, int length, String name) {
if (value != null && value.length() > length) throw new ServiceException(name + "不能超过" + length + "个字符");
return value;
}
}
@@ -0,0 +1,643 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.mapper.CustomerArchiveMapper;
import org.springblade.transport.mapper.CustomerContactMapper;
import org.springblade.transport.mapper.CustomerInvoiceInfoMapper;
import org.springblade.transport.mapper.FormalSettlementDetailMapper;
import org.springblade.transport.mapper.FormalSettlementMapper;
import org.springblade.transport.mapper.InvoiceApplicationDetailMapper;
import org.springblade.transport.mapper.InvoiceApplicationLineMapper;
import org.springblade.transport.mapper.InvoiceApplicationMapper;
import org.springblade.transport.mapper.InvoiceApplicationRecordMapper;
import org.springblade.transport.mapper.InvoiceApplicationSettlementMapper;
import org.springblade.transport.mapper.InvoiceApplicationSheetMapper;
import org.springblade.transport.pojo.dto.InvoiceApplicationSaveRequest;
import org.springblade.transport.pojo.dto.InvoiceApplicationStatusRequest;
import org.springblade.transport.pojo.entity.CustomerArchive;
import org.springblade.transport.pojo.entity.CustomerContact;
import org.springblade.transport.pojo.entity.CustomerInvoiceInfo;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
import org.springblade.transport.pojo.entity.InvoiceApplication;
import org.springblade.transport.pojo.entity.InvoiceApplicationDetail;
import org.springblade.transport.pojo.entity.InvoiceApplicationLine;
import org.springblade.transport.pojo.entity.InvoiceApplicationRecord;
import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement;
import org.springblade.transport.pojo.entity.InvoiceApplicationSheet;
import org.springblade.transport.pojo.vo.InvoiceApplicationSheetVO;
import org.springblade.transport.pojo.vo.InvoiceApplicationVO;
import org.springblade.transport.service.IInvoiceApplicationService;
import org.springblade.transport.wrapper.InvoiceApplicationWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 开票申请服务实现类
*
* @author Chill
*/
@Service
@RequiredArgsConstructor
public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplicationMapper, InvoiceApplication>
implements IInvoiceApplicationService {
private static final String DRAFT = "draft";
private static final String REVIEWING = "reviewing";
private static final String APPROVED = "approved";
private static final String RETURNED = "returned";
private static final String VOIDED = "voided";
private final InvoiceApplicationSettlementMapper settlementRelationMapper;
private final InvoiceApplicationSheetMapper sheetMapper;
private final InvoiceApplicationLineMapper lineMapper;
private final InvoiceApplicationDetailMapper applicationDetailMapper;
private final InvoiceApplicationRecordMapper recordMapper;
private final FormalSettlementMapper formalSettlementMapper;
private final FormalSettlementDetailMapper formalSettlementDetailMapper;
private final CustomerArchiveMapper customerArchiveMapper;
private final CustomerInvoiceInfoMapper customerInvoiceInfoMapper;
private final CustomerContactMapper customerContactMapper;
@Override
public IPage<InvoiceApplicationVO> selectPage(IPage<InvoiceApplication> page, InvoiceApplicationVO query) {
LambdaQueryWrapper<InvoiceApplication> wrapper = Wrappers.<InvoiceApplication>lambdaQuery()
.like(Func.isNotEmpty(query.getApplicationNo()), InvoiceApplication::getApplicationNo, query.getApplicationNo())
.like(Func.isNotEmpty(query.getProjectName()), InvoiceApplication::getProjectName, query.getProjectName())
.like(Func.isNotEmpty(query.getDeptName()), InvoiceApplication::getDeptName, query.getDeptName())
.eq(Func.isNotEmpty(query.getKingdeeStatus()), InvoiceApplication::getKingdeeStatus, query.getKingdeeStatus())
.eq(Func.isNotEmpty(query.getApprovalStatus()), InvoiceApplication::getApprovalStatus, query.getApprovalStatus())
.orderByDesc(InvoiceApplication::getCreateTime);
return page(page, wrapper).convert(this::toListVO);
}
@Override
public InvoiceApplicationVO detail(Long id) {
InvoiceApplication entity = existing(id);
InvoiceApplicationVO vo = toListVO(entity);
vo.setSettlements(settlementRelationMapper.selectList(Wrappers.<InvoiceApplicationSettlement>lambdaQuery()
.eq(InvoiceApplicationSettlement::getInvoiceApplicationId, id)
.orderByAsc(InvoiceApplicationSettlement::getCreateTime)));
List<InvoiceApplicationSheet> sheets = sheetMapper.selectList(Wrappers.<InvoiceApplicationSheet>lambdaQuery()
.eq(InvoiceApplicationSheet::getInvoiceApplicationId, id).orderByAsc(InvoiceApplicationSheet::getSheetNo));
vo.setSheets(sheets.stream().map(sheet -> {
InvoiceApplicationSheetVO sheetVO = Objects.requireNonNull(BeanUtil.copyProperties(sheet, InvoiceApplicationSheetVO.class));
sheetVO.setLines(lineMapper.selectList(Wrappers.<InvoiceApplicationLine>lambdaQuery()
.eq(InvoiceApplicationLine::getInvoiceSheetId, sheet.getId()).orderByAsc(InvoiceApplicationLine::getLineNo)));
return sheetVO;
}).toList());
vo.setDetails(applicationDetailMapper.selectList(Wrappers.<InvoiceApplicationDetail>lambdaQuery()
.eq(InvoiceApplicationDetail::getInvoiceApplicationId, id).orderByAsc(InvoiceApplicationDetail::getLineNo)));
vo.setRecords(recordMapper.selectList(Wrappers.<InvoiceApplicationRecord>lambdaQuery()
.eq(InvoiceApplicationRecord::getInvoiceApplicationId, id)
.orderByAsc(InvoiceApplicationRecord::getCreateTime)));
return vo;
}
@Override
public List<Map<String, Object>> settlementCandidates(String keyword) {
List<FormalSettlement> settlements = formalSettlementMapper.selectList(Wrappers.<FormalSettlement>lambdaQuery()
.eq(FormalSettlement::getSettlementType, "receivable")
.eq(FormalSettlement::getApprovalStatus, APPROVED)
.eq(FormalSettlement::getStatus, 1)
.and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(FormalSettlement::getFormalSettlementNo, keyword)
.or().like(FormalSettlement::getProjectName, keyword)
.or().like(FormalSettlement::getContractName, keyword))
.orderByDesc(FormalSettlement::getCreateTime).last("limit 200"));
return settlements.stream().map(settlement -> {
BigDecimal available = availableAmount(settlement, null);
Map<String, Object> row = new LinkedHashMap<>();
row.put("id", settlement.getId());
row.put("formalSettlementNo", settlement.getFormalSettlementNo());
row.put("projectId", settlement.getProjectId());
row.put("projectName", settlement.getProjectName());
row.put("deptId", settlement.getDeptId());
row.put("deptName", settlement.getDeptName());
row.put("contractId", settlement.getContractId());
row.put("contractNo", settlement.getContractNo());
row.put("contractName", settlement.getContractName());
row.put("issuerName", settlement.getPayeeName());
row.put("receiverName", settlement.getPayerName());
row.put("settlementAmount", money(settlement.getSettlementAmount()));
row.put("availableInvoiceAmount", available);
return row;
}).filter(row -> ((BigDecimal) row.get("availableInvoiceAmount")).compareTo(BigDecimal.ZERO) > 0).toList();
}
@Override
public List<FormalSettlementDetail> settlementDetails(String settlementIds) {
List<Long> ids = distinctIds(settlementIds);
if (ids.isEmpty()) return List.of();
assertCompatible(ids.stream().map(this::availableSettlement).toList());
return formalSettlementDetailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
.in(FormalSettlementDetail::getFormalSettlementId, ids).orderByAsc(FormalSettlementDetail::getLineNo));
}
@Override
public Map<String, Object> receiverInformation(String settlementIds) {
List<FormalSettlement> settlements = distinctIds(settlementIds).stream().map(this::availableSettlement).toList();
assertCompatible(settlements);
FormalSettlement first = settlements.get(0);
CustomerArchive customer = findCustomer(first.getPayerName());
Map<String, Object> result = new LinkedHashMap<>();
result.put("issuerName", first.getPayeeName());
result.put("receiverName", first.getPayerName());
result.put("customer", customer);
result.put("invoiceInfos", customer == null ? List.of() : customerInvoiceInfoMapper.selectList(
Wrappers.<CustomerInvoiceInfo>lambdaQuery().eq(CustomerInvoiceInfo::getCustomerId, customer.getId())
.eq(CustomerInvoiceInfo::getStatus, 1).orderByDesc(CustomerInvoiceInfo::getIsDefault)));
result.put("contacts", customer == null ? List.of() : customerContactMapper.selectList(
Wrappers.<CustomerContact>lambdaQuery().eq(CustomerContact::getCustomerId, customer.getId())
.eq(CustomerContact::getStatus, 1).orderByDesc(CustomerContact::getIsDefault)));
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long saveDraft(InvoiceApplicationSaveRequest request) {
validateRequest(request);
boolean creating = request.getId() == null;
InvoiceApplication entity = creating ? new InvoiceApplication() : editable(request.getId());
List<Long> oldSettlementIds = entity.getId() == null ? List.of() : relationSettlementIds(entity.getId());
List<InvoiceApplicationSaveRequest.SettlementRow> requestedRows = request.getSettlements().stream()
.filter(Objects::nonNull)
.peek(row -> {
if (row.getSettlementId() == null) throw new ServiceException("正式结算单不能为空");
})
.collect(Collectors.toMap(InvoiceApplicationSaveRequest.SettlementRow::getSettlementId,
Function.identity(), (first, duplicate) -> first, LinkedHashMap::new)).values().stream().toList();
List<FormalSettlement> settlements = requestedRows.stream().map(row -> availableSettlement(row.getSettlementId())).toList();
assertCompatible(settlements);
FormalSettlement first = settlements.get(0);
Map<Long, FormalSettlement> settlementMap = settlements.stream()
.collect(Collectors.toMap(FormalSettlement::getId, Function.identity()));
BigDecimal totalAvailable = BigDecimal.ZERO;
BigDecimal allocatedTotal = BigDecimal.ZERO;
for (InvoiceApplicationSaveRequest.SettlementRow row : requestedRows) {
FormalSettlement settlement = settlementMap.get(row.getSettlementId());
BigDecimal available = availableAmount(settlement, entity.getId());
BigDecimal allocated = nonNegative(row.getAllocatedInvoiceAmount(), "分摊发票金额");
if (allocated.compareTo(available) > 0) {
throw new ServiceException("结算单" + settlement.getFormalSettlementNo() + "的分摊金额超过剩余可开票金额");
}
totalAvailable = totalAvailable.add(available);
allocatedTotal = allocatedTotal.add(allocated);
}
BigDecimal lineTotal = validateSheets(request.getSheets());
if (lineTotal.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("本次开票金额必须大于0");
}
if (lineTotal.compareTo(allocatedTotal) != 0) {
throw new ServiceException("开票商品行金额合计必须等于结算单分摊金额合计");
}
CustomerArchive customer = findCustomer(first.getPayerName());
CustomerInvoiceInfo invoiceInfo = customerInvoiceInfoMapper.selectById(request.getReceiverInvoiceInfoId());
if (customer == null || invoiceInfo == null || !Objects.equals(customer.getId(), invoiceInfo.getCustomerId())
|| Objects.equals(invoiceInfo.getIsDeleted(), 1) || !Objects.equals(invoiceInfo.getStatus(), 1)) {
throw new ServiceException("请选择受票方有效的开票信息");
}
String invoiceTitle = required(invoiceInfo.getInvoiceTitle(), "受票方单位");
String taxpayerNo = limit(invoiceInfo.getTaxNo(), 20, "纳税人识别号");
String bankName = limit(invoiceInfo.getBankName(), 100, "开户行");
String bankAccount = limit(invoiceInfo.getBankAccount(), 50, "开户账号");
String registeredAddress = limit(invoiceInfo.getRegisteredAddress(), 200, "注册地址");
if ("electronic_special".equals(request.getInvoiceType())) {
required(taxpayerNo, "电子专票的纳税人识别号");
required(bankName, "电子专票的开户行");
required(bankAccount, "电子专票的开户账号");
required(registeredAddress, "电子专票的注册地址");
}
if (entity.getId() == null) {
entity.setApplicationNo(nextNo());
entity.setApprovalStatus(DRAFT);
entity.setCurrentNode("草稿");
entity.setKingdeeStatus("unsynced");
entity.setApplicantName(AuthUtil.getUserName());
}
entity.setProjectId(first.getProjectId());
entity.setProjectName(first.getProjectName());
entity.setDeptId(first.getDeptId());
entity.setDeptName(first.getDeptName());
entity.setIssuerName(first.getPayeeName());
entity.setReceiverCustomerId(customer.getId());
entity.setReceiverName(invoiceTitle);
entity.setInvoiceType(request.getInvoiceType());
entity.setAvailableInvoiceAmount(totalAvailable);
entity.setInvoiceAmount(lineTotal);
entity.setUndertakingDeptId(first.getDeptId());
entity.setUndertakingDeptName(first.getDeptName());
entity.setDepartmentEmails(normalizeEmails(request.getDepartmentEmails()));
entity.setReceiverInvoiceInfoId(invoiceInfo.getId());
entity.setTaxpayerNo(taxpayerNo);
entity.setBankName(bankName);
entity.setBankAccount(bankAccount);
entity.setRegisteredAddress(registeredAddress);
entity.setContactName(limit(request.getContactName(), 50, "联系人"));
entity.setContactPhone(validatePhone(request.getContactPhone()));
entity.setEmail(validateReceiverEmails(request.getEmail()));
entity.setAttachmentsJson(request.getAttachmentsJson());
entity.setRemark(limit(request.getRemark(), 200, "备注"));
saveOrUpdate(entity);
deleteChildren(entity.getId());
saveRelations(entity.getId(), requestedRows, settlementMap);
saveSheets(entity.getId(), request.getSheets());
saveDetails(entity.getId(), request.getDetailIds(), settlementMap.keySet());
Set<Long> refreshIds = new LinkedHashSet<>(oldSettlementIds);
refreshIds.addAll(settlementMap.keySet());
refreshIds.forEach(this::refreshSettlementInvoiceStatus);
record(entity.getId(), creating ? "create" : "save", creating ? "创建草稿" : "保存草稿",
entity.getApprovalStatus(), entity.getApprovalStatus(), null, null);
return entity.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void removeDraft(Long id) {
InvoiceApplication entity = editable(id);
List<Long> settlementIds = relationSettlementIds(id);
deleteChildren(id);
removeById(entity);
settlementIds.forEach(this::refreshSettlementInvoiceStatus);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void submit(InvoiceApplicationStatusRequest request) {
InvoiceApplication entity = editable(request.getId());
String fromStatus = entity.getApprovalStatus();
entity.setApprovalStatus(REVIEWING);
entity.setCurrentNode("开票审核");
entity.setCurrentProcessor(null);
entity.setApplicationDate(LocalDate.now());
updateById(entity);
record(entity.getId(), "submit", "提交审批", fromStatus, REVIEWING, null, null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void approve(InvoiceApplicationStatusRequest request) {
changeStatus(request.getId(), REVIEWING, APPROVED, "approve", "审批通过", null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void returnBill(InvoiceApplicationStatusRequest request) {
changeStatus(request.getId(), REVIEWING, RETURNED, "return", "已驳回",
required(request.getReason(), "驳回原因"));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void voidBill(InvoiceApplicationStatusRequest request) {
InvoiceApplication entity = existing(request.getId());
if (!APPROVED.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批通过的开票申请允许作废");
entity.setApprovalStatus(VOIDED);
entity.setCurrentNode("已作废");
entity.setCurrentProcessor(AuthUtil.getUserName());
entity.setVoidReason(limit(required(request.getReason(), "作废原因"), 200, "作废原因"));
updateById(entity);
record(entity.getId(), "void", "作废", APPROVED, VOIDED, entity.getVoidReason(), entity.getKingdeeBillNo());
relationSettlementIds(entity.getId()).forEach(this::refreshSettlementInvoiceStatus);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String syncKingdee(Long id) {
InvoiceApplication entity = existing(id);
if (!APPROVED.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批通过的开票申请允许同步金蝶");
if ("synced".equals(entity.getKingdeeStatus())) return entity.getKingdeeBillNo();
String kingdeeNo = "K3INV" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());
entity.setKingdeeBillNo(kingdeeNo);
entity.setKingdeeStatus("synced");
entity.setSyncedTime(LocalDateTime.now());
updateById(entity);
record(entity.getId(), "sync", "同步金蝶", entity.getApprovalStatus(), entity.getApprovalStatus(), null, kingdeeNo);
return kingdeeNo;
}
private InvoiceApplicationVO toListVO(InvoiceApplication entity) {
InvoiceApplicationVO vo = InvoiceApplicationWrapper.build().entityVO(entity);
vo.setSettlementNos(settlementRelationMapper.selectList(Wrappers.<InvoiceApplicationSettlement>lambdaQuery()
.eq(InvoiceApplicationSettlement::getInvoiceApplicationId, entity.getId())
.orderByAsc(InvoiceApplicationSettlement::getCreateTime)).stream()
.map(InvoiceApplicationSettlement::getFormalSettlementNo).collect(Collectors.joining(",")));
return vo;
}
private void validateRequest(InvoiceApplicationSaveRequest request) {
if (request.getSettlements() == null || request.getSettlements().isEmpty()) throw new ServiceException("请至少选择一张正式结算单");
if (request.getDetailIds() == null || request.getDetailIds().isEmpty()) throw new ServiceException("请至少选择一条开票明细");
if (!List.of("electronic_special", "electronic_normal").contains(request.getInvoiceType())) throw new ServiceException("请选择有效的发票类型");
if (request.getReceiverInvoiceInfoId() == null) throw new ServiceException("请选择受票方单位");
if (Func.isEmpty(request.getDepartmentEmails())) throw new ServiceException("请选择部门邮箱");
}
private BigDecimal validateSheets(List<InvoiceApplicationSaveRequest.SheetRow> sheets) {
if (sheets == null || sheets.isEmpty()) throw new ServiceException("请至少添加一张发票");
BigDecimal total = BigDecimal.ZERO;
for (InvoiceApplicationSaveRequest.SheetRow sheet : sheets) {
if (sheet.getLines() == null || sheet.getLines().isEmpty()) throw new ServiceException("每张发票至少需要一条商品行");
boolean containsFreight = sheet.getLines().stream().anyMatch(line -> "运费".equals(line.getGoodsName()));
boolean containsOther = sheet.getLines().stream().anyMatch(line -> !"运费".equals(line.getGoodsName()));
if (containsFreight && containsOther) throw new ServiceException("运费不能与其他费用合并开在同一张发票中");
for (InvoiceApplicationSaveRequest.LineRow line : sheet.getLines()) {
required(line.getGoodsCategory(), "商品和服务分类");
required(line.getGoodsName(), "货物或服务简称");
nonNegative(line.getQuantity(), "数量");
nonNegative(line.getUnitPriceNoTax(), "不含税单价");
BigDecimal amount = nonNegative(line.getAmountWithTax(), "含税金额");
BigDecimal taxRate = nonNegative(line.getTaxRate(), "税率");
if (taxRate.compareTo(BigDecimal.valueOf(100)) > 0) throw new ServiceException("税率必须在0-100之间");
line.setTaxAmount(calculateTax(amount, taxRate));
line.setRemark(limit(line.getRemark(), 200, "商品行备注"));
total = total.add(amount);
}
}
return total.setScale(2, RoundingMode.HALF_UP);
}
private void assertCompatible(List<FormalSettlement> settlements) {
if (settlements.isEmpty()) throw new ServiceException("请选择正式结算单");
FormalSettlement first = settlements.get(0);
if (settlements.stream().anyMatch(item -> !Objects.equals(first.getContractId(), item.getContractId())
|| !Objects.equals(first.getProjectId(), item.getProjectId())
|| !Objects.equals(first.getDeptId(), item.getDeptId())
|| !Objects.equals(first.getPayerName(), item.getPayerName())
|| !Objects.equals(first.getPayeeName(), item.getPayeeName()))) {
throw new ServiceException("合并开票的正式结算单必须属于同一合同、项目、组织及收付款方");
}
}
private FormalSettlement availableSettlement(Long id) {
if (id == null) throw new ServiceException("正式结算单不能为空");
FormalSettlement settlement = formalSettlementMapper.selectById(id);
if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) throw new ServiceException("正式结算单不存在");
if (!Objects.equals(settlement.getStatus(), 1) || !APPROVED.equals(settlement.getApprovalStatus())
|| !"receivable".equals(settlement.getSettlementType())) {
throw new ServiceException("只能选择审批通过、未作废的应收正式结算单");
}
return settlement;
}
private BigDecimal availableAmount(FormalSettlement settlement, Long excludeApplicationId) {
BigDecimal allocated = activeRelations(settlement.getId(), excludeApplicationId).stream()
.map(InvoiceApplicationSettlement::getAllocatedInvoiceAmount).map(this::money)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return money(settlement.getSettlementAmount()).subtract(allocated).max(BigDecimal.ZERO);
}
private List<InvoiceApplicationSettlement> activeRelations(Long settlementId, Long excludeApplicationId) {
List<InvoiceApplicationSettlement> relations = settlementRelationMapper.selectList(
Wrappers.<InvoiceApplicationSettlement>lambdaQuery()
.eq(InvoiceApplicationSettlement::getFormalSettlementId, settlementId)
.ne(excludeApplicationId != null, InvoiceApplicationSettlement::getInvoiceApplicationId, excludeApplicationId));
if (relations.isEmpty()) return List.of();
Map<Long, InvoiceApplication> applications = listByIds(relations.stream()
.map(InvoiceApplicationSettlement::getInvoiceApplicationId).distinct().toList()).stream()
.collect(Collectors.toMap(InvoiceApplication::getId, Function.identity()));
return relations.stream().filter(relation -> {
InvoiceApplication application = applications.get(relation.getInvoiceApplicationId());
return application != null && !VOIDED.equals(application.getApprovalStatus())
&& !Objects.equals(application.getIsDeleted(), 1);
}).toList();
}
private void saveRelations(Long applicationId, List<InvoiceApplicationSaveRequest.SettlementRow> rows,
Map<Long, FormalSettlement> settlementMap) {
for (InvoiceApplicationSaveRequest.SettlementRow row : rows) {
FormalSettlement source = settlementMap.get(row.getSettlementId());
InvoiceApplicationSettlement relation = new InvoiceApplicationSettlement();
relation.setInvoiceApplicationId(applicationId);
relation.setFormalSettlementId(source.getId());
relation.setFormalSettlementNo(source.getFormalSettlementNo());
relation.setSettlementAmount(source.getSettlementAmount());
relation.setAvailableInvoiceAmount(availableAmount(source, applicationId));
relation.setAllocatedInvoiceAmount(row.getAllocatedInvoiceAmount());
settlementRelationMapper.insert(relation);
}
}
private void saveSheets(Long applicationId, List<InvoiceApplicationSaveRequest.SheetRow> sheets) {
int sheetNo = 1;
for (InvoiceApplicationSaveRequest.SheetRow sheetRow : sheets) {
InvoiceApplicationSheet sheet = new InvoiceApplicationSheet();
sheet.setInvoiceApplicationId(applicationId);
sheet.setSheetNo(sheetNo++);
sheet.setInvoiceAmount(sheetRow.getLines().stream().map(InvoiceApplicationSaveRequest.LineRow::getAmountWithTax)
.map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add));
sheetMapper.insert(sheet);
int lineNo = 1;
for (InvoiceApplicationSaveRequest.LineRow lineRow : sheetRow.getLines()) {
InvoiceApplicationLine line = Objects.requireNonNull(BeanUtil.copyProperties(lineRow, InvoiceApplicationLine.class));
line.setId(null);
line.setInvoiceApplicationId(applicationId);
line.setInvoiceSheetId(sheet.getId());
line.setLineNo(lineNo++);
lineMapper.insert(line);
}
}
}
private void saveDetails(Long applicationId, List<Long> detailIds, Set<Long> settlementIds) {
List<FormalSettlementDetail> details = formalSettlementDetailMapper.selectBatchIds(detailIds.stream().distinct().toList());
if (details.size() != detailIds.stream().distinct().count()
|| details.stream().anyMatch(detail -> !settlementIds.contains(detail.getFormalSettlementId()))) {
throw new ServiceException("开票明细必须来自已选择的正式结算单");
}
int lineNo = 1;
for (FormalSettlementDetail source : details) {
InvoiceApplicationDetail detail = Objects.requireNonNull(BeanUtil.copyProperties(source, InvoiceApplicationDetail.class));
detail.setId(null);
detail.setInvoiceApplicationId(applicationId);
detail.setFormalSettlementDetailId(source.getId());
detail.setLineNo(lineNo++);
applicationDetailMapper.insert(detail);
}
}
private void deleteChildren(Long applicationId) {
List<Long> sheetIds = sheetMapper.selectList(Wrappers.<InvoiceApplicationSheet>lambdaQuery()
.eq(InvoiceApplicationSheet::getInvoiceApplicationId, applicationId)).stream()
.map(InvoiceApplicationSheet::getId).toList();
if (!sheetIds.isEmpty()) lineMapper.delete(Wrappers.<InvoiceApplicationLine>lambdaQuery()
.in(InvoiceApplicationLine::getInvoiceSheetId, sheetIds));
sheetMapper.delete(Wrappers.<InvoiceApplicationSheet>lambdaQuery()
.eq(InvoiceApplicationSheet::getInvoiceApplicationId, applicationId));
applicationDetailMapper.delete(Wrappers.<InvoiceApplicationDetail>lambdaQuery()
.eq(InvoiceApplicationDetail::getInvoiceApplicationId, applicationId));
settlementRelationMapper.delete(Wrappers.<InvoiceApplicationSettlement>lambdaQuery()
.eq(InvoiceApplicationSettlement::getInvoiceApplicationId, applicationId));
}
private void refreshSettlementInvoiceStatus(Long settlementId) {
FormalSettlement settlement = formalSettlementMapper.selectById(settlementId);
if (settlement == null) return;
BigDecimal allocated = activeRelations(settlementId, null).stream()
.map(InvoiceApplicationSettlement::getAllocatedInvoiceAmount).map(this::money)
.reduce(BigDecimal.ZERO, BigDecimal::add);
String status = allocated.compareTo(BigDecimal.ZERO) <= 0 ? "unreceived"
: allocated.compareTo(money(settlement.getSettlementAmount())) >= 0 ? "completed" : "partial";
settlement.setInvoiceStatus(status);
formalSettlementMapper.updateById(settlement);
}
private List<Long> relationSettlementIds(Long applicationId) {
return settlementRelationMapper.selectList(Wrappers.<InvoiceApplicationSettlement>lambdaQuery()
.eq(InvoiceApplicationSettlement::getInvoiceApplicationId, applicationId)).stream()
.map(InvoiceApplicationSettlement::getFormalSettlementId).distinct().toList();
}
private CustomerArchive findCustomer(String name) {
if (Func.isEmpty(name)) return null;
return customerArchiveMapper.selectOne(Wrappers.<CustomerArchive>lambdaQuery()
.and(wrapper -> wrapper.eq(CustomerArchive::getFullName, name).or().eq(CustomerArchive::getShortName, name))
.eq(CustomerArchive::getStatus, 1)
.eq(CustomerArchive::getIsDeleted, 0).last("limit 1"));
}
private void changeStatus(Long id, String from, String to, String actionType, String node, String reason) {
InvoiceApplication entity = existing(id);
if (!from.equals(entity.getApprovalStatus())) throw new ServiceException("当前状态不允许此操作");
entity.setApprovalStatus(to);
entity.setCurrentNode(node);
entity.setCurrentProcessor(AuthUtil.getUserName());
updateById(entity);
record(id, actionType, node, from, to, reason, entity.getKingdeeBillNo());
}
private void record(Long applicationId, String actionType, String actionName, String fromStatus,
String toStatus, String reason, String kingdeeBillNo) {
InvoiceApplicationRecord record = new InvoiceApplicationRecord();
record.setInvoiceApplicationId(applicationId);
record.setActionType(actionType);
record.setActionName(actionName);
record.setFromStatus(fromStatus);
record.setToStatus(toStatus);
record.setOperatorName(AuthUtil.getUserName());
record.setReason(reason);
record.setKingdeeBillNo(kingdeeBillNo);
recordMapper.insert(record);
}
private InvoiceApplication existing(Long id) {
InvoiceApplication entity = getById(id);
if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) throw new ServiceException("开票申请不存在");
return entity;
}
private InvoiceApplication editable(Long id) {
InvoiceApplication entity = existing(id);
if (!List.of(DRAFT, RETURNED).contains(entity.getApprovalStatus())) throw new ServiceException("当前状态不可编辑");
return entity;
}
private List<Long> distinctIds(String ids) {
return ids == null ? List.of() : Func.toLongList(ids).stream().distinct().toList();
}
private String nextNo() {
String prefix = "KP-" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
return prefix + String.format("%05d", count(Wrappers.<InvoiceApplication>lambdaQuery()
.likeRight(InvoiceApplication::getApplicationNo, prefix)) + 1);
}
private String normalizeEmails(String value) {
List<String> emails = value == null ? List.of() : List.of(value.split("[;,,;]"));
List<String> normalized = emails.stream().map(String::trim).filter(item -> !item.isEmpty()).distinct().toList();
if (normalized.isEmpty() || normalized.size() > 3) throw new ServiceException("部门邮箱必填且最多选择3个");
normalized.forEach(email -> validateEmail(email, "部门邮箱"));
return String.join(";", normalized);
}
private String validateEmail(String value, String name) {
String result = limit(value, 100, name);
if (Func.isNotEmpty(result) && !result.matches("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")) {
throw new ServiceException(name + "格式不正确");
}
return result;
}
private String validateReceiverEmails(String value) {
if (Func.isEmpty(value)) return value;
List<String> emails = List.of(value.split("[;,,;]")).stream()
.map(String::trim).filter(item -> !item.isEmpty()).distinct().toList();
if (emails.size() > 3) throw new ServiceException("邮箱最多填写3个");
emails.forEach(email -> validateEmail(email, "邮箱"));
return limit(String.join(";", emails), 100, "邮箱");
}
private String validatePhone(String value) {
if (Func.isNotEmpty(value) && !value.matches("^\\d{11}$")) throw new ServiceException("联系电话必须为11位数字");
return value;
}
private BigDecimal calculateTax(BigDecimal amount, BigDecimal rate) {
if (rate.compareTo(BigDecimal.ZERO) == 0) return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
return amount.subtract(amount.divide(BigDecimal.ONE.add(rate.divide(BigDecimal.valueOf(100), 8,
RoundingMode.HALF_UP)), 8, RoundingMode.HALF_UP)).setScale(2, RoundingMode.HALF_UP);
}
private BigDecimal nonNegative(BigDecimal value, String name) {
if (value == null || value.compareTo(BigDecimal.ZERO) < 0) throw new ServiceException(name + "不能小于0");
return value;
}
private BigDecimal money(BigDecimal value) {
return value == null ? BigDecimal.ZERO : value;
}
private String required(String value, String name) {
if (Func.isEmpty(value)) throw new ServiceException(name + "不能为空");
return value;
}
private String limit(String value, int length, String name) {
if (value != null && value.length() > length) throw new ServiceException(name + "不能超过" + length + "个字符");
return value;
}
}
@@ -0,0 +1,684 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.mapper.CustomerArchiveMapper;
import org.springblade.transport.mapper.CustomerContactMapper;
import org.springblade.transport.mapper.FormalSettlementMapper;
import org.springblade.transport.mapper.InvoiceReceiptMapper;
import org.springblade.transport.mapper.InvoiceReceiptRecordMapper;
import org.springblade.transport.mapper.InvoiceReceiptSettlementMapper;
import org.springblade.transport.mapper.KingdeeInvoicePoolMapper;
import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest;
import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
import org.springblade.transport.pojo.entity.CustomerArchive;
import org.springblade.transport.pojo.entity.CustomerContact;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.InvoiceReceipt;
import org.springblade.transport.pojo.entity.InvoiceReceiptRecord;
import org.springblade.transport.pojo.entity.InvoiceReceiptSettlement;
import org.springblade.transport.pojo.entity.KingdeeInvoicePool;
import org.springblade.transport.pojo.vo.InvoiceReceiptVO;
import org.springblade.transport.service.IInvoiceReceiptService;
import org.springblade.transport.wrapper.InvoiceReceiptWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 收票登记服务实现类
*
* @author Chill
*/
@Service
@RequiredArgsConstructor
public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMapper, InvoiceReceipt>
implements IInvoiceReceiptService {
private static final String DRAFT = "draft";
private static final String REVIEWING = "reviewing";
private static final String APPROVED = "approved";
private static final String RETURNED = "returned";
private static final String VOIDED = "voided";
private final KingdeeInvoicePoolMapper invoicePoolMapper;
private final InvoiceReceiptSettlementMapper settlementRelationMapper;
private final InvoiceReceiptRecordMapper recordMapper;
private final FormalSettlementMapper formalSettlementMapper;
private final CustomerArchiveMapper customerArchiveMapper;
private final CustomerContactMapper customerContactMapper;
@Override
public IPage<InvoiceReceiptVO> selectPage(IPage<InvoiceReceipt> page, InvoiceReceiptVO query) {
LambdaQueryWrapper<InvoiceReceipt> wrapper = Wrappers.<InvoiceReceipt>lambdaQuery()
.like(Func.isNotEmpty(query.getInvoiceNo()), InvoiceReceipt::getInvoiceNo, query.getInvoiceNo())
.eq(query.getInvoiceDate() != null, InvoiceReceipt::getInvoiceDate, query.getInvoiceDate())
.like(Func.isNotEmpty(query.getProjectName()), InvoiceReceipt::getProjectName, query.getProjectName())
.like(Func.isNotEmpty(query.getDeptName()), InvoiceReceipt::getDeptName, query.getDeptName())
.eq(Func.isNotEmpty(query.getApprovalStatus()), InvoiceReceipt::getApprovalStatus,
query.getApprovalStatus())
.eq(Func.isNotEmpty(query.getKingdeeStatus()), InvoiceReceipt::getKingdeeStatus,
query.getKingdeeStatus())
.orderByDesc(InvoiceReceipt::getCreateTime);
return page(page, wrapper).convert(this::toListVO);
}
@Override
public InvoiceReceiptVO detail(Long id) {
InvoiceReceipt entity = existing(id);
InvoiceReceiptVO vo = toListVO(entity);
List<InvoiceReceiptSettlement> settlements = settlementRelationMapper.selectList(
Wrappers.<InvoiceReceiptSettlement>lambdaQuery()
.eq(InvoiceReceiptSettlement::getInvoiceReceiptId, id)
.orderByAsc(InvoiceReceiptSettlement::getCreateTime));
settlements.forEach(item -> {
FormalSettlement source = formalSettlementMapper.selectById(item.getFormalSettlementId());
if (source != null) {
item.setSettlementAmount(money(source.getSettlementAmount()));
}
item.setReceivedInvoiceAmount(receivedAmount(item.getFormalSettlementId(), id));
});
vo.setSettlements(settlements);
vo.setRecords(recordMapper.selectList(Wrappers.<InvoiceReceiptRecord>lambdaQuery()
.eq(InvoiceReceiptRecord::getInvoiceReceiptId, id)
.orderByAsc(InvoiceReceiptRecord::getCreateTime)));
return vo;
}
@Override
public List<KingdeeInvoicePool> invoicePool(String keyword) {
return invoicePoolMapper.selectList(Wrappers.<KingdeeInvoicePool>lambdaQuery()
.eq(KingdeeInvoicePool::getStatus, 1)
.and(Func.isNotEmpty(keyword), wrapper -> wrapper.like(KingdeeInvoicePool::getInvoiceNo, keyword)
.or().like(KingdeeInvoicePool::getIssuerName, keyword)
.or().like(KingdeeInvoicePool::getReceiverName, keyword))
.orderByDesc(KingdeeInvoicePool::getSourceUpdatedTime)
.orderByDesc(KingdeeInvoicePool::getCreateTime)
.last("limit 200"));
}
@Override
public List<Map<String, Object>> settlementCandidates(String keyword, Long receiptId) {
List<FormalSettlement> settlements = formalSettlementMapper.selectList(
Wrappers.<FormalSettlement>lambdaQuery()
.eq(FormalSettlement::getSettlementType, "payable")
.eq(FormalSettlement::getApprovalStatus, APPROVED)
.eq(FormalSettlement::getStatus, 1)
.and(Func.isNotEmpty(keyword), wrapper -> wrapper
.like(FormalSettlement::getFormalSettlementNo, keyword)
.or().like(FormalSettlement::getProjectName, keyword)
.or().like(FormalSettlement::getContractName, keyword))
.orderByDesc(FormalSettlement::getCreateTime)
.last("limit 200"));
return settlements.stream().map(settlement -> {
BigDecimal received = receivedAmount(settlement.getId(), receiptId);
BigDecimal remaining = money(settlement.getSettlementAmount()).subtract(received).max(BigDecimal.ZERO);
Map<String, Object> row = new LinkedHashMap<>();
row.put("id", settlement.getId());
row.put("formalSettlementNo", settlement.getFormalSettlementNo());
row.put("projectId", settlement.getProjectId());
row.put("projectName", settlement.getProjectName());
row.put("deptId", settlement.getDeptId());
row.put("deptName", settlement.getDeptName());
row.put("contractId", settlement.getContractId());
row.put("contractNo", settlement.getContractNo());
row.put("contractName", settlement.getContractName());
row.put("payerName", settlement.getPayerName());
row.put("payeeName", settlement.getPayeeName());
row.put("settlementAmount", money(settlement.getSettlementAmount()));
row.put("receivedInvoiceAmount", received);
row.put("remainingInvoiceAmount", remaining);
return row;
}).filter(row -> ((BigDecimal) row.get("remainingInvoiceAmount")).compareTo(BigDecimal.ZERO) > 0)
.toList();
}
@Override
public Map<String, Object> referenceInformation(String settlementIds) {
List<FormalSettlement> settlements = distinctIds(settlementIds).stream()
.map(this::availableSettlement)
.toList();
assertCompatible(settlements);
FormalSettlement first = settlements.get(0);
CustomerArchive customer = findCustomer(first.getPayeeName());
List<String> customerEmails = customer == null ? List.of() : customerContactMapper.selectList(
Wrappers.<CustomerContact>lambdaQuery()
.eq(CustomerContact::getCustomerId, customer.getId())
.eq(CustomerContact::getStatus, 1)
.orderByDesc(CustomerContact::getIsDefault)
.orderByAsc(CustomerContact::getCreateTime)).stream()
.map(CustomerContact::getEmail)
.filter(item -> Func.isNotEmpty(item))
.map(String::trim)
.distinct()
.toList();
Map<String, Object> result = new LinkedHashMap<>();
result.put("projectId", first.getProjectId());
result.put("projectName", first.getProjectName());
result.put("deptId", first.getDeptId());
result.put("deptName", first.getDeptName());
result.put("payerName", first.getPayerName());
result.put("payeeName", first.getPayeeName());
result.put("customerEmails", customerEmails);
result.put("departmentEmails", List.of());
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long saveDraft(InvoiceReceiptSaveRequest request) {
validateRequest(request);
boolean creating = request.getId() == null;
InvoiceReceipt entity = creating ? new InvoiceReceipt() : editable(request.getId());
KingdeeInvoicePool invoice = lockedInvoice(request.getKingdeeInvoicePoolId());
assertInvoiceUnused(invoice, entity.getId());
Map<Long, InvoiceReceiptSaveRequest.SettlementRow> requestedRows = distinctSettlementRows(
request.getSettlements());
Map<Long, FormalSettlement> settlementMap = lockSettlements(requestedRows.keySet().stream().sorted().toList());
List<FormalSettlement> settlements = requestedRows.keySet().stream().map(settlementMap::get).toList();
assertCompatible(settlements);
assertInvoiceParties(invoice, settlements.get(0));
BigDecimal invoiceAmount = positive(invoice.getInvoiceAmount(), "开票金额");
BigDecimal allocatedTotal = BigDecimal.ZERO;
for (Map.Entry<Long, InvoiceReceiptSaveRequest.SettlementRow> entry : requestedRows.entrySet()) {
FormalSettlement settlement = settlementMap.get(entry.getKey());
BigDecimal allocated = nonNegative(entry.getValue().getAllocatedInvoiceAmount(), "分摊发票金额");
BigDecimal received = receivedAmount(settlement.getId(), entity.getId());
if (received.add(allocated).compareTo(money(settlement.getSettlementAmount())) > 0) {
throw new ServiceException("结算单" + settlement.getFormalSettlementNo()
+ "的累计收票金额不能超过结算总应付含税金额");
}
allocatedTotal = allocatedTotal.add(allocated);
}
if (allocatedTotal.compareTo(invoiceAmount) != 0) {
throw new ServiceException("分摊发票金额总和必须等于发票开票金额");
}
if (creating) {
entity.setApprovalStatus(DRAFT);
entity.setCurrentNode("草稿");
entity.setCurrentProcessor(AuthUtil.getUserName());
}
copyInvoiceInformation(entity, invoice);
FormalSettlement first = settlements.get(0);
entity.setProjectId(first.getProjectId());
entity.setProjectName(first.getProjectName());
entity.setDeptId(first.getDeptId());
entity.setDeptName(first.getDeptName());
entity.setPayerName(first.getPayerName());
entity.setPayeeName(first.getPayeeName());
entity.setPhone(limit(Func.isNotEmpty(request.getPhone()) ? request.getPhone() : invoice.getPhone(),
50, "电话"));
entity.setCustomerEmails(normalizeEmails(Func.isNotEmpty(request.getCustomerEmails())
? request.getCustomerEmails() : invoice.getCustomerEmails(), "客户邮箱"));
entity.setDepartmentEmails(normalizeEmails(Func.isNotEmpty(request.getDepartmentEmails())
? request.getDepartmentEmails() : invoice.getDepartmentEmails(), "部门邮箱"));
entity.setAttachmentsJson(request.getAttachmentsJson());
entity.setRemark(limit(request.getRemark(), 200, "备注"));
saveOrUpdate(entity);
settlementRelationMapper.delete(Wrappers.<InvoiceReceiptSettlement>lambdaQuery()
.eq(InvoiceReceiptSettlement::getInvoiceReceiptId, entity.getId()));
saveRelations(entity.getId(), requestedRows, settlementMap);
record(entity.getId(), creating ? "create" : "save", creating ? "创建草稿" : "保存草稿",
entity.getApprovalStatus(), entity.getApprovalStatus(), null, entity.getKingdeeBillNo());
return entity.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void removeDraft(Long id) {
InvoiceReceipt entity = existing(id);
if (!DRAFT.equals(entity.getApprovalStatus())) {
throw new ServiceException("仅草稿状态的收票登记允许删除");
}
settlementRelationMapper.delete(Wrappers.<InvoiceReceiptSettlement>lambdaQuery()
.eq(InvoiceReceiptSettlement::getInvoiceReceiptId, id));
recordMapper.delete(Wrappers.<InvoiceReceiptRecord>lambdaQuery()
.eq(InvoiceReceiptRecord::getInvoiceReceiptId, id));
removeById(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void submit(InvoiceReceiptStatusRequest request) {
InvoiceReceipt entity = editable(requiredId(request));
validateStoredAllocation(entity);
String fromStatus = entity.getApprovalStatus();
entity.setApprovalStatus(REVIEWING);
entity.setCurrentNode("收票审核");
entity.setCurrentProcessor(null);
updateById(entity);
record(entity.getId(), "submit", "提交审批", fromStatus, REVIEWING, null, entity.getKingdeeBillNo());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void approve(InvoiceReceiptStatusRequest request) {
InvoiceReceipt entity = existing(requiredId(request));
if (!REVIEWING.equals(entity.getApprovalStatus())) {
throw new ServiceException("当前状态不允许审批通过");
}
validateStoredAllocation(entity);
changeStatus(entity, APPROVED, "approve", "审批通过", null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void returnBill(InvoiceReceiptStatusRequest request) {
InvoiceReceipt entity = existing(requiredId(request));
if (!REVIEWING.equals(entity.getApprovalStatus())) {
throw new ServiceException("当前状态不允许驳回");
}
changeStatus(entity, RETURNED, "return", "已驳回",
limit(required(request.getReason(), "驳回原因"), 200, "驳回原因"));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void voidBill(InvoiceReceiptStatusRequest request) {
InvoiceReceipt entity = existing(requiredId(request));
if (!APPROVED.equals(entity.getApprovalStatus())) {
throw new ServiceException("仅审批通过的收票登记允许作废");
}
String reason = limit(required(request.getReason(), "作废原因"), 200, "作废原因");
entity.setVoidReason(reason);
changeStatus(entity, VOIDED, "void", "已作废", reason);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String syncKingdee(Long id) {
InvoiceReceipt entity = existing(id);
if (!APPROVED.equals(entity.getApprovalStatus())) {
throw new ServiceException("仅审批通过的收票登记允许同步金蝶状态");
}
KingdeeInvoicePool invoice = invoicePoolMapper.selectById(entity.getKingdeeInvoicePoolId());
if (invoice == null || Objects.equals(invoice.getIsDeleted(), 1)) {
throw new ServiceException("金蝶票据池发票不存在");
}
entity.setKingdeeBillNo(invoice.getKingdeeBillNo());
entity.setKingdeeStatus(normalizeKingdeeStatus(invoice.getKingdeeStatus()));
updateById(entity);
record(entity.getId(), "sync", "同步金蝶状态", entity.getApprovalStatus(),
entity.getApprovalStatus(), null, entity.getKingdeeBillNo());
return entity.getKingdeeBillNo();
}
private InvoiceReceiptVO toListVO(InvoiceReceipt entity) {
InvoiceReceiptVO vo = InvoiceReceiptWrapper.build().entityVO(entity);
vo.setSettlementNos(settlementRelationMapper.selectList(
Wrappers.<InvoiceReceiptSettlement>lambdaQuery()
.eq(InvoiceReceiptSettlement::getInvoiceReceiptId, entity.getId())
.orderByAsc(InvoiceReceiptSettlement::getCreateTime)).stream()
.map(InvoiceReceiptSettlement::getFormalSettlementNo)
.collect(Collectors.joining(",")));
return vo;
}
private void validateRequest(InvoiceReceiptSaveRequest request) {
if (request == null) {
throw new ServiceException("收票登记数据不能为空");
}
if (request.getKingdeeInvoicePoolId() == null) {
throw new ServiceException("请选择金蝶票据池发票");
}
if (request.getSettlements() == null || request.getSettlements().isEmpty()) {
throw new ServiceException("请至少选择一张应付正式结算单");
}
}
private Map<Long, InvoiceReceiptSaveRequest.SettlementRow> distinctSettlementRows(
List<InvoiceReceiptSaveRequest.SettlementRow> rows) {
Map<Long, InvoiceReceiptSaveRequest.SettlementRow> result = new LinkedHashMap<>();
for (InvoiceReceiptSaveRequest.SettlementRow row : rows) {
if (row == null || row.getSettlementId() == null) {
throw new ServiceException("正式结算单不能为空");
}
if (result.putIfAbsent(row.getSettlementId(), row) != null) {
throw new ServiceException("正式结算单不能重复选择");
}
}
return result;
}
private Map<Long, FormalSettlement> lockSettlements(List<Long> settlementIds) {
Map<Long, FormalSettlement> result = new LinkedHashMap<>();
for (Long settlementId : settlementIds) {
FormalSettlement settlement = formalSettlementMapper.selectOne(
Wrappers.<FormalSettlement>lambdaQuery()
.eq(FormalSettlement::getId, settlementId)
.last("FOR UPDATE"));
result.put(settlementId, validateSettlement(settlement));
}
return result;
}
private FormalSettlement availableSettlement(Long id) {
if (id == null) {
throw new ServiceException("正式结算单不能为空");
}
return validateSettlement(formalSettlementMapper.selectById(id));
}
private FormalSettlement validateSettlement(FormalSettlement settlement) {
if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) {
throw new ServiceException("正式结算单不存在");
}
if (!Objects.equals(settlement.getStatus(), 1)
|| !APPROVED.equals(settlement.getApprovalStatus())
|| !"payable".equals(settlement.getSettlementType())) {
throw new ServiceException("只能选择审批通过、未作废的应付正式结算单");
}
return settlement;
}
private KingdeeInvoicePool lockedInvoice(Long id) {
KingdeeInvoicePool invoice = invoicePoolMapper.selectOne(Wrappers.<KingdeeInvoicePool>lambdaQuery()
.eq(KingdeeInvoicePool::getId, id)
.last("FOR UPDATE"));
if (invoice == null || Objects.equals(invoice.getIsDeleted(), 1)
|| !Objects.equals(invoice.getStatus(), 1)) {
throw new ServiceException("金蝶票据池发票不存在或已失效");
}
required(invoice.getInvoiceNo(), "发票号码");
return invoice;
}
private void assertInvoiceUnused(KingdeeInvoicePool invoice, Long excludeReceiptId) {
long count = count(Wrappers.<InvoiceReceipt>lambdaQuery()
.and(wrapper -> wrapper.eq(InvoiceReceipt::getKingdeeInvoicePoolId, invoice.getId())
.or().eq(InvoiceReceipt::getInvoiceNo, invoice.getInvoiceNo()))
.ne(excludeReceiptId != null, InvoiceReceipt::getId, excludeReceiptId));
if (count > 0) {
throw new ServiceException("发票" + invoice.getInvoiceNo() + "已登记,不能重复收票");
}
}
private void assertCompatible(List<FormalSettlement> settlements) {
if (settlements.isEmpty()) {
throw new ServiceException("请选择应付正式结算单");
}
FormalSettlement first = settlements.get(0);
if (settlements.stream().anyMatch(item -> !Objects.equals(first.getProjectId(), item.getProjectId())
|| !Objects.equals(first.getDeptId(), item.getDeptId())
|| !Objects.equals(first.getPayerName(), item.getPayerName())
|| !Objects.equals(first.getPayeeName(), item.getPayeeName()))) {
throw new ServiceException("关联结算单必须属于同一项目、组织及收付款方");
}
}
private void assertInvoiceParties(KingdeeInvoicePool invoice, FormalSettlement settlement) {
if (!sameName(invoice.getReceiverName(), settlement.getPayerName())
|| !sameName(invoice.getIssuerName(), settlement.getPayeeName())) {
throw new ServiceException("金蝶发票的开票单位、受票单位与结算单收付款方不一致");
}
}
private boolean sameName(String first, String second) {
return Func.isNotEmpty(first) && Func.isNotEmpty(second) && first.trim().equals(second.trim());
}
private BigDecimal receivedAmount(Long settlementId, Long excludeReceiptId) {
return activeRelations(settlementId, excludeReceiptId).stream()
.map(InvoiceReceiptSettlement::getAllocatedInvoiceAmount)
.map(this::money)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private List<InvoiceReceiptSettlement> activeRelations(Long settlementId, Long excludeReceiptId) {
List<InvoiceReceiptSettlement> relations = settlementRelationMapper.selectList(
Wrappers.<InvoiceReceiptSettlement>lambdaQuery()
.eq(InvoiceReceiptSettlement::getFormalSettlementId, settlementId)
.ne(excludeReceiptId != null, InvoiceReceiptSettlement::getInvoiceReceiptId, excludeReceiptId));
if (relations.isEmpty()) {
return List.of();
}
Map<Long, InvoiceReceipt> receipts = listByIds(relations.stream()
.map(InvoiceReceiptSettlement::getInvoiceReceiptId)
.distinct()
.toList()).stream().collect(Collectors.toMap(InvoiceReceipt::getId, Function.identity()));
return relations.stream().filter(relation -> {
InvoiceReceipt receipt = receipts.get(relation.getInvoiceReceiptId());
return receipt != null && !VOIDED.equals(receipt.getApprovalStatus())
&& !Objects.equals(receipt.getIsDeleted(), 1);
}).toList();
}
private void saveRelations(Long receiptId,
Map<Long, InvoiceReceiptSaveRequest.SettlementRow> rows,
Map<Long, FormalSettlement> settlementMap) {
for (Map.Entry<Long, InvoiceReceiptSaveRequest.SettlementRow> entry : rows.entrySet()) {
FormalSettlement source = settlementMap.get(entry.getKey());
InvoiceReceiptSettlement relation = new InvoiceReceiptSettlement();
relation.setInvoiceReceiptId(receiptId);
relation.setFormalSettlementId(source.getId());
relation.setFormalSettlementNo(source.getFormalSettlementNo());
relation.setSettlementAmount(money(source.getSettlementAmount()));
relation.setReceivedInvoiceAmount(receivedAmount(source.getId(), receiptId));
relation.setAllocatedInvoiceAmount(entry.getValue().getAllocatedInvoiceAmount());
settlementRelationMapper.insert(relation);
}
}
private void validateStoredAllocation(InvoiceReceipt entity) {
KingdeeInvoicePool invoice = lockedInvoice(entity.getKingdeeInvoicePoolId());
assertInvoiceUnused(invoice, entity.getId());
List<InvoiceReceiptSettlement> relations = settlementRelationMapper.selectList(
Wrappers.<InvoiceReceiptSettlement>lambdaQuery()
.eq(InvoiceReceiptSettlement::getInvoiceReceiptId, entity.getId())
.orderByAsc(InvoiceReceiptSettlement::getCreateTime));
if (relations.isEmpty()) {
throw new ServiceException("请至少选择一张应付正式结算单");
}
Map<Long, FormalSettlement> settlementMap = lockSettlements(relations.stream()
.map(InvoiceReceiptSettlement::getFormalSettlementId)
.distinct()
.sorted()
.toList());
List<FormalSettlement> settlements = relations.stream()
.map(item -> settlementMap.get(item.getFormalSettlementId()))
.toList();
assertCompatible(settlements);
assertInvoiceParties(invoice, settlements.get(0));
BigDecimal allocatedTotal = BigDecimal.ZERO;
for (InvoiceReceiptSettlement relation : relations) {
FormalSettlement settlement = settlementMap.get(relation.getFormalSettlementId());
BigDecimal allocated = nonNegative(relation.getAllocatedInvoiceAmount(), "分摊发票金额");
BigDecimal received = receivedAmount(settlement.getId(), entity.getId());
if (received.add(allocated).compareTo(money(settlement.getSettlementAmount())) > 0) {
throw new ServiceException("结算单" + settlement.getFormalSettlementNo()
+ "的累计收票金额不能超过结算总应付含税金额");
}
allocatedTotal = allocatedTotal.add(allocated);
}
if (allocatedTotal.compareTo(positive(invoice.getInvoiceAmount(), "开票金额")) != 0) {
throw new ServiceException("分摊发票金额总和必须等于发票开票金额");
}
}
private void copyInvoiceInformation(InvoiceReceipt target, KingdeeInvoicePool source) {
target.setKingdeeInvoicePoolId(source.getId());
target.setInvoiceNo(limit(source.getInvoiceNo(), 32, "发票号码"));
target.setInvoiceDate(source.getInvoiceDate());
target.setInvoiceType(source.getInvoiceType());
target.setTaxRate(nonNegative(source.getTaxRate(), "税率"));
target.setInvoiceAmount(positive(source.getInvoiceAmount(), "开票金额"));
target.setTaxAmount(nonNegative(source.getTaxAmount(), "税额"));
target.setReceiverName(limit(required(source.getReceiverName(), "受票单位"), 100, "受票单位"));
target.setIssuerName(limit(required(source.getIssuerName(), "开票单位"), 100, "开票单位"));
target.setBankName(source.getBankName());
target.setBankAccount(source.getBankAccount());
target.setIssuingBank(source.getIssuingBank());
target.setKingdeeBillNo(source.getKingdeeBillNo());
target.setKingdeeStatus(normalizeKingdeeStatus(source.getKingdeeStatus()));
}
private String normalizeKingdeeStatus(String status) {
if ("synced".equals(status) || "failed".equals(status)) {
return status;
}
return "unsynced";
}
private void changeStatus(InvoiceReceipt entity, String toStatus, String actionType,
String actionName, String reason) {
String fromStatus = entity.getApprovalStatus();
entity.setApprovalStatus(toStatus);
entity.setCurrentNode(actionName);
entity.setCurrentProcessor(AuthUtil.getUserName());
updateById(entity);
record(entity.getId(), actionType, actionName, fromStatus, toStatus, reason, entity.getKingdeeBillNo());
}
private void record(Long receiptId, String actionType, String actionName, String fromStatus,
String toStatus, String reason, String kingdeeBillNo) {
InvoiceReceiptRecord record = new InvoiceReceiptRecord();
record.setInvoiceReceiptId(receiptId);
record.setActionType(actionType);
record.setActionName(actionName);
record.setFromStatus(fromStatus);
record.setToStatus(toStatus);
record.setOperatorName(AuthUtil.getUserName());
record.setReason(reason);
record.setKingdeeBillNo(kingdeeBillNo);
recordMapper.insert(record);
}
private InvoiceReceipt existing(Long id) {
if (id == null) {
throw new ServiceException("收票登记ID不能为空");
}
InvoiceReceipt entity = getById(id);
if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) {
throw new ServiceException("收票登记不存在");
}
return entity;
}
private InvoiceReceipt editable(Long id) {
InvoiceReceipt entity = existing(id);
if (!List.of(DRAFT, RETURNED).contains(entity.getApprovalStatus())) {
throw new ServiceException("当前状态不可编辑");
}
return entity;
}
private Long requiredId(InvoiceReceiptStatusRequest request) {
if (request == null || request.getId() == null) {
throw new ServiceException("收票登记ID不能为空");
}
return request.getId();
}
private List<Long> distinctIds(String ids) {
return ids == null ? List.of() : Func.toLongList(ids).stream().distinct().toList();
}
private CustomerArchive findCustomer(String name) {
if (Func.isEmpty(name)) {
return null;
}
return customerArchiveMapper.selectOne(Wrappers.<CustomerArchive>lambdaQuery()
.and(wrapper -> wrapper.eq(CustomerArchive::getFullName, name)
.or().eq(CustomerArchive::getShortName, name))
.eq(CustomerArchive::getStatus, 1)
.last("limit 1"));
}
private String normalizeEmails(String value, String name) {
if (Func.isEmpty(value)) {
return "";
}
List<String> emails = List.of(value.split("[;,,;]")).stream()
.map(String::trim)
.filter(Func::isNotEmpty)
.distinct()
.toList();
if (emails.size() > 3) {
throw new ServiceException(name + "最多填写3个");
}
emails.forEach(email -> {
if (email.length() > 100 || !email.matches("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")) {
throw new ServiceException(name + "格式不正确");
}
});
return limit(String.join(";", emails), 200, name);
}
private String required(String value, String name) {
if (Func.isEmpty(value)) {
throw new ServiceException(name + "不能为空");
}
return value.trim();
}
private String limit(String value, int length, String name) {
if (value != null && value.length() > length) {
throw new ServiceException(name + "不能超过" + length + "个字符");
}
return value;
}
private BigDecimal nonNegative(BigDecimal value, String name) {
if (value == null) {
throw new ServiceException(name + "不能为空");
}
if (value.compareTo(BigDecimal.ZERO) < 0) {
throw new ServiceException(name + "不能小于0");
}
return value;
}
private BigDecimal positive(BigDecimal value, String name) {
BigDecimal result = nonNegative(value, name);
if (result.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException(name + "必须大于0");
}
return result;
}
private BigDecimal money(BigDecimal value) {
return value == null ? BigDecimal.ZERO : value;
}
}
@@ -0,0 +1,392 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
* Copyright of this software remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.mapper.FormalSettlementMapper;
import org.springblade.transport.mapper.BillLedgerMapper;
import org.springblade.transport.mapper.BillLedgerUsageMapper;
import org.springblade.transport.mapper.PreSettlementMapper;
import org.springblade.transport.mapper.ProjectApplyMapper;
import org.springblade.transport.mapper.CustomerArchiveMapper;
import org.springblade.transport.mapper.PaymentApplicationInvoiceMapper;
import org.springblade.transport.mapper.PaymentApplicationMapper;
import org.springblade.transport.mapper.PaymentApplicationRecordMapper;
import org.springblade.transport.pojo.dto.PaymentApplicationInvoiceRequest;
import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest;
import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.BillLedger;
import org.springblade.transport.pojo.entity.BillLedgerUsage;
import org.springblade.transport.pojo.entity.PreSettlement;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.pojo.entity.CustomerArchive;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.pojo.entity.PaymentApplicationInvoice;
import org.springblade.transport.pojo.vo.PaymentApplicationVO;
import org.springblade.transport.service.IPaymentApplicationService;
import org.springblade.transport.wrapper.PaymentApplicationWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Objects;
/** 付款申请服务实现。 @author Chill */
@Service
@RequiredArgsConstructor
public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplicationMapper, PaymentApplication>
implements IPaymentApplicationService {
private static final String DRAFT = "draft";
private static final String REVIEWING = "reviewing";
private static final String APPROVED = "approved";
private static final String RETURNED = "returned";
private static final String VOIDED = "voided";
private final PaymentApplicationInvoiceMapper invoiceMapper;
private final PaymentApplicationRecordMapper recordMapper;
private final PreSettlementMapper preSettlementMapper;
private final FormalSettlementMapper formalSettlementMapper;
private final ProjectApplyMapper projectApplyMapper;
private final CustomerArchiveMapper customerArchiveMapper;
private final BillLedgerMapper billLedgerMapper;
private final BillLedgerUsageMapper billLedgerUsageMapper;
@Override
public IPage<PaymentApplicationVO> selectPage(IPage<PaymentApplication> page, PaymentApplicationVO query) {
LambdaQueryWrapper<PaymentApplication> wrapper = Wrappers.<PaymentApplication>lambdaQuery()
.like(Func.isNotEmpty(query.getPaymentNo()), PaymentApplication::getPaymentNo, query.getPaymentNo())
.like(Func.isNotEmpty(query.getPayeeName()), PaymentApplication::getPayeeName, query.getPayeeName())
.like(Func.isNotEmpty(query.getProjectName()), PaymentApplication::getProjectName, query.getProjectName())
.like(Func.isNotEmpty(query.getDeptName()), PaymentApplication::getDeptName, query.getDeptName())
.like(Func.isNotEmpty(query.getSettlementNo()), PaymentApplication::getSettlementNo, query.getSettlementNo())
.eq(Func.isNotEmpty(query.getPaymentType()), PaymentApplication::getPaymentType, query.getPaymentType())
.eq(Func.isNotEmpty(query.getApprovalStatus()), PaymentApplication::getApprovalStatus, query.getApprovalStatus())
.eq(Func.isNotEmpty(query.getKingdeeStatus()), PaymentApplication::getKingdeeStatus, query.getKingdeeStatus())
.ge(query.getApplyStartDate() != null, PaymentApplication::getApplyDate, query.getApplyStartDate())
.le(query.getApplyEndDate() != null, PaymentApplication::getApplyDate, query.getApplyEndDate())
.orderByDesc(PaymentApplication::getCreateTime);
return page(page, wrapper).convert(item -> {
PaymentApplicationVO vo = PaymentApplicationWrapper.build().entityVO(item);
return vo;
});
}
@Override
public PaymentApplicationVO detail(Long id) {
PaymentApplication entity = existing(id);
PaymentApplicationVO vo = PaymentApplicationWrapper.build().entityVO(entity);
vo.setInvoices(invoiceMapper.selectList(Wrappers.<PaymentApplicationInvoice>lambdaQuery()
.eq(PaymentApplicationInvoice::getPaymentApplicationId, id).orderByAsc(PaymentApplicationInvoice::getLineNo)));
vo.setPaymentRecords(recordMapper.selectList(Wrappers.<org.springblade.transport.pojo.entity.PaymentApplicationRecord>lambdaQuery()
.eq(org.springblade.transport.pojo.entity.PaymentApplicationRecord::getPaymentApplicationId, id)
.orderByDesc(org.springblade.transport.pojo.entity.PaymentApplicationRecord::getCreateTime)));
return vo;
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long saveDraft(PaymentApplicationSaveRequest request) {
PaymentApplication entity = request.getId() == null ? new PaymentApplication() : editable(request.getId());
validateRequest(request);
if (entity.getId() == null) {
entity.setPaymentNo(nextNo());
entity.setApprovalStatus(DRAFT);
entity.setCurrentNode("草稿");
entity.setKingdeeStatus("unsynced");
entity.setPaidAmount(BigDecimal.ZERO);
entity.setInvoiceStatus("unmatched");
}
entity.setPaymentType(request.getPaymentType());
entity.setPaymentMethod(request.getPaymentMethod());
entity.setPaymentRatio(request.getPaymentRatio());
entity.setAppliedAmount(nonNegative(request.getAppliedAmount(), "申请付款金额"));
entity.setReceiptAccountId(request.getReceiptAccountId());
entity.setReceiptAccountName(request.getReceiptAccountName());
entity.setBankName(request.getBankName());
entity.setBankAccount(request.getBankAccount());
entity.setAttachmentsJson(request.getAttachmentsJson());
entity.setRemark(limit(request.getRemark(), 200));
entity.setApplyDate(entity.getApplyDate() == null ? LocalDate.now() : entity.getApplyDate());
entity.setApplicantName(Func.isEmpty(entity.getApplicantName()) ? AuthUtil.getUserName() : entity.getApplicantName());
fillReference(entity, request);
fillBillLedger(entity, request);
validateQuota(entity);
saveOrUpdate(entity);
invoiceMapper.delete(Wrappers.<PaymentApplicationInvoice>lambdaQuery()
.eq(PaymentApplicationInvoice::getPaymentApplicationId, entity.getId()));
int lineNo = 1;
BigDecimal matchedInvoiceAmount = BigDecimal.ZERO;
for (PaymentApplicationInvoiceRequest item : request.getInvoices() == null ? List.<PaymentApplicationInvoiceRequest>of() : request.getInvoices()) {
validateInvoice(item);
PaymentApplicationInvoice invoice = Objects.requireNonNull(BeanUtil.copyProperties(item, PaymentApplicationInvoice.class));
invoice.setId(null);
invoice.setPaymentApplicationId(entity.getId());
invoice.setLineNo(lineNo++);
invoiceMapper.insert(invoice);
matchedInvoiceAmount = matchedInvoiceAmount.add(money(item.getMatchedAmount()));
}
if (matchedInvoiceAmount.compareTo(money(entity.getAppliedAmount())) > 0) {
throw new ServiceException("发票匹配金额合计不能超过申请付款金额");
}
entity.setMatchedInvoiceAmount(matchedInvoiceAmount);
entity.setInvoiceStatus(matchedInvoiceAmount.compareTo(BigDecimal.ZERO) > 0 ? "matched" : "unmatched");
updateById(entity);
return entity.getId();
}
@Override @Transactional(rollbackFor = Exception.class)
public void removeDraft(Long id) { PaymentApplication entity = editable(id); invoiceMapper.delete(Wrappers.<PaymentApplicationInvoice>lambdaQuery().eq(PaymentApplicationInvoice::getPaymentApplicationId, id)); removeById(entity); }
@Override
@Transactional(rollbackFor = Exception.class)
public void submit(PaymentApplicationStatusRequest request) {
PaymentApplication entity = lockedPayment(request.getId());
if (!List.of(DRAFT, RETURNED).contains(entity.getApprovalStatus())) {
throw new ServiceException("当前状态不允许提交");
}
validateSelectedBill(entity, false);
entity.setApprovalStatus(REVIEWING);
entity.setCurrentNode("财务审核");
entity.setCurrentProcessor(AuthUtil.getUserName());
updateById(entity);
}
@Override public void returnBill(PaymentApplicationStatusRequest request) { change(request.getId(), REVIEWING, RETURNED, "已驳回", request.getReason()); }
@Override
@Transactional(rollbackFor = Exception.class)
public void voidBill(PaymentApplicationStatusRequest request) {
PaymentApplication entity = lockedPayment(request.getId());
if (!APPROVED.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批通过的付款申请允许作废");
if (isBillPayment(entity.getPaymentMethod())) releaseBillBalance(entity);
entity.setApprovalStatus(VOIDED);
entity.setCurrentNode("已作废");
entity.setCurrentProcessor(AuthUtil.getUserName());
entity.setRemark(request.getReason() == null ? entity.getRemark() : limit(request.getReason(), 200));
updateById(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void approve(PaymentApplicationStatusRequest request) {
PaymentApplication entity = lockedPayment(request.getId());
if (!REVIEWING.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批中的付款申请允许审核");
if (isBillPayment(entity.getPaymentMethod())) useBillBalance(entity);
entity.setApprovalStatus(APPROVED); entity.setCurrentNode("审批通过"); entity.setCurrentProcessor(AuthUtil.getUserName());
updateById(entity);
}
@Override
public String syncKingdee(Long id) {
PaymentApplication entity = existing(id);
if (!APPROVED.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批通过的付款申请允许生成金蝶单据");
if ("synced".equals(entity.getKingdeeStatus())) return entity.getKingdeeBillNo();
String no = "FKD" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + String.format("%05d", count(Wrappers.<PaymentApplication>lambdaQuery().likeRight(PaymentApplication::getKingdeeBillNo, "FKD" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE))) + 1);
entity.setKingdeeBillNo(no); entity.setKingdeeStatus("synced"); updateById(entity); return no;
}
private void validateRequest(PaymentApplicationSaveRequest request) {
if (Func.isEmpty(request.getPaymentType())) throw new ServiceException("付款类型不能为空");
if (!List.of("project_advance", "progress_advance", "settlement_payment").contains(request.getPaymentType())) throw new ServiceException("付款类型不合法");
if (Func.isEmpty(request.getPaymentMethod())) throw new ServiceException("付款方式不能为空");
if (!List.of("bank_transfer", "bank_draft", "commercial_draft").contains(request.getPaymentMethod())) throw new ServiceException("付款方式不合法");
if (isBillPayment(request.getPaymentMethod()) && request.getBillLedgerId() == null) throw new ServiceException("汇票付款必须选择汇票台账");
if (request.getPaymentRatio() != null && (request.getPaymentRatio().compareTo(BigDecimal.ZERO) < 0 || request.getPaymentRatio().compareTo(BigDecimal.valueOf(100)) > 0)) throw new ServiceException("付款比例必须在0-100之间");
if (request.getAppliedAmount() == null || request.getAppliedAmount().compareTo(BigDecimal.ZERO) < 0) throw new ServiceException("申请付款金额不能小于0");
if (!"project_advance".equals(request.getPaymentType()) && request.getSettlementId() == null && request.getPreSettlementId() == null) throw new ServiceException("非项目预付必须关联结算单");
if ("project_advance".equals(request.getPaymentType()) && request.getProjectId() == null) throw new ServiceException("项目预付必须选择所属项目");
if ("progress_advance".equals(request.getPaymentType()) && request.getPreSettlementId() == null) throw new ServiceException("进度预付必须关联预结算单");
if ("settlement_payment".equals(request.getPaymentType()) && request.getSettlementId() == null) throw new ServiceException("结算付款必须关联正式结算单");
}
private void validateInvoice(PaymentApplicationInvoiceRequest invoice) {
BigDecimal invoiceAmount = money(invoice.getInvoiceAmount());
BigDecimal matchedAmount = money(invoice.getMatchedAmount());
if (invoiceAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new ServiceException("发票金额不能小于0");
}
if (matchedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new ServiceException("发票匹配金额不能小于0");
}
if (matchedAmount.compareTo(invoiceAmount) > 0) {
throw new ServiceException("单张发票匹配金额不能超过发票金额");
}
if (invoice.getTaxRate() != null && (invoice.getTaxRate().compareTo(BigDecimal.ZERO) < 0
|| invoice.getTaxRate().compareTo(BigDecimal.valueOf(100)) > 0)) {
throw new ServiceException("发票税率必须在0-100之间");
}
}
private void fillReference(PaymentApplication entity, PaymentApplicationSaveRequest request) {
if (request.getSettlementId() != null) {
FormalSettlement source = formalSettlementMapper.selectById(request.getSettlementId());
if (source == null) throw new ServiceException("正式结算单不存在");
if (!APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("只能选择审批通过、未作废的正式结算单");
entity.setSettlementId(source.getId()); entity.setSettlementNo(source.getFormalSettlementNo()); entity.setPreSettlementId(null); entity.setPreSettlementNo(null);
entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(money(source.getSettlementAmount()).subtract(money(source.getAppliedPaymentAmount()))); entity.setBillType("正式结算单");
} else if (request.getPreSettlementId() != null) {
PreSettlement source = preSettlementMapper.selectById(request.getPreSettlementId());
if (source == null) throw new ServiceException("预结算单不存在");
if (!APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("只能选择审批通过、未作废的预结算单");
entity.setPreSettlementId(source.getId()); entity.setPreSettlementNo(source.getPreSettlementNo()); entity.setSettlementId(null); entity.setSettlementNo(null); entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(money(source.getSettlementAmount()).subtract(money(source.getAdvanceAppliedAmount()))); entity.setBillType("预结算单");
} else {
entity.setProjectId(request.getProjectId()); entity.setProjectName(request.getProjectName()); entity.setDeptId(request.getDeptId()); entity.setDeptName(request.getDeptName()); entity.setContractId(request.getContractId()); entity.setContractNo(request.getContractNo()); entity.setContractName(request.getContractName()); entity.setPayerName(request.getPayerName()); entity.setPayeeName(request.getPayeeName()); entity.setSettlementAmount(request.getSettlementAmount()); entity.setPayableAmount(request.getPayableAmount()); entity.setBillType(request.getBillType());
}
if (money(entity.getAppliedAmount()).compareTo(money(entity.getPayableAmount())) > 0 && money(entity.getPayableAmount()).compareTo(BigDecimal.ZERO) > 0) throw new ServiceException("申请付款金额不能超过可付款金额");
}
private void validateQuota(PaymentApplication entity) {
BigDecimal usedByProject = sumApplied(Wrappers.<PaymentApplication>lambdaQuery()
.eq(entity.getProjectId() != null, PaymentApplication::getProjectId, entity.getProjectId())
.ne(entity.getId() != null, PaymentApplication::getId, entity.getId()));
ProjectApply project = entity.getProjectId() == null ? null : projectApplyMapper.selectById(entity.getProjectId());
if (project != null && project.getFundLimit() != null) {
BigDecimal projectLimit = project.getFundLimit().multiply(BigDecimal.valueOf(10000));
if (usedByProject.add(money(entity.getAppliedAmount())).compareTo(projectLimit) > 0) throw new ServiceException("申请付款金额超过项目剩余资金使用额度");
}
if (Func.isNotEmpty(entity.getPayeeName())) {
CustomerArchive customer = customerArchiveMapper.selectOne(Wrappers.<CustomerArchive>lambdaQuery()
.and(wrapper -> wrapper.eq(CustomerArchive::getFullName, entity.getPayeeName()).or().eq(CustomerArchive::getShortName, entity.getPayeeName()))
.eq(CustomerArchive::getIsDeleted, 0).last("limit 1"));
if (customer != null && customer.getMaxCreditLimit() != null) {
BigDecimal customerLimit = customer.getMaxCreditLimit().multiply(BigDecimal.valueOf(10000));
BigDecimal usedByCustomer = sumApplied(Wrappers.<PaymentApplication>lambdaQuery()
.eq(PaymentApplication::getPayeeName, entity.getPayeeName()).ne(entity.getId() != null, PaymentApplication::getId, entity.getId()));
if (usedByCustomer.add(money(entity.getAppliedAmount())).compareTo(customerLimit) > 0) throw new ServiceException("申请付款金额超过客户剩余资金使用额度");
}
}
}
private BigDecimal sumApplied(LambdaQueryWrapper<PaymentApplication> wrapper) {
return list(wrapper.ne(PaymentApplication::getApprovalStatus, VOIDED)).stream()
.map(PaymentApplication::getAppliedAmount).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
}
private void fillBillLedger(PaymentApplication entity, PaymentApplicationSaveRequest request) {
if (!isBillPayment(request.getPaymentMethod())) {
entity.setBillLedgerId(null);
entity.setBillNo(null);
return;
}
BillLedger ledger = billLedgerMapper.selectById(request.getBillLedgerId());
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true);
entity.setBillLedgerId(ledger.getId());
entity.setBillNo(ledger.getBillNo());
}
private void validateSelectedBill(PaymentApplication entity, boolean locked) {
if (!isBillPayment(entity.getPaymentMethod())) return;
BillLedger ledger = locked ? lockedBill(entity.getBillLedgerId()) : billLedgerMapper.selectById(entity.getBillLedgerId());
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true);
}
private void validateBill(BillLedger ledger, Long deptId, BigDecimal amount, boolean checkMaturity) {
if (ledger == null || Objects.equals(ledger.getIsDeleted(), 1)) throw new ServiceException("所选汇票台账不存在");
if (checkMaturity && (ledger.getMaturityDate() == null || ledger.getMaturityDate().isBefore(LocalDate.now()))) {
throw new ServiceException("所选汇票已到期");
}
if (money(amount).compareTo(money(ledger.getAvailableBalance())) > 0) throw new ServiceException("申请付款金额超过汇票可用余额");
if (!departmentAvailable(ledger, deptId)) throw new ServiceException("当前使用部门不在汇票可用部门范围内");
}
private void useBillBalance(PaymentApplication entity) {
BillLedger ledger = lockedBill(entity.getBillLedgerId());
validateBill(ledger, entity.getDeptId(), entity.getAppliedAmount(), true);
BillLedgerUsage exists = billLedgerUsageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
.eq(BillLedgerUsage::getPaymentApplicationId, entity.getId()).last("FOR UPDATE"));
if (exists != null) throw new ServiceException("该付款申请已生成汇票使用记录");
BigDecimal amount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP);
ledger.setAvailableBalance(money(ledger.getAvailableBalance()).subtract(amount));
billLedgerMapper.updateById(ledger);
BillLedgerUsage usage = new BillLedgerUsage();
usage.setBillLedgerId(ledger.getId());
usage.setPaymentApplicationId(entity.getId());
usage.setApplicationNo(entity.getPaymentNo());
usage.setUsedAmount(amount);
usage.setUseDeptId(entity.getDeptId());
usage.setUseDeptName(entity.getDeptName());
usage.setUsageStatus(APPROVED);
usage.setStatus(1);
billLedgerUsageMapper.insert(usage);
}
private void releaseBillBalance(PaymentApplication entity) {
BillLedgerUsage usage = billLedgerUsageMapper.selectOne(Wrappers.<BillLedgerUsage>lambdaQuery()
.eq(BillLedgerUsage::getPaymentApplicationId, entity.getId())
.eq(BillLedgerUsage::getUsageStatus, APPROVED).last("FOR UPDATE"));
if (usage == null) throw new ServiceException("未找到对应的汇票使用记录,无法作废");
BillLedger ledger = lockedBill(usage.getBillLedgerId());
ledger.setAvailableBalance(money(ledger.getAvailableBalance()).add(money(usage.getUsedAmount()))
.min(money(ledger.getFaceAmount())));
billLedgerMapper.updateById(ledger);
usage.setUsageStatus("released");
billLedgerUsageMapper.updateById(usage);
}
private BillLedger lockedBill(Long id) {
if (id == null) throw new ServiceException("汇票台账不能为空");
BillLedger ledger = billLedgerMapper.selectOne(Wrappers.<BillLedger>lambdaQuery()
.eq(BillLedger::getId, id).last("FOR UPDATE"));
if (ledger == null || Objects.equals(ledger.getIsDeleted(), 1)) throw new ServiceException("所选汇票台账不存在");
return ledger;
}
private PaymentApplication lockedPayment(Long id) {
PaymentApplication entity = baseMapper.selectOne(Wrappers.<PaymentApplication>lambdaQuery()
.eq(PaymentApplication::getId, id).last("FOR UPDATE"));
if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) throw new ServiceException("付款申请不存在");
return entity;
}
private boolean departmentAvailable(BillLedger ledger, Long deptId) {
if (Func.isEmpty(ledger.getAvailableDeptIdsJson())) return false;
try {
Object parsed = JsonUtil.parse(ledger.getAvailableDeptIdsJson(), List.class);
if (!(parsed instanceof List<?> values)) return false;
if (values.stream().anyMatch(value -> "all".equals(String.valueOf(value)))) return true;
return deptId != null && values.stream().anyMatch(value -> String.valueOf(deptId).equals(String.valueOf(value)));
} catch (Exception exception) {
throw new ServiceException("汇票可用部门配置不正确");
}
}
private boolean isBillPayment(String paymentMethod) {
return List.of("bank_draft", "commercial_draft").contains(paymentMethod);
}
private void change(Long id, String from, String to, String node, String reason) { PaymentApplication entity = existing(id); if (!from.equals(entity.getApprovalStatus())) throw new ServiceException("当前状态不允许此操作"); entity.setApprovalStatus(to); entity.setCurrentNode(node); entity.setCurrentProcessor(AuthUtil.getUserName()); entity.setRemark(reason == null ? entity.getRemark() : limit(reason, 200)); updateById(entity); }
private PaymentApplication existing(Long id) { PaymentApplication entity = getById(id); if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) throw new ServiceException("付款申请不存在"); return entity; }
private PaymentApplication editable(Long id) { PaymentApplication entity = existing(id); if (!List.of(DRAFT, RETURNED).contains(entity.getApprovalStatus())) throw new ServiceException("当前状态不可编辑"); return entity; }
private String nextNo() { String prefix = "FKD" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); return prefix + String.format("%05d", count(Wrappers.<PaymentApplication>lambdaQuery().likeRight(PaymentApplication::getPaymentNo, prefix)) + 1); }
private BigDecimal nonNegative(BigDecimal value, String name) { if (value == null || value.compareTo(BigDecimal.ZERO) < 0) throw new ServiceException(name + "不能小于0"); return value; }
private BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; }
private String limit(String value, int length) { if (value != null && value.length() > length) throw new ServiceException("备注不能超过" + length + "个字符"); return value; }
}
@@ -0,0 +1,259 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.mapper.FormalSettlementMapper;
import org.springblade.transport.mapper.KingdeeReceiptFlowMapper;
import org.springblade.transport.mapper.ReceiptClaimMapper;
import org.springblade.transport.mapper.ReceiptClaimSettlementMapper;
import org.springblade.transport.mapper.ReceiptFlowRecordMapper;
import org.springblade.transport.pojo.dto.ReceiptClaimAttachmentsRequest;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.KingdeeReceiptFlow;
import org.springblade.transport.pojo.entity.ReceiptClaim;
import org.springblade.transport.pojo.entity.ReceiptClaimSettlement;
import org.springblade.transport.pojo.entity.ReceiptFlowRecord;
import org.springblade.transport.pojo.vo.ReceiptClaimRecordVO;
import org.springblade.transport.service.IReceiptClaimRecordService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
/**
* 认领记录服务实现类
*
* @author Chill
*/
@Service
@RequiredArgsConstructor
public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl<ReceiptClaimMapper, ReceiptClaim>
implements IReceiptClaimRecordService {
private static final String CLAIMED = "claimed";
private static final String VOIDED = "voided";
private static final String APPROVED = "approved";
private final ReceiptClaimSettlementMapper claimSettlementMapper;
private final KingdeeReceiptFlowMapper receiptFlowMapper;
private final FormalSettlementMapper formalSettlementMapper;
private final ReceiptFlowRecordMapper recordMapper;
@Override
public IPage<ReceiptClaimRecordVO> selectPage(IPage<ReceiptClaimRecordVO> page,
ReceiptClaimRecordVO query) {
page.setRecords(baseMapper.selectClaimRecordPage(page, query, AuthUtil.getUserId()));
page.getRecords().forEach(this::fillStatusNames);
return page;
}
@Override
public ReceiptClaimRecordVO detail(Long id) {
if (id == null) {
throw new ServiceException("认领记录ID不能为空");
}
ReceiptClaimRecordVO record = baseMapper.selectClaimRecordDetail(id, AuthUtil.getUserId());
if (record == null) {
throw new ServiceException("认领记录不存在或无权查看");
}
record.setSettlements(claimSettlementMapper.selectList(
Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.eq(ReceiptClaimSettlement::getReceiptClaimId, id)
.orderByAsc(ReceiptClaimSettlement::getCreateTime)));
fillStatusNames(record);
return record;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAttachments(ReceiptClaimAttachmentsRequest request) {
if (request == null || request.getId() == null) {
throw new ServiceException("认领记录ID不能为空");
}
if (request.getAttachmentsJson() != null && request.getAttachmentsJson().length() > 2000000) {
throw new ServiceException("附件信息不能超过2MB");
}
ReceiptClaim claim = baseMapper.selectOne(Wrappers.<ReceiptClaim>lambdaQuery()
.eq(ReceiptClaim::getId, request.getId())
.eq(ReceiptClaim::getClaimerId, AuthUtil.getUserId())
.last("FOR UPDATE"));
if (claim == null || Objects.equals(claim.getIsDeleted(), 1)) {
throw new ServiceException("认领记录不存在或无权操作");
}
if (!CLAIMED.equals(normalizeClaimStatus(claim.getClaimStatus()))) {
throw new ServiceException("已作废认领记录不允许修改附件");
}
claim.setAttachmentsJson(request.getAttachmentsJson());
updateById(claim);
ReceiptFlowRecord operationRecord = new ReceiptFlowRecord();
operationRecord.setReceiptFlowId(claim.getReceiptFlowId());
operationRecord.setReceiptClaimId(claim.getId());
operationRecord.setActionType("update_claim_attachments");
operationRecord.setActionName("维护认领记录附件");
operationRecord.setFromStatus(CLAIMED);
operationRecord.setToStatus(CLAIMED);
operationRecord.setOperationAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
operationRecord.setOperatorName(Func.isEmpty(AuthUtil.getUserName()) ? "system"
: AuthUtil.getUserName());
operationRecord.setContent("更新认领记录附件");
recordMapper.insert(operationRecord);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String voidClaim(Long id) {
ReceiptClaim claim = baseMapper.selectOne(Wrappers.<ReceiptClaim>lambdaQuery()
.eq(ReceiptClaim::getId, id)
.eq(ReceiptClaim::getClaimerId, AuthUtil.getUserId())
.last("FOR UPDATE"));
if (claim == null || Objects.equals(claim.getIsDeleted(), 1)) {
throw new ServiceException("认领记录不存在或无权作废");
}
if (!CLAIMED.equals(normalizeClaimStatus(claim.getClaimStatus()))) {
throw new ServiceException("仅已认领记录允许作废");
}
KingdeeReceiptFlow flow = receiptFlowMapper.selectOne(
Wrappers.<KingdeeReceiptFlow>lambdaQuery()
.eq(KingdeeReceiptFlow::getId, claim.getReceiptFlowId())
.last("FOR UPDATE"));
if (flow == null || Objects.equals(flow.getIsDeleted(), 1)) {
throw new ServiceException("关联收款流水不存在");
}
List<ReceiptClaimSettlement> relations = claimSettlementMapper.selectList(
Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.eq(ReceiptClaimSettlement::getReceiptClaimId, claim.getId())
.eq(ReceiptClaimSettlement::getStatus, 1));
if (relations.isEmpty()) {
throw new ServiceException("认领记录不存在有效结算分摊");
}
List<ReceiptClaimSettlement> orderedRelations = relations.stream()
.sorted(Comparator.comparing(ReceiptClaimSettlement::getFormalSettlementId))
.toList();
for (ReceiptClaimSettlement relation : orderedRelations) {
FormalSettlement settlement = formalSettlementMapper.selectOne(
Wrappers.<FormalSettlement>lambdaQuery()
.eq(FormalSettlement::getId, relation.getFormalSettlementId())
.last("FOR UPDATE"));
if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) {
throw new ServiceException("关联结算单不存在");
}
BigDecimal paidAfter = money(settlement.getPaidAmount())
.subtract(money(relation.getAllocatedReceiptAmount())).max(BigDecimal.ZERO);
settlement.setPaidAmount(paidAfter);
settlement.setPaymentStatus(amountStatus(paidAfter, settlement.getSettlementAmount()));
formalSettlementMapper.updateById(settlement);
relation.setStatus(0);
claimSettlementMapper.updateById(relation);
}
BigDecimal flowClaimedAfter = money(flow.getClaimedAmount())
.subtract(money(claim.getClaimAmount())).max(BigDecimal.ZERO);
flow.setClaimedAmount(flowClaimedAfter);
flow.setClaimStatus(amountClaimStatus(flowClaimedAfter, flow.getReceiptAmount()));
receiptFlowMapper.updateById(flow);
String kingdeeBillNo = buildKingdeeBillNo(claim.getId());
claim.setClaimStatus(VOIDED);
claim.setKingdeeBillNo(kingdeeBillNo);
claim.setKingdeeBillStatus(APPROVED);
claim.setVoidedBy(AuthUtil.getUserId());
claim.setVoidedByName(Func.isEmpty(AuthUtil.getUserName()) ? claim.getClaimerName()
: AuthUtil.getUserName());
claim.setVoidedTime(LocalDateTime.now());
updateById(claim);
ReceiptFlowRecord operationRecord = new ReceiptFlowRecord();
operationRecord.setReceiptFlowId(flow.getId());
operationRecord.setReceiptClaimId(claim.getId());
operationRecord.setActionType("void_claim");
operationRecord.setActionName("作废认领记录");
operationRecord.setFromStatus(CLAIMED);
operationRecord.setToStatus(VOIDED);
operationRecord.setOperationAmount(money(claim.getClaimAmount()));
operationRecord.setOperatorName(Func.isEmpty(AuthUtil.getUserName()) ? "system"
: AuthUtil.getUserName());
operationRecord.setContent("生成金蝶认领冲单:" + kingdeeBillNo + ",状态:审核通过");
recordMapper.insert(operationRecord);
return kingdeeBillNo;
}
private void fillStatusNames(ReceiptClaimRecordVO record) {
record.setClaimStatusName(VOIDED.equals(normalizeClaimStatus(record.getClaimStatus()))
? "已作废" : "已认领");
record.setKingdeeBillStatusName(switch (record.getKingdeeBillStatus() == null
? "" : record.getKingdeeBillStatus()) {
case APPROVED -> "审核通过";
case "failed" -> "处理失败";
default -> "未生成";
});
}
private String normalizeClaimStatus(String claimStatus) {
return VOIDED.equals(claimStatus) ? VOIDED : CLAIMED;
}
private String buildKingdeeBillNo(Long claimId) {
String time = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());
String suffix = String.valueOf(claimId);
return "KDCX" + time + suffix.substring(Math.max(0, suffix.length() - 6));
}
private String amountClaimStatus(BigDecimal claimedAmount, BigDecimal receiptAmount) {
if (claimedAmount.compareTo(BigDecimal.ZERO) <= 0) {
return "unclaimed";
}
return claimedAmount.compareTo(money(receiptAmount)) >= 0 ? "claimed" : "partial";
}
private String amountStatus(BigDecimal paidAmount, BigDecimal settlementAmount) {
if (paidAmount.compareTo(BigDecimal.ZERO) <= 0) {
return "unpaid";
}
return paidAmount.compareTo(money(settlementAmount)) >= 0 ? "paid" : "partial";
}
private BigDecimal money(BigDecimal amount) {
return amount == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)
: amount.setScale(2, RoundingMode.HALF_UP);
}
}
@@ -0,0 +1,504 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.SysCache;
import org.springblade.system.cache.UserCache;
import org.springblade.system.pojo.entity.Dept;
import org.springblade.transport.mapper.FormalSettlementMapper;
import org.springblade.transport.mapper.KingdeeReceiptFlowMapper;
import org.springblade.transport.mapper.ReceiptClaimMapper;
import org.springblade.transport.mapper.ReceiptClaimSettlementMapper;
import org.springblade.transport.mapper.ReceiptFlowRecordMapper;
import org.springblade.transport.pojo.dto.ReceiptClaimRequest;
import org.springblade.transport.pojo.dto.ReceiptFlowSyncRequest;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.KingdeeReceiptFlow;
import org.springblade.transport.pojo.entity.ReceiptClaim;
import org.springblade.transport.pojo.entity.ReceiptClaimSettlement;
import org.springblade.transport.pojo.entity.ReceiptFlowRecord;
import org.springblade.transport.pojo.vo.ReceiptFlowVO;
import org.springblade.transport.service.IReceiptFlowService;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.wrapper.ReceiptFlowWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 收款流水服务实现类
*
* @author Chill
*/
@Service
@RequiredArgsConstructor
public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMapper, KingdeeReceiptFlow>
implements IReceiptFlowService {
private static final String UNCLAIMED = "unclaimed";
private static final String PARTIAL = "partial";
private static final String CLAIMED = "claimed";
private static final String APPROVED = "approved";
private static final String RECEIVABLE = "receivable";
private final ReceiptClaimMapper claimMapper;
private final ReceiptClaimSettlementMapper claimSettlementMapper;
private final ReceiptFlowRecordMapper recordMapper;
private final FormalSettlementMapper formalSettlementMapper;
@Override
public IPage<ReceiptFlowVO> selectPage(IPage<KingdeeReceiptFlow> page, ReceiptFlowVO query) {
LambdaQueryWrapper<KingdeeReceiptFlow> wrapper = Wrappers.<KingdeeReceiptFlow>lambdaQuery()
.like(Func.isNotEmpty(query.getReceiptNoticeNo()), KingdeeReceiptFlow::getReceiptNoticeNo,
query.getReceiptNoticeNo())
.like(Func.isNotEmpty(query.getCounterpartyName()), KingdeeReceiptFlow::getCounterpartyName,
query.getCounterpartyName())
.like(Func.isNotEmpty(query.getCounterpartyBank()), KingdeeReceiptFlow::getCounterpartyBank,
query.getCounterpartyBank())
.like(Func.isNotEmpty(query.getCounterpartyAccount()), KingdeeReceiptFlow::getCounterpartyAccount,
query.getCounterpartyAccount())
.like(Func.isNotEmpty(query.getSummary()), KingdeeReceiptFlow::getSummary, query.getSummary())
.eq(Func.isNotEmpty(query.getClaimStatus()), KingdeeReceiptFlow::getClaimStatus,
query.getClaimStatus())
.ge(query.getTransactionStartTime() != null, KingdeeReceiptFlow::getTransactionTime,
query.getTransactionStartTime())
.le(query.getTransactionEndTime() != null, KingdeeReceiptFlow::getTransactionTime,
query.getTransactionEndTime())
.eq(KingdeeReceiptFlow::getStatus, 1)
.orderByDesc(KingdeeReceiptFlow::getCreateTime);
return page(page, wrapper).convert(ReceiptFlowWrapper.build()::entityVO);
}
@Override
public ReceiptFlowVO detail(Long id) {
ReceiptFlowVO vo = ReceiptFlowWrapper.build().entityVO(existing(id));
vo.setClaimerName(UserCache.getUserRealName(AuthUtil.getUserId()));
Long deptId = Func.firstLong(AuthUtil.getDeptId());
Dept dept = deptId == null ? null : SysCache.getDept(deptId);
vo.setClaimerDeptName(dept == null ? null : dept.getDeptName());
vo.setClaimDate(LocalDate.now());
return vo;
}
@Override
public List<Map<String, Object>> settlementCandidates(String keyword, Long flowId) {
KingdeeReceiptFlow flow = existing(flowId);
List<FormalSettlement> settlements = formalSettlementMapper.selectList(
Wrappers.<FormalSettlement>lambdaQuery()
.eq(FormalSettlement::getSettlementType, RECEIVABLE)
.eq(FormalSettlement::getApprovalStatus, APPROVED)
.eq(FormalSettlement::getStatus, 1)
.and(Func.isNotEmpty(keyword), wrapper -> wrapper
.like(FormalSettlement::getFormalSettlementNo, keyword)
.or().like(FormalSettlement::getProjectName, keyword)
.or().like(FormalSettlement::getContractName, keyword))
.orderByDesc(FormalSettlement::getCreateTime)
.last("limit 200"));
return settlements.stream()
.filter(settlement -> Func.isEmpty(flow.getCounterpartyName())
|| sameName(flow.getCounterpartyName(), settlement.getPayerName()))
.map(settlement -> candidateRow(settlement))
.filter(row -> ((BigDecimal) row.get("remainingReceiptAmount")).compareTo(BigDecimal.ZERO) > 0)
.toList();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long claim(ReceiptClaimRequest request) {
if (request == null || request.getFlowId() == null) {
throw new ServiceException("收款流水ID不能为空");
}
if (request.getSettlements() == null || request.getSettlements().isEmpty()) {
throw new ServiceException("请选择应收正式结算单");
}
validateLength(request.getRemark(), 200, "备注不能超过200字");
KingdeeReceiptFlow flow = lockedFlow(request.getFlowId());
Map<Long, BigDecimal> allocationMap = allocationMap(request.getSettlements());
List<Long> settlementIds = allocationMap.keySet().stream().sorted().toList();
Map<Long, FormalSettlement> settlementMap = lockSettlements(settlementIds);
List<FormalSettlement> settlements = settlementIds.stream().map(settlementMap::get).toList();
assertCompatible(settlements);
assertCounterparty(flow, settlements.get(0));
Map<Long, BigDecimal> previousClaimedMap = new LinkedHashMap<>();
BigDecimal allocatedTotal = BigDecimal.ZERO;
for (FormalSettlement settlement : settlements) {
BigDecimal allocated = allocationMap.get(settlement.getId());
BigDecimal previousClaimed = settlementClaimedAmount(settlement.getId());
BigDecimal settlementAmount = positiveMoney(settlement.getSettlementAmount(), "结算总金额");
if (previousClaimed.add(allocated).compareTo(settlementAmount) > 0) {
throw new ServiceException("结算单" + settlement.getFormalSettlementNo()
+ "的累计认领金额不能超过结算总应收含税金额");
}
previousClaimedMap.put(settlement.getId(), previousClaimed);
allocatedTotal = allocatedTotal.add(allocated);
}
BigDecimal receiptAmount = positiveMoney(flow.getReceiptAmount(), "收款金额");
BigDecimal previousFlowClaimed = flowClaimedAmount(flow.getId());
if (previousFlowClaimed.add(allocatedTotal).compareTo(receiptAmount) > 0) {
throw new ServiceException("本次分摊金额不能超过流水剩余可认领金额");
}
Dept dept = TransportBusinessSupport.currentDept("收款流水认领");
ReceiptClaim claim = new ReceiptClaim();
claim.setReceiptFlowId(flow.getId());
claim.setClaimAmount(allocatedTotal);
claim.setClaimerId(AuthUtil.getUserId());
claim.setClaimerName(UserCache.getUserRealName(AuthUtil.getUserId()));
claim.setClaimerDeptId(dept.getId());
claim.setClaimerDeptName(dept.getDeptName());
claim.setClaimDate(LocalDate.now());
claim.setAttachmentsJson(request.getAttachmentsJson());
claim.setRemark(trimToNull(request.getRemark()));
claim.setClaimStatus(CLAIMED);
claim.setKingdeeBillStatus("none");
claimMapper.insert(claim);
for (FormalSettlement settlement : settlements) {
BigDecimal previousClaimed = previousClaimedMap.get(settlement.getId());
BigDecimal allocated = allocationMap.get(settlement.getId());
BigDecimal claimedAfter = previousClaimed.add(allocated);
ReceiptClaimSettlement relation = new ReceiptClaimSettlement();
relation.setReceiptClaimId(claim.getId());
relation.setReceiptFlowId(flow.getId());
relation.setFormalSettlementId(settlement.getId());
relation.setFormalSettlementNo(settlement.getFormalSettlementNo());
relation.setSettlementAmount(money(settlement.getSettlementAmount()));
relation.setClaimedReceiptAmount(previousClaimed);
relation.setAllocatedReceiptAmount(allocated);
claimSettlementMapper.insert(relation);
settlement.setPaidAmount(claimedAfter);
settlement.setPaymentStatus(amountStatus(claimedAfter, settlement.getSettlementAmount()));
formalSettlementMapper.updateById(settlement);
}
String fromStatus = normalizeClaimStatus(flow.getClaimStatus());
BigDecimal claimedAfter = previousFlowClaimed.add(allocatedTotal);
String toStatus = amountClaimStatus(claimedAfter, receiptAmount);
flow.setClaimedAmount(claimedAfter);
flow.setClaimStatus(toStatus);
updateById(flow);
record(flow.getId(), claim.getId(), "claim", "认领收款流水", fromStatus, toStatus,
allocatedTotal, "关联" + settlements.size() + "张应收正式结算单");
return claim.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public int sync(ReceiptFlowSyncRequest request) {
List<ReceiptFlowSyncRequest.FlowRow> rows = request == null || request.getFlows() == null
? List.of() : request.getFlows();
if (rows.isEmpty()) {
record(null, null, "sync", "手动同步流水", null, null, BigDecimal.ZERO,
"未接收到金蝶流水数据");
return 0;
}
Set<String> serialNumbers = new HashSet<>();
int syncedCount = 0;
for (ReceiptFlowSyncRequest.FlowRow row : rows) {
validateSyncRow(row);
if (!serialNumbers.add(row.getDetailSerialNo().trim())) {
throw new ServiceException("明细流水号" + row.getDetailSerialNo() + "重复");
}
KingdeeReceiptFlow entity = baseMapper.selectOne(Wrappers.<KingdeeReceiptFlow>lambdaQuery()
.eq(KingdeeReceiptFlow::getDetailSerialNo, row.getDetailSerialNo().trim())
.last("FOR UPDATE"));
boolean created = entity == null;
if (created) {
entity = new KingdeeReceiptFlow();
entity.setClaimedAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
entity.setClaimStatus(UNCLAIMED);
}
BigDecimal receiptAmount = positiveMoney(row.getReceiptAmount(), "收款金额");
BigDecimal claimedAmount = money(entity.getClaimedAmount());
if (claimedAmount.compareTo(receiptAmount) > 0) {
throw new ServiceException("流水" + row.getDetailSerialNo() + "同步金额不能小于已认领金额");
}
copySyncRow(entity, row, receiptAmount);
entity.setClaimStatus(amountClaimStatus(claimedAmount, receiptAmount));
if (created) {
baseMapper.insert(entity);
} else {
baseMapper.updateById(entity);
}
record(entity.getId(), null, "sync", created ? "新增金蝶收款流水" : "更新金蝶收款流水",
entity.getClaimStatus(), entity.getClaimStatus(), BigDecimal.ZERO, entity.getDetailSerialNo());
syncedCount++;
}
return syncedCount;
}
private Map<String, Object> candidateRow(FormalSettlement settlement) {
BigDecimal claimedAmount = settlementClaimedAmount(settlement.getId());
BigDecimal settlementAmount = money(settlement.getSettlementAmount());
Map<String, Object> row = new LinkedHashMap<>();
row.put("id", settlement.getId());
row.put("formalSettlementNo", settlement.getFormalSettlementNo());
row.put("projectId", settlement.getProjectId());
row.put("projectName", settlement.getProjectName());
row.put("deptId", settlement.getDeptId());
row.put("deptName", settlement.getDeptName());
row.put("contractId", settlement.getContractId());
row.put("contractNo", settlement.getContractNo());
row.put("contractName", settlement.getContractName());
row.put("payerName", settlement.getPayerName());
row.put("payeeName", settlement.getPayeeName());
row.put("settlementAmount", settlementAmount);
row.put("claimedReceiptAmount", claimedAmount);
row.put("remainingReceiptAmount", settlementAmount.subtract(claimedAmount).max(BigDecimal.ZERO));
return row;
}
private Map<Long, BigDecimal> allocationMap(List<ReceiptClaimRequest.SettlementRow> rows) {
Map<Long, BigDecimal> allocationMap = new LinkedHashMap<>();
for (ReceiptClaimRequest.SettlementRow row : rows) {
if (row == null || row.getSettlementId() == null) {
throw new ServiceException("结算单ID不能为空");
}
if (allocationMap.containsKey(row.getSettlementId())) {
throw new ServiceException("同一张结算单不能重复分摊");
}
allocationMap.put(row.getSettlementId(), positiveMoney(row.getAllocatedReceiptAmount(),
"分摊收款金额"));
}
return allocationMap;
}
private Map<Long, FormalSettlement> lockSettlements(List<Long> settlementIds) {
List<FormalSettlement> settlements = new ArrayList<>();
for (Long settlementId : settlementIds) {
FormalSettlement settlement = formalSettlementMapper.selectOne(
Wrappers.<FormalSettlement>lambdaQuery()
.eq(FormalSettlement::getId, settlementId)
.last("FOR UPDATE"));
settlements.add(availableSettlement(settlement));
}
return settlements.stream().collect(Collectors.toMap(FormalSettlement::getId,
Function.identity(), (first, second) -> first, LinkedHashMap::new));
}
private FormalSettlement availableSettlement(FormalSettlement settlement) {
if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)
|| !Objects.equals(settlement.getStatus(), 1)
|| !APPROVED.equals(settlement.getApprovalStatus())
|| !RECEIVABLE.equals(settlement.getSettlementType())) {
throw new ServiceException("只能选择审批通过、未作废的应收正式结算单");
}
return settlement;
}
private void assertCompatible(List<FormalSettlement> settlements) {
if (settlements.isEmpty()) {
throw new ServiceException("请选择应收正式结算单");
}
FormalSettlement first = settlements.get(0);
if (settlements.stream().anyMatch(item -> !Objects.equals(first.getProjectId(), item.getProjectId())
|| !Objects.equals(first.getDeptId(), item.getDeptId())
|| !Objects.equals(first.getPayerName(), item.getPayerName())
|| !Objects.equals(first.getPayeeName(), item.getPayeeName()))) {
throw new ServiceException("关联结算单必须属于同一项目、组织及收付款方");
}
}
private void assertCounterparty(KingdeeReceiptFlow flow, FormalSettlement settlement) {
if (!sameName(flow.getCounterpartyName(), settlement.getPayerName())) {
throw new ServiceException("对方户名与结算单付款方不一致");
}
}
private boolean sameName(String first, String second) {
return Func.isNotEmpty(first) && Func.isNotEmpty(second) && first.trim().equals(second.trim());
}
private BigDecimal settlementClaimedAmount(Long settlementId) {
return claimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.eq(ReceiptClaimSettlement::getFormalSettlementId, settlementId)
.eq(ReceiptClaimSettlement::getStatus, 1)).stream()
.map(ReceiptClaimSettlement::getAllocatedReceiptAmount)
.map(this::money)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private BigDecimal flowClaimedAmount(Long flowId) {
return claimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.eq(ReceiptClaimSettlement::getReceiptFlowId, flowId)
.eq(ReceiptClaimSettlement::getStatus, 1)).stream()
.map(ReceiptClaimSettlement::getAllocatedReceiptAmount)
.map(this::money)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private KingdeeReceiptFlow lockedFlow(Long flowId) {
KingdeeReceiptFlow flow = baseMapper.selectOne(Wrappers.<KingdeeReceiptFlow>lambdaQuery()
.eq(KingdeeReceiptFlow::getId, flowId)
.last("FOR UPDATE"));
if (flow == null || Objects.equals(flow.getIsDeleted(), 1) || !Objects.equals(flow.getStatus(), 1)) {
throw new ServiceException("收款流水不存在或已失效");
}
return flow;
}
private KingdeeReceiptFlow existing(Long id) {
if (id == null) {
throw new ServiceException("收款流水ID不能为空");
}
KingdeeReceiptFlow flow = getById(id);
if (flow == null || Objects.equals(flow.getIsDeleted(), 1) || !Objects.equals(flow.getStatus(), 1)) {
throw new ServiceException("收款流水不存在或已失效");
}
return flow;
}
private void validateSyncRow(ReceiptFlowSyncRequest.FlowRow row) {
if (row == null) {
throw new ServiceException("金蝶收款流水不能为空");
}
required(row.getReceiptNoticeNo(), "认领通知单");
required(row.getPayerName(), "付款人");
required(row.getCounterpartyName(), "对方户名");
required(row.getCounterpartyAccount(), "对方账号");
required(row.getCounterpartyBank(), "对方开户行");
required(row.getDetailSerialNo(), "明细流水号");
if (row.getTransactionTime() == null) {
throw new ServiceException("交易时间不能为空");
}
validateLength(row.getReceiptNoticeNo(), 100, "认领通知单不能超过100字");
validateLength(row.getPayerName(), 200, "付款人不能超过200字");
validateLength(row.getCounterpartyName(), 200, "对方户名不能超过200字");
validateLength(row.getCounterpartyAccount(), 100, "对方账号不能超过100字");
validateLength(row.getCounterpartyBank(), 200, "对方开户行不能超过200字");
validateLength(row.getSummary(), 500, "摘要不能超过500字");
validateLength(row.getDetailSerialNo(), 100, "明细流水号不能超过100字");
positiveMoney(row.getReceiptAmount(), "收款金额");
}
private void copySyncRow(KingdeeReceiptFlow target, ReceiptFlowSyncRequest.FlowRow source,
BigDecimal receiptAmount) {
target.setReceiptNoticeNo(source.getReceiptNoticeNo().trim());
target.setPayerName(source.getPayerName().trim());
target.setReceiptAmount(receiptAmount);
target.setCounterpartyName(source.getCounterpartyName().trim());
target.setCounterpartyAccount(source.getCounterpartyAccount().trim());
target.setCounterpartyBank(source.getCounterpartyBank().trim());
target.setSummary(trimToNull(source.getSummary()));
target.setTransactionTime(source.getTransactionTime());
target.setDetailSerialNo(source.getDetailSerialNo().trim());
target.setSourceUpdatedTime(source.getSourceUpdatedTime() == null
? LocalDateTime.now() : source.getSourceUpdatedTime());
}
private String amountClaimStatus(BigDecimal claimedAmount, BigDecimal receiptAmount) {
if (claimedAmount.compareTo(BigDecimal.ZERO) <= 0) {
return UNCLAIMED;
}
return claimedAmount.compareTo(money(receiptAmount)) >= 0 ? CLAIMED : PARTIAL;
}
private String amountStatus(BigDecimal claimedAmount, BigDecimal settlementAmount) {
if (claimedAmount.compareTo(BigDecimal.ZERO) <= 0) {
return "unpaid";
}
return claimedAmount.compareTo(money(settlementAmount)) >= 0 ? "paid" : "partial";
}
private String normalizeClaimStatus(String claimStatus) {
return List.of(UNCLAIMED, PARTIAL, CLAIMED).contains(claimStatus) ? claimStatus : UNCLAIMED;
}
private BigDecimal positiveMoney(BigDecimal amount, String fieldName) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException(fieldName + "必须大于0");
}
if (amount.stripTrailingZeros().scale() > 2) {
throw new ServiceException(fieldName + "最多保留2位小数");
}
return amount.setScale(2, RoundingMode.HALF_UP);
}
private BigDecimal money(BigDecimal amount) {
return amount == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)
: amount.setScale(2, RoundingMode.HALF_UP);
}
private String required(String value, String fieldName) {
String result = trimToNull(value);
if (result == null) {
throw new ServiceException(fieldName + "不能为空");
}
return result;
}
private String trimToNull(String value) {
return value == null || value.trim().isEmpty() ? null : value.trim();
}
private void validateLength(String value, int maxLength, String message) {
if (value != null && value.length() > maxLength) {
throw new ServiceException(message);
}
}
private void record(Long flowId, Long claimId, String actionType, String actionName,
String fromStatus, String toStatus, BigDecimal operationAmount, String content) {
ReceiptFlowRecord record = new ReceiptFlowRecord();
record.setReceiptFlowId(flowId);
record.setReceiptClaimId(claimId);
record.setActionType(actionType);
record.setActionName(actionName);
record.setFromStatus(fromStatus);
record.setToStatus(toStatus);
record.setOperationAmount(money(operationAmount));
record.setOperatorName(Func.isEmpty(AuthUtil.getUserName()) ? "system" : AuthUtil.getUserName());
record.setContent(content);
recordMapper.insert(record);
}
}
@@ -0,0 +1,44 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.wrapper;
import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.pojo.entity.BillLedger;
import org.springblade.transport.pojo.vo.BillLedgerVO;
import java.time.LocalDate;
import java.util.Objects;
/** 汇票台账包装器。 @author Chill */
public class BillLedgerWrapper extends BaseEntityWrapper<BillLedger, BillLedgerVO> {
public static BillLedgerWrapper build() {
return new BillLedgerWrapper();
}
@Override
public BillLedgerVO entityVO(BillLedger entity) {
BillLedgerVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, BillLedgerVO.class));
vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser()));
vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser()));
vo.setBillTypeName(switch (entity.getBillType() == null ? "" : entity.getBillType()) {
case "issued" -> "开票";
case "received" -> "收票";
default -> entity.getBillType();
});
LocalDate today = LocalDate.now();
if (entity.getMaturityDate() == null) {
vo.setMaturityStatusName("");
} else if (entity.getMaturityDate().isBefore(today)) {
vo.setMaturityStatusName("已到期");
} else if (entity.getMaturityDate().isEqual(today)) {
vo.setMaturityStatusName("今日到期");
} else {
vo.setMaturityStatusName("未到期");
}
return vo;
}
}
@@ -0,0 +1,57 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.wrapper;
import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.pojo.entity.BillPayment;
import org.springblade.transport.pojo.vo.BillPaymentVO;
import java.util.Objects;
/** 汇票付款包装器。 @author Chill */
public class BillPaymentWrapper extends BaseEntityWrapper<BillPayment, BillPaymentVO> {
public static BillPaymentWrapper build() {
return new BillPaymentWrapper();
}
@Override
public BillPaymentVO entityVO(BillPayment entity) {
BillPaymentVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, BillPaymentVO.class));
vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser()));
vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser()));
vo.setApprovalStatusName(switch (entity.getApprovalStatus() == null ? "" : entity.getApprovalStatus()) {
case "draft" -> "草稿";
case "reviewing" -> "审批中";
case "approved" -> "审批通过";
case "returned" -> "已驳回";
case "voided" -> "已作废";
default -> entity.getApprovalStatus();
});
return vo;
}
}
@@ -0,0 +1,66 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.wrapper;
import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.pojo.entity.InvoiceApplication;
import org.springblade.transport.pojo.vo.InvoiceApplicationVO;
import java.util.Objects;
/**
* 开票申请包装类
*
* @author Chill
*/
public class InvoiceApplicationWrapper extends BaseEntityWrapper<InvoiceApplication, InvoiceApplicationVO> {
public static InvoiceApplicationWrapper build() {
return new InvoiceApplicationWrapper();
}
@Override
public InvoiceApplicationVO entityVO(InvoiceApplication entity) {
InvoiceApplicationVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, InvoiceApplicationVO.class));
vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser()));
vo.setApprovalStatusName(switch (entity.getApprovalStatus() == null ? "" : entity.getApprovalStatus()) {
case "draft" -> "草稿";
case "reviewing" -> "审批中";
case "approved" -> "审批通过";
case "returned" -> "已驳回";
case "voided" -> "已作废";
default -> entity.getApprovalStatus();
});
vo.setKingdeeStatusName(switch (entity.getKingdeeStatus() == null ? "" : entity.getKingdeeStatus()) {
case "unsynced" -> "未同步";
case "synced" -> "已同步";
case "failed" -> "同步失败";
default -> entity.getKingdeeStatus();
});
return vo;
}
}
@@ -0,0 +1,67 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.wrapper;
import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.pojo.entity.InvoiceReceipt;
import org.springblade.transport.pojo.vo.InvoiceReceiptVO;
import java.util.Objects;
/**
* 收票登记包装器
*
* @author Chill
*/
public class InvoiceReceiptWrapper extends BaseEntityWrapper<InvoiceReceipt, InvoiceReceiptVO> {
public static InvoiceReceiptWrapper build() {
return new InvoiceReceiptWrapper();
}
@Override
public InvoiceReceiptVO entityVO(InvoiceReceipt entity) {
InvoiceReceiptVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, InvoiceReceiptVO.class));
vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser()));
vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser()));
vo.setApprovalStatusName(switch (entity.getApprovalStatus() == null ? "" : entity.getApprovalStatus()) {
case "draft" -> "草稿";
case "reviewing" -> "审批中";
case "approved" -> "审批通过";
case "returned" -> "已驳回";
case "voided" -> "已作废";
default -> entity.getApprovalStatus();
});
vo.setKingdeeStatusName(switch (entity.getKingdeeStatus() == null ? "" : entity.getKingdeeStatus()) {
case "synced" -> "已同步";
case "failed" -> "同步失败";
default -> "未同步";
});
return vo;
}
}
@@ -0,0 +1,59 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
* Copyright of this software remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.wrapper;
import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.pojo.vo.PaymentApplicationVO;
import java.util.Objects;
/** 付款申请包装器。 @author Chill */
public class PaymentApplicationWrapper extends BaseEntityWrapper<PaymentApplication, PaymentApplicationVO> {
public static PaymentApplicationWrapper build() { return new PaymentApplicationWrapper(); }
@Override
public PaymentApplicationVO entityVO(PaymentApplication entity) {
PaymentApplicationVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, PaymentApplicationVO.class));
vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser()));
vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser()));
vo.setPaymentTypeName(switch (entity.getPaymentType() == null ? "" : entity.getPaymentType()) {
case "project_advance" -> "项目预付";
case "progress_advance" -> "进度预付";
case "settlement_payment" -> "结算付款";
default -> entity.getPaymentType();
});
vo.setApprovalStatusName(switch (entity.getApprovalStatus() == null ? "" : entity.getApprovalStatus()) {
case "draft" -> "草稿";
case "reviewing" -> "审批中";
case "approved" -> "审批通过";
case "returned" -> "已驳回";
case "voided" -> "已作废";
default -> entity.getApprovalStatus();
});
vo.setKingdeeStatusName(switch (entity.getKingdeeStatus() == null ? "" : entity.getKingdeeStatus()) {
case "synced" -> "已生成";
case "failed" -> "生成失败";
default -> "未生成";
});
return vo;
}
}
@@ -0,0 +1,67 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.wrapper;
import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.pojo.entity.KingdeeReceiptFlow;
import org.springblade.transport.pojo.vo.ReceiptFlowVO;
import java.math.BigDecimal;
import java.util.Objects;
/**
* 收款流水包装器
*
* @author Chill
*/
public class ReceiptFlowWrapper extends BaseEntityWrapper<KingdeeReceiptFlow, ReceiptFlowVO> {
public static ReceiptFlowWrapper build() {
return new ReceiptFlowWrapper();
}
@Override
public ReceiptFlowVO entityVO(KingdeeReceiptFlow entity) {
ReceiptFlowVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, ReceiptFlowVO.class));
vo.setClaimStatusName(switch (Objects.toString(entity.getClaimStatus(), "")) {
case "partial" -> "部分认领";
case "claimed" -> "认领完成";
default -> "未认领";
});
BigDecimal receiptAmount = money(entity.getReceiptAmount());
BigDecimal claimedAmount = money(entity.getClaimedAmount());
vo.setRemainingAmount(receiptAmount.subtract(claimedAmount).max(BigDecimal.ZERO));
vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser()));
vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser()));
return vo;
}
private BigDecimal money(BigDecimal amount) {
return amount == null ? BigDecimal.ZERO : amount;
}
}