diff --git a/packages/Webkul/Admin/src/DataGrids/ProductDataGrid.php b/packages/Webkul/Admin/src/DataGrids/ProductDataGrid.php index 57c65fc4d..e48dec856 100644 --- a/packages/Webkul/Admin/src/DataGrids/ProductDataGrid.php +++ b/packages/Webkul/Admin/src/DataGrids/ProductDataGrid.php @@ -181,6 +181,13 @@ class ProductDataGrid extends DataGrid public function prepareMassActions() { + $this->addAction([ + 'title' => trans('admin::app.datagrid.copy'), + 'method' => 'GET', + 'route' => 'admin.catalog.products.copy', + 'icon' => 'icon note-icon', + ]); + $this->addMassAction([ 'type' => 'delete', 'label' => trans('admin::app.datagrid.delete'), diff --git a/packages/Webkul/Admin/src/Http/routes.php b/packages/Webkul/Admin/src/Http/routes.php index b4f9cce62..b0db82981 100755 --- a/packages/Webkul/Admin/src/Http/routes.php +++ b/packages/Webkul/Admin/src/Http/routes.php @@ -269,6 +269,10 @@ Route::group(['middleware' => ['web']], function () { 'redirect' => 'admin.catalog.products.edit', ])->name('admin.catalog.products.store'); + Route::get('products/copy/{id}', 'Webkul\Product\Http\Controllers\ProductController@copy')->defaults('_config', [ + 'view' => 'admin::catalog.products.edit', + ])->name('admin.catalog.products.copy'); + Route::get('/products/edit/{id}', 'Webkul\Product\Http\Controllers\ProductController@edit')->defaults('_config', [ 'view' => 'admin::catalog.products.edit', ])->name('admin.catalog.products.edit'); diff --git a/packages/Webkul/Admin/src/Resources/lang/de/app.php b/packages/Webkul/Admin/src/Resources/lang/de/app.php index 881147964..ff96194d3 100755 --- a/packages/Webkul/Admin/src/Resources/lang/de/app.php +++ b/packages/Webkul/Admin/src/Resources/lang/de/app.php @@ -1,18 +1,19 @@ 'Speichern', - 'copy-of' => 'Kopie von', - 'create' => 'Erstellen', - 'update' => 'Update', - 'delete' => 'Löschen', - 'failed' => 'Fehlgeschlagen', - 'store' => 'Speichern', - 'image' => 'Bild', - 'no result' => 'Kein Ergebnis', - 'product' => 'Produkt', - 'attribute' => 'Attribut', - 'actions' => 'Aktionen', +return array( + 'save' => 'Speichern', + 'copy-of' => 'Kopie von', + 'copy-of-slug' => 'kopie-von', + 'create' => 'Erstellen', + 'update' => 'Update', + 'delete' => 'Löschen', + 'failed' => 'Fehlgeschlagen', + 'store' => 'Speichern', + 'image' => 'Bild', + 'no result' => 'Kein Ergebnis', + 'product' => 'Produkt', + 'attribute' => 'Attribut', + 'actions' => 'Aktionen', 'id' => 'Id', 'action' => 'Aktion', 'yes' => 'Ja', diff --git a/packages/Webkul/Admin/src/Resources/lang/en/app.php b/packages/Webkul/Admin/src/Resources/lang/en/app.php index ac7a8ad25..90dd8df36 100755 --- a/packages/Webkul/Admin/src/Resources/lang/en/app.php +++ b/packages/Webkul/Admin/src/Resources/lang/en/app.php @@ -1,18 +1,19 @@ 'Save', - 'copy-of' => 'Copy of', - 'create' => 'Create', - 'update' => 'Update', - 'delete' => 'Delete', - 'failed' => 'Failed', - 'store' => 'Store', - 'image' => 'Image', - 'no result' => 'No result', - 'product' => 'Product', - 'attribute' => 'Attribute', - 'actions' => 'Actions', + 'save' => 'Save', + 'copy-of' => 'Copy of', + 'copy-of-slug' => 'copy-of', + 'create' => 'Create', + 'update' => 'Update', + 'delete' => 'Delete', + 'failed' => 'Failed', + 'store' => 'Store', + 'image' => 'Image', + 'no result' => 'No result', + 'product' => 'Product', + 'attribute' => 'Attribute', + 'actions' => 'Actions', 'id' => 'ID', 'action' => 'action', 'yes' => 'Yes', diff --git a/packages/Webkul/Product/src/Http/Controllers/ProductController.php b/packages/Webkul/Product/src/Http/Controllers/ProductController.php index cab805178..a11ac38a4 100755 --- a/packages/Webkul/Product/src/Http/Controllers/ProductController.php +++ b/packages/Webkul/Product/src/Http/Controllers/ProductController.php @@ -2,16 +2,20 @@ namespace Webkul\Product\Http\Controllers; +use Exception; use Illuminate\Support\Facades\Event; -use Webkul\Product\Http\Requests\ProductForm; +use Webkul\Attribute\Models\Attribute; +use Webkul\Product\Models\ProductFlat; use Webkul\Product\Helpers\ProductType; -use Webkul\Category\Repositories\CategoryRepository; +use Illuminate\Support\Facades\Storage; +use Webkul\Core\Contracts\Validations\Slug; +use Webkul\Product\Http\Requests\ProductForm; use Webkul\Product\Repositories\ProductRepository; -use Webkul\Product\Repositories\ProductDownloadableLinkRepository; -use Webkul\Product\Repositories\ProductDownloadableSampleRepository; +use Webkul\Category\Repositories\CategoryRepository; use Webkul\Attribute\Repositories\AttributeFamilyRepository; use Webkul\Inventory\Repositories\InventorySourceRepository; -use Illuminate\Support\Facades\Storage; +use Webkul\Product\Repositories\ProductDownloadableLinkRepository; +use Webkul\Product\Repositories\ProductDownloadableSampleRepository; class ProductController extends Controller { @@ -67,12 +71,13 @@ class ProductController extends Controller /** * Create a new controller instance. * - * @param \Webkul\Category\Repositories\CategoryRepository $categoryRepository - * @param \Webkul\Product\Repositories\ProductRepository $productRepository - * @param \Webkul\Product\Repositories\ProductDownloadableLinkRepository $productDownloadableLinkRepository - * @param \Webkul\Product\Repositories\ProductDownloadableSampleRepository $productDownloadableSampleRepository - * @param \Webkul\Attribute\Repositories\AttributeFamilyRepository $attributeFamilyRepository - * @param \Webkul\Inventory\Repositories\InventorySourceRepository $inventorySourceRepository + * @param \Webkul\Category\Repositories\CategoryRepository $categoryRepository + * @param \Webkul\Product\Repositories\ProductRepository $productRepository + * @param \Webkul\Product\Repositories\ProductDownloadableLinkRepository $productDownloadableLinkRepository + * @param \Webkul\Product\Repositories\ProductDownloadableSampleRepository $productDownloadableSampleRepository + * @param \Webkul\Attribute\Repositories\AttributeFamilyRepository $attributeFamilyRepository + * @param \Webkul\Inventory\Repositories\InventorySourceRepository $inventorySourceRepository + * * @return void */ public function __construct( @@ -143,7 +148,7 @@ class ProductController extends Controller if (ProductType::hasVariants(request()->input('type')) && (! request()->has('super_attributes') - || ! count(request()->get('super_attributes'))) + || ! count(request()->get('super_attributes'))) ) { session()->flash('error', trans('admin::app.catalog.products.configurable-error')); @@ -153,7 +158,7 @@ class ProductController extends Controller $this->validate(request(), [ 'type' => 'required', 'attribute_family_id' => 'required', - 'sku' => ['required', 'unique:products,sku', new \Webkul\Core\Contracts\Validations\Slug], + 'sku' => ['required', 'unique:products,sku', new Slug], ]); $product = $this->productRepository->create(request()->all()); @@ -166,7 +171,8 @@ class ProductController extends Controller /** * Show the form for editing the specified resource. * - * @param int $id + * @param int $id + * * @return \Illuminate\View\View */ public function edit($id) @@ -183,8 +189,9 @@ class ProductController extends Controller /** * Update the specified resource in storage. * - * @param \Webkul\Product\Http\Requests\ProductForm $request - * @param int $id + * @param \Webkul\Product\Http\Requests\ProductForm $request + * @param int $id + * * @return \Illuminate\Http\Response */ public function update(ProductForm $request, $id) @@ -201,20 +208,20 @@ class ProductController extends Controller if (count($customAttributes)) { foreach ($customAttributes as $attribute) { if ($attribute->type == 'multiselect') { - array_push($multiselectAttributeCodes,$attribute->code); - } + array_push($multiselectAttributeCodes, $attribute->code); + } } } } if (count($multiselectAttributeCodes)) { foreach ($multiselectAttributeCodes as $multiselectAttributeCode) { - if(! isset($data[$multiselectAttributeCode])){ + if (! isset($data[$multiselectAttributeCode])) { $data[$multiselectAttributeCode] = array(); } - } + } } - + $product = $this->productRepository->update($data, $id); session()->flash('success', trans('admin::app.response.update-success', ['name' => 'Product'])); @@ -225,7 +232,8 @@ class ProductController extends Controller /** * Uploads downloadable file * - * @param int $id + * @param int $id + * * @return \Illuminate\Http\Response */ public function uploadLink($id) @@ -235,10 +243,92 @@ class ProductController extends Controller ); } + /** + * Copy a given Product. Do not copy the images. + * Always make the copy is inactive so the admin is able to configure it before setting it live. + */ + public function copy(int $productId) + { + $originalProduct = $this->productRepository + ->findOrFail($productId) + ->load('attribute_family') + ->load('categories') + ->load('customer_group_prices') + ->load('inventories') + ->load('inventory_sources'); + + $copiedProduct = $originalProduct + ->replicate() + ->fill([ + // the sku and url_key needs to be unique and should be entered again newly by the admin: + 'sku' => 'temporary-sku-' . substr(md5(microtime()), 0, 6), + ]); + + $copiedProduct->save(); + + $attributeName = Attribute::query()->where(['code' => 'name'])->firstOrFail(); + $attributeSku = Attribute::query()->where(['code' => 'sku'])->firstOrFail(); + $attributeStatus = Attribute::query()->where(['code' => 'status'])->firstOrFail(); + $attributeUrlKey = Attribute::query()->where(['code' => 'url_key'])->firstOrFail(); + + $newProductFlat = new ProductFlat(); + + // only obey copied locale and channel: + if (isset($originalProduct->product_flats[0])) { + $newProductFlat = $originalProduct->product_flats[0]->replicate(); + } + + $newProductFlat->product_id = $copiedProduct->id; + + foreach ($originalProduct->attribute_values as $oldValue) { + $newValue = $oldValue->replicate(); + + if ($oldValue->attribute_id === $attributeName->id) { + $newValue->text_value = __('admin::app.copy-of') . ' ' . $originalProduct->name; + $newProductFlat->name = __('admin::app.copy-of') . ' ' . $originalProduct->name; + } + + if ($oldValue->attribute_id === $attributeUrlKey->id) { + $newValue->text_value = __('admin::app.copy-of-slug') . '-' . $originalProduct->url_key; + $newProductFlat->url_key = __('admin::app.copy-of-slug') . '-' . $originalProduct->url_key; + } + + if ($oldValue->attribute_id === $attributeSku->id) { + $newValue->text_value = $copiedProduct->sku; + $newProductFlat->sku = $copiedProduct->sku; + } + + if ($oldValue->attribute_id === $attributeStatus->id) { + $newValue->boolean_value = 0; + $newProductFlat->status = 0; + } + + $copiedProduct->attribute_values()->save($newValue); + + } + + $newProductFlat->save(); + + foreach ($originalProduct->categories as $category) { + $copiedProduct->categories()->save($category); + } + + foreach ($originalProduct->inventories as $inventory) { + $copiedProduct->inventories()->save($inventory); + } + + foreach ($originalProduct->customer_group_prices as $customer_group_price) { + $copiedProduct->customer_group_prices()->save($customer_group_price); + } + + return redirect()->to(route('admin.catalog.products.edit', ['id' => $copiedProduct->id])); + } + /** * Uploads downloadable sample file * - * @param int $id + * @param int $id + * * @return \Illuminate\Http\Response */ public function uploadSample($id) @@ -251,7 +341,8 @@ class ProductController extends Controller /** * Remove the specified resource from storage. * - * @param int $id + * @param int $id + * * @return \Illuminate\Http\Response */ public function destroy($id) @@ -264,7 +355,7 @@ class ProductController extends Controller session()->flash('success', trans('admin::app.response.delete-success', ['name' => 'Product'])); return response()->json(['message' => true], 200); - } catch (\Exception $e) { + } catch (Exception $e) { report($e); session()->flash('error', trans('admin::app.response.delete-failed', ['name' => 'Product'])); @@ -363,11 +454,12 @@ class ProductController extends Controller } } - /** + /** * Download image or file * - * @param int $productId - * @param int $attributeId + * @param int $productId + * @param int $attributeId + * * @return \Illuminate\Http\Response */ public function download($productId, $attributeId) diff --git a/packages/Webkul/Product/src/Models/Product.php b/packages/Webkul/Product/src/Models/Product.php index 86acea5a1..7f92a4426 100755 --- a/packages/Webkul/Product/src/Models/Product.php +++ b/packages/Webkul/Product/src/Models/Product.php @@ -3,10 +3,12 @@ namespace Webkul\Product\Models; use Illuminate\Database\Eloquent\Model; -use Webkul\Attribute\Models\AttributeFamilyProxy; use Webkul\Category\Models\CategoryProxy; use Webkul\Attribute\Models\AttributeProxy; +use Webkul\Product\Database\Eloquent\Builder; +use Webkul\Attribute\Models\AttributeFamilyProxy; use Webkul\Inventory\Models\InventorySourceProxy; +use Webkul\Attribute\Repositories\AttributeRepository; use Webkul\Product\Contracts\Product as ProductContract; class Product extends Model implements ProductContract @@ -220,8 +222,8 @@ class Product extends Model implements ProductContract public function inventory_source_qty($inventorySourceId) { return $this->inventories() - ->where('inventory_source_id', $inventorySourceId) - ->sum('qty'); + ->where('inventory_source_id', $inventorySourceId) + ->sum('qty'); } /** @@ -283,6 +285,7 @@ class Product extends Model implements ProductContract * * @param Group $group * @param bool $skipSuperAttribute + * * @return Collection */ public function getEditableAttributes($group = null, $skipSuperAttribute = true) @@ -293,7 +296,8 @@ class Product extends Model implements ProductContract /** * Get an attribute from the model. * - * @param string $key + * @param string $key + * * @return mixed */ public function getAttribute($key) @@ -305,8 +309,8 @@ class Product extends Model implements ProductContract if (isset($this->id)) { $this->attributes[$key] = ''; - $attribute = core()->getSingletonInstance(\Webkul\Attribute\Repositories\AttributeRepository::class) - ->getAttributeByCode($key); + $attribute = core()->getSingletonInstance(AttributeRepository::class) + ->getAttributeByCode($key); $this->attributes[$key] = $this->getCustomAttributeValue($attribute); @@ -327,8 +331,9 @@ class Product extends Model implements ProductContract $hiddenAttributes = $this->getHidden(); if (isset($this->id)) { - $familyAttributes = core()->getSingletonInstance(\Webkul\Attribute\Repositories\AttributeRepository::class) - ->getFamilyAttributes($this->attribute_family); + $familyAttributes = core() + ->getSingletonInstance(AttributeRepository::class) + ->getFamilyAttributes($this->attribute_family); foreach ($familyAttributes as $attribute) { if (in_array($attribute->code, $hiddenAttributes)) { @@ -377,12 +382,13 @@ class Product extends Model implements ProductContract /** * Overrides the default Eloquent query builder * - * @param \Illuminate\Database\Eloquent\Builder $query + * @param \Illuminate\Database\Eloquent\Builder $query + * * @return \Illuminate\Database\Eloquent\Builder */ public function newEloquentBuilder($query) { - return new \Webkul\Product\Database\Eloquent\Builder($query); + return new Builder($query); } /** diff --git a/tests/_support/FunctionalTester.php b/tests/_support/FunctionalTester.php index 6d1e1f8b0..f89e14708 100644 --- a/tests/_support/FunctionalTester.php +++ b/tests/_support/FunctionalTester.php @@ -1,5 +1,6 @@ amOnRoute($name, $params); - $I->seeCurrentRouteIs($name); + + if ($routeCheck) { + $I->seeCurrentRouteIs($name); + } /** @var RouteCollection $routes */ $routes = Route::getRoutes(); diff --git a/tests/functional/Product/ProductCopyCest.php b/tests/functional/Product/ProductCopyCest.php new file mode 100644 index 000000000..81eb159de --- /dev/null +++ b/tests/functional/Product/ProductCopyCest.php @@ -0,0 +1,86 @@ +loginAsAdmin(); + + $originalName = $I->fake()->name; + + $original = $I->haveProduct(Laravel5Helper::SIMPLE_PRODUCT, [ + 'productInventory' => [ + 'qty' => 10, + ], + 'attributeValues' => [ + 'name' => $originalName, + ], + ]); + + $count = count(Product::all()); + + $I->amOnAdminRoute('admin.catalog.products.copy', ['id' => $original->id], false); + + $copiedProduct = $I->grabRecord(Product::class, [ + 'id' => $original->id + 1, + 'parent_id' => $original->parent_id, + 'attribute_family_id' => $original->attribute_family_id, + ]); + + $I->seeRecord(ProductAttributeValue::class, [ + 'attribute_id' => 2, + 'product_id' => $copiedProduct->id, + 'text_value' => 'Copy of ' . $originalName, + ]); + + // url_key + $I->seeRecord(ProductAttributeValue::class, [ + 'attribute_id' => 3, + 'product_id' => $copiedProduct->id, + 'text_value' => 'copy-of-' . $original->url_key, + ]); + + // sku + $I->seeRecord(ProductAttributeValue::class, [ + 'attribute_id' => 1, + 'product_id' => $copiedProduct->id, + ]); + + // sku + $I->dontSeeRecord(ProductAttributeValue::class, [ + 'attribute_id' => 1, + 'product_id' => $copiedProduct->id, + 'text_value' => $original->sku, + ]); + + // status + $I->seeRecord(ProductAttributeValue::class, [ + 'attribute_id' => 8, + 'boolean_value' => 0, + ]); + + $I->seeRecord(ProductInventory::class, [ + 'product_id' => $copiedProduct->id, + 'qty' => 10, + ]); + + $I->seeRecord(ProductFlat::class, [ + 'product_id' => $copiedProduct->id, + 'name' => 'Copy of ' . $originalName, + ]); + + $I->assertCount($count + 1, Product::all()); + + $I->seeResponseCodeIsSuccessful(); + } +}