1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-22 01:11:34 +02: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 /.project
tests/_output/ tests/_output/
tests/_bootstrap.php tests/_bootstrap.php
tests/_support/_generated/
# composer stuff # composer stuff
/c3.php /c3.php

View File

@ -65,6 +65,7 @@ before_script:
- sleep 5 - sleep 5
# Make sure the app is up-to-date # Make sure the app is up-to-date
- curl -L http://ninja.dev:8000/update - curl -L http://ninja.dev:8000/update
#- php artisan ninja:create-test-data 25
script: script:
- php ./vendor/codeception/codeception/codecept run --debug acceptance AllPagesCept.php - 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; <?php namespace App\Console\Commands;
use DB; use DB;
use Mail;
use Carbon; use Carbon;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
@ -51,9 +52,12 @@ class CheckData extends Command {
*/ */
protected $description = 'Check/fix data'; protected $description = 'Check/fix data';
protected $log = '';
protected $isValid = true;
public function fire() public function fire()
{ {
$this->info(date('Y-m-d') . ' Running CheckData...'); $this->logMessage(date('Y-m-d') . ' Running CheckData...');
if (!$this->option('client_id')) { if (!$this->option('client_id')) {
$this->checkPaidToDate(); $this->checkPaidToDate();
@ -66,7 +70,21 @@ class CheckData extends Command {
$this->checkAccountData(); $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() private function checkBlankInvoiceHistory()
@ -75,9 +93,14 @@ class CheckData extends Command {
->where('activity_type_id', '=', 5) ->where('activity_type_id', '=', 5)
->where('json_backup', '=', '') ->where('json_backup', '=', '')
->whereNotIn('id', [634386, 756352, 756353, 756356]) ->whereNotIn('id', [634386, 756352, 756353, 756356])
->whereNotIn('id', [634386, 756352, 756353, 756356, 820872])
->count(); ->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() private function checkAccountData()
@ -132,7 +155,8 @@ class CheckData extends Command {
->get(["{$table}.id", 'clients.account_id', 'clients.user_id']); ->get(["{$table}.id", 'clients.account_id', 'clients.user_id']);
if (count($records)) { 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') { if ($this->option('fix') == 'true') {
foreach ($records as $record) { foreach ($records as $record) {
@ -162,7 +186,11 @@ class CheckData extends Command {
->groupBy('clients.id') ->groupBy('clients.id')
->havingRaw('clients.paid_to_date != sum(payments.amount - payments.refunded) and clients.paid_to_date != 999999999.9999') ->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')]); ->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') { if ($this->option('fix') == 'true') {
foreach ($clients as $client) { foreach ($clients as $client) {
@ -179,6 +207,7 @@ class CheckData extends Command {
$clients = DB::table('clients') $clients = DB::table('clients')
->join('invoices', 'invoices.client_id', '=', 'clients.id') ->join('invoices', 'invoices.client_id', '=', 'clients.id')
->join('accounts', 'accounts.id', '=', 'clients.account_id') ->join('accounts', 'accounts.id', '=', 'clients.account_id')
->where('accounts.id', '!=', 20432)
->where('clients.is_deleted', '=', 0) ->where('clients.is_deleted', '=', 0)
->where('invoices.is_deleted', '=', 0) ->where('invoices.is_deleted', '=', 0)
->where('invoices.invoice_type_id', '=', INVOICE_TYPE_STANDARD) ->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') $clients = $clients->groupBy('clients.id', 'clients.balance', 'clients.created_at')
->orderBy('accounts.company_id', 'DESC') ->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')]); ->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) { 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; $foundProblem = false;
$lastBalance = 0; $lastBalance = 0;
$lastAdjustment = 0; $lastAdjustment = 0;
@ -205,7 +238,7 @@ class CheckData extends Command {
->where('client_id', '=', $client->id) ->where('client_id', '=', $client->id)
->orderBy('activities.id') ->orderBy('activities.id')
->get(['activities.id', 'activities.created_at', 'activities.activity_type_id', 'activities.adjustment', 'activities.balance', 'activities.invoice_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) { 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 // **Fix for ninja invoices which didn't have the invoice_type_id value set
if ($noAdjustment && $client->account_id == 20432) { if ($noAdjustment && $client->account_id == 20432) {
$this->info("No adjustment for ninja invoice"); $this->logMessage("No adjustment for ninja invoice");
$foundProblem = true; $foundProblem = true;
$clientFix += $invoice->amount; $clientFix += $invoice->amount;
$activityFix = $invoice->amount; $activityFix = $invoice->amount;
// **Fix for allowing converting a recurring invoice to a normal one without updating the balance** // **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) { } 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; $foundProblem = true;
$clientFix += $invoice->amount; $clientFix += $invoice->amount;
$activityFix = $invoice->amount; $activityFix = $invoice->amount;
// **Fix for updating balance when creating a quote or recurring invoice** // **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)) { } 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; $foundProblem = true;
$clientFix -= $activity->adjustment; $clientFix -= $activity->adjustment;
$activityFix = 0; $activityFix = 0;
@ -272,7 +305,7 @@ class CheckData extends Command {
} elseif ($activity->activity_type_id == ACTIVITY_TYPE_DELETE_INVOICE) { } elseif ($activity->activity_type_id == ACTIVITY_TYPE_DELETE_INVOICE) {
// **Fix for updating balance when deleting a recurring invoice** // **Fix for updating balance when deleting a recurring invoice**
if ($activity->adjustment != 0 && $invoice->is_recurring) { 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; $foundProblem = true;
if ($activity->balance != $lastBalance) { if ($activity->balance != $lastBalance) {
$clientFix -= $activity->adjustment; $clientFix -= $activity->adjustment;
@ -282,7 +315,7 @@ class CheckData extends Command {
} elseif ($activity->activity_type_id == ACTIVITY_TYPE_ARCHIVE_INVOICE) { } elseif ($activity->activity_type_id == ACTIVITY_TYPE_ARCHIVE_INVOICE) {
// **Fix for updating balance when archiving an invoice** // **Fix for updating balance when archiving an invoice**
if ($activity->adjustment != 0 && !$invoice->is_recurring) { 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; $foundProblem = true;
$activityFix = 0; $activityFix = 0;
$clientFix += $activity->adjustment; $clientFix += $activity->adjustment;
@ -290,12 +323,12 @@ class CheckData extends Command {
} elseif ($activity->activity_type_id == ACTIVITY_TYPE_UPDATE_INVOICE) { } elseif ($activity->activity_type_id == ACTIVITY_TYPE_UPDATE_INVOICE) {
// **Fix for updating balance when updating recurring invoice** // **Fix for updating balance when updating recurring invoice**
if ($activity->adjustment != 0 && $invoice->is_recurring) { 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; $foundProblem = true;
$clientFix -= $activity->adjustment; $clientFix -= $activity->adjustment;
$activityFix = 0; $activityFix = 0;
} else if ((strtotime($activity->created_at) - strtotime($lastCreatedAt) <= 1) && $activity->adjustment > 0 && $activity->adjustment == $lastAdjustment) { } 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; $foundProblem = true;
$clientFix -= $activity->adjustment; $clientFix -= $activity->adjustment;
$activityFix = 0; $activityFix = 0;
@ -303,7 +336,7 @@ class CheckData extends Command {
} elseif ($activity->activity_type_id == ACTIVITY_TYPE_UPDATE_QUOTE) { } elseif ($activity->activity_type_id == ACTIVITY_TYPE_UPDATE_QUOTE) {
// **Fix for updating balance when updating a quote** // **Fix for updating balance when updating a quote**
if ($activity->balance != $lastBalance) { 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; $foundProblem = true;
$clientFix += $lastBalance - $activity->balance; $clientFix += $lastBalance - $activity->balance;
$activityFix = 0; $activityFix = 0;
@ -311,7 +344,7 @@ class CheckData extends Command {
} else if ($activity->activity_type_id == ACTIVITY_TYPE_DELETE_PAYMENT) { } else if ($activity->activity_type_id == ACTIVITY_TYPE_DELETE_PAYMENT) {
// **Fix for deleting payment after deleting invoice** // **Fix for deleting payment after deleting invoice**
if ($activity->adjustment != 0 && $invoice->is_deleted && $activity->created_at > $invoice->deleted_at) { 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; $foundProblem = true;
$activityFix = 0; $activityFix = 0;
$clientFix -= $activity->adjustment; $clientFix -= $activity->adjustment;
@ -340,7 +373,7 @@ class CheckData extends Command {
} }
if ($activity->balance + $clientFix != $client->actual_balance) { if ($activity->balance + $clientFix != $client->actual_balance) {
$this->info("** Creating 'recovered update' activity **"); $this->logMessage("** Creating 'recovered update' activity **");
if ($this->option('fix') == 'true') { if ($this->option('fix') == 'true') {
DB::table('activities')->insert([ DB::table('activities')->insert([
'created_at' => new Carbon, 'created_at' => new Carbon,
@ -354,7 +387,7 @@ class CheckData extends Command {
} }
$data = ['balance' => $client->actual_balance]; $data = ['balance' => $client->actual_balance];
$this->info("Corrected balance:{$client->actual_balance}"); $this->logMessage("Corrected balance:{$client->actual_balance}");
if ($this->option('fix') == 'true') { if ($this->option('fix') == 'true') {
DB::table('clients') DB::table('clients')
->where('id', $client->id) ->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; <?php namespace App\Exceptions;
use Braintree\Util;
use Illuminate\Support\Facades\Response;
use Redirect; use Redirect;
use Utils; use Utils;
use Exception; use Exception;
@ -69,7 +71,7 @@ class Handler extends ExceptionHandler
{ {
if ($e instanceof ModelNotFoundException) { if ($e instanceof ModelNotFoundException) {
return Redirect::to('/'); return Redirect::to('/');
} elseif ($e instanceof \Illuminate\Session\TokenMismatchException) { } if ($e instanceof \Illuminate\Session\TokenMismatchException) {
// prevent loop since the page auto-submits // prevent loop since the page auto-submits
if ($request->path() != 'get_started') { if ($request->path() != 'get_started') {
// https://gist.github.com/jrmadsen67/bd0f9ad0ef1ed6bb594e // 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 // In production, except for maintenance mode, we'll show a custom error screen
if (Utils::isNinjaProd() if (Utils::isNinjaProd()
&& !Utils::isDownForMaintenance() && !Utils::isDownForMaintenance()

View File

@ -4,6 +4,9 @@ use Auth;
use Utils; use Utils;
use Response; use Response;
use Cache; use Cache;
use Socialite;
use Exception;
use App\Services\AuthService;
use App\Models\Account; use App\Models\Account;
use App\Ninja\Repositories\AccountRepository; use App\Ninja\Repositories\AccountRepository;
use Illuminate\Http\Request; 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; <?php namespace App\Http\Controllers;
use App\Models\AccountGateway; use App\Models\AccountGateway;
use App\Models\AccountGatewaySettings;
use App\Models\GatewayType;
use App\Services\TemplateService; use App\Services\TemplateService;
use Auth; use Auth;
use File; use File;
@ -165,6 +167,10 @@ class AccountController extends BaseController
$term = Input::get('plan_term'); $term = Input::get('plan_term');
$numUsers = Input::get('num_users'); $numUsers = Input::get('num_users');
if ($plan != PLAN_ENTERPRISE) {
$numUsers = 1;
}
$planDetails = $account->getPlanDetails(false, false); $planDetails = $account->getPlanDetails(false, false);
$newPlan = [ $newPlan = [
@ -193,7 +199,9 @@ class AccountController extends BaseController
} }
} }
$hasPaid = false;
if (!empty($planDetails['paid']) && $plan != PLAN_FREE) { if (!empty($planDetails['paid']) && $plan != PLAN_FREE) {
$hasPaid = true;
$time_used = $planDetails['paid']->diff(date_create()); $time_used = $planDetails['paid']->diff(date_create());
$days_used = $time_used->days; $days_used = $time_used->days;
@ -209,7 +217,11 @@ class AccountController extends BaseController
if ($newPlan['price'] > $credit) { if ($newPlan['price'] > $credit) {
$invitation = $this->accountRepo->enablePlan($newPlan, $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 { } else {
if ($plan != PLAN_FREE) { if ($plan != PLAN_FREE) {
@ -417,6 +429,7 @@ class AccountController extends BaseController
'currencies' => Cache::get('currencies'), 'currencies' => Cache::get('currencies'),
'title' => trans('texts.localization'), 'title' => trans('texts.localization'),
'weekdays' => Utils::getTranslatedWeekdayNames(), 'weekdays' => Utils::getTranslatedWeekdayNames(),
'months' => Utils::getMonthOptions(),
]; ];
return View::make('accounts.localization', $data); return View::make('accounts.localization', $data);
@ -458,10 +471,12 @@ class AccountController extends BaseController
} }
return View::make('accounts.payments', [ return View::make('accounts.payments', [
'showAdd' => $count < count(Gateway::$alternate) + 1, 'showAdd' => $count < count(Gateway::$alternate) + 1,
'title' => trans('texts.online_payments'), 'title' => trans('texts.online_payments'),
'tokenBillingOptions' => $tokenBillingOptions, '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() private function showProducts()
{ {
$columns = ['product', 'description', 'unit_cost'];
if (Auth::user()->account->invoice_item_taxes) {
$columns[] = 'tax_rate';
}
$columns[] = 'action';
$data = [ $data = [
'account' => Auth::user()->account, 'account' => Auth::user()->account,
'title' => trans('texts.product_library'), 'title' => trans('texts.product_library'),
'columns' => Utils::trans($columns),
]; ];
return View::make('accounts.products', $data); return View::make('accounts.products', $data);
@ -667,11 +675,9 @@ class AccountController extends BaseController
* @param $section * @param $section
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
*/ */
public function doSection($section = ACCOUNT_COMPANY_DETAILS) public function doSection($section)
{ {
if ($section === ACCOUNT_COMPANY_DETAILS) { if ($section === ACCOUNT_LOCALIZATION) {
return AccountController::saveDetails();
} elseif ($section === ACCOUNT_LOCALIZATION) {
return AccountController::saveLocalization(); return AccountController::saveLocalization();
} elseif ($section == ACCOUNT_PAYMENTS) { } elseif ($section == ACCOUNT_PAYMENTS) {
return self::saveOnlinePayments(); return self::saveOnlinePayments();
@ -697,9 +703,27 @@ class AccountController extends BaseController
return AccountController::saveTaxRates(); return AccountController::saveTaxRates();
} elseif ($section === ACCOUNT_PAYMENT_TERMS) { } elseif ($section === ACCOUNT_PAYMENT_TERMS) {
return AccountController::savePaymetTerms(); 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 * @return \Illuminate\Http\RedirectResponse
*/ */
@ -723,12 +747,7 @@ class AccountController extends BaseController
private function saveClientPortal() private function saveClientPortal()
{ {
$account = Auth::user()->account; $account = Auth::user()->account;
$account->fill(Input::all());
$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');
// Only allowed for pro Invoice Ninja users or white labeled self-hosted users // Only allowed for pro Invoice Ninja users or white labeled self-hosted users
if (Auth::user()->account->hasFeature(FEATURE_CLIENT_PORTAL_CSS)) { 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->military_time = Input::get('military_time') ? true : false;
$account->show_currency_code = Input::get('show_currency_code') ? 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->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(); $account->save();
event(new UserSettingsChanged()); event(new UserSettingsChanged());
@ -1226,6 +1246,35 @@ class AccountController extends BaseController
return Redirect::to('settings/'.ACCOUNT_PAYMENTS); 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 * @return \Illuminate\Http\RedirectResponse
*/ */

View File

@ -48,8 +48,10 @@ class AccountGatewayController extends BaseController
$accountGateway = AccountGateway::scope($publicId)->firstOrFail(); $accountGateway = AccountGateway::scope($publicId)->firstOrFail();
$config = $accountGateway->getConfig(); $config = $accountGateway->getConfig();
foreach ($config as $field => $value) { if ($accountGateway->gateway_id != GATEWAY_CUSTOM) {
$config->$field = str_repeat('*', strlen($value)); foreach ($config as $field => $value) {
$config->$field = str_repeat('*', strlen($value));
}
} }
$data = self::getViewModel($accountGateway); $data = self::getViewModel($accountGateway);
@ -60,8 +62,6 @@ class AccountGatewayController extends BaseController
$data['hiddenFields'] = Gateway::$hiddenFields; $data['hiddenFields'] = Gateway::$hiddenFields;
$data['selectGateways'] = Gateway::where('id', '=', $accountGateway->gateway_id)->get(); $data['selectGateways'] = Gateway::where('id', '=', $accountGateway->gateway_id)->get();
$this->testGateway($accountGateway);
return View::make('accounts.account_gateway', $data); return View::make('accounts.account_gateway', $data);
} }
@ -100,7 +100,7 @@ class AccountGatewayController extends BaseController
if ($otherProviders) { if ($otherProviders) {
$availableGatewaysIds = $account->availableGatewaysIds(); $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['secondaryGateways'] = Gateway::secondary($availableGatewaysIds)->orderBy('name')->get();
$data['hiddenFields'] = Gateway::$hiddenFields; $data['hiddenFields'] = Gateway::$hiddenFields;
@ -132,7 +132,9 @@ class AccountGatewayController extends BaseController
foreach ($gateways as $gateway) { foreach ($gateways as $gateway) {
$fields = $gateway->getFields(); $fields = $gateway->getFields();
asort($fields); if ( ! $gateway->isCustom()) {
asort($fields);
}
$gateway->fields = $gateway->id == GATEWAY_WEPAY ? [] : $fields; $gateway->fields = $gateway->id == GATEWAY_WEPAY ? [] : $fields;
if ($accountGateway && $accountGateway->gateway_id == $gateway->id) { if ($accountGateway && $accountGateway->gateway_id == $gateway->id) {
$accountGateway->fields = $gateway->fields; $accountGateway->fields = $gateway->fields;
@ -247,6 +249,8 @@ class AccountGatewayController extends BaseController
} }
if (!$value && ($field == 'testMode' || $field == 'developerMode')) { if (!$value && ($field == 'testMode' || $field == 'developerMode')) {
// do nothing // do nothing
} elseif ($gatewayId == GATEWAY_CUSTOM) {
$config->$field = strip_tags($value);
} else { } else {
$config->$field = $value; $config->$field = $value;
} }
@ -312,14 +316,17 @@ class AccountGatewayController extends BaseController
if (isset($wepayResponse)) { if (isset($wepayResponse)) {
return $wepayResponse; return $wepayResponse;
} else { } else {
$this->testGateway($accountGateway);
if ($accountGatewayPublicId) { if ($accountGatewayPublicId) {
$message = trans('texts.updated_gateway'); $message = trans('texts.updated_gateway');
Session::flash('message', $message);
return Redirect::to("gateways/{$accountGateway->public_id}/edit");
} else { } else {
$message = trans('texts.created_gateway'); $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; <?php namespace App\Http\Controllers;
use Utils;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
@ -20,4 +21,24 @@ class BaseController extends Controller
$this->layout = View::make($this->layout); $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; <?php namespace App\Http\Controllers;
use App\Http\Requests\ClientRequest;
use Response; use Response;
use Input; use Input;
use App\Models\Client; use App\Models\Client;
@ -52,6 +53,31 @@ class ClientApiController extends BaseAPIController
return $this->listResponse($clients); 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( * @SWG\Post(
* path="/clients", * path="/clients",

View File

@ -95,7 +95,7 @@ class ClientController extends BaseController
if($user->can('create', ENTITY_TASK)){ if($user->can('create', ENTITY_TASK)){
$actionLinks[] = ['label' => trans('texts.new_task'), 'url' => URL::to('/tasks/create/'.$client->public_id)]; $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)]; $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); $message = Utils::pluralize($action.'d_client', $count);
Session::flash('message', $message); Session::flash('message', $message);
if ($action == 'restore' && $count == 1) { return $this->returnBulk(ENTITY_CLIENT, $action, $ids);
return Redirect::to('clients/'.Utils::getFirst($ids));
} else {
return Redirect::to('clients');
}
} }
} }

View File

@ -22,6 +22,7 @@ use App\Ninja\Repositories\InvoiceRepository;
use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\PaymentRepository;
use App\Ninja\Repositories\ActivityRepository; use App\Ninja\Repositories\ActivityRepository;
use App\Ninja\Repositories\DocumentRepository; use App\Ninja\Repositories\DocumentRepository;
use App\Ninja\Repositories\CreditRepository;
use App\Events\InvoiceInvitationWasViewed; use App\Events\InvoiceInvitationWasViewed;
use App\Events\QuoteInvitationWasViewed; use App\Events\QuoteInvitationWasViewed;
use App\Services\PaymentService; use App\Services\PaymentService;
@ -33,13 +34,14 @@ class ClientPortalController extends BaseController
private $paymentRepo; private $paymentRepo;
private $documentRepo; 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->invoiceRepo = $invoiceRepo;
$this->paymentRepo = $paymentRepo; $this->paymentRepo = $paymentRepo;
$this->activityRepo = $activityRepo; $this->activityRepo = $activityRepo;
$this->documentRepo = $documentRepo; $this->documentRepo = $documentRepo;
$this->paymentService = $paymentService; $this->paymentService = $paymentService;
$this->creditRepo = $creditRepo;
} }
public function view($invitationKey) public function view($invitationKey)
@ -102,7 +104,9 @@ class ClientPortalController extends BaseController
$paymentURL = ''; $paymentURL = '';
if (count($paymentTypes) == 1) { if (count($paymentTypes) == 1) {
$paymentURL = $paymentTypes[0]['url']; $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); $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()){ if($account->hasFeature(FEATURE_DOCUMENTS) && $this->canCreateZip()){
$zipDocs = $this->getInvoiceZipDocuments($invoice, $size); $zipDocs = $this->getInvoiceZipDocuments($invoice, $size);
@ -155,18 +164,6 @@ class ClientPortalController extends BaseController
return View::make('invoices.view', $data); 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) private function getPaymentTypes($account, $client, $invitation)
{ {
$links = []; $links = [];
@ -201,19 +198,41 @@ class ClientPortalController extends BaseController
return $pdfString; 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(); return $this->returnError();
} }
$client = $contact->client; $client = $contact->client;
$account = $client->account; $account = $client->account;
$account->loadLocalizationSettings($client);
$color = $account->primary_color ? $account->primary_color : '#0b4d78'; $color = $account->primary_color ? $account->primary_color : '#0b4d78';
$customer = false; $customer = false;
if (!$account->enable_client_portal || !$account->enable_client_portal_dashboard) { if (!$account->enable_client_portal) {
return $this->returnError(); return $this->returnError();
} elseif (!$account->enable_client_portal_dashboard) {
return redirect()->to('/client/invoices/');
} }
if ($paymentDriver = $account->paymentDriver(false, GATEWAY_TYPE_TOKEN)) { if ($paymentDriver = $account->paymentDriver(false, GATEWAY_TYPE_TOKEN)) {
@ -273,6 +292,7 @@ class ClientPortalController extends BaseController
} }
$account = $contact->account; $account = $contact->account;
$account->loadLocalizationSettings($contact->client);
if (!$account->enable_client_portal) { if (!$account->enable_client_portal) {
return $this->returnError(); return $this->returnError();
@ -300,6 +320,7 @@ class ClientPortalController extends BaseController
} }
$account = $contact->account; $account = $contact->account;
$account->loadLocalizationSettings($contact->client);
if (!$account->enable_client_portal) { if (!$account->enable_client_portal) {
return $this->returnError(); return $this->returnError();
@ -346,12 +367,14 @@ class ClientPortalController extends BaseController
} }
$account = $contact->account; $account = $contact->account;
$account->loadLocalizationSettings($contact->client);
if (!$account->enable_client_portal) { if (!$account->enable_client_portal) {
return $this->returnError(); return $this->returnError();
} }
$color = $account->primary_color ? $account->primary_color : '#0b4d78'; $color = $account->primary_color ? $account->primary_color : '#0b4d78';
$data = [ $data = [
'color' => $color, 'color' => $color,
'account' => $account, 'account' => $account,
@ -416,12 +439,14 @@ class ClientPortalController extends BaseController
} }
$account = $contact->account; $account = $contact->account;
$account->loadLocalizationSettings($contact->client);
if (!$account->enable_client_portal) { if (!$account->enable_client_portal) {
return $this->returnError(); return $this->returnError();
} }
$color = $account->primary_color ? $account->primary_color : '#0b4d78'; $color = $account->primary_color ? $account->primary_color : '#0b4d78';
$data = [ $data = [
'color' => $color, 'color' => $color,
'account' => $account, 'account' => $account,
@ -444,6 +469,42 @@ class ClientPortalController extends BaseController
return $this->invoiceRepo->getClientDatatable($contact->id, ENTITY_QUOTE, Input::get('sSearch')); 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() public function documentIndex()
{ {
if (!$contact = $this->getContact()) { if (!$contact = $this->getContact()) {
@ -451,12 +512,14 @@ class ClientPortalController extends BaseController
} }
$account = $contact->account; $account = $contact->account;
$account->loadLocalizationSettings($contact->client);
if (!$account->enable_client_portal) { if (!$account->enable_client_portal) {
return $this->returnError(); return $this->returnError();
} }
$color = $account->primary_color ? $account->primary_color : '#0b4d78'; $color = $account->primary_color ? $account->primary_color : '#0b4d78';
$data = [ $data = [
'color' => $color, 'color' => $color,
'account' => $account, 'account' => $account,

View File

@ -23,8 +23,8 @@ class DashboardApiController extends BaseAPIController
$dashboardRepo = $this->dashboardRepo; $dashboardRepo = $this->dashboardRepo;
$metrics = $dashboardRepo->totals($accountId, $userId, $viewAll); $metrics = $dashboardRepo->totals($accountId, $userId, $viewAll);
$paidToDate = $dashboardRepo->paidToDate($accountId, $userId, $viewAll); $paidToDate = $dashboardRepo->paidToDate($user->account, $userId, $viewAll);
$averageInvoice = $dashboardRepo->averages($accountId, $userId, $viewAll); $averageInvoice = $dashboardRepo->averages($user->account, $userId, $viewAll);
$balances = $dashboardRepo->balances($accountId, $userId, $viewAll); $balances = $dashboardRepo->balances($accountId, $userId, $viewAll);
$activities = $dashboardRepo->activities($accountId, $userId, $viewAll); $activities = $dashboardRepo->activities($accountId, $userId, $viewAll);
$pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll); $pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll);

View File

@ -33,9 +33,9 @@ class DashboardController extends BaseController
$dashboardRepo = $this->dashboardRepo; $dashboardRepo = $this->dashboardRepo;
$metrics = $dashboardRepo->totals($accountId, $userId, $viewAll); $metrics = $dashboardRepo->totals($accountId, $userId, $viewAll);
$paidToDate = $dashboardRepo->paidToDate($accountId, $userId, $viewAll); $paidToDate = $dashboardRepo->paidToDate($account, $userId, $viewAll);
$averageInvoice = $dashboardRepo->averages($accountId, $userId, $viewAll); $averageInvoice = $dashboardRepo->averages($account, $userId, $viewAll);
$balances = $dashboardRepo->balances($accountId, $userId, $viewAll); $balances = $dashboardRepo->balances($accountId, $userId, $viewAll);
$activities = $dashboardRepo->activities($accountId, $userId, $viewAll); $activities = $dashboardRepo->activities($accountId, $userId, $viewAll);
$pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll); $pastDue = $dashboardRepo->pastDue($accountId, $userId, $viewAll);
$upcoming = $dashboardRepo->upcoming($accountId, $userId, $viewAll); $upcoming = $dashboardRepo->upcoming($accountId, $userId, $viewAll);

View File

@ -1,8 +1,12 @@
<?php namespace App\Http\Controllers; <?php namespace App\Http\Controllers;
use App\Models\Expense; use App\Models\Expense;
use App\Ninja\Repositories\ExpenseRepository; use App\Ninja\Repositories\ExpenseRepository;
use App\Services\ExpenseService; use App\Services\ExpenseService;
use App\Http\Requests\ExpenseRequest;
use App\Http\Requests\CreateExpenseRequest;
use App\Http\Requests\UpdateExpenseRequest;
class ExpenseApiController extends BaseAPIController class ExpenseApiController extends BaseAPIController
{ {
@ -20,6 +24,22 @@ class ExpenseApiController extends BaseAPIController
$this->expenseService = $expenseService; $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() public function index()
{ {
$expenses = Expense::scope() $expenses = Expense::scope()
@ -30,23 +50,103 @@ class ExpenseApiController extends BaseAPIController
return $this->listResponse($expenses); 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 = [ $data = [
'vendor' => null, 'vendor' => null,
'expense' => $expense, 'expense' => $expense,
'entity' => $expense,
'method' => 'PUT', 'method' => 'PUT',
'url' => 'expenses/'.$expense->public_id, 'url' => 'expenses/'.$expense->public_id,
'title' => 'Edit Expense', 'title' => 'Edit Expense',
@ -245,7 +246,7 @@ class ExpenseController extends BaseController
Session::flash('message', $message); Session::flash('message', $message);
} }
return Redirect::to('expenses'); return $this->returnBulk($this->entityType, $action, $ids);
} }
private static function getViewModel() private static function getViewModel()

View File

@ -38,7 +38,7 @@ class HomeController extends BaseController
public function showIndex() public function showIndex()
{ {
Session::reflash(); Session::reflash();
if (!Utils::isNinja() && (!Utils::isDatabaseSetup() || Account::count() == 0)) { if (!Utils::isNinja() && (!Utils::isDatabaseSetup() || Account::count() == 0)) {
return Redirect::to('/setup'); return Redirect::to('/setup');
} elseif (Auth::check()) { } elseif (Auth::check()) {
@ -76,10 +76,8 @@ class HomeController extends BaseController
} }
// Track the referral/campaign code // Track the referral/campaign code
foreach (['rc', 'utm_campaign'] as $code) { if (Input::has('rc')) {
if (Input::has($code)) { Session::set(SESSION_REFERRAL_CODE, Input::get('rc'));
Session::set(SESSION_REFERRAL_CODE, Input::get($code));
}
} }
if (Auth::check()) { if (Auth::check()) {
@ -115,7 +113,7 @@ class HomeController extends BaseController
$user->save(); $user->save();
} }
} }
Session::forget('news_feed_message'); Session::forget('news_feed_message');
return 'success'; 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 { try {
if ($source === IMPORT_CSV) { if ($source === IMPORT_CSV) {
$data = $this->importService->mapCSV($files); $data = $this->importService->mapCSV($files);

View File

@ -176,7 +176,7 @@ class InvoiceApiController extends BaseAPIController
if (isset($data['email_invoice']) && $data['email_invoice']) { if (isset($data['email_invoice']) && $data['email_invoice']) {
if ($payment) { if ($payment) {
$this->mailer->sendPaymentConfirmation($payment); $this->mailer->sendPaymentConfirmation($payment);
} else { } elseif ( ! $invoice->is_recurring) {
$this->mailer->sendInvoice($invoice); $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_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_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_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) private static function getViewModel($invoice)
{ {
$recurringHelp = ''; $recurringHelp = '';
$recurringDueDateHelp = '';
$recurringDueDates = [];
foreach (preg_split("/((\r?\n)|(\r\n?))/", trans('texts.recurring_help')) as $line) { foreach (preg_split("/((\r?\n)|(\r\n?))/", trans('texts.recurring_help')) as $line) {
$parts = explode('=>', $line); $parts = explode('=>', $line);
if (count($parts) > 1) { 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) { foreach (preg_split("/((\r?\n)|(\r\n?))/", trans('texts.recurring_due_date_help')) as $line) {
$parts = explode('=>', $line); $parts = explode('=>', $line);
if (count($parts) > 1) { if (count($parts) > 1) {
@ -409,10 +412,10 @@ class InvoiceController extends BaseController
Session::flash('message', $message); Session::flash('message', $message);
if ($action == 'email') { 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); Session::flash('message', $message);
if ($action == 'clone') { if ($action == 'clone') {
return $this->cloneInvoice($request, $invoice->public_id); return url(sprintf('%ss/%s/clone', $entityType, $invoice->public_id));
} elseif ($action == 'convert') { } elseif ($action == 'convert') {
return $this->convertQuote($request, $invoice->public_id); return $this->convertQuote($request, $invoice->public_id);
} elseif ($action == 'email') { } 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 { } else {
Session::flash('error', $response); Session::flash('error', $response);
} }
return Redirect::to("{$entityType}s/{$invoice->public_id}/edit");
} }
private function emailRecurringInvoice(&$invoice) private function emailRecurringInvoice(&$invoice)
@ -527,11 +528,7 @@ class InvoiceController extends BaseController
Session::flash('message', $message); Session::flash('message', $message);
} }
if ($action == 'restore' && $count == 1) { return $this->returnBulk($entityType, $action, $ids);
return Redirect::to("{$entityType}s/".Utils::getFirst($ids));
} else {
return Redirect::to("{$entityType}s");
}
} }
public function convertQuote(InvoiceRequest $request) public function convertQuote(InvoiceRequest $request)
@ -540,7 +537,7 @@ class InvoiceController extends BaseController
Session::flash('message', trans('texts.converted_to_invoice')); 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) public function cloneInvoice(InvoiceRequest $request, $publicId)
@ -608,12 +605,19 @@ class InvoiceController extends BaseController
return View::make('invoices.history', $data); 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) ->whereInvoiceNumber($invoiceNumber)
->withTrashed() ->withTrashed();
->count();
if ($invoicePublicId) {
$query->where('public_id', '!=', $invoicePublicId);
}
$count = $query->count();
return $count ? RESULT_FAILURE : RESULT_SUCCESS; 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\ClientRepository;
use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\InvoiceRepository;
use App\Services\InvoiceService; use App\Services\InvoiceService;
use App\Models\GatewayType;
/** /**
* Class OnlinePaymentController * Class OnlinePaymentController
@ -60,7 +61,7 @@ class OnlinePaymentController extends BaseController
* @param bool $sourceId * @param bool $sourceId
* @return \Illuminate\Http\RedirectResponse * @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)) { if ( ! $invitation = $this->invoiceRepo->findInvoiceByInvitation($invitationKey)) {
return response()->view('error', [ 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); return redirect()->to('view/' . $invitation->invitation_key);
} }
$invitation = $invitation->load('invoice.client.account.account_gateways.gateway'); $invitation = $invitation->load('invoice.client.account.account_gateways.gateway');
$account = $invitation->account;
$account->loadLocalizationSettings($invitation->invoice->client);
if ( ! $gatewayType) { if ( ! $gatewayTypeAlias) {
$gatewayType = Session::get($invitation->id . 'gateway_type'); $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 { try {
return $paymentDriver->startPurchase(Input::all(), $sourceId); return $paymentDriver->startPurchase(Input::all(), $sourceId);
@ -95,8 +102,12 @@ class OnlinePaymentController extends BaseController
public function doPayment(CreateOnlinePaymentRequest $request) public function doPayment(CreateOnlinePaymentRequest $request)
{ {
$invitation = $request->invitation; $invitation = $request->invitation;
$gatewayType = Session::get($invitation->id . 'gateway_type'); $gatewayTypeId = Session::get($invitation->id . 'gateway_type');
$paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayType); $paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayTypeId);
if ( ! $invitation->invoice->canBePaid()) {
return redirect()->to('view/' . $invitation->invitation_key);
}
try { try {
$paymentDriver->completeOnsitePurchase($request->all()); $paymentDriver->completeOnsitePurchase($request->all());
@ -114,17 +125,24 @@ class OnlinePaymentController extends BaseController
/** /**
* @param bool $invitationKey * @param bool $invitationKey
* @param bool $gatewayType * @param mixed $gatewayTypeAlias
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
*/ */
public function offsitePayment($invitationKey = false, $gatewayType = false) public function offsitePayment($invitationKey = false, $gatewayTypeAlias = false)
{ {
$invitationKey = $invitationKey ?: Session::get('invitation_key'); $invitationKey = $invitationKey ?: Session::get('invitation_key');
$invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.account_gateways.gateway') $invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.account_gateways.gateway')
->where('invitation_key', '=', $invitationKey)->firstOrFail(); ->where('invitation_key', '=', $invitationKey)->firstOrFail();
$gatewayType = $gatewayType ?: Session::get($invitation->id . 'gateway_type'); if ( ! $gatewayTypeAlias) {
$paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayType); $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')) { if ($error = Input::get('error_description') ?: Input::get('error')) {
return $this->error($paymentDriver, $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()) { if (Crawler::isCrawler()) {
return redirect()->to(NINJA_WEB_URL, 301); return redirect()->to(NINJA_WEB_URL, 301);
@ -267,6 +285,8 @@ class OnlinePaymentController extends BaseController
$data = [ $data = [
'client_id' => $client->id, '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' => [[ 'invoice_items' => [[
'product_key' => $product->product_key, 'product_key' => $product->product_key,
'notes' => $product->notes, 'notes' => $product->notes,
@ -280,8 +300,8 @@ class OnlinePaymentController extends BaseController
$invitation = $invoice->invitations[0]; $invitation = $invoice->invitations[0];
$link = $invitation->getLink(); $link = $invitation->getLink();
if ($gatewayType) { if ($gatewayTypeAlias) {
return redirect()->to($invitation->getLink('payment') . "/{$gatewayType}"); return redirect()->to($invitation->getLink('payment') . "/{$gatewayTypeAlias}");
} else { } else {
return redirect()->to($invitation->getLink()); 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) 'invoices' => Invoice::scope()->invoiceType(INVOICE_TYPE_STANDARD)->where('is_recurring', '=', false)
->with('client', 'invoice_status')->orderBy('invoice_number')->get(), ->with('client', 'invoice_status')->orderBy('invoice_number')->get(),
'payment' => $payment, 'payment' => $payment,
'entity' => $payment,
'method' => 'PUT', 'method' => 'PUT',
'url' => 'payments/'.$payment->public_id, 'url' => 'payments/'.$payment->public_id,
'title' => trans('texts.edit_payment'), 'title' => trans('texts.edit_payment'),
'paymentTypes' => Cache::get('paymentTypes'), '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); return View::make('payments.edit', $data);
} }

View File

@ -3,6 +3,7 @@
use Auth; use Auth;
use URL; use URL;
use View; use View;
use Utils;
use Input; use Input;
use Session; use Session;
use Redirect; use Redirect;
@ -37,15 +38,40 @@ class ProductController extends BaseController
*/ */
public function index() 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 * @return \Illuminate\Http\JsonResponse
*/ */
public function getDatatable() 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) public function edit($publicId)
{ {
$account = Auth::user()->account; $account = Auth::user()->account;
$product = Product::scope($publicId)->withTrashed()->firstOrFail();
$data = [ $data = [
'account' => $account, 'account' => $account,
'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->get(['id', 'name', 'rate']) : null, 'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->get(['id', 'name', 'rate']) : null,
'product' => Product::scope($publicId)->firstOrFail(), 'product' => $product,
'entity' => $product,
'method' => 'PUT', 'method' => 'PUT',
'url' => 'products/'.$publicId, 'url' => 'products/'.$publicId,
'title' => trans('texts.edit_product'), 'title' => trans('texts.edit_product'),
@ -111,7 +139,7 @@ class ProductController extends BaseController
private function save($productPublicId = false) private function save($productPublicId = false)
{ {
if ($productPublicId) { if ($productPublicId) {
$product = Product::scope($productPublicId)->firstOrFail(); $product = Product::scope($productPublicId)->withTrashed()->firstOrFail();
} else { } else {
$product = Product::createNew(); $product = Product::createNew();
} }
@ -134,12 +162,12 @@ class ProductController extends BaseController
*/ */
public function bulk() public function bulk()
{ {
$action = Input::get('bulk_action'); $action = Input::get('action');
$ids = Input::get('bulk_public_id'); $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
$count = $this->productService->bulk($ids, $action); $count = $this->productService->bulk($ids, $action);
Session::flash('message', trans('texts.archived_product')); 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); Session::flash('message', $message);
} }
if ($action == 'restore' && $count == 1) { return $this->returnBulk(ENTITY_QUOTE, $action, $ids);
return Redirect::to('quotes/'.Utils::getFirst($ids));
} else {
return Redirect::to('quotes');
}
} }
public function approve($invitationKey) public function approve($invitationKey)

View File

@ -11,6 +11,7 @@ use App\Models\Account;
use App\Models\Client; use App\Models\Client;
use App\Models\Payment; use App\Models\Payment;
use App\Models\Expense; use App\Models\Expense;
use App\Models\Task;
/** /**
* Class ReportController * Class ReportController
@ -56,8 +57,8 @@ class ReportController extends BaseController
if (Input::get('report_type')) { if (Input::get('report_type')) {
$reportType = Input::get('report_type'); $reportType = Input::get('report_type');
$dateField = Input::get('date_field'); $dateField = Input::get('date_field');
$startDate = Utils::toSqlDate(Input::get('start_date'), false); $startDate = date_create(Input::get('start_date'));
$endDate = Utils::toSqlDate(Input::get('end_date'), false); $endDate = date_create(Input::get('end_date'));
} else { } else {
$reportType = ENTITY_INVOICE; $reportType = ENTITY_INVOICE;
$dateField = FILTER_INVOICE_DATE; $dateField = FILTER_INVOICE_DATE;
@ -71,15 +72,17 @@ class ReportController extends BaseController
ENTITY_PRODUCT => trans('texts.product'), ENTITY_PRODUCT => trans('texts.product'),
ENTITY_PAYMENT => trans('texts.payment'), ENTITY_PAYMENT => trans('texts.payment'),
ENTITY_EXPENSE => trans('texts.expense'), ENTITY_EXPENSE => trans('texts.expense'),
ENTITY_TASK => trans('texts.task'),
ENTITY_TAX_RATE => trans('texts.tax'), ENTITY_TAX_RATE => trans('texts.tax'),
]; ];
$params = [ $params = [
'startDate' => $startDate->format(Session::get(SESSION_DATE_FORMAT)), 'startDate' => $startDate->format('Y-m-d'),
'endDate' => $endDate->format(Session::get(SESSION_DATE_FORMAT)), 'endDate' => $endDate->format('Y-m-d'),
'reportTypes' => $reportTypes, 'reportTypes' => $reportTypes,
'reportType' => $reportType, 'reportType' => $reportType,
'title' => trans('texts.charts_and_reports'), 'title' => trans('texts.charts_and_reports'),
'account' => Auth::user()->account,
]; ];
if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) { if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) {
@ -120,9 +123,37 @@ class ReportController extends BaseController
return $this->generateTaxRateReport($startDate, $endDate, $dateField, $isExport); return $this->generateTaxRateReport($startDate, $endDate, $dateField, $isExport);
} elseif ($reportType == ENTITY_EXPENSE) { } elseif ($reportType == ENTITY_EXPENSE) {
return $this->generateExpenseReport($startDate, $endDate, $isExport); 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 $startDate
* @param $endDate * @param $endDate
@ -354,7 +385,7 @@ class ReportController extends BaseController
$isExport ? $client->getDisplayName() : $client->present()->link, $isExport ? $client->getDisplayName() : $client->present()->link,
$isExport ? $invoice->invoice_number : $invoice->present()->link, $isExport ? $invoice->invoice_number : $invoice->present()->link,
$invoice->present()->invoice_date, $invoice->present()->invoice_date,
$invoiceItem->qty, round($invoiceItem->qty, 2),
$invoiceItem->product_key, $invoiceItem->product_key,
]; ];
//$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'paid', $payment ? $payment->amount : 0); //$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) private function generateExpenseReport($startDate, $endDate, $isExport)
{ {
$columns = ['vendor', 'client', 'date', 'expense_amount', 'invoiced_amount']; $columns = ['vendor', 'client', 'date', 'expense_amount'];
$account = Auth::user()->account; $account = Auth::user()->account;
$displayData = []; $displayData = [];
$reportTotals = []; $reportTotals = [];
$expenses = Expense::scope() $expenses = Expense::scope()
->withTrashed() ->withArchived()
->with('client.contacts', 'vendor') ->with('client.contacts', 'vendor')
->where('expense_date', '>=', $startDate) ->where('expense_date', '>=', $startDate)
->where('expense_date', '<=', $endDate); ->where('expense_date', '<=', $endDate);
foreach ($expenses->get() as $expense) { foreach ($expenses->get() as $expense) {
$amount = $expense->amount; $amount = $expense->amountWithTax();
$invoiced = $expense->present()->invoiced_amount;
$displayData[] = [ $displayData[] = [
$expense->vendor ? ($isExport ? $expense->vendor->name : $expense->vendor->present()->link) : '', $expense->vendor ? ($isExport ? $expense->vendor->name : $expense->vendor->present()->link) : '',
$expense->client ? ($isExport ? $expense->client->getDisplayName() : $expense->client->present()->link) : '', $expense->client ? ($isExport ? $expense->client->getDisplayName() : $expense->client->present()->link) : '',
$expense->present()->expense_date, $expense->present()->expense_date,
Utils::formatMoney($amount, $expense->currency_id), 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->expense_currency_id, 'amount', $amount);
$reportTotals = $this->addToTotals($reportTotals, $expense->invoice_currency_id, 'amount', 0); $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 [ return [

View File

@ -2,10 +2,9 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Codedge\Updater\UpdaterManager; use Utils;
use App\Http\Requests;
use Redirect; use Redirect;
use Codedge\Updater\UpdaterManager;
class SelfUpdateController extends BaseController class SelfUpdateController extends BaseController
{ {
@ -21,6 +20,10 @@ class SelfUpdateController extends BaseController
*/ */
public function __construct(UpdaterManager $updater) public function __construct(UpdaterManager $updater)
{ {
if (Utils::isNinjaProd()) {
exit;
}
$this->updater = $updater; $this->updater = $updater;
} }

View File

@ -177,15 +177,14 @@ class TaskController extends BaseController
$data = [ $data = [
'task' => $task, 'task' => $task,
'entity' => $task,
'clientPublicId' => $task->client ? $task->client->public_id : 0, 'clientPublicId' => $task->client ? $task->client->public_id : 0,
'method' => 'PUT', 'method' => 'PUT',
'url' => 'tasks/'.$task->public_id, 'url' => 'tasks/'.$task->public_id,
'title' => trans('texts.edit_task'), 'title' => trans('texts.edit_task'),
'duration' => $task->is_running ? $task->getCurrentDuration() : $task->getDuration(),
'actions' => $actions, 'actions' => $actions,
'timezone' => Auth::user()->account->timezone ? Auth::user()->account->timezone->name : DEFAULT_TIMEZONE, 'timezone' => Auth::user()->account->timezone ? Auth::user()->account->timezone->name : DEFAULT_TIMEZONE,
'datetimeFormat' => Auth::user()->account->getMomentDateTimeFormat(), 'datetimeFormat' => Auth::user()->account->getMomentDateTimeFormat(),
//'entityStatus' => $task->present()->status,
]; ];
$data = array_merge($data, self::getViewModel()); $data = array_merge($data, self::getViewModel());
@ -295,16 +294,12 @@ class TaskController extends BaseController
return Redirect::to("invoices/{$invoiceId}/edit")->with('tasks', $data); return Redirect::to("invoices/{$invoiceId}/edit")->with('tasks', $data);
} }
} else { } else {
$count = $this->taskRepo->bulk($ids, $action); $count = $this->taskService->bulk($ids, $action);
$message = Utils::pluralize($action.'d_task', $count); $message = Utils::pluralize($action.'d_task', $count);
Session::flash('message', $message); Session::flash('message', $message);
if ($action == 'restore' && $count == 1) { return $this->returnBulk($this->entityType, $action, $ids);
return Redirect::to('tasks/'.$ids[0].'/edit');
} else {
return Redirect::to('tasks');
}
} }
} }

View File

@ -66,7 +66,9 @@ class UserController extends BaseController
public function edit($publicId) public function edit($publicId)
{ {
$user = User::where('account_id', '=', Auth::user()->account_id) $user = User::where('account_id', '=', Auth::user()->account_id)
->where('public_id', '=', $publicId)->firstOrFail(); ->where('public_id', '=', $publicId)
->withTrashed()
->firstOrFail();
$data = [ $data = [
'user' => $user, 'user' => $user,
@ -157,7 +159,9 @@ class UserController extends BaseController
if ($userPublicId) { if ($userPublicId) {
$user = User::where('account_id', '=', Auth::user()->account_id) $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'; $rules['email'] = 'required|email|unique:users,email,'.$user->id.',id';
} else { } 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) public function unlinkAccount($userAccountId, $userId)

View File

@ -1,5 +1,7 @@
<?php namespace App\Http\Controllers; <?php namespace App\Http\Controllers;
// vendor // vendor
use App\Http\Requests\UpdateVendorRequest;
use App\Http\Requests\VendorRequest;
use Utils; use Utils;
use Response; use Response;
use Input; use Input;
@ -83,4 +85,73 @@ class VendorApiController extends BaseAPIController
return $this->itemResponse($vendor); 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) public function show(VendorRequest $request)
{ {
$vendor = $request->entity(); $vendor = $request->entity();
$actionLinks = [ $actionLinks = [
['label' => trans('texts.new_vendor'), 'url' => URL::to('/vendors/create/' . $vendor->public_id)] ['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); $message = Utils::pluralize($action.'d_vendor', $count);
Session::flash('message', $message); Session::flash('message', $message);
if ($action == 'restore' && $count == 1) { return $this->returnBulk($this->entityType, $action, $ids);
return Redirect::to('vendors/' . Utils::getFirst($ids));
} else {
return Redirect::to('vendors');
}
} }
} }

View File

@ -18,7 +18,6 @@ class Kernel extends HttpKernel {
'App\Http\Middleware\VerifyCsrfToken', 'App\Http\Middleware\VerifyCsrfToken',
'App\Http\Middleware\DuplicateSubmissionCheck', 'App\Http\Middleware\DuplicateSubmissionCheck',
'App\Http\Middleware\QueryLogging', 'App\Http\Middleware\QueryLogging',
'App\Http\Middleware\SessionDataCheckMiddleware',
'App\Http\Middleware\StartupCheck', 'App\Http\Middleware\StartupCheck',
]; ];

View File

@ -23,18 +23,22 @@ class ApiCheck {
*/ */
public function handle($request, Closure $next) 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(); $headers = Utils::getApiHeaders();
$hasApiSecret = false;
if ($secret = env(API_SECRET)) { 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) { if ($loggingIn) {
// check API secret // check API secret
if ( ! $hasApiSecret) { if ( ! $hasApiSecret) {
sleep(ERROR_DELAY); sleep(ERROR_DELAY);
return Response::json('Invalid secret', 403, $headers); return Response::json('Invalid value for API_SECRET', 403, $headers);
} }
} else { } else {
// check for a valid token // check for a valid token

View File

@ -15,8 +15,7 @@ class DuplicateSubmissionCheck
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
if ($request->is('api/v1/*') || $request->is('documents')) {
if ($request->is('api/v1/*')) {
return $next($request); return $next($request);
} }

View File

@ -23,6 +23,7 @@ class QueryLogging
// Enable query logging for development // Enable query logging for development
if (Utils::isNinjaDev()) { if (Utils::isNinjaDev()) {
DB::enableQueryLog(); DB::enableQueryLog();
$timeStart = microtime(true);
} }
$response = $next($request); $response = $next($request);
@ -32,7 +33,9 @@ class QueryLogging
if (strstr($request->url(), '_debugbar') === false) { if (strstr($request->url(), '_debugbar') === false) {
$queries = DB::getQueryLog(); $queries = DB::getQueryLog();
$count = count($queries); $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); //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 Schema;
use App\Models\Language; use App\Models\Language;
use App\Models\InvoiceDesign; use App\Models\InvoiceDesign;
use App\Events\UserSettingsChanged; use App\Events\UserLoggedIn;
use App\Libraries\CurlUtils; use App\Libraries\CurlUtils;
/** /**
@ -118,13 +118,13 @@ class StartupCheck
// Make sure the account/user localization settings are in the session // Make sure the account/user localization settings are in the session
if (Auth::check() && !Session::has(SESSION_TIMEZONE)) { 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.) // 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'); $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'); $licenseKey = Input::get('license_key');
$productId = Input::get('product_id'); $productId = Input::get('product_id');
@ -154,6 +154,8 @@ class StartupCheck
$company->save(); $company->save();
Session::flash('message', trans('texts.bought_white_label')); 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); $class = Utils::getEntityClass($this->entityType);
if (method_exists($class, 'trashed')) { if (method_exists($class, 'trashed')) {
$this->entity = $class::scope($publicId)->withTrashed()->firstOrFail(); $this->entity = $class::scope($publicId)->withTrashed()->firstOrFail();
} else { } else {
$this->entity = $class::scope($publicId)->firstOrFail(); $this->entity = $class::scope($publicId)->firstOrFail();
} }
return $this->entity; return $this->entity;
} }

View File

@ -4,7 +4,6 @@
class UpdateTaxRateRequest extends TaxRateRequest class UpdateTaxRateRequest extends TaxRateRequest
{ {
// Expenses
/** /**
* Determine if the user is authorized to make this request. * 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 <?php
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Application Routes | Application Routes
@ -39,6 +38,7 @@ Route::post('/get_started', 'AccountController@getStarted');
Route::group(['middleware' => 'auth:client'], function() { Route::group(['middleware' => 'auth:client'], function() {
Route::get('view/{invitation_key}', 'ClientPortalController@view'); Route::get('view/{invitation_key}', 'ClientPortalController@view');
Route::get('download/{invitation_key}', 'ClientPortalController@download'); Route::get('download/{invitation_key}', 'ClientPortalController@download');
Route::put('sign/{invitation_key}', 'ClientPortalController@sign');
Route::get('view', 'HomeController@viewLogo'); Route::get('view', 'HomeController@viewLogo');
Route::get('approve/{invitation_key}', 'QuoteController@approve'); Route::get('approve/{invitation_key}', 'QuoteController@approve');
Route::get('payment/{invitation_key}/{gateway_type?}/{source_id?}', 'OnlinePaymentController@showPayment'); 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/default', 'ClientPortalController@setDefaultPaymentMethod');
Route::post('client/payment_methods/{source_id}/remove', 'ClientPortalController@removePaymentMethod'); Route::post('client/payment_methods/{source_id}/remove', 'ClientPortalController@removePaymentMethod');
Route::get('client/quotes', 'ClientPortalController@quoteIndex'); Route::get('client/quotes', 'ClientPortalController@quoteIndex');
Route::get('client/credits', 'ClientPortalController@creditIndex');
Route::get('client/invoices', 'ClientPortalController@invoiceIndex'); Route::get('client/invoices', 'ClientPortalController@invoiceIndex');
Route::get('client/invoices/recurring', 'ClientPortalController@recurringInvoiceIndex'); Route::get('client/invoices/recurring', 'ClientPortalController@recurringInvoiceIndex');
Route::post('client/invoices/auto_bill', 'ClientPortalController@setAutoBill'); Route::post('client/invoices/auto_bill', 'ClientPortalController@setAutoBill');
Route::get('client/documents', 'ClientPortalController@documentIndex'); Route::get('client/documents', 'ClientPortalController@documentIndex');
Route::get('client/payments', 'ClientPortalController@paymentIndex'); Route::get('client/payments', 'ClientPortalController@paymentIndex');
Route::get('client/dashboard', 'ClientPortalController@dashboard'); Route::get('client/dashboard/{contact_key?}', 'ClientPortalController@dashboard');
Route::get('client/dashboard/{contact_key}', 'ClientPortalController@contactIndex');
Route::get('client/documents/js/{documents}/{filename}', 'ClientPortalController@getDocumentVFSJS'); 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}/{documents}/{filename?}', 'ClientPortalController@getDocument');
Route::get('client/documents/{invitation_key}/{filename?}', 'ClientPortalController@getInvoiceDocumentsZip'); 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.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.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.recurring_invoices', ['as'=>'api.client.recurring_invoices', 'uses'=>'ClientPortalController@recurringInvoiceDatatable']);
Route::get('api/client.documents', ['as'=>'api.client.documents', 'uses'=>'ClientPortalController@documentDatatable']); 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('hide_message', 'HomeController@hideMessage');
Route::get('force_inline_pdf', 'UserController@forcePDFJS'); Route::get('force_inline_pdf', 'UserController@forcePDFJS');
Route::get('account/get_search_data', ['as' => 'get_search_data', 'uses' => 'AccountController@getSearchData']); Route::get('account/get_search_data', ['as' => 'get_search_data', 'uses' => 'AccountController@getSearchData']);
Route::get('check_invoice_number/{invoice_number}', 'InvoiceController@checkInvoiceNumber'); Route::get('check_invoice_number/{invoice_id?}', 'InvoiceController@checkInvoiceNumber');
Route::get('save_sidebar_state', 'UserController@saveSidebarState'); Route::post('save_sidebar_state', 'UserController@saveSidebarState');
Route::get('settings/user_details', 'AccountController@showUserDetails'); Route::get('settings/user_details', 'AccountController@showUserDetails');
Route::post('settings/user_details', 'AccountController@saveUserDetails'); Route::post('settings/user_details', 'AccountController@saveUserDetails');
Route::post('settings/payment_gateway_limits', 'AccountController@savePaymentGatewayLimits');
Route::post('users/change_password', 'UserController@changePassword'); Route::post('users/change_password', 'UserController@changePassword');
Route::resource('clients', 'ClientController'); 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::get('api/credits/{client_id?}', ['as'=>'api.credits', 'uses'=>'CreditController@getDatatable']);
Route::post('credits/bulk', 'CreditController@bulk'); 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::get('/resend_confirmation', 'AccountController@resendConfirmation');
Route::post('/update_setup', 'AppController@updateSetup'); Route::post('/update_setup', 'AppController@updateSetup');
@ -228,10 +235,6 @@ Route::group([
Route::resource('tokens', 'TokenController'); Route::resource('tokens', 'TokenController');
Route::post('tokens/bulk', 'TokenController@bulk'); 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::get('api/tax_rates', ['as'=>'api.tax_rates', 'uses'=>'TaxRateController@getDatatable']);
Route::resource('tax_rates', 'TaxRateController'); Route::resource('tax_rates', 'TaxRateController');
Route::post('tax_rates/bulk', 'TaxRateController@bulk'); 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::get('ping', 'AccountApiController@ping');
Route::post('login', 'AccountApiController@login'); Route::post('login', 'AccountApiController@login');
Route::post('oauth_login', 'AccountApiController@oauthLogin');
Route::post('register', 'AccountApiController@register'); Route::post('register', 'AccountApiController@register');
Route::get('static', 'AccountApiController@getStaticData'); Route::get('static', 'AccountApiController@getStaticData');
Route::get('accounts', 'AccountApiController@show'); Route::get('accounts', 'AccountApiController@show');
@ -305,12 +309,8 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function()
Route::post('update_notifications', 'AccountApiController@updatePushNotifications'); Route::post('update_notifications', 'AccountApiController@updatePushNotifications');
Route::get('dashboard', 'DashboardApiController@index'); Route::get('dashboard', 'DashboardApiController@index');
Route::resource('documents', 'DocumentAPIController'); Route::resource('documents', 'DocumentAPIController');
// Vendor
Route::resource('vendors', 'VendorApiController'); Route::resource('vendors', 'VendorApiController');
Route::resource('expense_categories', 'ExpenseCategoryApiController');
//Expense
Route::resource('expenses', 'ExpenseApiController');
}); });
// Redirects for legacy links // Redirects for legacy links
@ -430,56 +430,50 @@ if (!defined('CONTACT_EMAIL')) {
define('ACTIVITY_TYPE_CREATE_CLIENT', 1); define('ACTIVITY_TYPE_CREATE_CLIENT', 1);
define('ACTIVITY_TYPE_ARCHIVE_CLIENT', 2); define('ACTIVITY_TYPE_ARCHIVE_CLIENT', 2);
define('ACTIVITY_TYPE_DELETE_CLIENT', 3); define('ACTIVITY_TYPE_DELETE_CLIENT', 3);
define('ACTIVITY_TYPE_CREATE_INVOICE', 4); define('ACTIVITY_TYPE_CREATE_INVOICE', 4);
define('ACTIVITY_TYPE_UPDATE_INVOICE', 5); define('ACTIVITY_TYPE_UPDATE_INVOICE', 5);
define('ACTIVITY_TYPE_EMAIL_INVOICE', 6); define('ACTIVITY_TYPE_EMAIL_INVOICE', 6);
define('ACTIVITY_TYPE_VIEW_INVOICE', 7); define('ACTIVITY_TYPE_VIEW_INVOICE', 7);
define('ACTIVITY_TYPE_ARCHIVE_INVOICE', 8); define('ACTIVITY_TYPE_ARCHIVE_INVOICE', 8);
define('ACTIVITY_TYPE_DELETE_INVOICE', 9); define('ACTIVITY_TYPE_DELETE_INVOICE', 9);
define('ACTIVITY_TYPE_CREATE_PAYMENT', 10); define('ACTIVITY_TYPE_CREATE_PAYMENT', 10);
//define('ACTIVITY_TYPE_UPDATE_PAYMENT', 11); //define('ACTIVITY_TYPE_UPDATE_PAYMENT', 11);
define('ACTIVITY_TYPE_ARCHIVE_PAYMENT', 12); define('ACTIVITY_TYPE_ARCHIVE_PAYMENT', 12);
define('ACTIVITY_TYPE_DELETE_PAYMENT', 13); 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_CREATE_CREDIT', 14);
//define('ACTIVITY_TYPE_UPDATE_CREDIT', 15); //define('ACTIVITY_TYPE_UPDATE_CREDIT', 15);
define('ACTIVITY_TYPE_ARCHIVE_CREDIT', 16); define('ACTIVITY_TYPE_ARCHIVE_CREDIT', 16);
define('ACTIVITY_TYPE_DELETE_CREDIT', 17); define('ACTIVITY_TYPE_DELETE_CREDIT', 17);
define('ACTIVITY_TYPE_CREATE_QUOTE', 18); define('ACTIVITY_TYPE_CREATE_QUOTE', 18);
define('ACTIVITY_TYPE_UPDATE_QUOTE', 19); define('ACTIVITY_TYPE_UPDATE_QUOTE', 19);
define('ACTIVITY_TYPE_EMAIL_QUOTE', 20); define('ACTIVITY_TYPE_EMAIL_QUOTE', 20);
define('ACTIVITY_TYPE_VIEW_QUOTE', 21); define('ACTIVITY_TYPE_VIEW_QUOTE', 21);
define('ACTIVITY_TYPE_ARCHIVE_QUOTE', 22); define('ACTIVITY_TYPE_ARCHIVE_QUOTE', 22);
define('ACTIVITY_TYPE_DELETE_QUOTE', 23); define('ACTIVITY_TYPE_DELETE_QUOTE', 23);
define('ACTIVITY_TYPE_RESTORE_QUOTE', 24); define('ACTIVITY_TYPE_RESTORE_QUOTE', 24);
define('ACTIVITY_TYPE_RESTORE_INVOICE', 25); define('ACTIVITY_TYPE_RESTORE_INVOICE', 25);
define('ACTIVITY_TYPE_RESTORE_CLIENT', 26); define('ACTIVITY_TYPE_RESTORE_CLIENT', 26);
define('ACTIVITY_TYPE_RESTORE_PAYMENT', 27); define('ACTIVITY_TYPE_RESTORE_PAYMENT', 27);
define('ACTIVITY_TYPE_RESTORE_CREDIT', 28); define('ACTIVITY_TYPE_RESTORE_CREDIT', 28);
define('ACTIVITY_TYPE_APPROVE_QUOTE', 29); define('ACTIVITY_TYPE_APPROVE_QUOTE', 29);
// Vendors
define('ACTIVITY_TYPE_CREATE_VENDOR', 30); define('ACTIVITY_TYPE_CREATE_VENDOR', 30);
define('ACTIVITY_TYPE_ARCHIVE_VENDOR', 31); define('ACTIVITY_TYPE_ARCHIVE_VENDOR', 31);
define('ACTIVITY_TYPE_DELETE_VENDOR', 32); define('ACTIVITY_TYPE_DELETE_VENDOR', 32);
define('ACTIVITY_TYPE_RESTORE_VENDOR', 33); define('ACTIVITY_TYPE_RESTORE_VENDOR', 33);
// expenses
define('ACTIVITY_TYPE_CREATE_EXPENSE', 34); define('ACTIVITY_TYPE_CREATE_EXPENSE', 34);
define('ACTIVITY_TYPE_ARCHIVE_EXPENSE', 35); define('ACTIVITY_TYPE_ARCHIVE_EXPENSE', 35);
define('ACTIVITY_TYPE_DELETE_EXPENSE', 36); define('ACTIVITY_TYPE_DELETE_EXPENSE', 36);
define('ACTIVITY_TYPE_RESTORE_EXPENSE', 37); define('ACTIVITY_TYPE_RESTORE_EXPENSE', 37);
define('ACTIVITY_TYPE_VOIDED_PAYMENT', 39);
// tasks define('ACTIVITY_TYPE_REFUNDED_PAYMENT', 40);
define('ACTIVITY_TYPE_FAILED_PAYMENT', 41);
define('ACTIVITY_TYPE_CREATE_TASK', 42); define('ACTIVITY_TYPE_CREATE_TASK', 42);
define('ACTIVITY_TYPE_UPDATE_TASK', 43); 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('DEFAULT_INVOICE_NUMBER', '0001');
define('RECENTLY_VIEWED_LIMIT', 20); define('RECENTLY_VIEWED_LIMIT', 20);
@ -491,6 +485,7 @@ if (!defined('CONTACT_EMAIL')) {
define('MAX_IFRAME_URL_LENGTH', 250); define('MAX_IFRAME_URL_LENGTH', 250);
define('MAX_LOGO_FILE_SIZE', 200); // KB define('MAX_LOGO_FILE_SIZE', 200); // KB
define('MAX_FAILED_LOGINS', 10); 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_DOCUMENT_SIZE', env('MAX_DOCUMENT_SIZE', 10000));// KB
define('MAX_EMAIL_DOCUMENTS_SIZE', env('MAX_EMAIL_DOCUMENTS_SIZE', 10000));// Total 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) 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_TIMEZONE', 'timezone');
define('SESSION_CURRENCY', 'currency'); define('SESSION_CURRENCY', 'currency');
define('SESSION_CURRENCY_DECORATOR', 'currency_decorator');
define('SESSION_DATE_FORMAT', 'dateFormat'); define('SESSION_DATE_FORMAT', 'dateFormat');
define('SESSION_DATE_PICKER_FORMAT', 'datePickerFormat'); define('SESSION_DATE_PICKER_FORMAT', 'datePickerFormat');
define('SESSION_DATETIME_FORMAT', 'datetimeFormat'); define('SESSION_DATETIME_FORMAT', 'datetimeFormat');
@ -601,6 +597,7 @@ if (!defined('CONTACT_EMAIL')) {
define('GATEWAY_CYBERSOURCE', 49); define('GATEWAY_CYBERSOURCE', 49);
define('GATEWAY_WEPAY', 60); define('GATEWAY_WEPAY', 60);
define('GATEWAY_BRAINTREE', 61); define('GATEWAY_BRAINTREE', 61);
define('GATEWAY_CUSTOM', 62);
// The customer exists, but only as a local concept // The customer exists, but only as a local concept
// The remote gateway doesn't understand the concept of customers // 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_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest'));
define('NINJA_DATE', '2000-01-01'); define('NINJA_DATE', '2000-01-01');
define('NINJA_VERSION', '2.7.2' . env('NINJA_VERSION_SUFFIX')); 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_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'));
define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.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('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('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('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_LOGIN_URL', 'https://login.microsoftonline.com/common/oauth2/v2.0/token');
define('MSBOT_LUIS_URL', 'https://api.projectoxford.ai/luis/v1/application'); 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_VERIFICATION_FAILED', 'verification_failed');
define('PAYMENT_METHOD_STATUS_VERIFIED', 'verified'); define('PAYMENT_METHOD_STATUS_VERIFIED', 'verified');
define('GATEWAY_TYPE_CREDIT_CARD', 'credit_card'); define('GATEWAY_TYPE_CREDIT_CARD', 1);
define('GATEWAY_TYPE_BANK_TRANSFER', 'bank_transfer'); define('GATEWAY_TYPE_BANK_TRANSFER', 2);
define('GATEWAY_TYPE_PAYPAL', 'paypal'); define('GATEWAY_TYPE_PAYPAL', 3);
define('GATEWAY_TYPE_BITCOIN', 'bitcoin'); define('GATEWAY_TYPE_BITCOIN', 4);
define('GATEWAY_TYPE_DWOLLA', 'dwolla'); define('GATEWAY_TYPE_DWOLLA', 5);
define('GATEWAY_TYPE_CUSTOM', 6);
define('GATEWAY_TYPE_TOKEN', 'token'); define('GATEWAY_TYPE_TOKEN', 'token');
define('REMINDER1', 'reminder1'); define('REMINDER1', 'reminder1');
@ -744,6 +744,10 @@ if (!defined('CONTACT_EMAIL')) {
define('BANK_LIBRARY_OFX', 1); 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_REVENUE_SHARE', 'A');
define('RESELLER_LIMITED_USERS', 'B'); define('RESELLER_LIMITED_USERS', 'B');
@ -852,6 +856,7 @@ if (!defined('CONTACT_EMAIL')) {
'invoiceStatus' => 'App\Models\InvoiceStatus', 'invoiceStatus' => 'App\Models\InvoiceStatus',
'frequencies' => 'App\Models\Frequency', 'frequencies' => 'App\Models\Frequency',
'gateways' => 'App\Models\Gateway', 'gateways' => 'App\Models\Gateway',
'gatewayTypes' => 'App\Models\GatewayType',
'fonts' => 'App\Models\Font', 'fonts' => 'App\Models\Font',
'banks' => 'App\Models\Bank', 'banks' => 'App\Models\Bank',
]; ];

View File

@ -1,21 +1,21 @@
<?php <?php
class parseCSV { class parseCSV {
/* /*
Class: parseCSV v0.3.2 Class: parseCSV v0.3.2
http://code.google.com/p/parsecsv-for-php/ http://code.google.com/p/parsecsv-for-php/
Fully conforms to the specifications lined out on wikipedia: Fully conforms to the specifications lined out on wikipedia:
- http://en.wikipedia.org/wiki/Comma-separated_values - http://en.wikipedia.org/wiki/Comma-separated_values
Based on the concept of Ming Hong Ng's CsvFileParser class: Based on the concept of Ming Hong Ng's CsvFileParser class:
- http://minghong.blogspot.com/2006/07/csv-parser-for-php.html - http://minghong.blogspot.com/2006/07/csv-parser-for-php.html
Copyright (c) 2007 Jim Myhrberg (jim@zydev.info). Copyright (c) 2007 Jim Myhrberg (jim@zydev.info).
Permission is hereby granted, free of charge, to any person obtaining a copy 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, 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
Code Examples Code Examples
---------------- ----------------
# general usage # general usage
@ -74,7 +74,7 @@ class parseCSV {
$csv = new parseCSV(); $csv = new parseCSV();
$csv->output (true, 'movies.csv', $array); $csv->output (true, 'movies.csv', $array);
---------------- ----------------
*/ */
@ -83,87 +83,87 @@ class parseCSV {
* Configuration * Configuration
* - set these options with $object->var_name = 'value'; * - set these options with $object->var_name = 'value';
*/ */
# use first line/entry as field names # use first line/entry as field names
var $heading = true; var $heading = true;
# override field names # override field names
var $fields = []; var $fields = [];
# sort entries by this field # sort entries by this field
var $sort_by = null; var $sort_by = null;
var $sort_reverse = false; var $sort_reverse = false;
# delimiter (comma) and enclosure (double quote) # delimiter (comma) and enclosure (double quote)
var $delimiter = ','; var $delimiter = ',';
var $enclosure = '"'; var $enclosure = '"';
# basic SQL-like conditions for row matching # basic SQL-like conditions for row matching
var $conditions = null; var $conditions = null;
# number of rows to ignore from beginning of data # number of rows to ignore from beginning of data
var $offset = null; var $offset = null;
# limits the number of returned rows to specified amount # limits the number of returned rows to specified amount
var $limit = null; var $limit = null;
# number of rows to analyze when attempting to auto-detect delimiter # number of rows to analyze when attempting to auto-detect delimiter
var $auto_depth = 15; var $auto_depth = 15;
# characters to ignore when attempting to auto-detect delimiter # characters to ignore when attempting to auto-detect delimiter
var $auto_non_chars = "a-zA-Z0-9\n\r"; var $auto_non_chars = "a-zA-Z0-9\n\r";
# preferred delimiter characters, only used when all filtering method # preferred delimiter characters, only used when all filtering method
# returns multiple possible delimiters (happens very rarely) # returns multiple possible delimiters (happens very rarely)
var $auto_preferred = ",;\t.:|"; var $auto_preferred = ",;\t.:|";
# character encoding options # character encoding options
var $convert_encoding = false; var $convert_encoding = false;
var $input_encoding = 'ISO-8859-1'; var $input_encoding = 'ISO-8859-1';
var $output_encoding = 'ISO-8859-1'; var $output_encoding = 'ISO-8859-1';
# used by unparse(), save(), and output() functions # used by unparse(), save(), and output() functions
var $linefeed = "\r\n"; var $linefeed = "\r\n";
# only used by output() function # only used by output() function
var $output_delimiter = ','; var $output_delimiter = ',';
var $output_filename = 'data.csv'; var $output_filename = 'data.csv';
/** /**
* Internal variables * Internal variables
*/ */
# current file # current file
var $file; var $file;
# loaded file contents # loaded file contents
var $file_data; var $file_data;
# array of field values in data parsed # array of field values in data parsed
var $titles = []; var $titles = [];
# two dimentional array of CSV data # two dimentional array of CSV data
var $data = []; var $data = [];
/** /**
* Constructor * Constructor
* @param input CSV file or string * @param input CSV file or string
* @return nothing * @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 ( $offset !== null ) $this->offset = $offset;
if ( $limit !== null ) $this->limit = $limit; if ( $limit !== null ) $this->limit = $limit;
if ( count($conditions) > 0 ) $this->conditions = $conditions; if ( count($conditions) > 0 ) $this->conditions = $conditions;
if ( !empty($input) ) $this->parse($input); if ( !empty($input) ) $this->parse($input);
} }
// ============================================== // ==============================================
// ----- [ Main Functions ] --------------------- // ----- [ Main Functions ] ---------------------
// ============================================== // ==============================================
/** /**
* Parse CSV file or string * Parse CSV file or string
* @param input CSV file or string * @param input CSV file or string
@ -184,7 +184,7 @@ class parseCSV {
} }
return true; return true;
} }
/** /**
* Save changes, or new file and/or data * Save changes, or new file and/or data
* @param file file to save to * @param file file to save to
@ -199,7 +199,7 @@ class parseCSV {
$is_php = ( preg_match('/\.php$/i', $file) ) ? true : false ; $is_php = ( preg_match('/\.php$/i', $file) ) ? true : false ;
return $this->_wfile($file, $this->unparse($data, $fields, $append, $is_php), $mode); return $this->_wfile($file, $this->unparse($data, $fields, $append, $is_php), $mode);
} }
/** /**
* Generate CSV based string for output * Generate CSV based string for output
* @param output if true, prints headers and strings to browser * @param output if true, prints headers and strings to browser
@ -220,7 +220,7 @@ class parseCSV {
} }
return $data; return $data;
} }
/** /**
* Convert character encoding * Convert character encoding
* @param input input character encoding, uses default if left blank * @param input input character encoding, uses default if left blank
@ -232,7 +232,7 @@ class parseCSV {
if ( $input !== null ) $this->input_encoding = $input; if ( $input !== null ) $this->input_encoding = $input;
if ( $output !== null ) $this->output_encoding = $output; if ( $output !== null ) $this->output_encoding = $output;
} }
/** /**
* Auto-Detect Delimiter: Find delimiter by analyzing a specific number of * Auto-Detect Delimiter: Find delimiter by analyzing a specific number of
* rows to determine most probable delimiter character * rows to determine most probable delimiter character
@ -244,13 +244,13 @@ class parseCSV {
* @return delimiter character * @return delimiter character
*/ */
function auto ($file = null, $parse = true, $search_depth = null, $preferred = null, $enclosure = null) { function auto ($file = null, $parse = true, $search_depth = null, $preferred = null, $enclosure = null) {
if ( $file === null ) $file = $this->file; if ( $file === null ) $file = $this->file;
if ( empty($search_depth) ) $search_depth = $this->auto_depth; if ( empty($search_depth) ) $search_depth = $this->auto_depth;
if ( $enclosure === null ) $enclosure = $this->enclosure; if ( $enclosure === null ) $enclosure = $this->enclosure;
if ( $preferred === null ) $preferred = $this->auto_preferred; if ( $preferred === null ) $preferred = $this->auto_preferred;
if ( empty($this->file_data) ) { if ( empty($this->file_data) ) {
if ( $this->_check_data($file) ) { if ( $this->_check_data($file) ) {
$data = &$this->file_data; $data = &$this->file_data;
@ -258,24 +258,24 @@ class parseCSV {
} else { } else {
$data = &$this->file_data; $data = &$this->file_data;
} }
$chars = []; $chars = [];
$strlen = strlen($data); $strlen = strlen($data);
$enclosed = false; $enclosed = false;
$n = 1; $n = 1;
$to_end = true; $to_end = true;
// walk specific depth finding posssible delimiter characters // walk specific depth finding posssible delimiter characters
for ( $i=0; $i < $strlen; $i++ ) { for ( $i=0; $i < $strlen; $i++ ) {
$ch = $data{$i}; $ch = $data{$i};
$nch = ( isset($data{$i+1}) ) ? $data{$i+1} : false ; $nch = ( isset($data{$i+1}) ) ? $data{$i+1} : false ;
$pch = ( isset($data{$i-1}) ) ? $data{$i-1} : false ; $pch = ( isset($data{$i-1}) ) ? $data{$i-1} : false ;
// open and closing quotes // open and closing quotes
if ( $ch == $enclosure && (!$enclosed || $nch != $enclosure) ) { if ( $ch == $enclosure && (!$enclosed || $nch != $enclosure) ) {
$enclosed = ( $enclosed ) ? false : true ; $enclosed = ( $enclosed ) ? false : true ;
// inline quotes // inline quotes
} elseif ( $ch == $enclosure && $enclosed ) { } elseif ( $ch == $enclosure && $enclosed ) {
$i++; $i++;
@ -287,7 +287,7 @@ class parseCSV {
} else { } else {
$n++; $n++;
} }
// count character // count character
} elseif (!$enclosed) { } elseif (!$enclosed) {
if ( !preg_match('/['.preg_quote($this->auto_non_chars, '/').']/i', $ch) ) { if ( !preg_match('/['.preg_quote($this->auto_non_chars, '/').']/i', $ch) ) {
@ -299,7 +299,7 @@ class parseCSV {
} }
} }
} }
// filtering // filtering
$depth = ( $to_end ) ? $n-1 : $n ; $depth = ( $to_end ) ? $n-1 : $n ;
$filtered = []; $filtered = [];
@ -308,24 +308,24 @@ class parseCSV {
$filtered[$match] = $char; $filtered[$match] = $char;
} }
} }
// capture most probable delimiter // capture most probable delimiter
ksort($filtered); ksort($filtered);
$delimiter = reset($filtered); $delimiter = reset($filtered);
$this->delimiter = $delimiter; $this->delimiter = $delimiter;
// parse data // parse data
if ( $parse ) $this->data = $this->parse_string(); if ( $parse ) $this->data = $this->parse_string();
return $delimiter; return $delimiter;
} }
// ============================================== // ==============================================
// ----- [ Core Functions ] --------------------- // ----- [ Core Functions ] ---------------------
// ============================================== // ==============================================
/** /**
* Read file to string and call parse_string() * Read file to string and call parse_string()
* @param file local CSV file * @param file local CSV file
@ -336,7 +336,7 @@ class parseCSV {
if ( empty($this->file_data) ) $this->load_data($file); if ( empty($this->file_data) ) $this->load_data($file);
return ( !empty($this->file_data) ) ? $this->parse_string() : false ; return ( !empty($this->file_data) ) ? $this->parse_string() : false ;
} }
/** /**
* Parse CSV strings to arrays * Parse CSV strings to arrays
* @param data CSV string * @param data CSV string
@ -348,7 +348,7 @@ class parseCSV {
$data = &$this->file_data; $data = &$this->file_data;
} else return false; } else return false;
} }
$rows = []; $rows = [];
$row = []; $row = [];
$row_count = 0; $row_count = 0;
@ -358,19 +358,19 @@ class parseCSV {
$enclosed = false; $enclosed = false;
$was_enclosed = false; $was_enclosed = false;
$strlen = strlen($data); $strlen = strlen($data);
// walk through each character // walk through each character
for ( $i=0; $i < $strlen; $i++ ) { for ( $i=0; $i < $strlen; $i++ ) {
$ch = $data{$i}; $ch = $data{$i};
$nch = ( isset($data{$i+1}) ) ? $data{$i+1} : false ; $nch = ( isset($data{$i+1}) ) ? $data{$i+1} : false ;
$pch = ( isset($data{$i-1}) ) ? $data{$i-1} : false ; $pch = ( isset($data{$i-1}) ) ? $data{$i-1} : false ;
// open and closing quotes // open and closing quotes
if ( $ch == $this->enclosure && (!$enclosed || $nch != $this->enclosure) ) { if ( $ch == $this->enclosure && (!$enclosed || $nch != $this->enclosure) ) {
$enclosed = ( $enclosed ) ? false : true ; $enclosed = ( $enclosed ) ? false : true ;
if ( $enclosed ) $was_enclosed = true; if ( $enclosed ) $was_enclosed = true;
// inline quotes // inline quotes
} elseif ( $ch == $this->enclosure && $enclosed ) { } elseif ( $ch == $this->enclosure && $enclosed ) {
$current .= $ch; $current .= $ch;
$i++; $i++;
@ -382,7 +382,7 @@ class parseCSV {
$row[$key] = $current; $row[$key] = $current;
$current = ''; $current = '';
$col++; $col++;
// end of row // end of row
if ( $ch == "\n" || $ch == "\r" ) { if ( $ch == "\n" || $ch == "\r" ) {
if ( $this->_validate_offset($row_count) && $this->_validate_row_conditions($row, $this->conditions) ) { if ( $this->_validate_offset($row_count) && $this->_validate_row_conditions($row, $this->conditions) ) {
@ -406,7 +406,7 @@ class parseCSV {
$i = $strlen; $i = $strlen;
} }
} }
// append character to current field // append character to current field
} else { } else {
$current .= $ch; $current .= $ch;
@ -421,7 +421,7 @@ class parseCSV {
} }
return $rows; return $rows;
} }
/** /**
* Create CSV data from array * Create CSV data from array
* @param data 2D array with data * @param data 2D array with data
@ -436,10 +436,10 @@ class parseCSV {
if ( !is_array($data) || empty($data) ) $data = &$this->data; if ( !is_array($data) || empty($data) ) $data = &$this->data;
if ( !is_array($fields) || empty($fields) ) $fields = &$this->titles; if ( !is_array($fields) || empty($fields) ) $fields = &$this->titles;
if ( $delimiter === null ) $delimiter = $this->delimiter; if ( $delimiter === null ) $delimiter = $this->delimiter;
$string = ( $is_php ) ? "<?php header('Status: 403'); die(' '); ?>".$this->linefeed : '' ; $string = ( $is_php ) ? "<?php header('Status: 403'); die(' '); ?>".$this->linefeed : '' ;
$entry = []; $entry = [];
// create heading // create heading
if ( $this->heading && !$append ) { if ( $this->heading && !$append ) {
foreach( $fields as $key => $value ) { foreach( $fields as $key => $value ) {
@ -448,7 +448,7 @@ class parseCSV {
$string .= implode($delimiter, $entry).$this->linefeed; $string .= implode($delimiter, $entry).$this->linefeed;
$entry = []; $entry = [];
} }
// create data // create data
foreach( $data as $key => $row ) { foreach( $data as $key => $row ) {
foreach( $row as $field => $value ) { foreach( $row as $field => $value ) {
@ -457,10 +457,10 @@ class parseCSV {
$string .= implode($delimiter, $entry).$this->linefeed; $string .= implode($delimiter, $entry).$this->linefeed;
$entry = []; $entry = [];
} }
return $string; return $string;
} }
/** /**
* Load local file or string * Load local file or string
* @param input local CSV file * @param input local CSV file
@ -488,16 +488,16 @@ class parseCSV {
} }
return false; return false;
} }
// ============================================== // ==============================================
// ----- [ Internal Functions ] ----------------- // ----- [ Internal Functions ] -----------------
// ============================================== // ==============================================
/** /**
* Validate a row against specified conditions * Validate a row against specified conditions
* @param row array with values from a row * @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 * @return true of false
*/ */
function _validate_row_conditions ($row = [], $conditions = null) { function _validate_row_conditions ($row = [], $conditions = null) {
@ -523,11 +523,11 @@ class parseCSV {
} }
return false; return false;
} }
/** /**
* Validate a row against a single condition * Validate a row against a single condition
* @param row array with values from a row * @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 * @return true of false
*/ */
function _validate_row_condition ($row, $condition) { function _validate_row_condition ($row, $condition) {
@ -583,7 +583,7 @@ class parseCSV {
} }
return '1'; return '1';
} }
/** /**
* Validates if the row is within the offset or not if sorting is disabled * Validates if the row is within the offset or not if sorting is disabled
* @param current_row the current row number being processed * @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; if ( $this->sort_by === null && $this->offset !== null && $current_row < $this->offset ) return false;
return true; return true;
} }
/** /**
* Enclose values if needed * Enclose values if needed
* - only used by unparse() * - only used by unparse()
@ -611,7 +611,7 @@ class parseCSV {
} }
return $value; return $value;
} }
/** /**
* Check file data * Check file data
* @param file local filename * @param file local filename
@ -624,8 +624,8 @@ class parseCSV {
} }
return true; return true;
} }
/** /**
* Check if passed info might be delimiter * Check if passed info might be delimiter
* - only used by find_delimiter() * - only used by find_delimiter()
@ -656,7 +656,7 @@ class parseCSV {
} else return false; } else return false;
} }
} }
/** /**
* Read local file * Read local file
* @param file local filename * @param file local filename
@ -689,7 +689,7 @@ class parseCSV {
} }
return false; return false;
} }
} }
?> ?>

View File

@ -24,6 +24,8 @@ class HistoryUtils
ACTIVITY_TYPE_CREATE_CLIENT, ACTIVITY_TYPE_CREATE_CLIENT,
ACTIVITY_TYPE_CREATE_TASK, ACTIVITY_TYPE_CREATE_TASK,
ACTIVITY_TYPE_UPDATE_TASK, ACTIVITY_TYPE_UPDATE_TASK,
ACTIVITY_TYPE_CREATE_EXPENSE,
ACTIVITY_TYPE_UPDATE_EXPENSE,
ACTIVITY_TYPE_CREATE_INVOICE, ACTIVITY_TYPE_CREATE_INVOICE,
ACTIVITY_TYPE_UPDATE_INVOICE, ACTIVITY_TYPE_UPDATE_INVOICE,
ACTIVITY_TYPE_EMAIL_INVOICE, ACTIVITY_TYPE_EMAIL_INVOICE,
@ -35,7 +37,7 @@ class HistoryUtils
]; ];
$activities = Activity::scope() $activities = Activity::scope()
->with(['client.contacts', 'invoice', 'task']) ->with(['client.contacts', 'invoice', 'task', 'expense'])
->whereIn('user_id', $userIds) ->whereIn('user_id', $userIds)
->whereIn('activity_type_id', $activityTypes) ->whereIn('activity_type_id', $activityTypes)
->orderBy('id', 'asc') ->orderBy('id', 'asc')
@ -52,6 +54,12 @@ class HistoryUtils
continue; continue;
} }
$entity->setRelation('client', $activity->client); $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 { } else {
$entity = $activity->invoice; $entity = $activity->invoice;
if ( ! $entity) { if ( ! $entity) {

View File

@ -22,6 +22,9 @@ class Utils
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "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() public static function isRegistered()
{ {
@ -92,6 +95,17 @@ class Utils
return Utils::getResllerType() ? true : false; 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() public static function getResllerType()
{ {
return isset($_ENV['RESELLER_TYPE']) ? $_ENV['RESELLER_TYPE'] : false; return isset($_ENV['RESELLER_TYPE']) ? $_ENV['RESELLER_TYPE'] : false;
@ -151,6 +165,11 @@ class Utils
return Auth::check() && Auth::user()->isTrial(); return Auth::check() && Auth::user()->isTrial();
} }
public static function isPaidPro()
{
return static::isPro() && ! static::isTrial();
}
public static function isEnglish() public static function isEnglish()
{ {
return App::getLocale() == 'en'; return App::getLocale() == 'en';
@ -186,7 +205,7 @@ class Utils
$response = new stdClass(); $response = new stdClass();
$response->message = isset($_ENV["{$userType}_MESSAGE"]) ? $_ENV["{$userType}_MESSAGE"] : ''; $response->message = isset($_ENV["{$userType}_MESSAGE"]) ? $_ENV["{$userType}_MESSAGE"] : '';
$response->id = isset($_ENV["{$userType}_ID"]) ? $_ENV["{$userType}_ID"] : ''; $response->id = isset($_ENV["{$userType}_ID"]) ? $_ENV["{$userType}_ID"] : '';
$response->version = env('NINJA_SELF_HOST_VERSION', NINJA_VERSION); $response->version = NINJA_VERSION;
return $response; return $response;
} }
@ -354,7 +373,7 @@ class Utils
return $data->first(); 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); $value = floatval($value);
@ -362,6 +381,10 @@ class Utils
$currencyId = Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY); $currencyId = Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY);
} }
if (!$decorator) {
$decorator = Session::get(SESSION_CURRENCY_DECORATOR, CURRENCY_DECORATOR_SYMBOL);
}
if (!$countryId && Auth::check()) { if (!$countryId && Auth::check()) {
$countryId = Auth::user()->account->country_id; $countryId = Auth::user()->account->country_id;
} }
@ -387,7 +410,9 @@ class Utils
$value = number_format($value, $precision, $decimal, $thousand); $value = number_format($value, $precision, $decimal, $thousand);
$symbol = $currency->symbol; $symbol = $currency->symbol;
if ($showCode || !$symbol) { if ($decorator == CURRENCY_DECORATOR_NONE) {
return $value;
} elseif ($decorator == CURRENCY_DECORATOR_CODE || ! $symbol) {
return "{$value} {$code}"; return "{$value} {$code}";
} elseif ($swapSymbol) { } elseif ($swapSymbol) {
return "{$value} " . trim($symbol); 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) private static function getMonth($offset)
{ {
$months = ['january', 'february', 'march', 'april', 'may', 'june', $months = static::$months;
'july', 'august', 'september', 'october', 'november', 'december', ];
$month = intval(date('n')) - 1; $month = intval(date('n')) - 1;
$month += $offset; $month += $offset;
@ -1029,4 +1065,66 @@ class Utils
return trans('texts.'.strtolower($day)); 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; <?php namespace App\Listeners;
use App\Events\TaskWasCreated;
use App\Events\TaskWasUpdated;
use App\Models\Invoice; use App\Models\Invoice;
use App\Events\ClientWasCreated; use App\Events\ClientWasCreated;
use App\Events\ClientWasDeleted; use App\Events\ClientWasDeleted;
@ -33,6 +31,16 @@ use App\Events\CreditWasCreated;
use App\Events\CreditWasDeleted; use App\Events\CreditWasDeleted;
use App\Events\CreditWasArchived; use App\Events\CreditWasArchived;
use App\Events\CreditWasRestored; 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; use App\Ninja\Repositories\ActivityRepository;
/** /**
@ -123,7 +131,9 @@ class ActivityListener
return; 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( $activity = $this->activityRepo->create(
$event->invoice, $event->invoice,
@ -489,9 +499,92 @@ class ActivityListener
*/ */
public function updatedTask(TaskWasUpdated $event) public function updatedTask(TaskWasUpdated $event)
{ {
if ( ! $event->task->isChanged()) {
return;
}
$this->activityRepo->create( $this->activityRepo->create(
$event->task, $event->task,
ACTIVITY_TYPE_UPDATE_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', 'enable_second_tax_rate',
'include_item_taxes_inline', 'include_item_taxes_inline',
'start_of_week', '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, 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 * @return \Illuminate\Database\Eloquent\Relations\HasMany
*/ */
@ -438,7 +463,7 @@ class Account extends Eloquent
* @param bool $hideSymbol * @param bool $hideSymbol
* @return string * @return string
*/ */
public function formatMoney($amount, $client = null, $hideSymbol = false) public function formatMoney($amount, $client = null, $decorator = false)
{ {
if ($client && $client->currency_id) { if ($client && $client->currency_id) {
$currencyId = $client->currency_id; $currencyId = $client->currency_id;
@ -456,9 +481,11 @@ class Account extends Eloquent
$countryId = false; $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 $invitation
* @param bool $gatewayType * @param mixed $gatewayTypeId
* @return bool * @return bool
*/ */
public function paymentDriver($invitation = false, $gatewayType = false) public function paymentDriver($invitation = false, $gatewayTypeId = false)
{ {
/** @var AccountGateway $accountGateway */ /** @var AccountGateway $accountGateway */
if ($accountGateway = $this->getGatewayByType($gatewayType)) { if ($accountGateway = $this->getGatewayByType($gatewayTypeId)) {
return $accountGateway->paymentDriver($invitation, $gatewayType); return $accountGateway->paymentDriver($invitation, $gatewayTypeId);
} }
return false; return false;
@ -735,6 +762,22 @@ class Account extends Eloquent
return Document::getDirectFileUrl($this->logo, $this->getLogoDisk()); 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 * @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); $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, $currencyId);
Session::put(SESSION_CURRENCY_DECORATOR, $this->show_currency_code ? CURRENCY_DECORATOR_CODE : CURRENCY_DECORATOR_SYMBOL);
Session::put(SESSION_LOCALE, $locale); Session::put(SESSION_LOCALE, $locale);
App::setLocale($locale); App::setLocale($locale);
@ -1812,6 +1856,43 @@ class Account extends Eloquent
public function getFontFolders(){ public function getFontFolders(){
return array_map(function($item){return $item['folder'];}, $this->getFontsData()); 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) Account::updated(function ($account)

View File

@ -73,14 +73,14 @@ class AccountGateway extends EntityModel
/** /**
* @param bool $invitation * @param bool $invitation
* @param bool $gatewayType * @param mixed $gatewayTypeId
* @return mixed * @return mixed
*/ */
public function paymentDriver($invitation = false, $gatewayType = false) public function paymentDriver($invitation = false, $gatewayTypeId = false)
{ {
$class = static::paymentDriverClass($this->gateway->provider); $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(); return $this->belongsTo('App\Models\Task')->withTrashed();
} }
public function expense()
{
return $this->belongsTo('App\Models\Expense')->withTrashed();
}
public function key() public function key()
{ {
return sprintf('%s-%s-%s', $this->activity_type_id, $this->client_id, $this->created_at->timestamp); 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; $contactId = $this->contact_id;
$payment = $this->payment; $payment = $this->payment;
$credit = $this->credit; $credit = $this->credit;
$expense = $this->expense;
$isSystem = $this->is_system; $isSystem = $this->is_system;
/** @var Task $task */
$task = $this->task; $task = $this->task;
$data = [ $data = [
@ -117,6 +121,7 @@ class Activity extends Eloquent
'adjustment' => $this->adjustment ? $account->formatMoney($this->adjustment, $this) : null, 'adjustment' => $this->adjustment ? $account->formatMoney($this->adjustment, $this) : null,
'credit' => $credit ? $account->formatMoney($credit->amount, $client) : null, 'credit' => $credit ? $account->formatMoney($credit->amount, $client) : null,
'task' => $task ? link_to($task->getRoute(), substr($task->description, 0, 30).'...') : 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); return trans("texts.activity_{$activityTypeId}", $data);

View File

@ -89,6 +89,10 @@ class Client extends EntityModel
* @var string * @var string
*/ */
public static $fieldWebsite = 'website'; public static $fieldWebsite = 'website';
/**
* @var string
*/
public static $fieldVatNumber = 'vat_number';
/** /**
* @return array * @return array
@ -106,6 +110,7 @@ class Client extends EntityModel
Client::$fieldCountry, Client::$fieldCountry,
Client::$fieldNotes, Client::$fieldNotes,
Client::$fieldWebsite, Client::$fieldWebsite,
Client::$fieldVatNumber,
Contact::$fieldFirstName, Contact::$fieldFirstName,
Contact::$fieldLastName, Contact::$fieldLastName,
Contact::$fieldPhone, Contact::$fieldPhone,
@ -132,6 +137,7 @@ class Client extends EntityModel
'country' => 'country', 'country' => 'country',
'note' => 'notes', 'note' => 'notes',
'site|website' => 'website', 'site|website' => 'website',
'vat' => 'vat_number',
]; ];
} }
@ -159,6 +165,14 @@ class Client extends EntityModel
return $this->hasMany('App\Models\Invoice'); 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 * @return \Illuminate\Database\Eloquent\Relations\HasMany
*/ */
@ -223,6 +237,14 @@ class Client extends EntityModel
return $this->hasMany('App\Models\Credit'); 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 * @return mixed
*/ */
@ -329,7 +351,7 @@ class Client extends EntityModel
$contact = $this->contacts[0]; $contact = $this->contacts[0];
return $contact->getDisplayName() ?: trans('texts.unnamed_client'); return $contact->getDisplayName();
} }
/** /**

View File

@ -2,6 +2,7 @@
use Auth; use Auth;
use Eloquent; use Eloquent;
use Illuminate\Database\QueryException;
use Utils; use Utils;
use Validator; use Validator;
@ -14,6 +15,12 @@ class EntityModel extends Eloquent
* @var bool * @var bool
*/ */
public $timestamps = true; public $timestamps = true;
/**
* @var bool
*/
protected static $hasPublicId = true;
/** /**
* @var array * @var array
*/ */
@ -56,13 +63,16 @@ class EntityModel extends Eloquent
$lastEntity = $className::whereAccountId($entity->account_id); $lastEntity = $className::whereAccountId($entity->account_id);
} }
$lastEntity = $lastEntity->orderBy('public_id', 'DESC')
->first();
if ($lastEntity) { if (static::$hasPublicId) {
$entity->public_id = $lastEntity->public_id + 1; $lastEntity = $lastEntity->orderBy('public_id', 'DESC')
} else { ->first();
$entity->public_id = 1;
if ($lastEntity) {
$entity->public_id = $lastEntity->public_id + 1;
} else {
$entity->public_id = 1;
}
} }
return $entity; return $entity;
@ -244,6 +254,7 @@ class EntityModel extends Eloquent
$icons = [ $icons = [
'dashboard' => 'tachometer', 'dashboard' => 'tachometer',
'clients' => 'users', 'clients' => 'users',
'products' => 'cube',
'invoices' => 'file-pdf-o', 'invoices' => 'file-pdf-o',
'payments' => 'credit-card', 'payments' => 'credit-card',
'recurring_invoices' => 'files-o', 'recurring_invoices' => 'files-o',
@ -253,9 +264,21 @@ class EntityModel extends Eloquent
'expenses' => 'file-image-o', 'expenses' => 'file-image-o',
'vendors' => 'building', 'vendors' => 'building',
'settings' => 'cog', 'settings' => 'cog',
'self-update' => 'download',
]; ];
return array_get($icons, $entityType); 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; <?php namespace App\Models;
use Utils;
use Laracasts\Presenter\PresentableTrait; use Laracasts\Presenter\PresentableTrait;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use App\Events\ExpenseWasCreated; use App\Events\ExpenseWasCreated;
@ -31,6 +32,7 @@ class Expense extends EntityModel
'client_id', 'client_id',
'vendor_id', 'vendor_id',
'expense_currency_id', 'expense_currency_id',
'expense_date',
'invoice_currency_id', 'invoice_currency_id',
'amount', 'amount',
'foreign_amount', 'foreign_amount',
@ -46,6 +48,29 @@ class Expense extends EntityModel
'tax_name2', '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 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/ */
@ -107,7 +132,13 @@ class Expense extends EntityModel
*/ */
public function getName() 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; return $query;
} }
public function amountWithTax()
{
return Utils::calculateTaxes($this->amount, $this->tax_rate1, $this->tax_rate2);
}
} }
Expense::creating(function ($expense) { Expense::creating(function ($expense) {
@ -196,7 +232,3 @@ Expense::updated(function ($expense) {
Expense::deleting(function ($expense) { Expense::deleting(function ($expense) {
$expense->setNullValues(); $expense->setNullValues();
}); });
Expense::deleted(function ($expense) {
event(new ExpenseWasDeleted($expense));
});

View File

@ -14,6 +14,12 @@ class Gateway extends Eloquent
*/ */
public $timestamps = true; public $timestamps = true;
protected $fillable = [
'provider',
'is_offsite',
'sort_order',
];
/** /**
* @var array * @var array
*/ */
@ -39,6 +45,7 @@ class Gateway extends Eloquent
GATEWAY_BRAINTREE, GATEWAY_BRAINTREE,
GATEWAY_AUTHORIZE_NET, GATEWAY_AUTHORIZE_NET,
GATEWAY_MOLLIE, GATEWAY_MOLLIE,
GATEWAY_CUSTOM,
]; ];
// allow adding these gateway if another gateway // allow adding these gateway if another gateway
@ -174,6 +181,18 @@ class Gateway extends Eloquent
*/ */
public function getFields() 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(); $invoice->markViewed();
$client->markLoggedIn(); $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'; return storage_path() . '/pdfcache/cache-' . $this->id . '.pdf';
} }
public function canBePaid()
{
return floatval($this->balance) > 0 && ! $this->is_deleted;
}
/** /**
* @param $invoice * @param $invoice
* @return string * @return string

View File

@ -11,4 +11,12 @@ class PaymentType extends Eloquent
* @var bool * @var bool
*/ */
public $timestamps = false; 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 SoftDeletes;
use PresentableTrait; use PresentableTrait;
/**
* @var array
*/
protected $fillable = [
'client_id',
'description',
'time_log',
'is_running',
];
/** /**
* @return mixed * @return mixed
*/ */
@ -82,6 +92,18 @@ class Task extends EntityModel
return self::calcStartTime($this); 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 * @param $task
* @return int * @return int
@ -165,6 +187,14 @@ class Task extends EntityModel
return '#' . $this->public_id; 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.partial_due',
'invoice.custom_text_value1', 'invoice.custom_text_value1',
'invoice.custom_text_value2', 'invoice.custom_text_value2',
'.blank',
], ],
INVOICE_FIELDS_CLIENT => [ INVOICE_FIELDS_CLIENT => [
'client.client_name', 'client.client_name',
@ -97,9 +98,11 @@ trait PresentsInvoice
'client.city_state_postal', 'client.city_state_postal',
'client.country', 'client.country',
'client.email', 'client.email',
'client.phone',
'client.contact_name', 'client.contact_name',
'client.custom_value1', 'client.custom_value1',
'client.custom_value2', 'client.custom_value2',
'.blank',
], ],
INVOICE_FIELDS_ACCOUNT => [ INVOICE_FIELDS_ACCOUNT => [
'account.company_name', 'account.company_name',
@ -114,6 +117,7 @@ trait PresentsInvoice
'account.country', 'account.country',
'account.custom_value1', 'account.custom_value1',
'account.custom_value2', 'account.custom_value2',
'.blank',
] ]
]; ];
@ -198,6 +202,7 @@ trait PresentsInvoice
'company_name', 'company_name',
'website', 'website',
'phone', 'phone',
'blank',
]; ];
foreach ($fields as $field) { foreach ($fields as $field) {

View File

@ -5,19 +5,14 @@ use Event;
use App\Libraries\Utils; use App\Libraries\Utils;
use App\Events\UserSettingsChanged; use App\Events\UserSettingsChanged;
use App\Events\UserSignedUp; use App\Events\UserSignedUp;
use Illuminate\Auth\Authenticatable; use Illuminate\Foundation\Auth\User as 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\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
/** /**
* Class User * Class User
*/ */
class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract { class User extends Authenticatable
{
/** /**
* @var array * @var array
*/ */
@ -27,8 +22,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
'edit_all' => 0b0100, 'edit_all' => 0b0100,
]; ];
use Authenticatable, Authorizable, CanResetPassword;
/** /**
* The database table used by the model. * The database table used by the model.
* *
@ -102,26 +95,6 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return PERSON_USER; 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. * 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 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() public function clearSession()
{ {
$keys = [ $keys = [
@ -425,7 +373,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
public function caddAddUsers() public function caddAddUsers()
{ {
if ( ! Utils::isNinja()) { if ( ! Utils::isNinjaProd()) {
return true; return true;
} elseif ( ! $this->hasFeature(FEATURE_USERS)) { } elseif ( ! $this->hasFeature(FEATURE_USERS)) {
return false; return false;
@ -441,6 +389,12 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
return $numUsers < $company->num_users; 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) { User::updating(function ($user) {

View File

@ -211,7 +211,8 @@ class Vendor extends EntityModel
*/ */
public function addVendorContact($data, $isPrimary = false) 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') { if ($publicId && $publicId != '-1') {
$contact = VendorContact::scope($publicId)->firstOrFail(); $contact = VendorContact::scope($publicId)->firstOrFail();

View File

@ -1,10 +1,17 @@
<?php namespace App\Ninja\Datatables; <?php namespace App\Ninja\Datatables;
use App\Models\AccountGatewaySettings;
use App\Models\GatewayType;
use URL; use URL;
use Cache;
use Utils;
use Session;
use App\Models\AccountGateway; use App\Models\AccountGateway;
class AccountGatewayDatatable extends EntityDatatable class AccountGatewayDatatable extends EntityDatatable
{ {
private static $accountGateways;
public $entityType = ENTITY_ACCOUNT_GATEWAY; public $entityType = ENTITY_ACCOUNT_GATEWAY;
public function columns() public function columns()
@ -15,10 +22,14 @@ class AccountGatewayDatatable extends EntityDatatable
function ($model) { function ($model) {
if ($model->deleted_at) { if ($model->deleted_at) {
return $model->name; 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) { } elseif ($model->gateway_id != GATEWAY_WEPAY) {
return link_to("gateways/{$model->public_id}/edit", $model->name)->toHtml(); return link_to("gateways/{$model->public_id}/edit", $model->name)->toHtml();
} else { } else {
$accountGateway = AccountGateway::find($model->id); $accountGateway = $this->getAccountGateway($model->id);
$config = $accountGateway->getConfig(); $config = $accountGateway->getConfig();
$endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/'; $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/';
$wepayAccountId = $config->accountId; $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() public function actions()
{ {
return [ $actions = [
[ [
uctrans('texts.resend_confirmation_email'), uctrans('texts.resend_confirmation_email'),
function ($model) { function ($model) {
@ -78,7 +133,7 @@ class AccountGatewayDatatable extends EntityDatatable
], [ ], [
uctrans('texts.manage_account'), uctrans('texts.manage_account'),
function ($model) { 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/'; $endpoint = WEPAY_ENVIRONMENT == WEPAY_STAGE ? 'https://stage.wepay.com/' : 'https://www.wepay.com/';
return [ return [
'url' => $endpoint.'account/'.$accountGateway->getConfig()->accountId, '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 ?: '', 'payment' => $model->payment ?: '',
'credit' => $model->payment_amount ? Utils::formatMoney($model->credit, $model->currency_id, $model->country_id) : '', '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, '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); 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}"); return URL::to("quotes/create/{$model->public_id}");
}, },
function ($model) { 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', 'expense_date',
function ($model) { 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); return Utils::fromSqlDate($model->expense_date);
} }
@ -56,14 +56,16 @@ class ExpenseDatatable extends EntityDatatable
[ [
'amount', 'amount',
function ($model) { 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 // show both the amount and the converted amount
if ($model->exchange_rate != 1) { if ($model->exchange_rate != 1) {
$converted = round($model->amount * $model->exchange_rate, 2); $converted = round($amount * $model->exchange_rate, 2);
return Utils::formatMoney($model->amount, $model->expense_currency_id) . ' | ' . $str .= ' | ' . Utils::formatMoney($converted, $model->invoice_currency_id);
Utils::formatMoney($converted, $model->invoice_currency_id);
} else {
return Utils::formatMoney($model->amount, $model->expense_currency_id);
} }
return $str;
} }
], ],
[ [

View File

@ -16,7 +16,7 @@ class InvoiceDatatable extends EntityDatatable
[ [
'invoice_number', 'invoice_number',
function ($model) use ($entityType) { 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; return $model->invoice_number;
} }
@ -186,7 +186,7 @@ class InvoiceDatatable extends EntityDatatable
$class = 'success'; $class = 'success';
break; break;
} }
return "<h4><div class=\"label label-{$class}\">$label</div></h4>"; return "<h4><div class=\"label label-{$class}\">$label</div></h4>";
} }

View File

@ -21,7 +21,7 @@ class PaymentDatatable extends EntityDatatable
[ [
'invoice_number', 'invoice_number',
function ($model) { 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; return $model->invoice_number;
} }
@ -89,7 +89,11 @@ class PaymentDatatable extends EntityDatatable
[ [
'payment_date', 'payment_date',
function ($model) { 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}')"; return "javascript:showRefundModal({$model->public_id}, '{$max_refund}', '{$formatted}', '{$symbol}')";
}, },
function ($model) { function ($model) {
return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id]) && $model->payment_status_id >= PAYMENT_STATUS_COMPLETED && return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id])
$model->refunded < $model->amount && && $model->payment_status_id >= PAYMENT_STATUS_COMPLETED
( && $model->refunded < $model->amount
($model->transaction_reference && in_array($model->gateway_id , static::$refundableGateways)) && $model->transaction_reference
|| $model->payment_type_id == PAYMENT_TYPE_CREDIT && in_array($model->gateway_id , static::$refundableGateways);
);
} }
] ]
]; ];

View File

@ -58,7 +58,17 @@ class RecurringInvoiceDatatable extends EntityDatatable
function ($model) { function ($model) {
return Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]); 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', 'created_at',
function ($model) { 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(); return link_to("tasks/{$model->public_id}/edit", Task::calcStartTime($model))->toHtml();
} }
], ],

View File

@ -182,10 +182,21 @@ class BaseTransformer extends TransformerAbstract
* @param $name * @param $name
* @return null * @return null
*/ */
protected function getVendorId($name) public function getVendorId($name)
{ {
$name = strtolower($name); $name = strtolower($name);
return isset($this->maps[ENTITY_VENDOR][$name]) ? $this->maps[ENTITY_VENDOR][$name] : null; 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'), 'postal_code' => $this->getString($data, 'postal_code'),
'private_notes' => $this->getString($data, 'notes'), 'private_notes' => $this->getString($data, 'notes'),
'website' => $this->getString($data, 'website'), 'website' => $this->getString($data, 'website'),
'vat_number' => $this->getString($data, 'vat_number'),
'contacts' => [ 'contacts' => [
[ [
'first_name' => $this->getString($data, 'first_name'), '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; <?php namespace App\Ninja\Mailers;
use Utils;
use Exception; use Exception;
use Mail; use Mail;
use App\Models\Invoice; use App\Models\Invoice;
@ -29,7 +30,7 @@ class Mailer
if (stristr($toEmail, '@example.com')) { if (stristr($toEmail, '@example.com')) {
return true; return true;
} }
if (isset($_ENV['POSTMARK_API_TOKEN'])) { if (isset($_ENV['POSTMARK_API_TOKEN'])) {
$views = 'emails.'.$view.'_html'; $views = 'emails.'.$view.'_html';
} else { } else {
@ -55,7 +56,7 @@ class Mailer
if (!empty($data['pdfString']) && !empty($data['pdfFileName'])) { if (!empty($data['pdfString']) && !empty($data['pdfFileName'])) {
$message->attachData($data['pdfString'], $data['pdfFileName']); $message->attachData($data['pdfString'], $data['pdfFileName']);
} }
// Attach documents to the email // Attach documents to the email
if(!empty($data['documents'])){ if(!empty($data['documents'])){
foreach($data['documents'] as $document){ foreach($data['documents'] as $document){
@ -90,7 +91,7 @@ class Mailer
$invoice->markInvitationSent($invitation, $messageId); $invoice->markInvitationSent($invitation, $messageId);
} }
return true; return true;
} }
@ -107,11 +108,13 @@ class Mailer
} else { } else {
$emailError = $exception->getMessage(); $emailError = $exception->getMessage();
} }
if (isset($data['invitation'])) { if (isset($data['invitation'])) {
$invitation = $data['invitation']; $invitation = $data['invitation'];
$invitation->email_error = $emailError; $invitation->email_error = $emailError;
$invitation->save(); $invitation->save();
} elseif ( ! Utils::isNinja()) {
Utils::logError(Utils::getErrorString($exception));
} }
return $emailError; return $emailError;

View File

@ -2,16 +2,19 @@
use URL; use URL;
use Session; use Session;
use Utils;
use Request; use Request;
use Omnipay; use Omnipay;
use Exception; use Exception;
use CreditCard; use CreditCard;
use DateTime; use DateTime;
use App\Models\AccountGatewayToken; use App\Models\AccountGatewayToken;
use App\Models\AccountGatewaySettings;
use App\Models\Account; use App\Models\Account;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentMethod; use App\Models\PaymentMethod;
use App\Models\Country; use App\Models\Country;
use App\Models\GatewayType;
class BasePaymentDriver class BasePaymentDriver
{ {
@ -119,6 +122,12 @@ class BasePaymentDriver
$gateway = $this->accountGateway->gateway; $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 ($this->isGatewayType(GATEWAY_TYPE_TOKEN) || $gateway->is_offsite) {
if (Session::has('error')) { if (Session::has('error')) {
Session::reflash(); Session::reflash();
@ -158,12 +167,14 @@ class BasePaymentDriver
// check if a custom view exists for this provider // check if a custom view exists for this provider
protected function paymentView() 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)) { if (file_exists($file)) {
return sprintf('payments.%s/%s', $this->providerName(), $this->gatewayType); return sprintf('payments.%s/%s', $this->providerName(), $gatewayTypeAlias);
} else { } else {
return sprintf('payments.%s', $this->gatewayType); return sprintf('payments.%s', $gatewayTypeAlias);
} }
} }
@ -242,8 +253,22 @@ class BasePaymentDriver
->wherePublicId($this->sourceId) ->wherePublicId($this->sourceId)
->firstOrFail(); ->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()) { if ($this->isTwoStep()) {
@ -323,7 +348,8 @@ class BasePaymentDriver
protected function paymentDetails($paymentMethod = false) protected function paymentDetails($paymentMethod = false)
{ {
$invoice = $this->invoice(); $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 = [ $data = [
'amount' => $invoice->getRequestedAmount(), 'amount' => $invoice->getRequestedAmount(),
@ -760,6 +786,10 @@ class BasePaymentDriver
continue; continue;
} }
if ( ! $this->meetsGatewayTypeLimits($paymentMethod->payment_type->gateway_type_id)) {
continue;
}
$url = URL::to("/payment/{$this->invitation->invitation_key}/token/".$paymentMethod->public_id); $url = URL::to("/payment/{$this->invitation->invitation_key}/token/".$paymentMethod->public_id);
if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) { if ($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) {
@ -787,27 +817,68 @@ class BasePaymentDriver
{ {
$links = []; $links = [];
foreach ($this->gatewayTypes() as $gatewayType) { foreach ($this->gatewayTypes() as $gatewayTypeId) {
if ($gatewayType === GATEWAY_TYPE_TOKEN) { if ($gatewayTypeId === GATEWAY_TYPE_TOKEN) {
continue; 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[] = [ $links[] = [
'url' => $this->paymentUrl($gatewayType), 'gatewayTypeId' => $gatewayTypeId,
'label' => trans("texts.{$gatewayType}") 'url' => $url,
'label' => $label,
]; ];
} }
return $links; 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(); $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 // 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'; $url .= '#braintree_paypal';
if ($account->iframe_url) { 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) protected function creatingPaymentMethod($paymentMethod)
{ {
$data = $this->tokenResponse; $data = $this->tokenResponse;
$source = false;
if (!empty($data['object']) && ($data['object'] == 'card' || $data['object'] == 'bank_account')) { if (!empty($data['object']) && ($data['object'] == 'card' || $data['object'] == 'bank_account')) {
$source = $data; $source = $data;
} elseif (!empty($data['object']) && $data['object'] == 'customer') { } elseif (!empty($data['object']) && $data['object'] == 'customer') {
$sources = !empty($data['sources']) ? $data['sources'] : $data['cards']; $sources = !empty($data['sources']) ? $data['sources'] : $data['cards'];
$source = reset($sources['data']); $source = reset($sources['data']);
} else { } elseif (!empty($data['source'])) {
$source = !empty($data['source']) ? $data['source'] : $data['card']; $source = $data['source'];
} elseif (!empty($data['card'])) {
$source = $data['card'];
} }
if ( ! $source) { if ( ! $source) {

View File

@ -23,22 +23,4 @@ class ClientPresenter extends EntityPresenter {
return $account->formatMoney($client->paid_to_date, $client); 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); 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 * @return mixed
*/ */

View File

@ -24,12 +24,4 @@ class ExpensePresenter extends EntityPresenter
return Utils::fromSqlDate($this->entity->expense_date); 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(); return $this->entity->user->getDisplayName();
} }
public function description()
{
return substr($this->entity->description, 0, 40) . (strlen($this->entity->description) > 40 ? '...' : '');
}
/** /**
* @param $account * @param $account
* @return mixed * @return mixed

View File

@ -2,6 +2,7 @@
use Auth; use Auth;
use Request; use Request;
use Input;
use Session; use Session;
use Utils; use Utils;
use URL; use URL;
@ -27,6 +28,11 @@ class AccountRepository
public function create($firstName = '', $lastName = '', $email = '', $password = '') public function create($firstName = '', $lastName = '', $email = '', $password = '')
{ {
$company = new Company(); $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(); $company->save();
$account = new Account(); $account = new Account();

View File

@ -74,6 +74,8 @@ class ActivityRepository
->leftJoin('invoices', 'invoices.id', '=', 'activities.invoice_id') ->leftJoin('invoices', 'invoices.id', '=', 'activities.invoice_id')
->leftJoin('payments', 'payments.id', '=', 'activities.payment_id') ->leftJoin('payments', 'payments.id', '=', 'activities.payment_id')
->leftJoin('credits', 'credits.id', '=', 'activities.credit_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('clients.id', '=', $clientId)
->where('contacts.is_primary', '=', 1) ->where('contacts.is_primary', '=', 1)
->whereNull('contacts.deleted_at') ->whereNull('contacts.deleted_at')
@ -102,7 +104,11 @@ class ActivityRepository
'contacts.email as email', 'contacts.email as email',
'payments.transaction_reference as payment', 'payments.transaction_reference as payment',
'payments.amount as payment_amount', '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 * @return null
*/ */
public function getClassName() public function getClassName()
{ {
return null; return null;
} }
@ -40,7 +40,7 @@ class BaseRepository
if ($entity->trashed()) { if ($entity->trashed()) {
return; return;
} }
$entity->delete(); $entity->delete();
$className = $this->getEventClass($entity, 'Archived'); $className = $this->getEventClass($entity, 'Archived');
@ -83,7 +83,7 @@ class BaseRepository
if ($entity->is_deleted) { if ($entity->is_deleted) {
return; return;
} }
$entity->is_deleted = true; $entity->is_deleted = true;
$entity->save(); $entity->save();

View File

@ -31,6 +31,7 @@ class ClientRepository extends BaseRepository
->where('clients.account_id', '=', \Auth::user()->account_id) ->where('clients.account_id', '=', \Auth::user()->account_id)
->where('contacts.is_primary', '=', true) ->where('contacts.is_primary', '=', true)
->where('contacts.deleted_at', '=', null) ->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( ->select(
DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
@ -78,7 +79,10 @@ class ClientRepository extends BaseRepository
$client = Client::createNew(); $client = Client::createNew();
} else { } else {
$client = Client::scope($publicId)->with('contacts')->firstOrFail(); $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 // convert currency code to id
@ -107,7 +111,11 @@ class ClientRepository extends BaseRepository
// If the primary is set ensure it's listed first // If the primary is set ensure it's listed first
usort($contacts, function ($left, $right) { 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) { foreach ($contacts as $contact) {

View File

@ -58,10 +58,36 @@ class CreditRepository extends BaseRepository
return $query; 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) public function save($input, $credit = null)
{ {
$publicId = isset($data['public_id']) ? $data['public_id'] : false; $publicId = isset($data['public_id']) ? $data['public_id'] : false;
if ($credit) { if ($credit) {
// do nothing // do nothing
} elseif ($publicId) { } elseif ($publicId) {

View File

@ -39,13 +39,15 @@ class DashboardRepository
$data = []; $data = [];
$count = 0; $count = 0;
$balance = 0;
$records = $this->rawChartData($entityType, $account, $groupBy, $startDate, $endDate, $currencyId); $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; $data[$item->$groupBy] = $item->total;
$count += $item->count; $count += $item->count;
}, $records->get()); $balance += isset($item->balance) ? $item->balance : 0;
}, $records);
$padding = $groupBy == 'DAYOFYEAR' ? 'day' : ($groupBy == 'WEEK' ? 'week' : 'month'); $padding = $groupBy == 'DAYOFYEAR' ? 'day' : ($groupBy == 'WEEK' ? 'week' : 'month');
$endDate->modify('+1 '.$padding); $endDate->modify('+1 '.$padding);
$interval = new DateInterval('P1'.substr($groupBy, 0, 1)); $interval = new DateInterval('P1'.substr($groupBy, 0, 1));
@ -84,9 +86,9 @@ class DashboardRepository
if ($entityType == ENTITY_INVOICE) { if ($entityType == ENTITY_INVOICE) {
$totals->invoices = array_sum($data); $totals->invoices = array_sum($data);
$totals->average = $count ? round($totals->invoices / $count, 2) : 0; $totals->average = $count ? round($totals->invoices / $count, 2) : 0;
$totals->balance = $balance;
} elseif ($entityType == ENTITY_PAYMENT) { } elseif ($entityType == ENTITY_PAYMENT) {
$totals->revenue = array_sum($data); $totals->revenue = array_sum($data);
$totals->balance = $totals->invoices - $totals->revenue;
} elseif ($entityType == ENTITY_EXPENSE) { } elseif ($entityType == ENTITY_EXPENSE) {
//$totals->profit = $totals->revenue - array_sum($data); //$totals->profit = $totals->revenue - array_sum($data);
$totals->expenses = array_sum($data); $totals->expenses = array_sum($data);
@ -106,6 +108,10 @@ class DashboardRepository
private function rawChartData($entityType, $account, $groupBy, $startDate, $endDate, $currencyId) private function rawChartData($entityType, $account, $groupBy, $startDate, $endDate, $currencyId)
{ {
if ( ! in_array($groupBy, ['DAYOFYEAR', 'WEEK', 'MONTH'])) {
return [];
}
$accountId = $account->id; $accountId = $account->id;
$currencyId = intval($currencyId); $currencyId = intval($currencyId);
$timeframe = 'concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date))'; $timeframe = 'concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date))';
@ -128,7 +134,7 @@ class DashboardRepository
} }
if ($entityType == ENTITY_INVOICE) { 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('invoice_type_id', '=', INVOICE_TYPE_STANDARD)
->where('is_recurring', '=', false); ->where('is_recurring', '=', false);
} elseif ($entityType == ENTITY_PAYMENT) { } elseif ($entityType == ENTITY_PAYMENT) {
@ -137,10 +143,10 @@ class DashboardRepository
->where('invoices.is_deleted', '=', false) ->where('invoices.is_deleted', '=', false)
->whereNotIn('payment_status_id', [PAYMENT_STATUS_VOIDED, PAYMENT_STATUS_FAILED]); ->whereNotIn('payment_status_id', [PAYMENT_STATUS_VOIDED, PAYMENT_STATUS_FAILED]);
} elseif ($entityType == ENTITY_EXPENSE) { } 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) public function totals($accountId, $userId, $viewAll)
@ -175,29 +181,39 @@ class DashboardRepository
return $metrics->groupBy('accounts.id')->first(); return $metrics->groupBy('accounts.id')->first();
} }
public function paidToDate($accountId, $userId, $viewAll) public function paidToDate($account, $userId, $viewAll)
{ {
$accountId = $account->id;
$select = DB::raw( $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' .DB::getQueryGrammar()->wrap('clients.currency_id', true).' as currency_id'
); );
$paidToDate = DB::table('accounts') $paidToDate = DB::table('payments')
->select($select) ->select($select)
->leftJoin('clients', 'accounts.id', '=', 'clients.account_id') ->leftJoin('invoices', 'invoices.id', '=', 'payments.invoice_id')
->where('accounts.id', '=', $accountId) ->leftJoin('clients', 'clients.id', '=', 'invoices.client_id')
->where('clients.is_deleted', '=', false); ->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){ if (!$viewAll){
$paidToDate = $paidToDate->where('clients.user_id', '=', $userId); $paidToDate->where('invoices.user_id', '=', $userId);
} }
return $paidToDate->groupBy('accounts.id') if ($account->financial_year_start) {
->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')) $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(); ->get();
} }
public function averages($accountId, $userId, $viewAll) public function averages($account, $userId, $viewAll)
{ {
$accountId = $account->id;
$select = DB::raw( $select = DB::raw(
'AVG('.DB::getQueryGrammar()->wrap('invoices.amount', true).') as invoice_avg, ' 'AVG('.DB::getQueryGrammar()->wrap('invoices.amount', true).') as invoice_avg, '
.DB::getQueryGrammar()->wrap('clients.currency_id', true).' as currency_id' .DB::getQueryGrammar()->wrap('clients.currency_id', true).' as currency_id'
@ -213,7 +229,12 @@ class DashboardRepository
->where('invoices.is_recurring', '=', false); ->where('invoices.is_recurring', '=', false);
if (!$viewAll){ 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') return $averageInvoice->groupBy('accounts.id')
@ -252,7 +273,7 @@ class DashboardRepository
} }
return $activities->orderBy('activities.created_at', 'desc') 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) ->take(50)
->get(); ->get();
} }
@ -335,8 +356,12 @@ class DashboardRepository
public function expenses($accountId, $userId, $viewAll) 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( $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' .DB::getQueryGrammar()->wrap('expenses.expense_currency_id', true).' as currency_id'
); );
$paidToDate = DB::table('accounts') $paidToDate = DB::table('accounts')

View File

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

View File

@ -76,6 +76,8 @@ class ExpenseRepository extends BaseRepository
'expenses.expense_currency_id', 'expenses.expense_currency_id',
'expenses.invoice_currency_id', 'expenses.invoice_currency_id',
'expenses.user_id', 'expenses.user_id',
'expenses.tax_rate1',
'expenses.tax_rate2',
'expense_categories.name as category', 'expense_categories.name as category',
'invoices.public_id as invoice_public_id', 'invoices.public_id as invoice_public_id',
'invoices.user_id as invoice_user_id', 'invoices.user_id as invoice_user_id',
@ -118,20 +120,24 @@ class ExpenseRepository extends BaseRepository
// do nothing // do nothing
} elseif ($publicId) { } elseif ($publicId) {
$expense = Expense::scope($publicId)->firstOrFail(); $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 { } else {
$expense = Expense::createNew(); $expense = Expense::createNew();
} }
if ($expense->is_deleted) {
return $expense;
}
// First auto fill // First auto fill
$expense->fill($input); $expense->fill($input);
$expense->expense_date = Utils::toSqlDate($input['expense_date']); if (isset($input['expense_date'])) {
$expense->expense_date = Utils::toSqlDate($input['expense_date']);
if (isset($input['private_notes'])) {
$expense->private_notes = trim($input['private_notes']);
} }
$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; $expense->should_be_invoiced = isset($input['should_be_invoiced']) && floatval($input['should_be_invoiced']) || $expense->client_id ? true : false;
if ( ! $expense->expense_currency_id) { if ( ! $expense->expense_currency_id) {
@ -143,7 +149,9 @@ class ExpenseRepository extends BaseRepository
$rate = isset($input['exchange_rate']) ? Utils::parseFloat($input['exchange_rate']) : 1; $rate = isset($input['exchange_rate']) ? Utils::parseFloat($input['exchange_rate']) : 1;
$expense->exchange_rate = round($rate, 4); $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(); $expense->save();
@ -173,27 +181,4 @@ class ExpenseRepository extends BaseRepository
return $expense; 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('contacts.deleted_at', '=', null)
->where('invoices.is_recurring', '=', false) ->where('invoices.is_recurring', '=', false)
->where('contacts.is_primary', '=', true) ->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( ->select(
DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'), DB::raw('COALESCE(clients.currency_id, accounts.currency_id) currency_id'),
DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'), DB::raw('COALESCE(clients.country_id, accounts.country_id) country_id'),
@ -280,7 +281,13 @@ class InvoiceRepository extends BaseRepository
} }
} else { } else {
$invoice = Invoice::scope($publicId)->firstOrFail(); $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); $invoice->fill($data);
@ -306,9 +313,6 @@ class InvoiceRepository extends BaseRepository
if (isset($data['is_amount_discount'])) { if (isset($data['is_amount_discount'])) {
$invoice->is_amount_discount = $data['is_amount_discount'] ? true : false; $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'])) { if (isset($data['invoice_date_sql'])) {
$invoice->invoice_date = $data['invoice_date_sql']; $invoice->invoice_date = $data['invoice_date_sql'];
} elseif (isset($data['invoice_date'])) { } elseif (isset($data['invoice_date'])) {
@ -477,6 +481,10 @@ class InvoiceRepository extends BaseRepository
$invoice->balance = $total; $invoice->balance = $total;
} }
if (isset($data['partial'])) {
$invoice->partial = max(0,min(round(Utils::parseFloat($data['partial']), 2), $invoice->balance));
}
$invoice->amount = $total; $invoice->amount = $total;
$invoice->save(); $invoice->save();
@ -653,6 +661,9 @@ class InvoiceRepository extends BaseRepository
if ($quotePublicId) { if ($quotePublicId) {
$clone->invoice_type_id = INVOICE_TYPE_STANDARD; $clone->invoice_type_id = INVOICE_TYPE_STANDARD;
$clone->quote_id = $quotePublicId; $clone->quote_id = $quotePublicId;
if ($account->invoice_terms) {
$clone->terms = $account->invoice_terms;
}
} }
$clone->save(); $clone->save();

View File

@ -150,11 +150,17 @@ class PaymentRepository extends BaseRepository
// do nothing // do nothing
} elseif ($publicId) { } elseif ($publicId) {
$payment = Payment::scope($publicId)->firstOrFail(); $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 { } else {
$payment = Payment::createNew(); $payment = Payment::createNew();
} }
if ($payment->is_deleted) {
return $payment;
}
$paymentTypeId = false; $paymentTypeId = false;
if (isset($input['payment_type_id'])) { if (isset($input['payment_type_id'])) {
$paymentTypeId = $input['payment_type_id'] ? $input['payment_type_id'] : null; $paymentTypeId = $input['payment_type_id'] ? $input['payment_type_id'] : null;

View File

@ -17,15 +17,14 @@ class ProductRepository extends BaseRepository
->get(); ->get();
} }
public function find($accountId) public function find($accountId, $filter = null)
{ {
return DB::table('products') $query = DB::table('products')
->leftJoin('tax_rates', function($join) { ->leftJoin('tax_rates', function($join) {
$join->on('tax_rates.id', '=', 'products.default_tax_rate_id') $join->on('tax_rates.id', '=', 'products.default_tax_rate_id')
->whereNull('tax_rates.deleted_at'); ->whereNull('tax_rates.deleted_at');
}) })
->where('products.account_id', '=', $accountId) ->where('products.account_id', '=', $accountId)
->where('products.deleted_at', '=', null)
->select( ->select(
'products.public_id', 'products.public_id',
'products.product_key', 'products.product_key',
@ -35,6 +34,19 @@ class ProductRepository extends BaseRepository
'tax_rates.rate as tax_rate', 'tax_rates.rate as tax_rate',
'products.deleted_at' '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) public function save($data, $product = null)

View File

@ -5,8 +5,13 @@ use Session;
use App\Models\Client; use App\Models\Client;
use App\Models\Task; use App\Models\Task;
class TaskRepository class TaskRepository extends BaseRepository
{ {
public function getClassName()
{
return 'App\Models\Task';
}
public function find($clientPublicId = null, $filter = null) public function find($clientPublicId = null, $filter = null)
{ {
$query = \DB::table('tasks') $query = \DB::table('tasks')
@ -67,12 +72,15 @@ class TaskRepository
if ($task) { if ($task) {
// do nothing // do nothing
} elseif ($publicId) { } elseif ($publicId) {
$task = Task::scope($publicId)->firstOrFail(); $task = Task::scope($publicId)->withTrashed()->firstOrFail();
\Log::warning('Entity not set in task repo save');
} else { } else {
$task = Task::createNew(); $task = Task::createNew();
} }
if ($task->is_deleted) {
return $task;
}
if (isset($data['client']) && $data['client']) { if (isset($data['client']) && $data['client']) {
$task->client_id = Client::getPrivateId($data['client']); $task->client_id = Client::getPrivateId($data['client']);
} }
@ -109,26 +117,4 @@ class TaskRepository
return $task; 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; <?php namespace App\Ninja\Repositories;
use Utils;
use DB; use DB;
use App\Models\Vendor; use App\Models\Vendor;
@ -70,7 +71,13 @@ class VendorRepository extends BaseRepository
$vendor = Vendor::createNew(); $vendor = Vendor::createNew();
} else { } else {
$vendor = Vendor::scope($publicId)->with('vendor_contacts')->firstOrFail(); $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); $vendor->fill($data);
@ -78,12 +85,22 @@ class VendorRepository extends BaseRepository
$first = true; $first = true;
$vendorcontacts = isset($data['vendor_contact']) ? [$data['vendor_contact']] : $data['vendor_contacts']; $vendorcontacts = isset($data['vendor_contact']) ? [$data['vendor_contact']] : $data['vendor_contacts'];
$vendorcontactIds = [];
foreach ($vendorcontacts as $vendorcontact) { foreach ($vendorcontacts as $vendorcontact) {
$vendorcontact = $vendor->addVendorContact($vendorcontact, $first); $vendorcontact = $vendor->addVendorContact($vendorcontact, $first);
$vendorcontactIds[] = $vendorcontact->public_id;
$first = false; $first = false;
} }
if ( ! $vendor->wasRecentlyCreated) {
foreach ($vendor->vendor_contacts as $contact) {
if (!in_array($contact->public_id, $vendorcontactIds)) {
$contact->delete();
}
}
}
return $vendor; return $vendor;
} }
} }

View File

@ -16,7 +16,7 @@ class ActivityTransformer extends EntityTransformer
protected $availableIncludes = [ ]; protected $availableIncludes = [ ];
/** /**
* @param Client $client * @param Activity $activity
* @return array * @return array
*/ */
public function transform(Activity $activity) public function transform(Activity $activity)
@ -29,7 +29,10 @@ class ActivityTransformer extends EntityTransformer
'invoice_id' => $activity->invoice ? $activity->invoice->public_id : null, 'invoice_id' => $activity->invoice ? $activity->invoice->public_id : null,
'payment_id' => $activity->payment ? $activity->payment->public_id : null, 'payment_id' => $activity->payment ? $activity->payment->public_id : null,
'credit_id' => $activity->credit ? $activity->credit->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, 'send_invoice' => (bool) $contact->send_invoice,
]); ]);
} }
} }

View File

@ -23,11 +23,16 @@ class ExpenseTransformer extends EntityTransformer
'transaction_id' => $expense->transaction_id, 'transaction_id' => $expense->transaction_id,
'bank_id' => $expense->bank_id, 'bank_id' => $expense->bank_id,
'expense_currency_id' => (int) $expense->expense_currency_id, 'expense_currency_id' => (int) $expense->expense_currency_id,
'expense_category_id' => (int) $expense->expense_category_id,
'amount' => (float) $expense->amount, 'amount' => (float) $expense->amount,
'expense_date' => $expense->expense_date, 'expense_date' => $expense->expense_date,
'exchange_rate' => (float) $expense->exchange_rate, 'exchange_rate' => (float) $expense->exchange_rate,
'invoice_currency_id' => (int) $expense->invoice_currency_id, 'invoice_currency_id' => (int) $expense->invoice_currency_id,
'is_deleted' => (bool) $expense->is_deleted, '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), '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, '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, '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; protected $tokenName;
public function __construct(Account $account, $serializer, $tokenName) public function __construct(Account $account, $serializer, $tokenName)
{ {
parent::__construct($account, $serializer); parent::__construct($account, $serializer);
@ -31,8 +31,9 @@ class UserAccountTransformer extends EntityTransformer
'name' => $user->account->present()->name, 'name' => $user->account->present()->name,
'token' => $user->account->getToken($user->id, $this->tokenName), 'token' => $user->account->getToken($user->id, $this->tokenName),
'default_url' => SITE_URL, 'default_url' => SITE_URL,
'plan' => $user->account->company->plan,
'logo' => $user->account->logo, 'logo' => $user->account->logo,
'logo_url' => $user->account->getLogoURL(), 'logo_url' => $user->account->getLogoURL(),
]; ];
} }
} }

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