diff --git a/VERSION.txt b/VERSION.txt index be51f9de1b..417fbb1548 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.0.20 +5.0.21 diff --git a/app/Console/Commands/CheckData.php b/app/Console/Commands/CheckData.php index b681a12bce..4e6d72e9a5 100644 --- a/app/Console/Commands/CheckData.php +++ b/app/Console/Commands/CheckData.php @@ -112,7 +112,7 @@ class CheckData extends Command ->subject('Check-Data: '.strtoupper($this->isValid ? Account::RESULT_SUCCESS : Account::RESULT_FAILURE)." [{$database}]"); }); } elseif (! $this->isValid) { - throw new Exception("Check data failed!!\n".$this->log); + new Exception("Check data failed!!\n".$this->log); } } @@ -322,10 +322,10 @@ class CheckData extends Command $total_invoice_payments = 0; foreach ($client->invoices as $invoice) { - $total_amount = $invoice->payments->sum('pivot.amount'); + $total_amount = $invoice->payments->sum('pivot.amount'); $total_refund = $invoice->payments->sum('pivot.refunded'); - $total_invoice_payments += ($total_amount - $total_refund); + $total_invoice_payments += ($total_amount - $total_refund); } foreach($client->payments as $payment) @@ -333,7 +333,7 @@ class CheckData extends Command $credit_total_applied += $payment->paymentables->where('paymentable_type', App\Models\Credit::class)->sum(\DB::raw('amount')); } - $total_invoice_payments += $credit_total_applied; + //$total_invoice_payments += $credit_total_applied; //todo this is contentious info("total invoice payments = {$total_invoice_payments} with client paid to date of of {$client->paid_to_date}"); diff --git a/app/Console/Commands/CreateSingleAccount.php b/app/Console/Commands/CreateSingleAccount.php index 8a6db25176..155d92a1d9 100644 --- a/app/Console/Commands/CreateSingleAccount.php +++ b/app/Console/Commands/CreateSingleAccount.php @@ -203,6 +203,10 @@ class CreateSingleAccount extends Command $this->info('creating project for client #'.$client->id); $this->createProject($client); + + $this->info('creating credit for client #'.$client->id); + $this->createCredit($client); + } $this->createGateways($company, $user); @@ -344,11 +348,6 @@ class CreateSingleAccount extends Command private function createCredit($client) { - // for($x=0; $x<$this->count; $x++){ - - // dispatch(new CreateTestCreditJob($client)); - - // } $faker = \Faker\Factory::create(); $credit = Credit::factory()->create(['user_id' => $client->user->id, 'company_id' => $client->company->id, 'client_id' => $client->id]); @@ -356,24 +355,9 @@ class CreateSingleAccount extends Command $dateable = Carbon::now()->subDays(rand(0, 90)); $credit->date = $dateable; - $credit->line_items = $this->buildLineItems(rand(1, 10)); + $credit->line_items = $this->buildCreditItem(); $credit->uses_inclusive_taxes = false; - if (rand(0, 1)) { - $credit->tax_name1 = 'GST'; - $credit->tax_rate1 = 10.00; - } - - if (rand(0, 1)) { - $credit->tax_name2 = 'VAT'; - $credit->tax_rate2 = 17.50; - } - - if (rand(0, 1)) { - $credit->tax_name3 = 'CA Sales Tax'; - $credit->tax_rate3 = 5; - } - $credit->save(); $invoice_calc = new InvoiceSum($credit); @@ -428,6 +412,32 @@ class CreateSingleAccount extends Command $quote->service()->createInvitations(); } + + private function buildCreditItem() + { + $line_items = []; + + $item = InvoiceItemFactory::create(); + $item->quantity = 1; + $item->cost = 1000; + + $product = Product::all()->random(); + + $item->cost = (float) $product->cost; + $item->product_key = $product->product_key; + $item->notes = $product->notes; + $item->custom_value1 = $product->custom_value1; + $item->custom_value2 = $product->custom_value2; + $item->custom_value3 = $product->custom_value3; + $item->custom_value4 = $product->custom_value4; + + $line_items[] = $item; + + + return $line_items; + } + + private function buildLineItems($count = 1) { $line_items = []; diff --git a/app/Console/Commands/DemoMode.php b/app/Console/Commands/DemoMode.php index 4f6600f5ac..8a5070a1f9 100644 --- a/app/Console/Commands/DemoMode.php +++ b/app/Console/Commands/DemoMode.php @@ -17,6 +17,7 @@ use App\Factory\InvoiceFactory; use App\Factory\InvoiceItemFactory; use App\Helpers\Invoice\InvoiceSum; use App\Jobs\Company\CreateCompanyPaymentTerms; +use App\Jobs\Company\CreateCompanyTaskStatuses; use App\Jobs\Ninja\CompanySizeCheck; use App\Jobs\Util\VersionCheck; use App\Models\Account; @@ -175,6 +176,7 @@ class DemoMode extends Command } CreateCompanyPaymentTerms::dispatchNow($company, $user); + CreateCompanyTaskStatuses::dispatchNow($company, $user); $company_token = new CompanyToken; $company_token->user_id = $user->id; diff --git a/app/Factory/TaskStatusFactory.php b/app/Factory/TaskStatusFactory.php new file mode 100644 index 0000000000..bead18ebb7 --- /dev/null +++ b/app/Factory/TaskStatusFactory.php @@ -0,0 +1,27 @@ +user_id = $user_id; + $task_status->company_id = $company_id; + $task_status->name = ''; + + return $task_status; + } +} diff --git a/app/Helpers/Invoice/InvoiceSum.php b/app/Helpers/Invoice/InvoiceSum.php index c21308cdc1..3ee3c8d9f3 100644 --- a/app/Helpers/Invoice/InvoiceSum.php +++ b/app/Helpers/Invoice/InvoiceSum.php @@ -187,6 +187,18 @@ class InvoiceSum return $this->invoice; } + public function getRecurringInvoice() + { + + $this->invoice->amount = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision); + $this->invoice->total_taxes = $this->getTotalTaxes(); + $this->invoice->balance = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision); + + $this->invoice->save(); + + return $this->invoice; + } + /** * Build $this->invoice variables after * calculations have been performed. diff --git a/app/Helpers/Invoice/InvoiceSumInclusive.php b/app/Helpers/Invoice/InvoiceSumInclusive.php index 3887f96803..307ad07040 100644 --- a/app/Helpers/Invoice/InvoiceSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceSumInclusive.php @@ -174,6 +174,18 @@ class InvoiceSumInclusive return $this; } + public function getRecurringInvoice() + { + + $this->invoice->amount = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision); + $this->invoice->total_taxes = $this->getTotalTaxes(); + $this->invoice->balance = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision); + + $this->invoice->save(); + + return $this->invoice; + } + public function getInvoice() { //Build invoice values here and return Invoice diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 3c8a49610a..fc2c783e31 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -62,40 +62,40 @@ class BaseController extends Controller 'account', 'token.company_user', 'company.activities', + 'company.designs.company', + 'company.documents', 'company.users.company_users', - 'company.tax_rates', - 'company.groups', - 'company.company_gateways.gateway', 'company.clients.contacts', 'company.clients.gateway_tokens', 'company.clients.documents', - 'company.products', - 'company.products.documents', - 'company.recurring_invoices', + 'company.company_gateways.gateway', + 'company.credits.invitations.contact', + 'company.credits.invitations.company', + 'company.credits.documents', + 'company.expenses.documents', + 'company.groups', 'company.invoices.invitations.contact', 'company.invoices.invitations.company', 'company.invoices.documents', + 'company.products', + 'company.products.documents', + 'company.payments.paymentables', + 'company.payments.documents', + 'company.payment_terms.company', + 'company.projects.documents', 'company.recurring_invoices', 'company.recurring_invoices.invitations.contact', 'company.recurring_invoices.invitations.company', 'company.recurring_invoices.documents', - 'company.payments.paymentables', - 'company.payments.documents', 'company.quotes.invitations.contact', 'company.quotes.invitations.company', 'company.quotes.documents', - 'company.credits.invitations.contact', - 'company.credits.invitations.company', - 'company.credits.documents', - 'company.payment_terms.company', - 'company.vendors.contacts', - 'company.expenses.documents', + 'company.tasks', 'company.tasks.documents', - 'company.projects.documents', - 'company.designs.company', - 'company.documents', - 'company.webhooks', + 'company.tax_rates', 'company.tokens_hashed', + 'company.vendors.contacts', + 'company.webhooks', ]; private $mini_load = [ @@ -202,7 +202,7 @@ class BaseController extends Controller $updated_at = date('Y-m-d H:i:s', $updated_at); $query->with( - [ 'user.company_users', + [ 'company' => function ($query) use ($updated_at) { $query->whereNotNull('updated_at')->with('documents'); }, diff --git a/app/Http/Controllers/CompanyController.php b/app/Http/Controllers/CompanyController.php index a2cad28cc1..a023f7e63f 100644 --- a/app/Http/Controllers/CompanyController.php +++ b/app/Http/Controllers/CompanyController.php @@ -23,6 +23,7 @@ use App\Http\Requests\Company\UpdateCompanyRequest; use App\Http\Requests\SignupRequest; use App\Jobs\Company\CreateCompany; use App\Jobs\Company\CreateCompanyPaymentTerms; +use App\Jobs\Company\CreateCompanyTaskStatuses; use App\Jobs\Company\CreateCompanyToken; use App\Jobs\Ninja\RefundCancelledAccount; use App\Jobs\RegisterNewAccount; @@ -205,6 +206,7 @@ class CompanyController extends BaseController $company = CreateCompany::dispatchNow($request->all(), auth()->user()->company()->account); CreateCompanyPaymentTerms::dispatchNow($company, auth()->user()); + CreateCompanyTaskStatuses::dispatchNow($company, auth()->user()); $company = $this->company_repo->save($request->all(), $company); diff --git a/app/Http/Controllers/EmailController.php b/app/Http/Controllers/EmailController.php index 6f60ef6476..e78600c73d 100644 --- a/app/Http/Controllers/EmailController.php +++ b/app/Http/Controllers/EmailController.php @@ -122,6 +122,9 @@ class EmailController extends BaseController } }); + $entity_obj->last_sent_date = now(); + $entity_obj->save(); + /*Only notify the admin ONCE, not once per contact/invite*/ $invitation = $entity_obj->invitations->first(); diff --git a/app/Http/Controllers/OpenAPI/TaskStatusSchema.php b/app/Http/Controllers/OpenAPI/TaskStatusSchema.php new file mode 100644 index 0000000000..6cfaa811f9 --- /dev/null +++ b/app/Http/Controllers/OpenAPI/TaskStatusSchema.php @@ -0,0 +1,12 @@ +user()->company()->id, auth()->user()->id); $project->fill($request->all()); + $project->number = $this->getNextProjectNumber($request->getClient($request->input('client_id'))); $project->save(); if ($request->has('documents')) { diff --git a/app/Http/Controllers/TaskStatusController.php b/app/Http/Controllers/TaskStatusController.php new file mode 100644 index 0000000000..f012db3bab --- /dev/null +++ b/app/Http/Controllers/TaskStatusController.php @@ -0,0 +1,463 @@ +task_status_repo = $task_status_repo; + } + + /** + * @OA\Get( + * path="/api/v1/task_status", + * operationId="getTaskStatuss", + * tags={"task_status"}, + * summary="Gets a list of task statuses", + * description="Lists task statuses", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter(ref="#/components/parameters/index"), + * @OA\Response( + * response=200, + * description="A list of task statuses", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/TaskStatus"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function index() + { + $task_status = TaskStatus::whereCompanyId(auth()->user()->company()->id)->orWhere('company_id', null); + + return $this->listResponse($task_status); + } + + /** + * Show the form for creating a new resource. + * + * @param \App\Http\Requests\TaskStatus\CreateTaskStatusRequest $request The request + * + * @return \Illuminate\Http\Response + * + * + * + * @OA\Get( + * path="/api/v1/task_statuses/create", + * operationId="getTaskStatussCreate", + * tags={"task_status"}, + * summary="Gets a new blank TaskStatus object", + * description="Returns a blank object with default values", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Response( + * response=200, + * description="A blank TaskStatus object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/TaskStatus"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function create(CreateTaskStatusRequest $request) + { + $task_status = TaskStatusFactory::create(auth()->user()->company()->id, auth()->user()->id); + + return $this->itemResponse($task_status); + } + + /** + * Store a newly created resource in storage. + * + * @param \App\Http\Requests\TaskStatus\StoreTaskStatusRequest $request The request + * + * @return \Illuminate\Http\Response + * + * + * + * @OA\Post( + * path="/api/v1/task_status", + * operationId="storeTaskStatus", + * tags={"task_status"}, + * summary="Adds a TaskStatus", + * description="Adds a TaskStatusto the system", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\RequestBody( + * description="The task_status request", + * required=true, + * @OA\JsonContent(ref="#/components/schemas/TaskStatus"), + * ), + * @OA\Response( + * response=200, + * description="Returns the saved TaskStatus object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/TaskStatus"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function store(StoreTaskStatusRequest $request) + { + $task_status = TaskStatusFactory::create(auth()->user()->company()->id, auth()->user()->id); + $task_status->fill($request->all()); + $task_status->save(); + + return $this->itemResponse($task_status->fresh()); + } + + /** + * @OA\Get( + * path="/api/v1/task_statuses/{id}", + * operationId="showTaskStatus", + * tags={"task_status"}, + * summary="Shows a TaskStatus Term", + * description="Displays an TaskStatusby id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The TaskStatusHashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the TaskStatusobject", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/TaskStatus"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function show(ShowTaskStatusRequest $request, TaskStatus $task_status) + { + return $this->itemResponse($task_status); + } + + /** + * @OA\Get( + * path="/api/v1/task_statuses/{id}/edit", + * operationId="editTaskStatuss", + * tags={"task_status"}, + * summary="Shows an TaskStatusfor editting", + * description="Displays an TaskStatusby id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The TaskStatusHashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the TaskStatus object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/TaskStatus"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function edit(EditTaskStatusRequest $request, TaskStatus $payment) + { + return $this->itemResponse($payment); + } + + /** + * Update the specified resource in storage. + * + * @param \App\Http\Requests\TaskStatus\UpdateTaskStatusRequest $request The request + * @param \App\Models\TaskStatus $task_status The payment term + * + * @return \Illuminate\Http\Response + * + * + * @OA\Put( + * path="/api/v1/task_statuses/{id}", + * operationId="updateTaskStatus", + * tags={"task_status"}, + * summary="Updates a TaskStatus Term", + * description="Handles the updating of an TaskStatus Termby id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The TaskStatusHashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the TaskStatusobject", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/TaskStatus"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function update(UpdateTaskStatusRequest $request, TaskStatus $task_status) + { + $task_status->fill($request->all()); + $task_status->save(); + + return $this->itemResponse($task_status->fresh()); + } + + /** + * Remove the specified resource from storage. + * + * @param \App\Http\Requests\TaskStatus\DestroyTaskStatusRequest $request + * @param \App\Models\TaskStatus $task_status + * + * @return \Illuminate\Http\Response + * + * + * @OA\Delete( + * path="/api/v1/task_statuses/{id}", + * operationId="deleteTaskStatus", + * tags={"task_statuss"}, + * summary="Deletes a TaskStatus Term", + * description="Handles the deletion of an TaskStatus by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The TaskStatusHashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns a HTTP status", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function destroy(DestroyTaskStatusRequest $request, TaskStatus $task_status) + { + $task_status->delete(); + + return response()->json([], 200); + } + + /** + * Perform bulk actions on the list view. + * + * @return Collection + * + * + * @OA\Post( + * path="/api/v1/task_statuses/bulk", + * operationId="bulkTaskStatuss", + * tags={"task_status"}, + * summary="Performs bulk actions on an array of task statuses", + * description="", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/index"), + * @OA\RequestBody( + * description="TaskStatus Ter,s", + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema( + * type="array", + * @OA\Items( + * type="integer", + * description="Array of hashed IDs to be bulk 'actioned", + * example="[0,1,2,3]", + * ), + * ) + * ) + * ), + * @OA\Response( + * response=200, + * description="The TaskStatus Terms response", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/TaskStatus"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function bulk() + { + $action = request()->input('action'); + + $ids = request()->input('ids'); + + $task_status = TaskStatus::withTrashed()->company()->find($this->transformKeys($ids)); + + $task_status->each(function ($task_status, $key) use ($action) { + if (auth()->user()->can('edit', $task_status)) { + $this->task_status_repo->{$action}($task_status); + } + }); + + return $this->listResponse(TaskStatus::withTrashed()->whereIn('id', $this->transformKeys($ids))); + } +} diff --git a/app/Http/Requests/Payment/StorePaymentRequest.php b/app/Http/Requests/Payment/StorePaymentRequest.php index 60e178160c..67c50a24e6 100644 --- a/app/Http/Requests/Payment/StorePaymentRequest.php +++ b/app/Http/Requests/Payment/StorePaymentRequest.php @@ -38,7 +38,7 @@ class StorePaymentRequest extends Request { $input = $this->all(); - //info(print_r($input,1)); + // info(print_r($input,1)); $invoices_total = 0; $credits_total = 0; @@ -77,7 +77,7 @@ class StorePaymentRequest extends Request if (! isset($input['amount']) || $input['amount'] == 0) { //$input['amount'] = $invoices_total - $credits_total; - $input['amount'] = $invoices_total; + $input['amount'] = $invoices_total - $credits_total; //todo the payment amount is always less the credit amount applied } $input['is_manual'] = true; @@ -94,17 +94,15 @@ class StorePaymentRequest extends Request $rules = [ 'amount' => 'numeric|required', 'amount' => [new PaymentAmountsBalanceRule(), new ValidCreditsPresentRule()], - //'date' => 'required', 'client_id' => 'bail|required|exists:clients,id', - 'invoices.*.invoice_id' => 'required|distinct|exists:invoices,id', + 'invoices.*.invoice_id' => 'bail|required|distinct|exists:invoices,id', 'invoices.*.invoice_id' => new ValidInvoicesRules($this->all()), 'invoices.*.amount' => 'required', - 'credits.*.credit_id' => 'required|exists:credits,id', + 'credits.*.credit_id' => 'bail|required|exists:credits,id', 'credits.*.credit_id' => new ValidCreditsRules($this->all()), 'credits.*.amount' => 'required', 'invoices' => new ValidPayableInvoicesRule(), - 'number' => 'nullable|unique:payments,number,'.$this->id.',id,company_id,'.$this->company_id, - //'number' => 'nullable', + 'number' => 'bail|nullable|unique:payments,number,'.$this->id.',id,company_id,'.$this->company_id, ]; if ($this->input('documents') && is_array($this->input('documents'))) { diff --git a/app/Http/Requests/Payment/UpdatePaymentRequest.php b/app/Http/Requests/Payment/UpdatePaymentRequest.php index 910ef9002d..dbc6331405 100644 --- a/app/Http/Requests/Payment/UpdatePaymentRequest.php +++ b/app/Http/Requests/Payment/UpdatePaymentRequest.php @@ -34,12 +34,13 @@ class UpdatePaymentRequest extends Request } public function rules() - {//min:1 removed, 'required' - $rules = [ + { + + $rules = [ + 'number' => 'nullable|unique:payments,number,'.$this->id.',id,company_id,'.$this->payment->company_id, 'invoices' => ['array', new PaymentAppliedValidAmount, new ValidCreditsPresentRule], 'invoices.*.invoice_id' => 'distinct', 'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx', - 'number' => 'nullable|unique:payments,number,'.$this->id.',id,company_id,'.$this->company_id, ]; if ($this->input('documents') && is_array($this->input('documents'))) { diff --git a/app/Http/Requests/Project/StoreProjectRequest.php b/app/Http/Requests/Project/StoreProjectRequest.php index 6c1d3ef94f..1903854bbd 100644 --- a/app/Http/Requests/Project/StoreProjectRequest.php +++ b/app/Http/Requests/Project/StoreProjectRequest.php @@ -12,6 +12,7 @@ namespace App\Http\Requests\Project; use App\Http\Requests\Request; +use App\Models\Client; use App\Models\Project; use App\Utils\Traits\MakesHash; @@ -51,4 +52,9 @@ class StoreProjectRequest extends Request $this->replace($input); } + + public function getClient($client_id) + { + return Client::find($client_id); + } } diff --git a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php index f5782f7ad9..cd512f03f0 100644 --- a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php @@ -36,6 +36,7 @@ class UpdateRecurringInvoiceRequest extends Request public function rules() { + $rules = []; if ($this->input('documents') && is_array($this->input('documents'))) { @@ -49,7 +50,7 @@ class UpdateRecurringInvoiceRequest extends Request } if ($this->input('number')) { - $rules['number'] = 'unique:recurring_invoices,number,'.$this->id.',id,company_id,'.$this->recurring_invoice->company_id; + $rules['number'] = 'unique:recurring_invoices,number,'.$this->recurring_invoice->id.',id,company_id,'.$this->recurring_invoice->company_id; } return $rules; diff --git a/app/Http/Requests/TaskStatus/ActionTaskStatusRequest.php b/app/Http/Requests/TaskStatus/ActionTaskStatusRequest.php new file mode 100644 index 0000000000..379417df8d --- /dev/null +++ b/app/Http/Requests/TaskStatus/ActionTaskStatusRequest.php @@ -0,0 +1,28 @@ +user()->isAdmin(); + } +} diff --git a/app/Http/Requests/TaskStatus/CreateTaskStatusRequest.php b/app/Http/Requests/TaskStatus/CreateTaskStatusRequest.php new file mode 100644 index 0000000000..42850061ec --- /dev/null +++ b/app/Http/Requests/TaskStatus/CreateTaskStatusRequest.php @@ -0,0 +1,28 @@ +user()->isAdmin(); + } +} diff --git a/app/Http/Requests/TaskStatus/DestroyTaskStatusRequest.php b/app/Http/Requests/TaskStatus/DestroyTaskStatusRequest.php new file mode 100644 index 0000000000..37cceac60e --- /dev/null +++ b/app/Http/Requests/TaskStatus/DestroyTaskStatusRequest.php @@ -0,0 +1,28 @@ +user()->isAdmin(); + } +} diff --git a/app/Http/Requests/TaskStatus/EditTaskStatusRequest.php b/app/Http/Requests/TaskStatus/EditTaskStatusRequest.php new file mode 100644 index 0000000000..f47d92f4cd --- /dev/null +++ b/app/Http/Requests/TaskStatus/EditTaskStatusRequest.php @@ -0,0 +1,44 @@ +user()->isAdmin(); + } + + public function rules() + { + $rules = []; + + return $rules; + } + + protected function prepareForValidation() + { + $input = $this->all(); + + //$input['id'] = $this->encodePrimaryKey($input['id']); + + $this->replace($input); + } +} diff --git a/app/Http/Requests/TaskStatus/ShowTaskStatusRequest.php b/app/Http/Requests/TaskStatus/ShowTaskStatusRequest.php new file mode 100644 index 0000000000..9037c2628a --- /dev/null +++ b/app/Http/Requests/TaskStatus/ShowTaskStatusRequest.php @@ -0,0 +1,28 @@ +user()->isAdmin(); + } +} diff --git a/app/Http/Requests/TaskStatus/StoreTaskStatusRequest.php b/app/Http/Requests/TaskStatus/StoreTaskStatusRequest.php new file mode 100644 index 0000000000..9ecfc444ec --- /dev/null +++ b/app/Http/Requests/TaskStatus/StoreTaskStatusRequest.php @@ -0,0 +1,47 @@ +user()->isAdmin(); + } + + protected function prepareForValidation() + { + $input = $this->all(); + + $this->replace($input); + } + + public function rules() + { + $rules = []; + + $rules['name'] ='required|unique:task_statuses,name,null,null,company_id,'.auth()->user()->companyId(); + + return $rules; + } +} diff --git a/app/Http/Requests/TaskStatus/UpdateTaskStatusRequest.php b/app/Http/Requests/TaskStatus/UpdateTaskStatusRequest.php new file mode 100644 index 0000000000..d6e550427f --- /dev/null +++ b/app/Http/Requests/TaskStatus/UpdateTaskStatusRequest.php @@ -0,0 +1,42 @@ +user()->isAdmin(); + } + + public function rules() + { + $rules = []; + + if ($this->input('name')) + $rules['name'] = 'unique:task_statuses,name,'.$this->id.',id,company_id,'.$this->task_status->company_id; + + return $rules; + } + +} diff --git a/app/Http/ValidationRules/PaymentAmountsBalanceRule.php b/app/Http/ValidationRules/PaymentAmountsBalanceRule.php index 47305a2e96..cc9ff5251b 100644 --- a/app/Http/ValidationRules/PaymentAmountsBalanceRule.php +++ b/app/Http/ValidationRules/PaymentAmountsBalanceRule.php @@ -73,6 +73,9 @@ class PaymentAmountsBalanceRule implements Rule return true; } // if no invoices are present, then this is an unapplied payment, let this pass validation! +// info("payment amounts = {$payment_amounts}"); +// info("invoice amounts = {$invoice_amounts}"); + return $payment_amounts >= $invoice_amounts; } } diff --git a/app/Jobs/Account/CreateAccount.php b/app/Jobs/Account/CreateAccount.php index 70b9d1be08..1f69e0e60e 100644 --- a/app/Jobs/Account/CreateAccount.php +++ b/app/Jobs/Account/CreateAccount.php @@ -15,6 +15,7 @@ use App\DataMapper\Analytics\AccountCreated as AnalyticsAccountCreated; use App\Events\Account\AccountCreated; use App\Jobs\Company\CreateCompany; use App\Jobs\Company\CreateCompanyPaymentTerms; +use App\Jobs\Company\CreateCompanyTaskStatuses; use App\Jobs\Company\CreateCompanyToken; use App\Jobs\User\CreateUser; use App\Models\Account; @@ -74,6 +75,7 @@ class CreateAccount $spaa9f78 = CreateUser::dispatchNow($this->request, $sp794f3f, $sp035a66, true); CreateCompanyPaymentTerms::dispatchNow($sp035a66, $spaa9f78); + CreateCompanyTaskStatuses::dispatchNow($sp035a66, $spaa9f78); if ($spaa9f78) { auth()->login($spaa9f78, false); diff --git a/app/Jobs/Company/CreateCompanyTaskStatuses.php b/app/Jobs/Company/CreateCompanyTaskStatuses.php new file mode 100644 index 0000000000..04814e1735 --- /dev/null +++ b/app/Jobs/Company/CreateCompanyTaskStatuses.php @@ -0,0 +1,61 @@ +company = $company; + + $this->user = $user; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $task_statuses = [ + ['name' => ctrans('texts.backlog'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()], + ['name' => ctrans('texts.ready_to_do'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()], + ['name' => ctrans('texts.in_progress'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()], + ['name' => ctrans('texts.done'), 'company_id' => $this->company->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()], + + ]; + + TaskStatus::insert($task_statuses); + } +} diff --git a/app/Jobs/Cron/RecurringInvoicesCron.php b/app/Jobs/Cron/RecurringInvoicesCron.php index 34b5f6e2f4..29554493b9 100644 --- a/app/Jobs/Cron/RecurringInvoicesCron.php +++ b/app/Jobs/Cron/RecurringInvoicesCron.php @@ -54,7 +54,7 @@ class RecurringInvoicesCron info("Current date = " . now()->format("Y-m-d") . " Recurring date = " .$recurring_invoice->next_send_date); - SendRecurring::dispatch($recurring_invoice, $recurring_invoice->company->db); + SendRecurring::dispatchNow($recurring_invoice, $recurring_invoice->company->db); }); @@ -74,7 +74,7 @@ class RecurringInvoicesCron info("Current date = " . now()->format("Y-m-d") . " Recurring date = " .$recurring_invoice->next_send_date); - SendRecurring::dispatch($recurring_invoice, $recurring_invoice->company->db); + SendRecurring::dispatchNow($recurring_invoice, $recurring_invoice->company->db); }); } diff --git a/app/Jobs/RecurringInvoice/SendRecurring.php b/app/Jobs/RecurringInvoice/SendRecurring.php index ccbdc7ae94..9a0f0b74cb 100644 --- a/app/Jobs/RecurringInvoice/SendRecurring.php +++ b/app/Jobs/RecurringInvoice/SendRecurring.php @@ -95,9 +95,9 @@ class SendRecurring implements ShouldQueue if ($this->recurring_invoice->remaining_cycles == 0) $this->recurring_invoice->setCompleted(); - info($this->recurring_invoice->next_send_date); - info($this->recurring_invoice->remaining_cycles); - info($this->recurring_invoice->last_sent_date); + info("next send date = " . $this->recurring_invoice->next_send_date); + info("remaining cycles = " . $this->recurring_invoice->remaining_cycles); + info("last send date = " . $this->recurring_invoice->last_sent_date); $this->recurring_invoice->save(); diff --git a/app/Jobs/User/UserEmailChanged.php b/app/Jobs/User/UserEmailChanged.php index 8043d4e223..fcb9fc9692 100644 --- a/app/Jobs/User/UserEmailChanged.php +++ b/app/Jobs/User/UserEmailChanged.php @@ -93,6 +93,7 @@ class UserEmailChanged extends BaseMailerJob implements ShouldQueue 'signature' => $this->company->owner()->signature, 'logo' => $this->company->present()->logo(), 'settings' => $this->settings, + 'whitelabel' => $this->company->account->isPaid() ? true : false, ]; } } diff --git a/app/Listeners/Invoice/InvoiceEmailedNotification.php b/app/Listeners/Invoice/InvoiceEmailedNotification.php index 21b2348bed..dfd71d8b9c 100644 --- a/app/Listeners/Invoice/InvoiceEmailedNotification.php +++ b/app/Listeners/Invoice/InvoiceEmailedNotification.php @@ -46,6 +46,10 @@ class InvoiceEmailedNotification implements ShouldQueue $first_notification_sent = true; + $invoice = $event->invitation->invoice; + $invoice->last_sent_date = now(); + $invoice->save(); + foreach ($event->invitation->company->company_users as $company_user) { $user = $company_user->user; diff --git a/app/Mail/Admin/EntityPaidObject.php b/app/Mail/Admin/EntityPaidObject.php index a3ab38b12b..33c155a21b 100644 --- a/app/Mail/Admin/EntityPaidObject.php +++ b/app/Mail/Admin/EntityPaidObject.php @@ -90,6 +90,7 @@ class EntityPaidObject 'signature' => $settings->email_signature, 'logo' => $this->company->present()->logo(), 'settings' => $settings, + 'whitelabel' => $this->company->account->isPaid() ? true : false, ]; return $data; diff --git a/app/Mail/Admin/EntitySentObject.php b/app/Mail/Admin/EntitySentObject.php index 4367acda86..901adab718 100644 --- a/app/Mail/Admin/EntitySentObject.php +++ b/app/Mail/Admin/EntitySentObject.php @@ -85,7 +85,7 @@ class EntitySentObject 'signature' => $settings->email_signature, 'logo' => $this->company->present()->logo(), 'settings' => $settings, - + 'whitelabel' => $this->company->account->isPaid() ? true : false, ]; } } diff --git a/app/Mail/Admin/EntityViewedObject.php b/app/Mail/Admin/EntityViewedObject.php index 367c3f2de2..3690931271 100644 --- a/app/Mail/Admin/EntityViewedObject.php +++ b/app/Mail/Admin/EntityViewedObject.php @@ -85,7 +85,7 @@ class EntityViewedObject 'signature' => $settings->email_signature, 'logo' => $this->company->present()->logo(), 'settings' => $settings, - + 'whitelabel' => $this->company->account->isPaid() ? true : false, ]; return $data; diff --git a/app/Mail/Admin/PaymentFailureObject.php b/app/Mail/Admin/PaymentFailureObject.php index 12e041740f..ed34e338a1 100644 --- a/app/Mail/Admin/PaymentFailureObject.php +++ b/app/Mail/Admin/PaymentFailureObject.php @@ -77,7 +77,7 @@ class PaymentFailureObject 'signature' => $signature, 'logo' => $this->company->present()->logo(), 'settings' => $this->client->getMergedSettings(), - + 'whitelabel' => $this->company->account->isPaid() ? true : false, ]; return $data; diff --git a/app/Mail/TemplateEmail.php b/app/Mail/TemplateEmail.php index f8e1a1079a..d9cffa9c1f 100644 --- a/app/Mail/TemplateEmail.php +++ b/app/Mail/TemplateEmail.php @@ -60,6 +60,8 @@ class TemplateEmail extends Mailable ->text('email.template.plain', [ 'body' => $this->build_email->getBody(), 'footer' => $this->build_email->getFooter(), + 'whitelabel' => $this->client->user->account->isPaid() ? true : false, + 'settings' => $settings, ]) ->view($template_name, [ 'body' => $this->build_email->getBody(), diff --git a/app/Models/Client.php b/app/Models/Client.php index a97e306d51..afda91176e 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -524,7 +524,7 @@ class Client extends BaseModel implements HasLocalePreference } } - if($this->company->use_credits_payment == 'option' && $this->service()->getCreditBalance() > 0) { + if(($this->company->use_credits_payment == 'option' || $this->company->use_credits_payment == 'always') && $this->service()->getCreditBalance() > 0) { $payment_urls[] = [ 'label' => ctrans('texts.apply_credit'), 'company_gateway_id' => CompanyGateway::GATEWAY_CREDIT, diff --git a/app/Models/RecurringInvoiceInvitation.php b/app/Models/RecurringInvoiceInvitation.php index fe4e6c4b7b..3b54b32ad9 100644 --- a/app/Models/RecurringInvoiceInvitation.php +++ b/app/Models/RecurringInvoiceInvitation.php @@ -27,6 +27,11 @@ class RecurringInvoiceInvitation extends BaseModel protected $touches = ['recurring_invoice']; + protected $with = [ + 'company', + 'contact', + ]; + public function getEntityType() { return self::class; @@ -71,13 +76,13 @@ class RecurringInvoiceInvitation extends BaseModel public function markViewed() { - $this->viewed_date = Carbon::now(); + $this->viewed_date = now(); $this->save(); } public function markOpened() { - $this->opened_date = Carbon::now(); + $this->opened_date = now(); $this->save(); } diff --git a/app/Models/TaskStatus.php b/app/Models/TaskStatus.php new file mode 100644 index 0000000000..c90ab60197 --- /dev/null +++ b/app/Models/TaskStatus.php @@ -0,0 +1,34 @@ + $this->entity->company->present()->logo(), 'signature' => $this->settings->email_signature, 'settings' => $this->settings, - + 'whitelabel' => $this->entity->company->account->isPaid() ? true : false, ]; return $data; diff --git a/app/Policies/TaskStatusPolicy.php b/app/Policies/TaskStatusPolicy.php new file mode 100644 index 0000000000..7678e8f4c5 --- /dev/null +++ b/app/Policies/TaskStatusPolicy.php @@ -0,0 +1,32 @@ +isAdmin() || $user->hasPermission('create_all'); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index d0c8d29781..f8517417a3 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -31,6 +31,7 @@ use App\Models\Quote; use App\Models\RecurringInvoice; use App\Models\RecurringQuote; use App\Models\Task; +use App\Models\TaskStatus; use App\Models\TaxRate; use App\Models\User; use App\Models\Vendor; @@ -55,6 +56,7 @@ use App\Policies\QuotePolicy; use App\Policies\RecurringInvoicePolicy; use App\Policies\RecurringQuotePolicy; use App\Policies\TaskPolicy; +use App\Policies\TaskStatusPolicy; use App\Policies\TaxRatePolicy; use App\Policies\UserPolicy; use App\Policies\VendorPolicy; @@ -92,6 +94,7 @@ class AuthServiceProvider extends ServiceProvider RecurringQuote::class => RecurringQuotePolicy::class, Webhook::class => WebhookPolicy::class, Task::class => TaskPolicy::class, + TaskStatus::class => TaskStatusPolicy::class, TaxRate::class => TaxRatePolicy::class, User::class => UserPolicy::class, Vendor::class => VendorPolicy::class, diff --git a/app/Repositories/PaymentRepository.php b/app/Repositories/PaymentRepository.php index 366610d906..f837f0a317 100644 --- a/app/Repositories/PaymentRepository.php +++ b/app/Repositories/PaymentRepository.php @@ -70,21 +70,35 @@ class PaymentRepository extends BaseRepository */ private function applyPayment(array $data, Payment $payment): ?Payment { + $is_existing_payment = true; //check currencies here and fill the exchange rate data if necessary if (! $payment->id) { $this->processExchangeRates($data, $payment); + $is_existing_payment = false; + $client = Client::find($data['client_id']); + /*We only update the paid to date ONCE per payment*/ if (array_key_exists('invoices', $data) && is_array($data['invoices']) && count($data['invoices']) > 0) { if ($data['amount'] == '') { $data['amount'] = array_sum(array_column($data['invoices'], 'amount')); } - $client = Client::find($data['client_id']); - $client->service()->updatePaidToDate($data['amount'])->save(); + } + + if (array_key_exists('credits', $data) && is_array($data['credits']) && count($data['credits']) > 0) { + + $_credit_totals = array_sum(array_column($data['credits'], 'amount')); + + $data['amount'] -= $_credit_totals; + + $client->service()->updatePaidToDate($_credit_totals)->save(); + + } + } /*Fill the payment*/ @@ -144,7 +158,8 @@ class PaymentRepository extends BaseRepository } } - event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); + if(!$is_existing_payment) + event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); /*info("invoice totals = {$invoice_totals}"); info("credit totals = {$credit_totals}"); @@ -162,7 +177,7 @@ class PaymentRepository extends BaseRepository // $payment->applied += $invoice_totals; // } - $payment->applied = $invoice_totals; //wont work because - check tests + $payment->applied += ($invoice_totals - $credit_totals); //wont work because - check tests $payment->save(); diff --git a/app/Repositories/RecurringInvoiceRepository.php b/app/Repositories/RecurringInvoiceRepository.php index b88aca5e83..117c8dc5a1 100644 --- a/app/Repositories/RecurringInvoiceRepository.php +++ b/app/Repositories/RecurringInvoiceRepository.php @@ -38,7 +38,7 @@ class RecurringInvoiceRepository extends BaseRepository ->createInvitations() ->save(); - $invoice = $invoice_calc->build()->getInvoice(); + $invoice = $invoice_calc->build()->getRecurringInvoice(); return $invoice; } diff --git a/app/Repositories/TaskStatusRepository.php b/app/Repositories/TaskStatusRepository.php new file mode 100644 index 0000000000..7b71ea14af --- /dev/null +++ b/app/Repositories/TaskStatusRepository.php @@ -0,0 +1,19 @@ +applyPaymentToCredit(); - + $this->credit->balance -= $this->amount_applied; + + if((int)$this->credit->balance == 0) + $this->credit->status_id = Credit::STATUS_APPLIED; + else + $this->credit->status_id = Credit::STATUS_PARTIAL; + + $this->credit->save(); + $this->addPaymentToLedger(); return $this->credit; diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 860f49073e..ebaebbaf3c 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -126,7 +126,10 @@ class AutoBillInvoice extends AbstractService { $current_credit = Credit::find($credit['credit_id']); $payment->credits()->attach($current_credit->id, ['amount' => $credit['amount']]); - $this->applyPaymentToCredit($current_credit, $credit['amount']); + + $current_credit->balance -= $credit['amount']; + $current_credit->save(); + // $this->applyPaymentToCredit($current_credit, $credit['amount']); } $payment->ledger() diff --git a/app/Transformers/CompanyUserTransformer.php b/app/Transformers/CompanyUserTransformer.php index 7e0afe76b6..2d60dbca4b 100644 --- a/app/Transformers/CompanyUserTransformer.php +++ b/app/Transformers/CompanyUserTransformer.php @@ -29,7 +29,7 @@ class CompanyUserTransformer extends EntityTransformer protected $defaultIncludes = [ // 'account', // 'company', - // 'user', + 'user', // 'token' ]; diff --git a/app/Transformers/ProjectTransformer.php b/app/Transformers/ProjectTransformer.php index 6ba7d0749b..76ac59224a 100644 --- a/app/Transformers/ProjectTransformer.php +++ b/app/Transformers/ProjectTransformer.php @@ -48,6 +48,7 @@ class ProjectTransformer extends EntityTransformer 'assigned_user_id' => (string) $this->encodePrimaryKey($project->assigned_user_id), 'client_id' => (string) $this->encodePrimaryKey($project->client_id), 'name' => $project->name ?: '', + 'number' => $project->number, 'created_at' => (int) $project->created_at, 'updated_at' => (int) $project->updated_at, 'archived_at' => (int) $project->deleted_at, diff --git a/app/Transformers/TaskStatusTransformer.php b/app/Transformers/TaskStatusTransformer.php new file mode 100644 index 0000000000..f96f33a4fa --- /dev/null +++ b/app/Transformers/TaskStatusTransformer.php @@ -0,0 +1,32 @@ + (string) $this->encodePrimaryKey($task_status->id), + 'name' => (string) $task_status->name, + 'is_deleted' => (bool) $task_status->is_deleted, + 'created_at' => (int) $task_status->created_at, + 'updated_at' => (int) $task_status->updated_at, + 'archived_at' => (int) $task_status->deleted_at, + ]; + } +} diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index 2075ace404..6befe6d676 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -365,6 +365,8 @@ class HtmlEngine $arrKeysLength = array_map('strlen', array_keys($data)); array_multisort($arrKeysLength, SORT_DESC, $data); +//info(print_r($data,1)); + return $data; } diff --git a/app/Utils/Traits/GeneratesCounter.php b/app/Utils/Traits/GeneratesCounter.php index ad1dc4d989..6686245fde 100644 --- a/app/Utils/Traits/GeneratesCounter.php +++ b/app/Utils/Traits/GeneratesCounter.php @@ -16,6 +16,7 @@ use App\Models\Credit; use App\Models\Expense; use App\Models\Invoice; use App\Models\Payment; +use App\Models\Project; use App\Models\Quote; use App\Models\RecurringInvoice; use App\Models\Timezone; @@ -225,6 +226,44 @@ trait GeneratesCounter return (string) $payment_number; } + /** + * Project Number Generator. + * @return string The project number + */ + public function getNextProjectNumber(Client $client) :string + { + + //Reset counters if enabled + $this->resetCounters($client); + + $is_client_counter = false; + + //todo handle if we have specific client patterns in the future + $pattern = $client->company->settings->project_number_pattern; + + //Determine if we are using client_counters + if (strpos($pattern, 'client_counter') === false) { + $counter = $client->company->settings->project_number_counter; + } else { + $counter = $client->settings->project_number_counter; + $is_client_counter = true; + } + + //Return a valid counter + $pattern = ''; + $padding = $client->getSetting('counter_padding'); + $project_number = $this->checkEntityNumber(Project::class, $client, $counter, $padding, $pattern); + + //increment the correct invoice_number Counter (company vs client) + if ($is_client_counter) { + $this->incrementCounter($client, 'project_number_counter'); + } else { + $this->incrementCounter($client->company, 'project_number_counter'); + } + + return (string) $project_number; + } + /** * Gets the next client number. * @@ -479,6 +518,7 @@ trait GeneratesCounter $settings->vendor_number_counter = 1; $settings->ticket_number_counter = 1; $settings->payment_number_counter = 1; + $settings->project_number_counter = 1; $settings->task_number_counter = 1; $settings->expense_number_counter = 1; diff --git a/config/ninja.php b/config/ninja.php index 9ab231a660..b5ab21f67e 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -12,7 +12,7 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/').'/', 'app_domain' => env('APP_DOMAIN', ''), - 'app_version' => '5.0.20', + 'app_version' => '5.0.21', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), diff --git a/database/factories/TaskStatusFactory.php b/database/factories/TaskStatusFactory.php new file mode 100644 index 0000000000..02f49d3e0d --- /dev/null +++ b/database/factories/TaskStatusFactory.php @@ -0,0 +1,38 @@ + $this->faker->text(7), + ]; + } +} \ No newline at end of file diff --git a/database/migrations/2020_10_19_101823_project_name_unique_removal.php b/database/migrations/2020_10_19_101823_project_name_unique_removal.php index 970e6a14bb..22ea62bd6c 100644 --- a/database/migrations/2020_10_19_101823_project_name_unique_removal.php +++ b/database/migrations/2020_10_19_101823_project_name_unique_removal.php @@ -28,8 +28,31 @@ class ProjectNameUniqueRemoval extends Migration Schema::table('companies', function (Blueprint $table) { $table->boolean('invoice_expense_documents')->default(false); + $table->boolean('auto_start_tasks')->default(false); + }); + Schema::create('task_statuses', function (Blueprint $table) { + $table->increments('id'); + $table->string('name')->nullable(); + $table->unsignedInteger('company_id')->nullable(); + $table->unsignedInteger('user_id')->nullable(); + $table->boolean('is_deleted')->default(0); + $table->timestamps(6); + $table->softDeletes('deleted_at', 6); + + $table->index(['company_id', 'deleted_at']); + + $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade'); + }); + + Schema::table('tasks', function (Blueprint $table) { + $table->decimal('rate', 16, 4)->default(0); + $table->renameColumn('task_status_id', 'status_id'); + $table->renameColumn('task_status_sort_order', 'sort_order'); + + }); } /** diff --git a/resources/views/index/index.blade.php b/resources/views/index/index.blade.php index efdde9ef82..dfbae0aa30 100644 --- a/resources/views/index/index.blade.php +++ b/resources/views/index/index.blade.php @@ -1,6 +1,8 @@ + + Invoice Ninja diff --git a/routes/api.php b/routes/api.php index 711dcc2508..440ecfdd30 100644 --- a/routes/api.php +++ b/routes/api.php @@ -18,7 +18,9 @@ Route::group(['middleware' => ['api_secret_check']], function () { Route::group(['api_secret_check', 'email_db'], function () { Route::post('api/v1/login', 'Auth\LoginController@apiLogin')->name('login.submit'); - Route::post('api/v1/reset_password', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.reset'); + // Route::post('api/v1/reset_password', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.reset'); + Route::post('api/v1/reset_password', 'Auth\ForgotPasswordController@sendResetLinkEmail'); + }); Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'api/v1', 'as' => 'api.'], function () { @@ -77,6 +79,10 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::post('tasks/bulk', 'TaskController@bulk')->name('tasks.bulk'); + Route::resource('task_statuses', 'TaskStatusController'); // name = (task_statuses. index / create / show / update / destroy / edit + + Route::post('task_statuses/bulk', 'TaskStatusController@bulk')->name('task_statuses.bulk'); + Route::resource('projects', 'ProjectController'); // name = (projects. index / create / show / update / destroy / edit Route::post('projects/bulk', 'ProjectController@bulk')->name('projects.bulk'); diff --git a/routes/web.php b/routes/web.php index e26102700e..6a6673112a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -15,7 +15,7 @@ Route::post('setup/check_pdf', 'SetupController@checkPdf')->middleware('guest'); Route::get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request'); Route::post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email'); -//Route::get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset'); +Route::get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset'); Route::post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update'); /* diff --git a/tests/Feature/TaskStatusApiTest.php b/tests/Feature/TaskStatusApiTest.php new file mode 100644 index 0000000000..dc87c43cbe --- /dev/null +++ b/tests/Feature/TaskStatusApiTest.php @@ -0,0 +1,151 @@ +makeTestData(); + + Session::start(); + + $this->faker = \Faker\Factory::create(); + + Model::reguard(); + } + + public function testTaskStatusPost() + { + $data = [ + 'name' => $this->faker->firstName, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/task_statuses', $data); + + $response->assertStatus(200); + } + + public function testTaskStatusPut() + { + $data = [ + 'name' => $this->faker->firstName, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->put('/api/v1/task_statuses/'.$this->encodePrimaryKey($this->task_status->id), $data); + + $response->assertStatus(200); + } + + public function testTaskStatusGet() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/task_statuses/'.$this->encodePrimaryKey($this->task_status->id)); + + $response->assertStatus(200); + } + + public function testTaskStatusNotArchived() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/task_statuses/'.$this->encodePrimaryKey($this->task_status->id)); + + $arr = $response->json(); + + $this->assertEquals(0, $arr['data']['archived_at']); + } + + public function testTaskStatusArchived() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->task_status->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/task_statuses/bulk?action=archive', $data); + + $arr = $response->json(); + + $this->assertNotNull($arr['data'][0]['archived_at']); + } + + public function testTaskStatusRestored() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->task_status->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/task_statuses/bulk?action=restore', $data); + + $arr = $response->json(); + + $this->assertEquals(0, $arr['data'][0]['archived_at']); + } + + public function testTaskStatusDeleted() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->task_status->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/task_statuses/bulk?action=delete', $data); + + $arr = $response->json(); + + $this->assertTrue($arr['data'][0]['is_deleted']); + } +} diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index c6f2f659ce..d8e945c856 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -42,6 +42,7 @@ use App\Models\Quote; use App\Models\QuoteInvitation; use App\Models\RecurringInvoice; use App\Models\Task; +use App\Models\TaskStatus; use App\Models\User; use App\Models\Vendor; use App\Models\VendorContact; @@ -81,6 +82,8 @@ trait MockAccountData public $task; + public $task_status; + public $expense_category; public $cu; @@ -237,6 +240,11 @@ trait MockAccountData 'company_id' => $this->company->id, ]); + $this->task_status = TaskStatus::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + ]); + $gs = new GroupSetting; $gs->name = 'Test'; $gs->company_id = $this->client->company_id;