Merge pull request #9341 from turbo124/v5-develop

v5.8.31
This commit is contained in:
David Bomba 2024-02-29 21:35:55 +11:00 committed by GitHub
commit b8f75e4ca0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 424 additions and 33 deletions

View File

@ -1 +1 @@
5.8.30
5.8.31

View File

@ -947,7 +947,35 @@ class CheckData extends Command
});
Company::whereDoesntHave('company_users', function ($query){
$query->where('is_owner', 1);
})
->cursor()
->when(Ninja::isHosted())
->each(function ($c){
$this->logMessage("Orphan Account # {$c->account_id}");
});
CompanyUser::whereDoesntHave('tokens')
->cursor()
->when(Ninja::isHosted())
->each(function ($cu){
$this->logMessage("Missing tokens for Company User # {$cu->id}");
});
CompanyUser::whereDoesntHave('user')
->cursor()
->when(Ninja::isHosted())
->each(function ($cu) {
$this->logMessage("Missing user for Company User # {$cu->id}");
});
}

View File

@ -264,7 +264,7 @@ class BaseRule implements RuleInterface
return USStates::getState(strlen($this->client->postal_code) > 1 ? $this->client->postal_code : $this->client->shipping_postal_code);
} catch (\Exception $e) {
return $this->client->company->country()->iso_3166_2 == 'US' ? $this->client->company->tax_data->seller_subregion : 'CA';
return 'CA';
}
}

View File

@ -29,11 +29,13 @@ class DocumentFilters extends QueryFilters
*/
public function filter(string $filter = ''): Builder
{
if (strlen($filter) == 0) {
return $this->builder;
}
return $this->builder;
return $this->builder->where('name', 'like', '%'.$filter.'%');
}
/**
@ -47,9 +49,42 @@ class DocumentFilters extends QueryFilters
*/
public function client_id(string $client_id = ''): Builder
{
return $this->builder;
return $this->builder->where(function ($query) use ($client_id) {
$query->whereHasMorph('documentable', [
\App\Models\Invoice::class,
\App\Models\Quote::class,
\App\Models\Credit::class,
\App\Models\Expense::class,
\App\Models\Payment::class,
\App\Models\Task::class], function ($q2) use ($client_id) {
$q2->where('client_id', $this->decodePrimaryKey($client_id));
})->orWhereHasMorph('documentable', [\App\Models\Client::class], function ($q3) use ($client_id) {
$q3->where('id', $this->decodePrimaryKey($client_id));
});
});
}
public function type(string $types = '')
{
$types = explode(',', $types);
foreach ($types as $type)
{
match($type) {
'private' => $this->builder->where('is_public', 0),
'public' => $this->builder->where('is_public', 1),
'pdf' => $this->builder->where('type', 'pdf'),
'image' => $this->builder->whereIn('type', ['png','jpeg','jpg','gif','svg']),
'other' => $this->builder->whereNotIn('type', ['pdf','png','jpeg','jpg','gif','svg']),
default => $this->builder,
};
}
return $this->builder;
}
/**
* Sorts the list based on $sort.
*

View File

@ -277,7 +277,7 @@ class InvoiceItemSum
$item_tax += $item_tax_rate1_total;
if (strlen($this->item->tax_name1) > 2) {
if (strlen($this->item->tax_name1) > 1) {
$this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total);
}
@ -285,7 +285,7 @@ class InvoiceItemSum
$item_tax += $item_tax_rate2_total;
if (strlen($this->item->tax_name2) > 2) {
if (strlen($this->item->tax_name2) > 1) {
$this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total);
}
@ -293,7 +293,7 @@ class InvoiceItemSum
$item_tax += $item_tax_rate3_total;
if (strlen($this->item->tax_name3) > 2) {
if (strlen($this->item->tax_name3) > 1) {
$this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total);
}

View File

@ -231,7 +231,7 @@ class InvoiceItemSumInclusive
/** @var float $item_tax */
$item_tax += $this->formatValue($item_tax_rate1_total, $this->currency->precision);
if (strlen($this->item->tax_name1) > 2) {
if (strlen($this->item->tax_name1) > 1) {
$this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total);
}
@ -239,7 +239,7 @@ class InvoiceItemSumInclusive
$item_tax += $this->formatValue($item_tax_rate2_total, $this->currency->precision);
if (strlen($this->item->tax_name2) > 2) {
if (strlen($this->item->tax_name2) > 1) {
$this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total);
}
@ -247,7 +247,7 @@ class InvoiceItemSumInclusive
$item_tax += $this->formatValue($item_tax_rate3_total, $this->currency->precision);
if (strlen($this->item->tax_name3) > 2) {
if (strlen($this->item->tax_name3) > 1) {
$this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total);
}

View File

@ -340,8 +340,7 @@ class InvoiceSumInclusive
$this->total_taxes += $total_line_tax;
}
nlog($this->tax_map);
nlog($this->total_taxes);
return $this;
}

View File

@ -30,16 +30,24 @@ class SmtpController extends BaseController
$user = auth()->user();
$company = $user->company();
$smtp_host = $request->input('smtp_host', $company->smtp_host);
$smtp_port = $request->input('smtp_port', $company->smtp_port);
$smtp_username = $request->input('smtp_username', $company->smtp_username);
$smtp_password = $request->input('smtp_password', $company->smtp_password);
$smtp_encryption = $request->input('smtp_encryption', $company->smtp_encryption ?? 'tls');
$smtp_local_domain = $request->input('smtp_local_domain', strlen($company->smtp_local_domain) > 2 ? $company->smtp_local_domain : null);
$smtp_verify_peer = $request->input('verify_peer', $company->smtp_verify_peer ?? true);
config([
'mail.mailers.smtp' => [
'transport' => 'smtp',
'host' => $request->input('smtp_host', $company->smtp_host),
'port' => $request->input('smtp_port', $company->smtp_port),
'username' => $request->input('smtp_username', $company->smtp_username),
'password' => $request->input('smtp_password', $company->smtp_password),
'encryption' => $request->input('smtp_encryption', $company->smtp_encryption ?? 'tls'),
'local_domain' => $request->input('smtp_local_domain', strlen($company->smtp_local_domain) > 2 ? $company->smtp_local_domain : null),
'verify_peer' => $request->input('verify_peer', $company->smtp_verify_peer ?? true),
'host' => $smtp_host,
'port' => $smtp_port,
'username' => $smtp_username,
'password' => $smtp_password,
'encryption' => $smtp_encryption,
'local_domain' => $smtp_local_domain,
'verify_peer' => $smtp_verify_peer,
'timeout' => 5,
],
]);

View File

@ -54,6 +54,8 @@ class StripeConnectController extends BaseController
$redirect_uri = config('ninja.app_url').'/stripe/completed';
$endpoint = "https://connect.stripe.com/oauth/authorize?response_type=code&client_id={$stripe_client_id}&redirect_uri={$redirect_uri}&scope=read_write&state={$token}";
\Illuminate\Support\Facades\Cache::pull($token);
return redirect($endpoint);
}
@ -64,6 +66,8 @@ class StripeConnectController extends BaseController
if ($request->has('error') && $request->error == 'access_denied') {
return view('auth.connect.access_denied');
}
$response = false;
try {
/** @class \stdClass $response
@ -88,6 +92,11 @@ class StripeConnectController extends BaseController
nlog($response);
} catch (\Exception $e) {
}
if(!$response) {
return view('auth.connect.access_denied');
}
@ -144,11 +153,12 @@ class StripeConnectController extends BaseController
if(isset($request->getTokenContent()['is_react']) && $request->getTokenContent()['is_react']) {
$redirect_uri = config('ninja.react_url').'/#/settings/online_payments';
} else {
$redirect_uri = config('ninja.app_url').'/stripe/completed';
$redirect_uri = config('ninja.app_url');
}
//response here
return view('auth.connect.completed', ['url' => $redirect_uri]);
// return redirect($redirect_uri);
}
}

View File

@ -36,18 +36,46 @@ class CheckSmtpRequest extends Request
public function rules()
{
return [
'smtp_host' => 'sometimes|nullable|string|min:3',
'smtp_port' => 'sometimes|nullable|integer',
'smtp_username' => 'sometimes|nullable|string|min:3',
'smtp_password' => 'sometimes|nullable|string|min:3',
];
}
public function prepareForValidation()
{
/** @var \App\Models\User $user */
$user = auth()->user();
$company = $user->company();
$input = $this->input();
if(isset($input['smtp_username']) && $input['smtp_username'] == '********')
unset($input['smtp_username']);
if(isset($input['smtp_username']) && $input['smtp_username'] == '********'){
// unset($input['smtp_username']);
$input['smtp_username'] = $company->smtp_username;
}
if(isset($input['smtp_password'])&& $input['smtp_password'] == '********'){
// unset($input['smtp_password']);
$input['smtp_password'] = $company->smtp_password;
}
if(isset($input['smtp_host']) && strlen($input['smtp_host']) >=3){
}
else {
$input['smtp_host'] = $company->smtp_host;
}
if(isset($input['smtp_port']) && strlen($input['smtp_port']) >= 3) {
} else {
$input['smtp_port'] = $company->smtp_port;
}
if(isset($input['smtp_password'])&& $input['smtp_password'] == '********')
unset($input['smtp_password']);
$this->replace($input);
}

View File

@ -26,7 +26,6 @@ use App\Utils\Traits\MakesReminders;
use Illuminate\Support\Facades\Auth;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Spatie\OpenTelemetry\Jobs\TraceAware;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

View File

@ -208,6 +208,27 @@ class Document extends BaseModel
return ctrans('texts.document');
}
public function link()
{
$entity_id = $this->encodePrimaryKey($this->documentable_id);
$link = '';
match($this->documentable_type) {
'App\Models\Vendor' => $link = "vendors/{$entity_id}",
'App\Models\Project' => $link = "projects/{$entity_id}",
'invoices' => $link = "invoices/{$entity_id}/edit",
'App\Models\Quote' => $link = "quotes/{$entity_id}/edit",
'App\Models\Credit' => $link = "credits/{$entity_id}/edit",
'App\Models\Expense' => $link = "expenses/{$entity_id}/edit",
'App\Models\Payment' => $link = "payments/{$entity_id}/edit",
'App\Models\Task' => $link = "tasks/{$entity_id}/edit",
'App\Models\Client' => $link = "clients/{$entity_id}",
default => $link = '',
};
return $link;
}
public function compress(): mixed
{

View File

@ -52,6 +52,7 @@ class DocumentTransformer extends EntityTransformer
'created_at' => (int) $document->created_at,
'is_deleted' => (bool) false,
'is_public' => (bool) $document->is_public,
'link' => (string) $document->link(),
];
}
}

View File

@ -397,7 +397,8 @@ class HtmlEngine
$data['$credit.date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()), 'label' => ctrans('texts.credit_date')];
$data['$balance'] = ['value' => Number::formatMoney($this->getBalance(), $this->client) ?: ' ', 'label' => ctrans('texts.balance')];
$data['$credit.balance'] = ['value' => Number::formatMoney($this->entity_calc->getBalance(), $this->client) ?: ' ', 'label' => ctrans('texts.credit_balance')];
$data['$client.credit_balance'] = &$data['$credit.balance'];
$data['$invoice.balance'] = &$data['$balance'];
$data['$taxes'] = ['value' => Number::formatMoney($this->entity_calc->getItemTotalTaxes(), $this->client) ?: ' ', 'label' => ctrans('texts.taxes')];
$data['$invoice.taxes'] = &$data['$taxes'];

View File

@ -93,7 +93,7 @@ class Number
* @param string $value The formatted number to be converted back to float
* @return float The formatted value
*/
public static function parseFloat($value)
public static function parseFloat2($value)
{
if(!$value)
return 0;
@ -104,7 +104,7 @@ class Number
$decimal = strpos($value, '.');
$comma = strpos($value, ',');
if(!$comma) //no comma must be a decimal number already
if($comma === false) //no comma must be a decimal number already
return (float) $value;
if($decimal < $comma){ //decimal before a comma = euro
@ -143,6 +143,52 @@ class Number
// return (float) $s;
}
//next iteration of float parsing
public static function parseFloat($value)
{
if(!$value) {
return 0;
}
//remove everything except for numbers, decimals, commas and hyphens
$value = preg_replace('/[^0-9.,-]+/', '', $value);
$decimal = strpos($value, '.');
$comma = strpos($value, ',');
//check the 3rd last character
if(!in_array(substr($value, -3, 1), [".", ","])) {
if($comma && (substr($value, -3, 1) != ".")) {
$value .= ".00";
} elseif($decimal && (substr($value, -3, 1) != ",")) {
$value .= ",00";
}
}
$decimal = strpos($value, '.');
$comma = strpos($value, ',');
if($comma === false) { //no comma must be a decimal number already
return (float) $value;
}
if($decimal < $comma) { //decimal before a comma = euro
$value = str_replace(['.',','], ['','.'], $value);
return (float) $value;
}
//comma first = traditional thousan separator
$value = str_replace(',', '', $value);
return (float)$value;
}
public static function parseStringFloat($value)
{
$value = preg_replace('/[^0-9-.]+/', '', $value);

View File

@ -84,6 +84,7 @@ class SystemHealth
'trailing_slash' => (bool) self::checkUrlState(),
'file_permissions' => (string) self::checkFileSystem(),
'exchange_rate_api_not_configured' => (bool)self::checkCurrencySanity(),
'api_version' => (string) config('ninja.app_version'),
];
}

View File

@ -17,8 +17,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => env('APP_VERSION', '5.8.30'),
'app_tag' => env('APP_TAG', '5.8.30'),
'app_version' => env('APP_VERSION', '5.8.31'),
'app_tag' => env('APP_TAG', '5.8.31'),
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),

View File

@ -5240,6 +5240,7 @@ $lang = array(
'use_available_payments' => 'Use Available Payments',
'test_email_sent' => 'Successfully sent email',
'gateway_type' => 'Gateway Type',
'save_template_body' => 'Would you like to save this import mapping as a template for future use?',
);
return $lang;

View File

@ -11,13 +11,14 @@
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\Task;
use App\Models\Document;
use Tests\MockAccountData;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Session;
use Tests\MockAccountData;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* @test
@ -44,6 +45,135 @@ class DocumentsApiTest extends TestCase
Model::reguard();
}
public function testDocumentFilters()
{
Document::query()->withTrashed()->cursor()->each(function ($d){
$d->forceDelete();
});
$d = Document::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'name' => 'searchable.jpg',
'type' => 'jpg',
]);
$this->client->documents()->save($d);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get("/api/v1/documents/{$d->hashed_id}?client_id={$this->client->hashed_id}");
$response->assertStatus(200);
$this->assertCount(1, $response->json());
}
public function testDocumentFilters2()
{
Document::query()->withTrashed()->cursor()->each(function ($d){
$d->forceDelete();
});
$d = Document::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'name' => 'searchable.jpg',
'type' => 'jpg',
]);
$this->task->documents()->save($d);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get("/api/v1/documents/{$d->hashed_id}?client_id={$this->client->hashed_id}");
$response->assertStatus(200);
$this->assertCount(1, $response->json());
}
public function testDocumentFilters3()
{
Document::query()->withTrashed()->cursor()->each(function ($d){
$d->forceDelete();
});
$d = Document::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'name' => 'searchable.jpg',
'type' => 'jpg',
]);
$t = Task::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
]);
$t->documents()->save($d);
$dd = Document::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'name' => 'searchable2.jpg',
'type' => 'jpg',
]);
$this->client->documents()->save($dd);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get("/api/v1/documents?client_id={$this->client->hashed_id}");
$response->assertStatus(200);
$this->assertCount(2, $response->json()['data']);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get("/api/v1/documents?client_id={$this->client->hashed_id}&filter=craycray");
$response->assertStatus(200);
$this->assertCount(0, $response->json()['data']);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get("/api/v1/documents?client_id={$this->client->hashed_id}&filter=s");
$response->assertStatus(200);
$this->assertCount(2, $response->json()['data']);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get("/api/v1/documents?client_id={$this->client->hashed_id}&filter=searchable");
$response->assertStatus(200);
$this->assertCount(2, $response->json()['data']);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get("/api/v1/documents?client_id={$this->client->hashed_id}&filter=searchable2");
$response->assertStatus(200);
$this->assertCount(1, $response->json()['data']);
}
public function testIsPublicTypesForDocumentRequest()
{
$d = Document::factory()->create([

View File

@ -36,6 +36,42 @@ class InvoiceItemTest extends TestCase
}
public function testEdgeCasewithDiscountsPercentageAndTaxCalculations()
{
$invoice = InvoiceFactory::create($this->company->id, $this->user->id);
$invoice->client_id = $this->client->id;
$invoice->uses_inclusive_taxes = false;
$invoice->is_amount_discount =false;
$invoice->discount = 0;
$invoice->tax_rate1 = 0;
$invoice->tax_rate2 = 0;
$invoice->tax_rate3 = 0;
$invoice->tax_name1 = '';
$invoice->tax_name2 = '';
$invoice->tax_name3 = '';
$line_items = [];
$line_item = new InvoiceItem;
$line_item->quantity = 1;
$line_item->cost = 100;
$line_item->tax_rate1 = 22;
$line_item->tax_name1 = 'Km';
$line_item->product_key = 'Test';
$line_item->notes = 'Test';
$line_item->is_amount_discount = false;
$line_items[] = $line_item;
$invoice->line_items = $line_items;
$invoice->save();
$invoice = $invoice->calc()->getInvoice();
$this->assertEquals(122, $invoice->amount);
$this->assertEquals(22, $invoice->total_taxes);
}
public function testDiscountsWithInclusiveTaxes()
{
$invoice = InvoiceFactory::create($this->company->id, $this->user->id);

View File

@ -20,6 +20,53 @@ use Tests\TestCase;
*/
class NumberTest extends TestCase
{
public function testRangeOfNumberFormats()
{
$floatvals = [
"22000.76" =>"22 000,76",
"22000.76" =>"22.000,76",
"22000.76" =>"22,000.76",
"22000" =>"22 000",
"22000" =>"22,000",
"22000" =>"22.000",
"22000.76" =>"22000.76",
"22000.76" =>"22000,76",
"1022000.76" =>"1.022.000,76",
"1022000.76" =>"1,022,000.76",
"1000000" =>"1,000,000",
"1000000" =>"1.000.000",
"1022000.76" =>"1022000.76",
"1022000.76" =>"1022000,76",
"1022000" =>"1022000",
"0.76" =>"0.76",
"0.76" =>"0,76",
"0" =>"0.00",
"0" =>"0,00",
"1" =>"1.00",
"1" =>"1,00",
"423545" =>"423545 €",
"423545" =>"423,545 €",
"423545" =>"423.545 €",
"1" =>"1,00 €",
"1.02" =>"€ 1.02",
"1000.02" =>"1'000,02 EUR",
"1000.02" =>"1 000.02$",
"1000.02" =>"1,000.02$",
"1000.02" =>"1.000,02 EURO"
];
foreach($floatvals as $key => $value) {
$this->assertEquals($key, Number::parseFloat($value));
}
}
public function testNegativeFloatParse()
{