Add new PAC

This commit is contained in:
Mauricio Baeza 2020-01-22 15:25:40 -06:00
commit 77b35f8322
14 changed files with 1864 additions and 1197 deletions

View File

@ -1,3 +1,29 @@
v 1.33.0 [22-ene-2020]
----------------------
- Mejora: Cambio del mensaje para cuando se intenta dar de alta un cliente ya existente.
- Mejora: Solo los admins pueden ver la nómina.
- Se agrega un segundo PAC
- Se actualizan los catálogos del SAT
* IMPORTANTE:
Es necesario actualizar los catálogos del SAT
```
git pull origin master
cd source/app/models
python main.py -bk
python main.py -us
```
v 1.32.0 [05-ene-2020]
----------------------
- Mejora: Recuperar facturas no aceptadas para cancelación por el receptor
v 1.31.2 [28-oct-2019] v 1.31.2 [28-oct-2019]
---------------------- ----------------------
- Error: Al generar PDF con tags en las series - Error: Al generar PDF con tags en las series

View File

@ -1 +1 @@
1.32.0 1.33.0

View File

@ -0,0 +1,3 @@
#!/usr/bin/env python3
from .comercio import PACComercioDigital

View File

@ -0,0 +1,338 @@
#!/usr/bin/env python
# ~
# ~ PAC
# ~ Copyright (C) 2018-2019 Mauricio Baeza Servin - public [AT] elmau [DOT] net
# ~
# ~ This program is free software: you can redistribute it and/or modify
# ~ it under the terms of the GNU General Public License as published by
# ~ the Free Software Foundation, either version 3 of the License, or
# ~ (at your option) any later version.
# ~
# ~ This program is distributed in the hope that it will be useful,
# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# ~ GNU General Public License for more details.
# ~
# ~ 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 logging
import lxml.etree as ET
import requests
from requests.exceptions import ConnectionError
from .conf import DEBUG, AUTH
LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s'
LOG_DATE = '%d/%m/%Y %H:%M:%S'
logging.addLevelName(logging.ERROR, '\033[1;41mERROR\033[1;0m')
logging.addLevelName(logging.DEBUG, '\x1b[33mDEBUG\033[1;0m')
logging.addLevelName(logging.INFO, '\x1b[32mINFO\033[1;0m')
logging.basicConfig(level=logging.DEBUG, format=LOG_FORMAT, datefmt=LOG_DATE)
log = logging.getLogger(__name__)
logging.getLogger('requests').setLevel(logging.ERROR)
TIMEOUT = 10
class PACComercioDigital(object):
ws = 'https://{}.comercio-digital.mx/{}'
api = 'https://app2.comercio-digital.mx/{}'
URL = {
'timbra': ws.format('ws', 'timbre/timbrarV5.aspx'),
'cancel': ws.format('cancela', 'cancela3/cancelarUuid'),
'cancelxml': ws.format('cancela', 'cancela3/cancelarXml'),
'client': api.format('x3/altaEmpresa'),
'saldo': api.format('x3/saldo'),
'timbres': api.format('x3/altaTimbres'),
}
CODES = {
'000': '000 Exitoso',
'004': '004 RFC {} ya esta dado de alta con Estatus=A',
'704': '704 Usuario Invalido',
}
NS_CFDI = {
'cfdi': 'http://www.sat.gob.mx/cfd/3',
'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
}
if DEBUG:
ws = 'https://pruebas.comercio-digital.mx/{}'
URL = {
'timbra': ws.format('timbre/timbrarV5.aspx'),
'cancel': ws.format('cancela3/cancelarUuid'),
'cancelxml': ws.format('cancela3/cancelarXml'),
'client': api.format('x3/altaEmpresa'),
'saldo': api.format('x3/saldo'),
'timbres': api.format('x3/altaTimbres'),
}
def __init__(self):
self.error = ''
self.cfdi_uuid = ''
self.date_stamped = ''
def _error(self, msg):
self.error = str(msg)
log.error(msg)
return
def _post(self, url, data, headers={}):
result = None
headers['host'] = url.split('/')[2]
headers['Content-type'] = 'text/plain'
headers['Connection'] = 'Keep-Alive'
try:
result = requests.post(url, data=data, headers=headers, timeout=TIMEOUT)
except ConnectionError as e:
self._error(e)
return result
def _validate_cfdi(self, xml):
"""
Comercio Digital solo soporta la declaración con doble comilla
"""
tree = ET.fromstring(xml.encode())
xml = ET.tostring(tree,
pretty_print=True, doctype='<?xml version="1.0" encoding="utf-8"?>')
return xml
def stamp(self, cfdi, auth={}):
if DEBUG or not auth:
auth = AUTH
url = self.URL['timbra']
headers = {
'usrws': auth['user'],
'pwdws': auth['pass'],
'tipo': 'XML',
}
cfdi = self._validate_cfdi(cfdi)
result = self._post(url, cfdi, headers)
if result is None:
return ''
if result.status_code != 200:
return ''
if 'errmsg' in result.headers:
self._error(result.headers['errmsg'])
return ''
xml = result.content
tree = ET.fromstring(xml)
self.cfdi_uuid = tree.xpath(
'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@UUID)',
namespaces=self.NS_CFDI)
self.date_stamped = tree.xpath(
'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@FechaTimbrado)',
namespaces=self.NS_CFDI)
return xml.decode()
def _get_data_cancel(self, cfdi, info, auth):
NS_CFDI = {
'cfdi': 'http://www.sat.gob.mx/cfd/3',
'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
}
tree = ET.fromstring(cfdi)
tipo = tree.xpath(
'string(//cfdi:Comprobante/@TipoDeComprobante)',
namespaces=NS_CFDI)
total = tree.xpath(
'string(//cfdi:Comprobante/@Total)',
namespaces=NS_CFDI)
rfc_emisor = tree.xpath(
'string(//cfdi:Comprobante/cfdi:Emisor/@Rfc)',
namespaces=NS_CFDI)
rfc_receptor = tree.xpath(
'string(//cfdi:Comprobante/cfdi:Receptor/@Rfc)',
namespaces=NS_CFDI)
uid = tree.xpath(
'string(//cfdi:Complemento/tdf:TimbreFiscalDigital/@UUID)',
namespaces=NS_CFDI)
data = (
f"USER={auth['user']}",
f"PWDW={auth['pass']}",
f"RFCE={rfc_emisor}",
f"UUID={uid}",
f"PWDK={info['pass']}",
f"KEYF={info['key']}",
f"CERT={info['cer']}",
f"TIPO={info['tipo']}",
f"ACUS=SI",
f"RFCR={rfc_receptor}",
f"TIPOC={tipo}",
f"TOTAL={total}",
)
return '\n'.join(data)
def cancel(self, cfdi, info, auth={}):
if not auth:
auth = AUTH
url = self.URL['cancel']
data = self._get_data_cancel(cfdi, info, auth)
result = self._post(url, data)
if result is None:
return ''
if result.status_code != 200:
return ''
if result.headers['codigo'] != '000':
self._error(result.headers['errmsg'])
return ''
return result.content
def _get_headers_cancel_xml(self, cfdi, info, auth):
NS_CFDI = {
'cfdi': 'http://www.sat.gob.mx/cfd/3',
'tdf': 'http://www.sat.gob.mx/TimbreFiscalDigital',
}
tree = ET.fromstring(cfdi)
tipo = tree.xpath(
'string(//cfdi:Comprobante/@TipoDeComprobante)',
namespaces=NS_CFDI)
total = tree.xpath(
'string(//cfdi:Comprobante/@Total)',
namespaces=NS_CFDI)
rfc_receptor = tree.xpath(
'string(//cfdi:Comprobante/cfdi:Receptor/@Rfc)',
namespaces=NS_CFDI)
headers = {
'usrws': auth['user'],
'pwdws': auth['pass'],
'rfcr': rfc_receptor,
'total': total,
'tipocfdi': tipo,
}
headers.update(info)
return headers
def cancel_xml(self, cfdi, xml, info, auth={}):
if not auth:
auth = AUTH
url = self.URL['cancelxml']
headers = self._get_headers_cancel_xml(cfdi, info, auth)
result = self._post(url, xml, headers)
if result is None:
return ''
if result.status_code != 200:
return ''
if result.headers['codigo'] != '000':
self._error(result.headers['errmsg'])
return ''
return result.content
def _get_data_client(self, auth, values):
data = [f"usr_ws={auth['user']}", f"pwd_ws={auth['pass']}"]
fields = (
'rfc_contribuyente',
'nombre_contribuyente',
'calle',
'noExterior',
'noInterior',
'colonia',
'localidad',
'municipio',
'estado',
'pais',
'cp',
'contacto',
'telefono',
'email',
'rep_nom',
'rep_rfc',
'email_fact',
'pwd_asignado',
)
data += [f"{k}={values[k]}" for k in fields]
return '\n'.join(data)
def client_add(self, data):
auth = AUTH
url = self.URL['client']
data = self._get_data_client(auth, data)
result = self._post(url, data)
if result is None:
return False
if result.status_code != 200:
self._error(f'Code: {result.status_code}')
return False
if result.text != self.CODES['000']:
self._error(result.text)
return False
return True
def client_balance(self, data):
url = self.URL['saldo']
host = url.split('/')[2]
headers = {
'Content-type': 'text/plain',
'Host': host,
'Connection' : 'Keep-Alive',
}
try:
result = requests.get(url, params=data, headers=headers, timeout=TIMEOUT)
except ConnectionError as e:
self._error(e)
return ''
if result.status_code != 200:
return ''
if result.text == self.CODES['704']:
self._error(result.text)
return ''
return result.text
def client_add_timbres(self, data, auth={}):
if not auth:
auth = AUTH
url = self.URL['timbres']
data = '\n'.join((
f"usr_ws={auth['user']}",
f"pwd_ws={auth['pass']}",
f"rfc_recibir={data['rfc']}",
f"num_timbres={data['timbres']}"
))
result = self._post(url, data)
if result is None:
return False
if result.status_code != 200:
self._error(f'Code: {result.status_code}')
return False
if result.text != self.CODES['000']:
self._error(result.text)
return False
return True

View File

@ -0,0 +1,40 @@
#!/usr/bin/env python
# ~
# ~ PAC
# ~ Copyright (C) 2018-2019 Mauricio Baeza Servin - public [AT] elmau [DOT] net
# ~
# ~ This program is free software: you can redistribute it and/or modify
# ~ it under the terms of the GNU General Public License as published by
# ~ the Free Software Foundation, either version 3 of the License, or
# ~ (at your option) any later version.
# ~
# ~ This program is distributed in the hope that it will be useful,
# ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
# ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# ~ GNU General Public License for more details.
# ~
# ~ You should have received a copy of the GNU General Public License
# ~ along with this program. If not, see <http://www.gnu.org/licenses/>.
# ~ Siempre consulta la documentación de Finkok
# ~ AUTH = Puedes usar credenciales genericas para timbrar, o exclusivas para
# ~ cada emisor
# ~ RESELLER = Algunos procesos como agregar emisores, solo pueden ser usadas
# ~ con una cuenta de reseller
DEBUG = True
AUTH = {
'user': '',
'pass': '',
}
if DEBUG:
AUTH = {
'user': 'AAA010101AAA',
'pass': 'PWD',
}

View File

@ -589,7 +589,7 @@ def timbra_xml(xml, auth):
if not DEBUG and not auth: if not DEBUG and not auth:
msg = 'Sin datos para timbrar' msg = 'Sin datos para timbrar'
result = {'ok': True, 'error': msg} result = {'ok': False, 'error': msg}
return result return result
result = {'ok': True, 'error': ''} result = {'ok': True, 'error': ''}
@ -2738,521 +2738,6 @@ def sync_cfdi(auth, files):
return return
class ImportFacturaLibreGambas(object):
def __init__(self, conexion, rfc):
self._rfc = rfc
self._con = None
self._cursor = None
self._error = ''
self._is_connect = self._connect(conexion)
self._clientes = []
self._clientes_rfc = []
@property
def error(self):
return self._error
@property
def is_connect(self):
return self._is_connect
def _validate_rfc(self):
sql = "SELECT rfc FROM emisor LIMIT 1"
self._cursor.execute(sql)
obj = self._cursor.fetchone()
if obj is None:
self._error = 'No se encontró al emisor: {}'.format(self._rfc)
return False
if not DEBUG:
if obj['rfc'] != self._rfc:
self._error = 'Los datos no corresponden al RFC: {}'.format(self._rfc)
return False
return True
def _connect(self, conexion):
import psycopg2
import psycopg2.extras
try:
self._con = psycopg2.connect(conexion)
self._cursor = self._con.cursor(cursor_factory=psycopg2.extras.DictCursor)
return self._validate_rfc()
except Exception as e:
log.error(e)
self._error = 'No se pudo conectar a la base de datos'
return False
def close(self):
try:
self._cursor.close()
self._con.close()
except:
pass
return
def import_data(self):
data = {}
tables = (
('receptores', 'Socios'),
('cfdifacturas', 'Facturas'),
('categorias', 'Categorias'),
('productos', 'Productos'),
('tickets', 'Tickets'),
)
for source, target in tables:
data[target] = self._get_table(source)
data['Socios'] += self._clientes
return data
def _get_table(self, table):
return getattr(self, '_{}'.format(table))()
def _tickets(self):
sql = "SELECT * FROM tickets"
self._cursor.execute(sql)
rows = self._cursor.fetchall()
fields = (
('serie', 'serie'),
('folio', 'folio'),
('fecha', 'fecha'),
('formadepago', 'forma_pago'),
('subtotal', 'subtotal'),
('descuento', 'descuento'),
('total', 'total'),
('notas', 'notas'),
('factura', 'factura'),
('cancelada', 'cancelado'),
('vendedor', 'vendedor'),
)
data = []
totals = len(rows)
for i, row in enumerate(rows):
msg = '\tImportando ticket {} de {}'.format(i+1, totals)
log.info(msg)
new = {t: row[s] for s, t in fields}
new['notas'] = ''
new['fecha'] = new['fecha'].replace(microsecond=0)
new['estatus'] = 'Generado'
if new['cancelado']:
new['estatus'] = 'Cancelado'
new['factura'] = self._get_invoice_ticket(new['factura'])
new['details'] = self._get_details_ticket(row['id'])
new['taxes'] = self._get_taxes_ticket(row['id'])
data.append(new)
return data
def _get_invoice_ticket(self, invoice):
if not invoice:
return None
sql = "SELECT serie, folio FROM cfdifacturas WHERE id=%s"
self._cursor.execute(sql, [invoice])
row = self._cursor.fetchone()
if row is None:
return {}
return dict(row)
def _get_details_ticket(self, id):
sql = "SELECT * FROM t_detalle WHERE id_cfdi=%s"
self._cursor.execute(sql, [id])
rows = self._cursor.fetchall()
fields = (
('descripcion', 'descripcion'),
('cantidad', 'cantidad'),
('valorunitario', 'valor_unitario'),
('importe', 'importe'),
('precio', 'precio_final'),
)
data = []
for row in rows:
new = {t: row[s] for s, t in fields if row[s]}
data.append(new)
return data
def _get_taxes_ticket(self, id):
sql = "SELECT * FROM t_impuestos WHERE id_cfdi=%s"
self._cursor.execute(sql, [id])
rows = self._cursor.fetchall()
tasas = {
'0': 0.0,
'16': 0.16,
'16.00': 0.16,
'0.16': 0.16,
'11': 0.11,
'-10': 0.10,
'-2': 0.02,
'-0.5': 0.005,
'-2/3': 0.106667,
'-10.6667': 0.106667,
'-10.6666': 0.106667,
'-10.666666': 0.106667,
'-10.66660': 0.106667,
}
data = []
for row in rows:
filtro = {
'name': row['impuesto'],
'tasa': tasas[row['tasa']],
'tipo': row['tipo'][0],
}
new = {
'import': row['importe'],
'filter': filtro
}
data.append(new)
return data
def _productos(self):
UNIDADES = {
'k': 'KGM',
'kg': 'KGM',
'kg.': 'KGM',
'pieza': 'H87',
'pza': 'H87',
'pz': 'H87',
'bulto': 'H87',
'b': 'H87',
'exb': 'H87',
'ex': 'H87',
'caja': 'XBX',
'c': 'XBX',
'rollo': 'XRO',
'tira': 'SR',
't': 'SR',
'cono': 'XAJ',
'paquete': 'XPK',
'pq': 'XPK',
}
sql = "SELECT * FROM productos"
self._cursor.execute(sql)
rows = self._cursor.fetchall()
fields = (
('id_categoria', 'categoria'),
('noidentificacion', 'clave'),
('descripcion', 'descripcion'),
# ~ ('unidad', 'unidad'),
('id_unidad', 'unidad'),
# ~ ('costo', 'ultimo_costo'),
('valorunitario', 'valor_unitario'),
# ~ ('existencia', 'existencia'),
# ~ ('minimo', 'minimo'),
('inventario', 'inventario'),
('codigobarras', 'codigo_barras'),
('cuentapredial', 'cuenta_predial'),
)
data = []
sql = """
SELECT nombre, tasa, tipo
FROM impuestos
WHERE id=%s
"""
totals = len(rows)
for i, row in enumerate(rows):
msg = '\tImportando producto {} de {}'.format(i+1, totals)
log.info(msg)
# ~ print (i, dict(row))
new = {t: row[s] for s, t in fields}
# ~ print (new['unidad'])
if new['unidad'] == 2:
new['unidad'] = 'servicio'
u = new['unidad'].lower().strip()
if u in ('sin',):
continue
if not u:
u = 'pieza'
if not new['categoria']:
new['categoria'] = None
new['codigo_barras'] = new['codigo_barras'] or ''
new['cuenta_predial'] = new['cuenta_predial'] or ''
new['descripcion'] = ' '.join(new['descripcion'].split())
new['clave_sat'] = DEFAULT_SAT_PRODUCTO
new['unidad'] = UNIDADES.get(u, new['unidad'])
self._cursor.execute(sql, [row['id_impuesto1']])
impuestos = self._cursor.fetchall()
new['impuestos'] = tuple(impuestos)
data.append(new)
return data
def _categorias(self):
sql = "SELECT * FROM categorias ORDER BY id_padre"
self._cursor.execute(sql)
rows = self._cursor.fetchall()
fields = (
('id', 'id'),
('categoria', 'categoria'),
('id_padre', 'padre'),
)
data = []
for row in rows:
new = {t: row[s] for s, t in fields}
if new['padre'] == 0:
new['padre'] = None
data.append(new)
return data
def _get_cliente(self, invoice):
sql = "SELECT rfc, nombre FROM receptores WHERE id=%s"
self._cursor.execute(sql, [invoice['id_cliente']])
obj = self._cursor.fetchone()
if not obj is None:
data = {
'rfc': obj['rfc'],
'slug': to_slug(obj['nombre']),
}
return data
if not invoice['xml']:
return {}
doc = parse_xml(invoice['xml'])
version = doc.attrib['version']
node = doc.find('{}Receptor'.format(PRE[version]))
rfc = node.attrib['rfc']
nombre = node.attrib['nombre']
tipo_persona = 1
if rfc == 'XEXX010101000':
tipo_persona = 4
elif rfc == 'XAXX010101000':
tipo_persona = 3
elif len(rfc) == 12:
tipo_persona = 2
data = {
'tipo_persona': tipo_persona,
'rfc': rfc,
'nombre': nombre,
'slug': to_slug(nombre),
'es_cliente': True,
'es_activo': False,
}
if not rfc in self._clientes_rfc:
self._clientes_rfc.append(rfc)
self._clientes.append(data)
data = {
'rfc': data['rfc'],
'slug': data['slug'],
}
return data
def _get_detalles(self, id):
sql = "SELECT * FROM cfdidetalle WHERE id_cfdi=%s"
self._cursor.execute(sql, [id])
rows = self._cursor.fetchall()
fields = (
('categoria', 'categoria'),
('cantidad', 'cantidad'),
('unidad', 'unidad'),
('noidentificacion', 'clave'),
('descripcion', 'descripcion'),
('valorunitario', 'valor_unitario'),
('importe', 'importe'),
('numero', 'pedimento'),
('fecha', 'fecha_pedimento'),
('aduana', 'aduana'),
('cuentapredial', 'cuenta_predial'),
('descuento', 'descuento'),
('precio', 'precio_final'),
)
data = []
for row in rows:
new = {t: row[s] for s, t in fields if row[s]}
data.append(new)
return data
def _get_impuestos(self, id):
sql = "SELECT * FROM cfdiimpuestos WHERE id_cfdi=%s"
self._cursor.execute(sql, [id])
rows = self._cursor.fetchall()
tasas = {
'0': 0.0,
'16': 0.16,
'16.00': 0.16,
'11': 0.11,
'-10': 0.10,
'-2': 0.02,
'-0.5': 0.005,
'-2/3': 0.106667,
'-10.6666': 0.106667,
'-10.666666': 0.106667,
'-10.66660': 0.106667,
'-4': 0.04,
}
data = []
for row in rows:
filtro = {
'name': row['impuesto'],
'tasa': tasas[row['tasa']],
'tipo': row['tipo'][0],
}
new = {
'importe': row['importe'],
'filtro': filtro
}
data.append(new)
return data
def _cfdifacturas(self):
sql = "SELECT * FROM cfdifacturas"
self._cursor.execute(sql)
rows = self._cursor.fetchall()
fields = (
('version', 'version'),
('serie', 'serie'),
('folio', 'folio'),
('fecha', 'fecha'),
('fecha_timbrado', 'fecha_timbrado'),
('formadepago', 'forma_pago'),
('condicionesdepago', 'condiciones_pago'),
('subtotal', 'subtotal'),
('descuento', 'descuento'),
('tipocambio', 'tipo_cambio'),
('moneda', 'moneda'),
('total', 'total'),
('tipodecomprobante', 'tipo_comprobante'),
('metododepago', 'metodo_pago'),
('lugarexpedicion', 'lugar_expedicion'),
('totalimpuestosretenidos', 'total_retenciones'),
('totalimpuestostrasladados', 'total_traslados'),
('xml', 'xml'),
('id_cliente', 'cliente'),
('notas', 'notas'),
('uuid', 'uuid'),
('cancelada', 'cancelada'),
)
data = []
totals = len(rows)
for i, row in enumerate(rows):
msg = '\tImportando factura {} de {}'.format(i+1, totals)
log.info(msg)
new = {t: row[s] for s, t in fields}
for _, f in fields:
new[f] = new[f] or ''
new['fecha'] = new['fecha'].replace(microsecond=0)
if new['fecha_timbrado']:
new['fecha_timbrado'] = new['fecha_timbrado'].replace(microsecond=0)
else:
new['fecha_timbrado'] = None
new['estatus'] = 'Timbrada'
if new['cancelada']:
new['estatus'] = 'Cancelada'
if not new['uuid']:
new['uuid'] = None
elif new['uuid'] in('ok', '123', '??', 'X'):
new['uuid'] = None
new['estatus'] = 'Cancelada'
new['cancelada'] = True
if new['xml'] is None:
new['xml'] = ''
new['pagada'] = True
new['total_mn'] = round(row['tipocambio'] * row['total'], 2)
new['detalles'] = self._get_detalles(row['id'])
new['impuestos'] = self._get_impuestos(row['id'])
new['cliente'] = self._get_cliente(row)
data.append(new)
return data
def _receptores(self):
sql = "SELECT * FROM receptores"
self._cursor.execute(sql)
rows = self._cursor.fetchall()
fields = (
('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'),
)
data = []
sql1 = "SELECT correo FROM correos WHERE id_padre=%s"
sql2 = "SELECT telefono FROM telefonos WHERE id_padre=%s"
totals = len(rows)
for i, row in enumerate(rows):
msg = '\tImportando cliente {} de {}'.format(i+1, totals)
log.info(msg)
new = {t: row[s] for s, t in fields}
new['slug'] = to_slug(new['nombre'])
new['es_cliente'] = True
if new['fecha_alta'] is None:
new['fecha_alta'] = str(now())
else:
new['fecha_alta'] = str(new['fecha_alta'])
for _, f in fields:
new[f] = new[f] or ''
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
self._cursor.execute(sql1, (row['id'],))
tmp = self._cursor.fetchall()
if tmp:
new['correo_facturas'] = ', '.join([r[0] for r in tmp])
self._cursor.execute(sql2, (row['id'],))
tmp = self._cursor.fetchall()
if tmp:
new['telefonos'] = ', '.join([r[0] for r in tmp])
data.append(new)
return data
class ImportFacturaLibre(object): class ImportFacturaLibre(object):
def __init__(self, path, rfc): def __init__(self, path, rfc):

View File

@ -19,10 +19,12 @@
import base64 import base64
import collections import collections
import datetime import datetime
import getpass
import json import json
import logging import logging
import math import math
import os import os
import shlex
import shutil import shutil
import smtplib import smtplib
import sqlite3 import sqlite3
@ -48,6 +50,9 @@ from dateutil import parser
import seafileapi import seafileapi
from settings import DEBUG, DB_COMPANIES, PATHS
from .comercio import PACComercioDigital
LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s' LOG_FORMAT = '%(asctime)s - %(levelname)s - %(message)s'
LOG_DATE = '%d/%m/%Y %H:%M:%S' LOG_DATE = '%d/%m/%Y %H:%M:%S'
@ -61,7 +66,15 @@ logging.getLogger('peewee').setLevel(logging.WARNING)
TIMEOUT = 10 TIMEOUT = 10
PATH_INVOICES = 'facturas' PATH_INVOICES = 'facturas'
PG_DUMP = 'pg_dump -U postgres'
PSQL = 'psql -U postgres'
if DEBUG:
PG_DUMP = 'pg_dump -h localhost -U postgres'
PSQL = 'psql -h localhost -U postgres'
PACS = {
'comercio': PACComercioDigital
}
#~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37 #~ https://github.com/kennethreitz/requests/blob/v1.2.3/requests/structures.py#L37
class CaseInsensitiveDict(collections.MutableMapping): class CaseInsensitiveDict(collections.MutableMapping):
@ -259,6 +272,16 @@ def _call(args):
return subprocess.check_output(args, shell=True).decode() return subprocess.check_output(args, shell=True).decode()
def _run(args, wait=False):
result = ''
cmd = shlex.split(args)
if wait:
result = subprocess.run(cmd, shell=True, check=True).stdout.decode()
else:
subprocess.run(cmd)
return result
def _join(*paths): def _join(*paths):
return os.path.join(*paths) return os.path.join(*paths)
@ -332,62 +355,142 @@ def to_zip(files):
return zip_buffer.getvalue() return zip_buffer.getvalue()
def db_delete(user, path): def dumps(data):
dt = datetime.datetime.now().strftime('%y%m%d_%H%M') return json.dumps(data, default=str)
path_bk = _join(path, 'tmp', '{}_{}.bk'.format(user, dt))
args = 'pg_dump -U postgres -Fc {} > "{}"'.format(user, path_bk)
try:
_call(args)
except:
pass
args = 'psql -U postgres -c "DROP DATABASE {0};"'.format(user)
try:
_call(args)
except:
pass
args = 'psql -U postgres -c "DROP ROLE {0};"'.format(user) def loads(data):
try: return json.loads(data)
_call(args)
except:
pass def json_loads(path):
return json.loads(open(path, 'r').read())
def _validate_db_rfc():
con = sqlite3.connect(DB_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 return
def _sql_companies(sql, args=()):
_validate_db_rfc()
con = sqlite3.connect(DB_COMPANIES)
cursor = con.cursor()
try:
cursor.execute(sql, args)
data = cursor.fetchall()
except sqlite3.IntegrityError as e:
log.error(e)
return False
con.commit()
cursor.close()
con.close()
return data
def get_data_con(rfc):
data = rfc_get(rfc)[0][0]
return loads(data)
def rfc_get(rfc=''):
if rfc:
sql = "SELECT con FROM names WHERE rfc = ?"
w = (rfc,)
else:
w = ()
sql = "SELECT * FROM names"
data = _sql_companies(sql, w)
return data
def rfc_exists(rfc):
sql = "SELECT rfc FROM names WHERE rfc = ?"
data = _sql_companies(sql, (rfc,))
if isinstance(data, bool):
return
return bool(data)
def rfc_add(rfc, con):
sql = "INSERT INTO names VALUES (?, ?)"
data = _sql_companies(sql, (rfc.upper(), dumps(con)))
return True
def db_create(user):
args = f'{PSQL} -c "CREATE ROLE {user} WITH LOGIN ENCRYPTED PASSWORD \'{user}\';"'
_run(args)
args = f'{PSQL} -c "CREATE DATABASE {user} WITH OWNER {user};"'
_run(args)
return True
def db_delete(user, path, no_database=False):
sql = "DELETE FROM names WHERE rfc = ?"
data = _sql_companies(sql, (user,))
if no_database:
return True
user = user.replace('&', '').lower()
dt = now().strftime('%y%m%d_%H%M')
path_bk = _join(path, f'{user}_{dt}.bk')
args = f'{PG_DUMP} -d {user} -Fc -f "{path_bk}"'
_run(args)
args = f'{PSQL} -c "DROP DATABASE {user};"'
_run(args)
args = f'{PSQL} -c "DROP ROLE {user};"'
_run(args)
return True
def _get_pass(rfc): def _get_pass(rfc):
return rfc return rfc
def _backup_db(rfc, data, path_bk, is_mv, url_seafile): def _backup_db(rfc, is_mv, url_seafile):
if data['type'] != 'postgres': log.info(f'Generando backup de: {rfc.upper()}')
return bk_name = f'{rfc}.bk'
path = _join(PATHS['BK'], bk_name)
log.info('Generando backup de: {}'.format(rfc)) args = f'{PG_DUMP} -d {rfc} -Fc -f "{path}"'
bk_name = '{}.bk'.format(rfc.lower()) _run(args)
path_db = _join(path_bk, bk_name)
args = 'pg_dump -U postgres -Fc {} > "{}"'.format(data['name'], path_db)
result = _call(args)
log.info('\tBackup local generado...') log.info('\tBackup local generado...')
if is_mv: if is_mv:
path_target = _join(Path.home(), PATH_INVOICES) path_target = _validate_path_local()
if Path(path_target).exists(): if path_target:
path_target = _join(path_target, bk_name) path_target = _join(path_target, bk_name)
shutil.copy(path_db, path_target) shutil.copy(path, path_target)
else: else:
log.error('\tNo existe la carpeta compartida...') log.error('\tNo existe la carpeta compartida...')
sql = 'select correo_timbrado, token_soporte from emisor;' # ~ sql = 'select correo_timbrado, token_soporte from emisor;'
args = 'psql -U postgres -d {} -Atc "{}"'.format(data['name'], sql) # ~ args = 'psql -U postgres -d {} -Atc "{}"'.format(data['name'], sql)
result = _call(args) # ~ result = _call(args)
if not result: # ~ if not result:
log.error('\tSin datos para backup remoto') # ~ log.error('\tSin datos para backup remoto')
return # ~ return
# ~ data = result.strip().split('|')
# ~ if not data[1]:
# ~ log.error('\tSin token de soporte')
# ~ return
data = result.strip().split('|')
if not data[1]:
log.error('\tSin token de soporte')
return
# ~ email = data[0] # ~ email = data[0]
# ~ uuid = data[1] # ~ uuid = data[1]
# ~ email = 'hola@elmau.net' # ~ email = 'hola@elmau.net'
@ -401,22 +504,47 @@ def _backup_db(rfc, data, path_bk, is_mv, url_seafile):
return return
def db_backup(path_companies, path_bk, is_mv, url_seafile): def db_backup(is_mv, url_seafile):
con = sqlite3.connect(path_companies) data = rfc_get()
cursor = con.cursor() if not len(data):
sql = "SELECT * FROM names" msg = 'Sin bases de datos a respaldar'
cursor.execute(sql) log.info(msg)
rows = cursor.fetchall()
if rows is None:
return return
cursor.close()
con.close()
for rfc, data in rows: for rfc, _ in data:
_backup_db(rfc, json.loads(data), path_bk, is_mv, url_seafile) _backup_db(rfc.lower(), is_mv, url_seafile)
return return
def _validate_path_local():
path_bk = _join(str(Path.home()), PATHS['LOCAL'])
if not os.path.isdir(path_bk):
path_bk = ''
return path_bk
def db_backup_local():
path_bk = _validate_path_local()
if not path_bk:
msg = 'No existe la carpeta local'
return {'ok': False, 'msg': msg}
data = rfc_get()
if not len(data):
msg = 'Sin bases de datos a respaldar'
return {'ok': False, 'msg': msg}
for row in data:
user = row[0].lower()
db = loads(row[1])['name']
path = _join(path_bk, '{}.bk'.format(user))
args = f'{PG_DUMP} -d {user} -Fc -f "{path}"'
_run(args)
msg = 'Bases de datos respaldadas correctamente'
result = {'ok': True, 'msg': msg}
return result
def now(): def now():
return datetime.datetime.now().replace(microsecond=0) return datetime.datetime.now().replace(microsecond=0)
@ -424,3 +552,42 @@ def now():
def get_days(date): def get_days(date):
return (now() - date).days return (now() - date).days
def get_pass():
pass1 = getpass.getpass('Introduce la contraseña: ')
pass2 = getpass.getpass('Confirma la contraseña: ')
if pass1 != pass2:
msg = 'Las contraseñas son diferentes'
return False, msg
password = pass1.strip()
if not password:
msg = 'La contraseña es necesaria'
return False, msg
return True, password
def xml_stamp(xml, auth, name):
if not DEBUG and not auth:
msg = 'Sin datos para timbrar'
result = {'ok': False, 'error': msg}
return result
result = {'ok': True, 'error': ''}
auth = {'user': auth['USER'], 'pass': auth['PASS']}
pac = PACS[name]()
xml_stamped = pac.stamp(xml, auth)
if not xml_stamped:
result['ok'] = False
result['error'] = pac.error
return result
result['xml'] = xml_stamped
result['uuid'] = pac.cfdi_uuid
result['fecha'] = pac.date_stamped
return result

View File

@ -34,19 +34,18 @@ class StorageEngine(object):
return main.CfdiNomina.get_by(values) return main.CfdiNomina.get_by(values)
def empresa_agregar(self, values): def empresa_agregar(self, values):
return main.empresa_agregar(values['alta_rfc'], False) # ~ return main.empresa_agregar(values['alta_rfc'], False)
return main._new_client(values['alta_rfc'], False)
def empresa_borrar(self, values): def empresa_borrar(self, values):
return main.empresa_borrar(values['rfc']) # ~ return main.empresa_borrar(values['rfc'])
return main._delete_client(values['rfc'], False, False)
def respaldar_dbs(self): def respaldar_dbs(self):
return main.respaldar_dbs() return main.respaldar_dbs()
def _get_empresas(self, values):
return main.get_empresas()
def get_values(self, table, values=None, session=None): def get_values(self, table, values=None, session=None):
if table in ('allusuarios', 'usuarioupdate'): if table in ('allusuarios', 'usuarioupdate', 'main'):
return getattr(self, '_get_{}'.format(table))(values, session) return getattr(self, '_get_{}'.format(table))(values, session)
return getattr(self, '_get_{}'.format(table))(values) return getattr(self, '_get_{}'.format(table))(values)
@ -71,8 +70,8 @@ class StorageEngine(object):
def _get_importinvoice(self, values): def _get_importinvoice(self, values):
return main.import_invoice() return main.import_invoice()
def _get_main(self, values): def _get_main(self, values, session):
return main.config_main() return main.config_main(session['userobj'])
def _get_configtimbrar(self, values): def _get_configtimbrar(self, values):
return main.config_timbrar() return main.config_timbrar()
@ -463,3 +462,7 @@ class StorageEngine(object):
def nomina(self, values, user): def nomina(self, values, user):
return main.CfdiNomina.post(values, user) return main.CfdiNomina.post(values, user)
# Companies only in MV
def _get_empresas(self, values):
return main.companies_get()

View File

@ -16,9 +16,9 @@
# ~ You should have received a copy of the GNU General Public License # ~ You should have received a copy of the GNU General Public License
# ~ along with this program. If not, see <http://www.gnu.org/licenses/>. # ~ along with this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
from decimal import Decimal from decimal import Decimal
import sqlite3 import sqlite3
import click
from peewee import * from peewee import *
from playhouse.fields import PasswordField, ManyToManyField from playhouse.fields import PasswordField, ManyToManyField
from playhouse.shortcuts import case, SQL, cast from playhouse.shortcuts import case, SQL, cast
@ -31,7 +31,7 @@ if __name__ == '__main__':
from controllers import util from controllers import util
from settings import log, DEBUG, COMPANIES, VERSION, PATH_CP, PRE, CURRENT_CFDI, \ from settings import log, COMPANIES, VERSION, PATH_CP, PRE, CURRENT_CFDI, \
INIT_VALUES, DEFAULT_PASSWORD, DECIMALES, IMPUESTOS, DEFAULT_SAT_PRODUCTO, \ INIT_VALUES, DEFAULT_PASSWORD, DECIMALES, IMPUESTOS, DEFAULT_SAT_PRODUCTO, \
CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, CURRENT_CFDI_NOMINA, \ CANCEL_SIGNATURE, PUBLIC, DEFAULT_SERIE_TICKET, CURRENT_CFDI_NOMINA, \
DEFAULT_SAT_NOMINA, DECIMALES_TAX, TITLE_APP, MV, DECIMALES_PRECIOS, \ DEFAULT_SAT_NOMINA, DECIMALES_TAX, TITLE_APP, MV, DECIMALES_PRECIOS, \
@ -40,6 +40,7 @@ from settings import log, DEBUG, COMPANIES, VERSION, PATH_CP, PRE, CURRENT_CFDI,
# ~ v2 # ~ v2
from controllers import utils from controllers import utils
from settings import ( from settings import (
DEBUG,
DB_COMPANIES, DB_COMPANIES,
EXT, EXT,
IS_MV, IS_MV,
@ -249,14 +250,18 @@ def get_doc(type_doc, id, rfc):
return data, file_name, content_type return data, file_name, content_type
def config_main(): def config_main(user):
try: try:
obj = Emisor.select()[0] obj = Emisor.select()[0]
except IndexError: except IndexError:
obj = None obj = None
punto_de_venta = Configuracion.get_bool('chk_usar_punto_de_venta') punto_de_venta = Configuracion.get_bool('chk_usar_punto_de_venta')
nomina = Configuracion.get_bool('chk_usar_nomina') nomina = Configuracion.get_bool('chk_usar_nomina')
if not user.es_admin:
nomina = False
data = { data = {
'empresa': get_title_app(3), 'empresa': get_title_app(3),
'punto_de_venta': punto_de_venta, 'punto_de_venta': punto_de_venta,
@ -426,6 +431,7 @@ class Configuracion(BaseModel):
'chk_config_pagos', 'chk_config_pagos',
'chk_config_divisas', 'chk_config_divisas',
'chk_cfg_pays_data_bank', 'chk_cfg_pays_data_bank',
'chk_usar_nomina',
) )
data = (Configuracion data = (Configuracion
.select() .select()
@ -434,6 +440,9 @@ class Configuracion(BaseModel):
values = {r.clave: util.get_bool(r.valor) for r in data} values = {r.clave: util.get_bool(r.valor) for r in data}
fields = ( fields = (
'txt_ticket_printer',
'txt_config_nomina_serie',
'txt_config_nomina_folio',
'txt_config_cfdipay_serie', 'txt_config_cfdipay_serie',
'txt_config_cfdipay_folio', 'txt_config_cfdipay_folio',
) )
@ -475,6 +484,17 @@ class Configuracion(BaseModel):
values = {r.clave: util.get_bool(r.valor) for r in data} values = {r.clave: util.get_bool(r.valor) for r in data}
return values return values
@classmethod
def get_value(cls, key, default=''):
value = default
data = (Configuracion
.select(Configuracion.valor)
.where(Configuracion.clave == key)
)
if data:
value = data[0].valor
return value
@classmethod @classmethod
def get_(cls, keys): def get_(cls, keys):
if isinstance(keys, str): if isinstance(keys, str):
@ -535,7 +555,6 @@ class Configuracion(BaseModel):
'chk_ticket_direct_print', 'chk_ticket_direct_print',
'chk_ticket_edit_cant', 'chk_ticket_edit_cant',
'chk_ticket_total_up', 'chk_ticket_total_up',
'chk_usar_nomina',
) )
data = (Configuracion data = (Configuracion
.select() .select()
@ -543,15 +562,10 @@ class Configuracion(BaseModel):
) )
values = {r.clave: util.get_bool(r.valor) for r in data} values = {r.clave: util.get_bool(r.valor) for r in data}
fields = ( fields = (
'txt_ticket_printer', ('lst_pac', 'default'),
'txt_config_nomina_serie',
'txt_config_nomina_folio',
'txt_config_cfdipay_serie',
'txt_config_cfdipay_folio',
) )
# ~ tp = 'txt_ticket_printer' for k, d in fields:
for f in fields: values[k] = Configuracion.get_value(k, d)
values[f] = Configuracion.get_(f)
return values return values
if keys['fields'] == 'path_cer': if keys['fields'] == 'path_cer':
@ -2632,10 +2646,17 @@ class Socios(BaseModel):
def add(cls, values): def add(cls, values):
accounts = util.loads(values.pop('accounts', '[]')) accounts = util.loads(values.pop('accounts', '[]'))
fields = cls._clean(cls, values) fields = cls._clean(cls, values)
w = ((Socios.rfc==fields['rfc']) & (Socios.slug==fields['slug']))
if Socios.select().where(w).exists():
msg = 'Ya existe el RFC y Razón Social'
data = {'ok': False, 'row': {}, 'new': True, 'msg': msg}
return data
try: try:
obj = Socios.create(**fields) obj = Socios.create(**fields)
except IntegrityError as e: except IntegrityError as e:
msg = 'Ya existe el RFC y Razón Social' msg = 'Ocurrio un error, al dar de alta el emisor'
data = {'ok': False, 'row': {}, 'new': True, 'msg': msg} data = {'ok': False, 'row': {}, 'new': True, 'msg': msg}
return data return data
@ -4987,10 +5008,15 @@ class Facturas(BaseModel):
obj.save() obj.save()
enviar_correo = util.get_bool(Configuracion.get_('correo_directo')) enviar_correo = util.get_bool(Configuracion.get_('correo_directo'))
pac = Configuracion.get_('lst_pac').lower()
anticipo = False anticipo = False
msg = 'Factura timbrada correctamente' msg = 'Factura timbrada correctamente'
result = util.timbra_xml(obj.xml, auth) if pac:
result = utils.xml_stamp(obj.xml, auth, pac)
else:
result = util.timbra_xml(obj.xml, auth)
if result['ok']: if result['ok']:
obj.xml = result['xml'] obj.xml = result['xml']
obj.uuid = result['uuid'] obj.uuid = result['uuid']
@ -8996,7 +9022,7 @@ def _init_values(rfc):
pass pass
if not Certificado.select().count(): if not Certificado.select().count():
Certificado.create(rfc=rfc) Certificado.create(rfc=rfc.upper())
log.info('Valores iniciales insertados...') log.info('Valores iniciales insertados...')
return return
@ -9234,64 +9260,6 @@ def _migrate_tables(rfc=''):
return return
def _agregar_superusuario():
args = util.get_con()
if not args:
return
conectar(args)
usuario = input('Introduce el nuevo nombre para el superusuario: ').strip()
if not usuario:
msg = 'El nombre de usuario es requerido'
log.erro(msg)
return
ok, contraseña = util.get_pass()
if not ok:
log.error(contraseña)
return
try:
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
def _cambiar_contraseña():
args = util.get_con()
if not args:
return
conectar(args)
usuario = input('Introduce el nombre de usuario: ').strip()
if not usuario:
msg = 'El nombre de usuario es requerido'
log.error(msg)
return
try:
obj = Usuarios.get(usuario=usuario)
except Usuarios.DoesNotExist:
msg = 'El usuario no existe'
log.error(msg)
return
ok, contraseña = util.get_pass()
if not ok:
log.error(contraseña)
return
obj.contraseña = contraseña
obj.save()
log.info('Contraseña cambiada correctamente...')
return
def _add_emisor(rfc, args): def _add_emisor(rfc, args):
util._valid_db_companies() util._valid_db_companies()
con = sqlite3.connect(COMPANIES) con = sqlite3.connect(COMPANIES)
@ -9349,49 +9317,20 @@ def _iniciar_bd():
return return
def _agregar_rfc(no_bd):
rfc = input('Introduce el nuevo RFC: ').strip().upper()
if not rfc:
msg = 'El RFC es requerido'
log.error(msg)
return
datos = input('Introduce los datos de conexión: ').strip()
if not datos:
msg = 'Los datos de conexión son requeridos'
log.error(msg)
return
opt = util.parse_con(datos)
if not opt:
log.error('Datos de conexión incompletos')
return
args = opt.copy()
if conectar(args):
if _add_emisor(rfc, util.dumps(opt)):
if no_bd:
log.info('RFC agregado correctamente...')
return
_crear_tablas(rfc)
log.info('RFC agregado correctamente...')
return
log.error('No se pudo agregar el RFC')
return
def _borrar_rfc(): def _borrar_rfc():
rfc = input('Introduce el RFC a borrar: ').strip().upper() rfc = input('Introduce el RFC a borrar: ').strip().lower()
if not rfc: if not rfc:
msg = 'El RFC es requerido' msg = 'El RFC es requerido'
log.error(msg) log.error(msg)
return return
confirm = input('¿Estás seguro de borrar el RFC?') confirm = input('¿Estás seguro de borrar el RFC? [si]')
if confirm != 'si':
log.info('Proceso cancelado...')
return
if _delete_emisor(rfc): if _delete_emisor(rfc.upper()):
util.delete_db(rfc.lower()) utils.db_delete(rfc, PATHS['BK'])
log.info('RFC borrado correctamente...') log.info('RFC borrado correctamente...')
return return
@ -9460,9 +9399,7 @@ def empresa_agregar(rfc, no_bd):
def empresa_borrar(rfc): def empresa_borrar(rfc):
if _delete_emisor(rfc): utils.db_delete(rfc.lower(), PATHS['BK'])
# ~ util.delete_db(rfc.lower())
utils.db_delete(rfc.lower(), PATHS['DOCS'])
return True return True
@ -9471,21 +9408,8 @@ def respaldar_dbs():
msg = 'Solo MV' msg = 'Solo MV'
return {'ok': False, 'msg': msg} return {'ok': False, 'msg': msg}
result = util.validate_path_bk() result = utils.db_backup_local()
if not result['ok']: return result
return result
path_bk = result['msg']
data = util.get_rfcs()
if not len(data):
msg = 'Sin bases de datos a respaldar'
return {'ok': False, 'msg': msg}
for row in data:
util.respaldar_db(row, path_bk)
msg = 'Bases de datos respaldadas correctamente'
return {'ok': True, 'msg': msg}
def _importar_valores(archivo='', rfc=''): def _importar_valores(archivo='', rfc=''):
@ -9515,7 +9439,7 @@ def _importar_valores(archivo='', rfc=''):
try: try:
with database_proxy.atomic() as txn: with database_proxy.atomic() as txn:
table.create(**r) table.create(**r)
except IntegrityError: except:
pass pass
log.info('Importación terminada...') log.info('Importación terminada...')
@ -9776,37 +9700,6 @@ def _importar_factura_libre(archivo):
return return
def _importar_factura_libre_gambas(conexion):
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.ImportFacturaLibreGambas(conexion, rfc)
if not app.is_connect:
log.error('\t{}'.format(app._error))
return
data = app.import_data()
_importar_socios(data['Socios'])
_importar_facturas(data['Facturas'])
_importar_categorias(data['Categorias'])
_importar_productos_gambas(data['Productos'])
_import_tickets(data['Tickets'])
log.info('Importación terminada...')
return
def _exist_ticket(row): def _exist_ticket(row):
filters = ( filters = (
(Tickets.serie==row['serie']) & (Tickets.serie==row['serie']) &
@ -9856,44 +9749,6 @@ def _import_tickets(rows):
return return
def _importar_productos_gambas(rows):
log.info('Importando productos...')
KEYS = {
'Exento': '000',
'ISR': '001',
'IVA': '002',
}
totals = len(rows)
for i, row in enumerate(rows):
msg = '\tGuardando producto {} de {}'.format(i+1, totals)
log.info(msg)
source_taxes = row.pop('impuestos')
row['unidad'] = SATUnidades.get(SATUnidades.key==row['unidad'])
taxes = []
for tax in source_taxes:
w = {
'key': KEYS[tax[0]],
'name': tax[0],
'tasa': float(tax[1]),
'tipo': tax[2][0],
}
taxes.append(SATImpuestos.get_o_crea(w))
with database_proxy.transaction():
try:
obj = Productos.create(**row)
obj.impuestos = taxes
except IntegrityError as e:
msg = '\tProducto ya existe'
log.info(msg)
log.info('Importación terminada...')
return
def _importar_productos(archivo): def _importar_productos(archivo):
rfc = input('Introduce el RFC: ').strip().upper() rfc = input('Introduce el RFC: ').strip().upper()
if not rfc: if not rfc:
@ -10074,183 +9929,406 @@ def _exportar_documentos():
return return
def _test(): # ~ v2
rfc = input('Introduce el RFC: ').strip().upper()
def companies_get():
data = utils.rfc_get()
rows = []
for row in data:
rows.append({'delete': '-', 'rfc': row[0].upper()})
return tuple(rows)
def _list_clients():
rows = utils.rfc_get()
for row in rows:
msg = f'RFC: {row[0].upper()}'
print(msg)
return
def _new_client(rfc, no_database):
rfc = rfc.lower()
if not rfc: if not rfc:
msg = 'El RFC es requerido' log.error('Falta el RFC')
return
if utils.rfc_exists(rfc):
msg = 'El RFC ya esta dado de alta'
log.error(msg)
return {'ok': False, 'msg': msg}
user = rfc.replace('&', '').lower()
if not no_database:
if not utils.db_create(user):
msg = 'No se pudo crear la base de datos'
log.error(msg)
return {'ok': False, 'msg': msg}
args = {
'type': 'postgres',
'name': user,
'user': user,
'password': user,
}
if not conectar(args.copy()):
msg = 'No se pudo conectar a la base de datos'
log.error(msg)
return {'ok': False, 'msg': msg}
if not utils.rfc_add(rfc, args):
msg = 'No se pudo guardar el nuevo emisor'
log.error(msg)
return {'ok': False, 'msg': msg}
if not no_database:
if not _crear_tablas(rfc):
msg = 'No se pudo crear las tablas'
log.error(msg)
return {'ok': False, 'msg': msg}
desconectar()
msg = 'Emisor dado de alta correctamente'
row = {'delete': '-', 'rfc': rfc.upper()}
result = {'ok': True, 'msg': msg, 'row': row}
return result
def _delete_client(rfc, no_database, ask=True):
rfc = rfc.lower()
if not rfc:
log.error('Falta el RFC')
return
if not utils.rfc_exists(rfc):
msg = 'El RFC no esta dado de alta'
log.error(msg)
return {'ok': False, 'msg': msg}
if ask:
confirm = input('¿Estás seguro de borrar el RFC? [si]')
if confirm != 'si':
log.info('Proceso cancelado...')
return False
if utils.db_delete(rfc, PATHS['BK'], no_database):
log.info('RFC borrado correctamente...')
result = True
else:
log.error('No se pudo borrar el RFC')
result = False
return result
def _update_sat():
clients = utils.rfc_get()
tables = utils.json_loads(PATHS['SAT'])
for rfc, con in clients:
log.info(f'Importando datos en: {rfc.upper()}')
conectar(utils.loads(con))
for table in tables:
t = globals()[table['tabla']]
with database_proxy.atomic():
for r in table['datos']:
try:
t.get_or_create(**r)
except:
pass
log.info(f"\tTabla importada: {table['tabla']}")
desconectar()
log.info('Importación terminada...')
return
def _new_superuser(rfc):
rfc = rfc.lower()
if not rfc:
log.error('Falta el RFC')
return
if not utils.rfc_exists(rfc):
msg = 'El RFC no esta dado de alta'
log.error(msg) log.error(msg)
return return
args = util.get_con(rfc) args = utils.get_data_con(rfc)
if not args:
user = input('Introduce el nuevo nombre para el superusuario: ').strip()
if not user:
msg = 'El nombre de usuario es requerido'
log.error(msg)
return
ok, password = utils.get_pass()
if not ok:
log.error(password)
return return
conectar(args) conectar(args)
try:
obj = Usuarios.create(
usuario=user, contraseña=password, es_superusuario=True)
msg = 'SuperUsuario creado correctamente...'
log.info(msg)
except IntegrityError:
msg = 'El usuario ya existe'
log.error(msg)
desconectar()
return
def _change_pass(rfc):
rfc = rfc.lower()
if not rfc:
log.error('Falta el RFC')
return
if not utils.rfc_exists(rfc):
msg = 'El RFC no esta dado de alta'
log.error(msg)
return
args = utils.get_data_con(rfc)
conectar(args)
user = input('Introduce el nombre de usuario: ').strip()
if not user:
msg = 'El nombre de usuario es requerido'
log.error(msg)
desconectar()
return
try:
obj = Usuarios.get(usuario=user)
except Usuarios.DoesNotExist:
msg = 'El usuario no existe'
log.error(msg)
desconectar()
return
ok, password = utils.get_pass()
if not ok:
log.error(password)
desconectar()
return
obj.contraseña = password
obj.save()
desconectar()
log.info('Contraseña cambiada correctamente...')
return
def _process_command_line_arguments():
parser = argparse.ArgumentParser(
description='Empresa Libre')
parser.add_argument('-lc', '--list-clients', dest='list_clients',
action='store_true', default=False, required=False)
parser.add_argument('-nc', '--new-client', dest='new_client',
action='store_true', default=False, required=False)
parser.add_argument('-dc', '--delete-client', dest='delete_client',
action='store_true', default=False, required=False)
parser.add_argument('-ndb', '--no-database', dest='no_database',
action='store_true', default=False, required=False)
parser.add_argument('-m', '--migrate', dest='migrate',
action='store_true', default=False, required=False)
parser.add_argument('-us', '--update-sat', dest='update_sat',
action='store_true', default=False, required=False)
parser.add_argument('-ns', '--new-superuser', dest='new_superuser',
action='store_true', default=False, required=False)
parser.add_argument('-cp', '--change-pass', dest='change_pass',
action='store_true', default=False, required=False)
parser.add_argument('-bk', '--backup', dest='backup',
action='store_true', default=False, required=False)
parser.add_argument('-r', '--rfc', dest='rfc', default='')
return parser.parse_args()
def main(args):
if args.list_clients:
_list_clients()
return
if args.new_client:
_new_client(args.rfc, args.no_database)
return
if args.delete_client:
_delete_client(args.rfc, args.no_database)
return
if args.migrate:
_migrate_tables(args.rfc)
return
if args.update_sat:
_update_sat()
return
if args.new_superuser:
_new_superuser(args.rfc)
return
if args.change_pass:
_change_pass(args.rfc)
return
if args.backup:
utils.db_backup(IS_MV, URL['SEAFILE'])
return return
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) # ~ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
help_create_tables = 'Crea las tablas en la base de datos' # ~ help_create_tables = 'Crea las tablas en la base de datos'
help_migrate_db = 'Migra las tablas en la base de datos' # ~ help_migrate_db = 'Migra las tablas en la base de datos'
help_superuser = 'Crea un nuevo super usuario' # ~ help_superuser = 'Crea un nuevo super usuario'
help_change_pass = 'Cambia la contraseña a un usuario' # ~ help_change_pass = 'Cambia la contraseña a un usuario'
help_rfc = 'Agrega un nuevo RFC' # ~ help_rfc = 'Agrega un nuevo RFC'
help_br = 'Elimina un RFC' # ~ help_br = 'Elimina un RFC'
help_lr = 'Listar RFCs' # ~ help_lr = 'Listar RFCs'
@click.command(context_settings=CONTEXT_SETTINGS) # ~ @click.command(context_settings=CONTEXT_SETTINGS)
@click.option('-bd', '--iniciar-bd',help=help_create_tables, # ~ @click.option('-bd', '--iniciar-bd',help=help_create_tables,
is_flag=True, default=False) # ~ is_flag=True, default=False)
@click.option('-m', '--migrar-bd', help=help_migrate_db, # ~ @click.option('-m', '--migrar-bd', help=help_migrate_db,
is_flag=True, default=False) # ~ is_flag=True, default=False)
@click.option('-ns', '--nuevo-superusuario', help=help_superuser, # ~ @click.option('-ns', '--nuevo-superusuario', help=help_superuser,
is_flag=True, default=False) # ~ is_flag=True, default=False)
@click.option('-cc', '--cambiar-contraseña', help=help_change_pass, # ~ @click.option('-cc', '--cambiar-contraseña', help=help_change_pass,
is_flag=True, default=False) # ~ is_flag=True, default=False)
@click.option('-ar', '--agregar-rfc', help=help_rfc, is_flag=True, default=False) # ~ @click.option('-br', '--borrar-rfc', help=help_br, 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('-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('-f', '--archivo')
@click.option('-f', '--archivo') # ~ @click.option('-c', '--conexion')
@click.option('-c', '--conexion') # ~ @click.option('-fl', '--factura-libre', is_flag=True, default=False)
@click.option('-fl', '--factura-libre', is_flag=True, default=False) # ~ @click.option('-t', '--test', is_flag=True, default=False)
@click.option('-flg', '--factura-libre-gambas', is_flag=True, default=False) # ~ @click.option('-gap', '--generar-archivo-productos', is_flag=True, default=False)
@click.option('-t', '--test', is_flag=True, default=False) # ~ @click.option('-ip', '--importar-productos', is_flag=True, default=False)
@click.option('-gap', '--generar-archivo-productos', is_flag=True, default=False) # ~ @click.option('-bk', '--backup-dbs', is_flag=True, default=False)
@click.option('-ip', '--importar-productos', is_flag=True, default=False) # ~ @click.option('-n', '--no-bd', is_flag=True, default=False)
@click.option('-bk', '--backup-dbs', is_flag=True, default=False) # ~ @click.option('-a', '--alta', is_flag=True, default=False)
@click.option('-n', '--no-bd', is_flag=True, default=False) # ~ @click.option('-r', '--rfc')
@click.option('-a', '--alta', is_flag=True, default=False) # ~ @click.option('-d', '--detalle', is_flag=True, default=False)
@click.option('-r', '--rfc') # ~ @click.option('-id', '--importar-directorio')
@click.option('-d', '--detalle', is_flag=True, default=False) # ~ @click.option('-ed', '--exportar_documentos', is_flag=True, default=False)
@click.option('-id', '--importar-directorio') # ~ def main2(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña,
@click.option('-ed', '--exportar_documentos', is_flag=True, default=False) # ~ borrar_rfc, listar_rfc, importar_valores, archivo, conexion,
def main(iniciar_bd, migrar_bd, nuevo_superusuario, cambiar_contraseña, # ~ factura_libre, test, generar_archivo_productos,
agregar_rfc, borrar_rfc, listar_rfc, importar_valores, archivo, conexion, # ~ importar_productos, backup_dbs, no_bd, alta, rfc, detalle,
factura_libre, factura_libre_gambas, test, generar_archivo_productos, # ~ importar_directorio, exportar_documentos):
importar_productos, backup_dbs, no_bd, alta, rfc, detalle,
importar_directorio, exportar_documentos):
opt = locals() # ~ opt = locals()
if opt['test']: # ~ if opt['alta']:
_test() # ~ if not opt['rfc']:
sys.exit(0) # ~ msg = 'Falta el RFC'
# ~ raise click.ClickException(msg)
# ~ _new_client(opt['rfc'], no_bd)
# ~ sys.exit(0)
if opt['alta']: # ~ if opt['iniciar_bd']:
if not opt['rfc']: # ~ _iniciar_bd()
msg = 'Falta el RFC' # ~ sys.exit(0)
raise click.ClickException(msg)
empresa_agregar(opt['rfc'], no_bd)
sys.exit(0)
if opt['iniciar_bd']: # ~ if opt['migrar_bd']:
_iniciar_bd() # ~ _migrate_tables(rfc)
sys.exit(0) # ~ sys.exit(0)
if opt['migrar_bd']: # ~ if opt['nuevo_superusuario']:
_migrate_tables(rfc) # ~ _agregar_superusuario()
sys.exit(0) # ~ sys.exit(0)
if opt['nuevo_superusuario']: # ~ if opt['cambiar_contraseña']:
_agregar_superusuario() # ~ _cambiar_contraseña()
sys.exit(0) # ~ sys.exit(0)
if opt['cambiar_contraseña']: # ~ if opt['borrar_rfc']:
_cambiar_contraseña() # ~ _borrar_rfc()
sys.exit(0) # ~ sys.exit(0)
if opt['agregar_rfc']: # ~ if opt['listar_rfc']:
_agregar_rfc(no_bd) # ~ _listar_rfc(opt['detalle'])
sys.exit(0) # ~ sys.exit(0)
if opt['borrar_rfc']: # ~ if opt['importar_valores']:
_borrar_rfc() # ~ if not opt['archivo']:
sys.exit(0) # ~ msg = 'Falta la ruta del archivo importar'
# ~ raise click.ClickException(msg)
# ~ if not util.is_file(opt['archivo']):
# ~ msg = 'No es un archivo'
# ~ raise click.ClickException(msg)
if opt['listar_rfc']: # ~ _importar_valores(opt['archivo'])
_listar_rfc(opt['detalle']) # ~ sys.exit(0)
sys.exit(0)
if opt['importar_valores']: # ~ if opt['factura_libre']:
if not opt['archivo']: # ~ if not opt['archivo']:
msg = 'Falta la ruta del archivo importar' # ~ msg = 'Falta la ruta de la base de datos'
raise click.ClickException(msg) # ~ raise click.ClickException(msg)
if not util.is_file(opt['archivo']): # ~ if not util.is_file(opt['archivo']):
msg = 'No es un archivo' # ~ msg = 'No es un archivo'
raise click.ClickException(msg) # ~ 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_valores(opt['archivo']) # ~ _importar_factura_libre(opt['archivo'])
sys.exit(0) # ~ sys.exit(0)
if opt['factura_libre']: # ~ if opt['generar_archivo_productos']:
if not opt['archivo']: # ~ if not opt['archivo']:
msg = 'Falta la ruta de la base de datos' # ~ msg = 'Falta la ruta de la base de datos'
raise click.ClickException(msg) # ~ raise click.ClickException(msg)
if not util.is_file(opt['archivo']): # ~ if not util.is_file(opt['archivo']):
msg = 'No es un archivo' # ~ msg = 'No es un archivo'
raise click.ClickException(msg) # ~ raise click.ClickException(msg)
_, _, _, ext = util.get_path_info(opt['archivo']) # ~ _, _, _, ext = util.get_path_info(opt['archivo'])
if ext != '.sqlite': # ~ if ext != '.sqlite':
msg = 'No es una base de datos' # ~ msg = 'No es una base de datos'
raise click.ClickException(msg) # ~ raise click.ClickException(msg)
_importar_factura_libre(opt['archivo']) # ~ _generar_archivo_productos(opt['archivo'])
sys.exit(0) # ~ sys.exit(0)
if opt['factura_libre_gambas']: # ~ if opt['importar_productos']:
if not opt['conexion']: # ~ if not opt['archivo']:
msg = 'Falta los datos de conexión' # ~ msg = 'Falta la ruta del archivo'
raise click.ClickException(msg) # ~ raise click.ClickException(msg)
_importar_factura_libre_gambas(opt['conexion']) # ~ if not util.is_file(opt['archivo']):
sys.exit(0) # ~ msg = 'No es un archivo'
# ~ raise click.ClickException(msg)
# ~ _, _, _, ext = util.get_path_info(opt['archivo'])
# ~ if ext != '.txt':
# ~ msg = 'No es un archivo de texto'
# ~ raise click.ClickException(msg)
if opt['generar_archivo_productos']: # ~ _importar_productos(opt['archivo'])
if not opt['archivo']: # ~ sys.exit(0)
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)
_generar_archivo_productos(opt['archivo']) # ~ if opt['importar_directorio']:
sys.exit(0) # ~ _import_from_folder(opt['importar_directorio'])
# ~ sys.exit(0)
if opt['importar_productos']: # ~ if opt['backup_dbs']:
if not opt['archivo']: # ~ utils.db_backup(DB_COMPANIES, PATHS['BK'], IS_MV, URL['SEAFILE'])
msg = 'Falta la ruta del archivo' # ~ sys.exit(0)
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 != '.txt':
msg = 'No es un archivo de texto'
raise click.ClickException(msg)
_importar_productos(opt['archivo']) # ~ if opt['exportar_documentos']:
sys.exit(0) # ~ _exportar_documentos()
if opt['importar_directorio']: # ~ return
_import_from_folder(opt['importar_directorio'])
sys.exit(0)
if opt['backup_dbs']:
# ~ util.backup_dbs()
utils.db_backup(DB_COMPANIES, PATHS['BK'], IS_MV, URL['SEAFILE'])
sys.exit(0)
if opt['exportar_documentos']:
_exportar_documentos()
return
if __name__ == '__main__': if __name__ == '__main__':
main() args = _process_command_line_arguments()
main(args)
# ~ main2()

View File

@ -47,7 +47,7 @@ except ImportError:
DEBUG = DEBUG DEBUG = DEBUG
VERSION = '1.32.0' VERSION = '1.33.0'
EMAIL_SUPPORT = ('soporte@empresalibre.mx',) EMAIL_SUPPORT = ('soporte@empresalibre.mx',)
TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION) TITLE_APP = '{} v{}'.format(TITLE_APP, VERSION)
@ -200,6 +200,8 @@ CURRENCY_MN = 'MXN'
IS_MV = MV IS_MV = MV
DB_COMPANIES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'rfc.db')) DB_COMPANIES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'rfc.db'))
path_bk = os.path.join(path_docs, 'tmp') path_bk = os.path.join(path_docs, 'tmp')
path_local = 'facturas'
path_sat = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'valores_iniciales.json'))
EXT = { EXT = {
'CSS': 'css', 'CSS': 'css',
@ -218,6 +220,8 @@ PATHS = {
'USER': path_user_template, 'USER': path_user_template,
'LOGOS': path_user_logos, 'LOGOS': path_user_logos,
'BK': path_bk, 'BK': path_bk,
'LOCAL': path_local,
'SAT': path_sat,
} }
VALUES_PDF = { VALUES_PDF = {

File diff suppressed because it is too large Load Diff

View File

@ -120,6 +120,7 @@ var controllers = {
$$('txt_config_cfdipay_serie').attachEvent('onKeyPress', txt_config_cfdipay_serie_press) $$('txt_config_cfdipay_serie').attachEvent('onKeyPress', txt_config_cfdipay_serie_press)
$$('txt_config_cfdipay_folio').attachEvent('onKeyPress', txt_config_cfdipay_folio_press) $$('txt_config_cfdipay_folio').attachEvent('onKeyPress', txt_config_cfdipay_folio_press)
$$('chk_usar_nomina').attachEvent('onItemClick', chk_config_item_click) $$('chk_usar_nomina').attachEvent('onItemClick', chk_config_item_click)
$$('lst_pac').attachEvent('onChange', lst_pac_on_change)
$$('cmd_subir_bdfl').attachEvent('onItemClick', cmd_subir_bdfl_click) $$('cmd_subir_bdfl').attachEvent('onItemClick', cmd_subir_bdfl_click)
$$('cmd_subir_cfdixml').attachEvent('onItemClick', cmd_subir_cfdixml_click) $$('cmd_subir_cfdixml').attachEvent('onItemClick', cmd_subir_cfdixml_click)
@ -464,7 +465,11 @@ function get_config_values(opt){
success: function(text, data, xhr) { success: function(text, data, xhr) {
var values = data.json() var values = data.json()
Object.keys(values).forEach(function(key){ Object.keys(values).forEach(function(key){
$$(key).setValue(values[key]) if(key=='lst_pac'){
set_value(key, values[key])
}else{
$$(key).setValue(values[key])
}
}) })
} }
}) })
@ -2534,3 +2539,32 @@ function opt_make_pdf_from_on_change(new_value, old_value){
} }
function lst_pac_on_change(nv, ov){
if(nv=='default'){
webix.ajax().del('/config', {id: 'lst_pac'}, function(text, xml, xhr){
var msg = 'PAC predeterminado establecido correctamente'
if(xhr.status == 200){
msg_ok(msg)
}else{
msg = 'No se pudo eliminar'
msg_error(msg)
}
})
}else{
webix.ajax().post('/config', {'lst_pac': nv}, {
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 = 'PAC establecido correctamente'
msg_ok(msg)
}else{
msg_error(values.msg)
}
}
})
}
}

View File

@ -579,3 +579,12 @@ function lst_clear(lst){
function lst_parse(lst, values){ function lst_parse(lst, values){
lst.getList().parse(values) lst.getList().parse(values)
} }
function set_value(control, value){
obj = $$(control)
obj.blockEvent()
obj.setValue(value)
obj.unblockEvent()
}

View File

@ -643,6 +643,12 @@ var options_templates = [
{}] {}]
var options_pac = [
{id: 'default', value: 'Predeterminado'},
{id: 'comercio', value: 'Comercio Digital'},
]
var options_admin_otros = [ var options_admin_otros = [
{maxHeight: 15}, {maxHeight: 15},
{template: 'Facturación', type: 'section'}, {template: 'Facturación', type: 'section'},
@ -674,6 +680,13 @@ var options_admin_otros = [
{view: 'checkbox', id: 'chk_config_decimales_precios', labelWidth: 0, {view: 'checkbox', id: 'chk_config_decimales_precios', labelWidth: 0,
labelRight: 'Precios con 4 decimales'}, {}, labelRight: 'Precios con 4 decimales'}, {},
]}, ]},
{maxHeight: 15},
{cols: [{maxWidth: 15},
{view: 'richselect', id: 'lst_pac', name: 'lst_pac', width: 300,
label: 'PAC: ', value: 'default', required: false,
options: options_pac},
{},
]},
{maxHeight: 20}, {maxHeight: 20},
{template: 'Ayudas varias', type: 'section'}, {template: 'Ayudas varias', type: 'section'},
{cols: [{maxWidth: 15}, {cols: [{maxWidth: 15},
@ -700,15 +713,6 @@ var options_admin_otros = [
labelRight: 'Mostrar total arriba'}, labelRight: 'Mostrar total arriba'},
{}]}, {}]},
{maxHeight: 20}, {maxHeight: 20},
{template: 'Nómina', type: 'section'},
{cols: [{maxWidth: 15},
{view: 'checkbox', id: 'chk_usar_nomina', labelWidth: 0,
labelRight: 'Usar timbrado de Nómina'},
{view: 'text', id: 'txt_config_nomina_serie', name: 'config_nomina_serie',
label: 'Serie', labelWidth: 50, labelAlign: 'right'},
{view: 'text', id: 'txt_config_nomina_folio', name: 'config_nomina_folio',
label: 'Folio', labelWidth: 50, labelAlign: 'right'},
{}]},
{}] {}]
@ -738,12 +742,15 @@ var options_admin_products = [
var options_admin_complements = [ var options_admin_complements = [
{maxHeight: 20}, {maxHeight: 20},
{template: 'Complemento de Nómina', type: 'section'},
{cols: [{maxWidth: 15}, {cols: [{maxWidth: 15},
{view: 'checkbox', id: 'chk_config_ine', labelWidth: 0, {view: 'checkbox', id: 'chk_usar_nomina', labelWidth: 0,
labelRight: 'Usar el complemento INE'}, labelRight: 'Usar complemento de Nómina'},
{view: 'checkbox', id: 'chk_config_edu', labelWidth: 0, {view: 'text', id: 'txt_config_nomina_serie', name: 'config_nomina_serie',
labelRight: 'Usar el complemento EDU'}, label: 'Serie', labelWidth: 50, labelAlign: 'right'},
{}]}, {view: 'text', id: 'txt_config_nomina_folio', name: 'config_nomina_folio',
label: 'Folio', labelWidth: 50, labelAlign: 'right'},
{maxWidth: 15}]},
{maxHeight: 20}, {maxHeight: 20},
{template: 'Complemento de Pagos', type: 'section'}, {template: 'Complemento de Pagos', type: 'section'},
{cols: [{maxWidth: 15}, {cols: [{maxWidth: 15},
@ -762,6 +769,18 @@ var options_admin_complements = [
{view: 'checkbox', id: 'chk_config_divisas', labelWidth: 0, {view: 'checkbox', id: 'chk_config_divisas', labelWidth: 0,
labelRight: 'Usar complemento de divisas'}, labelRight: 'Usar complemento de divisas'},
{maxWidth: 15}]}, {maxWidth: 15}]},
{maxHeight: 20},
{template: 'Complemento INE', type: 'section'},
{cols: [{maxWidth: 15},
{view: 'checkbox', id: 'chk_config_ine', labelWidth: 0,
labelRight: 'Usar el complemento INE'},
{maxWidth: 15}]},
{maxHeight: 20},
{template: 'Complemento para escuelas EDU', type: 'section'},
{cols: [{maxWidth: 15},
{view: 'checkbox', id: 'chk_config_edu', labelWidth: 0,
labelRight: 'Usar el complemento EDU'},
{maxWidth: 15}]},
] ]