1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-19 16:01:34 +02:00

Working on new hosted pricing

This commit is contained in:
Hillel Coren 2016-07-11 20:08:43 +03:00
parent 28d8e53764
commit 986487b4c9
28 changed files with 390 additions and 254 deletions

View File

@ -76,7 +76,7 @@ script:
- php ./vendor/codeception/codeception/codecept run --debug acceptance InvoiceCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance QuoteCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance InvoiceDesignCest.php
- php ./vendor/codeception/codeception/codecept run acceptance OnlinePaymentCest.php
- 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

View File

@ -1,5 +1,6 @@
<?php namespace App\Console\Commands;
use Utils;
use Illuminate\Console\Command;
use App\Models\Company;
use App\Ninja\Mailers\ContactMailer as Mailer;
@ -43,7 +44,7 @@ class SendRenewalInvoices extends Command
$this->mailer = $mailer;
$this->accountRepo = $repo;
}
public function fire()
{
$this->info(date('Y-m-d').' Running SendRenewalInvoices...');
@ -58,28 +59,36 @@ class SendRenewalInvoices extends Command
if (!count($company->accounts)) {
continue;
}
$account = $company->accounts->sortBy('id')->first();
$plan = $company->plan;
$term = $company->plan_term;
$plan = [];
$plan['plan'] = $company->plan;
$plan['term'] = $company->plan_term;
$plan['num_users'] = $company->num_users;
$plan['price'] = min($company->plan_price, Utils::getPlanPrice($plan));
if ($company->pending_plan) {
$plan = $company->pending_plan;
$term = $company->pending_term;
$plan['plan'] = $company->pending_plan;
$plan['term'] = $company->pending_term;
$plan['num_users'] = $company->pending_num_users;
$plan['price'] = min($company->pending_plan_price, Utils::getPlanPrice($plan));
}
if ($plan == PLAN_FREE || !$plan || !$term ){
if ($plan['plan'] == PLAN_FREE || !$plan['plan'] || !$plan['term'] || !$plan['price']){
continue;
}
$client = $this->accountRepo->getNinjaClient($account);
$invitation = $this->accountRepo->createNinjaInvoice($client, $account, $plan, $term);
$invitation = $this->accountRepo->createNinjaInvoice($client, $account, $plan, 0, false);
// set the due date to 10 days from now
$invoice = $invitation->invoice;
$invoice->due_date = date('Y-m-d', strtotime('+ 10 days'));
$invoice->save();
$term = $plan['term'];
$plan = $plan['plan'];
if ($term == PLAN_TERM_YEARLY) {
$this->mailer->sendInvoice($invoice);
$this->info("Sent {$term}ly {$plan} invoice to {$client->getDisplayName()}");

View File

@ -154,20 +154,6 @@ class AccountController extends BaseController
return Redirect::to($redirectTo)->with('sign_up', Input::get('sign_up'));
}
/**
* @return bool|mixed
*/
public function enableProPlan()
{
if (Auth::user()->isPro() && ! Auth::user()->isTrial()) {
return false;
}
$invitation = $this->accountRepo->enablePlan();
return $invitation->invitation_key;
}
/**
* @return \Illuminate\Http\RedirectResponse
*/
@ -201,7 +187,8 @@ class AccountController extends BaseController
'plan' => $plan,
'term' => $term
];
} elseif ($planDetails['term'] == PLAN_TERM_MONTHLY && $term == PLAN_TERM_YEARLY) {
} elseif ($planDetails['term'] == PLAN_TERM_MONTHLY && $term == PLAN_TERM_YEARLY
|| $planDetails['num_users'] != Input::get('num_users')) {
$new_plan = [
'plan' => $plan,
'term' => $term,
@ -260,8 +247,7 @@ class AccountController extends BaseController
$days_total = $planDetails['paid']->diff($planDetails['expires'])->days;
$percent_used = $days_used / $days_total;
$old_plan_price = Account::$plan_prices[$planDetails['plan']][$planDetails['term']];
$credit = $old_plan_price * (1 - $percent_used);
$credit = $planDetails['plan_price'] * (1 - $percent_used);
}
} else {
$new_plan = [
@ -271,15 +257,23 @@ class AccountController extends BaseController
}
if (!empty($pending_change) && empty($new_plan)) {
$pending_change['num_users'] = Input::get('num_users');
$account->company->pending_plan = $pending_change['plan'];
$account->company->pending_term = $pending_change['term'];
$account->company->pending_num_users = $pending_change['num_users'];
$account->company->pending_plan_price = Utils::getPlanPrice($pending_change);
$account->company->save();
Session::flash('message', trans('texts.updated_plan'));
}
if (!empty($new_plan) && $new_plan['plan'] != PLAN_FREE) {
$invitation = $this->accountRepo->enablePlan($new_plan['plan'], $new_plan['term'], $credit, !empty($pending_monthly));
$new_plan['num_users'] = 1;
if ($new_plan['plan'] == PLAN_ENTERPRISE) {
$new_plan['num_users'] = Input::get('num_users');
}
$new_plan['price'] = Utils::getPlanPrice($new_plan);
$invitation = $this->accountRepo->enablePlan($new_plan, $credit, !empty($pending_monthly));
return Redirect::to('view/'.$invitation->invitation_key);
}
@ -483,7 +477,8 @@ class AccountController extends BaseController
private function showBankAccounts()
{
return View::make('accounts.banks', [
'title' => trans('texts.bank_accounts')
'title' => trans('texts.bank_accounts'),
'advanced' => ! Auth::user()->hasFeature(FEATURE_EXPENSES),
]);
}

View File

@ -41,10 +41,6 @@ class QuoteController extends BaseController
public function index()
{
if (!Utils::hasFeature(FEATURE_QUOTES)) {
return Redirect::to('/invoices/create');
}
$data = [
'title' => trans('texts.quotes'),
'entityType' => ENTITY_QUOTE,

View File

@ -93,21 +93,19 @@ class UserController extends BaseController
*/
public function create()
{
if (!Auth::user()->registered) {
if ( ! Auth::user()->registered) {
Session::flash('error', trans('texts.register_to_add_user'));
return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT);
}
if (!Auth::user()->confirmed) {
if ( ! Auth::user()->confirmed) {
Session::flash('error', trans('texts.confirmation_required'));
return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT);
}
if (Utils::isNinja()) {
$count = User::where('account_id', '=', Auth::user()->account_id)->count();
if ($count >= MAX_NUM_USERS) {
Session::flash('error', trans('texts.limit_users', ['limit' => MAX_NUM_USERS]));
return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT);
}
if (Utils::isNinja() && ! Auth::user()->caddAddUsers()) {
Session::flash('error', trans('texts.max_users_reached'));
return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT);
}
$data = [
@ -132,6 +130,11 @@ class UserController extends BaseController
if ($action === 'archive') {
$user->delete();
} else {
if ( ! Auth::user()->caddAddUsers()) {
return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT)
->with('error', trans('texts.max_users_reached'));
}
$user->restore();
}
@ -140,19 +143,6 @@ class UserController extends BaseController
return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT);
}
public function restoreUser($userPublicId)
{
$user = User::where('account_id', '=', Auth::user()->account_id)
->where('public_id', '=', $userPublicId)
->withTrashed()->firstOrFail();
$user->restore();
Session::flash('message', trans('texts.restored_user'));
return Redirect::to('settings/' . ACCOUNT_USER_MANAGEMENT);
}
/**
* Stores new account
*
@ -257,7 +247,7 @@ class UserController extends BaseController
$token = Password::getRepository()->create($user);
return Redirect::to("/password/reset/{$token}");
} else {
} else {
if (Auth::check()) {
if (Session::has(REQUESTED_PRO_PLAN)) {
Session::forget(REQUESTED_PRO_PLAN);

View File

@ -214,7 +214,6 @@ Route::group([
Route::get('send_confirmation/{user_id}', 'UserController@sendConfirmation');
Route::get('start_trial/{plan}', 'AccountController@startTrial')
->where(['plan'=>'pro']);
Route::get('restore_user/{user_id}', 'UserController@restoreUser');
Route::get('/switch_account/{user_id}', 'UserController@switchAccount');
Route::get('/unlink_account/{user_account_id}/{user_id}', 'UserController@unlinkAccount');
Route::get('/manage_companies', 'UserController@manageCompanies');
@ -245,7 +244,6 @@ Route::group([
Route::post('user/setTheme', 'UserController@setTheme');
Route::post('remove_logo', 'AccountController@removeLogo');
Route::post('account/go_pro', 'AccountController@enableProPlan');
Route::post('/export', 'ExportController@doExport');
Route::post('/import', 'ImportController@doImport');
@ -466,7 +464,7 @@ if (!defined('CONTACT_EMAIL')) {
define('ACTIVITY_TYPE_ARCHIVE_EXPENSE', 35);
define('ACTIVITY_TYPE_DELETE_EXPENSE', 36);
define('ACTIVITY_TYPE_RESTORE_EXPENSE', 37);
// tasks
define('ACTIVITY_TYPE_CREATE_TASK', 42);
define('ACTIVITY_TYPE_UPDATE_TASK', 43);
@ -638,10 +636,10 @@ if (!defined('CONTACT_EMAIL')) {
define('INVOICE_DESIGNS_AFFILIATE_KEY', 'T3RS74');
define('SELF_HOST_AFFILIATE_KEY', '8S69AD');
define('PLAN_PRICE_PRO_MONTHLY', env('PLAN_PRICE_PRO_MONTHLY', 5));
define('PLAN_PRICE_PRO_YEARLY', env('PLAN_PRICE_PRO_YEARLY', 50));
define('PLAN_PRICE_ENTERPRISE_MONTHLY', env('PLAN_PRICE_ENTERPRISE_MONTHLY', 10));
define('PLAN_PRICE_ENTERPRISE_YEARLY', env('PLAN_PRICE_ENTERPRISE_YEARLY', 100));
define('PLAN_PRICE_PRO_MONTHLY', env('PLAN_PRICE_PRO_MONTHLY', 12));
define('PLAN_PRICE_ENTERPRISE_MONTHLY_2', env('PLAN_PRICE_ENTERPRISE_MONTHLY_2', 18));
define('PLAN_PRICE_ENTERPRISE_MONTHLY_5', env('PLAN_PRICE_ENTERPRISE_MONTHLY_5', 26));
define('PLAN_PRICE_ENTERPRISE_MONTHLY_10', env('PLAN_PRICE_ENTERPRISE_MONTHLY_10', 38));
define('WHITE_LABEL_PRICE', env('WHITE_LABEL_PRICE', 20));
define('INVOICE_DESIGNS_PRICE', env('INVOICE_DESIGNS_PRICE', 10));
@ -751,6 +749,8 @@ if (!defined('CONTACT_EMAIL')) {
define('FEATURE_PDF_ATTACHMENT', 'pdf_attachment');
define('FEATURE_MORE_INVOICE_DESIGNS', 'more_invoice_designs');
define('FEATURE_QUOTES', 'quotes');
define('FEATURE_TASKS', 'tasks');
define('FEATURE_EXPENSES', 'expenses');
define('FEATURE_REPORTS', 'reports');
define('FEATURE_API', 'api');
define('FEATURE_CLIENT_PORTAL_PASSWORD', 'client_portal_password');
@ -771,6 +771,7 @@ if (!defined('CONTACT_EMAIL')) {
// Pro users who started paying on or before this date will be able to manage users
define('PRO_USERS_GRANDFATHER_DEADLINE', '2016-06-04');
define('EXTRAS_GRANDFATHER_COMPANY_ID', 0);
// WePay
define('WEPAY_PRODUCTION', 'production');

View File

@ -215,6 +215,46 @@ class Utils
}
}
public static function getPlanPrice($plan)
{
$term = $plan['term'];
$numUsers = $plan['num_users'];
$plan = $plan['plan'];
if ($plan == PLAN_FREE) {
$price = 0;
} elseif ($plan == PLAN_PRO) {
$price = PLAN_PRICE_PRO_MONTHLY;
} elseif ($plan == PLAN_ENTERPRISE) {
if ($numUsers <= 2) {
$price = PLAN_PRICE_ENTERPRISE_MONTHLY_2;
} elseif ($numUsers <= 5) {
$price = PLAN_PRICE_ENTERPRISE_MONTHLY_5;
} elseif ($numUsers <= 10) {
$price = PLAN_PRICE_ENTERPRISE_MONTHLY_10;
} else {
static::fatalError('Invalid number of users: ' . $numUsers);
}
}
if ($term == PLAN_TERM_YEARLY) {
$price = $price * 10;
}
return $price;
}
public static function getMinNumUsers($max)
{
if ($max <= 2) {
return 1;
} elseif ($max <= 5) {
return 3;
} else {
return 6;
}
}
public static function basePath()
{
return substr($_SERVER['SCRIPT_NAME'], 0, strrpos($_SERVER['SCRIPT_NAME'], '/') + 1);

View File

@ -20,20 +20,6 @@ class Account extends Eloquent
use PresentableTrait;
use SoftDeletes;
/**
* @var array
*/
public static $plan_prices = [
PLAN_PRO => [
PLAN_TERM_MONTHLY => PLAN_PRICE_PRO_MONTHLY,
PLAN_TERM_YEARLY => PLAN_PRICE_PRO_YEARLY,
],
PLAN_ENTERPRISE => [
PLAN_TERM_MONTHLY => PLAN_PRICE_ENTERPRISE_MONTHLY,
PLAN_TERM_YEARLY => PLAN_PRICE_ENTERPRISE_YEARLY,
],
];
/**
* @var string
*/
@ -90,7 +76,6 @@ class Account extends Eloquent
ACCOUNT_USER_DETAILS,
ACCOUNT_LOCALIZATION,
ACCOUNT_PAYMENTS,
ACCOUNT_BANKS,
ACCOUNT_TAX_RATES,
ACCOUNT_PRODUCTS,
ACCOUNT_NOTIFICATIONS,
@ -106,6 +91,7 @@ class Account extends Eloquent
ACCOUNT_INVOICE_DESIGN,
ACCOUNT_EMAIL_SETTINGS,
ACCOUNT_TEMPLATES_AND_REMINDERS,
ACCOUNT_BANKS,
ACCOUNT_CLIENT_PORTAL,
ACCOUNT_CHARTS_AND_REPORTS,
ACCOUNT_DATA_VISUALIZATIONS,
@ -1189,6 +1175,10 @@ class Account extends Eloquent
case FEATURE_CUSTOM_URL:
return $selfHost || !empty($planDetails);
case FEATURE_TASKS:
case FEATURE_EXPENSES:
return $selfHost || !empty($planDetails) || $planDetails['company_id'] < EXTRAS_GRANDFATHER_COMPANY_ID;
// Pro; No trial allowed, unless they're trialing enterprise with an active pro plan
case FEATURE_MORE_CLIENTS:
return $selfHost || !empty($planDetails) && (!$planDetails['trial'] || !empty($this->getPlanDetails(false, false)));
@ -1272,6 +1262,7 @@ class Account extends Eloquent
}
$plan = $this->company->plan;
$price = $this->company->plan_price;
$trial_plan = $this->company->trial_plan;
if(!$plan && (!$trial_plan || !$include_trial)) {
@ -1335,6 +1326,9 @@ class Account extends Eloquent
if ($use_plan) {
return [
'company_id' => $this->company->id,
'num_users' => $this->company->num_users,
'plan_price' => $price,
'trial' => false,
'plan' => $plan,
'started' => DateTime::createFromFormat('Y-m-d', $this->company->plan_started),
@ -1345,6 +1339,9 @@ class Account extends Eloquent
];
} else {
return [
'company_id' => $this->company->id,
'num_users' => 1,
'plan_price' => 0,
'trial' => true,
'plan' => $trial_plan,
'started' => $trial_started,

View File

@ -430,6 +430,23 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
public function filterId() {
return $this->hasPermission('view_all') ? false : $this->id;
}
public function caddAddUsers() {
if ( ! $this->hasFeature(FEATURE_USERS)) {
return false;
}
$account = $this->account;
$company = $account->company;
$numUsers = 1;
foreach ($company->accounts as $account) {
$numUsers += $account->users->count() - 1;
}
return $numUsers < $company->num_users;
}
}
User::updating(function ($user) {

View File

@ -576,6 +576,12 @@ class BasePaymentDriver
if (1 == preg_match('/^Plan - (.+) \((.+)\)$/', $invoice_item->product_key, $matches)) {
$plan = strtolower($matches[1]);
$term = strtolower($matches[2]);
if ($plan == PLAN_ENTERPRISE) {
preg_match('/###[\d] [\w]* (\d*)/', $invoice_item->notes, $matches);
$numUsers = $matches[1];
} else {
$numUsers = 1;
}
} elseif ($invoice_item->product_key == 'Pending Monthly') {
$pending_monthly = true;
}
@ -607,6 +613,8 @@ class BasePaymentDriver
$account->company->payment_id = $payment->id;
$account->company->plan = $plan;
$account->company->plan_term = $term;
$account->company->plan_price = $payment->amount;
$account->company->num_users = $numUsers;
$account->company->plan_expires = DateTime::createFromFormat('Y-m-d', $account->company->plan_paid)
->modify($term == PLAN_TERM_MONTHLY ? '+1 month' : '+1 year')->format('Y-m-d');

View File

@ -228,23 +228,26 @@ class AccountRepository
return $data;
}
public function enablePlan($plan = PLAN_PRO, $term = PLAN_TERM_MONTHLY, $credit = 0, $pending_monthly = false)
public function enablePlan($plan, $credit = 0, $pending_monthly = false)
{
$account = Auth::user()->account;
$client = $this->getNinjaClient($account);
$invitation = $this->createNinjaInvoice($client, $account, $plan, $term, $credit, $pending_monthly);
$invitation = $this->createNinjaInvoice($client, $account, $plan, $credit, $pending_monthly);
return $invitation;
}
public function createNinjaInvoice($client, $clientAccount, $plan = PLAN_PRO, $term = PLAN_TERM_MONTHLY, $credit = 0, $pending_monthly = false)
public function createNinjaInvoice($client, $clientAccount, $plan, $credit = 0, $pending_monthly = false)
{
$term = $plan['term'];
$plan_cost = $plan['price'];
$num_users = $plan['num_users'];
$plan = $plan['plan'];
if ($credit < 0) {
$credit = 0;
}
$plan_cost = Account::$plan_prices[$plan][$term];
$account = $this->getNinjaAccount();
$lastInvoice = Invoice::withTrashed()->whereAccountId($account->id)->orderBy('public_id', 'DESC')->first();
$publicId = $lastInvoice ? ($lastInvoice->public_id + 1) : 1;
@ -272,6 +275,11 @@ class AccountRepository
$item->cost = $plan_cost;
$item->notes = trans("texts.{$plan}_plan_{$term}_description");
if ($plan == PLAN_ENTERPRISE) {
$min = Utils::getMinNumUsers($num_users);
$item->notes .= "\n\n###" . trans('texts.min_to_max_users', ['min' => $min, 'max' => $num_users]);
}
// Don't change this without updating the regex in PaymentService->createPayment()
$item->product_key = 'Plan - '.ucfirst($plan).' ('.ucfirst($term).')';
$invoice->invoice_items()->save($item);

View File

@ -2,4 +2,20 @@
namespace App\Policies;
class ExpensePolicy extends EntityPolicy {}
use App\Models\User;
class ExpensePolicy extends EntityPolicy
{
/**
* @param User $user
* @return bool
*/
public static function create(User $user) {
if ( ! parent::create($user)) {
return false;
}
return $user->hasFeature(FEATURE_EXPENSES);
}
}

View File

@ -2,4 +2,4 @@
namespace App\Policies;
class InvoicePolicy extends EntityPolicy {}
class InvoicePolicy extends EntityPolicy {}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Policies;
use App\Models\User;
class QuotePolicy extends EntityPolicy
{
/**
* @param User $user
* @return bool
*/
public static function create(User $user) {
if ( ! parent::create($user)) {
return false;
}
return $user->hasFeature(FEATURE_QUOTES);
}
}

View File

@ -2,4 +2,20 @@
namespace App\Policies;
class TaskPolicy extends EntityPolicy {}
use App\Models\User;
class TaskPolicy extends EntityPolicy
{
/**
* @param User $user
* @return bool
*/
public static function create(User $user) {
if ( ! parent::create($user)) {
return false;
}
return $user->hasFeature(FEATURE_TASKS);
}
}

View File

@ -2,4 +2,20 @@
namespace App\Policies;
class VendorPolicy extends EntityPolicy {}
use App\Models\User;
class VendorPolicy extends EntityPolicy
{
/**
* @param User $user
* @return bool
*/
public static function create(User $user) {
if ( ! parent::create($user)) {
return false;
}
return $user->hasFeature(FEATURE_EXPENSES);
}
}

View File

@ -60,17 +60,17 @@ class AppServiceProvider extends ServiceProvider
$items = [];
if($user->can('create', $type))$items[] = '<li><a href="'.URL::to($types.'/create').'">'.trans("texts.new_$type").'</a></li>';
if ($user->can('create', $type)) {
$items[] = '<li><a href="'.URL::to($types.'/create').'">'.trans("texts.new_$type").'</a></li>';
}
if ($type == ENTITY_INVOICE) {
if(!empty($items))$items[] = '<li class="divider"></li>';
$items[] = '<li><a href="'.URL::to('recurring_invoices').'">'.trans('texts.recurring_invoices').'</a></li>';
if($user->can('create', ENTITY_INVOICE))$items[] = '<li><a href="'.URL::to('recurring_invoices/create').'">'.trans('texts.new_recurring_invoice').'</a></li>';
if ($user->hasFeature(FEATURE_QUOTES)) {
$items[] = '<li class="divider"></li>';
$items[] = '<li><a href="'.URL::to('quotes').'">'.trans('texts.quotes').'</a></li>';
if($user->can('create', ENTITY_INVOICE))$items[] = '<li><a href="'.URL::to('quotes/create').'">'.trans('texts.new_quote').'</a></li>';
}
$items[] = '<li class="divider"></li>';
$items[] = '<li><a href="'.URL::to('quotes').'">'.trans('texts.quotes').'</a></li>';
if($user->can('create', ENTITY_QUOTE))$items[] = '<li><a href="'.URL::to('quotes/create').'">'.trans('texts.new_quote').'</a></li>';
} else if ($type == ENTITY_CLIENT) {
if(!empty($items))$items[] = '<li class="divider"></li>';
$items[] = '<li><a href="'.URL::to('credits').'">'.trans('texts.credits').'</a></li>';

View File

@ -2026,7 +2026,8 @@ $LANG = array(
'restore_expense_category' => 'Restore expense category',
'restored_expense_category' => 'Successfully restored expense category',
'apply_taxes' => 'Apply taxes',
'min_to_max_users' => ':min to :max users',
'max_users_reached' => 'The maximum number of users has been reached.'
);

View File

@ -161,39 +161,41 @@
<p/>&nbsp;<p/>
{!! Former::actions(
count(Cache::get('banks')) > 0 ?
Button::normal(trans('texts.cancel'))
@if (Auth::user()->hasFeature(FEATURE_EXPENSES))
{!! Former::actions(
count(Cache::get('banks')) > 0 ?
Button::normal(trans('texts.cancel'))
->withAttributes([
'data-bind' => 'visible: !importResults()',
])
->large()
->asLinkTo(URL::to('/settings/bank_accounts'))
->appendIcon(Icon::create('remove-circle')) : false,
Button::success(trans('texts.validate'))
->withAttributes([
'data-bind' => 'visible: !importResults()',
'data-bind' => 'css: {disabled: disableValidate}, visible: page() == "login"',
'onclick' => 'validate()'
])
->large()
->asLinkTo(URL::to('/settings/bank_accounts'))
->appendIcon(Icon::create('remove-circle')) : false,
Button::success(trans('texts.validate'))
->withAttributes([
'data-bind' => 'css: {disabled: disableValidate}, visible: page() == "login"',
'onclick' => 'validate()'
])
->large()
->appendIcon(Icon::create('lock')),
Button::success(trans('texts.save'))
->withAttributes([
'data-bind' => 'css: {disabled: disableSave}, visible: page() == "setup"',
'style' => 'display:none',
'onclick' => 'save()'
])
->large()
->appendIcon(Icon::create('floppy-disk')) ,
Button::success(trans('texts.import'))
->withAttributes([
'data-bind' => 'css: {disabled: disableSaveExpenses}, visible: page() == "import"',
'style' => 'display:none',
'onclick' => 'saveExpenses()'
])
->large()
->appendIcon(Icon::create('floppy-disk'))) !!}
->appendIcon(Icon::create('lock')),
Button::success(trans('texts.save'))
->withAttributes([
'data-bind' => 'css: {disabled: disableSave}, visible: page() == "setup"',
'style' => 'display:none',
'onclick' => 'save()'
])
->large()
->appendIcon(Icon::create('floppy-disk')) ,
Button::success(trans('texts.import'))
->withAttributes([
'data-bind' => 'css: {disabled: disableSaveExpenses}, visible: page() == "import"',
'style' => 'display:none',
'onclick' => 'saveExpenses()'
])
->large()
->appendIcon(Icon::create('floppy-disk'))) !!}
@endif
{!! Former::close() !!}
<script type="text/javascript">

View File

@ -4,14 +4,16 @@
@parent
@include('accounts.nav', ['selected' => ACCOUNT_BANKS])
<div class="pull-right">
{!! Button::normal(trans('texts.import_ofx'))
->asLinkTo(URL::to('/bank_accounts/import_ofx'))
->appendIcon(Icon::create('open')) !!}
{!! Button::primary(trans('texts.add_bank_account'))
->asLinkTo(URL::to('/bank_accounts/create'))
->appendIcon(Icon::create('plus-sign')) !!}
</div>
@if (Auth::user()->hasFeature(FEATURE_EXPENSES))
<div class="pull-right">
{!! Button::normal(trans('texts.import_ofx'))
->asLinkTo(URL::to('/bank_accounts/import_ofx'))
->appendIcon(Icon::create('open')) !!}
{!! Button::primary(trans('texts.add_bank_account'))
->asLinkTo(URL::to('/bank_accounts/create'))
->appendIcon(Icon::create('plus-sign')) !!}
</div>
@endif
@include('partials.bulk_form', ['entityType' => ENTITY_BANK_ACCOUNT])

View File

@ -24,6 +24,9 @@
@elseif ($planDetails['expires'])
({{ trans('texts.plan_term_'.$planDetails['term'].'ly') }})
@endif
@if ($planDetails['plan'] == PLAN_ENTERPRISE)
{{ trans('texts.min_to_max_users', ['min' => Utils::getMinNumUsers($planDetails['num_users']), 'max' => $planDetails['num_users']])}}
@endif
@elseif(Utils::isNinjaProd())
{{ trans('texts.plan_free') }}
@else
@ -116,20 +119,33 @@
</h4>
</div>
<div class="modal-body">
@if ($planDetails && $planDetails['active'])
{!! Former::select('plan')
->addOption(trans('texts.plan_enterprise'), PLAN_ENTERPRISE)
->addOption(trans('texts.plan_pro'), PLAN_PRO)
->addOption(trans('texts.plan_free'), PLAN_FREE)!!}
{!! Former::select('plan')
->onchange('onPlanChange()')
->addOption(trans('texts.plan_free'), PLAN_FREE)
->addOption(trans('texts.plan_pro'), PLAN_PRO)
->addOption(trans('texts.plan_enterprise'), PLAN_ENTERPRISE) !!}
@else
{!! Former::select('plan')
->addOption(trans('texts.plan_pro'), PLAN_PRO)
->addOption(trans('texts.plan_enterprise'), PLAN_ENTERPRISE) !!}
{!! Former::select('plan')
->onchange('onPlanChange()')
->addOption(trans('texts.plan_pro'), PLAN_PRO)
->addOption(trans('texts.plan_enterprise'), PLAN_ENTERPRISE) !!}
@endif
<div id="numUsersDiv">
{!! Former::select('num_users')
->label(trans('texts.users'))
->addOption('1 to 2', 2)
->addOption('3 to 5', 5)
->addOption('6 to 10', 10) !!}
</div>
{!! Former::select('plan_term')
->addOption(trans('texts.plan_term_monthly'), PLAN_TERM_MONTHLY)
->addOption(trans('texts.plan_term_yearly'), PLAN_TERM_YEARLY)
->inlineHelp(trans('texts.enterprise_plan_features', ['link' => link_to(NINJA_WEB_URL . '/plans-pricing', trans('texts.click_here'), ['target' => '_blank'])])) !!}
</div>
<div class="modal-footer" style="margin-top: 0px">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ trans('texts.go_back') }}</button>
@ -197,30 +213,48 @@
$('form.cancel-account').submit();
}
function onPlanChange() {
if ($('#plan').val() == '{{ PLAN_ENTERPRISE }}') {
$('#numUsersDiv').show();
} else {
$('#numUsersDiv').hide();
}
}
@if ($account->company->pending_plan)
function cancelPendingChange(){
$('#plan').val('{{ $planDetails['plan'] }}')
$('#plan_term').val('{{ $planDetails['term'] }}')
confirmChangePlan();
return false;
}
function cancelPendingChange(){
$('#plan').val('{{ $planDetails['plan'] }}')
$('#plan_term').val('{{ $planDetails['term'] }}')
confirmChangePlan();
return false;
}
@endif
jQuery(document).ready(function($){
function updatePlanModal() {
var plan = $('#plan').val();
var numUsers = $('#num_users').val();
$('#plan_term').closest('.form-group').toggle(plan!='free');
if(plan=='{{PLAN_PRO}}'){
$('#plan_term option[value=month]').text({!! json_encode(trans('texts.plan_price_monthly', ['price'=>PLAN_PRICE_PRO_MONTHLY])) !!});
$('#plan_term option[value=year]').text({!! json_encode(trans('texts.plan_price_yearly', ['price'=>PLAN_PRICE_PRO_YEARLY])) !!});
$('#plan_term option[value=year]').text({!! json_encode(trans('texts.plan_price_yearly', ['price'=>PLAN_PRICE_PRO_MONTHLY * 10])) !!});
} else if(plan=='{{PLAN_ENTERPRISE}}') {
$('#plan_term option[value=month]').text({!! json_encode(trans('texts.plan_price_monthly', ['price'=>PLAN_PRICE_ENTERPRISE_MONTHLY])) !!});
$('#plan_term option[value=year]').text({!! json_encode(trans('texts.plan_price_yearly', ['price'=>PLAN_PRICE_ENTERPRISE_YEARLY])) !!});
if (numUsers == 2) {
$('#plan_term option[value=month]').text({!! json_encode(trans('texts.plan_price_monthly', ['price'=>PLAN_PRICE_ENTERPRISE_MONTHLY_2])) !!});
$('#plan_term option[value=year]').text({!! json_encode(trans('texts.plan_price_yearly', ['price'=>PLAN_PRICE_ENTERPRISE_MONTHLY_2 * 10])) !!});
} else if (numUsers == 5) {
$('#plan_term option[value=month]').text({!! json_encode(trans('texts.plan_price_monthly', ['price'=>PLAN_PRICE_ENTERPRISE_MONTHLY_5])) !!});
$('#plan_term option[value=year]').text({!! json_encode(trans('texts.plan_price_yearly', ['price'=>PLAN_PRICE_ENTERPRISE_MONTHLY_5 * 10])) !!});
} else {
$('#plan_term option[value=month]').text({!! json_encode(trans('texts.plan_price_monthly', ['price'=>PLAN_PRICE_ENTERPRISE_MONTHLY_10])) !!});
$('#plan_term option[value=year]').text({!! json_encode(trans('texts.plan_price_yearly', ['price'=>PLAN_PRICE_ENTERPRISE_MONTHLY_10 * 10])) !!});
}
}
}
$('#plan_term, #plan').change(updatePlanModal);
$('#plan_term, #plan, #num_users').change(updatePlanModal);
updatePlanModal();
onPlanChange();
if(window.location.hash) {
var hash = window.location.hash;

View File

@ -5,11 +5,13 @@
@include('accounts.nav', ['selected' => ACCOUNT_USER_MANAGEMENT, 'advanced' => true])
@if (Utils::hasFeature(FEATURE_USERS))
<div class="pull-right">
{!! Button::primary(trans('texts.add_user'))->asLinkTo(URL::to('/users/create'))->appendIcon(Icon::create('plus-sign')) !!}
</div>
@if (Auth::user()->caddAddUsers())
<div class="pull-right">
{!! Button::primary(trans('texts.add_user'))->asLinkTo(URL::to('/users/create'))->appendIcon(Icon::create('plus-sign')) !!}
</div>
@endif
@elseif (Utils::isTrial())
<div class="alert alert-warning">{!! trans('texts.add_users_not_supported') !!}</div>
<div class="alert alert-warning">{!! trans('texts.add_users_not_supported') !!}</div>
@endif
<label for="trashed" style="font-weight:normal; margin-left: 10px;">
@ -34,7 +36,7 @@
->render('datatable') !!}
<script>
window.onDatatableReady = actionListHandler;
function setTrashVisible() {

View File

@ -173,16 +173,18 @@
->appendIcon(Icon::create('remove-circle'))
->large() !!}
{!! Button::success(trans('texts.save'))
->appendIcon(Icon::create('floppy-disk'))
->large()
->submit() !!}
@if (Auth::user()->hasFeature(FEATURE_EXPENSES))
{!! Button::success(trans('texts.save'))
->appendIcon(Icon::create('floppy-disk'))
->large()
->submit() !!}
@if ($expense)
{!! DropdownButton::normal(trans('texts.more_actions'))
->withContents($actions)
->large()
->dropup() !!}
@if ($expense)
{!! DropdownButton::normal(trans('texts.more_actions'))
->withContents($actions)
->large()
->dropup() !!}
@endif
@endif
</center>

View File

@ -303,6 +303,11 @@
}
});
/* Set the defaults for Bootstrap datepicker */
$.extend(true, $.fn.datepicker.defaults, {
weekStart: {{ Session::get('start_of_week') }}
});
if (isStorageSupported()) {
@if (Auth::check() && !Auth::user()->registered)
localStorage.setItem('guest_key', '{{ Auth::user()->password }}');

View File

@ -496,6 +496,7 @@ function InvoiceModel(data) {
for (var key in taxes) {
if (taxes.hasOwnProperty(key)) {
total += taxes[key].amount;
total = roundToTwo(total);
}
}

View File

@ -93,11 +93,6 @@
}
});
/* Set the defaults for Bootstrap datepicker */
$.extend(true, $.fn.datepicker.defaults, {
weekStart: {{ Session::get('start_of_week') }}
});
/* This causes problems with some languages. ie, fr_CA
var appLocale = '{{App::getLocale()}}';
*/

View File

@ -38,7 +38,7 @@
{!! Former::text('action') !!}
{!! Former::text('time_log') !!}
</div>
<div class="row">
<div class="col-md-12">
@ -51,10 +51,10 @@
@if ($task)
<div class="form-group simple-time" id="editDetailsLink">
<label for="simple-time" class="control-label col-lg-4 col-sm-4">
<label for="simple-time" class="control-label col-lg-4 col-sm-4">
</label>
<div class="col-lg-8 col-sm-8" style="padding-top: 10px">
<p>{{ $task->getStartTime() }} -
<p>{{ $task->getStartTime() }} -
@if (Auth::user()->account->timezone_id)
{{ $timezone }}
@else
@ -73,7 +73,7 @@
</div>
@if ($task->is_running)
<center>
<center>
<div id="duration-text" style="font-size: 36px; font-weight: 300; padding: 30px 0 20px 0"/>
</center>
@endif
@ -96,20 +96,20 @@
<tr data-bind="event: { mouseover: showActions, mouseout: hideActions }">
<td style="padding: 0px 12px 12px 0 !important">
<div data-bind="css: { 'has-error': !isStartValid() }">
<input type="text" data-bind="dateTimePicker: startTime.pretty, event:{ change: $root.refresh }"
<input type="text" data-bind="dateTimePicker: startTime.pretty, event:{ change: $root.refresh }"
class="form-control time-input" placeholder="{{ trans('texts.start_time') }}"/>
</div>
</td>
<td style="padding: 0px 12px 12px 0 !important">
<div data-bind="css: { 'has-error': !isEndValid() }">
<input type="text" data-bind="dateTimePicker: endTime.pretty, event:{ change: $root.refresh }"
<input type="text" data-bind="dateTimePicker: endTime.pretty, event:{ change: $root.refresh }"
class="form-control time-input" placeholder="{{ trans('texts.end_time') }}"/>
</div>
</td>
<td style="width:100px">
<td style="width:100px">
<div data-bind="text: duration.pretty, visible: !isEmpty()"></div>
<a href="#" data-bind="click: function() { setNow(), $root.refresh() }, visible: isEmpty()">{{ trans('texts.set_now') }}</a>
</td>
</td>
<td style="width:30px" class="td-icon">
<i style="width:12px;cursor:pointer" data-bind="click: $root.removeItem, visible: actionsVisible() &amp;&amp; !isEmpty()" class="fa fa-minus-circle redlink" title="Remove item"/>
</td>
@ -122,30 +122,34 @@
</div>
</div>
</div>
</div>
</div>
<center class="buttons">
@if ($task && $task->is_running)
{!! Button::success(trans('texts.save'))->large()->appendIcon(Icon::create('floppy-disk'))->withAttributes(['id' => 'save-button']) !!}
{!! Button::primary(trans('texts.stop'))->large()->appendIcon(Icon::create('stop'))->withAttributes(['id' => 'stop-button']) !!}
@elseif ($task && $task->trashed())
{!! Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/tasks'))->appendIcon(Icon::create('remove-circle')) !!}
{!! Button::success(trans('texts.restore'))->large()->withAttributes(['onclick' => 'submitAction("restore")'])->appendIcon(Icon::create('cloud-download')) !!}
@if (Auth::user()->hasFeature(FEATURE_TASKS))
@if ($task && $task->is_running)
{!! Button::success(trans('texts.save'))->large()->appendIcon(Icon::create('floppy-disk'))->withAttributes(['id' => 'save-button']) !!}
{!! Button::primary(trans('texts.stop'))->large()->appendIcon(Icon::create('stop'))->withAttributes(['id' => 'stop-button']) !!}
@elseif ($task && $task->trashed())
{!! Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/tasks'))->appendIcon(Icon::create('remove-circle')) !!}
{!! Button::success(trans('texts.restore'))->large()->withAttributes(['onclick' => 'submitAction("restore")'])->appendIcon(Icon::create('cloud-download')) !!}
@else
{!! Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/tasks'))->appendIcon(Icon::create('remove-circle')) !!}
@if ($task)
{!! Button::success(trans('texts.save'))->large()->appendIcon(Icon::create('floppy-disk'))->withAttributes(['id' => 'save-button']) !!}
{!! Button::primary(trans('texts.resume'))->large()->appendIcon(Icon::create('play'))->withAttributes(['id' => 'resume-button']) !!}
{!! DropdownButton::normal(trans('texts.more_actions'))
->withContents($actions)
->large()
->dropup() !!}
@else
{!! Button::success(trans('texts.save'))->large()->appendIcon(Icon::create('floppy-disk'))->withAttributes(['id' => 'save-button']) !!}
{!! Button::success(trans('texts.start'))->large()->appendIcon(Icon::create('play'))->withAttributes(['id' => 'start-button']) !!}
@endif
@endif
@else
{!! Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/tasks'))->appendIcon(Icon::create('remove-circle')) !!}
@if ($task)
{!! Button::success(trans('texts.save'))->large()->appendIcon(Icon::create('floppy-disk'))->withAttributes(['id' => 'save-button']) !!}
{!! Button::primary(trans('texts.resume'))->large()->appendIcon(Icon::create('play'))->withAttributes(['id' => 'resume-button']) !!}
{!! DropdownButton::normal(trans('texts.more_actions'))
->withContents($actions)
->large()
->dropup() !!}
@else
{!! Button::success(trans('texts.save'))->large()->appendIcon(Icon::create('floppy-disk'))->withAttributes(['id' => 'save-button']) !!}
{!! Button::success(trans('texts.start'))->large()->appendIcon(Icon::create('play'))->withAttributes(['id' => 'start-button']) !!}
@endif
@endif
</center>
@ -199,7 +203,7 @@
timeLabels['{{ $period }}'] = '{{ trans("texts.{$period}") }}';
timeLabels['{{ $period }}s'] = '{{ trans("texts.{$period}s") }}';
@endforeach
function tock(duration) {
var str = convertDurationToString(duration);
$('#duration-text').html(str);
@ -217,7 +221,7 @@
for (var i=0; i<periods.length; i++) {
var period = periods[i];
var letter = period.charAt(0);
var value = parts[letter];
var value = parts[letter];
if (!value) {
continue;
}
@ -244,9 +248,9 @@
}
function onDeleteClick() {
if (confirm('{!! trans("texts.are_you_sure") !!}')) {
submitAction('delete');
}
if (confirm('{!! trans("texts.are_you_sure") !!}')) {
submitAction('delete');
}
}
function showTimeDetails() {
@ -277,9 +281,9 @@
});
self.startTime.pretty = ko.computed({
read: function() {
return self.startTime() ? moment.unix(self.startTime()).tz(timezone).format(dateTimeFormat) : '';
},
read: function() {
return self.startTime() ? moment.unix(self.startTime()).tz(timezone).format(dateTimeFormat) : '';
},
write: function(data) {
self.startTime(moment(data, dateTimeFormat).tz(timezone).unix());
}
@ -288,7 +292,7 @@
self.endTime.pretty = ko.computed({
read: function() {
return self.endTime() ? moment.unix(self.endTime()).tz(timezone).format(dateTimeFormat) : '';
},
},
write: function(data) {
self.endTime(moment(data, dateTimeFormat).tz(timezone).unix());
}
@ -316,7 +320,7 @@
self.isEmpty = function() {
return false;
};
*/
*/
self.hideActions = function() {
self.actionsVisible(false);
@ -324,7 +328,7 @@
self.showActions = function() {
self.actionsVisible(true);
};
};
}
function loadTimeLog(data) {
@ -385,7 +389,7 @@
if (timeLog.endTime() < Math.min(timeLog.startTime(), lastTime)) {
endValid = false;
}
lastTime = Math.max(lastTime, timeLog.endTime());
lastTime = Math.max(lastTime, timeLog.endTime());
}
timeLog.isStartValid(startValid);
timeLog.isEndValid(endValid);
@ -394,25 +398,25 @@
self.addItem = function() {
self.time_log.push(new TimeModel());
}
}
}
window.model = new ViewModel({!! $task !!});
ko.applyBindings(model);
$(function() {
var $clientSelect = $('select#client');
var $clientSelect = $('select#client');
for (var i=0; i<clients.length; i++) {
var client = clients[i];
$clientSelect.append(new Option(getClientDisplayName(client), client.public_id));
}
}
if ({{ $clientPublicId ? 'true' : 'false' }}) {
$clientSelect.val({{ $clientPublicId }});
}
$clientSelect.combobox();
@if (!$task && !$clientPublicId)
$('.client-select input.form-control').focus();
@else
@ -466,7 +470,7 @@
model.showTimeOverlaps();
showTimeDetails();
@endif
});
});
</script>

View File

@ -113,48 +113,6 @@
->fromQuery($currencies, 'name', 'id') !!}
{!! Former::textarea('private_notes')->rows(6) !!}
@if (Auth::user()->account->isNinjaAccount())
@if (isset($planDetails))
{!! Former::populateField('plan', $planDetails['plan']) !!}
{!! Former::populateField('plan_term', $planDetails['term']) !!}
@if (!empty($planDetails['paid']))
{!! Former::populateField('plan_paid', $planDetails['paid']->format('Y-m-d')) !!}
@endif
@if (!empty($planDetails['expires']))
{!! Former::populateField('plan_expires', $planDetails['expires']->format('Y-m-d')) !!}
@endif
@if (!empty($planDetails['started']))
{!! Former::populateField('plan_started', $planDetails['started']->format('Y-m-d')) !!}
@endif
@endif
{!! Former::select('plan')
->addOption(trans('texts.plan_free'), PLAN_FREE)
->addOption(trans('texts.plan_pro'), PLAN_PRO)
->addOption(trans('texts.plan_enterprise'), PLAN_ENTERPRISE)!!}
{!! Former::select('plan_term')
->addOption()
->addOption(trans('texts.plan_term_yearly'), PLAN_TERM_YEARLY)
->addOption(trans('texts.plan_term_monthly'), PLAN_TERM_MONTHLY)!!}
{!! Former::text('plan_started')
->data_date_format('yyyy-mm-dd')
->addGroupClass('plan_start_date')
->append('<i class="glyphicon glyphicon-calendar"></i>') !!}
{!! Former::text('plan_paid')
->data_date_format('yyyy-mm-dd')
->addGroupClass('plan_paid_date')
->append('<i class="glyphicon glyphicon-calendar"></i>') !!}
{!! Former::text('plan_expires')
->data_date_format('yyyy-mm-dd')
->addGroupClass('plan_expire_date')
->append('<i class="glyphicon glyphicon-calendar"></i>') !!}
<script type="text/javascript">
$(function() {
$('#plan_started, #plan_paid, #plan_expires').datepicker();
});
</script>
@endif
</div>
</div>