1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-11 05:32:39 +01:00

Merge branch 'release-2.8.0'

This commit is contained in:
Hillel Coren 2016-11-06 19:29:19 +02:00
commit b620356db1
236 changed files with 4898 additions and 2286 deletions

1
.gitignore vendored
View File

@ -32,6 +32,7 @@ Thumbs.db
/.project
tests/_output/
tests/_bootstrap.php
tests/_support/_generated/
# composer stuff
/c3.php

View File

@ -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

View File

@ -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

View File

@ -1,6 +1,7 @@
<?php namespace App\Console\Commands;
use DB;
use Mail;
use Carbon;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
@ -51,9 +52,12 @@ class CheckData extends Command {
*/
protected $description = 'Check/fix data';
protected $log = '';
protected $isValid = true;
public function fire()
{
$this->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)

View File

@ -0,0 +1,28 @@
<?php namespace App\Events;
use App\Models\Task;
use Illuminate\Queue\SerializesModels;
/**
* Class TaskWasArchived
*/
class TaskWasArchived extends Event
{
use SerializesModels;
/**
* @var Task
*/
public $task;
/**
* Create a new event instance.
*
* @param Task $task
*/
public function __construct(Task $task)
{
$this->task = $task;
}
}

View File

@ -0,0 +1,28 @@
<?php namespace App\Events;
use App\Models\Task;
use Illuminate\Queue\SerializesModels;
/**
* Class TaskWasDeleted
*/
class TaskWasDeleted extends Event
{
use SerializesModels;
/**
* @var Task
*/
public $task;
/**
* Create a new event instance.
*
* @param Task $task
*/
public function __construct(Task $task)
{
$this->task = $task;
}
}

View File

@ -0,0 +1,29 @@
<?php namespace App\Events;
use App\Models\Task;
use Illuminate\Queue\SerializesModels;
/**
* Class TaskWasRestored
*/
class TaskWasRestored extends Event
{
use SerializesModels;
/**
* @var Task
*/
public $task;
/**
* Create a new event instance.
*
* @param Task $task
*/
public function __construct(Task $task)
{
$this->task = $task;
}
}

View File

@ -1,5 +1,7 @@
<?php namespace App\Exceptions;
use Braintree\Util;
use Illuminate\Support\Facades\Response;
use Redirect;
use Utils;
use Exception;
@ -69,7 +71,7 @@ class Handler extends ExceptionHandler
{
if ($e instanceof ModelNotFoundException) {
return Redirect::to('/');
} elseif ($e instanceof \Illuminate\Session\TokenMismatchException) {
} if ($e instanceof \Illuminate\Session\TokenMismatchException) {
// prevent loop since the page auto-submits
if ($request->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()

View File

@ -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);
}
}
}

View File

@ -1,6 +1,8 @@
<?php namespace App\Http\Controllers;
use App\Models\AccountGateway;
use App\Models\AccountGatewaySettings;
use App\Models\GatewayType;
use App\Services\TemplateService;
use Auth;
use File;
@ -165,6 +167,10 @@ class AccountController extends BaseController
$term = Input::get('plan_term');
$numUsers = Input::get('num_users');
if ($plan != PLAN_ENTERPRISE) {
$numUsers = 1;
}
$planDetails = $account->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
*/

View File

@ -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");
}
}
}

View File

@ -1,5 +1,6 @@
<?php namespace App\Http\Controllers;
use Utils;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
@ -20,4 +21,24 @@ class BaseController extends Controller
$this->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}");
}
}
}

View File

@ -1,5 +1,6 @@
<?php namespace App\Http\Controllers;
use App\Http\Requests\ClientRequest;
use Response;
use Input;
use App\Models\Client;
@ -52,6 +53,31 @@ class ClientApiController extends BaseAPIController
return $this->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",

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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);

View File

@ -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);

View File

@ -1,8 +1,12 @@
<?php namespace App\Http\Controllers;
use App\Models\Expense;
use App\Ninja\Repositories\ExpenseRepository;
use App\Services\ExpenseService;
use App\Http\Requests\ExpenseRequest;
use App\Http\Requests\CreateExpenseRequest;
use App\Http\Requests\UpdateExpenseRequest;
class ExpenseApiController extends BaseAPIController
{
@ -20,6 +24,22 @@ class ExpenseApiController extends BaseAPIController
$this->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);
}
}
}

View File

@ -0,0 +1,41 @@
<?php namespace App\Http\Controllers;
use View;
use Utils;
use Input;
use Session;
use App\Services\ExpenseCategoryService;
use App\Http\Requests\CreateExpenseCategoryRequest;
use App\Http\Requests\UpdateExpenseCategoryRequest;
use App\Ninja\Repositories\ExpenseCategoryRepository;
class ExpenseCategoryApiController extends BaseAPIController
{
protected $categoryRepo;
protected $categoryService;
protected $entityType = ENTITY_EXPENSE_CATEGORY;
public function __construct(ExpenseCategoryRepository $categoryRepo, ExpenseCategoryService $categoryService)
{
parent::__construct();
$this->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);
}
}

View File

@ -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()

View File

@ -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';

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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());
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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 [

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -1,5 +1,7 @@
<?php namespace App\Http\Controllers;
// vendor
use App\Http\Requests\UpdateVendorRequest;
use App\Http\Requests\VendorRequest;
use Utils;
use Response;
use Input;
@ -83,4 +85,73 @@ class VendorApiController extends BaseAPIController
return $this->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);
}
}

View File

@ -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);
}
}

View File

@ -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',
];

View File

@ -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

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -1,31 +0,0 @@
<?php namespace App\Http\Middleware;
use Closure;
use Auth;
use Session;
// https://arjunphp.com/laravel5-inactivity-idle-session-logout/
class SessionDataCheckMiddleware {
/**
* Check session data, if role is not valid logout the request
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$bag = Session::getMetadataBag();
$max = env('IDLE_TIMEOUT_MINUTES', 6 * 60) * 60; // minute to second conversion
$elapsed = time() - $bag->getLastUsed();
if ( ! $bag || $elapsed > $max) {
$request->session()->flush();
Auth::logout();
$request->session()->flash('warning', trans('texts.inactive_logout'));
}
return $next($request);
}
}

View File

@ -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'));
}
}
}

View File

@ -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;
}

View File

@ -4,7 +4,6 @@
class UpdateTaxRateRequest extends TaxRateRequest
{
// Expenses
/**
* Determine if the user is authorized to make this request.
*

View File

@ -0,0 +1,52 @@
<?php
namespace App\Http\ViewComposers;
use DB;
use Cache;
use Illuminate\View\View;
use App\Models\Contact;
/**
* ClientPortalHeaderComposer.php.
*
* @copyright See LICENSE file that was distributed with this source code.
*/
class ClientPortalHeaderComposer
{
/**
* Bind data to the view.
*
* @param View $view
*
* @return void
*/
public function compose(View $view)
{
$contactKey = session('contact_key');
if ( ! $contactKey) {
return false;
}
$contact = Contact::where('contact_key', '=', $contactKey)
->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);
}
}

View File

@ -1,6 +1,5 @@
<?php
/*
|--------------------------------------------------------------------------
| Application Routes
@ -39,6 +38,7 @@ Route::post('/get_started', 'AccountController@getStarted');
Route::group(['middleware' => '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',
];

View File

@ -1,21 +1,21 @@
<?php
class parseCSV {
/*
Class: parseCSV v0.3.2
http://code.google.com/p/parsecsv-for-php/
Fully conforms to the specifications lined out on wikipedia:
- http://en.wikipedia.org/wiki/Comma-separated_values
Based on the concept of Ming Hong Ng's CsvFileParser class:
- http://minghong.blogspot.com/2006/07/csv-parser-for-php.html
Copyright (c) 2007 Jim Myhrberg (jim@zydev.info).
Permission is hereby granted, free of charge, to any person obtaining a copy
@ -35,9 +35,9 @@ class parseCSV {
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Code Examples
----------------
# general usage
@ -74,7 +74,7 @@ class parseCSV {
$csv = new parseCSV();
$csv->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 ) ? "<?php header('Status: 403'); die(' '); ?>".$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;
}
}
?>
?>

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -1,7 +1,5 @@
<?php namespace App\Listeners;
use App\Events\TaskWasCreated;
use App\Events\TaskWasUpdated;
use App\Models\Invoice;
use App\Events\ClientWasCreated;
use App\Events\ClientWasDeleted;
@ -33,6 +31,16 @@ use App\Events\CreditWasCreated;
use App\Events\CreditWasDeleted;
use App\Events\CreditWasArchived;
use App\Events\CreditWasRestored;
use App\Events\TaskWasCreated;
use App\Events\TaskWasUpdated;
use App\Events\TaskWasArchived;
use App\Events\TaskWasRestored;
use App\Events\TaskWasDeleted;
use App\Events\ExpenseWasCreated;
use App\Events\ExpenseWasUpdated;
use App\Events\ExpenseWasArchived;
use App\Events\ExpenseWasRestored;
use App\Events\ExpenseWasDeleted;
use App\Ninja\Repositories\ActivityRepository;
/**
@ -123,7 +131,9 @@ class ActivityListener
return;
}
$backupInvoice = Invoice::with('invoice_items', 'client.account', 'client.contacts')->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
);
}
}

View File

@ -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)

View File

@ -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);
}
/**

View File

@ -0,0 +1,32 @@
<?php namespace App\Models;
use Auth;
/**
* Class AccountGatewaySettings
*/
class AccountGatewaySettings extends EntityModel
{
/**
* @var array
*/
protected $dates = ['updated_at'];
/**
* @var bool
*/
protected static $hasPublicId = false;
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function gatewayType()
{
return $this->belongsTo('App\Models\GatewayType');
}
public function setCreatedAtAttribute($value)
{
// to Disable created_at
}
}

View File

@ -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);

View File

@ -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();
}
/**

View File

@ -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;
}
}

View File

@ -1,5 +1,6 @@
<?php namespace App\Models;
use Utils;
use Laracasts\Presenter\PresentableTrait;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Events\ExpenseWasCreated;
@ -31,6 +32,7 @@ class Expense extends EntityModel
'client_id',
'vendor_id',
'expense_currency_id',
'expense_date',
'invoice_currency_id',
'amount',
'foreign_amount',
@ -46,6 +48,29 @@ class Expense extends EntityModel
'tax_name2',
];
public static function getImportColumns()
{
return [
'client',
'vendor',
'amount',
'public_notes',
'expense_category',
'expense_date',
];
}
public static function getImportMap()
{
return [
'amount|total' => '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));
});

View File

@ -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;
}
}

View File

@ -0,0 +1,34 @@
<?php namespace App\Models;
use Eloquent;
use Cache;
use Utils;
/**
* Class GatewayType
*/
class GatewayType extends Eloquent
{
/**
* @var bool
*/
public $timestamps = false;
/**
* @return mixed
*/
public function getName()
{
return $this->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;
}
}

View File

@ -134,4 +134,13 @@ class Invitation extends EntityModel
$invoice->markViewed();
$client->markLoggedIn();
}
public function signatureDiv()
{
if ( ! $this->signature_base64) {
return false;
}
return sprintf('<img src="data:image/svg+xml;base64,%s"></img><p/>%s: %s', $this->signature_base64, trans('texts.signed'), Utils::fromSqlDateTime($this->signature_date));
}
}

View File

@ -514,6 +514,11 @@ class Invoice extends EntityModel implements BalanceAffecting
return storage_path() . '/pdfcache/cache-' . $this->id . '.pdf';
}
public function canBePaid()
{
return floatval($this->balance) > 0 && ! $this->is_deleted;
}
/**
* @param $invoice
* @return string

View File

@ -11,4 +11,12 @@ class PaymentType extends Eloquent
* @var bool
*/
public $timestamps = false;
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function gatewayType()
{
return $this->belongsTo('App\Models\GatewayType');
}
}

View File

@ -14,6 +14,16 @@ class Task extends EntityModel
use SoftDeletes;
use PresentableTrait;
/**
* @var array
*/
protected $fillable = [
'client_id',
'description',
'time_log',
'is_running',
];
/**
* @return mixed
*/
@ -82,6 +92,18 @@ class Task extends EntityModel
return self::calcStartTime($this);
}
public function getLastStartTime()
{
$parts = json_decode($this->time_log) ?: [];
if (count($parts)) {
$index = count($parts) - 1;
return $parts[$index][0];
} else {
return '';
}
}
/**
* @param $task
* @return int
@ -165,6 +187,14 @@ class Task extends EntityModel
return '#' . $this->public_id;
}
public function scopeDateRange($query, $startDate, $endDate)
{
$query->whereRaw('cast(substring(time_log, 3, 10) as unsigned) >= ' . $startDate->format('U'));
$query->whereRaw('cast(substring(time_log, 3, 10) as unsigned) <= ' . $endDate->format('U'));
return $query;
}
}

View File

@ -87,6 +87,7 @@ trait PresentsInvoice
'invoice.partial_due',
'invoice.custom_text_value1',
'invoice.custom_text_value2',
'.blank',
],
INVOICE_FIELDS_CLIENT => [
'client.client_name',
@ -97,9 +98,11 @@ trait PresentsInvoice
'client.city_state_postal',
'client.country',
'client.email',
'client.phone',
'client.contact_name',
'client.custom_value1',
'client.custom_value2',
'.blank',
],
INVOICE_FIELDS_ACCOUNT => [
'account.company_name',
@ -114,6 +117,7 @@ trait PresentsInvoice
'account.country',
'account.custom_value1',
'account.custom_value2',
'.blank',
]
];
@ -198,6 +202,7 @@ trait PresentsInvoice
'company_name',
'website',
'phone',
'blank',
];
foreach ($fields as $field) {

View File

@ -5,19 +5,14 @@ use Event;
use App\Libraries\Utils;
use App\Events\UserSettingsChanged;
use App\Events\UserSignedUp;
use Illuminate\Auth\Authenticatable;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Class User
*/
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract {
class User extends Authenticatable
{
/**
* @var array
*/
@ -27,8 +22,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'edit_all' => 0b0100,
];
use Authenticatable, Authorizable, CanResetPassword;
/**
* The database table used by the model.
*
@ -102,26 +95,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return PERSON_USER;
}
/**
* Get the unique identifier for the user.
*
* @return mixed
*/
public function getAuthIdentifier()
{
return $this->getKey();
}
/**
* Get the password for the user.
*
* @return string
*/
public function getAuthPassword()
{
return $this->password;
}
/**
* Get the e-mail address where password reminders are sent.
*
@ -258,31 +231,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return MAX_NUM_VENDORS;
}
/**
* @return mixed
*/
public function getRememberToken()
{
return $this->remember_token;
}
/**
* @param string $value
*/
public function setRememberToken($value)
{
$this->remember_token = $value;
}
/**
* @return string
*/
public function getRememberTokenName()
{
return 'remember_token';
}
public function clearSession()
{
$keys = [
@ -425,7 +373,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
public function caddAddUsers()
{
if ( ! Utils::isNinja()) {
if ( ! Utils::isNinjaProd()) {
return true;
} elseif ( ! $this->hasFeature(FEATURE_USERS)) {
return false;
@ -441,6 +389,12 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return $numUsers < $company->num_users;
}
public function canCreateOrEdit($entityType, $entity = false)
{
return (($entity && $this->can('edit', $entity))
|| (!$entity && $this->can('create', $entityType)));
}
}
User::updating(function ($user) {

View File

@ -211,7 +211,8 @@ class Vendor extends EntityModel
*/
public function addVendorContact($data, $isPrimary = false)
{
$publicId = isset($data['public_id']) ? $data['public_id'] : false;
//$publicId = isset($data['public_id']) ? $data['public_id'] : false;
$publicId = isset($data['public_id']) ? $data['public_id'] : (isset($data['id']) ? $data['id'] : false);
if ($publicId && $publicId != '-1') {
$contact = VendorContact::scope($publicId)->firstOrFail();

View File

@ -1,10 +1,17 @@
<?php namespace App\Ninja\Datatables;
use App\Models\AccountGatewaySettings;
use App\Models\GatewayType;
use URL;
use Cache;
use Utils;
use Session;
use App\Models\AccountGateway;
class AccountGatewayDatatable extends EntityDatatable
{
private static $accountGateways;
public $entityType = ENTITY_ACCOUNT_GATEWAY;
public function columns()
@ -15,10 +22,14 @@ class AccountGatewayDatatable extends EntityDatatable
function ($model) {
if ($model->deleted_at) {
return $model->name;
} elseif ($model->gateway_id == GATEWAY_CUSTOM) {
$accountGateway = $this->getAccountGateway($model->id);
$name = $accountGateway->getConfigField('name') . ' [' . trans('texts.custom') . ']';
return link_to("gateways/{$model->public_id}/edit", $name)->toHtml();
} elseif ($model->gateway_id != GATEWAY_WEPAY) {
return link_to("gateways/{$model->public_id}/edit", $model->name)->toHtml();
} else {
$accountGateway = AccountGateway::find($model->id);
$accountGateway = $this->getAccountGateway($model->id);
$config = $accountGateway->getConfig();
$endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/';
$wepayAccountId = $config->accountId;
@ -45,12 +56,56 @@ class AccountGatewayDatatable extends EntityDatatable
}
}
],
[
'limit',
function ($model) {
if ($model->gateway_id == GATEWAY_CUSTOM) {
$gatewayTypes = [GATEWAY_TYPE_CUSTOM];
} else {
$accountGateway = $this->getAccountGateway($model->id);
$paymentDriver = $accountGateway->paymentDriver();
$gatewayTypes = $paymentDriver->gatewayTypes();
$gatewayTypes = array_diff($gatewayTypes, array(GATEWAY_TYPE_TOKEN));
}
$html = '';
foreach ($gatewayTypes as $gatewayTypeId) {
$accountGatewaySettings = AccountGatewaySettings::scope()->where('account_gateway_settings.gateway_type_id',
'=', $gatewayTypeId)->first();
$gatewayType = GatewayType::find($gatewayTypeId);
if (count($gatewayTypes) > 1) {
if ($html) {
$html .= '<br>';
}
$html .= $gatewayType->name . ' &mdash; ';
}
if ($accountGatewaySettings && $accountGatewaySettings->min_limit !== null && $accountGatewaySettings->max_limit !== null) {
$html .= Utils::formatMoney($accountGatewaySettings->min_limit) . ' - ' . Utils::formatMoney($accountGatewaySettings->max_limit);
} elseif ($accountGatewaySettings && $accountGatewaySettings->min_limit !== null) {
$html .= trans('texts.min_limit',
array('min' => Utils::formatMoney($accountGatewaySettings->min_limit))
);
} elseif ($accountGatewaySettings && $accountGatewaySettings->max_limit !== null) {
$html .= trans('texts.max_limit',
array('max' => Utils::formatMoney($accountGatewaySettings->max_limit))
);
} else {
$html .= trans('texts.no_limit');
}
}
return $html;
}
],
];
}
public function actions()
{
return [
$actions = [
[
uctrans('texts.resend_confirmation_email'),
function ($model) {
@ -78,7 +133,7 @@ class AccountGatewayDatatable extends EntityDatatable
], [
uctrans('texts.manage_account'),
function ($model) {
$accountGateway = AccountGateway::find($model->id);
$accountGateway = $this->getAccountGateway($model->id);
$endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/';
return [
'url' => $endpoint.'account/'.$accountGateway->getConfig()->accountId,
@ -98,6 +153,46 @@ class AccountGatewayDatatable extends EntityDatatable
}
]
];
foreach (Cache::get('gatewayTypes') as $gatewayType) {
$actions[] = [
trans('texts.set_limits', ['gateway_type' => $gatewayType->name]),
function () use ($gatewayType) {
$accountGatewaySettings = AccountGatewaySettings::scope()
->where('account_gateway_settings.gateway_type_id', '=', $gatewayType->id)
->first();
$min = $accountGatewaySettings && $accountGatewaySettings->min_limit !== null ? $accountGatewaySettings->min_limit : 'null';
$max = $accountGatewaySettings && $accountGatewaySettings->max_limit !== null ? $accountGatewaySettings->max_limit : 'null';
return "javascript:showLimitsModal('{$gatewayType->name}', {$gatewayType->id}, $min, $max)";
},
function ($model) use ($gatewayType) {
// Only show this action if the given gateway supports this gateway type
if ($model->gateway_id == GATEWAY_CUSTOM) {
return $gatewayType->id == GATEWAY_TYPE_CUSTOM;
} else {
$accountGateway = $this->getAccountGateway($model->id);
$paymentDriver = $accountGateway->paymentDriver();
$gatewayTypes = $paymentDriver->gatewayTypes();
return in_array($gatewayType->id, $gatewayTypes);
}
}
];
}
return $actions;
}
private function getAccountGateway($id)
{
if (isset(static::$accountGateways[$id])) {
return static::$accountGateways[$id];
}
static::$accountGateways[$id] = AccountGateway::find($id);
return static::$accountGateways[$id];
}
}

View File

@ -27,7 +27,9 @@ class ActivityDatatable extends EntityDatatable
'payment' => $model->payment ?: '',
'credit' => $model->payment_amount ? Utils::formatMoney($model->credit, $model->currency_id, $model->country_id) : '',
'payment_amount' => $model->payment_amount ? Utils::formatMoney($model->payment_amount, $model->currency_id, $model->country_id) : null,
'adjustment' => $model->adjustment ? Utils::formatMoney($model->adjustment, $model->currency_id, $model->country_id) : null
'adjustment' => $model->adjustment ? Utils::formatMoney($model->adjustment, $model->currency_id, $model->country_id) : null,
'task' => $model->task_public_id ? link_to('/tasks/' . $model->task_public_id, substr($model->task_description, 0, 30).'...') : null,
'expense' => $model->expense_public_id ? link_to('/expenses/' . $model->expense_public_id, substr($model->expense_public_notes, 0, 30).'...') : null,
];
return trans("texts.activity_{$model->activity_type_id}", $data);

View File

@ -93,7 +93,7 @@ class ClientDatatable extends EntityDatatable
return URL::to("quotes/create/{$model->public_id}");
},
function ($model) {
return Auth::user()->hasFeature(FEATURE_QUOTES) && Auth::user()->can('create', ENTITY_INVOICE);
return Auth::user()->hasFeature(FEATURE_QUOTES) && Auth::user()->can('create', ENTITY_QUOTE);
}
],
[

View File

@ -46,7 +46,7 @@ class ExpenseDatatable extends EntityDatatable
[
'expense_date',
function ($model) {
if(!Auth::user()->can('editByOwner', [ENTITY_EXPENSE, $model->user_id])){
if(!Auth::user()->can('viewByOwner', [ENTITY_EXPENSE, $model->user_id])){
return Utils::fromSqlDate($model->expense_date);
}
@ -56,14 +56,16 @@ class ExpenseDatatable extends EntityDatatable
[
'amount',
function ($model) {
$amount = Utils::calculateTaxes($model->amount, $model->tax_rate1, $model->tax_rate2);
$str = Utils::formatMoney($amount, $model->expense_currency_id);
// show both the amount and the converted amount
if ($model->exchange_rate != 1) {
$converted = round($model->amount * $model->exchange_rate, 2);
return Utils::formatMoney($model->amount, $model->expense_currency_id) . ' | ' .
Utils::formatMoney($converted, $model->invoice_currency_id);
} else {
return Utils::formatMoney($model->amount, $model->expense_currency_id);
$converted = round($amount * $model->exchange_rate, 2);
$str .= ' | ' . Utils::formatMoney($converted, $model->invoice_currency_id);
}
return $str;
}
],
[

View File

@ -16,7 +16,7 @@ class InvoiceDatatable extends EntityDatatable
[
'invoice_number',
function ($model) use ($entityType) {
if(!Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id])){
if(!Auth::user()->can('viewByOwner', [ENTITY_INVOICE, $model->user_id])){
return $model->invoice_number;
}
@ -186,7 +186,7 @@ class InvoiceDatatable extends EntityDatatable
$class = 'success';
break;
}
return "<h4><div class=\"label label-{$class}\">$label</div></h4>";
}

View File

@ -21,7 +21,7 @@ class PaymentDatatable extends EntityDatatable
[
'invoice_number',
function ($model) {
if(!Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->invoice_user_id])){
if(!Auth::user()->can('viewByOwner', [ENTITY_INVOICE, $model->invoice_user_id])){
return $model->invoice_number;
}
@ -89,7 +89,11 @@ class PaymentDatatable extends EntityDatatable
[
'payment_date',
function ($model) {
return Utils::dateToString($model->payment_date);
if ($model->is_deleted) {
return Utils::dateToString($model->payment_date);
} else {
return link_to("payments/{$model->public_id}/edit", Utils::dateToString($model->payment_date))->toHtml();
}
}
],
[
@ -123,12 +127,11 @@ class PaymentDatatable extends EntityDatatable
return "javascript:showRefundModal({$model->public_id}, '{$max_refund}', '{$formatted}', '{$symbol}')";
},
function ($model) {
return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id]) && $model->payment_status_id >= PAYMENT_STATUS_COMPLETED &&
$model->refunded < $model->amount &&
(
($model->transaction_reference && in_array($model->gateway_id , static::$refundableGateways))
|| $model->payment_type_id == PAYMENT_TYPE_CREDIT
);
return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id])
&& $model->payment_status_id >= PAYMENT_STATUS_COMPLETED
&& $model->refunded < $model->amount
&& $model->transaction_reference
&& in_array($model->gateway_id , static::$refundableGateways);
}
]
];

View File

@ -58,7 +58,17 @@ class RecurringInvoiceDatatable extends EntityDatatable
function ($model) {
return Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]);
}
]
],
[
trans("texts.clone_invoice"),
function ($model) {
return URL::to("invoices/{$model->public_id}/clone");
},
function ($model) {
return Auth::user()->can('create', ENTITY_INVOICE);
}
],
];
}

View File

@ -26,6 +26,9 @@ class TaskDatatable extends EntityDatatable
[
'created_at',
function ($model) {
if(!Auth::user()->can('viewByOwner', [ENTITY_EXPENSE, $model->user_id])){
return Task::calcStartTime($model);
}
return link_to("tasks/{$model->public_id}/edit", Task::calcStartTime($model))->toHtml();
}
],

View File

@ -182,10 +182,21 @@ class BaseTransformer extends TransformerAbstract
* @param $name
* @return null
*/
protected function getVendorId($name)
public function getVendorId($name)
{
$name = strtolower($name);
return isset($this->maps[ENTITY_VENDOR][$name]) ? $this->maps[ENTITY_VENDOR][$name] : null;
}
/**
* @param $name
* @return null
*/
public function getExpenseCategoryId($name)
{
$name = strtolower($name);
return isset($this->maps[ENTITY_EXPENSE_CATEGORY][$name]) ? $this->maps[ENTITY_EXPENSE_CATEGORY][$name] : null;
}
}

View File

@ -28,6 +28,7 @@ class ClientTransformer extends BaseTransformer
'postal_code' => $this->getString($data, 'postal_code'),
'private_notes' => $this->getString($data, 'notes'),
'website' => $this->getString($data, 'website'),
'vat_number' => $this->getString($data, 'vat_number'),
'contacts' => [
[
'first_name' => $this->getString($data, 'first_name'),

View File

@ -0,0 +1,28 @@
<?php namespace App\Ninja\Import\CSV;
use App\Ninja\Import\BaseTransformer;
use League\Fractal\Resource\Item;
/**
* Class InvoiceTransformer
*/
class ExpenseTransformer extends BaseTransformer
{
/**
* @param $data
* @return bool|Item
*/
public function transform($data)
{
return new Item($data, function ($data) {
return [
'amount' => isset($data->amount) ? (float) $data->amount : null,
'vendor_id' => isset($data->vendor) ? $this->getVendorId($data->vendor) : null,
'client_id' => isset($data->client) ? $this->getClientId($data->client) : null,
'expense_date' => isset($data->expense_date) ? date('Y-m-d', strtotime($data->expense_date)) : null,
'public_notes' => $this->getString($data, 'public_notes'),
'expense_category_id' => isset($data->expense_category) ? $this->getExpenseCategoryId($data->expense_category) : null,
];
});
}
}

View File

@ -1,5 +1,6 @@
<?php namespace App\Ninja\Mailers;
use Utils;
use Exception;
use Mail;
use App\Models\Invoice;
@ -29,7 +30,7 @@ class Mailer
if (stristr($toEmail, '@example.com')) {
return true;
}
if (isset($_ENV['POSTMARK_API_TOKEN'])) {
$views = 'emails.'.$view.'_html';
} else {
@ -55,7 +56,7 @@ class Mailer
if (!empty($data['pdfString']) && !empty($data['pdfFileName'])) {
$message->attachData($data['pdfString'], $data['pdfFileName']);
}
// Attach documents to the email
if(!empty($data['documents'])){
foreach($data['documents'] as $document){
@ -90,7 +91,7 @@ class Mailer
$invoice->markInvitationSent($invitation, $messageId);
}
return true;
}
@ -107,11 +108,13 @@ class Mailer
} else {
$emailError = $exception->getMessage();
}
if (isset($data['invitation'])) {
$invitation = $data['invitation'];
$invitation->email_error = $emailError;
$invitation->save();
} elseif ( ! Utils::isNinja()) {
Utils::logError(Utils::getErrorString($exception));
}
return $emailError;

View File

@ -2,16 +2,19 @@
use URL;
use Session;
use Utils;
use Request;
use Omnipay;
use Exception;
use CreditCard;
use DateTime;
use App\Models\AccountGatewayToken;
use App\Models\AccountGatewaySettings;
use App\Models\Account;
use App\Models\Payment;
use App\Models\PaymentMethod;
use App\Models\Country;
use App\Models\GatewayType;
class BasePaymentDriver
{
@ -119,6 +122,12 @@ class BasePaymentDriver
$gateway = $this->accountGateway->gateway;
if ( ! $this->meetsGatewayTypeLimits($this->gatewayType)) {
// The customer must have hacked the URL
Session::flash('error', trans('texts.limits_not_met'));
return redirect()->to('view/' . $this->invitation->invitation_key);
}
if ($this->isGatewayType(GATEWAY_TYPE_TOKEN) || $gateway->is_offsite) {
if (Session::has('error')) {
Session::reflash();
@ -158,12 +167,14 @@ class BasePaymentDriver
// check if a custom view exists for this provider
protected function paymentView()
{
$file = sprintf('%s/views/payments/%s/%s.blade.php', resource_path(), $this->providerName(), $this->gatewayType);
$gatewayTypeAlias = GatewayType::getAliasFromId($this->gatewayType);
$file = sprintf('%s/views/payments/%s/%s.blade.php', resource_path(), $this->providerName(), $gatewayTypeAlias);
if (file_exists($file)) {
return sprintf('payments.%s/%s', $this->providerName(), $this->gatewayType);
return sprintf('payments.%s/%s', $this->providerName(), $gatewayTypeAlias);
} else {
return sprintf('payments.%s', $this->gatewayType);
return sprintf('payments.%s', $gatewayTypeAlias);
}
}
@ -242,8 +253,22 @@ class BasePaymentDriver
->wherePublicId($this->sourceId)
->firstOrFail();
}
} elseif ($this->shouldCreateToken()) {
$paymentMethod = $this->createToken();
if ( ! $this->meetsGatewayTypeLimits($paymentMethod->payment_type->gateway_type_id)) {
// The customer must have hacked the URL
Session::flash('error', trans('texts.limits_not_met'));
return redirect()->to('view/' . $this->invitation->invitation_key);
}
} else {
if ($this->shouldCreateToken()) {
$paymentMethod = $this->createToken();
}
if ( ! $this->meetsGatewayTypeLimits($this->gatewayType)) {
// The customer must have hacked the URL
Session::flash('error', trans('texts.limits_not_met'));
return redirect()->to('view/' . $this->invitation->invitation_key);
}
}
if ($this->isTwoStep()) {
@ -323,7 +348,8 @@ class BasePaymentDriver
protected function paymentDetails($paymentMethod = false)
{
$invoice = $this->invoice();
$completeUrl = url('complete/' . $this->invitation->invitation_key . '/' . $this->gatewayType);
$gatewayTypeAlias = $this->gatewayType == GATEWAY_TYPE_TOKEN ? $this->gatewayType : GatewayType::getAliasFromId($this->gatewayType);
$completeUrl = url('complete/' . $this->invitation->invitation_key . '/' . $gatewayTypeAlias);
$data = [
'amount' => $invoice->getRequestedAmount(),
@ -760,6 +786,10 @@ class BasePaymentDriver
continue;
}
if ( ! $this->meetsGatewayTypeLimits($paymentMethod->payment_type->gateway_type_id)) {
continue;
}
$url = URL::to("/payment/{$this->invitation->invitation_key}/token/".$paymentMethod->public_id);
if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) {
@ -787,27 +817,68 @@ class BasePaymentDriver
{
$links = [];
foreach ($this->gatewayTypes() as $gatewayType) {
if ($gatewayType === GATEWAY_TYPE_TOKEN) {
foreach ($this->gatewayTypes() as $gatewayTypeId) {
if ($gatewayTypeId === GATEWAY_TYPE_TOKEN) {
continue;
}
if ( ! $this->meetsGatewayTypeLimits($gatewayTypeId)) {
continue;
}
$gatewayTypeAlias = GatewayType::getAliasFromId($gatewayTypeId);
if ($gatewayTypeId == GATEWAY_TYPE_CUSTOM) {
$url = "javascript:showCustomModal();";
$label = e($this->accountGateway->getConfigField('name'));
} else {
$url = $this->paymentUrl($gatewayTypeAlias);
$label = trans("texts.{$gatewayTypeAlias}");
}
$links[] = [
'url' => $this->paymentUrl($gatewayType),
'label' => trans("texts.{$gatewayType}")
'gatewayTypeId' => $gatewayTypeId,
'url' => $url,
'label' => $label,
];
}
return $links;
}
protected function paymentUrl($gatewayType)
protected function meetsGatewayTypeLimits($gatewayTypeId)
{
if ( !$gatewayTypeId ) {
return true;
}
$accountGatewaySettings = AccountGatewaySettings::scope(false, $this->invitation->account_id)
->where('account_gateway_settings.gateway_type_id', '=', $gatewayTypeId)->first();
if ($accountGatewaySettings) {
$invoice = $this->invoice();
if ($accountGatewaySettings->min_limit !== null && $invoice->balance < $accountGatewaySettings->min_limit) {
return false;
}
if ($accountGatewaySettings->max_limit !== null && $invoice->balance > $accountGatewaySettings->max_limit) {
return false;
}
}
return true;
}
protected function paymentUrl($gatewayTypeAlias)
{
$account = $this->account();
$url = URL::to("/payment/{$this->invitation->invitation_key}/{$gatewayType}");
$url = URL::to("/payment/{$this->invitation->invitation_key}/{$gatewayTypeAlias}");
$gatewayTypeId = GatewayType::getIdFromAlias($gatewayTypeAlias);
// PayPal doesn't allow being run in an iframe so we need to open in new tab
if ($gatewayType === GATEWAY_TYPE_PAYPAL) {
if ($gatewayTypeId === GATEWAY_TYPE_PAYPAL) {
$url .= '#braintree_paypal';
if ($account->iframe_url) {

View File

@ -0,0 +1,12 @@
<?php namespace App\Ninja\PaymentDrivers;
class CustomPaymentDriver extends BasePaymentDriver
{
public function gatewayTypes()
{
return [
GATEWAY_TYPE_CUSTOM
];
}
}

View File

@ -151,14 +151,17 @@ class StripePaymentDriver extends BasePaymentDriver
protected function creatingPaymentMethod($paymentMethod)
{
$data = $this->tokenResponse;
$source = false;
if (!empty($data['object']) && ($data['object'] == 'card' || $data['object'] == 'bank_account')) {
$source = $data;
} elseif (!empty($data['object']) && $data['object'] == 'customer') {
$sources = !empty($data['sources']) ? $data['sources'] : $data['cards'];
$source = reset($sources['data']);
} else {
$source = !empty($data['source']) ? $data['source'] : $data['card'];
} elseif (!empty($data['source'])) {
$source = $data['source'];
} elseif (!empty($data['card'])) {
$source = $data['card'];
}
if ( ! $source) {

View File

@ -23,22 +23,4 @@ class ClientPresenter extends EntityPresenter {
return $account->formatMoney($client->paid_to_date, $client);
}
public function status()
{
$class = $text = '';
if ($this->entity->is_deleted) {
$class = 'danger';
$text = trans('texts.deleted');
} elseif ($this->entity->trashed()) {
$class = 'warning';
$text = trans('texts.archived');
} else {
$class = 'success';
$text = trans('texts.active');
}
return "<span class=\"label label-{$class}\">{$text}</span>";
}
}

View File

@ -17,6 +17,24 @@ class EntityPresenter extends Presenter
return URL::to($link);
}
public function statusLabel()
{
$class = $text = '';
if ($this->entity->is_deleted) {
$class = 'danger';
$text = trans('texts.deleted');
} elseif ($this->entity->trashed()) {
$class = 'warning';
$text = trans('texts.archived');
} else {
//$class = 'success';
//$text = trans('texts.active');
}
return "<span style=\"font-size:13px\" class=\"label label-{$class}\">{$text}</span>";
}
/**
* @return mixed
*/

View File

@ -24,12 +24,4 @@ class ExpensePresenter extends EntityPresenter
return Utils::fromSqlDate($this->entity->expense_date);
}
/**
* @return int
*/
public function invoiced_amount()
{
return $this->entity->invoice_id ? $this->entity->convertedAmount() : 0;
}
}

View File

@ -21,6 +21,11 @@ class TaskPresenter extends EntityPresenter
return $this->entity->user->getDisplayName();
}
public function description()
{
return substr($this->entity->description, 0, 40) . (strlen($this->entity->description) > 40 ? '...' : '');
}
/**
* @param $account
* @return mixed

View File

@ -2,6 +2,7 @@
use Auth;
use Request;
use Input;
use Session;
use Utils;
use URL;
@ -27,6 +28,11 @@ class AccountRepository
public function create($firstName = '', $lastName = '', $email = '', $password = '')
{
$company = new Company();
$company->utm_source = Input::get('utm_source');
$company->utm_medium = Input::get('utm_medium');
$company->utm_campaign = Input::get('utm_campaign');
$company->utm_term = Input::get('utm_term');
$company->utm_content = Input::get('utm_content');
$company->save();
$account = new Account();

View File

@ -74,6 +74,8 @@ class ActivityRepository
->leftJoin('invoices', 'invoices.id', '=', 'activities.invoice_id')
->leftJoin('payments', 'payments.id', '=', 'activities.payment_id')
->leftJoin('credits', 'credits.id', '=', 'activities.credit_id')
->leftJoin('tasks', 'tasks.id', '=', 'activities.task_id')
->leftJoin('expenses', 'expenses.id', '=', 'activities.expense_id')
->where('clients.id', '=', $clientId)
->where('contacts.is_primary', '=', 1)
->whereNull('contacts.deleted_at')
@ -102,7 +104,11 @@ class ActivityRepository
'contacts.email as email',
'payments.transaction_reference as payment',
'payments.amount as payment_amount',
'credits.amount as credit'
'credits.amount as credit',
'tasks.description as task_description',
'tasks.public_id as task_public_id',
'expenses.public_notes as expense_public_notes',
'expenses.public_id as expense_public_id'
);
}

View File

@ -8,7 +8,7 @@ class BaseRepository
/**
* @return null
*/
public function getClassName()
public function getClassName()
{
return null;
}
@ -40,7 +40,7 @@ class BaseRepository
if ($entity->trashed()) {
return;
}
$entity->delete();
$className = $this->getEventClass($entity, 'Archived');
@ -83,7 +83,7 @@ class BaseRepository
if ($entity->is_deleted) {
return;
}
$entity->is_deleted = true;
$entity->save();

View File

@ -31,6 +31,7 @@ class ClientRepository extends BaseRepository
->where('clients.account_id', '=', \Auth::user()->account_id)
->where('contacts.is_primary', '=', true)
->where('contacts.deleted_at', '=', null)
//->whereRaw('(clients.name != "" or contacts.first_name != "" or contacts.last_name != "" or contacts.email != "")') // filter out buy now invoices
->select(
DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
@ -78,7 +79,10 @@ class ClientRepository extends BaseRepository
$client = Client::createNew();
} else {
$client = Client::scope($publicId)->with('contacts')->firstOrFail();
\Log::warning('Entity not set in client repo save');
}
if ($client->is_deleted) {
return $client;
}
// convert currency code to id
@ -107,7 +111,11 @@ class ClientRepository extends BaseRepository
// If the primary is set ensure it's listed first
usort($contacts, function ($left, $right) {
return (isset($right['is_primary']) ? $right['is_primary'] : 1) - (isset($left['is_primary']) ? $left['is_primary'] : 0);
if (isset($right['is_primary']) && isset($left['is_primary'])) {
return $right['is_primary'] - $left['is_primary'];
} else {
return 0;
}
});
foreach ($contacts as $contact) {

View File

@ -58,10 +58,36 @@ class CreditRepository extends BaseRepository
return $query;
}
public function getClientDatatable($clientId)
{
$query = DB::table('credits')
->join('accounts', 'accounts.id', '=', 'credits.account_id')
->join('clients', 'clients.id', '=', 'credits.client_id')
->where('credits.client_id', '=', $clientId)
->where('clients.deleted_at', '=', null)
->where('credits.deleted_at', '=', null)
->where('credits.balance', '>', 0)
->select(
DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
'credits.amount',
'credits.balance',
'credits.credit_date'
);
$table = \Datatable::query($query)
->addColumn('credit_date', function ($model) { return Utils::fromSqlDate($model->credit_date); })
->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); })
->addColumn('balance', function ($model) { return Utils::formatMoney($model->balance, $model->currency_id, $model->country_id); })
->make();
return $table;
}
public function save($input, $credit = null)
{
$publicId = isset($data['public_id']) ? $data['public_id'] : false;
if ($credit) {
// do nothing
} elseif ($publicId) {

View File

@ -39,13 +39,15 @@ class DashboardRepository
$data = [];
$count = 0;
$balance = 0;
$records = $this->rawChartData($entityType, $account, $groupBy, $startDate, $endDate, $currencyId);
array_map(function ($item) use (&$data, &$count, $groupBy) {
array_map(function ($item) use (&$data, &$count, &$balance, $groupBy) {
$data[$item->$groupBy] = $item->total;
$count += $item->count;
}, $records->get());
$balance += isset($item->balance) ? $item->balance : 0;
}, $records);
$padding = $groupBy == 'DAYOFYEAR' ? 'day' : ($groupBy == 'WEEK' ? 'week' : 'month');
$endDate->modify('+1 '.$padding);
$interval = new DateInterval('P1'.substr($groupBy, 0, 1));
@ -84,9 +86,9 @@ class DashboardRepository
if ($entityType == ENTITY_INVOICE) {
$totals->invoices = array_sum($data);
$totals->average = $count ? round($totals->invoices / $count, 2) : 0;
$totals->balance = $balance;
} elseif ($entityType == ENTITY_PAYMENT) {
$totals->revenue = array_sum($data);
$totals->balance = $totals->invoices - $totals->revenue;
} elseif ($entityType == ENTITY_EXPENSE) {
//$totals->profit = $totals->revenue - array_sum($data);
$totals->expenses = array_sum($data);
@ -106,6 +108,10 @@ class DashboardRepository
private function rawChartData($entityType, $account, $groupBy, $startDate, $endDate, $currencyId)
{
if ( ! in_array($groupBy, ['DAYOFYEAR', 'WEEK', 'MONTH'])) {
return [];
}
$accountId = $account->id;
$currencyId = intval($currencyId);
$timeframe = 'concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date))';
@ -128,7 +134,7 @@ class DashboardRepository
}
if ($entityType == ENTITY_INVOICE) {
$records->select(DB::raw('sum(invoices.amount) as total, count(invoices.id) as count, '.$timeframe.' as '.$groupBy))
$records->select(DB::raw('sum(invoices.amount) as total, sum(invoices.balance) as balance, count(invoices.id) as count, '.$timeframe.' as '.$groupBy))
->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('is_recurring', '=', false);
} elseif ($entityType == ENTITY_PAYMENT) {
@ -137,10 +143,10 @@ class DashboardRepository
->where('invoices.is_deleted', '=', false)
->whereNotIn('payment_status_id', [PAYMENT_STATUS_VOIDED, PAYMENT_STATUS_FAILED]);
} elseif ($entityType == ENTITY_EXPENSE) {
$records->select(DB::raw('sum(expenses.amount) as total, count(expenses.id) as count, '.$timeframe.' as '.$groupBy));
$records->select(DB::raw('sum(expenses.amount + (expenses.amount * expenses.tax_rate1 / 100) + (expenses.amount * expenses.tax_rate2 / 100)) as total, count(expenses.id) as count, '.$timeframe.' as '.$groupBy));
}
return $records;
return $records->get();
}
public function totals($accountId, $userId, $viewAll)
@ -175,29 +181,39 @@ class DashboardRepository
return $metrics->groupBy('accounts.id')->first();
}
public function paidToDate($accountId, $userId, $viewAll)
public function paidToDate($account, $userId, $viewAll)
{
$accountId = $account->id;
$select = DB::raw(
'SUM('.DB::getQueryGrammar()->wrap('clients.paid_to_date', true).') as value,'
'SUM('.DB::getQueryGrammar()->wrap('payments.amount', true).' - '.DB::getQueryGrammar()->wrap('payments.refunded', true).') as value,'
.DB::getQueryGrammar()->wrap('clients.currency_id', true).' as currency_id'
);
$paidToDate = DB::table('accounts')
$paidToDate = DB::table('payments')
->select($select)
->leftJoin('clients', 'accounts.id', '=', 'clients.account_id')
->where('accounts.id', '=', $accountId)
->where('clients.is_deleted', '=', false);
->leftJoin('invoices', 'invoices.id', '=', 'payments.invoice_id')
->leftJoin('clients', 'clients.id', '=', 'invoices.client_id')
->where('payments.account_id', '=', $accountId)
->where('clients.is_deleted', '=', false)
->where('invoices.is_deleted', '=', false)
->whereNotIn('payments.payment_status_id', [PAYMENT_STATUS_VOIDED, PAYMENT_STATUS_FAILED]);
if (!$viewAll){
$paidToDate = $paidToDate->where('clients.user_id', '=', $userId);
$paidToDate->where('invoices.user_id', '=', $userId);
}
return $paidToDate->groupBy('accounts.id')
->groupBy(DB::raw('CASE WHEN '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' IS NULL THEN CASE WHEN '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' IS NULL THEN 1 ELSE '.DB::getQueryGrammar()->wrap('accounts.currency_id', true).' END ELSE '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' END'))
if ($account->financial_year_start) {
$yearStart = str_replace('2000', date('Y'), $account->financial_year_start);
$paidToDate->where('payments.payment_date', '>=', $yearStart);
}
return $paidToDate->groupBy('payments.account_id')
->groupBy(DB::raw('CASE WHEN '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' IS NULL THEN '.($account->currency_id ?: DEFAULT_CURRENCY).' ELSE '.DB::getQueryGrammar()->wrap('clients.currency_id', true).' END'))
->get();
}
public function averages($accountId, $userId, $viewAll)
public function averages($account, $userId, $viewAll)
{
$accountId = $account->id;
$select = DB::raw(
'AVG('.DB::getQueryGrammar()->wrap('invoices.amount', true).') as invoice_avg, '
.DB::getQueryGrammar()->wrap('clients.currency_id', true).' as currency_id'
@ -213,7 +229,12 @@ class DashboardRepository
->where('invoices.is_recurring', '=', false);
if (!$viewAll){
$averageInvoice = $averageInvoice->where('invoices.user_id', '=', $userId);
$averageInvoice->where('invoices.user_id', '=', $userId);
}
if ($account->financial_year_start) {
$yearStart = str_replace('2000', date('Y'), $account->financial_year_start);
$averageInvoice->where('invoices.invoice_date', '>=', $yearStart);
}
return $averageInvoice->groupBy('accounts.id')
@ -252,7 +273,7 @@ class DashboardRepository
}
return $activities->orderBy('activities.created_at', 'desc')
->with('client.contacts', 'user', 'invoice', 'payment', 'credit', 'account', 'task')
->with('client.contacts', 'user', 'invoice', 'payment', 'credit', 'account', 'task', 'expense', 'contact')
->take(50)
->get();
}
@ -335,8 +356,12 @@ class DashboardRepository
public function expenses($accountId, $userId, $viewAll)
{
$amountField = DB::getQueryGrammar()->wrap('expenses.amount', true);
$taxRate1Field = DB::getQueryGrammar()->wrap('expenses.tax_rate1', true);
$taxRate2Field = DB::getQueryGrammar()->wrap('expenses.tax_rate2', true);
$select = DB::raw(
'SUM('.DB::getQueryGrammar()->wrap('expenses.amount', true).') as value,'
"SUM({$amountField} + ({$amountField} * {$taxRate1Field} / 100) + ({$amountField} * {$taxRate2Field} / 100)) as value,"
.DB::getQueryGrammar()->wrap('expenses.expense_currency_id', true).' as currency_id'
);
$paidToDate = DB::table('accounts')

View File

@ -12,6 +12,11 @@ class ExpenseCategoryRepository extends BaseRepository
return 'App\Models\ExpenseCategory';
}
public function all()
{
return ExpenseCategory::scope()->get();
}
public function find($filter = null)
{
$query = DB::table('expense_categories')

View File

@ -76,6 +76,8 @@ class ExpenseRepository extends BaseRepository
'expenses.expense_currency_id',
'expenses.invoice_currency_id',
'expenses.user_id',
'expenses.tax_rate1',
'expenses.tax_rate2',
'expense_categories.name as category',
'invoices.public_id as invoice_public_id',
'invoices.user_id as invoice_user_id',
@ -118,20 +120,24 @@ class ExpenseRepository extends BaseRepository
// do nothing
} elseif ($publicId) {
$expense = Expense::scope($publicId)->firstOrFail();
\Log::warning('Entity not set in expense repo save');
if (Utils::isNinjaDev()) {
\Log::warning('Entity not set in expense repo save');
}
} else {
$expense = Expense::createNew();
}
if ($expense->is_deleted) {
return $expense;
}
// First auto fill
$expense->fill($input);
$expense->expense_date = Utils::toSqlDate($input['expense_date']);
if (isset($input['private_notes'])) {
$expense->private_notes = trim($input['private_notes']);
if (isset($input['expense_date'])) {
$expense->expense_date = Utils::toSqlDate($input['expense_date']);
}
$expense->public_notes = trim($input['public_notes']);
$expense->should_be_invoiced = isset($input['should_be_invoiced']) && floatval($input['should_be_invoiced']) || $expense->client_id ? true : false;
if ( ! $expense->expense_currency_id) {
@ -143,7 +149,9 @@ class ExpenseRepository extends BaseRepository
$rate = isset($input['exchange_rate']) ? Utils::parseFloat($input['exchange_rate']) : 1;
$expense->exchange_rate = round($rate, 4);
$expense->amount = round(Utils::parseFloat($input['amount']), 2);
if (isset($input['amount'])) {
$expense->amount = round(Utils::parseFloat($input['amount']), 2);
}
$expense->save();
@ -173,27 +181,4 @@ class ExpenseRepository extends BaseRepository
return $expense;
}
public function bulk($ids, $action)
{
$expenses = Expense::withTrashed()->scope($ids)->get();
foreach ($expenses as $expense) {
if ($action == 'restore') {
$expense->restore();
$expense->is_deleted = false;
$expense->save();
} else {
if ($action == 'delete') {
$expense->is_deleted = true;
$expense->save();
}
$expense->delete();
}
}
return count($tasks);
}
}

View File

@ -50,6 +50,7 @@ class InvoiceRepository extends BaseRepository
->where('contacts.deleted_at', '=', null)
->where('invoices.is_recurring', '=', false)
->where('contacts.is_primary', '=', true)
//->whereRaw('(clients.name != "" or contacts.first_name != "" or contacts.last_name != "" or contacts.email != "")') // filter out buy now invoices
->select(
DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
@ -280,7 +281,13 @@ class InvoiceRepository extends BaseRepository
}
} else {
$invoice = Invoice::scope($publicId)->firstOrFail();
\Log::warning('Entity not set in invoice repo save');
if (Utils::isNinjaDev()) {
\Log::warning('Entity not set in invoice repo save');
}
}
if ($invoice->is_deleted) {
return $invoice;
}
$invoice->fill($data);
@ -306,9 +313,6 @@ class InvoiceRepository extends BaseRepository
if (isset($data['is_amount_discount'])) {
$invoice->is_amount_discount = $data['is_amount_discount'] ? true : false;
}
if (isset($data['partial'])) {
$invoice->partial = round(Utils::parseFloat($data['partial']), 2);
}
if (isset($data['invoice_date_sql'])) {
$invoice->invoice_date = $data['invoice_date_sql'];
} elseif (isset($data['invoice_date'])) {
@ -477,6 +481,10 @@ class InvoiceRepository extends BaseRepository
$invoice->balance = $total;
}
if (isset($data['partial'])) {
$invoice->partial = max(0,min(round(Utils::parseFloat($data['partial']), 2), $invoice->balance));
}
$invoice->amount = $total;
$invoice->save();
@ -653,6 +661,9 @@ class InvoiceRepository extends BaseRepository
if ($quotePublicId) {
$clone->invoice_type_id = INVOICE_TYPE_STANDARD;
$clone->quote_id = $quotePublicId;
if ($account->invoice_terms) {
$clone->terms = $account->invoice_terms;
}
}
$clone->save();

View File

@ -150,11 +150,17 @@ class PaymentRepository extends BaseRepository
// do nothing
} elseif ($publicId) {
$payment = Payment::scope($publicId)->firstOrFail();
\Log::warning('Entity not set in payment repo save');
if (Utils::isNinjaDev()) {
\Log::warning('Entity not set in payment repo save');
}
} else {
$payment = Payment::createNew();
}
if ($payment->is_deleted) {
return $payment;
}
$paymentTypeId = false;
if (isset($input['payment_type_id'])) {
$paymentTypeId = $input['payment_type_id'] ? $input['payment_type_id'] : null;

View File

@ -17,15 +17,14 @@ class ProductRepository extends BaseRepository
->get();
}
public function find($accountId)
public function find($accountId, $filter = null)
{
return DB::table('products')
$query = DB::table('products')
->leftJoin('tax_rates', function($join) {
$join->on('tax_rates.id', '=', 'products.default_tax_rate_id')
->whereNull('tax_rates.deleted_at');
})
->where('products.account_id', '=', $accountId)
->where('products.deleted_at', '=', null)
->select(
'products.public_id',
'products.product_key',
@ -35,6 +34,19 @@ class ProductRepository extends BaseRepository
'tax_rates.rate as tax_rate',
'products.deleted_at'
);
if ($filter) {
$query->where(function ($query) use ($filter) {
$query->where('products.product_key', 'like', '%'.$filter.'%')
->orWhere('products.notes', 'like', '%'.$filter.'%');
});
}
if (!\Session::get('show_trash:product')) {
$query->where('products.deleted_at', '=', null);
}
return $query;
}
public function save($data, $product = null)

View File

@ -5,8 +5,13 @@ use Session;
use App\Models\Client;
use App\Models\Task;
class TaskRepository
class TaskRepository extends BaseRepository
{
public function getClassName()
{
return 'App\Models\Task';
}
public function find($clientPublicId = null, $filter = null)
{
$query = \DB::table('tasks')
@ -67,12 +72,15 @@ class TaskRepository
if ($task) {
// do nothing
} elseif ($publicId) {
$task = Task::scope($publicId)->firstOrFail();
\Log::warning('Entity not set in task repo save');
$task = Task::scope($publicId)->withTrashed()->firstOrFail();
} else {
$task = Task::createNew();
}
if ($task->is_deleted) {
return $task;
}
if (isset($data['client']) && $data['client']) {
$task->client_id = Client::getPrivateId($data['client']);
}
@ -109,26 +117,4 @@ class TaskRepository
return $task;
}
public function bulk($ids, $action)
{
$tasks = Task::withTrashed()->scope($ids)->get();
foreach ($tasks as $task) {
if ($action == 'restore') {
$task->restore();
$task->is_deleted = false;
$task->save();
} else {
if ($action == 'delete') {
$task->is_deleted = true;
$task->save();
}
$task->delete();
}
}
return count($tasks);
}
}

View File

@ -1,5 +1,6 @@
<?php namespace App\Ninja\Repositories;
use Utils;
use DB;
use App\Models\Vendor;
@ -70,7 +71,13 @@ class VendorRepository extends BaseRepository
$vendor = Vendor::createNew();
} else {
$vendor = Vendor::scope($publicId)->with('vendor_contacts')->firstOrFail();
\Log::warning('Entity not set in vendor repo save');
if (Utils::isNinjaDev()) {
\Log::warning('Entity not set in vendor repo save');
}
}
if ($vendor->is_deleted) {
return $vendor;
}
$vendor->fill($data);
@ -78,12 +85,22 @@ class VendorRepository extends BaseRepository
$first = true;
$vendorcontacts = isset($data['vendor_contact']) ? [$data['vendor_contact']] : $data['vendor_contacts'];
$vendorcontactIds = [];
foreach ($vendorcontacts as $vendorcontact) {
$vendorcontact = $vendor->addVendorContact($vendorcontact, $first);
$vendorcontactIds[] = $vendorcontact->public_id;
$first = false;
}
if ( ! $vendor->wasRecentlyCreated) {
foreach ($vendor->vendor_contacts as $contact) {
if (!in_array($contact->public_id, $vendorcontactIds)) {
$contact->delete();
}
}
}
return $vendor;
}
}

View File

@ -16,7 +16,7 @@ class ActivityTransformer extends EntityTransformer
protected $availableIncludes = [ ];
/**
* @param Client $client
* @param Activity $activity
* @return array
*/
public function transform(Activity $activity)
@ -29,7 +29,10 @@ class ActivityTransformer extends EntityTransformer
'invoice_id' => $activity->invoice ? $activity->invoice->public_id : null,
'payment_id' => $activity->payment ? $activity->payment->public_id : null,
'credit_id' => $activity->credit ? $activity->credit->public_id : null,
'updated_at' => $this->getTimestamp($activity->updated_at)
'updated_at' => $this->getTimestamp($activity->updated_at),
'expense_id' => $activity->expense_id ? $activity->expense->public_id : null,
'is_system' => (bool) $activity->is_system ? $activity->is_system : null,
'contact_id' => $activity->contact_id ? $activity->contact->public_id : null
];
}
}

View File

@ -26,4 +26,4 @@ class ContactTransformer extends EntityTransformer
'send_invoice' => (bool) $contact->send_invoice,
]);
}
}
}

View File

@ -23,11 +23,16 @@ class ExpenseTransformer extends EntityTransformer
'transaction_id' => $expense->transaction_id,
'bank_id' => $expense->bank_id,
'expense_currency_id' => (int) $expense->expense_currency_id,
'expense_category_id' => (int) $expense->expense_category_id,
'amount' => (float) $expense->amount,
'expense_date' => $expense->expense_date,
'exchange_rate' => (float) $expense->exchange_rate,
'invoice_currency_id' => (int) $expense->invoice_currency_id,
'is_deleted' => (bool) $expense->is_deleted,
'tax_name1' => $expense->tax_name1,
'tax_name2' => $expense->tax_name2,
'tax_rate1' => $expense->tax_rate1,
'tax_rate2' => $expense->tax_rate2,
'client_id' => $this->client ? $this->client->public_id : (isset($expense->client->public_id) ? (int) $expense->client->public_id : null),
'invoice_id' => isset($expense->invoice->public_id) ? (int) $expense->invoice->public_id : null,
'vendor_id' => isset($expense->vendor->public_id) ? (int) $expense->vendor->public_id : null,

View File

@ -10,7 +10,7 @@ class UserAccountTransformer extends EntityTransformer
];
protected $tokenName;
public function __construct(Account $account, $serializer, $tokenName)
{
parent::__construct($account, $serializer);
@ -31,8 +31,9 @@ class UserAccountTransformer extends EntityTransformer
'name' => $user->account->present()->name,
'token' => $user->account->getToken($user->id, $this->tokenName),
'default_url' => SITE_URL,
'plan' => $user->account->company->plan,
'logo' => $user->account->logo,
'logo_url' => $user->account->getLogoURL(),
];
}
}
}

Some files were not shown because too many files have changed in this diff Show More