Crear Sistema de Inventario y Ventas PHP MySQL

Si estás buscando construir un sistema de inventario web con PHP y MySQL, llegaste al lugar correcto. En esta guía vas a encontrar todo lo que necesitas: desde la estructura de la base de datos hasta el módulo de ventas y control de usuarios.

En esta guia les van a aprender como hacer un sistema de inventario y ventas con PHP Puro, las ventajas de PHP Puro les permite entender el funcionamiento del sistema y de la conexion de los modulos de esta forma este conocimiento lo pueden usar para crear el sistema en otro framework si asi lo desean.

Te invito a suscribirte a mi Canal de Youtube @evilnapsis

Y si no quieres construir el sistema de inventario desde cero puedes descargar Inventio Lite Gratis o aquirir Inventio Max con todas las funcionalidades para tu negocio.

¿ Que vas a aprender en esta Super GUIA ?

  • Qué módulos necesita un sistema de inventario real
  • Cómo diseñar la base de datos en MySQL
  • Como crear CRUD de Clientes
  • Como crear Crud de Proveedores
  • Como crear CRUD de categorias
  • Cómo construir el CRUD de productos con PHP
  • Cómo registrar entradas y salidas de stock
  • Cómo agregar alertas de stock mínimo
  • Cómo crear un módulo de ventas y compras
  • Cómo manejar usuarios y roles
  • Qué solución usar si necesitas el sistema ya listo

Al final del articulo encontraras un archivo link de descarga con todo el codigo fuente listo para instalar y usar.

¿ Que Necesitas saber para la Super GUIA ?

Esta GUIA esta creada para cualquiera con conocimientos basicos de PHP y MySQL.

Aqui les dejo unos articulos creados para quieres estan empezando desde CERO CERO.

Tambien necesias un editor de texto y el XAMPP.

Y claro muchas ganas de aprender.

¿Qué es un sistema de inventario web y para qué sirve?

Un sistema de inventario es una aplicación que te permite controlar los productos que tienes en existencia, registrar entradas y salidas, y llevar un historial de ventas. Cuando es web — construido con PHP y MySQL — puedes accederlo desde cualquier computadora sin instalar nada.

Los negocios que más lo usan son tiendas, farmacias, restaurantes, almacenes y pequeñas empresas que necesitan saber en todo momento cuánto tienen en stock y cuánto han vendido.

Un sistema bien construido tiene al menos estos módulos:

  • Catálogo de productos — nombre, precio, categoría
  • Catálogo de categorias— nombre
  • Entradas de inventario — compras
  • Salidas de inventario — ventas
  • Módulo de ventas — ticket o factura por cada transacción
  • Alertas de stock — aviso cuando un producto baja del mínimo
  • Reportes — ventas por día, productos más vendidos
  • Usuarios y roles — administrador, vendedor, almacenista

Estructura de la base de datos en MySQL

Antes de empezar con el codigo PHP vamos a definir la estructura de la base de datos, son las tablas donde se van a guardar los datos, para este proyecto la base de datos se llamara inventoryphp, aunque el modelo es el mismo que el inventio lite.

La base de datos se las dejo en el archivo schema.sql del archivo de descarga al final.

Con el siguiente comando para acceder a la consola de MySQL.

cd /xampp/mysql/bin
mysql -uroot

Ahora vamos a crear la base de datos.

create database inventoryphp;
use inventoryphp;

Y ahora les explico las tablas una por una, primero la tabla usuarios.

La tabla usuarios sirve para quienes va a acceder al sistema, administradores, vendedores.

create table user(
	id int not null auto_increment primary key,
	name varchar(50),
	lastname varchar(50),
	username varchar(50),
	email varchar(255),
	password varchar(60),
	image varchar(255),
	is_active boolean not null default 1,
	is_admin boolean not null default 0,
	created_at datetime
);
insert into user(name,lastname,email,password,is_active,is_admin,created_at) value ("Administrador", "","admin","90b9aa7e25f80cf4f64e990b78a9fc5ebd6cecad",1,1,NOW());

La tabla categoria para gestionar las categorias de los productos.

create table category(
	id int not null auto_increment primary key,
	image varchar(255),
	name varchar(50),
	description text,
	created_at datetime
);

La tabla de productos cuenta con los campos nombre, descripcion, inventary_min (para las alertas de inventario).

create table product(
	id int not null auto_increment primary key,
	image varchar(255),
	barcode varchar(50),
	name varchar(50),
	description text,
	inventary_min int default 10,
	price_in float,
	price_out float,
	unit varchar(255),
	presentation varchar(255),
	user_id int,
	category_id int,
	created_at datetime,
	is_active boolean default 1,
	foreign key (category_id) references category(id),
	foreign key (user_id) references user(id)
);

La tabla person sirve para clientes y proveedores, identificados por el campo kind (1 = cliente, 2 = proveedor)

create table person(
	id int not null auto_increment primary key,
	image varchar(255),
	name varchar(255),
	lastname varchar(50),
	company varchar(50),
	address1 varchar(50),
	address2 varchar(50),
	phone1 varchar(50),
	phone2 varchar(50),
	email1 varchar(50),
	email2 varchar(50),
	kind int,
	created_at datetime
);

La tabla operation_type que sirve para saber los tipos de operacion entrada y salida o podemos agregar mas, por ejemplo en Inventio ax tenemos (entrada-pendiente y salida-pendiente).

create table operation_type(
	id int not null auto_increment primary key,
	name varchar(50)
);

insert into operation_type (name) value ("entrada");
insert into operation_type (name) value ("salida");

Tabla box para cortes de caja, aunque no la vamos a profundizar ni a usar es importante mencionarla para futuras referencias.

create table box(
	id int not null auto_increment primary key,
	cash_in double,
	cash_out double,
	status int default 0,/* 0. Abierto, 1. Cerrado*/
	created_at datetime
);

La tabla de ventas y compras las unificamos en la tabla sell las vamos a identificar por el operation_type_id y en ventas el person_id es el cliente y en compras el person_id es el proveedor.

create table sell(
	id int not null auto_increment primary key,
	person_id int ,
	user_id int ,
	operation_type_id int default 2,
	box_id int,
	total double,
	cash double,
	discount double,
	foreign key (box_id) references box(id),
	foreign key (operation_type_id) references operation_type(id),
	foreign key (user_id) references user(id),
	foreign key (person_id) references person(id),
	created_at datetime
);

La tabla operation es donde se guarda el detalle de las ventas y compras estan relacionadas directamente por el campo sell_id y tambien por el operation_type_id (1=entrada/compra y 2=salida/venta).

create table operation(
	id int not null auto_increment primary key,
	product_id int,
	q float,
	operation_type_id int,
	sell_id int,
	created_at datetime,
	foreign key (product_id) references product(id),
	foreign key (operation_type_id) references operation_type(id),
	foreign key (sell_id) references sell(id)
);

Estructura del codigo fuente

Ahora que vamos a empezar a crear el codigo fuente te explico mas o menos como va a estar estructurado.

Cada modulo va a tener un archivo index.php principal donde mostraremos las vistas que estaran en la carpeta views/ por ejemplo views/productos.php para las vistas y un archivo de acciones con el mismo nombre pero en la carpeta backend por ejemplo backend/productos.php

Ademas en Backend tendremos utilidades como database.php que es la clase de conexion a la base de datos que vamos a usar en todo el proyecto y acceso.php que sirve para login y logout.

En la carpeta del proyecto vamos a tener la siguiente estructura

  • index.php archivo layout principal
  • schema.sql archivo de la base de datos.
  • views
    • productos.php
    • categorias.php
    • usuarios.php
    • ventas.php
    • compras.php
    • inventario.php
    • reporteventas.php
    • reportecompras.php
  • backend/
    • productos.php
    • categorias.php
    • ventas.php
    • compras.php
    • acceso.php
    • usuarios.php

Conexion a la base de datos

Vamos a usar la siguiente clase para la conexion a la base de datos, usaremos el patron singleton para garantizar que solo tengamos una instancia y una conexion a la base de datos.

<?php
class Database {
    private static $db = null;
    private static $pdo = null;

    private function __construct() {
        $host = "localhost";
        $dbname = "inventoryphp";
        $username = "root";
        $password = "";

        try {
            // Conexión usando PDO
            self::$pdo = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
            self::$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            self::$pdo->exec("set names utf8");
        } catch (PDOException $e) {
            die("Error de conexión: " . $e->getMessage());
        }
    }

    public static function getCon() {
        if (self::$db == null) {
            self::$db = new self();
        }
        return self::$pdo;
    }
}
?>

Plantilla o layout Principal index.php

El archivo index.php lo usaremos como pagina base donde el usuario llega pero vamos a cargar vistas y subvistas usando el parametro $_GET[“view”] para las vistas y $_GET[“opt”] para las subvistas.

Por ejemplo para productos quedaria algo asi: index.php?view=productos&opt=all

Al usar view=productos vamos a llamar a el archivo productos.php que esta en la carpeta views y el parametro opt=all es una subdivision que haremos en el archivo para mostrar las subvistas, ver todo, nuevo y editar.

<?php
session_start();
require_once("backend/database.php");

if(!isset($_SESSION["user_id"]) && $_GET["view"] != "login"){
    header("Location: ./login.php");
    exit;
}

$vistas_permitidas = array("login", "dashboard", "categorias",
 "productos", "clientes", "proveedores", 
 "ventas", "compras", "reporteventas", "reportescompras", "inventario","historial","usuarios","nuevaventa","nuevacompra","detallecompra","detalleventa");

 if(isset($_GET["view"]) and in_array($_GET["view"], $vistas_permitidas)){
    $view = $_GET["view"];
 }else{
    $view = "dashboard";
 }
?>
<html>
    <head>
        <title>Inventario PHP Puro - Evilnapsis</title>
        <link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
        <link rel="stylesheet" href="assets/bootstrap/css/bootstrap-icons.css">
    </head>
    <body>

        <nav class="navbar navbar-expand-lg navbar-dark bg-dark shadow-sm">
            <div class="container">
                <a class="navbar-brand fw-bold" href="./"><i class="bi bi-box-seam me-2"></i><b>INVENTORY</b></a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="collapse navbar-collapse" id="navbarNav">
                    <ul class="navbar-nav">
                        <li class="nav-item">
                            <a class="nav-link" href="./">INICIO</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="./?view=productos&opt=all">PRODUCTOS</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="./?view=inventario">INVENTARIO</a>
                        </li>
                        <li class="nav-item dropdown">
                            <a class="nav-link dropdown-toggle" href="#" id="navbarCatalogos" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                                CATALOGOS
                            </a>
                            <ul class="dropdown-menu shadow" aria-labelledby="navbarCatalogos">
                                <li><a class="dropdown-item" href="./?view=categorias&opt=all">Categorías</a></li>
                                <li><a class="dropdown-item" href="./?view=clientes&opt=all">Clientes</a></li>
                                <li><a class="dropdown-item" href="./?view=proveedores&opt=all">Proveedores</a></li>
                            </ul>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="./?view=usuarios&opt=all">USUARIOS</a>
                        </li>
                        <li class="nav-item dropdown">
                            <a class="nav-link dropdown-toggle" href="#" id="navbarVentas" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                                VENTAS
                            </a>
                            <ul class="dropdown-menu shadow" aria-labelledby="navbarVentas">
                                <li><a class="dropdown-item" href="./?view=nuevaventa">Nueva Venta</a></li>
                                <li><a class="dropdown-item" href="./?view=ventas&opt=all">Ver Ventas</a></li>
                            </ul>
                        </li>
                        <li class="nav-item dropdown">
                            <a class="nav-link dropdown-toggle" href="#" id="navbarCompras" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                                COMPRAS
                            </a>
                            <ul class="dropdown-menu shadow" aria-labelledby="navbarCompras">
                                <li><a class="dropdown-item" href="./?view=nuevacompra">Nueva Compra</a></li>
                                <li><a class="dropdown-item" href="./?view=compras&opt=all">Ver Compras</a></li>
                            </ul>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-danger fw-bold" href="backend/logout.php"><i class="bi bi-power me-1"></i> SALIR</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>

        
        <div class="container my-3">

                    <?php 
                    if(file_exists("views/".$view.".php")){
                        include("views/".$view.".php");
                    }else{
                        echo "<h1>Dashboard</h1>";
                        echo "<p class='lead'>Bienvenido al sistema de inventario.</p>";
                    }
                    ?>
        </div>
        <script src="assets/bootstrap/js/bootstrap.bundle.min.js"></script>
    </body>
</html>

En esta plantilla o layout se cargan los archivos css y javascript para el funcionamiento del proyecto y para que se vea bien le añadimos Bootstrap 5.

Aqui en el layout tambien contamos con el menu principal y un loader que recibe el parametros de las vistas e incluye el archivo de vistas correspondiente.

Tambien contamos con una lista de las vistas permitidas para el usuario no ingrese datos incorrectos.

Login de usuarios

Para acceder al sistema es necesario iniciar sesion, por default en la tabla de usuarios ya ingresamos el usuario administrador, ahora vamos a crear el login. Este es el archivo login.php

<html>
    <head>
        <title>Login - Inventory PHP</title>
        <link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
        <link rel="stylesheet" href="assets/bootstrap/css/bootstrap-icons.css">
        <style>
            body { background: #f4f7f6; }
            .login-container { margin-top: 100px; }
            .card { border: none; border-radius: 15px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
            .btn-primary { border-radius: 10px; padding: 12px; font-weight: 600; background: #6366f1; border: none; }
        </style>
    </head>
    <body>
        <div class="container login-container">
            <div class="row justify-content-center">
                <div class="col-md-4">
                    <div class="card p-4">
                        <div class="text-center mb-4">
                            <i class="bi bi-box-seam fs-1 text-primary"></i>
                            <h2 class="fw-bold mt-2">Bienvenido</h2>
                            <p class="text-muted">Inicia sesión para continuar</p>
                        </div>
                        <form action="backend/acceso.php" method="post">
                            <div class="mb-3">
                                <label class="form-label">Nombre de Usuario</label>
                                <input type="text" name="username" class="form-control form-control-lg bg-light border-0" required autofocus>
                            </div>
                            <div class="mb-3">
                                <label class="form-label">Contraseña</label>
                                <input type="password" name="password" class="form-control form-control-lg bg-light border-0" required>
                            </div>
                            <div class="d-grid">
                                <button type="submit" class="btn btn-primary btn-lg">Entrar al Sistema</button>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </body>
</html>

Algoritmo de Login

Para que el login funcione correctamente tendemos el archivo backend/acceso.php que recibe los datos del formulario de login y consulta si el usaurio y la contrasña pertenecen a un usuario registrado en la base de datos y si es correcto se crea la variable de session $_SESSION[“user_id”] con el id del usuario logeado y este id se va a insertar en las operaciones que realice el usuario logueado.

<?php
/**
 * Algoritmo de Acceso al Sistema
 * Powered by Evilnapsis
 **/
session_start();
require_once("database.php");

// Verificamos si se enviaron las credenciales
if(isset($_POST["username"]) && isset($_POST["password"])){
    $db = Database::getCon();
    $username = $_POST["username"];
    // Cifrado de contraseña solicitado: sha1(md5())
    $password = sha1(md5($_POST["password"]));

    // Buscamos al usuario en la base de datos
    $sql = "SELECT id FROM user WHERE username=? AND password=? AND is_active=1";
    $stmt = $db->prepare($sql);
    $stmt->execute([$username, $password]);
    $user = $stmt->fetch(PDO::FETCH_ASSOC);

    // Si el usuario existe, iniciamos sesión
    if($user){
        $_SESSION["user_id"] = $user["id"];
        header("Location: ../");
    }else{
        // Si no existe, regresamos con error
        header("Location: ../login.php?error=1");
    }
}else{
    // Si no se enviaron datos, regresamos al login
    header("Location: ../login.php");
}
?>

Tambien necesitaremos cerrar la sesion, para esta accion creamos el archivo backend/logout.php que elimina los datos de la sesion.

<?php
/**
 * Salir del Sistema
 **/
session_start();
// Destruimos la sesión activa
session_destroy();
// Redirigimos al formulario de login
header("Location: ../login.php");
?>

Modulo de Categorias

Vamos a empezar por el modulo que pienso que es el mas facil, las categorias, ya que estas solo necesitan el nombre para darse de alta y para editar.

Para manejar las categorias contamos con los archivos views/categorias.php que maneja las vistas y subvistas (all, new, edit).

He separado la tabla de listado de categorias, formulario de nueva categoria y formulario de editar en partes para poderlas explicar, pero realmente todo debe ir en el mismo archivo y pegar el codigo en la seccion que le corresponde a cada subvista.

<?php if(isset($_GET["opt"]) && $_GET["opt"]=="all"):?>
<!-- Tabla de categorias -->


<?php elseif(isset($_GET["opt"]) && $_GET["opt"]=="new"):?>
<!-- Formulario de nueva categoria -->

<?php elseif(isset($_GET["opt"]) && $_GET["opt"]=="edit"):?>
<!-- Formulario de editar categoria -->

<?php endif; ?>

Listado de Categorias

La vista de listado de categorias se muestra cuando se abre la vista de categorias y la subvista “all” index.php?view=categorias&opt=all

Mostramos las categorias en una tabla y los campos id, nombre y las acciones que sirven para movernos a las vistas de editar y a la accion de eliminar.

<?php
    $query = database::getCon()->query("SELECT * FROM category");
    $categorias = $query->fetchAll(PDO::FETCH_ASSOC);
    ?>
            <div class="row">
                <div class="col-md-12"><h1>Categorias</h1>
 <p>Administración de categorías.</p>
<a href="./?view=categorias&opt=new" class="btn btn-primary">Nueva Categoría</a>
<br><br>
<?php if(count($categorias)):?>
    <table class="table table-bordered table-hover">
        <thead>
            <tr>
                <th>ID</th>
                <th>Nombre</th>
                <th>Acciones</th>
            </tr>
        </thead>
        <tbody>
            <?php foreach($categorias as $categoria):?>
                <tr>
                    <td><?php echo $categoria["id"];?></td>
                    <td><?php echo $categoria["name"];?></td>
                    <td>
                        <a href="./?view=categorias&opt=edit&id=<?php echo $categoria["id"];?>" class="btn btn-warning">Editar</a>
                        <a href="./backend/categorias.php?opt=del&id=<?php echo $categoria["id"];?>" class="btn btn-danger">Eliminar</a>
                    </td>
                </tr>
            <?php endforeach;?>
        </tbody>
    </table>
    <?php else:?>
<?php endif;?>

</div>
</div>

Formulario de Nueva Categoria

La vista de formulario de nueva categoria se muestra cuando se abre la vista de categorias y la subvista “new” index.php?view=categorias&opt=new

<div class="row">
 <div class="col-md-12"><h1>Categorias</h1>
<p>Administración de categorías.</p></div>
    <h1>Nueva Categoría</h1>
    <form action="./backend/categorias.php?opt=add" method="post">
        <div class="mb-3">
            <label for="name" class="form-label">Nombre</label>
            <input type="text" class="form-control" id="name" name="name" required>
        </div>
        <button type="submit" class="btn btn-primary">Agregar</button>
    </form>
</div>
</div>

Formulario de Editar Categoria

La vista de formulario de editar categoria se muestra cuando se abre la vista de categorias y la subvista “edit” index.php?view=categorias&opt=edit

<?php
    $id = $_GET["id"];
    $query = database::getCon()->prepare("SELECT * FROM category WHERE id=?");
    $query->execute([$id]);
    $cat = $query->fetch(PDO::FETCH_ASSOC);
    ?>
    <div class="row">
        <div class="col-md-12"><h1>Categorias</h1>
<p>Administración de categorías.</p></div>
    <h1>Editar Categoría</h1>
    <form action="./backend/categorias.php?opt=update" method="post">
        <input type="hidden" name="id" value="<?php echo $cat["id"];?>">
        <div class="mb-3">
            <label for="name" class="form-label">Nombre</label>
            <input type="text" class="form-control" id="name" name="name" value="<?php echo $cat["name"];?>" required>
        </div>
        <button type="submit" class="btn btn-primary">Actualizar</button>
    </form>
</div>
</div>

Backed para el CRUD de Categorias

El archivo backend/categorias.php se encarga de las de las acciones en la base de datos, recibir los valores de la vista y guardar, acutalizar o eliminar.

<?php
/**
 * Backend para Categorías
 **/
require_once("database.php");

if(isset($_GET["opt"])){
    $db = Database::getCon();

    // Agregar categoría
    if($_GET["opt"]=="add"){
        $name = $_POST["name"];
        
        $sql = "INSERT INTO category (name) VALUES (?)";
        $stmt = $db->prepare($sql);
        $stmt->execute([$name]);
        
        header("Location: ../?view=categorias&opt=all");
    }

    // Actualizar categoría
    if($_GET["opt"]=="update"){
        $id = $_POST["id"];
        $name = $_POST["name"];
        
        $sql = "UPDATE category SET name=? WHERE id=?";
        $stmt = $db->prepare($sql);
        $stmt->execute([$name, $id]);
        
        header("Location: ../?view=categorias&opt=all");
    }

    // Eliminar categoría
    if($_GET["opt"]=="del"){
        $id = $_GET["id"];
        
        $sql = "DELETE FROM category WHERE id=?";
        $stmt = $db->prepare($sql);
        $stmt->execute([$id]);
        
        header("Location: ../?view=categorias&opt=all");
    }
}
?>

El action ejecuta las consultas SQL para agregar, editar y eliminar categorias.

Modulo de Productos

Ahora vamos con el modulo de productos que es el mas importante pero necesitabamos el modulo de categorias por que los productos requieren rellenar el campo de categorias.

La vista de productos cuenta con las subvistas: el listado de productos, formulario de nuevo y editar producto.

<?php if(isset($_GET["opt"]) && $_GET["opt"]=="all"):?>
<!-- Listado de productos -->

<?php elseif(isset($_GET["opt"]) && $_GET["opt"]=="new"):?>
<!-- Formulario de Nuevo Producto -->

<?php elseif(isset($_GET["opt"]) && $_GET["opt"]=="edit"):?>
<!-- Formulario de Editar Producto-->

<?php endif;?>

Listado de Productos

La vista listado de productos se muestra cuando se abre la vista de productos y la subvista “all” index.php?view=productos&opt=all

<?php
    $query = database::getCon()->query("SELECT * FROM product");
    $productos = $query->fetchAll(PDO::FETCH_ASSOC);
    ?>
    <div class="row">
        <div class="col-md-12">
            <h1>Productos</h1>
            <p>Administración de productos.</p>
            <a href="./?view=productos&opt=new" class="btn btn-primary">Nuevo Producto</a>
            <br><br>
            <?php if(count($productos)):?>
                <table class="table table-bordered table-hover">
                    <thead>
                        <tr>
                            <th>ID</th>
                            <th>Nombre</th>
                            <th>Precio Entrada</th>
                            <th>Precio Salida</th>
                            <th>Min. Inventario</th>
                            <th>Activo</th>
                            <th>Acciones</th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach($productos as $producto):?>
                            <tr>
                                <td><?php echo $producto["id"];?></td>
                                <td><?php echo $producto["name"];?></td>
                                <td>$<?php echo $producto["price_in"];?></td>
                                <td>$<?php echo $producto["price_out"];?></td>
                                <td><?php echo $producto["inventary_min"];?></td>
                                <td><?php echo $producto["is_active"] ? "Sí" : "No";?></td>
                                <td>
                                    <a href="./?view=productos&opt=edit&id=<?php echo $producto["id"];?>" class="btn btn-warning btn-sm">Editar</a>
                                    <a href="./backend/productos.php?opt=del&id=<?php echo $producto["id"];?>" class="btn btn-danger btn-sm">Eliminar</a>
                                </td>
                            </tr>
                        <?php endforeach;?>
                    </tbody>
                </table>
            <?php else:?>
                <p class="alert alert-warning">No hay productos registrados.</p>
            <?php endif;?>
        </div>
    </div>

Formulario de Nuevo Producto

La vista de formulario de nuevo producto se muestra cuando se abre la vista de productos y la subvista “new” index.php?view=productos&opt=new

<?php
    $categories = database::getCon()->query("SELECT * FROM category")->fetchAll(PDO::FETCH_ASSOC);
    ?>
    <div class="row">
        <div class="col-md-12">
            <h1>Nuevo Producto</h1>
            <form action="./backend/productos.php?opt=add" method="post">
                <div class="mb-3">
                    <label for="name" class="form-label">Nombre</label>
                    <input type="text" class="form-control" id="name" name="name" required>
                </div>
                <div class="mb-3">
                    <label for="description" class="form-label">Descripción (Opcional)</label>
                    <textarea class="form-control" id="description" name="description"></textarea>
                </div>
                <div class="mb-3">
                    <label for="category_id" class="form-label">Categoría (Opcional)</label>
                    <select name="category_id" id="category_id" class="form-control">
                        <option value="">-- SELECCIONAR --</option>
                        <?php foreach($categories as $cat):?>
                            <option value="<?php echo $cat["id"];?>"><?php echo $cat["name"];?></option>
                        <?php endforeach;?>
                    </select>
                </div>
                <div class="row">
                    <div class="col-md-4 mb-3">
                        <label for="price_in" class="form-label">Precio Entrada</label>
                        <input type="number" step="0.01" class="form-control" id="price_in" name="price_in" required>
                    </div>
                    <div class="col-md-4 mb-3">
                        <label for="price_out" class="form-label">Precio Salida</label>
                        <input type="number" step="0.01" class="form-control" id="price_out" name="price_out" required>
                    </div>
                    <div class="col-md-4 mb-3">
                        <label for="inventary_min" class="form-label">Mín. Inventario</label>
                        <input type="number" class="form-control" id="inventary_min" name="inventary_min" value="10" required>
                    </div>
                </div>
                <div class="mb-3 form-check">
                    <input type="checkbox" class="form-check-input" id="is_active" name="is_active" checked>
                    <label class="form-check-label" for="is_active">Producto Activo</label>
                </div>
                <button type="submit" class="btn btn-primary">Guardar Producto</button>
            </form>
        </div>
    </div>

Formulario de Editar Producto

La vista de formulario de editar productose muestra cuando se abre la vista de productos y la subvista “edit” index.php?view=productos&opt=edit

<?php
    $id = $_GET["id"];
    $query = database::getCon()->prepare("SELECT * FROM product WHERE id=?");
    $query->execute([$id]);
    $prod = $query->fetch(PDO::FETCH_ASSOC);
    $categories = database::getCon()->query("SELECT * FROM category")->fetchAll(PDO::FETCH_ASSOC);
    ?>
    <div class="row">
        <div class="col-md-12">
            <h1>Editar Producto</h1>
            <form action="./backend/productos.php?opt=update" method="post">
                <input type="hidden" name="id" value="<?php echo $prod["id"];?>">
                <div class="mb-3">
                    <label for="name" class="form-label">Nombre</label>
                    <input type="text" class="form-control" id="name" name="name" value="<?php echo $prod["name"];?>" required>
                </div>
                <div class="mb-3">
                    <label for="description" class="form-label">Descripción (Opcional)</label>
                    <textarea class="form-control" id="description" name="description"><?php echo $prod["description"];?></textarea>
                </div>
                <div class="mb-3">
                    <label for="category_id" class="form-label">Categoría (Opcional)</label>
                    <select name="category_id" id="category_id" class="form-control">
                        <option value="">-- SELECCIONAR --</option>
                        <?php foreach($categories as $cat):?>
                            <option value="<?php echo $cat["id"];?>" <?php echo ($cat["id"]==$prod["category_id"])?"selected":""; ?>><?php echo $cat["name"];?></option>
                        <?php endforeach;?>
                    </select>
                </div>
                <div class="row">
                    <div class="col-md-4 mb-3">
                        <label for="price_in" class="form-label">Precio Entrada</label>
                        <input type="number" step="0.01" class="form-control" id="price_in" name="price_in" value="<?php echo $prod["price_in"];?>" required>
                    </div>
                    <div class="col-md-4 mb-3">
                        <label for="price_out" class="form-label">Precio Salida</label>
                        <input type="number" step="0.01" class="form-control" id="price_out" name="price_out" value="<?php echo $prod["price_out"];?>" required>
                    </div>
                    <div class="col-md-4 mb-3">
                        <label for="inventary_min" class="form-label">Mín. Inventario</label>
                        <input type="number" class="form-control" id="inventary_min" name="inventary_min" value="<?php echo $prod["inventary_min"];?>" required>
                    </div>
                </div>
                <div class="mb-3 form-check">
                    <input type="checkbox" class="form-check-input" id="is_active" name="is_active" <?php echo ($prod["is_active"])?"checked":""; ?>>
                    <label class="form-check-label" for="is_active">Producto Activo</label>
                </div>
                <button type="submit" class="btn btn-primary">Actualizar Producto</button>
            </form>
        </div>
    </div>

Backend para CRUD de Productos

A pesar de que el modulo productos tienen mas campos, la logica es la misma, tenemos las subvistas nuevo y editar que al ejecutarse los formualrios llaman a las acciones add y update respectivamente.

En las acciones se ejecutan las consultas SQL para el guardado de los datos.

<?php
/**
 * Backend para Productos
 **/
require_once("database.php");

if(isset($_GET["opt"])){
    $db = Database::getCon();

    // Agregar producto
    if($_GET["opt"]=="add"){
        $name = $_POST["name"];
        $description = $_POST["description"];
        $category_id = !empty($_POST["category_id"]) ? $_POST["category_id"] : null;
        $price_in = $_POST["price_in"];
        $price_out = $_POST["price_out"];
        $inventary_min = $_POST["inventary_min"];
        $user_id = 1; // Por ahora el admin por defecto
        $is_active = isset($_POST["is_active"]) ? 1 : 0;
        
        $sql = "INSERT INTO product (name, description, category_id, price_in, price_out, inventary_min, user_id, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())";
        $stmt = $db->prepare($sql);
        $stmt->execute([$name, $description, $category_id, $price_in, $price_out, $inventary_min, $user_id, $is_active]);
        
        header("Location: ../?view=productos&opt=all");
    }

    // Actualizar producto
    if($_GET["opt"]=="update"){
        $id = $_POST["id"];
        $name = $_POST["name"];
        $description = $_POST["description"];
        $category_id = !empty($_POST["category_id"]) ? $_POST["category_id"] : null;
        $price_in = $_POST["price_in"];
        $price_out = $_POST["price_out"];
        $inventary_min = $_POST["inventary_min"];
        $is_active = isset($_POST["is_active"]) ? 1 : 0;
        
        $sql = "UPDATE product SET name=?, description=?, category_id=?, price_in=?, price_out=?, inventary_min=?, is_active=? WHERE id=?";
        $stmt = $db->prepare($sql);
        $stmt->execute([$name, $description, $category_id, $price_in, $price_out, $inventary_min, $is_active, $id]);
        
        header("Location: ../?view=productos&opt=all");
    }

    // Eliminar producto
    if($_GET["opt"]=="del"){
        $id = $_GET["id"];
        $sql = "DELETE FROM product WHERE id=?";
        $stmt = $db->prepare($sql);
        $stmt->execute([$id]);
        header("Location: ../?view=productos&opt=all");
    }
}
?>

Modulo de Clientes y Proveedores

Los clientes y proveedores comparten la misma tabla de la base de datos, pero tienen vistas separadas, clientes.php y proveedores.php.

Vista de clientes.php

<?php if(isset($_GET["opt"]) && $_GET["opt"]=="all"):
    $query = database::getCon()->query("SELECT * FROM person WHERE kind=1");
    $clientes = $query->fetchAll(PDO::FETCH_ASSOC);
    ?>
    <div class="row">
        <div class="col-md-12">
            <h1>Clientes</h1>
            <p>Directorio de clientes.</p>
            <a href="./?view=clientes&opt=new" class="btn btn-primary">Nuevo Cliente</a>
            <br><br>
            <?php if(count($clientes)):?>
                <table class="table table-bordered table-hover">
                    <thead>
                        <tr>
                            <th>ID</th>
                            <th>Nombre Completo</th>
                            <th>Teléfono</th>
                            <th>Acciones</th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach($clientes as $cli):?>
                            <tr>
                                <td><?php echo $cli["id"];?></td>
                                <td><?php echo $cli["name"]." ".$cli["lastname"];?></td>
                                <td><?php echo $cli["phone1"];?></td>
                                <td>
                                    <a href="./?view=clientes&opt=edit&id=<?php echo $cli["id"];?>" class="btn btn-warning btn-sm">Editar</a>
                                    <a href="./backend/personas.php?opt=del&kind=1&id=<?php echo $cli["id"];?>" class="btn btn-danger btn-sm" onclick="return confirm('¿Seguro?')">Eliminar</a>
                                </td>
                            </tr>
                        <?php endforeach;?>
                    </tbody>
                </table>
            <?php else:?>
                <p class="alert alert-warning">No hay clientes registrados.</p>
            <?php endif;?>
        </div>
    </div>

<?php elseif(isset($_GET["opt"]) && $_GET["opt"]=="new"):?>
    <div class="row">
        <div class="col-md-12">
            <h1>Nuevo Cliente</h1>
            <form action="./backend/personas.php?opt=add" method="post">
                <input type="hidden" name="kind" value="1">
                <div class="mb-3">
                    <label class="form-label">Nombre</label>
                    <input type="text" class="form-control" name="name" required>
                </div>
                <div class="mb-3">
                    <label class="form-label">Apellido (Opcional)</label>
                    <input type="text" class="form-control" name="lastname">
                </div>
                <div class="mb-3">
                    <label class="form-label">Teléfono (Opcional)</label>
                    <input type="text" class="form-control" name="phone1">
                </div>
                <button type="submit" class="btn btn-primary">Guardar Cliente</button>
            </form>
        </div>
    </div>

<?php elseif(isset($_GET["opt"]) && $_GET["opt"]=="edit"):
    $id = $_GET["id"];
    $query = database::getCon()->prepare("SELECT * FROM person WHERE id=? AND kind=1");
    $query->execute([$id]);
    $cli = $query->fetch(PDO::FETCH_ASSOC);
    ?>
    <div class="row">
        <div class="col-md-12">
            <h1>Editar Cliente</h1>
            <form action="./backend/personas.php?opt=update" method="post">
                <input type="hidden" name="id" value="<?php echo $cli["id"];?>">
                <input type="hidden" name="kind" value="1">
                <div class="mb-3">
                    <label class="form-label">Nombre</label>
                    <input type="text" class="form-control" name="name" value="<?php echo $cli["name"];?>" required>
                </div>
                <div class="mb-3">
                    <label class="form-label">Apellido (Opcional)</label>
                    <input type="text" class="form-control" name="lastname" value="<?php echo $cli["lastname"];?>">
                </div>
                <div class="mb-3">
                    <label class="form-label">Teléfono (Opcional)</label>
                    <input type="text" class="form-control" name="phone1" value="<?php echo $cli["phone1"];?>">
                </div>
                <button type="submit" class="btn btn-primary">Actualizar Cliente</button>
            </form>
        </div>
    </div>
<?php endif;?>

Vista de proveedores.php

<?php if(isset($_GET["opt"]) && $_GET["opt"]=="all"):
    $query = database::getCon()->query("SELECT * FROM person WHERE kind=2");
    $proveedores = $query->fetchAll(PDO::FETCH_ASSOC);
    ?>
    <div class="row">
        <div class="col-md-12">
            <h1>Proveedores</h1>
            <p>Directorio de proveedores.</p>
            <a href="./?view=proveedores&opt=new" class="btn btn-primary">Nuevo Proveedor</a>
            <br><br>
            <?php if(count($proveedores)):?>
                <table class="table table-bordered table-hover">
                    <thead>
                        <tr>
                            <th>ID</th>
                            <th>Nombre Completo</th>
                            <th>Teléfono</th>
                            <th>Acciones</th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach($proveedores as $prov):?>
                            <tr>
                                <td><?php echo $prov["id"];?></td>
                                <td><?php echo $prov["name"]." ".$prov["lastname"];?></td>
                                <td><?php echo $prov["phone1"];?></td>
                                <td>
                                    <a href="./?view=proveedores&opt=edit&id=<?php echo $prov["id"];?>" class="btn btn-warning btn-sm">Editar</a>
                                    <a href="./backend/personas.php?opt=del&kind=2&id=<?php echo $prov["id"];?>" class="btn btn-danger btn-sm" onclick="return confirm('¿Seguro?')">Eliminar</a>
                                </td>
                            </tr>
                        <?php endforeach;?>
                    </tbody>
                </table>
            <?php else:?>
                <p class="alert alert-warning">No hay proveedores registrados.</p>
            <?php endif;?>
        </div>
    </div>

<?php elseif(isset($_GET["opt"]) && $_GET["opt"]=="new"):?>
    <div class="row">
        <div class="col-md-12">
            <h1>Nuevo Proveedor</h1>
            <form action="./backend/personas.php?opt=add" method="post">
                <input type="hidden" name="kind" value="2">
                <div class="mb-3">
                    <label class="form-label">Nombre</label>
                    <input type="text" class="form-control" name="name" required>
                </div>
                <div class="mb-3">
                    <label class="form-label">Apellido (Opcional)</label>
                    <input type="text" class="form-control" name="lastname">
                </div>
                <div class="mb-3">
                    <label class="form-label">Teléfono (Opcional)</label>
                    <input type="text" class="form-control" name="phone1">
                </div>
                <button type="submit" class="btn btn-primary">Guardar Proveedor</button>
            </form>
        </div>
    </div>

<?php elseif(isset($_GET["opt"]) && $_GET["opt"]=="edit"):
    $id = $_GET["id"];
    $query = database::getCon()->prepare("SELECT * FROM person WHERE id=? AND kind=2");
    $query->execute([$id]);
    $prov = $query->fetch(PDO::FETCH_ASSOC);
    ?>
    <div class="row">
        <div class="col-md-12">
            <h1>Editar Proveedor</h1>
            <form action="./backend/personas.php?opt=update" method="post">
                <input type="hidden" name="id" value="<?php echo $prov["id"];?>">
                <input type="hidden" name="kind" value="2">
                <div class="mb-3">
                    <label class="form-label">Nombre</label>
                    <input type="text" class="form-control" name="name" value="<?php echo $prov["name"];?>" required>
                </div>
                <div class="mb-3">
                    <label class="form-label">Apellido (Opcional)</label>
                    <input type="text" class="form-control" name="lastname" value="<?php echo $prov["lastname"];?>">
                </div>
                <div class="mb-3">
                    <label class="form-label">Teléfono (Opcional)</label>
                    <input type="text" class="form-control" name="phone1" value="<?php echo $prov["phone1"];?>">
                </div>
                <button type="submit" class="btn btn-primary">Actualizar Proveedor</button>
            </form>
        </div>
    </div>
<?php endif;?>

Las acciones de agregar, editar y eliminar clientes o proveedores estan en un solo archivo backend/personas.php segun el campo kind definimos si es un cliente kind=1 o un proveedor kind=2.

<?php
/**
 * Backend para Personas (Clientes y Proveedores)
 **/
require_once("database.php");

if(isset($_GET["opt"])){
    $db = Database::getCon();

    // Agregar persona
    if($_GET["opt"]=="add"){
        $name = $_POST["name"];
        $lastname = $_POST["lastname"];
        $phone1 = $_POST["phone1"];
        $kind = $_POST["kind"]; // 1 para Cliente, 2 para Proveedor
        
        $sql = "INSERT INTO person (name, lastname, phone1, kind, created_at) VALUES (?, ?, ?, ?, NOW())";
        $stmt = $db->prepare($sql);
        $stmt->execute([$name, $lastname, $phone1, $kind]);
        
        $view = ($kind == 1) ? "clientes" : "proveedores";
        header("Location: ../?view=$view&opt=all");
    }

    // Actualizar persona
    if($_GET["opt"]=="update"){
        $id = $_POST["id"];
        $name = $_POST["name"];
        $lastname = $_POST["lastname"];
        $phone1 = $_POST["phone1"];
        $kind = $_POST["kind"];
        
        $sql = "UPDATE person SET name=?, lastname=?, phone1=? WHERE id=?";
        $stmt = $db->prepare($sql);
        $stmt->execute([$name, $lastname, $phone1, $id]);
        
        $view = ($kind == 1) ? "clientes" : "proveedores";
        header("Location: ../?view=$view&opt=all");
    }

    // Eliminar persona
    if($_GET["opt"]=="del"){
        $id = $_GET["id"];
        $kind = $_GET["kind"];
        
        $sql = "DELETE FROM person WHERE id=?";
        $stmt = $db->prepare($sql);
        $stmt->execute([$id]);
        
        $view = ($kind == 1) ? "clientes" : "proveedores";
        header("Location: ../?view=$view&opt=all");
    }
}
?>

Ahora ya tenemos categorias, productos, clientes y proveedores ahora si vamos a por el modulo de compras.

Modulo de compras (altas de inventario)

Antes de poder vender o ver el inventario (por que inicalmente esta en CERO) vamos a dar de alta productos y para eso vamos a crear el modulo de compras.

La logica es la siguiente,en la tabla sell conviven compras y ventas identificadas por el campo operation_type_id, si es 1 se trata de una compra y si operation_type_id=2 se trata de una venta.

Tambien en la tabla de operation las operaciones se identifican con el campo operation_type_id.

Formulario de Nueva Compra

Para dar de alta las compras vamos a crear un formulario facil, donde seleccionamos un proveedor y creamos una lista de compras usando un carrito de compras usando sessiones en PHP.

<?php
$providers = database::getCon()->query("SELECT * FROM person WHERE kind=2")->fetchAll(PDO::FETCH_ASSOC);
$products = database::getCon()->query("SELECT * FROM product WHERE is_active=1")->fetchAll(PDO::FETCH_ASSOC);

// Obtenemos el carro de la sesión
$cart = isset($_SESSION["cart_compra"]) ? $_SESSION["cart_compra"] : array();
?>
<div class="row">
    <div class="col-md-12">
        <h1>Nueva Compra</h1>
        <p>Registrar el ingreso de mercancía al inventario.</p>
        <hr>
        
        <!-- Formulario para agregar productos al carro -->
        <div class="border p-3 mb-3">
            <form action="./backend/cartcompra.php?opt=add" method="post">
                <div class="row align-items-end g-2">
                    <div class="col-md-6">
                        <label class="form-label">Buscar Producto</label>
                        <select name="product_id" class="form-select" required>
                            <option value="">-- SELECCIONAR PRODUCTO --</option>
                            <?php foreach($products as $prod):?>
                                <option value="<?php echo $prod["id"];?>"><?php echo $prod["name"];?> ($<?php echo $prod["price_in"];?>)</option>
                            <?php endforeach;?>
                        </select>
                    </div>
                    <div class="col-md-2">
                        <label class="form-label">Cantidad</label>
                        <input type="number" name="q" value="1" min="1" class="form-control" required>
                    </div>
                    <div class="col-md-4">
                        <button type="submit" class="btn btn-primary w-100">
                            Agregar a la Lista
                        </button>
                    </div>
                </div>
            </form>
        </div>

        <!-- Formulario final para procesar la compra -->
        <form action="./backend/compras.php?opt=add" method="post">
            <div class="row">
                <div class="col-md-6 mb-3">
                    <label class="form-label">Proveedor (Opcional)</label>
                    <select name="person_id" class="form-select">
                        <option value="">-- SELECCIONAR PROVEEDOR --</option>
                        <?php foreach($providers as $p):?>
                            <option value="<?php echo $p["id"];?>"><?php echo $p["name"]." ".$p["lastname"];?> (<?php echo $p["company"];?>)</option>
                        <?php endforeach;?>
                    </select>
                </div>
            </div>

            <table class="table table-bordered">
                <thead>
                    <tr>
                        <th>Producto</th>
                        <th>Precio Unit.</th>
                        <th>Cantidad</th>
                        <th>Subtotal</th>
                        <th></th>
                    </tr>
                </thead>
                <tbody>
                    <?php 
                    $total = 0;
                    if(count($cart) > 0):
                        foreach($cart as $index => $item):
                            // Consultar info del producto
                            $stmt = database::getCon()->prepare("SELECT * FROM product WHERE id=?");
                            $stmt->execute([$item["product_id"]]);
                            $p = $stmt->fetch(PDO::FETCH_ASSOC);
                            $subtotal = $p["price_in"] * $item["q"];
                            $total += $subtotal;
                        ?>
                            <tr>
                                <td><?php echo $p["name"]; ?></td>
                                <td>$<?php echo number_format($p["price_in"], 2); ?></td>
                                <td><?php echo $item["q"]; ?></td>
                                <td>$<?php echo number_format($subtotal, 2); ?></td>
                                <td class="text-center">
                                    <a href="./backend/cartcompra.php?opt=del&id=<?php echo $index; ?>" class="btn btn-danger btn-sm">Eliminar</a>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                    <?php else: ?>
                        <tr><td colspan="5" class="text-center text-muted">No hay productos en la lista</td></tr>
                    <?php endif; ?>
                </tbody>
                <tfoot>
                    <tr class="h4">
                        <td colspan="3" class="text-end">TOTAL:</td>
                        <td class="text-primary">$<?php echo number_format($total, 2); ?></td>
                        <td></td>
                    </tr>
                </tfoot>
            </table>

            <div class="mt-3 text-end">
                <input type="hidden" name="total" value="<?php echo $total; ?>">
                <button type="submit" class="btn btn-success btn-lg" <?php echo (count($cart)==0)?"disabled":""; ?>>
                    Procesar Compra
                </button>
            </div>
        </form>
    </div>
</div>

Para crear el listado de productos vamos a usar un backend de carrito de compras, en el cual se van agregando los productos y al finalizar la venta se consulta este carrito para realizar las insersiones en la vase de datos.

<?php
/**
 * Gestión del Carrito de Compras en Sesión
 **/
session_start();

if(isset($_GET["opt"])){
    
    // Inicializar carro si no existe
    if(!isset($_SESSION["cart_compra"])){
        $_SESSION["cart_compra"] = array();
    }

    // Agregar producto al carro
    if($_GET["opt"]=="add"){
        $product_id = $_POST["product_id"];
        $q = $_POST["q"];

        if(!empty($product_id) && $q > 0){
            $found = false;
            foreach($_SESSION["cart_compra"] as &$item){
                if($item["product_id"] == $product_id){
                    $item["q"] += $q;
                    $found = true;
                    break;
                }
            }
            if(!$found){
                $_SESSION["cart_compra"][] = array("product_id"=>$product_id, "q"=>$q);
            }
        }
    }

    // Eliminar producto del carro
    if($_GET["opt"]=="del"){
        $id = $_GET["id"];
        if(isset($_SESSION["cart_compra"][$id])){
            unset($_SESSION["cart_compra"][$id]);
            // Re-indexar array
            $_SESSION["cart_compra"] = array_values($_SESSION["cart_compra"]);
        }
    }

    // Limpiar carro
    if($_GET["opt"]=="clear"){
        unset($_SESSION["cart_compra"]);
    }

    header("Location: ../?view=nuevacompra");
}
?>

Procesar Compras con PHP y MySQL

Procesar una compra para procesar una compra tenemos el archivo backend/compras.php que guarda y elimina compras. Tanto para crear compras y para elinarlas usaremos transacciones para garantizar que todo salga bien.

<?php
/**
 * Backend para Procesar Compras
 **/
session_start();
require_once("database.php");

if(!isset($_SESSION["user_id"])){
    die("No autorizado");
}

if(isset($_GET["opt"]) && $_GET["opt"]=="add"){
    $db = Database::getCon();

    $person_id = !empty($_POST["person_id"]) ? $_POST["person_id"] : null;
    $total = $_POST["total"];
    $user_id = $_SESSION["user_id"];
    $operation_type_id = 1; // 1 = Compra (Entrada)

    // Obtenemos los productos del carro de sesión
    $cart = isset($_SESSION["cart_compra"]) ? $_SESSION["cart_compra"] : array();

    if(count($cart) == 0){
        die("El carro de compras está vacío.");
    }

    try {
        $db->beginTransaction();

        // 1. Insertamos la cabecera en la tabla sell
        $sql_sell = "INSERT INTO sell (person_id, user_id, operation_type_id, total, created_at) VALUES (?, ?, ?, ?, NOW())";
        $stmt_sell = $db->prepare($sql_sell);
        $stmt_sell->execute([$person_id, $user_id, $operation_type_id, $total]);
        $sell_id = $db->lastInsertId();

        // 2. Insertamos cada producto en la tabla operation
        $sql_op = "INSERT INTO operation (product_id, q, operation_type_id, sell_id, created_at) VALUES (?, ?, ?, ?, NOW())";
        $stmt_op = $db->prepare($sql_op);

        foreach($cart as $item){
            $stmt_op->execute([$item["product_id"], $item["q"], $operation_type_id, $sell_id]);
        }

        // 3. Limpiamos el carro de sesión
        unset($_SESSION["cart_compra"]);

        $db->commit();
        header("Location: ../?view=compras&opt=all");

    } catch (Exception $e) {
        $db->rollBack();
        die("Error al procesar la compra: " . $e->getMessage());
    }
}

if(isset($_GET["opt"]) && $_GET["opt"]=="del"){
    $db = Database::getCon();
    $id = $_GET["id"];

    try {
        $db->beginTransaction();

        // 1. Eliminamos las operaciones asociadas
        $sql_ops = "DELETE FROM operation WHERE sell_id=?";
        $stmt_ops = $db->prepare($sql_ops);
        $stmt_ops->execute([$id]);

        // 2. Eliminamos la cabecera de la venta/compra
        $sql_sell = "DELETE FROM sell WHERE id=?";
        $stmt_sell = $db->prepare($sql_sell);
        $stmt_sell->execute([$id]);

        $db->commit();
        header("Location: ../?view=compras&opt=all");

    } catch (Exception $e) {
        $db->rollBack();
        die("Error al eliminar la compra: " . $e->getMessage());
    }
}
?>

Modulo de Ventas (Bajas de inventario)

El modulo de ventas cumple la funcion de dar salida o baja a las unidades en el inventario, esta validado para que no se puedan dar de baja mas unidades de las que tenemos en el inventario.

Para crear una venta se selecciona un cliente y se crea un listado de los productos de la venta.

Formulario de nueva venta

<?php
$clients = database::getCon()->query("SELECT * FROM person WHERE kind=1")->fetchAll(PDO::FETCH_ASSOC);
$products = database::getCon()->query("SELECT * FROM product WHERE is_active=1")->fetchAll(PDO::FETCH_ASSOC);

// Obtenemos el carro de la sesión
$cart = isset($_SESSION["cart_venta"]) ? $_SESSION["cart_venta"] : array();
?>
<div class="row">
    <div class="col-md-12">
        <h1>Nueva Venta</h1>
        <p>Registrar la salida de mercancía.</p>
        <hr>
        
        <!-- Formulario para agregar productos al carro -->
        <div class="border p-3 mb-3">
            <form action="./backend/cartventa.php?opt=add" method="post">
                <div class="row align-items-end g-2">
                    <div class="col-md-6">
                        <label class="form-label">Buscar Producto</label>
                        <select name="product_id" class="form-select" required>
                            <option value="">-- SELECCIONAR PRODUCTO --</option>
                            <?php foreach($products as $prod):?>
                                <option value="<?php echo $prod["id"];?>"><?php echo $prod["name"];?> ($<?php echo $prod["price_out"];?>)</option>
                            <?php endforeach;?>
                        </select>
                    </div>
                    <div class="col-md-2">
                        <label class="form-label">Cantidad</label>
                        <input type="number" name="q" value="1" min="1" class="form-control" required>
                    </div>
                    <div class="col-md-4">
                        <button type="submit" class="btn btn-primary w-100">
                            Agregar a la Lista
                        </button>
                    </div>
                </div>
            </form>
        </div>

        <!-- Formulario final para procesar la venta -->
        <form action="./backend/ventas.php?opt=add" method="post">
            <div class="row">
                <div class="col-md-6 mb-3">
                    <label class="form-label">Cliente (Opcional)</label>
                    <select name="person_id" class="form-select">
                        <option value="">-- VENTA MOSTRADOR --</option>
                        <?php foreach($clients as $c):?>
                            <option value="<?php echo $c["id"];?>"><?php echo $c["name"]." ".$c["lastname"];?></option>
                        <?php endforeach;?>
                    </select>
                </div>
            </div>

            <table class="table table-bordered">
                <thead>
                    <tr>
                        <th>Producto</th>
                        <th>Precio Unit.</th>
                        <th>Cantidad</th>
                        <th>Subtotal</th>
                        <th></th>
                    </tr>
                </thead>
                <tbody>
                    <?php 
                    $total = 0;
                    if(count($cart) > 0):
                        foreach($cart as $index => $item):
                            // Consultar info del producto
                            $stmt = database::getCon()->prepare("SELECT * FROM product WHERE id=?");
                            $stmt->execute([$item["product_id"]]);
                            $p = $stmt->fetch(PDO::FETCH_ASSOC);
                            $subtotal = $p["price_out"] * $item["q"];
                            $total += $subtotal;
                        ?>
                            <tr>
                                <td><?php echo $p["name"]; ?></td>
                                <td>$<?php echo number_format($p["price_out"], 2); ?></td>
                                <td><?php echo $item["q"]; ?></td>
                                <td>$<?php echo number_format($subtotal, 2); ?></td>
                                <td class="text-center">
                                    <a href="./backend/cartventa.php?opt=del&id=<?php echo $index; ?>" class="btn btn-danger btn-sm">Eliminar</a>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                    <?php else: ?>
                        <tr><td colspan="5" class="text-center text-muted">No hay productos en la lista</td></tr>
                    <?php endif; ?>
                </tbody>
                <tfoot>
                    <tr class="h4">
                        <td colspan="3" class="text-end">TOTAL:</td>
                        <td class="text-primary">$<?php echo number_format($total, 2); ?></td>
                        <td></td>
                    </tr>
                </tfoot>
            </table>

            <div class="mt-3 text-end">
                <input type="hidden" name="total" value="<?php echo $total; ?>">
                <button type="submit" class="btn btn-success btn-lg" <?php echo (count($cart)==0)?"disabled":""; ?>>
                    Procesar Venta
                </button>
            </div>
        </form>
    </div>
</div>

Para crear el listado de productos se usa un carrito de compras, es un array con los datos de proudcto y cantidad que se almacena en una variable de session para garantizar la integridad de los datos.

<?php
/**
 * Gestión del Carrito de Ventas en Sesión
 **/
session_start();

if(isset($_GET["opt"])){
    
    // Inicializar carro si no existe
    if(!isset($_SESSION["cart_venta"])){
        $_SESSION["cart_venta"] = array();
    }

    // Agregar producto al carro
    if($_GET["opt"]=="add"){
        $product_id = $_POST["product_id"];
        $q = $_POST["q"];

        if(!empty($product_id) && $q > 0){
            $found = false;
            foreach($_SESSION["cart_venta"] as &$item){
                if($item["product_id"] == $product_id){
                    $item["q"] += $q;
                    $found = true;
                    break;
                }
            }
            if(!$found){
                $_SESSION["cart_venta"][] = array("product_id"=>$product_id, "q"=>$q);
            }
        }
    }

    // Eliminar producto del carro
    if($_GET["opt"]=="del"){
        $id = $_GET["id"];
        if(isset($_SESSION["cart_venta"][$id])){
            unset($_SESSION["cart_venta"][$id]);
            // Re-indexar array
            $_SESSION["cart_venta"] = array_values($_SESSION["cart_venta"]);
        }
    }

    // Limpiar carro
    if($_GET["opt"]=="clear"){
        unset($_SESSION["cart_venta"]);
    }

    header("Location: ../?view=nuevaventa");
}
?>

Procesar Venta en PHP y MySQL

La opcion procesar ventas es la mas importante del sistema ya que de aqui parte la generacion del inventario y los camculos entradas-salidas.

El algoritmo de procesar ventas verifica que las cantidades que hay en el carrito no sean mayores a las que hay en existencias para evitar que el inventario se valla a numeros negativos.

<?php
/**
 * Backend para Procesar Ventas
 * Powered by Evilnapsis
 **/
session_start();
require_once("database.php");

if(!isset($_SESSION["user_id"])){
    die("No autorizado");
}

if(isset($_GET["opt"]) && $_GET["opt"]=="add"){
    $db = Database::getCon();

    $person_id = !empty($_POST["person_id"]) ? $_POST["person_id"] : null;
    $total = $_POST["total"];
    $user_id = $_SESSION["user_id"];
    $operation_type_id = 2; // 2 = Venta (Salida)

    // Obtenemos los productos del carro de sesión
    $cart = isset($_SESSION["cart_venta"]) ? $_SESSION["cart_venta"] : array();

    if(count($cart) == 0){
        die("No hay productos para vender.");
    }

    try {
        $db->beginTransaction();

        // 1. Validar existencias antes de cualquier inserción
        foreach($cart as $item){
            $p_id = $item["product_id"];
            $q_requested = $item["q"];

            // Calcular stock actual
            $stmt_in = $db->prepare("SELECT sum(q) as total_in FROM operation WHERE product_id=? AND operation_type_id=1");
            $stmt_in->execute([$p_id]);
            $total_in = $stmt_in->fetch(PDO::FETCH_ASSOC)["total_in"] ?? 0;

            $stmt_out = $db->prepare("SELECT sum(q) as total_out FROM operation WHERE product_id=? AND operation_type_id=2");
            $stmt_out->execute([$p_id]);
            $total_out = $stmt_out->fetch(PDO::FETCH_ASSOC)["total_out"] ?? 0;

            $stock_actual = $total_in - $total_out;

            if($q_requested > $stock_actual){
                throw new Exception("Stock insuficiente para uno de los productos.");
            }
        }

        // 2. Insertamos la cabecera en la tabla sell
        $sql_sell = "INSERT INTO sell (person_id, user_id, operation_type_id, total, created_at) VALUES (?, ?, ?, ?, NOW())";
        $stmt_sell = $db->prepare($sql_sell);
        $stmt_sell->execute([$person_id, $user_id, $operation_type_id, $total]);
        $sell_id = $db->lastInsertId();

        // 3. Insertamos cada producto en la tabla operation
        $sql_op = "INSERT INTO operation (product_id, q, operation_type_id, sell_id, created_at) VALUES (?, ?, ?, ?, NOW())";
        $stmt_op = $db->prepare($sql_op);

        foreach($cart as $item){
            $stmt_op->execute([$item["product_id"], $item["q"], $operation_type_id, $sell_id]);
        }

        // 4. Limpiamos el carro de sesión
        unset($_SESSION["cart_venta"]);

        $db->commit();
        header("Location: ../?view=ventas&opt=all");

    } catch (Exception $e) {
        $db->rollBack();
        die("Error al procesar la venta: " . $e->getMessage());
    }
}

if(isset($_GET["opt"]) && $_GET["opt"]=="del"){
    $db = Database::getCon();
    $id = $_GET["id"];

    try {
        $db->beginTransaction();

        // 1. Eliminamos las operaciones asociadas
        $sql_ops = "DELETE FROM operation WHERE sell_id=?";
        $stmt_ops = $db->prepare($sql_ops);
        $stmt_ops->execute([$id]);

        // 2. Eliminamos la cabecera de la venta
        $sql_sell = "DELETE FROM sell WHERE id=?";
        $stmt_sell = $db->prepare($sql_sell);
        $stmt_sell->execute([$id]);

        $db->commit();
        header("Location: ../?view=ventas&opt=all");

    } catch (Exception $e) {
        $db->rollBack();
        die("Error al eliminar la venta: " . $e->getMessage());
    }
}
?>

Modulo de Inventario

Ahora ya generamos ventas, compras vamos a crear la vista de inventario, donde por cada producto vamos a calcular las cantidades existentes compras – ventas.

Los calculos son los siguientes, tomando el id del producto p[“id”].

                    $stmt_in = $db->prepare("SELECT sum(q) as total_in FROM operation WHERE product_id=? AND operation_type_id=1");
                    $stmt_in->execute([$p["id"]]);
                    $total_in = $stmt_in->fetch(PDO::FETCH_ASSOC)["total_in"] ?? 0;

                    // 2. Calculamos salidas (ventas: type=2)
                    $stmt_out = $db->prepare("SELECT sum(q) as total_out FROM operation WHERE product_id=? AND operation_type_id=2");
                    $stmt_out->execute([$p["id"]]);
                    $total_out = $stmt_out->fetch(PDO::FETCH_ASSOC)["total_out"] ?? 0;

                    // Stock final
                    $existencias = $total_in - $total_out;

La vista completa del inventario seria la siguiente.

<?php
$db = Database::getCon();
// Consultamos todos los productos
$query_products = $db->query("SELECT * FROM product WHERE is_active=1");
$productos = $query_products->fetchAll(PDO::FETCH_ASSOC);
?>

<div class="row">
    <div class="col-md-12">
        <h1>Inventario</h1>
        <p>Estado actual de existencias de productos.</p>
        <table class="table table-bordered table-hover">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Producto</th>
                    <th>Precio Entrada</th>
                    <th>Precio Salida</th>
                    <th>Existencias</th>
                </tr>
            </thead>
            <tbody>
                <?php foreach($productos as $p): 
                    // 1. Calculamos entradas (compras: type=1)
                    $stmt_in = $db->prepare("SELECT sum(q) as total_in FROM operation WHERE product_id=? AND operation_type_id=1");
                    $stmt_in->execute([$p["id"]]);
                    $total_in = $stmt_in->fetch(PDO::FETCH_ASSOC)["total_in"] ?? 0;

                    // 2. Calculamos salidas (ventas: type=2)
                    $stmt_out = $db->prepare("SELECT sum(q) as total_out FROM operation WHERE product_id=? AND operation_type_id=2");
                    $stmt_out->execute([$p["id"]]);
                    $total_out = $stmt_out->fetch(PDO::FETCH_ASSOC)["total_out"] ?? 0;

                    // Stock final
                    $existencias = $total_in - $total_out;
                ?>
                    <tr>
                        <td><?php echo $p["id"]; ?></td>
                        <td><?php echo $p["name"]; ?></td>
                        <td>$<?php echo number_format($p["price_in"], 2); ?></td>
                        <td>$<?php echo number_format($p["price_out"], 2); ?></td>
                        <td>
                            <b class="<?php echo ($existencias <= $p['inventary_min']) ? 'text-danger' : 'text-success'; ?>">
                                <?php echo $existencias; ?>
                            </b>
                        </td>
                    </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
    </div>
</div>

¿ Ahora que sigue ?

Les invito a estar pendientes de este articulo ya que lo estare actualizando y optimizando, como pueden ver un articulo de este tamaño requiere tiempo y dedicacion para pulirlo.

Descargar

A continuacion te dejo el proyecto completo para que lo instales, lo uses y sigas aprendiendo.

Link v1.1: https://drive.google.com/file/d/1CIHgtaMmk4jn38drs7jG9TW2E_ipUbBQ/view

¿No quieres construirlo desde cero?

Si llegaste hasta aquí y lo que necesitas es el sistema ya funcionando para entregarlo a un cliente o usarlo en tu negocio, tengo dos opciones listas:

Inventio Lite — Gratis

Sistema de inventario y ventas básico, 100% funcional, código fuente incluido y personalizable.

⬇ Descargar Inventio Lite gratis

Inventio Max — Versión Profesional

Sistema completo con módulos avanzados, dashboard con gráficas, reportes en PDF, múltiples usuarios, roles y mucho más. Código fuente 100% tuyo.

🛒 Ver Inventio Max en la tienda

Inventio Win — Para Windows

Si tu cliente no tiene servidor web, Inventio Win corre directamente en Windows sin configuración.

🖥 Ver Inventio Win

👉 ¿No sabes cuál elegir? Lee: Inventio Max vs Inventio Lite — ¿cuál necesita tu negocio?

Leave a Reply

Your email address will not be published. Required fields are marked *

Discover more from Evilnapsis

Subscribe now to keep reading and get access to the full archive.

Continue reading