diff --git a/facho/model/__init__.py b/facho/model/__init__.py
new file mode 100644
index 0000000..1f6c14b
--- /dev/null
+++ b/facho/model/__init__.py
@@ -0,0 +1,62 @@
+
+class ModelMeta(type):
+ def __new__(cls, name, bases, ns):
+ new = type.__new__(cls, name, bases, ns)
+ if '__name__' in ns:
+ new.__name__ = ns['__name__']
+ if '__namespace__' in ns:
+ new.__namespace__ = ns['__namespace__']
+ else:
+ new.__namespace__ = {}
+
+ return new
+
+class ModelBase(object, metaclass=ModelMeta):
+
+ def __new__(cls, *args, **kwargs):
+ obj = super().__new__(cls, *args, **kwargs)
+ obj._xml_attributes = {}
+ obj._fields = {}
+ obj._text = ""
+ obj._namespace_prefix = None
+
+ return obj
+
+ def __setitem__(self, key, val):
+ self._xml_attributes[key] = val
+
+ def __getitem__(self, key):
+ return self._xml_attributes[key]
+
+ def to_xml(self):
+ tag = self.__name__
+ ns = ''
+ if self._namespace_prefix is not None:
+ ns = "%s:" % (self._namespace_prefix)
+
+ pair_attributes = ["%s=\"%s\"" % (k, v) for (k, v) in self._xml_attributes.values()]
+
+ for (prefix, url) in self.__namespace__.items():
+ pair_attributes.append("xmlns:%s=\"%s\"" % (prefix, url))
+ attributes = ""
+ if pair_attributes:
+ attributes = " " + " ".join(pair_attributes)
+
+ content = ""
+
+ for name, value in self._fields.items():
+ if hasattr(value, 'to_xml'):
+ print(self._fields)
+ content += value.to_xml()
+ elif isinstance(value, str):
+ content += value
+ content += self._text
+
+ if content == "":
+ return "<%s%s%s/>" % (ns, tag, attributes)
+ else:
+ return "<%s%s%s>%s%s%s>" % (ns, tag, attributes, content, ns, tag)
+
+class Model(ModelBase):
+ pass
+
diff --git a/facho/model/fields/__init__.py b/facho/model/fields/__init__.py
new file mode 100644
index 0000000..f368aaf
--- /dev/null
+++ b/facho/model/fields/__init__.py
@@ -0,0 +1,5 @@
+from .attribute import Attribute
+from .many2one import Many2One
+from .model import Model
+
+__all__ = [Attribute, Many2One, Model]
diff --git a/facho/model/fields/attribute.py b/facho/model/fields/attribute.py
new file mode 100644
index 0000000..2ad9f73
--- /dev/null
+++ b/facho/model/fields/attribute.py
@@ -0,0 +1,17 @@
+from .field import Field
+
+class Attribute(Field):
+ def __init__(self, tag):
+ self.tag = tag
+
+ def __get__(self, inst, cls):
+ if inst is None:
+ return self
+
+ assert self.name is not None
+ (tag, value) = inst._xml_attributes[self.name]
+ return value
+
+ def __set__(self, inst, value):
+ assert self.name is not None
+ inst._xml_attributes[self.name] = (self.tag, value)
diff --git a/facho/model/fields/field.py b/facho/model/fields/field.py
new file mode 100644
index 0000000..251223e
--- /dev/null
+++ b/facho/model/fields/field.py
@@ -0,0 +1,21 @@
+class Field:
+ def __set_name__(self, owner, name):
+ self.name = name
+
+ def __get__(self, inst, cls):
+ if inst is None:
+ return self
+ assert self.name is not None
+ return inst._fields[self.name]
+
+ def __set__(self, inst, value):
+ assert self.name is not None
+ inst._fields[self.name] = value
+
+ def _set_namespace(self, inst, name, namespaces):
+ if name is None:
+ return
+
+ if name not in namespaces:
+ raise KeyError("namespace %s not found" % (name))
+ inst._namespace_prefix = name
diff --git a/facho/model/fields/many2one.py b/facho/model/fields/many2one.py
new file mode 100644
index 0000000..7f5e35f
--- /dev/null
+++ b/facho/model/fields/many2one.py
@@ -0,0 +1,35 @@
+from .field import Field
+
+class Many2One(Field):
+ def __init__(self, model, setter=None, namespace=None):
+ self.model = model
+ self.setter = setter
+ self.namespace = namespace
+
+ def __set_name__(self, owner, name):
+ self.name = name
+
+ def __get__(self, inst, cls):
+ if inst is None:
+ return self
+ assert self.name is not None
+ return inst._fields[self.name]
+
+ def __set__(self, inst, value):
+ assert self.name is not None
+ class_model = self.model
+ inst_model = class_model()
+
+ self._set_namespace(inst_model, self.namespace, inst.__namespace__)
+ inst._fields[self.name] = inst_model
+
+ # si hay setter manual se ejecuta
+ # de lo contrario se asigna como texto del elemento
+ setter = getattr(inst, self.setter or '', None)
+ if callable(setter):
+ setter(inst_model, value)
+ else:
+ inst_model._text = str(value)
+
+
+
diff --git a/facho/model/fields/model.py b/facho/model/fields/model.py
new file mode 100644
index 0000000..162be39
--- /dev/null
+++ b/facho/model/fields/model.py
@@ -0,0 +1,26 @@
+from .field import Field
+
+class Model(Field):
+ def __init__(self, model, namespace=None):
+ self.model = model
+ self.namespace = namespace
+
+ def __get__(self, inst, cls):
+ if inst is None:
+ return self
+ assert self.name is not None
+ return self._create_model(inst)
+
+ def __set__(self, inst, value):
+ obj = self._create_model(inst)
+ obj._text = str(value)
+
+ def _create_model(self, inst):
+ try:
+ return inst._fields[self.name]
+ except KeyError:
+ obj = self.model()
+ self._set_namespace(obj, self.namespace, inst.__namespace__)
+ inst._fields[self.name] = obj
+ return obj
+
diff --git a/tests/test_model.py b/tests/test_model.py
new file mode 100644
index 0000000..8aeb7e1
--- /dev/null
+++ b/tests/test_model.py
@@ -0,0 +1,167 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# This file is part of facho. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+"""Tests for `facho` package."""
+
+import pytest
+
+import facho.model
+import facho.model.fields as fields
+
+def test_model_to_element():
+ class Person(facho.model.Model):
+ __name__ = 'Person'
+
+ person = Person()
+
+ assert "" == person.to_xml()
+
+def test_model_to_element_with_attribute():
+ class Person(facho.model.Model):
+ __name__ = 'Person'
+ id = fields.Attribute('id')
+
+ person = Person()
+ person.id = 33
+
+ personb = Person()
+ personb.id = 44
+
+ assert "" == person.to_xml()
+ assert "" == personb.to_xml()
+
+def test_model_to_element_with_attribute_as_element():
+ class ID(facho.model.Model):
+ __name__ = 'ID'
+
+ class Person(facho.model.Model):
+ __name__ = 'Person'
+
+ id = fields.Many2One(ID)
+
+ person = Person()
+ person.id = 33
+ assert "33" == person.to_xml()
+
+def test_many2one_with_custom_attributes():
+ class TaxAmount(facho.model.Model):
+ __name__ = 'TaxAmount'
+
+ currencyID = fields.Attribute('currencyID')
+
+ class TaxTotal(facho.model.Model):
+ __name__ = 'TaxTotal'
+
+ amount = fields.Many2One(TaxAmount)
+
+ tax_total = TaxTotal()
+ tax_total.amount = 3333
+ tax_total.amount.currencyID = 'COP'
+ assert '3333' == tax_total.to_xml()
+
+def test_many2one_with_custom_setter():
+
+ class PhysicalLocation(facho.model.Model):
+ __name__ = 'PhysicalLocation'
+
+ id = fields.Attribute('ID')
+
+ class Party(facho.model.Model):
+ __name__ = 'Party'
+
+ location = fields.Many2One(PhysicalLocation, setter='location_setter')
+
+ def location_setter(self, field, value):
+ field.id = value
+
+ party = Party()
+ party.location = 99
+ assert '' == party.to_xml()
+
+def test_field_model():
+ class ID(facho.model.Model):
+ __name__ = 'ID'
+
+ class Person(facho.model.Model):
+ __name__ = 'Person'
+
+ id = fields.Model(ID)
+
+ person = Person()
+ person.id = ID()
+ person.id = 33
+ assert "33" == person.to_xml()
+
+def test_field_model_failed_initialization():
+ class ID(facho.model.Model):
+ __name__ = 'ID'
+
+ class Person(facho.model.Model):
+ __name__ = 'Person'
+
+ id = fields.Model(ID)
+
+
+ person = Person()
+ person.id = 33
+ assert "33" == person.to_xml()
+
+def test_field_model_default_initialization_with_attributes():
+ class ID(facho.model.Model):
+ __name__ = 'ID'
+
+ reference = fields.Attribute('REFERENCE')
+
+ class Person(facho.model.Model):
+ __name__ = 'Person'
+
+ id = fields.Model(ID)
+
+ person = Person()
+ person.id = 33
+ person.id.reference = 'haber'
+ assert '33' == person.to_xml()
+
+def test_model_with_xml_namespace():
+ class Person(facho.model.Model):
+ __name__ = 'Person'
+ __namespace__ = {
+ 'facho': 'http://lib.facho.cyou'
+ }
+
+ person = Person()
+ assert ''
+
+def test_model_with_xml_namespace_nested():
+ class ID(facho.model.Model):
+ __name__ = 'ID'
+
+ class Person(facho.model.Model):
+ __name__ = 'Person'
+ __namespace__ = {
+ 'facho': 'http://lib.facho.cyou'
+ }
+
+ id = fields.Many2One(ID, namespace='facho')
+
+ person = Person()
+ person.id = 33
+ assert '33' == person.to_xml()
+
+def test_field_model_with_namespace():
+ class ID(facho.model.Model):
+ __name__ = 'ID'
+
+ class Person(facho.model.Model):
+ __name__ = 'Person'
+ __namespace__ = {
+ "facho": "http://lib.facho.cyou"
+ }
+ id = fields.Model(ID, namespace="facho")
+
+
+ person = Person()
+ person.id = 33
+ assert '33' == person.to_xml()