gaoluyang
13 小时以前 9cd4074a456fd5c422506279191b6015836347e5
feat: 设备台账启用状态、审批签核展示与导入健壮性

- 设备台账:存放位置改为启用/停用(is_enabled),停用设备不参与巡检
- 新增 sql/migration_device_ledger_enable_status.sql
- 巡检任务创建、定时任务生成跳过停用设备
- 选择巡检设备时仅返回启用设备
- 协同审批:审批任务列表按节点回填审批方式(会签/或签),修复列表统一显示为会签的问题
- 设备台账导入:跳过表头不匹配产生的空行;解析为空或逐行转换/入库失败时返回明确提示,整单事务回滚
- 采购付款:入库单未审核时回退按付款申请中的采购合同号关联采购台账
- 销售台账:待发货数量按“订单数量-已发货数量-审批中数量”计算,最小为 0

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加1个文件
已修改13个文件
287 ■■■■■ 文件已修改
sql/migration_device_ledger_enable_status.sql 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApprovalTask.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceServiceImpl.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/dto/DeviceLedgerDto.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/execl/DeviceLedgerExeclDto.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/pojo/DeviceLedger.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/DeviceLedgerServiceImpl.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJob.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/SupplierManageMapper.xml 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/device/DeviceLedgerMapper.xml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/migration_device_ledger_enable_status.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,4 @@
-- è®¾å¤‡å°è´¦ï¼šå­˜æ”¾ä½ç½®æ”¹ä¸ºå¯ç”¨/停用状态
-- åœç”¨è®¾å¤‡ä¸å‚与巡检(定时巡检任务生成时会跳过)
ALTER TABLE device_ledger
    ADD COLUMN is_enabled tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用 1启用 0停用' AFTER status;
src/main/java/com/ruoyi/approve/pojo/ApprovalTask.java
@@ -89,6 +89,13 @@
    private Byte isRead;
    /**
     * èŠ‚ç‚¹å®¡æ‰¹æ–¹å¼ï¼ˆAND-会签 OR-或签),非任务表字段,列表展示时由节点实例回填
     */
    @Schema(description ="节点审批方式 AND-会签 OR-或签")
    @TableField(exist = false)
    private String approveType;
    /**
     * åˆ›å»ºäºº
     */
    @Schema(description ="创建人")
src/main/java/com/ruoyi/approve/service/impl/ApprovalInstanceServiceImpl.java
@@ -149,10 +149,23 @@
                            .eq(ApprovalTask::getDeleted, 0)
            ).stream().collect(Collectors.groupingBy(ApprovalTask::getInstanceId));
            // ä»»åŠ¡è¡¨æœ¬èº«ä¸å­˜å®¡æ‰¹æ–¹å¼ï¼ŒæŒ‰èŠ‚ç‚¹å®žä¾‹å›žå¡«ä¼šç­¾/或签,避免前端统一显示为会签
            Map<Long, String> nodeApproveTypeMap = approvalInstanceNodeService.list(
                    Wrappers.<ApprovalInstanceNode>lambdaQuery()
                            .in(ApprovalInstanceNode::getInstanceId, instanceIds)
                            .eq(ApprovalInstanceNode::getDeleted, 0)
            ).stream().collect(Collectors.toMap(
                    ApprovalInstanceNode::getId,
                    node -> node.getApproveType() == null ? "" : node.getApproveType(),
                    (existing, replacement) -> existing
            ));
            for (ApprovalInstanceVo vo : records) {
                vo.setIsApprove(approveProcessConfigNodeUtils.isCurrentApprover(vo.getId(), currentUserId));
                vo.setRecords(recordMap.getOrDefault(vo.getId(), new ArrayList<>()));
                vo.setTasks(taskMap.getOrDefault(vo.getId(), new ArrayList<>()));
                List<ApprovalTask> instanceTasks = taskMap.getOrDefault(vo.getId(), new ArrayList<>());
                instanceTasks.forEach(task -> task.setApproveType(nodeApproveTypeMap.get(task.getNodeId())));
                vo.setTasks(instanceTasks);
                vo.setStorageBlobVOList(fileUtil.getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum.APPROVAL_INSTANCE, vo.getId()));
            }
src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
@@ -465,37 +465,37 @@
                    {
                        val = Convert.toBigDecimal(val);
                    }
                    else if (Date.class == fieldType)
                    {
                        if (val instanceof String)
                        {
                            val = DateUtils.parseDate(val);
                    else if (Date.class == fieldType)
                    {
                        if (val instanceof String)
                        {
                            val = DateUtils.parseDate(val);
                        }
                        else if (val instanceof Double)
                        {
                            val = DateUtil.getJavaDate((Double) val);
                        }
                    }
                    else if (LocalDate.class == fieldType)
                    {
                        if (val instanceof String)
                        {
                            Date date = DateUtils.parseDate(val);
                            val = StringUtils.isNull(date) ? null : DateUtils.toLocalDate(date);
                        }
                        else if (val instanceof Date)
                        {
                            val = DateUtils.toLocalDate((Date) val);
                        }
                        else if (val instanceof Double)
                        {
                            val = DateUtils.toLocalDate(DateUtil.getJavaDate((Double) val));
                        }
                    }
                    else if (Boolean.TYPE == fieldType || Boolean.class == fieldType)
                    {
                        val = Convert.toBool(val, false);
                    }
                        {
                            val = DateUtil.getJavaDate((Double) val);
                        }
                    }
                    else if (LocalDate.class == fieldType)
                    {
                        if (val instanceof String)
                        {
                            Date date = DateUtils.parseDate(val);
                            val = StringUtils.isNull(date) ? null : DateUtils.toLocalDate(date);
                        }
                        else if (val instanceof Date)
                        {
                            val = DateUtils.toLocalDate((Date) val);
                        }
                        else if (val instanceof Double)
                        {
                            val = DateUtils.toLocalDate(DateUtil.getJavaDate((Double) val));
                        }
                    }
                    else if (Boolean.TYPE == fieldType || Boolean.class == fieldType)
                    {
                        val = Convert.toBool(val, false);
                    }
                    if (StringUtils.isNotNull(fieldType))
                    {
                        String propertyName = field.getName();
@@ -535,7 +535,11 @@
                        ReflectUtils.invokeSetter(entity, propertyName, val);
                    }
                }
                list.add(entity);
                // è¡¨å¤´ä¸Žæ¨¡æ¿ä¸ä¸€è‡´æ—¶è¿™ä¸€è¡Œå¯èƒ½æ²¡æœ‰åŒ¹é…åˆ°ä»»ä½•字段,entity ä¸º null,跳过以免调用方空指针
                if (entity != null)
                {
                    list.add(entity);
                }
            }
        }
        return list;
@@ -667,24 +671,24 @@
                        val = Convert.toFloat(val);
                    } else if (BigDecimal.class == fieldType) {
                        val = Convert.toBigDecimal(val);
                    } else if (Date.class == fieldType) {
                        if (val instanceof String) {
                            val = DateUtils.parseDate(val);
                        } else if (val instanceof Double) {
                            val = DateUtil.getJavaDate((Double) val);
                        }
                    } else if (LocalDate.class == fieldType) {
                        if (val instanceof String) {
                            Date date = DateUtils.parseDate(val);
                            val = StringUtils.isNull(date) ? null : DateUtils.toLocalDate(date);
                        } else if (val instanceof Date) {
                            val = DateUtils.toLocalDate((Date) val);
                        } else if (val instanceof Double) {
                            val = DateUtils.toLocalDate(DateUtil.getJavaDate((Double) val));
                        }
                    } else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) {
                        val = Convert.toBool(val, false);
                    }
                    } else if (Date.class == fieldType) {
                        if (val instanceof String) {
                            val = DateUtils.parseDate(val);
                        } else if (val instanceof Double) {
                            val = DateUtil.getJavaDate((Double) val);
                        }
                    } else if (LocalDate.class == fieldType) {
                        if (val instanceof String) {
                            Date date = DateUtils.parseDate(val);
                            val = StringUtils.isNull(date) ? null : DateUtils.toLocalDate(date);
                        } else if (val instanceof Date) {
                            val = DateUtils.toLocalDate((Date) val);
                        } else if (val instanceof Double) {
                            val = DateUtils.toLocalDate(DateUtil.getJavaDate((Double) val));
                        }
                    } else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) {
                        val = Convert.toBool(val, false);
                    }
                    if (StringUtils.isNotNull(fieldType)) {
                        String propertyName = field.getName();
@@ -714,7 +718,11 @@
                        ReflectUtils.invokeSetter(entity, propertyName, val);
                    }
                }
                list.add(entity);
                // è¡¨å¤´ä¸Žæ¨¡æ¿ä¸ä¸€è‡´æ—¶è¿™ä¸€è¡Œå¯èƒ½æ²¡æœ‰åŒ¹é…åˆ°ä»»ä½•字段,entity ä¸º null,跳过以免调用方空指针
                if (entity != null)
                {
                    list.add(entity);
                }
            }
        }
        return list;
src/main/java/com/ruoyi/device/dto/DeviceLedgerDto.java
@@ -135,6 +135,12 @@
    @Schema(description = "状态")
    private String status;
    /**
     * æ˜¯å¦å¯ç”¨ 1-启用 0-停用,停用设备不参与巡检
     */
    @Schema(description = "是否启用 1-启用 0-停用")
    private Integer isEnabled;
    @Schema(description = "计划运行时间")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
src/main/java/com/ruoyi/device/execl/DeviceLedgerExeclDto.java
@@ -82,6 +82,12 @@
    /**
     * æ˜¯å¦å¯ç”¨ 1-启用 0-停用
     */
    @Excel(name = "启用状态", sort = 9, readConverterExp = "1=启用,0=停用")
    private Integer isEnabled;
    /**
     * å½•入人
     */
    @Excel(name = "录入人",sort = 9)
src/main/java/com/ruoyi/device/pojo/DeviceLedger.java
@@ -124,6 +124,12 @@
    @Schema(description = "状态")
    private String status;
    /**
     * æ˜¯å¦å¯ç”¨ 1-启用 0-停用,停用设备不参与巡检
     */
    @Schema(description = "是否启用 1-启用 0-停用")
    private Integer isEnabled;
    @Schema(description = "计划运行时间")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
src/main/java/com/ruoyi/device/service/impl/DeviceLedgerServiceImpl.java
@@ -4,6 +4,7 @@
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
@@ -23,6 +24,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@@ -143,29 +145,59 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean importData(MultipartFile file) throws IOException {
        ExcelUtil<DeviceLedgerExeclDto> util = new ExcelUtil<DeviceLedgerExeclDto>(DeviceLedgerExeclDto.class);
        List<DeviceLedgerExeclDto> userList = util.importExcel(file.getInputStream());
        userList.forEach(c -> {
            DeviceLedger deviceLedger = new DeviceLedger();
            SysUser sysUser = sysUserMapper.selectUserByUserName(c.getCreateUser());
            if (sysUser!=null) {
                deviceLedger.setCreateUser(sysUser.getUserId().intValue());
            }else {
                deviceLedger.setCreateUser(SecurityUtils.getUserId().intValue());
        if (userList == null || userList.isEmpty()) {
            // è¡¨å¤´ä¸Žæ¨¡æ¿ä¸ä¸€è‡´æ—¶è§£æžä¸åˆ°ä»»ä½•数据,明确提示,避免前端只看到“导入成功”却没有任何数据
            throw new ServiceException("未读取到可导入的数据,请确认文件表头与导入模板一致后重试");
        }
        int importCount = 0;
        for (int i = 0; i < userList.size(); i++) {
            DeviceLedgerExeclDto c = userList.get(i);
            if (c == null) {
                continue;
            }
            BeanUtils.copyProperties(c,deviceLedger);
            // é€šè¿‡å«ç¨Žå•价、数量、税率计算含税总价,不含税总价
            deviceLedger.setTaxIncludingPriceTotal(c.getTaxIncludingPriceUnit());
            deviceLedger.setNumber(BigDecimal.ONE);
            deviceLedger.setPlanRuntimeTime(DateUtils.toLocalDate(c.getPlanRuntimeTime()));
            // è®¡ç®—不含税总价,处理空值情况
            if (deviceLedger.getTaxIncludingPriceTotal() != null && c.getTaxRate() != null) {
                deviceLedger.setUnTaxIncludingPriceTotal(deviceLedger.getTaxIncludingPriceTotal().divide(BigDecimal.ONE.add(c.getTaxRate()), 2, RoundingMode.HALF_UP));
            DeviceLedger deviceLedger;
            try {
                deviceLedger = buildDeviceLedger(c);
            } catch (Exception e) {
                throw new ServiceException("第" + (i + 2) + "行数据有误:" + e.getMessage());
            }
            deviceLedgerMapper.insert(deviceLedger);
        });
            try {
                deviceLedgerMapper.insert(deviceLedger);
            } catch (Exception e) {
                throw new ServiceException("第" + (i + 2) + "行保存失败:" + e.getMessage());
            }
            importCount++;
        }
        if (importCount == 0) {
            throw new ServiceException("未读取到可导入的数据,请确认文件表头与导入模板一致后重试");
        }
        return true;
    }
    /**
     * å°†å¯¼å…¥è¡Œè½¬æ¢ä¸ºè®¾å¤‡å°è´¦å®žä½“
     */
    private DeviceLedger buildDeviceLedger(DeviceLedgerExeclDto c) {
        DeviceLedger deviceLedger = new DeviceLedger();
        SysUser sysUser = sysUserMapper.selectUserByUserName(c.getCreateUser());
        if (sysUser != null) {
            deviceLedger.setCreateUser(sysUser.getUserId().intValue());
        } else {
            deviceLedger.setCreateUser(SecurityUtils.getUserId().intValue());
        }
        BeanUtils.copyProperties(c, deviceLedger);
        // é€šè¿‡å«ç¨Žå•价、数量、税率计算含税总价,不含税总价
        deviceLedger.setTaxIncludingPriceTotal(c.getTaxIncludingPriceUnit());
        deviceLedger.setNumber(BigDecimal.ONE);
        deviceLedger.setPlanRuntimeTime(DateUtils.toLocalDate(c.getPlanRuntimeTime()));
        // è®¡ç®—不含税总价,处理空值情况
        if (deviceLedger.getTaxIncludingPriceTotal() != null && c.getTaxRate() != null) {
            deviceLedger.setUnTaxIncludingPriceTotal(deviceLedger.getTaxIncludingPriceTotal().divide(BigDecimal.ONE.add(c.getTaxRate()), 2, RoundingMode.HALF_UP));
        }
        return deviceLedger;
    }
}
src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java
@@ -203,6 +203,10 @@
        if (deviceLedger == null) {
            throw new IllegalArgumentException("所选设备不存在,请重新选择");
        }
        boolean deviceChanged = oldInspectionTask == null || !Objects.equals(oldInspectionTask.getTaskId(), taskId);
        if (deviceChanged && Integer.valueOf(0).equals(deviceLedger.getIsEnabled())) {
            throw new IllegalArgumentException("所选设备已停用,不能安排巡检");
        }
    }
    private void validateInspectionInput(InspectionTaskDto inspectionTaskDto, InspectionTask oldInspectionTask) {
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskJob.java
@@ -56,9 +56,11 @@
//                throw new JobExecutionException("定时任务已禁用: " + taskId);
//            }
            // 2. åˆ›å»ºå¹¶ä¿å­˜å·¡æ£€ä»»åŠ¡è®°å½• - è¿™å°±æ˜¯æ‚¨æä¾›çš„代码应该放的位置
            InspectionTask inspectionTask = createInspectionTask(timingTask);
            inspectionTaskMapper.insert(inspectionTask);
            // 2. åˆ›å»ºå¹¶ä¿å­˜å·¡æ£€ä»»åŠ¡è®°å½•ï¼›è®¾å¤‡å·²åœç”¨æ—¶è·³è¿‡ï¼Œä¸ç”Ÿæˆå·¡æ£€ä»»åŠ¡
            if (!isDeviceDisabled(timingTask.getTaskId())) {
                InspectionTask inspectionTask = createInspectionTask(timingTask);
                inspectionTaskMapper.insert(inspectionTask);
            }
            // 3. æ›´æ–°å®šæ—¶ä»»åŠ¡çš„æ‰§è¡Œæ—¶é—´
            if (!tasks.isEmpty()) {
@@ -97,6 +99,21 @@
        }
    }
    /**
     * è®¾å¤‡æ˜¯å¦å·²åœç”¨ï¼ˆåœç”¨è®¾å¤‡ä¸å‚与巡检)
     */
    private boolean isDeviceDisabled(Integer deviceId) {
        if (deviceId == null || deviceId <= 0) {
            return false;
        }
        List<Integer> enabledList = jdbcTemplate.queryForList(
                "SELECT is_enabled FROM device_ledger WHERE id = ?", Integer.class, deviceId);
        if (enabledList.isEmpty() || enabledList.get(0) == null) {
            return false;
        }
        return enabledList.get(0) == 0;
    }
    // è¿™å°±æ˜¯æ‚¨æä¾›çš„代码封装成的方法
    private InspectionTask createInspectionTask(TimingTask timingTask) {
        InspectionTask inspectionTask = new InspectionTask();
src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java
@@ -6,6 +6,7 @@
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.inspectiontask.dto.TimingTaskDto;
import com.ruoyi.inspectiontask.mapper.TimingTaskMapper;
import com.ruoyi.inspectiontask.pojo.TimingTask;
@@ -191,9 +192,14 @@
        if (taskId == null || taskId <= 0) {
            return;
        }
        if (deviceLedgerMapper.selectById(taskId.longValue()) == null) {
        DeviceLedger deviceLedger = deviceLedgerMapper.selectById(taskId.longValue());
        if (deviceLedger == null) {
            throw new IllegalArgumentException("所选设备不存在,请重新选择");
        }
        boolean deviceChanged = oldTimingTask == null || !Objects.equals(oldTimingTask.getTaskId(), taskId);
        if (deviceChanged && Integer.valueOf(0).equals(deviceLedger.getIsEnabled())) {
            throw new IllegalArgumentException("所选设备已停用,不能安排巡检");
        }
    }
    public LocalDateTime calculateFirstExecutionTime(TimingTask task) {
src/main/resources/mapper/basic/SupplierManageMapper.xml
@@ -113,18 +113,25 @@
               select
                   app.id,
                   app.payment_amount,
                   pl.id as purchase_ledger_id
                   -- ä¼˜å…ˆæŒ‰å…¥åº“单关联采购合同;入库单未审核/不存在时,回退按付款申请中的采购合同号关联,
                   -- é¿å…â€œæ–°å¢žä»˜æ¬¾â€æˆåŠŸåŽæ˜Žç»†ä¸­çœ‹ä¸åˆ°è¯¥ç¬”ä»˜æ¬¾
                   COALESCE(
                       (select min(pl_sir.id)
                        from stock_in_record sir
                        left join quality_inspect qi on sir.record_type = 10 and sir.record_id = qi.id
                        left join purchase_ledger pl_sir
                               on pl_sir.id = if(sir.record_type = 7, sir.record_id, qi.purchase_ledger_id)
                        where find_in_set(sir.id, apa.stock_in_record_ids) > 0
                          and sir.approval_status = 1
                          and sir.record_type in ('7','10')),
                       (select pl_no.id
                        from purchase_ledger pl_no
                        where pl_no.purchase_contract_number = apa.payment_content
                        order by pl_no.id
                        limit 1)
                   ) as purchase_ledger_id
               from account_purchase_payment app
               left join account_payment_application apa on app.account_payment_application_id = apa.id
               left join stock_in_record sir on FIND_IN_SET(sir.id, apa.stock_in_record_ids) > 0
                   -- 10 ç±»åž‹æ‰å…³è”质检表
               LEFT JOIN quality_inspect qi ON sir.record_type = 10 AND sir.record_id = qi.id
                   -- åŠ¨æ€å…³è”é‡‡è´­ï¼ˆè‡ªåŠ¨é€‚é… 7 å’Œ 10)
               LEFT JOIN purchase_ledger pl
                         ON pl.id = IF(sir.record_type = 7, sir.record_id, qi.purchase_ledger_id)
               WHERE sir.approval_status = 1
                 AND sir.record_type IN ('7','10')
               group by app.id, pl.id
           ) t
           left join purchase_ledger pl on t.purchase_ledger_id = pl.id
           group by pl.id
src/main/resources/mapper/device/DeviceLedgerMapper.xml
@@ -16,6 +16,7 @@
        dl.unit,
        dl.number,
        dl.status,
        dl.is_enabled,
        dl.plan_runtime_time,
        dl.start_runtime_time,
        dl.end_runtime_time,
@@ -105,10 +106,13 @@
        dl.device_brand,
        dl.type,
        dl.area_id,
        dl.is_enabled,
        da.name AS area_name
        FROM device_ledger dl
        left join device_area da on dl.area_id = da.id
        <where>
            <!-- åœç”¨è®¾å¤‡ä¸å‚与巡检,选择设备时只返回启用设备 -->
            AND COALESCE(dl.is_enabled, 1) = 1
            <if test="areaId != null">
                AND (
                    dl.area_id = #{areaId}
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml
@@ -33,7 +33,8 @@
        WHEN (IFNULL(t2.qualitity, 0) - IFNULL(t2.locked_quantity, 0)) >0 THEN 1
        ELSE 0
        END as has_sufficient_stock,
        NULL as no_quantity,
        -- å¾…发货数量 = è®¢å•数量 - å·²å‘货数量 - å®¡æ‰¹ä¸­/待审核数量,最小为 0
        GREATEST(IFNULL(T1.quantity, 0) - IFNULL(t3.shipped_quantity, 0) - IFNULL(t5.pending_approval_quantity, 0), 0) as no_quantity,
        IFNULL(t3.shipped_quantity, 0) as shipped_quantity,
        IFNULL(t6.return_quantity, 0) as return_quantity,
        IFNULL(t4.approved_stock_in_num, 0) as approved_stock_in_num,