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

fix: 采购申请icitem_id精度修复+货源清单去重+交付跟踪执行采购Tab数据修复+排程字典键冲突修复+需求计划必填校验+发货单每箱数量必填

Pengxy 1 день назад
Родитель
Сommit
87aa69cc84

+ 1 - 1
Web/package.json

@@ -1,7 +1,7 @@
 {
 	"name": "admin.net",
 	"type": "module",
-	"version": "2.4.360",
+	"version": "2.4.361",
 	"packageManager": "pnpm@10.32.1",
 	"lastBuildTime": "2026.03.15",
 	"description": "Admin.NET 站在巨人肩膀上的 .NET 通用权限开发框架",

+ 3 - 1
Web/src/views/aidop/s3/supply/demandScheduleForm.vue

@@ -24,7 +24,7 @@
 				<el-col :span="12"><el-form-item label="需求数量"><el-input-number v-model="form.mesQty" :disabled="isView" :precision="4" :min="0" style="width: 100%" /></el-form-item></el-col>
 				<el-col :span="12"><el-form-item label="期初库存"><el-input-number v-model="form.locQty" :disabled="isView" :precision="4" :min="0" style="width: 100%" /></el-form-item></el-col>
 				<el-col :span="12"><el-form-item label="在途数量"><el-input-number v-model="form.sechedQty" :disabled="isView" :precision="4" :min="0" style="width: 100%" /></el-form-item></el-col>
-				<el-col :span="12"><el-form-item label="物料净需求数量"><el-input-number v-model="form.toSechedQty" :disabled="isView" :precision="4" :min="0" style="width: 100%" /></el-form-item></el-col>
+				<el-col :span="12"><el-form-item label="物料净需求数量" prop="toSechedQty"><el-input-number v-model="form.toSechedQty" :disabled="isView" :precision="4" :min="0" style="width: 100%" /></el-form-item></el-col>
 				<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remarks" :disabled="isView" type="textarea" rows="3" /></el-form-item></el-col>
 			</el-row>
 		</el-form>
@@ -82,6 +82,8 @@ const form = reactive<DemandScheduleSaveInput>({
 const rules: FormRules = {
 	itemNum: [{ required: true, message: '请选择物料', trigger: 'change' }],
 	requestDate: [{ required: true, message: '请选择需求日期', trigger: 'change' }],
+	arrivalDate: [{ required: true, message: '请选择建议到货日期', trigger: 'change' }],
+	toSechedQty: [{ required: true, message: '请输入物料净需求数量', trigger: 'blur' }],
 };
 
 function onPickItem(v: ItemRow) {

+ 5 - 1
Web/src/views/aidop/s4/delivery/supplierShipmentForm.vue

@@ -320,7 +320,7 @@ function onPrintShippingNote() {
 }
 
 async function onSave() {
-	// 校验生产日期和生产批号必填
+	// 校验生产日期、生产批号和每箱数量必填
 	for (let i = 0; i < form.details.length; i++) {
 		const row = form.details[i]!;
 		if (!row.scrq?.trim()) {
@@ -331,6 +331,10 @@ async function onSave() {
 			ElMessage.warning(`第 ${i + 1} 行请填写生产批号`);
 			return;
 		}
+		if (!row.bzsl || Number(row.bzsl) <= 0) {
+			ElMessage.warning(`第 ${i + 1} 行请填写每箱数量`);
+			return;
+		}
 	}
 	// 校验送货数量不能超过待交数量
 	for (let i = 0; i < form.details.length; i++) {

BIN
doc/S1产销协同模块详细设计说明书.docx


BIN
doc/S2制造协同模块详细设计说明书.docx


BIN
doc/S3供应协同模块详细设计说明书.docx


BIN
doc/S4采购执行模块详细设计说明书.docx


+ 3 - 3
server/Admin.NET.Web.Entry/Admin.NET.Web.Entry.csproj

@@ -11,9 +11,9 @@
     <GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
     <Copyright>Admin.NET</Copyright>
     <Description>Admin.NET 通用权限开发平台</Description>
-    <AssemblyVersion>1.0.461</AssemblyVersion>
-    <FileVersion>1.0.461</FileVersion>
-    <Version>1.0.461</Version>
+    <AssemblyVersion>1.0.462</AssemblyVersion>
+    <FileVersion>1.0.462</FileVersion>
+    <Version>1.0.462</Version>
   </PropertyGroup>
 
   <ItemGroup>

+ 102 - 63
server/Plugins/Admin.NET.Plugin.AiDOP/Order/OrderDeliveryService.cs

@@ -317,7 +317,7 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
                 bce.bom_number AS BomNumber,
                 bce.model AS Model,
                 DATE_FORMAT(bce.kitting_time, '%Y-%m-%d') AS KittingTime,
-                bce.item_type AS Type,
+                bce.type AS Type,
                 NULL AS Unit,
                 bce.erp_cls_name AS ErpClsName,
                 bce.qty AS Qty,
@@ -333,7 +333,7 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
                 NULL AS SubcontractingQty,
                 NULL AS Backflush,
                 DATE_FORMAT(bce.satisfy_time, '%Y-%m-%d') AS SatisfyTime,
-                bce.have_ic_subs AS HaveIcSubs,
+                bce.haveicsubs AS HaveIcSubs,
                 bce.substitute_code AS SubstituteCode
             FROM b_bom_child_examine bce
             INNER JOIN b_examine_result ber ON ber.Id = bce.examine_id
@@ -354,50 +354,67 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
         return await _db.Ado.SqlQueryAsync<FlowMaterialRow>(sql, new { EntryId = entryId, TenantId = tenantId });
     }
 
-    // ── Tab5 采购订单(MySQL:经占用/工单关联到订单分录)──
+    // ── Tab5 采购订单(MySQL:参考工单执行跟踪,双路合并 PW + DO/PO)──
     private async Task<List<FlowPoRow>> QueryPo(long entryId)
     {
         var tenantId = Infrastructure.AidopTenantScope.ResolveOrThrow(_userManager);
         const string sql = """
             SELECT
-                ROW_NUMBER() OVER (ORDER BY t.PoBillNo) AS Sno,
-                t.PoBillNo,
-                t.SupplierNo,
-                t.SupplierName,
-                t.PoTotal,
-                t.PoPurchaser,
-                t.State,
-                t.PoSsendDate,
-                t.CreateTime
+                ROW_NUMBER() OVER (ORDER BY PoBillno) AS Sno,
+                PoBillno, SupplierNo, SupplierName, PoTotal, PoPurchaser, State, PoSsendDate, CreateTime
             FROM (
                 SELECT DISTINCT
-                    pm.po_billno AS PoBillNo,
-                    pm.supplier_no AS SupplierNo,
-                    pm.supplier_name AS SupplierName,
-                    CAST(pm.po_total AS DECIMAL(18, 2)) AS PoTotal,
-                    pm.po_purchaser AS PoPurchaser,
-                    pm.create_time AS CreateTime,
-                    pm.po_ssend_date AS PoSsendDate,
-                    CASE pm.state
-                        WHEN 0 THEN '新增'
-                        WHEN 1 THEN '审核中'
-                        WHEN 2 THEN '同意'
-                        WHEN 3 THEN '关闭'
-                    END AS State
-                FROM srm_po_main pm
-                LEFT JOIN srm_po_list pl ON pm.id = pl.po_id AND pl.tenant_id = pm.tenant_id
-                LEFT JOIN srm_po_occupy occ ON pl.Id = occ.polist_id AND CAST(pm.factory_id AS CHAR(64)) = CAST(occ.factory_id AS CHAR(64)) AND occ.tenant_id = pm.tenant_id
-                LEFT JOIN mes_morder mo ON mo.id = occ.morder_id AND mo.tenant_id = pm.tenant_id
-                LEFT JOIN mes_moentry me ON mo.id = me.moentry_moid AND me.tenant_id = pm.tenant_id
-                LEFT JOIN crm_seorderentry se ON me.soentry_id = se.id
-                WHERE se.id = @EntryId AND se.tenant_id = @TenantId AND pm.IsDeleted = 0
+                    pm.PurOrd AS PoBillno,
+                    IFNULL(TRIM(pm.Supp), '') AS SupplierNo,
+                    IFNULL(TRIM(s.SortName), '') AS SupplierName,
+                    CAST(IFNULL(det.PoTotal, 0) AS DECIMAL(18, 2)) AS PoTotal,
+                    IFNULL(TRIM(pm.Buyer), '') AS PoPurchaser,
+                    CASE
+                        WHEN IFNULL(LENGTH(pm.Status), 0) = 0 THEN '新增'
+                        WHEN pm.Status = 'R' THEN '新增'
+                        WHEN pm.Status = 'S' THEN '已下达'
+                        WHEN pm.Status = 'C' THEN '关闭'
+                        ELSE pm.Status
+                    END AS State,
+                    pm.OrdDate AS PoSsendDate,
+                    pm.CreateTime AS CreateTime
+                FROM PurOrdMaster pm
+                LEFT JOIN SuppMaster s ON pm.Supp = s.Supp AND s.tenant_id = pm.tenant_id
+                LEFT JOIN (
+                    SELECT PurOrdRecID,
+                           SUM(IFNULL(QtyOrded, 0) * IFNULL(PurCost, 0)) AS PoTotal
+                    FROM PurOrdDetail
+                    GROUP BY PurOrdRecID
+                ) det ON det.PurOrdRecID = pm.RecID
+                WHERE pm.tenant_id = @TenantId
+                  AND IFNULL(pm.IsActive, 0) <= 1
+                  AND (
+                      pm.WorkOrd IN (
+                          SELECT me.moentry_mono FROM mes_moentry me
+                          WHERE me.soentry_id = @EntryId AND me.tenant_id = @TenantId
+                      )
+                      OR pm.RecID IN (
+                          SELECT pd.PurOrdRecID
+                          FROM PurOrdDetail pd
+                          INNER JOIN srm_pr_main pr
+                              ON (pr.pr_billno = pd.Req OR pr.SAP_pr_billno = pd.Req)
+                              AND pr.tenant_id = pd.tenant_id
+                          WHERE pr.pr_mono IN (
+                              SELECT me.moentry_mono FROM mes_moentry me
+                              WHERE me.soentry_id = @EntryId AND me.tenant_id = @TenantId
+                          )
+                          AND pr.tenant_id = @TenantId
+                          AND IFNULL(pr.state, 0) <> 0
+                          AND IFNULL(pr.IsDeleted, 0) = 0
+                      )
+                  )
             ) AS t
-            ORDER BY t.PoBillNo
+            ORDER BY PoBillno
             """;
         return await _db.Ado.SqlQueryAsync<FlowPoRow>(sql, new { EntryId = entryId, TenantId = tenantId });
     }
 
-    // ── Tab5 采购申请(MySQL)──
+    // ── Tab5 采购申请(MySQL:参考工单执行跟踪,pr_mono 直连工单)──
     private async Task<List<FlowPrRow>> QueryPr(long entryId)
     {
         var tenantId = Infrastructure.AidopTenantScope.ResolveOrThrow(_userManager);
@@ -405,29 +422,31 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
             SELECT
                 ROW_NUMBER() OVER (ORDER BY pm.pr_billno) AS Sno,
                 pm.pr_billno AS PrBillNo,
-                ic.number AS Number,
-                pm.icitem_name AS IcItemName,
-                ic.model AS Model,
-                pm.pr_purchasenumber AS PrPurchaseNumber,
-                pm.pr_purchasename AS PrPurchaseName,
-                CAST(pm.pr_aqty AS DECIMAL(18, 2)) AS PrAQty,
+                IFNULL(ic.number, '') AS Number,
+                IFNULL(pm.icitem_name, '') AS IcItemName,
+                IFNULL(ic.model, '') AS Model,
+                IFNULL(pm.pr_purchasenumber, '') AS PrPurchaseNumber,
+                IFNULL(pm.pr_purchasename, '') AS PrPurchaseName,
+                CAST(IFNULL(pm.pr_aqty, 0) AS DECIMAL(18, 4)) AS PrAQty,
                 pm.pr_ssend_date AS PrSsendDate,
                 pm.pr_sarrive_date AS PrSarriveDate,
-                pm.pr_unit AS PrUnit,
-                pm.pr_purchaser AS PrPurchaser
+                IFNULL(pm.pr_unit, '') AS PrUnit,
+                IFNULL(pm.pr_purchaser, '') AS PrPurchaser
             FROM srm_pr_main pm
-            LEFT JOIN srm_po_occupy occ ON pm.Id = occ.polist_id AND CAST(pm.factory_id AS CHAR(64)) = CAST(occ.factory_id AS CHAR(64)) AND occ.tenant_id = pm.tenant_id
             LEFT JOIN ic_item ic ON pm.icitem_id = ic.Id
-            LEFT JOIN mes_morder mo ON mo.id = occ.morder_id AND mo.tenant_id = pm.tenant_id
-            LEFT JOIN mes_moentry me ON mo.id = me.moentry_moid AND me.tenant_id = pm.tenant_id
-            LEFT JOIN crm_seorderentry se ON me.soentry_id = se.id
-            WHERE se.id = @EntryId AND se.tenant_id = @TenantId AND pm.state <> 0 AND pm.IsDeleted = 0
+            WHERE pm.pr_mono IN (
+                SELECT me.moentry_mono FROM mes_moentry me
+                WHERE me.soentry_id = @EntryId AND me.tenant_id = @TenantId
+            )
+            AND pm.tenant_id = @TenantId
+            AND IFNULL(pm.state, 0) <> 0
+            AND pm.IsDeleted = 0
             ORDER BY pm.pr_billno
             """;
         return await _db.Ado.SqlQueryAsync<FlowPrRow>(sql, new { EntryId = entryId, TenantId = tenantId });
     }
 
-    // ── Tab5 物料交货单(MySQL:srm_polist_ds)──
+    // ── Tab5 物料交货单(MySQL:经采购订单关联到交货单 srm_polist_ds)──
     private async Task<List<FlowDeliveryScheduleRow>> QueryDeliverySchedule(long entryId)
     {
         var tenantId = Infrastructure.AidopTenantScope.ResolveOrThrow(_userManager);
@@ -446,27 +465,47 @@ public class OrderDeliveryService : IDynamicApiController, ITransient
                 t.SentQty,
                 t.RestQty
             FROM (
-                SELECT
-                    ds.dsnum AS DsNum,
+                SELECT DISTINCT
                     ds.ponumber AS PoNumber,
+                    ds.dsnum AS DsNum,
                     ds.itemnum AS ItemNum,
-                    im.Descr AS Descr,
+                    IFNULL(im.Descr, '') AS Descr,
                     ds.suppliercode AS SupplierCode,
                     ds.supplier AS Supplier,
                     ds.requestdate AS RequestDate,
                     ds.needdate AS NeedDate,
-                    ds.schedqty AS SchedQty,
-                    ds.sentqty AS SentQty,
-                    ds.restqty AS RestQty
-                FROM srm_po_main pm
-                LEFT JOIN srm_po_list pl ON pm.id = pl.po_id AND pl.tenant_id = pm.tenant_id
-                LEFT JOIN srm_po_occupy occ ON pl.Id = occ.polist_id AND CAST(pm.factory_id AS CHAR(64)) = CAST(occ.factory_id AS CHAR(64)) AND occ.tenant_id = pm.tenant_id
-                LEFT JOIN mes_morder mo ON mo.id = occ.morder_id AND mo.tenant_id = pm.tenant_id
-                LEFT JOIN mes_moentry me ON mo.id = me.moentry_moid AND me.tenant_id = pm.tenant_id
-                LEFT JOIN crm_seorderentry se ON me.soentry_id = se.id
-                LEFT JOIN srm_polist_ds ds ON pl.po_billno = ds.ponumber AND pl.ItemNum = ds.itemnum AND ds.tenant_id = pm.tenant_id
-                LEFT JOIN ItemMaster im ON ds.itemnum = im.ItemNum AND im.tenant_id = pm.tenant_id
-                WHERE se.id = @EntryId AND se.tenant_id = @TenantId AND pm.IsDeleted = 0
+                    CAST(IFNULL(ds.schedqty, 0) AS DECIMAL(18, 4)) AS SchedQty,
+                    CAST(IFNULL(ds.sentqty, 0) AS DECIMAL(18, 4)) AS SentQty,
+                    CAST(IFNULL(ds.restqty, 0) AS DECIMAL(18, 4)) AS RestQty
+                FROM PurOrdMaster pm
+                INNER JOIN srm_polist_ds ds
+                    ON pm.PurOrd = ds.ponumber
+                    AND ds.tenant_id = pm.tenant_id
+                LEFT JOIN ItemMaster im
+                    ON ds.itemnum = im.ItemNum
+                    AND (im.tenant_id = pm.tenant_id OR pm.tenant_id IS NULL)
+                WHERE pm.tenant_id = @TenantId
+                  AND IFNULL(pm.IsActive, 0) <= 1
+                  AND (
+                      pm.WorkOrd IN (
+                          SELECT me.moentry_mono FROM mes_moentry me
+                          WHERE me.soentry_id = @EntryId AND me.tenant_id = @TenantId
+                      )
+                      OR pm.RecID IN (
+                          SELECT pd.PurOrdRecID
+                          FROM PurOrdDetail pd
+                          INNER JOIN srm_pr_main pr
+                              ON (pr.pr_billno = pd.Req OR pr.SAP_pr_billno = pd.Req)
+                              AND pr.tenant_id = pd.tenant_id
+                          WHERE pr.pr_mono IN (
+                              SELECT me.moentry_mono FROM mes_moentry me
+                              WHERE me.soentry_id = @EntryId AND me.tenant_id = @TenantId
+                          )
+                          AND pr.tenant_id = @TenantId
+                          AND IFNULL(pr.state, 0) <> 0
+                          AND IFNULL(pr.IsDeleted, 0) = 0
+                      )
+                  )
             ) AS t
             ORDER BY t.DsNum
             """;

+ 4 - 3
server/Plugins/Admin.NET.Plugin.AiDOP/Production/ProductionScheduleGenerationService.cs

@@ -807,10 +807,11 @@ public class ProductionScheduleGenerationService : ITransient
                 """
                 SELECT Op, Line, Site, OpType
                 FROM ProdLineDetail
-                WHERE Part = @Part
+                WHERE Part = @Part AND tenant_id = @TenantId
                 """,
-                new SugarParameter("@Part", itemNum));
-            var fbMap = fallback.ToDictionary(f => f.Op, f => f);
+                new SugarParameter("@Part", itemNum),
+                new SugarParameter("@TenantId", tenantId));
+            var fbMap = fallback.GroupBy(f => f.Op).ToDictionary(g => g.Key, g => g.First());
             foreach (var r in routings)
             {
                 if (fbMap.TryGetValue(r.Op, out var fb))

+ 3 - 0
server/Plugins/Admin.NET.Plugin.AiDOP/Supply/DemandScheduleService.cs

@@ -116,6 +116,9 @@ public class DemandScheduleService : IDynamicApiController, ITransient
     public async Task<object> Save([FromBody] DemandScheduleSaveInput input)
     {
         if (string.IsNullOrWhiteSpace(input.ItemNum)) throw Oops.Oh("物料编号不能为空");
+        if (string.IsNullOrWhiteSpace(input.RequestDate)) throw Oops.Oh("需求日期不能为空");
+        if (string.IsNullOrWhiteSpace(input.ArrivalDate)) throw Oops.Oh("建议到货日期不能为空");
+        if (input.ToSechedQty is null) throw Oops.Oh("物料净需求数量不能为空");
 
         if (input.Id is null or 0)
         {

+ 1 - 1
server/Plugins/Admin.NET.Plugin.AiDOP/Supply/Dto/PurchaseRequestDto.cs

@@ -16,7 +16,7 @@ public class PurchaseRequestSaveInput
 {
     public long? Id { get; set; }
     public string? PrBillNo { get; set; }
-    public long? IcitemId { get; set; }
+    public string? IcitemId { get; set; }
     public string? IcitemName { get; set; }
     public string? PrUnit { get; set; }
     public string? PrPurchaseNumber { get; set; }

+ 7 - 6
server/Plugins/Admin.NET.Plugin.AiDOP/Supply/PurchaseRequestService.cs

@@ -65,7 +65,7 @@ public class PurchaseRequestService : IDynamicApiController, ITransient
         var orderBy = BuildOrderBy(input.SortField, input.SortOrder);
         var fromSql = $"""
             FROM srm_pr_main pm
-            LEFT JOIN ic_item ic ON pm.icitem_id=ic.Id AND ic.tenant_id=pm.tenant_id
+            LEFT JOIN ic_item ic ON pm.icitem_id=ic.Id
             LEFT JOIN srm_purchase pur ON pm.icitem_id=pur.icitem_id AND pm.pr_purchaseid = pur.supplier_id AND pur.tenant_id=pm.tenant_id
             LEFT JOIN ic_item_stock its ON ic.Id=its.icitem_id
             LEFT JOIN PurOrdDetail pod ON (pm.pr_billno=pod.Req OR pm.SAP_pr_billno=pod.Req) AND pod.tenant_id=pm.tenant_id
@@ -119,7 +119,7 @@ public class PurchaseRequestService : IDynamicApiController, ITransient
             SELECT
                 pm.id AS Id,
                 pm.pr_billno AS PrBillNo,
-                pm.icitem_id AS IcitemId,
+                CAST(pm.icitem_id AS CHAR(30)) AS IcitemId,
                 pm.icitem_name AS IcitemName,
                 pm.pr_unit AS PrUnit,
                 pm.pr_purchasenumber AS PrPurchaseNumber,
@@ -148,7 +148,8 @@ public class PurchaseRequestService : IDynamicApiController, ITransient
     [ApiDescriptionSettings(Name = "SavePurchaseRequest"), HttpPost("purchase-request/save")]
     public async Task<object> Save([FromBody] PurchaseRequestSaveInput input)
     {
-        if (!input.IcitemId.HasValue || input.IcitemId.Value <= 0) throw Oops.Oh("请选择物料");
+        // icitem_id 为雪花ID,前端以字符串传递避免 JS Number 精度丢失,这里解析回 long
+        if (!long.TryParse(input.IcitemId?.Trim(), out var icitemId) || icitemId <= 0) throw Oops.Oh("请选择物料");
         if (input.PrPurchaseId is null or <= 0) throw Oops.Oh("请选择供应商");
         if (!input.PrAqty.HasValue || input.PrAqty.Value <= 0) throw Oops.Oh("申请数量必须大于0");
 
@@ -187,7 +188,7 @@ public class PurchaseRequestService : IDynamicApiController, ITransient
                 new SugarParameter("@PrRqty", aqty),
                 new SugarParameter("@PrAqty", aqty),
                 new SugarParameter("@PrSqty", aqty),
-                new SugarParameter("@IcitemId", input.IcitemId!.Value),
+                new SugarParameter("@IcitemId", icitemId),
                 new SugarParameter("@IcitemName", input.IcitemName?.Trim()),
                 new SugarParameter("@PrSsendDate", prSsendDate),
                 new SugarParameter("@PrSarriveDate", prSarriveDate),
@@ -259,7 +260,7 @@ public class PurchaseRequestService : IDynamicApiController, ITransient
                 new SugarParameter("@PrRqty", aqty),
                 new SugarParameter("@PrAqty", aqty),
                 new SugarParameter("@PrSqty", aqty),
-                new SugarParameter("@IcitemId", input.IcitemId!.Value),
+                new SugarParameter("@IcitemId", icitemId),
                 new SugarParameter("@IcitemName", input.IcitemName?.Trim()),
                 new SugarParameter("@PrSsendDate", prSsendDate),
                 new SugarParameter("@PrSarriveDate", prSarriveDate),
@@ -363,7 +364,7 @@ public class PurchaseRequestService : IDynamicApiController, ITransient
     {
         public long Id { get; set; }
         public string? PrBillNo { get; set; }
-        public long? IcitemId { get; set; }
+        public string? IcitemId { get; set; }
         public string? IcitemName { get; set; }
         public string? PrUnit { get; set; }
         public string? PrPurchaseNumber { get; set; }

+ 4 - 4
server/Plugins/Admin.NET.Plugin.AiDOP/Universal/UniversalSourceListService.cs

@@ -57,15 +57,15 @@ public class UniversalSourceListService : IDynamicApiController, ITransient
         var orderBy = BuildOrderBy(input.SortField, input.SortOrder);
         var fromSql = $"""
             FROM srm_purchase sp
-            LEFT JOIN ic_item it ON sp.number = it.number
-            LEFT JOIN ItemMaster im ON it.number = im.ItemNum
+            LEFT JOIN ic_item it ON sp.number = it.number AND (it.tenant_id = sp.tenant_id OR it.tenant_id IS NULL)
+            LEFT JOIN ItemMaster im ON it.number = im.ItemNum AND (im.tenant_id = sp.tenant_id OR im.tenant_id IS NULL)
             WHERE {string.Join(" AND ", where)}
             """;
 
-        var total = await _db.Ado.GetIntAsync($"SELECT COUNT(1) {fromSql}", pars);
+        var total = await _db.Ado.GetIntAsync($"SELECT COUNT(DISTINCT sp.id) {fromSql}", pars);
         var list = await _db.Ado.SqlQueryAsync<UniversalSourceListOutput>(
             $"""
-            SELECT
+            SELECT DISTINCT
                 sp.id AS Id,
                 sp.tenant_id AS TenantId,
                 sp.factory_id AS FactoryId,