mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-24 02:14:21 -04:00
Merge pull request #4082 from turbo124/v2
Working on recurring invoices
This commit is contained in:
commit
083e834400
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -61,6 +61,7 @@ class ClientContact extends Authenticatable implements HasLocalePreference
|
||||
'updated_at' => 'timestamp',
|
||||
'created_at' => 'timestamp',
|
||||
'deleted_at' => 'timestamp',
|
||||
'last_login' => 'timestamp',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
|
@ -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) {
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
33
app/Transformers/RecurringInvoiceInvitationTransformer.php
Normal file
33
app/Transformers/RecurringInvoiceInvitationTransformer.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
@ -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 ?: '',
|
||||
];
|
||||
}
|
||||
|
@ -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')];
|
||||
|
@ -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']);
|
||||
|
@ -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'];
|
||||
|
@ -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'];
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user