has('products')) { return [ 'warehouse_id' => 'required|exists:warehouses,id', 'supplier_id' => 'nullable|exists:suppliers,id', 'invoice_reference' => 'required|string|max:255', 'notes' => 'nullable|string|max:1000', // Validación del array de productos 'products' => 'required|array|min:1', 'products.*.inventory_id' => 'required|exists:inventories,id', 'products.*.quantity' => 'required|numeric|min:0.001', 'products.*.unit_cost' => 'required|numeric|min:0', 'products.*.serial_numbers' => 'nullable|array', 'products.*.serial_numbers.*' => 'required|string|distinct|unique:inventory_serials,serial_number', 'products.*.unit_of_measure_id' => 'nullable|exists:units_of_measurement,id', ]; } return [ 'inventory_id' => 'required|exists:inventories,id', 'warehouse_id' => 'required|exists:warehouses,id', 'supplier_id' => 'nullable|exists:suppliers,id', 'quantity' => 'required|numeric|min:0.001', 'unit_cost' => 'required|numeric|min:0', 'invoice_reference' => 'required|string|max:255', 'notes' => 'nullable|string|max:1000', 'serial_numbers' => 'nullable|array', 'serial_numbers.*' => 'required|string|distinct|unique:inventory_serials,serial_number', 'unit_of_measure_id' => 'nullable|exists:units_of_measurement,id', ]; } public function messages(): array { return [ // Mensajes para entrada única 'inventory_id.required' => 'El producto es requerido', 'inventory_id.exists' => 'El producto no existe', 'serial_numbers.array' => 'Los números de serie deben ser un arreglo', 'serial_numbers.*.required' => 'El número de serie no puede estar vacío', 'serial_numbers.*.string' => 'El número de serie debe ser texto', 'serial_numbers.*.distinct' => 'Los números de serie no pueden repetirse', 'serial_numbers.*.unique' => 'El número de serie ya existe en el sistema', // Mensajes para entrada múltiple 'products.required' => 'Debe incluir al menos un producto', 'products.*.inventory_id.required' => 'El producto es requerido', 'products.*.inventory_id.exists' => 'El producto no existe', 'products.*.quantity.required' => 'La cantidad es requerida', 'products.*.quantity.min' => 'La cantidad debe ser al menos 1', 'products.*.unit_cost.required' => 'El costo unitario es requerido', 'products.*.unit_cost.numeric' => 'El costo unitario debe ser un número', 'products.*.unit_cost.min' => 'El costo unitario no puede ser negativo', 'products.*.serial_numbers.array' => 'Los números de serie deben ser un arreglo', 'products.*.serial_numbers.*.required' => 'El número de serie no puede estar vacío', 'products.*.serial_numbers.*.string' => 'El número de serie debe ser texto', 'products.*.serial_numbers.*.distinct' => 'Los números de serie no pueden repetirse', 'products.*.serial_numbers.*.unique' => 'El número de serie ya existe en el sistema', // Mensajes comunes 'warehouse_id.required' => 'El almacén es requerido', 'warehouse_id.exists' => 'El almacén no existe', 'quantity.required' => 'La cantidad es requerida', 'quantity.min' => 'La cantidad debe ser al menos 1', 'unit_cost.required' => 'El costo unitario es requerido', 'invoice_reference.required' => 'La referencia de la factura es requerida', ]; } /** * Validaciones adicionales de negocio */ public function withValidator($validator) { $validator->after(function ($validator) { // Extraer productos del request (entrada simple o múltiple) $products = $this->has('products') ? $this->products : [[ 'inventory_id' => $this->inventory_id, 'quantity' => $this->quantity, 'serial_numbers' => $this->serial_numbers ?? null, ]]; foreach ($products as $index => $product) { $inventory = Inventory::with('unitOfMeasure')->find($product['inventory_id']); if (! $inventory || ! $inventory->unitOfMeasure) { continue; } // VALIDACIÓN 1: Cantidades decimales solo con unidades que permiten decimales $quantity = $product['quantity']; $isDecimal = floor($quantity) != $quantity; if ($isDecimal && ! $inventory->unitOfMeasure->allows_decimals) { $field = $this->has('products') ? "products.{$index}.quantity" : 'quantity'; $validator->errors()->add( $field, "El producto '{$inventory->name}' usa la unidad '{$inventory->unitOfMeasure->name}' que no permite cantidades decimales. Use cantidades enteras." ); } // VALIDACIÓN 2: Validar equivalencia de unidad si se especifica $unitOfMeasureId = $product['unit_of_measure_id'] ?? null; if ($unitOfMeasureId && $unitOfMeasureId != $inventory->unit_of_measure_id) { $hasEquivalence = $inventory->unitEquivalences() ->where('unit_of_measure_id', $unitOfMeasureId) ->where('is_active', true) ->exists(); if (! $hasEquivalence) { $field = $this->has('products') ? "products.{$index}.unit_of_measure_id" : 'unit_of_measure_id'; $validator->errors()->add( $field, "El producto '{$inventory->name}' no tiene equivalencia configurada para esta unidad de medida." ); } if ($inventory->track_serials) { $field = $this->has('products') ? "products.{$index}.unit_of_measure_id" : 'unit_of_measure_id'; $validator->errors()->add( $field, 'No se pueden usar equivalencias de unidad para productos con rastreo de seriales.' ); } } // VALIDACIÓN 3: No permitir seriales con unidades decimales $serialNumbers = $product['serial_numbers'] ?? null; if (! empty($serialNumbers) && $inventory->unitOfMeasure->allows_decimals) { $field = $this->has('products') ? "products.{$index}.serial_numbers" : 'serial_numbers'; $validator->errors()->add( $field, "No se pueden registrar números de serie para el producto '{$inventory->name}' porque usa la unidad '{$inventory->unitOfMeasure->name}' que permite cantidades decimales. Los seriales solo son válidos para unidades discretas como 'Pieza'." ); } } }); } }