Merge pull request #7484 from turbo124/v5-stable

v5.3.93
This commit is contained in:
David Bomba 2022-05-30 09:54:00 +10:00 committed by GitHub
commit 7de2900f36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 312874 additions and 310728 deletions

View File

@ -39,6 +39,19 @@ jobs:
sudo find ./vendor/bin/ -type f -exec chmod +x {} \; sudo find ./vendor/bin/ -type f -exec chmod +x {} \;
sudo find ./ -type d -exec chmod 755 {} \; sudo find ./ -type d -exec chmod 755 {} \;
- name: Prepare React FrontEnd
run: |
mkdir public/react
git clone https://${{secrets.commit_secret}}@github.com/invoiceninja/ui.git
cd ui
git checkout main
npm i
npm run build
cp -r dist/react/* ../public/react
cd ..
rm -rf ui
php artisan ninja:react
- name: Prepare JS/CSS assets - name: Prepare JS/CSS assets
run: | run: |
npm i npm i

View File

@ -1 +1 @@
5.3.92 5.3.93

View File

@ -866,6 +866,7 @@ class CheckData extends Command
foreach(Invoice::with(['payments'])->whereHas('payments')->where('status_id', 4)->where('balance', '>', 0)->where('is_deleted',0)->cursor() as $invoice) foreach(Invoice::with(['payments'])->whereHas('payments')->where('status_id', 4)->where('balance', '>', 0)->where('is_deleted',0)->cursor() as $invoice)
{ {
$this->$this->wrong_paid_status++;
$this->logMessage("# {$invoice->id} " . ' - '.$invoice->number." - Marked as paid, but balance = {$invoice->balance}"); $this->logMessage("# {$invoice->id} " . ' - '.$invoice->number." - Marked as paid, but balance = {$invoice->balance}");

View File

@ -0,0 +1,88 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\Libraries\MultiDB;
use App\Models\Backup;
use App\Models\Design;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use stdClass;
class ReactBuilder extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:react';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Builds blade component for react includes';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$includes = '';
$directoryIterator = new \RecursiveDirectoryIterator(public_path('react'), \RecursiveDirectoryIterator::SKIP_DOTS);
foreach (new \RecursiveIteratorIterator($directoryIterator) as $file) {
if(str_contains($file->getFileName(), '.js')) {
if(str_contains($file->getFileName(), 'index.')){
$includes .= '<script type="module" crossorigin src="/react/' . $file->getFileName() . '"></script>'."\n";
}
else{
$includes .= '<link rel="modulepreload" href="/react/' . $file->getFileName() . '">'."\n";
}
}
if(str_contains($file->getFileName(), '.css')) {
$includes .= '<link rel="stylesheet" href="/react/' . $file->getFileName() . '">'."\n";
}
}
file_put_contents(resource_path('views/react/head.blade.php'), $includes);
}
}

View File

@ -20,6 +20,7 @@ use App\Jobs\Ninja\AdjustEmailQuota;
use App\Jobs\Ninja\CompanySizeCheck; use App\Jobs\Ninja\CompanySizeCheck;
use App\Jobs\Ninja\QueueSize; use App\Jobs\Ninja\QueueSize;
use App\Jobs\Ninja\SystemMaintenance; use App\Jobs\Ninja\SystemMaintenance;
use App\Jobs\Ninja\TaskScheduler;
use App\Jobs\Util\DiskCleanup; use App\Jobs\Util\DiskCleanup;
use App\Jobs\Util\ReminderJob; use App\Jobs\Util\ReminderJob;
use App\Jobs\Util\SchedulerCheck; use App\Jobs\Util\SchedulerCheck;
@ -73,11 +74,12 @@ class Kernel extends ConsoleKernel
$schedule->job(new SchedulerCheck)->daily()->withoutOverlapping(); $schedule->job(new SchedulerCheck)->daily()->withoutOverlapping();
$schedule->job(new TaskScheduler())->daily()->withoutOverlapping();
$schedule->job(new SystemMaintenance)->weekly()->withoutOverlapping(); $schedule->job(new SystemMaintenance)->weekly()->withoutOverlapping();
if(Ninja::isSelfHost()) if(Ninja::isSelfHost())
{ {
$schedule->call(function () { $schedule->call(function () {
Account::whereNotNull('id')->update(['is_scheduler_running' => true]); Account::whereNotNull('id')->update(['is_scheduler_running' => true]);
})->everyFiveMinutes(); })->everyFiveMinutes();

View File

@ -49,7 +49,7 @@ class CreditFilters extends QueryFilters
} }
if (in_array('partial', $status_parameters)) { if (in_array('partial', $status_parameters)) {
$this->builder->where('status_id', Credit::STAUTS_PARTIAL); $this->builder->where('status_id', Credit::STATUS_PARTIAL);
} }
if (in_array('applied', $status_parameters)) { if (in_array('applied', $status_parameters)) {

View File

@ -81,7 +81,7 @@ abstract class QueryFilters
continue; continue;
} }
if (strlen($value)) { if (is_string($value) && strlen($value)) {
$this->$name($value); $this->$name($value);
} else { } else {
$this->$name(); $this->$name();

View File

@ -91,6 +91,33 @@ class ActivityController extends BaseController
$activities = Activity::orderBy('created_at', 'DESC')->company() $activities = Activity::orderBy('created_at', 'DESC')->company()
->take($default_activities); ->take($default_activities);
if($request->has('react')){
$system = ctrans('texts.system');
$data = $activities->cursor()->map(function ($activity) use($system){
$arr=
[
'client' => $activity->client ? $activity->client : '',
'contact' => $activity->contact ? $activity->contact : '',
'quote' => $activity->quote ? $activity->quote : '',
'user' => $activity->user ? $activity->user : '',
'expense' => $activity->expense ? $activity->expense : '',
'invoice' => $activity->invoice ? $activity->invoice : '',
'recurring_invoice' => $activity->recurring_invoice ? $activity->recurring_invoice : '',
'payment' => $activity->payment ? $activity->payment : '',
'credit' => $activity->credit ? $activity->credit : '',
'task' => $activity->task ? $activity->task : '',
];
return array_merge($arr, $activity->toArray());
});
return response()->json(['data' => $data->toArray()], 200);
}
return $this->listResponse($activities); return $this->listResponse($activities);
} }

View File

@ -780,6 +780,9 @@ class BaseController extends Controller
$this->buildCache(); $this->buildCache();
if(config('ninja.react_app_enabled'))
return response()->view('react.index', $data)->header('X-Frame-Options', 'SAMEORIGIN', false);
else
return response()->view('index.index', $data)->header('X-Frame-Options', 'SAMEORIGIN', false); return response()->view('index.index', $data)->header('X-Frame-Options', 'SAMEORIGIN', false);
} }

View File

@ -125,13 +125,14 @@ class NinjaPlanController extends Controller
$gateway_driver->storeGatewayToken($data, ['gateway_customer_reference' => $customer->id]); $gateway_driver->storeGatewayToken($data, ['gateway_customer_reference' => $customer->id]);
//set free trial //set free trial
// $account = auth()->guard('contact')->user()->company->account;
if(auth()->guard('contact')->user()->client->custom_value2){ if(auth()->guard('contact')->user()->client->custom_value2){
MultiDB::findAndSetDbByAccountKey(auth()->guard('contact')->user()->client->custom_value2); MultiDB::findAndSetDbByAccountKey(auth()->guard('contact')->user()->client->custom_value2);
$account = Account::where('key', auth()->guard('contact')->user()->client->custom_value2)->first(); $account = Account::where('key', auth()->guard('contact')->user()->client->custom_value2)->first();
$account->trial_started = now(); // $account->trial_started = now();
$account->trial_plan = 'pro'; // $account->trial_plan = 'pro';
$account->plan = 'pro'; $account->plan = 'pro';
$account->plan_term = 'month';
$account->plan_started = now();
$account->plan_expires = now()->addDays(14); $account->plan_expires = now()->addDays(14);
$account->save(); $account->save();
} }

View File

@ -220,8 +220,6 @@ class InvoiceController extends BaseController
public function store(StoreInvoiceRequest $request) public function store(StoreInvoiceRequest $request)
{ {
// $client = Client::find($request->input('client_id'));
$invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create(auth()->user()->company()->id, auth()->user()->id)); $invoice = $this->invoice_repo->save($request->all(), InvoiceFactory::create(auth()->user()->company()->id, auth()->user()->id));
$invoice = $invoice->service() $invoice = $invoice->service()

View File

@ -0,0 +1,51 @@
<?php
/**
* @OA\Schema(
* schema="TaskSchedulerSchema",
* type="object",
*
*
* @OA\Property(property="paused",type="boolean",example="false",description="The scheduler paused state"),
* @OA\Property(property="repeat_every",type="string",example="DAY",description="Accepted values (DAY,WEEK,MONTH,3MONTHS,YEAR)"),
* @OA\Property(property="start_from",type="integer",example="1652898504",description="Timestamp when we should start the scheduler, default is today"),
* @OA\Property(property="date_range", type="string", example="last7", description="The string representation of the date range of data to be returned"),
* @OA\Property(property="date_key", type="string", example="created_at", description="The date column to search between."),
* @OA\Property(property="start_date", type="string", example="2022-10-31", description="The start date to search between"),
* @OA\Property(property="end_date", type="string", example="2022-10-31", description="The end date to search between"),
* @OA\Property(
* property="report_keys",
* type="array",
* @OA\Items(
* type="string",
* description="Array of Keys to export",
* example="['name','date']",
* ),
* ),
*
* )
*/
/**
* @OA\Schema(
* schema="UpdateTaskSchedulerSchema",
* type="object",
*
* @OA\Property(property="paused",type="boolean",example="false",description="The scheduler paused state"),
* * @OA\Property(property="repeat_every",type="string",example="DAY",description="Accepted values (DAY,WEEK,MONTH,3MONTHS,YEAR)"),
* @OA\Property(property="start_from",type="integer",example="1652898504",description="Timestamp when we should start the scheduler, default is today"),
*
* )
*/
/**
* @OA\Schema(
* schema="UpdateJobForASchedulerSchema",
* type="object",
* @OA\Property(property="job",type="string",example="create_client_report",description="Set action name, action names can be found in ScheduledJob Model"),
*
* )
*/

View File

@ -31,11 +31,11 @@ class CreditReportController extends BaseController
/** /**
* @OA\Post( * @OA\Post(
* path="/api/v1/reports/clients", * path="/api/v1/reports/credit",
* operationId="getClientReport", * operationId="getCreditReport",
* tags={"reports"}, * tags={"reports"},
* summary="Client reports", * summary="Credit reports",
* description="Export client reports", * description="Export credit reports",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"), * @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\RequestBody( * @OA\RequestBody(

View File

@ -31,11 +31,11 @@ class QuoteItemReportController extends BaseController
/** /**
* @OA\Post( * @OA\Post(
* path="/api/v1/reports/invoice_items", * path="/api/v1/reports/quote_items",
* operationId="getQuoteItemReport", * operationId="getQuoteItemReport",
* tags={"reports"}, * tags={"reports"},
* summary="Invoice item reports", * summary="Quote item reports",
* description="Export invoice item reports", * description="Export Quote item reports",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"), * @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\RequestBody( * @OA\RequestBody(

View File

@ -0,0 +1,292 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers;
use App\Http\Requests\TaskScheduler\CreateScheduledTaskRequest;
use App\Http\Requests\TaskScheduler\UpdateScheduledJobRequest;
use App\Http\Requests\TaskScheduler\UpdateScheduleRequest;
use App\Jobs\Ninja\TaskScheduler;
use App\Jobs\Report\ProfitAndLoss;
use App\Models\ScheduledJob;
use App\Models\Scheduler;
use App\Repositories\TaskSchedulerRepository;
use App\Transformers\TaskSchedulerTransformer;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Symfony\Component\HttpFoundation\Request;
class TaskSchedulerController extends BaseController
{
protected $entity_type = TaskScheduler::class;
protected $entity_transformer = TaskSchedulerTransformer::class;
protected TaskSchedulerRepository $scheduler_repository;
public function __construct(TaskSchedulerRepository $scheduler_repository)
{
parent::__construct();
$this->scheduler_repository = $scheduler_repository;
}
/**
* @OA\GET(
* path="/api/v1/task_scheduler/",
* operationId="getTaskSchedulers",
* tags={"task_scheduler"},
* summary="Task Scheduler Index",
* description="Get all schedulers with associated jobs",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Response(
* response=200,
* description="success",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function index()
{
$schedulers = Scheduler::where('company_id', auth()->user()->company()->id);
return $this->listResponse($schedulers);
}
/**
* @OA\Post(
* path="/api/v1/task_scheduler/",
* operationId="createTaskScheduler",
* tags={"task_scheduler"},
* summary="Create task scheduler with job ",
* description="Create task scheduler with a job (action(job) request should be sent via request also. Example: We want client report to be job which will be run
* multiple times, we should send the same parameters in the request as we would send if we wanted to get report, see example",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(ref="#/components/schemas/TaskSchedulerSchema")
* ),
* @OA\Response(
* response=200,
* description="success",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function store(CreateScheduledTaskRequest $request)
{
$scheduler = new Scheduler();
$scheduler->service()->store($scheduler, $request);
return $this->itemResponse($scheduler);
}
/**
* @OA\GET(
* path="/api/v1/task_scheduler/{id}",
* operationId="showTaskScheduler",
* tags={"task_scheduler"},
* summary="Show given scheduler",
* description="Get scheduler with associated job",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Scheduler Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="success",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function show(Scheduler $scheduler)
{
return $this->itemResponse($scheduler);
}
/**
* @OA\PUT(
* path="/api/v1/task_scheduler/{id}",
* operationId="updateTaskScheduler",
* tags={"task_scheduler"},
* summary="Update task scheduler ",
* description="Update task scheduler",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Scheduler Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ), * @OA\RequestBody(
* required=true,
* @OA\JsonContent(ref="#/components/schemas/UpdateTaskSchedulerSchema")
* ),
* @OA\Response(
* response=200,
* description="success",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function update(Scheduler $scheduler, UpdateScheduleRequest $request)
{
$scheduler->service()->update($scheduler, $request);
return $this->itemResponse($scheduler);
}
/**
* @OA\PUT(
* path="/api/v1/task_scheduler/{id}/update_job/",
* operationId="updateTaskSchedulerJob",
* tags={"task_scheduler"},
* summary="Update job for a task scheduler ",
* description="Update job for a task scheduler | if we are changing action for a job, we should send the request for a new job same as we are creating new scheduler",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Scheduler Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(ref="#/components/schemas/UpdateJobForASchedulerSchema")
* ),
* @OA\Response(
* response=200,
* description="success",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function updateJob(Scheduler $scheduler, UpdateScheduledJobRequest $request)
{
$scheduler->service()->updateJob($scheduler, $request);
return $this->itemResponse($scheduler);
}
/**
* @OA\DELETE(
* path="/api/v1/task_scheduler/{id}",
* operationId="destroyTaskScheduler",
* tags={"task_scheduler"},
* summary="Destroy Task Scheduler",
* description="Destroy task scheduler and its associated job",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Scheduler Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="success",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function destroy(Scheduler $scheduler)
{
$this->scheduler_repository->delete($scheduler);
return $this->itemResponse($scheduler->fresh());
}
}

View File

@ -47,6 +47,9 @@ class GenericReportRequest extends Request
if(!array_key_exists('report_keys', $input)) if(!array_key_exists('report_keys', $input))
$input['report_keys'] = []; $input['report_keys'] = [];
if(!array_key_exists('send_email', $input))
$input['send_email'] = true;
$this->replace($input); $this->replace($input);
} }
} }

View File

@ -22,13 +22,6 @@ trait RuntimeFormRequest
$instance = $validator->getValidatorInstance(); $instance = $validator->getValidatorInstance();
return $instance; return $instance;
// if ($instance->fails()) {
// return $instance->errors();
// }
// $validator->passedValidation();
// return $validator->all();
} }

View File

@ -0,0 +1,40 @@
<?php
namespace App\Http\Requests\TaskScheduler;
use App\Http\Requests\Request;
class CreateScheduledTaskRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return auth()->user()->isAdmin();
}
public function rules()
{
return [
'paused' => 'sometimes|bool',
'repeat_every' => 'required|string|in:DAY,WEEK,MONTH,3MONTHS,YEAR',
'start_from' => 'sometimes|string',
'job' => 'required',
];
}
public function prepareForValidation()
{
$input = $this->all();
if(!array_key_exists('start_from', $input))
$input['start_from'] = now();
$this->replace($input);
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\TaskScheduler;
use App\Http\Requests\Request;
use App\Models\ScheduledJob;
use Carbon\Carbon;
use Illuminate\Validation\Rule;
class UpdateScheduleRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return auth()->user()->isAdmin();
}
public function rules(): array
{
return [
'paused' => 'sometimes|bool',
'repeat_every' => 'sometimes|string|in:DAY,WEEK,BIWEEKLY,MONTH,3MONTHS,YEAR',
'start_from' => 'sometimes',
'scheduled_run'=>'sometimes'
];
}
public function prepareForValidation()
{
$input = $this->all();
if (isset($input['start_from'])) {
$input['scheduled_run'] = Carbon::parse((int)$input['start_from']);
$input['start_from'] = Carbon::parse((int)$input['start_from']);
}
$this->replace($input);
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\TaskScheduler;
use App\Http\Requests\Request;
class UpdateScheduledJobRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return auth()->user()->isAdmin();
}
public function rules(): array
{
return [
'action_name' => 'sometimes|string',
];
}
}

View File

@ -62,7 +62,7 @@ class ValidInvoicesRules implements Rule
return false; return false;
} }
$inv = Invoice::whereId($invoice['invoice_id'])->first(); $inv = Invoice::withTrashed()->whereId($invoice['invoice_id'])->first();
if (! $inv) { if (! $inv) {

View File

@ -0,0 +1,140 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Jobs\Ninja;
use App\Jobs\Report\SendToAdmin;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\ScheduledJob;
use App\Models\Scheduler;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class TaskScheduler implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct()
{
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
foreach (MultiDB::$dbs as $db)
{
MultiDB::setDB($db);
$pending_schedulers = $this->fetchJobs();
Scheduler::with('company','job')
->where('paused', false)
->where('is_deleted', false)
->where('scheduled_run', '<', now())
->cursor()
->each(function ($scheduler){
$this->doJob($scheduler);
});
}
}
private function doJob(Scheduler $scheduler)
{
nlog("Doing job {$scheduler->id}");
$job = $scheduler->job;
$company = $scheduler->company;
if (!$job)
return;
$parameters = $job->parameters;
switch ($job->action_name) {
case ScheduledJob::CREATE_CLIENT_REPORT:
SendToAdmin::dispatch($company, $parameters, $job->action_class, 'contacts.csv');
break;
case ScheduledJob::CREATE_CLIENT_CONTACT_REPORT:
SendToAdmin::dispatch($company, $parameters, $job->action_class, 'clients.csv');
break;
case ScheduledJob::CREATE_CREDIT_REPORT:
SendToAdmin::dispatch($company, $parameters, $job->action_class, 'credits.csv');
break;
case ScheduledJob::CREATE_DOCUMENT_REPORT:
SendToAdmin::dispatch($company, $parameters, $job->action_class, 'documents.csv');
break;
case ScheduledJob::CREATE_EXPENSE_REPORT:
SendToAdmin::dispatch($company, $parameters, $job->action_class, 'expense.csv');
break;
case ScheduledJob::CREATE_INVOICE_ITEM_REPORT:
SendToAdmin::dispatch($company, $parameters, $job->action_class, 'invoice_items.csv');
break;
case ScheduledJob::CREATE_INVOICE_REPORT:
SendToAdmin::dispatch($company, $parameters, $job->action_class, 'invoices.csv');
break;
case ScheduledJob::CREATE_PAYMENT_REPORT:
SendToAdmin::dispatch($company, $parameters, $job->action_class, 'payments.csv');
break;
case ScheduledJob::CREATE_PRODUCT_REPORT:
SendToAdmin::dispatch($company, $parameters, $job->action_class, 'products.csv');
break;
case ScheduledJob::CREATE_PROFIT_AND_LOSS_REPORT:
SendToAdmin::dispatch($company, $parameters, $job->action_class, 'profit_and_loss.csv');
break;
case ScheduledJob::CREATE_QUOTE_ITEM_REPORT:
SendToAdmin::dispatch($company, $parameters, $job->action_class, 'quote_items.csv');
break;
case ScheduledJob::CREATE_QUOTE_REPORT:
SendToAdmin::dispatch($company, $parameters, $job->action_class, 'quotes.csv');
break;
case ScheduledJob::CREATE_RECURRING_INVOICE_REPORT:
SendToAdmin::dispatch($company, $parameters, $job->action_class, 'recurring_invoices.csv');
break;
case ScheduledJob::CREATE_TASK_REPORT:
SendToAdmin::dispatch($company, $parameters, $job->action_class, 'tasks.csv');
break;
}
$scheduler->scheduled_run = $scheduler->nextScheduledDate();
$scheduler->save();
}
private function fetchJobs()
{
return ;
}
}

View File

@ -48,6 +48,7 @@ class SendToAdmin implements ShouldQueue
public function handle() public function handle()
{ {
MultiDB::setDb($this->company->db); MultiDB::setDb($this->company->db);
$export = new $this->report_class($this->company, $this->request); $export = new $this->report_class($this->company, $this->request);
$csv = $export->run(); $csv = $export->run();

View File

@ -70,6 +70,7 @@ class Activity extends StaticModel
const ARCHIVE_USER = 50; const ARCHIVE_USER = 50;
const DELETE_USER = 51; const DELETE_USER = 51;
const RESTORE_USER = 52; const RESTORE_USER = 52;
const MARK_SENT_INVOICE = 53; // not needed? const MARK_SENT_INVOICE = 53; // not needed?
const PAID_INVOICE = 54; // const PAID_INVOICE = 54; //
const EMAIL_INVOICE_FAILED = 57; const EMAIL_INVOICE_FAILED = 57;
@ -138,13 +139,11 @@ class Activity extends StaticModel
return $this->hasOne(Backup::class); return $this->hasOne(Backup::class);
} }
public function history() public function history()
{ {
return $this->hasOne(Backup::class); return $this->hasOne(Backup::class);
} }
/** /**
* @return mixed * @return mixed
*/ */
@ -177,6 +176,14 @@ class Activity extends StaticModel
return $this->belongsTo(Invoice::class)->withTrashed(); return $this->belongsTo(Invoice::class)->withTrashed();
} }
/**
* @return mixed
*/
public function recurring_invoice()
{
return $this->belongsTo(RecurringInvoice::class)->withTrashed();
}
public function credit() public function credit()
{ {
return $this->belongsTo(Credit::class)->withTrashed(); return $this->belongsTo(Credit::class)->withTrashed();
@ -198,15 +205,16 @@ class Activity extends StaticModel
return $this->belongsTo(Payment::class)->withTrashed(); return $this->belongsTo(Payment::class)->withTrashed();
} }
// public function task() public function expense()
// { {
// return $this->belongsTo(Task::class)->withTrashed(); return $this->belongsTo(Expense::class)->withTrashed();
// } }
public function task()
{
return $this->belongsTo(Task::class)->withTrashed();
}
// public function expense()
// {
// return $this->belongsTo(Expense::class)->withTrashed();
// }
public function company() public function company()
{ {

View File

@ -0,0 +1,49 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property mixed|string action_class
* @property array parameters
* @property string action_name
* @property integer scheduler_id
* @property integer company_id
*/
class ScheduledJob extends BaseModel
{
use HasFactory, SoftDeletes;
const CREATE_CLIENT_REPORT = 'create_client_report';
const CREATE_CLIENT_CONTACT_REPORT = 'create_client_contact_report';
const CREATE_CREDIT_REPORT = 'create_credit_report';
const CREATE_DOCUMENT_REPORT = 'create_document_report';
const CREATE_EXPENSE_REPORT = 'create_expense_report';
const CREATE_INVOICE_ITEM_REPORT = 'create_invoice_item_report';
const CREATE_INVOICE_REPORT = 'create_invoice_report';
const CREATE_PAYMENT_REPORT = 'create_payment_report';
const CREATE_PRODUCT_REPORT = 'create_product_report';
const CREATE_PROFIT_AND_LOSS_REPORT = 'create_profit_and_loss_report';
const CREATE_QUOTE_ITEM_REPORT = 'create_quote_item_report';
const CREATE_QUOTE_REPORT = 'create_quote_report';
const CREATE_RECURRING_INVOICE_REPORT = 'create_recurring_invoice_report';
const CREATE_TASK_REPORT = 'create_task_report';
protected $fillable = ['action_class', 'action_name', 'parameters', 'scheduler_id', 'company_id'];
protected $casts = [
'parameters' => 'array'
];
}

123
app/Models/Scheduler.php Normal file
View File

@ -0,0 +1,123 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Models;
use App\Services\TaskScheduler\TaskSchedulerService;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
/**
* @property boolean paused
* @property boolean is_deleted
* @property \Carbon\Carbon|mixed start_from
* @property string repeat_every
* @property \Carbon\Carbon|mixed scheduled_run
* @property mixed job
* @property integer company_id
* @property integer updated_at
* @property integer created_at
* @property integer deleted_at
*/
class Scheduler extends BaseModel
{
use HasFactory, SoftDeletes;
protected $fillable = [
'start_from',
'paused',
'repeat_every',
'scheduled_run',
];
protected $casts = [
'start_from' => 'timestamp',
'scheduled_run' => 'timestamp',
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'deleted_at' => 'timestamp',
'paused' => 'boolean',
'is_deleted' => 'boolean',
];
protected $appends = ['linked_job'];
const DAILY = 'DAY';
const WEEKLY = 'WEEK';
const BIWEEKLY = 'BIWEEKLY';
const MONTHLY = 'MONTH';
const QUARTERLY = '3MONTHS';
const ANNUALLY = 'YEAR';
public function getLinkedJobAttribute()
{
return $this->job ?? [];
}
/**
* Service entry points.
*/
public function service(): TaskSchedulerService
{
return new TaskSchedulerService($this);
}
public function job(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(ScheduledJob::class, 'scheduler_id', 'id');
}
public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Company::class);
}
public function nextScheduledDate(): ?Carbon
{
$offset = 0;
$entity_send_time = $this->company->settings->entity_send_time;
if ($entity_send_time != 0) {
$timezone = $this->company->timezone();
$offset -= $timezone->utc_offset;
$offset += ($entity_send_time * 3600);
}
/*
As we are firing at UTC+0 if our offset is negative it is technically firing the day before so we always need
to add ON a day - a day = 86400 seconds
*/
if ($offset < 0)
$offset += 86400;
switch ($this->repeat_every) {
case self::DAILY:
return Carbon::parse($this->scheduled_run)->startOfDay()->addDay()->addSeconds($offset);
case self::WEEKLY:
return Carbon::parse($this->scheduled_run)->startOfDay()->addWeek()->addSeconds($offset);
case self::BIWEEKLY:
return Carbon::parse($this->scheduled_run)->startOfDay()->addWeeks(2)->addSeconds($offset);
case self::MONTHLY:
return Carbon::parse($this->scheduled_run)->startOfDay()->addMonthNoOverflow()->addSeconds($offset);
case self::QUARTERLY:
return Carbon::parse($this->scheduled_run)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset);
case self::ANNUALLY:
return Carbon::parse($this->scheduled_run)->startOfDay()->addYearNoOverflow()->addSeconds($offset);
default:
return null;
}
}
}

View File

@ -0,0 +1,97 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\Stripe\Jobs;
use App\Jobs\Mail\PaymentFailedMailer;
use App\Jobs\Util\SystemLogger;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\Stripe\Utilities;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class StripeWebhook implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Utilities;
public $tries = 1;
public $deleteWhenMissingModels = true;
public int $company_gateway_id;
public string $company_key;
private array $events = [
'source.chargeable',
'charge.succeeded',
'charge.failed',
'payment_intent.succeeded',
'payment_intent.payment_failed'
];
public function __construct(string $company_key, int $company_gateway_id)
{
$this->company_key = $company_key;
$this->company_gateway_id = $company_gateway_id;
}
public function handle()
{
MultiDB::findAndSetDbByCompanyKey($this->company_key);
$company = Company::where('company_key', $this->company_key)->first();
$company_gateway = CompanyGateway::find($this->company_gateway_id);
$driver = $company_gateway->driver();
$stripe = $driver->init();
$endpoints = \Stripe\WebhookEndpoint::all([], $stripe->stripe_connect_auth);
$webhook_url = $company_gateway->webhookUrl();
foreach($endpoints['data'] as $endpoint)
{
if($endpoint->url === $webhook_url)
{
\Stripe\WebhookEndpoint::update($endpoint->id, $this->events, $stripe->stripe_connect_auth);
}
}
/* Add new webhook */
if(count($endpoints['data'] == 0)
{
\Stripe\WebhookEndpoint::create([
'url' => $webhook_url,
'enabled_events' => $this->events,
], $stripe->stripe_connect_auth)
}
}
}

View File

@ -0,0 +1,18 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Repositories;
class TaskSchedulerRepository extends BaseRepository
{
}

View File

@ -51,6 +51,7 @@ class HandleCancellation extends AbstractService
//adjust client balance //adjust client balance
$this->invoice->client->service()->updateBalance($adjustment)->save(); $this->invoice->client->service()->updateBalance($adjustment)->save();
$this->invoice->fresh();
$this->invoice->service()->workFlow()->save(); $this->invoice->service()->workFlow()->save();
@ -78,6 +79,7 @@ class HandleCancellation extends AbstractService
$adjustment = $cancellation->adjustment * -1; $adjustment = $cancellation->adjustment * -1;
$this->invoice->ledger()->updateInvoiceBalance($adjustment, "Invoice {$this->invoice->number} reversal"); $this->invoice->ledger()->updateInvoiceBalance($adjustment, "Invoice {$this->invoice->number} reversal");
$this->invoice->fresh();
/* Reverse the invoice status and balance */ /* Reverse the invoice status and balance */
$this->invoice->balance += $adjustment; $this->invoice->balance += $adjustment;
@ -90,6 +92,7 @@ class HandleCancellation extends AbstractService
unset($backup->cancellation); unset($backup->cancellation);
$this->invoice->backup = $backup; $this->invoice->backup = $backup;
$this->invoice->saveQuietly(); $this->invoice->saveQuietly();
$this->invoice->fresh();
return $this->invoice; return $this->invoice;
} }

View File

@ -0,0 +1,194 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\TaskScheduler;
use App\Export\CSV\ClientExport;
use App\Export\CSV\ContactExport;
use App\Export\CSV\CreditExport;
use App\Export\CSV\DocumentExport;
use App\Export\CSV\ExpenseExport;
use App\Export\CSV\InvoiceExport;
use App\Export\CSV\InvoiceItemExport;
use App\Export\CSV\PaymentExport;
use App\Export\CSV\ProductExport;
use App\Export\CSV\QuoteExport;
use App\Export\CSV\QuoteItemExport;
use App\Export\CSV\RecurringInvoiceExport;
use App\Export\CSV\TaskExport;
use App\Http\Requests\Report\GenericReportRequest;
use App\Http\Requests\Report\ProfitLossRequest;
use App\Http\Requests\TaskScheduler\CreateScheduledTaskRequest;
use App\Http\Requests\TaskScheduler\UpdateScheduledJobRequest;
use App\Http\Requests\TaskScheduler\UpdateScheduleRequest;
use App\Jobs\Report\ProfitAndLoss;
use App\Models\Company;
use App\Models\ScheduledJob;
use App\Models\Scheduler;
use App\Utils\Ninja;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpFoundation\Request;
class TaskSchedulerService
{
public Scheduler $scheduler;
public function __construct(Scheduler $scheduler)
{
$this->scheduler = $scheduler;
}
public function store(Scheduler $scheduler, CreateScheduledTaskRequest $request)
{
$scheduler->paused = $request->get('paused', false);
$scheduler->start_from = $request->get('start_from') ? Carbon::parse((int)$request->get('start_from')) : Carbon::now();
$scheduler->repeat_every = $request->get('repeat_every');
$scheduler->scheduled_run = $request->get('start_from') ? Carbon::parse((int)$request->get('start_from')) : Carbon::now();;
$scheduler->company_id = auth()->user()->company()->id;
$scheduler->save();
$this->createJob($request, $scheduler);
}
public function update(Scheduler $scheduler, UpdateScheduleRequest $request)
{
$data = $request->validated();
$update = $this->scheduler->update($data);
if ($update) {
return response(['successfully_updated_scheduler'], 200);
}
return response(['failed_to_update_scheduler'], 400);
}
public function createJob(CreateScheduledTaskRequest $request, Scheduler $scheduler): bool
{
$job = new ScheduledJob();
$job = $this->setJobParameters($job, $request);
$job->scheduler_id = $scheduler->id;
$job->company_id = auth()->user()->company()->id;
return $job->save();
}
private function runValidation($form_request, $data)
{
$_syn_request_class = new $form_request();
$_syn_request_class->setContainer(app());
$_syn_request_class->initialize($data);
$_syn_request_class->prepareForValidation();
$_syn_request_class->setValidator(Validator::make($_syn_request_class->all(), $_syn_request_class->rules()));
return $_syn_request_class->validated();
}
public function setJobParameters(ScheduledJob $job, $request): ScheduledJob
{
switch ($request->job) {
case ScheduledJob::CREATE_CLIENT_REPORT:
$job->action_name = ScheduledJob::CREATE_CLIENT_REPORT;
$job->action_class = $this->getClassPath(ClientExport::class);
$job->parameters = $this->runValidation(GenericReportRequest::class, $request->all());
break;
case ScheduledJob::CREATE_CLIENT_CONTACT_REPORT:
$job->action_name = ScheduledJob::CREATE_CLIENT_CONTACT_REPORT;
$job->action_class = $this->getClassPath(ContactExport::class);
$job->parameters = $this->runValidation(GenericReportRequest::class, $request->all());
break;
case ScheduledJob::CREATE_CREDIT_REPORT:
$job->action_name = ScheduledJob::CREATE_CREDIT_REPORT;
$job->action_class = $this->getClassPath(CreditExport::class);
$job->parameters = $this->runValidation(GenericReportRequest::class, $request->all());
break;
case ScheduledJob::CREATE_DOCUMENT_REPORT:
$job->action_name = ScheduledJob::CREATE_DOCUMENT_REPORT;
$job->action_class = $this->getClassPath(DocumentExport::class);
$job->parameters = $this->runValidation(GenericReportRequest::class, $request->all());
break;
case ScheduledJob::CREATE_EXPENSE_REPORT:
$job->action_name = ScheduledJob::CREATE_EXPENSE_REPORT;
$job->action_class = $this->getClassPath(ExpenseExport::class);
$job->parameters = $this->runValidation(GenericReportRequest::class, $request->all());
break;
case ScheduledJob::CREATE_INVOICE_ITEM_REPORT:
$job->action_name = ScheduledJob::CREATE_INVOICE_ITEM_REPORT;
$job->action_class = $this->getClassPath(InvoiceItemExport::class);
$job->parameters = $this->runValidation(GenericReportRequest::class, $request->all());
break;
case ScheduledJob::CREATE_INVOICE_REPORT:
$job->action_name = ScheduledJob::CREATE_INVOICE_REPORT;
$job->action_class = $this->getClassPath(InvoiceExport::class);
$job->parameters = $this->runValidation(GenericReportRequest::class, $request->all());
break;
case ScheduledJob::CREATE_PAYMENT_REPORT:
$job->action_name = ScheduledJob::CREATE_PAYMENT_REPORT;
$job->action_class = $this->getClassPath(PaymentExport::class);
$job->parameters = $this->runValidation(GenericReportRequest::class, $request->all());
break;
case ScheduledJob::CREATE_PRODUCT_REPORT:
$job->action_name = ScheduledJob::CREATE_PRODUCT_REPORT;
$job->action_class = $this->getClassPath(ProductExport::class);
$job->parameters = $this->runValidation(GenericReportRequest::class, $request->all());
break;
case ScheduledJob::CREATE_PROFIT_AND_LOSS_REPORT:
$job->action_name = ScheduledJob::CREATE_PROFIT_AND_LOSS_REPORT;
$job->action_class = $this->getClassPath(ProfitAndLoss::class);
$job->parameters = $this->runValidation(GenericReportRequest::class, $request->all());
break;
case ScheduledJob::CREATE_QUOTE_ITEM_REPORT:
$job->action_name = ScheduledJob::CREATE_QUOTE_ITEM_REPORT;
$job->action_class = $this->getClassPath(QuoteItemExport::class);
$job->parameters = $this->runValidation(GenericReportRequest::class, $request->all());
break;
case ScheduledJob::CREATE_QUOTE_REPORT:
$job->action_name = ScheduledJob::CREATE_QUOTE_REPORT;
$job->action_class = $this->getClassPath(QuoteExport::class);
$job->parameters = $this->runValidation(GenericReportRequest::class, $request->all());
break;
case ScheduledJob::CREATE_RECURRING_INVOICE_REPORT:
$job->action_name = ScheduledJob::CREATE_RECURRING_INVOICE_REPORT;
$job->action_class = $this->getClassPath(RecurringInvoiceExport::class);
$job->parameters = $this->runValidation(GenericReportRequest::class, $request->all());
break;
case ScheduledJob::CREATE_TASK_REPORT:
$job->action_name = ScheduledJob::CREATE_TASK_REPORT;
$job->action_class = $this->getClassPath(TaskExport::class);
$job->parameters = $this->runValidation(GenericReportRequest::class, $request->all());
break;
}
return $job;
}
public function getClassPath($class): string
{
return $class = is_object($class) ? get_class($class) : $class;
}
public function updateJob(Scheduler $scheduler, UpdateScheduledJobRequest $request)
{
$job = $scheduler->job;
if (!$job) {
return abort(404);
}
$job = $this->setJobParameters($job, $request);
$job->save();
}
}

View File

@ -13,6 +13,10 @@ namespace App\Transformers;
use App\Models\Activity; use App\Models\Activity;
use App\Models\Backup; use App\Models\Backup;
use App\Models\ClientContact;
use App\Models\Invoice;
use App\Models\Task;
use App\Models\User;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
class ActivityTransformer extends EntityTransformer class ActivityTransformer extends EntityTransformer
@ -25,7 +29,17 @@ class ActivityTransformer extends EntityTransformer
* @var array * @var array
*/ */
protected $availableIncludes = [ protected $availableIncludes = [
'history' 'history',
'user',
'client',
'contact',
'recurring_invoice',
'invoice',
'credit',
'quote',
'payment',
'expense',
'task',
]; ];
/** /**
@ -66,4 +80,75 @@ class ActivityTransformer extends EntityTransformer
return $this->includeItem($activity->backup, $transformer, Backup::class); return $this->includeItem($activity->backup, $transformer, Backup::class);
} }
public function includeClient(Activity $activity)
{
$transformer = new ClientTransformer($this->serializer);
return $this->includeItem($activity->client, $transformer, Client::class);
}
public function includeContact(Activity $activity)
{
$transformer = new ClientContactTransformer($this->serializer);
return $this->includeItem($activity->contact, $transformer, ClientContact::class);
}
public function includeRecurringInvoice(Activity $activity)
{
$transformer = new RecurringInvoiceTransformer($this->serializer);
return $this->includeItem($activity->recurring_invoice, $transformer, RecurringInvoice::class);
}
public function includeQuote(Activity $activity)
{
$transformer = new RecurringInvoiceTransformer($this->serializer);
return $this->includeItem($activity->quote, $transformer, Quote::class);
}
public function includeInvoice(Activity $activity)
{
$transformer = new InvoiceTransformer($this->serializer);
return $this->includeItem($activity->invoice, $transformer, Invoice::class);
}
public function includeCredit(Activity $activity)
{
$transformer = new CreditTransformer($this->serializer);
return $this->includeItem($activity->credit, $transformer, Credit::class);
}
public function includePayment(Activity $activity)
{
$transformer = new PaymentTransformer($this->serializer);
return $this->includeItem($activity->payment, $transformer, Payment::class);
}
public function includeUser(Activity $activity)
{
$transformer = new UserTransformer($this->serializer);
return $this->includeItem($activity->user, $transformer, User::class);
}
public function includeExpense(Activity $activity)
{
$transformer = new ExpenseTransformer($this->serializer);
return $this->includeItem($activity->expense, $transformer, Expense::class);
}
public function includeTask(Activity $activity)
{
$transformer = new TaskTransformer($this->serializer);
return $this->includeItem($activity->task, $transformer, Task::class);
}
} }

View File

@ -0,0 +1,30 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Transformers;
use App\Models\ScheduledJob;
use App\Utils\Traits\MakesHash;
class ScheduledJobTransformer extends EntityTransformer
{
use MakesHash;
public function transform(ScheduledJob $job)
{
return [
'id' => $this->encodePrimaryKey($job->id),
'action_name' => (string)$job->action_name,
'parameters' => (array)$job->parameters
];
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Transformers;
use App\Models\ScheduledJob;
use App\Models\Scheduler;
use App\Utils\Traits\MakesHash;
class TaskSchedulerTransformer extends EntityTransformer
{
use MakesHash;
protected $defaultIncludes = [
'job'
];
public function includeJob(Scheduler $scheduler)
{
$transformer = new ScheduledJobTransformer($this->serializer);
return $this->item($scheduler->job, $transformer, ScheduledJob::class);
}
public function transform(Scheduler $scheduler)
{
return [
'id' => $this->encodePrimaryKey($scheduler->id),
'is_deleted' => (bool)$scheduler->is_deleted,
'paused' => (bool)$scheduler->paused,
'repeat_every' => (string)$scheduler->repeat_every,
'start_from' => (int)$scheduler->start_from,
'scheduled_run' => (int)$scheduler->scheduled_run,
'updated_at' => (int)$scheduler->updated_at,
'created_at' => (int)$scheduler->created_at,
'archived_at' => (int) $scheduler->deleted_at,
];
}
}

View File

@ -18,7 +18,7 @@ trait PageNumbering
private function pageNumbering($pdf_data_object, $company) private function pageNumbering($pdf_data_object, $company)
{ {
if(!$company->settings->page_numbering) if(!property_exists($company->settings, 'page_numbering') || !$company->settings->page_numbering)
return $pdf_data_object; return $pdf_data_object;
try try

View File

@ -14,8 +14,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' => '5.3.92', 'app_version' => '5.3.93',
'app_tag' => '5.3.92', 'app_tag' => '5.3.93',
'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', ''),
@ -187,7 +187,7 @@ return [
'ninja_apple_api_key' => env('APPLE_API_KEY', false), 'ninja_apple_api_key' => env('APPLE_API_KEY', false),
'ninja_apple_private_key' => env('APPLE_PRIVATE_KEY', false), 'ninja_apple_private_key' => env('APPLE_PRIVATE_KEY', false),
'ninja_apple_bundle_id' => env('APPLE_BUNDLE_ID', false), 'ninja_apple_bundle_id' => env('APPLE_BUNDLE_ID', false),
'ninja_apple_issuer_id' => env('APPLE_ISSUER_ID', false) 'ninja_apple_issuer_id' => env('APPLE_ISSUER_ID', false),
'react_app_enabled' => env('REACT_APP_ENABLED', false),
]; ];

View File

@ -0,0 +1,46 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateScheduledJobsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('scheduled_jobs', function (Blueprint $table) {
$table->id();
$table->string('action_name');
$table->string('action_class');
$table->json('parameters')->nullable();
$table->foreignIdFor(\App\Models\Company::class);
$table->foreignIdFor(\App\Models\Scheduler::class);
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('scheduled_jobs');
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateSchedulersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('schedulers', function (Blueprint $table) {
$table->id();
$table->boolean('paused')->default(false);
$table->boolean('is_deleted')->default(false);
$table->string('repeat_every');
$table->timestamp('start_from');
$table->timestamp('scheduled_run');
$table->foreignIdFor(\App\Models\Company::class);
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('schedulers');
}
}

View File

@ -3,13 +3,13 @@ const MANIFEST = 'flutter-app-manifest';
const TEMP = 'flutter-temp-cache'; const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache'; const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = { const RESOURCES = {
"main.dart.js": "b552020eead077b50b54a51de32ba513", "main.dart.js": "208309d12730fc06c9bf9ef0bd0c2483",
"canvaskit/canvaskit.wasm": "4b83d89d9fecbea8ca46f2f760c5a9ba", "canvaskit/canvaskit.wasm": "4b83d89d9fecbea8ca46f2f760c5a9ba",
"canvaskit/profiling/canvaskit.wasm": "95e736ab31147d1b2c7b25f11d4c32cd", "canvaskit/profiling/canvaskit.wasm": "95e736ab31147d1b2c7b25f11d4c32cd",
"canvaskit/profiling/canvaskit.js": "ae2949af4efc61d28a4a80fffa1db900", "canvaskit/profiling/canvaskit.js": "ae2949af4efc61d28a4a80fffa1db900",
"canvaskit/canvaskit.js": "c2b4e5f3d7a3d82aed024e7249a78487", "canvaskit/canvaskit.js": "c2b4e5f3d7a3d82aed024e7249a78487",
"flutter.js": "0816e65a103ba8ba51b174eeeeb2cb67", "flutter.js": "0816e65a103ba8ba51b174eeeeb2cb67",
"/": "15b97e9f966c26d599d1d9c0ecd01354", "/": "d60cdb0e60692160d5f962b98b88e24d",
"favicon.png": "dca91c54388f52eded692718d5a98b8b", "favicon.png": "dca91c54388f52eded692718d5a98b8b",
"version.json": "3afb81924daf4f751571755436069115", "version.json": "3afb81924daf4f751571755436069115",
"assets/AssetManifest.json": "38d9aea341601f3a5c6fa7b5a1216ea5", "assets/AssetManifest.json": "38d9aea341601f3a5c6fa7b5a1216ea5",

151368
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

154836
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

147746
public/main.html.dart.js vendored

File diff suppressed because one or more lines are too long

154760
public/main.next.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

View File

@ -794,12 +794,12 @@ $LANG = array(
'activity_45' => ':user deleted task :task', 'activity_45' => ':user deleted task :task',
'activity_46' => ':user restored task :task', 'activity_46' => ':user restored task :task',
'activity_47' => ':user updated expense :expense', 'activity_47' => ':user updated expense :expense',
'activity_48' => ':user updated ticket :ticket', 'activity_48' => ':user created user :user',
'activity_49' => ':user closed ticket :ticket', 'activity_49' => ':user updated user :user',
'activity_50' => ':user merged ticket :ticket', 'activity_50' => ':user archived user :user',
'activity_51' => ':user split ticket :ticket', 'activity_51' => ':user deleted user :user',
'activity_52' => ':contact opened ticket :ticket', 'activity_52' => ':user restored user :user',
'activity_53' => ':contact reopened ticket :ticket', 'activity_53' => ':user marked sent :invoice',
'activity_54' => ':user reopened ticket :ticket', 'activity_54' => ':user reopened ticket :ticket',
'activity_55' => ':contact replied ticket :ticket', 'activity_55' => ':contact replied ticket :ticket',
'activity_56' => ':user viewed ticket :ticket', 'activity_56' => ':user viewed ticket :ticket',
@ -4583,8 +4583,30 @@ $LANG = array(
'alternate_pdf_viewer' => 'Alternate PDF Viewer', 'alternate_pdf_viewer' => 'Alternate PDF Viewer',
'alternate_pdf_viewer_help' => 'Improve scrolling over the PDF preview [BETA]', 'alternate_pdf_viewer_help' => 'Improve scrolling over the PDF preview [BETA]',
'currency_cayman_island_dollar' => 'Cayman Island Dollar', 'currency_cayman_island_dollar' => 'Cayman Island Dollar',
'download_report_description' => 'Please see attached file to check your report.' 'download_report_description' => 'Please see attached file to check your report.',
'left' => 'Left',
'right' => 'Right',
'center' => 'Center',
'page_numbering' => 'Page Numbering',
'page_numbering_alignment' => 'Page Numbering Alignment',
'invoice_sent_notification_label' => 'Invoice Sent',
'show_product_description' => 'Show Product Description',
'show_product_description_help' => 'Include the description in the product dropdown',
'invoice_items' => 'Invoice Items',
'quote_items' => 'Quote Items',
'profitloss' => 'Profit and Loss',
'import_format' => 'Import Format',
'export_format' => 'Export Format',
'export_type' => 'Export Type',
'stop_on_unpaid' => 'Stop On Unpaid',
'stop_on_unpaid_help' => 'Stop creating recurring invoices if the last invoice is unpaid.',
'use_quote_terms' => 'Use Quote Terms',
'use_quote_terms_help' => 'When converting a quote to an invoice',
'add_country' => 'Add Country',
'enable_tooltips' => 'Enable Tooltips',
'enable_tooltips_help' => 'Show tooltips when hovering the mouse',
'multiple_client_error' => 'Error: records belong to more than one client',
'login_label' => 'Login to an existing account',
); );

View File

@ -0,0 +1,207 @@
<link rel="modulepreload" href="/react/nth-check.58e8385d.js">
<link rel="stylesheet" href="/react/index.bff791ad.css">
<link rel="modulepreload" href="/react/uuid.ef4f150d.js">
<link rel="modulepreload" href="/react/@emotion.9aa0086d.js">
<link rel="modulepreload" href="/react/react-select.0c11d83f.js">
<link rel="modulepreload" href="/react/space-separated-tokens.59bb4ca8.js">
<link rel="modulepreload" href="/react/raf-schd.43184c40.js">
<link rel="modulepreload" href="/react/recharts-scale.dc8389ce.js">
<link rel="modulepreload" href="/react/unist-util-position.04d68b2b.js">
<link rel="modulepreload" href="/react/reduce-css-calc.281532ee.js">
<link rel="modulepreload" href="/react/is-plain-obj.260ab441.js">
<link rel="modulepreload" href="/react/remark-rehype.66f21be3.js">
<link rel="modulepreload" href="/react/extend.ca41ef84.js">
<link rel="modulepreload" href="/react/micromark-extension-gfm-footnote.5e766f8c.js">
<link rel="modulepreload" href="/react/boolbase.bd705bc9.js">
<link rel="modulepreload" href="/react/react.967f52d5.js">
<link rel="modulepreload" href="/react/micromark-factory-whitespace.4e8e9fce.js">
<link rel="modulepreload" href="/react/react-router-dom.09ff5a14.js">
<script type="module" crossorigin src="/react/index.a4e53c8d.js"></script>
<link rel="modulepreload" href="/react/mdast-util-to-string.717718d5.js">
<link rel="modulepreload" href="/react/micromark-util-html-tag-name.8b7c83fe.js">
<link rel="modulepreload" href="/react/mdast-util-gfm-autolink-literal.4b04cd2e.js">
<link rel="stylesheet" href="/react/@uiw.d17b7851.css">
<link rel="modulepreload" href="/react/unist-util-is.bd39e4f2.js">
<link rel="modulepreload" href="/react/react-qr-code.4878cb6b.js">
<link rel="modulepreload" href="/react/web-namespaces.a24d7a09.js">
<link rel="modulepreload" href="/react/micromark-util-sanitize-uri.f9c714eb.js">
<link rel="modulepreload" href="/react/micromark-extension-gfm-task-list-item.33d9f10c.js">
<link rel="modulepreload" href="/react/html-parse-stringify.32417b46.js">
<link rel="modulepreload" href="/react/i18next.cf61342d.js">
<link rel="modulepreload" href="/react/html-escaper.7cd36ce3.js">
<link rel="modulepreload" href="/react/lodash-es.8df7cfb3.js">
<link rel="modulepreload" href="/react/d3-shape.c407f78b.js">
<link rel="modulepreload" href="/react/tslib.1cf50020.js">
<link rel="modulepreload" href="/react/use-memo-one.4a0d5bdc.js">
<link rel="modulepreload" href="/react/qr.js.da7c6dcd.js">
<link rel="modulepreload" href="/react/mdast-util-gfm.0152e39c.js">
<link rel="modulepreload" href="/react/is-decimal.247a88b7.js">
<link rel="modulepreload" href="/react/react-hot-toast.80758c68.js">
<link rel="modulepreload" href="/react/react-popper.d301c4dd.js">
<link rel="modulepreload" href="/react/react-debounce-input.1347744c.js">
<link rel="modulepreload" href="/react/d3-format.5a3f6ba7.js">
<link rel="modulepreload" href="/react/react-markdown.3a061863.js">
<link rel="modulepreload" href="/react/rehype-rewrite.cb5dac09.js">
<link rel="modulepreload" href="/react/attr-accept.6d6aa28d.js">
<link rel="modulepreload" href="/react/micromark-extension-gfm.3af3365b.js">
<link rel="modulepreload" href="/react/@popperjs.50883750.js">
<link rel="modulepreload" href="/react/warning.3e618ed9.js">
<link rel="modulepreload" href="/react/immer.a08a4913.js">
<link rel="modulepreload" href="/react/micromark-util-encode.b78670b5.js">
<link rel="modulepreload" href="/react/micromark-util-character.811e4fc8.js">
<link rel="modulepreload" href="/react/property-information.17621e99.js">
<link rel="modulepreload" href="/react/formik.0c2f9d9e.js">
<link rel="modulepreload" href="/react/character-reference-invalid.9429e209.js">
<link rel="modulepreload" href="/react/hast-util-to-parse5.d0422f4e.js">
<link rel="modulepreload" href="/react/prop-types.c5f1cd8f.js">
<link rel="modulepreload" href="/react/currency.js.cd0659e5.js">
<link rel="modulepreload" href="/react/redux.acdf7418.js">
<link rel="modulepreload" href="/react/react-feather.ba7315c7.js">
<link rel="modulepreload" href="/react/decode-named-character-reference.58fdd984.js">
<link rel="modulepreload" href="/react/raf.ef427214.js">
<link rel="modulepreload" href="/react/react-redux.7269b315.js">
<link rel="modulepreload" href="/react/ccount.24f4fb2a.js">
<link rel="modulepreload" href="/react/react-resize-detector.73849032.js">
<link rel="modulepreload" href="/react/hast-util-heading-rank.0127330c.js">
<link rel="modulepreload" href="/react/character-entities-html4.bfdecac8.js">
<link rel="modulepreload" href="/react/unist-util-visit.551af7f6.js">
<link rel="modulepreload" href="/react/is-alphabetical.a64573e0.js">
<link rel="modulepreload" href="/react/prismjs.1fc2e6a8.js">
<link rel="modulepreload" href="/react/comma-separated-tokens.826a3e44.js">
<link rel="modulepreload" href="/react/react-dropzone.87de0271.js">
<link rel="modulepreload" href="/react/parse-entities.0d3eeb8f.js">
<link rel="modulepreload" href="/react/unist-util-stringify-position.6cdff1ba.js">
<link rel="modulepreload" href="/react/hast-util-from-parse5.1bc31bb6.js">
<link rel="modulepreload" href="/react/lodash.debounce.bf978eb6.js">
<link rel="modulepreload" href="/react/rehype-prism-plus.2cab531a.js">
<link rel="modulepreload" href="/react/react-onclickoutside.05557887.js">
<link rel="modulepreload" href="/react/hast-util-select.9295ace7.js">
<link rel="modulepreload" href="/react/zwitch.84c833f3.js">
<link rel="modulepreload" href="/react/css-unit-converter.d617fa75.js">
<link rel="modulepreload" href="/react/rehype-autolink-headings.4312e6cb.js">
<link rel="modulepreload" href="/react/micromark-extension-gfm-autolink-literal.75d97f6e.js">
<link rel="modulepreload" href="/react/vfile.8a59bc59.js">
<link rel="modulepreload" href="/react/hoist-non-react-statics.cc69b77f.js">
<link rel="modulepreload" href="/react/collect.js.64c89be2.js">
<link rel="modulepreload" href="/react/react-beautiful-dnd.1242ed17.js">
<link rel="modulepreload" href="/react/micromark-extension-gfm-tagfilter.97dd1d78.js">
<link rel="modulepreload" href="/react/react-dom.bbdc7aed.js">
<link rel="modulepreload" href="/react/micromark-core-commonmark.894384bd.js">
<link rel="modulepreload" href="/react/mdast-util-gfm-footnote.9832ece2.js">
<link rel="modulepreload" href="/react/micromark-util-normalize-identifier.f045ed13.js">
<link rel="modulepreload" href="/react/array-move.20870289.js">
<link rel="modulepreload" href="/react/lodash.6e1b81d9.js">
<link rel="modulepreload" href="/react/date-fns.c9928d0b.js">
<link rel="modulepreload" href="/react/mdast-util-from-markdown.b6500a0a.js">
<link rel="modulepreload" href="/react/eventemitter3.e831f4a3.js">
<link rel="modulepreload" href="/react/is-alphanumerical.39e15300.js">
<link rel="modulepreload" href="/react/hastscript.aa96013f.js">
<link rel="modulepreload" href="/react/unist-util-generated.22d80e74.js">
<link rel="modulepreload" href="/react/remark-gfm.f031f689.js">
<link rel="modulepreload" href="/react/remark-parse.a34fb5cc.js">
<link rel="modulepreload" href="/react/axios.308741bb.js">
<link rel="modulepreload" href="/react/character-entities-legacy.cb27298c.js">
<link rel="modulepreload" href="/react/parse5.761ebfb8.js">
<link rel="modulepreload" href="/react/void-elements.fa8a9e47.js">
<link rel="modulepreload" href="/react/scheduler.84e669cb.js">
<link rel="modulepreload" href="/react/markdown-table.f01db7c9.js">
<link rel="modulepreload" href="/react/mdast-util-gfm-strikethrough.4389f7f3.js">
<link rel="stylesheet" href="/react/react-datepicker.e2be669b.css">
<link rel="modulepreload" href="/react/micromark-factory-label.2e0fc916.js">
<link rel="modulepreload" href="/react/micromark-extension-gfm-strikethrough.39c8bc63.js">
<link rel="modulepreload" href="/react/vfile-message.9560db1f.js">
<link rel="modulepreload" href="/react/mdast-util-to-markdown.7e1348ca.js">
<link rel="modulepreload" href="/react/redux-thunk.281dc3fb.js">
<link rel="modulepreload" href="/react/is-buffer.f91fab60.js">
<link rel="modulepreload" href="/react/rehype-raw.4c3d8b62.js">
<link rel="modulepreload" href="/react/d3-array.1895b022.js">
<link rel="modulepreload" href="/react/parse-numeric-range.fd682744.js">
<link rel="modulepreload" href="/react/hast-util-parse-selector.1be1f27a.js">
<link rel="modulepreload" href="/react/refractor.9012dc30.js">
<link rel="modulepreload" href="/react/css-box-model.54a3012b.js">
<link rel="modulepreload" href="/react/tiny-invariant.817bfd36.js">
<link rel="modulepreload" href="/react/object-assign.f7ed182d.js">
<link rel="modulepreload" href="/react/reselect.68dc670d.js">
<link rel="modulepreload" href="/react/rehype-slug.8f0b0db8.js">
<link rel="modulepreload" href="/react/resize-observer-polyfill.2e10476d.js">
<link rel="modulepreload" href="/react/micromark-factory-title.f4e107a2.js">
<link rel="modulepreload" href="/react/mdast-util-gfm-table.f1729384.js">
<link rel="modulepreload" href="/react/react-query.15f431cf.js">
<link rel="modulepreload" href="/react/react-i18next.b628690b.js">
<link rel="modulepreload" href="/react/micromark-factory-destination.08e32d90.js">
<link rel="modulepreload" href="/react/d3-scale.40adf854.js">
<link rel="modulepreload" href="/react/hast-util-to-string.03a4649a.js">
<link rel="modulepreload" href="/react/d3-path.a0f08300.js">
<link rel="modulepreload" href="/react/hast-util-whitespace.b9acc724.js">
<link rel="modulepreload" href="/react/micromark-util-classify-character.bb02743f.js">
<link rel="modulepreload" href="/react/@babel.d4f90752.js">
<link rel="modulepreload" href="/react/performance-now.3ab08c2f.js">
<link rel="modulepreload" href="/react/d3-time.03d82efa.js">
<link rel="modulepreload" href="/react/react-smooth.ec2093ff.js">
<link rel="modulepreload" href="/react/stringify-entities.a3ae839a.js">
<link rel="modulepreload" href="/react/@sentry.c26c3197.js">
<link rel="modulepreload" href="/react/react-lifecycles-compat.8d6bbd76.js">
<link rel="modulepreload" href="/react/html-void-elements.d3b5aea3.js">
<link rel="modulepreload" href="/react/rehype-parse.74f2042d.js">
<link rel="modulepreload" href="/react/d3-color.69ee14e3.js">
<link rel="modulepreload" href="/react/micromark-util-subtokenize.0273ad72.js">
<link rel="modulepreload" href="/react/react-router.47f931a4.js">
<link rel="modulepreload" href="/react/deepmerge.a6dda5ef.js">
<link rel="modulepreload" href="/react/file-selector.6172b388.js">
<link rel="modulepreload" href="/react/mdast-util-gfm-task-list-item.36dc3006.js">
<link rel="modulepreload" href="/react/hast-to-hyperscript.55881f10.js">
<link rel="modulepreload" href="/react/react-fast-compare.d5a5a050.js">
<link rel="modulepreload" href="/react/fast-equals.9df4a03b.js">
<link rel="modulepreload" href="/react/tippy.js.1053d821.js">
<link rel="modulepreload" href="/react/@headlessui.f2f54a45.js">
<link rel="modulepreload" href="/react/bail.58fa4042.js">
<link rel="modulepreload" href="/react/vfile-location.85c89654.js">
<link rel="modulepreload" href="/react/recharts.38803f97.js">
<link rel="modulepreload" href="/react/unist-util-filter.14ea3396.js">
<link rel="modulepreload" href="/react/direction.5373b893.js">
<link rel="modulepreload" href="/react/micromark.31ec2ae2.js">
<link rel="modulepreload" href="/react/goober.56221fd8.js">
<link rel="modulepreload" href="/react/micromark-util-decode-string.d48b94e2.js">
<link rel="modulepreload" href="/react/react-datepicker.3e071b73.js">
<link rel="modulepreload" href="/react/hast-util-is-element.4ae8c712.js">
<link rel="modulepreload" href="/react/micromark-util-chunked.62b98efd.js">
<link rel="modulepreload" href="/react/mdurl.530e10e1.js">
<link rel="modulepreload" href="/react/mdast-util-find-and-replace.c8360cfe.js">
<link rel="modulepreload" href="/react/internmap.bf78dfdb.js">
<link rel="modulepreload" href="/react/tiny-warning.14328f40.js">
<link rel="modulepreload" href="/react/css-selector-parser.838f527e.js">
<link rel="modulepreload" href="/react/decimal.js-light.b1218db1.js">
<link rel="modulepreload" href="/react/hast-util-has-property.bf6e0cae.js">
<link rel="modulepreload" href="/react/unified.ac4b28d0.js">
<link rel="modulepreload" href="/react/@uiw.e53102d7.js">
<link rel="modulepreload" href="/react/micromark-util-combine-extensions.fc6008e7.js">
<link rel="modulepreload" href="/react/d3-time-format.0353a6ed.js">
<link rel="modulepreload" href="/react/style-to-object.74a4cfe6.js">
<link rel="modulepreload" href="/react/hast-util-raw.6bdb4dc9.js">
<link rel="modulepreload" href="/react/micromark-util-resolve-all.b4e652e2.js">
<link rel="modulepreload" href="/react/bcp-47-match.b7ba7942.js">
<link rel="modulepreload" href="/react/stylis.ba53937c.js">
<link rel="modulepreload" href="/react/mdast-util-definitions.fd07577f.js">
<link rel="modulepreload" href="/react/micromark-util-decode-numeric-character-reference.029edbd3.js">
<link rel="modulepreload" href="/react/unist-builder.67dd3ae6.js">
<link rel="modulepreload" href="/react/memoize-one.413f527d.js">
<link rel="modulepreload" href="/react/hast-util-to-html.54b58e35.js">
<link rel="modulepreload" href="/react/micromark-extension-gfm-table.f7983341.js">
<link rel="modulepreload" href="/react/unist-util-visit-parents.78083329.js">
<link rel="modulepreload" href="/react/github-slugger.144f2824.js">
<link rel="modulepreload" href="/react/dayjs.6e560245.js">
<link rel="modulepreload" href="/react/rehype.bee4df94.js">
<link rel="modulepreload" href="/react/history.8119d98b.js">
<link rel="modulepreload" href="/react/inline-style-parser.ec55a77b.js">
<link rel="modulepreload" href="/react/is-hexadecimal.d91cb013.js">
<link rel="modulepreload" href="/react/react-is.c6bf3c04.js">
<link rel="modulepreload" href="/react/rehype-attr.8040248d.js">
<link rel="modulepreload" href="/react/pretty-bytes.1ef697de.js">
<link rel="modulepreload" href="/react/micromark-factory-space.84551a41.js">
<link rel="modulepreload" href="/react/rehype-stringify.540b84fb.js">
<link rel="modulepreload" href="/react/d3-interpolate.f5146873.js">
<link rel="modulepreload" href="/react/mdast-util-to-hast.d1d8e0b2.js">
<link rel="modulepreload" href="/react/trough.714478ec.js">
<link rel="modulepreload" href="/react/@tippyjs.36ad76fa.js">
<link rel="modulepreload" href="/react/@reduxjs.9b745b7e.js">
<link rel="modulepreload" href="/react/classnames.02e8c2fa.js">

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html data-report-errors="{{ $report_errors }}" data-rc="{{ $rc }}" data-user-agent="{{ $user_agent }}" data-login="{{ $login }}">
<head>
<!-- Source: https://github.com/invoiceninja/invoiceninja -->
<!-- Version: {{ config('ninja.app_version') }} -->
<meta charset="UTF-8">
<title>{{ config('ninja.app_name') }}</title>
<meta name="google-signin-client_id" content="{{ config('services.google.client_id') }}">
@include('react.head')
</head>
<body class="h-full">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@ -170,6 +170,10 @@ Route::group(['middleware' => ['throttle:100,1', 'api_db', 'token_auth', 'locale
Route::post('reports/tasks', 'Reports\TaskReportController'); Route::post('reports/tasks', 'Reports\TaskReportController');
Route::post('reports/profitloss', 'Reports\ProfitAndLossController'); Route::post('reports/profitloss', 'Reports\ProfitAndLossController');
Route::resource('task_scheduler', 'TaskSchedulerController')->except('edit')->parameters(['task_scheduler' => 'scheduler']);
Route::put('task_scheduler/{scheduler}/update_job','TaskSchedulerController@updateJob');
Route::get('scheduler', 'SchedulerController@index'); Route::get('scheduler', 'SchedulerController@index');
Route::post('support/messages/send', 'Support\Messages\SendingController'); Route::post('support/messages/send', 'Support\Messages\SendingController');

View File

@ -0,0 +1,162 @@
<?php
namespace Tests\Feature\Scheduler;
use App\Export\CSV\ClientExport;
use App\Models\ScheduledJob;
use App\Models\Scheduler;
use App\Utils\Traits\MakesHash;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutEvents;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\Session;
use Tests\MockUnitData;
use Tests\TestCase;
use Illuminate\Validation\ValidationException;
class SchedulerTest extends TestCase
{
use MakesHash;
use MockUnitData;
use WithoutEvents;
// use RefreshDatabase;
public function setUp(): void
{
parent::setUp();
Session::start();
$this->faker = \Faker\Factory::create();
Model::reguard();
$this->makeTestData();
$this->withoutMiddleware(
ThrottleRequests::class
);
// $this->withoutExceptionHandling();
}
public function testSchedulerCantBeCreatedWithWrongData()
{
$data = [
'repeat_every' => Scheduler::DAILY,
'job' => ScheduledJob::CREATE_CLIENT_REPORT,
'date_key' => '123',
'report_keys' => ['test'],
'date_range' => 'all',
// 'start_from' => '2022-01-01'
];
$response = false;
// try {
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/task_scheduler/', $data);
// } catch (ValidationException $e) {
// $message = json_decode($e->validator->getMessageBag(), 1);
// nlog($message);
// }
// $response->assertStatus(200);
$response->assertSessionHasErrors();
}
public function testSchedulerCanBeUpdated()
{
$response = $this->createScheduler();
$arr = $response->json();
$id = $arr['data']['id'];
$scheduler = Scheduler::find($this->decodePrimaryKey($id));
$updateData = [
'start_from' => 1655934741
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->put('/api/v1/task_scheduler/' . $this->encodePrimaryKey($scheduler->id), $updateData);
$responseData = $response->json();
$this->assertEquals($updateData['start_from'], $responseData['data']['start_from']);
}
public function testSchedulerCanBeSeen()
{
$response = $this->createScheduler();
$arr = $response->json();
$id = $arr['data']['id'];
$scheduler = Scheduler::find($this->decodePrimaryKey($id));
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->get('/api/v1/task_scheduler/' . $this->encodePrimaryKey($scheduler->id));
$arr = $response->json();
$this->assertEquals('create_client_report', $arr['data']['job']['action_name']);
}
public function testSchedulerJobCanBeUpdated()
{
$response = $this->createScheduler();
$arr = $response->json();
$id = $arr['data']['id'];
$scheduler = Scheduler::find($this->decodePrimaryKey($id));
$this->assertSame('create_client_report', $scheduler->job->action_name);
$updateData = [
'job' => ScheduledJob::CREATE_CREDIT_REPORT,
'date_range' => 'all',
'report_keys' => ['test1']
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->put('/api/v1/task_scheduler/' . $this->encodePrimaryKey($scheduler->id) . '/update_job', $updateData);
$updatedSchedulerJob = Scheduler::first()->job->action_name;
$arr = $response->json();
$this->assertSame('create_credit_report', $arr['data']['job']['action_name']);
}
public function createScheduler()
{
$data = [
'repeat_every' => Scheduler::DAILY,
'job' => ScheduledJob::CREATE_CLIENT_REPORT,
'date_key' => '123',
'report_keys' => ['test'],
'date_range' => 'all',
'start_from' => '2022-01-01'
];
return $response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/task_scheduler/', $data);
}
}