diff --git a/app/Console/Commands/SendReminders.php b/app/Console/Commands/SendReminders.php index 3243ac016e..58c38a7d81 100644 --- a/app/Console/Commands/SendReminders.php +++ b/app/Console/Commands/SendReminders.php @@ -36,7 +36,7 @@ class SendReminders extends Command $this->info(count($accounts).' accounts found'); foreach ($accounts as $account) { - if (!$account->isPro()) { + if (!$account->hasFeature(FEATURE_EMAIL_TEMPLATES_REMINDERS)) { continue; } diff --git a/app/Console/Commands/SendRenewalInvoices.php b/app/Console/Commands/SendRenewalInvoices.php index 0faf1304a9..64db2fad4f 100644 --- a/app/Console/Commands/SendRenewalInvoices.php +++ b/app/Console/Commands/SendRenewalInvoices.php @@ -5,7 +5,7 @@ use DateTime; use Illuminate\Console\Command; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; -use App\Models\Account; +use App\Models\Company; use App\Ninja\Mailers\ContactMailer as Mailer; use App\Ninja\Repositories\AccountRepository; @@ -30,24 +30,32 @@ class SendRenewalInvoices extends Command $today = new DateTime(); $sentTo = []; - // get all accounts with pro plans expiring in 10 days - $accounts = Account::whereRaw('datediff(curdate(), pro_plan_paid) = 355') + // get all accounts with plans expiring in 10 days + $companies = Company::whereRaw('datediff(plan_expires, curdate()) = 10') ->orderBy('id') ->get(); - $this->info(count($accounts).' accounts found'); + $this->info(count($companies).' companies found'); - foreach ($accounts as $account) { - // don't send multiple invoices to multi-company users - if ($userAccountId = $this->accountRepo->getUserAccountId($account)) { - if (isset($sentTo[$userAccountId])) { - continue; - } else { - $sentTo[$userAccountId] = true; - } + foreach ($companies as $company) { + if (!count($company->accounts)) { + continue; } - + + $account = $company->accounts->sortBy('id')->first(); + $plan = $company->plan; + $term = $company->plan_term; + + if ($company->pending_plan) { + $plan = $company->pending_plan; + $term = $company->pending_term; + } + + if ($plan == PLAN_FREE || !$plan || !$term ){ + continue; + } + $client = $this->accountRepo->getNinjaClient($account); - $invitation = $this->accountRepo->createNinjaInvoice($client, $account); + $invitation = $this->accountRepo->createNinjaInvoice($client, $account, $plan, $term); // set the due date to 10 days from now $invoice = $invitation->invoice; diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 76b2087ad8..5333d90f9e 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -71,11 +71,10 @@ class Handler extends ExceptionHandler { } } - return parent::render($request, $e); - - /* // In production, except for maintenance mode, we'll show a custom error screen - if (Utils::isNinjaProd() && !Utils::isDownForMaintenance()) { + if (Utils::isNinjaProd() + && !Utils::isDownForMaintenance() + && !($e instanceof HttpResponseException)) { $data = [ 'error' => get_class($e), 'hideHeader' => true, @@ -85,6 +84,5 @@ class Handler extends ExceptionHandler { } else { return parent::render($request, $e); } - */ } } diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 6fe75a6336..0ccb33545f 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -31,6 +31,7 @@ use App\Ninja\Mailers\ContactMailer; use App\Events\UserSignedUp; use App\Events\UserSettingsChanged; use App\Services\AuthService; +use App\Services\PaymentService; use App\Http\Requests\UpdateAccountRequest; @@ -40,8 +41,9 @@ class AccountController extends BaseController protected $userMailer; protected $contactMailer; protected $referralRepository; + protected $paymentService; - public function __construct(AccountRepository $accountRepo, UserMailer $userMailer, ContactMailer $contactMailer, ReferralRepository $referralRepository) + public function __construct(AccountRepository $accountRepo, UserMailer $userMailer, ContactMailer $contactMailer, ReferralRepository $referralRepository, PaymentService $paymentService) { //parent::__construct(); @@ -49,6 +51,7 @@ class AccountController extends BaseController $this->userMailer = $userMailer; $this->contactMailer = $contactMailer; $this->referralRepository = $referralRepository; + $this->paymentService = $paymentService; } public function demo() @@ -111,10 +114,135 @@ class AccountController extends BaseController public function enableProPlan() { - $invitation = $this->accountRepo->enableProPlan(); + if (Auth::user()->isPro() && ! Auth::user()->isTrial()) { + return false; + } + + $invitation = $this->accountRepo->enablePlan(); return $invitation->invitation_key; } + + public function changePlan() { + $user = Auth::user(); + $account = $user->account; + + $plan = Input::get('plan'); + $term = Input::get('plan_term'); + + $planDetails = $account->getPlanDetails(false, false); + + $credit = 0; + if ($planDetails) { + if ($planDetails['plan'] == PLAN_PRO && $plan == PLAN_ENTERPRISE) { + // Upgrade from pro to enterprise + if($planDetails['term'] == PLAN_TERM_YEARLY && $term == PLAN_TERM_MONTHLY) { + // Upgrade to yearly for now; switch to monthly in a year + $pending_monthly = true; + $term = PLAN_TERM_YEARLY; + } + + $new_plan = array( + 'plan' => PLAN_ENTERPRISE, + 'term' => $term, + ); + } elseif ($planDetails['plan'] == $plan) { + // Term switch + if ($planDetails['term'] == PLAN_TERM_YEARLY && $term == PLAN_TERM_MONTHLY) { + $pending_change = array( + 'plan' => $plan, + 'term' => $term + ); + } elseif ($planDetails['term'] == PLAN_TERM_MONTHLY && $term == PLAN_TERM_YEARLY) { + $new_plan = array( + 'plan' => $plan, + 'term' => $term, + ); + } else { + // Cancel the pending change + $account->company->pending_plan = null; + $account->company->pending_term = null; + $account->company->save(); + Session::flash('message', trans('texts.updated_plan')); + } + } else { + // Downgrade + $refund_deadline = clone $planDetails['started']; + $refund_deadline->modify('+30 days'); + + if ($plan == PLAN_FREE && $refund_deadline >= date_create()) { + // Refund + $account->company->plan = null; + $account->company->plan_term = null; + $account->company->plan_started = null; + $account->company->plan_expires = null; + $account->company->plan_paid = null; + $account->company->pending_plan = null; + $account->company->pending_term = null; + + if ($account->company->payment) { + $payment = $account->company->payment; + + $gateway = $this->paymentService->createGateway($payment->account_gateway); + $refund = $gateway->refund(array( + 'transactionReference' => $payment->transaction_reference, + 'amount' => $payment->amount * 100 + )); + $refund->send(); + $payment->delete(); + Session::flash('message', trans('texts.plan_refunded')); + \Log::info("Refunded Plan Payment: {$account->name} - {$user->email}"); + } else { + Session::flash('message', trans('texts.updated_plan')); + } + + $account->company->save(); + + } else { + $pending_change = array( + 'plan' => $plan, + 'term' => $plan == PLAN_FREE ? null : $term, + ); + } + } + + if (!empty($new_plan)) { + $time_used = $planDetails['paid']->diff(date_create()); + $days_used = $time_used->days; + + if ($time_used->invert) { + // They paid in advance + $days_used *= -1; + } + + $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); + } + } else { + $new_plan = array( + 'plan' => $plan, + 'term' => $term, + ); + } + + if (!empty($pending_change) && empty($new_plan)) { + $account->company->pending_plan = $pending_change['plan']; + $account->company->pending_term = $pending_change['term']; + $account->company->save(); + + Session::flash('message', trans('texts.updated_plan')); + } + + if (!empty($new_plan)) { + $invitation = $this->accountRepo->enablePlan($new_plan['plan'], $new_plan['term'], $credit, !empty($pending_monthly)); + return Redirect::to('payment/'.$invitation->invitation_key); + } + + return Redirect::to('/settings/'.ACCOUNT_MANAGEMENT, 301); + } public function setTrashVisible($entityType, $visible) { @@ -149,6 +277,8 @@ class AccountController extends BaseController return self::showInvoiceSettings(); } elseif ($section == ACCOUNT_IMPORT_EXPORT) { return View::make('accounts.import_export', ['title' => trans('texts.import_export')]); + } elseif ($section == ACCOUNT_MANAGEMENT) { + return self::showAccountManagement(); } elseif ($section == ACCOUNT_INVOICE_DESIGN || $section == ACCOUNT_CUSTOMIZE_DESIGN) { return self::showInvoiceDesign($section); } elseif ($section == ACCOUNT_CLIENT_PORTAL) { @@ -232,6 +362,18 @@ class AccountController extends BaseController return View::make('accounts.details', $data); } + private function showAccountManagement() + { + $account = Auth::user()->account; + $data = [ + 'account' => $account, + 'planDetails' => $account->getPlanDetails(true), + 'title' => trans('texts.account_management'), + ]; + + return View::make('accounts.management', $data); + } + public function showUserDetails() { $oauthLoginUrls = []; @@ -379,7 +521,7 @@ class AccountController extends BaseController $invoice->client = $client; $invoice->invoice_items = [$invoiceItem]; - //$invoice->documents = $account->isPro() ? [$document] : []; + //$invoice->documents = $account->hasFeature(FEATURE_DOCUMENTS) ? [$document] : []; $invoice->documents = []; $data['account'] = $account; @@ -389,6 +531,58 @@ class AccountController extends BaseController $data['invoiceDesigns'] = InvoiceDesign::getDesigns(); $data['invoiceFonts'] = Cache::get('fonts'); $data['section'] = $section; + + $pageSizes = [ + 'A0', + 'A1', + 'A2', + 'A3', + 'A4', + 'A5', + 'A6', + 'A7', + 'A8', + 'A9', + 'A10', + 'B0', + 'B1', + 'B2', + 'B3', + 'B4', + 'B5', + 'B6', + 'B7', + 'B8', + 'B9', + 'B10', + 'C0', + 'C1', + 'C2', + 'C3', + 'C4', + 'C5', + 'C6', + 'C7', + 'C8', + 'C9', + 'C10', + 'RA0', + 'RA1', + 'RA2', + 'RA3', + 'RA4', + 'SRA0', + 'SRA1', + 'SRA2', + 'SRA3', + 'SRA4', + 'Executive', + 'Folio', + 'Legal', + 'Letter', + 'Tabloid', + ]; + $data['pageSizes'] = array_combine($pageSizes, $pageSizes); $design = false; foreach ($data['invoiceDesigns'] as $item) { @@ -501,7 +695,7 @@ class AccountController extends BaseController private function saveCustomizeDesign() { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN)) { $account = Auth::user()->account; $account->custom_design = Input::get('custom_design'); $account->invoice_design_id = CUSTOM_DESIGN; @@ -516,7 +710,7 @@ class AccountController extends BaseController private function saveClientPortal() { // Only allowed for pro Invoice Ninja users or white labeled self-hosted users - if ((Utils::isNinja() && Auth::user()->account->isPro()) || Auth::user()->account->isWhiteLabel()) { + if (Auth::user()->account->hasFeature(FEATURE_CLIENT_PORTAL_CSS)) { $input_css = Input::get('client_view_css'); if (Utils::isNinja()) { // Allow referencing the body element @@ -567,7 +761,7 @@ class AccountController extends BaseController private function saveEmailTemplates() { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_EMAIL_TEMPLATES_REMINDERS)) { $account = Auth::user()->account; foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) { @@ -629,7 +823,7 @@ class AccountController extends BaseController private function saveEmailSettings() { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_CUSTOM_EMAILS)) { $rules = []; $user = Auth::user(); $iframeURL = preg_replace('/[^a-zA-Z0-9_\-\:\/\.]/', '', substr(strtolower(Input::get('iframe_url')), 0, MAX_IFRAME_URL_LENGTH)); @@ -672,7 +866,7 @@ class AccountController extends BaseController private function saveInvoiceSettings() { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_INVOICE_SETTINGS)) { $rules = [ 'invoice_number_pattern' => 'has_counter', 'quote_number_pattern' => 'has_counter', @@ -701,6 +895,7 @@ class AccountController extends BaseController $account->custom_invoice_item_label1 = trim(Input::get('custom_invoice_item_label1')); $account->custom_invoice_item_label2 = trim(Input::get('custom_invoice_item_label2')); + $account->invoice_number_padding = Input::get('invoice_number_padding'); $account->invoice_number_counter = Input::get('invoice_number_counter'); $account->quote_number_prefix = Input::get('quote_number_prefix'); $account->share_counter = Input::get('share_counter') ? true : false; @@ -752,7 +947,7 @@ class AccountController extends BaseController private function saveInvoiceDesign() { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN)) { $account = Auth::user()->account; $account->hide_quantity = Input::get('hide_quantity') ? true : false; $account->hide_paid_to_date = Input::get('hide_paid_to_date') ? true : false; @@ -764,9 +959,20 @@ class AccountController extends BaseController $account->primary_color = Input::get('primary_color'); $account->secondary_color = Input::get('secondary_color'); $account->invoice_design_id = Input::get('invoice_design_id'); + $account->font_size = intval(Input::get('font_size')); + $account->page_size = Input::get('page_size'); + $account->live_preview = Input::get('live_preview') ? true : false; - if (Input::has('font_size')) { - $account->font_size = intval(Input::get('font_size')); + // Automatically disable live preview when using a large font + $fonts = Cache::get('fonts')->filter(function($font) use ($account) { + if ($font->google_font) { + return false; + } + return $font->id == $account->header_font_id || $font->id == $account->body_font_id; + }); + if ($account->live_preview && count($fonts)) { + $account->live_preview = false; + Session::flash('warning', trans('texts.live_preview_disabled')); } $labels = []; @@ -990,7 +1196,7 @@ class AccountController extends BaseController $user->registered = true; $user->save(); - $user->account->startTrial(); + $user->account->startTrial(PLAN_PRO); if (Input::get('go_pro') == 'true') { Session::set(REQUESTED_PRO_PLAN, true); @@ -1046,6 +1252,9 @@ class AccountController extends BaseController \Log::info("Canceled Account: {$account->name} - {$user->email}"); $this->accountRepo->unlinkAccount($account); + if ($account->company->accounts->count() == 1) { + $account->company->forceDelete(); + } $account->forceDelete(); Auth::logout(); @@ -1062,12 +1271,12 @@ class AccountController extends BaseController return Redirect::to('/settings/'.ACCOUNT_USER_DETAILS)->with('message', trans('texts.confirmation_resent')); } - public function startTrial() + public function startTrial($plan) { $user = Auth::user(); - if ($user->isEligibleForTrial()) { - $user->account->startTrial(); + if ($user->isEligibleForTrial($plan)) { + $user->account->startTrial($plan); } return Redirect::back()->with('message', trans('texts.trial_success')); diff --git a/app/Http/Controllers/AppController.php b/app/Http/Controllers/AppController.php index 1fc3f75888..9dcf243fbe 100644 --- a/app/Http/Controllers/AppController.php +++ b/app/Http/Controllers/AppController.php @@ -178,9 +178,12 @@ class AppController extends BaseController $config = ''; foreach ($_ENV as $key => $val) { - if (preg_match('/\s/',$val)) { - $val = "'{$val}'"; - } + if (is_array($val)) { + continue; + } + if (preg_match('/\s/', $val)) { + $val = "'{$val}'"; + } $config .= "{$key}={$val}\n"; } diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index bd8813912d..e599890c6a 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -133,6 +133,9 @@ class AuthController extends Controller { if (Auth::check() && !Auth::user()->registered) { $account = Auth::user()->account; $this->accountRepo->unlinkAccount($account); + if ($account->company->accounts->count() == 1) { + $account->company->forceDelete(); + } $account->forceDelete(); } diff --git a/app/Http/Controllers/ClientAuth/AuthController.php b/app/Http/Controllers/ClientAuth/AuthController.php index c88c8a4b85..ed5f199ac2 100644 --- a/app/Http/Controllers/ClientAuth/AuthController.php +++ b/app/Http/Controllers/ClientAuth/AuthController.php @@ -33,7 +33,7 @@ class AuthController extends Controller { $client = $invoice->client; $account = $client->account; - $data['hideLogo'] = $account->isWhiteLabel(); + $data['hideLogo'] = $account->hasFeature(FEATURE_WHITE_LABEL); $data['clientViewCSS'] = $account->clientViewCSS(); $data['clientFontUrl'] = $account->getFontsUrl(); } diff --git a/app/Http/Controllers/ClientAuth/PasswordController.php b/app/Http/Controllers/ClientAuth/PasswordController.php index beefb01612..35576b47ae 100644 --- a/app/Http/Controllers/ClientAuth/PasswordController.php +++ b/app/Http/Controllers/ClientAuth/PasswordController.php @@ -50,7 +50,7 @@ class PasswordController extends Controller { $client = $invoice->client; $account = $client->account; - $data['hideLogo'] = $account->isWhiteLabel(); + $data['hideLogo'] = $account->hasFeature(FEATURE_WHITE_LABEL); $data['clientViewCSS'] = $account->clientViewCSS(); $data['clientFontUrl'] = $account->getFontsUrl(); } @@ -117,7 +117,7 @@ class PasswordController extends Controller { $client = $invoice->client; $account = $client->account; - $data['hideLogo'] = $account->isWhiteLabel(); + $data['hideLogo'] = $account->hasFeature(FEATURE_WHITE_LABEL); $data['clientViewCSS'] = $account->clientViewCSS(); $data['clientFontUrl'] = $account->getFontsUrl(); } diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 7f2fac3eed..2992eb9a95 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -114,7 +114,7 @@ class ClientController extends BaseController if(Task::canCreate()){ $actionLinks[] = ['label' => trans('texts.new_task'), 'url' => URL::to('/tasks/create/'.$client->public_id)]; } - if (Utils::isPro() && Invoice::canCreate()) { + if (Utils::hasFeature(FEATURE_QUOTES) && Invoice::canCreate()) { $actionLinks[] = ['label' => trans('texts.new_quote'), 'url' => URL::to('/quotes/create/'.$client->public_id)]; } @@ -201,7 +201,7 @@ class ClientController extends BaseController if (Auth::user()->account->isNinjaAccount()) { if ($account = Account::whereId($client->public_id)->first()) { - $data['proPlanPaid'] = $account['pro_plan_paid']; + $data['planDetails'] = $account->getPlanDetails(false, false); } } diff --git a/app/Http/Controllers/DocumentController.php b/app/Http/Controllers/DocumentController.php index 8af0d560d6..15f3ced57f 100644 --- a/app/Http/Controllers/DocumentController.php +++ b/app/Http/Controllers/DocumentController.php @@ -114,7 +114,7 @@ class DocumentController extends BaseController public function postUpload() { - if (!Utils::isPro()) { + if (!Utils::hasFeature(FEATURE_DOCUMENTS)) { return; } diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 794f09404a..774ed7a7a7 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -132,7 +132,11 @@ class InvoiceController extends BaseController $invoice->start_date = Utils::fromSqlDate($invoice->start_date); $invoice->end_date = Utils::fromSqlDate($invoice->end_date); $invoice->last_sent_date = Utils::fromSqlDate($invoice->last_sent_date); - $invoice->is_pro = Auth::user()->isPro(); + $invoice->features = [ + 'customize_invoice_design' => Auth::user()->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN), + 'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY), + 'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS), + ]; $actions = [ ['url' => 'javascript:onCloneClick()', 'label' => trans("texts.clone_{$entityType}")], @@ -573,7 +577,11 @@ class InvoiceController extends BaseController $invoice->load('user', 'invoice_items', 'documents', 'expenses', 'expenses.documents', 'account.country', 'client.contacts', 'client.country'); $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); $invoice->due_date = Utils::fromSqlDate($invoice->due_date); - $invoice->is_pro = Auth::user()->isPro(); + $invoice->features = [ + 'customize_invoice_design' => Auth::user()->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN), + 'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY), + 'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS), + ]; $invoice->is_quote = intval($invoice->is_quote); $activityTypeId = $invoice->is_quote ? ACTIVITY_TYPE_UPDATE_QUOTE : ACTIVITY_TYPE_UPDATE_INVOICE; @@ -591,7 +599,11 @@ class InvoiceController extends BaseController $backup = json_decode($activity->json_backup); $backup->invoice_date = Utils::fromSqlDate($backup->invoice_date); $backup->due_date = Utils::fromSqlDate($backup->due_date); - $backup->is_pro = Auth::user()->isPro(); + $invoice->features = [ + 'customize_invoice_design' => Auth::user()->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN), + 'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY), + 'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS), + ]; $backup->is_quote = isset($backup->is_quote) && intval($backup->is_quote); $backup->account = $invoice->account->toArray(); diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 2f2cdd92cc..1f53b4a0bc 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -191,7 +191,7 @@ class PaymentController extends BaseController 'currencyId' => $client->getCurrencyId(), 'currencyCode' => $client->currency ? $client->currency->code : ($account->currency ? $account->currency->code : 'USD'), 'account' => $client->account, - 'hideLogo' => $account->isWhiteLabel(), + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), 'hideHeader' => $account->isNinjaAccount(), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php index b7649471f0..32d6d53c38 100644 --- a/app/Http/Controllers/PublicClientController.php +++ b/app/Http/Controllers/PublicClientController.php @@ -72,7 +72,11 @@ class PublicClientController extends BaseController $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); $invoice->due_date = Utils::fromSqlDate($invoice->due_date); - $invoice->is_pro = $account->isPro(); + $invoice->features = [ + 'customize_invoice_design' => $account->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN), + 'remove_created_by' => $account->hasFeature(FEATURE_REMOVE_CREATED_BY), + 'invoice_settings' => $account->hasFeature(FEATURE_INVOICE_SETTINGS), + ]; $invoice->invoice_fonts = $account->getFontsData(); if ($invoice->invoice_design_id == CUSTOM_DESIGN) { @@ -122,10 +126,10 @@ class PublicClientController extends BaseController 'account' => $account, 'showApprove' => $showApprove, 'showBreadcrumbs' => false, - 'hideLogo' => $account->isWhiteLabel(), + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), 'hideHeader' => $account->isNinjaAccount() || !$account->enable_client_portal, 'hideDashboard' => !$account->enable_client_portal_dashboard, - 'showDocuments' => $account->isPro(), + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), 'invoice' => $invoice->hidePrivateFields(), @@ -140,7 +144,7 @@ class PublicClientController extends BaseController 'phantomjs' => Input::has('phantomjs'), ); - if($account->isPro() && $this->canCreateZip()){ + if($account->hasFeature(FEATURE_DOCUMENTS) && $this->canCreateZip()){ $zipDocs = $this->getInvoiceZipDocuments($invoice, $size); if(count($zipDocs) > 1){ @@ -220,8 +224,8 @@ class PublicClientController extends BaseController 'color' => $color, 'account' => $account, 'client' => $client, - 'hideLogo' => $account->isWhiteLabel(), - 'showDocuments' => $account->isPro(), + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), ]; @@ -273,9 +277,9 @@ class PublicClientController extends BaseController $data = [ 'color' => $color, - 'hideLogo' => $account->isWhiteLabel(), + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), 'hideDashboard' => !$account->enable_client_portal_dashboard, - 'showDocuments' => $account->isPro(), + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), 'title' => trans('texts.invoices'), @@ -310,9 +314,9 @@ class PublicClientController extends BaseController $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, - 'hideLogo' => $account->isWhiteLabel(), + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), 'hideDashboard' => !$account->enable_client_portal_dashboard, - 'showDocuments' => $account->isPro(), + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), 'entityType' => ENTITY_PAYMENT, @@ -354,9 +358,9 @@ class PublicClientController extends BaseController $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, - 'hideLogo' => $account->isWhiteLabel(), + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), 'hideDashboard' => !$account->enable_client_portal_dashboard, - 'showDocuments' => $account->isPro(), + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), 'title' => trans('texts.quotes'), @@ -392,9 +396,9 @@ class PublicClientController extends BaseController $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, - 'hideLogo' => $account->isWhiteLabel(), + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), 'hideDashboard' => !$account->enable_client_portal_dashboard, - 'showDocuments' => $account->isPro(), + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), 'title' => trans('texts.documents'), diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php index 20fdd484ba..4aaa504bfd 100644 --- a/app/Http/Controllers/QuoteController.php +++ b/app/Http/Controllers/QuoteController.php @@ -47,7 +47,7 @@ class QuoteController extends BaseController public function index() { - if (!Utils::isPro()) { + if (!Utils::hasFeature(FEATURE_QUOTES)) { return Redirect::to('/invoices/create'); } @@ -84,7 +84,7 @@ class QuoteController extends BaseController return $response; } - if (!Utils::isPro()) { + if (!Utils::hasFeature(FEATURE_QUOTES)) { return Redirect::to('/invoices/create'); } diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 2d21e0b593..a63b16d9fc 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -21,7 +21,7 @@ class ReportController extends BaseController $message = ''; $fileName = storage_path().'/dataviz_sample.txt'; - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) { $account = Account::where('id', '=', Auth::user()->account->id) ->with(['clients.invoices.invoice_items', 'clients.contacts']) ->first(); @@ -99,7 +99,7 @@ class ReportController extends BaseController 'title' => trans('texts.charts_and_reports'), ]; - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) { if ($enableReport) { $isExport = $action == 'export'; $params = array_merge($params, self::generateReport($reportType, $startDate, $endDate, $dateField, $isExport)); diff --git a/app/Http/Controllers/TokenController.php b/app/Http/Controllers/TokenController.php index 8e255d6057..aa5434a269 100644 --- a/app/Http/Controllers/TokenController.php +++ b/app/Http/Controllers/TokenController.php @@ -93,7 +93,7 @@ class TokenController extends BaseController */ public function save($tokenPublicId = false) { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_API)) { $rules = [ 'name' => 'required', ]; diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 2e3f675aa5..6b3ab9ca5b 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -164,7 +164,7 @@ class UserController extends BaseController */ public function save($userPublicId = false) { - if (Auth::user()->isPro() && ! Auth::user()->isTrial()) { + if (Auth::user()->hasFeature(FEATURE_USERS)) { $rules = [ 'first_name' => 'required', 'last_name' => 'required', @@ -190,8 +190,10 @@ class UserController extends BaseController $user->last_name = trim(Input::get('last_name')); $user->username = trim(Input::get('email')); $user->email = trim(Input::get('email')); - $user->is_admin = boolval(Input::get('is_admin')); - $user->permissions = Input::get('permissions'); + if (Auth::user()->hasFeature(FEATURE_USER_PERMISSIONS)) { + $user->is_admin = boolval(Input::get('is_admin')); + $user->permissions = Input::get('permissions'); + } } else { $lastUser = User::withTrashed()->where('account_id', '=', Auth::user()->account_id) ->orderBy('public_id', 'DESC')->first(); @@ -202,12 +204,14 @@ class UserController extends BaseController $user->last_name = trim(Input::get('last_name')); $user->username = trim(Input::get('email')); $user->email = trim(Input::get('email')); - $user->is_admin = boolval(Input::get('is_admin')); $user->registered = true; $user->password = str_random(RANDOM_KEY_LENGTH); $user->confirmation_code = str_random(RANDOM_KEY_LENGTH); $user->public_id = $lastUser->public_id + 1; - $user->permissions = Input::get('permissions'); + if (Auth::user()->hasFeature(FEATURE_USER_PERMISSIONS)) { + $user->is_admin = boolval(Input::get('is_admin')); + $user->permissions = Input::get('permissions'); + } } $user->save(); @@ -286,6 +290,9 @@ class UserController extends BaseController if (!Auth::user()->registered) { $account = Auth::user()->account; $this->accountRepo->unlinkAccount($account); + if ($account->company->accounts->count() == 1) { + $account->company->forceDelete(); + } $account->forceDelete(); } } diff --git a/app/Http/Controllers/VendorController.php b/app/Http/Controllers/VendorController.php index 032f8d423d..b337c1df29 100644 --- a/app/Http/Controllers/VendorController.php +++ b/app/Http/Controllers/VendorController.php @@ -175,8 +175,8 @@ class VendorController extends BaseController $data = array_merge($data, self::getViewModel()); if (Auth::user()->account->isNinjaAccount()) { - if ($account = Account::whereId($vendor->public_id)->first()) { - $data['proPlanPaid'] = $account['pro_plan_paid']; + if ($account = Account::whereId($client->public_id)->first()) { + $data['planDetails'] = $account->getPlanDetails(false, false); } } diff --git a/app/Http/Middleware/ApiCheck.php b/app/Http/Middleware/ApiCheck.php index 63e370cf41..8b38c60fe2 100644 --- a/app/Http/Middleware/ApiCheck.php +++ b/app/Http/Middleware/ApiCheck.php @@ -47,7 +47,7 @@ class ApiCheck { return $next($request); } - if (!Utils::isPro() && !$loggingIn) { + if (!Utils::hasFeature(FEATURE_API) && !$loggingIn) { return Response::json('API requires pro plan', 403, $headers); } else { $key = Auth::check() ? Auth::user()->account->id : $request->getClientIp(); diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index 3a49c762a9..81fde62439 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -42,7 +42,7 @@ class Authenticate { // Does this account require portal passwords? $account = Account::whereId($account_id)->first(); - if($account && (!$account->enable_portal_password || !$account->isPro())){ + if($account && (!$account->enable_portal_password || !$account->hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD))){ $authenticated = true; } diff --git a/app/Http/Middleware/StartupCheck.php b/app/Http/Middleware/StartupCheck.php index 14344e10e9..be96ade97e 100644 --- a/app/Http/Middleware/StartupCheck.php +++ b/app/Http/Middleware/StartupCheck.php @@ -141,9 +141,10 @@ class StartupCheck } } elseif ($productId == PRODUCT_WHITE_LABEL) { if ($data == 'valid') { - $account = Auth::user()->account; - $account->pro_plan_paid = date_create()->format('Y-m-d'); - $account->save(); + $company = Auth::user()->account->company; + $company->plan_paid = date_create()->format('Y-m-d'); + $company->plan = PLAN_WHITE_LABEL; + $company->save(); Session::flash('message', trans('texts.bought_white_label')); } diff --git a/app/Http/routes.php b/app/Http/routes.php index 8066690990..48f06e5e7b 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -188,7 +188,8 @@ Route::group([ Route::resource('users', 'UserController'); Route::post('users/bulk', 'UserController@bulk'); Route::get('send_confirmation/{user_id}', 'UserController@sendConfirmation'); - Route::get('start_trial', 'AccountController@startTrial'); + Route::get('start_trial/{plan}', 'AccountController@startTrial') + ->where(['plan'=>'pro']); Route::get('restore_user/{user_id}', 'UserController@restoreUser'); Route::post('users/change_password', 'UserController@changePassword'); Route::get('/switch_account/{user_id}', 'UserController@switchAccount'); @@ -212,6 +213,7 @@ Route::group([ Route::get('settings/charts_and_reports', 'ReportController@showReports'); Route::post('settings/charts_and_reports', 'ReportController@showReports'); + Route::post('settings/change_plan', 'AccountController@changePlan'); Route::post('settings/cancel_account', 'AccountController@cancelAccount'); Route::post('settings/company_details', 'AccountController@updateDetails'); Route::get('settings/{section?}', 'AccountController@showSection'); @@ -354,6 +356,7 @@ if (!defined('CONTACT_EMAIL')) { define('ACCOUNT_LOCALIZATION', 'localization'); define('ACCOUNT_NOTIFICATIONS', 'notifications'); define('ACCOUNT_IMPORT_EXPORT', 'import_export'); + define('ACCOUNT_MANAGEMENT', 'account_management'); define('ACCOUNT_PAYMENTS', 'online_payments'); define('ACCOUNT_BANKS', 'bank_accounts'); define('ACCOUNT_IMPORT_EXPENSES', 'import_expenses'); @@ -552,7 +555,6 @@ if (!defined('CONTACT_EMAIL')) { define('NINJA_WEB_URL', 'https://www.invoiceninja.com'); define('NINJA_APP_URL', 'https://app.invoiceninja.com'); define('NINJA_VERSION', '2.5.1.3'); - define('NINJA_DATE', '2000-01-01'); define('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'); define('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja'); @@ -582,6 +584,10 @@ if (!defined('CONTACT_EMAIL')) { define('SELF_HOST_AFFILIATE_KEY', '8S69AD'); define('PRO_PLAN_PRICE', 50); + define('PLAN_PRICE_PRO_MONTHLY', 5); + define('PLAN_PRICE_PRO_YEARLY', 50); + define('PLAN_PRICE_ENTERPRISE_MONTHLY', 10); + define('PLAN_PRICE_ENTERPRISE_YEARLY', 100); define('WHITE_LABEL_PRICE', 20); define('INVOICE_DESIGNS_PRICE', 10); @@ -644,7 +650,46 @@ if (!defined('CONTACT_EMAIL')) { define('RESELLER_REVENUE_SHARE', 'A'); define('RESELLER_LIMITED_USERS', 'B'); + + // These must be lowercase + define('PLAN_FREE', 'free'); + define('PLAN_PRO', 'pro'); + define('PLAN_ENTERPRISE', 'enterprise'); + define('PLAN_WHITE_LABEL', 'white_label'); + define('PLAN_TERM_MONTHLY', 'month'); + define('PLAN_TERM_YEARLY', 'year'); + + // Pro + define('FEATURE_CUSTOMIZE_INVOICE_DESIGN', 'customize_invoice_design'); + define('FEATURE_REMOVE_CREATED_BY', 'remove_created_by'); + define('FEATURE_DIFFERENT_DESIGNS', 'different_designs'); + define('FEATURE_EMAIL_TEMPLATES_REMINDERS', 'email_templates_reminders'); + define('FEATURE_INVOICE_SETTINGS', 'invoice_settings'); + define('FEATURE_CUSTOM_EMAILS', 'custom_emails'); + define('FEATURE_PDF_ATTACHMENT', 'pdf_attachment'); + define('FEATURE_MORE_INVOICE_DESIGNS', 'more_invoice_designs'); + define('FEATURE_QUOTES', 'quotes'); + define('FEATURE_REPORTS', 'reports'); + define('FEATURE_API', 'api'); + define('FEATURE_CLIENT_PORTAL_PASSWORD', 'client_portal_password'); + define('FEATURE_CUSTOM_URL', 'custom_url'); + + define('FEATURE_MORE_CLIENTS', 'more_clients'); // No trial allowed + + // Whitelabel + define('FEATURE_CLIENT_PORTAL_CSS', 'client_portal_css'); + define('FEATURE_WHITE_LABEL', 'feature_white_label'); + // Enterprise + define('FEATURE_DOCUMENTS', 'documents'); + + // No Trial allowed + define('FEATURE_USERS', 'users');// Grandfathered for old Pro users + define('FEATURE_USER_PERMISSIONS', 'user_permissions'); + + // Pro users who started paying on or before this date will be able to manage users + define('PRO_USERS_GRANDFATHER_DEADLINE', '2016-05-15'); + $creditCards = [ 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 439247f337..e4ad99c6ce 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -118,6 +118,11 @@ class Utils return Auth::check() && Auth::user()->isPro(); } + public static function hasFeature($feature) + { + return Auth::check() && Auth::user()->hasFeature($feature); + } + public static function isAdmin() { return Auth::check() && Auth::user()->is_admin; @@ -440,7 +445,12 @@ class Utils return false; } - $dateTime = new DateTime($date); + if ($date instanceof DateTime) { + $dateTime = $date; + } else { + $dateTime = new DateTime($date); + } + $timestamp = $dateTime->getTimestamp(); $format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT); @@ -961,38 +971,6 @@ class Utils return $entity1; } - public static function withinPastYear($date) - { - if (!$date || $date == '0000-00-00') { - return false; - } - - $today = new DateTime('now'); - $datePaid = DateTime::createFromFormat('Y-m-d', $date); - $interval = $today->diff($datePaid); - - return $interval->y == 0; - } - - public static function getInterval($date) - { - if (!$date || $date == '0000-00-00') { - return false; - } - - $today = new DateTime('now'); - $datePaid = DateTime::createFromFormat('Y-m-d', $date); - - return $today->diff($datePaid); - } - - public static function withinPastTwoWeeks($date) - { - $interval = Utils::getInterval($date); - - return $interval && $interval->d <= 14; - } - public static function addHttp($url) { if (!preg_match("~^(?:f|ht)tps?://~i", $url)) { diff --git a/app/Listeners/InvoiceListener.php b/app/Listeners/InvoiceListener.php index 9e40bbbe6d..91f3de8cc2 100644 --- a/app/Listeners/InvoiceListener.php +++ b/app/Listeners/InvoiceListener.php @@ -14,10 +14,11 @@ class InvoiceListener { public function createdInvoice(InvoiceWasCreated $event) { - if (Utils::isPro()) { + if (Utils::hasFeature(FEATURE_DIFFERENT_DESIGNS)) { return; } + // Make sure the account has the same design set as the invoice does if (Auth::check()) { $invoice = $event->invoice; $account = Auth::user()->account; diff --git a/app/Models/Account.php b/app/Models/Account.php index f50ed2cec1..e2e7d2f8a0 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -18,6 +18,17 @@ class Account extends Eloquent { use PresentableTrait; use SoftDeletes; + + public static $plan_prices = array( + PLAN_PRO => array( + PLAN_TERM_MONTHLY => PLAN_PRICE_PRO_MONTHLY, + PLAN_TERM_YEARLY => PLAN_PRICE_PRO_YEARLY, + ), + PLAN_ENTERPRISE => array( + PLAN_TERM_MONTHLY => PLAN_PRICE_ENTERPRISE_MONTHLY, + PLAN_TERM_YEARLY => PLAN_PRICE_ENTERPRISE_YEARLY, + ), + ); protected $presenter = 'App\Ninja\Presenters\AccountPresenter'; protected $dates = ['deleted_at']; @@ -57,6 +68,7 @@ class Account extends Eloquent ACCOUNT_PRODUCTS, ACCOUNT_NOTIFICATIONS, ACCOUNT_IMPORT_EXPORT, + ACCOUNT_MANAGEMENT, ]; public static $advancedSettings = [ @@ -176,6 +188,11 @@ class Account extends Eloquent return $this->hasMany('App\Models\Payment','account_id','id')->withTrashed(); } + public function company() + { + return $this->belongsTo('App\Models\Company'); + } + public function setIndustryIdAttribute($value) { $this->attributes['industry_id'] = $value ?: null; @@ -516,7 +533,7 @@ class Account extends Eloquent public function getNumberPrefix($isQuote) { - if ( ! $this->isPro()) { + if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) { return ''; } @@ -525,7 +542,7 @@ class Account extends Eloquent public function hasNumberPattern($isQuote) { - if ( ! $this->isPro()) { + if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) { return false; } @@ -551,7 +568,7 @@ class Account extends Eloquent $replace = [date('Y')]; $search[] = '{$counter}'; - $replace[] = str_pad($this->getCounter($invoice->is_quote), 4, '0', STR_PAD_LEFT); + $replace[] = str_pad($this->getCounter($invoice->is_quote), $this->invoice_number_padding, '0', STR_PAD_LEFT); if (strstr($pattern, '{$userId}')) { $search[] = '{$userId}'; @@ -617,7 +634,7 @@ class Account extends Eloquent // confirm the invoice number isn't already taken do { - $number = $prefix . str_pad($counter, 4, '0', STR_PAD_LEFT); + $number = $prefix . str_pad($counter, $this->invoice_number_padding, '0', STR_PAD_LEFT); $check = Invoice::scope(false, $this->id)->whereInvoiceNumber($number)->withTrashed()->first(); $counter++; $counterOffset++; @@ -645,7 +662,7 @@ class Account extends Eloquent $default = $this->invoice_number_counter; $actual = Utils::parseInt($invoice->invoice_number); - if ( ! $this->isPro() && $default != $actual) { + if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS) && $default != $actual) { $this->invoice_number_counter = $actual + 1; } else { $this->invoice_number_counter += 1; @@ -766,17 +783,80 @@ class Account extends Eloquent return $this->account_key === NINJA_ACCOUNT_KEY; } - public function startTrial() + public function startTrial($plan) { if ( ! Utils::isNinja()) { return; } - $this->pro_plan_trial = date_create()->format('Y-m-d'); - $this->save(); + $this->company->trial_plan = $plan; + $this->company->trial_started = date_create()->format('Y-m-d'); + $this->company->save(); } - public function isPro() + public function hasFeature($feature) + { + $planDetails = $this->getPlanDetails(); + $selfHost = !Utils::isNinjaProd(); + + if (!$selfHost && function_exists('ninja_account_features')) { + $result = ninja_account_features($this, $feature); + + if ($result != null) { + return $result; + } + } + + switch ($feature) { + // Pro + case FEATURE_CUSTOMIZE_INVOICE_DESIGN: + case FEATURE_REMOVE_CREATED_BY: + case FEATURE_DIFFERENT_DESIGNS: + case FEATURE_EMAIL_TEMPLATES_REMINDERS: + case FEATURE_INVOICE_SETTINGS: + case FEATURE_CUSTOM_EMAILS: + case FEATURE_PDF_ATTACHMENT: + case FEATURE_MORE_INVOICE_DESIGNS: + case FEATURE_QUOTES: + case FEATURE_REPORTS: + case FEATURE_API: + case FEATURE_CLIENT_PORTAL_PASSWORD: + case FEATURE_CUSTOM_URL: + return $selfHost || !empty($planDetails); + + // 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))); + + // White Label + case FEATURE_WHITE_LABEL: + if ($this->isNinjaAccount() || (!$selfHost && $planDetails && !$planDetails['expires'])) { + return false; + } + // Fallthrough + case FEATURE_CLIENT_PORTAL_CSS: + return !empty($planDetails);// A plan is required even for self-hosted users + + // Enterprise; No Trial allowed; grandfathered for old pro users + case FEATURE_USERS:// Grandfathered for old Pro users + if($planDetails && $planDetails['trial']) { + // Do they have a non-trial plan? + $planDetails = $this->getPlanDetails(false, false); + } + + return $selfHost || !empty($planDetails) && ($planDetails['plan'] == PLAN_ENTERPRISE || $planDetails['started'] <= date_create(PRO_USERS_GRANDFATHER_DEADLINE)); + + // Enterprise; No Trial allowed + case FEATURE_DOCUMENTS: + case FEATURE_USER_PERMISSIONS: + return $selfHost || !empty($planDetails) && $planDetails['plan'] == PLAN_ENTERPRISE && !$planDetails['trial']; + + default: + return false; + } + } + + public function isPro(&$plan_details = null) { if (!Utils::isNinjaProd()) { return true; @@ -786,14 +866,113 @@ class Account extends Eloquent return true; } - $datePaid = $this->pro_plan_paid; - $trialStart = $this->pro_plan_trial; + $plan_details = $this->getPlanDetails(); + + return !empty($plan_details); + } - if ($datePaid == NINJA_DATE) { + public function isEnterprise(&$plan_details = null) + { + if (!Utils::isNinjaProd()) { return true; } - return Utils::withinPastTwoWeeks($trialStart) || Utils::withinPastYear($datePaid); + if ($this->isNinjaAccount()) { + return true; + } + + $plan_details = $this->getPlanDetails(); + + return $plan_details && $plan_details['plan'] == PLAN_ENTERPRISE; + } + + public function getPlanDetails($include_inactive = false, $include_trial = true) + { + if (!$this->company) { + return null; + } + + $plan = $this->company->plan; + $trial_plan = $this->company->trial_plan; + + if(!$plan && (!$trial_plan || !$include_trial)) { + return null; + } + + $trial_active = false; + if ($trial_plan && $include_trial) { + $trial_started = DateTime::createFromFormat('Y-m-d', $this->company->trial_started); + $trial_expires = clone $trial_started; + $trial_expires->modify('+2 weeks'); + + if ($trial_expires >= date_create()) { + $trial_active = true; + } + } + + $plan_active = false; + if ($plan) { + if ($this->company->plan_expires == null) { + $plan_active = true; + $plan_expires = false; + } else { + $plan_expires = DateTime::createFromFormat('Y-m-d', $this->company->plan_expires); + if ($plan_expires >= date_create()) { + $plan_active = true; + } + } + } + + if (!$include_inactive && !$plan_active && !$trial_active) { + return null; + } + + // Should we show plan details or trial details? + if (($plan && !$trial_plan) || !$include_trial) { + $use_plan = true; + } elseif (!$plan && $trial_plan) { + $use_plan = false; + } else { + // There is both a plan and a trial + if (!empty($plan_active) && empty($trial_active)) { + $use_plan = true; + } elseif (empty($plan_active) && !empty($trial_active)) { + $use_plan = false; + } elseif (!empty($plan_active) && !empty($trial_active)) { + // Both are active; use whichever is a better plan + if ($plan == PLAN_ENTERPRISE) { + $use_plan = true; + } elseif ($trial_plan == PLAN_ENTERPRISE) { + $use_plan = false; + } else { + // They're both the same; show the plan + $use_plan = true; + } + } else { + // Neither are active; use whichever expired most recently + $use_plan = $plan_expires >= $trial_expires; + } + } + + if ($use_plan) { + return array( + 'trial' => false, + 'plan' => $plan, + 'started' => DateTime::createFromFormat('Y-m-d', $this->company->plan_started), + 'expires' => $plan_expires, + 'paid' => DateTime::createFromFormat('Y-m-d', $this->company->plan_paid), + 'term' => $this->company->plan_term, + 'active' => $plan_active, + ); + } else { + return array( + 'trial' => true, + 'plan' => $trial_plan, + 'started' => $trial_started, + 'expires' => $trial_expires, + 'active' => $trial_active, + ); + } } public function isTrial() @@ -801,35 +980,54 @@ class Account extends Eloquent if (!Utils::isNinjaProd()) { return false; } + + $plan_details = $this->getPlanDetails(); - if ($this->pro_plan_paid && $this->pro_plan_paid != '0000-00-00') { - return false; - } - - return Utils::withinPastTwoWeeks($this->pro_plan_trial); + return $plan_details && $plan_details['trial']; } - public function isEligibleForTrial() + public function isEligibleForTrial($plan = null) { - return ! $this->pro_plan_trial || $this->pro_plan_trial == '0000-00-00'; + if (!$this->company->trial_plan) { + if ($plan) { + return $plan == PLAN_PRO || $plan == PLAN_ENTERPRISE; + } else { + return array(PLAN_PRO, PLAN_ENTERPRISE); + } + } + + if ($this->company->trial_plan == PLAN_PRO) { + if ($plan) { + return $plan != PLAN_PRO; + } else { + return array(PLAN_ENTERPRISE); + } + } + + return false; } public function getCountTrialDaysLeft() { - $interval = Utils::getInterval($this->pro_plan_trial); + $planDetails = $this->getPlanDetails(true); - return $interval ? 14 - $interval->d : 0; + if(!$planDetails || !$planDetails['trial']) { + return 0; + } + + $today = new DateTime('now'); + $interval = $today->diff($planDetails['expires']); + + return $interval ? $interval->d : 0; } public function getRenewalDate() { - if ($this->pro_plan_paid && $this->pro_plan_paid != '0000-00-00') { - $date = DateTime::createFromFormat('Y-m-d', $this->pro_plan_paid); - $date->modify('+1 year'); + $planDetails = $this->getPlanDetails(); + + if ($planDetails) { + $date = $planDetails['expires']; $date = max($date, date_create()); - } elseif ($this->isTrial()) { - $date = date_create(); - $date->modify('+'.$this->getCountTrialDaysLeft().' day'); } else { $date = date_create(); } @@ -837,23 +1035,6 @@ class Account extends Eloquent return $date->format('Y-m-d'); } - public function isWhiteLabel() - { - if ($this->isNinjaAccount()) { - return false; - } - - if (Utils::isNinjaProd()) { - return self::isPro() && $this->pro_plan_paid != NINJA_DATE; - } else { - if ($this->pro_plan_paid == NINJA_DATE) { - return true; - } - - return Utils::withinPastYear($this->pro_plan_paid); - } - } - public function getLogoSize() { if(!$this->hasLogo()){ @@ -930,7 +1111,7 @@ class Account extends Eloquent public function getEmailSubject($entityType) { - if ($this->isPro()) { + if ($this->hasFeature(FEATURE_CUSTOM_EMAILS)) { $field = "email_subject_{$entityType}"; $value = $this->$field; @@ -950,7 +1131,7 @@ class Account extends Eloquent $template = "
'.trans('texts.password').': '.$data['password'].'
':false; $documentsHTML = ''; - if($account->isPro() && $invoice->hasDocuments()){ + if($account->hasFeature(FEATURE_DOCUMENTS) && $invoice->hasDocuments()){ $documentsHTML .= trans('texts.email_documents_header').'
=200&&o.status<400)return t(o.responseText);n(new Error("Unable to retrieve "+e))}},o.send()}function o(e,t,r){for(var n,o,i,a=/function\s+([^(]*?)\s*\(([^)]*)\)/,s=/['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*function\b/,u=/['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*(?:eval|new Function)\b/,c=e.split("\n"),l="",f=Math.min(t,20),p=0;f>p;++p)if(n=c[t-p-1],i=n.indexOf("//"),i>=0&&(n=n.substr(0,i)),n){if(l=n+l,o=s.exec(l),o&&o[1])return o[1];if(o=a.exec(l),o&&o[1])return o[1];if(o=u.exec(l),o&&o[1])return o[1]}return void 0}function i(){if("function"!=typeof Object.defineProperty||"function"!=typeof Object.create)throw new Error("Unable to consume source maps in older browsers")}function a(e){if("object"!=typeof e)throw new TypeError("Given StackFrame is not an object");if("string"!=typeof e.fileName)throw new TypeError("Given file name is not a String");if("number"!=typeof e.lineNumber||e.lineNumber%1!==0||e.lineNumber<1)throw new TypeError("Given line number must be a positive integer");if("number"!=typeof e.columnNumber||e.columnNumber%1!==0||e.columnNumber<0)throw new TypeError("Given column number must be a non-negative integer");return!0}function s(e){var t=/\/\/[#@] ?sourceMappingURL=([^\s'"]+)$/.exec(e);if(t&&t[1])return t[1];throw new Error("sourceMappingURL not found")}function u(r,n,o,i){var a=new e.SourceMapConsumer(r).originalPositionFor({line:o,column:i});return new t(a.name,n,a.source,a.line,a.column)}return function c(e){return this instanceof c?(e=e||{},this.sourceCache=e.sourceCache||{},this.ajax=n,this._atob=function(e){if(window&&window.atob)return window.atob(e);if("undefined"!=typeof Buffer)return new Buffer(e,"base64").toString("utf-8");throw new Error("No base64 decoder available")},this._get=function(t){return new Promise(function(r,n){var o="data:"===t.substr(0,5);if(this.sourceCache[t])r(this.sourceCache[t]);else if(e.offline&&!o)n(new Error("Cannot make network requests in offline mode"));else if(o){var i="application/json;base64";if(t.substr(5,i.length)!==i)n(new Error("The encoding of the inline sourcemap is not supported"));else{var a="data:".length+i.length+",".length,s=t.substr(a),u=this._atob(s);this.sourceCache[t]=u,r(u)}}else this.ajax(t,function(e){this.sourceCache[t]=e,r(e)}.bind(this),n)}.bind(this))},this.pinpoint=function(e){return new Promise(function(t,r){this.getMappedLocation(e).then(function(e){function r(){t(e)}this.findFunctionName(e).then(t,r)["catch"](r)}.bind(this),r)}.bind(this))},this.findFunctionName=function(e){return new Promise(function(r,n){a(e),this._get(e.fileName).then(function(n){var i=o(n,e.lineNumber,e.columnNumber);r(new t(i,e.args,e.fileName,e.lineNumber,e.columnNumber))},n)}.bind(this))},void(this.getMappedLocation=function(e){return new Promise(function(t,r){i(),a(e);var n=e.fileName;this._get(n).then(function(o){var i=s(o);"/"!==i[0]&&(i=n.substring(0,n.lastIndexOf("/")+1)+i),this._get(i).then(function(r){var n=e.lineNumber,o=e.columnNumber;t(u(r,e.args,n,o))},r)["catch"](r)}.bind(this),r)["catch"](r)}.bind(this))})):new c(e)}}),function(e,t){"use strict";"function"==typeof define&&define.amd?define("stack-generator",["stackframe"],t):"object"==typeof exports?module.exports=t(require("stackframe")):e.StackGenerator=t(e.StackFrame)}(this,function(e){return{backtrace:function(t){var r=[],n=10;"object"==typeof t&&"number"==typeof t.maxStackSize&&(n=t.maxStackSize);for(var o=arguments.callee;o&&r.length Automatically sets a due date for the invoice. Invoices on a monthly or yearly cycle set to be due on or before the day they are created will be due the next month. Invoices set to be due on the 29th or 30th in months that don\'t have that day will be due the last day of the month. Invoices on a weekly cycle set to be due on the day of the week they are created will be due the next week. For example: Definir automaticamente a data de vencimento da fatura. Faturas em um ciclo mensal ou anual com vencimento anterior ou na data em que são criadas serão faturadas para o próximo mês. Faturas com vencimento no dia 29 ou 30 nos meses que não tem esse dia será faturada no último dia do mês.. Faturas em um clclo mensal com vencimento no dia da semana em que foi criada serão faturadas para a próxima semana. Exemplo:
Next StepsA payable invoice has been sent to the email
address associated with your account. To unlock all of the awesome
@@ -368,7 +367,7 @@ $LANG = array(
'confirm_email_invoice' => 'Are you sure you want to email this invoice?',
'confirm_email_quote' => 'Are you sure you want to email this quote?',
'confirm_recurring_email_invoice' => 'Are you sure you want this invoice emailed?',
- 'cancel_account' => 'Cancel Account',
+ 'cancel_account' => 'Delete Account',
'cancel_account_message' => 'Warning: This will permanently erase all of your data, there is no undo.',
'go_back' => 'Go Back',
'data_visualizations' => 'Data Visualizations',
@@ -976,9 +975,9 @@ $LANG = array(
'expense_error_mismatch_currencies' => 'The client\'s currency does not match the expense currency.',
'trello_roadmap' => 'Trello Roadmap',
'header_footer' => 'Header/Footer',
- 'first_page' => 'first page',
- 'all_pages' => 'all pages',
- 'last_page' => 'last page',
+ 'first_page' => 'First page',
+ 'all_pages' => 'All pages',
+ 'last_page' => 'Last page',
'all_pages_header' => 'Show header on',
'all_pages_footer' => 'Show footer on',
'invoice_currency' => 'Invoice Currency',
@@ -1126,6 +1125,57 @@ $LANG = array(
'enable_client_portal_help' => 'Show/hide the client portal.',
'enable_client_portal_dashboard' => 'Dashboard',
'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.',
+
+ // Plans
+ 'account_management' => 'Account Management',
+ 'plan_status' => 'Plan Status',
+
+ 'plan_upgrade' => 'Upgrade',
+ 'plan_change' => 'Change Plan',
+ 'pending_change_to' => 'Changes To',
+ 'plan_changes_to' => ':plan on :date',
+ 'plan_term_changes_to' => ':plan (:term) on :date',
+ 'cancel_plan_change' => 'Cancel Change',
+ 'plan' => 'Plan',
+ 'expires' => 'Expires',
+ 'renews' => 'Renews',
+ 'plan_expired' => ':plan Plan Expired',
+ 'trial_expired' => ':plan Plan Trial Ended',
+ 'never' => 'Never',
+ 'plan_free' => 'Free',
+ 'plan_pro' => 'Pro',
+ 'plan_enterprise' => 'Enterprise',
+ 'plan_white_label' => 'Self Hosted (White labeled)',
+ 'plan_free_self_hosted' => 'Self Hosted (Free)',
+ 'plan_trial' => 'Trial',
+ 'plan_term' => 'Term',
+ 'plan_term_monthly' => 'Monthly',
+ 'plan_term_yearly' => 'Yearly',
+ 'plan_term_month' => 'Month',
+ 'plan_term_year' => 'Year',
+ 'plan_price_monthly' => '$:price/Month',
+ 'plan_price_yearly' => '$:price/Year',
+ 'updated_plan' => 'Updated plan settings',
+ 'plan_paid' => 'Term Started',
+ 'plan_started' => 'Plan Started',
+ 'plan_expires' => 'Plan Expires',
+
+ 'white_label_button' => 'White Label',
+
+ 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.',
+ 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.',
+ 'enterprise_plan_product' => 'Enterprise Plan',
+ 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.',
+ 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.',
+ 'plan_credit_product' => 'Credit',
+ 'plan_credit_description' => 'Credit for unused time',
+ 'plan_pending_monthly' => 'Will switch to monthly on :date',
+ 'plan_refunded' => 'A refund has been issued.',
+
+ 'live_preview' => 'Live Preview',
+ 'page_size' => 'Page Size',
+ 'live_preview_disabled' => 'Live preview has been disabled to support selected font',
+ 'invoice_number_padding' => 'Padding',
);
diff --git a/resources/lang/pt_BR/texts.php b/resources/lang/pt_BR/texts.php
index 30fcfbf05c..3b180e93d6 100644
--- a/resources/lang/pt_BR/texts.php
+++ b/resources/lang/pt_BR/texts.php
@@ -929,267 +929,264 @@ return array(
'client_portal' => 'Portal do Cliente',
'admin' => 'Admin',
- 'disabled' => 'Disabilitado',
+ 'disabled' => 'Desabilitado',
'show_archived_users' => 'Mostrar usuários arquivados',
'notes' => 'Observações',
'invoice_will_create' => 'cliente será criado',
'invoices_will_create' => 'faturas serão criadas',
'failed_to_import' => 'A importação dos seguintes registros falhou',
- 'publishable_key' => 'Publishable Key',
- 'secret_key' => 'Secret Key',
- 'missing_publishable_key' => 'Set your Stripe publishable key for an improved checkout process',
+ 'publishable_key' => 'Chave Publicável',
+ 'secret_key' => 'Chave Secreta',
+ 'missing_publishable_key' => 'Defina o sua chave publicável do Stripe para um processo de pagamento melhorado',
- 'email_design' => 'Email Design',
- 'due_by' => 'Due by :date',
- 'enable_email_markup' => 'Enable Markup',
- 'enable_email_markup_help' => 'Make it easier for your clients to pay you by adding schema.org markup to your emails.',
- 'template_help_title' => 'Templates Help',
- 'template_help_1' => 'Available variables:',
- 'email_design_id' => 'Email Style',
- 'email_design_help' => 'Make your emails look more professional with HTML layouts',
- 'plain' => 'Plain',
- 'light' => 'Light',
- 'dark' => 'Dark',
+ 'email_design' => 'Template de E-mail',
+ 'due_by' => 'Vencido em :date',
+ 'enable_email_markup' => 'Habilitar Marcação',
+ 'enable_email_markup_help' => 'Tornar mais fácil para os seus clientes efetuarem seus pagamentos, acrescentando marcação schema.org a seus e-mails.',
+ 'template_help_title' => 'Ajuda de Templates',
+ 'template_help_1' => 'Variáveis disponíveis:',
+ 'email_design_id' => 'Estilo de e-mails',
+ 'email_design_help' => 'Deixe seus e-mails mais profissionais com layouts HTML',
+ 'plain' => 'Plano',
+ 'light' => 'Claro',
+ 'dark' => 'Escuro',
- 'industry_help' => 'Used to provide comparisons against the averages of companies of similar size and industry.',
- 'subdomain_help' => 'Customize the invoice link subdomain or display the invoice on your own website.',
- 'invoice_number_help' => 'Specify a prefix or use a custom pattern to dynamically set the invoice number.',
- 'quote_number_help' => 'Specify a prefix or use a custom pattern to dynamically set the quote number.',
- 'custom_client_fields_helps' => 'Add a text input to the client create/edit page and display the label and value on the PDF.',
- 'custom_account_fields_helps' => 'Add a label and value to the company details section of the PDF.',
- 'custom_invoice_fields_helps' => 'Add a text input to the invoice create/edit page and display the label and value on the PDF.',
- 'custom_invoice_charges_helps' => 'Add a text input to the invoice create/edit page and include the charge in the invoice subtotals.',
- 'color_help' => 'Note: the primary color is also used in the client portal and custom email designs.',
+ 'industry_help' => 'Usado para fornecer comparações contra as médias das empresas de tamanho e indústria similar.',
+ 'subdomain_help' => 'Personalizar o link da fatura ou exibir a fatura em seu próprio site.',
+ 'invoice_number_help' => 'Especifique um prefixo ou usar um padrão personalizado para definir dinamicamente o número da fatura.',
+ 'quote_number_help' => 'Especifique um prefixo ou usar um padrão personalizado para definir dinamicamente o número do orçamento.',
+ 'custom_client_fields_helps' => 'Adicionar uma entrada de texto na página Criar/Editar Cliente e exibir no PDF.',
+ 'custom_account_fields_helps' => 'Adicionar um rótulo e um valor para a seção detalhes da empresa do PDF.',
+ 'custom_invoice_fields_helps' => 'Adicionar uma entrada de texto na página Criar/Editar Fatura e exibir no PDF.',
+ 'custom_invoice_charges_helps' => 'Adicionar uma entrada de texto na página Criar/Editar Fatura e incluir nos subtotais da fatura.',
+ 'color_help' => 'Nota: A cor primária também é utilizada nos projetos do portal do cliente e-mail personalizado.',
- 'token_expired' => 'Validation token was expired. Please try again.',
- 'invoice_link' => 'Invoice Link',
- 'button_confirmation_message' => 'Click to confirm your email address.',
- 'confirm' => 'Confirm',
- 'email_preferences' => 'Email Preferences',
- 'created_invoices' => 'Successfully created :count invoice(s)',
- 'next_invoice_number' => 'The next invoice number is :number.',
- 'next_quote_number' => 'The next quote number is :number.',
+ 'token_expired' => 'Token de acesso expirado. Tente novamente!',
+ 'invoice_link' => 'Link da Fatura',
+ 'button_confirmation_message' => 'Clique para confirmar seu endereço de e-mail.',
+ 'confirm' => 'Confirmar',
+ 'email_preferences' => 'Preferências de E-mails',
+ 'created_invoices' => ':count fatura(s) criadas com sucesso',
+ 'next_invoice_number' => 'O número da próxima fatura será :number.',
+ 'next_quote_number' => 'O número do próximo orçamento será :number.',
- 'days_before' => 'days before',
- 'days_after' => 'days after',
- 'field_due_date' => 'due date',
- 'field_invoice_date' => 'invoice date',
- 'schedule' => 'Schedule',
- 'email_designs' => 'Email Designs',
- 'assigned_when_sent' => 'Assigned when sent',
+ 'days_before' => 'dias antes',
+ 'days_after' => 'dias depois',
+ 'field_due_date' => 'data de vencimento',
+ 'field_invoice_date' => 'data da fatura',
+ 'schedule' => 'Agendamento',
+ 'email_designs' => 'Design de E-mails',
+ 'assigned_when_sent' => 'Assinar quando enviar',
- 'white_label_custom_css' => ':link for $'.WHITE_LABEL_PRICE.' to enable custom styling and help support our project.',
- 'white_label_purchase_link' => 'Purchase a white label license',
+ 'white_label_custom_css' => ':link apenas $'.WHITE_LABEL_PRICE.' para permitir um estilo personalizado e apoiar o nosso projecto.',
+ 'white_label_purchase_link' => 'Adquira uma licença white label',
// Expense / vendor
- 'expense' => 'Expense',
- 'expenses' => 'Expenses',
- 'new_expense' => 'Enter Expense',
- 'enter_expense' => 'Enter Expense',
- 'vendors' => 'Vendors',
- 'new_vendor' => 'New Vendor',
- 'payment_terms_net' => 'Net',
- 'vendor' => 'Vendor',
- 'edit_vendor' => 'Edit Vendor',
- 'archive_vendor' => 'Archive Vendor',
- 'delete_vendor' => 'Delete Vendor',
- 'view_vendor' => 'View Vendor',
- 'deleted_expense' => 'Successfully deleted expense',
- 'archived_expense' => 'Successfully archived expense',
- 'deleted_expenses' => 'Successfully deleted expenses',
- 'archived_expenses' => 'Successfully archived expenses',
+ 'expense' => 'Despesa',
+ 'expenses' => 'Despesas',
+ 'new_expense' => 'Adicionar Despesa',
+ 'enter_expense' => 'Incluir Despesa',
+ 'vendors' => 'Fornecedor',
+ 'new_vendor' => 'Novo Fornecedor',
+ 'payment_terms_net' => 'Rede',
+ 'vendor' => 'Fornecedor',
+ 'edit_vendor' => 'Editar Fornecedor',
+ 'archive_vendor' => 'Arquivar Fornecedor',
+ 'delete_vendor' => 'Deletar Fornecedor',
+ 'view_vendor' => 'Visualizar Fornecedor',
+ 'deleted_expense' => 'Despesa excluída com sucesso',
+ 'archived_expense' => 'Despesa arquivada com sucesso',
+ 'deleted_expenses' => 'Despesas excluídas com sucesso',
+ 'archived_expenses' => 'Despesas arquivada com sucesso',
// Expenses
- 'expense_amount' => 'Expense Amount',
- 'expense_balance' => 'Expense Balance',
- 'expense_date' => 'Expense Date',
- 'expense_should_be_invoiced' => 'Should this expense be invoiced?',
- 'public_notes' => 'Public Notes',
- 'invoice_amount' => 'Invoice Amount',
- 'exchange_rate' => 'Exchange Rate',
- 'yes' => 'Yes',
- 'no' => 'No',
- 'should_be_invoiced' => 'Should be invoiced',
- 'view_expense' => 'View expense # :expense',
- 'edit_expense' => 'Edit Expense',
- 'archive_expense' => 'Archive Expense',
- 'delete_expense' => 'Delete Expense',
- 'view_expense_num' => 'Expense # :expense',
- 'updated_expense' => 'Successfully updated expense',
- 'created_expense' => 'Successfully created expense',
- 'enter_expense' => 'Enter Expense',
- 'view' => 'View',
- 'restore_expense' => 'Restore Expense',
- 'invoice_expense' => 'Invoice Expense',
- 'expense_error_multiple_clients' => 'The expenses can\'t belong to different clients',
- 'expense_error_invoiced' => 'Expense has already been invoiced',
- 'convert_currency' => 'Convert currency',
+ 'expense_amount' => 'Total de Despesas',
+ 'expense_balance' => 'Saldo de Despesas',
+ 'expense_date' => 'Data da Despesa',
+ 'expense_should_be_invoiced' => 'Esta despesa deve ser faturada?',
+ 'public_notes' => 'Notas Públicas',
+ 'invoice_amount' => 'Total da Fatura',
+ 'exchange_rate' => 'Taxa de Câmbio',
+ 'yes' => 'Sim',
+ 'no' => 'Não',
+ 'should_be_invoiced' => 'Deve ser Faturada',
+ 'view_expense' => 'Visualizar despesa # :expense',
+ 'edit_expense' => 'Editar Despesa',
+ 'archive_expense' => 'Arquivar Despesa',
+ 'delete_expense' => 'Deletar Despesa',
+ 'view_expense_num' => 'Despesa # :expense',
+ 'updated_expense' => 'Despesa atualizada com sucesso',
+ 'created_expense' => 'Despesa criada com sucesso',
+ 'enter_expense' => 'Incluir Despesa',
+ 'view' => 'Visualizar',
+ 'restore_expense' => 'Restaurar Despesa',
+ 'invoice_expense' => 'Faturar Despesa',
+ 'expense_error_multiple_clients' => 'Despesas não podem pertencer a clientes diferentes',
+ 'expense_error_invoiced' => 'Despeja já faturada',
+ 'convert_currency' => 'Converter moeda',
// Payment terms
- 'num_days' => 'Number of days',
- 'create_payment_term' => 'Create Payment Term',
- 'edit_payment_terms' => 'Edit Payment Term',
- 'edit_payment_term' => 'Edit Payment Term',
- 'archive_payment_term' => 'Archive Payment Term',
+ 'num_days' => 'Número de dias',
+ 'create_payment_term' => 'Criar Termo de Pagamento',
+ 'edit_payment_terms' => 'Editar Termos de Pagamento',
+ 'edit_payment_term' => 'Editar Termo de Pagamento',
+ 'archive_payment_term' => 'Arquivar Termo de Pagamento',
// recurring due dates
- 'recurring_due_dates' => 'Recurring Invoice Due Dates',
- 'recurring_due_date_help' => '
-
',
- 'due' => 'Due',
- 'next_due_on' => 'Due Next: :date',
- 'use_client_terms' => 'Use client terms',
- 'day_of_month' => ':ordinal day of month',
- 'last_day_of_month' => 'Last day of month',
- 'day_of_week_after' => ':ordinal :day after',
- 'sunday' => 'Sunday',
- 'monday' => 'Monday',
- 'tuesday' => 'Tuesday',
- 'wednesday' => 'Wednesday',
- 'thursday' => 'Thursday',
- 'friday' => 'Friday',
- 'saturday' => 'Saturday',
+ 'due' => 'Vencimento',
+ 'next_due_on' => 'Próximo Vencimento: :date',
+ 'use_client_terms' => 'Usar condições do cliente',
+ 'day_of_month' => ':ordinal dia do mês ',
+ 'last_day_of_month' => 'Último dia do mês',
+ 'day_of_week_after' => ':ordinal :day depois',
+ 'sunday' => 'Domingo',
+ 'monday' => 'Segunda-Feira',
+ 'tuesday' => 'Terça-Feira',
+ 'wednesday' => 'Quarta-Feira',
+ 'thursday' => 'Quinta-Feira',
+ 'friday' => 'Sexta-Feira',
+ 'saturday' => 'Sábado',
// Fonts
- 'header_font_id' => 'Header Font',
- 'body_font_id' => 'Body Font',
- 'color_font_help' => 'Note: the primary color and fonts are also used in the client portal and custom email designs.',
+ 'header_font_id' => 'Fonte do Cabeçalho',
+ 'body_font_id' => 'Fonte dos Textos',
+ 'color_font_help' => 'Nota: A cor primária também é utilizada nos projetos do portal do cliente e-mail personalizado.',
- 'live_preview' => 'Live Preview',
- 'invalid_mail_config' => 'Unable to send email, please check that the mail settings are correct.',
+ 'live_preview' => 'Preview',
+ 'invalid_mail_config' => 'Falha ao enviar e-mail, verifique as configurações.',
- 'invoice_message_button' => 'To view your invoice for :amount, click the button below.',
- 'quote_message_button' => 'To view your quote for :amount, click the button below.',
- 'payment_message_button' => 'Thank you for your payment of :amount.',
- 'payment_type_direct_debit' => 'Direct Debit',
- 'bank_accounts' => 'Bank Accounts',
- 'add_bank_account' => 'Add Bank Account',
- 'setup_account' => 'Setup Account',
- 'import_expenses' => 'Import Expenses',
- 'bank_id' => 'bank',
- 'integration_type' => 'Integration Type',
- 'updated_bank_account' => 'Successfully updated bank account',
- 'edit_bank_account' => 'Edit Bank Account',
- 'archive_bank_account' => 'Archive Bank Account',
- 'archived_bank_account' => 'Successfully archived bank account',
- 'created_bank_account' => 'Successfully created bank account',
- 'validate_bank_account' => 'Validate Bank Account',
- 'bank_accounts_help' => 'Connect a bank account to automatically import expenses and create vendors. Supports American Express and 400+ US banks.',
- 'bank_password_help' => 'Note: your password is transmitted securely and never stored on our servers.',
- 'bank_password_warning' => 'Warning: your password may be transmitted in plain text, consider enabling HTTPS.',
- 'username' => 'Username',
- 'account_number' => 'Account Number',
- 'account_name' => 'Account Name',
- 'bank_account_error' => 'Failed to retreive account details, please check your credentials.',
- 'status_approved' => 'Approved',
- 'quote_settings' => 'Quote Settings',
- 'auto_convert_quote' => 'Auto convert quote',
- 'auto_convert_quote_help' => 'Automatically convert a quote to an invoice when approved by a client.',
- 'validate' => 'Validate',
+ 'invoice_message_button' => 'Para visualizar sua fatura de :amount, clique no botão abaixo.',
+ 'quote_message_button' => 'Para visualizar seu orçamento de :amount, clique no botão abaixo.',
+ 'payment_message_button' => 'Obrigado pelo seu pagamento de :amount.',
+ 'payment_type_direct_debit' => 'Débito',
+ 'bank_accounts' => 'Contas Bancárias',
+ 'add_bank_account' => 'Adicionar Conta Bancária',
+ 'setup_account' => 'Configurar Conta',
+ 'import_expenses' => 'Importar Despesas',
+ 'bank_id' => 'banco',
+ 'integration_type' => 'Tipo de Integração',
+ 'updated_bank_account' => 'Conta bancária atualizada com sucesso',
+ 'edit_bank_account' => 'Editar Conta Bancária',
+ 'archive_bank_account' => 'Arquivar Conta Bancária',
+ 'archived_bank_account' => 'Conta bancária arquivada com sucesso',
+ 'created_bank_account' => 'Conta bancária criada com sucesso',
+ 'validate_bank_account' => 'Validar Conta Bancária',
+ 'bank_accounts_help' => 'Conecte sua conta bancária para importar suas despesas e criar fornecedores. Suporte ao American Express e 400+ bancos americanos.',
+ 'bank_password_help' => 'Nota: sua senha é transferida de forma segura e não será armazenada em nossos servidores.',
+ 'bank_password_warning' => 'Atenção: sua senha será transferida de forma não segura, considere habilitar HTTPS.',
+ 'username' => 'Usuário',
+ 'account_number' => 'Conta',
+ 'account_name' => 'Nome da Conta',
+ 'bank_account_error' => 'Falha ao receber os detalhes da sua conta, verifique seus dados de acesso.',
+ 'status_approved' => 'Aprovado',
+ 'quote_settings' => 'Configuração de Orçamentos',
+ 'auto_convert_quote' => 'Auto converter orçamento',
+ 'auto_convert_quote_help' => 'Converter automaticamente um orçamento quando for aprovado pelo cliente.',
+ 'validate' => 'Validado',
'info' => 'Info',
- 'imported_expenses' => 'Successfully created :count_vendors vendor(s) and :count_expenses expense(s)',
+ 'imported_expenses' => ':count_vendors fornecedor(s) e :count_expenses despesa(s) importadas com sucesso',
- 'iframe_url_help3' => 'Note: if you plan on accepting credit cards details we strongly recommend enabling HTTPS on your site.',
- 'expense_error_multiple_currencies' => 'The expenses can\'t have different currencies.',
- 'expense_error_mismatch_currencies' => 'The client\'s currency does not match the expense currency.',
+ 'iframe_url_help3' => 'Nota: se o seu plano aceita detalhes do cartão de crédito recomendamos que seja habilitado o HTTPS em seu site.',
+ 'expense_error_multiple_currencies' => 'As despesas não podem ter diferentes moedas.',
+ 'expense_error_mismatch_currencies' => 'As configurações de moeda do cliente não coincide com a moeda nesta despesa.',
'trello_roadmap' => 'Trello Roadmap',
- 'header_footer' => 'Header/Footer',
- 'first_page' => 'first page',
- 'all_pages' => 'all pages',
- 'last_page' => 'last page',
- 'all_pages_header' => 'Show header on',
- 'all_pages_footer' => 'Show footer on',
- 'invoice_currency' => 'Invoice Currency',
- 'enable_https' => 'We strongly recommend using HTTPS to accept credit card details online.',
- 'quote_issued_to' => 'Quote issued to',
- 'show_currency_code' => 'Currency Code',
- 'trial_message' => 'Your account will receive a free two week trial of our pro plan.',
- 'trial_footer' => 'Your free trial lasts :count more days, :link to upgrade now.',
- 'trial_footer_last_day' => 'This is the last day of your free trial, :link to upgrade now.',
- 'trial_call_to_action' => 'Start Free Trial',
- 'trial_success' => 'Successfully enabled two week free pro plan trial',
- 'overdue' => 'Overdue',
- 'white_label_text' => 'Purchase a ONE YEAR white label license for $'.WHITE_LABEL_PRICE.' to remove the Invoice Ninja branding from the client portal and help support our project.',
+ 'header_footer' => 'Cabeçalho/Rodapé',
+ 'first_page' => 'primeira página',
+ 'all_pages' => 'todas as páginas',
+ 'last_page' => 'última página',
+ 'all_pages_header' => 'Mostrar cabeçalho on',
+ 'all_pages_footer' => 'Mostrar rodapé on',
+ 'invoice_currency' => 'Moeda da Fatura',
+ 'enable_https' => 'Recomendamos a utilização de HTTPS para receber os detalhes do cartão de crédito online.',
+ 'quote_issued_to' => 'Orçamento emitido para',
+ 'show_currency_code' => 'Código da Moeda',
+ 'trial_message' => 'Sua conta receberá duas semanas receberá duas semanas gratuitamente para testar nosso plano pro.',
+ 'trial_footer' => 'Seu período de teste expira em :count dias, :link para adquirir o plano pro.',
+ 'trial_footer_last_day' => 'Seu período de testes encerra hoje, :link para adiquirir o plano pro.',
+ 'trial_call_to_action' => 'Iniciar período de testes',
+ 'trial_success' => 'Duas semanas de testes foi habilitado com sucesso',
+ 'overdue' => 'Vencido',
+ 'white_label_text' => 'Adquira UM ano de licença white label por $'.WHITE_LABEL_PRICE.' para remover a marca Invoice Ninja do portal do cliente e ajudar nosso projeto.',
- 'navigation' => 'Navigation',
- 'list_invoices' => 'List Invoices',
- 'list_clients' => 'List Clients',
- 'list_quotes' => 'List Quotes',
- 'list_tasks' => 'List Tasks',
- 'list_expenses' => 'List Expenses',
- 'list_recurring_invoices' => 'List Recurring Invoices',
- 'list_payments' => 'List Payments',
- 'list_credits' => 'List Credits',
- 'tax_name' => 'Tax Name',
- 'report_settings' => 'Report Settings',
- 'search_hotkey' => 'shortcut is /',
+ 'navigation' => 'Navegação',
+ 'list_invoices' => 'Listar Faturas',
+ 'list_clients' => 'Listar Clientes',
+ 'list_quotes' => 'Listar Orçamentos',
+ 'list_tasks' => 'Listar Tarefas',
+ 'list_expenses' => 'Listar Despesas',
+ 'list_recurring_invoices' => 'Listar Faturas Recorrentes',
+ 'list_payments' => 'Listar Pagamentos',
+ 'list_credits' => 'Listar Créditos',
+ 'tax_name' => 'Nome da Taxa',
+ 'report_settings' => 'Configuração de Relatórios',
+ 'search_hotkey' => 'atalho /',
- 'new_user' => 'New User',
- 'new_product' => 'New Product',
- 'new_tax_rate' => 'New Tax Rate',
- 'invoiced_amount' => 'Invoiced Amount',
- 'invoice_item_fields' => 'Invoice Item Fields',
- 'custom_invoice_item_fields_help' => 'Add a field when creating an invoice item and display the label and value on the PDF.',
- 'recurring_invoice_number' => 'Recurring Invoice Number',
- 'recurring_invoice_number_prefix_help' => 'Speciy a prefix to be added to the invoice number for recurring invoices. The default value is \'R\'.',
- 'enable_client_portal' => 'Dashboard',
- 'enable_client_portal_help' => 'Show/hide the dashboard page in the client portal.',
+ 'new_user' => 'Novo Usuário',
+ 'new_product' => 'Novo Produto',
+ 'new_tax_rate' => 'Nova Taxa de Juro',
+ 'invoiced_amount' => 'Total Faturado',
+ 'invoice_item_fields' => 'Campos de Ítens da Fatura',
+ 'custom_invoice_item_fields_help' => 'Adicionar um campo ao adicionar um ítem na fatura e exibir no PDF.',
+ 'recurring_invoice_number' => 'Número da Fatura Recorrente',
+ 'recurring_invoice_number_prefix_help' => 'Informe um prefixo para a numeração das faturas recorrentes. O valor padrão é \'R\'.',
+ 'enable_client_portal' => 'Painel',
+ 'enable_client_portal_help' => 'Mostrar/Ocultar o painel no portal do cliente.',
// Client Passwords
- 'enable_portal_password'=>'Password protect invoices',
- 'enable_portal_password_help'=>'Allows you to set a password for each contact. If a password is set, the contact will be required to enter a password before viewing invoices.',
- 'send_portal_password'=>'Generate password automatically',
- 'send_portal_password_help'=>'If no password is set, one will be generated and sent with the first invoice.',
+ 'enable_portal_password'=>'Faturas protegidas por senha',
+ 'enable_portal_password_help'=>'Permite definir uma senha para cada contato. Se uma senha for definida, o contato deverá informar sua senha antes de visualizar a fatura.',
+ 'send_portal_password'=>'Gerar senha automaticamente',
+ 'send_portal_password_help'=>'Se uma senha não for definida, uma senha será gerada e enviada juntamente com a primeira fatura.',
- 'expired' => 'Expired',
- 'invalid_card_number' => 'The credit card number is not valid.',
- 'invalid_expiry' => 'The expiration date is not valid.',
- 'invalid_cvv' => 'The CVV is not valid.',
- 'cost' => 'Cost',
- 'create_invoice_for_sample' => 'Note: create your first invoice to see a preview here.',
+ 'expired' => 'Expireda',
+ 'invalid_card_number' => 'Cartão de Crédito inválido.',
+ 'invalid_expiry' => 'Data para expirar não é valida.',
+ 'invalid_cvv' => 'O código CVV não é válido.',
+ 'cost' => 'Custo',
+ 'create_invoice_for_sample' => 'Nota: cria sua primeira fatura para visualizar aqui.',
// User Permissions
- 'owner' => 'Owner',
- 'administrator' => 'Administrator',
- 'administrator_help' => 'Allow user to manage users, change settings and modify all records',
- 'user_create_all' => 'Create clients, invoices, etc.',
- 'user_view_all' => 'View all clients, invoices, etc.',
- 'user_edit_all' => 'Edit all clients, invoices, etc.',
- 'gateway_help_20' => ':link to sign up for Sage Pay.',
- 'gateway_help_21' => ':link to sign up for Sage Pay.',
- 'partial_due' => 'Partial Due',
- 'restore_vendor' => 'Restore Vendor',
- 'restored_vendor' => 'Successfully restored vendor',
- 'restored_expense' => 'Successfully restored expense',
- 'permissions' => 'Permissions',
- 'create_all_help' => 'Allow user to create and modify records',
- 'view_all_help' => 'Allow user to view records they didn\'t create',
- 'edit_all_help' => 'Allow user to modify records they didn\'t create',
- 'view_payment' => 'View Payment',
+ 'owner' => 'Proprietário',
+ 'administrator' => 'Administrador',
+ 'administrator_help' => 'Permite usuário gerenciar usuários, configurações e alterar todos os cadastros',
+ 'user_create_all' => 'Criar clientes, faturas, etc.',
+ 'user_view_all' => 'Visualizar todos os clientes, faturas, etc.',
+ 'user_edit_all' => 'Editar todos os clientes, faturas, etc.',
+ 'gateway_help_20' => ':link para habilitar Sage Pay.',
+ 'gateway_help_21' => ':link para habilitar Sage Pay.',
+ 'partial_due' => 'Vencimento Parcial',
+ 'restore_vendor' => 'Restaurar Fornecedor',
+ 'restored_vendor' => 'Fornecedor restarurado com sucesso',
+ 'restored_expense' => 'Despesa restaurada com sucesso',
+ 'permissions' => 'Permissões',
+ 'create_all_help' => 'Permite o usuário criar e alterar todos os regitros',
+ 'view_all_help' => 'Permite usuario visualizar regitros que ele não criou',
+ 'edit_all_help' => 'Permite usuario editar regitros que ele não criou',
+ 'view_payment' => 'Visualizar ',
- 'january' => 'January',
- 'february' => 'February',
- 'march' => 'March',
- 'april' => 'April',
- 'may' => 'May',
- 'june' => 'June',
- 'july' => 'July',
- 'august' => 'August',
- 'september' => 'September',
- 'october' => 'October',
- 'november' => 'November',
- 'december' => 'December',
+ 'january' => 'Janeiro',
+ 'february' => 'Fevereiro',
+ 'march' => 'Março',
+ 'april' => 'Abril',
+ 'may' => 'Maio',
+ 'june' => 'Junho',
+ 'july' => 'Julho',
+ 'august' => 'Agosto',
+ 'september' => 'Setembro',
+ 'october' => 'Outubro',
+ 'november' => 'Novembro',
+ 'december' => 'Dezembro',
-);
+);
\ No newline at end of file
diff --git a/resources/views/accounts/api_tokens.blade.php b/resources/views/accounts/api_tokens.blade.php
index 0b338c8026..9c328e6b26 100644
--- a/resources/views/accounts/api_tokens.blade.php
+++ b/resources/views/accounts/api_tokens.blade.php
@@ -9,7 +9,7 @@
@if (Utils::isNinja())
{!! Button::normal(trans('texts.zapier'))->asLinkTo(ZAPIER_URL)->withAttributes(['target' => '_blank']) !!}
@endif
- @if (Utils::isPro())
+ @if (Utils::hasFeature(FEATURE_API))
{!! Button::primary(trans('texts.add_token'))->asLinkTo(URL::to('/tokens/create'))->appendIcon(Icon::create('plus-sign')) !!}
@endif
diff --git a/resources/views/accounts/client_portal.blade.php b/resources/views/accounts/client_portal.blade.php
index 8c4171b0f7..710858ae98 100644
--- a/resources/views/accounts/client_portal.blade.php
+++ b/resources/views/accounts/client_portal.blade.php
@@ -18,7 +18,7 @@
{!! Former::populateField('enable_portal_password', intval($enable_portal_password)) !!}
{!! Former::populateField('send_portal_password', intval($send_portal_password)) !!}
-@if (!Utils::isNinja() && !Auth::user()->account->isWhiteLabel())
+@if (!Utils::isNinja() && !Auth::user()->account->hasFeature(FEATURE_WHITE_LABEL))
{!! trans('texts.custom_css') !!}
@@ -74,6 +75,7 @@
->style("min-width:100%;max-width:100%;font-family:'Roboto Mono', 'Lucida Console', Monaco, monospace;font-size:14px;'") !!}
diff --git a/resources/views/expenses/edit.blade.php b/resources/views/expenses/edit.blade.php
index 2b999114f5..49e3b9e379 100644
--- a/resources/views/expenses/edit.blade.php
+++ b/resources/views/expenses/edit.blade.php
@@ -362,7 +362,7 @@
}
}
- @if (Auth::user()->account->isPro())
+ @if (Auth::user()->account->hasFeature(FEATURE_DOCUMENTS))
function handleDocumentAdded(file){
if(file.mock)return;
file.index = model.documents().length;
diff --git a/resources/views/header.blade.php b/resources/views/header.blade.php
index 40b499fe88..50999b8cc1 100644
--- a/resources/views/header.blade.php
+++ b/resources/views/header.blade.php
@@ -431,7 +431,7 @@