Code Factory | Sistem Inventory atau Aplikasi Persediaan Barang adalah salah satu aplikasi yang umum dan cukup menantang terutama bagi para programmer pemula yang sedang mengasah skill programming-nya. Artikel ini merupakan lanjutan dari artikel ketiga yang membahas Pembuatan Form Master Barang. Artikel ini akan melanjutkan pembuatan form transaksi dengan desain master-detail menggunakan bantun extension dynamic-form, dan kita buat sederhana saja sehingga akan cukup mudah dipahami para programmer pemula.
Extension Tambahan
Untuk memudahkan kita membangun user interface yang cukup nyaman maka kita memerlukan dua buah extension tambahan, yaitu datepicker untuk memilih tanggal, dan dynamic-form untuk form CRUD master-detail. Gunakan perintah composer berikut ini untuk instalasi-nya.
composer require kartik-v/yii2-widget-datepicker "*" composer require wbraganca/yii2-dynamicform "*"
[collapsed title=Instalasi Extension]
[/collapsed]
Generate CRUD Menggunakan Gii
Silakan generate CRUD untuk Table Transactions dan TransactionDetails dengan cara yang sama seperti saat membuat form master barang.
[collapsed title=Generate CRUD Transactions] [/collapsed] [collapsed title=Generate CRUD TransactionDetails] [/collapsed]
Modifikasi List Transaksi Barang
Hasil generate CRUD dari Yii sangat membantu kita mempercepat proses pengerjaan aplikasi ini, namun masih perlu beberapa penyesuaian dari code standard Gii yang dihasilkan,
'columns' => [ ['class' => 'yii\grid\SerialColumn'], 'id', 'trans_code', 'trans_date', 'type_id', 'remarks', ['class' => 'yii\grid\ActionColumn'], ],
Kita dapat menghapus field id yang hanya field identity sehingga tidak penting untuk ditampilkan. Kita juga dapat mengubah field type_id menjadi type.name agar data yang ditampilkan adalah nama jenis transaksinya dan bukan angka id-nya sehingga hasilnya menjadi seperti di bawah ini
'columns' => [ ['class' => 'yii\grid\SerialColumn'], 'trans_code', 'trans_date', 'type.name', 'remarks', ['class' => 'yii\grid\ActionColumn'], ],
[collapsed title=GridView Transaksi][/collapsed]
Hasilnya cukup user-friendly. Namun sayangnya dengan cara di atas, kita tidak bisa melakukan filter terhadap jenis transaksi. Oleh karena itu kita perlu melakukan perubahan agar filter terhadap jenis transaksi barang kita kita lakukan, filter menggunakan dropdown list. Pertama yang kita lakukan adalah mengambil data semua jenis transaksi yang ada di dalam database menggunakan code berikut,
$typeList = ArrayHelper::map(TransactionTypes::find()->asArray()->all(), 'id', 'name');
Kemudian ubah setting column type.name di GridView dengan menambahkan atribut setting filter dan value barupa Closure yang bentuknya spesifik untuk GridView, sehingga menjadi seperti di bawah ini
'columns' => [ ['class' => 'yii\grid\SerialColumn'], 'trans_code', 'trans_date', [ 'attribute' => 'type_id', 'filter' => $typeList, 'label' => 'Transaction Type', 'value' => function ($model, $index, $widget) { return $model->type->name; } ], 'remarks', ['class' => 'yii\grid\ActionColumn'], ],
[collapsed title=GridView Transaksi dengan Filter Dropdown][/collapsed]
Modifikasi Form Transaksi Barang
Form input transaksi barang melibatkan dua buah table yaitu transactions dan transaction_details sehingga kita memerlukan extension dynamic-form untuk membuatnya. Instalasi dengan menggunakan perintah composer di bawah ini.
composer require wbraganca/yii2-dynamicform "*"
Yang perlu kita lakukan kemudian adalah
- Membuat class Model untuk memudahkan kita membuat beberapa row TransactionDetails dalam satu baris code saja.
- Modifikasi ActionCreate di TransactionController untuk proses pembuatan transaksi baru
- Modifikasi ActionUpdate di TransactionController untuk proses pembuatan transaksi yang sudah ada di database
- Modifikasi ActionDelete di TransactionController untuk proses penghapusan transaksi
- Modifikasi form untuk Create dan Update.
Class Model
Class Model ini merupakan turunan dari class yii\base\Model namun kita menambahkan sebuah function createMultiple untuk memudahkan kita membuat beberapa row TransactionDetails berdasarkan isi dari variable POST menggunakan satu baris code saja.
[collapsed title=Class Model]
namespace app\models; use Yii; use yii\helpers\ArrayHelper; class Model extends \yii\base\Model { /** * Creates and populates a set of models. * @param string $modelClass * @param array $multipleModels * @return array */ public static function createMultiple($modelClass, $multipleModels = []) { $model = new $modelClass; $formName = $model->formName(); $post = Yii::$app->request->post($formName); $models = []; if (! empty($multipleModels)) { $keys = array_keys(ArrayHelper::map($multipleModels, 'id', 'id')); $multipleModels = array_combine($keys, $multipleModels); } if ($post && is_array($post)) { foreach ($post as $i => $item) { if (isset($item['id']) && !empty($item['id']) && isset($multipleModels[$item['id']])) { $models[] = $multipleModels[$item['id']]; } else { $models[] = new $modelClass; } } } unset($model, $formName, $post); return $models; } }
[/collapsed]
Modifikasi ActionCreate
Saat melakukan proses penyimpanan data transaksi baru ke database, kita mengikuti algoritma berikut ini,
- Load master record
- Load semua detail records
- Assign foreign key field di detail records
- Validate master record
- Validate detail record
- Jika validasi berhasil
- Mulai transaksi database
- Simpan master record
- Simpan detail record satu-persatu
- Jika berhasil semua, commit
- Jika gagal rollback
- Tampilkan hasilnya
[collapsed title=ActionCreate]
/** * Creates a new Transactions model. * If creation is successful, the browser will be redirected to the 'view' page. * @return mixed */ public function actionCreate() { $model = new Transactions(); $details = [ new TransactionDetails ]; // proses isi post variable if ($model->load(Yii::$app->request->post())) { $details = Model::createMultiple(TransactionDetails::classname()); Model::loadMultiple($details, Yii::$app->request->post()); // assign default transaction_id foreach ($details as $detail) { $detail->trans_id = 0; } // ajax validation if (Yii::$app->request->isAjax) { Yii::$app->response->format = Response::FORMAT_JSON; return ArrayHelper::merge( ActiveForm::validateMultiple($details), ActiveForm::validate($model) ); } // validate all models $valid1 = $model->validate(); $valid2 = Model::validateMultiple($details); $valid = $valid1 && $valid2; // jika valid, mulai proses penyimpanan if ($valid) { // mulai database transaction $transaction = \Yii::$app->db->beginTransaction(); try { // simpan master record if ($flag = $model->save(false)) { // kemudian simpan detail records foreach ($details as $detail) { $detail->trans_id = $model->id; if (! ($flag = $detail->save(false))) { $transaction->rollBack(); break; } } } if ($flag) { // sukses, commit database transaction // kemudian tampilkan hasilnya $transaction->commit(); return $this->redirect(['view', 'id' => $model->id]); } else { return $this->render('create', [ 'model' => $model, 'details' => $details, ]); } } catch (Exception $e) { // penyimpanan galga, rollback database transaction $transaction->rollBack(); throw $e; } } else { return $this->render('create', [ 'model' => $model, 'details' => $details, 'error' => 'valid1: '.print_r($valid1,true).' - valid2: '.print_r($valid2,true), ]); } } else { // inisialisai id // diperlukan untuk form master-detail $model->id = 0; // render view return $this->render('create', [ 'model' => $model, 'details' => $details, ]); } }
[/collapsed]
Modifikasi ActionUpdate
Saat melakukan proses penyimpanan perubahan data transaksi ke database. Agak sedikit berbeda dengan proses pyimpanan transaksi baru, untuk proses edit, kita menghapus semua detail record yang sudah ada sebelumnya di database dan kemudian melakukan insert ulang sebagai record baru. Algoritma-nya adalah seperti berikut ini,
- Load master record
- Load semua detail records
- Assign foreign key field di detail records
- Validate master record
- Validate detail record
- Jika validasi berhasil
- Mulai transaksi database
- Simpan master record
- Delete semua detail record yang sudah ada di database
- Simpan detail record satu-persatu
- Jika berhasil semua, commit
- Jika gagal rollback
- Tampilkan hasilnya
[collapsed title=ActionUpdate]
/** * Updates an existing Transactions model. * If update is successful, the browser will be redirected to the 'view' page. * @param integer $id * @return mixed */ public function actionUpdate($id) { $model = $this->findModel($id); $details = $model->transactionDetails; if ($model->load(Yii::$app->request->post())) { $oldIDs = ArrayHelper::map($details, 'id', 'id'); $details = Model::createMultiple(TransactionDetails::classname(), $details); Model::loadMultiple($details, Yii::$app->request->post()); $deletedIDs = array_diff($oldIDs, array_filter(ArrayHelper::map($details, 'id', 'id'))); // assign default transaction_id foreach ($details as $detail) { $detail->trans_id = $model->id; } // ajax validation if (Yii::$app->request->isAjax) { Yii::$app->response->format = Response::FORMAT_JSON; return ArrayHelper::merge( ActiveForm::validateMultiple($details), ActiveForm::validate($model) ); } // validate all models $valid1 = $model->validate(); $valid2 = Model::validateMultiple($details); $valid = $valid1 && $valid2; // jika valid, mulai proses penyimpanan if ($valid) { // mulai database transaction $transaction = \Yii::$app->db->beginTransaction(); try { // simpan master record if ($flag = $model->save(false)) { // delete dahulu semua record yang ada if (! empty($deletedIDs)) { TransactionDetails::deleteAll(['id' => $deletedIDs]); } // kemudian, simpan details record foreach ($details as $detail) { $detail->trans_id = $model->id; if (! ($flag = $detail->save(false))) { $transaction->rollBack(); break; } } } if ($flag) { // sukses, commit database transaction // kemudian tampilkan hasilnya $transaction->commit(); return $this->redirect(['view', 'id' => $model->id]); } } catch (Exception $e) { // penyimpanan galga, rollback database transaction $transaction->rollBack(); throw $e; } } else { return $this->render('create', [ 'model' => $model, 'details' => $details, 'error' => 'valid1: '.print_r($valid1,true).' - valid2: '.print_r($valid2,true), ]); } } // render view return $this->render('update', [ 'model' => $model, 'details' => (empty($details)) ? [new TransactionDetails] : $details ]); }
[/collapsed]
Modifikasi ActionDelete
Pada saat menghapus data transaksi maka yang perlu kita lakukan adalah terlebih dahulu menghapus detail record sebelum menghapus master/parent record. Algoritma-nya adalah sebagai berikut
- Mulai transaksi database
- Delete semua detail record satu-persatu
- Delete master record
- Jika berhasil semua, commit
- Jika gagal rollback
- Tampilkan index transaksi barang
[collapsed title=ActionDelete]
/** * Deletes an existing Transactions model. * If deletion is successful, the browser will be redirected to the 'index' page. * @param integer $id * @return mixed */ public function actionDelete($id) { $model = $this->findModel($id); $details = $model->transactionDetails; // mulai database transaction $transaction = \Yii::$app->db->beginTransaction(); try { // pertama, delete semua detail records foreach ($details as $detail) { $detail->delete(); } // kemudian, delete master record $model->delete(); // sukses, commit transaction $transaction->commit(); } catch (Exception $e) { // gagal, rollback database transaction $transaction->rollBack(); } return $this->redirect(['index']); }
[/collapsed]
Modifikasi create.php
โKita perlu menambahkan variable $details agar dapat dikenali di dalam _form.php
โ <?= $this->render('_form', [ 'model' => $model, 'details' => $details, ]) ?>
Modifikasi update.php
โKita juga perlu menambahkan variable $details agar dapat dikenali di dalam _form.php
<?= $this->render('_form', [ 'model' => $model, 'details' => $details, ]) ?>
Modifikasi _form.php
โCode lengkapnya adalah seperti berikut ini
[collapsed title=Code HTML form transaksi]
<div class="transactions-form"> <?php $form = ActiveForm::begin(['id' => 'transactions-form']); ?> <div class="row"> <div class="col-sm-4 col-md-6"> <?= $form->field($model, 'trans_code')->textInput(['maxlength' => true]) ?> </div> <div class="col-sm-4 col-md-3"> <?php echo '<label class="control-label" for="transactions-trans_date">Transaction Date</label>'; echo DatePicker::widget([ 'id' => 'transactions-trans_date', 'name' => 'Transactions[trans_date]', 'type' => DatePicker::TYPE_COMPONENT_APPEND, 'value' => date('Y-m-d'), 'pluginOptions' => [ 'autoclose'=>true, 'format' => 'yyyy-mm-dd' ] ]); ?> </div> <div class="col-sm-4 col-md-3"> <?= $form->field($model, 'type_id')->dropDownList( ArrayHelper::map(TransactionTypes::find()->all(), 'id', 'name'), ['prompt'=>'* Pilih Transaksis *'] ); ?> </div> <div class="col-sm-12 col-md-12"> <?= $form->field($model, 'remarks')->textInput(['maxlength' => true]) ?> </div> </div><!-- .row --> <div class="panel panel-default"> <div class="panel-heading"><h4><i class="glyphicon glyphicon-th-list"></i> Transaction Details</h4></div> <div class="panel-body"> <?php DynamicFormWidget::begin([ 'widgetContainer' => 'dynamicform_wrapper', // required: only alphanumeric characters plus "_" [A-Za-z0-9_] 'widgetBody' => '.container-items', // required: css class selector 'widgetItem' => '.item', // required: css class 'limit' => 999, // the maximum times, an element can be cloned (default 999) 'min' => 1, // 0 or 1 (default 1) 'insertButton' => '.add-item', // css class 'deleteButton' => '.remove-item', // css class 'model' => $details[0], 'formId' => 'transactions-form', 'formFields' => [ 'trans_id', 'item_id', 'quantity', 'remarks', ], ]); ?> <div class="container-items"><!-- widgetContainer --> <?php foreach ($details as $i => $detail): ?> <div class="item row"> <?php // necessary for update action. if (! $detail->isNewRecord) { echo Html::activeHiddenInput($detail, "[{$i}]id"); } ?> <div class="col-sm-8 col-md-4"> <?= $form->field($detail, "[{$i}]item_id")->dropDownList( ArrayHelper::map(Items::find()->all(), 'id', 'name'), // Flat array ('id'=>'label') ['prompt'=>'* Pilih Barang *'] // options ); ?> </div> <div class="col-sm-4 col-md-2"> <?= $form->field($detail, "[{$i}]quantity")->textInput(['maxlength' => true]) ?> </div> <div class="col-sm-10 col-md-5"> <?= $form->field($detail, "[{$i}]remarks")->textInput(['maxlength' => true]) ?> </div> <div class="col-sm-2 col-md-1 item-action"> <div class="pull-right"> <button type="button" class="add-item btn btn-success btn-xs"> <i class="glyphicon glyphicon-plus"></i></button> <button type="button" class="remove-item btn btn-danger btn-xs"> <i class="glyphicon glyphicon-minus"></i></button> </div> </div> </div><!-- .row --> <?php endforeach; ?> </div> <?php DynamicFormWidget::end(); ?> </div> </div> <div class="form-group"> <?= Html::submitButton($model->isNewRecord ? Yii::t('app', 'Create') : Yii::t('app', 'Update'), ['class' => $model->isNewRecord ? 'btn btn-success' : 'btn btn-primary']) ?> </div> <?php ActiveForm::end(); ?> </div>
[/collapsed]
[collapsed title=Form Transaksi Barang][/collapsed]
Membuat View Transaksi Barang
Untuk menampilkan data master-detail, maka kita harus record master dan semua record detail yang berelasi, sehingga kita perlu mengubah ActionView menjadi seperti ini
[collapsed title=Code ActionView]
/** * Displays a single Transactions model. * @param integer $id * @return mixed */ public function actionView($id) { return $this->render('view', [ 'model' => $this->findModel($id), 'details' => $this->findDetails($id), ]); } /** * Finds the Transactions model based on its primary key value. * If the model is not found, a 404 HTTP exception will be thrown. * @param integer $id * @return Transactions the loaded model * @throws NotFoundHttpException if the model cannot be found */ protected function findModel($id) { if (($model = Transactions::findOne($id)) !== null) { return $model; } else { throw new NotFoundHttpException('The requested page does not exist.'); } } /** * Finds the TransactionDetails model based on its foreign key value. * @param integer $id * @return data provider TransactionDetails for GridView */ protected function findDetails($id) { $detailModel = new TransactionDetailsSearch(); return $detailModel->search(['TransactionDetailsSearch'=>['trans_id'=>$id]]); }
[/collapsed]
Dan jangan lupa mengubah view.php agar menampilkan data yang didapat dari controller. Kita akan menggunakan DetailView untuk master row, dan menggunakan GridView untuk menampilkan detail row.
[collapsed title=HTML Code View Transaksi]
<?= DetailView::widget([ 'model' => $model, 'attributes' => [ 'trans_code', [ 'attribute' => 'trans_date', 'format' => [ 'date', 'php: d-M-Y' ], 'labelColOptions' => [ 'style'=>'width:30%; text-align:right;' ] ], 'type.name', 'remarks', ], ]) ?> <div class="item panel panel-info"> <div class="panel-heading"> <h3 class="panel-title pull-left"><i class="glyphicon glyphicon-barcode"></i> Transaction Line Item</h3> <div class="clearfix"></div> </div> <div class="panel-body"> <?= GridView::widget([ 'dataProvider' => $details, 'columns' => [ ['class' => 'yii\grid\SerialColumn'], [ 'attribute' => 'item_id', 'value' => 'item.code', 'header' => 'Item Code', ], [ 'attribute' => 'item_id', 'value' => 'item.name', 'header' => 'Item Name', ], 'quantity', 'remarks', ], ]); ?> </div> </div>
[/collapsed]
[collapsed title=Penampakan View Transaksi Barang][/collapsed]
Simpulan
Mudah-mudahan tutorial ini dapat membantu teman-teman yang sedang belajar membuat aplikasi inventory, khususnya yang menggunakan Yii Framework. Source code aplikasi ini dapat dilihat di GitHub, silakan digunakan untuk keperluan pembelajaran saja.
.