├── .editorconfig
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .phpunit.result.cache
├── README.md
├── composer.json
├── composer.lock
├── config
└── laravel-inventory.php
├── database
├── factories
│ ├── CategoryFactory.php
│ ├── ProductFactory.php
│ └── WarehouseFactory.php
└── migrations
│ ├── 2020_01_18_123447_create_categories_table.php
│ ├── 2020_01_18_124138_create_products_table.php
│ ├── 2020_01_18_132758_create_warehouses_table.php
│ ├── 2020_01_19_104451_create_attributes_table.php
│ ├── 2020_01_19_104523_create_attribute_values_table.php
│ ├── 2020_01_20_141407_create_product_skus_table.php
│ ├── 2020_01_20_141921_create_product_variants_table.php
│ ├── 2020_01_24_160109_create_inventory_stocks_table.php
│ └── 2020_01_24_171541_create_inventory_stock_movements_table.php
├── docs
├── Adapters.md
├── Attributes.md
├── Category.md
├── Exceptions.md
├── Product.md
└── Variation.md
├── phpunit.xml
├── src
├── Adapters
│ ├── BaseAdapter.php
│ ├── ProductAdapter.php
│ ├── ProductAttributeAdapter.php
│ └── ProductVariantAdapter.php
├── Contracts
│ └── ResourceContract.php
├── Exceptions
│ ├── InvalidAttributeException.php
│ ├── InvalidMovementException.php
│ ├── InvalidProductException.php
│ ├── InvalidVariantException.php
│ └── NotEnoughStockException.php
├── Models
│ ├── Attribute.php
│ ├── AttributeValue.php
│ ├── Category.php
│ ├── InventoryStock.php
│ ├── InventoryStockMovement.php
│ ├── Product.php
│ ├── ProductSku.php
│ ├── ProductVariant.php
│ └── Warehouse.php
├── ProductServiceProvider.php
├── Resources
│ ├── AttributeResource.php
│ ├── ProductResource.php
│ └── VariantResource.php
└── Traits
│ ├── HasAttributes.php
│ ├── HasCategories.php
│ ├── HasInventory.php
│ ├── HasItemMovements.php
│ ├── HasItemStocks.php
│ ├── HasProducts.php
│ ├── HasVariants.php
│ └── Sluggable.php
└── tests
├── TestCase.php
└── Unit
├── AdapterTest.php
├── InventoryTest.php
├── ModelSluggableTest.php
├── ProductAttributeTest.php
├── ProductCategoryTest.php
└── ProductVariationTest.php
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | insert_final_newline = true
6 | indent_style = space
7 | indent_size = 4
8 | trim_trailing_whitespace = true
9 |
10 | [*.md]
11 | trim_trailing_whitespace = false
12 | indent_style = space
13 | indent_size = 2
14 |
15 | [*.yml]
16 | indent_style = space
17 | indent_size = 2
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: run-tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v1
10 |
11 | - name: Validate composer.json and composer.lock
12 | run: composer validate
13 |
14 | - name: Install dependencies
15 | run: composer install --prefer-dist --no-progress --no-suggest
16 |
17 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit"
18 | # Docs: https://getcomposer.org/doc/articles/scripts.md
19 |
20 | # - name: Run test suite
21 | # run: composer run-script test
22 | phpunit:
23 | name: PhpUnit
24 | runs-on: ubuntu-latest
25 | steps:
26 | - uses: actions/checkout@v1
27 | - uses: svikramjeet/git-action-laravel-phpunit@master
28 |
29 | check-validate:
30 | runs-on: ubuntu-latest
31 | steps:
32 | - uses: actions/checkout@v1
33 | - uses: glassmonkey/actions-php-audit@v0.1.1
34 | id: checker # id is required if called from other steps
35 | # Todo other actions
36 | - name: sample message
37 | run: echo "${{ steps.checker.outputs.message }}"
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
--------------------------------------------------------------------------------
/.phpunit.result.cache:
--------------------------------------------------------------------------------
1 | C:37:"PHPUnit\Runner\DefaultTestResultCache":3479:{a:2:{s:7:"defects";a:8:{s:75:"Ronmrcdo\Inventory\Tests\Unit\InventoryTest::itShouldListTheWarehouseStocks";i:4;s:78:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldHaveProductVariant";i:4;s:86:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldCreateSingleProductWithSku";i:4;s:76:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldFindProductBySku";i:4;s:77:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldListTheVariations";i:4;s:86:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldListCollectionOfVariations";i:4;s:71:"Ronmrcdo\Inventory\Tests\Unit\ProductCategoryTest::itShouldHaveCategory";i:3;s:76:"Ronmrcdo\Inventory\Tests\Unit\ProductCategoryTest::itShouldHaveChildCategory";i:4;}s:5:"times";a:28:{s:70:"Ronmrcdo\Inventory\Tests\Unit\AdapterTest::itShouldReturnArrayResource";d:0.251;s:84:"Ronmrcdo\Inventory\Tests\Unit\AdapterTest::itShouldReturnArrayResourceWithAttributes";d:0.022;s:75:"Ronmrcdo\Inventory\Tests\Unit\AdapterTest::itShouldReturnCollectionResource";d:0.03;s:68:"Ronmrcdo\Inventory\Tests\Unit\InventoryTest::itShouldCreateWarehouse";d:0.02;s:77:"Ronmrcdo\Inventory\Tests\Unit\InventoryTest::itShouldInsertProductInWarehouse";d:0.052;s:75:"Ronmrcdo\Inventory\Tests\Unit\InventoryTest::itShouldListTheWarehouseStocks";d:0.034;s:70:"Ronmrcdo\Inventory\Tests\Unit\ModelSluggableTest::itShouldGenerateSlug";d:0.02;s:71:"Ronmrcdo\Inventory\Tests\Unit\ModelSluggableTest::itShouldIncrementSlug";d:0.019;s:71:"Ronmrcdo\Inventory\Tests\Unit\ModelSluggableTest::itShouldNotUpdateSlug";d:0.017;s:71:"Ronmrcdo\Inventory\Tests\Unit\ModelSluggableTest::itShouldUpdateTheSlug";d:0.02;s:81:"Ronmrcdo\Inventory\Tests\Unit\ProductAttributeTest::itShouldAddAttributeToProduct";d:0.021;s:89:"Ronmrcdo\Inventory\Tests\Unit\ProductAttributeTest::itShouldAddAttributeAndValueToProduct";d:0.018;s:88:"Ronmrcdo\Inventory\Tests\Unit\ProductAttributeTest::itShouldGetProductAttributeAndValues";d:0.019;s:84:"Ronmrcdo\Inventory\Tests\Unit\ProductAttributeTest::itShouldCreateMultipleAttributes";d:0.018;s:94:"Ronmrcdo\Inventory\Tests\Unit\ProductAttributeTest::itShouldCreateMultipleAttributesUsingArray";d:0.02;s:90:"Ronmrcdo\Inventory\Tests\Unit\ProductAttributeTest::itShouldThrowInvalidAttributeException";d:0.016;s:86:"Ronmrcdo\Inventory\Tests\Unit\ProductAttributeTest::itShouldRemoveAttributeFromProduct";d:0.017;s:90:"Ronmrcdo\Inventory\Tests\Unit\ProductAttributeTest::itShouldRemoveAttributeTermFromProduct";d:0.018;s:71:"Ronmrcdo\Inventory\Tests\Unit\ProductCategoryTest::itShouldHaveCategory";d:0.016;s:76:"Ronmrcdo\Inventory\Tests\Unit\ProductCategoryTest::itShouldHaveChildCategory";d:0.016;s:81:"Ronmrcdo\Inventory\Tests\Unit\ProductCategoryTest::itShouldListProductsByCategory";d:0.03;s:78:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldHaveProductVariant";d:0.056;s:86:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldCreateSingleProductWithSku";d:0.024;s:93:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldThrowErrorDueToDuplicateAttribute";d:0.025;s:112:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldThrowVariantExceptionForDuplicateVariantDifferentSku";d:0.028;s:76:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldFindProductBySku";d:0.024;s:77:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldListTheVariations";d:0.023;s:86:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldListCollectionOfVariations";d:0.024;}}}
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # :construction: Inventory Package for Laravel
2 |
3 | [](https://packagist.org/packages/ronmrcdo/inventory)
4 | 
5 | [](https://scrutinizer-ci.com/g/ronmrcdo/laravel-inventory/?branch=master)
6 |
7 |
8 | ## Description
9 | Laravel Inventory provides the basic of inventory management. This package is based from the stevebauman/inventory and provides product variation using attributes and terms and have a similar structure of product format of woocommerce.
10 |
11 | ## Requirements
12 | * Laravel 6.0
13 | * PHP ^7.2
14 |
15 | ## Installation
16 |
17 | You can install the package via composer:
18 |
19 | ```bash
20 | composer require ronmrcdo/inventory
21 | ```
22 |
23 | ## Usage
24 |
25 |
33 |
34 | ## Versions
35 |
36 |
40 |
41 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ronmrcdo/inventory",
3 | "description": "An Inventory Management Package for products",
4 | "type": "package",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Ron Mercado",
9 | "email": "ronmercadoaa@gmail.com"
10 | }
11 | ],
12 | "keywords": ["Inventory", "Product", "Laravel"],
13 | "homepage": "https://github.com/ronmrcdo/laravel-inventory",
14 | "require": {
15 | "php": "^7.2",
16 | "illuminate/support": "^6.0|^7.0"
17 | },
18 | "require-dev": {
19 | "phpunit/phpunit": "^8.0",
20 | "mockery/mockery": "^1.0",
21 | "orchestra/testbench": "^3.5 || ^3.6 || ^3.7 || ^3.8 || ^4.0",
22 | "fzaninotto/faker": "^1.4"
23 | },
24 | "autoload": {
25 | "psr-4": {
26 | "Ronmrcdo\\Inventory\\": "src/"
27 | }
28 | },
29 | "autoload-dev": {
30 | "psr-4": {
31 | "Ronmrcdo\\Inventory\\Tests\\": "tests/"
32 | }
33 | },
34 | "extra": {
35 | "laravel": {
36 | "providers": [
37 | "Ronmrcdo\\Inventory\\ProductServiceProvider"
38 | ]
39 | }
40 | },
41 | "scripts": {
42 | "test": [
43 | "vendor/bin/phpunit --filter 'Ronmrcdo\\Inventory\\Tests"
44 | ]
45 | },
46 | "scripts-descriptions": {
47 | "test": "Run Unit Testing"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/config/laravel-inventory.php:
--------------------------------------------------------------------------------
1 | \Ronmrcdo\Inventory\Models\Product::class,
9 |
10 | /**
11 | * Base Category Class
12 | *
13 | */
14 | 'category' => \Ronmrcdo\Inventory\Models\Category::class
15 | ];
--------------------------------------------------------------------------------
/database/factories/CategoryFactory.php:
--------------------------------------------------------------------------------
1 | define(Category::class, function (Faker $faker) {
9 | return [
10 | 'name' => $faker->words(rand(1,4), true),
11 | 'description' => $faker->sentence,
12 | 'parent_id' => null
13 | ];
14 | });
--------------------------------------------------------------------------------
/database/factories/ProductFactory.php:
--------------------------------------------------------------------------------
1 | define(Product::class, function (Faker $faker) {
15 | return [
16 | 'category_id' => factory(Category::class)->create()->id,
17 | 'name' => $faker->words(rand(1,3), true),
18 | 'short_description' => $faker->sentences(5, true),
19 | 'description' => $faker->sentences(10, true),
20 | 'is_active' => true
21 | ];
22 | });
23 |
24 | $factory->define(Attribute::class, function (Faker $faker) {
25 | return [
26 | 'product_id' => null, // it should be attach manually
27 | 'name' => $faker->word
28 | ];
29 | });
30 |
31 | $factory->define(AttributeValue::class, function (Faker $faker) {
32 | return [
33 | 'product_attribute_id' => null, // it should be attach manually
34 | 'value' => $faker->word
35 | ];
36 | });
37 |
38 | $factory->define(ProductSku::class, function (Faker $faker) {
39 | return [
40 | 'product_id' => null, // it should be manually added
41 | 'code' => Str::random()
42 | ];
43 | });
44 |
45 | $factory->define(ProductVariant::class, function (Faker $faker) {
46 | return [
47 | 'product_id' => null, // it should be manually added
48 | 'product_sku_id' => null, // it should be manually added
49 | 'product_attribute_id' => null, // it should be manually added
50 | 'product_attribute_value_id' => null // it should be manually added
51 | ];
52 | });
--------------------------------------------------------------------------------
/database/factories/WarehouseFactory.php:
--------------------------------------------------------------------------------
1 | define(Warehouse::class, function (Faker $faker) {
7 | return [
8 | 'name' => $faker->word,
9 | 'description' => $faker->words(3, true)
10 | ];
11 | });
--------------------------------------------------------------------------------
/database/migrations/2020_01_18_123447_create_categories_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->string('name');
19 | $table->string('slug')->unique();
20 | $table->mediumText('description')->nullable();
21 | $table->unsignedBigInteger('parent_id')->nullable();
22 | $table->timestamps();
23 |
24 | $table->foreign('parent_id')
25 | ->references('id')
26 | ->on('product_categories')
27 | ->onDelete('cascade');
28 | });
29 | }
30 |
31 | /**
32 | * Reverse the migrations.
33 | *
34 | * @return void
35 | */
36 | public function down()
37 | {
38 | Schema::dropIfExists('product_categories');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/database/migrations/2020_01_18_124138_create_products_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->unsignedBigInteger('category_id');
19 | $table->string('name');
20 | $table->string('slug')->unique();
21 | $table->mediumText('short_description')->nullable();
22 | $table->text('description')->nullable();
23 | $table->boolean('is_active');
24 | $table->timestamps();
25 |
26 | $table->foreign('category_id')
27 | ->references('id')
28 | ->on('product_categories')
29 | ->onDelete('cascade');
30 | });
31 | }
32 |
33 | /**
34 | * Reverse the migrations.
35 | *
36 | * @return void
37 | */
38 | public function down()
39 | {
40 | Schema::dropIfExists('products');
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/database/migrations/2020_01_18_132758_create_warehouses_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->string('name');
19 | $table->mediumText('description');
20 | $table->timestamps();
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | *
27 | * @return void
28 | */
29 | public function down()
30 | {
31 | Schema::dropIfExists('warehouses');
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/database/migrations/2020_01_19_104451_create_attributes_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->unsignedBigInteger('product_id');
19 | $table->string('name');
20 |
21 | $table->foreign('product_id')
22 | ->references('id')
23 | ->on('products')
24 | ->onDelete('cascade');
25 | });
26 | }
27 |
28 | /**
29 | * Reverse the migrations.
30 | *
31 | * @return void
32 | */
33 | public function down()
34 | {
35 | Schema::dropIfExists('product_attributes');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/database/migrations/2020_01_19_104523_create_attribute_values_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->unsignedBigInteger('product_attribute_id');
19 | $table->string('value');
20 |
21 | $table->foreign('product_attribute_id')
22 | ->references('id')
23 | ->on('product_attributes')
24 | ->onDelete('cascade');
25 | });
26 | }
27 |
28 | /**
29 | * Reverse the migrations.
30 | *
31 | * @return void
32 | */
33 | public function down()
34 | {
35 | Schema::dropIfExists('product_attribute_values');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/database/migrations/2020_01_20_141407_create_product_skus_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->unsignedBigInteger('product_id');
19 | $table->string('code')->unique();
20 | $table->decimal('price', 8, 2)->default(0);
21 | $table->decimal('cost', 8, 2)->default(0);
22 |
23 |
24 | $table->foreign('product_id')
25 | ->references('id')
26 | ->on('products')
27 | ->onDelete('cascade');
28 | });
29 | }
30 |
31 | /**
32 | * Reverse the migrations.
33 | *
34 | * @return void
35 | */
36 | public function down()
37 | {
38 | Schema::dropIfExists('product_skus');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/database/migrations/2020_01_20_141921_create_product_variants_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->unsignedBigInteger('product_id');
19 | $table->unsignedBigInteger('product_sku_id');
20 | $table->unsignedBigInteger('product_attribute_id');
21 | $table->unsignedBigInteger('product_attribute_value_id');
22 |
23 | $table->foreign('product_id')
24 | ->references('id')
25 | ->on('products')
26 | ->onDelete('cascade');
27 |
28 | $table->foreign('product_sku_id')
29 | ->references('id')
30 | ->on('product_skus')
31 | ->onDelete('cascade');
32 |
33 | $table->foreign('product_attribute_id')
34 | ->references('id')
35 | ->on('product_attributes')
36 | ->onDelete('cascade');
37 |
38 | $table->foreign('product_attribute_value_id')
39 | ->references('id')
40 | ->on('product_attribute_values')
41 | ->onDelete('cascade');
42 |
43 | $table->unique(['product_id', 'product_attribute_id', 'product_sku_id'], 'product_variation_sku');
44 | // $table->unique(['product_id', 'product_attribute_id', 'product_attribute_value_id'], 'product_variation_attribute');
45 | });
46 | }
47 |
48 | /**
49 | * Reverse the migrations.
50 | *
51 | * @return void
52 | */
53 | public function down()
54 | {
55 | Schema::dropIfExists('product_variants');
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/database/migrations/2020_01_24_160109_create_inventory_stocks_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->unsignedBigInteger('warehouse_id');
19 | $table->unsignedBigInteger('product_sku_id');
20 | $table->integer('quantity')->default(0);
21 |
22 | $table->string('aisle')->nullable();
23 | $table->string('row')->nullable();
24 |
25 | $table->timestamps();
26 |
27 | $table->foreign('product_sku_id')
28 | ->references('id')
29 | ->on('product_skus')
30 | ->onDelete('cascade');
31 |
32 | $table->foreign('warehouse_id')
33 | ->references('id')
34 | ->on('warehouses')
35 | ->onDelete('cascade');
36 |
37 | $table->unique(['product_sku_id', 'warehouse_id'], 'product_warehouse_key');
38 | });
39 | }
40 |
41 | /**
42 | * Reverse the migrations.
43 | *
44 | * @return void
45 | */
46 | public function down()
47 | {
48 | Schema::dropIfExists('inventory_stocks');
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/database/migrations/2020_01_24_171541_create_inventory_stock_movements_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->unsignedBigInteger('stock_id');
19 | $table->integer('before')->default(0);
20 | $table->integer('after')->default(0);
21 | $table->decimal('cost', 8, 2)->default(0)->nullable();
22 | $table->string('reason')->nullable();
23 |
24 |
25 | $table->timestamps();
26 |
27 | $table->foreign('stock_id')
28 | ->references('id')
29 | ->on('inventory_stocks')
30 | ->onDelete('cascade');
31 | });
32 | }
33 |
34 | /**
35 | * Reverse the migrations.
36 | *
37 | * @return void
38 | */
39 | public function down()
40 | {
41 | Schema::dropIfExists('inventory_stock_movements');
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/docs/Adapters.md:
--------------------------------------------------------------------------------
1 | # Adapters
2 |
3 | Using adapters to transform it into array with similar structure from woocommerce product.
4 |
5 | ## Product Adapter
6 |
7 | ```php
8 | use Ronmrcdo\Inventory\Models\Product;
9 | use Ronmrcdo\Inventory\Adapters\ProductAdapter;
10 |
11 | $product = Product::create([
12 | 'name' => 'Test Product',
13 | 'short_description' => 'Lorem ipsum...',
14 | 'description' => 'Lorem ipsum dolor ...',
15 | 'category_id' => 1, // Category Model
16 | 'is_active' => true
17 | ]);
18 |
19 | $product->addSku('TESTPRODUCT1', 9.00, 4.00);
20 |
21 | $productResource = new ProductAdapter($product);
22 |
23 | return $productResource->transform();
24 | ```
25 |
26 | return array format
27 |
28 | ```php
29 | [
30 | "id" => 1
31 | "name" => "aut"
32 | "slug" => "aut"
33 | "sku" => "3vUfh6xi8hNhRaMv"
34 | "short_description" => "Recusandae est quo est exercitationem suscipit ipsam possimus. Voluptatibus sit unde laboriosam fugiat exercitationem rerum assumenda. Exercitationem esse minus corporis voluptatem debitis iure aut. Eum est eius qui iusto porro aut a distinctio. In saepe quia at ut sit reprehenderit."
35 | "description" => "Ea rerum omnis et magni ea quo nam. Debitis sapiente facere rerum unde magnam. A qui modi est ut cupiditate placeat. Ipsa magnam laboriosam voluptatem eaque consequatur ducimus. Rerum cum doloribus consequatur soluta in ut totam aliquid. Quod totam voluptas sed in praesentium enim quam id. Deleniti id modi et fugiat reprehenderit doloribus. Consequuntur reiciendis aut dolore accusamus sed rerum. Sit aut quae et voluptatum. Accusantium nihil molestias aut mollitia."
36 | "price" => "45.00"
37 | "cost" => "23.00"
38 | "is_active" => true
39 | "category" => [
40 | "id" => 1
41 | "name" => "illum architecto eveniet"
42 | ]
43 | "attributes" => []
44 | "variations" => []
45 | ]
46 |
47 | ```
48 |
49 | if the product has an attribute and variations, it will return an array like this.
50 |
51 | Output
52 | ```php
53 | [
54 | "id" => 1
55 | "name" => "veniam quasi"
56 | "slug" => "veniam-quasi"
57 | "sku" => "WOOPROTSHIRT-SMBLK"
58 | "short_description" => "Tempore rerum ratione tempora nulla. Blanditiis sit delectus consequatur tenetur. Ut iure quasi pariatur illo praesentium. Natus atque aut est non dolores. Qui quae ullam natus velit et."
59 | "description" => "Omnis consequatur suscipit aut sed. Ad molestiae architecto a consequatur necessitatibus. Voluptatibus ut fugit ducimus ipsum atque maxime quae. Libero error est atque a. Corporis aut sapiente sed minima aut suscipit corporis illum. Illum expedita autem itaque. Nostrum ab quia officia id eum maiores aut. Voluptas et rem eum unde. Provident eos sequi aliquam est occaecati omnis. Libero ratione sapiente laborum et praesentium enim quis omnis."
60 | "price" => "156"
61 | "cost" => "156"
62 | "is_active" => true
63 | "category" => [
64 | "id" => 1
65 | "name" => "possimus eaque dolorum"
66 | ]
67 | "attributes" => [
68 | [
69 | "id" => 1
70 | "name" => "size"
71 | "options" => [
72 | "small",
73 | "medium",
74 | "large"
75 | ]
76 | ],
77 | [
78 | "id" => 2
79 | "name" => "color"
80 | "options" => [
81 | "black",
82 | "white"
83 | ]
84 | ]
85 | ]
86 | "variations" => [
87 | [
88 | "id" => 1
89 | "parent_product_id" => "1"
90 | "sku" => "WOOPROTSHIRT-SMBLK"
91 | "name" => "veniam quasi"
92 | "short_description" => "Tempore rerum ratione tempora nulla. Blanditiis sit delectus consequatur tenetur. Ut iure quasi pariatur illo praesentium. Natus atque aut est non dolores. Qui quae ullam natus velit et."
93 | "description" => "Omnis consequatur suscipit aut sed. Ad molestiae architecto a consequatur necessitatibus. Voluptatibus ut fugit ducimus ipsum atque maxime quae. Libero error est atque a. Corporis aut sapiente sed minima aut suscipit corporis illum. Illum expedita autem itaque. Nostrum ab quia officia id eum maiores aut. Voluptas et rem eum unde. Provident eos sequi aliquam est occaecati omnis. Libero ratione sapiente laborum et praesentium enim quis omnis."
94 | "price" => "156"
95 | "cost" => "61"
96 | "category" => [
97 | "id" => "1"
98 | "name" => "possimus eaque dolorum"
99 | ]
100 | "attributes" => [
101 | [
102 | "name" => "color"
103 | "option" => "black"
104 | ],
105 | [
106 | "name" => "size"
107 | "option" => "small"
108 | ]
109 | ]
110 | ],
111 | [
112 | "id" => 2
113 | "parent_product_id" => "1"
114 | "sku" => "WOOPROTSHIRT-SMWHT"
115 | "name" => "veniam quasi"
116 | "short_description" => "Tempore rerum ratione tempora nulla. Blanditiis sit delectus consequatur tenetur. Ut iure quasi pariatur illo praesentium. Natus atque aut est non dolores. Qui quae ullam natus velit et."
117 | "description" => "Omnis consequatur suscipit aut sed. Ad molestiae architecto a consequatur necessitatibus. Voluptatibus ut fugit ducimus ipsum atque maxime quae. Libero error est atque a. Corporis aut sapiente sed minima aut suscipit corporis illum. Illum expedita autem itaque. Nostrum ab quia officia id eum maiores aut. Voluptas et rem eum unde. Provident eos sequi aliquam est occaecati omnis. Libero ratione sapiente laborum et praesentium enim quis omnis."
118 | "price" => "255"
119 | "cost" => "93"
120 | "category" => [
121 | "id" => "1"
122 | "name" => "possimus eaque dolorum"
123 | ]
124 | "attributes" => [
125 | [
126 | "name" => "color"
127 | "option" => "white"
128 | ],
129 | [
130 | "name" => "size"
131 | "option" => "small"
132 | ]
133 | ]
134 | ]
135 | ]
136 | ]
137 | ```
138 |
139 | using the ProductAdapter as ```collection```
140 |
141 | ```php
142 | use Ronmrcdo\Inventory\Models\Product;
143 | use Ronmrcdo\Inventory\Adapters\ProductAdapter;
144 |
145 | $product = Product::all();
146 |
147 | $productResourceCollection = ProductAdapter::collection($product);
148 |
149 | return $productResourceCollection;
150 |
151 | ```
152 |
153 | ## Product Variant Adapter
154 |
155 | ```php
156 | use Ronmrcdo\Inventory\Adapters\ProductVariantAdapter;
157 |
158 | $variantResource = ProductVariantAdapter::collection($product->getVariations());
159 |
160 | return $variantResource;
161 | ```
162 |
163 |
164 | ## ProductVariantAdapter
165 |
166 | ProductVariantAdapter only transform the product variation into array similar to the above functionality.
167 |
168 | ```php
169 | use Ronmrcdo\Inventory\Adapters\ProductVariantAdapter;
170 |
171 | $variantResource = new ProductVariantAdapter($product->findBySku('TSHIRT-SMBLK'))
172 |
173 | $variantResource->transform();
174 | ```
175 |
176 | It will return an array format like this
177 |
178 | ```php
179 | [
180 | "id" => 2
181 | "parent_product_id" => "1"
182 | "sku" => "TSHIRT-SMWHT"
183 | "name" => "harum"
184 | "short_description" => "Alias pariatur ab ut. Qui odit et qui placeat minus nulla voluptas. Possimus officia maxime in qui iste velit. Ex nesciunt quisquam iure rerum odio. Aut voluptas voluptatum sed."
185 | "description" => "Atque incidunt nostrum repellat cumque facilis. Reprehenderit dolorum nihil aut sed dolores dicta deserunt. Reprehenderit nisi aperiam velit vel et sit provident. Et nisi aperiam animi asperiores corporis architecto. Molestias velit ab esse quidem minima. Quod et quibusdam tempora laboriosam consequatur qui. Possimus ut sequi quam animi quos itaque et nobis. Accusamus minima ea et ex labore et similique. Amet repellendus distinctio consectetur error. Aliquam facere deserunt veritatis qui deleniti."
186 | "price" => "10"
187 | "cost" => "5"
188 | "category" => [
189 | "id" => "1"
190 | "name" => "est quis"
191 | ]
192 | "attributes" => [
193 | [
194 | "name" => "color"
195 | "option" => "white"
196 | ],
197 | [
198 | "name" => "size"
199 | "option" => "small"
200 | ]
201 | ]
202 | ```
203 |
204 | Using collection to list all the product variation and convert them into array format
205 |
206 | ```php
207 | use Ronmrcdo\Inventory\Adapters\ProductVariantAdapter;
208 |
209 | $variations = $product->getVariations();
210 | $variantResource = ProductVariantAdapter::collection($variations);
211 | ```
212 |
213 | it will return a collection of variant in array format
214 |
215 | ```php
216 | [
217 | 0 => [
218 | "id" => 2
219 | "parent_product_id" => "1"
220 | "sku" => "TSHIRT-SMWHT"
221 | "name" => "harum"
222 | "short_description" => "Alias pariatur ab ut. Qui odit et qui placeat minus nulla voluptas. Possimus officia maxime in qui iste velit. Ex nesciunt quisquam iure rerum odio. Aut voluptas voluptatum sed."
223 | "description" => "Atque incidunt nostrum repellat cumque facilis. Reprehenderit dolorum nihil aut sed dolores dicta deserunt. Reprehenderit nisi aperiam velit vel et sit provident. Et nisi aperiam animi asperiores corporis architecto. Molestias velit ab esse quidem minima. Quod et quibusdam tempora laboriosam consequatur qui. Possimus ut sequi quam animi quos itaque et nobis. Accusamus minima ea et ex labore et similique. Amet repellendus distinctio consectetur error. Aliquam facere deserunt veritatis qui deleniti."
224 | "price" => "10"
225 | "cost" => "5"
226 | "category" => [
227 | "id" => "1"
228 | "name" => "est quis"
229 | ]
230 | "attributes" => [
231 | [
232 | "name" => "color"
233 | "option" => "white"
234 | ],
235 | [
236 | "name" => "size"
237 | "option" => "small"
238 | ]
239 | ],
240 | 1 => [
241 | "id" => 2
242 | "parent_product_id" => "1"
243 | "sku" => "TSHIRT-SMBLK"
244 | "name" => "harum"
245 | "short_description" => "Alias pariatur ab ut. Qui odit et qui placeat minus nulla voluptas. Possimus officia maxime in qui iste velit. Ex nesciunt quisquam iure rerum odio. Aut voluptas voluptatum sed."
246 | "description" => "Atque incidunt nostrum repellat cumque facilis. Reprehenderit dolorum nihil aut sed dolores dicta deserunt. Reprehenderit nisi aperiam velit vel et sit provident. Et nisi aperiam animi asperiores corporis architecto. Molestias velit ab esse quidem minima. Quod et quibusdam tempora laboriosam consequatur qui. Possimus ut sequi quam animi quos itaque et nobis. Accusamus minima ea et ex labore et similique. Amet repellendus distinctio consectetur error. Aliquam facere deserunt veritatis qui deleniti."
247 | "price" => "10"
248 | "cost" => "5"
249 | "category" => [
250 | "id" => "1"
251 | "name" => "est quis"
252 | ]
253 | "attributes" => [
254 | [
255 | "name" => "color"
256 | "option" => "black"
257 | ],
258 | [
259 | "name" => "size"
260 | "option" => "small"
261 | ]
262 | ]
263 | ]
264 | ```
265 |
266 | ## Extending the Adapters
267 |
268 | If you want to extend or modify the adapters you can create your own new resource
269 | and use the BaseAdapter
270 |
271 | ```php
272 | setResource(ProductResource::collection($collection));
301 |
302 | return $resource->transform();
303 | }
304 | }
305 |
306 | >
--------------------------------------------------------------------------------
/docs/Attributes.md:
--------------------------------------------------------------------------------
1 | # Attribute and Terms
2 |
3 | Product attributes are options to enable product to have a variation
4 |
5 | ### Creating Product attribute and term
6 |
7 | There are two built-in functions in creating a product attribute. if there's an error occured during the creation of attribute or term it will throw an ```InvalidAttributeException```
8 |
9 | ```php
10 |
11 | // Create single attribute
12 | $product->addAttribute('size');
13 |
14 | // Create multiple attribute
15 | $product->addAttributes([
16 | ['name' => 'size'],
17 | ['name' => 'color']
18 | ]);
19 | ```
20 |
21 | ### Adding Term on attributes
22 |
23 | Terms are the values of attribute e.g for size (attribute) - small, medium, large (terms) and for color (attribute) - white, black and etc. (terms)
24 |
25 |
26 | ```php
27 |
28 | /**
29 | * @param string $attribute
30 | * @param array[String] $terms
31 | **/
32 | $product->addAttributeTerm('size', ['small', 'medium', 'large']);
33 | ```
34 |
35 | ### Removing Product Attribute and Terms
36 |
37 | Any error that will occur during removal of attribute or term will throw an ```InvalidAttributeException```
38 |
39 | ```php
40 | // Remove Attribute, take note that terms, skus, and stocks will also be deleted when
41 | // attribute is already established in the database.
42 | $product->removeAttribute('size');
43 |
44 | // Remove Attribute, take note that skus, and stocks will also be deleted when
45 | // attribute term is already established in the database.
46 | // Don't delete attributes or terms when it's already inserted in stocks or product skus
47 | $product->removeAttributeTerm('size', 'small');
48 | ```
49 |
50 | ### Buil-in functions questions and others.
51 |
52 | ```php
53 | // check if product has a attributes
54 | $product->hasAttributes();
55 |
56 | // check if product has a given attribute
57 | $product->hasAttribute('size');
58 |
59 | // Load product attributes and it's terms
60 | $product->loadAttributes();
61 | ```
--------------------------------------------------------------------------------
/docs/Category.md:
--------------------------------------------------------------------------------
1 | # Category
2 |
3 | Creating a category
4 |
5 | ```php
6 | use Ronmrcdo\Inventory\Models\Category;
7 |
8 | Category::create([
9 | 'name' => 'test-category',
10 | 'description' => 'Lorem ipsum...',
11 | 'parent_id' => null
12 | ]);
13 | ```
14 |
15 | Creating a category and sub category
16 |
17 | ```php
18 | use Ronmrcdo\Inventory\Models\Category;
19 |
20 | $parent = Category::create([
21 | 'name' => 'Test-Parent',
22 | 'description' => 'Lorem ipsum...',
23 | 'parent_id' => null
24 | ]);
25 |
26 | $child1 = $parent->children()->create([
27 | 'name' => 'Test-Child-1',
28 | 'description' => 'Lorem ipsum...',
29 | ]);
30 |
31 | $child2 = $parent->children()->create([
32 | 'name' => 'Test-Child-2',
33 | 'description' => 'Lorem ipsum...',
34 | ]);
35 | ```
36 |
37 | ## Category built-in functions
38 |
39 | ```php
40 |
41 | // Assert if the category has this product
42 | // @param mixed (string|int) product->name or product->id
43 | $category->hasProduct($product);
44 |
45 | // Assert if the category has this product by sku
46 | // @param string $sku
47 | $category->hasProductBySku($sku);
--------------------------------------------------------------------------------
/docs/Exceptions.md:
--------------------------------------------------------------------------------
1 | # Exceptions
2 |
3 | Here are the list of exceptions
4 |
5 | 1. ```InvalidAttributeException``` - it means there's an error regarding the given attribute or term.
6 |
7 | ```php
8 |
9 | use Ronmrcdo\Inventory\Exceptions\InvalidAttributeException;
10 |
11 | if ($err instanceof InvalidAttributeException) {
12 | // error
13 | }
14 | ```
15 |
16 | 2. ```InvalidVariantException``` - it means that there's an error regarding the given variation
17 |
18 | ex.
19 |
20 | 1. A variation with an attribute of size: small and color: black, then you decided to create a new variation with same attributes.
21 | 2. A duplicate sku.
22 | 3. double attribute with different term. ex. Color: black, Color: White, Size: Small.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/docs/Product.md:
--------------------------------------------------------------------------------
1 | # Product
2 |
3 | In this example we will show you on how to create a product that doesn't have a variation.
4 |
5 | ```php
6 | use Illuminate\Support\Facades\DB;
7 | use Ronmrcdo\Inventory\Models\Product;
8 |
9 | ...
10 |
11 | DB::beginTransaction();
12 |
13 | try {
14 | $product = Product::create([
15 | 'name' => 'Test Product',
16 | 'short_description' => 'Lorem ipsum...',
17 | 'description' => 'Lorem ipsum dolor ...',
18 | 'category_id' => 1, // Category Model
19 | 'is_active' => true
20 | ]);
21 |
22 | $product->addSku('TESTPRODUCT1', 9.00, 4.00);
23 |
24 | DB::commit();
25 | } catch (\Throwable $err) {
26 | DB::rollBack();
27 |
28 | }
29 | ```
30 |
31 | ## Product built-in functions
32 |
33 | ```php
34 | // return a bool if the product has an sku no matter if it's a variation sku
35 | $product->hasSku();
36 |
37 | // static function that will return a Product Sku Model
38 | $sku = Product::findBySku($sku);
39 |
40 | // Local Scope to find product by sku. Noted, it will return the parent product
41 | $product = Product::whereSku($sku)->firstOrFail();
42 | ```
43 |
44 | ## Extending Product and Category Model
45 |
46 | If you want to extend the product and category model like, adding attributes
47 | you can edit the config of laravel-inventory to use your product model and just
48 | extend the package product/category model
49 |
50 | ex.
51 | ```php
52 | \Your\Namespace\Product::class,
60 |
61 | /**
62 | * Base Category Class
63 | *
64 | */
65 | 'category' => \Your\Namespace\Category::class
66 | ];
67 | ```
68 |
69 | then in your Product model class
70 |
71 | ```php
72 |
86 | ```
87 |
88 | same with the category class
--------------------------------------------------------------------------------
/docs/Variation.md:
--------------------------------------------------------------------------------
1 | # Product Variation
2 |
3 | To create a product variation, product should have attribute and attribute term
4 |
5 | ## Creating variation
6 |
7 | If the attributes or terms doesn't exist in the product attributes, it will throw an ```InvalidAttributeException``` and if the variation attribute already exist in the product it will throw an ```InvalidVariantException```
8 |
9 | ```php
10 | $variant = [
11 | 'sku' => 'TSHIRT-SMBLK',
12 | 'price' => 10.00,
13 | 'cost' => 5.00,
14 | 'variation' => [
15 | ['option' => 'color', 'value' => 'black'],
16 | ['option' => 'size', 'value' => 'small'],
17 | ]
18 | ];
19 |
20 | $product->addVariant($variant);
21 | ```
22 |
23 | ### Product built-in functions for variations
24 |
25 | ```php
26 |
27 | // Return the variations in collection/model instance
28 | $product->getVariations();
29 |
30 | // Static function that will get the product variation
31 | Product::findBySku($sku);
32 | ```
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./tests/Unit
15 |
16 |
17 |
18 | ./tests/Feature
19 |
20 |
21 |
22 |
23 | src/
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
40 |
41 |
--------------------------------------------------------------------------------
/src/Adapters/BaseAdapter.php:
--------------------------------------------------------------------------------
1 | resource = $resource;
26 | }
27 |
28 | /**
29 | * Setter for resource if you want it to be a resource
30 | *
31 | * @param \Illuminate\Http\Resources\Json\JsonResource $resource
32 | * @return void
33 | */
34 | public function setResource($resource): void
35 | {
36 | $this->resource = $resource;
37 | }
38 |
39 | /**
40 | * Transform the resource into array
41 | *
42 | * @return array
43 | */
44 | public function transform(): array
45 | {
46 | return $this->resource->toArray(app('request'));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Adapters/ProductAdapter.php:
--------------------------------------------------------------------------------
1 | setResource(ProductResource::collection($collection));
30 |
31 | return $resource->transform();
32 | }
33 | }
--------------------------------------------------------------------------------
/src/Adapters/ProductAttributeAdapter.php:
--------------------------------------------------------------------------------
1 | setResource(AttributeResource::collection($collection));
30 |
31 | return $resource->transform();
32 | }
33 | }
--------------------------------------------------------------------------------
/src/Adapters/ProductVariantAdapter.php:
--------------------------------------------------------------------------------
1 | setResource(VariantResource::collection($collection));
30 |
31 | return $resource->transform();
32 | }
33 | }
--------------------------------------------------------------------------------
/src/Contracts/ResourceContract.php:
--------------------------------------------------------------------------------
1 | map(function ($term) {
52 | return ['value' => $term];
53 | })
54 | ->values()
55 | ->toArray();
56 |
57 | return $this->values()->createMany($terms);
58 | }
59 | return $this->values()->create(['value' => $value]);
60 | }
61 |
62 | /**
63 | * Remove a term on an attribute
64 | *
65 | * @param string $term
66 | */
67 | public function removeValue($term)
68 | {
69 | return $this->values()->where('value', $term)->firstOrFail()->delete();
70 | }
71 |
72 | /**
73 | * Relation of the attribute to the product
74 | *
75 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo $this
76 | */
77 | public function product(): BelongsTo
78 | {
79 | return $this->belongsTo(config('laravel-inventory.product'));
80 | }
81 |
82 | /**
83 | * Relation to the values
84 | *
85 | * @return \Illuminate\Database\Eloquent\Relations\HasMany $this
86 | */
87 | public function values(): HasMany
88 | {
89 | return $this->hasMany('Ronmrcdo\Inventory\Models\AttributeValue', 'product_attribute_id');
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Models/AttributeValue.php:
--------------------------------------------------------------------------------
1 | belongsTo('Ronmrcdo\Inventory\Models\Attribute', 'product_attribute_id');
51 | }
52 |
53 | /**
54 | * Relation of the attribute option to the variant
55 | *
56 | * @return \Illuminate\Database\Eloquent\Relations\HasMany $this
57 | */
58 | public function variations(): HasMany
59 | {
60 | return $this->hasMany('Ronmrcdo\Inventory\Models\ProductVariant');
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Models/Category.php:
--------------------------------------------------------------------------------
1 | parent_id);
46 | }
47 |
48 | /**
49 | * Local scope for getting only the parents
50 | *
51 | * @param \Illuminate\Database\Eloquent\Builder $query
52 | * @return \Illuminate\Database\Eloquent\Builder
53 | */
54 | public function scopeParentOnly($query)
55 | {
56 | return $query->whereNull('parent_id');
57 | }
58 |
59 | /**
60 | * Sub children relationship
61 | *
62 | * @return \Illuminate\Database\Eloquent\Relations\HasMany $this
63 | */
64 | public function children(): HasMany
65 | {
66 | return $this->hasMany(config('laravel-inventory.category'), 'parent_id', 'id');
67 | }
68 |
69 | /**
70 | * Parent Relationship
71 | *
72 | * @return \Illuminate\Database\Eloquent\Relations\HasOne $this
73 | */
74 | public function parent(): HasOne
75 | {
76 | return $this->hasOne(config('laravel-inventory.category'), 'id', 'parent_id');
77 | }
78 | }
--------------------------------------------------------------------------------
/src/Models/InventoryStock.php:
--------------------------------------------------------------------------------
1 | whereHas('product.code', function ($q) use ($sku) {
51 | $q->where('code', 'LIKE', '%'. $sku .'%');
52 | });
53 | }
54 |
55 | /**
56 | * Local scope to find an item based on product name
57 | *
58 | * @param \Illuminate\Database\Eloquent\Builder $query
59 | * @param string $sku
60 | * @return \Illuminate\Database\Eloquent\Builder
61 | */
62 | public function scopeFindItem(Builder $query, string $product): Builder
63 | {
64 | return $query->whereHas('product.product', function ($q) use ($product) {
65 | return $q->where('name', 'LIKE', '%'. $product .'%');
66 | });
67 | }
68 |
69 | /**
70 | * Relation to the warehouse
71 | *
72 | * @return \lluminate\Database\Eloquent\Relations\BelongsTo
73 | */
74 | public function warehouse(): BelongsTo
75 | {
76 | return $this->belongsTo('Ronmrcdo\Inventory\Models\Warehouse');
77 | }
78 |
79 | /**
80 | * Relation to the product
81 | *
82 | * @return \lluminate\Database\Eloquent\Relations\BelongsTo
83 | */
84 | public function product(): BelongsTo
85 | {
86 | return $this->belongsTo('Ronmrcdo\Inventory\Models\ProductSku' ,'product_sku_id');
87 | }
88 |
89 | /**
90 | * Relation to the stock Movements
91 | *
92 | * @return \lluminate\Database\Eloquent\Relations\HasMany
93 | */
94 | public function movements(): HasMany
95 | {
96 | return $this->hasMany('Ronmrcdo\Inventory\Models\InventoryStockMovement', 'stock_id', 'id');
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/Models/InventoryStockMovement.php:
--------------------------------------------------------------------------------
1 | 'boolean'
52 | ];
53 |
54 | /**
55 | * Sluggable field of the model
56 | *
57 | * @var string
58 | */
59 | protected $sluggable = 'name';
60 | }
--------------------------------------------------------------------------------
/src/Models/ProductSku.php:
--------------------------------------------------------------------------------
1 | belongsTo(config('laravel-inventory.product'));
51 | }
52 |
53 | /**
54 | * Product Variant
55 | *
56 | * @return \Illuminate\Database\Eloquent\Relations\HasMany
57 | */
58 | public function variant(): HasMany
59 | {
60 | return $this->hasMany('Ronmrcdo\Inventory\Models\ProductVariant', 'product_sku_id');
61 | }
62 |
63 | /**
64 | * Product sku has many stocks
65 | *
66 | * @return \Illuminate\Database\Eloquent\Relations\HasMany
67 | */
68 | public function stocks(): HasMany
69 | {
70 | return $this->hasMany('Ronmrcdo\Inventory\Models\InventoryStock');
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/Models/ProductVariant.php:
--------------------------------------------------------------------------------
1 | belongsTo('Ronmrcdo\Inventory\Models\ProductSku', 'product_sku_id');
51 | }
52 |
53 | /**
54 | * Relation of the variation to the product
55 | *
56 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
57 | */
58 | public function product(): BelongsTo
59 | {
60 | return $this->belongsTo(config('laravel-inventory.product'));
61 | }
62 |
63 | /**
64 | * Relation of the variation product to the attribute
65 | *
66 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
67 | */
68 | public function attribute(): BelongsTo
69 | {
70 | return $this->belongsTo('Ronmrcdo\Inventory\Models\Attribute', 'product_attribute_id');
71 | }
72 |
73 | /**
74 | * Relation of the variation product to the attribute option
75 | *
76 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
77 | */
78 | public function option(): BelongsTo
79 | {
80 | return $this->belongsTo('Ronmrcdo\Inventory\Models\AttributeValue', 'product_attribute_value_id');
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/Models/Warehouse.php:
--------------------------------------------------------------------------------
1 | hasMany('Ronmrcdo\Inventory\Models\InventoryStock');
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/ProductServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->runningInConsole()) {
18 | $this->bootForConsole();
19 | }
20 | }
21 |
22 | /**
23 | * Register the configurations
24 | *
25 | * @return void
26 | */
27 | public function register(): void
28 | {
29 | $this->mergeConfigFrom(__DIR__.'/../config/laravel-inventory.php', 'laravel-inventory');
30 | }
31 |
32 | /**
33 | * Get the services provided by the provider.
34 | *
35 | * @return array
36 | */
37 | public function provides()
38 | {
39 | return ['inventory'];
40 | }
41 |
42 | /**
43 | * Register the bootable configurations
44 | *
45 | * @return void
46 | */
47 | protected function bootForConsole(): void
48 | {
49 | $this->publishes([
50 | __DIR__.'/../config/laravel-inventory.php' => base_path('config/laravel-inventory.php'),
51 | ], 'config');
52 |
53 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
54 |
55 | $this->publishes([
56 | __DIR__.'/../database/migrations' => database_path('migrations')
57 | ], 'migrations');
58 | }
59 | }
--------------------------------------------------------------------------------
/src/Resources/AttributeResource.php:
--------------------------------------------------------------------------------
1 | $this->id,
13 | 'name' => $this->name,
14 | 'options' => collect($this->values)->map(function ($item) {
15 | return $item->value;
16 | })->values()->toArray()
17 | ];
18 | }
19 | }
--------------------------------------------------------------------------------
/src/Resources/ProductResource.php:
--------------------------------------------------------------------------------
1 | $this->id,
21 | 'name' => $this->name,
22 | 'slug' => $this->slug,
23 | 'sku' => $this->hasSku() ? $this->skus()->first()->code : null,
24 | 'short_description' => $this->short_description,
25 | 'description' => $this->description,
26 | 'price' => $this->hasSku() ? number_format($this->skus()->first()->price, 2, '.', '') : 0.00,
27 | 'cost' => $this->hasSku() ? number_format($this->skus()->first()->price, 2, '.', '') : 0.00,
28 | 'is_active' => $this->is_active,
29 | 'category' => [
30 | 'id' => $this->category->id,
31 | 'name' => $this->category->name
32 | ],
33 | 'attributes' => AttributeResource::collection($this->attributes)->toArray(app('request')),
34 | 'variations' => $this->when($this->hasAttributes() && $this->hasSku(),
35 | VariantResource::collection($this->skus)->toArray(app('request'))
36 | )
37 | ];
38 | }
39 | }
--------------------------------------------------------------------------------
/src/Resources/VariantResource.php:
--------------------------------------------------------------------------------
1 | $this->id,
13 | 'parent_product_id' => $this->product_id,
14 | 'sku' => $this->code,
15 | 'name' => $this->product->name,
16 | 'short_description' => $this->product->short_description,
17 | 'description' => $this->product->description,
18 | 'price' => number_format($this->price, 2, '.', ''),
19 | 'cost' => number_format($this->cost, 2, '.', ''),
20 | 'category' => [
21 | 'id' => $this->product->category_id,
22 | 'name' => $this->product->category->name
23 | ],
24 | 'attributes' => collect($this->variant)->map(function ($item) {
25 | return [
26 | 'name' => $item->attribute->name,
27 | 'option' => $item->option->value
28 | ];
29 | })->values()->toArray()
30 | ];
31 | }
32 | }
--------------------------------------------------------------------------------
/src/Traits/HasAttributes.php:
--------------------------------------------------------------------------------
1 | attributes()->create(['name' => $attribute]);
24 |
25 | DB::commit();
26 | } catch (\Throwable $err) { // No matter what error will occur we should throw invalidAttribute
27 | DB::rollBack();
28 |
29 | throw new InvalidAttributeException($err->getMessage(), 422);
30 | }
31 |
32 | return $this;
33 | }
34 |
35 | /**
36 | * Create multiple attributes
37 | *
38 | * @param mixed $attributes
39 | * @throw \Ronmrcdo\Inventory\Exceptions\InvalidAttributeException
40 | * @return $this
41 | */
42 | public function addAttributes($attributes)
43 | {
44 | DB::beginTransaction();
45 |
46 | try {
47 | $this->attributes()->createMany($attributes);
48 |
49 | DB::commit();
50 | } catch (\Throwable $err) { // No matter what error will occur we should throw invalidAttribute
51 | DB::rollBack();
52 |
53 | throw new InvalidAttributeException($err->getMessage(), 422);
54 | }
55 |
56 | return $this;
57 | }
58 |
59 | /**
60 | * It should remove attribute from product
61 | *
62 | * @param string $key
63 | * @return self
64 | */
65 | public function removeAttribute($attr)
66 | {
67 | DB::beginTransaction();
68 |
69 | try {
70 | $attribute = $this->attributes()->where('name', $attr)->firstOrFail();
71 |
72 | $attribute->delete();
73 |
74 | DB::commit();
75 | } catch (\Throwable $err) { // No matter what error will occur we should throw invalidAttribute
76 | DB::rollBack();
77 |
78 | throw new InvalidAttributeException($err->getMessage(), 422);
79 | }
80 |
81 | return $this;
82 | }
83 |
84 | /**
85 | * It should remove attribute from product
86 | *
87 | * @param string $key
88 | * @return self
89 | */
90 | public function removeAttributeTerm(string $attribute, string $term)
91 | {
92 | DB::beginTransaction();
93 |
94 | try {
95 | $attribute = $this->attributes()->where('name', $attribute)->firstOrFail();
96 |
97 | $attribute->removeValue($term);
98 |
99 | DB::commit();
100 | } catch (\Throwable $err) { // No matter what error will occur we should throw invalidAttribute
101 | DB::rollBack();
102 |
103 | throw new InvalidAttributeException($err->getMessage(), 422);
104 | }
105 |
106 | return $this;
107 | }
108 |
109 | /**
110 | * Assert if the Product has attributes
111 | *
112 | * @return bool
113 | */
114 | public function hasAttributes(): bool
115 | {
116 | return !! $this->attributes()->count();
117 | }
118 |
119 | /**
120 | * Assert if the product has this attributes
121 | *
122 | * @param string|int $key
123 | *
124 | * @return bool
125 | */
126 | public function hasAttribute($key): bool
127 | {
128 | // If the arg is a numeric use the id else use the name
129 | if (is_numeric($key)) {
130 | return $this->attributes()->where('id', $key)->exists();
131 | } elseif (is_string($key)) {
132 | return $this->attributes()->where('name', $key)->exists();
133 | }
134 |
135 | return false;
136 | }
137 |
138 | /**
139 | * Add Option Value on the attribute
140 | *
141 | * @param string $option
142 | * @param mixed $value
143 | *
144 | * @throw \Ronmrcdo\Inventory\Exceptions\InvalidAttributeException
145 | *
146 | * @return \Ronmrcdo\Inventory\Models\AttributeValue
147 | */
148 | public function addAttributeTerm(string $option, $value)
149 | {
150 | $attribute = $this->attributes()->where('name', $option)->first();
151 |
152 | if (! $attribute) {
153 | throw new InvalidAttributeException("Invalid attribute", 422);
154 | }
155 |
156 | return $attribute->addValue($value);
157 | }
158 |
159 | /**
160 | * Get Product Attributes
161 | *
162 | *
163 | */
164 | public function loadAttributes()
165 | {
166 | return $this->attributes()->get()->load('values');
167 | }
168 |
169 | /**
170 | * Relation on Attribute Model
171 | *
172 | * @return \Illuminate\Database\Eloquent\Relations\HasMany $this
173 | */
174 | public function attributes(): HasMany
175 | {
176 | return $this->hasMany('Ronmrcdo\Inventory\Models\Attribute');
177 | }
178 | }
--------------------------------------------------------------------------------
/src/Traits/HasCategories.php:
--------------------------------------------------------------------------------
1 | belongsTo(config('laravel-inventory.category'));
17 | }
18 | }
--------------------------------------------------------------------------------
/src/Traits/HasInventory.php:
--------------------------------------------------------------------------------
1 | warehouses)
19 | ->map(function ($item) {
20 | return $item->warehouse;
21 | })
22 | ->unique('id')
23 | ->values();
24 | }
25 |
26 | /**
27 | * Relation to get the stocks in inventory
28 | *
29 | * @param \Illuminate\Database\Eloquent\Relations\HasManyThrough
30 | */
31 | public function warehouses(): HasManyThrough
32 | {
33 | return $this->hasManyThrough('Ronmrcdo\Inventory\Models\InventoryStock', 'Ronmrcdo\Inventory\Models\ProductSku')->with('warehouse');
34 | }
35 | }
--------------------------------------------------------------------------------
/src/Traits/HasItemMovements.php:
--------------------------------------------------------------------------------
1 | stock;
19 |
20 | return $stock->rollback($this, $recursive);
21 | }
22 |
23 | /**
24 | * Stock relation
25 | *
26 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
27 | */
28 | public function stock(): BelongsTo
29 | {
30 | return $this->belongsTo('Ronmrcdo\Inventory\Models\InventoryStock', 'stock_id', 'id');
31 | }
32 | }
--------------------------------------------------------------------------------
/src/Traits/HasItemStocks.php:
--------------------------------------------------------------------------------
1 | reason) {
42 | $model->reason = 'First Item Record; Stock Increase';
43 | }
44 | });
45 |
46 | static::created(function (Model $model) {
47 | $model->postCreate();
48 | });
49 |
50 | static::updating(function (Model $model) {
51 | /*
52 | * Retrieve the original quantity before it was updated,
53 | * so we can create generate an update with it
54 | */
55 | $model->beforeQuantity = $model->getOriginal('quantity');
56 |
57 | /*
58 | * Check if a reason has been set, if not let's retrieve the default change reason
59 | */
60 | if (!$model->reason) {
61 | $model->reason = 'Stock Adjustment';
62 | }
63 | });
64 |
65 | static::updated(function (Model $model) {
66 | $model->postUpdate();
67 | });
68 | }
69 |
70 | /**
71 | * Generates a stock movement on the creation of a stock.
72 | */
73 | public function postCreate()
74 | {
75 | if (!$this->getLastMovement()) {
76 | $this->generateStockMovement(0, $this->quantity, $this->reason, $this->cost);
77 | }
78 | }
79 |
80 | /**
81 | * Generates a stock movement after a stock is updated.
82 | */
83 | public function postUpdate()
84 | {
85 | $this->generateStockMovement($this->beforeQuantity, $this->quantity, $this->reason, $this->cost);
86 | }
87 |
88 | /**
89 | * Performs a quantity update. Automatically determining
90 | * depending on the quantity entered if stock is being taken
91 | * or added.
92 | *
93 | * @param int|float|string $quantity
94 | * @param string $reason
95 | * @param int|float|string $cost
96 | *
97 | * @throws InvalidQuantityException
98 | *
99 | * @return $this
100 | */
101 | public function updateQuantity($quantity, $reason = '', $cost = 0)
102 | {
103 | if ($this->isValidQuantity($quantity)) {
104 | return $this->processUpdateQuantityOperation($quantity, $reason, $cost);
105 | }
106 | }
107 |
108 | /**
109 | * Removes the specified quantity from the current stock.
110 | *
111 | * @param int|float|string $quantity
112 | * @param string $reason
113 | * @param int|float|string $cost
114 | *
115 | * @return $this|bool
116 | */
117 | public function remove($quantity, $reason = '', $cost = 0)
118 | {
119 | return $this->take($quantity, $reason, $cost);
120 | }
121 |
122 | /**
123 | * Processes a 'take' operation on the current stock.
124 | *
125 | * @param int|float|string $quantity
126 | * @param string $reason
127 | * @param int|float|string $cost
128 | *
129 | * @throws InvalidQuantityException
130 | * @throws NotEnoughStockException
131 | *
132 | * @return $this|bool
133 | */
134 | public function take($quantity, $reason = '', $cost = 0)
135 | {
136 | if ($this->isValidQuantity($quantity) && $this->hasEnoughStock($quantity)) {
137 | return $this->processTakeOperation($quantity, $reason, $cost);
138 | }
139 | }
140 |
141 | /**
142 | * Alias for put function.
143 | *
144 | * @param int|float|string $quantity
145 | * @param string $reason
146 | * @param int|float|string $cost
147 | *
148 | * @return $this
149 | */
150 | public function add($quantity, $reason = '', $cost = 0)
151 | {
152 | return $this->put($quantity, $reason, $cost);
153 | }
154 |
155 | /**
156 | * Processes a 'put' operation on the current stock.
157 | *
158 | * @param int|float|string $quantity
159 | * @param string $reason
160 | * @param int|float|string $cost
161 | *
162 | * @throws InvalidQuantityException
163 | *
164 | * @return $this
165 | */
166 | public function put($quantity, $reason = '', $cost = 0)
167 | {
168 | if ($this->isValidQuantity($quantity)) {
169 | return $this->processPutOperation($quantity, $reason, $cost);
170 | }
171 | }
172 |
173 | /**
174 | * Moves a stock to the specified location.
175 | *
176 | * @param $location
177 | *
178 | * @return bool
179 | */
180 | public function moveTo($location)
181 | {
182 | $location = $this->getLocation($location);
183 |
184 | return $this->processMoveOperation($location);
185 | }
186 |
187 | /**
188 | * Rolls back the last movement, or the movement specified. If recursive is set to true,
189 | * it will rollback all movements leading up to the movement specified.
190 | *
191 | * @param mixed $movement
192 | * @param bool $recursive
193 | *
194 | * @return $this|bool
195 | */
196 | public function rollback($movement = null, $recursive = false)
197 | {
198 | if ($movement) {
199 | return $this->rollbackMovement($movement, $recursive);
200 | } else {
201 | $movement = $this->getLastMovement();
202 |
203 | if ($movement) {
204 | return $this->processRollbackOperation($movement, $recursive);
205 | }
206 | }
207 |
208 | return false;
209 | }
210 |
211 | /**
212 | * Rolls back a specific movement.
213 | *
214 | * @param mixed $movement
215 | * @param bool $recursive
216 | *
217 | * @throws InvalidMovementException
218 | *
219 | * @return $this|bool
220 | */
221 | public function rollbackMovement($movement, $recursive = false)
222 | {
223 | $movement = $this->getMovement($movement);
224 |
225 | return $this->processRollbackOperation($movement, $recursive);
226 | }
227 |
228 | /**
229 | * Returns true if there is enough stock for the specified quantity being taken.
230 | * Throws NotEnoughStockException otherwise.
231 | *
232 | * @param int|float|string $quantity
233 | *
234 | * @throws NotEnoughStockException
235 | *
236 | * @return bool
237 | */
238 | public function hasEnoughStock($quantity = 0)
239 | {
240 | /*
241 | * Using double equals for validation of complete value only, not variable type. For example:
242 | * '20' (string) equals 20 (int)
243 | */
244 | if ($this->quantity == $quantity || $this->quantity > $quantity) {
245 | return true;
246 | }
247 |
248 | $message = 'Not enough stock. Tried to take '. $quantity.' but only '. $this->quantity .' is available';
249 |
250 | throw new NotEnoughStockException($message);
251 | }
252 |
253 | /**
254 | * Returns the last movement on the current stock record.
255 | *
256 | * @return mixed
257 | */
258 | public function getLastMovement()
259 | {
260 | $movement = $this->movements()->orderBy('created_at', 'DESC')->first();
261 |
262 | if ($movement) {
263 | return $movement;
264 | }
265 |
266 | return false;
267 | }
268 |
269 | /**
270 | * Returns a movement depending on the specified argument. If an object is supplied, it is checked if it
271 | * is an instance of an eloquent model. If a numeric value is entered, it is retrieved by it's ID.
272 | *
273 | * @param mixed $movement
274 | *
275 | * @throws InvalidMovementException
276 | *
277 | * @return mixed
278 | */
279 | public function getMovement($movement)
280 | {
281 | if ($this->isModel($movement)) {
282 | return $movement;
283 | } elseif (is_numeric($movement)) {
284 | return $this->getMovementById($movement);
285 | } else {
286 | $message = 'Movement '. $movement .' is invalid';
287 |
288 | throw new InvalidMovementException($message);
289 | }
290 | }
291 |
292 | /**
293 | * Creates and returns a new un-saved stock transaction
294 | * instance with the current stock ID attached.
295 | *
296 | * @param string $name
297 | *
298 | * @return \Illuminate\Database\Eloquent\Model
299 | */
300 | public function newTransaction($name = '')
301 | {
302 | $transaction = $this->transactions()->getRelated();
303 |
304 | /*
305 | * Set the transaction attributes so they don't
306 | * need to be set manually
307 | */
308 | $transaction->stock_id = $this->getKey();
309 | $transaction->name = $name;
310 |
311 | return $transaction;
312 | }
313 |
314 | /**
315 | * Retrieves a movement by the specified ID.
316 | *
317 | * @param int|string $id
318 | *
319 | * @return mixed
320 | */
321 | private function getMovementById($id)
322 | {
323 | return $this->movements()->find($id);
324 | }
325 |
326 | /**
327 | * Processes a quantity update operation.
328 | *
329 | * @param int|float|string $quantity
330 | * @param string $reason
331 | * @param int|float|string $cost
332 | *
333 | * @return $this
334 | */
335 | private function processUpdateQuantityOperation($quantity, $reason = '', $cost = 0)
336 | {
337 | if ($quantity > $this->quantity) {
338 | $putting = $quantity - $this->quantity;
339 |
340 | return $this->put($putting, $reason, $cost);
341 | } else {
342 | $taking = $this->quantity - $quantity;
343 |
344 | return $this->take($taking, $reason, $cost);
345 | }
346 | }
347 |
348 | /**
349 | * Processes removing quantity from the current stock.
350 | *
351 | * @param int|float|string $taking
352 | * @param string $reason
353 | * @param int|float|string $cost
354 | *
355 | * @return $this|bool
356 | */
357 | private function processTakeOperation($taking, $reason = '', $cost = 0)
358 | {
359 | $left = $this->quantity - $taking;
360 |
361 | /*
362 | * If the updated total and the beginning total are the same, we'll check if
363 | * duplicate movements are allowed. We'll return the current record if
364 | * they aren't.
365 | */
366 | if ($left == $this->quantity && !$this->allowDuplicateMovementsEnabled()) {
367 | return $this;
368 | }
369 |
370 | $this->quantity = $left;
371 |
372 | $this->setReason($reason);
373 |
374 | $this->setCost($cost);
375 |
376 | DB::beginTransaction();
377 |
378 | try {
379 | if ($this->save()) {
380 |
381 | $this->fireModelEvent('inventory.stock.taken', [
382 | 'stock' => $this,
383 | ]);
384 |
385 | return $this;
386 | }
387 | } catch (\Exception $e) {
388 | DB::rollBack();
389 | }
390 |
391 | return false;
392 | }
393 |
394 | /**
395 | * Processes adding quantity to current stock.
396 | *
397 | * @param int|float|string $putting
398 | * @param string $reason
399 | * @param int|float|string $cost
400 | *
401 | * @return $this|bool
402 | */
403 | private function processPutOperation($putting, $reason = '', $cost = 0)
404 | {
405 | $before = $this->quantity;
406 |
407 | $total = $putting + $before;
408 |
409 | /*
410 | * If the updated total and the beginning total are the same,
411 | * we'll check if duplicate movements are allowed
412 | */
413 | if ($total == $this->quantity && !$this->allowDuplicateMovementsEnabled()) {
414 | return $this;
415 | }
416 |
417 | $this->quantity = $total;
418 |
419 | $this->setReason($reason);
420 |
421 | $this->setCost($cost);
422 |
423 | DB::beginTransaction();
424 |
425 | try {
426 | if ($this->save()) {
427 | DB::commit();
428 |
429 | $this->fireModelEvent('inventory.stock.added', [
430 | 'stock' => $this,
431 | ]);
432 |
433 | return $this;
434 | }
435 | } catch (\Exception $e) {
436 | DB::rollBack();
437 | }
438 |
439 | return false;
440 | }
441 |
442 | /**
443 | * Processes the stock moving from it's current
444 | * location, to the specified location.
445 | *
446 | * @param mixed $location
447 | *
448 | * @return bool
449 | */
450 | private function processMoveOperation(Model $location)
451 | {
452 | $this->location_id = $location->getKey();
453 |
454 | DB::beginTransaction();
455 |
456 | try {
457 | if ($this->save()) {
458 |
459 |
460 | $this->fireModelEvent('inventory.stock.moved', [
461 | 'stock' => $this,
462 | ]);
463 |
464 | DB::commit();
465 | return $this;
466 | }
467 | } catch (\Exception $e) {
468 | DB::rollBack();
469 | }
470 |
471 | return false;
472 | }
473 |
474 | /**
475 | * Processes a single rollback operation.
476 | *
477 | * @param mixed $movement
478 | * @param bool $recursive
479 | *
480 | * @return $this|bool
481 | */
482 | private function processRollbackOperation(Model $movement, $recursive = false)
483 | {
484 | if ($recursive) {
485 | return $this->processRecursiveRollbackOperation($movement);
486 | }
487 |
488 | $this->quantity = $movement->before;
489 |
490 | $reason = 'Rolled back to movement ID: '. $movement->getOriginal('id') .' on '. $movement->getOriginal('created_at');
491 |
492 | $this->setReason($reason);
493 |
494 | if ($this->rollbackCostEnabled()) {
495 | $this->setCost($movement->cost);
496 |
497 | $this->reverseCost();
498 | }
499 |
500 | DB::beginTransaction();
501 |
502 | try {
503 | if ($this->save()) {
504 | DB::commit();
505 |
506 | $this->fireModelEvent('inventory.stock.rollback', [
507 | 'stock' => $this,
508 | ]);
509 |
510 | return $this;
511 | }
512 | } catch (\Exception $e) {
513 | DB::rollBack();
514 | }
515 |
516 | return false;
517 | }
518 |
519 | /**
520 | * Processes a recursive rollback operation.
521 | *
522 | * @param mixed $movement
523 | *
524 | * @return array
525 | */
526 | private function processRecursiveRollbackOperation(Model $movement)
527 | {
528 | /*
529 | * Retrieve movements that were created after
530 | * the specified movement, and order them descending
531 | */
532 | $movements = $this
533 | ->movements()
534 | ->where('created_at', '>=', $movement->getOriginal('created_at'))
535 | ->orderBy('created_at', 'DESC')
536 | ->get();
537 |
538 | $rollbacks = [];
539 |
540 | if ($movements->count() > 0) {
541 | foreach ($movements as $movement) {
542 | $rollbacks = $this->processRollbackOperation($movement);
543 | }
544 | }
545 |
546 | return $rollbacks;
547 | }
548 |
549 | /**
550 | * Creates a new stock movement record.
551 | *
552 | * @param int|float|string $before
553 | * @param int|float|string $after
554 | * @param string $reason
555 | * @param int|float|string $cost
556 | *
557 | * @return \Illuminate\Database\Eloquent\Model
558 | */
559 | private function generateStockMovement($before, $after, $reason = '', $cost = 0)
560 | {
561 | $insert = [
562 | 'stock_id' => $this->getKey(),
563 | 'before' => $before,
564 | 'after' => $after,
565 | 'reason' => $reason,
566 | 'cost' => $cost,
567 | ];
568 |
569 | return $this->movements()->create($insert);
570 | }
571 |
572 | /**
573 | * Sets the cost attribute.
574 | *
575 | * @param int|float|string $cost
576 | */
577 | private function setCost($cost = 0)
578 | {
579 | $this->cost = $cost;
580 | }
581 |
582 | /**
583 | * Reverses the cost of a movement.
584 | */
585 | private function reverseCost()
586 | {
587 | if ($this->isPositive($this->cost)) {
588 | $this->setCost(-abs($this->cost));
589 | } else {
590 | $this->setCost(abs($this->cost));
591 | }
592 | }
593 |
594 | /**
595 | * Sets the reason attribute.
596 | *
597 | * @param string $reason
598 | */
599 | private function setReason($reason = '')
600 | {
601 | $this->reason = $reason;
602 | }
603 |
604 | /**
605 | * Returns true/false from the configuration file determining
606 | * whether or not stock movements can have the same before and after
607 | * quantities.
608 | *
609 | * @return bool
610 | */
611 | private function allowDuplicateMovementsEnabled()
612 | {
613 | return false;
614 | }
615 |
616 | /**
617 | * Returns true/false from the configuration file determining
618 | * whether or not to rollback costs when a rollback occurs on
619 | * a stock.
620 | *
621 | * @return bool
622 | */
623 | private function rollbackCostEnabled()
624 | {
625 | return true;
626 | }
627 |
628 | }
629 |
--------------------------------------------------------------------------------
/src/Traits/HasProducts.php:
--------------------------------------------------------------------------------
1 | products()->where('id', $product)->exists();
19 | } elseif (is_string($product)) {
20 | return $this->products()->where('name', $product)->exists();
21 | }
22 |
23 | return false;
24 | }
25 |
26 | /**
27 | * Assert if the Category has a product based on sku
28 | *
29 | * @param string $sku
30 | * @return bool
31 | */
32 | public function hasProductBySku(string $sku): bool
33 | {
34 | return $this->products()->whereHas('skus', function ($q) use ($sku) {
35 | $q->where('code', $sku);
36 | })->exists();
37 | }
38 |
39 | /**
40 | * Relation on the product
41 | *
42 | * @return \Illuminate\Database\Eloquent\Relations\HasMany $this
43 | */
44 | public function products(): HasMany
45 | {
46 | return $this->hasMany(config('laravel-inventory.product'));
47 | }
48 | }
--------------------------------------------------------------------------------
/src/Traits/HasVariants.php:
--------------------------------------------------------------------------------
1 | sortAttributes($variant['variation']), $this->getVariants())) {
30 | throw new InvalidVariantException("Duplicate variation attributes!", 400);
31 | }
32 |
33 | // Create the sku first, so basically you can't add new attributes to the sku
34 | $sku = $this->skus()->create([
35 | 'code' => $variant['sku'],
36 | 'price' => $variant['price'],
37 | 'cost' => $variant['cost']
38 | ]);
39 |
40 | foreach ($variant['variation'] as $item) {
41 | $attribute = $this->attributes()->where('name', $item['option'])->firstOrFail();
42 | $value = $attribute->values()->where('value', $item['value'])->firstOrFail();
43 |
44 | $this->variations()->create([
45 | 'product_sku_id' => $sku->id,
46 | 'product_attribute_id' => $attribute->id,
47 | 'product_attribute_value_id' => $value->id
48 | ]);
49 | }
50 |
51 | DB::commit();
52 | } catch (ModelNotFoundException $err) {
53 | DB::rollBack();
54 |
55 | throw new InvalidAttributeException($err->getMessage(), 404);
56 |
57 | } catch (\Throwable $err) {
58 | DB::rollBack();
59 |
60 | throw new InvalidVariantException($err->getMessage(), 400);
61 | }
62 |
63 | return $this;
64 | }
65 |
66 | /**
67 | * Get the variations
68 | *
69 | */
70 | public function getVariations()
71 | {
72 | return $this->skus;
73 | }
74 |
75 | /**
76 | * Get existing variants of the product
77 | * Note: There was a problem calling $this->variation relationship
78 | * it doesn't update model about the relationship that's why it always
79 | * return []
80 | *
81 | * @return array
82 | */
83 | protected function getVariants(): array
84 | {
85 | $variants = ProductVariant::where('product_id' , $this->id)->get();
86 |
87 | return $this->transformVariant($variants);
88 | }
89 |
90 | /**
91 | * Sort the variant attributes by name. this is a helper function
92 | * to assert if the variant attributes already exist.
93 | *
94 | * @param array $variant
95 | * @return array
96 | */
97 | protected function sortAttributes($variant): array
98 | {
99 | return collect($variant)
100 | ->sortBy('option')
101 | ->map(function ($item) {
102 | return [
103 | 'option' => strtolower($item['option']),
104 | 'value' => strtolower($item['value'])
105 | ];
106 | })
107 | ->values()
108 | ->toArray();
109 | }
110 |
111 | /**
112 | * Transform the variant to match it to the input
113 | * variant. To able to assert if the given new variant
114 | * already exist with the current variations
115 | *
116 | * @param \Ronmrcdo\Inventory\Models\ProductVariant Array<$variants>
117 | * @return array
118 | */
119 | protected function transformVariant($variants): array
120 | {
121 | return collect($variants)
122 | ->map(function ($item) {
123 | return [
124 | 'id' => $item->id,
125 | 'sku' => $item->productSku->code,
126 | 'attribute' => $item->attribute->name,
127 | 'option' => $item->option->value
128 | ];
129 | })
130 | ->keyBy('id')
131 | ->groupBy('sku')
132 | ->map(function ($item) {
133 | return collect($item)
134 | ->map(function ($var) {
135 | return [
136 | 'option' => strtolower($var['attribute']),
137 | 'value' => strtolower($var['option'])
138 | ];
139 | })
140 | ->sortBy('option')
141 | ->values()
142 | ->toArray();
143 | })
144 | ->all();
145 | }
146 |
147 | /**
148 | * Assert if the product has any sku given in the db
149 | *
150 | * @return bool
151 | */
152 | public function hasSku(): bool
153 | {
154 | return !! $this->skus()->count();
155 | }
156 |
157 | /**
158 | * Static function that automatically query for the sku
159 | *
160 | * @param string $sku
161 | * @return \Ronmrcdo\Inventory\Models\Product
162 | */
163 | public static function findBySku(string $sku)
164 | {
165 | return ProductSku::where('code', $sku)->firstOrFail();
166 | }
167 |
168 | /**
169 | * Scope for Find Product by sku
170 | *
171 | * @param \Illuminate\Database\Eloquent\Builder $query
172 | * @param string $sku
173 | * @return \Illuminate\Database\Eloquent\Builder
174 | */
175 | public function scopeWhereSku(Builder $query, string $sku): Builder
176 | {
177 | return $query->whereHas('skus', function ($q) use ($sku) {
178 | $q->where('code', $sku);
179 | });
180 | }
181 |
182 | /**
183 | * Create an sku for the product that has no
184 | * possible variation
185 | *
186 | * @param string $code
187 | * @throw \Ronmrcdo\Inventory\Exceptions\InvalidVariantException
188 | * @return void
189 | */
190 | public function addSku(string $code, $price = 0.00, $cost = 0.00): void
191 | {
192 | if ($this->hasAttributes()) {
193 | throw new InvalidVariantException("Cannot add single SKU due to there's a possible variation", 400);
194 | }
195 |
196 | $this->skus()->create([
197 | 'code' => $code,
198 | 'price' => $price,
199 | 'cost' => $cost
200 | ]);
201 | }
202 |
203 | /**
204 | * Product sku relation
205 | *
206 | * @return \Illuminate\Database\Eloquent\Relations\HasMany;
207 | */
208 | public function skus(): HasMany
209 | {
210 | return $this->hasMany('Ronmrcdo\Inventory\Models\ProductSku');
211 | }
212 |
213 | /**
214 | * Product Variations
215 | *
216 | * @return \Illuminate\Database\Eloquent\Relations\HasMany;
217 | */
218 | public function variations(): HasMany
219 | {
220 | return $this->hasMany('Ronmrcdo\Inventory\Models\ProductVariant');
221 | }
222 | }
--------------------------------------------------------------------------------
/src/Traits/Sluggable.php:
--------------------------------------------------------------------------------
1 | createUniqueSlug();
19 | });
20 |
21 | static::updating(function (Model $model) {
22 | $model->createUniqueSlug();
23 | });
24 | }
25 |
26 | protected function createUniqueSlug(): void
27 | {
28 | $this->slug = $this->generateUniqueSlug();
29 | }
30 |
31 | /**
32 | * Generate the unique string slug from the $sluggable property
33 | * in the model
34 | *
35 | * @return string
36 | */
37 | protected function generateUniqueSlug(): string
38 | {
39 | $slug = Str::slug($this->attributes[$this->sluggable]);
40 | $i = 1;
41 |
42 | while($this->isSlugExists($slug) || $slug === '') {
43 | $slug = $slug.'-'.$i++;
44 | }
45 |
46 | return $slug;
47 | }
48 |
49 | /**
50 | * Assert if the slug exists
51 | *
52 | * @return bool
53 | */
54 | protected function isSlugExists(string $slug): bool
55 | {
56 | $key = $this->getKey();
57 |
58 | if ($this->incrementing) {
59 | $key ?? '0';
60 | }
61 |
62 | $query = static::where('slug', $slug)
63 | ->where($this->getKeyName(), '!=', $key)
64 | ->withoutGlobalScopes();
65 |
66 | return $query->exists();
67 | }
68 | }
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | withFactories(__DIR__.'/../database/factories');
23 |
24 | $this->artisan('migrate',
25 | ['--database' => 'testing'])->run();
26 | }
27 |
28 | /**
29 | * add the package provider
30 | *
31 | * @param $app
32 | * @return array
33 | */
34 | protected function getPackageProviders($app)
35 | {
36 | return [ProductServiceProvider::class];
37 | }
38 |
39 | /**
40 | * Define environment setup.
41 | *
42 | * @param \Illuminate\Foundation\Application $app
43 | * @return void
44 | */
45 | protected function getEnvironmentSetUp($app)
46 | {
47 | // Setup default database to use sqlite :memory:
48 | $app['config']->set('database.default', 'testing');
49 | $app['config']->set('database.connections.testing', [
50 | 'driver' => 'sqlite',
51 | 'database' => ':memory:',
52 | 'prefix' => '',
53 | ]);
54 | }
55 |
56 | /**
57 | * Create a test product with variation
58 | */
59 | protected function createTestProduct()
60 | {
61 | $product = factory(Product::class)->create();
62 |
63 | $sizeAttr = factory(Attribute::class)->make([
64 | 'name' => 'size'
65 | ]);
66 | $sizeTerms = ['small', 'medium', 'large'];
67 | $colorAttr = factory(Attribute::class)->make([
68 | 'name' => 'color'
69 | ]);
70 | $colorTerm = ['black', 'white'];
71 |
72 | // Set the terms and attributes
73 | $product->addAttribute($sizeAttr->name);
74 | $product->addAttribute($colorAttr->name);
75 | $product->addAttributeTerm($sizeAttr->name, $sizeTerms);
76 | $product->addAttributeTerm($colorAttr->name, $colorTerm);
77 |
78 | $variantSmallBlack = [
79 | 'sku' => Str::random(16),
80 | 'price' => rand(100,300),
81 | 'cost' => rand(50, 99),
82 | 'variation' => [
83 | ['option' => 'color', 'value' => 'black'],
84 | ['option' => 'size', 'value' => 'small'],
85 | ]
86 | ];
87 | $variantSmallWhite = [
88 | 'sku' => Str::random(16),
89 | 'price' => rand(100,300),
90 | 'cost' => rand(50, 99),
91 | 'variation' => [
92 | ['option' => 'color', 'value' => 'white'],
93 | ['option' => 'size', 'value' => 'small'],
94 | ]
95 | ];
96 | $product->addVariant($variantSmallBlack);
97 | $product->addVariant($variantSmallWhite);
98 |
99 | return $product;
100 | }
101 | }
--------------------------------------------------------------------------------
/tests/Unit/AdapterTest.php:
--------------------------------------------------------------------------------
1 | create();
17 |
18 | $productResource = new ProductAdapter($product);
19 |
20 | $this->assertArrayHasKey('slug', $productResource->transform(), 'It should return the resource');
21 | $this->assertArrayHasKey('category', $productResource->transform(), 'It should have keys');
22 | }
23 |
24 | /** @test */
25 | public function itShouldReturnArrayResourceWithAttributes()
26 | {
27 | $product = factory(Product::class)->create();
28 | $size = rand(2,4);
29 | $attributes = factory(Attribute::class, $size)->make();
30 |
31 | $product->addAttributes($attributes->toArray());
32 |
33 | $product->attributes->each(function ($attribute) {
34 | $terms = factory(AttributeValue::class, 3)->make();
35 | $terms->each(function ($term) use ($attribute) {
36 | $attribute->addValue($term->value);
37 | });
38 | });
39 |
40 | $productResource = new ProductAdapter($product);
41 |
42 | $this->assertArrayHasKey('attributes', $productResource->transform(), 'It should have an attributes');
43 | $this->assertArrayHasKey('category', $productResource->transform(), 'It should have keys');
44 | }
45 |
46 | /** @test */
47 | public function itShouldReturnCollectionResource()
48 | {
49 | $size = rand(5, 10);
50 | $products = factory(Product::class, $size)->create();
51 | $productsCollection = ProductAdapter::collection($products);
52 | $selected = sizeof($productsCollection) < 1 ? 0 : rand(0, sizeof($productsCollection) - 1);
53 |
54 | $this->assertArrayHasKey('slug', $productsCollection[$selected], 'It should have slug');
55 | $this->assertEquals($size, sizeof($productsCollection), 'Collection should match the random size');
56 | }
57 | }
--------------------------------------------------------------------------------
/tests/Unit/InventoryTest.php:
--------------------------------------------------------------------------------
1 | create();
17 |
18 | $this->assertDatabaseHas('warehouses', [
19 | 'id' => $warehouse->id
20 | ]);
21 | }
22 |
23 | /** @test */
24 | public function itShouldInsertProductInWarehouse()
25 | {
26 | $warehouse = factory(Warehouse::class)->create();
27 |
28 | $product = $this->createTestProduct();
29 |
30 | $product->skus->each(function ($sku) use ($warehouse) {
31 | $warehouse->items()->create([
32 | 'product_sku_id' => $sku->id,
33 | 'quantity' => rand(5, 20),
34 | 'aisle' => 'ai-'. rand(20, 30),
35 | 'row' => 'rw-'. rand(1, 9)
36 | ]);
37 | });
38 |
39 | $warehouse->load('items');
40 |
41 | $this->assertArrayHasKey('items', $warehouse, 'Warehouse should have a stocks');
42 | }
43 |
44 | /** @test */
45 | public function itShouldListTheWarehouseStocks()
46 | {
47 | $warehouse = factory(Warehouse::class)->create();
48 |
49 | $product = $this->createTestProduct();
50 |
51 | $product->skus->each(function ($sku) use ($warehouse) {
52 | $warehouse->items()->create([
53 | 'product_sku_id' => $sku->id,
54 | 'quantity' => rand(5, 20),
55 | 'aisle' => 'ai-'. rand(20, 30),
56 | 'row' => 'rw-'. rand(1, 9)
57 | ]);
58 | });
59 |
60 | $warehouse->load('items');
61 | $items = collect($warehouse->items)
62 | ->map(function ($item) {
63 | return (new ProductVariantAdapter($item->product))->transform();
64 | })
65 | ->toArray();
66 |
67 | // Each product should have a parent_product
68 | collect($items)->each(function ($item) {
69 | $this->assertArrayHasKey('parent_product_id', $item, 'It should have a parent_product_id');
70 | });
71 | }
72 | }
--------------------------------------------------------------------------------
/tests/Unit/ModelSluggableTest.php:
--------------------------------------------------------------------------------
1 | create();
14 |
15 | $this->assertTrue(! is_null($category->slug), 'It should be true');
16 | }
17 |
18 | /** @test */
19 | public function itShouldIncrementSlug()
20 | {
21 | $category1 = factory(Category::class)->create();
22 | $category2 = factory(Category::class)->create([
23 | 'name' => $category1->name
24 | ]);
25 |
26 | $this->assertEquals($category2->slug, $category1->slug.'-1', 'The slug should have an incremental number');
27 | }
28 |
29 | /** @test */
30 | public function itShouldNotUpdateSlug()
31 | {
32 | $category = factory(Category::class)->create([
33 | 'name' => 'this is a test'
34 | ]);
35 |
36 | $category->name = $category->name;
37 | $category->save();
38 |
39 | $this->assertEquals('this-is-a-test', $category->slug, 'It should the same even though the record is being updated');
40 | }
41 |
42 | /** @test */
43 | public function itShouldUpdateTheSlug()
44 | {
45 | $category = factory(Category::class)->create();
46 |
47 | $category->name = 'This is a test';
48 | $category->save();
49 |
50 | $this->assertEquals('this-is-a-test', $category->slug, 'It should update the slug on name change');
51 | }
52 | }
--------------------------------------------------------------------------------
/tests/Unit/ProductAttributeTest.php:
--------------------------------------------------------------------------------
1 | create();
17 |
18 | $attribute = factory(Attribute::class)->make();
19 |
20 | $product->addAttribute($attribute['name']);
21 |
22 | $this->assertTrue($product->hasAttributes());
23 | $this->assertTrue($product->hasAttribute($attribute->name));
24 | }
25 |
26 | /** @test */
27 | public function itShouldAddAttributeAndValueToProduct()
28 | {
29 | $product = factory(Product::class)->create();
30 | $attribute = factory(Attribute::class)->create([
31 | 'product_id' => $product->id
32 | ]);
33 |
34 | $option = factory(AttributeValue::class)->make();
35 |
36 | $product->addAttributeTerm($attribute->name, $option->value);
37 |
38 | $this->assertTrue($attribute->values()->count() > 0);
39 | }
40 |
41 | /** @test */
42 | public function itShouldGetProductAttributeAndValues()
43 | {
44 | $product = factory(Product::class)->create();
45 | $attribute = factory(Attribute::class)->create([
46 | 'product_id' => $product->id
47 | ]);
48 | $size = rand(2,5);
49 |
50 | $options = factory(AttributeValue::class, $size)->make();
51 |
52 | $options->each(function ($option) use ($product, $attribute) {
53 | $product->addAttributeTerm($attribute->name, $option->value);
54 | });
55 |
56 | $this->assertTrue(sizeof($product->loadAttributes()->first()->toArray()['values']) >= $size, 'It should attach all the options');
57 | }
58 |
59 | /** @test */
60 | public function itShouldCreateMultipleAttributes()
61 | {
62 | $product = factory(Product::class)->create();
63 | $size = rand(2,4);
64 | $attributes = factory(Attribute::class, $size)->make();
65 |
66 | $attributes->each(function($attribute) use ($product) {
67 | $product->addAttribute($attribute['name']);
68 | });
69 |
70 | $this->assertEquals($size, sizeof($product->loadAttributes()->toArray()), 'Attributes should be equal to product attribute');
71 | }
72 |
73 | /** @test */
74 | public function itShouldCreateMultipleAttributesUsingArray()
75 | {
76 | $product = factory(Product::class)->create();
77 | $size = rand(2,4);
78 | $attributes = factory(Attribute::class, $size)->make();
79 |
80 | $product->addAttributes($attributes->toArray());
81 |
82 | $this->assertEquals($size, sizeof($product->loadAttributes()->toArray()), 'Attributes should be equal to product attribute');
83 | }
84 |
85 | /** @test */
86 | public function itShouldThrowInvalidAttributeException()
87 | {
88 | $this->expectException(InvalidAttributeException::class);
89 |
90 | $product = factory(Product::class)->create();
91 |
92 | $product->addAttributeTerm('test', 'test');
93 | }
94 |
95 | /** @test */
96 | public function itShouldRemoveAttributeFromProduct()
97 | {
98 | $product = factory(Product::class)->create();
99 | $size = rand(2,4);
100 | $attributes = factory(Attribute::class, $size)->make();
101 |
102 | $product->addAttributes($attributes->toArray());
103 |
104 | $selected = sizeof($attributes) < 1 ? 0 : rand(0, sizeof($attributes) - 1);
105 |
106 | $product->removeAttribute($attributes[$selected]['name']);
107 |
108 | $this->assertEquals($size - 1, sizeof($product->loadAttributes()->toArray()), 'Attributes should be equal to product attribute');
109 | }
110 |
111 | /** @test */
112 | public function itShouldRemoveAttributeTermFromProduct()
113 | {
114 | $product = factory(Product::class)->create();
115 | $attribute = factory(Attribute::class)->create([
116 | 'product_id' => $product->id
117 | ]);
118 | $size = rand(2,5);
119 |
120 | $options = factory(AttributeValue::class, $size)->make();
121 |
122 | // Add the terms on the product
123 | $options->each(function ($option) use ($product, $attribute) {
124 | $product->addAttributeTerm($attribute->name, $option->value);
125 | });
126 |
127 | $selected = sizeof($options) < 1 ? 0 : rand(0, sizeof($options) - 1);
128 |
129 | $product->removeAttributeTerm($attribute->name, $options[$selected]['value']);
130 |
131 |
132 | $this->assertEquals(sizeof($product->loadAttributes()->first()->toArray()['values']), $size - 1, 'It should attach all the options');
133 | }
134 | }
--------------------------------------------------------------------------------
/tests/Unit/ProductCategoryTest.php:
--------------------------------------------------------------------------------
1 | create();
15 | $product = factory(Product::class)->create([
16 | 'category_id' => $category->id
17 | ]);
18 |
19 | $this->assertEquals($category->name, $product->category->name, 'Category should be attached to product');
20 | }
21 |
22 | /** @test */
23 | public function itShouldHaveChildCategory()
24 | {
25 | $parentCategory = factory(Category::class)->create();
26 | $childCategory = factory(Category::class)->create([
27 | 'parent_id' => $parentCategory->id
28 | ]);
29 |
30 | $this->assertEquals($parentCategory->name, $childCategory->parent->name, 'Parent should equal');
31 | $this->assertTrue(! $childCategory->isParent(), 'Is Parent should return false');
32 | }
33 |
34 | /** @test */
35 | public function itShouldListProductsByCategory()
36 | {
37 | $category = factory(Category::class)->create();
38 | $products = factory(Product::class, rand(10,20))->create([
39 | 'category_id' => $category->id
40 | ]);
41 |
42 | $this->assertEquals(sizeof($products->toArray()), sizeof($category->products->toArray()), 'It should have the same length');
43 | }
44 | }
--------------------------------------------------------------------------------
/tests/Unit/ProductVariationTest.php:
--------------------------------------------------------------------------------
1 | create();
20 |
21 | $sizeAttr = factory(Attribute::class)->make([
22 | 'name' => 'size'
23 | ]);
24 | $sizeTerms = ['small', 'medium', 'large'];
25 | $colorAttr = factory(Attribute::class)->make([
26 | 'name' => 'color'
27 | ]);
28 | $colorTerm = ['black', 'white'];
29 |
30 | // Set the terms and attributes
31 | $product->addAttribute($sizeAttr->name);
32 | $product->addAttribute($colorAttr->name);
33 | $product->addAttributeTerm($sizeAttr->name, $sizeTerms);
34 | $product->addAttributeTerm($colorAttr->name, $colorTerm);
35 |
36 | $variantSmallBlack = [
37 | 'sku' => 'WOOPROTSHIRT-SMBLK',
38 | 'price' => rand(100,300),
39 | 'cost' => rand(50, 99),
40 | 'variation' => [
41 | ['option' => 'color', 'value' => 'black'],
42 | ['option' => 'size', 'value' => 'small'],
43 | ]
44 | ];
45 | $variantSmallWhite = [
46 | 'sku' => 'WOOPROTSHIRT-SMWHT',
47 | 'price' => rand(100,300),
48 | 'cost' => rand(50, 99),
49 | 'variation' => [
50 | ['option' => 'color', 'value' => 'white'],
51 | ['option' => 'size', 'value' => 'small'],
52 | ]
53 | ];
54 | $product->addVariant($variantSmallBlack);
55 | $product->addVariant($variantSmallWhite);
56 |
57 | $productResource = new ProductAdapter($product);
58 |
59 | $this->assertArrayHasKey('variations', $productResource->transform(), 'It should have a variation');
60 | }
61 |
62 | /** @test */
63 | public function itShouldCreateSingleProductWithSku()
64 | {
65 | $product = factory(Product::class)->create();
66 |
67 | // Add Sku for single product that has no variation
68 | $product->addSku(Str::random());
69 |
70 | $productResource = new ProductAdapter($product);
71 |
72 | $this->assertArrayHasKey('sku', $productResource->transform(), 'It should have an sku');
73 | }
74 |
75 | /** @test */
76 | public function itShouldThrowErrorDueToDuplicateAttribute()
77 | {
78 | $this->expectException(InvalidVariantException::class);
79 |
80 | // Parent Product
81 | $product = factory(Product::class)->create();
82 |
83 | $sizeAttr = factory(Attribute::class)->make([
84 | 'name' => 'size'
85 | ]);
86 | $sizeTerms = ['small', 'medium', 'large'];
87 | $colorAttr = factory(Attribute::class)->make([
88 | 'name' => 'color'
89 | ]);
90 | $colorTerm = ['black', 'white'];
91 |
92 | // Set the terms and attributes
93 | $product->addAttribute($sizeAttr->name);
94 | $product->addAttribute($colorAttr->name);
95 | $product->addAttributeTerm($sizeAttr->name, $sizeTerms);
96 | $product->addAttributeTerm($colorAttr->name, $colorTerm);
97 |
98 | $variantSmallBlack = [
99 | 'sku' => 'WOOPROTSHIRT-SMBLK',
100 | 'price' => rand(100,300),
101 | 'cost' => rand(50, 99),
102 | 'variation' => [
103 | ['option' => 'color', 'value' => 'black'],
104 | ['option' => 'color', 'value' => 'white'],
105 | ['option' => 'size', 'value' => 'small'],
106 | ]
107 | ];
108 | $variantSmallWhite = [
109 | 'sku' => 'WOOPROTSHIRT-SMWHT',
110 | 'price' => rand(100,300),
111 | 'cost' => rand(50, 99),
112 | 'variation' => [
113 | ['option' => 'color', 'value' => 'white'],
114 | ['option' => 'size', 'value' => 'small'],
115 | ]
116 | ];
117 | $product->addVariant($variantSmallBlack);
118 | $product->addVariant($variantSmallWhite);
119 |
120 | }
121 |
122 | /** @test */
123 | public function itShouldThrowVariantExceptionForDuplicateVariantDifferentSku()
124 | {
125 | $this->expectException(InvalidVariantException::class);
126 |
127 | // Parent Product
128 | $product = factory(Product::class)->create();
129 |
130 | $sizeAttr = factory(Attribute::class)->make([
131 | 'name' => 'size'
132 | ]);
133 | $sizeTerms = ['small', 'medium', 'large'];
134 | $colorAttr = factory(Attribute::class)->make([
135 | 'name' => 'color'
136 | ]);
137 | $colorTerm = ['black', 'white'];
138 |
139 | // Set the terms and attributes
140 | $product->addAttribute($sizeAttr->name);
141 | $product->addAttribute($colorAttr->name);
142 | $product->addAttributeTerm($sizeAttr->name, $sizeTerms);
143 | $product->addAttributeTerm($colorAttr->name, $colorTerm);
144 |
145 | $variantSmallBlack = [
146 | 'sku' => 'WOOPROTSHIRT-SMBLK',
147 | 'price' => rand(100,300),
148 | 'cost' => rand(50, 99),
149 | 'variation' => [
150 | ['option' => 'color', 'value' => 'black'],
151 | ['option' => 'size', 'value' => 'small'],
152 | ]
153 | ];
154 | $variantSmallWhite = [
155 | 'sku' => 'WOOPROTSHIRT-SMWHT',
156 | 'price' => rand(100,300),
157 | 'cost' => rand(50, 99),
158 | 'variation' => [
159 | ['option' => 'size', 'value' => 'small'],
160 | ['option' => 'color', 'value' => 'white']
161 | ]
162 | ];
163 |
164 | $product->addVariant($variantSmallBlack);
165 | $product->addVariant($variantSmallWhite);
166 |
167 | $newVariant = [
168 | 'sku' => 'WOOPROTSHIRT-SMNEW',
169 | 'price' => rand(100,300),
170 | 'cost' => rand(50, 99),
171 | 'variation' => [
172 | ['option' => 'size', 'value' => 'small'],
173 | ['option' => 'color', 'value' => 'black']
174 | ]
175 | ];
176 |
177 | // It should now throw due to same variation attributes of
178 | // WOOPROTSHIRT-SMNEW and WOOPROTSHIRT-SMBLK
179 | $product->addVariant($newVariant);
180 |
181 | }
182 |
183 | /** @test */
184 | public function itShouldFindProductBySku()
185 | {
186 | // Parent Product
187 | $product = factory(Product::class)->create();
188 |
189 | $sizeAttr = factory(Attribute::class)->make([
190 | 'name' => 'size'
191 | ]);
192 | $sizeTerms = ['small', 'sedium', 'large'];
193 | $colorAttr = factory(Attribute::class)->make([
194 | 'name' => 'color'
195 | ]);
196 | $colorTerms = ['black', 'white'];
197 |
198 | // Set the terms and attributes
199 | $product->addAttribute($sizeAttr->name);
200 | $product->addAttribute($colorAttr->name);
201 | $product->addAttributeTerm($sizeAttr->name, $sizeTerms);
202 | $product->addAttributeTerm($colorAttr->name, $colorTerms);
203 |
204 | $variantSmallBlack = [
205 | 'sku' => 'WOOPROTSHIRT-SMBLK',
206 | 'price' => rand(100,300),
207 | 'cost' => rand(50, 99),
208 | 'variation' => [
209 | ['option' => 'color', 'value' => 'black'],
210 | ['option' => 'size', 'value' => 'small'],
211 | ]
212 | ];
213 | $variantSmallWhite = [
214 | 'sku' => 'WOOPROTSHIRT-SMWHT',
215 | 'price' => rand(100,300),
216 | 'cost' => rand(50, 99),
217 | 'variation' => [
218 | ['option' => 'color', 'value' => 'white'],
219 | ['option' => 'size', 'value' => 'small'],
220 | ]
221 | ];
222 | $product->addVariant($variantSmallBlack);
223 | $product->addVariant($variantSmallWhite);
224 |
225 | $variantResource = new ProductVariantAdapter($product->findBySku('WOOPROTSHIRT-SMWHT'));
226 |
227 | $this->assertArrayHasKey('sku', $variantResource->transform(), 'It should have an sku');
228 | }
229 |
230 | /** @test */
231 | public function itShouldListTheVariations()
232 | {
233 | // Parent Product
234 | $product = factory(Product::class)->create();
235 |
236 | $sizeAttr = factory(Attribute::class)->make([
237 | 'name' => 'size'
238 | ]);
239 | $sizeTerms = ['small', 'medium', 'large'];
240 | $colorAttr = factory(Attribute::class)->make([
241 | 'name' => 'color'
242 | ]);
243 | $colorTerms = ['black', 'white'];
244 |
245 | // Set the terms and attributes
246 | $product->addAttribute($sizeAttr->name);
247 | $product->addAttribute($colorAttr->name);
248 | $product->addAttributeTerm($sizeAttr->name, $sizeTerms);
249 | $product->addAttributeTerm($colorAttr->name, $colorTerms);
250 |
251 | $variantSmallBlack = [
252 | 'sku' => 'WOOPROTSHIRT-SMBLK',
253 | 'price' => rand(100,300),
254 | 'cost' => rand(50, 99),
255 | 'variation' => [
256 | ['option' => 'color', 'value' => 'black'],
257 | ['option' => 'size', 'value' => 'small'],
258 | ]
259 | ];
260 | $variantSmallWhite = [
261 | 'sku' => 'WOOPROTSHIRT-SMWHT',
262 | 'price' => rand(100,300),
263 | 'cost' => rand(50, 99),
264 | 'variation' => [
265 | ['option' => 'color', 'value' => 'white'],
266 | ['option' => 'size', 'value' => 'small'],
267 | ]
268 | ];
269 | $product->addVariant($variantSmallBlack);
270 | $product->addVariant($variantSmallWhite);
271 |
272 | $variantResource = new ProductVariantAdapter($product->findBySku('WOOPROTSHIRT-SMWHT'));
273 |
274 | $this->assertArrayHasKey('sku', $variantResource->transform(), 'It should have an sku');
275 | $this->assertArrayHasKey('parent_product_id', $variantResource->transform(), 'It should have a parent_product_id');
276 | }
277 |
278 | /** @test */
279 | public function itShouldListCollectionOfVariations()
280 | {
281 | // Parent Product
282 | $product = factory(Product::class)->create();
283 |
284 | $sizeAttr = factory(Attribute::class)->make([
285 | 'name' => 'size'
286 | ]);
287 | $sizeTerms = ['small', 'medium', 'large'];
288 | $colorAttr = factory(Attribute::class)->make([
289 | 'name' => 'color'
290 | ]);
291 | $colorTerms = ['black', 'white'];
292 |
293 | // Set the terms and attributes
294 | $product->addAttribute($sizeAttr->name);
295 | $product->addAttribute($colorAttr->name);
296 | $product->addAttributeTerm($sizeAttr->name, $sizeTerms);
297 | $product->addAttributeTerm($colorAttr->name, $colorTerms);
298 |
299 | $variantSmallBlack = [
300 | 'sku' => 'WOOPROTSHIRT-SMBLK',
301 | 'price' => rand(100,300),
302 | 'cost' => rand(50, 99),
303 | 'variation' => [
304 | ['option' => 'color', 'value' => 'black'],
305 | ['option' => 'size', 'value' => 'small'],
306 | ]
307 | ];
308 | $variantSmallWhite = [
309 | 'sku' => 'WOOPROTSHIRT-SMWHT',
310 | 'price' => rand(100,300),
311 | 'cost' => rand(50, 99),
312 | 'variation' => [
313 | ['option' => 'color', 'value' => 'white'],
314 | ['option' => 'size', 'value' => 'small'],
315 | ]
316 | ];
317 | $product->addVariant($variantSmallBlack);
318 | $product->addVariant($variantSmallWhite);
319 |
320 | $variantResource = ProductVariantAdapter::collection($product->getVariations());
321 |
322 | $this->assertArrayHasKey('parent_product_id', head($variantResource), 'It should have a parent_product_id');
323 | }
324 | }
--------------------------------------------------------------------------------