akaunting/app/Models/Document/Document.php

711 lines
22 KiB
PHP
Raw Normal View History

2020-12-24 01:28:38 +03:00
<?php
namespace App\Models\Document;
use App\Abstracts\Model;
2023-04-29 01:43:48 +03:00
use App\Interfaces\Utility\DocumentNumber;
2021-01-22 12:38:17 +03:00
use App\Models\Common\Media as MediaModel;
2020-12-24 01:28:38 +03:00
use App\Models\Setting\Tax;
2020-12-26 16:13:34 +03:00
use App\Scopes\Document as Scope;
2020-12-24 01:28:38 +03:00
use App\Traits\Currencies;
use App\Traits\DateTime;
use App\Traits\Documents;
use App\Traits\Media;
use App\Traits\Recurring;
use Bkwld\Cloner\Cloneable;
use Database\Factories\Document as DocumentFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Document extends Model
{
use HasFactory, Documents, Cloneable, Currencies, DateTime, Media, Recurring;
public const INVOICE_TYPE = 'invoice';
2022-06-01 10:15:55 +03:00
public const INVOICE_RECURRING_TYPE = 'invoice-recurring';
2020-12-24 01:28:38 +03:00
public const BILL_TYPE = 'bill';
2022-06-01 10:15:55 +03:00
public const BILL_RECURRING_TYPE = 'bill-recurring';
2020-12-24 01:28:38 +03:00
protected $table = 'documents';
2021-11-08 02:40:40 +03:00
protected $appends = ['attachment', 'amount_without_tax', 'discount', 'paid', 'received_at', 'status_label', 'sent_at', 'reconciled', 'contact_location'];
2020-12-24 01:28:38 +03:00
protected $fillable = [
'company_id',
'type',
'document_number',
'order_number',
'status',
'issued_at',
'due_at',
'amount',
'currency_code',
'currency_rate',
2021-06-17 10:59:07 +03:00
'category_id',
2020-12-24 01:28:38 +03:00
'contact_id',
'contact_name',
'contact_email',
'contact_tax_number',
'contact_phone',
'contact_address',
2021-11-08 02:40:40 +03:00
'contact_country',
'contact_state',
'contact_zip_code',
'contact_city',
2020-12-24 01:28:38 +03:00
'notes',
'footer',
2021-06-17 10:59:07 +03:00
'parent_id',
2021-09-07 10:33:34 +03:00
'created_from',
2021-06-17 10:59:07 +03:00
'created_by',
2020-12-24 01:28:38 +03:00
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
2023-03-16 16:36:13 +03:00
'issued_at' => 'datetime',
'due_at' => 'datetime',
'amount' => 'double',
2020-12-24 01:28:38 +03:00
'currency_rate' => 'double',
2023-03-16 16:36:13 +03:00
'deleted_at' => 'datetime',
2020-12-24 01:28:38 +03:00
];
/**
* @var array
*/
public $sortable = ['document_number', 'contact_name', 'amount', 'status', 'issued_at', 'due_at'];
/**
* @var array
*/
public $cloneable_relations = ['items', 'recurring', 'totals'];
/**
2021-01-19 17:27:19 +03:00
* The "booted" method of the model.
2020-12-24 01:28:38 +03:00
*
* @return void
*/
2021-01-19 17:27:19 +03:00
protected static function booted()
2020-12-24 01:28:38 +03:00
{
static::addGlobalScope(new Scope);
}
public function category()
{
2022-07-22 00:52:06 +03:00
return $this->belongsTo('App\Models\Setting\Category')->withoutGlobalScope('App\Scopes\Category')->withDefault(['name' => trans('general.na')]);
2020-12-24 01:28:38 +03:00
}
2022-06-01 10:15:55 +03:00
public function children()
{
return $this->hasMany('App\Models\Document\Document', 'parent_id');
}
2020-12-24 01:28:38 +03:00
public function contact()
{
return $this->belongsTo('App\Models\Common\Contact')->withDefault(['name' => trans('general.na')]);
}
public function currency()
{
return $this->belongsTo('App\Models\Setting\Currency', 'currency_code', 'code');
}
public function items()
{
return $this->hasMany('App\Models\Document\DocumentItem', 'document_id');
}
public function item_taxes()
{
return $this->hasMany('App\Models\Document\DocumentItemTax', 'document_id');
}
public function histories()
{
return $this->hasMany('App\Models\Document\DocumentHistory', 'document_id');
}
2022-06-01 10:15:55 +03:00
public function last_history()
{
return $this->hasOne('App\Models\Document\DocumentHistory', 'document_id')->latest()->withDefault([
'description' => trans('messages.success.added', ['type' => $this->document_number]),
'created_at' => $this->created_at
]);
}
public function parent()
{
2022-06-05 10:15:59 +03:00
return $this->belongsTo('App\Models\Document\Document', 'parent_id')->isRecurring();
2022-06-01 10:15:55 +03:00
}
2020-12-24 01:28:38 +03:00
public function payments()
{
return $this->transactions();
}
public function recurring()
{
return $this->morphOne('App\Models\Common\Recurring', 'recurable');
}
public function totals()
{
return $this->hasMany('App\Models\Document\DocumentTotal', 'document_id');
}
public function transactions()
{
2021-01-23 23:29:22 +03:00
return $this->hasMany('App\Models\Banking\Transaction', 'document_id');
2020-12-24 01:28:38 +03:00
}
public function totals_sorted()
{
return $this->totals()->orderBy('sort_order');
}
2022-06-01 10:15:55 +03:00
public function scopeLatest(Builder $query): Builder
2020-12-24 01:28:38 +03:00
{
return $query->orderBy('issued_at', 'desc');
}
2022-06-01 10:15:55 +03:00
public function scopeNumber(Builder $query, string $number): Builder
2020-12-24 01:28:38 +03:00
{
return $query->where('document_number', '=', $number);
}
2022-06-01 10:15:55 +03:00
public function scopeDue(Builder $query, $date): Builder
2020-12-24 01:28:38 +03:00
{
return $query->whereDate('due_at', '=', $date);
}
2022-06-14 00:36:26 +03:00
public function scopeStatus(Builder $query, string $status): Builder
{
return $query->where($this->qualifyColumn('status'), '=', $status);
}
2022-06-01 10:15:55 +03:00
public function scopeAccrued(Builder $query): Builder
2020-12-24 01:28:38 +03:00
{
2022-06-14 00:36:26 +03:00
return $query->whereNotIn($this->qualifyColumn('status'), ['draft', 'cancelled']);
2020-12-24 01:28:38 +03:00
}
2022-06-01 10:15:55 +03:00
public function scopePaid(Builder $query): Builder
2020-12-24 01:28:38 +03:00
{
2022-06-14 00:36:26 +03:00
return $query->where($this->qualifyColumn('status'), '=', 'paid');
2020-12-24 01:28:38 +03:00
}
2022-06-01 10:15:55 +03:00
public function scopeNotPaid(Builder $query): Builder
2020-12-24 01:28:38 +03:00
{
2022-06-14 00:36:26 +03:00
return $query->where($this->qualifyColumn('status'), '<>', 'paid');
2020-12-24 01:28:38 +03:00
}
2022-06-01 10:15:55 +03:00
public function scopeFuture(Builder $query): Builder
{
2022-06-14 00:36:26 +03:00
return $query->whereIn($this->qualifyColumn('status'), $this->getDocumentStatusesForFuture());
2022-06-01 10:15:55 +03:00
}
public function scopeType(Builder $query, string $type): Builder
2020-12-24 01:28:38 +03:00
{
2021-09-10 09:41:15 +03:00
return $query->where($this->qualifyColumn('type'), '=', $type);
2020-12-24 01:28:38 +03:00
}
2022-06-01 10:15:55 +03:00
public function scopeInvoice(Builder $query): Builder
2020-12-24 01:28:38 +03:00
{
2021-09-10 09:41:15 +03:00
return $query->where($this->qualifyColumn('type'), '=', self::INVOICE_TYPE);
2020-12-24 01:28:38 +03:00
}
2022-06-01 10:15:55 +03:00
public function scopeInvoiceRecurring(Builder $query): Builder
{
return $query->where($this->qualifyColumn('type'), '=', self::INVOICE_RECURRING_TYPE)
->whereHas('recurring', function (Builder $query) {
$query->whereNull('deleted_at');
});
2022-06-01 10:15:55 +03:00
}
public function scopeBill(Builder $query): Builder
2020-12-24 01:28:38 +03:00
{
2021-09-10 09:41:15 +03:00
return $query->where($this->qualifyColumn('type'), '=', self::BILL_TYPE);
2020-12-24 01:28:38 +03:00
}
2022-06-01 10:15:55 +03:00
public function scopeBillRecurring(Builder $query): Builder
{
return $query->where($this->qualifyColumn('type'), '=', self::BILL_RECURRING_TYPE)
->whereHas('recurring', function (Builder $query) {
$query->whereNull('deleted_at');
});
2022-06-01 10:15:55 +03:00
}
2020-12-24 01:28:38 +03:00
/**
* @inheritDoc
*
* @param Document $src
* @param boolean $child
*/
public function onCloning($src, $child = null)
{
2022-06-01 10:15:55 +03:00
if (app()->has(\App\Console\Commands\RecurringCheck::class)) {
$type = $this->getRealTypeOfRecurringDocument($src->type);
} else {
$type = $src->type;
}
2020-12-24 01:28:38 +03:00
$this->status = 'draft';
2023-04-29 01:43:48 +03:00
$this->document_number = app(DocumentNumber::class)->getNextNumber($type, $src->contact);
2020-12-24 01:28:38 +03:00
}
public function getSentAtAttribute(string $value = null)
{
2022-06-14 01:26:52 +03:00
$sent = $this->histories()->where('document_histories.status', 'sent')->first();
2020-12-24 01:28:38 +03:00
return $sent->created_at ?? null;
}
public function getReceivedAtAttribute(string $value = null)
{
2022-06-14 01:26:52 +03:00
$received = $this->histories()->where('document_histories.status', 'received')->first();
2020-12-24 01:28:38 +03:00
return $received->created_at ?? null;
}
/**
* Get the current balance.
*
* @return string
*/
public function getAttachmentAttribute($value = null)
{
2023-07-21 12:15:06 +03:00
$has_attachment = $this->hasMedia('attachment');
if (! empty($value) && ! $has_attachment) {
2020-12-24 01:28:38 +03:00
return $value;
2023-07-21 12:15:06 +03:00
} elseif (! $has_attachment) {
2020-12-24 01:28:38 +03:00
return false;
}
2021-01-22 12:38:17 +03:00
return $this->getMedia('attachment')->all();
}
public function delete_attachment()
{
if ($attachments = $this->attachment) {
foreach ($attachments as $file) {
MediaModel::where('id', $file->id)->delete();
}
2021-01-22 12:38:17 +03:00
}
2020-12-24 01:28:38 +03:00
}
/**
* Get the discount percentage.
*
* @return string
*/
public function getDiscountAttribute()
{
$percent = 0;
$discount = $this->totals->where('code', 'discount')->makeHidden('title')->pluck('amount')->first();
if ($discount) {
$sub_total = $this->totals->where('code', 'sub_total')->makeHidden('title')->pluck('amount')->first();
$percent = number_format((($discount * 100) / $sub_total), 0);
}
return $percent;
}
/**
* Get the paid amount.
*
* @return string
*/
public function getPaidAttribute()
{
if (empty($this->amount)) {
return false;
}
$paid = 0;
$code = $this->currency_code;
$rate = $this->currency_rate;
2023-07-10 12:53:43 +03:00
$precision = config('money.currencies.' . $code . '.precision');
2020-12-24 01:28:38 +03:00
if ($this->transactions->count()) {
2021-06-07 14:51:18 +03:00
foreach ($this->transactions as $transaction) {
$amount = $transaction->amount;
2020-12-24 01:28:38 +03:00
2021-06-07 14:51:18 +03:00
if ($code != $transaction->currency_code) {
$amount = $this->convertBetween($amount, $transaction->currency_code, $transaction->currency_rate, $code, $rate);
2020-12-24 01:28:38 +03:00
}
$paid += $amount;
}
}
return round($paid, $precision);
}
/**
* Get the reconcilation status.
*
* @return integer
*/
public function getReconciledAttribute()
{
if (empty($this->amount)) {
return 0;
}
$reconciled = $reconciled_amount = 0;
$code = $this->currency_code;
$rate = $this->currency_rate;
2023-07-10 12:53:43 +03:00
$precision = config('money.currencies.' . $code . '.precision');
if ($this->transactions->count()) {
foreach ($this->transactions as $transaction) {
$amount = $transaction->amount;
if ($code != $transaction->currency_code) {
$amount = $this->convertBetween($amount, $transaction->currency_code, $transaction->currency_rate, $code, $rate);
}
2020-12-24 01:28:38 +03:00
2021-06-07 14:51:18 +03:00
if ($transaction->reconciled) {
2020-12-24 01:28:38 +03:00
$reconciled_amount = +$amount;
}
}
}
if (bccomp(round($this->amount, $precision), round($reconciled_amount, $precision), $precision) === 0) {
$reconciled = 1;
}
return $reconciled;
2020-12-24 01:28:38 +03:00
}
2021-01-22 14:24:53 +03:00
/**
* Get the not paid amount.
*
* @return string
*/
public function getAmountDueAttribute()
{
2023-07-10 12:53:43 +03:00
$precision = config('money.currencies.' . $this->currency_code . '.precision');
2021-01-22 14:24:53 +03:00
return round($this->amount - $this->paid, $precision);
}
2020-12-24 01:28:38 +03:00
/**
* Get the status label.
*
* @return string
*/
public function getStatusLabelAttribute()
{
2022-06-01 10:15:55 +03:00
return match($this->status) {
'paid' => 'status-success',
'partial' => 'status-partial',
'sent' => 'status-danger',
'received' => 'status-danger',
'viewed' => 'status-sent',
'cancelled' => 'status-canceled',
default => 'status-draft',
};
}
2020-12-24 01:28:38 +03:00
2022-06-01 10:15:55 +03:00
/**
* Get the recurring status label.
*
* @return string
*/
public function getRecurringStatusLabelAttribute()
{
return match($this->recurring->status) {
'active' => 'status-partial',
'ended' => 'status-success',
default => 'status-success',
};
2020-12-24 01:28:38 +03:00
}
/**
* Get the amount without tax.
*
* @return string
*/
public function getAmountWithoutTaxAttribute()
{
$amount = $this->amount;
$this->totals->where('code', 'tax')->each(function ($total) use(&$amount) {
$tax = Tax::name($total->name)->first();
if (!empty($tax) && ($tax->type == 'withholding')) {
return;
}
$amount -= $total->amount;
});
return $amount;
}
2021-04-29 18:12:37 +03:00
public function getTemplatePathAttribute($value = null)
{
return $value ?: 'sales.invoices.print_' . setting('invoice.template');
}
2021-11-08 02:40:40 +03:00
public function getContactLocationAttribute()
{
$location = [];
if ($this->contact_city) {
$location[] = $this->contact_city;
}
if ($this->contact_zip_code) {
$location[] = $this->contact_zip_code;
}
if ($this->contact_state) {
$location[] = $this->contact_state;
}
2023-06-01 16:42:11 +03:00
if ($this->contact_country && array_key_exists($this->contact_country, trans('countries'))) {
2021-11-08 02:40:40 +03:00
$location[] = trans('countries.' . $this->contact_country);
}
return implode(', ', $location);
}
2022-06-01 10:15:55 +03:00
/**
* Get the line actions.
*
* @return array
*/
public function getLineActionsAttribute()
{
$actions = [];
$group = config('type.document.' . $this->type . '.group');
$prefix = config('type.document.' . $this->type . '.route.prefix');
$permission_prefix = config('type.document.' . $this->type . '.permission.prefix');
$translation_prefix = config('type.document.' . $this->type . '.translation.prefix');
if (empty($prefix)) {
return $actions;
}
2022-10-17 16:40:11 +03:00
if (app('mobile-detect')->isMobile()) {
try {
$actions[] = [
'title' => trans('general.show'),
'icon' => 'visibility',
'url' => route($prefix . '.show', $this->id),
'permission' => 'read-' . $group . '-' . $permission_prefix,
'attributes' => [
'id' => 'index-more-actions-show-' . $this->id,
],
];
} catch (\Exception $e) {}
}
2022-06-01 10:15:55 +03:00
try {
if (! $this->reconciled) {
$actions[] = [
'title' => trans('general.edit'),
'icon' => 'edit',
'url' => route($prefix . '.edit', $this->id),
'permission' => 'update-' . $group . '-' . $permission_prefix,
'attributes' => [
2022-09-06 13:54:56 +03:00
'id' => 'index-line-actions-edit-' . $this->type . '-' . $this->id,
2022-06-01 10:15:55 +03:00
],
];
}
} catch (\Exception $e) {}
try {
$actions[] = [
'title' => trans('general.duplicate'),
'icon' => 'file_copy',
'url' => route($prefix . '.duplicate', $this->id),
'permission' => 'create-' . $group . '-' . $permission_prefix,
'attributes' => [
2022-09-06 13:54:56 +03:00
'id' => 'index-line-actions-duplicate-' . $this->type . '-' . $this->id,
2022-06-01 10:15:55 +03:00
],
];
} catch (\Exception $e) {}
2022-12-21 16:29:30 +03:00
if ($this->status != 'paid' && (empty($this->transactions->count()) || (! empty($this->transactions->count()) && $this->paid != $this->amount))) {
2022-09-06 00:02:56 +03:00
try {
if ($this->totals->count()) {
$actions[] = [
'type' => 'button',
'title' => trans('invoices.add_payment'),
'icon' => 'paid',
'url' => route('modals.documents.document.transactions.create', $this->id),
'permission' => 'read-' . $group . '-' . $permission_prefix,
'attributes' => [
'id' => 'index-line-actions-payment-' . $this->type . '-' . $this->id,
'@click' => 'onAddPayment("' . route('modals.documents.document.transactions.create', $this->id) . '")',
],
];
} else {
$actions[] = [
'type' => 'button',
'title' => trans('invoices.messages.totals_required', ['type' => $this->type]),
'icon' => 'paid',
'permission' => 'read-' . $group . '-' . $permission_prefix,
'attributes' => [
"disabled" => "disabled",
],
];
}
2022-09-06 00:02:56 +03:00
} catch (\Exception $e) {}
}
2022-10-25 13:34:50 +03:00
try {
$actions[] = [
'title' => trans('general.print'),
'icon' => 'print',
'url' => route($prefix . '.print', $this->id),
'permission' => 'read-' . $group . '-' . $permission_prefix,
'attributes' => [
'id' => 'index-line-actions-print-' . $this->type . '-' . $this->id,
'target' => '_blank',
],
];
} catch (\Exception $e) {}
2022-06-01 10:15:55 +03:00
try {
$actions[] = [
'title' => trans('general.download_pdf'),
'icon' => 'picture_as_pdf',
2022-06-01 10:15:55 +03:00
'url' => route($prefix . '.pdf', $this->id),
'permission' => 'read-' . $group . '-' . $permission_prefix,
'attributes' => [
2022-09-06 13:54:56 +03:00
'id' => 'index-line-actions-pdf-' . $this->type . '-' . $this->id,
2022-06-01 10:15:55 +03:00
'target' => '_blank',
],
];
} catch (\Exception $e) {}
2022-06-03 09:39:09 +03:00
if (! str_contains($this->type, 'recurring')) {
if ($this->status != 'cancelled') {
2022-06-01 10:15:55 +03:00
$actions[] = [
2022-06-03 09:39:09 +03:00
'type' => 'divider',
2022-06-01 10:15:55 +03:00
];
2022-06-03 09:39:09 +03:00
try {
2022-06-01 10:15:55 +03:00
$actions[] = [
'type' => 'button',
2022-06-03 09:39:09 +03:00
'title' => trans('general.share_link'),
'icon' => 'share',
'url' => route('modals.'. $prefix . '.share.create', $this->id),
2022-06-01 10:15:55 +03:00
'permission' => 'read-' . $group . '-' . $permission_prefix,
'attributes' => [
2022-09-06 13:54:56 +03:00
'id' => 'index-line-actions-share-link-' . $this->type . '-' . $this->id,
2022-06-03 09:39:09 +03:00
'@click' => 'onShareLink("' . route('modals.'. $prefix . '.share.create', $this->id) . '")',
2022-06-01 10:15:55 +03:00
],
];
2022-06-03 09:39:09 +03:00
} catch (\Exception $e) {}
try {
2022-09-06 00:02:56 +03:00
if (! empty($this->contact) && $this->contact->email && ($this->type == 'invoice')) {
2022-06-03 09:39:09 +03:00
$actions[] = [
'type' => 'button',
'title' => trans('invoices.send_mail'),
'icon' => 'email',
'url' => route('modals.'. $prefix . '.emails.create', $this->id),
'permission' => 'read-' . $group . '-' . $permission_prefix,
'attributes' => [
2022-09-06 13:54:56 +03:00
'id' => 'index-line-actions-send-email-' . $this->type . '-' . $this->id,
'@click' => 'onSendEmail("' . route('modals.'. $prefix . '.emails.create', $this->id) . '")',
2022-06-03 09:39:09 +03:00
],
];
}
} catch (\Exception $e) {}
}
2022-06-01 10:15:55 +03:00
$actions[] = [
'type' => 'divider',
];
if (! in_array($this->status, ['cancelled', 'draft'])) {
2022-06-03 09:39:09 +03:00
try {
$actions[] = [
'title' => trans('documents.actions.cancel'),
2022-06-03 09:39:09 +03:00
'icon' => 'cancel',
'url' => route($prefix . '.cancelled', $this->id),
'permission' => 'update-' . $group . '-' . $permission_prefix,
'attributes' => [
2022-09-06 13:54:56 +03:00
'id' => 'index-line-actions-cancel-' . $this->type . '-' . $this->id,
2022-06-03 09:39:09 +03:00
],
];
} catch (\Exception $e) {}
2022-06-01 10:15:55 +03:00
$actions[] = [
2022-06-03 09:39:09 +03:00
'type' => 'divider',
2022-06-01 10:15:55 +03:00
];
2022-06-03 09:39:09 +03:00
}
2022-06-01 10:15:55 +03:00
try {
$actions[] = [
'type' => 'delete',
'icon' => 'delete',
'title' => $translation_prefix,
'route' => $prefix . '.destroy',
'permission' => 'delete-' . $group . '-' . $permission_prefix,
2022-09-06 13:54:56 +03:00
'attributes' => [
'id' => 'index-line-actions-delete-' . $this->type . '-' . $this->id,
],
2022-06-01 10:15:55 +03:00
'model' => $this,
];
} catch (\Exception $e) {}
} else {
try {
$actions[] = [
'title' => trans('general.end'),
'icon' => 'block',
'url' => route($prefix. '.end', $this->id),
'permission' => 'update-' . $group . '-' . $permission_prefix,
2022-09-06 13:54:56 +03:00
'attributes' => [
'id' => 'index-line-actions-end-' . $this->type . '-' . $this->id,
],
2022-06-01 10:15:55 +03:00
];
} catch (\Exception $e) {}
}
return $actions;
}
/**
* Retrieve the model for a bound value.
*
* @param mixed $value
* @param string|null $field
* @return \Illuminate\Database\Eloquent\Model|null
*/
public function resolveRouteBinding($value, $field = null)
{
$query = $this->where('id', $value);
2022-06-05 14:11:10 +03:00
if (request()->route()->hasParameter('recurring_invoice')) {
$query->invoiceRecurring();
}
if (request()->route()->hasParameter('recurring_bill')) {
$query->billRecurring();
}
return $query->firstOrFail();
}
2020-12-24 01:28:38 +03:00
protected static function newFactory(): Factory
{
return DocumentFactory::new();
}
}