mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-06-04 05:44:36 -04:00
Merge branch 'release-4.5.0'
This commit is contained in:
commit
56f13a019e
@ -22,3 +22,4 @@ MAIL_DRIVER=log
|
||||
TRAVIS=true
|
||||
API_SECRET=password
|
||||
TEST_USERNAME=user@example.com
|
||||
TEST_PERMISSIONS_USERNAME=permissions@example.com
|
||||
|
@ -69,6 +69,7 @@ before_script:
|
||||
- php artisan db:seed --no-interaction --class=UserTableSeeder # development seed
|
||||
- sed -i 's/DB_TYPE=db-ninja-1/DB_TYPE=db-ninja-2/g' .env
|
||||
- sed -i 's/user@example.com/user2@example.com/g' .env
|
||||
- sed -i 's/permissions@example.com/permissions2@example.com/g' .env
|
||||
- php artisan db:seed --no-interaction --class=UserTableSeeder # development seed
|
||||
|
||||
script:
|
||||
@ -84,12 +85,13 @@ script:
|
||||
- php ./vendor/codeception/codeception/codecept run --debug acceptance OnlinePaymentCest.php
|
||||
- php ./vendor/codeception/codeception/codecept run --debug acceptance PaymentCest.php
|
||||
- php ./vendor/codeception/codeception/codecept run --debug acceptance TaskCest.php
|
||||
- php ./vendor/codeception/codeception/codecept run --debug acceptance GatewayFeesCest.php
|
||||
- php ./vendor/codeception/codeception/codecept run --debug acceptance DiscountCest.php
|
||||
- php ./vendor/codeception/codeception/codecept run --debug acceptance AllPagesCept.php
|
||||
- php ./vendor/codeception/codeception/codecept run --debug functional PermissionsCest.php
|
||||
|
||||
#- sed -i 's/NINJA_DEV=true/NINJA_PROD=true/g' .env
|
||||
#- php ./vendor/codeception/codeception/codecept run acceptance GoProCest.php
|
||||
#- php ./vendor/codeception/codeception/codecept run --debug run acceptance GoProCest.php
|
||||
#- php ./vendor/codeception/codeception/codecept run --debug acceptance GatewayFeesCest.php
|
||||
#- php ./vendor/codeception/codeception/codecept run --debug acceptance DiscountCest.php
|
||||
|
||||
after_script:
|
||||
- php artisan ninja:check-data --no-interaction --database='db-ninja-1'
|
||||
@ -105,6 +107,7 @@ after_script:
|
||||
- mysql -u root -e 'select * from lookup_invitations;' ninja0
|
||||
- mysql -u root -e 'select * from accounts;' ninja
|
||||
- mysql -u root -e 'select * from users;' ninja
|
||||
- mysql -u root -e 'select * from lookup_users;' ninja
|
||||
- mysql -u root -e 'select * from account_gateways;' ninja
|
||||
- mysql -u root -e 'select * from clients;' ninja
|
||||
- mysql -u root -e 'select * from contacts;' ninja
|
||||
|
@ -86,6 +86,7 @@ class CalculatePayouts extends Command
|
||||
$client = $payment->client;
|
||||
|
||||
$this->info("User: $user");
|
||||
$this->info("Client: " . $client->getDisplayName());
|
||||
|
||||
foreach ($client->payments as $payment) {
|
||||
$amount = $payment->getCompletedAmount();
|
||||
|
@ -87,6 +87,7 @@ class CheckData extends Command
|
||||
$this->checkClientBalances();
|
||||
$this->checkContacts();
|
||||
$this->checkUserAccounts();
|
||||
$this->checkLogoFiles();
|
||||
|
||||
if (! $this->option('client_id')) {
|
||||
$this->checkOAuth();
|
||||
@ -840,6 +841,30 @@ class CheckData extends Command
|
||||
}
|
||||
}
|
||||
|
||||
private function checkLogoFiles()
|
||||
{
|
||||
$accounts = DB::table('accounts')
|
||||
->where('logo', '!=', '')
|
||||
->orderBy('id')
|
||||
->get(['logo']);
|
||||
|
||||
$countMissing = 0;
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
$path = public_path('logo/' . $account->logo);
|
||||
if (! file_exists($path)) {
|
||||
$this->logMessage('Missing file: ' . $account->logo);
|
||||
$countMissing++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($countMissing > 0) {
|
||||
$this->isValid = false;
|
||||
}
|
||||
|
||||
$this->logMessage($countMissing . ' missing logo files');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
|
@ -4,6 +4,7 @@ namespace App\Console\Commands;
|
||||
|
||||
use Artisan;
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
|
||||
class MakeModule extends Command
|
||||
{
|
||||
@ -12,7 +13,7 @@ class MakeModule extends Command
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'ninja:make-module {name} {fields?} {--migrate=}';
|
||||
protected $signature = 'ninja:make-module {name : Module name} {fields? : Model fields} {--migrate : Run module migrations} {--p|--plain : Generate only base module scaffold}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
@ -41,6 +42,7 @@ class MakeModule extends Command
|
||||
$name = $this->argument('name');
|
||||
$fields = $this->argument('fields');
|
||||
$migrate = $this->option('migrate');
|
||||
$plain = $this->option('plain');
|
||||
$lower = strtolower($name);
|
||||
|
||||
// convert 'name:string,description:text' to 'name,description'
|
||||
@ -50,34 +52,85 @@ class MakeModule extends Command
|
||||
}, $fillable);
|
||||
$fillable = implode(',', $fillable);
|
||||
|
||||
ProgressBar::setFormatDefinition('custom', '%current%/%max% %elapsed:6s% [%bar%] %percent:3s%% %message%');
|
||||
$progressBar = $this->output->createProgressBar($plain ? 2 : ($migrate ? 15 : 14));
|
||||
$progressBar->setFormat('custom');
|
||||
|
||||
$this->info("Creating module: {$name}...");
|
||||
|
||||
$progressBar->setMessage("Starting module creation...");
|
||||
Artisan::call('module:make', ['name' => [$name]]);
|
||||
Artisan::call('module:make-migration', ['name' => "create_{$lower}_table", '--fields' => $fields, 'module' => $name]);
|
||||
Artisan::call('module:make-model', ['model' => $name, 'module' => $name, '--fillable' => $fillable]);
|
||||
$progressBar->advance();
|
||||
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'views', '--fields' => $fields, '--filename' => 'edit.blade']);
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'datatable', '--fields' => $fields]);
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'repository', '--fields' => $fields]);
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'policy']);
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'auth-provider']);
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'presenter']);
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'request']);
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'request', 'prefix' => 'create']);
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'request', 'prefix' => 'update']);
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'api-controller']);
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'transformer', '--fields' => $fields]);
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'lang', '--filename' => 'texts']);
|
||||
if (! $plain) {
|
||||
$progressBar->setMessage("Creating migrations...");
|
||||
Artisan::call('module:make-migration', ['name' => "create_{$lower}_table", '--fields' => $fields, 'module' => $name]);
|
||||
$progressBar->advance();
|
||||
|
||||
if ($migrate == 'true') {
|
||||
Artisan::call('module:migrate', ['module' => $name]);
|
||||
} else {
|
||||
$this->info("Use the following command to run the migrations:\nphp artisan module:migrate $name");
|
||||
$progressBar->setMessage("Creating models...");
|
||||
Artisan::call('module:make-model', ['model' => $name, 'module' => $name, '--fillable' => $fillable]);
|
||||
$progressBar->advance();
|
||||
|
||||
$progressBar->setMessage("Creating views...");
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'views', '--fields' => $fields, '--filename' => 'edit.blade']);
|
||||
$progressBar->advance();
|
||||
|
||||
$progressBar->setMessage("Creating datatables...");
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'datatable', '--fields' => $fields]);
|
||||
$progressBar->advance();
|
||||
|
||||
$progressBar->setMessage("Creating repositories...");
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'repository', '--fields' => $fields]);
|
||||
$progressBar->advance();
|
||||
|
||||
$progressBar->setMessage("Creating presenters...");
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'presenter']);
|
||||
$progressBar->advance();
|
||||
|
||||
$progressBar->setMessage("Creating requests...");
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'request']);
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'request', 'prefix' => 'create']);
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'request', 'prefix' => 'update']);
|
||||
$progressBar->advance();
|
||||
|
||||
$progressBar->setMessage("Creating api-controllers...");
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'api-controller']);
|
||||
$progressBar->advance();
|
||||
|
||||
$progressBar->setMessage("Creating transformers...");
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'transformer', '--fields' => $fields]);
|
||||
$progressBar->advance();
|
||||
|
||||
// if the migrate flag was specified, run the migrations
|
||||
if ($migrate) {
|
||||
$progressBar->setMessage("Running migrations...");
|
||||
Artisan::call('module:migrate', ['module' => $name]);
|
||||
$progressBar->advance();
|
||||
}
|
||||
}
|
||||
|
||||
$progressBar->setMessage("Creating policies...");
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'policy']);
|
||||
$progressBar->advance();
|
||||
|
||||
$progressBar->setMessage("Creating auth-providers...");
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'auth-provider']);
|
||||
$progressBar->advance();
|
||||
|
||||
$progressBar->setMessage("Creating translations...");
|
||||
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'lang', '--filename' => 'texts']);
|
||||
$progressBar->advance();
|
||||
|
||||
$progressBar->setMessage("Dumping module auto-load...");
|
||||
Artisan::call('module:dump');
|
||||
$progressBar->finish();
|
||||
$progressBar->clear();
|
||||
|
||||
$this->info('Done');
|
||||
|
||||
if (!$migrate && !$plain) {
|
||||
$this->info("==> Migrations were not run because the --migrate flag was not specified.");
|
||||
$this->info("==> Use the following command to run the migrations:\nphp artisan module:migrate $name");
|
||||
}
|
||||
}
|
||||
|
||||
protected function getArguments()
|
||||
@ -91,7 +144,8 @@ class MakeModule extends Command
|
||||
protected function getOptions()
|
||||
{
|
||||
return [
|
||||
['migrate', null, InputOption::VALUE_OPTIONAL, 'The model attributes.', null],
|
||||
['migrate', null, InputOption::VALUE_NONE, 'Run module migrations.', null],
|
||||
['plain', 'p', InputOption::VALUE_NONE, 'Generate only base module scaffold.', null],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,25 @@ if (! defined('APP_NAME')) {
|
||||
define('ENTITY_PROPOSAL_CATEGORY', 'proposal_category');
|
||||
define('ENTITY_PROPOSAL_INVITATION', 'proposal_invitation');
|
||||
|
||||
$permissionEntities = [
|
||||
ENTITY_CLIENT,
|
||||
//ENTITY_CONTACT,
|
||||
ENTITY_CREDIT,
|
||||
ENTITY_EXPENSE,
|
||||
ENTITY_INVOICE,
|
||||
ENTITY_PAYMENT,
|
||||
ENTITY_PRODUCT,
|
||||
ENTITY_PROJECT,
|
||||
ENTITY_PROPOSAL,
|
||||
ENTITY_QUOTE,
|
||||
'reports',
|
||||
ENTITY_TASK,
|
||||
ENTITY_VENDOR,
|
||||
ENTITY_RECURRING_INVOICE,
|
||||
];
|
||||
|
||||
define('PERMISSION_ENTITIES', json_encode($permissionEntities));
|
||||
|
||||
define('INVOICE_TYPE_STANDARD', 1);
|
||||
define('INVOICE_TYPE_QUOTE', 2);
|
||||
|
||||
@ -342,7 +361,7 @@ if (! defined('APP_NAME')) {
|
||||
define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com'));
|
||||
define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest'));
|
||||
define('NINJA_DATE', '2000-01-01');
|
||||
define('NINJA_VERSION', '4.4.4' . env('NINJA_VERSION_SUFFIX'));
|
||||
define('NINJA_VERSION', '4.5.0' . env('NINJA_VERSION_SUFFIX'));
|
||||
define('NINJA_TERMS_VERSION', '1.0.1');
|
||||
|
||||
define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'));
|
||||
@ -408,10 +427,11 @@ if (! defined('APP_NAME')) {
|
||||
define('NEW_VERSION_AVAILABLE', 'NEW_VERSION_AVAILABLE');
|
||||
|
||||
define('TEST_USERNAME', env('TEST_USERNAME', 'user@example.com'));
|
||||
define('TEST_PERMISSIONS_USERNAME', env('TEST_PERMISSIONS_USERNAME', 'permissions@example.com'));
|
||||
define('TEST_PASSWORD', 'password');
|
||||
define('API_SECRET', 'API_SECRET');
|
||||
define('DEFAULT_API_PAGE_SIZE', 15);
|
||||
define('MAX_API_PAGE_SIZE', 500);
|
||||
define('MAX_API_PAGE_SIZE', 5000);
|
||||
|
||||
define('IOS_DEVICE', env('IOS_DEVICE', ''));
|
||||
define('ANDROID_DEVICE', env('ANDROID_DEVICE', ''));
|
||||
|
@ -30,17 +30,4 @@ class Domain
|
||||
{
|
||||
return 'maildelivery@' . static::getDomainFromId($id);
|
||||
}
|
||||
|
||||
public static function getCookieDomain($url)
|
||||
{
|
||||
if (! Utils::isNinjaProd() || Utils::isReseller()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (strpos($url, '.services') !== false) {
|
||||
return '.invoice.services';
|
||||
} else {
|
||||
return '.invoiceninja.com';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
32
app/Events/ProductWasCreated.php
Normal file
32
app/Events/ProductWasCreated.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Product;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ProductWasCreated extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* @var Product
|
||||
*/
|
||||
public $product;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
**/
|
||||
public $input;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Product $product, $input = null)
|
||||
{
|
||||
$this->product = $product;
|
||||
$this->input = $input;
|
||||
}
|
||||
}
|
26
app/Events/ProductWasDeleted.php
Normal file
26
app/Events/ProductWasDeleted.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Product;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ProductWasDeleted extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* @var Product
|
||||
*/
|
||||
public $product;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Product $product)
|
||||
{
|
||||
$this->product = $product;
|
||||
}
|
||||
}
|
32
app/Events/ProductWasUpdated.php
Normal file
32
app/Events/ProductWasUpdated.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use App\Models\Product;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ProductWasUpdated extends Event
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* @var Product
|
||||
*/
|
||||
public $product;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
**/
|
||||
public $input;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Product $product, $input = null)
|
||||
{
|
||||
$this->product = $product;
|
||||
$this->input = $input;
|
||||
}
|
||||
}
|
@ -95,6 +95,14 @@ class AccountApiController extends BaseAPIController
|
||||
$transformer = new UserAccountTransformer($user->account, $request->serializer, $request->token_name);
|
||||
$data = $this->createCollection($users, $transformer, 'user_account');
|
||||
|
||||
if (request()->include_static) {
|
||||
$data = [
|
||||
'accounts' => $data,
|
||||
'static' => Utils::getStaticData(),
|
||||
'version' => NINJA_VERSION,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->response($data);
|
||||
}
|
||||
|
||||
@ -112,14 +120,7 @@ class AccountApiController extends BaseAPIController
|
||||
|
||||
public function getStaticData()
|
||||
{
|
||||
$data = [];
|
||||
|
||||
$cachedTables = unserialize(CACHED_TABLES);
|
||||
foreach ($cachedTables as $name => $class) {
|
||||
$data[$name] = Cache::get($name);
|
||||
}
|
||||
|
||||
return $this->response($data);
|
||||
return $this->response(Utils::getStaticData());
|
||||
}
|
||||
|
||||
public function getUserAccounts(Request $request)
|
||||
|
@ -1092,6 +1092,7 @@ class AccountController extends BaseController
|
||||
$user->notify_viewed = Input::get('notify_viewed');
|
||||
$user->notify_paid = Input::get('notify_paid');
|
||||
$user->notify_approved = Input::get('notify_approved');
|
||||
$user->only_notify_owned = Input::get('only_notify_owned');
|
||||
$user->slack_webhook_url = Input::get('slack_webhook_url');
|
||||
$user->save();
|
||||
|
||||
@ -1235,6 +1236,7 @@ class AccountController extends BaseController
|
||||
$user->notify_viewed = Input::get('notify_viewed');
|
||||
$user->notify_paid = Input::get('notify_paid');
|
||||
$user->notify_approved = Input::get('notify_approved');
|
||||
$user->only_notify_owned = Input::get('only_notify_owned');
|
||||
}
|
||||
|
||||
if ($user->google_2fa_secret && ! Input::get('enable_two_factor')) {
|
||||
|
@ -467,4 +467,9 @@ class AppController extends BaseController
|
||||
|
||||
return response(nl2br(Artisan::output()));
|
||||
}
|
||||
|
||||
public function redirect()
|
||||
{
|
||||
return redirect((Utils::isNinja() ? NINJA_WEB_URL : ''), 301);
|
||||
}
|
||||
}
|
||||
|
@ -99,6 +99,10 @@ class BaseAPIController extends Controller
|
||||
|
||||
$query->with($includes);
|
||||
|
||||
if (Input::get('filter_active')) {
|
||||
$query->whereNull('deleted_at');
|
||||
}
|
||||
|
||||
if (Input::get('updated_at') > 0) {
|
||||
$updatedAt = intval(Input::get('updated_at'));
|
||||
$query->where('updated_at', '>=', date('Y-m-d H:i:s', $updatedAt));
|
||||
@ -112,7 +116,7 @@ class BaseAPIController extends Controller
|
||||
$query->whereHas('client', $filter);
|
||||
}
|
||||
|
||||
if (! Utils::hasPermission('view_all')) {
|
||||
if (! Utils::hasPermission('admin')) {
|
||||
if ($this->entityType == ENTITY_USER) {
|
||||
$query->where('id', '=', Auth::user()->id);
|
||||
} else {
|
||||
|
@ -67,4 +67,5 @@ class BaseController extends Controller
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use App\Http\Requests\CreateClientRequest;
|
||||
use App\Http\Requests\UpdateClientRequest;
|
||||
use App\Jobs\LoadPostmarkHistory;
|
||||
use App\Jobs\ReactivatePostmarkEmail;
|
||||
use App\Jobs\Client\GenerateStatementData;
|
||||
use App\Models\Account;
|
||||
use App\Models\Client;
|
||||
use App\Models\Credit;
|
||||
@ -57,7 +58,7 @@ class ClientController extends BaseController
|
||||
public function getDatatable()
|
||||
{
|
||||
$search = Input::get('sSearch');
|
||||
$userId = Auth::user()->filterId();
|
||||
$userId = Auth::user()->filterIdByEntity(ENTITY_CLIENT);
|
||||
|
||||
return $this->clientService->getDatatable($search, $userId);
|
||||
}
|
||||
@ -85,10 +86,13 @@ class ClientController extends BaseController
|
||||
*/
|
||||
public function show(ClientRequest $request)
|
||||
{
|
||||
|
||||
$client = $request->entity();
|
||||
$user = Auth::user();
|
||||
$account = $user->account;
|
||||
|
||||
//$user->can('view', [ENTITY_CLIENT, $client]);
|
||||
|
||||
$actionLinks = [];
|
||||
if ($user->can('create', ENTITY_INVOICE)) {
|
||||
$actionLinks[] = ['label' => trans('texts.new_invoice'), 'url' => URL::to('/invoices/create/'.$client->public_id)];
|
||||
@ -146,6 +150,8 @@ class ClientController extends BaseController
|
||||
*/
|
||||
public function create(ClientRequest $request)
|
||||
{
|
||||
//Auth::user()->can('create', ENTITY_CLIENT);
|
||||
|
||||
if (Client::scope()->withTrashed()->count() > Auth::user()->getMaxNumClients()) {
|
||||
return View::make('error', ['hideHeader' => true, 'error' => "Sorry, you've exceeded the limit of ".Auth::user()->getMaxNumClients().' clients']);
|
||||
}
|
||||
@ -171,6 +177,7 @@ class ClientController extends BaseController
|
||||
*/
|
||||
public function edit(ClientRequest $request)
|
||||
{
|
||||
|
||||
$client = $request->entity();
|
||||
|
||||
$data = [
|
||||
@ -239,10 +246,12 @@ class ClientController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
public function statement($clientPublicId, $statusId = false, $startDate = false, $endDate = false)
|
||||
public function statement($clientPublicId)
|
||||
{
|
||||
$statusId = request()->status_id;
|
||||
$startDate = request()->start_date;
|
||||
$endDate = request()->end_date;
|
||||
$account = Auth::user()->account;
|
||||
$statusId = intval($statusId);
|
||||
$client = Client::scope(request()->client_id)->with('contacts')->firstOrFail();
|
||||
|
||||
if (! $startDate) {
|
||||
@ -250,32 +259,8 @@ class ClientController extends BaseController
|
||||
$endDate = Utils::today(false)->format('Y-m-d');
|
||||
}
|
||||
|
||||
$invoice = $account->createInvoice(ENTITY_INVOICE);
|
||||
$invoice->client = $client;
|
||||
$invoice->date_format = $account->date_format ? $account->date_format->format_moment : 'MMM D, YYYY';
|
||||
|
||||
$invoices = Invoice::scope()
|
||||
->with(['client'])
|
||||
->invoices()
|
||||
->whereClientId($client->id)
|
||||
->whereIsPublic(true)
|
||||
->orderBy('invoice_date', 'asc');
|
||||
|
||||
if ($statusId == INVOICE_STATUS_PAID) {
|
||||
$invoices->where('invoice_status_id', '=', INVOICE_STATUS_PAID);
|
||||
} elseif ($statusId == INVOICE_STATUS_UNPAID) {
|
||||
$invoices->where('invoice_status_id', '!=', INVOICE_STATUS_PAID);
|
||||
}
|
||||
|
||||
if ($statusId == INVOICE_STATUS_PAID || ! $statusId) {
|
||||
$invoices->where('invoice_date', '>=', $startDate)
|
||||
->where('invoice_date', '<=', $endDate);
|
||||
}
|
||||
|
||||
$invoice->invoice_items = $invoices->get();
|
||||
|
||||
if (request()->json) {
|
||||
return json_encode($invoice);
|
||||
return dispatch(new GenerateStatementData($client, request()->all()));
|
||||
}
|
||||
|
||||
$data = [
|
||||
|
@ -17,6 +17,7 @@ use App\Ninja\Repositories\InvoiceRepository;
|
||||
use App\Ninja\Repositories\PaymentRepository;
|
||||
use App\Ninja\Repositories\TaskRepository;
|
||||
use App\Services\PaymentService;
|
||||
use App\Jobs\Client\GenerateStatementData;
|
||||
use Auth;
|
||||
use Barracuda\ArchiveStream\ZipArchive;
|
||||
use Cache;
|
||||
@ -1009,4 +1010,41 @@ class ClientPortalController extends BaseController
|
||||
return redirect($account->enable_client_portal_dashboard ? '/client/dashboard' : '/client/payment_methods')
|
||||
->withMessage(trans('texts.updated_client_details'));
|
||||
}
|
||||
|
||||
public function statement() {
|
||||
if (! $contact = $this->getContact()) {
|
||||
return $this->returnError();
|
||||
}
|
||||
|
||||
$client = $contact->client;
|
||||
$account = $contact->account;
|
||||
|
||||
if (! $account->enable_client_portal || ! $account->enable_client_portal_dashboard) {
|
||||
return $this->returnError();
|
||||
}
|
||||
|
||||
$statusId = request()->status_id;
|
||||
$startDate = request()->start_date;
|
||||
$endDate = request()->end_date;
|
||||
|
||||
if (! $startDate) {
|
||||
$startDate = Utils::today(false)->modify('-6 month')->format('Y-m-d');
|
||||
$endDate = Utils::today(false)->format('Y-m-d');
|
||||
}
|
||||
|
||||
if (request()->json) {
|
||||
return dispatch(new GenerateStatementData($client, request()->all(), $contact));
|
||||
}
|
||||
|
||||
$data = [
|
||||
'extends' => 'public.header',
|
||||
'client' => $client,
|
||||
'account' => $account,
|
||||
'startDate' => $startDate,
|
||||
'endDate' => $endDate,
|
||||
];
|
||||
|
||||
return view('clients.statement', $data);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ class CreditController extends BaseController
|
||||
{
|
||||
$credit = Credit::withTrashed()->scope($publicId)->firstOrFail();
|
||||
|
||||
$this->authorize('edit', $credit);
|
||||
$this->authorize('view', $credit);
|
||||
|
||||
$credit->credit_date = Utils::fromSqlDate($credit->credit_date);
|
||||
|
||||
|
@ -18,7 +18,7 @@ class DashboardApiController extends BaseAPIController
|
||||
public function index()
|
||||
{
|
||||
$user = Auth::user();
|
||||
$viewAll = $user->hasPermission('view_all');
|
||||
$viewAll = $user->hasPermission('view_reports');
|
||||
$userId = $user->id;
|
||||
$accountId = $user->account->id;
|
||||
$defaultCurrency = $user->account->currency_id;
|
||||
@ -44,14 +44,14 @@ class DashboardApiController extends BaseAPIController
|
||||
|
||||
$data = [
|
||||
'id' => 1,
|
||||
'paidToDate' => $paidToDate->count() && $paidToDate[0]->value ? $paidToDate[0]->value : 0,
|
||||
'paidToDateCurrency' => $paidToDate->count() && $paidToDate[0]->currency_id ? $paidToDate[0]->currency_id : $defaultCurrency,
|
||||
'balances' => $balances->count() && $balances[0]->value ? $balances[0]->value : 0,
|
||||
'balancesCurrency' => $balances->count() && $balances[0]->currency_id ? $balances[0]->currency_id : $defaultCurrency,
|
||||
'averageInvoice' => $averageInvoice->count() && $averageInvoice[0]->invoice_avg ? $averageInvoice[0]->invoice_avg : 0,
|
||||
'averageInvoiceCurrency' => $averageInvoice->count() && $averageInvoice[0]->currency_id ? $averageInvoice[0]->currency_id : $defaultCurrency,
|
||||
'invoicesSent' => $metrics ? $metrics->invoices_sent : 0,
|
||||
'activeClients' => $metrics ? $metrics->active_clients : 0,
|
||||
'paidToDate' => (float) ($paidToDate->count() && $paidToDate[0]->value ? $paidToDate[0]->value : 0),
|
||||
'paidToDateCurrency' => (int) ($paidToDate->count() && $paidToDate[0]->currency_id ? $paidToDate[0]->currency_id : $defaultCurrency),
|
||||
'balances' => (float) ($balances->count() && $balances[0]->value ? $balances[0]->value : 0),
|
||||
'balancesCurrency' => (int) ($balances->count() && $balances[0]->currency_id ? $balances[0]->currency_id : $defaultCurrency),
|
||||
'averageInvoice' => (float) ($averageInvoice->count() && $averageInvoice[0]->invoice_avg ? $averageInvoice[0]->invoice_avg : 0),
|
||||
'averageInvoiceCurrency' => (int) ($averageInvoice->count() && $averageInvoice[0]->currency_id ? $averageInvoice[0]->currency_id : $defaultCurrency),
|
||||
'invoicesSent' => (int) ($metrics ? $metrics->invoices_sent : 0),
|
||||
'activeClients' => (int) ($metrics ? $metrics->active_clients : 0),
|
||||
'activities' => $this->createCollection($activities, new ActivityTransformer(), ENTITY_ACTIVITY),
|
||||
];
|
||||
|
||||
|
@ -25,7 +25,7 @@ class DashboardController extends BaseController
|
||||
public function index()
|
||||
{
|
||||
$user = Auth::user();
|
||||
$viewAll = $user->hasPermission('view_all');
|
||||
$viewAll = $user->hasPermission('view_reports');
|
||||
$userId = $user->id;
|
||||
$account = $user->account;
|
||||
$accountId = $account->id;
|
||||
|
@ -181,7 +181,11 @@ class InvoiceApiController extends BaseAPIController
|
||||
$client = $this->clientRepo->save($clientData);
|
||||
}
|
||||
} elseif (isset($data['client_id'])) {
|
||||
$client = Client::scope($data['client_id'])->firstOrFail();
|
||||
$client = Client::scope($data['client_id'])->first();
|
||||
|
||||
if (! $client) {
|
||||
return $this->errorResponse('Client not found', 404);
|
||||
}
|
||||
}
|
||||
|
||||
$data = self::prepareData($data, $client);
|
||||
|
@ -140,7 +140,7 @@ class InvoiceController extends BaseController
|
||||
|
||||
$lastSent = ($invoice->is_recurring && $invoice->last_sent_date) ? $invoice->recurring_invoices->last() : null;
|
||||
|
||||
if (! Auth::user()->hasPermission('view_all')) {
|
||||
if (! Auth::user()->hasPermission('view_client')) {
|
||||
$clients = $clients->where('clients.user_id', '=', Auth::user()->id);
|
||||
}
|
||||
|
||||
@ -211,7 +211,7 @@ class InvoiceController extends BaseController
|
||||
$invoice->loadFromRequest();
|
||||
|
||||
$clients = Client::scope()->with('contacts', 'country')->orderBy('name');
|
||||
if (! Auth::user()->hasPermission('view_all')) {
|
||||
if (! Auth::user()->hasPermission('view_client')) {
|
||||
$clients = $clients->where('clients.user_id', '=', Auth::user()->id);
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\CreateProductRequest;
|
||||
use App\Http\Requests\UpdateProductRequest;
|
||||
use App\Http\Requests\ProductRequest;
|
||||
use App\Models\Product;
|
||||
use App\Models\TaxRate;
|
||||
@ -9,6 +11,7 @@ use App\Ninja\Datatables\ProductDatatable;
|
||||
use App\Ninja\Repositories\ProductRepository;
|
||||
use App\Services\ProductService;
|
||||
use Auth;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Input;
|
||||
use Redirect;
|
||||
use Session;
|
||||
@ -84,6 +87,8 @@ class ProductController extends BaseController
|
||||
*/
|
||||
public function edit(ProductRequest $request, $publicId, $clone = false)
|
||||
{
|
||||
Auth::user()->can('view', [ENTITY_PRODUCT, $request->entity()]);
|
||||
|
||||
$account = Auth::user()->account;
|
||||
$product = Product::scope($publicId)->withTrashed()->firstOrFail();
|
||||
|
||||
@ -114,8 +119,9 @@ class ProductController extends BaseController
|
||||
/**
|
||||
* @return \Illuminate\Contracts\View\View
|
||||
*/
|
||||
public function create()
|
||||
public function create(ProductRequest $request)
|
||||
{
|
||||
|
||||
$account = Auth::user()->account;
|
||||
|
||||
$data = [
|
||||
@ -133,7 +139,7 @@ class ProductController extends BaseController
|
||||
/**
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function store()
|
||||
public function store(CreateProductRequest $request)
|
||||
{
|
||||
return $this->save();
|
||||
}
|
||||
@ -143,7 +149,7 @@ class ProductController extends BaseController
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function update($publicId)
|
||||
public function update(UpdateProductRequest $request, $publicId)
|
||||
{
|
||||
return $this->save($publicId);
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ class ProjectController extends BaseController
|
||||
public function getDatatable($expensePublicId = null)
|
||||
{
|
||||
$search = Input::get('sSearch');
|
||||
$userId = Auth::user()->filterId();
|
||||
$userId = Auth::user()->filterIdByEntity(ENTITY_PROJECT);
|
||||
|
||||
return $this->projectService->getDatatable($search, $userId);
|
||||
}
|
||||
|
@ -51,7 +51,8 @@ class ProposalController extends BaseController
|
||||
public function getDatatable($expensePublicId = null)
|
||||
{
|
||||
$search = Input::get('sSearch');
|
||||
$userId = Auth::user()->filterId();
|
||||
//$userId = Auth::user()->filterId();
|
||||
$userId = Auth::user()->filterIdByEntity(ENTITY_PROPOSAL);
|
||||
|
||||
return $this->proposalService->getDatatable($search, $userId);
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ class ReportController extends BaseController
|
||||
*/
|
||||
public function showReports()
|
||||
{
|
||||
if (! Auth::user()->hasPermission('view_all')) {
|
||||
if (! Auth::user()->hasPermission('view_reports')) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
|
@ -162,6 +162,7 @@ class UserController extends BaseController
|
||||
*/
|
||||
public function save($userPublicId = false)
|
||||
{
|
||||
|
||||
if (! Auth::user()->hasFeature(FEATURE_USERS)) {
|
||||
return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT);
|
||||
}
|
||||
@ -204,7 +205,7 @@ class UserController extends BaseController
|
||||
$user->email = trim(Input::get('email'));
|
||||
if (Auth::user()->hasFeature(FEATURE_USER_PERMISSIONS)) {
|
||||
$user->is_admin = boolval(Input::get('is_admin'));
|
||||
$user->permissions = Input::get('permissions');
|
||||
$user->permissions = self::formatUserPermissions(Input::get('permissions'));
|
||||
}
|
||||
} else {
|
||||
$lastUser = User::withTrashed()->where('account_id', '=', Auth::user()->account_id)
|
||||
@ -222,7 +223,7 @@ class UserController extends BaseController
|
||||
$user->public_id = $lastUser->public_id + 1;
|
||||
if (Auth::user()->hasFeature(FEATURE_USER_PERMISSIONS)) {
|
||||
$user->is_admin = boolval(Input::get('is_admin'));
|
||||
$user->permissions = Input::get('permissions');
|
||||
$user->permissions = self::formatUserPermissions(Input::get('permissions'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,6 +241,12 @@ class UserController extends BaseController
|
||||
return Redirect::to('users/' . $user->public_id . '/edit');
|
||||
}
|
||||
|
||||
private function formatUserPermissions(array $permissions) {
|
||||
|
||||
return json_encode(array_diff(array_values($permissions),[0]));
|
||||
|
||||
}
|
||||
|
||||
public function sendConfirmation($userPublicId)
|
||||
{
|
||||
$user = User::where('account_id', '=', Auth::user()->account_id)
|
||||
|
@ -37,6 +37,8 @@ class ApiCheck
|
||||
if ($secret = env(API_SECRET)) {
|
||||
$requestSecret = Request::header('X-Ninja-Secret') ?: ($request->api_secret ?: '');
|
||||
$hasApiSecret = hash_equals($requestSecret, $secret);
|
||||
} elseif (Utils::isSelfHost()) {
|
||||
$hasApiSecret = true;
|
||||
}
|
||||
|
||||
if ($loggingIn) {
|
||||
|
@ -45,7 +45,7 @@ class CreatePaymentAPIRequest extends PaymentRequest
|
||||
]);
|
||||
|
||||
$rules = [
|
||||
'amount' => 'required|numeric|not_in:0',
|
||||
'amount' => 'required|numeric',
|
||||
];
|
||||
|
||||
if ($this->payment_type_id == PAYMENT_TYPE_CREDIT) {
|
||||
|
160
app/Jobs/Client/GenerateStatementData.php
Normal file
160
app/Jobs/Client/GenerateStatementData.php
Normal file
@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Client;
|
||||
|
||||
use Utils;
|
||||
use App\Models\InvoiceItem;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Eloquent;
|
||||
|
||||
class GenerateStatementData
|
||||
{
|
||||
public function __construct($client, $options, $contact = false)
|
||||
{
|
||||
$this->client = $client;
|
||||
$this->options = $options;
|
||||
$this->contact = $contact;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$client = $this->client;
|
||||
$client->load('contacts');
|
||||
|
||||
$account = $client->account;
|
||||
$account->load(['date_format', 'datetime_format']);
|
||||
|
||||
$invoice = new Invoice();
|
||||
$invoice->invoice_date = Utils::today();
|
||||
$invoice->account = $account;
|
||||
$invoice->client = $client;
|
||||
|
||||
$invoice->invoice_items = $this->getInvoices();
|
||||
|
||||
if ($this->options['show_payments']) {
|
||||
$payments = $this->getPayments($invoice->invoice_items);
|
||||
$invoice->invoice_items = $invoice->invoice_items->merge($payments);
|
||||
}
|
||||
|
||||
$invoice->hidePrivateFields();
|
||||
|
||||
return json_encode($invoice);
|
||||
}
|
||||
|
||||
private function getInvoices()
|
||||
{
|
||||
$statusId = intval($this->options['status_id']);
|
||||
|
||||
$invoices = Invoice::with(['client'])
|
||||
->invoices()
|
||||
->whereClientId($this->client->id)
|
||||
->whereIsPublic(true)
|
||||
->withArchived()
|
||||
->orderBy('invoice_date', 'asc');
|
||||
|
||||
if ($statusId == INVOICE_STATUS_PAID) {
|
||||
$invoices->where('invoice_status_id', '=', INVOICE_STATUS_PAID);
|
||||
} elseif ($statusId == INVOICE_STATUS_UNPAID) {
|
||||
$invoices->where('invoice_status_id', '!=', INVOICE_STATUS_PAID);
|
||||
}
|
||||
|
||||
if ($statusId == INVOICE_STATUS_PAID || ! $statusId) {
|
||||
$invoices->where('invoice_date', '>=', $this->options['start_date'])
|
||||
->where('invoice_date', '<=', $this->options['end_date']);
|
||||
}
|
||||
|
||||
if ($this->contact) {
|
||||
$invoices->whereHas('invitations', function ($query) {
|
||||
$query->where('contact_id', $this->contact->id);
|
||||
});
|
||||
}
|
||||
|
||||
$invoices = $invoices->get();
|
||||
$data = collect();
|
||||
|
||||
for ($i=0; $i<$invoices->count(); $i++) {
|
||||
$invoice = $invoices[$i];
|
||||
$item = new InvoiceItem();
|
||||
$item->id = $invoice->id;
|
||||
$item->product_key = $invoice->invoice_number;
|
||||
$item->custom_value1 = $invoice->invoice_date;
|
||||
$item->custom_value2 = $invoice->due_date;
|
||||
$item->notes = $invoice->amount;
|
||||
$item->cost = $invoice->balance;
|
||||
$item->qty = 1;
|
||||
$item->invoice_item_type_id = 1;
|
||||
$data->push($item);
|
||||
}
|
||||
|
||||
if ($this->options['show_aging']) {
|
||||
$aging = $this->getAging($invoices);
|
||||
$data = $data->merge($aging);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getPayments($invoices)
|
||||
{
|
||||
$payments = Payment::with('invoice', 'payment_type')
|
||||
->withArchived()
|
||||
->whereClientId($this->client->id)
|
||||
//->excludeFailed()
|
||||
->where('payment_date', '>=', $this->options['start_date'])
|
||||
->where('payment_date', '<=', $this->options['end_date']);
|
||||
|
||||
if ($this->contact) {
|
||||
$payments->whereIn('invoice_id', $invoices->pluck('id'));
|
||||
}
|
||||
|
||||
$payments = $payments->get();
|
||||
$data = collect();
|
||||
|
||||
for ($i=0; $i<$payments->count(); $i++) {
|
||||
$payment = $payments[$i];
|
||||
$item = new InvoiceItem();
|
||||
$item->product_key = $payment->invoice->invoice_number;
|
||||
$item->custom_value1 = $payment->payment_date;
|
||||
$item->custom_value2 = $payment->present()->payment_type;
|
||||
$item->cost = $payment->getCompletedAmount();
|
||||
$item->invoice_item_type_id = 3;
|
||||
$data->push($item);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getAging($invoices)
|
||||
{
|
||||
$data = collect();
|
||||
$ageGroups = [
|
||||
'age_group_0' => 0,
|
||||
'age_group_30' => 0,
|
||||
'age_group_60' => 0,
|
||||
'age_group_90' => 0,
|
||||
'age_group_120' => 0,
|
||||
];
|
||||
|
||||
foreach ($invoices as $invoice) {
|
||||
$age = $invoice->present()->ageGroup;
|
||||
$ageGroups[$age] += $invoice->getRequestedAmount();
|
||||
}
|
||||
|
||||
$item = new InvoiceItem();
|
||||
$item->product_key = $ageGroups['age_group_0'];
|
||||
$item->notes = $ageGroups['age_group_30'];
|
||||
$item->custom_value1 = $ageGroups['age_group_60'];
|
||||
$item->custom_value1 = $ageGroups['age_group_90'];
|
||||
$item->cost = $ageGroups['age_group_120'];
|
||||
$item->invoice_item_type_id = 4;
|
||||
$data->push($item);
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ class ExportReportResults extends Job
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (! $this->user->hasPermission('view_all')) {
|
||||
if (! $this->user->hasPermission('view_reports')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ class LoadPostmarkStats extends Job
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (! auth()->user()->hasPermission('view_all')) {
|
||||
if (! auth()->user()->hasPermission('view_reports')) {
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ class RunReport extends Job
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if (! $this->user->hasPermission('view_all')) {
|
||||
if (! $this->user->hasPermission('view_reports')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -51,6 +51,7 @@ class CurlUtils
|
||||
|
||||
$client = Client::getInstance();
|
||||
$client->isLazy();
|
||||
//$client->getEngine()->addOption("--ignore-ssl-errors=true");
|
||||
$client->getEngine()->setPath($path);
|
||||
|
||||
$request = $client->getMessageFactory()->createRequest($url, $method);
|
||||
|
@ -501,6 +501,18 @@ class Utils
|
||||
}
|
||||
}
|
||||
|
||||
public static function getStaticData()
|
||||
{
|
||||
$data = [];
|
||||
|
||||
$cachedTables = unserialize(CACHED_TABLES);
|
||||
foreach ($cachedTables as $name => $class) {
|
||||
$data[$name] = Cache::get($name);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public static function getFromCache($id, $type)
|
||||
{
|
||||
$cache = Cache::get($type);
|
||||
@ -1280,6 +1292,7 @@ class Utils
|
||||
'tasks',
|
||||
'expenses',
|
||||
'vendors',
|
||||
'proposals',
|
||||
];
|
||||
|
||||
if ($path == 'dashboard') {
|
||||
|
@ -267,8 +267,6 @@ class SubscriptionListener
|
||||
$jsonData['client_name'] = $entity->client->getDisplayName();
|
||||
}
|
||||
|
||||
|
||||
|
||||
foreach ($subscriptions as $subscription) {
|
||||
switch ($subscription->format) {
|
||||
case SUBSCRIPTION_FORMAT_JSON:
|
||||
|
@ -1797,6 +1797,11 @@ class Account extends Eloquent
|
||||
return $this->company->accounts->count() > 1;
|
||||
}
|
||||
|
||||
public function hasMultipleUsers()
|
||||
{
|
||||
return $this->users->count() > 1;
|
||||
}
|
||||
|
||||
public function getPrimaryAccount()
|
||||
{
|
||||
return $this->company->accounts()->orderBy('id')->first();
|
||||
|
@ -183,6 +183,7 @@ class Contact extends EntityModel implements AuthenticatableContract, CanResetPa
|
||||
}
|
||||
|
||||
$account = $this->account;
|
||||
$iframe_url = $account->iframe_url;
|
||||
$url = trim(SITE_URL, '/');
|
||||
|
||||
if ($account->hasFeature(FEATURE_CUSTOM_URL)) {
|
||||
@ -190,7 +191,13 @@ class Contact extends EntityModel implements AuthenticatableContract, CanResetPa
|
||||
$url = $account->present()->clientPortalLink();
|
||||
}
|
||||
|
||||
if ($this->account->subdomain) {
|
||||
if ($iframe_url) {
|
||||
if ($account->is_custom_domain) {
|
||||
$url = $iframe_url;
|
||||
} else {
|
||||
return "{$iframe_url}?{$this->contact_key}/client";
|
||||
}
|
||||
} elseif ($this->account->subdomain) {
|
||||
$url = Utils::replaceSubdomain($url, $account->subdomain);
|
||||
}
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ class EntityModel extends Eloquent
|
||||
return $className::scope($publicId)->withTrashed()->value('id');
|
||||
} else {
|
||||
return $className::scope($publicId)->value('id');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -179,7 +179,7 @@ class EntityModel extends Eloquent
|
||||
}
|
||||
}
|
||||
|
||||
if (Auth::check() && ! Auth::user()->hasPermission('view_all') && method_exists($this, 'getEntityType') && $this->getEntityType() != ENTITY_TAX_RATE) {
|
||||
if (Auth::check() && method_exists($this, 'getEntityType') && ! Auth::user()->hasPermission('view_' . $this->getEntityType()) && $this->getEntityType() != ENTITY_TAX_RATE) {
|
||||
$query->where(Utils::pluralizeEntityType($this->getEntityType()) . '.user_id', '=', Auth::user()->id);
|
||||
}
|
||||
|
||||
@ -449,4 +449,25 @@ class EntityModel extends Eloquent
|
||||
|
||||
return $this->id == $obj->id && $this->getEntityType() == $obj->entityType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $method
|
||||
* @param $params
|
||||
*/
|
||||
public function __call($method, $params)
|
||||
{
|
||||
if (count(config('modules.relations'))) {
|
||||
$entityType = $this->getEntityType();
|
||||
|
||||
if ($entityType) {
|
||||
$config = implode('.', ['modules.relations.' . $entityType, $method]);
|
||||
if (config()->has($config)) {
|
||||
$function = config()->get($config);
|
||||
return $function($this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parent::__call($method, $params);
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,8 @@ class Gateway extends Eloquent
|
||||
GATEWAY_AUTHORIZE_NET,
|
||||
GATEWAY_MOLLIE,
|
||||
GATEWAY_GOCARDLESS,
|
||||
GATEWAY_BITPAY,
|
||||
GATEWAY_DWOLLA,
|
||||
GATEWAY_CUSTOM1,
|
||||
GATEWAY_CUSTOM2,
|
||||
GATEWAY_CUSTOM3,
|
||||
@ -60,7 +62,6 @@ class Gateway extends Eloquent
|
||||
*/
|
||||
public static $alternate = [
|
||||
GATEWAY_PAYPAL_EXPRESS,
|
||||
GATEWAY_GOCARDLESS,
|
||||
GATEWAY_BITPAY,
|
||||
GATEWAY_DWOLLA,
|
||||
GATEWAY_CUSTOM1,
|
||||
|
@ -1601,6 +1601,27 @@ class Invoice extends EntityModel implements BalanceAffecting
|
||||
|
||||
return $this->isSent() && ! $this->is_recurring;
|
||||
}
|
||||
|
||||
public function getInvoiceLinkForQuote($contactId)
|
||||
{
|
||||
if (! $this->quote_invoice_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$invoice = static::scope($this->quote_invoice_id)->with('invitations')->first();
|
||||
|
||||
if (! $invoice) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($invoice->invitations as $invitation) {
|
||||
if ($invitation->contact_id == $contactId) {
|
||||
return $invitation->getLink();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Invoice::creating(function ($invoice) {
|
||||
|
@ -138,9 +138,9 @@ class InvoiceItem extends EntityModel
|
||||
|
||||
if ($this->discount != 0) {
|
||||
if ($this->invoice->is_amount_discount) {
|
||||
$cost -= $discount / $this->qty;
|
||||
$cost -= $this->discount / $this->qty;
|
||||
} else {
|
||||
$cost -= $cost * $discount / 100;
|
||||
$cost -= $cost * $this->discount / 100;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -337,6 +337,11 @@ trait PresentsInvoice
|
||||
'custom_value2',
|
||||
'delivery_note',
|
||||
'date',
|
||||
'method',
|
||||
'payment_date',
|
||||
'reference',
|
||||
'amount',
|
||||
'amount_paid',
|
||||
];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
|
@ -331,72 +331,34 @@ class User extends Authenticatable
|
||||
return Utils::isNinjaProd() && $this->email != $this->getOriginal('email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the permissions attribute on the model.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function setPermissionsAttribute($value)
|
||||
{
|
||||
if (empty($value)) {
|
||||
$this->attributes['permissions'] = 0;
|
||||
} else {
|
||||
$bitmask = 0;
|
||||
foreach ($value as $permission) {
|
||||
if (! $permission) {
|
||||
continue;
|
||||
}
|
||||
$bitmask = $bitmask | static::$all_permissions[$permission];
|
||||
}
|
||||
|
||||
$this->attributes['permissions'] = $bitmask;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the value of the permissions attribute.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getPermissionsAttribute($value)
|
||||
{
|
||||
$permissions = [];
|
||||
foreach (static::$all_permissions as $permission => $bitmask) {
|
||||
if (($value & $bitmask) == $bitmask) {
|
||||
$permissions[$permission] = $permission;
|
||||
}
|
||||
}
|
||||
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the user has the required permission.
|
||||
*
|
||||
* @param mixed $permission Either a single permission or an array of possible permissions
|
||||
* @param bool True to require all permissions, false to require only one
|
||||
* @param mixed $requireAll
|
||||
* @param mixed $requireAll - True to require all permissions, false to require only one
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
public function hasPermission($permission, $requireAll = false)
|
||||
{
|
||||
if ($this->is_admin) {
|
||||
return true;
|
||||
} elseif (is_string($permission)) {
|
||||
return ! empty($this->permissions[$permission]);
|
||||
} elseif (is_array($permission)) {
|
||||
if ($requireAll) {
|
||||
return count(array_diff($permission, $this->permissions)) == 0;
|
||||
} else {
|
||||
return count(array_intersect($permission, $this->permissions)) > 0;
|
||||
|
||||
if( is_array(json_decode($this->permissions,1)) && in_array($permission, json_decode($this->permissions,1)) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
} elseif (is_array($permission)) {
|
||||
|
||||
if ($requireAll)
|
||||
return count(array_intersect($permission, json_decode($this->permissions,1))) == count( $permission );
|
||||
else
|
||||
return count(array_intersect($permission, json_decode($this->permissions,1))) > 0;
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -416,10 +378,15 @@ class User extends Authenticatable
|
||||
* @return bool|mixed
|
||||
*/
|
||||
public function filterId()
|
||||
{
|
||||
{ //todo permissions
|
||||
return $this->hasPermission('view_all') ? false : $this->id;
|
||||
}
|
||||
|
||||
public function filterIdByEntity($entity)
|
||||
{
|
||||
return $this->hasPermission('view_' . $entity) ? false : $this->id;
|
||||
}
|
||||
|
||||
public function caddAddUsers()
|
||||
{
|
||||
if (! Utils::isNinjaProd()) {
|
||||
@ -478,6 +445,28 @@ class User extends Authenticatable
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function ownsEntity($entity)
|
||||
{
|
||||
return $entity->user_id == $this->id;
|
||||
}
|
||||
|
||||
public function shouldNotify($invoice)
|
||||
{
|
||||
if (! $this->email || ! $this->confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->cannot('view', $invoice)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->only_notify_owned && ! $this->ownsEntity($invoice)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
User::created(function ($user)
|
||||
|
@ -67,10 +67,10 @@ class ClientDatatable extends EntityDatatable
|
||||
[
|
||||
trans('texts.edit_client'),
|
||||
function ($model) {
|
||||
return URL::to("clients/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_CLIENT, $model->user_id]);
|
||||
if(Auth::user()->can('edit', [ENTITY_CLIENT, $model]))
|
||||
return URL::to("clients/{$model->public_id}/edit");
|
||||
elseif(Auth::user()->can('view', [ENTITY_CLIENT, $model]))
|
||||
return URL::to("clients/{$model->public_id}");
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -78,9 +78,7 @@ class ClientDatatable extends EntityDatatable
|
||||
return false;
|
||||
},
|
||||
function ($model) {
|
||||
$user = Auth::user();
|
||||
|
||||
return $user->can('editByOwner', [ENTITY_CLIENT, $model->user_id]) && ($user->can('create', ENTITY_TASK) || $user->can('create', ENTITY_INVOICE));
|
||||
return Auth::user()->can('edit', [ENTITY_CLIENT, $model]) && (Auth::user()->can('create', ENTITY_TASK) || Auth::user()->can('create', ENTITY_INVOICE));
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -115,9 +113,7 @@ class ClientDatatable extends EntityDatatable
|
||||
return false;
|
||||
},
|
||||
function ($model) {
|
||||
$user = Auth::user();
|
||||
|
||||
return ($user->can('create', ENTITY_TASK) || $user->can('create', ENTITY_INVOICE)) && ($user->can('create', ENTITY_PAYMENT) || $user->can('create', ENTITY_CREDIT) || $user->can('create', ENTITY_EXPENSE));
|
||||
return (Auth::user()->can('create', ENTITY_TASK) || Auth::user()->can('create', ENTITY_INVOICE)) && (Auth::user()->can('create', ENTITY_PAYMENT) || Auth::user()->can('create', ENTITY_CREDIT) || Auth::user()->can('create', ENTITY_EXPENSE));
|
||||
},
|
||||
],
|
||||
[
|
||||
|
@ -17,46 +17,50 @@ class CreditDatatable extends EntityDatatable
|
||||
[
|
||||
'client_name',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_CLIENT, $model]))
|
||||
return $model->client_public_id ? link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml() : '';
|
||||
else
|
||||
return Utils::getClientDisplayName($model);
|
||||
}
|
||||
|
||||
return $model->client_public_id ? link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml() : '';
|
||||
},
|
||||
! $this->hideClient,
|
||||
],
|
||||
[
|
||||
'amount',
|
||||
function ($model) {
|
||||
return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id) . '<span '.Utils::getEntityRowClass($model).'/>';
|
||||
if(Auth::user()->can('view', [ENTITY_CLIENT, $model]))
|
||||
return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id) . '<span '.Utils::getEntityRowClass($model).'/>';
|
||||
},
|
||||
],
|
||||
[
|
||||
'balance',
|
||||
function ($model) {
|
||||
return Utils::formatMoney($model->balance, $model->currency_id, $model->country_id);
|
||||
if(Auth::user()->can('view', [ENTITY_CLIENT, $model]))
|
||||
return Utils::formatMoney($model->balance, $model->currency_id, $model->country_id);
|
||||
},
|
||||
],
|
||||
[
|
||||
'credit_date',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_CREDIT, $model->user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_CREDIT, $model]))
|
||||
return link_to("credits/{$model->public_id}/edit", Utils::fromSqlDate($model->credit_date_sql))->toHtml();
|
||||
else
|
||||
return Utils::fromSqlDate($model->credit_date_sql);
|
||||
}
|
||||
|
||||
return link_to("credits/{$model->public_id}/edit", Utils::fromSqlDate($model->credit_date_sql))->toHtml();
|
||||
},
|
||||
],
|
||||
[
|
||||
'public_notes',
|
||||
function ($model) {
|
||||
return e($model->public_notes);
|
||||
if (Auth::user()->can('view', [ENTITY_CREDIT, $model]))
|
||||
return e($model->public_notes);
|
||||
},
|
||||
],
|
||||
[
|
||||
'private_notes',
|
||||
function ($model) {
|
||||
return e($model->private_notes);
|
||||
if (Auth::user()->can('view', [ENTITY_CREDIT, $model]))
|
||||
return e($model->private_notes);
|
||||
},
|
||||
],
|
||||
];
|
||||
@ -71,7 +75,7 @@ class CreditDatatable extends EntityDatatable
|
||||
return URL::to("credits/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_CREDIT, $model->user_id]);
|
||||
return Auth::user()->can('view', [ENTITY_CREDIT, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
|
@ -16,11 +16,11 @@ class ExpenseCategoryDatatable extends EntityDatatable
|
||||
[
|
||||
'name',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('editByOwner', [ENTITY_EXPENSE_CATEGORY, $model->user_id])) {
|
||||
if (Auth::user()->can('edit', [ENTITY_EXPENSE_CATEGORY, $model]))
|
||||
return link_to("expense_categories/{$model->public_id}/edit", $model->category)->toHtml();
|
||||
else
|
||||
return $model->category;
|
||||
}
|
||||
|
||||
return link_to("expense_categories/{$model->public_id}/edit", $model->category)->toHtml();
|
||||
},
|
||||
],
|
||||
];
|
||||
@ -35,7 +35,7 @@ class ExpenseCategoryDatatable extends EntityDatatable
|
||||
return URL::to("expense_categories/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_EXPENSE_CATEGORY, $model->user_id]);
|
||||
return Auth::user()->can('edit', [ENTITY_EXPENSE_CATEGORY, $model]);
|
||||
},
|
||||
],
|
||||
];
|
||||
|
@ -19,11 +19,11 @@ class ExpenseDatatable extends EntityDatatable
|
||||
'vendor_name',
|
||||
function ($model) {
|
||||
if ($model->vendor_public_id) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_VENDOR, $model->vendor_user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_VENDOR, $model]))
|
||||
return link_to("vendors/{$model->vendor_public_id}", $model->vendor_name)->toHtml();
|
||||
else
|
||||
return $model->vendor_name;
|
||||
}
|
||||
|
||||
return link_to("vendors/{$model->vendor_public_id}", $model->vendor_name)->toHtml();
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
@ -34,11 +34,11 @@ class ExpenseDatatable extends EntityDatatable
|
||||
'client_name',
|
||||
function ($model) {
|
||||
if ($model->client_public_id) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_CLIENT, $model]))
|
||||
return link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml();
|
||||
else
|
||||
return Utils::getClientDisplayName($model);
|
||||
}
|
||||
|
||||
return link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml();
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
@ -48,12 +48,11 @@ class ExpenseDatatable extends EntityDatatable
|
||||
[
|
||||
'expense_date',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_EXPENSE, $model->user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_EXPENSE, $model]))
|
||||
return $this->addNote(link_to("expenses/{$model->public_id}/edit", Utils::fromSqlDate($model->expense_date_sql))->toHtml(), $model->private_notes);
|
||||
else
|
||||
return Utils::fromSqlDate($model->expense_date_sql);
|
||||
}
|
||||
|
||||
$str = link_to("expenses/{$model->public_id}/edit", Utils::fromSqlDate($model->expense_date_sql))->toHtml();
|
||||
return $this->addNote($str, $model->private_notes);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -75,11 +74,11 @@ class ExpenseDatatable extends EntityDatatable
|
||||
'category',
|
||||
function ($model) {
|
||||
$category = $model->category != null ? substr($model->category, 0, 100) : '';
|
||||
if (! Auth::user()->can('editByOwner', [ENTITY_EXPENSE_CATEGORY, $model->category_user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_EXPENSE_CATEGORY, $model]))
|
||||
return $model->category_public_id ? link_to("expense_categories/{$model->category_public_id}/edit", $category)->toHtml() : '';
|
||||
else
|
||||
return $category;
|
||||
}
|
||||
|
||||
return $model->category_public_id ? link_to("expense_categories/{$model->category_public_id}/edit", $category)->toHtml() : '';
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -106,7 +105,7 @@ class ExpenseDatatable extends EntityDatatable
|
||||
return URL::to("expenses/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_EXPENSE, $model->user_id]);
|
||||
return Auth::user()->can('view', [ENTITY_EXPENSE, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -115,7 +114,7 @@ class ExpenseDatatable extends EntityDatatable
|
||||
return URL::to("expenses/{$model->public_id}/clone");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('viewByOwner', [ENTITY_EXPENSE, $model->user_id]) && Auth::user()->can('create', ENTITY_EXPENSE);
|
||||
return Auth::user()->can('create', ENTITY_EXPENSE);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -124,7 +123,7 @@ class ExpenseDatatable extends EntityDatatable
|
||||
return URL::to("/invoices/{$model->invoice_public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return $model->invoice_public_id && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->invoice_user_id]);
|
||||
return $model->invoice_public_id && Auth::user()->can('view', [ENTITY_INVOICE, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
|
@ -20,22 +20,24 @@ class InvoiceDatatable extends EntityDatatable
|
||||
[
|
||||
$entityType == ENTITY_INVOICE ? 'invoice_number' : 'quote_number',
|
||||
function ($model) use ($entityType) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_INVOICE, $model->user_id])) {
|
||||
return $model->invoice_number;
|
||||
if(Auth::user()->can('view', [ENTITY_INVOICE, $model])) {
|
||||
$str = link_to("{$entityType}s/{$model->public_id}/edit", $model->invoice_number, ['class' => Utils::getEntityRowClass($model)])->toHtml();
|
||||
return $this->addNote($str, $model->private_notes);
|
||||
}
|
||||
else
|
||||
return $model->invoice_number;
|
||||
|
||||
|
||||
$str = link_to("{$entityType}s/{$model->public_id}/edit", $model->invoice_number, ['class' => Utils::getEntityRowClass($model)])->toHtml();
|
||||
return $this->addNote($str, $model->private_notes);
|
||||
},
|
||||
],
|
||||
[
|
||||
'client_name',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])) {
|
||||
if(Auth::user()->can('view', [ENTITY_CLIENT, $model]))
|
||||
return link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml();
|
||||
else
|
||||
return Utils::getClientDisplayName($model);
|
||||
}
|
||||
|
||||
return link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml();
|
||||
},
|
||||
! $this->hideClient,
|
||||
],
|
||||
@ -96,7 +98,7 @@ class InvoiceDatatable extends EntityDatatable
|
||||
return URL::to("invoices/{$model->public_id}/clone");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('viewByOwner', [ENTITY_INVOICE, $model->user_id]) && Auth::user()->can('create', ENTITY_INVOICE);
|
||||
return Auth::user()->can('create', ENTITY_INVOICE);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -105,7 +107,7 @@ class InvoiceDatatable extends EntityDatatable
|
||||
return URL::to("quotes/{$model->public_id}/clone");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('viewByOwner', [ENTITY_INVOICE, $model->user_id]) && Auth::user()->can('create', ENTITY_QUOTE);
|
||||
return Auth::user()->can('create', ENTITY_QUOTE);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -128,7 +130,7 @@ class InvoiceDatatable extends EntityDatatable
|
||||
return false;
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]) || Auth::user()->can('create', ENTITY_PAYMENT);
|
||||
return Auth::user()->canCreateOrEdit(ENTITY_INVOICE);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -137,7 +139,7 @@ class InvoiceDatatable extends EntityDatatable
|
||||
return "javascript:submitForm_{$entityType}('markSent', {$model->public_id})";
|
||||
},
|
||||
function ($model) {
|
||||
return ! $model->is_public && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]);
|
||||
return ! $model->is_public && Auth::user()->can('edit', [ENTITY_INVOICE, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -146,7 +148,7 @@ class InvoiceDatatable extends EntityDatatable
|
||||
return "javascript:submitForm_{$entityType}('markPaid', {$model->public_id})";
|
||||
},
|
||||
function ($model) use ($entityType) {
|
||||
return $entityType == ENTITY_INVOICE && $model->invoice_status_id != INVOICE_STATUS_PAID && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]);
|
||||
return $entityType == ENTITY_INVOICE && $model->invoice_status_id != INVOICE_STATUS_PAID && Auth::user()->can('edit', [ENTITY_INVOICE, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -164,7 +166,7 @@ class InvoiceDatatable extends EntityDatatable
|
||||
return URL::to("invoices/{$model->quote_invoice_id}/edit");
|
||||
},
|
||||
function ($model) use ($entityType) {
|
||||
return $entityType == ENTITY_QUOTE && $model->quote_invoice_id && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]);
|
||||
return $entityType == ENTITY_QUOTE && $model->quote_invoice_id && Auth::user()->can('view', [ENTITY_INVOICE, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -182,7 +184,7 @@ class InvoiceDatatable extends EntityDatatable
|
||||
return "javascript:submitForm_quote('convert', {$model->public_id})";
|
||||
},
|
||||
function ($model) use ($entityType) {
|
||||
return $entityType == ENTITY_QUOTE && ! $model->quote_invoice_id && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]);
|
||||
return $entityType == ENTITY_QUOTE && ! $model->quote_invoice_id && Auth::user()->can('edit', [ENTITY_INVOICE, $model]);
|
||||
},
|
||||
],
|
||||
];
|
||||
|
@ -25,21 +25,22 @@ class PaymentDatatable extends EntityDatatable
|
||||
[
|
||||
'invoice_name',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_INVOICE, $model->invoice_user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_INVOICE, $model->invoice_user_id]))
|
||||
return link_to("invoices/{$model->invoice_public_id}/edit", $model->invoice_number, ['class' => Utils::getEntityRowClass($model)])->toHtml();
|
||||
else
|
||||
return $model->invoice_number;
|
||||
}
|
||||
|
||||
return link_to("invoices/{$model->invoice_public_id}/edit", $model->invoice_number, ['class' => Utils::getEntityRowClass($model)])->toHtml();
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'client_name',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])) {
|
||||
if(Auth::user()->can('view', [ENTITY_CLIENT, ENTITY_CLIENT]))
|
||||
return $model->client_public_id ? link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml() : '';
|
||||
else
|
||||
return Utils::getClientDisplayName($model);
|
||||
}
|
||||
|
||||
return $model->client_public_id ? link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml() : '';
|
||||
|
||||
},
|
||||
! $this->hideClient,
|
||||
],
|
||||
@ -128,7 +129,7 @@ class PaymentDatatable extends EntityDatatable
|
||||
return URL::to("payments/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id]);
|
||||
return Auth::user()->can('view', [ENTITY_PAYMENT, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -137,7 +138,7 @@ class PaymentDatatable extends EntityDatatable
|
||||
return "javascript:submitForm_payment('email', {$model->public_id})";
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id]);
|
||||
return Auth::user()->can('edit', [ENTITY_PAYMENT, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -151,7 +152,7 @@ class PaymentDatatable extends EntityDatatable
|
||||
return "javascript:showRefundModal({$model->public_id}, '{$max_refund}', '{$formatted}', '{$symbol}', {$local})";
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_PAYMENT, $model->user_id])
|
||||
return Auth::user()->can('edit', [ENTITY_PAYMENT, $model])
|
||||
&& $model->payment_status_id >= PAYMENT_STATUS_COMPLETED
|
||||
&& $model->refunded < $model->amount;
|
||||
},
|
||||
|
@ -17,23 +17,23 @@ class ProjectDatatable extends EntityDatatable
|
||||
[
|
||||
'project',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('editByOwner', [ENTITY_PROJECT, $model->user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_PROJECT, $model]))
|
||||
return $this->addNote(link_to("projects/{$model->public_id}", $model->project)->toHtml(), $model->private_notes);
|
||||
else
|
||||
return $model->project;
|
||||
}
|
||||
|
||||
$str = link_to("projects/{$model->public_id}", $model->project)->toHtml();
|
||||
return $this->addNote($str, $model->private_notes);
|
||||
|
||||
},
|
||||
],
|
||||
[
|
||||
'client_name',
|
||||
function ($model) {
|
||||
if ($model->client_public_id) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_CLIENT, $model]))
|
||||
return link_to("clients/{$model->client_public_id}", $model->client_name)->toHtml();
|
||||
else
|
||||
return Utils::getClientDisplayName($model);
|
||||
}
|
||||
|
||||
return link_to("clients/{$model->client_public_id}", $model->client_name)->toHtml();
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
@ -69,7 +69,7 @@ class ProjectDatatable extends EntityDatatable
|
||||
return URL::to("projects/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_PROJECT, $model->user_id]);
|
||||
return Auth::user()->can('view', [ENTITY_PROJECT, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
|
@ -17,11 +17,11 @@ class ProposalCategoryDatatable extends EntityDatatable
|
||||
[
|
||||
'name',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('editByOwner', [ENTITY_PROPOSAL_CATEGORY, $model->user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_PROPOSAL_CATEGORY, $model]) )
|
||||
return link_to("proposals/categories/{$model->public_id}/edit", $model->name)->toHtml();
|
||||
else
|
||||
return $model->name;
|
||||
}
|
||||
|
||||
return link_to("proposals/categories/{$model->public_id}/edit", $model->name)->toHtml();
|
||||
},
|
||||
],
|
||||
];
|
||||
@ -36,7 +36,7 @@ class ProposalCategoryDatatable extends EntityDatatable
|
||||
return URL::to("proposals/categories/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_PROPOSAL_CATEGORY, $model->user_id]);
|
||||
return Auth::user()->can('view', [ENTITY_PROPOSAL_CATEGORY, $model]);
|
||||
},
|
||||
],
|
||||
];
|
||||
|
@ -17,41 +17,40 @@ class ProposalDatatable extends EntityDatatable
|
||||
[
|
||||
'quote',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_QUOTE, $model->invoice_user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_QUOTE, $model]))
|
||||
return link_to("quotes/{$model->invoice_public_id}", $model->invoice_number)->toHtml();
|
||||
else
|
||||
return $model->invoice_number;
|
||||
}
|
||||
|
||||
return link_to("quotes/{$model->invoice_public_id}", $model->invoice_number)->toHtml();
|
||||
},
|
||||
],
|
||||
[
|
||||
'client',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_CLIENT, $model]))
|
||||
return link_to("clients/{$model->client_public_id}", $model->client)->toHtml();
|
||||
else
|
||||
return $model->client;
|
||||
}
|
||||
|
||||
return link_to("clients/{$model->client_public_id}", $model->client)->toHtml();
|
||||
},
|
||||
],
|
||||
[
|
||||
'template',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_PROPOSAL_TEMPLATE, $model->template_user_id])) {
|
||||
if(Auth::user()->can('view', [ENTITY_PROPOSAL_TEMPLATE, $model]))
|
||||
return link_to("proposals/templates/{$model->template_public_id}/edit", $model->template ?: ' ')->toHtml();
|
||||
else
|
||||
return $model->template ?: ' ';
|
||||
}
|
||||
|
||||
return link_to("proposals/templates/{$model->template_public_id}/edit", $model->template ?: ' ')->toHtml();
|
||||
},
|
||||
],
|
||||
[
|
||||
'created_at',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_PROPOSAL, $model->user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_PROPOSAL, $model]))
|
||||
return link_to("proposals/{$model->public_id}/edit", Utils::timestampToDateString(strtotime($model->created_at)))->toHtml();
|
||||
else
|
||||
return Utils::timestampToDateString(strtotime($model->created_at));
|
||||
}
|
||||
|
||||
return link_to("proposals/{$model->public_id}/edit", Utils::timestampToDateString(strtotime($model->created_at)))->toHtml();
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -78,7 +77,7 @@ class ProposalDatatable extends EntityDatatable
|
||||
return URL::to("proposals/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_PROPOSAL, $model->user_id]);
|
||||
return Auth::user()->can('view', [ENTITY_PROPOSAL, $model]) ;
|
||||
},
|
||||
],
|
||||
];
|
||||
|
@ -19,21 +19,22 @@ class ProposalSnippetDatatable extends EntityDatatable
|
||||
function ($model) {
|
||||
$icon = '<i class="fa fa-' . $model->icon . '"></i> ';
|
||||
|
||||
if (! Auth::user()->can('editByOwner', [ENTITY_PROPOSAL_SNIPPET, $model->user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_PROPOSAL_SNIPPET, $model]))
|
||||
return $icon . link_to("proposals/snippets/{$model->public_id}/edit", $model->name)->toHtml();
|
||||
else
|
||||
return $icon . $model->name;
|
||||
}
|
||||
|
||||
return $icon . link_to("proposals/snippets/{$model->public_id}/edit", $model->name)->toHtml();
|
||||
|
||||
},
|
||||
],
|
||||
[
|
||||
'category',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('editByOwner', [ENTITY_PROPOSAL_CATEGORY, $model->category_user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_PROPOSAL_CATEGORY, $model]))
|
||||
return link_to("proposals/categories/{$model->category_public_id}/edit", $model->category ?: ' ')->toHtml();
|
||||
else
|
||||
return $model->category;
|
||||
}
|
||||
|
||||
return link_to("proposals/categories/{$model->category_public_id}/edit", $model->category ?: ' ')->toHtml();
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -60,7 +61,7 @@ class ProposalSnippetDatatable extends EntityDatatable
|
||||
return URL::to("proposals/snippets/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_PROPOSAL_SNIPPET, $model->user_id]);
|
||||
return Auth::user()->can('view', [ENTITY_PROPOSAL_SNIPPET, $model]);
|
||||
},
|
||||
],
|
||||
];
|
||||
|
@ -17,13 +17,10 @@ class ProposalTemplateDatatable extends EntityDatatable
|
||||
[
|
||||
'name',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('editByOwner', [ENTITY_PROPOSAL_TEMPLATE, $model->user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_PROPOSAL_TEMPLATE, $model]))
|
||||
return link_to("proposals/templates/{$model->public_id}", $model->name)->toHtml();
|
||||
else
|
||||
return $model->name;
|
||||
}
|
||||
|
||||
return link_to("proposals/templates/{$model->public_id}", $model->name)->toHtml();
|
||||
//$str = link_to("quotes/{$model->quote_public_id}", $model->quote_number)->toHtml();
|
||||
//return $this->addNote($str, $model->private_notes);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -50,7 +47,7 @@ class ProposalTemplateDatatable extends EntityDatatable
|
||||
return URL::to("proposals/templates/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_PROPOSAL_TEMPLATE, $model->user_id]);
|
||||
return Auth::user()->can('view', [ENTITY_PROPOSAL_TEMPLATE, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -59,7 +56,7 @@ class ProposalTemplateDatatable extends EntityDatatable
|
||||
return URL::to("proposals/templates/{$model->public_id}/clone");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_PROPOSAL_TEMPLATE, $model->user_id]);
|
||||
return Auth::user()->can('view', [ENTITY_PROPOSAL_TEMPLATE, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -68,7 +65,7 @@ class ProposalTemplateDatatable extends EntityDatatable
|
||||
return URL::to("proposals/create/0/{$model->public_id}");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('create', [ENTITY_PROPOSAL, $model->user_id]);
|
||||
return Auth::user()->can('create', [ENTITY_PROPOSAL, $model]);
|
||||
},
|
||||
],
|
||||
];
|
||||
|
@ -19,11 +19,11 @@ class RecurringExpenseDatatable extends EntityDatatable
|
||||
'vendor_name',
|
||||
function ($model) {
|
||||
if ($model->vendor_public_id) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_VENDOR, $model->vendor_user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_VENDOR, $model]))
|
||||
return link_to("vendors/{$model->vendor_public_id}", $model->vendor_name)->toHtml();
|
||||
else
|
||||
return $model->vendor_name;
|
||||
}
|
||||
|
||||
return link_to("vendors/{$model->vendor_public_id}", $model->vendor_name)->toHtml();
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
@ -34,11 +34,12 @@ class RecurringExpenseDatatable extends EntityDatatable
|
||||
'client_name',
|
||||
function ($model) {
|
||||
if ($model->client_public_id) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_CLIENT, $model]))
|
||||
return link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml();
|
||||
else
|
||||
return Utils::getClientDisplayName($model);
|
||||
}
|
||||
|
||||
return link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml();
|
||||
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
@ -88,11 +89,11 @@ class RecurringExpenseDatatable extends EntityDatatable
|
||||
'category',
|
||||
function ($model) {
|
||||
$category = $model->category != null ? substr($model->category, 0, 100) : '';
|
||||
if (! Auth::user()->can('editByOwner', [ENTITY_EXPENSE_CATEGORY, $model->category_user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_EXPENSE_CATEGORY, $model]))
|
||||
return $model->category_public_id ? link_to("expense_categories/{$model->category_public_id}/edit", $category)->toHtml() : '';
|
||||
else
|
||||
return $category;
|
||||
}
|
||||
|
||||
return $model->category_public_id ? link_to("expense_categories/{$model->category_public_id}/edit", $category)->toHtml() : '';
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -113,7 +114,7 @@ class RecurringExpenseDatatable extends EntityDatatable
|
||||
return URL::to("recurring_expenses/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_RECURRING_EXPENSE, $model->user_id]);
|
||||
return Auth::user()->can('view', [ENTITY_RECURRING_EXPENSE, $model]);
|
||||
},
|
||||
],
|
||||
];
|
||||
|
@ -101,7 +101,7 @@ class RecurringInvoiceDatatable extends EntityDatatable
|
||||
return URL::to("invoices/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->user_id]);
|
||||
return Auth::user()->can('view', [ENTITY_INVOICE, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
|
@ -19,42 +19,42 @@ class TaskDatatable extends EntityDatatable
|
||||
[
|
||||
'client_name',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_CLIENT, $model->client_user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_CLIENT, $model]))
|
||||
return $model->client_public_id ? link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml() : '';
|
||||
else
|
||||
return Utils::getClientDisplayName($model);
|
||||
}
|
||||
|
||||
return $model->client_public_id ? link_to("clients/{$model->client_public_id}", Utils::getClientDisplayName($model))->toHtml() : '';
|
||||
},
|
||||
! $this->hideClient,
|
||||
],
|
||||
[
|
||||
'project',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('editByOwner', [ENTITY_PROJECT, $model->project_user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_PROJECT, $model]))
|
||||
return $model->project_public_id ? link_to("projects/{$model->project_public_id}", $model->project)->toHtml() : '';
|
||||
else
|
||||
return $model->project;
|
||||
}
|
||||
|
||||
return $model->project_public_id ? link_to("projects/{$model->project_public_id}", $model->project)->toHtml() : '';
|
||||
},
|
||||
],
|
||||
[
|
||||
'date',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_EXPENSE, $model->user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_EXPENSE, $model]))
|
||||
return link_to("tasks/{$model->public_id}/edit", Task::calcStartTime($model))->toHtml();
|
||||
else
|
||||
return Task::calcStartTime($model);
|
||||
}
|
||||
|
||||
return link_to("tasks/{$model->public_id}/edit", Task::calcStartTime($model))->toHtml();
|
||||
},
|
||||
],
|
||||
[
|
||||
'duration',
|
||||
function ($model) {
|
||||
if (! Auth::user()->can('viewByOwner', [ENTITY_EXPENSE, $model->user_id])) {
|
||||
if (Auth::user()->can('view', [ENTITY_EXPENSE, $model]))
|
||||
return link_to("tasks/{$model->public_id}/edit", Utils::formatTime(Task::calcDuration($model)))->toHtml();
|
||||
else
|
||||
return Utils::formatTime(Task::calcDuration($model));
|
||||
}
|
||||
|
||||
return link_to("tasks/{$model->public_id}/edit", Utils::formatTime(Task::calcDuration($model)))->toHtml();
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -81,7 +81,7 @@ class TaskDatatable extends EntityDatatable
|
||||
return URL::to('tasks/'.$model->public_id.'/edit');
|
||||
},
|
||||
function ($model) {
|
||||
return (! $model->deleted_at || $model->deleted_at == '0000-00-00') && Auth::user()->can('editByOwner', [ENTITY_TASK, $model->user_id]);
|
||||
return (! $model->deleted_at || $model->deleted_at == '0000-00-00') && Auth::user()->can('view', [ENTITY_TASK, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -90,7 +90,7 @@ class TaskDatatable extends EntityDatatable
|
||||
return URL::to("/invoices/{$model->invoice_public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return $model->invoice_number && Auth::user()->can('editByOwner', [ENTITY_INVOICE, $model->invoice_user_id]);
|
||||
return $model->invoice_number && Auth::user()->can('view', [ENTITY_TASK, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -99,7 +99,7 @@ class TaskDatatable extends EntityDatatable
|
||||
return "javascript:submitForm_task('resume', {$model->public_id})";
|
||||
},
|
||||
function ($model) {
|
||||
return ! $model->is_running && Auth::user()->can('editByOwner', [ENTITY_TASK, $model->user_id]);
|
||||
return ! $model->is_running && Auth::user()->can('edit', [ENTITY_TASK, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -108,7 +108,7 @@ class TaskDatatable extends EntityDatatable
|
||||
return "javascript:submitForm_task('stop', {$model->public_id})";
|
||||
},
|
||||
function ($model) {
|
||||
return $model->is_running && Auth::user()->can('editByOwner', [ENTITY_TASK, $model->user_id]);
|
||||
return $model->is_running && Auth::user()->can('edit', [ENTITY_TASK, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -117,7 +117,7 @@ class TaskDatatable extends EntityDatatable
|
||||
return "javascript:submitForm_task('invoice', {$model->public_id})";
|
||||
},
|
||||
function ($model) {
|
||||
return ! $model->is_running && ! $model->invoice_number && (! $model->deleted_at || $model->deleted_at == '0000-00-00') && Auth::user()->can('create', ENTITY_INVOICE);
|
||||
return ! $model->is_running && ! $model->invoice_number && (! $model->deleted_at || $model->deleted_at == '0000-00-00') && Auth::user()->canCreateOrEdit(ENTITY_INVOICE);
|
||||
},
|
||||
],
|
||||
];
|
||||
|
@ -57,7 +57,7 @@ class VendorDatatable extends EntityDatatable
|
||||
return URL::to("vendors/{$model->public_id}/edit");
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_VENDOR, $model->user_id]);
|
||||
return Auth::user()->can('view', [ENTITY_VENDOR, $model]);
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -65,7 +65,7 @@ class VendorDatatable extends EntityDatatable
|
||||
return false;
|
||||
},
|
||||
function ($model) {
|
||||
return Auth::user()->can('editByOwner', [ENTITY_VENDOR, $model->user_id]) && Auth::user()->can('create', ENTITY_EXPENSE);
|
||||
return Auth::user()->can('edit', [ENTITY_VENDOR, $model]) && Auth::user()->can('create', ENTITY_EXPENSE);
|
||||
},
|
||||
|
||||
],
|
||||
|
29
app/Ninja/Import/Pancake/PaymentTransformer.php
Normal file
29
app/Ninja/Import/Pancake/PaymentTransformer.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Import\Pancake;
|
||||
|
||||
use App\Ninja\Import\BaseTransformer;
|
||||
use League\Fractal\Resource\Item;
|
||||
|
||||
/**
|
||||
* Class PaymentTransformer.
|
||||
*/
|
||||
class PaymentTransformer extends BaseTransformer
|
||||
{
|
||||
/**
|
||||
* @param $data
|
||||
*
|
||||
* @return Item
|
||||
*/
|
||||
public function transform($data)
|
||||
{
|
||||
return new Item($data, function ($data) {
|
||||
return [
|
||||
'amount' => (float) $data->amount_paid,
|
||||
'payment_date_sql' => $data->create_date,
|
||||
'client_id' => $data->client_id,
|
||||
'invoice_id' => $data->invoice_id,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
@ -76,7 +76,7 @@ class UserMailer extends Mailer
|
||||
Payment $payment = null,
|
||||
$notes = false
|
||||
) {
|
||||
if (! $user->email || $user->cannot('view', $invoice)) {
|
||||
if (! $user->shouldNotify($invoice)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -596,6 +596,7 @@ class BasePaymentDriver
|
||||
|
||||
$this->customer = AccountGatewayToken::clientAndGateway($clientId, $this->accountGateway->id)
|
||||
->with('payment_methods')
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
if ($this->customer && $this->invitation) {
|
||||
|
@ -37,6 +37,11 @@ class PaymentPresenter extends EntityPresenter
|
||||
return Carbon::parse($this->entity->payment_date)->format('Y m');
|
||||
}
|
||||
|
||||
public function payment_type()
|
||||
{
|
||||
return $this->entity->payment_type ? $this->entity->payment_type->name : trans('texts.manual_entry');
|
||||
}
|
||||
|
||||
public function method()
|
||||
{
|
||||
if ($this->entity->account_gateway) {
|
||||
|
@ -176,7 +176,7 @@ class AccountRepository
|
||||
$data[$account->present()->customLabel('client2')] = [];
|
||||
}
|
||||
|
||||
if ($user->hasPermission('view_all')) {
|
||||
if ($user->hasPermission(['view_client', 'view_invoice'], true)) {
|
||||
$clients = Client::scope()
|
||||
->with('contacts', 'invoices')
|
||||
->withTrashed()
|
||||
@ -234,6 +234,21 @@ class AccountRepository
|
||||
'tokens' => implode(',', [$invoice->invoice_number, $invoice->po_number]),
|
||||
'url' => $invoice->present()->url,
|
||||
];
|
||||
|
||||
if ($customValue = $invoice->custom_text_value1) {
|
||||
$data[$account->present()->customLabel('invoice_text1')][] = [
|
||||
'value' => "{$customValue}: {$invoice->getDisplayName()}",
|
||||
'tokens' => $customValue,
|
||||
'url' => $invoice->present()->url,
|
||||
];
|
||||
}
|
||||
if ($customValue = $invoice->custom_text_value2) {
|
||||
$data[$account->present()->customLabel('invoice_text2')][] = [
|
||||
'value' => "{$customValue}: {$invoice->getDisplayName()}",
|
||||
'tokens' => $customValue,
|
||||
'url' => $invoice->present()->url,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,6 +194,7 @@ class PaymentRepository extends BaseRepository
|
||||
if (! $publicId) {
|
||||
$clientId = $input['client_id'];
|
||||
$amount = Utils::parseFloat($input['amount']);
|
||||
$amount = min($amount, MAX_INVOICE_AMOUNT);
|
||||
|
||||
if ($paymentTypeId == PAYMENT_TYPE_CREDIT) {
|
||||
$credits = Credit::scope()->where('client_id', '=', $clientId)
|
||||
|
@ -3,6 +3,8 @@
|
||||
namespace App\Ninja\Repositories;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Events\ProductWasCreated;
|
||||
use App\Events\ProductWasUpdated;
|
||||
use Utils;
|
||||
use DB;
|
||||
|
||||
@ -72,6 +74,11 @@ class ProductRepository extends BaseRepository
|
||||
$product->qty = isset($data['qty']) ? Utils::parseFloat($data['qty']) : 1;
|
||||
$product->save();
|
||||
|
||||
if ($publicId) {
|
||||
event(new ProductWasUpdated($product, $data));
|
||||
} else {
|
||||
event(new ProductWasCreated($product, $data));
|
||||
}
|
||||
return $product;
|
||||
}
|
||||
|
||||
|
@ -29,14 +29,48 @@ class UserAccountTransformer extends EntityTransformer
|
||||
|
||||
public function transform(User $user)
|
||||
{
|
||||
$account = $user->account;
|
||||
|
||||
return [
|
||||
'account_key' => $user->account->account_key,
|
||||
'name' => $user->account->present()->name,
|
||||
'token' => $user->account->getToken($user->id, $this->tokenName),
|
||||
'account_key' => $account->account_key,
|
||||
'name' => $account->present()->name,
|
||||
'token' => $account->getToken($user->id, $this->tokenName),
|
||||
'default_url' => SITE_URL,
|
||||
'plan' => $user->account->company->plan,
|
||||
'logo' => $user->account->logo,
|
||||
'logo_url' => $user->account->getLogoURL(),
|
||||
'plan' => $account->company->plan,
|
||||
'logo' => $account->logo,
|
||||
'logo_url' => $account->getLogoURL(),
|
||||
'currency_id' => (int) $account->currency_id,
|
||||
'timezone_id' => (int) $account->timezone_id,
|
||||
'date_format_id' => (int) $account->date_format_id,
|
||||
'datetime_format_id' => (int) $account->datetime_format_id,
|
||||
'invoice_terms' => $account->invoice_terms,
|
||||
'invoice_taxes' => (bool) $account->invoice_taxes,
|
||||
'invoice_item_taxes' => (bool) $account->invoice_item_taxes,
|
||||
'invoice_design_id' => (int) $account->invoice_design_id,
|
||||
'quote_design_id' => (int) $account->quote_design_id,
|
||||
'language_id' => (int) $account->language_id,
|
||||
'invoice_footer' => $account->invoice_footer,
|
||||
'invoice_labels' => $account->invoice_labels,
|
||||
'show_item_taxes' => (bool) $account->show_item_taxes,
|
||||
'military_time' => (bool) $account->military_time,
|
||||
'tax_name1' => $account->tax_name1 ?: '',
|
||||
'tax_rate1' => (float) $account->tax_rate1,
|
||||
'tax_name2' => $account->tax_name2 ?: '',
|
||||
'tax_rate2' => (float) $account->tax_rate2,
|
||||
'quote_terms' => $account->quote_terms,
|
||||
'show_currency_code' => (bool) $account->show_currency_code,
|
||||
'enable_second_tax_rate' => (bool) $account->enable_second_tax_rate,
|
||||
'start_of_week' => $account->start_of_week,
|
||||
'financial_year_start' => $account->financial_year_start,
|
||||
'enabled_modules' => (int) $account->enabled_modules,
|
||||
'payment_terms' => (int) $account->payment_terms,
|
||||
'payment_type_id' => (int) $account->payment_type_id,
|
||||
'task_rate' => (float) $account->task_rate,
|
||||
'inclusive_taxes' => (bool) $account->inclusive_taxes,
|
||||
'convert_products' => (bool) $account->convert_products,
|
||||
'custom_invoice_taxes1' => $account->custom_invoice_taxes1,
|
||||
'custom_invoice_taxes2' => $account->custom_invoice_taxes1,
|
||||
'custom_fields' => $account->custom_fields,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ class DocumentPolicy extends EntityPolicy
|
||||
*/
|
||||
public static function view(User $user, $document)
|
||||
{
|
||||
if ($user->hasPermission('view_all')) {
|
||||
if ($user->hasPermission(['view_expense', 'view_invoice'], true)) {
|
||||
return true;
|
||||
}
|
||||
if ($document->expense) {
|
||||
|
@ -4,6 +4,7 @@ namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class EntityPolicy.
|
||||
@ -14,75 +15,97 @@ class EntityPolicy
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param mixed $item
|
||||
* @param $item - entity name or object
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
public static function create(User $user, $item)
|
||||
{
|
||||
if (! static::checkModuleEnabled($user, $item)) {
|
||||
if (! static::checkModuleEnabled($user, $item))
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->hasPermission('create_all');
|
||||
|
||||
$entityType = is_string($item) ? $item : $item->getEntityType();
|
||||
return $user->hasPermission('create_' . $entityType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param $item
|
||||
* @param $item - entity name or object
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
public static function edit(User $user, $item)
|
||||
{
|
||||
if (! static::checkModuleEnabled($user, $item)) {
|
||||
if (! static::checkModuleEnabled($user, $item))
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->hasPermission('edit_all') || $user->owns($item);
|
||||
|
||||
$entityType = is_string($item) ? $item : $item->getEntityType();
|
||||
return $user->hasPermission('edit_' . $entityType) || $user->owns($item);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param $item
|
||||
* @param $item - entity name or object
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
public static function view(User $user, $item)
|
||||
{
|
||||
if (! static::checkModuleEnabled($user, $item)) {
|
||||
if (! static::checkModuleEnabled($user, $item))
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->hasPermission('view_all') || $user->owns($item);
|
||||
$entityType = is_string($item) ? $item : $item->getEntityType();
|
||||
return $user->hasPermission('view_' . $entityType) || $user->owns($item);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param $ownerUserId
|
||||
*
|
||||
* Legacy permissions - retaining these for legacy code however new code
|
||||
* should use auth()->user()->can('view', $ENTITY_TYPE)
|
||||
*
|
||||
* $ENTITY_TYPE can be either the constant ie ENTITY_INVOICE, or the entity $object
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
public static function viewByOwner(User $user, $ownerUserId)
|
||||
{
|
||||
return $user->hasPermission('view_all') || $user->id == $ownerUserId;
|
||||
return $user->id == $ownerUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param $ownerUserId
|
||||
*
|
||||
* Legacy permissions - retaining these for legacy code however new code
|
||||
* should use auth()->user()->can('edit', $ENTITY_TYPE)
|
||||
*
|
||||
* $ENTITY_TYPE can be either the constant ie ENTITY_INVOICE, or the entity $object
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
public static function editByOwner(User $user, $ownerUserId)
|
||||
{
|
||||
return $user->hasPermission('edit_all') || $user->id == $ownerUserId;
|
||||
return $user->id == $ownerUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param $item - entity name or object
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
private static function checkModuleEnabled(User $user, $item)
|
||||
{
|
||||
$entityType = is_string($item) ? $item : $item->getEntityType();
|
||||
|
||||
return $user->account->isModuleEnabled($entityType);
|
||||
return $user->account->isModuleEnabled($entityType);
|
||||
}
|
||||
}
|
||||
|
@ -81,6 +81,37 @@ class GenericEntityPolicy
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param $item - entity name or object
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
public static function edit(User $user, $item)
|
||||
{
|
||||
if (! static::checkModuleEnabled($user, $item))
|
||||
return false;
|
||||
|
||||
|
||||
$entityType = is_string($item) ? $item : $item->getEntityType();
|
||||
return $user->hasPermission('edit_' . $entityType) || $user->owns($item);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param $item - entity name or object
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
private static function checkModuleEnabled(User $user, $item)
|
||||
{
|
||||
$entityType = is_string($item) ? $item : $item->getEntityType();
|
||||
return $user->account->isModuleEnabled($entityType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static function className($entityType)
|
||||
{
|
||||
if (! Utils::isNinjaProd()) {
|
||||
|
@ -65,7 +65,7 @@ class CreditService extends BaseService
|
||||
$datatable = new CreditDatatable(true, $clientPublicId);
|
||||
$query = $this->creditRepo->find($clientPublicId, $search);
|
||||
|
||||
if (! Utils::hasPermission('view_all')) {
|
||||
if (! Utils::hasPermission('view_credit')) {
|
||||
$query->where('credits.user_id', '=', Auth::user()->id);
|
||||
}
|
||||
|
||||
|
@ -26,8 +26,8 @@ class DatatableService
|
||||
$table = Datatable::query($query);
|
||||
|
||||
if ($datatable->isBulkEdit) {
|
||||
$table->addColumn('checkbox', function ($model) {
|
||||
$can_edit = Auth::user()->hasPermission('edit_all') || (isset($model->user_id) && Auth::user()->id == $model->user_id);
|
||||
$table->addColumn('checkbox', function ($model) use ($datatable) {
|
||||
$can_edit = Auth::user()->hasPermission('edit_' . $datatable->entityType) || (isset($model->user_id) && Auth::user()->id == $model->user_id);
|
||||
|
||||
return ! $can_edit ? '' : '<input type="checkbox" name="ids[]" value="' . $model->public_id
|
||||
. '" ' . Utils::getEntityRowClass($model) . '>';
|
||||
@ -65,7 +65,7 @@ class DatatableService
|
||||
$hasAction = false;
|
||||
$str = '<center style="min-width:100px">';
|
||||
|
||||
$can_edit = Auth::user()->hasPermission('edit_all') || (isset($model->user_id) && Auth::user()->id == $model->user_id);
|
||||
$can_edit = Auth::user()->hasPermission('edit_' . $datatable->entityType) || (isset($model->user_id) && Auth::user()->id == $model->user_id);
|
||||
|
||||
if (property_exists($model, 'is_deleted') && $model->is_deleted) {
|
||||
$str .= '<button type="button" class="btn btn-sm btn-danger tr-status">'.trans('texts.deleted').'</button>';
|
||||
|
@ -72,7 +72,7 @@ class ExpenseService extends BaseService
|
||||
{
|
||||
$query = $this->expenseRepo->find($search);
|
||||
|
||||
if (! Utils::hasPermission('view_all')) {
|
||||
if (! Utils::hasPermission('view_expense')) {
|
||||
$query->where('expenses.user_id', '=', Auth::user()->id);
|
||||
}
|
||||
|
||||
@ -90,7 +90,25 @@ class ExpenseService extends BaseService
|
||||
|
||||
$query = $this->expenseRepo->findVendor($vendorPublicId);
|
||||
|
||||
if (! Utils::hasPermission('view_all')) {
|
||||
if (! Utils::hasPermission('view_vendor')) {
|
||||
$query->where('expenses.user_id', '=', Auth::user()->id);
|
||||
}
|
||||
|
||||
return $this->datatableService->createDatatable($datatable, $query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $clientPublicId
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getDatatableClient($clientPublicId)
|
||||
{
|
||||
$datatable = new ExpenseDatatable(true, true);
|
||||
|
||||
$query = $this->expenseRepo->findClient($clientPublicId);
|
||||
|
||||
if (! Utils::hasPermission('view_client')) {
|
||||
$query->where('expenses.user_id', '=', Auth::user()->id);
|
||||
}
|
||||
|
||||
|
@ -163,7 +163,7 @@ class InvoiceService extends BaseService
|
||||
$query = $this->invoiceRepo->getInvoices($accountId, $clientPublicId, $entityType, $search)
|
||||
->where('invoices.invoice_type_id', '=', $entityType == ENTITY_QUOTE ? INVOICE_TYPE_QUOTE : INVOICE_TYPE_STANDARD);
|
||||
|
||||
if (! Utils::hasPermission('view_all')) {
|
||||
if (! Utils::hasPermission('view_invoice')) {
|
||||
$query->where('invoices.user_id', '=', Auth::user()->id);
|
||||
}
|
||||
|
||||
|
@ -174,7 +174,7 @@ class PaymentService extends BaseService
|
||||
$datatable = new PaymentDatatable(true, $clientPublicId);
|
||||
$query = $this->paymentRepo->find($clientPublicId, $search);
|
||||
|
||||
if (! Utils::hasPermission('view_all')) {
|
||||
if (! Utils::hasPermission('view_payment')) {
|
||||
$query->where('payments.user_id', '=', Auth::user()->id);
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,7 @@ class ProductService extends BaseService
|
||||
$datatable = new ProductDatatable(true);
|
||||
$query = $this->productRepo->find($accountId, $search);
|
||||
|
||||
if (! Utils::hasPermission('view_all')) {
|
||||
if (! Utils::hasPermission('view_product')) {
|
||||
$query->where('products.user_id', '=', Auth::user()->id);
|
||||
}
|
||||
|
||||
|
@ -73,7 +73,7 @@ class RecurringExpenseService extends BaseService
|
||||
{
|
||||
$query = $this->recurringExpenseRepo->find($search);
|
||||
|
||||
if (! Utils::hasPermission('view_all')) {
|
||||
if (! Utils::hasPermission('view_expense')) {
|
||||
$query->where('recurring_expenses.user_id', '=', Auth::user()->id);
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ class RecurringInvoiceService extends BaseService
|
||||
$datatable = new RecurringInvoiceDatatable(true, $clientPublicId);
|
||||
$query = $this->invoiceRepo->getRecurringInvoices($accountId, $clientPublicId, $search);
|
||||
|
||||
if (! Utils::hasPermission('view_all')) {
|
||||
if (! Utils::hasPermission('view_recurring_invoice')) {
|
||||
$query->where('invoices.user_id', '=', Auth::user()->id);
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ class TaskService extends BaseService
|
||||
|
||||
$query = $this->taskRepo->find($clientPublicId, $projectPublicId, $search);
|
||||
|
||||
if (! Utils::hasPermission('view_all')) {
|
||||
if (! Utils::hasPermission('view_task')) {
|
||||
$query->where('tasks.user_id', '=', Auth::user()->id);
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,7 @@ class VendorService extends BaseService
|
||||
$datatable = new VendorDatatable();
|
||||
$query = $this->vendorRepo->find($search);
|
||||
|
||||
if (! Utils::hasPermission('view_all')) {
|
||||
if (! Utils::hasPermission('view_vendor')) {
|
||||
$query->where('vendors.user_id', '=', Auth::user()->id);
|
||||
}
|
||||
|
||||
|
@ -173,4 +173,7 @@ return [
|
||||
'register' => [
|
||||
'translations' => true,
|
||||
],
|
||||
'relations' => [
|
||||
// all dynamic relations registered from modules are added here
|
||||
],
|
||||
];
|
||||
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class LimitNotifications extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function ($table) {
|
||||
$table->boolean('only_notify_owned')->nullable()->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
114
database/migrations/2018_05_19_095124_add_json_permissions.php
Normal file
114
database/migrations/2018_05_19_095124_add_json_permissions.php
Normal file
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use App\Models\User;
|
||||
class AddJsonPermissions extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function ($table) {
|
||||
$table->longtext('permissionsV2');
|
||||
});
|
||||
$users = User::where('permissions', '!=', 0)->get();
|
||||
foreach($users as $user) {
|
||||
$user->permissionsV2 = self::returnFormattedPermissions($user->permissions);
|
||||
$user->save();
|
||||
}
|
||||
|
||||
|
||||
Schema::table('users', function ($table) {
|
||||
$table->dropColumn('permissions');
|
||||
});
|
||||
|
||||
Schema::table('users', function($table)
|
||||
{
|
||||
$table->renameColumn('permissionsV2', 'permissions');
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('users', function ($table) {
|
||||
$table->dropColumn('permissionsV2');
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Transform permissions
|
||||
*
|
||||
* @return json_array
|
||||
*/
|
||||
public function returnFormattedPermissions($userPermission) {
|
||||
$viewPermissionEntities = [];
|
||||
$editPermissionEntities = [];
|
||||
$createPermissionEntities = [];
|
||||
|
||||
$permissionEntities = [
|
||||
'proposal',
|
||||
'expense',
|
||||
'project',
|
||||
'vendor',
|
||||
'product',
|
||||
'task',
|
||||
'quote',
|
||||
'credit',
|
||||
'payment',
|
||||
'contact',
|
||||
'invoice',
|
||||
'client',
|
||||
'recurring_invoice',
|
||||
'reports',
|
||||
];
|
||||
foreach($permissionEntities as $entity) {
|
||||
array_push($viewPermissionEntities, 'view_'.$entity);
|
||||
array_push($editPermissionEntities, 'edit_'.$entity);
|
||||
array_push($createPermissionEntities, 'create_'.$entity);
|
||||
}
|
||||
$returnPermissions = [];
|
||||
if(array_key_exists('create_all', self::getPermissions($userPermission)))
|
||||
$returnPermissions = array_merge($returnPermissions, $createPermissionEntities);
|
||||
if(array_key_exists('edit_all', self::getPermissions($userPermission)))
|
||||
$returnPermissions = array_merge($returnPermissions, $editPermissionEntities);
|
||||
if(array_key_exists('view_all', self::getPermissions($userPermission)))
|
||||
$returnPermissions = array_merge($returnPermissions, $viewPermissionEntities);
|
||||
return json_encode($returnPermissions);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Expands the value of the permissions attribute.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getPermissions($value)
|
||||
{
|
||||
$permissions = [];
|
||||
foreach (static::$all_permissions as $permission => $bitmask) {
|
||||
if (($value & $bitmask) == $bitmask) {
|
||||
$permissions[$permission] = $permission;
|
||||
}
|
||||
}
|
||||
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public static $all_permissions = [
|
||||
'create_all' => 0b0001,
|
||||
'view_all' => 0b0010,
|
||||
'edit_all' => 0b0100,
|
||||
];
|
||||
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -162,6 +162,9 @@ class CountriesSeeder extends Seeder
|
||||
'thousand_separator' => ',',
|
||||
'decimal_separator' => '.',
|
||||
],
|
||||
'SR' => [ // Suriname
|
||||
'swap_currency_symbol' => true,
|
||||
],
|
||||
'UY' => [
|
||||
'swap_postal_code' => true,
|
||||
],
|
||||
|
@ -85,6 +85,7 @@ class CurrenciesSeeder extends Seeder
|
||||
['name' => 'Georgian Lari', 'code' => 'GEL', 'symbol' => '', 'precision' => '2', 'thousand_separator' => ' ', 'decimal_separator' => ','],
|
||||
['name' => 'Qatari Riyal', 'code' => 'QAR', 'symbol' => 'QR', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
||||
['name' => 'Honduran Lempira', 'code' => 'HNL', 'symbol' => 'L', 'precision' => '2', 'thousand_separator' => ',', 'decimal_separator' => '.'],
|
||||
['name' => 'Surinamese Dollar', 'code' => 'SRD', 'symbol' => 'SRD', 'precision' => '2', 'thousand_separator' => '.', 'decimal_separator' => ','],
|
||||
];
|
||||
|
||||
foreach ($currencies as $currency) {
|
||||
|
@ -65,6 +65,21 @@ class UserTableSeeder extends Seeder
|
||||
'accepted_terms_version' => NINJA_TERMS_VERSION,
|
||||
]);
|
||||
|
||||
$permissionsUser = User::create([
|
||||
'first_name' => $faker->firstName,
|
||||
'last_name' => $faker->lastName,
|
||||
'email' => TEST_PERMISSIONS_USERNAME,
|
||||
'username' => TEST_PERMISSIONS_USERNAME,
|
||||
'account_id' => $account->id,
|
||||
'password' => Hash::make(TEST_PASSWORD),
|
||||
'registered' => true,
|
||||
'confirmed' => true,
|
||||
'notify_sent' => false,
|
||||
'notify_paid' => false,
|
||||
'is_admin' => 0,
|
||||
'accepted_terms_version' => NINJA_TERMS_VERSION,
|
||||
]);
|
||||
|
||||
$client = Client::create([
|
||||
'user_id' => $user->id,
|
||||
'account_id' => $account->id,
|
||||
|
File diff suppressed because one or more lines are too long
@ -57,9 +57,9 @@ author = u'Invoice Ninja'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = u'4.4'
|
||||
version = u'4.5'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = u'4.4.4'
|
||||
release = u'4.5.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
@ -73,6 +73,7 @@ Troubleshooting
|
||||
- If you require contacts to enter a password to see their invoice you'll need to set a random value for ``PHANTOMJS_SECRET``.
|
||||
- If you're using a proxy and/or self-signed certificate `this comment <https://github.com/invoiceninja/dockerfiles/issues/39#issuecomment-282489039>`_ may help.
|
||||
- If you're using a custom design try using a standard one, if the PDF is outside the printable area it can fail.
|
||||
- If you're using a non-English language try changing to English.
|
||||
|
||||
Custom Fonts
|
||||
""""""""""""
|
||||
@ -129,10 +130,10 @@ Lock Invoices
|
||||
|
||||
Adding ``LOCK_SENT_INVOICES=true`` to the .env file will prevent changing an invoice once it has been sent.
|
||||
|
||||
Using a Proxy
|
||||
Using a (Reverse) Proxy
|
||||
"""""""""""""
|
||||
|
||||
If you need to set a list of trusted proxies you can add a TRUSTED_PROXIES value in the .env file. ie,
|
||||
If you need to set a list of trusted (reverse) proxies you can add a TRUSTED_PROXIES value in the .env file. ie,
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
|
@ -45,9 +45,9 @@ To run the database migration use:
|
||||
php artisan module:migrate <module>
|
||||
|
||||
|
||||
.. Tip:: You can specify the module icon by setting a value from http://fontawesome.io/icons/ for "icon" in modules.json.
|
||||
.. Tip:: You can specify the module icon by setting a value from http://fontawesome.io/icons/ for "icon" in module.json.
|
||||
|
||||
There are two types of modules: you can either create a standard module which displays a list of a new entity type or you can create a blank module which adds functionality. For example, a custom integration with a third-party app.
|
||||
There are two types of modules: you can either create a standard module which displays a list of a new entity type or you can create a blank module which adds functionality. For example, a custom integration with a third-party app. If you do not want an entry in the application navigation sidebar, add "no-sidebar": 1 to the custom module's module.json.
|
||||
|
||||
If you're looking for a module to work on you can see suggested issues `listed here <https://github.com/invoiceninja/invoiceninja/issues?q=is%3Aissue+is%3Aopen+label%3A%22custom+module%22>`_.
|
||||
|
||||
|
@ -16,6 +16,7 @@ Want to find out everything there is to know about how to use your Invoice Ninja
|
||||
recurring_invoices
|
||||
credits
|
||||
quotes
|
||||
proposals
|
||||
tasks
|
||||
expenses
|
||||
vendors
|
||||
|
@ -16,8 +16,8 @@ Detailed Guides
|
||||
|
||||
- CentOS and Nginx: `thishosting.rocks <https://thishosting.rocks/how-to-install-invoice-ninja-on-centos/>`_
|
||||
|
||||
Automated Installers
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
Automatic Install/Update
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
- Ansible: `github.com <https://github.com/invoiceninja/ansible-installer>`_
|
||||
|
||||
@ -29,8 +29,8 @@ Automated Installers
|
||||
|
||||
.. Tip:: You can use `github.com/turbo124/Plane2Ninja <https://github.com/turbo124/Plane2Ninja>`_ to migrate your data from InvoicePlane.
|
||||
|
||||
Steps to Install
|
||||
^^^^^^^^^^^^^^^^
|
||||
Manual Install
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
Step 1: Download the code
|
||||
"""""""""""""""""""""""""
|
||||
@ -74,9 +74,14 @@ See the guides listed above for detailed information on configuring Apache or Ng
|
||||
|
||||
Once you can access the site the initial setup screen will enable you to configure the database and email settings as well as create the initial admin user.
|
||||
|
||||
.. Tip:: To remove public/ from the URL map the webroot to the /public folder, alternatively you can uncomment ``RewriteRule ^(.*)$ public/$1 [L]`` in the .htaccess file.
|
||||
.. Tip:: To remove public/ from the URL map the webroot to the /public folder, alternatively you can uncomment ``RewriteRule ^(.*)$ public/$1 [L]`` in the .htaccess file. There is more info `here <https://www.invoiceninja.com/forums/topic/clean-4-4-3-self-hosted-install-url-configuration-clarification/#post-14186>`_.
|
||||
|
||||
Step 5: Enable auto updates
|
||||
Step 5: Configure the application
|
||||
"""""""""""""""""""""""""""""""""
|
||||
|
||||
See the `details here <http://docs.invoiceninja.com/en/latest/configure.html>`_ for additional configuration options.
|
||||
|
||||
Step 6: Enable auto updates
|
||||
"""""""""""""""""""""""""""
|
||||
|
||||
Use this `shell script <https://pastebin.com/j657uv9A>`_ to automate the update process.
|
||||
|
243
docs/proposals.rst
Normal file
243
docs/proposals.rst
Normal file
@ -0,0 +1,243 @@
|
||||
Proposals
|
||||
=========
|
||||
|
||||
When submitting a regular quote is not enough, the advanced Proposals feature comes to the rescue.
|
||||
|
||||
Proposals are a powerful sales and marketing tool to present your offer for a gig. Whether you're competing against other providers, or you want to make a fantastic impression, the proposal function is a great way to custom-create a personalized proposal containing all the information you need to convey.
|
||||
|
||||
The proposals feature is based on grapesjs.com, which enables HTML and CSS management. This advanced tool gives experienced programmers the freedom to create customized, professional proposals with ease, directly within Invoice Ninja, integrating your quote information with one click.
|
||||
|
||||
There are three key parts to creating proposals: Proposals, Templates and Snippets. Let's explore them one by one.
|
||||
|
||||
Proposals
|
||||
"""""""""
|
||||
|
||||
Each proposal is based on a quote. In order to create a proposal, you'll first need to create a quote.
|
||||
|
||||
To create a quote, click on New Quote on the Quotes list page, or click the + sign on the Quotes tab in the main sidebar. Once you've created the quote, go to the Proposals list page by clicking on the Proposals tab in the main sidebar.
|
||||
|
||||
Proposals List Page
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The Proposals list page features a table of all active proposals and corresponding information.
|
||||
|
||||
The table consists of the following data columns:
|
||||
|
||||
- **Quote**: Every proposal is based on a quote. This is the quote number of the corresponding quote.
|
||||
- **Client**: The client's name.
|
||||
- **Template**: The template assigned to the proposal.
|
||||
- **Date created**: The date the proposal was created.
|
||||
- **Content**: A short preview of the content/topic of the proposal.
|
||||
- **Private notes**: Any notes to yourself included in the proposal (these are hidden from the client; only you can see them).
|
||||
- **Action column**: The action column has a drop-down menu with the option to Edit, Archive or Delete the proposal. To select an action, hover in the action column and click to open the drop-down menu.
|
||||
|
||||
Archive / Delete Proposal
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you wish to archive or delete a proposal, select the action in the column corresponding to the relevant quote. Once you select Archive or Delete, the proposal will be removed from the table.
|
||||
|
||||
.. TIP:: You can also archive a proposal by checking the box to the left of the quote number of the relevant proposal. Then click the Archive button at the top left of the Proposals table.
|
||||
|
||||
By default, the Proposals list page will display only active proposals in the table. To view archived or deleted proposals, you need to update the list in the labels field, situated at the top left of the Proposals table, to the right of the gray Archive button.
|
||||
|
||||
Click inside the labels field to open the drop-down menu. Then, select Archived and/or Deleted from the menu. The table will automatically refresh to display Archived/Deleted proposals. Archived proposals will display an orange "Archived" label and Deleted proposals will display a red "Deleted" label in the action column.
|
||||
|
||||
**To restore an Archived or Deleted Proposal**
|
||||
|
||||
First, display the proposal by updating the table to view Archived or Deleted proposals. Then, open the drop-down menu in the action column of the relevant proposal. Click Restore proposal.
|
||||
|
||||
New Proposal
|
||||
^^^^^^^^^^^^
|
||||
|
||||
To create a new proposal, click the blue New Proposal + button located at the top right of the Proposals list page. The Proposals/ Create page will open.
|
||||
|
||||
.. TIP:: You can create a new proposal from anywhere in the site by clicking the + icon on the Proposals tab on the static sidebar menu at the left of the screen.
|
||||
|
||||
Before beginning to work on the proposal design, you'll need to complete the fields in the top section of the page:
|
||||
|
||||
- **Quote**: A proposal must be based on an existing quote. To select the quote, click the arrow in the Quote field, and choose the relevant quote from the drop-down menu.
|
||||
- **Template**: Select the template you wish to use for the proposal by clicking the arrow in the Template field. Choose the relevant template from the drop-down menu.
|
||||
- **Private notes**: This is an optional field. You can enter notes and comments. They are for your eyes only; all private notes remain hidden from the client.
|
||||
|
||||
Now you're ready to design the proposal. You'll be working on the 'canvas' with the proposal editor, rich in design tools to custom create attractive, professional-looking proposals.
|
||||
|
||||
The proposal layout is based on the template you choose. Any information contained in the corresponding quote will be automatically embedded in the proposal.
|
||||
|
||||
Let's explore the proposal editor toolbar, from right to left:
|
||||
|
||||
- **Grid Editor**: When you create a new proposal, the toolbar will be set by default to the far-right icon – the grid editor. Proposals are built on a grid-like canvas. To insert sections, text blocks, icons, links and more, drag and drop the item from the selection that appears in the grid editor, in the sidebar at the right of the canvas.
|
||||
- **Options button**: Navigation menu that takes you through each design element in the proposal.
|
||||
- **Component Settings**: Displays the component ID that gives identifying information about the component.
|
||||
- **Style Manager**: Provides style information and editing options for each design element – Dimensions, Typography and Decorations.
|
||||
- **Undo/ Redo buttons**: You can undo or redo your last action by clicking on these buttons.
|
||||
- **Toggle Images**: Turn images in your proposal on or off.
|
||||
- **Import Template**: Import an external template to apply to the proposal.
|
||||
- **View Code**: Click to open a window that displays the proposal's code.
|
||||
- **FullScreen**: Click to work in full screen mode. Click again to return to normal screen.
|
||||
- **View Components**: Click to display all the components of the proposal.
|
||||
|
||||
View Modes
|
||||
^^^^^^^^^^
|
||||
|
||||
At the top left of the proposal canvas, there is a view mode bar that allows you to view the proposal in desktop, tablet or mobile size. Click on the desktop, tablet or mobile icon to choose the view mode.
|
||||
|
||||
When You Finish Working on the Proposal
|
||||
|
||||
When you've finished designing the proposal, you can choose from three options:
|
||||
|
||||
- **Cancel**: Don't like your work? Don't need the proposal after all? Click the gray Cancel button to discard the proposal.
|
||||
- **Save**: Save a draft to work on or send later by clicking on the green Save button. The proposal will appear in the Proposals table on the Proposals list page.
|
||||
- **Email**: If you're ready to present the proposal to your client, click the orange Email button. The proposal will be sent to the client's email address.
|
||||
|
||||
Templates
|
||||
"""""""""
|
||||
|
||||
The Proposals feature includes 10 templates (coming soon) to choose from. Templates enable you to quickly apply standard layout and design features, saving time and making the proposal creation process more efficient.
|
||||
|
||||
You can also custom design your own templates, from scratch or based on an existing template.
|
||||
|
||||
Templates List page
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
All existing templates are listed in the Templates table, on the Templates list page. To open the Templates list page, click the gray Templates button that appears on the Proposals list page at the top of the Proposals table.
|
||||
|
||||
The Templates list page displays a table with the following columns:
|
||||
|
||||
- **Name**: The name of the template.
|
||||
- **Content**: A preview of the template content.
|
||||
- **Private notes**: Any notes to yourself about the template (these are hidden from the client; only you can see them).
|
||||
|
||||
Action column: The action column has a drop-down menu with a number of options:
|
||||
|
||||
- **Edit Template**: Click to open the Templates/ Edit page.
|
||||
- **Clone Template**: Click to duplicate the template and create a new one.
|
||||
- **New Proposal**: Click to create a new proposal. You'll automatically go to the Proposals/ Create page.
|
||||
|
||||
Archive Template/ Delete Template: Select the relevant action to archive or delete a template. Once you select Archive or Delete, the template will be removed from the table.
|
||||
|
||||
.. TIP:: You can also archive a template by checking the box to the left of the relevant template name. Then click the Archive button at the top left of the Templates table.
|
||||
|
||||
To select an action, hover in the action column and click to open the drop-down menu.
|
||||
|
||||
By default, the Templates list page will display only active templates in the table. To view archived or deleted templates, you need to update the list in the labels field, situated at the top left of the Templates table, to the right of the gray Archive button.
|
||||
|
||||
Click inside the labels field to open the drop-down menu. Then, select Archived and/or Deleted from the menu. The table will automatically refresh to display Archived/Deleted templates. Archived templates will display an orange "Archived" label and Deleted templates will display a red "Deleted" label in the action column.
|
||||
|
||||
**To restore an Archived or Deleted Template**
|
||||
|
||||
First, display the template by updating the table to view Archived or Deleted templates. Then, open the drop-down menu of the action column of the relevant template. Click Restore template.
|
||||
|
||||
New Template
|
||||
^^^^^^^^^^^^
|
||||
|
||||
To create a new template, go to the Proposals list page. Click the arrow on the gray Templates button, which is situated at the top of the Proposals table. Select New Template from the drop-down menu. The Proposals/ Templates/ Create page will open.
|
||||
|
||||
First, complete the fields at the top part of the page:
|
||||
|
||||
- **Name**: Choose a template and enter it in the name field.
|
||||
- **Private notes**: This is an optional field. You can enter notes and comments. They are for your eyes only; all private notes remain hidden from the client.
|
||||
|
||||
Then, you can begin work designing the template on the canvas.
|
||||
|
||||
If you want to load an existing template to work from, click the Load Template field, located above the template canvas. A drop-down menu will open. Select the template you wish to load.
|
||||
|
||||
.. NOTE:: If you add a custom template, the Clean template will be removed. You can add it back by creating a custom template based on the Clean template.
|
||||
|
||||
- **Help**: Need help designing your template? Click the gray Help button.
|
||||
- **Cancel**: To cancel your new template, click the gray Cancel button. The work you've done so far will NOT be saved.
|
||||
- **Save**: To save the template, click the green Save button. The template will appear in the table on the Templates list page.
|
||||
|
||||
Snippets
|
||||
""""""""
|
||||
|
||||
Snippets are pre-defined content elements that you can create and reuse in your proposals over and over. Instead of designing parts of your proposal every time from scratch, you can save snippets, which you can then insert in any proposal with just a click. This saves you tons of time and effort, so you can create proposals faster. For example, you may want to include a short bio about yourself in every proposal. Create a snippet of your bio, and add it to proposals anywhere, anytime you want.
|
||||
|
||||
When you create a snippet, it will appear in the right sidebar in the proposal editor.
|
||||
|
||||
Snippets List page
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
All existing snippets are listed in the Snippets table, on the Snippets list page. To open the Snippets list page, click the gray Snippets button that appears on the Proposals list page at the top of the Proposals table.
|
||||
|
||||
The Snippets list page displays a table with the following columns:
|
||||
|
||||
- **Name**: The name of the snippet.
|
||||
- **Category**: The category the snippet belongs to.
|
||||
- **Content**: A preview of the snippet content.
|
||||
- **Private notes**: Any notes to yourself about the snippet (these are hidden from the client; only you can see them).
|
||||
|
||||
Action column - The action column has a drop-down menu with a number of options:
|
||||
|
||||
- **Edit Snippet**: Click to open the Proposals/ Snippets/ Edit page.
|
||||
|
||||
Archive Snippet/ Delete Snippet: Select the relevant action to archive or delete a snippet. Once you select Archive or Delete, the snippet will be removed from the table.
|
||||
|
||||
.. TIP:: You can also archive a snippet by checking the box to the left of the relevant snippet name in the table. Then click the Archive button at the top left of the Snippets table.
|
||||
|
||||
To select an action, hover in the action column and click to open the drop-down menu.
|
||||
|
||||
By default, the Snippets list page will display only active snippets in the table. To view archived or deleted snippets, you need to update the list in the labels field, situated at the top left of the Snippets table, to the right of the gray Archive button.
|
||||
|
||||
Click inside the labels field to open the drop-down menu. Then, select Archived and/or Deleted from the menu. The table will automatically refresh to display Archived/Deleted snippets. Archived snippets will display an orange "Archived" label and Deleted snippets will display a red "Deleted" label in the action column.
|
||||
|
||||
**To restore an Archived or Deleted Snippet**
|
||||
|
||||
First, display the snippet by updating the table to view Archived or Deleted snippets. Then, open the drop-down menu of the action column of the relevant snippet. Click Restore snippet.
|
||||
|
||||
New Snippet
|
||||
^^^^^^^^^^^
|
||||
|
||||
To create a new snippet, go to the Proposals list page. Click the arrow on the gray Snippets button, which is situated at the top of the Proposals table. Select New Snippet from the drop-down menu. The Proposals/ Snippets/ Create page will open.
|
||||
|
||||
First, complete the fields at the top part of the page:
|
||||
|
||||
- **Name**: Choose a name for the snippet and enter it in the name field.
|
||||
- **Category**: Choose a category for the snippet and enter it in the category field.
|
||||
- **Icon**: Choose an icon for the snippet from the selection available in the icon drop-down menu.
|
||||
- **Private notes**: This is an optional field. You can enter notes and comments. They are for your eyes only; all private notes remain hidden from the client.
|
||||
|
||||
Then, you can begin work designing the snippet on the canvas.
|
||||
- **Help**: Need help designing your snippet? Click the gray Help button.
|
||||
- **Cancel**: To cancel your new snippet, click the gray Cancel button. The work you've done so far will NOT be saved.
|
||||
- **Save**: To save the snippet, click the green Save button. The snippet will appear in the table on the Snippets list page.
|
||||
|
||||
Categories
|
||||
""""""""""
|
||||
|
||||
Arranging your snippets into categories can help you keep them organized and logical – which means you'll work faster to get your proposals ready.
|
||||
|
||||
You can create new categories and view the Categories list page from the Snippets list page.
|
||||
|
||||
**To view the Categories page**
|
||||
|
||||
Click the gray Categories button at the top of the Snippets list page.
|
||||
|
||||
Categories list page
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
All existing categories will appear in a table on the Categories list page.
|
||||
|
||||
The table includes a Name column, and an action column.
|
||||
|
||||
In the action column, you can edit, archive and delete categories.
|
||||
|
||||
New Category
|
||||
^^^^^^^^^^^^
|
||||
|
||||
To create a new category, go to the Snippets list page. Click the arrow on the gray Categories button, which is situated at the top of the Snippets table. Select New Category from the drop-down menu. The Proposals/ Categories/ Create page will open.
|
||||
|
||||
To create a category, enter a name for the category. Click the green Save button.
|
||||
|
||||
**To Edit/ Archive/ Delete a Category**
|
||||
|
||||
Click on the action column of the relevant category on the Categories list page and select the action from the drop-down menu. You can also archive a category by checking the box to the left of the Name column and clicking the gray Archive button at the top left of the Categories table.
|
||||
|
||||
**To restore an Archived or Deleted Category**
|
||||
|
||||
First, display the category by updating the table to view Archived or Deleted categories. You can do this by selecting the Archived/Deleted labels in the labels field, to the right of the gray Archive button above the Categories table. Then, open the drop-down menu of the action column of the relevant category. Click Restore category.
|
||||
|
||||
.. TIP:: You can filter and sort data about your Proposals, Templates, Snippets and Categories on the list pages for each.
|
||||
|
||||
To filter data, enter keywords in the Filter field, located at the top of the list page. The data in the table will filter automatically according to your keywords.
|
||||
|
||||
To sort data by column, click on the column you wish to sort. A white arrow will appear in the column header. An arrow pointing down sorts the data in order from highest to lowest. Click the arrow to reverse the sort order.
|
@ -1,7 +1,7 @@
|
||||
Update
|
||||
======
|
||||
|
||||
.. NOTE:: We recommend backing up your database before updating the app.
|
||||
.. NOTE:: We recommend backing up your database with mysqldump before updating the app.
|
||||
|
||||
To update the app you just need to copy over the latest code. The app tracks the current version in a file called version.txt, if it notices a change it loads ``/update`` to run the database migrations.
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -15,8 +15,9 @@ NINJA.TEMPLATES = {
|
||||
|
||||
function GetPdfMake(invoice, javascript, callback) {
|
||||
var itemsTable = false;
|
||||
|
||||
// check if we need to add a second table for tasks
|
||||
if (invoice.hasTasks) {
|
||||
// check if we need to add a second table for tasks
|
||||
if (invoice.hasSecondTable) {
|
||||
var json = JSON.parse(javascript);
|
||||
for (var i=0; i<json.content.length; i++) {
|
||||
@ -36,6 +37,16 @@ function GetPdfMake(invoice, javascript, callback) {
|
||||
javascript = javascript.replace('$invoiceLineItems', '$taskLineItems');
|
||||
javascript = javascript.replace('$invoiceLineItemColumns', '$taskLineItemColumns');
|
||||
}
|
||||
} else if (invoice.is_statement) {
|
||||
var json = JSON.parse(javascript);
|
||||
for (var i=0; i<json.content.length; i++) {
|
||||
var item = json.content[i];
|
||||
if (item.table && item.table.body == '$invoiceLineItems') {
|
||||
json.content.splice(i, 2);
|
||||
json.content.splice(i, 0, "$statementDetails");
|
||||
}
|
||||
}
|
||||
javascript = JSON.stringify(json);
|
||||
}
|
||||
|
||||
javascript = NINJA.decodeJavascript(invoice, javascript);
|
||||
@ -153,6 +164,7 @@ function GetPdfMake(invoice, javascript, callback) {
|
||||
// support setting noWrap as a style
|
||||
dd.styles.noWrap = {'noWrap': true};
|
||||
dd.styles.discount = {'alignment': 'right'};
|
||||
dd.styles.alignRight = {'alignment': 'right'};
|
||||
|
||||
// set page size
|
||||
dd.pageSize = invoice.account.page_size;
|
||||
@ -244,18 +256,19 @@ NINJA.decodeJavascript = function(invoice, javascript)
|
||||
'accountAddress': NINJA.accountAddress(invoice),
|
||||
'invoiceDetails': NINJA.invoiceDetails(invoice),
|
||||
'invoiceDetailsHeight': (NINJA.invoiceDetails(invoice).length * 16) + 16,
|
||||
'invoiceLineItems': invoice.is_statement ? NINJA.statementLines(invoice) : NINJA.invoiceLines(invoice),
|
||||
'invoiceLineItemColumns': invoice.is_statement ? NINJA.statementColumns(invoice) : NINJA.invoiceColumns(invoice, javascript),
|
||||
'invoiceLineItems': NINJA.invoiceLines(invoice),
|
||||
'invoiceLineItemColumns': NINJA.invoiceColumns(invoice, javascript),
|
||||
'taskLineItems': NINJA.invoiceLines(invoice, true),
|
||||
'taskLineItemColumns': NINJA.invoiceColumns(invoice, javascript, true),
|
||||
'invoiceDocuments' : NINJA.invoiceDocuments(invoice),
|
||||
'quantityWidth': NINJA.quantityWidth(invoice),
|
||||
'taxWidth': NINJA.taxWidth(invoice),
|
||||
'clientDetails': NINJA.clientDetails(invoice),
|
||||
'statementDetails': NINJA.statementDetails(invoice),
|
||||
'notesAndTerms': NINJA.notesAndTerms(invoice),
|
||||
'subtotals': invoice.is_statement ? NINJA.statementSubtotals(invoice) : NINJA.subtotals(invoice),
|
||||
'subtotals': NINJA.subtotals(invoice),
|
||||
'subtotalsHeight': (NINJA.subtotals(invoice).length * 16) + 16,
|
||||
'subtotalsWithoutBalance': invoice.is_statement ? [[]] : NINJA.subtotals(invoice, true),
|
||||
'subtotalsWithoutBalance': NINJA.subtotals(invoice, true),
|
||||
'subtotalsBalance': NINJA.subtotalsBalance(invoice),
|
||||
'balanceDue': formatMoneyInvoice(invoice.balance_amount, invoice),
|
||||
'invoiceFooter': NINJA.invoiceFooter(invoice),
|
||||
@ -400,6 +413,191 @@ NINJA.decodeJavascript = function(invoice, javascript)
|
||||
return javascript;
|
||||
}
|
||||
|
||||
NINJA.statementDetails = function(invoice) {
|
||||
if (! invoice.is_statement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var data = {
|
||||
"stack": []
|
||||
};
|
||||
|
||||
var table = {
|
||||
"style": "invoiceLineItemsTable",
|
||||
"margin": [0, 20, 0, 16],
|
||||
"table": {
|
||||
"headerRows": 1,
|
||||
"widths": false,
|
||||
"body": false,
|
||||
},
|
||||
"layout": {
|
||||
"hLineWidth": "$notFirst:.5",
|
||||
"vLineWidth": "$none",
|
||||
"hLineColor": "#D8D8D8",
|
||||
"paddingLeft": "$amount:8",
|
||||
"paddingRight": "$amount:8",
|
||||
"paddingTop": "$amount:14",
|
||||
"paddingBottom": "$amount:14"
|
||||
}
|
||||
};
|
||||
|
||||
var subtotals = {
|
||||
"columns": [
|
||||
{
|
||||
"text": " ",
|
||||
"width": "60%",
|
||||
},
|
||||
{
|
||||
"table": {
|
||||
"widths": [
|
||||
"*",
|
||||
"40%"
|
||||
],
|
||||
"body": false,
|
||||
},
|
||||
"margin": [0, 0, 0, 16],
|
||||
"layout": {
|
||||
"hLineWidth": "$none",
|
||||
"vLineWidth": "$none",
|
||||
"paddingLeft": "$amount:34",
|
||||
"paddingRight": "$amount:8",
|
||||
"paddingTop": "$amount:4",
|
||||
"paddingBottom": "$amount:4"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
var hasPayments = false;
|
||||
var hasAging = false;
|
||||
var paymentTotal = 0;
|
||||
for (var i = 0; i < invoice.invoice_items.length; i++) {
|
||||
var item = invoice.invoice_items[i];
|
||||
if (item.invoice_item_type_id == 3) {
|
||||
paymentTotal += item.cost;
|
||||
hasPayments = true;
|
||||
} else if (item.invoice_item_type_id == 4) {
|
||||
hasAging = true;
|
||||
}
|
||||
}
|
||||
|
||||
var clone = JSON.parse(JSON.stringify(table));
|
||||
clone.table.body = NINJA.prepareDataTable(NINJA.statementInvoices(invoice), 'invoiceItems');
|
||||
clone.table.widths = ["22%", "22%", "22%", "17%", "17%"];
|
||||
data.stack.push(clone);
|
||||
|
||||
var clone = JSON.parse(JSON.stringify(subtotals));
|
||||
clone.columns[1].table.body = [[
|
||||
{ text: invoiceLabels.balance_due, style: ['subtotalsLabel', 'subtotalsBalanceDueLabel'] },
|
||||
{ text: formatMoneyInvoice(invoice.balance_amount, invoice), style: ['subtotals', 'subtotalsBalanceDue', 'noWrap'] }
|
||||
]];
|
||||
data.stack.push(clone);
|
||||
|
||||
if (hasPayments) {
|
||||
var clone = JSON.parse(JSON.stringify(table));
|
||||
clone.table.body = NINJA.prepareDataTable(NINJA.statementPayments(invoice), 'invoiceItems');
|
||||
clone.table.widths = ["22%", "22%", "39%", "17%"];
|
||||
data.stack.push(clone);
|
||||
|
||||
var clone = JSON.parse(JSON.stringify(subtotals));
|
||||
clone.columns[1].table.body = [[
|
||||
{ text: invoiceLabels.amount_paid, style: ['subtotalsLabel', 'subtotalsBalanceDueLabel'] },
|
||||
{ text: formatMoneyInvoice(paymentTotal, invoice), style: ['subtotals', 'subtotalsBalanceDue', 'noWrap'] }
|
||||
]];
|
||||
data.stack.push(clone);
|
||||
}
|
||||
|
||||
if (hasAging) {
|
||||
var clone = JSON.parse(JSON.stringify(table));
|
||||
clone.table.body = NINJA.prepareDataTable(NINJA.statementAging(invoice), 'invoiceItems');
|
||||
clone.table.widths = ["20%", "20%", "20%", "20%", "20%"];
|
||||
data.stack.push(clone);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
NINJA.statementInvoices = function(invoice) {
|
||||
var grid = [[]];
|
||||
grid[0].push({text: invoiceLabels.invoice_number, style: ['tableHeader', 'itemTableHeader', 'firstColumn']});
|
||||
grid[0].push({text: invoiceLabels.invoice_date, style: ['tableHeader', 'invoiceDateTableHeader']});
|
||||
grid[0].push({text: invoiceLabels.due_date, style: ['tableHeader', 'dueDateTableHeader']});
|
||||
grid[0].push({text: invoiceLabels.total, style: ['tableHeader', 'totalTableHeader']});
|
||||
grid[0].push({text: invoiceLabels.balance, style: ['tableHeader', 'balanceTableHeader', 'lastColumn']});
|
||||
|
||||
var counter = 0;
|
||||
for (var i = 0; i < invoice.invoice_items.length; i++) {
|
||||
var item = invoice.invoice_items[i];
|
||||
if (item.invoice_item_type_id != 1) {
|
||||
continue;
|
||||
}
|
||||
var rowStyle = (counter++ % 2 == 0) ? 'odd' : 'even';
|
||||
grid.push([
|
||||
{text: item.product_key, style:['invoiceNumber', 'productKey', rowStyle, 'firstColumn']},
|
||||
{text: item.custom_value1 && item.custom_value1 != '0000-00-00' ? moment(item.custom_value1).format(invoice.account.date_format ? invoice.account.date_format.format_moment : 'MMM D, YYYY') : ' ', style:['invoiceDate', rowStyle]},
|
||||
{text: item.custom_value2 && item.custom_value2 != '0000-00-00' ? moment(item.custom_value2).format(invoice.account.date_format ? invoice.account.date_format.format_moment : 'MMM D, YYYY') : ' ', style:['dueDate', rowStyle]},
|
||||
{text: formatMoneyInvoice(item.notes, invoice), style:['subtotals', rowStyle]},
|
||||
{text: formatMoneyInvoice(item.cost, invoice), style:['lineTotal', rowStyle, 'lastColumn']},
|
||||
]);
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
NINJA.statementPayments = function(invoice) {
|
||||
var grid = [[]];
|
||||
|
||||
grid[0].push({text: invoiceLabels.invoice_number, style: ['tableHeader', 'itemTableHeader', 'firstColumn']});
|
||||
grid[0].push({text: invoiceLabels.payment_date, style: ['tableHeader', 'invoiceDateTableHeader']});
|
||||
grid[0].push({text: invoiceLabels.method, style: ['tableHeader', 'dueDateTableHeader']});
|
||||
//grid[0].push({text: invoiceLabels.reference, style: ['tableHeader', 'totalTableHeader']});
|
||||
grid[0].push({text: invoiceLabels.amount, style: ['tableHeader', 'balanceTableHeader', 'lastColumn']});
|
||||
|
||||
var counter = 0;
|
||||
for (var i = 0; i < invoice.invoice_items.length; i++) {
|
||||
var item = invoice.invoice_items[i];
|
||||
if (item.invoice_item_type_id != 3) {
|
||||
continue;
|
||||
}
|
||||
var rowStyle = (counter++ % 2 == 0) ? 'odd' : 'even';
|
||||
grid.push([
|
||||
{text: item.product_key, style:['invoiceNumber', 'productKey', rowStyle, 'firstColumn']},
|
||||
{text: item.custom_value1 && item.custom_value1 != '0000-00-00' ? moment(item.custom_value1).format(invoice.account.date_format ? invoice.account.date_format.format_moment : 'MMM D, YYYY') : ' ', style:['invoiceDate', rowStyle]},
|
||||
{text: item.custom_value2 ? item.custom_value2 : ' ', style:['dueDate', rowStyle]},
|
||||
//{text: item.transaction_reference, style:['subtotals', rowStyle]},
|
||||
{text: formatMoneyInvoice(item.cost, invoice), style:['lineTotal', rowStyle, 'lastColumn']},
|
||||
]);
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
NINJA.statementAging = function(invoice) {
|
||||
var grid = [[]];
|
||||
|
||||
grid[0].push({text: '0 - 30', style: ['tableHeader', 'alignRight', 'firstColumn']});
|
||||
grid[0].push({text: '30 - 60', style: ['tableHeader', 'alignRight']});
|
||||
grid[0].push({text: '60 - 90', style: ['tableHeader', 'alignRight']});
|
||||
grid[0].push({text: '90 - 120', style: ['tableHeader', 'alignRight']});
|
||||
grid[0].push({text: '120+', style: ['tableHeader', 'alignRight', 'lastColumn']});
|
||||
|
||||
for (var i = 0; i < invoice.invoice_items.length; i++) {
|
||||
var item = invoice.invoice_items[i];
|
||||
if (item.invoice_item_type_id != 4) {
|
||||
continue;
|
||||
}
|
||||
grid.push([
|
||||
{text: formatMoneyInvoice(item.product_key, invoice), style:['subtotals', 'odd', 'firstColumn']},
|
||||
{text: formatMoneyInvoice(item.notes, invoice), style:['subtotals', 'odd']},
|
||||
{text: formatMoneyInvoice(item.custom_value1, invoice), style:['subtotals', 'odd']},
|
||||
{text: formatMoneyInvoice(item.custom_value1, invoice), style:['subtotals', 'odd']},
|
||||
{text: formatMoneyInvoice(item.cost, invoice), style:['subtotals', 'odd', 'lastColumn']},
|
||||
]);
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
NINJA.signature = function(invoice) {
|
||||
var invitation = NINJA.getSignatureInvitation(invoice);
|
||||
if (invitation) {
|
||||
@ -500,36 +698,6 @@ NINJA.notesAndTerms = function(invoice)
|
||||
return NINJA.prepareDataList(data, 'notesAndTerms');
|
||||
}
|
||||
|
||||
NINJA.statementColumns = function(invoice)
|
||||
{
|
||||
return ["22%", "22%", "22%", "17%", "17%"];
|
||||
}
|
||||
|
||||
NINJA.statementLines = function(invoice)
|
||||
{
|
||||
var grid = [[]];
|
||||
grid[0].push({text: invoiceLabels.invoice_number, style: ['tableHeader', 'itemTableHeader']});
|
||||
grid[0].push({text: invoiceLabels.invoice_date, style: ['tableHeader', 'invoiceDateTableHeader']});
|
||||
grid[0].push({text: invoiceLabels.due_date, style: ['tableHeader', 'dueDateTableHeader']});
|
||||
grid[0].push({text: invoiceLabels.total, style: ['tableHeader', 'totalTableHeader']});
|
||||
grid[0].push({text: invoiceLabels.balance, style: ['tableHeader', 'balanceTableHeader']});
|
||||
|
||||
for (var i = 0; i < invoice.invoice_items.length; i++) {
|
||||
var item = invoice.invoice_items[i];
|
||||
var row = [];
|
||||
var rowStyle = (i % 2 == 0) ? 'odd' : 'even';
|
||||
grid.push([
|
||||
{text: item.invoice_number, style:['invoiceNumber', 'productKey', rowStyle]},
|
||||
{text: item.invoice_date && item.invoice_date != '0000-00-00' ? moment(item.invoice_date).format(invoice.date_format) : ' ', style:['invoiceDate', rowStyle]},
|
||||
{text: item.due_date && item.due_date != '0000-00-00' ? moment(item.due_date).format(invoice.date_format) : ' ', style:['dueDate', rowStyle]},
|
||||
{text: formatMoneyInvoice(item.amount, invoice), style:['subtotals', rowStyle]},
|
||||
{text: formatMoneyInvoice(item.balance, invoice), style:['lineTotal', rowStyle]},
|
||||
]);
|
||||
}
|
||||
|
||||
return NINJA.prepareDataTable(grid, 'invoiceItems');
|
||||
}
|
||||
|
||||
NINJA.invoiceColumns = function(invoice, design, isTasks)
|
||||
{
|
||||
var account = invoice.account;
|
||||
@ -920,16 +1088,6 @@ NINJA.invoiceDocuments = function(invoice) {
|
||||
return stack.length?{stack:stack}:[];
|
||||
}
|
||||
|
||||
NINJA.statementSubtotals = function(invoice)
|
||||
{
|
||||
var data = [[
|
||||
{ text: invoiceLabels.balance_due, style: ['subtotalsLabel', 'subtotalsBalanceDueLabel'] },
|
||||
{ text: formatMoneyInvoice(invoice.balance_amount, invoice), style: ['subtotals', 'subtotalsBalanceDue'] }
|
||||
]];
|
||||
|
||||
return NINJA.prepareDataPairs(data, 'subtotals');
|
||||
}
|
||||
|
||||
NINJA.subtotals = function(invoice, hideBalance)
|
||||
{
|
||||
if (! invoice || invoice.is_delivery_note) {
|
||||
@ -1214,7 +1372,7 @@ NINJA.renderField = function(invoice, field, twoColumn) {
|
||||
value = contact.custom_value2;
|
||||
}
|
||||
} else if (field == 'account.company_name') {
|
||||
value = account.name;
|
||||
value = account.name + ' ';
|
||||
} else if (field == 'account.id_number') {
|
||||
value = account.id_number;
|
||||
if (invoiceLabels.id_number_orig) {
|
||||
|
@ -586,19 +586,16 @@ function calculateAmounts(invoice) {
|
||||
// sum line item
|
||||
for (var i=0; i<invoice.invoice_items.length; i++) {
|
||||
var item = invoice.invoice_items[i];
|
||||
if (invoice.is_statement) {
|
||||
var lineTotal = roundToTwo(NINJA.parseFloat(item.balance));
|
||||
} else {
|
||||
var lineTotal = roundSignificant(NINJA.parseFloat(item.cost) * NINJA.parseFloat(item.qty));
|
||||
var discount = roundToTwo(NINJA.parseFloat(item.discount));
|
||||
if (discount != 0) {
|
||||
if (parseInt(invoice.is_amount_discount)) {
|
||||
lineTotal -= discount;
|
||||
} else {
|
||||
lineTotal -= roundToTwo(lineTotal * discount / 100);
|
||||
}
|
||||
var lineTotal = roundSignificant(NINJA.parseFloat(item.cost) * NINJA.parseFloat(item.qty));
|
||||
var discount = roundToTwo(NINJA.parseFloat(item.discount));
|
||||
if (discount != 0) {
|
||||
if (parseInt(invoice.is_amount_discount)) {
|
||||
lineTotal -= discount;
|
||||
} else {
|
||||
lineTotal -= roundToTwo(lineTotal * discount / 100);
|
||||
}
|
||||
}
|
||||
|
||||
lineTotal = roundToTwo(lineTotal);
|
||||
if (lineTotal) {
|
||||
total += lineTotal;
|
||||
@ -1161,7 +1158,7 @@ function prettyJson(json) {
|
||||
});
|
||||
}
|
||||
|
||||
function searchData(data, key, fuzzy) {
|
||||
function searchData(data, key, fuzzy, secondKey) {
|
||||
return function findMatches(q, cb) {
|
||||
var matches, substringRegex;
|
||||
if (fuzzy) {
|
||||
@ -1176,8 +1173,9 @@ function searchData(data, key, fuzzy) {
|
||||
$.each(data, function(i, obj) {
|
||||
if (substrRegex.test(obj[key])) {
|
||||
matches.push(obj);
|
||||
}
|
||||
});
|
||||
} else if (secondKey && substrRegex.test(obj[secondKey]))
|
||||
matches.push(obj);
|
||||
});
|
||||
}
|
||||
cb(matches);
|
||||
}
|
||||
|
@ -1345,6 +1345,7 @@ $LANG = array(
|
||||
'product_key' => 'Product',
|
||||
'created_products' => 'Successfully created/updated :count product(s)',
|
||||
'export_help' => 'Use JSON if you plan to import the data into Invoice Ninja.<br/>The file includes clients, products, invoices, quotes and payments.',
|
||||
'selfhost_export_help' => '<br/>We recommend using mysqldump to create a full backup.',
|
||||
'JSON_file' => 'JSON File',
|
||||
|
||||
'view_dashboard' => 'View Dashboard',
|
||||
@ -2111,7 +2112,7 @@ $LANG = array(
|
||||
'template' => 'Template',
|
||||
'start_of_week_help' => 'Used by <b>date</b> selectors',
|
||||
'financial_year_start_help' => 'Used by <b>date range</b> selectors',
|
||||
'reports_help' => 'Shift + Click to sort by multple columns, Ctrl + Click to clear the grouping.',
|
||||
'reports_help' => 'Shift + Click to sort by multiple columns, Ctrl + Click to clear the grouping.',
|
||||
'this_year' => 'This Year',
|
||||
|
||||
// Updated login screen
|
||||
@ -2409,6 +2410,7 @@ $LANG = array(
|
||||
'currency_georgian_lari' => 'Georgian Lari',
|
||||
'currency_qatari_riyal' => 'Qatari Riyal',
|
||||
'currency_honduran_lempira' => 'Honduran Lempira',
|
||||
'currency_surinamese_dollar' => 'Surinamese Dollar',
|
||||
|
||||
'review_app_help' => 'We hope you\'re enjoying using the app.<br/>If you\'d consider :link we\'d greatly appreciate it!',
|
||||
'writing_a_review' => 'writing a review',
|
||||
@ -2839,6 +2841,13 @@ $LANG = array(
|
||||
'guide' => 'Guide',
|
||||
'gateway_fee_item' => 'Gateway Fee Item',
|
||||
'gateway_fee_description' => 'Gateway Fee Surcharge',
|
||||
'show_payments' => 'Show Payments',
|
||||
'show_aging' => 'Show Aging',
|
||||
'reference' => 'Reference',
|
||||
'amount_paid' => 'Amount Paid',
|
||||
'send_notifications_for' => 'Send Notifications For',
|
||||
'all_invoices' => 'All Invoices',
|
||||
'my_invoices' => 'My Invoices',
|
||||
|
||||
);
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user