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