diff --git a/app/Http/Controllers/Reports/IncomeExpenseSummary.php b/app/Http/Controllers/Reports/IncomeExpenseSummary.php index 8aae900ee..c29899be2 100644 --- a/app/Http/Controllers/Reports/IncomeExpenseSummary.php +++ b/app/Http/Controllers/Reports/IncomeExpenseSummary.php @@ -80,7 +80,7 @@ class IncomeExpenseSummary extends Controller $compares['expense'][0][$dates[$j]] = array( 'category_id' => 0, - 'name' => trans_choice('general.invoices', 1), + 'name' => trans_choice('general.bills', 1), 'amount' => 0, 'currency_code' => setting('general.default_currency'), 'currency_rate' => 1 diff --git a/app/Http/Controllers/Reports/ProfitLoss.php b/app/Http/Controllers/Reports/ProfitLoss.php new file mode 100644 index 000000000..e6b0a10ff --- /dev/null +++ b/app/Http/Controllers/Reports/ProfitLoss.php @@ -0,0 +1,232 @@ +type('income')->pluck('name', 'id')->toArray(); + + // Add invoice to income categories + $income_categories[0] = trans_choice('general.invoices', 2); + + $expense_categories = Category::enabled()->type('expense')->pluck('name', 'id')->toArray(); + + // Add bill to expense categories + $expense_categories[0] = trans_choice('general.bills', 2); + + // Get year + $year = request('year'); + if (empty($year)) { + $year = Date::now()->year; + } + + // Dates + for ($j = 1; $j <= 12; $j++) { + $dates[$j] = Date::parse($year . '-' . $j)->quarter; + + // Totals + $totals[$dates[$j]] = array( + 'amount' => 0, + 'currency_code' => setting('general.default_currency'), + 'currency_rate' => 1 + ); + + // Compares + $compares['income'][0][$dates[$j]] = [ + 'category_id' => 0, + 'name' => trans_choice('general.invoices', 1), + 'amount' => 0, + 'currency_code' => setting('general.default_currency'), + 'currency_rate' => 1 + ]; + + foreach ($income_categories as $category_id => $category_name) { + $compares['income'][$category_id][$dates[$j]] = [ + 'category_id' => $category_id, + 'name' => $category_name, + 'amount' => 0, + 'currency_code' => setting('general.default_currency'), + 'currency_rate' => 1 + ]; + } + + $compares['expense'][0][$dates[$j]] = [ + 'category_id' => 0, + 'name' => trans_choice('general.bills', 1), + 'amount' => 0, + 'currency_code' => setting('general.default_currency'), + 'currency_rate' => 1 + ]; + + foreach ($expense_categories as $category_id => $category_name) { + $compares['expense'][$category_id][$dates[$j]] = [ + 'category_id' => $category_id, + 'name' => $category_name, + 'amount' => 0, + 'currency_code' => setting('general.default_currency'), + 'currency_rate' => 1 + ]; + } + + $j += 2; + } + + $totals['total'] = [ + 'amount' => 0, + 'currency_code' => setting('general.default_currency'), + 'currency_rate' => 1 + ]; + + $compares['income'][0]['total'] = [ + 'category_id' => 0, + 'name' => trans_choice('general.totals', 1), + 'amount' => 0, + 'currency_code' => setting('general.default_currency'), + 'currency_rate' => 1 + ]; + + foreach ($income_categories as $category_id => $category_name) { + $compares['income'][$category_id]['total'] = [ + 'category_id' => $category_id, + 'name' => trans_choice('general.totals', 1), + 'amount' => 0, + 'currency_code' => setting('general.default_currency'), + 'currency_rate' => 1 + ]; + } + + $compares['expense'][0]['total'] = [ + 'category_id' => 0, + 'name' => trans_choice('general.totals', 1), + 'amount' => 0, + 'currency_code' => setting('general.default_currency'), + 'currency_rate' => 1 + ]; + + foreach ($expense_categories as $category_id => $category_name) { + $compares['expense'][$category_id]['total'] = [ + 'category_id' => $category_id, + 'name' => trans_choice('general.totals', 1), + 'amount' => 0, + 'currency_code' => setting('general.default_currency'), + 'currency_rate' => 1 + ]; + } + + // Invoices + switch ($status) { + case 'paid': + $invoices = InvoicePayment::monthsOfYear('paid_at')->get(); + $this->setAmount($totals, $compares, $invoices, 'invoice', 'paid_at'); + break; + case 'upcoming': + $invoices = Invoice::accrued()->monthsOfYear('due_at')->get(); + $this->setAmount($totals, $compares, $invoices, 'invoice', 'due_at'); + break; + default: + $invoices = Invoice::accrued()->monthsOfYear('invoiced_at')->get(); + $this->setAmount($totals, $compares, $invoices, 'invoice', 'invoiced_at'); + break; + } + + // Revenues + if ($status != 'upcoming') { + $revenues = Revenue::monthsOfYear('paid_at')->isNotTransfer()->get(); + $this->setAmount($totals, $compares, $revenues, 'revenue', 'paid_at'); + } + + // Bills + switch ($status) { + case 'paid': + $bills = BillPayment::monthsOfYear('paid_at')->get(); + $this->setAmount($totals, $compares, $bills, 'bill', 'paid_at'); + break; + case 'upcoming': + $bills = Bill::accrued()->monthsOfYear('due_at')->get(); + $this->setAmount($totals, $compares, $bills, 'bill', 'due_at'); + break; + default: + $bills = Bill::accrued()->monthsOfYear('billed_at')->get(); + $this->setAmount($totals, $compares, $bills, 'bill', 'billed_at'); + break; + } + + // Payments + if ($status != 'upcoming') { + $payments = Payment::monthsOfYear('paid_at')->isNotTransfer()->get(); + $this->setAmount($totals, $compares, $payments, 'payment', 'paid_at'); + } + + // Check if it's a print or normal request + if (request('print')) { + $view_template = 'reports.profit_loss.print'; + } else { + $view_template = 'reports.profit_loss.index'; + } + + return view($view_template, compact('dates', 'income_categories', 'expense_categories', 'compares', 'totals')); + } + + private function setAmount(&$totals, &$compares, $items, $type, $date_field) + { + foreach ($items as $item) { + $date = Date::parse($item->$date_field)->quarter; + + if (($type == 'invoice') || ($type == 'bill')) { + $category_id = 0; + } else { + $category_id = $item->category_id; + } + + $group = (($type == 'invoice') || ($type == 'revenue')) ? 'income' : 'expense'; + + if (!isset($compares[$group][$category_id])) { + continue; + } + + $amount = $item->getConvertedAmount(); + + // Forecasting + if ((($type == 'invoice') || ($type == 'bill')) && ($date_field == 'due_at')) { + foreach ($item->payments as $payment) { + $amount -= $payment->getConvertedAmount(); + } + } + + $compares[$group][$category_id][$date]['amount'] += $amount; + $compares[$group][$category_id][$date]['currency_code'] = $item->currency_code; + $compares[$group][$category_id][$date]['currency_rate'] = $item->currency_rate; + $compares[$group][$category_id]['total']['amount'] += $amount; + + if ($group == 'income') { + $totals[$date]['amount'] += $amount; + $totals['total']['amount'] += $amount; + } else { + $totals[$date]['amount'] -= $amount; + $totals['total']['amount'] -= $amount; + } + } + } +} diff --git a/app/Http/Middleware/AdminMenu.php b/app/Http/Middleware/AdminMenu.php index cfd01f644..3d58d2516 100644 --- a/app/Http/Middleware/AdminMenu.php +++ b/app/Http/Middleware/AdminMenu.php @@ -111,7 +111,7 @@ class AdminMenu } // Reports - if ($user->can(['read-reports-income-summary', 'read-reports-expense-summary', 'read-reports-income-expense-summary'])) { + if ($user->can(['read-reports-income-summary', 'read-reports-expense-summary', 'read-reports-income-expense-summary', 'read-reports-profit-loss'])) { $menu->dropdown(trans_choice('general.reports', 2), function ($sub) use($user, $attr) { if ($user->can('read-reports-income-summary')) { $sub->url('reports/income-summary', trans('reports.summary.income'), 1, $attr); @@ -124,6 +124,10 @@ class AdminMenu if ($user->can('read-reports-income-expense-summary')) { $sub->url('reports/income-expense-summary', trans('reports.summary.income_expense'), 3, $attr); } + + if ($user->can('read-reports-profit-loss')) { + $sub->url('reports/profit-loss', trans('reports.profit_loss'), 4, $attr); + } }, 6, [ 'title' => trans_choice('general.reports', 2), 'icon' => 'fa fa-bar-chart', diff --git a/app/Listeners/Updates/Version120.php b/app/Listeners/Updates/Version120.php new file mode 100644 index 000000000..0ef59c704 --- /dev/null +++ b/app/Listeners/Updates/Version120.php @@ -0,0 +1,48 @@ +check($event)) { + return; + } + + // Create permission + $permission = Permission::firstOrCreate([ + 'name' => 'read-reports-profit-loss', + 'display_name' => 'Read Reports Profit Loss', + 'description' => 'Read Reports Profit Loss', + ]); + + // Attach permission to roles + $roles = Role::all(); + + foreach ($roles as $role) { + $allowed = ['admin', 'manager']; + + if (!in_array($role->name, $allowed)) { + continue; + } + + $role->attachPermission($permission); + } + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 002ecd50c..00ca6aaee 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -22,6 +22,7 @@ class EventServiceProvider extends ServiceProvider 'App\Listeners\Updates\Version112', 'App\Listeners\Updates\Version113', 'App\Listeners\Updates\Version119', + 'App\Listeners\Updates\Version120', ], 'Illuminate\Auth\Events\Login' => [ 'App\Listeners\Auth\Login', diff --git a/resources/lang/en-GB/reports.php b/resources/lang/en-GB/reports.php index 8589c7b9f..8b95b822d 100644 --- a/resources/lang/en-GB/reports.php +++ b/resources/lang/en-GB/reports.php @@ -7,6 +7,9 @@ return [ 'this_quarter' => 'This Quarter', 'previous_quarter' => 'Previous Quarter', 'last_12_months' => 'Last 12 Months', + 'profit_loss' => 'Profit & Loss', + 'gross_profit' => 'Gross Profit', + 'net_profit' => 'Net Profit', 'summary' => [ 'income' => 'Income Summary', @@ -14,4 +17,11 @@ return [ 'income_expense' => 'Income vs Expense', ], + 'quarter' => [ + '1' => 'Jan-Mar', + '2' => 'Apr-Jun', + '3' => 'Jul-Sep', + '4' => 'Oct-Dec', + ], + ]; diff --git a/resources/views/reports/profit_loss/body.blade.php b/resources/views/reports/profit_loss/body.blade.php new file mode 100644 index 000000000..2fbe6cb46 --- /dev/null +++ b/resources/views/reports/profit_loss/body.blade.php @@ -0,0 +1,67 @@ +
+
+ + + + + @foreach($dates as $date) + + @endforeach + + + + + @if ($compares) +
 {{ trans('reports.quarter.' . $date) }}{{ trans_choice('general.totals', 1) }}
+ + + + + @foreach($compares['income'] as $category_id => $category) + + + + @foreach($category as $item) + + @endforeach + + @endforeach + +
{{ trans_choice('general.incomes', 2) }}
{{ $income_categories[$category_id] }}@money($item['amount'], $item['currency_code'], true)
+ + + + + + @foreach($compares['expense'] as $category_id => $category) + + + + @foreach($category as $item) + + @endforeach + + @endforeach + +
{{ trans_choice('general.expenses', 2) }}
{{ $expense_categories[$category_id] }}@money($item['amount'], $item['currency_code'], true)
+ @else + + +
{{ trans('general.no_records') }}
+ + + @endif + + + + + + @foreach($totals as $total) + + @endforeach + + +
{{ trans('reports.net_profit') }}@money($total['amount'], $total['currency_code'], true)
+ +
+
diff --git a/resources/views/reports/profit_loss/index.blade.php b/resources/views/reports/profit_loss/index.blade.php new file mode 100644 index 000000000..0195b2cec --- /dev/null +++ b/resources/views/reports/profit_loss/index.blade.php @@ -0,0 +1,28 @@ +@extends('layouts.admin') + +@section('title', trans('reports.profit_loss')) + +@section('new_button') +  {{ trans('general.print') }} +@endsection + +@section('content') + +
+
+ + {!! Form::open(['url' => 'reports/profit-loss', 'role' => 'form', 'method' => 'GET']) !!} +
+ {!! Form::select('year', $years, request('year', $this_year), ['class' => 'form-control input-filter input-sm', 'onchange' => 'this.form.submit()']) !!} +
+ {!! Form::close() !!} +
+ + @include('reports.profit_loss.body') +
+ +@endsection diff --git a/resources/views/reports/profit_loss/print.blade.php b/resources/views/reports/profit_loss/print.blade.php new file mode 100644 index 000000000..9bc8a77ba --- /dev/null +++ b/resources/views/reports/profit_loss/print.blade.php @@ -0,0 +1,7 @@ +@extends('layouts.print') + +@section('title', trans('reports.profit_loss')) + +@section('content') + @include('reports.profit_loss.body') +@endsection \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 570545e20..af481a340 100644 --- a/routes/web.php +++ b/routes/web.php @@ -96,6 +96,7 @@ Route::group(['middleware' => 'language'], function () { Route::resource('income-summary', 'Reports\IncomeSummary'); Route::resource('expense-summary', 'Reports\ExpenseSummary'); Route::resource('income-expense-summary', 'Reports\IncomeExpenseSummary'); + Route::resource('profit-loss', 'Reports\ProfitLoss'); }); Route::group(['prefix' => 'settings'], function () {