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:
- archivos .rst o doctests
- 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