diff --git a/app/Http/Controllers/App/InventoryController.php b/app/Http/Controllers/App/InventoryController.php index 9a93c6e..f19d997 100644 --- a/app/Http/Controllers/App/InventoryController.php +++ b/app/Http/Controllers/App/InventoryController.php @@ -1,10 +1,10 @@ withCount('serials') + $products = CatalogItem::with(['category', 'price'])->withCount('serials') ->where('is_active', true); @@ -43,14 +43,14 @@ public function index(Request $request) ]); } - public function show(Inventory $inventario) + public function show(CatalogItem $catalogItem) { return ApiResponse::OK->response([ - 'model' => $inventario->load(['category', 'price'])->loadCount('serials') + 'model' => $catalogItem->load(['category', 'price'])->loadCount('serials') ]); } - public function store(InventoryStoreRequest $request) + public function store(CatalogItemStoreRequest $request) { $product = $this->productService->createProduct($request->validated()); @@ -59,26 +59,25 @@ public function store(InventoryStoreRequest $request) ]); } - public function update(InventoryUpdateRequest $request, Inventory $inventario) + public function update(CatalogItemUpdateRequest $request, CatalogItem $catalogItem) { - $product = $this->productService->updateProduct($inventario, $request->validated()); + $product = $this->productService->updateProduct($catalogItem, $request->validated()); return ApiResponse::OK->response([ 'model' => $product ]); } - public function destroy(Inventory $inventario) + public function destroy(CatalogItem $catalogItem) { - $inventario->delete(); - + $catalogItem->delete(); return ApiResponse::OK->response(); } /** * Importar productos desde Excel */ - public function import(InventoryImportRequest $request) + public function import(CatalogItemImportRequest $request) { try { $import = new ProductsImport(); diff --git a/app/Http/Controllers/App/InventorySerialController.php b/app/Http/Controllers/App/InventorySerialController.php index b61a161..31308da 100644 --- a/app/Http/Controllers/App/InventorySerialController.php +++ b/app/Http/Controllers/App/InventorySerialController.php @@ -4,7 +4,7 @@ */ use App\Http\Controllers\Controller; -use App\Models\Inventory; +use App\Models\CatalogItem; use App\Models\InventorySerial; use Illuminate\Http\Request; use Notsoweb\ApiResponse\Enums\ApiResponse; @@ -20,9 +20,9 @@ class InventorySerialController extends Controller /** * Listar seriales de un producto */ - public function index(Inventory $inventario, Request $request) + public function index(CatalogItem $catalogItem, Request $request) { - $query = $inventario->serials(); + $query = $catalogItem->serials(); if ($request->has('status')) { $query->where('status', $request->status); @@ -37,17 +37,17 @@ public function index(Inventory $inventario, Request $request) return ApiResponse::OK->response([ 'serials' => $serials, - 'inventory' => $inventario->load('category'), + 'catalogItem' => $catalogItem->load('category'), ]); } /** * Mostrar un serial específico */ - public function show(Inventory $inventario, InventorySerial $serial) + public function show(CatalogItem $catalogItem, InventorySerial $serial) { // Verificar que el serial pertenece al inventario - if ($serial->inventory_id !== $inventario->id) { + if ($serial->catalog_item_id !== $catalogItem->id) { return ApiResponse::NOT_FOUND->response([ 'message' => 'Serial no encontrado para este inventario' ]); @@ -55,14 +55,14 @@ public function show(Inventory $inventario, InventorySerial $serial) return ApiResponse::OK->response([ 'serial' => $serial->load('saleDetail'), - 'inventory' => $inventario->load('category'), + 'catalogItem' => $catalogItem->load('category'), ]); } /** * Crear un nuevo serial */ - public function store(Inventory $inventario, Request $request) + public function store(CatalogItem $catalogItem, Request $request) { $request->validate([ 'serial_number' => ['required', 'string', 'unique:inventory_serials,serial_number'], @@ -70,28 +70,28 @@ public function store(Inventory $inventario, Request $request) ]); $serial = InventorySerial::create([ - 'inventory_id' => $inventario->id, + 'catalog_item_id' => $catalogItem->id, 'serial_number' => $request->serial_number, 'status' => 'disponible', 'notes' => $request->notes, ]); // Sincronizar stock - $inventario->syncStock(); + $catalogItem->syncStock(); return ApiResponse::CREATED->response([ 'serial' => $serial, - 'inventory' => $inventario->fresh(), + 'catalogItem' => $catalogItem->fresh(), ]); } /** * Actualizar un serial */ - public function update(Inventory $inventario , InventorySerial $serial, Request $request) + public function update(CatalogItem $catalogItem , InventorySerial $serial, Request $request) { // Verificar que el serial pertenece al inventario - if ($serial->inventory_id !== $inventario->id) { + if ($serial->catalog_item_id !== $catalogItem->id) { return ApiResponse::NOT_FOUND->response([ 'message' => 'Serial no encontrado para este inventario' ]); @@ -106,21 +106,21 @@ public function update(Inventory $inventario , InventorySerial $serial, Request $serial->update($request->only(['serial_number', 'status', 'notes'])); // Sincronizar stock del inventario - $inventario->syncStock(); + $catalogItem->syncStock(); return ApiResponse::OK->response([ 'serial' => $serial->fresh(), - 'inventory' => $inventario->fresh(), + 'catalogItem' => $catalogItem->fresh(), ]); } /** * Eliminar un serial */ - public function destroy(Inventory $inventario, InventorySerial $serial) + public function destroy(CatalogItem $catalogItem, InventorySerial $serial) { // Verificar que el serial pertenece al inventario - if ($serial->inventory_id !== $inventario->id) { + if ($serial->catalog_item_id !== $catalogItem->id) { return ApiResponse::NOT_FOUND->response([ 'message' => 'Serial no encontrado para este inventario' ]); @@ -129,11 +129,10 @@ public function destroy(Inventory $inventario, InventorySerial $serial) $serial->delete(); // Sincronizar stock - $inventario->syncStock(); - + $catalogItem->syncStock(); return ApiResponse::OK->response([ 'message' => 'Serial eliminado exitosamente', - 'inventory' => $inventario->fresh(), + 'catalogItem' => $catalogItem->fresh(), ]); } } diff --git a/app/Http/Requests/App/InventoryImportRequest.php b/app/Http/Requests/App/CatalogItemImportRequest.php similarity index 98% rename from app/Http/Requests/App/InventoryImportRequest.php rename to app/Http/Requests/App/CatalogItemImportRequest.php index 7c9b2d2..dd3a11f 100644 --- a/app/Http/Requests/App/InventoryImportRequest.php +++ b/app/Http/Requests/App/CatalogItemImportRequest.php @@ -4,7 +4,7 @@ use Illuminate\Foundation\Http\FormRequest; -class InventoryImportRequest extends FormRequest +class CatalogItemImportRequest extends FormRequest { /** * Determine if the user is authorized to make this request. diff --git a/app/Http/Requests/App/InventoryStoreRequest.php b/app/Http/Requests/App/CatalogItemStoreRequest.php similarity index 78% rename from app/Http/Requests/App/InventoryStoreRequest.php rename to app/Http/Requests/App/CatalogItemStoreRequest.php index ac93521..52a234f 100644 --- a/app/Http/Requests/App/InventoryStoreRequest.php +++ b/app/Http/Requests/App/CatalogItemStoreRequest.php @@ -2,7 +2,7 @@ use Illuminate\Foundation\Http\FormRequest; -class InventoryStoreRequest extends FormRequest +class CatalogItemStoreRequest extends FormRequest { /** * Determine if the user is authorized to make this request. @@ -25,6 +25,12 @@ public function rules(): array 'barcode' => ['nullable', 'string', 'unique:inventories,barcode'], 'category_id' => ['required', 'exists:categories,id'], 'stock' => ['nullable', 'integer', 'min:0'], + 'type' => ['nullable', 'string', 'in:product,service,laundry'], + 'description' => ['nullable', 'string'], + 'is_stockable' => ['nullable', 'boolean'], + 'track_serials' => ['nullable', 'boolean'], + 'attributes' => ['nullable', 'array'], + // Campos de Price 'cost' => ['required', 'numeric', 'min:0'], @@ -48,6 +54,10 @@ public function messages(): array 'category_id.required' => 'La categoría es obligatoria.', 'category_id.exists' => 'La categoría seleccionada no es válida.', 'stock.min' => 'El stock no puede ser negativo.', + 'type.in' => 'El tipo debe ser uno de los siguientes: product, service, laundry.', + 'is_stockable.boolean' => 'El campo "es stockeable" debe ser verdadero o falso.', + 'track_serials.boolean' => 'El campo "rastrear números de serie" debe ser verdadero o falso.', + 'attributes.array' => 'Los atributos deben ser un arreglo.', // Mensajes de Price 'cost.required' => 'El costo es obligatorio.', diff --git a/app/Http/Requests/App/InventoryUpdateRequest.php b/app/Http/Requests/App/CatalogItemUpdateRequest.php similarity index 77% rename from app/Http/Requests/App/InventoryUpdateRequest.php rename to app/Http/Requests/App/CatalogItemUpdateRequest.php index edf78cb..1f89892 100644 --- a/app/Http/Requests/App/InventoryUpdateRequest.php +++ b/app/Http/Requests/App/CatalogItemUpdateRequest.php @@ -2,7 +2,7 @@ use Illuminate\Foundation\Http\FormRequest; -class InventoryUpdateRequest extends FormRequest +class CatalogItemUpdateRequest extends FormRequest { /** * Determine if the user is authorized to make this request. @@ -27,6 +27,11 @@ public function rules(): array 'barcode' => ['nullable', 'string', 'unique:inventories,barcode,' . $inventoryId], 'category_id' => ['nullable', 'exists:categories,id'], 'stock' => ['nullable', 'integer', 'min:0'], + 'type' => ['nullable', 'string', 'in:product,service,laundry'], + 'description' => ['nullable', 'string'], + 'is_stockable' => ['nullable', 'boolean'], + 'track_serials' => ['nullable', 'boolean'], + 'attributes' => ['nullable', 'array'], // Campos de Price 'cost' => ['nullable', 'numeric', 'min:0'], @@ -48,6 +53,10 @@ public function messages(): array 'barcode.unique' => 'El código de barras ya está registrado en otro producto.', 'category_id.exists' => 'La categoría seleccionada no es válida.', 'stock.min' => 'El stock no puede ser negativo.', + 'type.in' => 'El tipo debe ser uno de los siguientes: product, service, laundry.', + 'is_stockable.boolean' => 'El campo "es stockeable" debe ser verdadero o falso.', + 'track_serials.boolean' => 'El campo "rastrear números de serie" debe ser verdadero o falso.', + 'attributes.array' => 'Los atributos deben ser un arreglo.', // Mensajes de Price 'cost.numeric' => 'El costo debe ser un número.', diff --git a/app/Imports/ProductsImport.php b/app/Imports/ProductsImport.php index bde158a..3f2d03b 100644 --- a/app/Imports/ProductsImport.php +++ b/app/Imports/ProductsImport.php @@ -2,10 +2,10 @@ namespace App\Imports; -use App\Models\Inventory; +use App\Models\CatalogItem; use App\Models\Price; use App\Models\Category; -use App\Http\Requests\App\InventoryImportRequest; +use App\Http\Requests\App\CatalogItemImportRequest; use App\Models\InventorySerial; use Maatwebsite\Excel\Concerns\ToModel; use Maatwebsite\Excel\Concerns\WithHeadingRow; @@ -68,10 +68,10 @@ public function model(array $row) // Buscar producto existente por SKU o código de barras $existingInventory = null; if (!empty($row['sku'])) { - $existingInventory = Inventory::where('sku', trim($row['sku']))->first(); + $existingInventory = CatalogItem::where('sku', trim($row['sku']))->first(); } if (!$existingInventory && !empty($row['codigo_barras'])) { - $existingInventory = Inventory::where('barcode', trim($row['codigo_barras']))->first(); + $existingInventory = CatalogItem::where('barcode', trim($row['codigo_barras']))->first(); } // Si el producto ya existe, solo agregar stock y seriales @@ -100,29 +100,29 @@ public function model(array $row) } // Crear el producto en inventario - $inventory = new Inventory(); - $inventory->name = trim($row['nombre']); - $inventory->sku = !empty($row['sku']) ? trim($row['sku']) : null; - $inventory->barcode = !empty($row['codigo_barras']) ? trim($row['codigo_barras']) : null; - $inventory->category_id = $categoryId; - $inventory->stock = 0; - $inventory->is_active = true; - $inventory->save(); + $catalogItem = new CatalogItem(); + $catalogItem->name = trim($row['nombre']); + $catalogItem->sku = !empty($row['sku']) ? trim($row['sku']) : null; + $catalogItem->barcode = !empty($row['codigo_barras']) ? trim($row['codigo_barras']) : null; + $catalogItem->category_id = $categoryId; + $catalogItem->stock = 0; + $catalogItem->is_active = true; + $catalogItem->save(); // Crear el precio del producto Price::create([ - 'inventory_id' => $inventory->id, + 'catalog_item_id' => $catalogItem->id, 'cost' => $costo, 'retail_price' => $precioVenta, 'tax' => !empty($row['impuesto']) ? (float) $row['impuesto'] : 0, ]); // Crear números de serie si se proporcionan - $this->addSerials($inventory, $row['numeros_serie'] ?? null, $row['stock'] ?? 0); + $this->addSerials($catalogItem, $row['numeros_serie'] ?? null, $row['stock'] ?? 0); $this->imported++; - return $inventory; + return $catalogItem; } catch (\Exception $e) { $this->skipped++; $this->errors[] = "Error en fila: " . $e->getMessage(); @@ -133,7 +133,7 @@ public function model(array $row) /** * Actualiza un producto existente: suma stock y agrega seriales nuevos */ - private function updateExistingProduct(Inventory $inventory, array $row) + private function updateExistingProduct(CatalogItem $catalogItem, array $row) { $serialsAdded = 0; $serialsSkipped = 0; @@ -151,7 +151,7 @@ private function updateExistingProduct(Inventory $inventory, array $row) if (!$exists) { InventorySerial::create([ - 'inventory_id' => $inventory->id, + 'catalog_item_id' => $catalogItem->id, 'serial_number' => $serial, 'status' => 'disponible', ]); @@ -162,17 +162,17 @@ private function updateExistingProduct(Inventory $inventory, array $row) } // Sincronizar stock basado en seriales disponibles - $inventory->syncStock(); + $catalogItem->syncStock(); } else { // Producto sin seriales: sumar stock $stockToAdd = (int) ($row['stock'] ?? 0); - $inventory->increment('stock', $stockToAdd); + $catalogItem->increment('stock', $stockToAdd); } $this->updated++; if ($serialsSkipped > 0) { - $this->errors[] = "Producto '{$inventory->name}': {$serialsSkipped} seriales ya existían y fueron ignorados"; + $this->errors[] = "Producto '{$catalogItem->name}': {$serialsSkipped} seriales ya existían y fueron ignorados"; } return null; // No retornar modelo para evitar que Maatwebsite intente guardarlo @@ -181,7 +181,7 @@ private function updateExistingProduct(Inventory $inventory, array $row) /** * Agrega seriales a un producto nuevo */ - private function addSerials(Inventory $inventory, ?string $serialsString, int $stockFromExcel) + private function addSerials(CatalogItem $catalogItem, ?string $serialsString, int $stockFromExcel) { if (!empty($serialsString)) { $serials = explode(',', $serialsString); @@ -190,17 +190,17 @@ private function addSerials(Inventory $inventory, ?string $serialsString, int $s $serial = trim($serial); if (!empty($serial)) { InventorySerial::create([ - 'inventory_id' => $inventory->id, + 'catalog_item_id' => $catalogItem->id, 'serial_number' => $serial, 'status' => 'disponible', ]); } } - $inventory->syncStock(); + $catalogItem->syncStock(); } else { // Producto sin seriales - $inventory->stock = $stockFromExcel; - $inventory->save(); + $catalogItem->stock = $stockFromExcel; + $catalogItem->save(); } } @@ -209,7 +209,7 @@ private function addSerials(Inventory $inventory, ?string $serialsString, int $s */ public function rules(): array { - return InventoryImportRequest::rowRules(); + return CatalogItemImportRequest::rowRules(); } /** @@ -217,7 +217,7 @@ public function rules(): array */ public function customValidationMessages() { - return InventoryImportRequest::rowMessages(); + return CatalogItemImportRequest::rowMessages(); } /** diff --git a/app/Models/CatalogItem.php b/app/Models/CatalogItem.php new file mode 100644 index 0000000..43a61f2 --- /dev/null +++ b/app/Models/CatalogItem.php @@ -0,0 +1,149 @@ + 'boolean', + 'track_serials' => 'boolean', + 'is_active' => 'boolean', + 'attributes' => 'array', + ]; + + protected $appends = ['has_serials']; + + // RELACIONES + + public function category() + { + return $this->belongsTo(Category::class); + } + + public function price() + { + return $this->hasOne(Price::class); + } + + public function serials() + { + return $this->hasMany(InventorySerial::class); + } + + public function availableSerials() + { + return $this->hasMany(InventorySerial::class) + ->where('status', 'disponible'); + } + + // HELPERS DE TIPO + + public function isProduct(): bool + { + return $this->type === 'product'; + } + + public function isService(): bool + { + return $this->type === 'service'; + } + + public function isLaundry(): bool + { + return $this->type === 'laundry'; + } + + // HELPERS DE STOCK + + public function getAvailableStockAttribute(): int + { + if (!$this->is_stockable) { + return 0; + } + + if ($this->track_serials) { + return $this->availableSerials()->count(); + } + + return $this->stock ?? 0; + } + + public function syncStock(): void + { + if ($this->is_stockable && $this->track_serials) { + $this->update(['stock' => $this->availableSerials()->count()]); + } + } + + public function decrementStock(int $quantity): void + { + if ($this->is_stockable && !$this->track_serials) { + $this->decrement('stock', $quantity); + } + } + + public function incrementStock(int $quantity): void + { + if ($this->is_stockable && !$this->track_serials) { + $this->increment('stock', $quantity); + } + } + + public function getHasSerialsAttribute(): bool + { + return $this->track_serials && + isset($this->attributes['serials_count']) && + $this->attributes['serials_count'] > 0; + } + + //HELPERS DE ATRIBUTOS + + public function getAtributos(string $key, $default = null) + { + return $this->attributes[$key] ?? $default; + } + + //SCOPES + + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + public function scopeOfType($query, string $type) + { + return $query->where('type', $type); + } + + public function scopeProducts($query) + { + return $query->where('type', 'product'); + } + + public function scopeServices($query) + { + return $query->where('type', 'service'); + } + + public function scopeStockable($query) + { + return $query->where('is_stockable', true); + } +} diff --git a/app/Models/Category.php b/app/Models/Category.php index f06409e..bcc5471 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -25,8 +25,8 @@ class Category extends Model 'is_active' => 'boolean', ]; - public function inventories() + public function catalogItems() { - return $this->hasMany(Inventory::class); + return $this->hasMany(CatalogItem::class); } } diff --git a/app/Models/Inventory.php b/app/Models/Inventory.php deleted file mode 100644 index 697b911..0000000 --- a/app/Models/Inventory.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * @version 1.0.0 - */ -class Inventory extends Model -{ - protected $fillable = [ - 'category_id', - 'name', - 'sku', - 'barcode', - 'stock', - 'is_active', - ]; - - protected $casts = [ - 'is_active' => 'boolean', - ]; - - protected $appends = ['has_serials']; - - public function category() - { - return $this->belongsTo(Category::class); - } - - public function price() - { - return $this->hasOne(Price::class); - } - - public function serials() - { - return $this->hasMany(InventorySerial::class); - } - - /** - * Obtener seriales disponibles - */ - public function availableSerials() - { - return $this->hasMany(InventorySerial::class) - ->where('status', 'disponible'); - } - - /** - * Calcular stock basado en seriales disponibles - */ - public function getAvailableStockAttribute(): int - { - return $this->availableSerials()->count(); - } - - /** - * Sincronizar el campo stock con los seriales disponibles - */ - public function syncStock(): void - { - $this->update(['stock' => $this->getAvailableStockAttribute()]); - } - - public function getHasSerialsAttribute(): bool - { - return isset($this->attributes['serials_count']) && $this->attributes['serials_count'] > 0; - } -} diff --git a/app/Models/InventorySerial.php b/app/Models/InventorySerial.php index 1410154..3ccddf6 100644 --- a/app/Models/InventorySerial.php +++ b/app/Models/InventorySerial.php @@ -2,16 +2,10 @@ use Illuminate\Database\Eloquent\Model; -/** - * Modelo para números de serie de inventario - * - * @author Moisés Cortés C. - * @version 1.0.0 - */ class InventorySerial extends Model { protected $fillable = [ - 'inventory_id', + 'catalog_item_id', 'serial_number', 'status', 'sale_detail_id', @@ -22,9 +16,10 @@ class InventorySerial extends Model 'status' => 'string', ]; - public function inventory() + //RELACIONES + public function catalogItem() { - return $this->belongsTo(Inventory::class); + return $this->belongsTo(CatalogItem::class); } public function saleDetail() @@ -32,17 +27,12 @@ public function saleDetail() return $this->belongsTo(SaleDetail::class); } - /** - * Verificar si el serial está disponible - */ + //HELPERS public function isAvailable(): bool { return $this->status === 'disponible'; } - /** - * Marcar como vendido - */ public function markAsSold(int $saleDetailId): void { $this->update([ @@ -51,9 +41,6 @@ public function markAsSold(int $saleDetailId): void ]); } - /** - * Marcar como disponible (ej: cancelación de venta) - */ public function markAsAvailable(): void { $this->update([ diff --git a/app/Models/Price.php b/app/Models/Price.php index 8fed928..75ef7ad 100644 --- a/app/Models/Price.php +++ b/app/Models/Price.php @@ -16,7 +16,7 @@ class Price extends Model { protected $fillable = [ - 'inventory_id', + 'catalog_itme_id', 'cost', 'retail_price', 'tax', @@ -28,8 +28,8 @@ class Price extends Model 'tax' => 'decimal:2', ]; - public function inventory() + public function catalogItem() { - return $this->belongsTo(Inventory::class); + return $this->belongsTo(CatalogItem::class); } } diff --git a/app/Models/SaleDetail.php b/app/Models/SaleDetail.php index 0972e5f..c0e76a9 100644 --- a/app/Models/SaleDetail.php +++ b/app/Models/SaleDetail.php @@ -17,7 +17,7 @@ class SaleDetail extends Model { protected $fillable = [ 'sale_id', - 'inventory_id', + 'catalog_item_id', 'product_name', 'quantity', 'unit_price', @@ -34,9 +34,9 @@ public function sale() return $this->belongsTo(Sale::class); } - public function inventory() + public function catalogItem() { - return $this->belongsTo(Inventory::class); + return $this->belongsTo(CatalogItem::class); } public function serials() diff --git a/app/Models/SettingsGlobal.php b/app/Models/SettingsGlobal.php new file mode 100644 index 0000000..2ed6392 --- /dev/null +++ b/app/Models/SettingsGlobal.php @@ -0,0 +1,36 @@ + 'array', + 'invoice' => 'array', + ]; + + //HELPERS + public static function get(): ?self + { + return self::first(); + } + + public function getContactValue(string $key, $default = null) + { + return $this->contact_info[$key] ?? $default; + } + + public function getInvoiceValue(string $key, $default = null) + { + return $this->invoice[$key] ?? $default; + } +} diff --git a/app/Services/ProductService.php b/app/Services/ProductService.php index 88e0a3b..d38bf0f 100644 --- a/app/Services/ProductService.php +++ b/app/Services/ProductService.php @@ -1,6 +1,6 @@ $data['name'], 'sku' => $data['sku'], 'barcode' => $data['barcode'] ?? null, @@ -18,21 +18,21 @@ public function createProduct(array $data) ]); $price = Price::create([ - 'inventory_id' => $inventory->id, + 'catalog_item_id' => $catalogItem->id, 'cost' => $data['cost'], 'retail_price' => $data['retail_price'], 'tax' => $data['tax'] ?? 16.00, ]); - return $inventory->load(['category', 'price']); + return $catalogItem->load(['category', 'price']); }); } - public function updateProduct(Inventory $inventory, array $data) + public function updateProduct(CatalogItem $catalogItem, array $data) { - return DB::transaction(function () use ($inventory, $data) { - // Actualizar campos de Inventory solo si están presentes - $inventoryData = array_filter([ + return DB::transaction(function () use ($catalogItem, $data) { + // Actualizar campos de CatalogItem solo si están presentes + $catalogItemData = array_filter([ 'name' => $data['name'] ?? null, 'sku' => $data['sku'] ?? null, 'barcode' => $data['barcode'] ?? null, @@ -40,8 +40,8 @@ public function updateProduct(Inventory $inventory, array $data) 'stock' => $data['stock'] ?? null, ], fn($value) => $value !== null); - if (!empty($inventoryData)) { - $inventory->update($inventoryData); + if (!empty($catalogItemData)) { + $catalogItem->update($catalogItemData); } // Actualizar campos de Price solo si están presentes @@ -52,13 +52,13 @@ public function updateProduct(Inventory $inventory, array $data) ], fn($value) => $value !== null); if (!empty($priceData)) { - $inventory->price()->updateOrCreate( - ['inventory_id' => $inventory->id], + $catalogItem->price()->updateOrCreate( + ['catalog_item_id' => $catalogItem->id], $priceData ); } - return $inventory->fresh(['category', 'price']); + return $catalogItem->fresh(['category', 'price']); }); } } diff --git a/app/Services/ReportService.php b/app/Services/ReportService.php index 37cb3b4..54bbbb3 100644 --- a/app/Services/ReportService.php +++ b/app/Services/ReportService.php @@ -2,7 +2,7 @@ namespace App\Services; -use App\Models\Inventory; +use App\Models\CatalogItem; use App\Models\SaleDetail; use Illuminate\Support\Facades\DB; @@ -19,18 +19,18 @@ public function getTopSellingProduct(?string $fromDate = null, ?string $toDate = { $query = SaleDetail::query() ->selectRaw(' - inventories.id, - inventories.name, - inventories.sku, + catalog_items.id, + catalog_items.name, + catalog_items.sku, categories.name as category_name, SUM(sale_details.quantity) as total_quantity_sold, SUM(sale_details.subtotal) as total_revenue, COUNT(DISTINCT sale_details.sale_id) as times_sold, MAX(sales.created_at) as last_sale_date, - inventories.created_at as added_date + catalog_items.created_at as added_date ') - ->join('inventories', 'sale_details.inventory_id', '=', 'inventories.id') - ->join('categories', 'inventories.category_id', '=', 'categories.id') + ->join('catalog_items', 'sale_details.catalog_item_id', '=', 'catalog_items.id') + ->join('categories', 'catalog_items.category_id', '=', 'categories.id') ->join('sales', 'sale_details.sale_id', '=', 'sales.id') ->where('sales.status', 'completed') ->whereNull('sales.deleted_at'); @@ -41,8 +41,8 @@ public function getTopSellingProduct(?string $fromDate = null, ?string $toDate = } $result = $query - ->groupBy('inventories.id', 'inventories.name', 'inventories.sku', - 'categories.name', 'inventories.created_at') + ->groupBy('catalog_items.id', 'catalog_items.name', 'catalog_items.sku', + 'categories.name', 'catalog_items.created_at') ->orderByDesc('total_quantity_sold') ->first(); @@ -65,35 +65,35 @@ public function getProductsWithoutMovement(bool $includeStockValue = true, ?stri ->whereNull('sales.deleted_at') ->whereBetween(DB::raw('DATE(sales.created_at)'), [$fromDate, $toDate]) ->distinct() - ->pluck('sale_details.inventory_id') + ->pluck('sale_details.catalog_item_id') ->toArray(); // Construir query para productos SIN ventas - $query = Inventory::query() + $query = CatalogItem::query() ->selectRaw(' - inventories.id, - inventories.name, - inventories.sku, - inventories.stock, + catalog_items.id, + catalog_items.name, + catalog_items.sku, + catalog_items.stock, categories.name as category_name, - inventories.created_at as date_added, + catalog_items.created_at as date_added, prices.retail_price '); // Agregar valor del inventario si se solicita if ($includeStockValue) { $query->addSelect( - DB::raw('(inventories.stock * COALESCE(prices.cost, 0)) as inventory_value') + DB::raw('(catalog_items.stock * COALESCE(prices.cost, 0)) as inventory_value') ); } return $query - ->leftJoin('categories', 'inventories.category_id', '=', 'categories.id') - ->leftJoin('prices', 'inventories.id', '=', 'prices.inventory_id') - ->where('inventories.is_active', true) - ->whereNull('inventories.deleted_at') - ->whereNotIn('inventories.id', $inventoriesWithSales) - ->orderBy('inventories.created_at') + ->leftJoin('categories', 'catalog_items.category_id', '=', 'categories.id') + ->leftJoin('prices', 'catalog_items.id', '=', 'prices.catalog_item_id') + ->where('catalog_items.is_active', true) + ->whereNull('catalog_items.deleted_at') + ->whereNotIn('catalog_items.id', $inventoriesWithSales) + ->orderBy('catalog_items.created_at') ->get() ->toArray(); } diff --git a/app/Services/SaleService.php b/app/Services/SaleService.php index 09a730a..3b73846 100644 --- a/app/Services/SaleService.php +++ b/app/Services/SaleService.php @@ -1,9 +1,9 @@ $sale->id, - 'inventory_id' => $item['inventory_id'], + 'catalog_item_id' => $item['catalog_item_id'], 'product_name' => $item['product_name'], 'quantity' => $item['quantity'], 'unit_price' => $item['unit_price'], @@ -53,13 +53,13 @@ public function createSale(array $data) ]); // Obtener el inventario - $inventory = Inventory::find($item['inventory_id']); + $catalogItem = CatalogItem::find($item['catalog_item']); - if ($inventory) { + if ($catalogItem && $catalogItem->is_stockable) { // Si se proporcionaron números de serie específicos if (isset($item['serial_numbers']) && is_array($item['serial_numbers'])) { foreach ($item['serial_numbers'] as $serialNumber) { - $serial = InventorySerial::where('inventory_id', $inventory->id) + $serial = InventorySerial::where('catalog_item_id', $catalogItem->id) ->where('serial_number', $serialNumber) ->where('status', 'disponible') ->first(); @@ -72,7 +72,7 @@ public function createSale(array $data) } } else { // Asignar automáticamente los primeros N seriales disponibles - $serials = InventorySerial::where('inventory_id', $inventory->id) + $serials = InventorySerial::where('catalog_item_id', $catalogItem->id) ->where('status', 'disponible') ->limit($item['quantity']) ->get(); @@ -87,12 +87,12 @@ public function createSale(array $data) } // Sincronizar el stock - $inventory->syncStock(); + $catalogItem->syncStock(); } } // 3. Retornar la venta con sus relaciones cargadas - return $sale->load(['details.inventory', 'details.serials', 'user']); + return $sale->load(['details.catalogItem', 'details.serials', 'user']); }); } @@ -117,13 +117,13 @@ public function cancelSale(Sale $sale) } // Sincronizar stock - $detail->inventory->syncStock(); + $detail->catalogItem->syncStock(); } // Marcar venta como cancelada $sale->update(['status' => 'cancelled']); - return $sale->fresh(['details.inventory', 'details.serials', 'user']); + return $sale->fresh(['details.catalogItem', 'details.serials', 'user']); }); } diff --git a/database/migrations/2026_01_22_105301_rename_inventories_table_to_catalog_items_table.php b/database/migrations/2026_01_22_105301_rename_inventories_table_to_catalog_items_table.php new file mode 100644 index 0000000..0a26b93 --- /dev/null +++ b/database/migrations/2026_01_22_105301_rename_inventories_table_to_catalog_items_table.php @@ -0,0 +1,87 @@ +string('type', 50)->default('product')->after('id'); + $table->boolean('is_stockable')->default(false)->after('barcode'); + $table->boolean('track_serials')->default(false)->after('stock'); + $table->json('attributes')->nullable()->after('track_serials'); + $table->text('description')->nullable()->after('name'); + + $table->string('sku')->nullable()->change(); + $table->integer('stock')->nullable()->change(); + }); + + Schema::table('prices', function (Blueprint $table) { + $table->dropForeign(['inventory_id']); + $table->renameColumn('inventory_id', 'catalog_item_id'); + }); + + + Schema::table('prices', function (Blueprint $table) { + $table->foreign('catalog_item_id') + ->references('id') + ->on('catalog_items') + ->onDelete('cascade'); + }); + + Schema::table('inventory_serials', function (Blueprint $table) { + $table->dropForeign(['inventory_id']); + $table->renameColumn('inventory_id', 'catalog_item_id'); + }); + + Schema::table('inventory_serials', function (Blueprint $table) { + $table->foreign('catalog_item_id') + ->references('id') + ->on('catalog_items') + ->onDelete('cascade'); + }); + + Schema::table('sale_details', function (Blueprint $table) { + $table->dropForeign(['inventory_id']); + $table->renameColumn('inventory_id', 'catalog_item_id'); + }); + + + Schema::table('sale_details', function (Blueprint $table) { + $table->foreign('catalog_item_id') + ->references('id') + ->on('catalog_items') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('catalog_items', function (Blueprint $table) { + // Quitar lo agregado + $table->dropColumn([ + 'type', + 'is_stockable', + 'track_serials', + 'attributes', + 'description', + ]); + + // Revertir cambios (ajusta si antes eran NOT NULL) + $table->string('sku')->nullable(false)->change(); + $table->integer('stock')->nullable(false)->change(); + }); + + Schema::rename('catalog_items', 'inventories'); + } +}; diff --git a/database/migrations/2026_01_22_114546_create_settings_global_table.php b/database/migrations/2026_01_22_114546_create_settings_global_table.php new file mode 100644 index 0000000..316c9e2 --- /dev/null +++ b/database/migrations/2026_01_22_114546_create_settings_global_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('business_name', 255); + $table->string('logo_path', 255)->nullable(); + $table->json('contact_info')->nullable(); + $table->json('invoice')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('settings_global'); + } +};