diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index b6d63b3..e68a175 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -1084,6 +1084,12 @@ class LIBO(object): self._set_cell('{ine.%s}' % k, v) return + def _divisas(self, data): + if data: + for k, v in data.items(): + self._set_cell(f'{{divisas.{k}}}', v) + return + def _nomina(self, data): if not data: return @@ -1310,6 +1316,8 @@ class LIBO(object): self._donataria(data['donataria']) self._ine(data['ine']) + self._divisas(data.get('divisas', {})) + self._cancelado(data['cancelada']) self._clean() return diff --git a/source/app/controllers/utils.py b/source/app/controllers/utils.py index 8332596..6ffef47 100644 --- a/source/app/controllers/utils.py +++ b/source/app/controllers/utils.py @@ -17,6 +17,7 @@ # ~ along with this program. If not, see . import base64 +import collections import math import smtplib @@ -25,16 +26,88 @@ from email.mime.base import MIMEBase from email.mime.text import MIMEText from email import encoders from email.utils import formatdate +from io import BytesIO import requests from cryptography.fernet import Fernet from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes +import lxml.etree as ET TIMEOUT = 10 +#~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37 +class CaseInsensitiveDict(collections.MutableMapping): + """ + A case-insensitive ``dict``-like object. + Implements all methods and operations of + ``collections.MutableMapping`` as well as dict's ``copy``. Also + provides ``lower_items``. + All keys are expected to be strings. The structure remembers the + case of the last key to be set, and ``iter(instance)``, + ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()`` + will contain case-sensitive keys. However, querying and contains + testing is case insensitive: + cid = CaseInsensitiveDict() + cid['Accept'] = 'application/json' + cid['aCCEPT'] == 'application/json' # True + list(cid) == ['Accept'] # True + For example, ``headers['content-encoding']`` will return the + value of a ``'Content-Encoding'`` response header, regardless + of how the header name was originally stored. + If the constructor, ``.update``, or equality comparison + operations are given keys that have equal ``.lower()``s, the + behavior is undefined. + """ + def __init__(self, data=None, **kwargs): + self._store = dict() + if data is None: + data = {} + self.update(data, **kwargs) + + def __setitem__(self, key, value): + # Use the lowercased key for lookups, but store the actual + # key alongside the value. + self._store[key.lower()] = (key, value) + + def __getitem__(self, key): + return self._store[key.lower()][1] + + def __delitem__(self, key): + del self._store[key.lower()] + + def __iter__(self): + return (casedkey for casedkey, mappedvalue in self._store.values()) + + def __len__(self): + return len(self._store) + + def lower_items(self): + """Like iteritems(), but with all lowercase keys.""" + return ( + (lowerkey, keyval[1]) + for (lowerkey, keyval) + in self._store.items() + ) + + def __eq__(self, other): + if isinstance(other, collections.Mapping): + other = CaseInsensitiveDict(other) + else: + return NotImplemented + # Compare insensitively + return dict(self.lower_items()) == dict(other.lower_items()) + + # Copy is required + def copy(self): + return CaseInsensitiveDict(self._store.values()) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, dict(self.items())) + + class SendMail(object): def __init__(self, config): @@ -77,7 +150,6 @@ class SendMail(object): if '535' in str(e): self._error = 'Nombre de usuario o contraseña inválidos' return False - # ~ print (e) if '534' in str(e) and 'gmail' in self._config['server']: self._error = 'Necesitas activar el acceso a otras ' \ 'aplicaciones en tu cuenta de GMail' @@ -128,6 +200,35 @@ class SendMail(object): return +class CfdiToDict(object): + NS = { + 'cfdi': 'http://www.sat.gob.mx/cfd/3', + 'divisas': 'http://www.sat.gob.mx/divisas', + } + + def __init__(self, xml): + self._values = {} + self._root = ET.parse(BytesIO(xml.encode())).getroot() + self._get_values() + + @property + def values(self): + return self._values + + def _get_values(self): + self._complementos() + return + + def _complementos(self): + complemento = self._root.xpath('//cfdi:Complemento', namespaces=self.NS)[0] + + divisas = complemento.xpath('//divisas:Divisas', namespaces=self.NS) + if divisas: + d = CaseInsensitiveDict(divisas[0].attrib) + d.pop('version', '') + self._values.update({'divisas': d}) + return + def send_mail(data): msg = '' ok = True @@ -170,3 +271,4 @@ def to_bool(value): def get_url(url): r = requests.get(url).text return r + diff --git a/source/app/models/main.py b/source/app/models/main.py index 082b780..069c3d3 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -3816,7 +3816,11 @@ class Facturas(BaseModel): pdf_from = Configuracion.get_('make_pdf_from') or '1' values = cls._get_not_in_xml(cls, obj, emisor) + + #Tmp to v2 data = util.get_data_from_xml(obj, values) + data.update(utils.CfdiToDict(obj.xml).values) + doc = util.to_pdf(data, emisor.rfc, pdf_from=pdf_from) if sync: diff --git a/source/templates/plantilla_factura.ods b/source/templates/plantilla_factura.ods index cab2f95..a35d18a 100644 Binary files a/source/templates/plantilla_factura.ods and b/source/templates/plantilla_factura.ods differ