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

Workflow settings for quotes & invoices (#3655)

* Fix inputs

* Workflow settings: Auto archive

- New InvoiceWorkflowSettings job
- Updated PaymentCreatedActivity to dispatch the job
- New 'Archivable' trait
- New 'archived_at' field

* Workflow settings: Sending e-mail to client with auto_send

* Workflow settings: Quotes

* Fix HTML markup

* Fix archive & sending

* Remove Archivable trait & migration file

* Change order of sending the events
This commit is contained in:
Benjamin Beganović 2020-04-28 01:50:54 +02:00 committed by GitHub
parent 3aa884dc11
commit d516b1b097
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 328 additions and 14 deletions

View File

@ -0,0 +1,29 @@
<?php
namespace App\Events\Quote;
use App\Models\Quote;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class QuoteWasApproved
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $quote;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(Quote $quote)
{
$this->quote = $quote;
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\ClientPortal;
use App\Events\Quote\QuoteWasApproved;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\ProcessQuotesInBulkRequest;
use App\Http\Requests\ClientPortal\ShowQuoteRequest;
@ -98,6 +99,7 @@ class QuoteController extends Controller
if ($process) {
foreach ($quotes as $quote) {
$quote->service()->approve()->save();
event(new QuoteWasApproved($quote));
}
return route('client.quotes.index')->withSuccess('Quote(s) approved successfully.');

View File

@ -0,0 +1,64 @@
<?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\Jobs\Invoice;
use App\Mail\Invoices\InvoiceWasPaid;
use App\Models\Client;
use App\Models\Invoice;
use App\Repositories\BaseRepository;
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 InvoiceWorkflowSettings implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $invoice;
public $client;
private $base_repository;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Invoice $invoice, Client $client = null)
{
$this->invoice = $invoice;
$this->client = $client ?? $invoice->client;
$this->base_repository = new BaseRepository();
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
if ($this->client->getSetting('auto_archive_invoice')) {
/** Throws: Payment amount xxx does not match invoice totals. */
$this->base_repository->archive($this->invoice);
}
if ($this->client->getSetting('auto_email_invoice')) {
$this->invoice->invitations->each(function ($invitation, $key) {
$this->invoice->service()->sendEmail($invitation->contact);
});
}
}
}

View File

@ -0,0 +1,62 @@
<?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\Jobs\Quote;
use App\Mail\Quote\QuoteWasApproved;
use App\Models\Quote;
use App\Models\Client;
use App\Repositories\BaseRepository;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class QuoteWorkflowSettings implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $quote;
public $client;
public $base_repository;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(Quote $quote, Client $client = null)
{
$this->quote = $quote;
$this->client = $client ?? $quote->client;
$this->base_repository = new BaseRepository();
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
if ($this->client->getSetting('auto_archive_quote')) {
$this->base_repository->archive($this->quote);
}
if ($this->client->getSetting('auto_email_quote')) {
$this->quote->invitations->each(function ($invitation, $key) {
$this->quote->service()->sendEmail($invitation->contact);
});
}
}
}

View File

@ -11,6 +11,7 @@
namespace App\Listeners\Activity;
use App\Jobs\Invoice\InvoiceWorkflowSettings;
use App\Models\Activity;
use App\Models\Invoice;
use App\Models\Payment;
@ -54,6 +55,8 @@ class PaymentCreatedActivity implements ShouldQueue
foreach ($invoices as $invoice) { //todo we may need to add additional logic if in the future we apply payments to other entity Types, not just invoices
$fields->invoice_id = $invoice->id;
InvoiceWorkflowSettings::dispatchNow($invoice);
$this->activityRepo->save($fields, $invoice);
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Listeners\Quote;
use App\Jobs\Quote\QuoteWorkflowSettings;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class ReachWorkflowSettings
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
QuoteWorkflowSettings::dispatchNow($event->quote);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Mail\Invoices;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class InvoiceWasPaid extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->view('email.invoices.paid');
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Mail\Quote;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class QuoteWasApproved extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->view('email.quotes.approved');
}
}

View File

@ -26,6 +26,7 @@ use App\Models\PaymentTerm;
use App\Services\Invoice\InvoiceService;
use App\Services\Ledger\LedgerService;
use App\Utils\Number;
use App\Utils\Traits\Archivable;
use App\Utils\Traits\InvoiceEmailBuilder;
use App\Utils\Traits\Invoice\ActionsInvoice;
use App\Utils\Traits\MakesDates;

View File

@ -18,6 +18,7 @@ use App\Jobs\Invoice\CreateInvoicePdf;
use App\Jobs\Quote\CreateQuotePdf;
use App\Models\Filterable;
use App\Services\Quote\QuoteService;
use App\Utils\Traits\Archivable;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesInvoiceValues;

View File

@ -158,11 +158,9 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver
$this->attachInvoices($payment, $request->input('hashed_ids'));
event(new PaymentWasCreated($payment, $payment->company));
$payment->service()->UpdateInvoicePayment();
//UpdateInvoicePayment::dispatchNow($payment, $payment->company);
event(new PaymentWasCreated($payment, $payment->company));
return redirect()->route('client.payments.show', ['payment'=>$this->encodePrimaryKey($payment->id)]);
}

View File

@ -344,12 +344,11 @@ class StripePaymentDriver extends BasePaymentDriver
/* Link invoices to payment*/
$this->attachInvoices($payment, $hashed_ids);
$payment->service()->UpdateInvoicePayment();
event(new PaymentWasCreated($payment, $payment->company));
$payment->service()->UpdateInvoicePayment();
//UpdateInvoicePayment::dispatchNow($payment, $payment->company);
SystemLogger::dispatch(
[
'server_response' => $payment_intent,

View File

@ -28,6 +28,7 @@ use App\Events\Payment\PaymentWasCreated;
use App\Events\Payment\PaymentWasDeleted;
use App\Events\Payment\PaymentWasRefunded;
use App\Events\Payment\PaymentWasVoided;
use App\Events\Quote\QuoteWasApproved;
use App\Events\User\UserLoggedIn;
use App\Events\User\UserWasCreated;
use App\Events\User\UserWasDeleted;
@ -50,6 +51,7 @@ use App\Listeners\Invoice\UpdateInvoiceActivity;
use App\Listeners\Invoice\UpdateInvoiceInvitations;
use App\Listeners\Misc\InvitationViewedListener;
use App\Listeners\Payment\PaymentNotification;
use App\Listeners\Quote\ReachWorkflowSettings;
use App\Listeners\SendVerificationNotification;
use App\Listeners\SetDBListener;
use App\Listeners\User\DeletedUserActivity;
@ -144,10 +146,12 @@ class EventServiceProvider extends ServiceProvider
InvitationWasViewed::class => [
InvitationViewedListener::class
],
CompanyWasDeleted::class => [
DeleteCompanyDocuments::class,
],
QuoteWasApproved::class => [
ReachWorkflowSettings::class,
],
];
/**

View File

@ -71,7 +71,7 @@ class BaseRepository
}
$entity->delete();
$className = $this->getEventClass($entity, 'Archived');
if (class_exists($className)) {

View File

@ -0,0 +1,26 @@
@component('email.template.master', ['design' => 'light'])
@slot('header')
@component('email.components.header', ['p' => 'Payment for your invoice has been completed!', 'logo' => 'https://www.invoiceninja.com/wp-content/uploads/2019/01/InvoiceNinja-Logo-Round-300x300.png'])
Invoice paid
@endcomponent
@endslot
@slot('greeting')
Hello,
@endslot
We want to inform you that payment was completed for your invoice. Amount: $10,000.00.
@component('email.components.button', ['url' => 'https://invoiceninja.com', 'show_link' => true])
Visit InvoiceNinja
@endcomponent
@slot('below_card')
@component('email.components.footer', ['url' => 'https://invoiceninja.com', 'url_text' => '&copy; InvoiceNinja'])
For any info, please visit InvoiceNinja.
@endcomponent
@endslot
@endcomponent

View File

@ -0,0 +1,26 @@
@component('email.template.master', ['design' => 'light'])
@slot('header')
@component('email.components.header', ['p' => 'Your quote was approved!', 'logo' => 'https://www.invoiceninja.com/wp-content/uploads/2019/01/InvoiceNinja-Logo-Round-300x300.png'])
Quote approved
@endcomponent
@endslot
@slot('greeting')
Hello,
@endslot
We want to inform you that quote was approved. Put nicer text here.
@component('email.components.button', ['url' => 'https://invoiceninja.com', 'show_link' => true])
Visit InvoiceNinja
@endcomponent
@slot('below_card')
@component('email.components.footer', ['url' => 'https://invoiceninja.com', 'url_text' => '&copy; InvoiceNinja'])
For any info, please visit InvoiceNinja.
@endcomponent
@endslot
@endcomponent

View File

@ -37,7 +37,7 @@
{{ ctrans('texts.name') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input" id="cardholder-name" type="text" placeholder="{{ ctrans('texts.name') }}">
<input class="input w-full" id="cardholder-name" type="text" placeholder="{{ ctrans('texts.name') }}">
</dd>
</div><div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm leading-5 font-medium text-gray-500">
@ -52,7 +52,7 @@
{{ ctrans('texts.save_as_default') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input type="checkbox" class="form-check" name="proxy_is_default"
<input type="checkbox" class="form-checkbox" name="proxy_is_default"
id="proxy_is_default"/>
</dd>
</div>

View File

@ -4,6 +4,7 @@
@push('head')
<meta name="stripe-publishable-key" content="{{ $gateway->getPublishableKey() }}">
<meta name="using-token" content="{{ boolval($token) }}">
<meta name="turbolinks-visit-control" content="reload">
@endpush
@section('header')
@ -62,7 +63,7 @@
{{ ctrans('texts.name') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input" id="cardholder-name" type="text"
<input class="input w-full" id="cardholder-name" type="text"
placeholder="{{ ctrans('texts.name') }}">
</dd>
</div>

View File

@ -31,8 +31,8 @@
@endsection
@section('body')
<div class="flex justify-between items-center">
<span>{{ ctrans('texts.with_selected') }}</span>
<div class="flex justify-end items-center">
<span class="text-sm mr-2">{{ ctrans('texts.with_selected') }}:</span>
<form action="{{ route('client.invoices.bulk') }}" method="post" id="bulkActions">
@csrf
<button type="submit" class="button button-primary" name="action" value="download">{{ ctrans('texts.download') }}</button>

View File

@ -5,6 +5,7 @@
<meta name="show-invoice-terms" content="{{ $settings->show_accept_invoice_terms ? true : false }}">
<meta name="require-invoice-signature" content="{{ $settings->require_invoice_signature ? true : false }}">
<script src="https://cdn.jsdelivr.net/npm/signature_pad@2.3.2/dist/signature_pad.min.js"></script>
<meta name="turbolinks-visit-control" content="reload">
@endpush
@section('body')