Skip to main content

Descripción General

El módulo de inventario permite gestionar productos con control de stock en múltiples almacenes, seguimiento de movimientos y soporte para diferentes precios y unidades de medida.

Gestión de Productos

Listar Productos

Filtra productos por empresa, almacén y búsqueda de texto.
public function index(Request $request)
{
    try {
        $user = $request->user();
        
        // Obtener empresa activa del header o empresa del usuario
        $idEmpresa = $user->id_empresa;
        if ($request->header('X-Empresa-Activa')) {
            $idEmpresa = $request->header('X-Empresa-Activa');
        }
        
        $productos = $this->productoService->listar(
            $idEmpresa,
            $request->get('almacen', '1'),
            $request->get('search'),
            $request->boolean('solo_con_stock', false)
        );

        return response()->json(['success' => true, 'data' => $productos]);
    } catch (\Exception $e) {
        return response()->json([
            'success' => false,
            'message' => 'Error al obtener productos: ' . $e->getMessage()
        ], 500);
    }
}
Empresa ActivaEl sistema soporta multi-empresa. Se puede especificar la empresa activa mediante:
  • Header HTTP: X-Empresa-Activa: {id_empresa}
  • Por defecto usa $user->id_empresa

Crear Producto

Crea un producto con opción de imagen y sincronización entre almacenes.
public function store(ProductoRequest $request)
{
    try {
        $user = $request->user();
        $data = $request->validated();

        $idEmpresa = $user->id_empresa;
        if ($request->header('X-Empresa-Activa')) {
            $idEmpresa = $request->header('X-Empresa-Activa');
        }

        if ($request->hasFile('imagen')) {
            $data['imagen'] = $this->productoService->subirImagen($request->file('imagen'));
        }

        $producto = $this->productoService->crear($data, $idEmpresa);

        return response()->json([
            'success' => true,
            'message' => 'Producto creado exitosamente en ambos almacenes',
            'data' => $producto,
        ], 201);
    } catch (\Exception $e) {
        return response()->json([
            'success' => false,
            'message' => 'Error al crear producto: ' . $e->getMessage()
        ], 500);
    }
}
Creación en Ambos AlmacenesAl crear un producto, se genera automáticamente en ambos almacenes (virtual y real) con el mismo código para facilitar la sincronización.

Actualizar Producto

public function update(ProductoRequest $request, $id)
{
    try {
        $producto = Producto::findOrFail($id);
        $data = $request->except(['imagen']);

        if ($request->hasFile('imagen')) {
            $data['imagen'] = $this->productoService->subirImagen(
                $request->file('imagen'),
                $producto->imagen
            );
        }

        $resultado = $this->productoService->actualizar($producto, $data);

        return response()->json([
            'success' => true,
            'message' => 'Producto actualizado exitosamente' . ($resultado['sincronizado'] ? ' (sincronizado en ambos almacenes)' : ''),
            'data' => $resultado['producto'],
            'sincronizado' => $resultado['sincronizado'],
        ]);
    } catch (\Exception $e) {
        return response()->json([
            'success' => false,
            'message' => 'Error al actualizar producto: ' . $e->getMessage()
        ], 500);
    }
}

Modelo de Datos

Producto Model

protected $table = 'productos';
protected $primaryKey = 'id_producto';

protected $fillable = [
    'codigo',
    'cod_barra',
    'nombre',
    'descripcion',
    'precio',
    'costo',
    'precio_mayor',
    'precio_menor',
    'precio_unidad',
    'cantidad',
    'stock_minimo',
    'stock_maximo',
    'id_empresa',
    'categoria_id',
    'unidad_id',
    'almacen',
    'codsunat',
    'usar_barra',
    'usar_multiprecio',
    'moneda',
    'estado',
    'imagen',
    'ultima_salida',
    'fecha_ultimo_ingreso',
];

protected $casts = [
    'precio' => 'decimal:2',
    'costo' => 'decimal:2',
    'precio_mayor' => 'decimal:2',
    'precio_menor' => 'decimal:2',
    'precio_unidad' => 'decimal:2',
    'cantidad' => 'integer',
    'stock_minimo' => 'integer',
    'stock_maximo' => 'integer',
    'ultima_salida' => 'date',
    'fecha_ultimo_ingreso' => 'datetime',
];

Relaciones

public function empresa()
{
    return $this->belongsTo(Empresa::class, 'id_empresa', 'id_empresa');
}

public function categoria()
{
    return $this->belongsTo(Categoria::class, 'categoria_id');
}

public function unidad()
{
    return $this->belongsTo(Unidad::class, 'unidad_id');
}

// Scopes
public function scopeAlmacen($query, $almacen)
{
    return $query->where('almacen', $almacen);
}

public function scopeActivo($query)
{
    return $query->where('estado', '1');
}

public function scopeEmpresa($query, $idEmpresa)
{
    return $query->where('id_empresa', $idEmpresa);
}

Sistema de Almacenes

El sistema maneja dos almacenes principales:

Almacén 1 - Virtual

Stock virtual o de exhibiciónUsado para ventas y control interno

Almacén 2 - Real

Stock físico realInventario real en bodega

Movimientos de Stock

Modelo MovimientoStock

protected $table = 'movimientos_stock';
protected $primaryKey = 'id_movimiento';

protected $fillable = [
    'id_producto',
    'tipo_movimiento',
    'cantidad',
    'stock_anterior',
    'stock_nuevo',
    'tipo_documento',
    'id_documento',
    'documento_referencia',
    'motivo',
    'observaciones',
    'id_almacen',
    'id_empresa',
    'id_usuario',
    'fecha_movimiento'
];

protected $casts = [
    'cantidad' => 'decimal:2',
    'stock_anterior' => 'decimal:2',
    'stock_nuevo' => 'decimal:2',
    'fecha_movimiento' => 'datetime',
];

Tipos de Movimiento

Aumenta el stock
  • Compras
  • Ajustes de inventario positivos
  • Devoluciones de clientes
  • Anulación de ventas

Registro Automático de Movimientos

Cada operación que afecta stock genera un movimiento:
if ($afectaStock) {
    $productoModel = Producto::find($producto['id_producto']);
    if ($productoModel) {
        $stockAnterior = $productoModel->cantidad;
        $productoModel->decrement('cantidad', $producto['cantidad']);
        $stockNuevo = $stockAnterior - $producto['cantidad'];

        MovimientoStock::create([
            'id_producto' => $productoModel->id_producto,
            'tipo_movimiento' => 'salida',
            'cantidad' => $producto['cantidad'],
            'stock_anterior' => $stockAnterior,
            'stock_nuevo' => $stockNuevo,
            'tipo_documento' => 'venta',
            'id_documento' => $venta->id_venta,
            'documento_referencia' => $venta->serie . '-' . str_pad($venta->numero, 6, '0', STR_PAD_LEFT),
            'motivo' => 'Venta realizada',
            'id_almacen' => $productoModel->almacen,
            'id_empresa' => $user->id_empresa,
            'id_usuario' => $user->id,
            'fecha_movimiento' => now(),
        ]);
    }
}

Precios Múltiples

Cada producto puede tener diferentes niveles de precio:

Precio Regular

precioPrecio de venta al público

Precio Mayor

precio_mayorPrecio para venta al por mayor

Precio Menor

precio_menorPrecio con descuento o promoción

Precio Unidad

precio_unidadPrecio por unidad individual
Activar usar_multiprecio = true para habilitar la selección de precio en ventas.

Código de Barras

'cod_barra',
'usar_barra',
Si usar_barra = true, el sistema puede buscar productos por código de barras en las ventas.

Categorías y Unidades

Categorías

Permiten organizar productos en grupos:
  • Electrónicos
  • Alimentos
  • Textiles
  • etc.

Unidades de Medida

Basado en catálogo SUNAT:
  • NIU: Unidad
  • KGM: Kilogramo
  • MTR: Metro
  • LTR: Litro
  • etc.

Control de Stock

Stock Mínimo y Máximo

'stock_minimo',
'stock_maximo',
Estos campos permiten:
  • Alertas de stock bajo
  • Control de reabastecimiento
  • Optimización de inventario
El sistema puede configurarse para alertar cuando cantidad < stock_minimo.

Imagen de Producto

Soporte para imágenes de productos:
if ($request->hasFile('imagen')) {
    $data['imagen'] = $this->productoService->subirImagen($request->file('imagen'));
}
Las imágenes deben ser:
  • Formato: JPEG, PNG, JPG, WEBP
  • Tamaño máximo: 2MB

Moneda

Los productos pueden tener precios en:
  • PEN: Soles peruanos
  • USD: Dólares americanos

Código SUNAT

Campo codsunat para el código de producto según catálogo SUNAT (opcional).

Filtros Disponibles

Por Almacén

$request->get('almacen', '1')

Por Búsqueda

Busca en:
  • Código
  • Código de barras
  • Nombre
  • Descripción

Solo con Stock

$request->boolean('solo_con_stock', false)
Filtra productos con cantidad > 0.

Scopes Útiles

public function scopeAlmacen($query, $almacen)
{
    return $query->where('almacen', $almacen);
}

public function scopeActivo($query)
{
    return $query->where('estado', '1');
}

public function scopeEmpresa($query, $idEmpresa)
{
    return $query->where('id_empresa', $idEmpresa);
}

Ejemplo de Uso

// Productos activos del almacén 1
$productos = Producto::activo()
    ->almacen('1')
    ->empresa($idEmpresa)
    ->get();

// Productos con stock
$conStock = Producto::where('cantidad', '>', 0)
    ->activo()
    ->get();

Fechas de Control

Última Salida

ultima_salidaFecha de la última venta del producto

Último Ingreso

fecha_ultimo_ingresoFecha del último ingreso por compra
Estas fechas se actualizan automáticamente en cada movimiento.

Build docs developers (and LLMs) love