Factura HTML v1

This commit is contained in:
Mauricio Baeza 2018-11-20 00:03:07 -06:00
parent 7dc65a2a2b
commit 0d52e9b570
8 changed files with 178 additions and 121 deletions

View File

@ -16,9 +16,11 @@
# ~ You should have received a copy of the GNU General Public License
# ~ along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import datetime
import getpass
import hashlib
import io
import json
import locale
import mimetypes
@ -68,6 +70,13 @@ from settings import SEAFILE_SERVER, USAR_TOKEN, API, DECIMALES_TAX
from .configpac import AUTH
# ~ v2
from settings import (
MXN,
PATHS,
)
def _call(args):
return subprocess.check_output(args, shell=True).decode()
@ -650,7 +659,7 @@ class LIBO(object):
self._ctx = None
self._sm = None
self._desktop = None
self._currency = 'MXN'
self._currency = MXN
self._total_cantidades = 0
if self.is_running:
ctx = uno.getComponentContext()
@ -1583,11 +1592,11 @@ def to_pdf(data, emisor_rfc, ods=False):
def format_currency(value, currency, digits=2):
c = {
'MXN': '$',
MXN: '$',
'USD': '$',
'EUR': '',
}
s = c.get(currency, 'MXN')
s = c.get(currency, MXN)
return f'{s} {float(value):,.{digits}f}'
@ -1597,7 +1606,7 @@ def to_html(data):
template = template_lookup.get_template(name)
except TopLevelLookupException:
template = template_lookup.get_template('plantilla_factura.html')
data['rfc'] = 'estilos'
data['rfc'] = 'invoice'
return template.render(**data)
@ -1651,11 +1660,16 @@ def to_letters(value, currency):
return NumLet(value, currency).letras
def get_qr(data):
path = get_path_temp('.qr')
def get_qr(data, p=True):
qr = pyqrcode.create(data, mode='binary')
qr.png(path, scale=7)
return path
if p:
path = get_path_temp('.qr')
qr.png(path, scale=7)
return path
buffer = io.BytesIO()
qr.png(buffer, scale=8)
return base64.b64encode(buffer.getvalue()).decode()
def _get_relacionados(doc, version):
@ -1907,6 +1921,7 @@ def _timbre(doc, version, values):
'sello': '&fe={}'.format(data['sellocfd'][-8:]),
}
qr_data = '{url}{uuid}{emisor}{receptor}{total}{sello}'.format(**qr_data)
data['path_cbb'] = get_qr(qr_data)
data['cadenaoriginal'] = CADENA.format(**data)
return data
@ -2110,11 +2125,7 @@ def get_date(value, next_day=False):
def upload_file(rfc, opt, file_obj):
if opt == 'emisorlogo':
tmp = file_obj.filename.split('.')
name = '{}.{}'.format(rfc.lower(), tmp[-1].lower())
path = _join(PATH_MEDIA, 'logos', name)
elif opt == 'txt_plantilla_factura_32':
if opt == 'txt_plantilla_factura_32':
tmp = file_obj.filename.split('.')
ext = tmp[-1].lower()
if ext != 'ods':

View File

@ -8,7 +8,6 @@ from middleware import (
AuthMiddleware,
JSONTranslator,
ConnectionMiddleware,
static,
handle_404
)
from models.db import StorageEngine
@ -64,9 +63,6 @@ api.add_route('/satformapago', AppSATFormaPago(db))
api.add_route('/socioscb', AppSociosCuentasBanco(db))
# ~ Activa si usas waitress y NO estas usando servidor web
# ~ api.add_sink(static, '/static')
session_options = {
'session.type': 'file',
'session.cookie_expires': True,

View File

@ -3,7 +3,7 @@
import falcon
from controllers import util
from models import main
from settings import MV, PATH_STATIC
from settings import MV
def handle_404(req, resp):
@ -20,14 +20,14 @@ def get_template(req, resp, resource):
resp.body = util.get_template(resource.template, data)
def static(req, res):
path = PATH_STATIC + req.path
if util.is_file(path):
res.content_type = util.get_mimetype(path)
res.stream, res.stream_len = util.get_stream(path)
res.status = falcon.HTTP_200
else:
res.status = falcon.HTTP_404
# ~ def static(req, res):
# ~ path = PATH_STATIC + req.path
# ~ if util.is_file(path):
# ~ res.content_type = util.get_mimetype(path)
# ~ res.stream, res.stream_len = util.get_stream(path)
# ~ res.status = falcon.HTTP_200
# ~ else:
# ~ res.status = falcon.HTTP_404
class AuthMiddleware(object):

View File

@ -37,6 +37,12 @@ from settings import log, DEBUG, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI,
DEFAULT_SAT_NOMINA, DECIMALES_TAX, TITLE_APP, MV, DECIMALES_PRECIOS, \
DEFAULT_CFDIPAY, CURRENCY_MN
from settings import (
EXT,
MXN,
PATHS,
VALUES_PDF,
)
FORMAT = '{0:.2f}'
FORMAT3 = '{0:.3f}'
@ -97,9 +103,11 @@ def upload_file(rfc, opt, file_obj):
else:
return Facturas.import_cfdi(xml, sxml)
if opt == 'emisorlogo':
return Emisor.save_logo(file_obj)
result = util.upload_file(rfc, opt, file_obj)
if result['ok']:
names = ('bdfl', 'employees', 'nomina', 'products', 'invoiceods')
if not opt in names:
Configuracion.add({opt: file_obj.filename})
@ -765,6 +773,10 @@ class SATRegimenes(BaseModel):
def __str__(self):
return '{} ({})'.format(self.name, self.key)
@classmethod
def get_by_key(cls, key):
return SATRegimenes.get(SATRegimenes.key==key)
@classmethod
def get_(cls, ids):
if isinstance(ids, int):
@ -817,12 +829,30 @@ class Emisor(BaseModel):
registro_patronal = TextField(default='')
regimenes = ManyToManyField(SATRegimenes, related_name='emisores')
class Meta:
order_by = ('nombre',)
def __str__(self):
t = '{} ({})'
return t.format(self.nombre, self.rfc)
class Meta:
order_by = ('nombre',)
@classmethod
def save_logo(cls, file_obj):
result = {'status': 'error', 'ok': False}
name = file_obj.filename.split('.')
if name[-1].lower() != EXT['PNG']:
return result
emisor = Emisor.select()[0]
rfc = emisor.rfc.lower()
name = f'{rfc}.png'
path = util._join(PATHS['STATIC'], 'img', name)
if util.save_file(path, file_obj.file.read()):
emisor.logo = file_obj.filename
emisor.save()
result = {'status': 'server', 'name': file_obj.filename, 'ok': True}
return result
@classmethod
def get_(cls, rfc):
@ -850,6 +880,7 @@ class Emisor(BaseModel):
'emisor_telefono': obj.telefono,
'emisor_correo': obj.correo,
'emisor_web': obj.web,
'emisor_logo': obj.logo,
'es_escuela': obj.es_escuela,
'es_ong': obj.es_ong,
'ong_autorizacion': obj.autorizacion,
@ -1369,6 +1400,10 @@ class SATMonedas(BaseModel):
def __str__(self):
return 'Moneda: ({}) {}'.format(self.key, self.name)
@classmethod
def get_by_key(cls, key):
return SATMonedas.get(SATMonedas.key==key)
@classmethod
def get_multi_currency(cls):
count_currencies = len(cls.get_activos())
@ -1949,6 +1984,10 @@ class SATUsoCfdi(BaseModel):
def __str__(self):
return 'Uso del CFDI: {} ({})'.format(self.name, self.key)
@classmethod
def get_by_key(cls, key):
return SATUsoCfdi.get(SATUsoCfdi.key==key)
@classmethod
def actualizar(self, values):
id = int(values['id'])
@ -3674,26 +3713,40 @@ class Facturas(BaseModel):
return doc, name
def _get_concepto(self, node, currency):
d = util.get_dict(node.attrib)
concepto = {
'clave': f"{d['noidentificacion']}<BR>({d['claveprodserv']})",
'descripcion': self._get_description(self, node, d),
'unidad': f"{d['unidad']}<BR>({d['claveunidad']})",
'cantidad': d['cantidad'],
'valorunitario': util.format_currency(d['valorunitario'], currency),
'importe': util.format_currency(d['importe'], currency),
}
return concepto
def _get_description(self, node, data):
return data['descripcion']
def _get_others_values(self, invoice, emisor):
v_cancel = {True: 'inline', False: 'none'}
v_type = {'I': 'Ingreso', 'E': 'Egreso', 'T': 'Traslado'}
v_method = {
'PUE': 'Pago en una sola exhibición',
'PPD': 'Pago en parcialidades o diferido',
}
v_tax = {
'001': 'ISR',
'002': 'IVA',
'003': 'IEPS',
}
def _get_tax(self, node, data, currency):
type_tax = 'Traslado'
if 'Retenciones' in node.tag:
type_tax = 'Retención'
for n in node:
d = util.get_dict(n.attrib)
name = VALUES_PDF['TAX'].get(d['impuesto'])
tasa = FORMAT.format(float(d['tasaocuota']))
title = f"{type_tax} {name} {tasa}"
importe = util.format_currency(d['importe'], currency)
data['totales'].append((title, importe))
return
def _get_others_values(self, invoice, emisor):
data = {
'rfc': emisor.rfc.lower(),
'version': invoice.version,
'cancelada': v_cancel.get(invoice.cancelada),
'cancelada': VALUES_PDF['CANCEL'].get(invoice.cancelada),
'cfdi_notas': invoice.notas,
}
@ -3704,17 +3757,18 @@ class Facturas(BaseModel):
data.update({f'cfdi_{k.lower()}': v for k, v in d.items()})
data['cfdi_tipodecomprobante'] = v_type[data['cfdi_tipodecomprobante']]
data['cfdi_tipodecomprobante'] = \
VALUES_PDF['TYPE'][data['cfdi_tipodecomprobante']]
if data.get('cfdi_formapago', ''):
obj = SATFormaPago.get(SATFormaPago.key==invoice.forma_pago)
data['cfdi_formapago'] = str(obj)
data['cfdi_formapago'] = str(
SATFormaPago.get_by_key(data['cfdi_formapago']))
if data.get('cfdi_metodopago', ''):
data['cfdi_metodopago'] = 'Método de Pago: ({}) {}'.format(
data['cfdi_metodopago'], v_method[data['cfdi_metodopago']])
data['cfdi_metodopago'],
VALUES_PDF['METHOD'][data['cfdi_metodopago']])
obj = SATMonedas.get(SATMonedas.key==currency)
data['cfdi_moneda'] = str(obj)
data['cfdi_moneda'] = str(SATMonedas.get_by_key(currency))
data['cfdi_tipocambio'] = util.format_currency(
data['cfdi_tipocambio'], currency, 4)
data['cfdi_totalenletras'] = util.to_letters(
@ -3725,60 +3779,30 @@ class Facturas(BaseModel):
for node in xml:
if 'Emisor' in node.tag:
d = util.get_dict(node.attrib)
data.update(
{f'emisor_{k.lower()}': v for k, v in d.items()}
)
d = {f'emisor_{k.lower()}': v for k, v in node.attrib.items()}
data.update(d)
elif 'Receptor' in node.tag:
d = util.get_dict(node.attrib)
data.update(
{f'receptor_{k.lower()}': v for k, v in d.items()}
)
d = {f'receptor_{k.lower()}': v for k, v in node.attrib.items()}
data.update(d)
elif 'Conceptos' in node.tag:
data['conceptos'] = []
for subnode in node:
d = util.get_dict(subnode.attrib)
concepto = {
'clave': f"{d['noidentificacion']}<BR>({d['claveprodserv']})",
'descripcion': self._get_description(self, subnode, d),
'unidad': f"{d['unidad']}<BR>({d['claveunidad']})",
'cantidad': d['cantidad'],
'valorunitario': util.format_currency(d['valorunitario'], currency),
'importe': util.format_currency(d['importe'], currency),
}
concepto = self._get_concepto(self, subnode, currency)
data['conceptos'].append(concepto)
elif 'Impuestos' in node.tag:
for subnode in node:
if 'Traslados' in subnode.tag:
for t in subnode:
d = util.get_dict(t.attrib)
name = v_tax.get(d['impuesto'])
tasa = FORMAT.format(float(d['tasaocuota']))
title = f"Traslado {name} {tasa}"
importe = util.format_currency(d['importe'], currency)
data['totales'].append((title, importe))
elif 'Retenciones' in subnode.tag:
for r in subnode:
d = util.get_dict(r.attrib)
name = v_tax.get(d['impuesto'])
tasa = FORMAT.format(float(d['tasaocuota']))
title = f"Retención {name} {tasa}"
importe = util.format_currency(d['importe'], currency)
data['totales'].append((title, importe))
self._get_tax(self, subnode, data, currency)
elif 'Complemento' in node.tag:
for subnode in node:
if 'TimbreFiscalDigital' in subnode.tag:
d = util.get_dict(subnode.attrib)
data.update(
{f'timbre_{k.lower()}': v for k, v in d.items()}
)
for sn in node:
if 'TimbreFiscalDigital' in sn.tag:
d = {f'timbre_{k.lower()}': v for k, v in sn.attrib.items()}
data.update(d)
obj = SATRegimenes.get(SATRegimenes.key==data['emisor_regimenfiscal'])
data['emisor_regimenfiscal'] = str(obj)
data['emisor_logo'] = data['emisor_rfc'].lower()
obj = SATUsoCfdi.get(SATUsoCfdi.key==data['receptor_usocfdi'])
data['receptor_usocfdi'] = str(obj)
data['emisor_regimenfiscal'] = str(
SATRegimenes.get_by_key(data['emisor_regimenfiscal']))
data['receptor_usocfdi'] = str(
SATUsoCfdi.get_by_key(data['receptor_usocfdi']))
data['totales'].append((
'Total', util.format_currency(data['cfdi_total'], currency)))
@ -3786,7 +3810,16 @@ class Facturas(BaseModel):
data['timbre_cadenaoriginal'] = f"""||{data['timbre_version']}|
{data['timbre_uuid']}|{data['timbre_fechatimbrado']}|
{data['timbre_sellocfd']}|{data['timbre_nocertificadosat']}||"""
# ~ print(data)
qr_data = (
f"https://verificacfdi.facturaelectronica.sat.gob.mx/"
f"default.aspx?&id={data['timbre_uuid']}&re={data['emisor_rfc']}"
f"&rr={data['receptor_rfc']}&tt={data['cfdi_total']}"
f"&fe={data['cfdi_sello'][-8:]}"
)
cbb = util.get_qr(qr_data, False)
data['cbb'] = f'data:image/png;base64,{cbb}'
return data
@classmethod

View File

@ -47,13 +47,15 @@ except ImportError:
DEBUG = DEBUG
VERSION = '1.25.0'
VERSION = '1.26.0'
EMAIL_SUPPORT = ('soporte@empresalibre.net',)
TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION)
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
PATH_STATIC = os.path.abspath(os.path.join(BASE_DIR, '..'))
path_static = os.path.abspath(os.path.join(BASE_DIR, '..', 'static'))
# ~ 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'))
@ -185,3 +187,21 @@ DEFAULT_SAT_NOMINA = {
API = 'https://api.empresalibre.net{}'
CURRENCY_MN = 'MXN'
# ~ v2
EXT = {
'PNG': 'png',
}
MXN = 'MXN'
PATHS = {
'STATIC': path_static,
}
VALUES_PDF = {
'CANCEL': {True: 'inline', False: 'none'},
'TYPE': {'I': 'Ingreso', 'E': 'Egreso', 'T': 'Traslado'},
'TAX': {'001': 'ISR', '002': 'IVA', '003': 'IEPS'},
'METHOD': {
'PUE': 'Pago en una sola exhibición',
'PPD': 'Pago en parcialidades o diferido',
},
}

View File

@ -129,11 +129,12 @@ table.subtotal td{
}
body {
background-color: rgb(204,204,204);
font-family: 'Avenir Next';
}
#plantilla {
border: 1px solid #fff;
border-color: rgb(204,204,204);
background: #fff;
display: block;
margin: 0 auto;
@ -435,26 +436,26 @@ table.subtotal td{
float: left;
font-size: 10px;
line-height: 12px;
width: 85%;
width: 80%;
}
.tipocomite, .tipoproceso, .idcontabilidad{
float: right;
font-size: 10px;
line-height: 12px;
width: 30%;
width: 20%;
}
.sello{
margin: 20px 0;
margin: 10px 0;
}
.sello .cbb{
border: 1px solid #000;
height: auto;
margin-top: 20px;
margin-right: 5%;
width: 15%;
margin-top: 10px;
margin-right: 1%;
width: 18%;
}
.sello .cadenas-sello{
@ -462,7 +463,7 @@ table.subtotal td{
float: right;
font-size: 8px;
font-weight: bold;
line-height: 16px;
line-height: 15px;
width: 79%;
margin-left: 2px;
margin-right: 2px;
@ -479,20 +480,17 @@ table.subtotal td{
color: #7d1916;
}
.sello .cadena-original{
.cadena-original{
color: #000;
float: right;
font-size: 8px;
font-weight: bold;
line-height: 16px;
width: 100%;
margin: 10px;
margin-left: 5px;
margin-right: 2px;
line-height: 15px;
width: 99%;
margin: 5px;
word-break: break-all;
}
.sello .cadena-original div{
.cadena-original div{
font-weight: normal;
background-color: #dbc5c6;
color: #7d1916;
@ -502,7 +500,7 @@ table.subtotal td{
background-color: #dbc5c6;
color: #7d1916;
font-size: 10px;
line-height: 16px;
line-height: 15px;
text-align: center;
margin-right: 2px;
margin: 5px;
}

View File

@ -204,7 +204,7 @@ var emisor_otros_datos= [
{template: 'Generales', type: 'section'},
{cols: [
{view: 'search', id: 'emisor_logo', icon: 'file-image-o',
name: 'emisor_logo', label: 'Logotipo: '},
name: 'emisor_logo', label: 'Logotipo: ', placeholder: 'Solo formato PNG'},
{view: 'text', id: 'emisor_nombre_comercial',
name: 'emisor_nombre_comercial', label: 'Nombre comercial: '},
]},
@ -244,7 +244,7 @@ var emisor_otros_datos= [
{view: 'text', id: 'token_timbrado',
name: 'token_timbrado', label: 'Token de Timbrado: '},
{view: 'text', id: 'token_soporte',
name: 'token_soporte', label: 'Token para Respaldos: '},
name: 'token_soporte', label: 'Token de Soporte: '},
]

View File

@ -2,10 +2,10 @@
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="/static/css/${rfc}.css">
<link rel="stylesheet" type="text/css" href="/static/css/${rfc}.css">
<title>Empresa Libre</title>
</head>
<body>
<body onload="getQR();">
<div id="plantilla">
<div class="cancelada" style="display: ${cancelada}">
Cancelada
@ -122,6 +122,7 @@
</tr>
% endfor
</table>
<div class="clear"></div>
<div class="condiciones-pago">
${cfdi_condicionespago}
@ -153,8 +154,7 @@
<div class="clear"></div>
<div class="sello">
<img class="cbb" src="img/cbb.png" />
<img id="id_cbb" class="cbb" src="${cbb}" />
<div class="cadenas-sello">
<div class="cadena">
Sello Digital del CFDI:
@ -171,14 +171,13 @@
</div>
</div>
<div class="sello">
<div class="clear"></div>
<div class="cadena-original">
Cadena original del complemento de certificación digital del SAT:
<div>
${timbre_cadenaoriginal}
</div>
</div>
</div>
<div class="clear"></div>
<div class="rfc-pac">