diff --git a/source/app/controllers/cfdi_xml.py b/source/app/controllers/cfdi_xml.py
index 8b933d9..062b8eb 100644
--- a/source/app/controllers/cfdi_xml.py
+++ b/source/app/controllers/cfdi_xml.py
@@ -33,9 +33,9 @@ SAT = {
'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': {
+ 'nomina': {
'version': '1.2',
- 'prefix': 'nomina',
+ 'prefix': 'nomina12',
'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',
},
@@ -72,6 +72,7 @@ class CFDI(object):
self._impuestos_locales = False
self._donativo = False
self._ine = False
+ self._is_nomina = False
self.error = ''
def _now(self):
@@ -93,8 +94,6 @@ class CFDI(object):
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'))
@@ -108,35 +107,25 @@ class CFDI(object):
return xml
def _validate(self, datos):
- if datos['impuestos']['total_locales_trasladados'] or \
- datos['impuestos']['total_locales_retenciones']:
- self._impuestos_locales = True
+ if datos['impuestos']:
+ if datos['impuestos']['total_locales_trasladados'] or \
+ datos['impuestos']['total_locales_retenciones']:
+ self._impuestos_locales = True
if datos['donativo']:
self._donativo = True
- if 'ine' in datos['complementos']:
- self._ine = True
+ if datos['complementos']:
+ if 'ine' in datos['complementos']:
+ self._ine = True
if 'nomina' in datos:
+ self._is_nomina = True
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):
@@ -160,10 +149,16 @@ class CFDI(object):
if self._ine:
name = 'xmlns:{}'.format(SAT['ine']['prefix'])
attributes[name] = SAT['ine']['xmlns']
- schema_donativo = SAT['ine']['schema']
+ schema_ine = SAT['ine']['schema']
+
+ schema_nomina = ''
+ if self._nomina:
+ name = 'xmlns:{}'.format(SAT['nomina']['prefix'])
+ attributes[name] = SAT['nomina']['xmlns']
+ schema_nomina = SAT['nomina']['schema']
attributes['xsi:schemaLocation'] = self._sat_cfdi['schema'] + \
- schema_locales + schema_donativo +schema_ine
+ schema_locales + schema_donativo + schema_ine + schema_nomina
attributes.update(datos)
if not 'Version' in attributes:
@@ -263,6 +258,9 @@ class CFDI(object):
return
def _impuestos(self, datos):
+ if self._is_nomina:
+ return
+
if not datos:
node_name = '{}:Impuestos'.format(self._pre)
ET.SubElement(self._cfdi, node_name)
@@ -288,38 +286,62 @@ class CFDI(object):
return
def _nomina(self, datos):
- sat_nomina = SAT[NOMINA_ACTUAL]
- pre = sat_nomina['prefix']
- complemento = ET.SubElement(self._cfdi, '{}:Complemento'.format(self._pre))
+ pre = SAT['nomina']['prefix']
- emisor = datos.pop('Emisor', None)
- receptor = datos.pop('Receptor', None)
- percepciones = datos.pop('Percepciones', None)
- deducciones = datos.pop('Deducciones', None)
+ if self._complemento is None:
+ self._complemento = ET.SubElement(
+ self._cfdi, '{}:Complemento'.format(self._pre))
- attributes = {}
- attributes['xmlns:{}'.format(pre)] = sat_nomina['xmlns']
- attributes['xsi:schemaLocation'] = sat_nomina['schema']
- attributes.update(datos)
+ emisor = datos.pop('emisor', None)
+ receptor = datos.pop('receptor', None)
+ percepciones = datos.pop('percepciones', None)
+ deducciones = datos.pop('deducciones', None)
+ otros_pagos = datos.pop('otros_pagos', ())
+ incapacidades = datos.pop('incapacidades', ())
- if not 'Version' in attributes:
- attributes['Version'] = sat_nomina['version']
+ nomina = ET.SubElement(
+ self._complemento, '{}:Nomina'.format(pre), datos['nomina'])
- 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)
+ node = 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)
+ details = percepciones.pop('details', None)
+ hours_extra = percepciones.pop('hours_extra', None)
+ separacion = percepciones.pop('separacion', None)
+ if details:
+ node = ET.SubElement(nomina, '{}:Percepciones'.format(pre), percepciones)
+ for row in details:
+ nodep = ET.SubElement(node, '{}:Percepcion'.format(pre), row)
+ if row['TipoPercepcion'] == '019' and hours_extra:
+ for he in hours_extra:
+ ET.SubElement(nodep, '{}:HorasExtra'.format(pre), he)
+ hours_extra = None
+ if separacion:
+ ET.SubElement(node, '{}:SeparacionIndemnizacion'.format(pre), separacion)
+
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)
+ details = deducciones.pop('details', None)
+ if details:
+ deducciones = ET.SubElement(nomina, '{}:Deducciones'.format(pre), deducciones)
+ for row in details:
+ ET.SubElement(deducciones, '{}:Deduccion'.format(pre), row)
+
+ if otros_pagos:
+ node = ET.SubElement(nomina, '{}:OtrosPagos'.format(pre))
+ for row in otros_pagos:
+ subsidio = row.pop('subsidio', None)
+ subnode = ET.SubElement(node, '{}:OtroPago'.format(pre), row)
+ if subsidio:
+ ET.SubElement(subnode, '{}:SubsidioAlEmpleo'.format(pre), subsidio)
+
+ if incapacidades:
+ node = ET.SubElement(nomina, '{}:Incapacidades'.format(pre))
+ for row in incapacidades:
+ ET.SubElement(node, '{}:Incapacidad'.format(pre), row)
return
def _locales(self, datos):
diff --git a/source/app/controllers/main.py b/source/app/controllers/main.py
index 946c137..8ebe986 100644
--- a/source/app/controllers/main.py
+++ b/source/app/controllers/main.py
@@ -441,7 +441,7 @@ class AppNomina(object):
def on_delete(self, req, resp):
values = req.params
- if self._db.delete('nomina', values):
+ if self._db.delete('nomina', values['id']):
resp.status = falcon.HTTP_200
else:
resp.status = falcon.HTTP_204
diff --git a/source/app/controllers/pac.py b/source/app/controllers/pac.py
index 31acc20..fd6321d 100644
--- a/source/app/controllers/pac.py
+++ b/source/app/controllers/pac.py
@@ -174,8 +174,8 @@ class Finkok(object):
return
def _check_result(self, method, result):
- #~ print ('CODE', result.CodEstatus)
- #~ print ('INCIDENCIAS', result.Incidencias)
+ # ~ print ('CODE', result.CodEstatus)
+ # ~ print ('INCIDENCIAS', result.Incidencias)
self.message = ''
MSG = {
'OK': 'Comprobante timbrado satisfactoriamente',
@@ -184,8 +184,8 @@ class Finkok(object):
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'])
+ self.error += 'Error: {}\n{}\n{}'.format(
+ i['CodigoError'], i['MensajeIncidencia'], i['ExtraInfo'])
return ''
if method == 'timbra' and status in (MSG['OK'], MSG['307']):
diff --git a/source/app/models/db.py b/source/app/models/db.py
index 4d9e420..e0ea97c 100644
--- a/source/app/models/db.py
+++ b/source/app/models/db.py
@@ -17,6 +17,11 @@ class StorageEngine(object):
def get_nomina(self, values):
return main.CfdiNomina.get_by(values)
+ def nomina(self, values):
+ opt = values.pop('opt')
+ if opt == 'cancel':
+ return main.CfdiNomina.cancel(int(values['id']))
+
def empresa_agregar(self, values):
return main.empresa_agregar(values['alta_rfc'], False)
@@ -104,6 +109,9 @@ class StorageEngine(object):
def _get_filteryearsticket(self, values):
return main.Tickets.filter_years()
+ def _get_filteryearsnomina(self, values):
+ return main.CfdiNomina.filter_years()
+
def _get_cuentayears(self, values):
return main.CuentasBanco.get_years()
@@ -252,6 +260,8 @@ class StorageEngine(object):
return main.Configuracion.remove(id)
if table == 'employee':
return main.Empleados.remove(id)
+ if table == 'nomina':
+ return main.CfdiNomina.remove(id)
return False
def _get_client(self, values):
diff --git a/source/app/models/main.py b/source/app/models/main.py
index 5ee418d..646308f 100644
--- a/source/app/models/main.py
+++ b/source/app/models/main.py
@@ -22,6 +22,7 @@ from settings import log, VERSION, PATH_CP, COMPANIES, PRE, CURRENT_CFDI, \
FORMAT = '{0:.2f}'
+FORMAT3 = '{0:.3f}'
FORMAT_TAX = '{0:.4f}'
@@ -184,6 +185,8 @@ def get_doc(type_doc, id, rfc):
data, file_name = PreFacturas.get_pdf(id)
elif type_doc == 'tpdf':
data, file_name = Tickets.get_pdf(id)
+ elif type_doc == 'xmlnom':
+ data, file_name = CfdiNomina.get_xml(id)
return data, file_name, content_type
@@ -654,7 +657,6 @@ class SATRegimenes(BaseModel):
class Emisor(BaseModel):
rfc = TextField(unique=True)
- curp = TextField(unique=True)
nombre = TextField(default='')
nombre_comercial = TextField(default='')
calle = TextField(default='')
@@ -3625,8 +3627,8 @@ class Facturas(BaseModel):
comprobante['Serie'] = invoice.serie
if invoice.condiciones_pago:
comprobante['CondicionesDePago'] = invoice.condiciones_pago
- if invoice.descuento:
- comprobante['Descuento'] = invoice.descuento
+ # ~ if invoice.descuento:
+ # ~ comprobante['Descuento'] = invoice.descuento
comprobante['Folio'] = str(invoice.folio)
comprobante['Fecha'] = invoice.fecha.isoformat()[:19]
@@ -5367,7 +5369,7 @@ class Empleados(BaseModel):
es_activo = BooleanField(default=True)
es_extranjero = BooleanField(default=False)
fecha_alta = DateField(default=util.now)
- fecha_ingreso = DateField(default=util.now)
+ fecha_ingreso = DateField(null=True)
imss = TextField(default='')
tipo_contrato = ForeignKeyField(SATTipoContrato)
es_sindicalizado = BooleanField(default=False)
@@ -5399,7 +5401,8 @@ class Empleados(BaseModel):
data = row.copy()
data['nombre_completo'] = '{} {} {}'.format(
row['nombre'], row['paterno'], row['materno']).strip()
- data['fecha_ingreso'] = util.calc_to_date(row['fecha_ingreso'])
+ if row['fecha_ingreso']:
+ data['fecha_ingreso'] = util.calc_to_date(row['fecha_ingreso'])
data['tipo_contrato'] = SATTipoContrato.get_by_key(row['tipo_contrato'])
data['es_sindicalizado'] = sn.get(row['es_sindicalizado'].lower(), False)
data['tipo_jornada'] = SATTipoJornada.get_by_key(row['tipo_jornada'])
@@ -5501,7 +5504,7 @@ class CfdiNomina(BaseModel):
max_digits=20, decimal_places=6, auto_round=True, null=True)
xml = TextField(default='')
uuid = UUIDField(null=True)
- estatus = TextField(default='Guardada')
+ estatus = TextField(default='Guardado')
estatus_sat = TextField(default='Vigente')
regimen_fiscal = TextField(default='')
notas = TextField(default='')
@@ -5546,7 +5549,7 @@ class CfdiNomina(BaseModel):
if inicio is None:
new = 1
else:
- new += 1
+ new = inicio + 1
if folio > new:
new = folio
@@ -5797,6 +5800,11 @@ class CfdiNomina(BaseModel):
totals['total'] = round(totals['subtotal'] -
totals['descuento'], DECIMALES)
+ new_nomina['subtotal'] = totals['subtotal']
+ new_nomina['descuento'] = totals['descuento']
+ new_nomina['total'] = totals['total']
+ new_nomina['total_mn'] = totals['total']
+
with database_proxy.transaction():
obj = CfdiNomina.create(**new_nomina)
for row in new_percepciones:
@@ -5829,30 +5837,402 @@ class CfdiNomina(BaseModel):
msg = 'Nómina importada correctamente'
return {'ok': True, 'msg': msg}
- def _get(self):
- rows = (nomina
+ def _get(self, where=''):
+ if not where:
+ where = ((CfdiNomina.uuid.is_null(True)) & (CfdiNomina.cancelada==False))
+ rows = (CfdiNomina
.select(
- Nomina.id,
- Nomina.serie,
- Nomina.folio,
- Nomina.fecha,
- Nomina.status,
- Nomina.fecha_pago,
- Nomina.total,
- Nomina.empleado.nombre_completo
+ CfdiNomina.id,
+ CfdiNomina.serie,
+ CfdiNomina.folio,
+ CfdiNomina.fecha,
+ CfdiNomina.estatus,
+ CfdiNomina.fecha_pago,
+ CfdiNomina.total,
+ Empleados.nombre_completo.alias('empleado')
)
+ .where(where)
+ .join(Empleados)
+ .switch(CfdiNomina)
.dicts()
)
return {'ok': True, 'rows': tuple(rows)}
+ def _make_xml(self, cfdi, auth):
+ emisor = Emisor.select()[0]
+ empleado = cfdi.empleado
+ certificado = Certificado.select()[0]
+ totals = CfdiNominaTotales.select().where(CfdiNominaTotales.cfdi==cfdi)[0]
+
+ comprobante = {}
+ relacionados = {}
+ complementos = None
+
+ comprobante['Serie'] = cfdi.serie
+ comprobante['Folio'] = str(cfdi.folio)
+ comprobante['Fecha'] = cfdi.fecha.isoformat()[:19]
+ comprobante['FormaPago'] = cfdi.forma_pago
+ comprobante['NoCertificado'] = certificado.serie
+ comprobante['Certificado'] = certificado.cer_txt
+ comprobante['SubTotal'] = FORMAT.format(cfdi.subtotal)
+ comprobante['Moneda'] = cfdi.moneda
+ comprobante['Total'] = FORMAT.format(cfdi.total)
+ comprobante['TipoDeComprobante'] = cfdi.tipo_comprobante
+ comprobante['MetodoPago'] = cfdi.metodo_pago
+ comprobante['LugarExpedicion'] = cfdi.lugar_expedicion
+ if cfdi.descuento:
+ comprobante['Descuento'] = FORMAT.format(cfdi.descuento)
+
+ # ~ if invoice.tipo_relacion:
+ # ~ relacionados = {
+ # ~ 'tipo': invoice.tipo_relacion,
+ # ~ 'cfdis': FacturasRelacionadas.get_(invoice),
+ # ~ }
+
+ cfdi_emisor = {
+ 'Rfc': emisor.rfc,
+ 'Nombre': emisor.nombre,
+ 'RegimenFiscal': cfdi.regimen_fiscal,
+ }
+
+ receptor = {
+ 'Rfc': cfdi.empleado.rfc,
+ 'Nombre': cfdi.empleado.nombre_completo,
+ 'UsoCFDI': cfdi.uso_cfdi,
+ }
+
+ conceptos = []
+ rows = CfdiNominaDetalle.select().where(CfdiNominaDetalle.cfdi==cfdi)
+ for row in rows:
+ concepto = {
+ 'ClaveProdServ': row.clave_sat,
+ 'Cantidad': '1',
+ 'ClaveUnidad': row.clave_unidad,
+ 'Descripcion': row.descripcion,
+ 'ValorUnitario': FORMAT.format(row.valor_unitario),
+ 'Importe': FORMAT.format(row.importe),
+ }
+ if row.descuento:
+ concepto['Descuento'] = FORMAT.format(row.descuento)
+
+ conceptos.append(concepto)
+
+ nomina = {
+ 'Version': cfdi.version_nomina,
+ 'TipoNomina': cfdi.tipo_nomina.key,
+ 'FechaPago': str(cfdi.fecha_pago),
+ 'FechaInicialPago': str(cfdi.fecha_inicial_pago),
+ 'FechaFinalPago': str(cfdi.fecha_final_pago),
+ 'NumDiasPagados': FORMAT3.format(cfdi.dias_pagados),
+ }
+ if totals.total_percepciones:
+ nomina['TotalPercepciones'] = FORMAT.format(totals.total_percepciones)
+ if totals.total_deducciones:
+ nomina['TotalDeducciones'] = FORMAT.format(totals.total_deducciones)
+ if totals.total_otros_pagos:
+ nomina['TotalOtrosPagos'] = FORMAT.format(totals.total_otros_pagos)
+
+ nomina_emisor = {}
+ if emisor.curp:
+ nomina_emisor['Curp'] = emisor.curp
+ if emisor.registro_patronal:
+ nomina_emisor['RegistroPatronal'] = emisor.registro_patronal
+
+ nomina_receptor = {
+ 'Curp': empleado.curp,
+ 'TipoContrato': empleado.tipo_contrato.key,
+ 'Sindicalizado': {True: 'Si', False: 'No'}.get(empleado.es_sindicalizado),
+ 'TipoJornada': empleado.tipo_jornada.key,
+ 'TipoRegimen': empleado.tipo_regimen.key,
+ 'NumEmpleado': str(empleado.num_empleado),
+ 'RiesgoPuesto': empleado.riesgo_puesto.key,
+ 'PeriodicidadPago': empleado.periodicidad_pago.key,
+ 'ClaveEntFed': empleado.estado.key,
+ }
+
+ if empleado.imss:
+ nomina_receptor['NumSeguridadSocial'] = empleado.imss.replace('-', '')
+
+ if empleado.fecha_ingreso:
+ nomina_receptor['FechaInicioRelLaboral'] = str(empleado.fecha_ingreso)
+ days = util.get_days(empleado.fecha_ingreso, cfdi.fecha_final_pago)
+ weeks = days // 7
+ if weeks:
+ ant = 'P{}W'.format(weeks)
+ else:
+ ant = 'P{}D'.format(days)
+ nomina_receptor['Antigüedad'] = ant
+
+ if empleado.puesto:
+ if empleado.puesto.departamento:
+ nomina_receptor['Departamento'] = empleado.puesto.departamento.nombre
+ nomina_receptor['Puesto'] = empleado.puesto.nombre
+
+ if empleado.clabe:
+ nomina_receptor['CuentaBancaria'] = empleado.clabe
+ elif empleado.cuenta_bancaria:
+ nomina_receptor['CuentaBancaria'] = empleado.cuenta_bancaria
+ nomina_receptor['Banco'] = empleado.banco.key
+
+ if empleado.salario_base:
+ nomina_receptor['SalarioBaseCotApor'] = FORMAT.format(empleado.salario_base)
+ if empleado.salario_diario:
+ nomina_receptor['SalarioDiarioIntegrado'] = FORMAT.format(empleado.salario_diario)
+
+ percepciones = {
+ 'TotalSueldos': FORMAT.format(totals.total_sueldos),
+ 'TotalGravado': FORMAT.format(totals.total_gravado),
+ 'TotalExento': FORMAT.format(totals.total_exento),
+ }
+ if totals.total_separacion:
+ percepciones['TotalSeparacionIndemnizacion'] = FORMAT.format(totals.total_separacion)
+ if totals.total_jubilacion:
+ percepciones['TotalJubilacionPensionRetiro'] = FORMAT.format(totals.total_jubilacion)
+
+ rows = CfdiNominaPercepciones.select().where(
+ CfdiNominaPercepciones.cfdi==cfdi)
+ details = []
+ for row in rows:
+ concepto = row.concepto or row.tipo_percepcion.nombre or row.tipo_percepcion.name
+ p = {
+ 'TipoPercepcion': row.tipo_percepcion.key,
+ 'Clave': row.tipo_percepcion.clave or row.tipo_percepcion.key,
+ 'Concepto': concepto[:100],
+ 'ImporteGravado': FORMAT.format(row.importe_gravado),
+ 'ImporteExento': FORMAT.format(row.importe_exento),
+ }
+ details.append(p)
+ percepciones['details'] = details
+
+ rows = CfdiNominaHorasExtra.select().where(CfdiNominaHorasExtra.cfdi==cfdi)
+ details = []
+ for row in rows:
+ n = {
+ 'Dias': str(row.dias),
+ 'TipoHoras': row.tipos_horas.key,
+ 'HorasExtra': str(row.horas_extra),
+ 'ImportePagado': FORMAT.format(row.importe_pagado),
+ }
+ details.append(n)
+ percepciones['hours_extra'] = details
+
+ deducciones = {
+ 'TotalOtrasDeducciones': FORMAT.format(totals.total_otras_deducciones),
+ 'TotalImpuestosRetenidos': FORMAT.format(totals.total_retenciones),
+ }
+ rows = CfdiNominaDeducciones.select().where(CfdiNominaDeducciones.cfdi==cfdi)
+ details = []
+ for row in rows:
+ concepto = row.concepto or row.tipo_deduccion.nombre or row.tipo_deduccion.name
+ p = {
+ 'TipoDeduccion': row.tipo_deduccion.key,
+ 'Clave': row.tipo_deduccion.clave or row.tipo_deduccion.key,
+ 'Concepto': concepto[:100],
+ 'Importe': FORMAT.format(row.importe),
+ }
+ details.append(p)
+ deducciones['details'] = details
+
+ rows = CfdiNominaOtroPago.select().where(CfdiNominaOtroPago.cfdi==cfdi)
+ otros_pagos = []
+ for row in rows:
+ concepto = row.concepto or row.tipo_otro_pago.nombre or row.tipo_otro_pago.name
+ p = {
+ 'TipoOtroPago': row.tipo_otro_pago.key,
+ 'Clave': row.tipo_otro_pago.clave or row.tipo_otro_pago.key,
+ 'Concepto': concepto[:100],
+ 'Importe': FORMAT.format(row.importe),
+ }
+ if row.tipo_otro_pago.key == '002' and row.subsidio_causado:
+ p['subsidio'] = {
+ 'SubsidioCausado': FORMAT.format(row.subsidio_causado)
+ }
+ otros_pagos.append(p)
+
+ rows = CfdiNominaIncapacidad.select().where(CfdiNominaIncapacidad.cfdi==cfdi)
+ incapacidades = []
+ for row in rows:
+ n = {
+ 'DiasIncapacidad': str(row.dias),
+ 'TipoIncapacidad': row.tipo.key,
+ 'ImporteMonetario': FORMAT.format(row.importe),
+ }
+ incapacidades.append(n)
+
+ nomina = {
+ 'nomina': nomina,
+ 'emisor': nomina_emisor,
+ 'receptor': nomina_receptor,
+ 'percepciones': percepciones,
+ 'deducciones': deducciones,
+ 'otros_pagos': otros_pagos,
+ 'incapacidades': incapacidades,
+ }
+
+ data = {
+ 'comprobante': comprobante,
+ 'relacionados': relacionados,
+ 'emisor': cfdi_emisor,
+ 'receptor': receptor,
+ 'conceptos': conceptos,
+ 'complementos': complementos,
+ 'nomina': nomina,
+ 'impuestos': {},
+ 'donativo': {},
+ }
+ return util.make_xml(data, certificado, auth)
+
+ def _stamp_id(self, id):
+ auth = Emisor.get_auth()
+ obj = CfdiNomina.get(CfdiNomina.id==id)
+ obj.xml = self._make_xml(self, obj, auth)
+ obj.estatus = 'Generado'
+ obj.save()
+
+ result = util.timbra_xml(obj.xml, auth)
+ # ~ print (result)
+ if result['ok']:
+ obj.xml = result['xml']
+ obj.uuid = result['uuid']
+ obj.fecha_timbrado = result['fecha']
+ obj.estatus = 'Timbrado'
+ obj.error = ''
+ obj.save()
+ # ~ cls._sync(cls, id, auth)
+ else:
+ msg = result['error']
+ obj.estatus = 'Error'
+ obj.error = msg
+ obj.save()
+
+
+ return result['ok'], obj.error
+
+ def _stamp(self):
+ where = ((CfdiNomina.uuid.is_null(True)) & (CfdiNomina.cancelada==False))
+ rows = CfdiNomina.select().where(where)
+ util.log_file('nomina', kill=True)
+ msg_error = ''
+ ok_stamp = 0
+ for row in rows:
+ msg = 'Timbrando el recibo: {}-{}'.format(row.serie, row.folio)
+ util.log_file('nomina', msg)
+ result, msg = self._stamp_id(self, row.id)
+ if result:
+ msg = 'Recibo: {}-{}, timbrado correctamente'.format(row.serie, row.folio)
+ ok_stamp += 1
+ else:
+ msg_error = msg
+ break
+
+ ok = False
+ if ok_stamp:
+ msg = 'Se timbraron {} recibos'.format(ok_stamp)
+ ok = True
+ error = False
+ if msg_error:
+ error = True
+
+ return {'ok': ok, 'msg_ok': msg, 'error': error, 'msg_error': msg_error}
+
@classmethod
def get_by(cls, values):
- if not 'opt' in values:
+ if not values:
return cls._get(cls)
+ if values['opt'] == 'dates':
+ dates = util.loads(values['range'])
+ filters = CfdiNomina.fecha.between(
+ util.get_date(dates['start']),
+ util.get_date(dates['end'], True)
+ )
+ return cls._get(cls, filters)
+
+ if values['opt'] == 'yearmonth':
+ if values['year'] == '-1':
+ fy = (CfdiNomina.fecha.year > 0)
+ else:
+ fy = (CfdiNomina.fecha.year == int(values['year']))
+ if values['month'] == '-1':
+ fm = (CfdiNomina.fecha.month > 0)
+ else:
+ fm = (CfdiNomina.fecha.month == int(values['month']))
+ filters = (fy & fm)
+ return cls._get(cls, filters)
+
if values['opt'] == 'import':
return cls._import(cls)
+ if values['opt'] == 'stamp':
+ return cls._stamp(cls)
+
+ @classmethod
+ def remove(cls, id):
+ obj = CfdiNomina.get(CfdiNomina.id==id)
+ if obj.uuid:
+ return False
+
+ q = CfdiNominaDetalle.delete().where(CfdiNominaDetalle.cfdi==obj)
+ q.execute()
+ q = CfdiNominaTotales.delete().where(CfdiNominaTotales.cfdi==obj)
+ q.execute()
+ q = CfdiNominaPercepciones.delete().where(CfdiNominaPercepciones.cfdi==obj)
+ q.execute()
+ q = CfdiNominaDeducciones.delete().where(CfdiNominaDeducciones.cfdi==obj)
+ q.execute()
+ q = CfdiNominaOtroPago.delete().where(CfdiNominaOtroPago.cfdi==obj)
+ q.execute()
+ q = CfdiNominaHorasExtra.delete().where(CfdiNominaHorasExtra.cfdi==obj)
+ q.execute()
+ q = CfdiNominaIncapacidad.delete().where(CfdiNominaIncapacidad.cfdi==obj)
+ q.execute()
+
+ return bool(obj.delete_instance())
+
+ @classmethod
+ def get_xml(cls, id):
+ obj = CfdiNomina.get(CfdiNomina.id==id)
+ name = '{}{}_{}.xml'.format(obj.serie, obj.folio, obj.empleado.rfc)
+ # ~ cls._sync_xml(cls, obj)
+ return obj.xml, name
+
+ @classmethod
+ def filter_years(cls):
+ data = [{'id': -1, 'value': 'Todos'}]
+ rows = (CfdiNomina
+ .select(CfdiNomina.fecha.year.alias('year'))
+ .group_by(CfdiNomina.fecha.year)
+ .order_by(CfdiNomina.fecha.year)
+ )
+ if not rows is None:
+ data += [{'id': int(r.year), 'value': int(r.year)} for r in rows]
+ return tuple(data)
+
+ @classmethod
+ def cancel(cls, id):
+ msg = 'Recibo cancelado correctamente'
+ auth = Emisor.get_auth()
+ certificado = Certificado.select()[0]
+ obj = CfdiNomina.get(CfdiNomina.id==id)
+
+ if obj.uuid is None:
+ msg = 'Solo se pueden cancelar recibos timbrados'
+ return {'ok': False, 'msg': msg}
+
+ data, result = util.cancel_xml(auth, obj.uuid, certificado)
+ if data['ok']:
+ data['msg'] = 'Recibo cancelado correctamente'
+ data['row']['estatus'] = 'Cancelado'
+ obj.estatus = data['row']['estatus']
+ obj.error = ''
+ obj.cancelada = True
+ obj.fecha_cancelacion = result['Fecha']
+ obj.acuse = result['Acuse']
+ else:
+ obj.error = data['msg']
+ obj.save()
+ return data
+
class CfdiNominaDetalle(BaseModel):
cfdi = ForeignKeyField(CfdiNomina)
diff --git a/source/static/js/controller/nomina.js b/source/static/js/controller/nomina.js
index c09668a..2416f75 100644
--- a/source/static/js/controller/nomina.js
+++ b/source/static/js/controller/nomina.js
@@ -9,13 +9,22 @@ var nomina_controllers = {
$$('cmd_close_empleados').attachEvent('onItemClick', cmd_close_empleados_click)
$$('cmd_delete_empleado').attachEvent('onItemClick', cmd_delete_empleado_click)
$$('cmd_import_empleados').attachEvent('onItemClick', cmd_import_empleados_click)
+ $$('cmd_nomina_without_stamp').attachEvent('onItemClick', cmd_nomina_without_stamp_click)
+ $$('cmd_nomina_delete').attachEvent('onItemClick', cmd_nomina_delete_click)
+ $$('cmd_nomina_timbrar').attachEvent('onItemClick', cmd_nomina_timbrar_click)
+ $$('cmd_nomina_cancel').attachEvent('onItemClick', cmd_nomina_cancel_click)
+ $$('grid_nomina').attachEvent('onItemClick', grid_nomina_click)
+ $$('filter_year_nomina').attachEvent('onChange', filter_year_nomina_change)
+ $$('filter_month_nomina').attachEvent('onChange', filter_month_nomina_change)
+ $$('filter_dates_nomina').attachEvent('onChange', filter_dates_nomina_change)
webix.extend($$('grid_nomina'), webix.ProgressBar)
}
}
function default_config_nomina(){
-
+ current_dates_nomina()
+ get_nomina()
}
@@ -39,8 +48,26 @@ function current_dates_nomina(){
}
-function get_nomina(){
+function get_nomina(filters){
+ var grid = $$('grid_nomina')
+ grid.showProgress({type: 'icon'})
+
+ webix.ajax().get('/nomina', filters, {
+ error: function(text, data, xhr) {
+ msg = 'Error al consultar'
+ msg_error(msg)
+ },
+ success: function(text, data, xhr) {
+ var values = data.json();
+ if (values.ok){
+ grid.clearAll();
+ grid.parse(values.rows, 'json');
+ }else{
+ msg_error(values.msg)
+ }
+ }
+ })
}
@@ -259,4 +286,186 @@ function cmd_delete_empleado_click(){
}
}
})
+}
+
+
+function cmd_nomina_without_stamp_click(){
+ get_nomina()
+}
+
+
+function cmd_nomina_delete_click(){
+ var row = $$('grid_nomina').getSelectedItem()
+
+ if (row == undefined){
+ msg = 'Selecciona un registro'
+ msg_error(msg)
+ return
+ }
+
+ msg = '¿Estás seguro de eliminar el registro?
'
+ msg += row['empleado'] + ' (' + row['fecha_pago'] + ')'
+ msg += '
ESTA ACCIÓN NO SE PUEDE DESHACER
'
+ webix.confirm({
+ title: 'Eliminar Nomina',
+ ok: 'Si',
+ cancel: 'No',
+ type: 'confirm-error',
+ text: msg,
+ callback:function(result){
+ if (result){
+ delete_nomina(row['id'])
+ }
+ }
+ })
+}
+
+
+function delete_nomina(id){
+ webix.ajax().del('/nomina', {id: id}, function(text, xml, xhr){
+ var msg = 'Registro eliminado correctamente'
+ if (xhr.status == 200){
+ $$('grid_nomina').remove(id);
+ msg_ok(msg)
+ } else {
+ msg = 'No se pudo eliminar.'
+ msg_error(msg)
+ }
+ })
+}
+
+
+function cmd_nomina_timbrar_click(){
+ get_nomina()
+
+ msg = 'Se enviarán a timbrar todos los recibos sin timbrar
'
+ msg += '¿Estás seguro de continuar?
'
+ webix.confirm({
+ title: 'Enviar a timbrar',
+ ok: 'Si',
+ cancel: 'No',
+ type: 'confirm-error',
+ text: msg,
+ callback:function(result){
+ if (result){
+ timbrar_nomina()
+ }
+ }
+ })
+}
+
+
+function timbrar_nomina(){
+ webix.ajax().get('/nomina', {opt: 'stamp'}, {
+ error: function(text, data, xhr) {
+ msg = 'Error al timbrar'
+ msg_error(msg)
+ },
+ success: function(text, data, xhr) {
+ var values = data.json();
+ if(values.ok){
+ get_nomina()
+ msg_ok(values.msg_ok)
+ }
+ if(values.error){
+ webix.alert({
+ title: 'Error al Timbrar',
+ text: values.msg_error,
+ type: 'alert-error'
+ })
+ }
+ }
+ })
+}
+
+
+function grid_nomina_click(id, e, node){
+ var row = this.getItem(id)
+
+ if(id.column == 'xml'){
+ location = '/doc/xmlnom/' + row.id
+ }else if(id.column == 'pdf'){
+ //~ get_momina_pdf(row.id)
+ //~ }else if(id.column == 'email'){
+ //~ enviar_correo(row)
+ }
+
+}
+
+
+function filter_year_nomina_change(nv, ov){
+ var fm = $$('filter_month_nomina')
+ filters = {'opt': 'yearmonth', 'year': nv, 'month': fm.getValue()}
+ get_nomina(filters)
+}
+
+
+function filter_month_nomina_change(nv, ov){
+ var fy = $$('filter_year_nomina')
+ filters = {'opt': 'yearmonth', 'year': fy.getValue(), 'month': nv}
+ get_nomina(filters)
+}
+
+
+function filter_dates_nomina_change(range){
+ if(range.start != null && range.end != null){
+ filters = {'opt': 'dates', 'range': range}
+ get_nomina(filters)
+ }
+}
+
+
+function cmd_nomina_cancel_click(){
+ var row = $$('grid_nomina').getSelectedItem()
+
+ if(row == undefined){
+ msg = 'Selecciona un registro'
+ msg_error(msg)
+ return
+ }
+ if(row['estatus'] != 'Timbrado'){
+ msg = 'Solo se pueden cancelar recibos timbrados'
+ msg_error(msg)
+ return
+ }
+
+ msg = '¿Estás seguro de cancelar el recibo?
'
+ msg += row['empleado'] + ' (' + row['serie'] + '-' + row['folio'] + ')'
+ msg += '
ESTA ACCIÓN NO SE PUEDE DESHACER
'
+ webix.confirm({
+ title: 'Cancelar Nomina',
+ ok: 'Si',
+ cancel: 'No',
+ type: 'confirm-error',
+ text: msg,
+ callback:function(result){
+ if (result){
+ cancel_nomina(row['id'])
+ }
+ }
+ })
+}
+
+
+function cancel_nomina(id){
+ var grid = $$('grid_nomina')
+ var data = new Object()
+ data['opt'] = 'cancel'
+ data['id'] = id
+
+ webix.ajax().sync().post('nomina', data, {
+ error:function(text, data, XmlHttpRequest){
+ msg = 'Ocurrio un error, consulta a soporte técnico'
+ msg_error(msg)
+ },
+ success:function(text, data, XmlHttpRequest){
+ values = data.json();
+ if(values.ok){
+ grid.updateItem(id, values.row)
+ msg_ok(values.msg)
+ }else{
+ msg_error(values.msg)
+ }
+ }
+ })
}
\ No newline at end of file
diff --git a/source/static/js/ui/nomina.js b/source/static/js/ui/nomina.js
index 5cbe1a2..9dca50f 100644
--- a/source/static/js/ui/nomina.js
+++ b/source/static/js/ui/nomina.js
@@ -31,6 +31,9 @@ var toolbar_nomina_filter = [
labelAlign: 'right', labelWidth: 50, width: 200, options: months},
{view: 'daterangepicker', id: 'filter_dates_nomina', label: 'Fechas',
labelAlign: 'right', width: 300},
+ {},
+ {view: 'button', id: 'cmd_nomina_without_stamp', label: 'Sin Timbrar',
+ type: 'iconButton', autowidth: true, icon: 'filter'},
]
@@ -44,12 +47,14 @@ var grid_cols_nomina = [
{id: "fecha", header: ["Fecha y Hora"], adjust: "data", sort: "string"},
{id: "estatus", header: ["Estatus", {content: "selectFilter"}],
adjust: "data", sort:"string"},
- {id: "fecha_pago", header: ["Fecha de Pago"], adjust: "data", sort: "string"},
+ {id: 'fecha_pago', header: ['Fecha de Pago', {content: 'selectFilter'}],
+ adjust: 'data', sort: 'string'},
{id: 'total', header: ['Total', {content: 'numberFilter'}], width: 150,
sort: 'int', format: webix.i18n.priceFormat, css: 'right',
footer: {content: 'summActive', css: 'right'}},
{id: "empleado", header: ["Empleado", {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')},
]