diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationLine.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationLine.java index 60f04c39..ae4a57b1 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationLine.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationLine.java @@ -76,7 +76,7 @@ public class InvoiceApplicationLine extends TenantEntity { */ private BigDecimal quantity; /** - * 不含税单价 + * 不含税单价(由合计金额反算,保留 6 位小数) */ private BigDecimal unitPriceNoTax; /** diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java index a2ecf024..36b7f0d5 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillVO.java @@ -87,4 +87,8 @@ public class WaybillVO extends Waybill { @Schema(description = "运单主键集合") private List ids; + @TableField(exist = false) + @Schema(description = "承运商合同名称") + private String carrierContractName; + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java index ab1bed91..320c8b22 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/LoadingManageController.java @@ -56,9 +56,11 @@ public class LoadingManageController extends BladeController { @GetMapping("/carrier-contracts") @ApiOperationSupport(order = 13) - @Operation(summary = "可选承运商合同", description = "查询已审核生效的承运商合同") - public R> carrierContracts(@RequestParam List projectIds) { - return R.data(loadingManageService.carrierContracts(projectIds)); + @Operation(summary = "可选承运商合同", description = "查询已审核生效的承运商合同;传入 waybillIds 时按子运单客户合同甲方匹配后对乙方取交集") + public R> carrierContracts( + @RequestParam List projectIds, + @RequestParam(required = false) List waybillIds) { + return R.data(loadingManageService.carrierContracts(projectIds, waybillIds)); } @GetMapping("/list") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractManageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractManageService.java index e43ebcc7..33806995 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractManageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractManageService.java @@ -72,4 +72,19 @@ public interface IContractManageService extends BaseService { */ String nextContractNo(Long organizationId); + /** + * 合同是否可用于生成结算单:已审批、未终止,且临时合同仍在临时效力期内。 + * + * @param contract 合同 + * @return true=可用 + */ + boolean isUsableForSettlement(ContractManage contract); + + /** + * 校验合同可用于生成结算单,不满足时抛出业务异常。 + * + * @param contract 合同 + */ + void assertUsableForSettlement(ContractManage contract); + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java index 90562fec..f0998c45 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ILoadingManageService.java @@ -26,7 +26,12 @@ public interface ILoadingManageService extends BaseService { LoadingManageVO detail(Long id); - List carrierContracts(List projectIds); + /** + * 查询项目下已审核生效的承运商合同。 + *

+ * 传入 waybillIds 时,按各子运单客户合同甲方匹配可用承运商合同,再对乙方名称取交集后返回。 + */ + List carrierContracts(List projectIds, List waybillIds); boolean saveDraft(LoadingManage loadingManage); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java index aab31d8b..f1a4a920 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java @@ -592,7 +592,8 @@ public class ContractManageServiceImpl extends BaseServiceImpllambdaQuery() - .eq(ContractManage::getIsDeleted, 0) .eq(ContractManage::getContractNo, normalizedContractNo) .ne(Func.isNotEmpty(contractManage.getId()), ContractManage::getId, contractManage.getId())); if (duplicatedCount != null && duplicatedCount > 0) { @@ -613,6 +618,47 @@ public class ContractManageServiceImpl extends BaseServiceImpl 0) { + throw new ServiceException("税率必须在0-100之间"); + } + // 合计金额 = 数量 × 不含税单价 × (1 + 税率) + // 不含税单价 = 合计金额 / 数量 / (1 + 税率),保留 6 位小数 + BigDecimal rateFactor = BigDecimal.ONE.add( + taxRate.divide(BigDecimal.valueOf(100), 8, RoundingMode.HALF_UP)); + BigDecimal unitPriceNoTax = totalAmount + .divide(quantity, 10, RoundingMode.HALF_UP) + .divide(rateFactor, 6, RoundingMode.HALF_UP); BigDecimal amountNoTax = quantity.multiply(unitPriceNoTax) .setScale(2, RoundingMode.HALF_UP); - BigDecimal taxRate = nonNegative(line.getTaxRate(), "税率"); - if (taxRate.compareTo(BigDecimal.valueOf(100)) > 0) throw new ServiceException("税率必须在0-100之间"); - BigDecimal taxAmount = calculateTax(amountNoTax, taxRate); - BigDecimal totalAmount = amountNoTax.add(taxAmount).setScale(2, RoundingMode.HALF_UP); + BigDecimal taxAmount = totalAmount.subtract(amountNoTax) + .setScale(2, RoundingMode.HALF_UP); + line.setUnitPriceNoTax(unitPriceNoTax); line.setAmountNoTax(amountNoTax); line.setTaxAmount(taxAmount); line.setTotalAmount(totalAmount); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java index 268ddb9c..756f3c9c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java @@ -107,11 +107,22 @@ public class LoadingManageServiceImpl extends BaseServiceImpl carrierContracts(List projectIds) { + public List carrierContracts(List projectIds, List waybillIds) { if (Func.isEmpty(projectIds)) { return List.of(); } - return availableCarrierContracts(projectIds).stream().map(contract -> { + List contracts = availableCarrierContracts(projectIds); + if (Func.isNotEmpty(waybillIds)) { + List waybills = loadWaybills(waybillIds); + Set allowedPartyBs = intersectCarrierPartyBs(waybills, contracts); + if (allowedPartyBs.isEmpty()) { + return List.of(); + } + contracts = contracts.stream() + .filter(contract -> allowedPartyBs.contains(normalizePartyName(contract.getPartyB()))) + .toList(); + } + return contracts.stream().map(contract -> { LoadingCarrierContractVO option = new LoadingCarrierContractVO(); option.setId(contract.getId()); option.setContractName(contract.getContractName()); @@ -862,6 +873,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl waybills) { + if (Func.isEmpty(waybills) || waybills.size() < 2) { + return; + } + List contracts = availableCarrierContracts(projectIdsOf(waybills)); + Set allowedPartyBs = intersectCarrierPartyBs(waybills, contracts); + if (allowedPartyBs.isEmpty()) { + throw new ServiceException("所选运单在同一项目下没有共同的承运商合同乙方,不能配载到同一配载单"); + } + } + private void validateCarrierContract(LoadingManage loadingManage, boolean required) { validateCarrierContract(loadingManage, required, null); } @@ -931,6 +957,18 @@ public class LoadingManageServiceImpl extends BaseServiceImpl projectContracts = availableCarrierContracts(projectIdsOf(childWaybills)); + Set allowedPartyBs = intersectCarrierPartyBs(childWaybills, projectContracts); + String partyB = normalizePartyName(carrierContract.getPartyB()); + if (Func.isNotEmpty(allowedPartyBs) && !allowedPartyBs.contains(partyB)) { + throw new ServiceException("所选承运商合同乙方与子运单可共同承运的乙方不一致,可选:" + + String.join("、", allowedPartyBs)); + } + if (allowedPartyBs.isEmpty()) { + throw new ServiceException("所选运单在同一项目下没有共同的承运商合同乙方,不能配载到同一配载单"); + } + } loadingManage.setCarrierName(carrierContract.getPartyB()); } @@ -969,6 +1007,65 @@ public class LoadingManageServiceImpl extends BaseServiceImpl intersectCarrierPartyBs(List waybills, List projectContracts) { + Set intersection = null; + Map customerPartyAMap = loadCustomerContractPartyAMap(waybills); + for (Waybill waybill : waybills) { + Set available = availableCarrierPartyBs(waybill, projectContracts, customerPartyAMap); + if (intersection == null) { + intersection = new LinkedHashSet<>(available); + } else { + intersection.retainAll(available); + } + if (intersection.isEmpty()) { + return Set.of(); + } + } + return intersection == null ? Set.of() : intersection; + } + + private Set availableCarrierPartyBs(Waybill waybill, List projectContracts, + Map customerPartyAMap) { + if (waybill == null || Func.isEmpty(projectContracts)) { + return Set.of(); + } + String partyA = customerPartyAMap.get(waybill.getContractId()); + return projectContracts.stream() + .filter(contract -> Objects.equals(contract.getProjectId(), waybill.getProjectId())) + .filter(contract -> Func.isEmpty(partyA) || Objects.equals(normalizePartyName(contract.getPartyA()), partyA)) + .map(ContractManage::getPartyB) + .map(this::normalizePartyName) + .filter(Func::isNotEmpty) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + private Map loadCustomerContractPartyAMap(List waybills) { + List contractIds = waybills.stream() + .map(Waybill::getContractId) + .filter(Objects::nonNull) + .distinct() + .toList(); + if (Func.isEmpty(contractIds)) { + return Map.of(); + } + return contractManageService.list(Wrappers.lambdaQuery() + .eq(ContractManage::getIsDeleted, 0) + .in(ContractManage::getId, contractIds)) + .stream() + .collect(Collectors.toMap(ContractManage::getId, + contract -> normalizePartyName(contract.getPartyA()), + (left, right) -> left, + LinkedHashMap::new)); + } + + private String normalizePartyName(String value) { + return value == null ? "" : value.trim(); + } + /** * 按主键取一张可用的承运商合同。 *

diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java index e29b1aeb..f9bdc3a9 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java @@ -1318,14 +1318,15 @@ public class PreSettlementServiceImpl extends BaseServiceImpl settlementContractIds() { return contractManageService.list(Wrappers.lambdaQuery() - .select(ContractManage::getId) + .select(ContractManage::getId, ContractManage::getContractStage, ContractManage::getTemporaryEndDate, + ContractManage::getApprovalStatus, ContractManage::getIsDeleted) .eq(ContractManage::getIsDeleted, 0) .in(ContractManage::getApprovalStatus, "approved", "change_approved") .ne(ContractManage::getContractStage, "terminated")) - .stream().map(ContractManage::getId).toList(); + .stream() + .filter(contractManageService::isUsableForSettlement) + .map(ContractManage::getId) + .toList(); } private void validateSettlementContract(Long contractId) { ContractManage contract = contractId == null ? null : contractManageService.getById(contractId); - if (contract == null || Objects.equals(contract.getIsDeleted(), 1) - || !List.of("approved", "change_approved").contains(contract.getApprovalStatus()) - || "terminated".equals(contract.getContractStage())) { - throw new ServiceException("合同未审核完成,不可转预结算单或正式结算单"); - } + contractManageService.assertUsableForSettlement(contract); } @Override @@ -935,9 +935,7 @@ public class ReceivablePayableDetailServiceImpl return List.of(); } ContractManage contract = contractManageService.getById(contractId); - if (contract == null || Objects.equals(contract.getIsDeleted(), 1)) { - throw new ServiceException("合同不存在"); - } + contractManageService.assertUsableForSettlement(contract); String targetSettlementType = settlementType(settlementType); Long currentUserId = AuthUtil.getUserId(); Date generateTime = new Date(); @@ -3253,8 +3251,10 @@ public class ReceivablePayableDetailServiceImpl if (Func.isEmpty(request.getBillingPlanId())) { throw new ServiceException("请选择计费方案"); } + ContractManage contract = contractManageService.getById(request.getContractId()); + contractManageService.assertUsableForSettlement(contract); String targetSettlementType = settlementType(request.getSettlementType()); - validateContractSettlementType(contractManageService.getById(request.getContractId()), targetSettlementType); + validateContractSettlementType(contract, targetSettlementType); if (requireWaybill && Func.isEmpty(request.getWaybillIds())) { throw new ServiceException("请选择需要生成费用的运单"); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementImportBatchServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementImportBatchServiceImpl.java index 6ec5bcd0..83e1c859 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementImportBatchServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementImportBatchServiceImpl.java @@ -1079,6 +1079,9 @@ public class SettlementImportBatchServiceImpl || "terminated".equals(contract.getContractStage())) { throw new ServiceException(label + "未审核完成或已终止"); } + if (!contractManageService.isUsableForSettlement(contract)) { + throw new ServiceException(label + "为临时合同且已过期,不可生成结算单"); + } return contract; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementRuleAutoGenerateServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementRuleAutoGenerateServiceImpl.java index 67818e9e..ca3f5884 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementRuleAutoGenerateServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementRuleAutoGenerateServiceImpl.java @@ -128,6 +128,10 @@ public class SettlementRuleAutoGenerateServiceImpl implements ISettlementRuleAut } private int[] generateContract(ContractManage contract, LocalDate today) { + if (!contractManageService.isUsableForSettlement(contract)) { + log.warn("合同[{}]不可用于结算(未审批、已终止或临时合同已过期),跳过自动生成", contract.getContractNo()); + return new int[]{0, 0}; + } String settlementType = contractSettlementType(contract); if (settlementType == null || contract.getProjectId() == null || contract.getOrganizationId() == null) { log.warn("合同[{}]缺少结算类型、项目或所属组织,跳过自动生成", contract.getContractNo()); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index 98aff41c..d2eb2e7e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -141,6 +141,7 @@ public class WaybillServiceImpl extends BaseServiceImpl entityPage.getRecords().forEach(this::syncDriverAcceptState); IPage result = WaybillWrapper.build().pageVO(entityPage); fillMileageMaintainable(result.getRecords()); + fillCarrierContractName(result.getRecords()); return result; } @@ -148,6 +149,7 @@ public class WaybillServiceImpl extends BaseServiceImpl public WaybillVO detail(Long id) { WaybillVO result = WaybillWrapper.build().entityVO(syncDriverAcceptState(loadEditable(id, false))); fillCustomerNameFromContract(result); + fillCarrierContractName(List.of(result)); fillMileageMaintainable(List.of(result)); return result; } @@ -1206,6 +1208,29 @@ public class WaybillServiceImpl extends BaseServiceImpl } } + private void fillCarrierContractName(List waybills) { + if (Func.isEmpty(waybills)) { + return; + } + List contractIds = waybills.stream() + .map(Waybill::getCarrierContractId) + .filter(Objects::nonNull) + .distinct() + .toList(); + if (Func.isEmpty(contractIds)) { + return; + } + Map nameMap = contractManageService.listByIds(contractIds).stream() + .filter(contract -> contract != null && Objects.equals(contract.getIsDeleted(), 0)) + .collect(Collectors.toMap(ContractManage::getId, ContractManage::getContractName, (left, right) -> left)); + waybills.forEach(waybill -> { + if (waybill.getCarrierContractId() == null) { + return; + } + waybill.setCarrierContractName(nameMap.get(waybill.getCarrierContractId())); + }); + } + private boolean isCustomerContract(ContractManage contract) { return contract != null && "客户合同".equals(contract.getContractCategory()); } diff --git a/doc/sql/transport/blade_invoice_application_line_unit_price_scale_20261026.sql b/doc/sql/transport/blade_invoice_application_line_unit_price_scale_20261026.sql new file mode 100644 index 00000000..e6637ce4 --- /dev/null +++ b/doc/sql/transport/blade_invoice_application_line_unit_price_scale_20261026.sql @@ -0,0 +1,17 @@ +-- 开票申请明细:不含税单价按合计金额反算后保留 6 位小数 +SET @schema_name = DATABASE(); +SET @has_unit_price_column = ( + SELECT COUNT(1) + FROM information_schema.columns + WHERE table_schema = @schema_name + AND table_name = 'blade_invoice_application_line' + AND column_name = 'unit_price_no_tax' +); +SET @alter_unit_price_sql = IF( + @has_unit_price_column > 0, + 'ALTER TABLE `blade_invoice_application_line` MODIFY COLUMN `unit_price_no_tax` decimal(18,6) DEFAULT NULL COMMENT ''不含税单价''', + 'SELECT 1' +); +PREPARE stmt FROM @alter_unit_price_sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt;