Skip to main content

Clientes y Proveedores

Módulo centralizado para gestionar la información de clientes y proveedores, con búsqueda por documento, consulta de historial de transacciones y estadísticas.

Clientes

Características Principales

Registro Completo

Datos personales, contacto, dirección y ubicación geográfica

Historial de Ventas

Consulta todas las ventas realizadas al cliente

Búsqueda Rápida

Encuentra clientes por documento, nombre o correo

Foto del Cliente

Sube imagen de referencia

Estructura de Cliente

  • Tipo de documento: DNI (1), RUC (6), Carnet de extranjería (4)
  • Número de documento: 8 dígitos (DNI), 11 (RUC)
  • Nombre/Razón social: Datos del cliente
  • Teléfono principal
  • Teléfono secundario
  • Email: Para envío de comprobantes
  • Dirección principal
  • Dirección secundaria
  • Ubigeo: Código INEI
  • Departamento, Provincia, Distrito
Imagen de perfil o logo empresarial (JPEG, PNG, máx 2MB)

Implementación Backend

// app/Http/Controllers/Api/ClienteController.php

class ClienteController extends Controller
{
    public function index(Request $request)
    {
        $query = Cliente::with('empresa:id_empresa,comercial,ruc')
            ->select(
                'id_cliente', 'documento', 'datos', 'direccion',
                'telefono', 'email', 'foto_url', 'id_empresa',
                'ubigeo', 'departamento', 'provincia', 'distrito'
            )
            ->addSelect([
                // Última fecha de venta
                'ultima_venta' => DB::table('ventas')
                    ->selectRaw('MAX(fecha_emision)')
                    ->whereColumn('ventas.id_cliente', 'clientes.id_cliente')
                    ->whereNotIn('ventas.estado', ['2', 'A']),
                
                // Total vendido
                'total_venta' => DB::table('ventas')
                    ->selectRaw('COALESCE(SUM(total), 0)')
                    ->whereColumn('ventas.id_cliente', 'clientes.id_cliente')
                    ->whereNotIn('ventas.estado', ['2', 'A']),
            ]);

        // Filtrar por empresa del usuario
        $user = $request->user();
        if ($user && $user->id_empresa) {
            $query->byEmpresa($user->id_empresa);
        }

        // Búsqueda
        if ($request->has('search')) {
            $query->search($request->search);
        }

        $clientes = $query->orderBy('created_at', 'desc')->get();

        return response()->json([
            'success' => true,
            'data' => $clientes
        ]);
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'documento' => [
                'required', 'string', 'max:15',
                Rule::unique('clientes')->where(function ($query) use ($request) {
                    return $query->where('id_empresa', $request->id_empresa);
                })
            ],
            'tipo_doc' => 'nullable|string|max:1',
            'datos' => 'required|string|max:245',
            'direccion' => 'nullable|string|max:245',
            'telefono' => 'nullable|string|max:200',
            'email' => 'nullable|email|max:200',
            'foto' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
            'id_empresa' => 'required|exists:empresas,id_empresa',
            'ubigeo' => 'nullable|string|max:6',
            'departamento' => 'nullable|string|max:100',
            'provincia' => 'nullable|string|max:100',
            'distrito' => 'nullable|string|max:100',
        ]);

        // Auto-detectar tipo de documento
        if (empty($validated['tipo_doc']) && !empty($validated['documento'])) {
            $doc = $validated['documento'];
            $validated['tipo_doc'] = strlen($doc) === 11 ? '6' : 
                                     (strlen($doc) === 8 ? '1' : '4');
        }

        // Manejar foto
        if ($request->hasFile('foto')) {
            $foto = $request->file('foto');
            $nombreFoto = 'cliente_' . time() . '.' . $foto->getClientOriginalExtension();
            $ruta = $foto->storeAs('clientes', $nombreFoto, 'public');
            $validated['foto_url'] = '/storage/' . $ruta;
        }

        $cliente = Cliente::create($validated);

        return response()->json([
            'success' => true,
            'message' => 'Cliente creado exitosamente',
            'data' => $cliente->load('empresa:id_empresa,comercial,ruc')
        ], 201);
    }

    public function buscarPorDocumento(Request $request)
    {
        $request->validate([
            'documento' => 'required|string',
            'empresa_id' => 'required|exists:empresas,id_empresa'
        ]);

        $cliente = Cliente::where('documento', $request->documento)
            ->where('id_empresa', $request->empresa_id)
            ->with('empresa:id_empresa,comercial,ruc')
            ->first();

        if (!$cliente) {
            return response()->json([
                'success' => false,
                'message' => 'Cliente no encontrado'
            ], 404);
        }

        return response()->json([
            'success' => true,
            'data' => $cliente
        ]);
    }
}

Modelo: Cliente

// app/Models/Cliente.php

class Cliente extends Model
{
    protected $table = 'clientes';
    protected $primaryKey = 'id_cliente';

    protected $fillable = [
        'documento', 'tipo_doc', 'datos',
        'direccion', 'direccion2',
        'telefono', 'telefono2', 'email',
        'foto_url', 'id_empresa',
        'ubigeo', 'departamento', 'provincia', 'distrito'
    ];

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

    public function ventas()
    {
        return $this->hasMany(Venta::class, 'id_cliente');
    }

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

    public function scopeSearch($query, $search)
    {
        return $query->where(function($q) use ($search) {
            $q->where('documento', 'like', "%{$search}%")
              ->orWhere('datos', 'like', "%{$search}%")
              ->orWhere('email', 'like', "%{$search}%");
        });
    }
}

Validación de Documento Único

El documento debe ser único por empresa:
Rule::unique('clientes')->where(function ($query) use ($request) {
    return $query->where('id_empresa', $request->id_empresa);
})
Esto permite que empresas diferentes tengan clientes con el mismo documento.

Proveedores

Características Principales

Catálogo de Proveedores

Registro completo con RUC, razón social y contacto

Historial de Compras

Consulta todas las compras realizadas al proveedor

Estadísticas

Total de compras en PEN y USD

Búsqueda por RUC

Encuentra rápidamente proveedores

Estructura de Proveedor

  • RUC: Número de 11 dígitos
  • Razón social: Nombre empresarial
  • Dirección
  • Teléfono
  • Email
  • Ubigeo: Código INEI
  • Departamento
  • Provincia
  • Distrito

Implementación Backend

// app/Http/Controllers/ProveedorController.php

class ProveedorController extends Controller
{
    public function index(Request $request)
    {
        $user = $request->user();
        $busqueda = $request->get('busqueda', '');
        
        $query = Proveedor::where('id_empresa', $user->id_empresa)
            ->where('estado', 1);
        
        // Aplicar búsqueda
        if (!empty($busqueda)) {
            $query->buscar($busqueda);
        }
        
        $proveedores = $query->orderBy('razon_social', 'asc')->get();
        
        return response()->json([
            'success' => true,
            'data' => $proveedores
        ]);
    }

    public function store(Request $request)
    {
        $user = $request->user();
        
        $validator = Validator::make($request->all(), [
            'ruc' => [
                'required', 'string', 'max:11',
                Rule::unique('proveedores', 'ruc')
                    ->where('id_empresa', $user->id_empresa),
            ],
            'razon_social' => 'required|string|max:200',
            'direccion' => 'nullable|string|max:100',
            'telefono' => 'nullable|string|max:100',
            'email' => 'nullable|email|max:150',
            'departamento' => 'nullable|string|max:100',
            'provincia' => 'nullable|string|max:100',
            'distrito' => 'nullable|string|max:100',
            'ubigeo' => 'nullable|string|max:6',
        ]);

        if ($validator->fails()) {
            return response()->json([
                'success' => false,
                'errors' => $validator->errors()
            ], 422);
        }

        $data = $request->all();
        $data['id_empresa'] = $user->id_empresa;
        $data['estado'] = 1;
        
        $proveedor = Proveedor::create($data);
        
        return response()->json([
            'success' => true,
            'message' => 'Proveedor creado exitosamente',
            'data' => $proveedor
        ], 201);
    }

    public function buscarPorRuc(Request $request)
    {
        $ruc = $request->get('ruc');
        $user = $request->user();
        
        if (empty($ruc)) {
            return response()->json([
                'success' => false,
                'message' => 'RUC es requerido'
            ], 422);
        }
        
        $proveedor = Proveedor::where('ruc', $ruc)
            ->where('id_empresa', $user->id_empresa)
            ->where('estado', 1)
            ->first();
        
        if ($proveedor) {
            return response()->json([
                'success' => true,
                'data' => $proveedor,
                'existe' => true
            ]);
        }
        
        return response()->json([
            'success' => true,
            'data' => null,
            'existe' => false,
            'message' => 'Proveedor no encontrado'
        ]);
    }

    public function getDetalles(Request $request, $id)
    {
        $user = $request->user();
        $proveedor = Proveedor::where('id_empresa', $user->id_empresa)
            ->findOrFail($id);
        
        // Estadísticas
        $totalComprasCount = $proveedor->compras()->count();
        
        $montoTotalPEN = $proveedor->compras()
            ->where('moneda', 'PEN')
            ->sum('total');
            
        $montoTotalUSD = $proveedor->compras()
            ->where('moneda', 'USD')
            ->sum('total');
        
        // Últimas 10 compras
        $ultimasCompras = $proveedor->compras()
            ->orderBy('fecha_emision', 'desc')
            ->take(10)
            ->get();
        
        return response()->json([
            'success' => true,
            'data' => [
                'proveedor' => $proveedor,
                'stats' => [
                    'total_count' => $totalComprasCount,
                    'total_monto_pen' => $montoTotalPEN,
                    'total_monto_usd' => $montoTotalUSD,
                ],
                'compras' => $ultimasCompras
            ]
        ]);
    }

    public function estadisticas(Request $request)
    {
        $user = $request->user();
        
        $total = Proveedor::where('id_empresa', $user->id_empresa)
            ->where('estado', 1)
            ->count();
        
        $conCompras = DB::table('proveedores as p')
            ->join('compras as c', 'p.proveedor_id', '=', 'c.proveedor_id')
            ->where('p.id_empresa', $user->id_empresa)
            ->where('p.estado', 1)
            ->distinct('p.proveedor_id')
            ->count();
        
        $sinCompras = $total - $conCompras;
        
        return response()->json([
            'success' => true,
            'data' => [
                'total' => $total,
                'con_compras' => $conCompras,
                'sin_compras' => $sinCompras,
            ]
        ]);
    }
}

Modelo: Proveedor

// app/Models/Proveedor.php

class Proveedor extends Model
{
    protected $table = 'proveedores';
    protected $primaryKey = 'proveedor_id';

    protected $fillable = [
        'ruc', 'razon_social',
        'direccion', 'telefono', 'email',
        'ubigeo', 'departamento', 'provincia', 'distrito',
        'id_empresa', 'estado'
    ];

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

    public function compras()
    {
        return $this->hasMany(Compra::class, 'id_proveedor', 'proveedor_id');
    }

    // Scopes
    public function scopeBuscar($query, $termino)
    {
        return $query->where(function($q) use ($termino) {
            $q->where('ruc', 'like', "%{$termino}%")
              ->orWhere('razon_social', 'like', "%{$termino}%")
              ->orWhere('email', 'like', "%{$termino}%");
        });
    }
}

Tablas de Base de Datos

Clientes

CampoTipoDescripción
id_clienteINTPK autoincremental
documentoVARCHAR(15)DNI, RUC o CE
tipo_docCHAR(1)1=DNI, 6=RUC, 4=CE
datosVARCHAR(245)Nombre o razón social
direccionVARCHAR(245)Dirección principal
direccion2VARCHAR(220)Dirección secundaria
telefonoVARCHAR(200)Teléfono 1
telefono2VARCHAR(200)Teléfono 2
emailVARCHAR(200)Correo electrónico
foto_urlVARCHAR(255)Ruta de la foto
ubigeoVARCHAR(6)Código INEI
departamentoVARCHAR(100)Departamento
provinciaVARCHAR(100)Provincia
distritoVARCHAR(100)Distrito
id_empresaINTFK a empresas

Proveedores

CampoTipoDescripción
proveedor_idINTPK autoincremental
rucVARCHAR(11)RUC del proveedor
razon_socialVARCHAR(200)Nombre empresarial
direccionVARCHAR(100)Dirección
telefonoVARCHAR(100)Teléfono
emailVARCHAR(150)Correo
ubigeoVARCHAR(6)Código INEI
departamentoVARCHAR(100)Departamento
provinciaVARCHAR(100)Provincia
distritoVARCHAR(100)Distrito
id_empresaINTFK a empresas
estadoINT1=Activo, 0=Inactivo

Endpoints API

Clientes

Listar Clientes

GET /api/clientes?search=juan

Ver Cliente

GET /api/clientes/:id

Crear Cliente

POST /api/clientes

Actualizar

PUT /api/clientes/:id

Eliminar

DELETE /api/clientes/:id

Buscar por Documento

POST /api/clientes/buscar-documento

Proveedores

Listar Proveedores

GET /api/proveedores?busqueda=empresa

Ver Proveedor

GET /api/proveedores/:id

Crear Proveedor

POST /api/proveedores

Actualizar

PUT /api/proveedores/:id

Buscar por RUC

GET /api/proveedores/buscar-ruc?ruc=20123456789

Estadísticas

GET /api/proveedores/estadisticas

Buenas Prácticas

Recomendación: Usa la consulta RENIEC/SUNAT para autocompletar datos del cliente/proveedor al ingresar el documento.
No elimines clientes con ventas asociadas ni proveedores con compras. Usa el estado “Inactivo” en su lugar.
La foto del cliente se almacena en storage/app/public/clientes/ y es accesible vía /storage/clientes/.

Integración con Otros Módulos

1

Facturación

Al crear una venta, se busca o crea el cliente automáticamente
2

Compras

Al registrar una compra, se vincula al proveedor
3

Cuentas por Cobrar

Las ventas a crédito generan obligaciones del cliente
4

Cuentas por Pagar

Las compras a crédito generan deudas con el proveedor

Próximos Pasos

Facturación

Emite comprobantes a clientes

Compras

Registra compras a proveedores

Cuentas por Cobrar

Gestión de cobranzas

Cuentas por Pagar

Gestión de pagos a proveedores

Build docs developers (and LLMs) love