diff --git a/VERSION.txt b/VERSION.txt index 794e21dd8eba..6f3a0b275720 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.7.37 \ No newline at end of file +5.7.38 \ No newline at end of file diff --git a/app/Console/Commands/CheckData.php b/app/Console/Commands/CheckData.php index 89492f8fe2b8..e28c3dd1f7a2 100644 --- a/app/Console/Commands/CheckData.php +++ b/app/Console/Commands/CheckData.php @@ -904,26 +904,6 @@ class CheckData extends Command public function checkClientSettings() { 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->country_id = $client->company->settings->country_id; $client->save(); diff --git a/app/Export/CSV/BaseExport.php b/app/Export/CSV/BaseExport.php index 74474b98c789..be8660a24cd1 100644 --- a/app/Export/CSV/BaseExport.php +++ b/app/Export/CSV/BaseExport.php @@ -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 { @@ -864,6 +882,8 @@ class BaseExport protected function addDateRange($query) { + $query = $this->applyFilters($query); + $date_range = $this->input['date_range']; if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key']) > 1) { diff --git a/app/Export/CSV/InvoiceItemExport.php b/app/Export/CSV/InvoiceItemExport.php index 8ccc8a39ff55..cf1fddd26731 100644 --- a/app/Export/CSV/InvoiceItemExport.php +++ b/app/Export/CSV/InvoiceItemExport.php @@ -72,6 +72,8 @@ class InvoiceItemExport extends BaseExport $query = $this->addDateRange($query); + $query = $this->applyFilters($query); + return $query; } diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 22a1fdcb373c..d22a0d27cd87 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -487,13 +487,19 @@ class InvoiceController extends BaseController $user = auth()->user(); $action = $request->input('action'); - $ids = $request->input('ids'); if (Ninja::isHosted() && (stripos($action, 'email') !== false) && !$user->company()->account->account_sms_verified) { 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(); if (! $invoices) { @@ -645,9 +651,13 @@ class InvoiceController extends BaseController 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 */ switch ($action) { case 'auto_bill': + AutoBill::dispatch($invoice->id, $invoice->company->db); return $this->itemResponse($invoice); @@ -671,7 +681,6 @@ class InvoiceController extends BaseController break; case 'mark_paid': 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); } diff --git a/app/Http/Requests/Invoice/BulkInvoiceRequest.php b/app/Http/Requests/Invoice/BulkInvoiceRequest.php index c46fe6db8069..e7d947d6fc93 100644 --- a/app/Http/Requests/Invoice/BulkInvoiceRequest.php +++ b/app/Http/Requests/Invoice/BulkInvoiceRequest.php @@ -12,6 +12,7 @@ namespace App\Http\Requests\Invoice; use App\Http\Requests\Request; +use App\Models\Payment; class BulkInvoiceRequest extends Request { diff --git a/app/Http/Requests/Preview/PreviewInvoiceRequest.php b/app/Http/Requests/Preview/PreviewInvoiceRequest.php index 7bbdaebdbe29..5f094ebd4c9c 100644 --- a/app/Http/Requests/Preview/PreviewInvoiceRequest.php +++ b/app/Http/Requests/Preview/PreviewInvoiceRequest.php @@ -180,7 +180,7 @@ class PreviewInvoiceRequest extends Request $this->entity_plural = 'credits'; return $this; case 'recurring_invoice': - $this->entity_plural = 'invoices'; + $this->entity_plural = 'recurring_invoices'; return $this; default: $this->entity_plural = 'invoices'; diff --git a/app/Jobs/Util/VersionCheck.php b/app/Jobs/Util/VersionCheck.php index fe4cf701607b..76b177836eba 100644 --- a/app/Jobs/Util/VersionCheck.php +++ b/app/Jobs/Util/VersionCheck.php @@ -11,14 +11,20 @@ namespace App\Jobs\Util; -use App\Models\Account; -use App\Utils\Ninja; 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 App\Factory\ClientContactFactory; +use App\Factory\VendorContactFactory; +use Illuminate\Queue\SerializesModels; +use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; 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())) { $account->plan = 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(); + }); + + } } } diff --git a/app/Services/Invoice/EInvoice/FacturaEInvoice.php b/app/Services/Invoice/EInvoice/FacturaEInvoice.php index 24371ad2a2f5..935d3ab638d0 100644 --- a/app/Services/Invoice/EInvoice/FacturaEInvoice.php +++ b/app/Services/Invoice/EInvoice/FacturaEInvoice.php @@ -527,8 +527,8 @@ class FacturaEInvoice extends AbstractService "website" => substr($company->settings->website, 0, 50), // "contactPeople" => substr($company->owner()->present()->name(), 0, 40), "name" => $company->owner()->present()->firstName(), - // "firstSurname" => $company->owner()->present()->firstName(), - "lastSurname" => $company->owner()->present()->lastName(), + "firstSurname" => $company->owner()->present()->lastName(), + // "lastSurname" => $company->owner()->present()->lastName(), ]); $this->fac->setSeller($seller); diff --git a/app/Services/Pdf/PdfBuilder.php b/app/Services/Pdf/PdfBuilder.php index c83e0fed78c4..07f55b3c7e88 100644 --- a/app/Services/Pdf/PdfBuilder.php +++ b/app/Services/Pdf/PdfBuilder.php @@ -1102,7 +1102,7 @@ class PdfBuilder foreach ($variables as $variable) { if ($variable == '$total_taxes') { - $taxes = $this->service->config->entity->total_tax_map; + $taxes = $this->service->config->entity->calc()->getTotalTaxMap(); if (!$taxes) { continue; @@ -1115,7 +1115,7 @@ class PdfBuilder ]]; } } elseif ($variable == '$line_taxes') { - $taxes = $this->service->config->entity->tax_map; + $taxes = $this->service->config->entity->calc()->getTaxMap(); if (!$taxes) { continue; diff --git a/app/Services/Pdf/PdfConfiguration.php b/app/Services/Pdf/PdfConfiguration.php index fcca21481996..febe6d7d3ffe 100644 --- a/app/Services/Pdf/PdfConfiguration.php +++ b/app/Services/Pdf/PdfConfiguration.php @@ -181,7 +181,7 @@ class PdfConfiguration $this->entity_design_id = 'invoice_design_id'; $this->settings = $this->client->getMergedSettings(); $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) { $this->entity = $this->service->invitation->quote; $this->entity_string = 'quote'; @@ -191,7 +191,7 @@ class PdfConfiguration $this->entity_design_id = 'quote_design_id'; $this->settings = $this->client->getMergedSettings(); $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) { $this->entity = $this->service->invitation->credit; $this->entity_string = 'credit'; @@ -201,7 +201,7 @@ class PdfConfiguration $this->entity_design_id = 'credit_design_id'; $this->settings = $this->client->getMergedSettings(); $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) { $this->entity = $this->service->invitation->recurring_invoice; $this->entity_string = 'recurring_invoice'; @@ -211,7 +211,7 @@ class PdfConfiguration $this->entity_design_id = 'invoice_design_id'; $this->settings = $this->client->getMergedSettings(); $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) { $this->entity = $this->service->invitation->purchase_order; $this->entity_string = 'purchase_order'; @@ -223,7 +223,7 @@ class PdfConfiguration $this->settings = $this->vendor->company->settings; $this->settings_object = $this->vendor; $this->client = null; - $this->country = $this->vendor->country ?: $this->vendor->company->country(); + $this->country = $this->vendor->country ?? $this->vendor->company->country(); } else { throw new \Exception('Unable to resolve entity', 500); } diff --git a/app/Services/Template/TemplateAction.php b/app/Services/Template/TemplateAction.php index 2c753a754d1c..baec76e170c9 100644 --- a/app/Services/Template/TemplateAction.php +++ b/app/Services/Template/TemplateAction.php @@ -85,7 +85,7 @@ class TemplateAction implements ShouldQueue $template = Design::withTrashed()->find($this->decodePrimaryKey($this->template)); - $template_service = new TemplateService($template); + $template_service = new \App\Services\Template\TemplateService($template); match($this->entity) { Invoice::class => $resource->with('payments', 'client'), diff --git a/config/ninja.php b/config/ninja.php index 6d1c3534d3d3..8f153dc66225 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -15,8 +15,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.7.37'), - 'app_tag' => env('APP_TAG','5.7.37'), + 'app_version' => env('APP_VERSION','5.7.38'), + 'app_tag' => env('APP_TAG','5.7.38'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), diff --git a/tests/Feature/Export/ReportCsvGenerationTest.php b/tests/Feature/Export/ReportCsvGenerationTest.php index 2df5bf10648b..ae863ed95f0d 100644 --- a/tests/Feature/Export/ReportCsvGenerationTest.php +++ b/tests/Feature/Export/ReportCsvGenerationTest.php @@ -11,26 +11,28 @@ namespace Tests\Feature\Export; -use App\DataMapper\CompanySettings; -use App\Export\CSV\PaymentExport; -use App\Export\CSV\ProductExport; -use App\Export\CSV\TaskExport; -use App\Export\CSV\VendorExport; -use App\Factory\CompanyUserFactory; -use App\Models\Account; +use Tests\TestCase; +use App\Models\User; use App\Models\Client; -use App\Models\ClientContact; -use App\Models\Company; -use App\Models\CompanyToken; use App\Models\Credit; +use League\Csv\Reader; +use App\Models\Account; +use App\Models\Company; use App\Models\Expense; 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 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 League\Csv\Reader; -use Tests\TestCase; +use Illuminate\Routing\Middleware\ThrottleRequests; /** * @test @@ -52,13 +54,14 @@ class ReportCsvGenerationTest extends TestCase ); $this->withoutExceptionHandling(); + + Invoice::withTrashed()->cursor()->each(function ($i) { $i->forceDelete();}); $this->buildData(); if (config('ninja.testvars.travis') !== false) $this->markTestSkipped('Skip test no company gateways installed'); - } public $company; @@ -290,6 +293,155 @@ class ReportCsvGenerationTest extends TestCase 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() { @@ -1260,14 +1412,12 @@ class ReportCsvGenerationTest extends TestCase $this->assertEquals('GST', $this->getFirstValueByColumn($csv, 'Item Tax Name 1')); $this->assertEquals('10', $this->getFirstValueByColumn($csv, 'Item Tax Rate 1')); - $data = [ 'date_range' => 'all', 'report_keys' => $this->all_client_report_keys, 'send_email' => false, ]; - $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, @@ -1286,6 +1436,17 @@ class ReportCsvGenerationTest extends TestCase ])->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); }