# Supplier (Produsen) — Technical Overview

> **Scope:** Module Purchase — aplikasi Point of Sales (POS) multi-cabang berbasis Yii2 Framework.
> **Model Class:** `common\models\Produsen`
> **Table:** `produsen`

---

## 1. Overview & Business Rules

Model `Produsen` adalah **master data supplier** yang menjadi pusat relasi di seluruh ekosistem modul Purchase. Setiap transaksi pembelian—dari Purchase Requisition (PR), Purchase Order (PO), Goods Receive (GR), Purchase Invoice (PI), hingga Purchase Payment (PP)—selalu mereferensikan record dari tabel ini.

### Aturan Bisnis Kritikal

| # | Rule | Detail |
|---|------|--------|
| 1 | **Multi-Cabang** | Setiap supplier terikat pada satu `cabang_id`. Data supplier tidak di-share lintas cabang secara otomatis. |
| 2 | **Soft Delete** | Penghapusan dilakukan via `deleted_at IS NOT NULL`. Record tidak pernah dihapus secara fisik dari database. |
| 3 | **Payment Term** | Kolom `term` (integer, dalam hari) menentukan jatuh tempo pembayaran default supplier. Nilai ini di-snapshot ke `pembelian.produsen_term` saat transaksi PO dibuat, sehingga perubahan term di masa depan tidak mempengaruhi transaksi historis. |
| 4 | **Tax Status** | Kolom `tax` (TINYINT) menandai apakah supplier adalah Pengusaha Kena Pajak (PKP). Digunakan untuk menentukan apakah PPN perlu diperhitungkan pada transaksi purchase invoice. |
| 5 | **Activity Log** | Setiap perubahan data supplier direkam oleh `ActivityLogBehavior`. Ini merupakan audit trail wajib untuk kepatuhan operasional. |
| 6 | **Dual PIC Contact** | Supplier menyimpan dua set kontak PIC: PIC Operasional (`pic_name`, `pic_email`, `pic_phone`) dan PIC Finance (`pic_name_finance`, `pic_email_finance`, `pic_phone_finance`). |
| 7 | **Nama & Kota Required** | Kolom `nama` dan `kota` adalah field mandatory (`required` rule). Semua kolom lain bersifat opsional. |
| 8 | **Last PI Date Tracking** | Kolom `last_pi_date` diperbarui setiap kali Purchase Invoice baru dibuat untuk supplier ini. Berguna untuk analisis frekuensi transaksi per supplier. |
| 9 | **Supplier Category** | Supplier dapat dikategorikan via `katagori_id` → `produsen_katagori`. Kategori bersifat per-cabang. |
| 10 | **Active Invoice Filter** | Method `getHasActiveInv()` menyaring supplier yang memiliki invoice aktif (belum fully-paid) dan belum masuk dalam proses pembayaran aktif. Digunakan pada alur pembuatan `PurchasePayment`. |

---

## 2. Skema Database & Tipe Data

### 2.1 Tabel Utama: `produsen`

```sql
CREATE TABLE `produsen` (
    `id`                  INT(11)       NOT NULL AUTO_INCREMENT,
    `nama`                VARCHAR(100)  DEFAULT NULL,
    `alamat`              VARCHAR(150)  DEFAULT NULL,
    `katagori_id`         INT UNSIGNED  DEFAULT NULL,
    `kota`                VARCHAR(225)  DEFAULT NULL,
    `kontak`              VARCHAR(225)  DEFAULT NULL,
    `tax`                 TINYINT(4)    DEFAULT NULL,
    `fax`                 VARCHAR(100)  DEFAULT NULL,
    `term`                INT(5)        DEFAULT NULL,
    `email`               VARCHAR(225)  DEFAULT NULL,
    `pic_name`            VARCHAR(225)  DEFAULT NULL,
    `pic_email`           VARCHAR(225)  DEFAULT NULL,
    `pic_phone`           VARCHAR(225)  DEFAULT NULL,
    `pic_name_finance`    VARCHAR(225)  DEFAULT NULL,
    `pic_email_finance`   VARCHAR(225)  DEFAULT NULL,
    `pic_phone_finance`   VARCHAR(225)  DEFAULT NULL,
    `cabang_id`           TINYINT(4)    DEFAULT NULL,
    `last_pi_date`        DATETIME      DEFAULT NULL COMMENT 'last pembuatan pi',
    `created_at`          DATETIME      DEFAULT NULL,
    `updated_at`          DATETIME      DEFAULT NULL,
    `deleted_at`          DATETIME      DEFAULT NULL,
    PRIMARY KEY (`id`),
    INDEX `fk_produsen_produsen_katagori1_idx` (`katagori_id`),
    CONSTRAINT `fk_produsen_produsen_katagori1`
        FOREIGN KEY (`katagori_id`) REFERENCES `produsen_katagori` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
```

| Kolom | Tipe Data | Constraints | Label UI | Keterangan |
|-------|-----------|-------------|----------|------------|
| `id` | `INT(11)` | PK, AUTO_INCREMENT, NOT NULL | ID | Primary key auto-increment |
| `nama` | `VARCHAR(100)` | Nullable | Name | **Required** via model rule. Nama resmi supplier. |
| `alamat` | `VARCHAR(150)` | Nullable | Address | Alamat lengkap supplier |
| `katagori_id` | `INT UNSIGNED` | Nullable, FK → `produsen_katagori.id` | Type | Kategori/tipe supplier, per-cabang |
| `kota` | `VARCHAR(225)` | Nullable | City | **Required** via model rule. Kota domisili supplier. |
| `kontak` | `VARCHAR(225)` | Nullable | Contact | Nomor telepon utama supplier |
| `tax` | `TINYINT(4)` | Nullable | Tax | Flag PKP: nilai non-null/non-zero = supplier PKP |
| `fax` | `VARCHAR(100)` | Nullable | Fax | Nomor fax supplier |
| `term` | `INT(5)` | Nullable | No of days | Payment term default dalam hari (misal: 30 = Net 30) |
| `email` | `VARCHAR(225)` | Nullable | Email | Email utama supplier |
| `pic_name` | `VARCHAR(225)` | Nullable | Name (PIC) | Nama PIC operasional |
| `pic_email` | `VARCHAR(225)` | Nullable | Email (PIC) | Email PIC operasional |
| `pic_phone` | `VARCHAR(225)` | Nullable | HP (PIC) | Telepon PIC operasional |
| `pic_name_finance` | `VARCHAR(225)` | Nullable | Name (Finance PIC) | Nama PIC bagian finance |
| `pic_email_finance` | `VARCHAR(225)` | Nullable | Email (Finance PIC) | Email PIC finance |
| `pic_phone_finance` | `VARCHAR(225)` | Nullable | HP (Finance PIC) | Telepon PIC finance |
| `cabang_id` | `TINYINT(4)` | Nullable | Cabang ID | ID cabang pemilik data supplier ini |
| `last_pi_date` | `DATETIME` | Nullable | — | Tanggal terakhir Purchase Invoice dibuat untuk supplier ini |
| `created_at` | `DATETIME` | Nullable | Created At | Di-set otomatis oleh `TimestampBehavior` |
| `updated_at` | `DATETIME` | Nullable | Updated At | Di-update otomatis oleh `TimestampBehavior` |
| `deleted_at` | `DATETIME` | Nullable | Deleted At | Soft delete: `NULL` = aktif, non-`NULL` = terhapus |

---

### 2.2 Tabel Pendukung: `produsen_bank`

Menyimpan rekening bank milik supplier. Satu supplier dapat memiliki lebih dari satu rekening.

```sql
CREATE TABLE `produsen_bank` (
    `id`           BIGINT UNSIGNED  NOT NULL AUTO_INCREMENT,
    `produsen_id`  INT(11)          DEFAULT NULL,
    `bank_name`    VARCHAR(100)     DEFAULT NULL,
    `no_rek`       VARCHAR(100)     DEFAULT NULL,
    `name_holder`  VARCHAR(100)     DEFAULT NULL,
    `created_at`   DATETIME         DEFAULT NULL,
    `updated_at`   DATETIME         DEFAULT NULL,
    `deleted_at`   DATETIME         DEFAULT NULL,
    PRIMARY KEY (`id`),
    INDEX `fk_produsenbank_produsen_idx` (`produsen_id`),
    CONSTRAINT `fk_produsenbank_produsenfk`
        FOREIGN KEY (`produsen_id`) REFERENCES `produsen` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
```

| Kolom | Tipe Data | Constraints | Keterangan |
|-------|-----------|-------------|------------|
| `id` | `BIGINT UNSIGNED` | PK, AUTO_INCREMENT | Primary key |
| `produsen_id` | `INT(11)` | Nullable, FK → `produsen.id` | Referensi ke supplier |
| `bank_name` | `VARCHAR(100)` | Nullable | Nama bank (misal: BCA, Mandiri) |
| `no_rek` | `VARCHAR(100)` | Nullable | Nomor rekening bank |
| `name_holder` | `VARCHAR(100)` | Nullable | Nama pemilik rekening |
| `created_at` | `DATETIME` | Nullable | Timestamp dibuat |
| `updated_at` | `DATETIME` | Nullable | Timestamp diperbarui |
| `deleted_at` | `DATETIME` | Nullable | Soft delete |

---

### 2.3 Tabel Pendukung: `produsen_katagori`

Kategori/tipe supplier. Bersifat per-cabang dan dapat dikonfigurasi bebas oleh admin.

```sql
CREATE TABLE `produsen_katagori` (
    `id`         INT(10) UNSIGNED  NOT NULL AUTO_INCREMENT,
    `nama`       VARCHAR(100)      DEFAULT NULL,
    `cabang_id`  BIGINT UNSIGNED   NOT NULL,
    `created_at` DATETIME          NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated_at` DATETIME          DEFAULT (CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP),
    `deleted_at` DATETIME          DEFAULT NULL,
    PRIMARY KEY (`id`),
    INDEX `fk_produsen_katagori_cabang1_idx` (`cabang_id`),
    CONSTRAINT `fk_produsen_katagori_cabang1`
        FOREIGN KEY (`cabang_id`) REFERENCES `cabang` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

| Kolom | Tipe Data | Constraints | Keterangan |
|-------|-----------|-------------|------------|
| `id` | `INT(10) UNSIGNED` | PK, AUTO_INCREMENT | Primary key |
| `nama` | `VARCHAR(100)` | Nullable | Nama kategori (misal: "Distributor", "Importir") |
| `cabang_id` | `BIGINT UNSIGNED` | NOT NULL, FK → `cabang.id` | Kategori ini milik cabang mana |
| `created_at` | `DATETIME` | NOT NULL, DEFAULT CURRENT_TIMESTAMP | Timestamp dibuat |
| `updated_at` | `DATETIME` | Nullable | Timestamp diperbarui (auto-update) |
| `deleted_at` | `DATETIME` | Nullable | Soft delete |

---

## 3. Relasi Antar Tabel (Entity Relationships)

### 3.1 Diagram Relasi (Teks Logis)

```
cabang (1) ──────────────────────────── (N) produsen_katagori
                                                    │
                                                    │ (FK: produsen.katagori_id)
                                                    ▼
cabang (1) ──── (N) produsen (1) ──────────────── (N) produsen_bank
                        │
                        ├─── (1:N) ──→ barang
                        │               (barang.produsen_id = produsen.id)
                        │               [default supplier per produk]
                        │
                        ├─── (1:N) ──→ pembelian
                        │               (pembelian.produsen_id = produsen.id)
                        │               [PR / PO / SPP / DR / GR]
                        │                       │
                        │                       └─── (1:N) ──→ pembelian_item
                        │                                       (pembelian_item.pembelian_id)
                        │                                       (pembelian_item.petty_produsen_id) [petty cash override]
                        │
                        ├─── (1:N) ──→ purchase_invoice
                        │               (purchase_invoice.produsen_id = produsen.id)
                        │               [tagihan dari supplier]
                        │
                        └─── (1:N) ──→ purchase_payment
                                        (purchase_payment.produsen_id = produsen.id)
                                        [pembayaran ke supplier]
```

---

### 3.2 Detail Setiap Relasi

#### `produsen` → `barang` (1:N)

```php
// common/models/Produsen.php
public function getBarangs()
{
    return $this->hasMany(Barang::class, ['produsen_id' => 'id']);
}
```

- **Makna:** Satu supplier dapat menjadi **default supplier** untuk banyak barang/produk.
- **Kolom FK:** `barang.produsen_id` → `produsen.id`
- **Catatan:** Ini adalah default supplier di master barang. Pada transaksi aktual (PO/PTY), supplier aktual dicatat di level `pembelian` atau `pembelian_item`.

---

#### `produsen` → `pembelian` (1:N)

```php
// common/models/Produsen.php
public function getPembelians()
{
    return $this->hasMany(Pembelian::class, ['produsen_id' => 'id']);
}
```

- **Makna:** Satu supplier dapat memiliki banyak dokumen pembelian.
- **Kolom FK:** `pembelian.produsen_id` → `produsen.id`
- **Tipe Dokumen yang mereferensikan supplier ini:**

| Konstanta | Nilai | Deskripsi |
|-----------|-------|-----------|
| `TYPE_SPP` | `'spp'` | Simple Purchase (pembelian langsung) |
| `TYPE_PO` | `'po'` | Purchase Order |
| `TYPE_PO_FROM_DR` | `'pc'` | PO dari Direct Receive (konsinyasi) |
| `TYPE_PR` | `'pr'` | Purchase Requisition (supplier bisa kosong) |
| `TYPE_GR` | `'gr'` | Goods Receive (penerimaan barang) |
| `TYPE_RT` | `'rt'` | Purchase Return |
| `TYPE_PETTY_CASH` | `'pty'` | Petty Cash (supplier dicatat per-item) |

- **Catatan Khusus PR:** Pada Purchase Requisition (`TYPE_PR`), `produsen_id` boleh kosong saat pembuatan karena supplier belum ditentukan. Supplier baru ditetapkan saat PR dikonversi ke PO.
- **Snapshot Term:** Saat PO dibuat, nilai `produsen.term` di-snapshot ke `pembelian.produsen_term` agar perubahan term supplier di kemudian hari tidak mengubah data historis.

---

#### `pembelian_item` → `produsen` via `petty_produsen_id` (N:1)

```php
// common/models/PembelianItem.php
public function getPettyProdusen()
{
    return $this->hasOne(Produsen::class, ['id' => 'petty_produsen_id']);
}
```

- **Makna:** Khusus untuk transaksi tipe `Petty Cash (pty)`, supplier dicatat **per line item** bukan di level header `pembelian`. Ini karena satu transaksi petty cash bisa berisi pembelian dari supplier berbeda.
- **Kolom FK:** `pembelian_item.petty_produsen_id` → `produsen.id`
- **Query gabungan (contoh dari `Pembelian::lastBuyProductByProdusen`):**

```sql
-- Cari harga beli terakhir suatu barang dari supplier tertentu
-- dengan memperhitungkan kedua jalur supplier (PO dan Petty Cash)
WHERE (
    (pembelian.type = 'po'  AND pembelian.produsen_id       = :produsenId)
    OR
    (pembelian.type = 'pty' AND pembelian_item.petty_produsen_id = :produsenId)
)
```

---

#### `produsen` → `purchase_invoice` (1:N)

```php
// common/models/PurchaseInvoice.php
public function getProdusen()
{
    return $this->hasOne(Produsen::class, ['id' => 'produsen_id']);
}
```

- **Makna:** Satu supplier dapat memiliki banyak Purchase Invoice (PI) — tagihan yang diterima dari supplier.
- **Kolom FK:** `purchase_invoice.produsen_id` → `produsen.id`
- **Status PI yang relevan untuk supplier:**

| Konstanta | Nilai | Deskripsi |
|-----------|-------|-----------|
| `STATUS_NOPAYMENT` | `0` | Invoice dibuat, belum ada payment apapun |
| `STATUS_UNPAID` | `1` | Payment draft dibuat, belum dikonfirmasi |
| `STATUS_PAID_PARTIAL` | `2` | Sebagian telah dibayar |
| `STATUS_PAID` | `3` | Lunas |

- **Aging Payable:** Method `PurchaseInvoice::getAging($cabangId)` mengagregasi outstanding payable per supplier dengan bucket aging: 0-30, 31-60, 61-90, 91-120, dan >120 hari.

---

#### `produsen` → `purchase_payment` (1:N)

```php
// common/models/PurchasePayment.php
public function getProdusen()
{
    return $this->hasOne(Produsen::class, ['id' => 'produsen_id']);
}
```

- **Makna:** Satu supplier dapat memiliki banyak record pembayaran (Purchase Payment).
- **Kolom FK:** `purchase_payment.produsen_id` → `produsen.id`
- **Referensi Bank:** `purchase_payment.produsen_bank_id` → `produsen_bank.id`. Saat pembayaran dibuat, rekening bank supplier yang digunakan direkam di sini.
- **Status Payment:**

| Konstanta | Nilai | Deskripsi |
|-----------|-------|-----------|
| `STATUS_UNPAID` | `1` | Draft payment, belum dikonfirmasi |
| `STATUS_PAID` | `2` | Payment telah dikonfirmasi/lunas |

---

#### `produsen` → `produsen_bank` (1:N)

```php
// common/models/Produsen.php
public function getBanks()
{
    return $this->hasMany(ProdusenBank::class, ['produsen_id' => 'id']);
}
```

- **Makna:** Satu supplier dapat mendaftarkan lebih dari satu rekening bank.
- **Kolom FK:** `produsen_bank.produsen_id` → `produsen.id`
- **Penggunaan:** Rekening yang dipilih saat `PurchasePayment` dibuat disimpan di `purchase_payment.produsen_bank_id`.

---

#### `produsen` → `produsen_katagori` (N:1)

```php
// common/models/Produsen.php
public function getKatagori()
{
    return $this->hasOne(ProdusenKatagori::class, ['id' => 'katagori_id']);
}
```

- **Makna:** Setiap supplier dapat dikategorikan ke dalam satu kategori supplier.
- **Kolom FK:** `produsen.katagori_id` → `produsen_katagori.id`
- **Catatan:** Kategori bersifat per-cabang (`produsen_katagori.cabang_id`). Supplier lintas cabang dapat memiliki kategori yang berbeda atau tidak berkategori (nullable).

---

## 4. Behaviors & Cross-Cutting Concerns

### 4.1 Behaviors yang Aktif

```php
public function behaviors()
{
    return [
        MyBehavior::timestampBehavior('created_at', 'updated_at'),
        MyBehavior::activityLogBehavior(),
    ];
}
```

| Behavior | Fungsi |
|----------|--------|
| `TimestampBehavior` | Auto-set `created_at` saat insert, `updated_at` saat update |
| `ActivityLogBehavior` | Merekam seluruh perubahan atribut (old/new value) ke tabel activity log |

### 4.2 Soft Delete Pattern

Semua query yang benar untuk mengambil data supplier aktif **harus** memfilter `deleted_at IS NULL`:

```sql
SELECT * FROM produsen WHERE deleted_at IS NULL AND cabang_id = :cabangId;
```

Model mewarisi `MysqlActiveRecord` (bukan Yii2 standar). Pastikan base class ini mengimplementasikan default scope soft delete jika ada, atau filter manual di setiap query.

---

## 5. Method Penting pada Model

### `Produsen::getHasActiveInv($cbgId, $activeOnly = true)`

Mengambil daftar supplier yang memiliki Purchase Invoice aktif (belum fully-paid) **dan** belum masuk dalam draft Purchase Payment yang sedang berjalan.

```php
// Logika inti:
// 1. Ambil produsen_id yang sudah ada di purchase_payment aktif
// 2. Query produsen yang punya invoice di purchase_invoice (JOIN)
// 3. Filter: purchase_invoice.deleted_at IS NULL
// 4. Opsional: filter invoice yang belum FULL_PAYMENT
// 5. Exclude supplier yang sudah masuk payment aktif
```

**Penggunaan:** Dipakai pada controller `PurchasePaymentController` untuk menyajikan dropdown supplier yang eligible untuk dibayar.

---

### `Produsen::getNamaqty()`

```php
public function getNamaqty()
{
    return $this->nama . " (" . $this->qty . ")";
}
```

Menghasilkan string display `"Nama Supplier (jumlah)"`. Properti `$qty` adalah virtual property yang di-populate via `GROUP BY` query, bukan kolom database.

---

## 6. Peta Alur Bisnis (Purchase Flow)

```
[Supplier (produsen)] 
        │
        ▼
[Purchase Requisition (PR)]  ──(opsional, supplier bisa kosong)
        │ konversi
        ▼
[Purchase Order (PO)]  ←── supplier wajib
        │
        ▼
[Goods Receive (GR / penerimaan_barang)]
        │
        ▼
[Purchase Invoice (PI)]  ←── supplier wajib, referensi ke GR
        │
        ▼
[Purchase Payment (PP)]  ←── supplier wajib, pilih bank rekening supplier
```

Setiap node dalam alur ini menyimpan `produsen_id` secara independen (snapshot), sehingga perubahan data supplier tidak memutus referensi historis.
