mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2026-03-03 00:47:02 +00:00
Quote Rejection
This commit is contained in:
54
app/Events/Quote/QuoteWasRejected.php
Normal file
54
app/Events/Quote/QuoteWasRejected.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Events\Quote;
|
||||
|
||||
use App\Models\ClientContact;
|
||||
use App\Models\Company;
|
||||
use App\Models\Quote;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class QuoteWasRejected
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithSockets;
|
||||
use SerializesModels;
|
||||
|
||||
public $contact;
|
||||
|
||||
public $quote;
|
||||
|
||||
public $company;
|
||||
|
||||
public $event_vars;
|
||||
|
||||
public $notes;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param ClientContact $contact
|
||||
* @param Quote $quote
|
||||
* @param Company $company
|
||||
* @param array $event_vars
|
||||
*/
|
||||
public function __construct(ClientContact $contact, Quote $quote, Company $company, string $notes, array $event_vars)
|
||||
{
|
||||
$this->contact = $contact;
|
||||
$this->quote = $quote;
|
||||
$this->company = $company;
|
||||
$this->notes = $notes;
|
||||
$this->event_vars = $event_vars;
|
||||
}
|
||||
}
|
||||
@@ -287,9 +287,15 @@ class InvoiceFilters extends QueryFilters
|
||||
|
||||
if ($sort_col[0] == 'client_id') {
|
||||
|
||||
return $this->builder->orderByRaw('ISNULL(client_id), client_id '. $dir)
|
||||
|
||||
//2025-12-16: Better filtering for clients.
|
||||
return $this->builder->orderByRaw('client_id IS NULL')
|
||||
->orderBy(\App\Models\Client::select('name')
|
||||
->whereColumn('clients.id', 'invoices.client_id'), $dir);
|
||||
->whereColumn('clients.id', 'invoices.client_id')
|
||||
->limit(1), $dir);
|
||||
// return $this->builder->orderByRaw('ISNULL(client_id), client_id '. $dir)
|
||||
// ->orderBy(\App\Models\Client::select('name')
|
||||
// ->whereColumn('clients.id', 'invoices.client_id'), $dir);
|
||||
|
||||
}
|
||||
|
||||
|
||||
61
app/Listeners/Quote/QuoteRejectedActivity.php
Normal file
61
app/Listeners/Quote/QuoteRejectedActivity.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Listeners\Quote;
|
||||
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\Activity;
|
||||
use App\Repositories\ActivityRepository;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use stdClass;
|
||||
|
||||
class QuoteRejectedActivity implements ShouldQueue
|
||||
{
|
||||
protected $activity_repo;
|
||||
|
||||
public $delay = 5;
|
||||
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @param ActivityRepository $activity_repo
|
||||
*/
|
||||
public function __construct(ActivityRepository $activity_repo)
|
||||
{
|
||||
$this->activity_repo = $activity_repo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param object $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle($event)
|
||||
{
|
||||
MultiDB::setDb($event->company->db);
|
||||
|
||||
$fields = new stdClass();
|
||||
|
||||
$user_id = isset($event->event_vars['user_id']) ? $event->event_vars['user_id'] : $event->quote->user_id;
|
||||
|
||||
$fields->user_id = $user_id;
|
||||
$fields->quote_id = $event->quote->id;
|
||||
$fields->client_id = $event->quote->client_id;
|
||||
$fields->client_contact_id = $event->contact->id;
|
||||
$fields->company_id = $event->quote->company_id;
|
||||
$fields->activity_type_id = Activity::QUOTE_REJECTED;
|
||||
$fields->notes = $event->notes ?? '';
|
||||
|
||||
$this->activity_repo->save($fields, $event->quote, $event->event_vars);
|
||||
}
|
||||
}
|
||||
80
app/Listeners/Quote/QuoteRejectedNotification.php
Normal file
80
app/Listeners/Quote/QuoteRejectedNotification.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Quote Ninja (https://quoteninja.com).
|
||||
*
|
||||
* @link https://github.com/quoteninja/quoteninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Quote Ninja LLC (https://quoteninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Listeners\Quote;
|
||||
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Jobs\Mail\NinjaMailer;
|
||||
use App\Jobs\Mail\NinjaMailerJob;
|
||||
use App\Jobs\Mail\NinjaMailerObject;
|
||||
use App\Mail\Admin\QuoteRejectedObject;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use App\Utils\Traits\Notifications\UserNotifies;
|
||||
|
||||
class QuoteRejectedNotification implements ShouldQueue
|
||||
{
|
||||
use UserNotifies;
|
||||
|
||||
public $delay = 8;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param object $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle($event)
|
||||
{
|
||||
MultiDB::setDb($event->company->db);
|
||||
|
||||
$first_notification_sent = true;
|
||||
|
||||
$quote = $event->quote;
|
||||
|
||||
|
||||
/* We loop through each user and determine whether they need to be notified */
|
||||
foreach ($event->company->company_users as $company_user) {
|
||||
/* The User */
|
||||
$user = $company_user->user;
|
||||
|
||||
if (! $user) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Returns an array of notification methods */
|
||||
$methods = $this->findUserNotificationTypes($quote->invitations()->first(), $company_user, 'quote', ['all_notifications', 'quote_rejected', 'quote_rejected_all', 'quote_rejected_user']);
|
||||
|
||||
/* If one of the methods is email then we fire the EntitySentMailer */
|
||||
if (($key = array_search('mail', $methods)) !== false) {
|
||||
unset($methods[$key]);
|
||||
|
||||
$nmo = new NinjaMailerObject();
|
||||
$nmo->mailable = new NinjaMailer((new QuoteRejectedObject($quote, $event->company, $company_user->portalType(), $event->notes))->build());
|
||||
$nmo->company = $quote->company;
|
||||
$nmo->settings = $quote->company->settings;
|
||||
|
||||
$nmo->to_user = $user;
|
||||
|
||||
(new NinjaMailerJob($nmo))->handle();
|
||||
|
||||
$nmo = null;
|
||||
|
||||
/* This prevents more than one notification being sent */
|
||||
$first_notification_sent = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
101
app/Mail/Admin/QuoteRejectedObject.php
Normal file
101
app/Mail/Admin/QuoteRejectedObject.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Mail\Admin;
|
||||
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\Company;
|
||||
use App\Models\Quote;
|
||||
use App\Utils\Ninja;
|
||||
use App\Utils\Number;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use stdClass;
|
||||
|
||||
class QuoteRejectedObject
|
||||
{
|
||||
public function __construct(public Quote $quote, public Company $company, public bool $use_react_url, public string $notes)
|
||||
{
|
||||
}
|
||||
|
||||
public function build()
|
||||
{
|
||||
MultiDB::setDb($this->company->db);
|
||||
|
||||
if (! $this->quote) {
|
||||
return;
|
||||
}
|
||||
|
||||
App::forgetInstance('translator');
|
||||
/* Init a new copy of the translator*/
|
||||
$t = app('translator');
|
||||
/* Set the locale*/
|
||||
App::setLocale($this->company->getLocale());
|
||||
/* Set customized translations _NOW_ */
|
||||
$t->replace(Ninja::transformTranslations($this->company->settings));
|
||||
|
||||
$mail_obj = new stdClass();
|
||||
$mail_obj->amount = $this->getAmount();
|
||||
$mail_obj->subject = $this->getSubject();
|
||||
$mail_obj->data = $this->getData();
|
||||
$mail_obj->markdown = 'email.admin.generic';
|
||||
$mail_obj->tag = $this->company->company_key;
|
||||
$mail_obj->text_view = 'email.template.text';
|
||||
|
||||
return $mail_obj;
|
||||
}
|
||||
|
||||
private function getAmount()
|
||||
{
|
||||
return Number::formatMoney($this->quote->amount, $this->quote->client);
|
||||
}
|
||||
|
||||
private function getSubject()
|
||||
{
|
||||
return
|
||||
ctrans(
|
||||
'texts.notification_quote_rejected_subject',
|
||||
[
|
||||
'client' => $this->quote->client->present()->name(),
|
||||
'quote' => $this->quote->number,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function getData()
|
||||
{
|
||||
$settings = $this->quote->client->getMergedSettings();
|
||||
$content = ctrans(
|
||||
'texts.notification_quote_rejected',
|
||||
[
|
||||
'amount' => $this->getAmount(),
|
||||
'client' => $this->quote->client->present()->name(),
|
||||
'quote' => $this->quote->number,
|
||||
'notes' => $this->notes,
|
||||
]
|
||||
);
|
||||
|
||||
$data = [
|
||||
'title' => $this->getSubject(),
|
||||
'content' => $content,
|
||||
'url' => $this->quote->invitations->first()->getAdminLink($this->use_react_url),
|
||||
'button' => ctrans('texts.view_quote'),
|
||||
'signature' => $settings->email_signature,
|
||||
'logo' => $this->company->present()->logo(),
|
||||
'settings' => $settings,
|
||||
'whitelabel' => $this->company->account->isPaid() ? true : false,
|
||||
'text_body' => $content,
|
||||
'template' => $this->company->account->isPremium() ? 'email.template.admin_premium' : 'email.template.admin',
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -296,6 +296,8 @@ class Activity extends StaticModel
|
||||
|
||||
public const VERIFACTU_CANCELLATION_SENT_FAILURE = 157;
|
||||
|
||||
public const QUOTE_REJECTED = 158;
|
||||
|
||||
protected $casts = [
|
||||
'is_system' => 'boolean',
|
||||
'updated_at' => 'timestamp',
|
||||
|
||||
@@ -213,6 +213,8 @@ class Quote extends BaseModel
|
||||
|
||||
public const STATUS_CONVERTED = 4;
|
||||
|
||||
public const STATUS_REJECTED = 5;
|
||||
|
||||
public const STATUS_EXPIRED = -1;
|
||||
|
||||
public function toSearchableArray()
|
||||
|
||||
@@ -74,6 +74,7 @@ use App\Events\Credit\CreditWasViewed;
|
||||
use App\Events\Invoice\InvoiceWasPaid;
|
||||
use App\Events\Quote\QuoteWasApproved;
|
||||
use App\Events\Quote\QuoteWasArchived;
|
||||
use App\Events\Quote\QuoteWasRejected;
|
||||
use App\Events\Quote\QuoteWasRestored;
|
||||
use App\Events\Vendor\VendorWasMerged;
|
||||
use App\Listeners\LogResponseReceived;
|
||||
@@ -162,6 +163,7 @@ use App\Listeners\Invoice\InvoicePaidActivity;
|
||||
use App\Listeners\Payment\PaymentNotification;
|
||||
use App\Listeners\Quote\QuoteApprovedActivity;
|
||||
use App\Listeners\Quote\QuoteArchivedActivity;
|
||||
use App\Listeners\Quote\QuoteRejectedActivity;
|
||||
use App\Listeners\Quote\QuoteRestoredActivity;
|
||||
use App\Listeners\Quote\ReachWorkflowSettings;
|
||||
use App\Events\Company\CompanyDocumentsDeleted;
|
||||
@@ -570,6 +572,9 @@ class EventServiceProvider extends ServiceProvider
|
||||
CreatedQuoteActivity::class,
|
||||
QuoteCreatedNotification::class,
|
||||
],
|
||||
QuoteWasRejected::class => [
|
||||
QuoteRejectedActivity::class,
|
||||
],
|
||||
QuoteWasUpdated::class => [
|
||||
QuoteUpdatedActivity::class,
|
||||
],
|
||||
|
||||
@@ -5644,10 +5644,6 @@ $lang = array(
|
||||
'verifactu_cancellation_send_success' => 'Invoice cancellation for :invoice sent to AEAT successfully',
|
||||
'verifactu_cancellation_send_failure' => 'Invoice cancellation for :invoice failed to send to AEAT :notes',
|
||||
'verifactu' => 'Verifactu',
|
||||
'activity_150' => 'E-Invoice :invoice for :client sent to AEAT successfully',
|
||||
'activity_151' => 'E-Invoice :invoice for :client failed to send to AEAT :notes',
|
||||
'activity_152' => 'Invoice cancellation for :invoice sent to AEAT successfully',
|
||||
'activity_153' => 'Invoice cancellation for :invoice failed to send to AEAT :notes',
|
||||
'justify' => 'Justify',
|
||||
'outdent' => 'Outdent',
|
||||
'indent' => 'Indent',
|
||||
@@ -5669,6 +5665,17 @@ $lang = array(
|
||||
'enable_e_invoice_received_notification' => 'Enable E-Invoice Received Notification',
|
||||
'enable_e_invoice_received_notification_help' => 'Receive an email notification when a new E-Invoice is received.',
|
||||
'price_changes' => 'Plan Price Changes from January 1st 2026',
|
||||
'notification_quote_rejected_subject' => 'Quote :quote was rejected by :client',
|
||||
'notification_quote_rejected' => 'The following client :client rejected Quote :quote for :amount :notes.',
|
||||
'activity_150' => 'Account was deleted :notes',
|
||||
'activity_151' => 'Client :notes was merged into :client by :user',
|
||||
'activity_152' => 'Vendor :notes was merged into :vendor by :user',
|
||||
'activity_153' => 'Client :notes was purged by :user',
|
||||
'activity_154' => 'E-Invoice :invoice for :client sent to AEAT successfully',
|
||||
'activity_155' => 'E-Invoice :invoice for :client failed to send to AEAT :notes',
|
||||
'activity_156' => 'Invoice cancellation for :invoice sent to AEAT successfully',
|
||||
'activity_157' => 'Invoice cancellation for :invoice failed to send to AEAT :notes',
|
||||
'activity_158' => 'Quote :quote was rejected by :client :notes',
|
||||
);
|
||||
|
||||
return $lang;
|
||||
|
||||
Reference in New Issue
Block a user