Descripción General
El sistema permite gestionar tanto clientes como proveedores, manteniendo información de contacto, ubicación y vinculación con transacciones (ventas y compras).Gestión de Clientes
Listar Clientes
Obtiene todos los clientes con información agregada de ventas.public function index(Request $request)
{
try {
$query = Cliente::with('empresa:id_empresa,comercial,ruc')
->select(
'id_cliente',
'documento',
'datos',
'direccion',
'direccion2',
'telefono',
'telefono2',
'email',
'id_empresa',
'ubigeo',
'departamento',
'provincia',
'distrito',
'created_at',
'updated_at'
)
->addSelect([
'ultima_venta' => DB::table('ventas')
->selectRaw('MAX(fecha_emision)')
->whereColumn('ventas.id_cliente', 'clientes.id_cliente')
->whereNotIn('ventas.estado', ['2', 'A']),
'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);
}
// Buscar si se proporciona término de búsqueda
if ($request->has('search')) {
$query->search($request->search);
}
$clientes = $query->orderBy('created_at', 'desc')->get();
return response()->json([
'success' => true,
'data' => $clientes
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error al obtener clientes',
], 500);
}
}
Datos AgregadosEl listado incluye:
ultima_venta: Fecha de la última venta realizada al clientetotal_venta: Suma total de todas las ventas (excluyendo anuladas)
Crear Cliente
public function store(Request $request)
{
try {
$validated = $request->validate([
'documento' => [
'required',
'string',
'max:11',
Rule::unique('clientes')->where(function ($query) use ($request) {
return $query->where('id_empresa', $request->id_empresa);
})
],
'datos' => 'required|string|max:245',
'direccion' => 'nullable|string|max:245',
'direccion2' => 'nullable|string|max:220',
'telefono' => 'nullable|string|max:200',
'telefono2' => 'nullable|string|max:200',
'email' => 'nullable|email|max:200',
'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',
]);
$cliente = Cliente::create($validated);
return response()->json([
'success' => true,
'message' => 'Cliente creado exitosamente',
'data' => $cliente->load('empresa:id_empresa,comercial,ruc')
], 201);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error al crear cliente',
], 500);
}
}
Unicidad de DocumentoEl
documento debe ser único por empresa. No se pueden tener dos clientes con el mismo documento en la misma empresa.Buscar Cliente por Documento
public function buscarPorDocumento(Request $request)
{
try {
$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
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error al buscar cliente',
], 500);
}
}
Eliminar Cliente
Validación de integridad referencial antes de eliminar.public function destroy($id)
{
try {
$cliente = Cliente::findOrFail($id);
if ($cliente->ventas()->exists()) {
return response()->json([
'success' => false,
'message' => 'No se puede eliminar el cliente porque tiene ventas asociadas'
], 409);
}
$cliente->delete();
return response()->json([
'success' => true,
'message' => 'Cliente eliminado exitosamente'
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error al eliminar cliente',
], 500);
}
}
No se puede eliminar un cliente que tenga ventas asociadas. Esto previene pérdida de integridad de datos.
Modelo Cliente
protected $table = 'clientes';
protected $primaryKey = 'id_cliente';
protected $fillable = [
'documento',
'datos',
'direccion',
'direccion2',
'telefono',
'telefono2',
'email',
'id_empresa',
'ultima_venta',
'total_venta',
'ubigeo',
'departamento',
'provincia',
'distrito',
];
protected $casts = [
'ultima_venta' => 'datetime',
'total_venta' => 'decimal:2',
];
Relaciones del Cliente
public function empresa()
{
return $this->belongsTo(Empresa::class, 'id_empresa', 'id_empresa');
}
public function ventas()
{
return $this->hasMany(Venta::class, 'id_cliente', 'id_cliente');
}
Scopes del Cliente
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}%")
->orWhere('telefono', 'like', "%{$search}%");
});
}
public function scopeByEmpresa($query, $empresaId)
{
return $query->where('id_empresa', $empresaId);
}
Gestión de Proveedores
Listar Proveedores
public function index(Request $request)
{
try {
$user = $request->user();
$busqueda = $request->get('busqueda', '');
$query = Proveedor::where('id_empresa', $user->id_empresa)
->where('estado', 1);
if (!empty($busqueda)) {
$query->buscar($busqueda);
}
$proveedores = $query->orderBy('razon_social', 'asc')->get();
return response()->json([
'success' => true,
'data' => $proveedores
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error al obtener proveedores: ' . $e->getMessage()
], 500);
}
}
Crear Proveedor
public function store(Request $request)
{
try {
$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',
]);
$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);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error al crear proveedor: ' . $e->getMessage()
], 500);
}
}
Los proveedores usan RUC (11 dígitos) como identificación fiscal, ya que solo pueden ser empresas formales.
Buscar Proveedor por RUC
public function buscarPorRuc(Request $request)
{
try {
$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'
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error al buscar proveedor: ' . $e->getMessage()
], 500);
}
}
Eliminar Proveedor (Soft Delete)
public function destroy($id)
{
try {
$proveedor = Proveedor::findOrFail($id);
$proveedor->update(['estado' => 0]);
return response()->json([
'success' => true,
'message' => 'Proveedor eliminado exitosamente'
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error al eliminar proveedor: ' . $e->getMessage()
], 500);
}
}
La eliminación de proveedores es un soft delete: solo cambia
estado = 0 en lugar de eliminar el registro.Estadísticas de Proveedores
public function estadisticas(Request $request)
{
try {
$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,
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => true,
'data' => [
'total' => 0,
'con_compras' => 0,
'sin_compras' => 0,
]
]);
}
}
Detalles Completos de Proveedor
Incluye información de compras asociadas.public function getDetalles(Request $request, $id)
{
try {
$user = $request->user();
$proveedor = Proveedor::where('id_empresa', $user->id_empresa)
->findOrFail($id);
// Estadísticas básicas
$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
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Error al obtener detalles: ' . $e->getMessage()
], 500);
}
}
Modelo Proveedor
protected $table = 'proveedores';
protected $primaryKey = 'proveedor_id';
protected $fillable = [
'ruc',
'razon_social',
'direccion',
'telefono',
'email',
'id_empresa',
'departamento',
'provincia',
'distrito',
'ubigeo',
'estado',
];
protected $casts = [
'estado' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
Relaciones del Proveedor
public function empresa()
{
return $this->belongsTo(Empresa::class, 'id_empresa', 'id_empresa');
}
public function compras()
{
return $this->hasMany(Compra::class, 'proveedor_id', 'proveedor_id');
}
Scopes del Proveedor
public function scopeActivos($query)
{
return $query->where('estado', 1);
}
public function scopeBuscar($query, $termino)
{
return $query->where(function($q) use ($termino) {
$q->where('ruc', 'like', "%$termino%")
->orWhere('razon_social', 'like', "%$termino%")
->orWhere('direccion', 'like', "%$termino%")
->orWhere('telefono', 'like', "%$termino%")
->orWhere('email', 'like', "%$termino%");
});
}
Atributos Computados
public function getUbicacionCompletaAttribute()
{
$partes = array_filter([
$this->distrito,
$this->provincia,
$this->departamento
]);
return !empty($partes) ? implode(', ', $partes) : null;
}
public function getNombreCortoAttribute()
{
return strlen($this->razon_social) > 50
? substr($this->razon_social, 0, 47) . '...'
: $this->razon_social;
}
Ubicación Geográfica
Tanto clientes como proveedores almacenan información de ubicación:Ubigeo
Código de 6 dígitos según catálogo INEIIdentifica: Departamento + Provincia + Distrito
Campos Separados
departamento, provincia, distritoNombres legibles de la ubicaciónDiferencias Clave
Clientes vs Proveedores
Clientes vs Proveedores