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'