Compare commits
	
		
			1 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 50b1a13c0a | 
| @@ -3,4 +3,4 @@ History | ||||
| ======= | ||||
|  | ||||
|  | ||||
| * 0.2.1 version usada en produccion. | ||||
| * First release on PyPI. | ||||
|   | ||||
| @@ -257,9 +257,6 @@ 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 AttributeError('xpath %s invalid' % (xpath)) | ||||
|  | ||||
|         text = self.builder.get_text(elem) | ||||
|         return format_(text) | ||||
|  | ||||
|   | ||||
| @@ -119,7 +119,8 @@ class DianXMLExtensionCUDFE(FachoXMLExtension): | ||||
|         fachoxml.set_element('./cbc:UUID', cufe, | ||||
|                              schemeID=self.tipo_ambiente, | ||||
|                              schemeName=self.schemeName()) | ||||
|         fachoxml.set_element('./cbc:ProfileID', 'DIAN 2.1') | ||||
|         #DIAN 1.8.-2021: FAD03 | ||||
|         fachoxml.set_element('./cbc:ProfileID', 'DIAN 2.1: Factura Electrónica de Venta') | ||||
|         fachoxml.set_element('./cbc:ProfileExecutionID', self._tipo_ambiente_int()) | ||||
|         #DIAN 1.7.-2020: FAB36 | ||||
|         fachoxml.set_element('./ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sts:DianExtensions/sts:QRCode', | ||||
|   | ||||
| @@ -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'), precision = DECIMAL_PRECISION): | ||||
|         self.precision = precision | ||||
|     def __init__(self, amount: int or float or str or Amount, currency: Currency = Currency('COP')): | ||||
|  | ||||
|         #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=self.precision, | ||||
|             self.amount = Decimal(amount, decimal.Context(prec=DECIMAL_PRECISION, | ||||
|                                                           #DIAN 1.7.-2020: 1.2.1.1 | ||||
|                                                           rounding=decimal.ROUND_HALF_EVEN )) | ||||
|             self.currency = currency | ||||
| @@ -87,22 +87,19 @@ class Amount: | ||||
|     def __lt__(self, other): | ||||
|         if not self.is_same_currency(other): | ||||
|             raise AmountCurrencyError() | ||||
|         return round(self.amount, self.precision) < round(other, 2) | ||||
|         return round(self.amount, DECIMAL_PRECISION) < round(other, 2) | ||||
|  | ||||
|     def __eq__(self, other): | ||||
|         if not self.is_same_currency(other): | ||||
|             raise AmountCurrencyError() | ||||
|         return round(self.amount, self.precision) == round(other.amount, self.precision) | ||||
|         return round(self.amount, DECIMAL_PRECISION) == round(other.amount, DECIMAL_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))) | ||||
|         raise TypeError("cant cast to amount") | ||||
|      | ||||
|     def __add__(self, rother): | ||||
|         other = self._cast(rother) | ||||
|   | ||||
| @@ -1,367 +0,0 @@ | ||||
| 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 PhysicalLocation(model.Model): | ||||
|     __name__ = 'PhysicalLocation' | ||||
|  | ||||
|     address = fields.Many2One(Address, namespace='cac') | ||||
|      | ||||
| 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 | ||||
|         return value | ||||
|  | ||||
| class AccountingCustomerParty(model.Model): | ||||
|     __name__ = 'AccountingCustomerParty' | ||||
|  | ||||
|     party = fields.Many2One(Party, namespace='cac') | ||||
|  | ||||
| class AccountingSupplierParty(model.Model): | ||||
|     __name__ = 'AccountingSupplierParty' | ||||
|  | ||||
|     party = fields.Many2One(Party, namespace='cac') | ||||
|  | ||||
| class Quantity(model.Model): | ||||
|     __name__  = 'Quantity' | ||||
|  | ||||
|     code = fields.Attribute('unitCode', default='NAR') | ||||
|  | ||||
|     def __setup__(self): | ||||
|         self.value = 0 | ||||
|  | ||||
|     def __default_set__(self, value): | ||||
|         self.value = value | ||||
|         return value | ||||
|  | ||||
|     def __default_get__(self, name, value): | ||||
|         return self.value | ||||
|  | ||||
| class Amount(model.Model): | ||||
|     __name__ = 'Amount' | ||||
|  | ||||
|     currency = fields.Attribute('currencyID', default='COP') | ||||
|     value = fields.Amount(name='amount', default=0.00, precision=2) | ||||
|  | ||||
|     def __default_set__(self, value): | ||||
|         self.value = value | ||||
|         return value | ||||
|  | ||||
|     def __default_get__(self, name, value): | ||||
|         return self.value | ||||
|  | ||||
|     def __str__(self): | ||||
|         return str(self.value) | ||||
|  | ||||
| class Price(model.Model): | ||||
|     __name__ = 'Price' | ||||
|  | ||||
|     amount = fields.Many2One(Amount, name='PriceAmount', namespace='cbc') | ||||
|      | ||||
|     def __default_set__(self, value): | ||||
|         self.amount = value | ||||
|         return value | ||||
|  | ||||
|     def __default_get__(self, name, value): | ||||
|         return self.amount | ||||
|  | ||||
| class Percent(model.Model): | ||||
|     __name__ = 'Percent' | ||||
|  | ||||
| class TaxScheme(model.Model): | ||||
|     __name__ = 'TaxScheme' | ||||
|  | ||||
|     id = fields.Many2One(ID, namespace='cbc') | ||||
|     name= fields.Many2One(Name, namespace='cbc') | ||||
|  | ||||
| class TaxCategory(model.Model): | ||||
|     __name__ = 'TaxCategory' | ||||
|  | ||||
|     percent = fields.Many2One(Percent, namespace='cbc') | ||||
|     tax_scheme = fields.Many2One(TaxScheme, namespace='cac') | ||||
|      | ||||
| class TaxSubTotal(model.Model): | ||||
|     __name__ = 'TaxSubTotal' | ||||
|  | ||||
|     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') | ||||
|  | ||||
|     def set_category(self, name, value): | ||||
|         if name == 'percent': | ||||
|             self.tax_category.percent = value | ||||
|             # 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', namespace='cbc', default=0.00) | ||||
|     subtotals = fields.One2Many(TaxSubTotal, namespace='cac') | ||||
|  | ||||
|  | ||||
| class AllowanceCharge(model.Model): | ||||
|     __name__ = 'AllowanceCharge' | ||||
|  | ||||
|     amount = fields.Many2One(Amount, namespace='cbc') | ||||
|     is_discount = fields.Virtual(default=False) | ||||
|      | ||||
|     def isCharge(self): | ||||
|         return self.is_discount == False | ||||
|  | ||||
|     def isDiscount(self): | ||||
|         return self.is_discount == True | ||||
|  | ||||
| class Taxes: | ||||
|     class Scheme: | ||||
|         def __init__(self, scheme): | ||||
|             self.scheme = scheme | ||||
|  | ||||
|     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) | ||||
|      | ||||
| class InvoiceLine(model.Model): | ||||
|     __name__ = 'InvoiceLine' | ||||
|  | ||||
|     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): | ||||
|         self._taxs = defaultdict(list) | ||||
|         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 | ||||
|  | ||||
|         return total | ||||
|  | ||||
|     @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 = form.Amount(self.quantity)  * form.Amount(self.price) | ||||
|         self.amount = total + charge - discount | ||||
|          | ||||
|         for (scheme, subtotal) in self._subtotals.items(): | ||||
|             subtotal.tax_amount = 0 | ||||
|  | ||||
|         for (scheme, taxes) in self._taxs.items(): | ||||
|             for tax in taxes: | ||||
|                 self._subtotals[scheme].tax_amount += tax.calculate(self.amount) | ||||
|  | ||||
| class LegalMonetaryTotal(model.Model): | ||||
|     __name__ = 'LegalMonetaryTotal' | ||||
|  | ||||
|     line_extension_amount = fields.Many2One(Amount, name='LineExtensionAmount', namespace='cbc', default=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): | ||||
|         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(DIANExtensionContent, namespace='ext') | ||||
|  | ||||
|     def __default_get__(self, name, value): | ||||
|         return self.content.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__ = 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') | ||||
|     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(Period, name='InvoicePeriod', namespace='cac') | ||||
|  | ||||
|     supplier = fields.Many2One(AccountingSupplierParty, namespace='cac') | ||||
|     customer = fields.Many2One(AccountingCustomerParty, 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) | ||||
|     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() | ||||
|         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 cufe(self, token, environment): | ||||
|  | ||||
|         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) | ||||
|         valor_impuesto_03 = form.Amount(0.0) | ||||
|  | ||||
|         for line in self.lines: | ||||
|             for subtotal in line.taxtotal.subtotals: | ||||
|                 if subtotal.scheme.id == '01': | ||||
|                     valor_impuesto_01 += subtotal.tax_amount | ||||
|                 elif subtotal.scheme.id == '04': | ||||
|                     valor_impuesto_04 += subtotal.tax_amount | ||||
|                 elif subtotal.scheme.id == '03': | ||||
|                     valor_impuesto_03 += subtotal.tax_amount | ||||
|  | ||||
|         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(token), | ||||
|             str(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 = 0 | ||||
|         self.legal_monetary_total.tax_inclusive_amount = 0 | ||||
|  | ||||
|         for line in self.lines: | ||||
|             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): | ||||
|             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 | ||||
|  | ||||
|     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") | ||||
| @@ -1,90 +0,0 @@ | ||||
| import facho.model as model | ||||
| import facho.model.fields as fields | ||||
|  | ||||
| from datetime import date, datetime | ||||
|  | ||||
| __all__ = ['Element', 'PartyName', 'Name', 'Date', 'Time', 'Period', 'ID', 'Address', 'Country', 'Contact'] | ||||
|  | ||||
| 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) | ||||
|  | ||||
|  | ||||
| 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') | ||||
|      | ||||
|  | ||||
| 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') | ||||
| @@ -1,58 +0,0 @@ | ||||
| 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' | ||||
|  | ||||
|     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 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') | ||||
|     control = fields.Many2One(InvoiceControl, namespace='sts') | ||||
|  | ||||
| @@ -1,175 +0,0 @@ | ||||
| from .fields import Field | ||||
| 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: | ||||
|             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._value = None | ||||
|         obj._namespace_prefix = None | ||||
|         obj._on_change_fields = defaultdict(list) | ||||
|         obj._order_fields = [] | ||||
|          | ||||
|         def on_change_fields_for_function(): | ||||
|             # 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) | ||||
|                     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 | ||||
|         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) 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: | ||||
|                     obj._on_change_fields[field].append(fun) | ||||
|  | ||||
|  | ||||
|         # post inicializacion del objeto | ||||
|         obj.__setup__() | ||||
|         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 _get_field(self, name): | ||||
|         return self._fields[name] | ||||
|  | ||||
|     def _set_field(self, name, field): | ||||
|         field.name = name | ||||
|         self._fields[name] = field | ||||
|  | ||||
|     def _set_content(self, value): | ||||
|         default = self.__default_set__(value) | ||||
|         if default is not None: | ||||
|             self._value = default | ||||
|  | ||||
|     def to_xml(self): | ||||
|         """ | ||||
|         Genera xml del modelo y sus relaciones | ||||
|         """ | ||||
|         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 = '' | ||||
|         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 = "" | ||||
|  | ||||
|         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] | ||||
|             # 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): | ||||
|                 content += value | ||||
|  | ||||
|         if self._value is not None: | ||||
|             content += str(self._value) | ||||
|  | ||||
|         if content == "": | ||||
|             return "<%s%s%s/>" % (ns, tag, attributes) | ||||
|         else: | ||||
|             return "<%s%s%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 | ||||
|         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 | ||||
|  | ||||
|     def __default_get__(self, name, value): | ||||
|         """ | ||||
|         Al obtener el valor atraves de una relacion (age = person.age) | ||||
|         Retorno de valor por defecto | ||||
|         """ | ||||
|         return value | ||||
|  | ||||
|     def __setup__(self): | ||||
|         """ | ||||
|         Inicializar modelo | ||||
|         """ | ||||
|          | ||||
| @@ -1,21 +0,0 @@ | ||||
| from .attribute import Attribute | ||||
| from .many2one import Many2One | ||||
| 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, Amount] | ||||
|  | ||||
| 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 | ||||
| @@ -1,35 +0,0 @@ | ||||
| from .field import Field | ||||
| 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 = {} | ||||
|         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) | ||||
| @@ -1,29 +0,0 @@ | ||||
| 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 | ||||
|  | ||||
|     def __get__(self, inst, cls): | ||||
|         if inst is None: | ||||
|             return self | ||||
|  | ||||
|         assert self.name is not None | ||||
|         return self.value | ||||
|  | ||||
|     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) | ||||
| @@ -1,60 +0,0 @@ | ||||
| import warnings | ||||
|  | ||||
| class Field: | ||||
|     def __set_name__(self, owner, name, virtual=False): | ||||
|         self.name = name | ||||
|         self.virtual = virtual | ||||
|  | ||||
|     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 | ||||
|  | ||||
|         #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): | ||||
|         call = getattr(inst, method or '', None) | ||||
|  | ||||
|         if callable(call): | ||||
|             return call(*args) | ||||
|  | ||||
|     def _create_model(self, inst, name=None, model=None, attribute=None, namespace=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 | ||||
|  | ||||
|             if namespace: | ||||
|                 self._set_namespace(obj, namespace, inst.__namespace__) | ||||
|             else: | ||||
|                 self._set_namespace(obj, self.namespace, inst.__namespace__) | ||||
|  | ||||
|             if attribute: | ||||
|                 inst._fields[attribute] = obj | ||||
|             else: | ||||
|                 inst._fields[self.name] = obj | ||||
|  | ||||
|             return obj | ||||
|  | ||||
|     def _changed_field(self, inst, name, value): | ||||
|         for fun in inst._on_change_fields[name]: | ||||
|             fun(inst, name, value) | ||||
|              | ||||
| @@ -1,36 +0,0 @@ | ||||
| from .field import Field | ||||
|  | ||||
| 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 | ||||
|         self.getter = getter | ||||
|         self.default = default | ||||
|  | ||||
|     def __get__(self, inst, cls): | ||||
|         if inst is None: | ||||
|             return self | ||||
|         assert self.name is not None | ||||
|  | ||||
|         # si se indica `field` se adiciona | ||||
|         # como campo del modelo, esto es | ||||
|         # que se serializa a xml | ||||
|         inst._set_field(self.name, self.field) | ||||
|  | ||||
|         if self.getter is not None: | ||||
|             value = self._call(inst, self.getter, self.name, self.field) | ||||
|  | ||||
|             if value is not None: | ||||
|                 self.field.__set__(inst, value) | ||||
|  | ||||
|         return self.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) | ||||
| @@ -1,62 +0,0 @@ | ||||
| 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 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 | ||||
|         """ | ||||
|         self.model = model | ||||
|         self.setter = setter | ||||
|         self.namespace = namespace | ||||
|         self.field_name = name | ||||
|         self.default = default | ||||
|         self.virtual = virtual | ||||
|         self.relations = defaultdict(dict) | ||||
|         self.create = create | ||||
|  | ||||
|     def __get__(self, inst, cls): | ||||
|         if inst is None: | ||||
|             return self | ||||
|         assert self.name is not None | ||||
|  | ||||
|         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) | ||||
|         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 | ||||
|         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 | ||||
|         setter = getattr(inst, self.setter or '', None) | ||||
|         if callable(setter): | ||||
|             setter(inst_model, value) | ||||
|         else: | ||||
|             inst_model._set_content(value) | ||||
|              | ||||
|         self._changed_field(inst, self.name, value) | ||||
|  | ||||
|  | ||||
| @@ -1,86 +0,0 @@ | ||||
| from .field import Field | ||||
| from collections import defaultdict | ||||
|  | ||||
| # 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] | ||||
|  | ||||
|         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 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 | ||||
|  | ||||
| class _Relation(): | ||||
|     def __init__(self, creator, inst, attribute): | ||||
|         self.creator = creator | ||||
|         self.inst = inst | ||||
|         self.attribute = attribute | ||||
|         self.relations = [] | ||||
|  | ||||
|     def create(self): | ||||
|         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) | ||||
|  | ||||
|     def __iter__(self): | ||||
|         for relation in self.relations: | ||||
|             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 | ||||
|         self.default = default | ||||
|         self.relation = {} | ||||
|          | ||||
|     def __get__(self, inst, cls): | ||||
|         assert self.name is not None | ||||
|  | ||||
|         def creator(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] | ||||
|         else: | ||||
|             self.relation[inst] = _Relation(creator, inst, self.name) | ||||
|             return self.relation[inst] | ||||
| @@ -1,54 +0,0 @@ | ||||
| from .field import Field | ||||
|  | ||||
| # Un campo virtual | ||||
| # 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='', | ||||
|                  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 | ||||
|         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)         | ||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
									
									
									
									
								
							| @@ -61,6 +61,6 @@ setup( | ||||
|     test_suite='tests', | ||||
|     tests_require=test_requirements, | ||||
|     url='https://github.com/bit4bit/facho', | ||||
|     version='0.2.1', | ||||
|     version='0.1.2', | ||||
|     zip_safe=False, | ||||
| ) | ||||
|   | ||||
| @@ -1,601 +0,0 @@ | ||||
| #!/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/>" == 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 id=\"33\"/>" == person.to_xml() | ||||
|     assert "<Person id=\"44\"/>" == 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 "<Person><ID>33</ID></Person>" == 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 '<TaxTotal><TaxAmount currencyID="COP">3333</TaxAmount></TaxTotal>' == 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><PhysicalLocation ID="99"/></Party>' == 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 '<Person><Name>facho</Name></Person>' == 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 '<Person><Contact><Name>facho</Name></Contact></Person>' == person.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 '<TaxTotal><TaxAmount currencyID="COP">3333</TaxAmount></TaxTotal>' == tax_total.to_xml() | ||||
|  | ||||
| def test_field_model(): | ||||
|     class ID(facho.model.Model): | ||||
|         __name__ = 'ID' | ||||
|          | ||||
|     class Person(facho.model.Model): | ||||
|         __name__ = 'Person' | ||||
|  | ||||
|         id = fields.Many2One(ID) | ||||
|  | ||||
|     person = Person() | ||||
|     person.id = ID() | ||||
|     person.id = 33 | ||||
|     assert "<Person><ID>33</ID></Person>" == 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 "<Person><ID>33</ID><ID>44</ID></Person>" == 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.Many2One(ID) | ||||
|  | ||||
|  | ||||
|     person = Person() | ||||
|     person.id = 33 | ||||
|     assert "<Person><ID>33</ID></Person>" == 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.Many2One(ID, name='DID') | ||||
|  | ||||
|  | ||||
|     person = Person() | ||||
|     person.id = 33 | ||||
|     assert "<Person><DID>33</DID></Person>" == 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.Many2One(ID) | ||||
|  | ||||
|     person = Person() | ||||
|     person.id = 33 | ||||
|     person.id.reference = 'haber' | ||||
|     assert '<Person><ID REFERENCE="haber">33</ID></Person>' == 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 '<Person xmlns:facho="http://lib.facho.cyou"/>' | ||||
|  | ||||
| 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 '<Person xmlns:facho="http://lib.facho.cyou"><facho:ID>33</facho:ID></Person>' == 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 '<Person xmlns:person="http://lib.facho.cyou" xmlns:party="http://lib.facho.cyou"><person:Party><party:ID>33</party:ID></person:Party></Person>' == 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 '<Person xmlns:facho="http://lib.facho.cyou" xmlns:contact="http://lib.facho.cyou"><facho:Contact><contact:Name>contact1</contact:Name></facho:Contact><facho:Contact><contact:Name>contact2</contact:Name></facho:Contact></Person>' == 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.Many2One(ID, namespace="facho") | ||||
|  | ||||
|  | ||||
|     person = Person() | ||||
|     person.id = 33 | ||||
|     assert '<Person xmlns:facho="http://lib.facho.cyou"><facho:ID>33</facho:ID></Person>' == 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.Many2One(Hash) | ||||
|  | ||||
|         def __before_xml__(self): | ||||
|             self.hash = "calculate" | ||||
|              | ||||
|     person = Person() | ||||
|     assert "<Person><Hash>calculate</Hash></Person>" == person.to_xml() | ||||
|  | ||||
|  | ||||
| def test_field_function_with_attribute(): | ||||
|     class Person(facho.model.Model): | ||||
|         __name__ = 'Person' | ||||
|  | ||||
|         hash = fields.Function(fields.Attribute('hash'), getter='get_hash') | ||||
|  | ||||
|         def get_hash(self, name, field): | ||||
|             return 'calculate' | ||||
|          | ||||
|     person = Person() | ||||
|     assert '<Person hash="calculate"/>' | ||||
|  | ||||
| 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(fields.Many2One(Hash), getter='get_hash') | ||||
|  | ||||
|         def get_hash(self, name, field): | ||||
|             field.id = 'calculate' | ||||
|  | ||||
|          | ||||
|     person = Person() | ||||
|     assert person.hash.id == 'calculate' | ||||
|     assert '<Person/>' | ||||
|     | ||||
|  | ||||
| def test_field_function_setter(): | ||||
|     class Person(facho.model.Model): | ||||
|         __name__ = 'Person' | ||||
|  | ||||
|         hash = fields.Attribute('hash') | ||||
|         password = fields.Virtual(setter='set_hash') | ||||
|  | ||||
|         def set_hash(self, name, value): | ||||
|             self.hash = "%s+2" % (value) | ||||
|  | ||||
|     person = Person() | ||||
|     person.password = 'calculate' | ||||
|     assert '<Person hash="calculate+2"/>' == person.to_xml() | ||||
|  | ||||
| def test_field_function_only_setter(): | ||||
|     class Person(facho.model.Model): | ||||
|         __name__ = 'Person' | ||||
|  | ||||
|         hash = fields.Attribute('hash') | ||||
|         password = fields.Virtual(setter='set_hash') | ||||
|  | ||||
|         def set_hash(self, name, value): | ||||
|             self.hash = "%s+2" % (value) | ||||
|  | ||||
|     person = Person() | ||||
|     person.password = 'calculate' | ||||
|     assert '<Person hash="calculate+2"/>' == 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 '<Person><Hash>hola+3</Hash></Person>' == 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/>" == 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 hash="calculate"/>' == 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 hash="calculate"/>' == 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><ID key="oe"/></Person>' == 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 '<Person><ID>ole</ID></Person>' == 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 '<Person react="hola+4"><Hash>hola</Hash></Person>' == 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_react(self, name, value): | ||||
|             assert name == 'hash' | ||||
|             self.react = "%s+4" % (value) | ||||
|  | ||||
|     person = Person() | ||||
|     person.hash = 'hola' | ||||
|     assert '<Person react="hola+4" Hash="hola"/>' == 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><Line quantity="3"/><Line quantity="5"/></Invoice>' == 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 count="2"><Line quantity="3"/><Line quantity="5"/></Invoice>' == 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><Line quantity="3"/><Line quantity="5"/></Invoice>' == 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><Line1 quantity="1"/><Line2 quantity="2"/><Line3 quantity="3"/></Invoice>' == invoice.to_xml() | ||||
|  | ||||
|  | ||||
| def test_field_amount(): | ||||
|     class Line(facho.model.Model): | ||||
|         __name__ = 'Line' | ||||
|  | ||||
|         amount = fields.Amount(name='Amount', precision=1) | ||||
|         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 amount="33.0"/>' == 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 amount="23"/>' == line.to_xml() | ||||
| @@ -1,119 +0,0 @@ | ||||
| #!/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 | ||||
| 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(): | ||||
|     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' | ||||
|     invoice.customer.party.id = '800199436' | ||||
|  | ||||
|     line = invoice.lines.create() | ||||
|     line.add_tax(model.Taxes.Iva(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 | ||||
|  | ||||
|     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' | ||||
|  | ||||
| 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' | ||||
		Reference in New Issue
	
	Block a user