From d97c805d4600b17f199690d69fb5a43c125a5454 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Wed, 11 Dec 2013 13:11:59 +0200 Subject: [PATCH] Refactoring recurring invoices --- app/commands/SendRecurringInvoices.php | 2 +- app/controllers/ClientController.php | 2 +- app/controllers/InvoiceController.php | 72 ++++++++++++++++--- ...11_05_180133_confide_setup_users_table.php | 70 +++--------------- app/libraries/utils.php | 2 +- app/mailers/ContactMailer.php | 6 ++ app/models/Activity.php | 34 +++------ app/models/EntityModel.php | 2 +- app/models/Invoice.php | 57 +++++++++++++++ app/routes.php | 20 +++--- app/views/datatable.blade.php | 2 +- app/views/invoices/edit.blade.php | 14 ++-- app/views/list.blade.php | 15 +++- public/js/script.js | 2 +- 14 files changed, 180 insertions(+), 120 deletions(-) diff --git a/app/commands/SendRecurringInvoices.php b/app/commands/SendRecurringInvoices.php index 1e582149b8bc..b5e3fb874359 100755 --- a/app/commands/SendRecurringInvoices.php +++ b/app/commands/SendRecurringInvoices.php @@ -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) diff --git a/app/controllers/ClientController.php b/app/controllers/ClientController.php index 9430ffae2468..48531451356b 100755 --- a/app/controllers/ClientController.php +++ b/app/controllers/ClientController.php @@ -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); diff --git a/app/controllers/InvoiceController.php b/app/controllers/InvoiceController.php index 0294ee4c9e11..8e55799d0af5 100755 --- a/app/controllers/InvoiceController.php +++ b/app/controllers/InvoiceController.php @@ -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 ''; }); + } + + 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 ''; + }) + ->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); diff --git a/app/database/migrations/2013_11_05_180133_confide_setup_users_table.php b/app/database/migrations/2013_11_05_180133_confide_setup_users_table.php index 6081bbdc95a2..1a49df14620d 100755 --- a/app/database/migrations/2013_11_05_180133_confide_setup_users_table.php +++ b/app/database/migrations/2013_11_05_180133_confide_setup_users_table.php @@ -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,17 +223,22 @@ 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'); diff --git a/app/libraries/utils.php b/app/libraries/utils.php index fb8a64f256dc..90f73f0faad1 100755 --- a/app/libraries/utils.php +++ b/app/libraries/utils.php @@ -143,7 +143,7 @@ class Utils for ($i=0; $iinvitation_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); } } \ No newline at end of file diff --git a/app/models/Activity.php b/app/models/Activity.php index 97d223352dd4..c0190a074728 100755 --- a/app/models/Activity.php +++ b/app/models/Activity.php @@ -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() : 'System'; + + 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(); } @@ -97,27 +101,7 @@ class Activity extends Eloquent $activity->message = $userName . ' emailed invoice ' . link_to('invoices/'.$invitation->invoice->public_id, $invitation->invoice->invoice_number) . ' to ' . $invitation->contact->getFullName(); $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()) diff --git a/app/models/EntityModel.php b/app/models/EntityModel.php index 9b3a080ad213..15fd5b9e4704 100755 --- a/app/models/EntityModel.php +++ b/app/models/EntityModel.php @@ -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) { diff --git a/app/models/Invoice.php b/app/models/Invoice.php index 0bc5aac800f7..44f74a0c266a 100755 --- a/app/models/Invoice.php +++ b/app/models/Invoice.php @@ -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) diff --git a/app/routes.php b/app/routes.php index 6be7cbab6f90..b8b7abd3f8a4 100755 --- a/app/routes.php +++ b/app/routes.php @@ -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= ''; }); diff --git a/app/views/datatable.blade.php b/app/views/datatable.blade.php index cdf061671bd1..7f2f08eb1491 100755 --- a/app/views/datatable.blade.php +++ b/app/views/datatable.blade.php @@ -9,7 +9,7 @@ @foreach($columns as $i => $c) @if ($c == 'checkbox' && $hasCheckboxes = true) - + @else {{ $c }} @endif diff --git a/app/views/invoices/edit.blade.php b/app/views/invoices/edit.blade.php index dda109ab2a04..308c19f68f32 100755 --- a/app/views/invoices/edit.blade.php +++ b/app/views/invoices/edit.blade.php @@ -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 }}'); } /* diff --git a/app/views/list.blade.php b/app/views/list.blade.php index f2b3651b0b7b..1661258e9e26 100755 --- a/app/views/list.blade.php +++ b/app/views/list.blade.php @@ -20,13 +20,22 @@ {{ 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')) ->setOptions('sPaginationType', 'bootstrap') ->setOptions('bFilter', false) ->render('datatable') }} - + {{ Former::close() }}