Realizando un informe - Paso a paso - BRIDGE 7
Objetivo
El objetivo de este documento, es explicar paso a paso, como para realizar un informe en BRIDGE.
El ejemplo que tomaremos es el JIRA Nro 8830 definido por Andrea Veronica Asato
Pre requisitos:
- Contar con la historia de usuario y la asignación en JIRA. En este caso, es el siguiente: - NAPSEPD-8830Obtendo detalhes do item... STATUS
- Contar con un entorno de BRIDGE MANAGER, ya que aquí se harán las vistas.
- Contar con un entorno de BRIDGE API, ya que aquí se hará la consulta a la base de datos.
- Contar con una base de datos con información, para ello, se puede usar la base de datos de tienda DEMO o bien, consultar cual se puede utilizar de las que tenemos en BRIDGE PRODUCTO.
Resumen general, para hacer un informe debo (parecen muchos pasos, pero es simple)
- Crear los permisos (en Bridge API)
- Crear el ApiChangeLog que inserte los permisos (en Bridge API)
- Crear el query del informe según las especificaciones (en Bridge API).
- Probar el query enviando diferentes combinaciones de parámetros (usando Postman contra una IP de Bridge Api)
- Crear el service o servicio (en Bridge Manager)
- Crear el controller o controlador (en Bridge Manager)
- Crear los GSP (filtro e index) (en Bridge Manager)
- Listo!
Paso 1 - Creando los JSON en las tablas de permisos (Role, SeqRequestMap , Group y ReportQuery)
Debemos crear un JSON para las siguientes colecciones:
- Role: contiene los roles de la solución.
- SecRequestMap: contiene el permiso para acceder a la ruta del informe, por ejemplo: https://x.x.x.x/reportItem
- Group: contiene roles asociados, aquí debo agregar el nuevo rol a la colección.
- ReportQuery: aquí se referencia al reporte con la URL a la cual debe invocar el servicio. Por ejemplo, se le indica a BM que debe invocar al query a través de /report-item
¿ Que es lo mas fácil?
Usar esa base de prueba que tengo y agregar estos nuevos registros en las tablas.
¿ Por que lo hago así?
Por que al insertarlos, ya me genera un ID y con ese registro, yo puedo ya insertarlo en los archivos JSON de BridgeApi como script de instalación inicial o actualización.
Si nos fijamos en la siguiente imagen, BridgeApi contiene en el directorio Data, archivos JSON que se utilizan para instalar o actualizar Bridge.
Importante: recordar siempre la clave, en este caso, este informe tendrá como clave "reportItems" entonces todo girará en base a dicha clave, ya veremos como mas adelante.
Abrir el Studio 3T, ir a la colección "Role"
tabla role
Este JSON, lo podemos tomar como plantilla y cambiarle los campos roleName, description y name.
{ "name" : "bm-report-items", "roleName" : "ROLE_bm-report-items", "description" : "BM Reportes detalle de ventas por item", "familyRole" : ObjectId("5ea556545604c8593c6029c1"), "enabled" : true, "weight" : NumberInt(1), "version" : NumberInt(0), "homePage" : ObjectId("5ecd2430295f100553dcca22") }
En Studio 3t, lo inserto presionando el botón de "Nuevo"
Una vez insertado, MongDB ya me asignó un ID, por lo que si pido visualizar el JSON, quedará así:
{ "name" : "bm-report-items", "roleName" : "ROLE_bm-report-items", "description" : "BM Reportes detalle de ventas por item", "familyRole" : ObjectId("5ea556545604c8593c6029c1"), "enabled" : true, "weight" : NumberInt(1), "version" : NumberInt(0), "homePage" : ObjectId("5ecd2430295f100553dcca22"), "_id" : ObjectId("61488dc622bf867abb9abf9a") }
Ahora, vamos a poner ese JSON, pero en formato "MongoDB" dentro del archivo role.json presente en BridgeApi en el directorio /data/structural/role.json
Importante: al exportar el JSON para meterlo en la colección role.json , debo hacerlo en formato "MongoExport Format" tal como indica la siguiente foto:
Quedaría así:
TABLA SEQREQUESTMAP
Ahora, debemos insertar un SecRequestMap, esta tabla es usada por BridgeManager para tener seguridad en la ruta o URL a la que debemos acceder para ver el informe.
Vamos a armar primero el JSON para importar, el modelo es el siguiente:
{ "version" : NumberInt(0), "configAttribute" : "ROLE_bm-report-items", "description" : "BM Reportes detalle de ventas por item", "httpMethod" : null, "restrictedAccess" : true, "url" : "/reportItems/index", "securityScope" : "ALL" }
Importante:
- configAttribute debe ser el nombre del rol que cree, en este ejemplo es "ROLE_bm_report_items"
- Description: es una descripción del informe, que seguramente estará en el JIRA
- url: debe tener la URL del controlador de BridgeManager, en este caso, lo llamaremos igual que el rol, en cuanto a la parte del nombre del informe, por ejemplo /reportItems/index.
Una vez creado, viendolo en la tabla de MongoDB, veré que me asignó un ID tambien.
Ahora, vamos a poner ese JSON, pero en formato "MongoDB" dentro del archivo seqRequestMap.json presente en BridgeApi en los directorios /data/central/seqRequestMap.json y /data/store/seqRequestMap.json (son 2 en este caso)
Importante: al copia el JSON, debo elegir el formato MongodbExportFormat tal como se ve en la foto, ese formato es el que debo copiar a los archivos JSON existentes en el directorio data de BridgeApi
Una vez insertados estos nuevos registros en los archivos, se verán así:
TABLA REPORTQUERY
Esta tabla tiene la ruta a la que debe llamar BridgeManager a la hora de invocar el reporte en BridgeAPI
Un modelo de JSON es el siguiente:
{ "reportKey" : "reportItems", "url" : "/report-items" }
Debemos ahora realizar lo mismo que las tablas anteriores.
- Inserto el registro en la tabla de mongoDb (para que me genere un ID)
2. Copio el documento en el archivo json existente en el directorio data/structural/reportQuery.json
Paso 2 - Creando un ApiChangeLog para que BRIDGE API, la próxima vez que inicie, inserte esos registros en la base de nuevos clientes.
Esto sirve para que cuando BRIDGE API se instale en un servidor que no tenía ese informe, se inserten esos registros en la base de datos de ese cliente.
Para ello, debemos agregar un script de cambio o "ApiChangeLog".
Esto se hace en BridgeApi en el directorio src/apiChangeLog/[version]/[fecha-JIRA.ts]
Un modelo de clase apiChangeLog que realiza esto es el siguiente:
/** * * @link https://jira.linx.com.br/browse/NAPSEPD-8830 * @description BM IMPLEMENTAR EL REPORTE DE DETALLE DE VENTA POR ITEMS */ import {getLocationFromCache} from "../../../utils/entityCacheManager"; import { insertManyEntitiesIfNotExistsById, insertManyRolesInGroups, insertManyRolesInUsers, insertOrUpdateManyEntitiesIfNotExistsById, insertOrUpdateManyEntitiesIfNotExistsByKey } from "../../utils"; import {reportQueryTable, roleTable, secRequestMapTable} from "../../../constants/entities.constants"; import {structural} from "../../../constants/replication.constants"; export const description: string = 'BM IMPLEMENTAR EL REPORTE DE DETALLE DE VENTA POR ITEMS' export const up = async () => { /*** * Aqui voy a poner el "name" del rol que cree en el paso 1 * en este caso, para el JIRA 8830 el nombre fue bm-report-items * Recordemos como era el JSON *{ "ref" : NumberInt(1630), "name" : "bm-report-items", "roleName" : "ROLE_bm-report-items", "description" : "BM Reportes detalle de ventas por item", "familyRole" : ObjectId("5ea556545604c8593c6029c1"), "enabled" : true, "weight" : NumberInt(1), "version" : NumberInt(0), "homePage" : ObjectId("5ecd2430295f100553dcca22") } */ const valuesToInsertInRoles = [ "bm-report-items" ] await insertOrUpdateManyEntitiesIfNotExistsByKey(roleTable, 'name', valuesToInsertInRoles, structural) /*** * Ahora voy a insertar el rol creado en los grupos y usuarios creados en la solución * Los IDS que verán abajo, son los de el ROL creado */ const valuesToInsertInGroupsAndUsers = [ "61488dc622bf867abb9abf9a" ] await insertManyRolesInGroups(valuesToInsertInGroupsAndUsers) await insertManyRolesInUsers(valuesToInsertInGroupsAndUsers) /*** * Ahora, inserto los permisos o el permiso, en el caso de hablar del informe * para ello, pongo el ID del permiso de la tabla secRequestMap */ const valuesToInsert = [ '6148a2a722bf867abb9abf9e', ] const location = await getLocationFromCache() await insertManyEntitiesIfNotExistsById(secRequestMapTable, valuesToInsert, location) /*** * Ahora inserto el reporte en la tabla reportQuery para poder invocarlo * desde BridgeManagerCentral */ const valuesToInsertReportQuery = ['6149faf222bf867abb9abfe9'] await insertOrUpdateManyEntitiesIfNotExistsById(reportQueryTable, valuesToInsertReportQuery, structural) }
Nota los comemtarios en el código que indican que IDS hay que poner.
Paso 3 - Creando el query en BRIDGE API y probándolo.
Los queries en BRIDGE API, están en "src/services/reports"
Un query en BRIDGE API es básicamente un servicio que recibe parámetros y retorna una colección de información en formato JSON.
Idealmente, llamar al reporte en BRIDGE API, del mismo modo que llamamos el controlador en Bridge Manager.
En este ejemplo, lo vamos a llamar reportItems.service.js
MUY IMPORTANTE: si el query que debo realizar en MongoDB queda muy complicado ( con muchos joins y cálculos), será bueno analizar si no es conveniente agregar información a la distribución con el objetivo de que el query salga de una única colección (escenario ideal) o a lo sumo, de 2.
Si saliese de mas de 1 colección, en la que hacemos JOIN, debemos asegurarnos que tengamos un índice por ese JOIN.
¿ Con que base probamos?
Debo bajarme una base con información, puede ser la que tenemos en demo.napse.global, haciendo un backup de la base o buscar alguna que tenemos en producto.
Es importante probar el informe haciendo transacciones desde el POS, eso me va a dar la seguridad de que los datos son correctos.
Doy un ejemplo de prueba de un informe con el postman
Recordemos que primero debo obtener el token para pedir el informe.
En esta etapa, ya contamos con:
- Los permisos creados en las tablas Role, Group y SeqRequestMap
- El informe creado en BridgeApi
- Pruebas de que el informe funciona correctamente invocándolo como servicio
Nos queda
- Agregar el reporte en el menú de Bridge Manager
- Crear la pantalla en Bridge Manager
- Invocar el servicio desde un controlador
Paso 3 - Agregando el reporte en el menú de Bridge Manager
Ahora, pasamos al código de BRIDGE MANAGER (ya no utilizamos mas Bridge Api)
Tomamos blue.gsp , presente en el directorio "layouts" y agregamos la línea en la que será llamado el informe.
Por ejemplo:
<sec:access controller='reportItems' action='index'> <li> <a href="${request.contextPath}/reportItems/index"> <span class="icon-align-left"></span> <g:message code="report.reportItems"/> </a> </li> </sec:access>
Importante: este acceso va en 2 lugares del blue.gsp. Uno es el menú izquierdo y el otro, el menú superior.
Ya podremos acceder al menú en la izquierda y arriba, si notamos, hace referencia al controller, que está protegido por los permisos en la tabla secRequestMap que agregué antes.
Importante:
- Al controlador lo llamaremos reportItemsController, al servicio reportItemsService y a las vistas o pantallas reportItems
Notemos que este nombre, es el mismo que le puse al servicio de BRIDGE API, el mismo que puse en los permisos. Esto ayudará a que aquel que el día de mañana debe cambiar algo en el informe, le resulte simple.
Paso 4 - Agregamos Servicio, Controller y GSP
agregamos el servicio (en el directorio services)
El servicio respetará el estandard de nombre que definimos antes, en este caso se llamara reportItemService
Un ejemplo de este servicio es el siguiente:
package sts.console.reporting import grails.compiler.GrailsCompileStatic import grails.gorm.transactions.Transactional import grails.util.Holders import grails.web.databinding.DataBinder import groovy.json.JsonBuilder import groovy.sql.GroovyResultSet import groovy.sql.Sql import groovy.transform.TypeCheckingMode import sts.console.base.Store import sts.console.general.Till import sts.console.general.TillOperator import sts.console.general.TillWorkstation import sts.console.general.ixretail.TillWorkstationCategory import javax.servlet.http.HttpSession import javax.sql.DataSource @Transactional class ReportItemsService implements ReportServiceTrait, DataBinder { @GrailsCompileStatic(TypeCheckingMode.SKIP) def getReport(Integer fromDate, Integer toDate, String store, String tillOperator, String tillTerminal, String items, String draw, HttpSession session) { def resultMap = [draw: '', recordsTotal: '', recordsFiltered: '', data: ''] try{ def parameterMap = [:] parameterMap.storeCode = store; parameterMap.dateAsIntFrom = fromDate; parameterMap.dateAsIntTo = toDate; parameterMap.tillOperator = tillOperator; parameterMap.tillTerminal = tillTerminal; parameterMap.items = items; def jsonResult = bridgeCoreRestService.executeServiceCall(buildUrlReport('reportItems'), new JsonBuilder( parameterMap ).toPrettyString(), session) resultMap.draw = draw; resultMap.recordsTotal = jsonResult.target.result.size() resultMap.recordsFiltered = jsonResult.target.result.size() resultMap.data = jsonResult.target.result return resultMap } catch(Exception ex){ log.error("ERROR al ejecutar el informe reportItems : " + ex.message) return resultMap } } }
Es muy simple, recibe parámetros (los que corresponden al informe) e invoca una clase (es siempre la misma clase).
Los servicios, a excepción de los parámetros, son todos iguales.
agregamos el controlador (en el directorio controllers)
El Controller se llamará como indicamos, con el mismo nombre que definimos al inicio.
Un ejemplo de un controller para este reporte llamado reportItems es el siguiente:
package sts.console.reporting import grails.converters.JSON class ReportItemsController implements ReportTrait { def reportItemsService def reportCommonService def index() { this.applyDefaultParams() render(view: 'index', model: [params: params, stores: this.getStores(), storeDisabled: !this.isCentral(), storeSelected: this.getIdStore(), tillOperator: this.getTillOperator(), tillTerminal: this.getTillWorkstation()]) } def ajaxReport(){ if(params.first == "1") render reportCommonService.returnEmpty() as JSON; String store = (this.isCentral())?params.storeSelected.toString():this.idStore; String tillOperator = params.tillOperator; String tillTerminal = params.tillTerminal; String items = params.items; String fromDate = params.fromDate.toString().substring(6,10) + params.fromDate.toString().substring(3,5) + params.fromDate.toString().substring(0,2); String toDate = params.toDate.toString().substring(6,10) + params.toDate.toString().substring(3,5) + params.toDate.toString().substring(0,2); def resultMap = reportItemsService.getReport(fromDate.toInteger(), toDate.toInteger(), store, tillOperator, tillTerminal, items, params.draw.toString(), session); render resultMap as JSON } }
El método "index" es quien muestra la grilla al inicio y le pasa por parámetro, aquellas colecciones o filtros que deben ser cargados al inicio, notar que en este ejemplo le pasa una colección de tiendas, de "Tills del Operador", de "Tills de Terminales"
El método ajaxReport es quien invoca al servicio (en este caso llamado ReportItemService).
Los controladores también son casi todos iguales, solo varía el servicio al que llaman y los parámetros que manejan.
AGREGAMOS las vistas (en el directorio views)
Agregamos ahora las vistas (los GSP) que van a ser 2: los filtros y el informe en si.
Un ejemplo de los filtros:
<%@ page import="sts.console.item.SerializedUnit; sts.console.general.MerchandiseHierarchyLevel; sts.console.item.Item; grails.converters.JSON; sts.console.general.MerchandiseHierarchyGroup" %> <%@ page expressionCodec="raw" %> <div class="form-inline"> <div class="row" style="margin: 5px; width: 80%"> <g:select name="store" class="form-control" noSelection="${['':message(code: 'deptSalesReport.filter.storeId')]}" from="${stores}" disabled="${storeDisabled}" id="store" value="${storeSelected}" optionKey="id" optionValue="name"/> <a data-date="${formatDate(date: params.fromDate, format: 'dd/MM/yyyy')}" data-date-format="dd/mm/yyyy" id="txtFrom" class="btn btn-default" href="#">${formatDate(date: params.fromDate, format: 'dd/MM/yyyy')}</a> <i class="fa fa-arrows-h"></i> <a data-date="${formatDate(date: params.toDate, format: 'dd/MM/yyyy')}" data-date-format="dd/mm/yyyy" id="txtTo" class="btn btn-default" href="#">${formatDate(date: params.toDate, format: 'dd/MM/yyyy')}</a> </div> <div class="row" style="margin: 5px; width: 80%"> <g:select name="tillOperator" class="form-control" width="100px" optionValue="description" optionKey="id" noSelection="${["": message(code: 'reportItems.filter.operator')]}" from="${tillOperator}" id="tillOperator" value="${(params.operatorCode)}"/> </div> <div class="row" style="margin: 5px; width: 80%"> <g:select name="tillTerminal" class="form-control" width="100px" optionValue="description" optionKey="id" noSelection="${["": message(code: 'reportItems.filter.terminal')]}" from="${tillTerminal}" id="tillTerminal" value="${(params.terminalCode)}"/> </div> <div class="row" style="margin: 5px; width: 80%"> <g:textArea name="autocompleteSku" class="form-control multiselect-sku" style="width: 100%" rows="2" placeholder="${message(code: 'common.input.tSku.placeholder')}"/> <g:hiddenField name="sku"/> </div> <input type="hidden" value="1" id="first" name="first"/> </div> <script> jQuery(function ($) { let dateFrom = $('#txtFrom'); let dateTo = $('#txtTo'); dateFrom.datepicker().on('changeDate', function (ev) { const date = new Date(ev.date); dateFrom.text(moment(date).format('DD/MM/YYYY')); dateFrom.datepicker('hide'); }); dateTo.datepicker().on('changeDate', function (ev) { const date = new Date(ev.date); dateTo.text(moment(date).format('DD/MM/YYYY')); dateTo.datepicker('hide'); }); }) let Store = []; jQuery(function () { function getStore() { return Store } function getSource(getDataItems, request, response) { var data = jQuery.map(getDataItems(), function (item) { return { label: item.label, id: item.id, } }) var term = request.term.split(',').pop() response(data.filter(function (item) { return item.label.toLocaleLowerCase().indexOf(term.trim().toLocaleLowerCase()) >= 0 })); } jQuery('input#store').multiselectoptions({ content: '#fsearch', fnSource: getSource, getDataItems: getStore, }); jQuery('textarea[name="autocompleteSku"]').skumultiselectoptions({ content: '#fsearch', url: '${createLink(controller: "reportTrazaSkuSerie", action: "getAllIdItems")}' }); }) </script>
Los filtros, podrían variar de acuerdo a la definición del informe, pero es recomendable copiar el informe y en todo caso, si el filtro no existe, traerlo desde otro.
<%@ page expressionCodec="raw" %> <!DOCTYPE html> <html> <head> <meta name="layout" content="blue"> <g:set var="entityName" value="${message(code: 'report.extraCash', default: 'Adelanto de efectivo')}"/> <title><g:message code="default.list.label" args="[entityName]"/></title> <asset:javascript src="datatables/dataTables.buttons.js"/> <asset:javascript src="datatables/buttons.flash.min.js"/> <asset:javascript src="datatables/jszip.min.js"/> <asset:javascript src="datatables/pdfmake.min.js"/> <asset:javascript src="datatables/vfs_fonts.js"/> <asset:javascript src="datatables/buttons.html5.min.js"/> <asset:javascript src="datatables/buttons.print.min.js"/> </head> <body> <div class="pageheader"> <div class="pageicon"><span class="fa fa-area-chart"></span></div> <div class="pagetitle"> <h1> <g:message code="report.reportItems"></g:message> </h1> </div> </div> <div class="maincontent"> <div id="list-report" class="maincontentinner"> <div class="filter-table-div"> <div class="form-inline margin5"> <g:render template="reportItemsFilter"/> <a href="#" id="btnSearch" name="btnSearch" class="btn btn-info" style="height: 34px;margin-top: -2px;"><i class="fa fa-search"></i></a> </div> </div> </div> <div class="result-table table-responsive"> <table id="MyGrid" class="table table-striped table-bordered MyGrid" style="width:100%"> <tfoot> <tr> <td class="text-left"><b name="totalCount">TOTAL</b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> <td class="text-left"><b name="totalCount"></b></td> </tr> </tfoot> </table> </div> </div> <script> jQuery(document).ready(function(){ showReportMenu('items'); }); function updateFooters() { let formatCurrency = jQuery.fn.dataTable.render.number(Bridge.settings.milesSeparator, Bridge.settings.decimalSeparator, Bridge.settings.decimals) let grossTotals = jQuery('.MyGrid').DataTable().column(11).data().sum(); jQuery(jQuery('.MyGrid').DataTable().column(11).footer()).html('<b>' + formatCurrency.display(grossTotals) + '</b>'); let units = jQuery('.MyGrid').DataTable().column(12).data().sum(); jQuery(jQuery('.MyGrid').DataTable().column(12).footer()).html('<b>' + formatCurrency.display(units) + '</b>'); let discounts = jQuery('.MyGrid').DataTable().column(13).data().sum(); jQuery(jQuery('.MyGrid').DataTable().column(13).footer()).html('<b>' + formatCurrency.display(discounts) + '</b>'); let total = jQuery('.MyGrid').DataTable().column(14).data().sum(); jQuery(jQuery('.MyGrid').DataTable().column(14).footer()).html('<b>' + formatCurrency.display(total) + '</b>'); let iva = jQuery('.MyGrid').DataTable().column(15).data().sum(); if(!iva) iva = 0; jQuery(jQuery('.MyGrid').DataTable().column(15).footer()).html('<b>' + formatCurrency.display(iva) + '</b>'); let vat = jQuery('.MyGrid').DataTable().column(16).data().sum(); if(!vat) vat = 0; jQuery(jQuery('.MyGrid').DataTable().column(16).footer()).html('<b>' + formatCurrency.display(vat) + '</b>'); let netAmount = jQuery('.MyGrid').DataTable().column(17).data().sum(); jQuery(jQuery('.MyGrid').DataTable().column(17).footer()).html('<b>' + formatCurrency.display(netAmount) + '</b>'); } function data (d) { let data = {} data.storeSelected = jQuery('#store').val(); data.fromDate = jQuery('#txtFrom').text(); data.toDate = jQuery('#txtTo').text(); data.first = jQuery('#first').val(); data.tillOperator = jQuery('#tillOperator').val(); data.tillTerminal = jQuery('#tillTerminal').val(); data.items = jQuery('#sku').val(); return Object.assign({}, data, d); } var datatable = null jQuery(document).ready(function ($) { datatable = jQuery('.MyGrid').datatableReport({ appLocale: '${g.getLocaleLanguageTag([:])}', appCurrencyCode: '${g.getCurrencyCode([:])}', minFractionDigits: '${sts.console.config.PropertyValue.readAsInteger("system.minimumFractionDigits", 2)}', contentForSpinner: '#frmOrder', columnDefs: [{ "defaultContent": "-", "targets": "_all" }], updateFootersFunction: updateFooters, columns: [ {data: "storeName", title: "Tienda", render: $.fn.dataTable.render.text(), sortable: true}, {data: "businessDayDate", title: "Fecha.Contable", render: $.fn.dataTable.render.text(), sortable: false}, {data: "beginDateTime", title: "Fecha.Inicio.Transacción", render: $.fn.dataTable.render.text(), sortable: false}, {data: "periodNumber", title: "Periodo", sortable: false}, {data: "sbPeriodNumber", title: "Turno", sortable: false}, {data: "operatorName", title: "Operador", sortable: false}, {data: "terminalCode", title: "Terminal", render: $.fn.dataTable.render.text(), sortable: true}, {data: "trxNumber", title: "Nro.Transacción", render: $.fn.dataTable.render.text(), sortable: true}, {data: "bill", title: "Comprobante", sortable: false}, {data: "internalCode", title: "Codigo", sortable: false}, {data: "description", title: "Item", sortable: false}, {data: "grossSales", title: "$Bruto", render: $.fn.dataTable.render.number(Bridge.settings.milesSeparator, Bridge.settings.decimalSeparator, Bridge.settings.decimals), className: "text-right"}, {data: "units", title: "Unidades", sortable: false}, {data: "totalDiscount", title: "$Descuentos", render: $.fn.dataTable.render.number(Bridge.settings.milesSeparator, Bridge.settings.decimalSeparator, Bridge.settings.decimals), className: "text-right"}, {data: "extendedPrice", title: "$Bruto - $Descuentos", render: $.fn.dataTable.render.number(Bridge.settings.milesSeparator, Bridge.settings.decimalSeparator, Bridge.settings.decimals), className: "text-right"}, {data: "varTotal", title: "$IVA/VAT", render: $.fn.dataTable.render.number(Bridge.settings.milesSeparator, Bridge.settings.decimalSeparator, Bridge.settings.decimals), className: "text-right"}, {data: "taxTotal", title: "$Imp.Internos", render: $.fn.dataTable.render.number(Bridge.settings.milesSeparator, Bridge.settings.decimalSeparator, Bridge.settings.decimals), className: "text-right"}, {data: "netAmount", title: "$Venta.Neta", render: $.fn.dataTable.render.number(Bridge.settings.milesSeparator, Bridge.settings.decimalSeparator, Bridge.settings.decimals), className: "text-right"}, {data: "merchandiseHierarchyGroupCode", title: "Jerarquia(codigo)", sortable: false}, {data: "merchandiseHierarchyGroupName", title: "Jerarquia(nombre)", sortable: false}, {data: "partyCode", title: "Cod.Cliente", sortable: false}, {data: "denomination", title: "Nombre", sortable: false}, {data: "partyIdentificationNumber", title: "Identificacion", sortable: false}, {data: "partyAddressFirstLine", title: "Domicilio", sortable: false}, {data: "partyAddressCity", title: "Ciudad", sortable: false}, {data: "partyAddressState", title: "Estado/Provincia", sortable: false}, {data: "partyAddressPostalCode", title: "CP", sortable: false}, {data: "partyEmail", title: "Email", sortable: false}, {data: "partyTelephone", title: "Telefono", sortable: false}, {data: "partyTaxCategory", title: "Cat.Impositiva", sortable: false}, ], language: { lengthMenu: "${message(code:'datatable.lengthMenu')}", zeroRecords: "${message(code:'datatable.zeroRecords')}", sEmptyTable: "${message(code:'datatable.sEmptyTable')}", sSearch: "${message(code:'datatable.sSearch')}", processing: "${message(code:'datatable.processing')}", }, ajax: { dataSrc: "data", url: "${createLink(controller: 'reportItems', action: 'ajaxReport' )}", type: "post", data: function (d) { return data() }, }, }) $("#btnSearch").on("click", function (event) { $('#first').val("0") const dFrom = $('#txtFrom').text(); const dTo = $('#txtTo').text(); if(!validarFecha(dFrom, dTo, 'El rango de fechas seleccionado es inválido..')) return; datatable.DataTable().clear().draw(); datatable.DataTable().ajax.reload().draw(); } ); }); </script> </body> </html>
Si vemos el informe, también son todos muy parecidos, cambian las columnas que mostramos, el footer (de acuerdo a los campos que debemos totalizar) y la llamada al servicio.
Resumen general, para hacer un informe debo (parecen muchos pasos, pero es simple):
- Crear los permisos (en Bridge API)
- Crear el ApiChangeLog que inserte los permisos (en Bridge API)
- Crear el query del informe según las especificaciones (en Bridge API).
- Probar el query enviando diferentes combinaciones de parámetros (usando Postman contra una IP de Bridge Api)
- Crear el service o servicio (en Bridge Manager)
- Crear el controller o controlador (en Bridge Manager)
- Crear los GSP (filtro e index) (en Bridge Manager)
- Listo!
Formateo números
En el BM se pueden configurar determinadas propiedades para indicar separadores (miles y decimales) y cantidad de decimales a utilizar por la aplicación. Esto se configura en: Configuración → Sistema → Sistema → Setup Inicial.
Estos valores son seteados en Bridge.settings para su utilización general.
De todas formas, se agregaron dos funciones en Bridge.helpers para formateo de decimales y enteros. Las dos funciones son:
- Bridge.helpers.formatNumber: para formateo de decimales
- Bridge.helpers.formatInteger: para formateo de enteros
Estas dos funciones deben utilizarse en el datatable del reporte, tanto para los footers, como para los registros de la tabla en sí.
Ejemplo footer
Ejemplo tabla:
Formateo Fechas
De la misma forma, en el BM se pueden configurar determinadas propiedades para indicar formato de fecha y hora (sin segundos y con segundos). Esto se configura en: Configuración → Sistema → Sistema → Setup Inicial.
Estos valores también son setteados en Bridge.settings para su uso general.
De todas formas, en reportes deberían utilizarse las siguientes funciones:
- Bridge.helpers.formatDate: recibe un moment en string y formatea solo fecha.
- Bridge.helpers.formatDateTime: recibe un moment en string y formatea con fecha y hora
- Bridge.helpers.formatDateWithSeconds: recibe un moment en string y formatea con fecha y hora (con segundos)
- Bridge.helpers.formatDateInt: recibe una fecha en formato int (YYYYMMDD) y la formatea a solo fecha.
Ejemplo de uso en columna
Exportación a Excel y PDF
Indicar Nombre Archivo:
Para indicar el nombre con el cual se exportará el reporte, deberá indicarse al plugin de datatable la propiedad “fileName”. Esto contiene generalmente la misma key utilizada para el titulo del reporte.
Se utiliza de la siguiente forma:
Exportación a excel:
Para la exportación a Excel, hay que tener en cuenta que es necesario indicar qué columnas (letras del Excel) tienen formato decimal o entero, para mantener la configuración utilizada por la aplicacion. Para ello se agregaron dos propiedades en el plugin del datatable: excelDecimalFormatColumns y excelIntegerFormatColumns.
Dado que los separadores de miles y decimales en Excel, dependen de la configuración regional de la maquina donde se abra el archivo, desde nuestra exportación, solo podremos indicar que la celda tiene formato numérico, que queremos usar esos separadores, e indicar cuántos decimales utilizar. Pero luego el uso de “,” (coma) o “.” (punto) para separar decimales o miles, dependerá de dicha configuración regional.
Un ejemplo de como utilizar estas nuevas propiedades:
Exportación a PDF:
En la exportación a PDF hay varios puntos a tener en cuenta:
Tamaño y disposición de la hoja:
Por default, el plugin utiliza tamaño de hoja legal y disposición horizontal. Pero si un reporte requiere una configuración particular, estas propiedades pueden sobreescribirse.
Los nombres de dichas propiedades son pageSize y orientation.
Por ejemplo una forma de sobreescribir la orientación desde un reporte particular sería así:
Alineación de números a la derecha:
En este sentido, nuestro plugin, tomará como referencia cómo se muestra la columna en pantalla. Si la misma tiene el estilo “text-rigth” en pantalla se verá alineado a la derecha y en el reporte PDF también.
Ocultar columnas en la exportación a PDF:
Dado que algunos reportes tienen muchas columnas, se dificulta su exportación a PDF y que todas las mismas entren en la hoja. Es por ello que algunas es necesario que no sean mostradas en el archivo.
Para ello, se creó un estilo noPdfExport, y con indicarle a la columna que lo posee, la misma no será exportada.
Su utilización es como sigue:
Tamaño de cada columna en el archivo:
Tanto para que las tablas no queden “cortadas” en la hoja, como para que las mismas ocupen todo el ancho de la pagina y queden centradas, se decidió especificar en cada reporte, el ancho de cada columna. Este ancho es fijo, y con ello nos aseguramos no tener los problemas antes mencionados.
Para ello se creó una propiedad en el plugin de datatable bridge, llamada pdfColumnsWidth. La misma es un array de tamaños de cada columna. Su utilización es como sigue:
Cabecera con los filtros aplicados del reporte:
Para que en el PDF aparezca en la cabecera una línea con los filtros aplicados, es necesario que en cada reporte se especifique la variable filters y se construya de la siguiente forma en la función js del botón de búsqueda: