Guardando la Configuración de una Instancia de Odoo en un Archivo

Seguimos configurando un entorno de desarrollo para Odoo 10 y tal como hemos visto en el artículo anterior podíamos levantar una instancia de Odoo con algunos parámetros agregados:

python odoo-bin -d odoo10 --addons-path=addons

Odoo tiene múltiples parámetros de configuración, para ver que parámetros podemos utilizar ejecutamos el siguiente comando:

python odoo-bin --help | less

Estar cargando todos estos parámetros cada vez que levantamos nuestra instancia es un poco tedioso, por eso tenemos la opción de guardar un archivo de configuración donde tengamos todos nuestros parámetros establecidos.

Suponiendo que estamos ubicados en la carpeta que contiene el código fuente de Odoo, para generar el archivo de configuración debemos correr el siguiente comando:

python odoo-bin --save --config myodoo.cfg --stop-after-init

De esta manera generamos un archivo de configuración llamado myodoo.cfg(el nombre puede ser cualquiera) que se guardará en la misma carpeta donde está el script odoo-bin.

Es posible guardar este archivo en cualquier ubicación, eso ya depende de nosotros y de como nos ubiquemos para utilizar el script odoo-bin, en nuestro caso siempre creamos el archivo de configuración en la carpeta superior a la que contiene el código fuente de odoo(si seguimos la secuencia del artículo anterior esta carpeta será odoo-dev-10), por lo que haremos lo siguiente:

cd ~/odoo-dev-10
python odoo/odoo-bin --save --config myodoo.cfg --stop-after-init

Si editamos el archivo myodoo.cfg y cambiamos algunos de los parámetros, es posible tomar estos cambios la siguiente vez que iniciemos nuestra instancia usando el parámetro -c, que es una opción abreviada de --config:

python odoo/odoo-bin -c myodoo.cfg

Esto funciona de la siguiente manera, al arrancar, Odoo carga su configuración en tres pasos:

  • Primero se inicializa un conjunto de valores predeterminados para todas las opciones desde el código fuente.
  • A continuación, se analiza la configuración y cualquier valor definido en el archivo reemplaza los valores predeterminados.
  • Finalmente, se analizan las opciones de la línea de comandos y sus valores anulan la configuración obtenida del paso anterior.

Los nombres de las variables de configuración se pueden transformar desde los nombres de las opciones de la línea de comandos, eliminando los guiones principales y convirtiendo los guiones medios en guiones bajos. Hay algunas excepciones en particular, pero para obtener mayor detalle de esto remitanse a la documentación de Odoo o a los libros que hemos recomendado en los artículos anteriores y que estamos usando como guía.

Saludos.

Instalando Entorno de Desarrollo para Odoo 10 en Ubuntu 16.04

Continuando nuestra serie de tutoriales sobre Odoo esta vez nos toca hacer la instalación de un entorno de desarrollo para Odoo sobre Ubuntu 16.04.

Primero instalamos las dependencias necesarias:

sudo apt-get install git python2.7 postgresql nano python-virtualenv

Descargamos e instalamos wkhtmltox, esto es necesario para los reportes en pdf.

wget http://nightly.odoo.com/extra/wkhtmltox-0.12.1.2_linux-jessie-amd64.deb
dpkg -i wkhtmltox-0.12.1.2_linux-jessie-amd64.deb

Ahora instalamos las dependencias de desarrollo:

sudo apt-get install gcc python2.7-dev libxml2-dev \
libxslt1-dev libevent-dev libsasl2-dev libldap2-dev libpq-dev \
libpng12-dev libjpeg-dev

Instalamos NodeJS y su manejador de paquetes:

sudo apt-get install npm
ln -s /usr/bin/nodejs /usr/bin/node

Instalamos el compilador less, a partir de la versión 9.0, el cliente web Odoo requiere el preprocesador less CSS para que las páginas web se representen correctamente, esto depende de Node.js y npm, que hemos instalado en el paso anterior.

npm install -g less less-plugin-clean-css

Configuramos postgresql, primero creamos un usuario con posibilidad de crear bases de datos y que tenga nuestro mismo nombre de usuario de Ubuntu:

sudo -u postgres createuser --createdb $(whoami)

Creamos la base de datos odoo10

createdb odoo10

Configuramos git para poder descargar el código fuente de odoo:

git config --global user.name "Tu nombre"
git config --global user.email "Tu email"

Creamos la carpeta donde vamos a clonar odoo 10:

mkdir ~/odoo-dev-10
cd ~/odoo-dev-10
git clone -b 10.0 --depth=1 https://github.com/odoo/odoo.git odoo
cd odoo

Creamos el entorno virtual para odoo 10 y lo activamos:

virtualenv ~/odoo-10
source ~/odoo-10/bin/activate

Instalamos las python dependencias de Odoo en el entorno virtual:

pip install -r requirements.txt

Ahora levantamos odoo con el siguiente comando:

python odoo-bin -d odoo10 --addons-path=addons
  • El script para inicializar odoo se llama odoo-bin.
  • El parámetro -d es para indicarle que vamos a trabajar con la base de datos odoo10.
  • El parámetro --addons-path es para indicarle las rutas que contendrán los addons de Odoo.

Probamos el correcto funcionamiento de odoo usando nuestro navegador web y poniendo la siguiente dirección en la barra de direcciones:

http://localhost:8069

Nos logueamos con las siguientes credenciales:

  • Usuario: admin
  • Password: admin

En los siguientes tutoriales empezaremos a crear addons para Odoo, hasta la próxima.

Saludos.

Configurando un Proxy Inverso para Odoo 10

Teniendo en cuenta la configuración realizada en nuestro anterior artículo Instalación de Odoo 10 en Producción en esta oportunidad, vamos a configurar un proxy inverso para poder acceder a nuestro odoo mediante el puerto 80, en otro artículo haremos lo mismo para el protocolo https usando el puerto 443, debemos recalcar que en esta oportunidad hemos utilizado como guía el libro "Odoo 10 Development Essentials" de Daniel Reis, entonces manos a la obra.

Odoo puede servir páginas web por si mismo, pero se recomienda tener un proxy Inverso en frente. Un proxy inverso actúa como un intermediario que gestiona el tráfico entre los clientes que envían solicitudes y el servidor Odoo que les responde. El uso de un proxy inverso tiene varios beneficios.

A nivel de seguridad:

  • Manejar (y hacer cumplir) los protocolos HTTPS para cifrar el tráfico
  • Ocultar las características de la red interna
  • Actuar como un cortafuegos que limita las URL aceptadas para el procesamiento

A nivel de rendimiento:

  • Caché de contenido estático, reduciendo así la carga en los servidores Odoo
  • Comprimir contenido para acelerar los tiempos de carga
  • Actuar como un equilibrador de carga, distribuyendola entre varios servidores.

Apache es una opción popular para usar como un proxy inverso, pero en esta oportunidad usaremos Nginx, Nginx es una alternativa reciente con buenas características técnicas y fácil de configurar.

Configurando Nginx como Proxy Inverso

Nginx, escucha en los puertos HTTP predeterminados(80), por lo que debemos asegurarnos que ninguna otra aplicación esté usando estos puertos, probaremos esto con la herramienta curl:

apt-get install curl
curl http://localhost

Si nos sale un mensaje como el siguiente entonces el puerto está libre:

curl: (7) Failed to connect to localhost port 80: Conexión rehusada

Ahora procedemos a instalar nginx:

apt-get install nginx

Para confirmar que está funcionando correctamente debemos ver una página de bienvenida cuando visitemos el servidor con un navegador web o usando curl:

http://IP_SERVER

Los archivos de configuración de Nginx siguen el mismo enfoque que Apache se almacenan en:

/etc/nginx/available-sites/

Y se activan añadiendo un enlace simbólico en:

/etc/nginx/enabled-sites

También debemos desactivar la configuración predeterminada proporcionada por la instalación de Nginx, borrando el archivo default:

rm /etc/nginx/sites-enabled/default

Ahora creamos el archivo para odoo:

nano /etc/nginx/sites-available/odoo

y le agregamos el siguiente contenido:

upstream backend-odoo{
        server 127.0.0.1:8069;
}

server{
        location / {
                proxy_pass http://backend-odoo;
        }
}

Primero, agregamos los upstreams, y los servidores backend Nginx redirigirán el tráfico al servidor Odoo en que está escuchando en el puerto 8069

Añadimos el enlace simbólico:

ln -s /etc/nginx/sites-available/odoo /etc/nginx/sites-enabled/odoo

Para testear si la configuración es correcta hacemos lo siguiente:

nginx -t

Si hay errores revisar si hemos escrito correctamente la configuración en el archivo odoo.

Recargamos nginx:

/etc/init.d/nginx reload

Y visitamos nuestro servidor a través del navegador web:

http://IP_SERVER

Este nos redirigirá automáticamente a Odoo, eso es todo por hoy.

Saludos.

Instalación de Odoo 10 en Producción

Ultimamente estamos trabajando con Odoo 10 y queremos compartir con ustedes la instalación de este genial software sobre un servidor Debian Jessie listo para ponerlo en producción, este es el primer artículo sobre el tema, el segundo estará siendo lanzado en los proximos días. Para escribir este artículo hemos tenido en cuenta los libros "Odoo Development Cookbook" y "Odoo 10 Development Essentials"

Lo primero que debemos hacer es instalar Debian, los pasos para esto no los vamos a tener en cuenta en este artículo, ya que hay muy buenos tutoriales en la red.

Una vez instalado Debian es ingresar como root y actualizar el software:

apt update && apt upgrade

Ahora instalamos los paquetes necesarios que nos permitiran el correcto funcionamiento de Odoo

apt-get install git python2.7 postgresql nano python-virtualenv \
gcc python2.7-dev libxml2-dev libxslt1-dev \
libevent-dev libsasl2-dev libldap2-dev libpq-dev \
libpng12-dev libjpeg-dev node-less node-clean-css \
xfonts-75dpi xfonts-base

En odoo los reportes se generan usando la librería wkhtmltox, por lo que es necesario primero descargarla:

wget http://nightly.odoo.com/extra/wkhtmltox-0.12.1.2_linux-jessie-amd64.deb

Y luego instalarla:

dpkg -i wkhtmltox-0.12.1.2_linux-jessie-amd64.deb

Creamos el usuario odoo:

adduser odoo

Instalamos sudo para poder realizar algunas operaciones necesarias con permisos de superusuario:

apt install sudo

Creamos el usuario de base de datos odoo, esto lo hacemos con el usuario postgres:

sudo -u postgres createuser odoo

Si nos aparece el sguiente mensaje:

could not change directory to "/root": Permiso denegado

No debemos preocuparnos y procedemos a crear la base de datos odoo-project y la asignamos al usuario odoo:

sudo -u postgres createdb -O odoo odoo-project

Ahora nos cambiamos al usuario odoo:

su odoo

Creamos la carpeta odoo-prod donde vamos a guardar todos los archivos y carpetas necesarias de odoo:

mkdir ~/odoo-prod

Fijémonos que antes del nombre de la carpeta tenemos los caracteres "~/", con esto indicamos que no importa la carpeta donde estemos situados actualmente, siempre la creará en el home del usuario en sesión.

Ahora ingresamos a la carpeta creada:

cd ~/odoo-prod

Y dentro de esta creamos las carpetas project y src dentro de project:

mkdir -p project/src

Ingresamos a la carpeta src donde guardaremos el código de odoo:

cd project/src

Ahora clonamos desde github el código fuente de odoo:

git clone -b 10.0 --depth=1 https://github.com/odoo/odoo.git odoo

Observemos el parámetro --depth=1, este parámetro ignora la historia de los cambios y recupera sólo la última revisión de código, haciendo la descarga mucho más rápida.

Creamos un entorno virtual para trabajar con Odoo:

virtualenv ~/env-odoo-10.0

Activamos el entorno virtual:

source ~/env-odoo-10.0/bin/activate

Instalamos las dependencias necesarias para odoo 10:

pip install -r odoo/requirements.txt

Hasta aquí ya podemos correr odoo sin ningún problema usando el script odoo-bin ubicado en el código fuente de odoo, pero continuaremos haciendo algunos pasos adicionales para obtener una mejor performance.

Procedemos a crear la carpeta bin dentro de project:

mkdir ~/odoo-prod/project/bin

Ahora vamos a generar nuestro archivo de configuración de odoo, llamado production.conf, utilizando la siguiente orden:

~/odoo-prod/project/src/odoo/odoo-bin --save --config ~/odoo-prod/project/production.conf --stop-after-init

Los parámetros --save y --config nos permiten crear y guardar el archivo de configuración en la ruta indicada, con el parámetro --stop-after-init paramos la ejecución de odoo y volvemos a la terminal.

Ahora vamos a crear el archivo que arrancará nuestro odoo, le vamos a denominar start-odoo y va a estar ubicado en la carpeta bin:

nano ~/odoo-prod/project/bin/start-odoo

Este archivo es un script que contendrá lo siguiente:

#! /bin/sh
PYTHON=~odoo/env-odoo-10.0/bin/python
ODOO=~odoo/odoo-prod/project/src/odoo/odoo-bin
CONF=~odoo/odoo-prod/project/production.conf
${PYTHON} ${ODOO} -c ${CONF}

El detalle de este archivo se explica de la siguiente manera:

-Tenemos el intérprete del entorno virtual, con esto nos despreocupamos de estar activando el entorno virtual cada vez que querramos arrancar el odoo.

-El script que viene en el código de odoo y que inicia la aplicación, se llama odoo-bin.

-El archivo de configuración de odoo, llamado production.conf

-Y finalmente el llamado a la orden que arranca odoo utilizando los parámetros contenidos en el archivo de configuración.

Le damos permisos de ejecución al script start-odoo

chmod +x ~/odoo-prod/project/bin/start-odoo

Para probarlo arranquemos el script:

~/odoo-prod/project/bin/start-odoo

Y probemos que funciona correctamente poniendo la IP del servidor y el puerto 8069:

http://IP_SERVER_ODOO:8069

Tuneando PostgreSQL

La configuración predeterminada de PostgreSQL es generalmente muy conservadora y tiene la intención de evitar que el servidor de base de datos acapare todos los recursos del sistema. En servidores de producción, se pueden aumentar algunos parámetros en el archivo postgresql.conf para obtener un mejor rendimiento.

Vamos a editar el archivo postgresql.conf:

nano /etc/postgresql/9.4/main/postgresql.conf

Estas son algunas de las opciones que utilizaremos:

max_connections = 100
shared_buffers = 256MB
effective_cache_size = 768MB
work_mem = 10MB
maintenance_work_mem = 64MB
checkpoint_segments = 16
wal_buffers = 8MB
checkpoint_completion_target = 0.9

Para modificar el archivo procedemos a buscar cada uno de los parámetros y lo editamos o lo descomentamos según sea el caso.

Ahora reiniciamos el servicio de base de datos:

service postgresql restart

Editando Archivo de Configuración de Odoo

Ahora vamos a editar el archivo production.conf para modificar los parámetros necesarios para obtener un mejor rendimiento:

nano ~/odoo-prod/project/production.conf

Los parámetros a modificar serán los siguientes:

Cambio del directorio de datos:

data_dir = /home/odoo/odoo-prod/project/data

Cambio del log del servidor:

logfile = /home/odoo/odoo-prod/project/logs/odoo.log

Configuración de la rotación del log, esto permite la rotación de registros. Esto hará que Odoo configure el módulo de registro para archivar los registros del servidor diariamente y mantener los registros antiguos durante 30 días. Esto es útil en servidores de producción para evitar registros que eventualmente consumen todo el espacio disponible en el disco.:

logrotate = True

Configuración de los manejadores de login, esto configura el nivel de registro. La configuración propuesta es muy conservadora y sólo registrará mensajes con al menos el nivel WARNING, excepto werkzeug (CRITICAL) y openerp.service.server (INFO):

log_level = warn
log_handler = :WARNING,werkzeug:CRITICAL,openerp.service.server:INFO

Adaptación de los parámetros de conexión, esto funcionará si está ejecutando el servidor de base de datos PostgreSQL localmente y lo ha configurado como se explicó en anteriormente. Si se está ejecutando PostgreSQL en un servidor diferente, se tendrá que reemplazar los valores False con la configuración de conexión adecuada para su instancia de base de datos:

db_host = False
db_maxconn = 64
db_name = odoo-project
db_password = False
db_port = False
db_template = template1
db_user = False

Configuración del filtro de bases de datos y del listado de bases de datos, esto restringe las bases de datos disponibles para la instancia configurando un filtro de base de datos. Se desactiva el listado de las base de datos, esto no es estrictamente necesario dado que la expresión regular que establecemos en dbfilter sólo puede coincidir con una base de datos única. Sin embargo esto se hace para evitar mostrar la lista de bases de datos y que los usuarios se conecten a la base de datos incorrecta:

dbfilter = odoo-project$
list_db = False

Cambio del master password, la contraseña maestra se utiliza para la administración de la base de datos a través de la interfaz de usuario, y algunos addons de la comunidad también lo utilizan para añadir seguridad adicional antes de realizar acciones que pueden conducir a la pérdida de datos.:

admin_password = mypassword

Configuración de los workers, Odoo creará una serie de procesos de trabajo (en este ejemplo, 4) para manejar las solicitudes HTTP. Esto tiene varias ventajas sobre la configuración por defecto en la cual el manejo de la petición se realiza en hilos separados:

workers = 4
limit_memory_hard = 4294967296 # 4 GB
limit_memory_soft = 671088640 # 640MB
limit_request = 8192
limit_time_cpu = 120
limit_time_real = 300

Si queremos que el sistema cargue datos de prueba debemos mantener el parámetro without_demo en False y si queremos lo contrario lo ponemos a True:

without_demo = False

Arranque Automático

Ahora vamos a configurar systemd para iniciar Odoo automáticamente.

Si aún seguimos logueados con el usuario odoo, debemos salir y regresar a trabajar con el usuario root:

exit

Creamos el archivo llamado odoo.service en /lib/systemd/system/ de la siguiente manera:

nano /lib/systemd/system/odoo.service

Este archivo tendrá el siguiente contenido:

[Unit]
Description=Odoo 10.0
After=postgresql.service

[Service]
Type=simple
User=odoo
Group=odoo
ExecStart=/home/odoo/odoo-prod/project/bin/start-odoo

[Install]
WantedBy=multi-user.target

Registramos el servicio:

systemctl enable odoo.service

Iniciamos el servicio:

service odoo start

Revisamos que al menos esté corriendo:

service odoo status

Y lo paramos:

service odoo stop

Para probar el funcionamiento reiniciemos el servidor y veamos que odoo esté corriendo correctamente:

reboot

Ahora probemos nuevamente:

http://IP_SERVER_ODOO:8069

Y debemos ver la página de inicio del odoo.

El siguiente artículo nos permitirá configurar odoo con nginx y estará muy bueno, hasta la próxima.

Saludos.

Migrando de Wordpress a Nikola

Despues de haber trabajado durante mucho tiempo en Wordpress decidimos usar algo mas pythonesco y encontramos el proyecto Nikola, que se adapta muy bien a lo que nosotros necesitamos como comunidad, asi que decidimos migrar, para ello seguimos la documentacion oficial del proyecto y algunos tutoriales adicionales, en este post vamos a relatar nuestra experiencia, esperando que sea util a nuestros lectores.

Primero creamos nuestro entorno virtual, en esta oportunidad usamos python3 para obtener un mejor rendimiento con Nikola,

python3 -m venv nikola

Activamos el entorno virtual:

source nikola/bin/activate

Antes de iniciar la instalacion de Nikola debimos instalar algunas dependencias para asegurarnos que todo funcione sin ningun problema:

sudo apt-get install python3-dev
sudo apt-get install libxml2-dev libxslt1-dev zlib1g-dev
sudo apt-get build-dep python3-lxml python3-pil

Luego de ello debimos hacer una actualizacion de pip por un problema raro que se nos estaba presentando en la instalacion:

pip install --upgrade pip

Luego procedimos a instalar Nikola y lo hicimos, de acuerdo a la sugerencia de su propia pagina, de la siguiente manera:

pip install --upgrade "Nikola[extras]"

Nos demoramos un poquito, pero finalmente ya teniamos a Nikola instalado en nuestra maquina, como a nosotros no nos interesaba iniciar un sitio desde cero, sino migrar el que ya tenemos en wordpress, hicimos un backup del contenido del blog en wordpress, que es descargado en formato xml, el nuestro se llama pythonpiura.wordpress.2016-06-28.xml y las indicaciones para hacerlo son bastante sencillas y estan explicadas en la documentacion del propio wordpress. Para hacer la migracion de Wordpress a Nikola leimos rapidamente el manual y encontramos que teniamos 3 opciones, probamos las tres para ver cual nos resultaba mejor, pero antes debimos ingresar en la ruta donde tenemos nuestro archivo xml con el backup, debemos mencionar que las tres opciones nos crean una carpeta new_site.

1-La opcion por defecto:

nikola import_wordpress pythonpiura.wordpress.2016-06-28.xml

Esta opcion descarga todo el contenido del blog de wordpress e intenta hacer una conversion de cada uno de los posts y paginas al formato .md, no nos funciono correctamente ya que muchos posts se migraron a medias.

2-Convertir los posts a html:

nikola import_wordpress --transform-to-html pythonpiura.wordpress.2016-06-28.xml

Para ello primero debemos instalar el plugin de conversion a wordpress

nikola plugin -i wordpress_compiler

Esta opcion descarga el contenido y convierte los posts y paginas a html, esta fue la opcion que mejor nos funciono, salvo por el problema con los ejemplos de codigo que tenemos y que llevan la etiqueta:

[sourcecode language="language"]

Hasta donde hemos visto no es posible llevar a cabo la conversion de esto a un formato adecuado asi que simplemente los deja con el texto normal sin darle ningun formato, por lo que este todavia es un tema pendiente de resolver.

3-Dejar el contenido como formato de wordpress, los archivos de los posts y las paginas tienen las extension .wp

nikola import_wordpress --use-wordpress-compiler pythonpiura.wordpress.2016-06-28.xml

Probamos esta opcion pensando que nos solucionaria el problema de la etiqueta [sourcecode], pero funciono igual que la opcion anterior y encima debiamos activar el plugin de wordpress en el archivo de configuracion del sitio.

Como comentamos antes usamos la segunda opcion que nos creo la carpeta new_site con el contenido listo, asi que ahora debiamos construir el sitio:

nikola build

Y lanzar el servidor de pruebas:

nikola serve -b

Cambiamos el tema por defecto por uno que nos parecio mas bonito llamado zen, pueden ver mas temas aqui:

https://themes.getnikola.com

Lo instalamos asi:

nikola install_theme zen

Y lo configuramos en nuestro sitio modificando el archivo conf.py y cambiando la linea:

THEME = "bootstrap3"

por:

THEME = "zen"

y la linea:

NAVIGATION_LINKS = {
        DEFAULT_LANG: (
            ("/archive.html", "Archives"),
            ("/categories/index.html", "Tags"),
            ("/rss.xml", "RSS feed"),
        ),
}

Por:

NAVIGATION_LINKS = {
    DEFAULT_LANG: (
        ('/index.html', 'Home', 'icon-home'),
        ('/archive.html', 'Archives', 'icon-folder-open-alt'),
        ('/categories/index.html', 'Tags', 'icon-tags'),
        ('/rss.xml', 'RSS', 'icon-rss'),
        ('https://getnikola.com', 'About me', 'icon-user'),
        ('https://twitter.com/getnikola', 'My Twitter', 'icon-twitter'),
        ('https://github.com/getnikola', 'My Github', 'icon-github'),
    )
}

Lo anterior es por defecto para que funcione, pero ya luego lo modificamos a nuestro gusto.

Volvimos a construir nuestro sitio:

nikola build

Y a lanzar nuestro servidor:

nikola serve -b

Ahora si nos tocaba deployar nuestro sitio en github:

Para ello creamos un repositorio con el mismo nombre de nuestro usuario "pythonpiura" pero de la siguiente manera:

pythonpiura.github.io

Clonamos este repositorio:

git clone https://github.com/pythonpiura/pythonpiura.github.io.git

Lo siguiente que hicimos, fue copiar el contenido de nuestra carpeta new_site en la carpeta del repositorio recien creado.

Luego procedimos a modificar el archivo conf.py para que tenga los datos de nuetro nuevo sitio, modificando las siguientes lineas:

SITE_URL = "https://pythonpiura.github.io/"
BLOG_EMAIL = "pythonpiura@openmailbox.org"

y tambien las siguientes para que se deploye correctamente en github:

GITHUB_SOURCE_BRANCH = 'sources'
GITHUB_DEPLOY_BRANCH = 'master'
GITHUB_REMOTE_NAME = 'origin'

Agregamos lo siguiente a nuestro archivo .gitignore para que este contenido no sea subido al repositorio:

cache
.doit.db
__pycache__
output

Y corremos el comando:

nikola github_deploy

En este paso github nos solicito nuestro usuario y password, lo ingresamos y continuamos. Tardo varios minutos en subir el contenido pero ya con eso tenemos nuestro blog en la siguiente direccion.

http://pythonpiura.org/

Saludos.

IPython y Jupyter

Despues de un receso hemos regresado y esta vez para hablar de IPython y Jupyter, como todos sabemos cuando estamos aprendiendo Python utilizamos el interprete directamente desde una terminal y vamos probando nuestros programas, al principio todo nos va muy bien pero luego notamos que nos vamos quedando cortos, es alli donde aparece IPython que tal como nos dice su pagina web nos provee una arquitectura rica para computación interactiva con:

  • Una shell interactiva poderosa.
  • Un nucleo para Jupyter.
  • Soporte para visualización de datos interactivos y uso de herramientas GUI.
  • Interprete embebido flexible para cargar en nuestros propios proyectos.
  • Fácil de usar, herramientas de alta performance para computación paralela.

Vamos a instalarlo para ir conociendolo mejor, para ello vamos a crear un entorno virtual, al que llamaremos ipython:

virtualenv ipython

Lo activamos:

source ipython/bin/activate

Ahora procedemos a instalar ipython:

pip install ipython

Para probar que todo ha salido bien ponemos lo siguiente en la terminal:

ipython

Y nos aparecerá algo como esto, que nos indica que todo esta funcionando correctamente:

/images/blog/seleccic3b3n_369.png

Escribimos exit para salir del interprete y regresar a nuestra terminal:

exit

Ahora conozcamos a Jupyter, tal como nos dice su pagina web, es una aplicación web que permite crear y compartir documentos que contienen código vivo, ecuaciones, visualizaciones y texto explicativo. Se puede usar para simulación numérica, simulación estadística, machine learning y mucho mas. Vamos a instalarlo para ir conociéndolo:

pip install jupyter

Para correrlo debemos escribir lo siguiente:

ipython notebook

Y automáticamente nos abrirá en una pestaña del navegador lo siguiente:

/images/blog/seleccic3b3n_370.png

Aqui podemos crear un nuevo notebook, dando click al boton new en la parte superior derecha y seleccionando Python 2:

/images/blog/seleccic3b3n_371.png

Nos aparece una especie de consola web donde podemos ir escribiendo nuestros programas:

/images/blog/seleccic3b3n_373.png

Y ejecutando su contenido, como si trabajáramos en nuestra vieja terminal:

/images/blog/seleccic3b3n_372.png

Maravilloso verdad, un interprete web, donde vamos escribiendo nuestros programas y viendo su funcionamiento, agregando texto, imagenes, generando graficos, podemos guardar cada noteboook, exportarlo a pdf, intercambiarlo, etc, como veran es una herramienta valiosisima para utilizar en la enseñanza, en el analisis de datos, en los trabajos de investigacion, etc.

En la red podemos encontrar una gran de cantidad de notebooks listos para descargar, hay tutoriales, tesis, trabajos de investigacion, etc. A nosotros nos intereso el siguiente curso, que es introductorio al leguaje Python: Python For Developers

Vamos a descargar el archivo zip conteniendo todos los notebooks y lo guardamos en la ruta desde donde ejecutamos el comando “ipython notebook”, lo descomprimimos y tendremos lo siguiente:

/images/blog/seleccic3b3n_375.png

Como verán ahora ya nos aparece la carpeta donde esta el curso, ingresamos a ella:

/images/blog/seleccic3b3n_376.png

Ahora ingresamos a uno de los capítulos:

/images/blog/seleccic3b3n_378.png

Seleccionamos el archivo con la extensión “ipynb” y veremos un hermoso curso de introduccion a Python donde podemos ir interactuando con los programas de ejemplo:

/images/blog/seleccic3b3n_377.png

Genial no, ahora aprender Python y hacer demostraciones de código va a ser mas divertido :-)

Eso es todo por hoy, saludos.

Reporte PDF en Django con Reportlab

Vamos a retomar nuestro proyecto tutorial, al que hemos tenido bastante olvidado estos últimos días y haremos un reporte en pdf utilizando la librería reportlab, para ello la instalamos:

pip install reportlab

Lo primero que haremos es poner el logo de django en la parte superior de nuestro reporte, por lo que debemos tener la imagen guardada en una ubicación fácil de obtener, por eso crearemos una carpeta llamada “media” en nuestro proyecto y dentro de esta, otra carpeta llamada imagenes, es aquí donde pondremos nuestro archivo logo_django.png:

/images/blog/ubicacion_logo.jpg

Modificamos el archivo settings.py para poder establecer la ruta de la carpeta media:

MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

Creamos la clase ReportePersonasPDF en el archivo views.py, el código está explicado en los comentarios:

#Importamos settings para poder tener a la mano la ruta de la carpeta media
from django.conf import settings
from io import BytesIO
from reportlab.pdfgen import canvas
from django.views.generic import View

class ReportePersonasPDF(View):

def cabecera(self,pdf):
        #Utilizamos el archivo logo_django.png que está guardado en la carpeta media/imagenes
        archivo_imagen = settings.MEDIA_ROOT+'/imagenes/logo_django.png'
        #Definimos el tamaño de la imagen a cargar y las coordenadas correspondientes
        pdf.drawImage(archivo_imagen, 40, 750, 120, 90,preserveAspectRatio=True)

def get(self, request, *args, **kwargs):
        #Indicamos el tipo de contenido a devolver, en este caso un pdf
        response = HttpResponse(content_type='application/pdf')
        #La clase io.BytesIO permite tratar un array de bytes como un fichero binario, se utiliza como almacenamiento temporal
        buffer = BytesIO()
        #Canvas nos permite hacer el reporte con coordenadas X y Y
        pdf = canvas.Canvas(buffer)
        #Llamo al método cabecera donde están definidos los datos que aparecen en la cabecera del reporte.
        self.cabecera(pdf)
        #Con show page hacemos un corte de página para pasar a la siguiente
        pdf.showPage()
        pdf.save()
        pdf = buffer.getvalue()
        buffer.close()
        response.write(pdf)
        return response

Para poder acceder a esta clase debemos hacer la creación de la url correspondiente en el archivo urls.py:

url(r'^reporte_personas_pdf/$',login_required(ReportePersonasPDF.as_view()), name="reporte_personas_pdf"),

Ahora vamos a utilizar la url desde el archivo personas.html:

<div class='form-group'>
    <div class="row">
        <div class="col-lg-9">

        </div>
        <div class="col-lg-1">
            <a href="{% url 'personas:reporte_personas_excel' %}" class="btn btn-info btn-block">
                <span class="glyphicon glyphicon-list-alt"></span>
            </a>
        </div>
        {% if perms.personas.add_persona %}
        <div class="col-lg-1">
            <a href="{% url 'personas:crear_persona' %}" class="btn btn-info btn-block">
                <span class="glyphicon glyphicon-plus"></span>
            </a>
        </div>
        {% endif %}
        <div class="col-lg-1">
            <a href="{% url 'personas:reporte_personas_pdf' %}" class="btn btn-info btn-block">
                <span class="glyphicon glyphicon-file"></span>
            </a>
        </div>
    </div>
</div>

Nos tiene que aparecer un ícono de un archivo:

/images/blog/personas_pdf.jpg

El primer resultado será este:

/images/blog/reporte_pdf1.jpg

Vamos a ponerle un encabezado a nuestro reporte que diga “Python Piura” y debajo “Reporte de Personas”, para ello colocamos el siguiente código debajo de la última linea del método “cabecera”:

#Establecemos el tamaño de letra en 16 y el tipo de letra Helvetica
pdf.setFont("Helvetica", 16)
#Dibujamos una cadena en la ubicación X,Y especificada
pdf.drawString(230, 790, u"PYTHON PIURA")
pdf.setFont("Helvetica", 14)
pdf.drawString(200, 770, u"REPORTE DE PERSONAS")
/images/blog/reporte_pdf2.jpg

Visualizaremos la tabla de personas, por lo que creamos el método tabla en la clase ReportePersonasPDF:

def tabla(self,pdf,y):
        #Creamos una tupla de encabezados para neustra tabla
        encabezados = ('DNI', 'Nombre', 'Apellido Paterno', 'Apellido Materno')
        #Creamos una lista de tuplas que van a contener a las personas
        detalles = [(persona.dni, persona.nombre, persona.apellido_paterno, persona.apellido_materno) for persona in Persona.objects.all()]
        #Establecemos el tamaño de cada una de las columnas de la tabla
        detalle_orden = Table([encabezados] + detalles, colWidths=[2 * cm, 5 * cm, 5 * cm, 5 * cm])
        #Aplicamos estilos a las celdas de la tabla
        detalle_orden.setStyle(TableStyle(
        [
                #La primera fila(encabezados) va a estar centrada
                ('ALIGN',(0,0),(3,0),'CENTER'),
                #Los bordes de todas las celdas serán de color negro y con un grosor de 1
                ('GRID', (0, 0), (-1, -1), 1, colors.black),
                #El tamaño de las letras de cada una de las celdas será de 10
                ('FONTSIZE', (0, 0), (-1, -1), 10),
                ]
        ))
        #Establecemos el tamaño de la hoja que ocupará la tabla
        detalle_orden.wrapOn(pdf, 800, 600)
        #Definimos la coordenada donde se dibujará la tabla
        detalle_orden.drawOn(pdf, 60,y)

Veamos como queda nuestro método get ahora con la nueva llamada al método tabla:

def get(self, request, *args, **kwargs):
        #Indicamos el tipo de contenido a devolver, en este caso un pdf
        response = HttpResponse(content_type='application/pdf')
        #La clase io.BytesIO permite tratar un array de bytes como un fichero binario, se utiliza como almacenamiento temporal
        buffer = BytesIO()
        #Canvas nos permite hacer el reporte con coordenadas X y Y
        pdf = canvas.Canvas(buffer)
        #Llamo al método cabecera donde están definidos los datos que aparecen en la cabecera del reporte.
        self.cabecera(pdf)
        y = 600
        self.tabla(pdf, y)
        #Con show page hacemos un corte de página para pasar a la siguiente
        pdf.showPage()
        pdf.save()
        pdf = buffer.getvalue()
        buffer.close()
        response.write(pdf)
        return response

Y este será el resultado final:

/images/blog/reporte_pdf3.jpg

Eso es todo por hoy. Saludos.

Consulta DNI - RENIEC

En esta oportunidad vamos a hacer un ejemplo mas complejo, ya que el captcha así lo amerita, nuestra página objetivo es la siguiente:

https://cel.reniec.gob.pe/valreg/valreg.do

/images/blog/reniec.jpg

En esta página podemos ingresar el número de DNI y obtener el nombre completo del ciudadano. Como se ve en la imagen aquí nos enfrentamos a un captcha mas completo y a un teclado dinámico compuesto de botones, para romper este captcha tenemos que modificar la imagen para obtener el texto correcto, eliminando las lineas que atraviesan las letras y que tienen una tonalidad distinta de azul, el proceso no será 100% seguro pero funciona en la mayoría de los casos:

# -*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

try:
        import Image
except ImportError:
        from PIL import Image
import pytesseract

def ir_reniec_web(dni):
        fp = webdriver.FirefoxProfile()
        driver = webdriver.Firefox(fp)
        #Definimos nuestra página objetivo
        driver.get("https://cel.reniec.gob.pe/valreg/valreg.do")
        try:
                #Esperamos hasta que se cargue la imagen con el captcha
                WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.ID, "imagcodigo")))
        except:
                print "Element is not present"
        #Hacemos la captura de pantalla correspondiente
        driver.save_screenshot("screenshot.png")
        img=Image.open('screenshot.png')
        #Obtenemos el ancho y el largo de la imagen
        ancho = img.size[0]
        alto = img.size[1]
        #Recortamos la parte del captcha teniendo en cuenta el ancho y el largo de la imagen
        img_recortada = img.crop((int(ancho/4.26),int(alto/3.64),int(ancho/2.8),int(alto/3)))
        #Guardamos el recorte
        img_recortada.save("recorte.png")
        #Recorremos cada uno de los dígitos del DNI
        for num in dni:
                #Buscamos el boton que tenga como nombre tecla_0
                boton_0 = driver.find_element_by_name("tecla_0")
                #Obtenemos el valor del botón con nombre tecla_0
                valor_0 = boton_0.get_attribute('value')
                #Buscamos el boton que tenga como nombre tecla_1
                boton_1 = driver.find_element_by_name("tecla_1")
                #Obtenemos el valor del botón con nombre tecla_1
                valor_1 = boton_1.get_attribute('value')
                #Buscamos el boton que tenga como nombre tecla_2
                boton_2 = driver.find_element_by_name("tecla_2")
                #Obtenemos el valor del botón con nombre tecla_2
                valor_2 = boton_2.get_attribute('value')
                #Buscamos el boton que tenga como nombre tecla_3
                boton_3 = driver.find_element_by_name("tecla_3")
                #Obtenemos el valor del botón con nombre tecla_3
                valor_3 = boton_3.get_attribute('value')
                #Buscamos el boton que tenga como nombre tecla_4
                boton_4 = driver.find_element_by_name("tecla_4")
                #Obtenemos el valor del botón con nombre tecla_4
                valor_4 = boton_4.get_attribute('value')
                #Buscamos el boton que tenga como nombre tecla_5
                boton_5 = driver.find_element_by_name("tecla_5")
                #Obtenemos el valor del botón con nombre tecla_5
                valor_5 = boton_5.get_attribute('value')
                #Buscamos el boton que tenga como nombre tecla_6
                boton_6 = driver.find_element_by_name("tecla_6")
                #Obtenemos el valor del botón con nombre tecla_6
                valor_6 = boton_6.get_attribute('value')
                #Buscamos el boton que tenga como nombre tecla_7
                boton_7 = driver.find_element_by_name("tecla_7")
                #Obtenemos el valor del botón con nombre tecla_7
                valor_7 = boton_7.get_attribute('value')
                #Buscamos el boton que tenga como nombre tecla_8
                boton_8 = driver.find_element_by_name("tecla_8")
                #Obtenemos el valor del botón con nombre tecla_8
                valor_8 = boton_8.get_attribute('value')
                #Buscamos el boton que tenga como nombre tecla_9
                boton_9 = driver.find_element_by_name("tecla_9")
                #Obtenemos el valor del botón con nombre tecla_9
                valor_9 = boton_9.get_attribute('value')
        #Consultamos si el dígito del DNI es igual al valor del botón, si es el caso entonces se da click en ese botón
        if num==valor_0:
                boton_0.click()
        elif num==valor_1:
                boton_1 = driver.find_element_by_name("tecla_1")
                boton_1.click()
        elif num==valor_2:
                boton_2 = driver.find_element_by_name("tecla_2")
                boton_2.click()
        elif num==valor_3:
                boton_3 = driver.find_element_by_name("tecla_3")
                boton_3.click()
        elif num==valor_4:
                boton_4 = driver.find_element_by_name("tecla_4")
                boton_4.click()
        elif num==valor_5:
                boton_5 = driver.find_element_by_name("tecla_5")
                boton_5.click()
        elif num==valor_6:
                boton_6 = driver.find_element_by_name("tecla_6")
                boton_6.click()
        elif num==valor_7:
                boton_7 = driver.find_element_by_name("tecla_7")
                boton_7.click()
        elif num==valor_8:
                boton_8 = driver.find_element_by_name("tecla_8")
                boton_8.click()
        elif num==valor_9:
                boton_9 = driver.find_element_by_name("tecla_9")
                boton_9.click()
        #Se llama al método romper_captcha para obtener el texto correspondiente
        captcha = romper_captcha("recorte.png")
        try:
                #Obtenemos la caja de texto donde se escribe el texto del captcha
                codigo = driver.find_element_by_name("imagen")
                #Si el captcha está vacio o no se ha logrado romper se cierra el navegador y se termina la aplicación
                if captcha=="":
                        driver.close()
                        return
                #Escribimos el texto
                codigo.send_keys(captcha)
        except:
                pass
        try:
                #Obtenemos el botón de consulta
                boton_consultar=driver.find_element_by_name("bot_consultar")
                #Damos click al botón de consulta
                boton_consultar.click()
        except:
                print "Element is not present"
        #Obtengo el resultado que aparece en el elemento llamado style2
        resultado = driver.find_element_by_class_name("style2")
        #Partimos el resultado para obtener el nombre
        nombre = resultado.text.split('\n')
        driver.close()
        return nombre[0]

def romper_captcha(nombre_imagen):
        #Abro la imagen
        img = Image.open(nombre_imagen)
        #Obtengo un arreglo de píxeles de la imagen
        pixdata = img.load()
        """Modifico los píxeles de la imagen de acuerdo al análisis de los
        colores de las letras y las lineas que cruzan el texto, pintando de negro los píxeles del
        texto y de blanco las lineas"""
        for y in xrange(img.size[1]):
                for x in xrange(img.size[0]):
                        if pixdata[x, y][2] < 146:
                                pixdata[x, y] = (255, 255, 255)
        for y in xrange(img.size[1]):
                for x in xrange(img.size[0]):
                        if pixdata[x, y][1]     > 64:
                                pixdata[x, y] = (255, 255, 255)
                        else:
                                pixdata[x, y] = (0,0,0)
        #Guardo la imagen modificada
        img.save("modificado.jpg")
        #Abro la imagen modificado
        image = Image.open("modificado.jpg")
        #Obtenemos el texto de la imagen
        frase = pytesseract.image_to_string(image)
        #Retornamos el texto eliminado los espacios en blanco entre las palabras y convirtiendolas en mayúsculas
        return frase.replace(' ',"").upper()

def main():
        dni = '123456789'
        print ir_reniec_web(dni)

main()

Espero que les sea útil. Saludos.

Otro Ejemplo de Web Scrapping con Python

En esta oportunidad vamos a compartir con ustedes un nuevo script para consultar rucs de manera masiva a partir del DNI en la página web de sunat, esto ha sido un poco mas complejo debido a que la página en cuestión usa frames, también hemos mejorado un poquito el problema de los tamaños haciendo el recorte a partir del tamaño de la captura de pantalla:

# -*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
try:
        import Image
except ImportError:
        from PIL import Image
import pytesseract

def ir_sunat_web(dni):
        driver = webdriver.Firefox()
        #Fijamos nuestra página objetivo
        driver.get("http://ww1.sunat.gob.pe/cl-ti-itmrconsruc/jcrS00Alias")
        try:
                #Debido a que la página tiene frames debemos hacer el cambio al frame con nombre "leftFrame"
                driver.switch_to_frame("leftFrame")
        except:
                return []
                driver.close()
        try:
                #Obtenemos los radio buttons que nos permiten seleccionar el tipo de búsqueda a hacer
                radios = driver.find_elements_by_name("tQuery")
                #Seleccionamos la búsqueda por DNI
                radios[1].click()
                #Obtenemos la caja de texto donde se ingresa el DNI
                documento = driver.find_element_by_name("search2")
                #Escribimos el DNI
                documento.send_keys(dni)
                #Esperamos hasta que el texto esté escrito en la caja de texto del DNI
                WebDriverWait(driver,5).until(EC.text_to_be_present_in_element_value((By.NAME,"search2"),dni))
        except:
                driver.close()
                return []
        try:
                #Esperamos hasta que se cargue la imagen con el captcha
                WebDriverWait(driver,5).until(EC.presence_of_element_located((By.NAME, "imagen")))
                #Hacemos la captura de pantalla correspondiente
                driver.save_screenshot("screenshot.jpg")
                img=Image.open('screenshot.jpg')
                #Obtenemos el ancho y el largo de la imagen
                ancho = img.size[0]
                alto = img.size[1]
                #Recortamos la parte del captcha teniendo en cuenta el ancho y el largo de la imagen
                img_recortada = img.crop((ancho/1.4,alto/20,ancho/1.25,alto/6.9))
                #Guardamos el recorte
                img_recortada.save("recorte.jpg")
                #Obtenemos el texto del captcha
                captcha = pytesseract.image_to_string(img_recortada)
                #Obtenemos la caja de texto donde se escribe el texto del captcha
                codigo = driver.find_element_by_name("codigo")
                #Escribimos el texto
                codigo.send_keys(captcha)
        except:
                driver.close()
                return []
        try:
                #Obtenemos el botón de búsqueda
                boton = driver.find_element_by_class_name("form-button")
                #Damos click al botón de búsqueda
                boton.click()
                #Cambiamos al frame por defecto
                driver.switch_to_default_content()
                #Cambiamos al frame llamado "mainFrame" que contiene la tabla de resultados con un enlace conteniendo el ruc
                driver.switch_to_frame("mainFrame")
        except:
                driver.close()
                return []
        try:
                #Esperamos que se cargue la tabla de resultados
                WebDriverWait(driver,5).until(EC.presence_of_element_located((By.CLASS_NAME, "form-table")))
                #Obtenemos el enlace que contiene al RUC
                enlace=driver.find_element(By.TAG_NAME,"a")
                #Le damos click al enlace
                enlace.click()
                #Ahora si tenemos una tabla que contiene el detalle de los resultados del RUC consultado
                #Obtenemos todas las filas
                trs = driver.find_elements(By.TAG_NAME, "tr")
                #Obtenemos las celdas de la primera fila
                tds = trs[0].find_elements(By.TAG_NAME, "td")
                #A la segunda celda la partimos ya que tiene el ruc y la razón social separados por un guión
                primera_linea = tds[1].text.split('-')
                ruc = primera_linea[0].strip()
                razon_social = primera_linea[1].strip()
                #Obtenemos la segunda linea que tiene el tipo de contribuyente
                tds = trs[1].find_elements(By.TAG_NAME, "td")
                tipo_contribuyente = tds[1].text.strip()
                #Obtenemos la dirección
                tds = trs[7].find_elements(By.TAG_NAME, "td")
                direccion = tds[1].text.strip()
        except:
                driver.close()
                return []
        driver.close()
        #Devolvemos una lista con los resultados.
        datos = [ruc,razon_social,tipo_contribuyente,direccion]
        return datos

def main():
        #Ingresar DNI
        dni = '12345678'
        print ir_sunat_web(dni)

main()

Esperamos que les sea útil. Saludos.