Pequeño sistema de ventas con CodeIgniter y MVC

Introducción

Sistema de ventas MVC con PHP y CodeIgniter
Sistema de ventas MVC con PHP y CodeIgniter

Luego de mucho tiempo al fin traigo una versión 2 del sistema de ventas publicado anteriormente. No es un sistema de ventas como tal, pues es un simple ejemplo, pero no lo puedo llamar de otro modo.

En ese post prometí que haría otro tutorial pero siguiendo el patrón MVC y qué mejor que hacerlo con CodeIgniter (para repasar, ya que justo ahora tengo que hacer un proyecto usándolo)

Lo que trae esta versión no es nada diferente en cuanto a su uso, sino a su programación.

Antes de comenzar

Te invito a probar Sublime POS 3, un punto de venta evolutivo, gratuito y que funciona en la nube.

Vídeo relacionado

Si eres de los que prefieren ver un vídeo, mira la demo y la explicación en YT:

Cambios

Se hicieron muchos cambios. En primera porque siempre hay formas de mejorar el software. Lo que hicimos fue cambiar el tema CSS, además de ser más indicativos en cuanto a mensajes y avisos gracias a los datos temporales en sesión de CodeIgniter.

Por otro lado, se mejoró la base de datos. Se usa una tabla transaccional para los productos vendidos, eliminando la redundancia. Y para las ventas, ahora el total se calcula dependiendo de los productos vendidos.

También se agregó un apartado en donde se ven los detalles de una venta.

Compatibilidad

He puesto esfuerzo en que sea compatible con las versiones más antiguas de PHP. Por ejemplo, no utilicé funciones de tipado fuerte (así les llamo yo) ni definiciones cortas de arreglos.

Hablar es de mal gusto, muéstrame el código

Esta vez he alojado el código en GitHub. Es un repositorio público, tal vez le dé seguimiento más tarde, quién sabe. Pero aquí el link, déjale una estrella si te gusta 😉

https://github.com/parzibyte/ventasci

Se llama ventasci porque quiere decir “ventas con CodeIgniter” es decir, la ci es de CodeIgniter.

Puedes clonarlo si sabes manejar GitHub, o descargarlo como ZIP. Da igual, cualquiera puede usarlo y descargarlo; me he esforzado en hacerlo lo más expresivo posible.

En este post explicaré algunas cosas importantes, pero obviamente no pondré todo el código y configuraciones.

Lecturas recomendadas

Ya deberías saber de PHP y CodeIgniter. Si quieres una pequeña introducción a CodeIgniter haz click aquí.

Recuerda ver el sistema que creamos anteriormente, pues en ese nos basaremos y tal vez nos saltemos cosas básicas.

Por cierto, lee sobre los controladores y modelos, pues explicaré todo suponiendo que ya tienes una base.

Si quieres, explora todos los tutoriales de CodeIgniter que tengo publicados.

Pequeño, muy pequeño sistema de ventas con PHP y MVC usando CodeIgniter

Vamos a crear modelos, controladores y vistas para cada cosa. Pero antes de ello configuraremos nuestra base de datos.

Configuración de base de datos con MySQL

Vamos a usar PDO con MySQL, omitiré explicar la cadena de conexión y esas cosas, pues ya lo he hecho anteriormente.

La configuración de la base de datos queda así:

#<?php
$db['default'] = array(
	'dsn'	=> 'mysql:host=localhost;dbname=ventas',
	'hostname' => 'localhost',
	'username' => 'root',
	'password' => '',
	'database' => 'ventas',
	'dbdriver' => 'pdo',
	'dbprefix' => '',
	'pconnect' => FALSE,
	'db_debug' => (ENVIRONMENT !== 'production'),
	'cache_on' => FALSE,
	'cachedir' => '',
	'char_set' => 'utf8',
	'dbcollat' => 'utf8_general_ci',
	'swap_pre' => '',
	'encrypt' => FALSE,
	'compress' => FALSE,
	'stricton' => FALSE,
	'failover' => array(),
	'save_queries' => TRUE
);

El usuario es root y la contraseña es una string vacía. Si quieres ver todo el archivo, recuerda que está en GitHub.

La base de datos se llama ventas. El esquema, que puedes encontrar igualmente en el repositorio es el siguiente:

CREATE TABLE productos(
	id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
	codigo VARCHAR(255) NOT NULL,
	descripcion VARCHAR(255) NOT NULL,
	precioVenta DECIMAL(5, 2) NOT NULL,
	precioCompra DECIMAL(5, 2) NOT NULL,
	existencia DECIMAL(5, 2) NOT NULL,
	PRIMARY KEY(id)
) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8;
 
CREATE TABLE ventas(
	id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
	fecha DATETIME NOT NULL,
	PRIMARY KEY(id)
) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8;
 
CREATE TABLE productos_vendidos(
	id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
	id_producto BIGINT UNSIGNED NOT NULL,
	cantidad BIGINT UNSIGNED NOT NULL,
	precio DECIMAL(5, 2) NOT NULL,
	id_venta BIGINT UNSIGNED NOT NULL,
	PRIMARY KEY(id),
	FOREIGN KEY(id_producto) REFERENCES productos(id) ON DELETE CASCADE,
	FOREIGN KEY(id_venta) REFERENCES ventas(id) ON DELETE CASCADE
) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8;

Si cambias el nombre de algo, recuerda cambiarlo en el código. Pongo aquí el esquema como imagen porque se ve bonito.

Esquema de base de datos para sistema de ventas MVC con CodeIgniter
Esquema de base de datos para sistema de ventas MVC con CodeIgniter

Productos

Comenzamos con los productos. Vamos a registrarlos para después poder eliminarlos.

Modelo del producto

Definimos nuestro modelo, y definimos también las funciones que tendrá. Es un simple CRUD:

<?php
    class ProductoModel extends CI_Model{
        public $id;
        public $codigo;
        public $descripcion;
        public $precioVenta;
        public $precioCompra;
        public $existencia;
 
        public function __construct(){
            $this->load->database();
        }
 
        public function nuevo($codigo, $descripcion, $precioVenta, $precioCompra, $existencia){
            $this->codigo = $codigo;
            $this->descripcion = $descripcion;
            $this->precioVenta = $precioVenta;
            $this->precioCompra = $precioCompra;
            $this->existencia = $existencia;
            return $this->db->insert('productos', $this);
        }
 
        public function guardarCambios($id, $codigo, $descripcion, $precioVenta, $precioCompra, $existencia){
            $this->id = $id;
            $this->codigo = $codigo;
            $this->descripcion = $descripcion;
            $this->precioVenta = $precioVenta;
            $this->precioCompra = $precioCompra;
            $this->existencia = $existencia;
            return $this->db->update('productos', $this, array("id" => $id));
        }
 
        public function todos(){
            return $this->db->get("productos")->result();
        }
 
        public function eliminar($id){
            return $this->db->delete("productos", array("id" => $id));
        }
 
        public function uno($id){
            return $this->db->get_where("productos", array("id" => $id))->row();
        }
 
        public function porCodigoDeBarras($codigoDeBarras){
            return $this->db->get_where("productos", array("codigo" => $codigoDeBarras))->row();
        }
    }
?>

El método uno obtiene un producto a través de su ID; ojo, su ID, no su código de barras. Para obtenerlo por código de barras usamos el método porCodigoDeBarras.

De ahí, el de eliminar elimina, el de todos devuelve todos los productos (falta una paginación pero eso ya es otra cosa), el que se llama nuevo inserta un nuevo producto y finalmente el de guardarCambios se encarga de realizar un update o actualización.

Controlador

Este es el pegamento que se encarga de realizar acciones dependiendo de la URL. Para que esto funcione cargamos el helper url, como explico en un post que dejé al inicio.

El código es este:

<?php
class Productos extends CI_Controller{
 
    public function __construct(){
        parent::__construct();
        $this->load->model("ProductoModel");
        $this->load->library('session');
    }
 
    public function agregar(){
        $this->load->view("encabezado");
        $this->load->view("productos/agregar");
        $this->load->view("pie");
    }
 
    public function guardarCambios(){
        $resultado = $this->ProductoModel->guardarCambios(
            $this->input->post("id"),
            $this->input->post("codigo"),
            $this->input->post("descripcion"),
            $this->input->post("precioVenta"),
            $this->input->post("precioCompra"),
            $this->input->post("existencia")
        );
        if($resultado){
            $mensaje = "Producto actualizado correctamente";
            $clase = "success";
        }else{
            $mensaje = "Error al actualizar el producto";
            $clase = "danger";
        }
        $this->session->set_flashdata(array(
            "mensaje" => $mensaje,
            "clase" => $clase,
        ));
        redirect("productos/");
    }
 
    public function editar($id){
        $producto = $this->ProductoModel->uno($id);
        if(null === $producto){
            $this->session->set_flashdata(array(
                "mensaje" => "El producto que quieres editar no existe",
                "clase" => "danger",
            ));
            redirect("productos/");
        }
        $this->load->view("encabezado");
        $this->load->view("productos/editar", array("producto" => $producto));
        $this->load->view("pie");
    }
 
    public function eliminar($id){
        $resultado = $this->ProductoModel->eliminar($id);
        if($resultado){
            $mensaje = "Producto eliminado correctamente";
            $clase = "success";
        }else{
            $mensaje = "Error al eliminar el producto";
            $clase = "danger";
        }
        $this->session->set_flashdata(array(
            "mensaje" => $mensaje,
            "clase" => $clase,
        ));
        redirect("productos/");
    }
 
    public function index(){
        
        $this->load->view("encabezado");
        $this->load->view("productos/listar", array(
            "productos" => $this->ProductoModel->todos()
        ));
        $this->load->view("pie");
    }
 
    public function guardar(){
        $resultado = $this->ProductoModel->nuevo(
                $this->input->post("codigo"),
                $this->input->post("descripcion"),
                $this->input->post("precioVenta"),
                $this->input->post("precioCompra"),
                $this->input->post("existencia")
            );
        if($resultado){
            $mensaje = "Producto guardado correctamente";
            $clase = "success";
        }else{
            $mensaje = "Error al guardar el producto";
            $clase = "danger";
        }
        $this->session->set_flashdata(array(
            "mensaje" => $mensaje,
            "clase" => $clase,
        ));
        redirect("productos/agregar");
    }
}
?>

Lo que hace es igualmente un crud, pero renderiza vistas. Las vistas tienen un if dentro de ellas para ver si hay datos en la sesión, dependiendo de ello muestran una alerta, ya veremos más adelante. Recordemos que las alertas son gracias al framework css Bootstrap.

En el método guardar usamos los datos que nos enviaron a través del formulario accediendo a $this->input->post("clave");

Vistas de productos para listar, editar e insertar

Aquí tenemos a la vista que lista los productos:

<div class="col-xs-12">
    <h1>Productos</h1>
    <?php if(!empty($this->session->flashdata())): ?>
		<div class="alert alert-<?php echo $this->session->flashdata('clase')?>">
			<?php echo $this->session->flashdata('mensaje') ?>
		</div>
	<?php endif; ?>
    <div>
        <a class="btn btn-success" href="<?php echo base_url() ?>index.php/productos/agregar">Nuevo <i class="fa fa-plus"></i></a>
    </div>
    <br>
    <table class="table table-bordered">
        <thead>
            <tr>
                <th>ID</th>
                <th>Código</th>
                <th>Descripción</th>
                <th>Precio de compra</th>
                <th>Precio de venta</th>
                <th>Ganancia</th>
                <th>Existencia</th>
                <th>Editar</th>
                <th>Eliminar</th>
            </tr>
        </thead>
        <tbody>
            <?php foreach($productos as $producto){ ?>
            <tr>
                <td><?php echo $producto->id ?></td>
                <td><?php echo $producto->codigo ?></td>
                <td><?php echo $producto->descripcion ?></td>
                <td><?php echo $producto->precioCompra ?></td>
                <td><?php echo $producto->precioVenta ?></td>
                <td><?php echo $producto->precioVenta - $producto->precioCompra ?></td>
                <td><?php echo $producto->existencia ?></td>
                <td><a class="btn btn-warning" href="<?php echo base_url() ."index.php/productos/editar/" . $producto->id ?>"><i class="fa fa-edit"></i></a></td>
                <td><a class="btn btn-danger" href="<?php echo base_url() ."index.php/productos/eliminar/" . $producto->id ?>"><i class="fa fa-trash"></i></a></td>
            </tr>
            <?php } ?>
        </tbody>
    </table>
</div>

Como vemos, mostramos una alerta si hay datos en la sesión. De ahí es una simple tabla con enlaces para editar y eliminar, la cual se dibuja gracias a un arreglo de productos traídos por el controlador.

Si queremos insertar un nuevo producto, se dibuja esta vista o view de CodeIgniter. Es un simple formulario que manejamos en el controlador.

<div class="col-xs-12">
	<h1>Nuevo producto</h1>
	<?php if(!empty($this->session->flashdata())): ?>
		<div class="alert alert-<?php echo $this->session->flashdata('clase')?>">
			<?php echo $this->session->flashdata('mensaje') ?>
		</div>
	<?php endif; ?>
	<form method="post" action="<?php echo base_url() ?>index.php/productos/guardar">
		<label for="codigo">Código de barras:</label>
		<input class="form-control" name="codigo" required type="text" id="codigo" placeholder="Escribe el código">
 
		<label for="descripcion">Descripción:</label>
		<textarea required id="descripcion" name="descripcion" cols="30" rows="5" class="form-control"></textarea>
 
		<label for="precioVenta">Precio de venta:</label>
		<input class="form-control" name="precioVenta" required type="number" id="precioVenta" placeholder="Precio de venta">
 
		<label for="precioCompra">Precio de compra:</label>
		<input class="form-control" name="precioCompra" required type="number" id="precioCompra" placeholder="Precio de compra">
 
		<label for="existencia">Existencia:</label>
		<input class="form-control" name="existencia" required type="number" id="existencia" placeholder="Cantidad o existencia">
		<br><br><input class="btn btn-info" type="submit" value="Guardar">
	</form>
</div>

Igualmente si hay un mensaje que mostrar, lo muestra.

Finalmente, para editar es el mismo formulario pero con un input oculto de tipo hidden para mandar el id del producto.

Para rellenar el formulario usamos el producto que nos pasa el controlador. La vista se ve así, el código queda abajo.

Editar un producto
Editar un producto

Este es el código que hace a la vista de arriba:

<div class="col-xs-12">
	<h1>Editar producto</h1>
	<form method="post" action="<?php echo base_url() ?>index.php/productos/guardarCambios">
        <input name="id" type="hidden" value="<?php echo $producto->id ?>">
		<label for="codigo">Código de barras:</label>
		<input value="<?php echo $producto->codigo ?>" class="form-control" name="codigo" required type="text" id="codigo" placeholder="Escribe el código">
 
		<label for="descripcion">Descripción:</label>
		<textarea required id="descripcion" name="descripcion" cols="30" rows="5" class="form-control"><?php echo $producto->descripcion ?></textarea>
 
		<label for="precioVenta">Precio de venta:</label>
		<input value="<?php echo $producto->precioVenta ?>" class="form-control" name="precioVenta" required type="number" id="precioVenta" placeholder="Precio de venta">
 
		<label for="precioCompra">Precio de compra:</label>
		<input value="<?php echo $producto->precioCompra ?>" class="form-control" name="precioCompra" required type="number" id="precioCompra" placeholder="Precio de compra">
 
		<label for="existencia">Existencia:</label>
		<input value="<?php echo $producto->existencia ?>" class="form-control" name="existencia" required type="number" id="existencia" placeholder="Cantidad o existencia">
		<br><br><input class="btn btn-info" type="submit" value="Guardar">
	</form>
</div>

Vender

Este es el segundo apartado, aquí es en donde agregamos productos al carrito de compras. El carrito persiste en la sesión, así no importa si refrescan la página. Usamos igualmente los métodos de CodeIgniter para interactuar con el arreglo superglobal $_SESSION así como para iniciar sesiones.

Modelo

No hay modelo para vender, únicamente un controlador. Se usa el modelo de Ventas, explicado abajo.

Controlador

En este caso este controlador gestiona el carrito de compras. Se comunica con un modelo únicamente al realizar la venta, y solamente para eso, de ahí, la responsabilidad es toda suya.

<?php
class Vender extends CI_Controller{
    public function __construct(){
        parent::__construct();
        $this->load->library('session');
    }
 
 
    public function index(){
        if(!$this->session->has_userdata("carrito"))
            $this->session->set_userdata("carrito", array());
 
        $carrito = $this->session->carrito;
        $this->load->view("encabezado");
        $this->load->view("vender/vender", array(
            "carrito" => $carrito,
        ));
        $this->load->view("pie");
    }
 
    public function quitarDelCarrito($indice){
        $carrito = $this->session->carrito;
        array_splice($carrito, $indice, 1);
        $this->session->set_userdata("carrito", $carrito);
        redirect("vender/");
    }
 
    public function cancelarVenta(){
        $this->vaciarCarrito();
        $this->session->set_flashdata(array(
            "mensaje" => "Venta cancelada correctamente",
            "clase" => "success",
        ));
        redirect("vender/");
    }
 
    private function vaciarCarrito(){
        $this->session->set_userdata("carrito", array());
    }
 
    public function terminarVenta(){
        $carrito = $this->session->carrito;
        # Primero ver si hay algo en el carrito, si no, indicarlo
        if(count($carrito) < 1){
            $this->session->set_flashdata(array(
                "mensaje" => "Para vender, primero tienes que agregar productos al carrito",
                "clase" => "warning",
            ));
            redirect("vender/");
        }
        $this->load->model("VentaModel");
        $resultado = $this->VentaModel->nueva($carrito);
        if($resultado){
            $this->vaciarCarrito();
            $this->session->set_flashdata(array(
                "mensaje" => "Venta realizada correctamente",
                "clase" => "success",
            ));
            redirect("vender/");
        }else{
            $this->session->set_flashdata(array(
                "mensaje" => "Error realizando la venta, intente de nuevo",
                "clase" => "danger",
            ));
            redirect("vender/");
        }
        
        
    }
 
    private function agregarAlCarrito($producto){
        $carrito = $this->session->carrito;
        $producto->cantidad = 1;
        $producto->total = $producto->cantidad * $producto->precioVenta;
        array_push($carrito, $producto);
        $this->session->set_userdata("carrito", $carrito);
    }
 
    private function obtenerIndiceSiExiste($codigo){
        $carrito = $this->session->carrito;
        $conteo = count($carrito);
        for($indice = 0; $indice < $conteo; $indice++){
            if($carrito[$indice]->codigo === $codigo) return $indice;
        }
        return -1;
    }
 
    private function aumentarCantidad($indice){
        $carrito = $this->session->carrito;
        $producto = $carrito[$indice];
        $producto->cantidad++;
        $producto->total = $producto->cantidad * $producto->precioVenta;
        $carrito[$indice] = $producto;
        $this->session->set_userdata("carrito", $carrito);
    }
 
    public function agregar(){
        $codigoDeBarras = $this->input->post("codigo");
        $indice = $this->obtenerIndiceSiExiste($codigoDeBarras);
 
        # Si el producto ya estaba en el carrito
        if($indice !== -1){
            # Simplemente le aumentamos la cantidad
            $this->aumentarCantidad($indice);
        }else{
            #Si no, es uno nuevo
            
            $this->load->model("ProductoModel");
            $producto = $this->ProductoModel->porCodigoDeBarras($codigoDeBarras);
            # Pero puede que no exista un producto con ese código
            if(null === $producto){
                $this->session->set_flashdata(array(
                    "mensaje" => "No existe un producto registrado con el código de barras que se proporcionó",
                    "clase" => "warning",
                ));
            # O que no tenga existencia 
            }else if($producto->existencia < 1){
                $this->session->set_flashdata(array(
                    "mensaje" => "No hay suficiente existencia del producto",
                    "clase" => "warning",
                ));
            }else{
                # Y caso de que sí exista y la existencia sea suficiente
                $this->agregarAlCarrito($producto);
            }
        }
        
        # Al final, en cualquier caso redireccionamos, ya sea con o sin mensajes
        redirect("vender/");
    }
}
?>

Usamos a los métodos de sesiones. Aquí una explicación:

  • has_userdata para ver si ya hay carrito, si no, lo ponemos vacío.
  • Para obtener el carrito y hacerle modificaciones usamos los métodos mágicos para acceder a los datos de sesión, usando $this->session->carrito;
  • set_userdata para guardar el carrito, es decir, el arreglo de productos después de haberlo modificado. Ya sea que hayamos agregado o quitado uno, y de igual manera si hemos aumentado la cantidad.

Vale la pena mencionar que para ver si el producto ya está en el carrito usamos una simple búsqueda secuencial en nuestro arreglo de objetos. Y redireccionamos a vender la mayoría de veces, cambiando los mensajes que se muestran.

Para vaciar el carro simplemente definimos un nuevo arreglo en la sesión; bueno, un nuevo arreglo vacío.

Vista para vender

En la vista sólo tenemos un formulario y una tabla. La tabla dibuja los productos que ya existen, y el formulario es uno en donde leemos el código de barras, el cual es enviado al controlador y comparado con el carrito o con la base de datos.

Interfaz para vender
Interfaz para vender

Cada que agregamos un producto, se redirige al mismo lugar y se da la ilusión de que no hubo redirección. También tenemos la opción de quitar del carrito, en donde simplemente pasamos el índice.

<?php 
$granTotal = 0;
?>
<div class="col-xs-12">
	<h1>Vender</h1>
	<?php if(!empty($this->session->flashdata())): ?>
		<div class="alert alert-<?php echo $this->session->flashdata('clase')?>">
			<?php echo $this->session->flashdata('mensaje') ?>
		</div>
	<?php endif; ?>
	<br>
	<form method="post" action="<?php echo base_url() ?>index.php/vender/agregar">
		<label for="codigo">Código de barras:</label>
		<input autocomplete="off" autofocus class="form-control" name="codigo" required type="text" id="codigo" placeholder="Escribe el código">
	</form>
	<br><br>
	<table class="table table-bordered">
		<thead>
			<tr>
				<th>ID</th>
				<th>Código</th>
				<th>Descripción</th>
				<th>Precio de venta</th>
				<th>Cantidad</th>
				<th>Total</th>
				<th>Quitar</th>
			</tr>
		</thead>
		<tbody>
			<?php foreach($carrito as $indice => $producto){ 
					$granTotal += $producto->total;
				?>
			<tr>
				<td><?php echo $producto->id ?></td>
				<td><?php echo $producto->codigo ?></td>
				<td><?php echo $producto->descripcion ?></td>
				<td><?php echo $producto->precioVenta ?></td>
				<td><?php echo $producto->cantidad ?></td>
				<td><?php echo $producto->total ?></td>
				<td><a class="btn btn-danger" href="<?php echo base_url() . "index.php/vender/quitarDelCarrito/" . $indice?>"><i class="fa fa-trash"></i></a></td>
			</tr>
			<?php } ?>
		</tbody>
	</table>
	<h3>Total: <?php echo $granTotal; ?></h3>
	<input name="total" type="hidden" value="<?php echo $granTotal;?>">
	<a href="<?php echo base_url() ?>index.php/vender/terminarVenta" class="btn btn-success">Terminar venta</a>
	<a href="<?php echo base_url() ?>index.php/vender/cancelarVenta" class="btn btn-danger">Cancelar venta</a>
</div>

Puede que sea una mala práctica, pero no encontré una manera más óptima de calcular el total.

Podríamos calcularlo desde el controlador, pero sería recorrer dos veces el arreglo: una en el controlador y otra en la vista; y como de por sí estamos recorriéndolo en la vista, aprovechamos y vamos sumando en cada iteración.

Modelo para vender

Este modelo se encarga de registrar la venta, pero también de otras cosas relacionadas con las ventas. Aquí lo explicaré, porque tiene que ver tanto con vender y con ventas. Pondré únicamente el método:

#<?php
public function nueva($productosVendidos){
    # Primero registramos la venta
    $detalleDeVenta = array("fecha" => date("Y-m-d H:i:s"));
    $this->db->insert("ventas", $detalleDeVenta);
 
    # Ahora tomamos su ID
    # Ver: https://parzibyte.me/blog/2018/03/20/ultimo-id-insertado-codeigniter/
    $idVenta = $this->db->insert_id();
 
    # Recorrer el carrito
    foreach($productosVendidos as $producto){
        # El producto que insertamos es diferente al del carrito, sólo necesitamos algunas cosas:
        $detalleDeProductoVendido = array(
            "id_producto" => $producto->id,
            "cantidad" => $producto->cantidad,
            "precio" => $producto->precioVenta,
            "id_venta" => $idVenta,
        );
        $this->db->insert("productos_vendidos", $detalleDeProductoVendido);
    }
    return true;
}

El método se llama nueva, ya que pertenece al modelo VentaModel. Lo que hacemos es insertar una venta con la fecha de hoy. La insertamos y más tarde obtenemos el último id insertado, luego recorremos el arreglo de productos y vamos insertando producto a producto en nuestra tabla transaccional.

Ventas

Ya para terminar, veamos cómo funcionan las ventas. Lo que tenemos que hacer es cancelar una venta, listarlas a todas o ver el detalle de una.

Modelo de ventas

El modelo completo es el siguiente, ya explicamos arriba cómo se inserta una venta. De ahí, el único método un poco complejo es en donde se obtiene el detalle de una venta, porque se llaman a dos funciones privadas del modelo.

En los demás casos hacemos algunos inner joins con where, cosas muy interesantes.

<?php
class Ventas extends CI_Controller{
 
    public function __construct(){
        parent::__construct();
        $this->load->model("VentaModel");
        $this->load->library("session");
    }
    public function index(){
        $ventasRealizadas = $this->VentaModel->todas();
        $datos = array("ventas" => $ventasRealizadas);
 
        $this->load->view("encabezado");
        $this->load->view("ventas/todas", $datos);
        $this->load->view("pie");
    }
 
    public function detalle($id){
        $detallesDeVenta = $this->VentaModel->porId($id);
        # Por si no existe la venta
        if($detallesDeVenta->detalles === null){
            $this->session->set_flashdata(array(
                "mensaje" => "Los detalles de la venta no se pueden ver porque no existe una venta con ese ID",
                "clase" => "warning",
            ));
            redirect("ventas/");
        }
        $datos = array("venta" => $detallesDeVenta);
        $this->load->view("encabezado");
        $this->load->view("ventas/detalle", $datos);
        $this->load->view("pie");
    }
 
    public function eliminar($id){
        $resultado = $this->VentaModel->eliminar($id);
        if($resultado){
            $mensaje = "Venta eliminada";
            $clase = "success";
        }else{
            $mensaje = "Error al eliminar la venta";
            $clase = "warning";
        }
        $this->session->set_flashdata(array(
            "mensaje" => $mensaje,
            "clase" => $clase,
        ));
        redirect("ventas/");
    }
}
?>

Para eliminar ventas hacemos lo mismo que en los otros modelos, y como existen relaciones, no necesitamos eliminar los productos vendidos porque el proceso se hace automáticamente cuando eliminamos la venta.

Controlador de ventas

Este controlador redirige si queremos ver el detalle de una venta que no existe, y manda mensajes a la interfaz. Por defecto listará todas las ventas, únicamente las ventas, no sus detalles.

<?php
class Ventas extends CI_Controller{
 
    public function __construct(){
        parent::__construct();
        $this->load->model("VentaModel");
        $this->load->library("session");
    }
    public function index(){
        $ventasRealizadas = $this->VentaModel->todas();
        $datos = array("ventas" => $ventasRealizadas);
 
        $this->load->view("encabezado");
        $this->load->view("ventas/todas", $datos);
        $this->load->view("pie");
    }
 
    public function detalle($id){
        $detallesDeVenta = $this->VentaModel->porId($id);
        # Por si no existe la venta
        if($detallesDeVenta->detalles === null){
            $this->session->set_flashdata(array(
                "mensaje" => "Los detalles de la venta no se pueden ver porque no existe una venta con ese ID",
                "clase" => "warning",
            ));
            redirect("ventas/");
        }
        $datos = array("venta" => $detallesDeVenta);
        $this->load->view("encabezado");
        $this->load->view("ventas/detalle", $datos);
        $this->load->view("pie");
    }
 
    public function eliminar($id){
        $resultado = $this->VentaModel->eliminar($id);
        if($resultado){
            $mensaje = "Venta eliminada";
            $clase = "success";
        }else{
            $mensaje = "Error al eliminar la venta";
            $clase = "warning";
        }
        $this->session->set_flashdata(array(
            "mensaje" => $mensaje,
            "clase" => $clase,
        ));
        redirect("ventas/");
    }
}
?>

Cargamos la librería de la sesión en el constructor porque en el método index renderizamos una vista que muestra un mensaje si hay datos en la sesión; y si no la hubiéramos cargado habría errores. El método detalle trae los detalles de una venta a través del modelo, recibe un id como argumento.

Vista de ventas

El lugar en donde listamos todas las ventas es una simple tabla. Esa tabla tiene un botón que permite ver los detalles de la misma, o eliminarla. Para obtener el total de la venta usamos la función SUM de MySQL, y agrupamos. Eso tiene que ver más con bases de datos que con CodeIgniter o PHP.

Ver todas las ventas del sistema
Ver todas las ventas del sistema

La vista en donde se muestran es la siguiente:

<div class="col-xs-12">
    <h1>Ventas</h1>
    <?php if(!empty($this->session->flashdata())): ?>
        <div class="alert alert-<?php echo $this->session->flashdata('clase')?>">
            <?php echo $this->session->flashdata('mensaje') ?>
        </div>
    <?php endif; ?>
    <div>
        <a class="btn btn-success" href="<?php echo base_url() ?>index.php/vender">Nueva <i class="fa fa-plus"></i></a>
    </div>
    <br>
    <table class="table table-bordered">
        <thead>
            <tr>
                <th>Número</th>
                <th>Fecha</th>
                <th>Total</th>
                <th>Detalles</th>
                <th>Eliminar</th>
            </tr>
        </thead>
        <tbody>
            <?php foreach($ventas as $venta){ ?>
            <tr>
                <td><?php echo $venta->id ?></td>
                <td><?php echo $venta->fecha ?></td>
                <td><?php echo $venta->total ?></td>
                <td><a class="btn btn-info" href="<?php echo base_url() . "index.php/ventas/detalle/" . $venta->id?>"><i class="fa fa-info"></i></a></td>
                <td><a class="btn btn-danger" href="<?php echo base_url() . "index.php/ventas/eliminar/" . $venta->id?>"><i class="fa fa-trash"></i></a></td>
            </tr>
            <?php } ?>
        </tbody>
    </table>
</div>

La variable $ventas es pasada por el controlador.

Vista para el detalle de venta

Ver detalles de una venta
Ver detalles de una venta

Finalmente aquí está la vista de cuando vemos el detalle de una venta. En grande se muestra el total y la fecha, y más abajo la lista de los productos vendidos.

<div class="col-xs-12">
    <a class="btn btn-info" href="<?php echo base_url() ?>/index.php/ventas">Ver todas las ventas</a>
    <h1>Detalles de la venta #<?php echo $venta->detalles->id; ?></h1>
    <h4>Total: <span class="label label-success"><?php echo $venta->detalles->total ?></span></h4>
    <h4>Fecha: <span class="label label-primary"><?php echo $venta->detalles->fecha ?></span></h4>
    <h2>Productos vendidos</h2>
    <div class="table-responsive">
        <table class="table table-bordered">
            <thead>
                <tr>
                    <th>Código</th>
                    <th>Descripción</th>
                    <th>Cantidad</th>
                    <th>Precio</th>
                    <th>Total</th>
                </tr>
            </thead>
            <tbody>
                <?php foreach($venta->productos as $producto){ ?>
                <tr>
                    <td><?php echo $producto->codigo ?></td>
                    <td><?php echo $producto->descripcion ?></td>
                    <td><?php echo $producto->cantidad ?></td>
                    <td><?php echo $producto->precio ?></td>
                    <td><?php echo $producto->cantidad * $producto->precio ?></td>
                </tr>
                <?php } ?>
            </tbody>
        </table>
    </div>
</div>

Por cierto, ponemos un simple enlace que lleva al listado de todas las ventas. Fácil y sencillo.

Conclusión

Espero que algún día pueda seguir con este sistema, no me gusta mucho pero queda bien a modo de ejercicio. Recuerda que arriba dejé el código fuente en GitHub.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.

16 comentarios en “Pequeño sistema de ventas con CodeIgniter y MVC”

  1. Que tal amigo mi nombre es Carlos Antonio Rojas Gonzales de la Universidad Politécnica de Pachuca
    En la materia mantenimiento de software de la carreraing en software realice tanto retroalimentacion de tu proyecto como mantenimiento, me gustaria saber si esta interesado en ver mi trabajo y de ser asi podria contactarme

  2. hola, hay una observación, a la hora de hacer una venta, no llega a descontar la cantidad de producto en el almacén.
    se llego a solucionar?
    saludos

    1. Hola, primero que todo comentarte que es muy bueno y dinámico la plataforma, lo otro como pudiste solucionar que descuente la cantidad del articulo según el stock??

  3. hola, disculpe me sale un error a la hora de ejecutar el código, nose que estoy haciendo mal

    [Running] php “c:\xampp\htdocs\ventasci-master\index.php”
    “php” no se reconoce como un comando interno o externo,
    programa o archivo por lotes ejecutable.

    [Done] exited with code=1 in 0.461 seconds

    1. sale la interfas pero solo vender, cuando selecciono productos o ventas me sale:
      A Database Error Occurred
      SQLSTATE[HY000] [1049] Unknown database ‘ventas’

      Filename: C:/xampp/htdocs/ventasci/system/database/drivers/pdo/pdo_driver.php

      Line Number: 142

    2. Claro, basta con leer el mensaje de error: no existe la base de datos “ventas” recuerda que tienes que crearla, en el repositorio está el esquema para que veas cómo es la creación
      Saludos

  4. me gusta la forma en la que realizaste el punto de venta, muy sencillo y práctico, tengo una pregunta… como se puede adaptar tu código para que al finalizar la venta, se genere e imprima un ticket o recibo..?

  5. Pingback: Eliminar elemento de arreglo en PHP a partir de su índice - Parzibyte's blog

  6. Pingback: Pequeño, muy pequeño sistema de ventas con PHP y MySQL - Parzibyte's blog

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *