From 2f79bb290a843ccfe8c472e433b3b46bb011e8cb Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Thu, 12 Oct 2017 21:47:23 -0500 Subject: [PATCH 01/22] Modificar readme --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c7b3810..802558d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ ## Mini ERP para la legislación mexicana -Este proyecto esta en continuo desarrollo, contratar un esquema de soporte, + +**En cada relación comercial, hay una relación humana** + + +Este proyecto está en continuo desarrollo, contratar un esquema de soporte, nos ayuda a continuar su desarrollo. Ponte en contacto con nosotros para contratar. From 981fdba5f43a63846c88abde3b831be22612eb75 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 15 Oct 2017 02:30:55 -0500 Subject: [PATCH 02/22] Generar PDF --- source/app/controllers/helper.py | 227 +++++++++++++++++- source/app/controllers/main.py | 3 +- source/app/controllers/util.py | 304 +++++++++++++++++++++++- source/app/models/db.py | 7 +- source/app/models/main.py | 100 +++++++- source/app/settings.py | 14 ++ source/static/js/controller/invoices.js | 11 +- source/static/js/ui/invoices.js | 8 +- 8 files changed, 655 insertions(+), 19 deletions(-) diff --git a/source/app/controllers/helper.py b/source/app/controllers/helper.py index 159a6b6..6020d66 100644 --- a/source/app/controllers/helper.py +++ b/source/app/controllers/helper.py @@ -1,12 +1,225 @@ #!/usr/bin/env python3 -import falcon -from models.main import get_cp +#~ import falcon +#~ from models.main import get_cp -class AppPostalCode(object): +#~ class AppPostalCode(object): - def on_get(self, req, resp): - values = req.params - req.context['result'] = get_cp(values['cp']) - resp.status = falcon.HTTP_200 + #~ def on_get(self, req, resp): + #~ values = req.params + #~ req.context['result'] = get_cp(values['cp']) + #~ resp.status = falcon.HTTP_200 + +#~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37 + +import re +import collections +from collections import OrderedDict + + +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 = OrderedDict() + 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 str(dict(self.items())) + + +class NumLet(object): + + def __init__(self, value, moneda, **args): + self._letras = self._letters(value, moneda) + + @property + def letras(self): + return self._letras.upper() + + #~ def _letters(self, numero, moneda='peso', texto_inicial='-(', + #~ texto_final='/100 m.n.)-', fraccion_letras=False, fraccion=''): + def _letters(self, numero, moneda='peso'): + + texto_inicial = '' + texto_final = '/100 m.n.)-' + fraccion_letras = False + fraccion = '' + + enletras = texto_inicial + numero = abs(numero) + numtmp = '%015d' % numero + + if numero < 1: + enletras += 'cero ' + self._plural(moneda) + ' ' + else: + enletras += self._numlet(numero) + if numero == 1 or numero < 2: + enletras += moneda + ' ' + elif int(''.join(numtmp[3:])) == 0 or int(''.join(numtmp[9:])) == 0: + enletras += 'de ' + self._plural(moneda) + ' ' + else: + enletras += self._plural(moneda) + ' ' + + decimal = '%0.2f' % numero + decimal = decimal.split('.')[1] + #~ decimal = int((numero-int(numero))*100) + if fraccion_letras: + if decimal == 0: + enletras += 'con cero ' + self._plural(fraccion) + elif decimal == 1: + enletras += 'con un ' + fraccion + else: + enletras += 'con ' + self._numlet(int(decimal)) + self.plural(fraccion) + else: + enletras += decimal + + enletras += texto_final + return enletras + + def _numlet(self, numero): + numtmp = '%015d' % numero + co1=0 + letras = '' + leyenda = '' + for co1 in range(0,5): + inicio = co1*3 + cen = int(numtmp[inicio:inicio+1][0]) + dec = int(numtmp[inicio+1:inicio+2][0]) + uni = int(numtmp[inicio+2:inicio+3][0]) + letra3 = self.centena(uni, dec, cen) + letra2 = self.decena(uni, dec) + letra1 = self.unidad(uni, dec) + + if co1 == 0: + if (cen+dec+uni) == 1: + leyenda = 'billon ' + elif (cen+dec+uni) > 1: + leyenda = 'billones ' + elif co1 == 1: + if (cen+dec+uni) >= 1 and int(''.join(numtmp[6:9])) == 0: + leyenda = "mil millones " + elif (cen+dec+uni) >= 1: + leyenda = "mil " + elif co1 == 2: + if (cen+dec) == 0 and uni == 1: + leyenda = 'millon ' + elif cen > 0 or dec > 0 or uni > 1: + leyenda = 'millones ' + elif co1 == 3: + if (cen+dec+uni) >= 1: + leyenda = 'mil ' + elif co1 == 4: + if (cen+dec+uni) >= 1: + leyenda = '' + + letras += letra3 + letra2 + letra1 + leyenda + letra1 = '' + letra2 = '' + letra3 = '' + leyenda = '' + return letras + + def centena(self, uni, dec, cen): + letras = '' + numeros = ["","","doscientos ","trescientos ","cuatrocientos ","quinientos ","seiscientos ","setecientos ","ochocientos ","novecientos "] + if cen == 1: + if (dec+uni) == 0: + letras = 'cien ' + else: + letras = 'ciento ' + elif cen >= 2 and cen <= 9: + letras = numeros[cen] + return letras + + def decena(self, uni, dec): + letras = '' + numeros = ["diez ","once ","doce ","trece ","catorce ","quince ","dieci","dieci","dieci","dieci"] + decenas = ["","","","treinta ","cuarenta ","cincuenta ","sesenta ","setenta ","ochenta ","noventa "] + if dec == 1: + letras = numeros[uni] + elif dec == 2: + if uni == 0: + letras = 'veinte ' + elif uni > 0: + letras = 'veinti' + elif dec >= 3 and dec <= 9: + letras = decenas[dec] + if uni > 0 and dec > 2: + letras = letras+'y ' + return letras + + def unidad(self, uni, dec): + letras = '' + numeros = ["","un ","dos ","tres ","cuatro ","cinco ","seis ","siete ","ocho ","nueve "] + if dec != 1: + if uni > 0 and uni <= 5: + letras = numeros[uni] + if uni >= 6 and uni <= 9: + letras = numeros[uni] + return letras + + def _plural(self, palabra): + if re.search('[aeiou]$', palabra): + return re.sub('$', 's', palabra) + else: + return palabra + 'es' diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py index ac55355..0c1ebfb 100644 --- a/source/app/controllers/main.py +++ b/source/app/controllers/main.py @@ -206,8 +206,9 @@ class AppDocumentos(object): #~ self._not_json = True def on_get(self, req, resp, type_doc, id_doc): + session = req.env['beaker.session'] req.context['result'], file_name, content_type = \ - self._db.get_doc(type_doc, id_doc) + self._db.get_doc(type_doc, id_doc, session['rfc']) resp.append_header('Content-Disposition', 'attachment; filename={}'.format(file_name)) resp.content_type = content_type diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 156de65..0c6c519 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -8,15 +8,22 @@ import mimetypes import os import re import sqlite3 +import socket import subprocess import tempfile +import time import unicodedata import uuid +from xml.etree import ElementTree as ET + +import uno +from com.sun.star.beans import PropertyValue from dateutil import parser +from .helper import CaseInsensitiveDict, NumLet from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT, \ - PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL + PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PRE #~ def _get_hash(password): @@ -165,6 +172,18 @@ def get_template(name, data={}): return template.render(**data) +def get_path_template(name, default='plantilla_factura.ods'): + path = _join(PATH_TEMPLATES, name) + if is_file(path): + return path + + path = _join(PATH_TEMPLATES, default) + if is_file(path): + return path + + return '' + + def dumps(data): return json.dumps(data, default=str) @@ -207,7 +226,7 @@ def to_slug(string): value = (unicodedata.normalize('NFKD', string) .encode('ascii', 'ignore') .decode('ascii').lower()) - return value + return value.replace(' ', '_') class Certificado(object): @@ -399,3 +418,284 @@ def timbra_xml(xml): result['uuid'] = pac.uuid result['fecha'] = pac.fecha return result + + +class LIBO(object): + HOST = 'localhost' + PORT = '8100' + ARG = 'socket,host={},port={};urp;StarOffice.ComponentContext'.format( + HOST, PORT) + + def __init__(self): + self._app = None + self._start_office() + self._init_values() + + def _init_values(self): + self._ctx = None + self._sm = None + self._desktop = None + if self.is_running: + ctx = uno.getComponentContext() + service = 'com.sun.star.bridge.UnoUrlResolver' + resolver = ctx.ServiceManager.createInstanceWithContext(service, ctx) + self._ctx = resolver.resolve('uno:{}'.format(self.ARG)) + self._sm = self._ctx.ServiceManager + self._desktop = self._create_instance('com.sun.star.frame.Desktop') + return + + def _create_instance(self, name, with_context=True): + if with_context: + instance = self._sm.createInstanceWithContext(name, self._ctx) + else: + instance = self._sm.createInstance(name) + return instance + + @property + def is_running(self): + try: + s = socket.create_connection((self.HOST, self.PORT), 5.0) + s.close() + return True + except ConnectionRefusedError: + return False + + def _start_office(self): + if self.is_running: + return + + c = 1 + while c < 4: + c += 1 + self.app = subprocess.Popen([ + 'soffice', '--headless', '--accept={}'.format(self.ARG)], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + time.sleep(5) + if self.is_running: + return + return + + def _set_properties(self, properties): + pl = [] + for k, v in properties.items(): + pv = PropertyValue() + pv.Name = k + pv.Value = v + pl.append(pv) + return tuple(pl) + + def _doc_open(self, path, options): + options = self._set_properties(options) + path = self._path_url(path) + try: + doc = self._desktop.loadComponentFromURL(path, '_blank', 0, options) + return doc + except: + return None + + def _path_url(self, path): + if path.startswith('file://'): + return path + return uno.systemPathToFileUrl(path) + + def close(self): + if self.is_running: + if not self._desktop is None: + self._desktop.terminate() + if not self._app is None: + self._app.terminate() + return + + def _read(self, path): + try: + return open(path, 'rb').read() + except: + return b'' + + def _clean(self): + self._sd.SearchRegularExpression = True + self._sd.setSearchString("\{(\w.+)\}") + self._search.replaceAll(self._sd) + return + + def _cancelado(self, cancel): + if not cancel: + pd = self._sheet.getDrawPage() + if pd.getCount(): + pd.remove(pd.getByIndex(0)) + return + + def _set_search(self): + self._sheet = self._template.getSheets().getByIndex(0) + self._search = self._sheet.getPrintAreas()[0] + self._search = self._sheet.getCellRangeByPosition( + self._search.StartColumn, + self._search.StartRow, + self._search.EndColumn, + self._search.EndRow + ) + self._sd = self._sheet.createSearchDescriptor() + self._sd.SearchCaseSensitive = False + return + + def _set_cell(self, k='', v=None, cell=None, value=False): + if k: + self._sd.setSearchString(k) + ranges = self._search.findAll(self._sd) + if ranges: + ranges = ranges.getRangeAddressesAsString().split(';') + for r in ranges: + for c in r.split(','): + cell = self._sheet.getCellRangeByName(c) + if v is None: + return cell + if cell.getImplementationName() == 'ScCellObj': + pattern = re.compile(k, re.IGNORECASE) + nv = pattern.sub(v, cell.getString()) + if value: + cell.setValue(nv) + else: + cell.setString(nv) + return cell + if cell: + if cell.getImplementationName() == 'ScCellObj': + ca = cell.getCellAddress() + new_cell = self._sheet.getCellByPosition(ca.Column, ca.Row + 1) + if value: + new_cell.setValue(v) + else: + new_cell.setString(v) + return new_cell + + def _comprobante(self, data): + for k, v in data.items(): + if k in ('total', 'descuento', 'subtotal'): + self._set_cell('{cfdi.%s}' % k, v, value=True) + else: + self._set_cell('{cfdi.%s}' % k, v) + return + + def _timbre(self, data): + for k, v in data.items(): + self._set_cell('{timbre.%s}' % k, v) + #~ pd = self._sheet.getDrawPage() + #~ image = self._template.createInstance('com.sun.star.drawing.GraphicObjectShape') + #~ image.GraphicURL = data['path_cbb'] + #~ pd.add(image) + #~ s = Size() + #~ s.Width = 4500 + #~ s.Height = 4500 + #~ image.setSize(s) + #~ image.Anchor = self._set_cell('{timbre.cbb}') + return + + + def _render(self, data): + self._set_search() + self._comprobante(data['comprobante']) + #~ self._emisor(data['emisor']) + #~ self._receptor(data['receptor']) + #~ self._conceptos(data['conceptos']) + self._timbre(data['timbre']) + self._cancelado(False) + #~ self._clean() + return + + def pdf(self, path, data): + options = {'AsTemplate': True, 'Hidden': True} + self._template = self._doc_open(path, options) + if self._template is None: + return b'' + + self._render(data) + + options = {'FilterName': 'calc_pdf_Export'} + path = tempfile.mkstemp()[1] + self._template.storeToURL(self._path_url(path), self._set_properties(options)) + self._template.close(True) + + return self._read(path) + + +def to_pdf(path, data): + app = LIBO() + + if not app.is_running: + return b'' + + return app.pdf(path, data) + + +def parse_xml(xml): + return ET.fromstring(xml) + + +def get_dict(data): + return CaseInsensitiveDict(data) + + +def to_letters(value, moneda): + monedas = { + 'MXN': 'peso', + } + return NumLet(value, monedas[moneda]).letras + + +def get_qr(data): + scale = 10 + path = tempfile.mkstemp()[1] + code = QRCode(data, mode='binary') + code.png(path, scale) + return path + + +def _comprobante(values): + data = CaseInsensitiveDict(values) + del data['certificado'] + #~ print (data) + data['totalenletras'] = to_letters(float(data['total']), data['moneda']) + if data['version'] == '3.3': + tipos = { + 'I': 'ingreso', + 'E': 'egreso', + 'T': 'traslado', + } + data['tipodecomprobante'] = tipos.get(data['tipodecomprobante']) + data['lugarexpedicion'] = 'C.P. Expedición: {}'.format(data['lugarexpedicion']) + + return data + + +def _timbre(doc, version, values): + CADENA = '||{version}|{UUID}|{FechaTimbrado}|{selloCFD}|{noCertificadoSAT}||' + if version == '3.3': + CADENA = '||{Version}|{UUID}|{FechaTimbrado}|{SelloCFD}|{NoCertificadoSAT}||' + node = doc.find('{}Complemento/{}TimbreFiscalDigital'.format( + PRE[version], PRE['TIMBRE'])) + data = CaseInsensitiveDict(node.attrib.copy()) + total_s = '%017.06f' % float(values['total']) + qr_data = '?re=%s&rr=%s&tt=%s&id=%s' % ( + values['rfc_emisor'], + values['rfc_receptor'], + total_s, + node.attrib['UUID']) + #~ data['path_cbb'] = get_qr(qr_data) + data['cadenaoriginal'] = CADENA.format(**node.attrib) + return data + + +def get_data(invoice, rfc): + name = '{}_factura.ods'.format(rfc.lower()) + path = get_path_template(name) + + values = {} + data = {'cancelada': invoice.cancelada} + doc = parse_xml(invoice.xml) + data['comprobante'] = _comprobante(doc.attrib.copy()) + version = data['comprobante']['version'] + values['rfc_emisor'] = '123' + values['rfc_receptor'] = '456' + values['total'] = data['comprobante']['total'] + data['timbre'] = _timbre(doc, version, values) + + return path, data diff --git a/source/app/models/db.py b/source/app/models/db.py index fe07363..406880f 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -115,9 +115,12 @@ class StorageEngine(object): def add_folios(self, values): return main.Folios.add(values) - def get_doc(self, type_doc, id): + def get_doc(self, type_doc, id, rfc): if type_doc == 'xml': data, file_name = main.Facturas.get_xml(id) - content_type = 'application.xml' + content_type = 'application/xml' + if type_doc == 'pdf': + data, file_name = main.Facturas.get_pdf(id, rfc) + content_type = 'application/pdf' return data, file_name, content_type diff --git a/source/app/models/main.py b/source/app/models/main.py index 586b8c4..0e05d34 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -14,7 +14,7 @@ if __name__ == '__main__': from controllers import util -from settings import log, VERSION, PATH_CP, COMPANIES +from settings import log, VERSION, PATH_CP, COMPANIES, PRE FORMAT = '{0:.2f}' @@ -928,6 +928,7 @@ class Facturas(BaseModel): regimen_fiscal = TextField(default='') notas = TextField(default='') pagada = BooleanField(default=False) + cancelada = BooleanField(default=False) error = TextField(default='') class Meta: @@ -939,6 +940,103 @@ class Facturas(BaseModel): name = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.cliente.rfc) return obj.xml, name + def _get_data_cfdi_to_pdf(self, xml, cancel, version): + pre_nomina = PRE['NOMINA'][version] + + data['comprobante']['letters'] = NumerosLetras().letters( + float(data['comprobante']['total'])).upper() + + data['year'] = data['comprobante']['fecha'][0:4] + data['month'] = data['comprobante']['fecha'][5:7] + + node = doc.find('{}Emisor'.format(pre)) + data['emisor'] = node.attrib.copy() + rfc_emisor = data['emisor']['rfc'] + node = node.find('{}DomicilioFiscal'.format(pre)) + if not node is None: + data['emisor'].update(node.attrib.copy()) + + node = doc.find('{}Receptor'.format(pre)) + data['receptor'] = node.attrib.copy() + rfc_receptor = data['receptor']['rfc'] + node = node.find('{}Domicilio'.format(pre)) + if not node is None: + data['receptor'].update(node.attrib.copy()) + + data['conceptos'] = [] + conceptos = doc.find('{}Conceptos'.format(pre)) + for c in conceptos.getchildren(): + data['conceptos'].append(c.attrib.copy()) + + node = doc.find('{}Complemento/{}TimbreFiscalDigital'.format(pre, PRE['TIMBRE'])) + data['timbre'] = node.attrib.copy() + total_s = '%017.06f' % float(doc.attrib['total']) + qr_data = '?re=%s&rr=%s&tt=%s&id=%s' % ( + rfc_emisor, rfc_receptor, total_s, node.attrib['UUID']) + data['timbre']['path_cbb'] = get_qr(node.attrib['UUID'], qr_data) + data['timbre']['cadenaoriginal'] = CADENA.format(**node.attrib) + + data['nomina'] = {} + node = doc.find('{}Complemento/{}Nomina'.format(pre, pre_nomina)) + if not node is None: + data['nomina']['nomina'] = node.attrib.copy() + subnode = node.find('{}Emisor'.format(pre_nomina)) + if not subnode is None: + data['emisor'].update(subnode.attrib.copy()) + subnode = node.find('{}Receptor'.format(pre_nomina)) + data['receptor'].update(subnode.attrib.copy()) + + subnode = node.find('{}Percepciones'.format(pre_nomina)) + data['nomina']['percepciones'] = subnode.attrib.copy() + detalle = [] + for n in subnode.getchildren(): + if 'SeparacionIndemnizacion' in n.tag: + continue + detalle.append(n.attrib.copy()) + data['nomina']['percepciones']['detalle'] = detalle + + data['nomina']['deducciones'] = None + subnode = node.find('{}Deducciones'.format(pre_nomina)) + if not subnode is None: + data['nomina']['deducciones'] = subnode.attrib.copy() + detalle = [] + for n in subnode.getchildren(): + detalle.append(n.attrib.copy()) + data['nomina']['deducciones']['detalle'] = detalle + + data['nomina']['incapacidades'] = None + subnode = node.find('{}Incapacidades'.format(pre_nomina)) + if not subnode is None: + detalle = [] + for n in subnode.getchildren(): + detalle.append(n.attrib.copy()) + data['nomina']['incapacidades'] = detalle + + data['nomina']['otrospagos'] = None + subnode = node.find('{}OtrosPagos'.format(pre_nomina)) + if not subnode is None: + data['nomina']['otrospagos'] = subnode.attrib.copy() + detalle = [] + for n in subnode.getchildren(): + detalle.append(n.attrib.copy()) + ns = n.find('{}SubsidioAlEmpleo'.format(pre_nomina)) + if not ns is None: + data['nomina']['otrospagos']['SubsidioCausado'] = ns.attrib['SubsidioCausado'] + data['nomina']['otrospagos']['detalle'] = detalle + + return data + + @classmethod + def get_pdf(cls, id, rfc): + obj = Facturas.get(Facturas.id==id) + name = '{}{}_{}.pdf'.format(obj.serie, obj.folio, obj.cliente.rfc) + if obj.uuid is None: + return b'', name + + path, data = util.get_data(obj, rfc) + doc = util.to_pdf(path, data) + return doc, name + @classmethod def get_(cls, values): rows = tuple(Facturas diff --git a/source/app/settings.py b/source/app/settings.py index 88b87ff..60c08b0 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -59,3 +59,17 @@ PATH_OPENSSL = 'openssl' if 'win' in sys.platform: PATH_XSLTPROC = os.path.join(PATH_BIN, 'xsltproc.exe') PATH_OPENSSL = os.path.join(PATH_BIN, 'openssl.exe') + + +PRE = { + '2.0': '{http://www.sat.gob.mx/cfd/2}', + '2.2': '{http://www.sat.gob.mx/cfd/2}', + '3.0': '{http://www.sat.gob.mx/cfd/3}', + '3.2': '{http://www.sat.gob.mx/cfd/3}', + '3.3': '{http://www.sat.gob.mx/cfd/3}', + 'TIMBRE': '{http://www.sat.gob.mx/TimbreFiscalDigital}', + 'NOMINA': { + '1.1': '{http://www.sat.gob.mx/nomina}', + '1.2': '{http://www.sat.gob.mx/nomina12}', + } +} diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index eaf553d..af77be7 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -11,8 +11,8 @@ function get_series(){ pre = values[0] $$('lst_serie').getList().parse(values) $$('lst_serie').setValue(pre.id) - if(pre.usar_con){ - $$('lst_tipo_comprobante').setValue(pre.usar_con) + if(pre.usarcon){ + $$('lst_tipo_comprobante').setValue(pre.usarcon) $$('lst_tipo_comprobante').config.readonly = true $$('lst_tipo_comprobante').refresh() } @@ -644,8 +644,15 @@ function cmd_invoice_timbrar_click(){ function grid_invoices_click(id, e, node){ var row = this.getItem(id) + if(id.column == 'xml'){ location = '/doc/xml/' + row.id + }else if(id.column == 'pdf'){ + location = '/doc/pdf/' + row.id + }else if(id.column == 'zip'){ + location = '/doc/zip/' + row.id + }else if(id.column == 'email'){ + show('Correo') } } diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index 950baf1..1bd8b8a 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -142,10 +142,10 @@ var suggest_products = { header: true, columns: [ {id: 'id', hidden: true}, - {id: 'clave', adjust: 'data'}, - {id: 'descripcion', adjust: 'data'}, - {id: 'unidad', adjust: 'data'}, - {id: 'valor_unitario', adjust: 'data', + {id: 'clave', header: 'Clave', adjust: 'data'}, + {id: 'descripcion', header: 'Descripción', adjust: 'data'}, + {id: 'unidad', header: 'Unidad', adjust: 'data'}, + {id: 'valor_unitario', header: 'Valor Unitario', adjust: 'data', format: webix.i18n.priceFormat} ], dataFeed:function(text){ From 09e3b46eca16b5a20a1c3a7c3e6ab81fc87f229c Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 15 Oct 2017 17:20:20 -0500 Subject: [PATCH 03/22] Generar PDF --- requirements.txt | 1 + source/app/controllers/util.py | 272 +++++++++++++++++++++++++++++---- source/app/models/main.py | 71 ++++----- 3 files changed, 281 insertions(+), 63 deletions(-) diff --git a/requirements.txt b/requirements.txt index 759159c..8d5e242 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ bcrypt python-dateutil zeep chardet +pyqrcode diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 0c6c519..20851e8 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -18,7 +18,9 @@ from xml.etree import ElementTree as ET import uno from com.sun.star.beans import PropertyValue +from com.sun.star.awt import Size +import pyqrcode from dateutil import parser from .helper import CaseInsensitiveDict, NumLet @@ -538,6 +540,16 @@ class LIBO(object): self._sd.SearchCaseSensitive = False return + def _next_cell(self, cell): + col = cell.getCellAddress().Column + row = cell.getCellAddress().Row + 1 + return self._sheet.getCellByPosition(col, row) + + def _copy_cell(self, cell): + destino = self._next_cell(cell) + self._sheet.copyRange(destino.getCellAddress(), cell.getRangeAddress()) + return destino + def _set_cell(self, k='', v=None, cell=None, value=False): if k: self._sd.setSearchString(k) @@ -575,30 +587,111 @@ class LIBO(object): self._set_cell('{cfdi.%s}' % k, v) return + def _emisor(self, data): + for k, v in data.items(): + self._set_cell('{emisor.%s}' % k, v) + return + + def _receptor(self, data): + for k, v in data.items(): + self._set_cell('{receptor.%s}' % k, v) + return + + def _conceptos(self, data): + first = True + for concepto in data: + key = concepto.get('noidentificacion', '') + description = concepto['descripcion'] + unidad = concepto['unidad'] + cantidad = concepto['cantidad'] + valor_unitario = concepto['valorunitario'] + importe = concepto['importe'] + if first: + first = False + cell_1 = self._set_cell('{noidentificacion}', key) + cell_2 = self._set_cell('{descripcion}', description) + cell_3 = self._set_cell('{unidad}', unidad) + cell_4 = self._set_cell('{cantidad}', cantidad, value=True) + cell_5 = self._set_cell('{valorunitario}', valor_unitario, value=True) + cell_6 = self._set_cell('{importe}', importe, value=True) + return + + def _totales(self, data): + currency = data['moneda'] + + cell_title = self._set_cell('{subtotal.titulo}', 'SubTotal') + value = data['subtotal'] + cell_value = self._set_cell('{subtotal}', value, value=True) + cell_value.CellStyle = currency + + #~ Si encuentra el campo {total}, se asume que los totales e impuestos + #~ están declarados de forma independiente cada uno + #~ if self._add_totales(xml): + #~ return + + #~ Si no se encuentra, copia las celdas hacia abajo de + #~ {subtotal.titulo} y {subtotal} + if 'descuento' in data: + self._copy_cell(cell_title) + self._copy_cell(cell_value) + cell_title = self._set_cell(v='Descuento', cell=cell_title) + value = data['descuento'] + cell_value = self._set_cell(v=value, cell=cell_value, value=True) + cell_value.CellStyle = currency + + for tax in data['traslados']: + self._copy_cell(cell_title) + self._copy_cell(cell_value) + cell_title = self._set_cell(v=tax[0], cell=cell_title) + cell_value = self._set_cell(v=tax[1], cell=cell_value, value=True) + cell_value.CellStyle = currency + + for tax in data['retenciones']: + self._copy_cell(cell_title) + self._copy_cell(cell_value) + cell_title = self._set_cell(v=tax[0], cell=cell_title) + cell_value = self._set_cell(v=tax[1], cell=cell_value, value=True) + cell_value.CellStyle = currency + + for tax in data['taxlocales']: + self._copy_cell(cell_title) + self._copy_cell(cell_value) + cell_title = self._set_cell(v=tax[0], cell=cell_title) + cell_value = self._set_cell(v=tax[1], cell=cell_value, value=True) + cell_value.CellStyle = currency + + self._copy_cell(cell_title) + self._copy_cell(cell_value) + cell_title = self._set_cell(v='Total', cell=cell_title) + value = data['total'] + cell_value = self._set_cell(v=value, cell=cell_value, value=True) + cell_value.CellStyle = currency + return + def _timbre(self, data): for k, v in data.items(): self._set_cell('{timbre.%s}' % k, v) - #~ pd = self._sheet.getDrawPage() - #~ image = self._template.createInstance('com.sun.star.drawing.GraphicObjectShape') - #~ image.GraphicURL = data['path_cbb'] - #~ pd.add(image) - #~ s = Size() - #~ s.Width = 4500 - #~ s.Height = 4500 - #~ image.setSize(s) - #~ image.Anchor = self._set_cell('{timbre.cbb}') + pd = self._sheet.getDrawPage() + image = self._template.createInstance('com.sun.star.drawing.GraphicObjectShape') + image.GraphicURL = data['path_cbb'] + pd.add(image) + s = Size() + s.Width = 4250 + s.Height = 4500 + image.setSize(s) + image.Anchor = self._set_cell('{timbre.cbb}') return - def _render(self, data): self._set_search() self._comprobante(data['comprobante']) - #~ self._emisor(data['emisor']) - #~ self._receptor(data['receptor']) - #~ self._conceptos(data['conceptos']) + self._emisor(data['emisor']) + self._receptor(data['receptor']) + self._conceptos(data['conceptos']) + self._totales(data['totales']) self._timbre(data['timbre']) - self._cancelado(False) - #~ self._clean() + self._cancelado(data['cancelada']) + self._clean() return def pdf(self, path, data): @@ -609,9 +702,14 @@ class LIBO(object): self._render(data) + path = '{}.ods'.format(tempfile.mkstemp()[1]) + self._template.storeToURL(self._path_url(path), ()) + doc = self._doc_open(path, {'Hidden': True}) + options = {'FilterName': 'calc_pdf_Export'} path = tempfile.mkstemp()[1] - self._template.storeToURL(self._path_url(path), self._set_properties(options)) + doc.storeToURL(self._path_url(path), self._set_properties(options)) + doc.close(True) self._template.close(True) return self._read(path) @@ -637,22 +735,23 @@ def get_dict(data): def to_letters(value, moneda): monedas = { 'MXN': 'peso', + 'USD': 'dólar', + 'EUR': 'euro', } return NumLet(value, monedas[moneda]).letras def get_qr(data): - scale = 10 path = tempfile.mkstemp()[1] - code = QRCode(data, mode='binary') - code.png(path, scale) + qr = pyqrcode.create(data, mode='binary') + qr.png(path, scale=7) return path -def _comprobante(values): +def _comprobante(values, options): data = CaseInsensitiveDict(values) del data['certificado'] - #~ print (data) + data['totalenletras'] = to_letters(float(data['total']), data['moneda']) if data['version'] == '3.3': tipos = { @@ -662,7 +761,116 @@ def _comprobante(values): } data['tipodecomprobante'] = tipos.get(data['tipodecomprobante']) data['lugarexpedicion'] = 'C.P. Expedición: {}'.format(data['lugarexpedicion']) + data['metododepago'] = options['metododepago'] + data['formadepago'] = options['formadepago'] + data['moneda'] = options['moneda'] + data['tipocambio'] = 'Tipo de Cambio: $ {:0.2f}'.format( + float(data['tipocambio'])) + + return data + + +def _emisor(doc, version, values): + node = doc.find('{}Emisor'.format(PRE[version])) + data = CaseInsensitiveDict(node.attrib.copy()) + node = node.find('{}DomicilioFiscal'.format(PRE[version])) + if not node is None: + data.update(node.attrib.copy()) + data['regimenfiscal'] = values['regimenfiscal'] + return data + + +def _receptor(doc, version, values): + node = doc.find('{}Receptor'.format(PRE[version])) + data = CaseInsensitiveDict(node.attrib.copy()) + node = node.find('{}Domicilio'.format(PRE[version])) + if not node is None: + data.update(node.attrib.copy()) + data['usocfdi'] = values['usocfdi'] + return data + + +def _conceptos(doc, version): + data = [] + conceptos = doc.find('{}Conceptos'.format(PRE[version])) + for c in conceptos.getchildren(): + values = CaseInsensitiveDict(c.attrib.copy()) + if version == '3.3': + values['noidentificacion'] = '{}\n(SAT {})'.format( + values['noidentificacion'], values['ClaveProdServ']) + values['unidad'] = '({}) {}'.format( + values['ClaveUnidad'], values['unidad']) + data.append(values) + return data + + +def _totales(doc, cfdi, version): + data = {} + data['moneda'] = doc.attrib['Moneda'] + data['subtotal'] = cfdi['subtotal'] + if 'descuento' in cfdi: + data['descuento'] = cfdi['descuento'] + data['total'] = cfdi['total'] + + tn = { + '001': 'ISR', + '002': 'IVA', + } + traslados = [] + retenciones = [] + taxlocales = [] + + imp = doc.find('{}Impuestos'.format(PRE[version])) + if imp is not None: + tmp = CaseInsensitiveDict(imp.attrib.copy()) + for k, v in tmp.items(): + data[k] = v + + node = imp.find('{}Traslados'.format(PRE[version])) + if node is not None: + for n in node.getchildren(): + tmp = CaseInsensitiveDict(n.attrib.copy()) + if version == '3.3': + title = 'Traslado {} {}'.format( + tn.get(tmp['impuesto']), tmp['tasaocuota']) + else: + title = 'Traslado {} {}'.format(tmp['impuesto'], tmp['tasa']) + traslados.append((title, float(tmp['importe']))) + + node = imp.find('{}Retenciones'.format(PRE[version])) + if node is not None: + for n in node.getchildren(): + tmp = CaseInsensitiveDict(n.attrib.copy()) + if version == '3.3': + title = 'Retención {} {}'.format( + tn.get(tmp['impuesto']), '') + else: + title = 'Retención {} {}'.format(tmp['impuesto'], '') + retenciones.append((title, float(tmp['importe']))) + + #~ com = xml.find('%sComplemento' % PRE) + #~ if com is not None: + #~ otros = com.find('%sImpuestosLocales' % IMP_LOCAL) + #~ if otros is not None: + #~ for otro in list(otros): + #~ if otro.tag == '%sRetencionesLocales' % IMP_LOCAL: + #~ name = 'ImpLocRetenido' + #~ tasa = 'TasadeRetencion' + #~ else: + #~ name = 'ImpLocTrasladado' + #~ tasa = 'TasadeTraslado' + #~ title = '%s %s %%' % (otro.attrib[name], otro.attrib[tasa]) + #~ value = otro.attrib['Importe'] + #~ self._copy_cell(cell_title) + #~ self._copy_cell(cell_value) + #~ cell_title = self._set_cell(v=title, cell=cell_title) + #~ cell_value = self._set_cell(v=value, cell=cell_value, value=True) + #~ cell_value.CellStyle = currency + + data['traslados'] = traslados + data['retenciones'] = retenciones + data['taxlocales'] = taxlocales return data @@ -679,23 +887,29 @@ def _timbre(doc, version, values): values['rfc_receptor'], total_s, node.attrib['UUID']) - #~ data['path_cbb'] = get_qr(qr_data) + data['path_cbb'] = get_qr(qr_data) data['cadenaoriginal'] = CADENA.format(**node.attrib) return data -def get_data(invoice, rfc): +def get_data(invoice, rfc, values): name = '{}_factura.ods'.format(rfc.lower()) path = get_path_template(name) - values = {} data = {'cancelada': invoice.cancelada} doc = parse_xml(invoice.xml) - data['comprobante'] = _comprobante(doc.attrib.copy()) + data['comprobante'] = _comprobante(doc.attrib.copy(), values) version = data['comprobante']['version'] - values['rfc_emisor'] = '123' - values['rfc_receptor'] = '456' - values['total'] = data['comprobante']['total'] - data['timbre'] = _timbre(doc, version, values) + data['emisor'] = _emisor(doc, version, values) + data['receptor'] = _receptor(doc, version, values) + data['conceptos'] = _conceptos(doc, version) + data['totales'] = _totales(doc, data['comprobante'], version) + + options = { + 'rfc_emisor': data['emisor']['rfc'], + 'rfc_receptor': data['receptor']['rfc'], + 'total': data['comprobante']['total'], + } + data['timbre'] = _timbre(doc, version, options) return path, data diff --git a/source/app/models/main.py b/source/app/models/main.py index 0e05d34..bddb1d4 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -110,6 +110,9 @@ class SATRegimenes(BaseModel): (('key', 'name'), True), ) + def __str__(self): + return '({}) {}'.format(self.key, self.name) + @classmethod def get_(cls, ids): if isinstance(ids, int): @@ -458,6 +461,9 @@ class SATFormaPago(BaseModel): (('key', 'name'), True), ) + def __str__(self): + return 'Forma de pago: ({}) {}'.format(self.key, self.name) + @classmethod def get_activos(cls, values): field = SATFormaPago.id @@ -496,6 +502,9 @@ class SATMonedas(BaseModel): (('key', 'name'), True), ) + def __str__(self): + return 'Moneda: ({}) {}'.format(self.key, self.name) + @classmethod def get_activos(cls): rows = (SATMonedas @@ -543,6 +552,9 @@ class SATUsoCfdi(BaseModel): (('key', 'name'), True), ) + def __str__(self): + return 'Uso del CFDI: ({}) {}'.format(self.key, self.name) + @classmethod def get_activos(cls, values): field = SATUsoCfdi.id @@ -943,39 +955,6 @@ class Facturas(BaseModel): def _get_data_cfdi_to_pdf(self, xml, cancel, version): pre_nomina = PRE['NOMINA'][version] - data['comprobante']['letters'] = NumerosLetras().letters( - float(data['comprobante']['total'])).upper() - - data['year'] = data['comprobante']['fecha'][0:4] - data['month'] = data['comprobante']['fecha'][5:7] - - node = doc.find('{}Emisor'.format(pre)) - data['emisor'] = node.attrib.copy() - rfc_emisor = data['emisor']['rfc'] - node = node.find('{}DomicilioFiscal'.format(pre)) - if not node is None: - data['emisor'].update(node.attrib.copy()) - - node = doc.find('{}Receptor'.format(pre)) - data['receptor'] = node.attrib.copy() - rfc_receptor = data['receptor']['rfc'] - node = node.find('{}Domicilio'.format(pre)) - if not node is None: - data['receptor'].update(node.attrib.copy()) - - data['conceptos'] = [] - conceptos = doc.find('{}Conceptos'.format(pre)) - for c in conceptos.getchildren(): - data['conceptos'].append(c.attrib.copy()) - - node = doc.find('{}Complemento/{}TimbreFiscalDigital'.format(pre, PRE['TIMBRE'])) - data['timbre'] = node.attrib.copy() - total_s = '%017.06f' % float(doc.attrib['total']) - qr_data = '?re=%s&rr=%s&tt=%s&id=%s' % ( - rfc_emisor, rfc_receptor, total_s, node.attrib['UUID']) - data['timbre']['path_cbb'] = get_qr(node.attrib['UUID'], qr_data) - data['timbre']['cadenaoriginal'] = CADENA.format(**node.attrib) - data['nomina'] = {} node = doc.find('{}Complemento/{}Nomina'.format(pre, pre_nomina)) if not node is None: @@ -1026,6 +1005,29 @@ class Facturas(BaseModel): return data + def _get_not_in_xml(self, invoice): + values = {} + obj = SATRegimenes.get(SATRegimenes.key==invoice.regimen_fiscal) + values['regimenfiscal'] = str(obj) + + obj = SATUsoCfdi.get(SATUsoCfdi.key==invoice.uso_cfdi) + values['usocfdi'] = str(obj) + + mp = { + 'PUE': 'Pago en una sola exhibición', + 'PPD': 'Pago en parcialidades o diferido', + } + values['metododepago'] = 'Método de Pago: ({}) {}'.format( + invoice.metodo_pago, mp[invoice.metodo_pago]) + + obj = SATFormaPago.get(SATFormaPago.key==invoice.forma_pago) + values['formadepago'] = str(obj) + + obj = SATMonedas.get(SATMonedas.key==invoice.moneda) + values['moneda'] = str(obj) + + return values + @classmethod def get_pdf(cls, id, rfc): obj = Facturas.get(Facturas.id==id) @@ -1033,7 +1035,8 @@ class Facturas(BaseModel): if obj.uuid is None: return b'', name - path, data = util.get_data(obj, rfc) + values = cls._get_not_in_xml(cls, obj) + path, data = util.get_data(obj, rfc, values) doc = util.to_pdf(path, data) return doc, name From 28bb6a74d145fe6d5c91006a7853e0f9fd841f2e Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 15 Oct 2017 18:57:25 -0500 Subject: [PATCH 04/22] Generar ZIP --- source/app/controllers/util.py | 13 +++++++++++++ source/app/models/db.py | 3 +++ source/app/models/main.py | 19 +++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 20851e8..e673eb9 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -14,6 +14,8 @@ import tempfile import time import unicodedata import uuid +import zipfile +from io import BytesIO from xml.etree import ElementTree as ET import uno @@ -913,3 +915,14 @@ def get_data(invoice, rfc, values): data['timbre'] = _timbre(doc, version, options) return path, data + + +def to_zip(*files): + zip_buffer = BytesIO() + + with zipfile.ZipFile(zip_buffer, 'a', zipfile.ZIP_DEFLATED, False) as zip_file: + for data, file_name in files: + zip_file.writestr(file_name, data) + + return zip_buffer.getvalue() + diff --git a/source/app/models/db.py b/source/app/models/db.py index 406880f..7488143 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -122,5 +122,8 @@ class StorageEngine(object): if type_doc == 'pdf': data, file_name = main.Facturas.get_pdf(id, rfc) content_type = 'application/pdf' + if type_doc == 'zip': + data, file_name = main.Facturas.get_zip(id, rfc) + content_type = 'application/octet-stream' return data, file_name, content_type diff --git a/source/app/models/main.py b/source/app/models/main.py index bddb1d4..5d69c8c 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -1040,6 +1040,25 @@ class Facturas(BaseModel): doc = util.to_pdf(path, data) return doc, name + @classmethod + def get_zip(cls, id, rfc): + obj = Facturas.get(Facturas.id==id) + name_zip = '{}{}_{}.zip'.format(obj.serie, obj.folio, obj.cliente.rfc) + if obj.uuid is None: + return b'', name_zip + + file_xml = cls.get_xml(id) + if not file_xml[0]: + return b'', name_zip + + file_pdf = cls.get_pdf(id, rfc) + if not file_pdf[0]: + return b'', name_zip + + file_zip = util.to_zip(file_xml, file_pdf) + + return file_zip, name_zip + @classmethod def get_(cls, values): rows = tuple(Facturas From 8ff857ec73d637b44457f536a5edc80871e2a61e Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 16 Oct 2017 00:02:51 -0500 Subject: [PATCH 05/22] Enviar correo --- source/app/controllers/helper.py | 84 ++++++++++++++ source/app/controllers/main.py | 23 +++- source/app/controllers/util.py | 34 +++++- source/app/main.py | 3 +- source/app/models/db.py | 12 ++ source/app/models/main.py | 90 ++++++++++++++- source/static/js/controller/admin.js | 141 +++++++++++++++++++++++- source/static/js/controller/invoices.js | 37 ++++++- source/static/js/ui/admin.js | 92 ++++++++++++++++ 9 files changed, 508 insertions(+), 8 deletions(-) diff --git a/source/app/controllers/helper.py b/source/app/controllers/helper.py index 6020d66..32fe513 100644 --- a/source/app/controllers/helper.py +++ b/source/app/controllers/helper.py @@ -14,8 +14,15 @@ #~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37 import re +import smtplib import collections + from collections import OrderedDict +from email.mime.multipart import MIMEMultipart +from email.mime.base import MIMEBase +from email.mime.text import MIMEText +from email import encoders +from email.utils import formatdate class CaseInsensitiveDict(collections.MutableMapping): @@ -223,3 +230,80 @@ class NumLet(object): return re.sub('$', 's', palabra) else: return palabra + 'es' + + +class SendMail(object): + + def __init__(self, config): + self._config = config + self._server = None + self._error = '' + self._is_connect = self._login() + + @property + def is_connect(self): + return self._is_connect + + @property + def error(self): + return self._error + + def _login(self): + try: + if self._config['ssl']: + self._server = smtplib.SMTP_SSL( + self._config['servidor'], + self._config['puerto'], timeout=10) + else: + self._server = smtplib.SMTP( + self._config['servidor'], + self._config['puerto'], timeout=10) + self._server.login(self._config['usuario'], self._config['contra']) + return True + except smtplib.SMTPAuthenticationError as e: + if '535' in str(e): + self._error = 'Nombre de usuario o contraseña inválidos' + return False + if '534' in str(e) and 'gmail' in self._config['servidor']: + self._error = 'Necesitas activar el acceso a otras ' \ + 'aplicaciones en tu cuenta de GMail' + return False + except smtplib.SMTPException as e: + self._error = str(e) + return False + except Exception as e: + self._error = str(e) + return False + return + + def send(self, options): + try: + message = MIMEMultipart() + message['From'] = self._config['usuario'] + message['To'] = options['para'] + message['CC'] = options['copia'] + message['Subject'] = options['asunto'] + message['Date'] = formatdate(localtime=True) + message.attach(MIMEText(options['mensaje'], 'html')) + for f in options['files']: + part = MIMEBase('application', 'octet-stream') + part.set_payload(f[0]) + encoders.encode_base64(part) + part.add_header( + 'Content-Disposition', + "attachment; filename={}".format(f[1])) + message.attach(part) + + receivers = options['para'].split(',') + options['copia'].split(',') + self._server.sendmail( + self._config['usuario'], receivers, message.as_string()) + return '' + except Exception as e: + return str(e) + + def close(self): + try: + self._server.quit() + except: + pass + return diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py index 0c1ebfb..1939e44 100644 --- a/source/app/controllers/main.py +++ b/source/app/controllers/main.py @@ -76,12 +76,33 @@ class AppValues(object): if file_object is None: session = req.env['beaker.session'] values = req.params - req.context['result'] = self._db.validate_cert(values, session) + if table == 'correo': + req.context['result'] = self._db.validate_email(values) + elif table == 'sendmail': + req.context['result'] = self._db.send_email(values, session) + else: + req.context['result'] = self._db.validate_cert(values, session) else: req.context['result'] = self._db.add_cert(file_object) resp.status = falcon.HTTP_200 +class AppConfig(object): + + def __init__(self, db): + self._db = db + + def on_get(self, req, resp): + values = req.params + req.context['result'] = self._db.get_config(values) + resp.status = falcon.HTTP_200 + + def on_post(self, req, resp): + values = req.params + req.context['result'] = self._db.add_config(values) + resp.status = falcon.HTTP_200 + + class AppPartners(object): diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index e673eb9..6cba2be 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -15,7 +15,9 @@ import time import unicodedata import uuid import zipfile + from io import BytesIO +from smtplib import SMTPException, SMTPAuthenticationError from xml.etree import ElementTree as ET import uno @@ -25,7 +27,7 @@ from com.sun.star.awt import Size import pyqrcode from dateutil import parser -from .helper import CaseInsensitiveDict, NumLet +from .helper import CaseInsensitiveDict, NumLet, SendMail from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT, \ PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PRE @@ -926,3 +928,33 @@ def to_zip(*files): return zip_buffer.getvalue() + +def make_fields(xml): + doc = ET.fromstring(xml) + data = CaseInsensitiveDict(doc.attrib.copy()) + data.pop('certificado') + data.pop('sello') + version = data['version'] + receptor = doc.find('{}Receptor'.format(PRE[version])) + receptor = CaseInsensitiveDict(receptor.attrib.copy()) + data['receptor_nombre'] = receptor['nombre'] + data['receptor_rfc'] = receptor['rfc'] + data = {k.lower(): v for k, v in data.items()} + return data + + +def make_info_mail(data, fields): + return data.format(**fields).replace('\n', '
') + + +def send_mail(data): + msg = '' + server = SendMail(data['server']) + is_connect = server.is_connect + if is_connect: + msg = server.send(data['options']) + else: + msg = server.error + server.close() + return {'ok': is_connect, 'msg': msg} + diff --git a/source/app/main.py b/source/app/main.py index b423f09..a35833f 100644 --- a/source/app/main.py +++ b/source/app/main.py @@ -13,7 +13,7 @@ from middleware import ( ) from models.db import StorageEngine from controllers.main import ( - AppLogin, AppLogout, AppAdmin, AppEmisor, + AppLogin, AppLogout, AppAdmin, AppEmisor, AppConfig, AppMain, AppValues, AppPartners, AppProducts, AppInvoices, AppFolios, AppDocumentos ) @@ -38,6 +38,7 @@ api.add_route('/emisor', AppEmisor(db)) api.add_route('/folios', AppFolios(db)) api.add_route('/main', AppMain(db)) api.add_route('/values/{table}', AppValues(db)) +api.add_route('/config', AppConfig(db)) api.add_route('/doc/{type_doc}/{id_doc}', AppDocumentos(db)) api.add_route('/partners', AppPartners(db)) api.add_route('/products', AppProducts(db)) diff --git a/source/app/models/db.py b/source/app/models/db.py index 7488143..f5a460d 100644 --- a/source/app/models/db.py +++ b/source/app/models/db.py @@ -14,12 +14,24 @@ class StorageEngine(object): def get_values(self, table, values=None): return getattr(self, '_get_{}'.format(table))(values) + def get_config(self, values): + return main.Configuracion.get_(values) + + def add_config(self, values): + return main.Configuracion.add(values) + def add_cert(self, file_object): return main.Certificado.add(file_object) def validate_cert(self, values, session): return main.Certificado.validate(values, session) + def validate_email(self, values): + return main.test_correo(values) + + def send_email(self, values, session): + return main.Facturas.send(values['id'], session['rfc']) + def _get_cert(self, values): return main.Certificado.get_data() diff --git a/source/app/models/main.py b/source/app/models/main.py index 5d69c8c..24f9dcd 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -59,9 +59,34 @@ def desconectar(): class Configuracion(BaseModel): - clave = TextField() + clave = TextField(unique=True) valor = TextField(default='') + @classmethod + def get_(cls, keys): + if keys['fields'] == 'correo': + fields = ('correo_servidor', 'correo_puerto', 'correo_ssl', + 'correo_usuario', 'correo_contra', 'correo_copia', + 'correo_asunto', 'correo_mensaje', 'correo_directo') + data = (Configuracion + .select() + .where(Configuracion.clave.in_(fields)) + ) + values = {r.clave: r.valor for r in data} + return values + + @classmethod + def add(cls, values): + try: + for k, v in values.items(): + obj, created = Configuracion.get_or_create(clave=k) + obj.valor = v + obj.save() + return {'ok': True} + except Exception as e: + log.error(str(e)) + return {'ok': False, 'msg': str(e)} + class Meta: order_by = ('clave',) indexes = ( @@ -1059,6 +1084,47 @@ class Facturas(BaseModel): return file_zip, name_zip + @classmethod + def send(cls, id, rfc): + values = Configuracion.get_({'fields': 'correo'}) + #~ print (server) + obj = Facturas.get(Facturas.id==id) + if obj.uuid is None: + msg = 'La factura no esta timbrada' + return {'ok': False, 'msg': msg} + + if not obj.cliente.correo_facturas: + msg = 'El cliente no tiene configurado el correo para facturas' + return {'ok': False, 'msg': msg} + + files = (cls.get_zip(id, rfc),) + + fields = util.make_fields(obj.xml) + server = { + 'servidor': values['correo_servidor'], + 'puerto': values['correo_puerto'], + 'ssl': bool(int(values['correo_ssl'])), + 'usuario': values['correo_usuario'], + 'contra': values['correo_contra'], + } + options = { + 'para': obj.cliente.correo_facturas, + 'copia': values['correo_copia'], + 'asunto': util.make_info_mail(values['correo_asunto'], fields), + 'mensaje': util.make_info_mail(values['correo_mensaje'], fields), + 'files': files, + } + data= { + 'server': server, + 'options': options, + } + result = util.send_mail(data) + if not result['ok'] or result['msg']: + return {'ok': False, 'msg': result['msg']} + + msg = 'Factura enviada correctamente' + return {'ok': True, 'msg': msg} + @classmethod def get_(cls, values): rows = tuple(Facturas @@ -1450,6 +1516,28 @@ def get_sat_key(key): return util.get_sat_key('products', key) +def test_correo(values): + server = { + 'servidor': values['correo_servidor'], + 'puerto': values['correo_puerto'], + 'ssl': bool(values['correo_ssl'].replace('0', '')), + 'usuario': values['correo_usuario'], + 'contra': values['correo_contra'], + } + options = { + 'para': values['correo_usuario'], + 'copia': values['correo_copia'], + 'asunto': values['correo_asunto'], + 'mensaje': values['correo_mensaje'].replace('\n', '
'), + 'files': [], + } + data= { + 'server': server, + 'options': options, + } + return util.send_mail(data) + + def _init_values(): data = ( {'key': 'version', 'value': VERSION}, diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js index 47c90b8..52b3bcf 100644 --- a/source/static/js/controller/admin.js +++ b/source/static/js/controller/admin.js @@ -16,6 +16,8 @@ var controllers = { $$('up_cert').attachEvent('onUploadComplete', up_cert_upload_complete) $$('cmd_agregar_serie').attachEvent('onItemClick', cmd_agregar_serie_click) $$('grid_folios').attachEvent('onItemClick', grid_folios_click) + $$('cmd_probar_correo').attachEvent('onItemClick', cmd_probar_correo_click) + $$('cmd_guardar_correo').attachEvent('onItemClick', cmd_guardar_correo_click) } } @@ -175,6 +177,24 @@ function get_table_folios(){ } +function get_config_correo(){ + var form = $$('form_correo') + var fields = form.getValues() + + webix.ajax().get('/config', {'fields': 'correo'}, { + error: function(text, data, xhr) { + msg = 'Error al consultar' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json() + form.setValues(values) + } + }) + +} + + function multi_admin_change(prevID, nextID){ //~ webix.message(nextID) if(nextID == 'app_emisor'){ @@ -188,6 +208,11 @@ function multi_admin_change(prevID, nextID){ get_table_folios() return } + + if(nextID == 'app_correo'){ + get_config_correo() + return + } } @@ -442,10 +467,120 @@ function grid_folios_click(id, e, node){ msg_error(msg) } }) - - - } } }) } + + +function validar_correo(values){ + + if(!values.correo_servidor.trim()){ + msg = 'El servidor de salida no puede estar vacío' + msg_error(msg) + return false + } + if(!values.correo_puerto){ + msg = 'El puerto no puede ser cero' + msg_error(msg) + return false + } + if(!values.correo_usuario.trim()){ + msg = 'El nombre de usuario no puede estar vacío' + msg_error(msg) + return false + } + if(!values.correo_contra.trim()){ + msg = 'La contraseña no puede estar vacía' + msg_error(msg) + return false + } + if(!values.correo_asunto.trim()){ + msg = 'El asunto del correo no puede estar vacío' + msg_error(msg) + return false + } + if(!values.correo_mensaje.trim()){ + msg = 'El mensaje del correo no puede estar vacío' + msg_error(msg) + return false + } + + return true +} + + +function cmd_probar_correo_click(){ + var form = $$('form_correo') + var values = form.getValues() + + if(!validar_correo(values)){ + return + } + + webix.ajax().sync().post('/values/correo', values, { + error: function(text, data, xhr) { + msg = 'Error al probar el correo' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json(); + if (values.ok){ + msg = 'Correo de prueba enviado correctamente. Ya puedes \ + guardar esta configuración' + msg_sucess(msg) + }else{ + msg_error(values.msg) + } + } + }) + +} + + +function save_config_mail(values){ + + webix.ajax().sync().post('/config', values, { + error: function(text, data, xhr) { + msg = 'Error al guardar la configuración' + msg_error(msg) + }, + success: function(text, data, xhr) { + var values = data.json(); + if (values.ok){ + msg = 'Configuración guardada correctamente' + msg_sucess(msg) + }else{ + msg_error(values.msg) + } + } + }) + +} + + +function cmd_guardar_correo_click(){ + var form = $$('form_correo') + var values = form.getValues() + + if(!validar_correo(values)){ + return + } + + msg = 'Asegurate de haber probado la configuración

\ + ¿Estás seguro de guardar estos datos?' + webix.confirm({ + title: 'Configuración de correo', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + save_config_mail(values) + } + } + }) +} + + diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index af77be7..3adc7e3 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -642,6 +642,41 @@ function cmd_invoice_timbrar_click(){ } +function enviar_correo(row){ + if(!row.uuid){ + msg_error('La factura no esta timbrada') + return + } + + msg = '¿Estás seguro de enviar por correo esta factura?' + webix.confirm({ + title: 'Enviar Factura', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + webix.ajax().post('/values/sendmail', {'id': row.id}, { + error:function(text, data, XmlHttpRequest){ + msg = 'Ocurrio un error, consulta a soporte técnico' + msg_error(msg) + }, + success:function(text, data, XmlHttpRequest){ + values = data.json(); + if(values.ok){ + msg_sucess(values.msg) + }else{ + msg_error(values.msg) + } + } + }) + } + } + }) +} + + function grid_invoices_click(id, e, node){ var row = this.getItem(id) @@ -652,7 +687,7 @@ function grid_invoices_click(id, e, node){ }else if(id.column == 'zip'){ location = '/doc/zip/' + row.id }else if(id.column == 'email'){ - show('Correo') + enviar_correo(row) } } diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js index 9ca919d..e38e58c 100644 --- a/source/static/js/ui/admin.js +++ b/source/static/js/ui/admin.js @@ -3,6 +3,7 @@ var menu_data = [ {id: 'app_home', icon: 'dashboard', value: 'Inicio'}, {id: 'app_emisor', icon: 'user-circle', value: 'Emisor'}, {id: 'app_folios', icon: 'sort-numeric-asc', value: 'Folios'}, + {id: 'app_correo', icon: 'envelope-o', value: 'Correo'}, ] @@ -205,6 +206,55 @@ var emisor_folios = [ ] +var emisor_correo = [ + {template: 'Servidor de Salida', type: 'section'}, + {cols: [ + {view: 'text', id: 'correo_servidor', name: 'correo_servidor', + label: 'Servidor SMTP: '}, + {}]}, + {cols: [ + {view: 'counter', id: 'correo_puerto', name: 'correo_puerto', + label: 'Puerto: ', value: 26, step: 1}, + {}]}, + {cols: [ + {view: 'checkbox', id: 'correo_ssl', name: 'correo_ssl', + label: 'Usar TLS/SSL: '}, + {}]}, + {cols: [ + {view: 'text', id: 'correo_usuario', name: 'correo_usuario', + label: 'Usuario: '}, + {}]}, + {cols: [ + {view: 'text', id: 'correo_contra', name: 'correo_contra', + label: 'Contraseña: ', type: 'password'}, + {}]}, + {cols: [ + {view: 'text', id: 'correo_copia', name: 'correo_copia', + label: 'Con copia a: '} + ]}, + {cols: [ + {view: 'text', id: 'correo_asunto', name: 'correo_asunto', + label: 'Asunto del correo: '} + ]}, + {cols: [ + {view: 'textarea', id: 'correo_mensaje', name: 'correo_mensaje', + label: 'Mensaje del correo: ', height: 200} + ]}, + {cols: [ + {view: 'checkbox', id: 'correo_directo', name: 'correo_directo', + label: 'Enviar directamente: '}, + {}]}, + {minHeight: 25}, + {cols: [{}, + {view: 'button', id: 'cmd_probar_correo', label: 'Probar Configuración', + autowidth: true, type: 'form'}, + {maxWidth: 100}, + {view: 'button', id: 'cmd_guardar_correo', label: 'Guardar Configuración', + autowidth: true, type: 'form'}, + {}]} +] + + var controls_folios = [ { view: 'tabview', @@ -219,6 +269,20 @@ var controls_folios = [ ] +var controls_correo = [ + { + view: 'tabview', + id: 'tab_correo', + tabbar: {options: ['Correo Electrónico']}, + animate: true, + cells: [ + {id: 'Correo Electrónico', rows: emisor_correo}, + {}, + ] + } +] + + var form_folios = { type: 'space', cols: [{ @@ -239,6 +303,22 @@ var form_folios = { } +var form_correo = { + type: 'space', + cols: [{ + view: 'form', + id: 'form_correo', + complexData: true, + elements: controls_correo, + elementsConfig: { + labelWidth: 150, + labelAlign: 'right' + }, + autoheight: true + }] +} + + var app_emisor = { id: 'app_emisor', rows:[ @@ -267,6 +347,17 @@ var app_folios = { } +var app_correo = { + id: 'app_correo', + rows:[ + {view: 'template', id: 'th_correo', type: 'header', + template: 'Configuración de correo'}, + form_correo, + {}, + ] +} + + var multi_admin = { id: 'multi_admin', animate: true, @@ -278,6 +369,7 @@ var multi_admin = { }, app_emisor, app_folios, + app_correo, ] } From d7b880db6c711db608bec0b9a73d925654e4be62 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 16 Oct 2017 23:09:26 -0500 Subject: [PATCH 06/22] Importar Clientes --- source/app/controllers/util.py | 90 ++++++++++++++++++++++++++++++++++ source/app/models/main.py | 56 ++++++++++++++++++++- 2 files changed, 144 insertions(+), 2 deletions(-) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 6cba2be..3472b6d 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -820,6 +820,7 @@ def _totales(doc, cfdi, version): tn = { '001': 'ISR', '002': 'IVA', + '003': 'IEPS', } traslados = [] retenciones = [] @@ -958,3 +959,92 @@ def send_mail(data): server.close() return {'ok': is_connect, 'msg': msg} + +def get_path_info(path): + path, filename = os.path.split(path) + name, extension = os.path.splitext(filename) + return (path, filename, name, extension) + + +class ImportFacturaLibre(object): + + def __init__(self, path): + self._con = None + self._cursor = None + self._is_connect = self._connect(path) + + @property + def is_connect(self): + return self._is_connect + + def _connect(self, path): + try: + self._con = sqlite3.connect(path) + self._con.row_factory = sqlite3.Row + self._cursor = self._con.cursor() + return True + except Exception as e: + log.error(e) + return False + + def close(self): + try: + self._cursor.close() + self._con.close() + except: + pass + return + + def import_data(self): + data = {} + tables = ( + ('receptores', 'Socios'), + ) + for source, target in tables: + data[target] = self._get_table(source) + return data + + def _get_table(self, table): + return getattr(self, '_{}'.format(table))() + + def _receptores(self): + sql = "SELECT * FROM receptores" + self._cursor.execute(sql) + rows = self._cursor.fetchall() + #~ names = [d[0] for d in self._cursor.description] + fields = ( + ('id', 'id'), + ('rfc', 'rfc'), + ('nombre', 'nombre'), + ('calle', 'calle'), + ('noExterior', 'no_exterior'), + ('noInterior', 'no_interior'), + ('colonia', 'colonia'), + ('municipio', 'municipio'), + ('estado', 'estado'), + ('pais', 'pais'), + ('codigoPostal', 'codigo_postal'), + ('extranjero', 'es_extranjero'), + ('activo', 'es_activo'), + ('fechaalta', 'fecha_alta'), + ('notas', 'notas'), + ('cuentaCliente', 'cuenta_cliente'), + ('cuentaProveedor', 'cuenta_proveedor'), + ('saldoCliente', 'saldo_cliente'), + ('saldoProveedor', 'saldo_proveedor'), + ('esCliente', 'es_cliente'), + ('esProveedor', 'es_proveedor'), + ) + data = [] + + for row in rows: + new = {t: row[s] for s, t in fields} + new['slug'] = to_slug(new['nombre']) + if new['es_extranjero']: + new['tipo_persona'] = 4 + elif new['rfc'] == 'XAXX010101000': + new['tipo_persona'] = 3 + elif len(new['rfc']) == 12: + new['tipo_persona'] = 2 + data.append(new) + return data diff --git a/source/app/models/main.py b/source/app/models/main.py index 24f9dcd..3059944 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -631,6 +631,8 @@ class Socios(BaseModel): es_proveedor = BooleanField(default=False) cuenta_cliente = TextField(default='') cuenta_proveedor = TextField(default='') + saldo_cliente = DecimalField(default=0.0, decimal_places=6, auto_round=True) + saldo_proveedor = DecimalField(default=0.0, decimal_places=6, auto_round=True) web = TextField(default='') correo_facturas = TextField(default='') forma_pago = ForeignKeyField(SATFormaPago, null=True) @@ -1742,6 +1744,40 @@ def _importar_valores(archivo): return +def _importar_factura_libre(archivo): + rfc = input('Introduce el RFC: ').strip().upper() + if not rfc: + msg = 'El RFC es requerido' + log.error(msg) + return + + args = util.get_con(rfc) + if not args: + return + + conectar(args) + + log.info('Importando datos...') + app = util.ImportFacturaLibre(archivo) + if not app.is_connect: + log.error('\tNo se pudo conectar a la base de datos') + return + + data = app.import_data() + for table, rows in data.items(): + log.info('\tImportando: {}'.format(table)) + model = globals()[table] + for row in rows: + try: + model.create(**row) + except IntegrityError: + msg = '\t{}'.format(str(row)) + log.error(msg) + + log.info('Importación terminada...') + return + + CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) help_create_tables = 'Crea las tablas en la base de datos' help_migrate_db = 'Migra las tablas en la base de datos' @@ -1763,10 +1799,11 @@ help_lr = 'Listar RFCs' @click.option('-rfc', '--rfc', help=help_rfc, is_flag=True, default=False) @click.option('-br', '--borrar-rfc', help=help_br, is_flag=True, default=False) @click.option('-lr', '--listar-rfc', help=help_lr, is_flag=True, default=False) -@click.option('-i', '--importar_valores', is_flag=True, default=False) +@click.option('-i', '--importar-valores', is_flag=True, default=False) @click.option('-a', '--archivo') +@click.option('-fl', '--factura-libre', is_flag=True, default=False) def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, rfc, - borrar_rfc, listar_rfc, importar_valores, archivo): + borrar_rfc, listar_rfc, importar_valores, archivo, factura_libre): opt = locals() if opt['iniciar_bd']: @@ -1808,6 +1845,21 @@ def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, rfc, _importar_valores(opt['archivo']) sys.exit(0) + if opt['factura_libre']: + if not opt['archivo']: + msg = 'Falta la ruta de la base de datos' + raise click.ClickException(msg) + if not util.is_file(opt['archivo']): + msg = 'No es un archivo' + raise click.ClickException(msg) + _, _, _, ext = util.get_path_info(opt['archivo']) + if ext != '.sqlite': + msg = 'No es una base de datos' + raise click.ClickException(msg) + + _importar_factura_libre(opt['archivo']) + sys.exit(0) + return From f38d194e4a42ebc097151ae020ebf52f60abfe45 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 16 Oct 2017 23:36:10 -0500 Subject: [PATCH 07/22] UI cancelar --- source/app/models/main.py | 12 ++++++++ source/static/js/controller/invoices.js | 41 +++++++++++++++++++++++++ source/static/js/controller/main.js | 1 + source/static/js/ui/invoices.js | 3 ++ source/static/js/ui/partners.js | 4 +-- 5 files changed, 59 insertions(+), 2 deletions(-) diff --git a/source/app/models/main.py b/source/app/models/main.py index 3059944..7438909 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -1567,6 +1567,18 @@ def _crear_tablas(): ] database_proxy.create_tables(tablas, True) log.info('Tablas creadas correctamente...') + + try: + usuario = 'admin' + contraseña = 'blades3.3' + obj = Usuarios.create( + usuario=usuario, contraseña=contraseña, es_superusuario=True) + except IntegrityError: + msg = 'El usuario ya existe' + log.error(msg) + return + + log.info('SuperUsuario creado correctamente...') return True diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index 3adc7e3..9475aab 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -121,6 +121,9 @@ function delete_invoice(id){ function cmd_delete_invoice_click(id, e, node){ + if(gi.count() == 0){ + return + } var row = gi.getSelectedItem() if (row == undefined){ @@ -691,3 +694,41 @@ function grid_invoices_click(id, e, node){ } } + + +function send_cancel(id){ + show(id) +} + +function cmd_invoice_cancelar_click(){ + if(gi.count() == 0){ + return + } + + var row = gi.getSelectedItem() + if (row == undefined){ + msg_error('Selecciona una factura') + return + } + + if(!row.uuid){ + msg_error('La factura no esta timbrada, solo es posible cancelar \ + facturas timbradas') + return + } + + msg = '¿Estás seguro de enviar a cancelar esta factura?

\ + ESTA ACCIÓN NO SE PUEDE DESHACER' + webix.confirm({ + title: 'Cancelar Factura', + ok: 'Si', + cancel: 'No', + type: 'confirm-error', + text: msg, + callback:function(result){ + if(result){ + send_cancel(row.id) + } + } + }) +} diff --git a/source/static/js/controller/main.js b/source/static/js/controller/main.js index 8830ebd..4dd4c24 100644 --- a/source/static/js/controller/main.js +++ b/source/static/js/controller/main.js @@ -44,6 +44,7 @@ var controllers = { $$('grid_details').attachEvent('onBeforeEditStart', grid_details_before_edit_start) $$('grid_details').attachEvent('onBeforeEditStop', grid_details_before_edit_stop) $$('cmd_invoice_timbrar').attachEvent('onItemClick', cmd_invoice_timbrar_click) + $$('cmd_invoice_cancelar').attachEvent('onItemClick', cmd_invoice_cancelar_click) $$('grid_invoices').attachEvent('onItemClick', grid_invoices_click) } } diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js index 1bd8b8a..9e110c6 100644 --- a/source/static/js/ui/invoices.js +++ b/source/static/js/ui/invoices.js @@ -14,6 +14,9 @@ var toolbar_invoices = [ var toolbar_invoices_util = [ {view: 'button', id: 'cmd_invoice_timbrar', label: 'Timbrar', type: 'iconButton', autowidth: true, icon: 'ticket'}, + {}, + {view: 'button', id: 'cmd_invoice_cancelar', label: 'Cancelar', + type: 'iconButton', autowidth: true, icon: 'ban'}, ] diff --git a/source/static/js/ui/partners.js b/source/static/js/ui/partners.js index 9c031af..c927068 100644 --- a/source/static/js/ui/partners.js +++ b/source/static/js/ui/partners.js @@ -131,7 +131,7 @@ var toolbar_contacts = [ var grid_contacts_cols = [ - {id: 'index', header:'#', adjust:'data', css:'right', + {id: 'index', header: '#', adjust:'data', css:'right', footer: {content: 'rowCount'}}, {id: 'id', header: '', hidden: true}, {id: 'title', header: 'Título', adjust:'data', sort: 'string', @@ -160,7 +160,7 @@ var grid_contacts = { on:{ 'data->onStoreUpdated':function(){ this.data.each(function(obj, i){ - obj.index = i+1; + obj.index = i + 1 }) } }, From e0532b7cb03117576197fc6664dd82b6b7fd1ed4 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Tue, 17 Oct 2017 12:45:49 -0500 Subject: [PATCH 08/22] UI cancelar --- source/app/controllers/util.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 3472b6d..b951864 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -1037,6 +1037,8 @@ class ImportFacturaLibre(object): ) data = [] + sql1 = "SELECT correo FROM correos WHERE id_cliente=?" + sql2 = "SELECT telefono FROM telefonos WHERE id_cliente=?" for row in rows: new = {t: row[s] for s, t in fields} new['slug'] = to_slug(new['nombre']) @@ -1046,5 +1048,16 @@ class ImportFacturaLibre(object): new['tipo_persona'] = 3 elif len(new['rfc']) == 12: new['tipo_persona'] = 2 + + self._cursor.execute(sql1, (new['id'],)) + tmp = self._cursor.fetchall() + if tmp: + new['correo_facturas'] = ', '.join([r[0] for r in tmp]) + + self._cursor.execute(sql2, (new['id'],)) + tmp = self._cursor.fetchall() + if tmp: + new['telefonos'] = ', '.join([r[0] for r in tmp]) + data.append(new) return data From 03a7c5cf67302c50d7600928d2320cc4c337e901 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Wed, 18 Oct 2017 23:06:07 -0500 Subject: [PATCH 09/22] FIX - Obtener cliente --- source/app/models/main.py | 24 +++++++++++++++--------- source/static/js/controller/admin.js | 4 +++- source/static/js/controller/invoices.js | 4 ++-- source/static/js/ui/admin.js | 5 +++++ 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/source/app/models/main.py b/source/app/models/main.py index 7438909..aa7e612 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -183,6 +183,8 @@ class Emisor(BaseModel): correo = TextField(default='') web = TextField(default='') curp = TextField(default='') + token_timbrado = TextField(default='') + token_soporte = TextField(default='') regimenes = ManyToManyField(SATRegimenes, related_name='emisores') def __str__(self): @@ -683,12 +685,12 @@ class Socios(BaseModel): Socios.id, Socios.nombre, Socios.rfc, SATFormaPago.key.alias('forma_pago'), SATUsoCfdi.key.alias('uso_cfdi')) - .join(SATFormaPago).switch(Socios) - .join(SATUsoCfdi).switch(Socios) - .where( - (Socios.id==id) & (Socios.es_cliente==True)) + .join(SATFormaPago, JOIN.LEFT_OUTER).switch(Socios) + .join(SATUsoCfdi, JOIN.LEFT_OUTER).switch(Socios) + .where((Socios.id==id) & (Socios.es_cliente==True)) .dicts() ) + print (id, row) if len(row): return {'ok': True, 'row': row[0]} return {'ok': False} @@ -699,8 +701,8 @@ class Socios(BaseModel): .select(Socios.id, Socios.nombre, Socios.rfc, SATFormaPago.key.alias('forma_pago'), SATUsoCfdi.key.alias('uso_cfdi')) - .join(SATFormaPago).switch(Socios) - .join(SATUsoCfdi).switch(Socios) + .join(SATFormaPago, JOIN.LEFT_OUTER).switch(Socios) + .join(SATUsoCfdi, JOIN.LEFT_OUTER).switch(Socios) .where((Socios.es_cliente==True) & (Socios.rfc.contains(name) | Socios.nombre.contains(name))) @@ -763,7 +765,7 @@ class Socios(BaseModel): class Productos(BaseModel): categoria = ForeignKeyField(Categorias, null=True) clave = TextField(unique=True, index=True) - clave_sat = TextField() + clave_sat = TextField(default='') descripcion = TextField(index=True) unidad = ForeignKeyField(SATUnidades) valor_unitario = DecimalField(default=0.0, decimal_places=6, auto_round=True) @@ -979,6 +981,7 @@ class Facturas(BaseModel): name = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.cliente.rfc) return obj.xml, name + #~ Revisar def _get_data_cfdi_to_pdf(self, xml, cancel, version): pre_nomina = PRE['NOMINA'][version] @@ -1089,7 +1092,10 @@ class Facturas(BaseModel): @classmethod def send(cls, id, rfc): values = Configuracion.get_({'fields': 'correo'}) - #~ print (server) + if not values: + msg = 'No esta configurado el servidor de correo de salida' + return {'ok': False, 'msg': msg} + obj = Facturas.get(Facturas.id==id) if obj.uuid is None: msg = 'La factura no esta timbrada' @@ -1576,7 +1582,7 @@ def _crear_tablas(): except IntegrityError: msg = 'El usuario ya existe' log.error(msg) - return + return False log.info('SuperUsuario creado correctamente...') return True diff --git a/source/static/js/controller/admin.js b/source/static/js/controller/admin.js index 52b3bcf..0790a61 100644 --- a/source/static/js/controller/admin.js +++ b/source/static/js/controller/admin.js @@ -135,7 +135,9 @@ function get_emisor(){ var emisor = values.row.emisor $$('lst_emisor_regimen').parse(values.row.regimenes) form.setValues(emisor, true) - $$('lst_emisor_regimen').select(emisor.regimenes) + if(emisor.regimenes){ + $$('lst_emisor_regimen').select(emisor.regimenes) + } }else{ msg_error(values.msg) } diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js index 9475aab..a79cdc2 100644 --- a/source/static/js/controller/invoices.js +++ b/source/static/js/controller/invoices.js @@ -42,7 +42,7 @@ function get_monedas(){ $$('lst_moneda').setValue(pre.id) if(values.length == 1){ $$('fs_moneda').hide() - $$('fs_moneda').refresh() + //~ $$('fs_moneda').refresh() } }) } @@ -63,7 +63,7 @@ function get_regimen_fiscal(){ $$('lst_regimen_fiscal').setValue(pre.id) if(values.length == 1){ $$('fs_regimen_fiscal').hide() - $$('fs_regimen_fiscal').refresh() + //~ $$('fs_regimen_fiscal').refresh() } }) } diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js index e38e58c..3efb780 100644 --- a/source/static/js/ui/admin.js +++ b/source/static/js/ui/admin.js @@ -87,6 +87,11 @@ var emisor_otros_datos= [ {cols: [{view: 'datepicker', id: 'ong_fecha_dof', name: 'ong_fecha_dof', label: 'Fecha de DOF: ', disabled: true, format: '%d-%M-%Y', placeholder: 'Fecha de publicación en el DOF'}, {}]}, + {template: 'Timbrado y Soporte', type: 'section'}, + {view: 'text', id: 'token_timbrado', + name: 'token_timbrado', label: 'Token de Timbrado: '}, + {view: 'text', id: 'token_soporte', + name: 'token_soporte', label: 'Token de Soporte: '}, ] From 08318165f78006c89042ebcc387989022043b0d8 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Thu, 19 Oct 2017 19:13:59 -0500 Subject: [PATCH 10/22] Agregar CFDI relacionados en DB --- source/app/models/main.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/source/app/models/main.py b/source/app/models/main.py index aa7e612..ec5bea5 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -970,6 +970,7 @@ class Facturas(BaseModel): notas = TextField(default='') pagada = BooleanField(default=False) cancelada = BooleanField(default=False) + tipo_relacion = TextField(default='') error = TextField(default='') class Meta: @@ -1428,6 +1429,17 @@ class Facturas(BaseModel): return {'ok': result['ok'], 'msg': msg, 'row': row} +class FacturasRelacionadas(BaseModel): + factura = ForeignKeyField(Facturas, related_name='original') + factura_origen = ForeignKeyField(Facturas, related_name='relacion') + + class Meta: + order_by = ('factura',) + indexes = ( + (('factura', 'factura_origen'), True), + ) + + class FacturasDetalle(BaseModel): factura = ForeignKeyField(Facturas) producto = ForeignKeyField(Productos, null=True) @@ -1563,7 +1575,8 @@ def _init_values(): def _crear_tablas(): tablas = [Addendas, Categorias, Certificado, CondicionesPago, Configuracion, - Emisor, Facturas, FacturasDetalle, FacturasImpuestos, Folios, Productos, + Emisor, Facturas, FacturasDetalle, FacturasImpuestos, Folios, + FacturasRelacionadas, Productos, SATAduanas, SATFormaPago, SATImpuestos, SATMonedas, SATRegimenes, SATUnidades, SATUsoCfdi, Socios, Tags, Usuarios, Emisor.regimenes.get_through_model(), From d074f5078d284d2300627be2f1a3e7310802139f Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 22 Oct 2017 00:47:43 -0500 Subject: [PATCH 11/22] Agregar tabla SAT tipo de relaciones --- source/app/models/main.py | 62 ++++++++++++------- .../{db => app/models}/valores_iniciales.json | 15 ++++- 2 files changed, 53 insertions(+), 24 deletions(-) rename source/{db => app/models}/valores_iniciales.json (89%) diff --git a/source/app/models/main.py b/source/app/models/main.py index ec5bea5..282c944 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -565,6 +565,22 @@ class SATImpuestos(BaseModel): return tuple(rows) +class SATTipoRelacion(BaseModel): + key = TextField(index=True, unique=True) + name = TextField(default='', index=True) + activo = BooleanField(default=False) + default = BooleanField(default=False) + + class Meta: + order_by = ('-default', 'name',) + indexes = ( + (('key', 'name'), True), + ) + + def __str__(self): + return 'Tipo de relación: ({}) {}'.format(self.key, self.name) + + class SATUsoCfdi(BaseModel): key = TextField(index=True, unique=True) name = TextField(default='', index=True) @@ -1563,17 +1579,18 @@ def _init_values(): {'key': 'version', 'value': VERSION}, {'key': 'rfc_publico', 'value': 'XAXX010101000'}, {'key': 'rfc_extranjero', 'value': 'XEXX010101000'}, + {'key': 'decimales', 'value': '2'}, ) for row in data: try: - Configuration.create(**row) + Configuracion.create(**row) except IntegrityError: pass log.info('Valores iniciales insertados...') return -def _crear_tablas(): +def _crear_tablas(rfc): tablas = [Addendas, Categorias, Certificado, CondicionesPago, Configuracion, Emisor, Facturas, FacturasDetalle, FacturasImpuestos, Folios, FacturasRelacionadas, Productos, @@ -1592,12 +1609,15 @@ def _crear_tablas(): contraseña = 'blades3.3' obj = Usuarios.create( usuario=usuario, contraseña=contraseña, es_superusuario=True) + log.info('SuperUsuario creado correctamente...') except IntegrityError: msg = 'El usuario ya existe' log.error(msg) - return False + pass + + _init_values() + _importar_valores('', rfc) - log.info('SuperUsuario creado correctamente...') return True @@ -1695,7 +1715,7 @@ def _iniciar_bd(): return conectar(args) - if _crear_tablas(): + if _crear_tablas(rfc): return log.error('No se pudieron crear las tablas') @@ -1722,7 +1742,7 @@ def _agregar_rfc(): args = opt.copy() if conectar(args): - if _add_emisor(rfc, util.dumps(opt)) and _crear_tablas(): + if _add_emisor(rfc, util.dumps(opt)) and _crear_tablas(rfc): log.info('RFC agregado correctamente...') return @@ -1738,26 +1758,28 @@ def _listar_rfc(): return -def _importar_valores(archivo): - rfc = input('Introduce el RFC: ').strip().upper() +def _importar_valores(archivo='', rfc=''): if not rfc: - msg = 'El RFC es requerido' - log.error(msg) - return + rfc = input('Introduce el RFC: ').strip().upper() + if not rfc: + msg = 'El RFC es requerido' + log.error(msg) + return - args = util.get_con(rfc) - if not args: - return + args = util.get_con(rfc) + if not args: + return - conectar(args) + conectar(args) + + if not archivo: + archivo = 'valores_iniciales.json' log.info('Importando datos...') regimen = '' rows = util.loads(open(archivo, 'r').read()) for row in rows: log.info('\tImportando tabla: {}'.format(row['tabla'])) - if row['tabla'] == 'Emisor' and 'regimen' in row: - regimen = row['regimen'] table = globals()[row['tabla']] for r in row['datos']: try: @@ -1765,12 +1787,6 @@ def _importar_valores(archivo): except IntegrityError: pass - if regimen: - emisor = Emisor.select()[0] - regimen = SATRegimenes.get(SATRegimenes.key == regimen) - emisor.regimenes.clear() - emisor.regimenes.add(regimen) - log.info('Importación terminada...') return diff --git a/source/db/valores_iniciales.json b/source/app/models/valores_iniciales.json similarity index 89% rename from source/db/valores_iniciales.json rename to source/app/models/valores_iniciales.json index 60fae31..53adaed 100644 --- a/source/db/valores_iniciales.json +++ b/source/app/models/valores_iniciales.json @@ -23,7 +23,20 @@ {"key": "HUR", "name": "Hora", "activo": true}, {"key": "H87", "name": "Pieza", "activo": true}, {"key": "E48", "name": "Servicio", "activo": true}, - {"key": "E51", "name": "Trabajo", "activo": false} + {"key": "E51", "name": "Trabajo", "activo": false}, + {"key": "ACT", "name": "Actividad", "activo": false} + ] +}, +{ + "tabla": "SATTipoRelacion", + "datos": [ + {"key": "01", "name": "Nota de crédito de los documentos relacionados", "activo": true}, + {"key": "02", "name": "Nota de débito de los documentos relacionados", "activo": true}, + {"key": "03", "name": "Devolución de mercancía sobre facturas o traslados previos", "activo": true}, + {"key": "04", "name": "Sustitución de los CFDI previos", "activo": true, "default": true}, + {"key": "05", "name": "Traslados de mercancias facturados previamente", "activo": true}, + {"key": "06", "name": "Factura generada por los traslados previos", "activo": true}, + {"key": "07", "name": "Actividad", "CFDI por aplicación de anticipo": true} ] }, { From 76df8a101a3be1255323a59b1444f6bbe9abf5aa Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 22 Oct 2017 14:53:07 -0500 Subject: [PATCH 12/22] Mover DEBUG --- source/app/settings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/app/settings.py b/source/app/settings.py index 60c08b0..aac7cae 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -7,8 +7,10 @@ from mako.lookup import TemplateLookup from logbook import Logger, StreamHandler, RotatingFileHandler logbook.set_datetime_format('local') +from conf import DEBUG -DEBUG = True + +DEBUG = DEBUG VERSION = '0.1.0' EMAIL_SUPPORT = ('soporte@empresalibre.net',) From 7a2ea36b12b575c6eb3c379b3b18fd5aab7cede4 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 22 Oct 2017 15:13:35 -0500 Subject: [PATCH 13/22] =?UTF-8?q?Debug=20en=20shell=20en=20producci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/app/settings.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/source/app/settings.py b/source/app/settings.py index aac7cae..1e68df6 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -38,6 +38,7 @@ format_string = '[{record.time:%d-%b-%Y %H:%M:%S}] ' \ '{record.channel}: ' \ '{record.message}' + if DEBUG: LOG_LEVEL = 'DEBUG' StreamHandler( @@ -53,6 +54,12 @@ else: level=LOG_LEVEL, format_string=format_string).push_application() + StreamHandler( + sys.stdout, + level=LOG_LEVEL, + format_string=format_string).push_application() + + log = Logger(LOG_NAME) From ac9e066f8b0b153d4d2364e025d77ccc7c45f5a1 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 22 Oct 2017 15:23:22 -0500 Subject: [PATCH 14/22] Borrar RFC --- source/app/models/main.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/source/app/models/main.py b/source/app/models/main.py index 282c944..f9c12a9 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -1703,6 +1703,24 @@ def _add_emisor(rfc, args): return True +def _delete_emisor(rfc): + con = sqlite3.connect(COMPANIES) + cursor = con.cursor() + sql = """ + DELETE FROM names + WHERE rfc = '?'""" + try: + cursor.execute(sql, (rfc,)) + except Exception as e: + log.error(e) + return False + + con.commit() + cursor.close() + con.close() + return True + + def _iniciar_bd(): rfc = input('Introduce el RFC: ').strip().upper() if not rfc: @@ -1750,6 +1768,23 @@ def _agregar_rfc(): return +def _borrar_rfc(): + rfc = input('Introduce el RFC a borrar: ').strip().upper() + if not rfc: + msg = 'El RFC es requerido' + log.error(msg) + return + + confirm = input('¿Estás seguro de borrar el RFC?') + + if _delete_emisor(rfc): + log.info('RFC borrado correctamente...') + return + + log.error('No se pudo borrar el RFC') + return + + def _listar_rfc(): data = util.get_rfcs() for row in data: From e05567f21d31cd17f1b1bac493dc71efda5f611a Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 22 Oct 2017 15:26:45 -0500 Subject: [PATCH 15/22] Fix - Borrar RFC --- source/app/models/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/app/models/main.py b/source/app/models/main.py index f9c12a9..1da228e 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -1708,7 +1708,7 @@ def _delete_emisor(rfc): cursor = con.cursor() sql = """ DELETE FROM names - WHERE rfc = '?'""" + WHERE rfc = ?""" try: cursor.execute(sql, (rfc,)) except Exception as e: From d3671de3a4652cd883291ecef8a4f93c3cb88876 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Sun, 22 Oct 2017 15:35:09 -0500 Subject: [PATCH 16/22] Agregar nueva tabla --- source/app/models/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/app/models/main.py b/source/app/models/main.py index 1da228e..9944678 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -1595,7 +1595,8 @@ def _crear_tablas(rfc): Emisor, Facturas, FacturasDetalle, FacturasImpuestos, Folios, FacturasRelacionadas, Productos, SATAduanas, SATFormaPago, SATImpuestos, SATMonedas, SATRegimenes, - SATUnidades, SATUsoCfdi, Socios, Tags, Usuarios, + SATTipoRelacion, SATUnidades, SATUsoCfdi, + Socios, Tags, Usuarios, Emisor.regimenes.get_through_model(), Socios.tags.get_through_model(), Productos.impuestos.get_through_model(), From 5fabcceb7091fa963c71dda3571630b175562d85 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Mon, 23 Oct 2017 00:45:41 -0500 Subject: [PATCH 17/22] Generar PDF con reportlab --- requirements.txt | 1 + source/app/controllers/helper.py | 267 ++++++++++++++++++++++-- source/app/controllers/util.py | 54 +++-- source/app/models/main.py | 5 +- source/templates/plantilla_factura.json | 26 +++ 5 files changed, 319 insertions(+), 34 deletions(-) create mode 100644 source/templates/plantilla_factura.json diff --git a/requirements.txt b/requirements.txt index 8d5e242..68fa627 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ python-dateutil zeep chardet pyqrcode +reportlab diff --git a/source/app/controllers/helper.py b/source/app/controllers/helper.py index 32fe513..55d9056 100644 --- a/source/app/controllers/helper.py +++ b/source/app/controllers/helper.py @@ -1,18 +1,6 @@ #!/usr/bin/env python3 #~ import falcon -#~ from models.main import get_cp - - -#~ class AppPostalCode(object): - - #~ def on_get(self, req, resp): - #~ values = req.params - #~ req.context['result'] = get_cp(values['cp']) - #~ resp.status = falcon.HTTP_200 - -#~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37 - import re import smtplib import collections @@ -24,7 +12,17 @@ from email.mime.text import MIMEText from email import encoders from email.utils import formatdate +from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate +from reportlab.lib import colors +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.units import cm +from reportlab.lib.pagesizes import letter +from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT +from reportlab.platypus import Paragraph, Table, TableStyle, Spacer +from reportlab.pdfgen import canvas + +#~ 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 @@ -307,3 +305,248 @@ class SendMail(object): except: pass return + + +class NumberedCanvas(canvas.Canvas): + X = 20.59 * cm + Y = 1 * cm + + def __init__(self, *args, **kwargs): + canvas.Canvas.__init__(self, *args, **kwargs) + self._saved_page_states = [] + + def showPage(self): + self._saved_page_states.append(dict(self.__dict__)) + self._startPage() + return + + def save(self): + """add page info to each page (page x of y)""" + page_count = len(self._saved_page_states) + for state in self._saved_page_states: + self.__dict__.update(state) + self.draw_page_number(page_count) + canvas.Canvas.showPage(self) + canvas.Canvas.save(self) + return + + def draw_page_number(self, page_count): + self.setFont('Helvetica', 8) + self.setFillColor(colors.darkred) + text = 'Página {} de {}'.format(self._pageNumber, page_count) + self.drawRightString(self.X, self.Y, text) + return + + +class TemplateInvoice(BaseDocTemplate): + + def __init__(self, *args, **kwargs): + # letter 21.6 x 27.9 + kwargs['pagesize'] = letter + kwargs['rightMargin'] = 1 * cm + kwargs['leftMargin'] = 1 * cm + kwargs['topMargin'] = 1.5 * cm + kwargs['bottomMargin'] = 1.5 * cm + BaseDocTemplate.__init__(self, *args, **kwargs) + self._data = {} + self._rows = [] + + def _set_rect(self, style): + color = style.pop('color', 'black') + if isinstance(color, str): + self.canv.setFillColor(getattr(colors, color)) + else: + self.canv.setFillColorRGB(*color) + + keys = ('x', 'y', 'width', 'height', 'radius') + for k in keys: + style[k] = style[k] * cm + + self.canv.roundRect(**style) + return + + def _set_text(self, styles, value): + rect = styles['rectangulo'] + if value: + self.canv.setFillColor(colors.white) + self.canv.setStrokeColor(colors.white) + ps = ParagraphStyle(**styles['estilo']) + p = Paragraph(value, ps) + p.wrap(rect['width'] * cm, rect['height'] * cm) + p.drawOn(self.canv, rect['x'] * cm, rect['y'] * cm) + + self._set_rect(rect) + return + + def _emisor(self, styles, data): + for k, v in styles.items(): + self._set_text(styles[k], data.get(k, '')) + return + + def afterPage(self): + encabezado = self._custom_styles['encabezado'] + self.canv.saveState() + self._emisor(encabezado['emisor'], self._data['emisor']) + self.canv.restoreState() + return + + @property + def custom_styles(self): + return self._custom_styles + @custom_styles.setter + def custom_styles(self, values): + self._custom_styles = values + + @property + def data(self): + return self._data + @data.setter + def data(self, values): + self._data = values + text = 'Total este reporte = $ {}'.format('1,000.00') + ps = ParagraphStyle( + name='Total', + fontSize=12, + fontName='Helvetica-BoldOblique', + textColor=colors.darkred, + spaceBefore=0.5 * cm, + spaceAfter=0.5 * cm) + p1 = Paragraph(text, ps) + text = 'Nota: esta suma no incluye documentos cancelados' + ps = ParagraphStyle( + name='Note', + fontSize=7, + fontName='Helvetica-BoldOblique') + p2 = Paragraph('', ps) + self._rows = [p2] + + def render(self): + frame = Frame(self.leftMargin, self.bottomMargin, + self.width, self.height, id='normal') + template = PageTemplate(id='invoice', frames=frame) + self.addPageTemplates([template]) + self.build(self._rows, canvasmaker=NumberedCanvas) + return + + +class ReportTemplate(BaseDocTemplate): + """Override the BaseDocTemplate class to do custom handle_XXX actions""" + + def __init__(self, *args, **kwargs): + # letter 21.6 x 27.9 + kwargs['pagesize'] = letter + kwargs['rightMargin'] = 1 * cm + kwargs['leftMargin'] = 1 * cm + kwargs['topMargin'] = 1.5 * cm + kwargs['bottomMargin'] = 1.5 * cm + BaseDocTemplate.__init__(self, *args, **kwargs) + self.styles = getSampleStyleSheet() + self.header = {} + self.data = [] + + def afterPage(self): + """Called after each page has been processed""" + self.canv.saveState() + date = datetime.datetime.today().strftime('%A, %d de %B del %Y') + self.canv.setStrokeColorRGB(0.5, 0, 0) + self.canv.setFont("Helvetica", 8) + self.canv.drawRightString(20.59 * cm, 26.9 * cm, date) + self.canv.line(1 * cm, 26.4 * cm, 20.6 * cm, 26.4 * cm) + + path_cur = os.path.dirname(os.path.realpath(__file__)) + path_img = os.path.join(path_cur, 'logo.png') + try: + self.canv.drawImage(path_img, 1 * cm, 24.2 * cm, 4 * cm, 2 * cm) + except: + pass + + self.canv.roundRect( + 5 * cm, 25.4 * cm, 15.5 * cm, 0.6 * cm, 0.15 * cm, + stroke=True, fill=False) + self.canv.setFont('Helvetica-BoldOblique', 10) + self.canv.drawCentredString(12.75 * cm, 25.6 * cm, self.header['emisor']) + + self.canv.roundRect( + 5 * cm, 24.4 * cm, 15.5 * cm, 0.6 * cm, 0.15 * cm, + stroke=True, fill=False) + self.canv.setFont('Helvetica-BoldOblique', 9) + self.canv.drawCentredString(12.75 * cm, 24.6 * cm, self.header['title']) + + self.canv.line(1 * cm, 1.5 * cm, 20.6 * cm, 1.5 * cm) + self.canv.restoreState() + return + + def set_data(self, data): + self.header['emisor'] = data['emisor'] + self.header['title'] = data['title'] + cols = len(data['rows'][0]) + widths = [] + for w in data['widths']: + widths.append(float(w) * cm) + t_styles = [ + ('GRID', (0, 0), (-1, -1), 0.25, colors.darkred), + ('FONTSIZE', (0, 0), (-1, 0), 9), + ('BOX', (0, 0), (-1, 0), 1, colors.darkred), + ('TEXTCOLOR', (0, 0), (-1, 0), colors.darkred), + ('FONTSIZE', (0, 1), (-1, -1), 8), + ('ALIGN', (0, 0), (-1, 0), 'CENTER'), + ('ALIGN', (0, 0), (0, -1), 'RIGHT'), + ] + if cols == 6: + t_styles += [ + ('ALIGN', (1, 1), (1, -1), 'CENTER'), + ('ALIGN', (3, 1), (3, -1), 'CENTER'), + ('ALIGN', (4, 1), (4, -1), 'RIGHT'), + ] + elif cols == 3: + t_styles += [ + ('ALIGN', (-1, 0), (-1, -1), 'RIGHT'), + ('ALIGN', (-2, 0), (-2, -1), 'RIGHT'), + ('ALIGN', (0, 0), (-1, 0), 'CENTER'), + ] + elif cols == 2: + t_styles += [ + ('ALIGN', (-1, 0), (-1, -1), 'RIGHT'), + ('ALIGN', (0, 0), (-1, 0), 'CENTER'), + ] + rows = [] + for i, r in enumerate(data['rows']): + n = i + 1 + rows.append(('{}.-'.format(n),) + r) + if cols == 6: + if r[4] == 'Cancelado': + t_styles += [ + ('GRID', (0, n), (-1, n), 0.25, colors.red), + ('TEXTCOLOR', (0, n), (-1, n), colors.red), + ] + rows.insert(0, data['titles']) + t = Table(rows, colWidths=widths, repeatRows=1) + t.setStyle(TableStyle(t_styles)) + + text = 'Total este reporte = $ {}'.format(data['total']) + ps = ParagraphStyle( + name='Total', + fontSize=12, + fontName='Helvetica-BoldOblique', + textColor=colors.darkred, + spaceBefore=0.5 * cm, + spaceAfter=0.5 * cm) + p1 = Paragraph(text, ps) + text = 'Nota: esta suma no incluye documentos cancelados' + ps = ParagraphStyle( + name='Note', + fontSize=7, + fontName='Helvetica-BoldOblique') + p2 = Paragraph(text, ps) + self.data = [t, p1, p2] + return + + def render(self): + frame = Frame(self.leftMargin, self.bottomMargin, + self.width, self.height, id='normal') + template = PageTemplate(id='report', frames=frame) + self.addPageTemplates([template]) + self.build(self.data, canvasmaker=NumberedCanvas) + return + + diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index b951864..0ec5d31 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -20,14 +20,14 @@ from io import BytesIO from smtplib import SMTPException, SMTPAuthenticationError from xml.etree import ElementTree as ET -import uno -from com.sun.star.beans import PropertyValue -from com.sun.star.awt import Size +#~ import uno +#~ from com.sun.star.beans import PropertyValue +#~ from com.sun.star.awt import Size import pyqrcode from dateutil import parser -from .helper import CaseInsensitiveDict, NumLet, SendMail +from .helper import CaseInsensitiveDict, NumLet, SendMail, TemplateInvoice from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT, \ PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PRE @@ -168,6 +168,10 @@ def get_file(path): return open(path, 'rb') +def read_file(path, mode='rb'): + return open(path, mode).read() + + def get_size(path): return os.path.getsize(path) @@ -178,16 +182,18 @@ def get_template(name, data={}): return template.render(**data) -def get_path_template(name, default='plantilla_factura.ods'): +def get_custom_styles(name, default='plantilla_factura.json'): path = _join(PATH_TEMPLATES, name) if is_file(path): - return path + with open(path) as fh: + return loads(fh.read()) path = _join(PATH_TEMPLATES, default) if is_file(path): - return path + with open(path) as fh: + return loads(fh.read()) - return '' + return {} def dumps(data): @@ -719,13 +725,17 @@ class LIBO(object): return self._read(path) -def to_pdf(path, data): - app = LIBO() - - if not app.is_running: - return b'' - - return app.pdf(path, data) +def to_pdf(styles, data): + #~ app = LIBO() + #~ if not app.is_running: + #~ return b'' + #~ return app.pdf(path, data) + path = get_path_temp() + pdf = TemplateInvoice(path) + pdf.custom_styles = styles + pdf.data = data + pdf.render() + return read_file(path) def parse_xml(xml): @@ -780,7 +790,7 @@ def _emisor(doc, version, values): data = CaseInsensitiveDict(node.attrib.copy()) node = node.find('{}DomicilioFiscal'.format(PRE[version])) if not node is None: - data.update(node.attrib.copy()) + data.update(CaseInsensitiveDict(node.attrib.copy())) data['regimenfiscal'] = values['regimenfiscal'] return data @@ -897,9 +907,9 @@ def _timbre(doc, version, values): return data -def get_data(invoice, rfc, values): - name = '{}_factura.ods'.format(rfc.lower()) - path = get_path_template(name) +def get_data_from_xml(invoice, rfc, values): + name = '{}_factura.json'.format(rfc.lower()) + custom_styles = get_custom_styles(name) data = {'cancelada': invoice.cancelada} doc = parse_xml(invoice.xml) @@ -917,7 +927,7 @@ def get_data(invoice, rfc, values): } data['timbre'] = _timbre(doc, version, options) - return path, data + return custom_styles, data def to_zip(*files): @@ -966,6 +976,10 @@ def get_path_info(path): return (path, filename, name, extension) +def get_path_temp(): + return tempfile.mkstemp()[1] + + class ImportFacturaLibre(object): def __init__(self, path): diff --git a/source/app/models/main.py b/source/app/models/main.py index 9944678..392d58c 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -16,6 +16,7 @@ if __name__ == '__main__': from controllers import util from settings import log, VERSION, PATH_CP, COMPANIES, PRE + FORMAT = '{0:.2f}' @@ -1083,8 +1084,8 @@ class Facturas(BaseModel): return b'', name values = cls._get_not_in_xml(cls, obj) - path, data = util.get_data(obj, rfc, values) - doc = util.to_pdf(path, data) + custom_styles, data = util.get_data_from_xml(obj, rfc, values) + doc = util.to_pdf(custom_styles, data) return doc, name @classmethod diff --git a/source/templates/plantilla_factura.json b/source/templates/plantilla_factura.json new file mode 100644 index 0000000..9f17882 --- /dev/null +++ b/source/templates/plantilla_factura.json @@ -0,0 +1,26 @@ +{ +"encabezado": { + "emisor": { + "direccion": { + "rectangulo": {"x": 1.0, "y": 26.9, "width": 19.6, "height": 0.4, + "radius": 0.1, "stroke": false, "fill": true, "color": "darkred"} + }, + "nombre": { + "rectangulo": {"x": 10.6, "y": 25.9, "width": 10.0, "height": 0.4, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", "fontSize": 12, + "alignment": 2, "textColor": "darkred", "backColor": "white"} + }, + "rfc": { + "rectangulo": {"x": 10.6, "y": 25.1, "width": 10.0, "height": 0.4, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "rfc", "fontName": "Helvetica-Bold", "fontSize": 11, + "alignment": 2, "textColor": "darkred", "backColor": "white"} + } + } +}, +"conceptos": { +}, +"comprobante": { +} +} From d960158bf8b2a68f15c2ef4b99ab946b24e099ca Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Tue, 24 Oct 2017 00:03:07 -0500 Subject: [PATCH 18/22] Generar PDF ok --- source/app/controllers/helper.py | 214 +++++++++++++++++++++--- source/app/controllers/util.py | 15 +- source/app/models/main.py | 4 +- source/app/settings.py | 1 + source/templates/plantilla_factura.json | 205 ++++++++++++++++++++++- 5 files changed, 406 insertions(+), 33 deletions(-) diff --git a/source/app/controllers/helper.py b/source/app/controllers/helper.py index 55d9056..0ccfc90 100644 --- a/source/app/controllers/helper.py +++ b/source/app/controllers/helper.py @@ -12,7 +12,7 @@ from email.mime.text import MIMEText from email import encoders from email.utils import formatdate -from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate +from reportlab.platypus import BaseDocTemplate, Frame, PageTemplate, Image from reportlab.lib import colors from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import cm @@ -105,7 +105,7 @@ class NumLet(object): #~ texto_final='/100 m.n.)-', fraccion_letras=False, fraccion=''): def _letters(self, numero, moneda='peso'): - texto_inicial = '' + texto_inicial = '-(' texto_final = '/100 m.n.)-' fraccion_letras = False fraccion = '' @@ -309,7 +309,7 @@ class SendMail(object): class NumberedCanvas(canvas.Canvas): X = 20.59 * cm - Y = 1 * cm + Y = 1.5 * cm def __init__(self, *args, **kwargs): canvas.Canvas.__init__(self, *args, **kwargs) @@ -335,6 +335,8 @@ class NumberedCanvas(canvas.Canvas): self.setFillColor(colors.darkred) text = 'Página {} de {}'.format(self._pageNumber, page_count) self.drawRightString(self.X, self.Y, text) + text = 'Factura elaborada con software libre: www.empresalibre.net' + self.drawString(1.5 * cm, 1.5 * cm, text) return @@ -366,6 +368,10 @@ class TemplateInvoice(BaseDocTemplate): return def _set_text(self, styles, value): + text = styles.pop('valor', '') + if not value: + value = text + rect = styles['rectangulo'] if value: self.canv.setFillColor(colors.white) @@ -379,17 +385,174 @@ class TemplateInvoice(BaseDocTemplate): return def _emisor(self, styles, data): + logo_path = data.pop('logo', '') + logo_style = styles.pop('logo', {}) + for k, v in styles.items(): self._set_text(styles[k], data.get(k, '')) + + if logo_path and logo_style: + rect = logo_style['rectangulo'] + keys = ('x', 'y', 'width', 'height') + for k in keys: + rect[k] = rect[k] * cm + self.canv.drawImage(logo_path, **rect) + return + + def _receptor(self, styles, data): + title = styles.pop('titulo', {}) + + for k, v in styles.items(): + self._set_text(styles[k], data.get(k, '')) + + if title: + rect = title['rectangulo'] + self.canv.saveState() + self.canv.rotate(90) + value = title.pop('valor', '') + title['rectangulo']['x'], title['rectangulo']['y'] = \ + title['rectangulo']['y'], title['rectangulo']['x'] * -1 + self._set_text(title, value) + self.canv.restoreState() + return + + def _comprobante1(self, styles, data): + title = styles.pop('titulo', {}) + + for k, v in styles.items(): + self._set_text(styles[k], data.get(k, '')) + + if title: + rect = title['rectangulo'] + self.canv.saveState() + self.canv.rotate(90) + value = title.pop('valor', '') + title['rectangulo']['x'], title['rectangulo']['y'] = \ + title['rectangulo']['y'], title['rectangulo']['x'] * -1 + self._set_text(title, value) + self.canv.restoreState() return def afterPage(self): encabezado = self._custom_styles['encabezado'] self.canv.saveState() self._emisor(encabezado['emisor'], self._data['emisor']) + self._receptor(encabezado['receptor'], self._data['receptor']) + self._comprobante1(encabezado['comprobante'], self._data['comprobante']) self.canv.restoreState() return + def _currency(self, value, simbol='$'): + return '{} {:,.2f}'.format(simbol, float(value)) + + def _format(self, value, field): + fields = ('valorunitario', 'importe') + if field in fields: + return self._currency(value) + return value + + def _conceptos(self, conceptos): + headers = (('Clave', 'Descripción', 'Unidad', 'Cantidad', + 'Valor Unitario', 'Importe'),) + fields = ('noidentificacion', 'descripcion', 'unidad', 'cantidad', + 'valorunitario', 'importe') + rows = [] + for concepto in conceptos: + row = tuple([self._format(concepto[f], f) for f in fields]) + rows.append(row) + return headers + tuple(rows) + + def _totales(self, values): + #~ print (values) + rows = [('', 'Subtotal', self._currency(values['subtotal']))] + + for tax in values['traslados']: + row = ('', tax[0], self._currency(tax[1])) + rows.append(row) + for tax in values['retenciones']: + row = ('', tax[0], self._currency(tax[1])) + rows.append(row) + for tax in values['taxlocales']: + row = ('', tax[0], self._currency(tax[1])) + rows.append(row) + + row = ('', 'Total', self._currency(values['total'])) + rows.append(row) + + widths = [12.5 * cm, 4 * cm, 3 * cm] + table_styles = [ + ('GRID', (0, 0), (-1, -1), 0.05 * cm, colors.white), + ('ALIGN', (1, 0), (-1, -1), 'RIGHT'), + ('FONTSIZE', (0, 0), (-1, -1), 8), + ('BACKGROUND', (1, 0), (-1, -1), colors.linen), + ('TEXTCOLOR', (1, 0), (-1, -1), colors.darkred), + ('FACE', (1, 0), (-1, -1), 'Helvetica-Bold'), + ] + table = Table(rows, colWidths=widths, spaceBefore=0.25*cm) + table.setStyle(TableStyle(table_styles)) + return table + + def _comprobante2(self, styles, data): + leyenda = styles.pop('leyenda', {}) + + ls = [] + for k, v in styles.items(): + if k in data: + if 'spaceBefore' in v['estilo']: + v['estilo']['spaceBefore'] = v['estilo']['spaceBefore'] * cm + ps = ParagraphStyle(**v['estilo']) + p = Paragraph(data[k], ps) + ls.append(p) + + cbb = Image(data['path_cbb']) + cbb.drawHeight = 4 * cm + cbb.drawWidth = 4 * cm + + style_bt = getSampleStyleSheet()['BodyText'] + style_bt.leading = 8 + html_t = '{}' + html = '{}' + msg = 'Cadena original del complemento de certificación digital del SAT' + rows = [ + (cbb, Paragraph(html_t.format('Sello Digital del CFDI'), style_bt)), + ('', Paragraph(html.format(data['sellocfd']), style_bt)), + ('', Paragraph(html_t.format('Sello Digital del SAT'), style_bt)), + ('', Paragraph(html.format(data['sellosat']), style_bt)), + ('', Paragraph(html_t.format(msg), style_bt)), + ('', Paragraph(html.format(data['cadenaoriginal']), style_bt)), + ] + + widths = [4 * cm, 15.5 * cm] + table_styles = [ + ('FONTSIZE', (0, 0), (-1, -1), 6), + ('SPAN', (0, 0), (0, -1)), + ('FACE', (1, 0), (1, 0), 'Helvetica-Bold'), + ('BACKGROUND', (1, 1), (1, 1), colors.linen), + ('TEXTCOLOR', (1, 1), (1, 1), colors.darkred), + ('FACE', (1, 2), (1, 2), 'Helvetica-Bold'), + ('BACKGROUND', (1, 3), (1, 3), colors.linen), + ('TEXTCOLOR', (1, 3), (1, 3), colors.darkred), + ('FACE', (1, 4), (1, 4), 'Helvetica-Bold'), + ('BACKGROUND', (1, 5), (1, 5), colors.linen), + ('TEXTCOLOR', (1, 5), (1, 5), colors.darkred), + ('ALIGN', (0, 0), (0, 0), 'CENTER'), + ('VALIGN', (0, 0), (0, 0), 'MIDDLE'), + ] + table = Table(rows, colWidths=widths) + table.setStyle(TableStyle(table_styles)) + ls.append(table) + + if leyenda: + if 'spaceBefore' in leyenda['estilo']: + leyenda['estilo']['spaceBefore'] = \ + leyenda['estilo']['spaceBefore'] * cm + msg = 'Este documento es una representación impresa de un CFDI' + ps = ParagraphStyle(**leyenda['estilo']) + p = Paragraph(msg, ps) + ls.append(p) + + return ls + @property def custom_styles(self): return self._custom_styles @@ -402,23 +565,34 @@ class TemplateInvoice(BaseDocTemplate): return self._data @data.setter def data(self, values): + #~ print (values) self._data = values - text = 'Total este reporte = $ {}'.format('1,000.00') - ps = ParagraphStyle( - name='Total', - fontSize=12, - fontName='Helvetica-BoldOblique', - textColor=colors.darkred, - spaceBefore=0.5 * cm, - spaceAfter=0.5 * cm) - p1 = Paragraph(text, ps) - text = 'Nota: esta suma no incluye documentos cancelados' - ps = ParagraphStyle( - name='Note', - fontSize=7, - fontName='Helvetica-BoldOblique') - p2 = Paragraph('', ps) - self._rows = [p2] + + rows = self._conceptos(self._data['conceptos']) + widths = [2 * cm, 9 * cm, 1.5 * cm, 2 * cm, 2 * cm, 3 * cm] + table_styles = [ + ('GRID', (0, 0), (-1, -1), 0.05 * cm, colors.white), + ('FONTSIZE', (0, 0), (-1, 0), 7), + ('ALIGN', (0, 0), (-1, 0), 'CENTER'), + ('TEXTCOLOR', (0, 0), (-1, 0), colors.white), + ('FACE', (0, 0), (-1, 0), 'Helvetica-Bold'), + ('BACKGROUND', (0, 0), (-1, 0), colors.darkred), + ('FONTSIZE', (0, 1), (-1, -1), 7), + ('VALIGN', (0, 1), (-1, -1), 'TOP'), + ('ALIGN', (0, 1), (0, -1), 'CENTER'), + ('ALIGN', (2, 1), (2, -1), 'CENTER'), + ('ALIGN', (3, 1), (5, -1), 'RIGHT'), + ('LINEBELOW', (0, 1), (-1, -1), 0.05 * cm, colors.darkred), + ('LINEBEFORE', (0, 1), (-1, -1), 0.05 * cm, colors.white), + ] + table_conceptos = Table(rows, colWidths=widths, repeatRows=1) + table_conceptos.setStyle(TableStyle(table_styles)) + + totales = self._totales(self.data['totales']) + comprobante = self._comprobante2( + self._custom_styles['comprobante'], self.data['comprobante']) + + self._rows = [Spacer(0, 6*cm), table_conceptos, totales] + comprobante def render(self): frame = Frame(self.leftMargin, self.bottomMargin, @@ -487,7 +661,7 @@ class ReportTemplate(BaseDocTemplate): ('GRID', (0, 0), (-1, -1), 0.25, colors.darkred), ('FONTSIZE', (0, 0), (-1, 0), 9), ('BOX', (0, 0), (-1, 0), 1, colors.darkred), - ('TEXTCOLOR', (0, 0), (-1, 0), colors.darkred), + ('TEXTCOLOR', (0, 0), (-1, 0), colors.black), ('FONTSIZE', (0, 1), (-1, -1), 8), ('ALIGN', (0, 0), (-1, 0), 'CENTER'), ('ALIGN', (0, 0), (0, -1), 'RIGHT'), diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index 0ec5d31..a616f15 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -29,7 +29,7 @@ from dateutil import parser from .helper import CaseInsensitiveDict, NumLet, SendMail, TemplateInvoice from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT, \ - PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PRE + PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL, PATH_TEMPLATES, PATH_MEDIA, PRE #~ def _get_hash(password): @@ -774,14 +774,15 @@ def _comprobante(values, options): 'T': 'traslado', } data['tipodecomprobante'] = tipos.get(data['tipodecomprobante']) - data['lugarexpedicion'] = 'C.P. Expedición: {}'.format(data['lugarexpedicion']) + data['lugarexpedicion'] = 'C.P. de Expedición: {}'.format(data['lugarexpedicion']) data['metododepago'] = options['metododepago'] data['formadepago'] = options['formadepago'] data['moneda'] = options['moneda'] data['tipocambio'] = 'Tipo de Cambio: $ {:0.2f}'.format( float(data['tipocambio'])) - + if 'serie' in data: + data['folio'] = '{}-{}'.format(data['serie'], data['folio']) return data @@ -792,6 +793,11 @@ def _emisor(doc, version, values): if not node is None: data.update(CaseInsensitiveDict(node.attrib.copy())) data['regimenfiscal'] = values['regimenfiscal'] + + path = _join(PATH_MEDIA, 'logos', '{}.png'.format(data['rfc'].lower())) + if is_file(path): + data['logo'] = path + return data @@ -813,7 +819,7 @@ def _conceptos(doc, version): if version == '3.3': values['noidentificacion'] = '{}\n(SAT {})'.format( values['noidentificacion'], values['ClaveProdServ']) - values['unidad'] = '({}) {}'.format( + values['unidad'] = '({})\n{}'.format( values['ClaveUnidad'], values['unidad']) data.append(values) return data @@ -926,6 +932,7 @@ def get_data_from_xml(invoice, rfc, values): 'total': data['comprobante']['total'], } data['timbre'] = _timbre(doc, version, options) + data['comprobante'].update(data['timbre']) return custom_styles, data diff --git a/source/app/models/main.py b/source/app/models/main.py index 392d58c..432e8a7 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -137,7 +137,7 @@ class SATRegimenes(BaseModel): ) def __str__(self): - return '({}) {}'.format(self.key, self.name) + return '{} ({})'.format(self.name, self.key) @classmethod def get_(cls, ids): @@ -597,7 +597,7 @@ class SATUsoCfdi(BaseModel): ) def __str__(self): - return 'Uso del CFDI: ({}) {}'.format(self.key, self.name) + return 'Uso del CFDI: {} ({})'.format(self.name, self.key) @classmethod def get_activos(cls, values): diff --git a/source/app/settings.py b/source/app/settings.py index 1e68df6..52205e6 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -17,6 +17,7 @@ EMAIL_SUPPORT = ('soporte@empresalibre.net',) BASE_DIR = os.path.abspath(os.path.dirname(__file__)) PATH_STATIC = os.path.abspath(os.path.join(BASE_DIR, '..')) PATH_TEMPLATES = os.path.abspath(os.path.join(BASE_DIR, '..', 'templates')) +PATH_MEDIA = os.path.abspath(os.path.join(BASE_DIR, '..', 'docs')) PATH_CP = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'cp.db')) COMPANIES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'rfc.db')) diff --git a/source/templates/plantilla_factura.json b/source/templates/plantilla_factura.json index 9f17882..6aabf3f 100644 --- a/source/templates/plantilla_factura.json +++ b/source/templates/plantilla_factura.json @@ -2,25 +2,216 @@ "encabezado": { "emisor": { "direccion": { - "rectangulo": {"x": 1.0, "y": 26.9, "width": 19.6, "height": 0.4, + "rectangulo": {"x": 1.0, "y": 26.2, "width": 19.6, "height": 0.4, "radius": 0.1, "stroke": false, "fill": true, "color": "darkred"} }, "nombre": { - "rectangulo": {"x": 10.6, "y": 25.9, "width": 10.0, "height": 0.4, + "rectangulo": {"x": 10.6, "y": 25.6, "width": 10.0, "height": 0.4, "radius": 0.0, "stroke": false, "fill": false}, - "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", "fontSize": 12, - "alignment": 2, "textColor": "darkred", "backColor": "white"} + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 14, "alignment": 2, "textColor": "darkred", + "backColor": "white"} }, "rfc": { - "rectangulo": {"x": 10.6, "y": 25.1, "width": 10.0, "height": 0.4, + "rectangulo": {"x": 10.6, "y": 25.0, "width": 10.0, "height": 0.4, "radius": 0.0, "stroke": false, "fill": false}, - "estilo": {"name": "rfc", "fontName": "Helvetica-Bold", "fontSize": 11, - "alignment": 2, "textColor": "darkred", "backColor": "white"} + "estilo": {"name": "rfc", "fontName": "Helvetica-Bold", + "fontSize": 12, "alignment": 2, "textColor": "darkred", + "backColor": "white"} + }, + "regimenfiscal": { + "rectangulo": {"x": 10.6, "y": 24.4, "width": 10.0, "height": 0.4, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "regimenfiscal", "fontName": "Helvetica-Bold", + "fontSize": 7, "alignment": 2, "textColor": "darkred", + "backColor": "white"} + }, + "logo": { + "rectangulo": {"x": 1.0, "y": 24.2, "width": 2.5, "height": 2.5} + } + }, + "receptor": { + "titulo": { + "valor": "Receptor", + "rectangulo": {"x": 1.5, "y": 20.8, "width": 2.8, "height": 0.9, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 9, "alignment": 1, "textColor": "darkred", + "backColor": "linen"} + }, + "nombre": { + "rectangulo": {"x": 2.0, "y": 23.2, "width": 15.0, "height": 0.4, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 10, "alignment": 0, "textColor": "black", + "backColor": "white"} + }, + "rfc": { + "rectangulo": {"x": 2.0, "y": 22.5, "width": 10.0, "height": 0.4, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "rfc", "fontName": "Helvetica-Bold", + "fontSize": 9, "alignment": 0, "textColor": "black", + "backColor": "white"} + }, + "usocfdi": { + "rectangulo": {"x": 2.0, "y": 20.7, "width": 15.0, "height": 0.4, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "usocfdi", "fontName": "Helvetica", + "fontSize": 8, "alignment": 0, "textColor": "black", + "backColor": "white"} + } + }, + "comprobante": { + "titulo": { + "valor": "Datos CFDI", + "rectangulo": {"x": 14.0, "y": 20.8, "width": 2.8, "height": 0.9, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 9, "alignment": 1, "textColor": "darkred", + "backColor": "linen"} + }, + "t_folio": { + "valor": "Folio:", + "rectangulo": {"x": 14.2, "y": 23.2, "width": 1.0, "height": 0.8, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 8, "alignment": 0, "textColor": "black", + "backColor": "white"} + }, + "folio": { + "rectangulo": {"x": 15.1, "y": 23.2, "width": 3.0, "height": 0.4, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 8, "alignment": 0, "textColor": "red", + "backColor": "white"} + }, + "t_tipo": { + "valor": "Tipo:", + "rectangulo": {"x": 18.2, "y": 23.2, "width": 1.0, "height": 0.8, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 8, "alignment": 0, "textColor": "black", + "backColor": "white"} + }, + "tipodecomprobante": { + "rectangulo": {"x": 19.0, "y": 23.2, "width": 1.5, "height": 0.8, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 8, "alignment": 0, "textColor": "red", + "backColor": "white"} + }, + "t_uuid": { + "valor": "Folio Fiscal:", + "rectangulo": {"x": 14.2, "y": 22.7, "width": 3.0, "height": 0.4, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 6, "alignment": 0, "textColor": "black", + "backColor": "white"} + }, + "uuid": { + "rectangulo": {"x": 15.6, "y": 22.7, "width": 6.5, "height": 0.4, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 6, "alignment": 0, "textColor": "red", + "backColor": "white"} + }, + "t_fecha": { + "valor": "Fecha Expedición:", + "rectangulo": {"x": 14.2, "y": 22.1, "width": 2.2, "height": 0.3, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 6, "alignment": 2, "textColor": "black", + "backColor": "white"} + }, + "fecha": { + "rectangulo": {"x": 16.5, "y": 22.1, "width": 3.0, "height": 0.3, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 6, "alignment": 0, "textColor": "black", + "backColor": "white"} + }, + "t_fecha_timbrado": { + "valor": "Fecha Timbrado:", + "rectangulo": {"x": 14.2, "y": 21.8, "width": 2.2, "height": 0.3, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 6, "alignment": 2, "textColor": "black", + "backColor": "white"} + }, + "fechatimbrado": { + "rectangulo": {"x": 16.5, "y": 21.8, "width": 3.0, "height": 0.3, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 6, "alignment": 0, "textColor": "black", + "backColor": "white"} + }, + "t_serie_emisor": { + "valor": "Serie CSD Emisor:", + "rectangulo": {"x": 14.2, "y": 21.4, "width": 2.2, "height": 0.3, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 6, "alignment": 2, "textColor": "black", + "backColor": "white"} + }, + "nocertificado": { + "rectangulo": {"x": 16.5, "y": 21.4, "width": 3.0, "height": 0.3, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 6, "alignment": 0, "textColor": "black", + "backColor": "white"} + }, + "t_serie_sat": { + "valor": "Serie CSD SAT:", + "rectangulo": {"x": 14.2, "y": 21.1, "width": 2.2, "height": 0.3, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 6, "alignment": 2, "textColor": "black", + "backColor": "white"} + }, + "nocertificadosat": { + "rectangulo": {"x": 16.5, "y": 21.1, "width": 3.0, "height": 0.3, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 6, "alignment": 0, "textColor": "black", + "backColor": "white"} + }, + "lugarexpedicion": { + "rectangulo": {"x": 14.2, "y": 20.7, "width": 5.5, "height": 0.4, + "radius": 0.0, "stroke": false, "fill": false}, + "estilo": {"name": "nombre", "fontName": "Helvetica-Bold", + "fontSize": 7, "alignment": 1, "textColor": "black", + "backColor": "white"} } } }, "conceptos": { }, "comprobante": { + "totalenletras": { + "estilo": {"name": "enletras", "fontName": "Helvetica-Bold", + "fontSize": 7, "alignment": 1, "textColor": "black", + "spaceBefore": 0.1} + }, + "formadepago": { + "estilo": {"name": "formadepago", "fontName": "Helvetica", + "fontSize": 7, "alignment": 0, "textColor": "black"} + }, + "metododepago": { + "estilo": {"name": "metododepago", "fontName": "Helvetica", + "fontSize": 7, "alignment": 0, "textColor": "black"} + }, + "moneda": { + "estilo": {"name": "moneda", "fontName": "Helvetica", + "fontSize": 7, "alignment": 0, "textColor": "black"} + }, + "tipocambio": { + "estilo": {"name": "tipocambio", "fontName": "Helvetica", + "fontSize": 7, "alignment": 0, "textColor": "black"} + }, + "leyenda": { + "estilo": {"name": "leyenda", "fontName": "Helvetica-Bold", + "fontSize": 6, "alignment": 1, "textColor": "black", + "spaceBefore": 0.2} + } } } From e7a596965664ed82efefa6da7b4cc127e8f21d0d Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Tue, 24 Oct 2017 00:16:46 -0500 Subject: [PATCH 19/22] Agregar pypng como requerimiento --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 68fa627..9b10d62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ python-dateutil zeep chardet pyqrcode +pypng reportlab From a37875d063e9992a71f466ae9b49d9720226f94e Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Wed, 25 Oct 2017 19:46:13 -0500 Subject: [PATCH 20/22] PDF con json si no existe ods --- source/app/controllers/util.py | 54 ++++++++++++++++++++++++---------- source/app/models/main.py | 10 +++++-- source/app/settings.py | 2 ++ 3 files changed, 48 insertions(+), 18 deletions(-) diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index a616f15..f2b274c 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -20,9 +20,13 @@ from io import BytesIO from smtplib import SMTPException, SMTPAuthenticationError from xml.etree import ElementTree as ET -#~ import uno -#~ from com.sun.star.beans import PropertyValue -#~ from com.sun.star.awt import Size +try: + import uno + from com.sun.star.beans import PropertyValue + from com.sun.star.awt import Size + APP_LIBO = True +except: + APP_LIBO = False import pyqrcode from dateutil import parser @@ -183,7 +187,7 @@ def get_template(name, data={}): def get_custom_styles(name, default='plantilla_factura.json'): - path = _join(PATH_TEMPLATES, name) + path = _join(PATH_MEDIA, 'templates', name.lower()) if is_file(path): with open(path) as fh: return loads(fh.read()) @@ -196,6 +200,18 @@ def get_custom_styles(name, default='plantilla_factura.json'): return {} +def get_template_ods(name, default='plantilla_factura.ods'): + path = _join(PATH_MEDIA, 'templates', name.lower()) + if is_file(path): + return path + + path = _join(PATH_TEMPLATES, default) + if is_file(path): + return path + + return '' + + def dumps(data): return json.dumps(data, default=str) @@ -725,14 +741,24 @@ class LIBO(object): return self._read(path) -def to_pdf(styles, data): - #~ app = LIBO() - #~ if not app.is_running: - #~ return b'' - #~ return app.pdf(path, data) +def to_pdf(data): + rfc = data['emisor']['rfc'] + version = data['comprobante']['version'] + + if APP_LIBO: + app = LIBO() + if app.is_running: + name = '{}_{}.ods'.format(rfc, version) + path = get_template_ods(name) + if path: + return app.pdf(path, data) + + name = '{}_{}.json'.format(rfc, version) + custom_styles = get_custom_styles(name) + path = get_path_temp() pdf = TemplateInvoice(path) - pdf.custom_styles = styles + pdf.custom_styles = custom_styles pdf.data = data pdf.render() return read_file(path) @@ -913,10 +939,7 @@ def _timbre(doc, version, values): return data -def get_data_from_xml(invoice, rfc, values): - name = '{}_factura.json'.format(rfc.lower()) - custom_styles = get_custom_styles(name) - +def get_data_from_xml(invoice, values): data = {'cancelada': invoice.cancelada} doc = parse_xml(invoice.xml) data['comprobante'] = _comprobante(doc.attrib.copy(), values) @@ -932,9 +955,10 @@ def get_data_from_xml(invoice, rfc, values): 'total': data['comprobante']['total'], } data['timbre'] = _timbre(doc, version, options) + del data['timbre']['version'] data['comprobante'].update(data['timbre']) - return custom_styles, data + return data def to_zip(*files): diff --git a/source/app/models/main.py b/source/app/models/main.py index 432e8a7..95306cc 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -14,7 +14,7 @@ if __name__ == '__main__': from controllers import util -from settings import log, VERSION, PATH_CP, COMPANIES, PRE +from settings import log, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI FORMAT = '{0:.2f}' @@ -186,6 +186,7 @@ class Emisor(BaseModel): curp = TextField(default='') token_timbrado = TextField(default='') token_soporte = TextField(default='') + logo = TextField(default='') regimenes = ManyToManyField(SATRegimenes, related_name='emisores') def __str__(self): @@ -959,6 +960,7 @@ class Productos(BaseModel): class Facturas(BaseModel): cliente = ForeignKeyField(Socios) + version = TextField(default=CURRENT_CFDI) serie = TextField(default='') folio = IntegerField(default=0) fecha = DateTimeField(default=util.now, formats=['%Y-%m-%d %H:%M:%S']) @@ -987,6 +989,7 @@ class Facturas(BaseModel): notas = TextField(default='') pagada = BooleanField(default=False) cancelada = BooleanField(default=False) + donativo = BooleanField(default=False) tipo_relacion = TextField(default='') error = TextField(default='') @@ -1084,8 +1087,8 @@ class Facturas(BaseModel): return b'', name values = cls._get_not_in_xml(cls, obj) - custom_styles, data = util.get_data_from_xml(obj, rfc, values) - doc = util.to_pdf(custom_styles, data) + data = util.get_data_from_xml(obj, values) + doc = util.to_pdf(data) return doc, name @classmethod @@ -1434,6 +1437,7 @@ class Facturas(BaseModel): obj.uuid = result['uuid'] obj.fecha_timbrado = result['fecha'] obj.estatus = 'Timbrada' + obj.error = '' obj.save() row = {'uuid': obj.uuid, 'estatus': 'Timbrada'} else: diff --git a/source/app/settings.py b/source/app/settings.py index 52205e6..e89def6 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -83,3 +83,5 @@ PRE = { '1.2': '{http://www.sat.gob.mx/nomina12}', } } + +CURRENT_CFDI = '3.3' From 54d925c8b1fdcdc04042c70fd56631cfea673e47 Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Wed, 25 Oct 2017 22:26:18 -0500 Subject: [PATCH 21/22] Agregar datos para timbrado en emisor --- source/app/controllers/pac.py | 45 ++++++++++++++++++++++------------ source/app/controllers/util.py | 12 +++++++-- source/app/models/main.py | 18 +++++++++++++- source/static/js/ui/admin.js | 4 +++ 4 files changed, 60 insertions(+), 19 deletions(-) diff --git a/source/app/controllers/pac.py b/source/app/controllers/pac.py index 49e3cc5..bf2a4a8 100644 --- a/source/app/controllers/pac.py +++ b/source/app/controllers/pac.py @@ -20,7 +20,10 @@ from zeep.cache import SqliteCache from zeep.transports import Transport from zeep.exceptions import Fault, TransportError -from .configpac import DEBUG, TIMEOUT, AUTH, URL +if __name__ == '__main__': + from configpac import DEBUG, TIMEOUT, AUTH, URL +else: + from .configpac import DEBUG, TIMEOUT, AUTH, URL log = Logger('PAC') @@ -147,7 +150,7 @@ class Ecodex(object): class Finkok(object): - def __init__(self): + def __init__(self, auth={}): self.codes = URL['codes'] self.error = '' self.message = '' @@ -159,6 +162,9 @@ class Finkok(object): if DEBUG: self._history = HistoryPlugin() self._plugins = [self._history] + self._auth = AUTH + else: + self._auth = auth def _debug(self): if not DEBUG: @@ -225,6 +231,11 @@ class Finkok(object): def timbra_xml(self, file_xml): self.error = '' + + if not DEBUG and not self._auth: + self.error = 'Sin datos para timbrar' + return + method = 'timbra' ok, xml = self._validate_xml(file_xml) if not ok: @@ -233,8 +244,8 @@ class Finkok(object): URL[method], transport=self._transport, plugins=self._plugins) args = { - 'username': AUTH['USER'], - 'password': AUTH['PASS'], + 'username': self._auth['user'], + 'password': self._auth['pass'], 'xml': xml, } if URL['quick_stamp']: @@ -261,8 +272,8 @@ class Finkok(object): URL[method], transport=self._transport, plugins=self._plugins) args = { - 'username': AUTH['USER'], - 'password': AUTH['PASS'], + 'username': self._auth['user'], + 'password': self._auth['pass'], 'uuid': uuid, 'taxpayer_id': self.rfc, 'invoice_type': 'I', @@ -296,7 +307,8 @@ class Finkok(object): client = Client( URL[method], transport=self._transport, plugins=self._plugins) try: - result = client.service.stamped(xml, AUTH['USER'], AUTH['PASS']) + result = client.service.stamped( + xml, self._auth['user'], self._auth['pass']) except Fault as e: self.error = str(e) return '' @@ -310,7 +322,8 @@ class Finkok(object): client = Client( URL[method], transport=self._transport, plugins=self._plugins) try: - result = client.service.query_pending(AUTH['USER'], AUTH['PASS'], uuid) + result = client.service.query_pending( + self._auth['user'], self._auth['pass'], uuid) #~ print (result.date) #~ tree = parseString(unescape(result.xml)) #~ response = tree.toprettyxml(encoding='utf-8').decode('utf-8') @@ -334,8 +347,8 @@ class Finkok(object): args = { 'UUIDS': uuid_type(uuids=sa(string=uuids)), - 'username': AUTH['USER'], - 'password': AUTH['PASS'], + 'username': self._auth['user'], + 'password': self._auth['pass'], 'taxpayer_id': rfc, 'cer': cer, 'key': key, @@ -366,8 +379,8 @@ class Finkok(object): URL[method], transport=self._transport, plugins=self._plugins) args = { - 'username': AUTH['USER'], - 'password': AUTH['PASS'], + 'username': self._auth['user'], + 'password': self._auth['pass'], 'xml': xml, 'store_pending': True, } @@ -385,8 +398,8 @@ class Finkok(object): URL[method], transport=self._transport, plugins=self._plugins) args = { - 'username': AUTH['USER'], - 'password': AUTH['PASS'], + 'username': self._auth['user'], + 'password': self._auth['pass'], 'taxpayer_id': rfc, 'uuid': '', 'type': type_acuse, @@ -413,8 +426,8 @@ class Finkok(object): URL[method], transport=self._transport, plugins=self._plugins) args = { - 'username': AUTH['USER'], - 'password': AUTH['PASS'], + 'username': self._auth['user'], + 'password': self._auth['pass'], 'uuid': '', } try: diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index f2b274c..d354f03 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -431,11 +431,19 @@ def make_xml(data, certificado): return cfdi.add_sello(sello) -def timbra_xml(xml): +def timbra_xml(xml, auth): from .pac import Finkok as PAC + if DEBUG: + auth = {} + else: + if not auth: + msg = 'Sin datos para timbrar' + result = {'ok': True, 'error': msg} + return result + result = {'ok': True, 'error': ''} - pac = PAC() + pac = PAC(auth) xml = pac.timbra_xml(xml) if not xml: result['ok'] = False diff --git a/source/app/models/main.py b/source/app/models/main.py index 95306cc..ed85e89 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -184,6 +184,7 @@ class Emisor(BaseModel): correo = TextField(default='') web = TextField(default='') curp = TextField(default='') + correo_timbrado = TextField(default='') token_timbrado = TextField(default='') token_soporte = TextField(default='') logo = TextField(default='') @@ -216,6 +217,7 @@ class Emisor(BaseModel): 'emisor_municipio': obj.municipio, 'emisor_estado': obj.estado, 'emisor_pais': obj.pais, + 'emisor_logo': obj.logo, 'emisor_nombre_comercial': obj.nombre_comercial, 'emisor_telefono': obj.telefono, 'emisor_correo': obj.correo, @@ -225,6 +227,9 @@ class Emisor(BaseModel): 'ong_autorizacion': obj.autorizacion, 'ong_fecha': obj.fecha_autorizacion, 'ong_fecha_dof': obj.fecha_dof, + 'correo_timbrado': obj.correo_timbrado, + 'token_timbrado': obj.token_timbrado, + 'token_soporte': obj.token_soporte, 'regimenes': [row.id for row in obj.regimenes] } else: @@ -232,6 +237,14 @@ class Emisor(BaseModel): return {'ok': True, 'row': row} + @classmethod + def get_auth(cls): + try: + obj = Emisor.select()[0] + return {'user': obj.correo_timbrado, 'pass': obj.token_timbrado} + except: + return {} + @classmethod def get_regimenes(cls): obj = Emisor.select()[0] @@ -248,6 +261,7 @@ class Emisor(BaseModel): fields['municipio'] = fields.pop('emisor_municipio', '') fields['estado'] = fields.pop('emisor_estado', '') fields['pais'] = fields.pop('emisor_pais', 'México') + fields['logo'] = fields.pop('emisor_logo', '') fields['nombre_comercial'] = fields.pop('emisor_nombre_comercial', '') fields['telefono'] = fields.pop('emisor_telefono', '') fields['correo'] = fields.pop('emisor_correo', '') @@ -1429,9 +1443,11 @@ class Facturas(BaseModel): obj.estatus = 'Generada' obj.save() + auth = Emisor.get_auth() + error = False msg = 'Factura timbrada correctamente' - result = util.timbra_xml(obj.xml) + result = util.timbra_xml(obj.xml, auth) if result['ok']: obj.xml = result['xml'] obj.uuid = result['uuid'] diff --git a/source/static/js/ui/admin.js b/source/static/js/ui/admin.js index 3efb780..53d6fa8 100644 --- a/source/static/js/ui/admin.js +++ b/source/static/js/ui/admin.js @@ -62,6 +62,8 @@ var emisor_datos_fiscales = [ var emisor_otros_datos= [ {template: 'Generales', type: 'section'}, + {view: 'search', id: 'emisor_logo', icon: 'file-image-o', + name: 'emisor_logo', label: 'Logotipo: '}, {view: 'text', id: 'emisor_nombre_comercial', name: 'emisor_nombre_comercial', label: 'Nombre comercial: '}, {view: 'text', id: 'emisor_telefono', name: 'emisor_telefono', @@ -88,6 +90,8 @@ var emisor_otros_datos= [ label: 'Fecha de DOF: ', disabled: true, format: '%d-%M-%Y', placeholder: 'Fecha de publicación en el DOF'}, {}]}, {template: 'Timbrado y Soporte', type: 'section'}, + {view: 'text', id: 'correo_timbrado', + name: 'correo_timbrado', label: 'Usuario para Timbrado: '}, {view: 'text', id: 'token_timbrado', name: 'token_timbrado', label: 'Token de Timbrado: '}, {view: 'text', id: 'token_soporte', From c76418917eb5dfe779913d4bcb32b3c8d120f5dc Mon Sep 17 00:00:00 2001 From: Mauricio Baeza Date: Thu, 26 Oct 2017 13:09:47 -0500 Subject: [PATCH 22/22] Crear BD de rfcs de acceso --- source/app/controllers/pac.py | 26 ++++++------ source/app/controllers/util.py | 39 +++++++++++++----- source/app/models/main.py | 9 ++-- source/app/settings.py | 3 ++ source/db/rfc.db.ini | Bin 12288 -> 0 bytes .../{app/models => db}/valores_iniciales.json | 0 6 files changed, 50 insertions(+), 27 deletions(-) delete mode 100644 source/db/rfc.db.ini rename source/{app/models => db}/valores_iniciales.json (100%) diff --git a/source/app/controllers/pac.py b/source/app/controllers/pac.py index bf2a4a8..25b7fa4 100644 --- a/source/app/controllers/pac.py +++ b/source/app/controllers/pac.py @@ -244,8 +244,8 @@ class Finkok(object): URL[method], transport=self._transport, plugins=self._plugins) args = { - 'username': self._auth['user'], - 'password': self._auth['pass'], + 'username': self._auth['USER'], + 'password': self._auth['PASS'], 'xml': xml, } if URL['quick_stamp']: @@ -272,8 +272,8 @@ class Finkok(object): URL[method], transport=self._transport, plugins=self._plugins) args = { - 'username': self._auth['user'], - 'password': self._auth['pass'], + 'username': self._auth['USER'], + 'password': self._auth['PASS'], 'uuid': uuid, 'taxpayer_id': self.rfc, 'invoice_type': 'I', @@ -323,7 +323,7 @@ class Finkok(object): URL[method], transport=self._transport, plugins=self._plugins) try: result = client.service.query_pending( - self._auth['user'], self._auth['pass'], uuid) + self._auth['USER'], self._auth['PASS'], uuid) #~ print (result.date) #~ tree = parseString(unescape(result.xml)) #~ response = tree.toprettyxml(encoding='utf-8').decode('utf-8') @@ -347,8 +347,8 @@ class Finkok(object): args = { 'UUIDS': uuid_type(uuids=sa(string=uuids)), - 'username': self._auth['user'], - 'password': self._auth['pass'], + 'username': self._auth['USER'], + 'password': self._auth['PASS'], 'taxpayer_id': rfc, 'cer': cer, 'key': key, @@ -379,8 +379,8 @@ class Finkok(object): URL[method], transport=self._transport, plugins=self._plugins) args = { - 'username': self._auth['user'], - 'password': self._auth['pass'], + 'username': self._auth['USER'], + 'password': self._auth['PASS'], 'xml': xml, 'store_pending': True, } @@ -398,8 +398,8 @@ class Finkok(object): URL[method], transport=self._transport, plugins=self._plugins) args = { - 'username': self._auth['user'], - 'password': self._auth['pass'], + 'username': self._auth['USER'], + 'password': self._auth['PASS'], 'taxpayer_id': rfc, 'uuid': '', 'type': type_acuse, @@ -426,8 +426,8 @@ class Finkok(object): URL[method], transport=self._transport, plugins=self._plugins) args = { - 'username': self._auth['user'], - 'password': self._auth['pass'], + 'username': self._auth['USER'], + 'password': self._auth['PASS'], 'uuid': '', } try: diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py index d354f03..2b4fea4 100644 --- a/source/app/controllers/util.py +++ b/source/app/controllers/util.py @@ -95,7 +95,23 @@ def get_value(arg): return value +def _valid_db_companies(): + con = sqlite3.connect(COMPANIES) + sql = """ + CREATE TABLE IF NOT EXISTS names( + rfc TEXT NOT NULL COLLATE NOCASE UNIQUE, + con TEXT NOT NULL + ); + """ + cursor = con.cursor() + cursor.executescript(sql) + cursor.close() + con.close() + return + + def _get_args(rfc): + _valid_db_companies() con = sqlite3.connect(COMPANIES) cursor = con.cursor() sql = "SELECT con FROM names WHERE rfc=?" @@ -111,6 +127,18 @@ def _get_args(rfc): return values[0] +def get_rfcs(): + _valid_db_companies() + con = sqlite3.connect(COMPANIES) + cursor = con.cursor() + sql = "SELECT * FROM names" + cursor.execute(sql) + values = cursor.fetchall() + cursor.close() + con.close() + return values + + def get_con(rfc=''): if not rfc: rfc = get_value('RFC').upper() @@ -123,17 +151,6 @@ def get_con(rfc=''): return loads(args) -def get_rfcs(): - con = sqlite3.connect(COMPANIES) - cursor = con.cursor() - sql = "SELECT * FROM names" - cursor.execute(sql) - values = cursor.fetchall() - cursor.close() - con.close() - return values - - def get_sat_key(table, key): con = sqlite3.connect(DB_SAT) cursor = con.cursor() diff --git a/source/app/models/main.py b/source/app/models/main.py index ed85e89..60ecc57 100644 --- a/source/app/models/main.py +++ b/source/app/models/main.py @@ -14,7 +14,8 @@ if __name__ == '__main__': from controllers import util -from settings import log, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI +from settings import log, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI, \ + INIT_VALUES FORMAT = '{0:.2f}' @@ -241,7 +242,7 @@ class Emisor(BaseModel): def get_auth(cls): try: obj = Emisor.select()[0] - return {'user': obj.correo_timbrado, 'pass': obj.token_timbrado} + return {'USER': obj.correo_timbrado, 'PASS': obj.token_timbrado} except: return {} @@ -1708,6 +1709,7 @@ def _cambiar_contraseña(): def _add_emisor(rfc, args): + util._valid_db_companies() con = sqlite3.connect(COMPANIES) cursor = con.cursor() sql = """ @@ -1726,6 +1728,7 @@ def _add_emisor(rfc, args): def _delete_emisor(rfc): + util._valid_db_companies() con = sqlite3.connect(COMPANIES) cursor = con.cursor() sql = """ @@ -1830,7 +1833,7 @@ def _importar_valores(archivo='', rfc=''): conectar(args) if not archivo: - archivo = 'valores_iniciales.json' + archivo = INIT_VALUES log.info('Importando datos...') regimen = '' diff --git a/source/app/settings.py b/source/app/settings.py index e89def6..1776135 100644 --- a/source/app/settings.py +++ b/source/app/settings.py @@ -23,6 +23,9 @@ PATH_CP = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'cp.db')) COMPANIES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'rfc.db')) DB_SAT = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'sat.db')) +IV = 'valores_iniciales.json' +INIT_VALUES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', IV)) + PATH_XSLT = os.path.abspath(os.path.join(BASE_DIR, '..', 'xslt')) PATH_BIN = os.path.abspath(os.path.join(BASE_DIR, '..', 'bin')) diff --git a/source/db/rfc.db.ini b/source/db/rfc.db.ini deleted file mode 100644 index cd38d7c1cbaec31d574f364ae2d64671c0efbd2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI&K}*9h6bJB^D(VU&8wk1PxPl1cSy+`E#_Dvn1$S!e>|n69xK;5s_5uA01HXkw zlfc*zcG_ja|G`UMlBO-cTOd#UU|i*LRL-VZC9kPLj8R)kB5LY9(zy;(AFl_xXIu5I zvPqZc-*x^&4v&e){A<^Pn1=uaAOHafKmY;|fB*y_009X6JAn^&DuiImZk4@E@*