1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 21:22:58 +01:00

Merge pull request #4193 from turbo124/v5-develop

Credit payments and general fixes
This commit is contained in:
David Bomba 2020-10-21 15:14:25 +11:00 committed by GitHub
commit d881e7cea1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 1375 additions and 81 deletions

View File

@ -1 +1 @@
5.0.20 5.0.21

View File

@ -112,7 +112,7 @@ class CheckData extends Command
->subject('Check-Data: '.strtoupper($this->isValid ? Account::RESULT_SUCCESS : Account::RESULT_FAILURE)." [{$database}]"); ->subject('Check-Data: '.strtoupper($this->isValid ? Account::RESULT_SUCCESS : Account::RESULT_FAILURE)." [{$database}]");
}); });
} elseif (! $this->isValid) { } 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; $total_invoice_payments = 0;
foreach ($client->invoices as $invoice) { 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_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) 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')); $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}"); info("total invoice payments = {$total_invoice_payments} with client paid to date of of {$client->paid_to_date}");

View File

@ -203,6 +203,10 @@ class CreateSingleAccount extends Command
$this->info('creating project for client #'.$client->id); $this->info('creating project for client #'.$client->id);
$this->createProject($client); $this->createProject($client);
$this->info('creating credit for client #'.$client->id);
$this->createCredit($client);
} }
$this->createGateways($company, $user); $this->createGateways($company, $user);
@ -344,11 +348,6 @@ class CreateSingleAccount extends Command
private function createCredit($client) private function createCredit($client)
{ {
// for($x=0; $x<$this->count; $x++){
// dispatch(new CreateTestCreditJob($client));
// }
$faker = \Faker\Factory::create(); $faker = \Faker\Factory::create();
$credit = Credit::factory()->create(['user_id' => $client->user->id, 'company_id' => $client->company->id, 'client_id' => $client->id]); $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)); $dateable = Carbon::now()->subDays(rand(0, 90));
$credit->date = $dateable; $credit->date = $dateable;
$credit->line_items = $this->buildLineItems(rand(1, 10)); $credit->line_items = $this->buildCreditItem();
$credit->uses_inclusive_taxes = false; $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(); $credit->save();
$invoice_calc = new InvoiceSum($credit); $invoice_calc = new InvoiceSum($credit);
@ -428,6 +412,32 @@ class CreateSingleAccount extends Command
$quote->service()->createInvitations(); $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) private function buildLineItems($count = 1)
{ {
$line_items = []; $line_items = [];

View File

@ -17,6 +17,7 @@ use App\Factory\InvoiceFactory;
use App\Factory\InvoiceItemFactory; use App\Factory\InvoiceItemFactory;
use App\Helpers\Invoice\InvoiceSum; use App\Helpers\Invoice\InvoiceSum;
use App\Jobs\Company\CreateCompanyPaymentTerms; use App\Jobs\Company\CreateCompanyPaymentTerms;
use App\Jobs\Company\CreateCompanyTaskStatuses;
use App\Jobs\Ninja\CompanySizeCheck; use App\Jobs\Ninja\CompanySizeCheck;
use App\Jobs\Util\VersionCheck; use App\Jobs\Util\VersionCheck;
use App\Models\Account; use App\Models\Account;
@ -175,6 +176,7 @@ class DemoMode extends Command
} }
CreateCompanyPaymentTerms::dispatchNow($company, $user); CreateCompanyPaymentTerms::dispatchNow($company, $user);
CreateCompanyTaskStatuses::dispatchNow($company, $user);
$company_token = new CompanyToken; $company_token = new CompanyToken;
$company_token->user_id = $user->id; $company_token->user_id = $user->id;

View File

@ -0,0 +1,27 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Factory;
use App\Models\TaskStatus;
class TaskStatusFactory
{
public static function create(int $company_id, int $user_id) :TaskStatus
{
$task_status = new TaskStatus;
$task_status->user_id = $user_id;
$task_status->company_id = $company_id;
$task_status->name = '';
return $task_status;
}
}

View File

@ -187,6 +187,18 @@ class InvoiceSum
return $this->invoice; 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 * Build $this->invoice variables after
* calculations have been performed. * calculations have been performed.

View File

@ -174,6 +174,18 @@ class InvoiceSumInclusive
return $this; 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() public function getInvoice()
{ {
//Build invoice values here and return Invoice //Build invoice values here and return Invoice

View File

@ -62,40 +62,40 @@ class BaseController extends Controller
'account', 'account',
'token.company_user', 'token.company_user',
'company.activities', 'company.activities',
'company.designs.company',
'company.documents',
'company.users.company_users', 'company.users.company_users',
'company.tax_rates',
'company.groups',
'company.company_gateways.gateway',
'company.clients.contacts', 'company.clients.contacts',
'company.clients.gateway_tokens', 'company.clients.gateway_tokens',
'company.clients.documents', 'company.clients.documents',
'company.products', 'company.company_gateways.gateway',
'company.products.documents', 'company.credits.invitations.contact',
'company.recurring_invoices', 'company.credits.invitations.company',
'company.credits.documents',
'company.expenses.documents',
'company.groups',
'company.invoices.invitations.contact', 'company.invoices.invitations.contact',
'company.invoices.invitations.company', 'company.invoices.invitations.company',
'company.invoices.documents', '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',
'company.recurring_invoices.invitations.contact', 'company.recurring_invoices.invitations.contact',
'company.recurring_invoices.invitations.company', 'company.recurring_invoices.invitations.company',
'company.recurring_invoices.documents', 'company.recurring_invoices.documents',
'company.payments.paymentables',
'company.payments.documents',
'company.quotes.invitations.contact', 'company.quotes.invitations.contact',
'company.quotes.invitations.company', 'company.quotes.invitations.company',
'company.quotes.documents', 'company.quotes.documents',
'company.credits.invitations.contact', 'company.tasks',
'company.credits.invitations.company',
'company.credits.documents',
'company.payment_terms.company',
'company.vendors.contacts',
'company.expenses.documents',
'company.tasks.documents', 'company.tasks.documents',
'company.projects.documents', 'company.tax_rates',
'company.designs.company',
'company.documents',
'company.webhooks',
'company.tokens_hashed', 'company.tokens_hashed',
'company.vendors.contacts',
'company.webhooks',
]; ];
private $mini_load = [ private $mini_load = [
@ -202,7 +202,7 @@ class BaseController extends Controller
$updated_at = date('Y-m-d H:i:s', $updated_at); $updated_at = date('Y-m-d H:i:s', $updated_at);
$query->with( $query->with(
[ 'user.company_users', [
'company' => function ($query) use ($updated_at) { 'company' => function ($query) use ($updated_at) {
$query->whereNotNull('updated_at')->with('documents'); $query->whereNotNull('updated_at')->with('documents');
}, },

View File

@ -23,6 +23,7 @@ use App\Http\Requests\Company\UpdateCompanyRequest;
use App\Http\Requests\SignupRequest; use App\Http\Requests\SignupRequest;
use App\Jobs\Company\CreateCompany; use App\Jobs\Company\CreateCompany;
use App\Jobs\Company\CreateCompanyPaymentTerms; use App\Jobs\Company\CreateCompanyPaymentTerms;
use App\Jobs\Company\CreateCompanyTaskStatuses;
use App\Jobs\Company\CreateCompanyToken; use App\Jobs\Company\CreateCompanyToken;
use App\Jobs\Ninja\RefundCancelledAccount; use App\Jobs\Ninja\RefundCancelledAccount;
use App\Jobs\RegisterNewAccount; use App\Jobs\RegisterNewAccount;
@ -205,6 +206,7 @@ class CompanyController extends BaseController
$company = CreateCompany::dispatchNow($request->all(), auth()->user()->company()->account); $company = CreateCompany::dispatchNow($request->all(), auth()->user()->company()->account);
CreateCompanyPaymentTerms::dispatchNow($company, auth()->user()); CreateCompanyPaymentTerms::dispatchNow($company, auth()->user());
CreateCompanyTaskStatuses::dispatchNow($company, auth()->user());
$company = $this->company_repo->save($request->all(), $company); $company = $this->company_repo->save($request->all(), $company);

View File

@ -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*/ /*Only notify the admin ONCE, not once per contact/invite*/
$invitation = $entity_obj->invitations->first(); $invitation = $entity_obj->invitations->first();

View File

@ -0,0 +1,12 @@
<?php
/**
* @OA\Schema(
* schema="TaskStatus",
* type="object",
* @OA\Property(property="name", type="string", example="Backlog", description="The task status name"),
* @OA\Property(property="created_at", type="number", format="integer", example="134341234234", description="Timestamp"),
* @OA\Property(property="is_deleted", type="boolean", example=true, description="______"),
* @OA\Property(property="updated_at", type="number", format="integer", example="134341234234", description="Timestamp"),
* @OA\Property(property="archived_at", type="number", format="integer", example="134341234234", description="Timestamp"),
* )
*/

View File

@ -24,6 +24,7 @@ use App\Models\Project;
use App\Repositories\ProjectRepository; use App\Repositories\ProjectRepository;
use App\Transformers\ProjectTransformer; use App\Transformers\ProjectTransformer;
use App\Utils\Traits\BulkOptions; use App\Utils\Traits\BulkOptions;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments; use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -36,7 +37,8 @@ class ProjectController extends BaseController
{ {
use MakesHash; use MakesHash;
use SavesDocuments; use SavesDocuments;
use GeneratesCounter;
protected $entity_type = Project::class; protected $entity_type = Project::class;
protected $entity_transformer = ProjectTransformer::class; protected $entity_transformer = ProjectTransformer::class;
@ -353,6 +355,7 @@ class ProjectController extends BaseController
{ {
$project = ProjectFactory::create(auth()->user()->company()->id, auth()->user()->id); $project = ProjectFactory::create(auth()->user()->company()->id, auth()->user()->id);
$project->fill($request->all()); $project->fill($request->all());
$project->number = $this->getNextProjectNumber($request->getClient($request->input('client_id')));
$project->save(); $project->save();
if ($request->has('documents')) { if ($request->has('documents')) {

View File

@ -0,0 +1,463 @@
<?php
namespace App\Http\Controllers;
use App\Factory\TaskStatusFactory;
use App\Http\Requests\TaskStatus\CreateTaskStatusRequest;
use App\Http\Requests\TaskStatus\DestroyTaskStatusRequest;
use App\Http\Requests\TaskStatus\ShowTaskStatusRequest;
use App\Http\Requests\TaskStatus\StoreTaskStatusRequest;
use App\Http\Requests\TaskStatus\UpdateTaskStatusRequest;
use App\Models\TaskStatus;
use App\Repositories\TaskStatusRepository;
use App\Transformers\TaskStatusTransformer;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
class TaskStatusController extends BaseController
{
use MakesHash;
protected $entity_type = TaskStatus::class;
protected $entity_transformer = TaskStatusTransformer::class;
/**
* @var TaskStatusRepository
*/
protected $task_status_repo;
/**
* TaskStatusController constructor.
*
* @param \App\Repositories\TaskStatusRepository $task_status_repo The payment term repo
*/
public function __construct(TaskStatusRepository $task_status_repo)
{
parent::__construct();
$this->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)));
}
}

View File

@ -38,7 +38,7 @@ class StorePaymentRequest extends Request
{ {
$input = $this->all(); $input = $this->all();
//info(print_r($input,1)); // info(print_r($input,1));
$invoices_total = 0; $invoices_total = 0;
$credits_total = 0; $credits_total = 0;
@ -77,7 +77,7 @@ class StorePaymentRequest extends Request
if (! isset($input['amount']) || $input['amount'] == 0) { if (! isset($input['amount']) || $input['amount'] == 0) {
//$input['amount'] = $invoices_total - $credits_total; //$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; $input['is_manual'] = true;
@ -94,17 +94,15 @@ class StorePaymentRequest extends Request
$rules = [ $rules = [
'amount' => 'numeric|required', 'amount' => 'numeric|required',
'amount' => [new PaymentAmountsBalanceRule(), new ValidCreditsPresentRule()], 'amount' => [new PaymentAmountsBalanceRule(), new ValidCreditsPresentRule()],
//'date' => 'required',
'client_id' => 'bail|required|exists:clients,id', '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.*.invoice_id' => new ValidInvoicesRules($this->all()),
'invoices.*.amount' => 'required', '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.*.credit_id' => new ValidCreditsRules($this->all()),
'credits.*.amount' => 'required', 'credits.*.amount' => 'required',
'invoices' => new ValidPayableInvoicesRule(), 'invoices' => new ValidPayableInvoicesRule(),
'number' => 'nullable|unique:payments,number,'.$this->id.',id,company_id,'.$this->company_id, 'number' => 'bail|nullable|unique:payments,number,'.$this->id.',id,company_id,'.$this->company_id,
//'number' => 'nullable',
]; ];
if ($this->input('documents') && is_array($this->input('documents'))) { if ($this->input('documents') && is_array($this->input('documents'))) {

View File

@ -34,12 +34,13 @@ class UpdatePaymentRequest extends Request
} }
public function rules() 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' => ['array', new PaymentAppliedValidAmount, new ValidCreditsPresentRule],
'invoices.*.invoice_id' => 'distinct', 'invoices.*.invoice_id' => 'distinct',
'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx', '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'))) { if ($this->input('documents') && is_array($this->input('documents'))) {

View File

@ -12,6 +12,7 @@
namespace App\Http\Requests\Project; namespace App\Http\Requests\Project;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Models\Client;
use App\Models\Project; use App\Models\Project;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
@ -51,4 +52,9 @@ class StoreProjectRequest extends Request
$this->replace($input); $this->replace($input);
} }
public function getClient($client_id)
{
return Client::find($client_id);
}
} }

View File

@ -36,6 +36,7 @@ class UpdateRecurringInvoiceRequest extends Request
public function rules() public function rules()
{ {
$rules = []; $rules = [];
if ($this->input('documents') && is_array($this->input('documents'))) { if ($this->input('documents') && is_array($this->input('documents'))) {
@ -49,7 +50,7 @@ class UpdateRecurringInvoiceRequest extends Request
} }
if ($this->input('number')) { 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; return $rules;

View File

@ -0,0 +1,28 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\TaskStatus;
use App\Http\Requests\Request;
use App\Models\Payment;
class ActionTaskStatusRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->isAdmin();
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\TaskStatus;
use App\Http\Requests\Request;
use App\Models\TaskStatus;
class CreateTaskStatusRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->isAdmin();
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\TaskStatus;
use App\Http\Requests\Request;
use App\Models\TaskStatus;
class DestroyTaskStatusRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->isAdmin();
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\TaskStatus;
use App\Http\Requests\Request;
use App\Models\TaskStatus;
class EditTaskStatusRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return auth()->user()->isAdmin();
}
public function rules()
{
$rules = [];
return $rules;
}
protected function prepareForValidation()
{
$input = $this->all();
//$input['id'] = $this->encodePrimaryKey($input['id']);
$this->replace($input);
}
}

View File

@ -0,0 +1,28 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\TaskStatus;
use App\Http\Requests\Request;
use App\Models\TaskStatus;
class ShowTaskStatusRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->isAdmin();
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\TaskStatus;
use App\Http\Requests\Request;
use App\Models\TaskStatus;
use App\Utils\Traits\MakesHash;
class StoreTaskStatusRequest extends Request
{
use MakesHash;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->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;
}
}

View File

@ -0,0 +1,42 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\TaskStatus;
use App\Http\Requests\Request;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
class UpdateTaskStatusRequest extends Request
{
use MakesHash;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->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;
}
}

View File

@ -73,6 +73,9 @@ class PaymentAmountsBalanceRule implements Rule
return true; return true;
} // if no invoices are present, then this is an unapplied payment, let this pass validation! } // 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; return $payment_amounts >= $invoice_amounts;
} }
} }

View File

@ -15,6 +15,7 @@ use App\DataMapper\Analytics\AccountCreated as AnalyticsAccountCreated;
use App\Events\Account\AccountCreated; use App\Events\Account\AccountCreated;
use App\Jobs\Company\CreateCompany; use App\Jobs\Company\CreateCompany;
use App\Jobs\Company\CreateCompanyPaymentTerms; use App\Jobs\Company\CreateCompanyPaymentTerms;
use App\Jobs\Company\CreateCompanyTaskStatuses;
use App\Jobs\Company\CreateCompanyToken; use App\Jobs\Company\CreateCompanyToken;
use App\Jobs\User\CreateUser; use App\Jobs\User\CreateUser;
use App\Models\Account; use App\Models\Account;
@ -74,6 +75,7 @@ class CreateAccount
$spaa9f78 = CreateUser::dispatchNow($this->request, $sp794f3f, $sp035a66, true); $spaa9f78 = CreateUser::dispatchNow($this->request, $sp794f3f, $sp035a66, true);
CreateCompanyPaymentTerms::dispatchNow($sp035a66, $spaa9f78); CreateCompanyPaymentTerms::dispatchNow($sp035a66, $spaa9f78);
CreateCompanyTaskStatuses::dispatchNow($sp035a66, $spaa9f78);
if ($spaa9f78) { if ($spaa9f78) {
auth()->login($spaa9f78, false); auth()->login($spaa9f78, false);

View File

@ -0,0 +1,61 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Jobs\Company;
use App\DataMapper\CompanySettings;
use App\Events\UserSignedUp;
use App\Models\Company;
use App\Models\PaymentTerm;
use App\Models\TaskStatus;
use App\Utils\Traits\MakesHash;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Request;
class CreateCompanyTaskStatuses
{
use MakesHash;
use Dispatchable;
protected $company;
protected $user;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($company, $user)
{
$this->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);
}
}

View File

@ -54,7 +54,7 @@ class RecurringInvoicesCron
info("Current date = " . now()->format("Y-m-d") . " Recurring date = " .$recurring_invoice->next_send_date); 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); 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);
}); });
} }

View File

@ -95,9 +95,9 @@ class SendRecurring implements ShouldQueue
if ($this->recurring_invoice->remaining_cycles == 0) if ($this->recurring_invoice->remaining_cycles == 0)
$this->recurring_invoice->setCompleted(); $this->recurring_invoice->setCompleted();
info($this->recurring_invoice->next_send_date); info("next send date = " . $this->recurring_invoice->next_send_date);
info($this->recurring_invoice->remaining_cycles); info("remaining cycles = " . $this->recurring_invoice->remaining_cycles);
info($this->recurring_invoice->last_sent_date); info("last send date = " . $this->recurring_invoice->last_sent_date);
$this->recurring_invoice->save(); $this->recurring_invoice->save();

View File

@ -93,6 +93,7 @@ class UserEmailChanged extends BaseMailerJob implements ShouldQueue
'signature' => $this->company->owner()->signature, 'signature' => $this->company->owner()->signature,
'logo' => $this->company->present()->logo(), 'logo' => $this->company->present()->logo(),
'settings' => $this->settings, 'settings' => $this->settings,
'whitelabel' => $this->company->account->isPaid() ? true : false,
]; ];
} }
} }

View File

@ -46,6 +46,10 @@ class InvoiceEmailedNotification implements ShouldQueue
$first_notification_sent = true; $first_notification_sent = true;
$invoice = $event->invitation->invoice;
$invoice->last_sent_date = now();
$invoice->save();
foreach ($event->invitation->company->company_users as $company_user) { foreach ($event->invitation->company->company_users as $company_user) {
$user = $company_user->user; $user = $company_user->user;

View File

@ -90,6 +90,7 @@ class EntityPaidObject
'signature' => $settings->email_signature, 'signature' => $settings->email_signature,
'logo' => $this->company->present()->logo(), 'logo' => $this->company->present()->logo(),
'settings' => $settings, 'settings' => $settings,
'whitelabel' => $this->company->account->isPaid() ? true : false,
]; ];
return $data; return $data;

View File

@ -85,7 +85,7 @@ class EntitySentObject
'signature' => $settings->email_signature, 'signature' => $settings->email_signature,
'logo' => $this->company->present()->logo(), 'logo' => $this->company->present()->logo(),
'settings' => $settings, 'settings' => $settings,
'whitelabel' => $this->company->account->isPaid() ? true : false,
]; ];
} }
} }

View File

@ -85,7 +85,7 @@ class EntityViewedObject
'signature' => $settings->email_signature, 'signature' => $settings->email_signature,
'logo' => $this->company->present()->logo(), 'logo' => $this->company->present()->logo(),
'settings' => $settings, 'settings' => $settings,
'whitelabel' => $this->company->account->isPaid() ? true : false,
]; ];
return $data; return $data;

View File

@ -77,7 +77,7 @@ class PaymentFailureObject
'signature' => $signature, 'signature' => $signature,
'logo' => $this->company->present()->logo(), 'logo' => $this->company->present()->logo(),
'settings' => $this->client->getMergedSettings(), 'settings' => $this->client->getMergedSettings(),
'whitelabel' => $this->company->account->isPaid() ? true : false,
]; ];
return $data; return $data;

View File

@ -60,6 +60,8 @@ class TemplateEmail extends Mailable
->text('email.template.plain', [ ->text('email.template.plain', [
'body' => $this->build_email->getBody(), 'body' => $this->build_email->getBody(),
'footer' => $this->build_email->getFooter(), 'footer' => $this->build_email->getFooter(),
'whitelabel' => $this->client->user->account->isPaid() ? true : false,
'settings' => $settings,
]) ])
->view($template_name, [ ->view($template_name, [
'body' => $this->build_email->getBody(), 'body' => $this->build_email->getBody(),

View File

@ -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[] = [ $payment_urls[] = [
'label' => ctrans('texts.apply_credit'), 'label' => ctrans('texts.apply_credit'),
'company_gateway_id' => CompanyGateway::GATEWAY_CREDIT, 'company_gateway_id' => CompanyGateway::GATEWAY_CREDIT,

View File

@ -27,6 +27,11 @@ class RecurringInvoiceInvitation extends BaseModel
protected $touches = ['recurring_invoice']; protected $touches = ['recurring_invoice'];
protected $with = [
'company',
'contact',
];
public function getEntityType() public function getEntityType()
{ {
return self::class; return self::class;
@ -71,13 +76,13 @@ class RecurringInvoiceInvitation extends BaseModel
public function markViewed() public function markViewed()
{ {
$this->viewed_date = Carbon::now(); $this->viewed_date = now();
$this->save(); $this->save();
} }
public function markOpened() public function markOpened()
{ {
$this->opened_date = Carbon::now(); $this->opened_date = now();
$this->save(); $this->save();
} }

34
app/Models/TaskStatus.php Normal file
View File

@ -0,0 +1,34 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Class TaskStatus.
*/
class TaskStatus extends BaseModel
{
use SoftDeletes;
/**
* @var bool
*/
public $timestamps = true;
/**
* @var array
*/
protected $dates = ['deleted_at'];
protected $fillable = ['name'];
}

View File

@ -64,6 +64,8 @@ class Vendor extends BaseModel
// 'contacts', // 'contacts',
]; ];
protected $presenter = \App\Models\Presenters\VendorPresenter::class;
public function getEntityType() public function getEntityType()
{ {
return self::class; return self::class;

View File

@ -124,7 +124,7 @@ class BaseNotification extends Notification implements ShouldQueue
'logo' => $this->entity->company->present()->logo(), 'logo' => $this->entity->company->present()->logo(),
'signature' => $this->settings->email_signature, 'signature' => $this->settings->email_signature,
'settings' => $this->settings, 'settings' => $this->settings,
'whitelabel' => $this->entity->company->account->isPaid() ? true : false,
]; ];
return $data; return $data;

View File

@ -0,0 +1,32 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Policies;
use App\Models\Payment;
use App\Models\User;
/**
* Class TaskStatusPolicy.
*/
class TaskStatusPolicy extends EntityPolicy
{
/**
* Checks if the user has create permissions.
*
* @param User $user
* @return bool
*/
public function create(User $user) : bool
{
return $user->isAdmin() || $user->hasPermission('create_all');
}
}

View File

@ -31,6 +31,7 @@ use App\Models\Quote;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
use App\Models\RecurringQuote; use App\Models\RecurringQuote;
use App\Models\Task; use App\Models\Task;
use App\Models\TaskStatus;
use App\Models\TaxRate; use App\Models\TaxRate;
use App\Models\User; use App\Models\User;
use App\Models\Vendor; use App\Models\Vendor;
@ -55,6 +56,7 @@ use App\Policies\QuotePolicy;
use App\Policies\RecurringInvoicePolicy; use App\Policies\RecurringInvoicePolicy;
use App\Policies\RecurringQuotePolicy; use App\Policies\RecurringQuotePolicy;
use App\Policies\TaskPolicy; use App\Policies\TaskPolicy;
use App\Policies\TaskStatusPolicy;
use App\Policies\TaxRatePolicy; use App\Policies\TaxRatePolicy;
use App\Policies\UserPolicy; use App\Policies\UserPolicy;
use App\Policies\VendorPolicy; use App\Policies\VendorPolicy;
@ -92,6 +94,7 @@ class AuthServiceProvider extends ServiceProvider
RecurringQuote::class => RecurringQuotePolicy::class, RecurringQuote::class => RecurringQuotePolicy::class,
Webhook::class => WebhookPolicy::class, Webhook::class => WebhookPolicy::class,
Task::class => TaskPolicy::class, Task::class => TaskPolicy::class,
TaskStatus::class => TaskStatusPolicy::class,
TaxRate::class => TaxRatePolicy::class, TaxRate::class => TaxRatePolicy::class,
User::class => UserPolicy::class, User::class => UserPolicy::class,
Vendor::class => VendorPolicy::class, Vendor::class => VendorPolicy::class,

View File

@ -70,21 +70,35 @@ class PaymentRepository extends BaseRepository
*/ */
private function applyPayment(array $data, Payment $payment): ?Payment private function applyPayment(array $data, Payment $payment): ?Payment
{ {
$is_existing_payment = true;
//check currencies here and fill the exchange rate data if necessary //check currencies here and fill the exchange rate data if necessary
if (! $payment->id) { if (! $payment->id) {
$this->processExchangeRates($data, $payment); $this->processExchangeRates($data, $payment);
$is_existing_payment = false;
$client = Client::find($data['client_id']);
/*We only update the paid to date ONCE per payment*/ /*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 (array_key_exists('invoices', $data) && is_array($data['invoices']) && count($data['invoices']) > 0) {
if ($data['amount'] == '') { if ($data['amount'] == '') {
$data['amount'] = array_sum(array_column($data['invoices'], 'amount')); $data['amount'] = array_sum(array_column($data['invoices'], 'amount'));
} }
$client = Client::find($data['client_id']);
$client->service()->updatePaidToDate($data['amount'])->save(); $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*/ /*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("invoice totals = {$invoice_totals}");
info("credit totals = {$credit_totals}"); info("credit totals = {$credit_totals}");
@ -162,7 +177,7 @@ class PaymentRepository extends BaseRepository
// $payment->applied += $invoice_totals; // $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(); $payment->save();

View File

@ -38,7 +38,7 @@ class RecurringInvoiceRepository extends BaseRepository
->createInvitations() ->createInvitations()
->save(); ->save();
$invoice = $invoice_calc->build()->getInvoice(); $invoice = $invoice_calc->build()->getRecurringInvoice();
return $invoice; return $invoice;
} }

View File

@ -0,0 +1,19 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Repositories;
/**
* Class for task status repository.
*/
class TaskStatusRepository extends BaseRepository
{
}

View File

@ -74,8 +74,15 @@ class ApplyPayment
} }
$this->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(); $this->addPaymentToLedger();
return $this->credit; return $this->credit;

View File

@ -126,7 +126,10 @@ class AutoBillInvoice extends AbstractService
{ {
$current_credit = Credit::find($credit['credit_id']); $current_credit = Credit::find($credit['credit_id']);
$payment->credits()->attach($current_credit->id, ['amount' => $credit['amount']]); $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() $payment->ledger()

View File

@ -29,7 +29,7 @@ class CompanyUserTransformer extends EntityTransformer
protected $defaultIncludes = [ protected $defaultIncludes = [
// 'account', // 'account',
// 'company', // 'company',
// 'user', 'user',
// 'token' // 'token'
]; ];

View File

@ -48,6 +48,7 @@ class ProjectTransformer extends EntityTransformer
'assigned_user_id' => (string) $this->encodePrimaryKey($project->assigned_user_id), 'assigned_user_id' => (string) $this->encodePrimaryKey($project->assigned_user_id),
'client_id' => (string) $this->encodePrimaryKey($project->client_id), 'client_id' => (string) $this->encodePrimaryKey($project->client_id),
'name' => $project->name ?: '', 'name' => $project->name ?: '',
'number' => $project->number,
'created_at' => (int) $project->created_at, 'created_at' => (int) $project->created_at,
'updated_at' => (int) $project->updated_at, 'updated_at' => (int) $project->updated_at,
'archived_at' => (int) $project->deleted_at, 'archived_at' => (int) $project->deleted_at,

View File

@ -0,0 +1,32 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Transformers;
use App\Models\TaskStatus;
use App\Utils\Traits\MakesHash;
class TaskStatusTransformer extends EntityTransformer
{
use MakesHash;
public function transform(TaskStatus $task_status)
{
return [
'id' => (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,
];
}
}

View File

@ -365,6 +365,8 @@ class HtmlEngine
$arrKeysLength = array_map('strlen', array_keys($data)); $arrKeysLength = array_map('strlen', array_keys($data));
array_multisort($arrKeysLength, SORT_DESC, $data); array_multisort($arrKeysLength, SORT_DESC, $data);
//info(print_r($data,1));
return $data; return $data;
} }

View File

@ -16,6 +16,7 @@ use App\Models\Credit;
use App\Models\Expense; use App\Models\Expense;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\Project;
use App\Models\Quote; use App\Models\Quote;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
use App\Models\Timezone; use App\Models\Timezone;
@ -225,6 +226,44 @@ trait GeneratesCounter
return (string) $payment_number; 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. * Gets the next client number.
* *
@ -479,6 +518,7 @@ trait GeneratesCounter
$settings->vendor_number_counter = 1; $settings->vendor_number_counter = 1;
$settings->ticket_number_counter = 1; $settings->ticket_number_counter = 1;
$settings->payment_number_counter = 1; $settings->payment_number_counter = 1;
$settings->project_number_counter = 1;
$settings->task_number_counter = 1; $settings->task_number_counter = 1;
$settings->expense_number_counter = 1; $settings->expense_number_counter = 1;

View File

@ -12,7 +12,7 @@ return [
'require_https' => env('REQUIRE_HTTPS', true), 'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/').'/', 'app_url' => rtrim(env('APP_URL', ''), '/').'/',
'app_domain' => env('APP_DOMAIN', ''), 'app_domain' => env('APP_DOMAIN', ''),
'app_version' => '5.0.20', 'app_version' => '5.0.21',
'minimum_client_version' => '5.0.16', 'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1', 'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''), 'api_secret' => env('API_SECRET', ''),

View File

@ -0,0 +1,38 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace Database\Factories;
use App\Models\TaskStatus;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class TaskStatusFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = TaskStatus::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => $this->faker->text(7),
];
}
}

View File

@ -28,8 +28,31 @@ class ProjectNameUniqueRemoval extends Migration
Schema::table('companies', function (Blueprint $table) { Schema::table('companies', function (Blueprint $table) {
$table->boolean('invoice_expense_documents')->default(false); $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');
});
} }
/** /**

View File

@ -1,6 +1,8 @@
<!DOCTYPE html> <!DOCTYPE html>
<html data-report-errors="{{ $report_errors }}"> <html data-report-errors="{{ $report_errors }}">
<head> <head>
<!-- Source: https://github.com/invoiceninja/invoiceninja -->
<!-- Version: {{ config('ninja.app_version') }} -->
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Invoice Ninja</title> <title>Invoice Ninja</title>
<meta name="google-signin-client_id" content="{{ config('services.google.client_id') }}"> <meta name="google-signin-client_id" content="{{ config('services.google.client_id') }}">

View File

@ -18,7 +18,9 @@ Route::group(['middleware' => ['api_secret_check']], function () {
Route::group(['api_secret_check', 'email_db'], function () { Route::group(['api_secret_check', 'email_db'], function () {
Route::post('api/v1/login', 'Auth\LoginController@apiLogin')->name('login.submit'); 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 () { 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::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::resource('projects', 'ProjectController'); // name = (projects. index / create / show / update / destroy / edit
Route::post('projects/bulk', 'ProjectController@bulk')->name('projects.bulk'); Route::post('projects/bulk', 'ProjectController@bulk')->name('projects.bulk');

View File

@ -15,7 +15,7 @@ Route::post('setup/check_pdf', 'SetupController@checkPdf')->middleware('guest');
Route::get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request'); Route::get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
Route::post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email'); 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'); Route::post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update');
/* /*

View File

@ -0,0 +1,151 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace Tests\Feature;
use App\DataMapper\DefaultSettings;
use App\Models\Account;
use App\Models\TaskStatus;
use App\Models\TaskStatusContact;
use App\Models\Company;
use App\Models\User;
use App\Utils\Traits\MakesHash;
use Faker\Factory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Controllers\TaskStatusController
*/
class TaskStatusApiTest extends TestCase
{
use MakesHash;
use DatabaseTransactions;
use MockAccountData;
public function setUp() :void
{
parent::setUp();
$this->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']);
}
}

View File

@ -42,6 +42,7 @@ use App\Models\Quote;
use App\Models\QuoteInvitation; use App\Models\QuoteInvitation;
use App\Models\RecurringInvoice; use App\Models\RecurringInvoice;
use App\Models\Task; use App\Models\Task;
use App\Models\TaskStatus;
use App\Models\User; use App\Models\User;
use App\Models\Vendor; use App\Models\Vendor;
use App\Models\VendorContact; use App\Models\VendorContact;
@ -81,6 +82,8 @@ trait MockAccountData
public $task; public $task;
public $task_status;
public $expense_category; public $expense_category;
public $cu; public $cu;
@ -237,6 +240,11 @@ trait MockAccountData
'company_id' => $this->company->id, '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 = new GroupSetting;
$gs->name = 'Test'; $gs->name = 'Test';
$gs->company_id = $this->client->company_id; $gs->company_id = $this->client->company_id;