@things-factory/erp-systemever 10.1.117 → 10.1.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist-server/index.d.ts +8 -0
- package/dist-server/index.js +9 -4
- package/dist-server/index.js.map +1 -1
- package/dist-server/systemever-adapter.js +12 -20
- package/dist-server/systemever-adapter.js.map +1 -1
- package/dist-server/systemever-connector.d.ts +1 -0
- package/dist-server/systemever-connector.js +88 -0
- package/dist-server/systemever-connector.js.map +1 -0
- package/dist-server/systemever-query-policy.d.ts +30 -0
- package/dist-server/systemever-query-policy.js +37 -0
- package/dist-server/systemever-query-policy.js.map +1 -0
- package/dist-server/systemever-query-response.d.ts +2 -0
- package/dist-server/systemever-query-response.js +26 -0
- package/dist-server/systemever-query-response.js.map +1 -0
- package/dist-server/systemever-query-runner.d.ts +16 -0
- package/dist-server/systemever-query-runner.js +90 -0
- package/dist-server/systemever-query-runner.js.map +1 -0
- package/dist-server/systemever-query-task.d.ts +7 -0
- package/dist-server/systemever-query-task.js +65 -0
- package/dist-server/systemever-query-task.js.map +1 -0
- package/dist-server/systemever-schedule-task.d.ts +1 -0
- package/dist-server/systemever-schedule-task.js +35 -0
- package/dist-server/systemever-schedule-task.js.map +1 -0
- package/dist-server/systemever-work-order-query.d.ts +14 -0
- package/dist-server/systemever-work-order-query.js +157 -0
- package/dist-server/systemever-work-order-query.js.map +1 -0
- package/dist-server/systemever-work-order.d.ts +33 -0
- package/dist-server/systemever-work-order.js +160 -0
- package/dist-server/systemever-work-order.js.map +1 -0
- package/logs/.5e5d741d8b7784a2fbad65eedc0fd46946aaf6f2-audit.json +15 -0
- package/logs/connections-2026-10-05-11.log +0 -0
- package/package.json +5 -3
- package/server/index.ts +8 -0
- package/server/systemever-connector.ts +88 -0
- package/server/systemever-query-policy.ts +47 -0
- package/server/systemever-query-response.ts +29 -0
- package/server/systemever-query-runner.ts +79 -0
- package/server/systemever-query-task.ts +62 -0
- package/server/systemever-schedule-task.ts +37 -0
- package/server/systemever-work-order-query.ts +157 -0
- package/server/systemever-work-order.ts +184 -0
- package/test/fixtures/work-order-query-request.json +153 -0
- package/test/fixtures/work-order-query.json +239 -0
- package/test/systemever-connector.test.ts +84 -0
- package/test/systemever-query-policy.test.ts +41 -0
- package/test/systemever-query-response.test.ts +37 -0
- package/test/systemever-query-runner.test.ts +73 -0
- package/test/systemever-query-task.test.ts +106 -0
- package/test/systemever-schedule-task.test.ts +46 -0
- package/test/systemever-work-order.test.ts +101 -0
- package/tsconfig.json +2 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import integration from '@things-factory/integration-base'
|
|
2
|
+
import { SystemEverQueryError } from './systemever-query-policy.js'
|
|
3
|
+
const { ConnectionManager, HeadlessConnector, lookupDomainEnvVar } = integration
|
|
4
|
+
|
|
5
|
+
/** Domain-isolated cookie reuse; one initial login and at most one login retry. */
|
|
6
|
+
class SystemEverConnector extends HeadlessConnector {
|
|
7
|
+
get taskPrefixes() { return ['systemever-query'] }
|
|
8
|
+
get description() { return 'SystemEver read-only browser connection' }
|
|
9
|
+
get parameterSpec() {
|
|
10
|
+
return super.parameterSpec.filter(parameter => ![
|
|
11
|
+
'username', 'password', 'loginPagePath', 'loginApiUrl', 'forceLogin', 'retries'
|
|
12
|
+
].includes(parameter.name))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async connect(connection: any) {
|
|
16
|
+
try {
|
|
17
|
+
const endpoint = new URL(connection.endpoint)
|
|
18
|
+
if (endpoint.origin !== 'https://mfg.systemevererp.com' || endpoint.username || endpoint.password ||
|
|
19
|
+
endpoint.pathname !== '/' || endpoint.search || endpoint.hash) {
|
|
20
|
+
throw new SystemEverQueryError('systemever-query-invalid-connection')
|
|
21
|
+
}
|
|
22
|
+
const [username, password] = await Promise.all([
|
|
23
|
+
lookupDomainEnvVar(connection.domain, 'ErpConnection::systemever::username'),
|
|
24
|
+
lookupDomainEnvVar(connection.domain, 'ErpConnection::systemever::password')
|
|
25
|
+
])
|
|
26
|
+
if (!username || !password) {
|
|
27
|
+
throw new SystemEverQueryError('systemever-query-missing-credentials')
|
|
28
|
+
}
|
|
29
|
+
connection.params = { ...connection.params, username, password, retries: 2, forceLogin: false,
|
|
30
|
+
loginPagePath: '/Home/Oauth2OpenIdSignIn', loginApiUrl: null }
|
|
31
|
+
await super.connect(connection)
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error instanceof SystemEverQueryError) throw error
|
|
34
|
+
throw new SystemEverQueryError('systemever-query-connection-failed')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
protected isCookieValid(cookies: any[]) {
|
|
39
|
+
const now = Date.now() / 1000
|
|
40
|
+
return Array.isArray(cookies) && cookies.some(cookie => cookie.expires === -1 || cookie.expires > now)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async setupPage(page: any, uri: string, timeout: number) {
|
|
44
|
+
await super.setupPage(page, uri, timeout)
|
|
45
|
+
// Vendor console/request messages may contain session values. Do not forward them to logs.
|
|
46
|
+
page.removeAllListeners('console')
|
|
47
|
+
page.removeAllListeners('requestfailed')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async applyCookiesAndVerifySession(page: any, cookies: any[], loginInfo: any) {
|
|
51
|
+
await page.setCookie(...cookies)
|
|
52
|
+
await page.reload({ waitUntil: 'networkidle2', timeout: loginInfo.timeout })
|
|
53
|
+
const loggedIn = await page.evaluate(() => !!(window as any).Home?._base?.ConnInfo?.LoginSeq)
|
|
54
|
+
if (!loggedIn) await this.performLogin(page, '', loginInfo)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async performLogin(page: any, _uri: string, loginInfo: any) {
|
|
58
|
+
if (!loginInfo.username || !loginInfo.password) {
|
|
59
|
+
throw new SystemEverQueryError('systemever-query-missing-credentials')
|
|
60
|
+
}
|
|
61
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
62
|
+
try {
|
|
63
|
+
await page.goto('https://mfg.systemevererp.com/Home/Oauth2OpenIdSignIn', {
|
|
64
|
+
waitUntil: 'networkidle2', timeout: loginInfo.timeout
|
|
65
|
+
})
|
|
66
|
+
if (new URL(page.url()).origin !== 'https://mfg.systemevererp.com') {
|
|
67
|
+
throw new SystemEverQueryError('systemever-query-login-origin-mismatch')
|
|
68
|
+
}
|
|
69
|
+
const selectors = loginInfo.loginSelectors
|
|
70
|
+
const username = await this.resolveShadowDom(page, selectors.shadowDomSelectors, selectors.usernameSelector)
|
|
71
|
+
const password = await this.resolveShadowDom(page, selectors.shadowDomSelectors, selectors.passwordSelector)
|
|
72
|
+
const submit = await this.resolveShadowDom(page, selectors.shadowDomSelectors, selectors.submitSelector)
|
|
73
|
+
if (!username || !password || !submit) throw new Error('login-form-missing')
|
|
74
|
+
await username.type(loginInfo.username)
|
|
75
|
+
await password.type(loginInfo.password)
|
|
76
|
+
await submit.click()
|
|
77
|
+
await page.waitForFunction(() => !!(window as any).Home?._base?.ConnInfo?.LoginSeq, {
|
|
78
|
+
timeout: loginInfo.timeout
|
|
79
|
+
})
|
|
80
|
+
return
|
|
81
|
+
} catch {
|
|
82
|
+
if (attempt === 1) throw new SystemEverQueryError('systemever-query-login-failed')
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
ConnectionManager.registerConnector('systemever-connector', new SystemEverConnector())
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** ADR-0110: only verified read methods belong in the headless query task. */
|
|
2
|
+
const screens = Object.freeze({
|
|
3
|
+
'work-order': Object.freeze({
|
|
4
|
+
pgmSeq: 500880,
|
|
5
|
+
serviceSeq: 501041,
|
|
6
|
+
methods: Object.freeze([
|
|
7
|
+
Object.freeze({ name: 'ConfirmQuery', methodSeq: 6 }),
|
|
8
|
+
Object.freeze({ name: 'Query', methodSeq: 1 })
|
|
9
|
+
])
|
|
10
|
+
})
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export class SystemEverQueryError extends Error {
|
|
14
|
+
readonly code: string
|
|
15
|
+
|
|
16
|
+
constructor(code: string, cause?: unknown) {
|
|
17
|
+
// Do not include input values: they may contain credentials or script text. A cause is named, not quoted —
|
|
18
|
+
// its message may carry the same values — and kept whole on `cause` for whoever reads the log.
|
|
19
|
+
const named = cause && typeof cause === 'object' ? String((cause as any).code || (cause as any).name || '') : ''
|
|
20
|
+
super(named ? `${code} (cause: ${named})` : code, cause === undefined ? undefined : { cause })
|
|
21
|
+
this.name = 'SystemEverQueryError'
|
|
22
|
+
this.code = code
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SystemEverQueryPlan {
|
|
27
|
+
screen: keyof typeof screens
|
|
28
|
+
pgmSeq: number
|
|
29
|
+
serviceSeq: number
|
|
30
|
+
methods: readonly { readonly name: string; readonly methodSeq: number }[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** No caller-provided service, method, URL or executable code is accepted. */
|
|
34
|
+
export function planSystemEverQuery(input: unknown): SystemEverQueryPlan {
|
|
35
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
36
|
+
throw new SystemEverQueryError('systemever-query-invalid-params')
|
|
37
|
+
}
|
|
38
|
+
const params = input as Record<string, unknown>
|
|
39
|
+
if (Object.keys(params).some(key => key !== 'screen')) {
|
|
40
|
+
throw new SystemEverQueryError('systemever-query-unsupported-param')
|
|
41
|
+
}
|
|
42
|
+
if (typeof params.screen !== 'string' || !Object.hasOwn(screens, params.screen)) {
|
|
43
|
+
throw new SystemEverQueryError('systemever-query-unknown-screen')
|
|
44
|
+
}
|
|
45
|
+
const screen = params.screen as keyof typeof screens
|
|
46
|
+
return Object.freeze({ screen, ...screens[screen] })
|
|
47
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SystemEverQueryError } from './systemever-query-policy.js'
|
|
2
|
+
|
|
3
|
+
/** Decode a single named query block; never interpret a missing block as an empty result. */
|
|
4
|
+
export function decodeSystemEverQueryBlock(
|
|
5
|
+
response: unknown,
|
|
6
|
+
tableName: string,
|
|
7
|
+
requiredColumns: readonly string[]
|
|
8
|
+
): Record<string, unknown>[] {
|
|
9
|
+
const tables = (response as any)?.JSonData?.Tables
|
|
10
|
+
if (!Array.isArray(tables)) throw new SystemEverQueryError('systemever-query-invalid-response')
|
|
11
|
+
const matches = tables.filter(table => table?.TableName === tableName)
|
|
12
|
+
if (matches.length !== 1) throw new SystemEverQueryError('systemever-query-block-mismatch')
|
|
13
|
+
const { Columns: columns, Rows: rows } = matches[0]
|
|
14
|
+
if (
|
|
15
|
+
!Array.isArray(columns) ||
|
|
16
|
+
columns.some(column => typeof column !== 'string' || !column) ||
|
|
17
|
+
new Set(columns).size !== columns.length ||
|
|
18
|
+
!Array.isArray(rows)
|
|
19
|
+
) throw new SystemEverQueryError('systemever-query-invalid-table')
|
|
20
|
+
if (requiredColumns.some(column => !columns.includes(column))) {
|
|
21
|
+
throw new SystemEverQueryError('systemever-query-missing-columns')
|
|
22
|
+
}
|
|
23
|
+
return rows.map(row => {
|
|
24
|
+
if (!Array.isArray(row) || row.length !== columns.length) {
|
|
25
|
+
throw new SystemEverQueryError('systemever-query-invalid-row')
|
|
26
|
+
}
|
|
27
|
+
return Object.fromEntries(columns.map((column, index) => [column, row[index]]))
|
|
28
|
+
})
|
|
29
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { planSystemEverQuery, SystemEverQueryError } from './systemever-query-policy.js'
|
|
2
|
+
import { workOrderQueryData } from './systemever-work-order-query.js'
|
|
3
|
+
import { decodeSystemEverQueryBlock } from './systemever-query-response.js'
|
|
4
|
+
import { WORK_ORDER_COLUMNS } from './systemever-work-order.js'
|
|
5
|
+
|
|
6
|
+
/** Reject invalid task inputs before acquiring a browser or authenticating. */
|
|
7
|
+
export function prepareSystemEverQuery(input: unknown) {
|
|
8
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
9
|
+
throw new SystemEverQueryError('systemever-query-invalid-params')
|
|
10
|
+
}
|
|
11
|
+
const params = input as Record<string, any>
|
|
12
|
+
if (Object.keys(params).some(key => !['screen', 'dateFrom', 'dateTo', 'bizUnit'].includes(key))) {
|
|
13
|
+
throw new SystemEverQueryError('systemever-query-unsupported-param')
|
|
14
|
+
}
|
|
15
|
+
const plan = planSystemEverQuery({ screen: params.screen })
|
|
16
|
+
const data = workOrderQueryData(params as any)
|
|
17
|
+
return { plan, data }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Runs only the fixed vendor query methods inside an authenticated Puppeteer page. */
|
|
21
|
+
export async function runSystemEverQuery(page: any, input: unknown) {
|
|
22
|
+
const { plan, data } = prepareSystemEverQuery(input)
|
|
23
|
+
try {
|
|
24
|
+
if (new URL(page.url()).origin !== 'https://mfg.systemevererp.com') {
|
|
25
|
+
throw new SystemEverQueryError('systemever-query-unexpected-origin')
|
|
26
|
+
}
|
|
27
|
+
const result = await page.evaluate(async ({ data, methods }) => {
|
|
28
|
+
const home = (window as any).Home
|
|
29
|
+
if (!home?._base?.ConnInfo?.LoginSeq) return { code: 'systemever-query-session-expired' }
|
|
30
|
+
home.OpenPageSeq(500880)
|
|
31
|
+
let token: string | undefined
|
|
32
|
+
const deadline = Date.now() + 30000
|
|
33
|
+
while (!token && Date.now() < deadline) {
|
|
34
|
+
const frame = document.querySelector<HTMLIFrameElement>('iframe[id="500880_iframe"]')
|
|
35
|
+
token = frame?.contentDocument?.querySelector<HTMLInputElement>('#__PageVerificationToken')?.value
|
|
36
|
+
if (!token) await new Promise(resolve => setTimeout(resolve, 100))
|
|
37
|
+
}
|
|
38
|
+
if (!token) return { code: 'systemever-query-frame-timeout' }
|
|
39
|
+
let response: any
|
|
40
|
+
for (const method of methods) {
|
|
41
|
+
const message = new home.Const.Message()
|
|
42
|
+
message.Seq = home._base.ConnInfo.LoginSeq
|
|
43
|
+
message.PageToken = token
|
|
44
|
+
message.Action = 228002
|
|
45
|
+
message.ActionType = 0
|
|
46
|
+
message.PgmMethodName = 'Query'
|
|
47
|
+
message.ServiceSeq = 501041
|
|
48
|
+
message.MethodSeq = method.methodSeq
|
|
49
|
+
Object.assign(message.Option, { PgmSeq: 500880, Timeout: 180, PgmMethodSeq: 2, XmlFlags: '2' })
|
|
50
|
+
message.JSonData = structuredClone(data)
|
|
51
|
+
response = await new Promise<any>(resolve => {
|
|
52
|
+
const timer = setTimeout(() => resolve(null), 190000)
|
|
53
|
+
const finish = (value: any) => { clearTimeout(timer); resolve(value) }
|
|
54
|
+
try { home.Util.Ajax('', message, 'POST', finish, () => finish(null), 'json', true) }
|
|
55
|
+
catch { finish(null) }
|
|
56
|
+
})
|
|
57
|
+
if (!response) return { code: 'systemever-query-request-failed' }
|
|
58
|
+
if (Number(response.ExeMsg?.ErrorSeq || 0) !== 0 || response.ExeMsg?.IsSystemError === true ||
|
|
59
|
+
(response.ExeMsg?.ErrStatus != null && Number(response.ExeMsg.ErrStatus) !== 0)) {
|
|
60
|
+
return { code: 'systemever-query-vendor-error' }
|
|
61
|
+
}
|
|
62
|
+
if (method.methodSeq === 6 && response.JSonData?.Tables?.some((table: any) => {
|
|
63
|
+
const status = table.Columns?.indexOf('Status') ?? -1
|
|
64
|
+
return status >= 0 && table.Rows?.some((row: any[]) => Number(row[status] || 0) !== 0)
|
|
65
|
+
})) return { code: 'systemever-query-vendor-error' }
|
|
66
|
+
}
|
|
67
|
+
// Project only tabular data: no session values or vendor object methods leave the page.
|
|
68
|
+
return { response: { JSonData: { Tables: response.JSonData?.Tables?.map((table: any) => ({
|
|
69
|
+
TableName: table.TableName, Columns: table.Columns, Rows: table.Rows
|
|
70
|
+
})) } } }
|
|
71
|
+
}, { data, methods: plan.methods })
|
|
72
|
+
if (result.code) throw new SystemEverQueryError(result.code)
|
|
73
|
+
return decodeSystemEverQueryBlock(result.response, 'DataBlock1', WORK_ORDER_COLUMNS)
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error instanceof SystemEverQueryError) throw error
|
|
76
|
+
// Browser exceptions can contain URLs, scripts and authentication values.
|
|
77
|
+
throw new SystemEverQueryError('systemever-query-browser-failed')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import integration from '@things-factory/integration-base'
|
|
2
|
+
import { prepareSystemEverQuery, runSystemEverQuery } from './systemever-query-runner.js'
|
|
3
|
+
import { SystemEverQueryError } from './systemever-query-policy.js'
|
|
4
|
+
const { ConnectionManager, TaskRegistry } = integration
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The least time between two queries of one screen over one connection in one domain — a person does not press
|
|
8
|
+
* 조회 more often than this, and a schedule must not either (ADR-0110, ruling 2026-10-05, condition 3).
|
|
9
|
+
*/
|
|
10
|
+
export const SYSTEMEVER_QUERY_FLOOR_MS = 60_000
|
|
11
|
+
|
|
12
|
+
/** When each (domain, connection, screen) last started a query, in this process. */
|
|
13
|
+
export const lastQueryStartedAt = new Map<string, number>()
|
|
14
|
+
|
|
15
|
+
/** Refusals that are not ours to rename: they already say what happened and where (e.g. integration-runtime-off). */
|
|
16
|
+
function isNamedPlatformRefusal(error: unknown): boolean {
|
|
17
|
+
const code = (error as any)?.code
|
|
18
|
+
return typeof code === 'string' && code.startsWith('integration-')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function SystemEverQuery(step: any, { domain }: any) {
|
|
22
|
+
try {
|
|
23
|
+
prepareSystemEverQuery(step.params)
|
|
24
|
+
const key = `${domain?.id}:${step.connection}:${step.params?.screen}`
|
|
25
|
+
const now = Date.now()
|
|
26
|
+
const last = lastQueryStartedAt.get(key)
|
|
27
|
+
if (last !== undefined && now - last < SYSTEMEVER_QUERY_FLOOR_MS) {
|
|
28
|
+
throw new SystemEverQueryError('systemever-query-too-frequent')
|
|
29
|
+
}
|
|
30
|
+
lastQueryStartedAt.set(key, now)
|
|
31
|
+
const entity = await ConnectionManager.getConnectionEntityByName(domain, step.connection)
|
|
32
|
+
if (!entity || entity.type !== 'systemever-connector' ||
|
|
33
|
+
new URL(entity.endpoint).origin !== 'https://mfg.systemevererp.com') {
|
|
34
|
+
throw new SystemEverQueryError('systemever-query-invalid-connection')
|
|
35
|
+
}
|
|
36
|
+
const instance = await ConnectionManager.getConnectionInstanceByName(domain, step.connection)
|
|
37
|
+
const connection = { ...instance, type: entity.type, name: entity.name, domain }
|
|
38
|
+
const data = await ConnectionManager.withSession(connection, async page => {
|
|
39
|
+
try {
|
|
40
|
+
return await runSystemEverQuery(page, step.params)
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (!(error instanceof SystemEverQueryError) || error.code !== 'systemever-query-session-expired') throw error
|
|
43
|
+
const recovered = await connection.reAuthenticateSession()
|
|
44
|
+
try { return await runSystemEverQuery(recovered.page, step.params) }
|
|
45
|
+
finally { await connection.releasePage(recovered.page) }
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
return { data }
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (error instanceof SystemEverQueryError || isNamedPlatformRefusal(error)) throw error
|
|
51
|
+
throw new SystemEverQueryError('systemever-query-connection-failed', error)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
SystemEverQuery.parameterSpec = [
|
|
56
|
+
{ type: 'select', name: 'screen', label: 'screen', property: { options: [{ display: '작업지시조회', value: 'work-order' }] } },
|
|
57
|
+
{ type: 'string', name: 'dateFrom', label: 'date-from' },
|
|
58
|
+
{ type: 'string', name: 'dateTo', label: 'date-to' },
|
|
59
|
+
{ type: 'string', name: 'bizUnit', label: 'business-unit' }
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
TaskRegistry.registerTaskHandler('systemever-query', SystemEverQuery)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import integration from '@things-factory/integration-base'
|
|
2
|
+
import utils from '@things-factory/utils'
|
|
3
|
+
|
|
4
|
+
import { SystemEverQueryError } from './systemever-query-policy.js'
|
|
5
|
+
import { toScheduleEntries } from './systemever-work-order.js'
|
|
6
|
+
|
|
7
|
+
const { TaskRegistry } = integration
|
|
8
|
+
const { access } = utils
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* **SystemEver work-order rows → contract schedule entries**, as a scenario step (ADR-0110 addition 1 amended).
|
|
12
|
+
*
|
|
13
|
+
* `systemever-query` returns the 작업지시조회 rows with SystemEver's column names; this step maps them with the same
|
|
14
|
+
* `toScheduleEntries` the adapter would use, so the plant task that comes next receives only the contract's
|
|
15
|
+
* `ProductionScheduleEntry` and knows nothing of SystemEver. Orders it cannot map are passed on as `skipped`,
|
|
16
|
+
* each with its reason — none is dropped without a word.
|
|
17
|
+
*/
|
|
18
|
+
async function SystemEverWorkOrdersToSchedule(step: any, { data }: any) {
|
|
19
|
+
const { accessor, timeZone } = step.params || {}
|
|
20
|
+
const rows = accessor ? access(accessor, data) : undefined
|
|
21
|
+
if (!Array.isArray(rows)) throw new SystemEverQueryError('systemever-schedule-invalid-rows')
|
|
22
|
+
if (typeof timeZone !== 'string' || !timeZone.trim()) throw new SystemEverQueryError('systemever-schedule-invalid-time-zone')
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
return { data: toScheduleEntries(rows, { timeZone }) }
|
|
26
|
+
} catch (error) {
|
|
27
|
+
/* toScheduleEntries throws only for the zone: missing or unknown. */
|
|
28
|
+
throw new SystemEverQueryError('systemever-schedule-invalid-time-zone', error)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
SystemEverWorkOrdersToSchedule.parameterSpec = [
|
|
33
|
+
{ type: 'scenario-step-input', name: 'accessor', label: 'accessor' },
|
|
34
|
+
{ type: 'string', name: 'timeZone', label: 'time-zone' }
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
TaskRegistry.registerTaskHandler('systemever-work-orders-to-schedule', SystemEverWorkOrdersToSchedule)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { SystemEverQueryError } from './systemever-query-policy.js'
|
|
2
|
+
|
|
3
|
+
// Captured from a read-only work-order query on 2026-10-05. No session values.
|
|
4
|
+
const template = {
|
|
5
|
+
"TableName": "DataBlock1",
|
|
6
|
+
"Columns": [
|
|
7
|
+
"WorkingTag",
|
|
8
|
+
"IDX_NO",
|
|
9
|
+
"DataSeq",
|
|
10
|
+
"Status",
|
|
11
|
+
"Selected",
|
|
12
|
+
"IsChangedMst",
|
|
13
|
+
"TABLE_NAME",
|
|
14
|
+
"IsSum",
|
|
15
|
+
"ItemClassSSeq",
|
|
16
|
+
"ItemClassSName",
|
|
17
|
+
"ItemClassLSeq",
|
|
18
|
+
"ItemClassLName",
|
|
19
|
+
"ItemClassMSeq",
|
|
20
|
+
"BizUnit",
|
|
21
|
+
"BizUnitName",
|
|
22
|
+
"WorkDateFr",
|
|
23
|
+
"WorkDateTo",
|
|
24
|
+
"WorkOrderNo",
|
|
25
|
+
"ProgStatus",
|
|
26
|
+
"ProgStatusName",
|
|
27
|
+
"WorkCenterSeq",
|
|
28
|
+
"WorkCenterName",
|
|
29
|
+
"DeptSeq",
|
|
30
|
+
"DeptName",
|
|
31
|
+
"ProcSeq",
|
|
32
|
+
"ProcName",
|
|
33
|
+
"WorkType",
|
|
34
|
+
"WorkTypeName",
|
|
35
|
+
"GoodItemName",
|
|
36
|
+
"GoodItemNo",
|
|
37
|
+
"GoodItemSpec",
|
|
38
|
+
"WorkOrderDateFr",
|
|
39
|
+
"WorkOrderDateTo",
|
|
40
|
+
"FileSeq",
|
|
41
|
+
"ProdPlanNo",
|
|
42
|
+
"OrderNo",
|
|
43
|
+
"CustSeq",
|
|
44
|
+
"CustName",
|
|
45
|
+
"WHSeq",
|
|
46
|
+
"WHName",
|
|
47
|
+
"PoNo"
|
|
48
|
+
],
|
|
49
|
+
"ColumnsType": [
|
|
50
|
+
0,
|
|
51
|
+
1,
|
|
52
|
+
1,
|
|
53
|
+
1,
|
|
54
|
+
1,
|
|
55
|
+
0,
|
|
56
|
+
0,
|
|
57
|
+
0,
|
|
58
|
+
0,
|
|
59
|
+
0,
|
|
60
|
+
0,
|
|
61
|
+
0,
|
|
62
|
+
0,
|
|
63
|
+
0,
|
|
64
|
+
0,
|
|
65
|
+
0,
|
|
66
|
+
0,
|
|
67
|
+
0,
|
|
68
|
+
0,
|
|
69
|
+
0,
|
|
70
|
+
0,
|
|
71
|
+
0,
|
|
72
|
+
0,
|
|
73
|
+
0,
|
|
74
|
+
0,
|
|
75
|
+
0,
|
|
76
|
+
0,
|
|
77
|
+
0,
|
|
78
|
+
0,
|
|
79
|
+
0,
|
|
80
|
+
0,
|
|
81
|
+
0,
|
|
82
|
+
0,
|
|
83
|
+
0,
|
|
84
|
+
0,
|
|
85
|
+
0,
|
|
86
|
+
0,
|
|
87
|
+
0,
|
|
88
|
+
0,
|
|
89
|
+
0,
|
|
90
|
+
0
|
|
91
|
+
],
|
|
92
|
+
"Rows": [
|
|
93
|
+
[
|
|
94
|
+
"A",
|
|
95
|
+
1,
|
|
96
|
+
1,
|
|
97
|
+
0,
|
|
98
|
+
1,
|
|
99
|
+
"0",
|
|
100
|
+
"DataBlock1",
|
|
101
|
+
"0",
|
|
102
|
+
"",
|
|
103
|
+
"",
|
|
104
|
+
"",
|
|
105
|
+
"",
|
|
106
|
+
"",
|
|
107
|
+
"1",
|
|
108
|
+
"본사",
|
|
109
|
+
"20261005",
|
|
110
|
+
"20261012",
|
|
111
|
+
"",
|
|
112
|
+
"",
|
|
113
|
+
"",
|
|
114
|
+
"",
|
|
115
|
+
"",
|
|
116
|
+
"",
|
|
117
|
+
"",
|
|
118
|
+
"",
|
|
119
|
+
"",
|
|
120
|
+
"",
|
|
121
|
+
"",
|
|
122
|
+
"",
|
|
123
|
+
"",
|
|
124
|
+
"",
|
|
125
|
+
"",
|
|
126
|
+
"",
|
|
127
|
+
0,
|
|
128
|
+
"",
|
|
129
|
+
"",
|
|
130
|
+
"",
|
|
131
|
+
"",
|
|
132
|
+
"",
|
|
133
|
+
"",
|
|
134
|
+
""
|
|
135
|
+
]
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function workOrderQueryData(params: { dateFrom: string; dateTo: string; bizUnit: string }) {
|
|
140
|
+
for (const value of [params.dateFrom, params.dateTo]) {
|
|
141
|
+
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value) ||
|
|
142
|
+
!Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 10) !== value) {
|
|
143
|
+
throw new SystemEverQueryError('systemever-query-invalid-date')
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (params.dateFrom > params.dateTo) throw new SystemEverQueryError('systemever-query-invalid-date-range')
|
|
147
|
+
if (typeof params.bizUnit !== 'string' || !/^\d+$/.test(params.bizUnit)) {
|
|
148
|
+
throw new SystemEverQueryError('systemever-query-invalid-biz-unit')
|
|
149
|
+
}
|
|
150
|
+
const table = structuredClone(template)
|
|
151
|
+
const row = table.Rows[0]
|
|
152
|
+
row[table.Columns.indexOf('WorkDateFr')] = params.dateFrom.replaceAll('-', '')
|
|
153
|
+
row[table.Columns.indexOf('WorkDateTo')] = params.dateTo.replaceAll('-', '')
|
|
154
|
+
row[table.Columns.indexOf('BizUnit')] = params.bizUnit
|
|
155
|
+
row[table.Columns.indexOf('BizUnitName')] = ''
|
|
156
|
+
return { Tables: [table], IsSendXml: true, DataBlock1: 'DataBlock1' }
|
|
157
|
+
}
|