Membuat Aplikasi Invoice Laravel 5.7 #4: Create Invoice

Membuat Aplikasi Invoice Laravel 5.7 #4: Create Invoice

Pendahuluan

Lanjutan dari serial sebelumnya, kali ini kita akan membuat sebuah fitur untuk meng-handle invoice, seperti membuat invoice baru dan menampilkan invoice tersebut beserta detail yang telah di-input oleh user. Masih seputaran CRUD, sebab sepanjang serial Membuat aplikasi Invoice Laravel 5.7 adalah "seni" memainkan CRUD sehingga menjadi sebuah aplikasi yang diinginkan.

Kenapa masih disebut bagian dari CRUD? Sebab fitur untuk membuat invoice baru, kita menggunakan fungsi Create. sedangkan fitur untuk menampilkan, invoice yang digunakan adalah fungsi Read. Ada dua point yang akan kita capai pada artikel ini, yakni membuat invoice dari halaman invoice itu sendiri lalu menampilkannya dan atau membuat invoice dari halaman customer.

Baca Juga: Membuat Aplikasi Invoice Laravel 5.7 #1: Generate Database

Create new Invoice

Pada fitur ini, schema-nya adalah user terlebih dahulu memilih customer, lalu di-direct ke halaman selanjutnya untuk menambahkan produk yang akan dimasukkan ke invoice. Ada dua hal yang berperan, pertama fungsi untuk meng-input data ke table invoices, kemudian ketika menambahkan produk fungsi yang bekerja selanjutnya adalah untuk menyimpan ke table invoice_details lalu meng-update kembali table invoices, dalam hal ini field total.

Buat controller InvoiceController dengan command:

php artisan make:controller InvoiceController

Kemudian tambahkan code berikut:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Customer;

class InvoiceController extends Controller
{
    public function create()
    {
        $customers = Customer::orderBy('created_at', 'DESC')->get();
        return view('invoice.create', compact('customers'));
    }
}

Kemudian buat view create.blade.php didalam folder resources/views/invoice dan masukkan code berikut:

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-12">
                <div class="card">
                    <div class="card-header">
                        <h3 class="card-title">Buat Invoice</h3>
                    </div>
                    <div class="card-body">
                        @if (session('error'))
                            <div class="alert alert-danger">
                                {{ session('error') }}
                            </div>
                        @endif

                        <form action="{{ route('invoice.store') }}" method="post">
                            @csrf
                            <div class="form-group">
                                <label for="">Customer</label>
                                <select name="customer_id" class="form-control" required>
                                    <option value="">Pilih</option>
                                    @foreach ($customers as $customer) 
                                    <option value="{{ $customer->id }}">{{ $customer->name }} - {{ $customer->email }}</option>
                                    @endforeach
                                </select>
                            </div>
                            <div class="form-group">
                                <button class="btn btn-primary btn-sm">Buat</button>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

Penjelasan: Ada code yang baru yakni: {{ route('invoice.store') }}, dimana kita menggunakan helper route() untuk membuat url yang dinamis. Perbedaannya dengan helper url(), ketika path dari url berubah maka path didalam helper url() juga harus disesuaikan. Sedangkan untuk penggunakan helper route(), kita cukup mendefinisikan name dari route tersebut, lalu menggunakan name tersebut. Apabila path url berubah, maka kita tidak perlu menyesuaikan pathnya pada helper route() karena yang digunakan adalah name-nya bukan path-nya.

Selanjutnya definisikan route dengan name invoice.store, buka file routes/web.php kemudian tambahkan code berikut:

Route::group(['prefix' => 'invoice'], function() {
    //ROUTE UNTUK HALAMAN INVOICE
    Route::get('/new', 'InvoiceController@create')->name('invoice.create');
    //ROUTE UNTUK MENG-HANDLE DATA YANG DIKIRIM
    Route::post('/', 'InvoiceController@save')->name('invoice.store');
});

Pada InvoiceController tambahkan method save():

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Customer;
use App\Invoice; // ADD USE STATEMENT

class InvoiceController extends Controller
{
    // [.. CODE SEBELUMNYA ..]
    public function save(Request $request)
    {
        //VALIDASI
        $this->validate($request, [
            'customer_id' => 'required|exists:customers,id'
        ]);

        try {
            //MENYIMPAN DATA KE TABLE INVOICES
            $invoice = Invoice::create([
                'customer_id' => $request->customer_id,
                'total' => 0
            ]);
			
            //REDIRECT KE ROUTE invoice.edit DENGAN MENGIRIMKAN PARAMETER ID
            return redirect(route('invoice.edit', ['id' => $invoice->id]));
        } catch(\Exception $e) {
            //JIKA GAGAL REDIRECT BACK KE FORM, DAN MENAMPILKAN ERROR MESSAGE
            return redirect()->back()->with(['error' => $e->getMessage()]);
        }
    }
    
    public function edit($id)
    {
        $invoice = Invoice::with(['customer', 'detail', 'detail.product'])->find($id);
        $products = Product::orderBy('title', 'ASC')->get();
        return view('invoice.edit', compact('invoice', 'products'));
    }
}

Kembali ke routes/web.php kemudian tambahkan route untuk edit data invoice:

Route::group(['prefix' => 'invoice'], function() {
    // [.. CODE SEBELUMNYA ..]
    Route::get('/{id}', 'InvoiceController@edit')->name('invoice.edit');
});

Kemudian buat view edit.blade.php untuk meng-handle form memasukkan produk ke invoice, tambahkan code berikut:

@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-12">
                <div class="card">
                    <div class="card-body">
                        @if (session('error'))
                            <div class="alert alert-danger">
                                {{ session('error') }}
                            </div>
                        @endif

                        <div class="row">
                            <div class="col-md-12">
                                <div class="text-center">
                                    <img src="{{ asset('laravel.png') }}" alt="" width="350px" height="150px">
                                </div>
                            </div>
                            <div class="col-md-6">
                                <table>
                                    <tr>
                                        <td width="30%">Pelanggan</td>
                                        <td>:</td>
                                        <td>{{ $invoice->customer->name }}</td>
                                    </tr>
                                    <tr>
                                        <td>Alamat</td>
                                        <td>:</td>
                                        <td>{{ $invoice->customer->address }}</td>
                                    </tr>
                                    <tr>
                                        <td>No Telp</td>
                                        <td>:</td>
                                        <td>{{ $invoice->customer->phone }}</td>
                                    </tr>
                                    <tr>
                                        <td>Email</td>
                                        <td>:</td>
                                        <td>{{ $invoice->customer->email }}</td>
                                    </tr>
                                </table>
                            </div>
                            <div class="col-md-6">
                                <table>
                                    <tr>
                                        <td width="30%">Perusahaan</td>
                                        <td>:</td>
                                        <td>Daengweb</td>
                                    </tr>
                                    <tr>
                                        <td>Alamat</td>
                                        <td>:</td>
                                        <td>Jl Sultan Hasanuddin Makassar</td>
                                    </tr>
                                    <tr>
                                        <td>No Telp</td>
                                        <td>:</td>
                                        <td>085343966997</td>
                                    </tr>
                                    <tr>
                                        <td>Email</td>
                                        <td>:</td>
                                        <td>[email protected]</td>
                                    </tr>
                                </table>
                            </div>
                            <div class="col-md-12 mt-3">
                                <form action="{{ route('invoice.update', ['id' => $invoice->id]) }}" method="post">
                                @csrf
                                <table class="table table-hover table-bordered">
                                    <thead>
                                        <tr>
                                            <td>#</td>
                                            <td>Produk</td>
                                            <td>Qty</td>
                                            <td>Harga</td>
                                            <td>Subtotal</td>
                                            <th>Action</th>
                                        </tr>
                                    </thead>
                                    
                                    <!-- MENAMPILKAN PRODUK YANG TELAH DITAMBAHKAN -->
                                    <tbody>
                                        @php $no = 1 @endphp
                                        @foreach ($invoice->detail as $detail)
                                        <tr>
                                            <td>{{ $no++ }}</td>
                                            <td>{{ $detail->product->title }}</td>
                                            <td>{{ $detail->qty }}</td>
                                            <td>Rp {{ number_format($detail->price) }}</td>
                                            <td>Rp {{ $detail->subtotal }}</td>
                                            <td>
                                                <form action="{{ route('invoice.delete_product', ['id' => $detail->id]) }}" method="post">
                                                    @csrf
                                                    <input type="hidden" name="_method" value="DELETE" class="form-control">
                                                    <button class="btn btn-danger btn-sm">Hapus</button>
                                                </form>
                                            </td>
                                        </tr>
                                        @endforeach
                                    </tbody>
                                    <!-- MENAMPILKAN PRODUK YANG TELAH DITAMBAHKAN -->
                                    
                                    <!-- FORM UNTUK MEMILIH PRODUK YANG AKAN DITAMBAHKAN -->
                                    <tfoot>
                                        <tr>
                                            <td></td>
                                            <td>
                                                <input type="hidden" name="_method" value="PUT" class="form-control">
                                                <select name="product_id" class="form-control">
                                                    <option value="">Pilih Produk</option>
                                                    @foreach ($products as $product)
                                                    <option value="{{ $product->id }}">{{ $product->title }}</option>
                                                    @endforeach
                                                </select>
                                            </td>
                                            <td>
                                                <input type="number" min="1" value="1" name="qty" class="form-control" required>
                                            </td>
                                            <td colspan="3">
                                                <button class="btn btn-primary btn-sm">Tambahkan</button>
                                            </td>
                                        </tr>
                                    </tfoot>
                                    <!-- FORM UNTUK MEMILIH PRODUK YANG AKAN DITAMBAHKAN -->
                                </table>
                                </form>
                            </div>
                            
                            <!-- MENAMPILKAN TOTAL & TAX -->
                            <div class="col-md-4 offset-md-8">
                                <table class="table table-hover table-bordered">
                                    <tr>
                                        <td>Sub Total</td>
                                        <td>:</td>
                                        <td>Rp {{ number_format($invoice->total) }}</td>
                                    </tr>
                                    <tr>
                                        <td>Pajak</td>
                                        <td>:</td>
                                        <td>2% (Rp {{ number_format($invoice->tax) }})</td>
                                    </tr>
                                    <tr>
                                        <td>Total</td>
                                        <td>:</td>
                                        <td>Rp {{ number_format($invoice->total_price) }}</td>
                                    </tr>
                                </table>
                            </div>
                            <!-- MENAMPILKAN TOTAL & TAX -->
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

Penjelasan: Ada beberapa hal yang perlu diperhatikan, pertama $invoice->total_price, $invoice->tax, $detail->subtotal adalah attribute dari accessors yang akan dijelaskan pada sub topik selanjutnya. Selanjutnya, pada form add product akan mengirimkan product_id dan qty yang akan ditambahkan kedalam table invoice_details. Tombol tambahkan akan mengirim data tersebut ke route invoice.update dan tombol hapus akan mengirimkan data ke route invoice.delete_product.

Buka file InvoiceController.php, kemudian tambahkan method berikut:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Customer;
use App\Invoice;
//TAMBAHKAN USE STATEMENT
use App\Product; 
use App\Invoice_detail;

class InvoiceController extends Controller
{
	// [.. CODE SEBELUMNYA ..]
    public function update(Request $request, $id)
    {
        //VALIDASI
        $this->validate($request, [
            'product_id' => 'required|exists:products,id',
            'qty' => 'required|integer'
        ]);

        try {
            //SELECT DARI TABLE invoices BERDASARKAN ID
            $invoice = Invoice::find($id);
            //SELECT DARI TABLE products BERDASARKAN ID
            $product = Product::find($request->product_id);
            //SELECT DARI TABLE invoice_details BERDASARKAN product_id & invoice_id
            $invoice_detail = $invoice->detail()->where('product_id', $product->id)->first();
            
            //JIKA DATANYA ADA
            if ($invoice_detail) {
                //MAKA DATA TERSEBUT DI UPDATE QTY NYA
                $invoice_detail->update([
                    'qty' => $invoice_detail->qty + $request->qty
                ]);
            } else {
                //JIKA TIDAK MAKA DITAMBAHKAN RECORD BARU
                Invoice_detail::create([
                    'invoice_id' => $invoice->id,
                    'product_id' => $request->product_id,
                    'price' => $product->price,
                    'qty' => $request->qty
                ]);
            }
            
            //KEMUDIAN DI-REDIRECT KEMBALI KE FORM YANG SAMA
            return redirect()->back()->with(['success' => 'Product Telah Ditambahkan']);
        } catch (\Exception $e) {
            return redirect()->back()->with(['error' => $e->getMessage()]);
        }
    }

    public function deleteProduct($id)
    {
        //SELECT DARI TABLE invoice_details BERDASARKAN ID
        $detail = Invoice_detail::find($id);
        //KEMUDIAN DIHAPUS
        $detail->delete();
        //DAN DI-REDIRECT KEMBALI
        return redirect()->back()->with(['success' => 'Product telah dihapus']);
    }
}

Definisikan route-nya dengan menambahkan code berikut kedalam file routes/web.php:

Route::group(['prefix' => 'invoice'], function() {
    //[.. CODE SEBELUMNYA ..]
    Route::put('/{id}', 'InvoiceController@update')->name('invoice.update');
    Route::delete('/{id}', 'InvoiceController@deleteProduct')->name('invoice.delete_product');
});

Baca Juga: Fitur Upload Progress Bar Indikator Vue Laravel

Define Accessors & Relationships

Accessor atau yang lebih familiar adalah getters dimana memungkinkan untuk memanipulasi data dari sebuah attribute sebelum ditampilkan atau juga dapat membuat attribute baru (Note: Sebelumnya sudah saya tulis dalam artikel Accessor & Mutator Laravel 5.4). Pada kasus diatas kita membuat attribute baru yakni total_price, tax dan sub_total. Buka file app/Invoice.php, kemudian tambahkan method berikut:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Invoice extends Model
{
    protected $guarded = []; //JANGAN LUPA TAMBAHKAN CODE INI
    //AGAR DAPAT MENYIMPAN DATA KEDALAM TABLE TERKAIT

    //DEFINE ACCESSOR
    public function getTaxAttribute()
    {
        //MENDAPATKAN TAX 2% DARI TOTAL HARGA
        return ($this->total * 2) / 100; 
    }
    
    public function getTotalPriceAttribute()
    {
        //MENDAPATKAN TOTAL HARGA BARU YANG TELAH DIJUMLAHKAN DENGAN TAX
        return ($this->total + (($this->total * 2) / 100));
    }
    
    //DEFINE RELATIONSHIPS
    public function customer()
    {
        //Invoice reference ke table customers
        return $this->belongsTo(Customer::class);
    }

    public function detail()
    {
        //Invoice memiliki hubungan hasMany ke table invoice_detail
        return $this->hasMany(Invoice_detail::class);
    }
}

Penjelasan: Accessor ditandai dengan format getNamaBaruAttribute(), dimana NamaBaru adalah nama accessor yang akan dibuat, maka dalam hal ini adalah Tax dan TotalPrice.

Model selanjutnya yakni app/Invoice_detail.php, tambahkan code berikut:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Invoice_detail extends Model
{
    protected $guarded = [];

    //DEFINE ACCESSOR
    public function getSubtotalAttribute()
    {
        //NILAI DARI SUBTOTAL ADALAH QTY * PRICE
        return number_format($this->qty * $this->price);
    }

    //DEFINE RELATIONSHIPS
    public function product()
    {
        return $this->belongsTo(Product::class);
    }

    public function invoice()
    {
        return $this->belongsTo(Invoice::class);
    }
}

Make Observer Event

Event berjalan sesuai dengan trigger yang telah ditetapkan, dalam hal ini kita akan meng-update total pada table invoices ketika ada record baru yang telah ditambahkan kedalam table invoice_details. Tidak hanya ketika data baru tersebut disimpan, juga berlaku ketika terjadi perubahan pada data yang telah ada maupun data tersebut dihapus. Maka akan ada tiga event yang akan berjalan, yakni: created, updated dan deleted.

Buat observer dengan command:

php artisan make:observer Invoice_detailObserver --model=Invoice_detail

Kemudian tambahkan method berikut:

<?php

namespace App\Observers;

//TAMBAHKAN USE STATEMENT
use App\Invoice_detail;
use App\Invoice;

class Invoice_detailObserver
{	
    //KARENA FUNGSI YANG DIJALANKAN SAMA, MAKA KITA MEMBUATNYA KEDALAM FUNGCTION BARU
    private function generateTotal($invoiceDetail)
    {
        //MENGAMBIL INVOICE_ID
        $invoice_id = $invoiceDetail->invoice_id;
        //SELECT DARI TABLE invoice_details BERDASARKAN INVOICE
        $invoice_detail = Invoice_detail::where('invoice_id', $invoice_id)->get();
        //KEMUDIAN DIJUMLAH UNTUK MENDAPATKAN TOTALNYA
        $total = $invoice_detail->sum(function($i) {
            //DIMANA KETENTUAN YANG DIJUMLAHKAN ADALAH HASIL DARI price* qty
            return $i->price * $i->qty;
        });
        //UPDATE TABLE invoice PADA FIELD TOTAL
        $invoiceDetail->invoice()->update([
            'total' => $total
        ]);
    }

    public function created(Invoice_detail $invoiceDetail)
    {
        //PANGGIL METHOD BARU TERSEBUT
        $this->generateTotal($invoiceDetail);
    }

    public function updated(Invoice_detail $invoiceDetail)
    {
        //PANGGIL METHOD BARU TERSEBUT
        $this->generateTotal($invoiceDetail);
    }

    public function deleted(Invoice_detail $invoiceDetail)
    {
        //PANGGIL METHOD BARU TERSEBUT
        $this->generateTotal($invoiceDetail);
    }

    public function restored(Invoice_detail $invoiceDetail)
    {
        //
    }

    /**
     * Handle the invoice_detail "force deleted" event.
     *
     * @param  \App\Invoice_detail  $invoiceDetail
     * @return void
     */
    public function forceDeleted(Invoice_detail $invoiceDetail)
    {
        //
    }
}

Agar observer ini dapat berjalan ketika eventnya terpenuhi, maka definisikan terlebih dahulu kedalam Service providers. Buka file app/Providers/AppServiceProvider.php, kemudian modifikasi menjadi:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
//TAMBAHKAN USE STATEMENT
use App\Invoice_detail;
use App\Observers\Invoice_detailObserver;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //DEFINE OBSERVER YANG TELAH DIBUAT
        //Invoce_detail adalah nama class dari model
        //Invoice_detailObserver adalah nama class dari observer itu sendiri
        Invoice_detail::observe(Invoice_detailObserver::class);
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}

Agar bisa diklik dari menu navigasi, pada file resources/views/layouts/app.blade.php, tambahkan tag berikut:

<li class="nav-item">
    <a href="{{ route('invoice.create') }}" class="nav-link">Buat Invoice</a>
</li>

Maka hasil yang akan diperoleh akan tampak seperti berikut

add product to invoice laravel

Create Invoice From Customer

Schemanya adalah ketika user menekan tombol buat invoice dari halaman list pelanggan, maka secara otomatis akan membuat sebuah invoice baru dan diarahkan kehalaman tambah produk kedalam invoice tersebut. Hal yang perlu dimodifikasi adalah file index.blade.php pada folder resources/views/customer, lakukan perubahan pada bagian berikut:

<!-- [... CODE SEBELUMNYA ...] -->

<table class="table table-hover table-bordered">
    <thead>
        <tr>
            <th>Nama Lengkap</th>
            <th>No Telp</th>
            <th>Alamat</th>
            <th>Email</th>
            <th colspan="2">Aksi</th> <!-- MODIFIKASI INI DENGAN MENAMBAHKAN COLSPAN -->
        </tr>
    </thead>
    <tbody>
        @forelse($customers as $customer)
        <tr>
            <td>{{ $customer->name }}</td>
            <td>{{ $customer->phone }}</td>
            <td>{{ str_limit($customer->address, 50) }}</td>
            <td><a href="mailto:{{ $customer->email }}">{{ $customer->email }}</a></td>
            <td>
                <form action="{{ url('/customer/' . $customer->id) }}" method="POST">
                    @csrf
                    <input type="hidden" name="_method" value="DELETE" class="form-control">
                    <a href="{{ url('/customer/' . $customer->id) }}" class="btn btn-warning btn-sm">Edit</a>
                    <button class="btn btn-danger btn-sm">Hapus</button>
                </form>
            </td>
            
            <!-- [... TAMBAHKAN FORM INI ...] -->
            <!-- KARENA YANG DIBUTUHKAN METHOD POST MAKA KITA MEMASUKKANNYA KEDALAM FORM -->
            <td>
                <form action="{{ route('invoice.store') }}" method="post">
                    @csrf
                    <input type="hidden" name="customer_id" value="{{ $customer->id }}" class="form-control">
                    <button class="btn btn-primary btn-sm">Buat Invoice</button>
                </form>
            </td>
            <!-- [... TAMBAHKAN FORM INI ...] -->
        </tr>
        @empty
        <tr>
            <td class="text-center" colspan="5">Tidak ada data</td>
        </tr>
        @endforelse
    </tbody>
</table>

<!-- [... CODE SETELAHNYA ...] -->

Adapun hasil yang akan diperoleh adalah

create invoice from customer page - laravel invoice

Kesimpulan

Crud yang dibolak balik untuk menyelesaikan fitur ini, itulah istilah "tepat" yang saya gunakan. Sebab sepanjang artikel ini kita hanya memanipulasi database sesuai dengan kebutuhan. Maka penting adanya untuk memahami cara kerja dari sebuah operasi, sebab kerap kali operasi sederhana yang telah kita pelajari sebelumnya memberikan pengaruh besar dalam proses pembuatan sebuah aplikasi. Sebut saja, CRUD yang terlihat sederhana dan sudah "mainstream" namun banyak digunakan. Benar, bahwa sebuah aplikasi tidak hanya sebatas CRUD namun bukan berarti CRUD tidak memiliki peran didalamnya.

Ohya kamu dapat melihat dokumentasi code dari artikel ini di Github.

Category:
Share:

Comments