diff --git a/src/Events/QuoteCreated.php b/src/Events/QuoteCreated.php new file mode 100644 index 0000000..4dc970a --- /dev/null +++ b/src/Events/QuoteCreated.php @@ -0,0 +1,12 @@ +groupBy($groupBy) : $results; } + public static function getTransactionsByCategory(int $teamId, array $accounts, $startDate = null, $endDate = null, $transactionableType = null) { + $endDate = $endDate ?? Carbon::now()->endOfMonth()->format('Y-m-d'); + $startDate = $startDate ?? Carbon::now()->startOfMonth()->format('Y-m-d'); + + return DB::table('transaction_lines') + ->whereBetween('transactions.date', [$startDate, $endDate]) + ->where([ + 'transaction_lines.team_id' => $teamId, + 'transactions.status' => Transaction::STATUS_VERIFIED, + ]) + ->when($transactionableType, fn ($q) => $q->where('transactionable_type', $transactionableType) ) + ->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, 0)) as total, + SUM(CASE + WHEN transaction_lines.type = 1 THEN transaction_lines.amount + ELSE 0 + END) as income, + SUM(CASE + WHEN transaction_lines.type = -1 THEN transaction_lines.amount + ELSE 0 + END) outcome, + accounts.id account_id, + group_concat(concat(transaction_lines.amount * transaction_lines.type, ":", transaction_lines.id)) details, + date_format(transaction_lines.date, "%Y-%m-01") as date, + accounts.display_id account_display_id, + accounts.name account_name, + accounts.alias account_alias, + categories.name, + categories.id, + categories.display_id, + categories.alias, + g.display_id groupName, + g.alias groupAlias, + MONTH(transactions.date) as months' + )->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') + ->get(); + } + public static function getAccountTransactionsByMonths(int $teamId, array $accounts, $startDate = null, $endDate = null, $groupBy = null, $transactionableType = null) { $endDate = $endDate ?? Carbon::now()->endOfMonth()->format('Y-m-d'); $startDate = $startDate ?? Carbon::now()->startOfMonth()->format('Y-m-d'); diff --git a/src/Jobs/Quote/CreateQuoteLine.php b/src/Jobs/Quote/CreateQuoteLine.php new file mode 100644 index 0000000..5e52b3a --- /dev/null +++ b/src/Jobs/Quote/CreateQuoteLine.php @@ -0,0 +1,96 @@ +where('quote_id', $this->quote->id)->delete(); + QuoteLineTax::query()->where('quote_id', $this->quote->id)->delete(); + if (isset($this->formData['items']) && count($this->formData['items'])) { + foreach ($this->formData['items'] as $index => $item) { + $line = $this->quote->lines()->create([ + "team_id" => $this->quote->team_id, + "user_id" => $this->quote->user_id, + "concept" => $item['concept'], + "category_id" => $item['category_id'] ?? null, + "date" => $item['date'] ?? $this->quote->date, + "index" => $item['index'] ?? $index, + "product_id" => $item['product_id'] ?? null, + "quantity" => $item['quantity'], + "price" => $item['price'], + "amount" => $item['amount'], + "product_image" => $item['product_image'] ?? "", + "meta_data" => $item['meta_data'] ?? [], + ]); + + isset($item['taxes']) ? $this->createItemTaxes($item['taxes'], $line) : null; + } + } else { + $line = $this->quote->lines()->create([ + "team_id" => $this->quote->team_id, + "user_id" => $this->quote->user_id, + "concept" => $this->quote->concept, + "category_id" => null, + "date" => $this->quote->date, + "index" => 0, + "product_id" => null, + "quantity" => 1, + "price" => $this->formData['total'], + "amount" => $this->formData['total'], + "product_image" => "", + "meta_data" => [], + ]); + + } + + $this->quote->save(); + return $this->quote; + } + + private function createItemTaxes($taxes, $line) { + foreach ($taxes as $index => $tax) { + if (isset($tax['name'])) { + $taxRate = (double) $tax['rate']; + $taxLineTotal = (double) $taxRate * $line->amount / 100; + $line->taxes()->create([ + "team_id" => $this->quote->team_id, + "user_id" => $this->quote->user_id, + "quote_id" => $this->quote->id, + "quote_line_id" => $line->id, + "tax_id" => $tax['id'], + "name" => $tax['name'], + "is_fixed" => $tax['is_fixed'], + "label" => $tax['label'], + "concept" => $tax['description'] ?? $tax['concept'], + "rate" => $taxRate, + "type" => $tax['type'], + "amount" => $tax['amount'] ?? $taxLineTotal, + "amount_base" => $line->amount, + "index" => $index, + ]); + } + } + } +} diff --git a/src/Models/Invoice/Invoice.php b/src/Models/Invoice/Invoice.php index 188aebf..3c89e33 100644 --- a/src/Models/Invoice/Invoice.php +++ b/src/Models/Invoice/Invoice.php @@ -431,26 +431,50 @@ public static function createContactAccount($invoice) public static function createInvoiceAccount($invoice) { if ($invoice->invoice_account_id) return $invoice->invoice_account_id; - $accounts = Account::where([ - 'display_id' => 'sales', - 'team_id' => $invoice->team_id - ])->limit(1)->get(); - - if (count($accounts)) { - return $accounts[0]->id; - } else { - $category = Category::where('display_id', 'operating_income')->first(); - $account = Account::create([ - "team_id" => $invoice->team_id, - "client_id" => 0, - "user_id" => $invoice->user_id, - "category_id" => $category->id, - "display_id" => "sales", - "name" => "Sales", - "currency_code" => "DOP" - ]); - return $account->id; - } + if ($invoice->isOutgoingMovement()) { + $accounts = Account::where([ + 'display_id' => 'general_expenses', + 'team_id' => $invoice->team_id + ])->limit(1)->get(); + + if (count($accounts)) { + return $accounts[0]->id; + } else { + $category = Category::where('display_id', 'operating_expense')->first(); + $account = Account::create([ + "team_id" => $invoice->team_id, + "client_id" => 0, + "user_id" => $invoice->user_id, + "category_id" => $category->id, + "display_id" => "general_expenses", + "name" => "General Expenses", + "alias" => __("General Expenses"), + "currency_code" => "DOP" + ]); + return $account->id; + } + } else { + $accounts = Account::where([ + 'display_id' => 'sales', + 'team_id' => $invoice->team_id + ])->limit(1)->get(); + + if (count($accounts)) { + return $accounts[0]->id; + } else { + $category = Category::where('display_id', 'operating_income')->first(); + $account = Account::create([ + "team_id" => $invoice->team_id, + "client_id" => 0, + "user_id" => $invoice->user_id, + "category_id" => $category->id, + "display_id" => "sales", + "name" => "Sales", + "currency_code" => "DOP" + ]); + return $account->id; + } + } } public function getInvoiceData() { diff --git a/src/Models/Quote/Quote.php b/src/Models/Quote/Quote.php new file mode 100644 index 0000000..27fada1 --- /dev/null +++ b/src/Models/Quote/Quote.php @@ -0,0 +1,214 @@ +series = $quote->series ?? substr($quote->date, 0, 4); + self::setNumber($quote); + }); + + static::saving(function ($quote) { + self::calculateTotal($quote); + }); + + static::deleting(function ($quote) { + $quote->status = 'draft'; + $quote->save(); + foreach ($quote->lines as $item) { + if ($item->product) { + $item->product->updateStock(); + } + } + + $quote->lines()->delete(); + $quote->taxesLines()->delete(); + // DB::table('document_relations')->where('main_document_id', $quote->id)->delete(); + }); + + static::deleted(function ($quote) { + event(new QuoteDeleted($quote)); + }); + } + + public function scopeCategory($query, $category) + { + return $query->where('invoices.category_type', $category); + } + + public function scopeCategoryType($query, $category) + { + return $query->where('invoices.category_type', $category); + } + + public function scopeByClient($query, $clientId = null) { + if ($clientId) { + $query->where('invoices.client_id', $clientId); + } + return $query; + } + + public function scopeByTeam($query, $teamId = null) { + if ($teamId) { + $query->where('invoices.team_id', $teamId); + } + return $query; + } + + // relationships + public function user() + { + return $this->belongsTo(User::class); + } + + public function team() + { + return $this->belongsTo(Team::class); + } + + public function client() + { + return $this->belongsTo(Journal::$customerModel, 'client_id', 'id'); + } + + public function quotable() + { + return $this->morphTo('quotable'); + } + + public function lines() + { + return $this->hasMany(QuoteLine::class); + } + + public function taxesLines() + { + return $this->hasMany(QuoteLineTax::class); + } + + + // Utils + public static function setNumber($quote) + { + $isInvalidNumber = true; + + if ($quote->number) { + $isInvalidNumber = self::where([ + "team_id" => $quote->team_id, + "series" => $quote->series, + "number" => $quote->number, + ])->whereNot([ + "id" => $quote->id + ])->get(); + + $isInvalidNumber = count($isInvalidNumber); + } + + if ($isInvalidNumber) { + $result = self::where([ + "team_id" => $quote->team_id, + "series" => $quote->series, + ])->max('number'); + $quote->number = $result + 1; + } + } + + public static function calculateTotal($quote) + { + $total = QuoteLine::where(["quote_id" => $quote->id])->selectRaw('sum(price) as price, sum(discount) as discount, sum(amount) as amount')->get(); + $totalTax = QuoteLineTax::where(["quote_id" => $quote->id])->selectRaw('sum(amount * type) as amount')->get(); + + $discount = $total[0]['discount'] ?? 0; + $taxTotal = $totalTax[0]['amount'] ?? 0; + $quoteTotal = ($total[0]['amount'] ?? 0); + $quote->subtotal = $total[0]['price'] ?? 0; + $quote->discount = $discount; + $quote->total = $quoteTotal + $taxTotal - $discount; + } + + public static function createDocument($quoteData) { + $quote = self::create($quoteData); + event(new QuoteSaving($quoteData)); + DB::transaction(function () use ($quoteData, $quote) { + Bus::chain([ + new CreateQuoteLine($quote, $quoteData), + ])->dispatch(); + }); + event(new QuoteCreated($quote, $quoteData)); + return $quote; + } + + public function updateDocument($postData) { + $this->update($postData); + Bus::chain([ + new CreateQuoteLine($this, $postData), + ])->dispatch(); + return $this; + } + + public function addLine($lines = []) { + $this->updateDocument([ + ...$this->toArray(), + "items" => [ + ...$this->lines, + ...$lines + ] + ]); + } + + public function getQuoteData() { + $quoteData = $this->toArray(); + $quoteData['client'] = $this->client; + $quoteData['lines'] = $this->lines->toArray(); + return $quoteData; + } +} diff --git a/src/Models/Quote/QuoteLine.php b/src/Models/Quote/QuoteLine.php new file mode 100644 index 0000000..31d8712 --- /dev/null +++ b/src/Models/Quote/QuoteLine.php @@ -0,0 +1,39 @@ + 'array']; + + public function product() { + return $this->belongsTo(Product::class); + } + + public function quote() { + return $this->belongsTo(Quote::class); + } + + public function taxes() { + return $this->hasMany(QuoteLineTax::class); + } +} diff --git a/src/Models/Quote/QuoteLineTax.php b/src/Models/Quote/QuoteLineTax.php new file mode 100644 index 0000000..e0e5648 --- /dev/null +++ b/src/Models/Quote/QuoteLineTax.php @@ -0,0 +1,37 @@ +belongsTo(Product::class); + } + + public function tax() { + return $this->belongsTo(Tax::class); + } +} diff --git a/src/database/migrations/2024_01_04_100000_create_quotes_table.php b/src/database/migrations/2024_01_04_100000_create_quotes_table.php new file mode 100644 index 0000000..2f301c7 --- /dev/null +++ b/src/database/migrations/2024_01_04_100000_create_quotes_table.php @@ -0,0 +1,77 @@ +id(); + $table->foreignId('user_id'); + $table->foreignId('team_id'); + $table->foreignId('client_id'); + $table->foreignId('quotable_id')->nullable(); + $table->string('quotable_type')->nullable(); + $table->foreignId('invoice_id')->nullable(); + + // content + $table->string('series', 10); + $table->integer('number'); + $table->text('order_number')->nullable(); + $table->date('date'); + $table->date('due_date'); + + // header + $table->string('concept', 50); + $table->string('description', 200); + $table->text('logo')->nullable(); + // contact information + $table->string('contact_name')->nullable(); + $table->string('contact_email')->nullable(); + $table->string('contact_tax_number')->nullable(); + $table->string('contact_phone')->nullable(); + $table->string('contact_address')->nullable(); + + // footer + $table->text('notes')->nullable(); + $table->text('footer')->nullable(); + + // totals + $table->decimal('subtotal', 11, 2)->default(0.00); + $table->decimal('penalty', 11, 2)->default(0.00); + $table->decimal('extra_amount', 11, 2)->default(0.00); + $table->decimal('discount', 11, 2)->default(0.00); + $table->decimal('total', 11, 2)->default(0.00); + $table->decimal('debt', 11, 2)->default(0.00); + $table->string('currency_code')->default("DOP"); + $table->decimal('currency_rate', 11, 4)->default(1); + $table->boolean('taxes_included')->default(false); + + $table->string('category_type')->nullable(); + $table->enum('type', ['INVOICE','EXPENSE'])->default('INVOICE'); + $table->string('status')->default('draft'); + + // structure + $table->timestamp('deleted_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('quotes'); + } +}; diff --git a/src/database/migrations/2024_01_04_200000_create_quote_lines_table.php b/src/database/migrations/2024_01_04_200000_create_quote_lines_table.php new file mode 100644 index 0000000..2c7da90 --- /dev/null +++ b/src/database/migrations/2024_01_04_200000_create_quote_lines_table.php @@ -0,0 +1,48 @@ +id(); + $table->foreignId('user_id'); + $table->foreignId('team_id'); + $table->foreignId('quote_id'); + $table->foreignId('product_id')->nullable(); + + // context + $table->date('date'); + $table->text('concept', 200); + $table->text('product_image')->nullable(); + $table->json('meta_data')->nullable(); + $table->decimal('price', 11, 2)->default(0.00); + $table->decimal('quantity', 11, 2)->default(0.00); + $table->decimal('discount', 11, 2)->default(0.00); + $table->decimal('amount', 11, 2)->default(0.00); + $table->integer('index')->nullable(); + + // structure + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('quote_lines'); + } +}; diff --git a/src/database/migrations/2024__01_04_300000_create_quote_line_taxes_table.php b/src/database/migrations/2024__01_04_300000_create_quote_line_taxes_table.php new file mode 100644 index 0000000..8c68764 --- /dev/null +++ b/src/database/migrations/2024__01_04_300000_create_quote_line_taxes_table.php @@ -0,0 +1,44 @@ +id(); + $table->foreignId('user_id'); + $table->foreignId('team_id'); + $table->foreignId('quote_id'); + $table->foreignId('quote_line_id'); + $table->foreignId('tax_id'); + $table->integer('index')->default(0); + $table->string('name'); + $table->string('label')->nullable(); + $table->string('concept')->nullable(); + $table->decimal('amount', 11, 4); + $table->decimal('amount_base', 11, 4); + $table->decimal('rate', 11, 2)->default(0.00); + $table->integer('type')->default(1); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('quote_line_taxes'); + } +};