¿Jubilar XP? Ojalá

La primavera la sangre altera y los comerciales salen a cortejar empresas. Esta semana me han tocado un par de ellos intentando venderme planes para jubilar XP. Hasta nuestra empresa de soporte y apoyo me quiere quitar XP. Y, ojo, no es que no tenga ganas de perderlo de vista (ganas que no han hecho sino aumentar desde que lo mandé a paseo de mi viejo equipo personal en 2004). Si por mi fuera, salvo dos equipos virtualizados para administración, lo mandaba bien lejos de la empresa. Pero me encuentro con la dura realidad, que se traduce en dos situaciones bien distintas:

1) Venerables equipos (PIV, 1GB de RAM DDR 400 y cosas así) dando los últimos coletazos de su vida útil. No es que quiera jubilar XP, es que quiero jubilar los ordenadores. Ese hardware ya no me mueve la ofimática elemental de la empresa (Thunderbird, un navegador, LibreOffice y un visor de pdf) con razonable soltura. Si no tirarlos a la basura, pues mandarlos a las profundidades del almacén como terminales tontos, aunque me temo que su futuro es de equipos de becario.

2) Equipos que venían con Windows 7 y sufrieron un downgrade a XP. Los hay que intentaré cazarlos cuando el dueño esté de vacaciones y meterles su 7. Otros son críticos y me temo que tendrán que esperar: son los últimos XP de sus respectivos departamentos y son ab-so-lu-ta-men-te necesarios para el funcionamiento de la empresa. Porque al final las páginas de los bancos y de Hacienda con lo que mejor funcionan es con XP y versiones antiguas de Internet Explorer.

Bueno, y el miniportátil asignado a la impresora 3D, pues no hemos conseguido que su programa renderice bien en un equipo moderno.

En fin, que aún nos queda una temporada con XP en la empresa.

El Lenovo de alta gama que hundió la empresa

Problemón hoy en el curro con caídas de red, incluida telefonía (por ip) y que ha dejado una dolorosa baja, uno de los Lynksys que atesoramos. Desconozco aún si el causante del jamacuco del router fue el mismo del colapso de la red por la tarde, pero descubrir al causante de este último estropicio, que se ha llevado por delante toda nuestra red, ha sido un auténtico quebradero de cabeza. Primero dimos con el dispositivo causante, un Lenovo de gama alta. Luego, buscar el software responsable. Descartados antivirus, firewall e infecciones varias, todo apuntaba al software del fabricante o a un problema de drivers. Me llamó la atención el nombre de un proceso, Discovery.exe y, tirando de Google, llegué a esto. No era el mismo problema, pero merecía la pena probar y, ¡bingo! Desinstalado el Lenovo EMC Storage Connector (sirva para lo que sirva) y se acabó el problema.

Nunca me han gustado los equipos de marca, pero hoy Lenovo se ha ganado un huequecito especial en el cajón de “odiados profundamente”.

Eficiencia alemana (II): actualizar listas de precios en SAP B1

Problema pintoresco este que menciono. Las veces anteriores, mi compañero había actualizado los precios importando a partir de una hoja Excel desde dentro de SAP, pero, ya sea por mi poca pericia, ya por la actualización a 9.0, he sido incapaz de preparar el maldito Excel. La otra opción, que en su día se dejó por imposible, es importar a través de esa bestia del averno llamada Data Transfer. Así, preparo mi plantilla para los precios, tabla ITM1, con las columnas ItemCode, LineNum, PriceList, Price y Currency. De ellas, ItemCode es el código del artículo, PriceList es el id de la lista de precios. Añadimos el importe y la moneda (EUR) y listo… Uhm, salvo LineNum, que ponemos a 0, que es lo habitual (LineNum, como regla general, es el número de línea por código de la tabla maestra, es decir, que será 0 si sólo vamos a actualizar un precio por artículo, como era el caso).

Bien, pues eso no funciona.

Después de buscar por los foros oficiales, encuentro una respuesta ante este problema que es absurda. O, por lo menos, yo no le encuentro la lógica. Decía así: quite la columna PriceList y en LineNum ponga el id de la lista de precios menos 1.

Y funciona.

Si no quieres vender…

En el trabajo andamos buscando tablets. Descartado Android tras una serie de pruebas, el siguiente paso es Windows. La duda está en si 8, RT o CE. Para no entramparnos en comprar una tablet profesional (cuestan una pasta) decidimos buscar una baratita para pruebas en el departamento y luego decidir. Encontramos la Acer Iconia W3 como candidata ideal. Barata y con Windows 8 (8,1” de pantalla y una memoria interna ridícula, pues sólo vemos la versión de 32GB; vamos, que no me la compraba para mí ni loco).

Mi jefe de departamento se acerca a El Corte Inglés, donde la tienen clasificada como con Windows RT y el vendedor dice que tiene RT (y echa pestes del sistema; entiendo que se pueda considerar “capado” con respecto a Windows 8, pero Android e iOS entran en el mismo saco), así que ante la duda vuelve con las manos vacías.

Consultamos la página del fabricante. Queda bien claro. Procesador Atom, Windows 8… Encontramos de paso otro sitio donde está más barato, así que vamos allí, la pescamos y vuelta.

El tiempo que pierdes porque no te quieren vender.

Operador Update

El otro día necesitaba actualizar valores en una colección de DataRows tipadas y echaba de menos no poder hacerlo con LINQ, así que me puse a buscar. Y buscando, buscando, encontré el código para un método de extensión Update que soluciona la papeleta. El código está aquí en C# y en Visual Basic quedaría así:

Public Delegate Sub Func(Of TArg0)(element As TArg0)

''' <summary>
''' Executes an Update statement block on all elements in an IEnumerable(T) sequence.
''' </summary>
''' <typeparam name=TSource">The source element type.</typeparam>"
''' <param name=source">The source sequence.</param>"
''' <param name=Updater">The update statement to execute for each element.</param>"
''' <returns>The numer of records affected.</returns>
<System.Runtime.CompilerServices.Extension> _
Public Function Update(Of TSource)(source As IEnumerable(Of TSource), Updater As Func(Of TSource)) As Integer
    If source Is Nothing Then
        Throw New ArgumentNullException("source")
    End If
    If Updater Is Nothing Then
        Throw New ArgumentNullException("update")
    End If
    If GetType(TSource).IsValueType Then
        Throw New NotSupportedException("value type elements are not supported by update.")
    End If

    Dim count As Integer = 0
    For Each element As TSource In source
        Updater(element)
        count += 1
    Next
    Return count
End Function

Su uso sería algo así:

TareasParaArticulo.Where(Function(x) x.IdUsuario = 0).Update(Function(x) x.IdUsuario = JefeProyecto)

Donde TareasParaArticulo es un conjunto de datarows tipadas (un list, un datatable…, tanto da). Lo que hace esta parte de la aplicación, para ponernos en situación, es crear una serie de tareas (tomadas de una plantilla de tareas) cuando un artículo es añadido a un proyecto. El uso del Update permite asignar aquellas tareas que en la plantilla tienen como IdUsuario el 0 al jefe del proyecto de una forma sencilla.

Eficiencia alemana

Con la actualización a SAP Business One 9.0 se decidió cambiar la nomenclatura de los almacenes. En la práctica, esto supuso crear almacenes nuevos y mover masimavente la mercancía en el sistema (necesario, de todas formas, para aprovechar el nuevo sistema de ubicaciones). Para evitar que los usuarios metieran accidentalmente mercancía en los almacenes viejos, se marcaron como Inactivos (un bonito check en el formulario de almacenes, que se corresponde con el campo Inactive de la tabla correspondiente). Hasta ahí, sin problemas.

Como baja colateral, el enlace entre el gestor de proyectos (desarrollo propio) y SAP dejó de funcionar. El enlace permite tanto crear artículos en SAP desde el gestor como actualizarlos después y supuso un gran ahorro de trabajo en el departamento, que hasta entonces debía copiar los datos logísticos de un nuevo producto a mano desde el gestor a SAP.

El error que da al actualizar el artículo es que hay almacenes inactivos. Guay. La cosa se pone interesante porque el código del error no aparece en la documentación (he descubierto estos meses que lo raro es que aparezca). Es más, buscando en la documentación de la DI Api (la interfaz de datos que se usa para comunicarse con SAP) el dichoso campo Inactive no aparece en el objeto Almacén.

Vale, puede ser que yo sea un cegato y venga con otro nombre (no es raro), así que me voy a la documentación de la base de datos para ver si la descripción del campo me da alguna pista sobre qué nombre buscar en la DI Api.

Y no lo encuentro. En la documentación de la base de datos de la versión 9.0 no viene, para la tabla maestra de almacenes, el campo Inactive. Por más que exista y por más que tenga un bonito check en el formulario de gestión de almacenes. Con un par.

A eso se le llama “eficiencia alemana”.

Relación maestro-detalle entre dos ComboBox

Me encontré el otro día con la necesidad de establecer una relación maestro-detalle entre dos combobox. Básicamente, tenía que mostrar en un combobox una lista de procesos y, en el otro, las tipologías de cada proceso. Ahí donde lo ven ustedes, no es algo difícil de hacer, pero sí de encontrar cómo hacerlo. Quizás la solución más elegante sea usar un dataset con las dos tablas en cuestión (Procesos y Tipologías) y la relación entre ambas. De este modo, yo puedo asignar la tabla Procesos como DataSource del combobox de la forma habitual:

            cboProceso.DataSource = dsProcesos.Procesos
            cboProceso.DisplayMember = “Descripcion”
            cboProceso.ValueMember = “Id”

Donde cboProceso es el combobox en cuestión y dsProcesos, el dataset tipado. Para la parte detalle de nuestro sistema, esto es, el combo de Tipologías, usamos el mismo DataSource que para el maestro, es decir, la tabla Procesos. La magia la hacemos en el DisplayMember (en el ValueMember no funciona), que lo expresaremos de la formarelación_entre_tablas.campo_a_mostrar. Es decir, el nombre de la DataRelation que une las tablas de nuestro dataset y el nombre del campo de la tabla detalles (Tipologías en nuestro caso) que mostrará el combobox:

cboTipologia.DataSource = dsProcesos.Procesos
‘ Nótese la forma del displaymember para que muestre sólo las tipologías
‘ del proceso seleccionado en el combo de procesos:
cboTipologia.DisplayMember = “FK_Tipologias_Procesos.Descripcion”
cboTipologia.ValueMember = “IdTipologia”

Muy intuitivo, como pueden observar.

El sistema funciona incluso si queremos hacer un filtrado del maestro. Siguiendo con el ejemplo, puedo crear un dataview de Procesos, usar ese dataview como DataSource de ambos combobox y el invento funcionará sin más.

Donde ya no he conseguido que funcione es con un filtrado en la parte detalles. Digamos que necesito mostrar las tipologías que cumplen cierto requisito. Paso 1: filtrar los procesos, sólo necesito aquellos que tengan tipologías que cumplan el requisito pedido. No es una consulta simple que se pueda hacer en el Filter del DataView, pero puedo tirar de LINQ o de métodos de extensión y hacer algo como esto:

dvProcesos = dsProcesos.Procesos.Where( _
                 Function(Proceso) Proceso.GetTipologiasRows.Any( _
                 Function (Tipologia) (Tipologia.OpcionesTipologia _
                              And EOpcionesTipologia.TareaAutomática) _
                              = EOpcionesTipologia.TareaAutomática)).AsDataView
 

Ya tenemos un dataview (dvProcesos) con los procesos que tienen alguna (any) tipología que cumple los requisitos pedidos. Si yo uso este dataview como DataSource de los combobox, obtengo los procesos que busco, pero todas las tipologías de esos procesos. ¿Cómo filtro ahora las tipologías?

Pues ni idea. El objeto DataRelation no me permite “meter” nada en la relación que no sea equivalencia entre columnas y no he encontrado ninguna forma de filtrar el lado “detalle” de nuestra relación. Al final, he tenido que recurrir a dos bindingsources auxiliares, uno ligado al dataview de procesos y el segundo al bindingsource anterior, especificando en su DataMemeber el nombre de la DataRelation:

mbsProcesos.DataSource = dvProcesos
mbsTipologias.DataSource = mbsProcesos
mbsTipologias.DataMember = “FK_Tipologias_Procesos” 

Ahora uso estos bindingsources para “alimentar” los dos combobox y listo. Aunque me hubiera gustado una solución más elegante.

Problema con el correo

Semana plagada de problemas con nuestro proveedor de correo, más un ordenador infectado haciendo de bot y repartiendo spam a espuertas, más problemas con imágenes en las firmas y lusers que no ponen destinatario. Ganas de tirar el teléfono por la ventana o poner un contestador automático que filtre las llamadas por el problema del correo. En estas se acerca un luser con cara de pena.

—No puedo enviar un correo, me da error.

—¿Qué error te da?

—Error al enviar correo, no sé qué de un servidor.

—Necesito la información del error para buscarle solución al problema. Haz una captura de la ventana del error y me la mandas por correo.

Y el luser asiente y se va a su puesto.

Las risas aún resuenan por el departamento.

Y ahora, ¿qué?

Me encuentro dando los últimos retoques a la v3 del SOSNext, un programa de gestión de incidencias que ha crecido tanto que vamos a tener que cambiarle el nombre, porque el módulo de incidencias es ya sólo una pequeña parte del total.

En fin, que estos últimos tres meses le he hecho la cirugía mayor al programa, pasándolo de VB2010 Express a VS2012 (la versión Express ya no daba más), haciéndolo más modular (aumentando la complejidad un montón, de paso), adaptando algunos controles bajados de internet, cambiando algunas cosas de la BDD, metiéndolo todo en Team Foundation Server (Express) para coordinarme con un compañero… Un jaleo enorme.

Y ahora, cuando estoy dando los últimos retoques, un usercontrol que no ha sufrido modificaciones (ni una), que no lo he tocado ni abierto ni cambiado de proyecto o espacio de nombres, que ha pasado desapercibido y no ha recibido ni una mirada de atención, ese cabronazo, digo, ha decidido:

1) Cambiar el alto de las etiquetas y cajas de texto, cortando las letras tipo p, q…

2) Un ListBox con funciones de búsqueda de cadenas (que funciona bien en el resto de controles donde se usa) decide, de paso, funcionar como un ListBox normal (sólo primera letra).

Creo que le he contagiado la gripe, porque otra explicación no se me ocurre.

Validación de filas en DataGridView

Entrada muy rápida y sin código explicativo, pero que quizás sirva a alguien como me ha servido a mí.

El problema es la necesidad de validar los datos que el usuario introduce en un DataGridView. Si controlo el evento RowValidating sólo muestro aviso del error cuando el usuario ha rellenado toda la fila, con el consiguiente problema visual (la celda afectada puede estar fuera de la pantalla visible en ese momento). Si lo que hago es controlar el evento CellValidating, impido que el usuario pueda salir de la celda sin meter un valor válido, lo que puede no interesarme.

Sin embargo, puedo controlar el evento CellEndEdit para ir evaluando los datos introducidos por el usuario y, para aquellos no válidos, usar el ErrorText de la celda en cuestión para avisar del valor no válido. Esto es meramente informativo y usaríamos el evento RowValidating para hacer la validación real de los datos (evitando evaluar nada si IsNewRow es True).