introduce guest checkout configuration
This commit is contained in:
parent
840ae7b2d8
commit
132dd6c0f0
10
package.json
10
package.json
|
|
@ -13,12 +13,16 @@
|
|||
"devDependencies": {
|
||||
"axios": "^0.18",
|
||||
"bootstrap": "^4.0.0",
|
||||
"popper.js": "^1.12",
|
||||
"cross-env": "^5.1",
|
||||
"jquery": "^3.2",
|
||||
"laravel-mix": "^2.0",
|
||||
"laravel-mix": "^5.0.1",
|
||||
"lodash": "^4.17.4",
|
||||
"vue": "^2.5.7"
|
||||
"popper.js": "^1.12",
|
||||
"resolve-url-loader": "^3.1.0",
|
||||
"sass": "^1.24.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"vue": "^2.5.7",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"opencollective-postinstall": "^2.0.1",
|
||||
|
|
|
|||
|
|
@ -87,10 +87,21 @@ return [
|
|||
'key' => 'catalog.products',
|
||||
'name' => 'admin::app.admin.system.products',
|
||||
'sort' => 2
|
||||
], [
|
||||
'key' => 'catalog.products.guest-checkout',
|
||||
'name' => 'admin::app.admin.system.guest-checkout',
|
||||
'sort' => 1,
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'allow-guest-checkout',
|
||||
'title' => 'admin::app.admin.system.allow-guest-checkout',
|
||||
'type' => 'boolean'
|
||||
]
|
||||
]
|
||||
], [
|
||||
'key' => 'catalog.products.review',
|
||||
'name' => 'admin::app.admin.system.review',
|
||||
'sort' => 1,
|
||||
'sort' => 2,
|
||||
'fields' => [
|
||||
[
|
||||
'name' => 'guest_review',
|
||||
|
|
|
|||
|
|
@ -1202,6 +1202,9 @@ return [
|
|||
'system' => [
|
||||
'catalog' => 'Catalog',
|
||||
'products' => 'Products',
|
||||
'guest-checkout' => 'Guest Checkout',
|
||||
'allow-guest-checkout' => 'Allow Guest Checkout',
|
||||
'allow-guest-checkout-hint' => 'Hint: If turned on, this option can be configured for each product specifically.',
|
||||
'review' => 'Review',
|
||||
'allow-guest-review' => 'Allow Guest Review',
|
||||
'inventory' => 'Inventory',
|
||||
|
|
|
|||
|
|
@ -77,6 +77,10 @@
|
|||
@foreach ($customAttributes as $attribute)
|
||||
|
||||
<?php
|
||||
if ($attribute->code == 'guest_checkout' && ! core()->getConfigData('catalog.products.guest-checkout.allow-guest-checkout')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$validations = [];
|
||||
|
||||
if ($attribute->is_required) {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,11 @@
|
|||
|
||||
@include ('admin::configuration.field-type', ['field' => $field])
|
||||
|
||||
@php ($hint = $field['title'] . '-hint')
|
||||
@if ($hint !== __($hint))
|
||||
{{ __($hint) }}
|
||||
@endif
|
||||
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,16 +3,20 @@
|
|||
namespace Webkul\Attribute\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use DB;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AttributeFamilyTableSeeder extends Seeder
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||
|
||||
DB::table('attribute_families')->delete();
|
||||
|
||||
DB::table('attribute_families')->insert([
|
||||
['id' => '1','code' => 'default','name' => 'Default','status' => '0','is_user_defined' => '1']
|
||||
]);
|
||||
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,17 @@
|
|||
namespace Webkul\Attribute\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use DB;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AttributeGroupTableSeeder extends Seeder
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||
|
||||
DB::table('attribute_groups')->delete();
|
||||
DB::table('attribute_group_mappings')->delete();
|
||||
|
||||
DB::table('attribute_groups')->delete();
|
||||
|
||||
DB::table('attribute_groups')->insert([
|
||||
|
|
@ -42,9 +47,12 @@ class AttributeGroupTableSeeder extends Seeder
|
|||
['attribute_id' => '20','attribute_group_id' => '5','position' => '2'],
|
||||
['attribute_id' => '21','attribute_group_id' => '5','position' => '3'],
|
||||
['attribute_id' => '22','attribute_group_id' => '5','position' => '4'],
|
||||
['attribute_id' => '23','attribute_group_id' => '1','position' => '9'],
|
||||
['attribute_id' => '24','attribute_group_id' => '1','position' => '10'],
|
||||
['attribute_id' => '25','attribute_group_id' => '1','position' => '11']
|
||||
['attribute_id' => '23','attribute_group_id' => '1','position' => '10'],
|
||||
['attribute_id' => '24','attribute_group_id' => '1','position' => '11'],
|
||||
['attribute_id' => '25','attribute_group_id' => '1','position' => '12'],
|
||||
['attribute_id' => '26','attribute_group_id' => '1','position' => '9']
|
||||
]);
|
||||
|
||||
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
namespace Webkul\Attribute\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use DB;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AttributeOptionTableSeeder extends Seeder
|
||||
{
|
||||
|
|
@ -11,6 +11,7 @@ class AttributeOptionTableSeeder extends Seeder
|
|||
public function run()
|
||||
{
|
||||
DB::table('attribute_options')->delete();
|
||||
DB::table('attribute_option_translations')->delete();
|
||||
|
||||
DB::table('attribute_options')->insert([
|
||||
['id' => '1', 'admin_name' => 'Red', 'sort_order' => '1', 'attribute_id' => '23'],
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
namespace Webkul\Attribute\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use DB;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AttributeTableSeeder extends Seeder
|
||||
{
|
||||
|
|
@ -12,6 +12,7 @@ class AttributeTableSeeder extends Seeder
|
|||
public function run()
|
||||
{
|
||||
DB::table('attributes')->delete();
|
||||
DB::table('attribute_translations')->delete();
|
||||
|
||||
$now = Carbon::now();
|
||||
|
||||
|
|
@ -59,9 +60,11 @@ class AttributeTableSeeder extends Seeder
|
|||
['id' => '23','code' => 'color','admin_name' => 'Color','type' => 'select','validation' => NULL,'position' => '23','is_required' => '0','is_unique' => '0','value_per_locale' => '0','value_per_channel' => '0','is_filterable' => '1','is_configurable' => '1','is_user_defined' => '1','is_visible_on_front' => '0',
|
||||
'use_in_flat' => '1','created_at' => $now,'updated_at' => $now],
|
||||
['id' => '24','code' => 'size','admin_name' => 'Size','type' => 'select','validation' => NULL,'position' => '24','is_required' => '0','is_unique' => '0','value_per_locale' => '0','value_per_channel' => '0','is_filterable' => '1','is_configurable' => '1','is_user_defined' => '1','is_visible_on_front' => '0',
|
||||
'use_in_flat' => '1','created_at' => $now,'updated_at' => $now],
|
||||
'use_in_flat' => '1','created_at' => $now,'updated_at' => $now],
|
||||
['id' => '25','code' => 'brand','admin_name' => 'Brand','type' => 'select','validation' => NULL,'position' => '25','is_required' => '0','is_unique' => '0','value_per_locale' => '0','value_per_channel' => '0','is_filterable' => '1','is_configurable' => '0','is_user_defined' => '0','is_visible_on_front' => '1',
|
||||
'use_in_flat' => '1','created_at' => $now,'updated_at' => $now]
|
||||
'use_in_flat' => '1','created_at' => $now,'updated_at' => $now],
|
||||
['id' => '26','code' => 'guest_checkout','admin_name' => 'Guest Checkout','type' => 'boolean','validation' => NULL,'position' => '8','is_required' => '1','is_unique' => '0','value_per_locale' => '0','value_per_channel' => '0','is_filterable' => '0','is_configurable' => '0','is_user_defined' => '0','is_visible_on_front' => '0',
|
||||
'use_in_flat' => '1','created_at' => $now,'updated_at' => $now],
|
||||
]);
|
||||
|
||||
|
||||
|
|
@ -90,7 +93,8 @@ class AttributeTableSeeder extends Seeder
|
|||
['id' => '22','locale' => 'en','name' => 'Weight','attribute_id' => '22'],
|
||||
['id' => '23','locale' => 'en','name' => 'Color','attribute_id' => '23'],
|
||||
['id' => '24','locale' => 'en','name' => 'Size','attribute_id' => '24'],
|
||||
['id' => '25','locale' => 'en','name' => 'Brand','attribute_id' => '25']
|
||||
['id' => '25','locale' => 'en','name' => 'Brand','attribute_id' => '25'],
|
||||
['id' => '26','locale' => 'en','name' => 'Allow Guest Checkout','attribute_id' => '26']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,9 +13,9 @@ class DatabaseSeeder extends Seeder
|
|||
*/
|
||||
public function run()
|
||||
{
|
||||
$this->call(AttributeTableSeeder::class);
|
||||
$this->call(AttributeOptionTableSeeder::class);
|
||||
$this->call(AttributeFamilyTableSeeder::class);
|
||||
$this->call(AttributeGroupTableSeeder::class);
|
||||
$this->call(AttributeTableSeeder::class);
|
||||
$this->call(AttributeOptionTableSeeder::class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,16 @@
|
|||
|
||||
namespace Webkul\CMS\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use DB;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CMSPagesTableSeeder extends Seeder
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
DB::table('cms_pages')->delete();
|
||||
DB::table('cms_page_translations')->delete();
|
||||
|
||||
DB::table('cms_pages')->insert([
|
||||
[
|
||||
|
|
|
|||
|
|
@ -116,11 +116,11 @@ class Cart extends Model implements CartContract
|
|||
}
|
||||
|
||||
/**
|
||||
* Checks if cart have downloadable items
|
||||
* Checks if cart has downloadable items
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function haveDownloadableItems()
|
||||
public function hasDownloadableItems()
|
||||
{
|
||||
foreach ($this->items as $item) {
|
||||
if ($item->type == 'downloadable')
|
||||
|
|
@ -129,4 +129,20 @@ class Cart extends Model implements CartContract
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if cart has items that allow guest checkout
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasGuestCheckoutItems()
|
||||
{
|
||||
foreach ($this->items as $item) {
|
||||
if ($item->product->getAttribute('guest_checkout') === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace Webkul\Core\Database\Seeders;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ConfigTableSeeder extends Seeder
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
DB::table('core_config')->delete();
|
||||
|
||||
$now = Carbon::now();
|
||||
|
||||
DB::table('core_config')->insert([
|
||||
'id' => 1,
|
||||
'code' => 'catalog.products.guest-checkout.allow-guest-checkout',
|
||||
'value' => '1',
|
||||
'channel_code' => null,
|
||||
'locale_code' => null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,5 +18,6 @@ class DatabaseSeeder extends Seeder
|
|||
$this->call(CountriesTableSeeder::class);
|
||||
$this->call(StatesTableSeeder::class);
|
||||
$this->call(ChannelTableSeeder::class);
|
||||
$this->call(ConfigTableSeeder::class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
namespace Webkul\Core\Helpers;
|
||||
|
||||
// here you can define custom actions
|
||||
// all public methods declared in helper class will be available in $I
|
||||
|
||||
use Codeception\Module\Laravel5;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Webkul\Product\Models\Product;
|
||||
use Webkul\Product\Models\ProductAttributeValue;
|
||||
use Webkul\Product\Models\ProductInventory;
|
||||
|
||||
class Laravel5Helper extends Laravel5
|
||||
{
|
||||
/**
|
||||
* Returns field name of given attribute.
|
||||
*
|
||||
* @param string $attribute
|
||||
*
|
||||
* @return string|null
|
||||
* @part ORM
|
||||
*/
|
||||
public static function getAttributeFieldName(string $attribute): ?string
|
||||
{
|
||||
$attributes = [
|
||||
'product_id' => 'integer_value',
|
||||
'sku' => 'text_value',
|
||||
'name' => 'text_value',
|
||||
'url_key' => 'text_value',
|
||||
'tax_category_id' => 'integer_value',
|
||||
'new' => 'boolean_value',
|
||||
'featured' => 'boolean_value',
|
||||
'visible_individually' => 'boolean_value',
|
||||
'status' => 'boolean_value',
|
||||
'short_description' => 'text_value',
|
||||
'description' => 'text_value',
|
||||
'price' => 'float_value',
|
||||
'cost' => 'float_value',
|
||||
'special_price' => 'float_value',
|
||||
'special_price_from' => 'date_value',
|
||||
'special_price_to' => 'date_value',
|
||||
'meta_title' => 'text_value',
|
||||
'meta_keywords' => 'text_value',
|
||||
'meta_description' => 'text_value',
|
||||
'width' => 'integer_value',
|
||||
'height' => 'integer_value',
|
||||
'depth' => 'integer_value',
|
||||
'weight' => 'integer_value',
|
||||
'color' => 'integer_value',
|
||||
'size' => 'integer_value',
|
||||
'brand' => 'text_value',
|
||||
'guest_checkout' => 'boolean_value',
|
||||
];
|
||||
if (!array_key_exists($attribute, $attributes)) {
|
||||
return null;
|
||||
}
|
||||
return $attributes[$attribute];
|
||||
}
|
||||
/**
|
||||
* @param array $attributeValueStates
|
||||
*
|
||||
* @return \Webkul\Product\Models\Product
|
||||
* @part ORM
|
||||
*/
|
||||
public function haveProduct(
|
||||
array $configs = [],
|
||||
array $productStates = []
|
||||
): Product {
|
||||
$I = $this;
|
||||
/** @var Product $product */
|
||||
$product = factory(Product::class)->states($productStates)->create($configs['productAttributes'] ?? []);;
|
||||
$I->createAttributeValues($product->id,$configs['attributeValues'] ?? []);
|
||||
$I->have(ProductInventory::class, array_merge($configs['productInventory'] ?? [], [
|
||||
'product_id' => $product->id,
|
||||
'inventory_source_id' => 1,
|
||||
]));
|
||||
Event::fire('catalog.product.create.after', $product);
|
||||
return $product;
|
||||
}
|
||||
private function createAttributeValues($id, array $attributeValues = [])
|
||||
{
|
||||
$I = $this;
|
||||
$productAttributeValues = [
|
||||
'sku',
|
||||
'url_key',
|
||||
'tax_category_id',
|
||||
'price',
|
||||
'cost',
|
||||
'name',
|
||||
'new',
|
||||
'visible_individually',
|
||||
'featured',
|
||||
'status',
|
||||
'guest_checkout',
|
||||
'short_description',
|
||||
'description',
|
||||
'meta_title',
|
||||
'meta_keywords',
|
||||
'meta_description',
|
||||
'weight',
|
||||
];
|
||||
foreach ($productAttributeValues as $attribute) {
|
||||
$data = ['product_id' => $id];
|
||||
if (array_key_exists($attribute, $attributeValues)) {
|
||||
$fieldName = self::getAttributeFieldName($attribute);
|
||||
if (! array_key_exists($fieldName, $data)) {
|
||||
$data[$fieldName] = $attributeValues[$attribute];
|
||||
} else {
|
||||
$data = [$fieldName => $attributeValues[$attribute]];
|
||||
}
|
||||
}
|
||||
$I->have(ProductAttributeValue::class, $data, $attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Factory $factory */
|
||||
use Faker\Generator as Faker;
|
||||
use Webkul\Inventory\Models\InventorySource;
|
||||
|
||||
$factory->define(InventorySource::class, function (Faker $faker) {
|
||||
$now = date("Y-m-d H:i:s");
|
||||
$code = $faker->unique()->word;
|
||||
return [
|
||||
'code' => $faker->unique()->word,
|
||||
'name' => $code,
|
||||
'description' => $faker->sentence,
|
||||
'contact_name' => $faker->name,
|
||||
'contact_email' => $faker->safeEmail,
|
||||
'contact_number' => $faker->phoneNumber,
|
||||
'country' => $faker->countryCode,
|
||||
'state' => $faker->state,
|
||||
'city' => $faker->city,
|
||||
'street' => $faker->streetAddress,
|
||||
'postcode' => $faker->postcode,
|
||||
'priority' => 0,
|
||||
'status' => 1,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
});
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
<?php
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Factory $factory */
|
||||
|
||||
use Faker\Generator as Faker;
|
||||
use Webkul\Product\Models\Product;
|
||||
use Webkul\Product\Models\ProductAttributeValue;
|
||||
use Webkul\Attribute\Models\AttributeOption;
|
||||
|
||||
$factory->defineAs(ProductAttributeValue::class, 'sku', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'text_value' => $faker->uuid,
|
||||
'attribute_id' => 1,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'name', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'locale' => 'en', //$faker->languageCode,
|
||||
'channel' => 'default',
|
||||
'text_value' => $faker->words(2, true),
|
||||
'attribute_id' => 2,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'url_key', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'text_value' => $faker->unique()->slug,
|
||||
'attribute_id' => 3,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'tax_category_id', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'channel' => 'default',
|
||||
'integer_value' => null, // ToDo
|
||||
'attribute_id' => 4,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'new', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'boolean_value' => 1,
|
||||
'attribute_id' => 5,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'featured', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'boolean_value' => 1,
|
||||
'attribute_id' => 6,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'visible_individually', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'boolean_value' => 1,
|
||||
'attribute_id' => 7,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'status', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'boolean_value' => 1,
|
||||
'attribute_id' => 8,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'short_description', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'locale' => 'en', //$faker->languageCode,
|
||||
'channel' => 'default',
|
||||
'text_value' => $faker->sentence,
|
||||
'attribute_id' => 9,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'description', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'locale' => 'en', //$faker->languageCode,
|
||||
'channel' => 'default',
|
||||
'text_value' => $faker->sentences(3, true),
|
||||
'attribute_id' => 10,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'price', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'float_value' => $faker->randomFloat(4, 0, 1000),
|
||||
'attribute_id' => 11,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'cost', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'channel' => 'default',
|
||||
'float_value' => $faker->randomFloat(4, 0, 10),
|
||||
'attribute_id' => 12,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'special_price', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'float_value' => $faker->randomFloat(4, 0, 100),
|
||||
'attribute_id' => 13,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'special_price_from', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'channel' => 'default',
|
||||
'date_value' => $faker->dateTimeBetween('-5 days', 'now', 'Europe/Berlin'),
|
||||
'attribute_id' => 14,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'special_price_to', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'channel' => 'default',
|
||||
'date_value' => $faker->dateTimeBetween('now', '+ 5 days', 'Europe/Berlin'),
|
||||
'attribute_id' => 15,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'meta_title', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'locale' => 'en', //$faker->languageCode,
|
||||
'channel' => 'default',
|
||||
'text_value' => $faker->words(2, true),
|
||||
'attribute_id' => 16,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'meta_keywords', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'locale' => 'en', //$faker->languageCode,
|
||||
'channel' => 'default',
|
||||
'text_value' => $faker->words(5, true),
|
||||
'attribute_id' => 17,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'meta_description', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'locale' => 'en', //$faker->languageCode,
|
||||
'channel' => 'default',
|
||||
'text_value' => $faker->sentence,
|
||||
'attribute_id' => 18,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'width', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'integer_value' => $faker->numberBetween(1, 50),
|
||||
'attribute_id' => 19,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'height', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'integer_value' => $faker->numberBetween(1, 50),
|
||||
'attribute_id' => 20,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'depth', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'integer_value' => $faker->numberBetween(1, 50),
|
||||
'attribute_id' => 21,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'weight', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'integer_value' => $faker->numberBetween(1, 50),
|
||||
'attribute_id' => 22,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'color', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'integer_value' => $faker->numberBetween(1, 5),
|
||||
'attribute_id' => 23,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'size', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'integer_value' => $faker->numberBetween(1, 5),
|
||||
'attribute_id' => 24,
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'brand', function (Faker $faker) {
|
||||
return [
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'attribute_id' => 25,
|
||||
'integer_value' => function () {
|
||||
return factory(AttributeOption::class)->create()->id;
|
||||
},
|
||||
];
|
||||
});
|
||||
$factory->defineAs(ProductAttributeValue::class, 'guest_checkout', function ( Faker $faker) {
|
||||
return [
|
||||
'product_id' => function() {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'boolean_value' => 1,
|
||||
'attribute_id' => 26,
|
||||
];
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Factory $factory */
|
||||
|
||||
use Faker\Generator as Faker;
|
||||
use Webkul\Product\Models\Product;
|
||||
|
||||
$factory->define(Product::class, function (Faker $faker) {
|
||||
$now = date("Y-m-d H:i:s");
|
||||
return [
|
||||
'sku' => $faker->uuid,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
'attribute_family_id' => 1,
|
||||
];
|
||||
});
|
||||
|
||||
$factory->state(Product::class, 'simple', [
|
||||
'type' => 'simple',
|
||||
]);
|
||||
$factory->state(Product::class, 'downloadable_with_stock', [
|
||||
'type' => 'downloadable',
|
||||
]);
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Factory $factory */
|
||||
|
||||
use Faker\Generator as Faker;
|
||||
use Webkul\Inventory\Models\InventorySource;
|
||||
use Webkul\Product\Models\Product;
|
||||
use Webkul\Product\Models\ProductInventory;
|
||||
|
||||
$factory->define(ProductInventory::class, function (Faker $faker) {
|
||||
return [
|
||||
'qty' => $faker->numberBetween(1, 20),
|
||||
'product_id' => function () {
|
||||
return factory(Product::class)->create()->id;
|
||||
},
|
||||
'inventory_source_id' => function () {
|
||||
return factory(InventorySource::class)->create()->id;
|
||||
},
|
||||
];
|
||||
});
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Webkul\Product\Providers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factory as EloquentFactory;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Webkul\Product\Models\ProductProxy;
|
||||
use Webkul\Product\Observers\ProductObserver;
|
||||
|
|
@ -32,14 +33,21 @@ class ProductServiceProvider extends ServiceProvider
|
|||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
public function register(): void
|
||||
{
|
||||
$this->registerConfig();
|
||||
|
||||
$this->registerCommands();
|
||||
|
||||
$this->registerEloquentFactoriesFrom(__DIR__ . '/../Database/Factories');
|
||||
}
|
||||
|
||||
public function registerConfig() {
|
||||
/**
|
||||
* Register Configuration
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function registerConfig(): void {
|
||||
$this->mergeConfigFrom(
|
||||
dirname(__DIR__) . '/Config/product_types.php', 'product_types'
|
||||
);
|
||||
|
|
@ -47,10 +55,24 @@ class ProductServiceProvider extends ServiceProvider
|
|||
|
||||
/**
|
||||
* Register the console commands of this package
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerCommands()
|
||||
protected function registerCommands(): void
|
||||
{
|
||||
if ($this->app->runningInConsole())
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([PriceUpdate::class,]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register factories.
|
||||
*
|
||||
* @param string $path
|
||||
* @return void
|
||||
*/
|
||||
protected function registerEloquentFactoriesFrom($path): void
|
||||
{
|
||||
$this->app->make(EloquentFactory::class)->load($path);
|
||||
}
|
||||
}
|
||||
|
|
@ -22,14 +22,14 @@ class Downloadable extends AbstractType
|
|||
{
|
||||
/**
|
||||
* ProductDownloadableLinkRepository instance
|
||||
*
|
||||
*
|
||||
* @var ProductDownloadableLinkRepository
|
||||
*/
|
||||
protected $productDownloadableLinkRepository;
|
||||
|
||||
/**
|
||||
* ProductDownloadableSampleRepository instance
|
||||
*
|
||||
*
|
||||
* @var ProductDownloadableSampleRepository
|
||||
*/
|
||||
protected $productDownloadableSampleRepository;
|
||||
|
|
@ -39,11 +39,11 @@ class Downloadable extends AbstractType
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $skipAttributes = ['width', 'height', 'depth', 'weight'];
|
||||
protected $skipAttributes = ['width', 'height', 'depth', 'weight', 'guest_checkout'];
|
||||
|
||||
/**
|
||||
* These blade files will be included in product edit page
|
||||
*
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $additionalViews = [
|
||||
|
|
@ -111,7 +111,7 @@ class Downloadable extends AbstractType
|
|||
|
||||
if (request()->route()->getName() != 'admin.catalog.products.massupdate') {
|
||||
$this->productDownloadableLinkRepository->saveLinks($data, $product);
|
||||
|
||||
|
||||
$this->productDownloadableSampleRepository->saveSamples($data, $product);
|
||||
}
|
||||
|
||||
|
|
@ -127,11 +127,11 @@ class Downloadable extends AbstractType
|
|||
{
|
||||
if (! $this->product->status)
|
||||
return false;
|
||||
|
||||
|
||||
if ($this->product->downloadable_links()->count())
|
||||
return true;
|
||||
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -177,7 +177,7 @@ class Downloadable extends AbstractType
|
|||
|
||||
return $products;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $options1
|
||||
|
|
|
|||
|
|
@ -64,13 +64,22 @@ class OnepageController extends Controller
|
|||
*/
|
||||
public function index()
|
||||
{
|
||||
if (! auth()->guard('customer')->check() && ! core()->getConfigData('catalog.products.guest-checkout.allow-guest-checkout')) {
|
||||
return redirect()->route('customer.session.index');
|
||||
}
|
||||
|
||||
if (Cart::hasError())
|
||||
return redirect()->route('shop.checkout.cart.index');
|
||||
|
||||
$cart = Cart::getCart();
|
||||
|
||||
if (! auth()->guard('customer')->check() && $cart->haveDownloadableItems())
|
||||
if (! auth()->guard('customer')->check() && $cart->hasDownloadableItems()) {
|
||||
return redirect()->route('customer.session.index');
|
||||
}
|
||||
|
||||
if (! auth()->guard('customer')->check() && ! $cart->hasGuestCheckoutItems()) {
|
||||
return redirect()->route('customer.session.index');
|
||||
}
|
||||
|
||||
Cart::collectTotals();
|
||||
|
||||
|
|
|
|||
|
|
@ -23,4 +23,20 @@ class AcceptanceTester extends \Codeception\Actor
|
|||
/**
|
||||
* Define custom actions here
|
||||
*/
|
||||
|
||||
/**
|
||||
* Logging in as an Admin
|
||||
*/
|
||||
public function loginAsAdmin()
|
||||
{
|
||||
$I = $this;
|
||||
$I->amOnPage('/admin');
|
||||
$I->see('Sign In');
|
||||
$I->fillField('email', 'admin@example.com');
|
||||
$I->fillField('password', 'admin123');
|
||||
$I->dontSee('The "Email" field is required.');
|
||||
$I->dontSee('The "Password" field is required.');
|
||||
$I->click('Sign In');
|
||||
$I->see('Dashboard', '//h1');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use Illuminate\Routing\RouteCollection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Webkul\User\Models\Admin;
|
||||
|
||||
|
|
@ -58,4 +59,26 @@ class FunctionalTester extends \Codeception\Actor
|
|||
$I->assertContains('admin', $middlewares, 'check that admin middleware is applied');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set specific Webkul/Core configuration keys to a given value
|
||||
*
|
||||
* // TODO: change method as soon as there is a method to set core config data
|
||||
*
|
||||
* @param $data array containing 'code => value' pairs
|
||||
* @return void
|
||||
*/
|
||||
public function setConfigData($data): void {
|
||||
foreach ($data as $key => $value) {
|
||||
if (DB::table('core_config')->where('code', '=', $key)->exists()) {
|
||||
DB::table('core_config')->where('code', '=', $key)->update(['value' => $value]);
|
||||
} else {
|
||||
DB::table('core_config')->insert([
|
||||
'code' => $key,
|
||||
'value' => $value,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,23 @@
|
|||
actor: AcceptanceTester
|
||||
modules:
|
||||
enabled:
|
||||
- PhpBrowser:
|
||||
url: http://localhost
|
||||
- \Helper\Acceptance
|
||||
step_decorators: ~
|
||||
- \Helper\Acceptance
|
||||
- Asserts
|
||||
- WebDriver:
|
||||
url: http://nginx/
|
||||
host: selenium-chrome
|
||||
browser: chrome
|
||||
window_size: 1920x1080
|
||||
restart: true
|
||||
wait: 20
|
||||
pageload_timeout: 10
|
||||
connection_timeout: 60
|
||||
request_timeout: 60
|
||||
log_js_errors: true
|
||||
- Laravel5:
|
||||
part: ORM
|
||||
cleanup: false
|
||||
environment_file: .env
|
||||
database_seeder_class: DatabaseSeeder
|
||||
url: http://nginx
|
||||
step_decorators: ~
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Acceptance;
|
||||
|
||||
use AcceptanceTester;
|
||||
use Faker\Factory;
|
||||
|
||||
class GuestCheckoutCest
|
||||
{
|
||||
private $faker;
|
||||
|
||||
public function _before(AcceptanceTester $I)
|
||||
{
|
||||
$this->faker = Factory::create();
|
||||
}
|
||||
|
||||
function testToConfigureGlobalGuestCheckout(AcceptanceTester $I)
|
||||
{
|
||||
$I->loginAsAdmin();
|
||||
|
||||
$I->amGoingTo('turn ON the global guest checkout configuration');
|
||||
$I->amOnPage('/admin/configuration/catalog/products');
|
||||
$I->see(__('admin::app.admin.system.allow-guest-checkout'));
|
||||
$I->selectOption('catalog[products][guest-checkout][allow-guest-checkout]', 1);
|
||||
$I->click(__('admin::app.configuration.save-btn-title'));
|
||||
$I->seeRecord('core_config', ['code' => 'catalog.products.guest-checkout.allow-guest-checkout', 'value' => 1]);
|
||||
|
||||
$I->amGoingTo('assert that the product guest checkout configuration is shown');
|
||||
$I->amOnPage('admin/catalog/products');
|
||||
$I->click(__('admin::app.catalog.products.add-product-btn-title'));
|
||||
$I->selectOption('attribute_family_id', 1);
|
||||
$I->fillField('sku', $this->faker->uuid);
|
||||
$I->dontSeeInSource('<span class="control-error">The "SKU" field is required.</span>');
|
||||
$I->click(__('admin::app.catalog.products.save-btn-title'));
|
||||
$I->seeInCurrentUrl('admin/catalog/products/edit');
|
||||
$I->scrollTo('#new');
|
||||
$I->see('Guest Checkout');
|
||||
$I->seeInSource('<input type="checkbox" id="guest_checkout" name="guest_checkout"');
|
||||
|
||||
$I->amGoingTo('turn OFF the global guest checkout configuration');
|
||||
$I->amOnPage('/admin/configuration/catalog/products');
|
||||
$I->see(__('admin::app.admin.system.allow-guest-checkout'));
|
||||
$I->selectOption('catalog[products][guest-checkout][allow-guest-checkout]', 0);
|
||||
$I->click(__('admin::app.configuration.save-btn-title'));
|
||||
$I->seeRecord('core_config', ['code' => 'catalog.products.guest-checkout.allow-guest-checkout', 'value' => 0]);
|
||||
|
||||
$I->amGoingTo('assert that the product guest checkout configuration is not shown');
|
||||
$I->amOnPage('admin/catalog/products');
|
||||
$I->click(__('admin::app.catalog.products.add-product-btn-title'));
|
||||
$I->selectOption('attribute_family_id', 1);
|
||||
$I->fillField('sku', $this->faker->uuid);
|
||||
$I->dontSeeInSource('<span class="control-error">The "SKU" field is required.</span>');
|
||||
$I->click(__('admin::app.catalog.products.save-btn-title'));
|
||||
$I->seeInCurrentUrl('admin/catalog/products/edit');
|
||||
$I->scrollTo('#new');
|
||||
$I->dontSee('Guest Checkout');
|
||||
$I->dontSeeInSource('<input type="checkbox" id="guest_checkout" name="guest_checkout"');
|
||||
}
|
||||
}
|
||||
|
|
@ -11,8 +11,10 @@ modules:
|
|||
# add a framework module here
|
||||
- \Helper\Functional
|
||||
- Asserts
|
||||
- Laravel5:
|
||||
- Webkul\Core\Helpers\Laravel5Helper:
|
||||
environment_file: .env.testing
|
||||
packages: packages
|
||||
cleanup: false
|
||||
run_database_migrations: true
|
||||
run_database_seeder: true
|
||||
database_seeder_class: DatabaseSeeder
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class ProductCest
|
|||
|
||||
$I->selectOption('select#attribute_family_id', $attributeFamily->id);
|
||||
|
||||
$sku = $this->faker->randomNumber(3);
|
||||
$sku = $this->faker->uuid;
|
||||
|
||||
$I->fillField('sku', $sku);
|
||||
$I->click(__('admin::app.catalog.products.save-btn-title'));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
namespace Tests\Webkul\Unit\Shop;
|
||||
|
||||
use FunctionalTester;
|
||||
use Faker\Factory;
|
||||
use Cart;
|
||||
|
||||
class GuestCheckoutCest
|
||||
{
|
||||
private $faker,
|
||||
$productNoGuestCheckout, $productGuestCheckout;
|
||||
|
||||
function _before(FunctionalTester $I) {
|
||||
|
||||
$this->faker = Factory::create();
|
||||
|
||||
$pConfigDefault = [
|
||||
'productInventory' => ['qty' => $this->faker->randomNumber(2)],
|
||||
'attributeValues' => [
|
||||
'status' => true,
|
||||
'new' => 1,
|
||||
'guest_checkout' => 0
|
||||
],
|
||||
];
|
||||
$pConfigGuestCheckout = [
|
||||
'productInventory' => ['qty' => $this->faker->randomNumber(2)],
|
||||
'attributeValues' => [
|
||||
'status' => true,
|
||||
'new' => 1,
|
||||
'guest_checkout' => 1
|
||||
],
|
||||
];
|
||||
|
||||
$this->productNoGuestCheckout = $I->haveProduct($pConfigDefault, ['simple']);
|
||||
$this->productNoGuestCheckout->refresh();
|
||||
|
||||
$this->productGuestCheckout = $I->haveProduct($pConfigGuestCheckout, ['simple']);
|
||||
$this->productGuestCheckout->refresh();
|
||||
}
|
||||
|
||||
public function testGuestCheckout(FunctionalTester $I) {
|
||||
$I->amGoingTo('try to add products to cart with guest checkout turned on or off');
|
||||
|
||||
$scenarios = [
|
||||
[
|
||||
'name' => 'false / false',
|
||||
'globalConfig' => 0,
|
||||
'product' => $this->productNoGuestCheckout,
|
||||
'expectedRoute' => 'customer.session.index'
|
||||
],
|
||||
[
|
||||
'name' => 'false / true',
|
||||
'globalConfig' => 0,
|
||||
'product' => $this->productGuestCheckout,
|
||||
'expectedRoute' => 'customer.session.index'
|
||||
],
|
||||
[
|
||||
'name' => 'true / false',
|
||||
'globalConfig' => 1,
|
||||
'product' => $this->productNoGuestCheckout,
|
||||
'expectedRoute' => 'customer.session.index'
|
||||
],
|
||||
[
|
||||
'name' => 'true / true',
|
||||
'globalConfig' => 1,
|
||||
'product' => $this->productGuestCheckout,
|
||||
'expectedRoute' => 'shop.checkout.onepage.index'
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($scenarios as $scenario) {
|
||||
$I->wantTo('test conjunction "' . $scenario['name'] . '" with globalConfig = ' . $scenario['globalConfig'] . ' && product config = ' . $scenario['product']->getAttribute('guest_checkout'));
|
||||
$I->setConfigData(['catalog.products.guest-checkout.allow-guest-checkout' => $scenario['globalConfig']]);
|
||||
$I->assertEquals($scenario['globalConfig'], core()->getConfigData('catalog.products.guest-checkout.allow-guest-checkout'));
|
||||
$I->amOnRoute('shop.home.index');
|
||||
$I->see($scenario['product']->name, '//div[@class="product-information"]/div[@class="product-name"]');
|
||||
$I->click(__('shop::app.products.add-to-cart'),
|
||||
'//form[input[@name="product_id"][@value="' . $scenario['product']->id . '"]]/button');
|
||||
$I->seeInSource(__('shop::app.checkout.cart.item.success'));
|
||||
$I->amOnRoute('shop.checkout.cart.index');
|
||||
$I->see('Shopping Cart', '//div[@class="title"]');
|
||||
$I->makeHtmlSnapshot('guestCheckout_'.$scenario['globalConfig'].'_'.$scenario['product']->getAttribute('guest_checkout'));
|
||||
$I->see($scenario['product']->name, '//div[@class="item-title"]');
|
||||
$I->click( __('shop::app.checkout.cart.proceed-to-checkout'), '//a[@href="' . route('shop.checkout.onepage.index') . '"]');
|
||||
$I->seeCurrentRouteIs($scenario['expectedRoute']);
|
||||
$cart = Cart::getCart();
|
||||
$I->assertTrue(Cart::removeItem($cart->items[0]->id));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue