1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-24 10:21:35 +02:00

Merge pull request #7759 from turbo124/v5-stable

v5.5.12
This commit is contained in:
David Bomba 2022-08-19 14:15:00 +10:00 committed by GitHub
commit e86c5e126a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 487 additions and 51 deletions

View File

@ -1 +1 @@
5.5.11
5.5.12

View File

@ -587,21 +587,21 @@ class CreditController extends BaseController
$this->credit_repository->archive($credit);
if (! $bulk) {
return $this->listResponse($credit);
return $this->itemResponse($credit);
}
break;
case 'restore':
$this->credit_repository->restore($credit);
if (! $bulk) {
return $this->listResponse($credit);
return $this->itemResponse($credit);
}
break;
case 'delete':
$this->credit_repository->delete($credit);
if (! $bulk) {
return $this->listResponse($credit);
return $this->itemResponse($credit);
}
break;
case 'email':

View File

@ -722,14 +722,14 @@ class InvoiceController extends BaseController
$this->invoice_repo->restore($invoice);
if (! $bulk) {
return $this->listResponse($invoice);
return $this->itemResponse($invoice);
}
break;
case 'archive':
$this->invoice_repo->archive($invoice);
if (! $bulk) {
return $this->listResponse($invoice);
return $this->itemResponse($invoice);
}
break;
case 'delete':
@ -737,7 +737,7 @@ class InvoiceController extends BaseController
$this->invoice_repo->delete($invoice);
if (! $bulk) {
return $this->listResponse($invoice);
return $this->itemResponse($invoice);
}
break;
case 'cancel':

View File

@ -623,14 +623,14 @@ class PurchaseOrderController extends BaseController
$this->purchase_order_repository->restore($purchase_order);
if (! $bulk) {
return $this->listResponse($purchase_order);
return $this->itemResponse($purchase_order);
}
break;
case 'archive':
$this->purchase_order_repository->archive($purchase_order);
if (! $bulk) {
return $this->listResponse($purchase_order);
return $this->itemResponse($purchase_order);
}
break;
case 'delete':
@ -638,7 +638,7 @@ class PurchaseOrderController extends BaseController
$this->purchase_order_repository->delete($purchase_order);
if (! $bulk) {
return $this->listResponse($purchase_order);
return $this->itemResponse($purchase_order);
}
break;
@ -684,7 +684,7 @@ class PurchaseOrderController extends BaseController
}
if (! $bulk) {
return $this->listResponse($purchase_order);
return $this->itemResponse($purchase_order);
}
break;

View File

@ -728,7 +728,7 @@ class QuoteController extends BaseController
$this->quote_repo->restore($quote);
if (! $bulk) {
return $this->listResponse($quote);
return $this->itemResponse($quote);
}
break;
@ -736,7 +736,7 @@ class QuoteController extends BaseController
$this->quote_repo->archive($quote);
if (! $bulk) {
return $this->listResponse($quote);
return $this->itemResponse($quote);
}
break;
@ -744,7 +744,7 @@ class QuoteController extends BaseController
$this->quote_repo->delete($quote);
if (! $bulk) {
return $this->listResponse($quote);
return $this->itemResponse($quote);
}
break;

View File

@ -511,21 +511,21 @@ class RecurringExpenseController extends BaseController
$this->recurring_expense_repo->archive($recurring_expense);
if (! $bulk) {
return $this->listResponse($recurring_expense);
return $this->itemResponse($recurring_expense);
}
break;
case 'restore':
$this->recurring_expense_repo->restore($recurring_expense);
if (! $bulk) {
return $this->listResponse($recurring_expense);
return $this->itemResponse($recurring_expense);
}
break;
case 'delete':
$this->recurring_expense_repo->delete($recurring_expense);
if (! $bulk) {
return $this->listResponse($recurring_expense);
return $this->itemResponse($recurring_expense);
}
break;
case 'email':

View File

@ -662,21 +662,21 @@ class RecurringInvoiceController extends BaseController
$this->recurring_invoice_repo->archive($recurring_invoice);
if (! $bulk) {
return $this->listResponse($recurring_invoice);
return $this->itemResponse($recurring_invoice);
}
break;
case 'restore':
$this->recurring_invoice_repo->restore($recurring_invoice);
if (! $bulk) {
return $this->listResponse($recurring_invoice);
return $this->itemResponse($recurring_invoice);
}
break;
case 'delete':
$this->recurring_invoice_repo->delete($recurring_invoice);
if (! $bulk) {
return $this->listResponse($recurring_invoice);
return $this->itemResponse($recurring_invoice);
}
break;
case 'email':

View File

@ -47,15 +47,4 @@ class LoginRequest extends Request
];
}
// public function prepareForValidation()
// {
// $input = $this->all();
// // if(base64_decode(base64_encode($input['password'])) === $input['password'])
// // $input['password'] = base64_decode($input['password']);
// // nlog($input['password']);
// $this->replace($input);
// }
}

View File

@ -12,6 +12,7 @@
namespace App\Http\Requests\Task;
use App\Http\Requests\Request;
use App\Models\Project;
use App\Models\Task;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
@ -38,19 +39,49 @@ class StoreTaskRequest extends Request
$rules['number'] = Rule::unique('tasks')->where('company_id', auth()->user()->company()->id);
}
if(isset($this->client_id))
$rules['client_id'] = 'bail|required|exists:clients,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
if(isset($this->project_id))
$rules['project_id'] = 'bail|required|exists:projects,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['timelog'] = ['bail','array',function ($attribute, $values, $fail) {
foreach($values as $k)
{
if(!is_int($k[0]) || !is_int($k[1]))
$fail('The '.$attribute.' - '.print_r($k,1).' is invalid. Unix timestamps only.');
}
}];
return $this->globalRules($rules);
}
public function prepareForValidation()
{
$input = $this->all();
$input = $this->decodePrimaryKeys($this->all());
if (array_key_exists('status_id', $input) && is_string($input['status_id'])) {
$input['status_id'] = $this->decodePrimaryKey($input['status_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

@ -12,6 +12,7 @@
namespace App\Http\Requests\Task;
use App\Http\Requests\Request;
use App\Models\Project;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
@ -39,6 +40,22 @@ class UpdateTaskRequest extends Request
$rules['number'] = Rule::unique('tasks')->where('company_id', auth()->user()->company()->id)->ignore($this->task->id);
}
if(isset($this->client_id))
$rules['client_id'] = 'bail|required|exists:clients,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
if(isset($this->project_id))
$rules['project_id'] = 'bail|required|exists:projects,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['timelog'] = ['bail','array',function ($attribute, $values, $fail) {
foreach($values as $k)
{
if(!is_int($k[0]) || !is_int($k[1]))
$fail('The '.$attribute.' - '.print_r($k,1).' is invalid. Unix timestamps only.');
}
}];
return $this->globalRules($rules);
}
@ -50,6 +67,20 @@ class UpdateTaskRequest extends Request
$input['status_id'] = $this->decodePrimaryKey($input['status_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']);
}
}
if (array_key_exists('color', $input) && is_null($input['color'])) {
$input['color'] = '';
}

View File

@ -24,7 +24,8 @@ class BlackListRule implements Rule
'vusra.com',
'fourthgenet.com',
'arxxwalls.com',
'superhostforumla.com'
'superhostforumla.com',
'wnpop.com',
];
/**

View File

@ -324,6 +324,10 @@ class NinjaMailerJob implements ShouldQueue
if($this->company->is_disabled && !$this->override)
return true;
/* To handle spam users we drop all emails from flagged accounts */
if(Ninja::isHosted() && $this->company->account && $this->company->account->is_flagged)
return true;
/* On the hosted platform we set default contacts a @example.com email address - we shouldn't send emails to these types of addresses */
if(Ninja::isHosted() && $this->nmo->to_user && strpos($this->nmo->to_user->email, '@example.com') !== false)
return true;
@ -336,10 +340,6 @@ class NinjaMailerJob implements ShouldQueue
if(Ninja::isHosted() && $this->company->account && $this->company->account->emailQuotaExceeded())
return true;
/* To handle spam users we drop all emails from flagged accounts */
if(Ninja::isHosted() && $this->company->account && $this->company->account->is_flagged)
return true;
/* If the account is verified, we allow emails to flow */
if(Ninja::isHosted() && $this->company->account && $this->company->account->is_verified_account) {

View File

@ -236,6 +236,11 @@ class Client extends BaseModel implements HasLocalePreference
return $this->hasMany(Task::class)->withTrashed();
}
public function payments()
{
return $this->hasMany(Payment::class)->withTrashed();
}
public function recurring_invoices()
{
return $this->hasMany(RecurringInvoice::class)->withTrashed();
@ -627,11 +632,6 @@ class Client extends BaseModel implements HasLocalePreference
return $defaults;
}
public function payments()
{
return $this->hasMany(Payment::class)->withTrashed();
}
public function timezone_offset()
{
$offset = 0;

View File

@ -15,6 +15,7 @@ use App\Models\Invoice;
use App\Services\AbstractService;
use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter;
use Illuminate\Support\Facades\DB;
class HandleRestore extends AbstractService
{
@ -24,6 +25,10 @@ class HandleRestore extends AbstractService
private $payment_total = 0;
private $total_payments = 0;
private $adjustment_amount = 0;
public function __construct(Invoice $invoice)
{
$this->invoice = $invoice;
@ -47,16 +52,90 @@ class HandleRestore extends AbstractService
//adjust ledger balance
$this->invoice->ledger()->updateInvoiceBalance($this->invoice->balance, "Restored invoice {$this->invoice->number}")->save();
$this->invoice->client->service()->updateBalance($this->invoice->balance)->save();
$this->invoice->client
->service()
->updateBalance($this->invoice->balance)
->updatePaidToDate($this->invoice->paid_to_date)
->save();
$this->windBackInvoiceNumber();
$this->invoice->is_deleted = false;
$this->invoice->save();
$this->restorePaymentables()
->setAdjustmentAmount()
->adjustPayments();
return $this->invoice;
}
/* Touches all paymentables as deleted */
private function restorePaymentables()
{
$this->invoice->payments->each(function ($payment) {
$payment->paymentables()
->where('paymentable_type', '=', 'invoices')
->where('paymentable_id', $this->invoice->id)
->update(['deleted_at' => false]);
});
return $this;
}
private function setAdjustmentAmount()
{
foreach ($this->invoice->payments as $payment) {
$this->adjustment_amount += $payment->paymentables
->where('paymentable_type', '=', 'invoices')
->where('paymentable_id', $this->invoice->id)
->sum(DB::raw('amount'));
$this->adjustment_amount += $payment->paymentables
->where('paymentable_type', '=', 'invoices')
->where('paymentable_id', $this->invoice->id)
->sum(DB::raw('refunded'));
}
$this->total_payments = $this->invoice->payments->sum('amount') - $this->invoice->payments->sum('refunded');
return $this;
}
private function adjustPayments()
{
//if total payments = adjustment amount - that means we need to delete the payments as well.
if ($this->adjustment_amount == $this->total_payments) {
$this->invoice->payments()->update(['payments.deleted_at' => null, 'payments.is_deleted' => false]);
} else {
//adjust payments down by the amount applied to the invoice payment.
$this->invoice->payments->each(function ($payment) {
$payment_adjustment = $payment->paymentables
->where('paymentable_type', '=', 'invoices')
->where('paymentable_id', $this->invoice->id)
->sum(DB::raw('amount'));
$payment_adjustment -= $payment->paymentables
->where('paymentable_type', '=', 'invoices')
->where('paymentable_id', $this->invoice->id)
->sum(DB::raw('refunded'));
$payment->amount += $payment_adjustment;
$payment->applied += $payment_adjustment;
$payment->is_deleted = false;
$payment->restore();
$payment->save();
});
}
return $this;
}
private function windBackInvoiceNumber()
{
$findme = '_'.ctrans('texts.deleted');

View File

@ -394,7 +394,7 @@ class Design extends BaseDesign
public function productTable(): array
{
$product_items = collect($this->entity->line_items)->filter(function ($item) {
return $item->type_id == 1 || $item->type_id == 6;
return $item->type_id == 1 || $item->type_id == 6 || $item->type_id == 5;
});
if (count($product_items) == 0) {

View File

@ -282,9 +282,9 @@ trait MakesInvoiceValues
}
if ($table_type == '$task' && $item->type_id != 2) {
if ($item->type_id != 4 && $item->type_id != 5) {
// if ($item->type_id != 4 && $item->type_id != 5) {
continue;
}
// }
}
$helpers = new Helpers();

View File

@ -136,7 +136,6 @@
"if [ \"${IS_DOCKER:-false}\" != \"true\" ]; then vendor/bin/snappdf download; fi"
],
"post-update-cmd": [
"vendor/bin/snappdf download",
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [

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.11',
'app_tag' => '5.5.11',
'app_version' => '5.5.12',
'app_tag' => '5.5.12',
'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: "/canvaskit/"
canvasKitBaseUrl: "{{config('ninja.app_url')}}/canvaskit/"
};
</script>
</head>

View File

@ -15,6 +15,7 @@ use App\Factory\InvoiceItemFactory;
use App\Models\Client;
use App\Models\Invoice;
use App\Models\Payment;
use App\Repositories\InvoiceRepository;
use App\Utils\Traits\MakesHash;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Routing\Middleware\ThrottleRequests;
@ -41,6 +42,150 @@ class DeleteInvoiceTest extends TestCase
);
}
public function testDeleteAndRestoreInvoice()
{
//create an invoice for 36000 with a partial of 6000
$data = [
'name' => 'A Nice Client - About to be deleted',
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/clients', $data);
$response->assertStatus(200);
$arr = $response->json();
$client_hash_id = $arr['data']['id'];
$client = Client::find($this->decodePrimaryKey($client_hash_id));
$this->assertEquals($client->balance, 0);
$this->assertEquals($client->paid_to_date, 0);
$line_items = [];
$item = InvoiceItemFactory::create();
$item->quantity = 1;
$item->cost = 36000;
$line_items[] = (array) $item;
$invoice = [
'status_id' => 1,
'number' => '',
'discount' => 0,
'is_amount_discount' => 1,
'po_number' => '3434343',
'public_notes' => 'notes',
'is_deleted' => 0,
'partial' => 6000,
'custom_value1' => 0,
'custom_value2' => 0,
'custom_value3' => 0,
'custom_value4' => 0,
'client_id' => $client_hash_id,
'line_items' => (array) $line_items,
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/invoices/', $invoice)
->assertStatus(200);
$arr = $response->json();
$invoice_one_hashed_id = $arr['data']['id'];
$invoice = Invoice::find($this->decodePrimaryKey($invoice_one_hashed_id));
$invoice = $invoice->service()->markSent()->save();
$this->assertEquals(6000, $invoice->partial);
$this->assertEquals(36000, $invoice->amount);
// apply a payment of 6000
$data = [
'amount' => 6000,
'client_id' => $client->hashed_id,
'invoices' => [
[
'invoice_id' => $invoice->hashed_id,
'amount' => 6000,
],
],
'date' => '2019/12/12',
];
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/payments?include=invoices', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
$this->assertNotNull($message);
}
$response->assertStatus(200);
$arr = $response->json();
$payment_id = $arr['data']['id'];
$payment = Payment::withTrashed()->whereId($this->decodePrimaryKey($payment_id))->first();
$this->assertEquals(6000, $payment->amount);
$this->assertEquals(6000, $payment->applied);
$this->assertEquals(6000, $payment->client->paid_to_date);
$invoice = $invoice->fresh();
$this->assertEquals(30000, $invoice->balance);
$this->assertEquals(6000, $invoice->paid_to_date);
//delete the invoice an inspect the balances
$invoice_repo = new InvoiceRepository();
$invoice = $invoice_repo->delete($invoice);
$invoice = $invoice->fresh();
$this->assertTrue($invoice->is_deleted);
$payment = $payment->fresh();
$this->assertTrue($payment->is_deleted);
$this->assertEquals(4, $payment->status_id);
$client->fresh();
$this->assertEquals(0, $client->balance);
$this->assertEquals(0, $client->paid_to_date);
//restore the invoice. this should also rehydrate the payments and restore the correct paid to dates on the client record
$invoice_repo->restore($invoice);
$invoice = $invoice->fresh();
$client = $client->fresh();
$payment = $payment->fresh();
$this->assertEquals(30000, $invoice->balance);
$this->assertEquals(6000, $invoice->paid_to_date);
$this->assertEquals(6000, $client->paid_to_date);
$this->assertEquals(30000, $client->balance);
$this->assertEquals(6000, $payment->amount);
$this->assertFalse($payment->is_deleted);
$this->assertNull($payment->deleted_at);
}
public function testInvoiceDeletionAfterCancellation()
{
$data = [

View File

@ -46,6 +46,18 @@ class InvoiceTest extends TestCase
$this->makeTestData();
}
public function testInvoiceArchiveAction()
{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/invoices/'.$this->invoice->hashed_id.'/archive',)
->assertStatus(200);
}
public function testMarkingDeletedInvoiceAsSent()
{
Client::factory()->create(['user_id' => $this->user->id, 'company_id' => $this->company->id])->each(function ($c) {
@ -290,4 +302,6 @@ class InvoiceTest extends TestCase
])->post('/api/v1/invoices/', $data)
->assertStatus(200);
}
}

View File

@ -42,6 +42,97 @@ class TaskApiTest extends TestCase
Model::reguard();
}
public function testTimeLogValidation()
{
$data = [
'timelog' => $this->faker->firstName(),
];
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/tasks', $data);
$arr = $response->json();
} catch (ValidationException $e) {
$response->assertStatus(302);
}
}
public function testTimeLogValidation1()
{
$data = [
'timelog' => [[1,2],[3,4]],
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/tasks', $data);
$arr = $response->json();
$response->assertStatus(200);
}
public function testTimeLogValidation2()
{
$data = [
'timelog' => [],
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/tasks', $data);
$arr = $response->json();
$response->assertStatus(200);
}
public function testTimeLogValidation3()
{
$data = [
'timelog' => [["a","b"],["c","d"]],
];
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/tasks', $data);
$arr = $response->json();
} catch (ValidationException $e) {
$response->assertStatus(302);
}
}
public function testTimeLogValidation4()
{
$data = [
'timelog' => [[1,2],[3,0]],
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/tasks', $data);
$arr = $response->json();
$response->assertStatus(200);
}
public function testStartTask()
{
$log = [
@ -76,6 +167,7 @@ class TaskApiTest extends TestCase
$data = [
'description' => $this->faker->firstName(),
'number' => 'taskynumber',
'client_id' => $this->client->id,
];
$response = $this->withHeaders([
@ -126,6 +218,24 @@ class TaskApiTest extends TestCase
$this->assertNotEmpty($arr['data']['number']);
}
public function testTaskWithBadClientId()
{
$data = [
'client_id' => $this->faker->firstName(),
];
try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/tasks', $data);
$arr = $response->json();
} catch (ValidationException $e) {
$response->assertStatus(302);
}
}
public function testTaskPostWithActionStart()
{
$data = [

View File

@ -13,6 +13,7 @@ namespace Tests\Unit;
use App\Factory\InvoiceItemFactory;
use App\Helpers\Invoice\InvoiceSum;
use App\Helpers\Invoice\InvoiceSumInclusive;
use App\Models\Invoice;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\MockAccountData;
@ -41,11 +42,47 @@ class InvoiceTest extends TestCase
$this->invoice->line_items = $this->buildLineItems();
$this->invoice->usesinclusive_taxes = true;
$this->invoice->uses_inclusive_taxes = true;
$this->invoice_calc = new InvoiceSum($this->invoice);
}
public function testInclusiveRounding()
{
$this->invoice->line_items = [];
$this->invoice->discount = 0;
$this->invoice->uses_inclusive_taxes = true;
$this->invoice->save();
$item = InvoiceItemFactory::create();
$item->quantity = 1;
$item->cost = 50;
$item->tax_name1 = "taxy";
$item->tax_rate1 = 19;
$line_items[] = $item;
$item = InvoiceItemFactory::create();
$item->quantity = 1;
$item->cost = 50;
$item->tax_name1 = "taxy";
$item->tax_rate1 = 19;
$line_items[] = $item;
$this->invoice->line_items = $line_items;
$this->invoice->save();
$invoice_calc = new InvoiceSumInclusive($this->invoice);
$invoice_calc->build();
// $this->invoice->save();
$this->assertEquals($invoice_calc->getTotalTaxes(), 15.96);
}
private function buildLineItems()
{
$line_items = [];