add: full multi-tenancy control
This commit is contained in:
28
packages/Webkul/Product/composer.json
Executable file
28
packages/Webkul/Product/composer.json
Executable file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "krayin/laravel-product",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jitendra Singh",
|
||||
"email": "jitendra@webkul.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"krayin/laravel-attribute": "^1.0",
|
||||
"krayin/laravel-core": "^1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Webkul\\Product\\": "src/"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Webkul\\Product\\Providers\\ProductServiceProvider"
|
||||
],
|
||||
"aliases": {}
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev"
|
||||
}
|
||||
5
packages/Webkul/Product/src/Contracts/Product.php
Normal file
5
packages/Webkul/Product/src/Contracts/Product.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Product\Contracts;
|
||||
|
||||
interface Product {}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Product\Contracts;
|
||||
|
||||
interface ProductInventory {}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('products', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('sku')->unique();
|
||||
$table->string('name')->nullable();
|
||||
$table->string('description')->nullable();
|
||||
$table->integer('quantity')->default(0);
|
||||
$table->decimal('price', 12, 4)->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('products');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('product_inventories', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('in_stock')->default(0);
|
||||
$table->integer('allocated')->default(0);
|
||||
|
||||
$table->integer('product_id')->unsigned();
|
||||
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
|
||||
$table->integer('warehouse_id')->unsigned()->nullable();
|
||||
$table->foreign('warehouse_id')->references('id')->on('warehouses')->onDelete('cascade');
|
||||
|
||||
$table->integer('warehouse_location_id')->unsigned()->nullable();
|
||||
$table->foreign('warehouse_location_id')->references('id')->on('warehouse_locations')->onDelete('SET NULL');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('product_inventories');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('product_activities', function (Blueprint $table) {
|
||||
$table->integer('activity_id')->unsigned();
|
||||
$table->foreign('activity_id')->references('id')->on('activities')->onDelete('cascade');
|
||||
|
||||
$table->integer('product_id')->unsigned();
|
||||
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('product_activities');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('product_tags', function (Blueprint $table) {
|
||||
$table->integer('tag_id')->unsigned();
|
||||
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
|
||||
|
||||
$table->integer('product_id')->unsigned();
|
||||
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('product_tags');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
Schema::table('product_inventories', function (Blueprint $table) {
|
||||
$table->dropForeign(['warehouse_location_id']);
|
||||
|
||||
$table->foreign('warehouse_location_id')->references('id')->on('warehouse_locations')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
Schema::table('product_inventories', function (Blueprint $table) {
|
||||
$table->dropForeign(['warehouse_location_id']);
|
||||
|
||||
$table->foreign('warehouse_location_id')->references('id')->on('warehouse_locations')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
};
|
||||
72
packages/Webkul/Product/src/Models/Product.php
Normal file
72
packages/Webkul/Product/src/Models/Product.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Product\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Webkul\Activity\Models\ActivityProxy;
|
||||
use Webkul\Activity\Traits\LogsActivity;
|
||||
use Webkul\Attribute\Traits\CustomAttribute;
|
||||
use Webkul\Product\Contracts\Product as ProductContract;
|
||||
use Webkul\Tag\Models\TagProxy;
|
||||
use Webkul\Warehouse\Models\LocationProxy;
|
||||
use Webkul\Warehouse\Models\WarehouseProxy;
|
||||
|
||||
class Product extends Model implements ProductContract
|
||||
{
|
||||
use CustomAttribute, LogsActivity;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'sku',
|
||||
'description',
|
||||
'quantity',
|
||||
'price',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the product warehouses that owns the product.
|
||||
*/
|
||||
public function warehouses(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(WarehouseProxy::modelClass(), 'product_inventories');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the product locations that owns the product.
|
||||
*/
|
||||
public function locations(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(LocationProxy::modelClass(), 'product_inventories', 'product_id', 'warehouse_location_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the product inventories that owns the product.
|
||||
*/
|
||||
public function inventories(): HasMany
|
||||
{
|
||||
return $this->hasMany(ProductInventoryProxy::modelClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* The tags that belong to the Products.
|
||||
*/
|
||||
public function tags()
|
||||
{
|
||||
return $this->belongsToMany(TagProxy::modelClass(), 'product_tags');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the activities.
|
||||
*/
|
||||
public function activities()
|
||||
{
|
||||
return $this->belongsToMany(ActivityProxy::modelClass(), 'product_activities');
|
||||
}
|
||||
}
|
||||
61
packages/Webkul/Product/src/Models/ProductInventory.php
Executable file
61
packages/Webkul/Product/src/Models/ProductInventory.php
Executable file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Product\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Webkul\Product\Contracts\ProductInventory as ProductInventoryContract;
|
||||
use Webkul\Warehouse\Models\LocationProxy;
|
||||
use Webkul\Warehouse\Models\WarehouseProxy;
|
||||
|
||||
class ProductInventory extends Model implements ProductInventoryContract
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'in_stock',
|
||||
'allocated',
|
||||
'product_id',
|
||||
'warehouse_id',
|
||||
'warehouse_location_id',
|
||||
];
|
||||
|
||||
/**
|
||||
* Interact with the name.
|
||||
*/
|
||||
protected function onHand(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn ($value) => $this->in_stock - $this->allocated,
|
||||
set: fn ($value) => $this->in_stock - $this->allocated
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the product that owns the product inventory.
|
||||
*/
|
||||
public function product(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ProductProxy::modelClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the product attribute family that owns the product.
|
||||
*/
|
||||
public function warehouse(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WarehouseProxy::modelClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the product attribute family that owns the product.
|
||||
*/
|
||||
public function location(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(LocationProxy::modelClass(), 'warehouse_location_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Product\Models;
|
||||
|
||||
use Konekt\Concord\Proxies\ModelProxy;
|
||||
|
||||
class ProductInventoryProxy extends ModelProxy {}
|
||||
7
packages/Webkul/Product/src/Models/ProductProxy.php
Normal file
7
packages/Webkul/Product/src/Models/ProductProxy.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Product\Models;
|
||||
|
||||
use Konekt\Concord\Proxies\ModelProxy;
|
||||
|
||||
class ProductProxy extends ModelProxy {}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Product\Providers;
|
||||
|
||||
use Webkul\Core\Providers\BaseModuleServiceProvider;
|
||||
|
||||
class ModuleServiceProvider extends BaseModuleServiceProvider
|
||||
{
|
||||
protected $models = [
|
||||
\Webkul\Product\Models\Product::class,
|
||||
\Webkul\Product\Models\ProductInventory::class,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Product\Providers;
|
||||
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class ProductServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot(Router $router)
|
||||
{
|
||||
$this->loadMigrationsFrom(__DIR__.'/../Database/Migrations');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register() {}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Product\Repositories;
|
||||
|
||||
use Webkul\Core\Eloquent\Repository;
|
||||
|
||||
class ProductInventoryRepository extends Repository
|
||||
{
|
||||
/**
|
||||
* Specify Model class name
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function model()
|
||||
{
|
||||
return 'Webkul\Product\Contracts\ProductInventory';
|
||||
}
|
||||
}
|
||||
192
packages/Webkul/Product/src/Repositories/ProductRepository.php
Normal file
192
packages/Webkul/Product/src/Repositories/ProductRepository.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
namespace Webkul\Product\Repositories;
|
||||
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Support\Str;
|
||||
use Webkul\Attribute\Repositories\AttributeRepository;
|
||||
use Webkul\Attribute\Repositories\AttributeValueRepository;
|
||||
use Webkul\Core\Eloquent\Repository;
|
||||
use Webkul\Product\Contracts\Product;
|
||||
|
||||
class ProductRepository extends Repository
|
||||
{
|
||||
/**
|
||||
* Searchable fields.
|
||||
*/
|
||||
protected $fieldSearchable = [
|
||||
'sku',
|
||||
'name',
|
||||
'description',
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a new repository instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected AttributeRepository $attributeRepository,
|
||||
protected AttributeValueRepository $attributeValueRepository,
|
||||
protected ProductInventoryRepository $productInventoryRepository,
|
||||
Container $container
|
||||
) {
|
||||
parent::__construct($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify model class name.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function model()
|
||||
{
|
||||
return Product::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create.
|
||||
*
|
||||
* @return \Webkul\Product\Contracts\Product
|
||||
*/
|
||||
public function create(array $data)
|
||||
{
|
||||
$product = parent::create($data);
|
||||
|
||||
$this->attributeValueRepository->save(array_merge($data, [
|
||||
'entity_id' => $product->id,
|
||||
]));
|
||||
|
||||
return $product;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update.
|
||||
*
|
||||
* @param int $id
|
||||
* @param array $attribute
|
||||
* @return \Webkul\Product\Contracts\Product
|
||||
*/
|
||||
public function update(array $data, $id, $attributes = [])
|
||||
{
|
||||
$product = parent::update($data, $id);
|
||||
|
||||
/**
|
||||
* If attributes are provided then only save the provided attributes and return.
|
||||
*/
|
||||
if (! empty($attributes)) {
|
||||
$conditions = ['entity_type' => $data['entity_type']];
|
||||
|
||||
if (isset($data['quick_add'])) {
|
||||
$conditions['quick_add'] = 1;
|
||||
}
|
||||
|
||||
$attributes = $this->attributeRepository->where($conditions)
|
||||
->whereIn('code', $attributes)
|
||||
->get();
|
||||
|
||||
$this->attributeValueRepository->save(array_merge($data, [
|
||||
'entity_id' => $product->id,
|
||||
]), $attributes);
|
||||
|
||||
return $product;
|
||||
}
|
||||
|
||||
$this->attributeValueRepository->save(array_merge($data, [
|
||||
'entity_id' => $product->id,
|
||||
]));
|
||||
|
||||
return $product;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save inventories.
|
||||
*
|
||||
* @param int $id
|
||||
* @param ?int $warehouseId
|
||||
* @return void
|
||||
*/
|
||||
public function saveInventories(array $data, $id, $warehouseId = null)
|
||||
{
|
||||
$productInventories = $this->productInventoryRepository->where('product_id', $id);
|
||||
|
||||
if ($warehouseId) {
|
||||
$productInventories = $productInventories->where('warehouse_id', $warehouseId);
|
||||
}
|
||||
|
||||
$previousInventoryIds = $productInventories->pluck('id');
|
||||
|
||||
if (isset($data['inventories'])) {
|
||||
foreach ($data['inventories'] as $inventoryId => $inventoryData) {
|
||||
if (Str::contains($inventoryId, 'inventory_')) {
|
||||
$this->productInventoryRepository->create(array_merge($inventoryData, [
|
||||
'product_id' => $id,
|
||||
'warehouse_id' => $warehouseId,
|
||||
]));
|
||||
} else {
|
||||
if (is_numeric($index = $previousInventoryIds->search($inventoryId))) {
|
||||
$previousInventoryIds->forget($index);
|
||||
}
|
||||
|
||||
$this->productInventoryRepository->update($inventoryData, $inventoryId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($previousInventoryIds as $inventoryId) {
|
||||
$this->productInventoryRepository->delete($inventoryId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves customers count based on date.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getProductCount($startDate, $endDate)
|
||||
{
|
||||
return $this
|
||||
->whereBetween('created_at', [$startDate, $endDate])
|
||||
->get()
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inventories grouped by warehouse.
|
||||
*
|
||||
* @param int $id
|
||||
* @return array
|
||||
*/
|
||||
public function getInventoriesGroupedByWarehouse($id)
|
||||
{
|
||||
$product = $this->findOrFail($id);
|
||||
|
||||
$warehouses = [];
|
||||
|
||||
foreach ($product->inventories as $inventory) {
|
||||
if (! isset($warehouses[$inventory->warehouse_id])) {
|
||||
$warehouses[$inventory->warehouse_id] = [
|
||||
'id' => $inventory->warehouse_id,
|
||||
'name' => $inventory->warehouse->name,
|
||||
'in_stock' => $inventory->in_stock,
|
||||
'allocated' => $inventory->allocated,
|
||||
'on_hand' => $inventory->on_hand,
|
||||
];
|
||||
} else {
|
||||
$warehouses[$inventory->warehouse_id]['in_stock'] += $inventory->in_stock;
|
||||
$warehouses[$inventory->warehouse_id]['allocated'] += $inventory->allocated;
|
||||
$warehouses[$inventory->warehouse_id]['on_hand'] += $inventory->on_hand;
|
||||
}
|
||||
|
||||
$warehouses[$inventory->warehouse_id]['locations'][] = [
|
||||
'id' => $inventory->warehouse_location_id,
|
||||
'name' => $inventory->location->name,
|
||||
'in_stock' => $inventory->in_stock,
|
||||
'allocated' => $inventory->allocated,
|
||||
'on_hand' => $inventory->on_hand,
|
||||
];
|
||||
}
|
||||
|
||||
return $warehouses;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user