feat: 开票申请支持结算单重复选择并自动分摊控制累计额度

- 结算单候选接口放开未收票过滤,仅保留剩余可开票金额>0,响应新增已申请开票金额
- 保存申请时本次开票金额按选择顺序自动分摊到各结算单登记占用,摊不完报错,删除新建时未收票硬校验
- 详情实时重算已选结算单剩余可开票金额(排除本申请)
- 附改造说明与操作文档
This commit is contained in:
刘泉佳
2026-10-19 23:30:04 +08:00
parent 20038ae187
commit 44e3ecfaa2
4 changed files with 154 additions and 23 deletions
@@ -84,10 +84,9 @@ public class InvoiceApplicationController extends BladeController {
public R<IPage<Map<String, Object>>> settlementCandidates(Query query,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String contractCategory,
@RequestParam(defaultValue = "receivable") String settlementType,
@RequestParam(defaultValue = "unreceived") String invoiceStatus) {
@RequestParam(defaultValue = "receivable") String settlementType) {
return R.data(invoiceApplicationService.settlementCandidates(
Condition.getPage(query), keyword, contractCategory, settlementType, invoiceStatus));
Condition.getPage(query), keyword, contractCategory, settlementType));
}
@GetMapping("/settlement-details")
@@ -45,7 +45,7 @@ public interface IInvoiceApplicationService extends BaseService<InvoiceApplicati
IPage<InvoiceApplicationVO> selectPage(IPage<InvoiceApplication> page, InvoiceApplicationVO query);
InvoiceApplicationVO detail(Long id);
IPage<Map<String, Object>> settlementCandidates(IPage<?> page, String keyword, String contractCategory,
String settlementType, String invoiceStatus);
String settlementType);
List<FormalSettlementDetail> settlementDetails(String settlementIds);
Map<String, Object> receiverInformation(String settlementIds);
Long saveDraft(InvoiceApplicationSaveRequest request);
@@ -231,9 +231,23 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
InvoiceApplication entity = existing(id);
InvoiceApplicationVO vo = toListVO(entity);
fillKingdeeLockStatus(List.of(vo));
vo.setSettlements(settlementRelationMapper.selectList(Wrappers.<InvoiceApplicationSettlement>lambdaQuery()
.eq(InvoiceApplicationSettlement::getInvoiceApplicationId, id)
.orderByAsc(InvoiceApplicationSettlement::getCreateTime)));
// 剩余可开票金额按当前占用实时重算(排除本申请),避免编辑时展示保存时点的过期额度
List<InvoiceApplicationSettlement> relations = settlementRelationMapper.selectList(
Wrappers.<InvoiceApplicationSettlement>lambdaQuery()
.eq(InvoiceApplicationSettlement::getInvoiceApplicationId, id)
.orderByAsc(InvoiceApplicationSettlement::getCreateTime));
if (!relations.isEmpty()) {
Map<Long, FormalSettlement> settlementMap = formalSettlementMapper.selectBatchIds(
relations.stream().map(InvoiceApplicationSettlement::getFormalSettlementId).distinct().toList())
.stream().collect(Collectors.toMap(FormalSettlement::getId, Function.identity(), (first, duplicate) -> first));
relations.forEach(relation -> {
FormalSettlement settlement = settlementMap.get(relation.getFormalSettlementId());
if (settlement != null) {
relation.setAvailableInvoiceAmount(availableAmount(settlement, id));
}
});
}
vo.setSettlements(relations);
List<InvoiceApplicationSheet> sheets = sheetMapper.selectList(Wrappers.<InvoiceApplicationSheet>lambdaQuery()
.eq(InvoiceApplicationSheet::getInvoiceApplicationId, id).orderByAsc(InvoiceApplicationSheet::getSheetNo));
vo.setSheets(sheets.stream().map(sheet -> {
@@ -255,9 +269,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
@Override
public IPage<Map<String, Object>> settlementCandidates(IPage<?> page, String keyword,
String contractCategory, String settlementType, String invoiceStatus) {
String contractCategory, String settlementType) {
String candidateSettlementType = Func.isEmpty(settlementType) ? "receivable" : settlementType;
String candidateInvoiceStatus = Func.isEmpty(invoiceStatus) ? "unreceived" : invoiceStatus;
List<Long> contractIds = Func.isEmpty(contractCategory) ? List.of()
: contractManageMapper.selectList(Wrappers.<ContractManage>lambdaQuery()
.select(ContractManage::getId)
@@ -269,7 +282,6 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
}
List<FormalSettlement> settlements = formalSettlementMapper.selectList(Wrappers.<FormalSettlement>lambdaQuery()
.eq(FormalSettlement::getSettlementType, candidateSettlementType)
.eq(FormalSettlement::getInvoiceStatus, candidateInvoiceStatus)
.eq(FormalSettlement::getApprovalStatus, TransportBusiConstant.APPROVED)
.eq(FormalSettlement::getStatus, 1)
.in(Func.isNotEmpty(contractCategory), FormalSettlement::getContractId, contractIds)
@@ -337,6 +349,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
row.put("receiverName", settlement.getPayerName());
row.put("settlementAmount", money(settlement.getSettlementAmount()));
row.put("availableInvoiceAmount", available);
row.put("invoicedAmount", money(settlement.getSettlementAmount()).subtract(available));
return row;
}
@@ -402,28 +415,31 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
.collect(Collectors.toMap(InvoiceApplicationSaveRequest.SettlementRow::getSettlementId,
Function.identity(), (first, duplicate) -> first, LinkedHashMap::new)).values().stream().toList();
List<FormalSettlement> settlements = requestedRows.stream().map(row -> availableSettlement(row.getSettlementId())).toList();
if (settlements.stream().anyMatch(settlement -> !"receivable".equals(settlement.getSettlementType())
|| (creating && !"unreceived".equals(settlement.getInvoiceStatus())))) {
throw new ServiceException("只能选择审批通过、未作废、未收票的应收正式结算单");
if (settlements.stream().anyMatch(settlement -> !"receivable".equals(settlement.getSettlementType()))) {
throw new ServiceException("只能选择审批通过、未作废的应收正式结算单");
}
assertCompatible(settlements);
FormalSettlement first = settlements.get(0);
Map<Long, FormalSettlement> settlementMap = settlements.stream()
.collect(Collectors.toMap(FormalSettlement::getId, Function.identity()));
BigDecimal totalAvailable = BigDecimal.ZERO;
for (InvoiceApplicationSaveRequest.SettlementRow row : requestedRows) {
FormalSettlement settlement = settlementMap.get(row.getSettlementId());
BigDecimal available = availableAmount(settlement, entity.getId());
BigDecimal allocated = nonNegative(row.getAllocatedInvoiceAmount(), "分摊发票金额");
if (allocated.compareTo(available) > 0) {
throw new ServiceException("结算单" + settlement.getFormalSettlementNo() + "的分摊金额超过剩余可开票金额");
}
totalAvailable = totalAvailable.add(available);
}
BigDecimal lineTotal = validateSheets(request.getSheets());
if (lineTotal.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("本次开票金额必须大于0");
}
// 本次开票金额按选择顺序自动分摊到各结算单并登记累计占用,摊不完即超过可开票金额
BigDecimal totalAvailable = BigDecimal.ZERO;
BigDecimal remaining = lineTotal;
for (InvoiceApplicationSaveRequest.SettlementRow row : requestedRows) {
FormalSettlement settlement = settlementMap.get(row.getSettlementId());
BigDecimal available = availableAmount(settlement, entity.getId());
totalAvailable = totalAvailable.add(available);
BigDecimal allocated = available.min(remaining);
row.setAllocatedInvoiceAmount(allocated);
remaining = remaining.subtract(allocated);
}
if (remaining.compareTo(BigDecimal.ZERO) > 0) {
throw new ServiceException("本次开票金额不能超过可开票金额");
}
CustomerArchive customer = findCustomer(first.getPayerName());
if (customer == null) throw new ServiceException("未找到正式结算单受票方对应的客商档案");
List<CustomerInvoiceInfoVO> invoiceInfos = activeInvoiceInfoVOs(customer.getId());