diff --git a/.gitignore b/.gitignore
index 3f3eb5e..a434b5a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,4 +19,6 @@ credenciales.conf
*.sqlite
*.sql
rfc.db
+configpac.py
+
diff --git a/source/app/controllers/cfdi_xml.py b/source/app/controllers/cfdi_xml.py
new file mode 100644
index 0000000..6cef536
--- /dev/null
+++ b/source/app/controllers/cfdi_xml.py
@@ -0,0 +1,308 @@
+#!/usr/bin/env python
+
+import datetime
+from xml.etree import ElementTree as ET
+from xml.dom.minidom import parseString
+
+from logbook import Logger
+
+#~ from settings import DEBUG
+
+
+log = Logger('XML')
+CFDI_ACTUAL = 'cfdi33'
+NOMINA_ACTUAL = 'nomina12'
+
+SAT = {
+ 'xsi': 'http://www.w3.org/2001/XMLSchema-instance',
+ 'cfdi32': {
+ 'version': '3.2',
+ 'prefix': 'cfdi',
+ 'xmlns': 'http://www.sat.gob.mx/cfd/3',
+ 'schema': 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd',
+ },
+ 'cfdi33': {
+ 'version': '3.3',
+ 'prefix': 'cfdi',
+ 'xmlns': 'http://www.sat.gob.mx/cfd/3',
+ 'schema': 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd',
+ },
+ 'nomina11': {
+ 'version': '1.1',
+ 'prefix': 'nomina',
+ 'xmlns': 'http://www.sat.gob.mx/nomina',
+ 'schema': 'http://www.sat.gob.mx/nomina http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina11.xsd',
+ },
+ 'nomina12': {
+ 'version': '1.2',
+ 'prefix': 'nomina',
+ 'xmlns': 'http://www.sat.gob.mx/nomina12',
+ 'schema': 'http://www.sat.gob.mx/nomina12 http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina12.xsd',
+ },
+}
+
+
+class CFDI(object):
+
+ def __init__(self, version=CFDI_ACTUAL):
+ self._sat_cfdi = SAT[version]
+ self._xsi = SAT['xsi']
+ self._pre = self._sat_cfdi['prefix']
+ self._cfdi = None
+ self.error = ''
+
+ def _now(self):
+ return datetime.datetime.now().isoformat()[:19]
+
+ def get_xml(self, datos):
+ if not self._validate(datos):
+ return ''
+
+ self._comprobante(datos['comprobante'])
+ self._emisor(datos['emisor'])
+ self._receptor(datos['receptor'])
+ self._conceptos(datos['conceptos'])
+ self._impuestos(datos['impuestos'])
+ if 'nomina' in datos:
+ self._nomina(datos['nomina'])
+ if 'complementos' in datos:
+ self._complementos(datos['complementos'])
+ return self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8'))
+
+ def add_sello(self, sello):
+ self._cfdi.attrib['Sello'] = sello
+ return self._to_pretty_xml(ET.tostring(self._cfdi, encoding='utf-8'))
+
+ def _to_pretty_xml(self, source):
+ tree = parseString(source)
+ xml = tree.toprettyxml(encoding='utf-8').decode('utf-8')
+ return xml
+
+ def _validate(self, datos):
+ if 'nomina' in datos:
+ return self._validate_nomina(datos)
+ return True
+
+ def _validate_nomina(self, datos):
+ comprobante = datos['comprobante']
+
+ validators = (
+ ('MetodoDePago', 'NA'),
+ ('TipoCambio', '1'),
+ ('Moneda', 'MXN'),
+ ('TipoDeComprobante', 'egreso'),
+ )
+ for f, v in validators:
+ if f in comprobante:
+ if v != comprobante[f]:
+ msg = 'El atributo: {}, debe ser: {}'.format(f, v)
+ self.error = msg
+ return False
+ return True
+
+ def _comprobante(self, datos):
+ attributes = {}
+ attributes['xmlns:{}'.format(self._pre)] = self._sat_cfdi['xmlns']
+ attributes['xmlns:xsi'] = self._xsi
+ attributes['xsi:schemaLocation'] = self._sat_cfdi['schema']
+ attributes.update(datos)
+
+ #~ if DEBUG:
+ #~ attributes['Fecha'] = self._now()
+ #~ attributes['NoCertificado'] = CERT_NUM
+
+ if not 'Version' in attributes:
+ attributes['Version'] = self._sat_cfdi['version']
+ if not 'Fecha' in attributes:
+ attributes['Fecha'] = self._now()
+
+ self._cfdi = ET.Element('{}:Comprobante'.format(self._pre), attributes)
+ return
+
+ def _emisor(self, datos):
+ #~ if DEBUG:
+ #~ datos['Rfc'] = RFC_TEST
+
+ node_name = '{}:Emisor'.format(self._pre)
+ emisor = ET.SubElement(self._cfdi, node_name, datos)
+ return
+
+ def _receptor(self, datos):
+ node_name = '{}:Receptor'.format(self._pre)
+ emisor = ET.SubElement(self._cfdi, node_name, datos)
+ return
+
+ def _conceptos(self, datos):
+ conceptos = ET.SubElement(self._cfdi, '{}:Conceptos'.format(self._pre))
+ for row in datos:
+ complemento = {}
+ if 'complemento' in row:
+ complemento = row.pop('complemento')
+
+ taxes = {}
+ if 'impuestos' in row:
+ taxes = row.pop('impuestos')
+ node_name = '{}:Concepto'.format(self._pre)
+ concepto = ET.SubElement(conceptos, node_name, row)
+
+ if taxes:
+ node_name = '{}:Impuestos'.format(self._pre)
+ impuestos = ET.SubElement(concepto, node_name)
+ if 'traslados' in taxes and taxes['traslados']:
+ node_name = '{}:Traslados'.format(self._pre)
+ traslados = ET.SubElement(impuestos, node_name)
+ for traslado in taxes['traslados']:
+ ET.SubElement(
+ traslados, '{}:Traslado'.format(self._pre), traslado)
+ if 'retenciones' in taxes and taxes['retenciones']:
+ node_name = '{}:Retenciones'.format(self._pre)
+ retenciones = ET.SubElement(impuestos, node_name)
+ for retencion in taxes['retenciones']:
+ ET.SubElement(
+ retenciones, '{}:Retencion'.format(self._pre), retencion)
+
+ if 'InformacionAduanera' in row:
+ for field in fields:
+ if field in row['InformacionAduanera']:
+ attributes[field] = row['InformacionAduanera'][field]
+ if attributes:
+ node_name = '{}:InformacionAduanera'.format(self._pre)
+ ET.SubElement(concepto, node_name, attributes)
+
+ if 'CuentaPredial' in row:
+ attributes = {'numero': row['CuentaPredial']}
+ node_name = '{}:CuentaPredial'.format(self._pre)
+ ET.SubElement(concepto, node_name, attributes)
+
+ if 'autRVOE' in row:
+ fields = (
+ 'version',
+ 'nombreAlumno',
+ 'CURP',
+ 'nivelEducativo',
+ 'autRVOE',
+ )
+ for field in fields:
+ if field in row['autRVOE']:
+ attributes[field] = row['autRVOE'][field]
+ node_name = '{}:ComplementoConcepto'.format(self._pre)
+ complemento = ET.SubElement(concepto, node_name)
+ ET.SubElement(complemento, 'iedu:instEducativas', attributes)
+ return
+
+ def _impuestos(self, datos):
+ if not datos:
+ node_name = '{}:Impuestos'.format(self._pre)
+ ET.SubElement(self._cfdi, node_name)
+ return
+
+ attributes = {}
+ fields = ('TotalImpuestosTrasladados', 'TotalImpuestosRetenidos')
+ for field in fields:
+ if field in datos:
+ attributes[field] = datos[field]
+ node_name = '{}:Impuestos'.format(self._pre)
+ impuestos = ET.SubElement(self._cfdi, node_name, attributes)
+
+ if 'retenciones' in datos and datos['retenciones']:
+ retenciones = ET.SubElement(impuestos, '{}:Retenciones'.format(self._pre))
+ for row in datos['retenciones']:
+ ET.SubElement(retenciones, '{}:Retencion'.format(self._pre), row)
+
+ if 'traslados' in datos and datos['traslados']:
+ traslados = ET.SubElement(impuestos, '{}:Traslados'.format(self._pre))
+ for row in datos['traslados']:
+ ET.SubElement(traslados, '{}:Traslado'.format(self._pre), row)
+ return
+
+ def _nomina(self, datos):
+ sat_nomina = SAT[NOMINA_ACTUAL]
+ pre = sat_nomina['prefix']
+ complemento = ET.SubElement(self._cfdi, '{}:Complemento'.format(self._pre))
+
+ emisor = datos.pop('Emisor', None)
+ receptor = datos.pop('Receptor', None)
+ percepciones = datos.pop('Percepciones', None)
+ deducciones = datos.pop('Deducciones', None)
+
+ attributes = {}
+ attributes['xmlns:{}'.format(pre)] = sat_nomina['xmlns']
+ attributes['xsi:schemaLocation'] = sat_nomina['schema']
+ attributes.update(datos)
+
+ if not 'Version' in attributes:
+ attributes['Version'] = sat_nomina['version']
+
+ nomina = ET.SubElement(complemento, '{}:Nomina'.format(pre), attributes)
+ if emisor:
+ ET.SubElement(nomina, '{}:Emisor'.format(pre), emisor)
+ if receptor:
+ ET.SubElement(nomina, '{}:Receptor'.format(pre), receptor)
+ if percepciones:
+ detalle = percepciones.pop('detalle', None)
+ percepciones = ET.SubElement(nomina, '{}:Percepciones'.format(pre), percepciones)
+ for row in detalle:
+ ET.SubElement(percepciones, '{}:Percepcion'.format(pre), row)
+ if deducciones:
+ detalle = deducciones.pop('detalle', None)
+ deducciones = ET.SubElement(nomina, '{}:Deducciones'.format(pre), deducciones)
+ for row in detalle:
+ ET.SubElement(deducciones, '{}:Deduccion'.format(pre), row)
+ return
+
+ def _complementos(self, datos):
+ complemento = ET.SubElement(self._cfdi, '{}:Complemento'.format(self._pre))
+ if 'ce' in datos:
+ pre = 'cce11'
+ datos = datos.pop('ce')
+ emisor = datos.pop('emisor')
+ propietario = datos.pop('propietario')
+ receptor = datos.pop('receptor')
+ destinatario = datos.pop('destinatario')
+ conceptos = datos.pop('conceptos')
+
+ attributes = {}
+ attributes['xmlns:{}'.format(pre)] = \
+ 'http://www.sat.gob.mx/ComercioExterior11'
+ attributes['xsi:schemaLocation'] = \
+ 'http://www.sat.gob.mx/ComercioExterior11 ' \
+ 'http://www.sat.gob.mx/sitio_internet/cfd/ComercioExterior11/ComercioExterior11.xsd'
+ attributes.update(datos)
+ ce = ET.SubElement(
+ complemento, '{}:ComercioExterior'.format(pre), attributes)
+
+ attributes = {}
+ if 'Curp' in emisor:
+ attributes = {'Curp': emisor.pop('Curp')}
+ node = ET.SubElement(ce, '{}:Emisor'.format(pre), attributes)
+ ET.SubElement(node, '{}:Domicilio'.format(pre), emisor)
+
+ if propietario:
+ ET.SubElement(ce, '{}:Propietario'.format(pre), propietario)
+
+ attributes = {}
+ if 'NumRegIdTrib' in receptor:
+ attributes = {'NumRegIdTrib': receptor.pop('NumRegIdTrib')}
+ node = ET.SubElement(ce, '{}:Receptor'.format(pre), attributes)
+ ET.SubElement(node, '{}:Domicilio'.format(pre), receptor)
+
+ attributes = {}
+ if 'NumRegIdTrib' in destinatario:
+ attributes = {'NumRegIdTrib': destinatario.pop('NumRegIdTrib')}
+ if 'Nombre' in destinatario:
+ attributes.update({'Nombre': destinatario.pop('Nombre')})
+ node = ET.SubElement(ce, '{}:Destinatario'.format(pre), attributes)
+ ET.SubElement(node, '{}:Domicilio'.format(pre), destinatario)
+
+ node = ET.SubElement(ce, '{}:Mercancias'.format(pre))
+ fields = ('Marca', 'Modelo', 'SubModelo', 'NumeroSerie')
+ for row in conceptos:
+ detalle = {}
+ for f in fields:
+ if f in row:
+ detalle[f] = row.pop(f)
+ concepto = ET.SubElement(node, '{}:Mercancia'.format(pre), row)
+ if detalle:
+ ET.SubElement(
+ concepto, '{}:DescripcionesEspecificas'.format(pre), detalle)
+ return
diff --git a/source/app/controllers/pac.py b/source/app/controllers/pac.py
new file mode 100644
index 0000000..49e3cc5
--- /dev/null
+++ b/source/app/controllers/pac.py
@@ -0,0 +1,609 @@
+#!/usr/bin/env python
+
+#~ import re
+#~ from xml.etree import ElementTree as ET
+#~ from requests import Request, Session, exceptions
+import datetime
+import hashlib
+import os
+import requests
+import time
+from lxml import etree
+from xml.dom.minidom import parseString
+from xml.sax.saxutils import escape, unescape
+from uuid import UUID
+
+from logbook import Logger
+from zeep import Client
+from zeep.plugins import HistoryPlugin
+from zeep.cache import SqliteCache
+from zeep.transports import Transport
+from zeep.exceptions import Fault, TransportError
+
+from .configpac import DEBUG, TIMEOUT, AUTH, URL
+
+
+log = Logger('PAC')
+#~ node = client.create_message(client.service, SERVICE, **args)
+#~ print(etree.tostring(node, pretty_print=True).decode())
+
+
+class Ecodex(object):
+
+ def __init__(self):
+ self.codes = URL['codes']
+ self.error = ''
+ self.message = ''
+ self._transport = Transport(cache=SqliteCache(), timeout=TIMEOUT)
+ self._plugins = None
+ self._history = None
+ if DEBUG:
+ self._history = HistoryPlugin()
+ self._plugins = [self._history]
+
+ def _get_token(self, rfc):
+ client = Client(URL['seguridad'],
+ transport=self._transport, plugins=self._plugins)
+ try:
+ result = client.service.ObtenerToken(rfc, self._get_epoch())
+ except Fault as e:
+ self.error = str(e)
+ log.error(self.error)
+ return ''
+
+ s = '{}|{}'.format(AUTH['ID'], result.Token)
+ return hashlib.sha1(s.encode()).hexdigest()
+
+ def _get_token_rest(self, rfc):
+ data = {
+ 'rfc': rfc,
+ 'grant_type': 'authorization_token',
+ }
+ headers = {'Content-type': 'application/x-www-form-urlencoded'}
+ result = requests.post(URL['token'], data=data, headers=headers)
+ data = result.json()
+ s = '{}|{}'.format(AUTH['ID'], data['service_token'])
+ return hashlib.sha1(s.encode()).hexdigest(), data['access_token']
+
+ def _validate_xml(self, xml):
+ NS_CFDI = {'cfdi': 'http://www.sat.gob.mx/cfd/3'}
+ if os.path.isfile(xml):
+ tree = etree.parse(xml).getroot()
+ else:
+ tree = etree.fromstring(xml.encode())
+
+ fecha = tree.get('Fecha')
+ rfc = tree.xpath('string(//cfdi:Emisor/@Rfc)', namespaces=NS_CFDI)
+ data = {
+ 'ComprobanteXML': etree.tostring(tree).decode(),
+ 'RFC': rfc,
+ 'Token': self._get_token(rfc),
+ 'TransaccionID': self._get_epoch(fecha),
+ }
+ return data
+
+ def _get_by_hash(self, sh, rfc):
+ token, access_token = self._get_token_rest(rfc)
+ url = URL['hash'].format(sh)
+ headers = {
+ 'Authorization': 'Bearer {}'.format(access_token),
+ 'X-Auth-Token': token,
+ }
+ result = requests.get(url, headers=headers)
+ if result.status_code == 200:
+ print (result.json())
+ return
+
+ def timbra_xml(self, xml):
+ data = self._validate_xml(xml)
+ client = Client(URL['timbra'],
+ transport=self._transport, plugins=self._plugins)
+ try:
+ result = client.service.TimbraXML(**data)
+ except Fault as e:
+ error = str(e)
+ if self.codes['HASH'] in error:
+ sh = error.split(' ')[3]
+ return self._get_by_hash(sh[:40], data['RFC'])
+ self.error = error
+ return ''
+
+ tree = parseString(result.ComprobanteXML.DatosXML)
+ xml = tree.toprettyxml(encoding='utf-8').decode('utf-8')
+ return xml
+
+ def _get_epoch(self, date=None):
+ if isinstance(date, str):
+ f = '%Y-%m-%dT%H:%M:%S'
+ e = int(time.mktime(time.strptime(date, f)))
+ else:
+ date = datetime.datetime.now()
+ e = int(time.mktime(date.timetuple()))
+ return e
+
+ def estatus_cuenta(self, rfc):
+ #~ Codigos:
+ #~ 100 = Cuenta encontrada
+ #~ 101 = RFC no dado de alta en el sistema ECODEX
+ token = self._get_token(rfc)
+ if not token:
+ return {}
+
+ data = {
+ 'RFC': rfc,
+ 'Token': token,
+ 'TransaccionID': self._get_epoch()
+ }
+ client = Client(URL['clients'],
+ transport=self._transport, plugins=self._plugins)
+ try:
+ result = client.service.EstatusCuenta(**data)
+ except Fault as e:
+ log.error(str(e))
+ return
+ #~ print (result)
+ return result.Estatus
+
+
+class Finkok(object):
+
+ def __init__(self):
+ self.codes = URL['codes']
+ self.error = ''
+ self.message = ''
+ self._transport = Transport(cache=SqliteCache(), timeout=TIMEOUT)
+ self._plugins = None
+ self._history = None
+ self.uuid = ''
+ self.fecha = None
+ if DEBUG:
+ self._history = HistoryPlugin()
+ self._plugins = [self._history]
+
+ def _debug(self):
+ if not DEBUG:
+ return
+ print('SEND', self._history.last_sent)
+ print('RESULT', self._history.last_received)
+ return
+
+ def _check_result(self, method, result):
+ #~ print ('CODE', result.CodEstatus)
+ #~ print ('INCIDENCIAS', result.Incidencias)
+ self.message = ''
+ MSG = {
+ 'OK': 'Comprobante timbrado satisfactoriamente',
+ '307': 'Comprobante timbrado previamente',
+ }
+ status = result.CodEstatus
+ if status is None and result.Incidencias:
+ for i in result.Incidencias['Incidencia']:
+ self.error += 'Error: {}\n{}'.format(
+ i['CodigoError'], i['MensajeIncidencia'])
+ return ''
+
+ if method == 'timbra' and status in (MSG['OK'], MSG['307']):
+ #~ print ('UUID', result.UUID)
+ #~ print ('FECHA', result.Fecha)
+ if status == MSG['307']:
+ self.message = MSG['307']
+ tree = parseString(result.xml)
+ response = tree.toprettyxml(encoding='utf-8').decode('utf-8')
+ self.uuid = result.UUID
+ self.fecha = result.Fecha
+
+ return response
+
+ def _load_file(self, path):
+ try:
+ with open(path, 'rb') as f:
+ data = f.read()
+ except Exception as e:
+ self.error = str(e)
+ return
+ return data
+
+ def _validate_xml(self, file_xml):
+ if os.path.isfile(file_xml):
+ try:
+ with open(file_xml, 'rb') as f:
+ xml = f.read()
+ except Exception as e:
+ self.error = str(e)
+ return False, ''
+ else:
+ xml = file_xml.encode('utf-8')
+ return True, xml
+
+ def _validate_uuid(self, uuid):
+ try:
+ UUID(uuid)
+ return True
+ except ValueError:
+ self.error = 'UUID no válido: {}'.format(uuid)
+ return False
+
+ def timbra_xml(self, file_xml):
+ self.error = ''
+ method = 'timbra'
+ ok, xml = self._validate_xml(file_xml)
+ if not ok:
+ return ''
+ client = Client(
+ URL[method], transport=self._transport, plugins=self._plugins)
+
+ args = {
+ 'username': AUTH['USER'],
+ 'password': AUTH['PASS'],
+ 'xml': xml,
+ }
+ if URL['quick_stamp']:
+ try:
+ result = client.service.quick_stamp(**args)
+ except Fault as e:
+ self.error = str(e)
+ return
+ else:
+ try:
+ result = client.service.stamp(**args)
+ except Fault as e:
+ self.error = str(e)
+ return
+
+ return self._check_result(method, result)
+
+ def _get_xml(self, uuid):
+ if not self._validate_uuid(uuid):
+ return ''
+
+ method = 'util'
+ client = Client(
+ URL[method], transport=self._transport, plugins=self._plugins)
+
+ args = {
+ 'username': AUTH['USER'],
+ 'password': AUTH['PASS'],
+ 'uuid': uuid,
+ 'taxpayer_id': self.rfc,
+ 'invoice_type': 'I',
+ }
+ try:
+ result = client.service.get_xml(**args)
+ except Fault as e:
+ self.error = str(e)
+ return ''
+ except TransportError as e:
+ self.error = str(e)
+ return ''
+
+ if result.error:
+ self.error = result.error
+ return ''
+
+ tree = parseString(result.xml)
+ xml = tree.toprettyxml(encoding='utf-8').decode('utf-8')
+ return xml
+
+ def recupera_xml(self, file_xml='', uuid=''):
+ self.error = ''
+ if uuid:
+ return self._get_xml(uuid)
+
+ method = 'timbra'
+ ok, xml = self._validate_xml(file_xml)
+ if not ok:
+ return ''
+ client = Client(
+ URL[method], transport=self._transport, plugins=self._plugins)
+ try:
+ result = client.service.stamped(xml, AUTH['USER'], AUTH['PASS'])
+ except Fault as e:
+ self.error = str(e)
+ return ''
+
+ return self._check_result(method, result)
+
+ def estatus_xml(self, uuid):
+ method = 'timbra'
+ if not self._validate_uuid(uuid):
+ return ''
+ client = Client(
+ URL[method], transport=self._transport, plugins=self._plugins)
+ try:
+ result = client.service.query_pending(AUTH['USER'], AUTH['PASS'], uuid)
+ #~ print (result.date)
+ #~ tree = parseString(unescape(result.xml))
+ #~ response = tree.toprettyxml(encoding='utf-8').decode('utf-8')
+ return result.status
+ except Fault as e:
+ self.error = str(e)
+ return ''
+
+ def cancel_xml(self, rfc, uuids, path_cer, path_key):
+ for u in uuids:
+ if not self._validate_uuid(u):
+ return ''
+
+ cer = self._load_file(path_cer)
+ key = self._load_file(path_key)
+ method = 'cancel'
+ client = Client(
+ URL[method], transport=self._transport, plugins=self._plugins)
+ uuid_type = client.get_type('ns1:UUIDS')
+ sa = client.get_type('ns0:stringArray')
+
+ args = {
+ 'UUIDS': uuid_type(uuids=sa(string=uuids)),
+ 'username': AUTH['USER'],
+ 'password': AUTH['PASS'],
+ 'taxpayer_id': rfc,
+ 'cer': cer,
+ 'key': key,
+ 'store_pending': True,
+ }
+ try:
+ result = client.service.cancel(**args)
+ except Fault as e:
+ self.error = str(e)
+ return ''
+
+ if result.CodEstatus and self.codes['205'] in result.CodEstatus:
+ self.error = result.CodEstatus
+ return ''
+
+ return result
+
+ def cancel_signature(self, file_xml):
+ method = 'cancel'
+ if os.path.isfile(file_xml):
+ root = etree.parse(file_xml).getroot()
+ else:
+ root = etree.fromstring(file_xml)
+
+ xml = etree.tostring(root)
+
+ client = Client(
+ URL[method], transport=self._transport, plugins=self._plugins)
+
+ args = {
+ 'username': AUTH['USER'],
+ 'password': AUTH['PASS'],
+ 'xml': xml,
+ 'store_pending': True,
+ }
+
+ result = client.service.cancel_signature(**args)
+ return result
+
+ def get_acuse(self, rfc, uuids, type_acuse='C'):
+ for u in uuids:
+ if not self._validate_uuid(u):
+ return ''
+
+ method = 'cancel'
+ client = Client(
+ URL[method], transport=self._transport, plugins=self._plugins)
+
+ args = {
+ 'username': AUTH['USER'],
+ 'password': AUTH['PASS'],
+ 'taxpayer_id': rfc,
+ 'uuid': '',
+ 'type': type_acuse,
+ }
+ try:
+ result = []
+ for u in uuids:
+ args['uuid'] = u
+ r = client.service.get_receipt(**args)
+ result.append(r)
+ except Fault as e:
+ self.error = str(e)
+ return ''
+
+ return result
+
+ def estatus_cancel(self, uuids):
+ for u in uuids:
+ if not self._validate_uuid(u):
+ return ''
+
+ method = 'cancel'
+ client = Client(
+ URL[method], transport=self._transport, plugins=self._plugins)
+
+ args = {
+ 'username': AUTH['USER'],
+ 'password': AUTH['PASS'],
+ 'uuid': '',
+ }
+ try:
+ result = []
+ for u in uuids:
+ args['uuid'] = u
+ r = client.service.query_pending_cancellation(**args)
+ result.append(r)
+ except Fault as e:
+ self.error = str(e)
+ return ''
+
+ return result
+
+ def add_token(self, rfc, email):
+ """
+ Se requiere cuenta de reseller para usar este método
+ """
+ method = 'util'
+ client = Client(
+ URL[method], transport=self._transport, plugins=self._plugins)
+ args = {
+ 'username': AUTH['USER'],
+ 'password': AUTH['PASS'],
+ 'name': rfc,
+ 'token_username': email,
+ 'taxpayer_id': rfc,
+ 'status': True,
+ }
+ result = client.service.add_token(**args)
+ return result
+
+ def get_date(self):
+ method = 'util'
+ client = Client(
+ URL[method], transport=self._transport, plugins=self._plugins)
+ try:
+ result = client.service.datetime(AUTH['USER'], AUTH['PASS'])
+ except Fault as e:
+ self.error = str(e)
+ return ''
+
+ if result.error:
+ self.error = result.error
+ return
+
+ return result.datetime
+
+ def add_client(self, rfc, type_user=False):
+ """
+ Se requiere cuenta de reseller para usar este método
+ type_user: False == 'P' == Prepago or True == 'O' == On demand
+ """
+ tu = {False: 'P', True: 'O'}
+ method = 'client'
+ client = Client(
+ URL[method], transport=self._transport, plugins=self._plugins)
+ args = {
+ 'reseller_username': AUTH['USER'],
+ 'reseller_password': AUTH['PASS'],
+ 'taxpayer_id': rfc,
+ 'type_user': tu[type_user],
+ 'added': datetime.datetime.now().isoformat()[:19],
+ }
+ try:
+ result = client.service.add(**args)
+ except Fault as e:
+ self.error = str(e)
+ return ''
+
+ return result
+
+ def edit_client(self, rfc, status=True):
+ """
+ Se requiere cuenta de reseller para usar este método
+ status = 'A' or 'S'
+ """
+ sv = {False: 'S', True: 'A'}
+ method = 'client'
+ client = Client(
+ URL[method], transport=self._transport, plugins=self._plugins)
+ args = {
+ 'reseller_username': AUTH['USER'],
+ 'reseller_password': AUTH['PASS'],
+ 'taxpayer_id': rfc,
+ 'status': sv[status],
+ }
+ try:
+ result = client.service.edit(**args)
+ except Fault as e:
+ self.error = str(e)
+ return ''
+
+ return result
+
+ def get_client(self, rfc):
+ """
+ Se requiere cuenta de reseller para usar este método
+ """
+ method = 'client'
+ client = Client(
+ URL[method], transport=self._transport, plugins=self._plugins)
+ args = {
+ 'reseller_username': AUTH['USER'],
+ 'reseller_password': AUTH['PASS'],
+ 'taxpayer_id': rfc,
+ }
+
+ try:
+ result = client.service.get(**args)
+ except Fault as e:
+ self.error = str(e)
+ return ''
+ except TransportError as e:
+ self.error = str(e)
+ return ''
+
+ return result
+
+ def assign_client(self, rfc, credit):
+ """
+ Se requiere cuenta de reseller para usar este método
+ """
+ method = 'client'
+ client = Client(
+ URL[method], transport=self._transport, plugins=self._plugins)
+ args = {
+ 'username': AUTH['USER'],
+ 'password': AUTH['PASS'],
+ 'taxpayer_id': rfc,
+ 'credit': credit,
+ }
+ try:
+ result = client.service.assign(**args)
+ except Fault as e:
+ self.error = str(e)
+ return ''
+
+ return result
+
+
+def _get_data_sat(path):
+ BF = 'string(//*[local-name()="{}"]/@{})'
+ NS_CFDI = {'cfdi': 'http://www.sat.gob.mx/cfd/3'}
+
+ try:
+ if os.path.isfile(path):
+ tree = etree.parse(path).getroot()
+ else:
+ tree = etree.fromstring(path)
+
+ data = {}
+ emisor = escape(
+ tree.xpath('string(//cfdi:Emisor/@rfc)', namespaces=NS_CFDI) or
+ tree.xpath('string(//cfdi:Emisor/@Rfc)', namespaces=NS_CFDI)
+ )
+ receptor = escape(
+ tree.xpath('string(//cfdi:Receptor/@rfc)', namespaces=NS_CFDI) or
+ tree.xpath('string(//cfdi:Receptor/@Rfc)', namespaces=NS_CFDI)
+ )
+ data['total'] = tree.get('total') or tree.get('Total')
+ data['emisor'] = emisor
+ data['receptor'] = receptor
+ data['uuid'] = tree.xpath(BF.format('TimbreFiscalDigital', 'UUID'))
+ except Exception as e:
+ print (e)
+ return {}
+
+ return '?re={emisor}&rr={receptor}&tt={total}&id={uuid}'.format(**data)
+
+
+def get_status_sat(xml):
+ data = _get_data_sat(xml)
+ if not data:
+ return
+
+ URL = 'https://consultaqr.facturaelectronica.sat.gob.mx/ConsultaCFDIService.svc?wsdl'
+ client = Client(URL, transport=Transport(cache=SqliteCache()))
+ try:
+ result = client.service.Consulta(expresionImpresa=data)
+ except Exception as e:
+ return 'Error: {}'.format(str(e))
+
+ return result.Estado
+
+
+def main():
+ return
+
+
+if __name__ == '__main__':
+ main()
diff --git a/source/app/controllers/util.py b/source/app/controllers/util.py
index 4a71ac0..156de65 100644
--- a/source/app/controllers/util.py
+++ b/source/app/controllers/util.py
@@ -15,7 +15,8 @@ import uuid
from dateutil import parser
-from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT
+from settings import DEBUG, log, template_lookup, COMPANIES, DB_SAT, \
+ PATH_XSLT, PATH_XSLTPROC, PATH_OPENSSL
#~ def _get_hash(password):
@@ -30,6 +31,10 @@ def _call(args):
return subprocess.check_output(args, shell=True).decode()
+def _get_md5(data):
+ return hashlib.md5(data.encode()).hexdigest()
+
+
def _save_temp(data, modo='wb'):
path = tempfile.mkstemp()[1]
with open(path, modo) as f:
@@ -37,6 +42,18 @@ def _save_temp(data, modo='wb'):
return path
+def _join(*paths):
+ return os.path.join(*paths)
+
+
+def _kill(path):
+ try:
+ os.remove(path)
+ except:
+ pass
+ return
+
+
def get_pass():
password = getpass.getpass('Introduce la contraseña: ')
pass2 = getpass.getpass('Confirma la contraseña: ')
@@ -312,7 +329,7 @@ class Certificado(object):
args = 'openssl pkcs8 -inform DER -in "{}" -passin pass:{} | ' \
'openssl rsa -des3 -passout pass:{}'.format(
- self._path_key, password, hashlib.md5(rfc.encode()).hexdigest())
+ self._path_key, password, _get_md5(rfc))
key_enc = _call(args)
data['key'] = self._key
@@ -327,7 +344,7 @@ class Certificado(object):
return {}
data = self._get_info_cer(rfc)
- llave = self._get_info_key(password, rfc)
+ llave = self._get_info_key(password, data['rfc'])
if not llave:
return {}
@@ -336,3 +353,49 @@ class Certificado(object):
self._kill(self._path_key)
self._kill(self._path_cer)
return data
+
+
+def make_xml(data, certificado):
+ from .cfdi_xml import CFDI
+
+ if DEBUG:
+ data['emisor']['Rfc'] = certificado.rfc
+ data['emisor']['RegimenFiscal'] = '603'
+
+ cfdi = CFDI()
+ xml = cfdi.get_xml(data)
+
+ data = {
+ 'xsltproc': PATH_XSLTPROC,
+ 'xslt': _join(PATH_XSLT, 'cadena.xslt'),
+ 'xml': _save_temp(xml, 'w'),
+ 'openssl': PATH_OPENSSL,
+ 'key': _save_temp(certificado.key_enc, 'w'),
+ 'pass': _get_md5(certificado.rfc)
+ }
+ args = '"{xsltproc}" "{xslt}" "{xml}" | ' \
+ '"{openssl}" dgst -sha256 -sign "{key}" -passin pass:{pass} | ' \
+ '"{openssl}" enc -base64 -A'.format(**data)
+ sello = _call(args)
+
+ _kill(data['xml'])
+ _kill(data['key'])
+
+ return cfdi.add_sello(sello)
+
+
+def timbra_xml(xml):
+ from .pac import Finkok as PAC
+
+ result = {'ok': True, 'error': ''}
+ pac = PAC()
+ xml = pac.timbra_xml(xml)
+ if not xml:
+ result['ok'] = False
+ result['error'] = pac.error
+ return result
+
+ result['xml'] = xml
+ result['uuid'] = pac.uuid
+ result['fecha'] = pac.fecha
+ return result
diff --git a/source/app/models/main.py b/source/app/models/main.py
index 65a8f5e..c42b8d9 100644
--- a/source/app/models/main.py
+++ b/source/app/models/main.py
@@ -16,6 +16,8 @@ if __name__ == '__main__':
from controllers import util
from settings import log, VERSION, PATH_CP
+FORMAT = '{0:.2f}'
+
database_proxy = Proxy()
class BaseModel(Model):
@@ -947,6 +949,7 @@ class Facturas(BaseModel):
obj = Facturas.get(Facturas.id==id)
if obj.uuid:
return False
+
q = FacturasDetalle.delete().where(FacturasDetalle.factura==obj)
q.execute()
q = FacturasImpuestos.delete().where(FacturasImpuestos.factura==obj)
@@ -1108,21 +1111,21 @@ class Facturas(BaseModel):
receptor = {
'Rfc': invoice.cliente.rfc,
- 'Nombre': invoice.cliente.name,
+ 'Nombre': invoice.cliente.nombre,
'UsoCFDI': invoice.uso_cfdi,
}
conceptos = []
- rows = Details.select().where(Details.invoice==invoice)
+ rows = FacturasDetalle.select().where(FacturasDetalle.factura==invoice)
for row in rows:
concepto = {
- 'ClaveProdServ': row.product.key_sat,
- 'NoIdentificacion': row.product.key,
- 'Cantidad': FORMAT.format(row.cant),
- 'ClaveUnidad': row.product.unit.key,
- 'Unidad': row.product.unit.name,
- 'Descripcion': row.product.description,
- 'ValorUnitario': FORMAT.format(row.price),
+ 'ClaveProdServ': row.producto.clave_sat,
+ 'NoIdentificacion': row.producto.clave,
+ 'Cantidad': FORMAT.format(row.cantidad),
+ 'ClaveUnidad': row.producto.unidad.key,
+ 'Unidad': row.producto.unidad.name,
+ 'Descripcion': row.producto.descripcion,
+ 'ValorUnitario': FORMAT.format(row.valor_unitario),
'Importe': FORMAT.format(row.importe),
}
@@ -1130,25 +1133,21 @@ class Facturas(BaseModel):
traslados = []
retenciones = []
- impuestos = (ProductsTaxes
- .select()
- .where(ProductsTaxes.product==row.product))
-
- for impuesto in impuestos:
- if impuesto.tax.tipo == 'E':
+ for impuesto in row.producto.impuestos:
+ if impuesto.tipo == 'E':
continue
- import_tax = round(impuesto.tax.tasa * row.importe, 2)
+ import_tax = round(impuesto.tasa * row.importe, 2)
tipo_factor = 'Tasa'
- if impuesto.tax.factor != 'T':
+ if impuesto.factor != 'T':
tipo_factor = 'Cuota'
tax = {
"Base": FORMAT.format(row.importe),
- "Impuesto": impuesto.tax.key,
+ "Impuesto": impuesto.key,
"TipoFactor": tipo_factor,
- "TasaOCuota": str(impuesto.tax.tasa),
+ "TasaOCuota": str(impuesto.tasa),
"Importe": FORMAT.format(import_tax),
}
- if impuesto.tax.tipo == 'T':
+ if impuesto.tipo == 'T':
traslados.append(tax)
else:
retenciones.append(tax)
@@ -1168,24 +1167,24 @@ class Facturas(BaseModel):
impuestos['TotalImpuestosRetenidos'] = \
FORMAT.format(invoice.total_retenciones)
- taxes = (InvoicesTaxes
+ taxes = (FacturasImpuestos
.select()
- .where(InvoicesTaxes.invoice==invoice))
+ .where(FacturasImpuestos.factura==invoice))
for tax in taxes:
tipo_factor = 'Tasa'
- if tax.tax.factor != 'T':
+ if tax.impuesto.factor != 'T':
tipo_factor = 'Cuota'
- if tax.tax.tipo == 'T':
+ if tax.impuesto.tipo == 'T':
traslado = {
- "Impuesto": tax.tax.key,
+ "Impuesto": tax.impuesto.key,
"TipoFactor": tipo_factor,
- "TasaOCuota": str(tax.tax.tasa),
+ "TasaOCuota": str(tax.impuesto.tasa),
"Importe": FORMAT.format(tax.importe),
}
traslados.append(traslado)
else:
retencion = {
- "Impuesto": tax.tax.key,
+ "Impuesto": tax.impuesto.key,
"Importe": FORMAT.format(tax.importe),
}
retenciones.append(retencion)
@@ -1209,21 +1208,24 @@ class Facturas(BaseModel):
obj.estatus = 'Generada'
obj.save()
- #~ error = False
- #~ result = util.timbra_xml(obj.xml)
- #~ if result['ok']:
- #~ obj.xml = result['xml']
- #~ obj.uuid = result['uuid']
- #~ obj.fecha_timbrado = result['fecha']
- #~ obj.estatus = 'Timbrada'
- #~ obj.save()
- #~ else:
- #~ error = True
- #~ msg = result['error']
- #~ obj.estatus = 'Error'
- #~ obj.error = msg
- #~ obj.save()
- return
+ error = False
+ msg = 'Factura timbrada correctamente'
+ result = util.timbra_xml(obj.xml)
+ if result['ok']:
+ obj.xml = result['xml']
+ obj.uuid = result['uuid']
+ obj.fecha_timbrado = result['fecha']
+ obj.estatus = 'Timbrada'
+ obj.save()
+ row = {'uuid': obj.uuid, 'estatus': 'Timbrada'}
+ else:
+ error = True
+ msg = result['error']
+ obj.estatus = 'Error'
+ obj.error = msg
+ row = {'estatus': 'Error'}
+ obj.save()
+ return {'ok': result['ok'], 'msg': msg, 'row': row}
class FacturasDetalle(BaseModel):
diff --git a/source/app/settings.py b/source/app/settings.py
index 5c1d76d..88b87ff 100644
--- a/source/app/settings.py
+++ b/source/app/settings.py
@@ -20,6 +20,9 @@ PATH_CP = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'cp.db'))
COMPANIES = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'rfc.db'))
DB_SAT = os.path.abspath(os.path.join(BASE_DIR, '..', 'db', 'sat.db'))
+PATH_XSLT = os.path.abspath(os.path.join(BASE_DIR, '..', 'xslt'))
+PATH_BIN = os.path.abspath(os.path.join(BASE_DIR, '..', 'bin'))
+
template_lookup = TemplateLookup(directories=[PATH_TEMPLATES],
input_encoding='utf-8',
output_encoding='utf-8')
@@ -50,3 +53,9 @@ else:
log = Logger(LOG_NAME)
+
+PATH_XSLTPROC = 'xsltproc'
+PATH_OPENSSL = 'openssl'
+if 'win' in sys.platform:
+ PATH_XSLTPROC = os.path.join(PATH_BIN, 'xsltproc.exe')
+ PATH_OPENSSL = os.path.join(PATH_BIN, 'openssl.exe')
diff --git a/source/static/img/file-email.png b/source/static/img/file-email.png
new file mode 100644
index 0000000..b655a0b
Binary files /dev/null and b/source/static/img/file-email.png differ
diff --git a/source/static/img/file-pdf.png b/source/static/img/file-pdf.png
new file mode 100644
index 0000000..e8fbe0e
Binary files /dev/null and b/source/static/img/file-pdf.png differ
diff --git a/source/static/img/file-xml.png b/source/static/img/file-xml.png
index 74dae07..1fa648e 100644
Binary files a/source/static/img/file-xml.png and b/source/static/img/file-xml.png differ
diff --git a/source/static/img/file-zip.png b/source/static/img/file-zip.png
new file mode 100644
index 0000000..15175ac
Binary files /dev/null and b/source/static/img/file-zip.png differ
diff --git a/source/static/js/controller/invoices.js b/source/static/js/controller/invoices.js
index 5ee58a7..a4c2b98 100644
--- a/source/static/js/controller/invoices.js
+++ b/source/static/js/controller/invoices.js
@@ -111,7 +111,7 @@ function cmd_edit_invoice_click(id, e, node){
function delete_invoice(id){
webix.ajax().del('/invoices', {id: id}, function(text, xml, xhr){
if(xhr.status == 200){
- $$('grid_invoices').remove(id)
+ gi.remove(id)
msg_sucess('Factura eliminada correctamente')
}else{
msg_error('No se pudo eliminar')
@@ -120,16 +120,15 @@ function delete_invoice(id){
}
-
function cmd_delete_invoice_click(id, e, node){
- var row = $$('grid_invoices').getSelectedItem()
+ var row = gi.getSelectedItem()
if (row == undefined){
msg_error('Selecciona una factura')
return
}
- if(!row['uuid']==null){
+ if(row.uuid){
msg_error('Solo se pueden eliminar facturas sin timbrar')
return
}
@@ -221,9 +220,9 @@ function validate_invoice(values){
function update_grid_invoices(values){
if(values.new){
- $$('grid_invoices').add(values.row)
+ gi.add(values.row)
}else{
- $$("grid_invoices").updateItem(values.row['id'], values.row)
+ gi.updateItem(values.row['id'], values.row)
}
}
@@ -232,6 +231,7 @@ function send_timbrar(id){
var values = data.json()
if(values.ok){
msg_sucess(values.msg)
+ gi.updateItem(id, values.row)
}else{
webix.alert({
title: 'Error al Timbrar',
@@ -256,11 +256,12 @@ function save_invoice(data){
success:function(text, data, XmlHttpRequest){
values = data.json();
if(values.ok){
+ msg_sucess('Factura guardada correctamente. Enviando a timbrar')
update_grid_invoices(values)
send_timbrar(values.row['id'])
result = true
}else{
- webix.message({type:'error', text:values.msg})
+ msg_error(values.msg)
}
}
})
@@ -602,3 +603,40 @@ function grid_details_header_click(id){
}
})
}
+
+
+function cmd_refacturar_click(){
+ show('Refacturar')
+}
+
+
+function cmd_invoice_timbrar_click(){
+ if(gi.count() == 0){
+ return
+ }
+
+ var row = gi.getSelectedItem()
+ if (row == undefined){
+ msg_error('Selecciona una factura')
+ return
+ }
+
+ if(row.uuid){
+ msg_error('La factura ya esta timbrada')
+ return
+ }
+
+ msg = '¿Estás seguro de enviar a timbrar esta factura?'
+ webix.confirm({
+ title: 'Timbrar Factura',
+ ok: 'Si',
+ cancel: 'No',
+ type: 'confirm-error',
+ text: msg,
+ callback:function(result){
+ if(result){
+ send_timbrar(row.id)
+ }
+ }
+ })
+}
diff --git a/source/static/js/controller/main.js b/source/static/js/controller/main.js
index c44d63b..b9e64ba 100644
--- a/source/static/js/controller/main.js
+++ b/source/static/js/controller/main.js
@@ -1,3 +1,4 @@
+var gi = null
var controllers = {
@@ -29,9 +30,9 @@ var controllers = {
$$("chk_automatica").attachEvent("onChange", chk_automatica_change)
$$("valor_unitario").attachEvent("onChange", valor_unitario_change)
//~ Invoices
- $$("cmd_new_invoice").attachEvent("onItemClick", cmd_new_invoice_click)
- $$("cmd_edit_invoice").attachEvent("onItemClick", cmd_edit_invoice_click)
- $$("cmd_delete_invoice").attachEvent("onItemClick", cmd_delete_invoice_click)
+ $$('cmd_new_invoice').attachEvent("onItemClick", cmd_new_invoice_click)
+ $$('cmd_refacturar').attachEvent("onItemClick", cmd_refacturar_click)
+ $$('cmd_delete_invoice').attachEvent("onItemClick", cmd_delete_invoice_click)
$$('cmd_timbrar').attachEvent('onItemClick', cmd_timbrar_click)
$$('cmd_close_invoice').attachEvent('onItemClick', cmd_close_invoice_click)
$$('search_client_id').attachEvent('onKeyPress', search_client_id_key_press)
@@ -42,6 +43,7 @@ var controllers = {
$$('grid_details').attachEvent('onHeaderClick', grid_details_header_click)
$$('grid_details').attachEvent('onBeforeEditStart', grid_details_before_edit_start)
$$('grid_details').attachEvent('onBeforeEditStop', grid_details_before_edit_stop)
+ $$('cmd_invoice_timbrar').attachEvent('onItemClick', cmd_invoice_timbrar_click)
}
}
@@ -136,6 +138,7 @@ function multi_change(prevID, nextID){
if(active == 'invoices_home'){
get_invoices()
}
+ gi = $$('grid_invoices')
return
}
diff --git a/source/static/js/ui/invoices.js b/source/static/js/ui/invoices.js
index 528e13f..6d93c27 100644
--- a/source/static/js/ui/invoices.js
+++ b/source/static/js/ui/invoices.js
@@ -3,28 +3,46 @@
var toolbar_invoices = [
{view: "button", id: "cmd_new_invoice", label: "Nueva", type: "iconButton",
autowidth: true, icon: "plus"},
- {view: "button", id: "cmd_edit_invoice", label: "Editar", type: "iconButton",
+ {view: "button", id: "cmd_refacturar", label: "Refacturar", type: "iconButton",
autowidth: true, icon: "pencil"},
+ {},
{view: "button", id: "cmd_delete_invoice", label: "Eliminar", type: "iconButton",
autowidth: true, icon: "minus"},
]
+var toolbar_invoices_util = [
+ {view: 'button', id: 'cmd_invoice_timbrar', label: 'Timbrar',
+ type: 'iconButton', autowidth: true, icon: 'ticket'},
+]
+
+
function doc_xml(obj){
var node = ""
return node
}
+function doc_pdf(obj){
+ var node = ""
+ return node
+}
+
+
+function get_icon(tipo){
+ var node = ""
+ return node
+}
+
+
var grid_invoices_cols = [
{id: "id", header:"ID", hidden:true},
{id: "serie", header: ["Serie", {content: "selectFilter"}], adjust: "data",
sort:"string"},
{id: "folio", header: ["Folio", {content: "numberFilter"}], adjust: "data",
sort:"int", css: "cell_right"},
- {id: 'xml', header: '', adjust: 'data', template: doc_xml},
{id: "uuid", header: ["UUID", {content: "textFilter"}], adjust: "data",
- sort:"string"},
+ sort:"string", hidden:true},
{id: "fecha", header: ["Fecha y Hora"], adjust: "data", sort:"string"},
{id: "tipo_comprobante", header: ["Tipo", {content: "selectFilter"}],
adjust: 'header', sort: 'string'},
@@ -34,13 +52,17 @@ var grid_invoices_cols = [
sort: 'int', format: webix.i18n.priceFormat, css: 'right'},
{id: "cliente", header: ["Razón Social", {content: "selectFilter"}],
fillspace:true, sort:"string"},
+ {id: 'xml', header: 'XML', adjust: 'data', template: get_icon('xml')},
+ {id: 'pdf', header: 'PDF', adjust: 'data', template: get_icon('pdf')},
+ {id: 'zip', header: 'ZIP', adjust: 'data', template: get_icon('zip')},
+ {id: 'email', header: '', adjust: 'data', template: get_icon('email')}
]
var grid_invoices = {
- view: "datatable",
- id: "grid_invoices",
- select: "row",
+ view: 'datatable',
+ id: 'grid_invoices',
+ select: 'row',
adjust: true,
footer: true,
resizeColumn: true,
@@ -296,15 +318,16 @@ var form_invoice = {
var multi_invoices = {
- id: "multi_invoices",
+ id: 'multi_invoices',
view: 'multiview',
animate: true,
cells:[
- {id: "invoices_home", rows:[
- {view: "toolbar", elements: toolbar_invoices},
+ {id: 'invoices_home', rows:[
+ {view: 'toolbar', elements: toolbar_invoices},
+ {view: 'toolbar', elements: toolbar_invoices_util},
grid_invoices,
]},
- {id: "invoices_new", rows:[form_invoice, {}]}
+ {id: 'invoices_new', rows:[form_invoice, {}]}
]
}
diff --git a/source/xslt/cadena.xslt b/source/xslt/cadena.xslt
new file mode 100644
index 0000000..2b81c70
--- /dev/null
+++ b/source/xslt/cadena.xslt
@@ -0,0 +1,345 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |||
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/source/xslt/comercioexterior11.xslt b/source/xslt/comercioexterior11.xslt
new file mode 100644
index 0000000..fd71841
--- /dev/null
+++ b/source/xslt/comercioexterior11.xslt
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/source/xslt/leyendasFisc.xslt b/source/xslt/leyendasFisc.xslt
new file mode 100644
index 0000000..e0587a2
--- /dev/null
+++ b/source/xslt/leyendasFisc.xslt
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/source/xslt/nomina12.xslt b/source/xslt/nomina12.xslt
new file mode 100644
index 0000000..2570170
--- /dev/null
+++ b/source/xslt/nomina12.xslt
@@ -0,0 +1,412 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/source/xslt/utilerias.xslt b/source/xslt/utilerias.xslt
new file mode 100644
index 0000000..d5dd14e
--- /dev/null
+++ b/source/xslt/utilerias.xslt
@@ -0,0 +1,22 @@
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+