Escribiendo un Modulo de Tryton ERP Básico 1

Tryton ERP además de ERP es un excelente framework y más su versatilidad a la hora de ser personalizado, a continuación estudiaremos un poco como se puede extender el comportamiento y la vista de los módulos, el que considero como primer paso para aprender a personalizar trytond a nivel de código, asumo que se tiene conocimiento de Python, Tryton ERP y desarrollo de software en general.

Como ejercicio de práctica vamos a realizar la extensión del módulo party y adicionarle un campo, en nuestro caso vamos adicionar el campo CIIU, que en Colombia se utiliza para describir la actividad económica del tercero.

Creación del modulo

Primero instalamos dependencias necesarias para el desarrollo del módulo.

pip3 install trytond==6.8.0 --user
pip3 install proteus --user

Ahora creamos el módulo usando plantilla por medio de cookiecutter

cookiecutter git+https://gitlab.com/bit4bit/cookiecutter-tryton-module.git --directory template --no-input module_name=party_ciiu version=6.8.0 test_with_scenario=party_ciiu.rst

Lo anterior nos crea un directorio party_ciiu con la estructura de un módulo de tryton.

Activamos el módulo en modo desarrollo.

cd party_ciiu
python3 setup.py develop --user

Para confirmar que el módulo este operativo ejecutamos las pruebas.

cd party_ciiu
python3 -munittest

Y si todo es correcto veríamos

Ran 22 tests in 2.268s

OK

Adición campo ciiu_code

Nos vamos a orientador durante el desarrollo de la siguiente manera:

  • mantener el módulo todo el tiempo en operación es decir, no deberíamos tener errores por mucho más que unos minutos.
  • aplazar lo que mas podamos el trabajo sobre vista del módulo, ya que cuando editamos la vista se hace visible para que el usuario interactué con el modulo y si este esta incompleto abrimos el camino a las inconsistencias en el sistema.

Para avanzar y conocer el progreso de nuestro desarrollo, procederemos de la siguiente manera:

  • describimos los escenarios o pruebas que esperamos (etapa de especulación)
  • ejecutamos los escenarios o pruebas (etapa de ilustración)
  • corregimos el error, aclaramos la intención y volvemos al primer paso (etapa de ajuste)

Vamos a empezar creando el escenario que refleje nuestro objetivo de adicionar el campo ciiu_code

tests/scenario_create_party_with_ciiu_code.rst

===================
Crear Party con campo ciiu_code Scenario
===================

Dependencias::

    >>> from proteus import Model, Wizard
    >>> from trytond.tests.tools import activate_modules

Activacion de modulos::

    >>> config = activate_modules('party_ciiu')

Party con ciiu_code::

    >>> Party = Model.get('party.party')
    >>> party = Party()
    >>> party.ciiu_code = '0111' # valor real esperados
    >>> party.save()
    >>> party.ciiu_code
    '0111'

ejecutamos los escenarios

python3 -munittest

y vemos nuestro primer error

Failed example:
    Party = Model.get('party.party')
    ...
    File "/home/bit4bit/.local/lib/python3.9/site-packages/trytond/pool.py", line 190, in get
        return self._pool[self.database_name][type][name]
    KeyError: 'party.party'

KeyError: 'party.party' nos indica que estamos intentado acceder a un modelo que no esta registrado, para corregir vamos a instalar el modulo y notificar que nuestro modulo requiere del modulo party para operar.

instalamos dependencia

pip3 install trytond-party==6.8.0 --user

indicamos a trytond que requerimos del modulo party

tryton.cfg

[tryton]
version=6.8.0
depends:
    ir
    party
xml:

ejecutamos los escenarios

python3 -munittest

y vemos nuestro siguiente error

Failed example:
    party.ciiu_code = '0110'
Exception raised:
    Traceback (most recent call last):
      File "/usr/lib/python3.9/doctest.py", line 1336, in __run
        exec(compile(example.source, filename, "single",
      File "", line 1, in 
        party.ciiu_code = '0110'
    AttributeError: 'party.party' object has no attribute 'ciiu_code'

AttributeError: 'party.party' object has no attribute 'ciiu_code' nos indica que estamos intentando asignar un valor a un atributo que no existe en el modelo party.party, para corregir necesitamos este atributo en party.party.

Extendemos el modelo party.party y adicionamos el nuevo atributo.

party.py

from trytond.pool import PoolMeta
from trytond.model import fields

class Party( metaclass=PoolMeta):
    __name__ = 'party.party'

    ciiu_code = fields.Char('CIIU CODE', required=False)

Le indicamos a tryton el modelo que hemos extendido

init.py

from trytond.pool import Pool

from . import party

__all__ = ['register']

def register():
    Pool.register(
        party.Party,
        module='party_ciiu', type_='model')

ejecutamos los escenarios

python3 -munittest

y por fin ya no tenemos el error

Ran 24 tests in 9.231s

OK

Lo anterior nos indica que ya el party.party tiene el nuevo atributo, pero aun no hemos realizado una implementación aceptable, una implementación aceptable es una implementación que se ajuste a los escenarios planteados o acordados con el cliente, en nuestro caso tomaremos como aceptable la implementación del ciiu_code cuando este valide que el formato sea el correcto y este entre los valores esperados según cliente.

tests/test_entrada_es_opcional.py

from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.pool import Pool
from trytond.model.exceptions import RequiredValidationError

class PartyCiiu_EntradasRequeridas_TestCase(ModuleTestCase):
    "Test Party Ciiu module"
    module = 'party_ciiu'

    @with_transaction()
    def test_ciiud_code_es_opcional(self):
        pool = Pool()
        Party = pool.get('party.party')
        try:
            party, = Party.create([{}])
        except RequiredValidationError:
            self.fail('se espera campo `ciiu_code` como opcional')

del ModuleTestCase
python3 -munittest

siendo la salida de la ejecución

Ran 47 tests in 12.323s

OK

Lo cual nos indica que no hubo errores, en nuestro caso esta prueba nos garantiza que si por algún motivo cambia la obligatoriedad del campo ciiu_code nos alerte y tomemos las acciones pertinentes.

Ahora procederemos con la validación del campo, es recomendable partir de la entradas erróneas, al iniciar con la validación de las entradas erróneas se nos reduce el margen de que el programa entre en un estado inconsistente, ya que en el manejo de estas entradas es donde usualmente se presenta la mayoría de inconsistencia.

tests/test_ciiu_code.py

from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.pool import Pool
from trytond.model.exceptions import ValidationError

class PartyCiiu_ciiu_code_TestCase(ModuleTestCase):
    "Test Party Ciiu campo ciiu_code"
    module = 'party_ciiu'

    @with_transaction()
    def test_para_entrada_invalida_UserError(self):
        pool = Pool()
        Party = pool.get('party.party')

        party, = Party.create([{'name': 'CIIU'}])

        with self.assertRaises(ValidationError):
            party.ciiu_code = '99999'
            party.save()
        
        with self.assertRaises(ValidationError):
            party.ciiu_code = '0'
            party.save()

        with self.assertRaises(ValidationError):
            party.ciiu_code = '9999'
            party.save()

del ModuleTestCase
python3 -munittest
Traceback (most recent call last):
  File "/home/bit4bit/.local/lib/python3.9/site-packages/trytond/tests/test_tryton.py", line 223, in wrapper
    result = func(*args, **kwargs)
  File "/tmp/d20230621-16221-7fnzm8/party_ciiu/tests/test_ciiu_code.py", line 18, in test_para_entrada_invalida_UserError
    party.save()
AssertionError: ValidationError not raised

AssertionError: ValidationError not raised nos indica que no se lanza el error, ahora procederemos a corregir, como hemos visto corregir seria realizar la implementación del código o bien corregir un defecto.

party_ciiu/party.py

from trytond.pool import PoolMeta
from trytond.model import fields
from trytond.i18n import gettext
from trytond.model.exceptions import ValidationError

class Party( metaclass=PoolMeta):
    __name__ = 'party.party'

    ciiu_code = fields.Char('CIIU CODE', required=False)

    @classmethod
    def validate_fields(cls, records, field_names):
        super().validate_fields(records, field_names)
        cls.check_fields(records, field_names)

    @classmethod
    def check_fields(cls, records, field_names):
        if field_names and not (field_names & {'ciiu_code'}):
            return

        for record in records:
            if not cls.allowed_ciiu_code(record.ciiu_code):
                raise ValidationError(gettext('party_ciiu.ciiu_code_invalid'))

    @classmethod
    def allowed_ciiu_code(cls, code):
        # solo validamos los codigos de interes para nuestra implementacion
        codes = [None, '0111', '0170', '0210', '0240', '0311', '0510', '0520', '0610']
        return code in codes
python3 -munittest
Ran 70 tests in 15.363s

OK

ya volvimos a estar estables, ahora vamos a confirmar que el ciiu_code este operativo según los acordado con el cliente.

tests/test_ciiu_code.py

from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.pool import Pool
from trytond.model.exceptions import ValidationError

class PartyCiiu_ciiu_code_TestCase(ModuleTestCase):
    "Test Party Ciiu campo ciiu_code"
    module = 'party_ciiu'

    @with_transaction()
    def test_para_entrada_valida_no_hay_error(self):
        pool = Pool()
        Party = pool.get('party.party')

        party, = Party.create([{'name': 'CIIU Valid'}])

        # solo validamos los de interes para nuestro cliente
        company_codes = [None, '0111', '0170', '0210', '0240', '0311', '0510', '0520', '0610']
        for code in company_codes:
            party.ciiu_code = code
            party.save()
            
    @with_transaction()
    def test_para_entrada_invalida_UserError(self):
        pool = Pool()
        Party = pool.get('party.party')

        party, = Party.create([{'name': 'CIIU'}])

        with self.assertRaises(ValidationError):
            party.ciiu_code = '99999'
            party.save()
        
        with self.assertRaises(ValidationError):
            party.ciiu_code = '0'
            party.save()

        with self.assertRaises(ValidationError):
            party.ciiu_code = '9999'
            party.save()

del ModuleTestCase
python3 -munittest
Ran 71 tests in 15.529s

OK

excelente ahora que hemos validado el comportamiento aceptable por el cliente, procedemos a activarlo en la interfaz del usuario.

le indicamos al tryton que vamos a extender las vista party.party_view_tree usando el archivo view/party_list.xml y party.party_view_form con el archivo view/party_form.xml

party.xml

<?xml version="1.0"?>
<tryton>
  <data>
    <record model="ir.ui.view" id="party_view_tree">
      <field name="model">party.party</field>
      <field name="inherit" ref="party.party_view_tree"/>
      <field name="name">party_list</field>
    </record>
	
    <record model="ir.ui.view" id="party_view_form">
      <field name="model">party.party</field>
      <field name="inherit" ref="party.party_view_form"/>
      <field name="name">party_form</field>
    </record>
  </data>
</tryton>

view/party_form.xml

<?xml version="1.0"?>
<data>
  <xpath expr="//page[@id='general']" position="after">
    <field name="ciiu_code"/>
  </xpath>
</data> 

view/party_list.xml

<?xml version="1.0"?>
<data>
  <xpath expr="//field[@name='name']" position="after">
    <field name="ciiu_code"/>
  </xpath>
</data>

e informamos a tryton nuestra intención sobre cambios a la vista, adicionando party.xml a la lista xml en tryton.cfg

tryton.cfg

[tryton]
version=6.8.0
depends:
    ir
    party
xml:
    party.xml	

Aclaro que el objetivo del ejercicio no era entrar en las particularidades para la creación del módulo, sino conocer el procedimiento en general para escribir un módulo de trytond siendo este:

  • creación del módulo usando la plantilla
  • escribir el módulo usando el un ciclo de: especular, ilustrar y ajustar
  • plantear los escenarios aceptables acordados con el cliente en doctest
  • informar a trytond nuestra intención por medio de tryton.cfg y init.py

Y para finalizar me gustaría comentar acerca de como usamos los dos mecanismos que ofrece trytond para las pruebas, estos son:

  1. archivos .rst o doctests
  2. pruebas por medio de unittest

Usamos las pruebas doctests para verificar que el módulo opere aceptablemente en los escenarios planteados acordados con el cliente, y usamos unittest ya para verificar en detalle el comportamiento del módulo, diríamos que este último es una lupa a los escenarios de doctests.

Agradecimientos

  • @rakolnivok28 por las sugerencias