diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 2307feb938ee..7e4cc49f59e3 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -45,27 +45,27 @@ class Kernel extends ConsoleKernel { //$schedule->job(new RecurringInvoicesCron)->hourly(); - $schedule->job(new VersionCheck)->daily(); + $schedule->job(new VersionCheck)->daily()->withoutOverlapping(); - $schedule->command('ninja:check-data')->daily(); + $schedule->command('ninja:check-data')->daily()->withoutOverlapping(); - $schedule->job(new ReminderJob)->daily(); + $schedule->job(new ReminderJob)->daily()->withoutOverlapping(); - $schedule->job(new CompanySizeCheck)->daily(); + $schedule->job(new CompanySizeCheck)->daily()->withoutOverlapping(); - $schedule->job(new UpdateExchangeRates)->daily(); + $schedule->job(new UpdateExchangeRates)->daily()->withoutOverlapping(); - $schedule->job(new RecurringInvoicesCron)->hourly(); + $schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping(); /* Run hosted specific jobs */ if (Ninja::isHosted()) { - $schedule->job(new AdjustEmailQuota())->daily(); - $schedule->job(new SendFailedEmails())->daily(); + $schedule->job(new AdjustEmailQuota())->daily()->withoutOverlapping(); + $schedule->job(new SendFailedEmails())->daily()->withoutOverlapping(); } /* Run queue's in shared hosting with this*/ if (Ninja::isSelfHost()) { $schedule->command('queue:work')->everyMinute()->withoutOverlapping(); - $schedule->command('queue:restart')->everyFiveMinutes(); //we need to add this as we are seeing cached queues mess up the system on first load. + $schedule->command('queue:restart')->everyFiveMinutes()->withoutOverlapping(); //we need to add this as we are seeing cached queues mess up the system on first load. } } diff --git a/app/Factory/RecurringInvoiceToInvoiceFactory.php b/app/Factory/RecurringInvoiceToInvoiceFactory.php index 572f2727d3c5..cafa7ee385fb 100644 --- a/app/Factory/RecurringInvoiceToInvoiceFactory.php +++ b/app/Factory/RecurringInvoiceToInvoiceFactory.php @@ -31,13 +31,15 @@ class RecurringInvoiceToInvoiceFactory $invoice->public_notes = $recurring_invoice->public_notes; $invoice->private_notes = $recurring_invoice->private_notes; $invoice->date = date_create()->format($client->date_format()); - $invoice->due_date = $recurring_invoice->due_date; //todo calculate based on terms + $invoice->due_date = $recurring_invoice->calculateDueDate($recurring_invoice->next_send_date); $invoice->is_deleted = $recurring_invoice->is_deleted; $invoice->line_items = $recurring_invoice->line_items; $invoice->tax_name1 = $recurring_invoice->tax_name1; $invoice->tax_rate1 = $recurring_invoice->tax_rate1; $invoice->tax_name2 = $recurring_invoice->tax_name2; $invoice->tax_rate2 = $recurring_invoice->tax_rate2; + $invoice->tax_name3 = $recurring_invoice->tax_name3; + $invoice->tax_rate3 = $recurring_invoice->tax_rate3; $invoice->custom_value1 = $recurring_invoice->custom_value1; $invoice->custom_value2 = $recurring_invoice->custom_value2; $invoice->custom_value3 = $recurring_invoice->custom_value3; @@ -45,10 +47,12 @@ class RecurringInvoiceToInvoiceFactory $invoice->amount = $recurring_invoice->amount; $invoice->balance = $recurring_invoice->balance; $invoice->user_id = $recurring_invoice->user_id; + $invoice->assigned_user_id = $recurring_invoice->assigned_user_id; $invoice->company_id = $recurring_invoice->company_id; $invoice->recurring_id = $recurring_invoice->id; $invoice->client_id = $client->id; - + $invoice->auto_bill_enabled = $recurring_invoice->auto_bill_enabled; + return $invoice; } } diff --git a/app/Jobs/RecurringInvoice/SendRecurring.php b/app/Jobs/RecurringInvoice/SendRecurring.php index 82e645f24ace..bec81ae599cf 100644 --- a/app/Jobs/RecurringInvoice/SendRecurring.php +++ b/app/Jobs/RecurringInvoice/SendRecurring.php @@ -62,7 +62,7 @@ class SendRecurring implements ShouldQueue ->createInvitations() ->save(); - $invoice->invitations->each(function ($invitation) use ($invoice) { + $invoice->invitations->each(function ($invitation) use ($invoice) { $email_builder = (new InvoiceEmail())->build($invitation); @@ -72,6 +72,9 @@ class SendRecurring implements ShouldQueue }); + if($invoice->client->getSetting('auto_bill_date') == 'on_send_date' && $this->recurring_invoice->auto_bill_enabled) + $invoice->service()->autoBill()->save(); + /* Set next date here to prevent a recurring loop forming */ $this->recurring_invoice->next_send_date = $this->recurring_invoice->nextSendDate()->format('Y-m-d'); $this->recurring_invoice->remaining_cycles = $this->recurring_invoice->remainingCycles(); @@ -86,10 +89,6 @@ class SendRecurring implements ShouldQueue if ($invoice->invitations->count() > 0) event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars())); - // Fire Payment if auto-bill is enabled - if ($this->recurring_invoice->auto_bill) - $invoice->service()->autoBill()->save(); - } } diff --git a/app/Models/ClientContact.php b/app/Models/ClientContact.php index 91cba76ac520..b6403d967210 100644 --- a/app/Models/ClientContact.php +++ b/app/Models/ClientContact.php @@ -61,6 +61,7 @@ class ClientContact extends Authenticatable implements HasLocalePreference 'updated_at' => 'timestamp', 'created_at' => 'timestamp', 'deleted_at' => 'timestamp', + 'last_login' => 'timestamp', ]; protected $hidden = [ diff --git a/app/Models/RecurringInvoice.php b/app/Models/RecurringInvoice.php index a650ba3c7ea4..64de0049e65f 100644 --- a/app/Models/RecurringInvoice.php +++ b/app/Models/RecurringInvoice.php @@ -14,6 +14,7 @@ namespace App\Models; use App\Helpers\Invoice\InvoiceSum; use App\Helpers\Invoice\InvoiceSumInclusive; use App\Models\Filterable; +use App\Models\RecurringInvoiceInvitation; use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; use App\Utils\Traits\Recurring\HasRecurrence; @@ -99,6 +100,8 @@ class RecurringInvoice extends BaseModel 'amount', 'partial', 'frequency_id', + 'next_send_date', + 'remaining_cycles', ]; protected $casts = [ @@ -176,9 +179,14 @@ class RecurringInvoice extends BaseModel public function invitations() { - $this->morphMany(RecurringInvoiceInvitation::class); + return $this->hasMany(RecurringInvoiceInvitation::class); } + public function documents() + { + return $this->morphMany(Document::class, 'documentable'); + } + public function getStatusAttribute() { if ($this->status_id == self::STATUS_ACTIVE && $this->next_send_date > Carbon::now()) { //marked as active, but yet to fire first cycle @@ -407,7 +415,7 @@ class RecurringInvoice extends BaseModel } - private function calculateDueDate($date) + public function calculateDueDate($date) { switch ($this->due_date_days) { diff --git a/app/Models/RecurringInvoiceInvitation.php b/app/Models/RecurringInvoiceInvitation.php index d5a66a8415c0..6d3f7cca1922 100644 --- a/app/Models/RecurringInvoiceInvitation.php +++ b/app/Models/RecurringInvoiceInvitation.php @@ -11,12 +11,10 @@ namespace App\Models; -use App\Utils\Traits\MakesDates; use Illuminate\Database\Eloquent\Model; class RecurringInvoiceInvitation extends BaseModel { - use MakesDates; protected $fillable = ['client_contact_id']; @@ -59,12 +57,5 @@ class RecurringInvoiceInvitation extends BaseModel return $this->belongsTo(Company::class); } - public function signatureDiv() - { - if (! $this->signature_base64) { - return false; - } - return sprintf('

%s: %s', $this->signature_base64, ctrans('texts.signed'), $this->createClientDate($this->signature_date, $this->contact->client->timezone()->name)); - } } diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 8ce07c3c8b8c..3dad55c4e8f6 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -46,7 +46,7 @@ class AutoBillInvoice extends AbstractService $this->invoice = $this->invoice->service()->markSent()->save(); if ($this->invoice->balance > 0) { - $gateway_token = $this->getGateway($this->invoice->balance); + $gateway_token = $this->getGateway($this->invoice->balance); //todo what if it is only a partial amount? } else { return $this->invoice->service()->markPaid()->save(); } diff --git a/app/Transformers/RecurringInvoiceInvitationTransformer.php b/app/Transformers/RecurringInvoiceInvitationTransformer.php new file mode 100644 index 000000000000..aff8dee99096 --- /dev/null +++ b/app/Transformers/RecurringInvoiceInvitationTransformer.php @@ -0,0 +1,33 @@ + $this->encodePrimaryKey($invitation->id), + 'client_contact_id' => $this->encodePrimaryKey($invitation->client_contact_id), + 'key' => $invitation->key, + 'updated_at' => (int) $invitation->updated_at, + 'archived_at' => (int) $invitation->deleted_at, + 'created_at' => (int) $invitation->created_at, + ]; + } +} diff --git a/app/Transformers/RecurringInvoiceTransformer.php b/app/Transformers/RecurringInvoiceTransformer.php index 103679abc750..b43e1195dc38 100644 --- a/app/Transformers/RecurringInvoiceTransformer.php +++ b/app/Transformers/RecurringInvoiceTransformer.php @@ -11,8 +11,12 @@ namespace App\Transformers; +use App\Models\Document; use App\Models\Invoice; use App\Models\RecurringInvoice; +use App\Models\RecurringInvoiceInvitation; +use App\Transformers\DocumentTransformer; +use App\Transformers\RecurringInvoiceInvitationTransformer; use App\Utils\Traits\MakesHash; class RecurringInvoiceTransformer extends EntityTransformer @@ -20,14 +24,15 @@ class RecurringInvoiceTransformer extends EntityTransformer use MakesHash; protected $defaultIncludes = [ - // 'invoice_items', + 'invitations', + 'documents', ]; protected $availableIncludes = [ - // 'invitations', + 'invitations', + 'documents', // 'payments', // 'client', - // 'documents', ]; /* @@ -58,25 +63,21 @@ class RecurringInvoiceTransformer extends EntityTransformer return $this->includeItem($invoice->client, $transformer, ENTITY_CLIENT); } - - public function includeExpenses(Invoice $invoice) - { - $transformer = new ExpenseTransformer($this->account, $this->serializer); - - return $this->includeCollection($invoice->expenses, $transformer, ENTITY_EXPENSE); - } - - public function includeDocuments(Invoice $invoice) - { - $transformer = new DocumentTransformer($this->account, $this->serializer); - - $invoice->documents->each(function ($document) use ($invoice) { - $document->setRelation('invoice', $invoice); - }); - - return $this->includeCollection($invoice->documents, $transformer, ENTITY_DOCUMENT); - } */ + public function includeInvitations(RecurringInvoice $invoice) + { + $transformer = new RecurringInvoiceInvitationTransformer($this->serializer); + + return $this->includeCollection($invoice->invitations, $transformer, RecurringInvoiceInvitation::class); + } + + public function includeDocuments(RecurringInvoice $invoice) + { + $transformer = new DocumentTransformer($this->serializer); + + return $this->includeCollection($invoice->documents, $transformer, Document::class); + } + public function transform(RecurringInvoice $invoice) { return [ @@ -136,6 +137,7 @@ class RecurringInvoiceTransformer extends EntityTransformer 'remaining_cycles' => (int) $invoice->remaining_cycles, 'recurring_dates' => (array) $invoice->recurringDates(), 'auto_bill' => (string) $invoice->auto_bill, + 'auto_bill_enabled' => (bool) $invoice->auto_bill_enabled, 'due_date_days' => (string) $invoice->due_date_days ?: '', ]; } diff --git a/app/Utils/HtmlEngine.php b/app/Utils/HtmlEngine.php index f9cdc0814928..54bb3c296309 100644 --- a/app/Utils/HtmlEngine.php +++ b/app/Utils/HtmlEngine.php @@ -237,6 +237,7 @@ class HtmlEngine $data['$client.email'] = &$data['$email']; $data['$client.balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')]; + $data['$client_balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')]; $data['$paid_to_date'] = ['value' => Number::formatMoney($this->client->paid_to_date, $this->client), 'label' => ctrans('texts.paid_to_date')]; $data['$contact.full_name'] = ['value' => $this->contact->present()->name(), 'label' => ctrans('texts.name')]; diff --git a/app/Utils/TemplateEngine.php b/app/Utils/TemplateEngine.php index 49d4f7b0d46a..14ce8fb1fa52 100644 --- a/app/Utils/TemplateEngine.php +++ b/app/Utils/TemplateEngine.php @@ -88,12 +88,23 @@ class TemplateEngine private function setTemplates() { if (strlen($this->subject) == 0 && strlen($this->template) > 1) { + $subject_template = str_replace('template', 'subject', $this->template); - $this->subject = EmailTemplateDefaults::getDefaultTemplate($subject_template, $this->settings_entity->locale()); + + if(strlen($this->settings_entity->getSetting($subject_template)) > 1) + $this->subject = $this->settings_entity->getSetting($subject_template); + else + $this->subject = EmailTemplateDefaults::getDefaultTemplate($subject_template, $this->settings_entity->locale()); + } if (strlen($this->body) == 0 && strlen($this->template) > 1) { - $this->body = EmailTemplateDefaults::getDefaultTemplate($this->template, $this->settings_entity->locale()); + + if(strlen($this->settings_entity->getSetting($this->template)) > 1) + $this->body = $this->settings_entity->getSetting($this->template); + else + $this->body = EmailTemplateDefaults::getDefaultTemplate($this->template, $this->settings_entity->locale()); + } return $this; @@ -131,9 +142,12 @@ class TemplateEngine private function entityValues($contact) { $data = $this->entity_obj->buildLabelsAndValues($contact); + // $arrKeysLength = array_map('strlen', array_keys($data)); + // array_multisort($arrKeysLength, SORT_DESC, $data); $this->body = strtr($this->body, $data['labels']); $this->body = strtr($this->body, $data['values']); + $this->body = str_replace("\n", "
", $this->body); $this->subject = strtr($this->subject, $data['labels']); $this->subject = strtr($this->subject, $data['values']); diff --git a/app/Utils/Traits/MakesInvoiceValues.php b/app/Utils/Traits/MakesInvoiceValues.php index 7d4a5775ac98..54f2a56513d4 100644 --- a/app/Utils/Traits/MakesInvoiceValues.php +++ b/app/Utils/Traits/MakesInvoiceValues.php @@ -313,9 +313,10 @@ trait MakesInvoiceValues $data['$email'] = ['value' => isset($contact) ? $contact->email : 'no contact email on record', 'label' => ctrans('texts.email')]; $data['$client_name'] = ['value' => $this->present()->clientName() ?: ' ', 'label' => ctrans('texts.client_name')]; $data['$client.name'] = &$data['$client_name']; - $data['$client.balance'] = ['value' => $this->client->balance, 'label' => ctrans('texts.balance')]; - - $data['$paid_to_date'] = ['value' => $this->client->paid_to_date, 'label' => ctrans('texts.paid_to_date')]; + $data['$client.balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')]; + $data['$client_balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')]; + + $data['$paid_to_date'] = ['value' => Number::formatMoney($this->client->paid_to_date, $this->client), 'label' => ctrans('texts.paid_to_date')]; $data['$client.address1'] = &$data['$address1']; $data['$client.address2'] = &$data['$address2']; diff --git a/app/Utils/Traits/MakesTemplateData.php b/app/Utils/Traits/MakesTemplateData.php index ffab5e775bc7..dc8e63ca1284 100644 --- a/app/Utils/Traits/MakesTemplateData.php +++ b/app/Utils/Traits/MakesTemplateData.php @@ -135,6 +135,7 @@ trait MakesTemplateData $data['$country'] = ['value' => 'USA', 'label' => ctrans('texts.country')]; $data['$email'] = ['value' => 'user@example.com', 'label' => ctrans('texts.email')]; $data['$client_name'] = ['value' => 'Joe Denkins', 'label' => ctrans('texts.client_name')]; + $data['$client.balance'] = ['value' => '$100', 'label' => ctrans('texts.account_balance')]; $data['$client.name'] = &$data['$client_name']; $data['$client.address1'] = &$data['$address1']; $data['$client.address2'] = &$data['$address2']; diff --git a/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php b/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php index 8713851f8e66..b843e2bd93a1 100644 --- a/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php +++ b/database/migrations/2020_08_18_140557_add_is_public_to_documents_table.php @@ -48,7 +48,8 @@ class AddIsPublicToDocumentsTable extends Migration }); Schema::table('recurring_invoices', function ($table) { - $table->boolean('auto_bill')->default(0); + $table->string('auto_bill')->default('off'); + $table->boolean('auto_bill_enabled')->default(0); $table->unsignedInteger('design_id')->nullable(); $table->boolean('uses_inclusive_taxes')->default(0); $table->string('custom_surcharge1')->nullable(); @@ -59,19 +60,40 @@ class AddIsPublicToDocumentsTable extends Migration $table->boolean('custom_surcharge_tax2')->default(false); $table->boolean('custom_surcharge_tax3')->default(false); $table->boolean('custom_surcharge_tax4')->default(false); + $table->integer('remaining_cycles')->nullable()->change(); + $table->dropColumn('start_date'); + $table->string('due_date_days')->nullable(); + $table->date('partial_due_date')->nullable(); $table->decimal('exchange_rate', 13, 6)->default(1); }); + Schema::table('invoices', function ($table) { + $table->boolean('auto_bill_enabled')->default(0); + }); + Schema::table('companies', function ($table) { $table->enum('default_auto_bill', ['off', 'always', 'optin', 'optout'])->default('off'); }); - Schema::table('recurring_invoices', function (Blueprint $table) { - $table->integer('remaining_cycles')->nullable()->change(); - $table->dropColumn('start_date'); - $table->string('due_date_days')->nullable(); - $table->date('partial_due_date')->nullable(); + Schema::create('recurring_invoice_invitations', function ($t) { + $t->increments('id'); + $t->unsignedInteger('company_id'); + $t->unsignedInteger('user_id'); + $t->unsignedInteger('client_contact_id'); + $t->unsignedInteger('recurring_invoice_id')->index(); + $t->string('key')->index(); + + $t->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade'); + $t->foreign('client_contact_id')->references('id')->on('client_contacts')->onDelete('cascade')->onUpdate('cascade'); + $t->foreign('recurring_invoice_id')->references('id')->on('recurring_invoices')->onDelete('cascade')->onUpdate('cascade'); + $t->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade'); + + $t->timestamps(6); + $t->softDeletes('deleted_at', 6); + + $t->index(['deleted_at', 'recurring_invoice_id', 'company_id'], 'rec_co_del'); + $t->unique(['client_contact_id', 'recurring_invoice_id'], 'cli_rec'); }); }