Ringkasan Utama
Deloitte melaporkan 79 persen pemimpin bisnis melihat produktivitas terhambat oleh sistem yang terputus, dan manajemen dokumen adalah tempat hal itu paling terlihat: invoice dikirim sebagai PDF lewat email, kontrak tersimpan di hard drive seseorang. Melampirkan dokumen ke record ERP yang menjadi tempatnya adalah yang membuatnya bisa ditemukan kemudian.
Menurut Deloitte, 79% pemimpin bisnis melaporkan bahwa produktivitas tim mereka terhambat oleh ketidakselarasan antara sistem. Tidak ada yang lebih benar dari manajemen dokumen: faktur yang dikirim via email sebagai PDF, kontrak tersimpan di hard drive seseorang, gambar teknis tersebar di folder Google Drive. Ketika dokumen hidup di luar ERP, mereka tidak dapat ditautkan ke transaksi, tidak dapat memicu workflow, dan tidak dapat ditemukan dengan cepat selama audit.
Model Dokumen dan Metadata
Setiap dokumen dalam DMS ini memiliki: document_id, entity_type (vendor, produk, faktur, proyek, karyawan), entity_id (rekord ERP yang ditautkan), document_type (kontrak, faktur, sertifikat, gambar, foto), filename, storage_path (kunci Google Cloud Storage), file_size, mime_type, version, uploaded_by, uploaded_at, dan expires_at opsional.
Penyimpanan File dengan Google Cloud Storage
Semua file dokumen disimpan di Google Cloud Storage, bukan di database. Saya hanya menyimpan kunci GCS (path) di PostgreSQL. File diorganisir berdasarkan struktur folder: '[entity_type]'/'[entity_id]'/'[document_type]'/'[version]'/'[filename]'. Akses melalui GCS signed URL dengan TTL yang dapat dikonfigurasi: 1 jam untuk tampilan dalam aplikasi, 24 jam untuk tautan unduhan yang dikirim via email.
ERP Document Management Architecture
ERP Entity (vendor, invoice, project, employee)
│ linked via entity_type + entity_id
▼
┌─────────────────────────────────────────────────────┐
│ documents table (PostgreSQL) │
│ │
│ id │ UUID │
│ entity_type │ 'vendor' | 'invoice' | 'project' │
│ entity_id │ UUID (FK to entity) │
│ document_type│ 'contract' | 'invoice' | 'cert' │
│ storage_path │ GCS key (not the file itself) │
│ version │ INT (increments on replacement) │
│ expires_at │ TIMESTAMPTZ (nullable) │
│ text_content │ tsvector (for full-text search) │
└──────────────────────┬──────────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ GCS Storage │ │ OCR Extract │ │ Expiry Monitor │
│ (files) │ │ Vision API │ │ → alerts 60/30/ │
│ Signed URLs │ │ → tsvector │ │ 7 days before │
└──────────────┘ └──────────────┘ └──────────────────┘
Full-text search:
SELECT * FROM documents
WHERE text_content @@ to_tsquery('english', 'Sumber & Makmur')
AND uploaded_at BETWEEN '2025-01-01' AND '2025-03-31';Dari pengalaman saya membangun sistem ERP kustom untuk klien secara freelance: implementasikan versioning dokumen dari awal. Permintaan manajemen dokumen paling umum setelah go-live adalah 'Saya mengunggah versi kontrak yang salah — bisakah saya menggantinya tanpa kehilangan yang lama?' Jika Anda tidak memiliki versioning, jawabannya menyakitkan. Sistem saya membuat baris versi baru untuk setiap penggantian, mempertahankan semua versi sebelumnya. 'Versi aktif' adalah nomor versi tertinggi.
Integrasi Workflow Dokumen
Dokumen harus memicu dan berpartisipasi dalam workflow. Ketika vendor mengunggah sertifikat NPWP melalui portal vendor, dokumen muncul di antrian tinjauan tim pengadaan ERP. Ketika faktur AP disetujui, PDF faktur yang dilampirkan secara otomatis diarsipkan ke folder dokumen vendor. Integrasi workflow ini mengubah DMS dari lemari arsip menjadi peserta aktif dalam proses bisnis.
OCR dan Pencarian Full-Text
File PDF mentah tidak dapat dicari kecuali Anda mengekstrak teksnya. Saya menggunakan Google Cloud Vision API untuk OCR pada PDF dan gambar yang diunggah: konten teks diekstrak dan disimpan dalam tabel document_search sebagai PostgreSQL tsvector. Pencarian full-text menggunakan ts_query PostgreSQL, yang menangani teks Indonesia dan Inggris.
// NestJS: Document upload with GCS storage + OCR
@Post('/documents/upload')
@UseInterceptors(FileInterceptor('file'))
async uploadDocument(
@UploadedFile() file: Express.Multer.File,
@Body() dto: UploadDocumentDto,
@GetUser() user: User,
) {
// 1. Determine storage path and version
const existing = await this.docRepo.findLatestVersion(
dto.entityType, dto.entityId, dto.documentType
);
const version = (existing?.version ?? 0) + 1;
const storagePath = [
dto.entityType, dto.entityId, dto.documentType,
`v${version}`, file.originalname
].join('/');
// 2. Upload to GCS
await this.storageService.upload(storagePath, file.buffer, file.mimetype);
// 3. Extract text via Google Cloud Vision (async)
const doc = await this.docRepo.save({
entityType: dto.entityType,
entityId: dto.entityId,
documentType: dto.documentType,
storagePath, version,
fileSize: file.size,
mimeType: file.mimetype,
uploadedBy: user.id,
expiresAt: dto.expiresAt,
});
// Trigger OCR extraction asynchronously
await this.ocrQueue.add('extract-text', { documentId: doc.id, storagePath });
return { documentId: doc.id, version, storagePath };
}
// OCR processor: updates tsvector for full-text search
@Process('extract-text')
async extractText(job: Job<{ documentId: string; storagePath: string }>) {
const imageBytes = await this.storageService.download(job.data.storagePath);
const [result] = await this.visionClient.textDetection({
image: { content: imageBytes },
});
const text = result.fullTextAnnotation?.text ?? '';
await this.docRepo.update(job.data.documentId, {
textContent: () => `to_tsvector('english', ${JSON.stringify(text)})`,
});
}Bagaimana Saya Mendekati Ini
Backend DMS adalah NestJS DocumentModule dengan StorageService (membungkus GCS), DocumentService (CRUD metadata), dan SearchService (full-text PostgreSQL). Frontend adalah penampil dokumen React menggunakan react-pdf untuk rendering PDF dan tampilan pohon folder yang dibangun dengan react-arborist. Dokumen terbuka di panel samping tanpa meninggalkan halaman ERP saat ini.
Biaya penyimpanan dokumen menumpuk lebih cepat dari yang diharapkan. Perusahaan yang menyimpan semua faktur, kontrak, foto, dan laporan selama 5 tahun dapat dengan mudah mengumpulkan 500GB-1TB file. Biaya Google Cloud Storage untuk 1TB di wilayah asia-southeast2 (Jakarta) sekitar $20/bulan untuk penyimpanan ditambah biaya egress untuk unduhan. Implementasikan kebijakan siklus hidup dokumen: pindahkan dokumen yang lebih dari 1 tahun ke penyimpanan Coldline (10x lebih murah untuk arsip yang jarang diakses).
Pelacakan Dokumen Kepatuhan
Kepatuhan regulasi Indonesia mengharuskan pelacakan dokumen dengan tanggal kedaluwarsa: sertifikat PKP, SIUP, API (lisensi impor), sertifikasi SNI, sertifikat ISO, dan sertifikat kualifikasi staf. DMS ini mengirimkan email peringatan 60 hari, 30 hari, dan 7 hari sebelum dokumen kepatuhan kedaluwarsa. Modul pengadaan memeriksa validitas dokumen sebelum mengeluarkan PO kepada vendor.
Dukungan Audit dan eDiscovery
Selama audit (audit pajak oleh DJP, atau audit kepatuhan yang diperlukan klien), auditor meminta dokumen tertentu. DMS ini mendukung: ekspor massal berdasarkan rentang tanggal, jenis entitas, dan jenis dokumen; audit trail yang menunjukkan siapa yang mengakses setiap dokumen dan kapan; dan tingkat akses auditor read-only yang memberikan akses tampilan saja ke set dokumen tertentu.
