435 lines
12 KiB
Python
435 lines
12 KiB
Python
#!/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
|
|
|