Merge Invoice and Bill into Document

This commit is contained in:
Burak Çakırel
2020-12-24 01:28:38 +03:00
parent 830cc05957
commit 0c1424db47
436 changed files with 31655 additions and 37350 deletions

View File

@ -0,0 +1,353 @@
<?php
namespace App\Models\Document;
use App\Abstracts\Model;
use App\Scopes\Document as Scope;
use App\Models\Setting\Tax;
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;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphOne;
class Document extends Model
{
use HasFactory, Documents, Cloneable, Currencies, DateTime, Media, Recurring;
public const INVOICE_TYPE = 'invoice';
public const BILL_TYPE = 'bill';
protected $table = 'documents';
protected $appends = ['attachment', 'amount_without_tax', 'discount', 'paid', 'status_label'];
protected $dates = ['deleted_at', 'issued_at', 'due_at'];
protected $fillable = [
'company_id',
'type',
'document_number',
'order_number',
'status',
'issued_at',
'due_at',
'amount',
'currency_code',
'currency_rate',
'contact_id',
'contact_name',
'contact_email',
'contact_tax_number',
'contact_phone',
'contact_address',
'notes',
'category_id',
'parent_id',
'footer',
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'amount' => 'double',
'currency_rate' => 'double',
];
/**
* @var array
*/
public $sortable = ['document_number', 'contact_name', 'amount', 'status', 'issued_at', 'due_at'];
/**
* @var array
*/
public $cloneable_relations = ['items', 'recurring', 'totals'];
/**
* The "booting" method of the model.
*
* @return void
*/
protected static function boot()
{
parent::boot();
static::addGlobalScope(new Scope);
}
public function category()
{
return $this->belongsTo('App\Models\Setting\Category')->withDefault(['name' => trans('general.na')]);
}
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');
}
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()
{
return $this->hasMany('App\Models\Banking\Transaction', 'document_id')->where('type', 'income');
}
public function totals_sorted()
{
return $this->totals()->orderBy('sort_order');
}
public function scopeLatest(Builder $query)
{
return $query->orderBy('issued_at', 'desc');
}
public function scopeNumber(Builder $query, string $number)
{
return $query->where('document_number', '=', $number);
}
public function scopeDue($query, $date)
{
return $query->whereDate('due_at', '=', $date);
}
public function scopeAccrued($query)
{
return $query->whereNotIn('status', ['draft', 'cancelled']);
}
public function scopePaid($query)
{
return $query->where('status', '=', 'paid');
}
public function scopeNotPaid($query)
{
return $query->where('status', '<>', 'paid');
}
public function scopeType(Builder $query, string $type)
{
return $query->where($this->table . '.type', '=', $type);
}
public function scopeInvoice(Builder $query)
{
return $query->where($this->table . '.type', '=', self::INVOICE_TYPE);
}
public function scopeBill(Builder $query)
{
return $query->where($this->table . '.type', '=', self::BILL_TYPE);
}
/**
* @inheritDoc
*
* @param Document $src
* @param boolean $child
*/
public function onCloning($src, $child = null)
{
$this->status = 'draft';
$this->document_number = $this->getNextDocumentNumber($src->type);
}
public function getSentAtAttribute(string $value = null)
{
$sent = $this->histories()->where('status', 'sent')->first();
return $sent->created_at ?? null;
}
public function getReceivedAtAttribute(string $value = null)
{
$received = $this->histories()->where('status', 'received')->first();
return $received->created_at ?? null;
}
/**
* Get the current balance.
*
* @return string
*/
public function getAttachmentAttribute($value = null)
{
if (!empty($value) && !$this->hasMedia('attachment')) {
return $value;
} elseif (!$this->hasMedia('attachment')) {
return false;
}
return $this->getMedia('attachment')->last();
}
/**
* 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;
$reconciled = $reconciled_amount = 0;
$code = $this->currency_code;
$rate = config('money.' . $code . '.rate');
$precision = config('money.' . $code . '.precision');
if ($this->transactions->count()) {
foreach ($this->transactions as $item) {
$amount = $item->amount;
if ($code != $item->currency_code) {
$amount = $this->convertBetween($amount, $item->currency_code, $item->currency_rate, $code, $rate);
}
$paid += $amount;
if ($item->reconciled) {
$reconciled_amount = +$amount;
}
}
}
if (bccomp(round($this->amount, $precision), round($reconciled_amount, $precision), $precision) === 0) {
$reconciled = 1;
}
$this->setAttribute('reconciled', $reconciled);
return round($paid, $precision);
}
/**
* Get the status label.
*
* @return string
*/
public function getStatusLabelAttribute()
{
switch ($this->status) {
case 'paid':
$label = 'success';
break;
case 'partial':
$label = 'info';
break;
case 'sent':
case 'received':
$label = 'danger';
break;
case 'viewed':
$label = 'warning';
break;
case 'cancelled':
$label = 'dark';
break;
default:
$label = 'primary';
break;
}
return $label;
}
/**
* 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;
}
protected static function newFactory(): Factory
{
return DocumentFactory::new();
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Models\Document;
use App\Abstracts\Model;
use App\Traits\Currencies;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class DocumentHistory extends Model
{
use Currencies;
protected $table = 'document_histories';
protected $fillable = ['company_id', 'type', 'document_id', 'status', 'notify', 'description'];
public function document()
{
return $this->belongsTo('App\Models\Document\Document');
}
public function scopeType(Builder $query, string $type)
{
return $query->where($this->table . '.type', '=', $type);
}
public function scopeInvoice(Builder $query)
{
return $query->where($this->table . '.type', '=', Document::INVOICE_TYPE);
}
public function scopeBill(Builder $query)
{
return $query->where($this->table . '.type', '=', Document::BILL_TYPE);
}
}

View File

@ -0,0 +1,140 @@
<?php
namespace App\Models\Document;
use App\Abstracts\Model;
use App\Traits\Currencies;
use Bkwld\Cloner\Cloneable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class DocumentItem extends Model
{
use Cloneable, Currencies;
protected $table = 'document_items';
protected $appends = ['discount'];
protected $fillable = [
'company_id',
'type',
'document_id',
'item_id',
'name',
'description',
'quantity',
'price',
'total',
'tax',
'discount_rate',
'discount_type',
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'price' => 'double',
'total' => 'double',
'tax' => 'double',
];
/**
* @var array
*/
public $cloneable_relations = ['taxes'];
public static function boot()
{
parent::boot();
static::retrieved(
function ($model) {
$model->setTaxIds();
}
);
}
public function document()
{
return $this->belongsTo('App\Models\Document\Document');
}
public function item()
{
return $this->belongsTo('App\Models\Common\Item')->withDefault(['name' => trans('general.na')]);
}
public function taxes()
{
return $this->hasMany('App\Models\Document\DocumentItemTax', 'document_item_id', 'id');
}
public function scopeType(Builder $query, string $type)
{
return $query->where($this->table . '.type', '=', $type);
}
public function scopeInvoice(Builder $query)
{
return $query->where($this->table . '.type', '=', Document::INVOICE_TYPE);
}
public function scopeBill(Builder $query)
{
return $query->where($this->table . '.type', '=', Document::BILL_TYPE);
}
public function getDiscountAttribute(): string
{
if (setting('localisation.percent_position', 'after') === 'after') {
$text = ($this->discount_type === 'normal') ? $this->discount_rate . '%' : $this->discount_rate;
} else {
$text = ($this->discount_type === 'normal') ? '%' . $this->discount_rate : $this->discount_rate;
}
return $text;
}
public function getDiscountRateAttribute(int $value = 0)
{
$discount_rate = 0;
switch (setting('localisation.discount_location', 'total')) {
case 'no':
case 'total':
$discount_rate = 0;
break;
case 'both':
case 'item':
$discount_rate = $value;
break;
}
return $discount_rate;
}
/**
* Convert tax to Array.
*/
public function setTaxIds()
{
$tax_ids = [];
foreach ($this->taxes as $tax) {
$tax_ids[] = (string)$tax->tax_id;
}
$this->setAttribute('tax_ids', $tax_ids);
}
public function onCloning($src, $child = null)
{
unset($this->tax_ids);
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Models\Document;
use App\Abstracts\Model;
use App\Traits\Currencies;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Znck\Eloquent\Relations\BelongsToThrough as BelongsToThroughRelation;
use Znck\Eloquent\Traits\BelongsToThrough;
class DocumentItemTax extends Model
{
use Currencies, BelongsToThrough;
protected $table = 'document_item_taxes';
protected $fillable = ['company_id', 'type', 'document_id', 'document_item_id', 'tax_id', 'name', 'amount'];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'amount' => 'double',
];
public function document()
{
return $this->belongsTo('App\Models\Document\Document');
}
public function item()
{
return $this->belongsToThrough('App\Models\Common\Item', 'App\Models\Document\DocumentItem', 'document_item_id')->withDefault(['name' => trans('general.na')]);
}
public function tax()
{
return $this->belongsTo('App\Models\Setting\Tax')->withDefault(['name' => trans('general.na'), 'rate' => 0]);
}
public function scopeType(Builder $query, string $type)
{
return $query->where($this->table . '.type', '=', $type);
}
public function scopeInvoice(Builder $query)
{
return $query->where($this->table . '.type', '=', Document::INVOICE_TYPE);
}
public function scopeBill(Builder $query)
{
return $query->where($this->table . '.type', '=', Document::BILL_TYPE);
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace App\Models\Document;
use App\Abstracts\Model;
use App\Models\Setting\Tax;
use App\Traits\DateTime;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class DocumentTotal extends Model
{
use DateTime;
protected $table = 'document_totals';
protected $appends = ['title'];
protected $fillable = ['company_id', 'type', 'document_id', 'code', 'name', 'amount', 'sort_order'];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'amount' => 'double',
];
public function document()
{
return $this->belongsTo('App\Models\Document\Document');
}
public function getTitleAttribute()
{
$title = $this->name;
$percent = 0;
$tax = null;
switch ($this->code) {
case 'discount':
$title = trans($title);
$percent = $this->document->discount;
break;
case 'tax':
$tax = Tax::where('name', $title)->first();
if (!empty($tax->rate)) {
$percent = $tax->rate;
}
break;
}
if (!empty($percent)) {
$title .= ' (';
if (setting('localisation.percent_position', 'after') === 'after') {
$title .= ($this->code === 'discount') ? $percent . '%' : (($tax->type === 'fixed') ? $percent : $percent . '%');
} else {
$title .= ($this->code === 'discount') ? '%' . $percent : (($tax->type === 'fixed') ? $percent : '%' . $percent);
}
$title .= ')';
}
return $title;
}
public function scopeType(Builder $query, string $type)
{
return $query->where($this->table . '.type', '=', $type);
}
public function scopeInvoice(Builder $query)
{
return $query->where($this->table . '.type', '=', Document::INVOICE_TYPE);
}
public function scopeBill(Builder $query)
{
return $query->where($this->table . '.type', '=', Document::BILL_TYPE);
}
public function scopeCode($query, $code)
{
return $query->where('code', '=', $code);
}
}