Merge pull request #4082 from turbo124/v2

Working on recurring invoices
This commit is contained in:
David Bomba 2020-09-18 17:05:05 +10:00 committed by GitHub
commit 083e834400
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 137 additions and 60 deletions

View File

@ -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.
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -61,6 +61,7 @@ class ClientContact extends Authenticatable implements HasLocalePreference
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
'last_login' => 'timestamp',
];
protected $hidden = [

View File

@ -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) {

View File

@ -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('<img src="data:image/svg+xml;base64,%s"></img><p/>%s: %s', $this->signature_base64, ctrans('texts.signed'), $this->createClientDate($this->signature_date, $this->contact->client->timezone()->name));
}
}

View File

@ -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();
}

View File

@ -0,0 +1,33 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Transformers;
use App\Models\InvoiceInvitation;
use App\Models\RecurringInvoiceInvitation;
use App\Utils\Traits\MakesHash;
class RecurringInvoiceInvitationTransformer extends EntityTransformer
{
use MakesHash;
public function transform(RecurringInvoiceInvitation $invitation)
{
return [
'id' => $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,
];
}
}

View File

@ -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 ?: '',
];
}

View File

@ -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')];

View File

@ -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", "<br>", $this->body);
$this->subject = strtr($this->subject, $data['labels']);
$this->subject = strtr($this->subject, $data['values']);

View File

@ -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() ?: '&nbsp;', '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'];

View File

@ -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'];

View File

@ -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');
});
}