1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 05:02:36 +01:00

Merge pull request #7499 from CirkaN/Cirkovic/INA-6

Cirkovic/ina 6
This commit is contained in:
David Bomba 2022-06-05 19:12:21 +10:00 committed by GitHub
commit f2bfca648f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 677 additions and 71 deletions

View File

@ -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 = [

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

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

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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