207 lines
11 KiB
Markdown
207 lines
11 KiB
Markdown
# Penjelasan Aturan Regex & Simulasi Proses Ekstraksi Dokumen DO PFM
|
|
|
|
Dokumen ini menjelaskan spesifikasi ekspresi reguler (Regex) yang digunakan dalam sistem PFM OCR serta menyimulasikan bagaimana teks mentah (*Raw Markdown*) hasil bacaan vLLM diuraikan langkah demi langkah melalui tiap lapisan deteksi hingga menjadi data JSON bersih di database.
|
|
|
|
---
|
|
|
|
## Bagian 1: Spesifikasi Aturan Regex Utama
|
|
|
|
Semua logika parsing ini didefinisikan dalam file utilitas pembantu [parser.ts](file:///d:/Client/Data%20Bisnis%20Solusi/app-pfm-ocr-v2/backend/pfm-web-app/src/utils/parser.ts).
|
|
|
|
### 1. Deteksi Nomor PO (Purchase Order)
|
|
* **Regex Ekstraksi**: `/(?:No\.?[ \t]*PO|PO[ \t]*No\.?)[ \t]*[:\-][ \t]*([A-Z0-9\-\/]+)/i`
|
|
* **Logika Pembersihan (`cleanAndFormatPO`)**:
|
|
* Menghapus label bising di depan (seperti `No. PO : `).
|
|
* **Pola 1 (Dengan Garis Miring)**: `PO/YY/NNNN+` (Tahun dipaksa menggunakan tahun berjalan saat ini untuk mengoreksi kesalahan OCR pada angka tahun).
|
|
* **Pola 2 (Angka Fused Tanpa Slash)**: Misalnya `PO1207000019` diubah paksa menjadi format `PO/26/7000019` menggunakan regex `/^(?:PO|P0|F0|...)(\d+)$/i`.
|
|
|
|
### 2. Deteksi Nomor SO (Sales Order) & DO (Delivery Order)
|
|
* **Regex SO**: `/(?:No\.?[ \t]*SO|SO[ \t]*No\.?)[ \t]*[:\-][ \t]*([A-Z0-9\-]+)/i`
|
|
* **Regex DO**: `/(?:No\.?[ \t]*DO|Delivery Order[ \t]*No|D\.O\.[ \t]*No|Order[ \t]*No)[ \t]*[:\- \t]*([A-Z0-9\-]+)/i`
|
|
* **Aturan Validasi**: Harus berupa angka numerik sepanjang **7 s.d. 12 digit** (biasanya diawali angka `16`).
|
|
|
|
### 3. Deteksi Tanggal
|
|
* **Regex Ekstraksi**: `/Tanggal\s*[:\-.]?\s*([^\n]{6,100})/i`
|
|
* **Regex Validasi Format (`cleanDateValue`)**: `/\b(\d{1,2})[ \t\-\/]*(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)([a-zA-Z]*)[ \t\-\/]*(\d{4})\b/i`
|
|
* Mengekstrak hari (1-31), nama bulan (Jan-Dec), dan tahun (4 digit).
|
|
* Menstandarkan nama bulan (kapital depan, sisanya huruf kecil, contoh: `Jun` $\rightarrow$ `June`).
|
|
|
|
### 4. Deteksi Plat Truk
|
|
* **Regex Deteksi Eksplisit (Dengan Label)**:
|
|
`/(?:No\.?\s*(?:Polisi|Pol|Kendaraan|Mobil|Truck|Pol\.?)|Plat(?:\s*No)?|Truck\s*No\.?)\s*[:\-.]?\s*\b([A-Z]{1,2})[ \t\-]*(\d{1,4})[ \t\-]*([A-Z]{1,3})\b/i`
|
|
* **Regex Pencarian Global (Tanpa Label)**: `/\b([A-Z]{1,2})[ \t\-]*(\d{1,4})[ \t\-]*([A-Z]{1,3})\b/gi`
|
|
* **Aturan Validasi**: Kode wilayah (huruf depan) harus terdaftar di array plat nomor valid Indonesia (seperti `B`, `D`, `AB`, `DK`, dll.).
|
|
|
|
---
|
|
|
|
## Bagian 2: Contoh Teks Mentah (Raw Markdown) Hasil OCR
|
|
|
|
Berikut adalah contoh teks Markdown hasil pembacaan vLLM (Langkah B) yang miringnya sudah diluruskan tetapi tata letaknya sedikit berantakan:
|
|
|
|
```markdown
|
|
PT. CHAROEN POKPHAND INDONESIA TBK
|
|
Kawasan Industri Ancol Barat No. 10
|
|
Jakarta Utara
|
|
===================================================
|
|
Kepada Yth: PT.PRIMAFOOD INTERNATIONAL
|
|
Order Untuk: PFM ANCOL 2 (02-ANC2)
|
|
Alamat Kirim: Jl. Ancol Barat VIII No. 1, Kel. Ancol, Kec. Pademangan
|
|
|
|
No. SO :
|
|
No. DO :
|
|
No. PO :
|
|
Tanggal :
|
|
1602987162
|
|
1602877112
|
|
PO/26/902871
|
|
30 Jun 2026
|
|
|
|
Plat No : B 9283 PQR
|
|
Nama Driver: Budi Santoso
|
|
|
|
| No. | Kode Item | Deskripsi Produk | Banyak | Jumlah |
|
|
|-----|-----------|------------------|--------|--------|
|
|
| 1 | 11310024 | Griller 0.8-0.9 | 10 | 450000 |
|
|
| 2 | 11640053 | Bone In Leg | 5 BAG | 250000 |
|
|
| 3 | 999999 | Item Sampah OCR | 2 | 10000 |
|
|
```
|
|
|
|
---
|
|
|
|
## Bagian 3: Simulasi Proses Deteksi Tiap Layer
|
|
|
|
Berikut adalah urutan bagaimana parser memproses Raw Markdown di atas:
|
|
|
|
### Layer 1: Deteksi Awal & Penyelarasan Layout Tergeser (*Shifted Layout*)
|
|
Pada Raw Markdown di atas, nilai label **SO, DO, PO, dan Tanggal** bergeser ke baris bawahnya karena tata letak visual kertas yang terpotong bergeser:
|
|
```text
|
|
No. SO :
|
|
No. DO :
|
|
1602987162
|
|
1602877112
|
|
```
|
|
* **Deteksi Pergeseran**: Parser mendeteksi bahwa nilai `No. SO` dan `No. DO` kosong (terbaca `"Not Found"` pada pencarian regex langsung).
|
|
* **Eksekusi Koreksi**: Logika realinyasi diaktifkan karena ditemukan pola 10 digit berturut-turut di baris setelah kumpulan label label kosong tersebut.
|
|
* **Hasil Realinyasi**:
|
|
* `noSO` diarahkan mengambil angka 10-digit pertama $\rightarrow$ `"1602987162"`
|
|
* `noDO` diarahkan mengambil angka 10-digit kedua $\rightarrow$ `"1602877112"`
|
|
* `noPO` diarahkan mengambil pola PO $\rightarrow$ `"PO/26/902871"`
|
|
* `tanggal` diarahkan mengambil pola tanggal terdekat $\rightarrow$ `"30 Jun 2026"`
|
|
|
|
### Layer 2: Sanitasi Nilai Ketat (`sanitizeParsedMetadata`)
|
|
Fungsi ini menyaring dan memvalidasi tipe data agar sesuai dengan standar database:
|
|
* **Tanggal**: Nilai `"30 Jun 2026"` dicocokkan dengan regex tanggal. Hari (`30`), bulan (`Jun`), dan tahun (`2026`) valid. Nama bulan distandarkan menjadi format bahasa Inggris penuh $\rightarrow$ `"30 June 2026"`.
|
|
* **No PO**: String `"PO/26/902871"` divalidasi. Karena tahun berjalan adalah `26`, formatnya valid $\rightarrow$ `"PO/26/902871"`.
|
|
* **No SO & DO**: `"1602987162"` dan `"1602877112"` divalidasi dengan regex `/^\d{7,12}$/`. Keduanya lolos karena berisi tepat 10 digit angka numerik.
|
|
* **Plat Nomor**: `"B 9283 PQR"` divalidasi. Huruf depan `"B"` dicocokkan ke array plat valid Indonesia. Terbukti valid dan distandarkan spasinya $\rightarrow$ `"B 9283 PQR"`.
|
|
|
|
### Layer 3: Pencocokan Toko Fuzzy Database (`resolveStoreFromText`)
|
|
Teks *"PFM ANCOL 2 (02-ANC2) Jl. Ancol Barat VIII No. 1..."* dianalisis untuk mencari toko aktif di database:
|
|
* **Tokenisasi**: Teks dipecah menjadi token-token kata: `['pfm', 'ancol', 'anc2', 'barat', 'pademangan']` (kata tidak penting seperti "untuk", "jalan", "no" dihapus).
|
|
* **Fuzzy Match**: Token-token ini dicocokkan dengan data tabel `store_master`.
|
|
* **Hasil**: Ditemukan baris database `nama_toko: "Prima Fresh Mart Ancol 2"` dan `alamat: "Jl. Ancol Barat VIII..."` memiliki kecocokan token tertinggi. Kolom metadata `orderUntuk` dan `alamat` diisi otomatis dengan data valid dari database tersebut.
|
|
|
|
### Layer 4: Parsing Tabel SKU & Pembersihan Item
|
|
Parser membedah baris-baris tabel HTML/Markdown:
|
|
* **Baris 1**: Kode `"11310024"` valid 8-digit. Deskripsi `"Griller 0.8-0.9"`. Kuantitas `"10"` (hanya angka murni).
|
|
* **Auto-Complete Unit**: Karena SKU `"11310024"` dikenali sebagai produk griller karung, satuannya ditambahkan otomatis $\rightarrow$ `"10 KRG"`.
|
|
* **Baris 2**: Kode `"11640053"` valid 8-digit. Deskripsi `"Bone In Leg"`. Kuantitas `"5 BAG"` valid.
|
|
* **Baris 3**: Kode `"999999"` **TIDAK VALID** karena panjangnya hanya 6 digit (bukan standar SKU 8-digit). Baris ini **dibuang otomatis** untuk mencegah sampah hasil pembacaan OCR masuk ke database.
|
|
|
|
SOLUSI: ketika SKU dicari itu tidak ada yang match, barulah coba baca nama barangnya dan lakukan fuzzy ke sku nama barang, untuk mendapatkan nomor SKUnya jadi saling crosscheck gitu, begitupun kalo SKU sudah dapat duluan cari nama barangnya dan cocokan ke sku nama barang untuk verified dua arah.
|
|
|
|
---
|
|
|
|
## Bagian 4: Hasil Data JSON Final yang Disimpan ke Database
|
|
Setelah melalui seluruh layer pemrosesan di atas, dokumen disimpan dengan objek metadata bersih seperti ini:
|
|
|
|
```json
|
|
{
|
|
"tanggal": "30 June 2026",
|
|
"noPO": "PO/26/902871",
|
|
"noSO": "1602987162",
|
|
"noDO": "1602877112",
|
|
"customerInfo": "PT.PRIMAFOOD INTERNATIONAL",
|
|
"orderUntuk": "Prima Fresh Mart Ancol 2",
|
|
"alamat": "Jl. Ancol Barat VIII No. 1, Kel. Ancol, Kec. Pademangan",
|
|
"platTruk": "B 9283 PQR",
|
|
"items": [
|
|
{
|
|
"kodeBarang": "11310024",
|
|
"namaBarang": "Griller 0.8-0.9",
|
|
"banyak": "10 KRG",
|
|
"jumlah": "450000"
|
|
},
|
|
{
|
|
"kodeBarang": "11640053",
|
|
"namaBarang": "Bone In Leg",
|
|
"banyak": "5 BAG",
|
|
"jumlah": "250000"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
Kolom `ocr_items` database juga akan diisi secara rapi dengan 2 baris item SKU di atas.
|
|
|
|
---
|
|
|
|
## Bagian 5: Contoh Kasus Gagal Ekstraksi (SO, DO, PO Tidak Ditemukan)
|
|
|
|
Di bawah ini adalah simulasi kasus kegagalan ekstraksi karena kualitas gambar yang buruk atau label yang tidak terbaca sama sekali.
|
|
|
|
### 1. Contoh Raw Markdown yang Bermasalah (Bad OCR / Noise)
|
|
```markdown
|
|
PT. PRIMAFOOD INTERNATIONAL
|
|
Jl. Ancol Barat VIII No. 1
|
|
|
|
No. SO : 16O29B7162 <-- OCR salah membaca huruf 'O' (harusnya '0') dan 'B' (harusnya '8')
|
|
No. DO : 160287 <-- Terpotong secara visual, hanya terbaca 6 digit
|
|
No. PO : PQ/26/902 <-- Terbaca 'PQ' (harusnya 'PO') dan nomor belakang terpotong
|
|
Tanggal : 30-Hv-2026 <-- Nama bulan rusak parah ("Hv" bukan nama bulan yang valid)
|
|
```
|
|
|
|
### 2. Jalur Pemrosesan dan Penyebab Kegagalan
|
|
* **Kegagalan No SO**:
|
|
* *Teks OCR*: `"16O29B7162"` (mengandung huruf 'O' dan 'B').
|
|
* *Hasil Regex*: Regex lapis 1 mungkin menangkap teks ini, tetapi ketika masuk ke fungsi [sanitizeParsedMetadata()](file:///d:/Client/Data%20Bisnis%20Solusi/app-pfm-ocr-v2/backend/pfm-web-app/src/utils/parser.ts#L657-L664), ia dicocokkan dengan aturan `/^\d{7,12}$/` (wajib angka numerik murni).
|
|
* *Keputusan*: Karena mengandung huruf 'O' dan 'B', validasi **Gagal**. Nilai `noSO` diganti menjadi `"Not Found"`.
|
|
|
|
|
|
|
|
* **Kegagalan No DO**:
|
|
* *Teks OCR*: `"160287"` (hanya 6 digit).
|
|
* *Hasil Regex*: Lolos regex lapis 1, tetapi saat masuk ke [sanitizeParsedMetadata()](file:///d:/Client/Data%20Bisnis%20Solusi/app-pfm-ocr-v2/backend/pfm-web-app/src/utils/parser.ts#L666-L673), ia **Gagal** karena panjangnya kurang dari 7 digit.
|
|
* *Keputusan*: Nilai `noDO` diubah menjadi `"Not Found"`.
|
|
* **Kegagalan No PO**:
|
|
* *Teks OCR*: `"PQ/26/902"`.
|
|
* *Hasil Regex*: Lapis 1 gagal mencocokkan karena polanya diawali dengan huruf `"PQ"` (tidak sesuai dengan daftar awalan valid: `PO`, `P0`, `F0`, `O0`, `Q0`, dll.).
|
|
* *Keputusan*: Nilai `noPO` diganti menjadi `"Not Found"`.
|
|
|
|
SOLUSI: karena no SO atau DO itu numerik jadi O -> 0 dan B -> 8 dan seterusnya untuk angka yang hampir sama jangan malah dihapus menjadi not found, kecuali memang dari awal datanya ga ada atau tidak sesuainya jauh
|
|
|
|
* **Kegagalan Tanggal**:
|
|
* *Teks OCR*: `"30-Hv-2026"`.
|
|
* *Hasil Regex*: Pola `"Hv"` tidak cocok dengan pola regex bulan valid (`Jan|Feb|Mar|...`).
|
|
* *Keputusan*: Nilai `tanggal` diganti menjadi `"Not Found"`.
|
|
|
|
SOLUSI itu pola bukan Jan Feb tapi emang kata katanya full seperti January February May dll. lalu ketika misal terbaca DD-Hv-YYYY ini jangan langsung dihapus tapi ganti saja jadi bulan ini, atau misal yang hilang DD ganti jadi tanggal hari ini, begitupun tahun. ketika semua tidak terbaca barulah boleh not found
|
|
|
|
### 3. Hasil JSON Final yang Disimpan ke Database
|
|
Akibat kegagalan penyaringan di atas, dokumen tetap disimpan tetapi dengan nilai fallback `"Not Found"` pada field yang gagal divalidasi:
|
|
|
|
```json
|
|
{
|
|
"tanggal": "Not Found",
|
|
"noPO": "Not Found",
|
|
"noSO": "Not Found",
|
|
"noDO": "Not Found",
|
|
"customerInfo": "PT.PRIMAFOOD INTERNATIONAL",
|
|
"orderUntuk": "Prima Fresh Mart Ancol 1",
|
|
"alamat": "Jl. Ancol Barat VIII No. 1",
|
|
"platTruk": "",
|
|
"items": []
|
|
}
|
|
```
|
|
*Catatan: Nilai fallback `"Not Found"` ini akan ditampilkan sebagai warning berwarna merah di web UI agar divalidasi/diisi secara manual oleh user.*
|
|
|