diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 0eeb3170f5..371086bef4 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -98,7 +98,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')); } @@ -343,40 +343,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'); diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index d58021798d..18f99a0d78 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -187,7 +187,7 @@ class InvoiceController extends BaseController $server = explode('.', Request::server('HTTP_HOST')); $subdomain = $server[0]; - if ($subdomain != 'app' && $subdomain != $account->subdomain) { + if (!in_array($subdomain, ['app', 'www']) && $subdomain != $account->subdomain) { return View::make('invoices.deleted'); } } diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index bfbeba8cc6..cf4e4cce81 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -37,18 +37,26 @@ 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'); @@ -58,55 +66,142 @@ class ReportController extends BaseController $maxTotals = 0; $width = 10; + $displayData = []; + $exportData = []; + $columns = []; + $reportTotals = [ + 'amount' => [], + 'balance' => [], + 'paid' => [] + ]; + + 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')); + $columns = ['client', 'amount', 'paid', 'balance']; + } 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'); + $columns = ['client', 'invoice_number', 'invoice_date', 'amount', 'paid', 'balance']; } + + $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, '.$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); + + $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; + } + } + + $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 +215,11 @@ class ReportController extends BaseController 'Line' => 'Line', ]; + $reportTypes = [ + '' => '', + 'Client' => trans('texts.client') + ]; + $params = [ 'labels' => $labels, 'datasets' => $datasets, @@ -131,8 +231,35 @@ class ReportController extends BaseController 'endDate' => $endDate->modify('-1'.$padding)->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/Middleware/DuplicateSubmissionCheck.php b/app/Http/Middleware/DuplicateSubmissionCheck.php index 527b462d7e..782c9209dd 100644 --- a/app/Http/Middleware/DuplicateSubmissionCheck.php +++ b/app/Http/Middleware/DuplicateSubmissionCheck.php @@ -7,11 +7,17 @@ class DuplicateSubmissionCheck // Prevent users from submitting forms twice public function handle($request, Closure $next) { + $path = $request->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 == $request->path() && (microtime(true) - $lastTime <= 1.5)) { + if ($lastPage == $path && (microtime(true) - $lastTime <= 1.5)) { return redirect('/')->with('warning', trans('texts.duplicate_post')); } diff --git a/app/Http/Middleware/StartupCheck.php b/app/Http/Middleware/StartupCheck.php index 0fa1d6c77f..a5f93b432f 100644 --- a/app/Http/Middleware/StartupCheck.php +++ b/app/Http/Middleware/StartupCheck.php @@ -66,7 +66,8 @@ class StartupCheck $count = Session::get(SESSION_COUNTER, 0); Session::put(SESSION_COUNTER, ++$count); - if (!Utils::startsWith($_SERVER['REQUEST_URI'], '/news_feed') && !Session::has('news_feed_id')) { + //if (!Utils::startsWith($_SERVER['REQUEST_URI'], '/news_feed') && !Session::has('news_feed_id')) { + if (true) { $data = false; if (Utils::isNinja()) { $data = Utils::getNewsFeedResponse(); @@ -75,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 { diff --git a/app/Http/routes.php b/app/Http/routes.php index 33bbe3f249..9e1a441866 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -112,8 +112,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')); @@ -354,10 +354,11 @@ define('NINJA_GATEWAY_ID', GATEWAY_AUTHORIZE_NET); 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.0'); 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); diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index e3c95a2ab3..726d0bc6fc 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -623,4 +623,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/config/session.php b/config/session.php index 47470fabc7..417b506ccb 100644 --- a/config/session.php +++ b/config/session.php @@ -44,7 +44,7 @@ return [ | */ - 'encrypt' => false, + 'encrypt' => env('SESSION_ENCRYPT', false), /* |-------------------------------------------------------------------------- @@ -109,7 +109,7 @@ return [ | */ - 'cookie' => 'laravel_session', + 'cookie' => 'ninja_session', /* |-------------------------------------------------------------------------- @@ -135,7 +135,7 @@ return [ | */ - 'domain' => null, + 'domain' => env('SESSION_DOMAIN', null), /* |-------------------------------------------------------------------------- @@ -148,6 +148,6 @@ return [ | */ - 'secure' => false, + 'secure' => env('SESSION_SECURE', false), ]; diff --git a/public/css/built.css b/public/css/built.css index 35c8b6446a..a9c4a4d36b 100644 --- a/public/css/built.css +++ b/public/css/built.css @@ -3238,11 +3238,15 @@ div.checkbox > label { z-index: 9999; } -.dataTables_length { +div.dataTables_length { padding-left: 20px; - padding-top: 8px; + padding-top: 10px; } -.dataTables_length label { +div.dataTables_length select { + background-color: white !important; +} + +div.dataTables_length label { font-weight: 500; } \ No newline at end of file diff --git a/public/css/style.css b/public/css/style.css index be1631fb97..78f2acf992 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -854,11 +854,15 @@ div.checkbox > label { z-index: 9999; } -.dataTables_length { +div.dataTables_length { padding-left: 20px; - padding-top: 8px; + padding-top: 10px; } -.dataTables_length label { +div.dataTables_length select { + background-color: white !important; +} + +div.dataTables_length label { font-weight: 500; } \ No newline at end of file diff --git a/readme.md b/readme.md index 631561cf95..9c308b8ed0 100644 --- a/readme.md +++ b/readme.md @@ -4,6 +4,16 @@ If you'd like to use our code to sell your own invoicing app we have an affiliate program. Get in touch for more details. +### Introduction + +To setup the site you can either use the [zip file](https://www.invoiceninja.com/knowledgebase/self-host/) (easier to run) or checkout the code from GitHub (easier to make changes). + +For updates follow [@invoiceninja](https://twitter.com/invoiceninja) or join the [Facebook Group](https://www.facebook.com/invoiceninja). For discussion of the code please use the [Google Group](https://groups.google.com/d/forum/invoiceninja). + +If you'd like to translate the site please use [caouecs/Laravel4-long](https://github.com/caouecs/Laravel4-lang) for the starter files. + +Developed by [@hillelcoren](https://twitter.com/hillelcoren) | Designed by [kantorp-wegl.in](http://kantorp-wegl.in/). + ### Features * Core application built using Laravel 5 diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 83375c7cdc..2f7c020148 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -637,5 +637,18 @@ return array( 'www' => 'www', 'logo' => 'Logo', 'subdomain' => 'Subdomain', + 'provide_name_or_email' => 'Please provide a contact name or email', + 'charts_and_reports' => 'Charts & Reports', + 'chart' => 'Chart', + 'report' => 'Report', + 'group_by' => 'Group by', + 'paid' => 'Paid', + 'enable_report' => 'Report', + 'enable_chart' => 'Chart', + 'totals' => 'Totals', + 'run' => 'Run', + 'export' => 'Export', + 'documentation' => 'Documentation', + 'zapier' => 'Zapier Beta', ); diff --git a/resources/views/accounts/nav_advanced.blade.php b/resources/views/accounts/nav_advanced.blade.php index 61b3312618..928462ca12 100644 --- a/resources/views/accounts/nav_advanced.blade.php +++ b/resources/views/accounts/nav_advanced.blade.php @@ -2,7 +2,7 @@ {!! HTML::nav_link('company/advanced_settings/invoice_settings', 'invoice_settings') !!} {!! HTML::nav_link('company/advanced_settings/invoice_design', 'invoice_design') !!} {!! HTML::nav_link('company/advanced_settings/email_templates', 'email_templates') !!} - {!! HTML::nav_link('company/advanced_settings/chart_builder', 'chart_builder') !!} + {!! HTML::nav_link('company/advanced_settings/charts_and_reports', 'charts_and_reports') !!} {!! HTML::nav_link('company/advanced_settings/user_management', 'users_and_tokens') !!}

 

diff --git a/resources/views/accounts/token_management.blade.php b/resources/views/accounts/token_management.blade.php index 876ca6d5a6..66f6901bad 100644 --- a/resources/views/accounts/token_management.blade.php +++ b/resources/views/accounts/token_management.blade.php @@ -13,7 +13,8 @@
- {!! Button::normal(trans('texts.view_documentation'))->asLinkTo(NINJA_WEB_URL.'/knowledgebase/api-documentation/')->withAttributes(['target' => '_blank']) !!} + {!! Button::normal(trans('texts.documentation'))->asLinkTo(NINJA_WEB_URL.'/knowledgebase/api-documentation/')->withAttributes(['target' => '_blank']) !!} + {!! Button::normal(trans('texts.zapier'))->asLinkTo(ZAPIER_URL)->withAttributes(['target' => '_blank']) !!} @if (Utils::isPro()) {!! Button::primary(trans('texts.add_token'))->asLinkTo('/tokens/create')->appendIcon(Icon::create('plus-sign')) !!} @endif diff --git a/resources/views/header.blade.php b/resources/views/header.blade.php index 10b275ab7f..70bb49687c 100644 --- a/resources/views/header.blade.php +++ b/resources/views/header.blade.php @@ -215,7 +215,7 @@ $(function() { window.setTimeout(function() { $(".alert-hide").fadeOut(500); - }, 3000); + }, 2000); $('#search').focus(function(){ if (!window.hasOwnProperty('searchData')) { diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index 97e387dd3e..fe2a9868dd 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -473,7 +473,7 @@
{!! DropdownButton::normal(trans('texts.archive'))->withContents([ - ['label' => trans('texts.archive_'.$entityType), 'url' => "javascript:submitForm('archive')"], - ['label' => trans('texts.delete_'.$entityType), 'url' => "javascript:submitForm('delete')"], + ['label' => trans('texts.archive_'.$entityType), 'url' => 'javascript:submitForm("archive")'], + ['label' => trans('texts.delete_'.$entityType), 'url' => 'javascript:submitForm("delete")'], ])->withAttributes(['class'=>'archive'])->split() !!}