diff --git a/.env.example b/.env.example index 2d1db72861cc..18053683d7ba 100644 --- a/.env.example +++ b/.env.example @@ -26,9 +26,14 @@ MAILGUN_SECRET= #POSTMARK_API_TOKEN= PHANTOMJS_CLOUD_KEY='a-demo-key-with-low-quota-per-ip-address' +#PHANTOMJS_BIN_PATH=/usr/local/bin/phantomjs + LOG=single REQUIRE_HTTPS=false API_SECRET=password +IOS_DEVICE= +ANDROID_DEVICE= +FCM_API_TOKEN= #TRUSTED_PROXIES= @@ -46,6 +51,7 @@ API_SECRET=password #GOOGLE_CLIENT_SECRET= #GOOGLE_OAUTH_REDIRECT=http://ninja.dev/auth/google +GOOGLE_MAPS_ENABLED=true #GOOGLE_MAPS_API_KEY= #S3_KEY= diff --git a/.travis.yml b/.travis.yml index 727f351d7d1b..5eaa04417349 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,8 @@ php: # - 5.5.9 # - 5.6 # - 5.6 - - 7.0 +# - 7.0 + - 7.1 # - hhvm addons: diff --git a/app/Console/Commands/ChargeRenewalInvoices.php b/app/Console/Commands/ChargeRenewalInvoices.php index 0e9ef0e4c071..df3ee079a9d5 100644 --- a/app/Console/Commands/ChargeRenewalInvoices.php +++ b/app/Console/Commands/ChargeRenewalInvoices.php @@ -6,7 +6,6 @@ use App\Ninja\Repositories\AccountRepository; use App\Services\PaymentService; use App\Models\Invoice; use App\Models\Account; -use Exception; /** * Class ChargeRenewalInvoices @@ -81,11 +80,10 @@ class ChargeRenewalInvoices extends Command continue; } - try { - $this->info("Charging invoice {$invoice->invoice_number}"); - $this->paymentService->autoBillInvoice($invoice); - } catch (Exception $exception) { - $this->info('Error: ' . $exception->getMessage()); + $this->info("Charging invoice {$invoice->invoice_number}"); + if ( ! $this->paymentService->autoBillInvoice($invoice)) { + $this->info('Failed to auto-bill, emailing invoice'); + $this->mailer->sendInvoice($invoice); } } diff --git a/app/Console/Commands/CheckData.php b/app/Console/Commands/CheckData.php index a39681a38456..c5f2d8e2aed3 100644 --- a/app/Console/Commands/CheckData.php +++ b/app/Console/Commands/CheckData.php @@ -2,6 +2,7 @@ use DB; use Mail; +use Utils; use Carbon; use Illuminate\Console\Command; use Symfony\Component\Console\Input\InputOption; @@ -139,20 +140,26 @@ class CheckData extends Command { ENTITY_VENDOR, ENTITY_INVOICE, ENTITY_USER + ], + 'products' => [ + ENTITY_USER, + ], + 'expense_categories' => [ + ENTITY_USER, + ], + 'projects' => [ + ENTITY_USER, + ENTITY_CLIENT, ] ]; foreach ($tables as $table => $entityTypes) { foreach ($entityTypes as $entityType) { + $tableName = Utils::pluralizeEntityType($entityType); $records = DB::table($table) - ->join("{$entityType}s", "{$entityType}s.id", '=', "{$table}.{$entityType}_id"); - - if ($entityType != ENTITY_CLIENT) { - $records = $records->join('clients', 'clients.id', '=', "{$table}.client_id"); - } - - $records = $records->where("{$table}.account_id", '!=', DB::raw("{$entityType}s.account_id")) - ->get(["{$table}.id", 'clients.account_id', 'clients.user_id']); + ->join($tableName, "{$tableName}.id", '=', "{$table}.{$entityType}_id") + ->where("{$table}.account_id", '!=', DB::raw("{$tableName}.account_id")) + ->get(["{$table}.id"]); if (count($records)) { $this->isValid = false; diff --git a/app/Constants.php b/app/Constants.php new file mode 100644 index 000000000000..92a49a54bfc6 --- /dev/null +++ b/app/Constants.php @@ -0,0 +1,582 @@ + ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], + 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], + 4 => ['card' => 'images/credit_cards/Test-AmericanExpress-Icon.png', 'text' => 'American Express'], + 8 => ['card' => 'images/credit_cards/Test-Diners-Icon.png', 'text' => 'Diners'], + 16 => ['card' => 'images/credit_cards/Test-Discover-Icon.png', 'text' => 'Discover'] + ]; + define('CREDIT_CARDS', serialize($creditCards)); + + $cachedTables = [ + 'currencies' => 'App\Models\Currency', + 'sizes' => 'App\Models\Size', + 'industries' => 'App\Models\Industry', + 'timezones' => 'App\Models\Timezone', + 'dateFormats' => 'App\Models\DateFormat', + 'datetimeFormats' => 'App\Models\DatetimeFormat', + 'languages' => 'App\Models\Language', + 'paymentTerms' => 'App\Models\PaymentTerm', + 'paymentTypes' => 'App\Models\PaymentType', + 'countries' => 'App\Models\Country', + 'invoiceDesigns' => 'App\Models\InvoiceDesign', + 'invoiceStatus' => 'App\Models\InvoiceStatus', + 'frequencies' => 'App\Models\Frequency', + 'gateways' => 'App\Models\Gateway', + 'gatewayTypes' => 'App\Models\GatewayType', + 'fonts' => 'App\Models\Font', + 'banks' => 'App\Models\Bank', + ]; + define('CACHED_TABLES', serialize($cachedTables)); + + + // TODO remove these translation functions + function uctrans($text) + { + return ucwords(trans($text)); + } + + // optional trans: only return the string if it's translated + function otrans($text) + { + $locale = Session::get(SESSION_LOCALE); + + if ($locale == 'en') { + return trans($text); + } else { + $string = trans($text); + $english = trans($text, [], 'en'); + return $string != $english ? $string : ''; + } + } + + // include modules in translations + function mtrans($entityType, $text = false) + { + if ( ! $text) { + $text = $entityType; + } + + // check if this has been translated in a module language file + if ( ! Utils::isNinjaProd() && $module = Module::find($entityType)) { + $key = "{$module->getLowerName()}::texts.{$text}"; + $value = trans($key); + if ($key != $value) { + return $value; + } + } + + return trans("texts.{$text}"); + } +} diff --git a/app/Constants/Domain.php b/app/Constants/Domain.php new file mode 100644 index 000000000000..afdcbf90c52e --- /dev/null +++ b/app/Constants/Domain.php @@ -0,0 +1,29 @@ +invitation = $invitation; + $this->notes = $notes; } } diff --git a/app/Events/QuoteInvitationWasEmailed.php b/app/Events/QuoteInvitationWasEmailed.php index 54481ab9e763..a8d8d58ebeb7 100644 --- a/app/Events/QuoteInvitationWasEmailed.php +++ b/app/Events/QuoteInvitationWasEmailed.php @@ -15,14 +15,20 @@ class QuoteInvitationWasEmailed extends Event */ public $invitation; + /** + * @var String + */ + public $notes; + /** * Create a new event instance. * * @param Invitation $invitation */ - public function __construct(Invitation $invitation) + public function __construct(Invitation $invitation, $notes) { $this->invitation = $invitation; + $this->notes = $notes; } } diff --git a/app/Http/Controllers/AccountApiController.php b/app/Http/Controllers/AccountApiController.php index 1920978c0e77..f81e50510928 100644 --- a/app/Http/Controllers/AccountApiController.php +++ b/app/Http/Controllers/AccountApiController.php @@ -27,11 +27,14 @@ class AccountApiController extends BaseAPIController $this->accountRepo = $accountRepo; } - public function ping() + public function ping(Request $request) { $headers = Utils::getApiHeaders(); - return Response::make(RESULT_SUCCESS, 200, $headers); + if(hash_equals(env(API_SECRET),$request->api_secret)) + return Response::make(RESULT_SUCCESS, 200, $headers); + else + return $this->errorResponse(['message'=>'API Secret does not match .env variable'], 400); } public function register(RegisterRequest $request) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index e486d3e63254..83f90f0869e5 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -39,6 +39,9 @@ use App\Services\AuthService; use App\Services\PaymentService; use App\Http\Requests\UpdateAccountRequest; +use App\Http\Requests\SaveClientPortalSettings; +use App\Http\Requests\SaveEmailSettings; + /** * Class AccountController */ @@ -233,6 +236,7 @@ class AccountController extends BaseController $company->plan_expires = date_create()->modify($term == PLAN_TERM_MONTHLY ? '+1 month' : '+1 year')->format('Y-m-d'); } + $company->trial_plan = null; $company->plan = $plan; $company->save(); @@ -521,7 +525,7 @@ class AccountController extends BaseController $data = [ 'account' => Auth::user()->account, 'title' => trans('texts.tax_rates'), - 'taxRates' => TaxRate::scope()->get(['id', 'name', 'rate']), + 'taxRates' => TaxRate::scope()->whereIsInclusive(false)->get(['id', 'name', 'rate']), ]; return View::make('accounts.tax_rates', $data); @@ -555,12 +559,12 @@ class AccountController extends BaseController $document = new stdClass(); $client->name = 'Sample Client'; - $client->address1 = trans('texts.address1'); - $client->city = trans('texts.city'); - $client->state = trans('texts.state'); - $client->postal_code = trans('texts.postal_code'); - $client->work_phone = trans('texts.work_phone'); - $client->work_email = trans('texts.work_id'); + $client->address1 = '10 Main St.'; + $client->city = 'New York'; + $client->state = 'NY'; + $client->postal_code = '10000'; + $client->work_phone = '(212) 555-0000'; + $client->work_email = 'sample@example.com'; $invoice->invoice_number = '0000'; $invoice->invoice_date = Utils::fromSqlDate(date('Y-m-d')); @@ -713,14 +717,10 @@ class AccountController extends BaseController return AccountController::export(); } elseif ($section === ACCOUNT_INVOICE_SETTINGS) { return AccountController::saveInvoiceSettings(); - } elseif ($section === ACCOUNT_EMAIL_SETTINGS) { - return AccountController::saveEmailSettings(); } elseif ($section === ACCOUNT_INVOICE_DESIGN) { return AccountController::saveInvoiceDesign(); } elseif ($section === ACCOUNT_CUSTOMIZE_DESIGN) { return AccountController::saveCustomizeDesign(); - } elseif ($section === ACCOUNT_CLIENT_PORTAL) { - return AccountController::saveClientPortal(); } elseif ($section === ACCOUNT_TEMPLATES_AND_REMINDERS) { return AccountController::saveEmailTemplates(); } elseif ($section === ACCOUNT_PRODUCTS) { @@ -788,53 +788,30 @@ class AccountController extends BaseController /** * @return \Illuminate\Http\RedirectResponse */ - private function saveClientPortal() + public function saveClientPortalSettings(SaveClientPortalSettings $request) { - $account = Auth::user()->account; - $account->fill(Input::all()); - - // Only allowed for pro Invoice Ninja users or white labeled self-hosted users - if (Auth::user()->account->hasFeature(FEATURE_CLIENT_PORTAL_CSS)) { - $input_css = Input::get('client_view_css'); - if (Utils::isNinja()) { - // Allow referencing the body element - $input_css = preg_replace('/(? - // - - // Create a new configuration object - $config = \HTMLPurifier_Config::createDefault(); - $config->set('Filter.ExtractStyleBlocks', true); - $config->set('CSS.AllowImportant', true); - $config->set('CSS.AllowTricky', true); - $config->set('CSS.Trusted', true); - - // Create a new purifier instance - $purifier = new \HTMLPurifier($config); - - // Wrap our CSS in style tags and pass to purifier. - // we're not actually interested in the html response though - $html = $purifier->purify(''); - - // The "style" blocks are stored seperately - $output_css = $purifier->context->get('StyleBlocks'); - - // Get the first style block - $sanitized_css = count($output_css) ? $output_css[0] : ''; - } else { - $sanitized_css = $input_css; - } - - $account->client_view_css = $sanitized_css; - } - + $account = $request->user()->account; + $account->fill($request->all()); + $account->subdomain = $request->subdomain; + $account->iframe_url = $request->iframe_url; $account->save(); - Session::flash('message', trans('texts.updated_settings')); + return redirect('settings/' . ACCOUNT_CLIENT_PORTAL) + ->with('message', trans('texts.updated_settings')); + } - return Redirect::to('settings/'.ACCOUNT_CLIENT_PORTAL); + /** + * @return $this|\Illuminate\Http\RedirectResponse + */ + public function saveEmailSettings(SaveEmailSettings $request) + { + $account = $request->user()->account; + $account->fill($request->all()); + $account->bcc_email = $request->bcc_email; + $account->save(); + + return redirect('settings/' . ACCOUNT_EMAIL_SETTINGS) + ->with('message', trans('texts.updated_settings')); } /** @@ -904,83 +881,18 @@ class AccountController extends BaseController return Redirect::to('settings/'.ACCOUNT_PRODUCTS); } - /** - * @return $this|\Illuminate\Http\RedirectResponse - */ - private function saveEmailSettings() - { - if (Auth::user()->account->hasFeature(FEATURE_CUSTOM_EMAILS)) { - $user = Auth::user(); - $subdomain = null; - $iframeURL = null; - $rules = []; - - if (Input::get('custom_link') == 'subdomain') { - $subdomain = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', substr(strtolower(Input::get('subdomain')), 0, MAX_SUBDOMAIN_LENGTH)); - if (Utils::isNinja()) { - $exclude = [ - 'www', - 'app', - 'mail', - 'admin', - 'blog', - 'user', - 'contact', - 'payment', - 'payments', - 'billing', - 'invoice', - 'business', - 'owner', - 'info', - 'ninja', - 'docs', - 'doc', - 'documents' - ]; - $rules['subdomain'] = "unique:accounts,subdomain,{$user->account_id},id|not_in:" . implode(',', $exclude); - } - } else { - $iframeURL = preg_replace('/[^a-zA-Z0-9_\-\:\/\.]/', '', substr(strtolower(Input::get('iframe_url')), 0, MAX_IFRAME_URL_LENGTH)); - $iframeURL = rtrim($iframeURL, '/'); - } - - $validator = Validator::make(Input::all(), $rules); - - if ($validator->fails()) { - return Redirect::to('settings/'.ACCOUNT_EMAIL_SETTINGS) - ->withErrors($validator) - ->withInput(); - } else { - $account = Auth::user()->account; - $account->subdomain = $subdomain; - $account->iframe_url = $iframeURL; - $account->pdf_email_attachment = Input::get('pdf_email_attachment') ? true : false; - $account->document_email_attachment = Input::get('document_email_attachment') ? true : false; - $account->email_design_id = Input::get('email_design_id'); - - if (Utils::isNinja()) { - $account->enable_email_markup = Input::get('enable_email_markup') ? true : false; - } - - $account->save(); - Session::flash('message', trans('texts.updated_settings')); - } - } - - return Redirect::to('settings/'.ACCOUNT_EMAIL_SETTINGS); - } - /** * @return $this|\Illuminate\Http\RedirectResponse */ private function saveInvoiceSettings() { if (Auth::user()->account->hasFeature(FEATURE_INVOICE_SETTINGS)) { - $rules = [ - 'invoice_number_pattern' => 'has_counter', - 'quote_number_pattern' => 'has_counter', - ]; + $rules = []; + foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_CLIENT] as $entityType) { + if (Input::get("{$entityType}_number_type") == 'pattern') { + $rules["{$entityType}_number_pattern"] = 'has_counter'; + } + } $validator = Validator::make(Input::all(), $rules); @@ -1015,6 +927,10 @@ class AccountController extends BaseController $account->auto_convert_quote = Input::get('auto_convert_quote'); $account->recurring_invoice_number_prefix = Input::get('recurring_invoice_number_prefix'); + $account->client_number_prefix = trim(Input::get('client_number_prefix')); + $account->client_number_pattern = trim(Input::get('client_number_pattern')); + $account->client_number_counter = Input::get('client_number_counter'); + if (Input::has('recurring_hour')) { $account->recurring_hour = Input::get('recurring_hour'); } @@ -1023,20 +939,14 @@ class AccountController extends BaseController $account->quote_number_counter = Input::get('quote_number_counter'); } - if (Input::get('invoice_number_type') == 'prefix') { - $account->invoice_number_prefix = trim(Input::get('invoice_number_prefix')); - $account->invoice_number_pattern = null; - } else { - $account->invoice_number_pattern = trim(Input::get('invoice_number_pattern')); - $account->invoice_number_prefix = null; - } - - if (Input::get('quote_number_type') == 'prefix') { - $account->quote_number_prefix = trim(Input::get('quote_number_prefix')); - $account->quote_number_pattern = null; - } else { - $account->quote_number_pattern = trim(Input::get('quote_number_pattern')); - $account->quote_number_prefix = null; + foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_CLIENT] as $entityType) { + if (Input::get("{$entityType}_number_type") == 'prefix') { + $account->{"{$entityType}_number_prefix"} = trim(Input::get("{$entityType}_number_prefix")); + $account->{"{$entityType}_number_pattern"} = null; + } else { + $account->{"{$entityType}_number_pattern"} = trim(Input::get("{$entityType}_number_pattern")); + $account->{"{$entityType}_number_prefix"} = null; + } } if (!$account->share_counter @@ -1218,6 +1128,13 @@ class AccountController extends BaseController $user->email = trim(strtolower(Input::get('email'))); $user->phone = trim(Input::get('phone')); + if ( ! Auth::user()->is_admin) { + $user->notify_sent = Input::get('notify_sent'); + $user->notify_viewed = Input::get('notify_viewed'); + $user->notify_paid = Input::get('notify_paid'); + $user->notify_approved = Input::get('notify_approved'); + } + if (Utils::isNinja()) { if (Input::get('referral_code') && !$user->referral_code) { $user->referral_code = $this->accountRepo->getReferralCode(); @@ -1461,22 +1378,6 @@ class AccountController extends BaseController return Redirect::to('/settings/'.ACCOUNT_USER_DETAILS)->with('message', trans('texts.confirmation_resent')); } - /** - * @param $plan - * @return \Illuminate\Http\RedirectResponse - */ - public function startTrial($plan) - { - /** @var \App\Models\User $user */ - $user = Auth::user(); - - if ($user->isEligibleForTrial($plan)) { - $user->account->startTrial($plan); - } - - return Redirect::back()->with('message', trans('texts.trial_success')); - } - /** * @param $section * @param bool $subSection diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index dd35fc0ed032..7d2d97e294e1 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -266,21 +266,21 @@ class AccountGatewayController extends BaseController $config->publishableKey = $oldConfig->publishableKey; } - $plaidClientId = Input::get('plaid_client_id'); + $plaidClientId = trim(Input::get('plaid_client_id')); if ($plaidClientId = str_replace('*', '', $plaidClientId)) { $config->plaidClientId = $plaidClientId; } elseif ($oldConfig && property_exists($oldConfig, 'plaidClientId')) { $config->plaidClientId = $oldConfig->plaidClientId; } - $plaidSecret = Input::get('plaid_secret'); + $plaidSecret = trim(Input::get('plaid_secret')); if ($plaidSecret = str_replace('*', '', $plaidSecret)) { $config->plaidSecret = $plaidSecret; } elseif ($oldConfig && property_exists($oldConfig, 'plaidSecret')) { $config->plaidSecret = $oldConfig->plaidSecret; } - $plaidPublicKey = Input::get('plaid_public_key'); + $plaidPublicKey = trim(Input::get('plaid_public_key')); if ($plaidPublicKey = str_replace('*', '', $plaidPublicKey)) { $config->plaidPublicKey = $plaidPublicKey; } elseif ($oldConfig && property_exists($oldConfig, 'plaidPublicKey')) { diff --git a/app/Http/Controllers/AppController.php b/app/Http/Controllers/AppController.php index 647ea7dd2eec..9a6a32ea78eb 100644 --- a/app/Http/Controllers/AppController.php +++ b/app/Http/Controllers/AppController.php @@ -56,6 +56,7 @@ class AppController extends BaseController $app = Input::get('app'); $app['key'] = env('APP_KEY') ?: str_random(RANDOM_KEY_LENGTH); $app['debug'] = Input::get('debug') ? 'true' : 'false'; + $app['https'] = Input::get('https') ? 'true' : 'false'; $database = Input::get('database'); $dbType = 'mysql'; // $database['default']; @@ -80,6 +81,7 @@ class AppController extends BaseController $_ENV['APP_ENV'] = 'production'; $_ENV['APP_DEBUG'] = $app['debug']; + $_ENV['REQUIRE_HTTPS'] = $app['https']; $_ENV['APP_URL'] = $app['url']; $_ENV['APP_KEY'] = $app['key']; $_ENV['APP_CIPHER'] = env('APP_CIPHER', 'AES-256-CBC'); @@ -157,6 +159,7 @@ class AppController extends BaseController $_ENV['APP_URL'] = $app['url']; $_ENV['APP_DEBUG'] = Input::get('debug') ? 'true' : 'false'; + $_ENV['REQUIRE_HTTPS'] = Input::get('https') ? 'true' : 'false'; $_ENV['DB_TYPE'] = 'mysql'; // $db['default']; $_ENV['DB_HOST'] = $db['type']['host']; @@ -279,7 +282,7 @@ class AppController extends BaseController // legacy fix: check cipher is in .env file if ( ! env('APP_CIPHER')) { $fp = fopen(base_path().'/.env', 'a'); - fwrite($fp, "\nAPP_CIPHER=rijndael-128"); + fwrite($fp, "\nAPP_CIPHER=AES-256-CBC"); fclose($fp); } diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index 0824f4d44e4d..c016c6ba97ee 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -141,6 +141,7 @@ class AuthController extends Controller if (Auth::check()) { Event::fire(new UserLoggedIn()); + /* $users = false; // we're linking a new account if ($request->link_accounts && $userId && Auth::user()->id != $userId) { @@ -150,6 +151,9 @@ class AuthController extends Controller } else { $users = $this->accountRepo->loadAccounts(Auth::user()->id); } + */ + + $users = $this->accountRepo->loadAccounts(Auth::user()->id); Session::put(SESSION_USER_ACCOUNTS, $users); } elseif ($user) { diff --git a/app/Http/Controllers/BlueVineController.php b/app/Http/Controllers/BlueVineController.php index d6cd3bfa26f6..aeab0508c22e 100644 --- a/app/Http/Controllers/BlueVineController.php +++ b/app/Http/Controllers/BlueVineController.php @@ -61,9 +61,9 @@ class BlueVineController extends BaseController { } } - $account = $user->primaryAccount(); - $account->bluevine_status = 'signed_up'; - $account->save(); + $company = $user->account->company; + $company->bluevine_status = 'signed_up'; + $company->save(); $quote_data = json_decode( $response->getBody() ); @@ -74,9 +74,9 @@ class BlueVineController extends BaseController { $user = Auth::user(); if ( $user ) { - $account = $user->primaryAccount(); - $account->bluevine_status = 'ignored'; - $account->save(); + $company = $user->account->company; + $company->bluevine_status = 'ignored'; + $company->save(); } return 'success'; diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 321692f7e3d4..c2fd0a93d614 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -84,7 +84,10 @@ class ClientController extends BaseController $user = Auth::user(); $actionLinks = []; - if($user->can('create', ENTITY_TASK)){ + if ($user->can('create', ENTITY_INVOICE)){ + $actionLinks[] = ['label' => trans('texts.new_invoice'), 'url' => URL::to('/invoices/create/'.$client->public_id)]; + } + if ($user->can('create', ENTITY_TASK)){ $actionLinks[] = ['label' => trans('texts.new_task'), 'url' => URL::to('/tasks/create/'.$client->public_id)]; } if (Utils::hasFeature(FEATURE_QUOTES) && $user->can('create', ENTITY_QUOTE)) { @@ -215,4 +218,28 @@ class ClientController extends BaseController return $this->returnBulk(ENTITY_CLIENT, $action, $ids); } + + public function statement() + { + $account = Auth::user()->account; + $client = Client::scope(request()->client_id)->with('contacts')->firstOrFail(); + $invoice = $account->createInvoice(ENTITY_INVOICE); + $invoice->client = $client; + $invoice->date_format = $account->date_format ? $account->date_format->format_moment : 'MMM D, YYYY'; + $invoice->invoice_items = Invoice::scope() + ->with(['client']) + ->whereClientId($client->id) + ->invoices() + ->whereIsPublic(true) + ->where('balance', '>', 0) + ->get(); + + $data = [ + 'showBreadcrumbs' => false, + 'client' => $client, + 'invoice' => $invoice, + ]; + + return view('clients.statement', $data); + } } diff --git a/app/Http/Controllers/ClientPortalController.php b/app/Http/Controllers/ClientPortalController.php index 9ee46c7b9ec4..7cefa6e1e1bf 100644 --- a/app/Http/Controllers/ClientPortalController.php +++ b/app/Http/Controllers/ClientPortalController.php @@ -63,6 +63,8 @@ class ClientPortalController extends BaseController ]); } + $account->loadLocalizationSettings($client); + if (!Input::has('phantomjs') && !Input::has('silent') && !Session::has($invitationKey) && (!Auth::check() || Auth::user()->account_id != $invoice->account_id)) { if ($invoice->isType(INVOICE_TYPE_QUOTE)) { @@ -75,8 +77,6 @@ class ClientPortalController extends BaseController Session::put($invitationKey, true); // track this invitation has been seen Session::put('contact_key', $invitation->contact->contact_key);// track current contact - $account->loadLocalizationSettings($client); - $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); $invoice->due_date = Utils::fromSqlDate($invoice->due_date); $invoice->features = [ @@ -99,6 +99,11 @@ class ClientPortalController extends BaseController 'phone', ]); + // translate the client country name + if ($invoice->client->country) { + $invoice->client->country->name = trans('texts.country_' . $invoice->client->country->name); + } + $data = []; $paymentTypes = $this->getPaymentTypes($account, $client, $invitation); $paymentURL = ''; @@ -204,9 +209,13 @@ class ClientPortalController extends BaseController return RESULT_FAILURE; } - $invitation->signature_base64 = Input::get('signature'); - $invitation->signature_date = date_create(); - $invitation->save(); + if ($signature = Input::get('signature')) { + $invitation->signature_base64 = $signature; + $invitation->signature_date = date_create(); + $invitation->save(); + } + + session(['authorized:' . $invitation->invitation_key => true]); return RESULT_SUCCESS; } diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 764d78111517..02c0988a5368 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -43,11 +43,13 @@ class DashboardController extends BaseController $expenses = $dashboardRepo->expenses($accountId, $userId, $viewAll); $tasks = $dashboardRepo->tasks($accountId, $userId, $viewAll); - $showBlueVinePromo = $user->is_admin + $showBlueVinePromo = $user->is_admin && env('BLUEVINE_PARTNER_UNIQUE_ID') && ! $account->company->bluevine_status && $account->created_at <= date( 'Y-m-d', strtotime( '-1 month' )); + $showWhiteLabelExpired = Utils::isSelfHost() && $account->company->hasExpiredPlan(PLAN_WHITE_LABEL); + // check if the account has quotes $hasQuotes = false; foreach ([$upcoming, $pastDue] as $data) { @@ -97,6 +99,7 @@ class DashboardController extends BaseController 'expenses' => $expenses, 'tasks' => $tasks, 'showBlueVinePromo' => $showBlueVinePromo, + 'showWhiteLabelExpired' => $showWhiteLabelExpired, ]; if ($showBlueVinePromo) { diff --git a/app/Http/Controllers/DocumentAPIController.php b/app/Http/Controllers/DocumentAPIController.php index 3dd225d1445d..b639bb7dda65 100644 --- a/app/Http/Controllers/DocumentAPIController.php +++ b/app/Http/Controllers/DocumentAPIController.php @@ -33,7 +33,20 @@ class DocumentAPIController extends BaseAPIController } /** - * @return \Illuminate\Http\Response + * @SWG\Get( + * path="/documents", + * summary="List of document", + * tags={"document"}, + * @SWG\Response( + * response=200, + * description="A list with documents", + * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Document")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) */ public function index() { @@ -59,13 +72,29 @@ class DocumentAPIController extends BaseAPIController } /** - * @param CreateDocumentRequest $request - * - * @return \Illuminate\Http\Response + * @SWG\Post( + * path="/documents", + * tags={"document"}, + * summary="Create a document", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Document") + * ), + * @SWG\Response( + * response=200, + * description="New document", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Document")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) */ public function store(CreateDocumentRequest $request) { - + $document = $this->documentRepo->upload($request->all()); return $this->itemResponse($document); diff --git a/app/Http/Controllers/ExpenseCategoryApiController.php b/app/Http/Controllers/ExpenseCategoryApiController.php index 5a18735dee62..1d7515ceed40 100644 --- a/app/Http/Controllers/ExpenseCategoryApiController.php +++ b/app/Http/Controllers/ExpenseCategoryApiController.php @@ -9,7 +9,6 @@ use App\Http\Requests\CreateExpenseCategoryRequest; use App\Http\Requests\UpdateExpenseCategoryRequest; use App\Ninja\Repositories\ExpenseCategoryRepository; - class ExpenseCategoryApiController extends BaseAPIController { protected $categoryRepo; @@ -19,23 +18,65 @@ class ExpenseCategoryApiController extends BaseAPIController public function __construct(ExpenseCategoryRepository $categoryRepo, ExpenseCategoryService $categoryService) { parent::__construct(); - + $this->categoryRepo = $categoryRepo; $this->categoryService = $categoryService; } + /** + * @SWG\Post( + * path="/expense_categories", + * tags={"expense_category"}, + * summary="Create an expense category", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/ExpenseCategory") + * ), + * @SWG\Response( + * response=200, + * description="New expense category", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/ExpenseCategory")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ + public function store(CreateExpenseCategoryRequest $request) + { + $category = $this->categoryRepo->save($request->input()); + + return $this->itemResponse($category); + } + + + /** + * @SWG\Put( + * path="/expense_categories/{expense_category_id}", + * tags={"expense_category"}, + * summary="Update an expense category", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/ExpenseCategory") + * ), + * @SWG\Response( + * response=200, + * description="Update expense category", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/ExpenseCategory")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ public function update(UpdateExpenseCategoryRequest $request) { $category = $this->categoryRepo->save($request->input(), $request->entity()); return $this->itemResponse($category); } - - public function store(CreateExpenseCategoryRequest $request) - { - $category = $this->categoryRepo->save($request->input()); - - return $this->itemResponse($category); - - } } diff --git a/app/Http/Controllers/ExpenseCategoryController.php b/app/Http/Controllers/ExpenseCategoryController.php index fdea57cbe8e0..ebee16df7c89 100644 --- a/app/Http/Controllers/ExpenseCategoryController.php +++ b/app/Http/Controllers/ExpenseCategoryController.php @@ -74,7 +74,7 @@ class ExpenseCategoryController extends BaseController Session::flash('message', trans('texts.created_expense_category')); - return redirect()->to('/expense_categories'); + return redirect()->to($category->getRoute()); } public function update(UpdateExpenseCategoryRequest $request) diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php index cc850cbf3272..e159b268f22c 100644 --- a/app/Http/Controllers/ExpenseController.php +++ b/app/Http/Controllers/ExpenseController.php @@ -253,7 +253,7 @@ class ExpenseController extends BaseController 'customLabel1' => Auth::user()->account->custom_vendor_label1, 'customLabel2' => Auth::user()->account->custom_vendor_label2, 'categories' => ExpenseCategory::whereAccountId(Auth::user()->account_id)->withArchived()->orderBy('name')->get(), - 'taxRates' => TaxRate::scope()->orderBy('name')->get(), + 'taxRates' => TaxRate::scope()->whereIsInclusive(false)->orderBy('name')->get(), ]; } diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php index f1e74bd2a924..ed0d1480e12e 100644 --- a/app/Http/Controllers/ExportController.php +++ b/app/Http/Controllers/ExportController.php @@ -32,7 +32,21 @@ class ExportController extends BaseController { $format = $request->input('format'); $date = date('Y-m-d'); - $fileName = "invoice-ninja-{$date}"; + + // set the filename based on the entity types selected + if ($request->include == 'all') { + $fileName = "invoice-ninja-{$date}"; + } else { + $fields = $request->all(); + $fields = array_filter(array_map(function ($key) { + if ( ! in_array($key, ['format', 'include', '_token'])) { + return $key; + } else { + return null; + } + }, array_keys($fields), $fields)); + $fileName = "invoice-ninja-" . join('-', $fields) . "-{$date}"; + } if ($format === 'JSON') { return $this->returnJSON($request, $fileName); diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 4f153f78ef2b..5c5a7b485446 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -1,10 +1,12 @@ true]); - } - /** * @return \Illuminate\Contracts\View\View */ @@ -134,4 +128,24 @@ class HomeController extends BaseController { return RESULT_SUCCESS; } + + /** + * @return mixed + */ + public function contactUs() + { + Mail::raw(request()->message, function ($message) { + $subject = 'Customer Message'; + if ( ! Utils::isNinja()) { + $subject .= ': v' . NINJA_VERSION; + } + $message->to(CONTACT_EMAIL) + ->from(CONTACT_EMAIL, Auth::user()->present()->fullName) + ->replyTo(Auth::user()->email, Auth::user()->present()->fullName) + ->subject($subject); + }); + + return redirect(Request::server('HTTP_REFERER')) + ->with('message', trans('texts.contact_us_response')); + } } diff --git a/app/Http/Controllers/InvoiceApiController.php b/app/Http/Controllers/InvoiceApiController.php index 7ea84d3484cf..ce38734ff3a8 100644 --- a/app/Http/Controllers/InvoiceApiController.php +++ b/app/Http/Controllers/InvoiceApiController.php @@ -12,12 +12,13 @@ use App\Models\Product; use App\Ninja\Repositories\ClientRepository; use App\Ninja\Repositories\PaymentRepository; use App\Ninja\Repositories\InvoiceRepository; -use App\Ninja\Mailers\ContactMailer as Mailer; use App\Http\Requests\InvoiceRequest; use App\Http\Requests\CreateInvoiceAPIRequest; use App\Http\Requests\UpdateInvoiceAPIRequest; use App\Services\InvoiceService; use App\Services\PaymentService; +use App\Jobs\SendInvoiceEmail; +use App\Jobs\SendPaymentEmail; class InvoiceApiController extends BaseAPIController { @@ -25,7 +26,7 @@ class InvoiceApiController extends BaseAPIController protected $entityType = ENTITY_INVOICE; - public function __construct(InvoiceService $invoiceService, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, PaymentRepository $paymentRepo, Mailer $mailer, PaymentService $paymentService) + public function __construct(InvoiceService $invoiceService, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, PaymentRepository $paymentRepo, PaymentService $paymentService) { parent::__construct(); @@ -33,7 +34,6 @@ class InvoiceApiController extends BaseAPIController $this->clientRepo = $clientRepo; $this->paymentRepo = $paymentRepo; $this->invoiceService = $invoiceService; - $this->mailer = $mailer; $this->paymentService = $paymentService; } @@ -185,9 +185,9 @@ class InvoiceApiController extends BaseAPIController if ($isEmailInvoice) { if ($payment) { - $this->mailer->sendPaymentConfirmation($payment); + $this->dispatch(new SendPaymentEmail($payment)); } elseif ( ! $invoice->is_recurring) { - $this->mailer->sendInvoice($invoice); + $this->dispatch(new SendInvoiceEmail($invoice)); } } @@ -229,7 +229,7 @@ class InvoiceApiController extends BaseAPIController } if (!isset($data['invoice_date'])) { - $fields['invoice_date_sql'] = date_create()->format('Y-m-d'); + $fields['invoice_date_sql'] = Utils::today(); } if (!isset($data['due_date'])) { $fields['due_date_sql'] = false; @@ -293,7 +293,7 @@ class InvoiceApiController extends BaseAPIController { $invoice = $request->entity(); - $this->mailer->sendInvoice($invoice); + $this->dispatch(new SendInvoiceEmail($invoice)); $response = json_encode(RESULT_SUCCESS, JSON_PRETTY_PRINT); $headers = Utils::getApiHeaders(); diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index dde8f27a3e39..702e47df3859 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -9,16 +9,16 @@ use Cache; use Redirect; use DB; use URL; -use DropdownButton; use App\Models\Invoice; use App\Models\Client; use App\Models\Account; use App\Models\Product; use App\Models\Expense; +use App\Models\Payment; use App\Models\TaxRate; use App\Models\InvoiceDesign; use App\Models\Activity; -use App\Ninja\Mailers\ContactMailer as Mailer; +use App\Jobs\SendInvoiceEmail; use App\Ninja\Repositories\InvoiceRepository; use App\Ninja\Repositories\ClientRepository; use App\Ninja\Repositories\DocumentRepository; @@ -32,7 +32,6 @@ use App\Http\Requests\UpdateInvoiceRequest; class InvoiceController extends BaseController { - protected $mailer; protected $invoiceRepo; protected $clientRepo; protected $documentRepo; @@ -41,11 +40,10 @@ class InvoiceController extends BaseController protected $recurringInvoiceService; protected $entityType = ENTITY_INVOICE; - public function __construct(Mailer $mailer, InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, InvoiceService $invoiceService, DocumentRepository $documentRepo, RecurringInvoiceService $recurringInvoiceService, PaymentService $paymentService) + public function __construct(InvoiceRepository $invoiceRepo, ClientRepository $clientRepo, InvoiceService $invoiceService, DocumentRepository $documentRepo, RecurringInvoiceService $recurringInvoiceService, PaymentService $paymentService) { // parent::__construct(); - $this->mailer = $mailer; $this->invoiceRepo = $invoiceRepo; $this->clientRepo = $clientRepo; $this->invoiceService = $invoiceService; @@ -100,10 +98,10 @@ class InvoiceController extends BaseController if ($clone) { $invoice->id = $invoice->public_id = null; $invoice->is_public = false; - $invoice->invoice_number = $account->getNextInvoiceNumber($invoice); + $invoice->invoice_number = $account->getNextNumber($invoice); $invoice->balance = $invoice->amount; $invoice->invoice_status_id = 0; - $invoice->invoice_date = date_create()->format('Y-m-d'); + $invoice->invoice_date = Utils::today(); $method = 'POST'; $url = "{$entityType}s"; } else { @@ -124,44 +122,6 @@ class InvoiceController extends BaseController 'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS), ]; - $actions = [ - ['url' => 'javascript:onCloneClick()', 'label' => trans("texts.clone_{$entityType}")], - ['url' => URL::to("{$entityType}s/{$entityType}_history/{$invoice->public_id}"), 'label' => trans('texts.view_history')], - DropdownButton::DIVIDER - ]; - - if ($entityType == ENTITY_QUOTE) { - if ($invoice->quote_invoice_id) { - $actions[] = ['url' => URL::to("invoices/{$invoice->quote_invoice_id}/edit"), 'label' => trans('texts.view_invoice')]; - } else { - $actions[] = ['url' => 'javascript:onConvertClick()', 'label' => trans('texts.convert_to_invoice')]; - } - } elseif ($entityType == ENTITY_INVOICE) { - if ($invoice->quote_id) { - $actions[] = ['url' => URL::to("quotes/{$invoice->quote_id}/edit"), 'label' => trans('texts.view_quote')]; - } - - if (!$invoice->is_recurring && $invoice->balance > 0 && $invoice->is_public) { - $actions[] = ['url' => 'javascript:submitBulkAction("markPaid")', 'label' => trans('texts.mark_paid')]; - $actions[] = ['url' => 'javascript:onPaymentClick()', 'label' => trans('texts.enter_payment')]; - } - - foreach ($invoice->payments as $payment) { - $label = trans('texts.view_payment'); - if (count($invoice->payments) > 1) { - $label .= ' - ' . $account->formatMoney($payment->amount, $invoice->client); - } - $actions[] = ['url' => $payment->present()->url, 'label' => $label]; - } - } - - if (count($actions) > 3) { - $actions[] = DropdownButton::DIVIDER; - } - - $actions[] = ['url' => 'javascript:onArchiveClick()', 'label' => trans("texts.archive_{$entityType}")]; - $actions[] = ['url' => 'javascript:onDeleteClick()', 'label' => trans("texts.delete_{$entityType}")]; - $lastSent = ($invoice->is_recurring && $invoice->last_sent_date) ? $invoice->recurring_invoices->last() : null; if(!Auth::user()->hasPermission('view_all')){ @@ -179,7 +139,6 @@ class InvoiceController extends BaseController 'title' => trans("texts.edit_{$entityType}"), 'client' => $invoice->client, 'isRecurring' => $invoice->is_recurring, - 'actions' => $actions, 'lastSent' => $lastSent]; $data = array_merge($data, self::getViewModel($invoice)); @@ -326,7 +285,11 @@ class InvoiceController extends BaseController $defaultTax = false; foreach ($rates as $rate) { - $options[$rate->rate . ' ' . $rate->name] = $rate->name . ' ' . ($rate->rate+0) . '%'; + $name = $rate->name . ' ' . ($rate->rate+0) . '%'; + if ($rate->is_inclusive) { + $name .= ' - ' . trans('texts.inclusive'); + } + $options[($rate->is_inclusive ? '1 ' : '0 ') . $rate->rate . ' ' . $rate->name] = $name; // load default invoice tax if ($rate->id == $account->default_tax_rate_id) { @@ -340,7 +303,7 @@ class InvoiceController extends BaseController if (isset($options[$key])) { continue; } - $options[$key] = $rate['name'] . ' ' . $rate['rate'] . '%'; + $options['0 ' . $key] = $rate['name'] . ' ' . $rate['rate'] . '%'; } } @@ -452,7 +415,8 @@ class InvoiceController extends BaseController if ($invoice->is_recurring) { $response = $this->emailRecurringInvoice($invoice); } else { - $response = $this->mailer->sendInvoice($invoice, false, $pdfUpload); + $this->dispatch(new SendInvoiceEmail($invoice, false, $pdfUpload)); + return true; } if ($response === true) { @@ -482,7 +446,8 @@ class InvoiceController extends BaseController if ($invoice->isPaid()) { return true; } else { - return $this->mailer->sendInvoice($invoice); + $this->dispatch(new SendInvoiceEmail($invoice)); + return true; } } @@ -514,6 +479,8 @@ class InvoiceController extends BaseController if ($count > 0) { if ($action == 'markSent') { $key = 'marked_sent_invoice'; + } elseif ($action == 'emailInvoice') { + $key = 'emailed_' . $entityType; } elseif ($action == 'markPaid') { $key = 'created_payment'; } else { @@ -543,6 +510,8 @@ class InvoiceController extends BaseController public function invoiceHistory(InvoiceRequest $request) { $invoice = $request->entity(); + $paymentId = $request->payment_id ? Payment::getPrivateId($request->payment_id) : false; + $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); @@ -553,17 +522,21 @@ class InvoiceController extends BaseController ]; $invoice->invoice_type_id = intval($invoice->invoice_type_id); - $activityTypeId = $invoice->isType(INVOICE_TYPE_QUOTE) ? ACTIVITY_TYPE_UPDATE_QUOTE : ACTIVITY_TYPE_UPDATE_INVOICE; - $activities = Activity::scope(false, $invoice->account_id) - ->where('activity_type_id', '=', $activityTypeId) - ->where('invoice_id', '=', $invoice->id) - ->orderBy('id', 'desc') - ->get(['id', 'created_at', 'user_id', 'json_backup']); + $activities = Activity::scope(false, $invoice->account_id); + if ($paymentId) { + $activities->whereIn('activity_type_id', [ACTIVITY_TYPE_CREATE_PAYMENT]) + ->where('payment_id', '=', $paymentId); + } else { + $activities->whereIn('activity_type_id', [ACTIVITY_TYPE_UPDATE_INVOICE, ACTIVITY_TYPE_UPDATE_QUOTE]) + ->where('invoice_id', '=', $invoice->id); + } + $activities = $activities->orderBy('id', 'desc') + ->get(['id', 'created_at', 'user_id', 'json_backup', 'activity_type_id', 'payment_id']); $versionsJson = []; $versionsSelect = []; $lastId = false; - + //dd($activities->toArray()); foreach ($activities as $activity) { if ($backup = json_decode($activity->json_backup)) { $backup->invoice_date = Utils::fromSqlDate($backup->invoice_date); @@ -576,16 +549,17 @@ class InvoiceController extends BaseController $backup->invoice_type_id = isset($backup->invoice_type_id) && intval($backup->invoice_type_id) == INVOICE_TYPE_QUOTE; $backup->account = $invoice->account->toArray(); - $versionsJson[$activity->id] = $backup; + $versionsJson[$paymentId ? 0 : $activity->id] = $backup; $key = Utils::timestampToDateTimeString(strtotime($activity->created_at)) . ' - ' . $activity->user->getDisplayName(); - $versionsSelect[$lastId ? $lastId : 0] = $key; + $versionsSelect[$lastId ?: 0] = $key; $lastId = $activity->id; } else { Utils::logError('Failed to parse invoice backup'); } } - if ($lastId) { + // Show the current version as the last in the history + if ( ! $paymentId) { $versionsSelect[$lastId] = Utils::timestampToDateTimeString(strtotime($invoice->created_at)) . ' - ' . $invoice->user->getDisplayName(); } @@ -595,6 +569,7 @@ class InvoiceController extends BaseController 'versionsSelect' => $versionsSelect, 'invoiceDesigns' => InvoiceDesign::getDesigns(), 'invoiceFonts' => Cache::get('fonts'), + 'paymentId' => $paymentId, ]; return View::make('invoices.history', $data); diff --git a/app/Http/Controllers/NinjaController.php b/app/Http/Controllers/NinjaController.php index 2a0725239eec..cab57f9da2b5 100644 --- a/app/Http/Controllers/NinjaController.php +++ b/app/Http/Controllers/NinjaController.php @@ -6,6 +6,7 @@ use Input; use Utils; use View; use Validator; +use Auth; use URL; use Cache; use Omnipay; @@ -274,4 +275,15 @@ class NinjaController extends BaseController Session::flash('error', $message); Utils::logError("Payment Error [{$type}]: " . ($exception ? Utils::getErrorString($exception) : $message), 'PHP', true); } + + public function hideWhiteLabelMessage() + { + $user = Auth::user(); + $company = $user->account->company; + + $company->plan = null; + $company->save(); + + return RESULT_SUCCESS; + } } diff --git a/app/Http/Controllers/OnlinePaymentController.php b/app/Http/Controllers/OnlinePaymentController.php index b6c9a280cecf..ae596b89eaa7 100644 --- a/app/Http/Controllers/OnlinePaymentController.php +++ b/app/Http/Controllers/OnlinePaymentController.php @@ -11,6 +11,7 @@ use Exception; use Validator; use App\Models\Invitation; use App\Models\Account; +use App\Models\Client; use App\Models\Payment; use App\Models\Product; use App\Models\PaymentMethod; @@ -76,6 +77,11 @@ class OnlinePaymentController extends BaseController $invitation = $invitation->load('invoice.client.account.account_gateways.gateway'); $account = $invitation->account; + + if ($account->requiresAuthorization($invitation->invoice) && ! session('authorized:' . $invitation->invitation_key)) { + return redirect()->to('view/' . $invitation->invitation_key); + } + $account->loadLocalizationSettings($invitation->invoice->client); if ( ! $gatewayTypeAlias) { @@ -283,22 +289,31 @@ class OnlinePaymentController extends BaseController return redirect()->to("{$failureUrl}/?error=invalid product"); } - $rules = [ - 'first_name' => 'string|max:100', - 'last_name' => 'string|max:100', - 'email' => 'email|string|max:100', - ]; - - $validator = Validator::make(Input::all(), $rules); - if ($validator->fails()) { - return redirect()->to("{$failureUrl}/?error=" . $validator->errors()->first()); + // check for existing client using contact_key + $client = false; + if ($contactKey = Input::get('contact_key')) { + $client = Client::scope()->whereHas('contacts', function ($query) use ($contactKey) { + $query->where('contact_key', $contactKey); + })->first(); } + if ( ! $client) { + $rules = [ + 'first_name' => 'string|max:100', + 'last_name' => 'string|max:100', + 'email' => 'email|string|max:100', + ]; - $data = [ - 'currency_id' => $account->currency_id, - 'contact' => Input::all() - ]; - $client = $clientRepo->save($data); + $validator = Validator::make(Input::all(), $rules); + if ($validator->fails()) { + return redirect()->to("{$failureUrl}/?error=" . $validator->errors()->first()); + } + + $data = [ + 'currency_id' => $account->currency_id, + 'contact' => Input::all() + ]; + $client = $clientRepo->save($data); + } $data = [ 'client_id' => $client->id, diff --git a/app/Http/Controllers/PaymentApiController.php b/app/Http/Controllers/PaymentApiController.php index b311b1caf4d9..4daad26d0d88 100644 --- a/app/Http/Controllers/PaymentApiController.php +++ b/app/Http/Controllers/PaymentApiController.php @@ -108,6 +108,9 @@ class PaymentApiController extends BaseAPIController */ public function store(CreatePaymentAPIRequest $request) { + // check payment has been marked sent + $request->invoice->markSentIfUnsent(); + $payment = $this->paymentRepo->save($request->input()); if (Input::get('email_receipt')) { @@ -142,7 +145,7 @@ class PaymentApiController extends BaseAPIController public function destroy(UpdatePaymentRequest $request) { $payment = $request->entity(); - + $this->clientRepo->delete($payment); return $this->itemResponse($payment); diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 2b38ba25d76b..bcb244f1e21d 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -5,6 +5,7 @@ use Session; use Utils; use View; use Cache; +use DropdownButton; use App\Models\Invoice; use App\Models\Client; use App\Ninja\Repositories\PaymentRepository; @@ -84,7 +85,6 @@ class PaymentController extends BaseController { $invoices = Invoice::scope() ->invoices() - ->whereIsPublic(true) ->where('invoices.balance', '>', 0) ->with('client', 'invoice_status') ->orderBy('invoice_number')->get(); @@ -122,9 +122,21 @@ class PaymentController extends BaseController public function edit(PaymentRequest $request) { $payment = $request->entity(); - $payment->payment_date = Utils::fromSqlDate($payment->payment_date); + $actions = []; + if ($payment->invoiceJsonBackup()) { + $actions[] = ['url' => url("/invoices/invoice_history/{$payment->invoice->public_id}?payment_id={$payment->public_id}"), 'label' => trans('texts.view_invoice')]; + } + $actions[] = ['url' => url("/invoices/{$payment->invoice->public_id}/edit"), 'label' => trans('texts.edit_invoice')]; + $actions[] = DropdownButton::DIVIDER; + if ( ! $payment->trashed()) { + $actions[] = ['url' => 'javascript:submitAction("archive")', 'label' => trans('texts.archive_payment')]; + $actions[] = ['url' => 'javascript:onDeleteClick()', 'label' => trans('texts.delete_payment')]; + } else { + $actions[] = ['url' => 'javascript:submitAction("restore")', 'label' => trans('texts.restore_expense')]; + } + $data = [ 'client' => null, 'invoice' => null, @@ -138,6 +150,7 @@ class PaymentController extends BaseController 'method' => 'PUT', 'url' => 'payments/'.$payment->public_id, 'title' => trans('texts.edit_payment'), + 'actions' => $actions, 'paymentTypes' => Cache::get('paymentTypes'), 'clients' => Client::scope()->with('contacts')->orderBy('name')->get(), ]; @@ -151,8 +164,10 @@ class PaymentController extends BaseController */ public function store(CreatePaymentRequest $request) { - $input = $request->input(); + // check payment has been marked sent + $request->invoice->markSentIfUnsent(); + $input = $request->input(); $input['invoice_id'] = Invoice::getPrivateId($input['invoice']); $input['client_id'] = Client::getPrivateId($input['client']); $payment = $this->paymentRepo->save($input); @@ -173,6 +188,10 @@ class PaymentController extends BaseController */ public function update(UpdatePaymentRequest $request) { + if (in_array($request->action, ['archive', 'delete', 'restore'])) { + return self::bulk(); + } + $payment = $this->paymentRepo->save($request->input(), $request->entity()); Session::flash('message', trans('texts.updated_payment')); @@ -191,7 +210,7 @@ class PaymentController extends BaseController $count = $this->paymentService->bulk($ids, $action, ['amount'=>$amount]); if ($count > 0) { - $message = Utils::pluralize($action=='refund'?'refunded_payment':$action.'d_payment', $count); + $message = Utils::pluralize($action=='refund' ? 'refunded_payment':$action.'d_payment', $count); Session::flash('message', $message); } diff --git a/app/Http/Controllers/ProductApiController.php b/app/Http/Controllers/ProductApiController.php index 0143007d84dd..f0a0b8af43e4 100644 --- a/app/Http/Controllers/ProductApiController.php +++ b/app/Http/Controllers/ProductApiController.php @@ -33,7 +33,20 @@ class ProductApiController extends BaseAPIController } /** - * @return \Illuminate\Http\Response + * @SWG\Get( + * path="/products", + * summary="List of products", + * tags={"product"}, + * @SWG\Response( + * response=200, + * description="A list with products", + * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/Product")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) */ public function index() { @@ -45,8 +58,25 @@ class ProductApiController extends BaseAPIController } /** - * @param CreateProductRequest $request - * @return \Illuminate\Http\Response + * @SWG\Post( + * path="/products", + * tags={"product"}, + * summary="Create a product", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Product") + * ), + * @SWG\Response( + * response=200, + * description="New product", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Product")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) */ public function store(CreateProductRequest $request) { @@ -56,16 +86,32 @@ class ProductApiController extends BaseAPIController } /** - * @param UpdateProductRequest $request - * @param $publicId - * @return \Illuminate\Http\Response + * @SWG\Put( + * path="/products/{product_id}", + * tags={"product"}, + * summary="Update a product", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/Product") + * ), + * @SWG\Response( + * response=200, + * description="Update product", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/Product")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) */ public function update(UpdateProductRequest $request, $publicId) { if ($request->action) { return $this->handleAction($request); } - + $data = $request->input(); $data['public_id'] = $publicId; $product = $this->productRepo->save($data, $request->entity()); diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index e9ab8dd005a8..3dd650d5a5fd 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -74,7 +74,7 @@ class ProductController extends BaseController $data = [ 'account' => $account, - 'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->get(['id', 'name', 'rate']) : null, + 'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->whereIsInclusive(false)->get(['id', 'name', 'rate']) : null, 'product' => $product, 'entity' => $product, 'method' => 'PUT', @@ -94,7 +94,7 @@ class ProductController extends BaseController $data = [ 'account' => $account, - 'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->get(['id', 'name', 'rate']) : null, + 'taxRates' => $account->invoice_item_taxes ? TaxRate::scope()->whereIsInclusive(false)->get(['id', 'name', 'rate']) : null, 'product' => null, 'method' => 'POST', 'url' => 'products', diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 3431d5c18323..f23da88bd489 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -6,12 +6,9 @@ use Input; use Utils; use DB; use Session; +use Str; use View; use App\Models\Account; -use App\Models\Client; -use App\Models\Payment; -use App\Models\Expense; -use App\Models\Task; /** * Class ReportController @@ -67,19 +64,22 @@ class ReportController extends BaseController } $reportTypes = [ - ENTITY_CLIENT => trans('texts.client'), - ENTITY_INVOICE => trans('texts.invoice'), - ENTITY_PRODUCT => trans('texts.product'), - ENTITY_PAYMENT => trans('texts.payment'), - ENTITY_EXPENSE => trans('texts.expense'), - ENTITY_TASK => trans('texts.task'), - ENTITY_TAX_RATE => trans('texts.tax'), + 'client', + 'product', + 'invoice', + 'invoice_details', + 'aging', + 'profit_and_loss', + 'payment', + 'expense', + 'task', + 'tax_rate', ]; $params = [ 'startDate' => $startDate->format('Y-m-d'), 'endDate' => $endDate->format('Y-m-d'), - 'reportTypes' => $reportTypes, + 'reportTypes' => array_combine($reportTypes, Utils::trans($reportTypes)), 'reportType' => $reportType, 'title' => trans('texts.charts_and_reports'), 'account' => Auth::user()->account, @@ -87,8 +87,18 @@ class ReportController extends BaseController if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) { $isExport = $action == 'export'; - $params = array_merge($params, self::generateReport($reportType, $startDate, $endDate, $dateField, $isExport)); - + $reportClass = '\\App\\Ninja\\Reports\\' . Str::studly($reportType) . 'Report'; + $options = [ + 'date_field' => $dateField, + 'invoice_status' => request()->invoice_status, + 'group_dates_by' => request()->group_dates_by, + ]; + $report = new $reportClass($startDate, $endDate, $isExport, $options); + if (Input::get('report_type')) { + $report->run(); + } + $params['report'] = $report; + $params = array_merge($params, $report->results()); if ($isExport) { self::export($reportType, $params['displayData'], $params['columns'], $params['reportTotals']); } @@ -96,427 +106,12 @@ class ReportController extends BaseController $params['columns'] = []; $params['displayData'] = []; $params['reportTotals'] = []; + $params['report'] = false; } return View::make('reports.chart_builder', $params); } - /** - * @param $reportType - * @param $startDate - * @param $endDate - * @param $dateField - * @param $isExport - * @return array - */ - private function generateReport($reportType, $startDate, $endDate, $dateField, $isExport) - { - if ($reportType == ENTITY_CLIENT) { - return $this->generateClientReport($startDate, $endDate, $isExport); - } elseif ($reportType == ENTITY_INVOICE) { - return $this->generateInvoiceReport($startDate, $endDate, $isExport); - } elseif ($reportType == ENTITY_PRODUCT) { - return $this->generateProductReport($startDate, $endDate, $isExport); - } elseif ($reportType == ENTITY_PAYMENT) { - return $this->generatePaymentReport($startDate, $endDate, $isExport); - } elseif ($reportType == ENTITY_TAX_RATE) { - return $this->generateTaxRateReport($startDate, $endDate, $dateField, $isExport); - } elseif ($reportType == ENTITY_EXPENSE) { - return $this->generateExpenseReport($startDate, $endDate, $isExport); - } elseif ($reportType == ENTITY_TASK) { - return $this->generateTaskReport($startDate, $endDate, $isExport); - } - } - - private function generateTaskReport($startDate, $endDate, $isExport) - { - $columns = ['client', 'date', 'description', 'duration']; - $displayData = []; - - $tasks = Task::scope() - ->with('client.contacts') - ->withArchived() - ->dateRange($startDate, $endDate); - - foreach ($tasks->get() as $task) { - $displayData[] = [ - $task->client ? ($isExport ? $task->client->getDisplayName() : $task->client->present()->link) : trans('texts.unassigned'), - link_to($task->present()->url, $task->getStartTime()), - $task->present()->description, - Utils::formatTime($task->getDuration()), - ]; - } - - return [ - 'columns' => $columns, - 'displayData' => $displayData, - 'reportTotals' => [], - ]; - } - - /** - * @param $startDate - * @param $endDate - * @param $dateField - * @param $isExport - * @return array - */ - private function generateTaxRateReport($startDate, $endDate, $dateField, $isExport) - { - $columns = ['tax_name', 'tax_rate', 'amount', 'paid']; - - $account = Auth::user()->account; - $displayData = []; - $reportTotals = []; - - $clients = Client::scope() - ->withArchived() - ->with('contacts') - ->with(['invoices' => function($query) use ($startDate, $endDate, $dateField) { - $query->with('invoice_items')->withArchived(); - if ($dateField == FILTER_INVOICE_DATE) { - $query->where('invoice_date', '>=', $startDate) - ->where('invoice_date', '<=', $endDate) - ->with('payments'); - } else { - $query->whereHas('payments', function($query) use ($startDate, $endDate) { - $query->where('payment_date', '>=', $startDate) - ->where('payment_date', '<=', $endDate) - ->withArchived(); - }) - ->with(['payments' => function($query) use ($startDate, $endDate) { - $query->where('payment_date', '>=', $startDate) - ->where('payment_date', '<=', $endDate) - ->withArchived(); - }]); - } - }]); - - foreach ($clients->get() as $client) { - $currencyId = $client->currency_id ?: Auth::user()->account->getCurrencyId(); - $amount = 0; - $paid = 0; - $taxTotals = []; - - foreach ($client->invoices as $invoice) { - foreach ($invoice->getTaxes(true) as $key => $tax) { - if ( ! isset($taxTotals[$currencyId])) { - $taxTotals[$currencyId] = []; - } - if (isset($taxTotals[$currencyId][$key])) { - $taxTotals[$currencyId][$key]['amount'] += $tax['amount']; - $taxTotals[$currencyId][$key]['paid'] += $tax['paid']; - } else { - $taxTotals[$currencyId][$key] = $tax; - } - } - - $amount += $invoice->amount; - $paid += $invoice->getAmountPaid(); - } - - foreach ($taxTotals as $currencyId => $taxes) { - foreach ($taxes as $tax) { - $displayData[] = [ - $tax['name'], - $tax['rate'] . '%', - $account->formatMoney($tax['amount'], $client), - $account->formatMoney($tax['paid'], $client) - ]; - } - - $reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'amount', $tax['amount']); - $reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'paid', $tax['paid']); - } - } - - return [ - 'columns' => $columns, - 'displayData' => $displayData, - 'reportTotals' => $reportTotals, - ]; - - } - - /** - * @param $startDate - * @param $endDate - * @param $isExport - * @return array - */ - private function generatePaymentReport($startDate, $endDate, $isExport) - { - $columns = ['client', 'invoice_number', 'invoice_date', 'amount', 'payment_date', 'paid', 'method']; - - $account = Auth::user()->account; - $displayData = []; - $reportTotals = []; - - $payments = Payment::scope() - ->withArchived() - ->excludeFailed() - ->whereHas('client', function($query) { - $query->where('is_deleted', '=', false); - }) - ->whereHas('invoice', function($query) { - $query->where('is_deleted', '=', false); - }) - ->with('client.contacts', 'invoice', 'payment_type', 'account_gateway.gateway') - ->where('payment_date', '>=', $startDate) - ->where('payment_date', '<=', $endDate); - - foreach ($payments->get() as $payment) { - $invoice = $payment->invoice; - $client = $payment->client; - $displayData[] = [ - $isExport ? $client->getDisplayName() : $client->present()->link, - $isExport ? $invoice->invoice_number : $invoice->present()->link, - $invoice->present()->invoice_date, - $account->formatMoney($invoice->amount, $client), - $payment->present()->payment_date, - $account->formatMoney($payment->getCompletedAmount(), $client), - $payment->present()->method, - ]; - - $reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'amount', $invoice->amount); - $reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'paid', $payment->getCompletedAmount()); - } - - return [ - 'columns' => $columns, - 'displayData' => $displayData, - 'reportTotals' => $reportTotals, - ]; - } - - /** - * @param $startDate - * @param $endDate - * @param $isExport - * @return array - */ - private function generateInvoiceReport($startDate, $endDate, $isExport) - { - $columns = ['client', 'invoice_number', 'invoice_date', 'amount', 'payment_date', 'paid', 'method']; - - $account = Auth::user()->account; - $displayData = []; - $reportTotals = []; - - $clients = Client::scope() - ->withTrashed() - ->with('contacts') - ->where('is_deleted', '=', false) - ->with(['invoices' => function($query) use ($startDate, $endDate) { - $query->invoices() - ->withArchived() - ->where('invoice_date', '>=', $startDate) - ->where('invoice_date', '<=', $endDate) - ->with(['payments' => function($query) { - $query->withArchived() - ->excludeFailed() - ->with('payment_type', 'account_gateway.gateway'); - }, 'invoice_items']) - ->withTrashed(); - }]); - - foreach ($clients->get() as $client) { - foreach ($client->invoices as $invoice) { - - $payments = count($invoice->payments) ? $invoice->payments : [false]; - foreach ($payments as $payment) { - $displayData[] = [ - $isExport ? $client->getDisplayName() : $client->present()->link, - $isExport ? $invoice->invoice_number : $invoice->present()->link, - $invoice->present()->invoice_date, - $account->formatMoney($invoice->amount, $client), - $payment ? $payment->present()->payment_date : '', - $payment ? $account->formatMoney($payment->getCompletedAmount(), $client) : '', - $payment ? $payment->present()->method : '', - ]; - $reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'paid', $payment ? $payment->getCompletedAmount() : 0); - } - - $reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'amount', $invoice->amount); - $reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'balance', $invoice->balance); - } - } - - return [ - 'columns' => $columns, - 'displayData' => $displayData, - 'reportTotals' => $reportTotals, - ]; - } - - /** - * @param $startDate - * @param $endDate - * @param $isExport - * @return array - */ - private function generateProductReport($startDate, $endDate, $isExport) - { - $columns = ['client', 'invoice_number', 'invoice_date', 'quantity', 'product']; - - $account = Auth::user()->account; - $displayData = []; - $reportTotals = []; - - $clients = Client::scope() - ->withTrashed() - ->with('contacts') - ->where('is_deleted', '=', false) - ->with(['invoices' => function($query) use ($startDate, $endDate) { - $query->where('invoice_date', '>=', $startDate) - ->where('invoice_date', '<=', $endDate) - ->where('is_deleted', '=', false) - ->where('is_recurring', '=', false) - ->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD) - ->with(['invoice_items']) - ->withTrashed(); - }]); - - foreach ($clients->get() as $client) { - foreach ($client->invoices as $invoice) { - - foreach ($invoice->invoice_items as $invoiceItem) { - $displayData[] = [ - $isExport ? $client->getDisplayName() : $client->present()->link, - $isExport ? $invoice->invoice_number : $invoice->present()->link, - $invoice->present()->invoice_date, - round($invoiceItem->qty, 2), - $invoiceItem->product_key, - ]; - //$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'paid', $payment ? $payment->amount : 0); - } - - //$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'amount', $invoice->amount); - //$reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'balance', $invoice->balance); - } - } - - return [ - 'columns' => $columns, - 'displayData' => $displayData, - 'reportTotals' => [], - ]; - } - - /** - * @param $startDate - * @param $endDate - * @param $isExport - * @return array - */ - private function generateClientReport($startDate, $endDate, $isExport) - { - $columns = ['client', 'amount', 'paid', 'balance']; - - $account = Auth::user()->account; - $displayData = []; - $reportTotals = []; - - $clients = Client::scope() - ->withArchived() - ->with('contacts') - ->with(['invoices' => function($query) use ($startDate, $endDate) { - $query->where('invoice_date', '>=', $startDate) - ->where('invoice_date', '<=', $endDate) - ->where('invoice_type_id', '=', INVOICE_TYPE_STANDARD) - ->where('is_recurring', '=', false) - ->withArchived(); - }]); - - foreach ($clients->get() as $client) { - $amount = 0; - $paid = 0; - - foreach ($client->invoices as $invoice) { - $amount += $invoice->amount; - $paid += $invoice->getAmountPaid(); - } - - $displayData[] = [ - $isExport ? $client->getDisplayName() : $client->present()->link, - $account->formatMoney($amount, $client), - $account->formatMoney($paid, $client), - $account->formatMoney($amount - $paid, $client) - ]; - - $reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'amount', $amount); - $reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'paid', $paid); - $reportTotals = $this->addToTotals($reportTotals, $client->currency_id, 'balance', $amount - $paid); - } - - return [ - 'columns' => $columns, - 'displayData' => $displayData, - 'reportTotals' => $reportTotals, - ]; - } - - /** - * @param $startDate - * @param $endDate - * @param $isExport - * @return array - */ - private function generateExpenseReport($startDate, $endDate, $isExport) - { - $columns = ['vendor', 'client', 'date', 'expense_amount']; - - $account = Auth::user()->account; - $displayData = []; - $reportTotals = []; - - $expenses = Expense::scope() - ->withArchived() - ->with('client.contacts', 'vendor') - ->where('expense_date', '>=', $startDate) - ->where('expense_date', '<=', $endDate); - - - foreach ($expenses->get() as $expense) { - $amount = $expense->amountWithTax(); - - $displayData[] = [ - $expense->vendor ? ($isExport ? $expense->vendor->name : $expense->vendor->present()->link) : '', - $expense->client ? ($isExport ? $expense->client->getDisplayName() : $expense->client->present()->link) : '', - $expense->present()->expense_date, - Utils::formatMoney($amount, $expense->currency_id), - ]; - - $reportTotals = $this->addToTotals($reportTotals, $expense->expense_currency_id, 'amount', $amount); - $reportTotals = $this->addToTotals($reportTotals, $expense->invoice_currency_id, 'amount', 0); - } - - return [ - 'columns' => $columns, - 'displayData' => $displayData, - 'reportTotals' => $reportTotals, - ]; - } - - /** - * @param $data - * @param $currencyId - * @param $field - * @param $value - * @return mixed - */ - private function addToTotals($data, $currencyId, $field, $value) { - $currencyId = $currencyId ?: Auth::user()->account->getCurrencyId(); - - if (!isset($data[$currencyId][$field])) { - $data[$currencyId][$field] = 0; - } - - $data[$currencyId][$field] += $value; - - return $data; - } - /** * @param $reportType * @param $data @@ -534,6 +129,7 @@ class ReportController extends BaseController Utils::exportData($output, $data, Utils::trans($columns)); + /* fwrite($output, trans('texts.totals')); foreach ($totals as $currencyId => $fields) { foreach ($fields as $key => $value) { @@ -550,6 +146,7 @@ class ReportController extends BaseController } fwrite($output, $csv."\n"); } + */ fclose($output); exit; diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php index 9de256c98976..452ce50ebaa6 100644 --- a/app/Http/Controllers/TaskController.php +++ b/app/Http/Controllers/TaskController.php @@ -253,10 +253,11 @@ class TaskController extends BaseController Session::flash('message', trans('texts.stopped_task')); return Redirect::to('tasks'); } else if ($action == 'invoice' || $action == 'add_to_invoice') { - $tasks = Task::scope($ids)->with('client')->get(); + $tasks = Task::scope($ids)->with('client')->orderBy('project_id', 'id')->get(); $clientPublicId = false; $data = []; + $lastProjectId = false; foreach ($tasks as $task) { if ($task->client) { if (!$clientPublicId) { @@ -276,11 +277,13 @@ class TaskController extends BaseController } $account = Auth::user()->account; + $showProject = $lastProjectId != $task->project_id; $data[] = [ 'publicId' => $task->public_id, - 'description' => $task->description . "\n\n" . $task->present()->times($account), + 'description' => $task->present()->invoiceDescription($account, $showProject), 'duration' => $task->getHours(), ]; + $lastProjectId = $task->project_id; } if ($action == 'invoice') { diff --git a/app/Http/Controllers/TaxRateApiController.php b/app/Http/Controllers/TaxRateApiController.php index e5b0003d6e30..2fa442af49b2 100644 --- a/app/Http/Controllers/TaxRateApiController.php +++ b/app/Http/Controllers/TaxRateApiController.php @@ -28,6 +28,22 @@ class TaxRateApiController extends BaseAPIController $this->taxRateRepo = $taxRateRepo; } + /** + * @SWG\Get( + * path="/tax_rates", + * summary="List of tax rates", + * tags={"tax_rate"}, + * @SWG\Response( + * response=200, + * description="A list with tax rates", + * @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/TaxRate")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ public function index() { $taxRates = TaxRate::scope() @@ -37,6 +53,27 @@ class TaxRateApiController extends BaseAPIController return $this->listResponse($taxRates); } + /** + * @SWG\Post( + * path="/tax_rates", + * tags={"tax_rate"}, + * summary="Create a tax rate", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/TaxRate") + * ), + * @SWG\Response( + * response=200, + * description="New tax rate", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/TaxRate")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) + */ public function store(CreateTaxRateRequest $request) { $taxRate = $this->taxRateRepo->save($request->input()); @@ -45,16 +82,32 @@ class TaxRateApiController extends BaseAPIController } /** - * @param UpdateTaxRateRequest $request - * @param $publicId - * @return \Illuminate\Http\Response + * @SWG\Put( + * path="/tax_rates/{tax_rate_id}", + * tags={"tax_rate"}, + * summary="Update a tax rate", + * @SWG\Parameter( + * in="body", + * name="body", + * @SWG\Schema(ref="#/definitions/TaxRate") + * ), + * @SWG\Response( + * response=200, + * description="Update tax rate", + * @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/TaxRate")) + * ), + * @SWG\Response( + * response="default", + * description="an ""unexpected"" error" + * ) + * ) */ public function update(UpdateTaxRateRequest $request, $publicId) { if ($request->action) { return $this->handleAction($request); } - + $data = $request->input(); $data['public_id'] = $publicId; $taxRate = $this->taxRateRepo->save($data, $request->entity()); diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index e18c8148bb66..1646c539f8c8 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -240,7 +240,7 @@ class UserController extends BaseController $user = User::where('confirmation_code', '=', $code)->get()->first(); if ($user) { - $notice_msg = trans('texts.security.confirmation'); + $notice_msg = trans('texts.security_confirmation'); $user->confirmed = true; $user->confirmation_code = ''; @@ -356,7 +356,7 @@ class UserController extends BaseController Session::put(SESSION_USER_ACCOUNTS, $users); Session::flash('message', trans('texts.unlinked_account')); - return Redirect::to('/dashboard'); + return Redirect::to('/manage_companies'); } public function manageCompanies() diff --git a/app/Http/Controllers/VendorController.php b/app/Http/Controllers/VendorController.php index effda8c55283..bc16ee9e407c 100644 --- a/app/Http/Controllers/VendorController.php +++ b/app/Http/Controllers/VendorController.php @@ -82,7 +82,6 @@ class VendorController extends BaseController 'actionLinks' => $actionLinks, 'showBreadcrumbs' => false, 'vendor' => $vendor, - 'totalexpense' => $vendor->getTotalExpense(), 'title' => trans('texts.view_vendor'), 'hasRecurringInvoices' => false, 'hasQuotes' => false, diff --git a/app/Http/Middleware/ApiCheck.php b/app/Http/Middleware/ApiCheck.php index 6e7e73223d20..93de1b0c433c 100644 --- a/app/Http/Middleware/ApiCheck.php +++ b/app/Http/Middleware/ApiCheck.php @@ -25,7 +25,9 @@ class ApiCheck { { $loggingIn = $request->is('api/v1/login') || $request->is('api/v1/register') - || $request->is('api/v1/oauth_login'); + || $request->is('api/v1/oauth_login') + || $request->is('api/v1/ping'); + $headers = Utils::getApiHeaders(); $hasApiSecret = false; @@ -38,7 +40,8 @@ class ApiCheck { // check API secret if ( ! $hasApiSecret) { sleep(ERROR_DELAY); - return Response::json('Invalid value for API_SECRET', 403, $headers); + $error['error'] = ['message'=>'Invalid value for API_SECRET']; + return Response::json($error, 403, $headers); } } else { // check for a valid token @@ -50,7 +53,8 @@ class ApiCheck { Session::set('token_id', $token->id); } else { sleep(ERROR_DELAY); - return Response::json('Invalid token', 403, $headers); + $error['error'] = ['message'=>'Invalid token']; + return Response::json($error, 403, $headers); } } @@ -59,7 +63,8 @@ class ApiCheck { } if (!Utils::hasFeature(FEATURE_API) && !$hasApiSecret) { - return Response::json('API requires pro plan', 403, $headers); + $error['error'] = ['message'=>'API requires pro plan']; + return Response::json($error, 403, $headers); } else { $key = Auth::check() ? Auth::user()->account->id : $request->getClientIp(); diff --git a/app/Http/Middleware/StartupCheck.php b/app/Http/Middleware/StartupCheck.php index 776974a8946c..c18eb306cc9c 100644 --- a/app/Http/Middleware/StartupCheck.php +++ b/app/Http/Middleware/StartupCheck.php @@ -149,7 +149,8 @@ class StartupCheck $company = Auth::user()->account->company; $company->plan_term = PLAN_TERM_YEARLY; $company->plan_paid = $data; - $company->plan_expires = date_create($data)->modify('+1 year')->format('Y-m-d'); + $date = max(date_create($data), date_create($company->plan_expires)); + $company->plan_expires = $date->modify('+1 year')->format('Y-m-d'); $company->plan = PLAN_WHITE_LABEL; $company->save(); diff --git a/app/Http/Requests/CreateClientRequest.php b/app/Http/Requests/CreateClientRequest.php index 10d5e2496271..4bd296fea662 100644 --- a/app/Http/Requests/CreateClientRequest.php +++ b/app/Http/Requests/CreateClientRequest.php @@ -19,8 +19,14 @@ class CreateClientRequest extends ClientRequest */ public function rules() { - return [ + $rules = [ 'contacts' => 'valid_contacts', ]; + + if ($this->user()->account->client_number_counter) { + $rules['id_number'] = 'unique:clients,id_number,,id,account_id,' . $this->user()->account_id; + } + + return $rules; } } diff --git a/app/Http/Requests/CreateInvoiceAPIRequest.php b/app/Http/Requests/CreateInvoiceAPIRequest.php index 26eeb521d7f9..f5a8d1ac85f6 100644 --- a/app/Http/Requests/CreateInvoiceAPIRequest.php +++ b/app/Http/Requests/CreateInvoiceAPIRequest.php @@ -1,5 +1,7 @@ 'date', ]; + if ($this->user()->account->client_number_counter) { + $clientId = Client::getPrivateId(request()->input('client')['public_id']); + $rules['client.id_number'] = 'unique:clients,id_number,'.$clientId.',id,account_id,' . $this->user()->account_id; + } + return $rules; } } diff --git a/app/Http/Requests/CreateInvoiceRequest.php b/app/Http/Requests/CreateInvoiceRequest.php index 3fc6f66f7f30..68edaa54efb8 100644 --- a/app/Http/Requests/CreateInvoiceRequest.php +++ b/app/Http/Requests/CreateInvoiceRequest.php @@ -1,5 +1,7 @@ 'date', ]; + if ($this->user()->account->client_number_counter) { + $clientId = Client::getPrivateId(request()->input('client')['public_id']); + $rules['client.id_number'] = 'unique:clients,id_number,'.$clientId.',id,account_id,' . $this->user()->account_id; + } + /* There's a problem parsing the dates if (Request::get('is_recurring') && Request::get('start_date') && Request::get('end_date')) { $rules['end_date'] = 'after' . Request::get('start_date'); diff --git a/app/Http/Requests/CreatePaymentAPIRequest.php b/app/Http/Requests/CreatePaymentAPIRequest.php index 4a43bf368b2b..d809c4d3d044 100644 --- a/app/Http/Requests/CreatePaymentAPIRequest.php +++ b/app/Http/Requests/CreatePaymentAPIRequest.php @@ -33,9 +33,8 @@ class CreatePaymentAPIRequest extends PaymentRequest ]; } - $invoice = Invoice::scope($this->invoice_id) + $this->invoice = $invoice = Invoice::scope($this->invoice_id) ->invoices() - ->whereIsPublic(true) ->firstOrFail(); $this->merge([ diff --git a/app/Http/Requests/CreatePaymentRequest.php b/app/Http/Requests/CreatePaymentRequest.php index ff30f0da9d8e..9b45b2486dd7 100644 --- a/app/Http/Requests/CreatePaymentRequest.php +++ b/app/Http/Requests/CreatePaymentRequest.php @@ -22,9 +22,8 @@ class CreatePaymentRequest extends PaymentRequest public function rules() { $input = $this->input(); - $invoice = Invoice::scope($input['invoice']) + $this->invoice = $invoice = Invoice::scope($input['invoice']) ->invoices() - ->whereIsPublic(true) ->firstOrFail(); $rules = [ diff --git a/app/Http/Requests/SaveClientPortalSettings.php b/app/Http/Requests/SaveClientPortalSettings.php new file mode 100644 index 000000000000..3c2485e7d7ef --- /dev/null +++ b/app/Http/Requests/SaveClientPortalSettings.php @@ -0,0 +1,58 @@ +user()->is_admin && $this->user()->isPro(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + $rules = []; + + if ($this->custom_link == 'subdomain' && Utils::isNinja()) { + $rules['subdomain'] = "unique:accounts,subdomain,{$this->user()->account_id},id|valid_subdomain"; + } + + return $rules; + } + + public function sanitize() + { + $input = $this->all(); + + if ($this->client_view_css && Utils::isNinja()) { + $input['client_view_css'] = HTMLUtils::sanitize($this->client_view_css); + } + + if ($this->custom_link == 'subdomain') { + $subdomain = substr(strtolower($input['subdomain']), 0, MAX_SUBDOMAIN_LENGTH); + $input['subdomain'] = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', $subdomain); + $input['iframe_url'] = null; + } else { + $iframeURL = substr(strtolower($input['iframe_url']), 0, MAX_IFRAME_URL_LENGTH); + $iframeURL = preg_replace('/[^a-zA-Z0-9_\-\:\/\.]/', '', $iframeURL); + $input['iframe_url'] = rtrim($iframeURL, '/'); + $input['subdomain'] = null; + } + + $this->replace($input); + + return $this->all(); + } + +} diff --git a/app/Http/Requests/SaveEmailSettings.php b/app/Http/Requests/SaveEmailSettings.php new file mode 100644 index 000000000000..84f8c5baf8d0 --- /dev/null +++ b/app/Http/Requests/SaveEmailSettings.php @@ -0,0 +1,27 @@ +user()->is_admin && $this->user()->isPro(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'bcc_email' => 'email', + ]; + } + +} diff --git a/app/Http/Requests/UpdateClientRequest.php b/app/Http/Requests/UpdateClientRequest.php index c1cdebe1891e..07e9bc665b5c 100644 --- a/app/Http/Requests/UpdateClientRequest.php +++ b/app/Http/Requests/UpdateClientRequest.php @@ -19,8 +19,14 @@ class UpdateClientRequest extends ClientRequest */ public function rules() { - return [ + $rules = [ 'contacts' => 'valid_contacts', ]; + + if ($this->user()->account->client_number_counter) { + $rules['id_number'] = 'unique:clients,id_number,'.$this->entity()->id.',id,account_id,' . $this->user()->account_id; + } + + return $rules; } } diff --git a/app/Http/Requests/UpdateInvoiceAPIRequest.php b/app/Http/Requests/UpdateInvoiceAPIRequest.php index f0432d94e008..fb3f1b42a82b 100644 --- a/app/Http/Requests/UpdateInvoiceAPIRequest.php +++ b/app/Http/Requests/UpdateInvoiceAPIRequest.php @@ -1,5 +1,7 @@ 'date', ]; + if ($this->user()->account->client_number_counter) { + $clientId = Client::getPrivateId(request()->input('client')['public_id']); + $rules['client.id_number'] = 'unique:clients,id_number,'.$clientId.',id,account_id,' . $this->user()->account_id; + } + return $rules; } } diff --git a/app/Http/Requests/UpdateInvoiceRequest.php b/app/Http/Requests/UpdateInvoiceRequest.php index 92e5c236dde2..476fc74c9009 100644 --- a/app/Http/Requests/UpdateInvoiceRequest.php +++ b/app/Http/Requests/UpdateInvoiceRequest.php @@ -1,5 +1,7 @@ 'date', ]; + if ($this->user()->account->client_number_counter) { + $clientId = Client::getPrivateId(request()->input('client')['public_id']); + $rules['client.id_number'] = 'unique:clients,id_number,'.$clientId.',id,account_id,' . $this->user()->account_id; + } + /* There's a problem parsing the dates if (Request::get('is_recurring') && Request::get('start_date') && Request::get('end_date')) { $rules['end_date'] = 'after' . Request::get('start_date'); diff --git a/app/Http/routes.php b/app/Http/routes.php index 3acbbc9e0d5a..cb9acd590c18 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -11,14 +11,6 @@ | */ -//Crypt::decrypt(); -//apc_clear_cache(); -//dd(DB::getQueryLog()); -//dd(Client::getPrivateId(1)); -//dd(new DateTime()); -//dd(App::environment()); -//dd(gethostname()); -//Log::error('test'); // Application setup Route::get('/setup', 'AppController@showSetup'); @@ -28,7 +20,6 @@ Route::get('/update', 'AppController@update'); // Public pages Route::get('/', 'HomeController@showIndex'); -Route::get('/terms', 'HomeController@showTerms'); Route::get('/log_error', 'HomeController@logError'); Route::get('/invoice_now', 'HomeController@invoiceNow'); Route::get('/keep_alive', 'HomeController@keepAlive'); @@ -131,6 +122,7 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('account/get_search_data', ['as' => 'get_search_data', 'uses' => 'AccountController@getSearchData']); Route::get('check_invoice_number/{invoice_id?}', 'InvoiceController@checkInvoiceNumber'); Route::post('save_sidebar_state', 'UserController@saveSidebarState'); + Route::post('contact_us', 'HomeController@contactUs'); Route::get('settings/user_details', 'AccountController@showUserDetails'); Route::post('settings/user_details', 'AccountController@saveUserDetails'); @@ -141,10 +133,11 @@ Route::group(['middleware' => 'auth:user'], function() { Route::get('api/clients', 'ClientController@getDatatable'); Route::get('api/activities/{client_id?}', 'ActivityController@getDatatable'); Route::post('clients/bulk', 'ClientController@bulk'); + Route::get('clients/statement/{client_id}', 'ClientController@statement'); Route::resource('tasks', 'TaskController'); Route::get('api/tasks/{client_id?}', 'TaskController@getDatatable'); - Route::get('tasks/create/{client_id?}', 'TaskController@create'); + Route::get('tasks/create/{client_id?}/{project_id?}', 'TaskController@create'); Route::post('tasks/bulk', 'TaskController@bulk'); Route::get('projects', 'ProjectController@index'); Route::get('api/projects', 'ProjectController@getDatatable'); @@ -211,7 +204,7 @@ Route::group(['middleware' => 'auth:user'], function() { // Expense Route::resource('expenses', 'ExpenseController'); - Route::get('expenses/create/{vendor_id?}/{client_id?}', 'ExpenseController@create'); + Route::get('expenses/create/{vendor_id?}/{client_id?}/{category_id?}', 'ExpenseController@create'); Route::get('api/expenses', 'ExpenseController@getDatatable'); Route::get('api/expenses/{id}', 'ExpenseController@getDatatableVendor'); Route::post('expenses/bulk', 'ExpenseController@bulk'); @@ -227,6 +220,7 @@ Route::group(['middleware' => 'auth:user'], function() { Route::post('bluevine/signup', 'BlueVineController@signup'); Route::get('bluevine/hide_message', 'BlueVineController@hideMessage'); Route::get('bluevine/completed', 'BlueVineController@handleCompleted'); + Route::get('white_label/hide_message', 'NinjaController@hideWhiteLabelMessage'); }); Route::group([ @@ -237,8 +231,6 @@ Route::group([ Route::resource('users', 'UserController'); Route::post('users/bulk', 'UserController@bulk'); Route::get('send_confirmation/{user_id}', 'UserController@sendConfirmation'); - Route::get('start_trial/{plan}', 'AccountController@startTrial') - ->where(['plan'=>'pro']); 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'); @@ -252,10 +244,12 @@ Route::group([ Route::post('tax_rates/bulk', 'TaxRateController@bulk'); Route::get('settings/email_preview', 'AccountController@previewEmail'); + Route::post('settings/client_portal', 'AccountController@saveClientPortalSettings'); + Route::post('settings/email_settings', 'AccountController@saveEmailSettings'); Route::get('company/{section}/{subSection?}', 'AccountController@redirectLegacy'); Route::get('settings/data_visualizations', 'ReportController@d3'); - Route::get('settings/reports', 'ReportController@showReports'); - Route::post('settings/reports', 'ReportController@showReports'); + Route::get('reports', 'ReportController@showReports'); + Route::post('reports', 'ReportController@showReports'); Route::post('settings/change_plan', 'AccountController@changePlan'); Route::post('settings/cancel_account', 'AccountController@cancelAccount'); @@ -359,573 +353,6 @@ Route::get('/comments/feed', function() { return Redirect::to(NINJA_WEB_URL.'/comments/feed', 301); }); -if (!defined('CONTACT_EMAIL')) { - define('CONTACT_EMAIL', config('mail.from.address')); - define('CONTACT_NAME', config('mail.from.name')); - define('SITE_URL', config('app.url')); - - define('ENV_DEVELOPMENT', 'local'); - define('ENV_STAGING', 'staging'); - - define('RECENTLY_VIEWED', 'recent_history'); - - define('ENTITY_CLIENT', 'client'); - define('ENTITY_CONTACT', 'contact'); - define('ENTITY_INVOICE', 'invoice'); - define('ENTITY_DOCUMENT', 'document'); - define('ENTITY_INVOICE_ITEM', 'invoice_item'); - define('ENTITY_INVITATION', 'invitation'); - define('ENTITY_RECURRING_INVOICE', 'recurring_invoice'); - define('ENTITY_PAYMENT', 'payment'); - define('ENTITY_CREDIT', 'credit'); - define('ENTITY_QUOTE', 'quote'); - define('ENTITY_TASK', 'task'); - define('ENTITY_ACCOUNT_GATEWAY', 'account_gateway'); - define('ENTITY_USER', 'user'); - define('ENTITY_TOKEN', 'token'); - define('ENTITY_TAX_RATE', 'tax_rate'); - define('ENTITY_PRODUCT', 'product'); - define('ENTITY_ACTIVITY', 'activity'); - define('ENTITY_VENDOR', 'vendor'); - define('ENTITY_VENDOR_ACTIVITY', 'vendor_activity'); - define('ENTITY_EXPENSE', 'expense'); - define('ENTITY_PAYMENT_TERM', 'payment_term'); - define('ENTITY_EXPENSE_ACTIVITY', 'expense_activity'); - define('ENTITY_BANK_ACCOUNT', 'bank_account'); - define('ENTITY_BANK_SUBACCOUNT', 'bank_subaccount'); - define('ENTITY_EXPENSE_CATEGORY', 'expense_category'); - define('ENTITY_PROJECT', 'project'); - - define('INVOICE_TYPE_STANDARD', 1); - define('INVOICE_TYPE_QUOTE', 2); - - define('PERSON_CONTACT', 'contact'); - define('PERSON_USER', 'user'); - define('PERSON_VENDOR_CONTACT','vendorcontact'); - - define('BASIC_SETTINGS', 'basic_settings'); - define('ADVANCED_SETTINGS', 'advanced_settings'); - - define('ACCOUNT_COMPANY_DETAILS', 'company_details'); - define('ACCOUNT_USER_DETAILS', 'user_details'); - 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'); - define('ACCOUNT_MAP', 'import_map'); - define('ACCOUNT_EXPORT', 'export'); - define('ACCOUNT_TAX_RATES', 'tax_rates'); - define('ACCOUNT_PRODUCTS', 'products'); - define('ACCOUNT_ADVANCED_SETTINGS', 'advanced_settings'); - define('ACCOUNT_INVOICE_SETTINGS', 'invoice_settings'); - define('ACCOUNT_INVOICE_DESIGN', 'invoice_design'); - define('ACCOUNT_CLIENT_PORTAL', 'client_portal'); - define('ACCOUNT_EMAIL_SETTINGS', 'email_settings'); - define('ACCOUNT_REPORTS', 'reports'); - define('ACCOUNT_USER_MANAGEMENT', 'user_management'); - define('ACCOUNT_DATA_VISUALIZATIONS', 'data_visualizations'); - define('ACCOUNT_TEMPLATES_AND_REMINDERS', 'templates_and_reminders'); - define('ACCOUNT_API_TOKENS', 'api_tokens'); - define('ACCOUNT_CUSTOMIZE_DESIGN', 'customize_design'); - define('ACCOUNT_SYSTEM_SETTINGS', 'system_settings'); - define('ACCOUNT_PAYMENT_TERMS','payment_terms'); - - define('ACTION_RESTORE', 'restore'); - define('ACTION_ARCHIVE', 'archive'); - define('ACTION_CLONE', 'clone'); - define('ACTION_CONVERT', 'convert'); - define('ACTION_DELETE', 'delete'); - - define('ACTIVITY_TYPE_CREATE_CLIENT', 1); - define('ACTIVITY_TYPE_ARCHIVE_CLIENT', 2); - define('ACTIVITY_TYPE_DELETE_CLIENT', 3); - define('ACTIVITY_TYPE_CREATE_INVOICE', 4); - define('ACTIVITY_TYPE_UPDATE_INVOICE', 5); - define('ACTIVITY_TYPE_EMAIL_INVOICE', 6); - define('ACTIVITY_TYPE_VIEW_INVOICE', 7); - define('ACTIVITY_TYPE_ARCHIVE_INVOICE', 8); - define('ACTIVITY_TYPE_DELETE_INVOICE', 9); - define('ACTIVITY_TYPE_CREATE_PAYMENT', 10); - //define('ACTIVITY_TYPE_UPDATE_PAYMENT', 11); - define('ACTIVITY_TYPE_ARCHIVE_PAYMENT', 12); - define('ACTIVITY_TYPE_DELETE_PAYMENT', 13); - define('ACTIVITY_TYPE_CREATE_CREDIT', 14); - //define('ACTIVITY_TYPE_UPDATE_CREDIT', 15); - define('ACTIVITY_TYPE_ARCHIVE_CREDIT', 16); - define('ACTIVITY_TYPE_DELETE_CREDIT', 17); - define('ACTIVITY_TYPE_CREATE_QUOTE', 18); - define('ACTIVITY_TYPE_UPDATE_QUOTE', 19); - define('ACTIVITY_TYPE_EMAIL_QUOTE', 20); - define('ACTIVITY_TYPE_VIEW_QUOTE', 21); - define('ACTIVITY_TYPE_ARCHIVE_QUOTE', 22); - define('ACTIVITY_TYPE_DELETE_QUOTE', 23); - define('ACTIVITY_TYPE_RESTORE_QUOTE', 24); - define('ACTIVITY_TYPE_RESTORE_INVOICE', 25); - define('ACTIVITY_TYPE_RESTORE_CLIENT', 26); - define('ACTIVITY_TYPE_RESTORE_PAYMENT', 27); - define('ACTIVITY_TYPE_RESTORE_CREDIT', 28); - define('ACTIVITY_TYPE_APPROVE_QUOTE', 29); - define('ACTIVITY_TYPE_CREATE_VENDOR', 30); - define('ACTIVITY_TYPE_ARCHIVE_VENDOR', 31); - define('ACTIVITY_TYPE_DELETE_VENDOR', 32); - define('ACTIVITY_TYPE_RESTORE_VENDOR', 33); - define('ACTIVITY_TYPE_CREATE_EXPENSE', 34); - define('ACTIVITY_TYPE_ARCHIVE_EXPENSE', 35); - define('ACTIVITY_TYPE_DELETE_EXPENSE', 36); - define('ACTIVITY_TYPE_RESTORE_EXPENSE', 37); - define('ACTIVITY_TYPE_VOIDED_PAYMENT', 39); - define('ACTIVITY_TYPE_REFUNDED_PAYMENT', 40); - define('ACTIVITY_TYPE_FAILED_PAYMENT', 41); - define('ACTIVITY_TYPE_CREATE_TASK', 42); - define('ACTIVITY_TYPE_UPDATE_TASK', 43); - define('ACTIVITY_TYPE_ARCHIVE_TASK', 44); - define('ACTIVITY_TYPE_DELETE_TASK', 45); - define('ACTIVITY_TYPE_RESTORE_TASK', 46); - define('ACTIVITY_TYPE_UPDATE_EXPENSE', 47); - - - define('DEFAULT_INVOICE_NUMBER', '0001'); - define('RECENTLY_VIEWED_LIMIT', 20); - define('LOGGED_ERROR_LIMIT', 100); - define('RANDOM_KEY_LENGTH', 32); - define('MAX_NUM_USERS', 20); - define('MAX_IMPORT_ROWS', 500); - define('MAX_SUBDOMAIN_LENGTH', 30); - define('MAX_IFRAME_URL_LENGTH', 250); - define('MAX_LOGO_FILE_SIZE', 200); // KB - define('MAX_FAILED_LOGINS', 10); - define('MAX_INVOICE_ITEMS', env('MAX_INVOICE_ITEMS', 100)); - define('MAX_DOCUMENT_SIZE', env('MAX_DOCUMENT_SIZE', 10000));// KB - define('MAX_EMAIL_DOCUMENTS_SIZE', env('MAX_EMAIL_DOCUMENTS_SIZE', 10000));// Total KB - define('MAX_ZIP_DOCUMENTS_SIZE', env('MAX_EMAIL_DOCUMENTS_SIZE', 30000));// Total KB (uncompressed) - define('DOCUMENT_PREVIEW_SIZE', env('DOCUMENT_PREVIEW_SIZE', 300));// pixels - define('DEFAULT_FONT_SIZE', 9); - define('DEFAULT_HEADER_FONT', 1);// Roboto - define('DEFAULT_BODY_FONT', 1);// Roboto - define('DEFAULT_SEND_RECURRING_HOUR', 8); - - define('IMPORT_CSV', 'CSV'); - define('IMPORT_JSON', 'JSON'); - define('IMPORT_FRESHBOOKS', 'FreshBooks'); - define('IMPORT_WAVE', 'Wave'); - define('IMPORT_RONIN', 'Ronin'); - define('IMPORT_HIVEAGE', 'Hiveage'); - define('IMPORT_ZOHO', 'Zoho'); - define('IMPORT_NUTCACHE', 'Nutcache'); - define('IMPORT_INVOICEABLE', 'Invoiceable'); - define('IMPORT_HARVEST', 'Harvest'); - - define('MAX_NUM_CLIENTS', 100); - define('MAX_NUM_CLIENTS_PRO', 20000); - define('MAX_NUM_CLIENTS_LEGACY', 500); - define('MAX_INVOICE_AMOUNT', 1000000000); - define('LEGACY_CUTOFF', 57800); - define('ERROR_DELAY', 3); - - define('MAX_NUM_VENDORS', 100); - define('MAX_NUM_VENDORS_PRO', 20000); - - define('STATUS_ACTIVE', 'active'); - define('STATUS_ARCHIVED', 'archived'); - define('STATUS_DELETED', 'deleted'); - - define('INVOICE_STATUS_DRAFT', 1); - define('INVOICE_STATUS_SENT', 2); - define('INVOICE_STATUS_VIEWED', 3); - define('INVOICE_STATUS_APPROVED', 4); - define('INVOICE_STATUS_PARTIAL', 5); - define('INVOICE_STATUS_PAID', 6); - define('INVOICE_STATUS_OVERDUE', 7); - - define('PAYMENT_STATUS_PENDING', 1); - define('PAYMENT_STATUS_VOIDED', 2); - define('PAYMENT_STATUS_FAILED', 3); - define('PAYMENT_STATUS_COMPLETED', 4); - define('PAYMENT_STATUS_PARTIALLY_REFUNDED', 5); - define('PAYMENT_STATUS_REFUNDED', 6); - - define('TASK_STATUS_LOGGED', 1); - define('TASK_STATUS_RUNNING', 2); - define('TASK_STATUS_INVOICED', 3); - define('TASK_STATUS_PAID', 4); - - define('EXPENSE_STATUS_LOGGED', 1); - define('EXPENSE_STATUS_INVOICED', 2); - define('EXPENSE_STATUS_PAID', 3); - - define('CUSTOM_DESIGN', 11); - - define('FREQUENCY_WEEKLY', 1); - define('FREQUENCY_TWO_WEEKS', 2); - define('FREQUENCY_FOUR_WEEKS', 3); - define('FREQUENCY_MONTHLY', 4); - define('FREQUENCY_THREE_MONTHS', 5); - define('FREQUENCY_SIX_MONTHS', 6); - define('FREQUENCY_ANNUALLY', 7); - - define('SESSION_TIMEZONE', 'timezone'); - define('SESSION_CURRENCY', 'currency'); - define('SESSION_CURRENCY_DECORATOR', 'currency_decorator'); - define('SESSION_DATE_FORMAT', 'dateFormat'); - define('SESSION_DATE_PICKER_FORMAT', 'datePickerFormat'); - define('SESSION_DATETIME_FORMAT', 'datetimeFormat'); - define('SESSION_COUNTER', 'sessionCounter'); - define('SESSION_LOCALE', 'sessionLocale'); - define('SESSION_USER_ACCOUNTS', 'userAccounts'); - define('SESSION_REFERRAL_CODE', 'referralCode'); - define('SESSION_LEFT_SIDEBAR', 'showLeftSidebar'); - define('SESSION_RIGHT_SIDEBAR', 'showRightSidebar'); - - define('SESSION_LAST_REQUEST_PAGE', 'SESSION_LAST_REQUEST_PAGE'); - define('SESSION_LAST_REQUEST_TIME', 'SESSION_LAST_REQUEST_TIME'); - - define('CURRENCY_DOLLAR', 1); - define('CURRENCY_EURO', 3); - - define('DEFAULT_TIMEZONE', 'US/Eastern'); - define('DEFAULT_COUNTRY', 840); // United Stated - define('DEFAULT_CURRENCY', CURRENCY_DOLLAR); - define('DEFAULT_LANGUAGE', 1); // English - define('DEFAULT_DATE_FORMAT', 'M j, Y'); - define('DEFAULT_DATE_PICKER_FORMAT', 'M d, yyyy'); - define('DEFAULT_DATETIME_FORMAT', 'F j, Y g:i a'); - define('DEFAULT_DATETIME_MOMENT_FORMAT', 'MMM D, YYYY h:mm:ss a'); - define('DEFAULT_LOCALE', 'en'); - define('DEFAULT_MAP_ZOOM', 10); - - define('RESULT_SUCCESS', 'success'); - define('RESULT_FAILURE', 'failure'); - - - define('PAYMENT_LIBRARY_OMNIPAY', 1); - define('PAYMENT_LIBRARY_PHP_PAYMENTS', 2); - - define('GATEWAY_AUTHORIZE_NET', 1); - define('GATEWAY_EWAY', 4); - define('GATEWAY_MOLLIE', 9); - define('GATEWAY_PAYFAST', 13); - define('GATEWAY_PAYPAL_EXPRESS', 17); - define('GATEWAY_PAYPAL_PRO', 18); - define('GATEWAY_SAGE_PAY_DIRECT', 20); - define('GATEWAY_SAGE_PAY_SERVER', 21); - define('GATEWAY_STRIPE', 23); - define('GATEWAY_GOCARDLESS', 6); - define('GATEWAY_TWO_CHECKOUT', 27); - define('GATEWAY_BEANSTREAM', 29); - define('GATEWAY_PSIGATE', 30); - define('GATEWAY_MOOLAH', 31); - define('GATEWAY_BITPAY', 42); - define('GATEWAY_DWOLLA', 43); - define('GATEWAY_CHECKOUT_COM', 47); - define('GATEWAY_CYBERSOURCE', 49); - define('GATEWAY_WEPAY', 60); - define('GATEWAY_BRAINTREE', 61); - define('GATEWAY_CUSTOM', 62); - - // The customer exists, but only as a local concept - // The remote gateway doesn't understand the concept of customers - define('CUSTOMER_REFERENCE_LOCAL', 'local'); - - define('EVENT_CREATE_CLIENT', 1); - define('EVENT_CREATE_INVOICE', 2); - define('EVENT_CREATE_QUOTE', 3); - define('EVENT_CREATE_PAYMENT', 4); - define('EVENT_CREATE_VENDOR',5); - - define('REQUESTED_PRO_PLAN', 'REQUESTED_PRO_PLAN'); - define('DEMO_ACCOUNT_ID', 'DEMO_ACCOUNT_ID'); - define('PREV_USER_ID', 'PREV_USER_ID'); - define('NINJA_ACCOUNT_KEY', 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx79h'); - define('NINJA_GATEWAY_ID', GATEWAY_STRIPE); - define('NINJA_GATEWAY_CONFIG', 'NINJA_GATEWAY_CONFIG'); - define('NINJA_WEB_URL', env('NINJA_WEB_URL', 'https://www.invoiceninja.com')); - define('NINJA_APP_URL', env('NINJA_APP_URL', 'https://app.invoiceninja.com')); - define('NINJA_DOCS_URL', env('NINJA_DOCS_URL', 'http://docs.invoiceninja.com/en/latest')); - define('NINJA_DATE', '2000-01-01'); - define('NINJA_VERSION', '2.9.5' . env('NINJA_VERSION_SUFFIX')); - - define('SOCIAL_LINK_FACEBOOK', env('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja')); - define('SOCIAL_LINK_TWITTER', env('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja')); - define('SOCIAL_LINK_GITHUB', env('SOCIAL_LINK_GITHUB', 'https://github.com/invoiceninja/invoiceninja/')); - - define('NINJA_FORUM_URL', env('NINJA_FORUM_URL', 'https://www.invoiceninja.com/forums/forum/support/')); - define('NINJA_CONTACT_URL', env('NINJA_CONTACT_URL', 'https://www.invoiceninja.com/contact/')); - define('NINJA_FROM_EMAIL', env('NINJA_FROM_EMAIL', 'maildelivery@invoiceninja.com')); - define('RELEASES_URL', env('RELEASES_URL', 'https://trello.com/b/63BbiVVe/invoice-ninja')); - define('ZAPIER_URL', env('ZAPIER_URL', 'https://zapier.com/zapbook/invoice-ninja')); - define('OUTDATE_BROWSER_URL', env('OUTDATE_BROWSER_URL', 'http://browsehappy.com/')); - define('PDFMAKE_DOCS', env('PDFMAKE_DOCS', 'http://pdfmake.org/playground.html')); - define('PHANTOMJS_CLOUD', env('PHANTOMJS_CLOUD', 'http://api.phantomjscloud.com/api/browser/v2/')); - define('PHP_DATE_FORMATS', env('PHP_DATE_FORMATS', 'http://php.net/manual/en/function.date.php')); - define('REFERRAL_PROGRAM_URL', env('REFERRAL_PROGRAM_URL', 'https://www.invoiceninja.com/referral-program/')); - define('EMAIL_MARKUP_URL', env('EMAIL_MARKUP_URL', 'https://developers.google.com/gmail/markup')); - define('OFX_HOME_URL', env('OFX_HOME_URL', 'http://www.ofxhome.com/index.php/home/directory/all')); - define('GOOGLE_ANALYITCS_URL', env('GOOGLE_ANALYITCS_URL', 'https://www.google-analytics.com/collect')); - define('TRANSIFEX_URL', env('TRANSIFEX_URL', 'https://www.transifex.com/invoice-ninja/invoice-ninja')); - define('CHROME_PDF_HELP_URL', 'https://support.google.com/chrome/answer/6213030?hl=en'); - define('FIREFOX_PDF_HELP_URL', 'https://support.mozilla.org/en-US/kb/view-pdf-files-firefox'); - - define('MSBOT_LOGIN_URL', 'https://login.microsoftonline.com/common/oauth2/v2.0/token'); - define('MSBOT_LUIS_URL', 'https://api.projectoxford.ai/luis/v1/application'); - define('SKYPE_API_URL', 'https://apis.skype.com/v3'); - define('MSBOT_STATE_URL', 'https://state.botframework.com/v3'); - - define('BLANK_IMAGE', 'data:image/png;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='); - - define('COUNT_FREE_DESIGNS', 4); - define('COUNT_FREE_DESIGNS_SELF_HOST', 5); // include the custom design - define('PRODUCT_ONE_CLICK_INSTALL', 1); - define('PRODUCT_INVOICE_DESIGNS', 2); - define('PRODUCT_WHITE_LABEL', 3); - define('PRODUCT_SELF_HOST', 4); - define('WHITE_LABEL_AFFILIATE_KEY', '92D2J5'); - define('INVOICE_DESIGNS_AFFILIATE_KEY', 'T3RS74'); - define('SELF_HOST_AFFILIATE_KEY', '8S69AD'); - - define('PLAN_PRICE_PRO_MONTHLY', env('PLAN_PRICE_PRO_MONTHLY', 8)); - define('PLAN_PRICE_ENTERPRISE_MONTHLY_2', env('PLAN_PRICE_ENTERPRISE_MONTHLY_2', 12)); - define('PLAN_PRICE_ENTERPRISE_MONTHLY_5', env('PLAN_PRICE_ENTERPRISE_MONTHLY_5', 18)); - define('PLAN_PRICE_ENTERPRISE_MONTHLY_10', env('PLAN_PRICE_ENTERPRISE_MONTHLY_10', 24)); - define('WHITE_LABEL_PRICE', env('WHITE_LABEL_PRICE', 20)); - define('INVOICE_DESIGNS_PRICE', env('INVOICE_DESIGNS_PRICE', 10)); - - define('USER_TYPE_SELF_HOST', 'SELF_HOST'); - define('USER_TYPE_CLOUD_HOST', 'CLOUD_HOST'); - define('NEW_VERSION_AVAILABLE', 'NEW_VERSION_AVAILABLE'); - - define('TEST_USERNAME', 'user@example.com'); - define('TEST_PASSWORD', 'password'); - define('API_SECRET', 'API_SECRET'); - define('DEFAULT_API_PAGE_SIZE', 15); - define('MAX_API_PAGE_SIZE', 500); - - define('IOS_PUSH_CERTIFICATE', env('IOS_PUSH_CERTIFICATE', '')); - - define('TOKEN_BILLING_DISABLED', 1); - define('TOKEN_BILLING_OPT_IN', 2); - define('TOKEN_BILLING_OPT_OUT', 3); - define('TOKEN_BILLING_ALWAYS', 4); - - define('PAYMENT_TYPE_CREDIT', 1); - define('PAYMENT_TYPE_ACH', 5); - define('PAYMENT_TYPE_VISA', 6); - define('PAYMENT_TYPE_MASTERCARD', 7); - define('PAYMENT_TYPE_AMERICAN_EXPRESS', 8); - define('PAYMENT_TYPE_DISCOVER', 9); - define('PAYMENT_TYPE_DINERS', 10); - define('PAYMENT_TYPE_EUROCARD', 11); - define('PAYMENT_TYPE_NOVA', 12); - define('PAYMENT_TYPE_CREDIT_CARD_OTHER', 13); - define('PAYMENT_TYPE_PAYPAL', 14); - define('PAYMENT_TYPE_CARTE_BLANCHE', 17); - define('PAYMENT_TYPE_UNIONPAY', 18); - define('PAYMENT_TYPE_JCB', 19); - define('PAYMENT_TYPE_LASER', 20); - define('PAYMENT_TYPE_MAESTRO', 21); - define('PAYMENT_TYPE_SOLO', 22); - define('PAYMENT_TYPE_SWITCH', 23); - - define('PAYMENT_METHOD_STATUS_NEW', 'new'); - define('PAYMENT_METHOD_STATUS_VERIFICATION_FAILED', 'verification_failed'); - define('PAYMENT_METHOD_STATUS_VERIFIED', 'verified'); - - define('GATEWAY_TYPE_CREDIT_CARD', 1); - define('GATEWAY_TYPE_BANK_TRANSFER', 2); - define('GATEWAY_TYPE_PAYPAL', 3); - define('GATEWAY_TYPE_BITCOIN', 4); - define('GATEWAY_TYPE_DWOLLA', 5); - define('GATEWAY_TYPE_CUSTOM', 6); - define('GATEWAY_TYPE_TOKEN', 'token'); - - define('REMINDER1', 'reminder1'); - define('REMINDER2', 'reminder2'); - define('REMINDER3', 'reminder3'); - - define('REMINDER_DIRECTION_AFTER', 1); - define('REMINDER_DIRECTION_BEFORE', 2); - - define('REMINDER_FIELD_DUE_DATE', 1); - define('REMINDER_FIELD_INVOICE_DATE', 2); - - define('FILTER_INVOICE_DATE', 'invoice_date'); - define('FILTER_PAYMENT_DATE', 'payment_date'); - - define('SOCIAL_GOOGLE', 'Google'); - define('SOCIAL_FACEBOOK', 'Facebook'); - define('SOCIAL_GITHUB', 'GitHub'); - define('SOCIAL_LINKEDIN', 'LinkedIn'); - - define('USER_STATE_ACTIVE', 'active'); - define('USER_STATE_PENDING', 'pending'); - define('USER_STATE_DISABLED', 'disabled'); - define('USER_STATE_ADMIN', 'admin'); - define('USER_STATE_OWNER', 'owner'); - - define('API_SERIALIZER_ARRAY', 'array'); - define('API_SERIALIZER_JSON', 'json'); - - define('EMAIL_DESIGN_PLAIN', 1); - define('EMAIL_DESIGN_LIGHT', 2); - define('EMAIL_DESIGN_DARK', 3); - - define('BANK_LIBRARY_OFX', 1); - - define('CURRENCY_DECORATOR_CODE', 'code'); - define('CURRENCY_DECORATOR_SYMBOL', 'symbol'); - define('CURRENCY_DECORATOR_NONE', 'none'); - - define('RESELLER_REVENUE_SHARE', 'A'); - define('RESELLER_LIMITED_USERS', 'B'); - - define('AUTO_BILL_OFF', 1); - define('AUTO_BILL_OPT_IN', 2); - define('AUTO_BILL_OPT_OUT', 3); - define('AUTO_BILL_ALWAYS', 4); - - // 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_TASKS', 'tasks'); - define('FEATURE_EXPENSES', 'expenses'); - define('FEATURE_REPORTS', 'reports'); - define('FEATURE_BUY_NOW_BUTTONS', 'buy_now_buttons'); - 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-06-04'); - define('EXTRAS_GRANDFATHER_COMPANY_ID', 35089); - - // WePay - define('WEPAY_PRODUCTION', 'production'); - define('WEPAY_STAGE', 'stage'); - define('WEPAY_CLIENT_ID', env('WEPAY_CLIENT_ID')); - define('WEPAY_CLIENT_SECRET', env('WEPAY_CLIENT_SECRET')); - define('WEPAY_AUTO_UPDATE', env('WEPAY_AUTO_UPDATE', false)); - define('WEPAY_ENVIRONMENT', env('WEPAY_ENVIRONMENT', WEPAY_PRODUCTION)); - define('WEPAY_ENABLE_CANADA', env('WEPAY_ENABLE_CANADA', false)); - define('WEPAY_THEME', env('WEPAY_THEME','{"name":"Invoice Ninja","primary_color":"0b4d78","secondary_color":"0b4d78","background_color":"f8f8f8","button_color":"33b753"}')); - - define('SKYPE_CARD_RECEIPT', 'message/card.receipt'); - define('SKYPE_CARD_CAROUSEL', 'message/card.carousel'); - define('SKYPE_CARD_HERO', ''); - - define('BOT_STATE_GET_EMAIL', 'get_email'); - define('BOT_STATE_GET_CODE', 'get_code'); - define('BOT_STATE_READY', 'ready'); - define('SIMILAR_MIN_THRESHOLD', 50); - - // https://docs.botframework.com/en-us/csharp/builder/sdkreference/attachments.html - define('SKYPE_BUTTON_OPEN_URL', 'openUrl'); - define('SKYPE_BUTTON_IM_BACK', 'imBack'); - define('SKYPE_BUTTON_POST_BACK', 'postBack'); - define('SKYPE_BUTTON_CALL', 'call'); // "tel:123123123123" - define('SKYPE_BUTTON_PLAY_AUDIO', 'playAudio'); - define('SKYPE_BUTTON_PLAY_VIDEO', 'playVideo'); - define('SKYPE_BUTTON_SHOW_IMAGE', 'showImage'); - define('SKYPE_BUTTON_DOWNLOAD_FILE', 'downloadFile'); - - define('INVOICE_FIELDS_CLIENT', 'client_fields'); - define('INVOICE_FIELDS_INVOICE', 'invoice_fields'); - define('INVOICE_FIELDS_ACCOUNT', 'account_fields'); - - $creditCards = [ - 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], - 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], - 4 => ['card' => 'images/credit_cards/Test-AmericanExpress-Icon.png', 'text' => 'American Express'], - 8 => ['card' => 'images/credit_cards/Test-Diners-Icon.png', 'text' => 'Diners'], - 16 => ['card' => 'images/credit_cards/Test-Discover-Icon.png', 'text' => 'Discover'] - ]; - define('CREDIT_CARDS', serialize($creditCards)); - - $cachedTables = [ - 'currencies' => 'App\Models\Currency', - 'sizes' => 'App\Models\Size', - 'industries' => 'App\Models\Industry', - 'timezones' => 'App\Models\Timezone', - 'dateFormats' => 'App\Models\DateFormat', - 'datetimeFormats' => 'App\Models\DatetimeFormat', - 'languages' => 'App\Models\Language', - 'paymentTerms' => 'App\Models\PaymentTerm', - 'paymentTypes' => 'App\Models\PaymentType', - 'countries' => 'App\Models\Country', - 'invoiceDesigns' => 'App\Models\InvoiceDesign', - 'invoiceStatus' => 'App\Models\InvoiceStatus', - 'frequencies' => 'App\Models\Frequency', - 'gateways' => 'App\Models\Gateway', - 'gatewayTypes' => 'App\Models\GatewayType', - 'fonts' => 'App\Models\Font', - 'banks' => 'App\Models\Bank', - ]; - define('CACHED_TABLES', serialize($cachedTables)); - - function uctrans($text) - { - return ucwords(trans($text)); - } - - // optional trans: only return the string if it's translated - function otrans($text) - { - $locale = Session::get(SESSION_LOCALE); - - if ($locale == 'en') { - return trans($text); - } else { - $string = trans($text); - $english = trans($text, [], 'en'); - return $string != $english ? $string : ''; - } - } - - // include modules in translations - function mtrans($entityType, $text = false) - { - if ( ! $text) { - $text = $entityType; - } - - if ( ! Utils::isNinjaProd() && $module = Module::find($entityType)) { - return trans("{$module->getLowerName()}::texts.{$text}"); - } else { - return trans("texts.{$text}"); - } - } -} - - /* if (Utils::isNinjaDev()) { @@ -934,3 +361,6 @@ if (Utils::isNinjaDev()) Auth::loginUsingId(1); } */ + +// Include static app constants +require_once app_path() . '/Constants.php'; diff --git a/app/Jobs/Job.php b/app/Jobs/Job.php new file mode 100644 index 000000000000..cd1bae1ceb3d --- /dev/null +++ b/app/Jobs/Job.php @@ -0,0 +1,47 @@ +sendTo( + config('queue.failed.notify_email'), + config('mail.from.address'), + config('mail.from.name'), + config('queue.failed.notify_subject', trans('texts.job_failed', ['name'=>$this->jobName])), + 'job_failed', + [ + 'name' => $this->jobName, + ] + ); + } + + $logger->error( + trans('texts.job_failed', ['name' => $this->jobName]) + ); + } + */ +} diff --git a/app/Jobs/SendInvoiceEmail.php b/app/Jobs/SendInvoiceEmail.php new file mode 100644 index 000000000000..8194a1c95684 --- /dev/null +++ b/app/Jobs/SendInvoiceEmail.php @@ -0,0 +1,73 @@ +invoice = $invoice; + $this->reminder = $reminder; + $this->pdfString = $pdfString; + } + + /** + * Execute the job. + * + * @param ContactMailer $mailer + */ + public function handle(ContactMailer $mailer) + { + $mailer->sendInvoice($this->invoice, $this->reminder, $this->pdfString); + } + + /** + * Handle a job failure. + * + * @param ContactMailer $mailer + * @param Logger $logger + */ + /* + public function failed(ContactMailer $mailer, Logger $logger) + { + $this->jobName = $this->job->getName(); + + parent::failed($mailer, $logger); + } + */ +} diff --git a/app/Jobs/SendNotificationEmail.php b/app/Jobs/SendNotificationEmail.php new file mode 100644 index 000000000000..32eaa6c42de0 --- /dev/null +++ b/app/Jobs/SendNotificationEmail.php @@ -0,0 +1,66 @@ +user = $user; + $this->invoice = $invoice; + $this->type = $type; + $this->payment = $payment; + } + + /** + * Execute the job. + * + * @param ContactMailer $mailer + */ + public function handle(UserMailer $userMailer) + { + $userMailer->sendNotification($this->user, $this->invoice, $this->type, $this->payment); + } + +} diff --git a/app/Jobs/SendPaymentEmail.php b/app/Jobs/SendPaymentEmail.php new file mode 100644 index 000000000000..c27c0f5f44ad --- /dev/null +++ b/app/Jobs/SendPaymentEmail.php @@ -0,0 +1,47 @@ +payment = $payment; + } + + /** + * Execute the job. + * + * @param ContactMailer $mailer + */ + public function handle(ContactMailer $contactMailer) + { + $contactMailer->sendPaymentConfirmation($this->payment); + } + + +} diff --git a/app/Jobs/SendPushNotification.php b/app/Jobs/SendPushNotification.php new file mode 100644 index 000000000000..a9774e1f084c --- /dev/null +++ b/app/Jobs/SendPushNotification.php @@ -0,0 +1,52 @@ +invoice = $invoice; + $this->type = $type; + } + + /** + * Execute the job. + * + * @param PushService $pushService + */ + public function handle(PushService $pushService) + { + $pushService->sendNotification($this->invoice, $this->type); + } + +} diff --git a/app/Libraries/CurlUtils.php b/app/Libraries/CurlUtils.php index d6ce9d0e37c0..4a11a90939dd 100644 --- a/app/Libraries/CurlUtils.php +++ b/app/Libraries/CurlUtils.php @@ -1,5 +1,7 @@ getEngine()->setPath($path); + + $request = $client->getMessageFactory()->createRequest($url, $method); + $response = $client->getMessageFactory()->createResponse(); + + // Send the request + $client->send($request, $response); + + if ($response->getStatus() === 200) { + return $response->getContent(); + } else { + //$response->getStatus(); + return false; + } + + } } diff --git a/app/Libraries/HTMLUtils.php b/app/Libraries/HTMLUtils.php new file mode 100644 index 000000000000..cb0ce92c02ec --- /dev/null +++ b/app/Libraries/HTMLUtils.php @@ -0,0 +1,37 @@ + + // + + // Create a new configuration object + $config = HTMLPurifier_Config::createDefault(); + $config->set('Filter.ExtractStyleBlocks', true); + $config->set('CSS.AllowImportant', true); + $config->set('CSS.AllowTricky', true); + $config->set('CSS.Trusted', true); + + // Create a new purifier instance + $purifier = new HTMLPurifier($config); + + // Wrap our CSS in style tags and pass to purifier. + // we're not actually interested in the html response though + $purifier->purify(''); + + // The "style" blocks are stored seperately + $css = $purifier->context->get('StyleBlocks'); + + // Get the first style block + return count($css) ? $css[0] : ''; + } +} diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index c869cce10d3f..12028c660eb1 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -67,6 +67,11 @@ class Utils return self::isNinjaProd() || self::isNinjaDev(); } + public static function isSelfHost() + { + return ! static::isNinjaProd(); + } + public static function isNinjaProd() { if (Utils::isReseller()) { @@ -97,13 +102,21 @@ class Utils public static function isWhiteLabel() { - if (Utils::isNinjaProd()) { - return false; + $account = false; + + if (Utils::isNinja()) { + if (Auth::check()) { + $account = Auth::user()->account; + } elseif ($contactKey = session('contact_key')) { + if ($contact = \App\Models\Contact::whereContactKey($contactKey)->first()) { + $account = $contact->account; + } + } + } else { + $account = \App\Models\Account::first(); } - $account = \App\Models\Account::first(); - - return $account && $account->hasFeature(FEATURE_WHITE_LABEL); + return $account ? $account->hasFeature(FEATURE_WHITE_LABEL) : false; } public static function getResllerType() @@ -111,6 +124,11 @@ class Utils return isset($_ENV['RESELLER_TYPE']) ? $_ENV['RESELLER_TYPE'] : false; } + public static function getTermsLink() + { + return static::isNinja() ? NINJA_WEB_URL.'/terms' : NINJA_WEB_URL.'/self-hosting-the-invoice-ninja-platform'; + } + public static function isOAuthEnabled() { $providers = [ @@ -238,6 +256,8 @@ class Utils $price = PLAN_PRICE_ENTERPRISE_MONTHLY_5; } elseif ($numUsers <= 10) { $price = PLAN_PRICE_ENTERPRISE_MONTHLY_10; + } elseif ($numUsers <= 20) { + $price = PLAN_PRICE_ENTERPRISE_MONTHLY_20; } else { static::fatalError('Invalid number of users: ' . $numUsers); } @@ -256,8 +276,10 @@ class Utils return 1; } elseif ($max <= 5) { return 3; - } else { + } elseif ($max <= 10) { return 6; + } else { + return 11; } } @@ -266,7 +288,7 @@ class Utils return substr($_SERVER['SCRIPT_NAME'], 0, strrpos($_SERVER['SCRIPT_NAME'], '/') + 1); } - public static function trans($input) + public static function trans($input, $module = false) { $data = []; @@ -274,7 +296,11 @@ class Utils if ($field == 'checkbox') { $data[] = $field; } elseif ($field) { - $data[] = trans("texts.$field"); + if ($module) { + $data[] = mtrans($module, $field); + } else { + $data[] = trans("texts.$field"); + } } else { $data[] = ''; } diff --git a/app/Listeners/ActivityListener.php b/app/Listeners/ActivityListener.php index ee016cbff39b..2a3985f54ff6 100644 --- a/app/Listeners/ActivityListener.php +++ b/app/Listeners/ActivityListener.php @@ -132,7 +132,7 @@ class ActivityListener } $backupInvoice = Invoice::with('invoice_items', 'client.account', 'client.contacts') - ->withArchived() + ->withTrashed() ->find($event->invoice->id); $activity = $this->activityRepo->create( @@ -200,7 +200,8 @@ class ActivityListener ACTIVITY_TYPE_EMAIL_INVOICE, false, false, - $event->invitation + $event->invitation, + $event->notes ); } @@ -238,7 +239,9 @@ class ActivityListener return; } - $backupQuote = Invoice::with('invoice_items', 'client.account', 'client.contacts')->find($event->quote->id); + $backupQuote = Invoice::with('invoice_items', 'client.account', 'client.contacts') + ->withTrashed() + ->find($event->quote->id); $activity = $this->activityRepo->create( $event->quote, @@ -296,7 +299,8 @@ class ActivityListener ACTIVITY_TYPE_EMAIL_QUOTE, false, false, - $event->invitation + $event->invitation, + $event->notes ); } diff --git a/app/Listeners/HandleUserLoggedIn.php b/app/Listeners/HandleUserLoggedIn.php index b8a9e3f1ad70..f8b3bb8cfb4e 100644 --- a/app/Listeners/HandleUserLoggedIn.php +++ b/app/Listeners/HandleUserLoggedIn.php @@ -52,6 +52,10 @@ class HandleUserLoggedIn { $account->loadLocalizationSettings(); + if (strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') || strpos($_SERVER['HTTP_USER_AGENT'], 'iPad')) { + Session::flash('warning', trans('texts.iphone_app_message', ['link' => link_to(NINJA_IOS_APP_URL, trans('texts.iphone_app'))])); + } + // if they're using Stripe make sure they're using Stripe.js $accountGateway = $account->getGatewayConfig(GATEWAY_STRIPE); if ($accountGateway && ! $accountGateway->getPublishableStripeKey()) { diff --git a/app/Listeners/InvoiceListener.php b/app/Listeners/InvoiceListener.php index 566d9b4994c2..c7353c768ad5 100644 --- a/app/Listeners/InvoiceListener.php +++ b/app/Listeners/InvoiceListener.php @@ -2,6 +2,7 @@ use Utils; use Auth; +use App\Models\Activity; use App\Events\InvoiceWasUpdated; use App\Events\InvoiceWasCreated; use App\Events\PaymentWasCreated; @@ -69,6 +70,13 @@ class InvoiceListener $invoice->updateBalances($adjustment, $partial); $invoice->updatePaidStatus(); + + // store a backup of the invoice + $activity = Activity::wherePaymentId($payment->id) + ->whereActivityTypeId(ACTIVITY_TYPE_CREATE_PAYMENT) + ->first(); + $activity->json_backup = $invoice->hidePrivateFields()->toJSON(); + $activity->save(); } /** diff --git a/app/Listeners/NotificationListener.php b/app/Listeners/NotificationListener.php index ad94e5767a61..8772ef02e0ff 100644 --- a/app/Listeners/NotificationListener.php +++ b/app/Listeners/NotificationListener.php @@ -1,46 +1,20 @@ userMailer = $userMailer; - $this->contactMailer = $contactMailer; - $this->pushService = $pushService; - } - /** * @param $invoice * @param $type @@ -52,7 +26,7 @@ class NotificationListener { if ($user->{"notify_{$type}"}) { - $this->userMailer->sendNotification($user, $invoice, $type, $payment); + dispatch(new SendNotificationEmail($user, $invoice, $type, $payment)); } } } @@ -63,7 +37,7 @@ class NotificationListener public function emailedInvoice(InvoiceWasEmailed $event) { $this->sendEmails($event->invoice, 'sent'); - $this->pushService->sendNotification($event->invoice, 'sent'); + dispatch(new SendPushNotification($event->invoice, 'sent')); } /** @@ -72,7 +46,7 @@ class NotificationListener public function emailedQuote(QuoteWasEmailed $event) { $this->sendEmails($event->quote, 'sent'); - $this->pushService->sendNotification($event->quote, 'sent'); + dispatch(new SendPushNotification($event->quote, 'sent')); } /** @@ -85,7 +59,7 @@ class NotificationListener } $this->sendEmails($event->invoice, 'viewed'); - $this->pushService->sendNotification($event->invoice, 'viewed'); + dispatch(new SendPushNotification($event->invoice, 'viewed')); } /** @@ -98,7 +72,7 @@ class NotificationListener } $this->sendEmails($event->quote, 'viewed'); - $this->pushService->sendNotification($event->quote, 'viewed'); + dispatch(new SendPushNotification($event->quote, 'viewed')); } /** @@ -107,7 +81,7 @@ class NotificationListener public function approvedQuote(QuoteInvitationWasApproved $event) { $this->sendEmails($event->quote, 'approved'); - $this->pushService->sendNotification($event->quote, 'approved'); + dispatch(new SendPushNotification($event->quote, 'approved')); } /** @@ -120,10 +94,9 @@ class NotificationListener return; } - $this->contactMailer->sendPaymentConfirmation($event->payment); $this->sendEmails($event->payment->invoice, 'paid', $event->payment); - - $this->pushService->sendNotification($event->payment->invoice, 'paid'); + dispatch(new SendPaymentEmail($event->payment)); + dispatch(new SendPushNotification($event->payment->invoice, 'paid')); } } diff --git a/app/Listeners/SubscriptionListener.php b/app/Listeners/SubscriptionListener.php index 5b84413cb25a..595bc868bb12 100644 --- a/app/Listeners/SubscriptionListener.php +++ b/app/Listeners/SubscriptionListener.php @@ -1,5 +1,9 @@ invoice->account); + $this->checkSubscriptions(EVENT_UPDATE_INVOICE, $event->invoice, $transformer, ENTITY_CLIENT); + } + + /** + * @param InvoiceWasDeleted $event + */ + public function deletedInvoice(InvoiceWasDeleted $event) + { + $transformer = new InvoiceTransformer($event->invoice->account); + $this->checkSubscriptions(EVENT_DELETE_INVOICE, $event->invoice, $transformer, ENTITY_CLIENT); + } + + /** + * @param QuoteWasUpdated $event + */ + public function updatedQuote(QuoteWasUpdated $event) + { + $transformer = new InvoiceTransformer($event->quote->account); + $this->checkSubscriptions(EVENT_UPDATE_QUOTE, $event->quote, $transformer, ENTITY_CLIENT); + } + + /** + * @param InvoiceWasDeleted $event + */ + public function deletedQuote(QuoteWasDeleted $event) + { + $transformer = new InvoiceTransformer($event->quote->account); + $this->checkSubscriptions(EVENT_DELETE_QUOTE, $event->quote, $transformer, ENTITY_CLIENT); } /** diff --git a/app/Models/Account.php b/app/Models/Account.php index a27074edbd03..b1df65189390 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -13,6 +13,8 @@ use Illuminate\Support\Facades\Storage; use Illuminate\Database\Eloquent\SoftDeletes; use Laracasts\Presenter\PresentableTrait; use App\Models\Traits\PresentsInvoice; +use App\Models\Traits\GeneratesNumbers; +use App\Models\Traits\SendsEmails; /** * Class Account @@ -22,6 +24,8 @@ class Account extends Eloquent use PresentableTrait; use SoftDeletes; use PresentsInvoice; + use GeneratesNumbers; + use SendsEmails; /** * @var string @@ -80,6 +84,12 @@ class Account extends Eloquent 'show_accept_quote_terms', 'require_invoice_signature', 'require_quote_signature', + 'pdf_email_attachment', + 'document_email_attachment', + 'email_design_id', + 'enable_email_markup', + 'domain_id', + 'payment_terms', ]; /** @@ -103,11 +113,11 @@ class Account extends Eloquent public static $advancedSettings = [ ACCOUNT_INVOICE_SETTINGS, ACCOUNT_INVOICE_DESIGN, + ACCOUNT_CLIENT_PORTAL, ACCOUNT_EMAIL_SETTINGS, ACCOUNT_TEMPLATES_AND_REMINDERS, ACCOUNT_BANKS, - ACCOUNT_CLIENT_PORTAL, - ACCOUNT_REPORTS, + //ACCOUNT_REPORTS, ACCOUNT_DATA_VISUALIZATIONS, ACCOUNT_API_TOKENS, ACCOUNT_USER_MANAGEMENT, @@ -466,6 +476,17 @@ class Account extends Eloquent return $this->date_format ? $this->date_format->format : DEFAULT_DATE_FORMAT; } + + public function getSampleLink() + { + $invitation = new Invitation(); + $invitation->account = $this; + $invitation->invitation_key = '...'; + + return $invitation->getLink(); + } + + /** * @param $amount * @param null $client @@ -862,7 +883,7 @@ class Account extends Eloquent if ($this->hasClientNumberPattern($invoice) && !$clientId) { // do nothing, we don't yet know the value } elseif ( ! $invoice->invoice_number) { - $invoice->invoice_number = $this->getNextInvoiceNumber($invoice); + $invoice->invoice_number = $this->getNextNumber($invoice); } } @@ -874,191 +895,6 @@ class Account extends Eloquent return $invoice; } - /** - * @param $invoice_type_id - * @return string - */ - public function getNumberPrefix($invoice_type_id) - { - if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) { - return ''; - } - - return ($invoice_type_id == INVOICE_TYPE_QUOTE ? $this->quote_number_prefix : $this->invoice_number_prefix) ?: ''; - } - - /** - * @param $invoice_type_id - * @return bool - */ - public function hasNumberPattern($invoice_type_id) - { - if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) { - return false; - } - - return $invoice_type_id == INVOICE_TYPE_QUOTE ? ($this->quote_number_pattern ? true : false) : ($this->invoice_number_pattern ? true : false); - } - - /** - * @param $invoice - * @return string - */ - public function hasClientNumberPattern($invoice) - { - $pattern = $invoice->invoice_type_id == INVOICE_TYPE_QUOTE ? $this->quote_number_pattern : $this->invoice_number_pattern; - - return strstr($pattern, '$custom'); - } - - /** - * @param $invoice - * @return bool|mixed - */ - public function getNumberPattern($invoice) - { - $pattern = $invoice->invoice_type_id == INVOICE_TYPE_QUOTE ? $this->quote_number_pattern : $this->invoice_number_pattern; - - if (!$pattern) { - return false; - } - - $search = ['{$year}']; - $replace = [date('Y')]; - - $search[] = '{$counter}'; - $replace[] = str_pad($this->getCounter($invoice->invoice_type_id), $this->invoice_number_padding, '0', STR_PAD_LEFT); - - if (strstr($pattern, '{$userId}')) { - $search[] = '{$userId}'; - $replace[] = str_pad(($invoice->user->public_id + 1), 2, '0', STR_PAD_LEFT); - } - - $matches = false; - preg_match('/{\$date:(.*?)}/', $pattern, $matches); - if (count($matches) > 1) { - $format = $matches[1]; - $search[] = $matches[0]; - $replace[] = str_replace($format, date($format), $matches[1]); - } - - $pattern = str_replace($search, $replace, $pattern); - - if ($invoice->client_id) { - $pattern = $this->getClientInvoiceNumber($pattern, $invoice); - } - - return $pattern; - } - - /** - * @param $pattern - * @param $invoice - * @return mixed - */ - private function getClientInvoiceNumber($pattern, $invoice) - { - if (!$invoice->client) { - return $pattern; - } - - $search = [ - '{$custom1}', - '{$custom2}', - ]; - - $replace = [ - $invoice->client->custom_value1, - $invoice->client->custom_value2, - ]; - - return str_replace($search, $replace, $pattern); - } - - /** - * @param $invoice_type_id - * @return mixed - */ - public function getCounter($invoice_type_id) - { - return $invoice_type_id == INVOICE_TYPE_QUOTE && !$this->share_counter ? $this->quote_number_counter : $this->invoice_number_counter; - } - - /** - * @param $entityType - * @return mixed|string - */ - public function previewNextInvoiceNumber($entityType = ENTITY_INVOICE) - { - $invoice = $this->createInvoice($entityType); - return $this->getNextInvoiceNumber($invoice); - } - - /** - * @param $invoice - * @param bool $validateUnique - * @return mixed|string - */ - public function getNextInvoiceNumber($invoice, $validateUnique = true) - { - if ($this->hasNumberPattern($invoice->invoice_type_id)) { - $number = $this->getNumberPattern($invoice); - } else { - $counter = $this->getCounter($invoice->invoice_type_id); - $prefix = $this->getNumberPrefix($invoice->invoice_type_id); - $counterOffset = 0; - $check = false; - - // confirm the invoice number isn't already taken - do { - $number = $prefix . str_pad($counter, $this->invoice_number_padding, '0', STR_PAD_LEFT); - if ($validateUnique) { - $check = Invoice::scope(false, $this->id)->whereInvoiceNumber($number)->withTrashed()->first(); - $counter++; - $counterOffset++; - } - } while ($check); - - // update the invoice counter to be caught up - if ($counterOffset > 1) { - if ($invoice->isType(INVOICE_TYPE_QUOTE)) { - if ( ! $this->share_counter) { - $this->quote_number_counter += $counterOffset - 1; - } - } else { - $this->invoice_number_counter += $counterOffset - 1; - } - - $this->save(); - } - } - - if ($invoice->recurring_invoice_id) { - $number = $this->recurring_invoice_number_prefix . $number; - } - - return $number; - } - - /** - * @param $invoice - */ - public function incrementCounter($invoice) - { - // if they didn't use the counter don't increment it - if ($invoice->invoice_number != $this->getNextInvoiceNumber($invoice, false)) { - return; - } - - if ($invoice->isType(INVOICE_TYPE_QUOTE) && !$this->share_counter) { - $this->quote_number_counter += 1; - } else { - $this->invoice_number_counter += 1; - } - - $this->save(); - } - /** * @param bool $client */ @@ -1107,6 +943,10 @@ class Account extends Eloquent return; } + if ($this->company->trial_started && $this->company->trial_started != '0000-00-00') { + return; + } + $this->company->trial_plan = $plan; $this->company->trial_started = date_create()->format('Y-m-d'); $this->company->save(); @@ -1166,7 +1006,6 @@ class Account extends Eloquent return false; } // Fallthrough - case FEATURE_CLIENT_PORTAL_CSS: case FEATURE_REMOVE_CREATED_BY: return !empty($planDetails);// A plan is required even for self-hosted users @@ -1342,31 +1181,6 @@ class Account extends Eloquent return $plan_details && $plan_details['trial']; } - /** - * @param null $plan - * @return array|bool - */ - public function isEligibleForTrial($plan = null) - { - if (!$this->company->trial_plan) { - if ($plan) { - return $plan == PLAN_PRO || $plan == PLAN_ENTERPRISE; - } else { - return [PLAN_PRO, PLAN_ENTERPRISE]; - } - } - - if ($this->company->trial_plan == PLAN_PRO) { - if ($plan) { - return $plan != PLAN_PRO; - } else { - return [PLAN_ENTERPRISE]; - } - } - - return false; - } - /** * @return int */ @@ -1709,8 +1523,8 @@ class Account extends Eloquent */ public function showCustomField($field, $entity = false) { - if ($this->hasFeature(FEATURE_INVOICE_SETTINGS)) { - return $this->$field ? true : false; + if ($this->hasFeature(FEATURE_INVOICE_SETTINGS) && $this->$field) { + return true; } if (!$entity) { @@ -1753,9 +1567,7 @@ class Account extends Eloquent if ($headerFont != $bodyFont) { $css .= 'h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{'.$headerFont.'}'; } - } - if ($this->hasFeature(FEATURE_CLIENT_PORTAL_CSS)) { - // For self-hosted users, a white-label license is required for custom CSS + $css .= $this->client_view_css; } @@ -1882,7 +1694,7 @@ class Account extends Eloquent return $this->enabled_modules & static::$modules[$entityType]; } - public function showAuthenticatePanel($invoice) + public function requiresAuthorization($invoice) { return $this->showAcceptTerms($invoice) || $this->showSignature($invoice); } @@ -1904,6 +1716,22 @@ class Account extends Eloquent return $invoice->isQuote() ? $this->require_quote_signature : $this->require_invoice_signature; } + + public function emailMarkupEnabled() + { + if ( ! Utils::isNinja()) { + return false; + } + + return $this->enable_email_markup; + } + + public function defaultDueDate() + { + $numDays = $this->payment_terms == -1 ? 0 : $this->payment_terms; + + return Carbon::now($this->getTimezone())->addDays($numDays)->format('Y-m-d'); + } } Account::updated(function ($account) diff --git a/app/Models/Client.php b/app/Models/Client.php index 02da4dc912a5..d88f5986c0b8 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -275,6 +275,12 @@ class Client extends EntityModel } else { $contact = Contact::createNew(); $contact->send_invoice = true; + + if (isset($data['contact_key']) && $this->account->account_key == env('NINJA_LICENSE_ACCOUNT_KEY')) { + $contact->contact_key = $data['contact_key']; + } else { + $contact->contact_key = str_random(RANDOM_KEY_LENGTH); + } } if (Utils::hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD) && $this->account->enable_portal_password){ @@ -379,6 +385,14 @@ class Client extends EntityModel return ENTITY_CLIENT; } + /** + * @return bool + */ + public function showMap() + { + return $this->hasAddress() && env('GOOGLE_MAPS_ENABLED') !== false; + } + /** * @return bool */ @@ -521,6 +535,7 @@ class Client extends EntityModel Client::creating(function ($client) { $client->setNullValues(); + $client->account->incrementCounter($client); }); Client::updating(function ($client) { diff --git a/app/Models/Company.php b/app/Models/Company.php index 94111e446556..f1bfd81b9a2b 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -56,13 +56,17 @@ class Company extends Eloquent // handle promos and discounts public function hasActiveDiscount(Carbon $date = null) { - if ( ! $this->discount) { + if ( ! $this->discount || ! $this->discount_expires) { return false; } $date = $date ?: Carbon::today(); - return $this->discount_expires && $this->discount_expires->gt($date); + if ($this->plan_term == PLAN_TERM_MONTHLY) { + return $this->discount_expires->gt($date); + } else { + return $this->discount_expires->subMonths(11)->gt($date); + } } public function discountedPrice($price) @@ -74,6 +78,29 @@ class Company extends Eloquent return $price - ($price * $this->discount); } + public function daysUntilPlanExpires() + { + if ( ! $this->hasActivePlan()) { + return 0; + } + + return Carbon::parse($this->plan_expires)->diffInDays(Carbon::today()); + } + + public function hasActivePlan() + { + return Carbon::parse($this->plan_expires) >= Carbon::today(); + } + + public function hasExpiredPlan($plan) + { + if ($this->plan != $plan) { + return false; + } + + return Carbon::parse($this->plan_expires) < Carbon::today(); + } + public function hasEarnedPromo() { if ( ! Utils::isNinjaProd() || Utils::isPro()) { diff --git a/app/Models/Contact.php b/app/Models/Contact.php index feece59de005..94f6c388a1ad 100644 --- a/app/Models/Contact.php +++ b/app/Models/Contact.php @@ -128,7 +128,7 @@ class Contact extends EntityModel implements AuthenticatableContract, CanResetPa public function getFullName() { if ($this->first_name || $this->last_name) { - return $this->first_name.' '.$this->last_name; + return trim($this->first_name.' '.$this->last_name); } else { return ''; } diff --git a/app/Models/EntityModel.php b/app/Models/EntityModel.php index e272b4350857..05cf083bcb4f 100644 --- a/app/Models/EntityModel.php +++ b/app/Models/EntityModel.php @@ -289,6 +289,7 @@ class EntityModel extends Eloquent 'vendors' => 'building', 'settings' => 'cog', 'self-update' => 'download', + 'reports' => 'th-list', ]; return array_get($icons, $entityType); @@ -335,4 +336,15 @@ class EntityModel extends Eloquent return $class::getStatuses($entityType); } + + public function statusClass() + { + return ''; + } + + public function statusLabel() + { + return ''; + } + } diff --git a/app/Models/Expense.php b/app/Models/Expense.php index 1829fed8d186..3a747c0d5c29 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -221,6 +221,53 @@ class Expense extends EntityModel return $statuses; } + + public static function calcStatusLabel($shouldBeInvoiced, $invoiceId, $balance) + { + if ($invoiceId) { + if (floatval($balance) > 0) { + $label = 'invoiced'; + } else { + $label = 'paid'; + } + } elseif ($shouldBeInvoiced) { + $label = 'pending'; + } else { + $label = 'logged'; + } + + return trans("texts.{$label}"); + } + + public static function calcStatusClass($shouldBeInvoiced, $invoiceId, $balance) + { + if ($invoiceId) { + if (floatval($balance) > 0) { + return 'default'; + } else { + return 'success'; + } + } elseif ($shouldBeInvoiced) { + return 'warning'; + } else { + return 'primary'; + } + } + + public function statusClass() + { + $balance = $this->invoice ? $this->invoice->balance : 0; + + return static::calcStatusClass($this->should_be_invoiced, $this->invoice_id, $balance); + } + + public function statusLabel() + { + $balance = $this->invoice ? $this->invoice->balance : 0; + + return static::calcStatusLabel($this->should_be_invoiced, $this->invoice_id, $balance); + } + } Expense::creating(function ($expense) { diff --git a/app/Models/Invitation.php b/app/Models/Invitation.php index 370c521b7830..5b62b8218183 100644 --- a/app/Models/Invitation.php +++ b/app/Models/Invitation.php @@ -68,14 +68,19 @@ class Invitation extends EntityModel $this->load('account'); } + $account = $this->account; + $iframe_url = $account->iframe_url; $url = trim(SITE_URL, '/'); - $iframe_url = $this->account->iframe_url; - if ($this->account->hasFeature(FEATURE_CUSTOM_URL)) { + if ($account->hasFeature(FEATURE_CUSTOM_URL)) { + if (Utils::isNinjaProd()) { + $url = $account->present()->clientPortalLink(); + } + if ($iframe_url && !$forceOnsite) { return "{$iframe_url}?{$this->invitation_key}"; } elseif ($this->account->subdomain) { - $url = Utils::replaceSubdomain($url, $this->account->subdomain); + $url = Utils::replaceSubdomain($url, $account->subdomain); } } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 2c7fc0fcc555..4c16459a11bd 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -10,6 +10,7 @@ use App\Events\InvoiceWasCreated; use App\Events\InvoiceWasUpdated; use App\Events\InvoiceInvitationWasEmailed; use App\Events\QuoteInvitationWasEmailed; +use App\Libraries\CurlUtils; /** * Class Invoice @@ -64,10 +65,22 @@ class Invoice extends EntityModel implements BalanceAffecting 'date:', ]; + public static $statusClasses = [ + INVOICE_STATUS_SENT => 'info', + INVOICE_STATUS_VIEWED => 'warning', + INVOICE_STATUS_APPROVED => 'success', + INVOICE_STATUS_PARTIAL => 'primary', + INVOICE_STATUS_PAID => 'success', + ]; + /** * @var string */ public static $fieldInvoiceNumber = 'invoice_number'; + /** + * @var string + */ + public static $fieldPONumber = 'po_number'; /** * @var string */ @@ -101,6 +114,7 @@ class Invoice extends EntityModel implements BalanceAffecting return [ Client::$fieldName, Invoice::$fieldInvoiceNumber, + Invoice::$fieldPONumber, Invoice::$fieldInvoiceDate, Invoice::$fieldDueDate, Invoice::$fieldAmount, @@ -118,9 +132,11 @@ class Invoice extends EntityModel implements BalanceAffecting return [ 'number^po' => 'invoice_number', 'amount' => 'amount', - 'organization' => 'name', + 'client|organization' => 'name', 'paid^date' => 'paid', - 'invoice_date|create_date' => 'invoice_date', + 'invoice date|create date' => 'invoice_date', + 'po number' => 'po_number', + 'due date' => 'due_date', 'terms' => 'terms', 'notes' => 'notes', ]; @@ -181,31 +197,38 @@ class Invoice extends EntityModel implements BalanceAffecting return floatval($this->amount) - floatval($this->getOriginal('amount')); } - /** - * @return bool - */ public function isChanged() { - if ($this->getRawAdjustment() != 0) { - return true; - } - - foreach ([ - 'invoice_number', - 'po_number', - 'invoice_date', - 'due_date', - 'terms', - 'public_notes', - 'invoice_footer', - 'partial', - ] as $field) { - if ($this->$field != $this->getOriginal($field)) { + if (Utils::isNinja()) { + if ($this->getRawAdjustment() != 0) { return true; } - } - return false; + foreach ([ + 'invoice_number', + 'po_number', + 'invoice_date', + 'due_date', + 'terms', + 'public_notes', + 'invoice_footer', + 'partial', + ] as $field) { + if ($this->$field != $this->getOriginal($field)) { + return true; + } + } + + return false; + } else { + $dirty = $this->getDirty(); + + unset($dirty['invoice_status_id']); + unset($dirty['client_enable_auto_bill']); + unset($dirty['quote_invoice_id']); + + return count($dirty) > 0; + } } /** @@ -410,17 +433,36 @@ class Invoice extends EntityModel implements BalanceAffecting return $this->isType(INVOICE_TYPE_STANDARD) && ! $this->is_recurring; } + public function markSentIfUnsent() + { + if ( ! $this->isSent()) { + $this->markSent(); + } + } + + public function markSent() + { + if ( ! $this->isSent()) { + $this->invoice_status_id = INVOICE_STATUS_SENT; + } + + $this->is_public = true; + $this->save(); + + $this->markInvitationsSent(); + } + /** * @param bool $notify */ - public function markInvitationsSent($notify = false) + public function markInvitationsSent($notify = false, $reminder = false) { if ( ! $this->relationLoaded('invitations')) { $this->load('invitations'); } foreach ($this->invitations as $invitation) { - $this->markInvitationSent($invitation, false, $notify); + $this->markInvitationSent($invitation, false, $notify, $reminder); } } @@ -444,9 +486,9 @@ class Invoice extends EntityModel implements BalanceAffecting * @param bool $messageId * @param bool $notify */ - public function markInvitationSent($invitation, $messageId = false, $notify = true) + public function markInvitationSent($invitation, $messageId = false, $notify = true, $notes = false) { - if (!$this->isSent()) { + if ( ! $this->isSent()) { $this->invoice_status_id = INVOICE_STATUS_SENT; $this->save(); } @@ -460,9 +502,9 @@ class Invoice extends EntityModel implements BalanceAffecting } if ($this->isType(INVOICE_TYPE_QUOTE)) { - event(new QuoteInvitationWasEmailed($invitation)); + event(new QuoteInvitationWasEmailed($invitation, $notes)); } else { - event(new InvoiceInvitationWasEmailed($invitation)); + event(new InvoiceInvitationWasEmailed($invitation, $notes)); } } @@ -550,7 +592,58 @@ class Invoice extends EntityModel implements BalanceAffecting public function canBePaid() { - return floatval($this->balance) > 0 && ! $this->is_deleted && $this->isInvoice() && $this->is_public; + return floatval($this->balance) > 0 && ! $this->is_deleted && $this->isInvoice(); + } + + public static function calcStatusLabel($status, $class, $entityType, $quoteInvoiceId) + { + if ($quoteInvoiceId) { + $label = 'converted'; + } else if ($class == 'danger') { + $label = $entityType == ENTITY_INVOICE ? 'overdue' : 'expired'; + } else { + $label = 'status_' . strtolower($status); + } + + return trans("texts.{$label}"); + } + + public static function calcStatusClass($statusId, $balance, $dueDate) + { + if (static::calcIsOverdue($balance, $dueDate)) { + return 'danger'; + } + + if (isset(static::$statusClasses[$statusId])) { + return static::$statusClasses[$statusId]; + } + + return 'default'; + } + + public static function calcIsOverdue($balance, $dueDate) + { + if ( ! Utils::parseFloat($balance) > 0) { + return false; + } + + if ( ! $dueDate || $dueDate == '0000-00-00') { + return false; + } + + // it isn't considered overdue until the end of the day + return time() > (strtotime($dueDate) + (60*60*24)); + } + + public function statusClass() + { + return static::calcStatusClass($this->invoice_status_id, $this->balance, $this->due_date); + } + + public function statusLabel() + { + return static::calcStatusLabel($this->invoice_status->name, $this->statusClass(), $this->getEntityType(), $this->quote_invoice_id); +>>>>>>> release-3.0.0 } /** @@ -601,7 +694,7 @@ class Invoice extends EntityModel implements BalanceAffecting */ public function isSent() { - return $this->invoice_status_id >= INVOICE_STATUS_SENT && $this->is_public; + return $this->invoice_status_id >= INVOICE_STATUS_SENT && $this->getOriginal('is_public'); } /** @@ -633,11 +726,7 @@ class Invoice extends EntityModel implements BalanceAffecting */ public function isOverdue() { - if ( ! $this->due_date) { - return false; - } - - return time() > strtotime($this->due_date); + return static::calcIsOverdue($this->balance, $this->due_date); } /** @@ -1099,21 +1188,23 @@ class Invoice extends EntityModel implements BalanceAffecting */ public function getPDFString() { - if (!env('PHANTOMJS_CLOUD_KEY')) { + if ( ! env('PHANTOMJS_CLOUD_KEY') && ! env('PHANTOMJS_BIN_PATH')) { return false; } $invitation = $this->invitations[0]; $link = $invitation->getLink('view', true); - $key = env('PHANTOMJS_CLOUD_KEY'); - if (Utils::isNinjaDev()) { - $link = env('TEST_LINK'); + if (env('PHANTOMJS_BIN_PATH')) { + $pdfString = CurlUtils::phantom('GET', $link . '?phantomjs=true'); + } elseif ($key = env('PHANTOMJS_CLOUD_KEY')) { + if (Utils::isNinjaDev()) { + $link = env('TEST_LINK'); + } + $url = "http://api.phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$link}?phantomjs=true%22,renderType:%22html%22%7D"; + $pdfString = CurlUtils::get($url); } - $url = "http://api.phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$link}?phantomjs=true%22,renderType:%22html%22%7D"; - - $pdfString = file_get_contents($url); $pdfString = strip_tags($pdfString); if ( ! $pdfString || strlen($pdfString) < 200) { diff --git a/app/Models/Payment.php b/app/Models/Payment.php index aa98e24d7960..4f080180603a 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -17,6 +17,15 @@ class Payment extends EntityModel use PresentableTrait; use SoftDeletes; + public static $statusClasses = [ + PAYMENT_STATUS_PENDING => 'info', + PAYMENT_STATUS_COMPLETED => 'success', + PAYMENT_STATUS_FAILED => 'danger', + PAYMENT_STATUS_PARTIALLY_REFUNDED => 'primary', + PAYMENT_STATUS_VOIDED => 'default', + PAYMENT_STATUS_REFUNDED => 'default', + ]; + /** * @var array */ @@ -302,6 +311,44 @@ class Payment extends EntityModel { return $value ? str_pad($value, 4, '0', STR_PAD_LEFT) : null; } + + public static function calcStatusLabel($statusId, $statusName, $amount) + { + if ($statusId == PAYMENT_STATUS_PARTIALLY_REFUNDED) { + return trans('texts.status_partially_refunded_amount', [ + 'amount' => $amount, + ]); + } else { + return trans('texts.status_' . strtolower($statusName)); + } + } + + public static function calcStatusClass($statusId) + { + return static::$statusClasses[$statusId]; + } + + + public function statusClass() + { + return static::calcStatusClass($this->payment_status_id); + } + + public function statusLabel() + { + $amount = $this->account->formatMoney($this->refunded, $this->client); + return static::calcStatusLabel($this->payment_status_id, $this->payment_status->name, $amount); + } + + public function invoiceJsonBackup() + { + $activity = Activity::wherePaymentId($this->id) + ->whereActivityTypeId(ACTIVITY_TYPE_CREATE_PAYMENT) + ->get(['json_backup']) + ->first(); + + return $activity->json_backup; + } } Payment::creating(function ($payment) { diff --git a/app/Models/Task.php b/app/Models/Task.php index 5a114a245ad0..73f0af286f5c 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -215,8 +215,64 @@ class Task extends EntityModel return $statuses; } -} + public static function calcStatusLabel($isRunning, $balance, $invoiceNumber) + { + if ($invoiceNumber) { + if (floatval($balance) > 0) { + $label = 'invoiced'; + } else { + $label = 'paid'; + } + } elseif ($isRunning) { + $label = 'running'; + } else { + $label = 'logged'; + } + return trans("texts.{$label}"); + } + + public static function calcStatusClass($isRunning, $balance, $invoiceNumber) + { + if ($invoiceNumber) { + if (floatval($balance)) { + return 'default'; + } else { + return 'success'; + } + } elseif ($isRunning) { + return 'primary'; + } else { + return 'warning'; + } + } + + public function statusClass() + { + if ($this->invoice) { + $balance = $this->invoice->balance; + $invoiceNumber = $this->invoice->invoice_number; + } else { + $balance = 0; + $invoiceNumber = false; + } + + return static::calcStatusClass($this->is_running, $balance, $invoiceNumber); + } + + public function statusLabel() + { + if ($this->invoice) { + $balance = $this->invoice->balance; + $invoiceNumber = $this->invoice->invoice_number; + } else { + $balance = 0; + $invoiceNumber = false; + } + + return static::calcStatusLabel($this->is_running, $balance, $invoiceNumber); + } +} Task::created(function ($task) { diff --git a/app/Models/TaxRate.php b/app/Models/TaxRate.php index f041c735d989..f9c25afc389c 100644 --- a/app/Models/TaxRate.php +++ b/app/Models/TaxRate.php @@ -18,7 +18,8 @@ class TaxRate extends EntityModel */ protected $fillable = [ 'name', - 'rate' + 'rate', + 'is_inclusive', ]; /** diff --git a/app/Models/Traits/GeneratesNumbers.php b/app/Models/Traits/GeneratesNumbers.php new file mode 100644 index 000000000000..0353c80f4be6 --- /dev/null +++ b/app/Models/Traits/GeneratesNumbers.php @@ -0,0 +1,236 @@ +getEntityType(); + + $counter = $this->getCounter($entityType); + $prefix = $this->getNumberPrefix($entityType); + $counterOffset = 0; + $check = false; + + if ($entityType == ENTITY_CLIENT && ! $this->clientNumbersEnabled()) { + return ''; + } + + // confirm the invoice number isn't already taken + do { + if ($this->hasNumberPattern($entityType)) { + $number = $this->applyNumberPattern($entity, $counter); + } else { + $number = $prefix . str_pad($counter, $this->invoice_number_padding, '0', STR_PAD_LEFT); + } + + if ($entity->isEntityType(ENTITY_CLIENT)) { + $check = Client::scope(false, $this->id)->whereIdNumber($number)->withTrashed()->first(); + } else { + $check = Invoice::scope(false, $this->id)->whereInvoiceNumber($number)->withTrashed()->first(); + } + $counter++; + $counterOffset++; + } while ($check); + + // update the counter to be caught up + if ($counterOffset > 1) { + if ($entity->isEntityType(ENTITY_CLIENT)) { + if ($this->clientNumbersEnabled()) { + $this->client_number_counter += $counterOffset - 1; + $this->save(); + } + } elseif ($entity->isType(INVOICE_TYPE_QUOTE)) { + if ( ! $this->share_counter) { + $this->quote_number_counter += $counterOffset - 1; + $this->save(); + } + } else { + $this->invoice_number_counter += $counterOffset - 1; + $this->save(); + } + } + + if ($entity->recurring_invoice_id) { + $number = $this->recurring_invoice_number_prefix . $number; + } + + return $number; + } + + /** + * @param $entityType + * @return string + */ + public function getNumberPrefix($entityType) + { + if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) { + return ''; + } + + $field = "{$entityType}_number_prefix"; + return $this->$field ?: ''; + } + + /** + * @param $entityType + * @return bool + */ + public function getNumberPattern($entityType) + { + if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) { + return false; + } + + $field = "{$entityType}_number_pattern"; + return $this->$field; + } + + /** + * @param $entityType + * @return bool + */ + public function hasNumberPattern($entityType) + { + return $this->getNumberPattern($entityType) ? true : false; + } + + /** + * @param $entityType + * @return string + */ + public function hasClientNumberPattern($invoice) + { + $pattern = $invoice->invoice_type_id == INVOICE_TYPE_QUOTE ? $this->quote_number_pattern : $this->invoice_number_pattern; + + return strstr($pattern, '$custom'); + } + + /** + * @param $entity + * @return bool|mixed + */ + public function applyNumberPattern($entity, $counter = 0) + { + $entityType = $entity->getEntityType(); + $counter = $counter ?: $this->getCounter($entityType); + $pattern = $this->getNumberPattern($entityType); + + if (!$pattern) { + return false; + } + + $search = ['{$year}']; + $replace = [date('Y')]; + + $search[] = '{$counter}'; + $replace[] = str_pad($counter, $this->invoice_number_padding, '0', STR_PAD_LEFT); + + if (strstr($pattern, '{$userId}')) { + $userId = $entity->user ? $entity->user->public_id : (Auth::check() ? Auth::user()->public_id : 0); + $search[] = '{$userId}'; + $replace[] = str_pad(($userId + 1), 2, '0', STR_PAD_LEFT); + } + + $matches = false; + preg_match('/{\$date:(.*?)}/', $pattern, $matches); + if (count($matches) > 1) { + $format = $matches[1]; + $search[] = $matches[0]; + $date = Carbon::now(session(SESSION_TIMEZONE, DEFAULT_TIMEZONE))->format($format); + $replace[] = str_replace($format, $date, $matches[1]); + } + + $pattern = str_replace($search, $replace, $pattern); + + if ($entity->client_id) { + $pattern = $this->getClientInvoiceNumber($pattern, $entity); + } + + return $pattern; + } + + /** + * @param $pattern + * @param $invoice + * @return mixed + */ + private function getClientInvoiceNumber($pattern, $invoice) + { + if (!$invoice->client) { + return $pattern; + } + + $search = [ + '{$custom1}', + '{$custom2}', + ]; + + $replace = [ + $invoice->client->custom_value1, + $invoice->client->custom_value2, + ]; + + return str_replace($search, $replace, $pattern); + } + + /** + * @param $entityType + * @return mixed + */ + public function getCounter($entityType) + { + if ($entityType == ENTITY_CLIENT) { + return $this->client_number_counter; + } elseif ($entityType == ENTITY_QUOTE && ! $this->share_counter) { + return $this->quote_number_counter; + } else { + return $this->invoice_number_counter; + } + } + + /** + * @param $entityType + * @return mixed|string + */ + public function previewNextInvoiceNumber($entityType = ENTITY_INVOICE) + { + $invoice = $this->createInvoice($entityType); + return $this->getNextNumber($invoice); + } + + /** + * @param $entity + */ + public function incrementCounter($entity) + { + if ($entity->isEntityType(ENTITY_CLIENT)) { + if ($this->client_number_counter) { + $this->client_number_counter += 1; + } + } elseif ($entity->isType(INVOICE_TYPE_QUOTE) && ! $this->share_counter) { + $this->quote_number_counter += 1; + } else { + $this->invoice_number_counter += 1; + } + + $this->save(); + } + + public function clientNumbersEnabled() + { + return $this->hasFeature(FEATURE_INVOICE_SETTINGS) && $this->client_number_counter; + } +} diff --git a/app/Models/Traits/PresentsInvoice.php b/app/Models/Traits/PresentsInvoice.php index 493a4aecdbc5..4fd9e225ac76 100644 --- a/app/Models/Traits/PresentsInvoice.php +++ b/app/Models/Traits/PresentsInvoice.php @@ -96,6 +96,7 @@ trait PresentsInvoice 'client.address1', 'client.address2', 'client.city_state_postal', + 'client.postal_city_state', 'client.country', 'client.email', 'client.phone', @@ -114,6 +115,7 @@ trait PresentsInvoice 'account.address1', 'account.address2', 'account.city_state_postal', + 'account.postal_city_state', 'account.country', 'account.custom_value1', 'account.custom_value2', @@ -196,6 +198,7 @@ trait PresentsInvoice 'id_number', 'vat_number', 'city_state_postal', + 'postal_city_state', 'country', 'email', 'contact_name', @@ -203,6 +206,11 @@ trait PresentsInvoice 'website', 'phone', 'blank', + 'adjustment', + 'tax_invoice', + 'tax_quote', + 'statement', + 'statement_date', ]; foreach ($fields as $field) { diff --git a/app/Models/Traits/SendsEmails.php b/app/Models/Traits/SendsEmails.php new file mode 100644 index 000000000000..4843afa5e364 --- /dev/null +++ b/app/Models/Traits/SendsEmails.php @@ -0,0 +1,24 @@ +isPro() ? $this->bcc_email : false; + } + + public function getFromEmail() + { + if ( ! $this->isPro() || ! Utils::isNinjaProd() || Utils::isReseller()) { + return false; + } + + return Domain::getEmailFromId($this->domain_id); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 9ba3db5d2b77..d60a7c988943 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -7,12 +7,20 @@ use App\Events\UserSettingsChanged; use App\Events\UserSignedUp; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Database\Eloquent\SoftDeletes; +use Laracasts\Presenter\PresentableTrait; /** * Class User */ class User extends Authenticatable { + use PresentableTrait; + + /** + * @var string + */ + protected $presenter = 'App\Ninja\Presenters\UserPresenter'; + /** * @var array */ @@ -130,15 +138,6 @@ class User extends Authenticatable return $this->account->isTrial(); } - /** - * @param null $plan - * @return mixed - */ - public function isEligibleForTrial($plan = null) - { - return $this->account->isEligibleForTrial($plan); - } - /** * @return int */ diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index a68cf42e2ff3..466c6cc8fa47 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -267,6 +267,14 @@ class Vendor extends EntityModel return 'vendor'; } + /** + * @return bool + */ + public function showMap() + { + return $this->hasAddress() && env('GOOGLE_MAPS_ENABLED') !== false; + } + /** * @return bool */ @@ -321,12 +329,14 @@ class Vendor extends EntityModel /** * @return float|int */ - public function getTotalExpense() + public function getTotalExpenses() { return DB::table('expenses') - ->where('vendor_id', '=', $this->id) - ->whereNull('deleted_at') - ->sum('amount'); + ->select('expense_currency_id', DB::raw('SUM(amount) as amount')) + ->whereVendorId($this->id) + ->whereIsDeleted(false) + ->groupBy('expense_currency_id') + ->get(); } } diff --git a/app/Ninja/Datatables/ActivityDatatable.php b/app/Ninja/Datatables/ActivityDatatable.php index 62fdc646d839..0261bfc3bb1d 100644 --- a/app/Ninja/Datatables/ActivityDatatable.php +++ b/app/Ninja/Datatables/ActivityDatatable.php @@ -32,7 +32,13 @@ class ActivityDatatable extends EntityDatatable 'expense' => $model->expense_public_id ? link_to('/expenses/' . $model->expense_public_id, substr($model->expense_public_notes, 0, 30).'...') : null, ]; - return trans("texts.activity_{$model->activity_type_id}", $data); + $str = trans("texts.activity_{$model->activity_type_id}", $data); + + if ($model->notes) { + $str .= ' - ' . trans("texts.notes_{$model->notes}"); + } + + return $str; } ], [ diff --git a/app/Ninja/Datatables/EntityDatatable.php b/app/Ninja/Datatables/EntityDatatable.php index 99e2a414ad89..8d6249c1990c 100644 --- a/app/Ninja/Datatables/EntityDatatable.php +++ b/app/Ninja/Datatables/EntityDatatable.php @@ -64,4 +64,30 @@ class EntityDatatable return $data; } + + public function rightAlignIndices() + { + return $this->alignIndices(['amount', 'balance', 'cost']); + } + + public function centerAlignIndices() + { + return $this->alignIndices(['status']); + } + + public function alignIndices($fields) + { + $columns = $this->columnFields(); + $indices = []; + + foreach ($columns as $index => $column) { + if (in_array($column, $fields)) { + $indices[] = $index + 1; + } + } + + return $indices; + } + + } diff --git a/app/Ninja/Datatables/ExpenseDatatable.php b/app/Ninja/Datatables/ExpenseDatatable.php index 6bf432e6d02b..a818eb0b79d8 100644 --- a/app/Ninja/Datatables/ExpenseDatatable.php +++ b/app/Ninja/Datatables/ExpenseDatatable.php @@ -3,6 +3,7 @@ use Utils; use URL; use Auth; +use App\Models\Expense; class ExpenseDatatable extends EntityDatatable { @@ -123,24 +124,10 @@ class ExpenseDatatable extends EntityDatatable ]; } - private function getStatusLabel($invoiceId, $shouldBeInvoiced, $balance) { - if ($invoiceId) { - if (floatval($balance)) { - $label = trans('texts.invoiced'); - $class = 'default'; - } else { - $label = trans('texts.paid'); - $class = 'success'; - } - } elseif ($shouldBeInvoiced) { - $label = trans('texts.pending'); - $class = 'warning'; - } else { - $label = trans('texts.logged'); - $class = 'primary'; - } + $label = Expense::calcStatusLabel($shouldBeInvoiced, $invoiceId, $balance); + $class = Expense::calcStatusClass($shouldBeInvoiced, $invoiceId, $balance); return "
t |
p,v=i.minHeight&&i.minHeight>f;i.grid=c,b&&(p+=l),v&&(f+=u),m&&(p-=l),g&&(f-=u),/^(se|s|e)$/.test(s)?(n.size.width=p,n.size.height=f):/^(ne)$/.test(s)?(n.size.width=p,n.size.height=f,n.position.top=a.top-d):/^(sw)$/.test(s)?(n.size.width=p,n.size.height=f,n.position.left=a.left-h):((f-u<=0||p-l<=0)&&(e=n._getPaddingPlusBorderDimensions(this)),f-u>0?(n.size.height=f,n.position.top=a.top-d):(f=u-e.height,n.size.height=f,n.position.top=a.top+r.height-f),p-l>0?(n.size.width=p,n.position.left=a.left-h):(p=u-e.height,n.size.width=p,n.position.left=a.left+r.width-p))}});t.ui.resizable,t.widget("ui.dialog",{version:"1.11.2",options:{appendTo:"body",autoOpen:!0,buttons:[],closeOnEscape:!0,closeText:"Close",dialogClass:"",draggable:!0,hide:null,height:"auto",maxHeight:null,maxWidth:null,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",of:window,collision:"fit",using:function(e){var n=t(this).css(e).offset().top;n<0&&t(this).css("top",e.top-n)}},resizable:!0,show:null,title:null,width:300,beforeClose:null,close:null,drag:null,dragStart:null,dragStop:null,focus:null,open:null,resize:null,resizeStart:null,resizeStop:null},sizeRelatedOptions:{buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},resizableRelatedOptions:{maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0},_create:function(){this.originalCss={display:this.element[0].style.display,width:this.element[0].style.width,minHeight:this.element[0].style.minHeight,maxHeight:this.element[0].style.maxHeight,height:this.element[0].style.height},this.originalPosition={parent:this.element.parent(),index:this.element.parent().children().index(this.element)},this.originalTitle=this.element.attr("title"),this.options.title=this.options.title||this.originalTitle,this._createWrapper(),this.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(this.uiDialog),this._createTitlebar(),this._createButtonPane(),this.options.draggable&&t.fn.draggable&&this._makeDraggable(),this.options.resizable&&t.fn.resizable&&this._makeResizable(),this._isOpen=!1,this._trackFocus()},_init:function(){this.options.autoOpen&&this.open()},_appendTo:function(){var e=this.options.appendTo;return e&&(e.jquery||e.nodeType)?t(e):this.document.find(e||"body").eq(0)},_destroy:function(){var t,e=this.originalPosition;this._destroyOverlay(),this.element.removeUniqueId().removeClass("ui-dialog-content ui-widget-content").css(this.originalCss).detach(),this.uiDialog.stop(!0,!0).remove(),this.originalTitle&&this.element.attr("title",this.originalTitle),t=e.parent.children().eq(e.index),t.length&&t[0]!==this.element[0]?t.before(this.element):e.parent.append(this.element)},widget:function(){return this.uiDialog},disable:t.noop,enable:t.noop,close:function(e){var n,i=this;if(this._isOpen&&this._trigger("beforeClose",e)!==!1){if(this._isOpen=!1,this._focusedElement=null,this._destroyOverlay(),this._untrackInstance(),!this.opener.filter(":focusable").focus().length)try{n=this.document[0].activeElement,n&&"body"!==n.nodeName.toLowerCase()&&t(n).blur()}catch(o){}this._hide(this.uiDialog,this.options.hide,function(){i._trigger("close",e)})}},isOpen:function(){return this._isOpen},moveToTop:function(){this._moveToTop()},_moveToTop:function(e,n){var i=!1,o=this.uiDialog.siblings(".ui-front:visible").map(function(){return+t(this).css("z-index")}).get(),r=Math.max.apply(null,o);return r>=+this.uiDialog.css("z-index")&&(this.uiDialog.css("z-index",r+1),i=!0),i&&!n&&this._trigger("focus",e),i},open:function(){var e=this;return this._isOpen?void(this._moveToTop()&&this._focusTabbable()):(this._isOpen=!0,this.opener=t(this.document[0].activeElement),this._size(),this._position(),this._createOverlay(),this._moveToTop(null,!0),this.overlay&&this.overlay.css("z-index",this.uiDialog.css("z-index")-1),this._show(this.uiDialog,this.options.show,function(){e._focusTabbable(),e._trigger("focus")}),this._makeFocusTarget(),void this._trigger("open"))},_focusTabbable:function(){var t=this._focusedElement;t||(t=this.element.find("[autofocus]")),t.length||(t=this.element.find(":tabbable")),t.length||(t=this.uiDialogButtonPane.find(":tabbable")),t.length||(t=this.uiDialogTitlebarClose.filter(":tabbable")),t.length||(t=this.uiDialog),t.eq(0).focus()},_keepFocus:function(e){function n(){var e=this.document[0].activeElement,n=this.uiDialog[0]===e||t.contains(this.uiDialog[0],e);n||this._focusTabbable()}e.preventDefault(),n.call(this),this._delay(n)},_createWrapper:function(){this.uiDialog=t("
1&&M.splice.apply(M,[1,0].concat(M.splice(y,f+1))),a.dequeue()},t.effects.effect.clip=function(e,n){var i,o,r,a=t(this),s=["position","top","bottom","left","right","height","width"],c=t.effects.setMode(a,e.mode||"hide"),l="show"===c,u=e.direction||"vertical",h="vertical"===u,d=h?"height":"width",p=h?"top":"left",f={};t.effects.save(a,s),a.show(),i=t.effects.createWrapper(a).css({overflow:"hidden"}),o="IMG"===a[0].tagName?i:a,r=o[d](),l&&(o.css(d,0),o.css(p,r/2)),f[d]=l?r:0,f[p]=l?0:r/2,o.animate(f,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){l||a.hide(),t.effects.restore(a,s),t.effects.removeWrapper(a),n()}})},t.effects.effect.drop=function(e,n){var i,o=t(this),r=["position","top","bottom","left","right","opacity","height","width"],a=t.effects.setMode(o,e.mode||"hide"),s="show"===a,c=e.direction||"left",l="up"===c||"down"===c?"top":"left",u="up"===c||"left"===c?"pos":"neg",h={opacity:s?1:0};t.effects.save(o,r),o.show(),t.effects.createWrapper(o),i=e.distance||o["top"===l?"outerHeight":"outerWidth"](!0)/2,s&&o.css("opacity",0).css(l,"pos"===u?-i:i),h[l]=(s?"pos"===u?"+=":"-=":"pos"===u?"-=":"+=")+i,o.animate(h,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){"hide"===a&&o.hide(),t.effects.restore(o,r),t.effects.removeWrapper(o),n()}})},t.effects.effect.explode=function(e,n){function i(){M.push(this),M.length===h*d&&o()}function o(){p.css({visibility:"visible"}),t(M).remove(),m||p.hide(),n()}var r,a,s,c,l,u,h=e.pieces?Math.round(Math.sqrt(e.pieces)):3,d=h,p=t(this),f=t.effects.setMode(p,e.mode||"hide"),m="show"===f,g=p.show().css("visibility","hidden").offset(),b=Math.ceil(p.outerWidth()/d),v=Math.ceil(p.outerHeight()/h),M=[];for(r=0;r
t<"F"ip>'),x.renderer?i.isPlainObject(x.renderer)&&!x.renderer.header&&(x.renderer.header="jqueryui"):x.renderer="jqueryui"):i.extend(C,Yt.ext.classes,m.oClasses),i(this).addClass(C.sTable),""===x.oScroll.sX&&""===x.oScroll.sY||(x.oScroll.iBarWidth=Tt()),x.oScroll.sX===!0&&(x.oScroll.sX="100%"),x.iInitDisplayStart===n&&(x.iInitDisplayStart=m.iDisplayStart,x._iDisplayStart=m.iDisplayStart),null!==m.iDeferLoading){x.bDeferLoading=!0;var S=i.isArray(m.iDeferLoading);x._iRecordsDisplay=S?m.iDeferLoading[0]:m.iDeferLoading,x._iRecordsTotal=S?m.iDeferLoading[1]:m.iDeferLoading}var O=x.oLanguage;i.extend(!0,O,m.oLanguage),""!==O.sUrl&&(i.ajax({dataType:"json",url:O.sUrl,success:function(t){a(t),r(w.oLanguage,t),i.extend(!0,O,t),st(x)},error:function(){st(x)}}),v=!0),null===m.asStripeClasses&&(x.asStripeClasses=[C.sStripeOdd,C.sStripeEven]);var N=x.asStripeClasses,L=i("tbody tr:eq(0)",this);i.inArray(!0,i.map(N,function(t,e){return L.hasClass(t)}))!==-1&&(i("tbody tr",this).removeClass(N.join(" ")),x.asDestroyStripes=N.slice());var k,q=[],E=this.getElementsByTagName("thead");if(0!==E.length&&(X(x.aoHeader,E[0]),q=F(x)),null===m.aoColumns)for(k=[],g=0,p=q.length;g
").appendTo(this)),x.nTHead=R[0];var H=i(this).children("tbody");0===H.length&&(H=i("
").appendTo(this)),x.nTBody=H[0];var j=i(this).children("tfoot");if(0===j.length&&P.length>0&&(""!==x.oScroll.sX||""!==x.oScroll.sY)&&(j=i("").appendTo(this)),0===j.length||0===j.children().length?i(this).addClass(C.sNoFooter):j.length>0&&(x.nTFoot=j[0],X(x.aoFooter,x.nTFoot)),m.aaData)for(g=0;g"+this.options.dictFallbackText+"
"),i+='',e=n.createElement(i),"FORM"!==this.element.tagName?(o=n.createElement(''),o.appendChild(e)):(this.element.setAttribute("enctype","multipart/form-data"),this.element.setAttribute("method",this.options.method)),null!=o?o:e)},n.prototype.getExistingFallback=function(){var t,e,n,i,o,r;for(e=function(t){var e,n,i;for(n=0,i=t.length;n0){for(a=["TB","GB","MB","KB","b"],n=s=0,c=a.length;s