diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index a3d272a5c410..8f9dfe2408f4 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -98,11 +98,11 @@ class CompanySettings extends BaseSettings public $expense_number_pattern = ''; //@implemented public $expense_number_counter = 1; //@implemented - public $recurring_expense_number_pattern = ''; - public $recurring_expense_number_counter = 1; + public $recurring_expense_number_pattern = ''; + public $recurring_expense_number_counter = 1; - public $recurring_quote_number_pattern = ''; - public $recurring_quote_number_counter = 1; + public $recurring_quote_number_pattern = ''; + public $recurring_quote_number_counter = 1; public $vendor_number_pattern = ''; //@implemented public $vendor_number_counter = 1; //@implemented @@ -276,6 +276,9 @@ class CompanySettings extends BaseSettings public $email_from_name = ''; public $auto_archive_invoice_cancelled = false; + + public $purchase_order_number_counter = 1; //TODO + public static $casts = [ 'page_numbering_alignment' => 'string', 'page_numbering' => 'bool', @@ -474,6 +477,7 @@ class CompanySettings extends BaseSettings 'portal_custom_footer' => 'string', 'portal_custom_js' => 'string', 'client_portal_enable_uploads' => 'bool', + 'purchase_order_number_counter' => 'integer', ]; public static $free_plan_casts = [ diff --git a/app/Events/PurchaseOrder/PurchaseOrderWasMarkedSent.php b/app/Events/PurchaseOrder/PurchaseOrderWasMarkedSent.php new file mode 100644 index 000000000000..3b17142b9aab --- /dev/null +++ b/app/Events/PurchaseOrder/PurchaseOrderWasMarkedSent.php @@ -0,0 +1,38 @@ +purchase_order = $purchase_order; + $this->company = $company; + $this->event_vars = $event_vars; + } +} diff --git a/app/Factory/PurchaseOrderInvitationFactory.php b/app/Factory/PurchaseOrderInvitationFactory.php new file mode 100644 index 000000000000..ee3987ea7e39 --- /dev/null +++ b/app/Factory/PurchaseOrderInvitationFactory.php @@ -0,0 +1,31 @@ +company_id = $company_id; + $ci->user_id = $user_id; + $ci->vendor_contact_id = null; + $ci->purchase_order_id = null; + $ci->key = Str::random(config('ninja.key_length')); + $ci->transaction_reference = null; + $ci->message_id = null; + $ci->email_error = ''; + $ci->signature_base64 = ''; + $ci->signature_date = null; + $ci->sent_date = null; + $ci->viewed_date = null; + $ci->opened_date = null; + + return $ci; + } +} diff --git a/app/Http/Controllers/ClientPortal/InvitationController.php b/app/Http/Controllers/ClientPortal/InvitationController.php index 199497ccd7df..48b4a455754b 100644 --- a/app/Http/Controllers/ClientPortal/InvitationController.php +++ b/app/Http/Controllers/ClientPortal/InvitationController.php @@ -22,6 +22,7 @@ use App\Models\ClientContact; use App\Models\CreditInvitation; use App\Models\InvoiceInvitation; use App\Models\Payment; +use App\Models\PurchaseOrderInvitation; use App\Models\QuoteInvitation; use App\Services\ClientPortal\InstantPayment; use App\Utils\CurlUtils; @@ -41,7 +42,7 @@ class InvitationController extends Controller use MakesDates; public function router(string $entity, string $invitation_key) - { + { Auth::logout(); return $this->genericRouter($entity, $invitation_key); @@ -166,7 +167,7 @@ class InvitationController extends Controller { set_time_limit(45); - + if(Ninja::isHosted()) return $this->returnRawPdf($entity, $invitation_key); @@ -202,7 +203,7 @@ class InvitationController extends Controller return response()->streamDownload(function () use($file) { echo $file; }, $file_name, $headers); - + } public function routerForIframe(string $entity, string $client_hash, string $invitation_key) @@ -228,14 +229,14 @@ class InvitationController extends Controller $invitation = InvoiceInvitation::where('key', $invitation_key) ->with('contact.client') ->firstOrFail(); - + auth()->guard('contact')->loginUsingId($invitation->contact->id, true); $invoice = $invitation->invoice; if($invoice->partial > 0) $amount = round($invoice->partial, (int)$invoice->client->currency()->precision); - else + else $amount = round($invoice->balance, (int)$invoice->client->currency()->precision); $gateways = $invitation->contact->client->service()->getPaymentMethods($amount); @@ -279,6 +280,10 @@ class InvitationController extends Controller $invite = CreditInvitation::withTrashed()->where('key', $invitation_key)->first(); $invite->contact->send_email = false; $invite->contact->save(); + }elseif($entity == 'purchase_order'){ + $invite = PurchaseOrderInvitation::withTrashed()->where('key', $invitation_key)->first(); + $invite->contact->send_email = false; + $invite->contact->save(); } else return abort(404); diff --git a/app/Models/PurchaseOrder.php b/app/Models/PurchaseOrder.php index 4d73c609559d..3a82bb9215f8 100644 --- a/app/Models/PurchaseOrder.php +++ b/app/Models/PurchaseOrder.php @@ -12,8 +12,12 @@ namespace App\Models; +use App\Jobs\Entity\CreateEntityPdf; use App\Services\PurchaseOrder\PurchaseOrderService; +use App\Utils\Ninja; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Storage; class PurchaseOrder extends BaseModel { @@ -129,11 +133,52 @@ class PurchaseOrder extends BaseModel { return $this->belongsTo(Client::class)->withTrashed(); } + public function markInvitationsSent() + { + $this->invitations->each(function ($invitation) { + if (! isset($invitation->sent_date)) { + $invitation->sent_date = Carbon::now(); + $invitation->save(); + } + }); + } + public function pdf_file_path($invitation = null, string $type = 'path', bool $portal = false) + { + if (! $invitation) { + + if($this->invitations()->exists()) + $invitation = $this->invitations()->first(); + else{ + $this->service()->createInvitations(); + $invitation = $this->invitations()->first(); + } + + } + + if(!$invitation) + throw new \Exception('Hard fail, could not create an invitation - is there a valid contact?'); + + $file_path = $this->client->credit_filepath($invitation).$this->numberFormatter().'.pdf'; + + if(Ninja::isHosted() && $portal && Storage::disk(config('filesystems.default'))->exists($file_path)){ + return Storage::disk(config('filesystems.default'))->{$type}($file_path); + } + elseif(Ninja::isHosted() && $portal){ + $file_path = CreateEntityPdf::dispatchNow($invitation,config('filesystems.default')); + return Storage::disk(config('filesystems.default'))->{$type}($file_path); + } + + if(Storage::disk('public')->exists($file_path)) + return Storage::disk('public')->{$type}($file_path); + + $file_path = CreateEntityPdf::dispatchNow($invitation); + return Storage::disk('public')->{$type}($file_path); + } public function invitations() { - return $this->hasMany(CreditInvitation::class); + return $this->hasMany(PurchaseOrderInvitation::class); } public function project() diff --git a/app/Models/PurchaseOrderInvitation.php b/app/Models/PurchaseOrderInvitation.php new file mode 100644 index 000000000000..c0601653e559 --- /dev/null +++ b/app/Models/PurchaseOrderInvitation.php @@ -0,0 +1,81 @@ +belongsTo(PurchaseOrder::class)->withTrashed(); + } + + /** + * @return mixed + */ + public function contact() + { + return $this->belongsTo(VendorContact::class, 'vendor_contact_id', 'id')->withTrashed(); + } + + /** + * @return mixed + */ + public function user() + { + return $this->belongsTo(User::class)->withTrashed(); + } + + + public function company() + { + return $this->belongsTo(Company::class); + } + + public function getName() + { + return $this->key; + } + + public function markViewed() + { + $this->viewed_date = Carbon::now(); + $this->save(); + } + + +} diff --git a/app/Models/VendorContact.php b/app/Models/VendorContact.php index 5c2e373c8311..2b2fcd3f1252 100644 --- a/app/Models/VendorContact.php +++ b/app/Models/VendorContact.php @@ -136,4 +136,8 @@ class VendorContact extends Authenticatable implements HasLocalePreference ->withTrashed() ->where('id', $this->decodePrimaryKey($value))->firstOrFail(); } + public function purchase_order_invitations(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(PurchaseOrderInvitation::class); + } } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 57afa3f7e5fe..d2b7e232acfa 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -60,6 +60,7 @@ use App\Events\Payment\PaymentWasRefunded; use App\Events\Payment\PaymentWasRestored; use App\Events\Payment\PaymentWasUpdated; use App\Events\Payment\PaymentWasVoided; +use App\Events\PurchaseOrder\PurchaseOrderWasMarkedSent; use App\Events\Quote\QuoteWasApproved; use App\Events\Quote\QuoteWasArchived; use App\Events\Quote\QuoteWasCreated; @@ -558,6 +559,8 @@ class EventServiceProvider extends ServiceProvider VendorWasUpdated::class => [ VendorUpdatedActivity::class, ], + PurchaseOrderWasMarkedSent::class => [ + ], ]; diff --git a/app/Repositories/PurchaseOrderRepository.php b/app/Repositories/PurchaseOrderRepository.php index cf0653c25c8a..d620fdcdd142 100644 --- a/app/Repositories/PurchaseOrderRepository.php +++ b/app/Repositories/PurchaseOrderRepository.php @@ -13,6 +13,7 @@ namespace App\Repositories; use App\Models\PurchaseOrder; +use App\Models\PurchaseOrderInvitation; use App\Utils\Traits\MakesHash; class PurchaseOrderRepository extends BaseRepository @@ -30,5 +31,9 @@ class PurchaseOrderRepository extends BaseRepository return $purchase_order; } + public function getInvitationByKey($key) :?PurchaseOrderInvitation + { + return PurchaseOrderInvitation::where('key', $key)->first(); + } } diff --git a/app/Services/PurchaseOrder/ApplyNumber.php b/app/Services/PurchaseOrder/ApplyNumber.php new file mode 100644 index 000000000000..c3fd4d5df078 --- /dev/null +++ b/app/Services/PurchaseOrder/ApplyNumber.php @@ -0,0 +1,59 @@ +client = $client; + + $this->purchase_order = $purchase_order; + } + + public function run() + { + if ($this->purchase_order->number != '') { + return $this->purchase_order; + } + + $this->trySaving(); + + return $this->purchase_order; + } + private function trySaving() + { + $x=1; + do{ + try{ + $this->purchase_order->number = $this->getNextPurchaseOrderNumber($this->client, $this->purchase_order); + $this->purchase_order->saveQuietly(); + $this->completed = false; + } + catch(QueryException $e){ + $x++; + if($x>10) + $this->completed = false; + } + } + while($this->completed); + + } +} diff --git a/app/Services/PurchaseOrder/CreateInvitations.php b/app/Services/PurchaseOrder/CreateInvitations.php new file mode 100644 index 000000000000..4ae8f85323e6 --- /dev/null +++ b/app/Services/PurchaseOrder/CreateInvitations.php @@ -0,0 +1,91 @@ +purchase_order = $purchase_order; + } + private function createBlankContact() + { + $new_contact = PurchaseOrderInvitationFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id); + $new_contact->client_id = $this->purchase_order->client_id; + $new_contact->contact_key = Str::random(40); + $new_contact->is_primary = true; + $new_contact->save(); + } + public function run() + { + $contacts = $this->purchase_order->vendor->contacts; + + if($contacts->count() == 0){ + $this->createBlankContact(); + + $this->purchase_order->refresh(); + $contacts = $this->purchase_order->vendor->contacts; + } + + $contacts->each(function ($contact) { + $invitation = PurchaseOrderInvitation::whereCompanyId($this->purchase_order->company_id) + ->whereClientContactId($contact->id) + ->whereCreditId($this->purchase_order->id) + ->withTrashed() + ->first(); + + if (! $invitation) { + $ii = PurchaseOrderInvitation::create($this->purchase_order->company_id, $this->purchase_order->user_id); + $ii->key = $this->createDbHash($this->purchase_order->company->db); + $ii->purchase_order_id = $this->purchase_order->id; + $ii->vendor_contact_id = $contact->id; + $ii->save(); + } elseif (! $contact->send_email) { + $invitation->delete(); + } + }); + + if($this->purchase_order->invitations()->count() == 0) { + + if($contacts->count() == 0){ + $contact = $this->createBlankContact(); + } + else{ + $contact = $contacts->first(); + + $invitation = PurchaseOrder::where('company_id', $this->purchase_order->company_id) + ->where('vendor_contact_id', $contact->id) + ->where('purchase_order_id', $this->purchase_order->id) + ->withTrashed() + ->first(); + + if($invitation){ + $invitation->restore(); + return $this->purchase_order; + } + } + + $ii = PurchaseOrderInvitation::create($this->purchase_order->company_id, $this->purchase_order->user_id); + $ii->key = $this->createDbHash($this->purchase_order->company->db); + $ii->purchase_order_id = $this->purchase_order->id; + $ii->vendor_contact_id = $contact->id; + $ii->save(); + } + + + return $this->purchase_order; + } +} diff --git a/app/Services/PurchaseOrder/MarkSent.php b/app/Services/PurchaseOrder/MarkSent.php new file mode 100644 index 000000000000..fe51346066d0 --- /dev/null +++ b/app/Services/PurchaseOrder/MarkSent.php @@ -0,0 +1,45 @@ +client = $client; + $this->purchase_order = $purchase_order; + } + + public function run() + { + + /* Return immediately if status is not draft */ + if ($this->purchase_order->status_id != PurchaseOrder::STATUS_DRAFT) { + return $this->purchase_order; + } + + $this->purchase_order->markInvitationsSent(); + + $this->purchase_order + ->service() + ->setStatus(PurchaseOrder::STATUS_SENT) + ->applyNumber() + // ->adjustBalance($this->purchase_order->amount) + // ->touchPdf() + ->save(); + + event(new PurchaseOrderWasMarkedSent($this->purchase_order, $this->purchase_order->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); + + return $this->purchase_order; + } +} diff --git a/app/Services/PurchaseOrder/PurchaseOrderService.php b/app/Services/PurchaseOrder/PurchaseOrderService.php index ffb2cb84d164..b86ced3750c7 100644 --- a/app/Services/PurchaseOrder/PurchaseOrderService.php +++ b/app/Services/PurchaseOrder/PurchaseOrderService.php @@ -52,6 +52,30 @@ class PurchaseOrderService $this->purchase_order->public_notes = $this->purchase_order->client->public_notes; + return $this; + } + + public function setStatus($status) + { + $this->purchase_order->status_id = $status; + + return $this; + } + + public function markSent() + { + $this->purchase_order = (new MarkSent($this->purchase_order->client, $this->purchase_order))->run(); + + return $this; + } + /** + * Applies the purchase order number. + * @return $this PurchaseOrderService object + */ + public function applyNumber() + { + $this->purchase_order = (new ApplyNumber($this->purchase_order->client, $this->purchase_order))->run(); + return $this; } } diff --git a/app/Transformers/PurchaseOrderInvitationTransformer.php b/app/Transformers/PurchaseOrderInvitationTransformer.php new file mode 100644 index 000000000000..95187cc81c2c --- /dev/null +++ b/app/Transformers/PurchaseOrderInvitationTransformer.php @@ -0,0 +1,32 @@ + $this->encodePrimaryKey($invitation->id), + 'vendor_contact_id' => $this->encodePrimaryKey($invitation->vendor_contact_id), + 'key' => $invitation->key, + 'link' => $invitation->getLink() ?: '', + 'sent_date' => $invitation->sent_date ?: '', + 'viewed_date' => $invitation->viewed_date ?: '', + 'opened_date' => $invitation->opened_date ?: '', + 'updated_at' => (int)$invitation->updated_at, + 'archived_at' => (int)$invitation->deleted_at, + 'created_at' => (int)$invitation->created_at, + 'email_status' => $invitation->email_status ?: '', + 'email_error' => (string)$invitation->email_error, + ]; + } + +} diff --git a/app/Transformers/PurchaseOrderTransformer.php b/app/Transformers/PurchaseOrderTransformer.php index aef5ea1d6552..332879037ab2 100644 --- a/app/Transformers/PurchaseOrderTransformer.php +++ b/app/Transformers/PurchaseOrderTransformer.php @@ -13,12 +13,24 @@ namespace App\Transformers; use App\Models\PurchaseOrder; +use App\Models\PurchaseOrderInvitation; use App\Utils\Traits\MakesHash; class PurchaseOrderTransformer extends EntityTransformer { use MakesHash; + protected $defaultIncludes = [ + 'invitations', + ]; + + public function includeInvitations(PurchaseOrder $purchase_order) + { + $transformer = new PurchaseOrderInvitationTransformer($this->serializer); + + return $this->includeCollection($purchase_order->invitations, $transformer, PurchaseOrderInvitation::class); + } + public function transform(PurchaseOrder $purchase_order) { return [ @@ -26,19 +38,18 @@ class PurchaseOrderTransformer extends EntityTransformer 'user_id' => $this->encodePrimaryKey($purchase_order->user_id), 'project_id' => $this->encodePrimaryKey($purchase_order->project_id), 'assigned_user_id' => $this->encodePrimaryKey($purchase_order->assigned_user_id), - 'vendor_id' => (string) $this->encodePrimaryKey($purchase_order->vendor_id), - 'amount' => (float) $purchase_order->amount, - 'balance' => (float) $purchase_order->balance, - 'client_id' => (string) $this->encodePrimaryKey($purchase_order->client_id), - 'vendor_id' => (string) $this->encodePrimaryKey($purchase_order->vendor_id), - 'status_id' => (string) ($purchase_order->status_id ?: 1), - 'design_id' => (string) $this->encodePrimaryKey($purchase_order->design_id), - 'created_at' => (int) $purchase_order->created_at, - 'updated_at' => (int) $purchase_order->updated_at, - 'archived_at' => (int) $purchase_order->deleted_at, - 'is_deleted' => (bool) $purchase_order->is_deleted, + 'vendor_id' => (string)$this->encodePrimaryKey($purchase_order->vendor_id), + 'amount' => (float)$purchase_order->amount, + 'balance' => (float)$purchase_order->balance, + 'client_id' => (string)$this->encodePrimaryKey($purchase_order->client_id), + 'status_id' => (string)($purchase_order->status_id ?: 1), + 'design_id' => (string)$this->encodePrimaryKey($purchase_order->design_id), + 'created_at' => (int)$purchase_order->created_at, + 'updated_at' => (int)$purchase_order->updated_at, + 'archived_at' => (int)$purchase_order->deleted_at, + 'is_deleted' => (bool)$purchase_order->is_deleted, 'number' => $purchase_order->number ?: '', - 'discount' => (float) $purchase_order->discount, + 'discount' => (float)$purchase_order->discount, 'po_number' => $purchase_order->po_number ?: '', 'date' => $purchase_order->date ?: '', 'last_sent_date' => $purchase_order->last_sent_date ?: '', @@ -51,36 +62,36 @@ class PurchaseOrderTransformer extends EntityTransformer 'terms' => $purchase_order->terms ?: '', 'public_notes' => $purchase_order->public_notes ?: '', 'private_notes' => $purchase_order->private_notes ?: '', - 'uses_inclusive_taxes' => (bool) $purchase_order->uses_inclusive_taxes, + 'uses_inclusive_taxes' => (bool)$purchase_order->uses_inclusive_taxes, 'tax_name1' => $purchase_order->tax_name1 ? $purchase_order->tax_name1 : '', - 'tax_rate1' => (float) $purchase_order->tax_rate1, + 'tax_rate1' => (float)$purchase_order->tax_rate1, 'tax_name2' => $purchase_order->tax_name2 ? $purchase_order->tax_name2 : '', - 'tax_rate2' => (float) $purchase_order->tax_rate2, + 'tax_rate2' => (float)$purchase_order->tax_rate2, 'tax_name3' => $purchase_order->tax_name3 ? $purchase_order->tax_name3 : '', - 'tax_rate3' => (float) $purchase_order->tax_rate3, - 'total_taxes' => (float) $purchase_order->total_taxes, - 'is_amount_discount' => (bool) ($purchase_order->is_amount_discount ?: false), + 'tax_rate3' => (float)$purchase_order->tax_rate3, + 'total_taxes' => (float)$purchase_order->total_taxes, + 'is_amount_discount' => (bool)($purchase_order->is_amount_discount ?: false), 'footer' => $purchase_order->footer ?: '', - 'partial' => (float) ($purchase_order->partial ?: 0.0), + 'partial' => (float)($purchase_order->partial ?: 0.0), 'partial_due_date' => $purchase_order->partial_due_date ?: '', - 'custom_value1' => (string) $purchase_order->custom_value1 ?: '', - 'custom_value2' => (string) $purchase_order->custom_value2 ?: '', - 'custom_value3' => (string) $purchase_order->custom_value3 ?: '', - 'custom_value4' => (string) $purchase_order->custom_value4 ?: '', - 'has_tasks' => (bool) $purchase_order->has_tasks, - 'has_expenses' => (bool) $purchase_order->has_expenses, - 'custom_surcharge1' => (float) $purchase_order->custom_surcharge1, - 'custom_surcharge2' => (float) $purchase_order->custom_surcharge2, - 'custom_surcharge3' => (float) $purchase_order->custom_surcharge3, - 'custom_surcharge4' => (float) $purchase_order->custom_surcharge4, - 'custom_surcharge_tax1' => (bool) $purchase_order->custom_surcharge_tax1, - 'custom_surcharge_tax2' => (bool) $purchase_order->custom_surcharge_tax2, - 'custom_surcharge_tax3' => (bool) $purchase_order->custom_surcharge_tax3, - 'custom_surcharge_tax4' => (bool) $purchase_order->custom_surcharge_tax4, - 'line_items' => $purchase_order->line_items ?: (array) [], + 'custom_value1' => (string)$purchase_order->custom_value1 ?: '', + 'custom_value2' => (string)$purchase_order->custom_value2 ?: '', + 'custom_value3' => (string)$purchase_order->custom_value3 ?: '', + 'custom_value4' => (string)$purchase_order->custom_value4 ?: '', + 'has_tasks' => (bool)$purchase_order->has_tasks, + 'has_expenses' => (bool)$purchase_order->has_expenses, + 'custom_surcharge1' => (float)$purchase_order->custom_surcharge1, + 'custom_surcharge2' => (float)$purchase_order->custom_surcharge2, + 'custom_surcharge3' => (float)$purchase_order->custom_surcharge3, + 'custom_surcharge4' => (float)$purchase_order->custom_surcharge4, + 'custom_surcharge_tax1' => (bool)$purchase_order->custom_surcharge_tax1, + 'custom_surcharge_tax2' => (bool)$purchase_order->custom_surcharge_tax2, + 'custom_surcharge_tax3' => (bool)$purchase_order->custom_surcharge_tax3, + 'custom_surcharge_tax4' => (bool)$purchase_order->custom_surcharge_tax4, + 'line_items' => $purchase_order->line_items ?: (array)[], 'entity_type' => 'credit', - 'exchange_rate' => (float) $purchase_order->exchange_rate, - 'paid_to_date' => (float) $purchase_order->paid_to_date, + 'exchange_rate' => (float)$purchase_order->exchange_rate, + 'paid_to_date' => (float)$purchase_order->paid_to_date, 'subscription_id' => $this->encodePrimaryKey($purchase_order->subscription_id), ]; } diff --git a/app/Utils/Traits/GeneratesCounter.php b/app/Utils/Traits/GeneratesCounter.php index b9606621246a..3dd2bcefb330 100644 --- a/app/Utils/Traits/GeneratesCounter.php +++ b/app/Utils/Traits/GeneratesCounter.php @@ -18,6 +18,7 @@ use App\Models\Expense; use App\Models\Invoice; use App\Models\Payment; use App\Models\Project; +use App\Models\PurchaseOrder; use App\Models\Quote; use App\Models\RecurringExpense; use App\Models\RecurringInvoice; @@ -44,8 +45,8 @@ trait GeneratesCounter $is_client_counter = false; - $counter_string = $this->getEntityCounter($entity, $client); - $pattern = $this->getNumberPattern($entity, $client); + $counter_string = $this->getEntityCounter($entity, $client); + $pattern = $this->getNumberPattern($entity, $client); if ((strpos($pattern, 'clientCounter') !== false) || (strpos($pattern, 'client_counter') !==false) ) { @@ -71,9 +72,9 @@ trait GeneratesCounter $counter_entity = $client->company; } - //If it is a quote - we need to + //If it is a quote - we need to $pattern = $this->getNumberPattern($entity, $client); - + if(strlen($pattern) > 1 && (stripos($pattern, 'counter') === false)){ $pattern = $pattern.'{$counter}'; } @@ -127,9 +128,9 @@ trait GeneratesCounter break; case Quote::class: - if ($this->hasSharedCounter($client, 'quote')) + if ($this->hasSharedCounter($client, 'quote')) return 'invoice_number_counter'; - + return 'quote_number_counter'; break; case RecurringInvoice::class: @@ -145,14 +146,17 @@ trait GeneratesCounter return 'payment_number_counter'; break; case Credit::class: - if ($this->hasSharedCounter($client, 'credit')) + if ($this->hasSharedCounter($client, 'credit')) return 'invoice_number_counter'; - + return 'credit_number_counter'; break; case Project::class: return 'project_number_counter'; break; + case PurchaseOrder::class: + return 'purchase_order_number_counter'; + break; default: return 'default_number_counter'; @@ -188,6 +192,20 @@ trait GeneratesCounter return $this->replaceUserVars($credit, $entity_number); + } + /** + * Gets the next purchase order number. + * + * @param PurchaseOrder $purchase_order The purchase order + * + * @return string The next purchase order number. + */ + public function getNextPurchaseOrderNumber(Client $client, ?PurchaseOrder $purchase_order) :string + { + $entity_number = $this->getNextEntityNumber(PurchaseOrder::class, $client); + + return $this->replaceUserVars($purchase_order, $entity_number); + } /** @@ -385,7 +403,7 @@ trait GeneratesCounter * * @return bool True if has shared counter, False otherwise. */ - public function hasSharedCounter(Client $client, string $type = 'quote') : bool + public function hasSharedCounter(Client $client, string $type = 'quote') : bool { if($type == 'quote') return (bool) $client->getSetting('shared_invoice_quote_counter'); @@ -438,9 +456,9 @@ trait GeneratesCounter public function checkNumberAvailable($class, $entity, $number) :bool { - if ($entity = $class::whereCompanyId($entity->company_id)->whereNumber($number)->withTrashed()->exists()) + if ($entity = $class::whereCompanyId($entity->company_id)->whereNumber($number)->withTrashed()->exists()) return false; - + return true; } @@ -504,7 +522,7 @@ trait GeneratesCounter if($reset_counter_frequency == 0) return; - + $timezone = Timezone::find($client->getSetting('timezone_id')); $reset_date = Carbon::parse($client->getSetting('reset_counter_date'), $timezone->name); @@ -558,6 +576,7 @@ trait GeneratesCounter $settings->invoice_number_counter = 1; $settings->quote_number_counter = 1; $settings->credit_number_counter = 1; + $settings->purchase_order_number_counter = 1; $client->company->settings = $settings; $client->company->save(); @@ -622,6 +641,7 @@ trait GeneratesCounter $settings->task_number_counter = 1; $settings->expense_number_counter = 1; $settings->recurring_expense_number_counter =1; + $settings->purchase_order_number_counter = 1; $company->settings = $settings; $company->save(); @@ -644,7 +664,7 @@ trait GeneratesCounter $search = []; $replace = []; - + $search[] = '{$counter}'; $replace[] = $counter; @@ -659,7 +679,7 @@ trait GeneratesCounter $search[] = '{$year}'; $replace[] = Carbon::now($entity->company->timezone()->name)->format('Y'); - + if (strstr($pattern, '{$user_id}') || strstr($pattern, '{$userId}')) { $user_id = $entity->user_id ? $entity->user_id : 0; $search[] = '{$user_id}'; @@ -683,7 +703,7 @@ trait GeneratesCounter $search[] = '{$vendor_id_number}'; $replace[] = $entity->id_number; } - + if ($entity instanceof Expense) { if ($entity->vendor) { $search[] = '{$vendor_id_number}'; @@ -708,7 +728,7 @@ trait GeneratesCounter $search[] = '{$expense_id_number}'; $replace[] = $entity->id_number; } - + if ($entity->client || ($entity instanceof Client)) { $client = $entity->client ?: $entity; diff --git a/database/factories/PurchaseOrderInvitationFactory.php b/database/factories/PurchaseOrderInvitationFactory.php new file mode 100644 index 000000000000..d2051c0c8d88 --- /dev/null +++ b/database/factories/PurchaseOrderInvitationFactory.php @@ -0,0 +1,31 @@ + Str::random(40), + ]; + } +} diff --git a/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php b/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php new file mode 100644 index 000000000000..984e6c6cd634 --- /dev/null +++ b/database/migrations/2022_06_01_224339_create_purchase_order_invitations_table.php @@ -0,0 +1,53 @@ +id(); + $table->unsignedInteger('company_id')->index(); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('vendor_contact_id')->unique(); + $table->unsignedBigInteger('purchase_order_id')->index()->unique(); + $table->string('key')->index(); + $table->string('transaction_reference')->nullable(); + $table->string('message_id')->nullable()->index(); + $table->mediumText('email_error')->nullable(); + $table->text('signature_base64')->nullable(); + $table->datetime('signature_date')->nullable(); + + $table->datetime('sent_date')->nullable(); + $table->datetime('viewed_date')->nullable(); + $table->datetime('opened_date')->nullable(); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade'); + $table->foreign('vendor_contact_id')->references('id')->on('vendor_contacts')->onDelete('cascade')->onUpdate('cascade'); + $table->foreign('purchase_order_id')->references('id')->on('purchase_orders')->onDelete('cascade')->onUpdate('cascade'); + $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade'); + + $table->timestamps(6); + $table->softDeletes('deleted_at', 6); + + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('purchase_order_invitations'); + } +} diff --git a/tests/Feature/PurchaseOrderTest.php b/tests/Feature/PurchaseOrderTest.php index 69504545a804..63799022b7aa 100644 --- a/tests/Feature/PurchaseOrderTest.php +++ b/tests/Feature/PurchaseOrderTest.php @@ -37,6 +37,7 @@ class PurchaseOrderTest extends TestCase Model::reguard(); $this->makeTestData(); + } public function testPurchaseOrderRest() @@ -44,18 +45,18 @@ class PurchaseOrderTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, - ])->get('/api/v1/purchase_orders/'.$this->encodePrimaryKey($this->purchase_order->id)); + ])->get('/api/v1/purchase_orders/' . $this->encodePrimaryKey($this->purchase_order->id)); $response->assertStatus(200); $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, - ])->get('/api/v1/purchase_orders/'.$this->encodePrimaryKey($this->purchase_order->id).'/edit'); + ])->get('/api/v1/purchase_orders/' . $this->encodePrimaryKey($this->purchase_order->id) . '/edit'); $response->assertStatus(200); - $credit_update = [ + $purchase_order_update = [ 'tax_name1' => 'dippy', ]; @@ -64,14 +65,14 @@ class PurchaseOrderTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, - ])->put('/api/v1/purchase_orders/'.$this->encodePrimaryKey($this->purchase_order->id), $credit_update) + ])->put('/api/v1/purchase_orders/' . $this->encodePrimaryKey($this->purchase_order->id), $purchase_order_update) ->assertStatus(200); } + public function testPostNewPurchaseOrder() { $purchase_order = [ 'status_id' => 1, - 'number' => 'dfdfd', 'discount' => 0, 'is_amount_discount' => 1, 'number' => '34343xx43', @@ -91,20 +92,21 @@ class PurchaseOrderTest extends TestCase ])->post('/api/v1/purchase_orders/', $purchase_order) ->assertStatus(200); } + public function testPurchaseOrderDelete() { $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, - ])->delete('/api/v1/purchase_orders/'.$this->encodePrimaryKey($this->purchase_order->id)); + ])->delete('/api/v1/purchase_orders/' . $this->encodePrimaryKey($this->purchase_order->id)); $response->assertStatus(200); } + public function testPurchaseOrderUpdate() { $data = [ 'status_id' => 1, - 'number' => 'dfdfd', 'discount' => 0, 'is_amount_discount' => 1, 'number' => '3434343', @@ -121,14 +123,14 @@ class PurchaseOrderTest extends TestCase $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, - ])->put('/api/v1/purchase_orders/'.$this->encodePrimaryKey($this->purchase_order->id), $data); + ])->put('/api/v1/purchase_orders/' . $this->encodePrimaryKey($this->purchase_order->id), $data); $response->assertStatus(200); $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, - ])->put('/api/v1/purchase_orders/'.$this->encodePrimaryKey($this->purchase_order->id), $data); + ])->put('/api/v1/purchase_orders/' . $this->encodePrimaryKey($this->purchase_order->id), $data); $response->assertStatus(200); diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index 0dddbaa32df2..2bb170b9388b 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -36,6 +36,8 @@ use App\Models\GroupSetting; use App\Models\InvoiceInvitation; use App\Models\Product; use App\Models\Project; +use App\Models\PurchaseOrder; +use App\Models\PurchaseOrderInvitation; use App\Models\Quote; use App\Models\QuoteInvitation; use App\Models\RecurringExpense; @@ -476,6 +478,26 @@ trait MockAccountData $this->purchase_order->save(); + PurchaseOrderInvitation::factory()->create([ + 'user_id' => $user_id, + 'company_id' => $this->company->id, + 'vendor_contact_id' => $vendor_contact->id, + 'purchase_order_id' => $this->purchase_order->id, + ]); + + + + $purchase_order_invitations = PurchaseOrderInvitation::whereCompanyId($this->purchase_order->company_id) + ->wherePurchaseOrderId($this->purchase_order->id); + + $this->purchase_order->setRelation('invitations', $purchase_order_invitations); + + $this->purchase_order->service()->markSent(); + + $this->purchase_order->setRelation('client', $this->client); + $this->purchase_order->setRelation('company', $this->company); + + $this->purchase_order->save(); $this->credit = CreditFactory::create($this->company->id, $user_id);