diff --git a/app/Http/Requests/App/InventoryStoreRequest.php b/app/Http/Requests/App/InventoryStoreRequest.php index e58aed7..7b1a88a 100644 --- a/app/Http/Requests/App/InventoryStoreRequest.php +++ b/app/Http/Requests/App/InventoryStoreRequest.php @@ -24,6 +24,7 @@ public function rules(): array 'sku' => ['nullable', 'string', 'max:50', 'unique:inventories,sku'], 'barcode' => ['nullable', 'string', 'unique:inventories,barcode'], 'category_id' => ['required', 'exists:categories,id'], + 'unit_of_measure_id' => ['required', 'exists:units_of_measurement,id'], 'track_serials' => ['nullable', 'boolean'], // Campos de Price @@ -60,6 +61,7 @@ public function messages(): array /** * Validación condicional: retail_price > cost solo si cost > 0 + * Y validación track_serials con unidades decimales */ public function withValidator($validator) { @@ -73,6 +75,17 @@ public function withValidator($validator) 'El precio de venta debe ser mayor que el costo.' ); } + + // Validar incompatibilidad track_serials + unidades decimales + if ($this->input('track_serials')) { + $unit = \App\Models\UnitOfMeasurement::find($this->input('unit_of_measure_id')); + if ($unit && $unit->allows_decimals) { + $validator->errors()->add( + 'track_serials', + 'No se pueden usar seriales con unidades fraccionarias (kg, L, m). Cambia a Unidad/Pieza/Caja o desactiva los seriales.' + ); + } + } }); } } diff --git a/app/Http/Requests/App/InventoryUpdateRequest.php b/app/Http/Requests/App/InventoryUpdateRequest.php index c47cf0c..2168aa3 100644 --- a/app/Http/Requests/App/InventoryUpdateRequest.php +++ b/app/Http/Requests/App/InventoryUpdateRequest.php @@ -26,6 +26,7 @@ public function rules(): array 'sku' => ['nullable', 'string', 'max:50'], 'barcode' => ['nullable', 'string', 'unique:inventories,barcode,' . $inventoryId], 'category_id' => ['nullable', 'exists:categories,id'], + 'unit_of_measure_id' => ['nullable', 'exists:units_of_measurement,id'], 'track_serials' => ['nullable', 'boolean'], // Campos de Price @@ -61,6 +62,7 @@ public function messages(): array /** * Validación condicional: retail_price > cost solo si cost > 0 + * Y validación track_serials con unidades decimales */ public function withValidator($validator) { @@ -74,6 +76,20 @@ public function withValidator($validator) 'El precio de venta debe ser mayor que el costo.' ); } + + // Validar incompatibilidad track_serials + unidades decimales + $trackSerials = $this->input('track_serials', $this->route('inventario')?->track_serials); + $unitId = $this->input('unit_of_measure_id', $this->route('inventario')?->unit_of_measure_id); + + if ($trackSerials && $unitId) { + $unit = \App\Models\UnitOfMeasurement::find($unitId); + if ($unit && $unit->allows_decimals) { + $validator->errors()->add( + 'track_serials', + 'No se pueden usar seriales con unidades fraccionarias (kg, L, m). Cambia a Unidad/Pieza/Caja o desactiva los seriales.' + ); + } + } }); } } diff --git a/app/Models/Inventory.php b/app/Models/Inventory.php index 29226d7..a59513b 100644 --- a/app/Models/Inventory.php +++ b/app/Models/Inventory.php @@ -20,6 +20,7 @@ class Inventory extends Model { protected $fillable = [ 'category_id', + 'unit_of_measure_id', 'name', 'sku', 'barcode', @@ -42,29 +43,29 @@ public function warehouses() } /** - * Stock total en todos los almacenes + * Stock total en todos los almacenes (ahora soporta decimales) */ - public function getStockAttribute(): int + public function getStockAttribute(): float { - return $this->warehouses()->sum('inventory_warehouse.stock'); + return (float) $this->warehouses()->sum('inventory_warehouse.stock'); } /** * Alias para compatibilidad */ - public function getTotalStockAttribute(): int + public function getTotalStockAttribute(): float { return $this->stock; } /** - * Stock en un almacén específico + * Stock en un almacén específico (ahora soporta decimales) */ - public function stockInWarehouse(int $warehouseId): int + public function stockInWarehouse(int $warehouseId): float { - return $this->warehouses() + return (float) ($this->warehouses() ->where('warehouse_id', $warehouseId) - ->value('inventory_warehouse.stock') ?? 0; + ->value('inventory_warehouse.stock') ?? 0); } public function category() @@ -72,6 +73,11 @@ public function category() return $this->belongsTo(Category::class); } + public function unitOfMeasure() + { + return $this->belongsTo(UnitOfMeasurement::class); + } + public function price() { return $this->hasOne(Price::class); diff --git a/app/Models/InventoryMovement.php b/app/Models/InventoryMovement.php index d62dad3..05be84d 100644 --- a/app/Models/InventoryMovement.php +++ b/app/Models/InventoryMovement.php @@ -21,7 +21,7 @@ class InventoryMovement extends Model ]; protected $casts = [ - 'quantity' => 'integer', + 'quantity' => 'decimal:3', 'unit_cost' => 'decimal:2', 'created_at' => 'datetime', ]; diff --git a/app/Models/InventoryWarehouse.php b/app/Models/InventoryWarehouse.php index a0ca484..dee1aa4 100644 --- a/app/Models/InventoryWarehouse.php +++ b/app/Models/InventoryWarehouse.php @@ -15,9 +15,9 @@ class InventoryWarehouse extends Model ]; protected $casts = [ - 'stock' => 'integer', - 'min_stock' => 'integer', - 'max_stock' => 'integer', + 'stock' => 'decimal:3', + 'min_stock' => 'decimal:3', + 'max_stock' => 'decimal:3', ]; // Relaciones @@ -38,7 +38,7 @@ public function isOverStock(): bool { return $this->max_stock && $this->stock >= $this->max_stock; } - public function hasAvailableStock(int $quantity): bool { + public function hasAvailableStock(float $quantity): bool { return $this->stock >= $quantity; } } diff --git a/app/Models/ReturnDetail.php b/app/Models/ReturnDetail.php index 61f01bc..72e4e32 100644 --- a/app/Models/ReturnDetail.php +++ b/app/Models/ReturnDetail.php @@ -15,9 +15,9 @@ class ReturnDetail extends Model ]; protected $casts = [ + 'quantity_returned' => 'decimal:3', 'unit_price' => 'decimal:2', 'subtotal' => 'decimal:2', - 'quantity_returned' => 'integer', ]; /** diff --git a/app/Models/SaleDetail.php b/app/Models/SaleDetail.php index 2cbcb94..07b6821 100644 --- a/app/Models/SaleDetail.php +++ b/app/Models/SaleDetail.php @@ -28,6 +28,7 @@ class SaleDetail extends Model ]; protected $casts = [ + 'quantity' => 'decimal:3', 'unit_price' => 'decimal:2', 'subtotal' => 'decimal:2', 'discount_percentage' => 'decimal:2', diff --git a/app/Models/UnitOfMeasurement.php b/app/Models/UnitOfMeasurement.php new file mode 100644 index 0000000..2690a73 --- /dev/null +++ b/app/Models/UnitOfMeasurement.php @@ -0,0 +1,50 @@ + + * + * @version 1.0.0 + */ +class UnitOfMeasurement extends Model +{ + protected $table = 'units_of_measurement'; + + protected $fillable = [ + 'name', + 'abbreviation', + 'allows_decimals', + 'is_active', + ]; + + protected $casts = [ + 'allows_decimals' => 'boolean', + 'is_active' => 'boolean', + ]; + + /** + * Scope para unidades activas + */ + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + /** + * Relación con inventarios + */ + public function inventories() + { + return $this->hasMany(Inventory::class, 'unit_of_measure_id'); + } +} diff --git a/database/migrations/2026_02_09_155850_create_units_of_measurement_table.php b/database/migrations/2026_02_09_155850_create_units_of_measurement_table.php new file mode 100644 index 0000000..09c115c --- /dev/null +++ b/database/migrations/2026_02_09_155850_create_units_of_measurement_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name')->unique(); + $table->string('abbreviation', 10); + $table->boolean('allows_decimals')->default(false); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('units_of_measurement'); + } +}; diff --git a/database/migrations/2026_02_09_155942_alter_quantities_to_decimal.php b/database/migrations/2026_02_09_155942_alter_quantities_to_decimal.php new file mode 100644 index 0000000..70b7053 --- /dev/null +++ b/database/migrations/2026_02_09_155942_alter_quantities_to_decimal.php @@ -0,0 +1,61 @@ +decimal('stock', 15, 3)->default(0)->change(); + $table->decimal('min_stock', 15, 3)->default(0)->change(); + $table->decimal('max_stock', 15, 3)->default(0)->change(); + }); + + // 2. sale_details.quantity + Schema::table('sale_details', function (Blueprint $table) { + $table->decimal('quantity', 15, 3)->change(); + }); + + // 3. inventory_movements.quantity + Schema::table('inventory_movements', function (Blueprint $table) { + $table->decimal('quantity', 15, 3)->change(); + }); + + // 4. return_details.quantity_returned + Schema::table('return_details', function (Blueprint $table) { + $table->decimal('quantity_returned', 15, 3)->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Revertir a integer + Schema::table('inventory_warehouse', function (Blueprint $table) { + $table->integer('stock')->default(0)->change(); + $table->integer('min_stock')->default(0)->change(); + $table->integer('max_stock')->default(0)->change(); + }); + + Schema::table('sale_details', function (Blueprint $table) { + $table->integer('quantity')->change(); + }); + + Schema::table('inventory_movements', function (Blueprint $table) { + $table->integer('quantity')->change(); + }); + + Schema::table('return_details', function (Blueprint $table) { + $table->integer('quantity_returned')->change(); + }); + } +}; diff --git a/database/migrations/2026_02_09_160008_add_unit_of_measure_id_to_inventories.php b/database/migrations/2026_02_09_160008_add_unit_of_measure_id_to_inventories.php new file mode 100644 index 0000000..94986bf --- /dev/null +++ b/database/migrations/2026_02_09_160008_add_unit_of_measure_id_to_inventories.php @@ -0,0 +1,32 @@ +foreignId('unit_of_measure_id') + ->after('category_id') + ->constrained('units_of_measurement') + ->onDelete('restrict'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('inventories', function (Blueprint $table) { + $table->dropForeign(['unit_of_measure_id']); + $table->dropColumn('unit_of_measure_id'); + }); + } +}; diff --git a/database/seeders/DevSeeder.php b/database/seeders/DevSeeder.php index ffea706..d0e969f 100644 --- a/database/seeders/DevSeeder.php +++ b/database/seeders/DevSeeder.php @@ -7,9 +7,9 @@ /** * Seeder de desarrollo - * + * * @author Moisés Cortés C. - * + * * @version 1.0.0 */ class DevSeeder extends Seeder @@ -22,5 +22,7 @@ public function run(): void $this->call(RoleSeeder::class); $this->call(UserSeeder::class); $this->call(SettingSeeder::class); + $this->call(ClientTierSeeder::class); + $this->call(UnitsSeeder::class); } } diff --git a/database/seeders/UnitsSeeder.php b/database/seeders/UnitsSeeder.php new file mode 100644 index 0000000..3670405 --- /dev/null +++ b/database/seeders/UnitsSeeder.php @@ -0,0 +1,27 @@ + 'Pieza', 'abbreviation' => 'u', 'allows_decimals' => false], + ['name' => 'Kilogramo', 'abbreviation' => 'kg', 'allows_decimals' => true], + ['name' => 'Litro', 'abbreviation' => 'L', 'allows_decimals' => true], + ['name' => 'Metro', 'abbreviation' => 'm', 'allows_decimals' => true], + ]; + + foreach ($units as $unit) { + UnitOfMeasurement::firstOrCreate( + ['name' => $unit['name']], + $unit + ); + } + } +}