diff --git a/.gitignore b/.gitignore index f3b06ceeb9..eba40da49d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ Thumbs.db /.project tests/_output/ tests/_bootstrap.php +tests/_support/_generated/ # composer stuff /c3.php diff --git a/.travis.yml b/.travis.yml index 6bd3c485a8..727f351d7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -65,6 +65,7 @@ before_script: - sleep 5 # Make sure the app is up-to-date - curl -L http://ninja.dev:8000/update + #- php artisan ninja:create-test-data 25 script: - php ./vendor/codeception/codeception/codecept run --debug acceptance AllPagesCept.php diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 6100d090e1..0000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,36 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. -This project adheres to [Semantic Versioning](http://semver.org/). - - -## [Unreleased] - -### Changed -- Auto billing uses credits if they exist - - -## [2.6.4] - 2016-07-19 - -### Added -- Added 'Buy Now' buttons - -### Fixed -- Setting default tax rate breaks invoice creation #974 - - -## [2.6] - 2016-07-12 - -### Added -- Configuration for first day of the week #950 -- StyleCI configuration #929 -- Added expense category - -### Changed -- Removed `invoiceninja.komodoproject` from Git #932 -- `APP_CIPHER` changed from `rinjdael-128` to `AES-256-CBC` #898 -- Improved options when exporting data - -### Fixed -- "Manual entry" untranslatable #562 -- Using a database table prefix breaks the dashboard #203 -- Request statically called in StartupCheck.php #977 diff --git a/app/Console/Commands/CheckData.php b/app/Console/Commands/CheckData.php index d81bf5ff6e..ae37a429f4 100644 --- a/app/Console/Commands/CheckData.php +++ b/app/Console/Commands/CheckData.php @@ -1,6 +1,7 @@ info(date('Y-m-d') . ' Running CheckData...'); + $this->logMessage(date('Y-m-d') . ' Running CheckData...'); if (!$this->option('client_id')) { $this->checkPaidToDate(); @@ -66,7 +70,21 @@ class CheckData extends Command { $this->checkAccountData(); } - $this->info('Done'); + $this->logMessage('Done'); + $errorEmail = env('ERROR_EMAIL'); + + if ( ! $this->isValid && $errorEmail) { + Mail::raw($this->log, function ($message) use ($errorEmail) { + $message->to($errorEmail) + ->from(CONTACT_EMAIL) + ->subject('Check-Data'); + }); + } + } + + private function logMessage($str) + { + $this->log .= $str . "\n"; } private function checkBlankInvoiceHistory() @@ -75,9 +93,14 @@ class CheckData extends Command { ->where('activity_type_id', '=', 5) ->where('json_backup', '=', '') ->whereNotIn('id', [634386, 756352, 756353, 756356]) + ->whereNotIn('id', [634386, 756352, 756353, 756356, 820872]) ->count(); - $this->info($count . ' activities with blank invoice backup'); + if ($count > 0) { + $this->isValid = false; + } + + $this->logMessage($count . ' activities with blank invoice backup'); } private function checkAccountData() @@ -132,7 +155,8 @@ class CheckData extends Command { ->get(["{$table}.id", 'clients.account_id', 'clients.user_id']); if (count($records)) { - $this->info(count($records) . " {$table} records with incorrect {$entityType} account id"); + $this->isValid = false; + $this->logMessage(count($records) . " {$table} records with incorrect {$entityType} account id"); if ($this->option('fix') == 'true') { foreach ($records as $record) { @@ -162,7 +186,11 @@ class CheckData extends Command { ->groupBy('clients.id') ->havingRaw('clients.paid_to_date != sum(payments.amount - payments.refunded) and clients.paid_to_date != 999999999.9999') ->get(['clients.id', 'clients.paid_to_date', DB::raw('sum(payments.amount) as amount')]); - $this->info(count($clients) . ' clients with incorrect paid to date'); + $this->logMessage(count($clients) . ' clients with incorrect paid to date'); + + if (count($clients) > 0) { + $this->isValid = false; + } if ($this->option('fix') == 'true') { foreach ($clients as $client) { @@ -179,6 +207,7 @@ class CheckData extends Command { $clients = DB::table('clients') ->join('invoices', 'invoices.client_id', '=', 'clients.id') ->join('accounts', 'accounts.id', '=', 'clients.account_id') + ->where('accounts.id', '!=', 20432) ->where('clients.is_deleted', '=', 0) ->where('invoices.is_deleted', '=', 0) ->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD) @@ -192,10 +221,14 @@ class CheckData extends Command { $clients = $clients->groupBy('clients.id', 'clients.balance', 'clients.created_at') ->orderBy('accounts.company_id', 'DESC') ->get(['accounts.company_id', 'clients.account_id', 'clients.id', 'clients.balance', 'clients.paid_to_date', DB::raw('sum(invoices.balance) actual_balance')]); - $this->info(count($clients) . ' clients with incorrect balance/activities'); + $this->logMessage(count($clients) . ' clients with incorrect balance/activities'); + + if (count($clients) > 0) { + $this->isValid = false; + } foreach ($clients as $client) { - $this->info("=== Company: {$client->company_id} Account:{$client->account_id} Client:{$client->id} Balance:{$client->balance} Actual Balance:{$client->actual_balance} ==="); + $this->logMessage("=== Company: {$client->company_id} Account:{$client->account_id} Client:{$client->id} Balance:{$client->balance} Actual Balance:{$client->actual_balance} ==="); $foundProblem = false; $lastBalance = 0; $lastAdjustment = 0; @@ -205,7 +238,7 @@ class CheckData extends Command { ->where('client_id', '=', $client->id) ->orderBy('activities.id') ->get(['activities.id', 'activities.created_at', 'activities.activity_type_id', 'activities.adjustment', 'activities.balance', 'activities.invoice_id']); - //$this->info(var_dump($activities)); + //$this->logMessage(var_dump($activities)); foreach ($activities as $activity) { @@ -252,19 +285,19 @@ class CheckData extends Command { // **Fix for ninja invoices which didn't have the invoice_type_id value set if ($noAdjustment && $client->account_id == 20432) { - $this->info("No adjustment for ninja invoice"); + $this->logMessage("No adjustment for ninja invoice"); $foundProblem = true; $clientFix += $invoice->amount; $activityFix = $invoice->amount; // **Fix for allowing converting a recurring invoice to a normal one without updating the balance** } elseif ($noAdjustment && $invoice->invoice_type_id == INVOICE_TYPE_STANDARD && !$invoice->is_recurring) { - $this->info("No adjustment for new invoice:{$activity->invoice_id} amount:{$invoice->amount} invoiceTypeId:{$invoice->invoice_type_id} isRecurring:{$invoice->is_recurring}"); + $this->logMessage("No adjustment for new invoice:{$activity->invoice_id} amount:{$invoice->amount} invoiceTypeId:{$invoice->invoice_type_id} isRecurring:{$invoice->is_recurring}"); $foundProblem = true; $clientFix += $invoice->amount; $activityFix = $invoice->amount; // **Fix for updating balance when creating a quote or recurring invoice** } elseif ($activity->adjustment != 0 && ($invoice->invoice_type_id == INVOICE_TYPE_QUOTE || $invoice->is_recurring)) { - $this->info("Incorrect adjustment for new invoice:{$activity->invoice_id} adjustment:{$activity->adjustment} invoiceTypeId:{$invoice->invoice_type_id} isRecurring:{$invoice->is_recurring}"); + $this->logMessage("Incorrect adjustment for new invoice:{$activity->invoice_id} adjustment:{$activity->adjustment} invoiceTypeId:{$invoice->invoice_type_id} isRecurring:{$invoice->is_recurring}"); $foundProblem = true; $clientFix -= $activity->adjustment; $activityFix = 0; @@ -272,7 +305,7 @@ class CheckData extends Command { } elseif ($activity->activity_type_id == ACTIVITY_TYPE_DELETE_INVOICE) { // **Fix for updating balance when deleting a recurring invoice** if ($activity->adjustment != 0 && $invoice->is_recurring) { - $this->info("Incorrect adjustment for deleted invoice adjustment:{$activity->adjustment}"); + $this->logMessage("Incorrect adjustment for deleted invoice adjustment:{$activity->adjustment}"); $foundProblem = true; if ($activity->balance != $lastBalance) { $clientFix -= $activity->adjustment; @@ -282,7 +315,7 @@ class CheckData extends Command { } elseif ($activity->activity_type_id == ACTIVITY_TYPE_ARCHIVE_INVOICE) { // **Fix for updating balance when archiving an invoice** if ($activity->adjustment != 0 && !$invoice->is_recurring) { - $this->info("Incorrect adjustment for archiving invoice adjustment:{$activity->adjustment}"); + $this->logMessage("Incorrect adjustment for archiving invoice adjustment:{$activity->adjustment}"); $foundProblem = true; $activityFix = 0; $clientFix += $activity->adjustment; @@ -290,12 +323,12 @@ class CheckData extends Command { } elseif ($activity->activity_type_id == ACTIVITY_TYPE_UPDATE_INVOICE) { // **Fix for updating balance when updating recurring invoice** if ($activity->adjustment != 0 && $invoice->is_recurring) { - $this->info("Incorrect adjustment for updated recurring invoice adjustment:{$activity->adjustment}"); + $this->logMessage("Incorrect adjustment for updated recurring invoice adjustment:{$activity->adjustment}"); $foundProblem = true; $clientFix -= $activity->adjustment; $activityFix = 0; } else if ((strtotime($activity->created_at) - strtotime($lastCreatedAt) <= 1) && $activity->adjustment > 0 && $activity->adjustment == $lastAdjustment) { - $this->info("Duplicate adjustment for updated invoice adjustment:{$activity->adjustment}"); + $this->logMessage("Duplicate adjustment for updated invoice adjustment:{$activity->adjustment}"); $foundProblem = true; $clientFix -= $activity->adjustment; $activityFix = 0; @@ -303,7 +336,7 @@ class CheckData extends Command { } elseif ($activity->activity_type_id == ACTIVITY_TYPE_UPDATE_QUOTE) { // **Fix for updating balance when updating a quote** if ($activity->balance != $lastBalance) { - $this->info("Incorrect adjustment for updated quote adjustment:{$activity->adjustment}"); + $this->logMessage("Incorrect adjustment for updated quote adjustment:{$activity->adjustment}"); $foundProblem = true; $clientFix += $lastBalance - $activity->balance; $activityFix = 0; @@ -311,7 +344,7 @@ class CheckData extends Command { } else if ($activity->activity_type_id == ACTIVITY_TYPE_DELETE_PAYMENT) { // **Fix for deleting payment after deleting invoice** if ($activity->adjustment != 0 && $invoice->is_deleted && $activity->created_at > $invoice->deleted_at) { - $this->info("Incorrect adjustment for deleted payment adjustment:{$activity->adjustment}"); + $this->logMessage("Incorrect adjustment for deleted payment adjustment:{$activity->adjustment}"); $foundProblem = true; $activityFix = 0; $clientFix -= $activity->adjustment; @@ -340,7 +373,7 @@ class CheckData extends Command { } if ($activity->balance + $clientFix != $client->actual_balance) { - $this->info("** Creating 'recovered update' activity **"); + $this->logMessage("** Creating 'recovered update' activity **"); if ($this->option('fix') == 'true') { DB::table('activities')->insert([ 'created_at' => new Carbon, @@ -354,7 +387,7 @@ class CheckData extends Command { } $data = ['balance' => $client->actual_balance]; - $this->info("Corrected balance:{$client->actual_balance}"); + $this->logMessage("Corrected balance:{$client->actual_balance}"); if ($this->option('fix') == 'true') { DB::table('clients') ->where('id', $client->id) diff --git a/app/Events/TaskWasArchived.php b/app/Events/TaskWasArchived.php new file mode 100644 index 0000000000..74a7d1e4f6 --- /dev/null +++ b/app/Events/TaskWasArchived.php @@ -0,0 +1,28 @@ +task = $task; + } + +} diff --git a/app/Events/TaskWasDeleted.php b/app/Events/TaskWasDeleted.php new file mode 100644 index 0000000000..3e41c77ff6 --- /dev/null +++ b/app/Events/TaskWasDeleted.php @@ -0,0 +1,28 @@ +task = $task; + } + +} diff --git a/app/Events/TaskWasRestored.php b/app/Events/TaskWasRestored.php new file mode 100644 index 0000000000..58891f6e3d --- /dev/null +++ b/app/Events/TaskWasRestored.php @@ -0,0 +1,29 @@ +task = $task; + } + +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 9dfadde1b7..1935b0df7a 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -1,5 +1,7 @@ path() != 'get_started') { // https://gist.github.com/jrmadsen67/bd0f9ad0ef1ed6bb594e @@ -82,6 +84,40 @@ class Handler extends ExceptionHandler } } + if($this->isHttpException($e)) + { + switch ($e->getStatusCode()) + { + // not found + case 404: + if($request->header('X-Ninja-Token') != '') { + //API request which has hit a route which does not exist + + $error['error'] = ['message'=>'Route does not exist']; + $error = json_encode($error, JSON_PRETTY_PRINT); + $headers = Utils::getApiHeaders(); + + return response()->make($error, 404, $headers); + + } + break; + + // internal error + case '500': + if($request->header('X-Ninja-Token') != '') { + //API request which produces 500 error + + $error['error'] = ['message'=>'Internal Server Error']; + $error = json_encode($error, JSON_PRETTY_PRINT); + $headers = Utils::getApiHeaders(); + + return response()->make($error, 500, $headers); + } + break; + + } + } + // In production, except for maintenance mode, we'll show a custom error screen if (Utils::isNinjaProd() && !Utils::isDownForMaintenance() diff --git a/app/Http/Controllers/AccountApiController.php b/app/Http/Controllers/AccountApiController.php index a74536a3c1..05039ca99a 100644 --- a/app/Http/Controllers/AccountApiController.php +++ b/app/Http/Controllers/AccountApiController.php @@ -4,6 +4,9 @@ use Auth; use Utils; use Response; use Cache; +use Socialite; +use Exception; +use App\Services\AuthService; use App\Models\Account; use App\Ninja\Repositories\AccountRepository; use Illuminate\Http\Request; @@ -181,4 +184,30 @@ class AccountApiController extends BaseAPIController } } + + public function oauthLogin(Request $request) + { + $user = false; + $token = $request->input('token'); + $provider = $request->input('provider'); + + try { + $user = Socialite::driver($provider)->userFromToken($token); + } catch (Exception $exception) { + return $this->errorResponse(['message' => $exception->getMessage()], 401); + } + + if ($user) { + $providerId = AuthService::getProviderId($provider); + $user = $this->accountRepo->findUserByOauth($providerId, $user->id); + } + + if ($user) { + Auth::login($user); + return $this->processLogin($request); + } else { + sleep(ERROR_DELAY); + return $this->errorResponse(['message' => 'Invalid credentials'], 401); + } + } } diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 434cffe5e9..6f73cb19c3 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -1,6 +1,8 @@ getPlanDetails(false, false); $newPlan = [ @@ -193,7 +199,9 @@ class AccountController extends BaseController } } + $hasPaid = false; if (!empty($planDetails['paid']) && $plan != PLAN_FREE) { + $hasPaid = true; $time_used = $planDetails['paid']->diff(date_create()); $days_used = $time_used->days; @@ -209,7 +217,11 @@ class AccountController extends BaseController if ($newPlan['price'] > $credit) { $invitation = $this->accountRepo->enablePlan($newPlan, $credit); - return Redirect::to('view/' . $invitation->invitation_key); + if ($hasPaid) { + return Redirect::to('view/' . $invitation->invitation_key); + } else { + return Redirect::to('payment/' . $invitation->invitation_key); + } } else { if ($plan != PLAN_FREE) { @@ -417,6 +429,7 @@ class AccountController extends BaseController 'currencies' => Cache::get('currencies'), 'title' => trans('texts.localization'), 'weekdays' => Utils::getTranslatedWeekdayNames(), + 'months' => Utils::getMonthOptions(), ]; return View::make('accounts.localization', $data); @@ -458,10 +471,12 @@ class AccountController extends BaseController } return View::make('accounts.payments', [ - 'showAdd' => $count < count(Gateway::$alternate) + 1, - 'title' => trans('texts.online_payments'), + 'showAdd' => $count < count(Gateway::$alternate) + 1, + 'title' => trans('texts.online_payments'), 'tokenBillingOptions' => $tokenBillingOptions, - 'account' => $account, + 'currency' => Utils::getFromCache(Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY), + 'currencies'), + 'account' => $account, ]); } } @@ -471,16 +486,9 @@ class AccountController extends BaseController */ private function showProducts() { - $columns = ['product', 'description', 'unit_cost']; - if (Auth::user()->account->invoice_item_taxes) { - $columns[] = 'tax_rate'; - } - $columns[] = 'action'; - $data = [ 'account' => Auth::user()->account, 'title' => trans('texts.product_library'), - 'columns' => Utils::trans($columns), ]; return View::make('accounts.products', $data); @@ -667,11 +675,9 @@ class AccountController extends BaseController * @param $section * @return \Illuminate\Http\RedirectResponse */ - public function doSection($section = ACCOUNT_COMPANY_DETAILS) + public function doSection($section) { - if ($section === ACCOUNT_COMPANY_DETAILS) { - return AccountController::saveDetails(); - } elseif ($section === ACCOUNT_LOCALIZATION) { + if ($section === ACCOUNT_LOCALIZATION) { return AccountController::saveLocalization(); } elseif ($section == ACCOUNT_PAYMENTS) { return self::saveOnlinePayments(); @@ -697,9 +703,27 @@ class AccountController extends BaseController return AccountController::saveTaxRates(); } elseif ($section === ACCOUNT_PAYMENT_TERMS) { return AccountController::savePaymetTerms(); + } elseif ($section === ACCOUNT_MANAGEMENT) { + return AccountController::saveAccountManagement(); } } + /** + * @return \Illuminate\Http\RedirectResponse + */ + private function saveAccountManagement() + { + $account = Auth::user()->account; + $modules = Input::get('modules'); + + $account->enabled_modules = $modules ? array_sum($modules) : 0; + $account->save(); + + Session::flash('message', trans('texts.updated_settings')); + + return Redirect::to('settings/'.ACCOUNT_MANAGEMENT); + } + /** * @return \Illuminate\Http\RedirectResponse */ @@ -723,12 +747,7 @@ class AccountController extends BaseController private function saveClientPortal() { $account = Auth::user()->account; - - $account->enable_client_portal = !!Input::get('enable_client_portal'); - $account->enable_client_portal_dashboard = !!Input::get('enable_client_portal_dashboard'); - $account->enable_portal_password = !!Input::get('enable_portal_password'); - $account->send_portal_password = !!Input::get('send_portal_password'); - $account->enable_buy_now_buttons = !!Input::get('enable_buy_now_buttons'); + $account->fill(Input::all()); // Only allowed for pro Invoice Ninja users or white labeled self-hosted users if (Auth::user()->account->hasFeature(FEATURE_CLIENT_PORTAL_CSS)) { @@ -1200,6 +1219,7 @@ class AccountController extends BaseController $account->military_time = Input::get('military_time') ? true : false; $account->show_currency_code = Input::get('show_currency_code') ? true : false; $account->start_of_week = Input::get('start_of_week') ? Input::get('start_of_week') : 0; + $account->financial_year_start = Input::get('financial_year_start') ? Input::get('financial_year_start') : null; $account->save(); event(new UserSettingsChanged()); @@ -1226,6 +1246,35 @@ class AccountController extends BaseController return Redirect::to('settings/'.ACCOUNT_PAYMENTS); } + /** + * @return \Illuminate\Http\RedirectResponse + */ + public function savePaymentGatewayLimits() + { + $gateway_type_id = intval(Input::get('gateway_type_id')); + $gateway_settings = AccountGatewaySettings::scope()->where('gateway_type_id', '=', $gateway_type_id)->first(); + + if ( ! $gateway_settings) { + $gateway_settings = AccountGatewaySettings::createNew(); + $gateway_settings->gateway_type_id = $gateway_type_id; + } + + $gateway_settings->min_limit = Input::get('limit_min_enable') ? intval(Input::get('limit_min')) : null; + $gateway_settings->max_limit = Input::get('limit_max_enable') ? intval(Input::get('limit_max')) : null; + + if ($gateway_settings->max_limit !== null && $gateway_settings->min_limit > $gateway_settings->max_limit) { + $gateway_settings->max_limit = $gateway_settings->min_limit; + } + + $gateway_settings->save(); + + event(new UserSettingsChanged()); + + Session::flash('message', trans('texts.updated_settings')); + + return Redirect::to('settings/' . ACCOUNT_PAYMENTS); + } + /** * @return \Illuminate\Http\RedirectResponse */ diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index d0ce1b6f62..844cd21754 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -48,8 +48,10 @@ class AccountGatewayController extends BaseController $accountGateway = AccountGateway::scope($publicId)->firstOrFail(); $config = $accountGateway->getConfig(); - foreach ($config as $field => $value) { - $config->$field = str_repeat('*', strlen($value)); + if ($accountGateway->gateway_id != GATEWAY_CUSTOM) { + foreach ($config as $field => $value) { + $config->$field = str_repeat('*', strlen($value)); + } } $data = self::getViewModel($accountGateway); @@ -60,8 +62,6 @@ class AccountGatewayController extends BaseController $data['hiddenFields'] = Gateway::$hiddenFields; $data['selectGateways'] = Gateway::where('id', '=', $accountGateway->gateway_id)->get(); - $this->testGateway($accountGateway); - return View::make('accounts.account_gateway', $data); } @@ -100,7 +100,7 @@ class AccountGatewayController extends BaseController if ($otherProviders) { $availableGatewaysIds = $account->availableGatewaysIds(); - $data['primaryGateways'] = Gateway::primary($availableGatewaysIds)->orderBy('name', 'desc')->get(); + $data['primaryGateways'] = Gateway::primary($availableGatewaysIds)->orderBy('sort_order')->get(); $data['secondaryGateways'] = Gateway::secondary($availableGatewaysIds)->orderBy('name')->get(); $data['hiddenFields'] = Gateway::$hiddenFields; @@ -132,7 +132,9 @@ class AccountGatewayController extends BaseController foreach ($gateways as $gateway) { $fields = $gateway->getFields(); - asort($fields); + if ( ! $gateway->isCustom()) { + asort($fields); + } $gateway->fields = $gateway->id == GATEWAY_WEPAY ? [] : $fields; if ($accountGateway && $accountGateway->gateway_id == $gateway->id) { $accountGateway->fields = $gateway->fields; @@ -247,6 +249,8 @@ class AccountGatewayController extends BaseController } if (!$value && ($field == 'testMode' || $field == 'developerMode')) { // do nothing + } elseif ($gatewayId == GATEWAY_CUSTOM) { + $config->$field = strip_tags($value); } else { $config->$field = $value; } @@ -312,14 +316,17 @@ class AccountGatewayController extends BaseController if (isset($wepayResponse)) { return $wepayResponse; } else { + $this->testGateway($accountGateway); + if ($accountGatewayPublicId) { $message = trans('texts.updated_gateway'); + Session::flash('message', $message); + return Redirect::to("gateways/{$accountGateway->public_id}/edit"); } else { $message = trans('texts.created_gateway'); + Session::flash('message', $message); + return Redirect::to("/settings/online_payments"); } - - Session::flash('message', $message); - return Redirect::to("gateways/{$accountGateway->public_id}/edit"); } } } diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index c120661db7..c583f919b8 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -1,5 +1,6 @@ layout = View::make($this->layout); } } + + protected function returnBulk($entityType, $action, $ids) + { + if ( ! is_array($ids)) { + $ids = [$ids]; + } + + $isDatatable = filter_var(request()->datatable, FILTER_VALIDATE_BOOLEAN); + $entityTypes = Utils::pluralizeEntityType($entityType); + + if ($action == 'restore' && count($ids) == 1) { + return redirect("{$entityTypes}/" . $ids[0]); + } elseif ($isDatatable || ($action == 'archive' || $action == 'delete')) { + return redirect("{$entityTypes}"); + } elseif (count($ids)) { + return redirect("{$entityTypes}/" . $ids[0]); + } else { + return redirect("{$entityTypes}"); + } + } } diff --git a/app/Http/Controllers/ClientApiController.php b/app/Http/Controllers/ClientApiController.php index 918b9c701e..4f6ca6521c 100644 --- a/app/Http/Controllers/ClientApiController.php +++ b/app/Http/Controllers/ClientApiController.php @@ -1,5 +1,6 @@ listResponse($clients); } + /** + * @SWG\Get( + * path="/clients/{client_id}", + * summary="Individual Client", + * tags={"client"}, + * @SWG\Response( + * response=200, + * description="A single client", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Client")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + + public function show(ClientRequest $request) + { + return $this->itemResponse($request->entity()); + } + + + + /** * @SWG\Post( * path="/clients", diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index b964f49206..e7bcd6e82e 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -95,7 +95,7 @@ class ClientController extends BaseController if($user->can('create', ENTITY_TASK)){ $actionLinks[] = ['label' => trans('texts.new_task'), 'url' => URL::to('/tasks/create/'.$client->public_id)]; } - if (Utils::hasFeature(FEATURE_QUOTES) && $user->can('create', ENTITY_INVOICE)) { + if (Utils::hasFeature(FEATURE_QUOTES) && $user->can('create', ENTITY_QUOTE)) { $actionLinks[] = ['label' => trans('texts.new_quote'), 'url' => URL::to('/quotes/create/'.$client->public_id)]; } @@ -221,10 +221,6 @@ class ClientController extends BaseController $message = Utils::pluralize($action.'d_client', $count); Session::flash('message', $message); - if ($action == 'restore' && $count == 1) { - return Redirect::to('clients/'.Utils::getFirst($ids)); - } else { - return Redirect::to('clients'); - } + return $this->returnBulk(ENTITY_CLIENT, $action, $ids); } } diff --git a/app/Http/Controllers/ClientPortalController.php b/app/Http/Controllers/ClientPortalController.php index fa396a6824..c2d850f0a1 100644 --- a/app/Http/Controllers/ClientPortalController.php +++ b/app/Http/Controllers/ClientPortalController.php @@ -22,6 +22,7 @@ use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\ActivityRepository; use App\Ninja\Repositories\DocumentRepository; +use App\Ninja\Repositories\CreditRepository; use App\Events\InvoiceInvitationWasViewed; use App\Events\QuoteInvitationWasViewed; use App\Services\PaymentService; @@ -33,13 +34,14 @@ class ClientPortalController extends BaseController private $paymentRepo; private $documentRepo; - public function __construct(InvoiceRepository $invoiceRepo, PaymentRepository $paymentRepo, ActivityRepository $activityRepo, DocumentRepository $documentRepo, PaymentService $paymentService) + public function __construct(InvoiceRepository $invoiceRepo, PaymentRepository $paymentRepo, ActivityRepository $activityRepo, DocumentRepository $documentRepo, PaymentService $paymentService, CreditRepository $creditRepo) { $this->invoiceRepo = $invoiceRepo; $this->paymentRepo = $paymentRepo; $this->activityRepo = $activityRepo; $this->documentRepo = $documentRepo; $this->paymentService = $paymentService; + $this->creditRepo = $creditRepo; } public function view($invitationKey) @@ -102,7 +104,9 @@ class ClientPortalController extends BaseController $paymentURL = ''; if (count($paymentTypes) == 1) { $paymentURL = $paymentTypes[0]['url']; - if (!$account->isGatewayConfigured(GATEWAY_PAYPAL_EXPRESS)) { + if ($paymentTypes[0]['gatewayTypeId'] == GATEWAY_TYPE_CUSTOM) { + // do nothing + } elseif (!$account->isGatewayConfigured(GATEWAY_PAYPAL_EXPRESS)) { $paymentURL = URL::to($paymentURL); } } @@ -141,7 +145,12 @@ class ClientPortalController extends BaseController ]; } - + if ($accountGateway = $account->getGatewayByType(GATEWAY_TYPE_CUSTOM)) { + $data += [ + 'customGatewayName' => $accountGateway->getConfigField('name'), + 'customGatewayText' => $accountGateway->getConfigField('text'), + ]; + } if($account->hasFeature(FEATURE_DOCUMENTS) && $this->canCreateZip()){ $zipDocs = $this->getInvoiceZipDocuments($invoice, $size); @@ -155,18 +164,6 @@ class ClientPortalController extends BaseController return View::make('invoices.view', $data); } - public function contactIndex($contactKey) { - if (!$contact = Contact::where('contact_key', '=', $contactKey)->first()) { - return $this->returnError(); - } - - $client = $contact->client; - - Session::put('contact_key', $contactKey);// track current contact - - return redirect()->to($client->account->enable_client_portal_dashboard?'/client/dashboard':'/client/invoices/'); - } - private function getPaymentTypes($account, $client, $invitation) { $links = []; @@ -201,19 +198,41 @@ class ClientPortalController extends BaseController return $pdfString; } - public function dashboard() + public function sign($invitationKey) { - if (!$contact = $this->getContact()) { + if (!$invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) { + return RESULT_FAILURE; + } + + $invitation->signature_base64 = Input::get('signature'); + $invitation->signature_date = date_create(); + $invitation->save(); + + return RESULT_SUCCESS; + } + + public function dashboard($contactKey = false) + { + if ($contactKey) { + if (!$contact = Contact::where('contact_key', '=', $contactKey)->first()) { + return $this->returnError(); + } + Session::put('contact_key', $contactKey);// track current contact + } else if (!$contact = $this->getContact()) { return $this->returnError(); } $client = $contact->client; $account = $client->account; + $account->loadLocalizationSettings($client); + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $customer = false; - if (!$account->enable_client_portal || !$account->enable_client_portal_dashboard) { + if (!$account->enable_client_portal) { return $this->returnError(); + } elseif (!$account->enable_client_portal_dashboard) { + return redirect()->to('/client/invoices/'); } if ($paymentDriver = $account->paymentDriver(false, GATEWAY_TYPE_TOKEN)) { @@ -273,6 +292,7 @@ class ClientPortalController extends BaseController } $account = $contact->account; + $account->loadLocalizationSettings($contact->client); if (!$account->enable_client_portal) { return $this->returnError(); @@ -300,6 +320,7 @@ class ClientPortalController extends BaseController } $account = $contact->account; + $account->loadLocalizationSettings($contact->client); if (!$account->enable_client_portal) { return $this->returnError(); @@ -346,12 +367,14 @@ class ClientPortalController extends BaseController } $account = $contact->account; + $account->loadLocalizationSettings($contact->client); if (!$account->enable_client_portal) { return $this->returnError(); } $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + $data = [ 'color' => $color, 'account' => $account, @@ -416,12 +439,14 @@ class ClientPortalController extends BaseController } $account = $contact->account; + $account->loadLocalizationSettings($contact->client); if (!$account->enable_client_portal) { return $this->returnError(); } $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + $data = [ 'color' => $color, 'account' => $account, @@ -444,6 +469,42 @@ class ClientPortalController extends BaseController return $this->invoiceRepo->getClientDatatable($contact->id, ENTITY_QUOTE, Input::get('sSearch')); } + public function creditIndex() + { + if (!$contact = $this->getContact()) { + return $this->returnError(); + } + + $account = $contact->account; + $account->loadLocalizationSettings($contact->client); + + if (!$account->enable_client_portal) { + return $this->returnError(); + } + + $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + + $data = [ + 'color' => $color, + 'account' => $account, + 'clientFontUrl' => $account->getFontsUrl(), + 'title' => trans('texts.credits'), + 'entityType' => ENTITY_CREDIT, + 'columns' => Utils::trans(['credit_date', 'credit_amount', 'credit_balance']), + ]; + + return response()->view('public_list', $data); + } + + public function creditDatatable() + { + if (!$contact = $this->getContact()) { + return false; + } + + return $this->creditRepo->getClientDatatable($contact->client_id); + } + public function documentIndex() { if (!$contact = $this->getContact()) { @@ -451,12 +512,14 @@ class ClientPortalController extends BaseController } $account = $contact->account; + $account->loadLocalizationSettings($contact->client); if (!$account->enable_client_portal) { return $this->returnError(); } $color = $account->primary_color ? $account->primary_color : '#0b4d78'; + $data = [ 'color' => $color, 'account' => $account, diff --git a/app/Http/Controllers/DashboardApiController.php b/app/Http/Controllers/DashboardApiController.php index d5ff416e7e..0db08e5550 100644 --- a/app/Http/Controllers/DashboardApiController.php +++ b/app/Http/Controllers/DashboardApiController.php @@ -23,8 +23,8 @@ class DashboardApiController extends BaseAPIController $dashboardRepo = $this->dashboardRepo; $metrics = $dashboardRepo->totals($accountId, $userId, $viewAll); - $paidToDate = $dashboardRepo->paidToDate($accountId, $userId, $viewAll); - $averageInvoice = $dashboardRepo->averages($accountId, $userId, $viewAll); + $paidToDate = $dashboardRepo->paidToDate($user->account, $userId, $viewAll); + $averageInvoice = $dashboardRepo->averages($user->account, $userId, $viewAll); $balances = $dashboardRepo->balances($accountId, $userId, $viewAll); $activities = $dashboardRepo->activities($accountId, $userId, $viewAll); $pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll); diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 50c5d3d394..14cb4c638b 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -33,9 +33,9 @@ class DashboardController extends BaseController $dashboardRepo = $this->dashboardRepo; $metrics = $dashboardRepo->totals($accountId, $userId, $viewAll); - $paidToDate = $dashboardRepo->paidToDate($accountId, $userId, $viewAll); - $averageInvoice = $dashboardRepo->averages($accountId, $userId, $viewAll); - $balances = $dashboardRepo->balances($accountId, $userId, $viewAll); + $paidToDate = $dashboardRepo->paidToDate($account, $userId, $viewAll); + $averageInvoice = $dashboardRepo->averages($account, $userId, $viewAll); + $balances = $dashboardRepo->balances($accountId, $userId, $viewAll); $activities = $dashboardRepo->activities($accountId, $userId, $viewAll); $pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll); $upcoming = $dashboardRepo->upcoming($accountId, $userId, $viewAll); diff --git a/app/Http/Controllers/ExpenseApiController.php b/app/Http/Controllers/ExpenseApiController.php index ea4425df86..465edcc852 100644 --- a/app/Http/Controllers/ExpenseApiController.php +++ b/app/Http/Controllers/ExpenseApiController.php @@ -1,8 +1,12 @@ expenseService = $expenseService; } + /** + * @SWG\Get( + * path="/expenses", + * summary="List of expenses", + * tags={"expense"}, + * @SWG\Response( + * response=200, + * description="A list with expenses", + * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Expense")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ public function index() { $expenses = Expense::scope() @@ -30,23 +50,103 @@ class ExpenseApiController extends BaseAPIController return $this->listResponse($expenses); } - public function update() + /** + * @SWG\Post( + * path="/expenses", + * tags={"expense"}, + * summary="Create a expense", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Expense") + * ), + * @SWG\Response( + * response=200, + * description="New expense", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Expense")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + public function store(CreateExpenseRequest $request) { - //stub + $expense = $this->expenseRepo->save($request->input()); + $expense = Expense::scope($expense->public_id) + ->with('client', 'invoice', 'vendor') + ->first(); + + return $this->itemResponse($expense); } - public function store() + /** + * @SWG\Put( + * path="/expenses/{expense_id}", + * tags={"expense"}, + * summary="Update a expense", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Expense") + * ), + * @SWG\Response( + * response=200, + * description="Update expense", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Expense")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + public function update(UpdateExpenseRequest $request, $publicId) { - //stub + if ($request->action) { + return $this->handleAction($request); + } + $data = $request->input(); + $data['public_id'] = $publicId; + $expense = $this->expenseRepo->save($data, $request->entity()); + + return $this->itemResponse($expense); } - public function destroy() + /** + * @SWG\Delete( + * path="/expenses/{expense_id}", + * tags={"expense"}, + * summary="Delete a expense", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Expense") + * ), + * @SWG\Response( + * response=200, + * description="Delete expense", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Expense")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + public function destroy(ExpenseRequest $request) { - //stub + $expense = $request->entity(); + $this->expenseRepo->delete($expense); + + return $this->itemResponse($expense); } -} \ No newline at end of file + + +} diff --git a/app/Http/Controllers/ExpenseCategoryApiController.php b/app/Http/Controllers/ExpenseCategoryApiController.php new file mode 100644 index 0000000000..5a18735dee --- /dev/null +++ b/app/Http/Controllers/ExpenseCategoryApiController.php @@ -0,0 +1,41 @@ +categoryRepo = $categoryRepo; + $this->categoryService = $categoryService; + } + + public function update(UpdateExpenseCategoryRequest $request) + { + $category = $this->categoryRepo->save($request->input(), $request->entity()); + + return $this->itemResponse($category); + } + + public function store(CreateExpenseCategoryRequest $request) + { + $category = $this->categoryRepo->save($request->input()); + + return $this->itemResponse($category); + + } +} diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php index 97ab3adb7b..1130b6b103 100644 --- a/app/Http/Controllers/ExpenseController.php +++ b/app/Http/Controllers/ExpenseController.php @@ -134,6 +134,7 @@ class ExpenseController extends BaseController $data = [ 'vendor' => null, 'expense' => $expense, + 'entity' => $expense, 'method' => 'PUT', 'url' => 'expenses/'.$expense->public_id, 'title' => 'Edit Expense', @@ -245,7 +246,7 @@ class ExpenseController extends BaseController Session::flash('message', $message); } - return Redirect::to('expenses'); + return $this->returnBulk($this->entityType, $action, $ids); } private static function getViewModel() diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 395d4a0f24..4f153f78ef 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -38,7 +38,7 @@ class HomeController extends BaseController public function showIndex() { Session::reflash(); - + if (!Utils::isNinja() && (!Utils::isDatabaseSetup() || Account::count() == 0)) { return Redirect::to('/setup'); } elseif (Auth::check()) { @@ -76,10 +76,8 @@ class HomeController extends BaseController } // Track the referral/campaign code - foreach (['rc', 'utm_campaign'] as $code) { - if (Input::has($code)) { - Session::set(SESSION_REFERRAL_CODE, Input::get($code)); - } + if (Input::has('rc')) { + Session::set(SESSION_REFERRAL_CODE, Input::get('rc')); } if (Auth::check()) { @@ -115,7 +113,7 @@ class HomeController extends BaseController $user->save(); } } - + Session::forget('news_feed_message'); return 'success'; diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php index 3e893d57b5..e5b74453e4 100644 --- a/app/Http/Controllers/ImportController.php +++ b/app/Http/Controllers/ImportController.php @@ -31,6 +31,11 @@ class ImportController extends BaseController } } + if ( ! count($files)) { + Session::flash('error', trans('texts.select_file')); + return Redirect::to('/settings/' . ACCOUNT_IMPORT_EXPORT); + } + try { if ($source === IMPORT_CSV) { $data = $this->importService->mapCSV($files); diff --git a/app/Http/Controllers/InvoiceApiController.php b/app/Http/Controllers/InvoiceApiController.php index 4eec4346ec..3e2341b0e4 100644 --- a/app/Http/Controllers/InvoiceApiController.php +++ b/app/Http/Controllers/InvoiceApiController.php @@ -176,7 +176,7 @@ class InvoiceApiController extends BaseAPIController if (isset($data['email_invoice']) && $data['email_invoice']) { if ($payment) { $this->mailer->sendPaymentConfirmation($payment); - } else { + } elseif ( ! $invoice->is_recurring) { $this->mailer->sendInvoice($invoice); } } diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index f41fd49f15..843a83777d 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -218,6 +218,7 @@ class InvoiceController extends BaseController $contact->invitation_viewed = $invitation->viewed_date && $invitation->viewed_date != '0000-00-00 00:00:00' ? $invitation->viewed_date : false; $contact->invitation_openend = $invitation->opened_date && $invitation->opened_date != '0000-00-00 00:00:00' ? $invitation->opened_date : false; $contact->invitation_status = $contact->email_error ? false : $invitation->getStatus(); + $contact->invitation_signature_svg = $invitation->signatureDiv(); } } } @@ -269,6 +270,9 @@ class InvoiceController extends BaseController private static function getViewModel($invoice) { $recurringHelp = ''; + $recurringDueDateHelp = ''; + $recurringDueDates = []; + foreach (preg_split("/((\r?\n)|(\r\n?))/", trans('texts.recurring_help')) as $line) { $parts = explode('=>', $line); if (count($parts) > 1) { @@ -279,7 +283,6 @@ class InvoiceController extends BaseController } } - $recurringDueDateHelp = ''; foreach (preg_split("/((\r?\n)|(\r\n?))/", trans('texts.recurring_due_date_help')) as $line) { $parts = explode('=>', $line); if (count($parts) > 1) { @@ -409,10 +412,10 @@ class InvoiceController extends BaseController Session::flash('message', $message); if ($action == 'email') { - return $this->emailInvoice($invoice, Input::get('pdfupload')); + $this->emailInvoice($invoice, Input::get('pdfupload')); } - return redirect()->to($invoice->getRoute()); + return url($invoice->getRoute()); } /** @@ -435,14 +438,14 @@ class InvoiceController extends BaseController Session::flash('message', $message); if ($action == 'clone') { - return $this->cloneInvoice($request, $invoice->public_id); + return url(sprintf('%ss/%s/clone', $entityType, $invoice->public_id)); } elseif ($action == 'convert') { return $this->convertQuote($request, $invoice->public_id); } elseif ($action == 'email') { - return $this->emailInvoice($invoice, Input::get('pdfupload')); + $this->emailInvoice($invoice, Input::get('pdfupload')); } - return redirect()->to($invoice->getRoute()); + return url($invoice->getRoute()); } @@ -469,8 +472,6 @@ class InvoiceController extends BaseController } else { Session::flash('error', $response); } - - return Redirect::to("{$entityType}s/{$invoice->public_id}/edit"); } private function emailRecurringInvoice(&$invoice) @@ -527,11 +528,7 @@ class InvoiceController extends BaseController Session::flash('message', $message); } - if ($action == 'restore' && $count == 1) { - return Redirect::to("{$entityType}s/".Utils::getFirst($ids)); - } else { - return Redirect::to("{$entityType}s"); - } + return $this->returnBulk($entityType, $action, $ids); } public function convertQuote(InvoiceRequest $request) @@ -540,7 +537,7 @@ class InvoiceController extends BaseController Session::flash('message', trans('texts.converted_to_invoice')); - return Redirect::to('invoices/' . $clone->public_id); + return url('invoices/' . $clone->public_id); } public function cloneInvoice(InvoiceRequest $request, $publicId) @@ -608,12 +605,19 @@ class InvoiceController extends BaseController return View::make('invoices.history', $data); } - public function checkInvoiceNumber($invoiceNumber) + public function checkInvoiceNumber($invoicePublicId = false) { - $count = Invoice::scope() + $invoiceNumber = request()->invoice_number; + + $query = Invoice::scope() ->whereInvoiceNumber($invoiceNumber) - ->withTrashed() - ->count(); + ->withTrashed(); + + if ($invoicePublicId) { + $query->where('public_id', '!=', $invoicePublicId); + } + + $count = $query->count(); return $count ? RESULT_FAILURE : RESULT_SUCCESS; } diff --git a/app/Http/Controllers/OnlinePaymentController.php b/app/Http/Controllers/OnlinePaymentController.php index bd621e82d9..e7d709096e 100644 --- a/app/Http/Controllers/OnlinePaymentController.php +++ b/app/Http/Controllers/OnlinePaymentController.php @@ -20,6 +20,7 @@ use App\Http\Requests\CreateOnlinePaymentRequest; use App\Ninja\Repositories\ClientRepository; use App\Ninja\Repositories\InvoiceRepository; use App\Services\InvoiceService; +use App\Models\GatewayType; /** * Class OnlinePaymentController @@ -60,7 +61,7 @@ class OnlinePaymentController extends BaseController * @param bool $sourceId * @return \Illuminate\Http\RedirectResponse */ - public function showPayment($invitationKey, $gatewayType = false, $sourceId = false) + public function showPayment($invitationKey, $gatewayTypeAlias = false, $sourceId = false) { if ( ! $invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) { return response()->view('error', [ @@ -69,17 +70,23 @@ class OnlinePaymentController extends BaseController ]); } - if ( ! floatval($invitation->invoice->balance)) { + if ( ! $invitation->invoice->canBePaid()) { return redirect()->to('view/' . $invitation->invitation_key); } $invitation = $invitation->load('invoice.client.account.account_gateways.gateway'); + $account = $invitation->account; + $account->loadLocalizationSettings($invitation->invoice->client); - if ( ! $gatewayType) { - $gatewayType = Session::get($invitation->id . 'gateway_type'); + if ( ! $gatewayTypeAlias) { + $gatewayTypeId = Session::get($invitation->id . 'gateway_type'); + } elseif ($gatewayTypeAlias != GATEWAY_TYPE_TOKEN) { + $gatewayTypeId = GatewayType::getIdFromAlias($gatewayTypeAlias); + } else { + $gatewayTypeId = $gatewayTypeAlias; } - $paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayType); + $paymentDriver = $account->paymentDriver($invitation, $gatewayTypeId); try { return $paymentDriver->startPurchase(Input::all(), $sourceId); @@ -95,8 +102,12 @@ class OnlinePaymentController extends BaseController public function doPayment(CreateOnlinePaymentRequest $request) { $invitation = $request->invitation; - $gatewayType = Session::get($invitation->id . 'gateway_type'); - $paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayType); + $gatewayTypeId = Session::get($invitation->id . 'gateway_type'); + $paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayTypeId); + + if ( ! $invitation->invoice->canBePaid()) { + return redirect()->to('view/' . $invitation->invitation_key); + } try { $paymentDriver->completeOnsitePurchase($request->all()); @@ -114,17 +125,24 @@ class OnlinePaymentController extends BaseController /** * @param bool $invitationKey - * @param bool $gatewayType + * @param mixed $gatewayTypeAlias * @return \Illuminate\Http\RedirectResponse */ - public function offsitePayment($invitationKey = false, $gatewayType = false) + public function offsitePayment($invitationKey = false, $gatewayTypeAlias = false) { $invitationKey = $invitationKey ?: Session::get('invitation_key'); $invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.account_gateways.gateway') ->where('invitation_key', '=', $invitationKey)->firstOrFail(); - $gatewayType = $gatewayType ?: Session::get($invitation->id . 'gateway_type'); - $paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayType); + if ( ! $gatewayTypeAlias) { + $gatewayTypeId = Session::get($invitation->id . 'gateway_type'); + } elseif ($gatewayTypeAlias != GATEWAY_TYPE_TOKEN) { + $gatewayTypeId = GatewayType::getIdFromAlias($gatewayTypeAlias); + } else { + $gatewayTypeId = $gatewayTypeAlias; + } + + $paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayTypeId); if ($error = Input::get('error_description') ?: Input::get('error')) { return $this->error($paymentDriver, $error); @@ -228,7 +246,7 @@ class OnlinePaymentController extends BaseController } } - public function handleBuyNow(ClientRepository $clientRepo, InvoiceService $invoiceService, $gatewayType = false) + public function handleBuyNow(ClientRepository $clientRepo, InvoiceService $invoiceService, $gatewayTypeAlias = false) { if (Crawler::isCrawler()) { return redirect()->to(NINJA_WEB_URL, 301); @@ -267,6 +285,8 @@ class OnlinePaymentController extends BaseController $data = [ 'client_id' => $client->id, + 'tax_rate1' => $account->default_tax_rate ? $account->default_tax_rate->rate : 0, + 'tax_name1' => $account->default_tax_rate ? $account->default_tax_rate->name : '', 'invoice_items' => [[ 'product_key' => $product->product_key, 'notes' => $product->notes, @@ -280,8 +300,8 @@ class OnlinePaymentController extends BaseController $invitation = $invoice->invitations[0]; $link = $invitation->getLink(); - if ($gatewayType) { - return redirect()->to($invitation->getLink('payment') . "/{$gatewayType}"); + if ($gatewayTypeAlias) { + return redirect()->to($invitation->getLink('payment') . "/{$gatewayTypeAlias}"); } else { return redirect()->to($invitation->getLink()); } diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 59f79661c2..62a1183278 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -142,11 +142,13 @@ class PaymentController extends BaseController 'invoices' => Invoice::scope()->invoiceType(INVOICE_TYPE_STANDARD)->where('is_recurring', '=', false) ->with('client', 'invoice_status')->orderBy('invoice_number')->get(), 'payment' => $payment, + 'entity' => $payment, 'method' => 'PUT', 'url' => 'payments/'.$payment->public_id, 'title' => trans('texts.edit_payment'), 'paymentTypes' => Cache::get('paymentTypes'), - 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), ]; + 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), + ]; return View::make('payments.edit', $data); } diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index 2d21dbd3cb..9c72694949 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -3,6 +3,7 @@ use Auth; use URL; use View; +use Utils; use Input; use Session; use Redirect; @@ -37,15 +38,40 @@ class ProductController extends BaseController */ public function index() { - return Redirect::to('settings/' . ACCOUNT_PRODUCTS); + $columns = [ + 'checkbox', + 'product', + 'description', + 'unit_cost' + ]; + + if (Auth::user()->account->invoice_item_taxes) { + $columns[] = 'tax_rate'; + } + $columns[] = 'action'; + + return View::make('list', [ + 'entityType' => ENTITY_PRODUCT, + 'title' => trans('texts.products'), + 'sortCol' => '4', + 'columns' => Utils::trans($columns), + ]); } + public function show($publicId) + { + Session::reflash(); + + return Redirect::to("products/$publicId/edit"); + } + + /** * @return \Illuminate\Http\JsonResponse */ public function getDatatable() { - return $this->productService->getDatatable(Auth::user()->account_id); + return $this->productService->getDatatable(Auth::user()->account_id, Input::get('sSearch')); } /** @@ -55,11 +81,13 @@ class ProductController extends BaseController public function edit($publicId) { $account = Auth::user()->account; + $product = Product::scope($publicId)->withTrashed()->firstOrFail(); $data = [ 'account' => $account, 'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->get(['id', 'name', 'rate']) : null, - 'product' => Product::scope($publicId)->firstOrFail(), + 'product' => $product, + 'entity' => $product, 'method' => 'PUT', 'url' => 'products/'.$publicId, 'title' => trans('texts.edit_product'), @@ -111,7 +139,7 @@ class ProductController extends BaseController private function save($productPublicId = false) { if ($productPublicId) { - $product = Product::scope($productPublicId)->firstOrFail(); + $product = Product::scope($productPublicId)->withTrashed()->firstOrFail(); } else { $product = Product::createNew(); } @@ -134,12 +162,12 @@ class ProductController extends BaseController */ public function bulk() { - $action = Input::get('bulk_action'); - $ids = Input::get('bulk_public_id'); + $action = Input::get('action'); + $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids'); $count = $this->productService->bulk($ids, $action); Session::flash('message', trans('texts.archived_product')); - return Redirect::to('settings/' . ACCOUNT_PRODUCTS); + return $this->returnBulk(ENTITY_PRODUCT, $action, $ids); } } diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php index a9719798a1..c8c952debe 100644 --- a/app/Http/Controllers/QuoteController.php +++ b/app/Http/Controllers/QuoteController.php @@ -154,11 +154,7 @@ class QuoteController extends BaseController Session::flash('message', $message); } - if ($action == 'restore' && $count == 1) { - return Redirect::to('quotes/'.Utils::getFirst($ids)); - } else { - return Redirect::to('quotes'); - } + return $this->returnBulk(ENTITY_QUOTE, $action, $ids); } public function approve($invitationKey) diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 3f954da853..3431d5c183 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -11,6 +11,7 @@ use App\Models\Account; use App\Models\Client; use App\Models\Payment; use App\Models\Expense; +use App\Models\Task; /** * Class ReportController @@ -56,8 +57,8 @@ class ReportController extends BaseController if (Input::get('report_type')) { $reportType = Input::get('report_type'); $dateField = Input::get('date_field'); - $startDate = Utils::toSqlDate(Input::get('start_date'), false); - $endDate = Utils::toSqlDate(Input::get('end_date'), false); + $startDate = date_create(Input::get('start_date')); + $endDate = date_create(Input::get('end_date')); } else { $reportType = ENTITY_INVOICE; $dateField = FILTER_INVOICE_DATE; @@ -71,15 +72,17 @@ class ReportController extends BaseController ENTITY_PRODUCT => trans('texts.product'), ENTITY_PAYMENT => trans('texts.payment'), ENTITY_EXPENSE => trans('texts.expense'), + ENTITY_TASK => trans('texts.task'), ENTITY_TAX_RATE => trans('texts.tax'), ]; $params = [ - 'startDate' => $startDate->format(Session::get(SESSION_DATE_FORMAT)), - 'endDate' => $endDate->format(Session::get(SESSION_DATE_FORMAT)), + 'startDate' => $startDate->format('Y-m-d'), + 'endDate' => $endDate->format('Y-m-d'), 'reportTypes' => $reportTypes, 'reportType' => $reportType, 'title' => trans('texts.charts_and_reports'), + 'account' => Auth::user()->account, ]; if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) { @@ -120,9 +123,37 @@ class ReportController extends BaseController return $this->generateTaxRateReport($startDate, $endDate, $dateField, $isExport); } elseif ($reportType == ENTITY_EXPENSE) { return $this->generateExpenseReport($startDate, $endDate, $isExport); + } elseif ($reportType == ENTITY_TASK) { + return $this->generateTaskReport($startDate, $endDate, $isExport); } } + private function generateTaskReport($startDate, $endDate, $isExport) + { + $columns = ['client', 'date', 'description', 'duration']; + $displayData = []; + + $tasks = Task::scope() + ->with('client.contacts') + ->withArchived() + ->dateRange($startDate, $endDate); + + foreach ($tasks->get() as $task) { + $displayData[] = [ + $task->client ? ($isExport ? $task->client->getDisplayName() : $task->client->present()->link) : trans('texts.unassigned'), + link_to($task->present()->url, $task->getStartTime()), + $task->present()->description, + Utils::formatTime($task->getDuration()), + ]; + } + + return [ + 'columns' => $columns, + 'displayData' => $displayData, + 'reportTotals' => [], + ]; + } + /** * @param $startDate * @param $endDate @@ -354,7 +385,7 @@ class ReportController extends BaseController $isExport ? $client->getDisplayName() : $client->present()->link, $isExport ? $invoice->invoice_number : $invoice->present()->link, $invoice->present()->invoice_date, - $invoiceItem->qty, + round($invoiceItem->qty, 2), $invoiceItem->product_key, ]; //$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'paid', $payment ? $payment->amount : 0); @@ -433,36 +464,31 @@ class ReportController extends BaseController */ private function generateExpenseReport($startDate, $endDate, $isExport) { - $columns = ['vendor', 'client', 'date', 'expense_amount', 'invoiced_amount']; + $columns = ['vendor', 'client', 'date', 'expense_amount']; $account = Auth::user()->account; $displayData = []; $reportTotals = []; $expenses = Expense::scope() - ->withTrashed() + ->withArchived() ->with('client.contacts', 'vendor') ->where('expense_date', '>=', $startDate) ->where('expense_date', '<=', $endDate); foreach ($expenses->get() as $expense) { - $amount = $expense->amount; - $invoiced = $expense->present()->invoiced_amount; + $amount = $expense->amountWithTax(); $displayData[] = [ $expense->vendor ? ($isExport ? $expense->vendor->name : $expense->vendor->present()->link) : '', $expense->client ? ($isExport ? $expense->client->getDisplayName() : $expense->client->present()->link) : '', $expense->present()->expense_date, Utils::formatMoney($amount, $expense->currency_id), - Utils::formatMoney($invoiced, $expense->invoice_currency_id), ]; $reportTotals = $this->addToTotals($reportTotals, $expense->expense_currency_id, 'amount', $amount); $reportTotals = $this->addToTotals($reportTotals, $expense->invoice_currency_id, 'amount', 0); - - $reportTotals = $this->addToTotals($reportTotals, $expense->invoice_currency_id, 'invoiced', $invoiced); - $reportTotals = $this->addToTotals($reportTotals, $expense->expense_currency_id, 'invoiced', 0); } return [ diff --git a/app/Http/Controllers/SelfUpdateController.php b/app/Http/Controllers/SelfUpdateController.php index 1f362da3eb..a58bb1c93c 100644 --- a/app/Http/Controllers/SelfUpdateController.php +++ b/app/Http/Controllers/SelfUpdateController.php @@ -2,10 +2,9 @@ namespace App\Http\Controllers; -use Codedge\Updater\UpdaterManager; - -use App\Http\Requests; +use Utils; use Redirect; +use Codedge\Updater\UpdaterManager; class SelfUpdateController extends BaseController { @@ -21,6 +20,10 @@ class SelfUpdateController extends BaseController */ public function __construct(UpdaterManager $updater) { + if (Utils::isNinjaProd()) { + exit; + } + $this->updater = $updater; } diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php index 42099026eb..bcc803243f 100644 --- a/app/Http/Controllers/TaskController.php +++ b/app/Http/Controllers/TaskController.php @@ -177,15 +177,14 @@ class TaskController extends BaseController $data = [ 'task' => $task, + 'entity' => $task, 'clientPublicId' => $task->client ? $task->client->public_id : 0, 'method' => 'PUT', 'url' => 'tasks/'.$task->public_id, 'title' => trans('texts.edit_task'), - 'duration' => $task->is_running ? $task->getCurrentDuration() : $task->getDuration(), 'actions' => $actions, 'timezone' => Auth::user()->account->timezone ? Auth::user()->account->timezone->name : DEFAULT_TIMEZONE, 'datetimeFormat' => Auth::user()->account->getMomentDateTimeFormat(), - //'entityStatus' => $task->present()->status, ]; $data = array_merge($data, self::getViewModel()); @@ -295,16 +294,12 @@ class TaskController extends BaseController return Redirect::to("invoices/{$invoiceId}/edit")->with('tasks', $data); } } else { - $count = $this->taskRepo->bulk($ids, $action); + $count = $this->taskService->bulk($ids, $action); $message = Utils::pluralize($action.'d_task', $count); Session::flash('message', $message); - if ($action == 'restore' && $count == 1) { - return Redirect::to('tasks/'.$ids[0].'/edit'); - } else { - return Redirect::to('tasks'); - } + return $this->returnBulk($this->entityType, $action, $ids); } } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 1d504978c6..b2d587c1a8 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -66,7 +66,9 @@ class UserController extends BaseController public function edit($publicId) { $user = User::where('account_id', '=', Auth::user()->account_id) - ->where('public_id', '=', $publicId)->firstOrFail(); + ->where('public_id', '=', $publicId) + ->withTrashed() + ->firstOrFail(); $data = [ 'user' => $user, @@ -157,7 +159,9 @@ class UserController extends BaseController if ($userPublicId) { $user = User::where('account_id', '=', Auth::user()->account_id) - ->where('public_id', '=', $userPublicId)->firstOrFail(); + ->where('public_id', '=', $userPublicId) + ->withTrashed() + ->firstOrFail(); $rules['email'] = 'required|email|unique:users,email,'.$user->id.',id'; } else { @@ -334,7 +338,13 @@ class UserController extends BaseController } } - return Redirect::to($referer); + // If the user is looking at an entity redirect to the dashboard + preg_match('/\/[0-9*][\/edit]*$/', $referer, $matches); + if (count($matches)) { + return Redirect::to('/dashboard'); + } else { + return Redirect::to($referer); + } } public function unlinkAccount($userAccountId, $userId) diff --git a/app/Http/Controllers/VendorApiController.php b/app/Http/Controllers/VendorApiController.php index 3b0c2dadd7..c38e5867e2 100644 --- a/app/Http/Controllers/VendorApiController.php +++ b/app/Http/Controllers/VendorApiController.php @@ -1,5 +1,7 @@ itemResponse($vendor); } + + /** + * @SWG\Put( + * path="/vendors/{vendor_id}", + * tags={"vendor"}, + * summary="Update a vendor", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Vendor") + * ), + * @SWG\Response( + * response=200, + * description="Update vendor", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Vendor")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + + public function update(UpdateVendorRequest $request, $publicId) + { + if ($request->action) { + return $this->handleAction($request); + } + + $data = $request->input(); + $data['public_id'] = $publicId; + $vendor = $this->vendorRepo->save($data, $request->entity()); + + $vendor->load(['vendor_contacts']); + + return $this->itemResponse($vendor); + } + + + /** + * @SWG\Delete( + * path="/vendors/{vendor_id}", + * tags={"vendor"}, + * summary="Delete a vendor", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Vendor") + * ), + * @SWG\Response( + * response=200, + * description="Delete vendor", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Vendor")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + + public function destroy(VendorRequest $request) + { + $vendor = $request->entity(); + + $this->vendorRepo->delete($vendor); + + return $this->itemResponse($vendor); + } } diff --git a/app/Http/Controllers/VendorController.php b/app/Http/Controllers/VendorController.php index 280f788b52..f1bd21e984 100644 --- a/app/Http/Controllers/VendorController.php +++ b/app/Http/Controllers/VendorController.php @@ -81,7 +81,7 @@ class VendorController extends BaseController public function show(VendorRequest $request) { $vendor = $request->entity(); - + $actionLinks = [ ['label' => trans('texts.new_vendor'), 'url' => URL::to('/vendors/create/' . $vendor->public_id)] ]; @@ -185,10 +185,6 @@ class VendorController extends BaseController $message = Utils::pluralize($action.'d_vendor', $count); Session::flash('message', $message); - if ($action == 'restore' && $count == 1) { - return Redirect::to('vendors/' . Utils::getFirst($ids)); - } else { - return Redirect::to('vendors'); - } + return $this->returnBulk($this->entityType, $action, $ids); } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 23ed4f62ca..b20e0b7544 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -18,7 +18,6 @@ class Kernel extends HttpKernel { 'App\Http\Middleware\VerifyCsrfToken', 'App\Http\Middleware\DuplicateSubmissionCheck', 'App\Http\Middleware\QueryLogging', - 'App\Http\Middleware\SessionDataCheckMiddleware', 'App\Http\Middleware\StartupCheck', ]; diff --git a/app/Http/Middleware/ApiCheck.php b/app/Http/Middleware/ApiCheck.php index 3561695e5c..6e7e73223d 100644 --- a/app/Http/Middleware/ApiCheck.php +++ b/app/Http/Middleware/ApiCheck.php @@ -23,18 +23,22 @@ class ApiCheck { */ public function handle($request, Closure $next) { - $loggingIn = $request->is('api/v1/login') || $request->is('api/v1/register'); + $loggingIn = $request->is('api/v1/login') + || $request->is('api/v1/register') + || $request->is('api/v1/oauth_login'); $headers = Utils::getApiHeaders(); + $hasApiSecret = false; if ($secret = env(API_SECRET)) { - $hasApiSecret = hash_equals($request->api_secret ?: '', $secret); + $requestSecret = Request::header('X-Ninja-Secret') ?: ($request->api_secret ?: ''); + $hasApiSecret = hash_equals($requestSecret, $secret); } if ($loggingIn) { // check API secret if ( ! $hasApiSecret) { sleep(ERROR_DELAY); - return Response::json('Invalid secret', 403, $headers); + return Response::json('Invalid value for API_SECRET', 403, $headers); } } else { // check for a valid token diff --git a/app/Http/Middleware/DuplicateSubmissionCheck.php b/app/Http/Middleware/DuplicateSubmissionCheck.php index aa128bbc43..f92a31a483 100644 --- a/app/Http/Middleware/DuplicateSubmissionCheck.php +++ b/app/Http/Middleware/DuplicateSubmissionCheck.php @@ -15,8 +15,7 @@ class DuplicateSubmissionCheck */ public function handle(Request $request, Closure $next) { - - if ($request->is('api/v1/*')) { + if ($request->is('api/v1/*') || $request->is('documents')) { return $next($request); } diff --git a/app/Http/Middleware/QueryLogging.php b/app/Http/Middleware/QueryLogging.php index 57a26166a1..1bc8160dff 100644 --- a/app/Http/Middleware/QueryLogging.php +++ b/app/Http/Middleware/QueryLogging.php @@ -23,6 +23,7 @@ class QueryLogging // Enable query logging for development if (Utils::isNinjaDev()) { DB::enableQueryLog(); + $timeStart = microtime(true); } $response = $next($request); @@ -32,7 +33,9 @@ class QueryLogging if (strstr($request->url(), '_debugbar') === false) { $queries = DB::getQueryLog(); $count = count($queries); - Log::info($request->method() . ' - ' . $request->url() . ": $count queries"); + $timeEnd = microtime(true); + $time = $timeEnd - $timeStart; + Log::info($request->method() . ' - ' . $request->url() . ": $count queries - " . $time); //Log::info($queries); } } diff --git a/app/Http/Middleware/SessionDataCheckMiddleware.php b/app/Http/Middleware/SessionDataCheckMiddleware.php deleted file mode 100644 index c626fa6ee8..0000000000 --- a/app/Http/Middleware/SessionDataCheckMiddleware.php +++ /dev/null @@ -1,31 +0,0 @@ -getLastUsed(); - - if ( ! $bag || $elapsed > $max) { - $request->session()->flush(); - Auth::logout(); - $request->session()->flash('warning', trans('texts.inactive_logout')); - } - - return $next($request); - } -} diff --git a/app/Http/Middleware/StartupCheck.php b/app/Http/Middleware/StartupCheck.php index 1aee1c59b8..776974a894 100644 --- a/app/Http/Middleware/StartupCheck.php +++ b/app/Http/Middleware/StartupCheck.php @@ -13,7 +13,7 @@ use Event; use Schema; use App\Models\Language; use App\Models\InvoiceDesign; -use App\Events\UserSettingsChanged; +use App\Events\UserLoggedIn; use App\Libraries\CurlUtils; /** @@ -118,13 +118,13 @@ class StartupCheck // Make sure the account/user localization settings are in the session if (Auth::check() && !Session::has(SESSION_TIMEZONE)) { - Event::fire(new UserSettingsChanged()); + Event::fire(new UserLoggedIn()); } // Check if the user is claiming a license (ie, additional invoices, white label, etc.) - if (isset($_SERVER['REQUEST_URI'])) { + if ( ! Utils::isNinjaProd() && isset($_SERVER['REQUEST_URI'])) { $claimingLicense = Utils::startsWith($_SERVER['REQUEST_URI'], '/claim_license'); - if (!$claimingLicense && Input::has('license_key') && Input::has('product_id')) { + if ( ! $claimingLicense && Input::has('license_key') && Input::has('product_id')) { $licenseKey = Input::get('license_key'); $productId = Input::get('product_id'); @@ -154,6 +154,8 @@ class StartupCheck $company->save(); Session::flash('message', trans('texts.bought_white_label')); + } else { + Session::flash('error', trans('texts.invalid_white_label_license')); } } } diff --git a/app/Http/Requests/EntityRequest.php b/app/Http/Requests/EntityRequest.php index 94dae7a443..88ba623d03 100644 --- a/app/Http/Requests/EntityRequest.php +++ b/app/Http/Requests/EntityRequest.php @@ -36,11 +36,11 @@ class EntityRequest extends Request { $class = Utils::getEntityClass($this->entityType); - if (method_exists($class, 'trashed')) { - $this->entity = $class::scope($publicId)->withTrashed()->firstOrFail(); - } else { - $this->entity = $class::scope($publicId)->firstOrFail(); - } + if (method_exists($class, 'trashed')) { + $this->entity = $class::scope($publicId)->withTrashed()->firstOrFail(); + } else { + $this->entity = $class::scope($publicId)->firstOrFail(); + } return $this->entity; } diff --git a/app/Http/Requests/UpdateTaxRateRequest.php b/app/Http/Requests/UpdateTaxRateRequest.php index 381990f32b..e0a741ee96 100644 --- a/app/Http/Requests/UpdateTaxRateRequest.php +++ b/app/Http/Requests/UpdateTaxRateRequest.php @@ -4,7 +4,6 @@ class UpdateTaxRateRequest extends TaxRateRequest { - // Expenses /** * Determine if the user is authorized to make this request. * diff --git a/app/Http/ViewComposers/ClientPortalHeaderComposer.php b/app/Http/ViewComposers/ClientPortalHeaderComposer.php new file mode 100644 index 0000000000..ac0d6ca686 --- /dev/null +++ b/app/Http/ViewComposers/ClientPortalHeaderComposer.php @@ -0,0 +1,52 @@ +with('client') + ->first(); + + if ( ! $contact || $contact->is_deleted) { + return false; + } + + $client = $contact->client; + + $hasDocuments = DB::table('invoices') + ->where('invoices.client_id', '=', $client->id) + ->whereNull('invoices.deleted_at') + ->join('documents', 'documents.invoice_id', '=', 'invoices.id') + ->count(); + + $view->with('hasQuotes', $client->quotes->count()); + $view->with('hasCredits', $client->creditsWithBalance->count()); + $view->with('hasDocuments', $hasDocuments); + } +} diff --git a/app/Http/routes.php b/app/Http/routes.php index bdaa3837f3..a40d1e0656 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -1,6 +1,5 @@ 'auth:client'], function() { Route::get('view/{invitation_key}', 'ClientPortalController@view'); Route::get('download/{invitation_key}', 'ClientPortalController@download'); + Route::put('sign/{invitation_key}', 'ClientPortalController@sign'); Route::get('view', 'HomeController@viewLogo'); Route::get('approve/{invitation_key}', 'QuoteController@approve'); Route::get('payment/{invitation_key}/{gateway_type?}/{source_id?}', 'OnlinePaymentController@showPayment'); @@ -52,18 +52,19 @@ Route::group(['middleware' => 'auth:client'], function() { Route::post('client/payment_methods/default', 'ClientPortalController@setDefaultPaymentMethod'); Route::post('client/payment_methods/{source_id}/remove', 'ClientPortalController@removePaymentMethod'); Route::get('client/quotes', 'ClientPortalController@quoteIndex'); + Route::get('client/credits', 'ClientPortalController@creditIndex'); Route::get('client/invoices', 'ClientPortalController@invoiceIndex'); Route::get('client/invoices/recurring', 'ClientPortalController@recurringInvoiceIndex'); Route::post('client/invoices/auto_bill', 'ClientPortalController@setAutoBill'); Route::get('client/documents', 'ClientPortalController@documentIndex'); Route::get('client/payments', 'ClientPortalController@paymentIndex'); - Route::get('client/dashboard', 'ClientPortalController@dashboard'); - Route::get('client/dashboard/{contact_key}', 'ClientPortalController@contactIndex'); + Route::get('client/dashboard/{contact_key?}', 'ClientPortalController@dashboard'); Route::get('client/documents/js/{documents}/{filename}', 'ClientPortalController@getDocumentVFSJS'); Route::get('client/documents/{invitation_key}/{documents}/{filename?}', 'ClientPortalController@getDocument'); Route::get('client/documents/{invitation_key}/{filename?}', 'ClientPortalController@getInvoiceDocumentsZip'); Route::get('api/client.quotes', ['as'=>'api.client.quotes', 'uses'=>'ClientPortalController@quoteDatatable']); + Route::get('api/client.credits', ['as'=>'api.client.credits', 'uses'=>'ClientPortalController@creditDatatable']); Route::get('api/client.invoices', ['as'=>'api.client.invoices', 'uses'=>'ClientPortalController@invoiceDatatable']); Route::get('api/client.recurring_invoices', ['as'=>'api.client.recurring_invoices', 'uses'=>'ClientPortalController@recurringInvoiceDatatable']); Route::get('api/client.documents', ['as'=>'api.client.documents', 'uses'=>'ClientPortalController@documentDatatable']); @@ -128,11 +129,12 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('hide_message', 'HomeController@hideMessage'); Route::get('force_inline_pdf', 'UserController@forcePDFJS'); Route::get('account/get_search_data', ['as' => 'get_search_data', 'uses' => 'AccountController@getSearchData']); - Route::get('check_invoice_number/{invoice_number}', 'InvoiceController@checkInvoiceNumber'); - Route::get('save_sidebar_state', 'UserController@saveSidebarState'); + Route::get('check_invoice_number/{invoice_id?}', 'InvoiceController@checkInvoiceNumber'); + Route::post('save_sidebar_state', 'UserController@saveSidebarState'); Route::get('settings/user_details', 'AccountController@showUserDetails'); Route::post('settings/user_details', 'AccountController@saveUserDetails'); + Route::post('settings/payment_gateway_limits', 'AccountController@savePaymentGatewayLimits'); Route::post('users/change_password', 'UserController@changePassword'); Route::resource('clients', 'ClientController'); @@ -186,6 +188,11 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('api/credits/{client_id?}', ['as'=>'api.credits', 'uses'=>'CreditController@getDatatable']); Route::post('credits/bulk', 'CreditController@bulk'); + Route::get('api/products', ['as'=>'api.products', 'uses'=>'ProductController@getDatatable']); + Route::resource('products', 'ProductController'); + Route::post('products/bulk', 'ProductController@bulk'); + + Route::get('/resend_confirmation', 'AccountController@resendConfirmation'); Route::post('/update_setup', 'AppController@updateSetup'); @@ -228,10 +235,6 @@ Route::group([ Route::resource('tokens', 'TokenController'); Route::post('tokens/bulk', 'TokenController@bulk'); - Route::get('api/products', ['as'=>'api.products', 'uses'=>'ProductController@getDatatable']); - Route::resource('products', 'ProductController'); - Route::post('products/bulk', 'ProductController@bulk'); - Route::get('api/tax_rates', ['as'=>'api.tax_rates', 'uses'=>'TaxRateController@getDatatable']); Route::resource('tax_rates', 'TaxRateController'); Route::post('tax_rates/bulk', 'TaxRateController@bulk'); @@ -281,6 +284,7 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function() { Route::get('ping', 'AccountApiController@ping'); Route::post('login', 'AccountApiController@login'); + Route::post('oauth_login', 'AccountApiController@oauthLogin'); Route::post('register', 'AccountApiController@register'); Route::get('static', 'AccountApiController@getStaticData'); Route::get('accounts', 'AccountApiController@show'); @@ -305,12 +309,8 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function() Route::post('update_notifications', 'AccountApiController@updatePushNotifications'); Route::get('dashboard', 'DashboardApiController@index'); Route::resource('documents', 'DocumentAPIController'); - - // Vendor Route::resource('vendors', 'VendorApiController'); - - //Expense - Route::resource('expenses', 'ExpenseApiController'); + Route::resource('expense_categories', 'ExpenseCategoryApiController'); }); // Redirects for legacy links @@ -430,56 +430,50 @@ if (!defined('CONTACT_EMAIL')) { define('ACTIVITY_TYPE_CREATE_CLIENT', 1); define('ACTIVITY_TYPE_ARCHIVE_CLIENT', 2); define('ACTIVITY_TYPE_DELETE_CLIENT', 3); - define('ACTIVITY_TYPE_CREATE_INVOICE', 4); define('ACTIVITY_TYPE_UPDATE_INVOICE', 5); define('ACTIVITY_TYPE_EMAIL_INVOICE', 6); define('ACTIVITY_TYPE_VIEW_INVOICE', 7); define('ACTIVITY_TYPE_ARCHIVE_INVOICE', 8); define('ACTIVITY_TYPE_DELETE_INVOICE', 9); - define('ACTIVITY_TYPE_CREATE_PAYMENT', 10); //define('ACTIVITY_TYPE_UPDATE_PAYMENT', 11); define('ACTIVITY_TYPE_ARCHIVE_PAYMENT', 12); define('ACTIVITY_TYPE_DELETE_PAYMENT', 13); - define('ACTIVITY_TYPE_VOIDED_PAYMENT', 39); - define('ACTIVITY_TYPE_REFUNDED_PAYMENT', 40); - define('ACTIVITY_TYPE_FAILED_PAYMENT', 41); - define('ACTIVITY_TYPE_CREATE_CREDIT', 14); //define('ACTIVITY_TYPE_UPDATE_CREDIT', 15); define('ACTIVITY_TYPE_ARCHIVE_CREDIT', 16); define('ACTIVITY_TYPE_DELETE_CREDIT', 17); - define('ACTIVITY_TYPE_CREATE_QUOTE', 18); define('ACTIVITY_TYPE_UPDATE_QUOTE', 19); define('ACTIVITY_TYPE_EMAIL_QUOTE', 20); define('ACTIVITY_TYPE_VIEW_QUOTE', 21); define('ACTIVITY_TYPE_ARCHIVE_QUOTE', 22); define('ACTIVITY_TYPE_DELETE_QUOTE', 23); - define('ACTIVITY_TYPE_RESTORE_QUOTE', 24); define('ACTIVITY_TYPE_RESTORE_INVOICE', 25); define('ACTIVITY_TYPE_RESTORE_CLIENT', 26); define('ACTIVITY_TYPE_RESTORE_PAYMENT', 27); define('ACTIVITY_TYPE_RESTORE_CREDIT', 28); define('ACTIVITY_TYPE_APPROVE_QUOTE', 29); - - // Vendors define('ACTIVITY_TYPE_CREATE_VENDOR', 30); define('ACTIVITY_TYPE_ARCHIVE_VENDOR', 31); define('ACTIVITY_TYPE_DELETE_VENDOR', 32); define('ACTIVITY_TYPE_RESTORE_VENDOR', 33); - - // expenses define('ACTIVITY_TYPE_CREATE_EXPENSE', 34); define('ACTIVITY_TYPE_ARCHIVE_EXPENSE', 35); define('ACTIVITY_TYPE_DELETE_EXPENSE', 36); define('ACTIVITY_TYPE_RESTORE_EXPENSE', 37); - - // tasks + define('ACTIVITY_TYPE_VOIDED_PAYMENT', 39); + define('ACTIVITY_TYPE_REFUNDED_PAYMENT', 40); + define('ACTIVITY_TYPE_FAILED_PAYMENT', 41); define('ACTIVITY_TYPE_CREATE_TASK', 42); define('ACTIVITY_TYPE_UPDATE_TASK', 43); + define('ACTIVITY_TYPE_ARCHIVE_TASK', 44); + define('ACTIVITY_TYPE_DELETE_TASK', 45); + define('ACTIVITY_TYPE_RESTORE_TASK', 46); + define('ACTIVITY_TYPE_UPDATE_EXPENSE', 47); + define('DEFAULT_INVOICE_NUMBER', '0001'); define('RECENTLY_VIEWED_LIMIT', 20); @@ -491,6 +485,7 @@ if (!defined('CONTACT_EMAIL')) { define('MAX_IFRAME_URL_LENGTH', 250); define('MAX_LOGO_FILE_SIZE', 200); // KB define('MAX_FAILED_LOGINS', 10); + define('MAX_INVOICE_ITEMS', env('MAX_INVOICE_ITEMS', 100)); define('MAX_DOCUMENT_SIZE', env('MAX_DOCUMENT_SIZE', 10000));// KB define('MAX_EMAIL_DOCUMENTS_SIZE', env('MAX_EMAIL_DOCUMENTS_SIZE', 10000));// Total KB define('MAX_ZIP_DOCUMENTS_SIZE', env('MAX_EMAIL_DOCUMENTS_SIZE', 30000));// Total KB (uncompressed) @@ -547,6 +542,7 @@ if (!defined('CONTACT_EMAIL')) { define('SESSION_TIMEZONE', 'timezone'); define('SESSION_CURRENCY', 'currency'); + define('SESSION_CURRENCY_DECORATOR', 'currency_decorator'); define('SESSION_DATE_FORMAT', 'dateFormat'); define('SESSION_DATE_PICKER_FORMAT', 'datePickerFormat'); define('SESSION_DATETIME_FORMAT', 'datetimeFormat'); @@ -601,6 +597,7 @@ if (!defined('CONTACT_EMAIL')) { define('GATEWAY_CYBERSOURCE', 49); define('GATEWAY_WEPAY', 60); define('GATEWAY_BRAINTREE', 61); + define('GATEWAY_CUSTOM', 62); // The customer exists, but only as a local concept // The remote gateway doesn't understand the concept of customers @@ -623,6 +620,7 @@ if (!defined('CONTACT_EMAIL')) { define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest')); define('NINJA_DATE', '2000-01-01'); define('NINJA_VERSION', '2.7.2' . env('NINJA_VERSION_SUFFIX')); + define('NINJA_VERSION', '2.8.0' . env('NINJA_VERSION_SUFFIX')); define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja')); define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja')); @@ -639,6 +637,7 @@ if (!defined('CONTACT_EMAIL')) { define('EMAIL_MARKUP_URL', env('EMAIL_MARKUP_URL', 'https://developers.google.com/gmail/markup')); define('OFX_HOME_URL', env('OFX_HOME_URL', 'http://www.ofxhome.com/index.php/home/directory/all')); define('GOOGLE_ANALYITCS_URL', env('GOOGLE_ANALYITCS_URL', 'https://www.google-analytics.com/collect')); + define('TRANSIFEX_URL', env('TRANSIFEX_URL', 'https://www.transifex.com/invoice-ninja/invoice-ninja')); define('MSBOT_LOGIN_URL', 'https://login.microsoftonline.com/common/oauth2/v2.0/token'); define('MSBOT_LUIS_URL', 'https://api.projectoxford.ai/luis/v1/application'); @@ -704,11 +703,12 @@ if (!defined('CONTACT_EMAIL')) { define('PAYMENT_METHOD_STATUS_VERIFICATION_FAILED', 'verification_failed'); define('PAYMENT_METHOD_STATUS_VERIFIED', 'verified'); - define('GATEWAY_TYPE_CREDIT_CARD', 'credit_card'); - define('GATEWAY_TYPE_BANK_TRANSFER', 'bank_transfer'); - define('GATEWAY_TYPE_PAYPAL', 'paypal'); - define('GATEWAY_TYPE_BITCOIN', 'bitcoin'); - define('GATEWAY_TYPE_DWOLLA', 'dwolla'); + define('GATEWAY_TYPE_CREDIT_CARD', 1); + define('GATEWAY_TYPE_BANK_TRANSFER', 2); + define('GATEWAY_TYPE_PAYPAL', 3); + define('GATEWAY_TYPE_BITCOIN', 4); + define('GATEWAY_TYPE_DWOLLA', 5); + define('GATEWAY_TYPE_CUSTOM', 6); define('GATEWAY_TYPE_TOKEN', 'token'); define('REMINDER1', 'reminder1'); @@ -744,6 +744,10 @@ if (!defined('CONTACT_EMAIL')) { define('BANK_LIBRARY_OFX', 1); + define('CURRENCY_DECORATOR_CODE', 'code'); + define('CURRENCY_DECORATOR_SYMBOL', 'symbol'); + define('CURRENCY_DECORATOR_NONE', 'none'); + define('RESELLER_REVENUE_SHARE', 'A'); define('RESELLER_LIMITED_USERS', 'B'); @@ -852,6 +856,7 @@ if (!defined('CONTACT_EMAIL')) { 'invoiceStatus' => 'App\Models\InvoiceStatus', 'frequencies' => 'App\Models\Frequency', 'gateways' => 'App\Models\Gateway', + 'gatewayTypes' => 'App\Models\GatewayType', 'fonts' => 'App\Models\Font', 'banks' => 'App\Models\Bank', ]; diff --git a/app/Includes/parsecsv.lib.php b/app/Includes/parsecsv.lib.php index 7fe04984db..797beef303 100644 --- a/app/Includes/parsecsv.lib.php +++ b/app/Includes/parsecsv.lib.php @@ -1,21 +1,21 @@ output (true, 'movies.csv', $array); ---------------- - + */ @@ -83,87 +83,87 @@ class parseCSV { * Configuration * - set these options with $object->var_name = 'value'; */ - + # use first line/entry as field names var $heading = true; - + # override field names var $fields = []; - + # sort entries by this field var $sort_by = null; var $sort_reverse = false; - + # delimiter (comma) and enclosure (double quote) var $delimiter = ','; var $enclosure = '"'; - + # basic SQL-like conditions for row matching var $conditions = null; - + # number of rows to ignore from beginning of data var $offset = null; - + # limits the number of returned rows to specified amount var $limit = null; - + # number of rows to analyze when attempting to auto-detect delimiter var $auto_depth = 15; - + # characters to ignore when attempting to auto-detect delimiter var $auto_non_chars = "a-zA-Z0-9\n\r"; - + # preferred delimiter characters, only used when all filtering method # returns multiple possible delimiters (happens very rarely) var $auto_preferred = ",;\t.:|"; - + # character encoding options var $convert_encoding = false; var $input_encoding = 'ISO-8859-1'; var $output_encoding = 'ISO-8859-1'; - + # used by unparse(), save(), and output() functions var $linefeed = "\r\n"; - + # only used by output() function var $output_delimiter = ','; var $output_filename = 'data.csv'; - - + + /** * Internal variables */ - + # current file var $file; - + # loaded file contents var $file_data; - + # array of field values in data parsed var $titles = []; - + # two dimentional array of CSV data var $data = []; - - + + /** * Constructor * @param input CSV file or string * @return nothing */ - function parseCSV ($input = null, $offset = null, $limit = null, $conditions = null) { + function __construct ($input = null, $offset = null, $limit = null, $conditions = null) { if ( $offset !== null ) $this->offset = $offset; if ( $limit !== null ) $this->limit = $limit; if ( count($conditions) > 0 ) $this->conditions = $conditions; if ( !empty($input) ) $this->parse($input); } - - + + // ============================================== // ----- [ Main Functions ] --------------------- // ============================================== - + /** * Parse CSV file or string * @param input CSV file or string @@ -184,7 +184,7 @@ class parseCSV { } return true; } - + /** * Save changes, or new file and/or data * @param file file to save to @@ -199,7 +199,7 @@ class parseCSV { $is_php = ( preg_match('/\.php$/i', $file) ) ? true : false ; return $this->_wfile($file, $this->unparse($data, $fields, $append, $is_php), $mode); } - + /** * Generate CSV based string for output * @param output if true, prints headers and strings to browser @@ -220,7 +220,7 @@ class parseCSV { } return $data; } - + /** * Convert character encoding * @param input input character encoding, uses default if left blank @@ -232,7 +232,7 @@ class parseCSV { if ( $input !== null ) $this->input_encoding = $input; if ( $output !== null ) $this->output_encoding = $output; } - + /** * Auto-Detect Delimiter: Find delimiter by analyzing a specific number of * rows to determine most probable delimiter character @@ -244,13 +244,13 @@ class parseCSV { * @return delimiter character */ function auto ($file = null, $parse = true, $search_depth = null, $preferred = null, $enclosure = null) { - + if ( $file === null ) $file = $this->file; if ( empty($search_depth) ) $search_depth = $this->auto_depth; if ( $enclosure === null ) $enclosure = $this->enclosure; - + if ( $preferred === null ) $preferred = $this->auto_preferred; - + if ( empty($this->file_data) ) { if ( $this->_check_data($file) ) { $data = &$this->file_data; @@ -258,24 +258,24 @@ class parseCSV { } else { $data = &$this->file_data; } - + $chars = []; $strlen = strlen($data); $enclosed = false; $n = 1; $to_end = true; - + // walk specific depth finding posssible delimiter characters for ( $i=0; $i < $strlen; $i++ ) { $ch = $data{$i}; $nch = ( isset($data{$i+1}) ) ? $data{$i+1} : false ; $pch = ( isset($data{$i-1}) ) ? $data{$i-1} : false ; - + // open and closing quotes if ( $ch == $enclosure && (!$enclosed || $nch != $enclosure) ) { $enclosed = ( $enclosed ) ? false : true ; - - // inline quotes + + // inline quotes } elseif ( $ch == $enclosure && $enclosed ) { $i++; @@ -287,7 +287,7 @@ class parseCSV { } else { $n++; } - + // count character } elseif (!$enclosed) { if ( !preg_match('/['.preg_quote($this->auto_non_chars, '/').']/i', $ch) ) { @@ -299,7 +299,7 @@ class parseCSV { } } } - + // filtering $depth = ( $to_end ) ? $n-1 : $n ; $filtered = []; @@ -308,24 +308,24 @@ class parseCSV { $filtered[$match] = $char; } } - + // capture most probable delimiter ksort($filtered); $delimiter = reset($filtered); $this->delimiter = $delimiter; - + // parse data if ( $parse ) $this->data = $this->parse_string(); - + return $delimiter; - + } - - + + // ============================================== // ----- [ Core Functions ] --------------------- // ============================================== - + /** * Read file to string and call parse_string() * @param file local CSV file @@ -336,7 +336,7 @@ class parseCSV { if ( empty($this->file_data) ) $this->load_data($file); return ( !empty($this->file_data) ) ? $this->parse_string() : false ; } - + /** * Parse CSV strings to arrays * @param data CSV string @@ -348,7 +348,7 @@ class parseCSV { $data = &$this->file_data; } else return false; } - + $rows = []; $row = []; $row_count = 0; @@ -358,19 +358,19 @@ class parseCSV { $enclosed = false; $was_enclosed = false; $strlen = strlen($data); - + // walk through each character for ( $i=0; $i < $strlen; $i++ ) { $ch = $data{$i}; $nch = ( isset($data{$i+1}) ) ? $data{$i+1} : false ; $pch = ( isset($data{$i-1}) ) ? $data{$i-1} : false ; - + // open and closing quotes if ( $ch == $this->enclosure && (!$enclosed || $nch != $this->enclosure) ) { $enclosed = ( $enclosed ) ? false : true ; if ( $enclosed ) $was_enclosed = true; - - // inline quotes + + // inline quotes } elseif ( $ch == $this->enclosure && $enclosed ) { $current .= $ch; $i++; @@ -382,7 +382,7 @@ class parseCSV { $row[$key] = $current; $current = ''; $col++; - + // end of row if ( $ch == "\n" || $ch == "\r" ) { if ( $this->_validate_offset($row_count) && $this->_validate_row_conditions($row, $this->conditions) ) { @@ -406,7 +406,7 @@ class parseCSV { $i = $strlen; } } - + // append character to current field } else { $current .= $ch; @@ -421,7 +421,7 @@ class parseCSV { } return $rows; } - + /** * Create CSV data from array * @param data 2D array with data @@ -436,10 +436,10 @@ class parseCSV { if ( !is_array($data) || empty($data) ) $data = &$this->data; if ( !is_array($fields) || empty($fields) ) $fields = &$this->titles; if ( $delimiter === null ) $delimiter = $this->delimiter; - + $string = ( $is_php ) ? "".$this->linefeed : '' ; $entry = []; - + // create heading if ( $this->heading && !$append ) { foreach( $fields as $key => $value ) { @@ -448,7 +448,7 @@ class parseCSV { $string .= implode($delimiter, $entry).$this->linefeed; $entry = []; } - + // create data foreach( $data as $key => $row ) { foreach( $row as $field => $value ) { @@ -457,10 +457,10 @@ class parseCSV { $string .= implode($delimiter, $entry).$this->linefeed; $entry = []; } - + return $string; } - + /** * Load local file or string * @param input local CSV file @@ -488,16 +488,16 @@ class parseCSV { } return false; } - - + + // ============================================== // ----- [ Internal Functions ] ----------------- // ============================================== - + /** * Validate a row against specified conditions * @param row array with values from a row - * @param conditions specified conditions that the row must match + * @param conditions specified conditions that the row must match * @return true of false */ function _validate_row_conditions ($row = [], $conditions = null) { @@ -523,11 +523,11 @@ class parseCSV { } return false; } - + /** * Validate a row against a single condition * @param row array with values from a row - * @param condition specified condition that the row must match + * @param condition specified condition that the row must match * @return true of false */ function _validate_row_condition ($row, $condition) { @@ -583,7 +583,7 @@ class parseCSV { } return '1'; } - + /** * Validates if the row is within the offset or not if sorting is disabled * @param current_row the current row number being processed @@ -593,7 +593,7 @@ class parseCSV { if ( $this->sort_by === null && $this->offset !== null && $current_row < $this->offset ) return false; return true; } - + /** * Enclose values if needed * - only used by unparse() @@ -611,7 +611,7 @@ class parseCSV { } return $value; } - + /** * Check file data * @param file local filename @@ -624,8 +624,8 @@ class parseCSV { } return true; } - - + + /** * Check if passed info might be delimiter * - only used by find_delimiter() @@ -656,7 +656,7 @@ class parseCSV { } else return false; } } - + /** * Read local file * @param file local filename @@ -689,7 +689,7 @@ class parseCSV { } return false; } - + } -?> \ No newline at end of file +?> diff --git a/app/Libraries/HistoryUtils.php b/app/Libraries/HistoryUtils.php index feb5d331d4..8d34c2bed6 100644 --- a/app/Libraries/HistoryUtils.php +++ b/app/Libraries/HistoryUtils.php @@ -24,6 +24,8 @@ class HistoryUtils ACTIVITY_TYPE_CREATE_CLIENT, ACTIVITY_TYPE_CREATE_TASK, ACTIVITY_TYPE_UPDATE_TASK, + ACTIVITY_TYPE_CREATE_EXPENSE, + ACTIVITY_TYPE_UPDATE_EXPENSE, ACTIVITY_TYPE_CREATE_INVOICE, ACTIVITY_TYPE_UPDATE_INVOICE, ACTIVITY_TYPE_EMAIL_INVOICE, @@ -35,7 +37,7 @@ class HistoryUtils ]; $activities = Activity::scope() - ->with(['client.contacts', 'invoice', 'task']) + ->with(['client.contacts', 'invoice', 'task', 'expense']) ->whereIn('user_id', $userIds) ->whereIn('activity_type_id', $activityTypes) ->orderBy('id', 'asc') @@ -52,6 +54,12 @@ class HistoryUtils continue; } $entity->setRelation('client', $activity->client); + } else if ($activity->activity_type_id == ACTIVITY_TYPE_CREATE_EXPENSE || $activity->activity_type_id == ACTIVITY_TYPE_UPDATE_EXPENSE) { + $entity = $activity->expense; + if ( ! $entity) { + continue; + } + $entity->setRelation('client', $activity->client); } else { $entity = $activity->invoice; if ( ! $entity) { diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 448ca17bb0..2dede0e95d 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -22,6 +22,9 @@ class Utils "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", ]; + public static $months = [ + 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december', + ]; public static function isRegistered() { @@ -92,6 +95,17 @@ class Utils return Utils::getResllerType() ? true : false; } + public static function isWhiteLabel() + { + if (Utils::isNinjaProd()) { + return false; + } + + $account = \App\Models\Account::first(); + + return $account && $account->hasFeature(FEATURE_WHITE_LABEL); + } + public static function getResllerType() { return isset($_ENV['RESELLER_TYPE']) ? $_ENV['RESELLER_TYPE'] : false; @@ -151,6 +165,11 @@ class Utils return Auth::check() && Auth::user()->isTrial(); } + public static function isPaidPro() + { + return static::isPro() && ! static::isTrial(); + } + public static function isEnglish() { return App::getLocale() == 'en'; @@ -186,7 +205,7 @@ class Utils $response = new stdClass(); $response->message = isset($_ENV["{$userType}_MESSAGE"]) ? $_ENV["{$userType}_MESSAGE"] : ''; $response->id = isset($_ENV["{$userType}_ID"]) ? $_ENV["{$userType}_ID"] : ''; - $response->version = env('NINJA_SELF_HOST_VERSION', NINJA_VERSION); + $response->version = NINJA_VERSION; return $response; } @@ -354,7 +373,7 @@ class Utils return $data->first(); } - public static function formatMoney($value, $currencyId = false, $countryId = false, $showCode = false) + public static function formatMoney($value, $currencyId = false, $countryId = false, $decorator = false) { $value = floatval($value); @@ -362,6 +381,10 @@ class Utils $currencyId = Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY); } + if (!$decorator) { + $decorator = Session::get(SESSION_CURRENCY_DECORATOR, CURRENCY_DECORATOR_SYMBOL); + } + if (!$countryId && Auth::check()) { $countryId = Auth::user()->account->country_id; } @@ -387,7 +410,9 @@ class Utils $value = number_format($value, $precision, $decimal, $thousand); $symbol = $currency->symbol; - if ($showCode || !$symbol) { + if ($decorator == CURRENCY_DECORATOR_NONE) { + return $value; + } elseif ($decorator == CURRENCY_DECORATOR_CODE || ! $symbol) { return "{$value} {$code}"; } elseif ($swapSymbol) { return "{$value} " . trim($symbol); @@ -635,11 +660,22 @@ class Utils } } + public static function getMonthOptions() + { + $months = []; + + for ($i=1; $i<=count(static::$months); $i++) { + $month = static::$months[$i-1]; + $number = $i < 10 ? '0' . $i : $i; + $months["2000-{$number}-01"] = trans("texts.{$month}"); + } + + return $months; + } + private static function getMonth($offset) { - $months = ['january', 'february', 'march', 'april', 'may', 'june', - 'july', 'august', 'september', 'october', 'november', 'december', ]; - + $months = static::$months; $month = intval(date('n')) - 1; $month += $offset; @@ -1029,4 +1065,66 @@ class Utils return trans('texts.'.strtolower($day)); }); } + + public static function getDocsUrl($path) + { + $page = ''; + $parts = explode('/', $path); + $first = count($parts) ? $parts[0] : false; + $second = count($parts) > 1 ? $parts[1] : false; + + $entityTypes = [ + 'clients', + 'invoices', + 'payments', + 'recurring_invoices', + 'credits', + 'quotes', + 'tasks', + 'expenses', + 'vendors', + ]; + + if ($path == 'dashboard') { + $page = '/introduction.html#dashboard'; + } elseif (in_array($path, $entityTypes)) { + $page = "/{$path}.html#list-" . str_replace('_', '-', $path); + } elseif (in_array($first, $entityTypes)) { + $action = ($first == 'payments' || $first == 'credits') ? 'enter' : 'create'; + $page = "/{$first}.html#{$action}-" . substr(str_replace('_', '-', $first), 0, -1); + } elseif ($first == 'expense_categories') { + $page = '/expenses.html#expense-categories'; + } elseif ($first == 'settings') { + if ($second == 'bank_accounts') { + $page = ''; // TODO write docs + } elseif (in_array($second, \App\Models\Account::$basicSettings)) { + if ($second == 'products') { + $second = 'product_library'; + } elseif ($second == 'notifications') { + $second = 'email_notifications'; + } + $page = '/settings.html#' . str_replace('_', '-', $second); + } elseif (in_array($second, \App\Models\Account::$advancedSettings)) { + $page = "/{$second}.html"; + } elseif ($second == 'customize_design') { + $page = '/invoice_design.html#customize'; + } + } elseif ($first == 'tax_rates') { + $page = '/settings.html#tax-rates'; + } elseif ($first == 'products') { + $page = '/settings.html#product-library'; + } elseif ($first == 'users') { + $page = '/user_management.html#create-user'; + } + + return url(NINJA_DOCS_URL . $page); + } + + public static function calculateTaxes($amount, $taxRate1, $taxRate2) + { + $tax1 = round($amount * $taxRate1 / 100, 2); + $tax2 = round($amount * $taxRate2 / 100, 2); + + return round($amount + $tax1 + $tax2, 2); + } } diff --git a/app/Listeners/ActivityListener.php b/app/Listeners/ActivityListener.php index 91e327454d..ee016cbff3 100644 --- a/app/Listeners/ActivityListener.php +++ b/app/Listeners/ActivityListener.php @@ -1,7 +1,5 @@ find($event->invoice->id); + $backupInvoice = Invoice::with('invoice_items', 'client.account', 'client.contacts') + ->withArchived() + ->find($event->invoice->id); $activity = $this->activityRepo->create( $event->invoice, @@ -489,9 +499,92 @@ class ActivityListener */ public function updatedTask(TaskWasUpdated $event) { + if ( ! $event->task->isChanged()) { + return; + } + $this->activityRepo->create( $event->task, ACTIVITY_TYPE_UPDATE_TASK ); } + + public function archivedTask(TaskWasArchived $event) + { + if ($event->task->is_deleted) { + return; + } + + $this->activityRepo->create( + $event->task, + ACTIVITY_TYPE_ARCHIVE_TASK + ); + } + + public function deletedTask(TaskWasDeleted $event) + { + $this->activityRepo->create( + $event->task, + ACTIVITY_TYPE_DELETE_TASK + ); + } + + public function restoredTask(TaskWasRestored $event) + { + $this->activityRepo->create( + $event->task, + ACTIVITY_TYPE_RESTORE_TASK + ); + } + + + public function createdExpense(ExpenseWasCreated $event) + { + $this->activityRepo->create( + $event->expense, + ACTIVITY_TYPE_CREATE_EXPENSE + ); + } + + public function updatedExpense(ExpenseWasUpdated $event) + { + if ( ! $event->expense->isChanged()) { + return; + } + + $this->activityRepo->create( + $event->expense, + ACTIVITY_TYPE_UPDATE_EXPENSE + ); + } + + public function archivedExpense(ExpenseWasArchived $event) + { + if ($event->expense->is_deleted) { + return; + } + + $this->activityRepo->create( + $event->expense, + ACTIVITY_TYPE_ARCHIVE_EXPENSE + ); + } + + public function deletedExpense(ExpenseWasDeleted $event) + { + $this->activityRepo->create( + $event->expense, + ACTIVITY_TYPE_DELETE_EXPENSE + ); + } + + public function restoredExpense(ExpenseWasRestored $event) + { + $this->activityRepo->create( + $event->expense, + ACTIVITY_TYPE_RESTORE_EXPENSE + ); + } + + } diff --git a/app/Models/Account.php b/app/Models/Account.php index d06763ab01..8a81fa24f1 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -69,6 +69,16 @@ class Account extends Eloquent 'enable_second_tax_rate', 'include_item_taxes_inline', 'start_of_week', + 'financial_year_start', + 'enable_client_portal', + 'enable_client_portal_dashboard', + 'enable_portal_password', + 'send_portal_password', + 'enable_buy_now_buttons', + 'show_accept_invoice_terms', + 'show_accept_quote_terms', + 'require_invoice_signature', + 'require_quote_signature', ]; /** @@ -102,6 +112,21 @@ class Account extends Eloquent ACCOUNT_USER_MANAGEMENT, ]; + public static $modules = [ + ENTITY_RECURRING_INVOICE => 1, + ENTITY_CREDIT => 2, + ENTITY_QUOTE => 4, + ENTITY_TASK => 8, + ENTITY_EXPENSE => 16, + ENTITY_VENDOR => 32, + ]; + + public static $dashboardSections = [ + 'total_revenue' => 1, + 'average_invoice' => 2, + 'outstanding' => 4, + ]; + /** * @return \Illuminate\Database\Eloquent\Relations\HasMany */ @@ -438,7 +463,7 @@ class Account extends Eloquent * @param bool $hideSymbol * @return string */ - public function formatMoney($amount, $client = null, $hideSymbol = false) + public function formatMoney($amount, $client = null, $decorator = false) { if ($client && $client->currency_id) { $currencyId = $client->currency_id; @@ -456,9 +481,11 @@ class Account extends Eloquent $countryId = false; } - $hideSymbol = $this->show_currency_code || $hideSymbol; + if ( ! $decorator) { + $decorator = $this->show_currency_code ? CURRENCY_DECORATOR_CODE : CURRENCY_DECORATOR_SYMBOL; + } - return Utils::formatMoney($amount, $currencyId, $countryId, $hideSymbol); + return Utils::formatMoney($amount, $currencyId, $countryId, $decorator); } /** @@ -610,14 +637,14 @@ class Account extends Eloquent /** * @param bool $invitation - * @param bool $gatewayType + * @param mixed $gatewayTypeId * @return bool */ - public function paymentDriver($invitation = false, $gatewayType = false) + public function paymentDriver($invitation = false, $gatewayTypeId = false) { /** @var AccountGateway $accountGateway */ - if ($accountGateway = $this->getGatewayByType($gatewayType)) { - return $accountGateway->paymentDriver($invitation, $gatewayType); + if ($accountGateway = $this->getGatewayByType($gatewayTypeId)) { + return $accountGateway->paymentDriver($invitation, $gatewayTypeId); } return false; @@ -735,6 +762,22 @@ class Account extends Eloquent return Document::getDirectFileUrl($this->logo, $this->getLogoDisk()); } + public function getLogoPath() + { + if ( ! $this->hasLogo()){ + return null; + } + + $disk = $this->getLogoDisk(); + $adapter = $disk->getAdapter(); + + if ($adapter instanceof \League\Flysystem\Adapter\Local) { + return $adapter->applyPathPrefix($this->logo); + } else { + return Document::getDirectFileUrl($this->logo, $this->getLogoDisk()); + } + } + /** * @return mixed */ @@ -1024,6 +1067,7 @@ class Account extends Eloquent $locale = ($client && $client->language_id) ? $client->language->locale : ($this->language_id ? $this->Language->locale : DEFAULT_LOCALE); Session::put(SESSION_CURRENCY, $currencyId); + Session::put(SESSION_CURRENCY_DECORATOR, $this->show_currency_code ? CURRENCY_DECORATOR_CODE : CURRENCY_DECORATOR_SYMBOL); Session::put(SESSION_LOCALE, $locale); App::setLocale($locale); @@ -1812,6 +1856,43 @@ class Account extends Eloquent public function getFontFolders(){ return array_map(function($item){return $item['folder'];}, $this->getFontsData()); } + + public function isModuleEnabled($entityType) + { + if (in_array($entityType, [ + ENTITY_CLIENT, + ENTITY_INVOICE, + ENTITY_PRODUCT, + ENTITY_PAYMENT, + ])) { + return true; + } + + return $this->enabled_modules & static::$modules[$entityType]; + } + + public function showAuthenticatePanel($invoice) + { + return $this->showAcceptTerms($invoice) || $this->showSignature($invoice); + } + + public function showAcceptTerms($invoice) + { + if ( ! $this->isPro() || ! $invoice->terms) { + return false; + } + + return $invoice->is_quote ? $this->show_accept_quote_terms : $this->show_accept_invoice_terms; + } + + public function showSignature($invoice) + { + if ( ! $this->isPro()) { + return false; + } + + return $invoice->is_quote ? $this->require_quote_signature : $this->require_invoice_signature; + } } Account::updated(function ($account) diff --git a/app/Models/AccountGateway.php b/app/Models/AccountGateway.php index a2250b9bfc..241d1013f8 100644 --- a/app/Models/AccountGateway.php +++ b/app/Models/AccountGateway.php @@ -73,14 +73,14 @@ class AccountGateway extends EntityModel /** * @param bool $invitation - * @param bool $gatewayType + * @param mixed $gatewayTypeId * @return mixed */ - public function paymentDriver($invitation = false, $gatewayType = false) + public function paymentDriver($invitation = false, $gatewayTypeId = false) { $class = static::paymentDriverClass($this->gateway->provider); - return new $class($this, $invitation, $gatewayType); + return new $class($this, $invitation, $gatewayTypeId); } /** diff --git a/app/Models/AccountGatewaySettings.php b/app/Models/AccountGatewaySettings.php new file mode 100644 index 0000000000..6b95c585c0 --- /dev/null +++ b/app/Models/AccountGatewaySettings.php @@ -0,0 +1,32 @@ +belongsTo('App\Models\GatewayType'); + } + + public function setCreatedAtAttribute($value) + { + // to Disable created_at + } +} diff --git a/app/Models/Activity.php b/app/Models/Activity.php index 5641967634..88a3aa1a9d 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -83,6 +83,11 @@ class Activity extends Eloquent return $this->belongsTo('App\Models\Task')->withTrashed(); } + public function expense() + { + return $this->belongsTo('App\Models\Expense')->withTrashed(); + } + public function key() { return sprintf('%s-%s-%s', $this->activity_type_id, $this->client_id, $this->created_at->timestamp); @@ -101,9 +106,8 @@ class Activity extends Eloquent $contactId = $this->contact_id; $payment = $this->payment; $credit = $this->credit; + $expense = $this->expense; $isSystem = $this->is_system; - - /** @var Task $task */ $task = $this->task; $data = [ @@ -117,6 +121,7 @@ class Activity extends Eloquent 'adjustment' => $this->adjustment ? $account->formatMoney($this->adjustment, $this) : null, 'credit' => $credit ? $account->formatMoney($credit->amount, $client) : null, 'task' => $task ? link_to($task->getRoute(), substr($task->description, 0, 30).'...') : null, + 'expense' => $expense ? link_to($expense->getRoute(), substr($expense->public_notes, 0, 30).'...') : null, ]; return trans("texts.activity_{$activityTypeId}", $data); diff --git a/app/Models/Client.php b/app/Models/Client.php index feef118779..a1db398ef5 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -89,6 +89,10 @@ class Client extends EntityModel * @var string */ public static $fieldWebsite = 'website'; + /** + * @var string + */ + public static $fieldVatNumber = 'vat_number'; /** * @return array @@ -106,6 +110,7 @@ class Client extends EntityModel Client::$fieldCountry, Client::$fieldNotes, Client::$fieldWebsite, + Client::$fieldVatNumber, Contact::$fieldFirstName, Contact::$fieldLastName, Contact::$fieldPhone, @@ -132,6 +137,7 @@ class Client extends EntityModel 'country' => 'country', 'note' => 'notes', 'site|website' => 'website', + 'vat' => 'vat_number', ]; } @@ -159,6 +165,14 @@ class Client extends EntityModel return $this->hasMany('App\Models\Invoice'); } + /** + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function quotes() + { + return $this->hasMany('App\Models\Invoice')->where('invoice_type_id', '=', INVOICE_TYPE_QUOTE); + } + /** * @return \Illuminate\Database\Eloquent\Relations\HasMany */ @@ -223,6 +237,14 @@ class Client extends EntityModel return $this->hasMany('App\Models\Credit'); } + /** + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function creditsWithBalance() + { + return $this->hasMany('App\Models\Credit')->where('balance', '>', 0); + } + /** * @return mixed */ @@ -329,7 +351,7 @@ class Client extends EntityModel $contact = $this->contacts[0]; - return $contact->getDisplayName() ?: trans('texts.unnamed_client'); + return $contact->getDisplayName(); } /** diff --git a/app/Models/EntityModel.php b/app/Models/EntityModel.php index b0933ea159..51ec29da1a 100644 --- a/app/Models/EntityModel.php +++ b/app/Models/EntityModel.php @@ -2,6 +2,7 @@ use Auth; use Eloquent; +use Illuminate\Database\QueryException; use Utils; use Validator; @@ -14,6 +15,12 @@ class EntityModel extends Eloquent * @var bool */ public $timestamps = true; + + /** + * @var bool + */ + protected static $hasPublicId = true; + /** * @var array */ @@ -56,13 +63,16 @@ class EntityModel extends Eloquent $lastEntity = $className::whereAccountId($entity->account_id); } - $lastEntity = $lastEntity->orderBy('public_id', 'DESC') - ->first(); - if ($lastEntity) { - $entity->public_id = $lastEntity->public_id + 1; - } else { - $entity->public_id = 1; + if (static::$hasPublicId) { + $lastEntity = $lastEntity->orderBy('public_id', 'DESC') + ->first(); + + if ($lastEntity) { + $entity->public_id = $lastEntity->public_id + 1; + } else { + $entity->public_id = 1; + } } return $entity; @@ -244,6 +254,7 @@ class EntityModel extends Eloquent $icons = [ 'dashboard' => 'tachometer', 'clients' => 'users', + 'products' => 'cube', 'invoices' => 'file-pdf-o', 'payments' => 'credit-card', 'recurring_invoices' => 'files-o', @@ -253,9 +264,21 @@ class EntityModel extends Eloquent 'expenses' => 'file-image-o', 'vendors' => 'building', 'settings' => 'cog', + 'self-update' => 'download', ]; return array_get($icons, $entityType); } + // isDirty return true if the field's new value is the same as the old one + public function isChanged() + { + foreach ($this->fillable as $field) { + if ($this->$field != $this->getOriginal($field)) { + return true; + } + } + + return false; + } } diff --git a/app/Models/Expense.php b/app/Models/Expense.php index f5cb229fea..b19b7e2637 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -1,5 +1,6 @@ 'amount', + 'category' => 'expense_category', + 'client' => 'client', + 'vendor' => 'vendor', + 'notes|details' => 'public_notes', + 'date' => 'expense_date', + ]; + } /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ @@ -107,7 +132,13 @@ class Expense extends EntityModel */ public function getName() { - return $this->transaction_id ?: '#' . $this->public_id; + if ($this->transaction_id) { + return $this->transaction_id; + } elseif ($this->public_notes) { + return mb_strimwidth($this->public_notes, 0, 16, "..."); + } else { + return '#' . $this->public_id; + } } /** @@ -175,6 +206,11 @@ class Expense extends EntityModel return $query; } + + public function amountWithTax() + { + return Utils::calculateTaxes($this->amount, $this->tax_rate1, $this->tax_rate2); + } } Expense::creating(function ($expense) { @@ -196,7 +232,3 @@ Expense::updated(function ($expense) { Expense::deleting(function ($expense) { $expense->setNullValues(); }); - -Expense::deleted(function ($expense) { - event(new ExpenseWasDeleted($expense)); -}); diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index c13065c73e..6f7c42d90c 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -14,6 +14,12 @@ class Gateway extends Eloquent */ public $timestamps = true; + protected $fillable = [ + 'provider', + 'is_offsite', + 'sort_order', + ]; + /** * @var array */ @@ -39,6 +45,7 @@ class Gateway extends Eloquent GATEWAY_BRAINTREE, GATEWAY_AUTHORIZE_NET, GATEWAY_MOLLIE, + GATEWAY_CUSTOM, ]; // allow adding these gateway if another gateway @@ -174,6 +181,18 @@ class Gateway extends Eloquent */ public function getFields() { - return Omnipay::create($this->provider)->getDefaultParameters(); + if ($this->isCustom()) { + return [ + 'name' => '', + 'text' => '', + ]; + } else { + return Omnipay::create($this->provider)->getDefaultParameters(); + } + } + + public function isCustom() + { + return $this->id === GATEWAY_CUSTOM; } } diff --git a/app/Models/GatewayType.php b/app/Models/GatewayType.php new file mode 100644 index 0000000000..b695dcf3df --- /dev/null +++ b/app/Models/GatewayType.php @@ -0,0 +1,34 @@ +name; + } + + public static function getAliasFromId($id) + { + return Utils::getFromCache($id, 'gatewayTypes')->alias; + } + + public static function getIdFromAlias($alias) + { + return Cache::get('gatewayTypes')->where('alias', $alias)->first()->id; + } +} diff --git a/app/Models/Invitation.php b/app/Models/Invitation.php index a86cc72173..fd0fc7d720 100644 --- a/app/Models/Invitation.php +++ b/app/Models/Invitation.php @@ -134,4 +134,13 @@ class Invitation extends EntityModel $invoice->markViewed(); $client->markLoggedIn(); } + + public function signatureDiv() + { + if ( ! $this->signature_base64) { + return false; + } + + return sprintf('
%s: %s', $this->signature_base64, trans('texts.signed'), Utils::fromSqlDateTime($this->signature_date)); + } } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 14b3d68a90..ad20fc1830 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -514,6 +514,11 @@ class Invoice extends EntityModel implements BalanceAffecting return storage_path() . '/pdfcache/cache-' . $this->id . '.pdf'; } + public function canBePaid() + { + return floatval($this->balance) > 0 && ! $this->is_deleted; + } + /** * @param $invoice * @return string diff --git a/app/Models/PaymentType.php b/app/Models/PaymentType.php index 01e96239fd..811ed0a486 100644 --- a/app/Models/PaymentType.php +++ b/app/Models/PaymentType.php @@ -11,4 +11,12 @@ class PaymentType extends Eloquent * @var bool */ public $timestamps = false; + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function gatewayType() + { + return $this->belongsTo('App\Models\GatewayType'); + } } diff --git a/app/Models/Task.php b/app/Models/Task.php index c1534d8898..5b4ca6b6eb 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -14,6 +14,16 @@ class Task extends EntityModel use SoftDeletes; use PresentableTrait; + /** + * @var array + */ + protected $fillable = [ + 'client_id', + 'description', + 'time_log', + 'is_running', + ]; + /** * @return mixed */ @@ -82,6 +92,18 @@ class Task extends EntityModel return self::calcStartTime($this); } + public function getLastStartTime() + { + $parts = json_decode($this->time_log) ?: []; + + if (count($parts)) { + $index = count($parts) - 1; + return $parts[$index][0]; + } else { + return ''; + } + } + /** * @param $task * @return int @@ -165,6 +187,14 @@ class Task extends EntityModel return '#' . $this->public_id; } + + public function scopeDateRange($query, $startDate, $endDate) + { + $query->whereRaw('cast(substring(time_log, 3, 10) as unsigned) >= ' . $startDate->format('U')); + $query->whereRaw('cast(substring(time_log, 3, 10) as unsigned) <= ' . $endDate->format('U')); + + return $query; + } } diff --git a/app/Models/Traits/PresentsInvoice.php b/app/Models/Traits/PresentsInvoice.php index 0249368368..493a4aecdb 100644 --- a/app/Models/Traits/PresentsInvoice.php +++ b/app/Models/Traits/PresentsInvoice.php @@ -87,6 +87,7 @@ trait PresentsInvoice 'invoice.partial_due', 'invoice.custom_text_value1', 'invoice.custom_text_value2', + '.blank', ], INVOICE_FIELDS_CLIENT => [ 'client.client_name', @@ -97,9 +98,11 @@ trait PresentsInvoice 'client.city_state_postal', 'client.country', 'client.email', + 'client.phone', 'client.contact_name', 'client.custom_value1', 'client.custom_value2', + '.blank', ], INVOICE_FIELDS_ACCOUNT => [ 'account.company_name', @@ -114,6 +117,7 @@ trait PresentsInvoice 'account.country', 'account.custom_value1', 'account.custom_value2', + '.blank', ] ]; @@ -198,6 +202,7 @@ trait PresentsInvoice 'company_name', 'website', 'phone', + 'blank', ]; foreach ($fields as $field) { diff --git a/app/Models/User.php b/app/Models/User.php index 6d98a49c01..f4e49a259a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,19 +5,14 @@ use Event; use App\Libraries\Utils; use App\Events\UserSettingsChanged; use App\Events\UserSignedUp; -use Illuminate\Auth\Authenticatable; -use Illuminate\Foundation\Auth\Access\Authorizable; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Auth\Passwords\CanResetPassword; -use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; -use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; -use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; +use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Database\Eloquent\SoftDeletes; /** * Class User */ -class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract { +class User extends Authenticatable +{ /** * @var array */ @@ -27,8 +22,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac 'edit_all' => 0b0100, ]; - use Authenticatable, Authorizable, CanResetPassword; - /** * The database table used by the model. * @@ -102,26 +95,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac return PERSON_USER; } - /** - * Get the unique identifier for the user. - * - * @return mixed - */ - public function getAuthIdentifier() - { - return $this->getKey(); - } - - /** - * Get the password for the user. - * - * @return string - */ - public function getAuthPassword() - { - return $this->password; - } - /** * Get the e-mail address where password reminders are sent. * @@ -258,31 +231,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac return MAX_NUM_VENDORS; } - - /** - * @return mixed - */ - public function getRememberToken() - { - return $this->remember_token; - } - - /** - * @param string $value - */ - public function setRememberToken($value) - { - $this->remember_token = $value; - } - - /** - * @return string - */ - public function getRememberTokenName() - { - return 'remember_token'; - } - public function clearSession() { $keys = [ @@ -425,7 +373,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac public function caddAddUsers() { - if ( ! Utils::isNinja()) { + if ( ! Utils::isNinjaProd()) { return true; } elseif ( ! $this->hasFeature(FEATURE_USERS)) { return false; @@ -441,6 +389,12 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac return $numUsers < $company->num_users; } + + public function canCreateOrEdit($entityType, $entity = false) + { + return (($entity && $this->can('edit', $entity)) + || (!$entity && $this->can('create', $entityType))); + } } User::updating(function ($user) { diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index 6a387e9246..a68cf42e2f 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -211,7 +211,8 @@ class Vendor extends EntityModel */ public function addVendorContact($data, $isPrimary = false) { - $publicId = isset($data['public_id']) ? $data['public_id'] : false; + //$publicId = isset($data['public_id']) ? $data['public_id'] : false; + $publicId = isset($data['public_id']) ? $data['public_id'] : (isset($data['id']) ? $data['id'] : false); if ($publicId && $publicId != '-1') { $contact = VendorContact::scope($publicId)->firstOrFail(); diff --git a/app/Ninja/Datatables/AccountGatewayDatatable.php b/app/Ninja/Datatables/AccountGatewayDatatable.php index 09c7018734..e7a4be2558 100644 --- a/app/Ninja/Datatables/AccountGatewayDatatable.php +++ b/app/Ninja/Datatables/AccountGatewayDatatable.php @@ -1,10 +1,17 @@ deleted_at) { return $model->name; + } elseif ($model->gateway_id == GATEWAY_CUSTOM) { + $accountGateway = $this->getAccountGateway($model->id); + $name = $accountGateway->getConfigField('name') . ' [' . trans('texts.custom') . ']'; + return link_to("gateways/{$model->public_id}/edit", $name)->toHtml(); } elseif ($model->gateway_id != GATEWAY_WEPAY) { return link_to("gateways/{$model->public_id}/edit", $model->name)->toHtml(); } else { - $accountGateway = AccountGateway::find($model->id); + $accountGateway = $this->getAccountGateway($model->id); $config = $accountGateway->getConfig(); $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; $wepayAccountId = $config->accountId; @@ -45,12 +56,56 @@ class AccountGatewayDatatable extends EntityDatatable } } ], + [ + 'limit', + function ($model) { + if ($model->gateway_id == GATEWAY_CUSTOM) { + $gatewayTypes = [GATEWAY_TYPE_CUSTOM]; + } else { + $accountGateway = $this->getAccountGateway($model->id); + $paymentDriver = $accountGateway->paymentDriver(); + $gatewayTypes = $paymentDriver->gatewayTypes(); + $gatewayTypes = array_diff($gatewayTypes, array(GATEWAY_TYPE_TOKEN)); + } + + $html = ''; + foreach ($gatewayTypes as $gatewayTypeId) { + $accountGatewaySettings = AccountGatewaySettings::scope()->where('account_gateway_settings.gateway_type_id', + '=', $gatewayTypeId)->first(); + $gatewayType = GatewayType::find($gatewayTypeId); + + if (count($gatewayTypes) > 1) { + if ($html) { + $html .= 't |
p,v=i.minHeight&&i.minHeight>f;i.grid=c,b&&(p+=l),v&&(f+=u),m&&(p-=l),g&&(f-=u),/^(se|s|e)$/.test(r)?(n.size.width=p,n.size.height=f):/^(ne)$/.test(r)?(n.size.width=p,n.size.height=f,n.position.top=s.top-d):/^(sw)$/.test(r)?(n.size.width=p,n.size.height=f,n.position.left=s.left-h):((f-u<=0||p-l<=0)&&(e=n._getPaddingPlusBorderDimensions(this)),f-u>0?(n.size.height=f,n.position.top=s.top-d):(f=u-e.height,n.size.height=f,n.position.top=s.top+a.height-f),p-l>0?(n.size.width=p,n.position.left=s.left-h):(p=u-e.height,n.size.width=p,n.position.left=s.left+a.width-p))}});t.ui.resizable,t.widget("ui.dialog",{version:"1.11.2",options:{appendTo:"body",autoOpen:!0,buttons:[],closeOnEscape:!0,closeText:"Close",dialogClass:"",draggable:!0,hide:null,height:"auto",maxHeight:null,maxWidth:null,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",of:window,collision:"fit",using:function(e){var n=t(this).css(e).offset().top;n<0&&t(this).css("top",e.top-n)}},resizable:!0,show:null,title:null,width:300,beforeClose:null,close:null,drag:null,dragStart:null,dragStop:null,focus:null,open:null,resize:null,resizeStart:null,resizeStop:null},sizeRelatedOptions:{buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},resizableRelatedOptions:{maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0},_create:function(){this.originalCss={display:this.element[0].style.display,width:this.element[0].style.width,minHeight:this.element[0].style.minHeight,maxHeight:this.element[0].style.maxHeight,height:this.element[0].style.height},this.originalPosition={parent:this.element.parent(),index:this.element.parent().children().index(this.element)},this.originalTitle=this.element.attr("title"),this.options.title=this.options.title||this.originalTitle,this._createWrapper(),this.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(this.uiDialog),this._createTitlebar(),this._createButtonPane(),this.options.draggable&&t.fn.draggable&&this._makeDraggable(),this.options.resizable&&t.fn.resizable&&this._makeResizable(),this._isOpen=!1,this._trackFocus()},_init:function(){this.options.autoOpen&&this.open()},_appendTo:function(){var e=this.options.appendTo;return e&&(e.jquery||e.nodeType)?t(e):this.document.find(e||"body").eq(0)},_destroy:function(){var t,e=this.originalPosition;this._destroyOverlay(),this.element.removeUniqueId().removeClass("ui-dialog-content ui-widget-content").css(this.originalCss).detach(),this.uiDialog.stop(!0,!0).remove(),this.originalTitle&&this.element.attr("title",this.originalTitle),t=e.parent.children().eq(e.index),t.length&&t[0]!==this.element[0]?t.before(this.element):e.parent.append(this.element)},widget:function(){return this.uiDialog},disable:t.noop,enable:t.noop,close:function(e){var n,i=this;if(this._isOpen&&this._trigger("beforeClose",e)!==!1){if(this._isOpen=!1,this._focusedElement=null,this._destroyOverlay(),this._untrackInstance(),!this.opener.filter(":focusable").focus().length)try{n=this.document[0].activeElement,n&&"body"!==n.nodeName.toLowerCase()&&t(n).blur()}catch(o){}this._hide(this.uiDialog,this.options.hide,function(){i._trigger("close",e)})}},isOpen:function(){return this._isOpen},moveToTop:function(){this._moveToTop()},_moveToTop:function(e,n){var i=!1,o=this.uiDialog.siblings(".ui-front:visible").map(function(){return+t(this).css("z-index")}).get(),a=Math.max.apply(null,o);return a>=+this.uiDialog.css("z-index")&&(this.uiDialog.css("z-index",a+1),i=!0),i&&!n&&this._trigger("focus",e),i},open:function(){var e=this;return this._isOpen?void(this._moveToTop()&&this._focusTabbable()):(this._isOpen=!0,this.opener=t(this.document[0].activeElement),this._size(),this._position(),this._createOverlay(),this._moveToTop(null,!0),this.overlay&&this.overlay.css("z-index",this.uiDialog.css("z-index")-1),this._show(this.uiDialog,this.options.show,function(){e._focusTabbable(),e._trigger("focus")}),this._makeFocusTarget(),void this._trigger("open"))},_focusTabbable:function(){var t=this._focusedElement;t||(t=this.element.find("[autofocus]")),t.length||(t=this.element.find(":tabbable")),t.length||(t=this.uiDialogButtonPane.find(":tabbable")),t.length||(t=this.uiDialogTitlebarClose.filter(":tabbable")),t.length||(t=this.uiDialog),t.eq(0).focus()},_keepFocus:function(e){function n(){var e=this.document[0].activeElement,n=this.uiDialog[0]===e||t.contains(this.uiDialog[0],e);n||this._focusTabbable()}e.preventDefault(),n.call(this),this._delay(n)},_createWrapper:function(){this.uiDialog=t("
1&&M.splice.apply(M,[1,0].concat(M.splice(y,f+1))),s.dequeue()},t.effects.effect.clip=function(e,n){var i,o,a,s=t(this),r=["position","top","bottom","left","right","height","width"],c=t.effects.setMode(s,e.mode||"hide"),l="show"===c,u=e.direction||"vertical",h="vertical"===u,d=h?"height":"width",p=h?"top":"left",f={};t.effects.save(s,r),s.show(),i=t.effects.createWrapper(s).css({overflow:"hidden"}),o="IMG"===s[0].tagName?i:s,a=o[d](),l&&(o.css(d,0),o.css(p,a/2)),f[d]=l?a:0,f[p]=l?0:a/2,o.animate(f,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){l||s.hide(),t.effects.restore(s,r),t.effects.removeWrapper(s),n()}})},t.effects.effect.drop=function(e,n){var i,o=t(this),a=["position","top","bottom","left","right","opacity","height","width"],s=t.effects.setMode(o,e.mode||"hide"),r="show"===s,c=e.direction||"left",l="up"===c||"down"===c?"top":"left",u="up"===c||"left"===c?"pos":"neg",h={opacity:r?1:0};t.effects.save(o,a),o.show(),t.effects.createWrapper(o),i=e.distance||o["top"===l?"outerHeight":"outerWidth"](!0)/2,r&&o.css("opacity",0).css(l,"pos"===u?-i:i),h[l]=(r?"pos"===u?"+=":"-=":"pos"===u?"-=":"+=")+i,o.animate(h,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){"hide"===s&&o.hide(),t.effects.restore(o,a),t.effects.removeWrapper(o),n()}})},t.effects.effect.explode=function(e,n){function i(){M.push(this),M.length===h*d&&o()}function o(){p.css({visibility:"visible"}),t(M).remove(),m||p.hide(),n()}var a,s,r,c,l,u,h=e.pieces?Math.round(Math.sqrt(e.pieces)):3,d=h,p=t(this),f=t.effects.setMode(p,e.mode||"hide"),m="show"===f,g=p.show().css("visibility","hidden").offset(),b=Math.ceil(p.outerWidth()/d),v=Math.ceil(p.outerHeight()/h),M=[];for(a=0;a
t<"F"ip>'),x.renderer?i.isPlainObject(x.renderer)&&!x.renderer.header&&(x.renderer.header="jqueryui"):x.renderer="jqueryui"):i.extend(C,Vt.ext.classes,m.oClasses),i(this).addClass(C.sTable),""===x.oScroll.sX&&""===x.oScroll.sY||(x.oScroll.iBarWidth=Tt()),x.oScroll.sX===!0&&(x.oScroll.sX="100%"),x.iInitDisplayStart===n&&(x.iInitDisplayStart=m.iDisplayStart,x._iDisplayStart=m.iDisplayStart),null!==m.iDeferLoading){x.bDeferLoading=!0;var O=i.isArray(m.iDeferLoading);x._iRecordsDisplay=O?m.iDeferLoading[0]:m.iDeferLoading,x._iRecordsTotal=O?m.iDeferLoading[1]:m.iDeferLoading}var S=x.oLanguage;i.extend(!0,S,m.oLanguage),""!==S.sUrl&&(i.ajax({dataType:"json",url:S.sUrl,success:function(t){s(t),a(w.oLanguage,t),i.extend(!0,S,t),rt(x)},error:function(){rt(x)}}),v=!0),null===m.asStripeClasses&&(x.asStripeClasses=[C.sStripeOdd,C.sStripeEven]);var N=x.asStripeClasses,L=i("tbody tr:eq(0)",this);i.inArray(!0,i.map(N,function(t,e){return L.hasClass(t)}))!==-1&&(i("tbody tr",this).removeClass(N.join(" ")),x.asDestroyStripes=N.slice());var D,k=[],W=this.getElementsByTagName("thead");if(0!==W.length&&(X(x.aoHeader,W[0]),k=F(x)),null===m.aoColumns)for(D=[],g=0,p=k.length;g
").appendTo(this)),x.nTHead=P[0];var H=i(this).children("tbody");0===H.length&&(H=i("
").appendTo(this)),x.nTBody=H[0];var j=i(this).children("tfoot");if(0===j.length&&R.length>0&&(""!==x.oScroll.sX||""!==x.oScroll.sY)&&(j=i("").appendTo(this)),0===j.length||0===j.children().length?i(this).addClass(C.sNoFooter):j.length>0&&(x.nTFoot=j[0],X(x.aoFooter,x.nTFoot)),m.aaData)for(g=0;g