473 lines
25 KiB
Java
473 lines
25 KiB
Java
package com.baiyue.firstleg.service;
|
|
|
|
import com.baiyue.common.core.api.ResultCode;
|
|
import com.baiyue.common.core.exception.BizException;
|
|
import com.baiyue.firstleg.dto.InboundFeeDetailDtos.SaveRequest;
|
|
import com.baiyue.firstleg.entity.FeeItemEntity;
|
|
import com.baiyue.firstleg.entity.InboundFeeEntity;
|
|
import com.baiyue.firstleg.entity.InboundFeeItemEntity;
|
|
import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
|
|
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
|
import com.baiyue.firstleg.repository.FbaPricingRepository;
|
|
import com.baiyue.firstleg.repository.FeeItemRepository;
|
|
import jakarta.annotation.Resource;
|
|
import org.springframework.dao.DuplicateKeyException;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
import java.math.RoundingMode;
|
|
import java.time.LocalDateTime;
|
|
import java.util.List;
|
|
|
|
/** 入库单人工费用明细应用服务。 */
|
|
@Service
|
|
public class InboundFeeDetailService {
|
|
private static final String FREIGHT_FEE_CODE = "FBA_HEAD_FREIGHT";
|
|
@Resource
|
|
private FeeItemRepository repository;
|
|
@Resource
|
|
private FbaPricingRepository pricingRepository;
|
|
@Resource
|
|
private FinanceFeeSourceOutboxService financeOutbox;
|
|
@Resource
|
|
private WarehouseSupplierSnapshotService suppliers;
|
|
@Resource
|
|
private FeePaymentSnapshotValidator paymentSnapshots;
|
|
|
|
@Transactional
|
|
public List<InboundFeeDetailEntity> list(long tenantId, long inboundId, boolean includeHistory) {
|
|
requiredInbound(tenantId, inboundId);
|
|
materializeLatestFreightIfMissing(tenantId, inboundId);
|
|
return repository.listDetails(tenantId, inboundId, includeHistory);
|
|
}
|
|
|
|
private void materializeLatestFreightIfMissing(long tenantId, long inboundId) {
|
|
InboundFeeEntity latest = pricingRepository.findLatestFee(tenantId, inboundId);
|
|
if (latest == null || repository.findBySourceFee(
|
|
tenantId, "FBA_PRICING", latest.getId()) != null) return;
|
|
LocalDateTime occurredAt = latest.getSentAt() != null ? latest.getSentAt()
|
|
: latest.getCalculatedAt();
|
|
if ("SENT".equals(latest.getFeeStatus())) {
|
|
publishFreight(tenantId, inboundId, latest.getId(), 0L, "SYSTEM", occurredAt);
|
|
} else if ("CALCULATED".equals(latest.getFeeStatus())) {
|
|
materializeCalculatedFreight(tenantId, inboundId, latest.getId(), 0L, occurredAt);
|
|
}
|
|
}
|
|
|
|
public InboundFeeDetailEntity publishedFreight(long tenantId, long inboundId) {
|
|
requiredInbound(tenantId, inboundId);
|
|
InboundFeeDetailEntity detail = repository.findPublishedFreight(tenantId, inboundId);
|
|
if (detail == null) throw new BizException(ResultCode.NOT_FOUND, "入库运费明细尚未发布");
|
|
return detail;
|
|
}
|
|
|
|
/** 将计费快照发布为后台和客户共同读取的入库应收运费明细。 */
|
|
@Transactional
|
|
public InboundFeeDetailEntity publishFreight(long tenantId, long inboundId, long feeId,
|
|
long operatorId, String operatorName,
|
|
LocalDateTime publishedAt) {
|
|
InboundFeeDetailEntity existing = repository.findBySourceFee(tenantId, "FBA_PRICING", feeId);
|
|
if (existing != null && "PUBLISHED".equals(existing.getPublicationStatus())) return existing;
|
|
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
|
|
InboundFeeEntity fee = pricingRepository.findFee(tenantId, feeId);
|
|
if (fee == null || !Long.valueOf(inboundId).equals(fee.getInboundId())) {
|
|
throw new BizException(ResultCode.NOT_FOUND, "待发布的入库计费快照不存在");
|
|
}
|
|
if (!"SENT".equals(fee.getFeeStatus())) {
|
|
throw conflict("只有已发送状态的计费快照才能生成客户运费明细");
|
|
}
|
|
LocalDateTime now = publishedAt == null ? LocalDateTime.now() : publishedAt;
|
|
if (existing != null) {
|
|
repository.supersedeFreight(tenantId, inboundId, feeId, operatorId, now);
|
|
if (repository.publishCalculatedFreight(
|
|
tenantId, feeId, operatorId, operatorName, now) != 1) {
|
|
throw conflict("初算费用明细状态已变化,请刷新后重试");
|
|
}
|
|
return repository.findBySourceFee(tenantId, "FBA_PRICING", feeId);
|
|
}
|
|
FeeItemEntity feeItem = requiredFreightItem();
|
|
InboundFeeItemEntity baseItem = pricingRepository.findBaseItem(tenantId, feeId);
|
|
repository.supersedePublishedFreight(tenantId, inboundId, feeId, operatorId, now);
|
|
|
|
InboundFeeDetailEntity detail = new InboundFeeDetailEntity();
|
|
detail.setTenantId(tenantId);
|
|
detail.setInboundId(inboundId);
|
|
detail.setFeeItemId(feeItem.getId());
|
|
detail.setSourceType("FBA_PRICING");
|
|
detail.setSourceFeeId(feeId);
|
|
detail.setSourceFeeVersion(fee.getFeeVersion());
|
|
detail.setSourceFeeNo(fee.getFeeNo());
|
|
detail.setInboundCode(inbound.getInboundCode());
|
|
detail.setDirection("RECEIVABLE");
|
|
detail.setBusinessDate(now.toLocalDate());
|
|
detail.setCustomerId(inbound.getCustomerId());
|
|
detail.setCustomerName(inbound.getCustomerName());
|
|
detail.setInvoiceTitle(inbound.getCompanyName());
|
|
detail.setFeeCategorySnapshot(feeItem.getCategory());
|
|
detail.setTradeScopeSnapshot(feeItem.getTradeScope());
|
|
detail.setFeeTypeNameSnapshot(feeItem.getFeeTypeName());
|
|
detail.setChannelCode(inbound.getChannelCode());
|
|
detail.setChannelName(inbound.getChannelName());
|
|
detail.setDestinationCode(inbound.getDestinationCode());
|
|
detail.setDestinationName(inbound.getDestinationName());
|
|
detail.setPackageCount(inbound.getTotalPackageCount());
|
|
detail.setActualGrossWeightKg(fee.getActualWeightKg());
|
|
detail.setActualVolumeCbm(fee.getActualVolumeCbm());
|
|
detail.setVolumeWeightKg(inbound.getVolumeWeightKg());
|
|
detail.setChargeableWeightKg("KG".equals(fee.getChargeUnit())
|
|
? fee.getChargeableQuantity() : inbound.getChargeableWeightKg());
|
|
detail.setChargeableVolumeCbm("CBM".equals(fee.getChargeUnit())
|
|
? fee.getChargeableQuantity() : inbound.getChargeableVolumeCbm());
|
|
detail.setChargeableQuantity(fee.getChargeableQuantity());
|
|
detail.setChargeUnit(fee.getChargeUnit());
|
|
detail.setUnitPrice(effectiveUnitPrice(fee));
|
|
detail.setCurrency(fee.getCurrency());
|
|
detail.setAmount(fee.getTotalAmount().setScale(2, RoundingMode.HALF_UP));
|
|
detail.setRemark(baseItem == null ? "来源费用单:" + fee.getFeeNo()
|
|
: "来源费用单:" + fee.getFeeNo() + ";基础单价:" + baseItem.getUnitPrice());
|
|
detail.setReviewStatus("APPROVED");
|
|
detail.setReviewerId(operatorId);
|
|
detail.setReviewerName(operatorName);
|
|
detail.setReviewedAt(now);
|
|
detail.setPublicationStatus("PUBLISHED");
|
|
detail.setPublishedBy(operatorId);
|
|
detail.setPublishedByName(operatorName);
|
|
detail.setPublishedAt(now);
|
|
detail.setInboundedAt(inbound.getInboundedAt());
|
|
detail.setCreatedBy(operatorId);
|
|
detail.setUpdatedBy(operatorId);
|
|
try {
|
|
repository.insertDetail(detail);
|
|
return detail;
|
|
} catch (DuplicateKeyException exception) {
|
|
InboundFeeDetailEntity concurrent = repository.findBySourceFee(
|
|
tenantId, "FBA_PRICING", feeId);
|
|
if (concurrent != null) return concurrent;
|
|
throw exception;
|
|
}
|
|
}
|
|
|
|
/** 初算成功后生成后台可见的草稿运费明细,不发布 Finance。 */
|
|
@Transactional
|
|
public InboundFeeDetailEntity materializeCalculatedFreight(long tenantId, long inboundId, long feeId,
|
|
long operatorId, LocalDateTime calculatedAt) {
|
|
InboundFeeDetailEntity existing = repository.findBySourceFee(tenantId, "FBA_PRICING", feeId);
|
|
if (existing != null) return existing;
|
|
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
|
|
InboundFeeEntity fee = pricingRepository.findFee(tenantId, feeId);
|
|
if (fee == null || !Long.valueOf(inboundId).equals(fee.getInboundId())) {
|
|
throw new BizException(ResultCode.NOT_FOUND, "待展示的入库计费快照不存在");
|
|
}
|
|
if (!"CALCULATED".equals(fee.getFeeStatus())) {
|
|
throw conflict("只有初算状态的计费快照可生成草稿运费明细");
|
|
}
|
|
LocalDateTime now = calculatedAt == null ? LocalDateTime.now() : calculatedAt;
|
|
repository.supersedeFreight(tenantId, inboundId, feeId, operatorId, now);
|
|
InboundFeeDetailEntity detail = freightDetail(
|
|
tenantId, inboundId, operatorId, null, now, inbound, fee, requiredFreightItem(), false);
|
|
try {
|
|
repository.insertDetail(detail);
|
|
return detail;
|
|
} catch (DuplicateKeyException exception) {
|
|
InboundFeeDetailEntity concurrent = repository.findBySourceFee(
|
|
tenantId, "FBA_PRICING", feeId);
|
|
if (concurrent != null) return concurrent;
|
|
throw exception;
|
|
}
|
|
}
|
|
|
|
private InboundFeeDetailEntity freightDetail(long tenantId, long inboundId, long operatorId,
|
|
String operatorName, LocalDateTime now,
|
|
InboundOrderEntity inbound, InboundFeeEntity fee,
|
|
FeeItemEntity feeItem, boolean published) {
|
|
InboundFeeItemEntity baseItem = pricingRepository.findBaseItem(tenantId, fee.getId());
|
|
InboundFeeDetailEntity detail = new InboundFeeDetailEntity();
|
|
detail.setTenantId(tenantId); detail.setInboundId(inboundId); detail.setFeeItemId(feeItem.getId());
|
|
detail.setSourceType("FBA_PRICING"); detail.setSourceFeeId(fee.getId());
|
|
detail.setSourceFeeVersion(fee.getFeeVersion()); detail.setSourceFeeNo(fee.getFeeNo());
|
|
detail.setInboundCode(inbound.getInboundCode()); detail.setDirection("RECEIVABLE");
|
|
detail.setBusinessDate(now.toLocalDate()); detail.setCustomerId(inbound.getCustomerId());
|
|
detail.setCustomerName(inbound.getCustomerName()); detail.setInvoiceTitle(inbound.getCompanyName());
|
|
detail.setFeeCategorySnapshot(feeItem.getCategory()); detail.setTradeScopeSnapshot(feeItem.getTradeScope());
|
|
detail.setFeeTypeNameSnapshot(feeItem.getFeeTypeName()); detail.setChannelCode(inbound.getChannelCode());
|
|
detail.setChannelName(inbound.getChannelName()); detail.setDestinationCode(inbound.getDestinationCode());
|
|
detail.setDestinationName(inbound.getDestinationName()); detail.setPackageCount(inbound.getTotalPackageCount());
|
|
detail.setActualGrossWeightKg(fee.getActualWeightKg()); detail.setActualVolumeCbm(fee.getActualVolumeCbm());
|
|
detail.setVolumeWeightKg(inbound.getVolumeWeightKg());
|
|
detail.setChargeableWeightKg("KG".equals(fee.getChargeUnit()) ? fee.getChargeableQuantity() : inbound.getChargeableWeightKg());
|
|
detail.setChargeableVolumeCbm("CBM".equals(fee.getChargeUnit()) ? fee.getChargeableQuantity() : inbound.getChargeableVolumeCbm());
|
|
detail.setChargeableQuantity(fee.getChargeableQuantity()); detail.setChargeUnit(fee.getChargeUnit());
|
|
detail.setUnitPrice(baseItem == null ? effectiveUnitPrice(fee) : baseItem.getUnitPrice());
|
|
detail.setCurrency(fee.getCurrency()); detail.setAmount(fee.getTotalAmount().setScale(2, RoundingMode.HALF_UP));
|
|
detail.setRemark("来源费用单:" + fee.getFeeNo());
|
|
detail.setReviewStatus(published ? "APPROVED" : "PENDING");
|
|
detail.setPublicationStatus(published ? "PUBLISHED" : "DRAFT");
|
|
detail.setCreatedBy(operatorId); detail.setUpdatedBy(operatorId);
|
|
if (published) {
|
|
detail.setReviewerId(operatorId); detail.setReviewerName(operatorName); detail.setReviewedAt(now);
|
|
detail.setPublishedBy(operatorId); detail.setPublishedByName(operatorName); detail.setPublishedAt(now);
|
|
}
|
|
return detail;
|
|
}
|
|
|
|
@Transactional
|
|
public InboundFeeDetailEntity create(long tenantId, long inboundId, long operatorId,
|
|
SaveRequest request) {
|
|
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
|
|
FeeItemEntity item = requiredActiveItem(request.feeItemId());
|
|
InboundFeeDetailEntity value = new InboundFeeDetailEntity();
|
|
value.setTenantId(tenantId);
|
|
value.setInboundId(inboundId);
|
|
value.setCreatedBy(operatorId);
|
|
apply(value, inbound, item, request, operatorId);
|
|
value.setReviewStatus("PENDING");
|
|
value.setSourceType("MANUAL");
|
|
value.setSourceFeeVersion(1);
|
|
value.setPublicationStatus("DRAFT");
|
|
repository.insertDetail(value);
|
|
return value;
|
|
}
|
|
|
|
@Transactional
|
|
public InboundFeeDetailEntity update(long tenantId, long inboundId, long detailId,
|
|
long operatorId, SaveRequest request) {
|
|
InboundFeeDetailEntity value = requiredEditable(tenantId, inboundId, detailId);
|
|
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
|
|
FeeItemEntity item = requiredActiveItem(request.feeItemId());
|
|
apply(value, inbound, item, request, operatorId);
|
|
value.setReviewStatus("PENDING");
|
|
value.setReviewerId(null);
|
|
value.setReviewerName(null);
|
|
value.setReviewedAt(null);
|
|
value.setReviewReason(null);
|
|
if (repository.updateDetail(value) != 1) throw conflict("费用明细已被其他人修改,请刷新后重试");
|
|
return required(tenantId, inboundId, detailId);
|
|
}
|
|
|
|
@Transactional
|
|
public void delete(long tenantId, long inboundId, long detailId) {
|
|
requiredEditable(tenantId, inboundId, detailId);
|
|
if (repository.deleteDetail(tenantId, inboundId, detailId) != 1) {
|
|
throw conflict("费用明细删除冲突");
|
|
}
|
|
}
|
|
|
|
@Transactional
|
|
public void approve(long tenantId, long inboundId, long detailId,
|
|
long reviewerId, String reviewerName) {
|
|
InboundFeeDetailEntity pending = requiredManual(tenantId, inboundId, detailId);
|
|
FeeItemEntity item = requiredActiveItem(pending.getFeeItemId());
|
|
revalidatePaymentSnapshot(pending);
|
|
refreshSupplierSnapshot(pending, item, reviewerId);
|
|
if (repository.review(tenantId, inboundId, detailId, "APPROVED",
|
|
reviewerId, reviewerName, null) != 1) {
|
|
throw conflict("只有待审核费用明细可以审核通过");
|
|
}
|
|
InboundFeeDetailEntity approved = required(tenantId, inboundId, detailId);
|
|
publishFinanceSource(approved, "AVAILABLE", LocalDateTime.now());
|
|
}
|
|
|
|
@Transactional
|
|
public InboundFeeDetailEntity adjust(long tenantId, long inboundId, long detailId,
|
|
int expectedSourceVersion, long operatorId,
|
|
SaveRequest request) {
|
|
InboundFeeDetailEntity value = requiredManual(tenantId, inboundId, detailId);
|
|
if (!"APPROVED".equals(value.getReviewStatus()) || "VOIDED".equals(value.getPublicationStatus())) {
|
|
throw conflict("只有审核通过且未作废的费用明细可以调整");
|
|
}
|
|
if (value.getSourceFeeVersion() == null || value.getSourceFeeVersion() != expectedSourceVersion) {
|
|
throw conflict("费用来源版本已变化,请刷新后重试");
|
|
}
|
|
InboundOrderEntity inbound = requiredInbound(tenantId, inboundId);
|
|
FeeItemEntity item = requiredActiveItem(request.feeItemId());
|
|
LocalDateTime now = LocalDateTime.now();
|
|
if (repository.supersedeApprovedManual(tenantId, inboundId, detailId,
|
|
expectedSourceVersion, operatorId, now) != 1) {
|
|
throw conflict("费用明细已被其他人调整,请刷新后重试");
|
|
}
|
|
InboundFeeDetailEntity next = new InboundFeeDetailEntity();
|
|
next.setTenantId(tenantId);
|
|
next.setInboundId(inboundId);
|
|
next.setSourceType("MANUAL");
|
|
next.setSourceFeeId(value.getSourceFeeId() == null ? value.getId() : value.getSourceFeeId());
|
|
next.setSourceFeeVersion(expectedSourceVersion + 1);
|
|
next.setSourceFeeNo(value.getSourceFeeNo());
|
|
next.setCreatedBy(operatorId);
|
|
apply(next, inbound, item, request, operatorId);
|
|
next.setReviewStatus("PENDING");
|
|
next.setPublicationStatus("DRAFT");
|
|
repository.insertDetail(next);
|
|
return next;
|
|
}
|
|
|
|
@Transactional
|
|
public void voidApproved(long tenantId, long inboundId, long detailId,
|
|
int expectedSourceVersion, long operatorId) {
|
|
InboundFeeDetailEntity current = requiredManual(tenantId, inboundId, detailId);
|
|
if (repository.voidApproved(tenantId, inboundId, detailId, expectedSourceVersion, operatorId) != 1) {
|
|
throw conflict("只有当前版本的已审核费用明细可以作废");
|
|
}
|
|
current.setPublicationStatus("VOIDED");
|
|
publishFinanceSource(current, "VOIDED", LocalDateTime.now());
|
|
}
|
|
|
|
@Transactional
|
|
public void reject(long tenantId, long inboundId, long detailId, long reviewerId,
|
|
String reviewerName, String reason) {
|
|
requiredManual(tenantId, inboundId, detailId);
|
|
if (repository.review(tenantId, inboundId, detailId, "REJECTED",
|
|
reviewerId, reviewerName, reason.trim()) != 1) {
|
|
throw conflict("只有待审核费用明细可以驳回");
|
|
}
|
|
}
|
|
|
|
private InboundOrderEntity requiredInbound(long tenantId, long inboundId) {
|
|
InboundOrderEntity value = repository.findInbound(tenantId, inboundId);
|
|
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
|
return value;
|
|
}
|
|
|
|
private FeeItemEntity requiredActiveItem(long itemId) {
|
|
FeeItemEntity value = repository.findItem(itemId);
|
|
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "费用项目不存在");
|
|
if (!"ACTIVE".equals(value.getStatus())) throw conflict("费用项目已停用,不能绑定");
|
|
return value;
|
|
}
|
|
|
|
private FeeItemEntity requiredFreightItem() {
|
|
FeeItemEntity item = repository.findItemByCode(FREIGHT_FEE_CODE);
|
|
if (item == null) throw conflict("系统费用项目FBA头程运费未初始化");
|
|
if (!"ACTIVE".equals(item.getStatus())) throw conflict("系统费用项目FBA头程运费已停用");
|
|
return item;
|
|
}
|
|
|
|
private static java.math.BigDecimal effectiveUnitPrice(InboundFeeEntity fee) {
|
|
if (fee.getChargeableQuantity() == null || fee.getChargeableQuantity().signum() <= 0) {
|
|
throw conflict("计费快照的计费量必须大于零");
|
|
}
|
|
return fee.getTotalAmount().divide(fee.getChargeableQuantity(), 4, RoundingMode.HALF_UP);
|
|
}
|
|
|
|
private static void ensureWarehouseApplicable(FeeItemEntity item, String direction) {
|
|
boolean applicable = "RECEIVABLE".equals(direction)
|
|
? Boolean.TRUE.equals(item.getWarehouseReceivable())
|
|
: Boolean.TRUE.equals(item.getWarehousePayable());
|
|
if (!applicable) {
|
|
throw conflict("所选费用项目未启用对应的仓储应收/应付,不能绑定入库单");
|
|
}
|
|
}
|
|
|
|
private InboundFeeDetailEntity required(long tenantId, long inboundId, long detailId) {
|
|
InboundFeeDetailEntity value = repository.findDetail(tenantId, inboundId, detailId);
|
|
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "入库费用明细不存在");
|
|
return value;
|
|
}
|
|
|
|
private InboundFeeDetailEntity requiredEditable(long tenantId, long inboundId, long detailId) {
|
|
InboundFeeDetailEntity value = requiredManual(tenantId, inboundId, detailId);
|
|
if ("APPROVED".equals(value.getReviewStatus())) throw conflict("审核通过的费用明细不能修改或删除");
|
|
return value;
|
|
}
|
|
|
|
private InboundFeeDetailEntity requiredManual(long tenantId, long inboundId, long detailId) {
|
|
InboundFeeDetailEntity value = required(tenantId, inboundId, detailId);
|
|
if ("FBA_PRICING".equals(value.getSourceType())) {
|
|
throw conflict("自动头程运费明细为只读派生,请在头程费用中调整或发布");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
private void apply(InboundFeeDetailEntity value, InboundOrderEntity inbound,
|
|
FeeItemEntity item, SaveRequest request, long operatorId) {
|
|
ensureWarehouseApplicable(item, request.direction());
|
|
value.setFeeItemId(item.getId());
|
|
value.setInboundCode(inbound.getInboundCode());
|
|
value.setDirection(request.direction());
|
|
applySupplierSnapshot(value, item, request.direction(), request.supplierId());
|
|
applyPaymentSnapshot(value, request.direction(), request.settlementMode(), request.paidAt(),
|
|
request.paymentMethod(), request.paymentReferenceNo(), request.paymentVoucherFileIds());
|
|
value.setBusinessDate(request.businessDate());
|
|
value.setFeeCategorySnapshot(item.getCategory());
|
|
value.setTradeScopeSnapshot(item.getTradeScope());
|
|
value.setFeeTypeNameSnapshot(item.getFeeTypeName());
|
|
value.setChannelCode(inbound.getChannelCode());
|
|
value.setChannelName(inbound.getChannelName());
|
|
value.setDestinationCode(inbound.getDestinationCode());
|
|
value.setDestinationName(inbound.getDestinationName());
|
|
value.setPackageCount(inbound.getTotalPackageCount());
|
|
// 财务历史字段名保留,但计费事实统一使用扣除托盘皮重后的货物净重。
|
|
value.setActualGrossWeightKg(inbound.getActualNetWeightKg());
|
|
value.setActualVolumeCbm(inbound.getActualVolumeCbm());
|
|
value.setVolumeWeightKg(inbound.getVolumeWeightKg());
|
|
value.setChargeableWeightKg(inbound.getChargeableWeightKg());
|
|
value.setChargeableVolumeCbm(inbound.getChargeableVolumeCbm());
|
|
value.setUnitPrice(request.unitPrice().setScale(4, RoundingMode.HALF_UP));
|
|
value.setCurrency(request.currency());
|
|
value.setAmount(request.amount().setScale(2, RoundingMode.HALF_UP));
|
|
value.setRemark(request.remark() == null ? null : request.remark().trim());
|
|
value.setUpdatedBy(operatorId);
|
|
}
|
|
|
|
private void applySupplierSnapshot(InboundFeeDetailEntity value, FeeItemEntity item,
|
|
String direction, Long supplierId) {
|
|
if (!"PAYABLE".equals(direction)) {
|
|
value.setSupplierId(null);
|
|
value.setSupplierName(null);
|
|
value.setSupplierCategory(null);
|
|
return;
|
|
}
|
|
if (supplierId == null || supplierId <= 0) {
|
|
throw new BizException(ResultCode.VALIDATION_FAILED, "应付入库费用必须选择有效供应商");
|
|
}
|
|
String category = FeeItemSupplierCategory.normalize(item.getSupplierCategory());
|
|
var snapshot = suppliers.fee(supplierId, category);
|
|
value.setSupplierId(snapshot.supplierId());
|
|
value.setSupplierName(snapshot.name());
|
|
value.setSupplierCategory(category);
|
|
}
|
|
|
|
private void refreshSupplierSnapshot(InboundFeeDetailEntity value, FeeItemEntity item,
|
|
long operatorId) {
|
|
applySupplierSnapshot(value, item, value.getDirection(), value.getSupplierId());
|
|
value.setUpdatedBy(operatorId);
|
|
if (repository.updateDetail(value) != 1) {
|
|
throw conflict("费用明细供应商快照已变化,请刷新后重试");
|
|
}
|
|
}
|
|
|
|
private void revalidatePaymentSnapshot(InboundFeeDetailEntity value) {
|
|
applyPaymentSnapshot(value, value.getDirection(), value.getSettlementMode(), value.getPaidAt(),
|
|
value.getPaymentMethod(), value.getPaymentReferenceNo(), value.getPaymentVoucherFileIds());
|
|
}
|
|
|
|
private void applyPaymentSnapshot(InboundFeeDetailEntity value, String direction,
|
|
String settlementMode, LocalDateTime paidAt,
|
|
String paymentMethod, String paymentReferenceNo,
|
|
List<Long> paymentVoucherFileIds) {
|
|
var snapshot = paymentSnapshots.validate(value.getTenantId(), direction, settlementMode,
|
|
paidAt, paymentMethod, paymentReferenceNo, paymentVoucherFileIds);
|
|
value.setSettlementMode(snapshot.settlementMode());
|
|
value.setPaidAt(snapshot.paidAt());
|
|
value.setPaymentMethod(snapshot.paymentMethod());
|
|
value.setPaymentReferenceNo(snapshot.paymentReferenceNo());
|
|
value.setPaymentVoucherFileIds(snapshot.paymentVoucherFileIds().isEmpty()
|
|
? null : snapshot.paymentVoucherFileIds());
|
|
}
|
|
|
|
private static BizException conflict(String message) {
|
|
return new BizException(ResultCode.CONFLICT, message);
|
|
}
|
|
|
|
private void publishFinanceSource(InboundFeeDetailEntity detail, String status,
|
|
LocalDateTime occurredAt) {
|
|
if ("FBA_PRICING".equals(detail.getSourceType())) return;
|
|
int version = detail.getSourceFeeVersion() == null ? 1 : detail.getSourceFeeVersion();
|
|
String eventType = "VOIDED".equals(status) ? FinanceFeeSourceOutboxService.VOIDED
|
|
: version > 1 ? FinanceFeeSourceOutboxService.ADJUSTED
|
|
: FinanceFeeSourceOutboxService.PUBLISHED;
|
|
long sourceId = detail.getSourceFeeId() == null ? detail.getId() : detail.getSourceFeeId();
|
|
financeOutbox.publishInbound(eventType, detail.getTenantId(), detail.getInboundId(),
|
|
sourceId, version, occurredAt);
|
|
}
|
|
}
|