Browse Source
Add invoice edit feature
Add invoice edit feature
Move Invoice related controllers into Invoices namespace Add date and due_date column on invoice datepull/3/head
20 changed files with 559 additions and 141 deletions
-
15app/Http/Controllers/Invoices/DraftsController.php
-
55app/Http/Controllers/Invoices/InvoicesController.php
-
75app/Http/Controllers/Invoices/ItemsController.php
-
30app/Http/Controllers/InvoicesController.php
-
6app/Services/InvoiceDraft/InvoiceDraft.php
-
8app/Services/InvoiceDraft/InvoiceDraftCollection.php
-
22database/factories/InvoiceFactory.php
-
17database/factories/ModelFactory.php
-
2database/migrations/2017_10_05_162758_create_invoices_table.php
-
5resources/lang/id/invoice.php
-
6resources/views/invoice-drafts/partials/form-draft-detail.blade.php
-
59resources/views/invoices/edit.blade.php
-
18resources/views/invoices/index.blade.php
-
77resources/views/invoices/partials/item-list.blade.php
-
68resources/views/invoices/show.blade.php
-
16resources/views/projects/invoices.blade.php
-
34routes/web/invoices.php
-
23tests/Feature/Invoices/InvoiceEntryTest.php
-
19tests/Feature/Invoices/ManageInvoiceTest.php
-
145tests/Feature/Invoices/ManageInvoicesTest.php
@ -0,0 +1,55 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Controllers\Invoices; |
|||
|
|||
use App\Entities\Invoices\Invoice; |
|||
use App\Entities\Projects\Project; |
|||
use App\Http\Controllers\Controller; |
|||
|
|||
/** |
|||
* Invoices Controller. |
|||
* |
|||
* @author Nafies Luthfi <nafiesL@gmail.com> |
|||
*/ |
|||
class InvoicesController extends Controller |
|||
{ |
|||
public function index() |
|||
{ |
|||
$invoices = Invoice::paginate(); |
|||
|
|||
return view('invoices.index', compact('invoices')); |
|||
} |
|||
|
|||
public function show(Invoice $invoice) |
|||
{ |
|||
return view('invoices.show', compact('invoice')); |
|||
} |
|||
|
|||
public function edit(Invoice $invoice) |
|||
{ |
|||
$projects = Project::pluck('name', 'id'); |
|||
|
|||
return view('invoices.edit', compact('invoice', 'projects')); |
|||
} |
|||
|
|||
public function update(Invoice $invoice) |
|||
{ |
|||
$invoiceData = request()->validate([ |
|||
'project_id' => 'required|exists:projects,id', |
|||
'date' => 'required|date', |
|||
'due_date' => 'nullable|date|after:date', |
|||
'notes' => 'nullable|string|max:255', |
|||
]); |
|||
|
|||
$invoice->update($invoiceData); |
|||
|
|||
flash(trans('invoice.updated'), 'success'); |
|||
|
|||
return redirect()->route('invoices.show', $invoice); |
|||
} |
|||
|
|||
public function pdf(Invoice $invoice) |
|||
{ |
|||
return view('invoices.pdf', compact('invoice')); |
|||
} |
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Controllers\Invoices; |
|||
|
|||
use App\Entities\Invoices\Invoice; |
|||
use App\Http\Controllers\Controller; |
|||
|
|||
class ItemsController extends Controller |
|||
{ |
|||
public function store(Invoice $invoice) |
|||
{ |
|||
$itemData = request()->validate([ |
|||
'new_item_description' => 'required|string|max:255', |
|||
'new_item_amount' => 'required|numeric', |
|||
]); |
|||
|
|||
$items = $invoice->items; |
|||
$items[] = [ |
|||
'description' => $itemData['new_item_description'], |
|||
'amount' => $itemData['new_item_amount'], |
|||
]; |
|||
$invoice->items = $items; |
|||
$invoice->amount = collect($items)->sum('amount'); |
|||
$invoice->save(); |
|||
|
|||
flash(trans('invoice.item_added')); |
|||
|
|||
return back(); |
|||
} |
|||
|
|||
public function update(Invoice $invoice) |
|||
{ |
|||
$rawItemData = request()->validate([ |
|||
'item_key.*' => 'required|numeric', |
|||
'description.*' => 'required|string|max:255', |
|||
'amount.*' => 'required|numeric', |
|||
]); |
|||
|
|||
$itemKey = array_shift($rawItemData['item_key']); |
|||
$amount = array_shift($rawItemData['amount']); |
|||
$description = array_shift($rawItemData['description']); |
|||
|
|||
$items = $invoice->items; |
|||
$items[$itemKey] = [ |
|||
'description' => $description, |
|||
'amount' => $amount, |
|||
]; |
|||
$invoice->items = $items; |
|||
$invoice->amount = collect($items)->sum('amount'); |
|||
$invoice->save(); |
|||
|
|||
flash(trans('invoice.item_updated')); |
|||
|
|||
return back(); |
|||
} |
|||
|
|||
public function destroy(Invoice $invoice) |
|||
{ |
|||
$itemData = request()->validate([ |
|||
'item_index' => 'required|numeric', |
|||
]); |
|||
|
|||
$itemIndex = $itemData['item_index']; |
|||
|
|||
$items = $invoice->items; |
|||
unset($items[$itemIndex]); |
|||
$invoice->items = $items; |
|||
$invoice->amount = (int) collect($items)->sum('amount'); |
|||
$invoice->save(); |
|||
|
|||
flash(trans('invoice.item_removed')); |
|||
|
|||
return back(); |
|||
} |
|||
} |
|||
@ -1,30 +0,0 @@ |
|||
<?php |
|||
|
|||
namespace App\Http\Controllers; |
|||
|
|||
use App\Entities\Invoices\Invoice; |
|||
|
|||
/** |
|||
* Invoices Controller. |
|||
* |
|||
* @author Nafies Luthfi <nafiesL@gmail.com> |
|||
*/ |
|||
class InvoicesController extends Controller |
|||
{ |
|||
public function index() |
|||
{ |
|||
$invoices = Invoice::paginate(); |
|||
|
|||
return view('invoices.index', compact('invoices')); |
|||
} |
|||
|
|||
public function show(Invoice $invoice) |
|||
{ |
|||
return view('invoices.show', compact('invoice')); |
|||
} |
|||
|
|||
public function pdf(Invoice $invoice) |
|||
{ |
|||
return view('invoices.pdf', compact('invoice')); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
<?php |
|||
|
|||
use App\Entities\Invoices\Invoice; |
|||
use App\Entities\Projects\Project; |
|||
use App\Entities\Users\User; |
|||
use Faker\Generator as Faker; |
|||
|
|||
$factory->define(Invoice::class, function (Faker $faker) { |
|||
return [ |
|||
'project_id' => function () { |
|||
return factory(Project::class)->create()->id; |
|||
}, |
|||
'number' => (new Invoice)->generateNewNumber(), |
|||
'items' => [], |
|||
'date' => '2010-10-10', |
|||
'amount' => 100000, |
|||
'status_id' => 1, |
|||
'creator_id' => function () { |
|||
return factory(User::class)->create()->id; |
|||
}, |
|||
]; |
|||
}); |
|||
@ -0,0 +1,59 @@ |
|||
@extends('layouts.app') |
|||
|
|||
@section('title', $invoice->number . ' - ' . trans('invoice.edit')) |
|||
|
|||
@section('content') |
|||
<h1 class="page-header"> |
|||
<div class="pull-right"> |
|||
{{ link_to_route('invoices.show', trans('invoice.back_to_show'), $invoice, ['class' => 'btn btn-default']) }} |
|||
</div> |
|||
{{ $invoice->number }} <small>{{ trans('invoice.edit') }}</small> |
|||
</h1> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-4"> |
|||
<div class="panel panel-default"> |
|||
<div class="panel-heading"><h3 class="panel-title">{{ trans('invoice.detail') }}</h3></div> |
|||
{{ Form::model($invoice, ['route' => ['invoices.update', $invoice], 'method' => 'patch']) }} |
|||
<div class="panel-body"> |
|||
{!! FormField::select('project_id', $projects, ['label' => trans('project.project')]) !!} |
|||
<div class="row"> |
|||
<div class="col-md-6">{!! FormField::text('date', ['label' => trans('invoice.date')]) !!}</div> |
|||
<div class="col-md-6">{!! FormField::text('due_date', ['label' => trans('invoice.due_date')]) !!}</div> |
|||
</div> |
|||
{!! FormField::textarea('notes', ['label' => trans('invoice.notes')]) !!} |
|||
</div> |
|||
<div class="panel-footer"> |
|||
{{ Form::submit(trans('invoice.update'), ['class' => 'btn btn-info']) }} |
|||
{{ link_to_route('invoices.show', trans('invoice.back_to_show'), $invoice, ['class' => 'btn btn-default']) }} |
|||
</div> |
|||
{{ Form::close() }} |
|||
</div> |
|||
</div> |
|||
<div class="col-md-8"> |
|||
@include('invoices.partials.item-list') |
|||
</div> |
|||
</div> |
|||
|
|||
@endsection |
|||
|
|||
@section('ext_css') |
|||
{!! Html::style(url('assets/css/plugins/jquery.datetimepicker.css')) !!} |
|||
@endsection |
|||
|
|||
@section('ext_js') |
|||
{!! Html::script(url('assets/js/plugins/jquery.datetimepicker.js')) !!} |
|||
@endsection |
|||
|
|||
@section('script') |
|||
<script> |
|||
(function() { |
|||
$('#date,#due_date').datetimepicker({ |
|||
timepicker:false, |
|||
format:'Y-m-d', |
|||
closeOnDateSelect: true, |
|||
scrollInput: false |
|||
}); |
|||
})(); |
|||
</script> |
|||
@endsection |
|||
@ -0,0 +1,77 @@ |
|||
<div class="panel panel-default"> |
|||
<div class="panel-heading"> |
|||
<h3 class="panel-title"> |
|||
{{ trans('invoice.items') }} |
|||
<small class="text-muted">({{ count($invoice->items) }} Item)</small> |
|||
</h3> |
|||
</div> |
|||
<table class="table"> |
|||
<thead> |
|||
<tr> |
|||
<th>#</th>
|
|||
<th>Deskripsi</th> |
|||
<th class="text-center">Biaya</th> |
|||
<th class="text-center">Action</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<?php $no = 1?>
|
|||
@foreach($invoice->items as $key => $item) |
|||
<tr> |
|||
<td>{{ $no }}</td> |
|||
<?php $no++;?>
|
|||
{{ Form::open(['route' => ['invoices.items.update', $invoice], 'method' => 'patch']) }} |
|||
{{ Form::hidden('item_key['.$key.']', $key) }} |
|||
<td class="col-md-8"> |
|||
{!! FormField::textarea( |
|||
'description['.$key.']', |
|||
['id' => 'description-'.$key, 'value' => $item['description'], 'label' => false] |
|||
) !!} |
|||
</td> |
|||
<td class="col-md-3"> |
|||
{!! FormField::price( |
|||
'amount['.$key.']', |
|||
['id' => 'amount-'.$key, 'value' => $item['amount'], 'label' => false] |
|||
) !!} |
|||
{{ Form::submit('Update', ['id' => 'update-item-'.$key, 'class' => 'btn btn-success btn-xs pull-right']) }} |
|||
</td> |
|||
{{ Form::close() }} |
|||
<td class="col-md-1 text-center show-on-hover-parent"> |
|||
{!! FormField::delete([ |
|||
'route' => ['invoices.items.destroy', $invoice], |
|||
'onsubmit' => 'Yakin ingin menghapus Item ini?', |
|||
'class' => '', |
|||
], 'x', ['id' => 'remove-item-'.$key, 'class' => 'btn btn-danger btn-xs show-on-hover','title' => 'Hapus item ini'], ['item_index' => $key]) !!} |
|||
</td> |
|||
</tr> |
|||
@endforeach |
|||
<tr> |
|||
<th colspan="4">Tambah Item Invoice</th> |
|||
</tr> |
|||
<tr> |
|||
{{ Form::open(['route' => ['invoices.items.store', $invoice]]) }} |
|||
<td colspan="2"> |
|||
{!! FormField::textarea( |
|||
'new_item_description', |
|||
['id' => 'new_item_description', 'label' => false, 'placeholder' => trans('invoice.item_description')] |
|||
) !!} |
|||
</td> |
|||
<td colspan="2"> |
|||
{!! FormField::price( |
|||
'new_item_amount', |
|||
['id' => 'new_item_amount', 'label' => false, 'placeholder' => trans('invoice.item_amount')] |
|||
) !!} |
|||
{{ Form::submit(trans('invoice.add_item'), ['class' => 'btn btn-primary btn-block']) }} |
|||
</td> |
|||
{{ Form::close() }} |
|||
</tr> |
|||
</tbody> |
|||
<tfoot> |
|||
<tr> |
|||
<th colspan="2" class="text-right">{{ trans('invoice.amount') }} :</th> |
|||
<th class="text-right">{{ formatRp($invoice->amount) }}</th> |
|||
<th></th> |
|||
</tr> |
|||
</tfoot> |
|||
</table> |
|||
</div> |
|||
@ -1,25 +1,31 @@ |
|||
<?php |
|||
|
|||
Route::group(['middleware' => ['web', 'role:admin']], function () { |
|||
Route::group(['middleware' => ['web', 'role:admin'], 'namespace' => 'Invoices'], function () { |
|||
/* |
|||
* Invoice Drafts Routes |
|||
*/ |
|||
Route::get('invoice-drafts', 'InvoiceDraftsController@index')->name('invoice-drafts.index'); |
|||
Route::get('invoice-drafts/{draftKey}', 'InvoiceDraftsController@show')->name('invoice-drafts.show'); |
|||
Route::post('invoice-drafts', 'InvoiceDraftsController@create')->name('invoice-drafts.create'); |
|||
Route::post('invoice-drafts/{draftKey}/add-draft-item', 'InvoiceDraftsController@addDraftItem')->name('invoice-drafts.add-draft-item'); |
|||
Route::patch('invoice-drafts/{draftKey}/update-draft-item', 'InvoiceDraftsController@updateDraftItem')->name('invoice-drafts.update-draft-item'); |
|||
Route::patch('invoice-drafts/{draftKey}/proccess', 'InvoiceDraftsController@proccess')->name('invoice-drafts.draft-proccess'); |
|||
Route::delete('invoice-drafts/{draftKey}/remove-draft-item', 'InvoiceDraftsController@removeDraftItem')->name('invoice-drafts.remove-draft-item'); |
|||
Route::delete('invoice-drafts/{draftKey}/empty-draft', 'InvoiceDraftsController@emptyDraft')->name('invoice-drafts.empty-draft'); |
|||
Route::delete('invoice-drafts/{draftKey}/remove', 'InvoiceDraftsController@remove')->name('invoice-drafts.remove'); |
|||
Route::delete('invoice-drafts/destroy', 'InvoiceDraftsController@destroy')->name('invoice-drafts.destroy'); |
|||
Route::post('invoice-drafts/{draftKey}/store', 'InvoiceDraftsController@store')->name('invoice-drafts.store'); |
|||
Route::get('invoice-drafts', 'DraftsController@index')->name('invoice-drafts.index'); |
|||
Route::get('invoice-drafts/{draftKey}', 'DraftsController@show')->name('invoice-drafts.show'); |
|||
Route::post('invoice-drafts', 'DraftsController@create')->name('invoice-drafts.create'); |
|||
Route::post('invoice-drafts/{draftKey}/add-draft-item', 'DraftsController@addDraftItem')->name('invoice-drafts.add-draft-item'); |
|||
Route::patch('invoice-drafts/{draftKey}/update-draft-item', 'DraftsController@updateDraftItem')->name('invoice-drafts.update-draft-item'); |
|||
Route::patch('invoice-drafts/{draftKey}/proccess', 'DraftsController@proccess')->name('invoice-drafts.draft-proccess'); |
|||
Route::delete('invoice-drafts/{draftKey}/remove-draft-item', 'DraftsController@removeDraftItem')->name('invoice-drafts.remove-draft-item'); |
|||
Route::delete('invoice-drafts/{draftKey}/empty-draft', 'DraftsController@emptyDraft')->name('invoice-drafts.empty-draft'); |
|||
Route::delete('invoice-drafts/{draftKey}/remove', 'DraftsController@remove')->name('invoice-drafts.remove'); |
|||
Route::delete('invoice-drafts/destroy', 'DraftsController@destroy')->name('invoice-drafts.destroy'); |
|||
Route::post('invoice-drafts/{draftKey}/store', 'DraftsController@store')->name('invoice-drafts.store'); |
|||
|
|||
/* |
|||
* Invoices Routes |
|||
*/ |
|||
Route::get('invoices', ['as' => 'invoices.index', 'uses' => 'InvoicesController@index']); |
|||
Route::get('invoices/{invoice}', ['as' => 'invoices.show', 'uses' => 'InvoicesController@show']); |
|||
Route::get('invoices/{invoice}/pdf', ['as' => 'invoices.pdf', 'uses' => 'InvoicesController@pdf']); |
|||
Route::resource('invoices', 'InvoicesController'); |
|||
|
|||
/** |
|||
* Invoice Items Routes |
|||
*/ |
|||
Route::post('invoices/{invoice}/items', ['as' => 'invoices.items.store', 'uses' => 'ItemsController@store']); |
|||
Route::patch('invoices/{invoice}/items', ['as' => 'invoices.items.update', 'uses' => 'ItemsController@update']); |
|||
Route::delete('invoices/{invoice}/items', ['as' => 'invoices.items.destroy', 'uses' => 'ItemsController@destroy']); |
|||
}); |
|||
@ -1,19 +0,0 @@ |
|||
<?php |
|||
|
|||
namespace Tests\Feature\Invoices; |
|||
|
|||
use Tests\TestCase; |
|||
|
|||
class ManageInvoiceTest extends TestCase |
|||
{ |
|||
/** @test */ |
|||
public function user_can_browse_invoice_list_page() |
|||
{ |
|||
$this->adminUserSigningIn(); |
|||
|
|||
$this->visit(route('invoices.index')); |
|||
|
|||
$this->seePageIs(route('invoices.index')); |
|||
$this->see(trans('invoice.list')); |
|||
} |
|||
} |
|||
@ -0,0 +1,145 @@ |
|||
<?php |
|||
|
|||
namespace Tests\Feature\Invoices; |
|||
|
|||
use App\Entities\Invoices\Invoice; |
|||
use Tests\TestCase; |
|||
|
|||
/** |
|||
* Manage Invoices Feature Test. |
|||
* |
|||
* @author Nafies Luthfi <nafiesl@gmail.com> |
|||
*/ |
|||
class ManageInvoicesTest extends TestCase |
|||
{ |
|||
/** @test */ |
|||
public function user_can_browse_invoice_list_page() |
|||
{ |
|||
$this->adminUserSigningIn(); |
|||
|
|||
$invoice = factory(Invoice::class)->create(); |
|||
|
|||
$this->visit(route('invoices.index')); |
|||
|
|||
$this->seePageIs(route('invoices.index')); |
|||
$this->see(trans('invoice.list')); |
|||
$this->see($invoice->number); |
|||
} |
|||
|
|||
/** @test */ |
|||
public function user_can_edit_invoice_data() |
|||
{ |
|||
$this->adminUserSigningIn(); |
|||
$invoice = factory(Invoice::class)->create(); |
|||
|
|||
$this->visit(route('invoices.edit', $invoice)); |
|||
|
|||
$this->submitForm(trans('invoice.update'), [ |
|||
'project_id' => $invoice->project_id, |
|||
'date' => '2011-01-01', |
|||
'due_date' => '2011-01-30', |
|||
'notes' => 'Catatan invoice 123', |
|||
]); |
|||
|
|||
$this->see(trans('invoice.updated')); |
|||
$this->seePageIs(route('invoices.show', $invoice)); |
|||
|
|||
$this->seeInDatabase('invoices', [ |
|||
'id' => $invoice->id, |
|||
'notes' => 'Catatan invoice 123', |
|||
'date' => '2011-01-01', |
|||
'due_date' => '2011-01-30', |
|||
]); |
|||
} |
|||
|
|||
/** @test */ |
|||
public function user_can_add_invoice_item() |
|||
{ |
|||
$this->adminUserSigningIn(); |
|||
$invoice = factory(Invoice::class)->create(); |
|||
|
|||
$this->visit(route('invoices.edit', $invoice)); |
|||
|
|||
$this->submitForm(trans('invoice.add_item'), [ |
|||
'new_item_description' => 'Testing deskripsi invoice item', |
|||
'new_item_amount' => 2000, |
|||
]); |
|||
|
|||
$this->see(trans('invoice.item_added')); |
|||
|
|||
$this->submitForm(trans('invoice.add_item'), [ |
|||
'new_item_description' => 'Testing deskripsi invoice item', |
|||
'new_item_amount' => 3000, |
|||
]); |
|||
|
|||
$this->see(trans('invoice.item_added')); |
|||
|
|||
$this->seePageIs(route('invoices.edit', $invoice)); |
|||
|
|||
$this->seeInDatabase('invoices', [ |
|||
'id' => $invoice->id, |
|||
'items' => '[{"description":"Testing deskripsi invoice item","amount":"2000"},{"description":"Testing deskripsi invoice item","amount":"3000"}]', |
|||
'amount' => 5000, |
|||
]); |
|||
} |
|||
|
|||
/** @test */ |
|||
public function user_can_update_invoice_item() |
|||
{ |
|||
$this->adminUserSigningIn(); |
|||
|
|||
$invoice = factory(Invoice::class)->create([ |
|||
'items' => [ |
|||
['description' => 'Testing deskripsi invoice item', 'amount' => '1111'], |
|||
['description' => 'Testing deskripsi invoice item', 'amount' => '2222'], |
|||
], |
|||
]); |
|||
|
|||
$this->visit(route('invoices.edit', $invoice)); |
|||
|
|||
$this->submitForm('update-item-1', [ |
|||
'item_key[1]' => 1, |
|||
'description[1]' => 'Testing deskripsi Update', |
|||
'amount[1]' => 100, |
|||
]); |
|||
|
|||
$this->see(trans('invoice.item_updated')); |
|||
|
|||
$this->seePageIs(route('invoices.edit', $invoice)); |
|||
|
|||
$this->seeInDatabase('invoices', [ |
|||
'id' => $invoice->id, |
|||
'items' => '[{"description":"Testing deskripsi invoice item","amount":"1111"},{"description":"Testing deskripsi Update","amount":"100"}]', |
|||
'amount' => 1211, |
|||
]); |
|||
} |
|||
|
|||
/** @test */ |
|||
public function user_can_remove_invoice_item() |
|||
{ |
|||
$this->adminUserSigningIn(); |
|||
|
|||
$invoice = factory(Invoice::class)->create([ |
|||
'items' => [ |
|||
['description' => 'Testing deskripsi invoice item', 'amount' => '1111'], |
|||
['description' => 'Testing deskripsi invoice item', 'amount' => '2222'], |
|||
], |
|||
]); |
|||
|
|||
$this->visit(route('invoices.edit', $invoice)); |
|||
|
|||
$this->submitForm('remove-item-1', [ |
|||
'item_index' => 1, |
|||
]); |
|||
|
|||
$this->see(trans('invoice.item_removed')); |
|||
|
|||
$this->seePageIs(route('invoices.edit', $invoice)); |
|||
|
|||
$this->seeInDatabase('invoices', [ |
|||
'id' => $invoice->id, |
|||
'items' => '[{"description":"Testing deskripsi invoice item","amount":"1111"}]', |
|||
'amount' => 1111, |
|||
]); |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue