const fs = require('fs'); const path = require('path'); const root = path.resolve(__dirname, '..'); function read(relativePath) { return fs.readFileSync(path.join(root, relativePath), 'utf8'); } function assertContains(file, needle, message) { const content = read(file); if (!content.includes(needle)) { throw new Error(`${message}\nMissing in ${file}: ${needle}`); } } function assertMatches(file, pattern, message) { const content = read(file); if (!pattern.test(content)) { throw new Error(`${message}\nPattern not found in ${file}: ${pattern}`); } } assertMatches( 'ruoyi-admin/src/main/java/com/ruoyi/bas/stock/service/impl/BasStockServiceImpl.java', /"KC"\s*\+\s*STOCK_ID_FORMATTER\.format/, 'Manual stock creation must generate KC + millisecond timestamp IDs.' ); assertContains( 'sql/bas_stock_master_detail.sql', "in_type", 'Inbound completion stock generation must persist the real inbound type.' ); assertMatches( 'sql/bas_stock_master_detail.sql', /'KC'\s*\+\s*FORMAT\(@now,\s*'yyyyMMddHHmmssfff'\)/, 'Inbound completion procedure must generate KC + millisecond timestamp stock IDs.' ); assertContains( 'sql/bas_stock_master_detail.sql', "@stock_id + N'-' + CONVERT", 'Inbound completion stock detail IDs must use stock ID plus detail sequence.' ); assertContains( 'ruoyi-admin/src/main/resources/mapper/bas/BasStockDetailMapper.xml', 'bsd.product_id, bsd.product_name', 'Stock detail queries must keep material code and material name in separate columns.' ); assertContains( 'ruoyi-ui/src/views/bas/stock/index.vue', 'label="物料编码" align="center" prop="productId"', 'Stock detail UI must render material code from productId.' ); assertContains( 'ruoyi-ui/src/views/bas/stock/index.vue', 'label="物料名称" align="center" prop="productName"', 'Stock detail UI must render material name from productName.' ); const outJob = read('ruoyi-ui/src/views/out/job/index.vue'); if (!outJob.includes('prop="status"') || !outJob.includes('dict.type.mydo_job_status')) { throw new Error('Outbound job page must filter/render task status with mydo_job_status.'); } if (!outJob.includes('dict.type.out_job_type') || !outJob.includes("dicts: ['mydo_job_execution_phase', 'mydo_job_status', 'out_job_type']")) { throw new Error('Outbound job page must render task type with out_job_type.'); } if (!outJob.includes("parseTime(scope.row.createTime")) { throw new Error('Outbound job page must render createTime from createTime, not createDate.'); } if (!outJob.includes('this.detailQueryParams.status = row.status')) { throw new Error('Finished outbound jobs must pass parent status when loading child job details.'); } assertContains( 'ruoyi-ui/src/views/out/stockout/index.vue', 'selectedExitId', 'Outbound allocation UI must require an exit selection.' ); assertContains( 'ruoyi-ui/src/views/out/stockout/index.vue', 'listStationPoint', 'Outbound allocation UI must load warehouse exit/entry-exit station points.' ); assertContains( 'ruoyi-ui/src/views/out/stockout/index.vue', 'this.detailQueryParams.status = row.status', 'Finished outbound bills must pass the parent status when loading child details.' ); assertContains( 'ruoyi-admin/src/main/java/com/ruoyi/out/stockout/domain/OutStockoutManualAssignDTO.java', 'private String exitId;', 'Manual outbound allocation DTO must carry the selected exit.' ); assertContains( 'ruoyi-admin/src/main/resources/mapper/out/OutStockoutMapper.xml', '#{v_exit_id,mode=IN,jdbcType=VARCHAR}', 'Outbound allocation mapper must pass the selected exit to procedures.' ); assertContains( 'sql/out_stockout_flow.sql', '@v_exit_id varchar(80)', 'Outbound allocation procedures must accept a selected exit.' ); assertContains( 'sql/out_stockout_flow.sql', '@exit_id = @v_exit_id', 'Outbound allocation procedure must store the selected exit on generated jobs.' ); assertContains( 'ruoyi-admin/src/main/java/com/ruoyi/in/job/service/impl/MydoJobServiceImpl.java', 'requestWcsCancelBeforeManualCancel', 'Manual job cancellation must reserve a WCS cancellation check.' ); assertContains( 'pda/pages/warehouse/outbound/confirm.vue', '扫描托盘条码', 'PDA outbound confirmation must scan tray barcodes.' ); assertContains( 'pda/pages/warehouse/outbound/confirm.vue', 'materials', 'PDA outbound confirmation must render tray material details.' ); assertContains( 'pda/pages/warehouse/outbound/confirm.vue', 'v-model="item.actualQuantity"', 'PDA outbound confirmation must capture actual outbound quantity per material.' ); assertContains( 'pda/pages/warehouse/outbound/confirm.vue', 'materials: this.outboundInfo.materials.map', 'PDA outbound confirmation must submit actual quantities per material.' ); assertContains( 'ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java', 'createReturnInboundJobIfNeeded', 'PDA outbound confirmation must create return inbound jobs for residual tray materials.' ); assertContains( 'ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java', 'jobIdService.getId("HL")', 'Residual tray return jobs must use the 回流作业 job-id prefix.' ); assertContains( 'ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java', 'taskNoService.getTaskNo("5")', 'Residual tray return jobs must use the 回流入库 task-number type.' ); assertContains( 'sql/20260624_update_job_dicts.sql', "N'回流入库'", 'Inbound job dictionaries must render 回流入库 return jobs.' ); assertContains( 'ruoyi-admin/src/main/resources/mapper/pda/PdaMapper.xml', 'selectResidualTrayMaterials', 'PDA mapper must query residual tray materials after outbound confirmation.' ); assertContains( 'ruoyi-ui/src/views/out/locationOutbound/index.vue', 'active-text="所有货位"', 'Specified-location outbound filter switch must be labelled 所有货位.' ); assertContains( 'ruoyi-ui/src/views/out/locationOutbound/index.vue', 'listWarehouse', 'Specified-location outbound page must load warehouse dropdown options.' ); assertContains( 'ruoyi-ui/src/views/out/locationOutbound/index.vue', 'v-model="queryParams.warehouseId"', 'Specified-location outbound page must filter by warehouse id dropdown.' ); assertContains( 'ruoyi-ui/src/views/out/locationOutbound/index.vue', '@selection-change="handleSelectionChange"', 'Specified-location outbound table must support selecting locations.' ); assertContains( 'ruoyi-ui/src/views/out/locationOutbound/index.vue', '批量生成作业', 'Specified-location outbound page must provide a batch generation action.' ); assertContains( 'ruoyi-ui/src/views/out/locationOutbound/index.vue', 'Promise.all(rows.map', 'Specified-location outbound page must generate outbound jobs for selected locations.' ); const locationOutbound = read('ruoyi-ui/src/views/out/locationOutbound/index.vue'); if (locationOutbound.includes('type="expand"')) { throw new Error('Specified-location outbound page must use a lower child-detail table instead of expand rows.'); } if (!locationOutbound.includes('@row-click="handleRowClick"') || !locationOutbound.includes('currentLocation') || !locationOutbound.includes('locationDetailList')) { throw new Error('Specified-location outbound page must click a parent location row and show child details below.'); } if (!locationOutbound.includes('cargoSpaceId: row.cargoSpaceId')) { throw new Error('Specified-location outbound generation must send cargoSpaceId so empty locations can generate jobs.'); } if (!locationOutbound.includes("['1', '6'].includes(String(row.cargoSpaceStatus))")) { throw new Error('Specified-location outbound must allow empty and no-stock abnormal locations to generate jobs.'); } console.log('Outbound and stock regression checks passed.');