introduce ability to copy an product

This commit is contained in:
Herbert Maschke 2020-07-30 14:15:54 +02:00
parent e6d11ed809
commit 0509f45762
8 changed files with 270 additions and 67 deletions

View File

@ -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'),

View File

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

View File

@ -1,18 +1,19 @@
<?php
return array (
'save' => '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',

View File

@ -1,18 +1,19 @@
<?php
return [
'save' => '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',

View File

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

View File

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

View File

@ -1,5 +1,6 @@
<?php
use Codeception\Actor;
use Webkul\User\Models\Admin;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
@ -22,7 +23,7 @@ use Illuminate\Routing\RouteCollection;
*
* @SuppressWarnings(PHPMD)
*/
class FunctionalTester extends \Codeception\Actor
class FunctionalTester extends Actor
{
use _generated\FunctionalTesterActions;
@ -47,7 +48,7 @@ class FunctionalTester extends \Codeception\Actor
}
if (! $admin) {
throw new \Exception(
throw new Exception(
'Admin user not found in database. Please ensure Seeders are executed');
}
@ -85,12 +86,17 @@ class FunctionalTester extends \Codeception\Actor
/**
* @param string $name
* @param array $params
* @param bool $routeCheck set this to false if the action is doing a redirection
*/
public function amOnAdminRoute(string $name, array $params = [])
public function amOnAdminRoute(string $name, array $params = [], bool $routeCheck = true)
{
$I = $this;
$I->amOnRoute($name, $params);
$I->seeCurrentRouteIs($name);
if ($routeCheck) {
$I->seeCurrentRouteIs($name);
}
/** @var RouteCollection $routes */
$routes = Route::getRoutes();

View File

@ -0,0 +1,86 @@
<?php
namespace Tests\Functional\Product;
use FunctionalTester;
use Webkul\Product\Models\Product;
use Webkul\Product\Models\ProductFlat;
use Webkul\Core\Helpers\Laravel5Helper;
use Webkul\Product\Models\ProductInventory;
use Webkul\Product\Models\ProductAttributeValue;
class ProductCopyCest
{
public function testProductCopy(FunctionalTester $I)
{
$I->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();
}
}