diff --git a/app/Factory/QuoteFactory.php b/app/Factory/QuoteFactory.php new file mode 100644 index 0000000000..43ffebf7db --- /dev/null +++ b/app/Factory/QuoteFactory.php @@ -0,0 +1,47 @@ +status_id = Quote::STATUS_DRAFT; + $quote->quote_number = ''; + $quote->discount = 0; + $quote->is_amount_discount = true; + $quote->po_number = ''; + $quote->footer = ''; + $quote->terms = ''; + $quote->public_notes = ''; + $quote->private_notes = ''; + $quote->quote_date = null; + $quote->valid_until = null; + $quote->partial_due_date = null; + $quote->is_deleted = false; + $quote->line_items = json_encode([]); + $quote->settings = ClientSettings::buildClientSettings(new CompanySettings(CompanySettings::defaults()), new ClientSettings(ClientSettings::defaults())); //todo need to embed the settings here + $quote->backup = json_encode([]); + $quote->tax_name1 = ''; + $quote->tax_rate1 = 0; + $quote->tax_name2 = ''; + $quote->tax_rate2 = 0; + $quote->custom_value1 = 0; + $quote->custom_value2 = 0; + $quote->custom_value3 = 0; + $quote->custom_value4 = 0; + $quote->amount = 0; + $quote->balance = 0; + $quote->partial = 0; + $quote->user_id = $user_id; + $quote->company_id = $company_id; + + return $quote; + } +} diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php index 4a4b334418..22c162ee62 100644 --- a/app/Filters/InvoiceFilters.php +++ b/app/Filters/InvoiceFilters.php @@ -6,7 +6,7 @@ use App\Models\User; use Illuminate\Database\Eloquent\Builder; /** - * ProductFilters + * InvoiceFilters */ class InvoiceFilters extends QueryFilters { @@ -45,7 +45,7 @@ class InvoiceFilters extends QueryFilters if(strlen($filter) == 0) return $this->builder; - $table = 'products'; + $table = 'invoices'; $filters = explode(',', $filter); return $this->builder->where(function ($query) use ($filters, $table) { diff --git a/app/Filters/QuoteFilters.php b/app/Filters/QuoteFilters.php new file mode 100644 index 0000000000..b54d12ad71 --- /dev/null +++ b/app/Filters/QuoteFilters.php @@ -0,0 +1,111 @@ +builder; + + return $this->builder->where(function ($query) use ($filter) { + $query->where('quotes.custom_value1', 'like', '%'.$filter.'%') + ->orWhere('quotes.custom_value2', 'like' , '%'.$filter.'%') + ->orWhere('quotes.custom_value3', 'like' , '%'.$filter.'%') + ->orWhere('quotes.custom_value4', 'like' , '%'.$filter.'%'); + }); + } + + /** + * Filters the list based on the status + * archived, active, deleted + * + * @param string filter + * @return Illuminate\Database\Query\Builder + */ + public function status(string $filter = '') : Builder + { + if(strlen($filter) == 0) + return $this->builder; + + $table = 'quotes'; + $filters = explode(',', $filter); + + return $this->builder->where(function ($query) use ($filters, $table) { + $query->whereNull($table . '.id'); + + if (in_array(parent::STATUS_ACTIVE, $filters)) { + $query->orWhereNull($table . '.deleted_at'); + } + + if (in_array(parent::STATUS_ARCHIVED, $filters)) { + $query->orWhere(function ($query) use ($table) { + $query->whereNotNull($table . '.deleted_at'); + + if (! in_array($table, ['users'])) { + $query->where($table . '.is_deleted', '=', 0); + } + }); + } + + if (in_array(parent::STATUS_DELETED, $filters)) { + $query->orWhere($table . '.is_deleted', '=', 1); + } + }); + } + + /** + * Sorts the list based on $sort + * + * @param string sort formatted as column|asc + * @return Illuminate\Database\Query\Builder + */ + public function sort(string $sort) : Builder + { + $sort_col = explode("|", $sort); + return $this->builder->orderBy($sort_col[0], $sort_col[1]); + } + + /** + * Returns the base query + * + * @param int company_id + * @return Illuminate\Database\Query\Builder + * @deprecated + */ + public function baseQuery(int $company_id, User $user) : Builder + { + + } + + /** + * Filters the query by the users company ID + * + * @param $company_id The company Id + * @return Illuminate\Database\Query\Builder + */ + public function entityFilter() + { + + return $this->builder->whereCompanyId(auth()->user()->company()->id); + + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php index a9c1717b17..60e279492d 100644 --- a/app/Http/Controllers/QuoteController.php +++ b/app/Http/Controllers/QuoteController.php @@ -2,18 +2,67 @@ namespace App\Http\Controllers; +use App\Factory\QuoteFactory; +use App\Filters\QuoteFilters; +use App\Http\Requests\Quote\ActionQuoteRequest; +use App\Http\Requests\Quote\CreateQuoteRequest; +use App\Http\Requests\Quote\DestroyQuoteRequest; +use App\Http\Requests\Quote\EditQuoteRequest; +use App\Http\Requests\Quote\ShowQuoteRequest; +use App\Http\Requests\Quote\StoreQuoteRequest; +use App\Http\Requests\Quote\UpdateQuoteRequest; +use App\Models\Quote; +use App\Repositories\QuoteRepository; +use App\Transformers\QuoteTransformer; +use App\Utils\Traits\MakesHash; use Illuminate\Http\Request; +/** + * Class QuoteController + * @package App\Http\Controllers\QuoteController + */ + class QuoteController extends BaseController { + + use MakesHash; + + protected $entity_type = Quote::class; + + protected $entity_transformer = QuoteTransformer::class; + + /** + * @var QuoteRepository + */ + protected $quote_repo; + + protected $base_repo; + + /** + * QuoteController constructor. + * + * @param \App\Repositories\QuoteRepository $Quote_repo The Quote repo + */ + public function __construct(QuoteRepository $quote_repo) + { + + parent::__construct(); + + $this->quote_repo = $quote_repo; + + } + /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ - public function index() + public function index(QuoteFilters $filters) { - // + + $quotes = Quote::filter($filters); + + return $this->listResponse($quotes); } /** @@ -21,64 +70,156 @@ class QuoteController extends BaseController * * @return \Illuminate\Http\Response */ - public function create() + public function create(CreateQuoteRequest $request) { - // + + $quote = QuoteFactory::create(auth()->user()->company()->id, auth()->user()->id); + + return $this->itemResponse($quote); + } /** * Store a newly created resource in storage. * - * @param \Illuminate\Http\Request $request + * @param \App\Http\Requests\Quote\StoreQuoteRequest $request The request + * * @return \Illuminate\Http\Response */ - public function store(Request $request) + public function store(StoreQuoteRequest $request) { - // + + $quote = $this->quote_repo->save($request, QuoteFactory::create(auth()->user()->company()->id, auth()->user()->id)); + + return $this->itemResponse($quote); + } /** * Display the specified resource. * - * @param int $id + * @param \App\Http\Requests\Quote\ShowQuoteRequest $request The request + * @param \App\Models\Quote $quote The quote + * * @return \Illuminate\Http\Response */ - public function show($id) + public function show(ShowQuoteRequest $request, Quote $quote) { - // + + return $this->itemResponse($quote); + } /** * Show the form for editing the specified resource. * - * @param int $id + * @param \App\Http\Requests\Quote\EditQuoteRequest $request The request + * @param \App\Models\Quote $quote The quote + * * @return \Illuminate\Http\Response */ - public function edit($id) + public function edit(EditQuoteRequest $request, Quote $quote) { - // - } + return $this->itemResponse($quote); + + } + /** * Update the specified resource in storage. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \App\Http\Requests\Quote\UpdateQuoteRequest $request The request + * @param \App\Models\Quote $quote The quote + * * @return \Illuminate\Http\Response */ - public function update(Request $request, $id) + public function update(UpdateQuoteRequest $request, Quote $quote) { - // + + $quote = $this->quote_repo->save(request(), $quote); + + return $this->itemResponse($quote); + } /** * Remove the specified resource from storage. * - * @param int $id - * @return \Illuminate\Http\Response + * @param \App\Http\Requests\Quote\DestroyQuoteRequest $request + * @param \App\Models\Quote $quote + * + * @return \Illuminate\Http\Response */ - public function destroy($id) + public function destroy(DestroyQuoteRequest $request, Quote $quote) { - // + + $quote->delete(); + + return response()->json([], 200); + } -} + + /** + * Perform bulk actions on the list view + * + * @return Collection + */ + public function bulk() + { + + $action = request()->input('action'); + + $ids = request()->input('ids'); + + $quotes = Quote::withTrashed()->find($ids); + + $quotes->each(function ($quote, $key) use($action){ + + if(auth()->user()->can('edit', $quote)) + $this->quote_repo->{$action}($quote); + + }); + + //todo need to return the updated dataset + return $this->listResponse(Quote::withTrashed()->whereIn('id', $ids)); + + } + + public function action(ActionQuoteRequest $request, Quote $quote, $action) + { + + switch ($action) { + case 'clone_to_invoice': + //$quote = CloneInvoiceFactory::create($quote, auth()->user()->id); + return $this->itemResponse($quote); + break; + case 'clone_to_quote': + //$quote = CloneInvoiceToQuoteFactory::create($quote, auth()->user()->id); + // todo build the quote transformer and return response here + break; + case 'history': + # code... + break; + case 'delivery_note': + # code... + break; + case 'mark_paid': + # code... + break; + case 'archive': + # code... + break; + case 'delete': + # code... + break; + case 'email': + //dispatch email to queue + break; + + default: + # code... + break; + } + } + +} \ No newline at end of file diff --git a/app/Http/Requests/Quote/ActionQuoteRequest.php b/app/Http/Requests/Quote/ActionQuoteRequest.php new file mode 100644 index 0000000000..49dac2dfae --- /dev/null +++ b/app/Http/Requests/Quote/ActionQuoteRequest.php @@ -0,0 +1,21 @@ +user()->can('edit', $this->quote); + } + +} \ No newline at end of file diff --git a/app/Http/Requests/Quote/CreateQuoteRequest.php b/app/Http/Requests/Quote/CreateQuoteRequest.php new file mode 100644 index 0000000000..8597a6128d --- /dev/null +++ b/app/Http/Requests/Quote/CreateQuoteRequest.php @@ -0,0 +1,21 @@ +user()->can('create', Quote::class); + } + +} \ No newline at end of file diff --git a/app/Http/Requests/Quote/DestroyQuoteRequest.php b/app/Http/Requests/Quote/DestroyQuoteRequest.php new file mode 100644 index 0000000000..1560e0ea2e --- /dev/null +++ b/app/Http/Requests/Quote/DestroyQuoteRequest.php @@ -0,0 +1,21 @@ +user()->can('edit', $this->quote); + } + +} \ No newline at end of file diff --git a/app/Http/Requests/Quote/EditQuoteRequest.php b/app/Http/Requests/Quote/EditQuoteRequest.php new file mode 100644 index 0000000000..f39114a0c3 --- /dev/null +++ b/app/Http/Requests/Quote/EditQuoteRequest.php @@ -0,0 +1,40 @@ +user()->can('edit', $this->quote); + } + + public function rules() + { + $rules = []; + + return $rules; + } + + + public function sanitize() + { + $input = $this->all(); + + //$input['id'] = $this->encodePrimaryKey($input['id']); + + //$this->replace($input); + + return $this->all(); + } + +} \ No newline at end of file diff --git a/app/Http/Requests/Quote/ShowQuoteRequest.php b/app/Http/Requests/Quote/ShowQuoteRequest.php new file mode 100644 index 0000000000..0f1d908d4b --- /dev/null +++ b/app/Http/Requests/Quote/ShowQuoteRequest.php @@ -0,0 +1,21 @@ +user()->can('view', $this->quote); + } + +} \ No newline at end of file diff --git a/app/Http/Requests/Quote/StoreQuoteRequest.php b/app/Http/Requests/Quote/StoreQuoteRequest.php new file mode 100644 index 0000000000..4db96fe8d2 --- /dev/null +++ b/app/Http/Requests/Quote/StoreQuoteRequest.php @@ -0,0 +1,41 @@ +user()->can('create', Quote::class); + } + + public function rules() + { + return [ + 'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx', + ]; + } + + + public function sanitize() + { + //do post processing of Quote request here, ie. Quote_items + } + + public function messages() + { + + } + + +} + diff --git a/app/Http/Requests/Quote/UpdateQuoteRequest.php b/app/Http/Requests/Quote/UpdateQuoteRequest.php new file mode 100644 index 0000000000..81743934f4 --- /dev/null +++ b/app/Http/Requests/Quote/UpdateQuoteRequest.php @@ -0,0 +1,32 @@ +user()->can('edit', $this->quote); + + } + + + public function rules() + { + return [ + 'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx', + ]; + } + +} \ No newline at end of file diff --git a/app/Policies/QuotePolicy.php b/app/Policies/QuotePolicy.php new file mode 100644 index 0000000000..48740057ce --- /dev/null +++ b/app/Policies/QuotePolicy.php @@ -0,0 +1,25 @@ +isAdmin() || $user->hasPermission('create_quote'); + } + +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index ee8a9aff8a..f48b7ba088 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -5,10 +5,12 @@ namespace App\Providers; use App\Models\Client; use App\Models\Invoice; use App\Models\Product; +use App\Models\Quote; use App\Models\User; use App\Policies\ClientPolicy; use App\Policies\InvoicePolicy; use App\Policies\ProductPolicy; +use App\Policies\QuotePolicy; use Auth; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Gate; @@ -24,6 +26,7 @@ class AuthServiceProvider extends ServiceProvider Client::class => ClientPolicy::class, Product::class => ProductPolicy::class, Invoice::class => InvoicePolicy::class, + Quote::class => QuotePolicy::class, User::class => UserPolicy::class, ]; diff --git a/app/Repositories/QuoteRepository.php b/app/Repositories/QuoteRepository.php new file mode 100644 index 0000000000..290e23b3c5 --- /dev/null +++ b/app/Repositories/QuoteRepository.php @@ -0,0 +1,38 @@ +fill($request->input()); + + $quote->save(); + + + $invoice_calc = new InvoiceCalc($quote, $quote->settings); + + $quote = $invoice_calc->build()->getInvoice(); + + //fire events here that cascading from the saving of an invoice + //ie. client balance update... + + return $quote; + } + +} \ No newline at end of file diff --git a/app/Transformers/QuoteTransformer.php b/app/Transformers/QuoteTransformer.php new file mode 100644 index 0000000000..e73ebe5a24 --- /dev/null +++ b/app/Transformers/QuoteTransformer.php @@ -0,0 +1,158 @@ +serializer); + + return $this->includeCollection($quote->quote_items, $transformer, ENTITY_quote_ITEM); + } + + public function includeInvitations(quote $quote) + { + $transformer = new InvitationTransformer($this->account, $this->serializer); + + return $this->includeCollection($quote->invitations, $transformer, ENTITY_INVITATION); + } + + public function includePayments(quote $quote) + { + $transformer = new PaymentTransformer($this->account, $this->serializer, $quote); + + return $this->includeCollection($quote->payments, $transformer, ENTITY_PAYMENT); + } + + public function includeClient(quote $quote) + { + $transformer = new ClientTransformer($this->account, $this->serializer); + + return $this->includeItem($quote->client, $transformer, ENTITY_CLIENT); + } + + public function includeExpenses(quote $quote) + { + $transformer = new ExpenseTransformer($this->account, $this->serializer); + + return $this->includeCollection($quote->expenses, $transformer, ENTITY_EXPENSE); + } + + public function includeDocuments(quote $quote) + { + $transformer = new DocumentTransformer($this->account, $this->serializer); + + $quote->documents->each(function ($document) use ($quote) { + $document->setRelation('quote', $quote); + }); + + return $this->includeCollection($quote->documents, $transformer, ENTITY_DOCUMENT); + } +*/ + public function transform(Quote $quote) + { + return [ + 'id' => $this->encodePrimaryKey($quote->id), + 'amount' => (float) $quote->amount, + 'balance' => (float) $quote->balance, + 'client_id' => (int) $quote->client_id, + 'status_id' => (int) ($quote->status_id ?: 1), + 'updated_at' => $quote->updated_at, + 'archived_at' => $quote->deleted_at, + 'quote_number' => $quote->quote_number, + 'discount' => (float) $quote->discount, + 'po_number' => $quote->po_number, + 'quote_date' => $quote->quote_date ?: '', + 'valid_until' => $quote->valid_until ?: '', + 'terms' => $quote->terms ?: '', + 'public_notes' => $quote->public_notes ?: '', + 'private_notes' => $quote->private_notes ?: '', + 'is_deleted' => (bool) $quote->is_deleted, + 'quote_type_id' => (int) $quote->quote_type_id, + 'tax_name1' => $quote->tax_name1 ? $quote->tax_name1 : '', + 'tax_rate1' => (float) $quote->tax_rate1, + 'tax_name2' => $quote->tax_name2 ? $quote->tax_name2 : '', + 'tax_rate2' => (float) $quote->tax_rate2, + 'is_amount_discount' => (bool) ($quote->is_amount_discount ?: false), + 'quote_footer' => $quote->quote_footer ?: '', + 'partial' => (float) ($quote->partial ?: 0.0), + 'partial_due_date' => $quote->partial_due_date ?: '', + 'custom_value1' => (float) $quote->custom_value1, + 'custom_value2' => (float) $quote->custom_value2, + 'custom_taxes1' => (bool) $quote->custom_taxes1, + 'custom_taxes2' => (bool) $quote->custom_taxes2, + 'has_tasks' => (bool) $quote->has_tasks, + 'has_expenses' => (bool) $quote->has_expenses, + 'custom_text_value1' => $quote->custom_text_value1 ?: '', + 'custom_text_value2' => $quote->custom_text_value2 ?: '', + 'backup' => $quote->backup ?: '', + 'settings' => $quote->settings, + ]; + } +} \ No newline at end of file diff --git a/database/factories/QuoteFactory.php b/database/factories/QuoteFactory.php new file mode 100644 index 0000000000..f092d3ecce --- /dev/null +++ b/database/factories/QuoteFactory.php @@ -0,0 +1,28 @@ +define(App\Models\Quote::class, function (Faker $faker) { + return [ + 'status_id' => App\Models\Quote::STATUS_DRAFT, + 'quote_number' => $faker->text(256), + 'discount' => $faker->numberBetween(1,10), + 'is_amount_discount' => $faker->boolean(), + 'tax_name1' => 'GST', + 'tax_rate1' => 10, + 'tax_name2' => 'VAT', + 'tax_rate2' => 17.5, + 'custom_value1' => $faker->numberBetween(1,4), + 'custom_value2' => $faker->numberBetween(1,4), + 'custom_value3' => $faker->numberBetween(1,4), + 'custom_value4' => $faker->numberBetween(1,4), + 'is_deleted' => false, + 'po_number' => $faker->text(10), + 'quote_date' => $faker->date(), + 'valid_until' => $faker->date(), + 'line_items' => false, + 'backup' => '', + ]; +}); \ No newline at end of file diff --git a/database/migrations/2014_10_13_000000_create_users_table.php b/database/migrations/2014_10_13_000000_create_users_table.php index 0a6e214da2..71145e8546 100644 --- a/database/migrations/2014_10_13_000000_create_users_table.php +++ b/database/migrations/2014_10_13_000000_create_users_table.php @@ -485,7 +485,7 @@ class CreateUsersTable extends Migration $t->string('po_number'); $t->date('quote_date')->nullable(); - $t->date('due_date')->nullable(); + $t->date('valid_until')->nullable(); $t->boolean('is_deleted')->default(false); diff --git a/tests/Unit/GenerateNumberTest.php b/tests/Unit/GenerateNumberTest.php index debc8592aa..4fb61e074c 100644 --- a/tests/Unit/GenerateNumberTest.php +++ b/tests/Unit/GenerateNumberTest.php @@ -189,7 +189,7 @@ class GenerateNumberTest extends TestCase $this->assertEquals($this->client->getNextNumber($this->client), date('j') . '-1'); } - public function testClientNumberPatternWithDate() + public function testClientNumberPatternWithDate2() { $settings = $this->client->getSettingsByKey('client_number_pattern'); $settings->client_number_pattern = '{$date:d M Y}-{$counter}';