diff --git a/src/Helpers/ReportHelper.php b/src/Helpers/ReportHelper.php index 27f7463..23cd347 100644 --- a/src/Helpers/ReportHelper.php +++ b/src/Helpers/ReportHelper.php @@ -5,6 +5,7 @@ use Carbon\Carbon; use Illuminate\Support\Facades\DB; use Insane\Journal\Models\Core\Account; +use Insane\Journal\Models\Core\Category; use Insane\Journal\Models\Core\Transaction; use Insane\Journal\Models\Invoice\Invoice; @@ -92,7 +93,7 @@ public static function getExpensesByPeriod($teamId, $startDate, $endDate) { ->get(); } - public static function getAccountTransactionsByPeriod(int $teamId, array $accounts, $startDate = null, $endDate = null) { + public static function getTransactionsByAccount(int $teamId, array $accounts, $startDate = null, $endDate = null, $groupBy = "display_id") { $endDate = $endDate ?? Carbon::now()->endOfMonth()->format('Y-m-d'); $startDate = $startDate ?? Carbon::now()->startOfMonth()->format('Y-m-d'); @@ -108,7 +109,16 @@ public static function getAccountTransactionsByPeriod(int $teamId, array $accoun ->orWhereIn('categories.display_id', $accounts) ->orWhereIn('g.display_id', $accounts); }) - ->selectRaw('sum(COALESCE(amount * transaction_lines.type, 0)) as total, + ->selectRaw(' + sum(COALESCE(amount * transaction_lines.type, 0)) as total, + SUM(CASE + WHEN transaction_lines.type > 0 THEN amount + ELSE 0 + END) as income, + SUM(CASE + WHEN transaction_lines.type < 0 THEN amount + ELSE 0 + END) outcome, date_format(transactions.date, "%Y-%m-01") as date, categories.name, categories.id, @@ -121,7 +131,16 @@ public static function getAccountTransactionsByPeriod(int $teamId, array $accoun ->join(DB::raw('categories g'), 'g.id', 'categories.parent_id') ->get(); - $resultGroup = $results->groupBy('display_id'); + return $results->groupBy($groupBy); + + + } + + public static function getAccountTransactionsByPeriod(int $teamId, array $accounts, $startDate = null, $endDate = null, $groupBy = "display_id") { + $endDate = $endDate ?? Carbon::now()->endOfMonth()->format('Y-m-d'); + $startDate = $startDate ?? Carbon::now()->startOfMonth()->format('Y-m-d'); + + $resultGroup = self::getTransactionsByAccount($teamId, $accounts, $startDate, $endDate); return array_map(function ($account) use ($resultGroup) { return $resultGroup[$account][0] ?? [ @@ -135,34 +154,10 @@ public static function getChartTransactionsByPeriod(int $teamId, array $accounts $endDate = $endDate ?? Carbon::now()->endOfMonth()->format('Y-m-d'); $startDate = $startDate ?? Carbon::now()->startOfMonth()->format('Y-m-d'); - $results = DB::table('transaction_lines') - ->whereBetween('transaction_lines.date', [$startDate, $endDate]) - ->where([ - 'transaction_lines.team_id' => $teamId, - 'transactions.status' => 'verified' - ]) - ->where(function($query) use ($accounts) { - $query - ->whereIn('accounts.display_id', $accounts) - ->orWhereIn('categories.display_id', $accounts) - ->orWhereIn('g.display_id', $accounts); - }) - ->selectRaw('sum(COALESCE(amount * transaction_lines.type * accounts.type, 0)) as total, - date_format(transactions.date, "%Y-%m-01") as date, - categories.name, - categories.id, - categories.display_id, - g.display_id groupName' - )->groupByRaw('date_format(transactions.date, "%Y-%m"), categories.id') - ->join('accounts', 'accounts.id', '=', 'transaction_lines.account_id') - ->join('categories', 'accounts.category_id', '=', 'categories.id') - ->join('transactions', 'transactions.id', '=', 'transaction_id') - ->join(DB::raw('categories g'), 'g.id', 'categories.parent_id') - ->groupBy('categories.display_id') - ->get()->groupBy('groupName'); + $results = self::getTransactionsByAccount($teamId, $accounts, $startDate, $endDate, "groupName"); return collect($accounts)->reduce(function ($groups, $account) use ($results) { - $groups[$account] = isset($results[$account]) ? $results[$account][0]->total : 0; + $groups[$account] = isset($results[$account]) ? $results[$account][0] : []; return $groups; }); } @@ -234,5 +229,128 @@ public function debtors($teamId) { ->take(5) ->groupBy('invoices.client_id', 'clients.names', 'clients.id') ->get(); + } + + public static function getReportCategories(string $reportName) { + + $categoriesGroups = [ + "income" => ["income"], + "expense" => ["expenses"], + "tax" => ["liabilities"], + "cash-flow" => ["assets", "liabilities", "equity"], + "balance-sheet" => ["assets", "liabilities", "equity"], + "income-statement" => ["income", "expenses"], + "account-balance" => ["assets", "liabilities", "income", "expenses", "equity"], + ]; + + return $categoriesGroups[$reportName]; + } + + public static function getGeneralLedger($teamId, $reportName, $config = []) { + $categories = self::getReportCategories($reportName); + $categoryData = Category::whereIn('display_id', $categories)->get(); + + $categoryIds = $categoryData->pluck('id')->toArray(); + + + $accountQuery = DB::table('categories') + ->whereIn('categories.parent_id', $categoryIds) + ->selectRaw('group_concat(accounts.id) as account_ids, group_concat(accounts.name) as account_names') + ->joinSub(DB::table('accounts')->where('team_id', $teamId), 'accounts','category_id', '=', 'categories.id') + ->get() + ->pluck('account_ids'); + + + if (isset($config['account_id'])) { + $accountQuery->whereIn('accounts.id', [$config['account_id']]); } + + $accountIds = $accountQuery->toArray(); + + + $accountIds = explode(",", $accountIds[0]); + + $balanceByAccounts = DB::table('transaction_lines') + ->whereIn('transaction_lines.account_id', $accountIds) + ->selectRaw(" + SUM(amount * transaction_lines.type) as total, + SUM(CASE + WHEN transaction_lines.type > 0 THEN amount + ELSE 0 + END) as income, + SUM(CASE + WHEN transaction_lines.type < 0 THEN amount + ELSE 0 + END) outcome, + group_concat(date) dates, + transaction_lines.account_id, + accounts.id, + accounts.name, + accounts.display_id, + categories.display_id category, + gl.display_id ledger, + gl.id ledger_id + ") + ->join('accounts', 'accounts.id', '=', 'transaction_lines.account_id') + ->join('categories', 'accounts.category_id', 'categories.id') + ->join(DB::raw('categories gl'), 'categories.parent_id', 'gl.id') + ->groupBy('transaction_lines.account_id') + ->orderBy(DB::raw("gl.index, categories.index")); + + + if (isset($config['dates'])) { + $balanceByAccounts = $balanceByAccounts->whereBetween('transaction_lines.date', $config['dates']); + } + + $accountsWithActivity = $config['account_id'] ? [$config['account_id']] : $balanceByAccounts->pluck('id')->toArray(); + + // @todo Analyze this, since I think I just will display subcategories with account transactions I just need to group the first query and the last. + $categoryAccounts = Category::where([ + 'depth' => 1, + ]) + ->whereIn('parent_id', $categoryIds) + ->hasAccounts($accountsWithActivity) + ->with(['accounts' => function ($query) use ($teamId, $accountsWithActivity) { + $query->where('team_id', '=', $teamId); + $query->whereIn('id', $accountsWithActivity); + }, + 'category' + ]) + ->get() + ->toArray(); + + $balance = $balanceByAccounts->get()->toArray(); + + $categoryAccounts = array_map(function ($subCategory) use ($balance) { + $total = []; + if (isset($subCategory['accounts'])) { + foreach ($subCategory['accounts'] as $accountIndex => $account) { + $index = array_search($account['id'], array_column($balance, 'id')); + if ($index !== false ) { + $subCategory['accounts'][$accountIndex]['balance'] = $balance[$index]->total; + $subCategory['accounts'][$accountIndex]['income'] = $balance[$index]->income; + $subCategory['accounts'][$accountIndex]['outcome'] = $balance[$index]->outcome; + $total[] = $balance[$index]->total; + } + } + } + $subCategory['total'] = array_sum($total); + return $subCategory; + }, $categoryAccounts); + + + $ledger = DB::table('categories') + ->whereIn('categories.display_id', $categories) + ->selectRaw('sum(COALESCE(total, 0)) total, categories.alias, categories.display_id, categories.name') + ->leftJoinSub($balanceByAccounts, 'balance', function($join) { + $join->on('categories.id', 'balance.ledger_id'); + })->groupBy('categories.id'); + + $ledger = $ledger->get(); + + return [ + "ledger" => $ledger, + "categoryAccounts" => $categoryAccounts + ]; + } } diff --git a/src/Http/Controllers/AccountController.php b/src/Http/Controllers/AccountController.php index 2e528dd..36b3282 100644 --- a/src/Http/Controllers/AccountController.php +++ b/src/Http/Controllers/AccountController.php @@ -9,6 +9,7 @@ use Insane\Journal\Contracts\DeleteAccounts; use Insane\Journal\Events\AccountCreated; use Insane\Journal\Events\AccountUpdated; +use Insane\Journal\Helpers\ReportHelper; use Insane\Journal\Models\Core\Account; use Insane\Journal\Models\Core\AccountDetailType; use Insane\Journal\Models\Core\Category; @@ -188,88 +189,21 @@ public function statementsIndex(Request $request, string $category = "income") { ]); } - public function statements(Request $request, string $category = "income") { - $categories = [ - "income" => ["income"], - "expense" => ["expenses"], - "tax" => ["liabilities"], - "balance-sheet" => ["assets", "liabilities", "equity"], - "income-statement" => ["income", "expenses"], - "account-balance" => ["assets", "liabilities", "income", "expenses", "equity"], - ]; - - $categoryData = Category::whereIn('display_id', $categories[$category] )->get(); - - $categoryIds = $categoryData->pluck('id')->toArray(); - - $accountIds = DB::table('categories') - ->whereIn('categories.parent_id', $categoryIds) - ->selectRaw('group_concat(accounts.id) as account_ids, group_concat(accounts.name) as account_names') - ->joinSub(DB::table('accounts')->where('team_id', $request->user()->current_team_id), 'accounts','category_id', '=', 'categories.id') - ->get()->pluck('account_ids')->toArray(); - - - $accountIds = explode(",", $accountIds[0]); - - $balanceByAccounts = DB::table('transaction_lines') - ->whereIn('transaction_lines.account_id', $accountIds) - ->selectRaw(" - sum(amount * transaction_lines.type) as total, - transaction_lines.account_id, - accounts.id, - accounts.name, - accounts.display_id, - categories.display_id category, - g.display_id ledger, - g.id ledger_id - ") - ->join('accounts', 'accounts.id', '=', 'transaction_lines.account_id') - ->join('categories', 'accounts.category_id', 'categories.id') - ->join(DB::raw('categories g'), 'categories.parent_id', 'g.id') - ->groupBy('transaction_lines.account_id'); + public function statements(Request $request, string $reportName = "income") { + $filters = $request->query('filters'); + $accountId = $filters ? $filters['account'] : null; - // all the accounts with balance are here - - - $categoryAccounts = Category::where([ - 'depth' => 1, - ]) - ->whereIn('parent_id', $categoryIds) - ->with([ - 'accounts' => function ($query) use ($request) { - $query->where('team_id', '=', $request->user()->current_team_id); - }, - 'category' - ])->get()->toArray(); - - $balance = $balanceByAccounts->get()->toArray(); - $categoryAccounts = array_map(function ($subCategory) use ($balance) { - $total = []; - if (isset($subCategory['accounts'])) { - foreach ($subCategory['accounts'] as $accountIndex => $account) { - $index = array_search($account['id'], array_column($balance, 'id')); - if ($index !== false ) { - $subCategory['accounts'][$accountIndex]['balance'] = $balance[$index]->total; - $total[] = $balance[$index]->total; - } - } - } - $subCategory['total'] = array_sum($total); - return $subCategory; - }, $categoryAccounts); - - - $ledger = DB::table('categories') - ->whereIn('categories.display_id', $categories[$category]) - ->selectRaw('sum(COALESCE(total, 0)) total, categories.alias, categories.display_id, categories.name') - ->leftJoinSub($balanceByAccounts, 'balance', function($join) { - $join->on('categories.id', 'balance.ledger_id'); - })->groupBy('categories.id')->get(); + [ + "ledger" => $ledger, + "categoryAccounts" => $categoryAccounts + ] = ReportHelper::getGeneralLedger(request()->user()->current_team_id, $reportName, [ + "account_id" => $accountId + ]); return Jetstream::inertia()->render($request, config('journal.statements_inertia_path') . '/Category', [ "categories" => $categoryAccounts, "ledger" => $ledger->groupBy('display_id'), - 'categoryType' => $category + 'categoryType' => $reportName ]); } } diff --git a/src/Jobs/Invoice/CreateExpenseDetails.php b/src/Jobs/Invoice/CreateExpenseDetails.php new file mode 100644 index 0000000..6792272 --- /dev/null +++ b/src/Jobs/Invoice/CreateExpenseDetails.php @@ -0,0 +1,47 @@ +invoice = $invoice; + $this->formData = $formData; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if (isset($this->formData['expense_details']) && $this->invoice->type == Invoice::DOCUMENT_TYPE_BILL) { + $expense = $this->formData['expense_details']; + $this->invoice->expenseDetails()->create([ + 'customer_id' => $expense['customer_id'], + 'payment_account_id' => $expense['customer_id'], + 'is_personal' => $expense['is_personal'], + 'is_billable' => $expense['is_billable'], + 'expense_type' => $expense['expense_type'] + ]); + } + } +} diff --git a/src/Models/Core/Category.php b/src/Models/Core/Category.php index 14e5adf..f66e744 100644 --- a/src/Models/Core/Category.php +++ b/src/Models/Core/Category.php @@ -118,19 +118,19 @@ public function getAllAccounts() { } public static function saveBulk(mixed $categories, mixed $extraData) { - + foreach ($categories as $index => $category) { $newCategory = array_merge($category, $extraData, ['index' => $index]); unset($newCategory['childs']); $parentCategory = Category::create($newCategory); - + if (isset($category['childs'])) { Category::saveBulk($category['childs'], array_merge( $extraData, [ 'depth' => $extraData['depth'] + 1, - 'type' => $parentCategory->type, + 'type' => $parentCategory->type, 'parent_id' => $parentCategory->id ])); } @@ -156,4 +156,16 @@ public function transactionBalance($clientId) { ->join('accounts', 'accounts.id', 'transaction_lines.account_id') ->get(); } + + + public function scopeHasAccounts($query, mixed $accountIds) { + if ($accountIds) { + return $query->whereHas('accounts', function($query) use ($accountIds) { + $query->when($accountIds, function($q) use ($accountIds) { + $q->whereIn('accounts.id', $accountIds); + }); + }); + } + return $query; + } } diff --git a/src/Models/Invoice/ExpenseDetail.php b/src/Models/Invoice/ExpenseDetail.php new file mode 100644 index 0000000..2d090bb --- /dev/null +++ b/src/Models/Invoice/ExpenseDetail.php @@ -0,0 +1,9 @@ + $invoice->total, ] )), - new CreateInvoiceRelations($invoice, $invoiceData) + new CreateInvoiceRelations($invoice, $invoiceData), + new CreateExpenseDetails($invoice, $invoiceData) ])->dispatch(); }); event(new InvoiceCreated($invoice, $invoiceData)); @@ -305,7 +307,7 @@ public static function checkStatus($invoice) $status = 'paid'; } elseif ($invoice->debt > 0 && $invoice->debt < $invoice->total) { $status = 'partial'; - } elseif ($invoice->debt && $invoice->due_date < date('Y-m-d')) { + } elseif ($invoice->debt > 0 && $invoice->due_date < date('Y-m-d')) { $status = 'overdue'; } elseif ($invoice->debt) { $status = 'unpaid'; @@ -435,7 +437,7 @@ protected function getBillPaymentItems($payment) $mainAccount = $isExpense ? $this->invoice_account_id : Account::where([ "team_id" => $this->team_id, "display_id" => "products"])->first()->id; - + $lineCount = 0; foreach ($this->lines as $line) { @@ -475,7 +477,7 @@ protected function getBillPaymentItems($payment) ]; } } - + // credits $items[] = [ "index" => count($items), diff --git a/src/database/migrations/2021_04_20_700000_create_expense_details_table.php b/src/database/migrations/2021_04_20_700000_create_expense_details_table.php new file mode 100644 index 0000000..ab71ff5 --- /dev/null +++ b/src/database/migrations/2021_04_20_700000_create_expense_details_table.php @@ -0,0 +1,45 @@ +id(); + $table->foreignId('user_id'); + $table->foreignId('team_id'); + $table->foreignId('invoice_id'); + $table->foreignId('customer_id'); + $table->string('customer_name'); + $table->foreignId('payment_account_id'); + $table->string('payment_account_name'); + + $table->boolean('is_personal')->default(false); + $table->boolean('is_billable')->default(true); + $table->enum('status', ['unbilled','billed'])->default('unbilled'); + + // structure + $table->timestamp('deleted_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('expense_details'); + } +};