diff --git a/app/Http/Controllers/Auth/VendorContactLoginController.php b/app/Http/Controllers/Auth/VendorContactLoginController.php
new file mode 100644
index 000000000000..c9cc34802d7f
--- /dev/null
+++ b/app/Http/Controllers/Auth/VendorContactLoginController.php
@@ -0,0 +1,54 @@
+middleware('guest:vendor', ['except' => ['logout']]);
+ }
+
+ public function catch()
+ {
+
+ }
+
+ public function logout()
+ {
+ Auth::guard('vendor')->logout();
+ request()->session()->invalidate();
+
+ return redirect('/vendor');
+ }
+
+
+}
diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php
index e048291d7230..e1bfbf1af4f5 100644
--- a/app/Http/Controllers/BaseController.php
+++ b/app/Http/Controllers/BaseController.php
@@ -84,6 +84,7 @@ class BaseController extends Controller
'company.products.documents',
'company.payments.paymentables',
'company.payments.documents',
+ 'company.purchase_orders.documents',
'company.payment_terms.company',
'company.projects.documents',
'company.recurring_expenses',
@@ -296,6 +297,13 @@ class BaseController extends Controller
if(!$user->hasPermission('view_project'))
$query->where('projects.user_id', $user->id)->orWhere('projects.assigned_user_id', $user->id);
+ },
+ 'company.purchase_orders'=> function ($query) use ($updated_at, $user) {
+ $query->where('updated_at', '>=', $updated_at)->with('documents');
+
+ if(!$user->hasPermission('view_purchase_order'))
+ $query->where('purchase_orders.user_id', $user->id)->orWhere('purchase_orders.assigned_user_id', $user->id);
+
},
'company.quotes'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
@@ -533,6 +541,13 @@ class BaseController extends Controller
if(!$user->hasPermission('view_project'))
$query->where('projects.user_id', $user->id)->orWhere('projects.assigned_user_id', $user->id);
+ },
+ 'company.purchase_orders'=> function ($query) use ($created_at, $user) {
+ $query->where('created_at', '>=', $created_at)->with('documents');
+
+ if(!$user->hasPermission('view_purchase_order'))
+ $query->where('purchase_orders.user_id', $user->id)->orWhere('purchase_orders.assigned_user_id', $user->id);
+
},
'company.quotes'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at)->with('invitations', 'documents');
diff --git a/app/Http/Controllers/SubdomainController.php b/app/Http/Controllers/SubdomainController.php
index 6ad54f007535..63b4812e3ba9 100644
--- a/app/Http/Controllers/SubdomainController.php
+++ b/app/Http/Controllers/SubdomainController.php
@@ -35,6 +35,7 @@ class SubdomainController extends BaseController
'html',
'lb',
'shopify',
+ 'beta',
];
public function __construct()
diff --git a/app/Http/Controllers/VendorPortal/PurchaseOrderController.php b/app/Http/Controllers/VendorPortal/PurchaseOrderController.php
index 4742fcb4d2e9..bbc03e8b7867 100644
--- a/app/Http/Controllers/VendorPortal/PurchaseOrderController.php
+++ b/app/Http/Controllers/VendorPortal/PurchaseOrderController.php
@@ -14,6 +14,7 @@ namespace App\Http\Controllers\VendorPortal;
use App\Events\Misc\InvitationWasViewed;
use App\Events\PurchaseOrder\PurchaseOrderWasViewed;
use App\Http\Controllers\Controller;
+use App\Http\Requests\VendorPortal\PurchaseOrders\ProcessPurchaseOrdersInBulkRequest;
use App\Http\Requests\VendorPortal\PurchaseOrders\ShowPurchaseOrderRequest;
use App\Http\Requests\VendorPortal\PurchaseOrders\ShowPurchaseOrdersRequest;
use App\Models\PurchaseOrder;
@@ -115,4 +116,73 @@ class PurchaseOrderController extends Controller
return $data;
}
+ public function bulk(ProcessPurchaseOrdersInBulkRequest $request)
+ {
+ $transformed_ids = $this->transformKeys($request->purchase_orders);
+
+ if ($request->input('action') == 'download') {
+ return $this->downloadInvoices((array) $transformed_ids);
+ }
+
+ return redirect()
+ ->back()
+ ->with('message', ctrans('texts.no_action_provided'));
+ }
+
+ public function downloadInvoices($ids)
+ {
+
+ $purchase_orders = PurchaseOrder::whereIn('id', $ids)
+ ->where('vendor_id', auth()->guard('vendor')->user()->vendor_id)
+ ->withTrashed()
+ ->get();
+
+ if(count($purchase_orders) == 0)
+ return back()->with(['message' => ctrans('texts.no_items_selected')]);
+
+
+ if(count($purchase_orders) == 1){
+
+ $purchase_order = $purchase_orders->first();
+
+ $file = $purchase_order->service()->getPurchaseOrderPdf(auth()->guard('vendor')->user());
+
+ return response()->streamDownload(function () use($file) {
+ echo Storage::get($file);
+ }, basename($file), ['Content-Type' => 'application/pdf']);
+ }
+
+ return $this->buildZip($purchase_orders);
+ }
+
+
+ private function buildZip($purchase_orders)
+ {
+ // create new archive
+ $zipFile = new \PhpZip\ZipFile();
+ try{
+
+ foreach ($purchase_orders as $purchase_order) {
+
+ #add it to the zip
+ $zipFile->addFromString(basename($purchase_order->pdf_file_path()), file_get_contents($purchase_order->pdf_file_path(null, 'url', true)));
+
+ }
+
+ $filename = date('Y-m-d').'_'.str_replace(' ', '_', trans('texts.purchase_orders')).'.zip';
+ $filepath = sys_get_temp_dir() . '/' . $filename;
+
+ $zipFile->saveAsFile($filepath) // save the archive to a file
+ ->close(); // close archive
+
+ return response()->download($filepath, $filename)->deleteFileAfterSend(true);
+
+ }
+ catch(\PhpZip\Exception\ZipException $e){
+ // handle exception
+ }
+ finally{
+ $zipFile->close();
+ }
+ }
}
diff --git a/app/Http/Requests/VendorPortal/PurchaseOrders/ProcessPurchaseOrdersInBulkRequest.php b/app/Http/Requests/VendorPortal/PurchaseOrders/ProcessPurchaseOrdersInBulkRequest.php
new file mode 100644
index 000000000000..88244f1edc6e
--- /dev/null
+++ b/app/Http/Requests/VendorPortal/PurchaseOrders/ProcessPurchaseOrdersInBulkRequest.php
@@ -0,0 +1,31 @@
+guard('vendor')->user()->vendor->company->enabled_modules & PortalComposer::MODULE_PURCHASE_ORDERS;
+ }
+
+ public function rules()
+ {
+ return [
+ 'purchase_orders' => ['array'],
+ ];
+ }
+}
diff --git a/app/Jobs/Vendor/CreatePurchaseOrderPdf.php b/app/Jobs/Vendor/CreatePurchaseOrderPdf.php
index 2b601a8d1fd5..fde78e220368 100644
--- a/app/Jobs/Vendor/CreatePurchaseOrderPdf.php
+++ b/app/Jobs/Vendor/CreatePurchaseOrderPdf.php
@@ -70,7 +70,7 @@ class CreatePurchaseOrderPdf implements ShouldQueue
*
* @param $invitation
*/
- public function __construct($invitation, $disk = 'public')
+ public function __construct($invitation, $disk = null)
{
$this->invitation = $invitation;
$this->company = $invitation->company;
@@ -83,7 +83,7 @@ class CreatePurchaseOrderPdf implements ShouldQueue
$this->vendor = $invitation->contact->vendor;
$this->vendor->load('company');
- $this->disk = Ninja::isHosted() ? config('filesystems.default') : $disk;
+ $this->disk = $disk ?? config('filesystems.default');
}
diff --git a/app/Models/Company.php b/app/Models/Company.php
index 4c02942c854e..10977900fe42 100644
--- a/app/Models/Company.php
+++ b/app/Models/Company.php
@@ -14,6 +14,7 @@ namespace App\Models;
use App\DataMapper\CompanySettings;
use App\Models\Language;
use App\Models\Presenters\CompanyPresenter;
+use App\Models\PurchaseOrder;
use App\Models\User;
use App\Services\Notification\NotificationService;
use App\Utils\Ninja;
@@ -192,6 +193,11 @@ class Company extends BaseModel
return $this->hasMany(Subscription::class)->withTrashed();
}
+ public function purchase_orders()
+ {
+ return $this->hasMany(PurchaseOrder::class)->withTrashed();
+ }
+
public function task_statuses()
{
return $this->hasMany(TaskStatus::class)->withTrashed();
diff --git a/app/Models/PurchaseOrder.php b/app/Models/PurchaseOrder.php
index 326f8b9791f5..70419412e960 100644
--- a/app/Models/PurchaseOrder.php
+++ b/app/Models/PurchaseOrder.php
@@ -101,7 +101,7 @@ class PurchaseOrder extends BaseModel
const STATUS_DRAFT = 1;
const STATUS_SENT = 2;
- const STATUS_APPROVED = 3;
+ const STATUS_ACCEPTED = 3;
const STATUS_CANCELLED = 4;
public static function stringStatus(int $status)
@@ -113,8 +113,8 @@ class PurchaseOrder extends BaseModel
case self::STATUS_SENT:
return ctrans('texts.sent');
break;
- case self::STATUS_APPROVED:
- return ctrans('texts.approved');
+ case self::STATUS_ACCEPTED:
+ return ctrans('texts.accepted');
break;
case self::STATUS_CANCELLED:
return ctrans('texts.cancelled');
@@ -134,8 +134,8 @@ class PurchaseOrder extends BaseModel
case self::STATUS_SENT:
return '
'.ctrans('texts.sent').'
';
break;
- case self::STATUS_APPROVED:
- return ''.ctrans('texts.approved').'
';
+ case self::STATUS_ACCEPTED:
+ return ''.ctrans('texts.accepted').'
';
break;
case self::STATUS_CANCELLED:
return ''.ctrans('texts.cancelled').'
';
diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php
index a0a90deb0363..234392313b31 100644
--- a/app/Services/Invoice/InvoiceService.php
+++ b/app/Services/Invoice/InvoiceService.php
@@ -341,7 +341,7 @@ class InvoiceService
if(Storage::disk(config('filesystems.default'))->exists($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf'))
Storage::disk(config('filesystems.default'))->delete($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf');
- if(Ninja::isHosted() && Storage::disk(config('filesystems.default'))->exists($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf')) {
+ if(Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf')) {
Storage::disk('public')->delete($this->invoice->client->invoice_filepath($invitation) . $this->invoice->numberFormatter().'.pdf');
}
diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php
index e1a9ee0d13eb..554e33ab27f2 100644
--- a/app/Transformers/CompanyTransformer.php
+++ b/app/Transformers/CompanyTransformer.php
@@ -29,6 +29,7 @@ use App\Models\Payment;
use App\Models\PaymentTerm;
use App\Models\Product;
use App\Models\Project;
+use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\RecurringExpense;
use App\Models\RecurringInvoice;
@@ -39,6 +40,7 @@ use App\Models\TaskStatus;
use App\Models\TaxRate;
use App\Models\User;
use App\Models\Webhook;
+use App\Transformers\PurchaseOrderTransformer;
use App\Transformers\RecurringExpenseTransformer;
use App\Utils\Traits\MakesHash;
use stdClass;
@@ -95,6 +97,7 @@ class CompanyTransformer extends EntityTransformer
'task_statuses',
'subscriptions',
'recurring_expenses',
+ 'purchase_orders',
];
/**
@@ -391,4 +394,11 @@ class CompanyTransformer extends EntityTransformer
return $this->includeCollection($company->subscriptions, $transformer, Subscription::class);
}
+
+ public function includePurchaseOrders(Company $company)
+ {
+ $transformer = new PurchaseOrderTransformer($this->serializer);
+
+ return $this->includeCollection($company->purchase_orders, $transformer, PurchaseOrder::class);
+ }
}
diff --git a/public/js/clients/purchase_orders/action-selectors.js b/public/js/clients/purchase_orders/action-selectors.js
new file mode 100644
index 000000000000..afc05b746e16
--- /dev/null
+++ b/public/js/clients/purchase_orders/action-selectors.js
@@ -0,0 +1,2 @@
+/*! For license information please see action-selectors.js.LICENSE.txt */
+(()=>{function e(e,n){var r="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!r){if(Array.isArray(e)||(r=function(e,n){if(!e)return;if("string"==typeof e)return t(e,n);var r=Object.prototype.toString.call(e).slice(8,-1);"Object"===r&&e.constructor&&(r=e.constructor.name);if("Map"===r||"Set"===r)return Array.from(e);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return t(e,n)}(e))||n&&e&&"number"==typeof e.length){r&&(e=r);var o=0,c=function(){};return{s:c,n:function(){return o>=e.length?{done:!0}:{done:!1,value:e[o++]}},e:function(e){throw e},f:c}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var i,a=!0,l=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return a=e.done,e},e:function(e){l=!0,i=e},f:function(){try{a||null==r.return||r.return()}finally{if(l)throw i}}}}function t(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n2&&void 0!==arguments[2]?arguments[2]:{};if(r.hasOwnProperty("single")&&document.querySelectorAll(".child-hidden-input").forEach((function(e){return e.remove()})),!1!==t.checked){var o=document.createElement("INPUT");o.setAttribute("name","purchase_orders[]"),o.setAttribute("value",t.dataset.value),o.setAttribute("class","child-hidden-input"),o.hidden=!0,n.append(o)}else{var c,i=document.querySelectorAll("input.child-hidden-input"),a=e(i);try{for(a.s();!(c=a.n()).done;){var l=c.value;l.value==t.dataset.value&&l.remove()}}catch(e){a.e(e)}finally{a.f()}}}},{key:"handle",value:function(){var t=this;this.parentElement.addEventListener("click",(function(){t.watchCheckboxes(t.parentElement)}));var n,r=e(document.querySelectorAll(".form-check-child"));try{var o=function(){var e=n.value;e.addEventListener("click",(function(){t.processChildItem(e,t.parentForm)}))};for(r.s();!(n=r.n()).done;)o()}catch(e){r.e(e)}finally{r.f()}}}],o&&n(r.prototype,o),c&&n(r,c),Object.defineProperty(r,"prototype",{writable:!1}),t}())).handle()})();
\ No newline at end of file
diff --git a/public/js/clients/purchase_orders/action-selectors.js.LICENSE.txt b/public/js/clients/purchase_orders/action-selectors.js.LICENSE.txt
new file mode 100644
index 000000000000..82efe1d67ef8
--- /dev/null
+++ b/public/js/clients/purchase_orders/action-selectors.js.LICENSE.txt
@@ -0,0 +1,9 @@
+/**
+ * Invoice Ninja (https://invoiceninja.com)
+ *
+ * @link https://github.com/invoiceninja/invoiceninja source repository
+ *
+ * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
+ *
+ * @license https://www.elastic.co/licensing/elastic-license
+ */
diff --git a/public/mix-manifest.json b/public/mix-manifest.json
index 8f526ac40699..f97b196ea88a 100755
--- a/public/mix-manifest.json
+++ b/public/mix-manifest.json
@@ -4,6 +4,7 @@
"/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=803182f668c39d631ca5c55437876da4",
"/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=7bed15f51bca764378d9a3aa605b8664",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=d4f86ddee4e8a1d6e9719010aa0fe62b",
+ "/js/clients/purchase_orders/action-selectors.js": "/js/clients/purchase_orders/action-selectors.js?id=160b8161599fc2429b449b0970d3ba6c",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=b88ad7c8881cc87df07b129c5a7c76df",
"/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js?id=1c5493a4c53a5b862d07ee1818179ea9",
"/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js?id=0274ab4f8d2b411f2a2fe5142301e7af",
diff --git a/resources/js/clients/purchase_orders/action-selectors.js b/resources/js/clients/purchase_orders/action-selectors.js
new file mode 100644
index 000000000000..360261e92b91
--- /dev/null
+++ b/resources/js/clients/purchase_orders/action-selectors.js
@@ -0,0 +1,79 @@
+/**
+ * Invoice Ninja (https://invoiceninja.com)
+ *
+ * @link https://github.com/invoiceninja/invoiceninja source repository
+ *
+ * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
+ *
+ * @license https://www.elastic.co/licensing/elastic-license
+ */
+
+class ActionSelectors {
+ constructor() {
+ this.parentElement = document.querySelector('.form-check-parent');
+ this.parentForm = document.getElementById('bulkActions');
+ }
+
+ watchCheckboxes(parentElement) {
+ document
+ .querySelectorAll('.child-hidden-input')
+ .forEach((element) => element.remove());
+
+ document.querySelectorAll('.form-check-child').forEach((child) => {
+ if (parentElement.checked) {
+ child.checked = parentElement.checked;
+ this.processChildItem(
+ child,
+ document.getElementById('bulkActions')
+ );
+ } else {
+ child.checked = false;
+ document
+ .querySelectorAll('.child-hidden-input')
+ .forEach((element) => element.remove());
+ }
+ });
+ }
+
+ processChildItem(element, parent, options = {}) {
+ if (options.hasOwnProperty('single')) {
+ document
+ .querySelectorAll('.child-hidden-input')
+ .forEach((element) => element.remove());
+ }
+
+ if (element.checked === false) {
+ let inputs = document.querySelectorAll('input.child-hidden-input');
+
+ for (let i of inputs) {
+ if (i.value == element.dataset.value) i.remove();
+ }
+
+ return;
+ }
+
+ let _temp = document.createElement('INPUT');
+
+ _temp.setAttribute('name', 'purchase_orders[]');
+ _temp.setAttribute('value', element.dataset.value);
+ _temp.setAttribute('class', 'child-hidden-input');
+ _temp.hidden = true;
+
+ parent.append(_temp);
+ }
+
+ handle() {
+ this.parentElement.addEventListener('click', () => {
+ this.watchCheckboxes(this.parentElement);
+ });
+
+ for (let child of document.querySelectorAll('.form-check-child')) {
+ child.addEventListener('click', () => {
+ this.processChildItem(child, this.parentForm);
+ });
+ }
+ }
+}
+
+/** @handle **/
+new ActionSelectors().handle();
diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php
index 64cc99d50e0c..00a578f1470d 100644
--- a/resources/lang/en/texts.php
+++ b/resources/lang/en/texts.php
@@ -4628,7 +4628,7 @@ $LANG = array(
'purchase_order_date' => 'Purchase Order Date',
'purchase_orders' => 'Purchase Orders',
'purchase_order_number_placeholder' => 'Purchase Order # :purchase_order',
-
+ 'accepted' => 'Accepted',
);
return $LANG;
diff --git a/resources/views/portal/ninja2020/components/livewire/purchase-orders-table.blade.php b/resources/views/portal/ninja2020/components/livewire/purchase-orders-table.blade.php
index 8907f454e83d..237003fcbff7 100644
--- a/resources/views/portal/ninja2020/components/livewire/purchase-orders-table.blade.php
+++ b/resources/views/portal/ninja2020/components/livewire/purchase-orders-table.blade.php
@@ -121,5 +121,5 @@
@push('footer')
-
+
@endpush
diff --git a/resources/views/portal/ninja2020/purchase_orders/catch.blade.php b/resources/views/portal/ninja2020/purchase_orders/catch.blade.php
new file mode 100644
index 000000000000..f3a8543df73a
--- /dev/null
+++ b/resources/views/portal/ninja2020/purchase_orders/catch.blade.php
@@ -0,0 +1,18 @@
+@extends('portal.ninja2020.layout.vendor_app')
+@section('meta_title', ctrans('texts.purchase_orders'))
+
+@section('header')
+ @if($errors->any())
+
+ @foreach($errors->all() as $error)
+
{{ $error }}
+ @endforeach
+
+ @endif
+@endsection
+
+@section('body')
+
+
Vendor Portal
+
+@endsection
diff --git a/routes/vendor.php b/routes/vendor.php
index 575daaeea9d0..22a5235e173c 100644
--- a/routes/vendor.php
+++ b/routes/vendor.php
@@ -9,10 +9,13 @@
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
+use App\Http\Controllers\Auth\VendorContactLoginController;
use App\Http\Controllers\VendorPortal\InvitationController;
use App\Http\Controllers\VendorPortal\PurchaseOrderController;
use Illuminate\Support\Facades\Route;
+Route::get('vendor', [VendorContactLoginController::class, 'catch'])->name('vendor.catchall')->middleware(['domain_db', 'contact_account','vendor_locale']); //catch all
+
Route::group(['middleware' => ['invite_db'], 'prefix' => 'vendor', 'as' => 'vendor.'], function () {
/*Invitation catches*/
Route::get('purchase_order/{invitation_key}', [InvitationController::class, 'purchaseOrder']);
@@ -30,5 +33,6 @@ Route::group(['middleware' => ['auth:vendor', 'vendor_locale', 'domain_db'], 'pr
Route::get('profile/{vendor_contact}/edit', [PurchaseOrderController::class, 'index'])->name('profile.edit');
Route::post('invoices/payment', [PurchaseOrderController::class, 'bulk'])->name('purchase_orders.bulk');
+ Route::get('logout', [VendorContactLoginController::class, 'logout'])->name('logout');
});
\ No newline at end of file
diff --git a/webpack.mix.js b/webpack.mix.js
index 9f0c809188b5..a6e0ebb55a36 100644
--- a/webpack.mix.js
+++ b/webpack.mix.js
@@ -18,6 +18,10 @@ mix.js("resources/js/app.js", "public/js")
"resources/js/clients/invoices/action-selectors.js",
"public/js/clients/invoices/action-selectors.js"
)
+ .js(
+ "resources/js/clients/purchase_orders/action-selectors.js",
+ "public/js/clients/purchase_orders/action-selectors.js"
+ )
.js(
"resources/js/clients/invoices/payment.js",
"public/js/clients/invoices/payment.js"