![]() Server : Apache System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64 User : corals ( 1002) PHP Version : 7.4.33 Disable Function : exec,passthru,shell_exec,system Directory : /home/corals/ts.corals.io/corals-api/Corals/modules/Timesheet/Classes/ |
<?php namespace Corals\Modules\Timesheet\Classes; use Carbon\Carbon; use Corals\Modules\Timesheet\Models\Entry; use Corals\Modules\Timesheet\Models\Expense; use Corals\Modules\Timesheet\Models\Invoice; use Corals\Modules\Timesheet\Models\Project; use Corals\Modules\Timesheet\Services\EntryService; use Corals\Modules\Timesheet\Services\InvoiceService; use Corals\Settings\Facades\Settings; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; class InvoiceGenerator { /** * @var array */ protected array $relatedEntries = []; protected array $invoiceItems = []; protected $object; /** * @var BagParameters */ protected BagParameters $bagParameters; protected InvoiceService $invoiceService; /** * InvoiceGenerator constructor. * @param BagParameters $bagParameters */ public function __construct(BagParameters $bagParameters) { $this->bagParameters = $bagParameters; $this->invoiceService = new InvoiceService(); } /** * @return array|\Illuminate\Support\HigherOrderTapProxy|mixed * @throws \Exception */ public function generate() { try { DB::beginTransaction(); $calculationsResult = $this->calculateTotalAmount(); if ($this->bagParameters->get('only_totals')) { return ['amount' => $calculationsResult, 'hours' => $this->getInvoiceEntriesTimeTotal()]; } $invoice = tap($this->createInvoice($calculationsResult), function (Invoice $invoice = null) { $this->updateCreatedEntries($invoice); }); DB::commit(); return $invoice; } catch (\Exception $exception) { DB::rollBack(); throw $exception; } } protected function getInvoiceEntriesTimeTotal() { return round(collect($this->relatedEntries)->sum('hours'), 2); } /** * @param Invoice|null $invoice */ public function updateCreatedEntries(Invoice $invoice = null) { if (!$invoice || !$this->relatedEntries) { return; } foreach ($this->relatedEntries as $entry) { Entry::query() ->where('id', $entry['id']) ->update(array_merge( Arr::only($entry, ['amount', 'hourly_rate']), [ 'invoice_id' => $invoice->id, ]) ); } } /** * @return int|mixed */ protected function calculateTotalAmount() { if ($this->bagParameters->hasProject()) { $this->object = $this->bagParameters->getProject(); return $this->calculateTotalAmountForProject(); } if ($this->bagParameters->hasClient()) { $this->object = $this->bagParameters->getClient(); return $this->calculateTotalAmountForClient(); } } /** * @param $totalAmount * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|void */ public function createInvoice($subtotalAmount) { if ($this->bagParameters->has('date_range')) { $invoiceDate = $this->bagParameters->get('date_range')[1]; } else { $entry = collect($this->relatedEntries)->sortByDesc('spent_at')->first(); $spent_at = $entry['spent_at']; $client = $this->bagParameters->getClient(); $cycleForObject = (new EntryService())->setClient($client)->getBillingCycleEdges($spent_at, $this->object); $invoiceDate = $cycleForObject['end']; } $invoiceDate = Carbon::parse($invoiceDate)->subDay(); $invoiceStatus = 'pending'; if ($this->bagParameters->has('status')) { $invoiceStatus = $this->bagParameters->get('status'); } $project = $this->bagParameters->getProject(); if (!$project) { if ($this->bagParameters->getClient()->projects()->count() === 1) { $project = $this->bagParameters->getClient()->projects()->first(); } } //FIXME handle taxes for invoices in different way if ($project && $project->taxable) { $taxTotal = $subtotalAmount * Settings::get('sales_tax_rate'); $totalAmount = $subtotalAmount + $taxTotal; $this->invoiceItems[] = [ 'title' => $project->name . ' taxes', 'type' => 'tax', 'rate' => Settings::get('sales_tax_rate'), 'quantity' => $subtotalAmount, 'amount' => $taxTotal, 'notes' => null, ]; } else { $totalAmount = $subtotalAmount; //TODO:: handle multiple projects (client level) } /** * @var Invoice $invoice */ $invoice = Invoice::query()->create([ 'code' => Invoice::getCode('INV'), 'project_id' => data_get($project, 'id'), 'client_id' => data_get($this->bagParameters->getClient(), 'id'), 'subtotal' => $subtotalAmount, 'tax_total' => $taxTotal ?? 0, 'total' => $totalAmount, 'invoice_date' => $invoiceDate, 'due_date' => $invoiceDate, 'status' => $invoiceStatus, 'properties' => [ 'total_hours' => $this->getInvoiceEntriesTimeTotal(), ] ]); $expenseObject = $project ?: $this->bagParameters->getClient(); $expenseObject->expenses() ->where('billable', 1) ->whereDate('expense_date', '<=', $invoiceDate) ->whereNull('invoice_id') ->eachById(function (Expense $expense) use ($invoice) { $this->invoiceItems[] = [ 'title' => $expense->category->name, 'type' => 'expense', 'rate' => $expense->amount, 'quantity' => 1, 'amount' => $expense->amount, 'notes' => sprintf("Date: %s, %s", format_date($expense->expense_date), $expense->notes), ]; $expense->invoice_id = $invoice->id; $expense->status = 'paid'; $expense->save(); }); $invoice->items()->createMany($this->invoiceItems); $totals = $this->invoiceService->calculateInvoiceTotals($invoice->items); foreach ($totals as $key => $value) { if ($key == 'total_hours') { $invoice->setProperty($key, $value, false); continue; } $invoice->{$key} = $value; } $invoice->save(); return $invoice->fresh(); } /** * @return int|mixed */ public function calculateTotalAmountForClient() { $client = $this->bagParameters->getClient(); $totalAmount = 0; $client->billableProjects()->each(function ($project) use (&$totalAmount) { $totalAmount += $this->calculateTotalAmountForProject($project); }); return $totalAmount; } /** * @param Project|null $project * @return mixed */ public function calculateTotalAmountForProject(Project $project = null) { if (is_null($project)) { $project = $this->bagParameters->getProject(); } $totalAmount = 0; $quantity = 0; $userSumDetails = []; $project->entries() ->when($this->bagParameters->has('date_range'), function (Builder $builder) { $builder->where(function ($mainQuery) { $mainQuery->where(function ($dateQuery) { $dateQuery->where('spent_at', '>=', $this->bagParameters->get('date_range')[0]) ->where('spent_at', '<', $this->bagParameters->get('date_range')[1]); }); if ($this->bagParameters->get('include_previous_entries')) { $mainQuery->orWhere(function ($prevEntriesQuery) { $prevEntriesQuery->where('spent_at', '<', $this->bagParameters->get('date_range')[0]) ->whereNull('invoice_id'); }); } }); }) ->when(!$this->bagParameters->has('date_range'), function (Builder $builder) { $builder->where(function ($query) { $client = $this->bagParameters->getClient(); $cycleForObject = (new EntryService())->setClient($client)->getBillingCycleEdges(now(), $this->object); $query->where('spent_at', '<', $cycleForObject['start']); }); }) ->whereNull('invoice_id') ->where('has_reviewed', '=', 1) ->whereHas('activity', function ($activityQuery) { $activityQuery->where('billable', 1); })->with('activity') ->get() ->each(function (Entry $entry) use ($project, &$totalAmount, &$quantity, &$userSumDetails) { $hours = \Corals\Modules\Timesheet\Facades\Timesheet::getEntryTotalHours($entry); $this->relatedEntries[] = [ 'id' => $entry->id, 'amount' => $entry->amount, 'hourly_rate' => $entry->hourlyRate, 'hours' => $hours, 'spent_at' => $entry->spent_at, ]; if (empty($userSumDetails[$entry->user_id])) { $userSumDetails[$entry->user_id] = [ 'name' => $entry->user->name, 'hours' => 0, 'amount' => 0, ]; } $userSumDetails[$entry->user_id]['hours'] += round($hours, 2); $userSumDetails[$entry->user_id]['amount'] += $entry->amount; $totalAmount += $entry->amount; if ($hours) { $quantity += $hours; } }); $quantity = round($quantity, 2); $rate = $quantity ? round($totalAmount / $quantity, 2) : 0; $amount = $rate * $quantity; if (!$amount) { return 0; } $invoiceItemDescription = sprintf("Hours Total: %s Avg Rate: %s\n", $quantity, $rate); foreach ($userSumDetails as $user) { $invoiceItemDescription .= sprintf("<small>%s [Hours: %s Amount: %s]</small>\n", $user['name'], $user['hours'], $user['amount']); } $this->invoiceItems[] = [ 'title' => $project->name, 'type' => 'hours', 'rate' => $rate, 'quantity' => $quantity, 'amount' => $amount, 'notes' => null, 'properties' => [ 'invoice_description' => $invoiceItemDescription ] ]; return round($totalAmount, 2); } }