diff --git a/app/Factory/CloneInvoiceToQuoteFactory.php b/app/Factory/CloneInvoiceToQuoteFactory.php index 9f9e8e293a62..f92c3328f2cf 100644 --- a/app/Factory/CloneInvoiceToQuoteFactory.php +++ b/app/Factory/CloneInvoiceToQuoteFactory.php @@ -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; diff --git a/app/Factory/QuoteFactory.php b/app/Factory/QuoteFactory.php index 21232de5271d..05e21e32a657 100644 --- a/app/Factory/QuoteFactory.php +++ b/app/Factory/QuoteFactory.php @@ -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([]); diff --git a/app/Factory/QuoteInvitationFactory.php b/app/Factory/QuoteInvitationFactory.php new file mode 100644 index 000000000000..bad3e31da873 --- /dev/null +++ b/app/Factory/QuoteInvitationFactory.php @@ -0,0 +1,42 @@ +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; + } + +} + + \ No newline at end of file diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php index 74652bd86515..0c6fdb22e61c 100644 --- a/app/Http/Controllers/QuoteController.php +++ b/app/Http/Controllers/QuoteController.php @@ -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); diff --git a/app/Http/Requests/Quote/StoreQuoteRequest.php b/app/Http/Requests/Quote/StoreQuoteRequest.php index 8bbd192d89ee..f8be7cfe5c43 100644 --- a/app/Http/Requests/Quote/StoreQuoteRequest.php +++ b/app/Http/Requests/Quote/StoreQuoteRequest.php @@ -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() - { - - } - - } diff --git a/app/Jobs/Invoice/CreateInvoiceInvitations.php b/app/Jobs/Invoice/CreateInvoiceInvitations.php index 7361f80f8820..007c2f51dc6e 100644 --- a/app/Jobs/Invoice/CreateInvoiceInvitations.php +++ b/app/Jobs/Invoice/CreateInvoiceInvitations.php @@ -25,7 +25,7 @@ class CreateInvoiceInvitations implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public $invoice; + private $invoice; /** * Create a new job instance. diff --git a/app/Jobs/Quote/CreateQuoteInvitations.php b/app/Jobs/Quote/CreateQuoteInvitations.php new file mode 100644 index 000000000000..e8870ab62d2d --- /dev/null +++ b/app/Jobs/Quote/CreateQuoteInvitations.php @@ -0,0 +1,67 @@ +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(); + } + + }); + + } +} \ No newline at end of file diff --git a/app/Models/Quote.php b/app/Models/Quote.php index b86ca5b38381..f083f21568ef 100644 --- a/app/Models/Quote.php +++ b/app/Models/Quote.php @@ -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(); + + } + } diff --git a/app/Models/QuoteInvitation.php b/app/Models/QuoteInvitation.php index b3c9a0c2f3a8..72040c4d862c 100644 --- a/app/Models/QuoteInvitation.php +++ b/app/Models/QuoteInvitation.php @@ -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 */ diff --git a/app/Repositories/InvoiceRepository.php b/app/Repositories/InvoiceRepository.php index 8e3260878451..e51bd7fbdc7c 100644 --- a/app/Repositories/InvoiceRepository.php +++ b/app/Repositories/InvoiceRepository.php @@ -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) { diff --git a/app/Repositories/QuoteRepository.php b/app/Repositories/QuoteRepository.php index 47a341995eed..68f22146c88c 100644 --- a/app/Repositories/QuoteRepository.php +++ b/app/Repositories/QuoteRepository.php @@ -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(); } } \ No newline at end of file diff --git a/app/Transformers/GroupSettingTransformer.php b/app/Transformers/GroupSettingTransformer.php index f4d98b34ae53..35107a21e316 100644 --- a/app/Transformers/GroupSettingTransformer.php +++ b/app/Transformers/GroupSettingTransformer.php @@ -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, ]; } } \ No newline at end of file diff --git a/database/migrations/2014_10_13_000000_create_users_table.php b/database/migrations/2014_10_13_000000_create_users_table.php index c16975ae6117..12622f99f5c4 100644 --- a/database/migrations/2014_10_13_000000_create_users_table.php +++ b/database/migrations/2014_10_13_000000_create_users_table.php @@ -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'); diff --git a/tests/Feature/ClientApiTest.php b/tests/Feature/ClientApiTest.php index 26519012f09d..576401809e23 100644 --- a/tests/Feature/ClientApiTest.php +++ b/tests/Feature/ClientApiTest.php @@ -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']); } diff --git a/tests/Feature/QuoteTest.php b/tests/Feature/QuoteTest.php index d6fb142eecf7..0020acb573ee 100644 --- a/tests/Feature/QuoteTest.php +++ b/tests/Feature/QuoteTest.php @@ -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); } } + +