Merge pull request #8921 from turbo124/v5-develop

v5.7.38
This commit is contained in:
David Bomba 2023-10-30 20:11:24 +11:00 committed by GitHub
commit b3fddb8afc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 289 additions and 58 deletions

View File

@ -1 +1 @@
5.7.37 5.7.38

View File

@ -904,26 +904,6 @@ class CheckData extends Command
public function checkClientSettings() public function checkClientSettings()
{ {
if ($this->option('fix') == 'true') { if ($this->option('fix') == 'true') {
// Client::query()->whereNull('settings->currency_id')->cursor()->each(function ($client){
// if(is_array($client->settings) && count($client->settings) == 0)
// {
// $settings = ClientSettings::defaults();
// $settings->currency_id = $client->company->settings->currency_id;
// }
// else {
// $settings = $client->settings;
// $settings->currency_id = $client->company->settings->currency_id;
// }
// $client->settings = $settings;
// $client->save();
// $this->logMessage("Fixing currency for # {$client->id}");
// });
Client::query()->whereNull('country_id')->cursor()->each(function ($client) { Client::query()->whereNull('country_id')->cursor()->each(function ($client) {
$client->country_id = $client->company->settings->country_id; $client->country_id = $client->company->settings->country_id;
$client->save(); $client->save();

View File

@ -807,6 +807,24 @@ class BaseExport
} }
public function applyFilters(Builder $query): Builder
{
if(isset($this->input['product_key'])) {
$products = explode(",", $this->input['product_key']);
$query->where(function ($q) use ($products) {
foreach($products as $product) {
$q->orWhereJsonContains('line_items', ['product_key' => $product]);
}
});
}
return $query;
}
protected function addInvoiceStatusFilter($query, $status): Builder protected function addInvoiceStatusFilter($query, $status): Builder
{ {
@ -864,6 +882,8 @@ class BaseExport
protected function addDateRange($query) protected function addDateRange($query)
{ {
$query = $this->applyFilters($query);
$date_range = $this->input['date_range']; $date_range = $this->input['date_range'];
if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key']) > 1) { if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key']) > 1) {

View File

@ -72,6 +72,8 @@ class InvoiceItemExport extends BaseExport
$query = $this->addDateRange($query); $query = $this->addDateRange($query);
$query = $this->applyFilters($query);
return $query; return $query;
} }

View File

@ -487,13 +487,19 @@ class InvoiceController extends BaseController
$user = auth()->user(); $user = auth()->user();
$action = $request->input('action'); $action = $request->input('action');
$ids = $request->input('ids'); $ids = $request->input('ids');
if (Ninja::isHosted() && (stripos($action, 'email') !== false) && !$user->company()->account->account_sms_verified) { if (Ninja::isHosted() && (stripos($action, 'email') !== false) && !$user->company()->account->account_sms_verified) {
return response(['message' => 'Please verify your account to send emails.'], 400); return response(['message' => 'Please verify your account to send emails.'], 400);
} }
/**@var \App\Models\User $user */
$user = auth()->user();
if(in_array($request->action, ['auto_bill','mark_paid']) && $user->cannot('create', \App\Models\Payment::class)) {
return response(['message' => ctrans('texts.not_authorized'), 'errors' => ['ids' => [ctrans('texts.not_authorized')]]], 422);
}
$invoices = Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get(); $invoices = Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get();
if (! $invoices) { if (! $invoices) {
@ -645,9 +651,13 @@ class InvoiceController extends BaseController
private function performAction(Invoice $invoice, $action, $bulk = false) private function performAction(Invoice $invoice, $action, $bulk = false)
{ {
/** @var \App\Models\User $user */
$user = auth()->user();
/*If we are using bulk actions, we don't want to return anything */ /*If we are using bulk actions, we don't want to return anything */
switch ($action) { switch ($action) {
case 'auto_bill': case 'auto_bill':
AutoBill::dispatch($invoice->id, $invoice->company->db); AutoBill::dispatch($invoice->id, $invoice->company->db);
return $this->itemResponse($invoice); return $this->itemResponse($invoice);
@ -671,7 +681,6 @@ class InvoiceController extends BaseController
break; break;
case 'mark_paid': case 'mark_paid':
if ($invoice->status_id == Invoice::STATUS_PAID || $invoice->is_deleted === true) { if ($invoice->status_id == Invoice::STATUS_PAID || $invoice->is_deleted === true) {
// if ($invoice->balance < 0 || $invoice->status_id == Invoice::STATUS_PAID || $invoice->is_deleted === true) {
return $this->errorResponse(['message' => ctrans('texts.invoice_cannot_be_marked_paid')], 400); return $this->errorResponse(['message' => ctrans('texts.invoice_cannot_be_marked_paid')], 400);
} }

View File

@ -12,6 +12,7 @@
namespace App\Http\Requests\Invoice; namespace App\Http\Requests\Invoice;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Models\Payment;
class BulkInvoiceRequest extends Request class BulkInvoiceRequest extends Request
{ {

View File

@ -180,7 +180,7 @@ class PreviewInvoiceRequest extends Request
$this->entity_plural = 'credits'; $this->entity_plural = 'credits';
return $this; return $this;
case 'recurring_invoice': case 'recurring_invoice':
$this->entity_plural = 'invoices'; $this->entity_plural = 'recurring_invoices';
return $this; return $this;
default: default:
$this->entity_plural = 'invoices'; $this->entity_plural = 'invoices';

View File

@ -11,14 +11,20 @@
namespace App\Jobs\Util; namespace App\Jobs\Util;
use App\Models\Account;
use App\Utils\Ninja;
use Carbon\Carbon; use Carbon\Carbon;
use App\Utils\Ninja;
use App\Models\Client;
use App\Models\Vendor;
use App\Models\Account;
use Illuminate\Support\Str;
use App\Models\ClientContact;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use App\Factory\ClientContactFactory;
use App\Factory\VendorContactFactory;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class VersionCheck implements ShouldQueue class VersionCheck implements ShouldQueue
{ {
@ -54,8 +60,60 @@ class VersionCheck implements ShouldQueue
if ($account->plan == 'white_label' && $account->plan_expires && Carbon::parse($account->plan_expires)->lt(now())) { if ($account->plan == 'white_label' && $account->plan_expires && Carbon::parse($account->plan_expires)->lt(now())) {
$account->plan = null; $account->plan = null;
$account->plan_expires = null; $account->plan_expires = null;
$account->save(); $account->saveQuietly();
} }
Client::query()->whereNull('country_id')->cursor()->each(function ($client) {
$client->country_id = $client->company->settings->country_id;
$client->saveQuietly();
});
Vendor::query()->whereNull('currency_id')->orWhere('currency_id', '')->cursor()->each(function ($vendor) {
$vendor->currency_id = $vendor->company->settings->currency_id;
$vendor->saveQuietly();
});
ClientContact::whereNull('email')
->where('send_email', true)
->cursor()
->each(function ($c) {
$c->send_email = false;
$c->saveQuietly();
});
ClientContact::query()
->whereNull('contact_key')
->update([
'contact_key' => Str::random(config('ninja.key_length')),
]);
Client::doesntHave('contacts')
->cursor()
->each(function ($client){
$new_contact = ClientContactFactory::create($client->company_id, $client->user_id);
$new_contact->client_id = $client->id;
$new_contact->contact_key = Str::random(40);
$new_contact->is_primary = true;
$new_contact->save();
});
Vendor::doesntHave('contacts')
->cursor()
->each(function ($vendor){
$new_contact = VendorContactFactory::create($vendor->company_id, $vendor->user_id);
$new_contact->vendor_id = $vendor->id;
$new_contact->contact_key = Str::random(40);
$new_contact->is_primary = true;
$new_contact->save();
});
} }
} }
} }

View File

@ -527,8 +527,8 @@ class FacturaEInvoice extends AbstractService
"website" => substr($company->settings->website, 0, 50), "website" => substr($company->settings->website, 0, 50),
// "contactPeople" => substr($company->owner()->present()->name(), 0, 40), // "contactPeople" => substr($company->owner()->present()->name(), 0, 40),
"name" => $company->owner()->present()->firstName(), "name" => $company->owner()->present()->firstName(),
// "firstSurname" => $company->owner()->present()->firstName(), "firstSurname" => $company->owner()->present()->lastName(),
"lastSurname" => $company->owner()->present()->lastName(), // "lastSurname" => $company->owner()->present()->lastName(),
]); ]);
$this->fac->setSeller($seller); $this->fac->setSeller($seller);

View File

@ -1102,7 +1102,7 @@ class PdfBuilder
foreach ($variables as $variable) { foreach ($variables as $variable) {
if ($variable == '$total_taxes') { if ($variable == '$total_taxes') {
$taxes = $this->service->config->entity->total_tax_map; $taxes = $this->service->config->entity->calc()->getTotalTaxMap();
if (!$taxes) { if (!$taxes) {
continue; continue;
@ -1115,7 +1115,7 @@ class PdfBuilder
]]; ]];
} }
} elseif ($variable == '$line_taxes') { } elseif ($variable == '$line_taxes') {
$taxes = $this->service->config->entity->tax_map; $taxes = $this->service->config->entity->calc()->getTaxMap();
if (!$taxes) { if (!$taxes) {
continue; continue;

View File

@ -181,7 +181,7 @@ class PdfConfiguration
$this->entity_design_id = 'invoice_design_id'; $this->entity_design_id = 'invoice_design_id';
$this->settings = $this->client->getMergedSettings(); $this->settings = $this->client->getMergedSettings();
$this->settings_object = $this->client; $this->settings_object = $this->client;
$this->country = $this->client->country; $this->country = $this->client->country ?? $this->client->company->country();
} elseif ($this->service->invitation instanceof QuoteInvitation) { } elseif ($this->service->invitation instanceof QuoteInvitation) {
$this->entity = $this->service->invitation->quote; $this->entity = $this->service->invitation->quote;
$this->entity_string = 'quote'; $this->entity_string = 'quote';
@ -191,7 +191,7 @@ class PdfConfiguration
$this->entity_design_id = 'quote_design_id'; $this->entity_design_id = 'quote_design_id';
$this->settings = $this->client->getMergedSettings(); $this->settings = $this->client->getMergedSettings();
$this->settings_object = $this->client; $this->settings_object = $this->client;
$this->country = $this->client->country; $this->country = $this->client->country ?? $this->client->company->country();
} elseif ($this->service->invitation instanceof CreditInvitation) { } elseif ($this->service->invitation instanceof CreditInvitation) {
$this->entity = $this->service->invitation->credit; $this->entity = $this->service->invitation->credit;
$this->entity_string = 'credit'; $this->entity_string = 'credit';
@ -201,7 +201,7 @@ class PdfConfiguration
$this->entity_design_id = 'credit_design_id'; $this->entity_design_id = 'credit_design_id';
$this->settings = $this->client->getMergedSettings(); $this->settings = $this->client->getMergedSettings();
$this->settings_object = $this->client; $this->settings_object = $this->client;
$this->country = $this->client->country; $this->country = $this->client->country ?? $this->client->company->country();
} elseif ($this->service->invitation instanceof RecurringInvoiceInvitation) { } elseif ($this->service->invitation instanceof RecurringInvoiceInvitation) {
$this->entity = $this->service->invitation->recurring_invoice; $this->entity = $this->service->invitation->recurring_invoice;
$this->entity_string = 'recurring_invoice'; $this->entity_string = 'recurring_invoice';
@ -211,7 +211,7 @@ class PdfConfiguration
$this->entity_design_id = 'invoice_design_id'; $this->entity_design_id = 'invoice_design_id';
$this->settings = $this->client->getMergedSettings(); $this->settings = $this->client->getMergedSettings();
$this->settings_object = $this->client; $this->settings_object = $this->client;
$this->country = $this->client->country; $this->country = $this->client->country ?? $this->client->company->country();
} elseif ($this->service->invitation instanceof PurchaseOrderInvitation) { } elseif ($this->service->invitation instanceof PurchaseOrderInvitation) {
$this->entity = $this->service->invitation->purchase_order; $this->entity = $this->service->invitation->purchase_order;
$this->entity_string = 'purchase_order'; $this->entity_string = 'purchase_order';
@ -223,7 +223,7 @@ class PdfConfiguration
$this->settings = $this->vendor->company->settings; $this->settings = $this->vendor->company->settings;
$this->settings_object = $this->vendor; $this->settings_object = $this->vendor;
$this->client = null; $this->client = null;
$this->country = $this->vendor->country ?: $this->vendor->company->country(); $this->country = $this->vendor->country ?? $this->vendor->company->country();
} else { } else {
throw new \Exception('Unable to resolve entity', 500); throw new \Exception('Unable to resolve entity', 500);
} }

View File

@ -85,7 +85,7 @@ class TemplateAction implements ShouldQueue
$template = Design::withTrashed()->find($this->decodePrimaryKey($this->template)); $template = Design::withTrashed()->find($this->decodePrimaryKey($this->template));
$template_service = new TemplateService($template); $template_service = new \App\Services\Template\TemplateService($template);
match($this->entity) { match($this->entity) {
Invoice::class => $resource->with('payments', 'client'), Invoice::class => $resource->with('payments', 'client'),

View File

@ -15,8 +15,8 @@ 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', 'invoicing.co'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => env('APP_VERSION','5.7.37'), 'app_version' => env('APP_VERSION','5.7.38'),
'app_tag' => env('APP_TAG','5.7.37'), 'app_tag' => env('APP_TAG','5.7.38'),
'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

@ -11,26 +11,28 @@
namespace Tests\Feature\Export; namespace Tests\Feature\Export;
use App\DataMapper\CompanySettings; use Tests\TestCase;
use App\Export\CSV\PaymentExport; use App\Models\User;
use App\Export\CSV\ProductExport;
use App\Export\CSV\TaskExport;
use App\Export\CSV\VendorExport;
use App\Factory\CompanyUserFactory;
use App\Models\Account;
use App\Models\Client; use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\CompanyToken;
use App\Models\Credit; use App\Models\Credit;
use League\Csv\Reader;
use App\Models\Account;
use App\Models\Company;
use App\Models\Expense; use App\Models\Expense;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\User; use App\Models\CompanyToken;
use App\Models\ClientContact;
use App\Export\CSV\TaskExport;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Routing\Middleware\ThrottleRequests; use App\Export\CSV\VendorExport;
use App\Export\CSV\PaymentExport;
use App\Export\CSV\ProductExport;
use App\DataMapper\CompanySettings;
use App\DataMapper\InvoiceItem;
use App\Factory\CompanyUserFactory;
use App\Factory\InvoiceItemFactory;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use League\Csv\Reader; use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\TestCase;
/** /**
* @test * @test
@ -52,13 +54,14 @@ class ReportCsvGenerationTest extends TestCase
); );
$this->withoutExceptionHandling(); $this->withoutExceptionHandling();
Invoice::withTrashed()->cursor()->each(function ($i) { $i->forceDelete();});
$this->buildData(); $this->buildData();
if (config('ninja.testvars.travis') !== false) if (config('ninja.testvars.travis') !== false)
$this->markTestSkipped('Skip test no company gateways installed'); $this->markTestSkipped('Skip test no company gateways installed');
} }
public $company; public $company;
@ -290,6 +293,155 @@ class ReportCsvGenerationTest extends TestCase
return $response; return $response;
} }
public function testProductJsonFiltering()
{
$query = Invoice::query();
$products = explode(",", "clown,joker,batman,bob the builder");
foreach($products as $product) {
$query->where(function ($q) use ($product) {
$q->orWhereJsonContains('line_items', ['product_key' => $product]);
});
}
$this->assertEquals(0, $query->count());
$item = InvoiceItemFactory::create();
$item->product_key = 'haloumi';
$line_items = [];
$line_items[] = $item;
Invoice::factory()->create(
[
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'line_items' => $line_items
]
);
$query->where(function ($q) use ($products) {
foreach($products as $product) {
$q->orWhereJsonContains('line_items', ['product_key' => $product]);
}
});
$this->assertEquals(0, $query->count());
$item = InvoiceItemFactory::create();
$item->product_key = 'batman';
$line_items = [];
$line_items[] = $item;
$item = InvoiceItemFactory::create();
$item->product_key = 'bob the builder';
$line_items[] = $item;
Invoice::factory()->create(
[
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'line_items' => $line_items
]
);
$query = Invoice::query();
$query->where(function ($q) use($products){
foreach($products as $product) {
$q->orWhereJsonContains('line_items', ['product_key' => $product]);
}
});
$this->assertEquals(1, $query->count());
$query = Invoice::query();
$query->where(function ($q){
$q->orWhereJsonContains('line_items', ['product_key' => 'bob the builder']);
});
$this->assertEquals(1, $query->count());
Invoice::withTrashed()->cursor()->each(function ($i) { $i->forceDelete();});
}
public function testProductKeyFilterQueries()
{
$item = InvoiceItemFactory::create();
$item->product_key = 'haloumi';
$line_items = [];
$line_items[] = $item;
$q = Invoice::whereJsonContains('line_items', ['product_key' => 'haloumi']);
$this->assertEquals(0, $q->count());
Invoice::factory()->create(
[
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'line_items' => $line_items
]
);
$this->assertEquals(1, $q->count());
$q->forceDelete();
Invoice::factory()->create(
[
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'line_items' => $line_items
]
);
$item = InvoiceItemFactory::create();
$item->product_key = 'bob the builder';
$line_items = [];
$line_items[] = $item;
$q = Invoice::whereJsonContains('line_items', ['product_key' => 'bob the builder']);
$this->assertEquals(0, $q->count());
Invoice::factory()->create(
[
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'client_id' => $this->client->id,
'line_items' => $line_items
]
);
$this->assertEquals(1, $q->count());
$q = Invoice::whereJsonContains('line_items', ['product_key' => 'Bob the builder']);
$this->assertEquals(0, $q->count());
$q = Invoice::whereJsonContains('line_items', ['product_key' => 'bob']);
$this->assertEquals(0, $q->count());
$q->forceDelete();
Invoice::withTrashed()->cursor()->each(function ($i){ $i->forceDelete();});
}
public function testVendorCsvGeneration() public function testVendorCsvGeneration()
{ {
@ -1260,14 +1412,12 @@ class ReportCsvGenerationTest extends TestCase
$this->assertEquals('GST', $this->getFirstValueByColumn($csv, 'Item Tax Name 1')); $this->assertEquals('GST', $this->getFirstValueByColumn($csv, 'Item Tax Name 1'));
$this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Item Tax Rate 1')); $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Item Tax Rate 1'));
$data = [ $data = [
'date_range' => 'all', 'date_range' => 'all',
'report_keys' => $this->all_client_report_keys, 'report_keys' => $this->all_client_report_keys,
'send_email' => false, 'send_email' => false,
]; ];
$response = $this->withHeaders([ $response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'), 'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token, 'X-API-TOKEN' => $this->token,
@ -1286,6 +1436,17 @@ class ReportCsvGenerationTest extends TestCase
])->post('/api/v1/reports/invoice_items', $data)->assertStatus(200); ])->post('/api/v1/reports/invoice_items', $data)->assertStatus(200);
$data = [
'date_range' => 'all',
'report_keys' => $this->all_payment_report_keys,
'send_email' => false,
'product_key' => 'haloumi,cheese',
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->postJson('/api/v1/reports/invoice_items', $data)->assertStatus(200);
} }