打通 T+ 快照、业务、发货、回款和核算准备链路,保留来源差异并提供待补状态。 新增业务自动接入任务、跨平台读取服务托管、权限复核、来源关联和前端核对页面。 补充迁移、隔离集成测试、浏览器模拟验收及角色授权边界修复。
134 lines
9.3 KiB
JavaScript
134 lines
9.3 KiB
JavaScript
/** 来源业务详情加载、错误及重试;接口为隔离模拟。 */
|
||
import assert from 'node:assert/strict'
|
||
import path from 'node:path'
|
||
import { createRequire } from 'node:module'
|
||
import { fileURLToPath } from 'node:url'
|
||
import vue from '@vitejs/plugin-vue'
|
||
import { createServer } from 'vite'
|
||
|
||
const require = createRequire(import.meta.url)
|
||
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'C:/Users/Administrator/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright')
|
||
const root = fileURLToPath(new URL('../', import.meta.url))
|
||
const id = 'virtual:source-documents-proof'
|
||
const harness = `import {createApp,h} from 'vue';import {createPinia} from 'pinia';import ElementPlus from 'element-plus';import 'element-plus/dist/index.css';import Source from '/src/views/business/businesses/SourceDocuments.vue';import {useUserStore} from '/src/store/user.ts';const pinia=createPinia();createApp({setup(){window.__sourceUser=useUserStore();window.__sourceUser.userInfo={permissions:['business:businesses:update','payroll:prepare']};return()=>h(Source,{businessId:12})}}).use(pinia).use(ElementPlus).mount('#app')`
|
||
const server = await createServer({ configFile: false, root,
|
||
resolve: { alias: { '@': path.join(root, 'src') } },
|
||
define: { 'import.meta.env.VITE_API_BASE_URL': JSON.stringify('/api') },
|
||
plugins: [vue(), { name: 'source-documents-proof', resolveId: value => value === id ? '\0' + id : undefined,
|
||
load: value => value === '\0' + id ? harness : undefined,
|
||
configureServer(instance) { instance.middlewares.use('/__source-proof', async (_request, response) => {
|
||
response.setHeader('Content-Type', 'text/html; charset=utf-8')
|
||
response.end(await instance.transformIndexHtml('/__source-proof', '<!doctype html><html lang="zh-CN"><meta charset="UTF-8"><div id="app"></div><script type="module" src="/@id/__x00__virtual:source-documents-proof"></script></html>'))
|
||
}) },
|
||
}], server: { host: '127.0.0.1', port: 0 } })
|
||
|
||
let browser, failNext = false, failDetail = false, failQuantity = false, repaired = false, repairRequests = 0
|
||
try {
|
||
await server.listen()
|
||
browser = await chromium.launch({ headless: true, channel: 'msedge' })
|
||
const page = await browser.newPage()
|
||
await page.route(url => url.pathname.startsWith('/api/'), async route => {
|
||
const url = new URL(route.request().url())
|
||
if (url.pathname.endsWith('/missing-price')) {
|
||
repairRequests++
|
||
const body = route.request().postDataJSON()
|
||
assert.equal(body.revision, 'a'.repeat(64))
|
||
assert.equal(body.evidence, '已核对原单')
|
||
repaired = true
|
||
return route.fulfill({ json: { code: 200, message: 'success', data: { document_id: 2, parent_status: 'APPLIED' } } })
|
||
}
|
||
if (url.pathname.endsWith('/source-quantities') && failQuantity) {
|
||
failQuantity = false
|
||
return route.fulfill({ status: 500, json: { code: 500, message: '加载失败' } })
|
||
}
|
||
if (url.pathname.endsWith('/details') && failDetail) {
|
||
failDetail = false
|
||
return route.fulfill({ status: 500, json: { code: 500, message: '加载失败' } })
|
||
}
|
||
if (url.pathname.endsWith('/source-documents') && failNext) {
|
||
failNext = false
|
||
return route.fulfill({ status: 500, json: { code: 500, message: '加载失败' } })
|
||
}
|
||
const data = url.pathname.endsWith('/details')
|
||
? { list: [
|
||
{ source_id: '12', state: 'CHANGED', before: { product_id: '9', quantity: '2', amount: '100', price: '50', price_type: '含税' }, after: { product_id: '9', quantity: '3', amount: '150', price: '50', price_type: '未税' }, changes: [{ field: 'quantity', label: '数量', before: '2', after: '3' }] },
|
||
{ source_id: '13', state: 'ADDED', before: null, after: { product_id: '9', quantity: '1', amount: '50', price: '10.12345', price_type: '未税' }, changes: [] },
|
||
], total: 2 }
|
||
: url.pathname.endsWith('/readiness')
|
||
? { status: 'PENDING', reasons: [], customer_type: 'personal', products: [], products_total: 0, eligible_receipts_snapshot: null }
|
||
: url.pathname.endsWith('/source-quantities')
|
||
? { list: [{ product_id: 9, product_name: '测试管材', spec_model: 'DN300', unit: '根', order_quantity: '10.00', shipment_quantity: '4.00' }], total: 1 }
|
||
: { list: [{ id: 1, entity: 'businesses', source_no: 'SO-001', current_source_no: 'SO-002', status: 'CONFLICT',
|
||
changes: [{ field: 'amount', label: '金额', before: '100', after: '200' }] },
|
||
{ id: 2, entity: 'shipment_items', source_no: 'L-1', snapshot_state: 'CHANGED', price: '10.1234', price_type: 'TAX_EXCLUDED', price_repair_revision: repaired ? null : 'a'.repeat(64), changes: [] },
|
||
{ id: 3, entity: 'shipment_items', source_no: 'L-2', snapshot_state: 'MISSING', price: '12.3400', price_type: 'TAX_INCLUDED', changes: [] },
|
||
{ id: 4, entity: 'shipment_items', source_no: 'L-3', snapshot_state: 'UNAVAILABLE', price: '5.00', changes: [] },
|
||
{ id: 5, entity: 'receipts', source_no: 'RC-001', status: 'CONFLICT', reason: 'PENDING_SOURCE_CUSTOMER_CHANGED',
|
||
changes: [{ field: 'customer_id', label: '客户标识', before: '8', after: '10' }] },
|
||
{ id: 6, entity: 'receipts', source_no: 'RC-002', status: 'CONFLICT', reason: 'PENDING_SOURCE_OUT_OF_RANGE',
|
||
changes: [{ field: 'date', label: '日期', before: '2026-03-14', after: '2026-02-28' }] }], total: 21 }
|
||
await route.fulfill({ json: { code: 200, message: 'success', data } })
|
||
})
|
||
await page.goto(`http://127.0.0.1:${server.httpServer.address().port}/__source-proof`)
|
||
await page.getByText('SO-001').waitFor()
|
||
await page.getByText('SO-002').waitFor()
|
||
await page.getByText('10.1234(未税)').waitFor()
|
||
await page.getByText('12.3400(含税)').waitFor()
|
||
await page.getByText('5.00(税别待确认)').waitFor()
|
||
await page.evaluate(() => { window.__sourceUser.userInfo.permissions = ['business:businesses:update'] })
|
||
assert.equal(await page.getByRole('button', { name: '核对单价' }).count(), 0)
|
||
await page.evaluate(() => { window.__sourceUser.userInfo.permissions = ['business:businesses:update', 'payroll:prepare'] })
|
||
await page.getByRole('button', { name: '核对单价' }).click()
|
||
await page.getByRole('button', { name: '取消' }).click()
|
||
assert.equal(repairRequests, 0)
|
||
await page.getByRole('button', { name: '核对单价' }).click()
|
||
await page.getByPlaceholder('填写核对依据').fill('已核对原单')
|
||
await page.getByRole('button', { name: '确认补齐' }).click()
|
||
await page.getByText('来源单价已补齐').waitFor()
|
||
assert.equal(repairRequests, 1)
|
||
const quantityRow = page.getByRole('row').filter({ hasText: '测试管材' })
|
||
await quantityRow.getByText('10.00', { exact: true }).waitFor()
|
||
await quantityRow.getByText('4.00', { exact: true }).waitFor()
|
||
await page.getByText('金额:100 → 200').waitFor()
|
||
await page.getByText('待核对单据的来源客户已变化,原关联已保留,请核对原单').waitFor()
|
||
await page.getByText('客户标识:8 → 10').waitFor()
|
||
await page.getByText('待核对单据的来源日期已早于接入起点,原关联已保留,请核对原单').waitFor()
|
||
await page.getByText('日期:2026-03-14 → 2026-02-28').waitFor()
|
||
failQuantity = true
|
||
await page.reload()
|
||
await page.getByText('SO-001').waitFor()
|
||
await page.locator('.quantity-section').getByText('列表加载失败,请重试').waitFor()
|
||
await page.locator('.quantity-section').getByRole('button', { name: '重新加载' }).click()
|
||
await page.getByRole('row').filter({ hasText: '测试管材' }).getByText('10.00', { exact: true }).waitFor()
|
||
await page.getByText('来源明细已变化,待核对').waitFor()
|
||
await page.getByText('最新完整快照中没有此明细').waitFor()
|
||
await page.getByText('完整快照不可用,显示已接入明细').waitFor()
|
||
await page.getByRole('button', { name: '查看明细' }).first().click()
|
||
const dialog = page.getByRole('dialog', { name: '来源明细对照' })
|
||
await dialog.getByText('数量:2 → 3').waitFor()
|
||
await dialog.getByText('产品 9,数量 1,单价 10.12345(未税),金额 50').waitFor()
|
||
await dialog.getByText('新增', { exact: true }).waitFor()
|
||
await dialog.locator('.el-dialog__headerbtn').click()
|
||
failDetail = true
|
||
await page.setViewportSize({ width: 375, height: 800 })
|
||
await page.getByText('客户标识:8 → 10').waitFor()
|
||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), true)
|
||
await page.getByRole('button', { name: '查看明细' }).first().click()
|
||
await dialog.getByText('列表加载失败,请重试').waitFor()
|
||
await dialog.getByRole('button', { name: '重新加载' }).click()
|
||
await dialog.getByText('数量:2 → 3').waitFor()
|
||
await dialog.locator('.el-dialog__headerbtn').click()
|
||
assert.match(await page.locator('.source-section').innerText(), /可参与平账回款.*待确认/)
|
||
failNext = true
|
||
await page.locator('.el-pagination button.btn-next').click()
|
||
await page.getByText('列表加载失败,请重试').waitFor()
|
||
assert.equal(await page.getByText('SO-001').count(), 0)
|
||
await page.getByRole('button', { name: '重新加载' }).click()
|
||
await page.getByText('SO-001').waitFor()
|
||
assert.equal(await page.getByText('列表加载失败,请重试').count(), 0)
|
||
process.stdout.write('来源详情加载失败、清除旧数据和重试通过。\n')
|
||
} finally {
|
||
await browser?.close()
|
||
await server.close()
|
||
}
|