diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index a3d272a5c410..a3ed7d2fb4d2 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -116,6 +116,9 @@ class CompanySettings extends BaseSettings public $project_number_pattern = ''; //@implemented public $project_number_counter = 1; //@implemented + public $purchase_order_number_pattern = ''; //@implemented + public $purchase_order_number_counter = 1; //@implemented + public $shared_invoice_quote_counter = false; //@implemented public $shared_invoice_credit_counter = false; //@implemented public $recurring_number_prefix = ''; //@implemented diff --git a/app/Helpers/Invoice/InvoiceSum.php b/app/Helpers/Invoice/InvoiceSum.php index 8121cbfcc04e..8e3313fb49f2 100644 --- a/app/Helpers/Invoice/InvoiceSum.php +++ b/app/Helpers/Invoice/InvoiceSum.php @@ -224,6 +224,14 @@ class InvoiceSum return $this->invoice; } + public function getPurchaseOrder() + { + $this->setCalculatedAttributes(); + $this->invoice->saveQuietly(); + + return $this->invoice; + } + public function getRecurringInvoice() { $this->invoice->amount = $this->formatValue($this->getTotal(), $this->invoice->client->currency()->precision); diff --git a/app/Helpers/Invoice/InvoiceSumInclusive.php b/app/Helpers/Invoice/InvoiceSumInclusive.php index 0cdb1638045d..eeb344a50c3d 100644 --- a/app/Helpers/Invoice/InvoiceSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceSumInclusive.php @@ -229,6 +229,15 @@ class InvoiceSumInclusive return $this->invoice; } + public function getPurchaseOrder() + { + //Build invoice values here and return Invoice + $this->setCalculatedAttributes(); + $this->invoice->saveQuietly(); + + return $this->invoice; + } + /** * Build $this->invoice variables after * calculations have been performed. diff --git a/app/Http/Controllers/PurchaseOrderController.php b/app/Http/Controllers/PurchaseOrderController.php index 677e00196903..b07f068aec3b 100644 --- a/app/Http/Controllers/PurchaseOrderController.php +++ b/app/Http/Controllers/PurchaseOrderController.php @@ -174,8 +174,6 @@ class PurchaseOrderController extends BaseController public function store(StorePurchaseOrderRequest $request) { - $client = Client::find($request->get('client_id')); - $purchase_order = $this->purchase_order_repository->save($request->all(), PurchaseOrderFactory::create(auth()->user()->company()->id, auth()->user()->id)); $purchase_order = $purchase_order->service() diff --git a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php index 1f531178050b..ef97d446ad58 100644 --- a/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/StorePurchaseOrderRequest.php @@ -38,8 +38,7 @@ class StorePurchaseOrderRequest extends Request { $rules = []; - $rules['client_id'] = 'required'; - + $rules['vendor_id'] = 'required'; $rules['number'] = ['nullable', Rule::unique('purchase_orders')->where('company_id', auth()->user()->company()->id)]; $rules['discount'] = 'sometimes|numeric'; diff --git a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php index 55e8f5354490..d2e83154fb95 100644 --- a/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php +++ b/app/Http/Requests/PurchaseOrder/UpdatePurchaseOrderRequest.php @@ -55,7 +55,6 @@ class UpdatePurchaseOrderRequest extends Request $input = $this->decodePrimaryKeys($input); - $input['id'] = $this->purchase_order->id; $this->replace($input); diff --git a/app/Models/PurchaseOrder.php b/app/Models/PurchaseOrder.php index 4d73c609559d..b1e6e87addd7 100644 --- a/app/Models/PurchaseOrder.php +++ b/app/Models/PurchaseOrder.php @@ -12,6 +12,8 @@ namespace App\Models; +use App\Helpers\Invoice\InvoiceSum; +use App\Helpers\Invoice\InvoiceSumInclusive; use App\Services\PurchaseOrder\PurchaseOrderService; use Illuminate\Database\Eloquent\SoftDeletes; @@ -92,8 +94,9 @@ class PurchaseOrder extends BaseModel const STATUS_DRAFT = 1; const STATUS_SENT = 2; - const STATUS_PARTIAL = 3; - const STATUS_APPLIED = 4; + const STATUS_APPROVED = 3; + const STATUS_CONVERTED = 4; + const STATUS_EXPIRED = -1; public function assigned_user() { @@ -130,7 +133,6 @@ class PurchaseOrder extends BaseModel return $this->belongsTo(Client::class)->withTrashed(); } - public function invitations() { return $this->hasMany(CreditInvitation::class); @@ -166,4 +168,17 @@ class PurchaseOrder extends BaseModel return $this->morphMany(Document::class, 'documentable'); } + public function calc() + { + $credit_calc = null; + + if ($this->uses_inclusive_taxes) { + $credit_calc = new InvoiceSumInclusive($this); + } else { + $credit_calc = new InvoiceSum($this); + } + + return $credit_calc->build(); + } + } diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php index bae220a60698..1076ecb29528 100644 --- a/app/Models/Vendor.php +++ b/app/Models/Vendor.php @@ -11,6 +11,7 @@ namespace App\Models; +use App\DataMapper\CompanySettings; use App\Models\Presenters\VendorPresenter; use App\Utils\Traits\GeneratesCounter; use Illuminate\Database\Eloquent\SoftDeletes; @@ -109,4 +110,40 @@ class Vendor extends BaseModel { return ctrans('texts.vendor'); } + + public function setCompanyDefaults($data, $entity_name) :array + { + $defaults = []; + + if (! (array_key_exists('terms', $data) && strlen($data['terms']) > 1)) { + $defaults['terms'] = $this->getSetting($entity_name.'_terms'); + } elseif (array_key_exists('terms', $data)) { + $defaults['terms'] = $data['terms']; + } + + if (! (array_key_exists('footer', $data) && strlen($data['footer']) > 1)) { + $defaults['footer'] = $this->getSetting($entity_name.'_footer'); + } elseif (array_key_exists('footer', $data)) { + $defaults['footer'] = $data['footer']; + } + + if (strlen($this->public_notes) >= 1) { + $defaults['public_notes'] = $this->public_notes; + } + + return $defaults; + } + + public function getSetting($setting) + { + if ((property_exists($this->company->settings, $setting) != false) && (isset($this->company->settings->{$setting}) !== false)) { + return $this->company->settings->{$setting}; + } + + elseif( property_exists(CompanySettings::defaults(), $setting) ) { + return CompanySettings::defaults()->{$setting}; + } + + return ''; + } } diff --git a/app/Repositories/PurchaseOrderRepository.php b/app/Repositories/PurchaseOrderRepository.php index cf0653c25c8a..f6ab93d5b5d3 100644 --- a/app/Repositories/PurchaseOrderRepository.php +++ b/app/Repositories/PurchaseOrderRepository.php @@ -11,8 +11,10 @@ namespace App\Repositories; - +use App\Factory\PurchaseOrderFactory; use App\Models\PurchaseOrder; +use App\Models\Vendor; +use App\Models\VendorContact; use App\Utils\Traits\MakesHash; class PurchaseOrderRepository extends BaseRepository @@ -25,10 +27,144 @@ class PurchaseOrderRepository extends BaseRepository public function save(array $data, PurchaseOrder $purchase_order) : ?PurchaseOrder { - $purchase_order->fill($data); + + if(array_key_exists('vendor_id', $data)) + $purchase_order->vendor_id = $data['vendor_id']; + + $vendor = Vendor::where('id', $purchase_order->vendor_id)->withTrashed()->firstOrFail(); + + $state = []; + + $resource = class_basename($purchase_order); //ie Invoice + + if (! $purchase_order->id) { + $company_defaults = $vendor->setCompanyDefaults($data, lcfirst($resource)); + $purchase_order->uses_inclusive_taxes = $vendor->getSetting('inclusive_taxes'); + $data = array_merge($company_defaults, $data); + } + + $tmp_data = $data; //preserves the $data array + + /* We need to unset some variable as we sometimes unguard the model */ + if (isset($tmp_data['invitations'])) + unset($tmp_data['invitations']); + + if (isset($tmp_data['vendor_contacts'])) + unset($tmp_data['vendor_contacts']); + + $purchase_order->fill($tmp_data); + + $purchase_order->custom_surcharge_tax1 = $vendor->company->custom_surcharge_taxes1; + $purchase_order->custom_surcharge_tax2 = $vendor->company->custom_surcharge_taxes2; + $purchase_order->custom_surcharge_tax3 = $vendor->company->custom_surcharge_taxes3; + $purchase_order->custom_surcharge_tax4 = $vendor->company->custom_surcharge_taxes4; + + if(!$purchase_order->id) + $this->new_model = true; + + $purchase_order->saveQuietly(); + + /* Save any documents */ + if (array_key_exists('documents', $data)) + $this->saveDocuments($data['documents'], $purchase_order); + + if (array_key_exists('file', $data)) + $this->saveDocuments($data['file'], $purchase_order); + + /* If invitations are present we need to filter existing invitations with the new ones */ + if (isset($data['invitations'])) { + $invitations = collect($data['invitations']); + + /* Get array of Keys which have been removed from the invitations array and soft delete each invitation */ + $purchase_order->invitations->pluck('key')->diff($invitations->pluck('key'))->each(function ($invitation) { + $invitation = PurchaseOrderInvitation::where('key', $invitation)->first(); + + if ($invitation) + $invitation->delete(); + + }); + + foreach ($data['invitations'] as $invitation) { + + //if no invitations are present - create one. + if (! $this->getInvitation($invitation, $resource)) { + + if (isset($invitation['id'])) + unset($invitation['id']); + + //make sure we are creating an invite for a contact who belongs to the client only! + $contact = VendorContact::find($invitation['vendor_contact_id']); + + if ($contact && $purchase_order->client_id == $contact->client_id) { + + $new_invitation = PurchaseOrderInvitation::withTrashed() + ->where('vendor_contact_id', $contact->id) + ->where('purchase_order_id', $purchase_order->id) + ->first(); + + if ($new_invitation && $new_invitation->trashed()) { + + $new_invitation->restore(); + + } else { + + $new_invitation = PurchaseOrderFactory::create($purchase_order->company_id, $purchase_order->user_id); + $new_invitation->purchase_order_id = $purchase_order->id; + $new_invitation->vendor_contact_id = $contact->id; + $new_invitation->key = $this->createDbHash($purchase_order->company->db); + $new_invitation->save(); + + } + } + } + } + } + + /* If no invitations have been created, this is our fail safe to maintain state*/ + if ($purchase_order->invitations()->count() == 0) + $purchase_order->service()->createInvitations(); + + /* Apply entity number */ + $purchase_order = $purchase_order->service()->applyNumber()->save(); + + /* Handle attempts where the deposit is greater than the amount/balance of the invoice */ + if((int)$purchase_order->balance != 0 && $purchase_order->partial > $purchase_order->amount) + $purchase_order->partial = min($purchase_order->amount, $purchase_order->balance); + + $purchase_order = $purchase_order->calc()->getPurchaseOrder(); + + if (! $purchase_order->design_id) + $purchase_order->design_id = $this->decodePrimaryKey($client->getSetting('credit_design_id')); + + if(array_key_exists('invoice_id', $data) && $data['invoice_id']) + $purchase_order->invoice_id = $data['invoice_id']; + + if($this->new_model) + event('eloquent.created: App\Models\PurchaseOrder', $purchase_order); + else + event('eloquent.updated: App\Models\PurchaseOrder', $purchase_order); + + $purchase_order->save(); - return $purchase_order; + return $purchase_order->fresh(); + + + // $purchase_order->fill($data); + // $purchase_order->save(); + + // return $purchase_order; } + public function getInvitation($invitation, $resource) + { + // if (is_array($invitation) && ! array_key_exists('key', $invitation)) + // return false; + + // $invitation = PurchaseOrderInvitation::where('key', $invitation['key'])->first(); + + return $invitation; + } + + } diff --git a/app/Services/PurchaseOrder/ApplyNumber.php b/app/Services/PurchaseOrder/ApplyNumber.php new file mode 100644 index 000000000000..bf4a33ddbcdf --- /dev/null +++ b/app/Services/PurchaseOrder/ApplyNumber.php @@ -0,0 +1,88 @@ +vendor = $vendor; + + $this->purchase_order = $purchase_order; + } + + public function run() + { + if ($this->purchase_order->number != '') { + return $this->purchase_order; + } + + switch ($this->client->getSetting('counter_number_applied')) { + case 'when_saved': + $this->trySaving(); + break; + case 'when_sent': + if ($this->purchase_order->status_id == PurchaseOrder::STATUS_SENT) { + $this->trySaving(); + } + break; + + default: + break; + } + + return $this->purchase_order; + } + + private function trySaving() + { + + $x=1; + + do{ + + try{ + + $this->purchase_order->number = $this->getNextPurchaseOrderNumber($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..301f21806509 --- /dev/null +++ b/app/Services/PurchaseOrder/CreateInvitations.php @@ -0,0 +1,104 @@ +purchase_order = $purchase_order; + } + + public function run() + { + + $contacts = $this->purchase_order->vendor->contacts()->where('send_email', true)->get(); + + if($contacts->count() == 0){ + $this->createBlankContact(); + + $this->purchase_order->refresh(); + $contacts = $this->purchase_order->vendor->contacts; + } + + $contacts->each(function ($contact) { + $invitation = PurchaseOrderInvitation::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 && $contact->send_email) { + $ii = PurchaseOrderInvitationFactory::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 ($invitation && ! $contact->send_email) { + $invitation->delete(); + } + }); + + if($this->purchase_order->invitations()->count() == 0) { + + if($contacts->count() == 0){ + $contact = $this->createBlankContact(); + } + else{ + $contact = $contacts->first(); + + $invitation = PurchaseOrderInvitation::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 = PurchaseOrderInvitationFactory::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; + } + + private function createBlankContact() + { + $new_contact = VendorContactFactory::create($this->purchase_order->company_id, $this->purchase_order->user_id); + $new_contact->vendor_id = $this->purchase_order->vendor_id; + $new_contact->contact_key = Str::random(40); + $new_contact->is_primary = true; + $new_contact->save(); + + return $new_contact; + } +} diff --git a/app/Services/PurchaseOrder/PurchaseOrderService.php b/app/Services/PurchaseOrder/PurchaseOrderService.php index ffb2cb84d164..9328a2a016a9 100644 --- a/app/Services/PurchaseOrder/PurchaseOrderService.php +++ b/app/Services/PurchaseOrder/PurchaseOrderService.php @@ -13,6 +13,8 @@ namespace App\Services\PurchaseOrder; use App\Models\PurchaseOrder; +use App\Services\PurchaseOrder\ApplyNumber; +use App\Services\PurchaseOrder\CreateInvitations; use App\Utils\Traits\MakesHash; class PurchaseOrderService @@ -37,19 +39,34 @@ class PurchaseOrderService return $this->purchase_order; } + public function createInvitations() + { + + $this->purchase_order = (new CreateInvitations($this->purchase_order))->run(); + + return $this; + } + + public function applyNumber() + { + $this->invoice = (new ApplyNumber($this->purchase_order->vendor, $this->purchase_order))->run(); + + return $this; + } + public function fillDefaults() { - $settings = $this->purchase_order->client->getMergedSettings(); + // $settings = $this->purchase_order->client->getMergedSettings(); - //TODO implement design, footer, terms + // //TODO implement design, footer, terms - /* If client currency differs from the company default currency, then insert the client exchange rate on the model.*/ - if (!isset($this->purchase_order->exchange_rate) && $this->purchase_order->client->currency()->id != (int)$this->purchase_order->company->settings->currency_id) - $this->purchase_order->exchange_rate = $this->purchase_order->client->currency()->exchange_rate; + // /* If client currency differs from the company default currency, then insert the client exchange rate on the model.*/ + // if (!isset($this->purchase_order->exchange_rate) && $this->purchase_order->client->currency()->id != (int)$this->purchase_order->company->settings->currency_id) + // $this->purchase_order->exchange_rate = $this->purchase_order->client->currency()->exchange_rate; - if (!isset($this->purchase_order->public_notes)) - $this->purchase_order->public_notes = $this->purchase_order->client->public_notes; + // if (!isset($this->purchase_order->public_notes)) + // $this->purchase_order->public_notes = $this->purchase_order->client->public_notes; return $this; diff --git a/app/Utils/Traits/GeneratesCounter.php b/app/Utils/Traits/GeneratesCounter.php index b9606621246a..6e470292edee 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; @@ -154,6 +155,10 @@ trait GeneratesCounter return 'project_number_counter'; break; + case PurchaseOrder::class: + return 'purchase_order_number_counter'; + break; + default: return 'default_number_counter'; break; @@ -345,6 +350,23 @@ trait GeneratesCounter } + public function getNextPurchaseOrderNumber(PurchaseOrder $purchase_order) :string + { + $this->resetCompanyCounters($purchase_order->company); + + $counter = $purchase_order->company->settings->purchase_order_number_counter; + $setting_entity = $purchase_order->company->settings->purchase_order_number_counter; + + $purchase_order_number = $this->checkEntityNumber(PurchaseOrder::class, $purchase_order, $counter, $purchase_order->company->settings->counter_padding, $purchase_order->company->settings->purchase_order_number_pattern); + + $this->incrementCounter($purchase_order->company, 'purchase_order_number_pattern'); + + $entity_number = $purchase_order_number; + + return $this->replaceUserVars($purchase_order, $entity_number); + + } + /** * Gets the next expense number. *