From 7d060e1786d2098a234684efc87521875ef0e84b Mon Sep 17 00:00:00 2001 From: bit4bit Date: Wed, 23 Jun 2021 13:55:41 +0000 Subject: [PATCH 01/38] Create new branch named "model" FossilOrigin-Name: 55404ca978d7476847f2b6de1d9e8d78bd2ad67200d63695501e1a3a22265eb5 From 84996066fac90d1ebbaae524fbf18136b517c890 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Wed, 23 Jun 2021 23:04:00 +0000 Subject: [PATCH 02/38] se implementa un esquema para modelar el xml FossilOrigin-Name: e4de658f60fe8fcbb330923e14958a5d8f8e0e6395db4f992ec7da45062fa193 --- facho/model/__init__.py | 62 ++++++++++++ facho/model/fields/__init__.py | 5 + facho/model/fields/attribute.py | 17 ++++ facho/model/fields/field.py | 21 ++++ facho/model/fields/many2one.py | 35 +++++++ facho/model/fields/model.py | 26 +++++ tests/test_model.py | 167 ++++++++++++++++++++++++++++++++ 7 files changed, 333 insertions(+) create mode 100644 facho/model/__init__.py create mode 100644 facho/model/fields/__init__.py create mode 100644 facho/model/fields/attribute.py create mode 100644 facho/model/fields/field.py create mode 100644 facho/model/fields/many2one.py create mode 100644 facho/model/fields/model.py create mode 100644 tests/test_model.py 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" % (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() From d78a429711e39666111dbab122a4ed2ed493ad6a Mon Sep 17 00:00:00 2001 From: bit4bit Date: Thu, 24 Jun 2021 01:04:49 +0000 Subject: [PATCH 03/38] se adiciona field.Function FossilOrigin-Name: 45a288bc30ad9b25fed59cd01c89bd2f7632926083384a7853c3b753a4d7f95b --- facho/model/__init__.py | 11 +++++++ facho/model/fields/__init__.py | 1 + facho/model/fields/field.py | 6 ++++ tests/test_model.py | 60 ++++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+) diff --git a/facho/model/__init__.py b/facho/model/__init__.py index 1f6c14b..fe8cc66 100644 --- a/facho/model/__init__.py +++ b/facho/model/__init__.py @@ -28,7 +28,18 @@ class ModelBase(object, metaclass=ModelMeta): def __getitem__(self, key): return self._xml_attributes[key] + def __before_xml__(self): + pass + + def _hook_before_xml(self): + self.__before_xml__() + for field in self._fields.values(): + if hasattr(field, '__before_xml__'): + field.__before_xml__() + def to_xml(self): + self._hook_before_xml() + tag = self.__name__ ns = '' if self._namespace_prefix is not None: diff --git a/facho/model/fields/__init__.py b/facho/model/fields/__init__.py index f368aaf..fb85bc9 100644 --- a/facho/model/fields/__init__.py +++ b/facho/model/fields/__init__.py @@ -1,5 +1,6 @@ from .attribute import Attribute from .many2one import Many2One from .model import Model +from .function import Function __all__ = [Attribute, Many2One, Model] diff --git a/facho/model/fields/field.py b/facho/model/fields/field.py index 251223e..e9c09ec 100644 --- a/facho/model/fields/field.py +++ b/facho/model/fields/field.py @@ -19,3 +19,9 @@ class Field: if name not in namespaces: raise KeyError("namespace %s not found" % (name)) inst._namespace_prefix = name + + def _call(self, inst, method, *args): + call = getattr(inst, method or '', None) + + if callable(call): + return call(*args) diff --git a/tests/test_model.py b/tests/test_model.py index 8aeb7e1..9d27d35 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -165,3 +165,63 @@ def test_field_model_with_namespace(): person = Person() person.id = 33 assert '33' == person.to_xml() + +def test_field_hook_before_xml(): + class Hash(facho.model.Model): + __name__ = 'Hash' + + class Person(facho.model.Model): + __name__ = 'Person' + + hash = fields.Model(Hash) + + def __before_xml__(self): + self.hash = "calculate" + + person = Person() + assert "calculate" == person.to_xml() + + +def test_field_function_with_attribute(): + class Person(facho.model.Model): + __name__ = 'Person' + + hash = fields.Function('get_hash', field=fields.Attribute('hash')) + + def get_hash(self, name, field): + return 'calculate' + + person = Person() + assert '' + +def test_field_function(): + class Person(facho.model.Model): + __name__ = 'Person' + + hash = fields.Function('get_hash') + + def get_hash(self, name): + return 'calculate' + + person = Person() + assert person.hash == 'calculate' + assert "" == person.to_xml() + + +def test_field_function_setter(): + class Person(facho.model.Model): + __name__ = 'Person' + + hash = fields.Attribute('hash') + password = fields.Function('get_hash', setter='set_hash') + + def get_hash(self, name): + return None + + def set_hash(self, name, value): + self.hash = "%s+2" % (value) + + person = Person() + person.password = 'calculate' + assert '' == person.to_xml() + From 49feee88091553b0ef8a3a028b46ee902fb8a923 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Thu, 24 Jun 2021 01:15:16 +0000 Subject: [PATCH 04/38] se adiciona mas pruebas FossilOrigin-Name: 68e716388ee3328b1b451997eca99dc1f20b47db4ebe3dfc761daec6fec3c8d6 --- tests/test_model.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_model.py b/tests/test_model.py index 9d27d35..b2c63b4 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -194,6 +194,23 @@ def test_field_function_with_attribute(): person = Person() assert '' +def test_field_function_with_model(): + class Hash(facho.model.Model): + __name__ = 'Hash' + + id = fields.Attribute('id') + + class Person(facho.model.Model): + __name__ = 'Person' + + hash = fields.Function('get_hash', field=fields.Model(Hash)) + + def get_hash(self, name, field): + field.id = 'calculate' + + person = Person() + assert '' + def test_field_function(): class Person(facho.model.Model): __name__ = 'Person' From 6cc4610b458603a3999710f7a04712b9363bd145 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Thu, 24 Jun 2021 01:18:27 +0000 Subject: [PATCH 05/38] fields.Model se permite cambiar el nombre de la etiqueta FossilOrigin-Name: 896b797629e426a5e366d5be76fc00c3cc272299d6749e40f8317893b1545a9e --- facho/model/fields/model.py | 5 ++++- tests/test_model.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/facho/model/fields/model.py b/facho/model/fields/model.py index 162be39..073504d 100644 --- a/facho/model/fields/model.py +++ b/facho/model/fields/model.py @@ -1,9 +1,10 @@ from .field import Field class Model(Field): - def __init__(self, model, namespace=None): + def __init__(self, model, name=None, namespace=None): self.model = model self.namespace = namespace + self.field_name = name def __get__(self, inst, cls): if inst is None: @@ -20,6 +21,8 @@ class Model(Field): return inst._fields[self.name] except KeyError: obj = self.model() + if self.field_name is not None: + obj.__name__ = self.field_name 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 index b2c63b4..a4fd540 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -108,6 +108,20 @@ def test_field_model_failed_initialization(): person.id = 33 assert "33" == person.to_xml() +def test_field_model_with_custom_name(): + class ID(facho.model.Model): + __name__ = 'ID' + + class Person(facho.model.Model): + __name__ = 'Person' + + id = fields.Model(ID, name='DID') + + + 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' From 0216d0141af6ddf48bfa00a5009d3e7ee123aa1b Mon Sep 17 00:00:00 2001 From: bit4bit Date: Thu, 24 Jun 2021 01:23:51 +0000 Subject: [PATCH 06/38] se adiciona archivo faltante FossilOrigin-Name: 9b0c4d69d898ddbcd9279b3055e72df525feaefb0d46700dc6c019acdba01e80 --- facho/model/fields/function.py | 24 ++++++++++++++++++++++++ tests/test_model.py | 6 ++++-- 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 facho/model/fields/function.py diff --git a/facho/model/fields/function.py b/facho/model/fields/function.py new file mode 100644 index 0000000..34e3195 --- /dev/null +++ b/facho/model/fields/function.py @@ -0,0 +1,24 @@ +from .field import Field +from .model import Model + +class Function(Field): + def __init__(self, getter, setter=None, field=None): + self.field = field + self.getter = getter + self.setter = setter + + def __get__(self, inst, cls): + if inst is None: + return self + assert self.name is not None + + if self.field is None: + return self._call(inst, self.getter, self.name) + else: + obj = Model(self.field) + return self._call(inst, self.getter, self.name, obj) + + def __set__(self, inst, value): + if self.setter is None: + return super().__set__(self.name, value) + self._call(inst, self.setter, self.name, value) diff --git a/tests/test_model.py b/tests/test_model.py index a4fd540..fa92e9f 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -217,13 +217,15 @@ def test_field_function_with_model(): class Person(facho.model.Model): __name__ = 'Person' - hash = fields.Function('get_hash', field=fields.Model(Hash)) + hash = fields.Function('get_hash', field=Hash) def get_hash(self, name, field): field.id = 'calculate' + return field person = Person() - assert '' + assert person.hash.id == 'calculate' + assert '' def test_field_function(): class Person(facho.model.Model): From a015a9361b8b5b2e2120398db07134e8978debb5 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Thu, 24 Jun 2021 01:28:07 +0000 Subject: [PATCH 07/38] fields.Function no requiere getter FossilOrigin-Name: 47f9b9427ef55c688678001361260e5d00ea53d82977ea13e3414ed04878fb36 --- facho/model/fields/function.py | 8 +++++++- tests/test_model.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/facho/model/fields/function.py b/facho/model/fields/function.py index 34e3195..9ea2ac3 100644 --- a/facho/model/fields/function.py +++ b/facho/model/fields/function.py @@ -2,7 +2,7 @@ from .field import Field from .model import Model class Function(Field): - def __init__(self, getter, setter=None, field=None): + def __init__(self, getter=None, setter=None, field=None): self.field = field self.getter = getter self.setter = setter @@ -12,6 +12,12 @@ class Function(Field): return self assert self.name is not None + if self.getter is None and self.field is None: + return None + + if self.getter is None and self.field is not None: + return Model(self.field) + if self.field is None: return self._call(inst, self.getter, self.name) else: diff --git a/tests/test_model.py b/tests/test_model.py index fa92e9f..cfcd65c 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -257,4 +257,18 @@ def test_field_function_setter(): person = Person() person.password = 'calculate' assert '' == person.to_xml() + +def test_field_function_only_setter(): + class Person(facho.model.Model): + __name__ = 'Person' + + hash = fields.Attribute('hash') + password = fields.Function(setter='set_hash') + + def set_hash(self, name, value): + self.hash = "%s+2" % (value) + + person = Person() + person.password = 'calculate' + assert '' == person.to_xml() From 58e73872924c08c0401308f52e800c27ab8db69d Mon Sep 17 00:00:00 2001 From: bit4bit Date: Thu, 24 Jun 2021 01:37:05 +0000 Subject: [PATCH 08/38] fields.Model se adiciona __default_set__ para remplaza comportamiento de asignacion directa FossilOrigin-Name: 436c5483cf534c8d457fb403302e511e7aad4b220d66569612f7ceb2da8d8cf8 --- facho/model/__init__.py | 6 ++++++ facho/model/fields/many2one.py | 2 +- facho/model/fields/model.py | 2 +- tests/test_model.py | 16 ++++++++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/facho/model/__init__.py b/facho/model/__init__.py index fe8cc66..13d7c13 100644 --- a/facho/model/__init__.py +++ b/facho/model/__init__.py @@ -31,6 +31,12 @@ class ModelBase(object, metaclass=ModelMeta): def __before_xml__(self): pass + def __default_set__(self, value): + return str(value) + + def _set_content(self, value): + self._text = str(self.__default_set__(value)) + def _hook_before_xml(self): self.__before_xml__() for field in self._fields.values(): diff --git a/facho/model/fields/many2one.py b/facho/model/fields/many2one.py index 7f5e35f..4b5caaa 100644 --- a/facho/model/fields/many2one.py +++ b/facho/model/fields/many2one.py @@ -29,7 +29,7 @@ class Many2One(Field): if callable(setter): setter(inst_model, value) else: - inst_model._text = str(value) + inst_model._set_content(value) diff --git a/facho/model/fields/model.py b/facho/model/fields/model.py index 073504d..9653f5a 100644 --- a/facho/model/fields/model.py +++ b/facho/model/fields/model.py @@ -14,7 +14,7 @@ class Model(Field): def __set__(self, inst, value): obj = self._create_model(inst) - obj._text = str(value) + obj._set_content(value) def _create_model(self, inst): try: diff --git a/tests/test_model.py b/tests/test_model.py index cfcd65c..4b1011b 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -272,3 +272,19 @@ def test_field_function_only_setter(): person.password = 'calculate' assert '' == person.to_xml() + +def test_model_set_default_setter(): + class Hash(facho.model.Model): + __name__ = 'Hash' + + def __default_set__(self, value): + return "%s+3" % (value) + + class Person(facho.model.Model): + __name__ = 'Person' + + hash = fields.Many2One(Hash) + + person = Person() + person.hash = 'hola' + assert 'hola+3' == person.to_xml() From 92bae58e514f1535f65f754ba3073fea47938c8c Mon Sep 17 00:00:00 2001 From: bit4bit Date: Thu, 24 Jun 2021 01:51:05 +0000 Subject: [PATCH 09/38] se instancia modelo en caso de no existir para Many2One FossilOrigin-Name: 006f6a780ae0436649addd2abe89eb6a9bfc5ad573ee1a1835a8f65ab039fd26 --- facho/model/fields/field.py | 14 ++++++++++++++ facho/model/fields/many2one.py | 8 ++------ facho/model/fields/model.py | 18 +++--------------- tests/test_model.py | 16 ++++++++++++++++ 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/facho/model/fields/field.py b/facho/model/fields/field.py index e9c09ec..6d8e523 100644 --- a/facho/model/fields/field.py +++ b/facho/model/fields/field.py @@ -25,3 +25,17 @@ class Field: if callable(call): return call(*args) + + def _create_model(self, inst, name=None, model=None): + try: + return inst._fields[self.name] + except KeyError: + if model is not None: + obj = model() + else: + obj = self.model() + if name is not None: + obj.__name__ = name + self._set_namespace(obj, self.namespace, inst.__namespace__) + inst._fields[self.name] = obj + return obj diff --git a/facho/model/fields/many2one.py b/facho/model/fields/many2one.py index 4b5caaa..1f6670c 100644 --- a/facho/model/fields/many2one.py +++ b/facho/model/fields/many2one.py @@ -13,15 +13,11 @@ class Many2One(Field): if inst is None: return self assert self.name is not None - return inst._fields[self.name] + return self._create_model(inst) 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 + inst_model = self._create_model(inst, model=self.model) # si hay setter manual se ejecuta # de lo contrario se asigna como texto del elemento diff --git a/facho/model/fields/model.py b/facho/model/fields/model.py index 9653f5a..7ae217a 100644 --- a/facho/model/fields/model.py +++ b/facho/model/fields/model.py @@ -10,20 +10,8 @@ class Model(Field): if inst is None: return self assert self.name is not None - return self._create_model(inst) + return self._create_model(inst, name=self.field_name) def __set__(self, inst, value): - obj = self._create_model(inst) - obj._set_content(value) - - def _create_model(self, inst): - try: - return inst._fields[self.name] - except KeyError: - obj = self.model() - if self.field_name is not None: - obj.__name__ = self.field_name - self._set_namespace(obj, self.namespace, inst.__namespace__) - inst._fields[self.name] = obj - return obj - + obj = self._create_model(inst, name=self.field_name) + obj._set_content(value) diff --git a/tests/test_model.py b/tests/test_model.py index 4b1011b..fb2a30c 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -80,6 +80,22 @@ def test_many2one_with_custom_setter(): party.location = 99 assert '' == party.to_xml() +def test_many2one_auto_create(): + 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.currencyID = 'COP' + tax_total.amount = 3333 + assert '3333' == tax_total.to_xml() + def test_field_model(): class ID(facho.model.Model): __name__ = 'ID' From ba4e3d546f8c9a0ee872e5bab6fb9c199e838e09 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Thu, 24 Jun 2021 02:10:46 +0000 Subject: [PATCH 10/38] se depreca fields.Model port fields.Many2One FossilOrigin-Name: 73d74488ca7458ff7dc84898ff76fa9b16b427cc6bc77540d7c81450e4f33869 --- facho/model/fields/many2one.py | 10 ++++------ facho/model/fields/model.py | 4 +++- tests/test_model.py | 27 +++++++++++++++++++++------ 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/facho/model/fields/many2one.py b/facho/model/fields/many2one.py index 1f6670c..0ead219 100644 --- a/facho/model/fields/many2one.py +++ b/facho/model/fields/many2one.py @@ -1,23 +1,21 @@ from .field import Field class Many2One(Field): - def __init__(self, model, setter=None, namespace=None): + def __init__(self, model, name=None, setter=None, namespace=None): self.model = model self.setter = setter self.namespace = namespace - - def __set_name__(self, owner, name): - self.name = name + self.field_name = name def __get__(self, inst, cls): if inst is None: return self assert self.name is not None - return self._create_model(inst) + return self._create_model(inst, name=self.field_name) def __set__(self, inst, value): assert self.name is not None - inst_model = self._create_model(inst, model=self.model) + inst_model = self._create_model(inst, name=self.field_name, model=self.model) # si hay setter manual se ejecuta # de lo contrario se asigna como texto del elemento diff --git a/facho/model/fields/model.py b/facho/model/fields/model.py index 7ae217a..ad48212 100644 --- a/facho/model/fields/model.py +++ b/facho/model/fields/model.py @@ -1,11 +1,13 @@ from .field import Field +import warnings class Model(Field): def __init__(self, model, name=None, namespace=None): self.model = model self.namespace = namespace self.field_name = name - + warnings.warn('deprecated use Many2One instead') + def __get__(self, inst, cls): if inst is None: return self diff --git a/tests/test_model.py b/tests/test_model.py index fb2a30c..220a4ec 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -103,13 +103,28 @@ def test_field_model(): class Person(facho.model.Model): __name__ = 'Person' - id = fields.Model(ID) + id = fields.Many2One(ID) person = Person() person.id = ID() person.id = 33 assert "33" == person.to_xml() +def test_field_multiple_model(): + class ID(facho.model.Model): + __name__ = 'ID' + + class Person(facho.model.Model): + __name__ = 'Person' + + id = fields.Many2One(ID) + id2 = fields.Many2One(ID) + + person = Person() + person.id = 33 + person.id2 = 44 + assert "3344" == person.to_xml() + def test_field_model_failed_initialization(): class ID(facho.model.Model): __name__ = 'ID' @@ -117,7 +132,7 @@ def test_field_model_failed_initialization(): class Person(facho.model.Model): __name__ = 'Person' - id = fields.Model(ID) + id = fields.Many2One(ID) person = Person() @@ -131,7 +146,7 @@ def test_field_model_with_custom_name(): class Person(facho.model.Model): __name__ = 'Person' - id = fields.Model(ID, name='DID') + id = fields.Many2One(ID, name='DID') person = Person() @@ -147,7 +162,7 @@ def test_field_model_default_initialization_with_attributes(): class Person(facho.model.Model): __name__ = 'Person' - id = fields.Model(ID) + id = fields.Many2One(ID) person = Person() person.id = 33 @@ -189,7 +204,7 @@ def test_field_model_with_namespace(): __namespace__ = { "facho": "http://lib.facho.cyou" } - id = fields.Model(ID, namespace="facho") + id = fields.Many2One(ID, namespace="facho") person = Person() @@ -203,7 +218,7 @@ def test_field_hook_before_xml(): class Person(facho.model.Model): __name__ = 'Person' - hash = fields.Model(Hash) + hash = fields.Many2One(Hash) def __before_xml__(self): self.hash = "calculate" From f630a544c2070210125f678cdb4c26a4762565e9 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Thu, 24 Jun 2021 02:11:33 +0000 Subject: [PATCH 11/38] se inicia modelo de facturacion usando nuevo esquema FossilOrigin-Name: 8e6c23e7baa837c64b81baaed342b07eaab7ab631302cd2a8fa86f4989227d07 --- facho/fe/model/__init__.py | 65 +++++++++++++++++++++++++++++++++++++ tests/test_model_invoice.py | 19 +++++++++++ 2 files changed, 84 insertions(+) create mode 100644 facho/fe/model/__init__.py create mode 100644 tests/test_model_invoice.py diff --git a/facho/fe/model/__init__.py b/facho/fe/model/__init__.py new file mode 100644 index 0000000..42536d6 --- /dev/null +++ b/facho/fe/model/__init__.py @@ -0,0 +1,65 @@ +import facho.model as model +import facho.model.fields as fields +from datetime import date, datetime + +class Date(model.Model): + __name__ = 'Date' + + def __default_set__(self, value): + if isinstance(value, str): + return value + if isinstance(value, date): + return value.isoformat() + +class Time(model.Model): + __name__ = 'Time' + + def __default_set__(self, value): + if isinstance(value, str): + return value + if isinstance(value, date): + return value.strftime('%H:%M%S-05:00') + +class InvoicePeriod(model.Model): + __name__ = 'InvoicePeriod' + + start_date = fields.Many2One(Date, name='StartDate') + + end_date = fields.Many2One(Date, name='EndDate') + +class ID(model.Model): + __name__ = 'ID' + +class Party(model.Model): + __name__ = 'Party' + + id = fields.Many2One(ID) + +class AccountingCustomerParty(model.Model): + __name__ = 'AccountingCustomerParty' + + party = fields.Many2One(Party) + +class AccountingSupplierParty(model.Model): + __name__ = 'AccountingSupplierParty' + + party = fields.Many2One(Party) + +class Invoice(model.Model): + __name__ = 'Invoice' + + id = fields.Many2One(ID) + issue = fields.Function(setter='set_issue') + issue_date = fields.Many2One(Date, name='IssueDate') + issue_time = fields.Many2One(Time, name='IssueTime') + + period = fields.Many2One(InvoicePeriod) + + supplier = fields.Many2One(AccountingSupplierParty) + customer = fields.Many2One(AccountingCustomerParty) + + def set_issue(self, name, value): + if not isinstance(value, datetime): + raise ValueError('expected type datetime') + self.issue_date = value + self.issue_time = value diff --git a/tests/test_model_invoice.py b/tests/test_model_invoice.py new file mode 100644 index 0000000..30f5b91 --- /dev/null +++ b/tests/test_model_invoice.py @@ -0,0 +1,19 @@ +#!/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. + +"""Nuevo esquema para modelar segun decreto""" + +from datetime import datetime + +import pytest + +import facho.fe.model as model + +def test_simple_invoice(): + invoice = model.Invoice() + invoice.id = '323200000129' + invoice.issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z') + invoice.supplier.party.id = '700085371' + invoice.customer.party.id = '800199436' From 3eacb29afabd4cf92ffd52abe55201ae4a7d7f47 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Thu, 24 Jun 2021 23:38:28 +0000 Subject: [PATCH 12/38] se separa responsabilidad de fields.Function FossilOrigin-Name: 4d5daa47a75a0e283e86bf992126bf60f3a8a14287e9acc437d5f2f3eca43150 --- facho/fe/model/__init__.py | 2 +- facho/model/fields/__init__.py | 3 ++- facho/model/fields/attribute.py | 9 ++++---- facho/model/fields/function.py | 25 ++++++++------------ tests/test_model.py | 41 +++++++++++++++------------------ 5 files changed, 36 insertions(+), 44 deletions(-) diff --git a/facho/fe/model/__init__.py b/facho/fe/model/__init__.py index 42536d6..a2cbe20 100644 --- a/facho/fe/model/__init__.py +++ b/facho/fe/model/__init__.py @@ -49,7 +49,7 @@ class Invoice(model.Model): __name__ = 'Invoice' id = fields.Many2One(ID) - issue = fields.Function(setter='set_issue') + issue = fields.Virtual(setter='set_issue') issue_date = fields.Many2One(Date, name='IssueDate') issue_time = fields.Many2One(Time, name='IssueTime') diff --git a/facho/model/fields/__init__.py b/facho/model/fields/__init__.py index fb85bc9..77d3bef 100644 --- a/facho/model/fields/__init__.py +++ b/facho/model/fields/__init__.py @@ -2,5 +2,6 @@ from .attribute import Attribute from .many2one import Many2One from .model import Model from .function import Function +from .virtual import Virtual -__all__ = [Attribute, Many2One, Model] +__all__ = [Attribute, Many2One, Model, Virtual] diff --git a/facho/model/fields/attribute.py b/facho/model/fields/attribute.py index 2ad9f73..e8fd7a0 100644 --- a/facho/model/fields/attribute.py +++ b/facho/model/fields/attribute.py @@ -1,17 +1,18 @@ from .field import Field class Attribute(Field): - def __init__(self, tag): + def __init__(self, tag, default=None): self.tag = tag - + self.value = default + 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 + return self.value def __set__(self, inst, value): assert self.name is not None + self.value = value inst._xml_attributes[self.name] = (self.tag, value) diff --git a/facho/model/fields/function.py b/facho/model/fields/function.py index 9ea2ac3..2294041 100644 --- a/facho/model/fields/function.py +++ b/facho/model/fields/function.py @@ -2,29 +2,24 @@ from .field import Field from .model import Model class Function(Field): - def __init__(self, getter=None, setter=None, field=None): + def __init__(self, field, getter=None): self.field = field self.getter = getter - self.setter = setter def __get__(self, inst, cls): if inst is None: return self assert self.name is not None - if self.getter is None and self.field is None: - return None + # si se indica `field` se adiciona + # como campo del modelo, esto es + # que se serializa a xml + inst._fields[self.name] = self.field - if self.getter is None and self.field is not None: - return Model(self.field) + if self.getter is not None: + value = self._call(inst, self.getter, self.name, self.field) - if self.field is None: - return self._call(inst, self.getter, self.name) - else: - obj = Model(self.field) - return self._call(inst, self.getter, self.name, obj) + if value is not None: + self.field.__set__(inst, value) - def __set__(self, inst, value): - if self.setter is None: - return super().__set__(self.name, value) - self._call(inst, self.setter, self.name, value) + return self.field diff --git a/tests/test_model.py b/tests/test_model.py index 220a4ec..b42b473 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -231,7 +231,7 @@ def test_field_function_with_attribute(): class Person(facho.model.Model): __name__ = 'Person' - hash = fields.Function('get_hash', field=fields.Attribute('hash')) + hash = fields.Function(fields.Attribute('hash'), getter='get_hash') def get_hash(self, name, field): return 'calculate' @@ -248,39 +248,23 @@ def test_field_function_with_model(): class Person(facho.model.Model): __name__ = 'Person' - hash = fields.Function('get_hash', field=Hash) + hash = fields.Function(fields.Many2One(Hash), getter='get_hash') def get_hash(self, name, field): field.id = 'calculate' - return field + person = Person() assert person.hash.id == 'calculate' assert '' - -def test_field_function(): - class Person(facho.model.Model): - __name__ = 'Person' - - hash = fields.Function('get_hash') - - def get_hash(self, name): - return 'calculate' - - person = Person() - assert person.hash == 'calculate' - assert "" == person.to_xml() - + def test_field_function_setter(): class Person(facho.model.Model): __name__ = 'Person' hash = fields.Attribute('hash') - password = fields.Function('get_hash', setter='set_hash') - - def get_hash(self, name): - return None + password = fields.Virtual(setter='set_hash') def set_hash(self, name, value): self.hash = "%s+2" % (value) @@ -294,7 +278,7 @@ def test_field_function_only_setter(): __name__ = 'Person' hash = fields.Attribute('hash') - password = fields.Function(setter='set_hash') + password = fields.Virtual(setter='set_hash') def set_hash(self, name, value): self.hash = "%s+2" % (value) @@ -303,7 +287,6 @@ def test_field_function_only_setter(): person.password = 'calculate' assert '' == person.to_xml() - def test_model_set_default_setter(): class Hash(facho.model.Model): __name__ = 'Hash' @@ -319,3 +302,15 @@ def test_model_set_default_setter(): person = Person() person.hash = 'hola' assert 'hola+3' == person.to_xml() + + +def test_field_virtual(): + class Person(facho.model.Model): + __name__ = 'Person' + + age = fields.Virtual() + + person = Person() + person.age = 55 + assert person.age == 55 + assert "" == person.to_xml() From a9dde83e81701c3bcf61e865f091edd373c93a8a Mon Sep 17 00:00:00 2001 From: bit4bit Date: Fri, 25 Jun 2021 01:39:02 +0000 Subject: [PATCH 13/38] se adiciona atributo default a many2one y attribute FossilOrigin-Name: 9ddb1d1b8bebef24da17cc47d8fc70392f6015bb61866f251992aea518ed3d0f --- facho/model/__init__.py | 18 +++++++++++---- facho/model/fields/__init__.py | 3 ++- facho/model/fields/attribute.py | 1 + facho/model/fields/function.py | 1 + facho/model/fields/many2one.py | 3 ++- tests/test_model.py | 41 +++++++++++++++++++++++++++++++++ 6 files changed, 61 insertions(+), 6 deletions(-) diff --git a/facho/model/__init__.py b/facho/model/__init__.py index 13d7c13..33cd824 100644 --- a/facho/model/__init__.py +++ b/facho/model/__init__.py @@ -1,3 +1,4 @@ +from .fields import Field class ModelMeta(type): def __new__(cls, name, bases, ns): @@ -19,7 +20,15 @@ class ModelBase(object, metaclass=ModelMeta): obj._fields = {} obj._text = "" obj._namespace_prefix = None - + + # forzamos registros de campos al modelo + # al instanciar + for (key, v) in type(obj).__dict__.items(): + if isinstance(v, fields.Attribute) or isinstance(v, fields.Many2One): + if hasattr(v, 'default') and v.default is not None: + setattr(obj, key, v.default) + + return obj def __setitem__(self, key, val): @@ -32,10 +41,12 @@ class ModelBase(object, metaclass=ModelMeta): pass def __default_set__(self, value): - return str(value) + return value def _set_content(self, value): - self._text = str(self.__default_set__(value)) + default = self.__default_set__(value) + if default is not None: + self._text = str(default) def _hook_before_xml(self): self.__before_xml__() @@ -63,7 +74,6 @@ class ModelBase(object, metaclass=ModelMeta): 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 diff --git a/facho/model/fields/__init__.py b/facho/model/fields/__init__.py index 77d3bef..4e35a11 100644 --- a/facho/model/fields/__init__.py +++ b/facho/model/fields/__init__.py @@ -3,5 +3,6 @@ from .many2one import Many2One from .model import Model from .function import Function from .virtual import Virtual +from .field import Field -__all__ = [Attribute, Many2One, Model, Virtual] +__all__ = [Attribute, Many2One, Model, Virtual, Field] diff --git a/facho/model/fields/attribute.py b/facho/model/fields/attribute.py index e8fd7a0..e89268a 100644 --- a/facho/model/fields/attribute.py +++ b/facho/model/fields/attribute.py @@ -4,6 +4,7 @@ class Attribute(Field): def __init__(self, tag, default=None): self.tag = tag self.value = default + self.default = default def __get__(self, inst, cls): if inst is None: diff --git a/facho/model/fields/function.py b/facho/model/fields/function.py index 2294041..20ba6e9 100644 --- a/facho/model/fields/function.py +++ b/facho/model/fields/function.py @@ -14,6 +14,7 @@ class Function(Field): # si se indica `field` se adiciona # como campo del modelo, esto es # que se serializa a xml + self.field.name = self.name inst._fields[self.name] = self.field if self.getter is not None: diff --git a/facho/model/fields/many2one.py b/facho/model/fields/many2one.py index 0ead219..d75e03b 100644 --- a/facho/model/fields/many2one.py +++ b/facho/model/fields/many2one.py @@ -1,11 +1,12 @@ from .field import Field class Many2One(Field): - def __init__(self, model, name=None, setter=None, namespace=None): + def __init__(self, model, name=None, setter=None, namespace=None, default=None): self.model = model self.setter = setter self.namespace = namespace self.field_name = name + self.default = default def __get__(self, inst, cls): if inst is None: diff --git a/tests/test_model.py b/tests/test_model.py index b42b473..c965649 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -314,3 +314,44 @@ def test_field_virtual(): person.age = 55 assert person.age == 55 assert "" == person.to_xml() + + +def test_field_inserted_default_attribute(): + class Person(facho.model.Model): + __name__ = 'Person' + + hash = fields.Attribute('hash', default='calculate') + + + person = Person() + assert '' == person.to_xml() + +def test_field_inserted_default_many2one(): + class ID(facho.model.Model): + __name__ = 'ID' + + key = fields.Attribute('key') + + def __default_set__(self, value): + self.key = value + + class Person(facho.model.Model): + __name__ = 'Person' + + id = fields.Many2One(ID, default="oe") + + person = Person() + assert '' == person.to_xml() + +def test_field_inserted_default_nested_many2one(): + class ID(facho.model.Model): + __name__ = 'ID' + + class Person(facho.model.Model): + __name__ = 'Person' + + id = fields.Many2One(ID, default="ole") + + person = Person() + assert 'ole' == person.to_xml() + From b6219bd1711d2611b347222d85466782a88c2d8d Mon Sep 17 00:00:00 2001 From: bit4bit Date: Fri, 25 Jun 2021 01:56:49 +0000 Subject: [PATCH 14/38] fields.Function manejan default FossilOrigin-Name: ec954ac9253429b99095fbcce443a2691f516f603282aec1ee59c4b7cbbd6c4a --- facho/model/__init__.py | 35 ++++++++++++++++++++++++++------- facho/model/fields/attribute.py | 6 +++--- facho/model/fields/function.py | 14 ++++++++++--- tests/test_model.py | 9 +++++++++ 4 files changed, 51 insertions(+), 13 deletions(-) diff --git a/facho/model/__init__.py b/facho/model/__init__.py index 33cd824..51a9345 100644 --- a/facho/model/__init__.py +++ b/facho/model/__init__.py @@ -24,25 +24,29 @@ class ModelBase(object, metaclass=ModelMeta): # forzamos registros de campos al modelo # al instanciar for (key, v) in type(obj).__dict__.items(): - if isinstance(v, fields.Attribute) or isinstance(v, fields.Many2One): + if isinstance(v, fields.Attribute) or isinstance(v, fields.Many2One) or isinstance(v, fields.Function): if hasattr(v, 'default') and v.default is not None: setattr(obj, key, v.default) return obj + def _set_attribute(self, field, name, value): + self._xml_attributes[field] = (name, value) + def __setitem__(self, key, val): self._xml_attributes[key] = val def __getitem__(self, key): return self._xml_attributes[key] - def __before_xml__(self): - pass + def _get_field(self, name): + return self._fields[name] + + def _set_field(self, name, field): + field.name = name + self._fields[name] = field - def __default_set__(self, value): - return value - def _set_content(self, value): default = self.__default_set__(value) if default is not None: @@ -55,6 +59,9 @@ class ModelBase(object, metaclass=ModelMeta): field.__before_xml__() def to_xml(self): + """ + Genera xml del modelo y sus relaciones + """ self._hook_before_xml() tag = self.__name__ @@ -85,5 +92,19 @@ class ModelBase(object, metaclass=ModelMeta): return "<%s%s%s>%s" % (ns, tag, attributes, content, ns, tag) class Model(ModelBase): - pass + def __before_xml__(self): + """ + Ejecuta antes de generar el xml, este + metodo sirve para realizar actualizaciones + en los campos en el ultimo momento + """ + pass + + def __default_set__(self, value): + """ + Al asignar un valor al modelo atraves de una relacion (person.relation = '33') + se puede personalizar como hacer esta asignacion. + """ + return value + diff --git a/facho/model/fields/attribute.py b/facho/model/fields/attribute.py index e89268a..9d8677c 100644 --- a/facho/model/fields/attribute.py +++ b/facho/model/fields/attribute.py @@ -1,8 +1,8 @@ from .field import Field class Attribute(Field): - def __init__(self, tag, default=None): - self.tag = tag + def __init__(self, name, default=None): + self.attribute = name self.value = default self.default = default @@ -16,4 +16,4 @@ class Attribute(Field): def __set__(self, inst, value): assert self.name is not None self.value = value - inst._xml_attributes[self.name] = (self.tag, value) + inst._set_attribute(self.name, self.attribute, value) diff --git a/facho/model/fields/function.py b/facho/model/fields/function.py index 20ba6e9..58388e5 100644 --- a/facho/model/fields/function.py +++ b/facho/model/fields/function.py @@ -2,9 +2,14 @@ from .field import Field from .model import Model class Function(Field): - def __init__(self, field, getter=None): + """ + Permite modificar el modelo cuando se intenta, + obtener el valor de este campo. + """ + def __init__(self, field, getter=None, default=None): self.field = field self.getter = getter + self.default = default def __get__(self, inst, cls): if inst is None: @@ -14,8 +19,7 @@ class Function(Field): # si se indica `field` se adiciona # como campo del modelo, esto es # que se serializa a xml - self.field.name = self.name - inst._fields[self.name] = self.field + inst._set_field(self.name, self.field) if self.getter is not None: value = self._call(inst, self.getter, self.name, self.field) @@ -24,3 +28,7 @@ class Function(Field): self.field.__set__(inst, value) return self.field + + def __set__(self, inst, value): + inst._set_field(self.name, self.field) + self.field.__set__(inst, value) diff --git a/tests/test_model.py b/tests/test_model.py index c965649..1028803 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -326,6 +326,15 @@ def test_field_inserted_default_attribute(): person = Person() assert '' == person.to_xml() +def test_field_function_inserted_default_attribute(): + class Person(facho.model.Model): + __name__ = 'Person' + + hash = fields.Function(fields.Attribute('hash'), default='calculate') + + person = Person() + assert '' == person.to_xml() + def test_field_inserted_default_many2one(): class ID(facho.model.Model): __name__ = 'ID' From c694603505eca2af9d2257c3e3ecae4de40d3c8c Mon Sep 17 00:00:00 2001 From: bit4bit Date: Fri, 25 Jun 2021 23:21:04 +0000 Subject: [PATCH 15/38] se adiciona @fields.on_change para ejecutar funcion cuando se cambian los varoles de algun attributo FossilOrigin-Name: bee19b201f8c1a6b972c2a9abfe5fb57a558a67be6ecddce4f7f07b5b6980215 --- facho/model/__init__.py | 21 +++++++++++++++++++- facho/model/fields/__init__.py | 12 +++++++++++ facho/model/fields/attribute.py | 2 ++ facho/model/fields/field.py | 5 +++++ facho/model/fields/function.py | 1 + facho/model/fields/many2one.py | 3 ++- tests/test_model.py | 35 +++++++++++++++++++++++++++++++++ 7 files changed, 77 insertions(+), 2 deletions(-) diff --git a/facho/model/__init__.py b/facho/model/__init__.py index 51a9345..f64bd55 100644 --- a/facho/model/__init__.py +++ b/facho/model/__init__.py @@ -1,4 +1,5 @@ from .fields import Field +from collections import defaultdict class ModelMeta(type): def __new__(cls, name, bases, ns): @@ -20,6 +21,19 @@ class ModelBase(object, metaclass=ModelMeta): obj._fields = {} obj._text = "" obj._namespace_prefix = None + obj._on_change_fields = defaultdict(list) + + def on_change_fields_for_function(function_name): + # se recorre arbol buscando el primero + for parent_cls in type(obj).__mro__: + parent_meth = getattr(parent_cls, function_name, None) + if not parent_meth: + continue + + on_changes = getattr(parent_meth, 'on_changes', None) + if on_changes: + return on_changes + return [] # forzamos registros de campos al modelo # al instanciar @@ -28,7 +42,12 @@ class ModelBase(object, metaclass=ModelMeta): if hasattr(v, 'default') and v.default is not None: setattr(obj, key, v.default) - + # register callbacks for changes + function_name = 'on_change_%s' % (key) + on_change_fields = on_change_fields_for_function(function_name) + for field in on_change_fields: + obj._on_change_fields[field].append(function_name) + return obj def _set_attribute(self, field, name, value): diff --git a/facho/model/fields/__init__.py b/facho/model/fields/__init__.py index 4e35a11..c3f59a3 100644 --- a/facho/model/fields/__init__.py +++ b/facho/model/fields/__init__.py @@ -6,3 +6,15 @@ from .virtual import Virtual from .field import Field __all__ = [Attribute, Many2One, Model, Virtual, Field] + +def on_change(fields): + from functools import wraps + + def decorator(func): + setattr(func, 'on_changes', fields) + + @wraps(func) + def wrapper(self, *arg, **kwargs): + return func(self, *arg, **kwargs) + return wrapper + return decorator diff --git a/facho/model/fields/attribute.py b/facho/model/fields/attribute.py index 9d8677c..6adfc36 100644 --- a/facho/model/fields/attribute.py +++ b/facho/model/fields/attribute.py @@ -16,4 +16,6 @@ class Attribute(Field): def __set__(self, inst, value): assert self.name is not None self.value = value + + self._changed_field(inst, self.name, value) inst._set_attribute(self.name, self.attribute, value) diff --git a/facho/model/fields/field.py b/facho/model/fields/field.py index 6d8e523..d340bbc 100644 --- a/facho/model/fields/field.py +++ b/facho/model/fields/field.py @@ -39,3 +39,8 @@ class Field: self._set_namespace(obj, self.namespace, inst.__namespace__) inst._fields[self.name] = obj return obj + + def _changed_field(self, inst, name, value): + for fun in inst._on_change_fields[name]: + getattr(inst, fun)(name, value) + diff --git a/facho/model/fields/function.py b/facho/model/fields/function.py index 58388e5..1da5e52 100644 --- a/facho/model/fields/function.py +++ b/facho/model/fields/function.py @@ -31,4 +31,5 @@ class Function(Field): def __set__(self, inst, value): inst._set_field(self.name, self.field) + self._changed_field(inst, self.name, value) self.field.__set__(inst, value) diff --git a/facho/model/fields/many2one.py b/facho/model/fields/many2one.py index d75e03b..5134e3d 100644 --- a/facho/model/fields/many2one.py +++ b/facho/model/fields/many2one.py @@ -25,6 +25,7 @@ class Many2One(Field): setter(inst_model, value) else: inst_model._set_content(value) - + + self._changed_field(inst, self.name, value) diff --git a/tests/test_model.py b/tests/test_model.py index 1028803..532c582 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -364,3 +364,38 @@ def test_field_inserted_default_nested_many2one(): person = Person() assert 'ole' == person.to_xml() +def test_model_on_change_field(): + class Hash(facho.model.Model): + __name__ = 'Hash' + + class Person(facho.model.Model): + __name__ = 'Person' + + react = fields.Attribute('react') + hash = fields.Many2One(Hash) + + @fields.on_change(['hash']) + def on_change_react(self, name, value): + assert name == 'hash' + self.react = "%s+4" % (value) + + person = Person() + person.hash = 'hola' + assert 'hola' == person.to_xml() + +def test_model_on_change_field_attribute(): + class Person(facho.model.Model): + __name__ = 'Person' + + react = fields.Attribute('react') + hash = fields.Attribute('Hash') + + @fields.on_change(['hash']) + def on_change_react(self, name, value): + assert name == 'hash' + self.react = "%s+4" % (value) + + person = Person() + person.hash = 'hola' + assert '' == person.to_xml() + From ab462a6ca538c6c3331494166a61610cd14076c0 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Fri, 25 Jun 2021 23:23:07 +0000 Subject: [PATCH 16/38] se retiran archivos no usados FossilOrigin-Name: 05431311b7cfe21cbde218f35b5adaef4171a78ed19c87b9e8bc8119ea091e45 --- facho/model/fields/__init__.py | 3 +-- facho/model/fields/function.py | 1 - facho/model/fields/model.py | 19 ------------------- 3 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 facho/model/fields/model.py diff --git a/facho/model/fields/__init__.py b/facho/model/fields/__init__.py index c3f59a3..df6d582 100644 --- a/facho/model/fields/__init__.py +++ b/facho/model/fields/__init__.py @@ -1,11 +1,10 @@ from .attribute import Attribute from .many2one import Many2One -from .model import Model from .function import Function from .virtual import Virtual from .field import Field -__all__ = [Attribute, Many2One, Model, Virtual, Field] +__all__ = [Attribute, Many2One, Virtual, Field] def on_change(fields): from functools import wraps diff --git a/facho/model/fields/function.py b/facho/model/fields/function.py index 1da5e52..2713d35 100644 --- a/facho/model/fields/function.py +++ b/facho/model/fields/function.py @@ -1,5 +1,4 @@ from .field import Field -from .model import Model class Function(Field): """ diff --git a/facho/model/fields/model.py b/facho/model/fields/model.py deleted file mode 100644 index ad48212..0000000 --- a/facho/model/fields/model.py +++ /dev/null @@ -1,19 +0,0 @@ -from .field import Field -import warnings - -class Model(Field): - def __init__(self, model, name=None, namespace=None): - self.model = model - self.namespace = namespace - self.field_name = name - warnings.warn('deprecated use Many2One instead') - - def __get__(self, inst, cls): - if inst is None: - return self - assert self.name is not None - return self._create_model(inst, name=self.field_name) - - def __set__(self, inst, value): - obj = self._create_model(inst, name=self.field_name) - obj._set_content(value) From 5f5a6182c9973563821a1e765269d316ff49e5aa Mon Sep 17 00:00:00 2001 From: bit4bit Date: Fri, 25 Jun 2021 23:55:36 +0000 Subject: [PATCH 17/38] se adiciona fields.One2Many FossilOrigin-Name: 94c1cca50451a46c417d925b27fdd53d8199b8dc58783e600c84179eac666a36 --- facho/model/fields/__init__.py | 3 ++- facho/model/fields/field.py | 9 +++++++-- facho/model/fields/one2many.py | 25 +++++++++++++++++++++++++ tests/test_model.py | 17 +++++++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 facho/model/fields/one2many.py diff --git a/facho/model/fields/__init__.py b/facho/model/fields/__init__.py index df6d582..6e9408d 100644 --- a/facho/model/fields/__init__.py +++ b/facho/model/fields/__init__.py @@ -1,10 +1,11 @@ from .attribute import Attribute from .many2one import Many2One +from .one2many import One2Many from .function import Function from .virtual import Virtual from .field import Field -__all__ = [Attribute, Many2One, Virtual, Field] +__all__ = [Attribute, One2Many, Many2One, Virtual, Field] def on_change(fields): from functools import wraps diff --git a/facho/model/fields/field.py b/facho/model/fields/field.py index d340bbc..b3b5e2f 100644 --- a/facho/model/fields/field.py +++ b/facho/model/fields/field.py @@ -26,7 +26,7 @@ class Field: if callable(call): return call(*args) - def _create_model(self, inst, name=None, model=None): + def _create_model(self, inst, name=None, model=None, attribute=None): try: return inst._fields[self.name] except KeyError: @@ -37,7 +37,12 @@ class Field: if name is not None: obj.__name__ = name self._set_namespace(obj, self.namespace, inst.__namespace__) - inst._fields[self.name] = obj + + if attribute: + inst._fields[attribute] = obj + else: + inst._fields[self.name] = obj + return obj def _changed_field(self, inst, name, value): diff --git a/facho/model/fields/one2many.py b/facho/model/fields/one2many.py new file mode 100644 index 0000000..571f61d --- /dev/null +++ b/facho/model/fields/one2many.py @@ -0,0 +1,25 @@ +from .field import Field + +class BoundModel: + def __init__(self, creator): + self.creator = creator + + def create(self): + return self.creator() + +class One2Many(Field): + def __init__(self, model, name=None, namespace=None, default=None): + self.model = model + self.field_name = name + self.namespace = namespace + self.default = default + self.count_relations = 0 + + def __get__(self, inst, cls): + assert self.name is not None + def creator(): + attribute = '%s_%d' % (self.name, self.count_relations) + self.count_relations += 1 + return self._create_model(inst, name=self.field_name, model=self.model, attribute=attribute) + + return BoundModel(creator) diff --git a/tests/test_model.py b/tests/test_model.py index 532c582..4754a3d 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -399,3 +399,20 @@ def test_model_on_change_field_attribute(): person.hash = 'hola' assert '' == person.to_xml() +def test_model_one2many(): + class Line(facho.model.Model): + __name__ = 'Line' + + quantity = fields.Attribute('quantity') + + class Invoice(facho.model.Model): + __name__ = 'Invoice' + + lines = fields.One2Many(Line) + + invoice = Invoice() + line = invoice.lines.create() + line.quantity = 3 + line = invoice.lines.create() + line.quantity = 5 + assert '' == invoice.to_xml() From bd25bef21fc1df74e5a2d128836df8a2226ca768 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Sat, 26 Jun 2021 00:11:57 +0000 Subject: [PATCH 18/38] se continua con el nuevo modelado de facturacion FossilOrigin-Name: 68ecac65b7ee5c2884161943e120df58ad596ffd3be82c3ce107ecf00eae6afa --- facho/fe/model/__init__.py | 28 +++++++++++++++++++++++++++- tests/test_model_invoice.py | 7 +++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/facho/fe/model/__init__.py b/facho/fe/model/__init__.py index a2cbe20..f144d32 100644 --- a/facho/fe/model/__init__.py +++ b/facho/fe/model/__init__.py @@ -44,6 +44,31 @@ class AccountingSupplierParty(model.Model): __name__ = 'AccountingSupplierParty' party = fields.Many2One(Party) + +class InvoicedQuantity(model.Model): + __name__ = 'InvoiceQuantity' + + code = fields.Attribute('unitCode', default='NAR') + + +class PriceAmount(model.Model): + __name__ = 'PriceAmount' + + currency = fields.Attribute('currencyID', default='COP') + +class Price(model.Model): + __name__ = 'Price' + + amount = fields.Many2One(PriceAmount) + + def __default_set__(self, value): + self.amount = value + +class InvoiceLine(model.Model): + __name__ = 'InvoiceLine' + + quantity = fields.Many2One(InvoicedQuantity) + price = fields.Many2One(Price) class Invoice(model.Model): __name__ = 'Invoice' @@ -57,7 +82,8 @@ class Invoice(model.Model): supplier = fields.Many2One(AccountingSupplierParty) customer = fields.Many2One(AccountingCustomerParty) - + lines = fields.One2Many(InvoiceLine) + def set_issue(self, name, value): if not isinstance(value, datetime): raise ValueError('expected type datetime') diff --git a/tests/test_model_invoice.py b/tests/test_model_invoice.py index 30f5b91..d85c87b 100644 --- a/tests/test_model_invoice.py +++ b/tests/test_model_invoice.py @@ -10,6 +10,7 @@ from datetime import datetime import pytest import facho.fe.model as model +import facho.fe.form as form def test_simple_invoice(): invoice = model.Invoice() @@ -17,3 +18,9 @@ def test_simple_invoice(): invoice.issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z') invoice.supplier.party.id = '700085371' invoice.customer.party.id = '800199436' + + line = invoice.lines.create() + line.quantity = form.Quantity(1, '94') + line.price = form.Amount(5_000) + + assert '3232000001292019-01-16T10:53:10-05:0010:5310-05:007000853718001994361.05000.0' == invoice.to_xml() From 53b5207e3562ac71e36c9bf87de027555cb58b3e Mon Sep 17 00:00:00 2001 From: bit4bit Date: Sat, 26 Jun 2021 20:25:07 +0000 Subject: [PATCH 19/38] fields.on_changes no requiere un nombre especifico para su ejecucion FossilOrigin-Name: 55d11605df9d1228737da18bf04b242fff3b08939021488f169ad2b042330d6f --- facho/model/__init__.py | 23 +++++++++++------------ facho/model/fields/field.py | 2 +- tests/test_model.py | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/facho/model/__init__.py b/facho/model/__init__.py index f64bd55..9a45981 100644 --- a/facho/model/__init__.py +++ b/facho/model/__init__.py @@ -23,17 +23,17 @@ class ModelBase(object, metaclass=ModelMeta): obj._namespace_prefix = None obj._on_change_fields = defaultdict(list) - def on_change_fields_for_function(function_name): + def on_change_fields_for_function(): # se recorre arbol buscando el primero for parent_cls in type(obj).__mro__: - parent_meth = getattr(parent_cls, function_name, None) - if not parent_meth: - continue - - on_changes = getattr(parent_meth, 'on_changes', None) - if on_changes: - return on_changes - return [] + for parent_attr in dir(parent_cls): + parent_meth = getattr(parent_cls, parent_attr, None) + if not callable(parent_meth): + continue + on_changes = getattr(parent_meth, 'on_changes', None) + if on_changes: + return (parent_meth, on_changes) + return (None, []) # forzamos registros de campos al modelo # al instanciar @@ -43,10 +43,9 @@ class ModelBase(object, metaclass=ModelMeta): setattr(obj, key, v.default) # register callbacks for changes - function_name = 'on_change_%s' % (key) - on_change_fields = on_change_fields_for_function(function_name) + (fun, on_change_fields) = on_change_fields_for_function() for field in on_change_fields: - obj._on_change_fields[field].append(function_name) + obj._on_change_fields[field].append(fun) return obj diff --git a/facho/model/fields/field.py b/facho/model/fields/field.py index b3b5e2f..c447020 100644 --- a/facho/model/fields/field.py +++ b/facho/model/fields/field.py @@ -47,5 +47,5 @@ class Field: def _changed_field(self, inst, name, value): for fun in inst._on_change_fields[name]: - getattr(inst, fun)(name, value) + fun(inst, name, value) diff --git a/tests/test_model.py b/tests/test_model.py index 4754a3d..34f972b 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -391,7 +391,7 @@ def test_model_on_change_field_attribute(): hash = fields.Attribute('Hash') @fields.on_change(['hash']) - def on_change_react(self, name, value): + def on_react(self, name, value): assert name == 'hash' self.react = "%s+4" % (value) From 47a0dd33e2357bd38d2a45e1f7c86ae43a75ff0f Mon Sep 17 00:00:00 2001 From: bit4bit Date: Sat, 26 Jun 2021 21:28:02 +0000 Subject: [PATCH 20/38] se adiciona notificaciones de cambios para fields.One2Many FossilOrigin-Name: b422aa912c7c7873edcbbecf1914e9ff21a24cab3fc7e2a788e00efc16fe2f51 --- facho/model/fields/one2many.py | 56 ++++++++++++++++++++++++++++------ tests/test_model.py | 26 ++++++++++++++++ 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/facho/model/fields/one2many.py b/facho/model/fields/one2many.py index 571f61d..231874b 100644 --- a/facho/model/fields/one2many.py +++ b/facho/model/fields/one2many.py @@ -1,11 +1,46 @@ from .field import Field -class BoundModel: - def __init__(self, creator): +# TODO(bit4bit) lograr que isinstance se aplique +# al objeto envuelto +class _RelationProxy(): + def __init__(self, obj, inst, attribute): + self.__dict__['_obj'] = obj + self.__dict__['_inst'] = inst + self.__dict__['_attribute'] = attribute + + def __getattr__(self, name): + if (name in self.__dict__): + return self.__dict__[name] + + return getattr(self.__dict__['_obj'], name) + + def __setattr__(self, attr, value): + # TODO(bit4bit) hacemos proxy al sistema de notificacion de cambios + # algo burdo, se usa __dict__ para saltarnos el __getattr__ y generar un fallo por recursion + for fun in self.__dict__['_inst']._on_change_fields[self.__dict__['_attribute']]: + fun(self.__dict__['_inst'], self.__dict__['_attribute'], value) + + return setattr(self._obj, attr, value) + +class _Relation(): + def __init__(self, creator, inst, attribute): self.creator = creator + self.inst = inst + self.attribute = attribute + self.relations = [] def create(self): - return self.creator() + n_relations = len(self.relations) + attribute = '%s_%d' % (self.attribute, n_relations) + relation = self.creator(attribute) + proxy = _RelationProxy(relation, self.inst, self.attribute) + + self.relations.append(relation) + return proxy + + def __len__(self): + return len(self.relations) + class One2Many(Field): def __init__(self, model, name=None, namespace=None, default=None): @@ -13,13 +48,16 @@ class One2Many(Field): self.field_name = name self.namespace = namespace self.default = default - self.count_relations = 0 + self.relation = None def __get__(self, inst, cls): assert self.name is not None - def creator(): - attribute = '%s_%d' % (self.name, self.count_relations) - self.count_relations += 1 - return self._create_model(inst, name=self.field_name, model=self.model, attribute=attribute) - return BoundModel(creator) + def creator(attribute): + return self._create_model(inst, name=self.field_name, model=self.model, attribute=attribute) + + if self.relation: + return self.relation + else: + self.relation = _Relation(creator, inst, self.name) + return self.relation diff --git a/tests/test_model.py b/tests/test_model.py index 34f972b..159b5d2 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -416,3 +416,29 @@ def test_model_one2many(): line = invoice.lines.create() line.quantity = 5 assert '' == invoice.to_xml() + + +def test_model_one2many_with_on_changes(): + class Line(facho.model.Model): + __name__ = 'Line' + + quantity = fields.Attribute('quantity') + + class Invoice(facho.model.Model): + __name__ = 'Invoice' + + lines = fields.One2Many(Line) + count = fields.Attribute('count', default=0) + + @fields.on_change(['lines']) + def refresh_count(self, name, value): + self.count = len(self.lines) + + invoice = Invoice() + line = invoice.lines.create() + line.quantity = 3 + line = invoice.lines.create() + line.quantity = 5 + + assert len(invoice.lines) == 2 + assert '' == invoice.to_xml() From 1d6d1e26017ca46dca960f0b22be14083538c237 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Sat, 26 Jun 2021 21:32:24 +0000 Subject: [PATCH 21/38] One2Many se puede iterar como lista FossilOrigin-Name: 1de9daae362feb6693b928c4a58c11423127eb1783fdf509f0ef3083b5563b24 --- facho/model/fields/one2many.py | 3 +++ tests/test_model.py | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/facho/model/fields/one2many.py b/facho/model/fields/one2many.py index 231874b..182a1b0 100644 --- a/facho/model/fields/one2many.py +++ b/facho/model/fields/one2many.py @@ -41,6 +41,9 @@ class _Relation(): def __len__(self): return len(self.relations) + def __iter__(self): + for relation in self.relations: + yield relation class One2Many(Field): def __init__(self, model, name=None, namespace=None, default=None): diff --git a/tests/test_model.py b/tests/test_model.py index 159b5d2..d588c14 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -442,3 +442,27 @@ def test_model_one2many_with_on_changes(): assert len(invoice.lines) == 2 assert '' == invoice.to_xml() + +def test_model_one2many_as_list(): + class Line(facho.model.Model): + __name__ = 'Line' + + quantity = fields.Attribute('quantity') + + class Invoice(facho.model.Model): + __name__ = 'Invoice' + + lines = fields.One2Many(Line) + + invoice = Invoice() + line = invoice.lines.create() + line.quantity = 3 + line = invoice.lines.create() + line.quantity = 5 + + lines = list(invoice.lines) + assert len(list(invoice.lines)) == 2 + + for line in lines: + assert isinstance(line, Line) + assert '' == invoice.to_xml() From 4f159266560063f69fe6f8ae0ebb517da8c5fa31 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Sat, 26 Jun 2021 22:05:30 +0000 Subject: [PATCH 22/38] se adiciona mas modelos a nuevo esquema FossilOrigin-Name: acac57e60f808abdd89937be338d819f4f6fa9f8b4dda725569f445f96c982d3 --- facho/fe/model/__init__.py | 46 +++++++++++++++++++++++++++++++--- facho/model/__init__.py | 5 ++-- facho/model/fields/one2many.py | 1 - tests/test_model_invoice.py | 6 ++++- 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/facho/fe/model/__init__.py b/facho/fe/model/__init__.py index f144d32..74aaf01 100644 --- a/facho/fe/model/__init__.py +++ b/facho/fe/model/__init__.py @@ -2,6 +2,9 @@ import facho.model as model import facho.model.fields as fields from datetime import date, datetime +class Name(model.Model): + __name__ = 'Name' + class Date(model.Model): __name__ = 'Date' @@ -51,23 +54,60 @@ class InvoicedQuantity(model.Model): code = fields.Attribute('unitCode', default='NAR') -class PriceAmount(model.Model): - __name__ = 'PriceAmount' +class Amount(model.Model): + __name__ = 'Amount' currency = fields.Attribute('currencyID', default='COP') class Price(model.Model): __name__ = 'Price' - amount = fields.Many2One(PriceAmount) + amount = fields.Many2One(Amount, name='PriceAmount') def __default_set__(self, value): self.amount = value +class Percent(model.Model): + __name__ = 'Percent' + +class TaxScheme(model.Model): + __name__ = 'TaxScheme' + + id = fields.Many2One(ID) + name= fields.Many2One(Name) + +class TaxCategory(model.Model): + __name__ = 'TaxCategory' + + percent = fields.Many2One(Percent, default='19.0') + tax_scheme = fields.Many2One(TaxScheme) + +class TaxSubTotal(model.Model): + __name__ = 'TaxSubTotal' + + taxable_amount = fields.Many2One(Amount, name='TaxableAmount') + tax_amount = fields.Many2One(Amount, name='TaxAmount') + tax_category = fields.Many2One(TaxCategory) + + percent = fields.Virtual(setter='set_percent') + + def set_percent(self, name, value): + self.tax_category.percent = value + # TODO(bit4bit) hacer variable + self.tax_category.tax_scheme.id = '01' + self.tax_category.tax_scheme.name = 'IVA' + +class TaxTotal(model.Model): + __name__ = 'TaxTotal' + + tax_amount = fields.Many2One(Amount, name='TaxAmount') + subtotals = fields.One2Many(TaxSubTotal) + class InvoiceLine(model.Model): __name__ = 'InvoiceLine' quantity = fields.Many2One(InvoicedQuantity) + taxtotal = fields.Many2One(TaxTotal) price = fields.Many2One(Price) class Invoice(model.Model): diff --git a/facho/model/__init__.py b/facho/model/__init__.py index 9a45981..e70b903 100644 --- a/facho/model/__init__.py +++ b/facho/model/__init__.py @@ -38,10 +38,11 @@ class ModelBase(object, metaclass=ModelMeta): # forzamos registros de campos al modelo # al instanciar for (key, v) in type(obj).__dict__.items(): + if isinstance(v, fields.Attribute) or isinstance(v, fields.Many2One) or isinstance(v, fields.Function): if hasattr(v, 'default') and v.default is not None: setattr(obj, key, v.default) - + # register callbacks for changes (fun, on_change_fields) = on_change_fields_for_function() for field in on_change_fields: @@ -97,7 +98,7 @@ class ModelBase(object, metaclass=ModelMeta): content = "" - for name, value in self._fields.items(): + for value in self._fields.values(): if hasattr(value, 'to_xml'): content += value.to_xml() elif isinstance(value, str): diff --git a/facho/model/fields/one2many.py b/facho/model/fields/one2many.py index 182a1b0..112a92f 100644 --- a/facho/model/fields/one2many.py +++ b/facho/model/fields/one2many.py @@ -19,7 +19,6 @@ class _RelationProxy(): # algo burdo, se usa __dict__ para saltarnos el __getattr__ y generar un fallo por recursion for fun in self.__dict__['_inst']._on_change_fields[self.__dict__['_attribute']]: fun(self.__dict__['_inst'], self.__dict__['_attribute'], value) - return setattr(self._obj, attr, value) class _Relation(): diff --git a/tests/test_model_invoice.py b/tests/test_model_invoice.py index d85c87b..6e6b49e 100644 --- a/tests/test_model_invoice.py +++ b/tests/test_model_invoice.py @@ -21,6 +21,10 @@ def test_simple_invoice(): line = invoice.lines.create() line.quantity = form.Quantity(1, '94') + subtotal = line.taxtotal.subtotals.create() + subtotal.percent = 19.0 + # TODO(bit4bit) el orden de los elementos + # en el xml lo debe determinar la declaracion en los modelos line.price = form.Amount(5_000) - assert '3232000001292019-01-16T10:53:10-05:0010:5310-05:007000853718001994361.05000.0' == invoice.to_xml() + assert '3232000001292019-01-16T10:53:10-05:0010:5310-05:007000853718001994361.019.001IVA5000.0' == invoice.to_xml() From ba908b938cac9605eafbb5b13ec4ac014c95f2a8 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Sat, 26 Jun 2021 23:23:25 +0000 Subject: [PATCH 23/38] se refleja el orden de los atributos del modelo en el xml FossilOrigin-Name: 5ff5ebc397a11977916b7008ab4d5104a375290a5c5d0356098b69242378f1f8 --- facho/model/__init__.py | 17 +++++++++++++++-- tests/test_model_invoice.py | 4 +--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/facho/model/__init__.py b/facho/model/__init__.py index e70b903..57f7489 100644 --- a/facho/model/__init__.py +++ b/facho/model/__init__.py @@ -22,6 +22,7 @@ class ModelBase(object, metaclass=ModelMeta): obj._text = "" obj._namespace_prefix = None obj._on_change_fields = defaultdict(list) + obj._order_fields = [] def on_change_fields_for_function(): # se recorre arbol buscando el primero @@ -38,7 +39,9 @@ class ModelBase(object, metaclass=ModelMeta): # forzamos registros de campos al modelo # al instanciar for (key, v) in type(obj).__dict__.items(): - + if isinstance(v, fields.Field): + obj._order_fields.append(key) + if isinstance(v, fields.Attribute) or isinstance(v, fields.Many2One) or isinstance(v, fields.Function): if hasattr(v, 'default') and v.default is not None: setattr(obj, key, v.default) @@ -98,7 +101,17 @@ class ModelBase(object, metaclass=ModelMeta): content = "" - for value in self._fields.values(): + ordered_fields = {} + for name in self._order_fields: + if name in self._fields: + ordered_fields[name] = True + else: + for key in self._fields.keys(): + if key.startswith(name): + ordered_fields[key] = True + + for name in ordered_fields.keys(): + value = self._fields[name] if hasattr(value, 'to_xml'): content += value.to_xml() elif isinstance(value, str): diff --git a/tests/test_model_invoice.py b/tests/test_model_invoice.py index 6e6b49e..a28df1c 100644 --- a/tests/test_model_invoice.py +++ b/tests/test_model_invoice.py @@ -21,10 +21,8 @@ def test_simple_invoice(): line = invoice.lines.create() line.quantity = form.Quantity(1, '94') + line.price = form.Amount(5_000) subtotal = line.taxtotal.subtotals.create() subtotal.percent = 19.0 - # TODO(bit4bit) el orden de los elementos - # en el xml lo debe determinar la declaracion en los modelos - line.price = form.Amount(5_000) assert '3232000001292019-01-16T10:53:10-05:0010:5310-05:007000853718001994361.019.001IVA5000.0' == invoice.to_xml() From 2e8aa35b2946b40fd54f58ab216bb4c8d4ff8c61 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Sat, 26 Jun 2021 23:25:51 +0000 Subject: [PATCH 24/38] prueba que confirma el orden del model y xml FossilOrigin-Name: fc039dec57eec4287d1c2352f8fd0cc6fceaf1f409af02e115c2d99a96bdc7bd --- tests/test_model.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_model.py b/tests/test_model.py index d588c14..f6ae1ee 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -466,3 +466,26 @@ def test_model_one2many_as_list(): for line in lines: assert isinstance(line, Line) assert '' == invoice.to_xml() + + +def test_model_attributes_order(): + class Line(facho.model.Model): + __name__ = 'Line' + + quantity = fields.Attribute('quantity') + + class Invoice(facho.model.Model): + __name__ = 'Invoice' + + line1 = fields.Many2One(Line, name='Line1') + line2 = fields.Many2One(Line, name='Line2') + line3 = fields.Many2One(Line, name='Line3') + + + invoice = Invoice() + invoice.line2.quantity = 2 + invoice.line3.quantity = 3 + invoice.line1.quantity = 1 + + assert '' == invoice.to_xml() + From 507ddbe5583c27f76ea03b8e3fc2a6213034575b Mon Sep 17 00:00:00 2001 From: bit4bit Date: Sun, 27 Jun 2021 02:16:16 +0000 Subject: [PATCH 25/38] se crea legal monetary total FossilOrigin-Name: 8ae3dfadfe9b90b8cc5ad59d2a73f4c8a987c5aac498573e56754a2d32e9e2ae --- facho/fe/form/__init__.py | 2 +- facho/fe/model/__init__.py | 77 +++++++++++++++++++++++++++++++--- facho/model/__init__.py | 8 ++-- facho/model/fields/one2many.py | 3 +- tests/test_model_invoice.py | 16 ++++++- 5 files changed, 94 insertions(+), 12 deletions(-) diff --git a/facho/fe/form/__init__.py b/facho/fe/form/__init__.py index 0aa6318..b12a86a 100644 --- a/facho/fe/form/__init__.py +++ b/facho/fe/form/__init__.py @@ -99,7 +99,7 @@ class Amount: return self.fromNumber(val) if isinstance(val, Amount): return val - raise TypeError("cant cast to amount") + raise TypeError("cant cast %s to amount" % (type(val))) def __add__(self, rother): other = self._cast(rother) diff --git a/facho/fe/model/__init__.py b/facho/fe/model/__init__.py index 74aaf01..e1d895c 100644 --- a/facho/fe/model/__init__.py +++ b/facho/fe/model/__init__.py @@ -1,6 +1,8 @@ import facho.model as model import facho.model.fields as fields +import facho.fe.form as form from datetime import date, datetime +from copy import copy class Name(model.Model): __name__ = 'Name' @@ -48,24 +50,44 @@ class AccountingSupplierParty(model.Model): party = fields.Many2One(Party) -class InvoicedQuantity(model.Model): - __name__ = 'InvoiceQuantity' +class Quantity(model.Model): + __name__ = 'Quantity' code = fields.Attribute('unitCode', default='NAR') + value = fields.Virtual(default=0, update_internal=True) + + def __default_set__(self, value): + self.value = value + return value + + def __mul__(self, other): + return form.Amount(self.value) * other.value class Amount(model.Model): __name__ = 'Amount' currency = fields.Attribute('currencyID', default='COP') - + value = fields.Virtual(default=form.Amount(0), update_internal=True) + + def __default_set__(self, value): + self.value = value + return value + class Price(model.Model): __name__ = 'Price' amount = fields.Many2One(Amount, name='PriceAmount') - + value = fields.Virtual(default=form.Amount(0)) + def __default_set__(self, value): self.amount = value + self.value = value + return value + + def __mul__(self, other): + return self.value * other.value + class Percent(model.Model): __name__ = 'Percent' @@ -102,13 +124,52 @@ class TaxTotal(model.Model): tax_amount = fields.Many2One(Amount, name='TaxAmount') subtotals = fields.One2Many(TaxSubTotal) + + +class AllowanceCharge(model.Model): + __name__ = 'AllowanceCharge' + + amount = fields.Many2One(Amount) + is_discount = fields.Virtual(default=False) + def isCharge(self): + return self.is_discount == False + + def isDiscount(self): + return self.is_discount == True + class InvoiceLine(model.Model): __name__ = 'InvoiceLine' - quantity = fields.Many2One(InvoicedQuantity) + quantity = fields.Many2One(Quantity, name='InvoicedQuantity') taxtotal = fields.Many2One(TaxTotal) price = fields.Many2One(Price) + amount = fields.Many2One(Amount, name='LineExtensionAmount') + allowance_charge = fields.One2Many(AllowanceCharge) + + @fields.on_change(['price', 'quantity']) + def update_amount(self, name, value): + charge = form.AmountCollection(self.allowance_charge)\ + .filter(lambda charge: charge.isCharge())\ + .map(lambda charge: charge.amount)\ + .sum() + + discount = form.AmountCollection(self.allowance_charge)\ + .filter(lambda charge: charge.isDiscount())\ + .map(lambda charge: charge.amount)\ + .sum() + + total = self.quantity * self.price + self.amount = total + charge - discount + +class LegalMonetaryTotal(model.Model): + __name__ = 'LegalMonetaryTotal' + + line_extension_amount = fields.Many2One(Amount, name='LineExtensionAmount', default=form.Amount(0)) + tax_exclusive_amount = fields.Many2One(Amount, name='TaxExclusiveAmount') + tax_inclusive_amount = fields.Many2One(Amount, name='TaxInclusiveAmount') + charge_total_amount = fields.Many2One(Amount, name='ChargeTotalAmount') + payable_amount = fields.Many2One(Amount, name='PayableAmount') class Invoice(model.Model): __name__ = 'Invoice' @@ -123,7 +184,13 @@ class Invoice(model.Model): supplier = fields.Many2One(AccountingSupplierParty) customer = fields.Many2One(AccountingCustomerParty) lines = fields.One2Many(InvoiceLine) + legal_monetary_total = fields.Many2One(LegalMonetaryTotal) + @fields.on_change(['lines']) + def update_legal_monetary_total(self, name, value): + for line in self.lines: + self.legal_monetary_total.line_extension_amount.value += line.amount.value + def set_issue(self, name, value): if not isinstance(value, datetime): raise ValueError('expected type datetime') diff --git a/facho/model/__init__.py b/facho/model/__init__.py index 57f7489..0c2f73c 100644 --- a/facho/model/__init__.py +++ b/facho/model/__init__.py @@ -19,7 +19,7 @@ class ModelBase(object, metaclass=ModelMeta): obj = super().__new__(cls, *args, **kwargs) obj._xml_attributes = {} obj._fields = {} - obj._text = "" + obj._value = None obj._namespace_prefix = None obj._on_change_fields = defaultdict(list) obj._order_fields = [] @@ -72,7 +72,7 @@ class ModelBase(object, metaclass=ModelMeta): def _set_content(self, value): default = self.__default_set__(value) if default is not None: - self._text = str(default) + self._value = default def _hook_before_xml(self): self.__before_xml__() @@ -116,7 +116,9 @@ class ModelBase(object, metaclass=ModelMeta): content += value.to_xml() elif isinstance(value, str): content += value - content += self._text + + if self._value is not None: + content += str(self._value) if content == "": return "<%s%s%s/>" % (ns, tag, attributes) diff --git a/facho/model/fields/one2many.py b/facho/model/fields/one2many.py index 112a92f..aeb6547 100644 --- a/facho/model/fields/one2many.py +++ b/facho/model/fields/one2many.py @@ -17,9 +17,10 @@ class _RelationProxy(): def __setattr__(self, attr, value): # TODO(bit4bit) hacemos proxy al sistema de notificacion de cambios # algo burdo, se usa __dict__ para saltarnos el __getattr__ y generar un fallo por recursion + response = setattr(self._obj, attr, value) for fun in self.__dict__['_inst']._on_change_fields[self.__dict__['_attribute']]: fun(self.__dict__['_inst'], self.__dict__['_attribute'], value) - return setattr(self._obj, attr, value) + return response class _Relation(): def __init__(self, creator, inst, attribute): diff --git a/tests/test_model_invoice.py b/tests/test_model_invoice.py index a28df1c..6472fd0 100644 --- a/tests/test_model_invoice.py +++ b/tests/test_model_invoice.py @@ -20,9 +20,21 @@ def test_simple_invoice(): invoice.customer.party.id = '800199436' line = invoice.lines.create() - line.quantity = form.Quantity(1, '94') + line.quantity = 1 line.price = form.Amount(5_000) subtotal = line.taxtotal.subtotals.create() subtotal.percent = 19.0 + assert '3232000001292019-01-16T10:53:10-05:0010:5310-05:00700085371800199436119.001IVA5000.05000.05000.035000.0' == invoice.to_xml() - assert '3232000001292019-01-16T10:53:10-05:0010:5310-05:007000853718001994361.019.001IVA5000.0' == invoice.to_xml() +def _test_simple_invoice_cufe(): + invoice = model.Invoice() + invoice.id = '323200000129' + invoice.issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z') + invoice.supplier.party.id = '700085371' + invoice.customer.party.id = '800199436' + + line = invoice.lines.create() + line.quantity = form.Quantity(1, '94') + line.price = form.Amount(1_500_000) + subtotal = line.taxtotal.subtotals.create() + subtotal.percent = 19.0 From b3e4a088b79e8ee1aa12a7851558a58204e10943 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Sat, 3 Jul 2021 21:50:56 +0000 Subject: [PATCH 26/38] se adicion campo fields.Amount FossilOrigin-Name: b23b2c243daaf0788cf47736015d75eecae9f4eb55cd4d31b9e063d7fa9a0691 --- facho/model/__init__.py | 2 +- facho/model/fields/__init__.py | 3 ++- tests/test_model.py | 17 +++++++++++++++++ tests/test_model_invoice.py | 4 ++-- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/facho/model/__init__.py b/facho/model/__init__.py index 0c2f73c..59b8d55 100644 --- a/facho/model/__init__.py +++ b/facho/model/__init__.py @@ -42,7 +42,7 @@ class ModelBase(object, metaclass=ModelMeta): if isinstance(v, fields.Field): obj._order_fields.append(key) - if isinstance(v, fields.Attribute) or isinstance(v, fields.Many2One) or isinstance(v, fields.Function): + if isinstance(v, fields.Attribute) or isinstance(v, fields.Many2One) or isinstance(v, fields.Function) or isinstance(v, fields.Amount): if hasattr(v, 'default') and v.default is not None: setattr(obj, key, v.default) diff --git a/facho/model/fields/__init__.py b/facho/model/fields/__init__.py index 6e9408d..081f195 100644 --- a/facho/model/fields/__init__.py +++ b/facho/model/fields/__init__.py @@ -4,8 +4,9 @@ from .one2many import One2Many from .function import Function from .virtual import Virtual from .field import Field +from .amount import Amount -__all__ = [Attribute, One2Many, Many2One, Virtual, Field] +__all__ = [Attribute, One2Many, Many2One, Virtual, Field, Amount] def on_change(fields): from functools import wraps diff --git a/tests/test_model.py b/tests/test_model.py index f6ae1ee..7104920 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -489,3 +489,20 @@ def test_model_attributes_order(): assert '' == invoice.to_xml() + +def test_field_amount(): + class Line(facho.model.Model): + __name__ = 'Line' + + amount = fields.Amount(name='Amount', precision=0) + amount_as_attribute = fields.Attribute('amount') + + @fields.on_change(['amount']) + def on_amount(self, name, value): + self.amount_as_attribute = self.amount + + line = Line() + line.amount = 33 + + assert '' == line.to_xml() + diff --git a/tests/test_model_invoice.py b/tests/test_model_invoice.py index 6472fd0..79d3604 100644 --- a/tests/test_model_invoice.py +++ b/tests/test_model_invoice.py @@ -34,7 +34,7 @@ def _test_simple_invoice_cufe(): invoice.customer.party.id = '800199436' line = invoice.lines.create() - line.quantity = form.Quantity(1, '94') - line.price = form.Amount(1_500_000) + line.quantity = 1 + line.price = 1_500_000 subtotal = line.taxtotal.subtotals.create() subtotal.percent = 19.0 From a1a97463539733c42034c8107a2ef534492ec839 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Fri, 9 Jul 2021 01:03:51 +0000 Subject: [PATCH 27/38] se adiciona __setup__ para inicializar modelos FossilOrigin-Name: 504ad84bee1c708c2b55fdde3552d39980bf1efad3a51de8050018e4d1f387f3 --- facho/fe/model/__init__.py | 20 +++++++++++++------- facho/model/__init__.py | 8 +++++++- tests/test_model.py | 12 ++++++++++++ tests/test_model_invoice.py | 7 +++++-- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/facho/fe/model/__init__.py b/facho/fe/model/__init__.py index e1d895c..5f6d6a8 100644 --- a/facho/fe/model/__init__.py +++ b/facho/fe/model/__init__.py @@ -111,13 +111,17 @@ class TaxSubTotal(model.Model): tax_amount = fields.Many2One(Amount, name='TaxAmount') tax_category = fields.Many2One(TaxCategory) - percent = fields.Virtual(setter='set_percent') - - def set_percent(self, name, value): - self.tax_category.percent = value - # TODO(bit4bit) hacer variable - self.tax_category.tax_scheme.id = '01' - self.tax_category.tax_scheme.name = 'IVA' + percent = fields.Virtual(setter='set_category') + scheme = fields.Virtual(setter='set_category') + def set_category(self, name, value): + if name == 'percent': + self.tax_category.percent = value + # TODO(bit4bit) hacer variable + self.tax_category.tax_scheme.id = '01' + self.tax_category.tax_scheme.name = 'IVA' + elif name == 'scheme': + self.tax_category.tax_scheme.id = value + class TaxTotal(model.Model): __name__ = 'TaxTotal' @@ -186,6 +190,8 @@ class Invoice(model.Model): lines = fields.One2Many(InvoiceLine) legal_monetary_total = fields.Many2One(LegalMonetaryTotal) + cufe = fields.Virtual() + @fields.on_change(['lines']) def update_legal_monetary_total(self, name, value): for line in self.lines: diff --git a/facho/model/__init__.py b/facho/model/__init__.py index 59b8d55..a6689c2 100644 --- a/facho/model/__init__.py +++ b/facho/model/__init__.py @@ -50,7 +50,9 @@ class ModelBase(object, metaclass=ModelMeta): (fun, on_change_fields) = on_change_fields_for_function() for field in on_change_fields: obj._on_change_fields[field].append(fun) - + + + obj.__setup__() return obj def _set_attribute(self, field, name, value): @@ -141,4 +143,8 @@ class Model(ModelBase): """ return value + def __setup__(self): + """ + Inicializar modelo + """ diff --git a/tests/test_model.py b/tests/test_model.py index 7104920..7dc5ef8 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -506,3 +506,15 @@ def test_field_amount(): assert '' == line.to_xml() + +def test_model_setup(): + class Line(facho.model.Model): + __name__ = 'Line' + + amount = fields.Attribute(name='amount') + + def __setup__(self): + self.amount = 23 + + line = Line() + assert '' == line.to_xml() diff --git a/tests/test_model_invoice.py b/tests/test_model_invoice.py index 79d3604..c7355d9 100644 --- a/tests/test_model_invoice.py +++ b/tests/test_model_invoice.py @@ -36,5 +36,8 @@ def _test_simple_invoice_cufe(): line = invoice.lines.create() line.quantity = 1 line.price = 1_500_000 - subtotal = line.taxtotal.subtotals.create() - subtotal.percent = 19.0 + line_subtotal = line.taxtotal.subtotals.create() + line_subtotal.percent = 19.0 + line.subtotal.scheme = '01' + + assert invoice.cufe == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4' From 69a74c0714c86fa474ba8353475c866a98cf0d35 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Sat, 31 Jul 2021 17:09:42 +0000 Subject: [PATCH 28/38] generacion de cufe desde invoice FossilOrigin-Name: d3494f20063452571b1e86d505f211e61fdf435aa43b870408136e3e9302bc17 --- facho/fe/form/__init__.py | 13 ++- facho/fe/model/__init__.py | 196 +++++++++++++++++++++++++++++---- facho/model/__init__.py | 12 +- facho/model/fields/field.py | 3 +- facho/model/fields/many2one.py | 21 +++- facho/model/fields/one2many.py | 11 +- tests/test_model.py | 4 +- tests/test_model_invoice.py | 18 ++- 8 files changed, 233 insertions(+), 45 deletions(-) diff --git a/facho/fe/form/__init__.py b/facho/fe/form/__init__.py index b12a86a..7f0c5e7 100644 --- a/facho/fe/form/__init__.py +++ b/facho/fe/form/__init__.py @@ -54,8 +54,8 @@ class AmountCollection(Collection): return total class Amount: - def __init__(self, amount: int or float or str or Amount, currency: Currency = Currency('COP')): - + def __init__(self, amount: int or float or str or Amount, currency: Currency = Currency('COP'), precision = DECIMAL_PRECISION): + self.precision = precision #DIAN 1.7.-2020: 1.2.3.1 if isinstance(amount, Amount): if amount < Amount(0.0): @@ -67,7 +67,7 @@ class Amount: if float(amount) < 0: raise ValueError('amount must be positive >= 0') - self.amount = Decimal(amount, decimal.Context(prec=DECIMAL_PRECISION, + self.amount = Decimal(amount, decimal.Context(prec=self.precision, #DIAN 1.7.-2020: 1.2.1.1 rounding=decimal.ROUND_HALF_EVEN )) self.currency = currency @@ -87,18 +87,21 @@ class Amount: def __lt__(self, other): if not self.is_same_currency(other): raise AmountCurrencyError() - return round(self.amount, DECIMAL_PRECISION) < round(other, 2) + return round(self.amount, self.precision) < round(other, 2) def __eq__(self, other): if not self.is_same_currency(other): raise AmountCurrencyError() - return round(self.amount, DECIMAL_PRECISION) == round(other.amount, DECIMAL_PRECISION) + return round(self.amount, self.precision) == round(other.amount, self.precision) def _cast(self, val): if type(val) in [int, float]: return self.fromNumber(val) if isinstance(val, Amount): return val + if isinstance(val, Decimal): + return self.fromNumber(float(val)) + raise TypeError("cant cast %s to amount" % (type(val))) def __add__(self, rother): diff --git a/facho/fe/model/__init__.py b/facho/fe/model/__init__.py index 5f6d6a8..7b714e1 100644 --- a/facho/fe/model/__init__.py +++ b/facho/fe/model/__init__.py @@ -1,8 +1,12 @@ import facho.model as model import facho.model.fields as fields import facho.fe.form as form +from facho import fe + from datetime import date, datetime +from collections import defaultdict from copy import copy +import hashlib class Name(model.Model): __name__ = 'Name' @@ -16,6 +20,9 @@ class Date(model.Model): if isinstance(value, date): return value.isoformat() + def __str__(self): + return str(self._value) + class Time(model.Model): __name__ = 'Time' @@ -23,7 +30,10 @@ class Time(model.Model): if isinstance(value, str): return value if isinstance(value, date): - return value.strftime('%H:%M%S-05:00') + return value.strftime('%H:%M:%S-05:00') + + def __str__(self): + return str(self._value) class InvoicePeriod(model.Model): __name__ = 'InvoicePeriod' @@ -35,6 +45,9 @@ class InvoicePeriod(model.Model): class ID(model.Model): __name__ = 'ID' + def __str__(self): + return str(self._value) + class Party(model.Model): __name__ = 'Party' @@ -63,22 +76,36 @@ class Quantity(model.Model): def __mul__(self, other): return form.Amount(self.value) * other.value + def __add__(self, other): + return form.Amount(self.value) + other.value class Amount(model.Model): __name__ = 'Amount' currency = fields.Attribute('currencyID', default='COP') - value = fields.Virtual(default=form.Amount(0), update_internal=True) + value = fields.Amount(name='amount', default=0.00, precision=2) def __default_set__(self, value): self.value = value return value + def __default__get__(self, value): + return value + + def __str__(self): + return str(self.value) + + def __add__(self, other): + if isinstance(other, form.Amount): + return self.value + other + + return self.value + other.value + class Price(model.Model): __name__ = 'Price' amount = fields.Many2One(Amount, name='PriceAmount') - value = fields.Virtual(default=form.Amount(0)) + value = fields.Amount(0.0) def __default_set__(self, value): self.amount = value @@ -101,32 +128,40 @@ class TaxScheme(model.Model): class TaxCategory(model.Model): __name__ = 'TaxCategory' - percent = fields.Many2One(Percent, default='19.0') + percent = fields.Many2One(Percent) tax_scheme = fields.Many2One(TaxScheme) class TaxSubTotal(model.Model): __name__ = 'TaxSubTotal' - taxable_amount = fields.Many2One(Amount, name='TaxableAmount') - tax_amount = fields.Many2One(Amount, name='TaxAmount') + taxable_amount = fields.Many2One(Amount, name='TaxableAmount', default=0.00) + tax_amount = fields.Many2One(Amount, name='TaxAmount', default=0.00) + tax_percent = fields.Many2One(Percent) tax_category = fields.Many2One(TaxCategory) - percent = fields.Virtual(setter='set_category') - scheme = fields.Virtual(setter='set_category') + percent = fields.Virtual(setter='set_category', getter='get_category') + scheme = fields.Virtual(setter='set_category', getter='get_category') + def set_category(self, name, value): if name == 'percent': self.tax_category.percent = value - # TODO(bit4bit) hacer variable - self.tax_category.tax_scheme.id = '01' - self.tax_category.tax_scheme.name = 'IVA' + # TODO(bit4bit) debe variar en conjunto? + self.tax_percent = value elif name == 'scheme': self.tax_category.tax_scheme.id = value - - + + return value + + def get_category(self, name, value): + if name == 'percent': + return value + elif name == 'scheme': + return self.tax_category.tax_scheme + class TaxTotal(model.Model): __name__ = 'TaxTotal' - tax_amount = fields.Many2One(Amount, name='TaxAmount') + tax_amount = fields.Many2One(Amount, name='TaxAmount', default=0.00) subtotals = fields.One2Many(TaxSubTotal) @@ -142,6 +177,17 @@ class AllowanceCharge(model.Model): def isDiscount(self): return self.is_discount == True +class TaxScheme: + pass + +class TaxIva(TaxScheme): + def __init__(self, percent): + self.scheme = '01' + self.percent = percent + + def calculate(self, amount): + return form.Amount(amount) * form.Amount(self.percent / 100) + class InvoiceLine(model.Model): __name__ = 'InvoiceLine' @@ -150,6 +196,26 @@ class InvoiceLine(model.Model): price = fields.Many2One(Price) amount = fields.Many2One(Amount, name='LineExtensionAmount') allowance_charge = fields.One2Many(AllowanceCharge) + tax_amount = fields.Virtual(getter='get_tax_amount') + + def __setup__(self): + self._taxs = defaultdict(list) + self._subtotals = { + '01': self.taxtotal.subtotals.create() + } + self._subtotals['01'].scheme = '01' + + def get_tax_amount(self, name, value): + total = form.Amount(0) + for (scheme, subtotal) in self._subtotals.items(): + total += subtotal.tax_amount.value + return total + + def add_tax(self, tax): + if not isinstance(tax, TaxScheme): + raise ValueError('tax expected TaxScheme') + + self._taxs[tax.scheme].append(tax) @fields.on_change(['price', 'quantity']) def update_amount(self, name, value): @@ -165,19 +231,38 @@ class InvoiceLine(model.Model): total = self.quantity * self.price self.amount = total + charge - discount + for (scheme, subtotal) in self._subtotals.items(): + subtotal.tax_amount.value = 0 + + for (scheme, taxes) in self._taxs.items(): + for tax in taxes: + self._subtotals[scheme].tax_amount += tax.calculate(self.amount.value) class LegalMonetaryTotal(model.Model): __name__ = 'LegalMonetaryTotal' - line_extension_amount = fields.Many2One(Amount, name='LineExtensionAmount', default=form.Amount(0)) - tax_exclusive_amount = fields.Many2One(Amount, name='TaxExclusiveAmount') - tax_inclusive_amount = fields.Many2One(Amount, name='TaxInclusiveAmount') - charge_total_amount = fields.Many2One(Amount, name='ChargeTotalAmount') - payable_amount = fields.Many2One(Amount, name='PayableAmount') + line_extension_amount = fields.Many2One(Amount, name='LineExtensionAmount', default=0) + + tax_exclusive_amount = fields.Many2One(Amount, name='TaxExclusiveAmount', default=form.Amount(0)) + tax_inclusive_amount = fields.Many2One(Amount, name='TaxInclusiveAmount', default=form.Amount(0)) + charge_total_amount = fields.Many2One(Amount, name='ChargeTotalAmount', default=form.Amount(0)) + payable_amount = fields.Many2One(Amount, name='PayableAmount', default=form.Amount(0)) + + @fields.on_change(['tax_inclusive_amount', 'charge_total']) + def update_payable_amount(self, name, value): + self.payable_amount = self.tax_inclusive_amount.value + self.charge_total_amount.value +class Technical(model.Model): + __name__ = 'Technical' + + token = fields.Virtual(default='') + environment = fields.Virtual(default=fe.AMBIENTE_PRODUCCION) + class Invoice(model.Model): __name__ = 'Invoice' + technical = fields.Many2One(Technical, virtual=True) + id = fields.Many2One(ID) issue = fields.Virtual(setter='set_issue') issue_date = fields.Many2One(Date, name='IssueDate') @@ -189,16 +274,83 @@ class Invoice(model.Model): customer = fields.Many2One(AccountingCustomerParty) lines = fields.One2Many(InvoiceLine) legal_monetary_total = fields.Many2One(LegalMonetaryTotal) + + taxtotal_01 = fields.Many2One(TaxTotal) + taxtotal_04 = fields.Many2One(TaxTotal) + taxtotal_03 = fields.Many2One(TaxTotal) - cufe = fields.Virtual() + cufe = fields.Virtual(getter='calculate_cufe') + + _subtotal_01 = fields.Virtual() + _subtotal_04 = fields.Virtual() + _subtotal_03 = fields.Virtual() + + def __setup__(self): + # Se requieren minimo estos impuestos para + # validar el cufe + self._subtotal_01 = self.taxtotal_01.subtotals.create() + self._subtotal_01.scheme = '01' + self._subtotal_01.percent = 19.0 + + self._subtotal_04 = self.taxtotal_04.subtotals.create() + self._subtotal_04.scheme = '04' + + self._subtotal_03 = self.taxtotal_03.subtotals.create() + self._subtotal_03.scheme = '03' + + def calculate_cufe(self, name, value): + + valor_bruto = self.legal_monetary_total.line_extension_amount.value + valor_total_pagar = self.legal_monetary_total.payable_amount.value + + valor_impuesto_01 = form.Amount(0.0) + valor_impuesto_04 = form.Amount(0.0) + valor_impuesto_03 = form.Amount(0.0) + + for line in self.lines: + for subtotal in line.taxtotal.subtotals: + scheme_id = subtotal.scheme + if str(subtotal.scheme.id) == '01': + valor_impuesto_01 += subtotal.tax_amount.value + elif subtotal.scheme.id == '04': + valor_impuesto_04 += subtotal.tax_amount.value + elif subtotal.scheme.id == '03': + valor_impuesto_03 += subtotal.tax_amount.value + + + + pattern = [ + '%s' % str(self.id), + '%s' % str(self.issue_date), + '%s' % str(self.issue_time), + valor_bruto.truncate_as_string(2), + '01', valor_impuesto_01.truncate_as_string(2), + '04', valor_impuesto_04.truncate_as_string(2), + '03', valor_impuesto_03.truncate_as_string(2), + valor_total_pagar.truncate_as_string(2), + str(self.supplier.party.id), + str(self.customer.party.id), + str(self.technical.token), + str(self.technical.environment) + ] + + cufe = "".join(pattern) + h = hashlib.sha384() + h.update(cufe.encode('utf-8')) + return h.hexdigest() @fields.on_change(['lines']) def update_legal_monetary_total(self, name, value): + self.legal_monetary_total.line_extension_amount.value = 0 + self.legal_monetary_total.tax_inclusive_amount.value = 0 + for line in self.lines: self.legal_monetary_total.line_extension_amount.value += line.amount.value - + self.legal_monetary_total.tax_inclusive_amount += line.amount.value + line.tax_amount + print("update legal monetary %s" % (str(line.amount.value))) + def set_issue(self, name, value): if not isinstance(value, datetime): raise ValueError('expected type datetime') - self.issue_date = value + self.issue_date = value.date() self.issue_time = value diff --git a/facho/model/__init__.py b/facho/model/__init__.py index a6689c2..d9b9ca7 100644 --- a/facho/model/__init__.py +++ b/facho/model/__init__.py @@ -25,7 +25,7 @@ class ModelBase(object, metaclass=ModelMeta): obj._order_fields = [] def on_change_fields_for_function(): - # se recorre arbol buscando el primero + # se recorre arbol de herencia buscando attributo on_changes for parent_cls in type(obj).__mro__: for parent_attr in dir(parent_cls): parent_meth = getattr(parent_cls, parent_attr, None) @@ -114,6 +114,10 @@ class ModelBase(object, metaclass=ModelMeta): for name in ordered_fields.keys(): value = self._fields[name] + # al ser virtual no adicinamos al arbol xml + if hasattr(value, 'virtual') and value.virtual: + continue + if hasattr(value, 'to_xml'): content += value.to_xml() elif isinstance(value, str): @@ -143,6 +147,12 @@ class Model(ModelBase): """ return value + def __default_get__(self, name, value): + """ + Retorno de valor por defecto + """ + return value + def __setup__(self): """ Inicializar modelo diff --git a/facho/model/fields/field.py b/facho/model/fields/field.py index c447020..d8a6fe4 100644 --- a/facho/model/fields/field.py +++ b/facho/model/fields/field.py @@ -1,6 +1,7 @@ class Field: - def __set_name__(self, owner, name): + def __set_name__(self, owner, name, virtual=False): self.name = name + self.virtual = virtual def __get__(self, inst, cls): if inst is None: diff --git a/facho/model/fields/many2one.py b/facho/model/fields/many2one.py index 5134e3d..c22bd3d 100644 --- a/facho/model/fields/many2one.py +++ b/facho/model/fields/many2one.py @@ -1,22 +1,37 @@ from .field import Field +from collections import defaultdict class Many2One(Field): - def __init__(self, model, name=None, setter=None, namespace=None, default=None): + def __init__(self, model, name=None, setter=None, namespace=None, default=None, virtual=False): self.model = model self.setter = setter self.namespace = namespace self.field_name = name self.default = default - + self.virtual = virtual + self.relations = defaultdict(dict) + def __get__(self, inst, cls): if inst is None: return self assert self.name is not None - return self._create_model(inst, name=self.field_name) + + if self.name in self.relations: + value = self.relations[inst][self.name] + else: + value = self._create_model(inst, name=self.field_name) + self.relations[inst][self.name] = value + + # se puede obtener directamente un valor indicado por el modelo + if hasattr(value, '__default_get__'): + return value.__default_get__(self.name, value) + else: + return inst.__default_get__(self.name, value) def __set__(self, inst, value): assert self.name is not None inst_model = self._create_model(inst, name=self.field_name, model=self.model) + self.relations[inst][self.name] = inst_model # si hay setter manual se ejecuta # de lo contrario se asigna como texto del elemento diff --git a/facho/model/fields/one2many.py b/facho/model/fields/one2many.py index aeb6547..13b2e51 100644 --- a/facho/model/fields/one2many.py +++ b/facho/model/fields/one2many.py @@ -1,4 +1,5 @@ from .field import Field +from collections import defaultdict # TODO(bit4bit) lograr que isinstance se aplique # al objeto envuelto @@ -51,7 +52,7 @@ class One2Many(Field): self.field_name = name self.namespace = namespace self.default = default - self.relation = None + self.relation = {} def __get__(self, inst, cls): assert self.name is not None @@ -59,8 +60,8 @@ class One2Many(Field): def creator(attribute): return self._create_model(inst, name=self.field_name, model=self.model, attribute=attribute) - if self.relation: - return self.relation + if inst in self.relation: + return self.relation[inst] else: - self.relation = _Relation(creator, inst, self.name) - return self.relation + self.relation[inst] = _Relation(creator, inst, self.name) + return self.relation[inst] diff --git a/tests/test_model.py b/tests/test_model.py index 7dc5ef8..10cc99e 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -494,7 +494,7 @@ def test_field_amount(): class Line(facho.model.Model): __name__ = 'Line' - amount = fields.Amount(name='Amount', precision=0) + amount = fields.Amount(name='Amount', precision=1) amount_as_attribute = fields.Attribute('amount') @fields.on_change(['amount']) @@ -504,7 +504,7 @@ def test_field_amount(): line = Line() line.amount = 33 - assert '' == line.to_xml() + assert '' == line.to_xml() def test_model_setup(): diff --git a/tests/test_model_invoice.py b/tests/test_model_invoice.py index c7355d9..c99cdb8 100644 --- a/tests/test_model_invoice.py +++ b/tests/test_model_invoice.py @@ -11,8 +11,9 @@ import pytest import facho.fe.model as model import facho.fe.form as form +from facho import fe -def test_simple_invoice(): +def _test_simple_invoice(): invoice = model.Invoice() invoice.id = '323200000129' invoice.issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z') @@ -24,20 +25,25 @@ def test_simple_invoice(): line.price = form.Amount(5_000) subtotal = line.taxtotal.subtotals.create() subtotal.percent = 19.0 - assert '3232000001292019-01-16T10:53:10-05:0010:5310-05:00700085371800199436119.001IVA5000.05000.05000.035000.0' == invoice.to_xml() + assert '3232000001292019-01-16T10:53:10-05:0010:5310-05:0070008537180019943610.00.00.019.019.05000.05000.05000.00.00.00.00.019.019.0010.00.00.0040.00.00.003' == invoice.to_xml() -def _test_simple_invoice_cufe(): + +def test_simple_invoice_cufe(): invoice = model.Invoice() + invoice.technical.token = '693ff6f2a553c3646a063436fd4dd9ded0311471' + invoice.technical.environment = fe.AMBIENTE_PRODUCCION invoice.id = '323200000129' invoice.issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z') invoice.supplier.party.id = '700085371' invoice.customer.party.id = '800199436' line = invoice.lines.create() + line.add_tax(model.TaxIva(19.0)) + + # TODO(bit4bit) acoplamiento temporal + # se debe crear primero el subotatl + # para poder calcularse al cambiar el precio line.quantity = 1 line.price = 1_500_000 - line_subtotal = line.taxtotal.subtotals.create() - line_subtotal.percent = 19.0 - line.subtotal.scheme = '01' assert invoice.cufe == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4' From ddee0e45c1ee1112f1367c9e7d3a04789c3cf57d Mon Sep 17 00:00:00 2001 From: bit4bit Date: Sat, 31 Jul 2021 18:31:38 +0000 Subject: [PATCH 29/38] el uso de __default_set__ y __default_get__ facilitan la creacion de un objeto como tipo FossilOrigin-Name: e7fa7e3a3b312ad6b88a85b508b496308e5bb54b1d51ab5d50f4d4207830f175 --- facho/fe/model/__init__.py | 129 ++++++++++++++------------------- facho/model/__init__.py | 16 ++-- facho/model/fields/many2one.py | 4 +- facho/model/fields/one2many.py | 15 +++- tests/test_model_invoice.py | 9 ++- 5 files changed, 85 insertions(+), 88 deletions(-) diff --git a/facho/fe/model/__init__.py b/facho/fe/model/__init__.py index 7b714e1..5de325e 100644 --- a/facho/fe/model/__init__.py +++ b/facho/fe/model/__init__.py @@ -45,6 +45,9 @@ class InvoicePeriod(model.Model): class ID(model.Model): __name__ = 'ID' + def __default_get__(self, name, value): + return self._value + def __str__(self): return str(self._value) @@ -67,17 +70,16 @@ class Quantity(model.Model): __name__ = 'Quantity' code = fields.Attribute('unitCode', default='NAR') - value = fields.Virtual(default=0, update_internal=True) + + def __setup__(self): + self.value = 0 def __default_set__(self, value): self.value = value return value - def __mul__(self, other): - return form.Amount(self.value) * other.value - - def __add__(self, other): - return form.Amount(self.value) + other.value + def __default_get__(self, name, value): + return self.value class Amount(model.Model): __name__ = 'Amount' @@ -89,32 +91,23 @@ class Amount(model.Model): self.value = value return value - def __default__get__(self, value): - return value + def __default_get__(self, name, value): + return self.value def __str__(self): return str(self.value) - def __add__(self, other): - if isinstance(other, form.Amount): - return self.value + other - - return self.value + other.value - class Price(model.Model): __name__ = 'Price' amount = fields.Many2One(Amount, name='PriceAmount') - value = fields.Amount(0.0) def __default_set__(self, value): self.amount = value - self.value = value return value - def __mul__(self, other): - return self.value * other.value - + def __default_get__(self, name, value): + return self.amount class Percent(model.Model): __name__ = 'Percent' @@ -177,16 +170,18 @@ class AllowanceCharge(model.Model): def isDiscount(self): return self.is_discount == True -class TaxScheme: - pass +class Taxes: + class Scheme: + def __init__(self, scheme): + self.scheme = scheme -class TaxIva(TaxScheme): - def __init__(self, percent): - self.scheme = '01' - self.percent = percent + class Iva(Scheme): + def __init__(self, percent): + super().__init__('01') + self.percent = percent - def calculate(self, amount): - return form.Amount(amount) * form.Amount(self.percent / 100) + def calculate(self, amount): + return form.Amount(amount) * form.Amount(self.percent / 100) class InvoiceLine(model.Model): __name__ = 'InvoiceLine' @@ -200,23 +195,28 @@ class InvoiceLine(model.Model): def __setup__(self): self._taxs = defaultdict(list) - self._subtotals = { - '01': self.taxtotal.subtotals.create() - } - self._subtotals['01'].scheme = '01' + self._subtotals = {} + + def add_tax(self, tax): + if not isinstance(tax, Taxes.Scheme): + raise ValueError('tax expected TaxIva') + + # inicialiamos subtotal para impuesto + if not tax.scheme in self._subtotals: + subtotal = self.taxtotal.subtotals.create() + subtotal.scheme = tax.scheme + + self._subtotals[tax.scheme] = subtotal + + self._taxs[tax.scheme].append(tax) def get_tax_amount(self, name, value): total = form.Amount(0) for (scheme, subtotal) in self._subtotals.items(): - total += subtotal.tax_amount.value + total += subtotal.tax_amount + return total - def add_tax(self, tax): - if not isinstance(tax, TaxScheme): - raise ValueError('tax expected TaxScheme') - - self._taxs[tax.scheme].append(tax) - @fields.on_change(['price', 'quantity']) def update_amount(self, name, value): charge = form.AmountCollection(self.allowance_charge)\ @@ -229,14 +229,15 @@ class InvoiceLine(model.Model): .map(lambda charge: charge.amount)\ .sum() - total = self.quantity * self.price + total = form.Amount(self.quantity) * form.Amount(self.price) self.amount = total + charge - discount + for (scheme, subtotal) in self._subtotals.items(): - subtotal.tax_amount.value = 0 + subtotal.tax_amount = 0 for (scheme, taxes) in self._taxs.items(): for tax in taxes: - self._subtotals[scheme].tax_amount += tax.calculate(self.amount.value) + self._subtotals[scheme].tax_amount += tax.calculate(self.amount) class LegalMonetaryTotal(model.Model): __name__ = 'LegalMonetaryTotal' @@ -250,19 +251,11 @@ class LegalMonetaryTotal(model.Model): @fields.on_change(['tax_inclusive_amount', 'charge_total']) def update_payable_amount(self, name, value): - self.payable_amount = self.tax_inclusive_amount.value + self.charge_total_amount.value - -class Technical(model.Model): - __name__ = 'Technical' - - token = fields.Virtual(default='') - environment = fields.Virtual(default=fe.AMBIENTE_PRODUCCION) + self.payable_amount = self.tax_inclusive_amount + self.charge_total_amount class Invoice(model.Model): __name__ = 'Invoice' - technical = fields.Many2One(Technical, virtual=True) - id = fields.Many2One(ID) issue = fields.Virtual(setter='set_issue') issue_date = fields.Many2One(Date, name='IssueDate') @@ -279,12 +272,6 @@ class Invoice(model.Model): taxtotal_04 = fields.Many2One(TaxTotal) taxtotal_03 = fields.Many2One(TaxTotal) - cufe = fields.Virtual(getter='calculate_cufe') - - _subtotal_01 = fields.Virtual() - _subtotal_04 = fields.Virtual() - _subtotal_03 = fields.Virtual() - def __setup__(self): # Se requieren minimo estos impuestos para # validar el cufe @@ -298,10 +285,10 @@ class Invoice(model.Model): self._subtotal_03 = self.taxtotal_03.subtotals.create() self._subtotal_03.scheme = '03' - def calculate_cufe(self, name, value): + def cufe(self, token, environment): - valor_bruto = self.legal_monetary_total.line_extension_amount.value - valor_total_pagar = self.legal_monetary_total.payable_amount.value + valor_bruto = self.legal_monetary_total.line_extension_amount + valor_total_pagar = self.legal_monetary_total.payable_amount valor_impuesto_01 = form.Amount(0.0) valor_impuesto_04 = form.Amount(0.0) @@ -309,15 +296,12 @@ class Invoice(model.Model): for line in self.lines: for subtotal in line.taxtotal.subtotals: - scheme_id = subtotal.scheme - if str(subtotal.scheme.id) == '01': - valor_impuesto_01 += subtotal.tax_amount.value + if subtotal.scheme.id == '01': + valor_impuesto_01 += subtotal.tax_amount elif subtotal.scheme.id == '04': - valor_impuesto_04 += subtotal.tax_amount.value + valor_impuesto_04 += subtotal.tax_amount elif subtotal.scheme.id == '03': - valor_impuesto_03 += subtotal.tax_amount.value - - + valor_impuesto_03 += subtotal.tax_amount pattern = [ '%s' % str(self.id), @@ -330,8 +314,8 @@ class Invoice(model.Model): valor_total_pagar.truncate_as_string(2), str(self.supplier.party.id), str(self.customer.party.id), - str(self.technical.token), - str(self.technical.environment) + str(token), + str(environment) ] cufe = "".join(pattern) @@ -341,13 +325,12 @@ class Invoice(model.Model): @fields.on_change(['lines']) def update_legal_monetary_total(self, name, value): - self.legal_monetary_total.line_extension_amount.value = 0 - self.legal_monetary_total.tax_inclusive_amount.value = 0 + self.legal_monetary_total.line_extension_amount = 0 + self.legal_monetary_total.tax_inclusive_amount = 0 for line in self.lines: - self.legal_monetary_total.line_extension_amount.value += line.amount.value - self.legal_monetary_total.tax_inclusive_amount += line.amount.value + line.tax_amount - print("update legal monetary %s" % (str(line.amount.value))) + self.legal_monetary_total.line_extension_amount += line.amount + self.legal_monetary_total.tax_inclusive_amount += line.amount + line.tax_amount def set_issue(self, name, value): if not isinstance(value, datetime): diff --git a/facho/model/__init__.py b/facho/model/__init__.py index d9b9ca7..acf29bc 100644 --- a/facho/model/__init__.py +++ b/facho/model/__init__.py @@ -52,6 +52,7 @@ class ModelBase(object, metaclass=ModelMeta): obj._on_change_fields[field].append(fun) + # post inicializacion del objeto obj.__setup__() return obj @@ -76,17 +77,17 @@ class ModelBase(object, metaclass=ModelMeta): if default is not None: self._value = default - def _hook_before_xml(self): - self.__before_xml__() - for field in self._fields.values(): - if hasattr(field, '__before_xml__'): - field.__before_xml__() - def to_xml(self): """ Genera xml del modelo y sus relaciones """ - self._hook_before_xml() + def _hook_before_xml(): + self.__before_xml__() + for field in self._fields.values(): + if hasattr(field, '__before_xml__'): + field.__before_xml__() + + _hook_before_xml() tag = self.__name__ ns = '' @@ -149,6 +150,7 @@ class Model(ModelBase): def __default_get__(self, name, value): """ + Al obtener el valor atraves de una relacion (age = person.age) Retorno de valor por defecto """ return value diff --git a/facho/model/fields/many2one.py b/facho/model/fields/many2one.py index c22bd3d..d39bf36 100644 --- a/facho/model/fields/many2one.py +++ b/facho/model/fields/many2one.py @@ -25,8 +25,10 @@ class Many2One(Field): # se puede obtener directamente un valor indicado por el modelo if hasattr(value, '__default_get__'): return value.__default_get__(self.name, value) - else: + elif hasattr(inst, '__default_get__'): return inst.__default_get__(self.name, value) + else: + return value def __set__(self, inst, value): assert self.name is not None diff --git a/facho/model/fields/one2many.py b/facho/model/fields/one2many.py index 13b2e51..2c2dca7 100644 --- a/facho/model/fields/one2many.py +++ b/facho/model/fields/one2many.py @@ -13,12 +13,21 @@ class _RelationProxy(): if (name in self.__dict__): return self.__dict__[name] - return getattr(self.__dict__['_obj'], name) + rel = getattr(self.__dict__['_obj'], name) + if hasattr(rel, '__default_get__'): + return rel.__default_get__(name, rel) + + return rel def __setattr__(self, attr, value): # TODO(bit4bit) hacemos proxy al sistema de notificacion de cambios - # algo burdo, se usa __dict__ para saltarnos el __getattr__ y generar un fallo por recursion - response = setattr(self._obj, attr, value) + # algo burdo, se usa __dict__ para saltarnos el __getattr__ y evitar un fallo por recursion + rel = getattr(self.__dict__['_obj'], attr) + if hasattr(rel, '__default_set__'): + response = setattr(self._obj, attr, rel.__default_set__(value)) + else: + response = setattr(self._obj, attr, value) + for fun in self.__dict__['_inst']._on_change_fields[self.__dict__['_attribute']]: fun(self.__dict__['_inst'], self.__dict__['_attribute'], value) return response diff --git a/tests/test_model_invoice.py b/tests/test_model_invoice.py index c99cdb8..4b18980 100644 --- a/tests/test_model_invoice.py +++ b/tests/test_model_invoice.py @@ -29,16 +29,17 @@ def _test_simple_invoice(): def test_simple_invoice_cufe(): + token = '693ff6f2a553c3646a063436fd4dd9ded0311471' + environment = fe.AMBIENTE_PRODUCCION + invoice = model.Invoice() - invoice.technical.token = '693ff6f2a553c3646a063436fd4dd9ded0311471' - invoice.technical.environment = fe.AMBIENTE_PRODUCCION invoice.id = '323200000129' invoice.issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z') invoice.supplier.party.id = '700085371' invoice.customer.party.id = '800199436' line = invoice.lines.create() - line.add_tax(model.TaxIva(19.0)) + line.add_tax(model.Taxes.Iva(19.0)) # TODO(bit4bit) acoplamiento temporal # se debe crear primero el subotatl @@ -46,4 +47,4 @@ def test_simple_invoice_cufe(): line.quantity = 1 line.price = 1_500_000 - assert invoice.cufe == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4' + assert invoice.cufe(token, environment) == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4' From 088fa9e6e03727bbbcbe0b5d1d53f701b162aa61 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Sun, 8 Aug 2021 21:20:54 +0000 Subject: [PATCH 30/38] se adiciona namespaces a invoice FossilOrigin-Name: caf85d4a30e7945ca2a6fc3a9430855fa8623442b1a30ae824f742e2a93e2956 --- facho/fe/model/__init__.py | 86 +++++++++++++++++++++------------- facho/model/fields/field.py | 16 +++++-- facho/model/fields/one2many.py | 2 +- tests/test_model.py | 52 ++++++++++++++++++++ 4 files changed, 119 insertions(+), 37 deletions(-) diff --git a/facho/fe/model/__init__.py b/facho/fe/model/__init__.py index 5de325e..504c644 100644 --- a/facho/fe/model/__init__.py +++ b/facho/fe/model/__init__.py @@ -38,9 +38,9 @@ class Time(model.Model): class InvoicePeriod(model.Model): __name__ = 'InvoicePeriod' - start_date = fields.Many2One(Date, name='StartDate') + start_date = fields.Many2One(Date, name='StartDate', namespace='cbc') - end_date = fields.Many2One(Date, name='EndDate') + end_date = fields.Many2One(Date, name='EndDate', namespace='cbc') class ID(model.Model): __name__ = 'ID' @@ -51,15 +51,27 @@ class ID(model.Model): def __str__(self): return str(self._value) +class PartyTaxScheme(model.Model): + __name__ = 'PartyTaxScheme' + + company_id = fields.Many2One(ID, name='CompanyID', namespace='cbc') + tax_level_code = fields.Many2One(ID, name='TaxLevelCode', namespace='cbc', default='ZZ') + class Party(model.Model): __name__ = 'Party' - id = fields.Many2One(ID) + id = fields.Virtual(setter='set_id') + + tax_scheme = fields.Many2One(PartyTaxScheme, namespace='cac') + + def set_id(self, name, value): + self.tax_scheme.company_id = value + return value class AccountingCustomerParty(model.Model): __name__ = 'AccountingCustomerParty' - party = fields.Many2One(Party) + party = fields.Many2One(Party, namespace='cac') class AccountingSupplierParty(model.Model): __name__ = 'AccountingSupplierParty' @@ -100,7 +112,7 @@ class Amount(model.Model): class Price(model.Model): __name__ = 'Price' - amount = fields.Many2One(Amount, name='PriceAmount') + amount = fields.Many2One(Amount, name='PriceAmount', namespace='cbc') def __default_set__(self, value): self.amount = value @@ -115,14 +127,14 @@ class Percent(model.Model): class TaxScheme(model.Model): __name__ = 'TaxScheme' - id = fields.Many2One(ID) - name= fields.Many2One(Name) + id = fields.Many2One(ID, namespace='cbc') + name= fields.Many2One(Name, namespace='cbc') class TaxCategory(model.Model): __name__ = 'TaxCategory' - percent = fields.Many2One(Percent) - tax_scheme = fields.Many2One(TaxScheme) + percent = fields.Many2One(Percent, namespace='cbc') + tax_scheme = fields.Many2One(TaxScheme, namespace='cac') class TaxSubTotal(model.Model): __name__ = 'TaxSubTotal' @@ -154,14 +166,14 @@ class TaxSubTotal(model.Model): class TaxTotal(model.Model): __name__ = 'TaxTotal' - tax_amount = fields.Many2One(Amount, name='TaxAmount', default=0.00) - subtotals = fields.One2Many(TaxSubTotal) + tax_amount = fields.Many2One(Amount, name='TaxAmount', namespace='cbc', default=0.00) + subtotals = fields.One2Many(TaxSubTotal, namespace='cac') class AllowanceCharge(model.Model): __name__ = 'AllowanceCharge' - amount = fields.Many2One(Amount) + amount = fields.Many2One(Amount, namespace='cbc') is_discount = fields.Virtual(default=False) def isCharge(self): @@ -186,11 +198,12 @@ class Taxes: class InvoiceLine(model.Model): __name__ = 'InvoiceLine' - quantity = fields.Many2One(Quantity, name='InvoicedQuantity') - taxtotal = fields.Many2One(TaxTotal) - price = fields.Many2One(Price) - amount = fields.Many2One(Amount, name='LineExtensionAmount') - allowance_charge = fields.One2Many(AllowanceCharge) + id = fields.Many2One(ID, namespace='cbc') + quantity = fields.Many2One(Quantity, name='InvoicedQuantity', namespace='cbc') + taxtotal = fields.Many2One(TaxTotal, namespace='cac') + price = fields.Many2One(Price, namespace='cac') + amount = fields.Many2One(Amount, name='LineExtensionAmount', namespace='cbc') + allowance_charge = fields.One2Many(AllowanceCharge, 'cac') tax_amount = fields.Virtual(getter='get_tax_amount') def __setup__(self): @@ -242,12 +255,12 @@ class InvoiceLine(model.Model): class LegalMonetaryTotal(model.Model): __name__ = 'LegalMonetaryTotal' - line_extension_amount = fields.Many2One(Amount, name='LineExtensionAmount', default=0) + line_extension_amount = fields.Many2One(Amount, name='LineExtensionAmount', namespace='cbc', default=0) - tax_exclusive_amount = fields.Many2One(Amount, name='TaxExclusiveAmount', default=form.Amount(0)) - tax_inclusive_amount = fields.Many2One(Amount, name='TaxInclusiveAmount', default=form.Amount(0)) - charge_total_amount = fields.Many2One(Amount, name='ChargeTotalAmount', default=form.Amount(0)) - payable_amount = fields.Many2One(Amount, name='PayableAmount', default=form.Amount(0)) + tax_exclusive_amount = fields.Many2One(Amount, name='TaxExclusiveAmount', namespace='cbc', default=form.Amount(0)) + tax_inclusive_amount = fields.Many2One(Amount, name='TaxInclusiveAmount', namespace='cbc', default=form.Amount(0)) + charge_total_amount = fields.Many2One(Amount, name='ChargeTotalAmount', namespace='cbc', default=form.Amount(0)) + payable_amount = fields.Many2One(Amount, name='PayableAmount', namespace='cbc', default=form.Amount(0)) @fields.on_change(['tax_inclusive_amount', 'charge_total']) def update_payable_amount(self, name, value): @@ -255,18 +268,27 @@ class LegalMonetaryTotal(model.Model): class Invoice(model.Model): __name__ = 'Invoice' - - id = fields.Many2One(ID) - issue = fields.Virtual(setter='set_issue') - issue_date = fields.Many2One(Date, name='IssueDate') - issue_time = fields.Many2One(Time, name='IssueTime') + __namespace__ = { + 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', + 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', + 'cdt': 'urn:DocumentInformation:names:specification:ubl:colombia:schema:xsd:DocumentInformationAggregateComponents-1', + 'ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2', + 'sts': 'http://www.dian.gov.co/contratos/facturaelectronica/v1/Structures', + 'xades': 'http://uri.etsi.org/01903/v1.3.2#', + 'ds': 'http://www.w3.org/2000/09/xmldsig#' + } - period = fields.Many2One(InvoicePeriod) + id = fields.Many2One(ID, namespace='cbc') + issue = fields.Virtual(setter='set_issue') + issue_date = fields.Many2One(Date, name='IssueDate', namespace='cbc') + issue_time = fields.Many2One(Time, name='IssueTime', namespace='cbc') + + period = fields.Many2One(InvoicePeriod, namespace='cac') - supplier = fields.Many2One(AccountingSupplierParty) - customer = fields.Many2One(AccountingCustomerParty) - lines = fields.One2Many(InvoiceLine) - legal_monetary_total = fields.Many2One(LegalMonetaryTotal) + supplier = fields.Many2One(AccountingSupplierParty, namespace='cac') + customer = fields.Many2One(AccountingCustomerParty, namespace='cac') + lines = fields.One2Many(InvoiceLine, namespace='cac') + legal_monetary_total = fields.Many2One(LegalMonetaryTotal, namespace='cac') taxtotal_01 = fields.Many2One(TaxTotal) taxtotal_04 = fields.Many2One(TaxTotal) diff --git a/facho/model/fields/field.py b/facho/model/fields/field.py index d8a6fe4..7deec60 100644 --- a/facho/model/fields/field.py +++ b/facho/model/fields/field.py @@ -1,3 +1,5 @@ +import warnings + class Field: def __set_name__(self, owner, name, virtual=False): self.name = name @@ -17,8 +19,10 @@ class Field: if name is None: return - if name not in namespaces: - raise KeyError("namespace %s not found" % (name)) + #TODO(bit4bit) aunque las pruebas confirmar + #que si se escribe el namespace que es + #no ahi confirmacion de declaracion previa del namespace + inst._namespace_prefix = name def _call(self, inst, method, *args): @@ -27,7 +31,7 @@ class Field: if callable(call): return call(*args) - def _create_model(self, inst, name=None, model=None, attribute=None): + def _create_model(self, inst, name=None, model=None, attribute=None, namespace=None): try: return inst._fields[self.name] except KeyError: @@ -37,7 +41,11 @@ class Field: obj = self.model() if name is not None: obj.__name__ = name - self._set_namespace(obj, self.namespace, inst.__namespace__) + + if namespace: + self._set_namespace(obj, namespace, inst.__namespace__) + else: + self._set_namespace(obj, self.namespace, inst.__namespace__) if attribute: inst._fields[attribute] = obj diff --git a/facho/model/fields/one2many.py b/facho/model/fields/one2many.py index 2c2dca7..da7960c 100644 --- a/facho/model/fields/one2many.py +++ b/facho/model/fields/one2many.py @@ -67,7 +67,7 @@ class One2Many(Field): assert self.name is not None def creator(attribute): - return self._create_model(inst, name=self.field_name, model=self.model, attribute=attribute) + return self._create_model(inst, name=self.field_name, model=self.model, attribute=attribute, namespace=self.namespace) if inst in self.relation: return self.relation[inst] diff --git a/tests/test_model.py b/tests/test_model.py index 10cc99e..0ceebec 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -195,6 +195,58 @@ def test_model_with_xml_namespace_nested(): person.id = 33 assert '33' == person.to_xml() +def test_model_with_xml_namespace_nested_nested(): + class ID(facho.model.Model): + __name__ = 'ID' + + class Party(facho.model.Model): + __name__ = 'Party' + + id = fields.Many2One(ID, namespace='party') + + def __default_set__(self, value): + self.id = value + + class Person(facho.model.Model): + __name__ = 'Person' + __namespace__ = { + 'person': 'http://lib.facho.cyou', + 'party': 'http://lib.facho.cyou' + } + + id = fields.Many2One(Party, namespace='person') + + person = Person() + person.id = 33 + assert '33' == person.to_xml() + +def test_model_with_xml_namespace_nested_one_many(): + class Name(facho.model.Model): + __name__ = 'Name' + + class Contact(facho.model.Model): + __name__ = 'Contact' + + name = fields.Many2One(Name, namespace='contact') + + class Person(facho.model.Model): + __name__ = 'Person' + __namespace__ = { + 'facho': 'http://lib.facho.cyou', + 'contact': 'http://lib.facho.cyou' + } + + contacts = fields.One2Many(Contact, namespace='facho') + + person = Person() + contact = person.contacts.create() + contact.name = 'contact1' + + contact = person.contacts.create() + contact.name = 'contact2' + + assert 'contact1contact2' == person.to_xml() + def test_field_model_with_namespace(): class ID(facho.model.Model): __name__ = 'ID' From 64b312a432ef0e9bf3f90f6e67dc7a22128b893a Mon Sep 17 00:00:00 2001 From: bit4bit Date: Sun, 8 Aug 2021 22:00:08 +0000 Subject: [PATCH 31/38] se adiciona extensions para la dian FossilOrigin-Name: f5521ddbfb903915de88a26ba5197b67efa1ebfd66337061ee9e3653c59dd217 --- facho/fe/model/__init__.py | 88 ++++++++++++++++------------------ facho/fe/model/common.py | 55 +++++++++++++++++++++ facho/fe/model/dian.py | 37 ++++++++++++++ facho/model/__init__.py | 4 +- facho/model/fields/amount.py | 31 ++++++++++++ facho/model/fields/many2one.py | 5 +- facho/model/fields/virtual.py | 45 +++++++++++++++++ tests/test_model.py | 29 +++++++++++ tests/test_model_invoice.py | 9 +++- 9 files changed, 251 insertions(+), 52 deletions(-) create mode 100644 facho/fe/model/common.py create mode 100644 facho/fe/model/dian.py create mode 100644 facho/model/fields/amount.py create mode 100644 facho/model/fields/virtual.py diff --git a/facho/fe/model/__init__.py b/facho/fe/model/__init__.py index 504c644..5a1c546 100644 --- a/facho/fe/model/__init__.py +++ b/facho/fe/model/__init__.py @@ -2,54 +2,14 @@ import facho.model as model import facho.model.fields as fields import facho.fe.form as form from facho import fe +from .common import * +from . import dian from datetime import date, datetime from collections import defaultdict from copy import copy import hashlib -class Name(model.Model): - __name__ = 'Name' - -class Date(model.Model): - __name__ = 'Date' - - def __default_set__(self, value): - if isinstance(value, str): - return value - if isinstance(value, date): - return value.isoformat() - - def __str__(self): - return str(self._value) - -class Time(model.Model): - __name__ = 'Time' - - def __default_set__(self, value): - if isinstance(value, str): - return value - if isinstance(value, date): - return value.strftime('%H:%M:%S-05:00') - - def __str__(self): - return str(self._value) - -class InvoicePeriod(model.Model): - __name__ = 'InvoicePeriod' - - start_date = fields.Many2One(Date, name='StartDate', namespace='cbc') - - end_date = fields.Many2One(Date, name='EndDate', namespace='cbc') - -class ID(model.Model): - __name__ = 'ID' - - def __default_get__(self, name, value): - return self._value - - def __str__(self): - return str(self._value) class PartyTaxScheme(model.Model): __name__ = 'PartyTaxScheme' @@ -139,10 +99,10 @@ class TaxCategory(model.Model): class TaxSubTotal(model.Model): __name__ = 'TaxSubTotal' - taxable_amount = fields.Many2One(Amount, name='TaxableAmount', default=0.00) - tax_amount = fields.Many2One(Amount, name='TaxAmount', default=0.00) - tax_percent = fields.Many2One(Percent) - tax_category = fields.Many2One(TaxCategory) + taxable_amount = fields.Many2One(Amount, name='TaxableAmount', namespace='cbc', default=0.00) + tax_amount = fields.Many2One(Amount, name='TaxAmount', namespace='cbc', default=0.00) + tax_percent = fields.Many2One(Percent, namespace='cbc') + tax_category = fields.Many2One(TaxCategory, namespace='cac') percent = fields.Virtual(setter='set_category', getter='get_category') scheme = fields.Virtual(setter='set_category', getter='get_category') @@ -266,6 +226,28 @@ class LegalMonetaryTotal(model.Model): def update_payable_amount(self, name, value): self.payable_amount = self.tax_inclusive_amount + self.charge_total_amount + +class DIANExtension(model.Model): + __name__ = 'UBLExtension' + + _content = fields.Many2One(Element, name='ExtensionContent', namespace='ext') + + dian = fields.Many2One(dian.DianExtensions, name='DianExtensions', namespace='sts') + + def __default_get__(self, name, value): + return self.dian + +class UBLExtension(model.Model): + __name__ = 'UBLExtension' + + content = fields.Many2One(Element, name='ExtensionContent', namespace='ext', default='') + +class UBLExtensions(model.Model): + __name__ = 'UBLExtensions' + + dian = fields.Many2One(DIANExtension, namespace='ext', create=True) + extension = fields.Many2One(UBLExtension, namespace='ext', create=True) + class Invoice(model.Model): __name__ = 'Invoice' __namespace__ = { @@ -277,18 +259,25 @@ class Invoice(model.Model): 'xades': 'http://uri.etsi.org/01903/v1.3.2#', 'ds': 'http://www.w3.org/2000/09/xmldsig#' } + + + _ubl_extensions = fields.Many2One(UBLExtensions, namespace='ext') + dian = fields.Virtual(getter='get_dian_extension') + + profile_id = fields.Many2One(Element, name='ProfileID', namespace='cbc', default='DIAN 2.1') + profile_execute_id = fields.Many2One(Element, name='ProfileExecuteID', namespace='cbc', default='2') id = fields.Many2One(ID, namespace='cbc') issue = fields.Virtual(setter='set_issue') issue_date = fields.Many2One(Date, name='IssueDate', namespace='cbc') issue_time = fields.Many2One(Time, name='IssueTime', namespace='cbc') - period = fields.Many2One(InvoicePeriod, namespace='cac') + period = fields.Many2One(Period, name='InvoicePeriod', namespace='cac') supplier = fields.Many2One(AccountingSupplierParty, namespace='cac') customer = fields.Many2One(AccountingCustomerParty, namespace='cac') - lines = fields.One2Many(InvoiceLine, namespace='cac') legal_monetary_total = fields.Many2One(LegalMonetaryTotal, namespace='cac') + lines = fields.One2Many(InvoiceLine, namespace='cac') taxtotal_01 = fields.Many2One(TaxTotal) taxtotal_04 = fields.Many2One(TaxTotal) @@ -359,3 +348,6 @@ class Invoice(model.Model): raise ValueError('expected type datetime') self.issue_date = value.date() self.issue_time = value + + def get_dian_extension(self, name, _value): + return self._ubl_extensions.dian diff --git a/facho/fe/model/common.py b/facho/fe/model/common.py new file mode 100644 index 0000000..6234537 --- /dev/null +++ b/facho/fe/model/common.py @@ -0,0 +1,55 @@ +import facho.model as model +import facho.model.fields as fields + +from datetime import date, datetime + +__all__ = ['Element', 'Name', 'Date', 'Time', 'Period', 'ID'] + +class Element(model.Model): + """ + Lo usuamos para elementos que solo manejan contenido + """ + __name__ = 'Element' + +class Name(model.Model): + __name__ = 'Name' + +class Date(model.Model): + __name__ = 'Date' + + def __default_set__(self, value): + if isinstance(value, str): + return value + if isinstance(value, date): + return value.isoformat() + + def __str__(self): + return str(self._value) + +class Time(model.Model): + __name__ = 'Time' + + def __default_set__(self, value): + if isinstance(value, str): + return value + if isinstance(value, date): + return value.strftime('%H:%M:%S-05:00') + + def __str__(self): + return str(self._value) + +class Period(model.Model): + __name__ = 'Period' + + start_date = fields.Many2One(Date, name='StartDate', namespace='cbc') + + end_date = fields.Many2One(Date, name='EndDate', namespace='cbc') + +class ID(model.Model): + __name__ = 'ID' + + def __default_get__(self, name, value): + return self._value + + def __str__(self): + return str(self._value) diff --git a/facho/fe/model/dian.py b/facho/fe/model/dian.py new file mode 100644 index 0000000..721f5ee --- /dev/null +++ b/facho/fe/model/dian.py @@ -0,0 +1,37 @@ +import facho.model as model +import facho.model.fields as fields +from .common import * + +class SoftwareProvider(model.Model): + __name__ = 'SoftwareProvider' + + provider_id = fields.Many2One(Element, name='ProviderID', namespace='sts') + software_id = fields.Many2One(Element, name='SoftwareID', namespace='sts') + +class InvoiceSource(model.Model): + __name__ = 'InvoiceSource' + + identification_code = fields.Many2One(Element, name='IdentificationCode', namespace='sts', default='CO') + +class AuthorizedInvoices(model.Model): + __name__ = 'AuthorizedInvoices' + + prefix = fields.Many2One(Element, name='Prefix', namespace='sts') + from_range = fields.Many2One(Element, name='From', namespace='sts') + to_range = fields.Many2One(Element, name='To', namespace='sts') + +class InvoiceControl(model.Model): + __name__ = 'InvoiceControl' + + authorization = fields.Many2One(Element, name='InvoiceAuthorization', namespace='sts') + period = fields.Many2One(Period, name='AuthorizationPeriod', namespace='sts') + invoices = fields.Many2One(AuthorizedInvoices, namespace='sts') + +class DianExtensions(model.Model): + __name__ = 'DianExtensions' + + software_security_code = fields.Many2One(Element, name='SoftwareSecurityCode', namespace='sts') + software_provider = fields.Many2One(SoftwareProvider, namespace='sts') + source = fields.Many2One(InvoiceSource, namespace='sts') + control = fields.Many2One(InvoiceControl, namespace='sts') + diff --git a/facho/model/__init__.py b/facho/model/__init__.py index acf29bc..0839644 100644 --- a/facho/model/__init__.py +++ b/facho/model/__init__.py @@ -45,7 +45,9 @@ class ModelBase(object, metaclass=ModelMeta): if isinstance(v, fields.Attribute) or isinstance(v, fields.Many2One) or isinstance(v, fields.Function) or isinstance(v, fields.Amount): if hasattr(v, 'default') and v.default is not None: setattr(obj, key, v.default) - + if hasattr(v, 'create') and v.create == True: + setattr(obj, key, '') + # register callbacks for changes (fun, on_change_fields) = on_change_fields_for_function() for field in on_change_fields: diff --git a/facho/model/fields/amount.py b/facho/model/fields/amount.py new file mode 100644 index 0000000..b756e0a --- /dev/null +++ b/facho/model/fields/amount.py @@ -0,0 +1,31 @@ +from .field import Field +from collections import defaultdict +import facho.fe.form as form + +class Amount(Field): + def __init__(self, name=None, default=None, precision=6): + self.field_name = name + self.values = {} + self.default = default + self.precision = precision + + def __get__(self, model, cls): + if model is None: + return self + assert self.name is not None + + self.__init_value(model) + model._set_field(self.name, self) + return self.values[model] + + def __set__(self, model, value): + assert self.name is not None + self.__init_value(model) + model._set_field(self.name, self) + self.values[model] = form.Amount(value, precision=self.precision) + + self._changed_field(model, self.name, value) + + def __init_value(self, model): + if model not in self.values: + self.values[model] = form.Amount(self.default or 0) diff --git a/facho/model/fields/many2one.py b/facho/model/fields/many2one.py index d39bf36..7201d87 100644 --- a/facho/model/fields/many2one.py +++ b/facho/model/fields/many2one.py @@ -2,7 +2,7 @@ from .field import Field from collections import defaultdict class Many2One(Field): - def __init__(self, model, name=None, setter=None, namespace=None, default=None, virtual=False): + def __init__(self, model, name=None, setter=None, namespace=None, default=None, virtual=False, create=False): self.model = model self.setter = setter self.namespace = namespace @@ -10,7 +10,8 @@ class Many2One(Field): self.default = default self.virtual = virtual self.relations = defaultdict(dict) - + self.create = create + def __get__(self, inst, cls): if inst is None: return self diff --git a/facho/model/fields/virtual.py b/facho/model/fields/virtual.py new file mode 100644 index 0000000..4fe4e02 --- /dev/null +++ b/facho/model/fields/virtual.py @@ -0,0 +1,45 @@ +from .field import Field + +# Un campo virtual +# no participa del renderizado +# pero puede interactura con este +class Virtual(Field): + def __init__(self, + setter=None, + getter='bob', + default=None, + update_internal=False): + self.default = default + self.setter = setter + self.getter = getter + self.values = {} + self.update_internal = update_internal + self.virtual = True + + def __get__(self, inst, cls): + if inst is None: + return self + assert self.name is not None + + value = self.default + try: + value = self.values[inst] + except KeyError: + pass + + try: + self.values[inst] = getattr(inst, self.getter)(self.name, value) + except AttributeError: + self.values[inst] = value + + return self.values[inst] + + def __set__(self, inst, value): + if self.update_internal: + inst._value = value + + if self.setter is None: + self.values[inst] = value + else: + self.values[inst] = self._call(inst, self.setter, self.name, value) + self._changed_field(inst, self.name, value) diff --git a/tests/test_model.py b/tests/test_model.py index 0ceebec..831460e 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -80,6 +80,35 @@ def test_many2one_with_custom_setter(): party.location = 99 assert '' == party.to_xml() +def test_many2one_always_create(): + class Name(facho.model.Model): + __name__ = 'Name' + + class Person(facho.model.Model): + __name__ = 'Person' + + name = fields.Many2One(Name, default='facho') + + person = Person() + assert 'facho' == person.to_xml() + +def test_many2one_nested_always_create(): + class Name(facho.model.Model): + __name__ = 'Name' + + class Contact(facho.model.Model): + __name__ = 'Contact' + + name = fields.Many2One(Name, default='facho') + + class Person(facho.model.Model): + __name__ = 'Person' + + contact = fields.Many2One(Contact, create=True) + + person = Person() + assert 'facho' == person.to_xml() + def test_many2one_auto_create(): class TaxAmount(facho.model.Model): __name__ = 'TaxAmount' diff --git a/tests/test_model_invoice.py b/tests/test_model_invoice.py index 4b18980..01d94cc 100644 --- a/tests/test_model_invoice.py +++ b/tests/test_model_invoice.py @@ -13,8 +13,15 @@ import facho.fe.model as model import facho.fe.form as form from facho import fe -def _test_simple_invoice(): +def test_simple_invoice(): invoice = model.Invoice() + invoice.dian.software_security_code = '12345' + invoice.dian.software_provider.provider_id = 'provider-id' + invoice.dian.software_provider.software_id = 'facho' + invoice.dian.control.prefix = 'SETP' + invoice.dian.control.from_range = '1000' + invoice.dian.control.to_range = '1000' + invoice.id = '323200000129' invoice.issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z') invoice.supplier.party.id = '700085371' From efe93ecc3c4dfe5a65747ff830034a3d2e910a4f Mon Sep 17 00:00:00 2001 From: bit4bit Date: Mon, 9 Aug 2021 00:21:03 +0000 Subject: [PATCH 32/38] mas pruebas y algunos pequenos cambios FossilOrigin-Name: 02bc90719bb17216a749568086887a7d27878fdbd81febfb88a9fb1e68aa8205 --- facho/facho.py | 3 +++ facho/fe/model/__init__.py | 30 +++++++++++---------- facho/fe/model/dian.py | 21 +++++++++++++++ tests/test_model_invoice.py | 53 +++++++++++++++++++++++-------------- 4 files changed, 73 insertions(+), 34 deletions(-) diff --git a/facho/facho.py b/facho/facho.py index 24f907f..b1bc2f8 100644 --- a/facho/facho.py +++ b/facho/facho.py @@ -257,6 +257,9 @@ class FachoXML: def get_element_text(self, xpath, format_=str): xpath = self.fragment_prefix + self._path_xpath_for(xpath) elem = self.builder.xpath(self.root, xpath) + if elem is None: + raise ValueError('xpath %s invalid' % (xpath)) + text = self.builder.get_text(elem) return format_(text) diff --git a/facho/fe/model/__init__.py b/facho/fe/model/__init__.py index 5a1c546..5bc5be3 100644 --- a/facho/fe/model/__init__.py +++ b/facho/fe/model/__init__.py @@ -227,15 +227,18 @@ class LegalMonetaryTotal(model.Model): self.payable_amount = self.tax_inclusive_amount + self.charge_total_amount +class DIANExtensionContent(model.Model): + __name__ = 'ExtensionContent' + + dian = fields.Many2One(dian.DianExtensions, name='DianExtensions', namespace='sts') + class DIANExtension(model.Model): __name__ = 'UBLExtension' - _content = fields.Many2One(Element, name='ExtensionContent', namespace='ext') - - dian = fields.Many2One(dian.DianExtensions, name='DianExtensions', namespace='sts') + content = fields.Many2One(DIANExtensionContent, namespace='ext') def __default_get__(self, name, value): - return self.dian + return self.content.dian class UBLExtension(model.Model): __name__ = 'UBLExtension' @@ -250,16 +253,7 @@ class UBLExtensions(model.Model): class Invoice(model.Model): __name__ = 'Invoice' - __namespace__ = { - 'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2', - 'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', - 'cdt': 'urn:DocumentInformation:names:specification:ubl:colombia:schema:xsd:DocumentInformationAggregateComponents-1', - 'ext': 'urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2', - 'sts': 'http://www.dian.gov.co/contratos/facturaelectronica/v1/Structures', - 'xades': 'http://uri.etsi.org/01903/v1.3.2#', - 'ds': 'http://www.w3.org/2000/09/xmldsig#' - } - + __namespace__ = fe.NAMESPACES _ubl_extensions = fields.Many2One(UBLExtensions, namespace='ext') dian = fields.Virtual(getter='get_dian_extension') @@ -284,6 +278,7 @@ class Invoice(model.Model): taxtotal_03 = fields.Many2One(TaxTotal) def __setup__(self): + self._namespace_prefix = 'fe' # Se requieren minimo estos impuestos para # validar el cufe self._subtotal_01 = self.taxtotal_01.subtotals.create() @@ -351,3 +346,10 @@ class Invoice(model.Model): def get_dian_extension(self, name, _value): return self._ubl_extensions.dian + + def to_xml(self, **kw): + # al generar documento el namespace + # se hace respecto a la raiz + return super().to_xml(**kw)\ + .replace("fe:", "")\ + .replace("xmlns:fe", "xmlns") diff --git a/facho/fe/model/dian.py b/facho/fe/model/dian.py index 721f5ee..dbbe3c2 100644 --- a/facho/fe/model/dian.py +++ b/facho/fe/model/dian.py @@ -2,6 +2,19 @@ import facho.model as model import facho.model.fields as fields from .common import * +class DIANElement(Element): + """ + Elemento que contiene atributos por defecto. + + Puede extender esta clase y modificar los atributos nuevamente + """ + __name__ = 'DIANElement' + + scheme_id = fields.Attribute('schemeID', default='4') + scheme_name = fields.Attribute('schemeName', default='31') + scheme_agency_name = fields.Attribute('schemeAgencyName', default='CO, DIAN (Dirección de Impuestos y Aduanas Nacionales)') + scheme_agency_id = fields.Attribute('schemeAgencyID', default='195') + class SoftwareProvider(model.Model): __name__ = 'SoftwareProvider' @@ -26,10 +39,18 @@ class InvoiceControl(model.Model): authorization = fields.Many2One(Element, name='InvoiceAuthorization', namespace='sts') period = fields.Many2One(Period, name='AuthorizationPeriod', namespace='sts') invoices = fields.Many2One(AuthorizedInvoices, namespace='sts') + +class AuthorizationProvider(model.Model): + __name__ = 'AuthorizationProvider' + + + id = fields.Many2One(DIANElement, name='AuthorizationProviderID', namespace='sts', default='800197268') class DianExtensions(model.Model): __name__ = 'DianExtensions' + authorization_provider = fields.Many2One(AuthorizationProvider, namespace='sts', create=True) + software_security_code = fields.Many2One(Element, name='SoftwareSecurityCode', namespace='sts') software_provider = fields.Many2One(SoftwareProvider, namespace='sts') source = fields.Many2One(InvoiceSource, namespace='sts') diff --git a/tests/test_model_invoice.py b/tests/test_model_invoice.py index 01d94cc..9b156b3 100644 --- a/tests/test_model_invoice.py +++ b/tests/test_model_invoice.py @@ -13,7 +13,9 @@ import facho.fe.model as model import facho.fe.form as form from facho import fe -def test_simple_invoice(): +import helpers + +def simple_invoice(): invoice = model.Invoice() invoice.dian.software_security_code = '12345' invoice.dian.software_provider.provider_id = 'provider-id' @@ -21,25 +23,6 @@ def test_simple_invoice(): invoice.dian.control.prefix = 'SETP' invoice.dian.control.from_range = '1000' invoice.dian.control.to_range = '1000' - - invoice.id = '323200000129' - invoice.issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z') - invoice.supplier.party.id = '700085371' - invoice.customer.party.id = '800199436' - - line = invoice.lines.create() - line.quantity = 1 - line.price = form.Amount(5_000) - subtotal = line.taxtotal.subtotals.create() - subtotal.percent = 19.0 - assert '3232000001292019-01-16T10:53:10-05:0010:5310-05:0070008537180019943610.00.00.019.019.05000.05000.05000.00.00.00.00.019.019.0010.00.00.0040.00.00.003' == invoice.to_xml() - - -def test_simple_invoice_cufe(): - token = '693ff6f2a553c3646a063436fd4dd9ded0311471' - environment = fe.AMBIENTE_PRODUCCION - - invoice = model.Invoice() invoice.id = '323200000129' invoice.issue = datetime.strptime('2019-01-16 10:53:10-05:00', '%Y-%m-%d %H:%M:%S%z') invoice.supplier.party.id = '700085371' @@ -54,4 +37,34 @@ def test_simple_invoice_cufe(): line.quantity = 1 line.price = 1_500_000 + return invoice + +def test_simple_invoice_cufe(): + token = '693ff6f2a553c3646a063436fd4dd9ded0311471' + environment = fe.AMBIENTE_PRODUCCION + invoice = simple_invoice() assert invoice.cufe(token, environment) == '8bb918b19ba22a694f1da11c643b5e9de39adf60311cf179179e9b33381030bcd4c3c3f156c506ed5908f9276f5bd9b4' + +def test_simple_invoice_sign_dian(monkeypatch): + invoice = simple_invoice() + + xmlstring = invoice.to_xml() + p12_data = open('./tests/example.p12', 'rb').read() + signer = fe.DianXMLExtensionSigner.from_bytes(p12_data) + + with monkeypatch.context() as m: + helpers.mock_urlopen(m) + xmlsigned = signer.sign_xml_string(xmlstring) + assert "Signature" in xmlsigned + + +def test_dian_extension_authorization_provider(): + invoice = simple_invoice() + xml = fe.FeXML.from_string(invoice.to_xml()) + provider_id = xml.get_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:AuthorizationProvider/sts:AuthorizationProviderID') + + assert provider_id.attrib['schemeID'] == '4' + assert provider_id.attrib['schemeName'] == '31' + assert provider_id.attrib['schemeAgencyName'] == 'CO, DIAN (Dirección de Impuestos y Aduanas Nacionales)' + assert provider_id.attrib['schemeAgencyID'] == '195' + assert provider_id.text == '800197268' From 3a349a746ec2d774959645fe2ed425201eecd20d Mon Sep 17 00:00:00 2001 From: bit4bit Date: Mon, 9 Aug 2021 00:29:37 +0000 Subject: [PATCH 33/38] se crea parcialmente Address, Country FossilOrigin-Name: 9db11d9b88e2e0065960ac07efe88e8dd8d87706bda88074845f6bfcfecc737b --- facho/facho.py | 2 +- facho/fe/model/__init__.py | 14 +++++++++++--- facho/fe/model/common.py | 22 ++++++++++++++++++++-- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/facho/facho.py b/facho/facho.py index b1bc2f8..02e70a0 100644 --- a/facho/facho.py +++ b/facho/facho.py @@ -258,7 +258,7 @@ class FachoXML: xpath = self.fragment_prefix + self._path_xpath_for(xpath) elem = self.builder.xpath(self.root, xpath) if elem is None: - raise ValueError('xpath %s invalid' % (xpath)) + raise AttributeError('xpath %s invalid' % (xpath)) text = self.builder.get_text(elem) return format_(text) diff --git a/facho/fe/model/__init__.py b/facho/fe/model/__init__.py index 5bc5be3..061890d 100644 --- a/facho/fe/model/__init__.py +++ b/facho/fe/model/__init__.py @@ -11,6 +11,12 @@ from copy import copy import hashlib + +class PhysicalLocation(model.Model): + __name__ = 'PhysicalLocation' + + address = fields.Many2One(Address, namespace='cac') + class PartyTaxScheme(model.Model): __name__ = 'PartyTaxScheme' @@ -20,11 +26,12 @@ class PartyTaxScheme(model.Model): class Party(model.Model): __name__ = 'Party' - id = fields.Virtual(setter='set_id') + id = fields.Virtual(setter='_on_set_id') tax_scheme = fields.Many2One(PartyTaxScheme, namespace='cac') - - def set_id(self, name, value): + location = fields.Many2One(PhysicalLocation, namespace='cac') + + def _on_set_id(self, name, value): self.tax_scheme.company_id = value return value @@ -256,6 +263,7 @@ class Invoice(model.Model): __namespace__ = fe.NAMESPACES _ubl_extensions = fields.Many2One(UBLExtensions, namespace='ext') + # nos interesa el acceso solo los atributos de la DIAN dian = fields.Virtual(getter='get_dian_extension') profile_id = fields.Many2One(Element, name='ProfileID', namespace='cbc', default='DIAN 2.1') diff --git a/facho/fe/model/common.py b/facho/fe/model/common.py index 6234537..c79a780 100644 --- a/facho/fe/model/common.py +++ b/facho/fe/model/common.py @@ -3,14 +3,14 @@ import facho.model.fields as fields from datetime import date, datetime -__all__ = ['Element', 'Name', 'Date', 'Time', 'Period', 'ID'] +__all__ = ['Element', 'Name', 'Date', 'Time', 'Period', 'ID', 'Address', 'Country'] class Element(model.Model): """ Lo usuamos para elementos que solo manejan contenido """ __name__ = 'Element' - + class Name(model.Model): __name__ = 'Name' @@ -53,3 +53,21 @@ class ID(model.Model): def __str__(self): return str(self._value) + + +class Country(model.Model): + __name__ = 'Country' + + name = fields.Many2One(Element, name='Name', namespace='cbc') + +class Address(model.Model): + __name__ = 'Address' + + #DIAN 1.7.-2020: FAJ08 + #DIAN 1.7.-2020: CAJ09 + id = fields.Many2One(Element, name='ID', namespace='cbc') + + #DIAN 1.7.-2020: FAJ09 + #DIAN 1.7.-2020: CAJ10 + city = fields.Many2One(Element, name='CityName', namespace='cbc') + From 3a89c6d3e5e67fec95c8c1e9010c510345071996 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Mon, 9 Aug 2021 01:15:51 +0000 Subject: [PATCH 34/38] se adicionan comentarios FossilOrigin-Name: 13a8728e99e2a715d68bee47a54adcfef5a1f0043abfdfe6aabb3afa99ca9f7f --- facho/model/__init__.py | 11 +++++++++++ facho/model/fields/amount.py | 4 ++++ facho/model/fields/attribute.py | 8 ++++++++ facho/model/fields/function.py | 2 ++ facho/model/fields/many2one.py | 12 ++++++++++++ facho/model/fields/one2many.py | 10 ++++++++++ facho/model/fields/virtual.py | 11 ++++++++++- 7 files changed, 57 insertions(+), 1 deletion(-) diff --git a/facho/model/__init__.py b/facho/model/__init__.py index 0839644..dc3bfaf 100644 --- a/facho/model/__init__.py +++ b/facho/model/__init__.py @@ -4,6 +4,9 @@ from collections import defaultdict class ModelMeta(type): def __new__(cls, name, bases, ns): new = type.__new__(cls, name, bases, ns) + + # mapeamos asignacion en declaracion de clase + # a attributo de objeto if '__name__' in ns: new.__name__ = ns['__name__'] if '__namespace__' in ns: @@ -134,7 +137,15 @@ class ModelBase(object, metaclass=ModelMeta): else: return "<%s%s%s>%s" % (ns, tag, attributes, content, ns, tag) + def __str__(self): + return self.to_xml() + + class Model(ModelBase): + """ + Model clase que representa el modelo + """ + def __before_xml__(self): """ Ejecuta antes de generar el xml, este diff --git a/facho/model/fields/amount.py b/facho/model/fields/amount.py index b756e0a..6402d88 100644 --- a/facho/model/fields/amount.py +++ b/facho/model/fields/amount.py @@ -3,6 +3,10 @@ from collections import defaultdict import facho.fe.form as form class Amount(Field): + """ + Amount representa un campo moneda usando form.Amount + """ + def __init__(self, name=None, default=None, precision=6): self.field_name = name self.values = {} diff --git a/facho/model/fields/attribute.py b/facho/model/fields/attribute.py index 6adfc36..383fb2f 100644 --- a/facho/model/fields/attribute.py +++ b/facho/model/fields/attribute.py @@ -1,7 +1,15 @@ from .field import Field class Attribute(Field): + """ + Attribute es un atributo del elemento actual. + """ + def __init__(self, name, default=None): + """ + :param name: nombre del atribute + :param default: valor por defecto del attributo + """ self.attribute = name self.value = default self.default = default diff --git a/facho/model/fields/function.py b/facho/model/fields/function.py index 2713d35..707fd55 100644 --- a/facho/model/fields/function.py +++ b/facho/model/fields/function.py @@ -4,6 +4,8 @@ class Function(Field): """ Permite modificar el modelo cuando se intenta, obtener el valor de este campo. + + DEPRECATED usar Virtual """ def __init__(self, field, getter=None, default=None): self.field = field diff --git a/facho/model/fields/many2one.py b/facho/model/fields/many2one.py index 7201d87..cd17011 100644 --- a/facho/model/fields/many2one.py +++ b/facho/model/fields/many2one.py @@ -2,7 +2,19 @@ from .field import Field from collections import defaultdict class Many2One(Field): + """ + Many2One describe una relacion pertenece a. + """ + def __init__(self, model, name=None, setter=None, namespace=None, default=None, virtual=False, create=False): + """ + :param model: nombre del modelo destino + :param name: nombre del elemento xml + :param setter: nombre de methodo usado cuando se asigna usa como asignacion ejemplo model.relation = 3 + :param default: el valor o contenido por defecto + :param virtual: se crea la relacion por no se ve reflejada en el xml final + :param create: fuerza la creacion del elemento en el xml, ya que los elementos no son creados sino tienen contenido + """ self.model = model self.setter = setter self.namespace = namespace diff --git a/facho/model/fields/one2many.py b/facho/model/fields/one2many.py index da7960c..8f7f339 100644 --- a/facho/model/fields/one2many.py +++ b/facho/model/fields/one2many.py @@ -56,7 +56,17 @@ class _Relation(): yield relation class One2Many(Field): + """ + One2Many describe una relacion tiene muchos. + """ + def __init__(self, model, name=None, namespace=None, default=None): + """ + :param model: nombre del modelo destino + :param name: nombre del elemento xml cuando se crea hijo + :param namespace: sufijo del namespace al que pertenece el elemento + :param default: el valor o contenido por defecto + """ self.model = model self.field_name = name self.namespace = namespace diff --git a/facho/model/fields/virtual.py b/facho/model/fields/virtual.py index 4fe4e02..e39b9dd 100644 --- a/facho/model/fields/virtual.py +++ b/facho/model/fields/virtual.py @@ -4,11 +4,20 @@ from .field import Field # no participa del renderizado # pero puede interactura con este class Virtual(Field): + """ + Virtual es un campo que no es renderizado en el xml final + """ def __init__(self, setter=None, - getter='bob', + getter='', default=None, update_internal=False): + """ + :param setter: nombre de methodo usado cuando se asigna usa como asignacion ejemplo model.relation = 3 + :param getter: nombre del metodo usando cuando se obtiene, ejemplo: valor = mode.relation + :param default: valor por defecto + :param update_internal: indica que cuando se asigne algun valor este se almacena localmente + """ self.default = default self.setter = setter self.getter = getter From 302812328e26200ba1d164ece9512a9966c4ffa7 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Mon, 9 Aug 2021 01:16:44 +0000 Subject: [PATCH 35/38] se adicionan comentarios FossilOrigin-Name: 7137f93d5b3081e2d8f3d1245c97f4aa109c2c671744eef5a4abc0f7551183d0 --- facho/model/fields/many2one.py | 1 + 1 file changed, 1 insertion(+) diff --git a/facho/model/fields/many2one.py b/facho/model/fields/many2one.py index cd17011..966d4d2 100644 --- a/facho/model/fields/many2one.py +++ b/facho/model/fields/many2one.py @@ -11,6 +11,7 @@ class Many2One(Field): :param model: nombre del modelo destino :param name: nombre del elemento xml :param setter: nombre de methodo usado cuando se asigna usa como asignacion ejemplo model.relation = 3 + :param namespace: sufijo del namespace al que pertenece el elemento :param default: el valor o contenido por defecto :param virtual: se crea la relacion por no se ve reflejada en el xml final :param create: fuerza la creacion del elemento en el xml, ya que los elementos no son creados sino tienen contenido From 6716efd1212a3b3f8f9ad584a040830f32363f07 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Mon, 9 Aug 2021 01:47:30 +0000 Subject: [PATCH 36/38] se actualiza a 0.2.1 FossilOrigin-Name: 65ab09bc6e7823c31d3b201825edc868c46e92ecca963a2c6a0bb9aec9957661 --- HISTORY.rst | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 780d980..5dc0811 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,4 +3,4 @@ History ======= -* First release on PyPI. +* 0.2.1 version usada en produccion. diff --git a/setup.py b/setup.py index e4e0df7..be0a9ed 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,6 @@ setup( test_suite='tests', tests_require=test_requirements, url='https://github.com/bit4bit/facho', - version='0.1.2', + version='0.2.1', zip_safe=False, ) From 2e130d39e6b75dcc18f38ff4668344e78af71e13 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Wed, 6 Oct 2021 01:08:01 +0000 Subject: [PATCH 37/38] se adicionan mas campos a Invoice.Party FossilOrigin-Name: 033c6a3c0297c83c1d0bf2bb246f55fa7ce0d8aaa752f2ae3ec9515c763d61bf --- facho/fe/model/__init__.py | 8 ++++-- facho/fe/model/common.py | 19 ++++++++++++- tests/test_model_invoice.py | 53 +++++++++++++++++++++++++++++++++++-- 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/facho/fe/model/__init__.py b/facho/fe/model/__init__.py index 061890d..6b10421 100644 --- a/facho/fe/model/__init__.py +++ b/facho/fe/model/__init__.py @@ -20,16 +20,20 @@ class PhysicalLocation(model.Model): class PartyTaxScheme(model.Model): __name__ = 'PartyTaxScheme' + registration_name = fields.Many2One(Name, name='RegistrationName', namespace='cbc') company_id = fields.Many2One(ID, name='CompanyID', namespace='cbc') tax_level_code = fields.Many2One(ID, name='TaxLevelCode', namespace='cbc', default='ZZ') - + + class Party(model.Model): __name__ = 'Party' id = fields.Virtual(setter='_on_set_id') + name = fields.Many2One(PartyName, namespace='cac') tax_scheme = fields.Many2One(PartyTaxScheme, namespace='cac') location = fields.Many2One(PhysicalLocation, namespace='cac') + contact = fields.Many2One(Contact, namespace='cac') def _on_set_id(self, name, value): self.tax_scheme.company_id = value @@ -43,7 +47,7 @@ class AccountingCustomerParty(model.Model): class AccountingSupplierParty(model.Model): __name__ = 'AccountingSupplierParty' - party = fields.Many2One(Party) + party = fields.Many2One(Party, namespace='cac') class Quantity(model.Model): __name__ = 'Quantity' diff --git a/facho/fe/model/common.py b/facho/fe/model/common.py index c79a780..fa875ee 100644 --- a/facho/fe/model/common.py +++ b/facho/fe/model/common.py @@ -3,7 +3,7 @@ import facho.model.fields as fields from datetime import date, datetime -__all__ = ['Element', 'Name', 'Date', 'Time', 'Period', 'ID', 'Address', 'Country'] +__all__ = ['Element', 'PartyName', 'Name', 'Date', 'Time', 'Period', 'ID', 'Address', 'Country', 'Contact'] class Element(model.Model): """ @@ -71,3 +71,20 @@ class Address(model.Model): #DIAN 1.7.-2020: CAJ10 city = fields.Many2One(Element, name='CityName', namespace='cbc') + +class PartyName(model.Model): + __name__ = 'PartyName' + + name = fields.Many2One(Name, namespace='cbc') + + def __default_set__(self, value): + self.name = value + return value + + def __default_get__(self, name, value): + return self.name + +class Contact(model.Model): + __name__ = 'Contact' + + email = fields.Many2One(Name, name='ElectronicEmail', namespace='cbc') diff --git a/tests/test_model_invoice.py b/tests/test_model_invoice.py index 9b156b3..f4b98df 100644 --- a/tests/test_model_invoice.py +++ b/tests/test_model_invoice.py @@ -8,11 +8,10 @@ from datetime import datetime import pytest - +from lxml import etree import facho.fe.model as model import facho.fe.form as form from facho import fe - import helpers def simple_invoice(): @@ -68,3 +67,53 @@ def test_dian_extension_authorization_provider(): assert provider_id.attrib['schemeAgencyName'] == 'CO, DIAN (Dirección de Impuestos y Aduanas Nacionales)' assert provider_id.attrib['schemeAgencyID'] == '195' assert provider_id.text == '800197268' + +def test_invoicesimple_xml_signed_using_fexml(monkeypatch): + invoice = simple_invoice() + + xml = fe.FeXML.from_string(invoice.to_xml()) + + signer = fe.DianXMLExtensionSigner('./tests/example.p12') + + print(xml.tostring()) + with monkeypatch.context() as m: + import helpers + helpers.mock_urlopen(m) + xml.add_extension(signer) + + elem = xml.get_element('/fe:Invoice/ext:UBLExtensions/ext:UBLExtension[2]/ext:ExtensionContent/ds:Signature') + assert elem.text is not None + +def test_invoice_supplier_party(): + invoice = simple_invoice() + invoice.supplier.party.name = 'superfacho' + invoice.supplier.party.tax_scheme.registration_name = 'legal-superfacho' + invoice.supplier.party.contact.email = 'superfacho@etrivial.net' + + xml = fe.FeXML.from_string(invoice.to_xml()) + + name = xml.get_element('/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name') + assert name.text == 'superfacho' + + name = xml.get_element('/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName') + assert name.text == 'legal-superfacho' + + name = xml.get_element('/fe:Invoice/cac:AccountingSupplierParty/cac:Party/cac:Contact/cbc:ElectronicEmail') + assert name.text == 'superfacho@etrivial.net' + +def test_invoice_customer_party(): + invoice = simple_invoice() + invoice.customer.party.name = 'superfacho-customer' + invoice.customer.party.tax_scheme.registration_name = 'legal-superfacho-customer' + invoice.customer.party.contact.email = 'superfacho@etrivial.net' + + xml = fe.FeXML.from_string(invoice.to_xml()) + + name = xml.get_element('/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name') + assert name.text == 'superfacho-customer' + + name = xml.get_element('/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:RegistrationName') + assert name.text == 'legal-superfacho-customer' + + name = xml.get_element('/fe:Invoice/cac:AccountingCustomerParty/cac:Party/cac:Contact/cbc:ElectronicEmail') + assert name.text == 'superfacho@etrivial.net' From 48d1ba37350fa5825b007622c9268ecb08cdf583 Mon Sep 17 00:00:00 2001 From: bit4bit Date: Wed, 6 Oct 2021 02:10:01 +0000 Subject: [PATCH 38/38] Create new branch named "morfo" FossilOrigin-Name: 49206404a73c744c29382b1a54ea35227b248c483f44dd09a77d7a7fbdefa691