Просмотр исходного кода

新增
1、增加供养类型的长者批量缴费账单的功能。
BUGFIX
1、解决房间内只有该长者转包房校验不通过的问题。
2、解决续约后能够重复续约的问题。
3、服务工单增加图片字段。
4、重构账单生成逻辑,统一以查询时的项目和金额为准,避免生成时不再重复计算,导致金额不正确。

liangwenxuan 2 месяцев назад
Родитель
Сommit
a6410712fd
14 измененных файлов с 233 добавлено и 210 удалено
  1. 4 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/BedChangeRecordController.java
  2. 8 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/ExpenseOrderController.java
  3. 3 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/ExpenseSaveReqVO.java
  4. 6 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/elderlyorder/ElderlyServiceOrderSaveReqVO.java
  5. 4 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/biz/ElderlyContractDO.java
  6. 5 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/biz/ElderlyServiceOrderDO.java
  7. 1 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyChangeRecordService.java
  8. 32 8
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyTempOutServiceImpl.java
  9. 2 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ExpenseOrderService.java
  10. 127 190
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ExpenseOrderServiceImpl.java
  11. 8 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ExpenseServiceImpl.java
  12. 31 5
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/NurseChangeRecordServiceImpl.java
  13. 0 1
      yudao-server/src/main/resources/application-test.yaml
  14. 2 2
      yudao-server/src/main/resources/application.yaml

+ 4 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/BedChangeRecordController.java

@@ -56,9 +56,11 @@ public class BedChangeRecordController {
     @GetMapping("/checkPrivateRoom")
     @Operation(summary = "校验是否可以包房")
     @Parameter(name = "bedId", description = "床位id", required = true, example = "1024")
+    @Parameter(name = "elderId", description = "申请该床位包房的长者id", required = true, example = "1024")
     //@PreAuthorize("@ss.hasPermission('elderly:bed-change-record:query')")
-    public CommonResult<Boolean> checkPrivateRoom(@RequestParam("bedId") Long bedId) {
-        Boolean result = changeRecordService.checkPrivateRoom(bedId);
+    public CommonResult<Boolean> checkPrivateRoom(@RequestParam("bedId") Long bedId,
+                                                  @RequestParam("elderId") Long elderId) {
+        Boolean result = changeRecordService.checkPrivateRoom(bedId, elderId);
         return success(result);
     }
 

+ 8 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/ExpenseOrderController.java

@@ -283,6 +283,14 @@ public class ExpenseOrderController {
         return success(true);
     }
 
+    @PostMapping("/markSupportElderlyBillsPaid")
+    @Operation(summary = "按账单月和机构批量将供养长者账单标记为已缴费")
+    @TenantIgnore
+    public CommonResult<Boolean> markSupportElderlyBillsPaid(@Valid @RequestBody MarkSupportElderlyBillsPaidReqVO reqVO) {
+        expenseOrderService.markSupportElderlyBillsPaid(reqVO);
+        return success(true);
+    }
+
     @PutMapping("/confirmStatus")
     @Operation(summary = "账单状态确认")
     @Parameter(name = "orderId", description = "账单编号", required = true)

+ 3 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/ExpenseSaveReqVO.java

@@ -65,4 +65,7 @@ public class ExpenseSaveReqVO {
 
     private Long tenantId;
 
+    @Schema(description = "旧合同id")
+    private Long oldContractId;
+
 }

+ 6 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/elderlyorder/ElderlyServiceOrderSaveReqVO.java

@@ -71,5 +71,11 @@ public class ElderlyServiceOrderSaveReqVO {
 
     @Schema(description = "租户id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     private Long tenantId;
+
+    /**
+     * 图片地址
+     */
+    @Schema(description = "图片地址")
+    private String image;
 }
 

+ 4 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/biz/ElderlyContractDO.java

@@ -72,6 +72,10 @@ public class ElderlyContractDO implements Serializable {
     private Integer orgType;
 
     private Long tenantId;
+    /**
+     * 是否续约,0:未续约,1:已续约
+     */
+    private Integer isRenew;
 
     @TableField(exist = false)
     private List<ElderlyContractDetailDO> details;

+ 5 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/biz/ElderlyServiceOrderDO.java

@@ -88,6 +88,11 @@ public class ElderlyServiceOrderDO {
     private String orderStatus;
 
     /**
+     * 图片地址
+     */
+    private String image;
+
+    /**
      * 租户ID
      */
     private Long tenantId;

+ 1 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyChangeRecordService.java

@@ -57,7 +57,7 @@ public interface ElderlyChangeRecordService {
 
     PageResult<BedChangeElderlyRespVO> findElderlyPage(ElderlyInfoPageReqVO pageVO);
 
-    Boolean checkPrivateRoom(Long bedId);
+    Boolean checkPrivateRoom(Long bedId, Long elderId);
 
     List<NurseChangeyEvaluationRespVO> getEvaluationByElderId(Long elderId);
 

+ 32 - 8
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyTempOutServiceImpl.java

@@ -17,6 +17,7 @@ import cn.iocoder.yudao.module.system.dal.dataobject.biz.ElderlyInfoDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.biz.ExpenseItemDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.biz.ExpenseOrderDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.biz.ExpenseOrderItemDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.biz.ElderlyContractDO;
 import cn.iocoder.yudao.module.system.dal.mysql.biz.*;
 import cn.iocoder.yudao.module.system.enums.change.BusinessConstants;
 import cn.iocoder.yudao.module.system.enums.common.BooleanEnum;
@@ -74,6 +75,9 @@ public class ElderlyTempOutServiceImpl implements ElderlyTempOutService {
     @Autowired
     private ExpenseOrderService expenseOrderService;
 
+    @Autowired
+    private ElderlyContractMapper elderlyContractMapper;
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void create(ElderlyTempOutCreateReqVO createReq) {
@@ -242,11 +246,31 @@ public class ElderlyTempOutServiceImpl implements ElderlyTempOutService {
             return;
         }
 
-        // 账单不存在:新建月度账单,并将固定费用(按返院日至月末折算)加入账单
+        // 合同到期月:返院补费区间终点取“月末 与 合同到期日”较早者;到期后月份不再补固定费
+        LocalDate actualEndDate = monthEnd;
+        ElderlyContractDO latestContract = elderlyContractMapper.selectOne(new LambdaQueryWrapperX<ElderlyContractDO>()
+                .eq(ElderlyContractDO::getElderId, elderId)
+                .orderByDesc(ElderlyContractDO::getCreatedTime)
+                .last("LIMIT 1"));
+        if (latestContract != null && latestContract.getExpireTime() != null) {
+            LocalDate contractExpireDate = latestContract.getExpireTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+            if (contractExpireDate.isBefore(monthStart)) {
+                return;
+            }
+            if (!contractExpireDate.isAfter(monthEnd)) {
+                actualEndDate = contractExpireDate;
+            }
+        }
+
+        if (actualEndDate.isBefore(comeDate)) {
+            return;
+        }
+
+        // 账单不存在:新建月度账单,并将固定费用(按返院日至实际截止日折算)加入账单
         if (monthOrder == null) {
-            int addDays = monthEnd.getDayOfMonth() - comeDate.getDayOfMonth() + 1;
+            int addDays = (int) (actualEndDate.toEpochDay() - comeDate.toEpochDay() + 1);
             int daysInMonth = billYm.lengthOfMonth();
-
+            final LocalDate finalActualEndDate = actualEndDate;
             List<ExpenseItemRespVO> items = fixedItems.stream().map(item -> {
                 ExpenseItemRespVO respVO = new ExpenseItemRespVO();
                 respVO.setExpenseType(0);
@@ -261,7 +285,7 @@ public class ElderlyTempOutServiceImpl implements ElderlyTempOutService {
                     addAmount = dayPrice.multiply(BigDecimal.valueOf(addDays)).setScale(2, RoundingMode.HALF_UP);
                 }
 
-                // 返院当月:按“返院日~月末(含当天)”折算总金额;price 按日单价回传,便于与账单已存在分支一致
+                // 返院当月:按“返院日~实际截止日(含当天)”折算总金额;price 按日单价回传,便于与账单已存在分支一致
                 respVO.setPrice(dayPrice.setScale(2, RoundingMode.HALF_UP));
                 respVO.setTotalAmount(addAmount);
 
@@ -269,7 +293,7 @@ public class ElderlyTempOutServiceImpl implements ElderlyTempOutService {
                 respVO.setItemCategory(item.getItemCategoryName());
                 respVO.setCount(addDays);
                 respVO.setStartTime(comeDate.toString());
-                respVO.setEndTime(monthEnd.toString());
+                respVO.setEndTime(finalActualEndDate.toString());
                 respVO.setType(item.getType());
                 return respVO;
             }).collect(Collectors.toList());
@@ -288,7 +312,7 @@ public class ElderlyTempOutServiceImpl implements ElderlyTempOutService {
             return;
         }
 
-        int addDays = monthEnd.getDayOfMonth() - comeDate.getDayOfMonth() + 1;
+        int addDays = (int) (actualEndDate.toEpochDay() - comeDate.toEpochDay() + 1);
         int daysInMonth = billYm.lengthOfMonth();
 
         for (ExpenseItemDO item : fixedItems) {
@@ -326,8 +350,8 @@ public class ElderlyTempOutServiceImpl implements ElderlyTempOutService {
             orderItem.setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname());
             orderItem.setTenantId(tenantId);
             orderItem.setStartDate(comeDate);
-            orderItem.setEndDate(monthEnd);
-            orderItem.setDescription("返院恢复固定费:" + comeDate + "至" + monthEnd + "共" + addDays + "天," + item.getItemName());
+            orderItem.setEndDate(actualEndDate);
+            orderItem.setDescription("返院恢复固定费:" + comeDate + "至" + actualEndDate + "共" + addDays + "天," + item.getItemName());
             orderItem.setType(item.getType());
             orderItem.setItemId(item.getItemId());
             // 标注月度费用(该字段为非持久化扩展字段,用于返回前端展示)

+ 2 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ExpenseOrderService.java

@@ -71,6 +71,8 @@ public interface ExpenseOrderService {
 
     void markCheckinBillsPaid(MarkCheckinBillsPaidReqVO reqVO);
 
+    void markSupportElderlyBillsPaid(MarkSupportElderlyBillsPaidReqVO reqVO);
+
     void updateConfirmStatus(Long orderId, boolean b);
 
     void sendSmsBillOrder(List<Long> orderIds);

+ 127 - 190
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ExpenseOrderServiceImpl.java

@@ -1009,171 +1009,54 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
             expenseOrder.setOrgType(elderlyInfo.getOrgType());
             expenseOrder.setTenantId(saveVO.getTenantId());
             expenseOrderMapper.insert(expenseOrder);
+            //生成账单费用项
+            BigDecimal totalAmount = BigDecimal.ZERO;
+            List<ExpenseItemRespVO> items = saveVO.getItems();
+            for (ExpenseItemRespVO item : items) {
+                ExpenseOrderItemDO orderItem = new ExpenseOrderItemDO();
+                orderItem.setExpenseOrderId(expenseOrder.getId());
+                orderItem.setSourceExpenseItemId(item.getSourceExpenseItemId());
+                orderItem.setExpenseSource(item.getExpenseSource());
+                orderItem.setCount(item.getCount());
+                orderItem.setItemName(item.getItemName());
+                orderItem.setDescription(StringUtils.isBlank(item.getDescription()) ?
+                        item.getStartTime() + "至" + item.getEndTime() + "的" + item.getItemName() :
+                        item.getDescription());
+                orderItem.setPrice(item.getPrice());  //单价
+                orderItem.setActualPrice(item.getPrice()); //实际单价
+                orderItem.setTotalAmount(item.getTotalAmount());  //总金额
+                orderItem.setRoundAmount(orderItem.getTotalAmount().setScale(0, RoundingMode.HALF_UP));
+                orderItem.setRoundTwoDecimalAmount(orderItem.getTotalAmount().setScale(2, RoundingMode.HALF_UP));
+                orderItem.setCreatedTime(new Date());
+                orderItem.setType(item.getType());
+                orderItem.setItemCategoryName(item.getItemCategory());
+                orderItem.setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname());
+                orderItem.setTenantId(saveVO.getTenantId());
+                orderItem.setStartDate(item.getStartTime() == null ? null : LocalDate.parse(item.getStartTime()));
+                orderItem.setEndDate(item.getEndTime() == null ? null : LocalDate.parse(item.getEndTime()));
+                orderItem.setContractId(item.getContractId());
+                expenseOrderItemMapper.insert(orderItem);
+                if (item.getExpenseSource().equals(BusinessConstants.DISCOUNT_DEDUCTION)) {
+                    addDiscountRecord(saveVO.getElderId(), saveVO.getBillingMonth(), item);
+                }
+                //判断是否生成过账单(用于标记那些生成账单后再生成的费用项)
+                DailyExpensesDO dailyExpenses = dailyExpensesMapper.selectById(item.getSourceExpenseItemId());
+                if (dailyExpenses != null) {
+                    dailyExpenses.setIsGenerateBill(BooleanEnum.TRUE.getValue());
+                    dailyExpenses.setExpenseOrderNumber(expenseOrder.getBillOrderNumber());
+                    dailyExpensesMapper.updateById(dailyExpenses);
+                }
 
-            // 获取进行中的合同
-            List<ElderlyContractDO> elderlyContractList = elderlyContractMapper.selectList(new LambdaQueryWrapperX<ElderlyContractDO>()
-                    .eq(ElderlyContractDO::getElderId, saveVO.getElderId())
-                    .in(ElderlyContractDO::getStatus, 1,2)
-                    .orderByDesc(ElderlyContractDO::getCreatedTime));
-
-            if (CollectionUtil.isEmpty(elderlyContractList)) {
-                return; // 没有合同则跳过
-            }
-
-            ElderlyContractDO elderlyContractDO = elderlyContractList.get(0); // 获取最新的合同
-            Date expireTime = elderlyContractDO.getExpireTime();
-
-            if (expireTime == null) {
-                return; // 没有设置过期时间则跳过
-            }
-
-            // 将账单月份和合同过期时间转换为 YearMonth 进行比较
-            YearMonth billYearMonth = YearMonth.parse(saveVO.getBillingMonth(), DateTimeFormatter.ofPattern("yyyy-MM"));
-            YearMonth expireYearMonth = YearMonth.from(expireTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
-
-            // 如果合同在当前账单月之前就已经过期,则跳过生成账单
-            if (expireYearMonth.isBefore(billYearMonth)) {
-                return;
-            }
-
-
-            if (!elderlyContractList.isEmpty()) {
-                if (expireTime != null) {
-                    // 判断合同是否在账单月份过期
-//                    String billingMonth = saveVO.getBillingMonth();
-//                    YearMonth billYearMonth = YearMonth.parse(billingMonth, DateTimeFormatter.ofPattern("yyyy-MM"));
-//                    YearMonth expireYearMonth = YearMonth.from(expireTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
-
-                    if (billYearMonth.equals(expireYearMonth)) {
-                        LocalDate expireDate = expireTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
-                        int dayOfMonth = expireDate.getDayOfMonth(); // 过期日期是当月第几天
-                        int lengthOfMonth = expireDate.lengthOfMonth(); // 当月总天数
-
-                        // 遍历所有账单明细,调整金额
-                        // 如果合同在账单月份过期
-                        BigDecimal newTotalAmount = BigDecimal.ZERO;
-                        List<ExpenseItemRespVO> items = saveVO.getItems();
-                        for (ExpenseItemRespVO item : items) {
-                            ExpenseOrderItemDO orderItem = new ExpenseOrderItemDO();
-                            orderItem.setExpenseOrderId(expenseOrder.getId());
-                            orderItem.setSourceExpenseItemId(item.getSourceExpenseItemId() == null ? null : item.getSourceExpenseItemId());
-                            orderItem.setExpenseSource(item.getExpenseSource());
-                            orderItem.setCount(item.getCount());
-                            orderItem.setItemName(item.getItemName());
-                            orderItem.setDescription(StringUtils.isBlank(item.getDescription()) ?
-                                    item.getStartTime() + "至" + item.getEndTime() + "的" + item.getItemName() :
-                                    item.getDescription());
-                            orderItem.setContractId(item.getContractId());
-                            if (item.getExpenseSource().equals(BusinessConstants.EXPENSE_ITEM)) {
-                                //计算单价
-                                orderItem.setPrice(item.getPrice());  //单价
-                                orderItem.setActualPrice(item.getPrice()); //实际单价
-                                BigDecimal itemTotalAmount = item.getTotalAmount();
-                                BigDecimal dayPrice = itemTotalAmount.divide(BigDecimal.valueOf(lengthOfMonth), 8, RoundingMode.DOWN);
-                                BigDecimal actualPrice = dayPrice.multiply(BigDecimal.valueOf(dayOfMonth)).setScale(8, RoundingMode.DOWN);
-                                orderItem.setTotalAmount(actualPrice);  //总金额
-                                orderItem.setRoundAmount(orderItem.getTotalAmount().setScale(0, RoundingMode.HALF_UP));
-                                orderItem.setRoundTwoDecimalAmount(orderItem.getTotalAmount().setScale(2, RoundingMode.HALF_UP));
-                            } else {
-                                orderItem.setPrice(item.getPrice());  //单价
-                                orderItem.setActualPrice(item.getPrice()); //实际单价
-                                orderItem.setTotalAmount(item.getTotalAmount());  //总金额
-                                orderItem.setRoundAmount(orderItem.getTotalAmount().setScale(0, RoundingMode.HALF_UP));
-                                orderItem.setRoundTwoDecimalAmount(orderItem.getTotalAmount().setScale(2, RoundingMode.HALF_UP));
-                                if (item.getExpenseSource().equals(BusinessConstants.DISCOUNT_DEDUCTION)) {
-                                    addDiscountRecord(saveVO.getElderId(), saveVO.getBillingMonth(), item);
-                                }
-                            }
-                            orderItem.setStartDate(item.getStartTime() == null ? null : LocalDate.parse(item.getStartTime()));
-                            orderItem.setEndDate(expireDate);
-                            orderItem.setCreatedTime(new Date());
-                            orderItem.setType(item.getType());
-                            orderItem.setItemCategoryName(item.getItemCategory());
-                            orderItem.setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname());
-                            orderItem.setTenantId(expenseOrder.getTenantId());
-                            expenseOrderItemMapper.insert(orderItem);
-
-                            //判断是否生成过账单(用于标记那些生成账单后再生成的费用项)
-                            DailyExpensesDO dailyExpenses = dailyExpensesMapper.selectById(item.getSourceExpenseItemId());
-                            if (dailyExpenses != null) {
-                                dailyExpenses.setIsGenerateBill(BooleanEnum.TRUE.getValue());
-                                dailyExpenses.setExpenseOrderNumber(expenseOrder.getBillOrderNumber());
-                                dailyExpensesMapper.updateById(dailyExpenses);
-                            }
-
-                            ExpenseSubsidyDO expenseSubsidyDO = expenseSubsidyMapper.selectById(item.getSourceExpenseItemId());
-                            if (expenseSubsidyDO != null) {
-                                expenseSubsidyDO.setStatus(BooleanEnum.TRUE.getValue());
-                                expenseSubsidyMapper.updateById(expenseSubsidyDO);
-                            }
-                            newTotalAmount = newTotalAmount.add(orderItem.getTotalAmount().setScale(2, RoundingMode.DOWN));
-                        }
-
-//                        List<ExpenseSubsidyDO> expenseSubsidList = expenseSubsidyMapper.selectList(new LambdaQueryWrapperX<ExpenseSubsidyDO>()
-//                                .eq(ExpenseSubsidyDO::getElderId, expenseOrder.getElderId())
-//                                .eq(ExpenseSubsidyDO::getDeductionBillMonth, expenseOrder.getBillingMonth()));
-
-//                        newTotalAmount = updateExpenseSubsidy(expenseSubsidList, expenseOrder, newTotalAmount);
-                        expenseOrder.setActualAmount(newTotalAmount);
-                        expenseOrderMapper.updateById(expenseOrder);
-                    } else {
-                        //生成账单费用项
-                        BigDecimal totalAmount = BigDecimal.ZERO;
-                        List<ExpenseItemRespVO> items = saveVO.getItems();
-                        for (ExpenseItemRespVO item : items) {
-                            ExpenseOrderItemDO orderItem = new ExpenseOrderItemDO();
-                            orderItem.setExpenseOrderId(expenseOrder.getId());
-                            orderItem.setSourceExpenseItemId(item.getSourceExpenseItemId());
-                            orderItem.setExpenseSource(item.getExpenseSource());
-                            orderItem.setCount(item.getCount());
-                            orderItem.setItemName(item.getItemName());
-                            orderItem.setDescription(StringUtils.isBlank(item.getDescription()) ?
-                                    item.getStartTime() + "至" + item.getEndTime() + "的" + item.getItemName() :
-                                    item.getDescription());
-                            orderItem.setPrice(item.getPrice());  //单价
-                            orderItem.setActualPrice(item.getPrice()); //实际单价
-                            orderItem.setTotalAmount(item.getTotalAmount());  //总金额
-                            orderItem.setRoundAmount(orderItem.getTotalAmount().setScale(0, RoundingMode.HALF_UP));
-                            orderItem.setRoundTwoDecimalAmount(orderItem.getTotalAmount().setScale(2, RoundingMode.HALF_UP));
-                            orderItem.setCreatedTime(new Date());
-                            orderItem.setType(item.getType());
-                            orderItem.setItemCategoryName(item.getItemCategory());
-                            orderItem.setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname());
-                            orderItem.setTenantId(saveVO.getTenantId());
-                            orderItem.setStartDate(item.getStartTime() == null ? null : LocalDate.parse(item.getStartTime()));
-                            orderItem.setEndDate(item.getEndTime() == null ? null : LocalDate.parse(item.getEndTime()));
-                            orderItem.setContractId(item.getContractId());
-                            expenseOrderItemMapper.insert(orderItem);
-                            if (item.getExpenseSource().equals(BusinessConstants.DISCOUNT_DEDUCTION)) {
-                                addDiscountRecord(saveVO.getElderId(), saveVO.getBillingMonth(), item);
-                            }
-                            //判断是否生成过账单(用于标记那些生成账单后再生成的费用项)
-                            DailyExpensesDO dailyExpenses = dailyExpensesMapper.selectById(item.getSourceExpenseItemId());
-                            if (dailyExpenses != null) {
-                                dailyExpenses.setIsGenerateBill(BooleanEnum.TRUE.getValue());
-                                dailyExpenses.setExpenseOrderNumber(expenseOrder.getBillOrderNumber());
-                                dailyExpensesMapper.updateById(dailyExpenses);
-                            }
-
-                            ExpenseSubsidyDO expenseSubsidyDO = expenseSubsidyMapper.selectById(item.getSourceExpenseItemId());
-                            if (expenseSubsidyDO != null) {
-                                expenseSubsidyDO.setStatus(BooleanEnum.TRUE.getValue());
-                                expenseSubsidyMapper.updateById(expenseSubsidyDO);
-                            }
-
-                            totalAmount = totalAmount.add(orderItem.getTotalAmount().setScale(2, RoundingMode.DOWN));
-                        }
-
-//                        List<ExpenseSubsidyDO> expenseSubsidList = expenseSubsidyMapper.selectList(new LambdaQueryWrapperX<ExpenseSubsidyDO>()
-//                                .eq(ExpenseSubsidyDO::getElderId, expenseOrder.getElderId())
-//                                .eq(ExpenseSubsidyDO::getDeductionBillMonth, expenseOrder.getBillingMonth()));
-//
-//                        totalAmount = updateExpenseSubsidy(expenseSubsidList, expenseOrder, totalAmount);
-                        expenseOrder.setActualAmount(totalAmount);
-                        expenseOrderMapper.updateById(expenseOrder);
-                    }
+                ExpenseSubsidyDO expenseSubsidyDO = expenseSubsidyMapper.selectById(item.getSourceExpenseItemId());
+                if (expenseSubsidyDO != null) {
+                    expenseSubsidyDO.setStatus(BooleanEnum.TRUE.getValue());
+                    expenseSubsidyMapper.updateById(expenseSubsidyDO);
                 }
+
+                totalAmount = totalAmount.add(orderItem.getTotalAmount().setScale(2, RoundingMode.DOWN));
             }
+            expenseOrder.setActualAmount(totalAmount);
+            expenseOrderMapper.updateById(expenseOrder);
         } else {
             BigDecimal amount = BigDecimal.ZERO;
             List<ExpenseItemRespVO> items = saveVO.getItems();
@@ -2994,18 +2877,50 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
             return;
         }
 
+        markBillsPaid(expenseOrders);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @TenantIgnore
+    public void markSupportElderlyBillsPaid(MarkSupportElderlyBillsPaidReqVO reqVO) {
+        // 1. 先查询该机构下供养长者(照护类型=1)
+        List<ElderlyInfoDO> supportElders = elderlyInfoMapper.selectList(new LambdaQueryWrapperX<ElderlyInfoDO>()
+                .eq(ElderlyInfoDO::getTenantId, reqVO.getTenantId())
+                .eq(ElderlyInfoDO::getCareType, 1));
+        if (CollectionUtil.isEmpty(supportElders)) {
+            return;
+        }
+
+        Set<Long> supportElderIds = supportElders.stream().map(ElderlyInfoDO::getId).collect(Collectors.toSet());
+
+        // 2. 查询账单月+机构下,未缴清的账单
+        List<ExpenseOrderDO> expenseOrders = expenseOrderMapper.selectList(new LambdaQueryWrapperX<ExpenseOrderDO>()
+                .eq(ExpenseOrderDO::getBillingMonth, reqVO.getBillingMonth())
+                .eq(ExpenseOrderDO::getTenantId, reqVO.getTenantId())
+                .in(ExpenseOrderDO::getElderId, supportElderIds)
+                .ne(ExpenseOrderDO::getPayStatus, BooleanEnum.TRUE.getValue()));
+
+        if (CollectionUtil.isEmpty(expenseOrders)) {
+            return;
+        }
+
+        markBillsPaid(expenseOrders);
+    }
+
+    /**
+     * 批量将账单置为已缴费(系统补缴)
+     */
+    private void markBillsPaid(List<ExpenseOrderDO> expenseOrders) {
         Date payTime = new Date();
         String payeeName = SecurityFrameworkUtils.getLoginUserNickname();
 
-        // 2. 逐单处理:补齐 payOrder/payOrderItem + 明细 payStatus/payOrderId + 主单 payStatus/payType/payTime/payableAmount
         for (ExpenseOrderDO expenseOrder : expenseOrders) {
-            // 2.1 取该账单“需要置为已缴费”的明细:当前未缴费且展示的条目(跟 payOrder 后续判断口径保持一致)
             List<ExpenseOrderItemDO> toPayItems = expenseOrderItemMapper.selectList(new LambdaQueryWrapperX<ExpenseOrderItemDO>()
                     .eq(ExpenseOrderItemDO::getExpenseOrderId, expenseOrder.getId())
                     .eq(ExpenseOrderItemDO::getPayStatus, BooleanEnum.FALSE.getValue()));
 
             if (CollectionUtil.isEmpty(toPayItems)) {
-                // 已无未缴费明细(可能已被其他流程补齐),直接跳过
                 continue;
             }
 
@@ -3015,7 +2930,6 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
                     .reduce(BigDecimal.ZERO, BigDecimal::add)
                     .setScale(2, RoundingMode.HALF_UP);
 
-            // 2.2 创建缴费单(模拟一次“系统补缴”),注意 round 字段与 payOrder 保持一致
             ExpensePayOrderDO payOrder = new ExpensePayOrderDO();
             payOrder.setPayAmount(payAmount);
             payOrder.setRoundPayAmount(payAmount.setScale(0, RoundingMode.HALF_UP));
@@ -3027,7 +2941,6 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
             payOrder.setTenantId(expenseOrder.getTenantId());
             expensePayOrderMapper.insert(payOrder);
 
-            // 2.3 创建缴费方式明细:用一个固定 payType 表示“系统补缴”,并沿用 payOrder 的金额
             ExpensePayOrderItemDO payOrderItem = new ExpensePayOrderItemDO();
             payOrderItem.setPayOrderId(payOrder.getId());
             payOrderItem.setPayType("system");
@@ -3039,7 +2952,6 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
             payOrderItem.setTenantId(expenseOrder.getTenantId());
             expensePayOrderItemMapper.insert(payOrderItem);
 
-            // 2.4 更新账单明细:缴费状态 + 缴费时间 + 关联 payOrderId,并补齐押金入账(参考 payOrder)
             for (ExpenseOrderItemDO item : toPayItems) {
                 ExpenseOrderItemDO updateItem = new ExpenseOrderItemDO();
                 updateItem.setId(item.getId());
@@ -3048,10 +2960,6 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
                 updateItem.setPayOrderId(payOrder.getId());
                 expenseOrderItemMapper.updateById(updateItem);
 
-                // 押金处理:
-                // 1) 日常费用押金(DailyExpenses itemName/itemCategory 包含押金/风险金/预备金 等)
-                // 2) 月度费用押金(ExpenseItem isDeposit=TRUE)
-                // 说明:markCheckinBillsPaid 没有票据号入参,因此 receiptNumber 不回填。
                 DailyExpensesDO dailyExpenses = null;
                 if (Objects.equals(item.getExpenseSource(), BusinessConstants.DAILY_EXPENSES)) {
                     dailyExpenses = dailyExpensesMapper.selectById(item.getSourceExpenseItemId());
@@ -3076,7 +2984,6 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
                 boolean isMonthlyDeposit = expenseItemDO != null && Objects.equals(expenseItemDO.getIsDeposit(), BooleanEnum.TRUE.getValue());
 
                 if (isDailyDeposit || isMonthlyDeposit) {
-                    // 幂等保护:如果该账单明细已经生成过押金缴费记录(用 itemId + type=1 + elderId 判断),则跳过
                     Long recordItemId = isDailyDeposit ? dailyExpenses.getId() : expenseItemDO.getItemId();
                     DepositRecordDO existed = depositRecordMapper.selectOne(new LambdaQueryWrapperX<DepositRecordDO>()
                             .eq(DepositRecordDO::getElderId, expenseOrder.getElderId())
@@ -3086,7 +2993,6 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
                         continue;
                     }
 
-                    // 获取押金账户(payOrder 中:日常押金按 elderId+tenantId 查询,月度押金仅按 elderId 查询;这里统一按 elderId+tenantId 优先)
                     DepositDO dbDeposit = depositMapper.selectOne(new LambdaQueryWrapperX<DepositDO>()
                             .eq(DepositDO::getElderId, expenseOrder.getElderId())
                             .eq(DepositDO::getTenantId, expenseOrder.getTenantId()));
@@ -3115,7 +3021,6 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
                     dbDeposit.setAmount((dbDeposit.getAmount() == null ? BigDecimal.ZERO : dbDeposit.getAmount()).add(depositAmount));
                     depositMapper.updateById(dbDeposit);
 
-                    // 日常费用押金:同步把日常费用置为已缴费并回填账单号(跟 payOrder 一致)
                     if (dailyExpenses != null) {
                         dailyExpenses.setStatus(BooleanEnum.TRUE.getValue());
                         dailyExpenses.setExpenseOrderNumber(expenseOrder.getBillOrderNumber());
@@ -3124,8 +3029,6 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
                 }
             }
 
-            // 2.5 更新账单主表:该接口语义为“系统强制缴清”,因此直接将主单置为已缴清
-            // 注意:此处字段 payableAmount 在现有代码中被当作“已缴金额”累加使用;这里强制将其置为账单实际金额。
             ExpenseOrderDO updateOrder = new ExpenseOrderDO();
             updateOrder.setId(expenseOrder.getId());
             updateOrder.setPayTime(payTime);
@@ -3757,6 +3660,24 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
         LocalDate monthStart = yearMonth.atDay(1);
         LocalDate monthEnd = yearMonth.atEndOfMonth();
 
+        // 合同到期规则:
+        // 1) 到期月只计 1号~到期日(包含到期日)
+        // 2) 到期后月份不再计固定费用
+        // 3) 与离住规则同时命中时,实际结束时间优先采用合同到期时间
+        LocalDate contractExpireDate = null;
+        ElderlyContractDO latestContract = elderlyContractMapper.selectOne(new LambdaQueryWrapperX<ElderlyContractDO>()
+                .eq(ElderlyContractDO::getElderId, elderId)
+                .orderByDesc(ElderlyContractDO::getCreatedTime)
+                .last("LIMIT 1"));
+        boolean isContractExpireInBillingMonth = false;
+        if (latestContract != null && latestContract.getExpireTime() != null) {
+            contractExpireDate = latestContract.getExpireTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+            if (contractExpireDate.isBefore(monthStart)) {
+                return collect;
+            }
+            isContractExpireInBillingMonth = !contractExpireDate.isBefore(monthStart) && !contractExpireDate.isAfter(monthEnd);
+        }
+
         // 计费核心:仅当目标月份与“在院区间”有交集才生成固定费用。
         // 这里的“在院区间”按离住/返院时间截断,避免出现:离住后未返院的月份不应计费,但后续补录返院导致回算。
         LocalDate actualStart = monthStart;
@@ -3777,29 +3698,45 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
                 if (outDate.isAfter(monthStart)) {
                     actualEnd = outDate.minusDays(1);
                 } else {
-                    return collect;
+                    // 原离住规则在“月初离住”会直接不计费;
+                    // 但若本月合同到期,需像离住按区间计费(1号~到期日),故不提前 return
+                    if (!isContractExpireInBillingMonth) {
+                        return collect;
+                    }
                 }
             }
 
             // 离住后的月份(本月月初在离院日之后)
             if (monthStart.isAfter(outDate)) {
                 if (comeDate == null) {
-                    // 仍处于离住中:离院后月份不计费
-                    return collect;
-                }
-                YearMonth comeYm = YearMonth.from(comeDate);
-                if (yearMonth.isBefore(comeYm)) {
-                    // 返院发生在未来月份:本月仍不计费
-                    return collect;
-                }
-                if (yearMonth.equals(comeYm)) {
-                    // 返院当月:从返院日开始计费(包含当天)
-                    actualStart = comeDate.isAfter(monthStart) ? comeDate : monthStart;
+                    // 仍处于离住中:离院后月份不计费;
+                    // 但若本月合同到期,仍需生成 1号~到期日费用
+                    if (!isContractExpireInBillingMonth) {
+                        return collect;
+                    }
+                } else {
+                    YearMonth comeYm = YearMonth.from(comeDate);
+                    if (yearMonth.isBefore(comeYm)) {
+                        // 返院发生在未来月份:本月仍不计费;
+                        // 但若本月合同到期,仍需生成 1号~到期日费用
+                        if (!isContractExpireInBillingMonth) {
+                            return collect;
+                        }
+                    } else if (yearMonth.equals(comeYm)) {
+                        // 返院当月:从返院日开始计费(包含当天)
+                        actualStart = comeDate.isAfter(monthStart) ? comeDate : monthStart;
+                    }
+                    // 返院后的月份:整月计费(保持 monthStart)
                 }
-                // 返院后的月份:整月计费(保持 monthStart)
             }
         }
 
+        // 合同到期月处理:像离住一样按天折算,只保留月初到到期日区间;
+        // 若与离住规则同时命中,结束时间优先取合同到期时间
+        if (contractExpireDate != null && !contractExpireDate.isBefore(monthStart) && !contractExpireDate.isAfter(monthEnd)) {
+            actualEnd = contractExpireDate;
+        }
+
         if (!actualEnd.isBefore(actualStart)) {
             //如果没有当月订单查询当月未缴费的日常费用和月度费用
             Optional<ExpenseDO> optional = expenseMapper.selectList(new LambdaQueryWrapperX<ExpenseDO>()

+ 8 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ExpenseServiceImpl.java

@@ -49,6 +49,7 @@ import java.util.List;
 import java.util.Optional;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exceptionCustomMsg;
 import static cn.iocoder.yudao.module.system.enums.LogRecordConstants.*;
 
 
@@ -450,6 +451,12 @@ public class ExpenseServiceImpl implements ExpenseService {
     @Transactional
     public void renewContract(ExpenseSaveReqVO createReqVO) {
         checkElderOrderExist(createReqVO);
+        if(createReqVO.getOldContractId() != null){
+            if(elderlyContractMapper.selectById(createReqVO.getOldContractId()).getIsRenew() == 1){
+                throw exceptionCustomMsg(ErrorCodeConstants.COMMON_ERROR,"已续约不能重复续约");
+            }
+            elderlyContractMapper.updateById(new ElderlyContractDO().setId(createReqVO.getOldContractId()).setIsRenew(1));
+        }
         // 将最新的费用记录的结束日期更新为续约日期的前一天
         ExpenseDO expenseDO = expenseMapper.selectList(new LambdaQueryWrapperX<ExpenseDO>()
                 .eq(ExpenseDO::getElderId, createReqVO.getElderId())
@@ -827,7 +834,7 @@ public class ExpenseServiceImpl implements ExpenseService {
     private void checkElderOrderExist(ExpenseSaveReqVO createReqVO){
         List<ElderlyContractDO> oldContract = elderlyContractMapper.selectList(new LambdaQueryWrapperX<ElderlyContractDO>()
                 .eq(ElderlyContractDO::getElderId, createReqVO.getElderId())
-                .eq(ElderlyContractDO::getStatus, 1));
+                .in(ElderlyContractDO::getStatus, 0,1));
         if(CollectionUtil.isEmpty(oldContract)){
             return;
         }

+ 31 - 5
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/NurseChangeRecordServiceImpl.java

@@ -16,6 +16,7 @@ import cn.iocoder.yudao.module.system.controller.admin.biz.vo.*;
 import cn.iocoder.yudao.module.system.dal.dataobject.biz.*;
 import cn.iocoder.yudao.module.system.dal.mysql.biz.ElderlyChangeRecordMapper;
 import cn.iocoder.yudao.module.system.dal.mysql.biz.BuildBedMapper;
+import cn.iocoder.yudao.module.system.dal.mysql.biz.ElderlyInfoMapper;
 import cn.iocoder.yudao.module.system.enums.common.BooleanEnum;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -48,6 +49,9 @@ public class NurseChangeRecordServiceImpl implements ElderlyChangeRecordService
     @Autowired
     private BuildBedMapper buildBedMapper;
 
+    @Autowired
+    private ElderlyInfoMapper elderlyInfoMapper;
+
     @Override
     public Long createBedChangeRecord(BedChangeRecordSaveReqVO createReqVO) {
         // 插入
@@ -131,25 +135,47 @@ public class NurseChangeRecordServiceImpl implements ElderlyChangeRecordService
 
     @Override
     @TenantIgnore
-    public Boolean checkPrivateRoom(Long bedId) {
+    public Boolean checkPrivateRoom(Long bedId, Long elderId) {
+        if (bedId == null || elderId == null) {
+            return Boolean.FALSE;
+        }
         BuildBedDO buildBedDO = buildBedMapper.selectById(bedId);
         if (null == buildBedDO) {
-            return Boolean.TRUE;
+            return Boolean.FALSE;
         }
         List<BuildBedDO> buildBedDOList = buildBedMapper.selectList(new LambdaQueryWrapperX<BuildBedDO>()
                 .eq(BuildBedDO::getRoomId, buildBedDO.getRoomId()));
         if (null != buildBedDOList && buildBedDOList.size() > 1) {
-            boolean b = buildBedDOList.stream().anyMatch(BuildBedDO -> buildBedDO.getStatus() == 1);
-            if (b) {
+            List<BuildBedDO> usedBedList = buildBedDOList.stream()
+                    .filter(item -> item.getStatus() != null && item.getStatus() == 1)
+                    .collect(Collectors.toList());
+            if (usedBedList.size() > 1) {
                 return Boolean.FALSE;
             }
+            if (usedBedList.size() == 1) {
+                // 房间只有一个长者时,必须是申请长者本人在目标床位,才允许包房
+                BuildBedDO usedBed = usedBedList.get(0);
+                if (!bedId.equals(usedBed.getId())) {
+                    return Boolean.FALSE;
+                }
+                ElderlyInfoDO elderlyInfoDO = elderlyInfoMapper.selectById(elderId);
+                if (elderlyInfoDO == null || !bedId.equals(elderlyInfoDO.getBedId())) {
+                    return Boolean.FALSE;
+                }
+            }
             List<Long> idList = buildBedDOList.stream().map(BuildBedDO::getId).collect(Collectors.toList());
             List<ElderlyChangeRecordDO> list = bedChangeRecordMapper.selectList(new LambdaQueryWrapperX<ElderlyChangeRecordDO>()
                     .in(ElderlyChangeRecordDO::getExpectId, idList)
                     .eq(ElderlyChangeRecordDO::getCurrentFlag, 0)
                     .eq(ElderlyChangeRecordDO::getChangeType, 0));
             if (null != list && list.size() > 0) {
-                return Boolean.FALSE;
+                // 若存在当前申请床位之外的最新床位记录,说明房间关联到其他变更,不能包房
+                List<ElderlyChangeRecordDO> otherBedRecordList = list.stream()
+                        .filter(item -> !bedId.equals(item.getExpectId()))
+                        .collect(Collectors.toList());
+                if (otherBedRecordList.size() > 0) {
+                    return Boolean.FALSE;
+                }
             }
         }
 

+ 0 - 1
yudao-server/src/main/resources/application-test.yaml

@@ -87,7 +87,6 @@ spring:
     host: 47.112.126.153 # 地址\
     port: 6379 # 端口
     database: 0 # 数据库索引
-    password: ynkbc0! # 密码,建议生产环境开启
 
 --- #################### 定时任务相关配置 ####################
 

+ 2 - 2
yudao-server/src/main/resources/application.yaml

@@ -3,8 +3,8 @@ spring:
     name: yudao-server
 
   profiles:
-#    active: test
-    active: dev
+    active: test
+#    active: dev
 
 
   main: