currency_api = new CurrencyApi(); $this->company = $company; $this->payload = $payload; $this->setBillingReportType(); } public function run() { return $this->build()->getCsv(); } public function build() { MultiDB::setDb($this->company->db); if ($this->is_income_billed) { //get invoiced amounts $this->filterIncome(); } else { //$this->filterPaymentIncome(); $this->filterInvoicePaymentIncome(); } $this->expenseData()->buildExpenseBreakDown(); return $this; } public function getIncome() :float { return round($this->income, 2); } public function getIncomeMap() :array { return $this->income_map; } public function getIncomeTaxes() :float { return round($this->income_taxes, 2); } public function getExpenses() :array { return $this->expenses; } public function getExpenseBreakDown() :array { ksort($this->expense_break_down); return $this->expense_break_down; } private function filterIncome() { $invoices = $this->invoiceIncome(); $this->foreign_income = []; $this->income = 0; $this->income_taxes = 0; $this->income_map = $invoices; foreach ($invoices as $invoice) { $this->income += $invoice->net_converted_amount; $this->income_taxes += $invoice->net_converted_taxes; $currency = Currency::find(intval(str_replace('"', '', $invoice->currency_id))); $currency->name = ctrans('texts.currency_'.Str::slug($currency->name, '_')); $this->foreign_income[] = ['currency' => $currency->name, 'amount' => $invoice->amount, 'total_taxes' => $invoice->total_taxes]; } return $this; } private function filterInvoicePaymentIncome() { $this->paymentEloquentIncome(); foreach ($this->invoice_payment_map as $map) { $this->income += $map->amount_payment_paid_converted - $map->tax_amount_converted; $this->income_taxes += $map->tax_amount_converted; $this->credit += $map->amount_credit_paid_converted - $map->tax_amount_credit_converted; $this->credit_taxes += $map->tax_amount_credit_converted; } // $invoices = $this->invoicePaymentIncome(); // $this->income = 0; // $this->income_taxes = 0; // $this->income_map = $invoices; // foreach($invoices as $invoice){ // $this->income += $invoice->net_converted_amount; // $this->income_taxes += $invoice->net_converted_taxes; // } return $this; } private function getForeignIncome() :array { return $this->foreign_income; } private function filterPaymentIncome() { $payments = $this->paymentIncome(); return $this; } /* //returns an array of objects => [ {#2047 +"amount": "706.480000", +"total_taxes": "35.950000", +"currency_id": ""1"", +"net_converted_amount": "670.5300000000", +"net_converted_taxes": "10" }, {#2444 +"amount": "200.000000", +"total_taxes": "0.000000", +"currency_id": ""23"", +"net_converted_amount": "1.7129479802", +"net_converted_taxes": "10" }, {#2654 +"amount": "140.000000", +"total_taxes": "40.000000", +"currency_id": ""12"", +"net_converted_amount": "69.3275024282", +"net_converted_taxes": "10" }, ] */ private function invoiceIncome() { return \DB::select(\DB::raw(" SELECT sum(invoices.amount) as amount, sum(invoices.total_taxes) as total_taxes, (sum(invoices.total_taxes) / IFNULL(invoices.exchange_rate, 1)) AS net_converted_taxes, sum(invoices.amount - invoices.total_taxes) as net_amount, IFNULL(JSON_EXTRACT( settings, '$.currency_id' ), :company_currency) AS currency_id, (sum(invoices.amount - invoices.total_taxes) / IFNULL(invoices.exchange_rate, 1)) AS net_converted_amount FROM clients JOIN invoices on invoices.client_id = clients.id WHERE invoices.status_id IN (2,3,4) AND invoices.company_id = :company_id AND invoices.amount > 0 AND clients.is_deleted = 0 AND invoices.is_deleted = 0 AND (invoices.date BETWEEN :start_date AND :end_date) GROUP BY currency_id "), ['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $this->start_date, 'end_date' => $this->end_date]); } /** * The income calculation is based on the total payments received during * the selected time period. * * Once we have the payments we iterate through the attached invoices and * we also determine the total taxes paid as our * Profit and loss statement should be net of all taxes * * This calculation also considers partial payments and pro rata's any taxes. * * This calculation also considers exchange rates and we convert (based on the payment exchange rate) * to the native company currency. */ private function paymentEloquentIncome() { $this->invoice_payment_map = []; Payment::where('company_id', $this->company->id) ->whereIn('status_id', [1, 4, 5]) ->where('is_deleted', 0) ->whereBetween('date', [$this->start_date, $this->end_date]) ->whereHas('client', function ($query) { $query->where('is_deleted', 0); }) ->with(['company', 'client']) ->cursor() ->each(function ($payment) { $company = $payment->company; $client = $payment->client; $map = new \stdClass; $amount_payment_paid = 0; $amount_credit_paid = 0; $amount_payment_paid_converted = 0; $amount_credit_paid_converted = 0; $tax_amount = 0; $tax_amount_converted = 0; $tax_amount_credit = 0; $tax_amount_credit_converted = $tax_amount_credit_converted = 0; foreach ($payment->paymentables as $pivot) { if ($pivot->paymentable instanceof \App\Models\Invoice) { $invoice = $pivot->paymentable; $amount_payment_paid += $pivot->amount - $pivot->refunded; $amount_payment_paid_converted += $amount_payment_paid / ($payment->exchange_rate ?: 1); $tax_amount += ($amount_payment_paid / $invoice->amount) * $invoice->total_taxes; $tax_amount_converted += (($amount_payment_paid / $invoice->amount) * $invoice->total_taxes) / $payment->exchange_rate; } if ($pivot->paymentable instanceof \App\Models\Credit) { $amount_credit_paid += $pivot->amount - $pivot->refunded; $amount_credit_paid_converted += $amount_payment_paid / ($payment->exchange_rate ?: 1); $tax_amount_credit += ($amount_payment_paid / $invoice->amount) * $invoice->total_taxes; $tax_amount_credit_converted += (($amount_payment_paid / $invoice->amount) * $invoice->total_taxes) / $payment->exchange_rate; } } $map->amount_payment_paid = $amount_payment_paid; $map->amount_payment_paid_converted = $amount_payment_paid_converted; $map->tax_amount = $tax_amount; $map->tax_amount_converted = $tax_amount_converted; $map->amount_credit_paid = $amount_credit_paid; $map->amount_credit_paid_converted = $amount_credit_paid_converted; $map->tax_amount_credit = $tax_amount_credit; $map->tax_amount_credit_converted = $tax_amount_credit_converted; $map->currency_id = $payment->currency_id; $this->invoice_payment_map[] = $map; }); return $this; } /** => [ {#2047 +"amount": "110.000000", +"total_taxes": "10.0000000000000000", +"net_converted_amount": "110.0000000000", +"net_converted_taxes": "10.00000000000000000000", +"currency_id": ""1"", }, {#2444 +"amount": "50.000000", +"total_taxes": "4.5454545454545455", +"net_converted_amount": "61.1682150381", +"net_converted_taxes": "5.56074682164393914741", +"currency_id": ""2"", }, ] */ public function getCsv() { MultiDB::setDb($this->company->db); App::forgetInstance('translator'); App::setLocale($this->company->locale()); $t = app('translator'); $t->replace(Ninja::transformTranslations($this->company->settings)); $csv = Writer::createFromString(); $csv->insertOne([ctrans('texts.profit_and_loss')]); $csv->insertOne([ctrans('texts.company_name'), $this->company->present()->name()]); $csv->insertOne([ctrans('texts.date_range'), Carbon::parse($this->start_date)->format($this->company->date_format()), Carbon::parse($this->end_date)->format($this->company->date_format())]); //gross sales ex tax $csv->insertOne(['--------------------']); $csv->insertOne([ctrans('texts.total_revenue'), Number::formatMoney($this->income, $this->company)]); //total taxes $csv->insertOne([ctrans('texts.total_taxes'), Number::formatMoney($this->income_taxes, $this->company)]); //expense $csv->insertOne(['--------------------']); foreach ($this->expense_break_down as $expense_breakdown) { $csv->insertOne([$expense_breakdown['category_name'], Number::formatMoney($expense_breakdown['total'], $this->company)]); } //total expense taxes $csv->insertOne(['--------------------']); $csv->insertOne([ctrans('texts.total_expenses'), Number::formatMoney(array_sum(array_column($this->expense_break_down, 'total')), $this->company)]); $csv->insertOne([ctrans('texts.total_taxes'), Number::formatMoney(array_sum(array_column($this->expense_break_down, 'tax')), $this->company)]); $csv->insertOne(['--------------------']); $csv->insertOne([ctrans('texts.total_profit'), Number::formatMoney($this->income - array_sum(array_column($this->expense_break_down, 'total')), $this->company)]); //net profit $csv->insertOne(['--------------------']); $csv->insertOne(['']); $csv->insertOne(['']); $csv->insertOne([ctrans('texts.currency'), ctrans('texts.amount'), ctrans('texts.total_taxes')]); foreach ($this->foreign_income as $foreign_income) { $csv->insertOne([$foreign_income['currency'], ($foreign_income['amount'] - $foreign_income['total_taxes']), $foreign_income['total_taxes']]); } return $csv->toString(); } private function invoicePaymentIncome() { return \DB::select(\DB::raw(" SELECT sum(invoices.amount - invoices.balance) as amount, sum(invoices.total_taxes) * ((sum(invoices.amount - invoices.balance)/invoices.amount)) as total_taxes, (sum(invoices.amount - invoices.balance) / IFNULL(invoices.exchange_rate, 1)) AS net_converted_amount, (sum(invoices.total_taxes) * ((sum(invoices.amount - invoices.balance)/invoices.amount)) / IFNULL(invoices.exchange_rate, 1)) AS net_converted_taxes, IFNULL(JSON_EXTRACT( settings, '$.currency_id' ), :company_currency) AS currency_id FROM clients JOIN invoices on invoices.client_id = clients.id WHERE invoices.status_id IN (3,4) AND invoices.company_id = :company_id AND invoices.amount > 0 AND clients.is_deleted = 0 AND invoices.is_deleted = 0 AND (invoices.date BETWEEN :start_date AND :end_date) GROUP BY currency_id "), ['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $this->start_date, 'end_date' => $this->end_date]); } /** +"payments": "12260.870000", +"payments_converted": "12260.870000000000", +"currency_id": 1, */ private function paymentIncome() { return \DB::select(\DB::raw(' SELECT SUM(coalesce(payments.amount - payments.refunded,0)) as payments, SUM(coalesce(payments.amount - payments.refunded,0)) * IFNULL(payments.exchange_rate ,1) as payments_converted, payments.currency_id as currency_id FROM clients INNER JOIN payments ON clients.id=payments.client_id WHERE payments.status_id IN (1,4,5,6) AND clients.is_deleted = false AND payments.is_deleted = false AND payments.company_id = :company_id AND (payments.date BETWEEN :start_date AND :end_date) GROUP BY currency_id ORDER BY currency_id; '), ['company_id' => $this->company->id, 'start_date' => $this->start_date, 'end_date' => $this->end_date]); } private function expenseData() { $expenses = Expense::where('company_id', $this->company->id) ->where('is_deleted', 0) ->withTrashed() ->whereBetween('date', [$this->start_date, $this->end_date]) ->cursor(); $this->expenses = []; foreach ($expenses as $expense) { $map = new \stdClass; $amount = $expense->amount; $map->total = $expense->amount; $map->converted_total = $converted_total = $this->getConvertedTotal($expense->amount, $expense->exchange_rate); $map->tax = $tax = $this->getTax($expense); $map->net_converted_total = $expense->uses_inclusive_taxes ? ($converted_total - $tax) : $converted_total; $map->category_id = $expense->category_id; $map->category_name = $expense->category ? $expense->category->name : 'No Category Defined'; $map->currency_id = $expense->currency_id ?: $expense->company->settings->currency_id; $this->expenses[] = $map; } return $this; } private function buildExpenseBreakDown() { $data = []; foreach ($this->expenses as $expense) { if (! array_key_exists($expense->category_id, $data)) { $data[$expense->category_id] = []; } if (! array_key_exists('total', $data[$expense->category_id])) { $data[$expense->category_id]['total'] = 0; } if (! array_key_exists('tax', $data[$expense->category_id])) { $data[$expense->category_id]['tax'] = 0; } $data[$expense->category_id]['total'] += $expense->net_converted_total; $data[$expense->category_id]['category_name'] = $expense->category_name; $data[$expense->category_id]['tax'] += $expense->tax; } $this->expense_break_down = $data; return $this; } private function getTax($expense) { $amount = $expense->amount; //is amount tax if ($expense->calculate_tax_by_amount) { nlog($expense->tax_amount1); nlog($expense->tax_amount2); nlog($expense->tax_amount3); return $expense->tax_amount1 + $expense->tax_amount2 + $expense->tax_amount3; } if ($expense->uses_inclusive_taxes) { $inclusive = 0; $inclusive += ($amount - ($amount / (1 + ($expense->tax_rate1 / 100)))); $inclusive += ($amount - ($amount / (1 + ($expense->tax_rate2 / 100)))); $inclusive += ($amount - ($amount / (1 + ($expense->tax_rate3 / 100)))); return round($inclusive, 2); } $exclusive = 0; $exclusive += $amount * ($expense->tax_rate1 / 100); $exclusive += $amount * ($expense->tax_rate2 / 100); $exclusive += $amount * ($expense->tax_rate3 / 100); return $exclusive; } private function getConvertedTotal($amount, $exchange_rate = 1) { return round(($amount * $exchange_rate), 2); } private function expenseCalcWithTax() { return \DB::select(\DB::raw(' SELECT sum(expenses.amount) as amount, IFNULL(expenses.currency_id, :company_currency) as currency_id FROM expenses WHERE expenses.is_deleted = 0 AND expenses.company_id = :company_id AND (expenses.date BETWEEN :start_date AND :end_date) GROUP BY currency_id '), ['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $this->start_date, 'end_date' => $this->end_date]); } private function setBillingReportType() { if (array_key_exists('is_income_billed', $this->payload)) { $this->is_income_billed = boolval($this->payload['is_income_billed']); } if (array_key_exists('is_expense_billed', $this->payload)) { $this->is_expense_billed = boolval($this->payload['is_expense_billed']); } if (array_key_exists('include_tax', $this->payload)) { $this->is_tax_included = boolval($this->payload['include_tax']); } $this->addDateRange(); return $this; } private function addDateRange() { $date_range = 'this_year'; if (array_key_exists('date_range', $this->payload)) { $date_range = $this->payload['date_range']; } try { $custom_start_date = Carbon::parse($this->payload['start_date']); $custom_end_date = Carbon::parse($this->payload['end_date']); } catch (\Exception $e) { $custom_start_date = now()->startOfYear(); $custom_end_date = now(); } switch ($date_range) { case 'all': $this->start_date = now()->subYears(50); $this->end_date = now(); // return $query; // no break case 'last7': $this->start_date = now()->subDays(7); $this->end_date = now(); // return $query->whereBetween($this->date_key, [now()->subDays(7), now()])->orderBy($this->date_key, 'ASC'); // no break case 'last30': $this->start_date = now()->subDays(30); $this->end_date = now(); // return $query->whereBetween($this->date_key, [now()->subDays(30), now()])->orderBy($this->date_key, 'ASC'); // no break case 'this_month': $this->start_date = now()->startOfMonth(); $this->end_date = now(); //return $query->whereBetween($this->date_key, [now()->startOfMonth(), now()])->orderBy($this->date_key, 'ASC'); // no break case 'last_month': $this->start_date = now()->startOfMonth()->subMonth(); $this->end_date = now()->startOfMonth()->subMonth()->endOfMonth(); //return $query->whereBetween($this->date_key, [now()->startOfMonth()->subMonth(), now()->startOfMonth()->subMonth()->endOfMonth()])->orderBy($this->date_key, 'ASC'); // no break case 'this_quarter': $this->start_date = (new \Carbon\Carbon('-3 months'))->firstOfQuarter(); $this->end_date = (new \Carbon\Carbon('-3 months'))->lastOfQuarter(); //return $query->whereBetween($this->date_key, [(new \Carbon\Carbon('-3 months'))->firstOfQuarter(), (new \Carbon\Carbon('-3 months'))->lastOfQuarter()])->orderBy($this->date_key, 'ASC'); // no break case 'last_quarter': $this->start_date = (new \Carbon\Carbon('-6 months'))->firstOfQuarter(); $this->end_date = (new \Carbon\Carbon('-6 months'))->lastOfQuarter(); //return $query->whereBetween($this->date_key, [(new \Carbon\Carbon('-6 months'))->firstOfQuarter(), (new \Carbon\Carbon('-6 months'))->lastOfQuarter()])->orderBy($this->date_key, 'ASC'); // no break case 'this_year': $this->start_date = now()->startOfYear(); $this->end_date = now(); //return $query->whereBetween($this->date_key, [now()->startOfYear(), now()])->orderBy($this->date_key, 'ASC'); // no break case 'custom': $this->start_date = $custom_start_date; $this->end_date = $custom_end_date; //return $query->whereBetween($this->date_key, [$custom_start_date, $custom_end_date])->orderBy($this->date_key, 'ASC'); // no break default: $this->start_date = now()->startOfYear(); $this->end_date = now(); // return $query->whereBetween($this->date_key, [now()->startOfYear(), now()])->orderBy($this->date_key, 'ASC'); } return $this; } }