mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-08 12:12:48 +01:00
Improvements to reports
This commit is contained in:
parent
faa1889d19
commit
a63f694d09
@ -12,7 +12,7 @@
|
||||
|
||||
Watch this [YouTube Video](https://www.youtube.com/watch?v=xHGKvadapbA) for an overview of the app's features.
|
||||
|
||||
All Pro and Enterprise features from the hosted app are included in the open-source code.
|
||||
All Pro and Enterprise features from the hosted app are included in the open-source code. We offer a $20 per year white-label license to remove our branding.
|
||||
|
||||
The [self-host zip](https://www.invoiceninja.com/self-host/) includes all third party libraries whereas downloading the code from GitHub requires using Composer to install the dependencies.
|
||||
|
||||
|
@ -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;
|
||||
|
@ -247,8 +247,8 @@ Route::group([
|
||||
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');
|
||||
|
@ -117,7 +117,7 @@ class Account extends Eloquent
|
||||
ACCOUNT_EMAIL_SETTINGS,
|
||||
ACCOUNT_TEMPLATES_AND_REMINDERS,
|
||||
ACCOUNT_BANKS,
|
||||
ACCOUNT_REPORTS,
|
||||
//ACCOUNT_REPORTS,
|
||||
ACCOUNT_DATA_VISUALIZATIONS,
|
||||
ACCOUNT_API_TOKENS,
|
||||
ACCOUNT_USER_MANAGEMENT,
|
||||
|
@ -289,6 +289,7 @@ class EntityModel extends Eloquent
|
||||
'vendors' => 'building',
|
||||
'settings' => 'cog',
|
||||
'self-update' => 'download',
|
||||
'reports' => 'th-list',
|
||||
];
|
||||
|
||||
return array_get($icons, $entityType);
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php namespace App\Ninja\Presenters;
|
||||
|
||||
use Utils;
|
||||
use Carbon;
|
||||
|
||||
/**
|
||||
* Class ExpensePresenter
|
||||
@ -24,6 +25,11 @@ class ExpensePresenter extends EntityPresenter
|
||||
return Utils::fromSqlDate($this->entity->expense_date);
|
||||
}
|
||||
|
||||
public function month()
|
||||
{
|
||||
return Carbon::parse($this->entity->payment_date)->format('Y m');
|
||||
}
|
||||
|
||||
public function amount()
|
||||
{
|
||||
return Utils::formatMoney($this->entity->amount, $this->entity->expense_currency_id);
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?php namespace App\Ninja\Presenters;
|
||||
|
||||
use Carbon;
|
||||
use stdClass;
|
||||
use Utils;
|
||||
use DropdownButton;
|
||||
@ -44,6 +45,38 @@ class InvoicePresenter extends EntityPresenter {
|
||||
}
|
||||
}
|
||||
|
||||
public function age()
|
||||
{
|
||||
if ( ! $this->entity->due_date || $this->entity->date_date == '0000-00-00') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$date = Carbon::parse($this->entity->due_date);
|
||||
|
||||
if ($date->isFuture()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $date->diffInDays();
|
||||
}
|
||||
|
||||
public function ageGroup()
|
||||
{
|
||||
$age = $this->age();
|
||||
|
||||
if ($age > 120) {
|
||||
return 'age_group_120';
|
||||
} elseif ($age > 90) {
|
||||
return 'age_group_90';
|
||||
} elseif ($age > 60) {
|
||||
return 'age_group_60';
|
||||
} elseif ($age > 30) {
|
||||
return 'age_group_30';
|
||||
} else {
|
||||
return 'age_group_0';
|
||||
}
|
||||
}
|
||||
|
||||
public function dueDateLabel()
|
||||
{
|
||||
if ($this->entity->isType(INVOICE_TYPE_STANDARD)) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?php namespace App\Ninja\Presenters;
|
||||
|
||||
use Carbon;
|
||||
use Utils;
|
||||
|
||||
class PaymentPresenter extends EntityPresenter {
|
||||
@ -19,6 +20,11 @@ class PaymentPresenter extends EntityPresenter {
|
||||
return Utils::fromSqlDate($this->entity->payment_date);
|
||||
}
|
||||
|
||||
public function month()
|
||||
{
|
||||
return Carbon::parse($this->entity->payment_date)->format('Y m');
|
||||
}
|
||||
|
||||
public function method()
|
||||
{
|
||||
if ($this->entity->account_gateway) {
|
||||
|
84
app/Ninja/Reports/AbstractReport.php
Normal file
84
app/Ninja/Reports/AbstractReport.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Reports;
|
||||
|
||||
use Auth;
|
||||
|
||||
class AbstractReport
|
||||
{
|
||||
public $startDate;
|
||||
public $endDate;
|
||||
public $isExport;
|
||||
public $options;
|
||||
|
||||
public $totals = [];
|
||||
public $columns = [];
|
||||
public $data = [];
|
||||
|
||||
public function __construct($startDate, $endDate, $isExport, $options = false)
|
||||
{
|
||||
$this->startDate = $startDate;
|
||||
$this->endDate = $endDate;
|
||||
$this->isExport = $isExport;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
public function run()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function results()
|
||||
{
|
||||
return [
|
||||
'columns' => $this->columns,
|
||||
'displayData' => $this->data,
|
||||
'reportTotals' => $this->totals,
|
||||
];
|
||||
}
|
||||
|
||||
protected function addToTotals($currencyId, $field, $value, $dimension = false)
|
||||
{
|
||||
$currencyId = $currencyId ?: Auth::user()->account->getCurrencyId();
|
||||
|
||||
if ( ! isset($this->totals[$currencyId][$dimension])) {
|
||||
$this->totals[$currencyId][$dimension] = [];
|
||||
}
|
||||
|
||||
if ( ! isset($this->totals[$currencyId][$dimension][$field])) {
|
||||
$this->totals[$currencyId][$dimension][$field] = 0;
|
||||
}
|
||||
|
||||
$this->totals[$currencyId][$dimension][$field] += $value;
|
||||
}
|
||||
|
||||
public function tableHeader()
|
||||
{
|
||||
$str = '';
|
||||
|
||||
foreach ($this->columns as $key => $val) {
|
||||
if (is_array($val)) {
|
||||
$field = $key;
|
||||
$class = $val;
|
||||
} else {
|
||||
$field = $val;
|
||||
$class = [];
|
||||
}
|
||||
|
||||
if (strpos($field, 'date') !== false) {
|
||||
//$class[] = 'group-date-monthyear';
|
||||
$class[] = 'group-date-' . (isset($this->options['group_dates_by']) ? $this->options['group_dates_by'] : 'monthyear');
|
||||
} elseif (in_array($field, ['client', 'method'])) {
|
||||
$class[] = 'group-letter-100';
|
||||
} elseif (in_array($field, ['amount', 'paid', 'balance'])) {
|
||||
$class[] = 'group-number-50';
|
||||
}
|
||||
|
||||
$class = count($class) ? implode(' ', $class) : 'group-false';
|
||||
$label = trans("texts.{$field}");
|
||||
$str .= "<th class=\"{$class}\">{$label}</th>";
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
}
|
58
app/Ninja/Reports/AgingReport.php
Normal file
58
app/Ninja/Reports/AgingReport.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Reports;
|
||||
|
||||
use Auth;
|
||||
use App\Models\Client;
|
||||
|
||||
class AgingReport extends AbstractReport
|
||||
{
|
||||
public $columns = [
|
||||
'client',
|
||||
'invoice_number',
|
||||
'invoice_date',
|
||||
'due_date',
|
||||
'age' => ['group-number-30'],
|
||||
'amount',
|
||||
'balance',
|
||||
];
|
||||
|
||||
public function run()
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
|
||||
$clients = Client::scope()
|
||||
->withArchived()
|
||||
->with('contacts')
|
||||
->with(['invoices' => function($query) {
|
||||
$query->invoices()
|
||||
->whereIsPublic(true)
|
||||
->withArchived()
|
||||
->where('balance', '>', 0)
|
||||
->where('invoice_date', '>=', $this->startDate)
|
||||
->where('invoice_date', '<=', $this->endDate)
|
||||
->with(['invoice_items']);
|
||||
}]);
|
||||
|
||||
foreach ($clients->get() as $client) {
|
||||
foreach ($client->invoices as $invoice) {
|
||||
|
||||
$this->data[] = [
|
||||
$this->isExport ? $client->getDisplayName() : $client->present()->link,
|
||||
$this->isExport ? $invoice->invoice_number : $invoice->present()->link,
|
||||
$invoice->present()->invoice_date,
|
||||
$invoice->present()->due_date,
|
||||
$invoice->present()->age,
|
||||
$account->formatMoney($invoice->amount, $client),
|
||||
$account->formatMoney($invoice->balance, $client),
|
||||
];
|
||||
|
||||
$this->addToTotals($client->currency_id, $invoice->present()->ageGroup, $invoice->balance);
|
||||
|
||||
//$this->addToTotals($client->currency_id, 'paid', $payment ? $payment->getCompletedAmount() : 0);
|
||||
//$this->addToTotals($client->currency_id, 'amount', $invoice->amount);
|
||||
//$this->addToTotals($client->currency_id, 'balance', $invoice->balance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
53
app/Ninja/Reports/ClientReport.php
Normal file
53
app/Ninja/Reports/ClientReport.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Reports;
|
||||
|
||||
use Auth;
|
||||
use App\Models\Client;
|
||||
|
||||
class ClientReport extends AbstractReport
|
||||
{
|
||||
public $columns = [
|
||||
'client',
|
||||
'amount',
|
||||
'paid',
|
||||
'balance',
|
||||
];
|
||||
|
||||
public function run()
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
|
||||
$clients = Client::scope()
|
||||
->withArchived()
|
||||
->with('contacts')
|
||||
->with(['invoices' => function($query) {
|
||||
$query->where('invoice_date', '>=', $this->startDate)
|
||||
->where('invoice_date', '<=', $this->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();
|
||||
}
|
||||
|
||||
$this->data[] = [
|
||||
$this->isExport ? $client->getDisplayName() : $client->present()->link,
|
||||
$account->formatMoney($amount, $client),
|
||||
$account->formatMoney($paid, $client),
|
||||
$account->formatMoney($amount - $paid, $client)
|
||||
];
|
||||
|
||||
$this->addToTotals($client->currency_id, 'amount', $amount);
|
||||
$this->addToTotals($client->currency_id, 'paid', $paid);
|
||||
$this->addToTotals($client->currency_id, 'balance', $amount - $paid);
|
||||
}
|
||||
}
|
||||
}
|
44
app/Ninja/Reports/ExpenseReport.php
Normal file
44
app/Ninja/Reports/ExpenseReport.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Reports;
|
||||
|
||||
use Auth;
|
||||
use Utils;
|
||||
use App\Models\Expense;
|
||||
|
||||
class ExpenseReport extends AbstractReport
|
||||
{
|
||||
public $columns = [
|
||||
'vendor',
|
||||
'client',
|
||||
'date',
|
||||
'category',
|
||||
'expense_amount',
|
||||
];
|
||||
|
||||
public function run()
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
|
||||
$expenses = Expense::scope()
|
||||
->withArchived()
|
||||
->with('client.contacts', 'vendor')
|
||||
->where('expense_date', '>=', $this->startDate)
|
||||
->where('expense_date', '<=', $this->endDate);
|
||||
|
||||
foreach ($expenses->get() as $expense) {
|
||||
$amount = $expense->amountWithTax();
|
||||
|
||||
$this->data[] = [
|
||||
$expense->vendor ? ($this->isExport ? $expense->vendor->name : $expense->vendor->present()->link) : '',
|
||||
$expense->client ? ($this->isExport ? $expense->client->getDisplayName() : $expense->client->present()->link) : '',
|
||||
$expense->present()->expense_date,
|
||||
$expense->present()->category,
|
||||
Utils::formatMoney($amount, $expense->currency_id),
|
||||
];
|
||||
|
||||
$this->addToTotals($expense->expense_currency_id, 'amount', $amount);
|
||||
$this->addToTotals($expense->invoice_currency_id, 'amount', 0);
|
||||
}
|
||||
}
|
||||
}
|
61
app/Ninja/Reports/InvoiceDetailsReport.php
Normal file
61
app/Ninja/Reports/InvoiceDetailsReport.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Reports;
|
||||
|
||||
use Auth;
|
||||
use App\Models\Client;
|
||||
|
||||
class InvoiceDetailsReport extends AbstractReport
|
||||
{
|
||||
public $columns = [
|
||||
'client',
|
||||
'invoice_number',
|
||||
'invoice_date',
|
||||
'product',
|
||||
'qty',
|
||||
'cost',
|
||||
//'tax_rate1',
|
||||
//'tax_rate2',
|
||||
];
|
||||
|
||||
public function run()
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
$status = $this->options['invoice_status'];
|
||||
|
||||
$clients = Client::scope()
|
||||
->withArchived()
|
||||
->with('contacts')
|
||||
->with(['invoices' => function($query) use ($status) {
|
||||
if ($status == 'draft') {
|
||||
$query->whereIsPublic(false);
|
||||
} elseif ($status == 'unpaid' || $status == 'paid') {
|
||||
$query->whereIsPublic(true);
|
||||
}
|
||||
$query->invoices()
|
||||
->withArchived()
|
||||
->where('invoice_date', '>=', $this->startDate)
|
||||
->where('invoice_date', '<=', $this->endDate)
|
||||
->with(['invoice_items']);
|
||||
}]);
|
||||
|
||||
foreach ($clients->get() as $client) {
|
||||
foreach ($client->invoices as $invoice) {
|
||||
foreach ($invoice->invoice_items as $item) {
|
||||
$this->data[] = [
|
||||
$this->isExport ? $client->getDisplayName() : $client->present()->link,
|
||||
$this->isExport ? $invoice->invoice_number : $invoice->present()->link,
|
||||
$invoice->present()->invoice_date,
|
||||
$item->product_key,
|
||||
$item->qty,
|
||||
$account->formatMoney($item->cost, $client),
|
||||
];
|
||||
}
|
||||
|
||||
//$this->addToTotals($client->currency_id, 'paid', $payment ? $payment->getCompletedAmount() : 0);
|
||||
//$this->addToTotals($client->currency_id, 'amount', $invoice->amount);
|
||||
//$this->addToTotals($client->currency_id, 'balance', $invoice->balance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
73
app/Ninja/Reports/InvoiceReport.php
Normal file
73
app/Ninja/Reports/InvoiceReport.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Reports;
|
||||
|
||||
use Auth;
|
||||
use App\Models\Client;
|
||||
|
||||
class InvoiceReport extends AbstractReport
|
||||
{
|
||||
public $columns = [
|
||||
'client',
|
||||
'invoice_number',
|
||||
'invoice_date',
|
||||
'amount',
|
||||
'payment_date',
|
||||
'paid',
|
||||
'method'
|
||||
];
|
||||
|
||||
public function run()
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
$status = $this->options['invoice_status'];
|
||||
|
||||
$clients = Client::scope()
|
||||
->withArchived()
|
||||
->with('contacts')
|
||||
->with(['invoices' => function($query) use ($status) {
|
||||
if ($status == 'draft') {
|
||||
$query->whereIsPublic(false);
|
||||
} elseif ($status == 'unpaid' || $status == 'paid') {
|
||||
$query->whereIsPublic(true);
|
||||
}
|
||||
$query->invoices()
|
||||
->withArchived()
|
||||
->where('invoice_date', '>=', $this->startDate)
|
||||
->where('invoice_date', '<=', $this->endDate)
|
||||
->with(['payments' => function($query) {
|
||||
$query->withArchived()
|
||||
->excludeFailed()
|
||||
->with('payment_type', 'account_gateway.gateway');
|
||||
}, 'invoice_items']);
|
||||
}]);
|
||||
|
||||
foreach ($clients->get() as $client) {
|
||||
foreach ($client->invoices as $invoice) {
|
||||
|
||||
$payments = count($invoice->payments) ? $invoice->payments : [false];
|
||||
foreach ($payments as $payment) {
|
||||
if ( ! $payment && $status == 'paid') {
|
||||
continue;
|
||||
} elseif ($payment && $status == 'unpaid') {
|
||||
continue;
|
||||
}
|
||||
$this->data[] = [
|
||||
$this->isExport ? $client->getDisplayName() : $client->present()->link,
|
||||
$this->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 : '',
|
||||
];
|
||||
|
||||
$this->addToTotals($client->currency_id, 'paid', $payment ? $payment->getCompletedAmount() : 0);
|
||||
}
|
||||
|
||||
$this->addToTotals($client->currency_id, 'amount', $invoice->amount);
|
||||
$this->addToTotals($client->currency_id, 'balance', $invoice->balance);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
app/Ninja/Reports/PaymentReport.php
Normal file
54
app/Ninja/Reports/PaymentReport.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Reports;
|
||||
|
||||
use Auth;
|
||||
use App\Models\Payment;
|
||||
|
||||
class PaymentReport extends AbstractReport
|
||||
{
|
||||
public $columns = [
|
||||
'client',
|
||||
'invoice_number',
|
||||
'invoice_date',
|
||||
'amount',
|
||||
'payment_date',
|
||||
'paid',
|
||||
'method',
|
||||
];
|
||||
|
||||
public function run()
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
|
||||
$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', '>=', $this->startDate)
|
||||
->where('payment_date', '<=', $this->endDate);
|
||||
|
||||
foreach ($payments->get() as $payment) {
|
||||
$invoice = $payment->invoice;
|
||||
$client = $payment->client;
|
||||
$this->data[] = [
|
||||
$this->isExport ? $client->getDisplayName() : $client->present()->link,
|
||||
$this->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,
|
||||
];
|
||||
|
||||
$this->addToTotals($client->currency_id, 'amount', $invoice->amount);
|
||||
$this->addToTotals($client->currency_id, 'paid', $payment->getCompletedAmount());
|
||||
}
|
||||
}
|
||||
}
|
55
app/Ninja/Reports/ProductReport.php
Normal file
55
app/Ninja/Reports/ProductReport.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Reports;
|
||||
|
||||
use Auth;
|
||||
use App\Models\Client;
|
||||
|
||||
class ProductReport extends AbstractReport
|
||||
{
|
||||
public $columns = [
|
||||
'client',
|
||||
'invoice_number',
|
||||
'invoice_date',
|
||||
'quantity',
|
||||
'product',
|
||||
];
|
||||
|
||||
public function run()
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
|
||||
$clients = Client::scope()
|
||||
->withTrashed()
|
||||
->with('contacts')
|
||||
->where('is_deleted', '=', false)
|
||||
->with(['invoices' => function($query) {
|
||||
$query->where('invoice_date', '>=', $this->startDate)
|
||||
->where('invoice_date', '<=', $this->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) {
|
||||
$this->data[] = [
|
||||
$this->isExport ? $client->getDisplayName() : $client->present()->link,
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
68
app/Ninja/Reports/ProfitAndLossReport.php
Normal file
68
app/Ninja/Reports/ProfitAndLossReport.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Reports;
|
||||
|
||||
use Auth;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Expense;
|
||||
|
||||
class ProfitAndLossReport extends AbstractReport
|
||||
{
|
||||
public $columns = [
|
||||
'type',
|
||||
'client',
|
||||
'amount',
|
||||
'date',
|
||||
'notes',
|
||||
];
|
||||
|
||||
public function run()
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
|
||||
$payments = Payment::scope()
|
||||
->with('client.contacts')
|
||||
->withArchived()
|
||||
->excludeFailed();
|
||||
|
||||
foreach ($payments->get() as $payment) {
|
||||
$client = $payment->client;
|
||||
$this->data[] = [
|
||||
trans('texts.payment'),
|
||||
$client ? ($this->isExport ? $client->getDisplayName() : $client->present()->link) : '',
|
||||
$account->formatMoney($payment->getCompletedAmount(), $client),
|
||||
$payment->present()->payment_date,
|
||||
$payment->present()->method,
|
||||
];
|
||||
|
||||
$this->addToTotals($client->currency_id, 'revenue', $payment->getCompletedAmount(), $payment->present()->month);
|
||||
$this->addToTotals($client->currency_id, 'expenses', 0, $payment->present()->month);
|
||||
$this->addToTotals($client->currency_id, 'profit', $payment->getCompletedAmount(), $payment->present()->month);
|
||||
}
|
||||
|
||||
|
||||
$expenses = Expense::scope()
|
||||
->with('client.contacts')
|
||||
->withArchived();
|
||||
|
||||
foreach ($expenses->get() as $expense) {
|
||||
$client = $expense->client;
|
||||
$this->data[] = [
|
||||
trans('texts.expense'),
|
||||
$client ? ($this->isExport ? $client->getDisplayName() : $client->present()->link) : '',
|
||||
$expense->present()->amount,
|
||||
$expense->present()->expense_date,
|
||||
$expense->present()->category,
|
||||
];
|
||||
|
||||
$this->addToTotals($client->currency_id, 'revenue', 0, $expense->present()->month);
|
||||
$this->addToTotals($client->currency_id, 'expenses', $expense->amount, $expense->present()->month);
|
||||
$this->addToTotals($client->currency_id, 'profit', $expense->amount * -1, $expense->present()->month);
|
||||
}
|
||||
|
||||
|
||||
//$this->addToTotals($client->currency_id, 'paid', $payment ? $payment->getCompletedAmount() : 0);
|
||||
//$this->addToTotals($client->currency_id, 'amount', $invoice->amount);
|
||||
//$this->addToTotals($client->currency_id, 'balance', $invoice->balance);
|
||||
}
|
||||
}
|
36
app/Ninja/Reports/TaskReport.php
Normal file
36
app/Ninja/Reports/TaskReport.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Reports;
|
||||
|
||||
use Auth;
|
||||
use Utils;
|
||||
use App\Models\Task;
|
||||
|
||||
class TaskReport extends AbstractReport
|
||||
{
|
||||
public $columns = [
|
||||
'client',
|
||||
'date',
|
||||
'project',
|
||||
'description',
|
||||
'duration',
|
||||
];
|
||||
|
||||
public function run()
|
||||
{
|
||||
$tasks = Task::scope()
|
||||
->with('client.contacts')
|
||||
->withArchived()
|
||||
->dateRange($this->startDate, $this->endDate);
|
||||
|
||||
foreach ($tasks->get() as $task) {
|
||||
$this->data[] = [
|
||||
$task->client ? ($this->isExport ? $task->client->getDisplayName() : $task->client->present()->link) : trans('texts.unassigned'),
|
||||
link_to($task->present()->url, $task->getStartTime()),
|
||||
$task->present()->project,
|
||||
$task->present()->description,
|
||||
Utils::formatTime($task->getDuration()),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
82
app/Ninja/Reports/TaxRateReport.php
Normal file
82
app/Ninja/Reports/TaxRateReport.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ninja\Reports;
|
||||
|
||||
use Auth;
|
||||
use App\Models\Client;
|
||||
|
||||
class TaxRateReport extends AbstractReport
|
||||
{
|
||||
public $columns = [
|
||||
'tax_name',
|
||||
'tax_rate',
|
||||
'amount',
|
||||
'paid',
|
||||
];
|
||||
|
||||
public function run()
|
||||
{
|
||||
$account = Auth::user()->account;
|
||||
|
||||
$clients = Client::scope()
|
||||
->withArchived()
|
||||
->with('contacts')
|
||||
->with(['invoices' => function($query) {
|
||||
$query->with('invoice_items')->withArchived();
|
||||
if ($this->options['date_field'] == FILTER_INVOICE_DATE) {
|
||||
$query->where('invoice_date', '>=', $this->startDate)
|
||||
->where('invoice_date', '<=', $this->endDate)
|
||||
->with('payments');
|
||||
} else {
|
||||
$query->whereHas('payments', function($query) {
|
||||
$query->where('payment_date', '>=', $this->startDate)
|
||||
->where('payment_date', '<=', $this->endDate)
|
||||
->withArchived();
|
||||
})
|
||||
->with(['payments' => function($query) {
|
||||
$query->where('payment_date', '>=', $this->startDate)
|
||||
->where('payment_date', '<=', $this->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) {
|
||||
$this->data[] = [
|
||||
$tax['name'],
|
||||
$tax['rate'] . '%',
|
||||
$account->formatMoney($tax['amount'], $client),
|
||||
$account->formatMoney($tax['paid'], $client)
|
||||
];
|
||||
}
|
||||
|
||||
$this->addToTotals($client->currency_id, 'amount', $tax['amount']);
|
||||
$this->addToTotals($client->currency_id, 'paid', $tax['paid']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -195,6 +195,7 @@ class AccountRepository
|
||||
|
||||
$features = array_merge($features, [
|
||||
['dashboard', '/dashboard'],
|
||||
['reports', '/reports'],
|
||||
['customize_design', '/settings/customize_design'],
|
||||
['new_tax_rate', '/tax_rates/create'],
|
||||
['new_product', '/products/create'],
|
||||
|
@ -772,7 +772,7 @@ class InvoiceRepository extends BaseRepository
|
||||
*/
|
||||
public function markPaid(Invoice $invoice)
|
||||
{
|
||||
if (floatval($invoice->balance) <= 0) {
|
||||
if ( ! $invoice->canBePaid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -822,15 +822,16 @@ class InvoiceRepository extends BaseRepository
|
||||
public function findOpenInvoices($clientId, $entityType = false)
|
||||
{
|
||||
$query = Invoice::scope()
|
||||
->invoiceType(INVOICE_TYPE_STANDARD)
|
||||
->whereClientId($clientId)
|
||||
->whereIsRecurring(false)
|
||||
->whereDeletedAt(null);
|
||||
->invoiceType(INVOICE_TYPE_STANDARD)
|
||||
->whereClientId($clientId)
|
||||
->whereIsRecurring(false)
|
||||
->whereDeletedAt(null)
|
||||
->where('balance', '>', 0);
|
||||
|
||||
if ($entityType == ENTITY_TASK) {
|
||||
$query->whereHasTasks(true);
|
||||
} elseif ($entityType == ENTITY_EXPENSE) {
|
||||
$query->whereHasExpenses(true);
|
||||
$query->whereHasTasks(false);
|
||||
}
|
||||
|
||||
return $query->where('invoice_status_id', '<', 5)
|
||||
|
@ -34,7 +34,8 @@
|
||||
"sweetalert2": "^5.3.8",
|
||||
"jSignature": "brinley/jSignature#^2.1.0",
|
||||
"select2": "select2-dist#^4.0.3",
|
||||
"mousetrap": "^1.6.0"
|
||||
"mousetrap": "^1.6.0",
|
||||
"tablesorter": "jquery.tablesorter#^2.28.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"jquery": "~1.11"
|
||||
|
13
gulpfile.js
13
gulpfile.js
@ -66,6 +66,12 @@ elixir(function(mix) {
|
||||
bowerDir + '/select2/dist/css/select2.css'
|
||||
], 'public/css/select2.css');
|
||||
|
||||
mix.styles([
|
||||
bowerDir + '/tablesorter/dist/css/theme.bootstrap_3.min.css',
|
||||
bowerDir + '/tablesorter/dist/css/theme.bootstrap.min.css',
|
||||
bowerDir + '/tablesorter/dist/css/widget.grouping.min.css'
|
||||
], 'public/css/tablesorter.css');
|
||||
|
||||
|
||||
/**
|
||||
* JS configuration
|
||||
@ -84,6 +90,13 @@ elixir(function(mix) {
|
||||
bowerDir + '/bootstrap-daterangepicker/daterangepicker.js'
|
||||
], 'public/js/daterangepicker.min.js');
|
||||
|
||||
mix.scripts([
|
||||
bowerDir + '/tablesorter/dist/js/jquery.tablesorter.combined.js',
|
||||
bowerDir + '/tablesorter/dist/js/widgets/widget-grouping.min.js',
|
||||
bowerDir + '/tablesorter/dist/js/widgets/widget-uitheme.min.js',
|
||||
bowerDir + '/tablesorter/dist/js/widgets/widget-filter.min.js',
|
||||
], 'public/js/tablesorter.min.js');
|
||||
|
||||
mix.scripts([
|
||||
bowerDir + '/select2/dist/js/select2.js',
|
||||
'resources/assets/js/maximize-select2-height.js',
|
||||
|
2
public/css/tablesorter.css
vendored
Normal file
2
public/css/tablesorter.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/css/tablesorter.css.map
Normal file
1
public/css/tablesorter.css.map
Normal file
File diff suppressed because one or more lines are too long
6
public/js/tablesorter.min.js
vendored
Normal file
6
public/js/tablesorter.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/js/tablesorter.min.js.map
Normal file
1
public/js/tablesorter.min.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -2318,7 +2318,30 @@ $LANG = array(
|
||||
'domain_help_website' => 'Used when sending emails.',
|
||||
'preview' => 'Preview',
|
||||
'import_invoices' => 'Import Invoices',
|
||||
|
||||
'new_report' => 'New Report',
|
||||
'edit_report' => 'Edit Report',
|
||||
'columns' => 'Columns',
|
||||
'filters' => 'Filters',
|
||||
'sort_by' => 'Sort By',
|
||||
'draft' => 'Draft',
|
||||
'unpaid' => 'Unpaid',
|
||||
'aging' => 'Aging',
|
||||
'age' => 'Age',
|
||||
'days' => 'Days',
|
||||
'age_group_0' => '0 - 30 Days',
|
||||
'age_group_30' => '30 - 60 Days',
|
||||
'age_group_60' => '60 - 90 Days',
|
||||
'age_group_90' => '90 - 120 Days',
|
||||
'age_group_120' => '120+ Days',
|
||||
'invoice_details' => 'Invoice Details',
|
||||
'qty' => 'Quantity',
|
||||
'profit_and_loss' => 'Profit and Loss',
|
||||
'revenue' => 'Revenue',
|
||||
'profit' => 'Profit',
|
||||
'group_when_sorted' => 'Group When Sorted',
|
||||
'group_dates_by' => 'Group Dates By',
|
||||
'year' => 'Year',
|
||||
|
||||
);
|
||||
|
||||
return $LANG;
|
||||
|
@ -473,6 +473,7 @@
|
||||
'tasks' => false,
|
||||
'expenses' => false,
|
||||
'vendors' => false,
|
||||
'reports' => false,
|
||||
'settings' => false,
|
||||
] as $key => $value)
|
||||
{!! Form::nav_link($key, $value ?: $key) !!}
|
||||
@ -514,6 +515,7 @@
|
||||
])
|
||||
@endforeach
|
||||
@endif
|
||||
@include('partials.navigation_option', ['option' => 'reports'])
|
||||
@include('partials.navigation_option', ['option' => 'settings'])
|
||||
<li style="width:100%;">
|
||||
<div class="nav-footer">
|
||||
|
@ -6,11 +6,20 @@
|
||||
<script src="{{ asset('js/daterangepicker.min.js') }}" type="text/javascript"></script>
|
||||
<link href="{{ asset('css/daterangepicker.css') }}" rel="stylesheet" type="text/css"/>
|
||||
|
||||
<link href="{{ asset('css/tablesorter.css') }}" rel="stylesheet" type="text/css"/>
|
||||
<script src="{{ asset('js/tablesorter.min.js') }}" type="text/javascript"></script>
|
||||
|
||||
<style type="text/css">
|
||||
table.tablesorter th {
|
||||
color: white;
|
||||
background-color: #777 !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@stop
|
||||
|
||||
@section('content')
|
||||
@parent
|
||||
@include('accounts.nav', ['selected' => ACCOUNT_REPORTS, 'advanced' => true])
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
@ -58,6 +67,11 @@
|
||||
{!! Former::populateField('start_date', $startDate) !!}
|
||||
{!! Former::populateField('end_date', $endDate) !!}
|
||||
|
||||
@if ( ! request()->report_type)
|
||||
{!! Former::populateField('group_when_sorted', 1) !!}
|
||||
{!! Former::populateField('group_dates_by', 'monthyear') !!}
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="panel panel-default">
|
||||
@ -68,12 +82,14 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
|
||||
<div class="form-group">
|
||||
{!! Former::select('report_type')->options($reportTypes, $reportType)->label(trans('texts.type')) !!}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="reportrange" class="control-label col-lg-4 col-sm-4">
|
||||
{{ trans('texts.date_range') }}
|
||||
</label>
|
||||
<div class="col-lg-8 col-sm-8">
|
||||
<div id="reportrange" style="background: #f9f9f9; cursor: pointer; padding: 9px 14px; border: 1px solid #dfe0e1; margin-top: 0px; margin-left:18px">
|
||||
<div id="reportrange" style="background: #f9f9f9; cursor: pointer; padding: 9px 14px; border: 1px solid #dfe0e1; margin-top: 0px;">
|
||||
<i class="glyphicon glyphicon-calendar fa fa-calendar"></i>
|
||||
<span></span> <b class="caret"></b>
|
||||
</div>
|
||||
@ -85,48 +101,109 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="statusField" style="display:{{ in_array($reportType, ['invoice', 'invoice_details']) ? 'block' : 'none' }}">
|
||||
{!! Former::select('invoice_status')->label('status')
|
||||
->addOption(trans('texts.all'), 'all')
|
||||
->addOption(trans('texts.draft'), 'draft')
|
||||
->addOption(trans('texts.unpaid'), 'unpaid')
|
||||
->addOption(trans('texts.paid'), 'paid') !!}
|
||||
</div>
|
||||
|
||||
<p> </p>
|
||||
<p> </p>
|
||||
{!! Former::actions(
|
||||
Button::primary(trans('texts.export'))->withAttributes(array('onclick' => 'onExportClick()'))->appendIcon(Icon::create('export')),
|
||||
Button::success(trans('texts.run'))->withAttributes(array('id' => 'submitButton'))->submit()->appendIcon(Icon::create('play'))
|
||||
) !!}
|
||||
|
||||
@if (!Auth::user()->hasFeature(FEATURE_REPORTS))
|
||||
<script>
|
||||
$(function() {
|
||||
$('form.warn-on-exit').find('input, button').prop('disabled', true);
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{!! Former::select('report_type')->options($reportTypes, $reportType)->label(trans('texts.type')) !!}
|
||||
<div id="dateField" style="display:{{ $reportType == ENTITY_TAX_RATE ? 'block' : 'none' }}">
|
||||
<div id="dateField" style="display:{{ $reportType == ENTITY_TAX_RATE ? 'block' : 'none' }}">
|
||||
{!! Former::select('date_field')->label(trans('texts.filter'))
|
||||
->addOption(trans('texts.invoice_date'), FILTER_INVOICE_DATE)
|
||||
->addOption(trans('texts.payment_date'), FILTER_PAYMENT_DATE) !!}
|
||||
</div>
|
||||
|
||||
{!! Former::close() !!}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
|
||||
{!! Former::checkbox('group_when_sorted')->text('enable') !!}
|
||||
{!! Former::select('group_dates_by')
|
||||
->addOption(trans('texts.day'), 'day')
|
||||
->addOption(trans('texts.month'), 'monthyear')
|
||||
->addOption(trans('texts.year'), 'year') !!}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!Auth::user()->hasFeature(FEATURE_REPORTS))
|
||||
<script>
|
||||
$(function() {
|
||||
$('form.warn-on-exit').find('input, button').prop('disabled', true);
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
|
||||
|
||||
<center>
|
||||
{!! Button::primary(trans('texts.export'))
|
||||
->withAttributes(array('onclick' => 'onExportClick()'))
|
||||
->appendIcon(Icon::create('export'))
|
||||
->large() !!}
|
||||
{!! Button::success(trans('texts.run'))
|
||||
->withAttributes(array('id' => 'submitButton'))
|
||||
->submit()
|
||||
->appendIcon(Icon::create('play'))
|
||||
->large() !!}
|
||||
</center><br/>
|
||||
|
||||
|
||||
{!! Former::close() !!}
|
||||
|
||||
|
||||
@if (request()->report_type)
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<table class="table table-striped invoice-table">
|
||||
|
||||
@if (count(array_values($reportTotals)))
|
||||
<table class="tablesorter tablesorter-totals" style="display:none">
|
||||
<thead>
|
||||
<tr>
|
||||
@foreach ($columns as $column)
|
||||
<th>{{ trans("texts.{$column}") }}</th>
|
||||
<th>{{ trans("texts.totals") }}</th>
|
||||
@foreach (array_values(array_values($reportTotals)[0])[0] as $key => $val)
|
||||
<th>{{ trans("texts.{$key}") }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($reportTotals as $currencyId => $each)
|
||||
@foreach ($each as $dimension => $val)
|
||||
<tr>
|
||||
<td>{!! Utils::getFromCache($currencyId, 'currencies')->name !!}
|
||||
@if ($dimension)
|
||||
- {{ $dimension }}
|
||||
@endif
|
||||
</td>
|
||||
@foreach ($val as $id => $field)
|
||||
<td>{!! Utils::formatMoney($field, $currencyId) !!}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<p> </p>
|
||||
@endif
|
||||
|
||||
<!--
|
||||
<div class="columnSelectorWrapper">
|
||||
<input id="colSelect1" type="checkbox" class="hidden">
|
||||
<label class="columnSelectorButton" for="colSelect1">Column</label>
|
||||
|
||||
<div id="columnSelector" class="columnSelector">
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<table class="tablesorter tablesorter-data" style="display:none">
|
||||
<thead>
|
||||
<tr>
|
||||
{!! $report->tableHeader() !!}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (count($displayData))
|
||||
@foreach ($displayData as $record)
|
||||
@ -144,36 +221,13 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p> </p>
|
||||
|
||||
@if (count(array_values($reportTotals)))
|
||||
<table class="table table-striped invoice-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ trans("texts.totals") }}</th>
|
||||
@foreach (array_values($reportTotals)[0] as $key => $val)
|
||||
<th>{{ trans("texts.{$key}") }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($reportTotals as $currencyId => $val)
|
||||
<tr>
|
||||
<td>{!! Utils::getFromCache($currencyId, 'currencies')->name !!}</td>
|
||||
@foreach ($val as $id => $field)
|
||||
<td>{!! Utils::formatMoney($field, $currencyId) !!}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@endif
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
function onExportClick() {
|
||||
@ -182,6 +236,11 @@
|
||||
$('#action').val('');
|
||||
}
|
||||
|
||||
var sumColumns = [];
|
||||
@foreach ($columns as $column)
|
||||
sumColumns.push("{{ in_array($column, ['amount', 'paid', 'balance']) ? trans("texts.{$column}") : false }}");
|
||||
@endforeach
|
||||
|
||||
$(function() {
|
||||
$('.start_date .input-group-addon').click(function() {
|
||||
toggleDatePicker('start_date');
|
||||
@ -192,12 +251,51 @@
|
||||
|
||||
$('#report_type').change(function() {
|
||||
var val = $('#report_type').val();
|
||||
if (val == '{{ ENTITY_TAX_RATE }}') {
|
||||
if (val == '{{ ENTITY_TAX_RATE }}') {
|
||||
$('#dateField').fadeIn();
|
||||
} else {
|
||||
$('#dateField').fadeOut();
|
||||
}
|
||||
if (val == '{{ ENTITY_INVOICE }}' || val == 'invoice_details') {
|
||||
$('#statusField').fadeIn();
|
||||
} else {
|
||||
$('#statusField').fadeOut();
|
||||
}
|
||||
});
|
||||
|
||||
$(function(){
|
||||
$(".tablesorter-data").tablesorter({
|
||||
theme: 'bootstrap',
|
||||
widgets: ['zebra', 'uitheme', 'filter'{!! request()->group_when_sorted ? ", 'group'" : "" !!}, 'columnSelector'],
|
||||
headerTemplate : '{content} {icon}',
|
||||
widgetOptions : {
|
||||
columnSelector_container : $('#columnSelector'),
|
||||
filter_cssFilter: 'form-control',
|
||||
group_collapsed: true,
|
||||
group_saveGroups: false,
|
||||
//group_formatter : function(txt, col, table, c, wo, data) {},
|
||||
group_callback: function ($cell, $rows, column, table) {
|
||||
for (var i=0; i<sumColumns.length; i++) {
|
||||
var label = sumColumns[i];
|
||||
if (!label) {
|
||||
continue;
|
||||
}
|
||||
var subtotal = 0;
|
||||
$rows.each(function() {
|
||||
var txt = $(this).find("td").eq(i).text().replace(/[$,]/g, '');
|
||||
subtotal += parseFloat(txt || 0);
|
||||
});
|
||||
$cell.find(".group-count").append(' - ' + label + ': ' + roundToTwo(subtotal));
|
||||
}
|
||||
},
|
||||
}
|
||||
}).show();
|
||||
|
||||
$(".tablesorter-totals").tablesorter({
|
||||
theme: 'bootstrap',
|
||||
widgets: ['zebra', 'uitheme'],
|
||||
}).show();
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user