1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-12 06:02:39 +01:00

Merge pull request #7777 from turbo124/v5-stable

5.5.15
This commit is contained in:
David Bomba 2022-08-22 19:49:39 +10:00 committed by GitHub
commit a61f41dad1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 417 additions and 45 deletions

View File

@ -1 +1 @@
5.5.14
5.5.15

View File

@ -14,6 +14,7 @@ namespace App\Http\Requests\Expense;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Expense\UniqueExpenseNumberRule;
use App\Models\Expense;
use App\Models\Project;
use App\Models\PurchaseOrder;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
@ -65,13 +66,29 @@ class StoreExpenseRequest extends Request
$input['color'] = '';
}
/* Ensure the project is related */
if (array_key_exists('project_id', $input) && isset($input['project_id'])) {
$project = Project::withTrashed()->find($input['project_id'])->company()->first();
if($project){
$input['client_id'] = $project->client_id;
}
else
{
unset($input['project_id']);
}
}
$this->replace($input);
}
public function messages()
{
return [
'unique' => ctrans('validation.unique', ['attribute' => 'email']),
// 'unique' => ctrans('validation.unique', ['attribute' => 'number']),
];
}
}

View File

@ -12,6 +12,7 @@
namespace App\Http\Requests\Expense;
use App\Http\Requests\Request;
use App\Models\Project;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
@ -35,9 +36,6 @@ class UpdateExpenseRequest extends Request
{
/* Ensure we have a client name, and that all emails are unique*/
$rules = [];
// $rules['country_id'] = 'integer|nullable';
// $rules['contacts.*.email'] = 'nullable|distinct';
if (isset($this->number)) {
$rules['number'] = Rule::unique('expenses')->where('company_id', auth()->user()->company()->id)->ignore($this->expense->id);
@ -46,16 +44,6 @@ class UpdateExpenseRequest extends Request
return $this->globalRules($rules);
}
public function messages()
{
return [
'unique' => ctrans('validation.unique', ['attribute' => 'email']),
'email' => ctrans('validation.email', ['attribute' => 'email']),
'name.required' => ctrans('validation.required', ['attribute' => 'name']),
'required' => ctrans('validation.required', ['attribute' => 'email']),
];
}
public function prepareForValidation()
{
$input = $this->all();
@ -74,6 +62,20 @@ class UpdateExpenseRequest extends Request
$input['currency_id'] = (string) auth()->user()->company()->settings->currency_id;
}
/* Ensure the project is related */
if (array_key_exists('project_id', $input) && isset($input['project_id'])) {
$project = Project::withTrashed()->find($input['project_id'])->company()->first();
if($project){
$input['client_id'] = $project->client_id;
}
else
{
unset($input['project_id']);
}
}
$this->replace($input);
}
}

View File

@ -68,15 +68,14 @@ class StorePaymentRequest extends Request
if (isset($input['credits']) && is_array($input['credits']) !== false) {
foreach ($input['credits'] as $key => $value) {
if (array_key_exists('credit_id', $input['credits'][$key])) {
$input['credits'][$key]['credit_id'] = $value['credit_id'];
// $input['credits'][$key]['credit_id'] = $value['credit_id'];
$input['credits'][$key]['credit_id'] = $this->decodePrimaryKey($value['credit_id']);
$credits_total += $value['amount'];
}
}
}
// if (array_key_exists('amount', $input))
// $input['amount'] = 0;
if (isset($input['credits']) && is_array($input['credits']) === false) {
$input['credits'] = null;
}
@ -97,14 +96,14 @@ class StorePaymentRequest extends Request
public function rules()
{
$rules = [
'amount' => ['numeric', 'bail', new PaymentAmountsBalanceRule(), new ValidCreditsPresentRule()],
'amount' => ['numeric', 'bail', new PaymentAmountsBalanceRule(), new ValidCreditsPresentRule($this->all())],
'client_id' => 'bail|required|exists:clients,id',
'invoices.*.invoice_id' => 'bail|required|distinct|exists:invoices,id',
'invoices.*.amount' => 'bail|required',
'invoices.*.invoice_id' => new ValidInvoicesRules($this->all()),
'credits.*.credit_id' => 'bail|required|exists:credits,id',
'credits.*.credit_id' => new ValidCreditsRules($this->all()),
'credits.*.amount' => ['required', new CreditsSumRule($this->all())],
'credits.*.amount' => ['bail','required', new CreditsSumRule($this->all())],
'invoices' => new ValidPayableInvoicesRule(),
'number' => ['nullable', 'bail', Rule::unique('payments')->where('company_id', auth()->user()->company()->id)],

View File

@ -36,7 +36,7 @@ class UpdatePaymentRequest extends Request
public function rules()
{
$rules = [
'invoices' => ['array', new PaymentAppliedValidAmount, new ValidCreditsPresentRule],
'invoices' => ['array', new PaymentAppliedValidAmount, new ValidCreditsPresentRule($this->all())],
'invoices.*.invoice_id' => 'distinct',
'documents' => 'mimes:png,ai,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx',
];
@ -79,6 +79,14 @@ class UpdatePaymentRequest extends Request
}
}
}
if (isset($input['credits']) && is_array($input['credits']) !== false) {
foreach ($input['credits'] as $key => $value) {
if (array_key_exists('credits', $input['credits'][$key])) {
$input['credits'][$key]['credit_id'] = $this->decodePrimaryKey($value['credit_id']);
}
}
}
$this->replace($input);
}

View File

@ -70,8 +70,8 @@ class StoreTaskRequest extends Request
/* Ensure the project is related */
if (array_key_exists('project_id', $input) && isset($input['project_id'])) {
$project = Project::withTrashed()->find($input['project_id'])->company()->first();
$project = Project::withTrashed()->where('id', $input['project_id'])->company()->first();
;
if($project){
$input['client_id'] = $project->client_id;
}

View File

@ -69,7 +69,7 @@ class UpdateTaskRequest extends Request
/* Ensure the project is related */
if (array_key_exists('project_id', $input) && isset($input['project_id'])) {
$project = Project::withTrashed()->find($input['project_id'])->company()->first();
$project = Project::withTrashed()->where('id', $input['project_id'])->company()->first();
if($project){
$input['client_id'] = $project->client_id;

View File

@ -51,10 +51,13 @@ class ValidCreditsRules implements Rule
$unique_array = [];
$cred_collection = Credit::withTrashed()->whereIn('id', array_column($this->input['credits'], 'credit_id'))->get();
foreach ($this->input['credits'] as $credit) {
$unique_array[] = $credit['credit_id'];
$cred = Credit::find($this->decodePrimaryKey($credit['credit_id']));
// $cred = Credit::find($this->decodePrimaryKey($credit['credit_id']));
$cred = $cred_collection->firstWhere('id', $credit['credit_id']);
if (! $cred) {
$this->error_msg = ctrans('texts.credit_not_found');

View File

@ -51,6 +51,9 @@ class ValidInvoicesRules implements Rule
$unique_array = [];
/////
$inv_collection = Invoice::withTrashed()->whereIn('id', array_column($this->input['invoices'], 'invoice_id'))->get();
//todo optimize this into a single query
foreach ($this->input['invoices'] as $invoice) {
$unique_array[] = $invoice['invoice_id'];
@ -61,7 +64,10 @@ class ValidInvoicesRules implements Rule
return false;
}
$inv = Invoice::withTrashed()->whereId($invoice['invoice_id'])->first();
/////
$inv = $inv_collection->firstWhere('id', $invoice['invoice_id']);
// $inv = Invoice::withTrashed()->whereId($invoice['invoice_id'])->first();
if (! $inv) {
$this->error_msg = ctrans('texts.invoice_not_found');

View File

@ -22,6 +22,13 @@ class ValidCreditsPresentRule implements Rule
{
use MakesHash;
private $input;
public function __construct($input)
{
$this->input = $input;
}
/**
* @param string $attribute
* @param mixed $value
@ -44,11 +51,10 @@ class ValidCreditsPresentRule implements Rule
{
//todo need to ensure the clients credits are here not random ones!
if (request()->input('credits') && is_array(request()->input('credits')) && count(request()->input('credits')) > 0) {
$credit_collection = Credit::whereIn('id', $this->transformKeys(array_column(request()->input('credits'), 'credit_id')))
->count();
if (array_key_exists('credits', $this->input) && is_array($this->input['credits']) && count($this->input['credits']) > 0) {
$credit_collection = Credit::whereIn('id', array_column($this->input['credits'], 'credit_id'))->count();
return $credit_collection == count(request()->input('credits'));
return $credit_collection == count($this->input['credits']);
}
return true;

View File

@ -85,6 +85,13 @@ class ApplyCreditPayment implements ShouldQueue
->save();
}
//22-08-2022
$this->credit
->client
->service()
->adjustCreditBalance($this->amount * -1)
->save();
/* Update Payment Applied Amount*/
$this->payment->save();
}

View File

@ -12,6 +12,7 @@
namespace App\Jobs\Ninja;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\Company;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -54,6 +55,8 @@ class CompanySizeCheck implements ShouldQueue
private function check()
{
nlog("Checking all company sizes");
Company::where('is_large', false)->withCount(['invoices', 'clients', 'products'])->cursor()->each(function ($company) {
if ($company->invoices_count > 500 || $company->products_count > 500 || $company->clients_count > 500) {
nlog("Marking company {$company->id} as large");
@ -61,5 +64,17 @@ class CompanySizeCheck implements ShouldQueue
$company->account->companies()->update(['is_large' => true]);
}
});
nlog("updating all client credit balances");
Client::where('updated_at', '>', now()->subDay())
->cursor()
->each(function ($client){
$client->credit_balance = $client->service()->getCreditBalance();
$client->save();
});
}
}

View File

@ -42,13 +42,16 @@ class MailSentListener implements ShouldQueue
*/
public function handle(MessageSent $event)
{
if(!Ninja::isHosted());
if(!Ninja::isHosted())
return;
$message_id = $event->sent->getMessageId();
$message = MessageConverter::toEmail($event->sent->getOriginalMessage());
if(!$message->getHeaders()->get('x-invitation'))
return;
$invitation_key = $message->getHeaders()->get('x-invitation')->getValue();
if($message_id && $invitation_key)

View File

@ -0,0 +1,89 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Notifications\Ninja;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class UserQualityNotification extends Notification
{
/**
* Create a new notification instance.
*
* @return void
*/
protected User $user;
protected string $account_key;
public function __construct(User $user, string $account_key)
{
$this->user = $user;
$this->account_key = $account_key;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['slack'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return MailMessage
*/
public function toMail($notifiable)
{
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
public function toSlack($notifiable)
{
$content = "User Quality notification {$this->user->present()->name()} \n";
$content .= "Account: {$this->account_key }\n";
return (new SlackMessage)
->success()
->from(ctrans('texts.notification_bot'))
->image('https://app.invoiceninja.com/favicon.png')
->content($content);
}
}

View File

@ -23,7 +23,10 @@ class UserObserver
*/
public function created(User $user)
{
//
if(class_exists(\Modules\Admin\Jobs\Account\UserQuality::class))
\Modules\Admin\Jobs\Account\UserQuality::dispatch($user, $user->account->key);
}
/**
@ -34,6 +37,10 @@ class UserObserver
*/
public function updated(User $user)
{
if(class_exists(\Modules\Admin\Jobs\Account\UserQuality::class))
\Modules\Admin\Jobs\Account\UserQuality::dispatch($user, $user->account->key);
}
/**

View File

@ -339,6 +339,11 @@ class BaseRepository
else
event('eloquent.updated: App\Models\Credit', $model);
if (($state['finished_amount'] != $state['starting_amount']) && ($model->status_id != Credit::STATUS_DRAFT)) {
$model->client->service()->adjustCreditBalance(($state['finished_amount'] - $state['starting_amount']))->save();
}
}
if ($model instanceof Quote) {

View File

@ -43,4 +43,31 @@ class CreditRepository extends BaseRepository
{
return CreditInvitation::where('key', $key)->first();
}
public function delete($credit)
{
if ($credit->is_deleted) {
return;
}
$credit = $credit->service()->deleteCredit()->save();
return parent::delete($credit);
}
public function restore($credit)
{
//we cannot restore a deleted payment.
if (! $credit->trashed()) {
return;
}
parent::restore($credit);
$credit = $credit->service()->restoreCredit()->save();
return $credit;
}
}

View File

@ -139,7 +139,8 @@ class PaymentRepository extends BaseRepository {
//todo optimize this into a single query
foreach ($data['invoices'] as $paid_invoice) {
$invoice = Invoice::withTrashed()->whereId($paid_invoice['invoice_id'])->first();
// $invoice = Invoice::withTrashed()->whereId($paid_invoice['invoice_id'])->first();
$invoice = $invoices->firstWhere('id', $paid_invoice['invoice_id']);
if ($invoice) {
$invoice = $invoice->service()
@ -157,16 +158,20 @@ class PaymentRepository extends BaseRepository {
if (array_key_exists('credits', $data) && is_array($data['credits'])) {
$credit_totals = array_sum(array_column($data['credits'], 'amount'));
$credits = Credit::whereIn('id', $this->transformKeys(array_column($data['credits'], 'credit_id')))->get();
// $credits = Credit::whereIn('id', $this->transformKeys(array_column($data['credits'], 'credit_id')))->get();
$credits = Credit::whereIn('id', array_column($data['credits'], 'credit_id'))->get();
$payment->credits()->saveMany($credits);
//todo optimize into a single query
foreach ($data['credits'] as $paid_credit) {
$credit = Credit::withTrashed()->find($this->decodePrimaryKey($paid_credit['credit_id']));
// $credit = Credit::withTrashed()->find($paid_credit['credit_id']);
$credit = $credits->firstWhere('id', $paid_credit['credit_id']);
if ($credit) {
$credit = $credit->service()->markSent()->save();
ApplyCreditPayment::dispatchNow($credit, $payment, $paid_credit['amount'], $credit->company);
(new ApplyCreditPayment($credit, $payment, $paid_credit['amount'], $credit->company))->handle();
}
}
}
@ -245,7 +250,7 @@ class PaymentRepository extends BaseRepository {
event(new PaymentWasDeleted($payment, $payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
return $payment;
//return parent::delete($payment);
}
public function restore($payment)

View File

@ -142,6 +142,7 @@ class CreditService
$client = $this->credit->client->fresh();
$client->service()
->updatePaidToDate($adjustment)
->adjustCreditBalance($adjustment * -1)
->save();
event('eloquent.created: App\Models\Payment', $payment);
@ -256,6 +257,29 @@ class CreditService
return $this;
}
public function deleteCredit()
{
$this->credit
->client
->service()
->adjustCreditBalance($this->credit->balance * -1)
->save();
return $this;
}
public function restoreCredit()
{
$this->credit
->client
->service()
->adjustCreditBalance($this->credit->balance)
->save();
return $this;
}
/**
* Saves the credit.
* @return Credit object

View File

@ -45,6 +45,11 @@ class MarkSent
->touchPdf()
->save();
$this->client
->service()
->adjustCreditBalance($this->credit->amount)
->save();
event(new CreditWasMarkedSent($this->credit, $this->credit->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
return $this->credit;

View File

@ -162,6 +162,7 @@ class DeletePayment
$client
->service()
->updatePaidToDate(($paymentable_credit->pivot->amount) * -1)
->adjustCreditBalance($paymentable_credit->pivot->amount)
->save();
});
}

View File

@ -90,7 +90,7 @@ class Number
return (float) $s;
}
// remove all seperators from first part and keep the end
// remove all separators from first part and keep the end
$s = str_replace('.', '', substr($s, 0, -3)).substr($s, -3);
// return float

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.5.14',
'app_tag' => '5.5.14',
'app_version' => '5.5.15',
'app_tag' => '5.5.15',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),

View File

@ -21,7 +21,7 @@
</script>
<script>
window.flutterConfiguration = {
canvasKitBaseUrl: "{{config('ninja.app_url')}}/canvaskit/"
canvasKitBaseUrl: "/canvaskit/"
};
</script>
</head>

View File

@ -13,11 +13,13 @@ namespace Tests\Feature;
use App\DataMapper\CompanySettings;
use App\DataMapper\DefaultSettings;
use App\Factory\InvoiceItemFactory;
use App\Models\Account;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\CompanyToken;
use App\Models\Credit;
use App\Models\User;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
@ -61,6 +63,97 @@ class ClientTest extends TestCase
$this->makeTestData();
}
private function buildLineItems($number = 2)
{
$line_items = [];
for($x=0; $x<$number; $x++)
{
$item = InvoiceItemFactory::create();
$item->quantity = 1;
$item->cost = 10;
$line_items[] = $item;
}
return $line_items;
}
public function testCreditBalance()
{
$this->client->credit_balance = 0;
$this->client->save();
$this->assertEquals(0, $this->client->credit_balance);
$credit = [
'status_id' => 1,
'number' => 'dfdfd',
'discount' => 0,
'is_amount_discount' => 1,
'number' => '34343xx43',
'public_notes' => 'notes',
'is_deleted' => 0,
'custom_value1' => 0,
'custom_value2' => 0,
'custom_value3' => 0,
'custom_value4' => 0,
'status' => 1,
'client_id' => $this->encodePrimaryKey($this->client->id),
'line_items' => $this->buildLineItems()
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits/', $credit)
->assertStatus(200);
$arr = $response->json();
$credit_id = $arr['data']['id'];
$credit = Credit::find($this->decodePrimaryKey($credit_id));
$this->assertNotNull($credit);
$this->assertEquals(0, $credit->balance);
$credit->service()->markSent()->save();
$this->assertEquals(20, $credit->balance);
$this->assertEquals(20, $credit->client->fresh()->credit_balance);
//lets now update the credit and increase its balance, this should also increase the credit balance
$data = [
'number' => 'dfdfd',
'discount' => 0,
'is_amount_discount' => 1,
'number' => '34343xx43',
'public_notes' => 'notes',
'is_deleted' => 0,
'custom_value1' => 0,
'custom_value2' => 0,
'custom_value3' => 0,
'custom_value4' => 0,
'status' => 1,
'client_id' => $this->encodePrimaryKey($this->client->id),
'line_items' => $this->buildLineItems(3)
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->put('/api/v1/credits/'.$credit->hashed_id, $data)
->assertStatus(200);
$credit = $credit->fresh();
$this->assertEquals(30, $credit->balance);
$this->assertEquals(30, $credit->client->fresh()->credit_balance);
}
public function testStoreClientUsingCountryCode()
{
$data = [

View File

@ -78,7 +78,7 @@ class ImportCompanyTest extends TestCase
{
parent::setUp();
$this->artisan('db:seed');
// $this->artisan('db:seed');
$this->withoutMiddleware(
ThrottleRequests::class

View File

@ -70,4 +70,54 @@ class CreditBalanceTest extends TestCase
$this->assertEquals($this->client->service()->getCreditBalance(), 0);
}
public function testCreditDeleteCheckClientBalance()
{
$credit = Credit::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'client_id' => $this->client->id,
'balance' => 10,
'number' => 'testing-number-01',
'status_id' => Credit::STATUS_SENT,
]);
$credit->client->credit_balance = 10;
$credit->push();
//delete invoice
$data = [
'ids' => [$credit->hashed_id],
];
//restore invoice
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits/bulk?action=delete', $data)->assertStatus(200);
$client = $credit->client->fresh();
$this->assertEquals(0, $client->credit_balance);
//restore invoice
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits/bulk?action=restore', $data)->assertStatus(200);
$client = $credit->client->fresh();
$this->assertEquals(10, $client->credit_balance);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/credits/bulk?action=archive', $data)->assertStatus(200);
$client = $credit->client->fresh();
$this->assertEquals(10, $client->credit_balance);
}
}