Add ledger and journal tables #130
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateJournalTransactionsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('journal_transactions', function (Blueprint $table) {
|
||||
$table->char('id', 36)->unique();
|
||||
$table->char('transaction_group', 36)->nullable();
|
||||
$table->integer('journal_id');
|
||||
$table->unsignedBigInteger('debit')->nullable();
|
||||
$table->unsignedBigInteger('credit')->nullable();
|
||||
$table->char('currency', 5);
|
||||
$table->text('memo')->nullable();
|
||||
$table->text('tags')->nullable();
|
||||
$table->char('ref_class', 32)->nullable();
|
||||
$table->integer('ref_class_id')->nullable();
|
||||
$table->timestamps();
|
||||
$table->dateTime('post_date');
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index('transaction_group');
|
||||
$table->index('journal_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('journal_transactions');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateJournalsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('journals', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('ledger_id')->nullable();
|
||||
$table->bigInteger('balance');
|
||||
$table->char('currency', 5);
|
||||
$table->char('morphed_type', 32);
|
||||
$table->integer('morphed_id');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('journals');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateLedgersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('ledgers', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('name');
|
||||
$table->enum('type', ['asset', 'liability', 'equity', 'income', 'expense']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('ledgers');
|
||||
}
|
||||
}
|
||||
388
app/Models/Journal.php
Normal file
388
app/Models/Journal.php
Normal file
@@ -0,0 +1,388 @@
|
||||
<?php
|
||||
/**
|
||||
* Based on https://github.com/scottlaurent/accounting
|
||||
* With modifications for phpVMS
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Support\Money;
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* Class Journal
|
||||
* @package Scottlaurent\Accounting
|
||||
* @property Money $balance
|
||||
* @property string $currency
|
||||
* @property Carbon $updated_at
|
||||
* @property Carbon $post_date
|
||||
* @property Carbon $created_at
|
||||
*/
|
||||
class Journal extends BaseModel
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'journals';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = [
|
||||
'deleted_at',
|
||||
'updated_at'
|
||||
];
|
||||
|
||||
/**
|
||||
* Get all of the morphed models.
|
||||
*/
|
||||
public function morphed()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Journal $journal
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
static::created(function (Journal $journal) {
|
||||
$journal->resetCurrentBalances();
|
||||
});
|
||||
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function ledger()
|
||||
{
|
||||
return $this->belongsTo(Ledger::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function transactions()
|
||||
{
|
||||
return $this->hasMany(JournalTransaction::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $currency
|
||||
*/
|
||||
public function setCurrency($currency)
|
||||
{
|
||||
$this->currency = $currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Ledger $ledger
|
||||
* @return Journal
|
||||
*/
|
||||
public function assignToLedger(Ledger $ledger)
|
||||
{
|
||||
$ledger->journals()->save($this);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function resetCurrentBalances()
|
||||
{
|
||||
$this->balance = $this->getBalance();
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @return Money
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getBalanceAttribute($value): Money
|
||||
{
|
||||
return new Money($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function setBalanceAttribute($value): void
|
||||
{
|
||||
$value = ($value instanceof Money)
|
||||
? $value
|
||||
: new Money($value);
|
||||
|
||||
$this->attributes['balance'] = $value ? (int)$value->getAmount() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the debit only balance of the journal based on a given date.
|
||||
* @param Carbon $date
|
||||
* @return Money
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getDebitBalanceOn(Carbon $date): Money
|
||||
{
|
||||
$balance = $this->transactions()
|
||||
->where('post_date', '<=', $date)
|
||||
->sum('debit') ?: 0;
|
||||
|
||||
return new Money($balance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Journal $object
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function transactionsReferencingObjectQuery($object)
|
||||
{
|
||||
return $this
|
||||
->transactions()
|
||||
->where('ref_class', \get_class($object))
|
||||
->where('ref_class_id', $object->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the credit only balance of the journal based on a given date.
|
||||
* @param Carbon $date
|
||||
* @return Money
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getCreditBalanceOn(Carbon $date)
|
||||
{
|
||||
$balance = $this->transactions()
|
||||
->where('post_date', '<=', $date)
|
||||
->sum('credit') ?: 0;
|
||||
|
||||
return new Money($balance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the balance of the journal based on a given date.
|
||||
* @param Carbon $date
|
||||
* @return Money
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getBalanceOn(Carbon $date)
|
||||
{
|
||||
return $this->getCreditBalanceOn($date)
|
||||
->subtract($this->getDebitBalanceOn($date));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the balance of the journal as of right now, excluding future transactions.
|
||||
* @return Money
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getCurrentBalance()
|
||||
{
|
||||
return $this->getBalanceOn(Carbon::now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the balance of the journal. This "could" include future dates.
|
||||
* @return Money
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getBalance()
|
||||
{
|
||||
$balance = $this
|
||||
->transactions()
|
||||
->sum('credit') - $this->transactions()->sum('debit');
|
||||
|
||||
return new Money($balance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the balance of the journal in dollars. This "could" include future dates.
|
||||
* @return float|int
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getCurrentBalanceInDollars()
|
||||
{
|
||||
return $this->getCurrentBalance()->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get balance
|
||||
* @return float|int
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getBalanceInDollars()
|
||||
{
|
||||
return $this->getBalance()->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @param null $memo
|
||||
* @param null $post_date
|
||||
* @return JournalTransaction
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function credit($value, $memo = null, $post_date = null, $transaction_group = null)
|
||||
{
|
||||
$value = ($value instanceof Money)
|
||||
? $value
|
||||
: new Money($value);
|
||||
|
||||
return $this->post($value, null, $memo, $post_date, $transaction_group);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @param null $memo
|
||||
* @param null $post_date
|
||||
* @return JournalTransaction
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function debit($value, $memo = null, $post_date = null, $transaction_group = null)
|
||||
{
|
||||
$value = ($value instanceof Money)
|
||||
? $value
|
||||
: new Money($value);
|
||||
|
||||
return $this->post(null, $value, $memo, $post_date, $transaction_group);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Money $credit
|
||||
* @param Money $debit
|
||||
* @param $memo
|
||||
* @param Carbon $post_date
|
||||
* @return JournalTransaction
|
||||
*/
|
||||
private function post(Money $credit = null, Money $debit = null, $memo = null, $post_date = null, $transaction_group)
|
||||
{
|
||||
$transaction = new JournalTransaction();
|
||||
$transaction->credit = $credit ? $credit->getAmount() : null;
|
||||
$transaction->debit = $debit ? $debit->getAmount() : null;
|
||||
$currency_code = $credit
|
||||
? $credit->getCurrency()->getCode()
|
||||
: $debit->getCurrency()->getCode();
|
||||
|
||||
$transaction->memo = $memo;
|
||||
$transaction->currency = $currency_code;
|
||||
$transaction->post_date = $post_date ?: Carbon::now();
|
||||
$transaction->transaction_group = $transaction_group;
|
||||
|
||||
$this->transactions()->save($transaction);
|
||||
|
||||
return $transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit a journal by a given dollar amount
|
||||
* @param $value
|
||||
* @param null $memo
|
||||
* @param null $post_date
|
||||
* @return JournalTransaction
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function creditDollars($value, $memo = null, $post_date = null)
|
||||
{
|
||||
$value = Money::convertToSubunit($value);
|
||||
return $this->credit($value, $memo, $post_date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Debit a journal by a given dollar amount
|
||||
* @param $value
|
||||
* @param null $memo
|
||||
* @param null $post_date
|
||||
* @return JournalTransaction
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function debitDollars($value, $memo = null, $post_date = null)
|
||||
{
|
||||
$value = Money::convertToSubunit($value);
|
||||
return $this->debit($value, $memo, $post_date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the dollar amount debited to a journal today
|
||||
* @return float|int
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getDollarsDebitedToday()
|
||||
{
|
||||
$today = Carbon::now();
|
||||
return $this->getDollarsDebitedOn($today);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the dollar amount credited to a journal today
|
||||
* @return Money
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getDollarsCreditedToday()
|
||||
{
|
||||
$today = Carbon::now();
|
||||
return $this->getDollarsCreditedOn($today);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the dollar amount debited to a journal on a given day
|
||||
* @param Carbon $date
|
||||
* @return Money|float|int
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getDollarsDebitedOn(Carbon $date)
|
||||
{
|
||||
$amount = $this->transactions()
|
||||
->whereBetween('post_date', [
|
||||
$date->copy()->startOfDay(),
|
||||
$date->copy()->endOfDay()
|
||||
])
|
||||
->sum('debit');
|
||||
|
||||
return new Money($amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the dollar amount credited to a journal on a given day
|
||||
* @param Carbon $date
|
||||
* @return Money
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getDollarsCreditedOn(Carbon $date)
|
||||
{
|
||||
$amount = $this
|
||||
->transactions()
|
||||
->whereBetween('post_date', [
|
||||
$date->copy()->startOfDay(),
|
||||
$date->copy()->endOfDay()
|
||||
])
|
||||
->sum('credit');
|
||||
|
||||
return new Money($amount);
|
||||
}
|
||||
}
|
||||
103
app/Models/JournalTransaction.php
Normal file
103
app/Models/JournalTransaction.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
/**
|
||||
* Based on https://github.com/scottlaurent/accounting
|
||||
* With modifications for phpVMS
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* @property string ref_class
|
||||
* @property string ref_class_id
|
||||
* @property string currency
|
||||
* @property string memo
|
||||
* @property string transaction_group
|
||||
* @property static post_date
|
||||
* @property integer credit
|
||||
* @property integer debit
|
||||
*/
|
||||
class JournalTransaction extends BaseModel
|
||||
{
|
||||
protected $table = 'journal_transactions';
|
||||
public $incrementing = false;
|
||||
|
||||
public $fillable = [
|
||||
'transaction_group',
|
||||
'journal_id',
|
||||
'debit',
|
||||
'credit',
|
||||
'currency',
|
||||
'memo',
|
||||
'tags',
|
||||
'ref_class',
|
||||
'ref_class_id',
|
||||
'post_date'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'debit' => 'integer',
|
||||
'credits' => 'integer',
|
||||
'post_date' => 'datetime',
|
||||
'tags' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
static::creating(function ($transaction) {
|
||||
$transaction->id = \Ramsey\Uuid\Uuid::uuid4()->toString();
|
||||
});
|
||||
|
||||
static::saved(function ($transaction) {
|
||||
$transaction->journal->resetCurrentBalances();
|
||||
});
|
||||
|
||||
static::deleted(function ($transaction) {
|
||||
$transaction->journal->resetCurrentBalances();
|
||||
});
|
||||
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function journal()
|
||||
{
|
||||
return $this->belongsTo(Journal::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Model $object
|
||||
* @return JournalTransaction
|
||||
*/
|
||||
public function referencesObject($object)
|
||||
{
|
||||
$this->ref_class = \get_class($object);
|
||||
$this->ref_class_id = $object->id;
|
||||
$this->save();
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function getReferencedObject()
|
||||
{
|
||||
if ($classname = $this->ref_class) {
|
||||
$_class = new $this->ref_class;
|
||||
return $_class->find($this->ref_class_id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $currency
|
||||
*/
|
||||
public function setCurrency($currency)
|
||||
{
|
||||
$this->currency = $currency;
|
||||
}
|
||||
}
|
||||
66
app/Models/Ledger.php
Normal file
66
app/Models/Ledger.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/**
|
||||
* Based on https://github.com/scottlaurent/accounting
|
||||
* With modifications for phpVMS
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Support\Money;
|
||||
use Carbon\Carbon;
|
||||
|
||||
/**
|
||||
* Class Journal
|
||||
* @package Scottlaurent\Accounting
|
||||
* @property Money $balance
|
||||
* @property string $currency
|
||||
* @property Carbon $updated_at
|
||||
* @property Carbon $post_date
|
||||
* @property Carbon $created_at
|
||||
*/
|
||||
class Ledger extends BaseModel
|
||||
{
|
||||
protected $table = 'ledgers';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function journals()
|
||||
{
|
||||
return $this->hasMany(Journal::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the posts for the country.
|
||||
*/
|
||||
public function journal_transctions()
|
||||
{
|
||||
return $this->hasManyThrough(JournalTransaction::class, Journal::class);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getCurrentBalance(): Money
|
||||
{
|
||||
if ($this->type === 'asset' || $this->type === 'expense') {
|
||||
$balance = $this->journal_transctions->sum('debit') - $this->journal_transctions->sum('credit');
|
||||
} else {
|
||||
$balance = $this->journal_transctions->sum('credit') - $this->journal_transctions->sum('debit');
|
||||
}
|
||||
|
||||
return new Money($balance);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getCurrentBalanceInDollars()
|
||||
{
|
||||
return $this->getCurrentBalance()->getValue();
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ class Money
|
||||
public static function convertToSubunit($amount)
|
||||
{
|
||||
$currency = config('phpvms.currency');
|
||||
return $amount * config('money.'.$currency.'.subunit');
|
||||
return (int) $amount * config('money.'.$currency.'.subunit');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,9 +64,20 @@ class Money
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the amount of currency in smallest denomination
|
||||
* @return string
|
||||
*/
|
||||
public function getAmount()
|
||||
{
|
||||
return $this->money->getAmount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value in whole amounts, e.g: 100.00
|
||||
* vs returning in all cents
|
||||
* @return float
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->money->getValue();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user