mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2026-03-03 03:07:01 +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') {
|
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')
|
->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 VERIFACTU_CANCELLATION_SENT_FAILURE = 157;
|
||||||
|
|
||||||
|
public const QUOTE_REJECTED = 158;
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'is_system' => 'boolean',
|
'is_system' => 'boolean',
|
||||||
'updated_at' => 'timestamp',
|
'updated_at' => 'timestamp',
|
||||||
|
|||||||
@@ -213,6 +213,8 @@ class Quote extends BaseModel
|
|||||||
|
|
||||||
public const STATUS_CONVERTED = 4;
|
public const STATUS_CONVERTED = 4;
|
||||||
|
|
||||||
|
public const STATUS_REJECTED = 5;
|
||||||
|
|
||||||
public const STATUS_EXPIRED = -1;
|
public const STATUS_EXPIRED = -1;
|
||||||
|
|
||||||
public function toSearchableArray()
|
public function toSearchableArray()
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ use App\Events\Credit\CreditWasViewed;
|
|||||||
use App\Events\Invoice\InvoiceWasPaid;
|
use App\Events\Invoice\InvoiceWasPaid;
|
||||||
use App\Events\Quote\QuoteWasApproved;
|
use App\Events\Quote\QuoteWasApproved;
|
||||||
use App\Events\Quote\QuoteWasArchived;
|
use App\Events\Quote\QuoteWasArchived;
|
||||||
|
use App\Events\Quote\QuoteWasRejected;
|
||||||
use App\Events\Quote\QuoteWasRestored;
|
use App\Events\Quote\QuoteWasRestored;
|
||||||
use App\Events\Vendor\VendorWasMerged;
|
use App\Events\Vendor\VendorWasMerged;
|
||||||
use App\Listeners\LogResponseReceived;
|
use App\Listeners\LogResponseReceived;
|
||||||
@@ -162,6 +163,7 @@ use App\Listeners\Invoice\InvoicePaidActivity;
|
|||||||
use App\Listeners\Payment\PaymentNotification;
|
use App\Listeners\Payment\PaymentNotification;
|
||||||
use App\Listeners\Quote\QuoteApprovedActivity;
|
use App\Listeners\Quote\QuoteApprovedActivity;
|
||||||
use App\Listeners\Quote\QuoteArchivedActivity;
|
use App\Listeners\Quote\QuoteArchivedActivity;
|
||||||
|
use App\Listeners\Quote\QuoteRejectedActivity;
|
||||||
use App\Listeners\Quote\QuoteRestoredActivity;
|
use App\Listeners\Quote\QuoteRestoredActivity;
|
||||||
use App\Listeners\Quote\ReachWorkflowSettings;
|
use App\Listeners\Quote\ReachWorkflowSettings;
|
||||||
use App\Events\Company\CompanyDocumentsDeleted;
|
use App\Events\Company\CompanyDocumentsDeleted;
|
||||||
@@ -570,6 +572,9 @@ class EventServiceProvider extends ServiceProvider
|
|||||||
CreatedQuoteActivity::class,
|
CreatedQuoteActivity::class,
|
||||||
QuoteCreatedNotification::class,
|
QuoteCreatedNotification::class,
|
||||||
],
|
],
|
||||||
|
QuoteWasRejected::class => [
|
||||||
|
QuoteRejectedActivity::class,
|
||||||
|
],
|
||||||
QuoteWasUpdated::class => [
|
QuoteWasUpdated::class => [
|
||||||
QuoteUpdatedActivity::class,
|
QuoteUpdatedActivity::class,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5644,10 +5644,6 @@ $lang = array(
|
|||||||
'verifactu_cancellation_send_success' => 'Invoice cancellation for :invoice sent to AEAT successfully',
|
'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_cancellation_send_failure' => 'Invoice cancellation for :invoice failed to send to AEAT :notes',
|
||||||
'verifactu' => 'Verifactu',
|
'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',
|
'justify' => 'Justify',
|
||||||
'outdent' => 'Outdent',
|
'outdent' => 'Outdent',
|
||||||
'indent' => 'Indent',
|
'indent' => 'Indent',
|
||||||
@@ -5669,6 +5665,17 @@ $lang = array(
|
|||||||
'enable_e_invoice_received_notification' => 'Enable E-Invoice Received Notification',
|
'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.',
|
'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',
|
'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;
|
return $lang;
|
||||||
|
|||||||
Reference in New Issue
Block a user