diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index cd510af..b4bca22 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -20,8 +20,8 @@ import zipfile from io import BytesIO from pathlib import Path -# ~ from smtplib import SMTPException, SMTPAuthenticationError from xml.etree import ElementTree as ET +from xml.dom.minidom import parseString try: import uno @@ -1360,7 +1360,15 @@ def import_nomina(rfc): def parse_xml(xml): - return ET.fromstring(xml) + try: + return ET.fromstring(xml) + except ET.ParseError: + return None + + +def to_pretty_xml(xml): + tree = parseString(xml) + return tree.toprettyxml(encoding='utf-8').decode('utf-8') def get_dict(data): @@ -1445,8 +1453,9 @@ def _comprobante(doc, options): pass data['fechaformato'] = fecha.strftime('%A, %d de %B de %Y') - data['tipocambio'] = 'Tipo de Cambio: $ {:0.2f}'.format( - float(data['tipocambio'])) + if 'tipocambio' in data: + data['tipocambio'] = 'Tipo de Cambio: $ {:0.2f}'.format( + float(data['tipocambio'])) data['notas'] = options['notas'] return data @@ -1503,10 +1512,17 @@ def _conceptos(doc, version, options): continue if version == '3.3': - values['noidentificacion'] = '{}\n(SAT {})'.format( - values['noidentificacion'], values['ClaveProdServ']) - values['unidad'] = '({})\n{}'.format( - values['ClaveUnidad'], values['unidad']) + if 'noidentificacion' in values: + values['noidentificacion'] = '{}\n(SAT {})'.format( + values['noidentificacion'], values['ClaveProdServ']) + else: + values['noidentificacion'] = 'SAT {}'.format( + values['ClaveProdServ']) + if 'unidad' in values: + values['unidad'] = '({})\n{}'.format( + values['ClaveUnidad'], values['unidad']) + else: + values['unidad'] = '{}'.format(values['ClaveUnidad']) n = c.find('{}CuentaPredial'.format(PRE[version])) if n is not None: @@ -1559,8 +1575,8 @@ def _totales(doc, cfdi, version): for n in node.getchildren(): tmp = CaseInsensitiveDict(n.attrib.copy()) if version == '3.3': - title = 'Traslado {} {}'.format( - tn.get(tmp['impuesto']), tmp['tasaocuota']) + tasa = round(float(tmp['tasaocuota']), DECIMALES) + title = 'Traslado {} {}'.format(tn.get(tmp['impuesto']), tasa) else: title = 'Traslado {} {}'.format(tmp['impuesto'], tmp['tasa']) traslados.append((title, float(tmp['importe']))) @@ -1691,7 +1707,7 @@ def _nomina(doc, data, values, version_cfdi): def get_data_from_xml(invoice, values): data = {'cancelada': invoice.cancelada, 'donativo': False} - if hasattr('invoice', 'donativo'): + if hasattr(invoice, 'donativo'): data['donativo'] = invoice.donativo doc = parse_xml(invoice.xml) data['comprobante'] = _comprobante(doc, values) @@ -1883,14 +1899,6 @@ def upload_file(rfc, opt, file_obj): name = '{}_nomina.ods'.format(rfc.lower()) path = _join(PATH_MEDIA, 'tmp', name) - elif opt == 'cfdixml': - tmp = file_obj.filename.split('.') - ext = tmp[-1].lower() - if ext != 'xml': - msg = 'Extensión de archivo incorrecta, selecciona un archivo XML' - return {'status': 'server', 'name': msg, 'ok': False} - - return import_xml(file_obj.file.read()) if save_file(path, file_obj.file.read()): return {'status': 'server', 'name': file_obj.filename, 'ok': True} @@ -3113,6 +3121,79 @@ class ImportFacturaLibre(object): return data +class ImportCFDI(object): + + def __init__(self, xml): + self._doc = xml + self._pre = '' + + def _emisor(self): + emisor = self._doc.find('{}Emisor'.format(self._pre)) + data = CaseInsensitiveDict(emisor.attrib.copy()) + node = emisor.find('{}RegimenFiscal'.format(self._pre)) + if not node is None: + data['regimen_fiscal'] = node.attrib['Regimen'] + return data + + def _receptor(self): + node = self._doc.find('{}Receptor'.format(self._pre)) + data = CaseInsensitiveDict(node.attrib.copy()) + node = node.find('{}Domicilio'.format(self._pre)) + if not node is None: + data.update(node.attrib.copy()) + return data + + def _conceptos(self): + data = [] + conceptos = self._doc.find('{}Conceptos'.format(self._pre)) + for c in conceptos.getchildren(): + values = CaseInsensitiveDict(c.attrib.copy()) + data.append(values) + return data + + def _impuestos(self): + data = {} + node = self._doc.find('{}Impuestos'.format(self._pre)) + if not node is None: + data = CaseInsensitiveDict(node.attrib.copy()) + return data + + def _timbre(self): + node = self._doc.find('{}Complemento/{}TimbreFiscalDigital'.format( + self._pre, PRE['TIMBRE'])) + data = CaseInsensitiveDict(node.attrib.copy()) + data.pop('SelloCFD', None) + data.pop('SelloSAT', None) + data.pop('Version', None) + return data + + def get_data(self): + invoice = CaseInsensitiveDict(self._doc.attrib.copy()) + invoice.pop('certificado', '') + invoice.pop('sello', '') + self._pre = PRE[invoice['version']] + + emisor = self._emisor() + receptor = self._receptor() + conceptos = self._conceptos() + impuestos = self._impuestos() + timbre = self._timbre() + + invoice.update(emisor) + invoice.update(receptor) + invoice.update(impuestos) + invoice.update(timbre) + + data = { + 'invoice': invoice, + 'emisor': emisor, + 'receptor': receptor, + 'conceptos': conceptos, + } + + return data + + def print_ticket(data, info): p = PrintTicket(info) return p.printer(data) @@ -3176,12 +3257,3 @@ def get_log(name): return data, name -def import_xml(stream): - try: - xml = ET.fromstring(stream.decode()) - except ET.ParseError: - return {'ok': False, 'status': 'error'} - - print (xml) - return {'ok': True, 'status': 'server'} - diff --git a/source/app/models/main.py b/source/app/models/main.py index dda0428..0fb72bf 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -66,8 +66,18 @@ def desconectar(): def upload_file(rfc, opt, file_obj): + if opt == 'cfdixml': + sxml = file_obj.file.read().decode() + xml = util.parse_xml(sxml) + # ~ sxml = util.to_pretty_xml(data) + if xml is None: + return {'status': 'error'} + else: + return Facturas.import_cfdi(xml, sxml) + result = util.upload_file(rfc, opt, file_obj) if result['ok']: + names = ('bdfl', 'employees', 'nomina', 'products', 'invoiceods') if not opt in names: Configuracion.add({opt: file_obj.filename}) @@ -4029,6 +4039,121 @@ class Facturas(BaseModel): return result + def _validate_import(self, data, sxml): + try: + emisor = Emisor.select()[0] + except IndexError: + msg = 'Falta Emisor' + log.error(msg) + return False, {} + + if not DEBUG: + if emisor.rfc != data['emisor']['rfc']: + msg = 'El CFDI no es del Emisor' + log.error(msg) + return False, {} + + invoice = data['invoice'] + if invoice['version'] != '3.3': + msg = 'CFDI no es 3.3' + log.error(msg) + return False, {} + + w = (Facturas.uuid==invoice['uuid']) + if Facturas.select().where(w).exists(): + msg = 'Factura ya existe: {}'.format(invoice['uuid']) + log.error(msg) + return False, {} + + receptor = data['receptor'] + name = receptor.get('nombre', '') + tipo_persona = 1 + if receptor['rfc'] == 'XEXX010101000': + tipo_persona = 4 + elif receptor['rfc'] == 'XAXX010101000': + tipo_persona = 3 + elif len(receptor['rfc']) == 12: + tipo_persona = 2 + new = { + 'tipo_persona': tipo_persona, + 'rfc': receptor['rfc'], + 'slug': util.to_slug(name), + 'nombre': name, + 'es_cliente': True, + } + cliente, _ = Socios.get_or_create(**new) + + tipo_cambio = float(invoice.get('TipoCambio', '1.0')) + total = float(invoice['Total']) + invoice = { + 'cliente': cliente, + 'version': invoice['version'], + 'serie': invoice.get('serie', ''), + 'folio': int(invoice.get('folio', '0')), + 'fecha': invoice['fecha'], + 'fecha_timbrado': invoice['FechaTimbrado'], + 'forma_pago': invoice['FormaPago'], + 'condiciones_pago': invoice.get('CondicionesDePago', ''), + 'subtotal': float(invoice['SubTotal']), + 'descuento': float(invoice.get('Descuento', '0.0')), + 'moneda': invoice['Moneda'], + 'tipo_cambio': tipo_cambio, + 'total': total, + 'total_mn': round(float(total * tipo_cambio), DECIMALES), + 'tipo_comprobante': invoice['TipoDeComprobante'], + 'metodo_pago': invoice['MetodoPago'], + 'lugar_expedicion': invoice['LugarExpedicion'], + 'uso_cfdi': invoice['UsoCFDI'], + 'total_retenciones': float(invoice.get('TotalImpuestosRetenidos', '0.0')), + 'total_trasladados': float(invoice.get('TotalImpuestosTrasladados', '0.0')), + 'xml': sxml, + 'uuid': invoice['uuid'], + 'estatus': 'Importada', + 'regimen_fiscal': invoice['RegimenFiscal'], + 'pagada': True, + } + # ~ donativo = BooleanField(default=False) + # ~ tipo_relacion = TextField(default='') + + conceptos = [] + for concepto in data['conceptos']: + valor_unitario = float(concepto['ValorUnitario']) + descuento = float(concepto.get('Descuento', '0.0')) + c = { + 'cantidad': float(concepto['Cantidad']), + 'valor_unitario': valor_unitario, + 'descuento': descuento, + 'precio_final': round(valor_unitario - descuento, DECIMALES), + 'importe': float(concepto['Importe']), + 'descripcion': concepto['Descripcion'], + 'unidad': concepto.get('Unidad', ''), + 'clave': concepto.get('NoIdentificacion', ''), + 'clave_sat': concepto['ClaveProdServ'], + } + conceptos.append(c) + + new = { + 'invoice': invoice, + 'conceptos': conceptos, + } + return True, new + + @classmethod + def import_cfdi(cls, xml, sxml): + result = {'status': 'error'} + data = util.ImportCFDI(xml).get_data() + ok, new = cls._validate_import(cls, data, sxml) + if not ok: + return result + + with database_proxy.atomic() as txn: + obj = Facturas.create(**new['invoice']) + for product in new['conceptos']: + product['factura'] = obj + FacturasDetalle.create(**product) + + return {'status': 'server'} + class PreFacturas(BaseModel): cliente = ForeignKeyField(Socios)