完善首付款管理模块

This commit is contained in:
2026-08-27 23:51:55 +08:00
parent cce9ac0f3d
commit 1be1a18ea8
15 changed files with 611 additions and 69 deletions
@@ -70,9 +70,11 @@ import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -183,14 +185,16 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
List<FormalSettlement> settlements = distinctIds(settlementIds).stream().map(this::availableSettlement).toList();
assertCompatible(settlements);
FormalSettlement first = settlements.get(0);
CustomerArchive customer = findCustomer(first.getPayerName());
CustomerArchive customer = findCustomer(first.getPayeeName());
List<CustomerInvoiceInfo> invoiceInfos = customer == null ? List.of() : activeInvoiceInfos(customer.getId());
Map<String, Object> result = new LinkedHashMap<>();
result.put("issuerName", first.getPayeeName());
result.put("receiverName", first.getPayerName());
result.put("customer", customer);
result.put("invoiceInfos", customer == null ? List.of() : customerInvoiceInfoMapper.selectList(
Wrappers.<CustomerInvoiceInfo>lambdaQuery().eq(CustomerInvoiceInfo::getCustomerId, customer.getId())
.eq(CustomerInvoiceInfo::getStatus, 1).orderByDesc(CustomerInvoiceInfo::getIsDefault)));
// 应付结算单的开票方(payeeName)是客商,受票方信息和部门邮箱均来源于该客商发票信息。
result.put("invoices", invoiceInfos);
result.put("invoiceInfos", invoiceInfos);
result.put("departmentEmails", invoiceInfoEmails(invoiceInfos));
result.put("contacts", customer == null ? List.of() : customerContactMapper.selectList(
Wrappers.<CustomerContact>lambdaQuery().eq(CustomerContact::getCustomerId, customer.getId())
.eq(CustomerContact::getStatus, 1).orderByDesc(CustomerContact::getIsDefault)));
@@ -217,7 +221,6 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
Map<Long, FormalSettlement> settlementMap = settlements.stream()
.collect(Collectors.toMap(FormalSettlement::getId, Function.identity()));
BigDecimal totalAvailable = BigDecimal.ZERO;
BigDecimal allocatedTotal = BigDecimal.ZERO;
for (InvoiceApplicationSaveRequest.SettlementRow row : requestedRows) {
FormalSettlement settlement = settlementMap.get(row.getSettlementId());
BigDecimal available = availableAmount(settlement, entity.getId());
@@ -226,21 +229,18 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
throw new ServiceException("结算单" + settlement.getFormalSettlementNo() + "的分摊金额超过剩余可开票金额");
}
totalAvailable = totalAvailable.add(available);
allocatedTotal = allocatedTotal.add(allocated);
}
BigDecimal lineTotal = validateSheets(request.getSheets());
if (lineTotal.compareTo(BigDecimal.ZERO) <= 0) {
throw new ServiceException("本次开票金额必须大于0");
}
if (lineTotal.compareTo(allocatedTotal) != 0) {
throw new ServiceException("开票商品行金额合计必须等于结算单分摊金额合计");
}
CustomerArchive customer = findCustomer(first.getPayerName());
CustomerInvoiceInfo invoiceInfo = customerInvoiceInfoMapper.selectById(request.getReceiverInvoiceInfoId());
if (customer == null || invoiceInfo == null || !Objects.equals(customer.getId(), invoiceInfo.getCustomerId())
|| Objects.equals(invoiceInfo.getIsDeleted(), 1) || !Objects.equals(invoiceInfo.getStatus(), 1)) {
throw new ServiceException("请选择受票方有效的开票信息");
}
CustomerArchive customer = findCustomer(first.getPayeeName());
if (customer == null) throw new ServiceException("未找到正式结算单开票方对应的客商档案");
List<CustomerInvoiceInfo> invoiceInfos = activeInvoiceInfos(customer.getId());
CustomerInvoiceInfo invoiceInfo = invoiceInfos.stream()
.filter(item -> Objects.equals(item.getId(), request.getReceiverInvoiceInfoId()))
.findFirst().orElseThrow(() -> new ServiceException("请选择受票方有效的开票信息"));
String departmentEmails = normalizeDepartmentEmails(request.getDepartmentEmails(), invoiceInfos);
String invoiceTitle = required(invoiceInfo.getInvoiceTitle(), "受票方单位");
String taxpayerNo = limit(invoiceInfo.getTaxNo(), 20, "纳税人识别号");
String bankName = limit(invoiceInfo.getBankName(), 100, "开户行");
@@ -271,7 +271,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
entity.setInvoiceAmount(lineTotal);
entity.setUndertakingDeptId(first.getDeptId());
entity.setUndertakingDeptName(first.getDeptName());
entity.setDepartmentEmails(normalizeEmails(request.getDepartmentEmails()));
entity.setDepartmentEmails(departmentEmails);
entity.setReceiverInvoiceInfoId(invoiceInfo.getId());
entity.setTaxpayerNo(taxpayerNo);
entity.setBankName(bankName);
@@ -535,10 +535,40 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
private CustomerArchive findCustomer(String name) {
if (Func.isEmpty(name)) return null;
return customerArchiveMapper.selectOne(Wrappers.<CustomerArchive>lambdaQuery()
.and(wrapper -> wrapper.eq(CustomerArchive::getFullName, name).or().eq(CustomerArchive::getShortName, name))
String value = name.trim();
CustomerArchive customer = customerArchiveMapper.selectOne(Wrappers.<CustomerArchive>lambdaQuery()
.and(wrapper -> wrapper.eq(CustomerArchive::getFullName, value).or().eq(CustomerArchive::getShortName, value))
.eq(CustomerArchive::getStatus, 1)
.eq(CustomerArchive::getIsDeleted, 0).last("limit 1"));
if (customer != null) return customer;
String normalized = normalizeCustomerName(value);
return customerArchiveMapper.selectList(Wrappers.<CustomerArchive>lambdaQuery()
.eq(CustomerArchive::getStatus, 1)
.eq(CustomerArchive::getIsDeleted, 0))
.stream()
.filter(item -> normalized.equals(normalizeCustomerName(item.getFullName()))
|| normalized.equals(normalizeCustomerName(item.getShortName())))
.findFirst().orElse(null);
}
private String normalizeCustomerName(String value) {
return value == null ? "" : value.replaceAll("\\s+", "");
}
private List<CustomerInvoiceInfo> activeInvoiceInfos(Long customerId) {
return customerInvoiceInfoMapper.selectList(Wrappers.<CustomerInvoiceInfo>lambdaQuery()
.eq(CustomerInvoiceInfo::getCustomerId, customerId)
.eq(CustomerInvoiceInfo::getStatus, 1)
.eq(CustomerInvoiceInfo::getIsDeleted, 0)
.orderByDesc(CustomerInvoiceInfo::getIsDefault)
.orderByAsc(CustomerInvoiceInfo::getCreateTime));
}
private List<String> invoiceInfoEmails(List<CustomerInvoiceInfo> invoiceInfos) {
return invoiceInfos.stream().map(CustomerInvoiceInfo::getEmail).filter(Func::isNotEmpty)
.flatMap(value -> splitEmails(value).stream())
.collect(Collectors.toMap(email -> email.toLowerCase(Locale.ROOT), Function.identity(),
(first, duplicate) -> first, LinkedHashMap::new)).values().stream().toList();
}
private void changeStatus(Long id, String from, String to, String actionType, String node, String reason) {
@@ -587,12 +617,28 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
.likeRight(InvoiceApplication::getApplicationNo, prefix)) + 1);
}
private String normalizeEmails(String value) {
List<String> emails = value == null ? List.of() : List.of(value.split("[;,,;]"));
List<String> normalized = emails.stream().map(String::trim).filter(item -> !item.isEmpty()).distinct().toList();
private String normalizeDepartmentEmails(String value, List<CustomerInvoiceInfo> invoiceInfos) {
Map<String, String> configuredEmails = invoiceInfoEmails(invoiceInfos).stream()
.collect(Collectors.toMap(email -> email.toLowerCase(Locale.ROOT), Function.identity(),
(first, duplicate) -> first, LinkedHashMap::new));
if (configuredEmails.isEmpty()) throw new ServiceException("当前客商的开票信息未配置邮箱");
List<String> normalized = splitEmails(value).stream()
.collect(Collectors.toMap(email -> email.toLowerCase(Locale.ROOT), Function.identity(),
(first, duplicate) -> first, LinkedHashMap::new)).values().stream().toList();
if (normalized.isEmpty() || normalized.size() > 3) throw new ServiceException("部门邮箱必填且最多选择3个");
normalized.forEach(email -> validateEmail(email, "部门邮箱"));
return String.join(";", normalized);
List<String> selectedEmails = normalized.stream().map(email -> {
validateEmail(email, "部门邮箱");
String configuredEmail = configuredEmails.get(email.toLowerCase(Locale.ROOT));
if (configuredEmail == null) throw new ServiceException("部门邮箱必须选择当前客商开票信息中配置的邮箱");
return configuredEmail;
}).distinct().toList();
return String.join(";", selectedEmails);
}
private List<String> splitEmails(String value) {
if (Func.isEmpty(value)) return List.of();
return Arrays.stream(value.split("[;,,;]"))
.map(String::trim).filter(item -> !item.isEmpty()).toList();
}
private String validateEmail(String value, String name) {
@@ -212,6 +212,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
InvoiceReceipt entity = creating ? new InvoiceReceipt() : editable(request.getId());
List<Long> oldSettlementIds = entity.getId() == null ? List.of() : relationSettlementIds(entity.getId());
KingdeeInvoicePool invoice = lockedInvoice(request.getKingdeeInvoicePoolId());
if (invoice == null) invoice = invoiceSnapshot(request);
assertInvoiceUnused(invoice, entity.getId());
Map<Long, InvoiceReceiptSaveRequest.SettlementRow> requestedRows = distinctSettlementRows(
@@ -219,10 +220,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
Map<Long, FormalSettlement> settlementMap = lockSettlements(requestedRows.keySet().stream().sorted().toList());
List<FormalSettlement> settlements = requestedRows.keySet().stream().map(settlementMap::get).toList();
assertCompatible(settlements);
assertInvoiceParties(invoice, settlements.get(0));
BigDecimal invoiceAmount = positive(invoice.getInvoiceAmount(), "开票金额");
BigDecimal allocatedTotal = BigDecimal.ZERO;
for (Map.Entry<Long, InvoiceReceiptSaveRequest.SettlementRow> entry : requestedRows.entrySet()) {
FormalSettlement settlement = settlementMap.get(entry.getKey());
BigDecimal allocated = nonNegative(entry.getValue().getAllocatedInvoiceAmount(), "分摊发票金额");
@@ -231,10 +229,6 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
throw new ServiceException("结算单" + settlement.getFormalSettlementNo()
+ "的累计收票金额不能超过结算总应付含税金额");
}
allocatedTotal = allocatedTotal.add(allocated);
}
if (allocatedTotal.compareTo(invoiceAmount) != 0) {
throw new ServiceException("分摊发票金额总和必须等于发票开票金额");
}
if (creating) {
@@ -422,14 +416,46 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
}
private KingdeeInvoicePool lockedInvoice(Long id) {
KingdeeInvoicePool invoice = invoicePoolMapper.selectOne(Wrappers.<KingdeeInvoicePool>lambdaQuery()
return invoicePoolMapper.selectOne(Wrappers.<KingdeeInvoicePool>lambdaQuery()
.eq(KingdeeInvoicePool::getId, id)
.last("FOR UPDATE"));
if (invoice == null || Objects.equals(invoice.getIsDeleted(), 1)
|| !Objects.equals(invoice.getStatus(), 1)) {
throw new ServiceException("金蝶票据池发票不存在或已失效");
}
required(invoice.getInvoiceNo(), "发票号码");
}
private KingdeeInvoicePool invoiceSnapshot(InvoiceReceiptSaveRequest request) {
KingdeeInvoicePool invoice = new KingdeeInvoicePool();
invoice.setId(request.getKingdeeInvoicePoolId());
invoice.setInvoiceNo(request.getInvoiceNo());
invoice.setInvoiceDate(request.getInvoiceDate());
invoice.setInvoiceType(request.getInvoiceType());
invoice.setTaxRate(request.getTaxRate());
invoice.setInvoiceAmount(request.getInvoiceAmount());
invoice.setTaxAmount(request.getTaxAmount());
invoice.setReceiverName(request.getReceiverName());
invoice.setIssuerName(request.getIssuerName());
invoice.setBankName(request.getBankName());
invoice.setBankAccount(request.getBankAccount());
invoice.setIssuingBank(request.getIssuingBank());
invoice.setKingdeeBillNo(request.getKingdeeBillNo());
invoice.setKingdeeStatus(request.getKingdeeStatus());
return invoice;
}
private KingdeeInvoicePool invoiceSnapshot(InvoiceReceipt receipt) {
KingdeeInvoicePool invoice = new KingdeeInvoicePool();
invoice.setId(receipt.getKingdeeInvoicePoolId());
invoice.setInvoiceNo(receipt.getInvoiceNo());
invoice.setInvoiceDate(receipt.getInvoiceDate());
invoice.setInvoiceType(receipt.getInvoiceType());
invoice.setTaxRate(receipt.getTaxRate());
invoice.setInvoiceAmount(receipt.getInvoiceAmount());
invoice.setTaxAmount(receipt.getTaxAmount());
invoice.setReceiverName(receipt.getReceiverName());
invoice.setIssuerName(receipt.getIssuerName());
invoice.setBankName(receipt.getBankName());
invoice.setBankAccount(receipt.getBankAccount());
invoice.setIssuingBank(receipt.getIssuingBank());
invoice.setKingdeeBillNo(receipt.getKingdeeBillNo());
invoice.setKingdeeStatus(receipt.getKingdeeStatus());
return invoice;
}
@@ -456,17 +482,6 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
}
}
private void assertInvoiceParties(KingdeeInvoicePool invoice, FormalSettlement settlement) {
if (!sameName(invoice.getReceiverName(), settlement.getPayerName())
|| !sameName(invoice.getIssuerName(), settlement.getPayeeName())) {
throw new ServiceException("金蝶发票的开票单位、受票单位与结算单收付款方不一致");
}
}
private boolean sameName(String first, String second) {
return Func.isNotEmpty(first) && Func.isNotEmpty(second) && first.trim().equals(second.trim());
}
private BigDecimal receivedAmount(Long settlementId, Long excludeReceiptId) {
return activeRelations(settlementId, excludeReceiptId).stream()
.map(InvoiceReceiptSettlement::getAllocatedInvoiceAmount)
@@ -528,6 +543,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
private void validateStoredAllocation(InvoiceReceipt entity) {
KingdeeInvoicePool invoice = lockedInvoice(entity.getKingdeeInvoicePoolId());
if (invoice == null) invoice = invoiceSnapshot(entity);
assertInvoiceUnused(invoice, entity.getId());
List<InvoiceReceiptSettlement> relations = settlementRelationMapper.selectList(
Wrappers.<InvoiceReceiptSettlement>lambdaQuery()
@@ -545,8 +561,6 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
.map(item -> settlementMap.get(item.getFormalSettlementId()))
.toList();
assertCompatible(settlements);
assertInvoiceParties(invoice, settlements.get(0));
BigDecimal allocatedTotal = BigDecimal.ZERO;
for (InvoiceReceiptSettlement relation : relations) {
FormalSettlement settlement = settlementMap.get(relation.getFormalSettlementId());
BigDecimal allocated = nonNegative(relation.getAllocatedInvoiceAmount(), "分摊发票金额");
@@ -555,10 +569,6 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
throw new ServiceException("结算单" + settlement.getFormalSettlementNo()
+ "的累计收票金额不能超过结算总应付含税金额");
}
allocatedTotal = allocatedTotal.add(allocated);
}
if (allocatedTotal.compareTo(positive(invoice.getInvoiceAmount(), "开票金额")) != 0) {
throw new ServiceException("分摊发票金额总和必须等于发票开票金额");
}
}
@@ -126,7 +126,7 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
@Override
public List<Map<String, Object>> settlementCandidates(String keyword, Long flowId) {
KingdeeReceiptFlow flow = existing(flowId);
existing(flowId);
List<FormalSettlement> settlements = formalSettlementMapper.selectList(
Wrappers.<FormalSettlement>lambdaQuery()
.eq(FormalSettlement::getSettlementType, RECEIVABLE)
@@ -139,9 +139,7 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
.orderByDesc(FormalSettlement::getCreateTime)
.last("limit 200"));
return settlements.stream()
.filter(settlement -> Func.isEmpty(flow.getCounterpartyName())
|| sameName(flow.getCounterpartyName(), settlement.getPayerName()))
.map(settlement -> candidateRow(settlement))
.map(this::candidateRow)
.filter(row -> ((BigDecimal) row.get("remainingReceiptAmount")).compareTo(BigDecimal.ZERO) > 0)
.toList();
}
@@ -163,7 +161,6 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
Map<Long, FormalSettlement> settlementMap = lockSettlements(settlementIds);
List<FormalSettlement> settlements = settlementIds.stream().map(settlementMap::get).toList();
assertCompatible(settlements);
assertCounterparty(flow, settlements.get(0));
Map<Long, BigDecimal> previousClaimedMap = new LinkedHashMap<>();
BigDecimal allocatedTotal = BigDecimal.ZERO;
@@ -351,16 +348,6 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl<KingdeeReceiptFlowMa
}
}
private void assertCounterparty(KingdeeReceiptFlow flow, FormalSettlement settlement) {
if (!sameName(flow.getCounterpartyName(), settlement.getPayerName())) {
throw new ServiceException("对方户名与结算单付款方不一致");
}
}
private boolean sameName(String first, String second) {
return Func.isNotEmpty(first) && Func.isNotEmpty(second) && first.trim().equals(second.trim());
}
private BigDecimal settlementClaimedAmount(Long settlementId) {
return claimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.eq(ReceiptClaimSettlement::getFormalSettlementId, settlementId)