diff --git a/.gitignore b/.gitignore
index f3b06ceeb933..eba40da49d7d 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 6bd3c485a878..727f351d7d1b 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 6100d090e1ee..000000000000
--- 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 d81bf5ff6eec..ae37a429f4a0 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 000000000000..74a7d1e4f6be
--- /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 000000000000..3e41c77ff65c
--- /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 000000000000..58891f6e3d85
--- /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 9dfadde1b7e9..1935b0df7a9a 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 a74536a3c102..05039ca99a3a 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 434cffe5e920..6f73cb19c3ec 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 d0ce1b6f62e0..844cd2175462 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 c120661db7c7..c583f919b86f 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 918b9c701ef1..4f6ca6521c62 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 b964f49206a3..e7bcd6e82e91 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 fa396a68240b..c2d850f0a1c5 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 d5ff416e7e13..0db08e555010 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 50c5d3d394d8..14cb4c638bf1 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 ea4425df86e8..465edcc852de 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 000000000000..5a18735dee62
--- /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 97ab3adb7b31..1130b6b10385 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 395d4a0f242d..4f153f78ef2b 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 3e893d57b59a..e5b74453e4e0 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 4eec4346eca0..3e2341b0e434 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 f41fd49f1514..843a83777d9c 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 bd621e82d9d4..e7d709096e37 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 59f79661c2ea..62a1183278ab 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 2d21dbd3cb4c..9c7269494907 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 a9719798a1b2..c8c952debe46 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 3f954da85356..3431d5c18323 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 1f362da3eb6a..a58bb1c93ca0 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 42099026eb67..bcc803243f99 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 1d504978c665..b2d587c1a8b4 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 3b0c2dadd764..c38e5867e2df 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 280f788b5282..f1bd21e98452 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 23ed4f62cab4..b20e0b754465 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 3561695e5cb4..6e7e73223d20 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 aa128bbc439a..f92a31a48398 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 57a26166a127..1bc8160dff7c 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 c626fa6ee88f..000000000000
--- 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 1aee1c59b8fc..776974a8946c 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 94dae7a443dd..88ba623d03ea 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 381990f32b97..e0a741ee968b 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 000000000000..ac0d6ca68618
--- /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 bdaa3837f3be..a40d1e065632 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 7fe04984db97..797beef30340 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 feb5d331d4b3..8d34c2bed60b 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 448ca17bb0fc..2dede0e95dab 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 91e327454db7..ee016cbff39b 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 d06763ab0138..8a81fa24f1cc 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 a2250b9bfc24..241d1013f884 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 000000000000..6b95c585c0a4
--- /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 5641967634d3..88a3aa1a9d43 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 feef11877984..a1db398ef58b 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 b0933ea15905..51ec29da1a00 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 f5cb229fea24..b19b7e2637f9 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 c13065c73e0a..6f7c42d90cad 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 000000000000..b695dcf3df21
--- /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 a86cc721735f..fd0fc7d720da 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('
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