diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 474df6d78b0c..0d2b1edae7da 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -15,9 +15,10 @@ https://invoiceninja.github.io/docs/self-host-troubleshooting/ --> - Environment: ## Checklist -- Can you replicate the issue on our v5 demo site https://demo.invoiceninja.com pr https://react.invoicing.co/demo? +- Can you replicate the issue on our v5 demo site https://demo.invoiceninja.com or https://react.invoicing.co/demo? - Have you searched existing issues? - Have you reported this to Slack/forum before posting? +- Have you inspected the logs in storage/logs/laravel.log for any errors? ## Describe the bug diff --git a/VERSION.txt b/VERSION.txt index cf3ecc59ddfb..bce494672536 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.5.63 \ No newline at end of file +5.5.64 \ No newline at end of file diff --git a/app/Console/Commands/CheckData.php b/app/Console/Commands/CheckData.php index 1b3857ff72a9..038d78e8374c 100644 --- a/app/Console/Commands/CheckData.php +++ b/app/Console/Commands/CheckData.php @@ -126,7 +126,8 @@ class CheckData extends Command $this->checkVendorSettings(); $this->checkClientSettings(); $this->checkCompanyTokens(); - + $this->checkUserState(); + if(Ninja::isHosted()){ $this->checkAccountStatuses(); $this->checkNinjaPortalUrls(); @@ -414,6 +415,16 @@ class CheckData extends Command } } + private function checkUserState() + { + User::withTrashed() + ->where('deleted_at', '0000-00-00 00:00:00.000000') + ->cursor() + ->each(function ($user){ + $user->restore(); + }); + } + private function checkEntityInvitations() { diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 0b3d132ebb81..6b7af205900d 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -229,7 +229,7 @@ class CompanySettings extends BaseSettings public $require_quote_signature = false; //@TODO ben to confirm //email settings - public $email_sending_method = 'default'; //enum 'default','gmail','office365' //@implemented + public $email_sending_method = 'default'; //enum 'default','gmail','office365' 'client_postmark', 'client_mailgun'//@implemented public $gmail_sending_user_id = '0'; //@implemented @@ -453,9 +453,15 @@ class CompanySettings extends BaseSettings public $show_email_footer = true; - public $company_logo_size = '65%'; + public $company_logo_size = ''; + + public $show_paid_stamp = false; + + public $show_shipping_address = false; public static $casts = [ + 'show_paid_stamp' => 'bool', + 'show_shipping_address' => 'bool', 'company_logo_size' => 'string', 'show_email_footer' => 'bool', 'email_alignment' => 'string', diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 75126c91a6b2..b3be6c2d793a 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -267,7 +267,7 @@ class BaseController extends Controller $updated_at = request()->has('updated_at') ? request()->input('updated_at') : 0; - if ($user->getCompany()->is_large && $updated_at == 0) { + if ($user->getCompany()->is_large && $updated_at == 0 && $this->complexPermissionsUser()) { $updated_at = time(); } @@ -613,11 +613,27 @@ class BaseController extends Controller return $this->response($this->manager->createData($resource)->toArray()); } + /** + * In case a user is not an admin and is + * able to access multiple companies, then we + * need to pass back the mini load only + * + * @return bool + */ + private function complexPermissionsUser(): bool + { + //if the user is attached to more than one company AND they are not an admin across all companies + if(auth()->user()->company_users()->count() > 1 && (auth()->user()->company_users()->where('is_admin',1)->count() != auth()->user()->company_users()->count())) + return true; + + return false; + } + protected function timeConstrainedResponse($query) { $user = auth()->user(); - if ($user->getCompany()->is_large) { + if ($user->getCompany()->is_large || $this->complexPermissionsUser()) { $this->manager->parseIncludes($this->mini_load); return $this->miniLoadResponse($query); diff --git a/app/Http/Controllers/ClientPortal/NinjaPlanController.php b/app/Http/Controllers/ClientPortal/NinjaPlanController.php index 4c392042c95c..112ab77a1ab0 100644 --- a/app/Http/Controllers/ClientPortal/NinjaPlanController.php +++ b/app/Http/Controllers/ClientPortal/NinjaPlanController.php @@ -218,7 +218,7 @@ class NinjaPlanController extends Controller if ($account) { //offer the option to have a free trial - if (!$account->is_trial) { + if (!$account->plan && !$account->is_trial) { return $this->trial(); } diff --git a/app/Http/Controllers/PreviewController.php b/app/Http/Controllers/PreviewController.php index 6b6468945cf8..241ae707bfae 100644 --- a/app/Http/Controllers/PreviewController.php +++ b/app/Http/Controllers/PreviewController.php @@ -16,12 +16,14 @@ use App\Factory\CreditFactory; use App\Factory\InvoiceFactory; use App\Factory\QuoteFactory; use App\Factory\RecurringInvoiceFactory; +use App\Http\Requests\Preview\DesignPreviewRequest; use App\Http\Requests\Preview\PreviewInvoiceRequest; use App\Jobs\Util\PreviewPdf; use App\Libraries\MultiDB; use App\Models\Client; use App\Models\ClientContact; use App\Models\Credit; +use App\Models\GroupSetting; use App\Models\Invoice; use App\Models\InvoiceInvitation; use App\Models\Quote; @@ -30,9 +32,9 @@ use App\Repositories\CreditRepository; use App\Repositories\InvoiceRepository; use App\Repositories\QuoteRepository; use App\Repositories\RecurringInvoiceRepository; -use App\Services\PdfMaker\Design; use App\Services\PdfMaker\Design as PdfDesignModel; use App\Services\PdfMaker\Design as PdfMakerDesign; +use App\Services\PdfMaker\Design; use App\Services\PdfMaker\PdfMaker; use App\Utils\HostedPDF\NinjaPdf; use App\Utils\HtmlEngine; @@ -173,8 +175,178 @@ class PreviewController extends BaseController return $this->blankEntity(); } + public function design(DesignPreviewRequest $request) + { + if(Ninja::isHosted() && $request->getHost() != 'preview.invoicing.co') + return response()->json(['message' => 'This server cannot handle this request.'], 400); + + $company = auth()->user()->company(); + + MultiDB::setDb($company->db); + + if ($request->input('entity') == 'quote') { + $repo = new QuoteRepository(); + $entity_obj = QuoteFactory::create($company->id, auth()->user()->id); + $class = Quote::class; + } elseif ($request->input('entity') == 'credit') { + $repo = new CreditRepository(); + $entity_obj = CreditFactory::create($company->id, auth()->user()->id); + $class = Credit::class; + } elseif ($request->input('entity') == 'recurring_invoice') { + $repo = new RecurringInvoiceRepository(); + $entity_obj = RecurringInvoiceFactory::create($company->id, auth()->user()->id); + $class = RecurringInvoice::class; + } else { //assume it is either an invoice or a null object + $repo = new InvoiceRepository(); + $entity_obj = InvoiceFactory::create($company->id, auth()->user()->id); + $class = Invoice::class; + } + + try { + DB::connection(config('database.default'))->beginTransaction(); + + if ($request->has('entity_id')) { + $entity_obj = $class::on(config('database.default')) + ->with('client.company') + ->where('id', $this->decodePrimaryKey($request->input('entity_id'))) + ->where('company_id', $company->id) + ->withTrashed() + ->first(); + } + + if($request->has('client_id')) { + $client = Client::withTrashed()->find($this->decodePrimaryKey($request->client_id)); + if($request->settings_type == 'client'){ + $client->settings = $request->settings; + $client->save(); + } + + } + + if($request->has('group_id')) { + $group = GroupSetting::withTrashed()->find($this->decodePrimaryKey($request->group_id)); + if($request->settings_type == 'group'){ + $group->settings = $request->settings; + $group->save(); + } + + } + + if($request->settings_type == 'company'){ + $company->settings = $request->settings; + $company->save(); + } + + if($request->has('footer') && !$request->filled('footer') && $request->input('entity') == 'recurring_invoice') + $request->merge(['footer' => $company->settings->invoice_footer]); + + if($request->has('terms') && !$request->filled('terms') && $request->input('entity') == 'recurring_invoice') + $request->merge(['terms' => $company->settings->invoice_terms]); + + $entity_obj = $repo->save($request->all(), $entity_obj); + + if (! $request->has('entity_id')) { + $entity_obj->service()->fillDefaults()->save(); + } + + App::forgetInstance('translator'); + $t = app('translator'); + App::setLocale($entity_obj->client->locale()); + $t->replace(Ninja::transformTranslations($entity_obj->client->getMergedSettings())); + + $html = new HtmlEngine($entity_obj->invitations()->first()); + + $design = \App\Models\Design::find($entity_obj->design_id); + + /* Catch all in case migration doesn't pass back a valid design */ + if (! $design) { + $design = \App\Models\Design::find(2); + } + + if ($design->is_custom) { + $options = [ + 'custom_partials' => json_decode(json_encode($design->design), true), + ]; + $template = new PdfMakerDesign(PdfDesignModel::CUSTOM, $options); + } else { + $template = new PdfMakerDesign(strtolower($design->name)); + } + + $variables = $html->generateLabelsAndValues(); + + $state = [ + 'template' => $template->elements([ + 'client' => $entity_obj->client, + 'entity' => $entity_obj, + 'pdf_variables' => (array) $entity_obj->company->settings->pdf_variables, + '$product' => $design->design->product, + 'variables' => $variables, + ]), + 'variables' => $variables, + 'options' => [ + 'all_pages_header' => $entity_obj->client->getSetting('all_pages_header'), + 'all_pages_footer' => $entity_obj->client->getSetting('all_pages_footer'), + ], + 'process_markdown' => $entity_obj->client->company->markdown_enabled, + ]; + + $maker = new PdfMaker($state); + + $maker + ->design($template) + ->build(); + + DB::connection(config('database.default'))->rollBack(); + + if (request()->query('html') == 'true') { + nlog($maker->getCompiledHTML()); + return $maker->getCompiledHTML(); + } + + } + catch(\Exception $e){ + nlog($e->getMessage()); + DB::connection(config('database.default'))->rollBack(); + + return; + } + + //if phantom js...... inject here.. + if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') { + return (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true)); + } + + if(config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja'){ + $pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true)); + + $numbered_pdf = $this->pageNumbering($pdf, auth()->user()->company()); + + + $numbered_pdf = $this->pageNumbering($pdf, auth()->user()->company()); + + if ($numbered_pdf) { + $pdf = $numbered_pdf; + } + + return $pdf; + } + + $file_path = (new PreviewPdf($maker->getCompiledHTML(true), $company))->handle(); + + $response = Response::make($file_path, 200); + $response->header('Content-Type', 'application/pdf'); + + return $response; + + + + } + public function live(PreviewInvoiceRequest $request) { + if(Ninja::isHosted() && $request->getHost() != 'preview.invoicing.co') + return response()->json(['message' => 'This server cannot handle this request.'], 400); + $company = auth()->user()->company(); MultiDB::setDb($company->db); diff --git a/app/Http/Controllers/Reports/ProfitAndLossController.php b/app/Http/Controllers/Reports/ProfitAndLossController.php index 327fdfff6649..8ee3f9a7d2dc 100644 --- a/app/Http/Controllers/Reports/ProfitAndLossController.php +++ b/app/Http/Controllers/Reports/ProfitAndLossController.php @@ -72,9 +72,7 @@ class ProfitAndLossController extends BaseController // expect a list of visible fields, or use the default $pnl = new ProfitLoss(auth()->user()->company(), $request->all()); - $pnl->build(); - - $csv = $pnl->getCsv(); + $csv = $pnl->run(); $headers = [ 'Content-Disposition' => 'attachment', diff --git a/app/Http/Requests/Preview/DesignPreviewRequest.php b/app/Http/Requests/Preview/DesignPreviewRequest.php new file mode 100644 index 000000000000..9930e87f877b --- /dev/null +++ b/app/Http/Requests/Preview/DesignPreviewRequest.php @@ -0,0 +1,71 @@ +user()->can('create', Invoice::class) || + auth()->user()->can('create', Quote::class) || + auth()->user()->can('create', RecurringInvoice::class) || + auth()->user()->can('create', Credit::class) || + auth()->user()->can('create', PurchaseOrder::class); + } + + public function rules() + { + $rules = [ + 'entity' => 'bail|sometimes|string', + 'entity_id' => 'bail|sometimes|string', + 'settings_type' => 'bail|required|in:company,group,client', + 'settings' => 'sometimes', + 'group_id' => 'sometimes', + 'client_id' => 'sometimes', + ]; + + return $rules; + } + + public function prepareForValidation() + { + $input = $this->all(); + + $input = $this->decodePrimaryKeys($input); + + $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; + $input['amount'] = 0; + $input['balance'] = 0; + $input['number'] = ctrans('texts.live_preview').' #'.rand(0, 1000); + + $this->replace($input); + } +} diff --git a/app/Http/Requests/Webhook/StoreWebhookRequest.php b/app/Http/Requests/Webhook/StoreWebhookRequest.php index 22978933693b..900cc14ed540 100644 --- a/app/Http/Requests/Webhook/StoreWebhookRequest.php +++ b/app/Http/Requests/Webhook/StoreWebhookRequest.php @@ -28,8 +28,10 @@ class StoreWebhookRequest extends Request public function rules() { return [ - 'target_url' => 'required|url', - 'event_id' => 'required', + 'target_url' => 'bail|required|url', + 'event_id' => 'bail|required', + 'headers' => 'bail|sometimes|json', + 'rest_method' => 'required|in:post,put' ]; } @@ -37,6 +39,9 @@ class StoreWebhookRequest extends Request { $input = $this->all(); + if(isset($input['headers']) && count($input['headers']) == 0) + $input['headers'] = null; + $this->replace($input); } } diff --git a/app/Http/Requests/Webhook/UpdateWebhookRequest.php b/app/Http/Requests/Webhook/UpdateWebhookRequest.php index bc255153f889..7d809acf1ba0 100644 --- a/app/Http/Requests/Webhook/UpdateWebhookRequest.php +++ b/app/Http/Requests/Webhook/UpdateWebhookRequest.php @@ -27,13 +27,16 @@ class UpdateWebhookRequest extends Request */ public function authorize() : bool { - return auth()->user()->isAdmin(); + return auth()->user()->can('edit', $this->webhook); } public function rules() { return [ - 'target_url' => 'url', + 'target_url' => 'bail|required|url', + 'event_id' => 'bail|required', + 'rest_method' => 'required|in:post,put', + 'headers' => 'bail|sometimes|json', ]; } @@ -41,6 +44,9 @@ class UpdateWebhookRequest extends Request { $input = $this->all(); + if(isset($input['headers']) && count($input['headers']) == 0) + $input['headers'] = null; + $this->replace($input); } } diff --git a/app/Jobs/Company/CompanyImport.php b/app/Jobs/Company/CompanyImport.php index 35fa2167199e..b9c9ed770f75 100644 --- a/app/Jobs/Company/CompanyImport.php +++ b/app/Jobs/Company/CompanyImport.php @@ -508,7 +508,7 @@ class CompanyImport implements ShouldQueue if(Ninja::isHosted()) { - $this->company->portal_mode = 'sub_domain'; + $this->company->portal_mode = 'subdomain'; $this->company->portal_domain = ''; } diff --git a/app/Policies/WebhookPolicy.php b/app/Policies/WebhookPolicy.php index b6c1f778eded..47d830362519 100644 --- a/app/Policies/WebhookPolicy.php +++ b/app/Policies/WebhookPolicy.php @@ -11,9 +11,22 @@ namespace App\Policies; +use App\Models\User; + /** * Class WebhookPolicy. */ class WebhookPolicy extends EntityPolicy { + /** + * Checks if the user has create permissions. + * + * @param User $user + * @return bool + */ + public function create(User $user) : bool + { + return $user->isAdmin(); + } + } diff --git a/app/Services/Report/ProfitLoss.php b/app/Services/Report/ProfitLoss.php index ac8c62a0727d..5c6c8c15c547 100644 --- a/app/Services/Report/ProfitLoss.php +++ b/app/Services/Report/ProfitLoss.php @@ -94,6 +94,11 @@ class ProfitLoss $this->setBillingReportType(); } + public function run() + { + return $this->build()->getCsv(); + } + public function build() { MultiDB::setDb($this->company->db); diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index c8bbe14ab8f0..ce3541fe8342 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -24,6 +24,7 @@ use App\Utils\Ninja; use App\Utils\Number; use App\Utils\Traits\AppSetup; use App\Utils\Traits\MakesDates; +use App\Utils\Traits\MakesHash; use Exception; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Cache; @@ -32,7 +33,8 @@ class HtmlEngine { use MakesDates; use AppSetup; - + use MakesHash; + public $entity; public $invitation; @@ -98,6 +100,56 @@ class HtmlEngine } } + private function resolveCompanyLogoSize() + { + $design_map = [ + "VolejRejNm" => "65%", // "Plain", + "Wpmbk5ezJn" => "65%", //"Clean", + "Opnel5aKBz" => "65%", //"Bold", + "wMvbmOeYAl" => "55%", //Modern", + "4openRe7Az" => "65%", //"Business", + "WJxbojagwO" => "65%", //"Creative", + "k8mep2bMyJ" => "55%", //"Elegant", + "l4zbq2dprO" => "65%", //"Hipster", + "yMYerEdOBQ" => "65%", //"Playful", + "gl9avmeG1v" => "65%", //"Tech", + "7LDdwRb1YK" => "65%", //"Calm", + "APdRoy0eGy" => "65%", //"Calm-DB2", + "y1aK83rbQG" => "65%", //"Calm-DB1", + ]; + + $design_int_map = [ + "1" => "65%", // "Plain", + "2" => "65%", //"Clean", + "3" => "65%", //"Bold", + "4" => "55%", //Modern", + "5" => "65%", //"Business", + "6" => "65%", //"Creative", + "7" => "55%", //"Elegant", + "8" => "65%", //"Hipster", + "9" => "65%", //"Playful", + "10" => "65%", //"Tech", + "11" => "65%", //"Calm", + "6972" => "65%", //"C-DB2" + "11221" => "65%", //"C-DB1" + ]; + + if(isset($this->settings->company_logo_size) && strlen($this->settings->company_logo_size) > 1) + return $this->settings->company_logo_size; + + if($this->entity->design_id && array_key_exists($this->entity->design_id, $design_int_map)) + return $design_int_map[$this->entity->design_id]; + + $default_design_id = $this->entity_string."_design_id"; + $design_id = $this->settings->{$default_design_id}; + + if(array_key_exists($design_id, $design_map)) + return $design_map[$design_id]; + + return '65%'; + + } + public function buildEntityDataArray() :array { if (! $this->client->currency()) { @@ -111,8 +163,9 @@ class HtmlEngine $t->replace(Ninja::transformTranslations($this->settings)); $data = []; - //$data[''] = ['value' => '', 'label' => '']; + $data['$global_margin'] = ['value' => '6.35mm', 'label' => '']; + $data['$company_logo_size'] = ['value' => $this->resolveCompanyLogoSize(), 'label' => '']; $data['$tax'] = ['value' => '', 'label' => ctrans('texts.tax')]; $data['$app_url'] = ['value' => $this->generateAppUrl(), 'label' => '']; $data['$from'] = ['value' => '', 'label' => ctrans('texts.from')]; diff --git a/app/Utils/Traits/GeneratesCounter.php b/app/Utils/Traits/GeneratesCounter.php index 037ab14e4262..02b023a71419 100644 --- a/app/Utils/Traits/GeneratesCounter.php +++ b/app/Utils/Traits/GeneratesCounter.php @@ -584,6 +584,12 @@ trait GeneratesCounter $settings->invoice_number_counter = 1; $settings->quote_number_counter = 1; $settings->credit_number_counter = 1; + $settings->ticket_number_counter = 1; + $settings->payment_number_counter = 1; + $settings->project_number_counter = 1; + $settings->task_number_counter = 1; + $settings->expense_number_counter = 1; + $settings->recurring_expense_number_counter = 1; $settings->purchase_order_number_counter = 1; $client->company->settings = $settings; @@ -600,48 +606,67 @@ trait GeneratesCounter return false; } - switch ($company->reset_counter_frequency_id) { + $settings = $company->settings; + + $reset_counter_frequency = (int) $settings->reset_counter_frequency_id; + + if ($reset_counter_frequency == 0) { + + if($settings->reset_counter_date){ + + $settings->reset_counter_date = ""; + $company->settings = $settings; + $company->save(); + + } + + return; + } + + switch ($reset_counter_frequency) { case RecurringInvoice::FREQUENCY_DAILY: - $reset_date->addDay(); + $new_reset_date = $reset_date->addDay(); break; case RecurringInvoice::FREQUENCY_WEEKLY: - $reset_date->addWeek(); + $new_reset_date = $reset_date->addWeek(); break; case RecurringInvoice::FREQUENCY_TWO_WEEKS: - $reset_date->addWeeks(2); + $new_reset_date = $reset_date->addWeeks(2); break; case RecurringInvoice::FREQUENCY_FOUR_WEEKS: - $reset_date->addWeeks(4); + $new_reset_date = $reset_date->addWeeks(4); break; case RecurringInvoice::FREQUENCY_MONTHLY: - $reset_date->addMonth(); + $new_reset_date = $reset_date->addMonth(); break; case RecurringInvoice::FREQUENCY_TWO_MONTHS: - $reset_date->addMonths(2); + $new_reset_date = $reset_date->addMonths(2); break; case RecurringInvoice::FREQUENCY_THREE_MONTHS: - $reset_date->addMonths(3); + $new_reset_date = $reset_date->addMonths(3); break; case RecurringInvoice::FREQUENCY_FOUR_MONTHS: - $reset_date->addMonths(4); + $new_reset_date = $reset_date->addMonths(4); break; case RecurringInvoice::FREQUENCY_SIX_MONTHS: - $reset_date->addMonths(6); + $new_reset_date = $reset_date->addMonths(6); break; case RecurringInvoice::FREQUENCY_ANNUALLY: - $reset_date->addYear(); + $new_reset_date = $reset_date->addYear(); break; case RecurringInvoice::FREQUENCY_TWO_YEARS: - $reset_date->addYears(2); + $new_reset_date = $reset_date->addYears(2); + break; + + default: + $new_reset_date = $reset_date->addYear(); break; } - $settings = $company->settings; - $settings->reset_counter_date = $reset_date->format('Y-m-d'); + $settings->reset_counter_date = $new_reset_date->format('Y-m-d'); $settings->invoice_number_counter = 1; $settings->quote_number_counter = 1; $settings->credit_number_counter = 1; - $settings->vendor_number_counter = 1; $settings->ticket_number_counter = 1; $settings->payment_number_counter = 1; $settings->project_number_counter = 1; diff --git a/config/ninja.php b/config/ninja.php index dca28a9ec4d0..f491829379c5 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -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.63', - 'app_tag' => '5.5.63', + 'app_version' => '5.5.64', + 'app_tag' => '5.5.64', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), diff --git a/database/migrations/2023_01_27_023127_update_design_templates.php b/database/migrations/2023_01_27_023127_update_design_templates.php new file mode 100644 index 000000000000..f0f45d4a4bb2 --- /dev/null +++ b/database/migrations/2023_01_27_023127_update_design_templates.php @@ -0,0 +1,28 @@ + 'Send an email to the vendor when the expense is marked as paid', 'update_payment' => 'Update Payment', 'markup' => 'Markup', + 'unlock_pro' => 'Unlock Pro', ); diff --git a/resources/views/pdf-designs/bold.html b/resources/views/pdf-designs/bold.html index 28aa512019f9..cde5d824c301 100644 --- a/resources/views/pdf-designs/bold.html +++ b/resources/views/pdf-designs/bold.html @@ -59,6 +59,7 @@ .company-logo { height: 100%; max-width: 100%; + /* max-width: $company_logo_size;*/ object-fit: contain; object-position: left center; } diff --git a/resources/views/pdf-designs/business.html b/resources/views/pdf-designs/business.html index d78cc61fed17..ae7925b0672e 100644 --- a/resources/views/pdf-designs/business.html +++ b/resources/views/pdf-designs/business.html @@ -41,6 +41,7 @@ .company-logo { max-width: 65%; + /* max-width: $company_logo_size;*/ } .header-container > span { diff --git a/resources/views/pdf-designs/calm.html b/resources/views/pdf-designs/calm.html index 038450849e5b..5f14d226ef7b 100644 --- a/resources/views/pdf-designs/calm.html +++ b/resources/views/pdf-designs/calm.html @@ -47,6 +47,7 @@ .company-logo { max-width: 65%; + /* max-width: $company_logo_size;*/ } .client-and-entity-wrapper { diff --git a/resources/views/pdf-designs/clean.html b/resources/views/pdf-designs/clean.html index e653e47a038b..454fba5f3b71 100644 --- a/resources/views/pdf-designs/clean.html +++ b/resources/views/pdf-designs/clean.html @@ -23,7 +23,7 @@ @page { margin-left: $global_margin; margin-right: $global_margin; - margin-top: 0; + margin-top: 5; margin-bottom: 0; size: $page_size $page_layout; } @@ -53,6 +53,7 @@ .company-logo { max-width: 65%; + /* max-width: $company_logo_size;*/ } #company-details { @@ -164,7 +165,7 @@ padding-top: .5rem; padding-right: 1rem; gap: 80px; - page-break-inside:auto; + page-break-inside:avoid; overflow: visible !important; } diff --git a/resources/views/pdf-designs/creative.html b/resources/views/pdf-designs/creative.html index fc21a3b9074d..bdb1ad20a1a7 100644 --- a/resources/views/pdf-designs/creative.html +++ b/resources/views/pdf-designs/creative.html @@ -42,6 +42,7 @@ .company-logo { max-width: 65%; + /* max-width: $company_logo_size;*/ } #entity-details p { margin-top: 5px; } diff --git a/resources/views/pdf-designs/elegant.html b/resources/views/pdf-designs/elegant.html index 66e9475c0969..4fbc6154fe69 100644 --- a/resources/views/pdf-designs/elegant.html +++ b/resources/views/pdf-designs/elegant.html @@ -32,6 +32,7 @@ .company-logo { max-width: 55%; + /* max-width: $company_logo_size;*/ margin-left: auto; margin-right: auto; display: block; diff --git a/resources/views/pdf-designs/hipster.html b/resources/views/pdf-designs/hipster.html index dcf869b0381c..0ea7231638a6 100644 --- a/resources/views/pdf-designs/hipster.html +++ b/resources/views/pdf-designs/hipster.html @@ -81,6 +81,7 @@ .company-logo { max-width: 65%; + /* max-width: $company_logo_size;*/ } .entity-label { diff --git a/resources/views/pdf-designs/modern.html b/resources/views/pdf-designs/modern.html index 68d8171f623b..a923209cdba3 100644 --- a/resources/views/pdf-designs/modern.html +++ b/resources/views/pdf-designs/modern.html @@ -85,6 +85,7 @@ .company-logo { max-width: 55%; + /* max-width: $company_logo_size;*/ } #client-details { diff --git a/resources/views/pdf-designs/plain.html b/resources/views/pdf-designs/plain.html index 69dab91793e5..4cedc8f519dd 100644 --- a/resources/views/pdf-designs/plain.html +++ b/resources/views/pdf-designs/plain.html @@ -42,6 +42,7 @@ .company-logo { max-width: 65%; + /* max-width: $company_logo_size;*/ } .header-wrapper #company-address { diff --git a/resources/views/pdf-designs/playful.html b/resources/views/pdf-designs/playful.html index fde299ccbe99..aa4df869e4e0 100644 --- a/resources/views/pdf-designs/playful.html +++ b/resources/views/pdf-designs/playful.html @@ -63,6 +63,7 @@ .company-logo { max-width: 65%; + /* max-width: $company_logo_size;*/ } .contacts-wrapper { diff --git a/resources/views/pdf-designs/tech.html b/resources/views/pdf-designs/tech.html index 7bb5fde54372..1a5babf4f9b7 100644 --- a/resources/views/pdf-designs/tech.html +++ b/resources/views/pdf-designs/tech.html @@ -63,10 +63,12 @@ .company-logo-wrapper { padding-bottom: 60px; + height: 5rem; } - .company-logo-wrapper { - height: 5rem; + .company-logo { + max-width: 65%; +/* max-width: $company_logo_size;*/ } .header-invoice-number { diff --git a/routes/api.php b/routes/api.php index 0969994a5f04..6a5805f757ae 100644 --- a/routes/api.php +++ b/routes/api.php @@ -103,7 +103,7 @@ Route::group(['middleware' => ['throttle:300,1', 'api_secret_check']], function Route::post('api/v1/oauth_login', [LoginController::class, 'oauthApiLogin']); }); -Route::group(['middleware' => ['throttle:10,1','api_secret_check','email_db']], function () { +Route::group(['middleware' => ['throttle:50,1','api_secret_check','email_db']], function () { Route::post('api/v1/login', [LoginController::class, 'apiLogin'])->name('login.submit')->middleware('throttle:20,1'); Route::post('api/v1/reset_password', [ForgotPasswordController::class, 'sendResetLinkEmail']); }); @@ -228,6 +228,7 @@ Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale Route::post('preview', [PreviewController::class, 'show'])->name('preview.show'); Route::post('live_preview', [PreviewController::class, 'live'])->name('preview.live'); + Route::post('live_design', [PreviewController::class, 'design'])->name('preview.design'); Route::post('preview/purchase_order', [PreviewPurchaseOrderController::class, 'show'])->name('preview_purchase_order.show'); Route::post('live_preview/purchase_order', [PreviewPurchaseOrderController::class, 'live'])->name('preview_purchase_order.live'); diff --git a/tests/Feature/DesignApiTest.php b/tests/Feature/DesignApiTest.php index dec0cc00bed7..723596e639b2 100644 --- a/tests/Feature/DesignApiTest.php +++ b/tests/Feature/DesignApiTest.php @@ -46,6 +46,7 @@ class DesignApiTest extends TestCase public function testDesignPost() { + $design = [ 'body' => 'body', 'includes' => 'includes', diff --git a/tests/Feature/LiveDesignTest.php b/tests/Feature/LiveDesignTest.php new file mode 100644 index 000000000000..51d6fbfdf3ba --- /dev/null +++ b/tests/Feature/LiveDesignTest.php @@ -0,0 +1,62 @@ +makeTestData(); + + $this->withoutMiddleware( + ThrottleRequests::class + ); + + if (config('ninja.testvars.travis') !== false) { + $this->markTestSkipped('Skip test for Travis'); + } + + } + + public function testDesignRoute200() + { + $data = [ + 'entity' => 'invoice', + 'entity_id' => $this->invoice->hashed_id, + 'settings_type' => 'company', + 'settings' => (array)$this->company->settings, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/live_design/', $data); + + $response->assertStatus(200); + } + + +} diff --git a/tests/Feature/RecurringInvoiceTest.php b/tests/Feature/RecurringInvoiceTest.php index 5350a38f420e..be9c0e002bb2 100644 --- a/tests/Feature/RecurringInvoiceTest.php +++ b/tests/Feature/RecurringInvoiceTest.php @@ -308,13 +308,15 @@ class RecurringInvoiceTest extends TestCase public function testRecurringDatePassesToInvoice() { $noteText = "Hello this is for :MONTH_AFTER"; - $recurringDate = \Carbon\Carbon::now()->subDays(10); + $recurringDate = \Carbon\Carbon::now()->timezone($this->client->timezone()->name)->subDays(10); $item = InvoiceItemFactory::create(); $item->cost = 10; $item->notes = $noteText; $recurring_invoice = InvoiceToRecurringInvoiceFactory::create($this->invoice); + + $recurring_invoice->user_id = $this->user->id; $recurring_invoice->next_send_date = $recurringDate; $recurring_invoice->status_id = RecurringInvoice::STATUS_ACTIVE; diff --git a/tests/Feature/WebhookAPITest.php b/tests/Feature/WebhookAPITest.php index 69c640a6ae61..b8af16692591 100644 --- a/tests/Feature/WebhookAPITest.php +++ b/tests/Feature/WebhookAPITest.php @@ -70,6 +70,7 @@ class WebhookAPITest extends TestCase $data = [ 'target_url' => 'http://hook.com', 'event_id' => 1, + 'rest_method' => 'post', 'format' => 'JSON', ]; @@ -85,7 +86,10 @@ class WebhookAPITest extends TestCase $this->assertEquals(1, $arr['data']['event_id']); $data = [ + 'target_url' => 'http://hook.com', 'event_id' => 2, + 'rest_method' => 'post', + 'format' => 'JSON', ]; $response = $this->withHeaders([ diff --git a/tests/cypress/screenshots/.gitignore b/tests/cypress/screenshots/.gitignore new file mode 100644 index 000000000000..d6b7ef32c847 --- /dev/null +++ b/tests/cypress/screenshots/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/cypress/videos/.gitignore b/tests/cypress/videos/.gitignore new file mode 100644 index 000000000000..d6b7ef32c847 --- /dev/null +++ b/tests/cypress/videos/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/cypress/videos/login.cy.js.mp4 b/tests/cypress/videos/login.cy.js.mp4 deleted file mode 100644 index a77a06bb697c..000000000000 Binary files a/tests/cypress/videos/login.cy.js.mp4 and /dev/null differ