diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 950cf01b7df0..9abe90b45cbc 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -9,7 +9,6 @@ use DateInterval; use DatePeriod; use Session; use View; - use App\Models\Account; class ReportController extends BaseController @@ -17,7 +16,7 @@ class ReportController extends BaseController public function d3() { $message = ''; - $fileName = storage_path() . '/dataviz_sample.txt'; + $fileName = storage_path().'/dataviz_sample.txt'; if (Auth::user()->account->isPro()) { $account = Account::where('id', '=', Auth::user()->account->id) @@ -55,200 +54,21 @@ class ReportController extends BaseController } else { $groupBy = 'MONTH'; $chartType = 'Bar'; - $reportType = ''; + $reportType = ENTITY_INVOICE; $startDate = Utils::today(false)->modify('-3 month'); $endDate = Utils::today(false); $enableReport = true; $enableChart = true; } - $datasets = []; - $labels = []; - $maxTotals = 0; - $width = 10; - $displayData = []; $exportData = []; $reportTotals = [ 'amount' => [], 'balance' => [], - 'paid' => [] + 'paid' => [], ]; - if ($reportType) { - $columns = ['client', 'amount', 'paid', 'balance']; - } else { - $columns = ['client', 'invoice_number', 'invoice_date', 'amount', 'paid', 'balance']; - } - - - if (Auth::user()->account->isPro()) { - - 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(); - - 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; - } - - if ($action == 'export') - { - self::export($exportData, $reportTotals); - } - } - - if ($enableChart) - { - foreach ([ENTITY_INVOICE, ENTITY_PAYMENT, ENTITY_CREDIT] as $entityType) - { - // SQLite does not support the YEAR(), MONTH(), WEEK() and similar functions. - // Let's see if SQLite is being used. - if (Config::get('database.connections.'.Config::get('database.default').'.driver') == 'sqlite') - { - // Replace the unsupported function with it's date format counterpart - switch ($groupBy) - { - case 'MONTH': - $dateFormat = '%m'; // returns 01-12 - break; - case 'WEEK': - $dateFormat = '%W'; // returns 00-53 - break; - case 'DAYOFYEAR': - $dateFormat = '%j'; // returns 001-366 - break; - default: - $dateFormat = '%m'; // MONTH by default - break; - } - - // Concatenate the year and the chosen timeframe (Month, Week or Day) - $timeframe = 'strftime("%Y", '.$entityType.'_date) || strftime("'.$dateFormat.'", '.$entityType.'_date)'; - } - else - { - // Supported by Laravel's other DBMS drivers (MySQL, MSSQL and PostgreSQL) - $timeframe = 'concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date))'; - } - - $records = DB::table($entityType.'s') - ->select(DB::raw('sum(amount) as total, '.$timeframe.' 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'); - // MySQL returns 1-366 for DAYOFYEAR, whereas PHP returns 0-365 - $date = $groupBy == 'DAYOFYEAR' ? $d->format('Y') . ($d->format($dateFormat) + 1) : $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 = [ 'DAYOFYEAR' => 'Daily', 'WEEK' => 'Weekly', @@ -261,23 +81,18 @@ class ReportController extends BaseController ]; $reportTypes = [ - '' => '', - 'Client' => trans('texts.client') + ENTITY_CLIENT => trans('texts.client'), + ENTITY_INVOICE => trans('texts.invoice'), + ENTITY_PAYMENT => trans('texts.payment'), ]; $params = [ - 'labels' => $labels, - 'datasets' => $datasets, - 'scaleStepWidth' => $width, 'dateTypes' => $dateTypes, 'chartTypes' => $chartTypes, 'chartType' => $chartType, 'startDate' => $startDate->format(Session::get(SESSION_DATE_FORMAT)), 'endDate' => $endDate->format(Session::get(SESSION_DATE_FORMAT)), 'groupBy' => $groupBy, - 'displayData' => $displayData, - 'columns' => $columns, - 'reportTotals' => $reportTotals, 'reportTypes' => $reportTypes, 'reportType' => $reportType, 'enableChart' => $enableChart, @@ -285,9 +100,244 @@ class ReportController extends BaseController 'title' => trans('texts.charts_and_reports'), ]; + if (Auth::user()->account->isPro()) { + if ($enableReport) { + $params = array_merge($params, self::generateReport($reportType, $groupBy, $startDate, $endDate)); + + if ($action == 'export') { + self::export($params['exportData'], $params['reportTotals']); + } + } + if ($enableChart) { + $params = array_merge($params, self::generateChart($groupBy, $startDate, $endDate)); + } + } + return View::make('reports.chart_builder', $params); } + private function generateChart($groupBy, $startDate, $endDate) + { + $width = 10; + $datasets = []; + $labels = []; + $maxTotals = 0; + + foreach ([ENTITY_INVOICE, ENTITY_PAYMENT, ENTITY_CREDIT] as $entityType) { + // SQLite does not support the YEAR(), MONTH(), WEEK() and similar functions. + // Let's see if SQLite is being used. + if (Config::get('database.connections.'.Config::get('database.default').'.driver') == 'sqlite') { + // Replace the unsupported function with it's date format counterpart + switch ($groupBy) { + case 'MONTH': + $dateFormat = '%m'; // returns 01-12 + break; + case 'WEEK': + $dateFormat = '%W'; // returns 00-53 + break; + case 'DAYOFYEAR': + $dateFormat = '%j'; // returns 001-366 + break; + default: + $dateFormat = '%m'; // MONTH by default + break; + } + + // Concatenate the year and the chosen timeframe (Month, Week or Day) + $timeframe = 'strftime("%Y", '.$entityType.'_date) || strftime("'.$dateFormat.'", '.$entityType.'_date)'; + } else { + // Supported by Laravel's other DBMS drivers (MySQL, MSSQL and PostgreSQL) + $timeframe = 'concat(YEAR('.$entityType.'_date), '.$groupBy.'('.$entityType.'_date))'; + } + + $records = DB::table($entityType.'s') + ->select(DB::raw('sum(amount) as total, '.$timeframe.' 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'); + // MySQL returns 1-366 for DAYOFYEAR, whereas PHP returns 0-365 + $date = $groupBy == 'DAYOFYEAR' ? $d->format('Y').($d->format($dateFormat) + 1) : $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); + + return [ + 'datasets' => $datasets, + 'scaleStepWidth' => $width, + 'labels' => $labels, + ]; + } + + private function generateReport($reportType, $groupBy, $startDate, $endDate) + { + if ($reportType == ENTITY_CLIENT) { + $columns = ['client', 'amount', 'paid', 'balance']; + } elseif ($reportType == ENTITY_INVOICE) { + $columns = ['client', 'invoice_number', 'invoice_date', 'amount', 'paid', 'balance']; + } else { + $columns = ['client', 'invoice_number', 'invoice_date', 'amount', 'payment_date', 'paid', 'method']; + } + + $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 == ENTITY_CLIENT) { + $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 { + $query->orderBy('invoices.id'); + array_push($select, 'invoices.invoice_number', 'invoices.amount', 'invoices.balance', 'invoices.invoice_date'); + if ($reportType == ENTITY_INVOICE) { + array_push($select, DB::raw('(invoices.amount - invoices.balance) paid')); + } else { + $query->join('payments', 'payments.invoice_id', '=', 'invoices.id') + ->leftJoin('payment_types', 'payment_types.id', '=', 'payments.payment_type_id') + ->leftJoin('account_gateways', 'account_gateways.id', '=', 'payments.account_gateway_id') + ->leftJoin('gateways', 'gateways.id', '=', 'account_gateways.gateway_id'); + array_push($select, 'payments.payment_date', 'payments.amount as paid', 'payment_types.name as payment_type', 'gateways.name as gateway'); + } + } + + $query->select($select); + $data = $query->get(); + + $lastInvoiceId = null; + $sameAsLast = false; + + foreach ($data as $record) { + $sameAsLast = ($lastInvoiceId == $record->invoice_public_id); + $lastInvoiceId = $record->invoice_public_id; + + $displayRow = []; + if ($sameAsLast) { + array_push($displayRow, '', '', '', ''); + } else { + array_push($displayRow, link_to('/clients/'.$record->client_public_id, Utils::getClientDisplayName($record))); + if ($reportType != ENTITY_CLIENT) { + 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)); + } + if ($reportType != ENTITY_PAYMENT) { + array_push($displayRow, Utils::formatMoney($record->paid, $record->currency_id)); + } + if ($reportType == ENTITY_PAYMENT) { + array_push($displayRow, + Utils::fromSqlDate($record->payment_date, true), + Utils::formatMoney($record->paid, $record->currency_id), + $record->gateway ?: $record->payment_type + ); + } else { + array_push($displayRow, Utils::formatMoney($record->balance, $record->currency_id)); + } + + // export data + $exportRow = []; + if ($sameAsLast) { + $exportRow[trans('texts.client')] = ' '; + $exportRow[trans('texts.invoice_number')] = ' '; + $exportRow[trans('texts.invoice_date')] = ' '; + $exportRow[trans('texts.amount')] = ' '; + } else { + $exportRow[trans('texts.client')] = Utils::getClientDisplayName($record); + if ($reportType != ENTITY_CLIENT) { + $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); + } + if ($reportType != ENTITY_PAYMENT) { + $exportRow[trans('texts.paid')] = Utils::formatMoney($record->paid, $record->currency_id); + } + if ($reportType == ENTITY_PAYMENT) { + $exportRow[trans('texts.payment_date')] = Utils::fromSqlDate($record->payment_date, true); + $exportRow[trans('texts.payment_amount')] = Utils::formatMoney($record->paid, $record->currency_id); + $exportRow[trans('texts.method')] = $record->gateway ?: $record->payment_type; + } else { + $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; + } + if (!$sameAsLast) { + $reportTotals['amount'][$currencyId] += $record->amount; + $reportTotals['balance'][$currencyId] += $record->balance; + } + $reportTotals['paid'][$currencyId] += $record->paid; + } + + return [ + 'columns' => $columns, + 'displayData' => $displayData, + 'reportTotals' => $reportTotals, + 'exportData' => $exportData + ]; + } + private function export($data, $totals) { $output = fopen('php://output', 'w') or Utils::fatalError(); @@ -297,11 +347,11 @@ class ReportController extends BaseController Utils::exportData($output, $data); foreach (['amount', 'paid', 'balance'] as $type) { - $csv = trans("texts.{$type}") . ','; + $csv = trans("texts.{$type}").','; foreach ($totals[$type] as $currencyId => $amount) { - $csv .= Utils::formatMoney($amount, $currencyId) . ','; + $csv .= Utils::formatMoney($amount, $currencyId).','; } - fwrite($output, $csv . "\n"); + fwrite($output, $csv."\n"); } fclose($output); diff --git a/app/Models/Account.php b/app/Models/Account.php index 506f00a6e2d1..478057883956 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -238,6 +238,11 @@ class Account extends Eloquent return file_exists($fileName.'.png') ? $fileName.'.png' : $fileName.'.jpg'; } + public function getLogoURL() + { + return SITE_URL . '/' . $this->getLogoPath(); + } + public function getToken($name) { foreach ($this->account_tokens as $token) { diff --git a/resources/views/emails/view_action.blade.php b/resources/views/emails/view_action.blade.php index ac4b145dda66..1a4abf04c318 100644 --- a/resources/views/emails/view_action.blade.php +++ b/resources/views/emails/view_action.blade.php @@ -20,6 +20,10 @@ "totalPaymentDue": { "@type": "PriceSpecification", "price": "{{ $invoice->present()->balance_due }}" + }, + "action": { + "@type": "ViewAction", + "url": "{!! $link !!}" } }, @endif @@ -29,7 +33,7 @@ "action": { "@type": "ViewAction", "url": "{!! $link !!}", - "name": "{{ trans("view_{$entityType}") }}" + "name": "{{ trans("texts.view_{$entityType}") }}" } } ] diff --git a/resources/views/reports/chart_builder.blade.php b/resources/views/reports/chart_builder.blade.php index 01f6778fcbbc..0b4e7fad9f00 100644 --- a/resources/views/reports/chart_builder.blade.php +++ b/resources/views/reports/chart_builder.blade.php @@ -97,7 +97,7 @@ {{ trans('texts.totals') }} - @if (!$reportType) + @if ($reportType != ENTITY_CLIENT) @endif @@ -106,17 +106,22 @@ {{ Utils::formatMoney($total, $currencyId) }}
@endforeach + @if ($reportType == ENTITY_PAYMENT) + + @endif @foreach ($reportTotals['paid'] as $currencyId => $total) {{ Utils::formatMoney($total, $currencyId) }}
@endforeach + @if ($reportType != ENTITY_PAYMENT) @foreach ($reportTotals['balance'] as $currencyId => $total) {{ Utils::formatMoney($total, $currencyId) }}
@endforeach - + @endif +