mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-10 05:02:36 +01:00
commit
f2bfca648f
@ -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 = [
|
||||
|
38
app/Events/PurchaseOrder/PurchaseOrderWasMarkedSent.php
Normal file
38
app/Events/PurchaseOrder/PurchaseOrderWasMarkedSent.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Events\PurchaseOrder;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\PurchaseOrder;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
/**
|
||||
* Class PurchaseOrderWasMarkedSent.
|
||||
*/
|
||||
class PurchaseOrderWasMarkedSent
|
||||
{
|
||||
use SerializesModels;
|
||||
|
||||
/**
|
||||
* @var \App\Models\PurchaseOrder
|
||||
*/
|
||||
public $purchase_order;
|
||||
|
||||
public $company;
|
||||
|
||||
public $event_vars;
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param PurchaseOrder $purchase_order
|
||||
* @param Company $company
|
||||
* @param array $event_vars
|
||||
*/
|
||||
public function __construct(PurchaseOrder $purchase_order, Company $company, array $event_vars)
|
||||
{
|
||||
$this->purchase_order = $purchase_order;
|
||||
$this->company = $company;
|
||||
$this->event_vars = $event_vars;
|
||||
}
|
||||
}
|
31
app/Factory/PurchaseOrderInvitationFactory.php
Normal file
31
app/Factory/PurchaseOrderInvitationFactory.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Factory;
|
||||
|
||||
use App\Models\PurchaseOrderInvitation;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PurchaseOrderInvitationFactory
|
||||
{
|
||||
public static function create(int $company_id, int $user_id) :PurchaseOrderInvitation
|
||||
{
|
||||
$ci = new PurchaseOrderInvitation();
|
||||
$ci->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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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()
|
||||
|
81
app/Models/PurchaseOrderInvitation.php
Normal file
81
app/Models/PurchaseOrderInvitation.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
|
||||
use App\Utils\Traits\Inviteable;
|
||||
use App\Utils\Traits\MakesDates;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class PurchaseOrderInvitation extends BaseModel
|
||||
{
|
||||
use MakesDates;
|
||||
use SoftDeletes;
|
||||
use Inviteable;
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'vendor_contact_id',
|
||||
];
|
||||
|
||||
protected $with = [
|
||||
'company',
|
||||
'contact',
|
||||
];
|
||||
protected $touches = ['purchase_order'];
|
||||
|
||||
public function getEntityType()
|
||||
{
|
||||
return self::class;
|
||||
}
|
||||
|
||||
public function entityType()
|
||||
{
|
||||
return PurchaseOrder::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function purchase_order()
|
||||
{
|
||||
return $this->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();
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 => [
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
59
app/Services/PurchaseOrder/ApplyNumber.php
Normal file
59
app/Services/PurchaseOrder/ApplyNumber.php
Normal file
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Services\PurchaseOrder;
|
||||
|
||||
|
||||
use App\Models\Client;
|
||||
use App\Models\Credit;
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Services\AbstractService;
|
||||
use App\Utils\Traits\GeneratesCounter;
|
||||
use Illuminate\Database\QueryException;
|
||||
|
||||
class ApplyNumber extends AbstractService
|
||||
{
|
||||
use GeneratesCounter;
|
||||
|
||||
private Client $client;
|
||||
|
||||
private PurchaseOrder $purchase_order;
|
||||
|
||||
private bool $completed = true;
|
||||
|
||||
public function __construct(Client $client, PurchaseOrder $purchase_order)
|
||||
{
|
||||
$this->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);
|
||||
|
||||
}
|
||||
}
|
91
app/Services/PurchaseOrder/CreateInvitations.php
Normal file
91
app/Services/PurchaseOrder/CreateInvitations.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Services\PurchaseOrder;
|
||||
|
||||
|
||||
use App\Factory\PurchaseOrderInvitationFactory;
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Models\PurchaseOrderInvitation;
|
||||
use App\Services\AbstractService;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CreateInvitations extends AbstractService
|
||||
{
|
||||
use MakesHash;
|
||||
|
||||
public PurchaseOrder $purchase_order;
|
||||
|
||||
public function __construct(PurchaseOrder $purchase_order)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
45
app/Services/PurchaseOrder/MarkSent.php
Normal file
45
app/Services/PurchaseOrder/MarkSent.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Services\PurchaseOrder;
|
||||
|
||||
|
||||
use App\Events\PurchaseOrder\PurchaseOrderWasMarkedSent;
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Utils\Ninja;
|
||||
|
||||
class MarkSent
|
||||
{
|
||||
private $client;
|
||||
|
||||
private $purchase_order;
|
||||
|
||||
public function __construct($client, $purchase_order)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
32
app/Transformers/PurchaseOrderInvitationTransformer.php
Normal file
32
app/Transformers/PurchaseOrderInvitationTransformer.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Transformers;
|
||||
|
||||
|
||||
use App\Models\PurchaseOrderInvitation;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
|
||||
class PurchaseOrderInvitationTransformer extends EntityTransformer
|
||||
{
|
||||
use MakesHash;
|
||||
|
||||
public function transform(PurchaseOrderInvitation $invitation)
|
||||
{
|
||||
return [
|
||||
'id' => $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,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
@ -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),
|
||||
];
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
31
database/factories/PurchaseOrderInvitationFactory.php
Normal file
31
database/factories/PurchaseOrderInvitationFactory.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
|
||||
use App\Models\PurchaseOrderInvitation;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PurchaseOrderInvitationFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = PurchaseOrderInvitation::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'key' => Str::random(40),
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreatePurchaseOrderInvitationsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('purchase_order_invitations', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user