Merge branch 'release-4.5.0'

This commit is contained in:
Hillel Coren 2018-06-17 14:05:50 +03:00
commit 56f13a019e
131 changed files with 2527 additions and 743 deletions

View File

@ -22,3 +22,4 @@ MAIL_DRIVER=log
TRAVIS=true
API_SECRET=password
TEST_USERNAME=user@example.com
TEST_PERMISSIONS_USERNAME=permissions@example.com

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

@ -467,4 +467,9 @@ class AppController extends BaseController
return response(nl2br(Artisan::output()));
}
public function redirect()
{
return redirect((Utils::isNinja() ? NINJA_WEB_URL : ''), 301);
}
}

View File

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

View File

@ -67,4 +67,5 @@ class BaseController extends Controller
exit;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -267,8 +267,6 @@ class SubscriptionListener
$jsonData['client_name'] = $entity->client->getDisplayName();
}
foreach ($subscriptions as $subscription) {
switch ($subscription->format) {
case SUBSCRIPTION_FORMAT_JSON:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -337,6 +337,11 @@ trait PresentsInvoice
'custom_value2',
'delivery_note',
'date',
'method',
'payment_date',
'reference',
'amount',
'amount_paid',
];
foreach ($fields as $field) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,21 +19,22 @@ class ProposalSnippetDatatable extends EntityDatatable
function ($model) {
$icon = '<i class="fa fa-' . $model->icon . '"></i>&nbsp;&nbsp;';
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]);
},
],
];

View File

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

View File

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

View File

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

View File

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

View File

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

View 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,
];
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -173,4 +173,7 @@ return [
'register' => [
'translations' => true,
],
'relations' => [
// all dynamic relations registered from modules are added here
],
];

View File

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

View 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

View File

@ -162,6 +162,9 @@ class CountriesSeeder extends Seeder
'thousand_separator' => ',',
'decimal_separator' => '.',
],
'SR' => [ // Suriname
'swap_currency_symbol' => true,
],
'UY' => [
'swap_postal_code' => true,
],

View File

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

View File

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

View File

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

View File

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

View File

@ -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>`_.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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