!4 fix: 结算调整单重推拦截报错补充具体结算单号
Merge pull request !4 from mindless/feature/20261016-settlement-adjustment-kingdee
This commit is contained in:
+3
@@ -32,6 +32,9 @@ public final class KingdeeLockConstant {
|
||||
/** 来源操作类型:单据删除(解锁回调携带 DELETE 时 WLTMS 废除源单并还原关联数据) */
|
||||
public static final String OPERATION_DELETE = "DELETE";
|
||||
|
||||
/** 来源操作类型:结算调整重推(结算调整单重推后解锁原结算单旧锁记录,非金蝶回调) */
|
||||
public static final String OPERATION_ADJUST_REPUSH = "ADJUST_REPUSH";
|
||||
|
||||
/** 系统标识:WLTMS(运输管理系统,即本系统) */
|
||||
public static final String SYSTEM_WLTMS = "WLTMS";
|
||||
|
||||
|
||||
+16
-1
@@ -7,6 +7,8 @@ import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.transport.listener.KingdeeApprovalSyncListener;
|
||||
import org.springblade.transport.pojo.dto.SettlementAdjustmentSaveRequest;
|
||||
import org.springblade.transport.pojo.dto.SettlementAdjustmentStatusRequest;
|
||||
import org.springblade.transport.pojo.entity.SettlementAdjustment;
|
||||
@@ -33,7 +35,20 @@ public class SettlementAdjustmentController extends BladeController {
|
||||
@PostMapping("/save") public R<Long> save(@RequestBody SettlementAdjustmentSaveRequest request) { return R.data(service.saveDraft(request)); }
|
||||
@PostMapping("/remove") public R remove(@RequestParam Long id) { service.removeDraft(id); return R.success("删除成功"); }
|
||||
@PostMapping("/submit") public R submit(@RequestBody SettlementAdjustmentStatusRequest request) { service.submit(request); return R.success("提交成功"); }
|
||||
@PostMapping("/approve") public R approve(@RequestBody SettlementAdjustmentStatusRequest request) { service.approve(request); return R.success("审批通过"); }
|
||||
@PostMapping("/approve") public R approve(@RequestBody SettlementAdjustmentStatusRequest request) {
|
||||
String hint = service.approve(request);
|
||||
// 未推送场景自动推送失败原因以警告文案返回(data=false 供前端区分提示样式)
|
||||
String syncFailure = KingdeeApprovalSyncListener.pollSyncFailure();
|
||||
if (Func.isNotBlank(syncFailure)) {
|
||||
return R.data(Boolean.FALSE, "审批通过,但自动同步金蝶失败:" + syncFailure);
|
||||
}
|
||||
if (Func.isNotBlank(hint)) {
|
||||
return R.data(Boolean.TRUE, "审批通过;" + hint);
|
||||
}
|
||||
return R.success("审批通过");
|
||||
}
|
||||
@PostMapping("/return") public R returnBill(@RequestBody SettlementAdjustmentStatusRequest request) { service.returnBill(request); return R.success("已驳回"); }
|
||||
@PostMapping("/repush") public R<String> repush(@RequestParam Long id) { return R.data(service.repush(id)); }
|
||||
@GetMapping("/kingdee-writeoff-precheck") public R<Map<String, Object>> kingdeeWriteOffPrecheck(@RequestParam Long formalSettlementId) { return R.data(service.kingdeeWriteOffPrecheck(formalSettlementId)); }
|
||||
@PostMapping("/sync-kingdee") public R<String> syncKingdee(@RequestParam Long id) { return R.data(service.syncKingdee(id)); }
|
||||
}
|
||||
|
||||
+11
@@ -15,6 +15,9 @@ import org.apache.ibatis.annotations.Param;
|
||||
import org.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 结算单项目预付核销 Mapper
|
||||
@@ -37,4 +40,12 @@ public interface SettlementProjectAdvanceWriteOffMapper extends BaseMapper<Settl
|
||||
BigDecimal sumWriteOffBySettlement(@Param("settlementType") String settlementType,
|
||||
@Param("settlementId") Long settlementId);
|
||||
|
||||
/**
|
||||
* 按结算单集合分组统计核销合计(一次 group-by,供批量刷新与列表页使用,避免逐单 N+1)。
|
||||
*
|
||||
* @return settlementId / totalAmount 两列
|
||||
*/
|
||||
List<Map<String, Object>> sumWriteOffBySettlementIds(@Param("settlementType") String settlementType,
|
||||
@Param("settlementIds") Collection<Long> settlementIds);
|
||||
|
||||
}
|
||||
|
||||
+12
@@ -32,4 +32,16 @@
|
||||
AND settlement_id = #{settlementId}
|
||||
</select>
|
||||
|
||||
<select id="sumWriteOffBySettlementIds" resultType="java.util.HashMap">
|
||||
SELECT settlement_id AS settlementId, COALESCE(SUM(write_off_amount), 0) AS totalAmount
|
||||
FROM blade_settlement_project_advance_write_off
|
||||
WHERE is_deleted = 0
|
||||
AND settlement_type = #{settlementType}
|
||||
AND settlement_id IN
|
||||
<foreach collection="settlementIds" item="settlementId" open="(" separator="," close=")">
|
||||
#{settlementId}
|
||||
</foreach>
|
||||
GROUP BY settlement_id
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
+27
@@ -24,6 +24,8 @@ import org.springblade.transport.pojo.dto.FormalSettlementPaymentRequest;
|
||||
import org.springblade.transport.pojo.dto.FormalSettlementBatchPaymentRequest;
|
||||
import org.springblade.transport.pojo.dto.FormalSettlementInvoiceClaimRequest;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 正式结算单服务
|
||||
*
|
||||
@@ -93,4 +95,29 @@ public interface IFormalSettlementService extends BaseService<FormalSettlement>
|
||||
String applyPayment(FormalSettlementPaymentRequest request);
|
||||
List<String> applyPayments(FormalSettlementBatchPaymentRequest request);
|
||||
void claimInvoices(FormalSettlementInvoiceClaimRequest request);
|
||||
|
||||
/**
|
||||
* 结算调整重推:克隆原正式结算单为新单(含明细、明细费项、汇总费项、来源预结算、附件、汇率),
|
||||
* 新单置审批通过、未同步金蝶,来源原单号指向原单;下游关联(开票申请/收款认领/对账)不迁移。
|
||||
* 不推送金蝶,由调用方在事务外调用 syncKingdee 推送
|
||||
*
|
||||
* @param sourceSettlementId 原正式结算单id
|
||||
* @return 新单id与「原明细/原费项 -> 新明细/新费项」id映射(供调整明细应用到新单)
|
||||
*/
|
||||
CloneResult cloneFormalSettlement(Long sourceSettlementId);
|
||||
|
||||
/**
|
||||
* 结算调整重推收尾:原单标记「已调整」、旧锁解锁、追加重推变更记录(不改原单业务数据)
|
||||
*
|
||||
* @param settlementId 原正式结算单id
|
||||
* @param adjustmentNo 结算调整单号
|
||||
* @param newSettlementNo 重推生成的新单号
|
||||
*/
|
||||
void markSettlementAdjusted(Long settlementId, String adjustmentNo, String newSettlementNo);
|
||||
|
||||
/**
|
||||
* 克隆结果:新单id + 原明细id->新明细id + 原费项id->新费项id
|
||||
*/
|
||||
record CloneResult(Long settlementId, Map<Long, Long> detailIdMap, Map<Long, Long> feeIdMap) {
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -60,6 +60,17 @@ public interface IKingdeeBillLockService extends IService<KingdeeBillLock> {
|
||||
*/
|
||||
void checkEditable(String sourceBillType, Long sourceBillId);
|
||||
|
||||
/**
|
||||
* 结算调整重推解锁:把源单当前「锁定中」的锁记录标记为 unlocked(非金蝶回调,仅本地解锁留痕),
|
||||
* 供原结算单被调整重推生成新单后解除旧锁使用;新单另行落新锁记录
|
||||
*
|
||||
* @param sourceBillType WLTMS 单据类型
|
||||
* @param sourceBillId WLTMS 业务主键
|
||||
* @param operator 操作人
|
||||
* @return 解锁的记录数
|
||||
*/
|
||||
int unlockByAdjustRepush(String sourceBillType, Long sourceBillId, String operator);
|
||||
|
||||
/**
|
||||
* 批量查源单当前锁状态(供列表/详情页填充展示标识):
|
||||
* 每源单取最新一条非终态锁记录(locked/failed),unlocked 为终态不返回(前端视为无锁/已解锁)
|
||||
|
||||
+18
-1
@@ -14,7 +14,9 @@ import org.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff;
|
||||
import org.springblade.transport.pojo.vo.ProjectAdvanceWriteOffOptionVO;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 结算单项目预付核销服务
|
||||
@@ -24,7 +26,7 @@ import java.util.List;
|
||||
public interface ISettlementProjectAdvanceWriteOffService {
|
||||
|
||||
/**
|
||||
* 按合同查询可选项目预付(审批通过且仍有可核销余额)。
|
||||
* 按合同查询可选项目预付(审批通过、金蝶已付款且仍有可核销余额)。
|
||||
*/
|
||||
List<ProjectAdvanceWriteOffOptionVO> listOptions(Long contractId, String excludeSettlementType,
|
||||
Long excludeSettlementId);
|
||||
@@ -39,6 +41,11 @@ public interface ISettlementProjectAdvanceWriteOffService {
|
||||
*/
|
||||
BigDecimal sumBySettlement(String settlementType, Long settlementId);
|
||||
|
||||
/**
|
||||
* 按结算单集合分组统计核销合计(一次 group-by,供批量刷新与列表页使用)。
|
||||
*/
|
||||
Map<Long, BigDecimal> sumBySettlementIds(String settlementType, Collection<Long> settlementIds);
|
||||
|
||||
/**
|
||||
* 覆盖保存核销明细,并校验不超过可核销金额与结算单金额。
|
||||
*/
|
||||
@@ -50,4 +57,14 @@ public interface ISettlementProjectAdvanceWriteOffService {
|
||||
*/
|
||||
void deleteBySettlement(String settlementType, Long settlementId);
|
||||
|
||||
/**
|
||||
* 删除指定结算单集合上的核销明细(核销行转移/结算单作废联动),返回删除行数。
|
||||
*/
|
||||
int deleteBySettlementIds(String settlementType, Collection<Long> settlementIds);
|
||||
|
||||
/**
|
||||
* 按付款申请删除其核销明细(付款申请作废联动),返回受影响的正式结算单id。
|
||||
*/
|
||||
List<Long> deleteByPaymentApplicationIds(Collection<Long> paymentApplicationIds);
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -46,11 +46,11 @@ public record KingdeeSyncContext(Map<Long, ContractManage> contractCache,
|
||||
Map<Long, List<FormalSettlementDetail>> settlementDetailCache,
|
||||
Map<Long, FormalSettlement> settlementCache,
|
||||
Map<Long, CustomerReceiptAccount> receiptAccountCache,
|
||||
Map<String, String> feeCategoryByName) {
|
||||
Map<String, String> feeCategoryByName, Map<Long, String> paymentNoCache) {
|
||||
|
||||
/** 空上下文:缓存可写但初始为空,首条记录命中后全批复用 */
|
||||
public static KingdeeSyncContext empty() {
|
||||
return new KingdeeSyncContext(new HashMap<>(), new HashMap<>(), new HashMap<>(),
|
||||
new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
|
||||
new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
|
||||
}
|
||||
}
|
||||
|
||||
+260
-9
@@ -33,12 +33,14 @@ import org.springblade.transport.mapper.InvoiceApplicationMapper;
|
||||
import org.springblade.transport.mapper.InvoiceApplicationSettlementMapper;
|
||||
import org.springblade.transport.mapper.PreSettlementDetailMapper;
|
||||
import org.springblade.transport.mapper.PreSettlementDetailFeeMapper;
|
||||
import org.springblade.transport.mapper.PreSettlementAdvanceMapper;
|
||||
import org.springblade.transport.mapper.PreSettlementMapper;
|
||||
import org.springblade.transport.mapper.PreSettlementSummaryFeeMapper;
|
||||
import org.springblade.transport.mapper.PaymentApplicationMapper;
|
||||
import org.springblade.transport.mapper.PaymentApplicationInvoiceMapper;
|
||||
import org.springblade.transport.mapper.PaymentApplicationSettlementMapper;
|
||||
import org.springblade.transport.mapper.ReceiptClaimSettlementMapper;
|
||||
import org.springblade.transport.mapper.SettlementProjectAdvanceWriteOffMapper;
|
||||
import org.springblade.transport.constant.KingdeeAttachmentConstant;
|
||||
import org.springblade.transport.config.BfiProperties;
|
||||
import org.springblade.transport.constant.KingdeeBfiConstant;
|
||||
@@ -69,6 +71,7 @@ import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice;
|
||||
import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.PreSettlement;
|
||||
import org.springblade.transport.pojo.entity.PreSettlementAdvance;
|
||||
import org.springblade.transport.pojo.entity.PreSettlementDetail;
|
||||
import org.springblade.transport.pojo.entity.PreSettlementDetailFee;
|
||||
import org.springblade.transport.pojo.entity.PreSettlementSummaryFee;
|
||||
@@ -79,9 +82,11 @@ import org.springblade.transport.pojo.entity.ReceiptClaimSettlement;
|
||||
import org.springblade.transport.pojo.entity.ReceivablePayableDetail;
|
||||
import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee;
|
||||
import org.springblade.transport.pojo.entity.SettlementAdjustment;
|
||||
import org.springblade.transport.pojo.entity.SettlementProjectAdvanceWriteOff;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.FormalSettlementVO;
|
||||
import org.springblade.transport.pojo.vo.PreSettlementVO;
|
||||
import org.springblade.transport.pojo.vo.SettlementAdvancePaymentVO;
|
||||
import org.springblade.transport.pojo.vo.SettlementAdjustmentVO;
|
||||
import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.IInvoiceReceiptService;
|
||||
@@ -113,6 +118,8 @@ import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -162,6 +169,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
private final PreSettlementSummaryFeeMapper preSummaryFeeMapper;
|
||||
private final PreSettlementDetailMapper preDetailMapper;
|
||||
private final PreSettlementDetailFeeMapper preDetailFeeMapper;
|
||||
private final PreSettlementAdvanceMapper preSettlementAdvanceMapper;
|
||||
private final SettlementProjectAdvanceWriteOffMapper projectAdvanceWriteOffMapper;
|
||||
private final ReceivablePayableDetailMapper receivablePayableMapper;
|
||||
private final ReceivablePayableCargoFeeMapper receivablePayableCargoFeeMapper;
|
||||
private final PaymentApplicationMapper paymentApplicationMapper;
|
||||
@@ -207,7 +216,13 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
if (ids.isEmpty()) wrapper.eq(FormalSettlement::getId, -1L); else wrapper.in(FormalSettlement::getId, ids);
|
||||
}
|
||||
IPage<FormalSettlement> result = page(page, wrapper.orderByDesc(FormalSettlement::getCreateTime));
|
||||
IPage<FormalSettlementVO> voPage = result.convert(this::toVO);
|
||||
// 本页应付结算单的项目预付核销合计一次查询,避免逐行 toVO 重算汇总时的 N+1
|
||||
Map<Long, BigDecimal> writeOffAmounts = this.projectAdvanceWriteOffService.sumBySettlementIds(
|
||||
TransportBusiConstant.SETTLEMENT_BILL_FORMAL,
|
||||
result.getRecords().stream()
|
||||
.filter(item -> "payable".equals(item.getSettlementType()))
|
||||
.map(FormalSettlement::getId).toList());
|
||||
IPage<FormalSettlementVO> voPage = result.convert(item -> this.toVO(item, writeOffAmounts));
|
||||
fillAttachmentSyncStatus(voPage.getRecords());
|
||||
fillKingdeeLockStatus(voPage.getRecords());
|
||||
return voPage;
|
||||
@@ -311,6 +326,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
vo.setIssuedInvoices(listIssuedInvoices(id));
|
||||
vo.setProjectAdvanceWriteOffs(projectAdvanceWriteOffService.listBySettlement(
|
||||
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, id));
|
||||
vo.setAdvancePayments(this.listAdvancePayments(sources, vo.getProjectAdvanceWriteOffs()));
|
||||
Set<Long> relationApplicationIds = paymentApplicationSettlementMapper.selectList(
|
||||
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
.select(PaymentApplicationSettlement::getPaymentApplicationId)
|
||||
@@ -333,7 +349,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
vo.setAdjustments(settlementAdjustmentMapper.selectList(Wrappers.<SettlementAdjustment>lambdaQuery()
|
||||
.eq(SettlementAdjustment::getFormalSettlementId, id)
|
||||
.orderByDesc(SettlementAdjustment::getCreateTime)).stream()
|
||||
.map(item -> toAdjustmentVO(item, settlement.getKingdeeBillNo())).toList());
|
||||
.map(this::toAdjustmentVO).toList());
|
||||
vo.setChangeRecords(changeRecordMapper.selectList(Wrappers.<FormalSettlementChangeRecord>lambdaQuery()
|
||||
.eq(FormalSettlementChangeRecord::getFormalSettlementId, id)
|
||||
.orderByDesc(FormalSettlementChangeRecord::getChangeTime)));
|
||||
@@ -564,12 +580,17 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
applySummaryRequest(settlement.getId(), request.getSummaryFees());
|
||||
refreshSettlementAmount(settlement);
|
||||
rebuildInvoices(settlement, request.getInvoices());
|
||||
// 预结算转正式/调整来源:先转移来源预结算的项目预付核销行(释放可核销占用),replaceWriteOffs 的余额校验才能通过
|
||||
this.projectAdvanceWriteOffService.deleteBySettlementIds(
|
||||
TransportBusiConstant.SETTLEMENT_BILL_PRE, request.getSourcePreSettlementIds());
|
||||
projectAdvanceWriteOffService.replaceWriteOffs(
|
||||
TransportBusiConstant.SETTLEMENT_BILL_FORMAL,
|
||||
settlement.getId(),
|
||||
settlement.getContractId(),
|
||||
settlement.getSettlementAmount(),
|
||||
request.getProjectAdvanceWriteOffs());
|
||||
// 核销行计入收付汇总(refreshSettlementAmount 内的刷新发生在核销落库之前,覆盖不到核销行)
|
||||
this.refreshPaymentSummary(settlement.getId());
|
||||
if (!creating && !restoring) {
|
||||
saveChange(settlement.getId(), "结算单基本信息", null, "调整",
|
||||
"保存正式结算单" + settlement.getFormalSettlementNo(), request.getRemark());
|
||||
@@ -624,6 +645,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
settlement.setCurrentNode("已作废");
|
||||
settlement.setVoidReason(required(limit(request.getReason(), 200), "作废原因"));
|
||||
updateById(settlement);
|
||||
// 联动删除项目预付核销行,释放可核销占用并避免残留记录
|
||||
this.projectAdvanceWriteOffService.deleteBySettlementIds(
|
||||
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, List.of(settlement.getId()));
|
||||
saveChange(settlement.getId(), "结算单基本信息", null, "调整", "作废正式结算单", request.getReason());
|
||||
}
|
||||
|
||||
@@ -643,6 +667,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
.set(FormalSettlement::getKingdeeBillNo, null)
|
||||
.set(FormalSettlement::getKingdeeBillId, null)
|
||||
.set(FormalSettlement::getSyncedTime, null));
|
||||
// 联动删除项目预付核销行,释放可核销占用并避免残留记录
|
||||
this.projectAdvanceWriteOffService.deleteBySettlementIds(
|
||||
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, List.of(id));
|
||||
// 变更流水:回调无登录态,操作人取金蝶报文(兜底 system,与 saveChange 同款)
|
||||
FormalSettlementChangeRecord record = new FormalSettlementChangeRecord();
|
||||
record.setFormalSettlementId(id);
|
||||
@@ -655,6 +682,118 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
changeRecordMapper.insert(record);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CloneResult cloneFormalSettlement(Long sourceSettlementId) {
|
||||
FormalSettlement source = existing(sourceSettlementId);
|
||||
FormalSettlement clone = Objects.requireNonNull(BeanUtil.copyProperties(source, FormalSettlement.class));
|
||||
// 新单头:新号、直接审批通过、未同步金蝶、来源原单号;不动原单
|
||||
resetAuditFields(clone);
|
||||
clone.setFormalSettlementNo(nextNo(source.getSettlementType()));
|
||||
clone.setApprovalStatus(TransportBusiConstant.APPROVED);
|
||||
clone.setCurrentNode("审批通过");
|
||||
clone.setCurrentProcessor(AuthUtil.getUserName());
|
||||
clone.setApprovedTime(LocalDateTime.now());
|
||||
clone.setKingdeeBillNo(null);
|
||||
clone.setKingdeeBillId(null);
|
||||
clone.setKingdeeSyncStatus(KingdeeBfiConstant.KINGDEE_STATUS_UNSYNCED);
|
||||
clone.setSyncedTime(null);
|
||||
clone.setVoidReason(null);
|
||||
clone.setOriginFormalSettlementNo(source.getFormalSettlementNo());
|
||||
clone.setAdjustStatus(null);
|
||||
// 下游关联(开票申请/收款认领/对账)不迁移:发票/收付款口径归零,新单为全新独立单
|
||||
clone.setInvoiceStatus("unreceived");
|
||||
clone.setPaymentStatus("unpaid");
|
||||
clone.setInvoiceAmount(BigDecimal.ZERO.setScale(2));
|
||||
clone.setAppliedPaymentAmount(BigDecimal.ZERO.setScale(2));
|
||||
clone.setPaidAmount(BigDecimal.ZERO.setScale(2));
|
||||
clone.setRemainingPayableAmount(money(source.getSettlementAmount()));
|
||||
save(clone);
|
||||
|
||||
// 明细与明细费项克隆,记录 原id->新id 映射(调整明细应用到新单时按映射定位)
|
||||
Map<Long, Long> detailIdMap = new HashMap<>();
|
||||
Map<Long, Long> feeIdMap = new HashMap<>();
|
||||
List<FormalSettlementDetail> details = detailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
|
||||
.eq(FormalSettlementDetail::getFormalSettlementId, sourceSettlementId)
|
||||
.eq(FormalSettlementDetail::getIsDeleted, 0)
|
||||
.orderByAsc(FormalSettlementDetail::getLineNo));
|
||||
for (FormalSettlementDetail detail : details) {
|
||||
FormalSettlementDetail cloneDetail = Objects.requireNonNull(BeanUtil.copyProperties(detail, FormalSettlementDetail.class));
|
||||
resetAuditFields(cloneDetail);
|
||||
cloneDetail.setFormalSettlementId(clone.getId());
|
||||
detailMapper.insert(cloneDetail);
|
||||
detailIdMap.put(detail.getId(), cloneDetail.getId());
|
||||
for (FormalSettlementDetailFee fee : detailFeeMapper.selectList(Wrappers.<FormalSettlementDetailFee>lambdaQuery()
|
||||
.eq(FormalSettlementDetailFee::getFormalSettlementDetailId, detail.getId())
|
||||
.orderByAsc(FormalSettlementDetailFee::getLineNo))) {
|
||||
FormalSettlementDetailFee cloneFee = Objects.requireNonNull(BeanUtil.copyProperties(fee, FormalSettlementDetailFee.class));
|
||||
resetAuditFields(cloneFee);
|
||||
cloneFee.setFormalSettlementDetailId(cloneDetail.getId());
|
||||
detailFeeMapper.insert(cloneFee);
|
||||
feeIdMap.put(fee.getId(), cloneFee.getId());
|
||||
}
|
||||
}
|
||||
// 汇总费用项克隆
|
||||
for (FormalSettlementSummaryFee summaryFee : listSummaryFees(sourceSettlementId)) {
|
||||
FormalSettlementSummaryFee cloneFee = Objects.requireNonNull(BeanUtil.copyProperties(summaryFee, FormalSettlementSummaryFee.class));
|
||||
resetAuditFields(cloneFee);
|
||||
cloneFee.setFormalSettlementId(clone.getId());
|
||||
summaryFeeMapper.insert(cloneFee);
|
||||
}
|
||||
// 来源预结算重建(接续原单来源,不重复锁定 PreSettlement.formalSettlementNo)
|
||||
for (FormalSettlementSource relation : sourceMapper.selectList(Wrappers.<FormalSettlementSource>lambdaQuery()
|
||||
.eq(FormalSettlementSource::getFormalSettlementId, sourceSettlementId))) {
|
||||
FormalSettlementSource cloneRelation = Objects.requireNonNull(BeanUtil.copyProperties(relation, FormalSettlementSource.class));
|
||||
resetAuditFields(cloneRelation);
|
||||
cloneRelation.setFormalSettlementId(clone.getId());
|
||||
sourceMapper.insert(cloneRelation);
|
||||
}
|
||||
// 核销行迁移:克隆到新单并删除原单行,避免预付可核销额度双占(原单为调整存档,收付口径以新单为准)
|
||||
List<SettlementProjectAdvanceWriteOff> writeOffRows = this.projectAdvanceWriteOffMapper.selectList(
|
||||
Wrappers.<SettlementProjectAdvanceWriteOff>lambdaQuery()
|
||||
.eq(SettlementProjectAdvanceWriteOff::getSettlementType, TransportBusiConstant.SETTLEMENT_BILL_FORMAL)
|
||||
.eq(SettlementProjectAdvanceWriteOff::getSettlementId, sourceSettlementId)
|
||||
.eq(SettlementProjectAdvanceWriteOff::getIsDeleted, 0));
|
||||
for (SettlementProjectAdvanceWriteOff writeOffRow : writeOffRows) {
|
||||
SettlementProjectAdvanceWriteOff cloneWriteOff = Objects.requireNonNull(
|
||||
BeanUtil.copyProperties(writeOffRow, SettlementProjectAdvanceWriteOff.class));
|
||||
resetAuditFields(cloneWriteOff);
|
||||
cloneWriteOff.setSettlementId(clone.getId());
|
||||
this.projectAdvanceWriteOffMapper.insert(cloneWriteOff);
|
||||
}
|
||||
this.projectAdvanceWriteOffService.deleteBySettlementIds(
|
||||
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, List.of(sourceSettlementId));
|
||||
this.refreshPaymentSummaries(List.of(sourceSettlementId, clone.getId()));
|
||||
log.info("结算调整重推克隆正式结算单:原单[{}] -> 新单[{}](明细{}条,核销迁移{}条)",
|
||||
source.getFormalSettlementNo(), clone.getFormalSettlementNo(), details.size(), writeOffRows.size());
|
||||
return new CloneResult(clone.getId(), detailIdMap, feeIdMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void markSettlementAdjusted(Long settlementId, String adjustmentNo, String newSettlementNo) {
|
||||
existing(settlementId);
|
||||
// 原单只加标记,业务字段不动
|
||||
update(Wrappers.<FormalSettlement>lambdaUpdate()
|
||||
.eq(FormalSettlement::getId, settlementId)
|
||||
.set(FormalSettlement::getAdjustStatus, "adjusted"));
|
||||
kingdeeBillLockService.unlockByAdjustRepush(KingdeeLockConstant.SOURCE_SETTLEMENT, settlementId, AuthUtil.getUserName());
|
||||
saveChange(settlementId, "结算单基本信息", null, "调整",
|
||||
"结算调整单" + Func.toStr(adjustmentNo) + "重推生成新单" + Func.toStr(newSettlementNo) + ",原单标记已调整", null);
|
||||
}
|
||||
|
||||
/** 重推/克隆复用:清空审计与主键字段,交由 MP 自动填充 */
|
||||
private void resetAuditFields(org.springblade.core.mp.base.BaseEntity entity) {
|
||||
entity.setId(null);
|
||||
entity.setCreateUser(null);
|
||||
entity.setCreateDept(null);
|
||||
entity.setCreateTime(null);
|
||||
entity.setUpdateUser(null);
|
||||
entity.setUpdateTime(null);
|
||||
entity.setStatus(null);
|
||||
entity.setIsDeleted(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String syncKingdee(Long id) {
|
||||
String billNo;
|
||||
@@ -719,6 +858,11 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
.ne(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED)
|
||||
.set(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED));
|
||||
if (updated > 0) {
|
||||
// 结算调整单回填:未推送场景审批通过后自动推送失败,调整单置 failed 供「推送金蝶」重试
|
||||
settlementAdjustmentMapper.update(null, Wrappers.<SettlementAdjustment>lambdaUpdate()
|
||||
.eq(SettlementAdjustment::getFormalSettlementId, id)
|
||||
.eq(SettlementAdjustment::getApprovalStatus, TransportBusiConstant.APPROVED)
|
||||
.set(SettlementAdjustment::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED));
|
||||
// 失败原因写变更记录留档(推送事务已回滚,此处独立事务);原因过长截断留档,不抛错
|
||||
String failReason = Func.toStrWithEmpty(reason, "未知异常");
|
||||
saveChange(id, "结算单基本信息", null, "调整",
|
||||
@@ -824,7 +968,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String doSyncKingdee(Long id) {
|
||||
FormalSettlement settlement = existing(id);
|
||||
FormalSettlement settlement = lockedExisting(id);
|
||||
if (!TransportBusiConstant.APPROVED.equals(settlement.getApprovalStatus())) throw new ServiceException("仅审批通过的正式结算单允许同步金蝶");
|
||||
// 已同步幂等返回(含历史由付款申请同步联动回写的存量已同步数据);星瀚内码由附件补传分支反查回填
|
||||
if ("synced".equals(settlement.getKingdeeSyncStatus())) return settlement.getKingdeeBillNo();
|
||||
@@ -870,6 +1014,12 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
settlement.setSyncedTime(LocalDateTime.now());
|
||||
updateById(settlement);
|
||||
saveChange(settlement.getId(), "结算单基本信息", null, "调整", changeDesc, "");
|
||||
// 结算调整单回填:未推送场景审批通过后自动推送成功,回填挂在本单的已审批调整单(重推场景调整单已改挂新单,不受影响)
|
||||
settlementAdjustmentMapper.update(null, Wrappers.<SettlementAdjustment>lambdaUpdate()
|
||||
.eq(SettlementAdjustment::getFormalSettlementId, settlement.getId())
|
||||
.eq(SettlementAdjustment::getApprovalStatus, TransportBusiConstant.APPROVED)
|
||||
.set(SettlementAdjustment::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED)
|
||||
.set(SettlementAdjustment::getKingdeeBillNo, billNo));
|
||||
log.info("正式结算单[{}]同步金蝶完成:settlementType={},billNo={},billPkId={}",
|
||||
settlement.getFormalSettlementNo(), settlement.getSettlementType(), billNo, billPkId);
|
||||
return billNo;
|
||||
@@ -1584,6 +1734,11 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
Collectors.reducing(BigDecimal.ZERO,
|
||||
item -> money(item.getAllocatedReceiptAmount()), BigDecimal::add)))
|
||||
: Map.of();
|
||||
// 项目预付核销合计(仅应付结算单需要,一次 group-by 避免逐单查询)
|
||||
Map<Long, BigDecimal> writeOffAmountsBySettlement = hasPayable
|
||||
? this.projectAdvanceWriteOffService.sumBySettlementIds(
|
||||
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, settledIds)
|
||||
: Map.of();
|
||||
|
||||
for (FormalSettlement settlement : settlements) {
|
||||
List<PaymentApplicationSettlement> relations = relationsBySettlement
|
||||
@@ -1599,7 +1754,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
PaymentSummary summary = calculatePaymentSummary(settlement, new PaymentSummaryData(
|
||||
relations, directApplications, relationStatuses,
|
||||
sourcesBySettlement.getOrDefault(settlement.getId(), List.of()),
|
||||
receiptClaimPaidBySettlement.getOrDefault(settlement.getId(), BigDecimal.ZERO)));
|
||||
receiptClaimPaidBySettlement.getOrDefault(settlement.getId(), BigDecimal.ZERO),
|
||||
writeOffAmountsBySettlement));
|
||||
settlement.setAppliedPaymentAmount(summary.appliedAmount());
|
||||
settlement.setPaidAmount(summary.paidAmount());
|
||||
settlement.setRemainingPayableAmount(money(settlement.getSettlementAmount())
|
||||
@@ -1919,9 +2075,16 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
}
|
||||
|
||||
private FormalSettlementVO toVO(FormalSettlement entity) {
|
||||
return this.toVO(entity, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param writeOffAmounts 结算单id -> 项目预付核销合计(列表页批量传入避免 N+1;null 表示按需单笔查询)
|
||||
*/
|
||||
private FormalSettlementVO toVO(FormalSettlement entity, Map<Long, BigDecimal> writeOffAmounts) {
|
||||
FormalSettlementVO vo = FormalSettlementWrapper.build().entityVO(entity);
|
||||
vo.setInvoiceStatus(invoiceStatus(entity.getInvoiceAmount(), entity.getSettlementAmount()));
|
||||
PaymentSummary summary = calculatePaymentSummary(entity);
|
||||
PaymentSummary summary = this.calculatePaymentSummary(entity, writeOffAmounts);
|
||||
vo.setAppliedPaymentAmount(summary.appliedAmount());
|
||||
vo.setPaidAmount(summary.paidAmount());
|
||||
vo.setRemainingPayableAmount(money(entity.getSettlementAmount()).subtract(summary.paidAmount())
|
||||
@@ -1931,6 +2094,10 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
}
|
||||
|
||||
private PaymentSummary calculatePaymentSummary(FormalSettlement settlement) {
|
||||
return this.calculatePaymentSummary(settlement, (Map<Long, BigDecimal>) null);
|
||||
}
|
||||
|
||||
private PaymentSummary calculatePaymentSummary(FormalSettlement settlement, Map<Long, BigDecimal> writeOffAmounts) {
|
||||
List<PaymentApplicationSettlement> relations = paymentApplicationSettlementMapper.selectList(
|
||||
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
.eq(PaymentApplicationSettlement::getFormalSettlementId, settlement.getId())
|
||||
@@ -1948,7 +2115,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
.in(PaymentApplication::getId, relationApplicationIds).eq(PaymentApplication::getIsDeleted, 0))
|
||||
.stream().collect(Collectors.toMap(PaymentApplication::getId, PaymentApplication::getApprovalStatus));
|
||||
return calculatePaymentSummary(settlement, new PaymentSummaryData(relations, applications,
|
||||
relationStatuses, null, null));
|
||||
relationStatuses, null, null, writeOffAmounts));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1981,6 +2148,13 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
.map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
paidAmount = paidAmount.add(sources.stream().map(FormalSettlementSource::getAdvancePaidAmount)
|
||||
.map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||
// 项目预付核销计入已付/已申请(核销行底层预付必已付款,核销额即实付抵扣)
|
||||
BigDecimal writeOffAmount = data.writeOffAmounts() != null
|
||||
? data.writeOffAmounts().getOrDefault(settlement.getId(), BigDecimal.ZERO)
|
||||
: this.projectAdvanceWriteOffService.sumBySettlement(
|
||||
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, settlement.getId());
|
||||
appliedAmount = appliedAmount.add(this.money(writeOffAmount));
|
||||
paidAmount = paidAmount.add(this.money(writeOffAmount));
|
||||
}
|
||||
if ("receivable".equals(settlement.getSettlementType())) {
|
||||
paidAmount = data.receiptClaimPaid() != null ? data.receiptClaimPaid()
|
||||
@@ -2016,22 +2190,91 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
* @param relationStatuses 付款申请id → 审批状态
|
||||
* @param sources 预收来源行(仅应付结算单;null 表示由纯计算按需查询)
|
||||
* @param receiptClaimPaid 收款认领分摊聚合(仅应收结算单;null 表示由纯计算按需查询)
|
||||
* @param writeOffAmounts 结算单id → 项目预付核销合计(仅应付结算单;null 表示由纯计算按需查询)
|
||||
*/
|
||||
private record PaymentSummaryData(List<PaymentApplicationSettlement> relations,
|
||||
List<PaymentApplication> directApplications, Map<Long, String> relationStatuses,
|
||||
List<FormalSettlementSource> sources, BigDecimal receiptClaimPaid) {
|
||||
List<FormalSettlementSource> sources, BigDecimal receiptClaimPaid,
|
||||
Map<Long, BigDecimal> writeOffAmounts) {
|
||||
}
|
||||
|
||||
private SettlementAdjustmentVO toAdjustmentVO(SettlementAdjustment entity, String kingdeeBillNo) {
|
||||
private SettlementAdjustmentVO toAdjustmentVO(SettlementAdjustment entity) {
|
||||
SettlementAdjustmentVO vo = Objects.requireNonNull(
|
||||
BeanUtil.copyProperties(entity, SettlementAdjustmentVO.class));
|
||||
vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser()));
|
||||
vo.setApprovalStatusName(approvalStatusName(entity.getApprovalStatus()));
|
||||
vo.setSettlementTypeName("receivable".equals(entity.getSettlementType()) ? "应收" : "应付");
|
||||
vo.setKingdeeBillNo(kingdeeBillNo);
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一预付付款记录:进度预付(来源预结算台账)+ 项目预付核销(回填底层付款申请实付)。
|
||||
* createTime 统一取底层付款申请创建时间(项目预付核销行已回填 paymentCreateTime,进度预付按单号反查)。
|
||||
*/
|
||||
private List<SettlementAdvancePaymentVO> listAdvancePayments(List<FormalSettlementSource> sources,
|
||||
List<SettlementProjectAdvanceWriteOff> writeOffs) {
|
||||
List<SettlementAdvancePaymentVO> records = new ArrayList<>();
|
||||
List<Long> preSettlementIds = sources.stream().map(FormalSettlementSource::getPreSettlementId)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
if (!preSettlementIds.isEmpty()) {
|
||||
List<PreSettlementAdvance> advances = this.preSettlementAdvanceMapper.selectList(
|
||||
Wrappers.<PreSettlementAdvance>lambdaQuery()
|
||||
.eq(PreSettlementAdvance::getIsDeleted, 0)
|
||||
.ne(PreSettlementAdvance::getBillStatus, TransportBusiConstant.VOIDED)
|
||||
.in(PreSettlementAdvance::getPreSettlementId, preSettlementIds));
|
||||
// 进度预付行的展示时间取底层付款申请创建时间(advanceNo 即付款申请单号)
|
||||
List<String> advanceNos = advances.stream().map(PreSettlementAdvance::getAdvanceNo)
|
||||
.filter(Func::isNotEmpty).distinct().toList();
|
||||
Map<String, Date> applicationTimes = advanceNos.isEmpty() ? Map.of()
|
||||
: this.paymentApplicationMapper.selectList(Wrappers.<PaymentApplication>lambdaQuery()
|
||||
.select(PaymentApplication::getPaymentNo, PaymentApplication::getCreateTime)
|
||||
.eq(PaymentApplication::getIsDeleted, 0)
|
||||
.in(PaymentApplication::getPaymentNo, advanceNos))
|
||||
.stream().filter(item -> item.getCreateTime() != null)
|
||||
.collect(Collectors.toMap(PaymentApplication::getPaymentNo,
|
||||
PaymentApplication::getCreateTime, (first, second) -> first));
|
||||
for (PreSettlementAdvance advance : advances) {
|
||||
SettlementAdvancePaymentVO record = new SettlementAdvancePaymentVO();
|
||||
record.setRecordType(TransportBusiConstant.PAYMENT_TYPE_PROGRESS_ADVANCE);
|
||||
record.setPaymentNo(advance.getAdvanceNo());
|
||||
record.setAppliedAmount(advance.getAppliedAmount());
|
||||
record.setPaidAmount(advance.getPaidAmount());
|
||||
record.setBillStatus(advance.getBillStatus());
|
||||
record.setKingdeeBillNo(advance.getKingdeeAdvanceNo());
|
||||
record.setCreateUserName(UserCache.getUserRealName(advance.getCreateUser()));
|
||||
record.setCreateTime(applicationTimes.getOrDefault(advance.getAdvanceNo(), advance.getCreateTime()));
|
||||
records.add(record);
|
||||
}
|
||||
}
|
||||
if (Func.isNotEmpty(writeOffs)) {
|
||||
List<Long> applicationIds = writeOffs.stream()
|
||||
.map(SettlementProjectAdvanceWriteOff::getPaymentApplicationId)
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
Map<Long, PaymentApplication> applications = applicationIds.isEmpty() ? Map.of()
|
||||
: this.paymentApplicationMapper.selectBatchIds(applicationIds).stream()
|
||||
.collect(Collectors.toMap(PaymentApplication::getId, Function.identity(),
|
||||
(first, second) -> first));
|
||||
for (SettlementProjectAdvanceWriteOff writeOff : writeOffs) {
|
||||
PaymentApplication application = applications.get(writeOff.getPaymentApplicationId());
|
||||
SettlementAdvancePaymentVO record = new SettlementAdvancePaymentVO();
|
||||
record.setRecordType(TransportBusiConstant.PAYMENT_TYPE_PROJECT_ADVANCE);
|
||||
record.setPaymentNo(writeOff.getPaymentNo());
|
||||
record.setAppliedAmount(writeOff.getWriteOffAmount());
|
||||
record.setPaidAmount(application == null ? null : application.getPaidAmount());
|
||||
record.setBillStatus(application == null ? null : application.getApprovalStatus());
|
||||
record.setKingdeeBillNo(writeOff.getKingdeeBillNo());
|
||||
record.setCreateUserName(UserCache.getUserRealName(application == null
|
||||
? writeOff.getCreateUser() : application.getCreateUser()));
|
||||
record.setCreateTime(writeOff.getPaymentCreateTime() != null
|
||||
? writeOff.getPaymentCreateTime() : writeOff.getCreateTime());
|
||||
records.add(record);
|
||||
}
|
||||
}
|
||||
records.sort(Comparator.comparing(SettlementAdvancePaymentVO::getCreateTime,
|
||||
Comparator.nullsLast(Comparator.reverseOrder())));
|
||||
return records;
|
||||
}
|
||||
|
||||
private String approvalStatusName(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
@@ -2069,6 +2312,14 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
|
||||
return entity;
|
||||
}
|
||||
|
||||
/** 行锁读:同一单据并发推送串行化(锁随 doSyncKingdee 事务提交释放,后者读到已同步状态幂等返回) */
|
||||
private FormalSettlement lockedExisting(Long id) {
|
||||
FormalSettlement entity = baseMapper.selectOne(Wrappers.<FormalSettlement>lambdaQuery()
|
||||
.eq(FormalSettlement::getId, id).last("FOR UPDATE"));
|
||||
if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) throw new ServiceException("正式结算单不存在");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private FormalSettlement editable(Long id) {
|
||||
FormalSettlement entity = existing(id);
|
||||
if (!TransportBusiConstant.DRAFT.equals(entity.getApprovalStatus()) && !TransportBusiConstant.RETURNED.equals(entity.getApprovalStatus())) throw new ServiceException("仅草稿或已驳回的正式结算单允许编辑");
|
||||
|
||||
+17
-13
@@ -38,8 +38,6 @@ import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.SysCache;
|
||||
import org.springblade.system.pojo.entity.Dept;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceApplyOpenApiQueryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceApplyQueryDTO;
|
||||
import org.springblade.thirdparty.bfi.pojo.dto.BfiInvoiceApplySaveDTO;
|
||||
@@ -701,7 +699,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String doSyncKingdee(Long id) {
|
||||
InvoiceApplication entity = existing(id);
|
||||
InvoiceApplication entity = lockedExisting(id);
|
||||
if (!TransportBusiConstant.APPROVED.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批通过的开票申请允许同步金蝶");
|
||||
// 已同步/已开票幂等返回(开票完成为终态);星瀚内码由附件补传分支经 A303-4 反查回填
|
||||
if (KingdeeBfiConstant.KINGDEE_STATUS_SYNCED.equals(entity.getKingdeeStatus())
|
||||
@@ -803,12 +801,10 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
}
|
||||
if (Func.isBlank(entity.getDepartmentEmails())) throw new ServiceException("推送金蝶开票申请失败:部门邮箱(发票推送邮箱)不能为空");
|
||||
if (Func.isBlank(entity.getTaxpayerNo())) throw new ServiceException("推送金蝶开票申请失败:购方纳税人识别号不能为空");
|
||||
Dept dept = entity.getDeptId() == null ? null : SysCache.getDept(entity.getDeptId());
|
||||
if (dept == null || Func.isBlank(dept.getKingdeeDeptCode())) {
|
||||
throw new ServiceException("推送金蝶开票申请失败:申请部门[" + Func.toStr(entity.getDeptId()) + "]未维护金蝶组织编码,请检查部门数据");
|
||||
}
|
||||
// 金蝶组织编码:项目业务部门反查唯一平台公司 → 平台公司金蝶组织编码
|
||||
String kingdeeOrgNumber = kingdeeApBillSyncSupport.kingdeeOrgNumber(entity.getProjectId(), KingdeeSyncContext.empty());
|
||||
// 金蝶组织编码:项目业务部门反查唯一平台公司 → 平台公司金蝶组织编码;申请部门编码:项目业务部门自身金蝶编码(与付款申请单部门.编码同源,共用上下文缓存)
|
||||
KingdeeSyncContext kingdeeContext = KingdeeSyncContext.empty();
|
||||
String kingdeeOrgNumber = kingdeeApBillSyncSupport.kingdeeOrgNumber(entity.getProjectId(), kingdeeContext);
|
||||
String kingdeeDeptNumber = kingdeeApBillSyncSupport.kingdeeDeptNumber(entity.getProjectId(), kingdeeContext);
|
||||
CustomerArchive customer = entity.getReceiverCustomerId() == null ? null : customerArchiveMapper.selectById(entity.getReceiverCustomerId());
|
||||
if (customer == null || Func.isBlank(customer.getCustomerCode())) {
|
||||
throw new ServiceException("推送金蝶开票申请失败:受票方客户未在客商档案中维护客商编码,请补充后重试");
|
||||
@@ -839,7 +835,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
+ "]无商品行,无法组装分录");
|
||||
}
|
||||
BfiInvoiceApplySaveDTO param = this.buildInvoiceApplyParam(entity, sheetBillNo, seller, salerAddr, salerBank,
|
||||
dept, kingdeeOrgNumber, customer, firstArSrcCode, money(sheet.getInvoiceAmount()), items);
|
||||
kingdeeDeptNumber, kingdeeOrgNumber, customer, firstArSrcCode, money(sheet.getInvoiceAmount()), items);
|
||||
log.info("金蝶开票申请推送:开票申请[{}]发票张[{}],billno=[{}],价税合计=[{}],分录{}条",
|
||||
entity.getApplicationNo(), sheet.getSheetNo(), sheetBillNo, sheet.getInvoiceAmount(), items.size());
|
||||
BfiBillSaveResultVO saveResult = bfiService.saveInvoiceApply(param);
|
||||
@@ -892,14 +888,14 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
* @param firstArSrcCode 上游来源单据号:首张关联结算单的金蝶应收单防重键 awrp_src_code(多张结算单取第一张)
|
||||
*/
|
||||
private BfiInvoiceApplySaveDTO buildInvoiceApplyParam(InvoiceApplication entity, String billNo,
|
||||
CustomerArchive seller, String salerAddr, String salerBank, Dept dept, String orgNumber, CustomerArchive customer,
|
||||
CustomerArchive seller, String salerAddr, String salerBank, String deptNumber, String orgNumber, CustomerArchive customer,
|
||||
String firstArSrcCode, BigDecimal sheetAmount, List<BfiInvoiceApplySaveEntryDTO> items) {
|
||||
BfiInvoiceApplySaveDTO param = new BfiInvoiceApplySaveDTO();
|
||||
// ==================== 以下为【必填】字段(BfiInvoiceApplySaveDTO 标注),非必填字段先注释保留 ====================
|
||||
// 【必填】组织.编码:项目业务部门反查唯一平台公司 → 平台公司金蝶组织编码
|
||||
param.setOrgidNumber(orgNumber);
|
||||
// 【必填】申请部门.编码:申请部门 deptId → 金蝶部门编码
|
||||
param.setAwrpOrgfieldNumber(dept.getKingdeeDeptCode());
|
||||
// 【必填】申请部门.编码:项目业务部门金蝶编码(与付款申请单 awrp_department_number 同源)
|
||||
param.setAwrpOrgfieldNumber(deptNumber);
|
||||
// 【必填】业务单据编号(候选键):单张发票候选 billno(多张发票时开票申请单号-张次序号)
|
||||
param.setBillno(billNo);
|
||||
// 【必填】单据日期:申请日期 yyyy-MM-dd HH:mm:ss
|
||||
@@ -1669,6 +1665,14 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
|
||||
return entity;
|
||||
}
|
||||
|
||||
/** 行锁读:同一单据并发推送串行化(锁随 doSyncKingdee 事务提交释放,后者读到已同步状态幂等返回) */
|
||||
private InvoiceApplication lockedExisting(Long id) {
|
||||
InvoiceApplication entity = baseMapper.selectOne(Wrappers.<InvoiceApplication>lambdaQuery()
|
||||
.eq(InvoiceApplication::getId, id).last("FOR UPDATE"));
|
||||
if (entity == null || Objects.equals(entity.getIsDeleted(), 1)) throw new ServiceException("开票申请不存在");
|
||||
return entity;
|
||||
}
|
||||
|
||||
private InvoiceApplication editable(Long id) {
|
||||
InvoiceApplication entity = existing(id);
|
||||
if (!List.of(TransportBusiConstant.DRAFT, TransportBusiConstant.RETURNED).contains(entity.getApprovalStatus())) throw new ServiceException("当前状态不可编辑");
|
||||
|
||||
+42
-2
@@ -90,6 +90,7 @@ import org.springblade.transport.wrapper.InvoiceReceiptWrapper;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -151,6 +152,8 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
private final CustomerInvoiceInfoMapper customerInvoiceInfoMapper;
|
||||
private final IBfiService bfiService;
|
||||
private final BfiProperties bfiProperties;
|
||||
/** 逐单推送事务模板:批量编排逐单开短事务,行锁跨远程调用,单笔失败只回滚自身不中断整批 */
|
||||
private final TransactionTemplate transactionTemplate;
|
||||
private final KingdeeApBillSyncSupport kingdeeApBillSyncSupport;
|
||||
private final KingdeeReceivedInvoiceSyncSupport kingdeeReceivedInvoiceSyncSupport;
|
||||
private final KingdeeAttachmentSyncSupport kingdeeAttachmentSyncSupport;
|
||||
@@ -608,12 +611,16 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
List<String> failures = new ArrayList<>();
|
||||
for (InvoiceReceipt receipt : receipts) {
|
||||
try {
|
||||
pushOneReceivedInvoice(planByReceipt.get(receipt.getId()), kingdeeRowByInvoiceNo, apBillNoBySrcCode, context, autoSync);
|
||||
// 逐单单据级事务:推送方法首行 FOR UPDATE 串行化同一登记的并发推送,单笔失败只回滚自身不中断整批
|
||||
transactionTemplate.executeWithoutResult(status -> pushOneReceivedInvoice(
|
||||
planByReceipt.get(receipt.getId()), kingdeeRowByInvoiceNo, apBillNoBySrcCode, context, autoSync));
|
||||
success++;
|
||||
} catch (RuntimeException pushException) {
|
||||
failures.add("发票" + Func.toStrWithEmpty(receipt.getInvoiceNo(), String.valueOf(receipt.getId()))
|
||||
+ ":" + pushException.getMessage());
|
||||
log.warn("推送金蝶收票单失败,收票登记id={}", receipt.getId(), pushException);
|
||||
// 单笔事务已回滚,事务外独立补落"推送失败"状态,纳入定时重推(与付款/开票失败口径统一)
|
||||
markReceivedInvoicePushFailed(receipt, planByReceipt.get(receipt.getId()), pushException, autoSync);
|
||||
}
|
||||
}
|
||||
String summary = "推送金蝶收票单完成:本批 " + receipts.size() + " 笔,成功 " + success + " 笔";
|
||||
@@ -674,7 +681,21 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
*/
|
||||
private String pushOneReceivedInvoice(ReceivedInvoicePushPlan plan, Map<String, BfiReceivedInvoiceVO> kingdeeRows,
|
||||
Map<String, String> apBillNoBySrcCode, KingdeeSyncContext context, boolean autoSync) {
|
||||
InvoiceReceipt receipt = plan.receipt();
|
||||
// 行锁读:同一登记并发推送串行化(锁随本单事务提交释放,事务由批量编排的 TransactionTemplate 开启)
|
||||
InvoiceReceipt receipt = baseMapper.selectOne(Wrappers.<InvoiceReceipt>lambdaQuery()
|
||||
.eq(InvoiceReceipt::getId, plan.receipt().getId()).last("FOR UPDATE"));
|
||||
if (receipt == null || Objects.equals(receipt.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("收票登记不存在");
|
||||
}
|
||||
// 锁后重查推送状态:并发场景另一事务已完成推送则幂等跳过,不再重复调金蝶
|
||||
if (KingdeeBfiConstant.RECEIVED_PUSH_PUSHED.equals(receipt.getKingdeePushStatus())
|
||||
|| KingdeeBfiConstant.RECEIVED_PUSH_ASSIGNED.equals(receipt.getKingdeePushStatus())) {
|
||||
log.info("收票登记[{}]金蝶收票单已推送(状态[{}]),幂等跳过", receipt.getInvoiceNo(), receipt.getKingdeePushStatus());
|
||||
return "发票" + Func.toStrWithEmpty(receipt.getInvoiceNo(), String.valueOf(receipt.getId())) + "已推送金蝶,幂等跳过";
|
||||
}
|
||||
// 计划内登记实体以行锁后最新数据替换(并发下推送状态可能已被其他事务改写)
|
||||
plan = new ReceivedInvoicePushPlan(receipt, plan.mirror(), plan.settlement(), plan.details(),
|
||||
plan.apBillSrcCode(), plan.blockedReason());
|
||||
log.info("收票登记[{}]推送金蝶收票单开始:id={},推送状态={},应付单防重键={}", receipt.getInvoiceNo(), receipt.getId(),
|
||||
receipt.getKingdeePushStatus(), plan.apBillSrcCode());
|
||||
if (Func.isNotBlank(plan.blockedReason())) {
|
||||
@@ -765,6 +786,25 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
|
||||
return "发票" + receipt.getInvoiceNo() + "推送成功,金蝶收票单[" + billNo + "]已指定应付单";
|
||||
}
|
||||
|
||||
/**
|
||||
* 单笔推送失败后事务外独立补落"推送失败"状态:事务版推送中的失败状态写随本单事务回滚,
|
||||
* 由批量编排的 catch 在事务外重写,纳入定时重推循环;手动前置拦截路径保持不动状态(仅逐单报因)
|
||||
*
|
||||
* @param receipt 收票登记(批量预取实体)
|
||||
* @param plan 推送计划
|
||||
* @param pushException 推送异常
|
||||
* @param autoSync 自动推送路径标记
|
||||
*/
|
||||
private void markReceivedInvoicePushFailed(InvoiceReceipt receipt, ReceivedInvoicePushPlan plan,
|
||||
RuntimeException pushException, boolean autoSync) {
|
||||
if (plan.blockedReason() != null && !autoSync) {
|
||||
return;
|
||||
}
|
||||
applyReceivedInvoicePushResult(receipt, receipt.getKingdeeReceivedInvoiceNo(), receipt.getKingdeeReceivedInvoiceId(),
|
||||
KingdeeBfiConstant.RECEIVED_PUSH_FAILED,
|
||||
truncatePushMsg("推送金蝶收票单失败:" + pushException.getMessage()), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 附件联带上传(A902):A303-2 保存成功(含幂等预检命中)后调用,上传登记附件与镜像发票文件;
|
||||
* billPkId 缺失跳过待补传;异常独立捕获,绝不影响推送状态与整批进度
|
||||
|
||||
+27
-2
@@ -251,6 +251,28 @@ public class KingdeeBillLockServiceImpl extends ServiceImpl<KingdeeBillLockMappe
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int unlockByAdjustRepush(String sourceBillType, Long sourceBillId, String operator) {
|
||||
List<KingdeeBillLock> lockedRecords = this.list(Wrappers.<KingdeeBillLock>lambdaQuery()
|
||||
.eq(KingdeeBillLock::getSourceBillType, sourceBillType)
|
||||
.eq(KingdeeBillLock::getSourceBillId, sourceBillId)
|
||||
.eq(KingdeeBillLock::getLockStatus, KingdeeLockConstant.STATUS_LOCKED));
|
||||
if (Func.isEmpty(lockedRecords)) {
|
||||
return 0;
|
||||
}
|
||||
LocalDateTime unlockTime = LocalDateTime.now();
|
||||
for (KingdeeBillLock lockRecord : lockedRecords) {
|
||||
lockRecord.setLockStatus(KingdeeLockConstant.STATUS_UNLOCKED);
|
||||
lockRecord.setOperationType(KingdeeLockConstant.OPERATION_ADJUST_REPUSH);
|
||||
lockRecord.setUnlockTime(unlockTime);
|
||||
lockRecord.setNotifyOperator(Func.isEmpty(operator) ? "system" : operator);
|
||||
}
|
||||
this.updateBatchById(lockedRecords);
|
||||
log.info("结算调整重推解锁旧锁:源单[{}:{}],解锁{}张,操作人={}", sourceBillType, sourceBillId, lockedRecords.size(), operator);
|
||||
return lockedRecords.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkEditable(String sourceBillType, Long sourceBillId) {
|
||||
long lockedCount = this.count(Wrappers.<KingdeeBillLock>lambdaQuery()
|
||||
@@ -309,8 +331,11 @@ public class KingdeeBillLockServiceImpl extends ServiceImpl<KingdeeBillLockMappe
|
||||
}
|
||||
row.setKingdeeUnlockTime(unlockRecord.getUnlockTime());
|
||||
row.setKingdeeUnlockOperator(unlockRecord.getNotifyOperator());
|
||||
row.setKingdeeUnlockOperationName(KingdeeLockConstant.OPERATION_DELETE.equals(unlockRecord.getOperationType())
|
||||
? "金蝶删单" : "金蝶锁单");
|
||||
row.setKingdeeUnlockOperationName(switch (Func.toStr(unlockRecord.getOperationType())) {
|
||||
case KingdeeLockConstant.OPERATION_DELETE -> "金蝶删单";
|
||||
case KingdeeLockConstant.OPERATION_ADJUST_REPUSH -> "结算调整重推";
|
||||
default -> "金蝶锁单";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+65
-16
@@ -494,6 +494,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
updateById(entity);
|
||||
// 联动作废提前建档的预结算预付记录,避免残留记录阻断预结算作废
|
||||
voidPreSettlementAdvance(entity);
|
||||
// 联动删除项目预付核销行,避免结算单已付汇总虚高
|
||||
List<Long> writeOffSettlementIds = this.projectAdvanceWriteOffService.deleteByPaymentApplicationIds(List.of(entity.getId()));
|
||||
if (!writeOffSettlementIds.isEmpty()) {
|
||||
formalSettlementService.refreshPaymentSummaries(writeOffSettlementIds);
|
||||
}
|
||||
refreshFormalSettlement(entity);
|
||||
}
|
||||
|
||||
@@ -589,6 +594,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String doSyncKingdee(Long id, KingdeeSyncContext context) {
|
||||
PaymentApplication entity = lockedPayment(id);
|
||||
// 单据号入上下文:批量同步失败提示用单号定位单据(雪花id用户无法对应),不再额外查库
|
||||
context.paymentNoCache().put(id, entity.getPaymentNo());
|
||||
if (!TransportBusiConstant.APPROVED.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批通过的付款申请允许生成金蝶单据");
|
||||
if (KingdeeBfiConstant.KINGDEE_STATUS_SYNCED.equals(entity.getKingdeeStatus())
|
||||
|| KingdeeBfiConstant.KINGDEE_STATUS_PAYING.equals(entity.getKingdeeStatus())
|
||||
@@ -791,7 +798,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
// 经自代理逐条调用:单据级事务,单条失败仅回滚自身且不中断整批
|
||||
result.add(self.syncKingdee(id, context));
|
||||
} catch (Exception exception) {
|
||||
result.add("付款申请[" + id + "]同步失败:" + exception.getMessage());
|
||||
// 失败提示用付款申请单号定位单据:单号已在推送时随上下文缓存,无需回查
|
||||
String paymentNo = context.paymentNoCache().get(id);
|
||||
String billNo = Func.isBlank(paymentNo) ? String.valueOf(id) : paymentNo;
|
||||
result.add("付款申请[" + billNo + "]同步失败:" + exception.getMessage());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -1218,8 +1228,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
param.setAwrpSrcSys(bfiProperties.getSrcSys());
|
||||
// 结算组织.编码
|
||||
param.setSettleorgNumber(orgNumber);
|
||||
// 部门.编码
|
||||
param.setAwrpDepartmentNumber(orgNumber);
|
||||
// 部门.编码:项目业务部门自身的金蝶编码(结算/申请/付款组织仍传上溯平台公司编码)
|
||||
param.setAwrpDepartmentNumber(kingdeeApBillSyncSupport.kingdeeDeptNumber(contract.getProjectId(), context));
|
||||
// 【必填】单据类型.编码
|
||||
param.setBilltypeNumber(bfiProperties.getPayApply().getBilltype());
|
||||
// 【必填】申请日期:yyyy-MM-dd HH:mm:ss(各接口统一格式)
|
||||
@@ -1636,42 +1646,73 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
private void syncPreSettlementAdvance(PaymentApplication entity, String billStatus, BigDecimal paidAmount) {
|
||||
if (entity.getPreSettlementId() == null) return;
|
||||
if (!List.of("progress_advance", "settlement_payment").contains(entity.getPaymentType())) return;
|
||||
// 多选预结算单时单据上仅存第一张预结算,全部选中单据在关联表:按关联行逐单回填预付信息,
|
||||
// 已付金额按关联行申请金额分摊(与 syncFormalSettlementPayments 同口径);单选无关联行,回退单据上的预结算与申请全额
|
||||
List<PaymentApplicationSettlement> relations = settlementRelationMapper.selectList(
|
||||
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
.eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())
|
||||
.isNotNull(PaymentApplicationSettlement::getPreSettlementId)
|
||||
.eq(PaymentApplicationSettlement::getIsDeleted, 0));
|
||||
if (relations.isEmpty()) {
|
||||
PaymentApplicationSettlement single = new PaymentApplicationSettlement();
|
||||
single.setPreSettlementId(entity.getPreSettlementId());
|
||||
single.setAppliedAmount(entity.getAppliedAmount());
|
||||
relations = List.of(single);
|
||||
}
|
||||
BigDecimal remainingPaid = money(paidAmount);
|
||||
for (int index = 0; index < relations.size(); index++) {
|
||||
PaymentApplicationSettlement relation = relations.get(index);
|
||||
BigDecimal paid = index == relations.size() - 1 ? remainingPaid
|
||||
: money(relation.getAppliedAmount()).min(remainingPaid);
|
||||
remainingPaid = remainingPaid.subtract(paid);
|
||||
relation.setPaidAmount(paid);
|
||||
syncOnePreSettlementAdvance(entity, relation, billStatus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按预结算单逐单建档/更新预付记录:金蝶付款申请单生成成功(synced)即回显预付信息,付款完成回写(paid)更新为已付款
|
||||
*
|
||||
* @param relation 预结算关联行,携带预结算单id、申请金额与本次分摊的已付金额
|
||||
*/
|
||||
private void syncOnePreSettlementAdvance(PaymentApplication entity, PaymentApplicationSettlement relation, String billStatus) {
|
||||
Long preSettlementId = relation.getPreSettlementId();
|
||||
PreSettlementAdvance advance = preSettlementAdvanceMapper.selectList(
|
||||
Wrappers.<PreSettlementAdvance>lambdaQuery()
|
||||
.eq(PreSettlementAdvance::getPreSettlementId, entity.getPreSettlementId())
|
||||
.eq(PreSettlementAdvance::getPreSettlementId, preSettlementId)
|
||||
.eq(PreSettlementAdvance::getIsDeleted, 0)
|
||||
.ne(PreSettlementAdvance::getBillStatus, TransportBusiConstant.VOIDED)
|
||||
.eq(PreSettlementAdvance::getAdvanceNo, entity.getPaymentNo())
|
||||
.last("limit 1")).stream().findFirst().orElse(null);
|
||||
if (advance == null) {
|
||||
advance = preSettlementAdvanceMapper.selectList(Wrappers.<PreSettlementAdvance>lambdaQuery()
|
||||
.eq(PreSettlementAdvance::getPreSettlementId, entity.getPreSettlementId())
|
||||
.eq(PreSettlementAdvance::getPreSettlementId, preSettlementId)
|
||||
.eq(PreSettlementAdvance::getIsDeleted, 0)
|
||||
.ne(PreSettlementAdvance::getBillStatus, TransportBusiConstant.VOIDED)
|
||||
.eq(PreSettlementAdvance::getPaidAmount, BigDecimal.ZERO)
|
||||
.eq(PreSettlementAdvance::getAppliedAmount, money(entity.getAppliedAmount()))
|
||||
.eq(PreSettlementAdvance::getAppliedAmount, money(relation.getAppliedAmount()))
|
||||
.orderByDesc(PreSettlementAdvance::getCreateTime).last("limit 1")).stream().findFirst().orElse(null);
|
||||
}
|
||||
if (advance == null) {
|
||||
advance = new PreSettlementAdvance();
|
||||
advance.setPreSettlementId(entity.getPreSettlementId());
|
||||
advance.setPreSettlementId(preSettlementId);
|
||||
advance.setAdvanceNo(entity.getPaymentNo());
|
||||
advance.setAppliedAmount(money(entity.getAppliedAmount()));
|
||||
advance.setAppliedAmount(money(relation.getAppliedAmount()));
|
||||
}
|
||||
advance.setPaidAmount(paidAmount);
|
||||
advance.setPaidAmount(money(relation.getPaidAmount()));
|
||||
advance.setBillStatus(billStatus);
|
||||
advance.setKingdeeAdvanceNo(entity.getKingdeeBillNo());
|
||||
if (advance.getId() == null) preSettlementAdvanceMapper.insert(advance);
|
||||
else preSettlementAdvanceMapper.updateById(advance);
|
||||
refreshPreSettlementAdvanceSummary(entity.getPreSettlementId());
|
||||
refreshPreSettlementAdvanceSummary(preSettlementId);
|
||||
formalSettlementSourceMapper.selectList(Wrappers.<FormalSettlementSource>lambdaQuery()
|
||||
.eq(FormalSettlementSource::getPreSettlementId, entity.getPreSettlementId())
|
||||
.eq(FormalSettlementSource::getPreSettlementId, preSettlementId)
|
||||
.eq(FormalSettlementSource::getIsDeleted, 0)).forEach(source -> {
|
||||
source.setAdvanceAppliedAmount(findPreSettlementApplied(entity.getPreSettlementId()));
|
||||
source.setAdvancePaidAmount(findPreSettlementPaid(entity.getPreSettlementId()));
|
||||
source.setAdvanceAppliedAmount(findPreSettlementApplied(preSettlementId));
|
||||
source.setAdvancePaidAmount(findPreSettlementPaid(preSettlementId));
|
||||
formalSettlementSourceMapper.updateById(source);
|
||||
});
|
||||
formalSettlementService.refreshPaymentSummariesForPreSettlement(entity.getPreSettlementId());
|
||||
formalSettlementService.refreshPaymentSummariesForPreSettlement(preSettlementId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1681,13 +1722,21 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
private void voidPreSettlementAdvance(PaymentApplication entity) {
|
||||
if (entity.getPreSettlementId() == null) return;
|
||||
if (!List.of("progress_advance", "settlement_payment").contains(entity.getPaymentType())) return;
|
||||
// 多选预结算单的预付记录按关联行逐单作废;单选回退单据上的预结算
|
||||
List<Long> preSettlementIds = new ArrayList<>(List.of(entity.getPreSettlementId()));
|
||||
settlementRelationMapper.selectList(Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
.eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())
|
||||
.isNotNull(PaymentApplicationSettlement::getPreSettlementId)
|
||||
.eq(PaymentApplicationSettlement::getIsDeleted, 0))
|
||||
.stream().map(PaymentApplicationSettlement::getPreSettlementId).filter(Objects::nonNull).distinct()
|
||||
.forEach(preSettlementIds::add);
|
||||
preSettlementAdvanceMapper.update(null, Wrappers.<PreSettlementAdvance>lambdaUpdate()
|
||||
.eq(PreSettlementAdvance::getPreSettlementId, entity.getPreSettlementId())
|
||||
.in(PreSettlementAdvance::getPreSettlementId, preSettlementIds)
|
||||
.eq(PreSettlementAdvance::getAdvanceNo, entity.getPaymentNo())
|
||||
.eq(PreSettlementAdvance::getIsDeleted, 0)
|
||||
.ne(PreSettlementAdvance::getBillStatus, "paid")
|
||||
.set(PreSettlementAdvance::getBillStatus, TransportBusiConstant.VOIDED));
|
||||
refreshPreSettlementAdvanceSummary(entity.getPreSettlementId());
|
||||
preSettlementIds.forEach(this::refreshPreSettlementAdvanceSummary);
|
||||
}
|
||||
|
||||
private BigDecimal findPreSettlementPaid(Long preSettlementId) {
|
||||
|
||||
+3
@@ -463,6 +463,9 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
settlement.setCurrentProcessor(AuthUtil.getUserName());
|
||||
settlement.setVoidReason(reason);
|
||||
updateById(settlement);
|
||||
// 联动删除项目预付核销行,避免残留记录与可核销占用
|
||||
this.projectAdvanceWriteOffService.deleteBySettlementIds(
|
||||
TransportBusiConstant.SETTLEMENT_BILL_PRE, List.of(settlement.getId()));
|
||||
releaseSourceDetails(settlement, listDetails(settlement.getId()));
|
||||
saveChange(settlement.getId(), "结算单基本信息", null, "调整", "作废预结算单", reason);
|
||||
}
|
||||
|
||||
+182
-16
@@ -4,12 +4,18 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.core.tool.utils.SpringUtil;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.constant.KingdeeBfiConstant;
|
||||
import org.springblade.transport.constant.TransportBusiConstant;
|
||||
import org.springblade.transport.event.KingdeeBillApprovedEvent;
|
||||
import org.springblade.transport.support.KingdeeApBillSyncSupport;
|
||||
import org.springblade.transport.support.KingdeeArBillSyncSupport;
|
||||
import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper;
|
||||
import org.springblade.transport.mapper.FormalSettlementDetailMapper;
|
||||
import org.springblade.transport.mapper.FormalSettlementChangeRecordMapper;
|
||||
@@ -33,7 +39,10 @@ import org.springblade.transport.pojo.entity.PaymentApplicationSettlement;
|
||||
import org.springblade.transport.pojo.entity.SettlementAdjustment;
|
||||
import org.springblade.transport.pojo.entity.SettlementAdjustmentDetail;
|
||||
import org.springblade.transport.pojo.vo.SettlementAdjustmentVO;
|
||||
import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.ISettlementAdjustmentService;
|
||||
import org.springblade.transport.service.ISettlementProjectAdvanceWriteOffService;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -51,6 +60,7 @@ import java.util.Set;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementAdjustmentMapper, SettlementAdjustment>
|
||||
implements ISettlementAdjustmentService {
|
||||
private static final String PAID = "paid";
|
||||
@@ -63,6 +73,11 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
private final FormalSettlementPaymentMapper formalPaymentMapper;
|
||||
private final PaymentApplicationMapper paymentApplicationMapper;
|
||||
private final PaymentApplicationSettlementMapper paymentApplicationSettlementMapper;
|
||||
private final IFormalSettlementService formalSettlementService;
|
||||
private final ISettlementProjectAdvanceWriteOffService projectAdvanceWriteOffService;
|
||||
private final KingdeeApBillSyncSupport kingdeeApBillSyncSupport;
|
||||
private final KingdeeArBillSyncSupport kingdeeArBillSyncSupport;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@Override
|
||||
public IPage<SettlementAdjustmentVO> selectPage(IPage<SettlementAdjustment> page, SettlementAdjustmentVO query) {
|
||||
@@ -204,24 +219,142 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
}
|
||||
@Override public void returnBill(SettlementAdjustmentStatusRequest request) { changeStatus(request.getId(), TransportBusiConstant.REVIEWING, TransportBusiConstant.RETURNED, "已驳回", limit(request.getReason(), 200)); }
|
||||
|
||||
/**
|
||||
* 重新推送(高危):原单已推送金蝶且旧单已在金蝶冲销后,克隆新正式结算单并应用调整,
|
||||
* 以新单号组成新防重键生成全新应付/应收单,调整单改挂新单、原单标记已调整并解锁旧锁。
|
||||
* <p>
|
||||
* 非事务编排:克隆+应用(独立事务)→ 推送(外部远程)→ 收尾回填,推送失败不回滚克隆,
|
||||
* 重试时按来源原单号幂等复用已克隆新单,从推送步骤续上。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String repush(Long adjustmentId) {
|
||||
SettlementAdjustment adjustment = existing(adjustmentId);
|
||||
if (!TransportBusiConstant.APPROVED.equals(adjustment.getApprovalStatus())) throw new ServiceException("仅审批通过的结算调整单允许重新推送");
|
||||
FormalSettlement formal = formalMapper.selectById(adjustment.getFormalSettlementId());
|
||||
if (formal == null || !"synced".equals(formal.getKingdeeSyncStatus())) throw new ServiceException("关联正式结算单尚未推送金蝶,无需重新推送");
|
||||
String kingdeeNo = "K3AP" + DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());
|
||||
formal.setKingdeeBillNo(kingdeeNo); formal.setSyncedTime(LocalDateTime.now()); formalMapper.updateById(formal);
|
||||
return kingdeeNo;
|
||||
if (!TransportBusiConstant.APPROVED.equals(adjustment.getApprovalStatus())) {
|
||||
throw new ServiceException("仅审批通过的结算调整单允许重新推送");
|
||||
}
|
||||
FormalSettlement original = formalMapper.selectById(adjustment.getFormalSettlementId());
|
||||
if (original == null) {
|
||||
throw new ServiceException("关联正式结算单不存在");
|
||||
}
|
||||
if (!KingdeeBfiConstant.KINGDEE_STATUS_SYNCED.equals(original.getKingdeeSyncStatus())) {
|
||||
throw new ServiceException("结算单[" + Func.toStr(original.getFormalSettlementNo())
|
||||
+ "]尚未推送金蝶,请先执行「推送金蝶」");
|
||||
}
|
||||
// 冲销硬闸门:仅当金蝶侧原应付/应收单已被冲销(部分冲销放行)才允许以新单号重推,避免重复生成财务单据
|
||||
if (!this.kingdeeWrittenOff(original)) {
|
||||
throw new ServiceException("结算单[" + Func.toStr(original.getFormalSettlementNo())
|
||||
+ "]的原应付/应收单尚未在金蝶冲销,禁止重推");
|
||||
}
|
||||
// 幂等:同一原单已克隆过新单则复用(重试从推送步骤续上),避免重复建单
|
||||
FormalSettlement clone = existingClone(original.getFormalSettlementNo());
|
||||
if (clone == null) {
|
||||
Long cloneId = self().cloneAndApplyForRepush(original.getId(), adjustment.getId());
|
||||
clone = formalMapper.selectById(cloneId);
|
||||
}
|
||||
// 推送新单:新单号天然组成新防重键,内部完成 A102-2/A302-2 推送 + 落锁 + 附件上传
|
||||
// shortcut: 成功后若再次点击重推,调整单已改挂新单且新单未冲销,会被冲销闸门拦截(无副作用);如需彻底隐藏由前端按「关联单是否克隆单」判断
|
||||
String billNo = formalSettlementService.syncKingdee(clone.getId());
|
||||
// 成功收尾:调整单改挂新单 + 回填金蝶单号/同步状态;原单标记已调整 + 旧锁解锁(业务数据不动)
|
||||
SettlementAdjustment patch = new SettlementAdjustment();
|
||||
patch.setId(adjustment.getId());
|
||||
patch.setFormalSettlementId(clone.getId());
|
||||
patch.setFormalSettlementNo(clone.getFormalSettlementNo());
|
||||
patch.setKingdeeSyncStatus(KingdeeBfiConstant.KINGDEE_STATUS_SYNCED);
|
||||
patch.setKingdeeBillNo(billNo);
|
||||
updateById(patch);
|
||||
formalSettlementService.markSettlementAdjusted(original.getId(), adjustment.getAdjustmentNo(), clone.getFormalSettlementNo());
|
||||
return billNo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void approve(SettlementAdjustmentStatusRequest request) {
|
||||
public Long cloneAndApplyForRepush(Long originalSettlementId, Long adjustmentId) {
|
||||
IFormalSettlementService.CloneResult cloneResult = formalSettlementService.cloneFormalSettlement(originalSettlementId);
|
||||
FormalSettlement clone = formalMapper.selectById(cloneResult.settlementId());
|
||||
applyAdjustmentToSettlement(clone, existing(adjustmentId), cloneResult.detailIdMap(), cloneResult.feeIdMap());
|
||||
return cloneResult.settlementId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String syncKingdee(Long adjustmentId) {
|
||||
SettlementAdjustment adjustment = existing(adjustmentId);
|
||||
if (!TransportBusiConstant.APPROVED.equals(adjustment.getApprovalStatus())) {
|
||||
throw new ServiceException("仅审批通过的结算调整单允许推送金蝶");
|
||||
}
|
||||
// 未推送/生成失败场景:推送关联正式结算单,成功后由正式结算单回填调整单金蝶单号与同步状态
|
||||
return formalSettlementService.syncKingdee(adjustment.getFormalSettlementId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> kingdeeWriteOffPrecheck(Long formalSettlementId) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
FormalSettlement settlement = formalMapper.selectById(formalSettlementId);
|
||||
if (settlement == null) {
|
||||
throw new ServiceException("关联正式结算单不存在");
|
||||
}
|
||||
if (!KingdeeBfiConstant.KINGDEE_STATUS_SYNCED.equals(settlement.getKingdeeSyncStatus())) {
|
||||
result.put("syncStatus", KingdeeBfiConstant.KINGDEE_STATUS_UNSYNCED);
|
||||
result.put("writtenOff", null);
|
||||
result.put("message", "该结算单尚未推送金蝶,审批通过后将自动推送原单");
|
||||
return result;
|
||||
}
|
||||
result.put("syncStatus", KingdeeBfiConstant.KINGDEE_STATUS_SYNCED);
|
||||
try {
|
||||
boolean writtenOff = this.kingdeeWrittenOff(settlement);
|
||||
result.put("writtenOff", writtenOff);
|
||||
result.put("message", writtenOff
|
||||
? "该结算单已推送金蝶且旧单已冲销,审批后可直接重新推送新单"
|
||||
: "该结算单已推送金蝶且旧单尚未冲销,审批通过后需先由财务在金蝶完成冲销,才能重新推送");
|
||||
} catch (Exception exception) {
|
||||
// 预检为软提示:金蝶查询异常降级为「未校验」,不阻断建单,由审批分流与重推闸门兜底
|
||||
log.warn("结算调整单建单预检:金蝶冲销状态查询失败,降级未校验,正式结算单[{}]",
|
||||
settlement.getFormalSettlementNo(), exception);
|
||||
result.put("writtenOff", null);
|
||||
result.put("message", "金蝶冲销状态查询失败,未校验;审批后请以实际冲销情况为准");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String approve(SettlementAdjustmentStatusRequest request) {
|
||||
SettlementAdjustment adjustment = existing(request.getId());
|
||||
if (!TransportBusiConstant.REVIEWING.equals(adjustment.getApprovalStatus())) throw new ServiceException("仅审批中的结算调整单允许审核");
|
||||
FormalSettlement formal = adjustableFormalSettlement(adjustment.getFormalSettlementId());
|
||||
boolean synced = KingdeeBfiConstant.KINGDEE_STATUS_SYNCED.equals(formal.getKingdeeSyncStatus());
|
||||
if (synced) {
|
||||
// 已推送场景:原单业务数据零改动,仅追加 1 条重推提示变更记录,待财务在金蝶手工冲销旧单后重推
|
||||
saveFormalChange(formal.getId(), "合计费用项", null, "调整",
|
||||
"结算调整单" + adjustment.getAdjustmentNo() + "审批通过,调整金额【" + money(adjustment.getAdjustmentAmount()) + "】,待重推金蝶",
|
||||
adjustment.getRemark());
|
||||
} else {
|
||||
// 未推送场景:回写原单费用/金额并重算结算金额,事务提交后由事件监听自动推送金蝶
|
||||
applyAdjustmentToSettlement(formal, adjustment, Map.of(), Map.of());
|
||||
saveFormalChange(formal.getId(), "合计费用项", null, "调整",
|
||||
"调整单" + adjustment.getAdjustmentNo() + "调整金额【" + money(adjustment.getAdjustmentAmount()) + "】",
|
||||
adjustment.getRemark());
|
||||
}
|
||||
adjustment.setKingdeeSyncStatus(formal.getKingdeeSyncStatus()); adjustment.setApprovalStatus(TransportBusiConstant.APPROVED); adjustment.setCurrentNode("审批通过"); adjustment.setCurrentProcessor(AuthUtil.getUserName()); adjustment.setApprovedTime(LocalDateTime.now()); updateById(adjustment);
|
||||
if (!synced) {
|
||||
// 审批事务提交后(AFTER_COMMIT)自动推原单,成功回填调整单 synced + 金蝶单号、失败置 failed
|
||||
eventPublisher.publishEvent(new KingdeeBillApprovedEvent(
|
||||
KingdeeBillApprovedEvent.BIZ_FORMAL_SETTLEMENT, formal.getId()));
|
||||
return null;
|
||||
}
|
||||
return "原结算单已推送金蝶,请财务人员在金蝶手工完成旧应付/应收单冲销处理后再重新推送";
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用调整到目标正式结算单(未推送场景作用于原单,重推场景作用于克隆新单):
|
||||
* 回写明细费项金额、汇总费用项、重算明细与结算金额;明细/费项 id 经映射转换(原单调整作用于原单时传空映射)。
|
||||
*
|
||||
* @param formal 目标正式结算单(改动后需落库)
|
||||
* @param adjustment 结算调整单
|
||||
* @param detailIdMap 原结算明细id -> 目标结算明细id(原单场景传空 Map)
|
||||
* @param feeIdMap 原明细费项id -> 目标明细费项id(原单场景传空 Map)
|
||||
*/
|
||||
private void applyAdjustmentToSettlement(FormalSettlement formal, SettlementAdjustment adjustment,
|
||||
Map<Long, Long> detailIdMap, Map<Long, Long> feeIdMap) {
|
||||
for (SettlementAdjustmentDetail item : detailMapper.selectList(Wrappers.<SettlementAdjustmentDetail>lambdaQuery().eq(SettlementAdjustmentDetail::getAdjustmentId, adjustment.getId()))) {
|
||||
if (money(item.getAdjustmentAmountTax()).signum() == 0) continue;
|
||||
if (isManualFee(item)) {
|
||||
@@ -229,12 +362,14 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
"新增【" + item.getFeeItem() + "】调整费用【" + money(item.getAdjustmentAmountTax()) + "】",
|
||||
Func.isEmpty(item.getRemark()) ? adjustment.getRemark() : item.getRemark());
|
||||
} else {
|
||||
FormalSettlementDetailFee fee = validateFee(formal.getId(), item.getFormalSettlementDetailId(), item.getFormalSettlementDetailFeeId());
|
||||
Long detailId = detailIdMap.getOrDefault(item.getFormalSettlementDetailId(), item.getFormalSettlementDetailId());
|
||||
Long feeId = feeIdMap.getOrDefault(item.getFormalSettlementDetailFeeId(), item.getFormalSettlementDetailFeeId());
|
||||
FormalSettlementDetailFee fee = validateFee(formal.getId(), detailId, feeId);
|
||||
BigDecimal beforeAmount = money(fee.getSettlementAmountTax());
|
||||
fee.setSettlementAmountTax(beforeAmount.add(money(item.getAdjustmentAmountTax())));
|
||||
if (item.getAdjustmentAmountNoTax() != null) fee.setSettlementAmountNoTax(money(fee.getSettlementAmountNoTax()).add(item.getAdjustmentAmountNoTax()));
|
||||
fee.setAdjustAmount(money(fee.getAdjustAmount()).add(item.getAdjustmentAmountTax())); formalFeeMapper.updateById(fee);
|
||||
FormalSettlementDetail formalDetail = formalDetailMapper.selectById(item.getFormalSettlementDetailId());
|
||||
FormalSettlementDetail formalDetail = formalDetailMapper.selectById(detailId);
|
||||
saveFormalChange(formal.getId(), "结算明细项", formalDetail == null ? null : formalDetail.getLineNo(),
|
||||
"调整", "【" + (Func.isEmpty(item.getFeeItem()) ? "结算费用" : item.getFeeItem())
|
||||
+ "】从【" + beforeAmount + "】调整为【" + fee.getSettlementAmountTax() + "】",
|
||||
@@ -249,15 +384,46 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|
||||
detail.setAdjustAmount(detail.getSettlementAmountTax().subtract(money(detail.getOriginalAmount()))); formalDetailMapper.updateById(detail);
|
||||
}
|
||||
BigDecimal amount = money(formal.getSettlementAmount()).add(money(adjustment.getAdjustmentAmount()));
|
||||
// 调整后结算金额不得低于项目预付核销合计,否则已付将超过结算金额且无法拦截
|
||||
BigDecimal writeOffTotal = this.projectAdvanceWriteOffService.sumBySettlement(
|
||||
TransportBusiConstant.SETTLEMENT_BILL_FORMAL, formal.getId());
|
||||
if (money(writeOffTotal).compareTo(money(amount)) > 0) {
|
||||
throw new ServiceException("调整后结算金额不能低于项目预付核销合计" + money(writeOffTotal) + ",请先调减项目预付核销");
|
||||
}
|
||||
formal.setSettlementAmount(amount);
|
||||
formal.setLocalSettlementAmount(amount.multiply(formal.getExchangeRate() == null ? BigDecimal.ONE : formal.getExchangeRate()));
|
||||
formal.setRemainingPayableAmount(amount.subtract(money(formal.getPaidAmount()))
|
||||
.max(BigDecimal.ZERO));
|
||||
formal.setRemainingPayableAmount(amount.subtract(money(formal.getPaidAmount())).max(BigDecimal.ZERO));
|
||||
formalMapper.updateById(formal);
|
||||
saveFormalChange(formal.getId(), "合计费用项", null, "调整",
|
||||
"调整单" + adjustment.getAdjustmentNo() + "调整金额【" + money(adjustment.getAdjustmentAmount()) + "】",
|
||||
adjustment.getRemark());
|
||||
adjustment.setKingdeeSyncStatus(formal.getKingdeeSyncStatus()); adjustment.setApprovalStatus(TransportBusiConstant.APPROVED); adjustment.setCurrentNode("审批通过"); adjustment.setCurrentProcessor(AuthUtil.getUserName()); adjustment.setApprovedTime(LocalDateTime.now()); updateById(adjustment);
|
||||
// 重算收付汇总(含核销计入),保持付款状态与剩余可付准确
|
||||
formalSettlementService.refreshPaymentSummary(formal.getId());
|
||||
}
|
||||
|
||||
/** 涉事事务的自代理:跨方法调用确保 {@link #cloneAndApplyForRepush} 的事务注解生效 */
|
||||
private ISettlementAdjustmentService self() {
|
||||
return SpringUtil.getBean(ISettlementAdjustmentService.class);
|
||||
}
|
||||
|
||||
/** 按来源原正式结算单号查已克隆新单(重推幂等:同一原单不重复建单) */
|
||||
private FormalSettlement existingClone(String originFormalSettlementNo) {
|
||||
List<FormalSettlement> clones = formalMapper.selectList(Wrappers.<FormalSettlement>lambdaQuery()
|
||||
.eq(FormalSettlement::getOriginFormalSettlementNo, originFormalSettlementNo)
|
||||
.orderByDesc(FormalSettlement::getCreateTime));
|
||||
return clones.isEmpty() ? null : clones.get(0);
|
||||
}
|
||||
|
||||
/** 按原单旧防重键(原结算单号_明细单据号)查金蝶是否已冲销(应付 A102-4 / 应收 A302-4,部分冲销放行) */
|
||||
private boolean kingdeeWrittenOff(FormalSettlement settlement) {
|
||||
List<FormalSettlementDetail> details = formalDetailMapper.selectList(Wrappers.<FormalSettlementDetail>lambdaQuery()
|
||||
.eq(FormalSettlementDetail::getFormalSettlementId, settlement.getId())
|
||||
.eq(FormalSettlementDetail::getIsDeleted, 0)
|
||||
.orderByAsc(FormalSettlementDetail::getLineNo));
|
||||
if (Func.isEmpty(details)) {
|
||||
return false;
|
||||
}
|
||||
if ("receivable".equals(settlement.getSettlementType())) {
|
||||
return kingdeeArBillSyncSupport.queryArBillWrittenOff(kingdeeArBillSyncSupport.arBillSrcCode(settlement, details));
|
||||
}
|
||||
return kingdeeApBillSyncSupport.queryApBillWrittenOff(kingdeeApBillSyncSupport.apBillSrcCode(settlement, details));
|
||||
}
|
||||
|
||||
private void saveFormalChange(Long settlementId, String changeType, Integer lineNo,
|
||||
|
||||
+56
@@ -14,6 +14,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.constant.KingdeeBfiConstant;
|
||||
import org.springblade.transport.constant.TransportBusiConstant;
|
||||
import org.springblade.transport.mapper.PaymentApplicationMapper;
|
||||
import org.springblade.transport.mapper.SettlementProjectAdvanceWriteOffMapper;
|
||||
@@ -28,8 +29,11 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -56,6 +60,7 @@ public class SettlementProjectAdvanceWriteOffServiceImpl implements ISettlementP
|
||||
.eq(PaymentApplication::getContractId, contractId)
|
||||
.eq(PaymentApplication::getPaymentType, TransportBusiConstant.PAYMENT_TYPE_PROJECT_ADVANCE)
|
||||
.eq(PaymentApplication::getApprovalStatus, TransportBusiConstant.APPROVED)
|
||||
.eq(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_PAID)
|
||||
.eq(PaymentApplication::getIsDeleted, 0)
|
||||
.orderByDesc(PaymentApplication::getCreateTime));
|
||||
List<ProjectAdvanceWriteOffOptionVO> options = new ArrayList<>();
|
||||
@@ -121,6 +126,20 @@ public class SettlementProjectAdvanceWriteOffServiceImpl implements ISettlementP
|
||||
return money(writeOffMapper.sumWriteOffBySettlement(settlementType, settlementId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Long, BigDecimal> sumBySettlementIds(String settlementType, Collection<Long> settlementIds) {
|
||||
if (Func.isEmpty(settlementType) || Func.isEmpty(settlementIds)) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<Long, BigDecimal> result = new HashMap<>();
|
||||
for (Map<String, Object> row : writeOffMapper.sumWriteOffBySettlementIds(settlementType, settlementIds)) {
|
||||
if (row.get("settlementId") instanceof Number settlementId && row.get("totalAmount") instanceof BigDecimal totalAmount) {
|
||||
result.put(settlementId.longValue(), money(totalAmount));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void replaceWriteOffs(String settlementType, Long settlementId, Long contractId,
|
||||
@@ -157,6 +176,9 @@ public class SettlementProjectAdvanceWriteOffServiceImpl implements ISettlementP
|
||||
if (!TransportBusiConstant.APPROVED.equals(application.getApprovalStatus())) {
|
||||
throw new ServiceException("仅可选择审核通过的项目预付单");
|
||||
}
|
||||
if (!KingdeeBfiConstant.KINGDEE_STATUS_PAID.equals(application.getKingdeeStatus())) {
|
||||
throw new ServiceException("预付单" + Func.toStr(application.getPaymentNo(), "") + "尚未付款,项目预付须金蝶付款完成后方可核销");
|
||||
}
|
||||
if (!Objects.equals(contractId, application.getContractId())) {
|
||||
throw new ServiceException("项目预付单与结算单合同不一致");
|
||||
}
|
||||
@@ -203,6 +225,40 @@ public class SettlementProjectAdvanceWriteOffServiceImpl implements ISettlementP
|
||||
.eq(SettlementProjectAdvanceWriteOff::getSettlementId, settlementId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int deleteBySettlementIds(String settlementType, Collection<Long> settlementIds) {
|
||||
if (Func.isEmpty(settlementType) || Func.isEmpty(settlementIds)) {
|
||||
return 0;
|
||||
}
|
||||
return writeOffMapper.delete(Wrappers.<SettlementProjectAdvanceWriteOff>lambdaQuery()
|
||||
.eq(SettlementProjectAdvanceWriteOff::getSettlementType, settlementType)
|
||||
.in(SettlementProjectAdvanceWriteOff::getSettlementId, settlementIds));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<Long> deleteByPaymentApplicationIds(Collection<Long> paymentApplicationIds) {
|
||||
if (Func.isEmpty(paymentApplicationIds)) {
|
||||
return List.of();
|
||||
}
|
||||
List<SettlementProjectAdvanceWriteOff> rows = writeOffMapper.selectList(
|
||||
Wrappers.<SettlementProjectAdvanceWriteOff>lambdaQuery()
|
||||
.eq(SettlementProjectAdvanceWriteOff::getIsDeleted, 0)
|
||||
.in(SettlementProjectAdvanceWriteOff::getPaymentApplicationId, paymentApplicationIds));
|
||||
if (rows.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
writeOffMapper.delete(Wrappers.<SettlementProjectAdvanceWriteOff>lambdaQuery()
|
||||
.in(SettlementProjectAdvanceWriteOff::getPaymentApplicationId, paymentApplicationIds));
|
||||
return rows.stream()
|
||||
.filter(row -> TransportBusiConstant.SETTLEMENT_BILL_FORMAL.equals(row.getSettlementType()))
|
||||
.map(SettlementProjectAdvanceWriteOff::getSettlementId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private BigDecimal money(BigDecimal value) {
|
||||
return value == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)
|
||||
: value.setScale(2, RoundingMode.HALF_UP);
|
||||
|
||||
+61
-11
@@ -200,6 +200,33 @@ public class KingdeeApBillSyncSupport {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A102-4 冲销状态查询:按防重键查询财务应付单,返回该防重键下是否存在「已被冲销」单据
|
||||
* (BfiApBillVO.hadwrittenoff=true,部分冲销同样视为已冲销)。
|
||||
* 结算调整重推硬闸门用:仅当原单已被冲销才允许以新来源单据号重推新应付单
|
||||
*
|
||||
* @param awrpSrcCode 财务应付单防重键
|
||||
* @return true=金蝶侧原应付单已被冲销
|
||||
*/
|
||||
public boolean queryApBillWrittenOff(String awrpSrcCode) {
|
||||
if (Func.isBlank(awrpSrcCode)) {
|
||||
return false;
|
||||
}
|
||||
BfiApBillQueryDTO param = new BfiApBillQueryDTO();
|
||||
param.setAwrpSrcCode(List.of(awrpSrcCode));
|
||||
param.setAwrpSrcSys(bfiProperties.getSrcSys());
|
||||
for (BfiApBillVO apBill : BfiUtil.queryAllPages(pageNo -> {
|
||||
param.setPageNo(pageNo);
|
||||
param.setPageSize(PAY_RESULT_PAGE_SIZE);
|
||||
return bfiService.queryApBill(param);
|
||||
})) {
|
||||
if (awrpSrcCode.equals(apBill.getAwrpSrcCode()) && Boolean.TRUE.equals(apBill.getHadwrittenoff())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ==================== A102-2 财务应付单保存 ====================
|
||||
|
||||
/**
|
||||
@@ -860,7 +887,7 @@ public class KingdeeApBillSyncSupport {
|
||||
BfiBillSaveResultItemVO item = saveResult.getResult().get(0);
|
||||
if (!Boolean.TRUE.equals(item.getBillStatus())) {
|
||||
String errorText = Func.isBlank(item.getErrorText()) ? "未知错误" : item.getErrorText();
|
||||
throw new ServiceException(action + "失败:" + errorText);
|
||||
throw new ServiceException(action + "失败:" + errorText + item.getFailMark());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -947,16 +974,7 @@ public class KingdeeApBillSyncSupport {
|
||||
* @return 组织编码
|
||||
*/
|
||||
public String kingdeeOrgNumber(Long projectId, KingdeeSyncContext context) {
|
||||
if (projectId == null) {
|
||||
throw new ServiceException("推送金蝶失败,单据未关联项目,无法确定金蝶组织编码");
|
||||
}
|
||||
ProjectApply project = context.projectCache().computeIfAbsent(projectId, projectApplyMapper::selectById);
|
||||
if (project == null) {
|
||||
throw new ServiceException("推送金蝶失败,项目[" + projectId + "]不存在,无法确定金蝶组织编码");
|
||||
}
|
||||
if (project.getBusinessDeptId() == null) {
|
||||
throw new ServiceException("推送金蝶失败,项目[" + Func.toStr(project.getProjectName()) + "]未维护业务部门,无法确定金蝶组织编码");
|
||||
}
|
||||
ProjectApply project = this.requireOrgProject(projectId, context);
|
||||
// 沿部门树向上反查平台公司,上限 20 级防脏数据成环死循环
|
||||
Dept dept = SysCache.getDept(project.getBusinessDeptId());
|
||||
for (int depth = 0; dept != null && depth < 20; depth++) {
|
||||
@@ -971,6 +989,38 @@ public class KingdeeApBillSyncSupport {
|
||||
throw new ServiceException("推送金蝶失败,项目业务部门[" + Func.toStr(project.getBusinessDeptName()) + "]未归属平台公司,无法确定金蝶组织编码");
|
||||
}
|
||||
|
||||
/**
|
||||
* 金蝶部门编码:项目业务部门自身的金蝶编码(blade_dept.kingdee_dept_code,不上溯平台公司),
|
||||
* 用于付款申请单 awrp_department_number(部门.编码);项目未关联/未维护业务部门/业务部门未维护金蝶编码时拦截
|
||||
*
|
||||
* @param projectId 项目id
|
||||
* @param context 金蝶同步上下文(缓存载体)
|
||||
* @return 部门编码
|
||||
*/
|
||||
public String kingdeeDeptNumber(Long projectId, KingdeeSyncContext context) {
|
||||
ProjectApply project = this.requireOrgProject(projectId, context);
|
||||
Dept dept = SysCache.getDept(project.getBusinessDeptId());
|
||||
if (dept == null || Func.isBlank(dept.getKingdeeDeptCode())) {
|
||||
throw new ServiceException("推送金蝶失败,项目业务部门[" + Func.toStr(project.getBusinessDeptName()) + "]未维护金蝶编码,请检查部门数据");
|
||||
}
|
||||
return dept.getKingdeeDeptCode();
|
||||
}
|
||||
|
||||
/** 金蝶组织/部门编码共用校验:项目存在且已维护业务部门 */
|
||||
private ProjectApply requireOrgProject(Long projectId, KingdeeSyncContext context) {
|
||||
if (projectId == null) {
|
||||
throw new ServiceException("推送金蝶失败,单据未关联项目,无法确定金蝶组织编码");
|
||||
}
|
||||
ProjectApply project = context.projectCache().computeIfAbsent(projectId, projectApplyMapper::selectById);
|
||||
if (project == null) {
|
||||
throw new ServiceException("推送金蝶失败,项目[" + projectId + "]不存在,无法确定金蝶组织编码");
|
||||
}
|
||||
if (project.getBusinessDeptId() == null) {
|
||||
throw new ServiceException("推送金蝶失败,项目[" + Func.toStr(project.getProjectName()) + "]未维护业务部门,无法确定金蝶组织编码");
|
||||
}
|
||||
return project;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同结算币种转金蝶币别代码(RMB 统一映射 CNY)
|
||||
*
|
||||
|
||||
+27
@@ -138,6 +138,33 @@ public class KingdeeArBillSyncSupport {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A302-4 冲销状态查询:按防重键查询财务应收单,返回该防重键下是否存在「已被冲销」单据
|
||||
* (BfiArBillVO.hadwrittenoff=true,部分冲销同样视为已冲销)。
|
||||
* 结算调整重推硬闸门用:仅当原单已被冲销才允许以新来源单据号重推新应收单
|
||||
*
|
||||
* @param arBillSrcCode 财务应收单防重键
|
||||
* @return true=金蝶侧原应收单已被冲销
|
||||
*/
|
||||
public boolean queryArBillWrittenOff(String arBillSrcCode) {
|
||||
if (Func.isBlank(arBillSrcCode)) {
|
||||
return false;
|
||||
}
|
||||
BfiArBillQueryDTO param = new BfiArBillQueryDTO();
|
||||
param.setAwrpSrcCode(List.of(arBillSrcCode));
|
||||
param.setAwrpSrcSys(bfiProperties.getSrcSys());
|
||||
for (BfiArBillVO arBill : BfiUtil.queryAllPages(pageNo -> {
|
||||
param.setPageNo(pageNo);
|
||||
param.setPageSize(AR_RESULT_PAGE_SIZE);
|
||||
return bfiService.queryArBill(param);
|
||||
})) {
|
||||
if (arBillSrcCode.equals(arBill.getAwrpSrcCode()) && Boolean.TRUE.equals(arBill.getHadwrittenoff())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ==================== A302-2 财务应收单保存 ====================
|
||||
|
||||
/**
|
||||
|
||||
+1
@@ -45,6 +45,7 @@ public class FormalSettlementWrapper extends BaseEntityWrapper<FormalSettlement,
|
||||
case "voided" -> "已作废";
|
||||
default -> entity.getApprovalStatus();
|
||||
});
|
||||
vo.setAdjustStatusName("adjusted".equals(entity.getAdjustStatus()) ? "已调整" : null);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user