From 7172621fd37b233f36ee5b5853dc0dc6df759a0c Mon Sep 17 00:00:00 2001 From: caojia <1301869380@qq.com> Date: Wed, 16 Sep 2026 11:02:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BA=94=E6=94=B6=E8=B4=A6=E5=8D=95=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/controller/ar/ArBillController.java | 147 +++ .../controller/ar/ArReceiptController.java | 106 ++ .../com/ruoyi/ar/domain/ArBankAccount.java | 28 + .../main/java/com/ruoyi/ar/domain/ArBill.java | 48 + .../com/ruoyi/ar/domain/ArBillDetail.java | 43 + .../com/ruoyi/ar/domain/ArBillDetailFee.java | 33 + .../com/ruoyi/ar/domain/ArBillExport.java | 29 + .../java/com/ruoyi/ar/domain/ArBillLog.java | 36 + .../java/com/ruoyi/ar/domain/ArReceipt.java | 33 + .../ruoyi/ar/domain/ArReceiptWriteoff.java | 35 + .../ar/domain/vo/ArBillActionRequest.java | 11 + .../ar/domain/vo/ArBillGenerateQuery.java | 19 + .../ar/domain/vo/ArBillGenerateRequest.java | 34 + .../ruoyi/ar/domain/vo/ArReceiptQuery.java | 16 + .../domain/vo/ArReceiptWriteoffRequest.java | 35 + .../java/com/ruoyi/ar/enums/ArBillStatus.java | 62 ++ .../com/ruoyi/ar/enums/ArExportStatus.java | 21 + .../java/com/ruoyi/ar/enums/ArLogResult.java | 20 + .../com/ruoyi/ar/enums/ArOrderBillStatus.java | 20 + .../com/ruoyi/ar/enums/ArReceiptStatus.java | 22 + .../com/ruoyi/ar/enums/ArWriteoffStatus.java | 20 + .../ruoyi/ar/mapper/ArBankAccountMapper.java | 9 + .../ar/mapper/ArBillDetailFeeMapper.java | 15 + .../ruoyi/ar/mapper/ArBillDetailMapper.java | 16 + .../ruoyi/ar/mapper/ArBillExportMapper.java | 15 + .../com/ruoyi/ar/mapper/ArBillLogMapper.java | 15 + .../com/ruoyi/ar/mapper/ArBillMapper.java | 33 + .../com/ruoyi/ar/mapper/ArReceiptMapper.java | 30 + .../ar/mapper/ArReceiptWriteoffMapper.java | 26 + .../ruoyi/ar/service/IArBillLogService.java | 14 + .../com/ruoyi/ar/service/IArBillService.java | 36 + .../ruoyi/ar/service/IArReceiptService.java | 22 + .../ar/service/impl/ArBillLogServiceImpl.java | 53 + .../ar/service/impl/ArBillServiceImpl.java | 902 ++++++++++++++++++ .../ar/service/impl/ArReceiptServiceImpl.java | 275 ++++++ .../com/ruoyi/price/domain/PrcOrderBill.java | 2 + .../price/mapper/PrcIncomeBillFeeMapper.java | 2 + .../price/mapper/PrcOrderBillMapper.java | 20 + .../mapper/ar/ArBankAccountMapper.xml | 30 + .../mapper/ar/ArBillDetailFeeMapper.xml | 62 ++ .../mapper/ar/ArBillDetailMapper.xml | 109 +++ .../mapper/ar/ArBillExportMapper.xml | 52 + .../resources/mapper/ar/ArBillLogMapper.xml | 76 ++ .../main/resources/mapper/ar/ArBillMapper.xml | 113 +++ .../resources/mapper/ar/ArReceiptMapper.xml | 97 ++ .../mapper/ar/ArReceiptWriteoffMapper.xml | 81 ++ .../mapper/price/PrcIncomeBillFeeMapper.xml | 10 + .../mapper/price/PrcOrderBillMapper.xml | 62 +- .../templates/ar/ar_bill_template.xlsx | Bin 0 -> 11983 bytes bms-back/sql/ar/ar_bank_account.sql | 33 + bms-back/sql/ar/ar_bill.sql | 193 ++++ .../ar/ar_bill_cutoff_status_migration.sql | 142 +++ .../sql/ar/ar_bill_runtime_optimization.sql | 84 ++ bms-back/sql/ar/ar_menu.sql | 57 ++ bms-back/sql/price/bill.sql | 2 + bms-front/src/api/ar/bill.js | 45 + bms-front/src/api/ar/receipt.js | 25 + bms-front/src/components/BasicTable/index.vue | 6 +- bms-front/src/constants/ar/bill.js | 23 + .../views/ar/bill/components/BillDetail.vue | 76 ++ .../ar/bill/components/GenerateBillDialog.vue | 224 +++++ bms-front/src/views/ar/bill/index.vue | 67 ++ bms-front/src/views/ar/bill/log.vue | 30 + bms-front/src/views/ar/receipt/index.vue | 105 ++ 64 files changed, 4103 insertions(+), 4 deletions(-) create mode 100644 bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ar/ArBillController.java create mode 100644 bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ar/ArReceiptController.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBankAccount.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBill.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillDetail.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillDetailFee.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillExport.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillLog.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArReceipt.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArReceiptWriteoff.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillActionRequest.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillGenerateQuery.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillGenerateRequest.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArReceiptQuery.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArReceiptWriteoffRequest.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArBillStatus.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArExportStatus.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArLogResult.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArOrderBillStatus.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArReceiptStatus.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArWriteoffStatus.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBankAccountMapper.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillDetailFeeMapper.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillDetailMapper.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillExportMapper.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillLogMapper.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillMapper.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArReceiptMapper.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArReceiptWriteoffMapper.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/IArBillLogService.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/IArBillService.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/IArReceiptService.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillLogServiceImpl.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillServiceImpl.java create mode 100644 bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArReceiptServiceImpl.java create mode 100644 bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBankAccountMapper.xml create mode 100644 bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillDetailFeeMapper.xml create mode 100644 bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillDetailMapper.xml create mode 100644 bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillExportMapper.xml create mode 100644 bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillLogMapper.xml create mode 100644 bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillMapper.xml create mode 100644 bms-back/ruoyi-system/src/main/resources/mapper/ar/ArReceiptMapper.xml create mode 100644 bms-back/ruoyi-system/src/main/resources/mapper/ar/ArReceiptWriteoffMapper.xml create mode 100644 bms-back/ruoyi-system/src/main/resources/templates/ar/ar_bill_template.xlsx create mode 100644 bms-back/sql/ar/ar_bank_account.sql create mode 100644 bms-back/sql/ar/ar_bill.sql create mode 100644 bms-back/sql/ar/ar_bill_cutoff_status_migration.sql create mode 100644 bms-back/sql/ar/ar_bill_runtime_optimization.sql create mode 100644 bms-back/sql/ar/ar_menu.sql create mode 100644 bms-front/src/api/ar/bill.js create mode 100644 bms-front/src/api/ar/receipt.js create mode 100644 bms-front/src/constants/ar/bill.js create mode 100644 bms-front/src/views/ar/bill/components/BillDetail.vue create mode 100644 bms-front/src/views/ar/bill/components/GenerateBillDialog.vue create mode 100644 bms-front/src/views/ar/bill/index.vue create mode 100644 bms-front/src/views/ar/bill/log.vue create mode 100644 bms-front/src/views/ar/receipt/index.vue diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ar/ArBillController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ar/ArBillController.java new file mode 100644 index 00000000..14ac307e --- /dev/null +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ar/ArBillController.java @@ -0,0 +1,147 @@ +package com.ruoyi.web.controller.ar; + +import java.util.List; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.ar.domain.ArBill; +import com.ruoyi.ar.domain.ArBankAccount; +import com.ruoyi.ar.domain.ArBillExport; +import com.ruoyi.ar.domain.ArBillLog; +import com.ruoyi.ar.domain.vo.ArBillActionRequest; +import com.ruoyi.ar.domain.vo.ArBillGenerateQuery; +import com.ruoyi.ar.domain.vo.ArBillGenerateRequest; +import com.ruoyi.ar.service.IArBillService; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.price.domain.PrcOrderBill; + +/** 应收账单管理。 */ +@RestController +@RequestMapping("/ar/bill") +@Validated +public class ArBillController extends BaseController +{ + @Autowired + private IArBillService billService; + + @PreAuthorize("@ss.hasPermi('ar:bill:list')") + @GetMapping("/list") + public TableDataInfo list(ArBill query) + { + startPage(); + List list = billService.selectArBillList(query); + return getDataTable(list); + } + + @PreAuthorize("@ss.hasPermi('ar:bill:query')") + @GetMapping("/{arBillId}") + public AjaxResult getInfo(@PathVariable Long arBillId) + { + return success(billService.selectArBillById(arBillId)); + } + + /** 查询指定币种的公司默认收款账户,账号返回脱敏值。 */ + @PreAuthorize("@ss.hasPermi('ar:bill:generate')") + @GetMapping("/default-bank-account") + public AjaxResult defaultBankAccount(@RequestParam(required = false, defaultValue = "CNY") String currency) + { + ArBankAccount account = billService.getDefaultBankAccount(currency); + return success(account); + } + + /** 查询生成账单前的订单计费结果。 */ + @PreAuthorize("@ss.hasPermi('ar:bill:generate')") + @GetMapping("/generate/preview") + public TableDataInfo preview(ArBillGenerateQuery query) + { + List list = billService.previewBill(query); + return getDataTable(list); + } + + @Log(title = "应收账单", businessType = BusinessType.INSERT) + @PreAuthorize("@ss.hasPermi('ar:bill:generate')") + @PostMapping("/generate") + public AjaxResult generate(@Valid @RequestBody ArBillGenerateRequest request, + @RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) + { + String requestId = requestId(request.getRequestId(), headerRequestId); + ArBill bill = billService.generateBill(request, getUsername(), requestId); + return success(bill).put("request_id", requestId); + } + + @Log(title = "应收账单", businessType = BusinessType.UPDATE) + @PreAuthorize("@ss.hasPermi('ar:bill:confirm')") + @PutMapping("/{arBillId}/confirm") + public AjaxResult confirm(@PathVariable Long arBillId, @RequestBody(required = false) ArBillActionRequest request, + @RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) + { + String requestId = requestId(request == null ? null : request.getRequestId(), headerRequestId); + return success(billService.confirmBill(arBillId, request, getUsername(), requestId)).put("request_id", requestId); + } + + @Log(title = "应收账单", businessType = BusinessType.UPDATE) + @PreAuthorize("@ss.hasPermi('ar:bill:void')") + @PutMapping("/{arBillId}/void") + public AjaxResult voidBill(@PathVariable Long arBillId, @RequestBody ArBillActionRequest request, + @RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) + { + String requestId = requestId(request == null ? null : request.getRequestId(), headerRequestId); + return success(billService.voidBill(arBillId, request, getUsername(), requestId)).put("request_id", requestId); + } + + @Log(title = "应收账单导出", businessType = BusinessType.EXPORT) + @PreAuthorize("@ss.hasPermi('ar:bill:export')") + @PostMapping("/{arBillId}/export") + public AjaxResult export(@PathVariable Long arBillId, + @RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) + { + String requestId = requestId(null, headerRequestId); + return success(billService.exportBill(arBillId, getUsername(), requestId)).put("request_id", requestId); + } + + @PreAuthorize("@ss.hasPermi('ar:bill:query')") + @GetMapping("/{arBillId}/exports") + public AjaxResult exports(@PathVariable Long arBillId) + { + return success(billService.selectExports(arBillId)); + } + + @PreAuthorize("@ss.hasPermi('ar:bill:export')") + @GetMapping("/export/{exportId}/download") + public void download(@PathVariable Long exportId, HttpServletResponse response) throws Exception + { + billService.downloadExport(exportId, response); + } + + @PreAuthorize("@ss.hasPermi('ar:log:list')") + @GetMapping("/log/list") + public TableDataInfo logs(ArBillLog query) + { + startPage(); + return getDataTable(billService.selectLogList(query)); + } + + private String requestId(String bodyRequestId, String headerRequestId) + { + if (StringUtils.isNotEmpty(bodyRequestId)) return bodyRequestId; + if (StringUtils.isNotEmpty(headerRequestId)) return headerRequestId; + return IdUtils.fastSimpleUUID(); + } +} diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ar/ArReceiptController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ar/ArReceiptController.java new file mode 100644 index 00000000..2d8a94a9 --- /dev/null +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/ar/ArReceiptController.java @@ -0,0 +1,106 @@ +package com.ruoyi.web.controller.ar; + +import java.util.List; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.ar.domain.ArReceipt; +import com.ruoyi.ar.domain.ArReceiptWriteoff; +import com.ruoyi.ar.domain.vo.ArBillActionRequest; +import com.ruoyi.ar.domain.vo.ArReceiptQuery; +import com.ruoyi.ar.domain.vo.ArReceiptWriteoffRequest; +import com.ruoyi.ar.service.IArReceiptService; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.uuid.IdUtils; + +/** 应收收款与核销管理。 */ +@RestController +@RequestMapping("/ar/receipt") +@Validated +public class ArReceiptController extends BaseController +{ + @Autowired + private IArReceiptService receiptService; + + @PreAuthorize("@ss.hasPermi('ar:receipt:list')") + @GetMapping("/list") + public TableDataInfo list(ArReceiptQuery query) + { + startPage(); + return getDataTable(receiptService.selectReceiptList(query)); + } + + @PreAuthorize("@ss.hasPermi('ar:receipt:list')") + @GetMapping("/{receiptId}/writeoffs") + public AjaxResult writeoffs(@PathVariable Long receiptId) + { + return success(receiptService.selectWriteoffs(receiptId)); + } + + @Log(title = "应收收款", businessType = BusinessType.INSERT) + @PreAuthorize("@ss.hasPermi('ar:receipt:add')") + @PostMapping + public AjaxResult add(@Valid @RequestBody ArReceipt receipt, + @RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) + { + String requestId = requestId(headerRequestId); + return success(receiptService.registerReceipt(receipt, getUsername(), requestId)).put("request_id", requestId); + } + + @Log(title = "应收收款", businessType = BusinessType.UPDATE) + @PreAuthorize("@ss.hasPermi('ar:receipt:cancel')") + @PutMapping("/{receiptId}/cancel") + public AjaxResult cancelReceipt(@PathVariable Long receiptId, @RequestBody(required = false) ArBillActionRequest request, + @RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) + { + String requestId = requestId(request == null ? null : request.getRequestId(), headerRequestId); + receiptService.cancelReceipt(receiptId, request == null ? null : request.getRemark(), getUsername(), requestId); + return success().put("request_id", requestId); + } + + @Log(title = "应收收款核销", businessType = BusinessType.UPDATE) + @PreAuthorize("@ss.hasPermi('ar:receipt:writeoff')") + @PostMapping("/writeoff") + public AjaxResult writeoff(@Valid @RequestBody ArReceiptWriteoffRequest request, + @RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) + { + String requestId = StringUtils.isNotEmpty(request.getRequestId()) ? request.getRequestId() : requestId(headerRequestId); + List rows = receiptService.writeoff(request, getUsername(), requestId); + return success(rows).put("request_id", requestId); + } + + @Log(title = "应收收款核销", businessType = BusinessType.UPDATE) + @PreAuthorize("@ss.hasPermi('ar:receipt:cancel')") + @PutMapping("/writeoff/{writeoffId}/cancel") + public AjaxResult cancel(@PathVariable Long writeoffId, @RequestBody(required = false) ArBillActionRequest request, + @RequestHeader(value = "X-Request-Id", required = false) String headerRequestId) + { + String requestId = requestId(request == null ? null : request.getRequestId(), headerRequestId); + receiptService.cancelWriteoff(writeoffId, request == null ? null : request.getRemark(), getUsername(), requestId); + return success().put("request_id", requestId); + } + + private String requestId(String headerRequestId) + { + return StringUtils.isNotEmpty(headerRequestId) ? headerRequestId : IdUtils.fastSimpleUUID(); + } + + private String requestId(String bodyRequestId, String headerRequestId) + { + return StringUtils.isNotEmpty(bodyRequestId) ? bodyRequestId : requestId(headerRequestId); + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBankAccount.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBankAccount.java new file mode 100644 index 00000000..de92e875 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBankAccount.java @@ -0,0 +1,28 @@ +package com.ruoyi.ar.domain; + +import java.io.Serializable; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +/** 公司收款账户配置。 */ +@Data +public class ArBankAccount implements Serializable +{ + private static final long serialVersionUID = 1L; + + private Long bankAccountId; + private String accountName; + private String accountNo; + private String bankName; + private String currency; + private String isDefault; + private String status; + private String remark; + private String createBy; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + private String updateBy; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBill.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBill.java new file mode 100644 index 00000000..0f12d83e --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBill.java @@ -0,0 +1,48 @@ +package com.ruoyi.ar.domain; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** 正式应收账单。 */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArBill extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + private Long arBillId; + private String billNo; + private String billName; + private Long customerId; + private String customerCode; + private String customerName; + @JsonFormat(pattern = "yyyy-MM-dd") + private Date billPeriodStart; + @JsonFormat(pattern = "yyyy-MM-dd") + private Date billPeriodEnd; + @JsonFormat(pattern = "yyyy-MM-dd") + private Date billDate; + private String currency; + private Integer orderCount; + private BigDecimal totalAmount; + private BigDecimal writtenOffAmount; + private BigDecimal unwrittenAmount; + private String status; + private Integer billVersion; + private String bankAccountName; + private String bankAccountNo; + private String bankName; + private String remark; + private String confirmedBy; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date confirmedTime; + + private List details; + private List exports; + private List logs; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillDetail.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillDetail.java new file mode 100644 index 00000000..7b2b7097 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillDetail.java @@ -0,0 +1,43 @@ +package com.ruoyi.ar.domain; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +/** 账单订单快照。 */ +@Data +public class ArBillDetail implements Serializable +{ + private static final long serialVersionUID = 1L; + + private Long detailId; + private Long arBillId; + private String prcBillId; + private Integer detailNo; + private String orderNo; + private String trackingNo; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date receiveTime; + private String productCode; + private String productName; + private String goodsDescription; + private String countryCode; + private String countryName; + private String destPostcode; + private BigDecimal actualWeight; + private BigDecimal volumeWeight; + private BigDecimal chargeWeight; + private BigDecimal pkgLength; + private BigDecimal pkgWidth; + private BigDecimal pkgHeight; + private String currency; + private BigDecimal totalAmount; + private String remark; + private String createBy; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + private List fees; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillDetailFee.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillDetailFee.java new file mode 100644 index 00000000..849e5464 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillDetailFee.java @@ -0,0 +1,33 @@ +package com.ruoyi.ar.domain; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +/** 账单费用快照。 */ +@Data +public class ArBillDetailFee implements Serializable +{ + private static final long serialVersionUID = 1L; + + private Long detailFeeId; + private Long detailId; + private String prcFeeId; + private Integer feeNo; + private String feeType; + private String feeName; + private Long scId; + private BigDecimal quantity; + private String unit; + private BigDecimal unitPrice; + private BigDecimal amount; + private String currency; + private String calculationFormula; + private String roundMode; + private String remark; + private String createBy; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillExport.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillExport.java new file mode 100644 index 00000000..fd09b927 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillExport.java @@ -0,0 +1,29 @@ +package com.ruoyi.ar.domain; + +import java.io.Serializable; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +/** 账单导出文件元数据。 */ +@Data +public class ArBillExport implements Serializable +{ + private static final long serialVersionUID = 1L; + + private Long exportId; + private Long arBillId; + private String fileName; + private String fileType; + private String templateCode; + private String templateVersion; + private String storageKey; + private Long fileSize; + private String fileHash; + private Integer rowCount; + private String exportStatus; + private String exportBy; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date exportTime; + private String remark; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillLog.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillLog.java new file mode 100644 index 00000000..caca2a32 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArBillLog.java @@ -0,0 +1,36 @@ +package com.ruoyi.ar.domain; + +import java.io.Serializable; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +/** 应收模块追加式审计日志。 */ +@Data +public class ArBillLog implements Serializable +{ + private static final long serialVersionUID = 1L; + + private Long logId; + private Long arBillId; + private String objectType; + private String objectId; + /** 对象业务标识,例如账单号、收款单号或核销单号。 */ + private String objectDisplay; + private String operationType; + private String operationDesc; + private String fromStatus; + private String toStatus; + private String beforeData; + private String afterData; + private String operatorId; + private String operatorName; + private String operationSource; + private String requestId; + private String traceId; + private String result; + private String errorMessage; + private String clientIp; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArReceipt.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArReceipt.java new file mode 100644 index 00000000..260f90ac --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArReceipt.java @@ -0,0 +1,33 @@ +package com.ruoyi.ar.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** 应收真实收款。 */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArReceipt extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + private Long receiptId; + private String receiptNo; + private Long customerId; + /** 客户名称,仅用于列表展示。 */ + private String customerName; + /** 客户编码,仅用于列表展示。 */ + private String customerCode; + @JsonFormat(pattern = "yyyy-MM-dd") + private Date receiptDate; + private String currency; + private BigDecimal receiptAmount; + private BigDecimal unallocatedAmount; + private String paymentMethod; + private String bankReference; + private String status; + private String remark; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArReceiptWriteoff.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArReceiptWriteoff.java new file mode 100644 index 00000000..378a2ce4 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/ArReceiptWriteoff.java @@ -0,0 +1,35 @@ +package com.ruoyi.ar.domain; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +/** 收款与账单的核销关系。 */ +@Data +public class ArReceiptWriteoff implements Serializable +{ + private static final long serialVersionUID = 1L; + + private Long writeoffId; + private Long receiptId; + private Long arBillId; + /** 关联账单号,仅用于核销记录展示。 */ + private String billNo; + /** 关联收款单号,仅用于核销记录展示。 */ + private String receiptNo; + private String writeoffNo; + private BigDecimal writeoffAmount; + private String currency; + @JsonFormat(pattern = "yyyy-MM-dd") + private Date writeoffDate; + private String status; + private String remark; + private String createBy; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + private String cancelBy; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date cancelTime; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillActionRequest.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillActionRequest.java new file mode 100644 index 00000000..f5d005ea --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillActionRequest.java @@ -0,0 +1,11 @@ +package com.ruoyi.ar.domain.vo; + +import lombok.Data; + +/** 账单状态动作参数。 */ +@Data +public class ArBillActionRequest +{ + private String remark; + private String requestId; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillGenerateQuery.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillGenerateQuery.java new file mode 100644 index 00000000..e3a11149 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillGenerateQuery.java @@ -0,0 +1,19 @@ +package com.ruoyi.ar.domain.vo; + +import java.util.Date; +import org.springframework.format.annotation.DateTimeFormat; +import lombok.Data; + +/** 生成应收账单前的订单计费结果查询条件。 */ +@Data +public class ArBillGenerateQuery +{ + private Long customerId; + + private String customerCode; + + @DateTimeFormat(pattern = "yyyy-MM-dd") + private Date billPeriodEnd; + + private String currency; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillGenerateRequest.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillGenerateRequest.java new file mode 100644 index 00000000..a85dd987 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArBillGenerateRequest.java @@ -0,0 +1,34 @@ +package com.ruoyi.ar.domain.vo; + +import java.util.Date; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** 从订单计费结果生成正式账单的参数。 */ +@Data +public class ArBillGenerateRequest +{ + @NotNull(message = "客户不能为空") + private Long customerId; + @NotBlank(message = "账单名称不能为空") + private String billName; + private String customerCode; + private String customerName; + @NotNull(message = "账单截止日不能为空") + @JsonFormat(pattern = "yyyy-MM-dd") + private Date billPeriodEnd; + @NotEmpty(message = "请至少选择一条订单") + private List sourceBillIds; + @JsonFormat(pattern = "yyyy-MM-dd") + private Date billDate; + private String currency = "CNY"; + private String bankAccountName; + private String bankAccountNo; + private String bankName; + private String remark; + private String requestId; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArReceiptQuery.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArReceiptQuery.java new file mode 100644 index 00000000..f108dac5 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArReceiptQuery.java @@ -0,0 +1,16 @@ +package com.ruoyi.ar.domain.vo; + +import java.util.Date; +import com.ruoyi.ar.domain.ArReceipt; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** 收款查询参数。 */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ArReceiptQuery extends ArReceipt +{ + private static final long serialVersionUID = 1L; + private Date receiptDateStart; + private Date receiptDateEnd; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArReceiptWriteoffRequest.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArReceiptWriteoffRequest.java new file mode 100644 index 00000000..dfa6239a --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/domain/vo/ArReceiptWriteoffRequest.java @@ -0,0 +1,35 @@ +package com.ruoyi.ar.domain.vo; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** 批量收款核销参数。 */ +@Data +public class ArReceiptWriteoffRequest +{ + @NotNull(message = "收款ID不能为空") + private Long receiptId; + @NotEmpty(message = "至少选择一张账单") + @Valid + private List allocations; + @JsonFormat(pattern = "yyyy-MM-dd") + private Date writeoffDate; + private String currency; + private String remark; + private String requestId; + + @Data + public static class Allocation + { + @NotNull(message = "账单ID不能为空") + private Long arBillId; + @NotNull(message = "核销金额不能为空") + private BigDecimal amount; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArBillStatus.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArBillStatus.java new file mode 100644 index 00000000..a4d83c8b --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArBillStatus.java @@ -0,0 +1,62 @@ +package com.ruoyi.ar.enums; + +/** 应收账单状态。数据库保存值使用 code,避免业务代码散落状态字符串。 */ +public enum ArBillStatus +{ + DRAFT("DRAFT", "草稿"), + CONFIRMED("CONFIRMED", "已确认"), + PARTIAL_WRITTEN_OFF("PARTIAL_WRITTEN_OFF", "部分核销"), + WRITTEN_OFF("WRITTEN_OFF", "已核销"), + VOID("VOID", "已作废"); + + private final String code; + private final String label; + + ArBillStatus(String code, String label) + { + this.code = code; + this.label = label; + } + + public String getCode() + { + return code; + } + + public String getLabel() + { + return label; + } + + public boolean canConfirm() + { + return this == DRAFT; + } + + public boolean canVoid() + { + return this == DRAFT || this == CONFIRMED; + } + + public boolean canWriteOff() + { + return this == CONFIRMED || this == PARTIAL_WRITTEN_OFF; + } + + public static ArBillStatus of(String code) + { + for (ArBillStatus status : values()) + { + if (status.code.equalsIgnoreCase(code)) + { + return status; + } + } + return null; + } + + public static boolean is(String code, ArBillStatus status) + { + return status != null && status.code.equalsIgnoreCase(code); + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArExportStatus.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArExportStatus.java new file mode 100644 index 00000000..86ee5267 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArExportStatus.java @@ -0,0 +1,21 @@ +package com.ruoyi.ar.enums; + +/** 应收账单导出任务状态。 */ +public enum ArExportStatus +{ + PROCESSING("PROCESSING"), + SUCCESS("SUCCESS"), + FAILED("FAILED"); + + private final String code; + + ArExportStatus(String code) + { + this.code = code; + } + + public String getCode() + { + return code; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArLogResult.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArLogResult.java new file mode 100644 index 00000000..f65c9d8d --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArLogResult.java @@ -0,0 +1,20 @@ +package com.ruoyi.ar.enums; + +/** 应收操作日志结果。 */ +public enum ArLogResult +{ + SUCCESS("SUCCESS"), + FAILED("FAILED"); + + private final String code; + + ArLogResult(String code) + { + this.code = code; + } + + public String getCode() + { + return code; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArOrderBillStatus.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArOrderBillStatus.java new file mode 100644 index 00000000..e165236b --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArOrderBillStatus.java @@ -0,0 +1,20 @@ +package com.ruoyi.ar.enums; + +/** 计费订单的应收账单占用状态。 */ +public enum ArOrderBillStatus +{ + UNBILLED("UNBILLED"), + BILLED("BILLED"); + + private final String code; + + ArOrderBillStatus(String code) + { + this.code = code; + } + + public String getCode() + { + return code; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArReceiptStatus.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArReceiptStatus.java new file mode 100644 index 00000000..026b6fb4 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArReceiptStatus.java @@ -0,0 +1,22 @@ +package com.ruoyi.ar.enums; + +/** 应收收款状态。 */ +public enum ArReceiptStatus +{ + REGISTERED("REGISTERED"), + PARTIAL_WRITTEN_OFF("PARTIAL_WRITTEN_OFF"), + WRITTEN_OFF("WRITTEN_OFF"), + CANCELLED("CANCELLED"); + + private final String code; + + ArReceiptStatus(String code) + { + this.code = code; + } + + public String getCode() + { + return code; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArWriteoffStatus.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArWriteoffStatus.java new file mode 100644 index 00000000..951c399b --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/enums/ArWriteoffStatus.java @@ -0,0 +1,20 @@ +package com.ruoyi.ar.enums; + +/** 收款核销状态。 */ +public enum ArWriteoffStatus +{ + EFFECTIVE("EFFECTIVE"), + CANCELLED("CANCELLED"); + + private final String code; + + ArWriteoffStatus(String code) + { + this.code = code; + } + + public String getCode() + { + return code; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBankAccountMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBankAccountMapper.java new file mode 100644 index 00000000..1174f854 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBankAccountMapper.java @@ -0,0 +1,9 @@ +package com.ruoyi.ar.mapper; + +import org.apache.ibatis.annotations.Param; +import com.ruoyi.ar.domain.ArBankAccount; + +public interface ArBankAccountMapper +{ + ArBankAccount selectDefaultByCurrency(@Param("currency") String currency); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillDetailFeeMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillDetailFeeMapper.java new file mode 100644 index 00000000..dc5c2431 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillDetailFeeMapper.java @@ -0,0 +1,15 @@ +package com.ruoyi.ar.mapper; + +import java.util.List; +import com.ruoyi.ar.domain.ArBillDetailFee; + +public interface ArBillDetailFeeMapper +{ + List selectByDetailId(Long detailId); + + List selectByBillId(Long arBillId); + + int insert(ArBillDetailFee fee); + + int insertBatch(List fees); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillDetailMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillDetailMapper.java new file mode 100644 index 00000000..7e532815 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillDetailMapper.java @@ -0,0 +1,16 @@ +package com.ruoyi.ar.mapper; + +import java.util.List; +import com.ruoyi.ar.domain.ArBillDetail; + +public interface ArBillDetailMapper +{ + List selectByBillId(Long arBillId); + + /** 查询账单明细及费用明细,避免详情页逐层查询。 */ + List selectByBillIdWithFees(Long arBillId); + + int insert(ArBillDetail detail); + + int insertBatch(List details); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillExportMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillExportMapper.java new file mode 100644 index 00000000..07539830 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillExportMapper.java @@ -0,0 +1,15 @@ +package com.ruoyi.ar.mapper; + +import java.util.List; +import com.ruoyi.ar.domain.ArBillExport; + +public interface ArBillExportMapper +{ + List selectByBillId(Long arBillId); + + ArBillExport selectById(Long exportId); + + ArBillExport selectLatestSuccessByBillId(Long arBillId); + + int insert(ArBillExport export); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillLogMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillLogMapper.java new file mode 100644 index 00000000..280257b8 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillLogMapper.java @@ -0,0 +1,15 @@ +package com.ruoyi.ar.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.ar.domain.ArBillLog; + +public interface ArBillLogMapper +{ + List selectList(ArBillLog query); + + ArBillLog selectSuccessByRequestId(@Param("requestId") String requestId, + @Param("objectType") String objectType); + + int insert(ArBillLog log); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillMapper.java new file mode 100644 index 00000000..885133ee --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArBillMapper.java @@ -0,0 +1,33 @@ +package com.ruoyi.ar.mapper; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.ar.domain.ArBill; + +public interface ArBillMapper +{ + List selectArBillList(ArBill query); + + ArBill selectArBillById(Long arBillId); + + ArBill selectArBillByBillNo(String billNo); + + Integer selectMaxBillVersion(@Param("customerId") Long customerId, + @Param("cutoffDate") Date cutoffDate, @Param("currency") String currency); + + ArBill selectArBillByIdForUpdate(Long arBillId); + + int insertArBill(ArBill bill); + + int updateArBillStatus(@Param("arBillId") Long arBillId, @Param("status") String status, + @Param("remark") String remark, @Param("operator") String operator, + @Param("fromStatus") String fromStatus); + + int updateArBillAmounts(@Param("arBillId") Long arBillId, + @Param("writtenOffAmount") BigDecimal writtenOffAmount, + @Param("unwrittenAmount") BigDecimal unwrittenAmount, + @Param("status") String status, @Param("operator") String operator, + @Param("fromStatus") String fromStatus); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArReceiptMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArReceiptMapper.java new file mode 100644 index 00000000..ac4e3b6a --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArReceiptMapper.java @@ -0,0 +1,30 @@ +package com.ruoyi.ar.mapper; + +import java.math.BigDecimal; +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.ar.domain.ArReceipt; +import com.ruoyi.ar.domain.vo.ArReceiptQuery; + +public interface ArReceiptMapper +{ + List selectArReceiptList(ArReceiptQuery query); + + ArReceipt selectArReceiptById(Long receiptId); + + ArReceipt selectArReceiptByIdForUpdate(Long receiptId); + + ArReceipt selectArReceiptByNo(String receiptNo); + + ArReceipt selectArReceiptByReference(@Param("paymentMethod") String paymentMethod, + @Param("bankReference") String bankReference); + + int insertArReceipt(ArReceipt receipt); + + int updateBalanceAndStatus(@Param("receiptId") Long receiptId, + @Param("unallocatedAmount") BigDecimal unallocatedAmount, + @Param("status") String status, @Param("operator") String operator); + + int updateCancelled(@Param("receiptId") Long receiptId, @Param("operator") String operator, + @Param("remark") String remark); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArReceiptWriteoffMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArReceiptWriteoffMapper.java new file mode 100644 index 00000000..21a1ece1 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/mapper/ArReceiptWriteoffMapper.java @@ -0,0 +1,26 @@ +package com.ruoyi.ar.mapper; + +import java.math.BigDecimal; +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.ar.domain.ArReceiptWriteoff; + +public interface ArReceiptWriteoffMapper +{ + List selectByBillId(Long arBillId); + + List selectByReceiptId(Long receiptId); + + ArReceiptWriteoff selectById(Long writeoffId); + + ArReceiptWriteoff selectByIdForUpdate(Long writeoffId); + + int insert(ArReceiptWriteoff writeoff); + + int updateCancelled(@Param("writeoffId") Long writeoffId, @Param("cancelBy") String cancelBy, + @Param("remark") String remark); + + BigDecimal sumEffectiveByBillId(Long arBillId); + + BigDecimal sumEffectiveByReceiptId(Long receiptId); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/IArBillLogService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/IArBillLogService.java new file mode 100644 index 00000000..94e9ecbc --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/IArBillLogService.java @@ -0,0 +1,14 @@ +package com.ruoyi.ar.service; + +import java.util.List; +import com.ruoyi.ar.domain.ArBillLog; + +public interface IArBillLogService +{ + List selectLogList(ArBillLog query); + + void record(ArBillLog log); + + void recordFailure(Long arBillId, String objectType, String objectId, String operationType, + String fromStatus, String message, String operator, String requestId); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/IArBillService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/IArBillService.java new file mode 100644 index 00000000..0ffe36d7 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/IArBillService.java @@ -0,0 +1,36 @@ +package com.ruoyi.ar.service; + +import java.util.List; +import com.ruoyi.ar.domain.ArBill; +import com.ruoyi.ar.domain.ArBankAccount; +import com.ruoyi.ar.domain.ArBillExport; +import com.ruoyi.ar.domain.ArBillLog; +import com.ruoyi.ar.domain.vo.ArBillGenerateRequest; +import com.ruoyi.ar.domain.vo.ArBillGenerateQuery; +import com.ruoyi.ar.domain.vo.ArBillActionRequest; +import com.ruoyi.price.domain.PrcOrderBill; + +public interface IArBillService +{ + List selectArBillList(ArBill query); + + ArBill selectArBillById(Long arBillId); + + ArBankAccount getDefaultBankAccount(String currency); + + List previewBill(ArBillGenerateQuery query); + + ArBill generateBill(ArBillGenerateRequest request, String operator, String requestId); + + ArBill confirmBill(Long arBillId, ArBillActionRequest request, String operator, String requestId); + + ArBill voidBill(Long arBillId, ArBillActionRequest request, String operator, String requestId); + + ArBillExport exportBill(Long arBillId, String operator, String requestId); + + List selectExports(Long arBillId); + + void downloadExport(Long exportId, jakarta.servlet.http.HttpServletResponse response) throws Exception; + + List selectLogList(ArBillLog query); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/IArReceiptService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/IArReceiptService.java new file mode 100644 index 00000000..3fe46f7e --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/IArReceiptService.java @@ -0,0 +1,22 @@ +package com.ruoyi.ar.service; + +import java.util.List; +import com.ruoyi.ar.domain.ArReceipt; +import com.ruoyi.ar.domain.ArReceiptWriteoff; +import com.ruoyi.ar.domain.vo.ArReceiptQuery; +import com.ruoyi.ar.domain.vo.ArReceiptWriteoffRequest; + +public interface IArReceiptService +{ + List selectReceiptList(ArReceiptQuery query); + + List selectWriteoffs(Long receiptId); + + ArReceipt registerReceipt(ArReceipt receipt, String operator, String requestId); + + void cancelReceipt(Long receiptId, String remark, String operator, String requestId); + + List writeoff(ArReceiptWriteoffRequest request, String operator, String requestId); + + void cancelWriteoff(Long writeoffId, String remark, String operator, String requestId); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillLogServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillLogServiceImpl.java new file mode 100644 index 00000000..99386065 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillLogServiceImpl.java @@ -0,0 +1,53 @@ +package com.ruoyi.ar.service.impl; + +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.ar.domain.ArBillLog; +import com.ruoyi.ar.enums.ArLogResult; +import com.ruoyi.ar.mapper.ArBillLogMapper; +import com.ruoyi.ar.service.IArBillLogService; +import com.ruoyi.common.utils.uuid.IdUtils; + +@Service +public class ArBillLogServiceImpl implements IArBillLogService +{ + @Autowired + private ArBillLogMapper logMapper; + + @Override + public List selectLogList(ArBillLog query) + { + return logMapper.selectList(query); + } + + @Override + public void record(ArBillLog log) + { + logMapper.insert(log); + } + + /** 失败日志必须独立提交,不能随业务异常一起回滚。 */ + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void recordFailure(Long arBillId, String objectType, String objectId, String operationType, + String fromStatus, String message, String operator, String requestId) + { + ArBillLog log = new ArBillLog(); + log.setArBillId(arBillId); + log.setObjectType(objectType); + log.setObjectId(objectId); + log.setOperationType(operationType); + log.setOperationDesc(message); + log.setFromStatus(fromStatus); + log.setOperatorId(operator); + log.setOperatorName(operator); + log.setOperationSource("WEB"); + log.setRequestId(requestId == null ? IdUtils.fastSimpleUUID() : requestId); + log.setResult(ArLogResult.FAILED.getCode()); + log.setErrorMessage(message); + logMapper.insert(log); + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillServiceImpl.java new file mode 100644 index 00000000..7f0fac01 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArBillServiceImpl.java @@ -0,0 +1,902 @@ +package com.ruoyi.ar.service.impl; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.ar.domain.ArBill; +import com.ruoyi.ar.domain.ArBankAccount; +import com.ruoyi.ar.domain.ArBillDetail; +import com.ruoyi.ar.domain.ArBillDetailFee; +import com.ruoyi.ar.domain.ArBillExport; +import com.ruoyi.ar.domain.ArBillLog; +import com.ruoyi.ar.enums.ArBillStatus; +import com.ruoyi.ar.enums.ArExportStatus; +import com.ruoyi.ar.enums.ArLogResult; +import com.ruoyi.ar.enums.ArOrderBillStatus; +import com.ruoyi.ar.mapper.ArBillDetailFeeMapper; +import com.ruoyi.ar.mapper.ArBillDetailMapper; +import com.ruoyi.ar.mapper.ArBillExportMapper; +import com.ruoyi.ar.mapper.ArBillLogMapper; +import com.ruoyi.ar.mapper.ArBillMapper; +import com.ruoyi.ar.mapper.ArBankAccountMapper; +import com.ruoyi.ar.service.IArBillLogService; +import com.ruoyi.ar.service.IArBillService; +import com.ruoyi.ar.domain.vo.ArBillActionRequest; +import com.ruoyi.ar.domain.vo.ArBillGenerateQuery; +import com.ruoyi.ar.domain.vo.ArBillGenerateRequest; +import com.ruoyi.common.config.RuoYiConfig; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.FileUtils; +import com.ruoyi.common.utils.file.FileUploadUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.price.domain.PrcIncomeBillFee; +import com.ruoyi.price.domain.PrcOrderBill; +import com.ruoyi.price.mapper.PrcIncomeBillFeeMapper; +import com.ruoyi.price.mapper.PrcOrderBillMapper; + +@Service +public class ArBillServiceImpl implements IArBillService +{ + private static final String EXPORT_TEMPLATE_RESOURCE = "templates/ar/ar_bill_template.xlsx"; + private static final int TEMPLATE_DATA_START_ROW = 4; + private static final int TEMPLATE_FOOTER_START_ROW = 5; + private static final int TEMPLATE_COLUMN_COUNT = 20; + + @Autowired private ArBillMapper billMapper; + @Autowired private ArBankAccountMapper bankAccountMapper; + @Autowired private ArBillDetailMapper detailMapper; + @Autowired private ArBillDetailFeeMapper detailFeeMapper; + @Autowired private ArBillExportMapper exportMapper; + @Autowired private ArBillLogMapper logMapper; + @Autowired private IArBillLogService logService; + @Autowired private PrcOrderBillMapper sourceBillMapper; + @Autowired private PrcIncomeBillFeeMapper sourceFeeMapper; + + @Override + public List selectArBillList(ArBill query) + { + List bills = billMapper.selectArBillList(query); + for (ArBill bill : bills) + { + bill.setBankAccountNo(maskAccount(bill.getBankAccountNo())); + } + return bills; + } + + @Override + public ArBill selectArBillById(Long arBillId) + { + ArBill bill = billMapper.selectArBillById(arBillId); + if (bill == null) + { + throw new ServiceException("应收账单不存在"); + } + fillDetails(bill); + bill.setBankAccountNo(maskAccount(bill.getBankAccountNo())); + bill.setExports(exportMapper.selectByBillId(arBillId)); + return bill; + } + + @Override + public ArBankAccount getDefaultBankAccount(String currency) + { + ArBankAccount account = bankAccountMapper.selectDefaultByCurrency( + StringUtils.isEmpty(currency) ? "CNY" : currency); + if (account != null) + { + account.setAccountNo(maskAccount(account.getAccountNo())); + } + return account; + } + + @Override + public List previewBill(ArBillGenerateQuery query) + { + validatePreviewQuery(query); + return sourceBillMapper.selectPrcOrderBillByCustomerPeriod(query.getCustomerId(), + query.getCustomerCode(), query.getBillPeriodEnd(), query.getCurrency(), ArOrderBillStatus.UNBILLED.getCode()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ArBill generateBill(ArBillGenerateRequest request, String operator, String requestId) + { + validatePeriod(request); + ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "BILL"); + if (previous != null && isNumber(previous.getObjectId())) + { + return selectArBillById(Long.valueOf(previous.getObjectId())); + } + + List requestedSourceBillIds = normalizeSourceBillIds(request.getSourceBillIds()); + if (requestedSourceBillIds.isEmpty()) + { + throw failure(null, "BILL", "", "GENERATE", null, "请至少选择一条有效订单", operator, requestId); + } + String requestedCurrency = StringUtils.isEmpty(request.getCurrency()) ? "CNY" : request.getCurrency(); + List sources = sourceBillMapper.selectPrcOrderBillByCustomerCutoffAndIds( + request.getCustomerId(), request.getCustomerCode(), request.getBillPeriodEnd(), requestedCurrency, + requestedSourceBillIds, ArOrderBillStatus.UNBILLED.getCode()); + if (sources == null || sources.isEmpty() || sources.size() != requestedSourceBillIds.size()) + { + throw failure(null, "BILL", "", "GENERATE", null, + "部分所选订单已出账、已变更或不符合当前筛选条件,请重新查询后生成", operator, requestId); + } + + String currency = requestedCurrency; + BigDecimal total = BigDecimal.ZERO; + for (PrcOrderBill source : sources) + { + if (source.getCurrency() != null && !currency.equalsIgnoreCase(source.getCurrency())) + { + throw failure(null, "BILL", "", "GENERATE", null, "截止日期内存在多个币种,不能合并为一张账单", operator, requestId); + } + total = total.add(nvl(source.getIncomeTotal())); + } + + ArBankAccount bankAccount = bankAccountMapper.selectDefaultByCurrency(currency); + if (bankAccount == null) + { + throw failure(null, "BILL", "", "GENERATE", null, + "未配置 " + currency + " 币种的公司默认收款账户,请先完成收款账户配置", operator, requestId); + } + + ArBill bill = new ArBill(); + bill.setBillNo("AR" + new SimpleDateFormat("yyyyMM").format(new Date()) + + IdUtils.fastSimpleUUID().substring(0, 8).toUpperCase()); + bill.setBillName(request.getBillName().trim()); + bill.setCustomerId(request.getCustomerId()); + bill.setCustomerCode(StringUtils.isEmpty(request.getCustomerCode()) ? sources.get(0).getCustomerCode() : request.getCustomerCode()); + bill.setCustomerName(StringUtils.isEmpty(request.getCustomerName()) ? sources.get(0).getCustomerName() : request.getCustomerName()); + bill.setBillPeriodStart(earliestBizDate(sources)); + bill.setBillPeriodEnd(request.getBillPeriodEnd()); + bill.setBillDate(request.getBillDate() == null ? new Date() : request.getBillDate()); + bill.setCurrency(currency); + bill.setOrderCount(sources.size()); + bill.setTotalAmount(total); + bill.setWrittenOffAmount(BigDecimal.ZERO); + bill.setUnwrittenAmount(total); + bill.setStatus(ArBillStatus.DRAFT.getCode()); + bill.setBankAccountName(bankAccount.getAccountName()); + bill.setBankAccountNo(bankAccount.getAccountNo()); + bill.setBankName(bankAccount.getBankName()); + bill.setRemark(request.getRemark()); + bill.setCreateBy(operator); + bill.setUpdateBy(operator); + + List sourceBillIds = new ArrayList<>(); + for (PrcOrderBill source : sources) + { + sourceBillIds.add(source.getBillId()); + } + int claimedCount = sourceBillMapper.claimArBillOrders(sourceBillIds, operator, + ArOrderBillStatus.UNBILLED.getCode(), ArOrderBillStatus.BILLED.getCode()); + if (claimedCount != sourceBillIds.size()) + { + throw failure(null, "BILL", "", "GENERATE", null, + "部分计费订单已被其他账单占用,请重新查询后生成", operator, requestId); + } + + // 订单占用成功后再锁定版本号,避免并发生成时先查到同一个版本。 + Integer maxVersion = billMapper.selectMaxBillVersion(request.getCustomerId(), request.getBillPeriodEnd(), currency); + bill.setBillVersion((maxVersion == null ? 0 : maxVersion) + 1); + + billMapper.insertArBill(bill); + + List details = new ArrayList<>(); + int detailNo = 1; + for (PrcOrderBill source : sources) + { + details.add(toDetail(bill, source, detailNo++, operator)); + } + detailMapper.insertBatch(details); + + // Batch insert does not need to expose generated IDs to the caller. Re-read the + // persisted details once, then attach source fees in one batch query. + Map detailsBySourceBill = new HashMap<>(); + for (ArBillDetail detail : detailMapper.selectByBillId(bill.getArBillId())) + { + detailsBySourceBill.put(detail.getPrcBillId(), detail); + } + List detailFees = new ArrayList<>(); + List sourceFees = sourceBillIds.isEmpty() + ? new ArrayList() : sourceFeeMapper.selectByBillIds(sourceBillIds); + Map feeNumbers = new HashMap<>(); + for (PrcIncomeBillFee sourceFee : sourceFees) + { + ArBillDetail detail = detailsBySourceBill.get(sourceFee.getBillId()); + if (detail == null) + { + throw new ServiceException("来源费用找不到对应的账单明细:" + sourceFee.getBillId()); + } + int feeNo = feeNumbers.merge(sourceFee.getBillId(), 1, Integer::sum); + detailFees.add(toDetailFee(detail, sourceFee, feeNo, operator)); + } + if (!detailFees.isEmpty()) + { + detailFeeMapper.insertBatch(detailFees); + } + recordSuccess(bill, "CREATE", "生成正式账单", null, ArBillStatus.DRAFT.getCode(), requestId, operator); + bill.setBankAccountNo(maskAccount(bill.getBankAccountNo())); + return bill; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ArBill confirmBill(Long arBillId, ArBillActionRequest request, String operator, String requestId) + { + ArBill bill = billMapper.selectArBillByIdForUpdate(arBillId); + if (bill == null) + { + throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", null, "应收账单不存在", operator, requestId); + } + ArBillStatus currentStatus = ArBillStatus.of(bill.getStatus()); + if (currentStatus == null || !currentStatus.canConfirm()) + { + throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", bill.getStatus(), + "只有草稿账单可以确认,当前状态为 " + bill.getStatus(), operator, requestId); + } + if (!validateDetails(arBillId, bill.getTotalAmount())) + { + throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", bill.getStatus(), + "账单费用明细汇总与订单或账单总额不一致", operator, requestId); + } + if (billMapper.updateArBillStatus(arBillId, ArBillStatus.CONFIRMED.getCode(), + StringUtils.isEmpty(request == null ? null : request.getRemark()) ? bill.getRemark() : request.getRemark(), + operator, ArBillStatus.DRAFT.getCode()) != 1) + { + throw failure(arBillId, "BILL", String.valueOf(arBillId), "CONFIRM", bill.getStatus(), + "账单状态已发生变化,请刷新后重试", operator, requestId); + } + recordSuccess(bill, "CONFIRM", "确认应收账单", ArBillStatus.DRAFT.getCode(), ArBillStatus.CONFIRMED.getCode(), requestId, operator); + return selectArBillById(arBillId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ArBill voidBill(Long arBillId, ArBillActionRequest request, String operator, String requestId) + { + String remark = request == null ? null : request.getRemark(); + if (StringUtils.isEmpty(remark)) + { + throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", null, "作废必须填写原因", operator, requestId); + } + ArBill bill = billMapper.selectArBillByIdForUpdate(arBillId); + if (bill == null) + { + throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", null, "应收账单不存在", operator, requestId); + } + ArBillStatus currentStatus = ArBillStatus.of(bill.getStatus()); + if (currentStatus == null || !currentStatus.canVoid()) + { + throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", bill.getStatus(), + "当前状态不允许作废", operator, requestId); + } + List sourceBillIds = new ArrayList<>(); + for (ArBillDetail detail : detailMapper.selectByBillId(arBillId)) + { + if (!StringUtils.isEmpty(detail.getPrcBillId())) + { + sourceBillIds.add(detail.getPrcBillId()); + } + } + if (billMapper.updateArBillStatus(arBillId, ArBillStatus.VOID.getCode(), remark, operator, currentStatus.getCode()) != 1) + { + throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", bill.getStatus(), + "账单状态已发生变化,请刷新后重试", operator, requestId); + } + if (!sourceBillIds.isEmpty()) + { + int releasedCount = sourceBillMapper.releaseArBillOrders(sourceBillIds, operator, + ArOrderBillStatus.BILLED.getCode(), ArOrderBillStatus.UNBILLED.getCode()); + if (releasedCount != sourceBillIds.size()) + { + throw failure(arBillId, "BILL", String.valueOf(arBillId), "VOID", bill.getStatus(), + "部分来源订单状态异常,账单未作废,请刷新后重试", operator, requestId); + } + } + recordSuccess(bill, "VOID", "作废应收账单:" + remark, bill.getStatus(), ArBillStatus.VOID.getCode(), requestId, operator); + return selectArBillById(arBillId); + } + + @Override + public ArBillExport exportBill(Long arBillId, String operator, String requestId) + { + ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "EXPORT"); + if (previous != null && isNumber(previous.getObjectId())) + { + ArBillExport previousExport = exportMapper.selectById(Long.valueOf(previous.getObjectId())); + if (previousExport != null) + { + return previousExport; + } + } + ArBill bill = billMapper.selectArBillById(arBillId); + if (bill == null) + { + throw failure(arBillId, "EXPORT", String.valueOf(arBillId), "EXPORT", null, "应收账单不存在", operator, requestId); + } + if (ArBillStatus.is(bill.getStatus(), ArBillStatus.DRAFT) || ArBillStatus.is(bill.getStatus(), ArBillStatus.VOID)) + { + throw failure(arBillId, "EXPORT", String.valueOf(arBillId), "EXPORT", bill.getStatus(), + "草稿或作废账单不可导出", operator, requestId); + } + fillDetails(bill); + String fileName = bill.getBillNo() + "_" + safeFilePart(bill.getCustomerName()) + "_" + + new SimpleDateFormat("yyyyMM").format(bill.getBillPeriodEnd()) + ".xlsx"; + ArBillExport export = new ArBillExport(); + export.setArBillId(arBillId); + export.setFileName(fileName); + export.setFileType("XLSX"); + export.setTemplateCode("AR_CUSTOMER_BILL"); + export.setTemplateVersion("1.0"); + export.setExportBy(operator); + export.setRowCount(bill.getOrderCount()); + try + { + byte[] data = buildWorkbook(bill); + String relative = "ar/" + DateUtils.datePath() + "/" + IdUtils.fastSimpleUUID() + ".xlsx"; + String downloadDir = RuoYiConfig.getProfile() + File.separator + "download"; + File file = FileUploadUtils.getAbsoluteFile(downloadDir, relative); + Files.write(file.toPath(), data); + export.setStorageKey(FileUploadUtils.getPathFileName(downloadDir, relative)); + export.setFileSize((long) data.length); + export.setFileHash(sha256(data)); + export.setExportStatus(ArExportStatus.SUCCESS.getCode()); + exportMapper.insert(export); + recordSuccess(bill, "EXPORT", "导出账单 XLSX", bill.getStatus(), bill.getStatus(), requestId, operator, export.getExportId()); + return export; + } + catch (Exception e) + { + export.setStorageKey(""); + export.setExportStatus(ArExportStatus.FAILED.getCode()); + export.setRemark(e.getMessage()); + exportMapper.insert(export); + logService.recordFailure(arBillId, "EXPORT", String.valueOf(arBillId), "EXPORT", bill.getStatus(), + e.getMessage(), operator, requestId); + throw new ServiceException("账单导出失败:" + e.getMessage()); + } + } + + @Override + public List selectExports(Long arBillId) + { + return exportMapper.selectByBillId(arBillId); + } + + @Override + public void downloadExport(Long exportId, HttpServletResponse response) throws Exception + { + ArBillExport export = exportMapper.selectById(exportId); + if (export == null || !ArExportStatus.SUCCESS.getCode().equals(export.getExportStatus()) || StringUtils.isEmpty(export.getStorageKey())) + { + throw new ServiceException("导出文件不存在或尚未生成成功"); + } + String resource = export.getStorageKey(); + if (!FileUtils.checkAllowDownload(resource)) + { + throw new ServiceException("导出文件路径非法"); + } + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + FileUtils.setAttachmentResponseHeader(response, export.getFileName()); + FileUtils.writeBytes(RuoYiConfig.getProfile() + FileUtils.stripPrefix(resource), response.getOutputStream()); + } + + @Override + public List selectLogList(ArBillLog query) + { + return logService.selectLogList(query); + } + + private void fillDetails(ArBill bill) + { + List details = detailMapper.selectByBillIdWithFees(bill.getArBillId()); + for (ArBillDetail detail : details) + { + if (detail.getFees() == null) + { + detail.setFees(new ArrayList<>()); + } + } + bill.setDetails(details); + } + + private boolean validateDetails(Long arBillId, BigDecimal billTotal) + { + List details = detailMapper.selectByBillIdWithFees(arBillId); + BigDecimal detailTotal = BigDecimal.ZERO; + for (ArBillDetail detail : details) + { + BigDecimal feeTotal = BigDecimal.ZERO; + List fees = detail.getFees(); + if (fees == null) + { + fees = new ArrayList(); + } + for (ArBillDetailFee fee : fees) + { + feeTotal = feeTotal.add(nvl(fee.getAmount())); + } + if (feeTotal.compareTo(nvl(detail.getTotalAmount())) != 0) + { + return false; + } + detailTotal = detailTotal.add(nvl(detail.getTotalAmount())); + } + return !details.isEmpty() && detailTotal.compareTo(nvl(billTotal)) == 0; + } + + private ArBillDetail toDetail(ArBill bill, PrcOrderBill source, int detailNo, String operator) + { + ArBillDetail detail = new ArBillDetail(); + detail.setArBillId(bill.getArBillId()); + detail.setPrcBillId(source.getBillId()); + detail.setDetailNo(detailNo); + detail.setOrderNo(source.getOrderNo()); + detail.setTrackingNo(source.getTrackingNo()); + detail.setReceiveTime(source.getOrderTime()); + detail.setProductCode(source.getProductCode()); + detail.setProductName(source.getProductName()); + detail.setGoodsDescription(source.getOrderRemark()); + detail.setCountryCode(source.getCountryCode()); + detail.setCountryName(source.getCountryName()); + detail.setDestPostcode(source.getDestPostcode()); + detail.setActualWeight(source.getActualWeight()); + detail.setVolumeWeight(source.getVolumeWeight()); + detail.setChargeWeight(source.getChargeWeight() == null ? source.getWeight() : source.getChargeWeight()); + detail.setPkgLength(source.getPkgLength()); + detail.setPkgWidth(source.getPkgWidth()); + detail.setPkgHeight(source.getPkgHeight()); + detail.setCurrency(source.getCurrency() == null ? bill.getCurrency() : source.getCurrency()); + detail.setTotalAmount(nvl(source.getIncomeTotal())); + detail.setRemark(source.getOrderRemark()); + detail.setCreateBy(operator); + return detail; + } + + private ArBillDetailFee toDetailFee(ArBillDetail detail, PrcIncomeBillFee source, int feeNo, String operator) + { + ArBillDetailFee fee = new ArBillDetailFee(); + fee.setDetailId(detail.getDetailId()); + fee.setPrcFeeId(source.getFeeId()); + fee.setFeeNo(feeNo); + fee.setFeeType(source.getFeeType()); + fee.setFeeName(source.getFeeName()); + fee.setScId(source.getScId()); + fee.setAmount(nvl(source.getAmount())); + fee.setCurrency(source.getCurrency() == null ? detail.getCurrency() : source.getCurrency()); + fee.setRoundMode(source.getRoundMode()); + fee.setRemark(source.getRemark()); + fee.setCreateBy(operator); + return fee; + } + + private void recordSuccess(ArBill bill, String operationType, String desc, String fromStatus, + String toStatus, String requestId, String operator) + { + recordSuccess(bill, operationType, desc, fromStatus, toStatus, requestId, operator, null); + } + + private void recordSuccess(ArBill bill, String operationType, String desc, String fromStatus, + String toStatus, String requestId, String operator, Long objectId) + { + ArBillLog log = new ArBillLog(); + log.setArBillId(bill.getArBillId()); + log.setObjectType("BILL".equals(operationType) ? "BILL" : operationType.equals("EXPORT") ? "EXPORT" : "BILL"); + log.setObjectId(objectId == null ? String.valueOf(bill.getArBillId()) : String.valueOf(objectId)); + log.setOperationType(operationType); + log.setOperationDesc(desc); + log.setFromStatus(fromStatus); + log.setToStatus(toStatus); + log.setBeforeData(safeStatusJson(fromStatus)); + log.setAfterData(safeStatusJson(toStatus)); + log.setOperatorId(operator); + log.setOperatorName(operator); + log.setOperationSource("WEB"); + log.setRequestId(requestId); + log.setResult(ArLogResult.SUCCESS.getCode()); + logService.record(log); + } + + private ServiceException failure(Long arBillId, String objectType, String objectId, String operationType, + String fromStatus, String message, String operator, String requestId) + { + logService.recordFailure(arBillId, objectType, objectId, operationType, fromStatus, message, operator, requestId); + return new ServiceException(message); + } + + private byte[] buildWorkbook(ArBill bill) throws IOException + { + ClassPathResource resource = new ClassPathResource(EXPORT_TEMPLATE_RESOURCE); + if (!resource.exists()) + { + throw new IOException("应收账单导出模板不存在: " + EXPORT_TEMPLATE_RESOURCE); + } + try (InputStream input = resource.getInputStream(); XSSFWorkbook workbook = new XSSFWorkbook(input)) + { + Sheet sheet = workbook.getSheet("8.31"); + if (sheet == null) + { + sheet = workbook.getSheetAt(0); + } + + Row templateDataRow = sheet.getRow(TEMPLATE_DATA_START_ROW); + List details = bill.getDetails() == null ? new ArrayList() : bill.getDetails(); + CellStyle bodyStyle = createTemplateBodyStyle(workbook); + int footerStartRow = TEMPLATE_FOOTER_START_ROW + Math.max(details.size() - 1, 0); + int footerShift = footerStartRow - TEMPLATE_FOOTER_START_ROW; + if (footerShift != 0) + { + sheet.shiftRows(TEMPLATE_FOOTER_START_ROW, sheet.getLastRowNum(), footerShift); + } + + set(sheet, 0, 0, StringUtils.isEmpty(bill.getBankAccountName()) ? "威狮国际供应链(深圳)有限公司" : bill.getBankAccountName()); + set(sheet, 2, 2, bill.getCustomerName()); + set(sheet, 2, 15, exportPeriod(bill)); + + for (int i = 0; i < details.size(); i++) + { + Row row = sheet.getRow(TEMPLATE_DATA_START_ROW + i); + if (row == null) + { + row = sheet.createRow(TEMPLATE_DATA_START_ROW + i); + copyRowStyle(templateDataRow, row); + } + writeTemplateDetail(row, details.get(i), bodyStyle); + } + + int bankRow = footerStartRow; + set(sheet, bankRow, 1, "银行信息"); + set(sheet, bankRow + 1, 1, "开户名"); + set(sheet, bankRow + 1, 2, bill.getBankAccountName()); + set(sheet, bankRow + 2, 1, "账号"); + set(sheet, bankRow + 2, 2, exportBankAccountNo(bill.getBankAccountNo())); + set(sheet, bankRow + 3, 1, "开户行"); + set(sheet, bankRow + 3, 2, bill.getBankName()); + + set(sheet, bankRow, 15, "本期应收金额"); + set(sheet, bankRow, 18, bill.getTotalAmount()); + set(sheet, bankRow + 1, 15, "本期已核销金额"); + set(sheet, bankRow + 1, 18, bill.getWrittenOffAmount()); + set(sheet, bankRow + 2, 15, "本期未核销金额"); + set(sheet, bankRow + 2, 18, bill.getUnwrittenAmount()); + return toBytes(workbook); + } + } + + private byte[] toBytes(XSSFWorkbook workbook) throws IOException + { + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + workbook.write(out); + return out.toByteArray(); + } + + private void set(Row row, int index, Object value) + { + Cell cell = row.getCell(index, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK); + cell.setCellValue(""); + if (value == null) + { + return; + } + if (value instanceof Number) + { + cell.setCellValue(((Number) value).doubleValue()); + } + else + { + cell.setCellValue(String.valueOf(value)); + } + } + + private void set(Sheet sheet, int rowIndex, int columnIndex, Object value) + { + Row row = sheet.getRow(rowIndex); + if (row == null) + { + row = sheet.createRow(rowIndex); + } + set(row, columnIndex, value); + } + + private void copyRowStyle(Row source, Row target) + { + if (source == null) + { + return; + } + target.setHeight(source.getHeight()); + for (int index = 0; index < TEMPLATE_COLUMN_COUNT; index++) + { + Cell sourceCell = source.getCell(index); + if (sourceCell != null) + { + target.getCell(index, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK).setCellStyle(sourceCell.getCellStyle()); + } + } + } + + private CellStyle createTemplateBodyStyle(XSSFWorkbook workbook) + { + CellStyle style = workbook.createCellStyle(); + Font font = workbook.createFont(); + font.setFontName("微软雅黑"); + font.setFontHeightInPoints((short) 10); + style.setFont(font); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setWrapText(true); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderTop(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + return style; + } + + private void writeTemplateDetail(Row row, ArBillDetail detail, CellStyle bodyStyle) + { + row.setHeightInPoints(24); + Object[] values = { + detail.getDetailNo(), + detail.getReceiveTime() == null ? "" : new SimpleDateFormat("yyyy-MM-dd").format(detail.getReceiveTime()), + detail.getOrderNo(), detail.getTrackingNo(), + StringUtils.isEmpty(detail.getProductName()) ? detail.getProductCode() : detail.getProductName(), + StringUtils.isEmpty(detail.getCountryName()) ? detail.getCountryCode() : detail.getCountryName(), + detail.getDestPostcode(), decimal(detail.getActualWeight(), 3), decimal(detail.getPkgLength(), 2), + decimal(detail.getPkgHeight(), 2), decimal(detail.getPkgWidth(), 2), decimal(detail.getVolumeWeight(), 3), + decimal(detail.getChargeWeight(), 3), exportUnitPrice(detail), exportProcessingFee(detail), + exportFormula(detail), exportFeeTypes(detail), detail.getCurrency(), detail.getTotalAmount(), exportRemark(detail) + }; + for (int index = 0; index < values.length; index++) + { + set(row, index, values[index]); + row.getCell(index, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK).setCellStyle(bodyStyle); + } + } + + private String exportRemark(ArBillDetail detail) + { + return StringUtils.isEmpty(detail.getRemark()) ? detail.getGoodsDescription() : detail.getRemark(); + } + + private String exportPeriod(ArBill bill) + { + if (bill.getBillPeriodEnd() == null) + { + return ""; + } + return "截至 " + new SimpleDateFormat("yyyy-MM-dd").format(bill.getBillPeriodEnd()); + } + + private BigDecimal exportUnitPrice(ArBillDetail detail) + { + BigDecimal direct = firstFeeUnitPrice(detail.getFees()); + if (direct != null) + { + return direct; + } + BigDecimal freight = feeAmount(detail, "BASIC_FREIGHT"); + if (freight == null || detail.getChargeWeight() == null || detail.getChargeWeight().compareTo(BigDecimal.ZERO) <= 0) + { + return null; + } + return freight.divide(detail.getChargeWeight(), 4, java.math.RoundingMode.HALF_UP); + } + + private BigDecimal firstFeeUnitPrice(List fees) + { + if (fees == null) + { + return null; + } + for (ArBillDetailFee fee : fees) + { + if (fee.getUnitPrice() != null) + { + return fee.getUnitPrice(); + } + } + return null; + } + + private BigDecimal exportProcessingFee(ArBillDetail detail) + { + BigDecimal total = BigDecimal.ZERO; + boolean found = false; + if (detail.getFees() != null) + { + for (ArBillDetailFee fee : detail.getFees()) + { + String name = fee.getFeeName() == null ? "" : fee.getFeeName(); + if ("REGISTRATION_FEE".equalsIgnoreCase(fee.getFeeType()) || name.contains("处理费")) + { + total = total.add(nvl(fee.getAmount())); + found = true; + } + } + } + return found ? total : null; + } + + private String exportFormula(ArBillDetail detail) + { + String formula = joinFeeText(detail.getFees(), true); + if (!StringUtils.isEmpty(formula)) + { + return formula; + } + boolean freight = feeAmount(detail, "BASIC_FREIGHT") != null; + boolean processing = exportProcessingFee(detail) != null; + boolean surcharge = hasFeeType(detail.getFees(), "SURCHARGE"); + if (!freight) + { + return ""; + } + return "结算重量*公斤价" + (processing ? "+处理费" : "") + (surcharge ? "+附加费" : ""); + } + + private String exportFeeTypes(ArBillDetail detail) + { + return joinFeeText(detail.getFees(), false); + } + + private String joinFeeText(List fees, boolean formula) + { + if (fees == null || fees.isEmpty()) + { + return ""; + } + StringBuilder result = new StringBuilder(); + for (ArBillDetailFee fee : fees) + { + String value = formula ? fee.getCalculationFormula() : (StringUtils.isEmpty(fee.getFeeName()) ? fee.getFeeType() : fee.getFeeName()); + if (StringUtils.isEmpty(value)) + { + continue; + } + if (result.length() > 0) + { + result.append("、"); + } + result.append(value); + } + return result.toString(); + } + + private BigDecimal feeAmount(ArBillDetail detail, String feeType) + { + if (detail.getFees() == null) + { + return null; + } + BigDecimal total = BigDecimal.ZERO; + boolean found = false; + for (ArBillDetailFee fee : detail.getFees()) + { + if (feeType.equalsIgnoreCase(fee.getFeeType())) + { + total = total.add(nvl(fee.getAmount())); + found = true; + } + } + return found ? total : null; + } + + private boolean hasFeeType(List fees, String feeType) + { + if (fees == null) + { + return false; + } + for (ArBillDetailFee fee : fees) + { + if (feeType.equalsIgnoreCase(fee.getFeeType())) + { + return true; + } + } + return false; + } + + private String exportBankAccountNo(String value) + { + if (StringUtils.isEmpty(value)) + { + return ""; + } + String normalized = value.replaceAll("\\s+", ""); + StringBuilder result = new StringBuilder(normalized.length() + normalized.length() / 4); + for (int index = 0; index < normalized.length(); index++) + { + if (index > 0 && index % 4 == 0) + { + result.append(' '); + } + result.append(normalized.charAt(index)); + } + return result.toString(); + } + + private BigDecimal decimal(BigDecimal value, int scale) { return value == null ? null : value.setScale(scale, java.math.RoundingMode.HALF_UP); } + private BigDecimal nvl(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } + private String safeFilePart(String value) { return StringUtils.isEmpty(value) ? "客户" : value.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5_-]", "_"); } + private String maskAccount(String value) { if (StringUtils.isEmpty(value)) return ""; return value.length() <= 4 ? "****" : "****" + value.substring(value.length() - 4); } + private String sha256(byte[] data) throws NoSuchAlgorithmException { byte[] digest = MessageDigest.getInstance("SHA-256").digest(data); StringBuilder sb = new StringBuilder(); for (byte b : digest) sb.append(String.format("%02x", b)); return sb.toString(); } + private boolean isNumber(String value) { if (StringUtils.isEmpty(value)) return false; try { Long.valueOf(value); return true; } catch (NumberFormatException e) { return false; } } + private String safeStatusJson(String status) { Map data = new HashMap<>(); data.put("status", status); return JSON.toJSONString(data); } + private void validatePeriod(ArBillGenerateRequest request) + { + if (request == null || request.getCustomerId() == null || StringUtils.isEmpty(request.getBillName()) + || request.getBillName().trim().isEmpty() + || request.getBillPeriodEnd() == null || request.getSourceBillIds() == null || request.getSourceBillIds().isEmpty()) + { + throw new ServiceException("客户、账单名称、账单截止日和订单选择不能为空"); + } + } + + private void validatePreviewQuery(ArBillGenerateQuery query) + { + if (query == null || query.getCustomerId() == null || query.getBillPeriodEnd() == null) + { + throw new ServiceException("客户和账单截止日不能为空"); + } + } + + private Date earliestBizDate(List sources) + { + Date earliest = null; + for (PrcOrderBill source : sources) + { + if (source.getBizDate() != null && (earliest == null || source.getBizDate().before(earliest))) + { + earliest = source.getBizDate(); + } + } + return earliest == null ? new Date() : earliest; + } + + private List normalizeSourceBillIds(List sourceBillIds) + { + LinkedHashSet uniqueIds = new LinkedHashSet<>(); + if (sourceBillIds != null) + { + for (String billId : sourceBillIds) + { + if (!StringUtils.isEmpty(billId) && billId.trim().length() <= 36) + { + uniqueIds.add(billId.trim()); + } + } + } + return new ArrayList<>(uniqueIds); + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArReceiptServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArReceiptServiceImpl.java new file mode 100644 index 00000000..2765311c --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/ar/service/impl/ArReceiptServiceImpl.java @@ -0,0 +1,275 @@ +package com.ruoyi.ar.service.impl; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.ar.domain.ArBill; +import com.ruoyi.ar.domain.ArBillLog; +import com.ruoyi.ar.domain.ArReceipt; +import com.ruoyi.ar.domain.ArReceiptWriteoff; +import com.ruoyi.ar.domain.vo.ArReceiptQuery; +import com.ruoyi.ar.domain.vo.ArReceiptWriteoffRequest; +import com.ruoyi.ar.enums.ArBillStatus; +import com.ruoyi.ar.enums.ArLogResult; +import com.ruoyi.ar.enums.ArReceiptStatus; +import com.ruoyi.ar.enums.ArWriteoffStatus; +import com.ruoyi.ar.mapper.ArBillMapper; +import com.ruoyi.ar.mapper.ArBillLogMapper; +import com.ruoyi.ar.mapper.ArReceiptMapper; +import com.ruoyi.ar.mapper.ArReceiptWriteoffMapper; +import com.ruoyi.ar.service.IArBillLogService; +import com.ruoyi.ar.service.IArReceiptService; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.uuid.IdUtils; + +@Service +public class ArReceiptServiceImpl implements IArReceiptService +{ + @Autowired private ArReceiptMapper receiptMapper; + @Autowired private ArReceiptWriteoffMapper writeoffMapper; + @Autowired private ArBillMapper billMapper; + @Autowired private ArBillLogMapper logMapper; + @Autowired private IArBillLogService logService; + + @Override + public List selectReceiptList(ArReceiptQuery query) + { + return receiptMapper.selectArReceiptList(query); + } + + @Override + public List selectWriteoffs(Long receiptId) + { + return writeoffMapper.selectByReceiptId(receiptId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ArReceipt registerReceipt(ArReceipt receipt, String operator, String requestId) + { + if (receipt == null || receipt.getCustomerId() == null || receipt.getReceiptDate() == null + || receipt.getReceiptAmount() == null || receipt.getReceiptAmount().compareTo(BigDecimal.ZERO) <= 0) + { + throw failure(null, "RECEIPT", "REGISTER", "收款客户、日期和正数金额不能为空", operator, requestId); + } + if (StringUtils.isEmpty(receipt.getReceiptNo())) + { + receipt.setReceiptNo("RC" + DateUtils.dateTimeNow("yyyyMMddHHmmss") + IdUtils.fastSimpleUUID().substring(0, 6).toUpperCase()); + } + if (StringUtils.isEmpty(receipt.getCurrency())) receipt.setCurrency("CNY"); + if (StringUtils.isEmpty(receipt.getPaymentMethod())) receipt.setPaymentMethod("BANK"); + ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "RECEIPT"); + if (previous != null && isNumber(previous.getObjectId())) + { + ArReceipt existed = receiptMapper.selectArReceiptById(Long.valueOf(previous.getObjectId())); + if (existed != null) return existed; + } + if (receiptMapper.selectArReceiptByNo(receipt.getReceiptNo()) != null) + { + throw failure(null, "RECEIPT", receipt.getReceiptNo(), "收款单号已存在", operator, requestId); + } + if (!StringUtils.isEmpty(receipt.getBankReference()) + && receiptMapper.selectArReceiptByReference(receipt.getPaymentMethod(), receipt.getBankReference()) != null) + { + throw failure(null, "RECEIPT", receipt.getReceiptNo(), "银行流水号已存在", operator, requestId); + } + receipt.setUnallocatedAmount(receipt.getReceiptAmount()); + receipt.setStatus(ArReceiptStatus.REGISTERED.getCode()); + receipt.setCreateBy(operator); + receipt.setUpdateBy(operator); + try + { + receiptMapper.insertArReceipt(receipt); + } + catch (DuplicateKeyException e) + { + ArReceipt existed = receiptMapper.selectArReceiptByNo(receipt.getReceiptNo()); + if (existed != null) return existed; + throw e; + } + ArBillLog log = new ArBillLog(); + log.setObjectType("RECEIPT"); log.setObjectId(String.valueOf(receipt.getReceiptId())); + log.setOperationType("RECEIPT"); log.setOperationDesc("登记应收收款"); + log.setOperatorId(operator); log.setOperatorName(operator); log.setOperationSource("WEB"); + log.setRequestId(requestId); log.setResult(ArLogResult.SUCCESS.getCode()); logService.record(log); + return receipt; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelReceipt(Long receiptId, String remark, String operator, String requestId) + { + ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "RECEIPT"); + if (previous != null && "CANCEL_RECEIPT".equals(previous.getOperationType())) return; + ArReceipt receipt = receiptMapper.selectArReceiptByIdForUpdate(receiptId); + if (receipt == null) throw failure(null, "RECEIPT", String.valueOf(receiptId), "收款不存在", operator, requestId); + if (!ArReceiptStatus.REGISTERED.getCode().equals(receipt.getStatus()) + || nvl(receipt.getUnallocatedAmount()).compareTo(nvl(receipt.getReceiptAmount())) != 0) + { + throw failure(null, "RECEIPT", String.valueOf(receiptId), "收款已发生核销,不能直接取消", operator, requestId); + } + receiptMapper.updateCancelled(receiptId, operator, StringUtils.isEmpty(remark) ? "取消收款" : remark); + ArBillLog log = new ArBillLog(); + log.setObjectType("RECEIPT"); log.setObjectId(String.valueOf(receiptId)); log.setOperationType("CANCEL_RECEIPT"); + log.setOperationDesc(StringUtils.isEmpty(remark) ? "取消收款" : remark); log.setFromStatus(ArReceiptStatus.REGISTERED.getCode()); + log.setToStatus(ArReceiptStatus.CANCELLED.getCode()); log.setOperatorId(operator); log.setOperatorName(operator); + log.setOperationSource("WEB"); log.setRequestId(requestId); log.setResult(ArLogResult.SUCCESS.getCode()); logService.record(log); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public List writeoff(ArReceiptWriteoffRequest request, String operator, String requestId) + { + if (request == null || request.getReceiptId() == null || request.getAllocations() == null || request.getAllocations().isEmpty()) + { + throw failure(null, "WRITEOFF", "", "核销收款和账单不能为空", operator, requestId); + } + ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "WRITEOFF"); + if (previous != null && isNumber(previous.getObjectId())) + { + ArReceiptWriteoff existed = writeoffMapper.selectById(Long.valueOf(previous.getObjectId())); + if (existed != null) return writeoffMapper.selectByReceiptId(existed.getReceiptId()); + } + ArReceipt receipt = receiptMapper.selectArReceiptByIdForUpdate(request.getReceiptId()); + if (receipt == null) throw failure(null, "WRITEOFF", String.valueOf(request.getReceiptId()), "收款不存在", operator, requestId); + if (ArReceiptStatus.CANCELLED.getCode().equals(receipt.getStatus())) + { + throw failure(null, "WRITEOFF", String.valueOf(request.getReceiptId()), "收款已取消,不允许核销", operator, requestId); + } + String currency = StringUtils.isEmpty(request.getCurrency()) ? receipt.getCurrency() : request.getCurrency(); + if (!currency.equalsIgnoreCase(receipt.getCurrency())) + { + throw failure(null, "WRITEOFF", String.valueOf(request.getReceiptId()), "收款与核销币种不一致", operator, requestId); + } + BigDecimal allocationTotal = BigDecimal.ZERO; + for (ArReceiptWriteoffRequest.Allocation allocation : request.getAllocations()) + { + if (allocation.getAmount() == null || allocation.getAmount().compareTo(BigDecimal.ZERO) <= 0) + { + throw failure(null, "WRITEOFF", String.valueOf(request.getReceiptId()), "核销金额必须大于零", operator, requestId); + } + allocationTotal = allocationTotal.add(allocation.getAmount()); + } + if (allocationTotal.compareTo(nvl(receipt.getUnallocatedAmount())) > 0) + { + throw failure(null, "WRITEOFF", String.valueOf(request.getReceiptId()), "核销金额超过收款未分配余额", operator, requestId); + } + + List result = new ArrayList<>(); + BigDecimal remainingReceipt = nvl(receipt.getUnallocatedAmount()); + Date writeoffDate = request.getWriteoffDate() == null ? new Date() : request.getWriteoffDate(); + for (ArReceiptWriteoffRequest.Allocation allocation : request.getAllocations()) + { + ArBill bill = billMapper.selectArBillByIdForUpdate(allocation.getArBillId()); + if (bill == null) throw failure(null, "WRITEOFF", String.valueOf(allocation.getArBillId()), "账单不存在", operator, requestId); + ArBillStatus billStatus = ArBillStatus.of(bill.getStatus()); + if (billStatus == ArBillStatus.VOID) throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "账单已作废,不允许核销", operator, requestId); + if (billStatus == null || !billStatus.canWriteOff()) + { + throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "账单尚未确认,不允许核销", operator, requestId); + } + if (!currency.equalsIgnoreCase(bill.getCurrency())) throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "收款与账单币种不一致", operator, requestId); + BigDecimal amount = allocation.getAmount(); + if (amount.compareTo(nvl(bill.getUnwrittenAmount())) > 0) + { + throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "核销金额超过账单未核销余额", operator, requestId); + } + ArReceiptWriteoff writeoff = new ArReceiptWriteoff(); + writeoff.setReceiptId(receipt.getReceiptId()); writeoff.setArBillId(bill.getArBillId()); + writeoff.setWriteoffNo("WO" + DateUtils.dateTimeNow("yyyyMMddHHmmss") + IdUtils.fastSimpleUUID().substring(0, 6).toUpperCase()); + writeoff.setWriteoffAmount(amount); writeoff.setCurrency(currency); writeoff.setWriteoffDate(writeoffDate); + writeoff.setStatus(ArWriteoffStatus.EFFECTIVE.getCode()); writeoff.setRemark(request.getRemark()); writeoff.setCreateBy(operator); + writeoffMapper.insert(writeoff); + remainingReceipt = remainingReceipt.subtract(amount); + BigDecimal written = nvl(bill.getWrittenOffAmount()).add(amount); + BigDecimal unwritten = nvl(bill.getUnwrittenAmount()).subtract(amount); + String nextBillStatus = billStatus(written, nvl(bill.getTotalAmount()), bill.getStatus()); + if (billMapper.updateArBillAmounts(bill.getArBillId(), written, unwritten, + nextBillStatus, operator, bill.getStatus()) != 1) + { + throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "账单状态已发生变化,请刷新后重试", operator, requestId); + } + writeoffLog(writeoff, bill, operator, requestId); + result.add(writeoff); + } + receiptMapper.updateBalanceAndStatus(receipt.getReceiptId(), remainingReceipt, + remainingReceipt.compareTo(BigDecimal.ZERO) == 0 ? ArReceiptStatus.WRITTEN_OFF.getCode() : ArReceiptStatus.PARTIAL_WRITTEN_OFF.getCode(), operator); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void cancelWriteoff(Long writeoffId, String remark, String operator, String requestId) + { + ArBillLog previous = logMapper.selectSuccessByRequestId(requestId, "WRITEOFF"); + if (previous != null && "CANCEL_WRITEOFF".equals(previous.getOperationType())) return; + ArReceiptWriteoff writeoff = writeoffMapper.selectByIdForUpdate(writeoffId); + if (writeoff == null) throw failure(null, "WRITEOFF", String.valueOf(writeoffId), "核销记录不存在", operator, requestId); + if (!ArWriteoffStatus.EFFECTIVE.getCode().equals(writeoff.getStatus())) throw failure(writeoff.getArBillId(), "WRITEOFF", String.valueOf(writeoffId), "只有有效核销可以取消", operator, requestId); + ArReceipt receipt = receiptMapper.selectArReceiptByIdForUpdate(writeoff.getReceiptId()); + ArBill bill = billMapper.selectArBillByIdForUpdate(writeoff.getArBillId()); + if (receipt == null || bill == null) throw failure(writeoff.getArBillId(), "WRITEOFF", String.valueOf(writeoffId), "核销关联的收款或账单不存在", operator, requestId); + writeoffMapper.updateCancelled(writeoffId, operator, StringUtils.isEmpty(remark) ? "取消核销" : remark); + BigDecimal receiptUnallocated = nvl(receipt.getUnallocatedAmount()).add(writeoff.getWriteoffAmount()); + receiptMapper.updateBalanceAndStatus(receipt.getReceiptId(), receiptUnallocated, + receiptUnallocated.compareTo(nvl(receipt.getReceiptAmount())) == 0 ? ArReceiptStatus.REGISTERED.getCode() : ArReceiptStatus.PARTIAL_WRITTEN_OFF.getCode(), operator); + BigDecimal written = nvl(bill.getWrittenOffAmount()).subtract(writeoff.getWriteoffAmount()); + if (written.compareTo(BigDecimal.ZERO) < 0) written = BigDecimal.ZERO; + BigDecimal unwritten = nvl(bill.getTotalAmount()).subtract(written); + String nextBillStatus = billStatus(written, nvl(bill.getTotalAmount()), bill.getStatus()); + if (billMapper.updateArBillAmounts(bill.getArBillId(), written, unwritten, + nextBillStatus, operator, bill.getStatus()) != 1) + { + throw failure(bill.getArBillId(), "WRITEOFF", String.valueOf(bill.getArBillId()), "账单状态已发生变化,请刷新后重试", operator, requestId); + } + ArBillLog log = new ArBillLog(); + log.setArBillId(bill.getArBillId()); log.setObjectType("WRITEOFF"); log.setObjectId(String.valueOf(writeoffId)); + log.setOperationType("CANCEL_WRITEOFF"); log.setOperationDesc(StringUtils.isEmpty(remark) ? "取消核销" : remark); + log.setFromStatus(ArWriteoffStatus.EFFECTIVE.getCode()); log.setToStatus(ArWriteoffStatus.CANCELLED.getCode()); log.setOperatorId(operator); log.setOperatorName(operator); + log.setOperationSource("WEB"); log.setRequestId(requestId); log.setResult(ArLogResult.SUCCESS.getCode()); logService.record(log); + } + + private void writeoffLog(ArReceiptWriteoff writeoff, ArBill bill, String operator, String requestId) + { + ArBillLog log = new ArBillLog(); + log.setArBillId(bill.getArBillId()); log.setObjectType("WRITEOFF"); log.setObjectId(String.valueOf(writeoff.getWriteoffId())); + log.setOperationType("WRITEOFF"); log.setOperationDesc("收款核销到账单"); log.setFromStatus(bill.getStatus()); + log.setToStatus(billStatus(nvl(bill.getWrittenOffAmount()).add(writeoff.getWriteoffAmount()), nvl(bill.getTotalAmount()), bill.getStatus())); + log.setOperatorId(operator); log.setOperatorName(operator); log.setOperationSource("WEB"); log.setRequestId(requestId); log.setResult(ArLogResult.SUCCESS.getCode()); + logService.record(log); + } + + private String billStatus(BigDecimal written, BigDecimal total, String current) + { + if (written.compareTo(BigDecimal.ZERO) == 0) + { + return ArBillStatus.DRAFT.getCode().equals(current) + ? ArBillStatus.DRAFT.getCode() : ArBillStatus.CONFIRMED.getCode(); + } + return written.compareTo(total) >= 0 + ? ArBillStatus.WRITTEN_OFF.getCode() : ArBillStatus.PARTIAL_WRITTEN_OFF.getCode(); + } + + private ServiceException failure(Long billId, String objectType, String message, String operator, String requestId) + { + logService.recordFailure(billId, objectType, objectType, "RECEIPT", null, message, operator, requestId); + return new ServiceException(message); + } + + private ServiceException failure(Long billId, String objectType, String objectId, String message, String operator, String requestId) + { + logService.recordFailure(billId, objectType, objectId, + "RECEIPT".equals(objectType) ? "RECEIPT" : "WRITEOFF", null, message, operator, requestId); + return new ServiceException(message); + } + + private BigDecimal nvl(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } + private boolean isNumber(String value) { if (StringUtils.isEmpty(value)) return false; try { Long.valueOf(value); return true; } catch (NumberFormatException e) { return false; } } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcOrderBill.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcOrderBill.java index 9d1655ed..11b2ab08 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcOrderBill.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcOrderBill.java @@ -71,6 +71,8 @@ public class PrcOrderBill extends BaseEntity /** payable total */ private BigDecimal payTotal; private String currency; + /** 应收账单占用状态:UNBILLED/BILLED。 */ + private String arBillStatus; private String roundMode; private List incomeFees; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcIncomeBillFeeMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcIncomeBillFeeMapper.java index 707b1cc5..4b479997 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcIncomeBillFeeMapper.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcIncomeBillFeeMapper.java @@ -9,6 +9,8 @@ public interface PrcIncomeBillFeeMapper { public List selectByBillId(String billId); + public List selectByBillIds(@Param("billIds") List billIds); + public int insertPrcIncomeBillFee(PrcIncomeBillFee fee); public int insertBatch(@Param("list") List fees); diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcOrderBillMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcOrderBillMapper.java index ec4c1ea6..79f70f79 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcOrderBillMapper.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcOrderBillMapper.java @@ -1,6 +1,7 @@ package com.ruoyi.price.mapper; import java.math.BigDecimal; +import java.util.Date; import java.util.List; import org.apache.ibatis.annotations.Param; import com.ruoyi.price.domain.PrcOrderBill; @@ -14,6 +15,25 @@ public interface PrcOrderBillMapper public List selectPrcOrderBillList(PrcOrderBill bill); + /** 应收账单生成读取指定客户截止日期前的未出账订单计费快照。 */ + public List selectPrcOrderBillByCustomerPeriod(@Param("customerId") Long customerId, + @Param("customerCode") String customerCode, @Param("cutoffDate") Date cutoffDate, + @Param("currency") String currency, @Param("arBillStatus") String arBillStatus); + + /** 按生成账单时用户勾选的订单读取,并再次校验账单筛选条件。 */ + public List selectPrcOrderBillByCustomerCutoffAndIds(@Param("customerId") Long customerId, + @Param("customerCode") String customerCode, @Param("cutoffDate") Date cutoffDate, + @Param("currency") String currency, @Param("billIds") List billIds, + @Param("arBillStatus") String arBillStatus); + + /** 原子占用订单,防止同一订单被并发生成到多张应收账单。 */ + public int claimArBillOrders(@Param("billIds") List billIds, @Param("operator") String operator, + @Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus); + + /** 作废账单后释放订单,允许后续重新生成新版本账单。 */ + public int releaseArBillOrders(@Param("billIds") List billIds, @Param("operator") String operator, + @Param("fromStatus") String fromStatus, @Param("toStatus") String toStatus); + public int insertPrcOrderBill(PrcOrderBill bill); public int updatePrcOrderBill(PrcOrderBill bill); diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBankAccountMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBankAccountMapper.xml new file mode 100644 index 00000000..c3d00049 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBankAccountMapper.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillDetailFeeMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillDetailFeeMapper.xml new file mode 100644 index 00000000..440870ac --- /dev/null +++ b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillDetailFeeMapper.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + insert into ar_bill_detail_fee ( + detail_id, prc_fee_id, fee_no, fee_type, fee_name, sc_id, quantity, unit, unit_price, + amount, currency, calculation_formula, round_mode, remark, create_by, create_time + ) values ( + #{detailId}, #{prcFeeId}, #{feeNo}, #{feeType}, #{feeName}, #{scId}, #{quantity}, #{unit}, + #{unitPrice}, #{amount}, #{currency}, #{calculationFormula}, #{roundMode}, #{remark}, + #{createBy}, sysdate() + ) + + + + insert into ar_bill_detail_fee ( + detail_id, prc_fee_id, fee_no, fee_type, fee_name, sc_id, quantity, unit, unit_price, + amount, currency, calculation_formula, round_mode, remark, create_by, create_time + ) values + + (#{item.detailId}, #{item.prcFeeId}, #{item.feeNo}, #{item.feeType}, #{item.feeName}, #{item.scId}, + #{item.quantity}, #{item.unit}, #{item.unitPrice}, #{item.amount}, #{item.currency}, + #{item.calculationFormula}, #{item.roundMode}, #{item.remark}, #{item.createBy}, sysdate()) + + + diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillDetailMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillDetailMapper.xml new file mode 100644 index 00000000..3a00cbe4 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillDetailMapper.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + insert into ar_bill_detail ( + ar_bill_id, prc_bill_id, detail_no, order_no, tracking_no, receive_time, product_code, + product_name, goods_description, country_code, country_name, dest_postcode, actual_weight, + volume_weight, charge_weight, pkg_length, pkg_width, pkg_height, currency, total_amount, + remark, create_by, create_time + ) values ( + #{arBillId}, #{prcBillId}, #{detailNo}, #{orderNo}, #{trackingNo}, #{receiveTime}, + #{productCode}, #{productName}, #{goodsDescription}, #{countryCode}, #{countryName}, + #{destPostcode}, #{actualWeight}, #{volumeWeight}, #{chargeWeight}, #{pkgLength}, #{pkgWidth}, + #{pkgHeight}, #{currency}, #{totalAmount}, #{remark}, #{createBy}, sysdate() + ) + + + + insert into ar_bill_detail ( + ar_bill_id, prc_bill_id, detail_no, order_no, tracking_no, receive_time, product_code, + product_name, goods_description, country_code, country_name, dest_postcode, actual_weight, + volume_weight, charge_weight, pkg_length, pkg_width, pkg_height, currency, total_amount, + remark, create_by, create_time + ) values + + (#{item.arBillId}, #{item.prcBillId}, #{item.detailNo}, #{item.orderNo}, #{item.trackingNo}, + #{item.receiveTime}, #{item.productCode}, #{item.productName}, #{item.goodsDescription}, + #{item.countryCode}, #{item.countryName}, #{item.destPostcode}, #{item.actualWeight}, + #{item.volumeWeight}, #{item.chargeWeight}, #{item.pkgLength}, #{item.pkgWidth}, #{item.pkgHeight}, + #{item.currency}, #{item.totalAmount}, #{item.remark}, #{item.createBy}, sysdate()) + + + diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillExportMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillExportMapper.xml new file mode 100644 index 00000000..0dde3c2e --- /dev/null +++ b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillExportMapper.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + select export_id, ar_bill_id, file_name, file_type, template_code, template_version, + storage_key, file_size, file_hash, row_count, export_status, export_by, export_time, remark + from ar_bill_export + + + + + + + + + + insert into ar_bill_export ( + ar_bill_id, file_name, file_type, template_code, template_version, storage_key, + file_size, file_hash, row_count, export_status, export_by, export_time, remark + ) values ( + #{arBillId}, #{fileName}, #{fileType}, #{templateCode}, #{templateVersion}, #{storageKey}, + #{fileSize}, #{fileHash}, #{rowCount}, #{exportStatus}, #{exportBy}, sysdate(), #{remark} + ) + + diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillLogMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillLogMapper.xml new file mode 100644 index 00000000..2fae1bc2 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillLogMapper.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + insert into ar_bill_log ( + ar_bill_id, object_type, object_id, operation_type, operation_desc, from_status, to_status, + before_data, after_data, operator_id, operator_name, operation_source, request_id, + trace_id, result, error_message, client_ip, create_time + ) values ( + #{arBillId}, #{objectType}, #{objectId}, #{operationType}, #{operationDesc}, #{fromStatus}, #{toStatus}, + #{beforeData}, #{afterData}, #{operatorId}, #{operatorName}, #{operationSource}, #{requestId}, + #{traceId}, #{result}, #{errorMessage}, #{clientIp}, sysdate() + ) + + diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillMapper.xml new file mode 100644 index 00000000..9153e047 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArBillMapper.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select ar_bill_id, bill_no, bill_name, customer_id, customer_code, customer_name, + bill_period_start, bill_period_end, bill_date, currency, order_count, + total_amount, written_off_amount, unwritten_amount, status, bill_version, + bank_account_name, bank_account_no, bank_name, remark, confirmed_by, + confirmed_time, create_by, create_time, update_by, update_time + from ar_bill + + + + + + + + + + + + + + insert into ar_bill ( + bill_no, bill_name, customer_id, customer_code, customer_name, bill_period_start, bill_period_end, + bill_date, currency, order_count, total_amount, written_off_amount, unwritten_amount, + status, bill_version, bank_account_name, bank_account_no, bank_name, remark, + confirmed_by, confirmed_time, create_by, create_time, update_by, update_time + ) values ( + #{billNo}, #{billName}, #{customerId}, #{customerCode}, #{customerName}, #{billPeriodStart}, #{billPeriodEnd}, + #{billDate}, #{currency}, #{orderCount}, #{totalAmount}, #{writtenOffAmount}, #{unwrittenAmount}, + #{status}, #{billVersion}, #{bankAccountName}, #{bankAccountNo}, #{bankName}, #{remark}, + #{confirmedBy}, #{confirmedTime}, #{createBy}, sysdate(), #{updateBy}, sysdate() + ) + + + + update ar_bill + set status = #{status}, remark = #{remark}, update_by = #{operator}, update_time = sysdate(), + confirmed_by = case when #{status} = 'CONFIRMED' then #{operator} else confirmed_by end, + confirmed_time = case when #{status} = 'CONFIRMED' then sysdate() else confirmed_time end + where ar_bill_id = #{arBillId} + and status = #{fromStatus} + + + + update ar_bill + set written_off_amount = #{writtenOffAmount}, unwritten_amount = #{unwrittenAmount}, + status = #{status}, update_by = #{operator}, update_time = sysdate() + where ar_bill_id = #{arBillId} + and status = #{fromStatus} + + diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArReceiptMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArReceiptMapper.xml new file mode 100644 index 00000000..2e50acaa --- /dev/null +++ b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArReceiptMapper.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + select r.receipt_id, r.receipt_no, r.customer_id, c.customer_name, c.customer_code, + r.receipt_date, r.currency, r.receipt_amount, r.unallocated_amount, + r.payment_method, r.bank_reference, r.status, r.remark, + r.create_by, r.create_time, r.update_by, r.update_time + from ar_receipt r + left join bms_customer c on c.customer_id = r.customer_id and c.del_flag = '0' + + + + + select receipt_id, receipt_no, customer_id, receipt_date, currency, receipt_amount, + unallocated_amount, payment_method, bank_reference, status, remark, + create_by, create_time, update_by, update_time + from ar_receipt + + + + + + + + + + + + + + insert into ar_receipt ( + receipt_no, customer_id, receipt_date, currency, receipt_amount, unallocated_amount, + payment_method, bank_reference, status, remark, create_by, create_time, update_by, update_time + ) values ( + #{receiptNo}, #{customerId}, #{receiptDate}, #{currency}, #{receiptAmount}, #{unallocatedAmount}, + #{paymentMethod}, #{bankReference}, #{status}, #{remark}, #{createBy}, sysdate(), #{updateBy}, sysdate() + ) + + + + update ar_receipt + set unallocated_amount = #{unallocatedAmount}, status = #{status}, + update_by = #{operator}, update_time = sysdate() + where receipt_id = #{receiptId} + + + + update ar_receipt set status = 'CANCELLED', remark = #{remark}, + update_by = #{operator}, update_time = sysdate() + where receipt_id = #{receiptId} + + diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArReceiptWriteoffMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArReceiptWriteoffMapper.xml new file mode 100644 index 00000000..fa730cdb --- /dev/null +++ b/bms-back/ruoyi-system/src/main/resources/mapper/ar/ArReceiptWriteoffMapper.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + select w.writeoff_id, w.receipt_id, w.ar_bill_id, b.bill_no, r.receipt_no, + w.writeoff_no, w.writeoff_amount, w.currency, w.writeoff_date, w.status, + w.remark, w.create_by, w.create_time, w.cancel_by, w.cancel_time + from ar_receipt_writeoff w + left join ar_bill b on b.ar_bill_id = w.ar_bill_id + left join ar_receipt r on r.receipt_id = w.receipt_id + + + + + select writeoff_id, receipt_id, ar_bill_id, writeoff_no, writeoff_amount, currency, + writeoff_date, status, remark, create_by, create_time, cancel_by, cancel_time + from ar_receipt_writeoff + + + + + + + + + + + + insert into ar_receipt_writeoff ( + receipt_id, ar_bill_id, writeoff_no, writeoff_amount, currency, writeoff_date, + status, remark, create_by, create_time + ) values ( + #{receiptId}, #{arBillId}, #{writeoffNo}, #{writeoffAmount}, #{currency}, #{writeoffDate}, + #{status}, #{remark}, #{createBy}, sysdate() + ) + + + + update ar_receipt_writeoff + set status = 'CANCELLED', remark = #{remark}, cancel_by = #{cancelBy}, cancel_time = sysdate() + where writeoff_id = #{writeoffId} and status = 'EFFECTIVE' + + + + + + diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcIncomeBillFeeMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcIncomeBillFeeMapper.xml index 7e5c196c..fe170ef9 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcIncomeBillFeeMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcIncomeBillFeeMapper.xml @@ -24,6 +24,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" order by field(fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE'), fee_id + + insert into prc_income_bill_fee ( fee_id, bill_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, create_time diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml index c975fddc..e32eda16 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcOrderBillMapper.xml @@ -28,7 +28,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - + @@ -55,6 +55,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -71,7 +72,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" set_type, bind_priority, customer_priceset_id, product_country_id, weight_min, weight_max, weight_label, effective_date, charge_weight, freight_rate, base_freight_rate, discount_percent, registration_fee, - freight_amount, surcharge_amount, income_total, pay_total, currency, round_mode, + freight_amount, surcharge_amount, income_total, pay_total, currency, ar_bill_status, round_mode, created_by, create_time, updated_by, update_time from prc_order_bill @@ -123,6 +124,39 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" order by create_time desc, bill_id desc + + + + + insert into prc_order_bill ( bill_id, bill_key, order_no, tracking_no, customer_id, customer_code, customer_name, @@ -172,7 +206,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" product_code = #{productCode}, product_name = #{productName}, country_code = #{countryCode}, - country_name = #{countryName}, + country_name = #{countryName}, + + weight = #{weight}, biz_date = #{bizDate}, tms_id = #{tmsId}, @@ -212,4 +248,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" update_time = sysdate() where bill_id = #{billId} + + + update prc_order_bill + set ar_bill_status = #{toStatus}, updated_by = #{operator}, update_time = sysdate() + where ar_bill_status = #{fromStatus} + and bill_id in + + #{billId} + + + + + update prc_order_bill + set ar_bill_status = #{toStatus}, updated_by = #{operator}, update_time = sysdate() + where ar_bill_status = #{fromStatus} + and bill_id in + + #{billId} + + diff --git a/bms-back/ruoyi-system/src/main/resources/templates/ar/ar_bill_template.xlsx b/bms-back/ruoyi-system/src/main/resources/templates/ar/ar_bill_template.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..749df8af68714ae3280def988552de9517b396a1 GIT binary patch literal 11983 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 'VOID' +) billed ON billed.prc_bill_id = o.bill_id +SET o.ar_bill_status = 'BILLED' +WHERE COALESCE(o.ar_bill_status, 'UNBILLED') <> 'BILLED'; + +-- 4. 账单唯一性改为“客户 + 币种 + 截止日 + 版本”。 +-- 同一截止日的作废账单保留历史,新账单使用递增版本号。 +DROP TEMPORARY TABLE IF EXISTS tmp_ar_bill_versions; +CREATE TEMPORARY TABLE tmp_ar_bill_versions AS + SELECT ar_bill_id, + ROW_NUMBER() OVER ( + PARTITION BY customer_id, currency, bill_period_end + ORDER BY create_time, ar_bill_id + ) AS version_no + FROM ar_bill; + +UPDATE ar_bill a +JOIN tmp_ar_bill_versions v ON v.ar_bill_id = a.ar_bill_id +SET a.bill_version = v.version_no; +DROP TEMPORARY TABLE tmp_ar_bill_versions; + +SET @has_old_unique := ( + SELECT COUNT(*) + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db_name + AND TABLE_NAME = 'ar_bill' + AND INDEX_NAME = 'uk_ar_bill_customer_period' +); +SET @sql := IF(@has_old_unique > 0, + 'ALTER TABLE ar_bill DROP INDEX uk_ar_bill_customer_period', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @has_new_unique := ( + SELECT COUNT(*) + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db_name + AND TABLE_NAME = 'ar_bill' + AND INDEX_NAME = 'uk_ar_bill_customer_cutoff_version' +); +SET @sql := IF(@has_new_unique = 0, + 'ALTER TABLE ar_bill ADD UNIQUE KEY uk_ar_bill_customer_cutoff_version (customer_id, currency, bill_period_end, bill_version)', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @has_cutoff_index := ( + SELECT COUNT(*) + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db_name + AND TABLE_NAME = 'ar_bill' + AND INDEX_NAME = 'idx_ar_bill_customer_cutoff' +); +SET @sql := IF(@has_cutoff_index = 0, + 'ALTER TABLE ar_bill ADD INDEX idx_ar_bill_customer_cutoff (customer_id, currency, bill_period_end)', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 5. 详情页按账单加载费用时使用的排序索引。可重复执行,不会重复创建。 +SET @has_detail_fee_index := ( + SELECT COUNT(*) + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db_name + AND TABLE_NAME = 'ar_bill_detail_fee' + AND INDEX_NAME = 'idx_ar_detail_fee_order' +); +SET @sql := IF(@has_detail_fee_index = 0, + 'ALTER TABLE ar_bill_detail_fee ADD INDEX idx_ar_detail_fee_order (detail_id, fee_no, detail_fee_id)', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/bms-back/sql/ar/ar_bill_runtime_optimization.sql b/bms-back/sql/ar/ar_bill_runtime_optimization.sql new file mode 100644 index 00000000..adb29288 --- /dev/null +++ b/bms-back/sql/ar/ar_bill_runtime_optimization.sql @@ -0,0 +1,84 @@ +-- 应收模块运行期优化脚本(不回填历史数据) +-- +-- 用于已经存在业务表的环境。脚本可重复执行:字段或索引已存在时自动跳过, +-- 不会修改历史订单的 ar_bill_status,也不会重算历史账单版本。 +SET NAMES utf8mb4; +SET @db_name := DATABASE(); + +-- 1. 账单唯一性调整。 +-- 旧版本 uk_ar_bill_customer_period 会禁止同一客户同一账期再次生成账单, +-- 与 bill_version 版本化方案冲突,必须删除。 +SET @has_old_bill_unique := ( + SELECT COUNT(*) + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db_name + AND TABLE_NAME = 'ar_bill' + AND INDEX_NAME = 'uk_ar_bill_customer_period' +); +SET @sql := IF(@has_old_bill_unique > 0, + 'ALTER TABLE ar_bill DROP INDEX uk_ar_bill_customer_period', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 新唯一键要求已有数据的 bill_version 已正确区分。 +-- 如果这里创建失败,请先执行 ar_bill_cutoff_status_migration.sql 回填历史版本。 +SET @has_bill_version_unique := ( + SELECT COUNT(*) + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db_name + AND TABLE_NAME = 'ar_bill' + AND INDEX_NAME = 'uk_ar_bill_customer_cutoff_version' +); +SET @sql := IF(@has_bill_version_unique = 0, + 'ALTER TABLE ar_bill ADD UNIQUE KEY uk_ar_bill_customer_cutoff_version (customer_id, currency, bill_period_end, bill_version)', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 2. 计费订单应收账单占用状态。 +SET @has_ar_bill_status := ( + SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db_name + AND TABLE_NAME = 'prc_order_bill' + AND COLUMN_NAME = 'ar_bill_status' +); +SET @sql := IF(@has_ar_bill_status = 0, + 'ALTER TABLE prc_order_bill ADD COLUMN ar_bill_status VARCHAR(20) NOT NULL DEFAULT ''UNBILLED'' COMMENT ''应收账单占用状态:UNBILLED/BILLED'' AFTER currency', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 3. 订单预览、占用和释放使用的组合索引。 +SET @has_order_status_index := ( + SELECT COUNT(*) + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db_name + AND TABLE_NAME = 'prc_order_bill' + AND INDEX_NAME = 'idx_bill_ar_status' +); +SET @sql := IF(@has_order_status_index = 0, + 'ALTER TABLE prc_order_bill ADD INDEX idx_bill_ar_status (customer_id, currency, ar_bill_status, biz_date)', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 4. 详情页批量关联费用时使用的排序索引。 +SET @has_detail_fee_index := ( + SELECT COUNT(*) + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db_name + AND TABLE_NAME = 'ar_bill_detail_fee' + AND INDEX_NAME = 'idx_ar_detail_fee_order' +); +SET @sql := IF(@has_detail_fee_index = 0, + 'ALTER TABLE ar_bill_detail_fee ADD INDEX idx_ar_detail_fee_order (detail_id, fee_no, detail_fee_id)', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/bms-back/sql/ar/ar_menu.sql b/bms-back/sql/ar/ar_menu.sql new file mode 100644 index 00000000..c5b00448 --- /dev/null +++ b/bms-back/sql/ar/ar_menu.sql @@ -0,0 +1,57 @@ +-- 应收账单菜单和权限 +SET NAMES utf8mb4; + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2400, N'应收管理', 0, 10, 'ar', NULL, '', '', 1, 0, 'M', '0', '0', '', 'money', 'admin', sysdate(), N'应收账单、收款与核销' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2400); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2401, N'应收账单', 2400, 1, 'bill', 'ar/bill/index', 'ArBill', 1, 0, 'C', '0', '0', 'ar:bill:list', 'list', 'admin', sysdate(), N'正式应收账单' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2401); +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2402, N'账单查询', 2401, 1, '', NULL, 1, 0, 'F', '0', '0', 'ar:bill:query', '#', 'admin', sysdate() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2402); +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2403, N'生成账单', 2401, 2, '', NULL, 1, 0, 'F', '0', '0', 'ar:bill:generate', '#', 'admin', sysdate() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2403); +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2404, N'确认账单', 2401, 3, '', NULL, 1, 0, 'F', '0', '0', 'ar:bill:confirm', '#', 'admin', sysdate() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2404); +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2405, N'作废账单', 2401, 4, '', NULL, 1, 0, 'F', '0', '0', 'ar:bill:void', '#', 'admin', sysdate() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2405); +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2406, N'导出账单', 2401, 5, '', NULL, 1, 0, 'F', '0', '0', 'ar:bill:export', '#', 'admin', sysdate() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2406); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2410, N'应收收款', 2400, 2, 'receipt', 'ar/receipt/index', 'ArReceipt', 1, 0, 'C', '0', '0', 'ar:receipt:list', 'money', 'admin', sysdate(), N'收款登记与核销' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2410); +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2411, N'收款登记', 2410, 1, '', NULL, 1, 0, 'F', '0', '0', 'ar:receipt:add', '#', 'admin', sysdate() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2411); +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2412, N'批量核销', 2410, 2, '', NULL, 1, 0, 'F', '0', '0', 'ar:receipt:writeoff', '#', 'admin', sysdate() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2412); +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2413, N'取消核销', 2410, 3, '', NULL, 1, 0, 'F', '0', '0', 'ar:receipt:cancel', '#', 'admin', sysdate() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2413); +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2414, N'取消收款', 2410, 4, '', NULL, 1, 0, 'F', '0', '0', 'ar:receipt:cancel', '#', 'admin', sysdate() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2414); + +INSERT INTO sys_menu ( + menu_id, menu_name, parent_id, order_num, path, component, + is_frame, is_cache, menu_type, visible, status, perms, icon, + create_by, create_time +) +SELECT + 2420, N'应收日志', 2400, 3, 'log', 'ar/bill/log', + 1, 0, 'C', '0', '0', 'ar:log:list', 'log', + 'admin', sysdate() +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2420); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, m.menu_id FROM (SELECT 2400 AS menu_id UNION SELECT 2401 UNION SELECT 2402 UNION SELECT 2403 UNION SELECT 2404 UNION SELECT 2405 UNION SELECT 2406 UNION SELECT 2410 UNION SELECT 2411 UNION SELECT 2412 UNION SELECT 2413 UNION SELECT 2414 UNION SELECT 2420) m +WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id); diff --git a/bms-back/sql/price/bill.sql b/bms-back/sql/price/bill.sql index 6aa9fcf6..59c11568 100644 --- a/bms-back/sql/price/bill.sql +++ b/bms-back/sql/price/bill.sql @@ -55,6 +55,7 @@ CREATE TABLE IF NOT EXISTS prc_order_bill ( income_total DECIMAL(10,2) DEFAULT NULL COMMENT '应收总额', pay_total DECIMAL(10,2) DEFAULT NULL COMMENT '应付总额', currency VARCHAR(10) NOT NULL DEFAULT 'CNY' COMMENT '币种', + ar_bill_status VARCHAR(20) NOT NULL DEFAULT 'UNBILLED' COMMENT '应收账单占用状态:UNBILLED/BILLED', round_mode VARCHAR(20) NOT NULL DEFAULT 'HALF_UP' COMMENT '取值方式', created_by VARCHAR(64) DEFAULT '' COMMENT '创建人', create_time DATETIME DEFAULT NULL COMMENT '创建时间', @@ -64,6 +65,7 @@ CREATE TABLE IF NOT EXISTS prc_order_bill ( UNIQUE KEY uk_bill_order (order_no), UNIQUE KEY uk_bill_key (bill_key), INDEX idx_bill_customer (customer_code, biz_date), + INDEX idx_bill_ar_status (customer_id, currency, ar_bill_status, biz_date), INDEX idx_bill_product (product_code, country_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单信息主表'; diff --git a/bms-front/src/api/ar/bill.js b/bms-front/src/api/ar/bill.js new file mode 100644 index 00000000..4b0eb61d --- /dev/null +++ b/bms-front/src/api/ar/bill.js @@ -0,0 +1,45 @@ +import request from '@/utils/request' + +export function listArBill(query) { + return request({ url: '/ar/bill/list', method: 'get', params: query }) +} + +export function getArBill(arBillId) { + return request({ url: '/ar/bill/' + arBillId, method: 'get', timeout: 30000 }) +} + +export function getDefaultArBankAccount(currency = 'CNY') { + return request({ url: '/ar/bill/default-bank-account', method: 'get', params: { currency } }) +} + +export function generateArBill(data) { + return request({ url: '/ar/bill/generate', method: 'post', data, timeout: 60000 }) +} + +export function previewArBill(query) { + return request({ url: '/ar/bill/generate/preview', method: 'get', params: query }) +} + +export function confirmArBill(arBillId, data = {}) { + return request({ url: '/ar/bill/' + arBillId + '/confirm', method: 'put', data }) +} + +export function voidArBill(arBillId, data) { + return request({ url: '/ar/bill/' + arBillId + '/void', method: 'put', data }) +} + +export function exportArBill(arBillId) { + return request({ url: '/ar/bill/' + arBillId + '/export', method: 'post' }) +} + +export function listArBillExports(arBillId) { + return request({ url: '/ar/bill/' + arBillId + '/exports', method: 'get' }) +} + +export function downloadArBillExport(exportId) { + return request({ url: '/ar/bill/export/' + exportId + '/download', method: 'get', responseType: 'blob' }) +} + +export function listArBillLogs(query) { + return request({ url: '/ar/bill/log/list', method: 'get', params: query }) +} diff --git a/bms-front/src/api/ar/receipt.js b/bms-front/src/api/ar/receipt.js new file mode 100644 index 00000000..1fc6357d --- /dev/null +++ b/bms-front/src/api/ar/receipt.js @@ -0,0 +1,25 @@ +import request from '@/utils/request' + +export function listArReceipt(query) { + return request({ url: '/ar/receipt/list', method: 'get', params: query }) +} + +export function addArReceipt(data) { + return request({ url: '/ar/receipt', method: 'post', data }) +} + +export function cancelArReceipt(receiptId, data = {}) { + return request({ url: '/ar/receipt/' + receiptId + '/cancel', method: 'put', data }) +} + +export function listArWriteoffs(receiptId) { + return request({ url: '/ar/receipt/' + receiptId + '/writeoffs', method: 'get' }) +} + +export function writeoffArReceipt(data) { + return request({ url: '/ar/receipt/writeoff', method: 'post', data }) +} + +export function cancelArWriteoff(writeoffId, data = {}) { + return request({ url: '/ar/receipt/writeoff/' + writeoffId + '/cancel', method: 'put', data }) +} diff --git a/bms-front/src/components/BasicTable/index.vue b/bms-front/src/components/BasicTable/index.vue index 1a1d849a..1757d976 100644 --- a/bms-front/src/components/BasicTable/index.vue +++ b/bms-front/src/components/BasicTable/index.vue @@ -13,7 +13,11 @@ v-show="showSearch" > 搜索 重置 diff --git a/bms-front/src/constants/ar/bill.js b/bms-front/src/constants/ar/bill.js new file mode 100644 index 00000000..c3f2d0cb --- /dev/null +++ b/bms-front/src/constants/ar/bill.js @@ -0,0 +1,23 @@ +export const AR_BILL_STATUS = Object.freeze({ + DRAFT: 'DRAFT', + CONFIRMED: 'CONFIRMED', + PARTIAL_WRITTEN_OFF: 'PARTIAL_WRITTEN_OFF', + WRITTEN_OFF: 'WRITTEN_OFF', + VOID: 'VOID' +}) + +export const AR_BILL_STATUS_OPTIONS = Object.freeze([ + { label: '草稿', value: AR_BILL_STATUS.DRAFT }, + { label: '已确认', value: AR_BILL_STATUS.CONFIRMED }, + { label: '部分核销', value: AR_BILL_STATUS.PARTIAL_WRITTEN_OFF }, + { label: '已核销', value: AR_BILL_STATUS.WRITTEN_OFF }, + { label: '已作废', value: AR_BILL_STATUS.VOID } +]) + +export const AR_BILL_STATUS_TYPES = Object.freeze({ + [AR_BILL_STATUS.DRAFT]: 'info', + [AR_BILL_STATUS.CONFIRMED]: 'primary', + [AR_BILL_STATUS.PARTIAL_WRITTEN_OFF]: 'warning', + [AR_BILL_STATUS.WRITTEN_OFF]: 'success', + [AR_BILL_STATUS.VOID]: 'danger' +}) diff --git a/bms-front/src/views/ar/bill/components/BillDetail.vue b/bms-front/src/views/ar/bill/components/BillDetail.vue new file mode 100644 index 00000000..2bb34d81 --- /dev/null +++ b/bms-front/src/views/ar/bill/components/BillDetail.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/bms-front/src/views/ar/bill/components/GenerateBillDialog.vue b/bms-front/src/views/ar/bill/components/GenerateBillDialog.vue new file mode 100644 index 00000000..d05b3d67 --- /dev/null +++ b/bms-front/src/views/ar/bill/components/GenerateBillDialog.vue @@ -0,0 +1,224 @@ + + + + + diff --git a/bms-front/src/views/ar/bill/index.vue b/bms-front/src/views/ar/bill/index.vue new file mode 100644 index 00000000..1d4d417a --- /dev/null +++ b/bms-front/src/views/ar/bill/index.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/bms-front/src/views/ar/bill/log.vue b/bms-front/src/views/ar/bill/log.vue new file mode 100644 index 00000000..de1162f8 --- /dev/null +++ b/bms-front/src/views/ar/bill/log.vue @@ -0,0 +1,30 @@ + + + diff --git a/bms-front/src/views/ar/receipt/index.vue b/bms-front/src/views/ar/receipt/index.vue new file mode 100644 index 00000000..96f49ec0 --- /dev/null +++ b/bms-front/src/views/ar/receipt/index.vue @@ -0,0 +1,105 @@ + + + + +