Fixes for Storing Quotes (#3159)

* Return blank object for group settings

* Implement Quote Store

* Clean up Logging
This commit is contained in:
David Bomba 2019-12-18 09:40:15 +11:00 committed by GitHub
parent da49880733
commit 556b2ab1c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 309 additions and 50 deletions

View File

@ -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;

View File

@ -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([]);

View 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;
}
}

View File

@ -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);

View File

@ -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()
{
}
}

View File

@ -25,7 +25,7 @@ class CreateInvoiceInvitations implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $invoice;
private $invoice;
/**
* Create a new job instance.

View 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();
}
});
}
}

View File

@ -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();
}
}

View File

@ -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
*/

View File

@ -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)
{

View File

@ -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
{
/* Always carry forward the initial invoice amount this is important for tracking client balance changes later......*/
$starting_amount = $quote->amount;
$quote->fill($request->input());
$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']))
{
$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();
return $quote;
$quote->save();
$finished_amount = $quote->amount;
//todo need answers on this
// $quote = ApplyInvoiceNumber::dispatchNow($quote, $quote->client->getMergedSettings());
return $quote->fresh();
}
}

View File

@ -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,
];
}
}

View File

@ -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');

View File

@ -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']);
}

View File

@ -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);
}
}