Mobile POS

Delineamientos de Desarrollo

Instancias de BIMS

BIMS puede distribuirse bajo dos modelos de negocio:

  1. Software as a Service

  2. Licenciamiento Perpétuo

Las cuentas de BIMS bajo el modelo de Software as a Service emplean la misma URL: https://bims.app.

Las instancias de BIMS de cuentas bajo el modelo de licenciamiento perpetuo pueden pueden implementarse en otros hosts. Ejemplo: https://beta.bims.app.

Es por ese motivo que la aplicación móvil debe permitirle al usuario configurar el host ($HOST) de su instancia de BIMS y la opción predeterminada debe ser la de SaaS: https://bims.app.

Para las cuentas SaaS, se debe indicar en el login también el código de empresa ($TENANT_CODE). Este código es alfanumérico. Ej.: "prueba24".

Se recomienda que las casillas de $HOST y de $TENANT_CODE, al no haber necesidad de cambiarlas en la app en un escenario normal, se configuren en una pantalla distinta del login.

Login

Para hacer login en BIMS se requiere de un nombre de usuario ($USERNAME) una contraseña ($PASSWORD) y en el caso de tratarse de una cuenta de SaaS: el $TENTANT_CODE.

Endpoint para Inicio de Sesión

POST https://$HOST/api/users/login

Request Body

NameTypeDescription

user*

String

Nombre de Usuario

password*

String

Hash MD5 de la Contraseña

tenant

String

Código de Tenant

{
    status: "ok",
    code: "200",
    data: {
        User: {
            id: 1,
            login: "demo",
            name: "Cuenta de Demo",
            ...
        },
        Group: {
            id: 1,
            name: "Administradores",
            full: true
            ...
        },
        Session {
            id: "vsknhtsefpt9pmr4pusgtgvdm3",
            expire: "2023-11-01 10:00:00",
            current_timestamp: "2023-11-01 08:00:00",
            ...
        }
    }
}

Ejemplo de Login en cURL

curl --include \
     --request POST \
     --header "Content-Type: application/json" \
     --data-binary "{
          \"user\": \"usuario2\",
          \"password\": \"2fb6c8d2f3842a5ceaa9bf320e649ff0\",
          \"tenant\": \"test123\"
     }" \
'https://bims.app/api/users/login?parse_perms=1';

Manejo de Sesiones

El Id de Sesión

En la respuesta del API a un login existoso, se retorna el Id de la sesión ($SESSION_ID) en el atributo: data.Session.id.

La aplicación debe guardar el valor $SESSION_ID y pasar a las siguientes consultas que haga al API en el argumento de la URL: "sid".

Ejemplo de uso en cURL

Suponiendo que el valor de $SESSION_ID = 7cr7cn9m6k598mv7gdvag2vrq7.

curl "https://bims.app/api/products?sid=7cr7cn9m6k598mv7gdvag2vrq7&company=1&limit=2"

Vigencia de la Sesión

Las sesiones son temporales. En la respuesta al login exitoso, el campo data.Session.expire se indica la fecha de expiración de la sesión, mientras que en el campo data.Session.current_timestamp se indica la marca de tiempo actual como referencia.

Se recomienda actualizar períodicamente el $SESSION_ID con un nuevo login en background y no exigirle al usuario un nuevo login hasta que el mismo cierre su sesión.

Permisos

Para que un usuario pueda usar el POS Mobile debe tener permisos para registrar ventas. Agregá a la URL del login el parámetro "?parse_perms=1" para obtener en la respuesta del login la lista de permisos del usuario en el atributo: data.Group.perms.

Si el valor del atributo data.Group.full es true, entonces el usuario tiene permisos irrestrictos y no es necesario validar nada más. Pero si data.Group.full es false, entonces se debe controlar que el código "salesAdd" esté presente en el array data.Group.perms.

Si el usuario no tiene permisos para registrar ventas, entonces se debe alertar en pantalla y rechazar el login.

Más sobre la gestión de permisos en BIMS aquí: https://ayuda.bims.app/productos/que-son-productos/permisos


Multiempresa

Comportamiento esperado

El sistema es multiempresa, lo que significa que cada instancia de BIMS puede gestionar las operaciones e información de más de una empresa ($COMPANY).

Cada usuario de BIMS puede estar a sociado a una empresa, cuando el valor en User.company_id indica el id de la empresa ($COMPANY_ID); o bien, el usuario puede tener acceso a TODAS las empresas, cuando el valor del campo User.company_id es NULL.

Si el usuario que inició sesión tiene acceso a múltiples empresas, entonces se debe autoseleccionar tras el login la última empresa utilizada, o bien, la primer empresa de la lista de empresas. Y se le debe dar la opción de seleccionar con cuál empresa operar desde una pantalla de configuración.

Se ofrece de muestra la implementación en el PWA actual.


Productos

Estructura de Datos

NombreTipo de DatoDescripción

id

BIGINT

Número Identificador

name

VARCHAR

Nombre del Producto

ptype_id

BIGINT

Categoría de Productos

sell_price

NUMERIC

Precio Principal

company_id

BIGINT

Id de la Empresa

image

VARCHAR

URL de la Imagen del Producto

sellable

BOOLEAN

Bandera de habilitación para la venta

enabled

BOOLEAN

Bander

Hay muchos otros campos en la tabla de productos, pero estos son los relevantes para una implementación sencilla del Mobile POS.

Consulta de Productos

Para consultar productos utilizá el endpoint (api/products) cuya documentación completa está disponible aquí: https://bims1.docs.apiary.io/#reference/0/products/list-products

Ejemplo:

curl "https://bims.app/api/products?sid=pidc5562uluse71b9u0kltj444&webpos=1&company=1"

Siempre indicá el $COMPANY_ID en el argumento "company" en la URL.

También indica siempre el argumento "webpos=1" en la URL para que el backend seleccione los campos necesarios para una implementación de un POS, tal cual lo hace con la versión web del POS.

Paginación de la Consulta de Productos

La lista de productos puede llegar a ser inmensa, algunas veces en término de millones de ítems. Por este motivo es necesario siempre paginar la consulta de productos, y esto se logra haciendo uso de los argumentos de URL: "offset" y "limit". Su uilización es tal cuál se utilizarían en una consulta SQL.

Por ejemplo: si deseáramos consultar la lista completa de productos de 100 en 100, las consultas serían las siguientes:

curl "https://bims.app/api/products?sid=pidc5562uluse71b9u0kltj444&webpos=1&company=1&limit=100&offset=0";
curl "https://bims.app/api/products?sid=pidc5562uluse71b9u0kltj444&webpos=1&company=1&limit=100&offset=100";
curl "https://bims.app/api/products?sid=pidc5562uluse71b9u0kltj444&webpos=1&company=1&limit=100&offset=200";
curl "https://bims.app/api/products?sid=pidc5562uluse71b9u0kltj444&webpos=1&company=1&limit=100&offset=300";

Y así sucesivamente.

Cuándo parar? Una opción rápida es parar cuando el array en el atributo "data" de la respuesta esté vacío. Pero una mejor implementación sería conocer la cantidad total de elementos en la muestra, y así, en función del limit que usemos, determinar cuántas consultas se requerirán. Esto también permitiría darle al usuario una idea del progreso en la sincronización de productos. La cantidad total de elementos se indica en la respuesta a la primera página, cuando offset=0 (o no está definido), y se indica en el atributo "count". Ejemplo:

curl "https://bims.app/api/products?sid=pidc5562uluse71b9u0kltj444&company=1&limit=100&offset=0"

{
    "code": "200",
    "status": "ok",
    "count": "1134",
    "last_update": "2023-10-31 18:29:22",
    "data": [
        {
            "Product": {
                "id": "1",
                "name": "Desarrollo de pagina web basico",
                "company_id": "1",
                ...

Evitá hacer llamados concurrentes.

Obtener solo productos actualizados

Dado el potencial volumen de la lista de productos, es recomendable consultar en cada ciclo solo los productos que fueron creados o modificados desde la última consulta. Esto se puede lograr haciendo uso del argumento "last_update".

En cada respuesta del API se incorpora un atributo llamado "last_update", que es una marca de tiempo del servidor del momento en que se generó la respuesta. Guardá este valor cada vez que comiences a sincronizar productos, y en el siguiente ciclo de sincronización de productos, pasá este mismo valor como argumento en la URL en un atributo con el mismo nombre: "last_update".

Si está presente el atributo "last_update", BIMS retornará solo los registros que fueron creados o modificados luego de esa marca de tiempo.

Ejemplo:

curl "https://bims.app/api/products?sid=pidc5562uluse71b9u0kltj444&company=1&limit=100&offset=0&last_update=2023-10-31 18:29:22"

{
    "code": "200",
    "status": "ok",
    "count": "1134",
    "last_update": "2023-11-01 18:00:01",
    "data": [
        {
            "Product": {
                "id": "1",
                "name": "Desarrollo de pagina web basico",
                "company_id": "1",
                ...

Sincronización de Productos

El POS Mobile apunta a un uso intensivo y debe ser muy ágil. Es por esto que no es recomendable que la búsqueda y exposición de produtos en la app se realice online, en su lugar, debe hacerse contra una base de datos local. Esta base de datos debe mantenerse sincronizada mediante los mecanismos descritos anteriormente.

Igualmente, las imágenes de los productos deben cachearse.

La sincronización de productos debe hacerse por empresa, lo que implicará que debas guardar el last_update del resultado de consultas de productos de cada empresa que se haya realizado.

Clientes

Estructura de Datos

NombreTipo de DatoDescripción

id

BIGINT

Número Identificador Único

name

VARCHAR

Nombre del Cliente

document_id

VARCHAR

Número de Documento

document_type

VARCHAR

Tipo de Documento

notaxes

BOOLEAN

Bandera de Exensión de Impuestos

Consulta de Clientes

Para consultar productos utilizá el endpoint (api/contacts) cuya documentación completa está disponible aquí: https://bims1.docs.apiary.io/#reference/0/contacts/list-contacts

Ejemplo:

Ejemplo:

curl "https://bims.app/api/contacts?sid=pidc5562uluse71b9u0kltj444&webpos=1&company=1"

Siempre indicá el $COMPANY_ID en el argumento "company" en la URL.

También indica siempre el argumento "webpos=1" en la URL para que el backend seleccione los campos necesarios para una implementación de un POS, tal cual lo hace con la versión web del POS.

Paginación de la Consulta de Clientes

La lista de clientes puede llegar a ser inmensa, algunas veces en término de millones de ítems. Por este motivo es necesario siempre paginar la consulta de clientes, y esto se logra haciendo uso de los argumentos de URL: "offset" y "limit". Su uilización es tal cuál se utilizarían en una consulta SQL.

Cuándo parar? Una opción rápida es parar cuando el array en el atributo "data" de la respuesta esté vacío. Pero una mejor implementación sería conocer la cantidad total de elementos en la muestra, y así, en función del limit que usemos, determinar cuántas consultas se requerirán. Esto también permitiría darle al usuario una idea del progreso en la sincronización de productos. La cantidad total de elementos se indica en la respuesta a la primera página, cuando offset=0 (o no está definido), y se indica en el atributo "count".

Obtener solo Clientes Actualizados

Dado el potencial volumen de la lista de clientes, es recomendable consultar en cada ciclo solo los clientes que fueron creados o modificados desde la última consulta. Esto se puede lograr haciendo uso del argumento "last_update".

En cada respuesta del API se incorpora un atributo llamado "last_update", que es una marca de tiempo del servidor del momento en que se generó la respuesta. Guardá este valor cada vez que comiences a sincronizar productos, y en el siguiente ciclo de sincronización de productos, pasá este mismo valor como argumento en la URL en un atributo con el mismo nombre: "last_update".

Si está presente el atributo "last_update", BIMS retornará solo los registros que fueron creados o modificados luego de esa marca de tiempo.

Formas de Pago

Definición

Una venta puede ser abonada con una o la combinación de varias formas de pago. Estas formas de pago se configuran en BIMS y su configuración reúne la lógica del tratamiento de los fondos recibidos con esa forma de pago.

Ejemplos de Formas de Pago: Efectivo, Cheques, Transferencias Bancarias.

Estructura de Datos

NombreTipo de DatoDescripción

id

BIGINT

Identificador Numérico Único

name

VARCHAR

Nombre de la Forma de Pago

Existen muchos otros atributos en la estructura de las Formas de Pago, pero para la construción del PMV bastan estos.

Consulta de Formas de Pago

Para consultar las formas de pago configuradas en BIMS, hacé uso del endpoint: "api/payment_methods", cuya documentación completa está disponible aquí: https://bims1.docs.apiary.io/#reference/0/payment-methods/list-payment-methods

La lista no será larga, así que no será necesario paginarla, pero sí se recomienda el uso del last_update.

Sucursales y Puntos de Venta

Una empresa en BIMS puede tener asociados uno o más establecimientos. Y cada establecimiento, a su vez, puede tener asociados uno o más puntos de venta (cajas). El POS Mobile siempre estará operando en una caja determinada, por lo que será necesario que el usuario, en el contexto de la configuración de la app, la pueda seleccionar.

Posteriormente, cuando se registre una venta, se deberá indicar en el objeto de la venta los ids de la sucursal y del punto de venta.

Estructura de Datos

NombreTipo de DatoDescripción

id

BIGINT

Identificador Numérico Único

name

VARCHAR

Nombre de la Forma de Pago

Para ambas entidades nos interesa de momento solo estos dos atributos.

Consulta de Sucursales

Ejemplo:

curl "https://bims.app/api/agencies?sid=pidc5562uluse71b9u0kltj444&company=1"

Consulta de Puntos de Venta

Ejemplo:

curl "https://bims.app/api/posales?sid=pidc5562uluse71b9u0kltj444&company=1"

Ventas

En este punto estamos listos para configurar una venta sencilla.

El endpoint para el registro de ventas está documentado aquí: https://bims1.docs.apiary.io/#reference/0/sales/create-a-new-sale

Comportamiento Esperado

El usuario deberá poder buscar / seleccionar los productos de una grilla, luego deberá poder buscar / seleccionar clientes de una lista.

Seguidamente el usuario deberá determinar la condición de venta, entre las opciones "Contado" y "Crédito".

Si se ha indicado la condición de venta: "Contado", se deberá determinar una o más formas de pago. Los subtotales por forma de pago deben igualar el valor total de la venta.

Si se ha indicado la condición de venta: "Crédito", se deberá configurar una fecha de vencimiento.

En la versión definitiva del Mobile POS, el regitro de la venta deberá hacerse offline, y sincronizarse a posteriori a la nube de BIMS. Sin embargo, esto requerirá la implementación de un amplio set de controles que no se recomiendan para un PMV. Así que en esta primer versión haremos el registro de la venta y su facturación en linea. En el atributo data.Sale.invoice_number indicá: "auto". Con esto se dejará a BIMS la tarea de asignar un número de factura que se retornará en la respuesta del API.

Con la respuesta del API se deberá confeccionar una factura para su impresión en ticket. Aunque BIMS admite la configuración de múltiples plantillas de impresión de facturas, para el Mobile POS admitiremos solo un diseño estándar. Ejemplo:


Registros de Pedidos

Definición de Pedidos

¡Felicitaciones! A este punto, el POS ya tiene capacidad de registrar ventas 🥳. Ahora trabajemos en otra operación que debe soportarse: Pedidos. Los Pedidos son operaciones que, a posteriori, se convertirán en ventas. La entidad en el sistema es la misma que la de las ventas: "sales", solo cambian algunos atributos en la composición del objeto:

  1. El atributo Sale.status lleva el valor "pending".

  2. No se establecen formas de pago [SalesPaymentMethod] ni planes de pago [SalesPnote].

Endpoint del API en el Backend

Se utiliza el mismo endpoint que para el registro de ventas, cuya documentación completa está disponible aquí: https://bims1.docs.apiary.io/#reference/0/sales/create-a-new-sale

Ejemplo:

curl --include \
     --request POST \
     --header "Content-Type: application/json" \
     --data-binary "{
    \"Sale\": {
        \"contact_id\": 351,
        \"company_id\": 1,
        \"agency_id\": 1,
        \"posale_id\": 1,
        \"currency_id\": 3,
        \"status\": \"pending\",
	\"preorder\": true,
        \"billed\": true,
        \"credit\": false,
	\"amount\": 220000
    },
    \"SalesProduct\": [
        {
            \"product_id\": 3433,
            \"quantity\": 1,
            \"price\": 220000
        }
    ]
}" \
'https://beta.bims.app/api/sales/?sid=kroi8vnrp20ojhhnjsgam7u823'

{
    "code": "200",
    "status": "ok",
    "data": {
        "Sale": {
            "id": "5842",
            ...
            "issue_date": "2024-03-11",
            ...
            "amount": "220000",
            "paid": "0",
            "currency_id": "3",
            "company_id": "1",
            "void": false,
            "created": "2024-03-11 12:21:42",
            "modified": "2024-03-11 12:21:42",
            "agency_id": "1",
            "contact_id": "351",
            ...
            "status": "pending",
            "billed": true,
            "credit": false,
            ...
            "preorder_number": "1399",
            "preorder_status": "confirmed",
            ...
        },
        "Contact": {
            "id": "351",
            "name": "JOSU\u00c9 V\u00c1SQUEZ",
            "document_id": "57070067"
        },
        "SalesProduct": [
            {
                "id": "14667",
                "sale_id": "5842",
                "product_id": "3433",
                "quantity": "1",
                "price": "220000",
                "cost": "176000",
                "created": "2024-03-11 12:21:43",
                "tax_id": "8",
                "tax_rate": "10",
                "tax_amount": "20000",
                ...
                "Product": {
                    "id": "3433",
                    "name": "producto prueba 6",
                    ...
                },
            }
        ],
    }
}

Interfaz de Usuario

Se utilizará el mismo flujo actual para el registro de operaciones de Ventas. En la pantalla del Carrito, hoy se presenta un solo botón con el título "Checkout". Este botón lleva a una pantalla donde el usuario determina la forma de pago y cierra y factura la venta. Renombrar el botón "Checkout" por "Facturar" e incoporar un segundo botón con el título "Crear Pedido". El botón "Crear Pedido" registrará directamente el Pedido en el backend y al recibir una confirmación de la transacción del backend, expondrá la pantalla de feedback al usuario con las opciones de imprimir el Pedido o regresar al home.

Lista de Pedidos

De la misma forma en que hoy se listan las Ventas registradas desde el POS, se debe listar los Pedidos en una opción diferente.

Donde en las pantallas lista y vista de Ventas se muestra el número de factura [Sale.invoice_number], en el caso de las pantallas de Pedidos se mostrará el número de Pedido [Sale.preorder_number] y además el nombre del cliente Contact.name.

Debe poderse buscar Pedidos a partir del número de pedido o del nombre del Cliente haciendo uso de la casilla de búsqueda en la parte superior de la pantalla.

En la pantalla de vista (detalle) del Pedido, se debe incluir las opciones de reabrir el Pedido en modo de edición. Esta opción configurará la orden actual con los datos del Pedido, incluyendo su id [Sale.id]. En la pantalla del Carrito, el título del botón "Crear Pedido" se cambiará a "Guardar Pedido". Su función será la misma, solo se añadirá el valor Sale.id al JSON enviado al backend, con el objeto de modificar el registro en lugar de crear uno nuevo.

Y la opción de facturación seguirá su curso normal con los siguientes cambios:

  1. Se añadira el valor Sale.id al JSON enviado al endpoint del API del backend [api/sales]. De esta manera, en lugar de crearse una nueva operación, se editará la preexistente.

  2. Se establecerá el valor de Sale.status a "approved".

Impresión de Pedidos

La impresión de pedidos debe tener la siguiente estructura:


Fixes Requeridos

Aplicación de Descuentos en Pedidos

En la pantalla de cierre del Pedido está presente un campo de descuentos. El valor ahí ingresado debe aplicarse.

En el POST al API el descuento se ingresa en el campo SalesProduct.discount_amount, y se resta del precio unitario en SalesProduct.price. Entonces, por ejemplo, dado el caso de un ítem con las siguientes características:

  • Id de Producto: 1

  • Precio Unitario: 60.000

  • Cantidad: 2

  • Descuento: 10.000

Los objetos dentro del array SalesProduct deberán construirse según el siguiente modelo:

SalesProduct: [
    {
        product_id: 1,
        price: 55000,
        quantity: 2,
        discount_amount: 10000
    }
]

El valor del precio unitario se ajusta según:

SalesProduct.price = SalesProduct.price - SalesProduct.discount_amount / SalesProduct.quantity

Corrección de Facturas con Descuentos

La factura para impresión generada por la app para una venta con descuentos, aplica dos veces el valor de descuento para el cálculo del total.

En el caso de abajo, la venta tiene un precio unitario de 120.000, y se aplica un descuento de 100.000. Sin embargo, se expone como precio unitario 100.000 y se le vuelve a aplicar el descuento dejando el valor final en 90.000.

Además, a la linea del descuento en la factura le falta la columna de "Descripción", donde debe llevar la palabra "Descuento".

Aplicar el mismo criterio para la impresión de Pedidos.

Liquidación de Impuestos en la Factura

En la factura generada actualmente se muestra la siguiente sección:

Añadir como título de la sección: "Liquidación de IVA". Anteponer la palabra "IVA" a las tasas de cada impuesto. Ejemplo: "IVA 10%", "IVA 5%". Suprimir la linea "EXENTO" y en su lugar incluir la suma de los valores de los impuestos con la etiqueta: "Total IVA:".

Campo "Observaciones" en Pedidos

Al registrar una venta en la app, en la pantalla de Checkout está presente el campo "Observaciones" (Sale.notes). Sin embargo, el mismo campo no se presenta al registrar un Pedido debido a que no se ingresa a la pantalla de Checkout. Pero las notas son una información importante para los Pedidos por lo que se requiere incoporar a estos, y para eso, deberá moverse el campo a la pantalla anterior al Checkout, la del Carrito.

El valor del campo se debe guardar en Sale.notes.

Cambios en Pantalla de Detalle de Pedidos y Ventas

Indicar para cada ítem de la venta los siguiente datos: Nombre del Producto, Precio Unitario, Cantidad, Subtotal (Precio Unitario * Cantidad).

  1. Exponer las notas del Pedido / Venta (Sale.notes).

  2. Ejecutar cambios estéticos según muestra a continuación o sugerir otra opción.

Fecha de Vencimiento de la Factura

Cuando en la pantalla de Checkout se indica como Condición de pago: "Crédito", se expone un campo de tipo fecha para el ingreso de la fecha de vencimiento.

Sin embargo, por error se envía al backend siempre la fecha predeterminada. Es necesario enviar al backend la fecha configurada por el usuario en el array de objetos SalesPnote, en el atributo expiration_date del único objeto contenido, según el ejemplo a continuación:

"SalesPnote":[
    {
        "expiration_date":"2024-05-02",
        "amount":2000
    }
]

Dígito Verificador del RUC en Factura Impresa

El dígito verificador de un número de documento es el resultado de la ejecución de una función matemática en base al número de documento como argumento. El dígito verificador (o de validación) es el que se indica a la derecha del guión en un RUC. Ejemplo: 80074954-5, donde el dígito verificador del RUC "80074954" es "5".

En BIMS, por lo general, el número de documento (RUC) del cliente se registra sin el dígito verificador. Esto con el objeto de ir calculándolo en la interfaz de usuario mientras el usuario lo va escribiendo, de manera que sirva como ayuda visual para la validación de ingreso del usuario. En muy raros casos, el documento registrado ya incluye el dígito verificador, con el guión incluído.

En la factura que se genera para impresión desde la aplicación, si el número de documento del Cliente no incluye el dígito verificador, y es un valor numérico, se debe calcular el dígito verificador y añadirlo al RUC en la impresión con un guión que separe el RUC de su dígito verificador.

Abajo se indica la función para generar el dígito verificador en JavaScript.

function dv_py(p_numero, p_basemax = 11) {
	p_numero = ''+p_numero;
	var v_total = 0;
	var v_resto;
	var k;
	var v_numero_aux;
	var v_numero_al = '';
	var v_caracter;
	var v_digit;

	for (i=0; i < p_numero.length; i++) {
		v_caracter = p_numero.substring(i, i+1);
		if ( v_caracter.charCodeAt(0) < 48 && v_caracter.charCodeAt(0) > 57) {
		        v_numero_al = v_numero_al + v_caracter.charCodeAt(0);
		}
		else {
			v_numero_al = v_numero_al + v_caracter;
		}
	}
	k = 2;
	v_total = 0;

	for (i = v_numero_al.length - 1; i >= 0; i--) {
		if (k > p_basemax) {
			k = 2;
		}
		v_numero_aux = v_numero_al.substring(i, i+1);
		v_total = eval(v_total) + eval(v_numero_aux) * k;
		k++;
	}

	v_resto = v_total % 11;

	if (v_resto > 1) {
		v_digit = 11 - v_resto;
	}
	else {
		v_digit = 0;
	}

	return v_digit;
}

Facturación de Valores Exentos

Una factura puede ser exenta de IVA por dos motivos:

  1. El producto vendido es exento de IVA;

  2. El cliente es exento de IVA.

En ambos escenarios la factura generada es incorrecta.

Cuando el Producto Vendido es Exento de IVA

Cuando el Producto vendido es exento de IVA (Product.tax_id = null), el valor del producto no sufre ninguna alteración, simplemente no se gravan impuestos. La aplicación opera correctamente y no es necesaria ninguna adaptación.

Cuando el cliente es Exento de IVA

Cuando es el ciente exento de IVA (Contact.notaxes = true) y el Producto es gravado (Product.tax_id != null) entonces el valor del IVA se resta del precio de venta del Producto.

En el ejemplo de abajo, se factura a un Cliente Exento un Producto con un precio de venta gravado de Gs. 10.000. El valor del IVA es Gs. 909, por lo que el precio final de venta queda en Gs. 9.091 (10.000 - 909). Sin embargo, en la sección de liquidación de IVA, se calcula un IVA sobre ese valor: Gs. 826. Si el cliente es exento, la factura no lleva IVA. Cuando el cliente es exento, la liquidación de IVA debe tener valor: 0. Esto debe corregirse.

Cantidades de Productos No Enteras

La pantalla de Selección de Productos para la venta no admite actualmente el ingresos de cantidades con valores no enteros. Es necesario soportar valores no enteros para las cantidades de los Productos. Como alternativa, se propone un pop-up con un campo de cantidad al hacer tap sobre el valor de la cantidad. Y conservar los comandos "+" y "-" con su funcionamiento actual que suman o restan una unidad a la cantidad actual.

Soporte Offline

Objeto

Hasta este punto, la app permite registrar Pedidos y Ventas que envía al backend de forma síncrona. En otras palabras, cuando se ejecuta el checkout de una Venta o se hace clic en el botón de creación del Pedido se envía los datos de la operación al backend y se espera su respuesta antes de emitir el comprobante. Ahora es necesario enviar la venta al backend de forma asíncrona con el objeto de:

  1. Agilizar la operación, evitando que el usuario deba esperar la respuesta del backend para cerrar la operación, emitir el comprobante y seguir operando.

  2. Poder operar offline.

Modo de Operación

Para esto, es necesario que las ventas se guarden primero en una BD local, y en background se vayan sincronizando con el backend siempre que haya conexión. El estado de sincronización debe indicarse en la lista de ventas y en la lista de pedidos. Se debe informar en la lista los errores de sincronización que pudieran ser retornados por el backend en caso de no poder guardar una transacción.

La causa más frecuente en errores de sincronización de ventas con este modus operandi es el conflicto en la numeración de facturas o de números de pedidos, debida por lo general a la utilización simultánea del mismo punto de expedición por más de una terminal operando offline.

Por este motivo, este modo de operación debe ser optativo y se debe poder configurar en la pantalla de Configuración de la App con el título "Soporte de Operación Offline" y las opciones: "Activado" y "Desactivado".

Emisión de Comprobantes

Tanto para emitir una factura de Venta, como un comprobante de Pedido, se expone información que se recibe actualmente del backend en la respuesta de la transacción. Estos campos son:

A. Para las Facturas de Ventas: Número de Factura (Sale.invoice_number).

B. Para los Pedidos: Número de Pedido (Sale.preorder_number).

Para poder operar con soporte offline, estos valores deben consultarse al backend a través de un endpoint provisto para el caso períodicamente, siempre que exista conexión. Luego se deben ir autoincrementando con cada operación.

En caso que otra terminal se encuentre utilizando el mismo punto de expedición (la misma "Caja" lógica), se recibirá un push informando el nuevo número de comprobante emitido para actualizar localmente el número de comprobante a utilizar en la siguiente emisión. Igualmente, deberá enviar un push cada vez que se genere una venta. La conexión al servidor push se documentará completamente.

Cómo Obtener los Últimos Números de Comprobantes Emitidos

Para determinar qué número de comprobante emitir, es necesario saber cuál fue el último número de comprobante emitido con el timbrado del Punto de Venta en uso. Para consultar este dato, utilizá el endpoint api/posales indicando los argumentos en la URL: id=<id del Punto de Venta> y set_current_invoice_number=1. Ejemplo:

curl 'https://beta.bims.app/api/posales?id=3&set_current_invoice_number=1&sid=...'
{
    "code": "200",
    "status": "ok",
    "data": [
        {
            "Posale": {
                "id": "3",
                "name": "Caja 002",
                "bill_code": "001",
                "latest_invoice_number": "1821",
                "latest_alt_invoice_number": "74"
            },
            "Agency": {
                "id": "1",
                "name": "Casa Matriz",
                "bill_code": "001",
                "address": "Mariscal López 377",
                "city": "Asunción",
                "phone": "021440104"
            },
            "Stamping": {
                "id": "6",
                "code": "411121312",
                "issue_date": "2022-06-17",
                "expiration_date": "2099-12-31",
                "invoice_from": "1",
                "invoice_to": "9999999999"
            }
        }
    ],
    "last_update": "2024-04-23 11:25:22"
}

Y esta es la descripción de los datos que se exponen en el JSON de la respuesta.

CampoDescripciónTipo de Dato

Posale.id

Id del Punto de Venta

INT

Posale.name

Nombre del Punto de Venta

VARCHAR

Posale.bill_code

Código Fiscal del Punto de Expedición

VARCHAR

Posale.latest_invoice_number

Último Número de Comprobante Fiscal (Factura) Emitido

INT

Posale.latest_alt_invoice_number

Último Número de Comprobante No Fiscal Emitido

INT

Agency.id

Id del Establecimiento

INT

Agency.name

Nombre del Establecimiento

VARCHAR

Agency.bill_code

Código Fiscal del Establecimiento

VARCHAR

Stamping.id

Id del Timbrado

INT

Stamping.code

Código de Timbrado

VARCHAR

Stamping.issue_date

Fecha de Emisión del Timbrado

DATE

Stamping.expiration_date

Fecha de Vencimiento del Timbrado

DATE

Stamping.invoice_from

Menor Número de Comprobante Admitido

INT

Stamping.invoice_to

Mayor Número de Comprobante Admitido

INT

Cómo Construir el Número de Factura

El número de factura que se expone en una factura tiene tres partes, separadas entre ellas con un guión y cada parte con una longitud específica, con ceros a la izquierda de relleno según se indica a continuación.

ParteDatoLongitudEjemplo

1

Código Fiscal del Establecimiento

3

001

2

Código Fiscal del Punto de Expedición

3

001

3

Número de Factura

7

0001822

Ejemplo: "001-001-0001822".

Controles

El número de factura a emitir debe estar en el rango de Stamping.invoce_from a Stamping.invoice_to. Si ya se ha alcanzado el valor de Stamping.invoice_to, no debe admitirse la facturación.

Dirección de la Empresa en la Factura

Si la Agency.address está definida, entonces esta debe ser la dirección que se exponga en la factura para la Empresa. Si Agency.address no está definida, usar Company.address.

Soporte de Precios Abiertos

Es posible configurar un producto con precio abierto, lo cual implica que el valor configurado como precio para el producto es un precio sugerido que podrá ser modificado por el usuario al momento de la venta.

El atributo Product.price_open (bool) determina si el precio del producto está abierto. La app debe permitir modificar el precio de un producto al momento de la venta cuando y solo cuando su atributo Product.price_open = TRUE.

La recomendación de implementación en la interfaz de usuario es que al hacer tap sobre un item del Carrito, se abra una ventana de tipo popup con los datos del item: Imagen, Nombre, Precio y Cantidad. Y si el precio es abierto, que se permita ahí modificar el precio unitario.

Last updated