diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 000000000000..7694ad7822ba --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "./public/vendor" +} \ No newline at end of file diff --git a/.env.example b/.env.example index bd40885edb04..b354269ebf64 100644 --- a/.env.example +++ b/.env.example @@ -2,18 +2,18 @@ APP_ENV=development APP_DEBUG=true APP_URL=http://ninja.dev APP_CIPHER=rijndael-128 -APP_KEY= +APP_KEY DB_TYPE=mysql DB_HOST=localhost DB_DATABASE=ninja -DB_USERNAME= -DB_PASSWORD= +DB_USERNAME +DB_PASSWORD MAIL_DRIVER=smtp MAIL_PORT=587 MAIL_ENCRYPTION=tls -MAIL_HOST= -MAIL_USERNAME= -MAIL_FROM_NAME= -MAIL_PASSWORD= \ No newline at end of file +MAIL_HOST +MAIL_USERNAME +MAIL_FROM_NAME +MAIL_PASSWORD \ No newline at end of file diff --git a/.gitignore b/.gitignore index dd49935a1d7d..7055b9135f86 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ /public/build /public/packages /public/vendor +/storage /bootstrap/compiled.php /bootstrap/environment.php /vendor @@ -24,3 +25,6 @@ public/error_log /ninja.sublime-project /ninja.sublime-workspace auth.json + +.phpstorm.meta.php +_ide_helper.php \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index 8753d476c384..005c733a9d5e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -57,9 +57,14 @@ module.exports = function(grunt) { 'public/vendor/spectrum/spectrum.js', 'public/vendor/jspdf/dist/jspdf.min.js', //'public/vendor/handsontable/dist/jquery.handsontable.full.min.js', + //'public/vendor/pdfmake/build/pdfmake.min.js', + //'public/vendor/pdfmake/build/vfs_fonts.js', + //'public/js/vfs_fonts.js', 'public/js/lightbox.min.js', 'public/js/bootstrap-combobox.js', 'public/js/script.js', + 'public/js/pdf.pdfmake.js', + ], dest: 'public/js/built.js', nonull: true @@ -73,6 +78,7 @@ module.exports = function(grunt) { 'public/js/simpleexpand.js', */ 'public/vendor/bootstrap/dist/js/bootstrap.min.js', + 'public/js/bootstrap-combobox.js', ], dest: 'public/js/built.public.js', @@ -84,7 +90,7 @@ module.exports = function(grunt) { 'public/vendor/datatables/media/css/jquery.dataTables.css', 'public/vendor/datatables-bootstrap3/BS3/assets/css/datatables.css', 'public/vendor/font-awesome/css/font-awesome.min.css', - 'public/vendor/bootstrap-datepicker/css/datepicker.css', + 'public/vendor/bootstrap-datepicker/css/datepicker3.css', 'public/vendor/spectrum/spectrum.css', 'public/css/bootstrap-combobox.css', 'public/css/typeahead.js-bootstrap.css', @@ -105,6 +111,7 @@ module.exports = function(grunt) { 'public/css/bootstrap.splash.css', 'public/css/splash.css', */ + 'public/css/bootstrap-combobox.css', 'public/vendor/datatables/media/css/jquery.dataTables.css', 'public/vendor/datatables-bootstrap3/BS3/assets/css/datatables.css', ], diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000000..eaa9f1e3672c --- /dev/null +++ b/LICENSE @@ -0,0 +1,40 @@ +Attribution Assurance License +Copyright (c) 2014 by Hillel Coren +http://www.hillelcoren.com + +All Rights Reserved +ATTRIBUTION ASSURANCE LICENSE (adapted from the original BSD license) +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the conditions below are met. +These conditions require a modest attribution to InvoiceNinja.com. The hope +is that its promotional value may help justify the thousands of dollars in +otherwise billable time invested in writing this and other freely available, +open-source software. + +1. Redistributions of source code, in whole or part and with or without +modification requires the express permission of the author and must prominently +display "Powered by InvoiceNinja" or the Invoice Ninja logo in verifiable form +with hyperlink to said site. +2. Neither the name nor any trademark of the Author may be used to +endorse or promote products derived from this software without specific +prior written permission. +3. Users are entirely responsible, to the exclusion of the Author and +any other persons, for compliance with (1) regulations set by owners or +administrators of employed equipment, (2) licensing terms of any other +software, and (3) local regulations regarding use, including those +regarding import, export, and use of encryption software. + +THIS FREE SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE AUTHOR OR ANY CONTRIBUTOR BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +EFFECTS OF UNAUTHORIZED OR MALICIOUS NETWORK ACCESS; +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/app/Console/Commands/CheckData.php b/app/Console/Commands/CheckData.php index 501f95b43e4f..297431612473 100644 --- a/app/Console/Commands/CheckData.php +++ b/app/Console/Commands/CheckData.php @@ -1,5 +1,8 @@ groupBy('clients.id', 'clients.balance', 'clients.created_at') ->orderBy('clients.id', 'DESC') - ->get(['clients.id', 'clients.balance', 'clients.paid_to_date']); + ->get(['clients.account_id', 'clients.id', 'clients.balance', 'clients.paid_to_date', DB::raw('sum(invoices.balance) actual_balance')]); $this->info(count($clients) . ' clients with incorrect balance/activities'); foreach ($clients as $client) { - $this->info("=== Client:{$client->id} Balance:{$client->balance} ==="); + $this->info("=== Client:{$client->id} Balance:{$client->balance} Actual Balance:{$client->actual_balance} ==="); $foundProblem = false; $lastBalance = 0; + $lastAdjustment = 0; + $lastCreatedAt = null; $clientFix = false; $activities = DB::table('activities') ->where('client_id', '=', $client->id) @@ -195,6 +200,11 @@ class CheckData extends Command { $foundProblem = true; $clientFix -= $activity->adjustment; $activityFix = 0; + } else if ((strtotime($activity->created_at) - strtotime($lastCreatedAt) <= 1) && $activity->adjustment > 0 && $activity->adjustment == $lastAdjustment) { + $this->info("Duplicate adjustment for updated invoice adjustment:{$activity->adjustment}"); + $foundProblem = true; + $clientFix -= $activity->adjustment; + $activityFix = 0; } } elseif ($activity->activity_type_id == ACTIVITY_TYPE_UPDATE_QUOTE) { // **Fix for updating balance when updating a quote** @@ -231,18 +241,32 @@ class CheckData extends Command { } $lastBalance = $activity->balance; + $lastAdjustment = $activity->adjustment; + $lastCreatedAt = $activity->created_at; } - if ($clientFix !== false) { - $balance = $activity->balance + $clientFix; - $data = ['balance' => $balance]; - $this->info("Corrected balance:{$balance}"); + if ($activity->balance + $clientFix != $client->actual_balance) { + $this->info("** Creating 'recovered update' activity **"); if ($this->option('fix') == 'true') { - DB::table('clients') - ->where('id', $client->id) - ->update($data); + DB::table('activities')->insert([ + 'created_at' => new Carbon, + 'updated_at' => new Carbon, + 'account_id' => $client->account_id, + 'client_id' => $client->id, + 'message' => 'Recovered update to invoice [details]', + 'adjustment' => $client->actual_balance - $activity->balance, + 'balance' => $client->actual_balance, + ]); } } + + $data = ['balance' => $client->actual_balance]; + $this->info("Corrected balance:{$client->actual_balance}"); + if ($this->option('fix') == 'true') { + DB::table('clients') + ->where('id', $client->id) + ->update($data); + } } $this->info('Done'); diff --git a/app/Console/Commands/SendRecurringInvoices.php b/app/Console/Commands/SendRecurringInvoices.php index a5fe2cdaf9b9..aefc79bda752 100644 --- a/app/Console/Commands/SendRecurringInvoices.php +++ b/app/Console/Commands/SendRecurringInvoices.php @@ -69,8 +69,12 @@ class SendRecurringInvoices extends Command $invoice->custom_taxes2 = $recurInvoice->custom_taxes2; $invoice->is_amount_discount = $recurInvoice->is_amount_discount; - if ($invoice->client->payment_terms) { - $invoice->due_date = date_create()->modify($invoice->client->payment_terms.' day')->format('Y-m-d'); + if ($invoice->client->payment_terms != 0) { + $days = $invoice->client->payment_terms; + if ($days == -1) { + $days = 0; + } + $invoice->due_date = date_create()->modify($days.' day')->format('Y-m-d'); } $invoice->save(); diff --git a/app/Console/Commands/SendRenewalInvoices.php b/app/Console/Commands/SendRenewalInvoices.php new file mode 100644 index 000000000000..ceae80e611dc --- /dev/null +++ b/app/Console/Commands/SendRenewalInvoices.php @@ -0,0 +1,57 @@ +mailer = $mailer; + $this->accountRepo = $repo; + } + + public function fire() + { + $this->info(date('Y-m-d').' Running SendRenewalInvoices...'); + $today = new DateTime(); + + $accounts = Account::whereRaw('datediff(curdate(), pro_plan_paid) = 355')->get(); + $this->info(count($accounts).' accounts found'); + dd(0); + foreach ($accounts as $account) { + $client = $this->accountRepo->getNinjaClient($account); + $invitation = $this->accountRepo->createNinjaInvoice($client); + $this->mailer->sendInvoice($invitation->invoice); + } + + $this->info('Done'); + } + + protected function getArguments() + { + return array( + //array('example', InputArgument::REQUIRED, 'An example argument.'), + ); + } + + protected function getOptions() + { + return array( + //array('example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null), + ); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index ddf38a4a3065..64d68d6f4642 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -16,6 +16,7 @@ class Kernel extends ConsoleKernel { 'App\Console\Commands\ResetData', 'App\Console\Commands\ImportTimesheetData', 'App\Console\Commands\CheckData', + 'App\Console\Commands\SendRenewalInvoices', ]; /** diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 1475cbb5cb09..084f74fcc498 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -27,6 +27,7 @@ class Handler extends ExceptionHandler { { Utils::logError(Utils::getErrorString($e)); return false; + //return parent::report($e); } @@ -39,6 +40,15 @@ class Handler extends ExceptionHandler { */ public function render($request, Exception $e) { - return parent::render($request, $e); + if (Utils::isNinjaProd()) { + $data = [ + 'error' => get_class($e), + 'hideHeader' => true, + ]; + + return response()->view('error', $data); + } else { + return parent::render($request, $e); + } } } diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index b80a59b86450..e67102df2b7a 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -12,9 +12,20 @@ use Validator; use View; use stdClass; use Cache; +use Response; +use parseCSV; +use Request; +use App\Models\Affiliate; +use App\Models\License; use App\Models\User; +use App\Models\Client; +use App\Models\Contact; +use App\Models\Invoice; +use App\Models\InvoiceItem; use App\Models\Activity; +use App\Models\Payment; +use App\Models\Credit; use App\Models\Account; use App\Models\Country; use App\Models\Currency; @@ -90,7 +101,7 @@ class AccountController extends BaseController Auth::login($user, true); Event::fire(new UserLoggedIn()); - + return Redirect::to('invoices/create')->with('sign_up', Input::get('sign_up')); } @@ -153,7 +164,7 @@ class AccountController extends BaseController if ($count == 0) { return Redirect::to('gateways/create'); } else { - return View::make('accounts.payments', ['showAdd' => $count < 2]); + return View::make('accounts.payments', ['showAdd' => $count < 3]); } } elseif ($section == ACCOUNT_NOTIFICATIONS) { $data = [ @@ -197,7 +208,7 @@ class AccountController extends BaseController $invoice->invoice_items = [$invoiceItem]; $data['invoice'] = $invoice; - $data['invoiceDesigns'] = InvoiceDesign::where('id', '<=', Auth::user()->maxInvoiceDesignId())->orderBy('id')->get(); + $data['invoiceDesigns'] = InvoiceDesign::availableDesigns(); } else if ($subSection == ACCOUNT_EMAIL_TEMPLATES) { $data['invoiceEmail'] = $account->getEmailTemplate(ENTITY_INVOICE); $data['quoteEmail'] = $account->getEmailTemplate(ENTITY_QUOTE); @@ -291,6 +302,8 @@ class AccountController extends BaseController $account->share_counter = Input::get('share_counter') ? true : false; $account->pdf_email_attachment = Input::get('pdf_email_attachment') ? true : false; + $account->utf8_invoices = Input::get('utf8_invoices') ? true : false; + $account->auto_wrap = Input::get('auto_wrap') ? true : false; if (!$account->share_counter) { $account->quote_number_counter = Input::get('quote_number_counter'); @@ -333,40 +346,27 @@ class AccountController extends BaseController header('Content-Disposition:attachment;filename=export.csv'); $clients = Client::scope()->get(); - AccountController::exportData($output, $clients->toArray()); + Utils::exportData($output, $clients->toArray()); $contacts = Contact::scope()->get(); - AccountController::exportData($output, $contacts->toArray()); + Utils::exportData($output, $contacts->toArray()); $invoices = Invoice::scope()->get(); - AccountController::exportData($output, $invoices->toArray()); + Utils::exportData($output, $invoices->toArray()); $invoiceItems = InvoiceItem::scope()->get(); - AccountController::exportData($output, $invoiceItems->toArray()); + Utils::exportData($output, $invoiceItems->toArray()); $payments = Payment::scope()->get(); - AccountController::exportData($output, $payments->toArray()); + Utils::exportData($output, $payments->toArray()); $credits = Credit::scope()->get(); - AccountController::exportData($output, $credits->toArray()); + Utils::exportData($output, $credits->toArray()); fclose($output); exit; } - private function exportData($output, $data) - { - if (count($data) > 0) { - fputcsv($output, array_keys($data[0])); - } - - foreach ($data as $record) { - fputcsv($output, $record); - } - - fwrite($output, "\n"); - } - private function importFile() { $data = Session::get('data'); @@ -574,6 +574,14 @@ class AccountController extends BaseController $rules['email'] = 'email|required|unique:users,email,'.$user->id.',id'; } + $subdomain = preg_replace('/[^a-zA-Z0-9_\-]/', '', substr(strtolower(Input::get('subdomain')), 0, MAX_SUBDOMAIN_LENGTH)); + if (!$subdomain || in_array($subdomain, ['www', 'app', 'mail', 'admin', 'blog', 'user', 'contact', 'payment', 'payments', 'billing', 'invoice', 'business', 'owner'])) { + $subdomain = null; + } + if ($subdomain) { + $rules['subdomain'] = "unique:accounts,subdomain,{$user->account_id},id"; + } + $validator = Validator::make(Input::all(), $rules); if ($validator->fails()) { @@ -583,6 +591,7 @@ class AccountController extends BaseController } else { $account = Auth::user()->account; $account->name = trim(Input::get('name')); + $account->subdomain = $subdomain; $account->id_number = trim(Input::get('id_number')); $account->vat_number = trim(Input::get('vat_number')); $account->work_email = trim(Input::get('work_email')); diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index 489b2fb2d65c..29002f8727b1 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -25,10 +25,11 @@ class AccountGatewayController extends BaseController ->join('gateways', 'gateways.id', '=', 'account_gateways.gateway_id') ->where('account_gateways.deleted_at', '=', null) ->where('account_gateways.account_id', '=', Auth::user()->account_id) - ->select('account_gateways.public_id', 'gateways.name', 'account_gateways.deleted_at'); + ->select('account_gateways.public_id', 'gateways.name', 'account_gateways.deleted_at', 'account_gateways.gateway_id'); return Datatable::query($query) ->addColumn('name', function ($model) { return link_to('gateways/'.$model->public_id.'/edit', $model->name); }) + ->addColumn('payment_type', function ($model) { return Gateway::getPrettyPaymentType($model->gateway_id); }) ->addColumn('dropdown', function ($model) { $actions = '
', $response->getMessage()); + return Redirect::to('view/'.$invitationKey); } } + + if ($response->isSuccessful()) { + $payment = self::createPayment($invitation, $ref); + Session::flash('message', trans('texts.applied_payment')); + + return Redirect::to('view/'.$payment->invitation->invitation_key); + } elseif ($response->isRedirect()) { + $invitation->transaction_reference = $ref; + $invitation->save(); + + Session::put('transaction_reference', $ref); + Session::save(); + $response->redirect(); + } else { + Session::flash('error', $response->getMessage()); + + return Utils::fatalError('Sorry, there was an error processing your payment. Please try again later.
', $response->getMessage()); + } } catch (\Exception $e) { $errorMessage = trans('texts.payment_error'); Session::flash('error', $errorMessage."
".$e->getMessage()); @@ -655,7 +613,12 @@ class PaymentController extends BaseController if ($invoice->account->account_key == NINJA_ACCOUNT_KEY) { $account = Account::find($invoice->client->public_id); - $account->pro_plan_paid = date_create()->format('Y-m-d'); + if ($account->pro_plan_paid && $account->pro_plan_paid != '0000-00-00') { + $date = DateTime::createFromFormat('Y-m-d', $account->pro_plan_paid); + $account->pro_plan_paid = $date->modify('+1 year')->format('Y-m-d'); + } else { + $account->pro_plan_paid = date_create()->format('Y-m-d'); + } $account->save(); } @@ -663,12 +626,12 @@ class PaymentController extends BaseController $payment->invitation_id = $invitation->id; $payment->account_gateway_id = $accountGateway->id; $payment->invoice_id = $invoice->id; - $payment->amount = $invoice->balance; + $payment->amount = $invoice->getRequestedAmount(); $payment->client_id = $invoice->client_id; $payment->contact_id = $invitation->contact_id; $payment->transaction_reference = $ref; $payment->payment_date = date_create()->format('Y-m-d'); - + if ($payerId) { $payment->payer_id = $payerId; } @@ -685,6 +648,14 @@ class PaymentController extends BaseController $payerId = Request::query('PayerID'); $token = Request::query('token'); + if (!$token) { + $token = Session::pull('transaction_reference'); + } + + if (!$token) { + return redirect(NINJA_WEB_URL); + } + $invitation = Invitation::with('invoice.client.currency', 'invoice.client.account.account_gateways.gateway')->where('transaction_reference', '=', $token)->firstOrFail(); $invoice = $invitation->invoice; @@ -692,20 +663,26 @@ class PaymentController extends BaseController $gateway = self::createGateway($accountGateway); try { - $details = self::getPaymentDetails($invitation); - $response = $gateway->completePurchase($details)->send(); - $ref = $response->getTransactionReference(); + if (method_exists($gateway, 'completePurchase')) { + $details = self::getPaymentDetails($invitation); + $response = $gateway->completePurchase($details)->send(); + $ref = $response->getTransactionReference(); - if ($response->isSuccessful()) { - $payment = self::createPayment($invitation, $ref, $payerId); + if ($response->isSuccessful()) { + $payment = self::createPayment($invitation, $ref, $payerId); + Session::flash('message', trans('texts.applied_payment')); - Session::flash('message', trans('texts.applied_payment')); + return Redirect::to('view/'.$invitation->invitation_key); + } else { + $errorMessage = trans('texts.payment_error')."\n\n".$response->getMessage(); + Session::flash('error', $errorMessage); + Utils::logError($errorMessage); - return Redirect::to('view/'.$invitation->invitation_key); + return Redirect::to('view/'.$invitation->invitation_key); + } } else { - $errorMessage = trans('texts.payment_error')."\n\n".$response->getMessage(); - Session::flash('error', $errorMessage); - Utils::logError($errorMessage); + $payment = self::createPayment($invitation, $token, $payerId); + Session::flash('message', trans('texts.applied_payment')); return Redirect::to('view/'.$invitation->invitation_key); } diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php index 378830c5bd43..039adc0b5643 100644 --- a/app/Http/Controllers/QuoteController.php +++ b/app/Http/Controllers/QuoteController.php @@ -155,7 +155,7 @@ class QuoteController extends BaseController 'sizes' => Cache::get('sizes'), 'paymentTerms' => Cache::get('paymentTerms'), 'industries' => Cache::get('industries'), - 'invoiceDesigns' => InvoiceDesign::where('id', '<=', Auth::user()->maxInvoiceDesignId())->orderBy('id')->get(), + 'invoiceDesigns' => InvoiceDesign::availableDesigns(), 'invoiceLabels' => Auth::user()->account->getInvoiceLabels() ]; } diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index bfbeba8cc6ab..96e82416e897 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -37,76 +37,175 @@ class ReportController extends BaseController return View::make('reports.d3', $data); } - public function report() + public function showReports() { + $action = Input::get('action'); + if (Input::all()) { $groupBy = Input::get('group_by'); $chartType = Input::get('chart_type'); + $reportType = Input::get('report_type'); $startDate = Utils::toSqlDate(Input::get('start_date'), false); $endDate = Utils::toSqlDate(Input::get('end_date'), false); + $enableReport = Input::get('enable_report') ? true : false; + $enableChart = Input::get('enable_chart') ? true : false; } else { $groupBy = 'MONTH'; $chartType = 'Bar'; + $reportType = ''; $startDate = Utils::today(false)->modify('-3 month'); $endDate = Utils::today(false); + $enableReport = true; + $enableChart = true; } - $padding = $groupBy == 'DAYOFYEAR' ? 'day' : ($groupBy == 'WEEK' ? 'week' : 'month'); - $endDate->modify('+1 '.$padding); $datasets = []; $labels = []; $maxTotals = 0; $width = 10; + $displayData = []; + $exportData = []; + $reportTotals = [ + 'amount' => [], + 'balance' => [], + 'paid' => [] + ]; + + if ($reportType) { + $columns = ['client', 'amount', 'paid', 'balance']; + } else { + $columns = ['client', 'invoice_number', 'invoice_date', 'amount', 'paid', 'balance']; + } + + if (Auth::user()->account->isPro()) { - foreach ([ENTITY_INVOICE, ENTITY_PAYMENT, ENTITY_CREDIT] as $entityType) { - $records = DB::table($entityType.'s') - ->select(DB::raw('sum(amount) as total, '.$groupBy.'('.$entityType.'_date) as '.$groupBy)) - ->where('account_id', '=', Auth::user()->account_id) - ->where($entityType.'s.is_deleted', '=', false) - ->where($entityType.'s.'.$entityType.'_date', '>=', $startDate->format('Y-m-d')) - ->where($entityType.'s.'.$entityType.'_date', '<=', $endDate->format('Y-m-d')) - ->groupBy($groupBy); - if ($entityType == ENTITY_INVOICE) { - $records->where('is_quote', '=', false) - ->where('is_recurring', '=', false); + if ($enableReport) { + $query = DB::table('invoices') + ->join('clients', 'clients.id', '=', 'invoices.client_id') + ->join('contacts', 'contacts.client_id', '=', 'clients.id') + ->where('invoices.account_id', '=', Auth::user()->account_id) + ->where('invoices.is_deleted', '=', false) + ->where('clients.is_deleted', '=', false) + ->where('contacts.deleted_at', '=', null) + ->where('invoices.invoice_date', '>=', $startDate->format('Y-m-d')) + ->where('invoices.invoice_date', '<=', $endDate->format('Y-m-d')) + ->where('invoices.is_quote', '=', false) + ->where('invoices.is_recurring', '=', false) + ->where('contacts.is_primary', '=', true); + + $select = ['clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'clients.name as client_name', 'clients.public_id as client_public_id', 'invoices.public_id as invoice_public_id']; + + if ($reportType) { + $query->groupBy('clients.id'); + array_push($select, DB::raw('sum(invoices.amount) amount'), DB::raw('sum(invoices.balance) balance'), DB::raw('sum(invoices.amount - invoices.balance) paid')); + } else { + array_push($select, 'invoices.invoice_number', 'invoices.amount', 'invoices.balance', 'invoices.invoice_date', DB::raw('(invoices.amount - invoices.balance) paid')); + $query->orderBy('invoices.id'); } + + $query->select($select); + $data = $query->get(); - $totals = $records->lists('total'); - $dates = $records->lists($groupBy); - $data = array_combine($dates, $totals); - - $interval = new DateInterval('P1'.substr($groupBy, 0, 1)); - $period = new DatePeriod($startDate, $interval, $endDate); - - $totals = []; - - foreach ($period as $d) { - $dateFormat = $groupBy == 'DAYOFYEAR' ? 'z' : ($groupBy == 'WEEK' ? 'W' : 'n'); - $date = $d->format($dateFormat); - $totals[] = isset($data[$date]) ? $data[$date] : 0; - - if ($entityType == ENTITY_INVOICE) { - $labelFormat = $groupBy == 'DAYOFYEAR' ? 'j' : ($groupBy == 'WEEK' ? 'W' : 'F'); - $label = $d->format($labelFormat); - $labels[] = $label; + foreach ($data as $record) { + // web display data + $displayRow = [link_to('/clients/'.$record->client_public_id, Utils::getClientDisplayName($record))]; + if (!$reportType) { + array_push($displayRow, + link_to('/invoices/'.$record->invoice_public_id, $record->invoice_number), + Utils::fromSqlDate($record->invoice_date, true) + ); } + array_push($displayRow, + Utils::formatMoney($record->amount, $record->currency_id), + Utils::formatMoney($record->paid, $record->currency_id), + Utils::formatMoney($record->balance, $record->currency_id) + ); + + // export data + $exportRow = [trans('texts.client') => Utils::getClientDisplayName($record)]; + if (!$reportType) { + $exportRow[trans('texts.invoice_number')] = $record->invoice_number; + $exportRow[trans('texts.invoice_date')] = Utils::fromSqlDate($record->invoice_date, true); + } + $exportRow[trans('texts.amount')] = Utils::formatMoney($record->amount, $record->currency_id); + $exportRow[trans('texts.paid')] = Utils::formatMoney($record->paid, $record->currency_id); + $exportRow[trans('texts.balance')] = Utils::formatMoney($record->balance, $record->currency_id); + + $displayData[] = $displayRow; + $exportData[] = $exportRow; + + $accountCurrencyId = Auth::user()->account->currency_id; + $currencyId = $record->currency_id ? $record->currency_id : ($accountCurrencyId ? $accountCurrencyId : DEFAULT_CURRENCY); + if (!isset($reportTotals['amount'][$currencyId])) { + $reportTotals['amount'][$currencyId] = 0; + $reportTotals['balance'][$currencyId] = 0; + $reportTotals['paid'][$currencyId] = 0; + } + $reportTotals['amount'][$currencyId] += $record->amount; + $reportTotals['paid'][$currencyId] += $record->paid; + $reportTotals['balance'][$currencyId] += $record->balance; } - $max = max($totals); - - if ($max > 0) { - $datasets[] = [ - 'totals' => $totals, - 'colors' => $entityType == ENTITY_INVOICE ? '78,205,196' : ($entityType == ENTITY_CREDIT ? '199,244,100' : '255,107,107'), - ]; - $maxTotals = max($max, $maxTotals); + if ($action == 'export') { + self::export($exportData, $reportTotals); } } - $width = (ceil($maxTotals / 100) * 100) / 10; - $width = max($width, 10); + if ($enableChart) { + foreach ([ENTITY_INVOICE, ENTITY_PAYMENT, ENTITY_CREDIT] as $entityType) { + $records = DB::table($entityType.'s') + ->select(DB::raw('sum(amount) as total, concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date)) as '.$groupBy)) + ->where('account_id', '=', Auth::user()->account_id) + ->where($entityType.'s.is_deleted', '=', false) + ->where($entityType.'s.'.$entityType.'_date', '>=', $startDate->format('Y-m-d')) + ->where($entityType.'s.'.$entityType.'_date', '<=', $endDate->format('Y-m-d')) + ->groupBy($groupBy); + + if ($entityType == ENTITY_INVOICE) { + $records->where('is_quote', '=', false) + ->where('is_recurring', '=', false); + } + + $totals = $records->lists('total'); + $dates = $records->lists($groupBy); + $data = array_combine($dates, $totals); + + $padding = $groupBy == 'DAYOFYEAR' ? 'day' : ($groupBy == 'WEEK' ? 'week' : 'month'); + $endDate->modify('+1 '.$padding); + $interval = new DateInterval('P1'.substr($groupBy, 0, 1)); + $period = new DatePeriod($startDate, $interval, $endDate); + $endDate->modify('-1 '.$padding); + + $totals = []; + + foreach ($period as $d) { + $dateFormat = $groupBy == 'DAYOFYEAR' ? 'z' : ($groupBy == 'WEEK' ? 'W' : 'n'); + $date = $d->format('Y'.$dateFormat); + $totals[] = isset($data[$date]) ? $data[$date] : 0; + + if ($entityType == ENTITY_INVOICE) { + $labelFormat = $groupBy == 'DAYOFYEAR' ? 'j' : ($groupBy == 'WEEK' ? 'W' : 'F'); + $label = $d->format($labelFormat); + $labels[] = $label; + } + } + + $max = max($totals); + + if ($max > 0) { + $datasets[] = [ + 'totals' => $totals, + 'colors' => $entityType == ENTITY_INVOICE ? '78,205,196' : ($entityType == ENTITY_CREDIT ? '199,244,100' : '255,107,107'), + ]; + $maxTotals = max($max, $maxTotals); + } + } + + $width = (ceil($maxTotals / 100) * 100) / 10; + $width = max($width, 10); + } } $dateTypes = [ @@ -120,6 +219,11 @@ class ReportController extends BaseController 'Line' => 'Line', ]; + $reportTypes = [ + '' => '', + 'Client' => trans('texts.client') + ]; + $params = [ 'labels' => $labels, 'datasets' => $datasets, @@ -128,11 +232,38 @@ class ReportController extends BaseController 'chartTypes' => $chartTypes, 'chartType' => $chartType, 'startDate' => $startDate->format(Session::get(SESSION_DATE_FORMAT)), - 'endDate' => $endDate->modify('-1'.$padding)->format(Session::get(SESSION_DATE_FORMAT)), + 'endDate' => $endDate->format(Session::get(SESSION_DATE_FORMAT)), 'groupBy' => $groupBy, 'feature' => ACCOUNT_CHART_BUILDER, + 'displayData' => $displayData, + 'columns' => $columns, + 'reportTotals' => $reportTotals, + 'reportTypes' => $reportTypes, + 'reportType' => $reportType, + 'enableChart' => $enableChart, + 'enableReport' => $enableReport, ]; - return View::make('reports.report_builder', $params); + return View::make('reports.chart_builder', $params); + } + + private function export($data, $totals) + { + $output = fopen('php://output', 'w') or Utils::fatalError(); + header('Content-Type:application/csv'); + header('Content-Disposition:attachment;filename=ninja-report.csv'); + + Utils::exportData($output, $data); + + foreach (['amount', 'paid', 'balance'] as $type) { + $csv = trans("texts.{$type}") . ','; + foreach ($totals[$type] as $currencyId => $amount) { + $csv .= Utils::formatMoney($amount, $currencyId) . ','; + } + fwrite($output, $csv . "\n"); + } + + fclose($output); + exit; } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 509dbf1cce9b..72ffce9c8bf3 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -16,6 +16,7 @@ class Kernel extends HttpKernel { 'Illuminate\Session\Middleware\StartSession', 'Illuminate\View\Middleware\ShareErrorsFromSession', 'App\Http\Middleware\VerifyCsrfToken', + 'App\Http\Middleware\DuplicateSubmissionCheck', 'App\Http\Middleware\StartupCheck', ]; diff --git a/app/Http/Middleware/DuplicateSubmissionCheck.php b/app/Http/Middleware/DuplicateSubmissionCheck.php new file mode 100644 index 000000000000..2468f7ac9b77 --- /dev/null +++ b/app/Http/Middleware/DuplicateSubmissionCheck.php @@ -0,0 +1,30 @@ +path(); + + if (strpos($path, 'charts_and_reports') !== false) { + return $next($request); + } + + if (in_array($request->method(), ['POST', 'PUT', 'DELETE'])) { + $lastPage = session(SESSION_LAST_REQUEST_PAGE); + $lastTime = session(SESSION_LAST_REQUEST_TIME); + + if ($lastPage == $path && (microtime(true) - $lastTime <= 1)) { + return redirect('/')->with('warning', trans('texts.duplicate_post')); + } + + session([SESSION_LAST_REQUEST_PAGE => $request->path()]); + session([SESSION_LAST_REQUEST_TIME => microtime(true)]); + } + + return $next($request); + } +} \ No newline at end of file diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index e6e3a4ebefd7..41394b4cbd3f 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -1,6 +1,8 @@ auth->check()) + if ($this->auth->check() && Client::scope()->count() > 0) { + Session::reflash(); + return new RedirectResponse(url('/dashboard')); } diff --git a/app/Http/Middleware/StartupCheck.php b/app/Http/Middleware/StartupCheck.php index 361177721b25..bcf67ef0cef5 100644 --- a/app/Http/Middleware/StartupCheck.php +++ b/app/Http/Middleware/StartupCheck.php @@ -9,6 +9,8 @@ use Redirect; use Cache; use Session; use Event; +use App\Models\Language; +use App\Models\InvoiceDesign; use App\Events\UserSettingsChanged; class StartupCheck @@ -45,17 +47,17 @@ class StartupCheck 'languages' => 'App\Models\Language', 'paymentTerms' => 'App\Models\PaymentTerm', 'paymentTypes' => 'App\Models\PaymentType', + 'countries' => 'App\Models\Country', ]; foreach ($cachedTables as $name => $class) { if (!Cache::has($name)) { - $orderBy = 'id'; - if ($name == 'paymentTerms') { $orderBy = 'num_days'; - } elseif (property_exists($class, 'name') && $name != 'paymentTypes') { + } elseif (in_array($name, ['currencies', 'sizes', 'industries', 'languages', 'countries'])) { $orderBy = 'name'; + } else { + $orderBy = 'id'; } - Cache::forever($name, $class::orderBy($orderBy)->get()); } } @@ -74,12 +76,12 @@ class StartupCheck $data = @json_decode($file); } if ($data) { - if ($data->version != NINJA_VERSION) { + if (version_compare(NINJA_VERSION, $data->version, '<')) { $params = [ - 'user_version' => NINJA_VERSION, - 'latest_version' => $data->version, - 'releases_link' => link_to(RELEASES_URL, 'Invoice Ninja', ['target' => '_blank']), - ]; + 'user_version' => NINJA_VERSION, + 'latest_version' => $data->version, + 'releases_link' => link_to(RELEASES_URL, 'Invoice Ninja', ['target' => '_blank']), + ]; Session::put('news_feed_id', NEW_VERSION_AVAILABLE); Session::put('news_feed_message', trans('texts.new_version_available', $params)); } else { @@ -123,7 +125,7 @@ class StartupCheck $licenseKey = Input::get('license_key'); $productId = Input::get('product_id'); - $data = trim(file_get_contents((Utils::isNinjaDev() ? 'http://ninja.dev' : NINJA_APP_URL)."/claim_license?license_key={$licenseKey}&product_id={$productId}")); + $data = trim(file_get_contents((Utils::isNinjaDev() ? 'http://www.ninja.dev' : NINJA_APP_URL)."/claim_license?license_key={$licenseKey}&product_id={$productId}")); if ($productId == PRODUCT_INVOICE_DESIGNS) { if ($data = json_decode($data)) { diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index 07e0877060b8..bc70cdf6a810 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -6,11 +6,13 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier; class VerifyCsrfToken extends BaseVerifier { private $openRoutes = [ + 'signup/register', 'api/v1/clients', 'api/v1/invoices', 'api/v1/quotes', 'api/v1/payments', 'api/v1/email_invoice', + 'api/v1/hooks', ]; /** diff --git a/app/Http/routes.php b/app/Http/routes.php index bdca441d9bee..8ac55f85f620 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -1,5 +1,6 @@ 'signup', 'uses' => 'Auth\AuthController@getRegister')); post('/signup', array('as' => 'signup', 'uses' => 'Auth\AuthController@postRegister')); -get('/login', array('as' => 'login', 'uses' => 'Auth\AuthController@getLogin')); +get('/login', array('as' => 'login', 'uses' => 'Auth\AuthController@getLoginWrapper')); post('/login', array('as' => 'login', 'uses' => 'Auth\AuthController@postLoginWrapper')); get('/logout', array('as' => 'logout', 'uses' => 'Auth\AuthController@getLogout')); get('/forgot', array('as' => 'forgot', 'uses' => 'Auth\PasswordController@getEmail')); post('/forgot', array('as' => 'forgot', 'uses' => 'Auth\PasswordController@postEmail')); -get('/password/reset', array('as' => 'forgot', 'uses' => 'Auth\PasswordController@getReset')); +get('/password/reset/{token}', array('as' => 'forgot', 'uses' => 'Auth\PasswordController@getReset')); post('/password/reset', array('as' => 'forgot', 'uses' => 'Auth\PasswordController@postReset')); get('/user/confirm/{code}', 'UserController@confirm'); @@ -84,7 +85,7 @@ Route::post('user/reset', 'UserController@do_reset_password'); Route::get('logout', 'UserController@logout'); */ -if (\App\Libraries\Utils::isNinja()) { +if (Utils::isNinja()) { Route::post('/signup/register', 'AccountController@doRegister'); Route::get('/news_feed/{user_type}/{version}/', 'HomeController@newsFeed'); Route::get('/demo', 'AccountController@demo'); @@ -112,8 +113,8 @@ Route::group(['middleware' => 'auth'], function() { Route::get('products/{product_id}/archive', 'ProductController@archive'); Route::get('company/advanced_settings/data_visualizations', 'ReportController@d3'); - Route::get('company/advanced_settings/chart_builder', 'ReportController@report'); - Route::post('company/advanced_settings/chart_builder', 'ReportController@report'); + Route::get('company/advanced_settings/charts_and_reports', 'ReportController@showReports'); + Route::post('company/advanced_settings/charts_and_reports', 'ReportController@showReports'); Route::post('company/cancel_account', 'AccountController@cancelAccount'); Route::get('account/getSearchData', array('as' => 'getSearchData', 'uses' => 'AccountController@getSearchData')); @@ -179,10 +180,37 @@ Route::group(['middleware' => 'api', 'prefix' => 'api/v1'], function() Route::resource('invoices', 'InvoiceApiController'); Route::resource('quotes', 'QuoteApiController'); Route::resource('payments', 'PaymentApiController'); - Route::post('api/hooks', 'IntegrationController@subscribe'); + Route::post('hooks', 'IntegrationController@subscribe'); Route::post('email_invoice', 'InvoiceApiController@emailInvoice'); }); +// Redirects for legacy links +Route::get('/rocksteady', function() { + return Redirect::to(NINJA_WEB_URL, 301); +}); +Route::get('/about', function() { + return Redirect::to(NINJA_WEB_URL, 301); +}); +Route::get('/contact', function() { + return Redirect::to(NINJA_WEB_URL.'/contact', 301); +}); +Route::get('/plans', function() { + return Redirect::to(NINJA_WEB_URL.'/pricing', 301); +}); +Route::get('/faq', function() { + return Redirect::to(NINJA_WEB_URL.'/how-it-works', 301); +}); +Route::get('/features', function() { + return Redirect::to(NINJA_WEB_URL.'/features', 301); +}); +Route::get('/testimonials', function() { + return Redirect::to(NINJA_WEB_URL, 301); +}); +Route::get('/compare-online-invoicing{sites?}', function() { + return Redirect::to(NINJA_WEB_URL, 301); +}); + + define('CONTACT_EMAIL', Config::get('mail.from.address')); define('CONTACT_NAME', Config::get('mail.from.name')); define('SITE_URL', Config::get('app.url')); @@ -260,6 +288,7 @@ define('RANDOM_KEY_LENGTH', 32); define('MAX_NUM_CLIENTS', 500); define('MAX_NUM_CLIENTS_PRO', 20000); define('MAX_NUM_USERS', 20); +define('MAX_SUBDOMAIN_LENGTH', 30); define('INVOICE_STATUS_DRAFT', 1); define('INVOICE_STATUS_SENT', 2); @@ -285,6 +314,9 @@ define('SESSION_DATETIME_FORMAT', 'datetimeFormat'); define('SESSION_COUNTER', 'sessionCounter'); define('SESSION_LOCALE', 'sessionLocale'); +define('SESSION_LAST_REQUEST_PAGE', 'SESSION_LAST_REQUEST_PAGE'); +define('SESSION_LAST_REQUEST_TIME', 'SESSION_LAST_REQUEST_TIME'); + define('DEFAULT_TIMEZONE', 'US/Eastern'); define('DEFAULT_CURRENCY', 1); // US Dollar define('DEFAULT_DATE_FORMAT', 'M j, Y'); @@ -309,6 +341,7 @@ define('GATEWAY_TWO_CHECKOUT', 27); define('GATEWAY_BEANSTREAM', 29); define('GATEWAY_PSIGATE', 30); define('GATEWAY_MOOLAH', 31); +define('GATEWAY_BITPAY', 42); define('EVENT_CREATE_CLIENT', 1); define('EVENT_CREATE_INVOICE', 2); @@ -318,14 +351,15 @@ define('EVENT_CREATE_PAYMENT', 4); define('REQUESTED_PRO_PLAN', 'REQUESTED_PRO_PLAN'); define('DEMO_ACCOUNT_ID', 'DEMO_ACCOUNT_ID'); define('NINJA_ACCOUNT_KEY', 'zg4ylmzDkdkPOT8yoKQw9LTWaoZJx79h'); -define('NINJA_GATEWAY_ID', GATEWAY_AUTHORIZE_NET); -define('NINJA_GATEWAY_CONFIG', '{"apiLoginId":"626vWcD5","transactionKey":"4bn26TgL9r4Br4qJ","testMode":"","developerMode":""}'); +define('NINJA_GATEWAY_ID', GATEWAY_STRIPE); +define('NINJA_GATEWAY_CONFIG', ''); define('NINJA_WEB_URL', 'https://www.invoiceninja.com'); define('NINJA_APP_URL', 'https://app.invoiceninja.com'); -define('NINJA_VERSION', '1.7.2'); +define('NINJA_VERSION', '2.0.1'); define('NINJA_DATE', '2000-01-01'); define('NINJA_FROM_EMAIL', 'maildelivery@invoiceninja.com'); define('RELEASES_URL', 'https://github.com/hillelcoren/invoice-ninja/releases/'); +define('ZAPIER_URL', 'https://zapier.com/developer/invite/11276/85cf0ee4beae8e802c6c579eb4e351f1/'); define('COUNT_FREE_DESIGNS', 4); define('PRODUCT_ONE_CLICK_INSTALL', 1); @@ -351,6 +385,8 @@ define('TOKEN_BILLING_ALWAYS', 4); define('PAYMENT_TYPE_PAYPAL', 'PAYMENT_TYPE_PAYPAL'); define('PAYMENT_TYPE_CREDIT_CARD', 'PAYMENT_TYPE_CREDIT_CARD'); +define('PAYMENT_TYPE_BITCOIN', 'PAYMENT_TYPE_BITCOIN'); +define('PAYMENT_TYPE_TOKEN', 'PAYMENT_TYPE_TOKEN'); define('PAYMENT_TYPE_ANY', 'PAYMENT_TYPE_ANY'); /* @@ -472,7 +508,7 @@ Validator::extend('has_credit', function($attribute, $value, $parameters) { $publicClientId = $parameters[0]; $amount = $parameters[1]; - $client = Client::scope($publicClientId)->firstOrFail(); + $client = \App\Models\Client::scope($publicClientId)->firstOrFail(); $credit = $client->getTotalCredit(); return $credit >= $amount; @@ -511,4 +547,4 @@ if (Auth::check() && Auth::user()->id === 1) { Auth::loginUsingId(1); } -*/ +*/ \ No newline at end of file diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index d6789909c4c5..45ca2ac27c87 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -146,7 +146,9 @@ class Utils public static function getErrorString($exception) { - return "{$exception->getFile()} [Line {$exception->getLine()}] => {$exception->getMessage()}"; + $class = get_class($exception); + $code = method_exists($exception, 'getStatusCode') ? $exception->getStatusCode() : $exception->getCode(); + return "***{$class}*** [{$code}] : {$exception->getFile()} [Line {$exception->getLine()}] => {$exception->getMessage()}"; } public static function logError($error, $context = 'PHP') @@ -234,9 +236,13 @@ class Utils $currencyId = Session::get(SESSION_CURRENCY); } - $currency = Currency::find($currencyId); - - if(!$currency){ + foreach (Cache::get('currencies') as $currency) { + if ($currency->id == $currencyId) { + break; + } + } + + if (!$currency) { $currency = Currency::find(1); } @@ -484,7 +490,7 @@ class Utils public static function encodeActivity($person = null, $action, $entity = null, $otherPerson = null) { $person = $person ? $person->getDisplayName() : 'System'; - $entity = $entity ? '['.$entity->getActivityKey().']' : ''; + $entity = $entity ? $entity->getActivityKey() : ''; $otherPerson = $otherPerson ? 'to '.$otherPerson->getDisplayName() : ''; $token = Session::get('token_id') ? ' ('.trans('texts.token').')' : ''; @@ -621,4 +627,17 @@ class Utils return $str; } + + public static function exportData($output, $data) + { + if (count($data) > 0) { + fputcsv($output, array_keys($data[0])); + } + + foreach ($data as $record) { + fputcsv($output, $record); + } + + fwrite($output, "\n"); + } } diff --git a/app/Listeners/HandleInvoicePaid.php b/app/Listeners/HandleInvoicePaid.php index 159b001abad4..d072abd00357 100644 --- a/app/Listeners/HandleInvoicePaid.php +++ b/app/Listeners/HandleInvoicePaid.php @@ -31,8 +31,10 @@ class HandleInvoicePaid { */ public function handle(InvoicePaid $event) { - $this->contactMailer->sendPaymentConfirmation($payment); + $payment = $event->payment; $invoice = $payment->invoice; + + $this->contactMailer->sendPaymentConfirmation($payment); foreach ($invoice->account->users as $user) { diff --git a/app/Listeners/HandleInvoiceViewed.php b/app/Listeners/HandleInvoiceViewed.php index 3a3f6e323756..47ee62a8585a 100644 --- a/app/Listeners/HandleInvoiceViewed.php +++ b/app/Listeners/HandleInvoiceViewed.php @@ -34,7 +34,7 @@ class HandleInvoiceViewed { { if ($user->{'notify_viewed'}) { - $this->userMailer->sendNotification($user, $invoice, 'viewed', $payment); + $this->userMailer->sendNotification($user, $invoice, 'viewed'); } } } diff --git a/app/Models/Account.php b/app/Models/Account.php index ff896e30779e..9989227851b7 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -3,6 +3,7 @@ use Eloquent; use Utils; use Session; +use DateTime; use Illuminate\Database\Eloquent\SoftDeletes; @@ -113,9 +114,7 @@ class Account extends Eloquent foreach ($this->account_gateways as $gateway) { if (!$type || $type == PAYMENT_TYPE_ANY) { return $gateway; - } elseif ($gateway->isPayPal() && $type == PAYMENT_TYPE_PAYPAL) { - return $gateway; - } elseif (!$gateway->isPayPal() && $type == PAYMENT_TYPE_CREDIT_CARD) { + } elseif ($gateway->isPaymentType($type)) { return $gateway; } } @@ -230,6 +229,7 @@ class Account extends Eloquent 'subtotal', 'paid_to_date', 'balance_due', + 'amount_due', 'terms', 'your_invoice', 'quote', @@ -356,7 +356,8 @@ class Account extends Eloquent public function getEmailFooter() { if ($this->email_footer) { - return $this->email_footer; + // Add line breaks if HTML isn't already being used + return strip_tags($this->email_footer) == $this->email_footer ? nl2br($this->email_footer) : $this->email_footer; } else { return "
" . trans('texts.email_signature') . "\$account
Enviar facturas automáticamente a clientes semanalmente, bi-mensualmente, mensualmente, trimestral o anualmente.
Uso :MONTH, :QUARTER or :YEAR para fechas dinámicas. Matemáticas básicas también funcionan bien. Por ejemplo: :MONTH-1.
Ejemplos de variables dinámicas de factura:
{!! Former::text('email')->placeholder(trans('texts.email_address'))->raw() !!} @@ -72,6 +83,7 @@
{!! link_to('/forgot', trans('texts.forgot_password')) !!} + {!! link_to(NINJA_WEB_URL.'/knowledgebase/', trans('texts.knowledge_base'), ['target' => '_blank', 'class' => 'pull-right']) !!}
+ @stop \ No newline at end of file diff --git a/resources/views/header.blade.php b/resources/views/header.blade.php index 073700d45d68..104704e7fdc5 100644 --- a/resources/views/header.blade.php +++ b/resources/views/header.blade.php @@ -19,10 +19,263 @@ } } + @media screen and (max-width: 768px) { + body { + padding-top: 56px; + } + } + @include('script') + + @stop @section('body') @@ -59,7 +312,7 @@ @if (!Auth::user()->registered) {!! Button::success(trans('texts.sign_up'))->withAttributes(array('id' => 'signUpButton', 'data-toggle'=>'modal', 'data-target'=>'#signUpModal'))->small() !!} @elseif (!Auth::user()->isPro()) - {!! Button::success(trans('texts.go_pro'))->withAttributes(array('id' => 'proPlanButton', 'data-toggle'=>'modal', 'data-target'=>'#proPlanModal'))->small() !!} + {!! Button::success(trans('texts.go_pro'))->withAttributes(array('id' => 'proPlanButton', 'onclick' => 'submitProPlan("")'))->small() !!} @endif @endif @@ -113,7 +366,8 @@
sudo chown yourname:www-data /path/to/ninja