1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 05:02:36 +01:00

Implement EmailInvoice Job (#3166)

* Working on quote counter

* Add tests for quote number + shared counter tests

* Create invoice job

* Add last_sent_date to invoice/quote table, remove type_id

* Implement EmailInvoice Job
This commit is contained in:
David Bomba 2019-12-22 21:28:41 +11:00 committed by GitHub
parent b0da84baa7
commit 5e7512071f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 619 additions and 43 deletions

View File

@ -11,6 +11,7 @@ use App\Factory\InvoiceItemFactory;
use App\Factory\PaymentFactory; use App\Factory\PaymentFactory;
use App\Helpers\Invoice\InvoiceSum; use App\Helpers\Invoice\InvoiceSum;
use App\Jobs\Company\UpdateCompanyLedgerWithInvoice; use App\Jobs\Company\UpdateCompanyLedgerWithInvoice;
use App\Jobs\Invoice\CreateInvoiceInvitations;
use App\Jobs\Invoice\UpdateInvoicePayment; use App\Jobs\Invoice\UpdateInvoicePayment;
use App\Listeners\Invoice\CreateInvoiceInvitation; use App\Listeners\Invoice\CreateInvoiceInvitation;
use App\Models\CompanyToken; use App\Models\CompanyToken;
@ -316,8 +317,8 @@ class CreateTestData extends Command
$this->invoice_repo->markSent($invoice); $this->invoice_repo->markSent($invoice);
event(new InvoiceWasMarkedSent($invoice)); CreateInvoiceInvitations::dispatch($invoice);
if(rand(0, 1)) { if(rand(0, 1)) {
$payment = PaymentFactory::create($client->company->id, $client->user->id); $payment = PaymentFactory::create($client->company->id, $client->user->id);

View File

@ -2,7 +2,10 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Factory\ClientFactory;
use App\Mail\TemplateEmail; use App\Mail\TemplateEmail;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\User; use App\Models\User;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@ -43,7 +46,9 @@ class SendTestEmails extends Command
public function handle() public function handle()
{ {
$this->sendTemplateEmails('plain'); $this->sendTemplateEmails('plain');
sleep(5);
$this->sendTemplateEmails('light'); $this->sendTemplateEmails('light');
sleep(5);
$this->sendTemplateEmails('dark'); $this->sendTemplateEmails('dark');
} }
@ -57,21 +62,48 @@ class SendTestEmails extends Command
]; ];
$user = User::whereEmail('user@example.com')->first(); $user = User::whereEmail('user@example.com')->first();
$client = Client::all()->first();
if(!$user){ if(!$user){
$user = factory(\App\Models\User::class)->create([ $user = factory(\App\Models\User::class)->create([
'confirmation_code' => '123', 'confirmation_code' => '123',
'email' => 'admin@business.com',
'first_name' => 'John',
'last_name' => 'Doe',
]);
}
if(!$client) {
$client = ClientFactory::create($user->company()->id, $user->id);
$client->save();
factory(\App\Models\ClientContact::class,1)->create([
'user_id' => $user->id,
'client_id' => $client->id,
'company_id' => $company->id,
'is_primary' => 1,
'send_invoice' => true,
'email' => 'exy@example.com',
]);
factory(\App\Models\ClientContact::class,1)->create([
'user_id' => $user->id,
'client_id' => $client->id,
'company_id' => $company->id,
'send_invoice' => true,
'email' => 'exy2@example.com',
]); ]);
} }
$cc_emails = [config('ninja.testvars.test_email')]; $cc_emails = [config('ninja.testvars.test_email')];
$bcc_emails = [config('ninja.testvars.test_email')]; $bcc_emails = [config('ninja.testvars.test_email')];
Mail::to(config('ninja.testvars.test_email')) Mail::to(config('ninja.testvars.test_email'),'Mr Test')
->cc($cc_emails) ->cc($cc_emails)
->bcc($bcc_emails) ->bcc($bcc_emails)
//->replyTo(also_available_if_needed) //->replyTo(also_available_if_needed)
->send(new TemplateEmail($message, $template, $user)); ->send(new TemplateEmail($message, $template, $user, $client));
} }
} }

View File

@ -69,6 +69,7 @@ class CompanySettings extends BaseSettings
public $translations; public $translations;
public $counter_number_applied = 'when_saved'; // when_saved , when_sent , when_paid public $counter_number_applied = 'when_saved'; // when_saved , when_sent , when_paid
public $quote_number_applied = 'when_saved'; // when_saved , when_sent
/* Counters */ /* Counters */
public $invoice_number_pattern = ''; public $invoice_number_pattern = '';
public $invoice_number_counter = 1; public $invoice_number_counter = 1;
@ -222,6 +223,7 @@ class CompanySettings extends BaseSettings
'gmail_sending_user_id' => 'string', 'gmail_sending_user_id' => 'string',
'currency_id' => 'string', 'currency_id' => 'string',
'counter_number_applied' => 'string', 'counter_number_applied' => 'string',
'quote_number_applied' => 'string',
'email_subject_custom1' => 'string', 'email_subject_custom1' => 'string',
'email_subject_custom2' => 'string', 'email_subject_custom2' => 'string',
'email_subject_custom3' => 'string', 'email_subject_custom3' => 'string',

View File

@ -26,19 +26,13 @@ class InvoiceWasEmailed
*/ */
public $invoice; public $invoice;
/**
* @var string
*/
public $notes;
/** /**
* Create a new event instance. * Create a new event instance.
* *
* @param Invoice $invoice * @param Invoice $invoice
*/ */
public function __construct(Invoice $invoice, $notes) public function __construct(Invoice $invoice)
{ {
$this->invoice = $invoice; $this->invoice = $invoice;
$this->notes = $notes;
} }
} }

View File

@ -0,0 +1,45 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Events\Invoice;
use App\Models\Invoice;
use Illuminate\Queue\SerializesModels;
/**
* Class InvoiceWasEmailedAndFailed.
*/
class InvoiceWasEmailedAndFailed
{
use SerializesModels;
/**
* @var Invoice
*/
public $invoice;
/**
* @var array
*/
public $errors;
/**
* Create a new event instance.
*
* @param Invoice $invoice
*/
public function __construct(Invoice $invoice, array $errors)
{
$this->invoice = $invoice;
$this->errors = $errors;
}
}

View File

@ -25,6 +25,7 @@ use App\Http\Requests\Invoice\ShowInvoiceRequest;
use App\Http\Requests\Invoice\StoreInvoiceRequest; use App\Http\Requests\Invoice\StoreInvoiceRequest;
use App\Http\Requests\Invoice\UpdateInvoiceRequest; use App\Http\Requests\Invoice\UpdateInvoiceRequest;
use App\Jobs\Entity\ActionEntity; use App\Jobs\Entity\ActionEntity;
use App\Jobs\Invoice\EmailInvoice;
use App\Jobs\Invoice\MarkInvoicePaid; use App\Jobs\Invoice\MarkInvoicePaid;
use App\Jobs\Invoice\StoreInvoice; use App\Jobs\Invoice\StoreInvoice;
use App\Models\Invoice; use App\Models\Invoice;
@ -660,7 +661,7 @@ class InvoiceController extends BaseController
return $this->listResponse($invoice); return $this->listResponse($invoice);
break; break;
case 'email': case 'email':
EmailInvoice::dispatch($invoice);
if(!$bulk) if(!$bulk)
return response()->json(['message'=>'email sent'],200); return response()->json(['message'=>'email sent'],200);
break; break;

View File

@ -21,10 +21,11 @@
* @OA\Property(property="email_style", type="string", example="light", description="options include plain,light,dark,custom"), * @OA\Property(property="email_style", type="string", example="light", description="options include plain,light,dark,custom"),
* @OA\Property(property="reply_to_email", type="string", example="email@gmail.com", description="The reply to email address"), * @OA\Property(property="reply_to_email", type="string", example="email@gmail.com", description="The reply to email address"),
* @OA\Property(property="bcc_email", type="string", example="email@gmail.com, contact@gmail.com", description="A comma separate list of BCC emails"), * @OA\Property(property="bcc_email", type="string", example="email@gmail.com, contact@gmail.com", description="A comma separate list of BCC emails"),
*
* @OA\Property(property="pdf_email_attachment", type="boolean", example=true, description="Toggles whether to attach PDF as attachment"), * @OA\Property(property="pdf_email_attachment", type="boolean", example=true, description="Toggles whether to attach PDF as attachment"),
* @OA\Property(property="ubl_email_attachment", type="boolean", example=true, description="Toggles whether to attach UBL as attachment"), * @OA\Property(property="ubl_email_attachment", type="boolean", example=true, description="Toggles whether to attach UBL as attachment"),
* @OA\Property(property="email_style_custom", type="string", example="<HTML></HTML>", description="The custom template"), * @OA\Property(property="email_style_custom", type="string", example="<HTML></HTML>", description="The custom template"),
* @OA\Property(property="counter_number_applied", type="string", example="when_sent", description="enum when the invoice number counter is set, ie when_saved, when_sent, when_paid"),
* @OA\Property(property="quote_number_applied", type="string", example="when_sent", description="enum when the quote number counter is set, ie when_saved, when_sent"),
* @OA\Property(property="custom_message_dashboard", type="string", example="Please pay invoices immediately", description="____________"), * @OA\Property(property="custom_message_dashboard", type="string", example="Please pay invoices immediately", description="____________"),
* @OA\Property(property="custom_message_unpaid_invoice", type="string", example="Please pay invoices immediately", description="____________"), * @OA\Property(property="custom_message_unpaid_invoice", type="string", example="Please pay invoices immediately", description="____________"),
* @OA\Property(property="custom_message_paid_invoice", type="string", example="Thanks for paying this invoice!", description="____________"), * @OA\Property(property="custom_message_paid_invoice", type="string", example="Thanks for paying this invoice!", description="____________"),
@ -44,7 +45,6 @@
* @OA\Property(property="vendor_number_counter", type="integer", example="1", description="____________"), * @OA\Property(property="vendor_number_counter", type="integer", example="1", description="____________"),
* @OA\Property(property="ticket_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the ticket number pattern"), * @OA\Property(property="ticket_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the ticket number pattern"),
* @OA\Property(property="ticket_number_counter", type="integer", example="1", description="____________"), * @OA\Property(property="ticket_number_counter", type="integer", example="1", description="____________"),
*
* @OA\Property(property="payment_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the payment number pattern"), * @OA\Property(property="payment_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the payment number pattern"),
* @OA\Property(property="payment_number_counter", type="integer", example="1", description="____________"), * @OA\Property(property="payment_number_counter", type="integer", example="1", description="____________"),
* @OA\Property(property="invoice_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the invoice number pattern"), * @OA\Property(property="invoice_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the invoice number pattern"),

View File

@ -5,10 +5,10 @@
* type="object", * type="object",
* @OA\Property(property="id", type="string", example="Opnel5aKBz", description="_________"), * @OA\Property(property="id", type="string", example="Opnel5aKBz", description="_________"),
* @OA\Property(property="user_id", type="string", example="", description="__________"), * @OA\Property(property="user_id", type="string", example="", description="__________"),
* @OA\Property(property="assigned_user_id", type="string", example="", description="__________"),
* @OA\Property(property="company_id", type="string", example="", description="________"), * @OA\Property(property="company_id", type="string", example="", description="________"),
* @OA\Property(property="client_id", type="string", example="", description="________"), * @OA\Property(property="client_id", type="string", example="", description="________"),
* @OA\Property(property="status_id", type="string", example="", description="________"), * @OA\Property(property="status_id", type="string", example="", description="________"),
* @OA\Property(property="invoice_type_id", type="string", example="", description="________"),
* @OA\Property(property="number", type="string", example="INV_101", description="The invoice number - is a unique alpha numeric number per invoice per company"), * @OA\Property(property="number", type="string", example="INV_101", description="The invoice number - is a unique alpha numeric number per invoice per company"),
* @OA\Property(property="po_number", type="string", example="", description="________"), * @OA\Property(property="po_number", type="string", example="", description="________"),
* @OA\Property(property="terms", type="string", example="", description="________"), * @OA\Property(property="terms", type="string", example="", description="________"),
@ -35,6 +35,7 @@
* @OA\Property(property="is_deleted", type="boolean", example=true, description="_________"), * @OA\Property(property="is_deleted", type="boolean", example=true, description="_________"),
* @OA\Property(property="uses_inclusive_taxes", type="boolean", example=true, description="Defines the type of taxes used as either inclusive or exclusive"), * @OA\Property(property="uses_inclusive_taxes", type="boolean", example=true, description="Defines the type of taxes used as either inclusive or exclusive"),
* @OA\Property(property="date", type="string", format="date", example="1994-07-30", description="The Invoice Date"), * @OA\Property(property="date", type="string", format="date", example="1994-07-30", description="The Invoice Date"),
* @OA\Property(property="last_sent_date", type="string", format="date", example="1994-07-30", description="The last date the invoice was sent out"),
* @OA\Property(property="next_send_date", type="string", format="date", example="1994-07-30", description="The Next date for a reminder to be sent"), * @OA\Property(property="next_send_date", type="string", format="date", example="1994-07-30", description="The Next date for a reminder to be sent"),
* @OA\Property(property="partial_due_date", type="string", format="date", example="1994-07-30", description="_________"), * @OA\Property(property="partial_due_date", type="string", format="date", example="1994-07-30", description="_________"),
* @OA\Property(property="due_date", type="string", format="date", example="1994-07-30", description="_________"), * @OA\Property(property="due_date", type="string", format="date", example="1994-07-30", description="_________"),

View File

@ -3,8 +3,50 @@
* @OA\Schema( * @OA\Schema(
* schema="Quote", * schema="Quote",
* type="object", * type="object",
* @OA\Property(property="id", type="string", example="Opnel5aKBz", description="______"), * @OA\Property(property="id", type="string", example="Opnel5aKBz", description="_________"),
* @OA\Property(property="user_id", type="string", example="", description="__________"),
* @OA\Property(property="assigned_user_id", type="string", example="", description="__________"),
* @OA\Property(property="company_id", type="string", example="", description="________"),
* @OA\Property(property="client_id", type="string", example="", description="________"),
* @OA\Property(property="status_id", type="string", example="", description="________"),
* @OA\Property(property="number", type="string", example="QUOTE_101", description="The quote number - is a unique alpha numeric number per quote per company"),
* @OA\Property(property="po_number", type="string", example="", description="________"),
* @OA\Property(property="terms", type="string", example="", description="________"),
* @OA\Property(property="public_notes", type="string", example="", description="________"),
* @OA\Property(property="private_notes", type="string", example="", description="________"),
* @OA\Property(property="footer", type="string", example="", description="________"),
* @OA\Property(property="custom_value1", type="string", example="", description="________"),
* @OA\Property(property="custom_value2", type="string", example="", description="________"),
* @OA\Property(property="custom_value3", type="string", example="", description="________"),
* @OA\Property(property="custom_value4", type="string", example="", description="________"),
* @OA\Property(property="tax_name1", type="string", example="", description="________"),
* @OA\Property(property="tax_name2", type="string", example="", description="________"),
* @OA\Property(property="tax_rate1", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="tax_rate2", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="tax_name3", type="string", example="", description="________"),
* @OA\Property(property="tax_rate3", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="total_taxes", type="number", format="float", example="10.00", description="The total taxes for the quote"), * @OA\Property(property="total_taxes", type="number", format="float", example="10.00", description="The total taxes for the quote"),
* @OA\Property(property="line_items", type="object", example="", description="_________"),
* @OA\Property(property="amount", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="balance", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="discount", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="partial", type="number", format="float", example="10.00", description="_________"),
* @OA\Property(property="is_amount_discount", type="boolean", example=true, description="_________"),
* @OA\Property(property="is_deleted", type="boolean", example=true, description="_________"),
* @OA\Property(property="uses_inclusive_taxes", type="boolean", example=true, description="Defines the type of taxes used as either inclusive or exclusive"),
* @OA\Property(property="date", type="string", format="date", example="1994-07-30", description="The Quote Date"),
* @OA\Property(property="last_sent_date", type="string", format="date", example="1994-07-30", description="The last date the quote was sent out"),
* @OA\Property(property="next_send_date", type="string", format="date", example="1994-07-30", description="The Next date for a reminder to be sent"), * @OA\Property(property="next_send_date", type="string", format="date", example="1994-07-30", description="The Next date for a reminder to be sent"),
* @OA\Property(property="partial_due_date", type="string", format="date", example="1994-07-30", description="_________"),
* @OA\Property(property="due_date", type="string", format="date", example="1994-07-30", description="_________"),
* @OA\Property(property="settings",ref="#/components/schemas/CompanySettings"),
* @OA\Property(property="last_viewed", type="number", format="integer", example="1434342123", description="Timestamp"),
* @OA\Property(property="updated_at", type="number", format="integer", example="1434342123", description="Timestamp"),
* @OA\Property(property="archived_at", type="number", format="integer", example="1434342123", description="Timestamp"),
* @OA\Property(property="custom_surcharge1", type="number", format="float", example="10.00", description="First Custom Surcharge"),
* @OA\Property(property="custom_surcharge2", type="number", format="float", example="10.00", description="Second Custom Surcharge"),
* @OA\Property(property="custom_surcharge3", type="number", format="float", example="10.00", description="Third Custom Surcharge"),
* @OA\Property(property="custom_surcharge4", type="number", format="float", example="10.00", description="Fourth Custom Surcharge"),
* @OA\Property(property="custom_surcharge_taxes", type="boolean", example=true, description="Toggles charging taxes on custom surcharge amounts"),
* ) * )
*/ */

View File

@ -0,0 +1,111 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Jobs\Invoice;
use App\Events\Invoice\InvoiceWasEmailed;
use App\Events\Invoice\InvoiceWasEmailedAndFailed;
use App\Libraries\MultiDB;
use App\Mail\TemplateEmail;
use App\Models\Invoice;
use App\Models\SystemLog;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class EmailInvoice implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $invoice;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Invoice $invoice)
{
$this->invoice = $invoice;
}
/**
* Execute the job.
*
*
* @return void
*/
public function handle()
{
/*Jobs are not multi-db aware, need to set! */
MultiDB::setDB($this->invoice->company->db);
$message_array = $this->invoice->getEmailData();
$message_array['title'] = &$message_array['subject'];
$message_array['footer'] = 'The Footer';
//
$variables = array_merge($this->invoice->makeLabels(), $this->invoice->makeValues());
$template_style = $this->invoice->client->getSetting('email_style');
$this->invoice->invitations->each(function ($invitation) use($message_array, $template_style, $variables){
if($invitation->contact->send_invoice && $invitation->contact->email)
{
//there may be template variables left over for the specific contact? need to reparse here //todo this wont work, as if the variables existed, they'll be overwritten already!
$message_array['body'] = str_replace(array_keys($variables), array_values($variables), $message_array['body']);
$message_array['subject'] = str_replace(array_keys($variables), array_values($variables), $message_array['subject']);
//change the runtime config of the mail provider here:
//send message
Mail::to($invitation->contact->email)
->send(new TemplateEmail($message_array, $template_style, $invitation->contact->user, $invitation->contact->client));
if( count(Mail::failures()) > 0 ) {
event(new InvoiceWasEmailedAndFailed($this->invoice, Mail::failures()));
return $this->logMailError($errors);
}
//fire any events
event(new InvoiceWasEmailed($this->invoice));
sleep(5);
}
});
}
private function logMailError($errors)
{
SystemLogger::dispatch(
$errors,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_SEND,
SystemLog::TYPE_FAILURE,
$this->invoice->client
);
}
}

View File

@ -137,7 +137,8 @@ class UpdateInvoicePayment implements ShouldQueue
'invoices' => $invoices, 'invoices' => $invoices,
'invoices_total' => $invoices_total, 'invoices_total' => $invoices_total,
'payment_amount' => $this->payment->amount, 'payment_amount' => $this->payment->amount,
'partial_check_amount' => $total, ], 'partial_check_amount' => $total,
],
SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_PAYMENT_RECONCILIATION_FAILURE, SystemLog::EVENT_PAYMENT_RECONCILIATION_FAILURE,
SystemLog::TYPE_LEDGER, SystemLog::TYPE_LEDGER,

View File

@ -0,0 +1,81 @@
<?php
/**
* Quote Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Quote Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Jobs\Quote;
use App\Models\Quote;
use App\Models\Payment;
use App\Models\PaymentTerm;
use App\Repositories\QuoteRepository;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\NumberFormatter;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
class ApplyQuoteNumber implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, NumberFormatter, GeneratesCounter;
private $quote;
private $settings;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Quote $quote, $settings)
{
$this->quote = $quote;
$this->settings = $settings;
}
/**
* Execute the job.
*
*
* @return void
*/
public function handle()
{
//return early
if($this->quote->number != '')
return $this->quote;
switch ($this->settings->quote_number_applied) {
case 'when_saved':
$this->quote->number = $this->getNextQuoteNumber($this->quote->client);
break;
case 'when_sent':
if($this->quote->status_id == Quote::STATUS_SENT)
$this->quote->number = $this->getNextQuoteNumber($this->quote->client);
break;
default:
# code...
break;
}
$this->quote->save();
return $this->quote;
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Listeners\Invoice;
use App\Models\Activity;
use App\Models\ClientContact;
use App\Models\InvoiceInvitation;
use App\Repositories\ActivityRepository;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
class InvoiceEmailActivity implements ShouldQueue
{
protected $activity_repo;
/**
* Create the event listener.
*
* @return void
*/
public function __construct(ActivityRepository $activity_repo)
{
$this->activity_repo = $activity_repo;
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
$fields = new \stdClass;
$fields->invoice_id = $event->invoice->id;
$fields->user_id = $event->invoice->user_id;
$fields->company_id = $event->invoice->company_id;
$fields->activity_type_id = Activity::EMAIL_INVOICE;
$this->activity_repo->save($fields, $event->invoice);
}
}

View File

@ -0,0 +1,54 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Listeners\Invoice;
use App\Models\Activity;
use App\Models\ClientContact;
use App\Models\InvoiceInvitation;
use App\Repositories\ActivityRepository;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
class InvoiceEmailFailedActivity implements ShouldQueue
{
protected $activity_repo;
/**
* Create the event listener.
*
* @return void
*/
public function __construct(ActivityRepository $activity_repo)
{
$this->activity_repo = $activity_repo;
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
$fields = new \stdClass;
$fields->invoice_id = $event->invoice->id;
$fields->user_id = $event->invoice->user_id;
$fields->company_id = $event->invoice->company_id;
$fields->activity_type_id = Activity::EMAIL_INVOICE_FAILED;
$this->activity_repo->save($fields, $event->invoice);
}
}

View File

@ -14,7 +14,7 @@ class TemplateEmail extends Mailable
private $template; //the template to use private $template; //the template to use
private $message; //the message array (subject and body) private $message; //the message array // ['body', 'footer', 'title', 'files']
private $user; //the user the email will be sent from private $user; //the user the email will be sent from
@ -45,7 +45,7 @@ class TemplateEmail extends Mailable
$company = $this->client->company; $company = $this->client->company;
return $this->from($this->user->email, $this->user->present()->name()) //todo this needs to be fixed to handle the hosted version $message = $this->from($this->user->email, $this->user->present()->name()) //todo this needs to be fixed to handle the hosted version
->subject($this->message['subject']) ->subject($this->message['subject'])
->text('email.template.plain', ['body' => $this->message['body'], 'footer' => $this->message['footer']]) ->text('email.template.plain', ['body' => $this->message['body'], 'footer' => $this->message['footer']])
->view($template_name, [ ->view($template_name, [
@ -56,5 +56,14 @@ class TemplateEmail extends Mailable
'company' => $company 'company' => $company
]); ]);
//conditionally attach files
if($settings->pdf_email_attachment !== false && array_key_exists($this->message['files'])){
foreach($this->message['files'] as $file)
$message->attach($file);
}
return $message;
} }
} }

View File

@ -69,6 +69,7 @@ class Activity extends StaticModel
const RESTORE_USER=52; const RESTORE_USER=52;
const MARK_SENT_INVOICE=53; const MARK_SENT_INVOICE=53;
const PAID_INVOICE=54; const PAID_INVOICE=54;
const EMAIL_INVOICE_FAILED=57;
protected $casts = [ protected $casts = [
'is_system' => 'boolean', 'is_system' => 'boolean',

View File

@ -254,7 +254,7 @@ class Client extends BaseModel
if($this->group_settings && (property_exists($this->group_settings->settings, $setting) !== false) && (isset($this->group_settings->settings->{$setting}) !== false)){ if($this->group_settings && (property_exists($this->group_settings->settings, $setting) !== false) && (isset($this->group_settings->settings->{$setting}) !== false)){
return $this->group_settings->settings->{$setting}; return $this->group_settings->settings->{$setting};
} }
/*Company Settings*/ /*Company Settings*/
if((property_exists($this->company->settings, $setting) != false ) && (isset($this->company->settings->{$setting}) !== false) ){ if((property_exists($this->company->settings, $setting) != false ) && (isset($this->company->settings->{$setting}) !== false) ){
return $this->company->settings->{$setting}; return $this->company->settings->{$setting};

View File

@ -17,6 +17,7 @@ class SystemLog extends Model
{ {
/* Category IDs */ /* Category IDs */
const CATEGORY_GATEWAY_RESPONSE = 1; const CATEGORY_GATEWAY_RESPONSE = 1;
const CATEGORY_MAIL = 2;
/* Event IDs*/ /* Event IDs*/
const EVENT_PAYMENT_RECONCILIATION_FAILURE = 10; const EVENT_PAYMENT_RECONCILIATION_FAILURE = 10;
@ -26,10 +27,13 @@ class SystemLog extends Model
const EVENT_GATEWAY_FAILURE = 22; const EVENT_GATEWAY_FAILURE = 22;
const EVENT_GATEWAY_ERROR = 23; const EVENT_GATEWAY_ERROR = 23;
const EVENT_MAIL_SEND = 30;
/*Type IDs*/ /*Type IDs*/
const TYPE_PAYPAL = 300; const TYPE_PAYPAL = 300;
const TYPE_STRIPE = 301; const TYPE_STRIPE = 301;
const TYPE_LEDGER = 302; const TYPE_LEDGER = 302;
const TYPE_FAILURE = 303;
protected $fillable = [ protected $fillable = [
'client_id', 'client_id',

View File

@ -199,6 +199,11 @@ class User extends Authenticatable implements MustVerifyEmail
} }
public function clients()
{
return $this->hasMany(Client::class);
}
/** /**
* Returns a comma separated list of user permissions * Returns a comma separated list of user permissions
* *

View File

@ -14,6 +14,7 @@ namespace App\Providers;
use App\Events\Client\ClientWasCreated; use App\Events\Client\ClientWasCreated;
use App\Events\Contact\ContactLoggedIn; use App\Events\Contact\ContactLoggedIn;
use App\Events\Invoice\InvoiceWasCreated; use App\Events\Invoice\InvoiceWasCreated;
use App\Events\Invoice\InvoiceWasEmailed;
use App\Events\Invoice\InvoiceWasMarkedSent; use App\Events\Invoice\InvoiceWasMarkedSent;
use App\Events\Invoice\InvoiceWasPaid; use App\Events\Invoice\InvoiceWasPaid;
use App\Events\Invoice\InvoiceWasUpdated; use App\Events\Invoice\InvoiceWasUpdated;
@ -29,6 +30,8 @@ use App\Listeners\Invoice\CreateInvoiceActivity;
use App\Listeners\Invoice\CreateInvoiceHtmlBackup; use App\Listeners\Invoice\CreateInvoiceHtmlBackup;
use App\Listeners\Invoice\CreateInvoiceInvitation; use App\Listeners\Invoice\CreateInvoiceInvitation;
use App\Listeners\Invoice\CreateInvoicePdf; use App\Listeners\Invoice\CreateInvoicePdf;
use App\Listeners\Invoice\InvoiceEmailActivity;
use App\Listeners\Invoice\InvoiceEmailFailedActivity;
use App\Listeners\Invoice\UpdateInvoiceActivity; use App\Listeners\Invoice\UpdateInvoiceActivity;
use App\Listeners\Invoice\UpdateInvoiceInvitations; use App\Listeners\Invoice\UpdateInvoiceInvitations;
use App\Listeners\Invoice\UpdateInvoicePayment; use App\Listeners\Invoice\UpdateInvoicePayment;
@ -96,7 +99,14 @@ class EventServiceProvider extends ServiceProvider
], ],
InvoiceWasPaid::class => [ InvoiceWasPaid::class => [
CreateInvoiceHtmlBackup::class, CreateInvoiceHtmlBackup::class,
] ],
InvoiceWasEmailed::class => [
InvoiceEmailActivity::class,
],
InvoiceWasEmailedAndFailed::class => [
InvoiceEmailFailedActivity::class,
],
]; ];
/** /**

View File

@ -13,6 +13,7 @@ namespace App\Repositories;
use App\Factory\QuoteInvitationFactory; use App\Factory\QuoteInvitationFactory;
use App\Helpers\Invoice\InvoiceSum; use App\Helpers\Invoice\InvoiceSum;
use App\Jobs\Quote\ApplyQuoteNumber;
use App\Jobs\Quote\CreateQuoteInvitations; use App\Jobs\Quote\CreateQuoteInvitations;
use App\Models\Client; use App\Models\Client;
use App\Models\ClientContact; use App\Models\ClientContact;
@ -99,8 +100,8 @@ class QuoteRepository extends BaseRepository
$quote->save(); $quote->save();
$finished_amount = $quote->amount; $finished_amount = $quote->amount;
//todo need answers on this
// $quote = ApplyInvoiceNumber::dispatchNow($quote, $quote->client->getMergedSettings()); $quote = ApplyQuoteNumber::dispatchNow($quote, $quote->client->getMergedSettings());
return $quote->fresh(); return $quote->fresh();
} }

View File

@ -96,6 +96,7 @@ class InvoiceTransformer extends EntityTransformer
'discount' => (float) $invoice->discount, 'discount' => (float) $invoice->discount,
'po_number' => $invoice->po_number ?: '', 'po_number' => $invoice->po_number ?: '',
'date' => $invoice->date ?: '', 'date' => $invoice->date ?: '',
'last_sent_date' => $invoice->last_sent_date ?: '',
'next_send_date' => $invoice->date ?: '', 'next_send_date' => $invoice->date ?: '',
'due_date' => $invoice->due_date ?: '', 'due_date' => $invoice->due_date ?: '',
'terms' => $invoice->terms ?: '', 'terms' => $invoice->terms ?: '',
@ -103,7 +104,6 @@ class InvoiceTransformer extends EntityTransformer
'private_notes' => $invoice->private_notes ?: '', 'private_notes' => $invoice->private_notes ?: '',
'is_deleted' => (bool) $invoice->is_deleted, 'is_deleted' => (bool) $invoice->is_deleted,
'uses_inclusive_taxes' => (bool) $invoice->uses_inclusive_taxes, 'uses_inclusive_taxes' => (bool) $invoice->uses_inclusive_taxes,
'invoice_type_id' => (string) $invoice->invoice_type_id ?: '',
'tax_name1' => $invoice->tax_name1 ? $invoice->tax_name1 : '', 'tax_name1' => $invoice->tax_name1 ? $invoice->tax_name1 : '',
'tax_rate1' => (float) $invoice->tax_rate1, 'tax_rate1' => (float) $invoice->tax_rate1,
'tax_name2' => $invoice->tax_name2 ? $invoice->tax_name2 : '', 'tax_name2' => $invoice->tax_name2 ? $invoice->tax_name2 : '',

View File

@ -85,6 +85,7 @@ class QuoteTransformer extends EntityTransformer
'discount' => (float) $quote->discount, 'discount' => (float) $quote->discount,
'po_number' => $quote->po_number ?: '', 'po_number' => $quote->po_number ?: '',
'date' => $quote->date ?: '', 'date' => $quote->date ?: '',
'last_sent_date' => $quote->last_sent_date ?: '',
'next_send_date' => $quote->date ?: '', 'next_send_date' => $quote->date ?: '',
'due_date' => $quote->due_date ?: '', 'due_date' => $quote->due_date ?: '',
'terms' => $quote->terms ?: '', 'terms' => $quote->terms ?: '',
@ -92,7 +93,6 @@ class QuoteTransformer extends EntityTransformer
'private_notes' => $quote->private_notes ?: '', 'private_notes' => $quote->private_notes ?: '',
'is_deleted' => (bool) $quote->is_deleted, 'is_deleted' => (bool) $quote->is_deleted,
'uses_inclusive_taxes' => (bool) $quote->uses_inclusive_taxes, 'uses_inclusive_taxes' => (bool) $quote->uses_inclusive_taxes,
'invoice_type_id' => (string) $quote->invoice_type_id ?: '',
'tax_name1' => $quote->tax_name1 ? $quote->tax_name1 : '', 'tax_name1' => $quote->tax_name1 ? $quote->tax_name1 : '',
'tax_rate1' => (float) $quote->tax_rate1, 'tax_rate1' => (float) $quote->tax_rate1,
'tax_name2' => $quote->tax_name2 ? $quote->tax_name2 : '', 'tax_name2' => $quote->tax_name2 ? $quote->tax_name2 : '',

View File

@ -96,9 +96,44 @@ trait GeneratesCounter
return $credit_number; return $credit_number;
} }
public function getNextQuoteNumber() public function getNextQuoteNumber(Client $client)
{ {
//Reset counters if enabled
$this->resetCounters($client);
$used_counter = 'quote_number_counter';
if($this->hasSharedCounter($client))
$used_counter = 'invoice_number_counter';
//todo handle if we have specific client patterns in the future
$pattern = $client->getSetting('quote_number_pattern');
//Determine if we are using client_counters
if(strpos($pattern, 'clientCounter'))
{
$counter = $client->settings->{$used_counter};
$counter_entity = $client;
}
elseif(strpos($pattern, 'groupCounter'))
{
$counter = $client->group_settings->{$used_counter};
$counter_entity = $client->group_settings;
}
else
{
$counter = $client->company->settings->{$used_counter};
$counter_entity = $client->company;
}
//Return a valid counter
$pattern = $client->getSetting('quote_number_pattern');
$padding = $client->getSetting('counter_padding');
$quote_number = $this->checkEntityNumber(Quote::class, $client, $counter, $padding, $pattern);
$this->incrementCounter($counter_entity, $used_counter);
return $quote_number;
} }
public function getNextRecurringInvoiceNumber() public function getNextRecurringInvoiceNumber()
@ -170,9 +205,10 @@ trait GeneratesCounter
* @return boolean True if has shared counter, False otherwise. * @return boolean True if has shared counter, False otherwise.
*/ */
public function hasSharedCounter(Client $client) : bool public function hasSharedCounter(Client $client) : bool
{ {
// \Log::error((bool) $client->getSetting('shared_invoice_quote_counter'));
return $client->getSetting('shared_invoice_quote_counter') === TRUE; // \Log::error($client->getSetting('shared_invoice_quote_counter'));
return (bool) $client->getSetting('shared_invoice_quote_counter');
} }

View File

@ -65,10 +65,12 @@ trait InvoiceEmailBuilder
} }
$data['body'] = $this->parseTemplate($body_template, false); $data['body'] = $this->parseTemplate($body_template, false);
$data['subject'] = $this->parseTemplate($subject_template, true); $data['subject'] = $this->parseTemplate($subject_template, true);
if($client->getSetting('pdf_email_attachment') !== false)
$data['files'][] = $this->pdf_file_path();
return $data; return $data;
} }
@ -108,8 +110,11 @@ trait InvoiceEmailBuilder
{ {
return 'template3'; return 'template3';
} }
else
return 'invoice';
//also implement endless reminders here //also implement endless reminders here
//
} }

View File

@ -59,7 +59,7 @@ return [
'stripe' => env('STRIPE_KEYS',''), 'stripe' => env('STRIPE_KEYS',''),
'paypal' => env('PAYPAL_KEYS', ''), 'paypal' => env('PAYPAL_KEYS', ''),
'travis' => env('TRAVIS', false), 'travis' => env('TRAVIS', false),
'test_email' => env('TEST_EMAIL',''), 'test_email' => env('TEST_EMAIL','test@example.com'),
], ],
'contact' => [ 'contact' => [
'email' => env('MAIL_FROM_ADDRESS'), 'email' => env('MAIL_FROM_ADDRESS'),

View File

@ -452,6 +452,8 @@ class CreateUsersTable extends Migration
$t->string('po_number')->nullable(); $t->string('po_number')->nullable();
$t->date('date')->nullable(); $t->date('date')->nullable();
$t->date('last_sent_date')->nullable();
$t->datetime('due_date')->nullable(); $t->datetime('due_date')->nullable();
$t->boolean('is_deleted')->default(false); $t->boolean('is_deleted')->default(false);
@ -660,6 +662,8 @@ class CreateUsersTable extends Migration
$t->string('po_number')->nullable(); $t->string('po_number')->nullable();
$t->date('date')->nullable(); $t->date('date')->nullable();
$t->date('last_sent_date')->nullable();
$t->datetime('due_date')->nullable(); $t->datetime('due_date')->nullable();
$t->datetime('next_send_date')->nullable(); $t->datetime('next_send_date')->nullable();

View File

@ -801,6 +801,7 @@ $LANG = array(
'activity_54' => ':user reopened ticket :ticket', 'activity_54' => ':user reopened ticket :ticket',
'activity_55' => ':contact replied ticket :ticket', 'activity_55' => ':contact replied ticket :ticket',
'activity_56' => ':user viewed ticket :ticket', 'activity_56' => ':user viewed ticket :ticket',
'activity_57' => ':invoice failed to send to :client',
'payment' => 'Payment', 'payment' => 'Payment',
'system' => 'System', 'system' => 'System',

View File

@ -55,6 +55,7 @@ class InvoiceEmailTest extends TestCase
$message_array['title'] = &$message_array['subject']; $message_array['title'] = &$message_array['subject'];
$message_array['footer'] = 'The Footer'; $message_array['footer'] = 'The Footer';
// $template_style = $this->client->getSetting('email_style'); // $template_style = $this->client->getSetting('email_style');
$template_style = 'light'; $template_style = 'light';
@ -62,24 +63,24 @@ class InvoiceEmailTest extends TestCase
$invitations = InvoiceInvitation::whereInvoiceId($this->invoice->id)->get(); $invitations = InvoiceInvitation::whereInvoiceId($this->invoice->id)->get();
$invitations->each(function($invitation) use($message_array, $template_style) { $invitations->each(function($invitation) use($message_array, $template_styles) {
$contact = ClientContact::find($invitation->client_contact_id)->first(); $contact = $invitation->contact;
if($contact->send_invoice && $contact->email) if($contact->send_invoice && $contact->email)
{ {
//there may be template variables left over for the specific contact? need to reparse here //there may be template variables left over for the specific contact? need to reparse here
//change the runtime config of the mail provider here: //change the runtime config of the mail provider here:
//send message //send message
Mail::to($contact->email) Mail::to($contact->email)
->send(new TemplateEmail($message_array, $template_style, $this->user, $this->client)); ->send(new TemplateEmail($message_array, $template_style, $this->user, $contact->client));
//fire any events //fire any events
sleep(5); sleep(5);//here to cope with mailtrap time delays
} }

View File

@ -50,6 +50,27 @@ class GeneratesCounterTest extends TestCase
$this->assertFalse($this->hasSharedCounter($this->client)); $this->assertFalse($this->hasSharedCounter($this->client));
} }
public function testHasTrueSharedCounter()
{
$settings = $this->client->getMergedSettings();
$settings->invoice_number_counter = 1;
$settings->invoice_number_pattern = '{$year}-{$counter}';
$settings->shared_invoice_quote_counter = 1;
$this->company->settings = $settings;
$this->company->save();
$this->client->settings = $settings;
$this->client->save();
$gs = $this->client->group_settings;
$gs->settings = $settings;
$gs->save();
$this->assertTrue($this->hasSharedCounter($this->client));
}
public function testInvoiceNumberValue() public function testInvoiceNumberValue()
{ {
@ -63,6 +84,19 @@ class GeneratesCounterTest extends TestCase
} }
public function testQuoteNumberValue()
{
$quote_number = $this->getNextQuoteNumber($this->client);
$this->assertEquals($quote_number, 0001);
$quote_number = $this->getNextQuoteNumber($this->client);
$this->assertEquals($quote_number, '0002');
}
public function testInvoiceNumberPattern() public function testInvoiceNumberPattern()
{ {
$settings = $this->client->company->settings; $settings = $this->client->company->settings;
@ -79,12 +113,58 @@ class GeneratesCounterTest extends TestCase
$invoice_number = $this->getNextInvoiceNumber($this->client); $invoice_number = $this->getNextInvoiceNumber($this->client);
$invoice_number2 = $this->getNextInvoiceNumber($this->client); $invoice_number2 = $this->getNextInvoiceNumber($this->client);
$this->assertEquals($invoice_number, '2019-0001'); $this->assertEquals($invoice_number, date('Y').'-0001');
$this->assertEquals($invoice_number2, '2019-0002'); $this->assertEquals($invoice_number2, date('Y').'-0002');
$this->assertEquals($this->client->company->settings->invoice_number_counter,3); $this->assertEquals($this->client->company->settings->invoice_number_counter,3);
} }
public function testQuoteNumberPattern()
{
$settings = $this->client->company->settings;
$settings->quote_number_counter = 1;
$settings->quote_number_pattern = '{$year}-{$counter}';
$this->client->company->settings = $settings;
$this->client->company->save();
$this->client->settings = $settings;
$this->client->save();
$this->client->fresh();
$quote_number = $this->getNextQuoteNumber($this->client);
$quote_number2 = $this->getNextQuoteNumber($this->client);
$this->assertEquals($quote_number, date('Y').'-0001');
$this->assertEquals($quote_number2, date('Y').'-0002');
$this->assertEquals($this->client->company->settings->quote_number_counter,3);
}
public function testQuoteNumberPatternWithSharedCounter()
{
$settings = $this->client->company->settings;
$settings->quote_number_counter = 100;
$settings->invoice_number_counter = 1000;
$settings->quote_number_pattern = '{$year}-{$counter}';
$settings->shared_invoice_quote_counter = true;
$this->client->company->settings = $settings;
$this->client->company->save();
$gs = $this->client->group_settings;
$gs->settings = $settings;
$gs->save();
$quote_number = $this->getNextQuoteNumber($this->client);
$quote_number2 = $this->getNextQuoteNumber($this->client);
$this->assertEquals($quote_number, date('Y').'-1000');
$this->assertEquals($quote_number2, date('Y').'-1001');
$this->assertEquals($this->client->company->settings->quote_number_counter,100);
}
public function testInvoiceClientNumberPattern() public function testInvoiceClientNumberPattern()
{ {
$settings = $this->company->settings; $settings = $this->company->settings;
@ -106,10 +186,10 @@ class GeneratesCounterTest extends TestCase
$invoice_number = $this->getNextClientNumber($this->client); $invoice_number = $this->getNextClientNumber($this->client);
$this->assertEquals($invoice_number, '2019-0001'); $this->assertEquals($invoice_number, date('Y').'-0001');
$invoice_number = $this->getNextClientNumber($this->client); $invoice_number = $this->getNextClientNumber($this->client);
$this->assertEquals($invoice_number, '2019-0002'); $this->assertEquals($invoice_number, date('Y').'-0002');
} }
@ -266,8 +346,8 @@ class GeneratesCounterTest extends TestCase
$this->client->setSettingsByEntity(Client::class, $settings); $this->client->setSettingsByEntity(Client::class, $settings);
$company = Company::find($this->client->company_id); $company = Company::find($this->client->company_id);
$this->assertEquals($company->settings->client_number_counter,1); $this->assertEquals($company->settings->client_number_counter,1);
$this->assertEquals($this->getNextNumber($this->client), '2019-1'); $this->assertEquals($this->getNextNumber($this->client), date('y').'-1');
$this->assertEquals($this->getNextNumber($this->client), '2019-2'); $this->assertEquals($this->getNextNumber($this->client), date('y').'-2');
$company = Company::find($this->client->company_id); $company = Company::find($this->client->company_id);
$this->assertEquals($company->settings->client_number_counter,2); $this->assertEquals($company->settings->client_number_counter,2);