This commit is contained in:
David Bomba 2024-07-05 11:53:58 +10:00
commit 1adbd3907e
24 changed files with 280157 additions and 279148 deletions

View File

@ -259,10 +259,17 @@ class ExpenseExport extends BaseExport
{
$precision = $expense->currency->precision ?? 2;
$entity['expense.net_amount'] = round($expense->amount, $precision);
if($expense->calculate_tax_by_amount) {
$total_tax_amount = round($expense->tax_amount1 + $expense->tax_amount2 + $expense->tax_amount3, $precision);
if($expense->uses_inclusive_taxes) {
$entity['expense.net_amount'] = round($expense->amount, $precision) - $total_tax_amount;
}
else {
$entity['expense.net_amount'] = round($expense->amount, $precision);
}
} else {
if($expense->uses_inclusive_taxes) {

View File

@ -152,22 +152,22 @@ class InvoiceFilters extends QueryFilters
{
return $this->builder->where(function ($query) {
$query->whereIn('status_id', [Invoice::STATUS_PARTIAL, Invoice::STATUS_SENT])
->where('is_deleted', 0)
->where('balance', '>', 0)
->where(function ($query) {
$query->whereIn('invoices.status_id', [Invoice::STATUS_PARTIAL, Invoice::STATUS_SENT])
->where('invoices.is_deleted', 0)
->where('invoices.balance', '>', 0)
->orWhere(function ($query) {
$query->whereNull('due_date')
$query->whereNull('invoices.due_date')
->orWhere(function ($q) {
$q->where('due_date', '>=', now()->startOfDay()->subSecond())->where('partial', 0);
$q->where('invoices.due_date', '>=', now()->startOfDay()->subSecond())->where('invoices.partial', 0);
})
->orWhere(function ($q) {
$q->where('partial_due_date', '>=', now()->startOfDay()->subSecond())->where('partial', '>', 0);
$q->where('invoices.partial_due_date', '>=', now()->startOfDay()->subSecond())->where('invoices.partial', '>', 0);
});
})
->orderByRaw('ISNULL(due_date), due_date ' . 'desc')
->orderByRaw('ISNULL(partial_due_date), partial_due_date ' . 'desc');
->orderByRaw('ISNULL(invoices.due_date), invoices.due_date ' . 'desc')
->orderByRaw('ISNULL(invoices.partial_due_date), invoices.partial_due_date ' . 'desc');
});
}
@ -337,10 +337,10 @@ class InvoiceFilters extends QueryFilters
// return $this->builder->orderByRaw('CAST(number AS UNSIGNED), number ' . $dir);
// return $this->builder->orderByRaw("number REGEXP '^[A-Za-z]+$',CAST(number as SIGNED INTEGER),CAST(REPLACE(number,'-','')AS SIGNED INTEGER) ,number");
// return $this->builder->orderByRaw('ABS(number) ' . $dir);
return $this->builder->orderByRaw("REGEXP_REPLACE(number,'[^0-9]+','')+0 " . $dir);
return $this->builder->orderByRaw("REGEXP_REPLACE(invoices.number,'[^0-9]+','')+0 " . $dir);
}
return $this->builder->orderBy($sort_col[0], $dir);
return $this->builder->orderBy("{$this->builder->getQuery()->from}.".$sort_col[0], $dir);
}
/**

View File

@ -273,23 +273,11 @@ abstract class QueryFilters
public function filter_deleted_clients($value)
{
// if ($value == 'true') {
// return $this->builder->whereHas('client', function (Builder $query) {
// $query->where('is_deleted', 0);
// });
// }
if($value == 'true')
{
return $this->builder->leftJoin('clients', function($join) {
$join->on("{$this->builder->getQuery()->from}.client_id", '=', 'clients.id')
->where('clients.is_deleted', 0)
->whereNull('clients.deleted_at');
if ($value == 'true') {
return $this->builder->whereHas('client', function (Builder $query) {
$query->where('is_deleted', 0);
});
}
}
return $this->builder;
}

View File

@ -115,13 +115,8 @@ class AccountController extends BaseController
public function update(UpdateAccountRequest $request, Account $account)
{
$fi = new \FilesystemIterator(public_path('react'), \FilesystemIterator::SKIP_DOTS);
if (iterator_count($fi) < 30) {
return response()->json(['message' => 'React App Not Installed, Please install the React app before attempting to switch.'], 400);
}
$account->fill($request->all());
$account->set_react_as_default_ap = $request->input('set_react_as_default_ap');
$account->save();
$this->entity_type = Account::class;

View File

@ -937,7 +937,9 @@ class BaseController extends Controller
} elseif (in_array($this->entity_type, [Design::class, GroupSetting::class, PaymentTerm::class, TaskStatus::class])) {
// nlog($this->entity_type);
} else {
$query->where('user_id', '=', $user->id)->orWhere('assigned_user_id', $user->id);
$query->where(function ($q) use ($user){ //grouping these together improves query performance significantly)
$q->where('user_id', '=', $user->id)->orWhere('assigned_user_id', $user->id);
});
}
}

View File

@ -56,7 +56,8 @@ class SetDomainNameDb
return response()->json($error, 403);
} else {
MultiDB::setDb('db-ninja-01');
nlog('I could not set the DB - defaulting to DB1');
nlog('SetDomainNameDb:: I could not set the DB - defaulting to DB1');
$request->session()->invalidate();
//abort(400, 'Domain not found');
}
}
@ -73,7 +74,8 @@ class SetDomainNameDb
return response()->json($error, 403);
} else {
MultiDB::setDb('db-ninja-01');
nlog('I could not set the DB - defaulting to DB1');
nlog('SetDomainNameDb:: I could not set the DB - defaulting to DB1');
$request->session()->invalidate();
}
}
}

View File

@ -47,7 +47,7 @@ class InvoiceTransformer extends BaseTransformer
'due_date' => isset($invoice_data['Due Date']) ? $this->parseDate($invoice_data['Due Date']) : null,
'po_number' => $this->getString($invoice_data, 'PurchaseOrder'),
'public_notes' => $this->getString($invoice_data, 'Notes'),
'currency_id' => $this->getCurrencyByCode($invoice_data, 'Currency'),
// 'currency_id' => $this->getCurrencyByCode($invoice_data, 'Currency'),
'amount' => $this->getFloat($invoice_data, 'Total'),
'balance' => $this->getFloat($invoice_data, 'Balance'),
'status_id' => $invoiceStatusMap[$status =

View File

@ -86,6 +86,9 @@ class UpdateCalculatedFields
foreach(json_decode($task->time_log) as $log) {
if(!is_array($log))
continue;
$start_time = $log[0];
$end_time = $log[1] == 0 ? time() : $log[1];

View File

@ -47,7 +47,7 @@ class TaskAssigned implements ShouldQueue
$company_user = $this->task->assignedCompanyUser();
if($this->findEntityAssignedNotification($company_user, 'task'))
if($company_user && $this->findEntityAssignedNotification($company_user, 'task'))
{
$mo = new EmailObject();
$mo->subject = ctrans('texts.task_assigned_subject', ['task' => $this->task->number, 'date' => now()->setTimeZone($this->task->company->timezone()->name)->format($this->task->company->date_format()) ]);

View File

@ -59,28 +59,19 @@ class ReminderJob implements ShouldQueue
nrlog("Sending invoice reminders on ".now()->format('Y-m-d h:i:s'));
Invoice::query()
->where('invoices.is_deleted', 0)
->whereIn('invoices.status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->whereNull('invoices.deleted_at')
->where('invoices.balance', '>', 0)
->where('invoices.next_send_date', '<=', now()->toDateTimeString())
// ->whereHas('client', function ($query) {
// $query->where('is_deleted', 0)
// ->where('deleted_at', null);
// })
// ->whereHas('company', function ($query) {
// $query->where('is_disabled', 0);
// })
->leftJoin('clients', function ($join) {
$join->on('invoices.client_id', '=', 'clients.id')
->where('clients.is_deleted', 0)
->whereNull('clients.deleted_at');
})
->leftJoin('companies', function ($join) {
$join->on('invoices.company_id', '=', 'companies.id')
->where('companies.is_disabled', 0);
})
->with('invitations')->chunk(50, function ($invoices) {
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->whereNull('deleted_at')
->where('balance', '>', 0)
->where('next_send_date', '<=', now()->toDateTimeString())
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
})
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
->with('invitations')->chunk(200, function ($invoices) {
foreach ($invoices as $invoice) {
$this->sendReminderForInvoice($invoice);
}
@ -96,28 +87,19 @@ class ReminderJob implements ShouldQueue
nrlog("Sending invoice reminders on db {$db} ".now()->format('Y-m-d h:i:s'));
Invoice::query()
->where('invoices.is_deleted', 0)
->whereIn('invoices.status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->whereNull('invoices.deleted_at')
->where('invoices.balance', '>', 0)
->where('invoices.next_send_date', '<=', now()->toDateTimeString())
// ->whereHas('client', function ($query) {
// $query->where('is_deleted', 0)
// ->where('deleted_at', null);
// })
// ->whereHas('company', function ($query) {
// $query->where('is_disabled', 0);
// })
->leftJoin('clients', function ($join) {
$join->on('invoices.client_id', '=', 'clients.id')
->where('clients.is_deleted', 0)
->whereNull('clients.deleted_at');
})
->leftJoin('companies', function ($join) {
$join->on('invoices.company_id', '=', 'companies.id')
->where('companies.is_disabled', 0);
})
->with('invitations')->chunk(50, function ($invoices) {
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->whereNull('deleted_at')
->where('balance', '>', 0)
->where('next_send_date', '<=', now()->toDateTimeString())
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
})
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
->with('invitations')->chunk(200, function ($invoices) {
foreach ($invoices as $invoice) {
$this->sendReminderForInvoice($invoice);

View File

@ -134,18 +134,6 @@ class BaseModel extends Model
return $query;
}
public function scopeWithoutDeletedClients($query)
{
$query->leftJoin('clients', function ($join) use ($query){
$join->on("{$query->getQuery()->from}.client_id", '=', 'clients.id')
->where('clients.is_deleted', 0)
->whereNull('clients.deleted_at');
});
return $query;
}
/**
* @deprecated version
*/

View File

@ -107,7 +107,6 @@ use Laracasts\Presenter\PresentableTrait;
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Document> $documents
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Backup> $history
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\QuoteInvitation> $invitations
* @method static \Illuminate\Database\Eloquent\Builder|BaseModel company()
* @mixin \Eloquent
* @mixin \Illuminate\Database\Eloquent\Builder
*/

View File

@ -261,7 +261,7 @@ class TaskRepository extends BaseRepository
public function roundTimeLog(int $start_time, int $end_time): int
{
if($this->task_round_to_nearest == 1 || $end_time == 0) {
if(in_array($this->task_round_to_nearest, [0,1]) || $end_time == 0) {
return $end_time;
}

View File

@ -41,6 +41,7 @@ class ZugferdEDocument extends AbstractService {
*/
public function run(): Expense
{
/** @var \App\Models\User $user */
$user = auth()->user();
$this->document = ZugferdDocumentReader::readAndGuessFromContent($this->tempdocument);
$this->document->getDocumentInformation($documentno, $documenttypecode, $documentdate, $invoiceCurrency, $taxCurrency, $documentname, $documentlanguage, $effectiveSpecifiedPeriod);
@ -102,13 +103,13 @@ class ZugferdEDocument extends AbstractService {
if ($taxid != null) {
$vendor->vat_number = $taxid;
}
$vendor->currency_id = Currency::whereCode($invoiceCurrency)->first()->id;
$vendor->currency_id = Currency::query()->where('code', $invoiceCurrency)->first()->id;
$vendor->phone = $contact_phone;
$vendor->address1 = $address_1;
$vendor->address2 = $address_2;
$vendor->city = $city;
$vendor->postal_code = $postcode;
$vendor->country_id = Country::where('iso_3166_2', $country)->first()->id;
$vendor->country_id = Country::query()->where('iso_3166_2', $country)->first()->id;
$vendor->save();
$expense->vendor_id = $vendor->id;

View File

@ -11,6 +11,8 @@
namespace App\Utils;
use Illuminate\Http\File;
use Illuminate\Http\UploadedFile;
class TempFile
{
public static function path($url): string
@ -34,4 +36,61 @@ class TempFile
return $file_path;
}
public static function UploadedFileFromRaw(string $fileData, string|null $fileName = null, string|null $mimeType = null): UploadedFile
{
// Create temp file and get its absolute path
$tempFile = tmpfile();
$tempFilePath = stream_get_meta_data($tempFile)['uri'];
// Save file data in file
file_put_contents($tempFilePath, $fileData);
$tempFileObject = new File($tempFilePath);
$file = new UploadedFile(
$tempFileObject->getPathname(),
$fileName ?: $tempFileObject->getFilename(),
$mimeType ?: $tempFileObject->getMimeType(),
0,
true // Mark it as test, since the file isn't from real HTTP POST.
);
// Close this file after response is sent.
// Closing the file will cause to remove it from temp director!
app()->terminating(function () use ($tempFile) {
fclose($tempFile);
});
// return UploadedFile object
return $file;
}
/* create a tmp file from a raw string: https://gist.github.com/waska14/8b3bcebfad1f86f7fcd3b82927576e38*/
public static function UploadedFileFromUrl(string $url, string|null $fileName = null, string|null $mimeType = null): UploadedFile
{
// Create temp file and get its absolute path
$tempFile = tmpfile();
$tempFilePath = stream_get_meta_data($tempFile)['uri'];
// Save file data in file
file_put_contents($tempFilePath, file_get_contents($url));
$tempFileObject = new File($tempFilePath);
$file = new UploadedFile(
$tempFileObject->getPathname(),
$fileName ?: $tempFileObject->getFilename(),
$mimeType ?: $tempFileObject->getMimeType(),
0,
true // Mark it as test, since the file isn't from real HTTP POST.
);
// Close this file after response is sent.
// Closing the file will cause to remove it from temp director!
app()->terminating(function () use ($tempFile) {
fclose($tempFile);
});
// return UploadedFile object
return $file;
}
}

View File

@ -16,9 +16,9 @@ const RESOURCES = {"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"canvaskit/canvaskit.js": "c86fbd9e7b17accae76e5ad116583dc4",
"canvaskit/canvaskit.wasm": "3d2a2d663e8c5111ac61a46367f751ac",
"canvaskit/skwasm.wasm": "e42815763c5d05bba43f9d0337fa7d84",
"version.json": "1592dbbd49cf08963e29ab3a85640d96",
"version.json": "f789e711f61e122f41a7eda7522a1fba",
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"main.dart.js": "0512d8a34cb5c2d5d565a27c3666d9f9",
"main.dart.js": "bb6cfbe200a5c6a0d8857eaffd21b759",
"assets/NOTICES": "412b336cf9e33e70058d612857effae1",
"assets/AssetManifest.bin": "bf3be26e7055ad9a32f66b3a56138224",
"assets/assets/images/logo_light.png": "e5f46d5a78e226e7a9553d4ca6f69219",
@ -307,7 +307,7 @@ const RESOURCES = {"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"assets/FontManifest.json": "087fb858dc3cbfbf6baf6a30004922f1",
"assets/fonts/MaterialIcons-Regular.otf": "a57618538ab8b4c4081d4491870ac333",
"assets/AssetManifest.json": "759f9ef9973f7e26c2a51450b55bb9fa",
"/": "a53ace1edfc2ce12fc50cbbade610be3",
"/": "e847f898173e2b358dcf6efc58032f91",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40"};
// The application shell files that are downloaded before a service worker can

271459
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

269785
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{"app_name":"invoiceninja_flutter","version":"5.0.160","build_number":"160","package_name":"invoiceninja_flutter"}
{"app_name":"invoiceninja_flutter","version":"5.0.161","build_number":"161","package_name":"invoiceninja_flutter"}

View File

@ -11,7 +11,9 @@
namespace Tests\Feature;
use App\Models\Client;
use App\Models\CompanyGateway;
use App\Models\Invoice;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\MockAccountData;
use Tests\TestCase;
@ -44,6 +46,122 @@ class ClientModelTest extends TestCase
}
public function testNewWithoutAndDeletedClientFilters()
{
$this->invoice->amount = 10;
$this->invoice->balance = 10;
$this->invoice->status_id=2;
$this->invoice->date = now()->subDays(2);
$this->invoice->due_date = now()->addDays(2);
$this->invoice->save();
$cd = Client::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
]);
$cd2 = Client::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
]);
$invoice_count = Invoice::where('company_id', $this->company->id)->count();
$this->assertGreaterThan(0, $invoice_count);
$i = Invoice::factory()->create([
'client_id' => $cd->id,
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'status_id' => 2,
'amount' => 10,
'balance' => 10,
'date' => now()->subDays(2)->format('Y-m-d'),
'due_date' => now()->addDays(5)->format('Y-m-d'),
]);
$i2 = Invoice::factory()->create([
'client_id' => $cd2->id,
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'status_id' => 2,
'amount' => 10,
'balance' => 10,
'date' => now()->subDays(2)->format('Y-m-d'),
'due_date' => now()->addDays(5)->format('Y-m-d'),
]);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/invoices?status=active');
$response->assertStatus(200);
$arr = $response->json();
$this->assertEquals($invoice_count+2, count($arr['data']));
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/invoices?upcoming=true&status=active&include=client');
$response->assertStatus(200);
$arr = $response->json();
$this->assertEquals($invoice_count + 2, count($arr['data']));
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/invoices?upcoming=true&status=active&without_deleted_clients=true');
$response->assertStatus(200);
$arr = $response->json();
$this->assertEquals($invoice_count + 2, count($arr['data']));
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/invoices?upcoming=true&status=active&filter_deleted_clients=true');
$response->assertStatus(200);
$arr = $response->json();
$this->assertEquals($invoice_count + 2, count($arr['data']));
$cd2->is_deleted = true;
$cd2->save();
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/invoices?upcoming=true&status=active&without_deleted_clients=true');
$response->assertStatus(200);
$arr = $response->json();
$this->assertEquals($invoice_count + 1, count($arr['data']));
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/invoices?upcoming=true&status=active&filter_deleted_clients=true');
$response->assertStatus(200);
$arr = $response->json();
$this->assertEquals($invoice_count + 1, count($arr['data']));
}
public function testPaymentMethodsWithCreditsEnforced()
{
@ -51,6 +169,6 @@ class ClientModelTest extends TestCase
$this->assertGreaterThan(0, CompanyGateway::count());
$this->assertEquals(1, count($payment_methods));
$this->assertEquals(2, count($payment_methods));
}
}

View File

@ -189,6 +189,22 @@ class TaskApiTest extends TestCase
}
public function testTaskDivisionByZero()
{
$data = [
"rate" => 0,
"time_log" => '[[1719350900,1719352700,"",true]]',
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson("/api/v1/tasks", $data);
$response->assertStatus(200);
}
public function testRequestRuleParsing()
{