Este post es la segunda parte del sistema de ventas con Laravel. Al final decidí agregarle dos cosas más:
- Clientes
- Usuarios
Eso trajo cambios que en conjunto con lo demás son:
- Dar la posibilidad de elegir el cliente que hace la venta
- El nombre del cliente aparece en el ticket
- Se pueden gestionar usuarios para el uso del sistema
Veamos cómo quedó todo.
Cambiando página de inicio
He modificado el diseño de la página de inicio del sistema de ventas. Ahora utilizo cards, y el layout se genera dinámicamente según un arreglo.
De este modo organizo mejor el diseño en distintas pantallas. El código que lo genera es el siguiente:
@extends('maestra')
@section("titulo", "Inicio")
@section('contenido')
<div class="col-12 text-center">
<h1>Bienvenido, {{Auth::user()->name}}</h1>
</div>
@foreach([
["productos", "ventas", "vender", "clientes"],
["usuarios", "acerca_de", "soporte"]
] as $modulos)
<div class="col-12 pb-2">
<div class="row">
@foreach($modulos as $modulo)
<div class="col-12 col-md-3">
<div class="card">
<img class="card-img-top" src="{{url("/img/$modulo.png")}}">
<div class="card-body">
<h5 class="card-title">
{{$modulo === "acerca_de" ? "Acerca de" : ucwords($modulo)}}
</h5>
<a href="{{route("$modulo.index")}}" class="btn btn-success">
Ir a {{$modulo === "acerca_de" ? "Acerca de" : ucwords($modulo)}}
<i class="fa fa-arrow-right"></i>
</a>
</div>
</div>
</div>
@endforeach
</div>
</div>
@endforeach
@endsection
Primero tengo un arreglo que tiene dos arreglos; por cada uno muestro una nueva fila. Después hago un ciclo dentro del anterior, por cada módulo. Utilizo el nombre para cargar las imágenes y poner el título, así como las rutas.
La única excepción es con acerca_de
que no se ve legible para el ser humano al convertirla en mayúsculas, por eso es que he usado el operador ternario para mostrar su valor.
Usuarios
He agregado el módulo de usuarios, que es un simple CRUD. Comenzamos viendo su controlador:
<?php
namespace App\Http\Controllers;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class UserController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view("usuarios.usuarios_index", ["usuarios" => User::all()]);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view("usuarios.usuarios_create");
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$usuario = new User($request->input());
$usuario->password = Hash::make($usuario->password);
$usuario->saveOrFail();
return redirect()->route("usuarios.index")->with("mensaje", "Usuario guardado");
}
/**
* Display the specified resource.
*
* @param \App\User $user
* @return \Illuminate\Http\Response
*/
public function show(User $user)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param \App\User $user
* @return \Illuminate\Http\Response
*/
public function edit(User $user)
{
$user->password = "";
return view("usuarios.usuarios_edit", ["usuario" => $user,
]);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\User $user
* @return \Illuminate\Http\Response
*/
public function update(Request $request, User $user)
{
$user->fill($request->input());
$user->password = Hash::make($user->password);
$user->saveOrFail();
return redirect()->route("usuarios.index")->with("mensaje", "Usuario actualizado");
}
/**
* Remove the specified resource from storage.
*
* @param \App\User $user
* @return \Illuminate\Http\Response
*/
public function destroy(User $user)
{
$user->delete();
return redirect()->route("usuarios.index")->with("mensaje", "Usuario eliminado");
}
}
Es parecido a los otros, pues se usa un simple resource. Por cierto, no estoy validando que la contraseña coincida (al registrar o actualizar). La lista se ve así:
En este caso no tuve que generar el modelo, pues ya viene incluido al generar la autenticación de Laravel.
Clientes
También he agregado un módulo para clientes, primero veamos el modelo que realmente solo cuenta con teléfono y nombre:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Cliente extends Model
{
protected $fillable = ["nombre", "telefono"];
}
Luego generé su migración:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateClientesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('clientes', function (Blueprint $table) {
$table->id();
$table->string("nombre");
$table->string("telefono");
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('clientes');
}
}
No me molesté en quitar los timestamps, aunque realmente no los estoy usando en ningún otro lugar. Finalmente veamos el controlador:
<?php
namespace App\Http\Controllers;
use App\Cliente;
use Illuminate\Http\Request;
class ClientesController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view("clientes.clientes_index", ["clientes" => Cliente::all()]);
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return view("clientes.clientes_create");
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
(new Cliente($request->input()))->saveOrFail();
return redirect()->route("clientes.index")->with("mensaje", "Cliente agregado");
}
/**
* Display the specified resource.
*
* @param \App\Cliente $cliente
* @return \Illuminate\Http\Response
*/
public function show(Cliente $cliente)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Cliente $cliente
* @return \Illuminate\Http\Response
*/
public function edit(Cliente $cliente)
{
return view("clientes.clientes_edit", ["cliente" => $cliente]);
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Cliente $cliente
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Cliente $cliente)
{
$cliente->fill($request->input());
$cliente->saveOrFail();
return redirect()->route("clientes.index")->with("mensaje", "Cliente actualizado");
}
/**
* Remove the specified resource from storage.
*
* @param \App\Cliente $cliente
* @return \Illuminate\Http\Response
*/
public function destroy(Cliente $cliente)
{
$cliente->delete();
return redirect()->route("clientes.index")->with("mensaje", "Cliente eliminado");
}
}
Las vistas no las muestro porque es muy repetitivo, solo colocaré el resultado:
Por cierto, se ve así porque estoy mostrando el resultado en un iPhone, pero el sistema se adapta a cualquier tamaño de pantalla.
Agregar cliente a venta
Tuve que hacer unas modificaciones a la base de datos ya que tenía que crear la relación entre el cliente y la venta. Para ello, generé una migración:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AgregarIdClienteVentas extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('ventas', function (Blueprint $table) {
$table->unsignedBigInteger('id_cliente');
$table->foreign("id_cliente")
->references("id")
->on("clientes")
->onDelete("cascade")
->onUpdate("cascade");
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('ventas', function (Blueprint $table) {
//
});
}
}
Ahora que lo veo, no agregué el código para revertir la migración, así que hay que tener cuidado con esa parte. Más tarde agregué la relación en el modelo de venta indicando que una venta le pertenece a un cliente:
<?php
public function cliente()
{
return $this->belongsTo("App\Cliente", "id_cliente");
}
Seleccionar cliente al vender
La parte más compleja de esto fue arreglar el diseño, ya que los botones de Terminar venta, Cancelar venta y Agregar producto apuntaban a distintas rutas; pero al agregar el cliente tuve que modificarlo.
Al final decidí que ambos botones de la venta apuntaran a la misma ruta y, dentro del controlador verificar cuál botón fue presionado e invocar a una función u otra:
<?php
public function terminarOCancelarVenta(Request $request)
{
if ($request->input("accion") == "terminar") {
return $this->terminarVenta($request);
} else {
return $this->cancelarVenta();
}
}
El código de la vista para vender quedó así:
<div class="row">
<div class="col-12 col-md-6">
<form action="{{route("terminarOCancelarVenta")}}" method="post">
@csrf
<div class="form-group">
<label for="id_cliente">Cliente</label>
<select required class="form-control" name="id_cliente" id="id_cliente">
@foreach($clientes as $cliente)
<option value="{{$cliente->id}}">{{$cliente->nombre}}</option>
@endforeach
</select>
</div>
@if(session("productos") !== null)
<div class="form-group">
<button name="accion" value="terminar" type="submit" class="btn btn-success">Terminar
venta
</button>
<button name="accion" value="cancelar" type="submit" class="btn btn-danger">Cancelar
venta
</button>
</div>
@endif
</form>
</div>
<div class="col-12 col-md-6">
<form action="{{route("agregarProductoVenta")}}" method="post">
@csrf
<div class="form-group">
<label for="codigo">Código de barras</label>
<input id="codigo" autocomplete="off" required autofocus name="codigo" type="text"
class="form-control"
placeholder="Código de barras">
</div>
</form>
</div>
</div>
Como ves, se dibuja un select que permite seleccionar al cliente. Una posible mejora sería que se quede el mismo cliente seleccionado, ya que si se selecciona antes de vender, el cambio se pierde pues al agregar un producto la página se refresca.
El resultado de la interfaz para agregar una venta con selección de cliente quedó así:
Detalle de venta con cliente
Del mismo modo, en el detalle de la venta (y también en el listado) aparece el nombre del cliente al que fue hecha la venta:
Ticket de venta con cliente
Para terminar el post debo mostrar que en el ticket también aparece el nombre del cliente. El código quedó así:
<?php
public function ticket(Request $request)
{
$venta = Venta::findOrFail($request->get("id"));
$nombreImpresora = env("NOMBRE_IMPRESORA");
$connector = new WindowsPrintConnector($nombreImpresora);
$impresora = new Printer($connector);
$impresora->setJustification(Printer::JUSTIFY_CENTER);
$impresora->setEmphasis(true);
$impresora->text("Ticket de venta\n");
$impresora->text($venta->created_at . "\n");
$impresora->setEmphasis(false);
$impresora->text("Cliente: ");
$impresora->text($venta->cliente->nombre . "\n");
$impresora->text("\nhttps://parzibyte.me/blog\n");
$impresora->text("\n===============================\n");
$total = 0;
foreach ($venta->productos as $producto) {
$subtotal = $producto->cantidad * $producto->precio;
$total += $subtotal;
$impresora->setJustification(Printer::JUSTIFY_LEFT);
$impresora->text(sprintf("%.2fx%s\n", $producto->cantidad, $producto->descripcion));
$impresora->setJustification(Printer::JUSTIFY_RIGHT);
$impresora->text('$' . number_format($subtotal, 2) . "\n");
}
$impresora->setJustification(Printer::JUSTIFY_CENTER);
$impresora->text("\n===============================\n");
$impresora->setJustification(Printer::JUSTIFY_RIGHT);
$impresora->setEmphasis(true);
$impresora->text("Total: $" . number_format($total, 2) . "\n");
$impresora->setJustification(Printer::JUSTIFY_CENTER);
$impresora->setTextSize(1, 1);
$impresora->text("Gracias por su compra\n");
$impresora->text("https://parzibyte.me/blog");
$impresora->feed(5);
$impresora->close();
return redirect()->back()->with("mensaje", "Ticket impreso");
}
Al imprimir se muestra el nombre del cliente que realizó la venta:
Creando primer usuario
Hay varias maneras de crear un usuario. Yo he probado aquí con Tkinter. Primero:
php artisan tinker
Luego:
$user = new App\User;
$user->name = "Parzibyte";
$user->email = "parzibyte@gmail.com";
$user->password = Hash::make("parzibyte");
$user->save();
Ahora inicia sesión con parzibyte@gmail.com
y la contraseña parzibyte
.
También puedes ejecutar la siguiente consulta SQL:
INSERT INTO `users` (`id`, `name`, `email`, `email_verified_at`, `password`, `remember_token`, `created_at`, `updated_at`) VALUES
(1, 'Parzibyte', 'parzibyte@gmail.com', NULL, '$2y$10$F.xZXrkwtyWeqZwGYgNcZuiDiT5PLlulESE91CvL40hUyhheS7NyG', NULL, '2024-09-19 19:22:45', '2024-09-19 19:22:45');
Conclusión
Recuerda que la primera parte de este tutorial está en un post anterior. En ese tutorial vas a encontrar la guía de instalación.
El código fuente lo encuentras en GitHub.
Más adelante traeré la aplicación móvil que corresponde a este sistema.
Hay un tutorial para le creacion de la apk para android?? o esta las fuentes en github tambien?
Así es, está en GitHub
Saludos 🙂
Probe el sistema en linea y siempre graba el primer cliente ademas que como lo comentas no se queda el mismo cliente seleccionado. Saludos muy bueno si tienes la mejora la podras compartir. Agradezco tu respuesta.
Hola, con gusto podemos arreglarlo. Más información aquí: https://parzibyte.me/blog/contrataciones-ayuda/
Saludos 🙂