LinuxParty
¿Que hemos usado para el proyecto?
-
Anexgrid: para páginar los registros.
-
jQuery UI: basicamente por el tema de autocomplete, el cual me parece bueno y lo he usado bastante tiempo.
-
Arquitectura MVC: ya que es la arquitectura que uso en todos mis proyectos.
Nuestro objeto facturador (Javascript)
Creamos un objeto llamado "facturador" en javascript que implementaba las reglas de negocio necesaria para manipular el DOM y comenzar a facturar, agregando detalle, calculando el monto por cada línea ingresada, el sub total, total y el IVA (IGV en otros países). Este ha sido modificado, ya que ahora los productos que se ingresan los elegimos desde la base de datos usando el autocomplete de jquery ui, adicionalmente, tambíen usamos el autocomplete para seleccionar un cliente.
Nuestro objeto es el siguiente:
var facturador = { detalle: { igv: 0, total: 0, subtotal: 0, cliente_id: 0, items: [] }, /* Encargado de agregar un producto a nuestra colección */ registrar: function(item) { var existe = false; item.total = (item.cantidad * item.precio); this.detalle.items.forEach(function(x){ if(x.producto_id === item.producto_id) { x.cantidad += item.cantidad; x.total += item.total; existe = true; } }); if(!existe) { this.detalle.items.push(item); } this.refrescar(); },
/* Encargado de actualizar el precio/cantidad de un producto */ actualizar: function(id, row) { /* Capturamos la fila actual para buscar los controles por sus nombres */ row = $(row).closest('.list-group-item'); /* Buscamos la columna que queremos actualizar */ $(this.detalle.items).each(function(indice, fila){ if(indice == id) { /* Agregamos un nuevo objeto para reemplazar al anterior */ facturador.detalle.items[indice] = { producto_id: row.find("input[name='producto_id']").val(), producto: row.find("input[name='producto']").val(), cantidad: row.find("input[name='cantidad']").val(), precio: row.find("input[name='precio']").val(), }; facturador.detalle.items[indice].total = facturador.detalle.items[indice].precio * facturador.detalle.items[indice].cantidad; return false; } }) this.refrescar(); }, /* Encargado de retirar el producto seleccionado */ retirar: function(id) { /* Declaramos un ID para cada fila */ $(this.detalle.items).each(function(indice, fila){ if(indice == id) { facturador.detalle.items.splice(id, 1); return false; } }) this.refrescar(); }, /* Refresca todo los productos elegidos */ refrescar: function() { this.detalle.total = 0; /* Declaramos un id y calculamos el total */ $(this.detalle.items).each(function(indice, fila){ facturador.detalle.items[indice].id = indice; facturador.detalle.total += fila.total; }) /* Calculamos el subtotal e IGV */ this.detalle.igv = (this.detalle.total * 0.18).toFixed(2); // 18 % El IGV y damos formato a 2 deciamles this.detalle.subtotal = (this.detalle.total - this.detalle.igv).toFixed(2); // Total - IGV y formato a 2 decimales this.detalle.total = this.detalle.total.toFixed(2); var template = $.templates("#facturador-detalle-template"); var htmlOutput = template.render(this.detalle); $("#facturador-detalle").html(htmlOutput); } }; $(document).ready(function(){ $("#btn-agregar").click(function(){ var producto_id = $("#producto_id"), producto = $("#producto"), cantidad = $("#cantidad"), precio = $("#precio"); // Validaciones if(producto_id.val() === '0') { alert('Debe seleccionar un producto'); return; } if(!isNumber(cantidad.val())) { alert('Debe ingresar una cantidad válida'); return; } else if( parseInt(cantidad.val()) <= 0 ) { alert('Debe ingresar una cantidad válida'); return; } facturador.registrar({ producto_id: parseInt(producto_id.val()), producto: producto.val(), cantidad: parseFloat(cantidad.val()), precio: parseFloat(precio.val()), }); producto_id.val('0'); producto.val(''); cantidad.val(''); precio.val(''); }) $("#frm-comprobante").submit(function(){ var form = $(this); if(facturador.detalle.cliente_id == 0) { alert('Debe agregar un cliente'); } else if(facturador.detalle.items.length == 0) { alert('Debe agregar por lo menos un detalle al comprobante'); }else { $.ajax({ dataType: 'JSON', type: 'POST', url: form.attr('action'), data: facturador.detalle, success: function (r) { if(r) window.location.href = '?c=Comprobante'; }, error: function(jqXHR, textStatus, errorThrown){ console.log(errorThrown + ' ' + textStatus); } }); } return false; }) /* Autocomplete de cliente, jquery UI */ $("#cliente").autocomplete({ dataType: 'JSON', source: function (request, response) { jQuery.ajax({ url: '?c=Comprobante&a=ClienteBuscar', type: "post", dataType: "json", data: { criterio: request.term }, success: function (data) { response($.map(data, function (item) { return { id: item.id, value: item.Nombre, direccion: item.Direccion, ruc: item.RUC, } })) } }) }, select: function (e, ui) { $("#cliente_id").val(ui.item.id); $("#direccion").val(ui.item.direccion); $("#ruc").val(ui.item.ruc); $(this).blur(); facturador.detalle.cliente_id = ui.item.id; } }) /* Autocomplete de producto, jquery UI */ $("#producto").autocomplete({ dataType: 'JSON', source: function (request, response) { jQuery.ajax({ url: '?c=Comprobante&a=ProductoBuscar', type: "post", dataType: "json", data: { criterio: request.term }, success: function (data) { response($.map(data, function (item) { return { id: item.id, value: item.Nombre, precio: item.Precio } })) } }) }, select: function (e, ui) { $("#producto_id").val(ui.item.id); $("#precio").val(ui.item.precio); $("#cantidad").focus(); } }) }) function isNumber(n) { return !isNaN(parseFloat(n)) && isFinite(n); }
Detalle del producto, IVA, sub total y total (JSRender)
Nuestro template ha sufrido cambios tambíen quedando de la siguiente manera.
<script id="facturador-detalle-template" type="text/x-jsrender" src=""> {{for items}} <li class="list-group-item"> <div class="row"> <div class="col-xs-7"> <div class="input-group"> <span class="input-group-btn"> <button type="button" class="btn btn-danger form-control"> <i class="glyphicon glyphicon-minus"></i> </button> </span> <input name="producto_id" type="hidden" value="{{:producto_id}}" /> <input disabled name="producto" class="form-control" type="text" placeholder="Nombre del producto" value="{{:producto}}" /> </div> </div> <div class="col-xs-1"> <input name="cantidad" class="form-control" type="text" placeholder="Cantidad" value="{{:cantidad}}" /> </div> <div class="col-xs-2"> <div class="input-group"> <span class="input-group-addon"> <input name="precio" class="form-control" type="text" placeholder="Precio" value="{{:precio}}" /> </div> </div> <div class="col-xs-2"> <div class="input-group"> <span class="input-group-addon">S/.</span> <input name="precio" class="form-control" type="text" readonly value="{{:total}}" /> <span class="input-group-btn"> <button type="button" class="btn btn-success form-control" class="btn-retirar"> <i class="glyphicon glyphicon-refresh"></i> </button> </span> </div> </div> </div> </li> {{else}} <li class="text-center list-group-item">No se han agregado productos al detalle</li> {{/for}} <li class="list-group-item"> <div class="row text-right"> <div class="col-xs-10 text-right"> Sub Total </div> <div class="col-xs-2"> <b>{{:subtotal}}</b> </div> </div> </li> <li class="list-group-item"> <div class="row text-right"> <div class="col-xs-10 text-right"> IGV (18%) </div> <div class="col-xs-2"> <b>{{:igv}}</b> </div> </div> </li> <li class="list-group-item"> <div class="row text-right"> <div class="col-xs-10 text-right"> Total </div> <div class="col-xs-2"> <b>{{:total}}</b> </div> </div> </li> </script>
Enviando la información del facturador al servidor mediante AJAX
Esto es muy simple, en nuestro ejemplo hemos usado AJAX, y si se fijaron, nuestro objeto facturador, tiene una propiedad llamada detalle, este detalle es lo que debemos enviar al servidor mediante una petición AJAX.
Revicemos la propeidad detalle:
detalle: { igv: 0, total: 0, subtotal: 0, cliente_id: 0, items: [] },
Dentro del detalle, la propiedad items va a guardar todos los productos que vayamos agregando al comprobante.
Entonces, como mencione, debemos pasar el detalle de nuestro facturador al servidor, lo que hice fue agregar un formulario dentro del facturador e implementar un botón del tipo submit, para luego, mediante jQuery declarar el evento SUBMIT y mandar toda la información mediante ajax. Veamos el código:
$("#frm-comprobante").submit(function(){ var form = $(this); if(facturador.detalle.cliente_id == 0) { alert('Debe agregar un cliente'); } else if(facturador.detalle.items.length == 0) { alert('Debe agregar por lo menos un detalle al comprobante'); }else { $.ajax({ dataType: 'JSON', type: 'POST', url: form.attr('action'), data: facturador.detalle, success: function (r) { if(r) window.location.href = '?c=Comprobante'; }, error: function(jqXHR, textStatus, errorThrown){ console.log(errorThrown + ' ' + textStatus); } }); } return false; })
Registrando la factura en al base de datos
Nuestra petición AJAX envía toda la información al servidor, y desde ahí solo debemos hacer los respectivos INSERTS. Nuestro controlador espera lo siguiente:
public function Guardar() { print_r(json_encode( $this->model->Registrar( $_POST ) )); }
Luego nuestro modelo se encarga de registrar dicha información en la base de datos.
public function Registrar($comprobante)
{
try
{
/* Registramos el comprobante */
$sql = "INSERT INTO comprobante(Cliente_id, IGV, SubTotal, Total) VALUES (?, ?, ?, ?);";
$this->pdo->prepare($sql)
->execute(
array(
$comprobante['cliente_id'],
$comprobante['igv'],
$comprobante['subtotal'],
$comprobante['total']
));
/* El ultimo ID que se ha generado */
$comprobante_id = $this->pdo->lastInsertId();
/* Recorremos el detalle para insertar */
foreach($comprobante['items'] as $d)
{
$sql = "INSERT INTO comprobante_detalle (Comprobante_id,Producto_id,Cantidad,PrecioUnitario,Total)
VALUES (?, ?, ?, ?, ?)";
$this->pdo->prepare($sql)
->execute(
array(
$comprobante_id,
$d['producto_id'],
$d['cantidad'],
$d['precio'],
$d['total']
));
}
return true;
}
catch (Exception $e)
{
return false;
}
}
Conclusión
La idea de este proyecto es trabajar todo desde el lado del cliente, es decir, agregar detalle a la factura, buscar un cliente/producto, calcular los montos. Toda esta responsabilidad la tiene ahora nuestro amigo javascript obteniendo un performance bastante alto (porque evitamos hacer las reglas de negocio que mencione en la base de datos) y nuestro servidor solo va a estar preparado para recibir el guardar del comprobante, es decir a PHP solo le interesa que le enviemos la factura lista para guardar, nada más .
PD 1: obviamente faltan validar varias cosas, ya eso es tarea de ustedes.
PD 2: si tienen dudas por favor comenten y no olviden compartir y valorar la publicación.
PD 3: disponemos de un software de venta en el siguiente enlace, el cual podría interesarte.
¡ Actualización !
- Había un BUG en las consultas hacia la base de datos, como nadie me notificó especificamente cual era el error tuve que bajar el proyecto para revisarlo y al final lo encontré. Por eso salia error cargando la data.
- He actualizado el script de la base de datos para que tenga data por defecto
- Cliente por defecto: Eduardo
- Productos por defecto:
- Guitarra eléctrica
- Amplicifador para guitarra eléctrica
Todo debería funcionar bien ahora.
Adjuntos

-
Espacio
- El primer satélite construido con paneles de madera fue lanzado al espacio
- Lo que quise saber del Universo pero la mala divulgación no contó
- ¿Puede existir vida en el espacio sin un planeta? Un nuevo estudio revela una respuesta sorprendente
- Los competidores de SpaceX se esfuerzan por construir cohetes reutilizables
- Descifraron la señal ¡Wow!: y su resultado no te gustará
- Starlink ya está disponible en todos los vuelos Airbus de Hawaiian Airlines
- Hace 4.500 millones de años, otro planeta impactó contra la Tierra. Hemos encontrado sus restos.
- Estudio Revela que las Civilizaciones Avanzadas en la Vía Láctea Podrían ser Extremadamente Raras
- Plutón no regresará, pero los astrónomos quieren redefinir los planetas nuevamente
- La Voyager 1 regresa a las operaciones científicas normales
- China aterriza con éxito una sonda en la cara oculta de la Luna y comienza a recolectar muestras
- Un estudio confirma la predicción de Einstein: los agujeros negros tienen una "región que se hunde"
- La Voyager 1, la primera nave en el espacio interestelar, puede haberse quedado a oscuras
- Adiós al Ingenuity, Perseverence toma una foto del helicóptero dañado y abandonado en Marte.
- Una Kilonova a años luz de la tierra, representa un potencial peligro para nuestro planeta
Comentarios