Ctrl K

PCE TW GUI MODULE — 銷項取號/手填單元測試指南

對象:開發/測試
目的:說明如何在本機用 Node 測 PCE_LIB_AR_BookAllocate.js,以及如何自己加案例
業務規則:銷項開立取號流程
文件日期:2026-09-13

這些測試不連 NetSuite 帳號。test/helpers/amd-loader.js 在 Node 載入 SuiteScript define(),test/helpers/ar-book-allocate-harness.js 則 mock N/record、N/search。

1. 怎麼跑

在專案根目錄:

# 只跑取號/手填
npm run test:ar-book-allocate

# 只跑某一檔
node --test test/ar-book-allocate.test.js
node --test test/ar-book-allocate-scenarios.test.js

# 只跑名稱符合的案例(Node 20+)
node --test --test-name-pattern="格式 36" test/ar-book-allocate-scenarios.test.js

# 全專案單元測試(含取號)
npm test

package.json 的 test:ar-book-allocate 會跑:

檔案內容
test/ar-book-allocate.test.js函式級:配號篩選、下一號、樂觀鎖、既有回歸
test/ar-book-allocate-scenarios.test.js路徑級:36/自動取號/手填情境

2. 預設發票本(不必每次重寫)

loadAllocateLib() 沒傳 searchRows 時,會放一本預設本(DEFAULT_BOOK):

欄位預設值意義
id501internal id
統編24536806賣方
期別11502民國 115 年 1–2 月雙月
字軌CA
起號/訖號50/99測試裡會補成 8 碼
last_no74下一號=CA00000075
狀態1-使用中(id 10)
傳至加值中心T自動取號才用得到

狀態 id 對照(mock buildLookupCache):0→8、1→9、2→10、9→11。

3. 新增案例的最小樣板

在 test/ar-book-allocate-scenarios.test.js(或新檔)加上:

const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const {
    DEFAULT_BOOK,
    state,
    cloneSearchRow,
    defaultAutoCriteria,
    loadAllocateLib
} = require('./helpers/ar-book-allocate-harness');

test('一句話說明預期行為', () => {
    loadAllocateLib(); // 每個 test 開頭都要呼叫,避免 mock 互相汙染
    const result = state.allocateLib.resolveInvoiceNumberForIssue({
        docFormatCode: '31',
        manualInvoiceNo: 'CA00000060',
        criteria: {
            taxId: DEFAULT_BOOK.taxId,
            period: DEFAULT_BOOK.period,
            docDate: '2026/02/01'
        }
    });
    assert.equal(result.ok, true);
    assert.equal(result.invoiceNo, 'CA00000060');
});

每個 test(...) 都要先 loadAllocateLib(...)。 函式會清掉上一筆的 mock 與 state。

跑過後確認綠燈:

npm run test:ar-book-allocate

4. 三條路徑要測什麼

開立入口一律是 resolveInvoiceNumberForIssue(options)。

state.allocateLib.resolveInvoiceNumberForIssue({
    docFormatCode: '35',          // '36' / '35' / '31' / '32' …
    vacUpload: 'T',               // 僅 35 有意義;只認 true 或 'T'
    manualInvoiceNo: '',          // 表單手填
    nowMs: 1234567890123,         // 僅 36:可重現的時間戳
    criteria: defaultAutoCriteria()
});
你想測的情況docFormatCodevacUploadmanualInvoiceNo預期
免用統一發票36任意空ok,號碼 SEUI+10 碼,不寫本
36 卻手填36任意有值MANUAL_INVOICE_NO_NOT_ALLOWED
電子發票上傳35true 或 'T'空自動取號 CA00000075
35 上傳卻手填35'T'有值MANUAL_INVOICE_NO_NOT_ALLOWED
紙本/35 不上傳沒填31/32/35+'F'空INVOICE_NO_REQUIRED
紙本手填31CA00000060反查本並回寫 last_no

也可以直接打內層函式(較精準):

  • 自動:allocateInvoiceNumber(defaultAutoCriteria())
  • 手填:applyManualInvoiceNumber('CA00000060', { taxId, period, docDate })

5. 怎麼改發票本與「已用號碼」

loadAllocateLib(options):

參數用途
searchRowsmock 搜尋到的發票本。用 cloneSearchRow({ id, values, texts }) 改欄位
usedInvoiceRows非空就視為號碼已被銷項主檔或 Invoice/CM 使用
recordLoadLastNos每次 record.load 看到的 last_no(用來測樂觀鎖衝突/重試)

改一本的欄位

loadAllocateLib({
    searchRows: [cloneSearchRow({
        values: {
            custrecord_pce_ar_book_last_no: '98',
            custrecord_pce_ar_book_last_issue_dt: '2026/03/01',
            custrecord_pce_ar_book_ns_platform: 'F',
            custrecord_pce_ar_book_department: '11'
        },
        texts: { custrecord_pce_ar_book_status: '0-未使用' }
    })]
});

cloneSearchRow 會跟預設本合併,只需覆寫要變的欄位。

兩本(測最小 id、多本涵蓋)

loadAllocateLib({
    searchRows: [
        cloneSearchRow({ id: '501' }),
        cloneSearchRow({
            id: '502',
            values: {
                custrecord_pce_ar_book_track: 'CB',
                custrecord_pce_ar_book_last_no: '10'
            }
        })
    ]
});

號碼已使用

loadAllocateLib({ usedInvoiceRows: [{ id: '9001' }] });

mock 不會比對號碼內容;只要 usedInvoiceRows 有列,任何非空白號碼都會被當成已用。

樂觀鎖:同一本被搶號

loadAllocateLib({
    recordLoadLastNos: ['75', '74'] // 第 1 次 load 看到 75(衝突),第 2 次看到 74(成功)
});

6. 斷言時看哪些欄位

成功:

assert.equal(result.ok, true);
assert.equal(result.invoiceNo, 'CA00000075');
assert.equal(result.bookId, '501');
assert.equal(result.allocated, true);   // 自動取號
assert.equal(result.skipped, true);     // 格式 36

失敗:

assert.equal(result.ok, false);
assert.equal(result.error.code, 'NO_ELIGIBLE_BOOK');
assert.match(result.error.message, /發票本 501/);

回寫發票本(state 在該次 loadAllocateLib 之後更新):

state意義
lastBookSaveValues.custrecord_pce_ar_book_last_no回寫的流水(8 碼)
lastBookSaveValues.custrecord_pce_ar_book_last_issue_dt回寫日期
lastBookSaveValues.custrecord_pce_ar_book_status狀態 id(9=使用中、10=使用完畢)
lastBookLoadId最後 record.load 的本 id
bookLoadCountload 次數(重試會 > 1)
lastSearchConfig最後一次 search.create(可檢查 filter)
lastBookSaveValues === null沒有回寫本(失敗或格式 36)

7. 常用錯誤代碼(對應案例)

完整定義見取號文件 §8。寫測試時用 error.code,不要只比中文訊息。

代碼建議怎麼造
MANUAL_INVOICE_NO_NOT_ALLOWED36 或 35+'T' 卻帶 manualInvoiceNo
INVOICE_NO_REQUIRED31/32/35+'F' 且沒填號碼
INVALID_INVOICE_NUMBER手填 12345678、SEUI…、或本的字軌不是 2 碼英文
INVOICE_NO_ALREADY_USEDusedInvoiceRows: [{ id: '1' }]
BOOK_NOT_FOUND手填 CA00000049(不在 50–99)
BOOK_AMBIGUOUS兩本都涵蓋同一號,且沒再篩部門/期別
INVOICE_NO_OUT_OF_RANGEcommitManualBookUsage('501', '100', …)
SELLER_TAX_ID_REQUIRED自動取號沒帶 taxId
DOC_FORMAT_REQUIRED自動取號沒帶 docFormatId
DOC_DATE_PERIOD_REQUIRED自動取號沒帶 period,且日期空白或無法解析(先算期別)
DOC_DATE_INVALID已有 period,但 docDate 仍無法解析為日曆日
NO_ELIGIBLE_BOOK本未勾傳至加值中心,或配號欄有值但表單有填且不同
DOC_DATE_BEFORE_LAST_ISSUE本的 last_issue_dt 晚於開立日
BOOK_EXHAUSTEDlast_no 已是訖號或超過
BOOK_NUMBER_CONFLICTrecordLoadLastNos 連續與預期下一號不符
BOOK_COMMIT_INVALIDcommitBookAllocation('', …) 或流水空白

8. 建議覆蓋清單(加案例時核對)

分流

  • [x] 36 產生 14 字 SEUI;手填拒絕;不寫本
  • [x] 35+T/true 自動取號;手填拒絕
  • [x] 35+'t' 不視為上傳(只認 true/'T')
  • [x] 31/32/35+F 必填手填

自動取號

  • [x] 下一號=last_no+1;空白 last=起號
  • [x] 訖號可配,狀態改使用完畢
  • [x] 多本只取最小 internal id,失敗不換本
  • [x] 須勾選傳至加值中心
  • [x] 部門、類別、配號須與發票本完全相同;空白只對空白
  • [x] 日期不可早於 last_issue_dt;同日可
  • [x] 衝突同一本最多重試 3 次

手填

  • [x] 字軌 2+流水(可小寫、連字、不足 8 碼補零)
  • [x] 不驗接續 last_no、不驗日期
  • [x] 不要求傳至加值中心;狀態 2 仍可反查
  • [x] 已用拒絕;找不到/多本涵蓋拒絕
  • [x] 手填亦依部門/類別/配號完全相同篩選;空白只對空白
  • [x] 成功 last_no=max(既有, 本次);起號可改未使用→使用中;訖號可改使用完畢

新規則出現時:先改 PCE_LIB_AR_BookAllocate.js,再在對應 describe 加一個失敗案例與一個成功案例。

9. 這套測試測不到的事

  • 開立 Suitelet/Client Script 畫面(欄位停用、必填提示)
  • 真正寫入 NetSuite 的治理、權限、SDF maxlength
  • 合併開立時 Name 是否等於發票號碼(在 PCE_LIB_AR_Issue.js,不在本 library)

那些要靠帳號冒煙或另外對 Issue library 寫測試。

10. 相關檔案

項目路徑
取號實作src/FileCabinet/SuiteApps/com.netsuite.pceguitw/ar/libraries/PCE_LIB_AR_BookAllocate.js
Harnesstest/helpers/ar-book-allocate-harness.js
AMD 載入test/helpers/amd-loader.js
函式測試test/ar-book-allocate.test.js
情境測試test/ar-book-allocate-scenarios.test.js