add: full multi-tenancy control

This commit is contained in:
Cauê Faleiros
2026-02-02 15:31:15 -03:00
commit c6ec92802b
1711 changed files with 258106 additions and 0 deletions

View 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"
}

View File

@@ -0,0 +1,5 @@
<?php
namespace Webkul\Product\Contracts;
interface Product {}

View File

@@ -0,0 +1,5 @@
<?php
namespace Webkul\Product\Contracts;
interface ProductInventory {}

View File

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

View File

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

View File

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

View File

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

View File

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

View 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');
}
}

View 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');
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Webkul\Product\Models;
use Konekt\Concord\Proxies\ModelProxy;
class ProductInventoryProxy extends ModelProxy {}

View File

@@ -0,0 +1,7 @@
<?php
namespace Webkul\Product\Models;
use Konekt\Concord\Proxies\ModelProxy;
class ProductProxy extends ModelProxy {}

View File

@@ -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,
];
}

View File

@@ -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() {}
}

View File

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

View 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;
}
}