diff --git a/src/Factories/GroupFactory.php b/database/factories/GroupFactory.php similarity index 100% rename from src/Factories/GroupFactory.php rename to database/factories/GroupFactory.php diff --git a/src/Factories/ProductStatusFactory.php b/database/factories/ProductStatusFactory.php similarity index 100% rename from src/Factories/ProductStatusFactory.php rename to database/factories/ProductStatusFactory.php diff --git a/src/Factories/ProductTypeDataFactory.php b/database/factories/ProductTypeDataFactory.php similarity index 100% rename from src/Factories/ProductTypeDataFactory.php rename to database/factories/ProductTypeDataFactory.php diff --git a/src/Factories/ProductTypeFactory.php b/database/factories/ProductTypeFactory.php similarity index 100% rename from src/Factories/ProductTypeFactory.php rename to database/factories/ProductTypeFactory.php diff --git a/src/Factories/PropertyFactory.php b/database/factories/PropertyFactory.php similarity index 100% rename from src/Factories/PropertyFactory.php rename to database/factories/PropertyFactory.php diff --git a/src/Factories/PropertyValueFactory.php b/database/factories/PropertyValueFactory.php similarity index 100% rename from src/Factories/PropertyValueFactory.php rename to database/factories/PropertyValueFactory.php diff --git a/database/seeders/CatalogueSeeder.php b/database/seeders/CatalogueSeeder.php index 4bcccdb..c57dba1 100644 --- a/database/seeders/CatalogueSeeder.php +++ b/database/seeders/CatalogueSeeder.php @@ -11,6 +11,9 @@ class CatalogueSeeder extends Seeder { public function run(): void { + $this->call(MeasureUnitSeeder::class); + $this->call(TaxClassSeeder::class); + $this->call(PriceListSeeder::class); $this->call(CategorySeeder::class); $this->call(ProductTypeSeeder::class); $this->call(GroupSeeder::class); diff --git a/database/seeders/MeasureUnitSeeder.php b/database/seeders/MeasureUnitSeeder.php new file mode 100644 index 0000000..45e0747 --- /dev/null +++ b/database/seeders/MeasureUnitSeeder.php @@ -0,0 +1,33 @@ + 'pcs / kos'], + ['is_default' => true] + ); + + // Ensure only one default remains true + MeasureUnit::where('id', '!=', $pcs->id)->update(['is_default' => false]); + + // Create set / set + MeasureUnit::updateOrCreate( + ['name' => 'set / set'], + ['is_default' => false] + ); + + // Create pair / par + MeasureUnit::updateOrCreate( + ['name' => 'pair / par'], + ['is_default' => false] + ); + } +} diff --git a/database/seeders/PriceListSeeder.php b/database/seeders/PriceListSeeder.php index 0a2d9cf..877e5fe 100644 --- a/database/seeders/PriceListSeeder.php +++ b/database/seeders/PriceListSeeder.php @@ -14,42 +14,85 @@ class PriceListSeeder extends Seeder */ public function run(): void { - // Ensure we have at least one currency - if (Currency::count() === 0) { + // Ensure currencies exist + if (! Currency::where('id', 'EUR')->exists()) { Currency::create(['id' => 'EUR', 'name' => 'Euro', 'is_active' => true]); } + if (! Currency::where('id', 'USD')->exists()) { + Currency::create(['id' => 'USD', 'name' => 'US Dollar', 'is_active' => true]); + } - // Create price lists using factory - $priceLists = PriceList::factory() - ->count(3) - ->create(); + // Desired price lists + $definitions = [ + [ + 'name' => 'Wholesale / Veleprodajni cenik', + 'code' => 'VPC', + 'currency_id' => 'EUR', + 'tax_included' => false, + 'is_default' => false, + 'is_default_purchase' => false, + ], + [ + 'name' => 'Retail / Maloprodajni cenik', + 'code' => 'MPC', + 'currency_id' => 'EUR', + 'tax_included' => true, + 'is_default' => true, // default selling + 'is_default_purchase' => false, + ], + [ + 'name' => 'Purchase / Nabavni cenik', + 'code' => 'NC', + 'currency_id' => 'USD', + 'tax_included' => false, + 'is_default' => false, + 'is_default_purchase' => true, // default purchase + ], + ]; - // Create price list data for each price list - foreach ($priceLists as $index => $priceList) { - // Create data for all tenants if tenancy is enabled - $tenantFK = config('eclipse-catalogue.tenancy.foreign_key'); - $tenantModel = config('eclipse-catalogue.tenancy.model'); + $tenantFK = config('eclipse-catalogue.tenancy.foreign_key'); + $tenantModel = config('eclipse-catalogue.tenancy.model'); + $tenants = collect(); + if ($tenantFK && $tenantModel && class_exists($tenantModel)) { + $tenants = $tenantModel::all(); + } - if ($tenantFK && $tenantModel && class_exists($tenantModel)) { - $tenants = $tenantModel::all(); + foreach ($definitions as $def) { + $priceList = PriceList::updateOrCreate( + ['code' => $def['code']], + [ + 'name' => $def['name'], + 'currency_id' => $def['currency_id'], + 'tax_included' => $def['tax_included'], + ] + ); + // Ensure per-tenant data + if ($tenants->isNotEmpty()) { foreach ($tenants as $tenant) { - PriceListData::factory()->create([ - 'price_list_id' => $priceList->id, - $tenantFK => $tenant->id, - 'is_active' => true, - 'is_default' => $index === 0, // First price list is default selling - 'is_default_purchase' => false, - ]); + PriceListData::updateOrCreate( + [ + 'price_list_id' => $priceList->id, + $tenantFK => $tenant->id, + ], + [ + 'is_active' => true, + 'is_default' => $def['is_default'], + 'is_default_purchase' => $def['is_default_purchase'], + ] + ); } } else { - // No tenancy - create single record - PriceListData::factory()->create([ - 'price_list_id' => $priceList->id, - 'is_active' => true, - 'is_default' => $index === 0, // First price list is default selling - 'is_default_purchase' => false, - ]); + PriceListData::updateOrCreate( + [ + 'price_list_id' => $priceList->id, + ], + [ + 'is_active' => true, + 'is_default' => $def['is_default'], + 'is_default_purchase' => $def['is_default_purchase'], + ] + ); } } } diff --git a/database/seeders/ProductSeeder.php b/database/seeders/ProductSeeder.php index d9ab4a1..33ef533 100644 --- a/database/seeders/ProductSeeder.php +++ b/database/seeders/ProductSeeder.php @@ -4,10 +4,14 @@ use Eclipse\Catalogue\Models\Category; use Eclipse\Catalogue\Models\Group; +use Eclipse\Catalogue\Models\PriceList; use Eclipse\Catalogue\Models\Product; +use Eclipse\Catalogue\Models\Product\Price as ProductPrice; use Eclipse\Catalogue\Models\ProductData; use Eclipse\Catalogue\Models\ProductStatus; use Eclipse\Catalogue\Models\ProductType; +use Eclipse\Catalogue\Models\Property; +use Eclipse\Catalogue\Models\PropertyValue; use Exception; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Http; @@ -38,6 +42,9 @@ public function run(): void $products = Product::query()->latest('id')->take(100)->get(); foreach ($products as $index => $product) { + // Assign product prices for all price lists + $this->assignRandomPrices($product); + if ($tenantFK && $tenantModel && class_exists($tenantModel)) { $tenants = $tenantModel::all(); foreach ($tenants as $tenant) { @@ -58,6 +65,9 @@ public function run(): void // Assign random product status for this tenant $this->assignRandomProductStatus($productData, $tenant->id); + // Attach random unit of measure (list property) and custom property values + $this->attachRandomUnitAndCustomProps($product); + // Get groups for this specific tenant $tenantGroups = Group::where($tenantFK, $tenant->id)->get(); $groupsToAdd = $this->determineGroupsForProduct($index, $tenantGroups); @@ -79,6 +89,9 @@ public function run(): void // Assign random product status for non-tenant scenario $this->assignRandomProductStatus($productData, null); + // Attach random unit of measure (list property) and custom property values + $this->attachRandomUnitAndCustomProps($product); + // For non-tenant scenarios, use all groups $groups = Group::all(); $groupsToAdd = $this->determineGroupsForProduct($index, $groups); @@ -198,4 +211,84 @@ private function assignRandomProductStatus(ProductData $productData, ?int $tenan } } } + + /** + * Create random current prices for all price lists for a product. + */ + private function assignRandomPrices(Product $product): void + { + $priceLists = PriceList::all(); + if ($priceLists->isEmpty()) { + return; + } + + foreach ($priceLists as $pl) { + // Generate a base retail price between 10 and 500 (EUR/USD agnostic) + $baseRetail = rand(1000, 50000) / 100; // 10.00 - 500.00 + + if ($pl->code === 'MPC') { + $priceValue = $baseRetail; + } elseif ($pl->code === 'VPC') { + // Wholesale discount 10% - 30% + $discountFactor = rand(70, 90) / 100; // 0.70 - 0.90 + $priceValue = round($baseRetail * $discountFactor, 2); + } elseif ($pl->code === 'NC') { + // Purchase price: slightly lower than wholesale or independent band + $priceValue = round(max(1, $baseRetail * rand(60, 80) / 100), 2); + } else { + $priceValue = $baseRetail; + } + + ProductPrice::updateOrCreate( + [ + 'product_id' => $product->id, + 'price_list_id' => $pl->id, + 'valid_from' => now()->toDateString(), + ], + [ + 'price' => $priceValue, + 'tax_included' => (bool) $pl->tax_included, + ] + ); + } + } + + /** + * Attach unit of measure (list property) and custom property values. + */ + private function attachRandomUnitAndCustomProps(Product $product): void + { + // Unit of measure via list property + $uomProperty = Property::where('code', 'unit_of_measure')->first(); + if ($uomProperty) { + $values = PropertyValue::where('property_id', $uomProperty->id)->get(); + if ($values->isNotEmpty()) { + $product->propertyValues()->syncWithoutDetaching([$values->random()->id]); + } + } + + // Custom properties + $materialDetails = Property::where('code', 'material_details')->first(); + if ($materialDetails) { + $product->setCustomPropertyValue($materialDetails, [ + 'en' => 'Made from premium materials', + 'sl' => 'Izdelano iz kakovostnih materialov', + ]); + } + + $skuNotes = Property::where('code', 'sku_notes')->first(); + if ($skuNotes) { + $product->setCustomPropertyValue($skuNotes, 'Handle with care'); + } + + $releaseDate = Property::where('code', 'release_date')->first(); + if ($releaseDate) { + $product->setCustomPropertyValue($releaseDate, now()->subDays(rand(0, 365))->toDateString()); + } + + $dimensions = Property::where('code', 'dimensions')->first(); + if ($dimensions) { + $product->setCustomPropertyValue($dimensions, rand(10, 200) / 10); // 1.0 - 20.0 + } + } } diff --git a/database/seeders/PropertySeeder.php b/database/seeders/PropertySeeder.php index f3ac957..b55af98 100644 --- a/database/seeders/PropertySeeder.php +++ b/database/seeders/PropertySeeder.php @@ -2,6 +2,8 @@ namespace Eclipse\Catalogue\Seeders; +use Eclipse\Catalogue\Enums\PropertyInputType; +use Eclipse\Catalogue\Enums\PropertyType; use Eclipse\Catalogue\Models\ProductType; use Eclipse\Catalogue\Models\Property; use Eclipse\Catalogue\Models\PropertyValue; @@ -14,8 +16,8 @@ public function run(): void // Create Brand property (global) $brandProperty = Property::create([ 'code' => 'brand', - 'name' => ['en' => 'Brand'], - 'description' => ['en' => 'Product brand or manufacturer'], + 'name' => ['en' => 'Brand', 'sl' => 'Znamka'], + 'description' => ['en' => 'Product brand or manufacturer', 'sl' => 'Znamka ali proizvajalec izdelka'], 'internal_name' => 'Brand/Manufacturer', 'is_active' => true, 'is_global' => true, @@ -29,7 +31,7 @@ public function run(): void foreach ($brands as $index => $brand) { PropertyValue::create([ 'property_id' => $brandProperty->id, - 'value' => ['en' => $brand], + 'value' => ['en' => $brand, 'sl' => $brand], 'sort' => $index * 10, ]); } @@ -37,8 +39,8 @@ public function run(): void // Create Color property (global) $colorProperty = Property::create([ 'code' => 'color', - 'name' => ['en' => 'Color'], - 'description' => ['en' => 'Product color'], + 'name' => ['en' => 'Color', 'sl' => 'Barva'], + 'description' => ['en' => 'Product color', 'sl' => 'Barva izdelka'], 'is_active' => true, 'is_global' => true, 'max_values' => 3, // Allow multiple colors @@ -47,11 +49,20 @@ public function run(): void ]); // Create color values - $colors = ['Red', 'Blue', 'Green', 'Black', 'White', 'Yellow', 'Purple', 'Orange']; + $colors = [ + ['en' => 'Red', 'sl' => 'Rdeča'], + ['en' => 'Blue', 'sl' => 'Modra'], + ['en' => 'Green', 'sl' => 'Zelena'], + ['en' => 'Black', 'sl' => 'Črna'], + ['en' => 'White', 'sl' => 'Bela'], + ['en' => 'Yellow', 'sl' => 'Rumena'], + ['en' => 'Purple', 'sl' => 'Vijolična'], + ['en' => 'Orange', 'sl' => 'Oranžna'], + ]; foreach ($colors as $index => $color) { PropertyValue::create([ 'property_id' => $colorProperty->id, - 'value' => ['en' => $color], + 'value' => $color, 'sort' => $index * 10, ]); } @@ -59,8 +70,8 @@ public function run(): void // Create Size property (for clothing type only) $sizeProperty = Property::create([ 'code' => 'size', - 'name' => ['en' => 'Size'], - 'description' => ['en' => 'Clothing size'], + 'name' => ['en' => 'Size', 'sl' => 'Velikost'], + 'description' => ['en' => 'Clothing size', 'sl' => 'Velikost oblačil'], 'is_active' => true, 'is_global' => false, 'max_values' => 1, @@ -91,11 +102,20 @@ public function run(): void ]); // Create material values - $materials = ['Cotton', 'Polyester', 'Wool', 'Silk', 'Leather', 'Plastic', 'Metal', 'Wood']; + $materials = [ + ['en' => 'Cotton', 'sl' => 'Bombaž'], + ['en' => 'Polyester', 'sl' => 'Poliester'], + ['en' => 'Wool', 'sl' => 'Volna'], + ['en' => 'Silk', 'sl' => 'Svila'], + ['en' => 'Leather', 'sl' => 'Usnje'], + ['en' => 'Plastic', 'sl' => 'Plastika'], + ['en' => 'Metal', 'sl' => 'Kovina'], + ['en' => 'Wood', 'sl' => 'Les'], + ]; foreach ($materials as $index => $material) { PropertyValue::create([ 'property_id' => $materialProperty->id, - 'value' => ['en' => $material], + 'value' => $material, 'sort' => $index * 10, ]); } @@ -113,5 +133,102 @@ public function run(): void $productType->properties()->attach($materialProperty->id, ['sort' => 20]); } } + + // Add Unit of measure as list property (global) + $uomProperty = Property::create([ + 'code' => 'unit_of_measure', + 'name' => ['en' => 'Unit of measure', 'sl' => 'Merska enota'], + 'description' => ['en' => 'Unit of measure for product', 'sl' => 'Merska enota za izdelek'], + 'is_active' => true, + 'is_global' => true, + 'max_values' => 1, + 'enable_sorting' => false, + 'is_filter' => true, + 'type' => PropertyType::LIST->value, + ]); + + $uomValues = [ + ['en' => 'pcs', 'sl' => 'kos'], + ['en' => 'set', 'sl' => 'set'], + ['en' => 'pair', 'sl' => 'par'], + ]; + foreach ($uomValues as $i => $unitValue) { + PropertyValue::create([ + 'property_id' => $uomProperty->id, + 'value' => $unitValue, + 'sort' => $i * 10, + ]); + } + + // Add realistic custom properties (global, custom types) + Property::create([ + 'code' => 'material_details', + 'name' => ['en' => 'Material details', 'sl' => 'Podrobnosti o materialih'], + 'description' => ['en' => 'Details about materials used', 'sl' => 'Podrobnosti o uporabljenih materialih'], + 'is_active' => true, + 'is_global' => true, + 'max_values' => 1, + 'enable_sorting' => false, + 'is_filter' => false, + 'type' => PropertyType::CUSTOM->value, + 'input_type' => PropertyInputType::TEXT->value, + 'is_multilang' => true, + ]); + + Property::create([ + 'code' => 'sku_notes', + 'name' => ['en' => 'SKU notes', 'sl' => 'Opombe SKU'], + 'description' => ['en' => 'Internal notes for SKU', 'sl' => 'Interne opombe za SKU'], + 'is_active' => true, + 'is_global' => true, + 'max_values' => 1, + 'enable_sorting' => false, + 'is_filter' => false, + 'type' => PropertyType::CUSTOM->value, + 'input_type' => PropertyInputType::STRING->value, + 'is_multilang' => false, + ]); + + Property::create([ + 'code' => 'release_date', + 'name' => ['en' => 'Release date', 'sl' => 'Datum izida'], + 'description' => ['en' => 'Product release date', 'sl' => 'Datum izida izdelka'], + 'is_active' => true, + 'is_global' => true, + 'max_values' => 1, + 'enable_sorting' => false, + 'is_filter' => true, + 'type' => PropertyType::CUSTOM->value, + 'input_type' => PropertyInputType::DATE->value, + 'is_multilang' => false, + ]); + + Property::create([ + 'code' => 'dimensions', + 'name' => ['en' => 'Dimensions', 'sl' => 'Dimenzije'], + 'description' => ['en' => 'Approximate dimensions', 'sl' => 'Približne dimenzije'], + 'is_active' => true, + 'is_global' => true, + 'max_values' => 1, + 'enable_sorting' => false, + 'is_filter' => false, + 'type' => PropertyType::CUSTOM->value, + 'input_type' => PropertyInputType::DECIMAL->value, + 'is_multilang' => false, + ]); + + Property::create([ + 'code' => 'tech_sheet', + 'name' => ['en' => 'Tech sheet', 'sl' => 'Tehnični list'], + 'description' => ['en' => 'Technical sheet file', 'sl' => 'Datoteka tehničnega lista'], + 'is_active' => true, + 'is_global' => true, + 'max_values' => 1, + 'enable_sorting' => false, + 'is_filter' => false, + 'type' => PropertyType::CUSTOM->value, + 'input_type' => PropertyInputType::FILE->value, + 'is_multilang' => false, + ]); } } diff --git a/database/seeders/TaxClassSeeder.php b/database/seeders/TaxClassSeeder.php new file mode 100644 index 0000000..fc50262 --- /dev/null +++ b/database/seeders/TaxClassSeeder.php @@ -0,0 +1,69 @@ + '22%', $tenantFK => $tenant->id], + [ + 'description' => 'Standard VAT rate', + 'rate' => 22.00, + 'is_default' => true, + ] + ); + + // Ensure only one default per tenant + TaxClass::where($tenantFK, $tenant->id) + ->where('id', '!=', $default->id) + ->update(['is_default' => false]); + + TaxClass::updateOrCreate( + ['name' => '9.5%', $tenantFK => $tenant->id], + [ + 'description' => 'Reduced VAT rate', + 'rate' => 9.50, + 'is_default' => false, + ] + ); + }); + } + } else { + // Single-tenant / no tenancy + EloquentModel::withoutEvents(function () { + $default = TaxClass::updateOrCreate( + ['name' => '22%'], + [ + 'description' => 'Standard VAT rate', + 'rate' => 22.00, + 'is_default' => true, + ] + ); + + TaxClass::where('id', '!=', $default->id)->update(['is_default' => false]); + + TaxClass::updateOrCreate( + ['name' => '9.5%'], + [ + 'description' => 'Reduced VAT rate', + 'rate' => 9.50, + 'is_default' => false, + ] + ); + }); + } + } +} diff --git a/src/Filament/Resources/ProductResource.php b/src/Filament/Resources/ProductResource.php index 6af21e6..2a6ccdf 100644 --- a/src/Filament/Resources/ProductResource.php +++ b/src/Filament/Resources/ProductResource.php @@ -331,25 +331,7 @@ function ($query) { ->label($displayName) ->options($valueOptions) ->descriptions($property->values->pluck('info_url', 'id')->filter()->toArray()) - ->helperText($property->description) - ->createOptionForm([ - TextInput::make('value') - ->label('Value') - ->required() - ->maxLength(255), - TextInput::make('info_url') - ->label('Info URL') - ->url() - ->maxLength(255), - TextInput::make('image') - ->label('Image') - ->maxLength(255), - ]) - ->createOptionAction(function ($action) { - return $action - ->modalHeading('Create New Property Value') - ->modalSubmitActionLabel('Create Value'); - }); + ->helperText($property->description); break; case 'select': @@ -357,24 +339,6 @@ function ($query) { ->label($displayName) ->options($valueOptions) ->searchable() - ->createOptionForm([ - TextInput::make('value') - ->label('Value') - ->required() - ->maxLength(255), - TextInput::make('info_url') - ->label('Info URL') - ->url() - ->maxLength(255), - TextInput::make('image') - ->label('Image') - ->maxLength(255), - ]) - ->createOptionAction(function ($action) { - return $action - ->modalHeading('Create New Property Value') - ->modalSubmitActionLabel('Create Value'); - }) ->helperText($property->description); break; @@ -384,25 +348,7 @@ function ($query) { ->options($valueOptions) ->descriptions($property->values->pluck('info_url', 'id')->filter()->toArray()) ->helperText($property->description) - ->rules($property->max_values > 1 ? ["max:{$property->max_values}"] : []) - ->createOptionForm([ - TextInput::make('value') - ->label('Value') - ->required() - ->maxLength(255), - TextInput::make('info_url') - ->label('Info URL') - ->url() - ->maxLength(255), - TextInput::make('image') - ->label('Image') - ->maxLength(255), - ]) - ->createOptionAction(function ($action) { - return $action - ->modalHeading('Create New Property Value') - ->modalSubmitActionLabel('Create Value'); - }); + ->rules($property->max_values > 1 ? ["max:{$property->max_values}"] : []); break; case 'multiselect': @@ -411,24 +357,6 @@ function ($query) { ->options($valueOptions) ->multiple() ->searchable() - ->createOptionForm([ - TextInput::make('value') - ->label('Value') - ->required() - ->maxLength(255), - TextInput::make('info_url') - ->label('Info URL') - ->url() - ->maxLength(255), - TextInput::make('image') - ->label('Image') - ->maxLength(255), - ]) - ->createOptionAction(function ($action) { - return $action - ->modalHeading('Create New Property Value') - ->modalSubmitActionLabel('Create Value'); - }) ->helperText($property->description) ->rules($property->max_values > 1 ? ["max:{$property->max_values}"] : []); break;