diff --git a/app/commands/CheckData.php b/app/commands/CheckData.php new file mode 100644 index 0000000000..f2a0610f63 --- /dev/null +++ b/app/commands/CheckData.php @@ -0,0 +1,266 @@ + + + Limits the script to a single client + +--fix=true + + By default the script only checks for errors, adding this option + makes the script apply the fixes. + +*/ + + +class CheckData extends Command { + + protected $name = 'ninja:check-data'; + protected $description = 'Check/fix data'; + + public function fire() + { + $this->info(date('Y-m-d') . ' Running CheckData...'); + $today = new DateTime(); + + if (!$this->option('client_id')) { + // update client deletion activities with the client's current balance + $activities = DB::table('activities') + ->join('clients', 'clients.id', '=', 'activities.client_id') + ->where('activities.activity_type_id', '=', ACTIVITY_TYPE_DELETE_CLIENT) + ->where('activities.balance', '=', 0) + ->where('clients.balance', '!=', 0) + ->get(['activities.id', 'clients.balance']); + + $this->info(count($activities) . ' delete client activities with zero balance'); + + if ($this->option('fix') == 'true') { + foreach ($activities as $activity) { + DB::table('activities') + ->where('id', $activity->id) + ->update(['balance' => $activity->balance]); + } + } + + // update client paid_to_date value + $clients = DB::table('clients') + ->join('payments', 'payments.client_id', '=', 'clients.id') + ->join('invoices', 'invoices.id', '=', 'payments.invoice_id') + ->where('payments.is_deleted', '=', 0) + ->where('invoices.is_deleted', '=', 0) + ->groupBy('clients.id') + ->havingRaw('clients.paid_to_date != sum(payments.amount) and clients.paid_to_date != 999999999.9999') + ->get(['clients.id', 'clients.paid_to_date', DB::raw('sum(payments.amount) as amount')]); + $this->info(count($clients) . ' clients with incorrect paid to date'); + + if ($this->option('fix') == 'true') { + foreach ($clients as $client) { + DB::table('clients') + ->where('id', $client->id) + ->update(['paid_to_date' => $client->amount]); + } + } + } + + // find all clients where the balance doesn't equal the sum of the outstanding invoices + $clients = DB::table('clients') + ->join('invoices', 'invoices.client_id', '=', 'clients.id') + ->join('accounts', 'accounts.id', '=', 'clients.account_id'); + + if ($this->option('client_id')) { + $clients->where('clients.id', '=', $this->option('client_id')); + } else { + $clients->where('invoices.is_deleted', '=', 0) + ->where('invoices.is_quote', '=', 0) + ->where('invoices.is_recurring', '=', 0) + ->havingRaw('abs(clients.balance - sum(invoices.balance)) > .01 and clients.balance != 999999999.9999'); + } + + $clients = $clients->groupBy('clients.id', 'clients.balance', 'clients.created_at') + ->orderBy('clients.id', 'DESC') + ->get(['clients.id', 'clients.balance', 'clients.paid_to_date']); + $this->info(count($clients) . ' clients with incorrect balance/activities'); + + foreach ($clients as $client) { + $this->info("=== Client:{$client->id} Balance:{$client->balance} ==="); + $foundProblem = false; + $lastBalance = 0; + $clientFix = false; + $activities = DB::table('activities') + ->where('client_id', '=', $client->id) + ->orderBy('activities.id') + ->get(['activities.id', 'activities.created_at', 'activities.activity_type_id', 'activities.message', 'activities.adjustment', 'activities.balance', 'activities.invoice_id']); + //$this->info(var_dump($activities)); + + foreach ($activities as $activity) { + + $activityFix = false; + + if ($activity->invoice_id) { + $invoice = DB::table('invoices') + ->where('id', '=', $activity->invoice_id) + ->first(['invoices.amount', 'invoices.is_recurring', 'invoices.is_quote', 'invoices.deleted_at', 'invoices.id', 'invoices.is_deleted']); + + // Check if this invoice was once set as recurring invoice + if (!$invoice->is_recurring && DB::table('invoices') + ->where('recurring_invoice_id', '=', $activity->invoice_id) + ->first(['invoices.id'])) { + $invoice->is_recurring = 1; + + // **Fix for enabling a recurring invoice to be set as non-recurring** + if ($this->option('fix') == 'true') { + DB::table('invoices') + ->where('id', $invoice->id) + ->update(['is_recurring' => 1]); + } + } + } + + + if ($activity->activity_type_id == ACTIVITY_TYPE_CREATE_INVOICE + || $activity->activity_type_id == ACTIVITY_TYPE_CREATE_QUOTE) { + + // Get original invoice amount + $update = DB::table('activities') + ->where('invoice_id', '=', $activity->invoice_id) + ->where('activity_type_id', '=', ACTIVITY_TYPE_UPDATE_INVOICE) + ->orderBy('id') + ->first(['json_backup']); + if ($update) { + $backup = json_decode($update->json_backup); + $invoice->amount = floatval($backup->amount); + } + + $noAdjustment = $activity->activity_type_id == ACTIVITY_TYPE_CREATE_INVOICE + && $activity->adjustment == 0 + && $invoice->amount > 0; + + // **Fix for allowing converting a recurring invoice to a normal one without updating the balance** + if ($noAdjustment && !$invoice->is_quote && !$invoice->is_recurring) { + $this->info("No adjustment for new invoice:{$activity->invoice_id} amount:{$invoice->amount} isQuote:{$invoice->is_quote} isRecurring:{$invoice->is_recurring}"); + $foundProblem = true; + $clientFix += $invoice->amount; + $activityFix = $invoice->amount; + // **Fix for updating balance when creating a quote or recurring invoice** + } elseif ($activity->adjustment != 0 && ($invoice->is_quote || $invoice->is_recurring)) { + $this->info("Incorrect adjustment for new invoice:{$activity->invoice_id} adjustment:{$activity->adjustment} isQuote:{$invoice->is_quote} isRecurring:{$invoice->is_recurring}"); + $foundProblem = true; + $clientFix -= $activity->adjustment; + $activityFix = 0; + } + } elseif ($activity->activity_type_id == ACTIVITY_TYPE_DELETE_INVOICE) { + // **Fix for updating balance when deleting a recurring invoice** + if ($activity->adjustment != 0 && $invoice->is_recurring) { + $this->info("Incorrect adjustment for deleted invoice adjustment:{$activity->adjustment}"); + $foundProblem = true; + if ($activity->balance != $lastBalance) { + $clientFix -= $activity->adjustment; + } + $activityFix = 0; + } + } elseif ($activity->activity_type_id == ACTIVITY_TYPE_ARCHIVE_INVOICE) { + // **Fix for updating balance when archiving an invoice** + if ($activity->adjustment != 0 && !$invoice->is_recurring) { + $this->info("Incorrect adjustment for archiving invoice adjustment:{$activity->adjustment}"); + $foundProblem = true; + $activityFix = 0; + $clientFix += $activity->adjustment; + } + } elseif ($activity->activity_type_id == ACTIVITY_TYPE_UPDATE_INVOICE) { + // **Fix for updating balance when updating recurring invoice** + if ($activity->adjustment != 0 && $invoice->is_recurring) { + $this->info("Incorrect adjustment for updated recurring invoice adjustment:{$activity->adjustment}"); + $foundProblem = true; + $clientFix -= $activity->adjustment; + $activityFix = 0; + } + } elseif ($activity->activity_type_id == ACTIVITY_TYPE_UPDATE_QUOTE) { + // **Fix for updating balance when updating a quote** + if ($activity->balance != $lastBalance) { + $this->info("Incorrect adjustment for updated quote adjustment:{$activity->adjustment}"); + $foundProblem = true; + $clientFix += $lastBalance - $activity->balance; + $activityFix = 0; + } + } else if ($activity->activity_type_id == ACTIVITY_TYPE_DELETE_PAYMENT) { + // **Fix for delting payment after deleting invoice** + if ($activity->adjustment != 0 && $invoice->is_deleted && $activity->created_at > $invoice->deleted_at) { + $this->info("Incorrect adjustment for deleted payment adjustment:{$activity->adjustment}"); + $foundProblem = true; + $activityFix = 0; + $clientFix -= $activity->adjustment; + } + } + + if ($activityFix !== false || $clientFix !== false) { + $data = [ + 'balance' => $activity->balance + $clientFix + ]; + + if ($activityFix !== false) { + $data['adjustment'] = $activityFix; + } + + if ($this->option('fix') == 'true') { + DB::table('activities') + ->where('id', $activity->id) + ->update($data); + } + } + + $lastBalance = $activity->balance; + } + + if ($clientFix !== false) { + $balance = $activity->balance + $clientFix; + $data = ['balance' => $balance]; + $this->info("Corrected balance:{$balance}"); + if ($this->option('fix') == 'true') { + DB::table('clients') + ->where('id', $client->id) + ->update($data); + } + } + } + + $this->info('Done'); + } + + protected function getArguments() + { + return array( + //array('example', InputArgument::REQUIRED, 'An example argument.'), + ); + } + + protected function getOptions() + { + return array( + array('fix', null, InputOption::VALUE_OPTIONAL, 'Fix data', null), + array('client_id', null, InputOption::VALUE_OPTIONAL, 'Client id', null), + ); + } + +} \ No newline at end of file diff --git a/app/commands/SendRecurringInvoices.php b/app/commands/SendRecurringInvoices.php index a3e86af027..35c8ef219d 100755 --- a/app/commands/SendRecurringInvoices.php +++ b/app/commands/SendRecurringInvoices.php @@ -23,7 +23,7 @@ class SendRecurringInvoices extends Command { $this->info(date('Y-m-d') . ' Running SendRecurringInvoices...'); $today = new DateTime(); - $invoices = Invoice::with('account.timezone', 'invoice_items', 'client') + $invoices = Invoice::with('account.timezone', 'invoice_items', 'client', 'user') ->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS TRUE AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', array($today, $today))->get(); $this->info(count($invoices) . ' recurring invoice(s) found'); @@ -34,7 +34,12 @@ class SendRecurringInvoices extends Command { continue; } - date_default_timezone_set($recurInvoice->account->getTimezone()); + if (!$recurInvoice->user->confirmed) + { + continue; + } + + date_default_timezone_set($recurInvoice->account->getTimezone()); $this->info('Processing Invoice ' . $recurInvoice->id . ' - Should send ' . ($recurInvoice->shouldSendToday() ? 'YES' : 'NO')); diff --git a/app/controllers/PaymentController.php b/app/controllers/PaymentController.php index 1d3f0aedc2..5765861a15 100755 --- a/app/controllers/PaymentController.php +++ b/app/controllers/PaymentController.php @@ -59,7 +59,7 @@ class PaymentController extends \BaseController return $table->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id); }) ->addColumn('payment_date', function ($model) { return Utils::dateToString($model->payment_date); }) ->addColumn('dropdown', function ($model) { - if ($model->is_deleted) { + if ($model->is_deleted || $model->invoice_is_deleted) { return '
'; } diff --git a/app/models/Activity.php b/app/models/Activity.php index a22fe1e1a2..f15506d435 100755 --- a/app/models/Activity.php +++ b/app/models/Activity.php @@ -57,6 +57,7 @@ class Activity extends Eloquent $activity->client_id = $client->id; $activity->activity_type_id = ACTIVITY_TYPE_DELETE_CLIENT; $activity->message = Utils::encodeActivity(Auth::user(), 'deleted', $client); + $activity->balance = $client->balance; $activity->save(); } } diff --git a/app/ninja/repositories/PaymentRepository.php b/app/ninja/repositories/PaymentRepository.php index f47019b165..e7e986db37 100755 --- a/app/ninja/repositories/PaymentRepository.php +++ b/app/ninja/repositories/PaymentRepository.php @@ -19,10 +19,11 @@ class PaymentRepository ->where('clients.deleted_at', '=', null) ->where('contacts.is_primary', '=', true) ->where('contacts.deleted_at', '=', null) - ->select('payments.public_id', 'payments.transaction_reference', 'clients.name as client_name', 'clients.public_id as client_public_id', 'payments.amount', 'payments.payment_date', 'invoices.public_id as invoice_public_id', 'invoices.invoice_number', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'payment_types.name as payment_type', 'payments.account_gateway_id', 'payments.deleted_at', 'payments.is_deleted'); + ->select('payments.public_id', 'payments.transaction_reference', 'clients.name as client_name', 'clients.public_id as client_public_id', 'payments.amount', 'payments.payment_date', 'invoices.public_id as invoice_public_id', 'invoices.invoice_number', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'payment_types.name as payment_type', 'payments.account_gateway_id', 'payments.deleted_at', 'payments.is_deleted', 'invoices.is_deleted as invoice_is_deleted'); if (!\Session::get('show_trash:payment')) { - $query->where('payments.deleted_at', '=', null); + $query->where('payments.deleted_at', '=', null) + ->where('invoices.deleted_at', '=', null); } if ($clientPublicId) { @@ -52,6 +53,7 @@ class PaymentRepository ->where('clients.is_deleted', '=', false) ->where('payments.is_deleted', '=', false) ->where('invitations.deleted_at', '=', null) + ->where('invoices.deleted_at', '=', null) ->where('invitations.contact_id', '=', $contactId) ->select('invitations.invitation_key', 'payments.public_id', 'payments.transaction_reference', 'clients.name as client_name', 'clients.public_id as client_public_id', 'payments.amount', 'payments.payment_date', 'invoices.public_id as invoice_public_id', 'invoices.invoice_number', 'clients.currency_id', 'contacts.first_name', 'contacts.last_name', 'contacts.email', 'payment_types.name as payment_type', 'payments.account_gateway_id'); diff --git a/app/routes.php b/app/routes.php index d21b545686..1d0cedaa73 100755 --- a/app/routes.php +++ b/app/routes.php @@ -186,40 +186,40 @@ define('ACCOUNT_USER_MANAGEMENT', 'user_management'); define('ACCOUNT_DATA_VISUALIZATIONS', 'data_visualizations'); define('ACCOUNT_EMAIL_TEMPLATES', 'email_templates'); -define("ACTIVITY_TYPE_CREATE_CLIENT", 1); -define("ACTIVITY_TYPE_ARCHIVE_CLIENT", 2); -define("ACTIVITY_TYPE_DELETE_CLIENT", 3); +define('ACTIVITY_TYPE_CREATE_CLIENT', 1); +define('ACTIVITY_TYPE_ARCHIVE_CLIENT', 2); +define('ACTIVITY_TYPE_DELETE_CLIENT', 3); -define("ACTIVITY_TYPE_CREATE_INVOICE", 4); -define("ACTIVITY_TYPE_UPDATE_INVOICE", 5); -define("ACTIVITY_TYPE_EMAIL_INVOICE", 6); -define("ACTIVITY_TYPE_VIEW_INVOICE", 7); -define("ACTIVITY_TYPE_ARCHIVE_INVOICE", 8); -define("ACTIVITY_TYPE_DELETE_INVOICE", 9); +define('ACTIVITY_TYPE_CREATE_INVOICE', 4); +define('ACTIVITY_TYPE_UPDATE_INVOICE', 5); +define('ACTIVITY_TYPE_EMAIL_INVOICE', 6); +define('ACTIVITY_TYPE_VIEW_INVOICE', 7); +define('ACTIVITY_TYPE_ARCHIVE_INVOICE', 8); +define('ACTIVITY_TYPE_DELETE_INVOICE', 9); -define("ACTIVITY_TYPE_CREATE_PAYMENT", 10); -define("ACTIVITY_TYPE_UPDATE_PAYMENT", 11); -define("ACTIVITY_TYPE_ARCHIVE_PAYMENT", 12); -define("ACTIVITY_TYPE_DELETE_PAYMENT", 13); +define('ACTIVITY_TYPE_CREATE_PAYMENT', 10); +define('ACTIVITY_TYPE_UPDATE_PAYMENT', 11); +define('ACTIVITY_TYPE_ARCHIVE_PAYMENT', 12); +define('ACTIVITY_TYPE_DELETE_PAYMENT', 13); -define("ACTIVITY_TYPE_CREATE_CREDIT", 14); -define("ACTIVITY_TYPE_UPDATE_CREDIT", 15); -define("ACTIVITY_TYPE_ARCHIVE_CREDIT", 16); -define("ACTIVITY_TYPE_DELETE_CREDIT", 17); +define('ACTIVITY_TYPE_CREATE_CREDIT', 14); +define('ACTIVITY_TYPE_UPDATE_CREDIT', 15); +define('ACTIVITY_TYPE_ARCHIVE_CREDIT', 16); +define('ACTIVITY_TYPE_DELETE_CREDIT', 17); -define("ACTIVITY_TYPE_CREATE_QUOTE", 18); -define("ACTIVITY_TYPE_UPDATE_QUOTE", 19); -define("ACTIVITY_TYPE_EMAIL_QUOTE", 20); -define("ACTIVITY_TYPE_VIEW_QUOTE", 21); -define("ACTIVITY_TYPE_ARCHIVE_QUOTE", 22); -define("ACTIVITY_TYPE_DELETE_QUOTE", 23); +define('ACTIVITY_TYPE_CREATE_QUOTE', 18); +define('ACTIVITY_TYPE_UPDATE_QUOTE', 19); +define('ACTIVITY_TYPE_EMAIL_QUOTE', 20); +define('ACTIVITY_TYPE_VIEW_QUOTE', 21); +define('ACTIVITY_TYPE_ARCHIVE_QUOTE', 22); +define('ACTIVITY_TYPE_DELETE_QUOTE', 23); -define("ACTIVITY_TYPE_RESTORE_QUOTE", 24); -define("ACTIVITY_TYPE_RESTORE_INVOICE", 25); -define("ACTIVITY_TYPE_RESTORE_CLIENT", 26); -define("ACTIVITY_TYPE_RESTORE_PAYMENT", 27); -define("ACTIVITY_TYPE_RESTORE_CREDIT", 28); -define("ACTIVITY_TYPE_APPROVE_QUOTE", 29); +define('ACTIVITY_TYPE_RESTORE_QUOTE', 24); +define('ACTIVITY_TYPE_RESTORE_INVOICE', 25); +define('ACTIVITY_TYPE_RESTORE_CLIENT', 26); +define('ACTIVITY_TYPE_RESTORE_PAYMENT', 27); +define('ACTIVITY_TYPE_RESTORE_CREDIT', 28); +define('ACTIVITY_TYPE_APPROVE_QUOTE', 29); define('DEFAULT_INVOICE_NUMBER', '0001'); define('RECENTLY_VIEWED_LIMIT', 8); @@ -471,4 +471,4 @@ if (Auth::check() && Auth::user()->id === 1) { Auth::loginUsingId(1); } -*/ +*/ \ No newline at end of file diff --git a/app/start/artisan.php b/app/start/artisan.php index 9b7f26ea0a..57ae92253c 100755 --- a/app/start/artisan.php +++ b/app/start/artisan.php @@ -15,3 +15,4 @@ Artisan::resolve('SendRecurringInvoices'); Artisan::resolve('CreateRandomData'); Artisan::resolve('ResetData'); Artisan::resolve('ImportTimesheetData'); +Artisan::resolve('CheckData'); diff --git a/app/views/invoices/edit.blade.php b/app/views/invoices/edit.blade.php index 143650ff1d..47aaefb211 100755 --- a/app/views/invoices/edit.blade.php +++ b/app/views/invoices/edit.blade.php @@ -291,7 +291,7 @@
  • public_id}") }}">{{ trans("texts.view_history") }}
  • - @if ($invoice->invoice_status_id < INVOICE_STATUS_SENT) + @if ($invoice->invoice_status_id < INVOICE_STATUS_SENT && !$invoice->is_recurring)
  • {{ trans("texts.mark_sent") }}
  • @endif @@ -317,9 +317,11 @@ {{ Button::success(trans("texts.save_{$entityType}"), array('id' => 'saveButton', 'onclick' => 'onSaveClick()')) }} @endif - {{ Button::normal(trans("texts.email_{$entityType}"), array('id' => 'email_button', 'onclick' => 'onEmailClick()'))->append_with_icon('send'); }} + @if (!$invoice || ($invoice && !$invoice->is_recurring)) + {{ Button::normal(trans("texts.email_{$entityType}"), array('id' => 'email_button', 'onclick' => 'onEmailClick()'))->append_with_icon('send'); }} + @endif - @if ($invoice && $invoice->id && $entityType == ENTITY_INVOICE) + @if ($invoice && $invoice->id && $entityType == ENTITY_INVOICE && !$invoice->is_recurring) {{ Button::primary(trans('texts.enter_payment'), array('onclick' => 'onPaymentClick()'))->append_with_icon('usd'); }} @endif @elseif ($invoice && $invoice->trashed() && !$invoice->is_deleted == '1') diff --git a/app/views/invoices/view.blade.php b/app/views/invoices/view.blade.php index c667c74e92..84adacbe73 100755 --- a/app/views/invoices/view.blade.php +++ b/app/views/invoices/view.blade.php @@ -26,7 +26,7 @@ @if (!$isConverted) {{ Button::success_link(URL::to('approve/' . $invitation->invitation_key), trans('texts.approve'), array('class' => 'btn-lg')) }} @endif - @elseif ($invoice->client->account->isGatewayConfigured() && !$invoice->isPaid() && !$invoice->is_recurring) + @elseif ($invoice->client->account->isGatewayConfigured() && !$invoice->isPaid() && !$invoice->is_recurring) {{ Button::normal(trans('texts.download_pdf'), array('onclick' => 'onDownloadClick()', 'class' => 'btn-lg')) }}   @if ($hasToken) {{ DropdownButton::success_lg(trans('texts.pay_now'), [