#!/usr/bin/env python3 import io import zipfile from pathlib import Path from django.conf import settings from .cfdi_to_dict import CfdiToDic from . import easymacro as app class NumLet(object): CURRENCIES = { 'MXN': 'peso', 'USD': 'dólar', 'EUR': 'euro', } def __init__(self, value, currency='MXN', **kwargs): self._letters = self._convert(value, currency, kwargs) @property def letters(self): return self._letters def _convert(self, value, currency, args): letters = '' currency = self.CURRENCIES.get(currency.upper(), 'peso') text_start = args.get('text_start', '-( ') plural = self._plural(currency) template = f'{int(value):0>15}' decimals = f'{value:0.12f}'.split('.')[1][:2] text_end = args.get('text_end', f' {decimals}/100 m.n. )-') if value < 1: letters = f'cero ' elif value < 2: plural = currency letters = f'un ' else: letters = self._to_letters(template) if int(''.join(template[3:])) == 0 or int(''.join(template[9:])) == 0: plural = f'de {plural}' letters = f'{text_start}{letters}{plural}{text_end}' return letters def _to_letters(self, template): letters = '' for i in range(0, 15, 3): cen = int(template[i]) dec = int(template[i+1]) uni = int(template[i+2]) letter3 = self._centena(uni, dec, cen) letter2 = self._decena(uni, dec) letter1 = self._unidad(uni, dec) legend = '' if i == 0: if (cen + dec + uni) == 1: legend = 'billon ' elif (cen + dec + uni) > 1: legend = 'billones ' elif i == 3: if (cen + dec + uni) >= 1 and int(''.join(template[6:9])) == 0: legend = "mil millones " elif (cen + dec + uni) >= 1: legend = "mil " elif i == 6: if (cen + dec) == 0 and uni == 1: legend = 'millon ' elif cen > 0 or dec > 0 or uni > 1: legend = 'millones ' elif i == 9: if (cen + dec + uni) >= 1: legend = 'mil ' letters += letter3 + letter2 + letter1 + legend return letters def _centena(self, uni, dec, cen): numbers = ('', 'ciento ', 'doscientos ', 'trescientos ', 'cuatrocientos ', 'quinientos ', 'seiscientos ', 'setecientos ', 'ochocientos ', 'novecientos ') letters = numbers[cen] if cen == 1 and (dec + uni) == 0: letters = 'cien ' return letters def _decena(self, uni, dec): numbers = ('diez ', 'once ', 'doce ', 'trece ', 'catorce ', 'quince ', 'dieci') decenas = ('', '', '', 'treinta ', 'cuarenta ', 'cincuenta ', 'sesenta ', 'setenta ', 'ochenta ', 'noventa ') letters = decenas[dec] if dec == 1: if uni > 5: letters = numbers[-1] else: letters = numbers[uni] elif dec == 2: letters = 'veinti' if uni == 0: letters = 'veinte ' if uni > 0 and dec > 2: letters = letters + 'y ' return letters def _unidad(self, uni, dec): letters = '' numbers = ('', 'un ', 'dos ', 'tres ', 'cuatro ', 'cinco ', 'seis ', 'siete ', 'ocho ', 'nueve ') if dec != 1: if uni > 0 and uni <= 5: letters = numbers[uni] if uni >= 6 and uni <= 9: letters = numbers[uni] return letters def _plural(self, word): if word[-1] in 'aeiou': word += 's' else: word += 'es' return word def get_data_from_cfdi(xml): cfdi = CfdiToDic() cfdi.complements = False cfdi.parse(xml) data = {} source = cfdi.data['comprobante'] fields = ( ('version', 'Version'), ('date_cfdi', 'Fecha'), ('type_cfdi', 'TipoDeComprobante'), ('no_cert', 'NoCertificado'), ('subtotal', 'SubTotal'), ('total', 'Total'), ('xml', 'xml'), ) for k1, k2 in fields: data[k1] = source[k2] fields = ( ('serie', 'Serie'), ('folio', 'Folio'), ('place_expedition', ''), ('currency', 'Moneda'), ('way_pay', 'FormaPago'), ('method_pay', 'MetodoPago'), ) for k1, k2 in fields: data[k1] = source.get(k2, '') fields = ( ('discount', 'Descuento'), ('type_change', 'TipoCambio'), ('tax_trasladados', 'TotalImpuestosTrasladados'), ('tax_retenidos', 'TotalImpuestosRetenidos'), ('tax_others', 'TotalOtrosImpuestos'), ) for k1, k2 in fields: data[k1] = source.get(k2, None) source = cfdi.data['timbre'] fields = ( ('uuid', 'UUID'), ('date_stamp', 'FechaTimbrado'), ('no_cert_sat', 'NoCertificadoSAT'), ('rfc_pac', 'RfcProvCertif'), ) for k1, k2 in fields: data[k1] = source[k2] source = cfdi.data['emisor'] fields = ( ('emisor_rfc', 'Rfc'), ('regimen_fiscal', 'RegimenFiscal'), ) for k1, k2 in fields: data[k1] = source[k2] fields = ( ('emisor', 'Nombre'), ('registro_patronal', 'RegistroPatronal'), ) for k1, k2 in fields: data[k1] = source.get(k1, '') source = cfdi.data['receptor'] fields = ( ('receptor_rfc', 'Rfc'), ('uso_cfdi', 'UsoCFDI'), ) for k1, k2 in fields: data[k1] = source[k2] fields = ( ('receptor', 'Nombre'), ) for k1, k2 in fields: data[k1] = source.get(k1, '') source = cfdi.data['impuestos'] fields = ( ('tax_trasladados', 'TotalImpuestosTrasladados'), ('tax_retenidos', 'TotalImpuestosRetenidos'), ('tax_others', 'TotalOtrosImpuestos'), ) for k1, k2 in fields: data[k1] = source.get(k2, None) taxes = [] source = cfdi.data['impuestos'].get('traslados', {}) if source: for t in source: tax = dict( type_tax = 'T', key_sat = t['Impuesto'], importe = t['Importe'], type_factor = t['TipoFactor'], rate = t['TasaOCuota'], ) taxes.append(tax) source = cfdi.data['impuestos'].get('retenciones', {}) if source: for t in source: tax = dict( type_tax = 'R', key_sat = t['Impuesto'], importe = t['Importe'], ) taxes.append(tax) data['taxes'] = taxes source = cfdi.data['conceptos'] details = [] for c in source: detail = dict( key = c.get('NoIdentificacion', ''), key_sat = c.get('ClaveProdServ', ''), unit = c.get('Unidad', ''), key_unit = c.get('ClaveUnidad', ''), description = c['Descripcion'], cant = c['Cantidad'], value = c['ValorUnitario'], discount = c.get('Descuento', None), importe = c['Importe'], ) taxes = [] rows = c['taxes']['traslados'] for row in rows: tax = dict( type_tax = 'T', base = row['Base'], key_sat = row['Impuesto'], importe = row['Importe'], type_factor = row['TipoFactor'], rate = row['TasaOCuota'], ) taxes.append(tax) rows = c['taxes']['retenciones'] for row in rows: tax = dict( type_tax = 'R', base = row['Base'], key_sat = row['Impuesto'], importe = row['Importe'], type_factor = row['TipoFactor'], rate = row['TasaOCuota'], ) taxes.append(tax) detail['taxes'] = taxes details.append(detail) data['details'] = details return data def read_zip(source): z = zipfile.ZipFile(io.BytesIO(source), compression=zipfile.ZIP_DEFLATED) return z def join(*paths): return str(Path(paths[0]).joinpath(*paths[1:])) def get_template(obj): nomina = '' if obj.version_nomina: nomina = f'_{obj.version_nomina}' name = f'{obj.emisor_rfc.lower()}_{obj.version}{nomina}.ods' path = join(settings.MEDIA_ROOT, 'templates', name) if not Path(path).exists(): name = f'template_{obj.version}{nomina}.ods' path = join(settings.MEDIA_ROOT, 'default', name) return path def _get_data_nomina(data): if not 'nomina' in data: return {} percepciones = () deducciones = () if 'percepciones' in data: percepciones = data['percepciones'].pop('percepciones', ()) if 'deducciones' in data: deducciones = data['deducciones'].pop('deducciones', ()) nomina = { 'percepciones': percepciones, 'deducciones': deducciones, } return nomina def _set_data_nomina(doc, data): if not data: return sheet = doc[0] percepciones = data['percepciones'] deducciones = data['deducciones'] count = len(percepciones) if len(deducciones) > count: count = len(deducciones) count -= 1 cells = {} for i, row in enumerate(percepciones): for j, k in enumerate(row.keys()): if i == 0: data = {'percepcion': {k: row[k]}} cell = sheet.render(data, clean=False) cells[k] = cell else: cell = cells[k] if cell.is_none: continue cell = cell.next_cell cell.value = row[k] cells[k] = cell if i == 0 and count: row = cell.address.Row rows = sheet.rows.insert(row + 1, count) rows.copy_format_from(sheet[row]) cells = {} for i, row in enumerate(deducciones): for j, k in enumerate(row.keys()): if i == 0: data = {'deduccion': {k: row[k]}} cell = sheet.render(data, clean=False) cells[k] = cell else: cell = cells[k] if cell.is_none: continue cell = cell.next_cell cell.value = row[k] cells[k] = cell return def _set_conceptos(doc, items): if not items: return return def get_pdf(obj): items = () name = f'{obj.uuid}' srv = app.LOServer() if srv.is_running: cfdi = CfdiToDic() cfdi.parse(obj.xml.encode()) data = cfdi.data total = data['comprobante']['Total'] currency = data['comprobante']['Moneda'] in_letters = NumLet(total, currency).letters.upper() data['comprobante']['totalenletras'] = in_letters path_template = get_template(obj) path_ods = join('/tmp', f'{name}.ods') path_pdf = join('/tmp', f'{name}.pdf') args = {'AsTemplate': True, 'Hidden': True} with app.docs.open(path_template, args) as doc: # ~ if len(data['conceptos']) > 1: # ~ items = data.pop('conceptos') # ~ nomina = _get_data_nomina(data) doc.render(data, clean=False) # ~ _set_conceptos(doc, items) # ~ _set_data_nomina(doc, nomina) # ~ doc[0].shapes[0].remove() # ~ doc.save(path_ods) if doc.to_pdf(path_pdf): pdf = read_bin(path_pdf) unlink(path_ods) unlink(path_pdf) return pdf def read_bin(path): obj = io.BytesIO(Path(path).read_bytes()) obj.seek(0) return obj def unlink(path): Path(path).unlink(missing_ok=True) return