mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Fixes for Storing Quotes (#3159)
* Return blank object for group settings * Implement Quote Store * Clean up Logging
This commit is contained in:
parent
da49880733
commit
556b2ab1c9
@ -47,8 +47,8 @@ class CloneInvoiceToQuoteFactory
|
||||
$quote->last_viewed = $invoice->last_viewed;
|
||||
|
||||
$quote->status_id = Quote::STATUS_DRAFT;
|
||||
$quote->quote_number = '';
|
||||
$quote->quote_date = null;
|
||||
$quote->number = '';
|
||||
$quote->date = null;
|
||||
$quote->due_date = null;
|
||||
$quote->partial_due_date = null;
|
||||
$quote->balance = $invoice->amount;
|
||||
|
@ -22,7 +22,7 @@ class QuoteFactory
|
||||
{
|
||||
$quote = new Quote();
|
||||
$quote->status_id = Quote::STATUS_DRAFT;
|
||||
$quote->quote_number = '';
|
||||
$quote->number = '';
|
||||
$quote->discount = 0;
|
||||
$quote->is_amount_discount = true;
|
||||
$quote->po_number = '';
|
||||
@ -30,8 +30,8 @@ class QuoteFactory
|
||||
$quote->terms = '';
|
||||
$quote->public_notes = '';
|
||||
$quote->private_notes = '';
|
||||
$quote->quote_date = null;
|
||||
$quote->valid_until = null;
|
||||
$quote->date = null;
|
||||
$quote->due_date = null;
|
||||
$quote->partial_due_date = null;
|
||||
$quote->is_deleted = false;
|
||||
$quote->line_items = json_encode([]);
|
||||
|
42
app/Factory/QuoteInvitationFactory.php
Normal file
42
app/Factory/QuoteInvitationFactory.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?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\Factory;
|
||||
|
||||
use App\Models\QuoteInvitation;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class QuoteInvitationFactory
|
||||
{
|
||||
|
||||
public static function create(int $company_id, int $user_id) :QuoteInvitation
|
||||
{
|
||||
$qi = new QuoteInvitation;
|
||||
$qi->company_id = $company_id;
|
||||
$qi->user_id = $user_id;
|
||||
$qi->client_contact_id = null;
|
||||
$qi->quote_id = null;
|
||||
$qi->key = Str::random(config('ninja.key_length'));
|
||||
$qi->transaction_reference = null;
|
||||
$qi->message_id = null;
|
||||
$qi->email_error = '';
|
||||
$qi->signature_base64 = '';
|
||||
$qi->signature_date = null;
|
||||
$qi->sent_date = null;
|
||||
$qi->viewed_date = null;
|
||||
$qi->opened_date = null;
|
||||
|
||||
return $qi;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -202,7 +202,7 @@ class QuoteController extends BaseController
|
||||
public function store(StoreQuoteRequest $request)
|
||||
{
|
||||
|
||||
$quote = $this->quote_repo->save($request, QuoteFactory::create(auth()->user()->company()->id, auth()->user()->id));
|
||||
$quote = $this->quote_repo->save($request->all(), QuoteFactory::create(auth()->user()->company()->id, auth()->user()->id));
|
||||
|
||||
return $this->itemResponse($quote);
|
||||
|
||||
@ -381,7 +381,7 @@ class QuoteController extends BaseController
|
||||
public function update(UpdateQuoteRequest $request, Quote $quote)
|
||||
{
|
||||
|
||||
$quote = $this->quote_repo->save(request(), $quote);
|
||||
$quote = $this->quote_repo->save($request->all(), $quote);
|
||||
|
||||
return $this->itemResponse($quote);
|
||||
|
||||
|
@ -13,9 +13,12 @@ namespace App\Http\Requests\Quote;
|
||||
|
||||
use App\Http\Requests\Request;
|
||||
use App\Models\Quote;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
|
||||
class StoreQuoteRequest extends Request
|
||||
{
|
||||
use MakesHash;
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
@ -27,26 +30,25 @@ class StoreQuoteRequest extends Request
|
||||
return auth()->user()->can('create', Quote::class);
|
||||
}
|
||||
|
||||
protected function prepareForValidation()
|
||||
{
|
||||
$input = $this->all();
|
||||
|
||||
if(isset($input['client_id']))
|
||||
$input['client_id'] = $this->decodePrimaryKey($input['client_id']);
|
||||
|
||||
$this->replace($input);
|
||||
|
||||
}
|
||||
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx',
|
||||
'client_id' => 'required|integer',
|
||||
|
||||
'client_id' => 'required',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public function sanitize()
|
||||
{
|
||||
//do post processing of Quote request here, ie. Quote_items
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ class CreateInvoiceInvitations implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $invoice;
|
||||
private $invoice;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
|
67
app/Jobs/Quote/CreateQuoteInvitations.php
Normal file
67
app/Jobs/Quote/CreateQuoteInvitations.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?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\Factory\QuoteInvitationFactory;
|
||||
use App\Models\Quote;
|
||||
use App\Models\QuoteInvitation;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Symfony\Component\Debug\Exception\FatalThrowableError;
|
||||
|
||||
class CreateQuoteInvitations implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
private $quote;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Quote $quote)
|
||||
{
|
||||
|
||||
$this->quote = $quote;
|
||||
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
|
||||
$contacts = $this->quote->client->contacts;
|
||||
|
||||
$contacts->each(function ($contact) {
|
||||
|
||||
$invitation = QuoteInvitation::whereCompanyId($this->quote->company_id)
|
||||
->whereClientContactId($contact->id)
|
||||
->whereQuoteId($this->quote->id)
|
||||
->first();
|
||||
|
||||
if(!$invitation && $contact->send_invoice) {
|
||||
$ii = QuoteInvitationFactory::create($this->quote->company_id, $this->quote->user_id);
|
||||
$ii->quote_id = $this->quote->id;
|
||||
$ii->client_contact_id = $contact->id;
|
||||
$ii->save();
|
||||
}
|
||||
else if($invitation && !$contact->send_invoice) {
|
||||
$invitation->delete();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
}
|
@ -11,6 +11,8 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Helpers\Invoice\InvoiceSum;
|
||||
use App\Helpers\Invoice\InvoiceSumInclusive;
|
||||
use App\Models\Filterable;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@ -22,37 +24,37 @@ class Quote extends BaseModel
|
||||
use Filterable;
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'client_id',
|
||||
'quote_number',
|
||||
protected $fillable = [
|
||||
'number',
|
||||
'discount',
|
||||
'is_amount_discount',
|
||||
'po_number',
|
||||
'quote_date',
|
||||
'valid_until',
|
||||
'line_items',
|
||||
'settings',
|
||||
'footer',
|
||||
'public_note',
|
||||
'private_notes',
|
||||
'date',
|
||||
'due_date',
|
||||
'terms',
|
||||
'public_notes',
|
||||
'private_notes',
|
||||
'invoice_type_id',
|
||||
'tax_name1',
|
||||
'tax_name2',
|
||||
'tax_name3',
|
||||
'tax_rate1',
|
||||
'tax_name2',
|
||||
'tax_rate2',
|
||||
'tax_name3',
|
||||
'tax_rate3',
|
||||
'is_amount_discount',
|
||||
'footer',
|
||||
'partial',
|
||||
'partial_due_date',
|
||||
'custom_value1',
|
||||
'custom_value2',
|
||||
'custom_value3',
|
||||
'custom_value4',
|
||||
'amount',
|
||||
'partial',
|
||||
'partial_due_date',
|
||||
];
|
||||
'line_items',
|
||||
'client_id',
|
||||
'footer',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'settings' => 'object',
|
||||
'line_items' => 'object',
|
||||
'updated_at' => 'timestamp',
|
||||
'created_at' => 'timestamp',
|
||||
'deleted_at' => 'timestamp',
|
||||
@ -94,4 +96,22 @@ class Quote extends BaseModel
|
||||
return $this->morphMany(Document::class, 'documentable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the quote calculator object
|
||||
*
|
||||
* @return object The quote calculator object getters
|
||||
*/
|
||||
public function calc()
|
||||
{
|
||||
$quote_calc = null;
|
||||
|
||||
if($this->uses_inclusive_taxes)
|
||||
$quote_calc = new InvoiceSumInclusive($this);
|
||||
else
|
||||
$quote_calc = new InvoiceSum($this);
|
||||
|
||||
return $quote_calc->build();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Quote;
|
||||
use App\Utils\Traits\MakesDates;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
@ -19,6 +20,17 @@ class QuoteInvitation extends BaseModel
|
||||
|
||||
use MakesDates;
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'client_contact_id',
|
||||
];
|
||||
|
||||
public function entityType()
|
||||
{
|
||||
return Quote::class;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
|
@ -91,7 +91,10 @@ class InvoiceRepository extends BaseRepository
|
||||
|
||||
foreach($data['invitations'] as $invitation)
|
||||
{
|
||||
$inv = InvoiceInvitation::whereKey($invitation['key'])->first();
|
||||
$inv = false;
|
||||
|
||||
if(array_key_exists ('key', $invitation))
|
||||
$inv = InvoiceInvitation::whereKey($invitation['key'])->first();
|
||||
|
||||
if(!$inv)
|
||||
{
|
||||
|
@ -11,9 +11,13 @@
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Factory\QuoteInvitationFactory;
|
||||
use App\Helpers\Invoice\InvoiceSum;
|
||||
use App\Jobs\Quote\CreateQuoteInvitations;
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientContact;
|
||||
use App\Models\Quote;
|
||||
use App\Models\QuoteInvitation;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
@ -28,21 +32,77 @@ class QuoteRepository extends BaseRepository
|
||||
return Quote::class;
|
||||
}
|
||||
|
||||
public function save(Request $request, Quote $quote) : ?Quote
|
||||
public function save($data, Quote $quote) : ?Quote
|
||||
{
|
||||
|
||||
$quote->fill($request->input());
|
||||
/* Always carry forward the initial invoice amount this is important for tracking client balance changes later......*/
|
||||
$starting_amount = $quote->amount;
|
||||
|
||||
$quote->fill($data);
|
||||
|
||||
$quote->save();
|
||||
|
||||
$invoice_calc = new InvoiceSum($quote);
|
||||
if(isset($data['client_contacts']))
|
||||
{
|
||||
foreach($data['client_contacts'] as $contact)
|
||||
{
|
||||
if($contact['send_invoice'] == 1)
|
||||
{
|
||||
$client_contact = ClientContact::find($this->decodePrimaryKey($contact['id']));
|
||||
$client_contact->send_invoice = true;
|
||||
$client_contact->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$quote = $invoice_calc->build()->getInvoice();
|
||||
|
||||
//fire events here that cascading from the saving of an invoice
|
||||
//ie. client balance update...
|
||||
if(isset($data['invitations']))
|
||||
{
|
||||
|
||||
return $quote;
|
||||
$invitations = collect($data['invitations']);
|
||||
|
||||
/* Get array of Keyss which have been removed from the invitations array and soft delete each invitation */
|
||||
collect($quote->invitations->pluck('key'))->diff($invitations->pluck('key'))->each(function($invitation){
|
||||
|
||||
QuoteInvitation::destroy($invitation);
|
||||
|
||||
});
|
||||
|
||||
|
||||
foreach($data['invitations'] as $invitation)
|
||||
{
|
||||
$inv = false;
|
||||
|
||||
if(array_key_exists ('key', $invitation))
|
||||
$inv = QuoteInvitation::whereKey($invitation['key'])->first();
|
||||
|
||||
if(!$inv)
|
||||
{
|
||||
$invitation['client_contact_id'] = $this->decodePrimaryKey($invitation['client_contact_id']);
|
||||
|
||||
$new_invitation = QuoteInvitationFactory::create($quote->company_id, $quote->user_id);
|
||||
$new_invitation->fill($invitation);
|
||||
$new_invitation->quote_id = $quote->id;
|
||||
$new_invitation->save();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* If no invitations have been created, this is our fail safe to maintain state*/
|
||||
if($quote->invitations->count() == 0)
|
||||
CreateQuoteInvitations::dispatchNow($quote);
|
||||
|
||||
$quote = $quote->calc()->getInvoice();
|
||||
|
||||
$quote->save();
|
||||
|
||||
$finished_amount = $quote->amount;
|
||||
//todo need answers on this
|
||||
// $quote = ApplyInvoiceNumber::dispatchNow($quote, $quote->client->getMergedSettings());
|
||||
|
||||
return $quote->fresh();
|
||||
}
|
||||
|
||||
}
|
@ -41,7 +41,7 @@ class GroupSettingTransformer extends EntityTransformer
|
||||
return [
|
||||
'id' => $this->encodePrimaryKey($group_setting->id),
|
||||
'name' => (string)$group_setting->name ?: '',
|
||||
'settings' => $group_setting->settings ?: '',
|
||||
'settings' => $group_setting->settings ?: new \stdClass,
|
||||
];
|
||||
}
|
||||
}
|
@ -747,6 +747,38 @@ class CreateUsersTable extends Migration
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
Schema::create('quote_invitations', function ($t) {
|
||||
$t->increments('id');
|
||||
$t->unsignedInteger('company_id');
|
||||
$t->unsignedInteger('user_id');
|
||||
$t->unsignedInteger('client_contact_id');
|
||||
$t->unsignedInteger('quote_id')->index();
|
||||
$t->string('key')->index();
|
||||
$t->string('transaction_reference')->nullable();
|
||||
$t->string('message_id')->nullable();
|
||||
$t->mediumText('email_error')->nullable();
|
||||
$t->text('signature_base64')->nullable();
|
||||
$t->datetime('signature_date')->nullable();
|
||||
|
||||
$t->datetime('sent_date')->nullable();
|
||||
$t->datetime('viewed_date')->nullable();
|
||||
$t->datetime('opened_date')->nullable();
|
||||
|
||||
$t->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
$t->foreign('client_contact_id')->references('id')->on('client_contacts')->onDelete('cascade');
|
||||
$t->foreign('quote_id')->references('id')->on('quotes')->onDelete('cascade');
|
||||
$t->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
|
||||
|
||||
$t->timestamps(6);
|
||||
$t->softDeletes('deleted_at', 6);
|
||||
|
||||
$t->index(['deleted_at', 'quote_id']);
|
||||
$t->unique(['client_contact_id', 'quote_id']);
|
||||
|
||||
});
|
||||
|
||||
Schema::create('tax_rates', function ($t) {
|
||||
|
||||
$t->increments('id');
|
||||
|
@ -116,7 +116,7 @@ class ClientApiTest extends TestCase
|
||||
])->post('/api/v1/clients/bulk?action=archive', $data);
|
||||
|
||||
$arr = $response->json();
|
||||
|
||||
\Log::error($arr);
|
||||
$this->assertNotNull($arr['data'][0]['deleted_at']);
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ use App\DataMapper\ClientSettings;
|
||||
use App\DataMapper\CompanySettings;
|
||||
use App\Models\Account;
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientContact;
|
||||
use App\Models\Quote;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@ -110,7 +111,7 @@ class QuoteTest extends TestCase
|
||||
$data = [
|
||||
'first_name' => $this->faker->firstName,
|
||||
'last_name' => $this->faker->lastName,
|
||||
'name' => $this->faker->company,
|
||||
'name' => $this->faker->company,
|
||||
'email' => $this->faker->unique()->safeEmail,
|
||||
'password' => 'ALongAndBrilliantPassword123',
|
||||
'_token' => csrf_token(),
|
||||
@ -197,6 +198,26 @@ class QuoteTest extends TestCase
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$client_contact = ClientContact::whereClientId($client->id)->first();
|
||||
|
||||
$data = [
|
||||
'client_id' => $this->encodePrimaryKey($client->id),
|
||||
'date' => "2019-12-14",
|
||||
'line_items' => [],
|
||||
'invitations' => [
|
||||
['client_contact_id' => $this->encodePrimaryKey($client_contact->id)]
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $token,
|
||||
])->post('/api/v1/quotes', $data);
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user