Refactoring recurring invoices

This commit is contained in:
Hillel Coren 2013-12-11 13:11:59 +02:00
parent f97fa8991f
commit d97c805d46
14 changed files with 180 additions and 120 deletions

View File

@ -24,7 +24,7 @@ class SendRecurringInvoices extends Command {
$today = new DateTime();
$invoices = RecurringInvoice::with('account', 'invoice_items')->whereRaw('start_date <= ? AND (end_date IS NULL OR end_date >= ?)', array($today, $today))->get();
$invoices = Invoice::with('account', 'invoice_items')->whereRaw('frequency_id > 0 AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', array($today, $today))->get();
$this->info(count($invoices) . ' recurring invoice(s) found');
foreach ($invoices as $recurInvoice)

View File

@ -100,7 +100,7 @@ class ClientController extends \BaseController {
$data = array(
'client' => $client,
'title' => '- ' . $client->name,
'hasRecurringInvoices' => RecurringInvoice::whereClientId($client->id)->count() > 0
'hasRecurringInvoices' => Invoice::scope()->where('frequency_id', '>', '0')->count() > 0
);
return View::make('clients.show', $data);

View File

@ -15,11 +15,19 @@ class InvoiceController extends \BaseController {
public function index()
{
return View::make('list', array(
'entityType'=>ENTITY_INVOICE,
$data = [
'title' => '- Invoices',
'entityType'=>ENTITY_INVOICE,
'columns'=>['checkbox', 'Invoice Number', 'Client', 'Total', 'Amount Due', 'Invoice Date', 'Due Date', 'Status', 'Action']
));
];
if (Invoice::scope()->where('frequency_id', '>', '0')->count() > 0)
{
$data['secEntityType'] = ENTITY_RECURRING_INVOICE;
$data['secColumns'] = ['checkbox', 'Client', 'Total', 'Frequency', 'Start Date', 'End Date', 'Action'];
}
return View::make('list', $data);
}
public function getDatatable($clientPublicId = null)
@ -29,6 +37,7 @@ class InvoiceController extends \BaseController {
->join('invoice_statuses', 'invoice_statuses.id', '=', 'invoices.invoice_status_id')
->where('invoices.account_id', '=', Auth::user()->account_id)
->where('invoices.deleted_at', '=', null)
->where('invoices.frequency_id', '=', 0)
->select('clients.public_id as client_public_id', 'invoice_number', 'clients.name as client_name', 'invoices.public_id', 'total', 'invoices.balance', 'invoice_date', 'due_date', 'invoice_statuses.name as invoice_status_name');
if ($clientPublicId) {
@ -70,6 +79,52 @@ class InvoiceController extends \BaseController {
->make();
}
public function getRecurringDatatable($clientPublicId = null)
{
$query = DB::table('invoices')
->join('clients', 'clients.id', '=','invoices.client_id')
->join('frequencies', 'frequencies.id', '=', 'invoices.frequency_id')
->where('invoices.account_id', '=', Auth::user()->account_id)
->where('invoices.deleted_at', '=', null)
->where('invoices.frequency_id', '>', 0)
->select('clients.public_id as client_public_id', 'clients.name as client_name', 'invoices.public_id', 'total', 'frequencies.name as frequency', 'start_date', 'end_date');
if ($clientPublicId) {
$query->where('clients.public_id', '=', $clientPublicId);
}
$table = Datatable::query($query);
if (!$clientPublicId) {
$table->addColumn('checkbox', function($model) { return '<input type="checkbox" name="ids[]" value="' . $model->public_id . '">'; });
}
if (!$clientPublicId) {
$table->addColumn('client', function($model) { return link_to('clients/' . $model->client_public_id, $model->client_name); });
}
return $table->addColumn('total', function($model) { return '$' . money_format('%i', $model->total); })
->addColumn('frequency', function($model) { return $model->frequency; })
->addColumn('start_date', function($model) { return Utils::fromSqlDate($model->start_date); })
->addColumn('end_date', function($model) { return Utils::fromSqlDate($model->end_date); })
->addColumn('dropdown', function($model)
{
return '<div class="btn-group tr-action" style="visibility:hidden;">
<button type="button" class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown">
Select <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="' . URL::to('invoices/'.$model->public_id.'/edit') . '">Edit Invoice</a></li>
<li class="divider"></li>
<li><a href="' . URL::to('invoices/'.$model->public_id.'/archive') . '">Archive Invoice</a></li>
<li><a href="javascript:deleteEntity(' . $model->public_id . ')">Delete Invoice</a></li>
</ul>
</div>';
})
->orderColumns('client','total','frequency','start_date','end_date')
->make();
}
public function view($invitationKey)
{
@ -279,10 +334,9 @@ class InvoiceController extends \BaseController {
return View::make('invoices.edit', $data);
}
public static function getViewModel($isRecurring = false)
public static function getViewModel()
{
return [
'isRecurring' => $isRecurring,
'account' => Auth::user()->account,
'products' => Product::scope()->get(array('product_key','notes','cost','qty')),
'countries' => Country::orderBy('name')->get(),
@ -320,8 +374,6 @@ class InvoiceController extends \BaseController {
$rules = array(
'client' => 'required',
'invoice_number' => 'required',
'invoice_date' => 'required'
);
$validator = Validator::make(Input::all(), $rules);
@ -374,7 +426,11 @@ class InvoiceController extends \BaseController {
$invoice->discount = 0;
$invoice->invoice_number = trim(Input::get('invoice_number'));
$invoice->invoice_date = Utils::toSqlDate(Input::get('invoice_date'));
$invoice->due_date = Utils::toSqlDate(Input::get('due_date', null));
$invoice->due_date = Utils::toSqlDate(Input::get('due_date'));
$invoice->frequency_id = Input::get('recurring') ? Input::get('frequency') : 0;
$invoice->start_date = Utils::toSqlDate(Input::get('start_date'));
$invoice->end_date = Utils::toSqlDate(Input::get('end_date'));
$invoice->notes = Input::get('notes');
$client->invoices()->save($invoice);

View File

@ -17,12 +17,10 @@ class ConfideSetupUsersTable extends Migration {
Schema::dropIfExists('account_gateways');
Schema::dropIfExists('gateways');
Schema::dropIfExists('payments');
Schema::dropIfExists('recurring_invoice_items');
Schema::dropIfExists('invoice_items');
Schema::dropIfExists('products');
Schema::dropIfExists('contacts');
Schema::dropIfExists('invoices');
Schema::dropIfExists('recurring_invoices');
Schema::dropIfExists('password_reminders');
Schema::dropIfExists('clients');
Schema::dropIfExists('users');
@ -209,33 +207,6 @@ class ConfideSetupUsersTable extends Migration {
$t->string('name');
});
Schema::create('recurring_invoices', function($t)
{
$t->increments('id');
$t->unsignedInteger('client_id');
$t->unsignedInteger('user_id');
$t->unsignedInteger('account_id');
$t->timestamps();
$t->softDeletes();
$t->float('discount');
$t->text('notes');
$t->decimal('total', 10, 2);
$t->unsignedInteger('frequency_id');
$t->date('start_date')->nullable();
$t->date('end_date')->nullable();
$t->date('last_sent_date')->nullable();
$t->foreign('client_id')->references('id')->on('clients')->onDelete('cascade');
$t->foreign('account_id')->references('id')->on('accounts');
$t->foreign('user_id')->references('id')->on('users');
$t->foreign('frequency_id')->references('id')->on('frequencies');
$t->unsignedInteger('public_id');
$t->unique( array('account_id','public_id') );
});
Schema::create('invoices', function($t)
{
$t->increments('id');
@ -252,16 +223,21 @@ class ConfideSetupUsersTable extends Migration {
$t->date('due_date')->nullable();
$t->text('notes');
$t->unsignedInteger('frequency_id');
$t->date('start_date')->nullable();
$t->date('end_date')->nullable();
$t->date('last_sent_date')->nullable();
$t->unsignedInteger('recurring_invoice_id')->nullable();
$t->decimal('total', 10, 2);
$t->decimal('balance', 10, 2);
$t->unsignedInteger('recurring_invoice_id')->nullable();
$t->foreign('client_id')->references('id')->on('clients')->onDelete('cascade');
$t->foreign('account_id')->references('id')->on('accounts');
$t->foreign('user_id')->references('id')->on('users');
$t->foreign('invoice_status_id')->references('id')->on('invoice_statuses');
$t->foreign('recurring_invoice_id')->references('id')->on('recurring_invoices');
$t->foreign('recurring_invoice_id')->references('id')->on('invoices');
$t->unsignedInteger('public_id');
$t->unique( array('account_id','public_id') );
@ -333,29 +309,6 @@ class ConfideSetupUsersTable extends Migration {
$t->unique( array('account_id','public_id') );
});
Schema::create('recurring_invoice_items', function($t)
{
$t->increments('id');
$t->unsignedInteger('account_id');
$t->unsignedInteger('user_id');
$t->unsignedInteger('recurring_invoice_id');
$t->unsignedInteger('product_id')->nullable();
$t->timestamps();
$t->softDeletes();
$t->string('product_key');
$t->string('notes');
$t->decimal('cost', 10, 2);
$t->decimal('qty', 10, 2);
$t->foreign('recurring_invoice_id')->references('id')->on('recurring_invoices')->onDelete('cascade');
$t->foreign('product_id')->references('id')->on('products');
$t->foreign('user_id')->references('id')->on('users');
$t->unsignedInteger('public_id');
$t->unique( array('account_id','public_id') );
});
Schema::create('payments', function($t)
{
$t->increments('id');
@ -417,7 +370,6 @@ class ConfideSetupUsersTable extends Migration {
$t->unsignedInteger('contact_id');
$t->unsignedInteger('payment_id');
$t->unsignedInteger('invoice_id');
$t->unsignedInteger('recurring_invoice_id');
$t->unsignedInteger('credit_id');
$t->unsignedInteger('invitation_id');
@ -445,12 +397,10 @@ class ConfideSetupUsersTable extends Migration {
Schema::dropIfExists('account_gateways');
Schema::dropIfExists('gateways');
Schema::dropIfExists('payments');
Schema::dropIfExists('recurring_invoice_items');
Schema::dropIfExists('invoice_items');
Schema::dropIfExists('products');
Schema::dropIfExists('contacts');
Schema::dropIfExists('invoices');
Schema::dropIfExists('recurring_invoices');
Schema::dropIfExists('password_reminders');
Schema::dropIfExists('clients');
Schema::dropIfExists('users');

View File

@ -143,7 +143,7 @@ class Utils
for ($i=0; $i<count($variables); $i++)
{
$variable = $variables[$i];
$regExp = '/\[' . $variable . '[+-]?[\d]*\]/';
$regExp = '/:' . $variable . '[+-]?[\d]*/';
preg_match_all($regExp, $str, $matches);
$matches = $matches[0];
if (count($matches) == 0) {

View File

@ -26,6 +26,12 @@ class ContactMailer extends Mailer {
$invitation->invitation_key = str_random(20);
$invitation->save();
if (!$invoice->isSent())
{
$invoice->invoice_status_id = INVOICE_STATUS_SENT;
$invoice->save();
}
return $this->sendTo($contact->email, $subject, $view, $data);
}
}

View File

@ -15,9 +15,6 @@ define("ACTIVITY_TYPE_DELETE_PAYMENT", 11);
define("ACTIVITY_TYPE_CREATE_CREDIT", 12);
define("ACTIVITY_TYPE_ARCHIVE_CREDIT", 13);
define("ACTIVITY_TYPE_DELETE_CREDIT", 14);
define("ACTIVITY_TYPE_CREATE_RECURRING_INVOICE", 15);
define("ACTIVITY_TYPE_ARCHIVE_RECURRING_INVOICE", 16);
define("ACTIVITY_TYPE_DELETE_RECURRING_INVOICE", 17);
class Activity extends Eloquent
@ -68,11 +65,18 @@ class Activity extends Eloquent
public static function createInvoice($invoice)
{
$userName = Auth::check() ? Auth::user()->getFullName() : '<i>System</i>';
if ($invoice->isRecurring()) {
$message = $userName . ' created ' . link_to('invoices/'.$invoice->public_id, 'recuring invoice');
} else {
$message = $userName . ' created invoice ' . link_to('invoices/'.$invoice->public_id, $invoice->invoice_number);
}
$activity = Activity::getBlank($invoice);
$activity->invoice_id = $invoice->id;
$activity->client_id = $invoice->client_id;
$activity->activity_type_id = ACTIVITY_TYPE_CREATE_INVOICE;
$activity->message = $userName . ' created invoice ' . link_to('invoices/'.$invoice->public_id, $invoice->invoice_number);
$activity->message = $message;
$activity->save();
}
@ -98,26 +102,6 @@ class Activity extends Eloquent
$activity->save();
}
public static function createRecurringInvoice($invoice)
{
$activity = Activity::getBlank();
$activity->recurring_invoice_id = $invoice->id;
$activity->client_id = $invoice->client_id;
$activity->activity_type_id = ACTIVITY_TYPE_CREATE_RECURRING_INVOICE;
$activity->message = Auth::user()->getFullName() . ' created recurring invoice ' . link_to('recurring_invoices/'.$invoice->public_id, $invoice->getName());
$activity->save();
}
public static function archiveRecurringInvoice($invoice)
{
$activity = Activity::getBlank();
$activity->recurring_invoice_id = $invoice->id;
$activity->client_id = $invoice->client_id;
$activity->activity_type_id = ACTIVITY_TYPE_ARCHIVE_INVOICE;
$activity->message = Auth::user()->getFullName() . ' archived recurring invoice ' . $invoice->getName();
$activity->save();
}
public static function createPayment($payment)
{
if (Auth::check())

View File

@ -20,7 +20,7 @@ class EntityModel extends Eloquent
exit; // TODO_FIX
}
$lastEntity = $className::scope(false, $entity->account_id)->orderBy('public_id', 'DESC')->first();
$lastEntity = $className::withTrashed()->scope(false, $entity->account_id)->orderBy('public_id', 'DESC')->first();
if ($lastEntity)
{

View File

@ -33,6 +33,63 @@ class Invoice extends EntityModel
{
return ENTITY_INVOICE;
}
public function isRecurring()
{
return $this->frequency_id > 0;
}
public function isSent()
{
return $this->invoice_status_id >= INVOICE_STATUS_SENT;
}
public function shouldSendToday()
{
$dayOfWeekToday = date('w');
$dayOfWeekStart = date('w', strtotime($this->start_date));
$dayOfMonthToday = date('j');
$dayOfMonthStart = date('j', strtotime($this->start_date));
if (!$this->last_sent_date) {
$daysSinceLastSent = 0;
$monthsSinceLastSent = 0;
} else {
$date1 = new DateTime($this->last_sent_date);
$date2 = new DateTime();
$diff = $date2->diff($date1);
$daysSinceLastSent = $diff->format("%a");
$monthsSinceLastSent = ($diff->format('%y') * 12) + $diff->format('%m');
if ($daysSinceLastSent == 0) {
return false;
}
}
switch ($this->frequency_id)
{
case FREQUENCY_WEEKLY:
return $dayOfWeekStart == $dayOfWeekToday;
case FREQUENCY_TWO_WEEKS:
return $dayOfWeekStart == $dayOfWeekToday && (!$daysSinceLastSent || $daysSinceLastSent == 14);
case FREQUENCY_FOUR_WEEKS:
return $dayOfWeekStart == $dayOfWeekToday && (!$daysSinceLastSent || $daysSinceLastSent == 28);
case FREQUENCY_MONTHLY:
return $dayOfMonthStart == $dayOfMonthToday || $daysSinceLastSent > 31;
case FREQUENCY_THREE_MONTHS:
return ($dayOfMonthStart == $dayOfMonthToday && (!$daysSinceLastSent || $monthsSinceLastSent == 3)) || $daysSinceLastSent > (3 * 31);
case FREQUENCY_SIX_MONTHS:
return ($dayOfMonthStart == $dayOfMonthToday && (!$daysSinceLastSent || $monthsSinceLastSent == 6)) || $daysSinceLastSent > (6 * 31);
case FREQUENCY_ANNUALLY:
return ($dayOfMonthStart == $dayOfMonthToday && (!$daysSinceLastSent || $monthsSinceLastSent == 12)) || $daysSinceLastSent > (12 *31);
default:
echo "Error: invalid frequency_id - ".$this->frequency_id; exit; //TODO_FIX
break;
}
return false;
}
}
Invoice::created(function($invoice)

View File

@ -60,14 +60,14 @@ Route::group(array('before' => 'auth'), function()
Route::get('api/activities/{client_id?}', array('as'=>'api.activities', 'uses'=>'ActivityController@getDatatable'));
Route::post('clients/bulk', 'ClientController@bulk');
Route::get('recurring_invoices', 'InvoiceController@recurringIndex');
Route::get('api/recurring_invoices/{client_id?}', array('as'=>'api.recurring_invoices', 'uses'=>'InvoiceController@getRecurringDatatable'));
Route::resource('invoices', 'InvoiceController');
Route::get('api/invoices/{client_id?}', array('as'=>'api.invoices', 'uses'=>'InvoiceController@getDatatable'));
Route::get('invoices/create/{client_id?}', 'InvoiceController@create');
Route::post('invoices/bulk', 'InvoiceController@bulk');
Route::resource('recurring_invoices', 'RecurringInvoiceController');
Route::get('api/recurring_invoices/{client_id?}', array('as'=>'api.recurring_invoices', 'uses'=>'RecurringInvoiceController@getDatatable'));
Route::get('payments/{id}/edit', function() { return View::make('header'); });
Route::resource('payments', 'PaymentController');
Route::get('payments/create/{client_id?}', 'PaymentController@create');
@ -104,14 +104,16 @@ HTML::macro('menu_link', function($type) {
$str= '<li class="dropdown '.$class.'">
<a href="'.URL::to($types).'" class="dropdown-toggle">'.$Types.'</a>
<ul class="dropdown-menu" id="menu1">
<li><a href="'.URL::to($types).'">View '.$Types.'</a></li>';
<li><a href="'.URL::to($types.'/create').'">New '.$Type.'</a></li>';
//<li><a href="'.URL::to($types).'">View '.$Types.'</a></li>';
/*
if ($Type == 'Invoice') {
$str .= '<li><a href="'.URL::to('recurring_invoices').'">View Recurring Invoices</a></li>';
$str .= '<li><a href="'.URL::to('recurring_invoices').'">Recurring Invoices</a></li>';
}
*/
return $str . '<li><a href="'.URL::to($types.'/create').'">New '.$Type.'</a></li>
</ul>
return $str . '</ul>
</li>';
});

View File

@ -9,7 +9,7 @@
@foreach($columns as $i => $c)
<th align="center" valign="middle" class="head{{ $i }}">
@if ($c == 'checkbox' && $hasCheckboxes = true)
<input type="checkbox" id="selectAll"/>
<input type="checkbox" class="selectAll"/>
@else
{{ $c }}
@endif

View File

@ -28,6 +28,7 @@
@else
{{ Former::populateField('invoice_number', $invoiceNumber) }}
{{ Former::populateField('invoice_date', date('m/d/Y')) }}
{{ Former::populateField('start_date', date('m/d/Y')) }}
{{ Former::populateField('frequency', FREQUENCY_MONTHLY) }}
@endif
@ -302,13 +303,12 @@
$('label.radio').addClass('radio-inline');
@if ($isRecurring)
@if ($invoice && $invoice->isRecurring())
$('#recurring').prop('checked', true);
@if ($invoice)
$('#recurring_checkbox').hide();
@endif
@elseif ($invoice && $invoice->isSent())
$('#recurring_checkbox').hide();
@elseif (isset($invoice->recurring_invoice_id) && $invoice->recurring_invoice_id)
$('#recurring_checkbox > div > div').html('Created by a {{ link_to('/recurring_invoices/'.$invoice->recurring_invoice_id, 'recurring invoice') }}').css('padding-top','6px');
$('#recurring_checkbox > div > div').html('Created by a {{ link_to('/invoices/'.$invoice->recurring_invoice_id, 'recurring invoice') }}').css('padding-top','6px');
@endif
toggleRecurring();
@ -659,14 +659,10 @@
$('#recurring_on').show();
$('#recurring_off').hide();
$('#email_button').prop('disabled', true);
@if (!$isRecurring)
$('.main_form').prop('action', '{{ URL::to('/recurring_invoices') }}');
@endif
} else {
$('#recurring_on').hide();
$('#recurring_off').show();
$('#email_button').prop('disabled', false);
$('.main_form').prop('action', '{{ $url }}');
}
/*

View File

@ -20,6 +20,15 @@
{{ Button::primary_link(URL::to($entityType . 's/create'), 'New ' . Utils::getEntityName($entityType), array('class' => 'pull-right')) }}
@if (isset($secEntityType))
{{ Datatable::table()
->addColumn($secColumns)
->setUrl(route('api.' . $secEntityType . 's'))
->setOptions('sPaginationType', 'bootstrap')
->setOptions('bFilter', false)
->render('datatable') }}
@endif
{{ Datatable::table()
->addColumn($columns)
->setUrl(route('api.' . $entityType . 's'))
@ -88,8 +97,8 @@
submitForm('archive');
});
$('#selectAll').click(function() {
$(':checkbox').prop('checked', this.checked);
$('.selectAll').click(function() {
$(this).closest('table').find(':checkbox').prop('checked', this.checked);
});

View File

@ -211,7 +211,7 @@ function processVariables(str) {
var variables = ['MONTH','QUARTER','YEAR'];
for (var i=0; i<variables.length; i++) {
var variable = variables[i];
var regexp = new RegExp('\\[' + variable + '[+-]?[\\d]*\\]', 'g');
var regexp = new RegExp(':' + variable + '[+-]?[\\d]*', 'g');
var matches = str.match(regexp);
if (!matches) {
continue;